@@ -62,6 +62,7 @@ pub struct UploadCounters {
6262
6363const SPEED_SAMPLE_WINDOW : usize = 20 ;
6464const STATUS_RESOURCE_REFRESH_INTERVAL : std:: time:: Duration = std:: time:: Duration :: from_secs ( 2 ) ;
65+ const MIN_DOWNLOAD_CHUNK_BYTES : usize = 64 * 1024 ;
6566
6667#[ derive( Debug ) ]
6768pub struct RuntimeMetrics {
@@ -1220,31 +1221,38 @@ async fn download_and_send_music(
12201221 return Err ( anyhow:: anyhow!( "HTTP {}" , response. status( ) ) ) ;
12211222 }
12221223
1223- // Check content length
1224- let content_length = response. content_length ( ) . unwrap_or ( 0 ) ;
1225- if content_length == 0 {
1224+ // Check content length. None means unknown/chunked transfer and is allowed.
1225+ let content_length = response. content_length ( ) ;
1226+ if content_length == Some ( 0 ) {
12261227 return Err ( anyhow:: anyhow!( "Empty file or unable to get file size" ) ) ;
12271228 }
12281229
12291230 // Create audio buffer based on storage mode configuration
12301231 let mut audio_buffer = AudioBuffer :: new (
12311232 & state. config ,
1232- content_length,
1233+ content_length. unwrap_or ( 0 ) ,
12331234 filename. clone ( ) ,
12341235 & state. config . cache_dir ,
12351236 )
12361237 . await ?;
12371238
12381239 let mut stream = response. bytes_stream ( ) ;
12391240 let downloaded = if audio_buffer. is_disk ( ) {
1241+ let chunk_bytes = download_chunk_bytes ( & state. config ) ;
12401242 let stream = stream. map_err ( std:: io:: Error :: other) ;
1241- let mut reader = StreamReader :: new ( stream) ;
1243+ let mut reader =
1244+ tokio:: io:: BufReader :: with_capacity ( chunk_bytes, StreamReader :: new ( stream) ) ;
12421245 let file = audio_buffer
12431246 . disk_file_mut ( )
12441247 . ok_or_else ( || anyhow:: anyhow!( "Disk buffer missing file handle" ) ) ?;
1245- tokio:: io:: copy ( & mut reader, file)
1248+ let mut writer = tokio:: io:: BufWriter :: with_capacity ( chunk_bytes, file) ;
1249+ let downloaded = tokio:: io:: copy_buf ( & mut reader, & mut writer)
12461250 . await
1247- . context ( "Failed to stream download to disk" ) ?
1251+ . context ( "Failed to stream download to disk" ) ?;
1252+ tokio:: io:: AsyncWriteExt :: flush ( & mut writer)
1253+ . await
1254+ . context ( "Failed to flush disk writer" ) ?;
1255+ downloaded
12481256 } else {
12491257 let mut downloaded = 0u64 ;
12501258 while let Some ( chunk) = stream. next ( ) . await {
@@ -1928,6 +1936,13 @@ fn should_set_upload_pool_idle_timeout(secs: u64) -> bool {
19281936 secs > 0
19291937}
19301938
1939+ fn download_chunk_bytes ( config : & Config ) -> usize {
1940+ config
1941+ . download_chunk_size_kb
1942+ . saturating_mul ( 1024 )
1943+ . max ( MIN_DOWNLOAD_CHUNK_BYTES )
1944+ }
1945+
19311946fn append_search_result_line ( results : & mut String , index : usize , song_name : & str , artists : & str ) {
19321947 use std:: fmt:: Write ;
19331948
@@ -2854,6 +2869,25 @@ mod tests {
28542869 assert ! ( super :: should_set_upload_pool_idle_timeout( 60 ) ) ;
28552870 }
28562871
2872+ #[ test]
2873+ fn download_chunk_bytes_uses_configured_kib ( ) {
2874+ let mut config = Config :: default ( ) ;
2875+ config. download_chunk_size_kb = 512 ;
2876+
2877+ assert_eq ! ( super :: download_chunk_bytes( & config) , 512 * 1024 ) ;
2878+ }
2879+
2880+ #[ test]
2881+ fn download_chunk_bytes_clamps_zero_to_minimum ( ) {
2882+ let mut config = Config :: default ( ) ;
2883+ config. download_chunk_size_kb = 0 ;
2884+
2885+ assert_eq ! (
2886+ super :: download_chunk_bytes( & config) ,
2887+ super :: MIN_DOWNLOAD_CHUNK_BYTES
2888+ ) ;
2889+ }
2890+
28572891 #[ test]
28582892 fn upload_log_level_allows_thresholds ( ) {
28592893 assert ! ( UploadLogLevel :: Info . allows( UploadLogLevel :: Error ) ) ;
0 commit comments