Commit Graph
17 Commits
Author SHA1 Message Date
vicrodh 6d8c951a38 chore(deps): rusqlite 0.40 (u64 casts; a latent two-statement execute fixed)
rusqlite 0.40 removed ToSql/FromSql for u64 (SQLite integers are i64):
every u64 column now reads/writes through an explicit i64 cast. It also
rejects a multi-statement execute() instead of silently running only the
first statement — which is what catalog.rs's editions/logical_albums
prune had been doing all along; it is an execute_batch now, so the
logical_albums prune finally runs.

cc
2026-08-29 08:36:21 -06:00
vicrodh 4e41e92347 feat(plex): request includeGuids and keep the recording MBID
Section listings ask for includeGuids=1 and the track parser reads the
<Guid id="mbid://…"/> child (only the Plex Music agent writes it; other
schemes are ignored). Stored in a new plex_cache_tracks.recording_mbid
column (additive; COALESCE on upsert) and read back by the PlexTrack
mappers. PlexCachedTrack and the qbz-source Plex constructor do not
carry it yet — the catalog projection does.

cc
2026-08-28 20:31:05 -06:00
vicrodh c7a3012955 feat(local-library): complete catalog and metadata workflows
Add bounded native library models, complete grouping rails and scan progress. Bring source-aware metadata editing, media information and collection offline preflight to Qt, and repair inherited Plex album artwork without overriding healthy tokens.

cx
2026-08-28 18:50:05 -06:00
vicrodh d244213a64 fix(library): harden cross-source catalog data
Preserve per-disc and collection artwork, multi-value genres and remote metadata across Local, Plex, Jellyfin and Subsonic.

Apply source, format and quality filters before pagination, reject invalid CUE rows, and keep Qt library surfaces aligned with the selected physical version.

cx
2026-08-25 18:26:06 -06:00
vicrodh dc0a37487b fix(plex): restore original media streaming
Request Plex media parts with download=1 so servers that reject the bare part route still return ranged original bytes. Preserve existing query parameters and cover both direct resolvers with a regression test.

cx
2026-08-25 15:03:31 -06:00
vicrodh 63ca21abaf fix(library): paginate grouped Tracks globally
Order every source by the active grouping key before the bounded merge so later pages preserve the visible prefix. Remove the accumulated QML reorder that moved the viewport when new rows sorted ahead.

cx
2026-08-23 19:30:24 -06:00
vicrodh 6e0e0d3165 feat(library): sync Plex sections incrementally
Fetch Plex tracks in bounded container pages and commit each page with its per-section generation checkpoint. Resume interrupted sections, preserve hydrated quality, and authorize stale-row pruning only after the declared total is complete and identity-exact. Sanitize HTTP failures so authenticated request URLs never reach logs.

cx
2026-08-23 13:45:05 -06:00
vicrodh 299dcde5b4 feat(library): page Albums from local catalog
Materialize album browse fields and FTS in the derived catalog, then expose exact counts and stable keyset queries for the current filters, sort, grouping, and enabled sources.

Bind the existing Albums layout to a bounded native Qt model with asynchronous page misses, anchors, selection ranges, generation cancellation, artwork updates, and per-surface fallback. Resolve Plex native edition keys directly and retain the metadata-mode legacy reader.

cx
2026-08-23 10:54:31 -06:00
vicrodh 1edae11e52 fix(library): correct cross-source pagination
Bound each enabled source to an independent candidate page and merge one stable global Tracks page while advancing only consumed offsets. Include remote counts and artists, preserve Plex rows past 5,000, and reject stale search or sort generations before state and UI publication.

cx
2026-08-23 05:31:35 -06:00
vicrodh c2944db973 feat(qt): source-aware playback, session persistence and loading affordances
One batch, committed together because the changes interleave in main.rs and
splitting them after the fact would be a fiction.

Playback routing: a track now carries its source, so a local or Plex row played
from Discover stops being resolved against Qobuz first (the 404s and the long
silent wait before audio). Jellyfin and Navidrome land on the same seam.
Plex tracks stream through the player instead of downloading whole files first;
the feeder moved into qbz-player so both frontends can reach it.

Session persistence, both levels the port was missing:
  - The startup-page preference is finally read. It had been rendering and
    persisting a value nothing consumed. `last_view` is written in the SHARED
    ui_prefs vocabulary, not this port's route ids -- four of the six differ,
    and writing the raw ids would have silently broken the other frontend's
    restore. `on_session_entered` recorded "home" unconditionally, which also
    rewrote last_view on every boot, so the setting could not survive its own
    startup.
  - Queue and active track survive a restart. session_persist was tied to a
    frontend by exactly one type alias, so it moved to qbz-app generic over the
    adapter rather than being copied. Verified end to end: the snapshot restored
    here had been written by the Slint binary, and the exit flush keeps an
    un-consumed resume position instead of overwriting it with the engine's
    idle 0.

