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.

at v4 164 lines 5.5 kB view raw
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 /** @type {any} */ (httpClient).getHeadInfo = async () => { 43 try { 44 const info = await getHeadInfo.call(httpClient); 45 return { ...info, acceptPartialRequests: true }; 46 } catch { 47 // Some servers (e.g. Dropbox temporary links) don't return Content-Length. 48 // Fall back to downloading the full file without range requests. 49 return { size: undefined, acceptPartialRequests: false }; 50 } 51 }; 52 53 /** @type {any} */ 54 const tokenizer = await rangeTokenizer(httpClient); 55 meta = await parseFromTokenizer(tokenizer, { skipCovers: !includeArtwork }); 56 } else if (stream) { 57 meta = await parseWebStream(stream, { mimeType }, { 58 skipCovers: !includeArtwork, 59 }); 60 } else { 61 throw new Error("Missing args, need either some urls or a stream."); 62 } 63 64 /** @type {TrackStats} */ 65 const statsFull = { 66 albumGain: maybeRound(meta.format.albumGain), 67 bitrate: maybeRound(meta.format.bitrate), 68 bitsPerSample: maybeRound(meta.format.bitsPerSample), 69 codec: meta.format.codec, 70 container: meta.format.container, 71 duration: meta.format.duration != null 72 ? Math.round(meta.format.duration * 1000) 73 : undefined, 74 lossless: meta.format.lossless, 75 numberOfChannels: maybeRound(meta.format.numberOfChannels), 76 sampleRate: maybeRound(meta.format.sampleRate), 77 trackGain: maybeRound(meta.format.trackGain), 78 }; 79 80 /** @type {TrackTags} */ 81 const tagsFull = { 82 album: meta.common.album, 83 albumartist: meta.common.albumartist, 84 albumartists: Array.isArray(meta.common.albumartist) 85 ? meta.common.albumartist 86 : (meta.common.albumartist ? [meta.common.albumartist] : undefined), 87 albumartistsort: meta.common.albumartistsort, 88 albumsort: meta.common.albumsort, 89 arranger: meta.common.arranger, 90 artist: meta.common.artist, 91 artists: meta.common.artists ?? 92 (meta.common.artist ? [meta.common.artist] : []), 93 artistsort: meta.common.artistsort, 94 asin: meta.common.asin, 95 averageLevel: meta.common.averageLevel, 96 barcode: meta.common.barcode, 97 bpm: meta.common.bpm, 98 catalognumbers: meta.common.catalognumber, 99 compilation: meta.common.compilation, 100 composers: meta.common.composer, 101 composersort: meta.common.composersort, 102 conductors: meta.common.conductor, 103 date: meta.common.date, 104 disc: { 105 no: meta.common.disk.no || 1, 106 ...(meta.common.disk.of && { of: meta.common.disk.of }), 107 }, 108 djmixers: meta.common.djmixer, 109 engineers: meta.common.engineer, 110 gapless: meta.common.gapless, 111 genres: Array.isArray(meta.common.genre) 112 ? meta.common.genre 113 : meta.common.genre 114 ? [meta.common.genre] 115 : undefined, 116 isrc: meta.common.isrc, 117 labels: meta.common.label, 118 lyricists: meta.common.lyricist, 119 media: meta.common.media, 120 mixers: meta.common.mixer, 121 moods: Array.isArray(meta.common.mood) 122 ? meta.common.mood 123 : meta.common.mood 124 ? [meta.common.mood] 125 : undefined, 126 originaldate: meta.common.originaldate, 127 originalyear: meta.common.originalyear, 128 peakLevel: meta.common.peakLevel, 129 producers: meta.common.producer, 130 publishers: meta.common.publisher, 131 releasecountry: meta.common.releasecountry, 132 releasedate: meta.common.releasedate, 133 releasestatus: meta.common.releasestatus, 134 releasetypes: meta.common.releasetype, 135 remixers: meta.common.remixer, 136 technicians: meta.common.technician, 137 title: meta.common.title || filename || urls?.head || "Unknown", 138 titlesort: meta.common.titlesort, 139 track: { 140 no: meta.common.track.no || 1, 141 ...(meta.common.track.of && { of: meta.common.track.of }), 142 }, 143 work: meta.common.work, 144 writers: meta.common.writer, 145 year: meta.common.year, 146 }; 147 148 const stats = removeUndefinedValuesFromRecord(statsFull); 149 const tags = removeUndefinedValuesFromRecord(tagsFull); 150 151 return { 152 artwork: includeArtwork ? meta.common.picture : undefined, 153 stats, 154 tags, 155 }; 156} 157 158/** 159 * @param {number | undefined} value 160 * @returns {number | undefined} 161 */ 162function maybeRound(value) { 163 return typeof value === "number" ? Math.round(value) : value; 164}