Commit Graph
26 Commits
Author SHA1 Message Date
vicrodh 060395885b fix(release-watch): backfill album.artist from artists[0] on getNewReleases
AlbumDto from Qobuz carries both legacy singular 'artist' and current
'artists[]' array. /favorite/getNewReleases omits the singular field
and only populates the array, so our Album struct (which has a default
serde Artist) deserialized an empty name — every Release Watch card on
Home read 'Unknown Artist'.

Pre-process the JSON response items before deserializing: when 'artist'
is absent or null, copy artists[0] into it. Keeps the rest of the
album-list plumbing untouched.

cc
2026-04-14 10:40:42 -06:00
vicrodh 37c64d252c fix(api): project /favorite/getNewReleases envelope to SearchResultsPage
The endpoint returns V2GenericListDto<AlbumDto> which is just
{has_more: bool, items: [...]} — no total/offset/limit. Our caller
wants SearchResultsPage<Album> to stay compatible with the rest of
the album-list flow, so map items through directly and synthesise
total from has_more + items.len() + the offset we asked for.

Endpoint itself returns 200 OK with a real feed now (confirmed in
logs with Godsmack etc. on the test account).

cc
2026-04-14 08:01:30 -06:00
vicrodh ce375008ab fix(api): Release Watch lives at /favorite/getNewReleases?type=artists
The inferred OpenAPI spec
(qbz-nix-docs/qobuz-api-inferred-openapi-v9.7.0.3.yaml, source:
x20/b.java) documents the endpoint as /favorite/getNewReleases with
three required query params:

  @GET favorite/getNewReleases
  - type: String   (artists | labels | awards — matches the three
                    tabs in the mobile UI)
  - limit: Integer
  - offset: Integer

My earlier attempts at /albums/releaseWatch were chasing the Android
route-table entry for the Release Watch *screen*, not the HTTP call
that screen makes. Different layer.

