From d7f65636aa7d353a23be62071d6bb7c0b4b8cf16 Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Mon, 23 Jun 2025 23:59:22 +0200 Subject: [PATCH] feat: only add available tracks to the queue --- astro.config.js | 3 +- deno.lock | 1 - package.json | 1 - src/pages/configurator/input/_applet.astro | 59 +++++-- src/pages/configurator/input/_manifest.json | 7 + src/pages/core/types.d.ts | 18 ++ src/pages/engine/queue/_applet.astro | 4 +- src/pages/input/native-fs/_applet.astro | 9 +- src/pages/input/opensubsonic/_applet.astro | 9 +- src/pages/input/s3/_applet.astro | 9 +- .../orchestrator/input-cache/_applet.astro | 3 - .../orchestrator/queue-tracks/_applet.astro | 21 ++- src/pages/output/indexed-db/_applet.astro | 4 +- src/pages/output/native-fs/_applet.astro | 4 +- src/pages/processor/artwork/_applet.astro | 4 +- src/pages/processor/metadata/_applet.astro | 4 +- src/scripts/common.ts | 14 +- src/scripts/engine/queue/worker.ts | 4 + src/scripts/input/native-fs/common.ts | 9 + src/scripts/input/native-fs/worker.ts | 37 +++- src/scripts/input/opensubsonic/common.ts | 131 +++++++++++++- src/scripts/input/opensubsonic/worker.ts | 161 ++++++------------ src/scripts/input/s3/common.ts | 36 +++- src/scripts/input/s3/worker.ts | 75 +++++--- src/scripts/processor/artwork/worker.ts | 2 +- 25 files changed, 455 insertions(+), 174 deletions(-) diff --git a/astro.config.js b/astro.config.js index b853566..e3f7287 100644 --- a/astro.config.js +++ b/astro.config.js @@ -1,12 +1,11 @@ import { defineConfig } from "astro/config"; import scope from "astro-scope"; import wasm from "vite-plugin-wasm"; -import worker from "@astropub/worker"; import purgecss from "astro-purgecss"; export default defineConfig({ - integrations: [scope(), purgecss(), worker()], + integrations: [scope(), purgecss()], build: { inlineStylesheets: "never", }, diff --git a/deno.lock b/deno.lock index 5858e45..601b945 100644 --- a/deno.lock +++ b/deno.lock @@ -22,7 +22,6 @@ "packageJson": { "dependencies": [ "npm:98.css@~0.1.21", - "npm:@astropub/worker@0.2", "npm:@automerge/automerge@^3.0.0-beta.0", "npm:@js-temporal/polyfill@~0.5.1", "npm:@jsr/bradenmacdonald__s3-lite-client@0.9", diff --git a/package.json b/package.json index b62706e..76c1834 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,5 @@ { "dependencies": { - "@astropub/worker": "^0.2.0", "@automerge/automerge": "^3.0.0-beta.0", "@bradenmacdonald/s3-lite-client": "npm:@jsr/bradenmacdonald__s3-lite-client@^0.9.0", "@js-temporal/polyfill": "^0.5.1", diff --git a/src/pages/configurator/input/_applet.astro b/src/pages/configurator/input/_applet.astro index 7b80974..7663149 100644 --- a/src/pages/configurator/input/_applet.astro +++ b/src/pages/configurator/input/_applet.astro @@ -32,9 +32,12 @@ diff --git a/src/pages/output/indexed-db/_applet.astro b/src/pages/output/indexed-db/_applet.astro index d849ec1..a83c901 100644 --- a/src/pages/output/indexed-db/_applet.astro +++ b/src/pages/output/indexed-db/_applet.astro @@ -9,7 +9,9 @@ // SETUP //////////////////////////////////////////// const worker = endpoint( - new SharedWorker("../../../scripts/output/indexed-db/worker", { type: "module" }).port, + new SharedWorker(new URL("../../../scripts/output/indexed-db/worker", import.meta.url), { + type: "module", + }).port, ); // Register applet diff --git a/src/pages/output/native-fs/_applet.astro b/src/pages/output/native-fs/_applet.astro index 82b1a83..863dd59 100644 --- a/src/pages/output/native-fs/_applet.astro +++ b/src/pages/output/native-fs/_applet.astro @@ -13,7 +13,9 @@ // SETUP //////////////////////////////////////////// const worker = endpoint( - new SharedWorker("../../../scripts/output/native-fs/worker", { type: "module" }).port, + new SharedWorker(new URL("../../../scripts/output/native-fs/worker", import.meta.url), { + type: "module", + }).port, ); // Register applet diff --git a/src/pages/processor/artwork/_applet.astro b/src/pages/processor/artwork/_applet.astro index 4b9fc68..d6751e6 100644 --- a/src/pages/processor/artwork/_applet.astro +++ b/src/pages/processor/artwork/_applet.astro @@ -8,7 +8,9 @@ // SETUP //////////////////////////////////////////// const worker = endpoint( - new SharedWorker("../../../scripts/processor/artwork/worker", { type: "module" }).port, + new SharedWorker(new URL("../../../scripts/processor/artwork/worker", import.meta.url), { + type: "module", + }).port, ); // Register diff --git a/src/pages/processor/metadata/_applet.astro b/src/pages/processor/metadata/_applet.astro index f9ec33f..47e0b03 100644 --- a/src/pages/processor/metadata/_applet.astro +++ b/src/pages/processor/metadata/_applet.astro @@ -7,7 +7,9 @@ // SETUP //////////////////////////////////////////// const worker = endpoint( - new SharedWorker("../../../scripts/processor/metadata/worker", { type: "module" }).port, + new SharedWorker(new URL("../../../scripts/processor/metadata/worker", import.meta.url), { + type: "module", + }).port, ); // Register applet diff --git a/src/scripts/common.ts b/src/scripts/common.ts index 5883ffb..6655840 100644 --- a/src/scripts/common.ts +++ b/src/scripts/common.ts @@ -47,7 +47,7 @@ export function comparable(value: unknown) { export function endpoint>(port: MessagePort) { const e = createEndpoint(port); - if ("start" in port) port.start(); + port.start(); return e; } @@ -55,12 +55,22 @@ export function expose>(actions: T): T { (self as unknown as SharedWorkerGlobalScope).onconnect = (event: MessageEvent) => { const port = event.ports[0]; createEndpoint(port).expose(actions); - if ("start" in port) port.start(); + port.start(); }; return actions; } +export function groupTracksPerScheme( + tracks: Track[], + initial: Record = {}, +): Record { + return tracks.reduce((acc: Record, track: Track) => { + const scheme = track.uri.split(":", 1)[0]; + return { ...acc, [scheme]: [...(acc[scheme] || []), track] }; + }, initial); +} + export function inIframe() { return window.self !== window.top; } diff --git a/src/scripts/engine/queue/worker.ts b/src/scripts/engine/queue/worker.ts index 517b409..4011720 100644 --- a/src/scripts/engine/queue/worker.ts +++ b/src/scripts/engine/queue/worker.ts @@ -2,6 +2,10 @@ import type { Track } from "@applets/core/types.js"; import { expose } from "../../../scripts/common.ts"; import type { State } from "./types"; +self.addEventListener("message", (event) => { + console.log("QUEUE", event.data); +}); + //////////////////////////////////////////// // ACTIONS //////////////////////////////////////////// diff --git a/src/scripts/input/native-fs/common.ts b/src/scripts/input/native-fs/common.ts index c7aa7c4..4818c93 100644 --- a/src/scripts/input/native-fs/common.ts +++ b/src/scripts/input/native-fs/common.ts @@ -19,6 +19,15 @@ export async function fetchHandlesList() { return { id, handle }; }); } +export function groupTracksByHandle(tracks: Track[]) { + return tracks.reduce((acc: Record, track: Track) => { + const id = trackHandleId(track); + if (!id) return acc; + + const obj = { tracks: acc[id] ? [...acc[id].tracks, track] : [track] }; + return { ...acc, [id]: obj }; + }, {}); +} export function isSupported() { return !!(globalThis as any).showDirectoryPicker; diff --git a/src/scripts/input/native-fs/worker.ts b/src/scripts/input/native-fs/worker.ts index f372aec..8db8618 100644 --- a/src/scripts/input/native-fs/worker.ts +++ b/src/scripts/input/native-fs/worker.ts @@ -1,8 +1,14 @@ import * as URI from "uri-js"; -import type { Track } from "@applets/core/types.d.ts"; +import type { Consult, ConsultGrouping, GroupConsult, Track } from "@applets/core/types.d.ts"; import { SCHEME } from "./constants"; -import { fetchHandles, fetchHandlesList, recursiveList, trackHandleId } from "./common"; +import { + fetchHandles, + fetchHandlesList, + groupTracksByHandle, + recursiveList, + trackHandleId, +} from "./common"; import { expose } from "@scripts/common"; //////////////////////////////////////////// @@ -11,6 +17,7 @@ import { expose } from "@scripts/common"; const actions = expose({ consult, contextualize, + groupConsult, list, resolve, }); @@ -19,24 +26,44 @@ export type Actions = typeof actions; // Actions -export async function consult(fileUriOrScheme: string) { +export async function consult(fileUriOrScheme: string): Promise { if (!self.FileSystemDirectoryHandle) { return { supported: false, reason: "File System Access API is not supported" }; } if (!fileUriOrScheme.includes(":")) { if (fileUriOrScheme !== SCHEME) return { supported: false, reason: "Scheme does not match" }; - return { supported: true }; + return { supported: true, consult: "undetermined" }; } const handles = await fetchHandles(); const uri = URI.parse(fileUriOrScheme); if (uri.scheme !== SCHEME) return { supported: false, reason: "Scheme does not match" }; - return { supported: true, consultation: uri.host && !!handles[uri.host] }; + return { supported: true, consult: uri.host && !!handles[uri.host] ? true : false }; } export async function contextualize(cachedTracks: Track[]) {} +async function groupConsult(tracks: Track[]): Promise { + const groups = groupTracksByHandle(tracks); + const handles = await fetchHandles(); + + const promises = Object.entries(groups).map(async ([handleId, { tracks }]) => { + const handle = handles[handleId]; + const grouping: ConsultGrouping = handle + ? { available: true, tracks } + : { available: false, reason: "Handle not available" }; + + return { + key: URI.serialize({ scheme: SCHEME, host: handleId }), + grouping, + }; + }); + + const entries = (await Promise.all(promises)).map((entry) => [entry.key, entry.grouping]); + return Object.fromEntries(entries); +} + export async function list(cachedTracks: Track[] = []) { const handles = await fetchHandlesList(); diff --git a/src/scripts/input/opensubsonic/common.ts b/src/scripts/input/opensubsonic/common.ts index 7dc4a5c..1f585b2 100644 --- a/src/scripts/input/opensubsonic/common.ts +++ b/src/scripts/input/opensubsonic/common.ts @@ -1,18 +1,143 @@ +import { SubsonicAPI, type Child } from "subsonic-api"; import * as IDB from "idb-keyval"; +import * as URI from "uri-js"; +import QS from "query-string"; import type { Server } from "./types"; -import { IDB_SERVERS } from "./constants"; +import { IDB_SERVERS, SCHEME } from "./constants"; +import type { Track } from "@applets/core/types"; + +//////////////////////////////////////////// +// 🛠️ +//////////////////////////////////////////// +export function autoTypeToTrackKind(type: Child["type"]): Track["kind"] { + switch (type?.toLowerCase()) { + case "audiobook": + return "audiobook"; + + case "music": + return "music"; + + case "podcast": + return "podcast"; + + default: + return "miscellaneous"; + } +} + +export function buildURI(server: Server, args: { songId: string; path?: string }) { + return URI.serialize({ + scheme: SCHEME, + userinfo: server.apiKey + ? URI.escapeComponent(server.apiKey) + : `${URI.escapeComponent(server.username || "")}:${URI.escapeComponent(server.password || "")}`, + host: server.host.replace(/^https?:\/\//, ""), + path: args.path, + query: QS.stringify({ + songId: args.songId, + tls: server.tls ? "t" : "f", + }), + }); +} + +export async function consultServer(server: Server) { + const client = createClient(server); + const resp = await client.ping().catch(() => undefined); + + return resp?.status?.toLowerCase() === "ok"; +} + +export function createClient(server: Server) { + return new SubsonicAPI({ + url: `http${server.tls ? "s" : ""}://${server.host}`, + auth: server.apiKey + ? { apiKey: URI.unescapeComponent(server.apiKey) } + : { + username: URI.unescapeComponent(server.username || ""), + password: URI.unescapeComponent(server.password || ""), + }, + }); +} + +export function groupTracksByServer(tracks: Track[]) { + return tracks.reduce((acc: Record, track: Track) => { + const parsed = parseURI(track.uri); + if (!parsed) return acc; + + const id = serverId(parsed.server); + const obj = { server: parsed.server, tracks: acc[id] ? [...acc[id].tracks, track] : [track] }; + + return { ...acc, [id]: obj }; + }, {}); +} export async function loadServers(): Promise> { const i = await IDB.get(IDB_SERVERS); return i ? i : {}; } +export function parseURI( + uriString: string, +): { path: string | undefined; server: Server; songId: string | undefined } | undefined { + const uri = URI.parse(uriString); + if (uri.scheme !== SCHEME) return undefined; + if (!uri.host) return undefined; + + let apiKey: string | undefined = undefined; + let username: string | undefined = undefined; + let password: string | undefined = undefined; + + if (uri.userinfo?.includes(":")) { + // Username + Password + const [u, p] = uri.userinfo.split(":"); + username = u; + password = p; + if (!username || !password) return undefined; + } else { + // API key + apiKey = uri.userinfo; + if (!apiKey) return undefined; + } + + const qs = QS.parse(uri.query || ""); + + const server = { + apiKey, + host: uri.port ? `${uri.host}:${uri.port}` : uri.host, + password, + tls: qs.tls === "f" ? false : true, + username, + }; + + const path = uri.path; + const songId = typeof qs.songId === "string" ? qs.songId : undefined; + + return { path, server, songId }; +} + export async function saveServers(items: Record) { await IDB.set(IDB_SERVERS, items); } +export function serversFromTracks(tracks: Track[]) { + return tracks.reduce((acc: Record, track: Track) => { + const parsed = parseURI(track.uri); + if (!parsed) return acc; + + const id = serverId(parsed.server); + if (acc[id]) return acc; + + return { ...acc, [id]: parsed.server }; + }, {}); +} + export function serverId(server: Server) { - if (server.apiKey) return `${server.apiKey}@${server.host}`; - return `${server.username}:${server.password}@${server.host}`; + const parts = { + host: server.host, + query: `tls=${server.tls ? "t" : "f"}`, + }; + + if (server.apiKey) return URI.serialize({ ...parts, userinfo: server.apiKey }); + return URI.serialize({ ...parts, userinfo: `${server.username}:${server.password}` }); } diff --git a/src/scripts/input/opensubsonic/worker.ts b/src/scripts/input/opensubsonic/worker.ts index 98b107b..e6246a6 100644 --- a/src/scripts/input/opensubsonic/worker.ts +++ b/src/scripts/input/opensubsonic/worker.ts @@ -2,10 +2,19 @@ import { SubsonicAPI, type Child } from "subsonic-api"; import * as URI from "uri-js"; import QS from "query-string"; -import type { Track } from "@applets/core/types.d.ts"; -import type { Server } from "./types.d.ts"; +import type { Consult, ConsultGrouping, GroupConsult, Track } from "@applets/core/types.d.ts"; import { SCHEME } from "./constants.ts"; -import { loadServers, serverId } from "./common.ts"; +import { + autoTypeToTrackKind, + buildURI, + consultServer, + createClient, + groupTracksByServer, + loadServers, + parseURI, + serverId, + serversFromTracks, +} from "./common.ts"; import { expose } from "../../../scripts/common.ts"; //////////////////////////////////////////// @@ -14,6 +23,7 @@ import { expose } from "../../../scripts/common.ts"; const actions = expose({ consult, contextualize, + groupConsult, list, resolve, }); @@ -22,22 +32,52 @@ export type Actions = typeof actions; // Actions -async function consult(fileUriOrScheme: string) { - // TODO: Check if server is available + CORS works? - return { supported: true }; +async function consult(fileUriOrScheme: string): Promise { + if (!fileUriOrScheme.includes(":")) return { supported: true, consult: "undetermined" }; + + const parsed = parseURI(fileUriOrScheme); + if (!parsed) return { supported: true, consult: "undetermined" }; + + const consult = await consultServer(parsed.server); + return { supported: true, consult }; } async function contextualize(tracks: Track[]) { return serversFromTracks(tracks); } +async function groupConsult(tracks: Track[]): Promise { + const groups = groupTracksByServer(tracks); + + const promises = Object.entries(groups).map(async ([serverId, { server, tracks }]) => { + const available = await consultServer(server); + const grouping: ConsultGrouping = available + ? { available, tracks } + : { available, reason: "Server ping failed" }; + + return { + key: `${SCHEME}:${serverId}`, + grouping, + }; + }); + + const entries = (await Promise.all(promises)).map((entry) => [entry.key, entry.grouping]); + return Object.fromEntries(entries); +} + async function list(cachedTracks: Track[] = []) { - const cache = cachedTracks.reduce((acc: Record, t: Track) => { - const uri = URI.parse(t.uri); - if (!uri.path) return acc; - return { ...acc, [URI.unescapeComponent(uri.path)]: t }; + const cache = cachedTracks.reduce((acc: Record>, t: Track) => { + const parsed = parseURI(t.uri); + if (!parsed || !parsed.path) return acc; + + const bid = serverId(parsed?.server); + const trk = { [parsed.path]: t }; + + return { ...acc, [bid]: acc[bid] ? { ...acc[bid], ...trk } : trk }; }, {}); + // TODO + async function search(client: SubsonicAPI, offset = 0): Promise { const result = await client.search3({ query: "", @@ -95,14 +135,11 @@ async function list(cachedTracks: Track[] = []) { } async function resolve({ uri }: { method: string; uri: string }) { - const server = parseURI(uri); - if (!server) return undefined; - - const client = createClient(server); - const parsedURI = URI.parse(uri); - const qs = QS.parse(parsedURI.query || ""); + const parsed = parseURI(uri); + if (!parsed) return undefined; - const songId = typeof qs.songId === "string" ? qs.songId : undefined; + const client = createClient(parsed.server); + const songId = parsed.songId; if (!songId) return undefined; // TODO: @@ -124,93 +161,3 @@ async function resolve({ uri }: { method: string; uri: string }) { return { expiresAt: Infinity, url }; } - -//////////////////////////////////////////// -// 🛠️ -//////////////////////////////////////////// -function autoTypeToTrackKind(type: Child["type"]): Track["kind"] { - switch (type?.toLowerCase()) { - case "audiobook": - return "audiobook"; - - case "music": - return "music"; - - case "podcast": - return "podcast"; - - default: - return "miscellaneous"; - } -} - -function buildURI(server: Server, args: { songId: string; path?: string }) { - return URI.serialize({ - scheme: SCHEME, - userinfo: server.apiKey - ? URI.escapeComponent(server.apiKey) - : `${URI.escapeComponent(server.username || "")}:${URI.escapeComponent(server.password || "")}`, - host: server.host.replace(/^https?:\/\//, ""), - path: args.path, - query: QS.stringify({ - songId: args.songId, - tls: server.tls ? "t" : "f", - }), - }); -} - -function createClient(server: Server) { - return new SubsonicAPI({ - url: `http${server.tls ? "s" : ""}://${server.host}`, - auth: server.apiKey - ? { apiKey: URI.unescapeComponent(server.apiKey) } - : { - username: URI.unescapeComponent(server.username || ""), - password: URI.unescapeComponent(server.password || ""), - }, - }); -} - -function parseURI(uriString: string): Server | undefined { - const uri = URI.parse(uriString); - if (uri.scheme !== SCHEME) return undefined; - if (!uri.host) return undefined; - - let apiKey: string | undefined = undefined; - let username: string | undefined = undefined; - let password: string | undefined = undefined; - - if (uri.userinfo?.includes(":")) { - // Username + Password - const [u, p] = uri.userinfo.split(":"); - username = u; - password = p; - if (!username || !password) return undefined; - } else { - // API key - apiKey = uri.userinfo; - if (!apiKey) return undefined; - } - - const qs = QS.parse(uri.query || ""); - - return { - apiKey, - host: uri.port ? `${uri.host}:${uri.port}` : uri.host, - password, - tls: qs.tls === "f" ? false : true, - username, - }; -} - -function serversFromTracks(tracks: Track[]) { - return tracks.reduce((acc: Record, track: Track) => { - const server = parseURI(track.uri); - if (!server) return acc; - - const id = serverId(server); - if (acc[id]) return acc; - - return { ...acc, [id]: server }; - }, {}); -} diff --git a/src/scripts/input/s3/common.ts b/src/scripts/input/s3/common.ts index 0b12055..1eb84f0 100644 --- a/src/scripts/input/s3/common.ts +++ b/src/scripts/input/s3/common.ts @@ -12,13 +12,13 @@ import type { Bucket } from "./types"; //////////////////////////////////////////// export function bucketsFromTracks(tracks: Track[]) { return tracks.reduce((acc: Record, track: Track) => { - const bucket = parseURI(track.uri); - if (!bucket) return acc; + const parsed = parseURI(track.uri); + if (!parsed) return acc; - const id = bucketId(bucket); + const id = bucketId(parsed.bucket); if (acc[id]) return acc; - return { ...acc, [id]: bucket }; + return { ...acc, [id]: parsed.bucket }; }, {}); } @@ -40,6 +40,11 @@ export function buildURI(bucket: Bucket, path: string) { }); } +export async function consultBucket(bucket: Bucket) { + const client = createClient(bucket); + return await client.bucketExists(bucket.bucketName); +} + export function createClient(bucket: Bucket) { return new S3Client({ bucket: bucket.bucketName, @@ -58,12 +63,24 @@ export function encodeAwsUriComponent(a: string) { ); } +export function groupTracksByBucket(tracks: Track[]) { + return tracks.reduce((acc: Record, track: Track) => { + const parsed = parseURI(track.uri); + if (!parsed) return acc; + + const id = bucketId(parsed.bucket); + const obj = { bucket: parsed.bucket, tracks: acc[id] ? [...acc[id].tracks, track] : [track] }; + + return { ...acc, [id]: obj }; + }, {}); +} + export async function loadBuckets(): Promise> { const i = await IDB.get(IDB_BUCKETS); return i ? i : {}; } -export function parseURI(uriString: string): Bucket | undefined { +export function parseURI(uriString: string): { bucket: Bucket; path: string } | undefined { const uri = URI.parse(uriString); if (uri.scheme !== SCHEME) return undefined; if (!uri.host) return undefined; @@ -73,7 +90,7 @@ export function parseURI(uriString: string): Bucket | undefined { const qs = QS.parse(uri.query || ""); - return { + const bucket = { accessKey, bucketName: typeof qs.bucketName === "string" ? qs.bucketName : "", host: uri.host, @@ -81,6 +98,13 @@ export function parseURI(uriString: string): Bucket | undefined { region: typeof qs.region === "string" ? qs.region : "", secretKey, }; + + const path = (bucket.path.replace(/\/$/, "") + URI.unescapeComponent(uri.path || "")).replace( + /^\//, + "", + ); + + return { bucket, path }; } export async function saveBuckets(items: Record) { diff --git a/src/scripts/input/s3/worker.ts b/src/scripts/input/s3/worker.ts index e0b1720..255e412 100644 --- a/src/scripts/input/s3/worker.ts +++ b/src/scripts/input/s3/worker.ts @@ -1,9 +1,19 @@ import * as URI from "uri-js"; -import type { Track } from "@applets/core/types.d.ts"; +import type { Consult, ConsultGrouping, GroupConsult, Track } from "@applets/core/types.d.ts"; import { isAudioFile } from "@scripts/input/common"; -import { bucketsFromTracks, buildURI, createClient, loadBuckets, parseURI } from "./common"; +import { + bucketId, + bucketsFromTracks, + buildURI, + consultBucket, + createClient, + groupTracksByBucket, + loadBuckets, + parseURI, +} from "./common"; import { expose } from "@scripts/common"; +import { SCHEME } from "./constants"; //////////////////////////////////////////// // ACTIONS @@ -11,6 +21,7 @@ import { expose } from "@scripts/common"; const actions = expose({ consult, contextualize, + groupConsult, list, resolve, }); @@ -19,28 +30,54 @@ export type Actions = typeof actions; // Actions -async function consult(fileUriOrScheme: string) { - if (!navigator.onLine) - return { supported: false, reason: "Internet connection is not available" }; +async function consult(fileUriOrScheme: string): Promise { + if (!fileUriOrScheme.includes(":")) return { supported: true, consult: "undetermined" }; - // TODO: Check if bucket is available + CORS works? - return { supported: true }; + const parsed = parseURI(fileUriOrScheme); + if (!parsed) return { supported: true, consult: "undetermined" }; + + const consult = await consultBucket(parsed.bucket); + return { supported: true, consult }; } async function contextualize(tracks: Track[]) { return bucketsFromTracks(tracks); } +async function groupConsult(tracks: Track[]): Promise { + const groups = groupTracksByBucket(tracks); + + const promises = Object.entries(groups).map(async ([bucketId, { bucket, tracks }]) => { + const available = await consultBucket(bucket); + const grouping: ConsultGrouping = available + ? { available, tracks } + : { available, reason: "Bucket unavailable" }; + + return { + key: `${SCHEME}:${bucketId}`, + grouping, + }; + }); + + const entries = (await Promise.all(promises)).map((entry) => [entry.key, entry.grouping]); + return Object.fromEntries(entries); +} + async function list(cachedTracks: Track[] = []) { - const cache = cachedTracks.reduce((acc: Record, t: Track) => { - const uri = URI.parse(t.uri); - if (!uri.path) return acc; - return { ...acc, [URI.unescapeComponent(uri.path)]: t }; + const cache = cachedTracks.reduce((acc: Record>, t: Track) => { + const parsed = parseURI(t.uri); + if (!parsed) return acc; + + const bid = bucketId(parsed?.bucket); + const trk = { [parsed.path]: t }; + + return { ...acc, [bid]: acc[bid] ? { ...acc[bid], ...trk } : trk }; }, {}); const buckets = await loadBuckets(); const promises = Object.values(buckets).map(async (bucket) => { const client = createClient(bucket); + const bid = bucketId(bucket); const list = await Array.fromAsync( client.listObjects({ @@ -51,7 +88,7 @@ async function list(cachedTracks: Track[] = []) { return list .filter((l) => isAudioFile(l.key)) .map((l) => { - const cachedTrack = cache[`/${l.key}`]; + const cachedTrack = cache[bid][l.key]; const id = cachedTrack?.id || crypto.randomUUID(); const stats = cachedTrack?.stats; @@ -72,18 +109,14 @@ async function list(cachedTracks: Track[] = []) { } async function resolve({ method, uri }: { method: string; uri: string }) { - const bucket = parseURI(uri); - if (!bucket) return undefined; - - const client = createClient(bucket); - const parsedURI = URI.parse(uri); - const path = ( - bucket.path.replace(/\/$/, "") + URI.unescapeComponent(parsedURI.path || "") - ).replace(/^\//, ""); + const parsed = parseURI(uri); + if (!parsed) return undefined; const expiresInSeconds = 60 * 60 * 24 * 7; // 7 days const expiresAtSeconds = Math.round(Date.now() / 1000) + expiresInSeconds; - const url = await client.getPresignedUrl(method.toUpperCase() as any, path); + + const client = createClient(parsed.bucket); + const url = await client.getPresignedUrl(method.toUpperCase() as any, parsed.path); return { expiresAt: expiresAtSeconds, url }; } diff --git a/src/scripts/processor/artwork/worker.ts b/src/scripts/processor/artwork/worker.ts index e7206ea..01f98a3 100644 --- a/src/scripts/processor/artwork/worker.ts +++ b/src/scripts/processor/artwork/worker.ts @@ -11,7 +11,7 @@ let queue: ArtworkRequest[] = []; // Metadata worker const metadataWorker = endpoint( - new SharedWorker("../metadata/worker", { type: "module" }).port, + new SharedWorker(new URL("../metadata/worker", import.meta.url), { type: "module" }).port, ); //////////////////////////////////////////// -- 2.51.2