@@ -12,6 +12,7 @@ impl App {
1212 subtitle_url : Option < String > ,
1313 link : Option < String > ,
1414 headers : Vec < ( String , String ) > ,
15+ max_height : Option < u64 > ,
1516 ) {
1617 if self . state . download_progress . is_some ( ) || self . state . active_screen != Screen :: Details {
1718 return ;
@@ -265,8 +266,9 @@ impl App {
265266 cmd. arg ( "--add-header" ) . arg ( format ! ( "{clean_k}: {clean_v}" ) ) ;
266267 }
267268 }
269+ let format_spec = ytdlp_format_selector ( max_height) ;
268270 cmd. arg ( "-f" )
269- . arg ( "bestvideo+bestaudio/best" )
271+ . arg ( format_spec )
270272 . arg ( "--newline" )
271273 . arg ( "--part" )
272274 . arg ( "-o" )
@@ -537,11 +539,13 @@ impl App {
537539 || release. provider == ProviderKind :: BdixDhakaFlix
538540 {
539541 let sender_clone = self . action_sender . clone ( ) ;
542+ let max_height = Some ( release. resolution_u64 ( ) ) . filter ( |& h| h > 0 ) ;
540543 sender_clone
541544 . send ( Action :: StartDownload (
542545 subtitle_url,
543546 Some ( first_mirror. resolver_url . clone ( ) ) ,
544547 first_mirror. headers . clone ( ) ,
548+ max_height,
545549 ) )
546550 . ok ( ) ;
547551 return None ;
@@ -571,11 +575,14 @@ impl App {
571575 . await ;
572576 match result {
573577 Ok ( Ok ( source) ) => {
578+ let max_height =
579+ Some ( release. resolution_u64 ( ) ) . filter ( |& h| h > 0 ) ;
574580 sender
575581 . send ( Action :: StartDownload (
576582 subtitle_url,
577583 Some ( source. url ) ,
578584 source. headers ,
585+ max_height,
579586 ) )
580587 . ok ( ) ;
581588 }
@@ -600,7 +607,7 @@ impl App {
600607 } ) ;
601608 } else {
602609 self . action_sender
603- . send ( Action :: StartDownload ( subtitle_url, None , Vec :: new ( ) ) )
610+ . send ( Action :: StartDownload ( subtitle_url, None , Vec :: new ( ) , None ) )
604611 . ok ( ) ;
605612 }
606613 } else {
@@ -611,15 +618,24 @@ impl App {
611618 . and_then ( |r| r. mirrors . first ( ) )
612619 . map ( |m| m. headers . clone ( ) )
613620 . unwrap_or_default ( ) ;
621+ let max_height = release
622+ . as_ref ( )
623+ . map ( |r| r. resolution_u64 ( ) )
624+ . filter ( |& h| h > 0 ) ;
614625 self . action_sender
615- . send ( Action :: StartDownload ( subtitle_url, link, headers) )
626+ . send ( Action :: StartDownload (
627+ subtitle_url,
628+ link,
629+ headers,
630+ max_height,
631+ ) )
616632 . ok ( ) ;
617633 }
618634 return None ;
619635 }
620- Action :: StartDownload ( subtitle_url, link, headers) => {
636+ Action :: StartDownload ( subtitle_url, link, headers, max_height ) => {
621637 self . state . is_resolving_playback = false ;
622- self . start_resilient_download ( subtitle_url, link, headers) ;
638+ self . start_resilient_download ( subtitle_url, link, headers, max_height ) ;
623639 return None ;
624640 }
625641 Action :: DownloadEpisode => {
@@ -793,11 +809,8 @@ impl App {
793809 format ! ( "{completed}/{total} files finished before error: {error}" ) ,
794810 ) ;
795811 } else {
796- self . state . notify (
797- NotificationKind :: Error ,
798- "Download failed" ,
799- format ! ( "Partial file preserved. {error}" ) ,
800- ) ;
812+ self . state
813+ . notify ( NotificationKind :: Error , "Download failed" , error) ;
801814 }
802815 self . state . download_queue . clear ( ) ;
803816 self . state . download_queue_total = 0 ;
@@ -998,15 +1011,25 @@ async fn prepare_target_dir(
9981011}
9991012pub ( crate ) fn yt_dlp_missing_guidance ( ) -> String {
10001013 if crate :: updater:: artifact:: is_termux_environment ( ) {
1001- "MovieBox DASH streams require yt-dlp. Please install yt-dlp and ffmpeg on your device (e.g. ' pkg install yt-dlp ffmpeg') to download these streams. " . to_string ( )
1014+ "DASH streams require yt-dlp & ffmpeg. \n Run: pkg install yt-dlp ffmpeg" . to_string ( )
10021015 } else if cfg ! ( target_os = "macos" ) {
1003- "MovieBox DASH streams require yt-dlp. Please install yt-dlp and ffmpeg on your Mac (e.g. ' brew install yt-dlp ffmpeg') to download these streams. " . to_string ( )
1016+ "DASH streams require yt-dlp & ffmpeg. \n Run: brew install yt-dlp ffmpeg" . to_string ( )
10041017 } else if cfg ! ( target_os = "windows" ) {
1005- "MovieBox DASH streams require yt-dlp. Please install yt-dlp and ffmpeg on your system (e.g. 'winget install yt-dlp.yt-dlp Gyan.FFmpeg') to download these streams." . to_string ( )
1018+ "DASH streams require yt-dlp & ffmpeg.\n Run: winget install yt-dlp.yt-dlp Gyan.FFmpeg"
1019+ . to_string ( )
10061020 } else if cfg ! ( target_os = "linux" ) {
1007- "MovieBox DASH streams require yt-dlp. Please install yt-dlp and ffmpeg via your system package manager to download these streams." . to_string ( )
1021+ "DASH streams require yt-dlp & ffmpeg.\n Install via system package manager" . to_string ( )
1022+ } else {
1023+ "DASH streams require yt-dlp & ffmpeg." . to_string ( )
1024+ }
1025+ }
1026+ pub ( crate ) fn ytdlp_format_selector ( max_height : Option < u64 > ) -> String {
1027+ if let Some ( height) = max_height. filter ( |& h| h > 0 ) {
1028+ format ! (
1029+ "bestvideo[height<={height}]+bestaudio/best[height<={height}]/bestvideo+bestaudio/best"
1030+ )
10081031 } else {
1009- "MovieBox DASH streams require yt-dlp. Please install yt-dlp and ffmpeg on your system to download these streams. " . to_string ( )
1032+ "bestvideo+bestaudio/best " . to_string ( )
10101033 }
10111034}
10121035
@@ -1093,6 +1116,7 @@ pub(crate) fn parse_ytdlp_progress(line: &str) -> Option<(f64, String)> {
10931116
10941117#[ cfg( test) ]
10951118mod tests {
1119+ use super :: * ;
10961120 use crate :: providers:: models:: { ProviderKind , Release , SourceMirror } ;
10971121 use crate :: tui:: action:: Action ;
10981122 use crate :: tui:: app:: App ;
@@ -1195,8 +1219,11 @@ mod tests {
11951219 #[ test]
11961220 fn test_yt_dlp_missing_guidance_contains_platform_hint ( ) {
11971221 let guidance = super :: yt_dlp_missing_guidance ( ) ;
1198- assert ! ( guidance. contains( "MovieBox DASH streams require yt-dlp" ) ) ;
1199- assert ! ( guidance. contains( "install yt-dlp and ffmpeg" ) ) ;
1222+ assert ! ( guidance. contains( "DASH streams require yt-dlp & ffmpeg" ) ) ;
1223+ #[ cfg( target_os = "macos" ) ]
1224+ assert ! ( guidance. contains( "brew install yt-dlp ffmpeg" ) ) ;
1225+ #[ cfg( target_os = "windows" ) ]
1226+ assert ! ( guidance. contains( "winget install yt-dlp.yt-dlp Gyan.FFmpeg" ) ) ;
12001227 }
12011228
12021229 #[ tokio:: test]
@@ -1230,7 +1257,8 @@ mod tests {
12301257
12311258 let dispatched = app. action_receiver . try_recv ( ) . expect ( "action dispatched" ) ;
12321259 match dispatched {
1233- Action :: StartDownload ( _, link, headers) => {
1260+ Action :: StartDownload ( _, link, headers, max_height) => {
1261+ assert_eq ! ( max_height, Some ( 1080 ) ) ;
12341262 assert_eq ! (
12351263 link. as_deref( ) ,
12361264 Some ( "https://example.com/dash/123/index.mpd" )
@@ -1244,6 +1272,129 @@ mod tests {
12441272 other => panic ! ( "expected StartDownload, got {:?}" , other) ,
12451273 }
12461274 }
1275+ #[ tokio:: test]
1276+ async fn test_download_stream_respects_highlighted_resolution ( ) {
1277+ let mut app = App :: new ( ) ;
1278+ app. state . active_provider = ProviderKind :: MovieBox ;
1279+ app. state . active_screen = Screen :: Details ;
1280+ app. state . selected_resources = vec ! [
1281+ Release {
1282+ provider: ProviderKind :: MovieBox ,
1283+ filename: "Ek Deewane Ki Deewaniyat 1080p HEVC" . to_string( ) ,
1284+ quality: Some ( "1080p" . to_string( ) ) ,
1285+ codec: Some ( "hevc" . to_string( ) ) ,
1286+ language: None ,
1287+ size_bytes: Some ( 1_600_000_000 ) ,
1288+ season: None ,
1289+ episode: None ,
1290+ mirrors: vec![ SourceMirror {
1291+ label: "1080p HEVC" . to_string( ) ,
1292+ resolver_url: "https://example.com/dash/123/index.mpd" . to_string( ) ,
1293+ headers: vec![ ] ,
1294+ direct_file: true ,
1295+ } ] ,
1296+ resource_id: Some ( "123" . to_string( ) ) ,
1297+ } ,
1298+ Release {
1299+ provider: ProviderKind :: MovieBox ,
1300+ filename: "Ek Deewane Ki Deewaniyat 720p HEVC" . to_string( ) ,
1301+ quality: Some ( "720p" . to_string( ) ) ,
1302+ codec: Some ( "hevc" . to_string( ) ) ,
1303+ language: None ,
1304+ size_bytes: Some ( 839_000_000 ) ,
1305+ season: None ,
1306+ episode: None ,
1307+ mirrors: vec![ SourceMirror {
1308+ label: "720p HEVC" . to_string( ) ,
1309+ resolver_url: "https://example.com/dash/123/index.mpd" . to_string( ) ,
1310+ headers: vec![ ] ,
1311+ direct_file: true ,
1312+ } ] ,
1313+ resource_id: Some ( "123" . to_string( ) ) ,
1314+ } ,
1315+ Release {
1316+ provider: ProviderKind :: MovieBox ,
1317+ filename: "Ek Deewane Ki Deewaniyat 480p HEVC" . to_string( ) ,
1318+ quality: Some ( "480p" . to_string( ) ) ,
1319+ codec: Some ( "hevc" . to_string( ) ) ,
1320+ language: None ,
1321+ size_bytes: Some ( 438_000_000 ) ,
1322+ season: None ,
1323+ episode: None ,
1324+ mirrors: vec![ SourceMirror {
1325+ label: "480p HEVC" . to_string( ) ,
1326+ resolver_url: "https://example.com/dash/123/index.mpd" . to_string( ) ,
1327+ headers: vec![ ] ,
1328+ direct_file: true ,
1329+ } ] ,
1330+ resource_id: Some ( "123" . to_string( ) ) ,
1331+ } ,
1332+ ] ;
1333+
1334+ app. state . resource_list_state . select ( Some ( 2 ) ) ;
1335+ assert_eq ! (
1336+ app. get_selected_release( ) . unwrap( ) . quality. as_deref( ) ,
1337+ Some ( "480p" )
1338+ ) ;
1339+
1340+ app. handle_download ( Action :: DownloadStream ( None ) ) . await ;
1341+
1342+ let dispatched = app. action_receiver . try_recv ( ) . expect ( "action dispatched" ) ;
1343+ match dispatched {
1344+ Action :: StartDownload ( _, link, _, max_height) => {
1345+ assert_eq ! (
1346+ link. as_deref( ) ,
1347+ Some ( "https://example.com/dash/123/index.mpd" )
1348+ ) ;
1349+ assert_eq ! ( max_height, Some ( 480 ) ) ;
1350+ let format_spec = ytdlp_format_selector ( max_height) ;
1351+ assert_eq ! (
1352+ format_spec,
1353+ "bestvideo[height<=480]+bestaudio/best[height<=480]/bestvideo+bestaudio/best"
1354+ ) ;
1355+ }
1356+ other => panic ! ( "expected StartDownload, got {:?}" , other) ,
1357+ }
1358+ app. state . is_resolving_playback = false ;
1359+ app. state . resource_list_state . select ( Some ( 1 ) ) ;
1360+ assert_eq ! (
1361+ app. get_selected_release( ) . unwrap( ) . quality. as_deref( ) ,
1362+ Some ( "720p" )
1363+ ) ;
1364+
1365+ app. handle_download ( Action :: DownloadStream ( None ) ) . await ;
1366+
1367+ let dispatched = app. action_receiver . try_recv ( ) . expect ( "action dispatched" ) ;
1368+ match dispatched {
1369+ Action :: StartDownload ( _, _, _, max_height) => {
1370+ assert_eq ! ( max_height, Some ( 720 ) ) ;
1371+ let format_spec = ytdlp_format_selector ( max_height) ;
1372+ assert_eq ! (
1373+ format_spec,
1374+ "bestvideo[height<=720]+bestaudio/best[height<=720]/bestvideo+bestaudio/best"
1375+ ) ;
1376+ }
1377+ other => panic ! ( "expected StartDownload, got {:?}" , other) ,
1378+ }
1379+ }
1380+
1381+ #[ test]
1382+ fn test_ytdlp_format_selector_specs ( ) {
1383+ assert_eq ! (
1384+ ytdlp_format_selector( Some ( 480 ) ) ,
1385+ "bestvideo[height<=480]+bestaudio/best[height<=480]/bestvideo+bestaudio/best"
1386+ ) ;
1387+ assert_eq ! (
1388+ ytdlp_format_selector( Some ( 720 ) ) ,
1389+ "bestvideo[height<=720]+bestaudio/best[height<=720]/bestvideo+bestaudio/best"
1390+ ) ;
1391+ assert_eq ! (
1392+ ytdlp_format_selector( Some ( 1080 ) ) ,
1393+ "bestvideo[height<=1080]+bestaudio/best[height<=1080]/bestvideo+bestaudio/best"
1394+ ) ;
1395+ assert_eq ! ( ytdlp_format_selector( None ) , "bestvideo+bestaudio/best" ) ;
1396+ assert_eq ! ( ytdlp_format_selector( Some ( 0 ) ) , "bestvideo+bestaudio/best" ) ;
1397+ }
12471398 #[ test]
12481399 fn test_download_directory_and_filename_conventions ( ) {
12491400 let base_dir = std:: path:: PathBuf :: from ( "/tmp/MovieBox-TUI" ) ;
0 commit comments