522 lines
18 KiB
JavaScript
522 lines
18 KiB
JavaScript
/*
|
|
* Projekt: File Renamer CLI
|
|
* Version: 1.2
|
|
* Beschreibung: Ein rekursives CLI-Tool zum Umbenennen von Dateien mit Suffix und TMDb-Titelerkennung.
|
|
* Autor: M_Viper
|
|
* Lizenz: MIT
|
|
* Gitea: https://git.viper.ipv64.net/M_Viper/file-renamer-cli
|
|
* Webseite: https://m-viper.de
|
|
*/
|
|
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
const os = require('os');
|
|
const readline = require('readline');
|
|
const https = require('https');
|
|
|
|
// Optional: dotenv für Umgebungsvariablen
|
|
let dotenv;
|
|
try {
|
|
dotenv = require('dotenv');
|
|
dotenv.config();
|
|
} catch {
|
|
console.warn('dotenv nicht gefunden, verwende Standardwerte oder direkte Eingabe.');
|
|
}
|
|
|
|
const localVersion = '1.2';
|
|
|
|
// TMDb API Bearer Token (aus .env oder direkt hier einfügen)
|
|
const TMDB_API_BEARER_TOKEN = process.env.TMDB_API_BEARER_TOKEN || 'YOUR_TOKEN_HERE';
|
|
|
|
// Konfigurationspfad relativ zur EXE oder im Benutzerverzeichnis
|
|
const configPath = process.env.NODE_ENV === 'production'
|
|
? path.join(path.dirname(process.execPath), 'config.json')
|
|
: path.join(os.homedir(), 'Documents', 'config.json');
|
|
|
|
const tmdbCache = new Map(); // Cache für TMDb-Abfragen
|
|
let processedFiles = 0;
|
|
|
|
// ================== Hilfsfunktionen ==================
|
|
function greenMessage(text) {
|
|
console.log('\x1b[32m%s\x1b[0m', text);
|
|
}
|
|
|
|
function centerText(text, width) {
|
|
const len = text.length;
|
|
if (len >= width) return text;
|
|
const leftPadding = Math.floor((width - len) / 2);
|
|
return ' '.repeat(leftPadding) + text;
|
|
}
|
|
|
|
function showAsciiLogo() {
|
|
const width = process.stdout.columns || 80;
|
|
const logoLines = [
|
|
'███████╗██╗██╗ ███████╗███╗ ██╗ █████╗ ███╗ ███╗███████╗ ██████╗██╗ ██╗',
|
|
'██╔════╝██║██║ ██╔════╝████╗ ██║██╔══██╗████╗ ████║██╔════╝ ██╔════╝██║ ██║',
|
|
'█████╗ ██║██║ █████╗ ██╔██╗ ██║███████║██╔████╔██║█████╗ ██║ ██║ ██║',
|
|
'██╔══╝ ██║██║ ██╔══╝ ██║╚██╗██║██╔══██║██║╚██╔╝██║██╔══╝ ██║ ██║ ██║',
|
|
'██║ ██║███████╗███████╗██║ ╚████║██║ ██║██║ ╚═╝ ██║███████╗ ╚██████╗███████╗██║',
|
|
'╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚══════╝╚═╝',
|
|
];
|
|
|
|
console.log('\x1b[32m');
|
|
for (const line of logoLines) {
|
|
console.log(centerText(line, width));
|
|
}
|
|
console.log('\x1b[0m');
|
|
console.log('');
|
|
}
|
|
|
|
function showBanner() {
|
|
console.clear();
|
|
showAsciiLogo();
|
|
const width = process.stdout.columns || 60;
|
|
const bannerLines = [
|
|
'Version 1.2',
|
|
'Script by',
|
|
'@M_Viper',
|
|
'__________________________',
|
|
'',
|
|
'Gitea: https://git.viper.ipv64.net/M_Viper/file-renamer-cli',
|
|
];
|
|
|
|
greenMessage('╔' + '═'.repeat(width - 2) + '╗');
|
|
for (const line of bannerLines) {
|
|
const centered = centerText(line, width - 4);
|
|
greenMessage('║ ' + centered + ' ║');
|
|
}
|
|
greenMessage('╚' + '═'.repeat(width - 2) + '╝');
|
|
greenMessage('');
|
|
}
|
|
|
|
// ================== Versionsprüfung (Gitea) ==================
|
|
async function checkForUpdates() {
|
|
return new Promise((resolve) => {
|
|
const options = {
|
|
hostname: 'git.viper.ipv64.net',
|
|
path: '/api/v1/repos/M_Viper/file-renamer-cli/releases',
|
|
method: 'GET',
|
|
headers: {
|
|
'User-Agent': 'File-Renamer-CLI',
|
|
'Accept': 'application/json',
|
|
},
|
|
};
|
|
|
|
const req = https.request(options, (res) => {
|
|
let data = '';
|
|
res.on('data', (chunk) => (data += chunk));
|
|
res.on('end', () => {
|
|
try {
|
|
const releases = JSON.parse(data);
|
|
if (!Array.isArray(releases) || releases.length === 0) {
|
|
return resolve(null);
|
|
}
|
|
const latestVersion = releases[0].tag_name.replace(/^v/i, '');
|
|
const cleanLocal = localVersion.replace(/^v/i, '');
|
|
if (latestVersion > cleanLocal) {
|
|
resolve(latestVersion);
|
|
} else {
|
|
resolve(null);
|
|
}
|
|
} catch {
|
|
resolve(null);
|
|
}
|
|
});
|
|
});
|
|
|
|
req.on('error', () => resolve(null));
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
// ================== Konfiguration ==================
|
|
async function saveConfig(config) {
|
|
try {
|
|
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
console.log(`Konfiguration gespeichert unter: ${configPath}`);
|
|
} catch (err) {
|
|
console.error('Fehler beim Speichern der Konfiguration:', err.message);
|
|
}
|
|
}
|
|
|
|
async function loadConfig() {
|
|
try {
|
|
const data = await fs.readFile(configPath, 'utf-8');
|
|
return JSON.parse(data);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function askQuestion(query) {
|
|
const rl = readline.createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout,
|
|
});
|
|
return new Promise((resolve) => rl.question(query, (ans) => {
|
|
rl.close();
|
|
resolve(ans.trim());
|
|
}));
|
|
}
|
|
|
|
async function getConfig() {
|
|
// Prüfe auf --setup Kommandozeilenoption
|
|
const forceSetup = process.argv.includes('--setup');
|
|
if (forceSetup) {
|
|
console.log('Erzwinge Ersteinrichtung (--setup angegeben)...');
|
|
return await setupConfig();
|
|
}
|
|
|
|
const existingConfig = await loadConfig();
|
|
|
|
// Prüfe, ob die Konfiguration vollständig und gültig ist
|
|
if (
|
|
existingConfig?.folderPath &&
|
|
existingConfig?.suffix &&
|
|
['preview', 'preview-confirm', 'direct'].includes(existingConfig?.renameMode)
|
|
) {
|
|
try {
|
|
await fs.access(existingConfig.folderPath); // Prüfe, ob der Ordner existiert
|
|
if (/[<>"|?*]/.test(existingConfig.suffix)) {
|
|
console.log('Ungültiger Suffix in Konfiguration gefunden. Starte Ersteinrichtung...');
|
|
return await setupConfig();
|
|
}
|
|
return existingConfig;
|
|
} catch {
|
|
console.log('Konfigurationsordner existiert nicht mehr. Starte Ersteinrichtung...');
|
|
return await setupConfig();
|
|
}
|
|
}
|
|
|
|
// Ersteinrichtung, wenn keine oder ungültige Konfiguration
|
|
return await setupConfig();
|
|
}
|
|
|
|
async function setupConfig() {
|
|
console.log('\n--- Ersteinrichtung ---');
|
|
|
|
// Ordnerauswahl
|
|
console.log('Möchtest du den automatischen Ordner nutzen?');
|
|
console.log('1 = Ja (Desktop\\Filme)');
|
|
console.log('2 = Benutzerdefinierter Ordner');
|
|
const antwort = await askQuestion('Deine Wahl (1 oder 2): ');
|
|
|
|
let folderPath;
|
|
if (antwort === '1') {
|
|
folderPath = path.join(os.homedir(), 'Desktop', 'Filme');
|
|
console.log(`Automatischer Ordner gewählt: ${folderPath}`);
|
|
} else if (antwort === '2') {
|
|
folderPath = await askQuestion('Bitte gib den vollständigen Pfad zum Ordner ein:\n');
|
|
try {
|
|
await fs.access(folderPath);
|
|
} catch {
|
|
console.log('Ordner existiert nicht, bitte überprüfe den Pfad.');
|
|
return setupConfig();
|
|
}
|
|
} else {
|
|
console.log('Ungültige Eingabe, bitte versuche es nochmal.');
|
|
return setupConfig();
|
|
}
|
|
|
|
// Suffix
|
|
const suffix = await askQuestion(
|
|
'Welcher Text soll am Ende der Dateinamen hinzugefügt werden? (z. B. @Name)\n'
|
|
);
|
|
if (!suffix || /[<>"|?*]/.test(suffix)) {
|
|
console.log('Ungültiger Suffix. Vermeide Sonderzeichen wie <, >, ?, * usw.');
|
|
return setupConfig();
|
|
}
|
|
|
|
// Umbenennungsmodus
|
|
console.log('Wie soll die Umbenennung durchgeführt werden?');
|
|
console.log('1 = Nur Vorschau der Änderungen');
|
|
console.log('2 = Vorschau mit Bestätigung');
|
|
console.log('3 = Direkte Umbenennung ohne Vorschau');
|
|
const modeChoice = await askQuestion('Deine Wahl (1, 2 oder 3): ');
|
|
let renameMode;
|
|
if (modeChoice === '1') {
|
|
renameMode = 'preview';
|
|
console.log('[DEBUG] Modus: Nur Vorschau');
|
|
} else if (modeChoice === '2') {
|
|
renameMode = 'preview-confirm';
|
|
console.log('[DEBUG] Modus: Vorschau mit Bestätigung');
|
|
} else if (modeChoice === '3') {
|
|
renameMode = 'direct';
|
|
console.log('[DEBUG] Modus: Direkte Umbenennung');
|
|
} else {
|
|
console.log('Ungültige Eingabe. Bitte gib "1", "2" oder "3" ein.');
|
|
return setupConfig();
|
|
}
|
|
|
|
const config = { folderPath, suffix, renameMode };
|
|
await saveConfig(config);
|
|
return config;
|
|
}
|
|
|
|
// ================== TMDb API ==================
|
|
async function fetchMovieDataFromTMDb(title, year) {
|
|
if (!title) return null;
|
|
const cacheKey = `${title}|${year || ''}`;
|
|
if (tmdbCache.has(cacheKey)) {
|
|
console.log(`Verwende gecachte TMDb-Daten für: ${title} (${year || 'ohne Jahr'})`);
|
|
return tmdbCache.get(cacheKey);
|
|
}
|
|
|
|
try {
|
|
const query = encodeURIComponent(title);
|
|
let url = `https://api.themoviedb.org/3/search/movie?query=${query}&include_adult=false`;
|
|
if (year) url += `&year=${year}`;
|
|
|
|
const options = {
|
|
headers: {
|
|
Authorization: `Bearer ${TMDB_API_BEARER_TOKEN}`,
|
|
'Content-Type': 'application/json;charset=utf-8',
|
|
},
|
|
};
|
|
|
|
return new Promise((resolve, reject) => {
|
|
https
|
|
.get(url, options, (res) => {
|
|
if (res.statusCode === 429) {
|
|
console.warn('TMDb Rate-Limit erreicht, warte kurz...');
|
|
setTimeout(() => fetchMovieDataFromTMDb(title, year).then(resolve).catch(reject), 1000);
|
|
return;
|
|
}
|
|
|
|
let data = '';
|
|
res.on('data', (chunk) => (data += chunk));
|
|
res.on('end', () => {
|
|
try {
|
|
const json = JSON.parse(data);
|
|
const result = json.results && json.results.length > 0 ? json.results[0] : null;
|
|
tmdbCache.set(cacheKey, result);
|
|
resolve(result);
|
|
} catch (err) {
|
|
console.error(`Fehler beim Parsen der TMDb-Antwort für ${title}:`, err.message);
|
|
resolve(null);
|
|
}
|
|
});
|
|
})
|
|
.on('error', (err) => {
|
|
console.error(`Fehler bei TMDb-Abfrage für ${title}:`, err.message);
|
|
resolve(null); // Fallback: Keine Daten
|
|
});
|
|
});
|
|
} catch (err) {
|
|
console.error(`Unerwarteter Fehler bei TMDb-Abfrage für ${title}:`, err.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// ================== Umbenennung ==================
|
|
async function umbenennenSync(pfad, suffix, mode = 'preview') {
|
|
console.log('Prüfe Ordner:', pfad);
|
|
console.log(`[DEBUG] Umbenennungsmodus: ${mode}`);
|
|
|
|
try {
|
|
await fs.access(pfad);
|
|
} catch (err) {
|
|
console.log('Ordner existiert nicht, erstelle:', pfad);
|
|
try {
|
|
await fs.mkdir(pfad, { recursive: true });
|
|
} catch (mkdirErr) {
|
|
console.error('Fehler beim Erstellen des Ordners:', mkdirErr.message);
|
|
}
|
|
return { shouldChangeConfig: false };
|
|
}
|
|
|
|
let eintraege;
|
|
try {
|
|
eintraege = await fs.readdir(pfad, { withFileTypes: true });
|
|
} catch (err) {
|
|
console.error('Fehler beim Lesen des Ordners:', err.message);
|
|
return { shouldChangeConfig: false };
|
|
}
|
|
|
|
if (eintraege.length === 0) {
|
|
console.log('Ordner ist leer:', pfad);
|
|
if (mode === 'preview') {
|
|
const changeConfig = await askQuestion('Möchten Sie die Konfiguration ändern? (ja/nein): ');
|
|
if (changeConfig.toLowerCase() === 'ja') {
|
|
return { shouldChangeConfig: true };
|
|
}
|
|
await askQuestion('Drücken Sie Enter zum Beenden...');
|
|
}
|
|
return { shouldChangeConfig: false };
|
|
}
|
|
|
|
const jahrRegex = /(\d{4})/;
|
|
const serieRegex = /(S\d{1,2}E\d{1,2})/i;
|
|
const changes = [];
|
|
|
|
for (const eintrag of eintraege) {
|
|
const vollerPfad = path.join(pfad, eintrag.name);
|
|
|
|
if (eintrag.isDirectory()) {
|
|
if (
|
|
eintrag.name === 'System Volume Information' ||
|
|
eintrag.name.startsWith('$') ||
|
|
eintrag.name.startsWith('.')
|
|
) {
|
|
console.log('Überspringe geschützten/verborgenen Ordner:', vollerPfad);
|
|
continue;
|
|
}
|
|
console.log('Betrete Unterordner:', vollerPfad);
|
|
const result = await umbenennenSync(vollerPfad, suffix, mode);
|
|
if (result.shouldChangeConfig) {
|
|
return { shouldChangeConfig: true };
|
|
}
|
|
} else if (eintrag.isFile()) {
|
|
const ext = path.extname(eintrag.name);
|
|
const name = path.basename(eintrag.name, ext);
|
|
|
|
if (name.endsWith(` ${suffix}`)) {
|
|
console.log('Datei schon umbenannt, überspringe:', eintrag.name);
|
|
continue;
|
|
}
|
|
|
|
const serieMatch = name.match(serieRegex);
|
|
let neuerNameTeil;
|
|
|
|
if (serieMatch) {
|
|
const serienTeil = serieMatch[0];
|
|
const titelVorSerie = name.substring(0, serieMatch.index).trim().replace(/[\.\-_]+/g, ' ');
|
|
neuerNameTeil = `${titelVorSerie} ${serienTeil} ${suffix}`.trim();
|
|
} else {
|
|
const jahrMatch = name.match(jahrRegex);
|
|
const jahr = jahrMatch ? jahrMatch[1] : null;
|
|
const titelRaw = name.replace(jahrRegex, '').replace(/[\.\-_]+/g, ' ').trim();
|
|
|
|
const filmDaten = await fetchMovieDataFromTMDb(titelRaw, jahr);
|
|
let filmTitel = titelRaw;
|
|
let filmJahr = jahr;
|
|
|
|
if (filmDaten) {
|
|
filmTitel = filmDaten.title || filmTitel;
|
|
filmJahr = filmDaten.release_date ? filmDaten.release_date.substring(0, 4) : filmJahr;
|
|
}
|
|
|
|
neuerNameTeil = filmJahr ? `${filmTitel} (${filmJahr}) ${suffix}` : `${filmTitel} ${suffix}`;
|
|
}
|
|
|
|
const neuerDateiname = neuerNameTeil + ext;
|
|
const neuerPfad = path.join(pfad, neuerDateiname);
|
|
|
|
if (neuerDateiname === eintrag.name) {
|
|
console.log('Dateiname ist bereits korrekt:', neuerDateiname);
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
await fs.access(neuerPfad);
|
|
console.log(`Datei existiert bereits: ${neuerDateiname}, überspringe`);
|
|
continue;
|
|
} catch {
|
|
// Datei existiert nicht, kann umbenannt werden
|
|
}
|
|
|
|
changes.push({ alt: eintrag.name, neu: neuerDateiname, vollerPfad, neuerPfad });
|
|
processedFiles++;
|
|
}
|
|
}
|
|
|
|
if (changes.length > 0) {
|
|
if (mode === 'preview' || mode === 'preview-confirm') {
|
|
console.log(`\nVorschau der Änderungen (${changes.length}):`);
|
|
changes.forEach(({ alt, neu }) => {
|
|
console.log(`${alt} → ${neu}`);
|
|
});
|
|
}
|
|
|
|
if (mode === 'preview') {
|
|
console.log('\nNur Vorschau: Keine Änderungen wurden vorgenommen.');
|
|
const changeConfig = await askQuestion('Möchten Sie die Konfiguration ändern? (ja/nein): ');
|
|
if (changeConfig.toLowerCase() === 'ja') {
|
|
return { shouldChangeConfig: true };
|
|
}
|
|
await askQuestion('Drücken Sie Enter zum Beenden...');
|
|
return { shouldChangeConfig: false };
|
|
}
|
|
|
|
if (mode === 'preview-confirm') {
|
|
console.log('\nAusführungs-Modus: Änderungen werden nach Bestätigung durchgeführt.');
|
|
const confirm = await askQuestion('Sollen diese Änderungen durchgeführt werden? (ja/nein): ');
|
|
if (confirm.toLowerCase() !== 'ja') {
|
|
console.log('Umbenennung abgebrochen.');
|
|
return { shouldChangeConfig: false };
|
|
}
|
|
}
|
|
|
|
// Für 'preview-confirm' (nach Bestätigung) oder 'direct'
|
|
if (mode === 'preview-confirm' || mode === 'direct') {
|
|
if (mode === 'direct') {
|
|
console.log('\nDirekter Umbenennungs-Modus: Änderungen werden sofort durchgeführt.');
|
|
}
|
|
for (const { vollerPfad, neuerPfad } of changes) {
|
|
try {
|
|
await fs.rename(vollerPfad, neuerPfad);
|
|
console.log(`Umbenannt: ${path.basename(vollerPfad)} → ${path.basename(neuerPfad)}`);
|
|
} catch (err) {
|
|
console.error(`Fehler beim Umbenennen von ${path.basename(vollerPfad)}:`, err.message);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
console.log(`\n${mode === 'preview' ? 'Vorschau-Modus' : mode === 'preview-confirm' ? 'Ausführungs-Modus' : 'Direkter Modus'}: Keine Änderungen erforderlich.`);
|
|
if (mode === 'preview') {
|
|
const changeConfig = await askQuestion('Möchten Sie die Konfiguration ändern? (ja/nein): ');
|
|
if (changeConfig.toLowerCase() === 'ja') {
|
|
return { shouldChangeConfig: true };
|
|
}
|
|
await askQuestion('Drücken Sie Enter zum Beenden...');
|
|
}
|
|
}
|
|
|
|
return { shouldChangeConfig: false };
|
|
}
|
|
|
|
// ================== Hauptprogramm ==================
|
|
(async () => {
|
|
showBanner();
|
|
|
|
if (!TMDB_API_BEARER_TOKEN || TMDB_API_BEARER_TOKEN === 'API-TOKEN') {
|
|
console.error('Fehler: TMDb API-Token fehlt. Bitte in .env-Datei konfigurieren.');
|
|
console.error('Hinweis: Verwende niemals einen echten Token direkt im Code!');
|
|
console.error('Erstelle eine .env-Datei mit: TMDB_API_BEARER_TOKEN=dein_tmdb_token');
|
|
process.exit(1);
|
|
}
|
|
|
|
try {
|
|
const updateVerfuegbar = await checkForUpdates();
|
|
if (updateVerfuegbar) {
|
|
console.log(`Neue Version verfügbar: ${updateVerfuegbar} (Du hast: ${localVersion})`);
|
|
console.log('Bitte aktualisiere das Tool über Gitea.\n');
|
|
} else {
|
|
console.log('Du hast die aktuellste Version.\n');
|
|
}
|
|
|
|
let config = await getConfig();
|
|
let shouldContinue = true;
|
|
|
|
while (shouldContinue) {
|
|
console.log(`Starte Umbenennung im Ordner: ${config.folderPath}`);
|
|
console.log(`Suffix: ${config.suffix}`);
|
|
console.log(`Umbenennungsmodus: ${config.renameMode}\n`);
|
|
|
|
processedFiles = 0; // Zurücksetzen für neue Umbenennung
|
|
const result = await umbenennenSync(config.folderPath, config.suffix, config.renameMode);
|
|
|
|
if (result.shouldChangeConfig) {
|
|
console.log('\nStarte Konfigurationsänderung...');
|
|
config = await setupConfig();
|
|
} else {
|
|
shouldContinue = false;
|
|
}
|
|
}
|
|
|
|
console.log(`\nFertig! Verarbeitete Dateien: ${processedFiles}`);
|
|
} catch (err) {
|
|
console.error('Unerwarteter Fehler im Hauptprogramm:', err.message);
|
|
process.exit(1);
|
|
}
|
|
})(); |