Experiment to rebuild Diffuse using web applets.
0

Configure Feed

Select the types of activity you want to include in your feed.

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