Experiment to rebuild Diffuse using web applets.
0
fork

Configure Feed

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

feat: bunch of art-controller improvements

+268 -131
+58 -29
src/pages/constituent/blur/artwork-controller/_applet.astro
··· 374 374 375 375 // Applet connections 376 376 const configurator = { 377 - input: await applet("/configurator/input"), 377 + input: applet("/configurator/input"), 378 378 }; 379 379 380 380 const engine = { ··· 383 383 }; 384 384 385 385 const orchestrator = { 386 - primary: await applet("/orchestrator/primary", { 386 + primary: applet("/orchestrator/primary", { 387 387 groupId: context.groupId, 388 388 }), 389 389 }; 390 390 391 391 const processor = { 392 - artwork: await applet("/processor/artwork"), 392 + artwork: applet("/processor/artwork"), 393 393 }; 394 394 395 395 //////////////////////////////////////////// ··· 464 464 465 465 // ORCHESTRATED 466 466 467 - context.settled().then(() => { 468 - if (context.isMainInstance()) 469 - orchestrator.primary.sendAction("monitor_audio_end", undefined, { timeoutDuration: 60000 }); 467 + context.settled().then(async () => { 468 + if (context.isMainInstance()) monitorAudioStuff(); 470 469 }); 471 470 471 + async function monitorAudioStuff() { 472 + (await orchestrator.primary).sendAction("monitorAudioEnd", undefined, { 473 + timeoutDuration: 60000, 474 + }); 475 + } 476 + 472 477 //////////////////////////////////////////// 473 478 // 🎢 QUEUE 474 479 //////////////////////////////////////////// ··· 478 483 reactive( 479 484 engine.queue, 480 485 (data) => data.now, 481 - (track) => setActiveTrack(track || undefined), 486 + (track) => { 487 + setActiveTrack(track || undefined); 488 + setProgress(0); 489 + }, 482 490 ); 483 491 484 492 // Changed artwork based on active queue item. 485 493 // (debounced) 486 494 487 - reactive(engine.queue, (data) => data.now, debounce(2000, changeArtwork)); 495 + reactive(engine.queue, (data) => data.now, debounce(1000, changeArtwork)); 488 496 489 497 async function changeArtwork() { 490 498 const track = engine.queue.data.now; ··· 494 502 return; 495 503 } 496 504 505 + const input = await configurator.input; 497 506 const cacheId = await trackArtworkCacheId(track); 498 - const art = await processor.artwork.sendAction( 507 + const urls = { 508 + get: await inputUrl(input, track.uri, "GET").then((a) => a?.url), 509 + head: await inputUrl(input, track.uri, "HEAD").then((a) => a?.url), 510 + }; 511 + 512 + const proc = await processor.artwork; 513 + const art = await proc.sendAction( 499 514 "artwork", 500 515 { 501 516 cacheId, 502 517 tags: track.tags, 503 - urls: { 504 - get: await inputUrl(configurator.input, track.uri, "GET").then((a) => a?.url), 505 - head: await inputUrl(configurator.input, track.uri, "HEAD").then((a) => a?.url), 506 - }, 518 + urls, 507 519 }, 508 520 { 509 521 timeoutDuration: 60000 * 5, ··· 518 530 519 531 // ORCHESTRATED 520 532 521 - context.settled().then(() => { 522 - if (context.isMainInstance()) 523 - orchestrator.primary.sendAction("monitor_active_queue_item", undefined, { 524 - timeoutDuration: 60000, 525 - }); 533 + context.settled().then(async () => { 534 + if (context.isMainInstance()) monitorQueueStuff(); 526 535 }); 536 + 537 + async function monitorQueueStuff() { 538 + (await orchestrator.primary).sendAction("monitorActiveQueueItem", undefined, { 539 + timeoutDuration: 60000, 540 + }); 541 + } 527 542 528 543 //////////////////////////////////////////// 529 544 // TRACKS ··· 532 547 // ORCHESTRATED 533 548 534 549 context.settled().then(() => { 535 - console.log("READY"); 536 - 537 550 if (isMainGroup() && context.isMainInstance()) { 538 - orchestrator.primary 539 - .sendAction("insert_tracks_into_queue", undefined, { 540 - timeoutDuration: 60000 * 5, 541 - }) 542 - .then(() => { 543 - orchestrator.primary.sendAction("process_inputs", undefined, { 544 - timeoutDuration: 60000 * 60, 545 - }); 546 - }); 551 + return monitorTracksStuff().then(processInputs); 547 552 } 548 553 }); 554 + 555 + async function monitorTracksStuff() { 556 + (await orchestrator.primary).sendAction("monitorTracks", undefined, { 557 + timeoutDuration: 60000 * 5, 558 + }); 559 + } 560 + 561 + async function processInputs() { 562 + (await orchestrator.primary).sendAction("processInputs", undefined, { 563 + timeoutDuration: 60000 * 60, 564 + }); 565 + } 566 + 567 + //////////////////////////////////////////// 568 + // 🚛 HYDRATE 569 + //////////////////////////////////////////// 570 + 571 + context.unloadHandler = async () => { 572 + if (context.isMainInstance()) { 573 + monitorAudioStuff(); 574 + monitorQueueStuff(); 575 + if (isMainGroup()) monitorTracksStuff(); 576 + } 577 + }; 549 578 550 579 //////////////////////////////////////////// 551 580 // UI
+27
src/pages/engine/audio/_applet.astro
··· 61 61 } 62 62 }); 63 63 64 + // Unload 65 + context.unloadHandler = async () => { 66 + await context.settled(); 67 + hydrateItems(); 68 + }; 69 + 70 + function hydrateItems() { 71 + const playingItem = context.data.isPlaying 72 + ? Object.values(context.data.items).find((item) => item.isPlaying) 73 + : undefined; 74 + 75 + render({ 76 + audio: Object.values(context.data.items).map((item: AudioState) => { 77 + return { 78 + id: item.id, 79 + isPreload: item.isPreload, 80 + mimeType: item.mimeType, 81 + progress: item.progress, 82 + url: item.url, 83 + }; 84 + }), 85 + play: playingItem ? { audioId: playingItem.id } : undefined, 86 + }); 87 + } 88 + 64 89 //////////////////////////////////////////// 65 90 // ACTIONS 66 91 //////////////////////////////////////////// ··· 187 212 loadingState: "loading", 188 213 isPlaying: true, 189 214 isPreload: item.isPreload ?? false, 215 + mimeType: item.mimeType, 190 216 progress: item.progress ?? 0, 217 + url: item.url, 191 218 }, 192 219 }; 193 220 }, {});
+2
src/pages/engine/audio/types.d.ts
··· 25 25 }; 26 26 isPlaying: boolean; 27 27 isPreload: boolean; 28 + mimeType?: string; 28 29 progress: number; 30 + url: string; 29 31 }
+6
src/pages/engine/queue/_applet.astro
··· 22 22 const groupId = context.groupId || "main"; 23 23 24 24 // Initial state 25 + context.data = { 26 + future: [], 27 + now: null, 28 + past: [], 29 + }; 30 + 25 31 context.data = await worker.data(groupId); 26 32 27 33 // Keep applet data with worker data in sync
+11 -15
src/pages/orchestrator/primary/_applet.astro
··· 19 19 20 20 // Applet connections 21 21 const configurator = { 22 - input: applet("/configurator/input", { context: self }), 22 + input: applet("/configurator/input"), 23 23 input_2: undefined as Applet | undefined, 24 24 output: applet<ManagedOutput>("/configurator/output"), 25 25 }; ··· 36 36 //////////////////////////////////////////// 37 37 // [ACTIONS] AUDIO ⭤ QUEUE 38 38 //////////////////////////////////////////// 39 - context.setActionHandler("monitor_active_queue_item", monitorActiveQueueItem); 40 - context.setActionHandler("monitor_audio_end", monitorAudioEnd); 39 + context.setActionHandler("monitorActiveQueueItem", monitorActiveQueueItem); 40 + context.setActionHandler("monitorAudioEnd", monitorAudioEnd); 41 41 42 42 async function monitorActiveQueueItem() { 43 43 await context.settled(); ··· 106 106 //////////////////////////////////////////// 107 107 // [ACTIONS] PROCESS 108 108 //////////////////////////////////////////// 109 - context.setActionHandler("process_inputs", processInputs); 109 + context.setActionHandler("processInputs", processInputs); 110 110 111 111 async function processInputs() { 112 112 if (context.data.isProcessing) return; ··· 190 190 //////////////////////////////////////////// 191 191 // [ACTIONS] QUEUE ⭤ TRACKS 192 192 //////////////////////////////////////////// 193 - context.setActionHandler("insert_tracks_into_queue", insertTracksIntoQueue); 193 + context.setActionHandler("monitorTracks", monitorTracks); 194 194 195 - async function insertTracksIntoQueue() { 195 + async function monitorTracks() { 196 196 await context.settled(); 197 197 198 - console.log("SETTLED"); 199 - 200 198 // Add tracks to the queue once the tracks have been loaded; 201 199 // and every time the collection changes. 202 200 ··· 205 203 const queue = await engine.queue; 206 204 207 205 await wait(output, (d) => d?.tracks.state === "loaded"); 208 - 209 - console.log("TRACKS LOADED"); 210 206 211 207 reactive( 212 208 output, ··· 218 214 { timeoutDuration: 60000 * 5, worker: true }, 219 215 ); 220 216 221 - console.log("CONSULTED"); 217 + // Available tracks 218 + let tracks: Track[] = []; 222 219 223 - // Available tracks 224 - const tracks = Object.values(groups).reduce((acc: Track[], value) => { 225 - if (value.available === false) return acc; 226 - return [...acc, ...value.tracks]; 220 + Object.values(groups).forEach((value) => { 221 + if (value.available === false) return; 222 + tracks = tracks.concat(value.tracks); 227 223 }, []); 228 224 229 225 // Set pool
+45 -19
src/scripts/applet/common.ts
··· 115 115 set data(data: T); 116 116 117 117 codec: Codec<T>; 118 + unloadHandler?: () => void; 118 119 119 120 isMainInstance(): boolean | null; 120 121 setActionHandler<H extends Function>(actionId: string, actionHandler: H): void; ··· 145 146 decode: (data: any) => data as DataType, 146 147 encode: (data: DataType) => data as any, 147 148 }; 148 - 149 - // Channel 150 - const channelContext = 151 - mode === "broadcast" 152 - ? broadcastChannel({ 153 - channelId, 154 - codec, 155 - instanceId, 156 - scope, 157 - }) 158 - : undefined; 159 149 160 150 // Context 161 151 const context: DiffuseApplet<DataType> = { ··· 202 192 }; 203 193 } 204 194 195 + // Channel 196 + const channelContext = 197 + mode === "broadcast" 198 + ? broadcastChannel<DataType>({ 199 + channelId, 200 + context, 201 + instanceId, 202 + scope, 203 + }) 204 + : undefined; 205 + 205 206 return context; 206 207 } 207 208 208 209 function broadcastChannel<DataType>({ 209 210 channelId, 210 - codec, 211 + context, 211 212 instanceId, 212 213 scope, 213 214 }: { 214 215 channelId: string; 215 - codec: Codec<DataType>; 216 + context: DiffuseApplet<DataType>; 216 217 instanceId: string; 217 218 scope: AppletScope<DataType>; 218 219 }) { ··· 234 235 channel.postMessage({ 235 236 type: "PONG", 236 237 instanceId: event.data.instanceId, 238 + originInstanceId: instanceId, 237 239 }); 238 240 239 241 if (isMain()) { 240 242 channel.postMessage({ 241 243 type: "data", 242 - data: codec.encode(scope.data), 244 + data: context.codec.encode(scope.data), 243 245 }); 244 246 } 245 247 break; ··· 252 254 break; 253 255 } 254 256 257 + case "UNLOADED": { 258 + if (!context.isMainInstance()) { 259 + // We need to wait until the other side is actually unloaded 🤷‍♀️ 260 + setTimeout(async () => { 261 + const promised = await makeMainPromise(); 262 + setIsMain(promised.isMain); 263 + context.unloadHandler?.(); 264 + }, 250); 265 + } 266 + break; 267 + } 268 + 255 269 case "action": { 256 270 if (isMain()) { 257 271 const result = await scope.actionHandlers[event.data.actionId]?.(...event.data.arguments); ··· 265 279 } 266 280 267 281 case "data": { 268 - scope.data = codec.decode(event.data.data); 282 + scope.data = context.codec.decode(event.data.data); 269 283 break; 270 284 } 271 285 } 272 286 }); 273 287 274 288 // Promise that fullfills whenever it figures out its the main instance or not. 275 - function makeMainPromise() { 289 + function makeMainPromise(timeoutDuration: number = 500) { 276 290 return new Promise<{ isMain: boolean }>((resolve) => { 277 291 const timeoutId = setTimeout(() => { 278 292 channel.removeEventListener("message", handler); 279 293 resolve({ isMain: true }); 280 - }, 5000); 294 + }, timeoutDuration); 281 295 282 296 const handler = (event: MessageEvent) => { 283 - if (event.data?.type === "PONG" || event.data?.type === "PING") { 297 + if ( 298 + (event.data?.type === "PONG" || event.data?.type === "PING") && 299 + event.data?.instanceId === instanceId 300 + ) { 284 301 clearTimeout(timeoutId); 285 302 channel.removeEventListener("message", handler); 286 303 resolve({ isMain: false }); ··· 305 322 if (isMain()) { 306 323 channel.postMessage({ 307 324 type: "data", 308 - data: codec.encode(event.data), 325 + data: context.codec.encode(event.data), 309 326 }); 310 327 } 311 328 }); ··· 351 368 352 369 scope.setActionHandler(actionId, handler); 353 370 }; 371 + 372 + // Before unload 373 + self.addEventListener("beforeunload", (event) => { 374 + if (context.isMainInstance()) { 375 + channel.postMessage({ 376 + type: "UNLOADED", 377 + }); 378 + } 379 + }); 354 380 355 381 // Fin 356 382 return {
+9 -4
src/scripts/common.ts
··· 96 96 tracks: Track[], 97 97 initial: Record<string, Track[]> = {}, 98 98 ): Record<string, Track[]> { 99 - return tracks.reduce((acc: Record<string, Track[]>, track: Track) => { 100 - const scheme = track.uri.split(":", 1)[0]; 101 - return { ...acc, [scheme]: [...(acc[scheme] || []), track] }; 102 - }, initial); 99 + const acc: Record<string, Track[]> = {}; 100 + 101 + tracks.forEach((track) => { 102 + const scheme = track.uri.substring(0, track.uri.indexOf(":")); 103 + acc[scheme] ??= []; 104 + acc[scheme].push(track); 105 + }); 106 + 107 + return acc; 103 108 } 104 109 105 110 export function inIframe() {
+11 -14
src/scripts/engine/queue/worker.ts
··· 125 125 const queue = state(groupId); 126 126 if (queue.future.length >= QUEUE_SIZE) return; 127 127 128 - let reducedPool = internal(groupId).pool.reduce( 129 - ({ past, pool }: { past: Set<string>; pool: Track[] }, track: Track) => { 130 - if (past.has(track.id)) 131 - return { 132 - past: past.difference(new Set(track.id)), 133 - pool, 134 - }; 128 + const pool: Track[] = []; 129 + 130 + let past = new Set(queue.past.map((t) => t.id)); 131 + let reducedPool = pool; 135 132 136 - return { 137 - past, 138 - pool: [...pool, track], 139 - }; 140 - }, 141 - { past: new Set(queue.past.map((t) => t.id)), pool: [] }, 142 - ).pool; 133 + internal(groupId).pool.forEach((track: Track) => { 134 + if (past.has(track.id)) { 135 + past = past.difference(new Set(track.id)); 136 + } else { 137 + pool.push(track); 138 + } 139 + }); 143 140 144 141 if (reducedPool.length === 0) { 145 142 reducedPool = internal(groupId).pool;
+11 -4
src/scripts/input/native-fs/common.ts
··· 20 20 }); 21 21 } 22 22 export function groupTracksByHandle(tracks: Track[]) { 23 - return tracks.reduce((acc: Record<string, { tracks: Track[] }>, track: Track) => { 23 + const acc: Record<string, { tracks: Track[] }> = {}; 24 + 25 + tracks.forEach((track: Track) => { 24 26 const id = trackHandleId(track); 25 27 if (!id) return acc; 26 28 27 - const obj = { tracks: acc[id] ? [...acc[id].tracks, track] : [track] }; 28 - return { ...acc, [id]: obj }; 29 - }, {}); 29 + if (acc[id]) { 30 + acc[id].tracks.push(track); 31 + } else { 32 + acc[id] = { tracks: [track] }; 33 + } 34 + }); 35 + 36 + return acc; 30 37 } 31 38 32 39 export function isSupported() {
+9 -4
src/scripts/input/native-fs/worker.ts
··· 80 80 ); 81 81 82 82 // Group tracks by handle id & index by track uri 83 - const cache = cachedTracks.reduce((acc: Record<string, Record<string, Track>>, track: Track) => { 83 + const cache: Record<string, Record<string, Track>> = {}; 84 + 85 + cachedTracks.forEach((track: Track) => { 84 86 const handleId = trackHandleId(track); 85 - if (!handleId) return acc; 87 + if (!handleId) return; 86 88 87 - return { ...acc, [handleId]: { ...(acc[handleId] || {}), [track.uri]: track } }; 88 - }, {}); 89 + cache[handleId] ??= {}; 90 + cache[handleId][track.uri] = track; 91 + }); 89 92 90 93 // Replace indexes in groups of which we have the handle. 91 94 // Keeping around tracks with handles we don't have access to, 92 95 // and removing tracks that are no longer available (for handles we do have access to). 96 + 97 + // TODO: Refactor to not use `reduce`, for performance. 93 98 const groups = processed.flat(1).reduce( 94 99 (acc, track) => { 95 100 const handleId = trackHandleId(track);
+21 -10
src/scripts/input/opensubsonic/common.ts
··· 61 61 } 62 62 63 63 export function groupTracksByServer(tracks: Track[]) { 64 - return tracks.reduce((acc: Record<string, { server: Server; tracks: Track[] }>, track: Track) => { 64 + const acc: Record<string, { server: Server; tracks: Track[] }> = {}; 65 + 66 + tracks.forEach((track: Track) => { 65 67 const parsed = parseURI(track.uri); 66 - if (!parsed) return acc; 68 + if (!parsed) return; 67 69 68 70 const id = serverId(parsed.server); 69 - const obj = { server: parsed.server, tracks: acc[id] ? [...acc[id].tracks, track] : [track] }; 71 + 72 + if (acc[id]) { 73 + acc[id].tracks.push(track); 74 + } else { 75 + acc[id] = { server: parsed.server, tracks: [track] }; 76 + } 77 + }); 70 78 71 - return { ...acc, [id]: obj }; 72 - }, {}); 79 + return acc; 73 80 } 74 81 75 82 export async function loadServers(): Promise<Record<string, Server>> { ··· 121 128 } 122 129 123 130 export function serversFromTracks(tracks: Track[]) { 124 - return tracks.reduce((acc: Record<string, Server>, track: Track) => { 131 + const acc: Record<string, Server> = {}; 132 + 133 + tracks.forEach((track: Track) => { 125 134 const parsed = parseURI(track.uri); 126 - if (!parsed) return acc; 135 + if (!parsed) return; 127 136 128 137 const id = serverId(parsed.server); 129 - if (acc[id]) return acc; 138 + if (acc[id]) return; 139 + 140 + acc[id] = parsed.server; 141 + }); 130 142 131 - return { ...acc, [id]: parsed.server }; 132 - }, {}); 143 + return acc; 133 144 } 134 145 135 146 export function serverId(server: Server) {
+8 -12
src/scripts/input/opensubsonic/worker.ts
··· 70 70 } 71 71 72 72 async function list(cachedTracks: Track[] = []) { 73 - const cache = cachedTracks.reduce((acc: Record<string, Record<string, Track>>, t: Track) => { 73 + const cache: Record<string, Record<string, Track>> = {}; 74 + 75 + cachedTracks.forEach((t: Track) => { 74 76 const parsed = parseURI(t.uri); 75 - if (!parsed || !parsed.path) return acc; 77 + if (!parsed || !parsed.path) return; 76 78 77 79 const sid = serverId(parsed?.server); 78 - const trk = { [URI.unescapeComponent(parsed.path)]: t }; 79 80 80 - return { ...acc, [sid]: acc[sid] ? { ...acc[sid], ...trk } : trk }; 81 - }, {}); 81 + cache[sid] ??= {}; 82 + cache[sid][URI.unescapeComponent(parsed.path)] = t; 83 + }); 82 84 83 85 async function search(client: SubsonicAPI, offset = 0): Promise<Child[]> { 84 86 const result = await client.search3({ ··· 161 163 id: songId, 162 164 format: "raw", 163 165 }) 164 - .then((a) => a.blob()) 165 - .then((blob) => URL.createObjectURL(blob)); 166 - 167 - // NOTE: 168 - // First idea was to get the URL for the download and use that instead. 169 - // Problem is, more often than not, servers don't allow for CORS Range requests, 170 - // so it's basically useless. 166 + .then((a) => a.url); 171 167 172 168 return { expiresAt: Infinity, url }; 173 169 }
+20 -9
src/scripts/input/s3/common.ts
··· 11 11 // 🛠️ 12 12 //////////////////////////////////////////// 13 13 export function bucketsFromTracks(tracks: Track[]) { 14 - return tracks.reduce((acc: Record<string, Bucket>, track: Track) => { 14 + const acc: Record<string, Bucket> = {}; 15 + 16 + tracks.forEach((track: Track) => { 15 17 const parsed = parseURI(track.uri); 16 - if (!parsed) return acc; 18 + if (!parsed) return; 17 19 18 20 const id = bucketId(parsed.bucket); 19 - if (acc[id]) return acc; 21 + if (acc[id]) return; 22 + 23 + acc[id] = parsed.bucket; 24 + }); 20 25 21 - return { ...acc, [id]: parsed.bucket }; 22 - }, {}); 26 + return acc; 23 27 } 24 28 25 29 export function bucketId(bucket: Bucket) { ··· 64 68 } 65 69 66 70 export function groupTracksByBucket(tracks: Track[]) { 67 - return tracks.reduce((acc: Record<string, { bucket: Bucket; tracks: Track[] }>, track: Track) => { 71 + const acc: Record<string, { bucket: Bucket; tracks: Track[] }> = {}; 72 + 73 + tracks.forEach((track: Track) => { 68 74 const parsed = parseURI(track.uri); 69 75 if (!parsed) return acc; 70 76 71 77 const id = bucketId(parsed.bucket); 72 - const obj = { bucket: parsed.bucket, tracks: acc[id] ? [...acc[id].tracks, track] : [track] }; 78 + 79 + if (acc[id]) { 80 + acc[id].tracks.push(track); 81 + } else { 82 + acc[id] = { bucket: parsed.bucket, tracks: [track] }; 83 + } 84 + }); 73 85 74 - return { ...acc, [id]: obj }; 75 - }, {}); 86 + return acc; 76 87 } 77 88 78 89 export async function loadBuckets(): Promise<Record<string, Bucket>> {
+10 -5
src/scripts/input/s3/worker.ts
··· 67 67 } 68 68 69 69 async function list(cachedTracks: Track[] = []) { 70 - const cache = cachedTracks.reduce((acc: Record<string, Record<string, Track>>, t: Track) => { 70 + const cache: Record<string, Record<string, Track>> = {}; 71 + 72 + cachedTracks.forEach((t: Track) => { 71 73 const parsed = parseURI(t.uri); 72 - if (!parsed) return acc; 74 + if (!parsed) return; 73 75 74 76 const bid = bucketId(parsed?.bucket); 75 - const trk = { [parsed.path]: t }; 76 77 77 - return { ...acc, [bid]: acc[bid] ? { ...acc[bid], ...trk } : trk }; 78 - }, {}); 78 + if (cache[bid]) { 79 + cache[bid][parsed.path] = t; 80 + } else { 81 + cache[bid] = { [parsed.path]: t }; 82 + } 83 + }); 79 84 80 85 const buckets = await loadBuckets(); 81 86 const promises = Object.values(buckets).map(async (bucket) => {
+8 -2
src/scripts/processor/artwork/worker.ts
··· 2 2 import * as IDB from "idb-keyval"; 3 3 4 4 import type { Artwork, ArtworkRequest } from "./types"; 5 + import type { Extraction } from "../metadata/types"; 5 6 import { provide } from "@scripts/common"; 6 7 import { IDB_ARTWORK_PREFIX } from "./constants"; 7 8 import { musicMetadataTags } from "../metadata/common"; ··· 170 171 let art: Artwork[] = []; 171 172 172 173 // Get metadata + possible artwork from file metadata 173 - const meta = await musicMetadataTags({ ...req, includeArtwork: true }); 174 - if (!req.tags) req.tags = meta.tags; 174 + const meta = await musicMetadataTags({ ...req, includeArtwork: true }).catch((err) => { 175 + console.error("music-metadata error", err); 176 + const extraction: Extraction = {}; 177 + return extraction; 178 + }); 179 + 180 + if (!req.tags && meta.tags) req.tags = meta.tags; 175 181 176 182 // Add artwork from metadata 177 183 const fromMeta =
+7 -1
src/scripts/processor/metadata/common.ts
··· 1 1 import { parseBlob, parseFromTokenizer, parseWebStream } from "music-metadata"; 2 - import { contentType } from "@std/media-types"; 3 2 import * as URI from "uri-js"; 4 3 import * as HTTP_TOKENIZER from "@tokenizer/http"; 5 4 import * as RANGE_TOKENIZER from "@tokenizer/range"; ··· 32 31 } else if (urls) { 33 32 const httpClient = new HTTP_TOKENIZER.HttpClient(urls.head, { resolveUrl: false }); 34 33 httpClient.resolvedUrl = urls.get; 34 + const getHeadInfo = httpClient.getHeadInfo; 35 + 36 + // FUCKAROUND: Not sure of the downsides of this 37 + httpClient.getHeadInfo = async () => { 38 + const info = await getHeadInfo.call(httpClient); 39 + return { ...info, acceptPartialRequests: true }; 40 + }; 35 41 36 42 const tokenizer = await RANGE_TOKENIZER.tokenizer(httpClient); 37 43
+5 -3
src/scripts/theme/webamp/index.ts
··· 95 95 ); 96 96 97 97 // Available tracks 98 - const tracks = Object.values(groups).reduce((acc: Track[], value) => { 99 - if (value.available === false) return acc; 100 - return [...acc, ...value.tracks]; 98 + let tracks: Track[] = []; 99 + 100 + Object.values(groups).forEach((value) => { 101 + if (value.available === false) return; 102 + tracks = tracks.concat(value.tracks); 101 103 }, []); 102 104 103 105 return tracks.map((track) => {