Commit Graph
12 Commits
Author SHA1 Message Date
vicrodh 2e34112868 feat(app): subscription grace clock and offline purge in the shared activation
The 30-day offline grace (D4) was produced by the Qt shell alone, and only
when a login was REJECTED; its purge predicate had no caller in the live
tree, so past the window the CMAF cache was blocked from playing but never
removed (Tauri purged it; Slint recorded the gap). With member accounts now
signing in, the clock has to key on the entitlement, not on a rejection,
and it has to run wherever a session activates.

`qbz_app::subscription_lifecycle` records the verdict from the core's
session (`offline_streaming` clears the clock; a member keeps it running
from its first observation) and purges the offline cache once the window
has passed, through the cache a host registers with
`AppRuntime::set_offline_cache`. `activate_at` runs it for every host (Qt
via `activate`, qbzd directly), a daily ticker per session re-checks it,
and a cache registered after activation is enforced against on
registration. qbzd registers no cache and keeps the verdict. The Qt
producers are gone; `offline_playback_allowed` stays the read gate.

cc
2026-09-02 07:41:09 -06:00
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 63a4f2daf1 feat(qt): the Local Library union, the settings store and the sweep
Three pieces that only mean anything together: rows in the shared cache,
a union that shows them, and settings that decide whether either
happens.

THE UNION. `get_albums_metadata_page` grows a second, INDEPENDENT
attachment. `plex_cache` stays; `remote_cache` is the shared mirror, and
they attach separately on purpose — a missing Plex cache must not cost
the user their Jellyfin rows. `attach_best_effort` is extracted because
the failure mode of a botched ATTACH is a silently local-only library,
and a failure stays non-fatal: refusing to list any albums because one
server is off turns "Jellyfin is unplugged" into "your library is
empty".

The union takes an ENABLED-SOURCES list, not just a path. The mirror
holds every remote source, so the master toggle has to be a filter
rather than a detach: turning Jellyfin off must hide Jellyfin's rows and
leave Subsonic's alone. That predicate is interpolated into SQL, so it
is VALIDATED rather than escaped — only [a-z] survives, an all-invalid
list yields `0` (show nothing, the safe direction), and a test feeds it
`x'; DROP TABLE local_tracks; --`.

The remote group key is `<source>:<album id>`, matching Plex's
`plex:<hash>`. That namespaces two servers using the same album id AND
lets a source claim an album card that arrives with no source word,
which is every card that round-trips through a QML string property —
`recognises` grew that arm and `claim` strips the prefix, because
carrying it inward is how Plex ended up with two album keys only one of
its queries understood (IC-2).

THE SETTINGS. One store for both servers, keyed by a `server` column,
mirroring the shared-cache decision. What each protocol needs differs
and `is_configured` is the one place that knows: Jellyfin needs its
issued token, Subsonic needs a password because it has no session and
re-derives `md5(password + salt)` per request.

Two identifiers are minted once and persisted, and both are
load-bearing. Jellyfin's DeviceId, because re-authenticating under the
same one revokes the previous token — a constant would make two installs
log each other out, a fresh one per launch drops the last run's token.
Subsonic's salt, because rolling it per request makes every cover URL
unique and the artwork cache keys on the URL.

THE SWEEP. In the frontend, where the runtime and the progress UI are,
writing through the cache handle the SOURCE owns so sweep and reads can
never point at different users. Jellyfin gets progress and a delta path
(45.8 s for 4924 rows); Subsonic needs neither (0.81 s for 6678).
`prune_stale` runs only after a COMPLETED full sweep — a delta never
prunes, because it deliberately does not see unchanged rows and would
read every one of them as deleted. `last_sync_at` is stamped only on
success for the same reason.

Verified against the running binary, twice:

  source registry bound: local_tracks=617 plex=connected
    ephemeral_store=yes remote=none configured
  jellyfin: minted a device id for this install
  subsonic: minted a salt for this install

...and on the second launch, zero re-mints and the same values on disk —
which is the property that keeps a Jellyfin session alive.

qbz-qt 428/428, qbz-app 224/224, qbz-library 100/100, qbz-source 88/88.
Release build clean, zero warnings.

cc
2026-08-20 09:20:07 -06:00
François Laignel a01097708e qbzd: install the rustls process-level CryptoProvider
The rustls crypto provider was not installed for `qbzd`, which caused the
process to panic:

