spotify_cli/cache/
metadata.rs1use std::fs;
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5
6use crate::domain::settings::Settings;
7use crate::error::Result;
8
9#[derive(Debug, Clone, Serialize, Deserialize, Default)]
11pub struct Metadata {
12 pub auth: Option<AuthTokenCache>,
13 pub client: Option<ClientIdentity>,
14 #[serde(default)]
15 pub settings: Settings,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct AuthTokenCache {
21 pub access_token: String,
22 pub refresh_token: Option<String>,
23 pub expires_at: Option<u64>,
24 #[serde(default)]
25 pub granted_scopes: Option<Vec<String>>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ClientIdentity {
31 pub client_id: String,
32}
33
34#[derive(Debug, Clone)]
36pub struct MetadataStore {
37 path: PathBuf,
38}
39
40impl MetadataStore {
41 pub fn new(path: PathBuf) -> Self {
42 Self { path }
43 }
44
45 pub fn load(&self) -> Result<Metadata> {
46 if !self.path.exists() {
47 return Ok(Metadata::default());
48 }
49
50 let contents = fs::read_to_string(&self.path)?;
51 let metadata = serde_json::from_str(&contents)?;
52 Ok(metadata)
53 }
54
55 pub fn save(&self, metadata: &Metadata) -> Result<()> {
56 let payload = serde_json::to_string_pretty(metadata)?;
57 fs::write(&self.path, payload)?;
58 #[cfg(unix)]
59 {
60 use std::os::unix::fs::PermissionsExt;
61 let _ = fs::set_permissions(&self.path, fs::Permissions::from_mode(0o600));
62 }
63 Ok(())
64 }
65}
66
67#[cfg(test)]
68mod tests {
69 use super::{AuthTokenCache, Metadata, MetadataStore};
70 use crate::domain::settings::Settings;
71 use std::fs;
72 use std::path::PathBuf;
73
74 fn temp_path(name: &str) -> PathBuf {
75 let mut path = std::env::temp_dir();
76 let stamp = std::time::SystemTime::now()
77 .duration_since(std::time::UNIX_EPOCH)
78 .unwrap()
79 .as_nanos();
80 path.push(format!("spotify-cli-{name}-{stamp}.json"));
81 path
82 }
83
84 #[test]
85 fn metadata_store_round_trip() {
86 let path = temp_path("metadata-store");
87 let store = MetadataStore::new(path.clone());
88 let metadata = Metadata {
89 auth: Some(AuthTokenCache {
90 access_token: "token".to_string(),
91 refresh_token: None,
92 expires_at: Some(1),
93 granted_scopes: Some(vec!["user-read-private".to_string()]),
94 }),
95 client: None,
96 settings: Settings {
97 country: Some("AU".to_string()),
98 user_name: Some("Me".to_string()),
99 },
100 };
101 store.save(&metadata).expect("save");
102 let loaded = store.load().expect("load");
103 assert_eq!(loaded.settings.country.as_deref(), Some("AU"));
104 assert_eq!(loaded.settings.user_name.as_deref(), Some("Me"));
105 let _ = fs::remove_file(path);
106 }
107}