Files
lintunes/scripts/clear_computed_ratings.py
Trav f6d35fa594 Import existing LinTunes project
Snapshot of the existing codebase before working through the TASKS.md
backlog. Real library data (data/) and the iTunes import fixture
(itunes-test-library/) are gitignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 21:12:01 -04:00

83 lines
3.0 KiB
Python

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