Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

backend/bookmarks: add handler for playqueue related operations #524

Merged
merged 5 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions nghe-api/src/bookmarks/get_playqueue.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use nghe_proc_macro::api_derive;
use uuid::Uuid;

use crate::id3;

#[api_derive]
#[endpoint(path = "getPlayQueue")]
pub struct Request {}

#[api_derive]
#[derive(Default)]
pub struct Playqueue {
pub entry: Vec<id3::song::Short>,
pub current: Option<Uuid>,
pub position: Option<u64>,
}

#[api_derive]
#[derive(Default)]
pub struct Response {
#[serde(rename = "playQueue")]
pub playqueue: Playqueue,
}
2 changes: 2 additions & 0 deletions nghe-api/src/bookmarks/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod get_playqueue;
pub mod save_playqueue;
14 changes: 14 additions & 0 deletions nghe-api/src/bookmarks/save_playqueue.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use nghe_proc_macro::api_derive;
use uuid::Uuid;

#[api_derive]
#[endpoint(path = "savePlayQueue")]
pub struct Request {
#[serde(rename = "id")]
pub ids: Vec<Uuid>,
pub current: Option<Uuid>,
pub position: Option<u64>,
}

#[api_derive]
pub struct Response;
1 change: 1 addition & 0 deletions nghe-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#![feature(trait_alias)]

pub mod auth;
pub mod bookmarks;
pub mod browsing;
pub mod common;
pub mod constant;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
drop table playqueues;
10 changes: 10 additions & 0 deletions nghe-backend/migrations/2024-11-28-201114_create_playqueues/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- Your SQL goes here
create table playqueues (
user_id uuid not null constraint playqueues_pkey primary key,
ids uuid [] not null check (array_position(ids, null) is null),
current uuid,
position bigint,
constraint playqueues_user_id_fkey foreign key (
user_id
) references users (id) on delete cascade
);
2 changes: 0 additions & 2 deletions nghe-backend/src/error/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,6 @@ pub enum Error {
CheckoutConnectionPool,
#[error("Could not decrypt value from database")]
DecryptDatabaseValue,
#[error("Language from database should not be null")]
LanguageFromDatabaseIsNull,
#[error("Inconsistency encountered while querying database for scan process")]
DatabaseScanQueryInconsistent,
#[error("Invalid config format for key {0}")]
Expand Down
8 changes: 2 additions & 6 deletions nghe-backend/src/file/audio/metadata.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::str::FromStr;

#[cfg(test)]
use fake::{Dummy, Fake};
use isolang::Language;
Expand All @@ -23,11 +21,9 @@ pub struct Song<'a> {
#[map(~.try_into()?)]
pub track_disc: position::TrackDisc,
#[from(~.into_iter().map(
|language|Language::from_str(language.ok_or_else(
|| Error::LanguageFromDatabaseIsNull)?.as_ref()
).map_err(Error::from)
|language| language.as_str().parse().map_err(Error::from)
).try_collect()?)]
#[into(~.iter().map(|language| Some(language.to_639_3().into())).collect())]
#[into(~.iter().map(|language| language.to_639_3().into()).collect())]
#[cfg_attr(
test,
dummy(expr = "((0..=7915), \
Expand Down
1 change: 1 addition & 0 deletions nghe-backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ pub async fn build(config: config::Config) -> Router {
cover_art: config.cover_art,
},
))
.merge(route::bookmarks::router())
.merge(route::browsing::router())
.merge(route::lists::router())
.merge(route::media_annotation::router())
Expand Down
1 change: 1 addition & 0 deletions nghe-backend/src/orm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub mod playlist;
pub mod playlists;
pub mod playlists_songs;
pub mod playlists_users;
pub mod playqueues;
pub mod songs;
pub mod songs_album_artists;
pub mod songs_artists;
Expand Down
46 changes: 46 additions & 0 deletions nghe-backend/src/orm/playqueues.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use diesel::dsl::sql;
use diesel::expression::SqlLiteral;
use diesel::prelude::*;
use diesel::sql_types;
use diesel_derives::AsChangeset;
use o2o::o2o;
use uuid::Uuid;

pub use crate::schema::playqueues::{self, *};
use crate::Error;

#[derive(Debug, Queryable, Selectable, Insertable, AsChangeset, o2o)]
#[diesel(table_name = playqueues, check_for_backend(crate::orm::Type))]
#[diesel(treat_none_as_null = true)]
#[try_from_owned(nghe_api::bookmarks::save_playqueue::Request, Error)]
pub struct Data {
#[diesel(select_expression = sql("playqueues.ids ids"))]
#[diesel(select_expression_type = SqlLiteral<sql_types::Array<sql_types::Uuid>>)]
pub ids: Vec<Uuid>,
pub current: Option<Uuid>,
#[from(~.map(i64::try_from).transpose()?)]
pub position: Option<i64>,
}

mod upsert {
use diesel::ExpressionMethods;
use diesel_async::RunQueryDsl;
use uuid::Uuid;

use super::{playqueues, Data};
use crate::database::Database;
use crate::Error;

impl crate::orm::upsert::Update for Data {
async fn update(&self, database: &Database, id: Uuid) -> Result<(), Error> {
diesel::insert_into(playqueues::table)
.values((playqueues::user_id.eq(id), self))
.on_conflict(playqueues::user_id)
.do_update()
.set(self)
.execute(&mut database.get().await?)
.await?;
Ok(())
}
}
}
7 changes: 6 additions & 1 deletion nghe-backend/src/orm/songs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ use std::borrow::Cow;
use std::str::FromStr;

