v0.20.0: the Rabbit's plays come home
andTunes phase 3. The R1 is now one more machine in the Round 38 play-count model, and nothing lands on it that Android can't play. Play counts: andTunes 0.2.0 counts a play on a natural finish (LinTunes' rule) and keeps Music/andTunes/plays/andtunes-<install id>.json in exactly the per-machine-totals journal shape, written beside the old one and renamed over it. Each sync first folds it into <data_dir>/plays/ with a new play_journal.merge_totals, the per-track max: the file now has two homes and both desktops may bring it back, so the max converges and an older copy can never pull a count down. Nothing is written when nothing moved, and PlayJournal.load needed no change. Format gate: the planner reuses export/web_support.conversion_for outright, since Android's MediaPlayer decodes the browser's set. FairPlay is refused and reported; ALAC, AIFF and oddities land as FLAC, converted once into a per-track cache and size-diffed after that. With no ffmpeg, the export's "sync without them?" question. A file ffmpeg can't read costs that song, not the sync. "Sync Playlist to Rabbit (Auxio)" is retired from the menu; device_sync's helpers stay because export and andTunes import them. The dev fixture grew a real ALAC track, a Protected AAC .m4p and an "Odd Formats" playlist. Verified on the R1 with it: AIFF and ALAC arrived as FLAC and played, the FairPlay track was refused, four plays came back on the next sync and each track's effective count rose by exactly one, and a third sync copied nothing. Found along the way: USB re-enumeration can wedge gvfsd-mtp, after which anything touching the mount (find_device, the GUI tests) hangs in an uninterruptible wait. CLAUDE.md now has the recovery. Claude-Session: https://claude.ai/code/session_018ZCVBTJRFJ2XfMshu2gtUv
This commit is contained in:
@@ -194,3 +194,34 @@ class PlayJournal:
|
||||
write_json(journal_dir / f"{self.machine}.json",
|
||||
{str(k): v for k, v in sorted(self._own.items())})
|
||||
self.dirty = False
|
||||
|
||||
|
||||
def merge_totals(mine: dict, incoming: dict) -> tuple[dict, int]:
|
||||
"""Two copies of *one machine's* journal → the higher total per track.
|
||||
|
||||
Totals only ever grow, so the larger count is always the newer one and
|
||||
taking it can never double-count. Used for the Rabbit's journal, which
|
||||
lives both on the device and in the data dir (`andtunes/plays.py`); the
|
||||
same rule as `conflict_resolver._merge_play_journal`, which predates it.
|
||||
Returns ``(merged, tracks that changed)``.
|
||||
"""
|
||||
merged = dict(mine)
|
||||
changed = 0
|
||||
for tid, entry in incoming.items():
|
||||
have = merged.get(tid)
|
||||
if have is None:
|
||||
merged[tid] = dict(entry)
|
||||
changed += 1
|
||||
continue
|
||||
combined = dict(have)
|
||||
for count in ("plays", "skips"):
|
||||
if count in have or count in entry:
|
||||
combined[count] = max(have.get(count, 0), entry.get(count, 0))
|
||||
for stamp in ("last_played", "last_skipped"):
|
||||
combined[stamp] = _newer(have.get(stamp), entry.get(stamp))
|
||||
if combined[stamp] is None:
|
||||
del combined[stamp]
|
||||
if combined != have:
|
||||
changed += 1
|
||||
merged[tid] = combined
|
||||
return merged, changed
|
||||
|
||||
Reference in New Issue
Block a user