Also removes my made-up speculation about mobile app_id gating — the
/favorite/* endpoints are standard web-API calls the existing client
already knows how to sign (via authenticated_headers + /0.2/ base URL).

v2_get_release_watch now takes an optional releaseType arg defaulting
to 'artists' so callers can switch tabs later without another command.

cc
2026-04-14 07:47:42 -06:00
vicrodh 541c3613c9 chore(api): promote release-watch API logs to info for diagnosability
The debug-level log lines for status + response keys + raw body never
printed at default log level, leaving zero observability on the call.
Bumped to info and added body capture so a non-success status or a
deserialize mismatch dumps the first 400-600 chars, which will tell
us whether the endpoint returned {albums:{items:[]}}, {items:[]}, an
auth error, or something else entirely.

cc
2026-04-14 07:26:59 -06:00
vicrodh b0e724d428 fix(api): call /albums/releaseWatch as REST, not signed RPC
Third try's the charm. The previous attempt called the endpoint via
signed_get_auth (RPC-style with request_sig + request_ts), which
yielded 'no Service found with those values' — the RPC dispatcher
simply doesn't know this path.

The /albums/* plural namespace (releaseWatch, favoriteAlbums,
purchasedAlbums, similarTo, essential, artistAlbums, discoverAlbums)
is REST-style, not RPC. QBZ already uses the same pattern for the
/radio/* endpoints in src-tauri/src/api/client.rs: plain
.get(url).headers(authenticated_headers()).query(...) with X-App-Id
and X-User-Auth-Token, and no signing. Same pattern here.

Also reverted the release_watch container addition to
DiscoverContainers — /discover/index does not return it alongside
the editorial sections, confirmed by the empty result when reading
from there even with follows set.

Radio and Artist Spotlight work in QBZ via this same REST + auth
headers pattern despite being 'mobile' features; Release Watch
should too.

cc
2026-04-13 23:23:07 -06:00
vicrodh 60e0861e1d refactor(discover): read Release Watch from /discover/index response
The initial implementation added a dedicated get_release_watch client
method calling /albums/releaseWatch, but that path is only routed on
the mobile API; the web API (api.json/0.2) that qbz-qobuz speaks to
returned 'no Service found with those values'.

The correct source is already available: /discover/index — which we
were already calling for newReleases/pressAwards/qobuzissimes/etc —
also returns a release_watch container when the user has follows. We
were silently dropping it because DiscoverContainers didn't declare
the field.

Now:
- DiscoverContainers (Rust + TS mirror) declares the release_watch
  field with a serde alias that accepts both camelCase and snake_case
  spellings, so future naming churn upstream is tolerated
- HomeView reads it in fetchAllDiscoverData next to the other editorial
  sections (one less network round-trip)
- ForYouTab calls v2_get_discover_index and extracts only the
  release_watch container
- Reverted the custom /albums/releaseWatch endpoint, client method,
  core passthrough, bridge wrapper and v2_get_release_watch command

Section identity (homeSettingsStore id, i18n keys, render blocks)
is unchanged.

cc
2026-04-13 22:49:34 -06:00
vicrodh 9945e93e0c fix(api): tolerate unknown release watch response shape
First iteration hard-parsed the /albums/releaseWatch response as
SearchResultsPage<Album>, which failed with 'missing field items'
because the wrapper key (albums / releaseWatch / release_watch /
bare items) differs across Qobuz API deployments and is not
publicly documented.

Now walks a candidate list of wrapper keys, accepts both
{items: [...]} objects and bare arrays, falls back to root-level
parsing, and finally logs the response shape + returns an empty
page rather than propagating an error that would kill the whole
Discover screen.

cc
2026-04-13 22:37:28 -06:00
vicrodh ea216adf0f feat(discover): add Release Watch section to Home and For You
The Qobuz mobile client (v9.7) surfaces a 'Release Watch' /
'Radar de Novedades' feed in its Discover screens — new releases
from the artists, labels and award series the user follows. We
didn't have parity for it. This adds it as a module in both
Discover tabs.

Backend:
- qbz-qobuz: new ALBUMS_RELEASE_WATCH endpoint constant
  (/albums/releaseWatch, plural as in mobile) and signed_get_auth
  client method returning a SearchResultsPage<Album>. Signing
  method is 'albumsreleaseWatch' per the Qobuz secret protocol.
- qbz-core: passthrough get_release_watch on QbzCore.
- src-tauri: core_bridge wrapper, new v2_get_release_watch Tauri
  command registered in lib.rs. Filters through the artist
  blacklist like other album endpoints.

Frontend:
- HomeView: new renderable section 'releaseWatch' in the home tab
  (default visible, placed right after New Releases). Parallel
  fetch alongside the existing discover_index call; mapped via
  the existing toAlbumCard helper.
- ForYouTab: section appears ahead of 'Essentials' since followed
  artists/labels are typically more personal.
- homeSettingsStore: new HomeSectionId 'releaseWatch' in the type
  union and DEFAULT_SECTIONS so the user can toggle/reorder it
  from home customization.
- i18n: discover.releaseWatch.title + subtitle added to all five
  locales, using Qobuz's own translations where they exist
  ('Radar de Novedades' for Spanish, 'Release-Radar' for German)
  with the 'followed artists/labels/awards' subtitle copy adapted
  from the mobile app's strings.xml resources.

cc
2026-04-13 22:33:59 -06:00
vicrodh 5aa0df1cbc feat(api): sign all remaining Qobuz API endpoints
Add request_ts + request_sig to every API call matching the mobile
app interceptor pattern. Introduces signed_get/signed_get_auth
helpers and a generic sign_request function.

Endpoints now signed:
- album/get, album/getFeatured
- track/get, track/getList
- artist/get, artist/getSimilarArtists, artist/page, artist/getReleasesGrid
- discover/index, discover/playlists, discover/* browse endpoints
- genre/list
- label/page, label/explore, label/get
- playlist/get, playlist/getUserPlaylists, playlist/create,
  playlist/delete, playlist/addTracks, playlist/deleteTracks,
  playlist/update, playlist/subscribe, playlist/unsubscribe
- playlist/getTags
- favorite/create, favorite/delete

Excluded (pre-auth, cannot sign): user/login, oauth/callback.
Already signed (unchanged): track/getFileUrl, favorite/getUserFavorites,
session/start, file/url, all search endpoints.

cc
2026-04-08 09:41:57 -06:00
vicrodh dbc97d6bd1 feat(search): add request signing to all search endpoints
Add request_ts + request_sig (MD5 signature) to catalog/search,
album/search, track/search, artist/search, and playlist/search
endpoints matching the mobile app authentication pattern.

Migrate v2_search_all from legacy AppState to CoreBridge, removing
the last direct API call that bypassed the V2 crate architecture.

cc
2026-04-08 08:34:32 -06:00
vicrodh 74e3ba9519 chore(logging): reduce noise in default log level
- Device enumeration (48 CPAL devices): info → debug
- Playlist fetch completion: info → debug
- Playlist command invocation: info → debug
- zbus socket_reader/tracing: filtered to warn level

These remain visible with RUST_LOG=debug or verbose log capture.

cc
2026-04-07 08:39:18 -06:00
vicrodh 4505312280 fix(security): remove hardcoded crypto seed from cmaf crate
Move the CMAF app seed from a hardcoded constant in qbz-cmaf to a
parameter passed by callers. The seed now lives in qbz-qobuz::auth
where other app-level constants are defined. This resolves CodeQL
alerts for hard-coded cryptographic values.

Also truncate session_id in logs to avoid cleartext logging of
sensitive session identifiers.

cc
2026-04-07 07:04:33 -06:00
vicrodh bf555ab143 feat(streaming): CMAF segmented download pipeline
Download tracks via Qobuz Akamai CDN with encrypted CMAF segments.
Derives session key (HKDF), unwraps content key (AES-CBC), decrypts
frames (AES-CTR), reassembles into standard FLAC. Falls back to legacy
track/getFileUrl if CMAF fails.

- Add qbz-cmaf dependency to src-tauri
- Make ensure_cmaf_session public on QobuzClient
- Add ensure_cmaf_session and get_file_url passthroughs to CoreBridge
- Add download_audio_cmaf and try_cmaf_download helpers
- Wire CMAF as primary download path in v2_play_track with legacy fallback

cc
2026-04-06 13:31:41 -06:00
vicrodh 0265cb996c feat(qobuz): add session/start and file/url CMAF endpoints
New CMAF session management with lazy renewal and file/url endpoint
returning segmented encrypted streaming URLs.

cc
2026-04-06 13:19:57 -06:00
vicrodh 3ac7073b9c feat(qobuz): migrate login_with_token to V2 crates
Add login_with_token to QobuzClient and QbzCore for restoring
sessions from saved OAuth tokens. Previously only existed in
the legacy client.

cc
2026-04-02 07:47:45 -06:00
vicrodh 1ad16338fa feat: add Qobuz playlist follow/unfollow (#172)
Discovered undocumented Qobuz API endpoints from bundle analysis:
- POST /playlist/subscribe { playlist_id }
- POST /playlist/unsubscribe { playlist_id }

This is the native "follow playlist" feature that syncs across all
Qobuz clients, separate from QBZ's existing "Copy to Library" (local
copy) and "Favorite" (local flag) features.

Added:
- subscribe_playlist/unsubscribe_playlist in both qbz-qobuz and legacy client
- v2_qobuz_subscribe_playlist / v2_qobuz_unsubscribe_playlist commands
- Bookmark toggle button in PlaylistDetailView (next to Copy button)
- i18n keys in all 5 locales

cc
2026-03-19 11:36:56 -06:00
vicrodh de29f1e2e0 chore(qconnect): checkpoint current poc state
cx
2026-03-13 17:28:05 -06:00
vicrodh c17c01b02b fix(security): remove unused qobuz user_id accessor
cx
2026-03-01 09:49:57 -06:00
vicrodh 133defbafa fix(security): redact sensitive identifiers and secrets from backend logs
cx
2026-03-01 09:24:42 -06:00
vicrodh 21358aa876 fix(oauth): use 'code' param for /oauth/callback API (Qobuz uses code_autorisation in redirect URL but 'code' in the API) 2026-02-23 15:24:40 -06:00
vicrodh 22060d1d35 fix(oauth): correct parameter names (code_autorisation, not code)
Qobuz redirects to play.qobuz.com/discover?code_autorisation=XXX
not ?code=XXX. Both the navigation intercept and the /oauth/callback
API call were using the wrong parameter name, so the code was never
captured and the exchange request was malformed.
2026-02-23 15:18:52 -06:00
vicrodh 5ed96c1d2d feat(qobuz): add private_key extraction and login_with_oauth_code to qbz-qobuz client 2026-02-23 14:45:20 -06:00
vicrodh 585053bd42 feat(label): redesign label detail view with Android-style sections
Replace the flat album grid LabelView with a rich detail page featuring:
- Header with label image, name, description, and play button
- Popular Tracks section (5/10/all expand, same UX as ArtistDetailView)
- Releases horizontal scroll row with "See all" → LabelReleasesView
- Critics' Picks section from second releases container
- Playlists, Artists, and More Labels sections

Backend: add /label/page and /label/explore API endpoints with V2
commands (v2_get_label_page, v2_get_label_explore). Models use
serde_json::Value for flexible section parsing.

Old LabelView renamed to LabelReleasesView for the "See all" flow.
i18n keys added to all 4 locales (en, es, de, fr).
2026-02-20 02:51:47 -06:00
vicrodh 985f634817 feat(qobuz): add link resolver module with tests
Pure-Rust URL parser for qobuzapp:// and https://play.qobuz.com/ links.
Resolves to typed ResolvedLink enum (OpenAlbum, OpenTrack, OpenArtist,
OpenPlaylist). 24 unit tests covering happy paths and edge cases.
2026-02-17 00:22:26 -06:00
vicrodh 0be0f6dcb0 feat(qbz-qobuz): implement Phase 4 - Qobuz API client
Extracted Qobuz API client from qbz-nix to qbz-qobuz crate.

Modules:
- client.rs: Main QobuzClient with all API methods
- auth.rs: Request signing and authentication
- bundle.rs: Bundle token extraction
- endpoints.rs: API endpoint definitions
- error.rs: API error types
- performers.rs: Performers parsing

Updated qbz-models with additional types:
- Discover types (DiscoverResponse, DiscoverAlbum, etc.)
- Artist page types (PageArtistResponse, ReleasesGridResponse, etc.)
- Playlist tags (PlaylistTag, PlaylistTagsResponse)

Dependencies:
- reqwest for HTTP
- md-5 for request signing
- futures-util for concurrent requests
- anyhow for error handling
2026-02-14 13:25:18 -06:00
vicrodh 0911cf75df refactor(backend): Phase 0 - Initialize multi-crate workspace
Create the foundational workspace structure for the multi-crate
architecture refactor. This phase establishes placeholder crates
that will be populated in subsequent phases.

Crates created:
- qbz-models: Shared types, events, and traits (Tier 0)
- qbz-audio: Audio backend system (Tier 1)
- qbz-player: Playback engine and queue (Tier 1)
- qbz-qobuz: Qobuz API client (Tier 1)
- qbz-core: Orchestrator (Tier 3)

All crates compile successfully with placeholder implementations.
The workspace uses shared dependencies via workspace.dependencies.

cc
2026-02-14 12:48:52 -06:00