use diesel::deserialize::{self, FromSql};
use diesel::dsl::sql;
use diesel::expression::SqlLiteral;
use diesel::pg::PgValue;
use diesel::prelude::*;
use diesel::serialize::{self, Output, ToSql};
use diesel::sql_types;
use diesel::sql_types::Text;
use diesel_derives::AsChangeset;
use uuid::Uuid;
Expand Down Expand Up @@ -34,7 +37,9 @@ pub struct Song<'a> {
pub main: name_date_mbz::NameDateMbz<'a>,
#[diesel(embed)]
pub track_disc: position::TrackDisc,
pub languages: Vec<Option<Cow<'a, str>>>,
#[diesel(select_expression = sql("songs.languages languages"))]
#[diesel(select_expression_type = SqlLiteral<sql_types::Array<sql_types::Text>>)]
pub languages: Vec<Cow<'a, str>>,
}

#[derive(Debug, Queryable, Selectable, Insertable, AsChangeset)]
Expand Down
97 changes: 97 additions & 0 deletions nghe-backend/src/route/bookmarks/get_playqueue.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, SelectableHelper};
use diesel_async::RunQueryDsl;
use futures_lite::{stream, StreamExt as _};
use nghe_api::bookmarks::get_playqueue::Playqueue;
pub use nghe_api::bookmarks::get_playqueue::{Request, Response};
use nghe_proc_macro::handler;
use uuid::Uuid;

use crate::database::Database;
use crate::orm::{id3, playqueues, songs};
use crate::Error;

#[handler]
pub async fn handler(
database: &Database,
user_id: Uuid,
request: Request,
) -> Result<Response, Error> {
Ok(
if let Some(data) = playqueues::table
.filter(playqueues::user_id.eq(user_id))
.select(playqueues::Data::as_select())
.get_result(&mut database.get().await?)
.await
.optional()?
{
let entry = stream::iter(data.ids)
.then(async |id| {
id3::song::short::query::with_user_id(user_id)
.filter(songs::id.eq(id))
.get_result(&mut database.get().await?)
.await?
.try_into()
})
.try_collect()
.await?;

Response {
playqueue: Playqueue {
entry,
current: data.current,
position: data.position.map(i64::try_into).transpose()?,
},
}
} else {
Response::default()
},
)
}

#[cfg(test)]
mod test {
use fake::Fake;
use rstest::rstest;

use super::*;
use crate::route::bookmarks::save_playqueue;
use crate::test::{mock, Mock};

#[rstest]
#[tokio::test]
async fn test_handler(
#[future(awt)]
#[with(1, 0)]
mock: Mock,
#[values(true, false)] allow: bool,
) {
mock.add_music_folder().allow(allow).call().await;
mock.add_music_folder().call().await;

let mut music_folder_permission = mock.music_folder(0).await;
let mut music_folder = mock.music_folder(1).await;

music_folder_permission.add_audio().n_song((2..4).fake()).call().await;
music_folder.add_audio().n_song((2..4).fake()).call().await;

let song_ids: Vec<_> = music_folder_permission
.database
.keys()
.copied()
.chain(music_folder.database.keys().copied())
.collect();

let user_id = mock.user_id(0).await;

save_playqueue::handler(
mock.database(),
user_id,
save_playqueue::Request { ids: song_ids, position: None, current: None },
)
.await
.unwrap();

let playqueue = handler(mock.database(), user_id, Request {}).await;
assert_eq!(playqueue.is_ok(), allow);
}
}
6 changes: 6 additions & 0 deletions nghe-backend/src/route/bookmarks/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
mod get_playqueue;
pub mod save_playqueue;

nghe_proc_macro::build_router! {
modules = [get_playqueue, save_playqueue],
}
18 changes: 18 additions & 0 deletions nghe-backend/src/route/bookmarks/save_playqueue.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
pub use nghe_api::bookmarks::save_playqueue::{Request, Response};
use nghe_proc_macro::handler;
use uuid::Uuid;

use crate::database::Database;
use crate::orm::playqueues;
use crate::orm::upsert::Update;
use crate::Error;

#[handler]
pub async fn handler(
database: &Database,
user_id: Uuid,
request: Request,
) -> Result<Response, Error> {
playqueues::Data::try_from(request)?.update(database, user_id).await?;
Ok(Response)
}
1 change: 1 addition & 0 deletions nghe-backend/src/route/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod bookmarks;
pub mod browsing;
pub mod lists;
pub mod media_annotation;
Expand Down
14 changes: 14 additions & 0 deletions nghe-backend/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,18 @@ diesel::table! {
}
}

diesel::table! {
use diesel::sql_types::*;
use diesel_full_text_search::*;

playqueues (user_id) {
user_id -> Uuid,
ids -> Array<Nullable<Uuid>>,
current -> Nullable<Uuid>,
position -> Nullable<Int8>,
}
}

diesel::table! {
use diesel::sql_types::*;
use diesel_full_text_search::*;
Expand Down Expand Up @@ -299,6 +311,7 @@ diesel::joinable!(playlists_songs -> playlists (playlist_id));
diesel::joinable!(playlists_songs -> songs (song_id));
diesel::joinable!(playlists_users -> playlists (playlist_id));
diesel::joinable!(playlists_users -> users (user_id));
diesel::joinable!(playqueues -> users (user_id));
diesel::joinable!(scans -> music_folders (music_folder_id));
diesel::joinable!(songs -> albums (album_id));
diesel::joinable!(songs -> cover_arts (cover_art_id));
Expand All @@ -323,6 +336,7 @@ diesel::allow_tables_to_appear_in_same_query!(
playlists,
playlists_songs,
playlists_users,
playqueues,
scans,
songs,
songs_album_artists,
Expand Down
Loading