139 lines
4.7 KiB
TypeScript
139 lines
4.7 KiB
TypeScript
import { parseFile } from 'music-metadata';
|
|
import path from 'path';
|
|
|
|
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);
|
|
}
|
|
|
|
const filename = path.basename(filePath);
|
|
let diskNo = metadata.common.disk?.no;
|
|
let trackNo = metadata.common.track?.no;
|
|
|
|
// Extract disc and track number from multi-disc filename prefix e.g. "1-03 - Title"
|
|
const multiDiscMatch = filename.match(/^(\d+)-(\d+)\b/);
|
|
if (multiDiscMatch) {
|
|
if (!diskNo) diskNo = parseInt(multiDiscMatch[1], 10);
|
|
if (!trackNo) trackNo = parseInt(multiDiscMatch[2], 10);
|
|
}
|
|
|
|
const trackObj = {
|
|
no: trackNo ?? metadata.common.track?.no ?? null,
|
|
of: metadata.common.track?.of ?? null
|
|
};
|
|
const diskObj = {
|
|
no: diskNo ?? metadata.common.disk?.no ?? null,
|
|
of: metadata.common.disk?.of ?? null
|
|
};
|
|
|
|
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: trackObj,
|
|
disk: diskObj,
|
|
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;
|
|
}
|