From 10803c449524bc430a23d87ec910438cab1bc38a Mon Sep 17 00:00:00 2001 From: Discreater Date: Tue, 5 Dec 2023 17:30:00 +0800 Subject: [PATCH] feat: cache ncm search result --- protos/musync.proto | 5 +- src-tauri/Cargo.lock | 2 + src-tauri/Cargo.toml | 2 + src-tauri/abi/src/pb/musync.rs | 7 +- src-tauri/dbm/Cargo.toml | 3 +- src-tauri/dbm/src/lib.rs | 4 + src-tauri/dbm/src/manager.rs | 621 +----------------- src-tauri/dbm/src/player.rs | 158 +++++ src-tauri/dbm/src/playlist.rs | 156 +++++ src-tauri/dbm/src/track.rs | 413 ++++++++++++ src-tauri/dbm/src/user.rs | 89 +++ src-tauri/entity/src/lib.rs | 3 +- src-tauri/entity/src/local_src.rs | 2 +- src-tauri/entity/src/local_src_folder.rs | 2 +- src-tauri/entity/src/netease_src.rs | 34 + src-tauri/entity/src/play_queue.rs | 2 +- src-tauri/entity/src/play_queue_track.rs | 2 +- src-tauri/entity/src/playlist.rs | 2 +- src-tauri/entity/src/playlist_track.rs | 2 +- src-tauri/entity/src/prelude.rs | 3 +- src-tauri/entity/src/track.rs | 10 +- src-tauri/entity/src/user.rs | 2 +- src-tauri/entity/src/user_playlist.rs | 2 +- src-tauri/migration/src/lib.rs | 8 +- .../src/m20230813_000001_create_table.rs | 2 +- .../src/m20231204_162103_add_ncm_src.rs | 71 ++ src-tauri/server/Cargo.toml | 3 +- src-tauri/server/src/grpc.rs | 28 +- src/generated/protos/musync.ts | 45 +- src/pages/main/SearchResult.vue | 47 +- 30 files changed, 1055 insertions(+), 675 deletions(-) create mode 100644 src-tauri/dbm/src/player.rs create mode 100644 src-tauri/dbm/src/playlist.rs create mode 100644 src-tauri/dbm/src/track.rs create mode 100644 src-tauri/dbm/src/user.rs create mode 100644 src-tauri/entity/src/netease_src.rs create mode 100644 src-tauri/migration/src/m20231204_162103_add_ncm_src.rs diff --git a/protos/musync.proto b/protos/musync.proto index e5f0887..7251a0e 100644 --- a/protos/musync.proto +++ b/protos/musync.proto @@ -67,6 +67,8 @@ message Track { message NeteaseSource { // id of the track in netease string id = 1; + // popularity of the track + optional float pop = 2; } // LocalSource @@ -315,9 +317,10 @@ message SearchAllRequest { } message SearchAllResponse { + reserved 2; repeated Track db_tracks = 1; // netease music search result - string ncm_res = 2; + repeated Track ncm_tracks = 3; } message RebuildIndexRequest {} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7aa6774..1b4919f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1320,6 +1320,7 @@ version = "0.1.0" dependencies = [ "abi", "anyhow", + "async-stream", "async-trait", "chrono", "debounced", @@ -1327,6 +1328,7 @@ dependencies = [ "futures-channel", "futures-util", "migration", + "ncmapi", "sea-orm", "thiserror", "tokio", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 43e171e..1ab8529 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -32,6 +32,8 @@ futures = "0.3.28" futures-channel = "0.3.25" futures-util = "0.3.25" dotenvy = "0.15.7" +# ncmapi = { path="../../ncmapi-rs" } +ncmapi = { git="https://github.com/Discreater/ncmapi-rs" } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src-tauri/abi/src/pb/musync.rs b/src-tauri/abi/src/pb/musync.rs index d9c3240..4cbb0e5 100644 --- a/src-tauri/abi/src/pb/musync.rs +++ b/src-tauri/abi/src/pb/musync.rs @@ -96,6 +96,9 @@ pub struct NeteaseSource { /// id of the track in netease #[prost(string, tag = "1")] pub id: ::prost::alloc::string::String, + /// popularity of the track + #[prost(float, optional, tag = "2")] + pub pop: ::core::option::Option, } /// LocalSource #[derive(serde::Serialize, serde::Deserialize)] @@ -526,8 +529,8 @@ pub struct SearchAllResponse { #[prost(message, repeated, tag = "1")] pub db_tracks: ::prost::alloc::vec::Vec, /// netease music search result - #[prost(string, tag = "2")] - pub ncm_res: ::prost::alloc::string::String, + #[prost(message, repeated, tag = "3")] + pub ncm_tracks: ::prost::alloc::vec::Vec, } #[derive(serde::Serialize, serde::Deserialize)] #[allow(clippy::derive_partial_eq_without_eq)] diff --git a/src-tauri/dbm/Cargo.toml b/src-tauri/dbm/Cargo.toml index 87fa777..f266d81 100644 --- a/src-tauri/dbm/Cargo.toml +++ b/src-tauri/dbm/Cargo.toml @@ -23,11 +23,12 @@ chrono = { workspace = true } tonic = { workspace = true } tokio = { workspace = true, features = ["full"] } tokio-stream = { workspace = true } +async-stream = { version = "0.3" } utils = { path = "../utils" } debounced = { git = "https://github.com/Discreater/debounced" } futures-channel = { workspace = true, features = ["sink"] } futures-util = { workspace = true, features = ["sink"] } - +ncmapi = { workspace = true } [dev-dependencies] tracing-subscriber = { workspace = true } diff --git a/src-tauri/dbm/src/lib.rs b/src-tauri/dbm/src/lib.rs index 592e5a4..5e5fee2 100644 --- a/src-tauri/dbm/src/lib.rs +++ b/src-tauri/dbm/src/lib.rs @@ -1,5 +1,9 @@ pub mod error; mod manager; +mod playlist; +mod track; +mod player; +mod user; pub use error::MusyncError; use sea_orm::DatabaseConnection; diff --git a/src-tauri/dbm/src/manager.rs b/src-tauri/dbm/src/manager.rs index eb54775..03a7974 100644 --- a/src-tauri/dbm/src/manager.rs +++ b/src-tauri/dbm/src/manager.rs @@ -1,26 +1,15 @@ use std::path::PathBuf; -use abi::{ - CreatePlayQueueRequest, CreatePlaylistRequest, LocalFolder, LoginRequest, - QueryLocalFoldersRequest, -}; -use chrono::Utc; use entity::prelude::*; use migration::{Migrator, MigratorTrait}; -use sea_orm::{ - ActiveModelTrait, ActiveValue::NotSet, ColumnTrait, Condition, Database, DatabaseConnection, - EntityTrait, QueryFilter, QueryOrder, QueryTrait, Set, TransactionTrait, -}; -use tracing::{info, trace, warn}; +use sea_orm::{ColumnTrait, Database, DatabaseConnection, EntityTrait, QueryFilter}; -use crate::{DbManager, MusyncError, PlaylistId, TrackId, UserId}; +use crate::{DbManager, MusyncError, PlaylistId, TrackId}; impl DbManager { pub fn new(db: DatabaseConnection, _data_folder: PathBuf) -> Self { - Self { - db, - } + Self { db } } pub async fn from_url(db_url: &str, data_folder: PathBuf) -> crate::error::Result { @@ -29,610 +18,6 @@ impl DbManager { Ok(Self::new(db, data_folder)) } } -/// Playlist -impl DbManager { - pub async fn create_playlist( - &self, - owner_id: UserId, - create: CreatePlaylistRequest, - ) -> Result { - let now = Utc::now(); - // create playlist - let inserted = entity::playlist::ActiveModel { - id: NotSet, - owner_id: Set(owner_id), - name: Set(create.name.clone()), - description: Set(create.description.clone()), - created_at: Set(now), - updated_at: Set(now), - } - .insert(&self.db) - .await?; - - trace!("playlist inserted, id: {}", inserted.id); - - let mut playlist = abi::Playlist::from_entity(inserted, vec![]); - - if create.track_ids.is_empty() { - return Ok(playlist); - } - - let track_list = create - .track_ids - .iter() - .map(|track_id| entity::playlist_track::ActiveModel { - playlist_id: Set(playlist.id), - track_id: Set(*track_id), - }); - PlaylistTrack::insert_many(track_list) - .exec(&self.db) - .await?; - - playlist.track_ids.extend(create.track_ids); - - Ok(playlist) - } - - pub async fn update_playlist( - &self, - playlist: abi::UpdatePlaylistRequest, - ) -> Result { - let now = Utc::now(); - - let old = Playlist::find_by_id(playlist.id) - .one(&self.db) - .await? - .ok_or(MusyncError::PlaylistNotFound(playlist.id))?; - - let mut updating: entity::playlist::ActiveModel = old.into(); - if let Some(name) = playlist.name { - updating.name = Set(name); - } - if let Some(description) = playlist.description { - updating.description = Set(description); - } - updating.updated_at = Set(now); - updating.update(&self.db).await?; - - if !playlist.added_track_ids.is_empty() { - let added_tracks = - playlist - .added_track_ids - .iter() - .map(|track_id| entity::playlist_track::ActiveModel { - playlist_id: Set(playlist.id), - track_id: Set(*track_id), - }); - PlaylistTrack::insert_many(added_tracks) - .exec(&self.db) - .await?; - } - - if !playlist.removed_track_ids.is_empty() { - PlaylistTrack::delete_many() - .filter( - Condition::all() - .add(entity::playlist_track::Column::PlaylistId.eq(playlist.id)) - .add(entity::playlist_track::Column::TrackId.is_in(playlist.removed_track_ids)), - ) - .exec(&self.db) - .await?; - } - - self.playlist(playlist.id).await - } - - pub async fn delete_playlists(&self, ids: &[PlaylistId]) -> Result { - if ids.is_empty() { - return Ok(0); - } - - let deleted = Playlist::delete_many() - .filter(entity::playlist::Column::Id.is_in(ids.to_owned())) - .exec(&self.db) - .await?; - - Ok(deleted.rows_affected) - } - - pub async fn query_playlists( - &self, - query: abi::QueryPlaylistsRequest, - ) -> Result, MusyncError> { - let playlists = Playlist::find() - .apply_if(query.name, |builder, name| { - builder.filter(entity::playlist::Column::Name.like(format!("%{}%", name))) - }) - .apply_if(query.user_id, |b, user_id| { - b.inner_join(UserPlaylist) - .filter(entity::user_playlist::Column::UserId.eq(user_id)) - }) - .apply_if(query.track_id, |b, track_id| { - b.inner_join(PlaylistTrack) - .filter(entity::playlist_track::Column::TrackId.eq(track_id)) - }) - .all(&self.db) - .await?; - - Ok( - playlists - .into_iter() - .map(|row| abi::Playlist::from_entity(row, vec![])) - .collect(), - ) - } - - pub async fn playlist(&self, id: PlaylistId) -> Result { - let queried = Playlist::find_by_id(id) - .one(&self.db) - .await? - .ok_or(MusyncError::PlaylistNotFound(id))?; - - let tracks: Vec<_> = self.get_tracks_in_playlist(id).await?; - - Ok(abi::Playlist::from_entity(queried, tracks)) - } -} -/// Track -impl DbManager { - pub async fn create_track(&self, track: abi::Track) -> Result { - let txn = self.db.begin().await?; - let now = Utc::now(); - let inserted = entity::track::ActiveModel { - id: NotSet, - title: Set(track.title.clone()), - artist: Set(track.artist.clone()), - album: Set(track.album.clone()), - duration: Set(track.duration), - genre: Set(track.genre.clone()), - year: Set(track.year), - created_at: Set(now), - updated_at: Set(now), - } - .insert(&txn) - .await?; - if let Some(src) = track.local_src { - entity::local_src::ActiveModel { - track_id: Set(inserted.id), - path: Set(src.path), - folder_id: NotSet, - } - .insert(&txn) - .await?; - } - txn.commit().await?; - Ok(abi::Track::from_entity(inserted)) - } - - pub async fn create_tracks( - &self, - tracks: Vec, - folder: &str, - ) -> Result { - let txn = self.db.begin().await?; - let now = Utc::now(); - let folder = entity::local_src_folder::ActiveModel { - id: NotSet, - path: Set(folder.to_owned()), - created_at: Set(now), - updated_at: Set(now), - } - .insert(&txn) - .await - .map_err(|_| MusyncError::FolderExists(folder.to_owned()))?; - let active_tracks = tracks.iter().map(|t| entity::track::ActiveModel { - id: NotSet, - title: Set(t.title.clone()), - artist: Set(t.artist.clone()), - album: Set(t.album.clone()), - duration: Set(t.duration), - genre: Set(t.genre.clone()), - year: Set(t.year), - created_at: Set(now), - updated_at: Set(now), - }); - let inserted = Track::insert_many(active_tracks).exec(&txn).await?; - info!( - "inserted tracks: {}", - inserted.last_insert_id - tracks.len() as i32 - ); - let start_id = inserted.last_insert_id - tracks.len() as i32 + 1; - let mut local_srcs = vec![]; - for (idx, track) in tracks.iter().enumerate() { - if let Some(src) = track.local_src.as_ref() { - local_srcs.push(entity::local_src::ActiveModel { - track_id: Set(start_id + idx as i32), - path: Set(src.path.clone()), - folder_id: Set(Some(folder.id)), - }); - } - if let Some(_src) = track.netease_src.as_ref() { - warn!("netease_src is not supported yet"); - } - } - LocalSrc::insert_many(local_srcs).exec(&txn).await?; - txn.commit().await?; - Ok(inserted.last_insert_id) - } - - pub async fn remove_folder(&self, folder: &str) -> Result { - let txn = self.db.begin().await?; - // delete cascade - LocalSrcFolder::delete_many() - .filter(entity::local_src_folder::Column::Path.eq(folder)) - .exec(&txn) - .await - .map_err(|_| (MusyncError::FolderNotFound(folder.to_owned())))?; - // delete tracks without local_src and netease_src - use sea_orm::query::*; - let deleted = Track::delete_many() - .filter( - entity::track::Column::Id.not_in_subquery( - LocalSrc::find() - .select_only() - .column(entity::local_src::Column::TrackId) - .into_query(), - ), - ) - .exec(&txn) - .await?; - txn.commit().await?; - trace!("deleted tracks: {:?}", deleted); - Ok(deleted.rows_affected) - } - - pub async fn query_local_folders( - &self, - query: QueryLocalFoldersRequest, - ) -> Result, MusyncError> { - let QueryLocalFoldersRequest {} = query; - let folders = LocalSrcFolder::find().all(&self.db).await?; - Ok(folders.into_iter().map(LocalFolder::from_entity).collect()) - } - - pub async fn update_track(&self, update: abi::TrackUpdate) -> Result { - if update.netease_src.is_some() { - warn!("netease_src is not supported yet"); - } - let now = Utc::now(); - let old = Track::find_by_id(update.id) - .one(&self.db) - .await? - .ok_or(MusyncError::TrackNotFound(update.id))?; - - let mut updating: entity::track::ActiveModel = old.into(); - if let Some(title) = update.title { - updating.title = Set(title); - } - if let Some(artist) = update.artist { - updating.artist = Set(Some(artist)); - } - if let Some(album) = update.album { - updating.album = Set(Some(album)); - } - if let Some(duration) = update.duration { - updating.duration = Set(Some(duration)); - } - if let Some(genre) = update.genre { - updating.genre = Set(Some(genre)); - } - if let Some(year) = update.year { - updating.year = Set(Some(year)); - } - updating.updated_at = Set(now); - updating.update(&self.db).await?; - - if let Some(src) = update.local_src { - let old = LocalSrc::find_by_id(update.id).one(&self.db).await?; - if let Some(old) = old { - let mut updating: entity::local_src::ActiveModel = old.into(); - updating.path = Set(src.path); - updating.update(&self.db).await?; - } else { - let inserted = entity::local_src::ActiveModel { - track_id: Set(update.id), - path: Set(src.path), - folder_id: NotSet, - }; - inserted.insert(&self.db).await?; - } - } - - self.track(update.id).await - } - - pub async fn delete_tracks(&self, ids: &[TrackId]) -> Result { - if ids.is_empty() { - return Ok(0); - } - - let deleted = Track::delete_many() - .filter(entity::track::Column::Id.is_in(ids.to_owned())) - .exec(&self.db) - .await?; - Ok(deleted.rows_affected) - } - - pub async fn query_tracks( - &self, - query: abi::QueryTracksRequest, - ) -> Result, MusyncError> { - trace!("query tracks: {:?}", query); - - let rows = if query.title.is_none() && query.artist.is_none() && query.album.is_none() { - Track::find().all(&self.db).await? - } else { - Track::find() - .filter( - Condition::any() - .add_option( - query - .title - .map(|title| entity::track::Column::Title.like(format!("%{}%", title))), - ) - .add_option( - query - .artist - .map(|artist| entity::track::Column::Artist.like(format!("%{}%", artist))), - ) - .add_option( - query - .album - .map(|album| entity::track::Column::Album.like(format!("%{}%", album))), - ), - ) - .all(&self.db) - .await? - }; - - rows - .into_iter() - .map(|row| Ok(abi::Track::from_entity(row))) - .collect() - } - - pub async fn track(&self, id: TrackId) -> Result { - let row = Track::find_by_id(id) - .one(&self.db) - .await? - .ok_or(MusyncError::TrackNotFound(id))?; - - let mut abi_track = abi::Track::from_entity(row); - let src = LocalSrc::find_by_id(id).one(&self.db).await?; - if let Some(src) = src { - abi_track.local_src = Some(abi::LocalSource { path: src.path }) - } - - Ok(abi_track) - } -} - -/// Player -impl DbManager { - pub async fn stop_all(&self) -> Result<(), MusyncError> { - trace!("stoping all"); - let updater = entity::play_queue::ActiveModel { - playing: Set(false), - paused_at: Set(0), - ..Default::default() - }; - PlayQueue::update_many().set(updater).exec(&self.db).await?; - Ok(()) - } - - pub async fn create_play_queue( - &self, - create: CreatePlayQueueRequest, - owner_id: UserId, - ) -> Result { - let CreatePlayQueueRequest { track_ids } = create; - let txn = self.db.begin().await?; - - let now = Utc::now(); - // insert play queue - let inserted = entity::play_queue::ActiveModel { - id: NotSet, - playing: Set(false), - position: Set(0), - started_at: Set(now), - paused_at: Set(0), - created_at: Set(now), - updated_at: Set(now), - } - .insert(&txn) - .await?; - - // insert tracks - let play_queue_tracks = track_ids.iter().enumerate().map(|(position, track_id)| { - entity::play_queue_track::ActiveModel { - play_queue_id: Set(inserted.id), - track_id: Set(*track_id), - position: Set(position as u32), - } - }); - PlayQueueTrack::insert_many(play_queue_tracks) - .exec(&txn) - .await?; - - // update user play queue id - let user = User::find_by_id(owner_id) - .one(&txn) - .await? - .ok_or(MusyncError::UserNotFound(owner_id))?; - let old_queue_id = user.play_queue_id; - let mut user: entity::user::ActiveModel = user.into(); - user.play_queue_id = Set(Some(inserted.id)); - user.update(&txn).await?; - - if let Some(old_queue_id) = old_queue_id { - // delete old play queue - PlayQueue::delete_by_id(old_queue_id).exec(&txn).await?; - } - - txn.commit().await?; - Ok(abi::PlayQueue::from_entity(inserted, track_ids)) - } - - pub async fn get_user_play_queue( - &self, - owner_id: UserId, - ) -> Result, MusyncError> { - let user = User::find_by_id(owner_id) - .one(&self.db) - .await? - .ok_or(MusyncError::UserNotFound(owner_id))?; - if user.play_queue_id.is_none() { - return Ok(None); - } - let play_queue_id = user.play_queue_id.unwrap(); - let play_queue = PlayQueue::find_by_id(play_queue_id) - .one(&self.db) - .await? - .ok_or(MusyncError::PlayQueueNotFound(play_queue_id))?; - let play_queue_tracks = PlayQueueTrack::find() - .filter(entity::play_queue_track::Column::PlayQueueId.eq(play_queue.id)) - .order_by_asc(entity::play_queue_track::Column::Position) - .all(&self.db) - .await?; - let track_ids = play_queue_tracks - .into_iter() - .map(|t| t.track_id) - .collect::>(); - Ok(Some(abi::PlayQueue::from_entity(play_queue, track_ids))) - } - - pub async fn update_player( - &self, - update: &abi::UpdatePlayerRequest, - owner_id: UserId, - ) -> Result, MusyncError> { - let user = User::find_by_id(owner_id) - .one(&self.db) - .await? - .ok_or(MusyncError::UserNotFound(owner_id))?; - if let Some(play_queue_id) = user.play_queue_id { - let abi::UpdatePlayerRequest { - manul: _manul, - playing, - position, - progress, - } = update; - - let play_queue = PlayQueue::find_by_id(play_queue_id) - .one(&self.db) - .await? - .ok_or(MusyncError::PlayQueueNotFound(play_queue_id))?; - let progress = progress.unwrap_or_else(|| { - if play_queue.playing { - (Utc::now() - play_queue.started_at).num_milliseconds() as u32 - } else { - play_queue.paused_at - } - }); - - let mut play_queue: entity::play_queue::ActiveModel = play_queue.into(); - if let Some(playing) = playing { - play_queue.playing = Set(*playing); - } - if let Some(position) = position { - play_queue.position = Set(*position); - } - if *play_queue.playing.as_ref() { - play_queue.started_at = Set(Utc::now() - chrono::Duration::milliseconds(progress as i64)); - } else { - play_queue.paused_at = Set(progress); - } - let updated = play_queue.update(&self.db).await?; - Ok(Some(abi::UpdatePlayerEvent { - playing: updated.playing, - position: updated.position, - progress, - })) - } else { - Ok(None) - } - } -} - -/// User -impl DbManager { - pub async fn create_user(&self, user: abi::User) -> Result { - let mut updating = user.clone(); - let now = Utc::now(); - let inserted = entity::user::ActiveModel { - name: Set(user.name), - created_at: Set(now), - updated_at: Set(now), - ..Default::default() - } - .insert(&self.db) - .await?; - - updating.id = inserted.id; - - Ok(updating) - } - - pub async fn update_user(&self, update: abi::UserUpdate) -> Result { - let now = Utc::now(); - - let old = User::find_by_id(update.id) - .one(&self.db) - .await? - .ok_or(MusyncError::UserNotFound(update.id))?; - let mut updating: entity::user::ActiveModel = old.into(); - updating.name = Set(update.name); - updating.updated_at = Set(now); - updating.update(&self.db).await?; - - self.user(update.id).await - } - - pub async fn delete_users(&self, ids: &[UserId]) -> Result { - if ids.is_empty() { - return Ok(0); - } - - let deleted = User::delete_many() - .filter(entity::user::Column::Id.is_in(ids.to_owned())) - .exec(&self.db) - .await?; - Ok(deleted.rows_affected) - } - - pub async fn query_users( - &self, - query: abi::QueryUsersRequest, - ) -> Result, MusyncError> { - if let Some(name) = query.name { - let rows = User::find() - .filter(entity::user::Column::Name.like(format!("%{}%", name))) - .all(&self.db) - .await?; - - Ok(rows.into_iter().map(abi::User::from_entity).collect()) - } else { - Ok(vec![]) - } - } - - pub async fn login(&self, req: LoginRequest) -> Result { - let user = User::find() - .filter(entity::user::Column::Name.eq(&req.name)) - .one(&self.db) - .await?; - user - .map(abi::User::from_entity) - .ok_or(MusyncError::LoginFailed(req.name)) - } - pub async fn user(&self, id: UserId) -> Result { - let row = User::find_by_id(id) - .one(&self.db) - .await? - .ok_or(MusyncError::UserNotFound(id))?; - Ok(abi::User::from_entity(row)) - } -} impl DbManager { pub async fn get_tracks_in_playlist( diff --git a/src-tauri/dbm/src/player.rs b/src-tauri/dbm/src/player.rs new file mode 100644 index 0000000..741ae11 --- /dev/null +++ b/src-tauri/dbm/src/player.rs @@ -0,0 +1,158 @@ +use abi::CreatePlayQueueRequest; +use chrono::Utc; +use entity::prelude::*; + +use sea_orm::{ + ActiveModelTrait, ActiveValue::NotSet, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set, + TransactionTrait, +}; +use tracing::trace; + +use crate::{DbManager, MusyncError, UserId}; + +/// Player +impl DbManager { + pub async fn stop_all(&self) -> Result<(), MusyncError> { + trace!("stoping all"); + let updater = entity::play_queue::ActiveModel { + playing: Set(false), + paused_at: Set(0), + ..Default::default() + }; + PlayQueue::update_many().set(updater).exec(&self.db).await?; + Ok(()) + } + + pub async fn create_play_queue( + &self, + create: CreatePlayQueueRequest, + owner_id: UserId, + ) -> Result { + let CreatePlayQueueRequest { track_ids } = create; + let txn = self.db.begin().await?; + + let now = Utc::now(); + // insert play queue + let inserted = entity::play_queue::ActiveModel { + id: NotSet, + playing: Set(false), + position: Set(0), + started_at: Set(now), + paused_at: Set(0), + created_at: Set(now), + updated_at: Set(now), + } + .insert(&txn) + .await?; + + // insert tracks + let play_queue_tracks = track_ids.iter().enumerate().map(|(position, track_id)| { + entity::play_queue_track::ActiveModel { + play_queue_id: Set(inserted.id), + track_id: Set(*track_id), + position: Set(position as u32), + } + }); + PlayQueueTrack::insert_many(play_queue_tracks) + .exec(&txn) + .await?; + + // update user play queue id + let user = User::find_by_id(owner_id) + .one(&txn) + .await? + .ok_or(MusyncError::UserNotFound(owner_id))?; + let old_queue_id = user.play_queue_id; + let mut user: entity::user::ActiveModel = user.into(); + user.play_queue_id = Set(Some(inserted.id)); + user.update(&txn).await?; + + if let Some(old_queue_id) = old_queue_id { + // delete old play queue + PlayQueue::delete_by_id(old_queue_id).exec(&txn).await?; + } + + txn.commit().await?; + Ok(abi::PlayQueue::from_entity(inserted, track_ids)) + } + + pub async fn get_user_play_queue( + &self, + owner_id: UserId, + ) -> Result, MusyncError> { + let user = User::find_by_id(owner_id) + .one(&self.db) + .await? + .ok_or(MusyncError::UserNotFound(owner_id))?; + if user.play_queue_id.is_none() { + return Ok(None); + } + let play_queue_id = user.play_queue_id.unwrap(); + let play_queue = PlayQueue::find_by_id(play_queue_id) + .one(&self.db) + .await? + .ok_or(MusyncError::PlayQueueNotFound(play_queue_id))?; + let play_queue_tracks = PlayQueueTrack::find() + .filter(entity::play_queue_track::Column::PlayQueueId.eq(play_queue.id)) + .order_by_asc(entity::play_queue_track::Column::Position) + .all(&self.db) + .await?; + let track_ids = play_queue_tracks + .into_iter() + .map(|t| t.track_id) + .collect::>(); + Ok(Some(abi::PlayQueue::from_entity(play_queue, track_ids))) + } + + pub async fn update_player( + &self, + update: &abi::UpdatePlayerRequest, + owner_id: UserId, + ) -> Result, MusyncError> { + let user = User::find_by_id(owner_id) + .one(&self.db) + .await? + .ok_or(MusyncError::UserNotFound(owner_id))?; + if let Some(play_queue_id) = user.play_queue_id { + let abi::UpdatePlayerRequest { + manul: _manul, + playing, + position, + progress, + } = update; + + let play_queue = PlayQueue::find_by_id(play_queue_id) + .one(&self.db) + .await? + .ok_or(MusyncError::PlayQueueNotFound(play_queue_id))?; + let progress = progress.unwrap_or_else(|| { + if play_queue.playing { + (Utc::now() - play_queue.started_at).num_milliseconds() as u32 + } else { + play_queue.paused_at + } + }); + + let mut play_queue: entity::play_queue::ActiveModel = play_queue.into(); + if let Some(playing) = playing { + play_queue.playing = Set(*playing); + } + if let Some(position) = position { + play_queue.position = Set(*position); + } + if *play_queue.playing.as_ref() { + play_queue.started_at = Set(Utc::now() - chrono::Duration::milliseconds(progress as i64)); + } else { + play_queue.paused_at = Set(progress); + } + let updated = play_queue.update(&self.db).await?; + Ok(Some(abi::UpdatePlayerEvent { + playing: updated.playing, + position: updated.position, + progress, + })) + } else { + Ok(None) + } + } +} diff --git a/src-tauri/dbm/src/playlist.rs b/src-tauri/dbm/src/playlist.rs new file mode 100644 index 0000000..9410f2e --- /dev/null +++ b/src-tauri/dbm/src/playlist.rs @@ -0,0 +1,156 @@ +use abi::CreatePlaylistRequest; +use chrono::Utc; +use entity::prelude::*; + +use sea_orm::{ + ActiveModelTrait, ActiveValue::NotSet, ColumnTrait, Condition, EntityTrait, QueryFilter, + QueryTrait, Set, +}; +use tracing::trace; + +use crate::{DbManager, MusyncError, PlaylistId, UserId}; + +/// Playlist +impl DbManager { + pub async fn create_playlist( + &self, + owner_id: UserId, + create: CreatePlaylistRequest, + ) -> Result { + let now = Utc::now(); + // create playlist + let inserted = entity::playlist::ActiveModel { + id: NotSet, + owner_id: Set(owner_id), + name: Set(create.name.clone()), + description: Set(create.description.clone()), + created_at: Set(now), + updated_at: Set(now), + } + .insert(&self.db) + .await?; + + trace!("playlist inserted, id: {}", inserted.id); + + let mut playlist = abi::Playlist::from_entity(inserted, vec![]); + + if create.track_ids.is_empty() { + return Ok(playlist); + } + + let track_list = create + .track_ids + .iter() + .map(|track_id| entity::playlist_track::ActiveModel { + playlist_id: Set(playlist.id), + track_id: Set(*track_id), + }); + PlaylistTrack::insert_many(track_list) + .exec(&self.db) + .await?; + + playlist.track_ids.extend(create.track_ids); + + Ok(playlist) + } + + pub async fn update_playlist( + &self, + playlist: abi::UpdatePlaylistRequest, + ) -> Result { + let now = Utc::now(); + + let old = Playlist::find_by_id(playlist.id) + .one(&self.db) + .await? + .ok_or(MusyncError::PlaylistNotFound(playlist.id))?; + + let mut updating: entity::playlist::ActiveModel = old.into(); + if let Some(name) = playlist.name { + updating.name = Set(name); + } + if let Some(description) = playlist.description { + updating.description = Set(description); + } + updating.updated_at = Set(now); + updating.update(&self.db).await?; + + if !playlist.added_track_ids.is_empty() { + let added_tracks = + playlist + .added_track_ids + .iter() + .map(|track_id| entity::playlist_track::ActiveModel { + playlist_id: Set(playlist.id), + track_id: Set(*track_id), + }); + PlaylistTrack::insert_many(added_tracks) + .exec(&self.db) + .await?; + } + + if !playlist.removed_track_ids.is_empty() { + PlaylistTrack::delete_many() + .filter( + Condition::all() + .add(entity::playlist_track::Column::PlaylistId.eq(playlist.id)) + .add(entity::playlist_track::Column::TrackId.is_in(playlist.removed_track_ids)), + ) + .exec(&self.db) + .await?; + } + + self.playlist(playlist.id).await + } + + pub async fn delete_playlists(&self, ids: &[PlaylistId]) -> Result { + if ids.is_empty() { + return Ok(0); + } + + let deleted = Playlist::delete_many() + .filter(entity::playlist::Column::Id.is_in(ids.to_owned())) + .exec(&self.db) + .await?; + + Ok(deleted.rows_affected) + } + + pub async fn query_playlists( + &self, + query: abi::QueryPlaylistsRequest, + ) -> Result, MusyncError> { + let playlists = Playlist::find() + .apply_if(query.name, |builder, name| { + builder.filter(entity::playlist::Column::Name.like(format!("%{}%", name))) + }) + .apply_if(query.user_id, |b, user_id| { + b.inner_join(UserPlaylist) + .filter(entity::user_playlist::Column::UserId.eq(user_id)) + }) + .apply_if(query.track_id, |b, track_id| { + b.inner_join(PlaylistTrack) + .filter(entity::playlist_track::Column::TrackId.eq(track_id)) + }) + .all(&self.db) + .await?; + + Ok( + playlists + .into_iter() + .map(|row| abi::Playlist::from_entity(row, vec![])) + .collect(), + ) + } + + pub async fn playlist(&self, id: PlaylistId) -> Result { + let queried = Playlist::find_by_id(id) + .one(&self.db) + .await? + .ok_or(MusyncError::PlaylistNotFound(id))?; + + let tracks: Vec<_> = self.get_tracks_in_playlist(id).await?; + + Ok(abi::Playlist::from_entity(queried, tracks)) + } +} diff --git a/src-tauri/dbm/src/track.rs b/src-tauri/dbm/src/track.rs new file mode 100644 index 0000000..f3bd850 --- /dev/null +++ b/src-tauri/dbm/src/track.rs @@ -0,0 +1,413 @@ +use std::collections::HashMap; + +use abi::{LocalFolder, QueryLocalFoldersRequest}; +use chrono::Utc; +use entity::prelude::*; + +use sea_orm::{ + ActiveModelTrait, + ActiveValue::{self, NotSet}, + ColumnTrait, Condition, EntityTrait, QueryFilter, Set, TransactionTrait, +}; +use tracing::{info, trace, warn}; + +use crate::{DbManager, MusyncError, TrackId}; + +impl DbManager { + pub async fn create_track(&self, track: abi::Track) -> Result { + let txn = self.db.begin().await?; + let now = Utc::now(); + let inserted = entity::track::ActiveModel { + id: NotSet, + title: Set(track.title.clone()), + artist: Set(track.artist.clone()), + album: Set(track.album.clone()), + duration: Set(track.duration), + genre: Set(track.genre.clone()), + year: Set(track.year), + created_at: Set(now), + updated_at: Set(now), + } + .insert(&txn) + .await?; + if let Some(src) = track.local_src { + entity::local_src::ActiveModel { + track_id: Set(inserted.id), + path: Set(src.path), + folder_id: NotSet, + } + .insert(&txn) + .await?; + } + txn.commit().await?; + Ok(abi::Track::from_entity(inserted)) + } + + pub async fn create_local_tracks( + &self, + tracks: Vec, + folder: &str, + ) -> Result { + let txn = self.db.begin().await?; + let now = Utc::now(); + let folder = entity::local_src_folder::ActiveModel { + id: NotSet, + path: Set(folder.to_owned()), + created_at: Set(now), + updated_at: Set(now), + } + .insert(&txn) + .await + .map_err(|_| MusyncError::FolderExists(folder.to_owned()))?; + let active_tracks = tracks.iter().map(|t| entity::track::ActiveModel { + id: NotSet, + title: Set(t.title.clone()), + artist: Set(t.artist.clone()), + album: Set(t.album.clone()), + duration: Set(t.duration), + genre: Set(t.genre.clone()), + year: Set(t.year), + created_at: Set(now), + updated_at: Set(now), + }); + let inserted = Track::insert_many(active_tracks).exec(&txn).await?; + info!( + "inserted tracks: {}", + inserted.last_insert_id - tracks.len() as i32 + ); + let start_id = inserted.last_insert_id - tracks.len() as i32 + 1; + let mut local_srcs = vec![]; + for (idx, track) in tracks.iter().enumerate() { + if let Some(src) = track.local_src.as_ref() { + local_srcs.push(entity::local_src::ActiveModel { + track_id: Set(start_id + idx as i32), + path: Set(src.path.clone()), + folder_id: Set(Some(folder.id)), + }); + } + if let Some(_src) = track.netease_src.as_ref() { + warn!("netease_src is not supported yet"); + } + } + LocalSrc::insert_many(local_srcs).exec(&txn).await?; + txn.commit().await?; + Ok(inserted.last_insert_id) + } + + pub async fn create_ncm_track( + &self, + ncm_track: &ncmapi::types::Song, + raw_json: Option<&str>, + ) -> Result { + let now = Utc::now(); + let artists = ncm_track + .artists + .iter() + .flat_map(|a| a.name.clone()) + .collect::>(); + let artist = if artists.is_empty() { + NotSet + } else { + Set(Some(artists.join(","))) + }; + let mut active_track = entity::track::ActiveModel { + id: NotSet, + title: Set(ncm_track.name.clone()), + artist, + album: Set(ncm_track.album.name.clone()), + duration: Set(Some(ncm_track.duration as u32)), + genre: NotSet, + year: NotSet, + created_at: NotSet, + updated_at: Set(now), + }; + + let mut active_netease_src = entity::netease_src::ActiveModel { + track_id: NotSet, + netease_id: Set(ncm_track.id.to_string()), + pop: Set(Some(ncm_track.pop as f64)), + raw_json: Set(raw_json.map(|s| s.to_owned())), + }; + + let ncm: Option = NeteaseSrc::find() + .filter(entity::netease_src::Column::NeteaseId.eq(ncm_track.id as u32)) + .one(&self.db) + .await?; + let (track_entity, netease_src) = if let Some(ncm) = ncm { + // update exists track info + let txn = self.db.begin().await?; + + active_track.id = Set(ncm.track_id); + let track_entity = active_track.update(&txn).await?; + + active_netease_src.track_id = Set(track_entity.id); + let netease_src = active_netease_src.update(&txn).await?; + txn.commit().await?; + + (track_entity, netease_src) + } else { + // create track with netease src + let txn = self.db.begin().await?; + + active_track.created_at = Set(now); + let track_entity = active_track.insert(&txn).await?; + + active_netease_src.track_id = Set(track_entity.id); + let netease_src = active_netease_src.insert(&txn).await?; + + txn.commit().await?; + + (track_entity, netease_src) + }; + let mut track = abi::Track::from_entity(track_entity); + track.netease_src = Some(abi::NeteaseSource { + id: netease_src.netease_id, + pop: netease_src.pop.map(|p| p as f32), + }); + Ok(track) + } + + pub async fn create_ncm_tracks( + &self, + tracks: &[ncmapi::types::Song], + ) -> Result, MusyncError> { + fn compress_artists(ncm_track: &ncmapi::types::Song) -> ActiveValue> { + let artists = ncm_track + .artists + .iter() + .flat_map(|a| a.name.clone()) + .collect::>(); + if artists.is_empty() { + NotSet + } else { + Set(Some(artists.join(","))) + } + } + trace!("creating ncm tracks, size: {}", tracks.len()); + let now = Utc::now(); + + let exist_tracks = NeteaseSrc::find() + .filter( + entity::netease_src::Column::NeteaseId.is_in( + tracks + .iter() + .map(|t| t.id.to_string()) + .collect::>() + .to_owned(), + ), + ) + .all(&self.db) + .await?; + let mut exists = HashMap::with_capacity(exist_tracks.len()); + for track in exist_tracks { + exists.insert(track.netease_id.clone(), track); + } + let mut abi_tracks = vec![]; + let txn = self.db.begin().await?; + + let mut updated_count = 0; + let mut inserted_count = 0; + for track in tracks { + let mut active_netease_src = entity::netease_src::ActiveModel { + track_id: NotSet, + netease_id: Set(track.id.to_string()), + pop: Set(Some(track.pop as f64)), + raw_json: NotSet, + }; + let mut active_track = entity::track::ActiveModel { + id: NotSet, + title: Set(track.name.clone()), + artist: compress_artists(track), + album: Set(track.album.name.clone()), + duration: Set(Some(track.duration as u32)), + genre: NotSet, + year: NotSet, + created_at: NotSet, + updated_at: Set(now), + }; + + let (track_entity, netease_src_entity) = + if let Some(exist) = exists.get(&track.id.to_string()) { + // update exists track info + active_track.id = Set(exist.track_id); + let track_entity = active_track.update(&txn).await?; + active_netease_src.track_id = Set(track_entity.id); + let netease_src_entity = active_netease_src.update(&txn).await?; + + updated_count += 1; + (track_entity, netease_src_entity) + } else { + // create track with netease src + active_track.created_at = Set(now); + let track_entity = active_track.insert(&txn).await?; + active_netease_src.track_id = Set(track_entity.id); + let netease_src_entity = active_netease_src.insert(&txn).await?; + + inserted_count += 1; + (track_entity, netease_src_entity) + }; + let mut track = abi::Track::from_entity(track_entity); + track.netease_src = Some(abi::NeteaseSource { + id: netease_src_entity.netease_id, + pop: netease_src_entity.pop.map(|p| p as f32), + }); + abi_tracks.push(track); + } + + txn.commit().await?; + + trace!("created ncm tracks, updated: {updated_count}, inserted: {inserted_count}"); + Ok(abi_tracks) + } + + pub async fn remove_folder(&self, folder: &str) -> Result { + let txn = self.db.begin().await?; + // delete cascade + LocalSrcFolder::delete_many() + .filter(entity::local_src_folder::Column::Path.eq(folder)) + .exec(&txn) + .await + .map_err(|_| (MusyncError::FolderNotFound(folder.to_owned())))?; + // delete tracks without local_src and netease_src + use sea_orm::query::*; + let deleted = Track::delete_many() + .filter( + entity::track::Column::Id.not_in_subquery( + LocalSrc::find() + .select_only() + .column(entity::local_src::Column::TrackId) + .into_query(), + ), + ) + .exec(&txn) + .await?; + txn.commit().await?; + trace!("deleted tracks: {:?}", deleted); + Ok(deleted.rows_affected) + } + + pub async fn query_local_folders( + &self, + query: QueryLocalFoldersRequest, + ) -> Result, MusyncError> { + let QueryLocalFoldersRequest {} = query; + let folders = LocalSrcFolder::find().all(&self.db).await?; + Ok(folders.into_iter().map(LocalFolder::from_entity).collect()) + } + + pub async fn update_track(&self, update: abi::TrackUpdate) -> Result { + if update.netease_src.is_some() { + warn!("netease_src is not supported yet"); + } + let now = Utc::now(); + let old = Track::find_by_id(update.id) + .one(&self.db) + .await? + .ok_or(MusyncError::TrackNotFound(update.id))?; + + let mut updating: entity::track::ActiveModel = old.into(); + if let Some(title) = update.title { + updating.title = Set(title); + } + if let Some(artist) = update.artist { + updating.artist = Set(Some(artist)); + } + if let Some(album) = update.album { + updating.album = Set(Some(album)); + } + if let Some(duration) = update.duration { + updating.duration = Set(Some(duration)); + } + if let Some(genre) = update.genre { + updating.genre = Set(Some(genre)); + } + if let Some(year) = update.year { + updating.year = Set(Some(year)); + } + updating.updated_at = Set(now); + updating.update(&self.db).await?; + + if let Some(src) = update.local_src { + let old = LocalSrc::find_by_id(update.id).one(&self.db).await?; + if let Some(old) = old { + let mut updating: entity::local_src::ActiveModel = old.into(); + updating.path = Set(src.path); + updating.update(&self.db).await?; + } else { + let inserted = entity::local_src::ActiveModel { + track_id: Set(update.id), + path: Set(src.path), + folder_id: NotSet, + }; + inserted.insert(&self.db).await?; + } + } + + self.track(update.id).await + } + + pub async fn delete_tracks(&self, ids: &[TrackId]) -> Result { + if ids.is_empty() { + return Ok(0); + } + + let deleted = Track::delete_many() + .filter(entity::track::Column::Id.is_in(ids.to_owned())) + .exec(&self.db) + .await?; + Ok(deleted.rows_affected) + } + + pub async fn query_tracks( + &self, + query: abi::QueryTracksRequest, + ) -> Result, MusyncError> { + trace!("query tracks: {:?}", query); + + let rows = if query.title.is_none() && query.artist.is_none() && query.album.is_none() { + Track::find().all(&self.db).await? + } else { + Track::find() + .filter( + Condition::any() + .add_option( + query + .title + .map(|title| entity::track::Column::Title.like(format!("%{}%", title))), + ) + .add_option( + query + .artist + .map(|artist| entity::track::Column::Artist.like(format!("%{}%", artist))), + ) + .add_option( + query + .album + .map(|album| entity::track::Column::Album.like(format!("%{}%", album))), + ), + ) + .all(&self.db) + .await? + }; + + rows + .into_iter() + .map(|row| Ok(abi::Track::from_entity(row))) + .collect() + } + + pub async fn track(&self, id: TrackId) -> Result { + let row = Track::find_by_id(id) + .one(&self.db) + .await? + .ok_or(MusyncError::TrackNotFound(id))?; + + let mut abi_track = abi::Track::from_entity(row); + let src = LocalSrc::find_by_id(id).one(&self.db).await?; + if let Some(src) = src { + abi_track.local_src = Some(abi::LocalSource { path: src.path }) + } + + Ok(abi_track) + } +} diff --git a/src-tauri/dbm/src/user.rs b/src-tauri/dbm/src/user.rs new file mode 100644 index 0000000..1eeec82 --- /dev/null +++ b/src-tauri/dbm/src/user.rs @@ -0,0 +1,89 @@ +use abi::LoginRequest; +use chrono::Utc; +use entity::prelude::*; + +use sea_orm::{ + ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set, +}; + +use crate::{DbManager, MusyncError, UserId}; + +/// User +impl DbManager { + pub async fn create_user(&self, user: abi::User) -> Result { + let mut updating = user.clone(); + let now = Utc::now(); + let inserted = entity::user::ActiveModel { + name: Set(user.name), + created_at: Set(now), + updated_at: Set(now), + ..Default::default() + } + .insert(&self.db) + .await?; + + updating.id = inserted.id; + + Ok(updating) + } + + pub async fn update_user(&self, update: abi::UserUpdate) -> Result { + let now = Utc::now(); + + let old = User::find_by_id(update.id) + .one(&self.db) + .await? + .ok_or(MusyncError::UserNotFound(update.id))?; + let mut updating: entity::user::ActiveModel = old.into(); + updating.name = Set(update.name); + updating.updated_at = Set(now); + updating.update(&self.db).await?; + + self.user(update.id).await + } + + pub async fn delete_users(&self, ids: &[UserId]) -> Result { + if ids.is_empty() { + return Ok(0); + } + + let deleted = User::delete_many() + .filter(entity::user::Column::Id.is_in(ids.to_owned())) + .exec(&self.db) + .await?; + Ok(deleted.rows_affected) + } + + pub async fn query_users( + &self, + query: abi::QueryUsersRequest, + ) -> Result, MusyncError> { + if let Some(name) = query.name { + let rows = User::find() + .filter(entity::user::Column::Name.like(format!("%{}%", name))) + .all(&self.db) + .await?; + + Ok(rows.into_iter().map(abi::User::from_entity).collect()) + } else { + Ok(vec![]) + } + } + + pub async fn login(&self, req: LoginRequest) -> Result { + let user = User::find() + .filter(entity::user::Column::Name.eq(&req.name)) + .one(&self.db) + .await?; + user + .map(abi::User::from_entity) + .ok_or(MusyncError::LoginFailed(req.name)) + } + pub async fn user(&self, id: UserId) -> Result { + let row = User::find_by_id(id) + .one(&self.db) + .await? + .ok_or(MusyncError::UserNotFound(id))?; + Ok(abi::User::from_entity(row)) + } +} diff --git a/src-tauri/entity/src/lib.rs b/src-tauri/entity/src/lib.rs index 3024da5..5986a8b 100644 --- a/src-tauri/entity/src/lib.rs +++ b/src-tauri/entity/src/lib.rs @@ -1,9 +1,10 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2 +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6 pub mod prelude; pub mod local_src; pub mod local_src_folder; +pub mod netease_src; pub mod play_queue; pub mod play_queue_track; pub mod playlist; diff --git a/src-tauri/entity/src/local_src.rs b/src-tauri/entity/src/local_src.rs index 747bed4..ba32891 100644 --- a/src-tauri/entity/src/local_src.rs +++ b/src-tauri/entity/src/local_src.rs @@ -1,4 +1,4 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2 +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6 use sea_orm::entity::prelude::*; diff --git a/src-tauri/entity/src/local_src_folder.rs b/src-tauri/entity/src/local_src_folder.rs index d66f4a5..2980ea0 100644 --- a/src-tauri/entity/src/local_src_folder.rs +++ b/src-tauri/entity/src/local_src_folder.rs @@ -1,4 +1,4 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2 +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6 use sea_orm::entity::prelude::*; diff --git a/src-tauri/entity/src/netease_src.rs b/src-tauri/entity/src/netease_src.rs new file mode 100644 index 0000000..44a8409 --- /dev/null +++ b/src-tauri/entity/src/netease_src.rs @@ -0,0 +1,34 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "netease_src")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub track_id: i32, + pub netease_id: String, + #[sea_orm(column_type = "Double", nullable)] + pub pop: Option, + pub raw_json: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::track::Entity", + from = "Column::TrackId", + to = "super::track::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Track, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Track.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src-tauri/entity/src/play_queue.rs b/src-tauri/entity/src/play_queue.rs index b2f38ca..081b389 100644 --- a/src-tauri/entity/src/play_queue.rs +++ b/src-tauri/entity/src/play_queue.rs @@ -1,4 +1,4 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2 +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6 use sea_orm::entity::prelude::*; diff --git a/src-tauri/entity/src/play_queue_track.rs b/src-tauri/entity/src/play_queue_track.rs index 3ffb814..8c93b35 100644 --- a/src-tauri/entity/src/play_queue_track.rs +++ b/src-tauri/entity/src/play_queue_track.rs @@ -1,4 +1,4 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2 +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6 use sea_orm::entity::prelude::*; diff --git a/src-tauri/entity/src/playlist.rs b/src-tauri/entity/src/playlist.rs index f74a841..3727e22 100644 --- a/src-tauri/entity/src/playlist.rs +++ b/src-tauri/entity/src/playlist.rs @@ -1,4 +1,4 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2 +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6 use sea_orm::entity::prelude::*; diff --git a/src-tauri/entity/src/playlist_track.rs b/src-tauri/entity/src/playlist_track.rs index 484ddcf..f1004ec 100644 --- a/src-tauri/entity/src/playlist_track.rs +++ b/src-tauri/entity/src/playlist_track.rs @@ -1,4 +1,4 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2 +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6 use sea_orm::entity::prelude::*; diff --git a/src-tauri/entity/src/prelude.rs b/src-tauri/entity/src/prelude.rs index 6a49f7e..1c4d99e 100644 --- a/src-tauri/entity/src/prelude.rs +++ b/src-tauri/entity/src/prelude.rs @@ -1,7 +1,8 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2 +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6 pub use super::local_src::Entity as LocalSrc; pub use super::local_src_folder::Entity as LocalSrcFolder; +pub use super::netease_src::Entity as NeteaseSrc; pub use super::play_queue::Entity as PlayQueue; pub use super::play_queue_track::Entity as PlayQueueTrack; pub use super::playlist::Entity as Playlist; diff --git a/src-tauri/entity/src/track.rs b/src-tauri/entity/src/track.rs index dc7952d..820acbc 100644 --- a/src-tauri/entity/src/track.rs +++ b/src-tauri/entity/src/track.rs @@ -1,4 +1,4 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2 +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6 use sea_orm::entity::prelude::*; @@ -21,6 +21,8 @@ pub struct Model { pub enum Relation { #[sea_orm(has_many = "super::local_src::Entity")] LocalSrc, + #[sea_orm(has_many = "super::netease_src::Entity")] + NeteaseSrc, #[sea_orm(has_many = "super::play_queue_track::Entity")] PlayQueueTrack, #[sea_orm(has_many = "super::playlist_track::Entity")] @@ -33,6 +35,12 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::NeteaseSrc.def() + } +} + impl Related for Entity { fn to() -> RelationDef { Relation::PlayQueueTrack.def() diff --git a/src-tauri/entity/src/user.rs b/src-tauri/entity/src/user.rs index a3d8c97..6b88b53 100644 --- a/src-tauri/entity/src/user.rs +++ b/src-tauri/entity/src/user.rs @@ -1,4 +1,4 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2 +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6 use sea_orm::entity::prelude::*; diff --git a/src-tauri/entity/src/user_playlist.rs b/src-tauri/entity/src/user_playlist.rs index 7bc65ea..59f591d 100644 --- a/src-tauri/entity/src/user_playlist.rs +++ b/src-tauri/entity/src/user_playlist.rs @@ -1,4 +1,4 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2 +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6 use sea_orm::entity::prelude::*; diff --git a/src-tauri/migration/src/lib.rs b/src-tauri/migration/src/lib.rs index 036688c..12e3ac0 100644 --- a/src-tauri/migration/src/lib.rs +++ b/src-tauri/migration/src/lib.rs @@ -1,12 +1,16 @@ pub use sea_orm_migration::prelude::*; -mod m20230813_000001_create_table; +pub(crate) mod m20230813_000001_create_table; +mod m20231204_162103_add_ncm_src; pub struct Migrator; #[async_trait::async_trait] impl MigratorTrait for Migrator { fn migrations() -> Vec> { - vec![Box::new(m20230813_000001_create_table::Migration)] + vec![ + Box::new(m20230813_000001_create_table::Migration), + Box::new(m20231204_162103_add_ncm_src::Migration), + ] } } diff --git a/src-tauri/migration/src/m20230813_000001_create_table.rs b/src-tauri/migration/src/m20230813_000001_create_table.rs index 16b06f0..ae81a20 100644 --- a/src-tauri/migration/src/m20230813_000001_create_table.rs +++ b/src-tauri/migration/src/m20230813_000001_create_table.rs @@ -467,7 +467,7 @@ impl MigrationTrait for Migration { } #[derive(DeriveIden)] -enum Track { +pub(crate) enum Track { Table, Id, Title, diff --git a/src-tauri/migration/src/m20231204_162103_add_ncm_src.rs b/src-tauri/migration/src/m20231204_162103_add_ncm_src.rs new file mode 100644 index 0000000..48107ff --- /dev/null +++ b/src-tauri/migration/src/m20231204_162103_add_ncm_src.rs @@ -0,0 +1,71 @@ +use sea_orm_migration::prelude::*; + +use crate::m20230813_000001_create_table::Track; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(NeteaseSrc::Table) + .if_not_exists() + .col( + ColumnDef::new(NeteaseSrc::TrackId) + .integer() + .not_null() + .primary_key(), + ) + .col( + ColumnDef::new(NeteaseSrc::NeteaseId) + .string() + .not_null() + .unique_key(), + ) + .col(ColumnDef::new(NeteaseSrc::Pop).float()) + .col(ColumnDef::new(NeteaseSrc::RawJson).string()) + .foreign_key( + ForeignKey::create() + .name("fk-netease_src-track_id") + .from(NeteaseSrc::Table, NeteaseSrc::TrackId) + .to(Track::Table, Track::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + manager.create_index( + sea_query::Index::create() + .name("idx-netease_src-netease_id") + .table(NeteaseSrc::Table) + .col(NeteaseSrc::NeteaseId) + .to_owned(), + ).await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table( + Table::drop() + .if_exists() + .table(NeteaseSrc::Table) + .to_owned(), + ) + .await + } +} + +#[derive(DeriveIden)] +enum NeteaseSrc { + Table, + TrackId, + NeteaseId, + Pop, + RawJson, +} diff --git a/src-tauri/server/Cargo.toml b/src-tauri/server/Cargo.toml index 9a6af6e..0ed29bc 100644 --- a/src-tauri/server/Cargo.toml +++ b/src-tauri/server/Cargo.toml @@ -36,8 +36,7 @@ futures = { workspace = true } mime_guess = "2.0.4" dashmap = "5.5.1" utils = { path = "../utils" } -# ncmapi = { path="../../../ncmapi-rs" } -ncmapi = { git="https://github.com/Discreater/ncmapi-rs" } +ncmapi = { workspace = true } [dev-dependencies] tracing-subscriber = { workspace = true } diff --git a/src-tauri/server/src/grpc.rs b/src-tauri/server/src/grpc.rs index f574b73..9709de1 100644 --- a/src-tauri/server/src/grpc.rs +++ b/src-tauri/server/src/grpc.rs @@ -15,7 +15,7 @@ use chrono::{Days, Utc}; use dbm::UserId; use futures::Stream; -use ncmapi::NcmApi; +use ncmapi::{types::SearchSongResp, NcmApi}; use tonic::{Extensions, Request, Response, Status}; use tracing::{error, trace}; use utils::WithError; @@ -78,7 +78,7 @@ impl abi::musync_service_server::MusyncService for GrpcServer { let tracks = musync::track::get_tracks_in_folder(&req.path); let len = tracks.len(); - self.db.create_tracks(tracks, &req.path).await?; + self.db.create_local_tracks(tracks, &req.path).await?; Ok(Response::new(AddLocalFolderResponse { added_tracks: len as u32, @@ -264,13 +264,25 @@ impl abi::musync_service_server::MusyncService for GrpcServer { ..Default::default() }); let ncm_search = self.ncm_api.search(&query, None); - let (db_tracks, ncm_res) = tokio::join!(db_search, ncm_search); + let (db_tracks, ncm_resp) = tokio::join!(db_search, ncm_search); let db_tracks = db_tracks?; - let ncm_res = match ncm_res { - Ok(res) => String::from_utf8_lossy(res.data()).into_owned(), - Err(e) => e.to_string(), - }; - Ok(Response::new(SearchAllResponse { db_tracks, ncm_res })) + + let api_resp = + ncm_resp.map_err(|e| Status::internal(format!("ncm api error: {}", e)))?; + + let parsed_resp = serde_json::from_slice::(api_resp.data()) + .map_err(|_| Status::internal("ncm api response parse failed"))?; + let result = parsed_resp.result.ok_or_else(|| { + Status::internal(format!( + "ncm api response error, ncm code: {}", + parsed_resp.code + )) + })?; + let ncm_tracks = self.db.create_ncm_tracks(&result.songs).await?; + Ok(Response::new(SearchAllResponse { + db_tracks, + ncm_tracks, + })) } async fn rebuild_index( diff --git a/src/generated/protos/musync.ts b/src/generated/protos/musync.ts index 9717a83..be783bd 100644 --- a/src/generated/protos/musync.ts +++ b/src/generated/protos/musync.ts @@ -92,6 +92,8 @@ export interface Track { export interface NeteaseSource { /** id of the track in netease */ id: string; + /** popularity of the track */ + pop?: number | undefined; } /** LocalSource */ @@ -408,7 +410,7 @@ export interface SearchAllRequest { export interface SearchAllResponse { dbTracks: Track[]; /** netease music search result */ - ncmRes: string; + ncmTracks: Track[]; } export interface RebuildIndexRequest { @@ -951,7 +953,7 @@ export const Track = { }; function createBaseNeteaseSource(): NeteaseSource { - return { id: "" }; + return { id: "", pop: undefined }; } export const NeteaseSource = { @@ -959,6 +961,9 @@ export const NeteaseSource = { if (message.id !== "") { writer.uint32(10).string(message.id); } + if (message.pop !== undefined) { + writer.uint32(21).float(message.pop); + } return writer; }, @@ -976,6 +981,13 @@ export const NeteaseSource = { message.id = reader.string(); continue; + case 2: + if (tag !== 21) { + break; + } + + message.pop = reader.float(); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -986,7 +998,10 @@ export const NeteaseSource = { }, fromJSON(object: any): NeteaseSource { - return { id: isSet(object.id) ? globalThis.String(object.id) : "" }; + return { + id: isSet(object.id) ? globalThis.String(object.id) : "", + pop: isSet(object.pop) ? globalThis.Number(object.pop) : undefined, + }; }, toJSON(message: NeteaseSource): unknown { @@ -994,6 +1009,9 @@ export const NeteaseSource = { if (message.id !== "") { obj.id = message.id; } + if (message.pop !== undefined) { + obj.pop = message.pop; + } return obj; }, @@ -1003,6 +1021,7 @@ export const NeteaseSource = { fromPartial, I>>(object: I): NeteaseSource { const message = createBaseNeteaseSource(); message.id = object.id ?? ""; + message.pop = object.pop ?? undefined; return message; }, }; @@ -3961,7 +3980,7 @@ export const SearchAllRequest = { }; function createBaseSearchAllResponse(): SearchAllResponse { - return { dbTracks: [], ncmRes: "" }; + return { dbTracks: [], ncmTracks: [] }; } export const SearchAllResponse = { @@ -3969,8 +3988,8 @@ export const SearchAllResponse = { for (const v of message.dbTracks) { Track.encode(v!, writer.uint32(10).fork()).ldelim(); } - if (message.ncmRes !== "") { - writer.uint32(18).string(message.ncmRes); + for (const v of message.ncmTracks) { + Track.encode(v!, writer.uint32(26).fork()).ldelim(); } return writer; }, @@ -3989,12 +4008,12 @@ export const SearchAllResponse = { message.dbTracks.push(Track.decode(reader, reader.uint32())); continue; - case 2: - if (tag !== 18) { + case 3: + if (tag !== 26) { break; } - message.ncmRes = reader.string(); + message.ncmTracks.push(Track.decode(reader, reader.uint32())); continue; } if ((tag & 7) === 4 || tag === 0) { @@ -4008,7 +4027,7 @@ export const SearchAllResponse = { fromJSON(object: any): SearchAllResponse { return { dbTracks: globalThis.Array.isArray(object?.dbTracks) ? object.dbTracks.map((e: any) => Track.fromJSON(e)) : [], - ncmRes: isSet(object.ncmRes) ? globalThis.String(object.ncmRes) : "", + ncmTracks: globalThis.Array.isArray(object?.ncmTracks) ? object.ncmTracks.map((e: any) => Track.fromJSON(e)) : [], }; }, @@ -4017,8 +4036,8 @@ export const SearchAllResponse = { if (message.dbTracks?.length) { obj.dbTracks = message.dbTracks.map((e) => Track.toJSON(e)); } - if (message.ncmRes !== "") { - obj.ncmRes = message.ncmRes; + if (message.ncmTracks?.length) { + obj.ncmTracks = message.ncmTracks.map((e) => Track.toJSON(e)); } return obj; }, @@ -4029,7 +4048,7 @@ export const SearchAllResponse = { fromPartial, I>>(object: I): SearchAllResponse { const message = createBaseSearchAllResponse(); message.dbTracks = object.dbTracks?.map((e) => Track.fromPartial(e)) || []; - message.ncmRes = object.ncmRes ?? ""; + message.ncmTracks = object.ncmTracks?.map((e) => Track.fromPartial(e)) || []; return message; }, }; diff --git a/src/pages/main/SearchResult.vue b/src/pages/main/SearchResult.vue index c9169ab..8593711 100644 --- a/src/pages/main/SearchResult.vue +++ b/src/pages/main/SearchResult.vue @@ -8,7 +8,6 @@ import QPivotItem from '~qui/pivot/QPivotItem.vue'; import { ApiClient } from '~/api/client'; import Basic from '~/layouts/Basic.vue'; import type { Track } from '~/generated/protos/musync'; -import type { NcmSearchResult } from '~/model_ext/ncm'; import QTable from '~qui/table/QTable.vue'; import type { Column } from '~qui/table/types'; @@ -21,14 +20,14 @@ defineProps<{ const loading = ref(true); const tracks = ref([]); -const ncmRes = ref(); +const ncmTracks = ref(); const route = useRoute(); watch(() => route.query, async () => { loading.value = true; const resp = await ApiClient.grpc().SearchAll({ query: route.query.q as string }); tracks.value = resp.dbTracks; - ncmRes.value = JSON.parse(resp.ncmRes); + ncmTracks.value = resp.ncmTracks; loading.value = false; }, { immediate: true, @@ -42,19 +41,32 @@ const localCols: Column[] = [ { key: 'title', title: t('track.title') }, { key: 'artist', title: t('track.artist') }, { key: 'album', title: t('track.album') }, + { key: 'duration', title: t('track.duration'), style: { gridTemplateColumn: '48px' } }, ]; const ncmCols: Column[] = [ { key: 'title', title: t('track.title') }, { key: 'artist', title: t('track.artist') }, { key: 'album', title: t('track.album') }, - { key: 'duration', title: t('track.duration'), style: { - gridTemplateColumn: '48px', - } }, - { key: 'pop', title: t('track.pop'), style: { - textAlign: 'right', - gridTemplateColumn: '24px', - } }, + { + key: 'duration', + title: t('track.duration'), + style: { + gridTemplateColumn: '48px', + }, + }, + { + key: 'pop', + title: t('track.pop'), + style: { + textAlign: 'right', + gridTemplateColumn: '24px', + }, + }, ]; + +function formatDuration(track: Track) { + return track.duration ? formatTime(track.duration! / 1000) : ''; +}