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: add metadata processor

+270 -329
-34
_backup/pages/processor/metadata/_applet.astro
··· 1 - <script> 2 - import type { Tasks } from "@scripts/processor/metadata/worker"; 3 - import type { Extraction, Urls } from "./types"; 4 - import { register } from "@scripts/applet/common"; 5 - import { endpoint, SharedWorker, transfer } from "@scripts/common"; 6 - import manifest from "./_manifest.json"; 7 - 8 - //////////////////////////////////////////// 9 - // SETUP 10 - //////////////////////////////////////////// 11 - const worker = endpoint<Tasks>( 12 - new SharedWorker(new URL("../../../scripts/processor/metadata/worker", import.meta.url), { 13 - type: "module", 14 - name: manifest.name, 15 - }).port, 16 - ); 17 - 18 - // Register applet 19 - const context = register({ worker }); 20 - 21 - //////////////////////////////////////////// 22 - // ACTIONS 23 - //////////////////////////////////////////// 24 - async function supply(args: { 25 - includeArtwork?: boolean; 26 - mimeType?: string; 27 - stream?: ReadableStream; 28 - urls?: Urls; 29 - }): Promise<Extraction> { 30 - return worker.supply(transfer(args)); 31 - } 32 - 33 - context.setActionHandler("supply", supply); 34 - </script>
-34
_backup/pages/processor/metadata/_manifest.json
··· 1 - { 2 - "name": "diffuse/processor/metadata", 3 - "title": "Diffuse Processor | Metadata fetcher", 4 - "entrypoint": "index.html", 5 - "actions": { 6 - "supply": { 7 - "title": "Supply", 8 - "description": "Get the metadata for a given URL or stream.", 9 - "params_schema": { 10 - "type": "object", 11 - "properties": { 12 - "includeArtwork": { 13 - "type": "boolean", 14 - "description": "Include artwork in the output." 15 - }, 16 - "mimeType": { 17 - "type": "string" 18 - }, 19 - "stream": { 20 - "type": "object" 21 - }, 22 - "urls": { 23 - "type": "object", 24 - "properties": { 25 - "get": { "type": "string" }, 26 - "head": { "type": "string" } 27 - }, 28 - "required": ["get", "head"] 29 - } 30 - } 31 - } 32 - } 33 - } 34 - }
-9
_backup/pages/processor/metadata/index.astro
··· 1 - --- 2 - import Layout from "@layouts/applet.astro"; 3 - import Applet from "./_applet.astro"; 4 - import { title } from "./_manifest.json"; 5 - --- 6 - 7 - <Layout title={title}> 8 - <Applet /> 9 - </Layout>
-1
_backup/pages/processor/metadata/types.d.ts
··· 1 - export * from "../../../scripts/processor/metadata/types.d.ts";
-70
_backup/scripts/processor/metadata/common.ts
··· 1 - import { parseBlob, parseFromTokenizer, parseWebStream } from "music-metadata"; 2 - import * as URI from "uri-js"; 3 - import * as HTTP_TOKENIZER from "@tokenizer/http"; 4 - import * as RANGE_TOKENIZER from "@tokenizer/range"; 5 - 6 - import type { TrackStats, TrackTags } from "@applets/core/types"; 7 - import type { Extraction, Urls } from "./types"; 8 - 9 - // 🛠️ 10 - 11 - export async function musicMetadataTags({ 12 - includeArtwork, 13 - mimeType, 14 - stream, 15 - urls, 16 - }: { 17 - includeArtwork?: boolean; 18 - mimeType?: string; 19 - stream?: ReadableStream; 20 - urls?: Urls; 21 - }): Promise<Extraction> { 22 - const uri = urls ? URI.parse(urls.get) : undefined; 23 - const pathParts = uri?.path?.split("/"); 24 - const filename = pathParts?.[pathParts.length - 1]; 25 - 26 - let meta; 27 - 28 - if (urls?.get.startsWith("blob:")) { 29 - const blob = await fetch(urls.get).then((r) => r.blob()); 30 - meta = await parseBlob(blob, { skipCovers: !includeArtwork }); 31 - } else if (urls) { 32 - const httpClient = new HTTP_TOKENIZER.HttpClient(urls.head, { resolveUrl: false }); 33 - httpClient.resolvedUrl = urls.get; 34 - const getHeadInfo = httpClient.getHeadInfo; 35 - 36 - // FUCKAROUND: Not sure of the downsides of this 37 - httpClient.getHeadInfo = async () => { 38 - const info = await getHeadInfo.call(httpClient); 39 - return { ...info, acceptPartialRequests: true }; 40 - }; 41 - 42 - const tokenizer = await RANGE_TOKENIZER.tokenizer(httpClient); 43 - 44 - meta = await parseFromTokenizer(tokenizer, { skipCovers: !includeArtwork }); 45 - } else if (stream) { 46 - meta = await parseWebStream(stream, { mimeType }, { skipCovers: !includeArtwork }); 47 - } else { 48 - throw new Error("Missing args, need either some urls or a stream."); 49 - } 50 - 51 - const stats: TrackStats = { 52 - duration: meta.format.duration, 53 - }; 54 - 55 - const tags: TrackTags = { 56 - album: meta.common.album, 57 - artist: meta.common.artist, 58 - disc: { no: meta.common.disk.no || 1, of: meta.common.disk.of ?? undefined }, 59 - genre: Array.isArray(meta.common.genre) ? meta.common.genre[0] : meta.common.genre, 60 - title: meta.common.title || filename || urls?.head || "Unknown", 61 - track: { no: meta.common.track.no || 1, of: meta.common.track.of ?? undefined }, 62 - year: meta.common.year, 63 - }; 64 - 65 - return { 66 - artwork: includeArtwork ? meta.common.picture : undefined, 67 - stats, 68 - tags, 69 - }; 70 - }
-5
_backup/scripts/processor/metadata/types.d.ts
··· 1 - import type { IPicture } from "music-metadata"; 2 - import type { TrackStats, TrackTags } from "@applets/core/types"; 3 - 4 - export type Extraction = { artwork?: IPicture[]; stats?: TrackStats; tags?: TrackTags }; 5 - export type Urls = { get: string; head: string };
-39
_backup/scripts/processor/metadata/worker.ts
··· 1 - import type { Extraction, Urls } from "./types.d.ts"; 2 - import { provide, transfer } from "@scripts/common"; 3 - import { musicMetadataTags } from "./common.ts"; 4 - 5 - //////////////////////////////////////////// 6 - // ACTIONS 7 - //////////////////////////////////////////// 8 - const actions = { 9 - supply, 10 - }; 11 - 12 - const { tasks } = provide({ 13 - actions, 14 - tasks: actions, 15 - }); 16 - 17 - export type Actions = typeof actions; 18 - export type Tasks = typeof tasks; 19 - 20 - // Actions 21 - 22 - async function supply(args: { 23 - includeArtwork?: boolean; 24 - mimeType?: string; 25 - stream?: ReadableStream; 26 - urls?: Urls; 27 - }): Promise<Extraction> { 28 - // Construct records 29 - // TODO: Use other metadata lib as fallback: https://github.com/buzz/mediainfo.js 30 - const response = await musicMetadataTags(args).catch((err): Extraction => { 31 - console.warn("Metadata processor error:", err); 32 - console.log(args); 33 - 34 - return {}; 35 - }); 36 - 37 - // Fin 38 - return transfer(response); 39 - }
+6
deno.jsonc
··· 9 9 "@okikio/transferables": "jsr:@okikio/transferables@^1.0.2", 10 10 "alien-signals": "npm:alien-signals@^3.0.0", 11 11 "morphdom": "npm:morphdom@^2.7.7/dist/morphdom.js", 12 + "uri-js": "npm:uri-js@^4.4.1", 12 13 "xxh32": "npm:xxh32@^2.0.5", 14 + 15 + // music-metadata 16 + "@tokenizer/http": "https://esm.sh/@tokenizer/http@0.9.2/lib/http-client.js", 17 + "@tokenizer/range": "https://esm.sh/@tokenizer/range@0.13.0/lib/index.js", 18 + "music-metadata": "https://esm.sh/music-metadata@11.9.0/lib/core.js", 13 19 14 20 // Paths 15 21 "@common/": "./src/common/",
+21 -103
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", 9 5 "jsr:@std/cli@1.0.22": "1.0.22", 10 6 "jsr:@std/cli@^1.0.21": "1.0.22", 11 7 "jsr:@std/collections@^1.1.3": "1.1.3", ··· 21 17 "jsr:@std/http@1.0.20": "1.0.20", 22 18 "jsr:@std/internal@^1.0.10": "1.0.12", 23 19 "jsr:@std/internal@^1.0.9": "1.0.12", 24 - "jsr:@std/json@^1.0.2": "1.0.2", 25 20 "jsr:@std/jsonc@1.0.2": "1.0.2", 26 21 "jsr:@std/media-types@^1.1.0": "1.1.0", 27 22 "jsr:@std/net@^1.0.4": "1.0.6", ··· 34 29 "jsr:@std/toml@^1.0.3": "1.0.10", 35 30 "jsr:@std/yaml@1.0.9": "1.0.9", 36 31 "jsr:@std/yaml@^1.0.5": "1.0.9", 37 - "npm:@types/node@*": "24.2.0", 38 - "npm:alien-signals@3": "3.0.1", 32 + "npm:alien-signals@3": "3.0.3", 39 33 "npm:autoprefixer@10.4.21": "10.4.21_postcss@8.5.6", 40 - "npm:broadcast-channel@^7.1.0": "7.1.0", 41 34 "npm:lightningcss-wasm@1.30.1": "1.30.1", 42 35 "npm:markdown-it-attrs@4.3.1": "4.3.1_markdown-it@14.1.0", 43 36 "npm:markdown-it-deflist@3.0.0": "3.0.0", ··· 45 38 "npm:morphdom@^2.7.7": "2.7.7", 46 39 "npm:postcss-import@16.1.1": "16.1.1_postcss@8.5.6", 47 40 "npm:postcss@8.5.6": "8.5.6", 48 - "npm:tab-election@^4.2.8": "4.2.8", 41 + "npm:uri-js@^4.4.1": "4.4.1", 49 42 "npm:xxh32@^2.0.5": "2.0.5" 50 43 }, 51 44 "jsr": { 52 45 "@deno/loader@0.3.6": { 53 46 "integrity": "98f08d837c18ece5ba15122264fb29580967407c34e6552e152b8f453a60c2be" 54 47 }, 55 - "@fry69/deep-diff@0.1.10": { 56 - "integrity": "cdd88fefaef1ac896a038a5f3c0895038d8c725e61bac50489c455156e0275f5" 57 - }, 58 - "@mys/m-rpc@0.12.2": { 59 - "integrity": "36599d3d4708db9f5c0f7da35a17b7e7da1fafddb69de6cfcdc6afe94cd4f084", 60 - "dependencies": [ 61 - "jsr:@okikio/transferables" 62 - ] 63 - }, 64 - "@mys/worker-fn@3.2.1": { 65 - "integrity": "330960f21041edd20fa9c5f78b136f62e3781e35797ac635534f003545be76cd", 66 - "dependencies": [ 67 - "jsr:@mys/m-rpc" 68 - ] 69 - }, 70 - "@okikio/transferables@1.0.2": { 71 - "integrity": "46a80015a1c4672b0b246e38838b3ea1e2edc6c775a235184a2f8eb49a8314f7" 72 - }, 73 48 "@std/cli@1.0.22": { 74 49 "integrity": "50d1e4f87887cb8a8afa29b88505ab5081188f5cad3985460c3b471fa49ff21a" 75 50 }, ··· 119 94 "@std/internal@1.0.12": { 120 95 "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" 121 96 }, 122 - "@std/json@1.0.2": { 123 - "integrity": "d9e5497801c15fb679f55a2c01c7794ad7a5dfda4dd1bebab5e409cb5e0d34d4" 124 - }, 125 97 "@std/jsonc@1.0.2": { 126 - "integrity": "909605dae3af22bd75b1cbda8d64a32cf1fd2cf6efa3f9e224aba6d22c0f44c7", 127 - "dependencies": [ 128 - "jsr:@std/json" 129 - ] 98 + "integrity": "909605dae3af22bd75b1cbda8d64a32cf1fd2cf6efa3f9e224aba6d22c0f44c7" 130 99 }, 131 100 "@std/media-types@1.1.0": { 132 101 "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" ··· 157 126 } 158 127 }, 159 128 "npm": { 160 - "@babel/runtime@7.27.0": { 161 - "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", 162 - "dependencies": [ 163 - "regenerator-runtime" 164 - ] 165 - }, 166 - "@types/node@24.2.0": { 167 - "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", 168 - "dependencies": [ 169 - "undici-types" 170 - ] 171 - }, 172 - "alien-signals@3.0.1": { 173 - "integrity": "sha512-ec02Wv5iOg7yG979PH9ykv5KN/KHznOxMlKy/Jr8lnBo3T94d4MUGo7FVdM8B2fM0e94twzEcWCyWzfIyeV19g==" 129 + "alien-signals@3.0.3": { 130 + "integrity": "sha512-2JXjom6R7ZwrISpUphLhf4htUq1aKRCennTJ6u9kFfr3sLmC9+I4CxxVi+McoFnIg+p1HnVrfLT/iCt4Dlz//Q==" 174 131 }, 175 132 "argparse@2.0.1": { 176 133 "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" ··· 188 145 ], 189 146 "bin": true 190 147 }, 191 - "baseline-browser-mapping@2.8.18": { 192 - "integrity": "sha512-UYmTpOBwgPScZpS4A+YbapwWuBwasxvO/2IOHArSsAhL/+ZdmATBXTex3t+l2hXwLVYK382ibr/nKoY9GKe86w==", 148 + "baseline-browser-mapping@2.8.19": { 149 + "integrity": "sha512-zoKGUdu6vb2jd3YOq0nnhEDQVbPcHhco3UImJrv5dSkvxTc2pl2WjOPsjZXDwPDSl5eghIMuY3R6J9NDKF3KcQ==", 193 150 "bin": true 194 151 }, 195 - "broadcast-channel@7.1.0": { 196 - "integrity": "sha512-InJljddsYWbEL8LBnopnCg+qMQp9KcowvYWOt4YWrjD5HmxzDYKdVbDS1w/ji5rFZdRD58V5UxJPtBdpEbEJYw==", 197 - "dependencies": [ 198 - "@babel/runtime", 199 - "oblivious-set", 200 - "p-queue", 201 - "unload" 202 - ] 203 - }, 204 152 "browserslist@4.26.3": { 205 153 "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", 206 154 "dependencies": [ ··· 223 171 }, 224 172 "escalade@3.2.0": { 225 173 "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" 226 - }, 227 - "eventemitter3@4.0.7": { 228 - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" 229 174 }, 230 175 "fraction.js@4.3.7": { 231 176 "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==" ··· 285 230 "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", 286 231 "bin": true 287 232 }, 288 - "node-releases@2.0.25": { 289 - "integrity": "sha512-4auku8B/vw5psvTiiN9j1dAOsXvMoGqJuKJcR+dTdqiXEK20mMTk1UEo3HS16LeGQsVG6+qKTPM9u/qQ2LqATA==" 233 + "node-releases@2.0.26": { 234 + "integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==" 290 235 }, 291 236 "normalize-range@0.1.2": { 292 237 "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==" 293 238 }, 294 - "oblivious-set@1.4.0": { 295 - "integrity": "sha512-szyd0ou0T8nsAqHtprRcP3WidfsN1TnAR5yWXf2mFCEr5ek3LEOkT6EZ/92Xfs74HIdyhG5WkGxIssMU0jBaeg==" 296 - }, 297 - "p-finally@1.0.0": { 298 - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==" 299 - }, 300 - "p-queue@6.6.2": { 301 - "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", 302 - "dependencies": [ 303 - "eventemitter3", 304 - "p-timeout" 305 - ] 306 - }, 307 - "p-timeout@3.2.0": { 308 - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", 309 - "dependencies": [ 310 - "p-finally" 311 - ] 312 - }, 313 239 "path-parse@1.0.7": { 314 240 "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" 315 241 }, ··· 342 268 "punycode.js@2.3.1": { 343 269 "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==" 344 270 }, 271 + "punycode@2.3.1": { 272 + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" 273 + }, 345 274 "read-cache@1.0.0": { 346 275 "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", 347 276 "dependencies": [ 348 277 "pify" 349 278 ] 350 279 }, 351 - "regenerator-runtime@0.14.1": { 352 - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" 353 - }, 354 - "resolve@1.22.10": { 355 - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", 280 + "resolve@1.22.11": { 281 + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", 356 282 "dependencies": [ 357 283 "is-core-module", 358 284 "path-parse", ··· 366 292 "supports-preserve-symlinks-flag@1.0.0": { 367 293 "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" 368 294 }, 369 - "tab-election@4.2.8": { 370 - "integrity": "sha512-qHmh4jCMh1KKppqhIz3VWQPXGicCWaq2xtx9a37ZQroqbcXmCVUmmaf2rvFb0WThoJr9iaimK0PyHTxPkXcrDQ==" 371 - }, 372 295 "uc.micro@2.1.0": { 373 296 "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" 374 297 }, 375 - "undici-types@7.10.0": { 376 - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==" 377 - }, 378 - "unload@2.4.1": { 379 - "integrity": "sha512-IViSAm8Z3sRBYA+9wc0fLQmU9Nrxb16rcDmIiR6Y9LJSZzI7QY5QsDhqPpKOjAn0O9/kfK1TfNEMMAGPTIraPw==" 380 - }, 381 298 "update-browserslist-db@1.1.3_browserslist@4.26.3": { 382 299 "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", 383 300 "dependencies": [ ··· 387 304 ], 388 305 "bin": true 389 306 }, 307 + "uri-js@4.4.1": { 308 + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 309 + "dependencies": [ 310 + "punycode" 311 + ] 312 + }, 390 313 "xxh32@2.0.5": { 391 314 "integrity": "sha512-glQIaPvLHV4xG2Sn0E4mZWY25JT34+XcG4e2c8OMIH2SXxVrm6MmJ8miCsqGBLtf+rn2YcaeS11vq/66vkXGUQ==" 392 315 } 393 316 }, 394 317 "remote": { 395 - "https://cdn.jsdelivr.net/gh/lumeland/bar@0.1.11/types.ts": "38f3714e1432c174009495333972f85fb306eb6313112ac8830fda9f1f47e87f", 396 318 "https://deno.land/std@0.170.0/_util/asserts.ts": "d0844e9b62510f89ce1f9878b046f6a57bf88f208a10304aab50efcb48365272", 397 319 "https://deno.land/std@0.170.0/_util/os.ts": "8a33345f74990e627b9dfe2de9b040004b08ea5146c7c9e8fe9a29070d193934", 398 320 "https://deno.land/std@0.170.0/encoding/base64.ts": "8605e018e49211efc767686f6f687827d7f5fd5217163e981d8d693105640d7a", ··· 598 520 "https://deno.land/x/lume@v3.0.11/deps/toml.ts": "32830bda333eaf4f1c3d79e4306ba449c17a85b25f94aae9b327d3790a2d1dea", 599 521 "https://deno.land/x/lume@v3.0.11/deps/vento.ts": "78db4022ee124fbcfd84caeb6c5a70f2c1e1706ec9f6415d0f1fe2e9aabcba2b", 600 522 "https://deno.land/x/lume@v3.0.11/deps/yaml.ts": "a639f4fc44ddcfc87f35e38980bbe9fc8101bf8ce34867522e76cc13cb156611", 601 - "https://deno.land/x/lume@v3.0.11/lint.ts": "23cf68a7cc17edfdb16f2e905de3c5d5a1da541638f04fb8f7d5c762288f2c52", 602 523 "https://deno.land/x/lume@v3.0.11/middlewares/basic_auth.ts": "c18f0da9f88be4581e5e3da99214fd7abdad829ab00dbdd2fb3116f1f876add2", 603 524 "https://deno.land/x/lume@v3.0.11/middlewares/logger.ts": "c96f1a9f9d5757555b6f141865ce8551ac176f90c8ee3e9ad797b2b400a9a567", 604 525 "https://deno.land/x/lume@v3.0.11/middlewares/no_cache.ts": "0119e3ae3a596ab12c42df693b93e5b03dd9608e289d862242751a9739438f35", ··· 620 541 "https://deno.land/x/lume@v3.0.11/plugins/url.ts": "15f2e80b6fcbf86f8795a3676b8d533bab003ac016ff127e58165a6ac3bffc1a", 621 542 "https://deno.land/x/lume@v3.0.11/plugins/vento.ts": "fd60ee80435994bcf88b2cda9c51eaed0ba49a2363f42920675f2d5a0a4a6ab2", 622 543 "https://deno.land/x/lume@v3.0.11/plugins/yaml.ts": "d0ebf37c38648172c6b95c502753a3edf60278ab4f6a063f3ca00f31e0dd90cc", 623 - "https://deno.land/x/lume@v3.0.11/types.ts": "5f580502f366b9b25106eb72d49b30d9af7715c8a304fe6e21f382d3c2a4cc38", 624 - "https://deno.land/x/ssx@v0.1.12/jsx-runtime.ts": "a334a1ee3a25de7f3b84b7b8d842bcae40e9116f6edb6ec76cb265712c8a2ab8", 625 544 "https://deno.land/x/vento@v2.1.1/core/environment.ts": "36f3e145adfe1af3740cfcfc6ff237d6fe48225d3627123b17022251afbe3074", 626 545 "https://deno.land/x/vento@v2.1.1/core/errors.ts": "8606b682b465d598a394feea135dd2f84033b5ef2a61a23d116ccb782a0a547a", 627 546 "https://deno.land/x/vento@v2.1.1/core/js.ts": "83084240150d7e8b83e43ec8fcf78564a8ba8599c3d517976efbb11b208903b2", ··· 654 573 "jsr:@std/fs@^1.0.19", 655 574 "jsr:@std/path@^1.1.2", 656 575 "npm:alien-signals@3", 657 - "npm:broadcast-channel@^7.1.0", 658 576 "npm:morphdom@^2.7.7", 659 - "npm:tab-election@^4.2.8", 577 + "npm:uri-js@^4.4.1", 660 578 "npm:xxh32@^2.0.5" 661 579 ] 662 580 }
+4 -1
src/common/worker.js
··· 118 118 context = /** @type {WorkerGlobalScope} */ (globalThis), 119 119 options, 120 120 ) { 121 - return useWorkerFn(name, /** @type {any} */ (context), options); 121 + return useWorkerFn(name, /** @type {any} */ (context), { 122 + timeout: 60000, 123 + ...(options || {}), 124 + }); 122 125 } 123 126 124 127 ////////////////////////////////////////////
+9 -9
src/component/engine/audio/types.d.ts
··· 1 - import type { Signal } from "../../../common/signal.d.ts"; 1 + import type { Signal } from "@common/signal.d.ts"; 2 2 3 - export interface Actions { 3 + export type Actions = { 4 4 pause: (_: { audioId: string }) => void; 5 5 play: (_: { audioId: string; volume?: number }) => void; 6 6 reload: (_: { audioId: string; play: boolean; progress?: number }) => void; ··· 8 8 supply: ( 9 9 _: { audio: Audio[]; play?: { audioId: string; volume?: number } }, 10 10 ) => void; 11 - } 11 + }; 12 12 13 - export interface Audio { 13 + export type Audio = { 14 14 id: string; 15 15 isPreload: boolean; 16 16 mimeType?: string; 17 17 progress?: number; 18 18 url: string; 19 - } 19 + }; 20 20 21 - export interface AudioState { 21 + export type AudioState = { 22 22 duration: number; 23 23 id: string; 24 24 hasEnded: boolean; ··· 34 34 mimeType?: string; 35 35 progress: number; 36 36 url: string; 37 - } 37 + }; 38 38 39 - export interface Signals { 39 + export type Signals = { 40 40 isPlaying: Signal<boolean>; 41 41 volume: Signal<number>; 42 - } 42 + }; 43 43 44 44 export type State = Signals & { 45 45 items: Signal<Audio[]>;
+9 -11
src/component/engine/queue/types.d.ts
··· 1 - import type { 2 - Track, 3 - TrackStats, 4 - TrackTags, 5 - } from "../../../component/core/types.d.ts"; 6 - import type { Signal } from "../../../common/signal.d.ts"; 1 + import type { Track, TrackStats, TrackTags } from "@component/core/types.d.ts"; 2 + import type { Signal } from "@common/signal.d.ts"; 7 3 8 - export interface Actions { 4 + export type Actions = { 9 5 add: (items: Item[]) => void; 10 - // TODO 11 - } 6 + pool: (tracks: Track[]) => void; 7 + shift: () => void; 8 + unshift: () => void; 9 + }; 12 10 13 11 export type Item<Stats = TrackStats, Tags = TrackTags> = 14 12 & Track<Stats, Tags> 15 13 & { manualEntry?: boolean }; 16 14 17 - export interface Signals { 15 + export type Signals = { 18 16 future: Signal<Item[]>; 19 17 now: Signal<Item | null>; 20 18 past: Signal<Item[]>; 21 - } 19 + };
+18 -12
src/component/engine/queue/worker.js
··· 3 3 import { arrayShuffle } from "@common/index.js"; 4 4 5 5 /** 6 - * @import {Item} from "./types.d.ts" 6 + * @import {Actions, Item} from "./types.d.ts" 7 7 * @import {Track} from "@component/core/types.d.ts" 8 8 */ 9 9 ··· 13 13 // STATE 14 14 //////////////////////////////////////////// 15 15 16 - const future = signal(/** @type {Item[]} */ ([])); 17 - const lake = signal(/** @type {Track[]} */ ([])); 18 - const now = signal(/** @type {Item | null} */ (null)); 19 - const past = signal(/** @type {Item[]} */ ([])); 16 + export const future = signal(/** @type {Item[]} */ ([])); 17 + export const lake = signal(/** @type {Track[]} */ ([])); 18 + export const now = signal(/** @type {Item | null} */ (null)); 19 + export const past = signal(/** @type {Item[]} */ ([])); 20 20 21 21 //////////////////////////////////////////// 22 22 // ACTIONS 23 23 //////////////////////////////////////////// 24 24 25 25 /** 26 - * @param {Item[]} items 26 + * @type {Actions['add']} 27 27 */ 28 - function add(items) { 28 + export function add(items) { 29 29 future([...future(), ...items]); 30 30 } 31 31 32 32 /** 33 - * @param {Track[]} tracks 33 + * @type {Actions['pool']} 34 34 */ 35 - function pool(tracks) { 35 + export function pool(tracks) { 36 36 lake(tracks); 37 37 38 38 // TODO: If the pool changes, only remove non-existing tracks ··· 46 46 if (!now()) return shift(); 47 47 } 48 48 49 - function shift() { 49 + /** 50 + * @type {Actions['shift']} 51 + */ 52 + export function shift() { 50 53 const n = now(); 51 54 const f = future(); 52 55 ··· 56 59 future(fill(f.slice(1))); 57 60 } 58 61 59 - function unshift() { 62 + /** 63 + * @type {Actions['unshift']} 64 + */ 65 + export function unshift() { 60 66 const p = past(); 61 67 if (p.length === 0) return; 62 68 ··· 83 89 define("shift", shift, port); 84 90 define("unshift", unshift, port); 85 91 86 - // Communicate 92 + // Communicate state 87 93 88 94 effect(() => announce("future", future(), port)); 89 95 effect(() => announce("now", now(), port));
+86
src/component/processor/metadata/common.js
··· 1 + import { parseBlob, parseFromTokenizer, parseWebStream } from "music-metadata"; 2 + import * as URI from "uri-js"; 3 + import { HttpClient } from "@tokenizer/http"; 4 + import { tokenizer as rangeTokenizer } from "@tokenizer/range"; 5 + 6 + /** 7 + * @import { TrackStats, TrackTags } from "@component/core/types.d.ts"; 8 + * @import { Extraction, Urls } from "./types.d.ts"; 9 + */ 10 + 11 + // 🛠️ 12 + 13 + /** 14 + * @param {{ includeArtwork?: boolean; mimeType?: string; stream?: ReadableStream; urls?: Urls; }} _ 15 + * @returns {Promise<Extraction>} 16 + */ 17 + export async function musicMetadataTags({ 18 + includeArtwork, 19 + mimeType, 20 + stream, 21 + urls, 22 + }) { 23 + const uri = urls ? URI.parse(urls.get) : undefined; 24 + const pathParts = uri?.path?.split("/"); 25 + const filename = pathParts?.[pathParts.length - 1]; 26 + 27 + let meta; 28 + 29 + if (urls?.get.startsWith("blob:")) { 30 + const blob = await fetch(urls.get).then((r) => r.blob()); 31 + meta = await parseBlob(blob, { skipCovers: !includeArtwork }); 32 + } else if (urls) { 33 + const httpClient = new HttpClient(urls.head, { 34 + resolveUrl: false, 35 + }); 36 + httpClient.resolvedUrl = urls.get; 37 + const getHeadInfo = httpClient.getHeadInfo; 38 + 39 + // FUCKAROUND: Not sure of the downsides of this 40 + httpClient.getHeadInfo = async () => { 41 + const info = await getHeadInfo.call(httpClient); 42 + return { ...info, acceptPartialRequests: true }; 43 + }; 44 + 45 + /** @type {any} */ 46 + const tokenizer = await rangeTokenizer(httpClient); 47 + 48 + meta = await parseFromTokenizer(tokenizer, { skipCovers: !includeArtwork }); 49 + } else if (stream) { 50 + meta = await parseWebStream(stream, { mimeType }, { 51 + skipCovers: !includeArtwork, 52 + }); 53 + } else { 54 + throw new Error("Missing args, need either some urls or a stream."); 55 + } 56 + 57 + /** @type {TrackStats} */ 58 + const stats = { 59 + duration: meta.format.duration, 60 + }; 61 + 62 + /** @type {TrackTags} */ 63 + const tags = { 64 + album: meta.common.album, 65 + artist: meta.common.artist, 66 + disc: { 67 + no: meta.common.disk.no || 1, 68 + of: meta.common.disk.of ?? undefined, 69 + }, 70 + genre: Array.isArray(meta.common.genre) 71 + ? meta.common.genre[0] 72 + : meta.common.genre, 73 + title: meta.common.title || filename || urls?.head || "Unknown", 74 + track: { 75 + no: meta.common.track.no || 1, 76 + of: meta.common.track.of ?? undefined, 77 + }, 78 + year: meta.common.year, 79 + }; 80 + 81 + return { 82 + artwork: includeArtwork ? meta.common.picture : undefined, 83 + stats, 84 + tags, 85 + }; 86 + }
+39
src/component/processor/metadata/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 MetadataProcessor 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.supply = use("supply", worker); 29 + } 30 + } 31 + 32 + export default MetadataProcessor; 33 + 34 + //////////////////////////////////////////// 35 + // REGISTER 36 + //////////////////////////////////////////// 37 + 38 + export const NAME = "dp-metadata"; 39 + customElements.define(NAME, MetadataProcessor);
+21
src/component/processor/metadata/types.d.ts
··· 1 + import type { IPicture } from "music-metadata"; 2 + import type { TrackStats, TrackTags } from "@component/core/types.d.ts"; 3 + 4 + export type Actions = { 5 + supply: ( 6 + args: { 7 + includeArtwork?: boolean; 8 + mimeType?: string; 9 + stream?: ReadableStream; 10 + urls?: Urls; 11 + }, 12 + ) => Promise<Extraction>; 13 + }; 14 + 15 + export type Extraction = { 16 + artwork?: IPicture[]; 17 + stats?: TrackStats; 18 + tags?: TrackTags; 19 + }; 20 + 21 + export type Urls = { get: string; head: string };
+43
src/component/processor/metadata/worker.js
··· 1 + import { define, ostiary } from "@common/worker.js"; 2 + import { musicMetadataTags } from "./common.js"; 3 + 4 + /** 5 + * @import { Actions, Extraction } from "./types.d.ts"; 6 + */ 7 + 8 + //////////////////////////////////////////// 9 + // ACTIONS 10 + //////////////////////////////////////////// 11 + 12 + /** 13 + * @type {Actions['supply']} 14 + */ 15 + export async function supply(args) { 16 + console.log(args); 17 + 18 + // Construct records 19 + // TODO: Use other metadata lib as fallback: https://github.com/buzz/mediainfo.js 20 + return await musicMetadataTags(args).catch( 21 + /** 22 + * @param {Error} err 23 + * @returns {Extraction} 24 + */ 25 + (err) => { 26 + console.warn("Metadata processor error:", err); 27 + console.log(args); 28 + 29 + return {}; 30 + }, 31 + ); 32 + } 33 + 34 + //////////////////////////////////////////// 35 + // ⚡️ 36 + //////////////////////////////////////////// 37 + 38 + ostiary((port) => { 39 + console.log("SETUP"); 40 + 41 + // Setup RPC 42 + define("supply", supply, port); 43 + });
+14 -1
src/theme/blur/index.vto
··· 7 7 <de-audio id="deck-a"></de-audio> 8 8 <de-queue id="deck-a"></de-queue> 9 9 10 + <dp-metadata></dp-metadata> 11 + 10 12 <main> 11 13 </main> 12 14 13 15 <script type="module"> 14 16 import * as Audio from "../../component/engine/audio/element.js"; 15 17 import * as Queue from "../../component/engine/queue/element.js"; 18 + import * as Metadata from "../../component/processor/metadata/element.js" 19 + 16 20 import { effect } from "../../common/signal.js" 17 21 18 22 const audio = document.querySelector(Audio.NAME) 19 23 const queue = document.querySelector(Queue.NAME) 24 + const metadata = document.querySelector(Metadata.NAME) 25 + 26 + const url = "https://archive.org/download/deathofsalesmans00mill/01_Side_1_Death_of_a_salesman_-_Introduction_Act_1__Part_1.mp3" 27 + 28 + const resp = await metadata.supply({ 29 + urls: { get: url, head: url } 30 + }) 31 + 32 + console.log(resp) 20 33 21 34 await audio.supply({ 22 35 audio: [ 23 36 { 24 37 id: "test", 25 38 isPreload: false, 26 - url: "https://archive.org/download/deathofsalesmans00mill/01_Side_1_Death_of_a_salesman_-_Introduction_Act_1__Part_1.mp3" 39 + url: url 27 40 } 28 41 ] 29 42 })