A music player that connects to your cloud/distributed storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Closes #251

+72 -45
-9
src/Applications/UI/Tracks/Covers.elm
··· 344 344 , identifiedTrackCover = identifiedTrack 345 345 346 346 -- 347 - , focus = 348 - case sortBy_ of 349 - Artist -> 350 - "artist" 351 - 352 - _ -> 353 - "album" 354 - 355 - -- 356 347 , group = group 357 348 , sameAlbum = sameAlbum 358 349 , sameArtist = sameArtist
-1
src/Applications/UI/Tracks/Scene/Covers.elm
··· 706 706 cover.identifiedTrackCover 707 707 in 708 708 [ A.attribute "data-key" cover.key 709 - , A.attribute "data-focus" cover.focus 710 709 , A.attribute "data-filename" identifiers.filename 711 710 , A.attribute "data-path" track.path 712 711 , A.attribute "data-source-id" track.sourceId
-1
src/Javascript/Brain/index.js
··· 240 240 241 241 app.ports.syncTags.subscribe(context => { 242 242 processing.processContext(context).then(newContext => { 243 - console.log(newContext) 244 243 app.ports.replaceTags.send(newContext) 245 244 }) 246 245 })
+4 -5
src/Javascript/audio-engine.js
··· 82 82 // which seems to happen with audio nodes using an url created by `createObjectURL`. 83 83 insertTrack(orchestrion, { url: silentMp3File, trackId: "" }).then(_ => { 84 84 const temporaryClickHandler = () => { 85 - orchestrion.audio.play() 85 + if (orchestrion.audio) orchestrion.audio.play() 86 86 document.body.removeEventListener("click", temporaryClickHandler) 87 87 } 88 88 ··· 176 176 if ("mediaSession" in navigator && queueItem.trackTags) { 177 177 let artwork = [] 178 178 179 - if (maybeArtwork) { 179 + if (maybeArtwork && typeof maybeArtwork !== "string") { 180 180 artwork = [{ 181 - src: "https://dummyimage.com/512x512", 182 - sizes: "512x512", 183 - type: "image/png" 181 + src: URL.createObjectURL(maybeArtwork), 182 + type: maybeArtwork.type 184 183 }] 185 184 } 186 185
+63 -27
src/Javascript/index.js
··· 117 117 118 118 brain.onmessage = event => { 119 119 if (event.data.action) return handleAction(event.data.action, event.data.data, event.ports) 120 - if (event.data.tag) return app.ports.fromAlien.send(event.data) 120 + if (event.data.tag) app.ports.fromAlien.send(event.data) 121 + 122 + switch (event.data.tag) { 123 + case "GOT_CACHED_COVER": return gotCachedCover(event.data.data) 124 + } 121 125 } 122 126 123 127 app.ports.toBrain.subscribe(a => brain.postMessage(a)) ··· 167 171 168 172 orchestrion.activeQueueItem = item 169 173 orchestrion.audio = null 174 + orchestrion.coverPrep = null 170 175 176 + // Reset scrobble timer 171 177 if (orchestrion.scrobbleTimer) { 172 178 orchestrion.scrobbleTimer.stop() 173 179 orchestrion.scrobbleTimer = null 174 180 } 175 181 182 + // Remove older audio elements if possible 176 183 audioEngine.usesSingleAudioNode() 177 184 ? false 178 185 : audioEngine.removeOlderAudioElements(timestampInMilliseconds) 179 186 187 + // 🎵 180 188 if (item) { 181 - albumCover() 189 + const coverPrep = { 190 + cacheKey: btoa(item.trackTags.artist + " --- " + item.trackTags.album), 191 + trackFilename: item.trackPath.split("/").reverse()[0], 192 + trackPath: item.trackPath, 193 + trackSourceId: item.sourceId, 194 + variousArtists: false 195 + } 196 + 197 + albumCover(coverPrep.cacheKey).then(maybeCover => { 198 + orchestrion.coverPrep = coverPrep 199 + 200 + audioEngine.insertTrack( 201 + orchestrion, 202 + item, 203 + maybeCover 204 + ) 182 205 183 - audioEngine.insertTrack( 184 - orchestrion, 185 - item, 186 - albumCover 187 - ) 206 + if (!maybeCover) { 207 + loadAlbumCovers([ coverPrep ]) 208 + } 209 + }) 210 + 211 + // ✋ 188 212 } else { 189 213 app.ports.setAudioIsPlaying.send(false) 190 214 app.ports.setAudioPosition.send(0) 191 215 if (navigator.mediaSession) navigator.mediaSession.playbackState = "none" 216 + 192 217 } 193 218 } 194 219 ··· 384 409 385 410 wire.covers = () => { 386 411 app.ports.loadAlbumCovers.subscribe( 387 - debounce(loadAlbumCovers, 500) 412 + debounce(loadAlbumCoversFromDom, 500) 388 413 ) 389 414 390 415 db.keys().then(cachedCovers) 391 416 } 392 417 393 418 394 - function albumCover(cacheKey) { 395 - return db.getFromIndex({ key: `coverCache.${cacheKey}` }) 419 + function albumCover(coverKey) { 420 + return db.getFromIndex({ key: `coverCache.${coverKey}` }) 396 421 } 397 422 398 423 399 - function loadAlbumCovers() { 424 + function gotCachedCover({ key, url }) { 425 + const item = orchestrion.activeQueueItem 426 + 427 + if (key === orchestrion.coverPrep.key && item) { 428 + navigator.mediaSession.metadata = new MediaMetadata({ 429 + title: item.trackTags.title, 430 + artist: item.trackTags.artist, 431 + album: item.trackTags.album, 432 + artwork: [{ src: url }] 433 + }) 434 + } 435 + } 436 + 437 + 438 + function loadAlbumCoversFromDom() { 400 439 const nodes = Array.from( 401 440 document.querySelectorAll("#diffuse__track-covers [data-key]") 402 441 ).concat(Array.from( ··· 405 444 406 445 if (!nodes.length) return; 407 446 408 - const artworkPrep = nodes.map(node => { 409 - return { 410 - cacheKey: node.getAttribute("data-key"), 411 - focus: node.getAttribute("data-focus"), 412 - trackFilename: node.getAttribute("data-filename"), 413 - trackPath: node.getAttribute("data-path"), 414 - trackSourceId: node.getAttribute("data-source-id"), 415 - variousArtists: node.getAttribute("data-various-artists") 416 - } 447 + const coverPrepList = nodes.map(node => ({ 448 + cacheKey: node.getAttribute("data-key"), 449 + trackFilename: node.getAttribute("data-filename"), 450 + trackPath: node.getAttribute("data-path"), 451 + trackSourceId: node.getAttribute("data-source-id"), 452 + variousArtists: node.getAttribute("data-various-artists") 453 + })) 417 454 418 - }).filter(prep => { 419 - return !loadingCovers[prep.cacheKey] 455 + return loadAlbumCovers(coverPrepList) 456 + } 420 457 421 - }) 422 458 423 - artworkPrep.forEach(prep => { 459 + function loadAlbumCovers(coverPrepList) { 460 + return coverPrepList.reduce((acc, prep) => { 461 + if (loadingCovers[prep.cacheKey]) return acc 424 462 loadingCovers[prep.cacheKey] = true 425 - }) 426 463 427 - artworkPrep.reduce((acc, prep) => { 428 464 return acc.then(arr => { 429 465 return albumCover(prep.cacheKey).then(a => { 430 466 if (!a) return arr.concat([ prep ]) ··· 464 500 465 501 cachePromise.then(cache => { 466 502 app.ports.insertCoverCache.send(cache) 467 - setTimeout(loadAlbumCovers, 500) 503 + setTimeout(loadAlbumCoversFromDom, 500) 468 504 }) 469 505 } 470 506
+4
src/Library/Queue.elm
··· 22 22 type alias EngineItem = 23 23 { isCached : Bool 24 24 , progress : Maybe Float 25 + , sourceId : String 25 26 , trackId : String 26 27 , trackTags : Tags 28 + , trackPath : String 27 29 , url : String 28 30 } 29 31 ··· 36 38 makeEngineItem timestamp sources cachedTrackIds progressTable track = 37 39 { isCached = List.member track.id cachedTrackIds 38 40 , progress = Dict.get track.id progressTable 41 + , sourceId = track.sourceId 39 42 , trackId = track.id 43 + , trackPath = track.path 40 44 , trackTags = track.tags 41 45 , url = makeTrackUrl timestamp sources track 42 46 }
+1 -2
src/Library/Tracks.elm
··· 46 46 47 47 48 48 type alias Cover = 49 - { focus : String 50 - , group : String 49 + { group : String 51 50 , identifiedTrackCover : IdentifiedTrack 52 51 , key : String 53 52 , sameAlbum : Bool