Experiment to rebuild Diffuse using web applets.
0
fork

Configure Feed

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

feat: artwork processor pt. 2

+113 -27
+6 -6
src/pages/orchestrator/input-cache/_applet.astro
··· 23 23 }; 24 24 25 25 // Start processing when tracks are loaded 26 - configurator.output 27 - .then((output) => { 28 - return waitUntilAppletData(output, (d) => d?.tracks.state === "loaded"); 29 - }) 30 - .then(process); 26 + context 27 + .settled() 28 + .then(() => configurator.output) 29 + .then((output) => waitUntilAppletData(output, (d) => d?.tracks.state === "loaded")) 30 + .then(() => process()); 31 31 32 32 //////////////////////////////////////////// 33 33 // ACTIONS ··· 78 78 79 79 const metadataProcessor = await processor.metadata; 80 80 const { stats, tags } = await metadataProcessor.sendAction( 81 - "extract", 81 + "supply", 82 82 { urls: { get: resGet.url, head: resHead?.url || resGet.url } }, 83 83 { 84 84 timeoutDuration: 60000 * 15,
+97 -17
src/pages/processor/artwork/_applet.astro
··· 1 1 <script> 2 + import type { IPicture } from "music-metadata"; 2 3 import * as IDB from "idb-keyval"; 3 4 4 5 import { applet, register } from "@scripts/applets/common"; 5 - import type { ArtworkRequest, State } from "./types.d.ts"; 6 + import type { ArtworkRequest, State, Artwork } from "./types.d.ts"; 7 + import type { Extraction } from "../metadata/types.d.ts"; 6 8 7 9 //////////////////////////////////////////// 8 10 // SETUP 9 11 //////////////////////////////////////////// 10 12 const IDB_PREFIX = "@applets/processor/artwork"; 13 + const IDB_ARTWORK_PREFIX = `${IDB_PREFIX}/artwork`; 11 14 12 15 const context = register<State>(); 13 16 let queue: ArtworkRequest[] = []; 14 17 15 18 // Initial data 16 19 context.data = { 17 - artwork: [], 20 + artwork: {}, 18 21 }; 19 22 20 23 // Applet connections ··· 23 26 }; 24 27 25 28 // Load already-downloaded artwork 26 - IDB.keys().then((keys) => { 27 - console.log(keys); 28 - const artworkKeys = keys.filter((k) => k.toString().startsWith(`${IDB_PREFIX}/artwork/`)); 29 + IDB.keys().then(async (keys) => { 30 + const artworkKeys = keys.filter((k) => k.toString().startsWith(`${IDB_ARTWORK_PREFIX}/`)); 31 + 32 + artworkKeys.forEach(async (key) => { 33 + if (typeof key !== "string") return; 34 + 35 + const artwork = await IDB.get(key); 36 + const cacheId = key.split("/").reverse()[0]; 37 + 38 + context.data.artwork[cacheId] = artwork; 39 + }); 29 40 }); 30 41 31 42 //////////////////////////////////////////// ··· 34 45 function supply(items: ArtworkRequest[]) { 35 46 const exe = !queue[0]; 36 47 queue = [...queue, ...items]; 37 - console.log("supply", queue); 38 48 if (exe) shiftQueue(); 39 49 } 40 50 ··· 43 53 //////////////////////////////////////////// 44 54 // 🛠️ 45 55 //////////////////////////////////////////// 56 + async function lastFm(req: ArtworkRequest): Promise<Artwork[]> { 57 + if (!navigator.onLine) return []; 58 + 59 + const query = req.tags?.artist; 60 + 61 + return await fetch( 62 + `https://ws.audioscrobbler.com/2.0/?method=album.search&album=${query}&api_key=4f0fe85b67baef8bb7d008a8754a95e5&format=json`, 63 + ) 64 + .then((r) => r.json()) 65 + .then((r) => lastFmCover(r.results.albummatches.album)); 66 + } 67 + 68 + function lastFmCover(remainingMatches: any[]): Promise<Artwork[]> { 69 + const album = remainingMatches[0]; 70 + const url = album ? album.image[album.image.length - 1]["#text"] : null; 71 + 72 + return url && url !== "" 73 + ? fetch(url) 74 + .then((r) => r.blob()) 75 + .then(async (b) => [{ bytes: await b.bytes(), mime: b.type }]) 76 + .catch((_) => lastFmCover(remainingMatches.slice(1))) 77 + : album && lastFmCover(remainingMatches.slice(1)); 78 + } 79 + 80 + async function musicBrainz(req: ArtworkRequest): Promise<Artwork[]> { 81 + const artist = req.tags?.artist; 82 + const album = req.tags?.album; 83 + 84 + if (!navigator.onLine) return []; 85 + if (!album && !artist) return []; 86 + 87 + // TODO 88 + const variousArtists = false; 89 + 90 + const query = `release:"${album}"` + (variousArtists ? `` : ` AND artist:"${artist}"`); 91 + const encodedQuery = encodeURIComponent(query); 92 + 93 + return await fetch(`https://musicbrainz.org/ws/2/release/?query=${encodedQuery}&fmt=json`) 94 + .then((r) => r.json()) 95 + .then((r) => musicBrainzCover(r.releases)); 96 + } 97 + 98 + async function musicBrainzCover(remainingReleases: any[]): Promise<Artwork[]> { 99 + const release = remainingReleases[0]; 100 + if (!release) return []; 101 + 102 + return await fetch(`https://coverartarchive.org/release/${release.id}/front-500`) 103 + .then((r) => r.blob()) 104 + .then(async (b) => { 105 + if (b && b.type.startsWith("image/")) { 106 + return [{ bytes: await b.bytes(), mime: b.type }]; 107 + } else { 108 + return musicBrainzCover(remainingReleases.slice(1)); 109 + } 110 + }) 111 + .catch(() => musicBrainzCover(remainingReleases.slice(1))); 112 + } 113 + 46 114 async function shiftQueue() { 47 115 const next = queue.shift(); 48 116 if (!next) return; 49 117 50 118 // Check if already processed 51 119 const cache = await IDB.get(`${IDB_PREFIX}/${next.cacheId}`); 52 - if (cache) return; 120 + if (cache && cache.length > 0) return; 53 121 54 122 // 🚀 55 - let art: Uint8Array | undefined; 123 + let art: Artwork[] = []; 56 124 57 125 // Get metadata + possible artwork from file metadata 58 126 const proc = await processor.metadata; 59 - const meta = await proc.sendAction("supply", { ...next, includeArtwork: true }); 127 + const meta = await proc.sendAction<Extraction>("supply", { ...next, includeArtwork: true }); 128 + if (!next.tags) next.tags = meta.tags; 60 129 61 - // 62 - art = meta.artwork?.sort((a: any, b: any) => { 63 - if (a.data.length > b.data.length) return -1; 64 - if (a.data.length < b.data.length) return 1; 65 - return 0; 66 - })?.[0]?.data; 130 + // Add artwork from metadata 131 + const fromMeta = 132 + meta.artwork?.map((a: IPicture) => { 133 + return { bytes: a.data, mime: a.format }; 134 + }) || []; 67 135 68 - console.log(art); 136 + art.push(...fromMeta); 137 + 138 + // If no artwork, try finding it on other sources 139 + if (art.length === 0) { 140 + const fromMusicBrainz = await musicBrainz(next); 141 + art.push(...fromMusicBrainz); 142 + } 143 + 144 + if (art.length === 0) { 145 + const fromLastFm = await lastFm(next); 146 + art.push(...fromLastFm); 147 + } 69 148 70 149 // Save artwork to IDB 71 - await IDB.set(`${IDB_PREFIX}/${next.cacheId}`, art || "TRIED"); 150 + await IDB.set(`${IDB_ARTWORK_PREFIX}/${next.cacheId}`, art); 151 + context.data.artwork[next.cacheId] = art; 72 152 73 153 // 🏹 74 154 shiftQueue();
+9 -3
src/pages/processor/artwork/types.d.ts
··· 1 - export type Artwork = {}; 1 + import { TrackTags } from "@applets/core/types"; 2 2 3 - export type ArtworkRequest = { 3 + export type Artwork = { 4 + bytes: Uint8Array; 5 + mime: string; 6 + }; 7 + 8 + export type ArtworkRequest<Tags = TrackTags> = { 4 9 cacheId: string; 5 10 mimeType?: string; 6 11 stream?: ReadableStream; 12 + tags?: Tags; 7 13 urls?: Urls; 8 14 }; 9 15 10 16 export type State = { 11 - artwork: Artwork[]; 17 + artwork: Record<string, Artwork[]>; 12 18 }; 13 19 14 20 export type Urls = { get: string; head: string };
+1 -1
src/pages/processor/metadata/_applet.astro
··· 24 24 mimeType?: string; 25 25 stream?: ReadableStream; 26 26 urls?: Urls; 27 - }) { 27 + }): Promise<Extraction> { 28 28 // Construct records 29 29 // TODO: Use other metadata lib as fallback: https://github.com/buzz/mediainfo.js 30 30 const response = await musicMetadataTags(args).catch((err): Extraction => {