use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use crate::remote::client::SubsonicAuth;
pub const LINK: &str = "koanLink";
pub const DEVICES: &str = "koanDevices";
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServerProfile {
pub kind: Option<String>,
pub version: Option<String>,
pub open_subsonic: bool,
pub extensions: Vec<(String, Vec<i64>)>,
}
impl ServerProfile {
pub fn offers(&self, extension: &str) -> bool {
self.extensions.iter().any(|(name, _)| name == extension)
}
pub fn links(&self) -> bool {
self.offers(LINK) || self.kind.as_deref() == Some("koan")
}
}
static PROFILE: Mutex<Option<(SubsonicAuth, ServerProfile, bool)>> = Mutex::new(None);
pub fn for_auth(auth: &SubsonicAuth) -> Option<ServerProfile> {
if let Some((held, profile, true)) = PROFILE.lock().as_ref()
&& held == auth
{
return Some(profile.clone());
}
let client = crate::remote::client::SubsonicClient::from_auth(auth.clone());
let profile = match client.profile() {
Ok(p) => p,
Err(e) => {
log::info!("profile: {} did not answer: {e}", auth.base_url);
return None;
}
};
log::info!(
"profile: {} is {} {} ({} extensions)",
auth.base_url,
profile.kind.as_deref().unwrap_or("a Subsonic server"),
profile.version.as_deref().unwrap_or(""),
profile.extensions.len()
);
*PROFILE.lock() = Some((auth.clone(), profile.clone(), true));
crate::remote::devices::touch();
Some(profile)
}
pub fn forget() {
if let Some(held) = PROFILE.lock().as_mut() {
held.2 = false;
}
}
pub fn current() -> Option<ServerProfile> {
let cfg = crate::config::Config::load().unwrap_or_default();
let auth = crate::helpers::subsonic_auth(&cfg)?;
PROFILE
.lock()
.as_ref()
.filter(|(held, _, _)| *held == auth)
.map(|(_, p, _)| p.clone())
}
pub fn is_koan(auth: &SubsonicAuth) -> bool {
for_auth(auth).is_some_and(|p| p.kind.as_deref() == Some("koan"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_older_koan_server_still_links() {
let old = ServerProfile {
kind: Some("koan".into()),
open_subsonic: true,
..Default::default()
};
assert!(old.links());
assert!(!old.offers(DEVICES));
let hub = ServerProfile {
kind: Some("navidrome".into()),
extensions: vec![(LINK.into(), vec![1]), (DEVICES.into(), vec![1])],
..Default::default()
};
assert!(hub.links() && hub.offers(DEVICES));
assert!(!ServerProfile::default().links());
}
}