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