77 Commits

Author SHA1 Message Date
Maksim Totmin
ba7144c88f feat: migrate LibraryCache to SQLite, add SessionStore + splash overlay
- 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
2026-06-01 22:12:56 +07:00
Maksim Totmin
2d77b3ea3e feat: modernize iPod tab with table view, search, and sorting, fix LibraryCache API 2026-06-01 22:11:36 +07:00
Maksim Totmin
597603f90e feat: add ready audio files (m4a/mp3/aac) to library with structured copy
- 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
2026-06-01 21:37:41 +07:00
Maksim Totmin
b7367e63c4 fix: correct playlist creation on iPod (real db_track_id, metadata fallback, Settings hover fix) 2026-06-01 21:05:39 +07:00
Maksim Totmin
8861e55f32 Add shuffle and repeat controls with end-of-playlist fix 2026-06-01 20:32:56 +07:00
Maksim Totmin
aaaf354c9d fix: restore JumpStyle seek slider and fix fractional truncation bug
- 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
2026-06-01 20:09:52 +07:00
Maksim Totmin
a66446120c refactor: remove dead code — unused tab_switch_requested signal, scope concept in hotkeys, redundant ipod_tab_index 2026-06-01 20:04:58 +07:00
Maksim Totmin
b68b86929b feat: drag-and-drop tracks onto playlist (sidebar) and iPod (tab + nav icon) 2026-06-01 20:00:56 +07:00
Maksim Totmin
bfd146a4a0 feat: add star rating to metadata editor with write support to audio tags
- ClickableStarRating widget — 5 stars with hover preview, toggle to unrate
- Rating field in MetadataEditorDialog (0–5 stars), saved to LibraryCache (0–100)
- MetadataHandler writes rating to MP3 (POPM), M4A (\xa9rt), FLAC/Ogg (RATING)
2026-06-01 19:59:56 +07:00
Maksim Totmin
309ba994a5 fix: rating no longer reset to -1 during mount/sync from iPod Play Counts
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).
2026-06-01 19:45:32 +07:00
Maksim Totmin
a36312c9d7 fix: QThread crash on iPod connect and device detection — proper wait() lifecycle
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
2026-06-01 19:25:30 +07:00
Maksim Totmin
dd93440146 fix: hide vertical header (row numbers) in library table 2026-06-01 15:18:25 +07:00
Maksim Totmin
a1d6465730 fix: sort by # now uses track_num from UserRole instead of cell text, so EQ animation doesn't break sorting 2026-06-01 15:13:00 +07:00
Maksim Totmin
199fb43f04 feat: iTunes-style sidebar + global player header + album grid
- 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
2026-06-01 14:46:58 +07:00
Maksim Totmin
4c703db802 Add incremental metadata/artwork sync to iPod (iTunes-style)
- content_hash() — SHA1 of first 64KB for matching local↔iPod tracks
- Nano7Database.update_track_metadata() — update DB records only,
  never touch audio files on the iPod
- Nano7Database.find_track_by_content_hash() + find_track_by_metadata()
- _write_databases_from_tracks() extracted from sync_itunescdb,
  supports artwork_overrides parameter
