mod actions;
mod detail;
mod library;
mod lyrics;
mod playback;
mod queue;
mod search;
mod track;
pub(crate) use actions::*;
pub(crate) use detail::*;
pub(crate) use library::*;
pub(crate) use lyrics::*;
pub(crate) use playback::*;
pub(crate) use queue::*;
pub(crate) use search::*;
pub(crate) use track::*;
use crate::*;
pub(crate) fn token_of(webapi: &Arc<Mutex<WebApi>>) -> Option<String> {
let token = {
let mut w = webapi.lock().ok()?;
match w.valid_token() {
Ok(t) => t,
Err(_) => w.cached_token(),
}
};
(!token.is_empty()).then_some(token)
}
pub(crate) const API: &str = "https://api.spotify.com/v1";
pub(crate) fn retry_delay(retry_after: Option<u64>) -> Option<Duration> {
match retry_after.unwrap_or(3) {
secs if secs <= 5 => Some(Duration::from_secs(secs + 1)),
_ => None,
}
}
pub(crate) fn get_json(
client: &reqwest::blocking::Client,
url: &str,
token: &str,
) -> Option<serde_json::Value> {
for _ in 0..5 {
let resp = match client.get(url).bearer_auth(token).send() {
Ok(r) => r,
Err(e) => {
liblog(format!("api: {url} transport error: {e}"));
return None;
}
};
if resp.status().as_u16() == 429 {
let after = resp
.headers()
.get("retry-after")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse::<u64>().ok());
let Some(wait) = retry_delay(after) else {
liblog(format!(
"api: {url} -> 429, retry-after {after:?}s, giving up"
));
return None;
};
std::thread::sleep(wait);
continue;
}
if !resp.status().is_success() {
liblog(format!("api: {url} -> HTTP {}", resp.status().as_u16()));
return None;
}
return resp.json::<serde_json::Value>().ok();
}
None
}
pub(crate) const CATALOGUE_TTL: Duration = Duration::from_secs(24 * 60 * 60);
pub(crate) fn get_json_cached(
client: &reqwest::blocking::Client,
url: &str,
token: &str,
) -> Option<serde_json::Value> {
if let Some(body) = myx::httpcache::get(url, Some(CATALOGUE_TTL)) {
return serde_json::from_str(&body).ok();
}
match get_json(client, url, token) {
Some(v) => {
myx::httpcache::put(url, &v.to_string());
Some(v)
}
None => {
let stale = myx::httpcache::get(url, None)?;
liblog(format!("api: {url} failed; serving cached copy"));
serde_json::from_str(&stale).ok()
}
}
}
pub(crate) fn fetch_cover(client: &reqwest::blocking::Client, url: &str) -> Option<Vec<u8>> {
if let Some(bytes) = myx::httpcache::get_bytes(url) {
return Some(bytes);
}
let resp = client.get(url).send().ok()?;
if !resp.status().is_success() {
liblog(format!("cover: {url} -> HTTP {}", resp.status().as_u16()));
return None;
}
let bytes = resp.bytes().ok()?.to_vec();
myx::httpcache::put_bytes(url, &bytes);
Some(bytes)
}
pub(crate) fn http_client() -> reqwest::blocking::Client {
reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(10))
.build()
.unwrap_or_default()
}