diff --git a/index.js b/index.js index 7d2928c..90f1cd3 100644 --- a/index.js +++ b/index.js @@ -1,20 +1,42 @@ /* -* Projekt: File Renamer CLI -* Beschreibung: Ein rekursives CLI-Tool zum Umbenennen von Dateien mit Suffix. -* Autor: M_Viper -* Lizenz: MIT -* GitHub: https://git.viper.ipv64.net/M_Viper/file-renamer-cli -* Webseite: https://m-viper.de -*/ + * 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'); +const fs = require('fs').promises; const path = require('path'); const os = require('os'); const readline = require('readline'); +const https = require('https'); -const configPath = path.join(os.homedir(), 'Documents', 'config.json'); +// Optional: dotenv für Umgebungsvariablen +let dotenv; +try { + dotenv = require('dotenv'); + dotenv.config(); +} catch { + console.warn('dotenv nicht gefunden, verwende Standardwerte oder direkte Eingabe.'); +} -// ================== Banner ================== +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); } @@ -27,8 +49,7 @@ function centerText(text, width) { } function showAsciiLogo() { - const width = process.stdout.columns || 80; // Terminalbreite, fallback 80 - + const width = process.stdout.columns || 80; const logoLines = [ '███████╗██╗██╗ ███████╗███╗ ██╗ █████╗ ███╗ ███╗███████╗ ██████╗██╗ ██╗', '██╔════╝██║██║ ██╔════╝████╗ ██║██╔══██╗████╗ ████║██╔════╝ ██╔════╝██║ ██║', @@ -38,57 +59,93 @@ function showAsciiLogo() { '╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚══════╝╚═╝', ]; - console.log('\x1b[32m'); // grün starten + console.log('\x1b[32m'); for (const line of logoLines) { console.log(centerText(line, width)); } - console.log('\x1b[0m'); // Farbe zurücksetzen + console.log('\x1b[0m'); console.log(''); } function showBanner() { console.clear(); showAsciiLogo(); - - const width = process.stdout.columns || 60; // Breite des Terminals, fallback 60 - + const width = process.stdout.columns || 60; const bannerLines = [ - 'Version 1.0', + 'Version 1.2', 'Script by', '@M_Viper', '__________________________', '', - 'Git: https://git.viper.ipv64.net/M_Viper/file-renamer-cli', + 'Gitea: https://git.viper.ipv64.net/M_Viper/file-renamer-cli', ]; - // Obere Rahmenlinie greenMessage('╔' + '═'.repeat(width - 2) + '╗'); - - // Bannerzeilen mit Rahmen und Zentrierung for (const line of bannerLines) { - const centered = centerText(line, width - 4); // 4 wegen Rahmen links und rechts + Leerzeichen + const centered = centerText(line, width - 4); greenMessage('║ ' + centered + ' ║'); } - - // Untere Rahmenlinie greenMessage('╚' + '═'.repeat(width - 2) + '╝'); greenMessage(''); } -// ============================================ -function saveConfig(config) { - fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); +// ================== 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(); + }); } -function loadConfig() { - if (fs.existsSync(configPath)) { - try { - return JSON.parse(fs.readFileSync(configPath, 'utf-8')); - } catch { - return null; - } +// ================== 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; } - return null; } function askQuestion(query) { @@ -96,61 +153,202 @@ function askQuestion(query) { input: process.stdin, output: process.stdout, }); - return new Promise(resolve => rl.question(query, ans => { + return new Promise((resolve) => rl.question(query, (ans) => { rl.close(); resolve(ans.trim()); })); } async function getConfig() { - const existingConfig = loadConfig(); - if (existingConfig?.folderPath && existingConfig?.suffix) { - console.log(`Verwende gespeicherte Konfiguration:`); - console.log(`Ordner: ${existingConfig.folderPath}`); - console.log(`Suffix: ${existingConfig.suffix}`); - return existingConfig; + // 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 getConfig(); + return setupConfig(); } - const suffix = await askQuestion('Welcher Text soll am Ende der Dateinamen hinzugefügt werden? (z. B. @Name)\n'); + // 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(); + } - const config = { folderPath, suffix }; - saveConfig(config); + // 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; } -function umbenennenSync(pfad, suffix) { - console.log('Prüfe Ordner:', pfad); - - if (!fs.existsSync(pfad)) { - console.log('Ordner existiert nicht, erstelle:', pfad); - fs.mkdirSync(pfad, { recursive: true }); - return; +// ================== 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); } - const eintraege = fs.readdirSync(pfad, { withFileTypes: true }); + 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); @@ -164,7 +362,10 @@ function umbenennenSync(pfad, suffix) { continue; } console.log('Betrete Unterordner:', vollerPfad); - umbenennenSync(vollerPfad, suffix); + 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); @@ -174,30 +375,148 @@ function umbenennenSync(pfad, suffix) { continue; } - const neuerName = `${name} ${suffix}${ext}`; - const neuerPfad = path.join(pfad, neuerName); + 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 { - fs.renameSync(vollerPfad, neuerPfad); - console.log(`Umbenannt: ${eintrag.name} → ${neuerName}`); - } catch (e) { - console.error(`Fehler bei ${eintrag.name}:`, e.message); - if (e.code === 'EPERM' || e.code === 'EACCES') { - console.error('Keine Zugriffsrechte oder Datei geschützt. Übersprungen.'); - } + await fs.access(neuerPfad); + console.log(`Datei existiert bereits: ${neuerDateiname}, überspringe`); + continue; + } catch { + // Datei existiert nicht, kann umbenannt werden } - } else { - console.log('Kein Datei- oder Ordner-Eintrag, überspringe:', eintrag.name); + + 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 }; } -async function main() { - showBanner(); // Banner anzeigen +// ================== Hauptprogramm ================== +(async () => { + showBanner(); - const config = await getConfig(); - umbenennenSync(config.folderPath, config.suffix); - console.log('Fertig!'); -} + if (!TMDB_API_BEARER_TOKEN || TMDB_API_BEARER_TOKEN === 'eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI4ZjlmNjk4MTk4ODE1ODFlZWUxNWI0NzU2NzkwYzU4ZCIsIm5iZiI6MTYwMTk3NzY4Ni45NTM5OTk4LCJzdWIiOiI1ZjdjM2Q1NjBlNTk3YjAwMzdiMjYxYzAiLCJzY29wZXMiOlsiYXBpX3JlYWQiXSwidmVyc2lvbiI6MX0.YxNqLdfqvV4bWOkUZIU1ObWrICh_8QILwRnf_I-2x5w') { + 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); + } -main(); + 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); + } +})(); \ No newline at end of file