- LibraryCache stores content_hash for cached tracks
- Worker + UI: 'Sync Metadata to iPod' button + context menu item
- _REMOVE_ARTWORK sentinel for explicit artwork removal
- track_info.py: add play_count, rating, last_played, skip_count fields
2026-06-01 13:21:22 +07:00
Maksim Totmin
3bfb0ebbfe Fix UI freeze on iPod mount/track-load and add content_hash/sync-metadata
- 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
2026-06-01 12:37:49 +07:00
Maksim Totmin
25bb8105ac feat: auto-detect iPod connection via QTimer polling + fix AlreadyMounted in _mount_linux 2026-06-01 12:29:43 +07:00
Maksim Totmin
71d162520c refactor: extract main.py into ui/ tabs + app.py, extract search_itunes into services/, remove MusicBrainz 2026-06-01 09:59:00 +07:00
Maksim Totmin
e569f8c731 refactor: remove YouTube download feature, extract TrackInfo dataclass, clean up config and hotkeys 2026-06-01 09:19:14 +07:00
Maksim Totmin
9cdb27dace refactor: replace JumpSlider subclass with QProxyStyle, clamp slider seek bounds 2026-06-01 08:57:55 +07:00
Maksim Totmin
d153d69bfd feat: hide library toolbar buttons option, Ctrl+O hotkey, iTunes-style equalizer animation on playing track 2026-06-01 08:56:03 +07:00
Maksim Totmin
ec797b8074 feat: persist play/pause state — auto-resume playback or pause-on-load on restart 2026-06-01 01:25:34 +07:00
Maksim Totmin
06f6529cbe feat: remember last played track, playback position, and volume across restarts 2026-06-01 01:22:16 +07:00
Maksim Totmin
949783e76c feat: hide library progress bar and status when iPod not connected 2026-06-01 01:14:17 +07:00
Maksim Totmin
61bcaec7a3 feat: hide iPod tab/button/menu when no device + fix settings persistence
- 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
2026-06-01 01:05:05 +07:00
Maksim Totmin
1ae1e90ae3 chore: remove redundant wasmtime install from build script (now a transitive dep of iopenpod) 2026-06-01 00:58:59 +07:00
Maksim Totmin
a0b1aba50e fix: resolve iOpenPod import conflict by removing src/ from sys.path during iop imports
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.
2026-06-01 00:55:12 +07:00
Maksim Totmin
5ba346cc84 fix: add project-local iOpenPod/ fallback so /tmp cleanup won't break the app 2026-06-01 00:46:44 +07:00
Maksim Totmin
e3666c8c64 refactor: dynamic iOpenPod discovery, add to requirements
- 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)
2026-06-01 00:40:06 +07:00
Maksim Totmin
cbc888660f fix: hotkey capture compatibility, add library metadata caching
- 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
2026-06-01 00:19:44 +07:00
Maksim Totmin
ba6935420d fix: use .value for KeyboardModifier/Key enum in keyPressEvent (PyQt6+Python 3.11 compat) 2026-06-01 00:09:23 +07:00
Maksim Totmin
a93bc7907d feat: add metadata editor dialog with cover art and batch editing support
- 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
2026-06-01 00:07:42 +07:00
Maksim Totmin
1ace4ad98c feat: add configurable keyboard shortcut system
- 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
2026-05-31 23:26:41 +07:00
Maksim Totmin
8cc4ef5279 Move cover cache and temp files from /tmp to XDG cache (~/.cache/neo-pod-desktop/)
- 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
2026-05-31 23:16:42 +07:00
Maksim Totmin
6a841afaa0 Add -bitexact to MP3 encoding to suppress ffmpeg timestamp warning 2026-05-31 22:52:30 +07:00
Maksim Totmin
340b0dea9e Switch default output to MP3, fix AAC for iPod Nano 7G, auto-remove gray source rows after convert
- 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
2026-05-31 22:44:02 +07:00
Maksim Totmin
63d72adae6 fix: move iPod track deletion to background thread with progress bar, batch DB sync 2026-05-31 22:43:59 +07:00
Maksim Totmin
3da5e15fe0 fix: add +global_header +genpts ffmpeg flags, preserve cover art streams, gapless metadata for iPod Nano 7 2026-05-31 22:13:47 +07:00
Maksim Totmin
71928d3478 feat: integrate artwork module into sync pipeline for Nano 7G 2026-05-31 21:45:51 +07:00
Maksim Totmin
9da689782a fix: clamp default audio bitrate to 160 kbps for iPod Nano 7 compatibility 2026-05-31 21:45:49 +07:00
Maksim Totmin
0f12c4985f fix: robust iOpenPod imports and sync error handling
_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/.
2026-05-31 21:20:42 +07:00
Maksim Totmin
4f7bd2709c fix: resolve ipod_device import conflict with lazy imports
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.
2026-05-31 21:03:11 +07:00
Maksim Totmin
4c4a19640d feat: integrate iOpenPod SQLiteDB_Writer and iTunesDB_Writer
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)
2026-05-31 20:54:45 +07:00
Maksim Totmin
fba3fe7d05 Add HASHAB signing to iTunesCDB header + fix sync_itunescdb
- Set hashing_scheme=3 at offset 0x30 (iPod Nano 7G requirement)
- Compute and write HASHAB signature at offset 0xAB
- Import compute_hashab from hashab module
- iTunesCDB.ext creation restored (was removed accidentally)
- 0-byte iTunesDB placeholder (firmware expects this file)
2026-05-31 19:10:44 +07:00
Maksim Totmin
01902d252a Fix container, version_info, db_info schemas per iOpenPod reference
- container: 24 columns including smart_is_dynamic, smart_is_filtered
- version_info: full schema (id, major, minor, compatibility, update_level)
- db_info: full schema (pid, audio_language=-1, subtitle_language=-1)
- genre_map: rewritten to match reference schema
- item: add genre_id column
- Music container: smart_is_dynamic=1, smart_is_filtered=1
2026-05-31 19:01:05 +07:00
Maksim Totmin
29e4cc7398 Fix missing DB tables: container, version_info, db_info, genre_map, extra DBs
- 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
2026-05-31 18:55:20 +07:00
Maksim Totmin
75ea0204ff Fix iTunesCDB header: always generate proper mhfd header
- Remove broken header-preservation logic (was reusing all-zero header)
- Always write header with correct total_size at offset 8
- Add version field 0x00020002 at offset 12
- Delete old iTunesDB (iTunesDB.old) to avoid firmware confusion
2026-05-31 18:47:36 +07:00
Maksim Totmin
ab912c2e49 Fix iPod transfer: create Music/F00 dir and iTunesCDB on first run
- 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
2026-05-31 18:39:25 +07:00
Maksim Totmin
cc429eed31 Add _ensure_schema() to create SQLite tables 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
2026-05-31 18:31:26 +07:00
Maksim Totmin
8c9e146a6c Add wasmtime to AppImage build for HASHAB signing 2026-05-31 18:24:07 +07:00