qobuz_api/api/content/
stream.rs1pub mod quality {
5 pub const MP3_320: i32 = 5;
7 pub const FLAC_16_44: i32 = 6;
9 pub const FLAC_24_96: i32 = 7;
11 pub const FLAC_24_192: i32 = 27;
13}
14
15use std::time::{SystemTime, UNIX_EPOCH};
16
17use serde::{Deserialize, Serialize};
18
19use crate::{
20 api::{
21 http_client::HttpClient,
22 requests::{RequestAuth, build_url_with_params, retry_with_backoff},
23 response::parse_response,
24 service::QobuzApiService,
25 },
26 errors::QobuzApiError,
27 signing::sign_track_file_url,
28};
29
30#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
32pub struct FileUrl {
33 pub track_id: Option<i32>,
35 pub duration: Option<f64>,
37 pub url: Option<String>,
39 pub format_id: Option<i32>,
41 pub mime_type: Option<String>,
43 pub sampling_rate: Option<f64>,
45 pub bit_depth: Option<i32>,
47 pub status: Option<i32>,
49 pub message: Option<String>,
51 pub code: Option<String>,
53}
54
55pub async fn get_track_file_url(
72 service: &QobuzApiService,
73 track_id: i32,
74 format_id: i32,
75) -> Result<FileUrl, QobuzApiError> {
76 let token = service.require_auth_token()?;
77
78 get_track_file_url_raw(
79 service.http_client(),
80 service.base_url(),
81 &RequestAuth {
82 app_id: &service.app_id,
83 app_secret: service.app_secret(),
84 user_auth_token: token,
85 },
86 track_id,
87 format_id,
88 )
89 .await
90}
91
92pub async fn get_track_file_url_raw(
110 client: &dyn HttpClient,
111 base_url: &str,
112 auth: &RequestAuth<'_>,
113 track_id: i32,
114 format_id: i32,
115) -> Result<FileUrl, QobuzApiError> {
116 let ts = SystemTime::now()
117 .duration_since(UNIX_EPOCH)
118 .unwrap_or_default()
119 .as_secs()
120 .to_string();
121
122 let sig = sign_track_file_url(format_id, track_id, &ts, auth.app_secret);
123
124 let params: Vec<(String, String)> = vec![
125 ("track_id".to_string(), track_id.to_string()),
126 ("format_id".to_string(), format_id.to_string()),
127 ("intent".to_string(), "stream".to_string()),
128 ("request_ts".to_string(), ts),
129 ("request_sig".to_string(), sig),
130 ("app_id".to_string(), auth.app_id.to_string()),
131 ];
132
133 let url = build_url_with_params(base_url, "/track/getFileUrl", ¶ms);
134 let response = retry_with_backoff(client, &url, auth.user_auth_token).await?;
135
136 parse_response::<FileUrl>(response, "/track/getFileUrl").await
137}
138
139#[cfg(test)]
140mod tests {
141 use {
142 anyhow::{Result, ensure},
143 serde_json::from_str,
144 };
145
146 use crate::api::content::stream::{
147 FileUrl,
148 quality::{FLAC_16_44, FLAC_24_96, FLAC_24_192, MP3_320},
149 };
150
151 #[test]
157 fn quality_constants_are_distinct() -> Result<()> {
158 ensure!(MP3_320 == 5, "MP3 constant mismatch");
159 ensure!(FLAC_16_44 == 6, "FLAC 16/44 constant mismatch");
160 ensure!(FLAC_24_96 == 7, "FLAC 24/96 constant mismatch");
161 ensure!(FLAC_24_192 == 27, "FLAC 24/192 constant mismatch");
162 Ok(())
163 }
164
165 #[test]
171 fn file_url_deserializes() -> Result<()> {
172 let body = r#"{"url":"https://example.com/file.flac","format_id":6}"#;
173 let file_url: FileUrl = from_str(body)?;
174 ensure!(
175 file_url.url.as_deref() == Some("https://example.com/file.flac"),
176 "URL mismatch"
177 );
178 ensure!(file_url.format_id == Some(6), "format mismatch");
179 Ok(())
180 }
181}