#!/usr/bin/env python3 """One-time migration: clear iTunes' guessed ("computed") ratings from an already-imported lintunes library, without touching anything else. Reads the iTunes XML to find which tracks had `Rating Computed` set, then zeroes `rating`/`album_rating` for those tracks in /library.json, matching by persistent ID (track IDs are not stable across exports). Usage: python3 scripts/clear_computed_ratings.py \ --xml "itunes-test-library/iTunes Library.xml" --data-dir ./data """ import argparse import json import plistlib import sys from pathlib import Path def main(): parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument("--xml", required=True, help="iTunes Library XML file") parser.add_argument("--data-dir", required=True, help="lintunes data dir") parser.add_argument("--dry-run", action="store_true", help="Report what would change without writing") args = parser.parse_args() xml_path = Path(args.xml) library_path = Path(args.data_dir) / "library.json" if not xml_path.exists(): sys.exit(f"XML not found: {xml_path}") if not library_path.exists(): sys.exit(f"Library not found: {library_path}") print(f"Reading {xml_path}...") with open(xml_path, "rb") as f: plist = plistlib.load(f) computed_rating_pids = set() computed_album_pids = set() for itunes_track in plist.get("Tracks", {}).values(): pid = itunes_track.get("Persistent ID") if not pid: continue if itunes_track.get("Rating Computed"): computed_rating_pids.add(pid) if itunes_track.get("Album Rating Computed"): computed_album_pids.add(pid) print(f"XML: {len(computed_rating_pids)} computed track ratings, " f"{len(computed_album_pids)} computed album ratings") with open(library_path, "r", encoding="utf-8") as f: tracks = json.load(f) rated_before = sum(1 for t in tracks.values() if t.get("rating")) cleared = album_cleared = 0 for track in tracks.values(): pid = track.get("persistent_id", "") if pid in computed_rating_pids and track.get("rating"): del track["rating"] cleared += 1 if pid in computed_album_pids: if track.pop("album_rating", None) is not None: album_cleared += 1 track.pop("album_rating_computed", None) rated_after = sum(1 for t in tracks.values() if t.get("rating")) print(f"Track ratings: {rated_before} before -> {rated_after} after " f"({cleared} guessed ratings cleared, {album_cleared} album ratings)") if args.dry_run: print("Dry run — nothing written.") return tmp = library_path.with_suffix(".json.tmp") with open(tmp, "w", encoding="utf-8") as f: json.dump(tracks, f, indent=2, ensure_ascii=False) tmp.replace(library_path) print(f"Wrote {library_path}") if __name__ == "__main__": main()