diff --git a/psst-core/src/cache.rs b/psst-core/src/cache.rs index 739253a4..b02cfd64 100644 --- a/psst-core/src/cache.rs +++ b/psst-core/src/cache.rs @@ -21,10 +21,10 @@ pub struct Cache { fn create_cache_dirs(base: &Path) -> io::Result<()> { mkdir_if_not_exists(base)?; - mkdir_if_not_exists(&base.join("track"))?; - mkdir_if_not_exists(&base.join("episode"))?; + mkdir_if_not_exists(&base.join("tracks"))?; + mkdir_if_not_exists(&base.join("episodes"))?; mkdir_if_not_exists(&base.join("audio"))?; - mkdir_if_not_exists(&base.join("key"))?; + mkdir_if_not_exists(&base.join("keys"))?; Ok(()) } @@ -71,7 +71,7 @@ impl Cache { } fn track_path(&self, item_id: ItemId) -> PathBuf { - self.base.join("track").join(item_id.to_base62()) + self.base.join("tracks").join(item_id.to_base62()) } } @@ -89,7 +89,7 @@ impl Cache { } fn episode_path(&self, item_id: ItemId) -> PathBuf { - self.base.join("episode").join(item_id.to_base62()) + self.base.join("episodes").join(item_id.to_base62()) } } @@ -115,7 +115,7 @@ impl Cache { let mut key_id = String::new(); key_id += &item_id.to_base62()[..16]; key_id += &file_id.to_base16()[..16]; - self.base.join("key").join(key_id) + self.base.join("keys").join(key_id) } } diff --git a/psst-gui/src/data/config.rs b/psst-gui/src/data/config.rs index 639fb98c..68755663 100644 --- a/psst-gui/src/data/config.rs +++ b/psst-gui/src/data/config.rs @@ -8,8 +8,8 @@ use std::{ #[cfg(target_family = "unix")] use std::os::unix::fs::OpenOptionsExt; +use directories::ProjectDirs; use druid::{Data, Lens, Size}; -use platform_dirs::AppDirs; use psst_core::{ cache::{mkdir_if_not_exists, CacheHandle}, connection::Credentials, @@ -153,25 +153,23 @@ impl Default for Config { } impl Config { - fn app_dirs() -> Option { - const USE_XDG_ON_MACOS: bool = false; - - AppDirs::new(Some(APP_NAME), USE_XDG_ON_MACOS) + fn app_dirs() -> Option { + ProjectDirs::from("", "", APP_NAME) } pub fn spotify_local_files_file(username: &str) -> Option { - AppDirs::new(Some("spotify"), false).map(|dir| { + ProjectDirs::from("", "", "spotify").map(|dir| { let path = format!("Users/{username}-user/local-files.bnk"); - dir.config_dir.join(path) + dir.config_dir().join(path) }) } pub fn cache_dir() -> Option { - Self::app_dirs().map(|dirs| dirs.cache_dir) + Self::app_dirs().map(|dirs| dirs.cache_dir().to_path_buf()) } pub fn config_dir() -> Option { - Self::app_dirs().map(|dirs| dirs.config_dir) + Self::app_dirs().map(|dirs| dirs.config_dir().to_path_buf()) } fn config_path() -> Option { @@ -179,6 +177,8 @@ impl Config { } pub fn load() -> Option { + // To be removed... at some point soon! + crate::data::migration::perform_migration(); let path = Self::config_path().expect("Failed to get config path"); if let Ok(file) = File::open(&path) { log::info!("loading config: {:?}", &path); diff --git a/psst-gui/src/data/migration.rs b/psst-gui/src/data/migration.rs new file mode 100644 index 00000000..7df6451e --- /dev/null +++ b/psst-gui/src/data/migration.rs @@ -0,0 +1,109 @@ +use std::{fs, path::Path}; + +use directories::ProjectDirs; +use platform_dirs::AppDirs; + +use crate::data::config::Config; + +const APP_NAME: &str = "Psst"; + +pub fn perform_migration() { + let old_app_dirs = AppDirs::new(Some(APP_NAME), false); + let new_project_dirs = ProjectDirs::from("", "", APP_NAME); + + if let (Some(old_dirs), Some(new_dirs)) = (old_app_dirs, new_project_dirs) { + migrate_path( + &old_dirs.config_dir, + new_dirs.config_dir(), + &["config.json"], + ); + migrate_path( + &old_dirs.cache_dir, + new_dirs.cache_dir(), + &[ + "tracks", + "track", + "episodes", + "episode", + "audio", + "keys", + "key", + "lyrics", + "images", + "artist-info", + "related-artists", + "album", + "show", + "user-info", + ], + ); + } + + if let Some(active_cache_dir) = Config::cache_dir() { + rename_cache_subdirectories(&active_cache_dir); + } +} + +fn migrate_path(old_dir: &Path, new_dir: &Path, items_to_move: &[&str]) { + if old_dir == new_dir || !old_dir.exists() { + return; + } + + // New path is a subdirectory of the old path (e.g. .../Psst/config vs .../Psst). + // We must move specific items into the new subdirectory to avoid recursion. + if new_dir.starts_with(old_dir) { + log::info!( + "migrating content from {:?} into subdirectory {:?}", + old_dir, + new_dir + ); + if let Err(err) = fs::create_dir_all(new_dir) { + log::error!("failed to create directory {:?}: {}", new_dir, err); + return; + } + + for &item in items_to_move { + let old_item = old_dir.join(item); + let new_item = new_dir.join(item); + move_if_exists(&old_item, &new_item); + } + } else if !new_dir.exists() { + log::info!("migrating directory from {:?} to {:?}", old_dir, new_dir); + if let Some(parent) = new_dir.parent() { + let _ = fs::create_dir_all(parent); + } + if let Err(err) = fs::rename(old_dir, new_dir) { + log::error!("failed to migrate directory: {}", err); + } + } +} + +fn rename_cache_subdirectories(cache_dir: &Path) { + if !cache_dir.exists() { + return; + } + + let renames = [ + ("track", "tracks"), + ("episode", "episodes"), + ("show", "shows"), + ("album", "albums"), + ("artist", "artists"), + ("key", "keys"), + ]; + + for (old_name, new_name) in renames { + let old_path = cache_dir.join(old_name); + let new_path = cache_dir.join(new_name); + move_if_exists(&old_path, &new_path); + } +} + +fn move_if_exists(from: &Path, to: &Path) { + if from.exists() && !to.exists() { + log::info!("moving {:?} to {:?}", from, to); + if let Err(err) = fs::rename(from, to) { + log::error!("failed to move {:?}: {}", from, err); + } + } +} diff --git a/psst-gui/src/data/mod.rs b/psst-gui/src/data/mod.rs index 047e43a9..c206e7df 100644 --- a/psst-gui/src/data/mod.rs +++ b/psst-gui/src/data/mod.rs @@ -4,6 +4,7 @@ pub mod config; mod ctx; mod find; mod id; +mod migration; mod nav; mod playback; mod playlist; diff --git a/psst-gui/src/webapi/client.rs b/psst-gui/src/webapi/client.rs index f104d353..3516d5a8 100644 --- a/psst-gui/src/webapi/client.rs +++ b/psst-gui/src/webapi/client.rs @@ -21,7 +21,7 @@ use parking_lot::Mutex; use serde::{de::DeserializeOwned, Deserialize}; use serde_json::json; -use psst_core::session::{SessionService}; +use psst_core::session::SessionService; use ureq::{ http::{Response, StatusCode}, Agent, Body, @@ -39,9 +39,9 @@ use crate::{ }; use super::{cache::WebApiCache, local::LocalTrackManager}; +use psst_core::session::login5::Login5; use sanitize_html::rules::predefined::DEFAULT; use sanitize_html::sanitize_str; -use psst_core::session::login5::Login5; pub struct WebApi { session: SessionService, @@ -711,7 +711,7 @@ impl WebApi { // https://developer.spotify.com/documentation/web-api/reference/get-artist/ pub fn get_artist(&self, id: &str) -> Result { let request = &RequestBuilder::new(format!("v1/artists/{id}"), Method::Get, None); - let result = self.load_cached(request, "artist", id)?; + let result = self.load_cached(request, "artists", id)?; Ok(result.data) } @@ -909,7 +909,7 @@ impl WebApi { pub fn get_album(&self, id: &str) -> Result>, Error> { let request = &RequestBuilder::new(format!("v1/albums/{id}"), Method::Get, None) .query("market", "from_token"); - let result = self.load_cached(request, "album", id)?; + let result = self.load_cached(request, "albums", id)?; Ok(result) } } @@ -921,7 +921,7 @@ impl WebApi { let request = &RequestBuilder::new(format!("v1/shows/{id}"), Method::Get, None) .query("market", "from_token"); - let result = self.load_cached(request, "show", id)?; + let result = self.load_cached(request, "shows", id)?; Ok(result) } @@ -1043,8 +1043,7 @@ impl WebApi { // https://developer.spotify.com/documentation/web-api/reference/remove-albums-user/ pub fn unsave_album(&self, id: &str) -> Result<(), Error> { - let request = - &RequestBuilder::new("v1/me/albums", Method::Delete, None).query("ids", id); + let request = &RequestBuilder::new("v1/me/albums", Method::Delete, None).query("ids", id); self.send_empty_json(request) } @@ -1221,11 +1220,8 @@ impl WebApi { } pub fn unfollow_playlist(&self, id: &str) -> Result<(), Error> { - let request = &RequestBuilder::new( - format!("v1/playlists/{id}/followers"), - Method::Delete, - None, - ); + let request = + &RequestBuilder::new(format!("v1/playlists/{id}/followers"), Method::Delete, None); self.request(request)?; Ok(()) } @@ -1254,10 +1250,9 @@ impl WebApi { Json(serde_json::Value), } - let request = - &RequestBuilder::new(format!("v1/playlists/{id}/tracks"), Method::Get, None) - .query("marker", "from_token") - .query("additional_types", "track"); + let request = &RequestBuilder::new(format!("v1/playlists/{id}/tracks"), Method::Get, None) + .query("marker", "from_token") + .query("additional_types", "track"); let result: Vector = self.load_all_pages(request)?; @@ -1278,9 +1273,8 @@ impl WebApi { } pub fn change_playlist_details(&self, id: &str, name: &str) -> Result<(), Error> { - let request = - &RequestBuilder::new(format!("v1/playlists/{id}/tracks"), Method::Get, None) - .set_body(Some(json!({ "name": name }))); + let request = &RequestBuilder::new(format!("v1/playlists/{id}/tracks"), Method::Get, None) + .set_body(Some(json!({ "name": name }))); self.request(request)?; Ok(()) }