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

Configure Feed

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

feat: fill action for queue

+136 -66
+11 -23
src/common/index.js
··· 1 1 // import * as Uint8 from "uint8arrays"; 2 + import { xxh32r } from "xxh32/dist/raw.js"; 2 3 3 4 /** 4 5 * @import {Track} from "@common/types.d.ts" ··· 27 28 } 28 29 29 30 /** 30 - * @param {Track[]} tracks 31 - * @returns {Track[]} 31 + * @param {string | undefined | null} value 32 32 */ 33 - export function cleanUndefinedValuesForTracks(tracks) { 34 - return tracks.map((track) => { 35 - const t = { ...track }; 36 - 37 - if (t.tags) { 38 - if ("album" in t.tags && t.tags.album === undefined) delete t.tags.album; 39 - if ("artist" in t.tags && t.tags.artist === undefined) { 40 - delete t.tags.artist; 41 - } 42 - if ("genre" in t.tags && t.tags.genre === undefined) delete t.tags.genre; 43 - if ("year" in t.tags && t.tags.year === undefined) delete t.tags.year; 44 - 45 - if ("of" in t.tags.disc && t.tags.disc.of === undefined) { 46 - delete t.tags.disc.of; 47 - } 48 - if ("of" in t.tags.track && t.tags.track.of === undefined) { 49 - delete t.tags.track.of; 50 - } 51 - } 33 + export function boolAttr(value) { 34 + return value === ""; 35 + } 52 36 53 - return t; 54 - }); 37 + /** 38 + * @param {any} object 39 + */ 40 + export function hash(object) { 41 + return xxh32r(jsonEncode(object)).toString(); 55 42 } 56 43 57 44 /** ··· 105 92 * @returns {Promise<string>} 106 93 */ 107 94 export async function trackArtworkCacheId(track) { 95 + // TODO: 108 96 return ""; 109 97 // return await crypto.subtle 110 98 // .digest("SHA-256", new TextEncoder().encode(track.uri))
+8 -4
src/common/worker.js
··· 139 139 fn, 140 140 context = /** @type {WorkerGlobalScope} */ (globalThis), 141 141 ) { 142 - if (!context.incoming) { 142 + const c = /** @type {any} */ (context); 143 + 144 + if (!c.incoming) { 143 145 context.addEventListener("message", incomingAnnouncementsHandler(context)); 144 - context.incoming = {}; 146 + c.incoming = {}; 145 147 } 146 148 147 - context.incoming[name] = debounceMicrotask(fn, { updateArguments: true }); 149 + c.incoming[name] = debounceMicrotask(fn, { updateArguments: true }); 148 150 } 149 151 150 152 //////////////////////////////////////////// ··· 239 241 } 240 242 241 243 batch(() => { 244 + const c = /** @type {any} */ (context); 245 + 242 246 arr.forEach((announcement) => { 243 - context.incoming[announcement.name]?.(announcement.args); 247 + c.incoming[announcement.name]?.(announcement.args); 244 248 }); 245 249 }); 246 250 },
+12 -2
src/components/engine/queue/element.js
··· 2 2 3 3 import { DiffuseElement } from "@common/element.js"; 4 4 import { signal } from "@common/signal.js"; 5 - import { listen, proxyProvider } from "@common/worker.js"; 5 + import { listen, proxyProvider, use } from "@common/worker.js"; 6 + import { hash } from "@common/index.js"; 6 7 7 8 /** 8 9 * @import {ProxiedActions, ProxyProvider} from "@common/worker.d.ts"; ··· 44 45 listen("future", this.#future.set, port); 45 46 listen("now", this.#now.set, port); 46 47 listen("past", this.#past.set, port); 48 + listen("poolHash", this.#poolHash.set, port); 49 + 50 + use("future", port)().then(this.#future.set); 51 + use("now", port)().then(this.#now.set); 52 + use("past", port)().then(this.#past.set); 53 + use("poolHash", port)().then(this.#poolHash.set); 47 54 48 55 /** @type {ProxyProvider<Actions>} */ 49 - const proxy = proxyProvider(["add", "pool", "shift", "unshift"]); 56 + const proxy = proxyProvider(["add", "fill", "pool", "shift", "unshift"]); 50 57 51 58 // Worker proxy 52 59 const w = proxy(port); 53 60 54 61 this.add = w.add; 62 + this.fill = w.fill; 55 63 this.pool = w.pool; 56 64 this.shift = w.shift; 57 65 this.unshift = w.unshift; ··· 62 70 #future = signal(/** @type {Array<Item>} */ ([])); 63 71 #now = signal(/** @type {Item | null} */ (null)); 64 72 #past = signal(/** @type {Array<Item>} */ ([])); 73 + #poolHash = signal(hash([])); 65 74 66 75 // STATE 67 76 68 77 future = this.#future.get; 69 78 now = this.#now.get; 70 79 past = this.#past.get; 80 + poolHash = this.#poolHash.get; 71 81 } 72 82 73 83 export default QueueEngine;
+8
src/components/engine/queue/types.d.ts
··· 3 3 4 4 export type Actions = { 5 5 add: (args: { inFront?: boolean; tracks: Track[] }) => void; 6 + fill: ( 7 + args: { 8 + /** Always keep adding, even if the amount of non-manual items in the queue are passed the given `amount` */ 9 + augment?: boolean; 10 + amount: number; 11 + shuffled: boolean; 12 + }, 13 + ) => void; 6 14 pool: (tracks: Track[]) => void; 7 15 shift: () => void; 8 16 unshift: () => void;
+76 -33
src/components/engine/queue/worker.js
··· 1 - import QS from "query-string"; 2 - 3 1 import { announce, define, ostiary } from "@common/worker.js"; 4 2 import { effect, signal } from "@common/signal.js"; 5 - import { arrayShuffle } from "@common/index.js"; 3 + import { arrayShuffle, hash } from "@common/index.js"; 6 4 7 5 /** 8 6 * @import {Actions, Item} from "./types.d.ts" 9 7 * @import {Track} from "@common/types.d.ts" 10 8 */ 11 - 12 - const QUERY = QS.parse(location.search); 13 - const qFill = QUERY?.["fill"]; 14 - 15 - /** @type {number | null} */ 16 - const FILL = qFill && qFill !== null 17 - ? Array.isArray(qFill) && qFill[0] !== null 18 - ? parseInt(qFill[0], 10) 19 - : parseInt(/** @type {string} */ (qFill), 10) 20 - : null; 21 9 22 10 //////////////////////////////////////////// 23 11 // STATE 24 12 //////////////////////////////////////////// 25 13 14 + export const $lake = signal(/** @type {Track[]} */ ([])); 15 + 16 + // Communicated state 26 17 export const $future = signal(/** @type {Item[]} */ ([])); 27 - export const $lake = signal(/** @type {Track[]} */ ([])); 28 18 export const $now = signal(/** @type {Item | null} */ (null)); 29 19 export const $past = signal(/** @type {Item[]} */ ([])); 20 + export const $poolHash = signal(hash([])); 30 21 31 22 //////////////////////////////////////////// 32 23 // ACTIONS ··· 46 37 } 47 38 48 39 /** 40 + * @type {Actions['fill']} 41 + */ 42 + export function fill({ augment, amount, shuffled }) { 43 + $future.value = fillQueue( 44 + shuffled, 45 + amount + 46 + (augment 47 + ? $future.value.filter((i) => i.manualEntry === false).length 48 + : 0), 49 + $future.value, 50 + ); 51 + } 52 + 53 + /** 49 54 * @type {Actions['pool']} 50 55 */ 51 56 export function pool(tracks) { 52 57 $lake.value = tracks; 53 - 54 - // TODO: If the pool changes, only remove non-existing tracks 55 - // instead of resetting the whole future queue. 56 - // 57 - // What about past queue items? 58 + $poolHash.value = hash(tracks); 58 59 59 - // Automatically insert track if there isn't any 60 - if (!$now.value) _shift(fill([])); 61 - else $future.value = fill([]); 60 + // TODO: Clear the queue, 61 + // there might be items in there that are no longer in the pool. 62 62 } 63 63 64 64 /** ··· 92 92 define("future", $future.get, port); 93 93 define("now", $now.get, port); 94 94 define("past", $past.get, port); 95 + define("poolHash", $poolHash.get, port); 95 96 96 97 define("add", add, port); 98 + define("fill", fill, port); 97 99 define("pool", pool, port); 98 100 define("shift", shift, port); 99 101 define("unshift", unshift, port); ··· 103 105 effect(() => announce("future", $future.value, port)); 104 106 effect(() => announce("now", $now.value, port)); 105 107 effect(() => announce("past", $past.value, port)); 108 + effect(() => announce("poolHash", $poolHash.value, port)); 106 109 }); 107 110 108 111 //////////////////////////////////////////// ··· 110 113 //////////////////////////////////////////// 111 114 112 115 /** 113 - * Automatically add non-manual items to the queue. 116 + * Add non-manual items to the queue. 114 117 * 118 + * @param {boolean} shuffled 119 + * @param {number | undefined | null} fillAmount 115 120 * @param {Item[]} future 116 121 * @returns {Item[]} 117 122 */ 118 - function fill(future) { 119 - if (!FILL) return future; 123 + function fillQueue(shuffled, fillAmount, future) { 124 + if (!fillAmount) return future; 120 125 121 126 // Count 122 127 let autoFutureCount = 0; ··· 127 132 else autoFutureCount++; 128 133 }); 129 134 130 - // Exit early if queue already filled appropriatly 131 - if (autoFutureCount >= FILL) return future; 132 - 133 135 // Fill 134 - return fillShuffle(future, autoFutureCount); 136 + if (shuffled) { 137 + if (autoFutureCount >= fillAmount) return future; 138 + return fillShuffle(fillAmount, future, autoFutureCount); 139 + } else { 140 + return fillSequentially(fillAmount, future); 141 + } 135 142 } 136 143 137 144 /** 145 + * @param {number} fillAmount 146 + * @param {Item[]} future 147 + * @returns {Item[]} 148 + */ 149 + export function fillSequentially(fillAmount, future) { 150 + const onlyManual = future.filter((i) => i.manualEntry); 151 + const lastManual = onlyManual.slice(-1)[0]; 152 + const startIndex = lastManual 153 + ? $lake.value.findIndex((t) => t.id === lastManual.id) + 1 154 + : $now.value 155 + ? $lake.value.findIndex((t) => t.id === $now.value?.id) + 1 156 + : 0; 157 + 158 + const maxIndex = $lake.value.length - 1; 159 + let currIndex = startIndex; 160 + 161 + /** @type {Item[]} */ 162 + const autoItems = []; 163 + 164 + for (let i = 0; i < fillAmount; i++) { 165 + if (currIndex > maxIndex) currIndex = 0; 166 + const item = $lake.value[currIndex]; 167 + if (item) { 168 + autoItems.push({ 169 + ...item, 170 + manualEntry: false, 171 + }); 172 + } 173 + currIndex++; 174 + } 175 + 176 + return [...onlyManual, ...autoItems]; 177 + } 178 + 179 + /** 180 + * @param {number} fillAmount 138 181 * @param {Item[]} future 139 182 * @param {number} autoFutureCount 140 183 * @returns {Item[]} 141 184 */ 142 - export function fillShuffle(future, autoFutureCount) { 143 - // Determine pool of available tracks 185 + export function fillShuffle(fillAmount, future, autoFutureCount) { 186 + // Determine pool of available queue items 144 187 /** @type {Item[]} */ 145 188 const pool = []; 146 189 ··· 162 205 163 206 const poolSelection = arrayShuffle(reducedPool).slice( 164 207 0, 165 - Math.max(0, (FILL ?? 0) - autoFutureCount), 208 + Math.max(0, fillAmount - autoFutureCount), 166 209 ); 167 210 168 211 return [...future, ...poolSelection]; ··· 177 220 178 221 $now.value = f[0] ?? null; 179 222 if (n) $past.value = [...$past.value, n]; 180 - $future.value = fill(f.slice(1)); 223 + $future.value = f.slice(1); 181 224 }
+1 -1
src/index.vto
··· 33 33 desc: "Plays audio through audio elements." 34 34 - url: "components/engine/queue/element.js" 35 35 title: "Queue" 36 - desc: "A simple queue for tracks. NOTE: Temporarily automatically adds tracks shuffled to the queue." 36 + desc: "A simple queue for tracks." 37 37 38 38 input: 39 39 - url: "components/input/opensubsonic/element.js"
+17
src/themes/webamp/index.js
··· 126 126 } 127 127 }); 128 128 129 + /** 130 + * AUTOPLAY: 131 + * Make sure there's always some random tracks in the queue. 132 + */ 133 + effect(() => { 134 + const _trigger = queue.now(); 135 + queue.fill({ amount: 10, shuffled: true }); 136 + }); 137 + 138 + effect(() => { 139 + const _trigger = queue.poolHash(); 140 + queue.fill({ amount: 10, shuffled: true }); 141 + 142 + // Automatically insert track if there isn't any 143 + if (!queue.now) queue.shift(); 144 + }); 145 + 129 146 //////////////////////////////////////////// 130 147 // DESKTOP 131 148 ////////////////////////////////////////////
+3 -3
src/themes/webamp/index.vto
··· 75 75 COMPONENTS 76 76 77 77 --> 78 - <de-queue fill="10"></de-queue> 78 + <de-queue></de-queue> 79 79 80 80 <!-- Inputs, Output & Processors --> 81 81 <di-opensubsonic></di-opensubsonic> ··· 86 86 <dtos-json id="output" output-selector="dop-indexed-db"></dtos-json> 87 87 88 88 <!-- Orchestrators --> 89 - <do-process-tracks 89 + <!--<do-process-tracks 90 90 input-selector="di-opensubsonic" 91 91 metadata-processor-selector="dp-metadata" 92 92 output-selector="#output" 93 - ></do-process-tracks> 93 + ></do-process-tracks>--> 94 94 95 95 <do-queue-tracks 96 96 input-selector="di-opensubsonic"