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 bc7ea05fe3cee7f92fe4e87298e2da29b39e4019 158 lines 5.2 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 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}