init
This commit is contained in:
319
rename_roms.py
Normal file
319
rename_roms.py
Normal file
@@ -0,0 +1,319 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from main import fetch_all_roms, session, base
|
||||
|
||||
DEFAULT_HOST_ROMS_PATH = "/home/jawz/Games/roms"
|
||||
|
||||
|
||||
def translate_docker_path(
|
||||
docker_path: str, host_path: str = DEFAULT_HOST_ROMS_PATH
|
||||
) -> str:
|
||||
"""Translate Docker container path to host path."""
|
||||
if not docker_path:
|
||||
return docker_path
|
||||
|
||||
if docker_path.startswith(host_path):
|
||||
return docker_path
|
||||
|
||||
docker_prefixes = ["/roms", "/data/roms", "/mnt/roms"]
|
||||
|
||||
matching_prefix = next(
|
||||
(prefix for prefix in docker_prefixes if docker_path.startswith(prefix)), None
|
||||
)
|
||||
|
||||
if matching_prefix:
|
||||
relative_path = docker_path[len(matching_prefix) :].lstrip("/")
|
||||
return os.path.join(host_path, relative_path)
|
||||
|
||||
path_parts = Path(docker_path).parts
|
||||
if len(path_parts) > 1:
|
||||
return str(Path(host_path) / Path(*path_parts[1:]))
|
||||
|
||||
return docker_path
|
||||
|
||||
|
||||
def is_matched(rom: Dict) -> bool:
|
||||
"""Check if ROM is matched (has metadata)."""
|
||||
metadata_keys = ["igdb_id", "moby_id", "ss_id", "launchbox_id"]
|
||||
return any(rom.get(key) for key in metadata_keys)
|
||||
|
||||
|
||||
def should_skip_rom(rom: Dict) -> bool:
|
||||
"""Check if ROM should be skipped (e.g., notgame, ZZZ prefix, etc.)."""
|
||||
name = rom.get("name", "")
|
||||
if not name:
|
||||
return True
|
||||
|
||||
name_lower = name.lower()
|
||||
skip_patterns = ["(notgame)", "zzz", "zzz:"]
|
||||
|
||||
return any(pattern in name_lower for pattern in skip_patterns)
|
||||
|
||||
|
||||
def already_has_proper_name(rom: Dict) -> bool:
|
||||
"""Check if ROM already has the proper name format."""
|
||||
if not (fs_name := rom.get("fs_name")):
|
||||
return False
|
||||
|
||||
if not (rom_name := rom.get("name")):
|
||||
return False
|
||||
|
||||
if not (extension := get_extension(rom)):
|
||||
return False
|
||||
|
||||
sanitized_name = sanitize_filename(rom_name)
|
||||
region_code = get_region_code(rom)
|
||||
|
||||
if region_code:
|
||||
expected_name = f"{sanitized_name} ({region_code}).{extension}"
|
||||
else:
|
||||
expected_name = f"{sanitized_name}.{extension}"
|
||||
|
||||
current_name = os.path.basename(fs_name)
|
||||
return current_name.lower() == expected_name.lower()
|
||||
|
||||
|
||||
def is_valid_for_rename(rom: Dict) -> bool:
|
||||
"""Check if ROM is valid for renaming (matched, not skipped, and needs renaming)."""
|
||||
return (
|
||||
is_matched(rom)
|
||||
and not should_skip_rom(rom)
|
||||
and not already_has_proper_name(rom)
|
||||
)
|
||||
|
||||
|
||||
def get_region_code(rom: Dict) -> Optional[str]:
|
||||
"""Extract region code from ROM regions.
|
||||
Returns single letter code like 'U', 'E', 'J' etc.
|
||||
"""
|
||||
regions = rom.get("regions", [])
|
||||
if not regions:
|
||||
return None
|
||||
|
||||
region_map = {
|
||||
"usa": "U",
|
||||
"north america": "U",
|
||||
"ntsc-u": "U",
|
||||
"eur": "E",
|
||||
"europe": "E",
|
||||
"pal": "E",
|
||||
"jpn": "J",
|
||||
"japan": "J",
|
||||
"ntsc-j": "J",
|
||||
"asia": "A",
|
||||
"australia": "A",
|
||||
"world": "W",
|
||||
}
|
||||
|
||||
def find_matching_code(region: str) -> Optional[str]:
|
||||
region_lower = region.lower()
|
||||
return next(
|
||||
(code for key, code in region_map.items() if key in region_lower), None
|
||||
)
|
||||
|
||||
matching_code = next(
|
||||
(code for region in regions if (code := find_matching_code(region))), None
|
||||
)
|
||||
|
||||
if matching_code:
|
||||
return matching_code
|
||||
|
||||
if regions and len(regions[0]) >= 1:
|
||||
return regions[0].upper()[0]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def sanitize_filename(name: str) -> str:
|
||||
"""Sanitize filename to remove invalid characters."""
|
||||
invalid_chars = '<>:"/\\|?*'
|
||||
return name.translate(str.maketrans("", "", invalid_chars)).strip(" .")
|
||||
|
||||
|
||||
def get_extension(rom: Dict) -> Optional[str]:
|
||||
"""Extract file extension from ROM."""
|
||||
if extension := rom.get("fs_extension"):
|
||||
return extension
|
||||
|
||||
if fs_name := rom.get("fs_name"):
|
||||
ext = os.path.splitext(fs_name)[1].lstrip(".")
|
||||
return ext if ext else None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def generate_new_filename(rom: Dict) -> Optional[str]:
|
||||
"""Generate new filename in format: {rom_name} ({region}).{ext} or {rom_name}.{ext} if no region"""
|
||||
if not (rom_name := rom.get("name")):
|
||||
return None
|
||||
|
||||
if not (extension := get_extension(rom)):
|
||||
return None
|
||||
|
||||
sanitized_name = sanitize_filename(rom_name)
|
||||
region_code = get_region_code(rom)
|
||||
|
||||
if region_code:
|
||||
return f"{sanitized_name} ({region_code}).{extension}"
|
||||
|
||||
return f"{sanitized_name}.{extension}"
|
||||
|
||||
|
||||
def rename_rom_file(
|
||||
rom: Dict, dry_run: bool = True, host_path: str = DEFAULT_HOST_ROMS_PATH
|
||||
) -> Tuple[bool, str, str]:
|
||||
"""Rename a ROM file.
|
||||
Returns: (success, old_path, new_path_or_error)
|
||||
"""
|
||||
if not (full_path := rom.get("full_path")):
|
||||
return False, "", "No full_path available"
|
||||
|
||||
translated_path = translate_docker_path(full_path, host_path)
|
||||
|
||||
if not os.path.exists(translated_path):
|
||||
return False, translated_path, f"File does not exist: {translated_path}"
|
||||
|
||||
if not (new_filename := generate_new_filename(rom)):
|
||||
return False, translated_path, "Could not generate new filename"
|
||||
|
||||
old_path = Path(translated_path)
|
||||
new_path = old_path.parent / new_filename
|
||||
|
||||
if new_path.exists() and new_path != old_path:
|
||||
return False, str(old_path), f"Target file already exists: {new_path}"
|
||||
|
||||
if dry_run:
|
||||
return True, str(old_path), str(new_path)
|
||||
|
||||
try:
|
||||
os.rename(str(old_path), str(new_path))
|
||||
return True, str(old_path), str(new_path)
|
||||
except Exception as e:
|
||||
return False, str(old_path), f"Error: {e}"
|
||||
|
||||
|
||||
def process_rom_rename(
|
||||
rom: Dict, dry_run: bool, host_path: str = DEFAULT_HOST_ROMS_PATH
|
||||
) -> Tuple[Dict, bool, str, str]:
|
||||
"""Process a single ROM rename."""
|
||||
success, old_path, new_path = rename_rom_file(
|
||||
rom, dry_run=dry_run, host_path=host_path
|
||||
)
|
||||
return (rom, success, old_path, new_path)
|
||||
|
||||
|
||||
def format_rename_result(rom: Dict, old_path: str, new_path: str) -> str:
|
||||
"""Format a rename result for display."""
|
||||
old_name = os.path.basename(old_path)
|
||||
new_name = os.path.basename(new_path)
|
||||
return (
|
||||
f" {rom.get('name', 'Unknown')} (ID: {rom['id']})\n"
|
||||
f" {old_name}\n"
|
||||
f" -> {new_name}\n"
|
||||
)
|
||||
|
||||
|
||||
def format_error_result(rom: Dict, old_path: str, error: str) -> str:
|
||||
"""Format an error result for display."""
|
||||
return (
|
||||
f" {rom.get('name', 'Unknown')} (ID: {rom['id']})\n"
|
||||
f" {old_path}\n"
|
||||
f" Error: {error}\n"
|
||||
)
|
||||
|
||||
|
||||
def print_results(results: List[Tuple[Dict, bool, str, str]]) -> None:
|
||||
"""Print rename results."""
|
||||
successful = [r for r in results if r[1]]
|
||||
failed = [r for r in results if not r[1]]
|
||||
|
||||
print(f"Successful: {len(successful)}")
|
||||
print(f"Failed: {len(failed)}\n")
|
||||
|
||||
if successful:
|
||||
print("Successfully processed:")
|
||||
print(
|
||||
"".join(
|
||||
format_rename_result(rom, old_path, new_path)
|
||||
for rom, _, old_path, new_path in successful
|
||||
)
|
||||
)
|
||||
|
||||
if failed:
|
||||
print("Failed:")
|
||||
print(
|
||||
"".join(
|
||||
format_error_result(rom, old_path, error)
|
||||
for rom, _, old_path, error in failed
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def rename_matched_roms(
|
||||
dry_run: bool = True, host_path: str = DEFAULT_HOST_ROMS_PATH
|
||||
) -> None:
|
||||
"""Rename all matched ROMs."""
|
||||
print("Fetching all ROMs...")
|
||||
roms = fetch_all_roms(session, base)
|
||||
print(f"Fetched {len(roms)} ROMs\n")
|
||||
|
||||
matched_roms = list(filter(is_matched, roms))
|
||||
valid_roms = list(filter(is_valid_for_rename, roms))
|
||||
already_named = [
|
||||
rom
|
||||
for rom in matched_roms
|
||||
if not should_skip_rom(rom) and already_has_proper_name(rom)
|
||||
]
|
||||
|
||||
print(f"Found {len(matched_roms)} matched ROMs")
|
||||
print(f"Found {len(valid_roms)} valid ROMs for renaming")
|
||||
if already_named:
|
||||
print(f"Skipped {len(already_named)} ROMs that already have proper names\n")
|
||||
else:
|
||||
print()
|
||||
|
||||
if not valid_roms:
|
||||
print("No valid ROMs to rename.")
|
||||
return
|
||||
|
||||
mode = "DRY RUN" if dry_run else "RENAMING"
|
||||
print(f"{'='*60}")
|
||||
print(f"{mode} MODE")
|
||||
print(f"Host path: {host_path}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
results = [process_rom_rename(rom, dry_run, host_path) for rom in valid_roms]
|
||||
|
||||
print_results(results)
|
||||
|
||||
if dry_run and any(r[1] for r in results):
|
||||
print(f"{'='*60}")
|
||||
print("This was a DRY RUN. No files were actually renamed.")
|
||||
print("Run with --execute to perform the renames.")
|
||||
print(f"{'='*60}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Rename matched ROMs")
|
||||
parser.add_argument(
|
||||
"--execute",
|
||||
action="store_true",
|
||||
help="Actually perform the renames (default is dry run)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--host-path",
|
||||
type=str,
|
||||
default=os.getenv("ROMS_HOST_PATH", DEFAULT_HOST_ROMS_PATH),
|
||||
help=f"Host path for ROMs (default: {DEFAULT_HOST_ROMS_PATH})",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
rename_matched_roms(dry_run=not args.execute, host_path=args.host_path)
|
||||
Reference in New Issue
Block a user