···11<script>
22 import type { Actions } from "@scripts/output/native-fs/worker";
33 import * as IDB from "idb-keyval";
44- import "wicg-file-system-access";
44+ import type * as FSA from "wicg-file-system-access";
5566 import type { ManagedOutput, Track } from "@applets/core/types";
77 import { register } from "@scripts/applet/common";
+11-157
src/pages/processor/artwork/_applet.astro
···11<script>
22- import type { IPicture } from "music-metadata";
33- import * as IDB from "idb-keyval";
44-55- import { applet, register } from "@scripts/applet/common";
66- import type { ArtworkRequest, State, Artwork } from "./types.d.ts";
77- import type { Extraction } from "../metadata/types.d.ts";
22+ import type { Actions } from "@scripts/processor/artwork/worker";
33+ import type { ArtworkRequest } from "./types.d.ts";
44+ import { register } from "@scripts/applet/common";
55+ import { endpoint } from "@scripts/common";
8697 ////////////////////////////////////////////
108 // SETUP
119 ////////////////////////////////////////////
1212- const IDB_PREFIX = "@applets/processor/artwork";
1313- const IDB_ARTWORK_PREFIX = `${IDB_PREFIX}/artwork`;
1414-1515- const context = register<State>();
1616- let queue: ArtworkRequest[] = [];
1717-1818- // Initial data
1919- context.data = {
2020- artwork: {},
2121- };
2222-2323- // Applet connections
2424- // TODO: Ideally only configurator, orchestrator and UI applets have nested applets.
2525- // Can we find a way to remove this dependency?
2626- const processor = {
2727- metadata: applet("../../processor/metadata"),
2828- };
1010+ const worker = endpoint<Actions>(
1111+ new Worker("../../../scripts/processor/artwork/worker", { type: "module" }),
1212+ );
29133030- // Load already-downloaded artwork
3131- IDB.keys().then(async (keys) => {
3232- const artworkKeys = keys.filter((k) => k.toString().startsWith(`${IDB_ARTWORK_PREFIX}/`));
3333-3434- artworkKeys.forEach(async (key) => {
3535- if (typeof key !== "string") return;
3636-3737- const artwork = await IDB.get(key);
3838- const cacheId = key.split("/").reverse()[0];
3939-4040- context.data.artwork[cacheId] = artwork;
4141- });
4242- });
1414+ // Register
1515+ const context = register();
43164417 ////////////////////////////////////////////
4518 // ACTIONS
4619 ////////////////////////////////////////////
4720 function artwork(request: ArtworkRequest) {
4848- return processRequest(request);
2121+ return worker.call.artwork(request);
4922 }
50235124 function supply(items: ArtworkRequest[]) {
5252- const exe = !queue[0];
5353- queue = [...queue, ...items];
5454- if (exe) shiftQueue();
2525+ return worker.call.supply(items);
5526 }
56275728 context.setActionHandler("artwork", artwork);
5829 context.setActionHandler("supply", supply);
5959-6060- ////////////////////////////////////////////
6161- // 🛠️
6262- ////////////////////////////////////////////
6363- async function lastFm(req: ArtworkRequest): Promise<Artwork[]> {
6464- if (!navigator.onLine) return [];
6565-6666- const query = req.tags?.artist;
6767-6868- return await fetch(
6969- `https://ws.audioscrobbler.com/2.0/?method=album.search&album=${query}&api_key=4f0fe85b67baef8bb7d008a8754a95e5&format=json`,
7070- )
7171- .then((r) => r.json())
7272- .then((r) => lastFmCover(r.results.albummatches.album));
7373- }
7474-7575- function lastFmCover(remainingMatches: any[]): Promise<Artwork[]> {
7676- const album = remainingMatches[0];
7777- const url = album ? album.image[album.image.length - 1]["#text"] : null;
7878-7979- return url && url !== ""
8080- ? fetch(url)
8181- .then((r) => r.blob())
8282- .then(async (b) => [{ bytes: await b.bytes(), mime: b.type }])
8383- .catch((_) => lastFmCover(remainingMatches.slice(1)))
8484- : album && lastFmCover(remainingMatches.slice(1));
8585- }
8686-8787- async function musicBrainz(req: ArtworkRequest): Promise<Artwork[]> {
8888- const artist = req.tags?.artist;
8989- const album = req.tags?.album;
9090-9191- if (!navigator.onLine) return [];
9292- if (!album && !artist) return [];
9393-9494- // TODO
9595- const variousArtists = false;
9696-9797- const query = `release:"${album}"` + (variousArtists ? `` : ` AND artist:"${artist}"`);
9898- const encodedQuery = encodeURIComponent(query);
9999-100100- return await fetch(`https://musicbrainz.org/ws/2/release/?query=${encodedQuery}&fmt=json`)
101101- .then((r) => r.json())
102102- .then((r) => musicBrainzCover(r.releases));
103103- }
104104-105105- async function musicBrainzCover(remainingReleases: any[]): Promise<Artwork[]> {
106106- const release = remainingReleases[0];
107107- if (!release) return [];
108108-109109- return await fetch(`https://coverartarchive.org/release/${release.id}/front-500`)
110110- .then((r) => r.blob())
111111- .then(async (b) => {
112112- if (b && b.type.startsWith("image/")) {
113113- return [{ bytes: await b.bytes(), mime: b.type }];
114114- } else {
115115- return musicBrainzCover(remainingReleases.slice(1));
116116- }
117117- })
118118- .catch(() => musicBrainzCover(remainingReleases.slice(1)));
119119- }
120120-121121- async function processRequest(req: ArtworkRequest): Promise<Artwork[]> {
122122- // Check if already processed
123123- // TODO: Retry if none was found?
124124- const cache = await IDB.get(`${IDB_ARTWORK_PREFIX}/${req.cacheId}`);
125125- if (cache) return cache;
126126-127127- // 🚀
128128- let art: Artwork[] = [];
129129-130130- // Get metadata + possible artwork from file metadata
131131- const proc = await processor.metadata;
132132- const meta = await proc.sendAction<Extraction>(
133133- "supply",
134134- { ...req, includeArtwork: true },
135135- {
136136- timeoutDuration: 60000 * 5,
137137- },
138138- );
139139-140140- if (!req.tags) req.tags = meta.tags;
141141-142142- // Add artwork from metadata
143143- const fromMeta =
144144- meta.artwork?.map((a: IPicture) => {
145145- return { bytes: a.data, mime: a.format };
146146- }) || [];
147147-148148- art.push(...fromMeta);
149149-150150- // If no artwork, try finding it on other sources
151151- if (art.length === 0) {
152152- const fromMusicBrainz = await musicBrainz(req);
153153- art.push(...fromMusicBrainz);
154154- }
155155-156156- if (art.length === 0) {
157157- const fromLastFm = await lastFm(req);
158158- art.push(...fromLastFm);
159159- }
160160-161161- // Save artwork to IDB
162162- await IDB.set(`${IDB_ARTWORK_PREFIX}/${req.cacheId}`, art);
163163- context.data.artwork[req.cacheId] = art;
164164-165165- // Fin
166166- return art;
167167- }
168168-169169- async function shiftQueue() {
170170- const next = queue.shift();
171171- if (!next) return;
172172-173173- await processRequest(next);
174174- await shiftQueue();
175175- }
17630</script>
+1-20
src/pages/processor/artwork/types.d.ts
···11-import type { TrackTags } from "@applets/core/types";
22-33-export type Artwork = {
44- bytes: Uint8Array;
55- mime: string;
66-};
77-88-export type ArtworkRequest<Tags = TrackTags> = {
99- cacheId: string;
1010- mimeType?: string;
1111- stream?: ReadableStream;
1212- tags?: Tags;
1313- urls?: Urls;
1414-};
1515-1616-export type State = {
1717- artwork: Record<string, Artwork[]>;
1818-};
1919-2020-export type Urls = { get: string; head: string };
11+export * from "../../../scripts/processor/artwork/types.d.ts";