Loading affordances: the transport shows a spinner from the moment a play is
dispatched, and the restored session explicitly takes it back down -- a
restored track has no dispatch behind it and no audio ahead of it, so nothing
would ever have cleared it. Cortinilla results now navigate at album and artist
level.

Verified by the offscreen smoke gate and by driving a restore/quit cycle over
MPRIS.

cc
2026-08-13 23:10:32 -06:00
vicrodh 47c315251c fix(slint): plex rows resolve in local playlists; catalog-id guards
Closes the slice-4 'plex rows render basic' deferral: local-playlist plex
refs bulk-resolve from the plex cache by rating key (new qbz-plex
by-keys getter with the cached-track shape) into full-metadata rows -
real titles, duration, PlexThumb artwork, playable through the existing
plex path (induced offline allowed). Cache misses render an honest
'Unavailable Plex track' fallback and legacy garbage refs (plex synthetic
ids stored as qobuz ids by the pre-typed-drag flow) surface as removable
unavailable rows instead of being D11-hidden forever.

Still-live writers of that garbage are fixed: the shared add-to-playlist
arm routes plex:<key>/local refs through the local picker and refuses
synthetic ids with a toast; row_drag_track no longer falls through plex
rows to the Qobuz arm; and add_qobuz/add_drag blocking writers refuse ids
at or above the plex synthetic floor as a last gate. main.rs also carries
the context-menu mixtape/favorite arms from the previous commit.

cc
2026-06-10 10:09:11 -06:00
vicrodh 41c5261613 feat(slint): local album homologation, Recently Played fixes, local artist nav
Album view (local/Plex):
- Rebuild LocalAlbumView as a near copy of the Qobuz AlbumPageView (whole-page
  scroll, 224px cover, action row, toolbar with quality badge + track search,
  column header, source-aware rows) minus the label/awards sidebar and Qobuz
  context menus. Add a per-album client-side track search.

Recently Played (Home) — D:
- Carry the track source (qobuz|plex|local) through the recently-played store so
  the cards resolve source-aware artwork (PlexThumb / local file), the album-card
  play routes to local/Plex playback instead of the empty Qobuz path, and the
  card click opens the right album view.
- Fix a regression from the Plex version split: local_queue_track now derives the
  content-hash plex_album_key for album_id (navigation) while album_group_key keeps
  the per-edition split key, so "go to album" and recent Plex albums open correctly.

Local artist navigation — B:
- Implement open_artist (was a TODO): latch the requested artist and select it once
  the Artists tab loads. The shared open-artist is now source-aware (numeric id ->
  Qobuz artist; name -> LocalLibrary Artists tab). Wire the LocalAlbum header artist
  link and the now-playing "Go to artist" for Plex/local.

Folder delete -> Recently Played cascade:
- Removing a Local Library folder now also prunes its albums/tracks from the
  recently-played store (album_keys_in_folder + recently::prune_albums).

Covers — C:
- Robust on-disk folder cover lookup (cover/folder/front/art/<album>.jpg, any image
  as a last resort), shared by the Folders subcards and now-playing/album rows.

cc
2026-06-09 01:44:39 -06:00
vicrodh 1409c9d917 feat(plex): split same-title Plex albums into selectable versions
A Plex album_key is a hash of artist+album, so two distinct Plex albums
that share a title (e.g. a matched album plus loose files added as a second
album) collapsed into one cache album and their tracks interleaved
(1,1,2,2,...) in the album view with no way to pick a version.

Capture parentRatingKey (the real per-album Plex id) on sync, store it in a
new plex_cache_tracks column, and key the album-view version grouping on it
so each Plex album becomes its own version in the existing header picker
(best quality first). Pre-resync rows lack the column and fall back to the
old single-group behaviour; a re-sync backfills it.

cc
2026-06-08 18:28:15 -06:00
vicrodh b37c29af14 feat(slint): Plex progressive streaming (fast play) + fix re-sync wiping hydrated quality
Two Plex-port fixes (both in the Plex play/data path):

1. Progressive streaming — play starts in ~1s instead of ~10s. play_plex_audible
   no longer downloads the whole file before play_data; it resolves only the
   part URL (new additive qbz_plex::plex_resolve_part_url, no body GET) and feeds
   it into the player's streaming source (play_streaming_dynamic) via a shared
   feeder module (remote_stream.rs, extracted from the QConnect renderer's HTTP
   feeder — both now share it). Bit-perfect preserved: the original /file bytes
   are streamed (no transcode), device init derives from the decoded stream; the
   protected audio backend is untouched. Cast/DLNA keep the whole-file path.
   Falls back to whole-file play_data on a non-direct part / streaming-setup error.

