initial commit
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Batch-add compilation + albumartist tags to MP3 files.
|
||||
|
||||
Usage:
|
||||
python3 fix-compilation-tags.py /path/to/music/
|
||||
python3 fix-compilation-tags.py --albumartist="Ray's Mix" /path/to/music/
|
||||
python3 fix-compilation-tags.py --compilation-only /path/to/music/
|
||||
|
||||
Fixes Plex album grouping when tracks are singles/compilations and Plex
|
||||
incorrectly merges them into phantom albums. Adds:
|
||||
compilation = 1
|
||||
albumartist = "Various Artists" (customizable)
|
||||
|
||||
Requires: mutagen (pip install mutagen)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from mutagen.easyid3 import EasyID3
|
||||
|
||||
|
||||
def fix_tags(base: str, albumartist: str, compilation_only: bool) -> tuple[int, int]:
|
||||
"""Walk base directory, add missing tags to .mp3 files. Returns (fixed, total)."""
|
||||
fixed = total = 0
|
||||
|
||||
for root, dirs, files in os.walk(base):
|
||||
for f in files:
|
||||
if not f.lower().endswith(".mp3"):
|
||||
continue
|
||||
total += 1
|
||||
path = os.path.join(root, f)
|
||||
try:
|
||||
audio = EasyID3(path)
|
||||
changed = False
|
||||
|
||||
if "compilation" not in audio:
|
||||
audio["compilation"] = "1"
|
||||
changed = True
|
||||
|
||||
if not compilation_only and "albumartist" not in audio:
|
||||
audio["albumartist"] = albumartist
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
audio.save()
|
||||
fixed += 1
|
||||
except Exception:
|
||||
pass # Skip unreadable/corrupt files
|
||||
|
||||
if total % 500 == 0:
|
||||
print(f"{total} scanned, {fixed} fixed...", flush=True)
|
||||
|
||||
return fixed, total
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Add compilation/albumartist tags to MP3 files for Plex"
|
||||
)
|
||||
parser.add_argument(
|
||||
"directory", help="Root directory to scan recursively"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--albumartist",
|
||||
default="Various Artists",
|
||||
help='albumartist value (default: "Various Artists")',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--compilation-only",
|
||||
action="store_true",
|
||||
help="Only set compilation=1, skip albumartist",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
base = os.path.abspath(args.directory)
|
||||
if not os.path.isdir(base):
|
||||
print(f"Error: {base} is not a directory", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Processing {base}...")
|
||||
fixed, total = fix_tags(base, args.albumartist, args.compilation_only)
|
||||
print(f"DONE: {fixed}/{total} tracks fixed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Strip album_artist=Various Artists (and empty album_artist TPE2 frames) from MP3 files.
|
||||
|
||||
Walks a directory recursively, removes the album_artist tag when it's
|
||||
"Various Artists" (case-insensitive) or when it's an empty/missing TPE2 frame.
|
||||
All other tags (title, artist, album, year, genre, etc.) are untouched.
|
||||
|
||||
Usage:
|
||||
# Process a directory
|
||||
python3 strip-various-artists.py /mnt/seagate8tb/Music/Albanian/
|
||||
|
||||
# Dry-run — show what would change without modifying files
|
||||
python3 strip-various-artists.py --dry-run /path/to/music/
|
||||
|
||||
# Specific artist name to strip (default: "Various Artists")
|
||||
python3 strip-various-artists.py --artist="Various" /path/to/music/
|
||||
|
||||
After running: Plex will NOT re-read the tags on a normal refresh.
|
||||
Use the "Plex Dance" (remove location → empty trash → re-add → scan)
|
||||
to force Plex to process the corrected tags from scratch.
|
||||
|
||||
Requirements: pip install mutagen
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
try:
|
||||
from mutagen.easyid3 import EasyID3
|
||||
from mutagen.id3 import ID3
|
||||
except ImportError:
|
||||
sys.exit("mutagen is required. Install: pip install mutagen")
|
||||
|
||||
|
||||
def strip_album_artist(filepath: str, target_artist: str, dry_run: bool) -> str | None:
|
||||
"""Remove album_artist from a file if it matches target_artist or is empty.
|
||||
Returns the old value if removed, None otherwise."""
|
||||
try:
|
||||
audio = EasyID3(filepath)
|
||||
aa = audio.get("albumartist", [None])[0]
|
||||
|
||||
if aa and aa.lower() == target_artist.lower():
|
||||
if not dry_run:
|
||||
del audio["albumartist"]
|
||||
audio.save()
|
||||
return aa
|
||||
|
||||
if not aa:
|
||||
# Empty/missing album_artist via EasyID3 — check raw TPE2
|
||||
try:
|
||||
id3 = ID3(filepath)
|
||||
if "TPE2" in id3:
|
||||
old_val = str(id3["TPE2"])
|
||||
if not dry_run:
|
||||
del id3["TPE2"]
|
||||
id3.save()
|
||||
return f"(empty TPE2: {old_val})"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
return f"ERROR: {e}"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Strip album_artist=Various Artists from MP3 files"
|
||||
)
|
||||
parser.add_argument("path", help="Directory to scan recursively")
|
||||
parser.add_argument(
|
||||
"--dry-run", action="store_true", help="Show what would change without modifying files"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--artist",
|
||||
default="Various Artists",
|
||||
help='Target album_artist value to strip (default: "Various Artists")',
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not os.path.isdir(args.path):
|
||||
sys.exit(f"Not a directory: {args.path}")
|
||||
|
||||
fixed = 0
|
||||
errors = 0
|
||||
skipped = 0
|
||||
|
||||
for root, dirs, files in os.walk(args.path):
|
||||
for fname in files:
|
||||
if not fname.lower().endswith(".mp3"):
|
||||
continue
|
||||
fpath = os.path.join(root, fname)
|
||||
result = strip_album_artist(fpath, args.artist, args.dry_run)
|
||||
if result:
|
||||
if result.startswith("ERROR"):
|
||||
print(f" ERROR: {fpath} — {result}")
|
||||
errors += 1
|
||||
else:
|
||||
action = "WOULD strip" if args.dry_run else "Stripped"
|
||||
print(f" {action}: {os.path.relpath(fpath, args.path)} (was: {result})")
|
||||
fixed += 1
|
||||
else:
|
||||
skipped += 1
|
||||
|
||||
action = "Would fix" if args.dry_run else "Fixed"
|
||||
print(f"\n{action}: {fixed}, Errors: {errors}, Skipped (no match): {skipped}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user