import {IAudioMetadata} from "music-metadata-browser"; import ID3Writer from "browser-id3-writer"; import MetaFlac from "metaflac-js"; export const FLAC_HEADER = [0x66, 0x4C, 0x61, 0x43]; export const MP3_HEADER = [0x49, 0x44, 0x33]; export const OGG_HEADER = [0x4F, 0x67, 0x67, 0x53]; export const M4A_HEADER = [0x66, 0x74, 0x79, 0x70]; export const WMA_HEADER = [ 0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11, 0xA6, 0xD9, 0x00, 0xAA, 0x00, 0x62, 0xCE, 0x6C, ] export const WAV_HEADER = [0x52, 0x49, 0x46, 0x46] export const AAC_HEADER = [0xFF, 0xF1] export const AudioMimeType: { [key: string]: string } = { mp3: "audio/mpeg", flac: "audio/flac", m4a: "audio/mp4", ogg: "audio/ogg", wma: "audio/x-ms-wma", wav: "audio/x-wav" }; export const IXAREA_API_ENDPOINT = "https://stats.ixarea.com/apis" export function BytesHasPrefix(data: Uint8Array, prefix: number[]): boolean { if (prefix.length > data.length) return false return prefix.every((val, idx) => { return val === data[idx]; }) } export function BytesEquals(data: Uint8Array, another: Uint8Array): boolean { if (another.length != data.length) return false return data.every((val, idx) => { return val === another[idx]; }) } export function SniffAudioExt(data: Uint8Array, fallback_ext: string = "mp3"): string { if (BytesHasPrefix(data, MP3_HEADER)) return "mp3" if (BytesHasPrefix(data, FLAC_HEADER)) return "flac" if (BytesHasPrefix(data, OGG_HEADER)) return "ogg" if (data.length >= 4 + M4A_HEADER.length && BytesHasPrefix(data.slice(4), M4A_HEADER)) return "m4a" if (BytesHasPrefix(data, WAV_HEADER)) return "wav" if (BytesHasPrefix(data, WMA_HEADER)) return "wma" if (BytesHasPrefix(data, AAC_HEADER)) return "aac" return fallback_ext; } export function GetArrayBuffer(obj: Blob): Promise { if (!!obj.arrayBuffer) return obj.arrayBuffer() return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => { const rs = e.target?.result if (!rs) { reject("read file failed") } else { resolve(rs as ArrayBuffer) } }; reader.readAsArrayBuffer(obj); }); } export function GetCoverFromFile(metadata: IAudioMetadata): string { if (metadata.common?.picture && metadata.common.picture.length > 0) { return URL.createObjectURL(new Blob( [metadata.common.picture[0].data], {type: metadata.common.picture[0].format} )); } return ""; } export interface IMusicMetaBasic { title: string artist?: string } export function GetMetaFromFile(filename: string, exist_title?: string, exist_artist?: string, separator = "-") : IMusicMetaBasic { const meta: IMusicMetaBasic = {title: exist_title ?? "", artist: exist_artist} const items = filename.split(separator); if (items.length > 1) { if (!meta.artist) meta.artist = items[0].trim(); if (!meta.title) meta.title = items[1].trim(); } else if (items.length === 1) { if (!meta.title) meta.title = items[0].trim(); } return meta } export async function GetImageFromURL(src: string): Promise<{ mime: string; buffer: ArrayBuffer; url: string } | undefined> { try { const resp = await fetch(src); const mime = resp.headers.get("Content-Type"); if (mime?.startsWith("image/")) { const buffer = await resp.arrayBuffer(); const url = URL.createObjectURL(new Blob([buffer], {type: mime})) return {buffer, url, mime} } } catch (e) { console.warn(e) } } export interface IMusicMeta { title: string artists?: string[] album?: string picture?: ArrayBuffer picture_desc?: string } export function WriteMetaToMp3(audioData: ArrayBuffer, info: IMusicMeta, original: IAudioMetadata) { const writer = new ID3Writer(audioData); // reserve original data const frames = original.native['ID3v2.4'] || original.native['ID3v2.3'] || original.native['ID3v2.2'] || [] frames.forEach(frame => { if (frame.id !== 'TPE1' && frame.id !== 'TIT2' && frame.id !== 'TALB') { try { writer.setFrame(frame.id, frame.value) } catch (e) { } } }) const old = original.common writer.setFrame('TPE1', old?.artists || info.artists || []) .setFrame('TIT2', old?.title || info.title) .setFrame('TALB', old?.album || info.album || ""); if (info.picture) { writer.setFrame('APIC', { type: 3, data: info.picture, description: info.picture_desc || "Cover", }) } return writer.addTag(); } export function WriteMetaToFlac(audioData: Buffer, info: IMusicMeta, original: IAudioMetadata) { const writer = new MetaFlac(audioData) const old = original.common if (!old.title && !old.album && old.artists) { writer.setTag("TITLE=" + info.title) writer.setTag("ALBUM=" + info.album) if (info.artists) { writer.removeTag("ARTIST") info.artists.forEach(artist => writer.setTag("ARTIST=" + artist)) } } if (info.picture) { writer.importPictureFromBuffer(Buffer.from(info.picture)) } return writer.save() }