2. Re-sync no longer wipes hydrated quality — the sync did plex_cache_clear()
   (full wipe) BEFORE the per-section save_tracks, defeating save_tracks' COALESCE
   hydration carry-over, so already-hydrated albums reverted to a bare FLAC badge
   on every settings-open / background-refresh / section-toggle. Replaced the full
   wipe with a new additive plex_cache_prune_sections(keep) that removes only the
   DE-selected sections, keeping the selected sections (and their hydration);
   save_tracks then re-applies its per-section carry-over.

cc
2026-06-08 14:39:05 -06:00
vicrodh 5939c090ca feat(slint): Plex quality hydration on album-open + consistent quality badges (port slice 6)
Hydration:
- qbz-plex: additive plex_hydrate_album_quality(base, token, rating_keys) — per
  key plex_get_track_metadata (batch 5, 5s/call timeout, skip failures) then the
  COALESCE write-back plex_cache_update_track_quality; awaited before re-read.
- qbz-player/qbz-core: patch_plex_quality / patch_plex_queue_quality patch the
  in-memory queue in place by rating_key (for the now-playing stamp).
- qbz-slint: album-open trigger (gated on DB-NULL, not a value heuristic) then
  refresh of all badge surfaces — album-card grid (re-read aggregate + patch
  every model + the raw filter source), album-detail header + rows, now-playing
  stamp. Re-sync stays routed through plex_cache_save_tracks (carry-over).

Quality badge consistency (the four surfaces must agree — and pre-hydration
show the filetype, not nothing):
- One shared classifier crate::quality::badge(format, bit_depth, sample_rate) ->
  (tier, detail, tooltip); the album card, album-detail header and track rows
  all route through it (kills the per-surface duplication that produced
  "96 kHz" vs "96000 kHz"). sample_rate normalized to kHz via a >=1000 guard so
  Qobuz's already-kHz values are never re-divided (no "0.041 kHz").
- New "lossless" tier: an un-hydrated lossless file (FLAC/WAV/ALAC… with unknown
  bit depth) shows a generic filetype badge ("FLAC") instead of an empty badge,
  rendered in QualityBadge (compact chip) + QualityBadgeFull; upgrades to
  "24-bit / 96 kHz" after hydration. Local/Qobuz/mp3 unchanged.

cc
2026-06-08 10:27:28 -06:00
vicrodh 334ddf315d feat(slint): Plex artists + tracks in Local Library Artists/Tracks tabs (port slice 3c)
Both tabs were Plex-blind. Loader-level merge (no qbz-library change):
- qbz-plex: additive PlexCachedArtist + plex_cache_get_artists() (aggregates
  the flat plex_cache_tracks by normalized artist) + plex_cache_count_tracks().
- Artists tab: fold Plex artists into the local set before merge_artists
  (same-name local+Plex collapse, summed tracks); artist-detail album grid +
  counts switch to the Plex-aware get_albums_metadata_page; portraits fall back
  custom/Qobuz -> Plex thumb -> album cover, dispatched via spawn_local_or_plex_loads.
- Tracks tab: prepend the bounded Plex set (<=5000) once at offset 0; later
  pages stay pure-local so the virtualized 16K-row LIMIT/OFFSET path is intact.
- Tracks badge now includes the Plex count (matches albums/artists badges).
- All gated on plex_settings enabled (Plex OFF = byte-for-byte local-only).

Plex track id namespaced into [2^40, 2^41) so it cannot collide with local
track ids (< 2^40) or ephemeral ids (>= 2^48) in the merged Tracks tab — the
string rating_key is preserved in file_path, so playback is unaffected.

cc
2026-06-07 23:43:34 -06:00
vicrodh 2fe7008e23 refactor(plex): extract Plex integration into frontend-agnostic qbz-plex crate
Move the Plex LAN client, hand-rolled XML metadata parsing, and the
SQLite plex_cache.db logic out of src-tauri/src/plex/mod.rs into a new
qbz-plex core crate so it can be shared by every frontend (Tauri, Slint,
TUI) per ADR-006. Plex previously lived entirely in src-tauri (~1715
lines), which blocked the Slint Local Library port from shipping Plex.

The extraction is mechanical: the module was already almost entirely
plain Rust (reqwest + rusqlite + serde + hand-rolled XML). The only
Tauri coupling was a dead plex_play_track command bound to AppState
(unreferenced; the live path is commands_v2::v2_plex_play_track, which
calls plex_resolve_track_media + AppState.player itself). That dead fn
and the vestigial #[tauri::command] attributes are dropped.

src-tauri/src/plex/mod.rs becomes a `pub use qbz_plex::*;` re-export so
every existing crate::plex::* call site compiles unchanged. Full Tauri
workspace cargo check stays green; qbz-plex tests pass.

cc
2026-05-28 15:20:23 -06:00