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,
);
////////////////////////////////////////////