├── profile.json
├── SpaceMono-Regular.ttf
├── .datignore
├── dat.json
├── index.html
├── playlists
└── playlist.json
├── README.md
├── package.json
├── tutorial.md
├── .gitignore
├── links
└── style.css
└── app.js
/profile.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "listener",
3 | "following": []
4 | }
--------------------------------------------------------------------------------
/SpaceMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cblgh/datradio/HEAD/SpaceMono-Regular.ttf
--------------------------------------------------------------------------------
/.datignore:
--------------------------------------------------------------------------------
1 | *.sw[op]
2 | .DS*
3 | .idea
4 | *.spec.js
5 | jest.config.js
6 | __snapshots__
7 | node_modules
--------------------------------------------------------------------------------
/dat.json:
--------------------------------------------------------------------------------
1 | {
2 | "url": "dat://47fe02a7bc5022f755d2421a2f7b9af441286ee4120b1a8186de4411b9c68f1b/",
3 | "title": "datradio",
4 | "fallback_page": "index.html",
5 | "description": "it's time"
6 | }
7 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | datradio
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/playlists/playlist.json:
--------------------------------------------------------------------------------
1 | {
2 | "archives": [
3 | "dat://7b8c5b60a64282ece25649363dc33d26d051d6265a993de70d5355cc1af7b95a"
4 | ],
5 | "tracks": [
6 | "dat://7b8c5b60a64282ece25649363dc33d26d051d6265a993de70d5355cc1af7b95a/weekly-beats-week-15-italo-easter-cblgh-2018.mp3",
7 | "dat://7b8c5b60a64282ece25649363dc33d26d051d6265a993de70d5355cc1af7b95a/weekly-beats-week-14-cblgh-sunless-society.mp3"
8 | ],
9 | "removed": [],
10 | "description": "the latest weekly beats",
11 | "profile": {
12 | "bg": "black",
13 | "color": "#f2f2f2",
14 | "archive": "dat://7b8c5b60a64282ece25649363dc33d26d051d6265a993de70d5355cc1af7b95a"
15 | }
16 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # datradio
2 | here we go again
3 |
4 | 
5 |
6 |
7 | # Setup
8 | * Clone the repo
9 | * Navigate to the folder
10 | * Run `npm install && npm run build`
11 | * Create a new site in Beaker and import the folder containing the cloned project
12 | * Make sure to publish the revisions for the site in Beaker, using their workspace tools (accessible via Library)
13 | * Check out the [tutorial](https://github.com/cblgh/datradio/blob/master/tutorial.md) for more instructions and specifics
14 |
15 |
16 | note: this is extremely wip and lacks any kind of onboarding atm
17 |
18 | but hey see the [demo](https://www.youtube.com/watch?v=-0cgl6okmUs&feature=youtu.be&t=1670) from a talk i held, or venture into the [code](https://github.com/cblgh/datradio/blob/master/app.js) if you're brave
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "datradio",
3 | "version": "0.0.1",
4 | "description": "",
5 | "main": "app.js",
6 | "dependencies": {
7 | "choo": "^6.7.0",
8 | "choo-devtools": "^2.3.3",
9 | "nanocomponent": "^6.5.1"
10 | },
11 | "devDependencies": {
12 | "browser-pack-flat": "^3.0.3",
13 | "browserify": "^14.4.0",
14 | "browserify-nodent": "^1.0.22",
15 | "common-shakeify": "^0.4.4",
16 | "css-extract": "^1.2.0",
17 | "sheetify": "^7.0.0",
18 | "sheetify-cssnext": "^1.0.7",
19 | "uglifyify": "^4.0.4",
20 | "watchify": "^3.9.0"
21 | },
22 | "scripts": {
23 | "build": "date && browserify -t browserify-nodent -t [ sheetify -u sheetify-cssnext ] -p [ css-extract -o bundle.css ] -p common-shakeify -p browser-pack-flat/plugin app.js -g uglifyify > bundle.js"
24 | },
25 | "keywords": [
26 | "dat",
27 | "datradio",
28 | "beaker",
29 | "merveilles",
30 | "piratradio"
31 | ],
32 | "author": "cblgh",
33 | "license": "MIT"
34 | }
35 |
--------------------------------------------------------------------------------
/tutorial.md:
--------------------------------------------------------------------------------
1 | # Get started
2 | * Install [Beaker Browser](https://beakerbrowser.com/install/). You will need Beaker at least pre-release of 0.8
3 | * Visit dat://31efd7c43603b57d18d0dcc4e2a32bf5cae08ab5930071e4da3513dbc4c60f5f/ and make an **editable copy**
4 | *(psst. click on the **three dots** in the URL bar of Beaker to create an editable copy)*
5 | * **Create dat archives and fill them with music.** (You create the archives with Beaker, outside of datradio).
6 | Paste the url of the archive into datradio's terminal, titled *i love tracks*.
7 | The tracks have now been added, and the archive is now monitored for future changes.
8 | * If you create an info.txt and place it beside your tracks, that will be used to provide extra info about
9 | the tracks in that dat archive. This info is viewable by clicking **INFO** beside any track in a playlist.
10 | * You can easily add and remove tracks using something like kodedninja's ntain:
11 | Create an editable copy of dat://ntain-kodedninja.hashbase.io/ in beaker and visit
12 | https://github.com/kodedninja/ntain for more info.
13 | Note: if you use ntain, you will have to add the dat archive as follows: dat://7331..1337**/files** (because that is where ntain puts its files).
14 | * Check out the commands in the right sidebar
15 | * To ensure the tracks in your playlist are available offline, you will currently have to manually seed
16 | them in Beaker. Future versions of Beaker will probably add an API that allows datradio to seed them
17 | automatically for you, but that is not the case right now.
18 | So visit the archives in the left sidebar under *archives in playlist* and make sure they are seeded if
19 | you want to listen offline.
20 | *(psst. click on the **weird triangle-with-dots** thing in the url bar to **make sure things are
21 | seeded**)*
22 |
23 |
24 |
25 | Let [cblgh](https://twitter.com/cblgh) know about any issues
26 | Report issues or add patches at https://github.com/cblgh/datradio
27 |
28 | <3 p2p
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /playlists/
2 | #################
3 | ## cblgh town
4 | #################
5 | *.db
6 | #################
7 | ## Pirate ignore
8 | #################
9 | node_modules/
10 | *.css
11 | *.vim
12 | *bundle*.js
13 | *.bat
14 |
15 | #################
16 | ## Eclipse
17 | #################
18 |
19 | *.pydevproject
20 | .project
21 | .metadata
22 | bin/
23 | tmp/
24 | *.tmp
25 | *.bak
26 | *.swp
27 | *~.nib
28 | local.properties
29 | .classpath
30 | .settings/
31 | .loadpath
32 |
33 | # External tool builders
34 | .externalToolBuilders/
35 |
36 | # Locally stored "Eclipse launch configurations"
37 | *.launch
38 |
39 | # CDT-specific
40 | .cproject
41 |
42 | # PDT-specific
43 | .buildpath
44 |
45 |
46 | #################
47 | ## Visual Studio
48 | #################
49 |
50 | ## Ignore Visual Studio temporary files, build results, and
51 | ## files generated by popular Visual Studio add-ons.
52 |
53 | # User-specific files
54 | *.suo
55 | *.user
56 | *.sln.docstates
57 |
58 | # Build results
59 |
60 | [Dd]ebug/
61 | [Rr]elease/
62 | x64/
63 | build/
64 | [Bb]in/
65 | [Oo]bj/
66 |
67 | # MSTest test Results
68 | [Tt]est[Rr]esult*/
69 | [Bb]uild[Ll]og.*
70 |
71 | *_i.c
72 | *_p.c
73 | *.ilk
74 | *.meta
75 | *.obj
76 | *.pch
77 | *.pdb
78 | *.pgc
79 | *.pgd
80 | *.rsp
81 | *.sbr
82 | *.tlb
83 | *.tli
84 | *.tlh
85 | *.tmp
86 | *.tmp_proj
87 | *.log
88 | *.vspscc
89 | *.vssscc
90 | .builds
91 | *.pidb
92 | *.log
93 | *.scc
94 |
95 | # Visual C++ cache files
96 | ipch/
97 | *.aps
98 | *.ncb
99 | *.opensdf
100 | *.sdf
101 | *.cachefile
102 |
103 | # Visual Studio profiler
104 | *.psess
105 | *.vsp
106 | *.vspx
107 |
108 | # Guidance Automation Toolkit
109 | *.gpState
110 |
111 | # ReSharper is a .NET coding add-in
112 | _ReSharper*/
113 | *.[Rr]e[Ss]harper
114 |
115 | # TeamCity is a build add-in
116 | _TeamCity*
117 |
118 | # DotCover is a Code Coverage Tool
119 | *.dotCover
120 |
121 | # NCrunch
122 | *.ncrunch*
123 | .*crunch*.local.xml
124 |
125 | # Installshield output folder
126 | [Ee]xpress/
127 |
128 | # DocProject is a documentation generator add-in
129 | DocProject/buildhelp/
130 | DocProject/Help/*.HxT
131 | DocProject/Help/*.HxC
132 | DocProject/Help/*.hhc
133 | DocProject/Help/*.hhk
134 | DocProject/Help/*.hhp
135 | DocProject/Help/Html2
136 | DocProject/Help/html
137 |
138 | # Click-Once directory
139 | publish/
140 |
141 | # Publish Web Output
142 | *.Publish.xml
143 | *.pubxml
144 |
145 | # NuGet Packages Directory
146 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line
147 | #packages/
148 |
149 | # Windows Azure Build Output
150 | csx
151 | *.build.csdef
152 |
153 | # Windows Store app package directory
154 | AppPackages/
155 |
156 | # Others
157 | sql/
158 | *.Cache
159 | ClientBin/
160 | [Ss]tyle[Cc]op.*
161 | ~$*
162 | *~
163 | *.dbmdl
164 | *.[Pp]ublish.xml
165 | *.pfx
166 | *.publishsettings
167 |
168 | # RIA/Silverlight projects
169 | Generated_Code/
170 |
171 | # Backup & report files from converting an old project file to a newer
172 | # Visual Studio version. Backup files are not needed, because we have git ;-)
173 | _UpgradeReport_Files/
174 | Backup*/
175 | UpgradeLog*.XML
176 | UpgradeLog*.htm
177 |
178 | # SQL Server files
179 | App_Data/*.mdf
180 | App_Data/*.ldf
181 |
182 | #############
183 | ## Windows detritus
184 | #############
185 |
186 | # Windows image file caches
187 | Thumbs.db
188 | ehthumbs.db
189 |
190 | # Folder config file
191 | Desktop.ini
192 |
193 | # Recycle Bin used on file shares
194 | $RECYCLE.BIN/
195 |
196 | # Mac crap
197 | .DS_Store
198 |
199 |
200 | #############
201 | ## Python
202 | #############
203 |
204 | *.py[co]
205 |
206 | # Packages
207 | *.egg
208 | *.egg-info
209 | dist/
210 | build/
211 | eggs/
212 | parts/
213 | var/
214 | sdist/
215 | develop-eggs/
216 | .installed.cfg
217 |
218 | # Installer logs
219 | pip-log.txt
220 |
221 | # Unit test / coverage reports
222 | .coverage
223 | .tox
224 |
225 | #Translations
226 | *.mo
227 |
228 | #Mr Developer
229 | .mr.developer.cfg
230 |
--------------------------------------------------------------------------------
/links/style.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "SpaceMono";
3 | src: url("/SpaceMono-Regular.ttf");
4 | }
5 |
6 | body {
7 | background-color: black;
8 | color: #f2f2f2;
9 | font-family: "SpaceMono", Arial;
10 | margin-top: 30px;
11 | }
12 |
13 | @keyframes fade {
14 | 0% {opacity: 1;}
15 | 100% {opacity: 0.5;}
16 | }
17 |
18 | .drag-fade {
19 | animation-name: fade;
20 | animation-duration: 0.4s;
21 | animation-fill-mode: forwards;
22 | }
23 |
24 | li {
25 | list-style: none;
26 | }
27 |
28 | /* MODAL STUFF */
29 | #info-modal {
30 | grid-area: modal;
31 | display: grid;
32 | background: inherit;
33 | width: 500px;
34 | /* height: 500px; */
35 |
36 | grid-template-rows: 0px 0px 40px 60px 1fr;
37 |
38 | grid-template-areas:
39 | "modal-close"
40 | "modal-header"
41 | "modal-title"
42 | "modal-archive"
43 | "modal-text"
44 | }
45 |
46 | #info-close {
47 | grid-area: modal-close;
48 | justify-self: right;
49 | /* text-align: right; */
50 | cursor: pointer;
51 | }
52 |
53 | #info-title {
54 | grid-area: modal-title;
55 | font-size: 1.1em;
56 | margin-bottom: 15px;
57 | }
58 |
59 |
60 | #info-archive {
61 | grid-area: modal-archive;
62 | display: inline-block;
63 | }
64 |
65 | #info-text {
66 | grid-area: modal-text;
67 | }
68 |
69 | #info-text pre {
70 | margin: 0;
71 | }
72 |
73 |
74 | /* TRACK STUFF */
75 | .track-title {
76 | display: inline-block;
77 | }
78 |
79 | .track-link {
80 | display: inline-block;
81 | margin-right: 5px;
82 | cursor: pointer;
83 | }
84 |
85 | .track-link:hover {
86 | opacity: 0.5 !important;
87 | }
88 |
89 | #tracks li:hover {
90 | cursor: pointer;
91 | opacity: 0.6 !important;
92 | transition: opacity 0.4;
93 | }
94 |
95 | a:visited {
96 | color: inherit;
97 | text-decoration: none;
98 | }
99 |
100 | a, #fork-url {
101 | opacity: 1;
102 | color: inherit;
103 | text-decoration: none;
104 | transition: opacity 0.4;
105 | }
106 |
107 | a:hover, #fork-url:hover {
108 | cursor: pointer;
109 | opacity: 0.6 !important;
110 | transition: opacity 0.4;
111 | border-bottom-style: solid;
112 | }
113 |
114 | .help-container, .hotkey-container {
115 | padding-bottom: 15px;
116 | font-size: 9pt;
117 | }
118 |
119 | .help-container:hover {
120 | cursor: pointer;
121 | opacity: 0.8;
122 | }
123 |
124 | .hotkey-container {
125 | padding-bottom: 0;
126 | }
127 |
128 | .help-cmd:before {
129 | content: ".";
130 | }
131 |
132 | .help-cmd, .help-value, .help-hotkey {
133 | display: inline-block;
134 | padding-right: 10px;
135 | }
136 |
137 | .help-value {
138 | font-style: italic;
139 | }
140 |
141 | .help-desc {
142 | padding-left: 10px;
143 | }
144 |
145 | #tracks {
146 | grid-area: tracks;
147 | padding: 0;
148 | }
149 |
150 | #playlists {
151 | grid-area: playlists;
152 | }
153 |
154 | #terminal {
155 | grid-area: terminal;
156 | font-size: 12pt;
157 | color: #c4c4c4;
158 | background-color: #1d1d1d;
159 | border: none;
160 | padding: 7px;
161 | outline: 0;
162 | }
163 |
164 | #title {
165 | grid-area: title;
166 | margin-bottom: 5px;
167 | }
168 |
169 | #player {
170 | grid-area: player;
171 | display: none;
172 | }
173 |
174 | #toggle {
175 | grid-area: toggle;
176 | }
177 |
178 | #time {
179 | grid-area: time;
180 | justify-self: end;
181 | }
182 |
183 | #commands {
184 | grid-area: commands;
185 | justify-self: end;
186 | }
187 |
188 | .playing:before {
189 | content: "► ";
190 | }
191 |
192 | .playing {
193 | margin-left: -20px;
194 | }
195 |
196 | .paused:before {
197 | content: "■ ";
198 | }
199 |
200 | .paused {
201 | margin-left: -15px;
202 | }
203 |
204 | #description {
205 | grid-area: description;
206 | }
207 |
208 | #fork-url {
209 | display: block;
210 | position: absolute;
211 | top: 10px;
212 | left: 45px;
213 | }
214 |
215 | #tutorial-url {
216 | display: block;
217 | position: absolute;
218 | top: 10px;
219 | right: 45px;
220 | text-align: right;
221 | }
222 |
223 | .center {
224 | display: grid;
225 | grid-area: center;
226 | grid-template-columns: 1fr;
227 | grid-template-rows: auto auto 35px 50px 1fr 1fr;
228 | grid-template-areas:
229 | "title"
230 | "description"
231 | "time"
232 | "terminal"
233 | "tracks"
234 | "modal"
235 | ;
236 | }
237 |
238 | #archives-container {
239 | grid-area: archives;
240 | margin-top: 0;
241 | }
242 |
243 | #grid-container {
244 | display: grid;
245 | /* grid-template-columns: repeat(4, 1fr); */
246 | grid-template-columns: 325px 650px 1fr;
247 | grid-template-rows: auto;
248 | grid-template-areas:
249 | "playlists center commands"
250 | "playlists center commands"
251 | "playlists center commands"
252 | "archives center commands"
253 | "player center commands"
254 | ;
255 | }
256 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | var html = require("choo/html")
2 | var devtools = require("choo-devtools")
3 | var Nanocomponent = require("nanocomponent")
4 | var choo = require("choo")
5 | var css = require("sheetify")
6 |
7 | css("./links/style.css")
8 |
9 | var remoteRoute = "/remote/:url/:playlist"
10 |
11 | var archive = new DatArchive(window.location.toString())
12 | var title = "datradio"
13 |
14 | var app = choo()
15 | app.use(devtools())
16 | app.use(init)
17 | app.use(inputHandler)
18 | app.route(remoteRoute, mainView)
19 | app.route("/:playlist", mainView)
20 | app.mount("body")
21 |
22 | // fix modulo for negative integers
23 | function mod(n, m) {
24 | return ((n % m) + m) % m
25 | }
26 |
27 | function format(durationStr) {
28 | durationStr = parseInt(durationStr)
29 | var min = pad(parseInt(durationStr / 60), 2)
30 | var sec = pad(parseInt(durationStr % 60), 2)
31 | return `${min}:${sec}`
32 | }
33 |
34 | function shuffle(inArr) {
35 | var output = [].concat(inArr)
36 | // fisher-yates shuffle
37 | for (var i = output.length-1; i > 1; i--) {
38 | // 0 <= j <= i
39 | var j = Math.floor(Math.random() * (i+1))
40 | // do the swap
41 | var temp = output[i]
42 | output[i] = output[j]
43 | output[j] = temp
44 | }
45 | return output
46 | }
47 |
48 | class Counter extends Nanocomponent {
49 | constructor() {
50 | super()
51 | this.time = "--:--"
52 | this.duration = "--:--"
53 | }
54 |
55 | createElement(time, duration) {
56 | this.time = time
57 | this.duration = duration
58 | return html`${format(this.time)}/${format(this.duration)}
`
59 | }
60 |
61 | update(time, duration) {
62 | console.log("nanocomponent update - time:", time)
63 | time = format(time)
64 | duration = format(duration)
65 | return time != this.time || duration != this.duration
66 | }
67 | }
68 |
69 | var hotkeySheet = {
70 | "toggle play/pause": {
71 | key: "spacebar",
72 | },
73 | "next track": {
74 | key: "n",
75 | },
76 | "previous track": {
77 | key: "p",
78 | },
79 | "random track": {
80 | key: "r",
81 | },
82 | "close info": {
83 | key: "x",
84 | }
85 | }
86 |
87 | var commands = {
88 | "bg": {
89 | value: "#1d1d1d",
90 | desc: "change the background colour",
91 | call: function(state, emit, value) {
92 | state.profile.bg = value
93 | }
94 | },
95 | "color": {
96 | value: "pink",
97 | desc: "change the font colour",
98 | call: function(state, emit, value) {
99 | state.profile.color = value
100 | }
101 | },
102 | "nick": {
103 | value: "",
104 | desc: "sets the name of your profile",
105 | call: function(state, emit, value) {
106 | state.user.name = value
107 | }
108 | },
109 | "desc": {
110 | value: "",
111 | desc: "a description of this playlist",
112 | call: function(state, emit, value) {
113 | state.description = value
114 | save(state)
115 | emit.emit("render")
116 | }
117 | },
118 | "create": {
119 | value: "playlist-name-no-spaces",
120 | desc: "create a playlist",
121 | call: function(state, emit, value) {
122 | value = value.replace(/\W*/g, "")
123 | state.playlists.push(value)
124 | window.location.hash = value
125 | reset(state)
126 | savePlaylist(state, value)
127 | .then(() => {
128 | save(state)
129 | emit.emit("render")
130 | })
131 | }
132 | },
133 | "delete-playlist": {
134 | value: "playlist-name",
135 | desc: "delete the playlist",
136 | call: function(state, emit, value) {
137 | deletePlaylist(value).then(() => {
138 | loadPlaylists().then((playlists) => {
139 | state.playlists = playlists
140 | // handle deleting the current playlist
141 | if (value === state.params.playlist) {
142 | window.location.hash = "playlist"
143 | }
144 | emit.emit("render")
145 | })
146 | })
147 | }
148 | },
149 | "rename": {
150 | value: "new-playlist-name-no-spaces",
151 | desc: "rename the current playlist",
152 | call: function(state, emit, value) {
153 | if (value) {
154 | value = value.replace(/\W*/g, "")
155 | var oldPlaylist = state.params.playlist ? state.params.playlist : "playlist"
156 | state.playlists.splice(state.playlists.indexOf(oldPlaylist), 1)
157 |
158 | savePlaylist(state, value).then(() => {
159 | deletePlaylist(oldPlaylist).then(() => {
160 | loadPlaylists().then((playlists) => {
161 | state.playlists = playlists
162 | window.location.hash = value.replace(" ", "")
163 | emit.emit("render")
164 | })
165 | })
166 | })
167 | }
168 | }
169 | },
170 | "unsub": {
171 | value: "",
172 | desc: "unsub from current playlist",
173 | call: function(state, emit, value) {
174 | parts = window.location.pathname.split("/remote/")
175 | if (parts.length <= 1) {
176 | return
177 | }
178 | value = prefix(parts[1])
179 | state.following.forEach((f, index) => {
180 | if (f.link === value) {
181 | state.following.splice(index, 1)
182 | return
183 | }
184 | })
185 | }
186 | },
187 | "sub": {
188 | value: "dat://1337...7331/#playlist-name",
189 | desc: "subscribe to a playlist",
190 | call: function(state, emit, value) {
191 | extractSub(value).then((info) => {
192 | state.following.push(info)
193 | emit.emit("render")
194 | save(state)
195 | })
196 | }
197 | },
198 | "del": {
199 | value: "track index",
200 | desc: "delete track from playlist",
201 | call: function(state, emit, value) {
202 | emit.emit("deleteTrack", parseInt(value))
203 | }
204 | },
205 | "del-archive": {
206 | value: "dat://1337...7331",
207 | desc: "delete all tracks from the archive",
208 | call: function(state, emit, value) {
209 | emit.emit("deleteArchive", value)
210 | }
211 | },
212 | "mv": {
213 | value: "trackIndex newIndex",
214 | desc: "move a track in the current playlist",
215 | call: function(state, emit, value) {
216 | var [src, dst] = value.split(/\W+/g)
217 | emit.emit("moveTrack", src, dst)
218 | }
219 | },
220 | "shuffle": {
221 | value: "on|off",
222 | desc: "turn on/off shuffle for playlist. [COMING SOON]",
223 | call: function(state, emit, value) {
224 | console.log(shuffle(state.tracks))
225 | }
226 | }
227 | }
228 |
229 | async function loadTracks(playlist) {
230 | var tracks = playlist.tracks
231 | var fromArchives = []
232 | return new Promise((resolve, reject) => {
233 | // TODO: refactor/clean this?
234 | if (playlist) {
235 | var promises = playlist.archives.map((address) => {
236 | return new Promise((res1, rej1) => {
237 | var a = new DatArchive(address)
238 | var path = address.substring(70) || "/"
239 | var files = await a.readdir(path)
240 | var archiveTracks = files.filter((i) => isTrack(i)).map((i) => prefix(address, i))
241 | var newTracks = archiveTracks.filter((i) => {
242 | return playlist.removed.indexOf(i) < 0 && tracks.indexOf(i) < 0
243 | })
244 | tracks = tracks.concat(newTracks)
245 | fromArchives = fromArchives.concat(archiveTracks)
246 | res1()
247 | })
248 | })
249 | await Promise.all(promises)
250 | // TODO: is this good? would we rather preserve tracks that have been added
251 | // and load them using version numbers?
252 | // filter out tracks that have been added to our playlist previously
253 | // but have been removed from the hosting archive (i.e. outside of datradio)
254 | tracks = tracks.filter((i) => fromArchives.indexOf(i) >= 0)
255 | resolve(tracks)
256 | }
257 | })
258 | }
259 |
260 | async function deletePlaylist(name) {
261 | // don't delete the default playlist
262 | if (name === "playlist") {
263 | var emptyState = {archives: [], tracks: [], removed: [], description: "", profile: {bg: "black", color: "#f2f2f2"}}
264 | return savePlaylist(emptyState, "playlist")
265 | }
266 | await archive.unlink(`playlists/${name}.json`)
267 | }
268 |
269 | function createHelpSidebar() {
270 | var items = []
271 | var hotkeyItems = []
272 | for (var key in commands) {
273 | items.push({key: key, cmd: commands[key]})
274 | }
275 |
276 | for (var key in hotkeySheet) {
277 | hotkeyItems.push({key: key, hotkey: hotkeySheet[key].key})
278 | }
279 |
280 | function createHelpEl(p) {
281 | return html`${p.key}
${p.cmd.value}
${p.cmd.desc}
`
282 |
283 | function fillTerminal() {
284 | var term = document.getElementById("terminal")
285 | term.value = `.${p.key} ${p.cmd.value}`
286 | term.focus()
287 | }
288 | }
289 |
290 | function createHotkeyEl(p) {
291 | return html``
292 | }
293 |
294 | return html`