From 9f7ea90d3342c52d42da2b665f95e71f2de6d52f Mon Sep 17 00:00:00 2001 From: Vo Van Nghia Date: Wed, 27 Nov 2024 16:52:06 +0100 Subject: [PATCH 01/20] backend/orm/id3: add created for album/song --- nghe-api/src/id3/album/mod.rs | 2 ++ nghe-api/src/id3/song/mod.rs | 2 ++ .../up.sql | 2 ++ nghe-backend/src/orm/id3/album/mod.rs | 8 ++++- nghe-backend/src/orm/id3/song/mod.rs | 32 +++++++++++-------- 5 files changed, 32 insertions(+), 14 deletions(-) diff --git a/nghe-api/src/id3/album/mod.rs b/nghe-api/src/id3/album/mod.rs index 282243f8..a8ce937c 100644 --- a/nghe-api/src/id3/album/mod.rs +++ b/nghe-api/src/id3/album/mod.rs @@ -3,6 +3,7 @@ mod full; use bon::Builder; pub use full::Full; use nghe_proc_macro::api_derive; +use time::OffsetDateTime; use uuid::Uuid; use super::{artist, date, genre}; @@ -17,6 +18,7 @@ pub struct Album { pub cover_art: Option, pub song_count: u16, pub duration: u32, + pub created: OffsetDateTime, pub year: Option, pub music_brainz_id: Option, #[builder(default)] diff --git a/nghe-api/src/id3/song/mod.rs b/nghe-api/src/id3/song/mod.rs index e6e423fd..15ba6b18 100644 --- a/nghe-api/src/id3/song/mod.rs +++ b/nghe-api/src/id3/song/mod.rs @@ -5,6 +5,7 @@ use std::borrow::Cow; use bon::Builder; pub use full::Full; use nghe_proc_macro::api_derive; +use time::OffsetDateTime; use uuid::Uuid; use super::artist; @@ -28,6 +29,7 @@ pub struct Song { pub sampling_rate: u32, pub channel_count: u8, pub disc_number: Option, + pub created: OffsetDateTime, pub artists: Vec, pub music_brainz_id: Option, } diff --git a/nghe-backend/migrations/2024-11-22-065114_add_cover_arts_source_constraint_updated_at_scanned_at/up.sql b/nghe-backend/migrations/2024-11-22-065114_add_cover_arts_source_constraint_updated_at_scanned_at/up.sql index 32ab8b74..7daa39fd 100644 --- a/nghe-backend/migrations/2024-11-22-065114_add_cover_arts_source_constraint_updated_at_scanned_at/up.sql +++ b/nghe-backend/migrations/2024-11-22-065114_add_cover_arts_source_constraint_updated_at_scanned_at/up.sql @@ -9,3 +9,5 @@ unique nulls not distinct ( source, file_hash, file_size ), drop constraint cover_arts_format_file_hash_file_size_key; + +select add_updated_at_leave_scanned_at('cover_arts'); diff --git a/nghe-backend/src/orm/id3/album/mod.rs b/nghe-backend/src/orm/id3/album/mod.rs index 3b0c3ca9..41f64d40 100644 --- a/nghe-backend/src/orm/id3/album/mod.rs +++ b/nghe-backend/src/orm/id3/album/mod.rs @@ -7,6 +7,7 @@ use diesel::prelude::*; use diesel::sql_types; use nghe_api::id3; use nghe_api::id3::builder::album as builder; +use time::OffsetDateTime; use uuid::Uuid; use super::genre::Genres; @@ -25,6 +26,8 @@ pub struct Album { ))] #[diesel(select_expression_type = SqlLiteral>)] pub cover_art: Option, + #[diesel(column_name = created_at)] + pub created: OffsetDateTime, #[diesel(embed)] pub date: albums::date::Date, #[diesel(column_name = mbz_id)] @@ -41,7 +44,9 @@ pub type BuilderSet = builder::SetReleaseDate< builder::SetOriginalReleaseDate< builder::SetGenres< builder::SetMusicBrainzId< - builder::SetYear>>, + builder::SetYear< + builder::SetCreated>>, + >, >, >, >, @@ -53,6 +58,7 @@ impl Album { .id(self.id) .name(self.name) .cover_art(self.cover_art) + .created(self.created) .year(self.date.year.map(u16::try_from).transpose()?) .music_brainz_id(self.music_brainz_id) .genres(self.genres.into()) diff --git a/nghe-backend/src/orm/id3/song/mod.rs b/nghe-backend/src/orm/id3/song/mod.rs index 197cd0f7..f8cf5b1c 100644 --- a/nghe-backend/src/orm/id3/song/mod.rs +++ b/nghe-backend/src/orm/id3/song/mod.rs @@ -8,6 +8,7 @@ use diesel::sql_types; use nghe_api::common::format::Trait as _; use nghe_api::id3; use nghe_api::id3::builder::song as builder; +use time::OffsetDateTime; use uuid::Uuid; use super::artist; @@ -35,6 +36,8 @@ pub struct Song { pub property: songs::property::Property, #[diesel(embed)] pub disc: songs::position::Disc, + #[diesel(column_name = created_at)] + pub created: OffsetDateTime, #[diesel(embed)] pub artists: artist::required::Artists, #[diesel(column_name = mbz_id)] @@ -43,19 +46,21 @@ pub struct Song { pub type BuilderSet = builder::SetMusicBrainzId< builder::SetArtists< - builder::SetDiscNumber< - builder::SetChannelCount< - builder::SetSamplingRate< - builder::SetBitDepth< - builder::SetBitRate< - builder::SetDuration< - builder::SetSuffix< - builder::SetContentType< - builder::SetSize< - builder::SetCoverArt< - builder::SetYear< - builder::SetTrack< - builder::SetTitle, + builder::SetCreated< + builder::SetDiscNumber< + builder::SetChannelCount< + builder::SetSamplingRate< + builder::SetBitDepth< + builder::SetBitRate< + builder::SetDuration< + builder::SetSuffix< + builder::SetContentType< + builder::SetSize< + builder::SetCoverArt< + builder::SetYear< + builder::SetTrack< + builder::SetTitle, + >, >, >, >, @@ -89,6 +94,7 @@ impl Song { .sampling_rate(self.property.sample_rate.try_into()?) .channel_count(self.property.channel_count.try_into()?) .disc_number(self.disc.number.map(u16::try_from).transpose()?) + .created(self.created) .artists(self.artists.into()) .music_brainz_id(self.music_brainz_id)) } From 31fdc90725b82194088951bfd301b8603f498004 Mon Sep 17 00:00:00 2001 From: Vo Van Nghia Date: Wed, 27 Nov 2024 19:21:04 +0100 Subject: [PATCH 02/20] nghe-api/browsing: fix get album list2 serialize --- nghe-api/src/lists/get_album_list2.rs | 30 ++++++++----------- .../src/route/lists/get_album_list2.rs | 4 +-- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/nghe-api/src/lists/get_album_list2.rs b/nghe-api/src/lists/get_album_list2.rs index 9409d41d..3b96dbcd 100644 --- a/nghe-api/src/lists/get_album_list2.rs +++ b/nghe-api/src/lists/get_album_list2.rs @@ -6,16 +6,7 @@ use crate::id3; // TODO: Optimize this after https://github.com/serde-rs/serde/issues/1183 #[serde_as] -#[api_derive(request = true, test_only = false)] -#[derive(Clone, Copy)] -pub struct ByYear { - #[serde_as(as = "serde_with::DisplayFromStr")] - pub from_year: u16, - #[serde_as(as = "serde_with::DisplayFromStr")] - pub to_year: u16, -} - -#[api_derive(request = true, copy = false)] +#[api_derive(copy = false)] #[serde(tag = "type")] #[cfg_attr(test, derive(Default))] pub enum Type { @@ -25,7 +16,12 @@ pub enum Type { Frequent, Recent, AlphabeticalByName, - ByYear(ByYear), + ByYear { + #[serde_as(as = "serde_with::DisplayFromStr")] + from_year: u16, + #[serde_as(as = "serde_with::DisplayFromStr")] + to_year: u16, + }, ByGenre { genre: String, }, @@ -75,17 +71,17 @@ mod tests { #[case( "type=byYear&fromYear=1000&toYear=2000", Some(Request { - ty: Type::ByYear ( - ByYear{ from_year: 1000, to_year: 2000 } - ), size: None, ..Default::default() + ty: Type::ByYear { + from_year: 1000, to_year: 2000 + }, size: None, ..Default::default() }) )] #[case( "type=byYear&fromYear=1000&toYear=2000&size=10", Some(Request { - ty: Type::ByYear ( - ByYear{ from_year: 1000, to_year: 2000 } - ), size: Some(10), ..Default::default() + ty: Type::ByYear { + from_year: 1000, to_year: 2000 + }, size: Some(10), ..Default::default() }) )] #[case( diff --git a/nghe-backend/src/route/lists/get_album_list2.rs b/nghe-backend/src/route/lists/get_album_list2.rs index 71e3354e..f8de9a6d 100644 --- a/nghe-backend/src/route/lists/get_album_list2.rs +++ b/nghe-backend/src/route/lists/get_album_list2.rs @@ -1,7 +1,7 @@ use diesel::dsl::{max, sum}; use diesel::{ExpressionMethods, JoinOnDsl, PgSortExpressionMethods as _, QueryDsl}; use diesel_async::RunQueryDsl; -use nghe_api::lists::get_album_list2::{AlbumList2, ByYear, Type}; +use nghe_api::lists::get_album_list2::{AlbumList2, Type}; pub use nghe_api::lists::get_album_list2::{Request, Response}; use nghe_proc_macro::{check_music_folder, handler}; use uuid::Uuid; @@ -51,7 +51,7 @@ pub async fn handler( .await? } Type::AlphabeticalByName => query.get_results(&mut database.get().await?).await?, - Type::ByYear(ByYear { from_year, to_year }) => { + Type::ByYear { from_year, to_year } => { let from_year: i16 = from_year.try_into()?; let to_year: i16 = to_year.try_into()?; if from_year < to_year { From 03fccff0da46e658ff592fddf6d7e5555d89f844 Mon Sep 17 00:00:00 2001 From: Vo Van Nghia Date: Wed, 27 Nov 2024 20:14:44 +0100 Subject: [PATCH 03/20] api/playlists: add api for create playlist --- nghe-api/src/lib.rs | 1 + nghe-api/src/playlists/create_playlist.rs | 83 +++++++++++++++++++++++ nghe-api/src/playlists/mod.rs | 1 + 3 files changed, 85 insertions(+) create mode 100644 nghe-api/src/playlists/create_playlist.rs create mode 100644 nghe-api/src/playlists/mod.rs diff --git a/nghe-api/src/lib.rs b/nghe-api/src/lib.rs index 6effd188..8367e0ef 100644 --- a/nghe-api/src/lib.rs +++ b/nghe-api/src/lib.rs @@ -11,6 +11,7 @@ pub mod media_annotation; pub mod media_retrieval; pub mod music_folder; pub mod permission; +pub mod playlists; pub mod scan; pub mod search; pub mod system; diff --git a/nghe-api/src/playlists/create_playlist.rs b/nghe-api/src/playlists/create_playlist.rs new file mode 100644 index 00000000..6902519e --- /dev/null +++ b/nghe-api/src/playlists/create_playlist.rs @@ -0,0 +1,83 @@ +use nghe_proc_macro::api_derive; +use serde_with::serde_as; +use uuid::Uuid; + +// TODO: Optimize this after https://github.com/serde-rs/serde/issues/1183 +#[serde_as] +#[api_derive(request = true, copy = false)] +#[serde(untagged)] +pub enum CreateOrUpdate { + Create { + name: String, + }, + Update { + #[serde_as(as = "serde_with::DisplayFromStr")] + playlist_id: Uuid, + }, +} + +#[api_derive] +#[endpoint(path = "createPlaylist")] +pub struct Request { + #[serde(flatten)] + pub create_or_update: CreateOrUpdate, + #[serde(rename = "songId")] + pub song_ids: Option>, +} + +#[api_derive] +pub struct Response { + // TODO: Return playlist +} + +#[cfg(test)] +mod tests { + use rstest::rstest; + use uuid::uuid; + + use super::*; + + #[rstest] + #[case( + "name=ef14c42b-6efa-45f3-961c-74856fd431d5", + Some(Request { + create_or_update: CreateOrUpdate::Create { + name: "ef14c42b-6efa-45f3-961c-74856fd431d5".to_owned() + }, + song_ids: None, + }) + )] + #[case( + "name=ef14c42b-6efa-45f3-961c-74856fd431d5&\ + songId=2b839103-04ab-4b39-9b05-8c664590eda4", + Some(Request { + create_or_update: CreateOrUpdate::Create { + name: "ef14c42b-6efa-45f3-961c-74856fd431d5".to_owned() + }, + song_ids: Some(vec![uuid!("2b839103-04ab-4b39-9b05-8c664590eda4")]), + }) + )] + #[case( + "playlistId=ef14c42b-6efa-45f3-961c-74856fd431d5", + Some(Request { + create_or_update: CreateOrUpdate::Update { + playlist_id: uuid!("ef14c42b-6efa-45f3-961c-74856fd431d5") + }, + song_ids: None, + }) + )] + #[case( + "playlistId=ef14c42b-6efa-45f3-961c-74856fd431d5&\ + songId=2b839103-04ab-4b39-9b05-8c664590eda4", + Some(Request { + create_or_update: CreateOrUpdate::Update { + playlist_id: uuid!("ef14c42b-6efa-45f3-961c-74856fd431d5") + }, + song_ids: Some(vec![uuid!("2b839103-04ab-4b39-9b05-8c664590eda4")]), + }) + )] + #[case("playlistId=none", None)] + fn test_deserialize(#[case] url: &str, #[case] request: Option) { + assert_eq!(serde_html_form::from_str::(url).ok(), request); + } +} diff --git a/nghe-api/src/playlists/mod.rs b/nghe-api/src/playlists/mod.rs new file mode 100644 index 00000000..eebf9996 --- /dev/null +++ b/nghe-api/src/playlists/mod.rs @@ -0,0 +1 @@ +pub mod create_playlist; From e4508a1601dd72a0ff868997e4ba1b30e812798c Mon Sep 17 00:00:00 2001 From: Vo Van Nghia Date: Wed, 27 Nov 2024 20:31:33 +0100 Subject: [PATCH 04/20] api/playlists: add api for Playlist response type --- nghe-api/src/playlists/mod.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/nghe-api/src/playlists/mod.rs b/nghe-api/src/playlists/mod.rs index eebf9996..f07940d6 100644 --- a/nghe-api/src/playlists/mod.rs +++ b/nghe-api/src/playlists/mod.rs @@ -1 +1,17 @@ pub mod create_playlist; + +use nghe_proc_macro::api_derive; +use time::OffsetDateTime; +use uuid::Uuid; + +#[api_derive] +pub struct Playlist { + pub id: Uuid, + pub name: String, + pub comment: Option, + pub public: bool, + pub song_count: u16, + pub duration: u32, + pub created: OffsetDateTime, + pub changed: OffsetDateTime, +} From d302cd3c94797d277699fc5c2e6b95358bf392d8 Mon Sep 17 00:00:00 2001 From: Vo Van Nghia Date: Wed, 27 Nov 2024 20:34:37 +0100 Subject: [PATCH 05/20] backend/playlists: add route and orm for playlists --- nghe-backend/src/orm/mod.rs | 4 ++++ nghe-backend/src/orm/playlist/mod.rs | 17 +++++++++++++++++ nghe-backend/src/orm/playlists.rs | 17 +++++++++++++++++ nghe-backend/src/orm/playlists_songs.rs | 4 ++++ nghe-backend/src/orm/playlists_users.rs | 4 ++++ nghe-backend/src/route/mod.rs | 1 + nghe-backend/src/route/playlists/mod.rs | 0 7 files changed, 47 insertions(+) create mode 100644 nghe-backend/src/orm/playlist/mod.rs create mode 100644 nghe-backend/src/orm/playlists.rs create mode 100644 nghe-backend/src/orm/playlists_songs.rs create mode 100644 nghe-backend/src/orm/playlists_users.rs create mode 100644 nghe-backend/src/route/playlists/mod.rs diff --git a/nghe-backend/src/orm/mod.rs b/nghe-backend/src/orm/mod.rs index 8b4082ba..b3d961c6 100644 --- a/nghe-backend/src/orm/mod.rs +++ b/nghe-backend/src/orm/mod.rs @@ -9,6 +9,10 @@ pub mod id3; pub mod music_folders; pub mod permission; pub mod playbacks; +pub mod playlist; +pub mod playlists; +pub mod playlists_songs; +pub mod playlists_users; pub mod songs; pub mod songs_album_artists; pub mod songs_artists; diff --git a/nghe-backend/src/orm/playlist/mod.rs b/nghe-backend/src/orm/playlist/mod.rs new file mode 100644 index 00000000..1ed27f22 --- /dev/null +++ b/nghe-backend/src/orm/playlist/mod.rs @@ -0,0 +1,17 @@ +use diesel::prelude::*; +use time::OffsetDateTime; +use uuid::Uuid; + +use crate::orm::playlists; + +#[derive(Debug, Queryable, Selectable)] +#[diesel(table_name = playlists, check_for_backend(crate::orm::Type))] +#[diesel(treat_none_as_null = false)] +pub struct Playlist { + pub id: Uuid, + pub name: String, + pub comment: Option, + pub public: bool, + #[diesel(column_name = created_at)] + pub created: OffsetDateTime, +} diff --git a/nghe-backend/src/orm/playlists.rs b/nghe-backend/src/orm/playlists.rs new file mode 100644 index 00000000..523e1542 --- /dev/null +++ b/nghe-backend/src/orm/playlists.rs @@ -0,0 +1,17 @@ +#![allow(clippy::option_option)] + +use std::borrow::Cow; + +use diesel::prelude::*; +use diesel_derives::AsChangeset; + +pub use crate::schema::playlists::{self, *}; + +#[derive(Insertable, AsChangeset, Default)] +#[diesel(table_name = playlists, check_for_backend(crate::orm::Type))] +#[diesel(treat_none_as_null = false)] +pub struct Upsert<'a> { + pub name: Option>, + pub comment: Option>>, + pub public: Option, +} diff --git a/nghe-backend/src/orm/playlists_songs.rs b/nghe-backend/src/orm/playlists_songs.rs new file mode 100644 index 00000000..ad89cc9f --- /dev/null +++ b/nghe-backend/src/orm/playlists_songs.rs @@ -0,0 +1,4 @@ +use diesel::prelude::*; +use diesel_derives::AsChangeset; + +pub use crate::schema::playlists_songs::{self, *}; diff --git a/nghe-backend/src/orm/playlists_users.rs b/nghe-backend/src/orm/playlists_users.rs new file mode 100644 index 00000000..688e03e7 --- /dev/null +++ b/nghe-backend/src/orm/playlists_users.rs @@ -0,0 +1,4 @@ +use diesel::prelude::*; +use diesel_derives::AsChangeset; + +pub use crate::schema::playlists_users::{self, *}; diff --git a/nghe-backend/src/route/mod.rs b/nghe-backend/src/route/mod.rs index 84e4dcd0..81fa69d6 100644 --- a/nghe-backend/src/route/mod.rs +++ b/nghe-backend/src/route/mod.rs @@ -4,6 +4,7 @@ pub mod media_annotation; pub mod media_retrieval; pub mod music_folder; pub mod permission; +pub mod playlists; pub mod scan; pub mod search; pub mod system; diff --git a/nghe-backend/src/route/playlists/mod.rs b/nghe-backend/src/route/playlists/mod.rs new file mode 100644 index 00000000..e69de29b From 9b9266e7ab156025b8d817358fa1832853db8d01 Mon Sep 17 00:00:00 2001 From: Vo Van Nghia Date: Thu, 28 Nov 2024 09:15:05 +0100 Subject: [PATCH 06/20] backend/playlists: add create playlist --- .../2024-04-19-095649_create_playlists/up.sql | 12 ++-- nghe-backend/src/lib.rs | 1 + nghe-backend/src/orm/playlist/mod.rs | 2 + nghe-backend/src/orm/playlist/permission.rs | 56 +++++++++++++++++++ nghe-backend/src/orm/playlists.rs | 20 +++++++ nghe-backend/src/orm/playlists_songs.rs | 41 ++++++++++++++ nghe-backend/src/orm/playlists_users.rs | 36 ++++++++++++ .../src/route/playlists/create_playlist.rs | 41 ++++++++++++++ nghe-backend/src/route/playlists/mod.rs | 5 ++ nghe-backend/src/schema.rs | 3 +- 10 files changed, 209 insertions(+), 8 deletions(-) create mode 100644 nghe-backend/src/orm/playlist/permission.rs create mode 100644 nghe-backend/src/route/playlists/create_playlist.rs diff --git a/nghe-backend/migrations/2024-04-19-095649_create_playlists/up.sql b/nghe-backend/migrations/2024-04-19-095649_create_playlists/up.sql index e303904e..8fc125b7 100644 --- a/nghe-backend/migrations/2024-04-19-095649_create_playlists/up.sql +++ b/nghe-backend/migrations/2024-04-19-095649_create_playlists/up.sql @@ -10,16 +10,11 @@ create table playlists ( select add_updated_at('playlists'); --- access level: --- 1 - read songs --- 2 - add/remove songs --- 3 - admin (add/remove users, edit, delete) create table playlists_users ( playlist_id uuid not null, user_id uuid not null, - access_level smallint not null constraint playlists_users_access_level check ( - access_level between 1 and 3 - ), + write boolean not null, + owner boolean not null default false, constraint playlists_users_pkey primary key (playlist_id, user_id), constraint playlists_users_playlist_id_fkey foreign key ( playlist_id @@ -29,6 +24,9 @@ create table playlists_users ( ) references users (id) on delete cascade ); +create unique index playlists_users_owner on playlists_users (playlist_id) +where owner; + create table playlists_songs ( playlist_id uuid not null, song_id uuid not null, diff --git a/nghe-backend/src/lib.rs b/nghe-backend/src/lib.rs index 04991882..00a2662d 100644 --- a/nghe-backend/src/lib.rs +++ b/nghe-backend/src/lib.rs @@ -62,6 +62,7 @@ pub async fn build(config: config::Config) -> Router { .merge(route::browsing::router()) .merge(route::lists::router()) .merge(route::media_annotation::router()) + .merge(route::playlists::router()) .merge(route::search::router()) .merge(route::system::router()) .with_state(database::Database::new(&config.database)) diff --git a/nghe-backend/src/orm/playlist/mod.rs b/nghe-backend/src/orm/playlist/mod.rs index 1ed27f22..a74eef07 100644 --- a/nghe-backend/src/orm/playlist/mod.rs +++ b/nghe-backend/src/orm/playlist/mod.rs @@ -1,3 +1,5 @@ +pub mod permission; + use diesel::prelude::*; use time::OffsetDateTime; use uuid::Uuid; diff --git a/nghe-backend/src/orm/playlist/permission.rs b/nghe-backend/src/orm/playlist/permission.rs new file mode 100644 index 00000000..85f1e0c9 --- /dev/null +++ b/nghe-backend/src/orm/playlist/permission.rs @@ -0,0 +1,56 @@ +use diesel::dsl::{exists, select}; +use diesel_async::RunQueryDsl; +use uuid::Uuid; + +use crate::database::Database; +use crate::Error; + +pub async fn check( + database: &Database, + playlist_id: Uuid, + user_id: Uuid, + write: bool, + owner: bool, +) -> Result<(), Error> { + let exist = if owner { + select(exists(query::owner(playlist_id, user_id))) + .get_result(&mut database.get().await?) + .await? + } else if write { + select(exists(query::write(playlist_id, user_id))) + .get_result(&mut database.get().await?) + .await? + } else { + select(exists(query::read(playlist_id, user_id))) + .get_result(&mut database.get().await?) + .await? + }; + if exist { Ok(()) } else { Err(Error::NotFound) } +} + +pub mod query { + use diesel::dsl::auto_type; + use diesel::{ExpressionMethods, QueryDsl}; + + use super::*; + use crate::orm::playlists_users; + + #[auto_type] + pub fn read(playlist_id: Uuid, user_id: Uuid) -> _ { + playlists_users::table + .filter(playlists_users::playlist_id.eq(playlist_id)) + .filter(playlists_users::user_id.eq(user_id)) + } + + #[auto_type] + pub fn write(playlist_id: Uuid, user_id: Uuid) -> _ { + let read: read = read(playlist_id, user_id); + read.filter(playlists_users::write) + } + + #[auto_type] + pub fn owner(playlist_id: Uuid, user_id: Uuid) -> _ { + let read: read = read(playlist_id, user_id); + read.filter(playlists_users::owner) + } +} diff --git a/nghe-backend/src/orm/playlists.rs b/nghe-backend/src/orm/playlists.rs index 523e1542..7ae3d8e4 100644 --- a/nghe-backend/src/orm/playlists.rs +++ b/nghe-backend/src/orm/playlists.rs @@ -15,3 +15,23 @@ pub struct Upsert<'a> { pub comment: Option>>, pub public: Option, } + +mod upsert { + use diesel_async::RunQueryDsl; + use uuid::Uuid; + + use super::{playlists, Upsert}; + use crate::database::Database; + use crate::Error; + + impl crate::orm::upsert::Insert for Upsert<'_> { + async fn insert(&self, database: &Database) -> Result { + diesel::insert_into(playlists::table) + .values(self) + .returning(playlists::id) + .get_result(&mut database.get().await?) + .await + .map_err(Error::from) + } + } +} diff --git a/nghe-backend/src/orm/playlists_songs.rs b/nghe-backend/src/orm/playlists_songs.rs index ad89cc9f..ac82cb28 100644 --- a/nghe-backend/src/orm/playlists_songs.rs +++ b/nghe-backend/src/orm/playlists_songs.rs @@ -1,4 +1,45 @@ use diesel::prelude::*; use diesel_derives::AsChangeset; +use uuid::Uuid; pub use crate::schema::playlists_songs::{self, *}; + +#[derive(Insertable, AsChangeset)] +#[diesel(table_name = playlists_songs, check_for_backend(crate::orm::Type))] +pub struct Upsert { + pub playlist_id: Uuid, + pub song_id: Uuid, +} + +mod upsert { + use diesel::ExpressionMethods; + use diesel_async::RunQueryDsl; + use uuid::Uuid; + + use super::{playlists_songs, Upsert}; + use crate::database::Database; + use crate::Error; + + impl Upsert { + pub async fn upsert(&self, database: &Database) -> Result<(), Error> { + diesel::insert_into(playlists_songs::table) + .values((self, playlists_songs::created_at.eq(crate::time::now().await))) + .on_conflict_do_nothing() + .execute(&mut database.get().await?) + .await?; + Ok(()) + } + + pub async fn upserts( + database: &Database, + playlist_id: Uuid, + song_ids: &[Uuid], + ) -> Result<(), Error> { + // Upserting one by one to avoid two songs having too close `created_at`. + for song_id in song_ids.iter().copied() { + Self { playlist_id, song_id }.upsert(database).await?; + } + Ok(()) + } + } +} diff --git a/nghe-backend/src/orm/playlists_users.rs b/nghe-backend/src/orm/playlists_users.rs index 688e03e7..da4ad081 100644 --- a/nghe-backend/src/orm/playlists_users.rs +++ b/nghe-backend/src/orm/playlists_users.rs @@ -1,4 +1,40 @@ use diesel::prelude::*; use diesel_derives::AsChangeset; +use uuid::Uuid; pub use crate::schema::playlists_users::{self, *}; + +#[derive(Insertable, AsChangeset)] +#[diesel(table_name = playlists_users, check_for_backend(crate::orm::Type))] +pub struct Upsert { + pub playlist_id: Uuid, + pub user_id: Uuid, + pub write: bool, +} + +mod upsert { + use diesel::ExpressionMethods; + use diesel_async::RunQueryDsl; + use uuid::Uuid; + + use super::{playlists_users, Upsert}; + use crate::database::Database; + use crate::Error; + + impl Upsert { + pub async fn insert_owner( + database: &Database, + playlist_id: Uuid, + user_id: Uuid, + ) -> Result<(), Error> { + diesel::insert_into(playlists_users::table) + .values(( + Upsert { playlist_id, user_id, write: true }, + playlists_users::owner.eq(true), + )) + .execute(&mut database.get().await?) + .await?; + Ok(()) + } + } +} diff --git a/nghe-backend/src/route/playlists/create_playlist.rs b/nghe-backend/src/route/playlists/create_playlist.rs new file mode 100644 index 00000000..fa6be3d1 --- /dev/null +++ b/nghe-backend/src/route/playlists/create_playlist.rs @@ -0,0 +1,41 @@ +use diesel::ExpressionMethods; +use diesel_async::RunQueryDsl; +use nghe_api::playlists::create_playlist::CreateOrUpdate; +pub use nghe_api::playlists::create_playlist::{Request, Response}; +use nghe_proc_macro::handler; +use uuid::Uuid; + +use crate::database::Database; +use crate::orm::upsert::Insert; +use crate::orm::{playlist, playlists, playlists_songs, playlists_users}; +use crate::Error; + +#[handler] +pub async fn handler( + database: &Database, + user_id: Uuid, + request: Request, +) -> Result { + let playlist_id = match request.create_or_update { + CreateOrUpdate::Create { name } => { + let playlist_id = playlists::Upsert { name: Some(name.into()), ..Default::default() } + .insert(database) + .await?; + playlists_users::Upsert::insert_owner(database, playlist_id, user_id).await?; + playlist_id + } + CreateOrUpdate::Update { playlist_id } => { + playlist::permission::check(database, playlist_id, user_id, true, false).await?; + diesel::delete(playlists_songs::table) + .filter(playlists_songs::playlist_id.eq(playlist_id)) + .execute(&mut database.get().await?) + .await?; + playlist_id + } + }; + if let Some(ref song_ids) = request.song_ids { + playlists_songs::Upsert::upserts(database, playlist_id, song_ids).await?; + } + + Ok(Response {}) +} diff --git a/nghe-backend/src/route/playlists/mod.rs b/nghe-backend/src/route/playlists/mod.rs index e69de29b..a7f48696 100644 --- a/nghe-backend/src/route/playlists/mod.rs +++ b/nghe-backend/src/route/playlists/mod.rs @@ -0,0 +1,5 @@ +mod create_playlist; + +nghe_proc_macro::build_router! { + modules = [create_playlist] +} diff --git a/nghe-backend/src/schema.rs b/nghe-backend/src/schema.rs index 3b4c71af..6039bf2f 100644 --- a/nghe-backend/src/schema.rs +++ b/nghe-backend/src/schema.rs @@ -162,7 +162,8 @@ diesel::table! { playlists_users (playlist_id, user_id) { playlist_id -> Uuid, user_id -> Uuid, - access_level -> Int2, + write -> Bool, + owner -> Bool, } } From 91c089e49c5971c09f4d87079f58b71d69ea07dc Mon Sep 17 00:00:00 2001 From: Vo Van Nghia Date: Thu, 28 Nov 2024 10:18:22 +0100 Subject: [PATCH 07/20] backend/playlists: add query playlist --- nghe-api/src/playlists/create_playlist.rs | 33 ++++++---- nghe-api/src/playlists/mod.rs | 17 +---- nghe-api/src/playlists/playlist.rs | 24 +++++++ nghe-backend/src/orm/playlist/mod.rs | 80 +++++++++++++++++++++++ nghe-backend/src/route/playlists/mod.rs | 2 +- 5 files changed, 127 insertions(+), 29 deletions(-) create mode 100644 nghe-api/src/playlists/playlist.rs diff --git a/nghe-api/src/playlists/create_playlist.rs b/nghe-api/src/playlists/create_playlist.rs index 6902519e..88828c67 100644 --- a/nghe-api/src/playlists/create_playlist.rs +++ b/nghe-api/src/playlists/create_playlist.rs @@ -30,6 +30,23 @@ pub struct Response { // TODO: Return playlist } +#[cfg(any(test, feature = "test"))] +mod test { + use super::*; + + impl From for CreateOrUpdate { + fn from(value: String) -> Self { + Self::Create { name: value } + } + } + + impl From for CreateOrUpdate { + fn from(value: Uuid) -> Self { + Self::Update { playlist_id: value } + } + } +} + #[cfg(test)] mod tests { use rstest::rstest; @@ -41,9 +58,7 @@ mod tests { #[case( "name=ef14c42b-6efa-45f3-961c-74856fd431d5", Some(Request { - create_or_update: CreateOrUpdate::Create { - name: "ef14c42b-6efa-45f3-961c-74856fd431d5".to_owned() - }, + create_or_update: "ef14c42b-6efa-45f3-961c-74856fd431d5".to_owned().into(), song_ids: None, }) )] @@ -51,18 +66,14 @@ mod tests { "name=ef14c42b-6efa-45f3-961c-74856fd431d5&\ songId=2b839103-04ab-4b39-9b05-8c664590eda4", Some(Request { - create_or_update: CreateOrUpdate::Create { - name: "ef14c42b-6efa-45f3-961c-74856fd431d5".to_owned() - }, + create_or_update: "ef14c42b-6efa-45f3-961c-74856fd431d5".to_owned().into(), song_ids: Some(vec![uuid!("2b839103-04ab-4b39-9b05-8c664590eda4")]), }) )] #[case( "playlistId=ef14c42b-6efa-45f3-961c-74856fd431d5", Some(Request { - create_or_update: CreateOrUpdate::Update { - playlist_id: uuid!("ef14c42b-6efa-45f3-961c-74856fd431d5") - }, + create_or_update: uuid!("ef14c42b-6efa-45f3-961c-74856fd431d5").into(), song_ids: None, }) )] @@ -70,9 +81,7 @@ mod tests { "playlistId=ef14c42b-6efa-45f3-961c-74856fd431d5&\ songId=2b839103-04ab-4b39-9b05-8c664590eda4", Some(Request { - create_or_update: CreateOrUpdate::Update { - playlist_id: uuid!("ef14c42b-6efa-45f3-961c-74856fd431d5") - }, + create_or_update: uuid!("ef14c42b-6efa-45f3-961c-74856fd431d5").into(), song_ids: Some(vec![uuid!("2b839103-04ab-4b39-9b05-8c664590eda4")]), }) )] diff --git a/nghe-api/src/playlists/mod.rs b/nghe-api/src/playlists/mod.rs index f07940d6..09491ea3 100644 --- a/nghe-api/src/playlists/mod.rs +++ b/nghe-api/src/playlists/mod.rs @@ -1,17 +1,2 @@ pub mod create_playlist; - -use nghe_proc_macro::api_derive; -use time::OffsetDateTime; -use uuid::Uuid; - -#[api_derive] -pub struct Playlist { - pub id: Uuid, - pub name: String, - pub comment: Option, - pub public: bool, - pub song_count: u16, - pub duration: u32, - pub created: OffsetDateTime, - pub changed: OffsetDateTime, -} +pub mod playlist; diff --git a/nghe-api/src/playlists/playlist.rs b/nghe-api/src/playlists/playlist.rs new file mode 100644 index 00000000..5716047b --- /dev/null +++ b/nghe-api/src/playlists/playlist.rs @@ -0,0 +1,24 @@ +use bon::Builder; +use nghe_proc_macro::api_derive; +use time::OffsetDateTime; +use uuid::Uuid; + +#[api_derive] +#[derive(Builder)] +#[builder(on(_, required))] +#[builder(state_mod(vis = "pub"))] +pub struct Playlist { + pub id: Uuid, + pub name: String, + pub comment: Option, + pub public: bool, + pub song_count: u16, + pub duration: u32, + pub created: OffsetDateTime, + pub changed: OffsetDateTime, +} + +pub mod builder { + pub use super::playlist_builder::*; + pub use super::PlaylistBuilder as Builder; +} diff --git a/nghe-backend/src/orm/playlist/mod.rs b/nghe-backend/src/orm/playlist/mod.rs index a74eef07..7c16e139 100644 --- a/nghe-backend/src/orm/playlist/mod.rs +++ b/nghe-backend/src/orm/playlist/mod.rs @@ -1,6 +1,11 @@ pub mod permission; +use diesel::dsl::sql; +use diesel::expression::SqlLiteral; use diesel::prelude::*; +use diesel::sql_types; +use nghe_api::playlists::playlist; +use nghe_api::playlists::playlist::builder; use time::OffsetDateTime; use uuid::Uuid; @@ -16,4 +21,79 @@ pub struct Playlist { pub public: bool, #[diesel(column_name = created_at)] pub created: OffsetDateTime, + #[diesel(select_expression = sql( + "greatest(max(playlists_songs.created_at), playlists.updated_at)" + ))] + #[diesel(select_expression_type = SqlLiteral)] + pub changed: OffsetDateTime, +} + +pub type BuilderSet = builder::SetChanged< + builder::SetCreated>>>, +>; + +impl Playlist { + pub fn into_builder(self) -> builder::Builder { + playlist::Playlist::builder() + .id(self.id) + .name(self.name) + .comment(self.comment) + .public(self.public) + .created(self.created) + .changed(self.changed) + } +} + +pub mod query { + use diesel::dsl::{auto_type, AsSelect}; + + use super::*; + use crate::orm::{playlists, playlists_songs}; + + #[auto_type] + pub fn unchecked_no_group_by() -> _ { + playlists::table.left_join(playlists_songs::table) + } + + #[auto_type] + pub fn unchecked() -> _ { + let playlist: AsSelect = Playlist::as_select(); + unchecked_no_group_by().group_by(playlists::id).select(playlist) + } +} + +#[cfg(test)] +mod tests { + use diesel_async::RunQueryDsl; + use fake::{Fake, Faker}; + use rstest::rstest; + + use super::*; + use crate::route::playlists::create_playlist; + use crate::test::{mock, Mock}; + + #[rstest] + #[tokio::test] + async fn test_query_playlist(#[future(awt)] mock: Mock, #[values(0, 5)] n_song: usize) { + let mut music_folder = mock.music_folder(0).await; + music_folder.add_audio().n_song(n_song).call().await; + + create_playlist::handler( + mock.database(), + mock.user_id(0).await, + create_playlist::Request { + create_or_update: Faker.fake::().into(), + song_ids: Some(music_folder.database.keys().copied().collect()), + }, + ) + .await + .unwrap(); + + let playlist = query::unchecked().get_result(&mut mock.get().await).await.unwrap(); + if n_song == 0 { + assert_eq!(playlist.created, playlist.changed); + } else { + assert!(playlist.changed > playlist.created); + } + } } diff --git a/nghe-backend/src/route/playlists/mod.rs b/nghe-backend/src/route/playlists/mod.rs index a7f48696..97610d13 100644 --- a/nghe-backend/src/route/playlists/mod.rs +++ b/nghe-backend/src/route/playlists/mod.rs @@ -1,4 +1,4 @@ -mod create_playlist; +pub mod create_playlist; nghe_proc_macro::build_router! { modules = [create_playlist] From 77a332e8e7d73cea15002a869b2f38d4cb4d697b Mon Sep 17 00:00:00 2001 From: Vo Van Nghia Date: Thu, 28 Nov 2024 11:25:18 +0100 Subject: [PATCH 08/20] backend/playlists: add query for short --- nghe-backend/src/orm/id3/duration.rs | 18 +++-- nghe-backend/src/orm/id3/song/durations.rs | 9 +-- nghe-backend/src/orm/playlist/mod.rs | 18 ++--- nghe-backend/src/orm/playlist/short.rs | 84 ++++++++++++++++++++++ 4 files changed, 111 insertions(+), 18 deletions(-) create mode 100644 nghe-backend/src/orm/playlist/short.rs diff --git a/nghe-backend/src/orm/id3/duration.rs b/nghe-backend/src/orm/id3/duration.rs index 31a10edc..c0fe8db3 100644 --- a/nghe-backend/src/orm/id3/duration.rs +++ b/nghe-backend/src/orm/id3/duration.rs @@ -18,12 +18,18 @@ impl Trait for f32 { impl Trait for song::durations::Durations { fn duration(&self) -> Result { self.value - .iter() - .copied() - .reduce(song::durations::Duration::add) - .ok_or_else(|| Error::DatabaseSongDurationIsEmpty)? - .value - .duration() + .as_ref() + .map(|value| { + value + .iter() + .copied() + .reduce(song::durations::Duration::add) + .ok_or_else(|| Error::DatabaseSongDurationIsEmpty)? + .value + .duration() + }) + .transpose() + .map(Option::unwrap_or_default) } } diff --git a/nghe-backend/src/orm/id3/song/durations.rs b/nghe-backend/src/orm/id3/song/durations.rs index 8433149a..17d9844e 100644 --- a/nghe-backend/src/orm/id3/song/durations.rs +++ b/nghe-backend/src/orm/id3/song/durations.rs @@ -18,10 +18,11 @@ pub type SqlType = sql_types::Record<(sql_types::Uuid, sql_types::Float)>; #[derive(Debug, Queryable, Selectable)] pub struct Durations { #[diesel(select_expression = sql( - "array_agg(distinct(songs.id, songs.duration)) song_id_durations" + "array_agg(distinct(songs.id, songs.duration)) \ + filter (where songs.id is not null) song_id_durations" ))] - #[diesel(select_expression_type = SqlLiteral::>)] - pub value: Vec, + #[diesel(select_expression_type = SqlLiteral::>>)] + pub value: Option>, } impl Add for Duration { @@ -34,7 +35,7 @@ impl Add for Duration { impl Durations { pub fn count(&self) -> usize { - self.value.len() + self.value.as_ref().map(Vec::len).unwrap_or_default() } } diff --git a/nghe-backend/src/orm/playlist/mod.rs b/nghe-backend/src/orm/playlist/mod.rs index 7c16e139..96713f82 100644 --- a/nghe-backend/src/orm/playlist/mod.rs +++ b/nghe-backend/src/orm/playlist/mod.rs @@ -1,11 +1,11 @@ pub mod permission; +pub mod short; use diesel::dsl::sql; use diesel::expression::SqlLiteral; use diesel::prelude::*; use diesel::sql_types; -use nghe_api::playlists::playlist; -use nghe_api::playlists::playlist::builder; +use nghe_api::playlists::playlist::{self, builder}; use time::OffsetDateTime; use uuid::Uuid; @@ -48,11 +48,13 @@ pub mod query { use diesel::dsl::{auto_type, AsSelect}; use super::*; - use crate::orm::{playlists, playlists_songs}; + use crate::orm::{playlists, playlists_songs, songs}; #[auto_type] pub fn unchecked_no_group_by() -> _ { - playlists::table.left_join(playlists_songs::table) + playlists::table + .left_join(playlists_songs::table) + .left_join(songs::table.on(songs::id.eq(playlists_songs::song_id))) } #[auto_type] @@ -74,7 +76,7 @@ mod tests { #[rstest] #[tokio::test] - async fn test_query_playlist(#[future(awt)] mock: Mock, #[values(0, 5)] n_song: usize) { + async fn test_query(#[future(awt)] mock: Mock, #[values(0, 5)] n_song: usize) { let mut music_folder = mock.music_folder(0).await; music_folder.add_audio().n_song(n_song).call().await; @@ -89,11 +91,11 @@ mod tests { .await .unwrap(); - let playlist = query::unchecked().get_result(&mut mock.get().await).await.unwrap(); + let database_playlist = query::unchecked().get_result(&mut mock.get().await).await.unwrap(); if n_song == 0 { - assert_eq!(playlist.created, playlist.changed); + assert_eq!(database_playlist.created, database_playlist.changed); } else { - assert!(playlist.changed > playlist.created); + assert!(database_playlist.changed > database_playlist.created); } } } diff --git a/nghe-backend/src/orm/playlist/short.rs b/nghe-backend/src/orm/playlist/short.rs new file mode 100644 index 00000000..06f17adb --- /dev/null +++ b/nghe-backend/src/orm/playlist/short.rs @@ -0,0 +1,84 @@ +use diesel::prelude::*; +use nghe_api::playlists::playlist::{self, builder}; + +use super::Playlist; +use crate::orm::id3::duration::Trait as _; +use crate::orm::id3::song; +use crate::Error; + +#[derive(Debug, Queryable, Selectable)] +pub struct Short { + #[diesel(embed)] + pub playlist: Playlist, + #[diesel(embed)] + pub durations: song::durations::Durations, +} + +pub type BuilderSet = builder::SetDuration>; + +impl Short { + pub fn try_into_builder(self) -> Result, Error> { + Ok(self + .playlist + .into_builder() + .song_count(self.durations.count().try_into()?) + .duration(self.durations.duration()?)) + } +} + +impl TryFrom for playlist::Playlist { + type Error = Error; + + fn try_from(value: Short) -> Result { + Ok(value.try_into_builder()?.build()) + } +} + +pub mod query { + use diesel::dsl::{auto_type, AsSelect}; + + use super::*; + use crate::orm::playlist; + + #[auto_type] + pub fn unchecked() -> _ { + let short: AsSelect = Short::as_select(); + playlist::query::unchecked().select(short) + } +} + +#[cfg(test)] +mod tests { + use diesel_async::RunQueryDsl; + use fake::{Fake, Faker}; + use rstest::rstest; + + use super::*; + use crate::route::playlists::create_playlist; + use crate::test::{mock, Mock}; + + #[rstest] + #[tokio::test] + async fn test_query(#[future(awt)] mock: Mock, #[values(0, 5)] n_song: usize) { + let mut music_folder = mock.music_folder(0).await; + music_folder.add_audio().n_song(n_song).call().await; + + create_playlist::handler( + mock.database(), + mock.user_id(0).await, + create_playlist::Request { + create_or_update: Faker.fake::().into(), + song_ids: Some(music_folder.database.keys().copied().collect()), + }, + ) + .await + .unwrap(); + + let database_playlist = query::unchecked().get_result(&mut mock.get().await).await.unwrap(); + assert_eq!(database_playlist.durations.count(), n_song); + assert_eq!( + database_playlist.durations.duration().unwrap(), + music_folder.database.duration().unwrap() + ); + } +} From 72f1bd15423867829aa024722b35dda0f5618e18 Mon Sep 17 00:00:00 2001 From: Vo Van Nghia Date: Thu, 28 Nov 2024 12:00:52 +0100 Subject: [PATCH 09/20] typo --- nghe-backend/src/orm/id3/album/full.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nghe-backend/src/orm/id3/album/full.rs b/nghe-backend/src/orm/id3/album/full.rs index 541bb6fc..e5144cd8 100644 --- a/nghe-backend/src/orm/id3/album/full.rs +++ b/nghe-backend/src/orm/id3/album/full.rs @@ -22,7 +22,7 @@ pub struct Full { #[diesel(select_expression = sql("bool_or(songs_album_artists.compilation) is_compilation"))] #[diesel(select_expression_type = SqlLiteral::)] pub is_compilation: bool, - #[diesel(select_expression = sql("array_agg(distinct(songs.id)) album_artists"))] + #[diesel(select_expression = sql("array_agg(distinct(songs.id)) song_ids"))] #[diesel(select_expression_type = SqlLiteral::>)] pub songs: Vec, } From ebe47b6c2daa73a7bfd757bfcc28240aacd936ee Mon Sep 17 00:00:00 2001 From: Vo Van Nghia Date: Thu, 28 Nov 2024 12:13:47 +0100 Subject: [PATCH 10/20] backend/playlist: add query for full --- nghe-api/src/playlists/playlist.rs | 9 +++ nghe-backend/src/orm/playlist/full.rs | 94 ++++++++++++++++++++++++++ nghe-backend/src/orm/playlist/mod.rs | 7 +- nghe-backend/src/orm/playlist/short.rs | 6 +- 4 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 nghe-backend/src/orm/playlist/full.rs diff --git a/nghe-api/src/playlists/playlist.rs b/nghe-api/src/playlists/playlist.rs index 5716047b..a1c30e5c 100644 --- a/nghe-api/src/playlists/playlist.rs +++ b/nghe-api/src/playlists/playlist.rs @@ -3,6 +3,8 @@ use nghe_proc_macro::api_derive; use time::OffsetDateTime; use uuid::Uuid; +use crate::id3; + #[api_derive] #[derive(Builder)] #[builder(on(_, required))] @@ -18,6 +20,13 @@ pub struct Playlist { pub changed: OffsetDateTime, } +#[api_derive] +pub struct Full { + #[serde(flatten)] + pub playlist: Playlist, + pub entry: Vec, +} + pub mod builder { pub use super::playlist_builder::*; pub use super::PlaylistBuilder as Builder; diff --git a/nghe-backend/src/orm/playlist/full.rs b/nghe-backend/src/orm/playlist/full.rs new file mode 100644 index 00000000..5533f087 --- /dev/null +++ b/nghe-backend/src/orm/playlist/full.rs @@ -0,0 +1,94 @@ +use diesel::dsl::sql; +use diesel::expression::SqlLiteral; +use diesel::prelude::*; +use diesel::sql_types; +use diesel_async::RunQueryDsl; +use nghe_api::playlists::playlist; +use uuid::Uuid; + +use super::Playlist; +use crate::database::Database; +use crate::orm::id3::duration::Trait; +use crate::orm::id3::song; +use crate::orm::songs; +use crate::Error; + +#[derive(Debug, Queryable, Selectable)] +pub struct Full { + #[diesel(embed)] + pub playlist: Playlist, + #[diesel(select_expression = sql( + "array_remove(array_agg(distinct(playlists_songs.song_id)), null) entry_ids" + ))] + #[diesel(select_expression_type = SqlLiteral::>)] + pub entries: Vec, +} + +impl Full { + pub async fn try_into(self, database: &Database) -> Result { + let entry = song::query::unchecked() + .filter(songs::id.eq_any(self.entries)) + .get_results(&mut database.get().await?) + .await?; + let duration = entry.duration()?; + let entry: Vec<_> = entry.into_iter().map(song::Song::try_into).try_collect()?; + + let playlist = self + .playlist + .into_builder() + .song_count(entry.len().try_into()?) + .duration(duration) + .build(); + + Ok(playlist::Full { playlist, entry }) + } +} + +pub mod query { + use diesel::dsl::{auto_type, AsSelect}; + + use super::*; + use crate::orm::playlist; + + #[auto_type] + pub fn unchecked() -> _ { + let full: AsSelect = Full::as_select(); + playlist::query::unchecked().select(full) + } +} + +#[cfg(test)] +mod tests { + use diesel_async::RunQueryDsl; + use fake::{Fake, Faker}; + use indexmap::IndexSet; + use rstest::rstest; + + use super::*; + use crate::route::playlists::create_playlist; + use crate::test::{mock, Mock}; + + #[rstest] + #[tokio::test] + async fn test_query(#[future(awt)] mock: Mock, #[values(0, 5)] n_song: usize) { + let mut music_folder = mock.music_folder(0).await; + music_folder.add_audio().n_song(n_song).call().await; + + create_playlist::handler( + mock.database(), + mock.user_id(0).await, + create_playlist::Request { + create_or_update: Faker.fake::().into(), + song_ids: Some(music_folder.database.keys().copied().collect()), + }, + ) + .await + .unwrap(); + + let database_playlist = query::unchecked().get_result(&mut mock.get().await).await.unwrap(); + assert_eq!( + database_playlist.entries.iter().collect::>(), + music_folder.database.keys().collect::>() + ); + } +} diff --git a/nghe-backend/src/orm/playlist/mod.rs b/nghe-backend/src/orm/playlist/mod.rs index 96713f82..2fddff82 100644 --- a/nghe-backend/src/orm/playlist/mod.rs +++ b/nghe-backend/src/orm/playlist/mod.rs @@ -1,3 +1,4 @@ +pub mod full; pub mod permission; pub mod short; @@ -48,13 +49,11 @@ pub mod query { use diesel::dsl::{auto_type, AsSelect}; use super::*; - use crate::orm::{playlists, playlists_songs, songs}; + use crate::orm::{playlists, playlists_songs}; #[auto_type] pub fn unchecked_no_group_by() -> _ { - playlists::table - .left_join(playlists_songs::table) - .left_join(songs::table.on(songs::id.eq(playlists_songs::song_id))) + playlists::table.left_join(playlists_songs::table) } #[auto_type] diff --git a/nghe-backend/src/orm/playlist/short.rs b/nghe-backend/src/orm/playlist/short.rs index 06f17adb..d831fa5c 100644 --- a/nghe-backend/src/orm/playlist/short.rs +++ b/nghe-backend/src/orm/playlist/short.rs @@ -38,12 +38,14 @@ pub mod query { use diesel::dsl::{auto_type, AsSelect}; use super::*; - use crate::orm::playlist; + use crate::orm::{playlist, playlists_songs, songs}; #[auto_type] pub fn unchecked() -> _ { let short: AsSelect = Short::as_select(); - playlist::query::unchecked().select(short) + playlist::query::unchecked() + .left_join(songs::table.on(songs::id.eq(playlists_songs::song_id))) + .select(short) } } From d610475e0d82169ba5dfc04366322d2fab4b8892 Mon Sep 17 00:00:00 2001 From: Vo Van Nghia Date: Thu, 28 Nov 2024 12:23:27 +0100 Subject: [PATCH 11/20] api/playlists: return playlist in create playlist --- nghe-api/src/playlists/create_playlist.rs | 4 +++- nghe-backend/src/route/playlists/create_playlist.rs | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/nghe-api/src/playlists/create_playlist.rs b/nghe-api/src/playlists/create_playlist.rs index 88828c67..f91c7570 100644 --- a/nghe-api/src/playlists/create_playlist.rs +++ b/nghe-api/src/playlists/create_playlist.rs @@ -2,6 +2,8 @@ use nghe_proc_macro::api_derive; use serde_with::serde_as; use uuid::Uuid; +use super::playlist; + // TODO: Optimize this after https://github.com/serde-rs/serde/issues/1183 #[serde_as] #[api_derive(request = true, copy = false)] @@ -27,7 +29,7 @@ pub struct Request { #[api_derive] pub struct Response { - // TODO: Return playlist + pub playlist: playlist::Full, } #[cfg(any(test, feature = "test"))] diff --git a/nghe-backend/src/route/playlists/create_playlist.rs b/nghe-backend/src/route/playlists/create_playlist.rs index fa6be3d1..62eeb43e 100644 --- a/nghe-backend/src/route/playlists/create_playlist.rs +++ b/nghe-backend/src/route/playlists/create_playlist.rs @@ -37,5 +37,11 @@ pub async fn handler( playlists_songs::Upsert::upserts(database, playlist_id, song_ids).await?; } - Ok(Response {}) + Ok(Response { + playlist: playlist::full::query::unchecked() + .get_result(&mut database.get().await?) + .await? + .try_into(database) + .await?, + }) } From 66623ae7ed02445119a16e6f68b132599bff23c1 Mon Sep 17 00:00:00 2001 From: Vo Van Nghia Date: Thu, 28 Nov 2024 12:59:04 +0100 Subject: [PATCH 12/20] backend/playlist: add api/handler for get_playlist --- nghe-api/src/playlists/get_playlist.rs | 15 ++++ nghe-api/src/playlists/mod.rs | 1 + nghe-backend/src/orm/playlist/full.rs | 5 +- nghe-backend/src/orm/playlist/mod.rs | 7 +- .../src/route/playlists/create_playlist.rs | 7 +- .../src/route/playlists/get_playlist.rs | 87 +++++++++++++++++++ nghe-backend/src/route/playlists/mod.rs | 1 + 7 files changed, 111 insertions(+), 12 deletions(-) create mode 100644 nghe-api/src/playlists/get_playlist.rs create mode 100644 nghe-backend/src/route/playlists/get_playlist.rs diff --git a/nghe-api/src/playlists/get_playlist.rs b/nghe-api/src/playlists/get_playlist.rs new file mode 100644 index 00000000..f45332a0 --- /dev/null +++ b/nghe-api/src/playlists/get_playlist.rs @@ -0,0 +1,15 @@ +use nghe_proc_macro::api_derive; +use uuid::Uuid; + +use super::playlist; + +#[api_derive] +#[endpoint(path = "getPlaylist")] +pub struct Request { + pub id: Uuid, +} + +#[api_derive] +pub struct Response { + pub playlist: playlist::Full, +} diff --git a/nghe-api/src/playlists/mod.rs b/nghe-api/src/playlists/mod.rs index 09491ea3..f03d0077 100644 --- a/nghe-api/src/playlists/mod.rs +++ b/nghe-api/src/playlists/mod.rs @@ -1,2 +1,3 @@ pub mod create_playlist; +pub mod get_playlist; pub mod playlist; diff --git a/nghe-backend/src/orm/playlist/full.rs b/nghe-backend/src/orm/playlist/full.rs index 5533f087..da787324 100644 --- a/nghe-backend/src/orm/playlist/full.rs +++ b/nghe-backend/src/orm/playlist/full.rs @@ -10,7 +10,7 @@ use super::Playlist; use crate::database::Database; use crate::orm::id3::duration::Trait; use crate::orm::id3::song; -use crate::orm::songs; +use crate::orm::{playlists_songs, songs}; use crate::Error; #[derive(Debug, Queryable, Selectable)] @@ -27,7 +27,10 @@ pub struct Full { impl Full { pub async fn try_into(self, database: &Database) -> Result { let entry = song::query::unchecked() + .inner_join(playlists_songs::table) .filter(songs::id.eq_any(self.entries)) + .filter(playlists_songs::playlist_id.eq(self.playlist.id)) + .order_by(sql::("any_value(playlists_songs.created_at)")) .get_results(&mut database.get().await?) .await?; let duration = entry.duration()?; diff --git a/nghe-backend/src/orm/playlist/mod.rs b/nghe-backend/src/orm/playlist/mod.rs index 2fddff82..5e13eacd 100644 --- a/nghe-backend/src/orm/playlist/mod.rs +++ b/nghe-backend/src/orm/playlist/mod.rs @@ -51,15 +51,10 @@ pub mod query { use super::*; use crate::orm::{playlists, playlists_songs}; - #[auto_type] - pub fn unchecked_no_group_by() -> _ { - playlists::table.left_join(playlists_songs::table) - } - #[auto_type] pub fn unchecked() -> _ { let playlist: AsSelect = Playlist::as_select(); - unchecked_no_group_by().group_by(playlists::id).select(playlist) + playlists::table.left_join(playlists_songs::table).group_by(playlists::id).select(playlist) } } diff --git a/nghe-backend/src/route/playlists/create_playlist.rs b/nghe-backend/src/route/playlists/create_playlist.rs index 62eeb43e..29c6a055 100644 --- a/nghe-backend/src/route/playlists/create_playlist.rs +++ b/nghe-backend/src/route/playlists/create_playlist.rs @@ -5,6 +5,7 @@ pub use nghe_api::playlists::create_playlist::{Request, Response}; use nghe_proc_macro::handler; use uuid::Uuid; +use super::get_playlist; use crate::database::Database; use crate::orm::upsert::Insert; use crate::orm::{playlist, playlists, playlists_songs, playlists_users}; @@ -38,10 +39,6 @@ pub async fn handler( } Ok(Response { - playlist: playlist::full::query::unchecked() - .get_result(&mut database.get().await?) - .await? - .try_into(database) - .await?, + playlist: get_playlist::handler_unchecked(database, user_id, playlist_id).await?.playlist, }) } diff --git a/nghe-backend/src/route/playlists/get_playlist.rs b/nghe-backend/src/route/playlists/get_playlist.rs new file mode 100644 index 00000000..8af929b9 --- /dev/null +++ b/nghe-backend/src/route/playlists/get_playlist.rs @@ -0,0 +1,87 @@ +use diesel::{ExpressionMethods, QueryDsl}; +use diesel_async::RunQueryDsl; +pub use nghe_api::playlists::get_playlist::{Request, Response}; +use nghe_proc_macro::handler; +use uuid::Uuid; + +use crate::database::Database; +use crate::orm::{playlist, playlists}; +use crate::Error; + +pub async fn handler_unchecked( + database: &Database, + user_id: Uuid, + playlist_id: Uuid, +) -> Result { + Ok(Response { + playlist: playlist::full::query::unchecked() + .filter(playlists::id.eq(playlist_id)) + .get_result(&mut database.get().await?) + .await? + .try_into(database) + .await?, + }) +} + +#[handler] +pub async fn handler( + database: &Database, + user_id: Uuid, + request: Request, +) -> Result { + let playlist_id = request.id; + playlist::permission::check(database, playlist_id, user_id, false, false).await?; + handler_unchecked(database, user_id, playlist_id).await +} + +#[cfg(test)] +mod tests { + use fake::{Fake, Faker}; + use rstest::rstest; + + use crate::route::playlists::create_playlist; + 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; + + let n_song_permission = (2..4).fake(); + let n_song = (2..4).fake(); + music_folder_permission.add_audio().n_song(n_song_permission).call().await; + music_folder.add_audio().n_song(n_song).call().await; + + let song_ids: Vec<_> = music_folder_permission + .database + .keys() + .copied() + .chain(music_folder.database.keys().copied()) + .collect(); + + let playlist = create_playlist::handler( + mock.database(), + mock.user_id(0).await, + create_playlist::Request { + create_or_update: Faker.fake::().into(), + song_ids: Some(song_ids.clone()), + }, + ) + .await + .unwrap() + .playlist; + + let database_song_ids: Vec<_> = playlist.entry.iter().map(|entry| entry.id).collect(); + let index = if allow { 0 } else { n_song_permission }; + assert_eq!(database_song_ids, song_ids[index..]); + } +} diff --git a/nghe-backend/src/route/playlists/mod.rs b/nghe-backend/src/route/playlists/mod.rs index 97610d13..ae041649 100644 --- a/nghe-backend/src/route/playlists/mod.rs +++ b/nghe-backend/src/route/playlists/mod.rs @@ -1,4 +1,5 @@ pub mod create_playlist; +pub mod get_playlist; nghe_proc_macro::build_router! { modules = [create_playlist] From b897e93c5fcfa797fa86a5719bbf5948816e8490 Mon Sep 17 00:00:00 2001 From: Vo Van Nghia Date: Thu, 28 Nov 2024 13:26:13 +0100 Subject: [PATCH 13/20] backend/playlist: add checking music_folder for playlist --- nghe-backend/src/orm/playlist/full.rs | 13 ++++++++----- nghe-backend/src/orm/playlist/mod.rs | 19 ++++++++++++++----- nghe-backend/src/orm/playlist/short.rs | 18 ++++++++++-------- .../src/route/playlists/get_playlist.rs | 2 +- 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/nghe-backend/src/orm/playlist/full.rs b/nghe-backend/src/orm/playlist/full.rs index da787324..c0053db6 100644 --- a/nghe-backend/src/orm/playlist/full.rs +++ b/nghe-backend/src/orm/playlist/full.rs @@ -18,7 +18,7 @@ pub struct Full { #[diesel(embed)] pub playlist: Playlist, #[diesel(select_expression = sql( - "array_remove(array_agg(distinct(playlists_songs.song_id)), null) entry_ids" + "array_remove(array_agg(distinct(songs.id)), null) entry_ids" ))] #[diesel(select_expression_type = SqlLiteral::>)] pub entries: Vec, @@ -54,9 +54,10 @@ pub mod query { use crate::orm::playlist; #[auto_type] - pub fn unchecked() -> _ { + pub fn with_user_id(user_id: Uuid) -> _ { + let with_user_id: playlist::query::with_user_id = playlist::query::with_user_id(user_id); let full: AsSelect = Full::as_select(); - playlist::query::unchecked().select(full) + with_user_id.select(full) } } @@ -77,9 +78,10 @@ mod tests { let mut music_folder = mock.music_folder(0).await; music_folder.add_audio().n_song(n_song).call().await; + let user_id = mock.user_id(0).await; create_playlist::handler( mock.database(), - mock.user_id(0).await, + user_id, create_playlist::Request { create_or_update: Faker.fake::().into(), song_ids: Some(music_folder.database.keys().copied().collect()), @@ -88,7 +90,8 @@ mod tests { .await .unwrap(); - let database_playlist = query::unchecked().get_result(&mut mock.get().await).await.unwrap(); + let database_playlist = + query::with_user_id(user_id).get_result(&mut mock.get().await).await.unwrap(); assert_eq!( database_playlist.entries.iter().collect::>(), music_folder.database.keys().collect::>() diff --git a/nghe-backend/src/orm/playlist/mod.rs b/nghe-backend/src/orm/playlist/mod.rs index 5e13eacd..37b2f3f9 100644 --- a/nghe-backend/src/orm/playlist/mod.rs +++ b/nghe-backend/src/orm/playlist/mod.rs @@ -49,12 +49,19 @@ pub mod query { use diesel::dsl::{auto_type, AsSelect}; use super::*; - use crate::orm::{playlists, playlists_songs}; + use crate::orm::{albums, permission, playlists, playlists_songs, songs}; #[auto_type] - pub fn unchecked() -> _ { + pub fn with_user_id(user_id: Uuid) -> _ { + let permission: permission::with_album = permission::with_album(user_id); let playlist: AsSelect = Playlist::as_select(); - playlists::table.left_join(playlists_songs::table).group_by(playlists::id).select(playlist) + playlists::table + .left_join(playlists_songs::table) + .left_join(songs::table.on(songs::id.eq(playlists_songs::song_id))) + .left_join(albums::table.on(albums::id.eq(songs::album_id))) + .filter(albums::id.is_null().or(permission)) + .group_by(playlists::id) + .select(playlist) } } @@ -74,9 +81,10 @@ mod tests { let mut music_folder = mock.music_folder(0).await; music_folder.add_audio().n_song(n_song).call().await; + let user_id = mock.user_id(0).await; create_playlist::handler( mock.database(), - mock.user_id(0).await, + user_id, create_playlist::Request { create_or_update: Faker.fake::().into(), song_ids: Some(music_folder.database.keys().copied().collect()), @@ -85,7 +93,8 @@ mod tests { .await .unwrap(); - let database_playlist = query::unchecked().get_result(&mut mock.get().await).await.unwrap(); + let database_playlist = + query::with_user_id(user_id).get_result(&mut mock.get().await).await.unwrap(); if n_song == 0 { assert_eq!(database_playlist.created, database_playlist.changed); } else { diff --git a/nghe-backend/src/orm/playlist/short.rs b/nghe-backend/src/orm/playlist/short.rs index d831fa5c..36798dbe 100644 --- a/nghe-backend/src/orm/playlist/short.rs +++ b/nghe-backend/src/orm/playlist/short.rs @@ -36,16 +36,16 @@ impl TryFrom for playlist::Playlist { pub mod query { use diesel::dsl::{auto_type, AsSelect}; + use uuid::Uuid; use super::*; - use crate::orm::{playlist, playlists_songs, songs}; + use crate::orm::playlist; #[auto_type] - pub fn unchecked() -> _ { - let short: AsSelect = Short::as_select(); - playlist::query::unchecked() - .left_join(songs::table.on(songs::id.eq(playlists_songs::song_id))) - .select(short) + pub fn with_user_id(user_id: Uuid) -> _ { + let with_user_id: playlist::query::with_user_id = playlist::query::with_user_id(user_id); + let full: AsSelect = Short::as_select(); + with_user_id.select(full) } } @@ -65,9 +65,10 @@ mod tests { let mut music_folder = mock.music_folder(0).await; music_folder.add_audio().n_song(n_song).call().await; + let user_id = mock.user_id(0).await; create_playlist::handler( mock.database(), - mock.user_id(0).await, + user_id, create_playlist::Request { create_or_update: Faker.fake::().into(), song_ids: Some(music_folder.database.keys().copied().collect()), @@ -76,7 +77,8 @@ mod tests { .await .unwrap(); - let database_playlist = query::unchecked().get_result(&mut mock.get().await).await.unwrap(); + let database_playlist = + query::with_user_id(user_id).get_result(&mut mock.get().await).await.unwrap(); assert_eq!(database_playlist.durations.count(), n_song); assert_eq!( database_playlist.durations.duration().unwrap(), diff --git a/nghe-backend/src/route/playlists/get_playlist.rs b/nghe-backend/src/route/playlists/get_playlist.rs index 8af929b9..11279eb4 100644 --- a/nghe-backend/src/route/playlists/get_playlist.rs +++ b/nghe-backend/src/route/playlists/get_playlist.rs @@ -14,7 +14,7 @@ pub async fn handler_unchecked( playlist_id: Uuid, ) -> Result { Ok(Response { - playlist: playlist::full::query::unchecked() + playlist: playlist::full::query::with_user_id(user_id) .filter(playlists::id.eq(playlist_id)) .get_result(&mut database.get().await?) .await? From 29ca1c25a4b0827dea3944f7d6244e46674f18a7 Mon Sep 17 00:00:00 2001 From: Vo Van Nghia Date: Thu, 28 Nov 2024 14:16:45 +0100 Subject: [PATCH 14/20] backend/playlist: add checking playlists users --- nghe-backend/src/orm/playlist/mod.rs | 4 +- nghe-backend/src/orm/playlist/permission.rs | 9 +-- .../src/route/playlists/create_playlist.rs | 10 ++- .../src/route/playlists/get_playlist.rs | 62 ++++++++++++++----- 4 files changed, 61 insertions(+), 24 deletions(-) diff --git a/nghe-backend/src/orm/playlist/mod.rs b/nghe-backend/src/orm/playlist/mod.rs index 37b2f3f9..1e9aa412 100644 --- a/nghe-backend/src/orm/playlist/mod.rs +++ b/nghe-backend/src/orm/playlist/mod.rs @@ -49,16 +49,18 @@ pub mod query { use diesel::dsl::{auto_type, AsSelect}; use super::*; - use crate::orm::{albums, permission, playlists, playlists_songs, songs}; + use crate::orm::{albums, permission, playlists, playlists_songs, playlists_users, songs}; #[auto_type] pub fn with_user_id(user_id: Uuid) -> _ { let permission: permission::with_album = permission::with_album(user_id); let playlist: AsSelect = Playlist::as_select(); playlists::table + .inner_join(playlists_users::table) .left_join(playlists_songs::table) .left_join(songs::table.on(songs::id.eq(playlists_songs::song_id))) .left_join(albums::table.on(albums::id.eq(songs::album_id))) + .filter(playlists_users::user_id.eq(user_id)) .filter(albums::id.is_null().or(permission)) .group_by(playlists::id) .select(playlist) diff --git a/nghe-backend/src/orm/playlist/permission.rs b/nghe-backend/src/orm/playlist/permission.rs index 85f1e0c9..215b462d 100644 --- a/nghe-backend/src/orm/playlist/permission.rs +++ b/nghe-backend/src/orm/playlist/permission.rs @@ -5,23 +5,18 @@ use uuid::Uuid; use crate::database::Database; use crate::Error; -pub async fn check( +pub async fn check_write( database: &Database, playlist_id: Uuid, user_id: Uuid, - write: bool, owner: bool, ) -> Result<(), Error> { let exist = if owner { select(exists(query::owner(playlist_id, user_id))) .get_result(&mut database.get().await?) .await? - } else if write { - select(exists(query::write(playlist_id, user_id))) - .get_result(&mut database.get().await?) - .await? } else { - select(exists(query::read(playlist_id, user_id))) + select(exists(query::write(playlist_id, user_id))) .get_result(&mut database.get().await?) .await? }; diff --git a/nghe-backend/src/route/playlists/create_playlist.rs b/nghe-backend/src/route/playlists/create_playlist.rs index 29c6a055..996f0930 100644 --- a/nghe-backend/src/route/playlists/create_playlist.rs +++ b/nghe-backend/src/route/playlists/create_playlist.rs @@ -26,7 +26,7 @@ pub async fn handler( playlist_id } CreateOrUpdate::Update { playlist_id } => { - playlist::permission::check(database, playlist_id, user_id, true, false).await?; + playlist::permission::check_write(database, playlist_id, user_id, false).await?; diesel::delete(playlists_songs::table) .filter(playlists_songs::playlist_id.eq(playlist_id)) .execute(&mut database.get().await?) @@ -39,6 +39,12 @@ pub async fn handler( } Ok(Response { - playlist: get_playlist::handler_unchecked(database, user_id, playlist_id).await?.playlist, + playlist: get_playlist::handler( + database, + user_id, + get_playlist::Request { id: playlist_id }, + ) + .await? + .playlist, }) } diff --git a/nghe-backend/src/route/playlists/get_playlist.rs b/nghe-backend/src/route/playlists/get_playlist.rs index 11279eb4..1d353230 100644 --- a/nghe-backend/src/route/playlists/get_playlist.rs +++ b/nghe-backend/src/route/playlists/get_playlist.rs @@ -8,11 +8,13 @@ use crate::database::Database; use crate::orm::{playlist, playlists}; use crate::Error; -pub async fn handler_unchecked( +#[handler] +pub async fn handler( database: &Database, user_id: Uuid, - playlist_id: Uuid, + request: Request, ) -> Result { + let playlist_id = request.id; Ok(Response { playlist: playlist::full::query::with_user_id(user_id) .filter(playlists::id.eq(playlist_id)) @@ -23,28 +25,19 @@ pub async fn handler_unchecked( }) } -#[handler] -pub async fn handler( - database: &Database, - user_id: Uuid, - request: Request, -) -> Result { - let playlist_id = request.id; - playlist::permission::check(database, playlist_id, user_id, false, false).await?; - handler_unchecked(database, user_id, playlist_id).await -} - #[cfg(test)] mod tests { use fake::{Fake, Faker}; + use futures_lite::{stream, StreamExt as _}; use rstest::rstest; + use super::*; use crate::route::playlists::create_playlist; use crate::test::{mock, Mock}; #[rstest] #[tokio::test] - async fn test_handler( + async fn test_handler_music_folder( #[future(awt)] #[with(1, 0)] mock: Mock, @@ -84,4 +77,45 @@ mod tests { let index = if allow { 0 } else { n_song_permission }; assert_eq!(database_song_ids, song_ids[index..]); } + + #[rstest] + #[tokio::test] + async fn test_handler_user( + #[future(awt)] + #[with(2, 1)] + mock: Mock, + ) { + let mut music_folder = mock.music_folder(0).await; + music_folder.add_audio().n_song((2..4).fake()).call().await; + let song_ids: Vec<_> = music_folder.database.keys().copied().collect(); + + let (user_ids, playlist_ids): (Vec<_>, Vec<_>) = stream::iter(0..2) + .then(async |i| { + let user_id = mock.user_id(i).await; + ( + user_id, + create_playlist::handler( + mock.database(), + user_id, + create_playlist::Request { + create_or_update: Faker.fake::().into(), + song_ids: Some(song_ids.clone()), + }, + ) + .await + .unwrap() + .playlist + .playlist + .id, + ) + }) + .collect() + .await; + + for i in 0..2 { + let user_id = user_ids[i]; + let playlist_id = playlist_ids[1 - i]; + assert!(handler(mock.database(), user_id, Request { id: playlist_id }).await.is_err()); + } + } } From c45cd03445424c5b9f0fd9bd41c2e6aeedeb0623 Mon Sep 17 00:00:00 2001 From: Vo Van Nghia Date: Thu, 28 Nov 2024 14:39:50 +0100 Subject: [PATCH 15/20] backend/playlists: add api/handler for get_playlists --- nghe-api/src/playlists/get_playlists.rs | 17 ++++ nghe-api/src/playlists/mod.rs | 1 + nghe-backend/src/orm/playlist/short.rs | 4 +- .../src/route/playlists/get_playlists.rs | 92 +++++++++++++++++++ nghe-backend/src/route/playlists/mod.rs | 3 +- 5 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 nghe-api/src/playlists/get_playlists.rs create mode 100644 nghe-backend/src/route/playlists/get_playlists.rs diff --git a/nghe-api/src/playlists/get_playlists.rs b/nghe-api/src/playlists/get_playlists.rs new file mode 100644 index 00000000..c6ce3be8 --- /dev/null +++ b/nghe-api/src/playlists/get_playlists.rs @@ -0,0 +1,17 @@ +use nghe_proc_macro::api_derive; + +use super::playlist; + +#[api_derive] +#[endpoint(path = "getPlaylists")] +pub struct Request {} + +#[api_derive] +pub struct Playlists { + pub playlist: Vec, +} + +#[api_derive] +pub struct Response { + pub playlists: Playlists, +} diff --git a/nghe-api/src/playlists/mod.rs b/nghe-api/src/playlists/mod.rs index f03d0077..c860d47d 100644 --- a/nghe-api/src/playlists/mod.rs +++ b/nghe-api/src/playlists/mod.rs @@ -1,3 +1,4 @@ pub mod create_playlist; pub mod get_playlist; +pub mod get_playlists; pub mod playlist; diff --git a/nghe-backend/src/orm/playlist/short.rs b/nghe-backend/src/orm/playlist/short.rs index 36798dbe..1cceda07 100644 --- a/nghe-backend/src/orm/playlist/short.rs +++ b/nghe-backend/src/orm/playlist/short.rs @@ -39,13 +39,13 @@ pub mod query { use uuid::Uuid; use super::*; - use crate::orm::playlist; + use crate::orm::{playlist, playlists}; #[auto_type] pub fn with_user_id(user_id: Uuid) -> _ { let with_user_id: playlist::query::with_user_id = playlist::query::with_user_id(user_id); let full: AsSelect = Short::as_select(); - with_user_id.select(full) + with_user_id.order_by(playlists::created_at.desc()).select(full) } } diff --git a/nghe-backend/src/route/playlists/get_playlists.rs b/nghe-backend/src/route/playlists/get_playlists.rs new file mode 100644 index 00000000..0a4bda67 --- /dev/null +++ b/nghe-backend/src/route/playlists/get_playlists.rs @@ -0,0 +1,92 @@ +use diesel_async::RunQueryDsl; +use nghe_api::playlists::get_playlists::Playlists; +pub use nghe_api::playlists::get_playlists::{Request, Response}; +use nghe_proc_macro::handler; +use uuid::Uuid; + +use crate::database::Database; +use crate::orm::playlist; +use crate::Error; + +#[handler] +pub async fn handler( + database: &Database, + user_id: Uuid, + request: Request, +) -> Result { + Ok(Response { + playlists: Playlists { + playlist: playlist::short::query::with_user_id(user_id) + .get_results(&mut database.get().await?) + .await? + .into_iter() + .map(playlist::short::Short::try_into) + .try_collect()?, + }, + }) +} + +#[cfg(test)] +mod tests { + use fake::{Fake, Faker}; + use futures_lite::{stream, StreamExt as _}; + use rstest::rstest; + + use super::*; + use crate::route::playlists::create_playlist; + use crate::test::{mock, Mock}; + + #[rstest] + #[tokio::test] + async fn test_handler( + #[future(awt)] + #[with(2, 1)] + mock: Mock, + ) { + let mut music_folder = mock.music_folder(0).await; + music_folder.add_audio().n_song((2..4).fake()).call().await; + let song_ids: Vec<_> = music_folder.database.keys().copied().collect(); + + let (user_ids, playlist_ids): (Vec<_>, Vec>) = stream::iter(0..2) + .then(async |i| { + let user_id = mock.user_id(i).await; + ( + user_id, + stream::iter(0..(2..4).fake()) + .then(async |_| { + create_playlist::handler( + mock.database(), + user_id, + create_playlist::Request { + create_or_update: Faker.fake::().into(), + song_ids: Some(song_ids.clone()), + }, + ) + .await + .unwrap() + .playlist + .playlist + .id + }) + .collect() + .await, + ) + }) + .collect() + .await; + + for i in 0..2 { + let user_id = user_ids[i]; + let database_playlist_ids: Vec<_> = handler(mock.database(), user_id, Request {}) + .await + .unwrap() + .playlists + .playlist + .into_iter() + .map(|playlist| playlist.id) + .rev() + .collect(); + assert_eq!(database_playlist_ids, playlist_ids[i]); + } + } +} diff --git a/nghe-backend/src/route/playlists/mod.rs b/nghe-backend/src/route/playlists/mod.rs index ae041649..a89c48af 100644 --- a/nghe-backend/src/route/playlists/mod.rs +++ b/nghe-backend/src/route/playlists/mod.rs @@ -1,6 +1,7 @@ pub mod create_playlist; pub mod get_playlist; +pub mod get_playlists; nghe_proc_macro::build_router! { - modules = [create_playlist] + modules = [create_playlist, get_playlist, get_playlists] } From 90a6c62dfd205e601e5bdd0f49ba2ee7cfc5493c Mon Sep 17 00:00:00 2001 From: Vo Van Nghia Date: Thu, 28 Nov 2024 14:47:41 +0100 Subject: [PATCH 16/20] backend/playlists: add api/handler for delete playlist --- nghe-api/src/playlists/delete_playlist.rs | 11 +++++++++ nghe-api/src/playlists/mod.rs | 1 + .../src/route/playlists/delete_playlist.rs | 24 +++++++++++++++++++ nghe-backend/src/route/playlists/mod.rs | 5 ++-- 4 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 nghe-api/src/playlists/delete_playlist.rs create mode 100644 nghe-backend/src/route/playlists/delete_playlist.rs diff --git a/nghe-api/src/playlists/delete_playlist.rs b/nghe-api/src/playlists/delete_playlist.rs new file mode 100644 index 00000000..8fc89a21 --- /dev/null +++ b/nghe-api/src/playlists/delete_playlist.rs @@ -0,0 +1,11 @@ +use nghe_proc_macro::api_derive; +use uuid::Uuid; + +#[api_derive] +#[endpoint(path = "deletePlaylist")] +pub struct Request { + pub id: Uuid, +} + +#[api_derive] +pub struct Response; diff --git a/nghe-api/src/playlists/mod.rs b/nghe-api/src/playlists/mod.rs index c860d47d..914c4f26 100644 --- a/nghe-api/src/playlists/mod.rs +++ b/nghe-api/src/playlists/mod.rs @@ -1,4 +1,5 @@ pub mod create_playlist; +pub mod delete_playlist; pub mod get_playlist; pub mod get_playlists; pub mod playlist; diff --git a/nghe-backend/src/route/playlists/delete_playlist.rs b/nghe-backend/src/route/playlists/delete_playlist.rs new file mode 100644 index 00000000..cf7ff7ed --- /dev/null +++ b/nghe-backend/src/route/playlists/delete_playlist.rs @@ -0,0 +1,24 @@ +use diesel::ExpressionMethods; +use diesel_async::RunQueryDsl; +pub use nghe_api::playlists::delete_playlist::{Request, Response}; +use nghe_proc_macro::handler; +use uuid::Uuid; + +use crate::database::Database; +use crate::orm::{playlist, playlists}; +use crate::Error; + +#[handler] +pub async fn handler( + database: &Database, + user_id: Uuid, + request: Request, +) -> Result { + let playlist_id = request.id; + playlist::permission::check_write(database, playlist_id, user_id, true).await?; + diesel::delete(playlists::table) + .filter(playlists::id.eq(playlist_id)) + .execute(&mut database.get().await?) + .await?; + Ok(Response) +} diff --git a/nghe-backend/src/route/playlists/mod.rs b/nghe-backend/src/route/playlists/mod.rs index a89c48af..5394f6cd 100644 --- a/nghe-backend/src/route/playlists/mod.rs +++ b/nghe-backend/src/route/playlists/mod.rs @@ -1,7 +1,8 @@ pub mod create_playlist; +mod delete_playlist; pub mod get_playlist; -pub mod get_playlists; +mod get_playlists; nghe_proc_macro::build_router! { - modules = [create_playlist, get_playlist, get_playlists] + modules = [create_playlist, delete_playlist, get_playlist, get_playlists] } From 946a9d8d220019665dfd68190da013c454b589b2 Mon Sep 17 00:00:00 2001 From: Vo Van Nghia Date: Thu, 28 Nov 2024 16:42:25 +0100 Subject: [PATCH 17/20] backend/playlists: add api/handler for update playlist --- nghe-api/src/playlists/mod.rs | 1 + nghe-api/src/playlists/update_playlist.rs | 19 +++ nghe-backend/src/orm/playlist/mod.rs | 10 +- nghe-backend/src/orm/playlists.rs | 20 ++- nghe-backend/src/route/playlists/mod.rs | 3 +- .../src/route/playlists/update_playlist.rs | 148 ++++++++++++++++++ 6 files changed, 197 insertions(+), 4 deletions(-) create mode 100644 nghe-api/src/playlists/update_playlist.rs create mode 100644 nghe-backend/src/route/playlists/update_playlist.rs diff --git a/nghe-api/src/playlists/mod.rs b/nghe-api/src/playlists/mod.rs index 914c4f26..544da198 100644 --- a/nghe-api/src/playlists/mod.rs +++ b/nghe-api/src/playlists/mod.rs @@ -3,3 +3,4 @@ pub mod delete_playlist; pub mod get_playlist; pub mod get_playlists; pub mod playlist; +pub mod update_playlist; diff --git a/nghe-api/src/playlists/update_playlist.rs b/nghe-api/src/playlists/update_playlist.rs new file mode 100644 index 00000000..8c8d0b42 --- /dev/null +++ b/nghe-api/src/playlists/update_playlist.rs @@ -0,0 +1,19 @@ +use nghe_proc_macro::api_derive; +use uuid::Uuid; + +#[api_derive] +#[endpoint(path = "updatePlaylist")] +#[cfg_attr(feature = "test", derive(Default))] +pub struct Request { + pub playlist_id: Uuid, + pub name: Option, + pub comment: Option, + pub public: Option, + #[serde(rename = "songIdToAdd")] + pub add_ids: Option>, + #[serde(rename = "songIndexToRemove")] + pub remove_indexes: Option>, +} + +#[api_derive] +pub struct Response; diff --git a/nghe-backend/src/orm/playlist/mod.rs b/nghe-backend/src/orm/playlist/mod.rs index 1e9aa412..f885c2b2 100644 --- a/nghe-backend/src/orm/playlist/mod.rs +++ b/nghe-backend/src/orm/playlist/mod.rs @@ -52,8 +52,14 @@ pub mod query { use crate::orm::{albums, permission, playlists, playlists_songs, playlists_users, songs}; #[auto_type] - pub fn with_user_id(user_id: Uuid) -> _ { + pub fn with_album(user_id: Uuid) -> _ { let permission: permission::with_album = permission::with_album(user_id); + albums::id.is_null().or(permission) + } + + #[auto_type] + pub fn with_user_id(user_id: Uuid) -> _ { + let with_album: with_album = with_album(user_id); let playlist: AsSelect = Playlist::as_select(); playlists::table .inner_join(playlists_users::table) @@ -61,7 +67,7 @@ pub mod query { .left_join(songs::table.on(songs::id.eq(playlists_songs::song_id))) .left_join(albums::table.on(albums::id.eq(songs::album_id))) .filter(playlists_users::user_id.eq(user_id)) - .filter(albums::id.is_null().or(permission)) + .filter(with_album) .group_by(playlists::id) .select(playlist) } diff --git a/nghe-backend/src/orm/playlists.rs b/nghe-backend/src/orm/playlists.rs index 7ae3d8e4..29f701be 100644 --- a/nghe-backend/src/orm/playlists.rs +++ b/nghe-backend/src/orm/playlists.rs @@ -4,19 +4,26 @@ use std::borrow::Cow; use diesel::prelude::*; use diesel_derives::AsChangeset; +use o2o::o2o; pub use crate::schema::playlists::{self, *}; -#[derive(Insertable, AsChangeset, Default)] +#[derive(Insertable, AsChangeset, Default, o2o)] #[diesel(table_name = playlists, check_for_backend(crate::orm::Type))] #[diesel(treat_none_as_null = false)] +#[from_ref(nghe_api::playlists::update_playlist::Request)] pub struct Upsert<'a> { + #[from(~.as_ref().map(|value| value.as_str().into()))] pub name: Option>, + #[from(~.as_ref().map( + |value| if value.is_empty() { None } else { Some(value.as_str().into()) } + ))] pub comment: Option>>, pub public: Option, } mod upsert { + use diesel::ExpressionMethods; use diesel_async::RunQueryDsl; use uuid::Uuid; @@ -34,4 +41,15 @@ mod upsert { .map_err(Error::from) } } + + impl crate::orm::upsert::Update for Upsert<'_> { + async fn update(&self, database: &Database, id: Uuid) -> Result<(), Error> { + diesel::update(playlists::table) + .filter(playlists::id.eq(id)) + .set(self) + .execute(&mut database.get().await?) + .await?; + Ok(()) + } + } } diff --git a/nghe-backend/src/route/playlists/mod.rs b/nghe-backend/src/route/playlists/mod.rs index 5394f6cd..5127f31a 100644 --- a/nghe-backend/src/route/playlists/mod.rs +++ b/nghe-backend/src/route/playlists/mod.rs @@ -2,7 +2,8 @@ pub mod create_playlist; mod delete_playlist; pub mod get_playlist; mod get_playlists; +mod update_playlist; nghe_proc_macro::build_router! { - modules = [create_playlist, delete_playlist, get_playlist, get_playlists] + modules = [create_playlist, delete_playlist, get_playlist, get_playlists, update_playlist] } diff --git a/nghe-backend/src/route/playlists/update_playlist.rs b/nghe-backend/src/route/playlists/update_playlist.rs new file mode 100644 index 00000000..19712f6b --- /dev/null +++ b/nghe-backend/src/route/playlists/update_playlist.rs @@ -0,0 +1,148 @@ +use diesel::{ExpressionMethods, JoinOnDsl, QueryDsl}; +use diesel_async::RunQueryDsl; +pub use nghe_api::playlists::update_playlist::{Request, Response}; +use nghe_proc_macro::handler; +use uuid::Uuid; + +use crate::database::Database; +use crate::orm::upsert::Update; +use crate::orm::{albums, playlist, playlists, playlists_songs, songs}; +use crate::Error; + +#[handler] +pub async fn handler( + database: &Database, + user_id: Uuid, + request: Request, +) -> Result { + let playlist_id = request.playlist_id; + playlist::permission::check_write(database, playlist_id, user_id, false).await?; + + if request.name.is_some() || request.comment.is_some() || request.public.is_some() { + playlists::Upsert::from(&request).update(database, playlist_id).await?; + } + + if let Some(song_indexes) = request.remove_indexes { + // TODO: Do it in one query. + let song_ids = playlists_songs::table + .left_join(songs::table) + .left_join(albums::table.on(albums::id.eq(songs::album_id))) + .filter(playlists_songs::playlist_id.eq(playlist_id)) + .filter(playlist::query::with_album(user_id)) + .select(playlists_songs::song_id) + .order_by(playlists_songs::created_at) + .get_results::(&mut database.get().await?) + .await?; + + let song_ids: Vec<_> = song_indexes + .into_iter() + .filter_map(|index| song_ids.get::((index - 1).into())) + .collect(); + + diesel::delete(playlists_songs::table) + .filter(playlists_songs::playlist_id.eq(playlist_id)) + .filter(playlists_songs::song_id.eq_any(song_ids)) + .execute(&mut database.get().await?) + .await?; + } + + if let Some(song_ids) = request.add_ids { + playlists_songs::Upsert::upserts(database, playlist_id, &song_ids).await?; + } + + Ok(Response) +} + +#[cfg(test)] +mod tests { + use fake::{Fake, Faker}; + use rstest::rstest; + + use super::*; + use crate::route::playlists::{create_playlist, get_playlist}; + use crate::test::{mock, Mock}; + + #[rstest] + #[case(0, false, &[], &[])] + #[case(0, true, &[], &[])] + #[case(5, false, &[], &[6, 7, 8, 9, 10])] + #[case(5, true, &[], &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10])] + #[case(5, false, &[1, 2, 6, 10], &[8, 9, 10])] + #[case(5, true, &[1, 2, 6, 10], &[3, 4, 5, 7, 8, 9])] + #[case(5, false, &[6, 2, 1, 10], &[8, 9, 10])] + #[case(5, true, &[6, 2, 1, 10], &[3, 4, 5, 7, 8, 9])] + #[case(5, false, &[1, 2, 6, 10, 20], &[8, 9, 10])] + #[case(5, true, &[1, 2, 6, 10, 20], &[3, 4, 5, 7, 8, 9])] + #[tokio::test] + async fn test_delete( + #[future(awt)] + #[with(1, 0)] + mock: Mock, + #[case] n_song: usize, + #[case] allow: bool, + #[case] remove_indexes: &[u16], + #[case] retain_indexes: &[u16], + ) { + 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(n_song).call().await; + music_folder.add_audio().n_song(n_song).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; + let playlist_id = create_playlist::handler( + mock.database(), + user_id, + create_playlist::Request { + create_or_update: Faker.fake::().into(), + song_ids: Some(song_ids.clone()), + }, + ) + .await + .unwrap() + .playlist + .playlist + .id; + + handler( + mock.database(), + user_id, + Request { + playlist_id, + remove_indexes: Some(remove_indexes.into()), + ..Default::default() + }, + ) + .await + .unwrap(); + + let song_ids: Vec<_> = retain_indexes + .iter() + .filter_map(|index| song_ids.get::((index - 1).into())) + .copied() + .collect(); + let database_song_ids: Vec<_> = get_playlist::handler( + mock.database(), + user_id, + get_playlist::Request { id: playlist_id }, + ) + .await + .unwrap() + .playlist + .entry + .into_iter() + .map(|entry| entry.id) + .collect(); + assert_eq!(database_song_ids, song_ids); + } +} From 3e570d5cc6859c189636bcee294f7e724748d71a Mon Sep 17 00:00:00 2001 From: Vo Van Nghia Date: Thu, 28 Nov 2024 16:54:59 +0100 Subject: [PATCH 18/20] fix song index start from 0 --- .../src/route/playlists/update_playlist.rs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/nghe-backend/src/route/playlists/update_playlist.rs b/nghe-backend/src/route/playlists/update_playlist.rs index 19712f6b..75c76a18 100644 --- a/nghe-backend/src/route/playlists/update_playlist.rs +++ b/nghe-backend/src/route/playlists/update_playlist.rs @@ -36,7 +36,7 @@ pub async fn handler( let song_ids: Vec<_> = song_indexes .into_iter() - .filter_map(|index| song_ids.get::((index - 1).into())) + .filter_map(|index| song_ids.get::(index.into())) .collect(); diesel::delete(playlists_songs::table) @@ -65,14 +65,14 @@ mod tests { #[rstest] #[case(0, false, &[], &[])] #[case(0, true, &[], &[])] - #[case(5, false, &[], &[6, 7, 8, 9, 10])] - #[case(5, true, &[], &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10])] - #[case(5, false, &[1, 2, 6, 10], &[8, 9, 10])] - #[case(5, true, &[1, 2, 6, 10], &[3, 4, 5, 7, 8, 9])] - #[case(5, false, &[6, 2, 1, 10], &[8, 9, 10])] - #[case(5, true, &[6, 2, 1, 10], &[3, 4, 5, 7, 8, 9])] - #[case(5, false, &[1, 2, 6, 10, 20], &[8, 9, 10])] - #[case(5, true, &[1, 2, 6, 10, 20], &[3, 4, 5, 7, 8, 9])] + #[case(5, false, &[], &[5, 6, 7, 8, 9])] + #[case(5, true, &[], &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9])] + #[case(5, false, &[0, 1, 5, 9], &[7, 8, 9])] + #[case(5, true, &[0, 1, 5, 9], &[2, 3, 4, 6, 7, 8])] + #[case(5, false, &[5, 1, 0, 9], &[7, 8, 9])] + #[case(5, true, &[5, 1, 0, 9], &[2, 3, 4, 6, 7, 8])] + #[case(5, false, &[0, 1, 5, 9, 19], &[7, 8, 9])] + #[case(5, true, &[0, 1, 5, 9, 19], &[2, 3, 4, 6, 7, 8])] #[tokio::test] async fn test_delete( #[future(awt)] @@ -128,7 +128,7 @@ mod tests { let song_ids: Vec<_> = retain_indexes .iter() - .filter_map(|index| song_ids.get::((index - 1).into())) + .filter_map(|index| song_ids.get::((*index).into())) .copied() .collect(); let database_song_ids: Vec<_> = get_playlist::handler( From c47eee31758875fe7eac904e5e9448f31697365b Mon Sep 17 00:00:00 2001 From: Vo Van Nghia Date: Thu, 28 Nov 2024 17:08:59 +0100 Subject: [PATCH 19/20] api/id3/orm: add short song --- nghe-api/src/id3/song/mod.rs | 2 + nghe-api/src/id3/song/short.rs | 12 +++++ nghe-api/src/playlists/playlist.rs | 2 +- nghe-api/src/search/search3.rs | 2 +- nghe-backend/src/orm/id3/duration.rs | 6 +++ nghe-backend/src/orm/id3/song/mod.rs | 15 +----- nghe-backend/src/orm/id3/song/short.rs | 50 +++++++++++++++++++ nghe-backend/src/orm/playlist/full.rs | 4 +- .../src/route/playlists/get_playlist.rs | 2 +- .../src/route/playlists/update_playlist.rs | 2 +- nghe-backend/src/route/search/search3.rs | 4 +- 11 files changed, 80 insertions(+), 21 deletions(-) create mode 100644 nghe-api/src/id3/song/short.rs create mode 100644 nghe-backend/src/orm/id3/song/short.rs diff --git a/nghe-api/src/id3/song/mod.rs b/nghe-api/src/id3/song/mod.rs index 15ba6b18..44a936b7 100644 --- a/nghe-api/src/id3/song/mod.rs +++ b/nghe-api/src/id3/song/mod.rs @@ -1,10 +1,12 @@ mod full; +mod short; use std::borrow::Cow; use bon::Builder; pub use full::Full; use nghe_proc_macro::api_derive; +pub use short::Short; use time::OffsetDateTime; use uuid::Uuid; diff --git a/nghe-api/src/id3/song/short.rs b/nghe-api/src/id3/song/short.rs new file mode 100644 index 00000000..29e2853e --- /dev/null +++ b/nghe-api/src/id3/song/short.rs @@ -0,0 +1,12 @@ +use nghe_proc_macro::api_derive; +use uuid::Uuid; + +use super::Song; + +#[api_derive] +pub struct Short { + #[serde(flatten)] + pub song: Song, + pub album: String, + pub album_id: Uuid, +} diff --git a/nghe-api/src/playlists/playlist.rs b/nghe-api/src/playlists/playlist.rs index a1c30e5c..4abe2e95 100644 --- a/nghe-api/src/playlists/playlist.rs +++ b/nghe-api/src/playlists/playlist.rs @@ -24,7 +24,7 @@ pub struct Playlist { pub struct Full { #[serde(flatten)] pub playlist: Playlist, - pub entry: Vec, + pub entry: Vec, } pub mod builder { diff --git a/nghe-api/src/search/search3.rs b/nghe-api/src/search/search3.rs index f680e0ea..e5b38e23 100644 --- a/nghe-api/src/search/search3.rs +++ b/nghe-api/src/search/search3.rs @@ -21,7 +21,7 @@ pub struct Request { pub struct SearchResult3 { pub artist: Vec, pub album: Vec, - pub song: Vec, + pub song: Vec, } #[api_derive] diff --git a/nghe-backend/src/orm/id3/duration.rs b/nghe-backend/src/orm/id3/duration.rs index c0fe8db3..1c582007 100644 --- a/nghe-backend/src/orm/id3/duration.rs +++ b/nghe-backend/src/orm/id3/duration.rs @@ -44,3 +44,9 @@ impl Trait for Vec { self.iter().map(|song| song.property.duration).sum::().duration() } } + +impl Trait for Vec { + fn duration(&self) -> Result { + self.iter().map(|song| song.song.property.duration).sum::().duration() + } +} diff --git a/nghe-backend/src/orm/id3/song/mod.rs b/nghe-backend/src/orm/id3/song/mod.rs index f8cf5b1c..9415e097 100644 --- a/nghe-backend/src/orm/id3/song/mod.rs +++ b/nghe-backend/src/orm/id3/song/mod.rs @@ -1,5 +1,6 @@ pub mod durations; pub mod full; +pub mod short; use diesel::dsl::sql; use diesel::expression::SqlLiteral; @@ -112,7 +113,7 @@ pub mod query { use diesel::dsl::{auto_type, AsSelect}; use super::*; - use crate::orm::{albums, permission, songs_artists}; + use crate::orm::{albums, songs_artists}; #[auto_type] pub fn unchecked_no_group_by() -> _ { @@ -132,18 +133,6 @@ pub mod query { let song: AsSelect = Song::as_select(); unchecked_no_group_by().group_by(songs::id).select(song) } - - #[auto_type] - pub fn with_user_id(user_id: Uuid) -> _ { - let permission: permission::with_album = permission::with_album(user_id); - unchecked().filter(permission) - } - - #[auto_type] - pub fn with_music_folder<'ids>(user_id: Uuid, music_folder_ids: &'ids [Uuid]) -> _ { - let with_user_id: with_user_id = with_user_id(user_id); - with_user_id.filter(albums::music_folder_id.eq_any(music_folder_ids)) - } } #[cfg(test)] diff --git a/nghe-backend/src/orm/id3/song/short.rs b/nghe-backend/src/orm/id3/song/short.rs new file mode 100644 index 00000000..33098129 --- /dev/null +++ b/nghe-backend/src/orm/id3/song/short.rs @@ -0,0 +1,50 @@ +use diesel::dsl::sql; +use diesel::expression::SqlLiteral; +use diesel::prelude::*; +use diesel::sql_types; +use nghe_api::id3; +use o2o::o2o; +use uuid::Uuid; + +use super::Song; +use crate::Error; + +#[derive(Debug, Queryable, Selectable, o2o)] +#[owned_try_into(id3::song::Short, Error)] +pub struct Short { + #[into(~.try_into()?)] + #[diesel(embed)] + pub song: Song, + #[diesel(select_expression = sql("any_value(albums.name) album_name"))] + #[diesel(select_expression_type = SqlLiteral)] + pub album: String, + #[diesel(select_expression = sql("any_value(albums.id) album_id"))] + #[diesel(select_expression_type = SqlLiteral)] + pub album_id: Uuid, +} + +pub mod query { + use diesel::dsl::{auto_type, AsSelect}; + + use super::*; + use crate::orm::id3::song; + use crate::orm::{albums, permission, songs}; + + #[auto_type] + pub fn unchecked() -> _ { + let full: AsSelect = Short::as_select(); + song::query::unchecked_no_group_by().group_by(songs::id).select(full) + } + + #[auto_type] + pub fn with_user_id(user_id: Uuid) -> _ { + let permission: permission::with_album = permission::with_album(user_id); + unchecked().filter(permission) + } + + #[auto_type] + pub fn with_music_folder<'ids>(user_id: Uuid, music_folder_ids: &'ids [Uuid]) -> _ { + let with_user_id: with_user_id = with_user_id(user_id); + with_user_id.filter(albums::music_folder_id.eq_any(music_folder_ids)) + } +} diff --git a/nghe-backend/src/orm/playlist/full.rs b/nghe-backend/src/orm/playlist/full.rs index c0053db6..09068054 100644 --- a/nghe-backend/src/orm/playlist/full.rs +++ b/nghe-backend/src/orm/playlist/full.rs @@ -26,7 +26,7 @@ pub struct Full { impl Full { pub async fn try_into(self, database: &Database) -> Result { - let entry = song::query::unchecked() + let entry = song::short::query::unchecked() .inner_join(playlists_songs::table) .filter(songs::id.eq_any(self.entries)) .filter(playlists_songs::playlist_id.eq(self.playlist.id)) @@ -34,7 +34,7 @@ impl Full { .get_results(&mut database.get().await?) .await?; let duration = entry.duration()?; - let entry: Vec<_> = entry.into_iter().map(song::Song::try_into).try_collect()?; + let entry: Vec<_> = entry.into_iter().map(song::short::Short::try_into).try_collect()?; let playlist = self .playlist diff --git a/nghe-backend/src/route/playlists/get_playlist.rs b/nghe-backend/src/route/playlists/get_playlist.rs index 1d353230..8cfad046 100644 --- a/nghe-backend/src/route/playlists/get_playlist.rs +++ b/nghe-backend/src/route/playlists/get_playlist.rs @@ -73,7 +73,7 @@ mod tests { .unwrap() .playlist; - let database_song_ids: Vec<_> = playlist.entry.iter().map(|entry| entry.id).collect(); + let database_song_ids: Vec<_> = playlist.entry.iter().map(|entry| entry.song.id).collect(); let index = if allow { 0 } else { n_song_permission }; assert_eq!(database_song_ids, song_ids[index..]); } diff --git a/nghe-backend/src/route/playlists/update_playlist.rs b/nghe-backend/src/route/playlists/update_playlist.rs index 75c76a18..571091ae 100644 --- a/nghe-backend/src/route/playlists/update_playlist.rs +++ b/nghe-backend/src/route/playlists/update_playlist.rs @@ -141,7 +141,7 @@ mod tests { .playlist .entry .into_iter() - .map(|entry| entry.id) + .map(|entry| entry.song.id) .collect(); assert_eq!(database_song_ids, song_ids); } diff --git a/nghe-backend/src/route/search/search3.rs b/nghe-backend/src/route/search/search3.rs index 7438d805..427ed49d 100644 --- a/nghe-backend/src/route/search/search3.rs +++ b/nghe-backend/src/route/search/search3.rs @@ -93,7 +93,7 @@ pub async fn handler( let count = request.song_count.unwrap_or(20).into(); let song = if count > 0 { let offset = request.song_offset.unwrap_or(0).into(); - let query = id3::song::query::with_user_id(user_id).limit(count).offset(offset); + let query = id3::song::short::query::with_user_id(user_id).limit(count).offset(offset); if sync { query .order_by((songs::title, songs::mbz_id)) @@ -126,7 +126,7 @@ pub async fn handler( search_result3: SearchResult3 { artist: artist.into_iter().map(id3::artist::Artist::try_into).try_collect()?, album: album.into_iter().map(id3::album::short::Short::try_into).try_collect()?, - song: song.into_iter().map(id3::song::Song::try_into).try_collect()?, + song: song.into_iter().map(id3::song::short::Short::try_into).try_collect()?, }, }) } From e46aca6e2d822c14b6d802997b4a172b0c0b046d Mon Sep 17 00:00:00 2001 From: Vo Van Nghia Date: Thu, 28 Nov 2024 17:13:58 +0100 Subject: [PATCH 20/20] api/id3/orm: use song short inside full --- nghe-api/src/id3/song/full.rs | 7 ++----- nghe-backend/src/orm/id3/song/full.rs | 19 +++++-------------- nghe-backend/src/route/browsing/get_song.rs | 6 +++--- 3 files changed, 10 insertions(+), 22 deletions(-) diff --git a/nghe-api/src/id3/song/full.rs b/nghe-api/src/id3/song/full.rs index 05a42e6a..2711e7af 100644 --- a/nghe-api/src/id3/song/full.rs +++ b/nghe-api/src/id3/song/full.rs @@ -1,14 +1,11 @@ use nghe_proc_macro::api_derive; -use uuid::Uuid; -use super::Song; +use super::Short; use crate::id3::genre; #[api_derive] pub struct Full { #[serde(flatten)] - pub song: Song, - pub album: String, - pub album_id: Uuid, + pub short: Short, pub genres: genre::Genres, } diff --git a/nghe-backend/src/orm/id3/song/full.rs b/nghe-backend/src/orm/id3/song/full.rs index 53956c4b..bdadfdd9 100644 --- a/nghe-backend/src/orm/id3/song/full.rs +++ b/nghe-backend/src/orm/id3/song/full.rs @@ -1,12 +1,9 @@ -use diesel::dsl::sql; -use diesel::expression::SqlLiteral; use diesel::prelude::*; -use diesel::sql_types; use nghe_api::id3; use o2o::o2o; use uuid::Uuid; -use super::Song; +use super::short::Short; use crate::orm::id3::genre; use crate::Error; @@ -15,13 +12,7 @@ use crate::Error; pub struct Full { #[into(~.try_into()?)] #[diesel(embed)] - pub song: Song, - #[diesel(select_expression = sql("any_value(albums.name) album_name"))] - #[diesel(select_expression_type = SqlLiteral)] - pub album: String, - #[diesel(select_expression = sql("any_value(albums.id) album_id"))] - #[diesel(select_expression_type = SqlLiteral)] - pub album_id: Uuid, + pub short: Short, #[into(~.into())] #[diesel(embed)] pub genres: genre::Genres, @@ -103,9 +94,9 @@ mod test { if allow { let database_song = database_song.unwrap(); - let database_artists: Vec = database_song.song.artists.into(); - assert_eq!(database_song.album, album.name); - assert_eq!(database_song.album_id, album_id); + let database_artists: Vec = database_song.short.song.artists.into(); + assert_eq!(database_song.short.album, album.name); + assert_eq!(database_song.short.album_id, album_id); assert_eq!(database_artists, artists); assert_eq!(database_song.genres.value.len(), n_genre); } else { diff --git a/nghe-backend/src/route/browsing/get_song.rs b/nghe-backend/src/route/browsing/get_song.rs index 09529090..859d980d 100644 --- a/nghe-backend/src/route/browsing/get_song.rs +++ b/nghe-backend/src/route/browsing/get_song.rs @@ -60,11 +60,11 @@ mod test { .unwrap() .song; - assert_eq!(database_song.album, album.name); - assert_eq!(database_song.album_id, album_id); + assert_eq!(database_song.short.album, album.name); + assert_eq!(database_song.short.album_id, album_id); let database_artists: Vec<_> = - database_song.song.artists.into_iter().map(|artist| artist.name).collect(); + database_song.short.song.artists.into_iter().map(|artist| artist.name).collect(); assert_eq!(database_artists, artists); let genres = database_song.genres.value;