use std::{ fs::{create_dir_all, read_to_string, File}, io::Write, path::Path, }; use async_trait::async_trait; pub use clap_serde_derive::{self, clap, serde, ClapSerde}; use proto::crabidy::{LibraryNode, LibraryNodeChild, Track}; pub mod proto; /// A media provider addressed like a file system. /// /// Every node and track has a `/`-separated absolute path whose first /// segment names the provider, e.g. `/tidal/playlists//`. /// The path encodes the position in the library tree: ancestors are /// obtained by trimming trailing segments. #[async_trait] pub trait ProviderClient: std::fmt::Debug + Send + Sync { async fn init(raw_toml_settings: &str) -> Result where Self: Sized; fn settings(&self) -> String; /// Whether the path addresses a single track (as opposed to a node). fn is_track_path(&self, path: &str) -> bool; async fn get_urls_for_track(&self, track_path: &str) -> Result, ProviderError>; async fn get_metadata_for_track(&self, track_path: &str) -> Result; fn get_lib_root(&self) -> LibraryNode; async fn get_lib_node(&self, path: &str) -> Result; } #[derive(Clone, Debug, Hash)] pub enum ProviderError { Config(String), UnknownUser, CouldNotLogin, FetchError, MalformedPath, InternalError, Other, } impl std::fmt::Display for ProviderError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self) } } impl std::error::Error for ProviderError {} /// The path of the global library root. pub const ROOT_PATH: &str = "/"; /// Returns the parent path, or `None` when at the root. /// /// `/tidal/playlists/abc` -> `/tidal/playlists` -> `/tidal` -> `/`. pub fn parent_path(path: &str) -> Option<&str> { let trimmed = path.trim_end_matches('/'); if trimmed.is_empty() { return None; } match trimmed.rfind('/') { Some(0) => Some(ROOT_PATH), Some(idx) => Some(&trimmed[..idx]), None => None, } } /// Appends a segment to a path. pub fn join_path(base: &str, segment: &str) -> String { let base = base.trim_end_matches('/'); format!("{base}/{segment}") } /// Splits a path into its segments: `/tidal/playlists/x` -> ["tidal", "playlists", "x"]. pub fn path_segments(path: &str) -> Vec<&str> { path.split('/').filter(|s| !s.is_empty()).collect() } impl LibraryNode { pub fn new() -> Self { Self { path: ROOT_PATH.to_string(), title: "/".to_string(), children: Vec::new(), parent: None, tracks: Vec::new(), is_queable: false, } } } impl LibraryNodeChild { pub fn new(path: String, title: String, is_queable: bool) -> Self { Self { path, title, is_queable, } } } #[cfg(test)] mod tests { use super::*; #[test] fn parent_path_walks_up_to_root() { assert_eq!( parent_path("/tidal/playlists/abc"), Some("/tidal/playlists") ); assert_eq!(parent_path("/tidal/playlists"), Some("/tidal")); assert_eq!(parent_path("/tidal"), Some("/")); assert_eq!(parent_path("/"), None); } #[test] fn join_path_appends_segments() { assert_eq!(join_path("/", "tidal"), "/tidal"); assert_eq!(join_path("/tidal", "playlists"), "/tidal/playlists"); } #[test] fn path_segments_splits() { assert_eq!(path_segments("/"), Vec::<&str>::new()); assert_eq!( path_segments("/tidal/artists/1/2"), vec!["tidal", "artists", "1", "2"] ); } } pub enum QueueError { NotQueable, } pub fn init_config(config_file_name: &str) -> T where T: Default + ClapSerde + serde::Serialize + std::fmt::Debug, { if let Some(config_dir) = dirs::config_dir() { let dir = Path::new(&config_dir).join("crabidy"); if !dir.is_dir() { create_dir_all(&dir).expect("Could not create crabidy config directory"); } let config_file_path = dir.join(config_file_name); if !config_file_path.is_file() { let config = T::default().merge_clap(); let content = toml::to_string_pretty(&config).expect("Could not serialize config"); let mut config_file = File::create(config_file_path).expect("Could not open config file for writing"); config_file .write_all(content.as_bytes()) .expect("Failed to write to file"); return config; } else { let content = read_to_string(config_file_path).expect("Could not read config file"); let parsed = toml::from_str::<::Opt>(&content).unwrap(); let config: T = T::from(parsed).merge_clap(); return config; } } T::default().merge_clap() }