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
This commit is contained in:
vicrodh committed 2026-02-14 13:25:18 -06:00
1 parent ad7333d426
commit 0be0f6dcb0
10 files changed
+2410 -13

No files matched your search

+11
View File
@@ -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,
};
+320
View File
@@ -410,3 +410,323 @@ pub struct Favorites {
pub tracks: Option<SearchResultsPage<Track>>,
pub artists: Option<SearchResultsPage<Artist>>,
}
// ============ 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<DiscoverContainer<DiscoverPlaylist>>,
pub ideal_discography: Option<DiscoverContainer<DiscoverAlbum>>,
pub playlists_tags: Option<DiscoverContainer<PlaylistTag>>,
pub new_releases: Option<DiscoverContainer<DiscoverAlbum>>,
pub qobuzissims: Option<DiscoverContainer<DiscoverAlbum>>,
pub most_streamed: Option<DiscoverContainer<DiscoverAlbum>>,
pub press_awards: Option<DiscoverContainer<DiscoverAlbum>>,
pub album_of_the_week: Option<DiscoverContainer<DiscoverAlbum>>,
}
/// Generic discover container
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiscoverContainer<T> {
pub id: String,
pub data: DiscoverData<T>,
}
/// Generic discover data with items
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiscoverData<T> {
pub has_more: bool,
pub items: Vec<T>,
}
/// 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<String>,
pub duration: u32,
pub tracks_count: u32,
pub genres: Option<Vec<PlaylistGenre>>,
pub tags: Option<Vec<PlaylistTag>>,
}
/// Playlist image from discover
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiscoverPlaylistImage {
pub rectangle: Option<String>,
pub covers: Option<Vec<String>>,
}
/// 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<String>,
pub is_discover: Option<String>,
pub featured_tag_id: Option<String>,
}
/// Response from /playlist/getTags
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlaylistTagsResponse {
pub tags: Vec<RawPlaylistTag>,
}
/// Response from discover/playlists endpoint
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiscoverPlaylistsResponse {
pub has_more: bool,
pub items: Vec<DiscoverPlaylist>,
}
/// Album from discover endpoint
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiscoverAlbum {
pub id: String,
pub title: String,
pub version: Option<String>,
pub track_count: Option<u32>,
pub duration: Option<u32>,
pub parental_warning: Option<bool>,
pub image: DiscoverAlbumImage,
pub artists: Vec<DiscoverArtist>,
pub label: Option<Label>,
pub genre: Option<Genre>,
pub dates: Option<DiscoverAlbumDates>,
pub audio_info: Option<DiscoverAudioInfo>,
}
/// Album image from discover endpoint
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiscoverAlbumImage {
pub small: Option<String>,
pub thumbnail: Option<String>,
pub large: Option<String>,
}
/// Artist in discover album
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiscoverArtist {
pub id: u64,
pub name: String,
pub roles: Option<Vec<String>>,
}
/// Album dates from discover
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiscoverAlbumDates {
pub download: Option<String>,
pub original: Option<String>,
pub stream: Option<String>,
}
/// Audio info from discover album
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiscoverAudioInfo {
pub maximum_sampling_rate: Option<f64>,
pub maximum_bit_depth: Option<u32>,
pub maximum_channel_count: Option<u32>,
}
// ============ Artist Page Types (/artist/page) ============
/// Top-level response from /artist/page
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageArtistResponse {
pub id: u64,
pub name: PageArtistName,
pub artist_category: Option<String>,
pub biography: Option<PageArtistBiography>,
pub images: Option<PageArtistImages>,
pub similar_artists: Option<PageArtistSimilar>,
pub top_tracks: Option<Vec<PageArtistTrack>>,
pub last_release: Option<serde_json::Value>,
pub releases: Option<Vec<PageArtistReleaseGroup>>,
pub tracks_appears_on: Option<Vec<PageArtistTrack>>,
pub playlists: Option<PageArtistPlaylists>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageArtistName {
pub display: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageArtistBiography {
pub content: Option<String>,
pub source: Option<serde_json::Value>,
pub language: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageArtistImages {
pub portrait: Option<PageArtistPortrait>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageArtistPortrait {
pub hash: String,
pub format: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageArtistSimilar {
pub has_more: bool,
pub items: Vec<PageArtistSimilarItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageArtistSimilarItem {
pub id: u64,
pub name: PageArtistName,
pub images: Option<PageArtistImages>,
}
/// A group of releases by type
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageArtistReleaseGroup {
#[serde(rename = "type")]
pub release_type: String,
pub has_more: bool,
pub items: Vec<PageArtistRelease>,
}
/// A release item from /artist/page
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageArtistRelease {
pub id: String,
pub title: String,
pub version: Option<String>,
pub tracks_count: Option<u32>,
pub artist: Option<PageArtistReleaseArtist>,
pub artists: Option<Vec<PageArtistReleaseContributor>>,
pub image: Option<ImageSet>,
pub label: Option<Label>,
pub genre: Option<Genre>,
pub release_type: Option<String>,
pub release_tags: Option<Vec<String>>,
pub duration: Option<u32>,
pub dates: Option<DiscoverAlbumDates>,
pub parental_warning: Option<bool>,
pub audio_info: Option<DiscoverAudioInfo>,
pub rights: Option<PageArtistRights>,
pub awards: Option<Vec<PageArtistAward>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageArtistReleaseArtist {
pub id: u64,
pub name: PageArtistName,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageArtistReleaseContributor {
pub id: u64,
pub name: String,
pub roles: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageArtistRights {
pub streamable: Option<bool>,
pub hires_streamable: Option<bool>,
pub hires_purchasable: Option<bool>,
pub purchasable: Option<bool>,
pub downloadable: Option<bool>,
pub previewable: Option<bool>,
pub sampleable: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageArtistAward {
pub id: u64,
pub name: String,
pub awarded_at: Option<String>,
}
/// Track from /artist/page
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageArtistTrack {
pub id: u64,
pub title: String,
pub version: Option<String>,
pub duration: Option<u32>,
pub isrc: Option<String>,
pub parental_warning: Option<bool>,
pub artist: Option<PageArtistReleaseArtist>,
pub composer: Option<serde_json::Value>,
pub audio_info: Option<DiscoverAudioInfo>,
pub rights: Option<PageArtistRights>,
pub physical_support: Option<PageArtistPhysicalSupport>,
pub album: Option<PageArtistTrackAlbum>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageArtistPhysicalSupport {
pub media_number: Option<u32>,
pub track_number: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageArtistTrackAlbum {
pub id: String,
pub title: String,
pub version: Option<String>,
pub image: Option<ImageSet>,
pub label: Option<Label>,
pub genre: Option<Genre>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageArtistPlaylists {
pub has_more: bool,
pub items: Vec<PageArtistPlaylist>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageArtistPlaylist {
pub id: u64,
pub title: Option<String>,
pub description: Option<String>,
pub owner: Option<PageArtistPlaylistOwner>,
pub tracks_count: Option<u32>,
pub duration: Option<u32>,
pub images: Option<PageArtistPlaylistImages>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageArtistPlaylistOwner {
pub id: u64,
pub name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageArtistPlaylistImages {
pub rectangle: Option<Vec<String>>,
}
/// Response from /artist/getReleasesGrid
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReleasesGridResponse {
pub has_more: bool,
pub items: Vec<PageArtistRelease>,
}