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.

refactor: queue, minimal queue structure

+133 -105
+2
src/common/facets/foundation.js
··· 227 227 function queueAudio() { 228 228 const a = audio(); 229 229 const i = input(); 230 + const o = output(); 230 231 const q = queue(); 231 232 const r = repeatShuffle(); 232 233 ··· 234 235 oqa.setAttribute("group", GROUP); 235 236 oqa.setAttribute("audio-engine-selector", a.selector); 236 237 oqa.setAttribute("input-selector", i.selector); 238 + oqa.setAttribute("output-selector", o.selector); 237 239 oqa.setAttribute("queue-engine-selector", q.selector); 238 240 oqa.setAttribute("repeat-shuffle-engine-selector", r.selector); 239 241
+6 -6
src/components/engine/queue/types.d.ts
··· 1 - import type { Track } from "@definitions/types.d.ts"; 2 1 import type { SignalReader } from "@common/signal.d.ts"; 3 2 4 3 export type Actions = { 5 - add: (args: { inFront?: boolean; tracks: Track[] }) => void; 4 + add: (args: { inFront?: boolean; trackIds: string[] }) => void; 6 5 /** 7 6 * Clear the `future()` items. 8 7 */ ··· 16 15 }, 17 16 ) => void; 18 17 shift: () => void; 19 - supply: (args: { tracks: Track[] }) => void; 18 + supply: (args: { trackIds: string[] }) => void; 20 19 unshift: () => void; 21 20 }; 22 21 23 - export type Item = 24 - & Track 25 - & { manualEntry?: boolean }; 22 + export type Item = { 23 + id: string; 24 + manualEntry: boolean; 25 + }; 26 26 27 27 export type State = { 28 28 future: SignalReader<Item[]>;
+22 -26
src/components/engine/queue/worker.js
··· 1 1 import { announce, ostiary, rpc } from "@common/worker.js"; 2 2 import { effect, signal } from "@common/signal.js"; 3 - import { arrayShuffle, hash } from "@common/utils.js"; 3 + import { arrayShuffle } from "@common/utils.js"; 4 + import { xxh32 } from "xxh32"; 4 5 5 6 /** 6 7 * @import {Actions, Item} from "./types.d.ts" 7 - * @import {Track} from "@definitions/types.d.ts" 8 8 */ 9 9 10 10 //////////////////////////////////////////// 11 11 // STATE 12 12 //////////////////////////////////////////// 13 13 14 - export const $lake = signal(/** @type {Track[]} */ ([])); 14 + /** Ordered list of available track IDs. */ 15 + export const $lake = signal(/** @type {string[]} */ ([])); 15 16 16 17 // Communicated state 17 18 export const $future = signal(/** @type {Item[]} */ ([])); ··· 28 29 /** 29 30 * @type {Actions['add']} 30 31 */ 31 - export function add({ inFront, tracks }) { 32 - const items = tracks.map((track) => { 33 - return { ...track, manualEntry: true }; 32 + export function add({ inFront, trackIds }) { 33 + const items = trackIds.map((id) => { 34 + return { id, manualEntry: true }; 34 35 }); 35 36 36 37 $future.value = inFront ··· 71 72 /** 72 73 * @type {Actions['supply']} 73 74 */ 74 - export function supply({ tracks }) { 75 - $lake.value = tracks; 76 - $supplyFingerprint.value = tracks.length ? hash(tracks) : undefined; 75 + export function supply({ trackIds }) { 76 + $lake.value = trackIds; 77 + $supplyFingerprint.value = trackIds.length 78 + ? xxh32(trackIds.join("\0")).toString() 79 + : undefined; 77 80 } 78 81 79 82 /** ··· 140 143 141 144 // Count 142 145 let autoFutureCount = 0; 143 - let manualFutureCount = 0; 144 146 145 147 future.forEach((item) => { 146 - if (item.manualEntry) manualFutureCount++; 148 + if (item.manualEntry) {} 147 149 else autoFutureCount++; 148 150 }); 149 151 ··· 165 167 const onlyManual = future.filter((i) => i.manualEntry); 166 168 const lastManual = onlyManual.slice(-1)[0]; 167 169 const startIndex = lastManual 168 - ? $lake.value.findIndex((t) => t.id === lastManual.id) + 1 170 + ? $lake.value.indexOf(lastManual.id) + 1 169 171 : $now.value 170 - ? $lake.value.findIndex((t) => t.id === $now.value?.id) + 1 172 + ? $lake.value.indexOf($now.value.id) + 1 171 173 : 0; 172 174 173 175 const maxIndex = $lake.value.length - 1; ··· 178 180 179 181 for (let i = 0; i < fillAmount; i++) { 180 182 if (currIndex > maxIndex) currIndex = 0; 181 - const item = $lake.value[currIndex]; 182 - if (item) { 183 - autoItems.push({ 184 - ...item, 185 - manualEntry: false, 186 - }); 183 + const id = $lake.value[currIndex]; 184 + if (id) { 185 + autoItems.push({ id, manualEntry: false }); 187 186 } 188 187 currIndex++; 189 188 } ··· 205 204 const pastSet = new Set($past.value.map((i) => i.id)); 206 205 let reducedPool = pool; 207 206 208 - $lake.value.forEach((track) => { 209 - if (pastSet.delete(track.id) === false) { 210 - pool.push({ 211 - ...track, 212 - manualEntry: false, 213 - }); 207 + $lake.value.forEach((id) => { 208 + if (pastSet.delete(id) === false) { 209 + pool.push({ id, manualEntry: false }); 214 210 } 215 211 }); 216 212 217 213 if (reducedPool.length === 0) { 218 - reducedPool = $lake.value; 214 + reducedPool = $lake.value.map((id) => ({ id, manualEntry: false })); 219 215 } 220 216 221 217 const poolSelection = arrayShuffle(reducedPool).slice(
+1 -1
src/components/orchestrator/auto-queue/element.js
··· 52 52 53 53 this.isLeader().then(async (isLeader) => { 54 54 if (!isLeader) return; 55 - queue.supply({ tracks }); 55 + queue.supply({ trackIds: tracks.map((t) => t.id) }); 56 56 }); 57 57 }); 58 58
+15 -8
src/components/orchestrator/queue-audio/element.js
··· 3 3 4 4 /** 5 5 * @import {InputElement} from "@components/input/types.d.ts" 6 + * @import {OutputElement} from "@components/output/types.d.ts" 6 7 * @import RepeatShuffleEngine from "@components/engine/repeat-shuffle/element.js" 7 8 */ 8 9 ··· 34 35 // Super 35 36 super.connectedCallback(); 36 37 38 + /** @type {import("@components/engine/audio/element.js").CLASS} */ 39 + this.audio = query(this, "audio-engine-selector"); 40 + 37 41 /** @type {InputElement} */ 38 42 this.input = query(this, "input-selector"); 39 43 40 - /** @type {import("@components/engine/audio/element.js").CLASS} */ 41 - this.audio = query(this, "audio-engine-selector"); 44 + /** @type {OutputElement} */ 45 + this.output = query(this, "output-selector"); 42 46 43 47 /** @type {import("@components/engine/queue/element.js").CLASS} */ 44 48 this.queue = query(this, "queue-engine-selector"); ··· 64 68 if (!this.input) return; 65 69 if (!this.queue) return; 66 70 67 - const activeTrack = this.queue.now(); 71 + const activeItem = this.queue.now(); 72 + const activeTrack = activeItem 73 + ? this.output?.tracks.collection().find((t) => t.id === activeItem.id) 74 + : undefined; 68 75 if ((await this.isLeader()) === false) return; 69 76 70 77 const isPlaying = untracked(this.audio.isPlaying); ··· 81 88 const url = resolvedUri?.url; 82 89 83 90 // Check if we still need to render 84 - if (this.queue.now?.()?.id !== activeTrack?.id) return; 91 + if (this.queue.now?.()?.id !== activeItem?.id) return; 85 92 86 93 // Play new active queue item 87 94 // TODO: Take URL expiration timestamp into account 88 95 // TODO: Preload next queue item 89 96 this.audio.supply({ 90 - audio: activeTrack && url 97 + audio: activeItem && url 91 98 ? [{ 92 - id: activeTrack.id, 99 + id: activeItem.id, 93 100 isPreload: false, 94 101 url, 95 102 }] 96 103 // TODO: Keep preloads 97 104 : [], 98 - play: activeTrack && isPlaying ? { audioId: activeTrack.id } : undefined, 105 + play: activeItem && isPlaying ? { audioId: activeItem.id } : undefined, 99 106 }); 100 107 } 101 108 ··· 115 122 if (now) { 116 123 await this.queue.add({ 117 124 inFront: true, 118 - tracks: [now], 125 + trackIds: [now.id], 119 126 }); 120 127 } 121 128 }
+8 -3
src/facets/examples/generate-playlist.html.txt
··· 13 13 import foundation from "./common/facets/foundation.js"; 14 14 15 15 const queue = foundation.engine.queue(); 16 + const output = foundation.orchestrator.output(); 16 17 17 18 /** 18 19 * Playlist generator 19 20 */ 20 - function generatePlaylist(items) { 21 - const playlist = [ 21 + function generatePlaylist() { 22 + const queueItems = [ 22 23 ...queue.past(), 23 24 ...(queue.now() ? [queue.now()] : []), 24 25 ...queue.future().filter((i) => i.manualEntry), 25 26 ]; 26 27 28 + const playlist = queueItems 29 + .map((item) => output.tracks.collection().find(t => t.id === item.id)) 30 + .filter((t) => t); 31 + 27 32 const element = document.querySelector("main ol"); 28 33 if (!element) return; 29 34 30 35 element.innerHTML = playlist 31 - .map((item) => `<li>${item.tags.artist} - ${item.tags.title}</li>`) 36 + .map((track) => `<li>${track.tags.artist} - ${track.tags.title}</li>`) 32 37 .join(""); 33 38 } 34 39
+2 -1
src/facets/examples/now-playing.html.txt
··· 23 23 }); 24 24 25 25 effect(() => { 26 - const currentlyPlaying = queue.now(); 26 + const now = components.engine.queue.now(); 27 + const currentlyPlaying = now ? output.tracks.collection().find(t => t.id === now.id) : undefined; 27 28 const tags = currentlyPlaying?.tags; 28 29 29 30 const element = document.querySelector("#now-playing");
+2 -1
src/facets/index.js
··· 220 220 const myHtmlElement = document.querySelector("#now-playing"); 221 221 222 222 effect(() => { 223 - const currentlyPlaying = components.engine.queue.now(); 223 + const now = components.engine.queue.now(); 224 + const currentlyPlaying = now ? components.orchestrator.output.tracks.collection().find(t => t.id === now.id) : undefined; 224 225 if (currentlyPlaying && myHtmlElement) { 225 226 myHtmlElement.innerText = \`\$\{currentlyPlaying.tags.artist} - \$\{currentlyPlaying.tags.title}\`; 226 227 }
+68 -52
src/themes/blur/artwork-controller/element.js
··· 18 18 * @import {RenderArg} from "@common/element.d.ts" 19 19 * 20 20 * @import {InputElement} from "@components/input/types.d.ts" 21 + * @import {OutputElement} from "@components/output/types.d.ts" 21 22 * @import {Artwork} from "@components/processor/artwork/types.d.ts" 22 23 * @import AudioEngine from "@components/engine/audio/element.js" 23 24 * @import QueueEngine from "@components/engine/queue/element.js" ··· 60 61 /** @type {FavouritesOrchestrator | undefined} */ (undefined), 61 62 ); 62 63 $input = signal(/** @type {InputElement | undefined} */ (undefined)); 64 + $output = signal(/** @type {OutputElement | undefined} */ (undefined)); 63 65 $queue = signal(/** @type {QueueEngine | undefined} */ (undefined)); 64 66 65 67 // SIGNALS - COMPUTED 66 68 67 - #audio = computed(() => { 69 + audio = computed(() => { 68 70 const curr = this.$queue.value?.now(); 69 71 return curr ? this.$audio.value?.state(curr.id) : undefined; 70 72 }); 71 73 72 - #isPlaying = computed(() => { 74 + currentTrack = computed(() => { 75 + const item = this.$queue.value?.now(); 76 + if (!item) return undefined; 77 + return this.$output.value?.tracks.collection().find((t) => 78 + t.id === item.id 79 + ); 80 + }); 81 + 82 + isPlaying = computed(() => { 73 83 return this.$audio.value?.isPlaying(); 74 84 }); 75 85 ··· 90 100 /** @type {InputElement} */ 91 101 const input = query(this, "input-selector"); 92 102 103 + /** @type {OutputElement} */ 104 + const output = query(this, "output-selector"); 105 + 93 106 /** @type {QueueEngine} */ 94 107 const queue = query(this, "queue-engine-selector"); 95 108 96 109 /** @type {FavouritesOrchestrator} */ 97 110 const favourites = query(this, "favourites-orchestrator-selector"); 98 111 99 - whenElementsDefined({ audio, artwork, favourites, input, queue }).then( 100 - () => { 101 - this.$artwork.value = artwork; 102 - this.$audio.value = audio; 103 - this.$input.value = input; 104 - this.$queue.value = queue; 105 - this.$favourites.value = favourites; 112 + whenElementsDefined({ audio, artwork, favourites, input, output, queue }) 113 + .then( 114 + () => { 115 + this.$artwork.value = artwork; 116 + this.$audio.value = audio; 117 + this.$input.value = input; 118 + this.$output.value = output; 119 + this.$queue.value = queue; 120 + this.$favourites.value = favourites; 106 121 107 - // Changed artwork based on active queue item. 108 - const debouncedChangeArtwork = debounce( 109 - 1000, 110 - this.#setArtwork.bind(this), 111 - ); 122 + // Changed artwork based on active queue item. 123 + const debouncedChangeArtwork = debounce( 124 + 1000, 125 + this.#setArtwork.bind(this), 126 + ); 112 127 113 - this.effect(() => { 114 - const _trigger = queue.now(); 115 - debouncedChangeArtwork(); 116 - }); 128 + this.effect(() => { 129 + const _trigger = this.currentTrack(); 130 + debouncedChangeArtwork(); 131 + }); 117 132 118 - this.effect(() => this.#formatTimestamps()); 119 - this.effect(() => this.#lightOrDark()); 133 + this.effect(() => this.#formatTimestamps()); 134 + this.effect(() => this.#lightOrDark()); 120 135 121 - this.effect(() => { 122 - const now = !!queue.now(); 123 - const aud = this.#audio()?.loadingState(); 124 - const bool = now && aud !== "loaded"; 136 + this.effect(() => { 137 + const now = !!queue.now(); 138 + const aud = this.audio()?.loadingState(); 139 + const bool = now && aud !== "loaded"; 125 140 126 - if (this.#isLoadingTimeout) { 127 - clearTimeout(this.#isLoadingTimeout); 128 - } 141 + if (this.#isLoadingTimeout) { 142 + clearTimeout(this.#isLoadingTimeout); 143 + } 129 144 130 - if (bool) { 131 - this.#isLoadingTimeout = setTimeout( 132 - () => this.#isLoading.value = true, 133 - 2000, 134 - ); 135 - } else { 136 - this.#isLoading.value = false; 137 - } 138 - }); 139 - }, 140 - ); 145 + if (bool) { 146 + this.#isLoadingTimeout = setTimeout( 147 + () => this.#isLoading.value = true, 148 + 2000, 149 + ); 150 + } else { 151 + this.#isLoading.value = false; 152 + } 153 + }); 154 + }, 155 + ); 141 156 } 142 157 143 158 //////////////////////////////////////////// ··· 156 171 157 172 /** */ 158 173 async #setArtwork() { 159 - const track = this.$queue.value?.now(); 174 + const track = this.currentTrack(); 160 175 const currArtwork = untracked(this.#artwork.get); 161 176 162 177 if (!track) { ··· 195 210 }, 196 211 }; 197 212 198 - if (this.$queue.value?.now()?.id !== track.id) { 213 + if (this.$queue.value?.now()?.id !== track?.id) { 199 214 return; 200 215 } 201 216 202 217 const allArt = await this.$artwork.value?.artwork(request) ?? []; 203 218 204 - const currTrack = this.$queue.value?.now(); 219 + // Check if queue item has changed while fetching the artwork 220 + const currTrack = this.currentTrack(); 205 221 const currCacheId = currTrack 206 222 ? await trackArtworkCacheId(currTrack) 207 223 : undefined; ··· 241 257 // ⌚️ Time 242 258 //////////////////////////////////////////// 243 259 #formatTimestamps() { 244 - const curr = this.$queue.value?.now?.() ?? undefined; 245 - const audio = this.#audio(); 260 + const currTrack = this.currentTrack(); 261 + const audio = this.audio(); 246 262 const prog = audio?.progress() ?? 0; 247 - const durMs = curr?.stats?.duration ?? 263 + const durMs = currTrack?.stats?.duration ?? 248 264 (audio?.duration() != null ? audio.duration() * 1000 : undefined); 249 265 250 266 if (audio && durMs != undefined && !isNaN(durMs)) { ··· 330 346 playPause = () => { 331 347 const audioId = this.$queue.value?.now()?.id; 332 348 333 - if (this.#isPlaying() && audioId) { 349 + if (this.isPlaying() && audioId) { 334 350 this.$audio.value?.pause({ audioId }); 335 351 } else if (audioId) { 336 352 this.$audio.value?.play({ audioId }); ··· 367 383 }; 368 384 369 385 toggleFavourite = () => { 370 - const activeQueueItem = this.$queue.value?.now(); 371 - if (!activeQueueItem) return; 386 + const track = this.currentTrack(); 387 + if (!track) return; 372 388 373 - this.$favourites.value?.toggle(activeQueueItem); 389 + this.$favourites.value?.toggle(track); 374 390 }; 375 391 376 392 // RENDER ··· 379 395 * @param {RenderArg} _ 380 396 */ 381 397 render({ html }) { 382 - const activeQueueItem = this.$queue.value?.now(); 398 + const activeQueueItem = this.currentTrack(); 383 399 const isFav = activeQueueItem 384 400 ? this.$favourites.value?.isFavourite(activeQueueItem) ?? false 385 401 : false; ··· 462 478 <!-- PROGRESS --> 463 479 464 480 <div class="progress" @click="${this.seek}"> 465 - <progress max="100" value="${(this.#audio()?.progress() ?? 481 + <progress max="100" value="${(this.audio()?.progress() ?? 466 482 0) * 100}"></progress> 467 483 <div class="timestamps"> 468 484 <time datetime="${this.#time.value}">${this.#time.value}</time> ··· 491 507 <li 492 508 @click="${this.playPause}" 493 509 style="display: ${!this.#isLoading.value && 494 - !this.#isPlaying() 510 + !this.isPlaying() 495 511 ? `inline` 496 512 : `none`};" 497 513 > ··· 501 517 <!-- pause --> 502 518 <li 503 519 @click="${this.playPause}" 504 - style="display: ${!this.#isLoading.value && this.#isPlaying() 520 + style="display: ${!this.#isLoading.value && this.isPlaying() 505 521 ? `inline` 506 522 : `none`};" 507 523 >
+2
src/themes/blur/artwork-controller/facet.html.txt
··· 16 16 const art = foundation.processor.artwork(); 17 17 const fav = foundation.orchestrator.favourites(); 18 18 const inp = foundation.orchestrator.input(); 19 + const out = foundation.orchestrator.output(); 19 20 const que = foundation.engine.queue(); 20 21 21 22 // Controller ··· 23 24 dac.setAttribute("artwork-processor-selector", art.selector); 24 25 dac.setAttribute("audio-engine-selector", aud.selector); 25 26 dac.setAttribute("input-selector", inp.selector); 27 + dac.setAttribute("output-selector", out.selector); 26 28 dac.setAttribute("queue-engine-selector", que.selector); 27 29 dac.setAttribute("favourites-orchestrator-selector", fav.selector); 28 30
+1 -1
src/themes/webamp/browser/element.js
··· 193 193 playTrack(track) { 194 194 this.$queue.value?.add({ 195 195 inFront: true, 196 - tracks: [track], 196 + trackIds: [track.id], 197 197 }); 198 198 199 199 this.$queue.value?.shift();
+4 -6
src/themes/webamp/index.js
··· 100 100 /** @type {Record<string, number>} */ 101 101 const newIdx = {}; 102 102 103 - /** @type {Record<string, Track>} */ 104 - const idMap = {}; 105 - 106 103 list.forEach((item) => { 107 104 newIdx[item.id] = (newIdx[item.id] ?? 0) + 1; 108 - idMap[item.id] = item; 109 105 }); 110 106 111 107 /** @type {Track[]} */ ··· 114 110 Object.entries(newIdx).forEach(([id, n]) => { 115 111 const x = index[id] ?? 0; 116 112 if (n > x) { 117 - tracksToAdd.push(idMap[id]); 113 + const track = output.tracks.collection().find((t) => t.id === id); 114 + if (track) tracksToAdd.push(track); 118 115 index[id] = x + 1; 119 116 } 120 117 }); ··· 131 128 * Fill queue supply with available tracks. 132 129 */ 133 130 effect(() => { 134 - queue.supply({ tracks: scopedTracks.tracks() }); 131 + const tracks = scopedTracks.tracks(); 132 + queue.supply({ trackIds: tracks.map((t) => t.id) }); 135 133 }); 136 134 137 135 /**