Skip to main content

qobuz_api/api/content/
catalog.rs

1//! Catalog search: searches all content types simultaneously.
2
3use {
4    serde::{Deserialize, Serialize},
5    tokio::try_join,
6};
7
8use crate::{
9    api::{
10        content::{
11            albums::{Album, search_albums},
12            artists::{Artist, search_artists},
13            playlists::{Playlist, search_playlists},
14            tracks::{Track, search_tracks},
15        },
16        service::QobuzApiService,
17    },
18    errors::QobuzApiError,
19};
20
21/// API response wrapper for album search (`/album/search`).
22#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
23pub struct AlbumSearchResponse {
24    /// Album search results.
25    pub albums: ItemSearchResult<Box<Album>>,
26}
27
28/// API response wrapper for artist search (`/artist/search`).
29#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
30pub struct ArtistSearchResponse {
31    /// Artist search results.
32    pub artists: ItemSearchResult<Box<Artist>>,
33}
34
35/// Generic paginated result container.
36#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
37pub struct ItemSearchResult<T> {
38    /// Result items.
39    pub items: Option<Vec<T>>,
40    /// Total matching items.
41    pub total: Option<i32>,
42    /// Items per page.
43    pub limit: Option<i32>,
44    /// Current page offset.
45    pub offset: Option<i32>,
46}
47
48/// API response wrapper for playlist search (`/playlist/search`).
49#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
50pub struct PlaylistSearchResponse {
51    /// Playlist search results.
52    pub playlists: ItemSearchResult<Box<Playlist>>,
53}
54
55/// Grouped search results across content types.
56#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
57pub struct SearchResult {
58    /// Matching albums.
59    pub albums: Option<ItemSearchResult<Box<Album>>>,
60    /// Matching artists.
61    pub artists: Option<ItemSearchResult<Box<Artist>>>,
62    /// Matching tracks.
63    pub tracks: Option<ItemSearchResult<Box<Track>>>,
64    /// Matching playlists.
65    pub playlists: Option<ItemSearchResult<Box<Playlist>>>,
66}
67
68/// API response wrapper for track search (`/track/search`).
69#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
70pub struct TrackSearchResponse {
71    /// Track search results.
72    pub tracks: ItemSearchResult<Box<Track>>,
73}
74
75/// Collection of user's favorited items.
76#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
77pub struct UserFavorites {
78    /// Favorite albums.
79    pub albums: Option<ItemSearchResult<Box<Album>>>,
80    /// Favorite artists.
81    pub artists: Option<ItemSearchResult<Box<Artist>>>,
82    /// Favorite tracks.
83    pub tracks: Option<ItemSearchResult<Box<Track>>>,
84    /// Favorite article IDs.
85    pub article_ids: Option<Vec<i32>>,
86    /// Favorite artist IDs.
87    pub artist_ids: Option<Vec<i32>>,
88    /// Favorite album IDs.
89    pub album_ids: Option<Vec<i32>>,
90    /// Favorite track IDs.
91    pub track_ids: Option<Vec<i32>>,
92}
93
94/// Searches all content types (albums, artists, tracks, playlists).
95///
96/// Returns grouped results for each content type.
97///
98/// # Arguments
99///
100/// * `service` - Authenticated API service
101/// * `query` - Search query string
102/// * `limit` - Maximum number of results per content type
103/// * `offset` - Pagination offset
104///
105/// # Returns
106///
107/// A `SearchResult` with grouped results for each content type.
108///
109/// # Errors
110///
111/// Returns a `QobuzApiError` if any of the parallel search requests fails.
112pub async fn search_catalog(
113    service: &QobuzApiService,
114    query: &str,
115    limit: Option<i32>,
116    offset: Option<i32>,
117) -> Result<SearchResult, QobuzApiError> {
118    let (albums, artists, tracks, playlists) = try_join!(
119        search_albums(service, query, limit, offset),
120        search_artists(service, query, limit, offset),
121        search_tracks(service, query, limit, offset),
122        search_playlists(service, query, limit, offset),
123    )?;
124
125    Ok(SearchResult {
126        albums: Some(albums),
127        artists: Some(artists),
128        tracks: Some(tracks),
129        playlists: Some(playlists),
130    })
131}
132
133#[cfg(test)]
134mod tests {
135    use {
136        anyhow::{Result, ensure},
137        tokio::runtime::Runtime,
138    };
139
140    use crate::api::{
141        content::catalog::search_catalog,
142        fixture::{MockServer, make_service},
143    };
144
145    /// Tests search catalog groups all types.
146    ///
147    /// # Errors
148    ///
149    /// Returns an error if the test setup or assertion fails.
150    #[test]
151    fn search_catalog_groups_all_types() -> Result<()> {
152        let body = r#"{"albums":{"items":[],"total":0},"artists":{"items":[],"total":0},"tracks":{"items":[],"total":0},"playlists":{"items":[],"total":0}}"#;
153        let server = MockServer::start_with_max_requests(200, body, 16)?;
154        let service = make_service(&server.base_url())?;
155        let rt = Runtime::new()?;
156        let result = rt.block_on(search_catalog(&service, "Test", Some(5), None))?;
157        ensure!(result.albums.is_some());
158        ensure!(result.artists.is_some());
159        ensure!(result.tracks.is_some());
160        ensure!(result.playlists.is_some());
161        Ok(())
162    }
163
164    /// Tests search catalog error stops all.
165    ///
166    /// # Errors
167    ///
168    /// Returns an error if the test setup or assertion fails.
169    #[test]
170    fn search_catalog_error_stops_all() -> Result<()> {
171        let body = r#"{"status":"error","code":500,"message":"Fail"}"#;
172        let server = MockServer::start_with_max_requests(500, body, 16)?;
173        let service = make_service(&server.base_url())?;
174        let rt = Runtime::new()?;
175        let result = rt.block_on(search_catalog(&service, "fail", None, None));
176        ensure!(result.is_err());
177        Ok(())
178    }
179}