Files
crossfade/lib/utils/audio.ts
T

118 lines
4.0 KiB
TypeScript

import { parseFile } from 'music-metadata';
export interface AudioMetadata {
format: {
sampleRate?: number;
bitsPerSample?: number;
duration?: number;
bitrate?: number;
};
common: {
title?: string;
artist?: string;
album?: string;
track?: { no: number | null; of: number | null };
disk?: { no: number | null; of: number | null };
year?: number;
date?: string;
copyright?: string;
barcode?: string; // UPC
artists?: string[];
albumartist?: string;
};
native?: {
tidalUrl?: string;
};
}
export async function getFlacMetadata(filePath: string): Promise<AudioMetadata> {
try {
const metadata = await parseFile(filePath);
// Extract Tidal URL from native tags if available
let tidalUrl: string | undefined;
if (metadata.native && metadata.native.vorbis) {
const urlTag = metadata.native.vorbis.find(tag => tag.id === 'URL');
if (urlTag) {
tidalUrl = String(urlTag.value);
}
//console.log(metadata.native.vorbis);
}
return {
format: {
sampleRate: metadata.format.sampleRate,
bitsPerSample: metadata.format.bitsPerSample,
duration: metadata.format.duration,
bitrate: metadata.format.bitrate,
},
common: {
title: metadata.common.title,
artist: metadata.common.artist,
album: metadata.common.album,
track: metadata.common.track,
disk: metadata.common.disk,
year: metadata.common.year,
date: metadata.common.date,
copyright: metadata.common.copyright,
barcode: (metadata.native?.vorbis?.find(tag => tag.id === 'UPC')?.value as string) || metadata.common.barcode,
artists: metadata.common.artists,
albumartist: metadata.common.albumartist,
},
native: {
tidalUrl
}
};
} catch (error) {
console.error(`Error parsing metadata for ${filePath}:`, error);
throw error;
}
}
export function parseLabel(rawLabel: string): string {
let label = rawLabel;
const labelCleaners = [
// Remove year and copyright/phonorecord symbols (strict 1900-2099 range)
(l: string) => l.replace(/(\(c\)|\(p\)|\u00A9|\u2117)?\s*\b(19|20)\d{2}\b\s*/gi, '').trim(),
// Remove leading/trailing copyright and phonorecord symbols
(l: string) => l.replace(/^(\(c\)|\(p\)|\u00A9|\u2117|\s)+/gi, '').replace(/(\(c\)|\(p\)|\u00A9|\u2117|\s)+$/gi, '').trim(),
// Handle "under exclusive license to"
(l: string) => {
const match = l.match(/under exclusive license to\s+(.*)/i);
return match ? match[1].trim() : l;
},
// Handle "exclusively distributed by"
(l: string) => {
const match = l.match(/exclusively distributed by\s+(.*)/i);
return match ? match[1].trim() : l;
},
// Handle "sob licença exclusiva de" (Portuguese)
(l: string) => {
const match = l.match(/sob licen[çc]a exclusiva de\s+(.*)/i);
return match ? match[1].trim() : l;
},
// Handle "under license to" (non-exclusive) - keep text BEFORE this
(l: string) => {
const match = l.match(/(.*)\s+under license to/i);
return match ? match[1].trim().replace(/,$/, '').trim() : l;
},
// Handle ", a division of"
(l: string) => {
const match = l.match(/(.*), a division of/i);
return match ? match[1].trim() : l;
}
];
for (const cleaner of labelCleaners) {
label = cleaner(label);
}
// Blank out if it contains "Records DK"
if (label.includes("Records DK")) {
return "";
}
return label;
}