> thread 'tokio-rt-worker' (15737) panicked at [...]/rustls-0.23.37/src/crypto/mod.rs:249:14:
>
> Could not automatically determine the process-level CryptoProvider from
> Rustls crate features.
> Call CryptoProvider::install_default() before this point to select a
> provider manually, or make sure exactly one of the 'aws-lc-rs' and 'ring'
> features is enabled.
> See the documentation of the CryptoProvider type for more information.

The function `ensure_crypto_provider()` was defined in the `qbz-cast` crate
which is obviously not a dependency of `qbzd`.

This commit moves `ensure_crypto_provider()` to `qbz-app`, calls it from `qbzd`
and updates `qbz` accordingly.
2026-07-26 21:12:44 +02:00
vicrodh 0b7cd6c5e3 feat(qbz-app): settings bundle engine with importer-side classification
Export/import CLI on qbzd; requirements 4 and 5 complete with zero desktop builds.

cc
2026-07-14 19:44:01 -06:00
vicrodh edae9d2463 feat(app): shared offline-mode engine with connectivity actor and grace gate
New qbz_app::offline_mode (frontend-agnostic, ADR-006): three-state mode
machine (Online / RealOffline session-scoped / InducedOffline persisted),
Tauri-compatible offline_settings.db store with the issue #279
stream_first_track snapshot, and a layered connectivity actor (default-route
signal on Linux, audio liveness window, IP-literal + vendor-diverse strict
probes, asymmetric hysteresis with confirmation burst, captive-portal and
suspend/resume handling).

qbz-qobuz gains the process-wide offline_gate and ApiError::OfflineMode (the
single Qobuz choke point; HTTP funnel wiring lands next).
SubscriptionStateStore gains offline_playback_allowed: binary 30-day-grace
playback gate for the offline cache, never a preview-degradation path.

cc
2026-06-09 19:48:13 -06:00
vicrodh f907d9d984 refactor(qconnect): move device_uuid persistence to qbz-app (slice 1)
Relocate the persistent QConnect device-identity resolution out of the
Tauri adapter into the frontend-agnostic qbz-app crate
(qbz-app/src/qconnect_identity.rs) so both the Tauri and a future Slint
frontend resolve the SAME uuid for the same install.

Moved: resolve_qconnect_device_uuid, device_uuid_from_db,
load/persist_device_uuid, qconnect_settings_db_path.

Byte-identical persistence contract preserved: DB path
(<data_dir>/qbz/qconnect_settings.db), the 'device_uuid' key, and the
QBZ_QCONNECT_DEVICE_UUID env override are unchanged, so existing installs
keep their uuid. The Tauri transport.rs::resolve_qconnect_device_uuid is
now a one-line delegate; the device-NAME persistence shares the same DB
file via the qbz-app path helper.

qbz-app gains a uuid dependency (it already had rusqlite + dirs, so no
SQLite dep was added to qconnect-app per R6). The 2 device_uuid tests
move to qbz-app.

cc
2026-06-01 09:03:13 -06:00
vicrodh a213b37807 refactor(qbz-app): add AppRuntime shell facade skeleton
Adds a framework-agnostic composition root for non-Tauri UI shells
(Slint, TUI, headless) as Task 1 of the Slint POC readiness audit
(session-20-slint-poc-readiness-audit).

AppRuntime<A> owns an Arc<QbzCore<A>> plus the runtime state machine,
built with the same recipe as the Tauri CoreBridge but with no Tauri
dependency. Scope is composition and accessors only; per-user store
ownership and session activation are deferred to Task 2.

qbz-app now depends on qbz-core, qbz-player, qbz-audio, and qbz-models.
The Tauri app does not consume this module; it is purely additive.

cc
2026-05-18 07:28:19 -06:00
vicrodh 2e43831800 refactor(qbz-app): add remote control settings store
cx
2026-05-15 14:17:35 -06:00
vicrodh 61d5715a22 refactor(qbz-app): add preference stores
cx
2026-05-14 19:48:54 -06:00
vicrodh 800447d33c refactor(qbz-app): add settings stores
cx
2026-05-14 17:08:45 -06:00
vicrodh 4f008cd219 refactor(qbz-app): scaffold app crate 2026-05-14 11:40:19 -06:00