index.js aktualisiert

This commit is contained in:
M_Viper 2025-05-21 14:53:02 +00:00
parent 160b56634c
commit e3d0ad5dfa

471
index.js
View File

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