Browse and listen to thousands of radio stations across the globe right from your terminal ๐ŸŒŽ ๐Ÿ“ป ๐ŸŽตโœจ
radio rust tokio web-radio command-line-tool tui
at main 3.6 kB view raw
1use std::fs; 2use std::path::{Path, PathBuf}; 3 4use anyhow::{Context, Error}; 5use directories::ProjectDirs; 6use serde::{Deserialize, Serialize}; 7 8/// Metadata describing a favourited station. 9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 10pub struct FavoriteStation { 11 pub id: String, 12 pub name: String, 13 pub provider: String, 14} 15 16/// File-backed favourites store. 17pub struct FavoritesStore { 18 path: PathBuf, 19 favorites: Vec<FavoriteStation>, 20} 21 22impl FavoritesStore { 23 /// Load favourites from disk, falling back to an empty list when the file 24 /// does not exist or is corrupted. 25 pub fn load() -> Result<Self, Error> { 26 let path = favorites_path()?; 27 ensure_parent(&path)?; 28 29 let favorites = match fs::read_to_string(&path) { 30 Ok(content) => match serde_json::from_str::<Vec<FavoriteStation>>(&content) { 31 Ok(entries) => entries, 32 Err(err) => { 33 eprintln!( 34 "warning: favourites file corrupted ({}), starting fresh", 35 err 36 ); 37 Vec::new() 38 } 39 }, 40 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Vec::new(), 41 Err(err) => return Err(Error::from(err).context("failed to read favourites file")), 42 }; 43 44 Ok(Self { path, favorites }) 45 } 46 47 /// Return a snapshot of all favourite stations. 48 pub fn all(&self) -> &[FavoriteStation] { 49 &self.favorites 50 } 51 52 /// Check whether the provided station is already a favourite. 53 pub fn is_favorite(&self, id: &str, provider: &str) -> bool { 54 self.favorites 55 .iter() 56 .any(|fav| fav.id == id && fav.provider == provider) 57 } 58 59 /// Add a station to favourites if it is not already present. 60 pub fn add(&mut self, favorite: FavoriteStation) -> Result<(), Error> { 61 if !self.is_favorite(&favorite.id, &favorite.provider) { 62 self.favorites.push(favorite); 63 self.save()?; 64 } 65 Ok(()) 66 } 67 68 /// Remove a station from favourites. 69 pub fn remove(&mut self, id: &str, provider: &str) -> Result<(), Error> { 70 let initial_len = self.favorites.len(); 71 self.favorites 72 .retain(|fav| !(fav.id == id && fav.provider == provider)); 73 if self.favorites.len() != initial_len { 74 self.save()?; 75 } 76 Ok(()) 77 } 78 79 /// Toggle a station in favourites, returning whether it was added (`true`) or removed (`false`). 80 pub fn toggle(&mut self, favorite: FavoriteStation) -> Result<bool, Error> { 81 if self.is_favorite(&favorite.id, &favorite.provider) { 82 self.remove(&favorite.id, &favorite.provider)?; 83 Ok(false) 84 } else { 85 self.add(favorite)?; 86 Ok(true) 87 } 88 } 89 90 fn save(&self) -> Result<(), Error> { 91 let serialized = serde_json::to_string_pretty(&self.favorites) 92 .context("failed to serialize favourites list")?; 93 fs::write(&self.path, serialized).context("failed to write favourites file") 94 } 95} 96 97fn favorites_path() -> Result<PathBuf, Error> { 98 let dirs = ProjectDirs::from("io", "tunein-cli", "tunein-cli") 99 .ok_or_else(|| Error::msg("unable to determine configuration directory"))?; 100 Ok(dirs.config_dir().join("favorites.json")) 101} 102 103fn ensure_parent(path: &Path) -> Result<(), Error> { 104 if let Some(parent) = path.parent() { 105 fs::create_dir_all(parent).context("failed to create favourites directory")?; 106 } 107 Ok(()) 108}