Restart repository history from target event

This commit is contained in:
2026-07-18 16:54:26 +01:00
commit e08aa0a667
71 changed files with 15963 additions and 0 deletions
+117
View File
@@ -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;
}
+37
View File
@@ -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);
});
}
+55
View File
@@ -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;
}
+29
View File
@@ -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();
}
+59
View File
@@ -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();