0.16.1-1 release snapshot
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function ensureDir(dirPath) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
function transliterateForFilename(input) {
|
||||
return String(input || '')
|
||||
// German — must come before NFD to get ae/oe/ue instead of just a/o/u
|
||||
.replace(/ä/g, 'ae').replace(/Ä/g, 'Ae')
|
||||
.replace(/ö/g, 'oe').replace(/Ö/g, 'Oe')
|
||||
.replace(/ü/g, 'ue').replace(/Ü/g, 'Ue')
|
||||
.replace(/ß/g, 'ss')
|
||||
// Danish / Norwegian / Swedish
|
||||
.replace(/å/g, 'a').replace(/Å/g, 'A')
|
||||
.replace(/æ/g, 'ae').replace(/Æ/g, 'Ae')
|
||||
.replace(/ø/g, 'o').replace(/Ø/g, 'O')
|
||||
// Icelandic
|
||||
.replace(/ð/g, 'd').replace(/Ð/g, 'D')
|
||||
.replace(/þ/g, 'th').replace(/Þ/g, 'Th')
|
||||
// NFD decomposition + strip combining diacritical marks (handles é→e, ñ→n, ç→c, etc.)
|
||||
.normalize('NFD')
|
||||
.replace(/\p{M}+/gu, '')
|
||||
// Drop any remaining non-ASCII characters
|
||||
.replace(/[^\x00-\x7F]/g, '');
|
||||
}
|
||||
|
||||
function sanitizeFileName(input) {
|
||||
return transliterateForFilename(String(input || 'untitled'))
|
||||
.replace(/[^a-zA-Z0-9 ()[\]_+-]/g, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.slice(0, 180);
|
||||
}
|
||||
|
||||
function sanitizeFileNameWithExtension(input) {
|
||||
const str = String(input || 'untitled');
|
||||
const ext = path.extname(str);
|
||||
const base = ext ? str.slice(0, -ext.length) : str;
|
||||
const cleanBase = sanitizeFileName(base);
|
||||
const cleanExt = ext.toLowerCase().replace(/[^a-z0-9.]/g, '');
|
||||
return (cleanBase || 'untitled') + cleanExt;
|
||||
}
|
||||
|
||||
function renderTemplate(template, values) {
|
||||
const rawSource = String(template || '${title} (${year})');
|
||||
const parseToken = (rawToken) => {
|
||||
const token = String(rawToken || '').trim();
|
||||
if (!token) {
|
||||
return { format: '', key: '' };
|
||||
}
|
||||
const separatorIndex = token.indexOf(':');
|
||||
if (separatorIndex <= 0) {
|
||||
return { format: '', key: token };
|
||||
}
|
||||
return {
|
||||
format: token.slice(0, separatorIndex).trim(),
|
||||
key: token.slice(separatorIndex + 1).trim()
|
||||
};
|
||||
};
|
||||
const applyFormat = (value, format) => {
|
||||
const normalizedFormat = String(format || '').trim().toLowerCase();
|
||||
if (!normalizedFormat) {
|
||||
return String(value);
|
||||
}
|
||||
// {0:key} => number with leading zero (width 2), e.g. 2 -> 02
|
||||
if (normalizedFormat === '0') {
|
||||
const raw = String(value);
|
||||
const match = raw.match(/^(-?)(\d+)$/);
|
||||
if (match) {
|
||||
const sign = match[1] || '';
|
||||
const digits = String(match[2] || '');
|
||||
return `${sign}${digits.padStart(2, '0')}`;
|
||||
}
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
const resolveToken = (rawKey) => {
|
||||
const { format, key } = parseToken(rawKey);
|
||||
const val = values[key];
|
||||
if (val === undefined || val === null || val === '') {
|
||||
return 'unknown';
|
||||
}
|
||||
return applyFormat(val, format);
|
||||
};
|
||||
|
||||
const source = (
|
||||
/[{}]/.test(rawSource)
|
||||
? rawSource
|
||||
: rawSource.replace(/\b(trackNr|trackNumber|artist|album|title|year)\b/g, '{$1}')
|
||||
);
|
||||
|
||||
// Support both ${key} and legacy {key} placeholders (+ recovered bare tokens).
|
||||
return source
|
||||
.replace(/\$\{([^}]+)\}/g, (_m, key) => resolveToken(key))
|
||||
.replace(/\{([^{}$]+)\}/g, (_m, key) => resolveToken(key));
|
||||
}
|
||||
|
||||
function findLargestMediaFile(dirPath, extensions = ['.mkv', '.mp4']) {
|
||||
const files = findMediaFiles(dirPath, extensions);
|
||||
if (files.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return files.reduce((largest, file) => (largest === null || file.size > largest.size ? file : largest), null);
|
||||
}
|
||||
|
||||
function findMediaFiles(dirPath, extensions = ['.mkv', '.mp4']) {
|
||||
const results = [];
|
||||
|
||||
function walk(current) {
|
||||
const entries = fs.readdirSync(current, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const abs = path.join(current, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walk(abs);
|
||||
} else {
|
||||
const ext = path.extname(entry.name).toLowerCase();
|
||||
if (!extensions.includes(ext)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const stat = fs.statSync(abs);
|
||||
results.push({
|
||||
path: abs,
|
||||
size: stat.size
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(dirPath);
|
||||
results.sort((a, b) => b.size - a.size || a.path.localeCompare(b.path));
|
||||
return results;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ensureDir,
|
||||
transliterateForFilename,
|
||||
sanitizeFileName,
|
||||
sanitizeFileNameWithExtension,
|
||||
renderTemplate,
|
||||
findLargestMediaFile,
|
||||
findMediaFiles
|
||||
};
|
||||
Reference in New Issue
Block a user