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