#!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.11" # dependencies = [ # "click", # ] # /// import re import os import shutil from pathlib import Path from collections import defaultdict import click def extract_game_title(filename: str) -> str | None: """ Extract the game title from a filename like: 'Splatoon 2 - 001 - Opening - Wet Floor.mp3' Returns: 'Splatoon 2' Pattern: Everything before ' - [digits only] - ' is the title """ # Match pattern: ' - [digits] - ' match = re.match(r'^(.+?)\s+-\s+\d+\s+-\s+.+\.mp3$', filename) if match: return match.group(1).strip() return None def sanitize_folder_name(name: str) -> str: """ Sanitize a folder name by removing problematic characters. - Removes trailing dots (e.g., 'Donkey Kong Jr.' -> 'Donkey Kong Jr') - Removes trailing spaces """ return name.rstrip('. ') def scan_and_group_mp3s(directory: Path) -> dict[str, list[str]]: """ Scan directory for MP3 files and group them by extracted game title. Returns: dict mapping game title -> list of filenames """ groups = defaultdict(list) for file in directory.glob('*.mp3'): filename = file.name title = extract_game_title(filename) if title: groups[title].append(filename) else: # If we can't extract a title, create a special group groups['[Unknown Format]'].append(filename) return dict(groups) def display_group_info(title: str, files: list[str]) -> None: """Display information about a group of files.""" click.echo(f"\n{'='*70}") click.secho(f"Vorgeschlagener Ordner: {title}", fg='cyan', bold=True) click.secho(f"Anzahl Dateien: {len(files)}", fg='yellow') click.echo(f"{'='*70}") # Show up to 3 example files examples = files[:3] click.echo("\nBeispiel-Dateien:") for i, filename in enumerate(examples, 1): click.echo(f" {i}. {filename}") if len(files) > 3: click.secho(f" ... und {len(files) - 3} weitere", fg='bright_black') def move_files_to_folder(files: list[str], folder_name: str, source_dir: Path) -> None: """Move files to the specified folder.""" # Sanitize folder name (remove trailing dots, etc.) folder_name = sanitize_folder_name(folder_name) target_dir = source_dir / folder_name # Create folder if it doesn't exist target_dir.mkdir(exist_ok=True) moved_count = 0 error_count = 0 for filename in files: source_file = source_dir / filename target_file = target_dir / filename try: if target_file.exists(): click.secho(f" ⚠ Übersprungen (existiert bereits): {filename}", fg='yellow') else: shutil.move(str(source_file), str(target_file)) moved_count += 1 except Exception as e: click.secho(f" ✗ Fehler beim Verschieben von {filename}: {e}", fg='red') error_count += 1 if moved_count > 0: click.secho(f" ✓ {moved_count} Datei(en) verschoben nach '{folder_name}'", fg='green') if error_count > 0: click.secho(f" ✗ {error_count} Fehler beim Verschieben", fg='red') @click.command() @click.option('--auto', is_flag=True, help='Automatisch alle Dateien verschieben ohne Bestätigung') def main(auto: bool): """ Organisiert MP3-Dateien in Ordner basierend auf dem Spieltitel. Scannt das aktuelle Verzeichnis nach MP3-Dateien, extrahiert den Spieltitel aus dem Dateinamen und verschiebt die Dateien in entsprechende Ordner. """ current_dir = Path.cwd() click.secho("\n🎵 MP3-Datei-Organizer für Nintendo Music\n", fg='green', bold=True) click.echo(f"Verzeichnis: {current_dir}\n") # Scan and group files click.echo("Scanne MP3-Dateien...") groups = scan_and_group_mp3s(current_dir) if not groups: click.secho("Keine MP3-Dateien gefunden!", fg='red') return # Sort groups by title for consistent ordering sorted_groups = sorted(groups.items(), key=lambda x: x[0]) click.secho(f"\n✓ {len(sorted_groups)} Gruppen gefunden", fg='green') click.secho(f"✓ {sum(len(files) for files in groups.values())} Dateien insgesamt", fg='green') # Process each group processed_count = 0 skipped_count = 0 for title, files in sorted_groups: display_group_info(title, files) if auto: # In auto mode, just move everything move_files_to_folder(files, title, current_dir) processed_count += 1 else: # Interactive mode click.echo() choice = click.prompt( "Aktion? [Y]a / [E]dit / [S]kip", type=str, default='Y', show_default=True ).strip().upper() if choice == 'Y' or choice == 'YA' or choice == 'J' or choice == 'JA': move_files_to_folder(files, title, current_dir) processed_count += 1 elif choice == 'E' or choice == 'EDIT': new_name = click.prompt("Neuer Ordnername", type=str, default=title) if new_name.strip(): move_files_to_folder(files, new_name.strip(), current_dir) processed_count += 1 else: click.secho(" ⊘ Übersprungen (leerer Name)", fg='yellow') skipped_count += 1 else: click.secho(" ⊘ Übersprungen", fg='yellow') skipped_count += 1 # Summary click.echo(f"\n{'='*70}") click.secho("Zusammenfassung:", fg='green', bold=True) click.echo(f" Verarbeitet: {processed_count}") click.echo(f" Übersprungen: {skipped_count}") click.secho("\n✓ Fertig!", fg='green', bold=True) if __name__ == '__main__': main()