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>
83 lines
3.0 KiB
Python
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()
|