diff --git a/src/application/handlers.rs b/src/application/handlers.rs index c157e17..dc79cc4 100644 --- a/src/application/handlers.rs +++ b/src/application/handlers.rs @@ -5,7 +5,7 @@ use ratatui::widgets::ListState; use crate::application::app_state::App; use crate::data::{Artist, Track}; -use crate::domain::conversions::{convert_artist, convert_track}; +use crate::domain::conversions::{convert_album, convert_artist, convert_track}; use crate::domain::navigation::Tab; use crate::grpc::GrpcResponse; use crate::ui::library::{LibraryFocus, LibraryState}; @@ -285,8 +285,25 @@ impl App { ); } GrpcResponse::Album { album, tracks } => { - let converted: Vec = tracks.into_iter().map(convert_track).collect(); - self.library.cache_tracks(album.id, converted); + let album_downloaded = album + .download + .as_ref() + .is_some_and(|d| matches!(d.state.as_str(), "completed" | "downloaded")); + let album_quality = album + .download + .as_ref() + .map(|d| d.format.as_str()) + .filter(|f| !f.is_empty()) + .unwrap_or("—") + .to_string(); + let album_id = album.id.clone(); + let updated = convert_album(album); + self.library.update_album(updated); + let converted: Vec = tracks + .into_iter() + .map(|t| convert_track(t, album_downloaded, &album_quality)) + .collect(); + self.library.cache_tracks(album_id, converted); } GrpcResponse::Error(msg) => { self.set_error(msg); diff --git a/src/application/library_state.rs b/src/application/library_state.rs index 84bacd7..67eb50e 100644 --- a/src/application/library_state.rs +++ b/src/application/library_state.rs @@ -249,6 +249,19 @@ impl LibraryState { self.selected_album().map(|a| a.id.clone()) } + pub fn update_album(&mut self, updated: Album) { + for artist in &mut self.artists { + if let Some(album) = artist.albums.iter_mut().find(|a| a.id == updated.id) { + album.have = updated.have; + album.total = updated.total; + album.status = updated.status; + album.quality = updated.quality.clone(); + album.monitored = updated.monitored; + return; + } + } + } + pub fn clear_cache(&mut self) { self.tracks_cache.clear(); self.pending_album_id = None; diff --git a/src/domain/conversions.rs b/src/domain/conversions.rs index 7255f95..5acab49 100644 --- a/src/domain/conversions.rs +++ b/src/domain/conversions.rs @@ -21,26 +21,30 @@ pub fn convert_album(detail: AlbumDetail) -> Album { let monitor_state = MonitorState::from_proto(detail.monitor_state); let monitored = monitor_state.is_monitored(); - let (have, status, quality) = if let Some(download) = detail.download { - let have = match download.state.as_str() { - "completed" | "downloaded" => detail.total_tracks as u16, - _ => 0, + let (have, status, quality) = if let Some(download) = &detail.download { + let is_completed = matches!(download.state.as_str(), "completed" | "downloaded"); + let have = if is_completed { + detail + .release + .as_ref() + .map(|r| r.track_count as u16) + .unwrap_or(detail.total_tracks as u16) + } else { + 0 }; - let status = match download.state.as_str() { - "completed" | "downloaded" => AlbumStatus::Complete, - "downloading" => AlbumStatus::Partial, - _ => { - if monitored { - AlbumStatus::Wanted - } else { - AlbumStatus::Unmonitored - } - } + let status = if is_completed { + AlbumStatus::Complete + } else if download.state == "downloading" { + AlbumStatus::Partial + } else if monitored { + AlbumStatus::Wanted + } else { + AlbumStatus::Unmonitored }; let quality = if !download.format.is_empty() { - download.format + download.format.clone() } else if !download.quality.is_empty() { - download.quality + download.quality.clone() } else { "—".to_string() }; @@ -54,13 +58,23 @@ pub fn convert_album(detail: AlbumDetail) -> Album { (0, status, "—".to_string()) }; + let total = if detail.total_tracks > 0 { + detail.total_tracks as u16 + } else { + detail + .release + .as_ref() + .map(|r| r.track_count as u16) + .unwrap_or(0) + }; + Album { id: detail.id, title: detail.title, year, album_type: detail.album_type, monitored, - total: detail.total_tracks as u16, + total, have, quality, status, @@ -75,13 +89,16 @@ pub fn parse_year(date_str: &str) -> u16 { .unwrap_or(0) } -pub fn convert_track(detail: TrackDetail) -> Track { - let have = detail.file.is_some(); - let quality = detail - .file - .as_ref() - .map(|f| f.format.clone()) - .unwrap_or_else(|| "—".to_string()); +pub fn convert_track(detail: TrackDetail, album_downloaded: bool, album_quality: &str) -> Track { + let has_file = !detail.file_path.is_empty(); + let have = has_file || album_downloaded; + let quality = if !detail.format.is_empty() { + detail.format + } else if album_downloaded { + album_quality.to_string() + } else { + "—".to_string() + }; let duration = format_duration(detail.duration_ms); Track { diff --git a/src/infrastructure/grpc/mod.rs b/src/infrastructure/grpc/mod.rs index 5a8a2d1..8151d37 100644 --- a/src/infrastructure/grpc/mod.rs +++ b/src/infrastructure/grpc/mod.rs @@ -51,10 +51,13 @@ impl GrpcClient { ) -> Result<(AlbumDetail, Vec), tonic::Status> { let response = self.music.get_album(GetAlbumRequest { album_id }).await?; let inner = response.into_inner(); - let album = inner + let info = inner + .info + .ok_or_else(|| tonic::Status::not_found("Album info not found in response"))?; + let album = info .album .ok_or_else(|| tonic::Status::not_found("Album not found in response"))?; - Ok((album, inner.tracks)) + Ok((album, info.tracks)) } } diff --git a/src/proto/music_agregator.v1.rs b/src/proto/music_agregator.v1.rs index b164079..9d07841 100644 --- a/src/proto/music_agregator.v1.rs +++ b/src/proto/music_agregator.v1.rs @@ -76,6 +76,8 @@ pub struct AlbumDetail { pub monitor_state: i32, #[prost(message, optional, tag = "12")] pub download: ::core::option::Option, + #[prost(message, optional, tag = "13")] + pub release: ::core::option::Option, } #[derive(Clone, PartialEq, ::prost::Message)] pub struct DownloadInfo { @@ -94,13 +96,28 @@ pub struct GetAlbumRequest { pub album_id: ::prost::alloc::string::String, } #[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetAlbumResponse { +pub struct AlbumInfo { #[prost(message, optional, tag = "1")] pub album: ::core::option::Option, #[prost(message, repeated, tag = "2")] pub tracks: ::prost::alloc::vec::Vec, } #[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetAlbumResponse { + #[prost(message, optional, tag = "1")] + pub info: ::core::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AnalyzeAlbumReleaseRequest { + #[prost(string, tag = "1")] + pub album_id: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AnalyzeAlbumReleaseResponse { + #[prost(message, optional, tag = "1")] + pub info: ::core::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] pub struct TrackDetail { #[prost(string, tag = "1")] pub id: ::prost::alloc::string::String, @@ -120,8 +137,20 @@ pub struct TrackDetail { pub explicit: bool, #[prost(message, repeated, tag = "9")] pub artists: ::prost::alloc::vec::Vec, - #[prost(message, optional, tag = "10")] - pub file: ::core::option::Option, + #[prost(string, tag = "10")] + pub file_path: ::prost::alloc::string::String, + #[prost(int64, tag = "11")] + pub file_size: i64, + #[prost(string, tag = "12")] + pub format: ::prost::alloc::string::String, + #[prost(int32, tag = "13")] + pub bit_depth: i32, + #[prost(int32, tag = "14")] + pub sample_rate: i32, + #[prost(int32, tag = "15")] + pub channels: i32, + #[prost(int32, tag = "16")] + pub bitrate_kbps: i32, } #[derive(Clone, PartialEq, ::prost::Message)] pub struct ArtistCredit { @@ -131,13 +160,35 @@ pub struct ArtistCredit { pub name: ::prost::alloc::string::String, } #[derive(Clone, PartialEq, ::prost::Message)] -pub struct TrackFile { +pub struct AlbumReleaseDetail { #[prost(string, tag = "1")] - pub path: ::prost::alloc::string::String, + pub id: ::prost::alloc::string::String, #[prost(string, tag = "2")] pub format: ::prost::alloc::string::String, - #[prost(int64, tag = "3")] - pub size: i64, + #[prost(int32, tag = "3")] + pub bit_depth: i32, + #[prost(int32, tag = "4")] + pub sample_rate: i32, + #[prost(int32, tag = "5")] + pub channels: i32, + #[prost(bool, tag = "6")] + pub is_lossless: bool, + #[prost(string, tag = "7")] + pub source: ::prost::alloc::string::String, + #[prost(int64, tag = "8")] + pub total_size: i64, + #[prost(int32, tag = "9")] + pub total_duration_ms: i32, + #[prost(int32, tag = "10")] + pub track_count: i32, + #[prost(bool, tag = "11")] + pub has_cover_art: bool, + #[prost(bool, tag = "12")] + pub has_cue_sheet: bool, + #[prost(bool, tag = "13")] + pub has_rip_log: bool, + #[prost(string, tag = "14")] + pub path: ::prost::alloc::string::String, } #[derive(Clone, PartialEq, ::prost::Message)] pub struct MonitoredRelease { @@ -417,5 +468,34 @@ pub mod music_agregator_service_client { ); self.inner.unary(req, path, codec).await } + pub async fn analyze_album_release( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/music_agregator.v1.MusicAgregatorService/AnalyzeAlbumRelease", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "music_agregator.v1.MusicAgregatorService", + "AnalyzeAlbumRelease", + ), + ); + self.inner.unary(req, path, codec).await + } } }