qobuz_api_rust/api/content/albums.rs
1use std::fs::create_dir_all;
2
3use crate::{
4 api::service::QobuzApiService,
5 errors::QobuzApiError::{self, ApiErrorResponse, IoError},
6 metadata::MetadataConfig,
7 models::{Album, SearchResult},
8 utils::sanitize_filename,
9};
10
11impl QobuzApiService {
12 /// Retrieves an album with the specified ID.
13 ///
14 /// This method fetches detailed information about a specific album from the Qobuz API,
15 /// including metadata, track listing, and other album-related information.
16 ///
17 /// # Arguments
18 ///
19 /// * `album_id` - The unique identifier of the album to retrieve
20 /// * `with_auth` - Optional boolean to execute request with or without user authentication token.
21 /// When `None`, defaults to `false` (no authentication).
22 /// * `extra` - Optional string specifying additional album information to include in the response,
23 /// such as "items", "tracks", "release_tags", etc.
24 /// * `limit` - Optional integer specifying the maximum number of tracks to include in the response.
25 /// When `None`, defaults to 1200.
26 /// * `offset` - Optional integer specifying the offset of the first track to include in the response.
27 /// When `None`, defaults to 0.
28 ///
29 /// # Returns
30 ///
31 /// * `Ok(Album)` - Contains the complete album information if the request is successful
32 /// * `Err(QobuzApiError)` - If the API request fails due to network issues, invalid parameters,
33 /// or other API-related errors
34 ///
35 /// # Example
36 ///
37 /// ```
38 /// # use qobuz_api_rust::QobuzApiService;
39 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
40 /// let service = QobuzApiService::new().await?;
41 /// let album = service.get_album("12345", None, Some("tracks"), Some(10), None).await?;
42 /// println!("Album title: {}", album.title.unwrap_or_default());
43 /// # Ok(())
44 /// # }
45 /// ```
46 pub async fn get_album(
47 &self,
48 album_id: &str,
49 with_auth: Option<bool>,
50 extra: Option<&str>,
51 limit: Option<i32>,
52 offset: Option<i32>,
53 ) -> Result<Album, QobuzApiError> {
54 let mut params = vec![("album_id".to_string(), album_id.to_string())];
55
56 if let Some(extra_val) = extra {
57 params.push(("extra".to_string(), extra_val.to_string()));
58 }
59
60 params.push(("limit".to_string(), limit.unwrap_or(1200).to_string()));
61 params.push(("offset".to_string(), offset.unwrap_or(0).to_string()));
62
63 let _use_auth = with_auth.unwrap_or(false);
64
65 self.get("/album/get", ¶ms).await
66 }
67
68 /// Searches for albums using the specified query.
69 ///
70 /// This method allows searching for albums based on a text query, with optional pagination
71 /// parameters to control the number of results returned.
72 ///
73 /// # Arguments
74 ///
75 /// * `query` - The search query string (e.g., album title, artist name)
76 /// * `limit` - Optional integer specifying the maximum number of results to return.
77 /// When `None`, defaults to 50.
78 /// * `offset` - Optional integer specifying the offset of the first result to return.
79 /// When `None`, defaults to 0.
80 /// * `with_auth` - Optional boolean to execute search with or without user authentication token.
81 /// When `None`, defaults to `false` (no authentication).
82 ///
83 /// # Returns
84 ///
85 /// * `Ok(SearchResult)` - Contains the search results with albums matching the query
86 /// * `Err(QobuzApiError)` - If the API request fails due to network issues, invalid parameters,
87 /// or other API-related errors
88 ///
89 /// # Example
90 ///
91 /// ```
92 /// # use qobuz_api_rust::QobuzApiService;
93 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
94 /// let service = QobuzApiService::new().await?;
95 /// let results = service.search_albums("radiohead", Some(10), None, None).await?;
96 /// if let Some(albums) = results.albums {
97 /// println!("Found {} albums", albums.total.unwrap_or(0));
98 /// }
99 /// # Ok(())
100 /// # }
101 /// ```
102 pub async fn search_albums(
103 &self,
104 query: &str,
105 limit: Option<i32>,
106 offset: Option<i32>,
107 with_auth: Option<bool>,
108 ) -> Result<SearchResult, QobuzApiError> {
109 let params = vec![
110 ("query".to_string(), query.to_string()),
111 ("limit".to_string(), limit.unwrap_or(50).to_string()),
112 ("offset".to_string(), offset.unwrap_or(0).to_string()),
113 ];
114
115 let _use_auth = with_auth.unwrap_or(false);
116
117 self.get("/album/search", ¶ms).await
118 }
119
120 /// Downloads an entire album to the specified path.
121 ///
122 /// This method downloads all tracks of an album to a specified directory, with options for
123 /// different audio quality formats. The method handles track-by-track downloads and includes
124 /// automatic credential refresh if signature errors occur during the download process.
125 ///
126 /// # Arguments
127 ///
128 /// * `album_id` - The unique identifier of the album to download
129 /// * `format_id` - The format ID specifying audio quality:
130 /// - "5": MP3 320 kbps
131 /// - "6": FLAC Lossless (16-bit/44.1kHz)
132 /// - "7": FLAC Hi-Res (24-bit/96kHz)
133 /// - "27": FLAC Hi-Res (24-bit/192kHz)
134 /// * `path` - The directory path where the album should be saved. The directory will be created
135 /// if it doesn't exist. The path should already include artist/album folder structure.
136 ///
137 /// # Returns
138 ///
139 /// * `Ok(())` - If all tracks in the album are downloaded successfully
140 /// * `Err(QobuzApiError)` - If the API request fails, download fails for any track, or other
141 /// errors occur during the process
142 ///
143 /// # Note
144 ///
145 /// This method includes automatic retry with credential refresh if signature errors occur.
146 /// Each track is downloaded with progress reporting and metadata embedding.
147 ///
148 /// # Example
149 ///
150 /// ```
151 /// # use qobuz_api_rust::QobuzApiService;
152 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
153 /// let service = QobuzApiService::new().await?;
154 /// service.download_album("12345", "6", "./downloads/Artist/Album Title").await?;
155 /// println!("Album downloaded successfully!");
156 /// # Ok(())
157 /// # }
158 /// ```
159 pub async fn download_album(
160 &self,
161 album_id: &str,
162 format_id: &str,
163 path: &str,
164 config: &MetadataConfig,
165 ) -> Result<(), QobuzApiError> {
166 let album = self
167 .get_album(album_id, None, Some("track_ids"), None, None)
168 .await?;
169
170 if let Some(track_ids) = album.track_ids {
171 let total_tracks = track_ids.len();
172 println!();
173 println!("Album contains {} tracks", total_tracks);
174 println!();
175
176 // Create the directory structure as provided in path parameter
177 let album_dir = path;
178 create_dir_all(album_dir).map_err(IoError)?;
179
180 for (index, track_id) in track_ids.iter().enumerate() {
181 let track = self.get_track(&track_id.to_string(), None).await?;
182 let file_extension = match format_id {
183 "5" => "mp3",
184 "6" | "7" | "27" => "flac",
185 _ => "flac", // default to flac
186 };
187
188 // Get track number and title for the filename
189 let track_number = track.track_number.unwrap_or(0);
190 let track_title = track
191 .title
192 .as_ref()
193 .unwrap_or(&format!("Track {}", track_id))
194 .clone();
195
196 // Create filename following MusicBrainz Picard style: [Titelnr.]. [Titel]
197 let track_filename = format!("{:02}. {}", track_number, track_title);
198 let sanitized_filename = sanitize_filename(&track_filename);
199 let track_path = format!("{}/{}.{}", album_dir, sanitized_filename, file_extension);
200
201 println!(
202 "Downloading track {}/{}: {} - {}",
203 index + 1,
204 total_tracks,
205 track_number,
206 track_title
207 );
208
209 // Attempt to download the track, with credential refresh on signature errors
210 match self
211 .download_track(&track_id.to_string(), format_id, &track_path, config)
212 .await
213 {
214 Ok(()) => {
215 // Success, continue to next track
216 }
217
218 Err(ApiErrorResponse { message, .. })
219 if message.contains("Invalid Request Signature parameter") =>
220 {
221 eprintln!(
222 "Invalid signature detected during album download, attempting to refresh app credentials..."
223 );
224
225 // Refresh credentials and retry the track download
226 match self.refresh_app_credentials().await {
227 Ok(new_service) => {
228 // Use the new service instance to download the track
229 match new_service
230 .download_track(
231 &track_id.to_string(),
232 format_id,
233 &track_path,
234 config,
235 )
236 .await
237 {
238 Ok(()) => {
239 // Successfully downloaded with new credentials
240 }
241
242 Err(e) => {
243 // If it still fails, return the error
244 return Err(e);
245 }
246 }
247 }
248
249 Err(e) => {
250 eprintln!("Failed to refresh credentials: {}", e);
251 return Err(ApiErrorResponse {
252 code: 400.to_string(),
253 message,
254 status: "error".to_string(),
255 });
256 }
257 }
258 }
259
260 Err(e) => {
261 // Any other error, return immediately
262 return Err(e);
263 }
264 }
265 }
266
267 println!();
268 println!(
269 "Album download completed: {}/{} tracks downloaded",
270 total_tracks, total_tracks
271 );
272 println!();
273 }
274
275 Ok(())
276 }
277}