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