use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use crate::remote::client::SubsonicAuth;
pub const LINK: &str = "koanLink";
pub const DEVICES: &str = "koanDevices";
pub const INVITE: &str = "koanInvite";
pub const SHARES: &str = "koanShares";
pub const PAIR: &str = "koanPair";
pub const HISTORY: &str = "koanHistory";
pub const SIGN_IN: &str = "koanSignIn";
pub const PASSWORDS: &str = "koanPasswords";
pub const API_KEYS: &str = "koanApiKeys";
pub const APP_PASSWORDS: &str = "koanAppPasswords";
pub const MCP: &str = "koanMcp";
pub const ACK: &str = "koanAck";
pub const DEVICE_KEYS: &str = "koanDeviceKeys";
pub const SCROBBLING: &str = "koanScrobbling";
pub const DSP_PROFILES: &str = "koanDspProfiles";
pub const DSP_DELETED: &str = "koanDspDeleted";
#[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 probed = client.profile();
crate::remote::refusal::observe(auth, &probed);
let profile = match probed {
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());
}
}