Experiment to rebuild Diffuse using web applets.
1import { parseFromTokenizer, parseWebStream } from "music-metadata";
2import { contentType } from "@std/media-types";
3import * as URI from "uri-js";
4import * as HTTP_TOKENIZER from "@tokenizer/http";
5import * as RANGE_TOKENIZER from "@tokenizer/range";
6
7import type { TrackStats, TrackTags } from "@applets/core/types";
8import type { Extraction, Urls } from "./types.d.ts";
9import { expose } from "@scripts/common";
10
11////////////////////////////////////////////
12// ACTIONS
13////////////////////////////////////////////
14const actions = expose({
15 supply,
16});
17
18export type Actions = typeof actions;
19
20// Actions
21
22async 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 response;
39}
40
41////////////////////////////////////////////
42// 🛠️
43////////////////////////////////////////////
44async function musicMetadataTags({
45 includeArtwork,
46 mimeType,
47 stream,
48 urls,
49}: {
50 includeArtwork?: boolean;
51 mimeType?: string;
52 stream?: ReadableStream;
53 urls?: Urls;
54}): Promise<Extraction> {
55 const uri = urls ? URI.parse(urls.get) : undefined;
56 const pathParts = uri?.path?.split("/");
57 const filename = pathParts?.[pathParts.length - 1];
58
59 let meta;
60
61 if (urls?.get.startsWith("blob:")) {
62 const mimeFallback = filename?.includes(".")
63 ? contentType(filename.split(".").reverse()[0])
64 : undefined;
65
66 const resp = await fetch(urls.get);
67 const stream = resp.body;
68
69 if (!stream) return {};
70 meta = await parseWebStream(
71 stream,
72 { mimeType: mimeType || mimeFallback },
73 { skipCovers: !includeArtwork },
74 );
75 } else if (urls) {
76 const httpClient = new HTTP_TOKENIZER.HttpClient(urls.head, { resolveUrl: false });
77 httpClient.resolvedUrl = urls.get;
78
79 const tokenizer = await RANGE_TOKENIZER.tokenizer(httpClient);
80
81 meta = await parseFromTokenizer(tokenizer, { skipCovers: !includeArtwork });
82 } else if (stream) {
83 meta = await parseWebStream(stream, { mimeType }, { skipCovers: !includeArtwork });
84 } else {
85 throw new Error("Missing args, need either some urls or a stream.");
86 }
87
88 const stats: TrackStats = {
89 duration: meta.format.duration,
90 };
91
92 const tags: TrackTags = {
93 album: meta.common.album,
94 artist: meta.common.artist,
95 disc: { no: meta.common.disk.no || 1, of: meta.common.disk.of ?? undefined },
96 genre: Array.isArray(meta.common.genre) ? meta.common.genre[0] : meta.common.genre,
97 title: meta.common.title || filename || urls?.head || "Unknown",
98 track: { no: meta.common.track.no || 1, of: meta.common.track.of ?? undefined },
99 year: meta.common.year,
100 };
101
102 return {
103 artwork: includeArtwork ? meta.common.picture : undefined,
104 stats,
105 tags,
106 };
107}