Skip to main content

config/
source.rs

1use serde::{Deserialize, Serialize};
2
3/// Where a track/playlist/favorite comes from, and what the app is currently
4/// sourcing from: the local library, or a specific media server.
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
6pub enum Source {
7    #[default]
8    Local,
9    Server(String),
10}
11
12impl Source {
13    /// The `source` column value: `"local"` or the server id.
14    pub fn as_str(&self) -> &str {
15        match self {
16            Source::Local => "local",
17            Source::Server(id) => id.as_str(),
18        }
19    }
20
21    /// Build from a stored `source` column value.
22    pub fn from_column(s: &str) -> Self {
23        if s == "local" {
24            Source::Local
25        } else {
26            Source::Server(s.to_owned())
27        }
28    }
29
30    /// The server id, if this is a server source.
31    pub fn server_id(&self) -> Option<&str> {
32        match self {
33            Source::Server(id) => Some(id),
34            Source::Local => None,
35        }
36    }
37}
38
39#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
40pub enum MusicService {
41    #[default]
42    Jellyfin,
43    #[serde(alias = "Navidrome")]
44    Subsonic,
45    Custom,
46    YtMusic,
47    SoundCloud,
48}
49
50impl MusicService {
51    pub fn display_name(&self) -> &'static str {
52        match self {
53            Self::Jellyfin => "Jellyfin",
54            Self::Subsonic => "Subsonic",
55            Self::Custom => "Custom",
56            Self::YtMusic => "YouTube Music",
57            Self::SoundCloud => "SoundCloud",
58        }
59    }
60
61    /// Backends that authenticate via a browser sign-in window (OAuth/cookies)
62    /// rather than a URL + username/password form.
63    pub fn uses_browser_signin(&self) -> bool {
64        matches!(self, Self::YtMusic | Self::SoundCloud)
65    }
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
69pub struct MusicServer {
70    pub name: String,
71    pub url: String,
72    #[serde(default)]
73    pub service: MusicService,
74    pub access_token: Option<String>,
75    pub user_id: Option<String>,
76    #[serde(default)]
77    pub id: Option<String>,
78    /// For browser sign-in services: which Chromium-family browser was used.
79    #[serde(default)]
80    pub yt_browser: Option<Browser>,
81    /// For `MusicService::YtMusic` only: anonymous mode.
82    #[serde(default)]
83    pub yt_anonymous: bool,
84}
85
86impl MusicServer {
87    pub fn new(name: String, url: String) -> Self {
88        Self::new_with_service(name, url, MusicService::Jellyfin)
89    }
90
91    pub fn new_with_service(name: String, url: String, service: MusicService) -> Self {
92        Self {
93            name,
94            url: url.trim_end_matches('/').to_string(),
95            service,
96            access_token: None,
97            user_id: None,
98            id: Some(uuid::Uuid::new_v4().to_string()),
99            yt_browser: None,
100            yt_anonymous: false,
101        }
102    }
103
104    pub fn yt_browser(&self) -> Option<Browser> {
105        self.yt_browser
106    }
107}
108
109impl Default for MusicServer {
110    fn default() -> Self {
111        Self {
112            name: String::new(),
113            url: String::new(),
114            service: MusicService::Jellyfin,
115            access_token: None,
116            user_id: None,
117            id: None,
118            yt_browser: None,
119            yt_anonymous: false,
120        }
121    }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
125#[serde(rename_all = "lowercase")]
126pub enum Browser {
127    Chrome,
128    Chromium,
129    Brave,
130    Edge,
131    Vivaldi,
132}
133
134impl Browser {
135    pub const ALL: &'static [Browser] = &[
136        Browser::Chrome,
137        Browser::Chromium,
138        Browser::Brave,
139        Browser::Edge,
140        Browser::Vivaldi,
141    ];
142
143    /// The stable id used in URL routes, settings UI option values,
144    /// libsecret lookups, etc.
145    pub fn id(self) -> &'static str {
146        match self {
147            Browser::Chrome => "chrome",
148            Browser::Chromium => "chromium",
149            Browser::Brave => "brave",
150            Browser::Edge => "edge",
151            Browser::Vivaldi => "vivaldi",
152        }
153    }
154
155    pub fn label(self) -> &'static str {
156        match self {
157            Browser::Chrome => "Chrome",
158            Browser::Chromium => "Chromium",
159            Browser::Brave => "Brave",
160            Browser::Edge => "Edge",
161            Browser::Vivaldi => "Vivaldi",
162        }
163    }
164
165    pub fn from_id(s: &str) -> Option<Browser> {
166        Browser::ALL
167            .iter()
168            .copied()
169            .find(|browser| browser.id() == s)
170    }
171}
172
173impl std::fmt::Display for Browser {
174    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175        f.write_str(self.label())
176    }
177}
178
179pub type JellyfinServer = MusicServer;
180
181#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
182pub struct SavedServer {
183    pub id: String,
184    pub name: String,
185    pub url: String,
186    #[serde(default)]
187    pub service: MusicService,
188    /// Persisted browser choice for browser sign-in servers.
189    #[serde(default)]
190    pub yt_browser: Option<Browser>,
191    /// Persisted anonymous-mode flag.
192    #[serde(default)]
193    pub yt_anonymous: bool,
194}
195
196impl SavedServer {
197    pub fn new(name: String, url: String, service: MusicService) -> Self {
198        Self {
199            id: uuid::Uuid::new_v4().to_string(),
200            name,
201            url: url.trim_end_matches('/').to_string(),
202            service,
203            yt_browser: None,
204            yt_anonymous: false,
205        }
206    }
207
208    pub fn from_music_server(server: &MusicServer) -> Self {
209        Self {
210            id: server
211                .id
212                .clone()
213                .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
214            name: server.name.clone(),
215            url: server.url.clone(),
216            service: server.service,
217            yt_browser: server.yt_browser,
218            yt_anonymous: server.yt_anonymous,
219        }
220    }
221
222    pub fn matches(&self, server: &MusicServer) -> bool {
223        if let Some(sid) = server.id.as_ref()
224            && sid == &self.id
225        {
226            return true;
227        }
228        self.url == server.url && self.service == server.service
229    }
230}