Restart repository history from target event
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Recursively copies a directory or file using streams to avoid 'copyfile' syscall issues (EINVAL)
|
||||
* on some cross-volume operations.
|
||||
*/
|
||||
export async function copyDirRecursive(src: string, dest: string) {
|
||||
const stats = await fs.stat(src);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
await fs.ensureDir(dest);
|
||||
const files = await fs.readdir(src);
|
||||
|
||||
await Promise.all(files.map(async (file) => {
|
||||
const srcPath = path.join(src, file);
|
||||
const destPath = path.join(dest, file);
|
||||
await copyDirRecursive(srcPath, destPath);
|
||||
}));
|
||||
} else {
|
||||
await copyFileStream(src, dest);
|
||||
}
|
||||
}
|
||||
|
||||
async function copyFileStream(src: string, dest: string) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const readStream = fs.createReadStream(src);
|
||||
const writeStream = fs.createWriteStream(dest);
|
||||
|
||||
readStream.on('error', reject);
|
||||
writeStream.on('error', reject);
|
||||
writeStream.on('finish', resolve);
|
||||
|
||||
readStream.pipe(writeStream);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
// The watch folder is in the project root, which is the parent of the 'src' directory.
|
||||
// process.cwd() in Next.js usually points to the project root (where package.json is).
|
||||
// If package.json is in 'src', then process.cwd() is 'src'.
|
||||
// Let's assume the structure:
|
||||
// /Project
|
||||
// /src (Next.js app)
|
||||
// /watch
|
||||
|
||||
export const WATCH_DIR_NAME = 'watch';
|
||||
export const SPECTROGRAMS_DIR_NAME = 'spectrograms';
|
||||
|
||||
export function getWatchPath(): string {
|
||||
if (process.env.WATCH_DIR) {
|
||||
return process.env.WATCH_DIR;
|
||||
}
|
||||
|
||||
// We assume the app is running from 'src' or the root containing 'src'.
|
||||
// If process.cwd() is .../Crossfade/src, then watch is ../watch
|
||||
// If process.cwd() is .../Crossfade, then watch is ./watch
|
||||
|
||||
const cwd = process.cwd();
|
||||
if (cwd.endsWith('src')) {
|
||||
return path.join(cwd, '..', WATCH_DIR_NAME);
|
||||
}
|
||||
return path.join(cwd, WATCH_DIR_NAME);
|
||||
}
|
||||
|
||||
export function getSpectrogramsPath(): string {
|
||||
const cwd = process.cwd();
|
||||
if (cwd.endsWith('src')) {
|
||||
return path.join(cwd, '..', SPECTROGRAMS_DIR_NAME);
|
||||
}
|
||||
return path.join(cwd, SPECTROGRAMS_DIR_NAME);
|
||||
}
|
||||
|
||||
export async function ensureWatchDir() {
|
||||
const watchPath = getWatchPath();
|
||||
await fs.ensureDir(watchPath);
|
||||
return watchPath;
|
||||
}
|
||||
|
||||
export async function ensureSpectrogramsDir() {
|
||||
const specPath = getSpectrogramsPath();
|
||||
await fs.ensureDir(specPath);
|
||||
return specPath;
|
||||
}
|
||||
|
||||
export function getOutputDir(): string | null {
|
||||
const outputDir = process.env.OUTPUT_DIR;
|
||||
if (!outputDir) return null;
|
||||
return outputDir;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import sanitize from 'sanitize-filename';
|
||||
|
||||
export function sanitizeName(name: string): string {
|
||||
// Use the same logic as album name to keep consistency and allow brackets
|
||||
return sanitizeAlbumName(name);
|
||||
}
|
||||
|
||||
export function sanitizeAlbumName(name: string): string {
|
||||
// 1. Replace " / " or "/" with ", "
|
||||
let clean = name.replace(/\s*\/\s*/g, ', ');
|
||||
|
||||
// 1.5 Replace "&" with "and"
|
||||
clean = clean.replace(/&/g, 'and');
|
||||
|
||||
// 2. Remove accents/diacritics (e.g. "Espírito" -> "Espirito")
|
||||
// Note: Re-normalizing to NFC to correctly recompose Korean Hangul characters
|
||||
clean = clean.normalize("NFD").replace(/[\u0300-\u036f]/g, "").normalize("NFC");
|
||||
|
||||
// 3. Remove non-ascii characters but keep [], (), spaces, hyphens and plus signs.
|
||||
// Also allow Asian characters (Chinese, Japanese, Korean) along with CJK punctuation.
|
||||
const allowed = /[^a-zA-Z0-9 \-_.\[\](),+\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\u3000-\u303F\uFF00-\uFFEF]/gu;
|
||||
|
||||
clean = clean.replace(allowed, '');
|
||||
|
||||
// 4. Remove double spaces
|
||||
clean = clean.replace(/\s+/g, ' ');
|
||||
|
||||
return clean.trim();
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
|
||||
function verifyRedFixes() {
|
||||
console.log("--- Verifying RED Template Helper Functions ---");
|
||||
|
||||
// Test Split Logic which was duplicated
|
||||
const splitArtists = (artistString: string) => {
|
||||
return artistString.split(/,\s+|&|\s+\/\s+/).map((a: string) => a.trim()).filter((a: string) => a.length > 0);
|
||||
};
|
||||
|
||||
console.log("--- Artist Split Check ---");
|
||||
const testStr = "Artist A, Artist B & Artist C";
|
||||
const res = splitArtists(testStr);
|
||||
console.log(`Input: "${testStr}"`);
|
||||
console.log(`Output: ${JSON.stringify(res)}`);
|
||||
if (res.length === 3 && res[1] === "Artist B") {
|
||||
console.log("PASS: Artist split logic works.");
|
||||
} else {
|
||||
console.log("FAIL: Artist split logic failed.");
|
||||
}
|
||||
|
||||
console.log("\n--- RED Artist Logic Check ---");
|
||||
// Simulate the logic in route.ts
|
||||
const mainArtistName = "Artist A, Artist B";
|
||||
const allUniqueArtists = new Set(["Artist A", "Artist B", "Artist C"]);
|
||||
|
||||
const mainArtistComponents = splitArtists(mainArtistName);
|
||||
const mainArtists = mainArtistComponents.map(name => ({ name }));
|
||||
const withArtists: { name: string }[] = [];
|
||||
|
||||
allUniqueArtists.forEach(a => {
|
||||
if (!mainArtistComponents.includes(a) && a !== 'Various Artists') {
|
||||
withArtists.push({ name: a });
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Main Artist Input: "${mainArtistName}"`);
|
||||
console.log(`Main Artists Output: ${JSON.stringify(mainArtists)}`);
|
||||
console.log(`With Artists Output: ${JSON.stringify(withArtists)}`);
|
||||
|
||||
if (mainArtists.length === 2 && withArtists.length === 1 && withArtists[0].name === "Artist C") {
|
||||
console.log("PASS: RED Template artist logic correct.");
|
||||
} else {
|
||||
console.log("FAIL: RED Template artist logic incorrect.");
|
||||
}
|
||||
|
||||
console.log("\n--- RED Label Logic Check ---");
|
||||
let label = "4059882 Records DK";
|
||||
label = label.replace(/(\(c\)|\u00A9|\u2117)?\s*\b(19|20)\d{2}\b\s*/gi, '').trim();
|
||||
console.log(`Label Input: "4059882 Records DK"`);
|
||||
console.log(`Label Output: "${label}"`);
|
||||
|
||||
if (label === "4059882 Records DK") {
|
||||
console.log("PASS: Label preserved.");
|
||||
} else {
|
||||
console.log("FAIL: Label corrupted.");
|
||||
}
|
||||
}
|
||||
|
||||
verifyRedFixes();
|
||||
Reference in New Issue
Block a user