koan_core/remote/
profile.rs1use parking_lot::Mutex;
10use serde::{Deserialize, Serialize};
11
12use crate::remote::client::SubsonicAuth;
13
14pub const LINK: &str = "koanLink";
17pub const DEVICES: &str = "koanDevices";
21
22#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
23pub struct ServerProfile {
24 pub kind: Option<String>,
27 pub version: Option<String>,
28 pub open_subsonic: bool,
29 pub extensions: Vec<(String, Vec<i64>)>,
31}
32
33impl ServerProfile {
34 pub fn offers(&self, extension: &str) -> bool {
35 self.extensions.iter().any(|(name, _)| name == extension)
36 }
37
38 pub fn links(&self) -> bool {
41 self.offers(LINK) || self.kind.as_deref() == Some("koan")
42 }
43}
44
45static PROFILE: Mutex<Option<(SubsonicAuth, ServerProfile, bool)>> = Mutex::new(None);
48
49pub fn for_auth(auth: &SubsonicAuth) -> Option<ServerProfile> {
52 if let Some((held, profile, true)) = PROFILE.lock().as_ref()
53 && held == auth
54 {
55 return Some(profile.clone());
56 }
57 let client = crate::remote::client::SubsonicClient::from_auth(auth.clone());
58 let profile = match client.profile() {
59 Ok(p) => p,
60 Err(e) => {
61 log::info!("profile: {} did not answer: {e}", auth.base_url);
62 return None;
63 }
64 };
65 log::info!(
66 "profile: {} is {} {} ({} extensions)",
67 auth.base_url,
68 profile.kind.as_deref().unwrap_or("a Subsonic server"),
69 profile.version.as_deref().unwrap_or(""),
70 profile.extensions.len()
71 );
72 *PROFILE.lock() = Some((auth.clone(), profile.clone(), true));
73 crate::remote::devices::touch();
74 Some(profile)
75}
76
77pub fn forget() {
80 if let Some(held) = PROFILE.lock().as_mut() {
81 held.2 = false;
82 }
83}
84
85pub fn current() -> Option<ServerProfile> {
87 let cfg = crate::config::Config::load().unwrap_or_default();
88 let auth = crate::helpers::subsonic_auth(&cfg)?;
89 PROFILE
90 .lock()
91 .as_ref()
92 .filter(|(held, _, _)| *held == auth)
93 .map(|(_, p, _)| p.clone())
94}
95
96pub fn is_koan(auth: &SubsonicAuth) -> bool {
98 for_auth(auth).is_some_and(|p| p.kind.as_deref() == Some("koan"))
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn an_older_koan_server_still_links() {
107 let old = ServerProfile {
108 kind: Some("koan".into()),
109 open_subsonic: true,
110 ..Default::default()
111 };
112 assert!(old.links());
113 assert!(!old.offers(DEVICES));
114 let hub = ServerProfile {
115 kind: Some("navidrome".into()),
116 extensions: vec![(LINK.into(), vec![1]), (DEVICES.into(), vec![1])],
117 ..Default::default()
118 };
119 assert!(hub.links() && hub.offers(DEVICES));
120 assert!(!ServerProfile::default().links());
121 }
122}