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
- Rewrite LibraryCache from JSON (cache dir) to SQLite (data dir with WAL)
- Each track gets a stable UUID so play stats survive file renames/re-encodes
- Thread-safe via PRAGMA busy_timeout + RLock
- Automatic backup rotation (.backup, .bak) on every write
- One-shot migration from old ~/.cache/library_cache.json
- Add SessionStore (~/.local/share/session.json) for playback state
- Add SplashOverlay with spinning animation on first (empty-DB) launch
- _scan_library() now accepts on_progress callback for the overlay
- Fix _mtime → mtime SQLite column name lookup in recently-added filter
- PlaylistManager path migrated to ~/.local/share/ with auto-migration
- Metadata sync: remove dead consume_play_deltas(), use library API
- Expand Add Files dialog to accept all supported audio formats
- Copy ready files to output_dir/Artist/Album/{track} - Title.ext
- Fallback to output_dir root when metadata is missing
- Handle filename collisions with numeric suffix
- Add JumpStyle(QProxyStyle) to player_header.py so clicking the seek
bar jumps to the clicked position (lost during player_header extraction)
- Fix _on_slider_user_action: int(frac) always truncated to 0
(e.g. 500/1000=0.5 → int(0.5)=0), now emits raw value 0-1000
- Fix _on_user_seek_frac: properly converts slider value (0-1000) to
playback position with bounds clamping
The Play Counts parser uses rating=-1 as sentinel for 'no change',
but the merge logic used which treats -1 as truthy, overwriting
real ratings with -1 in all three paths: mount-time sync (worker.py),
DB regeneration (_merge_play_stats), and track listing (get_all_tracks).
Root cause: QThread C++ destructor ran while underlying OS thread was
still terminating. The pattern of checking isRunning() and using
short timeouts (wait(3000/5000)) left a window where the Python
reference was dropped and Shiboken destroyed the C++ object before
the thread fully exited.
Fixes:
- Remove all self.finished.connect(self.deleteLater) — unsafe,
deleteLater from within run() or from worker thread races with
thread termination
- _cleanup_worker: always call wait() (no timeout) before nulling
the Python reference — guarantees OS thread is fully dead
- Add _is_worker_running() helper with try/except RuntimeError
guard to safely check stale C++ objects
- DeviceMonitor._poll/stop/wait: use wait() with no timeout
- closeEvent: guard isRunning() with try/except RuntimeError
- New: sidebar_widget.py — iTunes-like sidebar with library/playlist/nav sections, GTK palette
- New: player_header.py — global player header (cover, now playing, controls) above splitter
- New: playlist_manager.py — playlist CRUD with JSON persistence
- Refactor: moved QMediaPlayer from LibraryTab to MainWindow for clean architecture
- Refactor: LibraryTab — removed all player code, added play_requested signal
- Feat: album grid (IconMode) — grouped by album name, rounded covers 8px, Spotify-like
- Feat: NumericTableItem — numeric sort for track # column (1,2,3...10,11 not 1,10,2)
- Feat: track_num animation fixed — finds row by path, not index (no freeze on sort)
- Feat: album_artist tag extracted and stored in TrackInfo
- Feat: state persistence — view_mode/filtered_album saved to config.ini on close
- Feat: Ctrl+B toggle sidebar
- Fix: sidebar splitter handle colour from GTK palette
- Move mount_device, get_device_info, and track scanning to background
WorkerThread (new 'mount_and_load' task type) so the UI stays responsive
- Fix DeviceMonitor auto-poll to run detect_devices() in a background thread
- Refactor _on_device_detection_finished and _on_mount_clicked to use the
new worker; remove synchronous _set_device_mounted
- Avoid double scan (get_track_count + get_all_tracks) — scan once in worker
- Add content_hash fingerprint to _scan_ipod_files and library_cache for
track matching
- Add update_track_metadata / _write_databases_from_tracks with artwork
override support in Nano7Database
- Add 'Sync Metadata' button and context menu action in LibraryTab
- Hide iPod tab when no iPod connected, show on device detection
- Disable Transfer button and context menu item when no iPod mounted
- Add missing settings load/save: video_resolution, use_musicbrainz, show_download
- Fix clean_temp section mismatch (was [General], now [Advanced])
- Add _save_settings() called on closeEvent
iTunesDB_Writer's internal 'from ipod_device import ChecksumType' was resolving
to our local src/ipod_device.py instead of iOpenPod's version, even with
iop path at sys.path[0] (Python 3.14 import resolution quirk).
Now temporarily removes project src/ from sys.path during iOpenPod imports,
restoring it afterwards. The validated flow: main.py imports local ipod_device
first, then _import_iop evicts and replaces it when iPod tab is used.
- Replace hardcoded /tmp/iOpenPod with importlib.util.find_spec lookup
with fallback to /tmp/iOpenPod and ~/iOpenPod for dev/backward compat
- Add iopenpod>=1.0.53 to requirements.txt
- Remove deprecated mkdir temp/downloads/converted from install.sh
(all paths now XDG-compliant)
- Fix KeyCaptureDialog for both old (int) and new (enum) PyQt6 enum types
using universal _to_int() helper
- New library_cache.py: JSON cache (~/.cache/neo-pod-desktop/library_cache.json)
with mtime-based invalidation — second launch skips tag extraction for
unchanged files (instant load for large libraries)
- _scan_library(force=False) uses cache by default; Refresh/F5 uses force=True
- _incremental_update_after_edit() updates cache + refreshes UI without
full filesystem walk
- Cache entries invalidated on file deletion
- Add update_tags() method to MetadataHandler for writing arbitrary tag
changes to MP3/M4A/FLAC/OGG files (title, artist, album, album_artist,
genre, date, track/disc number, comment, cover art)
- New MetadataEditorDialog (QDialog) with: cover art preview (drag-drop,
click-to-change, remove), form fields for all common tags, F2 / context
menu / toolbar button integration
- Batch editing: select multiple tracks to edit common fields (title and
track numbers hidden), mixed values shown as placeholder
- Search now filters across Artist, Title, and Album columns
- Fix: layout warning from double setLayout in dialog
- Fix: cover cache invalidation after metadata edit
- Fix: missing QDialog import causing crash in shortcut remapping
- Add F2 hotkey default for Edit Metadata action
- src/hotkeys.py: HotkeyManager with 17 default shortcuts, config
load/save via config.ini, KeyCaptureDialog for reassignment UI
- src/config_loader.py: [Hotkeys] section defaults, _set_defaults
called on every load to backfill missing sections
- src/main.py: integrate HotkeyManager, seek_forward/backward (+/-5s),
context-aware select_all/delete_selected, Settings tab UI with
shortcut table and Reset to Defaults button
- Add xdg_cache_path() utility respecting
- CoverCache now stores covers in ~/.cache/neo-pod-desktop/covers/
- MetadataHandler temp files in ~/.cache/neo-pod-desktop/thumbnails/
- Survives reboot, no more lost cover_path during process_file
- Separate AAC and MP3 ffmpeg paths: remove +global_header and +genpts from MP3
- Clamp AAC bitrate to 160 kbps (iPod Nano 7G max); warn on override
- Make mp3 the default format in UI, converter, and CLI
- Auto-remove converted source tracks from library UI (keep files on disk)
- Bulk delete_tracks with single scan+sync (progress support)
- Async duplicate removal with progress via WorkerThread
_import_iop() now permanently evicts local ipod_device from sys.modules
on first call (instead of restoring it), caches all iOpenPod modules,
and avoids the spurious ModuleNotFoundError cascade on rapid calls.
sync_itunescdb() wrapped in try/except — no longer propagates
import/sync errors to add_track() callers.
ipod_device.py fallback no longer duplicates files to /Music/ when
add_track() already copied them to F00/.
main.py imports our local ipod_device first, which caches it in
sys.modules. When ipod_nano7_db later tried 'from ipod_device import
ChecksumType', Python returned the cached local module (which lacks
ChecksumType), causing 'ipod_nano7_db not available' and falling back
to simple file copy into wrong /Music/ directory.
Fix: replace top-level iOpenPod imports with lazy _import_iop() helper
that temporarily evicts local ipod_device from sys.modules, adds
/tmp/iOpenPod to sys.path, loads the iOpenPod module, then restores
the path. All iOpenPod imports now happen inside method bodies only.
Replace custom SQLite schema and binary mhbd writer with iOpenPod's
battle-tested database generators. The Nano 7G firmware reads SQLite
exclusively and cross-references db_id between binary and SQLite —
the old code had mismatched IDs, missing base_location table, and
incomplete schema.
Changes:
- ipod_nano7_db.py: full rewrite — use iOpenPod write_sqlite_databases
+ write_itunesdb instead of manual SQLite inserts and broken mhfd
- add_track() now copies file then regenerates ALL databases atomically
- sync_itunescdb() scans iPod filesystem, builds TrackInfo, calls
iOpenPod writers with matching db_pid
- get_all_tracks() scans filesystem with mutagen tag extraction
- ipod_mhbd_writer.py: kept as standalone reference/utility
Fix: music finally shows on iPod Nano 7G (xC695)
- Add container table with Master (iPod) and Music containers
- Add version_info, db_info, genre_map, location_kind_map, category_map
- Create Dynamic.itdb, Extras.itdb, Genius.itdb with minimal schemas
- Insert default container entries with correct PIDs
- Nano 7G firmware requires all these for music display
- copy_track_to_ipod: create target directory via os.makedirs
- sync_itunescdb: create iTunesCDB from scratch if missing
- sync_itunescdb: create iTunesCDB.ext with hash if missing