Compare commits

...

4 Commits
1.1 ... main

Author SHA1 Message Date
0e967d751c index.js aktualisiert 2025-05-21 14:58:55 +00:00
d9d355f1fb README.md aktualisiert 2025-05-21 14:56:55 +00:00
1760afdf39 README.md aktualisiert 2025-05-21 14:56:06 +00:00
e3d0ad5dfa index.js aktualisiert 2025-05-21 14:53:02 +00:00
2 changed files with 420 additions and 99 deletions

View File

@ -1,31 +1,33 @@
# Suffix Renamer
# File Renamer CLI
Ein einfaches, rekursives CLI-Tool zum automatisierten Umbenennen von Dateien mit einem Suffix.
![Version](https://img.shields.io/badge/version-1.2-blue.svg)
![Lizenz](https://img.shields.io/badge/license-MIT-green.svg)
![Node.js](https://img.shields.io/badge/Node.js-%3E%3D14.0.0-brightgreen.svg)
> Autor: M_Viper
Ein rekursives CLI-Tool zum Umbenennen von Dateien mit Suffix und automatischer TMDb-Titelerkennung. Ideal für die Organisation von Film- und Serien-Dateien.
---
## Features
## 🔍 Überblick
- **Rekursives Umbenennen**: Verarbeitet Dateien in Ordnern und Unterordnern.
- **TMDb-Integration**: Erkennt Film- und Serientitel automatisch über die TMDb-API.
- **Konfigurierbare Suffixe**: Fügt benutzerdefinierte Suffixe zu Dateinamen hinzu.
- **Flexible Modi**:
- Vorschau-Modus: Zeigt Änderungen ohne sie anzuwenden.
- Vorschau mit Bestätigung: Änderungen werden nach Bestätigung durchgeführt.
- Direkter Modus: Sofortiges Umbenennen ohne Vorschau.
- **Caching**: Speichert TMDb-Abfragen für schnellere Verarbeitung.
- **Konfigurationsmanagement**: Speichert Einstellungen in einer JSON-Datei.
- **Versionsprüfung**: Prüft automatisch auf Updates über Gitea.
Dieses Tool durchsucht einen Ordner (rekursiv), fragt bei der ersten Ausführung den gewünschten Ordnerpfad und ein Suffix ab, speichert diese Konfiguration, und hängt das Suffix an alle Dateinamen an (z.B. `film.mp4``film @Name.mp4`).
## Voraussetzungen
---
- **Node.js**: Version >= 14.0.0
- **TMDb API-Token**: Erforderlich für die Titelabfrage. [Hier anmelden](https://www.themoviedb.org/documentation/api).
- Optional: `dotenv` für die Verwaltung von Umgebungsvariablen.
## 🖥️ Funktionen
- ✅ Rekursives Durchsuchen von Ordnern
- ✅ Automatische oder benutzerdefinierte Pfadauswahl
- ✅ Benutzerdefiniertes Suffix
- ✅ Konfigurationsspeicherung in `Documents/config.json`
- ✅ Überspringt bereits umbenannte oder geschützte Dateien
---
## 🚀 Verwendung
### 1. Repository klonen
## Installation
1. **Repository klonen**:
```bash
git clone https://github.com/M-Viper/suffix-renamer.git
cd suffix-renamer
git clone https://git.viper.ipv64.net/M_Viper/file-renamer-cli.git
cd file-renamer-cli

447
index.js
View File

@ -1,20 +1,42 @@
/*
* Projekt: File Renamer CLI
* Beschreibung: Ein rekursives CLI-Tool zum Umbenennen von Dateien mit Suffix.
* Version: 1.2
* Beschreibung: Ein rekursives CLI-Tool zum Umbenennen von Dateien mit Suffix und TMDb-Titelerkennung.
* Autor: M_Viper
* Lizenz: MIT
* GitHub: https://git.viper.ipv64.net/M_Viper/file-renamer-cli
* 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,118 +59,295 @@ 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)) {
// ================== Konfiguration ==================
async function saveConfig(config) {
try {
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
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) {
const rl = readline.createInterface({
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);
// ================== 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);
}
if (!fs.existsSync(pfad)) {
console.log('Ordner existiert nicht, erstelle:', pfad);
fs.mkdirSync(pfad, { recursive: 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;
}
const eintraege = fs.readdirSync(pfad, { withFileTypes: true });
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
}
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('Kein Datei- oder Ordner-Eintrag, überspringe:', eintrag.name);
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...');
}
}
async function main() {
showBanner(); // Banner anzeigen
const config = await getConfig();
umbenennenSync(config.folderPath, config.suffix);
console.log('Fertig!');
return { shouldChangeConfig: false };
}
main();
// ================== 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);
}
})();