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