diff --git a/.gitignore b/.gitignore index b6aaed4..8ea7b59 100644 --- a/.gitignore +++ b/.gitignore @@ -11,9 +11,6 @@ test-output TestConfig.toml # Runtime artifacts -*.sqlite -**/*.sqlite-shm -**/*.sqlite-wal auth.secret collection.index polaris.log diff --git a/src/app.rs b/src/app.rs index 8500ed4..23a9824 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,7 +1,5 @@ -use std::cmp::min; use std::fs; use std::path::{Path, PathBuf}; -use std::time::Duration; use log::info; use rand::rngs::OsRng; @@ -109,6 +107,8 @@ pub enum Error { InvalidDirectory(String), #[error("The following virtual path could not be mapped to a real path: `{0}`")] CouldNotMapToRealPath(PathBuf), + #[error("The following real path could not be mapped to a virtual path: `{0}`")] + CouldNotMapToVirtualPath(PathBuf), #[error("User not found")] UserNotFound, #[error("Directory not found: {0}")] @@ -275,27 +275,14 @@ impl App { self.config_manager.apply_config(config).await?; self.config_manager.save_config().await?; - self.scanner.try_trigger_scan(); - let mut wait_seconds = 1; - loop { - tokio::time::sleep(Duration::from_secs(wait_seconds)).await; - if matches!( - self.scanner.get_status().await.state, - scanner::State::UpToDate - ) { - break; - } else { - info!("Migration is waiting for collection scan to finish"); - wait_seconds = min(2 * wait_seconds, 30); - } - } - info!("Migrating playlists"); for (name, owner, songs) in read_legacy_playlists( db_file_path, - self.config_manager.clone(), self.index_manager.clone(), - )? { + self.scanner.clone(), + ) + .await? + { self.playlist_manager .save_playlist(&name, &owner, songs) .await?; diff --git a/src/app/legacy.rs b/src/app/legacy.rs index ae06b58..a07fd25 100644 --- a/src/app/legacy.rs +++ b/src/app/legacy.rs @@ -1,8 +1,12 @@ -use std::{path::PathBuf, str::FromStr}; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, + str::FromStr, +}; use rusqlite::Connection; -use crate::app::{config, index, Error}; +use crate::app::{config, index, scanner, Error}; pub fn read_legacy_auth_secret(db_file_path: &PathBuf) -> Result<[u8; 32], Error> { let connection = Connection::open(db_file_path)?; @@ -18,14 +22,26 @@ pub fn read_legacy_config( ) -> Result<Option<config::storage::Config>, Error> { let connection = Connection::open(db_file_path)?; - // Album art pattern let album_art_pattern: String = connection.query_row( "SELECT index_album_art_pattern FROM misc_settings", [], |row| row.get(0), )?; - // Mount directories + let mount_dirs = read_mount_dirs(db_file_path)?; + let users = read_users(db_file_path)?; + + Ok(Some(config::storage::Config { + album_art_pattern: Some(album_art_pattern), + mount_dirs, + ddns_update_url: None, + users: users.into_values().collect(), + })) +} + +fn read_mount_dirs(db_file_path: &PathBuf) -> Result<Vec<config::storage::MountDir>, Error> { + let connection = Connection::open(db_file_path)?; + let mut mount_dirs_statement = connection.prepare("SELECT source, name FROM mount_points")?; let mount_dirs_rows = mount_dirs_statement.query_and_then([], |row| { let source_string = row.get::<_, String>(0)?; @@ -37,40 +53,108 @@ pub fn read_legacy_config( name: row.get::<_, String>(1)?, }) })?; + let mut mount_dirs = vec![]; for mount_dir_result in mount_dirs_rows { mount_dirs.push(mount_dir_result?); } - // Users - let mut users_statement = connection.prepare("SELECT name, password_hash, admin FROM users")?; - let users_rows = users_statement.query_map([], |row| { - Ok(config::storage::User { - name: row.get(0)?, - admin: row.get(2)?, - initial_password: None, - hashed_password: row.get(1)?, - }) - })?; - let mut users = vec![]; - for user_result in users_rows { - users.push(user_result?); - } - - Ok(Some(config::storage::Config { - album_art_pattern: Some(album_art_pattern), - mount_dirs, - ddns_update_url: None, - users, - })) + Ok(mount_dirs) } -pub fn read_legacy_playlists( +fn read_users(db_file_path: &PathBuf) -> Result<HashMap<u32, config::storage::User>, Error> { + let connection = Connection::open(db_file_path)?; + let mut users_statement = + connection.prepare("SELECT id, name, password_hash, admin FROM users")?; + let users_rows = users_statement.query_map([], |row| { + Ok(( + row.get(0)?, + config::storage::User { + name: row.get(1)?, + admin: row.get(3)?, + initial_password: None, + hashed_password: row.get(2)?, + }, + )) + })?; + + let mut users = HashMap::new(); + for users_row in users_rows { + let (id, user) = users_row?; + users.insert(id, user); + } + + Ok(users) +} + +fn virtualize_path( + real_path: &PathBuf, + mount_dirs: &Vec<config::storage::MountDir>, +) -> Result<PathBuf, Error> { + for mount_dir in mount_dirs { + if let Ok(tail) = real_path.strip_prefix(&mount_dir.source) { + return Ok(Path::new(&mount_dir.name).join(tail)); + } + } + Err(Error::CouldNotMapToVirtualPath(real_path.clone())) +} + +pub async fn read_legacy_playlists( db_file_path: &PathBuf, - config_manager: config::Manager, index_manager: index::Manager, + scanner: scanner::Scanner, ) -> Result<Vec<(String, String, Vec<index::Song>)>, Error> { - Ok(vec![]) + scanner.run_scan().await?; + + let users = read_users(db_file_path)?; + let mount_dirs = read_mount_dirs(db_file_path)?; + let connection = Connection::open(db_file_path)?; + + let mut playlists_statement = connection.prepare("SELECT id, owner, name FROM playlists")?; + let playlists_rows = + playlists_statement.query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))?; + let mut playlists = HashMap::new(); + for playlists_row in playlists_rows { + let (id, owner, name): (u32, u32, String) = playlists_row?; + playlists.insert(id, (users.get(&owner).ok_or(Error::UserNotFound)?, name)); + } + + let mut playlists_by_user: HashMap<String, HashMap<String, Vec<index::Song>>> = HashMap::new(); + let mut songs_statement = + connection.prepare("SELECT playlist, path FROM playlist_songs ORDER BY ordering")?; + let mut songs_rows = songs_statement.query([])?; + while let Some(row) = songs_rows.next()? { + let playlist = playlists.get(&row.get(0)?).ok_or(Error::PlaylistNotFound)?; + let user = playlist.0.name.clone(); + let name = playlist.1.clone(); + let real_path = PathBuf::from(row.get::<_, String>(1)?); + let Ok(virtual_path) = virtualize_path(&real_path, &mount_dirs) else { + continue; + }; + let Ok(song) = index_manager + .get_songs(vec![virtual_path]) + .await + .pop() + .unwrap() + else { + continue; + }; + playlists_by_user + .entry(user) + .or_default() + .entry(name) + .or_default() + .push(song); + } + + let mut results = vec![]; + for (user, playlists) in playlists_by_user { + for (playlist_name, songs) in playlists { + results.push((playlist_name.clone(), user.clone(), songs)); + } + } + + Ok(results) } pub async fn delete_legacy_db(db_file_path: &PathBuf) -> Result<(), Error> { @@ -85,7 +169,10 @@ mod test { use std::path::PathBuf; use super::*; - use crate::app::config; + use crate::{ + app::{config, test}, + test_name, + }; #[test] fn can_read_auth_secret() { @@ -145,4 +232,73 @@ mod test { assert_eq!(actual, expected); } + + #[tokio::test] + async fn can_read_blank_playlists() { + let ctx = test::ContextBuilder::new(test_name!()).build().await; + + let actual = read_legacy_playlists( + &PathBuf::from_iter(["test-data", "legacy_db_blank.sqlite"]), + ctx.index_manager, + ctx.scanner, + ) + .await + .unwrap(); + + let expected = vec![]; + + assert_eq!(actual, expected); + } + + #[tokio::test] + async fn can_read_populated_playlists() { + let ctx = test::ContextBuilder::new(test_name!()).build().await; + let db_file_path = PathBuf::from_iter(["test-data", "legacy_db_populated.sqlite"]); + + let config = read_legacy_config(&db_file_path).unwrap().unwrap(); + ctx.config_manager.apply_config(config).await.unwrap(); + + let actual = read_legacy_playlists( + &db_file_path, + ctx.index_manager.clone(), + ctx.scanner.clone(), + ) + .await + .unwrap(); + + #[rustfmt::skip] + let song_paths = vec![ + PathBuf::from_iter(["root", "Omodaka","2011 - Cantata No.147", "01 - Otemoyan.mp3"]), + PathBuf::from_iter(["root", "Omodaka","2011 - Cantata No.147", "02 - Asadoya Yunta.mp3"]), + PathBuf::from_iter(["root", "Omodaka","2011 - Cantata No.147", "03 - Fortunate 1mark (A-4 Mix).mp3"]), + PathBuf::from_iter(["root", "Omodaka","2011 - Cantata No.147", "04 - Hanagasa Ondo.mp3"]), + PathBuf::from_iter(["root", "Omodaka","2011 - Cantata No.147", "05 - Cantata No.147 (Video Mix).mp3"]), + PathBuf::from_iter(["root", "Omodaka","2011 - Cantata No.147", "06 - Kokirikobushi (Video Mix).mp3"]), + PathBuf::from_iter(["root", "Omodaka","2011 - Cantata No.147", "07 - Monkey Turn (Mahoroba Mix).mp3"]), + PathBuf::from_iter(["root", "Omodaka","2011 - Cantata No.147", "08 - Otemoyan (Inst).mp3"]), + PathBuf::from_iter(["root", "Omodaka","2011 - Cantata No.147", "09 - Asadoya Yunta (Inst).mp3"]), + PathBuf::from_iter(["root", "Omodaka","2011 - Cantata No.147", "10 - Hanagasa Ondo (Inst).mp3"]), + PathBuf::from_iter(["root", "Holy Konni","2013 - Fetushouse", "01 - Kate Moss Magic.mp3"]), + PathBuf::from_iter(["root", "Holy Konni","2013 - Fetushouse", "02 - Self Portrait 19.mp3"]), + PathBuf::from_iter(["root", "Holy Konni","2013 - Fetushouse", "03 - Vlieg Berg-Stop Nie.mp3"]), + PathBuf::from_iter(["root", "Holy Konni","2013 - Fetushouse", "04 - DREAM CRY BREAST LIFE.mp3"]), + PathBuf::from_iter(["root", "Holy Konni","2013 - Fetushouse", "05 - Natalie Reborn.mp3"]), + ]; + + let songs: Vec<index::Song> = ctx + .index_manager + .get_songs(song_paths) + .await + .into_iter() + .map(|s| s.unwrap()) + .collect(); + + let expected = vec![( + "Example Playlist".to_owned(), + "example_user".to_owned(), + songs, + )]; + + assert_eq!(actual, expected); + } } diff --git a/src/main.rs b/src/main.rs index 0331921..d9d2268 100644 --- a/src/main.rs +++ b/src/main.rs @@ -128,7 +128,7 @@ fn main() -> Result<(), Error> { info!("Cache files location is {:#?}", paths.cache_dir_path); info!("Data files location is {:#?}", paths.data_dir_path); info!("Config file location is {:#?}", paths.config_file_path); - info!("Database file location is {:#?}", paths.db_file_path); + info!("Legacy database file location is {:#?}", paths.db_file_path); info!("Log file location is {:#?}", paths.log_file_path); #[cfg(unix)] if !cli_options.foreground { diff --git a/src/server/error.rs b/src/server/error.rs index 11245f4..16bc717 100644 --- a/src/server/error.rs +++ b/src/server/error.rs @@ -132,6 +132,7 @@ impl From<app::Error> for APIError { app::Error::IndexSerializationError => APIError::Internal, app::Error::CouldNotMapToRealPath(_) => APIError::VFSPathNotFound, + app::Error::CouldNotMapToVirtualPath(_) => APIError::Internal, app::Error::UserNotFound => APIError::UserNotFound, app::Error::DirectoryNotFound(d) => APIError::DirectoryNotFound(d), app::Error::ArtistNotFound => APIError::ArtistNotFound, diff --git a/test-data/legacy_db_blank.sqlite b/test-data/legacy_db_blank.sqlite new file mode 100644 index 0000000..73a36dd Binary files /dev/null and b/test-data/legacy_db_blank.sqlite differ diff --git a/test-data/legacy_db_populated.sqlite b/test-data/legacy_db_populated.sqlite new file mode 100644 index 0000000..4bc3b27 Binary files /dev/null and b/test-data/legacy_db_populated.sqlite differ