fix: reliable play count sync with auto-sync + deferred iTunesDB commit
Stops double-counting when iPod Nano 7G regenerates Play Counts file after a DB write (firmware quirk, new mtime with stale data). Architecture: - Mount: only max(cache, base) — never reads Play Counts delta - 2s after mount → auto-sync worker: reads delta, subtracts stored (persistent guard), applies effective to cache, defers DB write - iTunesDB commit deferred to next stable mount (checks W_OK first) - read_play_stats() uses 3s retry + read_play_counts_delta() fallback (filesystem-based PC parsing, independent of iTunesDB I/O errors) Changes: - ipod_nano7_db.py: sync_itunescdb() returns bool; new read_play_counts_delta() method; get_all_tracks() no longer adds delta; _merge_play_stats() drops delta addition; suppress iOpenPod ERROR logs to CRITICAL - library_cache.py: schema v2 with playcounts_log table + get/set_last_synced_delta, migration from v1 - worker.py: session guards (_processed_fwids, _auto_synced_fwids, _pending_commit_fwids); auto_sync_playcounts task; deferred commit in mount - app.py: QTimer.singleShot(2000) after mount for auto-sync - ipod_tab.py: _on_auto_state_changed rewritten (no unmount, mp guard) - library_tab.py: refresh_play_counts_from_cache() method
This commit is contained in:
+37
-1
@@ -1023,9 +1023,45 @@ class iPodTab(QWidget):
|
||||
pass
|
||||
|
||||
def _on_auto_state_changed(self, devices):
|
||||
"""Automatic device detection callback from the monitor timer.
|
||||
|
||||
Updates the device combo silently and only mounts a device that
|
||||
is not already active. Idempotent Play Counts processing in
|
||||
the worker prevents double‑counting even if the mount handler
|
||||
runs multiple times due to USB glitches.
|
||||
"""
|
||||
if self._is_worker_running():
|
||||
return
|
||||
self._on_device_detection_finished(True, f"Found {len(devices)} devices", devices)
|
||||
|
||||
self._refresh_device_combo(devices)
|
||||
|
||||
if not devices:
|
||||
return
|
||||
|
||||
mp = devices[0].get("mount_point", "")
|
||||
|
||||
if mp and mp == self.current_mount_point:
|
||||
return
|
||||
|
||||
self._start_mount_and_load(
|
||||
devices[0],
|
||||
mount_point=mp,
|
||||
already_mounted=devices[0].get("mounted", False),
|
||||
)
|
||||
|
||||
def _refresh_device_combo(self, devices):
|
||||
"""Update the device combo box without triggering re‑mount."""
|
||||
self.device_combo.blockSignals(True)
|
||||
self.device_combo.clear()
|
||||
for device in devices:
|
||||
mounted = device.get("mounted", False)
|
||||
suffix = " [mounted]" if mounted else " [not mounted]"
|
||||
self.device_combo.addItem(device.get("name", "Unknown Device") + suffix)
|
||||
idx = self.device_combo.count() - 1
|
||||
self.device_combo.setItemData(idx, device, Qt.ItemDataRole.UserRole)
|
||||
if devices:
|
||||
self.device_combo.setCurrentIndex(0)
|
||||
self.device_combo.blockSignals(False)
|
||||
|
||||
def _on_device_combo_changed(self, index):
|
||||
device = self.device_combo.itemData(index, Qt.ItemDataRole.UserRole)
|
||||
|
||||
@@ -685,6 +685,52 @@ class LibraryTab(QWidget):
|
||||
self._restore_playing_row()
|
||||
self._build_album_grid()
|
||||
|
||||
def refresh_play_counts_from_cache(self):
|
||||
"""Reload play-count values from the persistent cache without a
|
||||
full library rescan.
|
||||
|
||||
Called after ``mount_and_load`` completes — the cache already
|
||||
holds merged iPod + local play stats, but the table was
|
||||
rendered before the mount and displays stale values.
|
||||
"""
|
||||
cache = get_library_cache()
|
||||
updated = 0
|
||||
|
||||
ready_index = {t["path"]: i for i, t in enumerate(self.library_ready)}
|
||||
|
||||
for row in range(self.library_table.rowCount()):
|
||||
item_0 = self.library_table.item(row, 0)
|
||||
if item_0 is None:
|
||||
continue
|
||||
data = item_0.data(Qt.ItemDataRole.UserRole)
|
||||
if data is None:
|
||||
continue
|
||||
_is_ready, track = data
|
||||
if not _is_ready:
|
||||
continue
|
||||
path = track.get("path", "")
|
||||
if not path:
|
||||
continue
|
||||
entry = cache.get(path)
|
||||
if entry is None:
|
||||
continue
|
||||
|
||||
new_pc = entry.get("play_count", 0)
|
||||
old_pc = track.get("play_count", 0)
|
||||
if new_pc != old_pc:
|
||||
track["play_count"] = new_pc
|
||||
pc_cell = self.library_table.item(row, 7)
|
||||
if pc_cell:
|
||||
pc_cell.setText(str(new_pc))
|
||||
|
||||
idx = ready_index.get(path)
|
||||
if idx is not None:
|
||||
self.library_ready[idx]["play_count"] = new_pc
|
||||
updated += 1
|
||||
|
||||
if updated:
|
||||
logger.debug("Refreshed play counts for %d tracks from cache", updated)
|
||||
|
||||
def _restore_playing_row(self):
|
||||
idx = self.current_playback_index
|
||||
if idx < 0:
|
||||
|
||||
Reference in New Issue
Block a user