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"
|
||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcode"
|
||||
version = "0.6.3"
|
||||
|
@ -341,6 +350,12 @@ version = "3.16.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||
|
||||
[[package]]
|
||||
name = "bytecount"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.16.3"
|
||||
|
@ -365,6 +380,37 @@ version = "1.7.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "cc"
|
||||
version = "1.1.7"
|
||||
|
@ -668,6 +714,15 @@ dependencies = [
|
|||
"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]]
|
||||
name = "etcetera"
|
||||
version = "0.8.0"
|
||||
|
@ -902,6 +957,12 @@ version = "0.28.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "779ae4bf7e8421cf91c0b3b64e7e8b40b862fba4d393f59150042de7c4965a94"
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
|
@ -1389,6 +1450,57 @@ dependencies = [
|
|||
"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]]
|
||||
name = "nohash-hasher"
|
||||
version = "0.2.0"
|
||||
|
@ -1704,6 +1816,8 @@ dependencies = [
|
|||
"mp4ameta",
|
||||
"native-windows-derive",
|
||||
"native-windows-gui",
|
||||
"native_db",
|
||||
"native_model",
|
||||
"nohash-hasher",
|
||||
"num_cpus",
|
||||
"opus_headers",
|
||||
|
@ -1810,6 +1924,17 @@ dependencies = [
|
|||
"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]]
|
||||
name = "qstring"
|
||||
version = "0.7.2"
|
||||
|
@ -1878,6 +2003,24 @@ dependencies = [
|
|||
"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]]
|
||||
name = "redox_syscall"
|
||||
version = "0.4.1"
|
||||
|
@ -2123,6 +2266,15 @@ version = "1.0.18"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
|
@ -2159,6 +2311,9 @@ name = "semver"
|
|||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver-parser"
|
||||
|
@ -2293,6 +2448,21 @@ dependencies = [
|
|||
"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]]
|
||||
name = "slab"
|
||||
version = "0.4.9"
|
||||
|
@ -3365,6 +3535,16 @@ dependencies = [
|
|||
"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]]
|
||||
name = "want"
|
||||
version = "0.3.1"
|
||||
|
|
|
@ -27,6 +27,8 @@ log = "0.4.22"
|
|||
metaflac = "0.2.7"
|
||||
mp3-duration = "0.1.10"
|
||||
mp4ameta = "0.11.0"
|
||||
native_db = { git = "https://github.com/vincent-herlemont/native_db" }
|
||||
native_model = "0.4.19"
|
||||
nohash-hasher = "0.2.0"
|
||||
num_cpus = "1.14.0"
|
||||
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 index;
|
||||
pub mod lastfm;
|
||||
pub mod ndb;
|
||||
pub mod peaks;
|
||||
pub mod playlist;
|
||||
pub mod scanner;
|
||||
|
@ -73,6 +74,11 @@ pub enum Error {
|
|||
#[error("Could not apply database migrations: {0}")]
|
||||
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}`")]
|
||||
UpdateQueryFailed(u16),
|
||||
#[error("DDNS update query failed due to a transport error")]
|
||||
|
@ -179,6 +185,7 @@ impl App {
|
|||
fs::create_dir_all(&thumbnails_dir_path)
|
||||
.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 settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().await?;
|
||||
|
@ -198,7 +205,7 @@ impl App {
|
|||
ddns_manager.clone(),
|
||||
);
|
||||
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 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::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::db::DB;
|
||||
use crate::app::{index, ndb, Error};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Manager {
|
||||
db: DB,
|
||||
db: ndb::Manager,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -20,161 +21,137 @@ pub struct PlaylistHeader {
|
|||
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 {
|
||||
pub fn new(db: DB) -> Self {
|
||||
pub fn new(db: ndb::Manager) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub async fn list_playlists(&self, owner: &str) -> Result<Vec<PlaylistHeader>, Error> {
|
||||
let mut connection = self.db.connect().await?;
|
||||
|
||||
let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE name = $1", owner)
|
||||
.fetch_optional(connection.as_mut())
|
||||
.await?
|
||||
.ok_or(Error::UserNotFound)?;
|
||||
|
||||
Ok(
|
||||
sqlx::query_scalar!("SELECT name FROM playlists WHERE owner = $1", user_id)
|
||||
.fetch_all(connection.as_mut())
|
||||
.await?,
|
||||
)
|
||||
let transaction = self.db.r_transaction()?;
|
||||
let playlists = transaction
|
||||
.scan()
|
||||
.secondary::<PlaylistModel>(PlaylistModelKey::owner)?
|
||||
.range(owner..=owner)?
|
||||
.filter_map(|p| p.ok())
|
||||
.map(PlaylistHeader::from)
|
||||
.collect::<Vec<_>>();
|
||||
Ok(playlists)
|
||||
}
|
||||
|
||||
pub async fn save_playlist(
|
||||
&self,
|
||||
playlist_name: &str,
|
||||
owner: &str,
|
||||
content: &[PathBuf],
|
||||
songs: Vec<index::Song>,
|
||||
) -> Result<(), Error> {
|
||||
struct PlaylistSong {
|
||||
virtual_path: String,
|
||||
ordering: i64,
|
||||
let transaction = self.db.rw_transaction()?;
|
||||
|
||||
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());
|
||||
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 virtual_paths = songs.into_iter().map(|s| s.virtual_path).collect();
|
||||
|
||||
let mut connection = self.db.connect().await?;
|
||||
transaction.remove::<PlaylistModel>(PlaylistModel {
|
||||
owner: owner.to_owned(),
|
||||
name: playlist_name.to_owned(),
|
||||
..Default::default()
|
||||
})?;
|
||||
|
||||
// Find owner
|
||||
let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE name = $1", owner)
|
||||
.fetch_optional(connection.as_mut())
|
||||
.await?
|
||||
.ok_or(Error::UserNotFound)?;
|
||||
transaction.insert::<PlaylistModel>(PlaylistModel {
|
||||
owner: owner.to_owned(),
|
||||
name: playlist_name.to_owned(),
|
||||
duration: Duration::from_secs(duration),
|
||||
num_songs_by_genre,
|
||||
virtual_paths,
|
||||
})?;
|
||||
|
||||
// Create playlist
|
||||
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?;
|
||||
}
|
||||
transaction.commit()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn read_playlist(
|
||||
&self,
|
||||
playlist_name: &str,
|
||||
owner: &str,
|
||||
) -> Result<Vec<PathBuf>, Error> {
|
||||
let songs = {
|
||||
let mut connection = self.db.connect().await?;
|
||||
|
||||
// Find owner
|
||||
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 read_playlist(&self, playlist_name: &str, owner: &str) -> Result<Playlist, Error> {
|
||||
let transaction = self.db.r_transaction()?;
|
||||
match transaction
|
||||
.get()
|
||||
.primary::<PlaylistModel>((owner, playlist_name))
|
||||
{
|
||||
Ok(Some(p)) => Ok(Playlist::from(p)),
|
||||
Ok(None) => Err(Error::PlaylistNotFound),
|
||||
Err(e) => Err(Error::NativeDatabase(e)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_playlist(&self, playlist_name: &str, owner: &str) -> Result<(), Error> {
|
||||
let mut connection = self.db.connect().await?;
|
||||
|
||||
let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE name = $1", owner)
|
||||
.fetch_optional(connection.as_mut())
|
||||
.await?
|
||||
.ok_or(Error::UserNotFound)?;
|
||||
|
||||
let num_deletions = sqlx::query_scalar!(
|
||||
"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(()),
|
||||
}
|
||||
let transaction = self.db.rw_transaction()?;
|
||||
transaction.remove::<PlaylistModel>(PlaylistModel {
|
||||
name: playlist_name.to_owned(),
|
||||
owner: owner.to_owned(),
|
||||
..Default::default()
|
||||
})?;
|
||||
transaction.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -182,7 +159,8 @@ impl Manager {
|
|||
mod test {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::app::test;
|
||||
use crate::app::index;
|
||||
use crate::app::test::{self, Context};
|
||||
use crate::test_name;
|
||||
|
||||
const TEST_USER: &str = "test_user";
|
||||
|
@ -190,6 +168,27 @@ mod test {
|
|||
const TEST_PLAYLIST_NAME: &str = "Chill & Grill";
|
||||
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]
|
||||
async fn save_playlist_golden_path() {
|
||||
let ctx = test::ContextBuilder::new(test_name!())
|
||||
|
@ -198,7 +197,7 @@ mod test {
|
|||
.await;
|
||||
|
||||
ctx.playlist_manager
|
||||
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &Vec::new())
|
||||
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, Vec::new())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -207,6 +206,7 @@ mod test {
|
|||
.list_playlists(TEST_USER)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(found_playlists.len(), 1);
|
||||
assert_eq!(found_playlists[0].name, TEST_PLAYLIST_NAME);
|
||||
}
|
||||
|
@ -221,31 +221,24 @@ mod test {
|
|||
|
||||
ctx.scanner.update().await.unwrap();
|
||||
|
||||
let playlist_content = ctx
|
||||
.index_manager
|
||||
.flatten(PathBuf::from(TEST_MOUNT_NAME))
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(playlist_content.len(), 13);
|
||||
let songs = list_all_songs(&ctx).await;
|
||||
|
||||
ctx.playlist_manager
|
||||
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
|
||||
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, songs.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
ctx.playlist_manager
|
||||
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
|
||||
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, songs.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let songs = ctx
|
||||
let playlist = ctx
|
||||
.playlist_manager
|
||||
.read_playlist(TEST_PLAYLIST_NAME, TEST_USER)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(songs.len(), 13);
|
||||
assert_eq!(playlist.songs.len(), 13);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -255,10 +248,8 @@ mod test {
|
|||
.build()
|
||||
.await;
|
||||
|
||||
let playlist_content = Vec::new();
|
||||
|
||||
ctx.playlist_manager
|
||||
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
|
||||
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, Vec::new())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -285,27 +276,20 @@ mod test {
|
|||
|
||||
ctx.scanner.update().await.unwrap();
|
||||
|
||||
let playlist_content = ctx
|
||||
.index_manager
|
||||
.flatten(PathBuf::from(TEST_MOUNT_NAME))
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(playlist_content.len(), 13);
|
||||
let songs = list_all_songs(&ctx).await;
|
||||
|
||||
ctx.playlist_manager
|
||||
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
|
||||
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, songs)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let songs = ctx
|
||||
let playlist = ctx
|
||||
.playlist_manager
|
||||
.read_playlist(TEST_PLAYLIST_NAME, TEST_USER)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(songs.len(), 13);
|
||||
assert_eq!(playlist.songs.len(), 13);
|
||||
|
||||
let first_song_path: PathBuf = [
|
||||
TEST_MOUNT_NAME,
|
||||
|
@ -315,6 +299,6 @@ mod test {
|
|||
]
|
||||
.iter()
|
||||
.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 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::test::*;
|
||||
|
||||
|
@ -52,8 +52,10 @@ impl ContextBuilder {
|
|||
}
|
||||
pub async fn build(self) -> Context {
|
||||
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 ndb_manager = ndb::Manager::new(&ndb_path).unwrap();
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().await.unwrap();
|
||||
let user_manager = user::Manager::new(db.clone(), auth_secret);
|
||||
|
@ -73,7 +75,7 @@ impl ContextBuilder {
|
|||
)
|
||||
.await
|
||||
.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();
|
||||
|
||||
|
|
|
@ -51,20 +51,3 @@ CREATE TABLE collection_index (
|
|||
);
|
||||
|
||||
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!("Config files location is {:#?}", paths.config_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);
|
||||
#[cfg(unix)]
|
||||
if !cli_options.foreground {
|
||||
|
|
|
@ -6,6 +6,7 @@ pub struct Paths {
|
|||
pub cache_dir_path: PathBuf,
|
||||
pub config_file_path: Option<PathBuf>,
|
||||
pub db_file_path: PathBuf,
|
||||
pub ndb_file_path: PathBuf,
|
||||
pub log_file_path: Option<PathBuf>,
|
||||
#[cfg(unix)]
|
||||
pub pid_file_path: PathBuf,
|
||||
|
@ -22,6 +23,7 @@ impl Default for Paths {
|
|||
cache_dir_path: ["."].iter().collect(),
|
||||
config_file_path: None,
|
||||
db_file_path: [".", "db.sqlite"].iter().collect(),
|
||||
ndb_file_path: [".", "polaris.ndb"].iter().collect(),
|
||||
log_file_path: Some([".", "polaris.log"].iter().collect()),
|
||||
pid_file_path: [".", "polaris.pid"].iter().collect(),
|
||||
swagger_dir_path: [".", "docs", "swagger"].iter().collect(),
|
||||
|
@ -40,6 +42,7 @@ impl Default for Paths {
|
|||
cache_dir_path: install_directory.clone(),
|
||||
config_file_path: None,
|
||||
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")),
|
||||
swagger_dir_path: install_directory.join("swagger"),
|
||||
web_dir_path: install_directory.join("web"),
|
||||
|
@ -55,6 +58,10 @@ impl Paths {
|
|||
.map(PathBuf::from)
|
||||
.map(|p| p.join("db.sqlite"))
|
||||
.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,
|
||||
cache_dir_path: option_env!("POLARIS_CACHE_DIR")
|
||||
.map(PathBuf::from)
|
||||
|
|
|
@ -628,11 +628,18 @@ async fn get_playlists(
|
|||
async fn put_playlist(
|
||||
auth: Auth,
|
||||
State(playlist_manager): State<playlist::Manager>,
|
||||
State(index_manager): State<index::Manager>,
|
||||
Path(name): Path<String>,
|
||||
playlist: Json<dto::SavePlaylistInput>,
|
||||
) -> Result<(), APIError> {
|
||||
let songs = index_manager
|
||||
.get_songs(playlist.tracks.clone())
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|s| s.ok())
|
||||
.collect();
|
||||
playlist_manager
|
||||
.save_playlist(&name, auth.get_username(), &playlist.tracks)
|
||||
.save_playlist(&name, auth.get_username(), songs)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -644,14 +651,14 @@ async fn get_playlist(
|
|||
State(playlist_manager): State<playlist::Manager>,
|
||||
Path(name): Path<String>,
|
||||
) -> Response {
|
||||
let paths = match playlist_manager
|
||||
let playlist = match playlist_manager
|
||||
.read_playlist(&name, auth.get_username())
|
||||
.await
|
||||
{
|
||||
Ok(s) => s,
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ impl IntoResponse for APIError {
|
|||
APIError::DdnsUpdateQueryFailed(s) => {
|
||||
StatusCode::from_u16(s).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
APIError::NativeDatabase(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
APIError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
APIError::DeletingOwnAccount => StatusCode::CONFLICT,
|
||||
APIError::DirectoryNotFound(_) => StatusCode::NOT_FOUND,
|
||||
|
|
|
@ -25,6 +25,7 @@ impl TestService for AxumTestService {
|
|||
cache_dir_path: ["test-output", test_name].iter().collect(),
|
||||
config_file_path: None,
|
||||
db_file_path: output_dir.join("db.sqlite"),
|
||||
ndb_file_path: output_dir.join("polaris.ndb"),
|
||||
#[cfg(unix)]
|
||||
pid_file_path: output_dir.join("polaris.pid"),
|
||||
log_file_path: None,
|
||||
|
|
|
@ -23,6 +23,8 @@ pub enum APIError {
|
|||
BrancaTokenEncoding,
|
||||
#[error("Database error:\n\n{0}")]
|
||||
Database(sqlx::Error),
|
||||
#[error("Native Database error:\n\n{0}")]
|
||||
NativeDatabase(native_db::db_type::Error),
|
||||
#[error("Directory not found: {0}")]
|
||||
DirectoryNotFound(PathBuf),
|
||||
#[error("Artist not found")]
|
||||
|
@ -117,6 +119,9 @@ impl From<app::Error> for APIError {
|
|||
app::Error::PeaksSerialization(_) => 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::ConnectionPoolBuild => APIError::Internal,
|
||||
app::Error::ConnectionPool => APIError::Internal,
|
||||
|
|
|
@ -143,14 +143,3 @@ async fn delete_playlist_golden_path() {
|
|||
let response = service.fetch(&request).await;
|
||||
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