spotify_cli/cache/
pins.rs

1use std::fs;
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5
6use crate::domain::pin::PinnedPlaylist;
7use crate::error::Result;
8
9/// JSON-backed pin store for local playlist shortcuts.
10#[derive(Debug, Clone)]
11pub struct PinStore {
12    path: PathBuf,
13}
14
15impl PinStore {
16    pub fn new(path: PathBuf) -> Self {
17        Self { path }
18    }
19
20    pub fn load(&self) -> Result<Pins> {
21        if !self.path.exists() {
22            return Ok(Pins::default());
23        }
24        let contents = fs::read_to_string(&self.path)?;
25        let pins = serde_json::from_str(&contents)?;
26        Ok(pins)
27    }
28
29    pub fn save(&self, pins: &Pins) -> Result<()> {
30        let payload = serde_json::to_string_pretty(pins)?;
31        fs::write(&self.path, payload)?;
32        Ok(())
33    }
34
35    pub fn add(&self, name: String, url: String) -> Result<()> {
36        let mut pins = self.load()?;
37        let lower = name.to_lowercase();
38        if let Some(existing) = pins
39            .items
40            .iter_mut()
41            .find(|item| item.name.to_lowercase() == lower)
42        {
43            existing.url = url;
44            existing.name = name;
45        } else {
46            pins.items.push(PinnedPlaylist { name, url });
47        }
48        self.save(&pins)
49    }
50
51    pub fn remove(&self, name: &str) -> Result<bool> {
52        let mut pins = self.load()?;
53        let before = pins.items.len();
54        let lower = name.to_lowercase();
55        pins.items.retain(|item| item.name.to_lowercase() != lower);
56        let removed = pins.items.len() != before;
57        if removed {
58            self.save(&pins)?;
59        }
60        Ok(removed)
61    }
62}
63
64/// Pin collection payload.
65#[derive(Debug, Clone, Serialize, Deserialize, Default)]
66pub struct Pins {
67    pub items: Vec<PinnedPlaylist>,
68}
69
70#[cfg(test)]
71mod tests {
72    use super::PinStore;
73    use std::fs;
74    use std::path::PathBuf;
75
76    fn temp_path(name: &str) -> PathBuf {
77        let mut path = std::env::temp_dir();
78        let stamp = std::time::SystemTime::now()
79            .duration_since(std::time::UNIX_EPOCH)
80            .unwrap()
81            .as_nanos();
82        path.push(format!("spotify-cli-{name}-{stamp}.json"));
83        path
84    }
85
86    #[test]
87    fn pin_store_add_and_remove() {
88        let path = temp_path("pins");
89        let store = PinStore::new(path.clone());
90
91        store
92            .add(
93                "Release Radar".to_string(),
94                "https://example.com".to_string(),
95            )
96            .unwrap();
97        let loaded = store.load().unwrap();
98        assert_eq!(loaded.items.len(), 1);
99
100        let removed = store.remove("Release Radar").unwrap();
101        assert!(removed);
102        let loaded = store.load().unwrap();
103        assert!(loaded.items.is_empty());
104
105        let _ = fs::remove_file(path);
106    }
107}