#!/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()