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
This commit is contained in:
vicrodh committed 2026-08-13 23:10:32 -06:00
1 parent 3b03682e09
commit c2944db973
20 files changed
+1431 -17

No files matched your search

+25 -1
View File
@@ -324,8 +324,32 @@ fn open_plex_cache_db() -> Result<Connection, String> {
Ok(conn)
}
/// The default-timeout Plex client, built ONCE.
///
/// It used to be rebuilt on every call, and a fresh `reqwest::Client` is not
/// free: new connection pool (so no keep-alive to reuse), fresh TLS handshake,
/// and a fresh DNS resolution. On Plex that last one is the expensive part —
/// LAN servers are addressed as `https://<ip>.<hash>.plex.direct:32400`, and
/// `*.plex.direct` resolves through Plex's own nameservers, i.e. over the
/// INTERNET. So every play paid an internet DNS round trip plus a TLS handshake
/// before the first byte of a file that was sitting on the local network.
///
/// That is the shape the owner reported (2026-08-13): playing a Plex track took
/// LONGER than streaming a Qobuz track from the internet, which no amount of
/// LAN transfer time can explain — but a per-call client can, because the Qobuz
/// path reuses one long-lived client and this one threw its away every time.
///
/// Cached here rather than at the call sites so every Plex entry point gets it.
/// `build_plex_client_with_timeout` stays uncached: its callers pick a short
/// bound on purpose (quality hydration), and those are genuinely different
/// clients.
static PLEX_CLIENT: std::sync::OnceLock<Result<reqwest::Client, String>> =
std::sync::OnceLock::new();
fn build_plex_client() -> Result<reqwest::Client, String> {
build_plex_client_with_timeout(Duration::from_secs(120))
PLEX_CLIENT
.get_or_init(|| build_plex_client_with_timeout(Duration::from_secs(120)))
.clone()
}
/// Build the LAN Plex client with a caller-chosen request timeout. Quality