diff --git a/organize_mp3s.py b/organize_mp3s.py new file mode 100644 index 0000000..342f246 --- /dev/null +++ b/organize_mp3s.py @@ -0,0 +1,183 @@ +#!/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()