pub mod albums;
pub mod artists;
pub mod bundle;
pub mod catalog;
pub mod cover;
pub mod discography;
pub mod mixtape;
pub mod persistence;
pub mod playlists;
pub mod quality;
pub mod record_media;
pub mod records;
pub mod stream;
pub mod tracks;
use std::sync::atomic::{AtomicBool, Ordering::Relaxed};
use serde::de::DeserializeOwned;
use crate::{
api::{
requests::{RequestAuth, signed_get},
service_state::QobuzApiService,
},
errors::QobuzApiError::{self, Canceled},
};
pub fn check_cancel(cancel: Option<&AtomicBool>) -> Result<(), QobuzApiError> {
if cancel.is_some_and(|c| c.load(Relaxed)) {
return Err(Canceled);
}
Ok(())
}
pub async fn fetch_with_cancel<T, F, Fut>(
cancel: Option<&AtomicBool>,
f: F,
) -> Result<T, QobuzApiError>
where
F: FnOnce() -> Fut,
Fut: Future<Output = Result<T, QobuzApiError>>,
{
check_cancel(cancel)?;
let result = f().await?;
check_cancel(cancel)?;
Ok(result)
}
pub fn push_pagination_params(
params: &mut Vec<(String, String)>,
limit: Option<i32>,
offset: Option<i32>,
) {
if let Some(l) = limit {
params.push(("limit".to_string(), l.to_string()));
}
if let Some(o) = offset {
params.push(("offset".to_string(), o.to_string()));
}
}
async fn do_signed_get<T>(
service: &QobuzApiService,
endpoint: &str,
params: &mut Vec<(String, String)>,
) -> Result<T, QobuzApiError>
where
T: DeserializeOwned,
{
let token = service.require_auth_token()?;
signed_get(
service.http_client(),
service.base_url(),
endpoint,
params,
&RequestAuth {
app_id: &service.app_id,
app_secret: service.app_secret(),
user_auth_token: token,
},
)
.await
}
pub async fn search<T>(
service: &QobuzApiService,
endpoint: &str,
query: &str,
limit: Option<i32>,
offset: Option<i32>,
) -> Result<T, QobuzApiError>
where
T: DeserializeOwned,
{
let mut params = vec![("query".to_string(), query.to_string())];
push_pagination_params(&mut params, limit, offset);
do_signed_get(service, endpoint, &mut params).await
}
pub async fn get_by_id<T, I>(
service: &QobuzApiService,
endpoint: &str,
id_field: &str,
id: I,
extra: Option<&str>,
) -> Result<T, QobuzApiError>
where
T: DeserializeOwned,
I: ToString,
{
let mut params = vec![(id_field.to_string(), id.to_string())];
if let Some(e) = extra {
params.push(("extra".to_string(), e.to_string()));
}
do_signed_get(service, endpoint, &mut params).await
}
pub async fn paginated<T, I>(
service: &QobuzApiService,
endpoint: &str,
id_field: &str,
id: I,
limit: Option<i32>,
offset: Option<i32>,
) -> Result<T, QobuzApiError>
where
T: DeserializeOwned,
I: ToString,
{
let mut params = vec![(id_field.to_string(), id.to_string())];
push_pagination_params(&mut params, limit, offset);
do_signed_get(service, endpoint, &mut params).await
}
#[cfg(test)]
mod tests {
use std::sync::atomic::AtomicBool;
use {
anyhow::{Result, anyhow, ensure},
tokio::runtime::Runtime,
};
use crate::{
api::content::{check_cancel, fetch_with_cancel},
errors::QobuzApiError::{self, Canceled as CanceledVariant},
};
#[test]
fn check_cancel_without_flag_succeeds() -> Result<()> {
check_cancel(None)?;
Ok(())
}
#[test]
fn check_cancel_with_flag_fails() -> Result<()> {
let cancel = AtomicBool::new(true);
let result = check_cancel(Some(&cancel));
ensure!(result.is_err(), "expected canceled error");
let err = result.err().ok_or_else(|| anyhow!("expected error"))?;
ensure!(matches!(err, CanceledVariant), "expected Canceled variant");
Ok(())
}
#[test]
fn fetch_with_cancel_returns_value() -> Result<()> {
let rt = Runtime::new()?;
let value = rt.block_on(fetch_with_cancel(None, async || {
Ok::<i32, QobuzApiError>(7)
}))?;
ensure!(value == 7, "unexpected fetch value");
Ok(())
}
#[test]
fn fetch_with_cancel_respects_cancel() -> Result<()> {
let cancel = AtomicBool::new(true);
let rt = Runtime::new()?;
let result = rt.block_on(fetch_with_cancel(Some(&cancel), async || {
Ok::<i32, QobuzApiError>(7)
}));
ensure!(result.is_err(), "expected canceled error");
Ok(())
}
}