use deadpool_sqlite::Pool; use std::{collections::HashMap, path::PathBuf, sync::Arc, time::Instant}; use tokio::{sync::Mutex, task::JoinSet}; use tonic::{Request, Response, Status}; use walkdir::{DirEntry, WalkDir}; use crate::{ checksum::generate_hash, database::tracks::{ add_track_to_playlist, create_playlist, delete_playlist, get_playlist, get_playlists, get_tracks, insert_tracks, remove_track_from_playlist, swap_playlist_tracks, }, music::metadata::{extract_track_data, TrackMetadata}, proto::library::{ library_server::Library, AddTrackToPlaylistRequest, CreatePlaylistRequest, CreatePlaylistResponse, DeletePlaylistRequest, ListPlaylistsResponse, RemoveTrackFromPlaylistRequest, SwapTracksRequest, Track, TrackList, }, state::GrooveState, }; pub struct LibraryService { #[allow(dead_code)] state: GrooveState, pool: Pool, } impl LibraryService { pub fn new(state: GrooveState, pool: Pool) -> Self { Self { state, pool } } } #[tonic::async_trait] impl Library for LibraryService { async fn list_tracks(&self, _request: Request<()>) -> Result, Status> { let Ok(tracks) = get_tracks(&self.pool).await else { return Err(Status::internal("")); }; let response = TrackList { tracks: tracks .into_iter() .map(|t| Track { hash: t.hash.clone(), name: t.name.clone(), artist_name: t.artist_name.clone(), artist_id: t.artist_id, duration: t.duration, }) .collect::>(), }; Ok(Response::new(response)) } async fn list_playlists( &self, _request: Request<()>, ) -> Result, Status> { let Ok(playlists) = get_playlists(&self.pool).await else { return Err(Status::internal("")); }; let response = ListPlaylistsResponse { playlists }; Ok(Response::new(response)) } async fn create_playlist( &self, request: Request, ) -> Result, Status> { let input = request.get_ref(); let Ok(playlist) = create_playlist(&self.pool, &input.name).await else { return Err(Status::internal("")); }; let response = CreatePlaylistResponse { playlist: Some(playlist), }; Ok(Response::new(response)) } async fn delete_playlist( &self, request: Request, ) -> Result, Status> { let input = request.get_ref(); let Ok(success) = delete_playlist(&self.pool, input.id).await else { return Err(Status::internal("")); }; if !success { return Err(Status::internal("")); } Ok(Response::new(())) } async fn add_track_to_playlist( &self, request: Request, ) -> Result, Status> { let input = request.get_ref(); let Ok(success) = add_track_to_playlist(&self.pool, input.playlist_id, &input.track_hash).await else { return Err(Status::internal("")); }; if !success { return Err(Status::internal("")); } let Ok(playlist) = get_playlist(&self.pool, input.playlist_id).await else { return Err(Status::internal("")); }; let response = TrackList { tracks: playlist.tracks, }; Ok(Response::new(response)) } async fn remove_track_from_playlist( &self, request: Request, ) -> Result, Status> { let input = request.get_ref(); let Ok(success) = remove_track_from_playlist(&self.pool, input.playlist_id, input.track_rank).await else { return Err(Status::internal("")); }; if !success { return Err(Status::internal("")); } let Ok(playlist) = get_playlist(&self.pool, input.playlist_id).await else { return Err(Status::internal("")); }; let response = TrackList { tracks: playlist.tracks, }; Ok(Response::new(response)) } async fn swap_tracks( &self, request: Request, ) -> Result, Status> { let input = request.get_ref(); let Ok(success) = swap_playlist_tracks(&self.pool, input.playlist_id, input.a_rank, input.b_rank).await else { return Err(Status::internal("")); }; if !success { return Err(Status::internal("")); } let Ok(playlist) = get_playlist(&self.pool, input.playlist_id).await else { return Err(Status::internal("")); }; let response = TrackList { tracks: playlist.tracks, }; Ok(Response::new(response)) } } pub async fn index_path(path: PathBuf, db: &Pool, path_id: u64) -> Result<(), rusqlite::Error> { let home = home::home_dir().unwrap(); let correct_path = path.to_str().unwrap().replace("~", home.to_str().unwrap()); let path_offset = correct_path.len() + 1; let now = Instant::now(); let entries: Vec = WalkDir::new(correct_path) .into_iter() .filter_map(Result::ok) .collect(); let tracks: Arc>> = Arc::new(Mutex::new(HashMap::new())); let mut set = JoinSet::new(); for entry in entries.into_iter() { if entry.file_type().is_file() && entry.path().extension().is_some_and(|ext| ext == "mp3") { let tracks = tracks.clone(); set.spawn(async move { let file_path = entry.path(); if let Ok(content) = std::fs::read(file_path) { let hash = generate_hash(&content); let relative_path = file_path.to_str().unwrap().to_string()[path_offset..].to_string(); if let Some(metadata) = extract_track_data(content, relative_path) { tracks.lock().await.insert(hash, metadata); } } }); } } set.join_all().await; let elapsed = now.elapsed(); println!("indexing took {:.2?}", elapsed); let now = Instant::now(); insert_tracks(db, Arc::try_unwrap(tracks).unwrap().into_inner(), path_id).await?; let elapsed = now.elapsed(); println!("inserting took {:.2?}", elapsed); Ok(()) }