Experiment to rebuild Diffuse using web applets.
1import type { IPicture } from "music-metadata";
2import { SharedWorkerPolyfill as SharedWorker } from "@okikio/sharedworker";
3import * as IDB from "idb-keyval";
4
5import type { Actions as MetadataActions } from "../metadata/worker";
6import type { Artwork, ArtworkRequest } from "./types";
7import { endpoint, expose } from "@scripts/common";
8import { IDB_ARTWORK_PREFIX } from "./constants";
9
10// State
11let queue: ArtworkRequest[] = [];
12
13// Metadata worker
14const metadataWorker = endpoint<MetadataActions>(
15 new SharedWorker("../metadata/worker", {
16 type: "module",
17 }).port,
18);
19
20////////////////////////////////////////////
21// ACTIONS
22////////////////////////////////////////////
23const actions = expose({
24 artwork,
25 supply,
26});
27
28export type Actions = typeof actions;
29
30// Actions
31
32function artwork(request: ArtworkRequest) {
33 return processRequest(request);
34}
35
36function supply(items: ArtworkRequest[]) {
37 const exe = !queue[0];
38 queue = [...queue, ...items];
39 if (exe) shiftQueue();
40}
41
42////////////////////////////////////////////
43// 🛠️
44////////////////////////////////////////////
45async function lastFm(req: ArtworkRequest): Promise<Artwork[]> {
46 if (!navigator.onLine) return [];
47
48 const query = req.tags?.artist;
49
50 return await fetch(
51 `https://ws.audioscrobbler.com/2.0/?method=album.search&album=${query}&api_key=4f0fe85b67baef8bb7d008a8754a95e5&format=json`,
52 )
53 .then((r) => r.json())
54 .then((r) => lastFmCover(r.results.albummatches.album));
55}
56
57function lastFmCover(remainingMatches: any[]): Promise<Artwork[]> {
58 const album = remainingMatches[0];
59 const url = album ? album.image[album.image.length - 1]["#text"] : null;
60
61 return url && url !== ""
62 ? fetch(url)
63 .then((r) => r.blob())
64 .then(async (b) => [{ bytes: await b.bytes(), mime: b.type }])
65 .catch((_) => lastFmCover(remainingMatches.slice(1)))
66 : album && lastFmCover(remainingMatches.slice(1));
67}
68
69async function musicBrainz(req: ArtworkRequest): Promise<Artwork[]> {
70 const artist = req.tags?.artist;
71 const album = req.tags?.album;
72
73 if (!navigator.onLine) return [];
74 if (!album && !artist) return [];
75
76 // TODO
77 const variousArtists = false;
78
79 const query = `release:"${album}"` + (variousArtists ? `` : ` AND artist:"${artist}"`);
80 const encodedQuery = encodeURIComponent(query);
81
82 return await fetch(`https://musicbrainz.org/ws/2/release/?query=${encodedQuery}&fmt=json`)
83 .then((r) => r.json())
84 .then((r) => musicBrainzCover(r.releases));
85}
86
87async function musicBrainzCover(remainingReleases: any[]): Promise<Artwork[]> {
88 const release = remainingReleases[0];
89 if (!release) return [];
90
91 return await fetch(`https://coverartarchive.org/release/${release.id}/front-500`)
92 .then((r) => r.blob())
93 .then(async (b) => {
94 if (b && b.type.startsWith("image/")) {
95 return [{ bytes: await b.bytes(), mime: b.type }];
96 } else {
97 return musicBrainzCover(remainingReleases.slice(1));
98 }
99 })
100 .catch(() => musicBrainzCover(remainingReleases.slice(1)));
101}
102
103async function processRequest(req: ArtworkRequest): Promise<Artwork[]> {
104 // Check if already processed
105 // TODO: Retry if none was found?
106 const cache = await IDB.get(`${IDB_ARTWORK_PREFIX}/${req.cacheId}`);
107 if (cache) return cache;
108
109 // 🚀
110 let art: Artwork[] = [];
111
112 // Get metadata + possible artwork from file metadata
113 const meta = await metadataWorker.call.supply({ ...req, includeArtwork: true });
114 if (!req.tags) req.tags = meta.tags;
115
116 // Add artwork from metadata
117 const fromMeta =
118 meta.artwork?.map((a: IPicture) => {
119 return { bytes: a.data, mime: a.format };
120 }) || [];
121
122 art.push(...fromMeta);
123
124 // If no artwork, try finding it on other sources
125 if (art.length === 0) {
126 const fromMusicBrainz = await musicBrainz(req);
127 art.push(...fromMusicBrainz);
128 }
129
130 if (art.length === 0) {
131 const fromLastFm = await lastFm(req);
132 art.push(...fromLastFm);
133 }
134
135 // Save artwork to IDB
136 await IDB.set(`${IDB_ARTWORK_PREFIX}/${req.cacheId}`, art);
137
138 // Fin
139 return art;
140}
141
142async function shiftQueue() {
143 const next = queue.shift();
144 if (!next) return;
145
146 await processRequest(next);
147 await shiftQueue();
148}