Commit Graph
100 Commits
Author SHA1 Message Date
vicrodh 9b3b52cf9c fix(qconnect): preserve controller state across renderer handoffs
Keep queue reads from starving controls without weakening real queue-write serialization. Restore official proto3 zero defaults and next/previous loop, shuffle and autoplay semantics.

Project controls from the active peer state and target exact upcoming queue occurrences without optimistic cursor changes or local playback fallback. Cover takeover, seek, mute, wire decoding and manual skip regressions in the CI gate.

cx
2026-09-05 18:37:07 -06:00
vicrodh db84e733d6 fix(log): compact consecutive repeated diagnostics
Group identical redacted diagnostics across stderr, file and the live ring. Flush repeat summaries on visible transitions, exports and shutdown while keeping periodic counts for sustained bursts.

cx
2026-09-05 18:36:51 -06:00
vicrodh 7ec9788728 merge: integrate final Qt polish
cx
2026-09-05 14:39:16 -06:00
vicrodh 93ba7584b0 fix(qt): polish purchases queue and search loading
cx
2026-09-05 14:39:07 -06:00
vicrodh 9cbe58c7d8 merge: integrate purchases local copy fixes
cx
2026-09-05 12:11:07 -06:00
vicrodh f01950571d fix(purchases): restore local copy and download semantics
Restore downward stream-format downloads while keeping purchased masters on the download grant. Make Purchases playback prefer the highest complete local copy, repair duplicate copy inventory, and move copy management into a bounded modal with safe library and record actions.

cx
2026-09-05 12:10:55 -06:00
vicrodh 92c87d190f merge: integrate SACD raw and DST ISO support
cx
2026-09-05 10:01:37 -06:00
vicrodh 4bb4982312 feat(sacd): support raw and DST-compressed ISO playback
Parse resilient SACD images, decode DST, scan ISO releases into the local library, and keep explicit opens ephemeral.

Ship decoder notices across distribution formats and align DSD album filtering and version selection.

cx
2026-09-05 10:00:24 -06:00
vicrodh b4c706b828 merge: integrate distribution and artist library release work
cx
2026-09-04 14:21:20 -06:00
vicrodh c7cee95ac2 merge: integrate artist library fixes into release branch
cx
2026-09-04 14:04:10 -06:00
vicrodh 2fdf8878dc i18n: add the artist-page and Qobuz-links strings to all eight locales
"Play random", "Play selected", "Cancel selection", "QOBUZ LINKS", "Open
Qobuz links in QBZ" and the toggle's description. msgfmt --check clean.

cc
2026-09-04 13:10:42 -06:00
vicrodh 4ac6b4006e feat(integrations): make the qobuzapp:// handler a setting
Claiming the official client's `qobuzapp://` scheme was a side effect
of installing. On Linux that displaces nobody; on macOS and Windows the
official client registers the same scheme, and on Windows — where QBZ
is only starting to be adopted — taking the browser's links over at
install time is not a reasonable default. `qbz://` (our own scheme)
stays registered unconditionally.

Settings > Integrations > "Open Qobuz links in QBZ" (link_handler_qt,
pref `qobuz_links_enabled` beside the other Integrations opt-ins),
default on for Linux/macOS and OFF on Windows. The registration moves
only when the toggle is flipped; the one startup reconciliation is
conservative — a claim pointing at this executable while the setting is
off (the 2.0.x MSI wrote one) is dropped, nothing is ever claimed on its
own.

Windows writes/removes HKCU\Software\Classes\qobuzapp (deleting only a
tree whose command names this exe); macOS uses Launch Services and hands
the default back to the first other handler on off; Linux runs xdg-mime
on. The MSI no longer registers qobuzapp and the workflow's protocol
checks cover qbz only. The Windows and macOS arms build on their
platform gates, not on this box.

