A music player that connects to your cloud/distributed storage.
0
fork

Configure Feed

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

feat: artwork processor

+399 -228
-10
_backup/pages/theme/blur/index.astro
··· 1 - --- 2 - import Page from "../../../layouts/page.astro"; 3 - import "@styles/theme/blur/index.css"; 4 - --- 5 - 6 - <Page title="Diffuse"> 7 - <script src="../../../scripts/theme/blur/index.js"></script> 8 - 9 - <main></main> 10 - </Page>
-2
_backup/scripts/processor/artwork/constants.ts
··· 1 - export const IDB_PREFIX = "@applets/processor/artwork"; 2 - export const IDB_ARTWORK_PREFIX = `${IDB_PREFIX}/artwork`;
+6 -1
_backup/scripts/processor/artwork/types.d.ts src/component/processor/artwork/types.d.ts
··· 1 - import type { TrackTags } from "@applets/core/types"; 1 + import type { TrackTags } from "@component/core/types.d.ts"; 2 + 3 + export type Actions = { 4 + artwork(request: ArtworkRequest): Promise<Artwork[]>; 5 + supply(items: ArtworkRequest[]): void; 6 + }; 2 7 3 8 export type Artwork = { 4 9 bytes: Uint8Array;
-214
_backup/scripts/processor/artwork/worker.ts
··· 1 - import type { IPicture } from "music-metadata"; 2 - import * as IDB from "idb-keyval"; 3 - 4 - import type { Artwork, ArtworkRequest } from "./types"; 5 - import type { Extraction } from "../metadata/types"; 6 - import { provide } from "@scripts/common"; 7 - import { IDB_ARTWORK_PREFIX } from "./constants"; 8 - import { musicMetadataTags } from "../metadata/common"; 9 - 10 - // State 11 - let queue: ArtworkRequest[] = []; 12 - 13 - //////////////////////////////////////////// 14 - // SETUP 15 - //////////////////////////////////////////// 16 - 17 - const actions = { 18 - artwork, 19 - supply, 20 - }; 21 - 22 - const { tasks } = provide({ actions, tasks: actions }); 23 - 24 - export type Actions = typeof actions; 25 - export type Tasks = typeof tasks; 26 - 27 - //////////////////////////////////////////// 28 - // ACTIONS 29 - //////////////////////////////////////////// 30 - 31 - async function artwork(request: ArtworkRequest) { 32 - const art = await processRequest(request); 33 - return art; 34 - } 35 - 36 - function supply(items: ArtworkRequest[]) { 37 - const exe = !queue[0]; 38 - queue = [...queue, ...items]; 39 - if (exe) shiftQueue(); 40 - } 41 - 42 - //////////////////////////////////////////// 43 - // 🛠️ 44 - //////////////////////////////////////////// 45 - function escapeLucene(str: string) { 46 - return [].map 47 - .call(str, (char) => { 48 - if ( 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 - char === "?" || 65 - char === ":" || 66 - char === "\\" || 67 - char === "/" 68 - ) 69 - return "\\" + char; 70 - else return char; 71 - }) 72 - .join(""); 73 - } 74 - 75 - async function lastFm(req: ArtworkRequest): Promise<Artwork[]> { 76 - if (!navigator.onLine) return []; 77 - 78 - const query = req.tags?.artist; 79 - 80 - return await fetch( 81 - `https://ws.audioscrobbler.com/2.0/?method=album.search&album=${query}&api_key=4f0fe85b67baef8bb7d008a8754a95e5&format=json`, 82 - ) 83 - .then((r) => r.json()) 84 - .then((r) => lastFmCover(r.results.albummatches.album)) 85 - .catch((err) => { 86 - console.error(err); 87 - return []; 88 - }); 89 - } 90 - 91 - async function lastFmCover(remainingMatches: any[]): Promise<Artwork[]> { 92 - const album = remainingMatches[0]; 93 - const url = album ? album.image[album.image.length - 1]["#text"] : null; 94 - 95 - return url && url !== "" 96 - ? await fetch(url) 97 - .then((r) => r.blob()) 98 - .then(async (b) => [ 99 - { bytes: await b.arrayBuffer().then((buf) => new Uint8Array(buf)), mime: b.type }, 100 - ]) 101 - .catch((err) => { 102 - console.error(err); 103 - return lastFmCover(remainingMatches.slice(1)); 104 - }) 105 - : album 106 - ? lastFmCover(remainingMatches.slice(1)) 107 - : []; 108 - } 109 - 110 - async function musicBrainz(req: ArtworkRequest): Promise<Artwork[]> { 111 - const artist = req.tags?.artist; 112 - const album = req.tags?.album; 113 - 114 - if (!navigator.onLine) return []; 115 - if (!album && !artist) return []; 116 - 117 - const query = 118 - `release:"${escapeLucene(album || "")}"` + 119 - (req.variousArtists ? `` : ` AND artistname:"${escapeLucene(artist || "")}"`); 120 - const encodedQuery = encodeURIComponent(query); 121 - 122 - return await fetch(`https://musicbrainz.org/ws/2/release/?query=${encodedQuery}&fmt=json`) 123 - .then((r) => r.json()) 124 - .then((r) => { 125 - if (r.releases.length === 0 && !req.variousArtists) { 126 - return musicBrainz({ ...req, variousArtists: true }); 127 - } else { 128 - return musicBrainzCover(r.releases, req); 129 - } 130 - }) 131 - .catch((err) => { 132 - console.error(err); 133 - return []; 134 - }); 135 - } 136 - 137 - async function musicBrainzCover(remainingReleases: any[], req: ArtworkRequest): Promise<Artwork[]> { 138 - const release = remainingReleases[0]; 139 - if (!release) return []; 140 - 141 - const credit = release?.["artist-credit"]?.[0]?.name; 142 - if (req.variousArtists && credit !== "Various Artists" && credit !== req.tags?.artist) return []; 143 - 144 - return await fetch(`https://coverartarchive.org/release/${release.id}/front-1200`) 145 - .then((r) => r.blob()) 146 - .then(async (b) => { 147 - if (b.type.startsWith("image/")) { 148 - return [{ bytes: await b.arrayBuffer().then((buf) => new Uint8Array(buf)), mime: b.type }]; 149 - } else { 150 - return musicBrainzCover(remainingReleases.slice(1), req); 151 - } 152 - }) 153 - .catch((err) => { 154 - console.error(err); 155 - return musicBrainzCover(remainingReleases.slice(1), req); 156 - }); 157 - } 158 - 159 - async function processRequest(req: ArtworkRequest): Promise<Artwork[]> { 160 - // Check if already processed 161 - // TODO: Retry if none was found? 162 - const cache = await IDB.get(`${IDB_ARTWORK_PREFIX}/${req.cacheId}`); 163 - if (cache && Array.isArray(cache) && cache.length) return cache; 164 - 165 - // Request override 166 - if (req.tags?.artist?.toUpperCase() === "VA") { 167 - req.variousArtists = true; 168 - } 169 - 170 - // 🚀 171 - let art: Artwork[] = []; 172 - 173 - // Get metadata + possible artwork from file metadata 174 - const meta = await musicMetadataTags({ ...req, includeArtwork: true }).catch((err) => { 175 - console.error("music-metadata error", err); 176 - const extraction: Extraction = {}; 177 - return extraction; 178 - }); 179 - 180 - if (!req.tags && meta.tags) req.tags = meta.tags; 181 - 182 - // Add artwork from metadata 183 - const fromMeta = 184 - meta.artwork?.map((a: IPicture) => { 185 - return { bytes: a.data, mime: a.format }; 186 - }) || []; 187 - 188 - art.push(...fromMeta); 189 - 190 - // If no artwork, try finding it on other sources 191 - if (art.length === 0) { 192 - const fromMusicBrainz = await musicBrainz(req); 193 - art.push(...fromMusicBrainz); 194 - } 195 - 196 - if (art.length === 0) { 197 - const fromLastFm = await lastFm(req); 198 - art.push(...fromLastFm); 199 - } 200 - 201 - // Save artwork to IDB 202 - await IDB.set(`${IDB_ARTWORK_PREFIX}/${req.cacheId}`, art); 203 - 204 - // Fin 205 - return art; 206 - } 207 - 208 - async function shiftQueue() { 209 - const next = queue.shift(); 210 - if (!next) return; 211 - 212 - await processRequest(next); 213 - await shiftQueue(); 214 - }
+2
deno.jsonc
··· 8 8 "@mys/worker-fn": "jsr:@mys/worker-fn@^3.2.1", 9 9 "@okikio/transferables": "jsr:@okikio/transferables@^1.0.2", 10 10 "alien-signals": "npm:alien-signals@^3.0.0", 11 + "idb-keyval": "npm:idb-keyval@^6.2.2", 11 12 "morphdom": "npm:morphdom@^2.7.7/dist/morphdom.js", 12 13 "uri-js": "npm:uri-js@^4.4.1", 13 14 "xxh32": "npm:xxh32@^2.0.5", 14 15 15 16 // music-metadata 17 + // NOTE: A lot of issues with `node:` imports, hence this mess. 16 18 "@tokenizer/http": "https://esm.sh/@tokenizer/http@0.9.2/lib/http-client.js", 17 19 "@tokenizer/range": "https://esm.sh/@tokenizer/range@0.13.0/lib/index.js", 18 20 "music-metadata": "https://esm.sh/music-metadata@11.9.0/lib/core.js",
+91 -1
deno.lock
··· 2 2 "version": "5", 3 3 "specifiers": { 4 4 "jsr:@deno/loader@0.3.6": "0.3.6", 5 + "jsr:@fry69/deep-diff@~0.1.10": "0.1.10", 6 + "jsr:@mys/m-rpc@~0.12.2": "0.12.2", 7 + "jsr:@mys/worker-fn@^3.2.1": "3.2.1", 8 + "jsr:@okikio/transferables@^1.0.2": "1.0.2", 5 9 "jsr:@std/cli@1.0.22": "1.0.22", 6 10 "jsr:@std/cli@^1.0.21": "1.0.22", 7 11 "jsr:@std/collections@^1.1.3": "1.1.3", ··· 31 35 "jsr:@std/yaml@^1.0.5": "1.0.9", 32 36 "npm:alien-signals@3": "3.0.3", 33 37 "npm:autoprefixer@10.4.21": "10.4.21_postcss@8.5.6", 38 + "npm:idb-keyval@^6.2.2": "6.2.2", 34 39 "npm:lightningcss-wasm@1.30.1": "1.30.1", 35 40 "npm:markdown-it-attrs@4.3.1": "4.3.1_markdown-it@14.1.0", 36 41 "npm:markdown-it-deflist@3.0.0": "3.0.0", ··· 44 49 "jsr": { 45 50 "@deno/loader@0.3.6": { 46 51 "integrity": "98f08d837c18ece5ba15122264fb29580967407c34e6552e152b8f453a60c2be" 52 + }, 53 + "@fry69/deep-diff@0.1.10": { 54 + "integrity": "cdd88fefaef1ac896a038a5f3c0895038d8c725e61bac50489c455156e0275f5" 55 + }, 56 + "@mys/m-rpc@0.12.2": { 57 + "integrity": "36599d3d4708db9f5c0f7da35a17b7e7da1fafddb69de6cfcdc6afe94cd4f084", 58 + "dependencies": [ 59 + "jsr:@okikio/transferables" 60 + ] 61 + }, 62 + "@mys/worker-fn@3.2.1": { 63 + "integrity": "330960f21041edd20fa9c5f78b136f62e3781e35797ac635534f003545be76cd", 64 + "dependencies": [ 65 + "jsr:@mys/m-rpc" 66 + ] 67 + }, 68 + "@okikio/transferables@1.0.2": { 69 + "integrity": "46a80015a1c4672b0b246e38838b3ea1e2edc6c775a235184a2f8eb49a8314f7" 47 70 }, 48 71 "@std/cli@1.0.22": { 49 72 "integrity": "50d1e4f87887cb8a8afa29b88505ab5081188f5cad3985460c3b471fa49ff21a" ··· 183 206 "dependencies": [ 184 207 "function-bind" 185 208 ] 209 + }, 210 + "idb-keyval@6.2.2": { 211 + "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==" 186 212 }, 187 213 "is-core-module@2.16.1": { 188 214 "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", ··· 542 568 "https://deno.land/x/lume@v3.0.11/plugins/url.ts": "15f2e80b6fcbf86f8795a3676b8d533bab003ac016ff127e58165a6ac3bffc1a", 543 569 "https://deno.land/x/lume@v3.0.11/plugins/vento.ts": "fd60ee80435994bcf88b2cda9c51eaed0ba49a2363f42920675f2d5a0a4a6ab2", 544 570 "https://deno.land/x/lume@v3.0.11/plugins/yaml.ts": "d0ebf37c38648172c6b95c502753a3edf60278ab4f6a063f3ca00f31e0dd90cc", 571 + "https://deno.land/x/ssx@v0.1.12/jsx-runtime.ts": "a334a1ee3a25de7f3b84b7b8d842bcae40e9116f6edb6ec76cb265712c8a2ab8", 545 572 "https://deno.land/x/vento@v2.1.1/core/environment.ts": "36f3e145adfe1af3740cfcfc6ff237d6fe48225d3627123b17022251afbe3074", 546 573 "https://deno.land/x/vento@v2.1.1/core/errors.ts": "8606b682b465d598a394feea135dd2f84033b5ef2a61a23d116ccb782a0a547a", 547 574 "https://deno.land/x/vento@v2.1.1/core/js.ts": "83084240150d7e8b83e43ec8fcf78564a8ba8599c3d517976efbb11b208903b2", ··· 563 590 "https://deno.land/x/vento@v2.1.1/plugins/mod.ts": "017d5bb3e3c80b7f67271cdf8779686f55916070c5d168a143e6a37c35bcd731", 564 591 "https://deno.land/x/vento@v2.1.1/plugins/set.ts": "cf9dfbf68b52039781fd86ec0b9587a8bcd486fdef9f08989719cfdb7fa233d0", 565 592 "https://deno.land/x/vento@v2.1.1/plugins/trim.ts": "8d33271327b09ffd8f569ebde85125b1324fa9538a54d6072ac97a9fb5d24264", 566 - "https://deno.land/x/vento@v2.1.1/plugins/unescape.ts": "1c56f0310c7757880df7684fc6b7bf9efd27fdb6b929b89626802f5a99cb93ee" 593 + "https://deno.land/x/vento@v2.1.1/plugins/unescape.ts": "1c56f0310c7757880df7684fc6b7bf9efd27fdb6b929b89626802f5a99cb93ee", 594 + "https://esm.sh/@borewit/text-codec@0.1.1/es2022/text-codec.mjs": "ea8f41f92f2798340cf2b568602fc1e6f9957e8192265d92d6d06725d0bbfcff", 595 + "https://esm.sh/@borewit/text-codec@0.2.0/es2022/text-codec.mjs": "1fa7af74bcd1b8c1a460836447074360915a752cffe0dbdb8ac472f422a7fbd9", 596 + "https://esm.sh/@borewit/text-codec@^0.1.0?target=es2022": "e3ef0acd0c052b05ad09e59f90283c46e8b5d82d92548ab93b11badd8258d78f", 597 + "https://esm.sh/@borewit/text-codec@^0.2.0?target=es2022": "41f958f8bdbc8407bd83126648cdeca660a1c86662ad3358ecc544f84e8a9f25", 598 + "https://esm.sh/@tokenizer/http@0.9.2/es2022/lib/http-client.mjs": "a3b75a64606cf525497598e45357efbc03a9093f5000b180aca3ab278a9c69f3", 599 + "https://esm.sh/@tokenizer/http@0.9.2/lib/http-client.js": "3ebcb337b382c299e9267b8950aa63764404b50909a4f9dfde183005f532fc6b", 600 + "https://esm.sh/@tokenizer/inflate@0.2.7/es2022/inflate.mjs": "8fa443aca90ad2baa671baab9f31c244a65d3e415f0856f6cb7c01a91885a4c7", 601 + "https://esm.sh/@tokenizer/inflate@^0.2.7?target=es2022": "a0937e70b3279c427154214ee94f847f05ee18590d624ed09e13b166bc4b8802", 602 + "https://esm.sh/@tokenizer/range@0.12.0/es2022/range.mjs": "6e0bf369e99ec1608583522af1e3ea8e4967c202ccf0d853d7c180022693da16", 603 + "https://esm.sh/@tokenizer/range@0.13.0/es2022/range.mjs": "0aab315c6101c786d5af2a3442c7afdf619cc9c678529440d99cc9c93bc616e9", 604 + "https://esm.sh/@tokenizer/range@0.13.0/lib/index.js": "b6884ad1ad20d5ca12bcf051481f1d0f36640df2e4f5d827a8b9d100d56a2514", 605 + "https://esm.sh/@tokenizer/range@^0.12.0?target=es2022": "a5be7fc23c6c1538ccdf83ab1d945c97618dbc5c886c099fae9fa2569f9cd389", 606 + "https://esm.sh/content-type@1.0.5/es2022/content-type.mjs": "6fd962b99a80821956a22379aded6b7ca37cfbf95be5639047437e03211e1b56", 607 + "https://esm.sh/content-type@^1.0.5?target=es2022": "ad3579fa1808af61b587ce37a37756eae3306dfbbdea9d02e31a03e0b2ad70d4", 608 + "https://esm.sh/debug@4.4.3/es2022/debug.mjs": "42103c758115e696a3c7c269457f7827d957907fa8abcdbbaca2cb7060c7bd4f", 609 + "https://esm.sh/debug@^4.3.7?target=es2022": "0a96b7d9989513d07f02f0d5533e0dc7a49c492107702631f6490d754fbc4fe7", 610 + "https://esm.sh/debug@^4.4.0?target=es2022": "0a96b7d9989513d07f02f0d5533e0dc7a49c492107702631f6490d754fbc4fe7", 611 + "https://esm.sh/debug@^4.4.3?target=es2022": "0a96b7d9989513d07f02f0d5533e0dc7a49c492107702631f6490d754fbc4fe7", 612 + "https://esm.sh/fflate@0.8.2/es2022/fflate.mjs": "51759ec52e8522bbcd6dce941956a878c733975dcc159618c683c659270a59d3", 613 + "https://esm.sh/fflate@^0.8.2?target=es2022": "c127de9122b64d0866c6988baf5d7b21d6a1aca3989e8828f9c7f27029081b0c", 614 + "https://esm.sh/file-type@21.0.0/es2022/file-type.mjs": "c66d14ae76887e16cca26b4524bef9baba40585a8c499051ad5a0e12c980fcc2", 615 + "https://esm.sh/file-type@^21.0.0?target=es2022": "d9b06a8b5508acf4b5e3b5a6e886517f62f112bb4b25f49d409218d243aa6979", 616 + "https://esm.sh/ieee754@1.2.1/es2022/ieee754.mjs": "6e87635f6124ae21ea355a521f021a04afc277248bd5097dc85edce4d7017f86", 617 + "https://esm.sh/ieee754@^1.2.1?target=es2022": "503072b5aea2f29a14531251e497979ecea64585897d1561010acd83fdb4ce07", 618 + "https://esm.sh/media-typer@1.1.0/es2022/media-typer.mjs": "3dc6174765452b8271cad4031a308c8015c67bee4de2b0aff5daec36cb1b766e", 619 + "https://esm.sh/media-typer@^1.1.0?target=es2022": "c6132b2b491d8f497c7c2158602814796aaceb5bbcaee6bf086842a0a0e36947", 620 + "https://esm.sh/ms@2.1.3/es2022/ms.mjs": "9039464da1f4ae1c2042742d335c82556c048bbe49449b5d0cd5198193afa147", 621 + "https://esm.sh/ms@^2.1.3?target=es2022": "05ddb185b1d26c888c647f2aaac723379f0933fb608b3a9a994e602c41900019", 622 + "https://esm.sh/music-metadata@11.9.0/es2022/lib/aiff/AiffParser.mjs": "63997d01b96586c6e4e25ebe00d71749d42cbddc928ae871dbefa01ffbf7ece6", 623 + "https://esm.sh/music-metadata@11.9.0/es2022/lib/apev2/APEv2Parser.mjs": "5428c9b05edde4fb3781da175c3fa6909c741eca89da7f632c5771288a555c80", 624 + "https://esm.sh/music-metadata@11.9.0/es2022/lib/asf/AsfParser.mjs": "ad8173bfeae034c96cd21a091db2336ad5f389d2afda2ffb31c1f6af9f3e743a", 625 + "https://esm.sh/music-metadata@11.9.0/es2022/lib/core.mjs": "dddd9f4492a00db509f80f6e03c2aaee068793a0f6f35e975446fee39bdf5565", 626 + "https://esm.sh/music-metadata@11.9.0/es2022/lib/dsdiff/DsdiffParser.mjs": "6d77407d3d97a56224a169e367848bde75f3a7bd48429f077cb76be9bb6b490b", 627 + "https://esm.sh/music-metadata@11.9.0/es2022/lib/dsf/DsfParser.mjs": "2dfd30eae0cadd1cdbb9d0685e6f0d7598aefa58cd49b3d65048bd8caf492ef8", 628 + "https://esm.sh/music-metadata@11.9.0/es2022/lib/flac/FlacParser.mjs": "9ac86d7a4136b44b039262c20601a12162d447dcc8ac78c661df5b2caff4eeaf", 629 + "https://esm.sh/music-metadata@11.9.0/es2022/lib/matroska/MatroskaParser.mjs": "d43c319eab16d7b05f6e8f7fe7171ab3ccd401539dd5558f171a6005dfade0a5", 630 + "https://esm.sh/music-metadata@11.9.0/es2022/lib/mp4/MP4Parser.mjs": "46f7ebd05e2e95b31eef24f01a3292759d503cd07df6ed851b5e70c76334ca6b", 631 + "https://esm.sh/music-metadata@11.9.0/es2022/lib/mpeg/MpegParser.mjs": "b5cb01e7abccb149d02207a8ae44ec5127c6bd7d386b3b0150f4e0603d6988d5", 632 + "https://esm.sh/music-metadata@11.9.0/es2022/lib/musepack/MusepackParser.mjs": "02fb87f6653928ed7a23f44e0e5ae47d598b6d3aa8074f662d0540eecef5d82e", 633 + "https://esm.sh/music-metadata@11.9.0/es2022/lib/ogg/OggParser.mjs": "bacd1c45fd7b606b2a6ef2b8569469e979fb834eedd58e2e051e40c9a0aae9ae", 634 + "https://esm.sh/music-metadata@11.9.0/es2022/lib/wav/WaveParser.mjs": "aaa526c14b601e5953d91bb13c73c4e78291ccb4f8ff85c24a1b7f4624f9c34c", 635 + "https://esm.sh/music-metadata@11.9.0/es2022/lib/wavpack/WavPackParser.mjs": "af8ebb6037058ffb9792e3096a3f67c7eaf7ef2c39518274900b7a99bef552e5", 636 + "https://esm.sh/music-metadata@11.9.0/lib/core.js": "57a0a07bfddf9592e6572f2730f6a71acfb3af8719bc6d794cb3fb974d74d8ac", 637 + "https://esm.sh/node/process.mjs": "2c8eeaf6090c03d21bdcec5eddb33b105208c9ba3ef2c7be98f4d9b2495a624e", 638 + "https://esm.sh/peek-readable@5.4.2/es2022/peek-readable.mjs": "7e784eacb3816872f18606f82cc3f30f2d11e674c5199f84e6cf7d820a3ced23", 639 + "https://esm.sh/peek-readable@^5.3.1?target=es2022": "dbd46373159faf17590ec762a4f14921d7a6a5e6c3a64f334f302d96043077b0", 640 + "https://esm.sh/strtok3@10.3.4/es2022/core.mjs": "3183a30c522666467d6d0b5234113fe216e1ec79f4bd135002545458323d62f4", 641 + "https://esm.sh/strtok3@10.3.4/es2022/lib/stream/AbstractStreamReader.mjs": "675c8d26bd83e2e8907a988ba996e1ca45ae10397579d83ed027c14b13e78134", 642 + "https://esm.sh/strtok3@10.3.4/es2022/lib/stream/Errors.mjs": "1459007f24621eb036ba3be2fa592ce39381874722aab8e0302f8d6345a620d6", 643 + "https://esm.sh/strtok3@10.3.4/es2022/lib/stream/index.mjs": "7b6b3d37331712b157f2f6108081831b574d2b6493049aa8738834c4961ae633", 644 + "https://esm.sh/strtok3@10.3.4/es2022/strtok3.mjs": "7228ab10418d0fcee23ac018de8c4843c796471c691c9e940540a23145d589e6", 645 + "https://esm.sh/strtok3@9.1.1/es2022/strtok3.mjs": "f8cf21145d3d1be1cd7b4dff4a006b1587efb84d7c07953ed47a4f2c80759784", 646 + "https://esm.sh/strtok3@^10.2.0?target=es2022": "40daa79a2c4d6133be6732ad5ad53831401eda38a72129b911562d6d42d34fe2", 647 + "https://esm.sh/strtok3@^10.2.2/core?target=es2022": "9e188cff879adba3fe0cb8eb89d284ccda6b662bfa7b436c81370bda44e63415", 648 + "https://esm.sh/strtok3@^10.3.4?target=es2022": "40daa79a2c4d6133be6732ad5ad53831401eda38a72129b911562d6d42d34fe2", 649 + "https://esm.sh/strtok3@^9.1.1?target=es2022": "0b1ee38504da3418cd1f101f127415f5ba191b85089ae5a25beffa333c608499", 650 + "https://esm.sh/token-types@6.1.1/es2022/token-types.mjs": "2d6a177766ee99c5a482c7bf7c55d38f076beff8e4781d1cb8c295f3cb4da5e9", 651 + "https://esm.sh/token-types@^6.0.0?target=es2022": "15b612895b05b0120117c96dc3df21f4e211a7c0b126b36034f1a4ed803d216d", 652 + "https://esm.sh/token-types@^6.1.1?target=es2022": "15b612895b05b0120117c96dc3df21f4e211a7c0b126b36034f1a4ed803d216d", 653 + "https://esm.sh/uint8array-extras@1.5.0/es2022/uint8array-extras.mjs": "660b5b7967799e1ab7274f0062b66fc33a86c68df583429d8c52fa0b32332ae9", 654 + "https://esm.sh/uint8array-extras@^1.4.0?target=es2022": "24bf344529db04523ab848989bd810f22dce714f11f5e9a619dbe113b899b49f", 655 + "https://esm.sh/uint8array-extras@^1.5.0?target=es2022": "24bf344529db04523ab848989bd810f22dce714f11f5e9a619dbe113b899b49f" 567 656 }, 568 657 "workspace": { 569 658 "dependencies": [ ··· 574 663 "jsr:@std/fs@^1.0.19", 575 664 "jsr:@std/path@^1.1.2", 576 665 "npm:alien-signals@3", 666 + "npm:idb-keyval@^6.2.2", 577 667 "npm:morphdom@^2.7.7", 578 668 "npm:uri-js@^4.4.1", 579 669 "npm:xxh32@^2.0.5"
+2
src/component/processor/artwork/constants.js
··· 1 + export const IDB_PREFIX = "@component/processor/artwork"; 2 + export const IDB_ARTWORK_PREFIX = `${IDB_PREFIX}/cache`;
+40
src/component/processor/artwork/element.js
··· 1 + import { DiffuseElement } from "@common/element.js"; 2 + import { use } from "@common/worker.js"; 3 + 4 + /** 5 + * @import {Actions} from "./types.d.ts" 6 + */ 7 + 8 + //////////////////////////////////////////// 9 + // ELEMENT 10 + //////////////////////////////////////////// 11 + 12 + /** 13 + * @implements {Actions} 14 + */ 15 + class ArtworkProcessor extends DiffuseElement { 16 + constructor() { 17 + super(); 18 + 19 + // Group 20 + const group = crypto.randomUUID(); 21 + 22 + // Setup worker 23 + const name = `diffuse/processor/metadata/${group}`; 24 + const url = new URL("./worker.js", import.meta.url); 25 + const worker = new Worker(url, { name, type: "module" }); 26 + 27 + // Worker proxy 28 + this.artwork = use("artwork", worker); 29 + this.supply = use("supply", worker); 30 + } 31 + } 32 + 33 + export default ArtworkProcessor; 34 + 35 + //////////////////////////////////////////// 36 + // REGISTER 37 + //////////////////////////////////////////// 38 + 39 + export const NAME = "dp-artwork"; 40 + customElements.define(NAME, ArtworkProcessor);
+258
src/component/processor/artwork/worker.js
··· 1 + import * as IDB from "idb-keyval"; 2 + 3 + import { IDB_ARTWORK_PREFIX } from "./constants.js"; 4 + import { musicMetadataTags } from "../metadata/common.js"; 5 + 6 + /** 7 + * @import {IPicture} from "music-metadata" 8 + * @import {Actions, Artwork, ArtworkRequest} from "./types.d.ts" 9 + * @import {Extraction} from "../metadata/types.d.ts" 10 + */ 11 + 12 + /** 13 + * @type {ArtworkRequest[]} 14 + */ 15 + let queue = []; 16 + 17 + //////////////////////////////////////////// 18 + // ACTIONS 19 + //////////////////////////////////////////// 20 + 21 + /** 22 + * @type {Actions['artwork']} 23 + */ 24 + export async function artwork(request) { 25 + const art = await processRequest(request); 26 + return art; 27 + } 28 + 29 + /** 30 + * @type {Actions['supply']} 31 + */ 32 + export function supply(items) { 33 + const exe = !queue[0]; 34 + queue = [...queue, ...items]; 35 + if (exe) shiftQueue(); 36 + } 37 + 38 + //////////////////////////////////////////// 39 + // 🛠️ 40 + //////////////////////////////////////////// 41 + 42 + /** 43 + * @param {string} str 44 + */ 45 + function escapeLucene(str) { 46 + return [].map 47 + .call(str, (char) => { 48 + if ( 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 + char === "?" || 65 + char === ":" || 66 + char === "\\" || 67 + char === "/" 68 + ) { 69 + return "\\" + char; 70 + } else return char; 71 + }) 72 + .join(""); 73 + } 74 + 75 + /** 76 + * @param {ArtworkRequest} req 77 + * @returns {Promise<Artwork[]>} 78 + */ 79 + async function lastFm(req) { 80 + if (!navigator.onLine) return []; 81 + 82 + const query = req.tags?.artist; 83 + 84 + return await fetch( 85 + `https://ws.audioscrobbler.com/2.0/?method=album.search&album=${query}&api_key=4f0fe85b67baef8bb7d008a8754a95e5&format=json`, 86 + ) 87 + .then((r) => r.json()) 88 + .then((r) => lastFmCover(r.results.albummatches.album)) 89 + .catch((err) => { 90 + console.error(err); 91 + return []; 92 + }); 93 + } 94 + 95 + /** 96 + * @param {any[]} remainingMatches 97 + * @returns {Promise<Artwork[]>} 98 + */ 99 + async function lastFmCover(remainingMatches) { 100 + const album = remainingMatches[0]; 101 + const url = album ? album.image[album.image.length - 1]["#text"] : null; 102 + 103 + return url && url !== "" 104 + ? await fetch(url) 105 + .then((r) => r.blob()) 106 + .then(async (b) => [ 107 + { 108 + bytes: await b.arrayBuffer().then((buf) => new Uint8Array(buf)), 109 + mime: b.type, 110 + }, 111 + ]) 112 + .catch((err) => { 113 + console.error(err); 114 + return lastFmCover(remainingMatches.slice(1)); 115 + }) 116 + : album 117 + ? lastFmCover(remainingMatches.slice(1)) 118 + : []; 119 + } 120 + 121 + /** 122 + * @param {ArtworkRequest} req 123 + * @returns {Promise<Artwork[]>} 124 + */ 125 + async function musicBrainz(req) { 126 + const artist = req.tags?.artist; 127 + const album = req.tags?.album; 128 + 129 + if (!navigator.onLine) return []; 130 + if (!album && !artist) return []; 131 + 132 + const query = `release:"${escapeLucene(album || "")}"` + 133 + (req.variousArtists 134 + ? `` 135 + : ` AND artistname:"${escapeLucene(artist || "")}"`); 136 + const encodedQuery = encodeURIComponent(query); 137 + 138 + return await fetch( 139 + `https://musicbrainz.org/ws/2/release/?query=${encodedQuery}&fmt=json`, 140 + ) 141 + .then((r) => r.json()) 142 + .then((r) => { 143 + if (r.releases.length === 0 && !req.variousArtists) { 144 + return musicBrainz({ ...req, variousArtists: true }); 145 + } else { 146 + return musicBrainzCover(r.releases, req); 147 + } 148 + }) 149 + .catch((err) => { 150 + console.error(err); 151 + return []; 152 + }); 153 + } 154 + 155 + /** 156 + * @param {any[]} remainingReleases 157 + * @param {ArtworkRequest} req 158 + * @returns {Promise<Artwork[]>} 159 + */ 160 + async function musicBrainzCover(remainingReleases, req) { 161 + const release = remainingReleases[0]; 162 + if (!release) return []; 163 + 164 + const credit = release?.["artist-credit"]?.[0]?.name; 165 + if ( 166 + req.variousArtists && credit !== "Various Artists" && 167 + credit !== req.tags?.artist 168 + ) return []; 169 + 170 + return await fetch( 171 + `https://coverartarchive.org/release/${release.id}/front-1200`, 172 + ) 173 + .then((r) => r.blob()) 174 + .then(async (b) => { 175 + if (b.type.startsWith("image/")) { 176 + return [{ 177 + bytes: await b.arrayBuffer().then((buf) => new Uint8Array(buf)), 178 + mime: b.type, 179 + }]; 180 + } else { 181 + return musicBrainzCover(remainingReleases.slice(1), req); 182 + } 183 + }) 184 + .catch((err) => { 185 + console.error(err); 186 + return musicBrainzCover(remainingReleases.slice(1), req); 187 + }); 188 + } 189 + 190 + /** 191 + * @param {ArtworkRequest} req 192 + * @returns {Promise<Artwork[]>} 193 + */ 194 + async function processRequest(req) { 195 + // Check if already processed 196 + // TODO: Retry if none was found? 197 + const cache = await IDB.get(`${IDB_ARTWORK_PREFIX}/${req.cacheId}`); 198 + if (cache && Array.isArray(cache) && cache.length) return cache; 199 + 200 + // Request override 201 + if (req.tags?.artist?.toUpperCase() === "VA") { 202 + req.variousArtists = true; 203 + } 204 + 205 + // 🚀 206 + 207 + /** @type {Artwork[]} */ 208 + let art = []; 209 + 210 + // Get metadata + possible artwork from file metadata 211 + const meta = await musicMetadataTags({ ...req, includeArtwork: true }).catch( 212 + /** @param {Error} err */ (err) => { 213 + console.error("music-metadata error", err); 214 + /** @type {Extraction} */ 215 + const extraction = {}; 216 + return extraction; 217 + }, 218 + ); 219 + 220 + if (!req.tags && meta.tags) req.tags = meta.tags; 221 + 222 + // Add artwork from metadata 223 + const fromMeta = meta.artwork?.map( 224 + /** 225 + * @param {IPicture} a 226 + */ 227 + (a) => { 228 + return { bytes: a.data, mime: a.format }; 229 + }, 230 + ) || []; 231 + 232 + art.push(...fromMeta); 233 + 234 + // If no artwork, try finding it on other sources 235 + if (art.length === 0) { 236 + const fromMusicBrainz = await musicBrainz(req); 237 + art.push(...fromMusicBrainz); 238 + } 239 + 240 + if (art.length === 0) { 241 + const fromLastFm = await lastFm(req); 242 + art.push(...fromLastFm); 243 + } 244 + 245 + // Save artwork to IDB 246 + await IDB.set(`${IDB_ARTWORK_PREFIX}/${req.cacheId}`, art); 247 + 248 + // Fin 249 + return art; 250 + } 251 + 252 + async function shiftQueue() { 253 + const next = queue.shift(); 254 + if (!next) return; 255 + 256 + await processRequest(next); 257 + await shiftQueue(); 258 + }