forked from
tokono.ma/diffuse
A music player that connects to your cloud/distributed storage.
1import { parseBlob, parseFromTokenizer, parseWebStream } from "music-metadata";
2import * as URI from "fast-uri";
3import { HttpClient } from "@tokenizer/http";
4import { tokenizer as rangeTokenizer } from "@tokenizer/range";
5
6import { removeUndefinedValuesFromRecord } from "~/common/utils.js";
7
8/**
9 * @import { TrackStats, TrackTags } from "~/definitions/types.d.ts";
10 * @import { Extraction, Urls } from "~/components/metadata/audio-file/types.d.ts";
11 */
12
13// 🛠️
14
15/**
16 * @param {{ includeArtwork?: boolean; mimeType?: string; stream?: ReadableStream; urls?: Urls; }} _
17 * @returns {Promise<Extraction>}
18 */
19export async function musicMetadataTags({
20 includeArtwork,
21 mimeType,
22 stream,
23 urls,
24}) {
25 const uri = urls ? URI.parse(urls.get) : undefined;
26 const pathParts = uri?.path?.split("/");
27 const filename = pathParts?.[pathParts.length - 1];
28
29 let meta;
30
31 if (urls?.get.startsWith("blob:")) {
32 const blob = await fetch(urls.get).then((r) => r.blob());
33 meta = await parseBlob(blob, { skipCovers: !includeArtwork });
34 } else if (urls) {
35 const httpClient = new HttpClient(urls.head, {
36 resolveUrl: false,
37 });
38 httpClient.resolvedUrl = urls.get;
39 const getHeadInfo = httpClient.getHeadInfo;
40
41 // FUCKAROUND: Not sure of the downsides of this
42 httpClient.getHeadInfo = async () => {
43 const info = await getHeadInfo.call(httpClient);
44 return { ...info, acceptPartialRequests: true };
45 };
46
47 /** @type {any} */
48 const tokenizer = await rangeTokenizer(httpClient);
49 meta = await parseFromTokenizer(tokenizer, { skipCovers: !includeArtwork });
50 } else if (stream) {
51 meta = await parseWebStream(stream, { mimeType }, {
52 skipCovers: !includeArtwork,
53 });
54 } else {
55 throw new Error("Missing args, need either some urls or a stream.");
56 }
57
58 /** @type {TrackStats} */
59 const statsFull = {
60 albumGain: maybeRound(meta.format.albumGain),
61 bitrate: maybeRound(meta.format.bitrate),
62 bitsPerSample: maybeRound(meta.format.bitsPerSample),
63 codec: meta.format.codec,
64 container: meta.format.container,
65 duration: meta.format.duration != null
66 ? Math.round(meta.format.duration * 1000)
67 : undefined,
68 lossless: meta.format.lossless,
69 numberOfChannels: maybeRound(meta.format.numberOfChannels),
70 sampleRate: maybeRound(meta.format.sampleRate),
71 trackGain: maybeRound(meta.format.trackGain),
72 };
73
74 /** @type {TrackTags} */
75 const tagsFull = {
76 album: meta.common.album,
77 albumartist: meta.common.albumartist,
78 albumartists: Array.isArray(meta.common.albumartist)
79 ? meta.common.albumartist
80 : (meta.common.albumartist ? [meta.common.albumartist] : undefined),
81 albumartistsort: meta.common.albumartistsort,
82 albumsort: meta.common.albumsort,
83 arranger: meta.common.arranger,
84 artist: meta.common.artist,
85 artists: meta.common.artists ??
86 (meta.common.artist ? [meta.common.artist] : []),
87 artistsort: meta.common.artistsort,
88 asin: meta.common.asin,
89 averageLevel: meta.common.averageLevel,
90 barcode: meta.common.barcode,
91 bpm: meta.common.bpm,
92 catalognumbers: meta.common.catalognumber,
93 compilation: meta.common.compilation,
94 composers: meta.common.composer,
95 composersort: meta.common.composersort,
96 conductors: meta.common.conductor,
97 date: meta.common.date,
98 disc: {
99 no: meta.common.disk.no || 1,
100 ...(meta.common.disk.of && { of: meta.common.disk.of }),
101 },
102 djmixers: meta.common.djmixer,
103 engineers: meta.common.engineer,
104 gapless: meta.common.gapless,
105 genres: Array.isArray(meta.common.genre)
106 ? meta.common.genre
107 : meta.common.genre
108 ? [meta.common.genre]
109 : undefined,
110 isrc: meta.common.isrc,
111 labels: meta.common.label,
112 lyricists: meta.common.lyricist,
113 media: meta.common.media,
114 mixers: meta.common.mixer,
115 moods: Array.isArray(meta.common.mood)
116 ? meta.common.mood
117 : meta.common.mood
118 ? [meta.common.mood]
119 : undefined,
120 originaldate: meta.common.originaldate,
121 originalyear: meta.common.originalyear,
122 peakLevel: meta.common.peakLevel,
123 producers: meta.common.producer,
124 publishers: meta.common.publisher,
125 releasecountry: meta.common.releasecountry,
126 releasedate: meta.common.releasedate,
127 releasestatus: meta.common.releasestatus,
128 releasetypes: meta.common.releasetype,
129 remixers: meta.common.remixer,
130 technicians: meta.common.technician,
131 title: meta.common.title || filename || urls?.head || "Unknown",
132 titlesort: meta.common.titlesort,
133 track: {
134 no: meta.common.track.no || 1,
135 ...(meta.common.track.of && { of: meta.common.track.of }),
136 },
137 work: meta.common.work,
138 writers: meta.common.writer,
139 year: meta.common.year,
140 };
141
142 const stats = removeUndefinedValuesFromRecord(statsFull);
143 const tags = removeUndefinedValuesFromRecord(tagsFull);
144
145 return {
146 artwork: includeArtwork ? meta.common.picture : undefined,
147 stats,
148 tags,
149 };
150}
151
152/**
153 * @param {number | undefined} value
154 * @returns {number | undefined}
155 */
156function maybeRound(value) {
157 return typeof value === "number" ? Math.round(value) : value;
158}