ndb playlists first pass
This commit is contained in:
parent
b175e319b7
commit
664ff721e2
15 changed files with 405 additions and 198 deletions
180
Cargo.lock
generated
180
Cargo.lock
generated
|
@ -276,6 +276,15 @@ version = "1.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bincode"
|
||||||
|
version = "1.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitcode"
|
name = "bitcode"
|
||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
|
@ -341,6 +350,12 @@ version = "3.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytecount"
|
||||||
|
version = "0.6.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.16.3"
|
version = "1.16.3"
|
||||||
|
@ -365,6 +380,37 @@ version = "1.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
|
checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "camino"
|
||||||
|
version = "1.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cargo-platform"
|
||||||
|
version = "0.1.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cargo_metadata"
|
||||||
|
version = "0.14.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa"
|
||||||
|
dependencies = [
|
||||||
|
"camino",
|
||||||
|
"cargo-platform",
|
||||||
|
"semver 1.0.23",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.1.7"
|
version = "1.1.7"
|
||||||
|
@ -668,6 +714,15 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "error-chain"
|
||||||
|
version = "0.12.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc"
|
||||||
|
dependencies = [
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "etcetera"
|
name = "etcetera"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
|
@ -902,6 +957,12 @@ version = "0.28.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "779ae4bf7e8421cf91c0b3b64e7e8b40b862fba4d393f59150042de7c4965a94"
|
checksum = "779ae4bf7e8421cf91c0b3b64e7e8b40b862fba4d393f59150042de7c4965a94"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glob"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
|
@ -1389,6 +1450,57 @@ dependencies = [
|
||||||
"winapi-build",
|
"winapi-build",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "native_db"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "git+https://github.com/vincent-herlemont/native_db#6daae43c5461e5944f076d40b4026be6c9730107"
|
||||||
|
dependencies = [
|
||||||
|
"native_db_macro",
|
||||||
|
"native_model",
|
||||||
|
"redb 1.5.1",
|
||||||
|
"redb 2.1.3",
|
||||||
|
"semver 1.0.23",
|
||||||
|
"serde",
|
||||||
|
"skeptic",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "native_db_macro"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "git+https://github.com/vincent-herlemont/native_db#6daae43c5461e5944f076d40b4026be6c9730107"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.72",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "native_model"
|
||||||
|
version = "0.4.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "360da481893ec9bfa71593193b796400d8616c3b2aa8145b7b72e561224e6259"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"bincode",
|
||||||
|
"native_model_macro",
|
||||||
|
"serde",
|
||||||
|
"skeptic",
|
||||||
|
"thiserror",
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "native_model_macro"
|
||||||
|
version = "0.4.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "35ade955118c8776435064b3c0e95b75e89867ed19eaaf6709a2f3eea9c46420"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.72",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nohash-hasher"
|
name = "nohash-hasher"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
@ -1704,6 +1816,8 @@ dependencies = [
|
||||||
"mp4ameta",
|
"mp4ameta",
|
||||||
"native-windows-derive",
|
"native-windows-derive",
|
||||||
"native-windows-gui",
|
"native-windows-gui",
|
||||||
|
"native_db",
|
||||||
|
"native_model",
|
||||||
"nohash-hasher",
|
"nohash-hasher",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"opus_headers",
|
"opus_headers",
|
||||||
|
@ -1810,6 +1924,17 @@ dependencies = [
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pulldown-cmark"
|
||||||
|
version = "0.9.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.6.0",
|
||||||
|
"memchr",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "qstring"
|
name = "qstring"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
|
@ -1878,6 +2003,24 @@ dependencies = [
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redb"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fd7f82ecd6ba647a39dd1a7172b8a1cd9453c0adee6da20cb553d83a9a460fa5"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redb"
|
||||||
|
version = "2.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e4760ad04a88ef77075ba86ba9ea79b919e6bab29c1764c5747237cd6eaedcaa"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
|
@ -2123,6 +2266,15 @@ version = "1.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "same-file"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
@ -2159,6 +2311,9 @@ name = "semver"
|
||||||
version = "1.0.23"
|
version = "1.0.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
|
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver-parser"
|
name = "semver-parser"
|
||||||
|
@ -2293,6 +2448,21 @@ dependencies = [
|
||||||
"time 0.3.36",
|
"time 0.3.36",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "skeptic"
|
||||||
|
version = "0.13.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8"
|
||||||
|
dependencies = [
|
||||||
|
"bytecount",
|
||||||
|
"cargo_metadata",
|
||||||
|
"error-chain",
|
||||||
|
"glob",
|
||||||
|
"pulldown-cmark",
|
||||||
|
"tempfile",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.9"
|
version = "0.4.9"
|
||||||
|
@ -3365,6 +3535,16 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "walkdir"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||||
|
dependencies = [
|
||||||
|
"same-file",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "want"
|
name = "want"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
|
|
@ -27,6 +27,8 @@ log = "0.4.22"
|
||||||
metaflac = "0.2.7"
|
metaflac = "0.2.7"
|
||||||
mp3-duration = "0.1.10"
|
mp3-duration = "0.1.10"
|
||||||
mp4ameta = "0.11.0"
|
mp4ameta = "0.11.0"
|
||||||
|
native_db = { git = "https://github.com/vincent-herlemont/native_db" }
|
||||||
|
native_model = "0.4.19"
|
||||||
nohash-hasher = "0.2.0"
|
nohash-hasher = "0.2.0"
|
||||||
num_cpus = "1.14.0"
|
num_cpus = "1.14.0"
|
||||||
opus_headers = "0.1.2"
|
opus_headers = "0.1.2"
|
||||||
|
|
BIN
polaris.ndb
Normal file
BIN
polaris.ndb
Normal file
Binary file not shown.
|
@ -9,6 +9,7 @@ pub mod ddns;
|
||||||
pub mod formats;
|
pub mod formats;
|
||||||
pub mod index;
|
pub mod index;
|
||||||
pub mod lastfm;
|
pub mod lastfm;
|
||||||
|
pub mod ndb;
|
||||||
pub mod peaks;
|
pub mod peaks;
|
||||||
pub mod playlist;
|
pub mod playlist;
|
||||||
pub mod scanner;
|
pub mod scanner;
|
||||||
|
@ -73,6 +74,11 @@ pub enum Error {
|
||||||
#[error("Could not apply database migrations: {0}")]
|
#[error("Could not apply database migrations: {0}")]
|
||||||
Migration(sqlx::migrate::MigrateError),
|
Migration(sqlx::migrate::MigrateError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
NativeDatabase(#[from] native_db::db_type::Error),
|
||||||
|
#[error("Could not initialize database")]
|
||||||
|
NativeDatabaseCreationError(native_db::db_type::Error),
|
||||||
|
|
||||||
#[error("DDNS update query failed with HTTP status code `{0}`")]
|
#[error("DDNS update query failed with HTTP status code `{0}`")]
|
||||||
UpdateQueryFailed(u16),
|
UpdateQueryFailed(u16),
|
||||||
#[error("DDNS update query failed due to a transport error")]
|
#[error("DDNS update query failed due to a transport error")]
|
||||||
|
@ -179,6 +185,7 @@ impl App {
|
||||||
fs::create_dir_all(&thumbnails_dir_path)
|
fs::create_dir_all(&thumbnails_dir_path)
|
||||||
.map_err(|e| Error::Io(thumbnails_dir_path.clone(), e))?;
|
.map_err(|e| Error::Io(thumbnails_dir_path.clone(), e))?;
|
||||||
|
|
||||||
|
let ndb_manager = ndb::Manager::new(&paths.ndb_file_path)?;
|
||||||
let vfs_manager = vfs::Manager::new(db.clone());
|
let vfs_manager = vfs::Manager::new(db.clone());
|
||||||
let settings_manager = settings::Manager::new(db.clone());
|
let settings_manager = settings::Manager::new(db.clone());
|
||||||
let auth_secret = settings_manager.get_auth_secret().await?;
|
let auth_secret = settings_manager.get_auth_secret().await?;
|
||||||
|
@ -198,7 +205,7 @@ impl App {
|
||||||
ddns_manager.clone(),
|
ddns_manager.clone(),
|
||||||
);
|
);
|
||||||
let peaks_manager = peaks::Manager::new(peaks_dir_path);
|
let peaks_manager = peaks::Manager::new(peaks_dir_path);
|
||||||
let playlist_manager = playlist::Manager::new(db.clone());
|
let playlist_manager = playlist::Manager::new(ndb_manager);
|
||||||
let thumbnail_manager = thumbnail::Manager::new(thumbnails_dir_path);
|
let thumbnail_manager = thumbnail::Manager::new(thumbnails_dir_path);
|
||||||
let lastfm_manager = lastfm::Manager::new(index_manager.clone(), user_manager.clone());
|
let lastfm_manager = lastfm::Manager::new(index_manager.clone(), user_manager.clone());
|
||||||
|
|
||||||
|
|
38
src/app/ndb.rs
Normal file
38
src/app/ndb.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
use std::{
|
||||||
|
ops::Deref,
|
||||||
|
path::Path,
|
||||||
|
sync::{Arc, LazyLock},
|
||||||
|
};
|
||||||
|
|
||||||
|
use native_db::{Database, Models};
|
||||||
|
|
||||||
|
use crate::app::{playlist, Error};
|
||||||
|
|
||||||
|
static MODELS: LazyLock<Models> = LazyLock::new(|| {
|
||||||
|
let mut models = Models::new();
|
||||||
|
models.define::<playlist::v1::PlaylistModel>().unwrap();
|
||||||
|
models
|
||||||
|
});
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Manager {
|
||||||
|
database: Arc<Database<'static>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Manager {
|
||||||
|
pub fn new(path: &Path) -> Result<Self, Error> {
|
||||||
|
let database = native_db::Builder::new()
|
||||||
|
.create(&MODELS, path)
|
||||||
|
.map_err(|e| Error::NativeDatabaseCreationError(e))?;
|
||||||
|
let database = Arc::new(database);
|
||||||
|
Ok(Self { database })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Manager {
|
||||||
|
type Target = Database<'static>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.database.as_ref()
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,14 +3,15 @@ use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use sqlx::{Acquire, QueryBuilder, Sqlite};
|
use native_db::*;
|
||||||
|
use native_model::{native_model, Model};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::app::Error;
|
use crate::app::{index, ndb, Error};
|
||||||
use crate::db::DB;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Manager {
|
pub struct Manager {
|
||||||
db: DB,
|
db: ndb::Manager,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -20,161 +21,137 @@ pub struct PlaylistHeader {
|
||||||
pub num_songs_by_genre: HashMap<String, u32>,
|
pub num_songs_by_genre: HashMap<String, u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Playlist {
|
||||||
|
pub header: PlaylistHeader,
|
||||||
|
pub songs: Vec<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type PlaylistModel = v1::PlaylistModel;
|
||||||
|
type PlaylistModelKey = v1::PlaylistModelKey;
|
||||||
|
|
||||||
|
pub mod v1 {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||||
|
#[native_model(id = 1, version = 1)]
|
||||||
|
#[native_db(primary_key(custom_id))]
|
||||||
|
pub struct PlaylistModel {
|
||||||
|
#[secondary_key]
|
||||||
|
pub owner: String,
|
||||||
|
pub name: String,
|
||||||
|
pub duration: Duration,
|
||||||
|
pub num_songs_by_genre: HashMap<String, u32>,
|
||||||
|
pub virtual_paths: Vec<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlaylistModel {
|
||||||
|
fn custom_id(&self) -> (&str, &str) {
|
||||||
|
(&self.owner, &self.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PlaylistModel> for PlaylistHeader {
|
||||||
|
fn from(p: PlaylistModel) -> Self {
|
||||||
|
Self {
|
||||||
|
name: p.name,
|
||||||
|
duration: p.duration,
|
||||||
|
num_songs_by_genre: p.num_songs_by_genre,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PlaylistModel> for Playlist {
|
||||||
|
fn from(mut p: PlaylistModel) -> Self {
|
||||||
|
let songs = p.virtual_paths.drain(0..).collect();
|
||||||
|
Self {
|
||||||
|
songs,
|
||||||
|
header: p.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Manager {
|
impl Manager {
|
||||||
pub fn new(db: DB) -> Self {
|
pub fn new(db: ndb::Manager) -> Self {
|
||||||
Self { db }
|
Self { db }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_playlists(&self, owner: &str) -> Result<Vec<PlaylistHeader>, Error> {
|
pub async fn list_playlists(&self, owner: &str) -> Result<Vec<PlaylistHeader>, Error> {
|
||||||
let mut connection = self.db.connect().await?;
|
let transaction = self.db.r_transaction()?;
|
||||||
|
let playlists = transaction
|
||||||
let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE name = $1", owner)
|
.scan()
|
||||||
.fetch_optional(connection.as_mut())
|
.secondary::<PlaylistModel>(PlaylistModelKey::owner)?
|
||||||
.await?
|
.range(owner..=owner)?
|
||||||
.ok_or(Error::UserNotFound)?;
|
.filter_map(|p| p.ok())
|
||||||
|
.map(PlaylistHeader::from)
|
||||||
Ok(
|
.collect::<Vec<_>>();
|
||||||
sqlx::query_scalar!("SELECT name FROM playlists WHERE owner = $1", user_id)
|
Ok(playlists)
|
||||||
.fetch_all(connection.as_mut())
|
|
||||||
.await?,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn save_playlist(
|
pub async fn save_playlist(
|
||||||
&self,
|
&self,
|
||||||
playlist_name: &str,
|
playlist_name: &str,
|
||||||
owner: &str,
|
owner: &str,
|
||||||
content: &[PathBuf],
|
songs: Vec<index::Song>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
struct PlaylistSong {
|
let transaction = self.db.rw_transaction()?;
|
||||||
virtual_path: String,
|
|
||||||
ordering: i64,
|
let duration = songs
|
||||||
|
.iter()
|
||||||
|
.filter_map(|s| s.duration.map(|d| d as u64))
|
||||||
|
.sum();
|
||||||
|
|
||||||
|
let mut num_songs_by_genre = HashMap::<String, u32>::new();
|
||||||
|
for song in &songs {
|
||||||
|
for genre in &song.genres {
|
||||||
|
*num_songs_by_genre.entry(genre.clone()).or_default() += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut new_songs: Vec<PlaylistSong> = Vec::with_capacity(content.len());
|
let virtual_paths = songs.into_iter().map(|s| s.virtual_path).collect();
|
||||||
for (i, virtual_path) in content.iter().enumerate() {
|
|
||||||
new_songs.push(PlaylistSong {
|
|
||||||
virtual_path: virtual_path.to_string_lossy().to_string(),
|
|
||||||
ordering: i as i64,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut connection = self.db.connect().await?;
|
transaction.remove::<PlaylistModel>(PlaylistModel {
|
||||||
|
owner: owner.to_owned(),
|
||||||
|
name: playlist_name.to_owned(),
|
||||||
|
..Default::default()
|
||||||
|
})?;
|
||||||
|
|
||||||
// Find owner
|
transaction.insert::<PlaylistModel>(PlaylistModel {
|
||||||
let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE name = $1", owner)
|
owner: owner.to_owned(),
|
||||||
.fetch_optional(connection.as_mut())
|
name: playlist_name.to_owned(),
|
||||||
.await?
|
duration: Duration::from_secs(duration),
|
||||||
.ok_or(Error::UserNotFound)?;
|
num_songs_by_genre,
|
||||||
|
virtual_paths,
|
||||||
|
})?;
|
||||||
|
|
||||||
// Create playlist
|
transaction.commit()?;
|
||||||
sqlx::query!(
|
|
||||||
"INSERT INTO playlists (owner, name) VALUES($1, $2)",
|
|
||||||
user_id,
|
|
||||||
playlist_name
|
|
||||||
)
|
|
||||||
.execute(connection.as_mut())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let playlist_id = sqlx::query_scalar!(
|
|
||||||
"SELECT id FROM playlists WHERE owner = $1 AND name = $2",
|
|
||||||
user_id,
|
|
||||||
playlist_name
|
|
||||||
)
|
|
||||||
.fetch_one(connection.as_mut())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
connection.acquire().await?;
|
|
||||||
|
|
||||||
sqlx::query!(
|
|
||||||
"DELETE FROM playlist_songs WHERE playlist = $1",
|
|
||||||
playlist_id
|
|
||||||
)
|
|
||||||
.execute(connection.as_mut())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
for chunk in new_songs.chunks(10_000) {
|
|
||||||
QueryBuilder::<Sqlite>::new(
|
|
||||||
"INSERT INTO playlist_songs (playlist, virtual_path, ordering) ",
|
|
||||||
)
|
|
||||||
.push_values(chunk, |mut b, song| {
|
|
||||||
b.push_bind(playlist_id)
|
|
||||||
.push_bind(&song.virtual_path)
|
|
||||||
.push_bind(song.ordering);
|
|
||||||
})
|
|
||||||
.build()
|
|
||||||
.execute(connection.as_mut())
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn read_playlist(
|
pub async fn read_playlist(&self, playlist_name: &str, owner: &str) -> Result<Playlist, Error> {
|
||||||
&self,
|
let transaction = self.db.r_transaction()?;
|
||||||
playlist_name: &str,
|
match transaction
|
||||||
owner: &str,
|
.get()
|
||||||
) -> Result<Vec<PathBuf>, Error> {
|
.primary::<PlaylistModel>((owner, playlist_name))
|
||||||
let songs = {
|
{
|
||||||
let mut connection = self.db.connect().await?;
|
Ok(Some(p)) => Ok(Playlist::from(p)),
|
||||||
|
Ok(None) => Err(Error::PlaylistNotFound),
|
||||||
// Find owner
|
Err(e) => Err(Error::NativeDatabase(e)),
|
||||||
let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE name = $1", owner)
|
}
|
||||||
.fetch_optional(connection.as_mut())
|
|
||||||
.await?
|
|
||||||
.ok_or(Error::UserNotFound)?;
|
|
||||||
|
|
||||||
// Find playlist
|
|
||||||
let playlist_id = sqlx::query_scalar!(
|
|
||||||
"SELECT id FROM playlists WHERE name = $1 and owner = $2",
|
|
||||||
playlist_name,
|
|
||||||
user_id
|
|
||||||
)
|
|
||||||
.fetch_optional(connection.as_mut())
|
|
||||||
.await?
|
|
||||||
.ok_or(Error::PlaylistNotFound)?;
|
|
||||||
|
|
||||||
// List songs
|
|
||||||
sqlx::query_scalar!(
|
|
||||||
r#"
|
|
||||||
SELECT virtual_path
|
|
||||||
FROM playlist_songs ps
|
|
||||||
WHERE ps.playlist = $1
|
|
||||||
ORDER BY ps.ordering
|
|
||||||
"#,
|
|
||||||
playlist_id
|
|
||||||
)
|
|
||||||
.fetch_all(connection.as_mut())
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.map(PathBuf::from)
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(songs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_playlist(&self, playlist_name: &str, owner: &str) -> Result<(), Error> {
|
pub async fn delete_playlist(&self, playlist_name: &str, owner: &str) -> Result<(), Error> {
|
||||||
let mut connection = self.db.connect().await?;
|
let transaction = self.db.rw_transaction()?;
|
||||||
|
transaction.remove::<PlaylistModel>(PlaylistModel {
|
||||||
let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE name = $1", owner)
|
name: playlist_name.to_owned(),
|
||||||
.fetch_optional(connection.as_mut())
|
owner: owner.to_owned(),
|
||||||
.await?
|
..Default::default()
|
||||||
.ok_or(Error::UserNotFound)?;
|
})?;
|
||||||
|
transaction.commit()?;
|
||||||
let num_deletions = sqlx::query_scalar!(
|
Ok(())
|
||||||
"DELETE FROM playlists WHERE owner = $1 AND name = $2",
|
|
||||||
user_id,
|
|
||||||
playlist_name
|
|
||||||
)
|
|
||||||
.execute(connection.as_mut())
|
|
||||||
.await?
|
|
||||||
.rows_affected();
|
|
||||||
|
|
||||||
match num_deletions {
|
|
||||||
0 => Err(Error::PlaylistNotFound),
|
|
||||||
_ => Ok(()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,7 +159,8 @@ impl Manager {
|
||||||
mod test {
|
mod test {
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::app::test;
|
use crate::app::index;
|
||||||
|
use crate::app::test::{self, Context};
|
||||||
use crate::test_name;
|
use crate::test_name;
|
||||||
|
|
||||||
const TEST_USER: &str = "test_user";
|
const TEST_USER: &str = "test_user";
|
||||||
|
@ -190,6 +168,27 @@ mod test {
|
||||||
const TEST_PLAYLIST_NAME: &str = "Chill & Grill";
|
const TEST_PLAYLIST_NAME: &str = "Chill & Grill";
|
||||||
const TEST_MOUNT_NAME: &str = "root";
|
const TEST_MOUNT_NAME: &str = "root";
|
||||||
|
|
||||||
|
async fn list_all_songs(ctx: &Context) -> Vec<index::Song> {
|
||||||
|
let paths = ctx
|
||||||
|
.index_manager
|
||||||
|
.flatten(PathBuf::from(TEST_MOUNT_NAME))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let songs = ctx
|
||||||
|
.index_manager
|
||||||
|
.get_songs(paths)
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| s.unwrap())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(songs.len(), 13);
|
||||||
|
songs
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn save_playlist_golden_path() {
|
async fn save_playlist_golden_path() {
|
||||||
let ctx = test::ContextBuilder::new(test_name!())
|
let ctx = test::ContextBuilder::new(test_name!())
|
||||||
|
@ -198,7 +197,7 @@ mod test {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
ctx.playlist_manager
|
ctx.playlist_manager
|
||||||
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &Vec::new())
|
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, Vec::new())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -207,6 +206,7 @@ mod test {
|
||||||
.list_playlists(TEST_USER)
|
.list_playlists(TEST_USER)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(found_playlists.len(), 1);
|
assert_eq!(found_playlists.len(), 1);
|
||||||
assert_eq!(found_playlists[0].name, TEST_PLAYLIST_NAME);
|
assert_eq!(found_playlists[0].name, TEST_PLAYLIST_NAME);
|
||||||
}
|
}
|
||||||
|
@ -221,31 +221,24 @@ mod test {
|
||||||
|
|
||||||
ctx.scanner.update().await.unwrap();
|
ctx.scanner.update().await.unwrap();
|
||||||
|
|
||||||
let playlist_content = ctx
|
let songs = list_all_songs(&ctx).await;
|
||||||
.index_manager
|
|
||||||
.flatten(PathBuf::from(TEST_MOUNT_NAME))
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.into_iter()
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
assert_eq!(playlist_content.len(), 13);
|
|
||||||
|
|
||||||
ctx.playlist_manager
|
ctx.playlist_manager
|
||||||
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
|
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, songs.clone())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
ctx.playlist_manager
|
ctx.playlist_manager
|
||||||
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
|
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, songs.clone())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let songs = ctx
|
let playlist = ctx
|
||||||
.playlist_manager
|
.playlist_manager
|
||||||
.read_playlist(TEST_PLAYLIST_NAME, TEST_USER)
|
.read_playlist(TEST_PLAYLIST_NAME, TEST_USER)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(songs.len(), 13);
|
assert_eq!(playlist.songs.len(), 13);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
@ -255,10 +248,8 @@ mod test {
|
||||||
.build()
|
.build()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let playlist_content = Vec::new();
|
|
||||||
|
|
||||||
ctx.playlist_manager
|
ctx.playlist_manager
|
||||||
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
|
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, Vec::new())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -285,27 +276,20 @@ mod test {
|
||||||
|
|
||||||
ctx.scanner.update().await.unwrap();
|
ctx.scanner.update().await.unwrap();
|
||||||
|
|
||||||
let playlist_content = ctx
|
let songs = list_all_songs(&ctx).await;
|
||||||
.index_manager
|
|
||||||
.flatten(PathBuf::from(TEST_MOUNT_NAME))
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.into_iter()
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
assert_eq!(playlist_content.len(), 13);
|
|
||||||
|
|
||||||
ctx.playlist_manager
|
ctx.playlist_manager
|
||||||
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
|
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, songs)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let songs = ctx
|
let playlist = ctx
|
||||||
.playlist_manager
|
.playlist_manager
|
||||||
.read_playlist(TEST_PLAYLIST_NAME, TEST_USER)
|
.read_playlist(TEST_PLAYLIST_NAME, TEST_USER)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(songs.len(), 13);
|
assert_eq!(playlist.songs.len(), 13);
|
||||||
|
|
||||||
let first_song_path: PathBuf = [
|
let first_song_path: PathBuf = [
|
||||||
TEST_MOUNT_NAME,
|
TEST_MOUNT_NAME,
|
||||||
|
@ -315,6 +299,6 @@ mod test {
|
||||||
]
|
]
|
||||||
.iter()
|
.iter()
|
||||||
.collect();
|
.collect();
|
||||||
assert_eq!(songs[0], first_song_path);
|
assert_eq!(playlist.songs[0], first_song_path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::app::{config, ddns, index, playlist, scanner, settings, user, vfs};
|
use crate::app::{config, ddns, index, ndb, playlist, scanner, settings, user, vfs};
|
||||||
use crate::db::DB;
|
use crate::db::DB;
|
||||||
use crate::test::*;
|
use crate::test::*;
|
||||||
|
|
||||||
|
@ -52,8 +52,10 @@ impl ContextBuilder {
|
||||||
}
|
}
|
||||||
pub async fn build(self) -> Context {
|
pub async fn build(self) -> Context {
|
||||||
let db_path = self.test_directory.join("db.sqlite");
|
let db_path = self.test_directory.join("db.sqlite");
|
||||||
|
let ndb_path = self.test_directory.join("polaris.ndb");
|
||||||
|
|
||||||
let db = DB::new(&db_path).await.unwrap();
|
let db = DB::new(&db_path).await.unwrap();
|
||||||
|
let ndb_manager = ndb::Manager::new(&ndb_path).unwrap();
|
||||||
let settings_manager = settings::Manager::new(db.clone());
|
let settings_manager = settings::Manager::new(db.clone());
|
||||||
let auth_secret = settings_manager.get_auth_secret().await.unwrap();
|
let auth_secret = settings_manager.get_auth_secret().await.unwrap();
|
||||||
let user_manager = user::Manager::new(db.clone(), auth_secret);
|
let user_manager = user::Manager::new(db.clone(), auth_secret);
|
||||||
|
@ -73,7 +75,7 @@ impl ContextBuilder {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let playlist_manager = playlist::Manager::new(db.clone());
|
let playlist_manager = playlist::Manager::new(ndb_manager.clone());
|
||||||
|
|
||||||
config_manager.apply(&self.config).await.unwrap();
|
config_manager.apply(&self.config).await.unwrap();
|
||||||
|
|
||||||
|
|
|
@ -51,20 +51,3 @@ CREATE TABLE collection_index (
|
||||||
);
|
);
|
||||||
|
|
||||||
INSERT INTO collection_index (id, content) VALUES (0, NULL);
|
INSERT INTO collection_index (id, content) VALUES (0, NULL);
|
||||||
|
|
||||||
CREATE TABLE playlists (
|
|
||||||
id INTEGER PRIMARY KEY NOT NULL,
|
|
||||||
owner INTEGER NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
FOREIGN KEY(owner) REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
UNIQUE(owner, name) ON CONFLICT REPLACE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE playlist_songs (
|
|
||||||
id INTEGER PRIMARY KEY NOT NULL,
|
|
||||||
playlist INTEGER NOT NULL,
|
|
||||||
virtual_path TEXT NOT NULL,
|
|
||||||
ordering INTEGER NOT NULL,
|
|
||||||
FOREIGN KEY(playlist) REFERENCES playlists(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
|
||||||
UNIQUE(playlist, ordering) ON CONFLICT REPLACE
|
|
||||||
);
|
|
||||||
|
|
|
@ -129,6 +129,7 @@ fn main() -> Result<(), Error> {
|
||||||
info!("Cache files location is {:#?}", paths.cache_dir_path);
|
info!("Cache files location is {:#?}", paths.cache_dir_path);
|
||||||
info!("Config files location is {:#?}", paths.config_file_path);
|
info!("Config files location is {:#?}", paths.config_file_path);
|
||||||
info!("Database file location is {:#?}", paths.db_file_path);
|
info!("Database file location is {:#?}", paths.db_file_path);
|
||||||
|
info!("NativeDatabase file location is {:#?}", paths.ndb_file_path);
|
||||||
info!("Log file location is {:#?}", paths.log_file_path);
|
info!("Log file location is {:#?}", paths.log_file_path);
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
if !cli_options.foreground {
|
if !cli_options.foreground {
|
||||||
|
|
|
@ -6,6 +6,7 @@ pub struct Paths {
|
||||||
pub cache_dir_path: PathBuf,
|
pub cache_dir_path: PathBuf,
|
||||||
pub config_file_path: Option<PathBuf>,
|
pub config_file_path: Option<PathBuf>,
|
||||||
pub db_file_path: PathBuf,
|
pub db_file_path: PathBuf,
|
||||||
|
pub ndb_file_path: PathBuf,
|
||||||
pub log_file_path: Option<PathBuf>,
|
pub log_file_path: Option<PathBuf>,
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
pub pid_file_path: PathBuf,
|
pub pid_file_path: PathBuf,
|
||||||
|
@ -22,6 +23,7 @@ impl Default for Paths {
|
||||||
cache_dir_path: ["."].iter().collect(),
|
cache_dir_path: ["."].iter().collect(),
|
||||||
config_file_path: None,
|
config_file_path: None,
|
||||||
db_file_path: [".", "db.sqlite"].iter().collect(),
|
db_file_path: [".", "db.sqlite"].iter().collect(),
|
||||||
|
ndb_file_path: [".", "polaris.ndb"].iter().collect(),
|
||||||
log_file_path: Some([".", "polaris.log"].iter().collect()),
|
log_file_path: Some([".", "polaris.log"].iter().collect()),
|
||||||
pid_file_path: [".", "polaris.pid"].iter().collect(),
|
pid_file_path: [".", "polaris.pid"].iter().collect(),
|
||||||
swagger_dir_path: [".", "docs", "swagger"].iter().collect(),
|
swagger_dir_path: [".", "docs", "swagger"].iter().collect(),
|
||||||
|
@ -40,6 +42,7 @@ impl Default for Paths {
|
||||||
cache_dir_path: install_directory.clone(),
|
cache_dir_path: install_directory.clone(),
|
||||||
config_file_path: None,
|
config_file_path: None,
|
||||||
db_file_path: install_directory.join("db.sqlite"),
|
db_file_path: install_directory.join("db.sqlite"),
|
||||||
|
ndb_file_path: install_directory.join("polaris.ndb"),
|
||||||
log_file_path: Some(install_directory.join("polaris.log")),
|
log_file_path: Some(install_directory.join("polaris.log")),
|
||||||
swagger_dir_path: install_directory.join("swagger"),
|
swagger_dir_path: install_directory.join("swagger"),
|
||||||
web_dir_path: install_directory.join("web"),
|
web_dir_path: install_directory.join("web"),
|
||||||
|
@ -55,6 +58,10 @@ impl Paths {
|
||||||
.map(PathBuf::from)
|
.map(PathBuf::from)
|
||||||
.map(|p| p.join("db.sqlite"))
|
.map(|p| p.join("db.sqlite"))
|
||||||
.unwrap_or(defaults.db_file_path),
|
.unwrap_or(defaults.db_file_path),
|
||||||
|
ndb_file_path: option_env!("POLARIS_DB_DIR")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.map(|p| p.join("polaris.ndb"))
|
||||||
|
.unwrap_or(defaults.ndb_file_path),
|
||||||
config_file_path: None,
|
config_file_path: None,
|
||||||
cache_dir_path: option_env!("POLARIS_CACHE_DIR")
|
cache_dir_path: option_env!("POLARIS_CACHE_DIR")
|
||||||
.map(PathBuf::from)
|
.map(PathBuf::from)
|
||||||
|
|
|
@ -628,11 +628,18 @@ async fn get_playlists(
|
||||||
async fn put_playlist(
|
async fn put_playlist(
|
||||||
auth: Auth,
|
auth: Auth,
|
||||||
State(playlist_manager): State<playlist::Manager>,
|
State(playlist_manager): State<playlist::Manager>,
|
||||||
|
State(index_manager): State<index::Manager>,
|
||||||
Path(name): Path<String>,
|
Path(name): Path<String>,
|
||||||
playlist: Json<dto::SavePlaylistInput>,
|
playlist: Json<dto::SavePlaylistInput>,
|
||||||
) -> Result<(), APIError> {
|
) -> Result<(), APIError> {
|
||||||
|
let songs = index_manager
|
||||||
|
.get_songs(playlist.tracks.clone())
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|s| s.ok())
|
||||||
|
.collect();
|
||||||
playlist_manager
|
playlist_manager
|
||||||
.save_playlist(&name, auth.get_username(), &playlist.tracks)
|
.save_playlist(&name, auth.get_username(), songs)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -644,14 +651,14 @@ async fn get_playlist(
|
||||||
State(playlist_manager): State<playlist::Manager>,
|
State(playlist_manager): State<playlist::Manager>,
|
||||||
Path(name): Path<String>,
|
Path(name): Path<String>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let paths = match playlist_manager
|
let playlist = match playlist_manager
|
||||||
.read_playlist(&name, auth.get_username())
|
.read_playlist(&name, auth.get_username())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => return APIError::from(e).into_response(),
|
Err(e) => return APIError::from(e).into_response(),
|
||||||
};
|
};
|
||||||
let song_list = make_song_list(paths, &index_manager).await;
|
let song_list = make_song_list(playlist.songs, &index_manager).await;
|
||||||
song_list_to_response(song_list, api_version)
|
song_list_to_response(song_list, api_version)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ impl IntoResponse for APIError {
|
||||||
APIError::DdnsUpdateQueryFailed(s) => {
|
APIError::DdnsUpdateQueryFailed(s) => {
|
||||||
StatusCode::from_u16(s).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
|
StatusCode::from_u16(s).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
}
|
}
|
||||||
|
APIError::NativeDatabase(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
APIError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
APIError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
APIError::DeletingOwnAccount => StatusCode::CONFLICT,
|
APIError::DeletingOwnAccount => StatusCode::CONFLICT,
|
||||||
APIError::DirectoryNotFound(_) => StatusCode::NOT_FOUND,
|
APIError::DirectoryNotFound(_) => StatusCode::NOT_FOUND,
|
||||||
|
|
|
@ -25,6 +25,7 @@ impl TestService for AxumTestService {
|
||||||
cache_dir_path: ["test-output", test_name].iter().collect(),
|
cache_dir_path: ["test-output", test_name].iter().collect(),
|
||||||
config_file_path: None,
|
config_file_path: None,
|
||||||
db_file_path: output_dir.join("db.sqlite"),
|
db_file_path: output_dir.join("db.sqlite"),
|
||||||
|
ndb_file_path: output_dir.join("polaris.ndb"),
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
pid_file_path: output_dir.join("polaris.pid"),
|
pid_file_path: output_dir.join("polaris.pid"),
|
||||||
log_file_path: None,
|
log_file_path: None,
|
||||||
|
|
|
@ -23,6 +23,8 @@ pub enum APIError {
|
||||||
BrancaTokenEncoding,
|
BrancaTokenEncoding,
|
||||||
#[error("Database error:\n\n{0}")]
|
#[error("Database error:\n\n{0}")]
|
||||||
Database(sqlx::Error),
|
Database(sqlx::Error),
|
||||||
|
#[error("Native Database error:\n\n{0}")]
|
||||||
|
NativeDatabase(native_db::db_type::Error),
|
||||||
#[error("Directory not found: {0}")]
|
#[error("Directory not found: {0}")]
|
||||||
DirectoryNotFound(PathBuf),
|
DirectoryNotFound(PathBuf),
|
||||||
#[error("Artist not found")]
|
#[error("Artist not found")]
|
||||||
|
@ -117,6 +119,9 @@ impl From<app::Error> for APIError {
|
||||||
app::Error::PeaksSerialization(_) => APIError::Internal,
|
app::Error::PeaksSerialization(_) => APIError::Internal,
|
||||||
app::Error::PeaksDeserialization(_) => APIError::Internal,
|
app::Error::PeaksDeserialization(_) => APIError::Internal,
|
||||||
|
|
||||||
|
app::Error::NativeDatabaseCreationError(_) => APIError::Internal,
|
||||||
|
app::Error::NativeDatabase(e) => APIError::NativeDatabase(e),
|
||||||
|
|
||||||
app::Error::Database(e) => APIError::Database(e),
|
app::Error::Database(e) => APIError::Database(e),
|
||||||
app::Error::ConnectionPoolBuild => APIError::Internal,
|
app::Error::ConnectionPoolBuild => APIError::Internal,
|
||||||
app::Error::ConnectionPool => APIError::Internal,
|
app::Error::ConnectionPool => APIError::Internal,
|
||||||
|
|
|
@ -143,14 +143,3 @@ async fn delete_playlist_golden_path() {
|
||||||
let response = service.fetch(&request).await;
|
let response = service.fetch(&request).await;
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn delete_playlist_bad_name_returns_not_found() {
|
|
||||||
let mut service = ServiceType::new(&test_name!()).await;
|
|
||||||
service.complete_initial_setup().await;
|
|
||||||
service.login().await;
|
|
||||||
|
|
||||||
let request = protocol::delete_playlist(TEST_PLAYLIST_NAME);
|
|
||||||
let response = service.fetch(&request).await;
|
|
||||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue