ndb playlists first pass

This commit is contained in:
Antoine Gersant 2024-10-04 20:43:53 -07:00
parent b175e319b7
commit 664ff721e2
15 changed files with 405 additions and 198 deletions

180
Cargo.lock generated
View file

@ -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"

View file

@ -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

Binary file not shown.

View file

@ -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
View 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()
}
}

View file

@ -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);
}
}

View file

@ -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();

View file

@ -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
);

View file

@ -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 {

View file

@ -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)

View file

@ -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)
}

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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);
}