83 lines
2.4 KiB
Python
83 lines
2.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Bulk-rename music files in-place using ID3 tags via mutagen.
|
|
|
|
Reads artist/title from file metadata and renames to `Artist - Title.ext`.
|
|
Handles collisions, special characters, and files already correctly named.
|
|
|
|
Usage:
|
|
python3 bulk-rename.py /path/to/music/dir
|
|
|
|
Place in ~/.hermes/skills/media/music-library-management/scripts/
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
from mutagen import File
|
|
|
|
def sanitize(name):
|
|
return name.replace('/', '-').replace('\x00', '')
|
|
|
|
def rename_tree(base):
|
|
renamed = 0
|
|
skipped_tag = 0
|
|
skipped_ok = 0
|
|
errors = []
|
|
|
|
for root, dirs, files in os.walk(base):
|
|
for fname in files:
|
|
if not fname.lower().endswith(('.mp3', '.flac', '.m4a', '.ogg')):
|
|
continue
|
|
|
|
fullpath = os.path.join(root, fname)
|
|
|
|
try:
|
|
audio = File(fullpath, easy=True)
|
|
except Exception as e:
|
|
errors.append(f"READ {fullpath}: {e}")
|
|
continue
|
|
|
|
if audio is None or not audio.tags:
|
|
skipped_tag += 1
|
|
continue
|
|
|
|
artist = audio.tags.get('artist', [None])[0]
|
|
title = audio.tags.get('title', [None])[0]
|
|
|
|
if not artist or not title:
|
|
skipped_tag += 1
|
|
continue
|
|
|
|
ext = os.path.splitext(fname)[1]
|
|
new_name = f"{sanitize(artist)} - {sanitize(title)}{ext}"
|
|
new_path = os.path.join(root, new_name)
|
|
|
|
if fullpath == new_path:
|
|
skipped_ok += 1
|
|
continue
|
|
|
|
# Handle collisions
|
|
counter = 1
|
|
while os.path.exists(new_path) and new_path != fullpath:
|
|
name_no_ext = f"{sanitize(artist)} - {sanitize(title)} ({counter})"
|
|
new_path = os.path.join(root, name_no_ext + ext)
|
|
counter += 1
|
|
|
|
try:
|
|
os.rename(fullpath, new_path)
|
|
renamed += 1
|
|
except OSError as e:
|
|
errors.append(f"RENAME {fullpath}: {e}")
|
|
|
|
print(f"Renamed: {renamed}")
|
|
print(f"Skipped (no tags): {skipped_tag}")
|
|
print(f"Skipped (already correct): {skipped_ok}")
|
|
print(f"Errors: {len(errors)}")
|
|
for e in errors[:20]:
|
|
print(f" {e}")
|
|
|
|
if __name__ == '__main__':
|
|
if len(sys.argv) < 2:
|
|
print(f"Usage: {sys.argv[0]} /path/to/music/dir")
|
|
sys.exit(1)
|
|
rename_tree(sys.argv[1])
|