cc
2026-09-04 13:10:42 -06:00
vicrodh 6d07dfdf4d feat(artist): sort for the in-library albums and a split play button on every album section
The "In library" Albums section gains the release buckets' sort select
(same five keys, persisted by artist_prefs under the pseudo release type
"library"; rows sorted in QML off the document's seed). Every album
section — each catalog bucket and the library grid — gains a split
button right of its sort (controls/QbzSplitButton.qml, sized like the
sm select): the body plays the whole section in its sort order as one
queue replacing the current one (`QbzPlayer.playAlbums` ->
`playback_qt::play_albums`, each album resolved through the single-album
path, failures skipped), the chevron offers Play random (one album of
the section) and Play selected, which arms a per-section select mode:
the cards show their check bottom-right inside the art
(`AlbumCard.selectMarkBottom`), the button reads "Play selected (n)" and
plays the checked albums in sort order, and the menu swaps that row for
Cancel selection. One section is armed at a time; changing artist or
tab disarms. No queue options on purpose.

cc
2026-09-04 13:10:42 -06:00
vicrodh c85fbac4c7 fix(artist): keep the jump-nav mounted in library mode and park the filters past the magnifier
With both source switches on over an artist whose purchases are not
hearted, the library tab's section set was empty, the strip unmounted,
and the switches — which live in the strip — went with it, leaving no
way to reset the filter. "About" (the strip's back-to-top) is now always
present in library mode, landing on the page top since the library
column carries no bio anchor; the active tab on a mode switch is still
the first real section.

The trailing slot moves to the far right of the bar, past the search
toggle: the field grows LEFT over the tabs when it opens, so controls
left of the magnifier were covered while typing.

cc
2026-09-04 11:51:59 -06:00
vicrodh 32109e4ca4 ci(release): require RPM repository dispatch
cx
2026-09-04 10:48:12 -06:00
vicrodh 3bce452f6d fix(packaging): prevent qbz and qbzd file conflicts
cx
2026-09-04 10:45:52 -06:00
vicrodh 89b621264e fix(artist): in-library tab queues its own list, gets a Tracks section and a mode-aware jump-nav (#737)
A track clicked under "In library" played the artist's POPULAR queue:
every row of the page went through `QbzPlayer.playArtistTrack`, whose
queue is TOP_QUEUE and which starts that list at row 0 for any id it
does not hold — the exact symptom in #737, including "it works only when
the song is also popular". The library rows now play through
`QbzLibrary.libraryPlayVisible` with the tab's full filtered list, the
source-aware seam LibraryView uses; `PopularTrackRow` takes a
`playHandler` instead of knowing which tab it is in.

The tab itself becomes a first-class view over `artist.libraryItems`:

- Tracks section modelled on Popular Tracks — header action cluster
  (Play all, select mode, the shared ⋯ menu re-pointed at the library
  list), 10-row preview built from a numeric model, Load more / View
  less opening the whole list.
- JUMP TO follows the mode: in library only the Tracks / Albums sections
  that have rows, resolving anchors against the library column (the
  catalog set stayed up before and "Popular Tracks" scrolled to a section
  that was not drawn).
- Purchases / favourites switches float in the strip while the tab is
  active, with the Library > Albums/Tracks semantics (neither = union,
  one = that source, both = intersection) and page-local state.
- A cleared heart fades the row (purchases stay); the settle republishes
  the document and the row leaves. When the last row goes the page
  returns to the catalog instead of an empty tab.
- Selection and bulk actions run over the active mode's lists; a mode
  switch clears the selection and re-arms the warm from cache.

The view no longer parses `QbzLibrary.libraryJson` per binding.

cc
2026-09-04 10:41:52 -06:00
vicrodh 010b1da8d8 feat(qt): warmLibrary invokable and a trailing slot on the jump-nav bar
`QbzArtist.warmLibrary` lets the artist page's "In library" toggle re-arm
the Library feed warm (a fetch only if the session never loaded it, a
cache repaint otherwise). `QbzJumpNavBar.trailing` is a host-filled slot
between the tab strip and the magnifier, sized by its children and
costing nothing when empty, so a page can float mode-specific controls
in the strip without reflowing the tabs.

cc
2026-09-04 10:41:51 -06:00
vicrodh 40df0d83be feat(library): keep the artist page's in-library rows live
The artist page's "In library" count was a snapshot of the Library feed
taken when the document was built, over a feed that only loads when the
Library view is opened and that never gains a row for a favourite created
afterwards. On a cold session, or after an account switch, every artist
opened with an invisible toggle; hearting an album on the page moved
nothing until the next session.

- library_qt: `artist_library_items` computes the tab's rows with the
  Library > Albums/Tracks rule (favourites ∪ purchases, one row per
  entity, purchase row representative, `is_favorite` = also hearted);
  local/Plex and `following` rows never enter. A track is a member
  through its performer OR its album's main artist (new
  `FeedItem::album_artist_id`, fed by the new `AlbumSummary::artist`,
  which the wire carries — search-results capture).
- library_qt: `reconcile_qobuz_favorite` inserts the row of a freshly
  hearted track/album (one `/track/get` or `/album/get`, mapped by the
  same producers the full load uses) and drops the `favorites` rows of a
  cleared one, keeping a purchase row with its heart cleared.
- artist_qt: the rows ride inside the artist document (`libraryItems`),
  and `refresh_library_items` / `apply_favorite_change` republish them
  through `publish_patch` when the feed lands or a heart settles. A
  debug-level log prints the membership split for the open artist.
- main: `settle_favorite` is the one settle point (reconcile, fan out,
  republish on membership change) for the Library and queue-panel
  hearts; `open_artist` warms the feed in the background; `reload_library`
  hands the open page its rows; `warm_library` serves the toggle from
  cache.

Tests: membership by performer or album artist, exclusion of local /
following, dedup on the purchase row, and the `top_queue` contract that
a foreign id starts the popular list at row 0 (the #737 defect class).

cc
2026-09-04 10:41:51 -06:00
vicrodh 49227b49e8 ci(release): harden remaining artifact workflows
cx
2026-09-04 09:16:21 -06:00
vicrodh 3705c0592c fix(packaging): update Nix package for Qt frontend
cx
2026-09-04 09:16:15 -06:00
vicrodh a9a849c1ec fix(packaging): harden Snap release inputs
cx
2026-09-04 09:16:10 -06:00
vicrodh 41fb509f09 fix(packaging): align Flatpak permissions and assets
cx
2026-09-04 09:16:04 -06:00
vicrodh 0d1cdb5e89 feat(packaging): complete Linux release assets
cx
2026-09-04 09:06:02 -06:00
vicrodh ad9378e1d1 feat(packaging): add complete Gentoo package set
cx
2026-09-04 09:05:19 -06:00
vicrodh 56059e6814 feat(packaging): add complete AUR package set
cx
2026-09-04 08:27:17 -06:00
vicrodh c85908e262 docs(qbzd): document non-systemd services
cx
2026-09-04 08:27:05 -06:00
vicrodh 30912962d3 feat(library): refine track source controls
Preserve the remaining queue when shuffle is disabled, expose clear transport state tooltips, retain Library source filters for the session, and add independent track-artwork preferences for Queue and Library.

cx
2026-09-03 14:53:22 -06:00
vicrodh f833a5a401 fix(tooling): audit the active Qt tree by default
cx
2026-09-03 09:34:31 -06:00
vicrodh fb928df2ff fix(library): retain source filters for session
cx
2026-09-03 09:07:22 -06:00
vicrodh 23941a0958 feat(purchases): refine purchased album presentation
cx
2026-09-03 09:07:18 -06:00
vicrodh 38263d2bc0 fix(playback): fence late purchase materialization
cx
2026-09-03 05:52:13 -06:00
vicrodh 639fdc082d feat(playback): honor album purchase preference
cx
2026-09-03 05:41:22 -06:00
vicrodh 8566b68411 feat(purchases): persist copy coverage manifests
cx
2026-09-03 05:18:22 -06:00
vicrodh c0a37e6b87 feat(purchases): manage independent download copies
cx
2026-09-03 05:13:05 -06:00
vicrodh 59bd3ab6b2 feat(purchases): add copy-aware download receipts
cx
2026-09-03 04:36:41 -06:00
vicrodh 561b3c25c7 feat(purchases): gate album actions by entitlement
cx
2026-09-03 04:32:23 -06:00
vicrodh 247c0403b7 feat(library): type folder registration and queue scans
cx
2026-09-03 03:58:44 -06:00
vicrodh 03e55684e0 feat(library): persist distinct purchase copies
cx
2026-09-03 03:45:21 -06:00
vicrodh 8eb2afe4db fix(library): preserve unreachable purchase records
cx
2026-09-03 03:40:14 -06:00
vicrodh bb5172812a merge: repair test Track fixtures
cx
2026-09-03 03:05:32 -06:00
vicrodh 7f5e28ccac test(models): update Track availability fixtures
cx
2026-09-03 03:05:20 -06:00
vicrodh de82962ed2 merge: local library fixes and direct DSD seek
cx
2026-09-03 02:33:01 -06:00
vicrodh 8aab112afe feat(dsd): support seeking in direct playback
Add random access for DSF, DFF, and SACD demuxers, replace live DoP/native sources without releasing ALSA, and expose seek through Qt, MPRIS, and the daemon API.

cx
2026-09-03 02:32:52 -06:00
vicrodh ea92f07b66 fix(local-library): restore responsive source-aware browsing
Show SACD images in folder grouping, keep track details responsive, avoid repeated album reload work, and exclude disabled or unconfigured media servers from filters and sync.

cx
2026-09-03 02:32:37 -06:00
vicrodh 6afcfad4f4 merge: local mp3 duration, dlna end-stall, dsd chip, sacd scan and memory trims
cc
2026-09-02 20:35:37 -06:00
vicrodh 478a80a5d7 perf(qt): cap malloc arenas, eight tokio workers, idle L1 trim
Measured on a 24-core box: 52 threads held 82 glibc malloc arenas, 257 MB
of fragmented anonymous mappings at idle, and the 400 MB in-memory audio
cache kept every played track until another evicted it. main now calls
mallopt(M_ARENA_MAX, 2) before the first thread (an explicit
MALLOC_ARENA_MAX wins), the tokio runtime runs eight workers instead of
one per core, and the playback poll moves the L1 audio cache to the L2
disk cache after 300 s with nothing audible locally, remotely or on a
cast. Idle RSS went from 728 MB to 537 MB; the trim verified at exactly
300 s after a pause.

cc
2026-09-02 20:34:59 -06:00
vicrodh 024cc121a9 feat(library): adopt SACD images found by the folder scan
The scan never treated an .iso as audio; images only entered the library
through Open > disc image. Each root's scan now walks its .iso files,
asks qbz_disc::sacd::is_sacd_image for the SACDMTOC signature (PVD plus
Master TOC sector, four 2 KB reads at most, no directory parsing) so a DVD
or data ISO leaves silently, skips images already imported with the same
size and mtime without any I/O, and adopts the rest through the
generation-based import. The row builder moved from sacd_qt into
qbz_library::sacd_scan and the manual open is now its client, with the
translated fallback labels. Parser and import rejections are scan errors.

cc
2026-09-02 20:34:59 -06:00
vicrodh 1f54fd9fcb fix(qt): Local Library header gear opens Settings > Local Library
It landed on the Audio root; the row now sets section 4 through the same
invokable the settings sub-navigation uses before navigating.

cc
2026-09-02 20:34:59 -06:00
vicrodh baf0dc2bb9 feat(local-library): DSD quality chip
DSD gets its own Quality chip and Hi-Res goes back to 24-bit PCM only, in
the MediaFilter, the native catalog SQL (tracks CASE and the materialized
album quality_tier) and the legacy QML filters. A Format chip would be the
same predicate, so there is none. Schema version 6 re-derives the album
tiers that were materialized as hires.

cc
2026-09-02 20:34:59 -06:00
vicrodh 35ff1818f6 feat(qt): say "not yet available" on pre-release tracks
/album/get sends streamable_at and release_date_stream per track: a future
value marks a pre-release, null a pulled track. Track carries both fields
and Track::is_upcoming applies the one rule (future streamable_at wins,
then a future release date); the album, playlist, search and label rows
publish qobuzUpcoming and the unavailable glyph reads "This track is not
yet available" instead of "no longer". Artist page rows carry no dates and
keep the generic copy. New msgid in the seven translated catalogs.

cc
2026-09-02 20:34:58 -06:00
vicrodh feb2c9de12 fix(qt): pin the quality stamp tooltip to its text
The now-playing stamp bubble came out wider than its lines with the text
offset, and never reproduced in isolation. The three stamp ToolTips now
bind width/height straight to the content's implicit size with zero
paddings, insets and margins, leaving nothing for the style to add.

cc
2026-09-02 20:34:58 -06:00
vicrodh ca0aa0e6bf fix(cast): survive a DLNA renderer that never reports STOPPED
A KEF LSX reached the end of every track and never advanced (#733); its
poll answers were swallowed, so an INFO log could not tell a mute renderer
from one that never reports STOPPED. The DLNA poll now logs a failed
position read, GetTransportInfo faults degrade to the last transport
command instead of failing the whole read, and a RelTime frozen inside the
end guard for five polls without STOPPED counts as end-of-track. A user
pause never counts. Pure helper with unit tests.

cc
2026-09-02 20:34:58 -06:00
vicrodh 7a311e504f fix(player): derive the duration of MP3s without a Xing header
rodio's Decoder::new leaves byte_len unset and the source non-seekable, so
symphonia's MP3 demuxer never estimates the duration of a file without a
Xing/Info/VBRI frame. The local play path then fell back to duration 0 and
the playback clock stayed clamped at 0 for the whole track, which the Slint
2.0.2 frontend read as a stuck load and restarted every 45 s (#734).

The decoder is now built with the buffer length, and an unknown (0)
duration no longer clamps the position. Regression fixture: a 2 s CBR MP3
written without an Info tag.

cc
2026-09-02 20:34:58 -06:00
vicrodh fb228af653 chore: ignore the session scratchpad directory
cc
2026-09-02 20:34:58 -06:00
vicrodh bef2a8847a fix(discover): honor blacklist and prune stale pins
cx
2026-09-02 16:03:26 -06:00
vicrodh 2a94a1803f merge: settings migration accessibility and scaling
cx
2026-09-02 14:33:33 -06:00
vicrodh 472706f84e fix(settings): harden migration and accessibility
Apply interface scaling through Qt at startup, complete keyboard navigation, move account migration into setup and progress modals, and defer Subsonic sync while the server scans.

cx
2026-09-02 14:33:08 -06:00
vicrodh 015ca48994 merge: integrate ListenBrainz connect-state fix
cx
2026-09-02 10:28:57 -06:00
vicrodh 4d5c15ab82 merge: integrate member mode and account migration
cx
2026-09-02 10:28:50 -06:00
vicrodh c8ad6a5215 fix(settings): serialize ListenBrainz token validation
cx
2026-09-02 10:28:41 -06:00
vicrodh fb4cfd6ed1 merge: fold the stale-track-edge playback fix into member-mode
cc
2026-09-02 08:48:57 -06:00
vicrodh b85a81ee1d fix(playback): ignore the ended track's stale id while its successor loads
After a recovery advance (`recovery: engine empty … poll: advancing to X`)
the poll loop reset `last_track_id` and started the next track, but for
1-2 s (CMAF session + init segment, or a DSD→PCM conversion) the player
kept reporting the id of the track that had just ENDED. The 1 Hz tick took
that as a track change: `sync_current_to_id` rewound the queue cursor one
row and `refresh_now_playing` republished the previous track (now-playing,
MPRIS, notification), then the next tick flipped back — three
notifications per change, the middle one a ghost, intermittent because it
depends on the tick landing in that window. With the cursor rewound, the
gapless stage asked `peek_upcoming(1)` and was handed the track already
playing, so it played twice (smoke 2026-09-02, local DSD row; the queue had
no duplicate).

`stale_after_advance` remembers the ended id at the advance (0 under
repeat-one, where old == new); while the player still reports it and
nothing plays, the tick does its bookkeeping and skips the edge; any other
id or running audio clears it. The gapless stage also refuses a successor
equal to the player's own current track id. No change to the audio
backend.

cc
2026-09-02 08:48:57 -06:00
vicrodh e235cd3e23 test: require the account-migration and portable-blacklist suites in the gate
Same shape as the listen-log presence check: the workspace run already
executes them; the counts keep a renamed module or a dropped cfg from
shrinking coverage without a trace. The workflow calls the script, so the
mirror stays exact; its header names the new check.

cc
2026-09-02 08:25:09 -06:00
vicrodh a39ea8c12d feat(settings): Import / Export section with portable blacklist and account migration
A new top-level Settings section, Import / Export, gathers everything that
moves data in or out of this install:

- SETTINGS PORTABILITY: the settings bundle export moves here from
  Developer, with a note on what the bundle does not hold (favorites,
  playlists, blacklist, purchases).
- BLACKLIST: export the blocked artists and albums as a JSON one user can
  hand to another; import merges additively and never touches the enabled
  flag (qbz_app::settings::blacklist_portable, frontend-agnostic).
- ACCOUNT MIGRATION: create a snapshot while signed in with the old
  account, then migrate it into the account signed in now, with the local
  profile copy and its three opt-in toggles (media servers, scrobblers,
  listening history, all on). The disclaimer states that subscriptions and
  purchases do not move. Progress and results are status lines in the
  settings document; the favorites cache re-warms and the sidebar reloads
  after a run, the local half asks for a restart.

Also: the header shows a translated "Member" for accounts without a
subscription, and the login screen says what a member can and cannot do.
50 new strings in all eight catalogs.

cc
2026-09-02 08:24:02 -06:00
vicrodh 800255ac58 feat(account-migration): snapshot, plan, apply and local-profile copy
A frontend-agnostic crate for moving a Qobuz account's favorites and
playlists into another account, additively (contract
qbz-nix-docs/specs/2026-09-02-account-migration-contract.md §9):

- snapshot: capture the signed-in account (five favorite kinds with
  favorited_at, owned playlists with track ids in order, followed
  playlists) into users/<uid>/account_migration/snapshot-*.json; the same
  capture is the target's live state at migration time.
- plan: the pure delta against the target and the ledger. Same-name
  playlists holding nothing foreign are the same playlist and get the
  missing tracks appended; anything foreign means a different playlist and
  a " (migrated)" copy. Favorites are written oldest first so the target's
  own stamps keep the chronology.
- apply: one favorite per call with pacing and bounded retries, 50-track
  chunks, subscriptions re-followed; every success lands in the ledger
  before the next write, so a run resumes and a second run adds nothing.
- local: copy the old profile directory row by row (library folders and
  scanned tracks on the fast path, playlist folders, settings keyed by
  qobuz_playlist_id remapped through the ledger, pinned items, orders,
  preferences, blacklist; media servers, scrobblers and listening history
  behind options). Never the favorites cache, the subscription clock, the
  session or the purchases.

The writes are the ones the app already issues when a user hearts or
creates a playlist; nothing touches subscriptions, purchases or account
data.

cc
2026-09-02 08:24:01 -06:00
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 2b7d804f09 feat(auth): member accounts are valid sessions; typed entitlements
Qobuz answers `/user/login` with 200 for an account without an active
subscription and the web player signs it in as a "Qobuz member" with its
favorites, playlists, purchases and 30-second previews. QBZ threw that
session away: `parse_login_response` returned `IneligibleUser` whenever
`credential.parameters` was empty (verified 2026-09-02 with a lapsed
account). The gate was ours, not Qobuz's.

The parser now reads the entitlement flags (`lossy/lossless/hires/
hires_purchases/offline/mobile_streaming`) into a typed `Entitlements`
on `UserSession`, labels a member "Member", and keeps `subscription.
end_date`/`offer` as information (the embedded Qobuz fixture has a past
end_date with every flag true: the flags are the verdict, the date is
not). `StreamUrl` gains `sample`, the flag Qobuz sets when it serves a
preview instead of the track. `IneligibleUser` stays in the enum for its
existing matchers; nothing emits it from this path any more. Persisted
pre-member sessions still load (`serde(default)`).

cc
2026-09-02 07:27:38 -06:00
vicrodh 89efde792a feat(qobuz): read-only member-login probe and raw OAuth helpers
Qobuz answers 200 for an account without an active subscription, but the
login parser rejects it (IneligibleUser) before anything can be captured.
Add `exchange_oauth_code` (OAuth step 1, extracted from
`login_with_oauth_code` unchanged), `login_with_token_raw` and
`get_stream_url_raw`, which return status and body verbatim without
parsing or storing a session, and a probe example that signs in through
the same system-browser OAuth flow as the app (or a pasted token), then
prints the redacted login payload, favorites totals, playlists and the
getFileUrl responses at CD and Hi-Res quality. Nothing is written to
Qobuz or to disk.

cc
2026-09-02 07:23:06 -06:00
vicrodh 87f32bc5e5 merge: purchases round — albums visible, DSD entitlement, local playback
Merges bugfix/internal/purchases-albums-tab (17 commits): the blank
Albums tab root cause (on-prefixed QML property, now gated by an audit),
entitlement-driven formats with the streaming ladder below the purchase,
intent=download grants and a DSD-safe streaming writer, album-artist
folders with master cover and goodies, the purchased copy playing under
its Qobuz id, the DSD tier and mark across purchases and the Local
Library, purchases list/detail polish with a genre filter, and active
downloads in the sidebar. Gates: source 95, offline-cache 75+9,
qbz-qt 663, six QML audits; owner smokes on the live purchases account.

cc
2026-09-02 07:22:09 -06:00
vicrodh a5376e9064 feat(shell): active purchase downloads live in the sidebar, as a ring when compact
The Downloads section leaves the nav flyout. With the sidebar open, the
purchases downloading now sit under the Purchases entry, one row per
album with a thin progress bar, each reopening its detail. With the
sidebar collapsed, and in the compact header nav when it is closed, they
fold into one progress ring whose hover bubble lists every album with
its count and percentage. The ring is a Canvas that repaints only when a
publish moves the value — no timer, per the repaint-pulse rule. The
active set now carries done/total/percent per album.

cc
2026-09-02 07:11:03 -06:00
vicrodh db1d36537b feat(purchases): genre filter, zebra rows, and a list that no longer overlaps
The list view's quality column was 110 px for a badge that is 136 px
wide, so the badge ran into the release-date column; it is 150 now, the
album list's width. Both list rows and track rows draw the odd-row
zebra every other list in the app draws. The filter panel gains a Genre
section built from the catalog genre on each purchased album (albums
tab only, transient like group and sort), counted and summarised with
the other filters.

cc
2026-09-02 07:11:03 -06:00
vicrodh 29a6528ec1 feat(quality): DSD wears its mark in the Local Library and MyQBZ too
The local rows, the source-side quality hint and the MyQBZ badge still
tiered a 1-bit file as "hires"; they now say `dsd`, so the same DSD mark
the purchases show reaches every local surface, and the Local Library's
hi-res filter keeps counting DSD as hi-res.

cc
2026-09-02 07:11:03 -06:00
vicrodh 2d7a3ef722 fix(purchases): offer the hi-res streaming rungs only where hi-res streams
The downward streaming ladder read `album.hires` to add 27/7, but the
live DSD album is hires:true with a 16/44.1 stream, so "[FLAC][24-bit,
192kHz]" was offered and the CDN filled a folder by that name with CD
files. Gate the two hi-res rungs on the catalog's `hires_streamable`.

cc
2026-09-02 01:42:01 -06:00
vicrodh 07e683f5ef feat(purchases): detail polish and a Downloads section in the nav flyout
The purchase detail badges what was bought on every row (DSD128 on a
DSD purchase, not the catalog's 24/352.8), marks the formats already on
disk with a check in the dropdown, offers a track filter, links to the
album in the Local Library once its folder is indexed, and gives the
download the accent while Play drops to a plain control of the same
size — on this screen the download is the point.

Downloads are process-wide and survive leaving the page; the nav flyout
now lists the purchases with a download in flight under "Downloads · N"
so the user can find them again, each row reopening its detail.

cc
2026-09-02 01:30:12 -06:00
vicrodh fd44d38cd3 feat(quality): a DSD tier with the DSD mark
A 1-bit master wore the Hi-Res logo and "24-bit / 352.8 kHz" — a stream
Qobuz never serves (nothing above 24/192 streams). Give DSD its own
tier: the official DSD mark, baked light and dark so the monochrome logo
follows the theme, on the mini badge, the full badge and the now-playing
stamp, with the multiple (DSD64 / DSD128) as the detail line. The
now-playing derivation and the delivered-tier evaluation report `dsd`
for a 1-bit stream instead of folding it into hires.

cc
2026-09-02 01:17:48 -06:00
vicrodh e079916aee fix(source): a purchased file badges local, not offline
`qobuz_purchase` rows badged `Offline`, the mark reserved for the
encrypted CMAF downloads only qbz-core's offline tier can open, so the
local source refused a purchased DSF on disk as "a CMAF bundle" and the
album the user had just downloaded and added answered "File not
available — is the drive mounted?". A purchase is a plain DRM-free file:
badge it Local and let the file path play it.

cc
2026-09-02 01:17:48 -06:00
vicrodh 065d63369a feat(purchases): master-size cover and every goodie ride the album download
The Qobuz desktop client leaves `folder.jpg` and `large_cover.jpg` beside
a purchase and fetches its booklet with it; QBZ wrote a 600 px `cover.jpg`
and kept the goodies behind a separate control. Write `large_cover.jpg`
from the `_max` variant next to `cover.jpg`/`back.jpg`, and fetch every
goodie the catalog lists into the album folder as part of "Download all",
counted separately from the tracks as before. The "Download goodies"
control stays as the re-download for a folder that lost them.

cc
2026-09-02 00:18:31 -06:00
vicrodh 554d43cfdf feat(playback): play the purchased copy of a Qobuz track when it is on disk
A downloaded purchase played nothing: Play on the purchase detail (and
every other Qobuz entry) streamed the catalog, so a DSD64 album bought
and saved to disk still came out of the CD stream with a "downgrade"
badge. Resolve the download registry at the audible step instead: the
best registered copy of the track that answers the bounded reachability
probe plays under the SAME Qobuz id (`play_id` = catalog id, so
now-playing, history, scrobbling and QConnect see the track they always
did), `.dsf`/`.dff` through the player's additive DSD ticket and PCM
through the file ticket. The gapless successor takes the same route, so
an album bought as DSD stays DSD across the track edge. A play that asks
for a start offset keeps the stream for a DSD copy (DSD cannot seek), and
anything the copy cannot do falls through to the tier walk unchanged.

Contract: qt-frontend/2026-09-01-purchases-local-playback §6, first
slice (implicit preference: the copy wins; no per-album select yet).

cc
2026-09-02 00:08:46 -06:00
vicrodh 7d302e0c04 feat(purchases): offer the streaming ladder below the purchased quality
The store grants `intent=download` for exactly the entitlement ids
(measured live: a DSD64 purchase gets 55 and nothing else), so a DSD
album could no longer be saved as a CD FLAC the way the Slint and Tauri
builds allowed. Those builds fetched every download through the
STREAMING grant, which serves any streamable album, bought or not.

Keep that route, but only downward: the menu lists the entitlement
first, then the catalog-synthesized 27/7/6/5 options ranked below the
purchase and not already owned, each marked `streaming` and fetched
through `intent=stream`. Nothing above the purchase is offered, and a
purchased id always goes through the purchase grant.

cc
2026-09-01 23:59:37 -06:00
vicrodh e41235f791 feat(purchases): readable dates, a release-date column and sort, column labels
The list printed the purchase date as "9/1/26" and nothing else, so a
row said neither when the record came out nor, at a glance, what the
number meant. Rows now carry the catalog's original release date next to
the purchase date, both as "Sep 1, 2026", under a column-label strip
like the album list's, and the toolbar sorts by release date (`released`)
beside the four existing keys.

cc
2026-09-01 23:59:36 -06:00
vicrodh 0756e96940 fix(purchases): draw the shared quality badge instead of the raw tier word
The purchase card and list row printed `qualityTier` as text ("hires")
under the artist, the one surface in the app that did. Use the same
icon-only QualityMini on the card and QualityBadgeFull on the row that
every other album card and list row draws, with the exact quality
(DSD64/DSD128, 24-bit / 96 kHz) in the badge's tooltip and detail line.

cc
2026-09-01 23:44:43 -06:00
vicrodh 66f49467f2 fix(purchases): file a downloaded album under its album artist
The download path used the track performer as the artist folder — the
reference did the same — so a Various-Artists compilation landed as one
folder per performer with one track each (fourteen for "Audiophile
Analog Collection Vol.3", smoke 2026-09-01) and the cover only beside
the first track. Use the album artist from the download context for the
folder; the ARTIST tag keeps the performer.

cc
2026-09-01 23:42:14 -06:00
vicrodh d5f4ced66f feat(purchases): entitlement-driven formats and a DSD-safe download path
The purchases listing carries `downloadable_format_ids` per album, and
it is the only honest source for the download menu: a DSD64 purchase
streams as 16/44.1 (`hires:false`), so the catalog-derived ladder
(27/7/6/5) offered a CD FLAC the account never bought and could not
reach the DSD it did. Model the field (plus `hires_purchased`), build
the menu from it highest quality first, and keep the synthesis only as
the fallback for a listing without the field. Fixture from the first
populated capture, redacted.

The download grant is a distinct signed operation: `intent` is part of
the `getFileUrl` preimage, and a DSD entitlement answers 400 to
`intentstream` and 200 to `intentdownload` (measured live). Sign the
purchase grant with `download`, stream the body straight into the
`.part` instead of holding a 650 MB master in memory, take the file
extension from the served container (`audio/x-dsf` -> `.dsf`), verify
its magic before the rename, and leave neither `.part` nor registry row
behind on a truncated or foreign body. Tags stay FLAC/MP3-only.

The tab counters now come from `getUserPurchases?type=...&limit=1`;
`getUserPurchasesIds` counts album tracks as tracks (34 against 0 on
the smoke account). The list card and detail header badge what was
bought: 55/56 ride the hires tier with a DSD64/DSD128 detail.

cc
2026-09-01 23:40:33 -06:00
vicrodh 3c8ab80be5 refactor(purchases): drop the blind-mode disclaimer modal
Purchases is now exercised against an account with real purchases, so
the one-time "built in blind mode" notice no longer describes the
feature. Remove the modal, its controller state, the bridge invokable,
the persisted pref and the three strings from the seven catalogs that
carried them.

cc
2026-09-01 23:40:33 -06:00
vicrodh 1cb988954e fix(qt): rename on-prefixed QML properties and gate the pattern
A property named `on<Upper>` is filed as a signal handler as soon as a
sibling member `<upper>` exists: its initializer never binds, it sits at
the type default, and nothing warns. `PurchasesView.qml` declared
`onAlbums` next to `albums`, so the Albums tab bound `[]` once and never
re-evaluated, which blanked Purchases for every account with real
purchases. Reproduced standalone with the `qml` runtime: the identical
expression under another name reads `true`.

Rename it to `albumsTab` and the four other `on<Upper>` properties
(`collectionsTab`, `surfaceTint`, `accentFloor`, `litColor`) so the rule
can be exact, and add `qml_on_prefixed_property_audit.py` to the four
QML audit sites (qt-run, cargo-test, test-crates, qt-gate-windows).

cc
2026-09-01 23:40:33 -06:00
vicrodh 7627ac7a36 docs(release): prepare 2.1.0 notes and packaging
cx
2026-09-01 20:12:53 -06:00
vicrodh db1ac051f2 fix(local-catalog): prune obsolete generations
cx
2026-09-01 20:12:36 -06:00
vicrodh 840a9805b5 merge: record subsumed qconnect queue fixes
The final release polish contains and extends the cold takeback, partial queue admission, and NCSI work from this branch.

cx
2026-09-01 17:25:04 -06:00
vicrodh 159a964c2e merge: final release polish
cx
2026-09-01 17:23:55 -06:00
vicrodh f6e1c4e023 fix(qconnect): harden queue ownership handoffs
Preserve local playback when it is authoritative, make renderer conflicts explicit and persist the selected policy. Filter unsupported queue rows, restore portable Qobuz identities after disconnect, and fence takeover commands to avoid stale queue and playback loops.

cx
2026-09-01 17:23:14 -06:00
vicrodh 74b0c8e512 fix(qt): correct immersive visuals on Windows
cx
2026-09-01 17:22:50 -06:00
vicrodh d563d9aa63 fix(qt): align the playlist picker footer
cx
2026-09-01 17:22:45 -06:00
vicrodh bb1dfe2d19 fix(connectivity): use the defined Microsoft NCSI probe
cx
2026-09-01 17:22:38 -06:00
vicrodh 53a77791c6 fix(windows): zero the safe-area paddings on Windows too
The Qt 6.9 SafeArea padding neutralizer was gated to macOS in eada3bd12,
which was correct THEN — the Windows ExpandedClientAreaHint arm did not
exist yet. 415cba57d added it without widening the gate, so on Windows
the caption-height safe-area margin auto-padded the contentItem down and
the strip rendered with the window's #1a1a1a background: a dark grey bar
above the QBZ header, pushing the drawn window controls down with it.
The Loader stays imperative, so Linux/Qt 6.8 still never instantiates it.

cc
2026-09-01 07:45:45 -06:00
vicrodh 0b5ce47f22 merge: immersive visual cleanup, ambient light-theme veil, album add-to-playlist everywhere, tray position and notification fixes
cc
2026-08-31 17:05:27 -06:00
vicrodh e2e414b56b fix(qt): wrap the split-card title at full size; themed badge on the song card
The cinematic split card's shared shrink-to-fit typeScale is retired: a
long title now word-wraps at its nominal size instead of scaling the
whole metadata block down, and the album/artist lines elide instead of
dictating a reduction. The bottom-right song card's badge also drops the
overAmbient chip — that card only mounts over dark surfaces and the dark
backdrop read as a bigger badge; the themed look was already right.

cc
2026-08-31 16:51:05 -06:00
vicrodh f6574cdb93 fix(linux): present each track notification under a fresh portal id
The track toast published under ONE stable portal id, replacing the
previous notification in place — and Plasma never re-presents a same-id
replacement as a banner, show-as-new hint included (measured on-desktop:
a same-id replacement with the hint produced no popup while a fresh id
bannered every time). So only the first track after boot could ever
toast. Each publish now uses track-now-playing-<generation> and removes
the previously published id first, keeping the no-pileup lifecycle;
withdraw (stop, opt-out, shutdown) removes whichever id was last
published.

cc
2026-08-31 16:38:29 -06:00
vicrodh f64b8480de fix(qt): restore the window position after un-hiding from the tray
Qt keeps the window SIZE across hide()/show(), but the remap is a fresh
mapping and KWin's placement policy re-placed it at the horizontal centre
of the screen. The persisted geometry cannot help (it carries only
width/height + flags), so the position is captured at hideToTray and
re-applied around showFromTray — seeded before the show and re-asserted
once 50ms after the map, since a placement WM can overwrite the hint on
the map itself. Windowed frames only: maximized/fullscreen restore via
their latches and an iconified window's x/y are not trustworthy. The
miniplayer enter/exit pair rides the same verbs and inherits the fix.

cc
2026-08-31 16:06:52 -06:00
vicrodh e41ec15e0e feat(qt): Add to playlist for local albums everywhere
Playlists can be fully local or mixed, so hiding the row on local albums
was wrong — the resolution logic already existed end to end. The quick
view drops its !localAlbum gates (album and track menus) and a new
'playlist' arm hands the overlay's row snapshot to the picker in LOCAL
MODE, fired before close so the generation guard cannot refuse it. The
local album cards and list rows reuse the existing bulk 'album' scope
(group key -> rows -> picker). My QBZ's heterogeneous cells stay out via
an explicit opt-in, since their local ids are container items the bulk
seam cannot resolve.

cc
2026-08-31 16:00:11 -06:00