From 0be0f6dcb02033db4a46760c32d89d681169cf7a Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Sat, 14 Feb 2026 13:25:18 -0600 Subject: [PATCH] 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 --- crates/qbz-models/src/lib.rs | 11 + crates/qbz-models/src/types.rs | 320 +++++++ crates/qbz-qobuz/Cargo.toml | 2 + crates/qbz-qobuz/src/auth.rs | 151 +++ crates/qbz-qobuz/src/bundle.rs | 199 ++++ crates/qbz-qobuz/src/client.rs | 1439 ++++++++++++++++++++++++++++ crates/qbz-qobuz/src/endpoints.rs | 68 ++ crates/qbz-qobuz/src/error.rs | 47 + crates/qbz-qobuz/src/lib.rs | 47 +- crates/qbz-qobuz/src/performers.rs | 139 +++ 10 files changed, 2410 insertions(+), 13 deletions(-) create mode 100644 crates/qbz-qobuz/src/auth.rs create mode 100644 crates/qbz-qobuz/src/bundle.rs create mode 100644 crates/qbz-qobuz/src/client.rs create mode 100644 crates/qbz-qobuz/src/endpoints.rs create mode 100644 crates/qbz-qobuz/src/error.rs create mode 100644 crates/qbz-qobuz/src/performers.rs diff --git a/crates/qbz-models/src/lib.rs b/crates/qbz-models/src/lib.rs index a6cb3ee50..09d555fc6 100644 --- a/crates/qbz-models/src/lib.rs +++ b/crates/qbz-models/src/lib.rs @@ -46,4 +46,15 @@ pub use types::{ PlaylistDuplicateResult, PlaylistGenre, PlaylistOwner, PlaylistWithTrackIds, Quality, SearchResults, SearchResultsPage, StreamRestriction, StreamUrl, Track, TracksContainer, UserSession, + // Discover types + DiscoverAlbum, DiscoverAlbumDates, DiscoverAlbumImage, DiscoverArtist, DiscoverAudioInfo, + DiscoverContainer, DiscoverContainers, DiscoverData, DiscoverPlaylist, DiscoverPlaylistImage, + DiscoverPlaylistsResponse, DiscoverResponse, PlaylistTag, PlaylistTagsResponse, RawPlaylistTag, + // Artist page types + PageArtistAward, PageArtistBiography, PageArtistImages, PageArtistName, + PageArtistPhysicalSupport, PageArtistPlaylist, PageArtistPlaylistImages, + PageArtistPlaylistOwner, PageArtistPlaylists, PageArtistPortrait, PageArtistRelease, + PageArtistReleaseArtist, PageArtistReleaseContributor, PageArtistReleaseGroup, + PageArtistResponse, PageArtistRights, PageArtistSimilar, PageArtistSimilarItem, + PageArtistTrack, PageArtistTrackAlbum, ReleasesGridResponse, }; diff --git a/crates/qbz-models/src/types.rs b/crates/qbz-models/src/types.rs index 59f2b47a2..8ada888ea 100644 --- a/crates/qbz-models/src/types.rs +++ b/crates/qbz-models/src/types.rs @@ -410,3 +410,323 @@ pub struct Favorites { pub tracks: Option>, pub artists: Option>, } + +// ============ Discover API Types ============ + +/// Discover index response +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DiscoverResponse { + pub containers: DiscoverContainers, +} + +/// All discover containers +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DiscoverContainers { + pub playlists: Option>, + pub ideal_discography: Option>, + pub playlists_tags: Option>, + pub new_releases: Option>, + pub qobuzissims: Option>, + pub most_streamed: Option>, + pub press_awards: Option>, + pub album_of_the_week: Option>, +} + +/// Generic discover container +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DiscoverContainer { + pub id: String, + pub data: DiscoverData, +} + +/// Generic discover data with items +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DiscoverData { + pub has_more: bool, + pub items: Vec, +} + +/// Playlist from discover endpoint +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DiscoverPlaylist { + pub id: u64, + pub name: String, + pub owner: PlaylistOwner, + pub image: DiscoverPlaylistImage, + pub description: Option, + pub duration: u32, + pub tracks_count: u32, + pub genres: Option>, + pub tags: Option>, +} + +/// Playlist image from discover +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DiscoverPlaylistImage { + pub rectangle: Option, + pub covers: Option>, +} + +/// Playlist tag (for filtering) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PlaylistTag { + pub id: u64, + pub slug: String, + pub name: String, +} + +/// Raw playlist tag from /playlist/getTags +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RawPlaylistTag { + pub slug: String, + pub name_json: String, + pub position: Option, + pub is_discover: Option, + pub featured_tag_id: Option, +} + +/// Response from /playlist/getTags +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PlaylistTagsResponse { + pub tags: Vec, +} + +/// Response from discover/playlists endpoint +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DiscoverPlaylistsResponse { + pub has_more: bool, + pub items: Vec, +} + +/// Album from discover endpoint +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DiscoverAlbum { + pub id: String, + pub title: String, + pub version: Option, + pub track_count: Option, + pub duration: Option, + pub parental_warning: Option, + pub image: DiscoverAlbumImage, + pub artists: Vec, + pub label: Option