Browse and listen to thousands of radio stations across the globe right from your terminal ๐ ๐ป ๐ตโจ
radio
rust
tokio
web-radio
command-line-tool
tui
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}