@@ -7,26 +7,23 @@ pub mod queue;
77pub mod search;
88use crate :: {
99 config:: { Config , ConfigError } ,
10- mpris_handler:: { MprisPlayer } ,
10+ mpris_handler:: MprisPlayer ,
1111 player:: { Player , PlayerCommand , PlayerState , SharedPlayerState } ,
1212 search:: SearchEngine ,
1313 subsonic:: SubsonicClient ,
1414} ;
1515use anyhow:: Result ;
16- use crossterm:: {
17- terminal:: disable_raw_mode,
18- } ;
19- use mpris_server:: { Metadata , PlaybackStatus , Server , Time } ;
16+ use crossterm:: terminal:: disable_raw_mode;
17+ use mpris_server:: { Metadata , PlaybackStatus , Server , Time } ;
2018use ratatui:: widgets:: ListState ;
21- use ratatui_image:: { protocol:: StatefulProtocol } ;
19+ use ratatui_image:: protocol:: StatefulProtocol ;
2220use std:: {
2321 io:: { self , Write } ,
2422 rc:: Rc ,
2523 sync:: { Arc , RwLock } ,
24+ time:: Duration ,
2625} ;
27- use tokio:: {
28- sync:: { Mutex , mpsc} ,
29- } ;
26+ use tokio:: sync:: { Mutex , mpsc} ;
3027
3128pub struct TerminalGuard ; // used to make sure terminal goes back to normal
3229impl TerminalGuard {
@@ -57,7 +54,6 @@ impl Drop for TerminalGuard {
5754 }
5855}
5956
60-
6157#[ derive( Debug , thiserror:: Error ) ]
6258pub enum AppError {
6359 #[ error( "No Track Loaded" ) ]
@@ -157,9 +153,23 @@ pub enum ActiveTab {
157153 Favorites ,
158154}
159155
156+ pub enum LibraryMessage {
157+ Loaded {
158+ songs : Vec < Track > ,
159+ artists : Vec < Artist > ,
160+ albums : Vec < Album > ,
161+ playlists : Vec < Playlists > ,
162+ favorites : Vec < Track > ,
163+ } ,
164+ SongsAppended ( Vec < Track > ) ,
165+ Error ( String ) ,
166+ }
167+
160168pub struct App {
161169 pub config : Config ,
162170 pub subsonic_client : Arc < SubsonicClient > ,
171+ pub needs_initial_load : bool ,
172+ pub library_rx : Option < mpsc:: Receiver < LibraryMessage > > ,
163173 pub player : Rc < Mutex < Player > > ,
164174 pub is_playing : bool ,
165175 pub current_track : Option < Track > ,
@@ -169,6 +179,9 @@ pub struct App {
169179 pub shared_state : SharedPlayerState ,
170180 pub command_receiver : mpsc:: Receiver < PlayerCommand > ,
171181 pub metadata : Metadata ,
182+ pub widget_notification : Option < ( String , std:: time:: Instant ) > ,
183+ pub w_notification_duration : std:: time:: Duration ,
184+ pub last_search_keystroke : Option < std:: time:: Instant > ,
172185 // TabSelection
173186 pub queue_tab : TabSelection < Track > ,
174187 pub tracks_tab : TabSelection < Track > ,
@@ -234,31 +247,33 @@ impl App {
234247 position : Time :: ZERO ,
235248 } ) ) ;
236249 let mprisserver = {
237-
238- let mut result = None ;
239- for i in 0 .. 10u32 {
240- let iface = MprisPlayer :: new ( tx . clone ( ) , shared_state . clone ( ) ) ;
241- let name = if i == 0 {
242- "sonicrust" . to_string ( )
243- } else {
244- format ! ( "sonicrust.instance{}" , i )
245- } ;
246- match Server :: new ( & name , iface ) . await {
247- Ok ( s ) => {
248- result = Some ( s ) ;
249- break ;
250- }
251- Err ( e ) => {
252- eprintln ! ( "Mpris name '{}' taken, trying next... ({})" , name , e ) ;
250+ let mut result = None ;
251+ for i in 0 .. 10u32 {
252+ let iface = MprisPlayer :: new ( tx . clone ( ) , shared_state . clone ( ) ) ;
253+ let name = if i == 0 {
254+ "sonicrust" . to_string ( )
255+ } else {
256+ format ! ( "sonicrust.instance{}" , i )
257+ } ;
258+ match Server :: new ( & name , iface ) . await {
259+ Ok ( s ) => {
260+ result = Some ( s ) ;
261+ break ;
262+ }
263+ Err ( e ) => {
264+ eprintln ! ( "Mpris name '{}' taken, trying next... ({})" , name , e ) ;
265+ }
253266 }
254267 }
255- }
256- result. ok_or_else ( || anyhow:: anyhow!( "Failed to register any MPRIS name after 10 attempts" ) ) ?
257- } ;
268+ result. ok_or_else ( || {
269+ anyhow:: anyhow!( "Failed to register any MPRIS name after 10 attempts" )
270+ } ) ?
271+ } ;
258272 let search_engine = SearchEngine :: new ( config. search . fuzzy_threshold , 30 ) ;
259273
260- let mut app = Self {
274+ let app = Self {
261275 config,
276+ needs_initial_load : true ,
262277 subsonic_client : subsonic_client. clone ( ) ,
263278 player,
264279 metadata : Metadata :: default ( ) ,
@@ -267,6 +282,9 @@ impl App {
267282 current_track : None ,
268283 current_volume : 1.0 ,
269284 shared_state,
285+ last_search_keystroke : None ,
286+ widget_notification : None ,
287+ w_notification_duration : std:: time:: Duration :: from_secs ( 3 ) ,
270288 tracks_tab : TabSelection :: new ( ) ,
271289 queue_tab : TabSelection :: new ( ) ,
272290 artist_tab : TabSelection :: new ( ) ,
@@ -276,6 +294,7 @@ impl App {
276294 favorite_tab : TabSelection :: new ( ) ,
277295 mpris : mprisserver,
278296 command_receiver : rx,
297+ library_rx : None ,
279298 active_tab : ActiveTab :: Songs ,
280299 active_section : ActiveSection :: Others ,
281300 input_mode : InputMode :: Normal ,
@@ -287,10 +306,47 @@ impl App {
287306 cover_art_protocol : None ,
288307 } ;
289308
290- app. refresh_library ( ) . await ?;
309+ // app.refresh_library().await?;
291310 Ok ( app)
292311 }
293312 pub async fn update ( & mut self ) -> Result < ( ) > {
313+ if self . needs_initial_load {
314+ self . needs_initial_load = false ;
315+ self . start_background_load ( ) ;
316+ self . set_notification ( "Loading Library..." ) ;
317+ // self.refresh_library().await?;
318+ // self.set_notification("Library loaded");
319+ }
320+ if let Some ( rx) = & mut self . library_rx {
321+ match rx. try_recv ( ) {
322+ Ok ( LibraryMessage :: Loaded {
323+ songs,
324+ artists,
325+ albums,
326+ playlists,
327+ favorites,
328+ } ) => {
329+ self . tracks_tab . data = songs;
330+ self . artist_tab . data = artists;
331+ self . album_tab . data = albums;
332+ self . playlist_tab . data = playlists;
333+ self . favorite_tab . data = favorites;
334+ // self.library_rx = None;
335+ self . set_notification ( "Library Loaded" ) ;
336+ }
337+ Ok ( LibraryMessage :: SongsAppended ( songs) ) => {
338+ self . tracks_tab . data . extend ( songs) ;
339+ }
340+ Ok ( LibraryMessage :: Error ( e) ) => {
341+ self . set_notification ( format ! ( "Load Error: {}" , e) ) ;
342+ self . library_rx = None ;
343+ }
344+ Err ( mpsc:: error:: TryRecvError :: Empty ) => { } // This means it is still loading
345+ Err ( _) => {
346+ self . library_rx = None ;
347+ }
348+ }
349+ }
294350 while let Ok ( cmd) = self . command_receiver . try_recv ( ) {
295351 match cmd {
296352 PlayerCommand :: Play => {
@@ -351,17 +407,96 @@ impl App {
351407 }
352408 }
353409 }
410+ if let Some ( t) = self . last_search_keystroke
411+ && t. elapsed ( ) > Duration :: from_millis ( 300 )
412+ && self . is_searching
413+ {
414+ self . last_search_keystroke = None ;
415+ self . perform_search ( ) . await ?;
416+ }
354417
355418 self . check_track_finished ( ) . await ?;
356419 self . update_mpris_position ( ) . await ?;
420+ self . tick_notification ( ) ;
357421 Ok ( ( ) )
358422 }
423+ pub fn set_notification ( & mut self , msg : impl Into < String > ) {
424+ self . widget_notification = Some ( ( msg. into ( ) , std:: time:: Instant :: now ( ) ) ) ;
425+ }
426+ pub fn tick_notification ( & mut self ) {
427+ if let Some ( ( _, created) ) = & self . widget_notification
428+ && created. elapsed ( ) >= self . w_notification_duration
429+ {
430+ self . widget_notification = None ;
431+ }
432+ }
433+ pub fn start_background_load ( & mut self ) {
434+ let ( tx, rx) = mpsc:: channel ( 4 ) ;
435+ self . library_rx = Some ( rx) ;
436+ let client = self . subsonic_client . clone ( ) ;
437+ tokio:: spawn ( async move {
438+ let ( first_page, artists, albums, playlists, favorites) = match tokio:: try_join!(
439+ client. get_album_page( 0 , 10 ) ,
440+ // client.get_all_songs(),
441+ client. get_all_artists( ) ,
442+ client. get_all_albums( ) ,
443+ client. get_playlists( ) ,
444+ client. get_all_favorites( ) ,
445+ ) {
446+ Ok ( r) => r,
447+ Err ( e) => {
448+ let _ = tx. send ( LibraryMessage :: Error ( e. to_string ( ) ) ) . await ;
449+ return ;
450+ }
451+ } ;
452+ let first_songs = {
453+ let futures = first_page. iter ( ) . map ( |a| client. get_songs_in_album ( a) ) ;
454+ futures:: future:: join_all ( futures)
455+ . await
456+ . into_iter ( )
457+ . flat_map ( |r| r. unwrap_or_default ( ) )
458+ . collect :: < Vec < _ > > ( )
459+ } ;
460+ let _ = tx
461+ . send ( LibraryMessage :: Loaded {
462+ songs : first_songs,
463+ artists,
464+ albums : albums. clone ( ) ,
465+ playlists,
466+ favorites,
467+ } )
468+ . await ;
469+ let remaining = albums. iter ( ) . skip ( 10 ) ;
470+ let chunks = remaining. collect :: < Vec < _ > > ( ) ;
471+ for c in chunks. chunks ( 100 ) {
472+ let futures = c. iter ( ) . map ( |a| client. get_songs_in_album ( a) ) ;
473+ let songs = futures:: future:: join_all ( futures)
474+ . await
475+ . into_iter ( )
476+ . flat_map ( |r| r. unwrap_or_default ( ) )
477+ . collect :: < Vec < _ > > ( ) ;
478+ if tx. send ( LibraryMessage :: SongsAppended ( songs) ) . await . is_err ( ) {
479+ break ;
480+ }
481+ }
482+ } ) ;
483+ }
359484 pub async fn refresh_library ( & mut self ) -> Result < ( ) > {
360- self . tracks_tab . data = self . subsonic_client . get_all_songs ( ) . await ?;
361- self . artist_tab . data = self . subsonic_client . get_all_artists ( ) . await ?;
362- self . album_tab . data = self . subsonic_client . get_all_albums ( ) . await ?;
363- self . playlist_tab . data = self . subsonic_client . get_playlists ( ) . await ?;
364- self . favorite_tab . data = self . subsonic_client . get_all_favorites ( ) . await ?;
485+ let client = self . subsonic_client . clone ( ) ;
486+ self . set_notification ( "Loading Library..." ) ;
487+ let ( songs, artist, albums, playlists, favorites) = tokio:: try_join!(
488+ client. get_all_songs( ) ,
489+ client. get_all_artists( ) ,
490+ client. get_all_albums( ) ,
491+ client. get_playlists( ) ,
492+ client. get_all_favorites( ) ,
493+ ) ?;
494+ self . tracks_tab . data = songs;
495+ self . artist_tab . data = artist;
496+ self . album_tab . data = albums;
497+ self . playlist_tab . data = playlists;
498+ self . favorite_tab . data = favorites;
499+
365500 Ok ( ( ) )
366501 }
367502}
0 commit comments