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 { 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}