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
- Creates all required tables (item, artist, album, avformat_info,
store_info, item_to_container, track_size_calc) in Library.itdb
- Creates location table in Locations.itdb
- Called from Nano7Database.__init__(), so empty DB is always valid
- Fixes 'no such table: item' error on iPod with wiped DB
- Change ithmb file open from 'wb' (truncate) to 'ab' (append)
- Compute write offset from actual file position via seek(0,2)
- Prevents losing all previous artwork data on each transfer
- Config is now in XDG_CONFIG_HOME (~/.config/) for write access in AppImage
- Auto-migrates existing config.ini from project root on first run
- Creates directory on save() if not exists
- Create venv with --copies to avoid system pip restrictions (PEP 668)
- Install deps into venv before PyInstaller run
- Use absolute paths for --add-data