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>
This commit is contained in:
82
scripts/clear_computed_ratings.py
Normal file
82
scripts/clear_computed_ratings.py
Normal file
@ -0,0 +1,82 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user