/* * 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); } })();