184 lines
5.8 KiB
Python
184 lines
5.8 KiB
Python
#!/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()
|