@@ -11,7 +11,7 @@ use std::{
1111 io:: { stdout, Write } ,
1212 path:: Path ,
1313 sync:: {
14- mpsc:: { self , Receiver , SyncSender , TrySendError } ,
14+ mpsc:: { self , Receiver , SyncSender , TryRecvError , TrySendError } ,
1515 Arc , Mutex ,
1616 } ,
1717 thread,
@@ -28,7 +28,7 @@ use crate::plugin::{self, ytdlp, TrackInfo};
2828use crate :: storage:: { track_record, HistorySortField , SharedStorage , Storage , SyncWarning } ;
2929use crate :: tui:: {
3030 draw, draw_history_browser, draw_startup, restore_terminal, setup_terminal, AppState ,
31- HistoryPanelState , StartupScreenState , N_BANDS ,
31+ HistoryPanelState , StartupProgressState , StartupScreenState , N_BANDS ,
3232} ;
3333
3434pub struct PlaybackContext < ' a > {
@@ -88,6 +88,7 @@ fn browse_history_session(
8888 status : "db migrations... teaching SQLite to keep a beat" . to_string ( ) ,
8989 logs : startup_logs ( ) ,
9090 frame_count : 0 ,
91+ progress : None ,
9192 sync_warning : None ,
9293 } ;
9394 title_state. set ( "looper — playlist history" . to_string ( ) ) ?;
@@ -178,6 +179,10 @@ fn play_file_session(
178179 status : "db migrations... teaching SQLite to keep a beat" . to_string ( ) ,
179180 logs : startup_logs ( ) ,
180181 frame_count : 0 ,
182+ progress : Some ( StartupProgressState {
183+ label : "opening local history" . to_string ( ) ,
184+ progress : None ,
185+ } ) ,
181186 sync_warning : None ,
182187 } ;
183188 title_state. set ( "looper — booting" . to_string ( ) ) ?;
@@ -187,12 +192,16 @@ fn play_file_session(
187192 startup. sync_warning = sync_warning. clone ( ) ;
188193
189194 loop {
190- startup. frame_count += 1 ;
191- startup. status = format ! ( "loading song... bribing the aux cord for `{current_url}`" ) ;
192- title_state. set ( "looper — loading song" . to_string ( ) ) ?;
193- terminal. draw ( |frame| draw_startup ( frame, & startup) ) ?;
195+ let resolved =
196+ match resolve_url_with_startup ( terminal, title_state, & mut startup, & current_url) ? {
197+ ResolveStartupOutcome :: Resolved ( resolved) => resolved,
198+ ResolveStartupOutcome :: Quit => {
199+ push_replica_best_effort ( & storage) ;
200+ return Ok ( ( ) ) ;
201+ }
202+ } ;
194203
195- let next = match plugin :: resolve_url ( & current_url ) ? {
204+ let next = match resolved {
196205 None => play_tracks (
197206 terminal,
198207 title_state,
@@ -239,6 +248,65 @@ fn play_file_session(
239248 }
240249}
241250
251+ enum ResolveStartupOutcome {
252+ Resolved ( Option < Vec < TrackInfo > > ) ,
253+ Quit ,
254+ }
255+
256+ fn resolve_url_with_startup (
257+ terminal : & mut ratatui:: Terminal < ratatui:: backend:: CrosstermBackend < std:: io:: Stdout > > ,
258+ title_state : & mut TitleState ,
259+ startup : & mut StartupScreenState ,
260+ current_url : & str ,
261+ ) -> Result < ResolveStartupOutcome > {
262+ let ( sender, receiver) = mpsc:: channel ( ) ;
263+ let url = current_url. to_string ( ) ;
264+ thread:: spawn ( move || {
265+ let result = plugin:: resolve_url ( & url) . map_err ( |err| err. to_string ( ) ) ;
266+ let _ = sender. send ( result) ;
267+ } ) ;
268+
269+ loop {
270+ startup. frame_count += 1 ;
271+ startup. status = format ! (
272+ "loading song... bribing the aux cord for `{}`" ,
273+ truncate_title( current_url, 72 )
274+ ) ;
275+ startup. logs = startup_logs ( ) ;
276+ startup. progress = Some ( StartupProgressState {
277+ label : "resolving source" . to_string ( ) ,
278+ progress : None ,
279+ } ) ;
280+ title_state. set ( "looper — loading song" . to_string ( ) ) ?;
281+ terminal. draw ( |frame| draw_startup ( frame, startup) ) ?;
282+
283+ match receiver. try_recv ( ) {
284+ Ok ( Ok ( resolved) ) => {
285+ startup. progress = None ;
286+ return Ok ( ResolveStartupOutcome :: Resolved ( resolved) ) ;
287+ }
288+ Ok ( Err ( message) ) => return Err ( color_eyre:: eyre:: eyre!( message) ) ,
289+ Err ( TryRecvError :: Empty ) => { }
290+ Err ( TryRecvError :: Disconnected ) => {
291+ return Err ( color_eyre:: eyre:: eyre!(
292+ "remote resolver stopped before returning a result"
293+ ) ) ;
294+ }
295+ }
296+
297+ if event:: poll ( Duration :: from_millis ( 30 ) ) ? {
298+ if let Event :: Key ( key) = event:: read ( ) ? {
299+ match ( key. code , key. modifiers ) {
300+ ( KeyCode :: Char ( 'q' ) , _) | ( KeyCode :: Char ( 'c' ) , KeyModifiers :: CONTROL ) => {
301+ return Ok ( ResolveStartupOutcome :: Quit ) ;
302+ }
303+ _ => { }
304+ }
305+ }
306+ }
307+ }
308+ }
309+
242310enum LoopAction {
243311 Quit ,
244312 NextTrack ,
@@ -1214,18 +1282,15 @@ fn render_track_startup(
12141282 sync_warning : Option < & SyncWarning > ,
12151283) -> Result < ( ) > {
12161284 title_state. set ( format_loading_title ( frame_count, & track. title ) ) ?;
1217- let status = startup_status (
1218- track,
1219- track_index,
1220- total_tracks,
1221- is_playlist,
1222- & phase,
1223- progress,
1224- ) ;
1285+ let status = startup_status ( track, track_index, total_tracks, is_playlist, & phase) ;
12251286 let startup = StartupScreenState {
12261287 status,
12271288 logs : track_startup_logs ( note) ,
12281289 frame_count,
1290+ progress : Some ( StartupProgressState {
1291+ label : loading_phase_label ( & phase) . to_string ( ) ,
1292+ progress,
1293+ } ) ,
12291294 sync_warning : sync_warning. cloned ( ) ,
12301295 } ;
12311296 terminal. draw ( |frame| draw_startup ( frame, & startup) ) ?;
@@ -1238,7 +1303,6 @@ fn startup_status(
12381303 total_tracks : usize ,
12391304 is_playlist : bool ,
12401305 phase : & LoadingPhase ,
1241- progress : Option < crate :: download:: DownloadProgress > ,
12421306) -> String {
12431307 let position = if is_playlist {
12441308 format ! ( "track {track_index}/{total_tracks}" )
@@ -1250,26 +1314,7 @@ fn startup_status(
12501314
12511315 match phase {
12521316 LoadingPhase :: Resolving => format ! ( "{position} • {service} • sizing up `{title}`" ) ,
1253- LoadingPhase :: Downloading => {
1254- let progress_label = progress
1255- . map ( |p| {
1256- let percent = p
1257- . fraction ( )
1258- . map ( |f| format ! ( "{}%" , ( f * 100.0 ) . round( ) as u64 ) )
1259- . unwrap_or_else ( || "warming up" . to_string ( ) ) ;
1260- let speed = p
1261- . speed_bytes_per_sec
1262- . map ( human_speed)
1263- . unwrap_or_else ( || "--" . to_string ( ) ) ;
1264- let eta = p
1265- . eta_seconds
1266- . map ( human_eta)
1267- . unwrap_or_else ( || "--:--" . to_string ( ) ) ;
1268- format ! ( "{percent} • {speed} • eta {eta}" )
1269- } )
1270- . unwrap_or_else ( || "warming up" . to_string ( ) ) ;
1271- format ! ( "{position} • {service} • downloading `{title}` • {progress_label}" )
1272- }
1317+ LoadingPhase :: Downloading => format ! ( "{position} • {service} • downloading `{title}`" ) ,
12731318 LoadingPhase :: Finalizing => format ! ( "{position} • {service} • polishing `{title}`" ) ,
12741319 LoadingPhase :: Ready => format ! ( "{position} • {service} • cueing `{title}`" ) ,
12751320 LoadingPhase :: Error ( message) => {
@@ -1278,6 +1323,16 @@ fn startup_status(
12781323 }
12791324}
12801325
1326+ fn loading_phase_label ( phase : & LoadingPhase ) -> & ' static str {
1327+ match phase {
1328+ LoadingPhase :: Resolving => "resolving source" ,
1329+ LoadingPhase :: Downloading => "downloading audio" ,
1330+ LoadingPhase :: Finalizing => "finalizing cache" ,
1331+ LoadingPhase :: Ready => "cueing playback" ,
1332+ LoadingPhase :: Error ( _) => "download failed" ,
1333+ }
1334+ }
1335+
12811336fn track_startup_logs ( note : String ) -> Vec < String > {
12821337 vec ! [
12831338 note,
@@ -1286,38 +1341,6 @@ fn track_startup_logs(note: String) -> Vec<String> {
12861341 ]
12871342}
12881343
1289- fn human_speed ( bytes_per_sec : u64 ) -> String {
1290- format ! ( "{}/s" , human_bytes( bytes_per_sec) )
1291- }
1292-
1293- fn human_bytes ( bytes : u64 ) -> String {
1294- const UNITS : [ & str ; 5 ] = [ "B" , "KB" , "MB" , "GB" , "TB" ] ;
1295- let mut value = bytes as f64 ;
1296- let mut unit = 0 ;
1297- while value >= 1024.0 && unit < UNITS . len ( ) - 1 {
1298- value /= 1024.0 ;
1299- unit += 1 ;
1300- }
1301-
1302- if unit == 0 {
1303- format ! ( "{bytes} {}" , UNITS [ unit] )
1304- } else {
1305- format ! ( "{value:.1} {}" , UNITS [ unit] )
1306- }
1307- }
1308-
1309- fn human_eta ( eta : u64 ) -> String {
1310- let minutes = eta / 60 ;
1311- let seconds = eta % 60 ;
1312- if minutes >= 60 {
1313- let hours = minutes / 60 ;
1314- let minutes = minutes % 60 ;
1315- format ! ( "{hours}:{minutes:02}:{seconds:02}" )
1316- } else {
1317- format ! ( "{minutes}:{seconds:02}" )
1318- }
1319- }
1320-
13211344fn format_playback_title ( frame_count : u64 , title : & str , paused : bool ) -> String {
13221345 let title = truncate_title ( title, 48 ) ;
13231346 if paused {
0 commit comments