@@ -497,6 +497,38 @@ impl StreamingSession {
497497 self . http_addr
498498 }
499499
500+ /// Prioritize downloading a specific file by making a range request
501+ /// This triggers librqbit to prioritize pieces for that file
502+ pub async fn prioritize_file ( & self , torrent_id : usize , file_idx : usize ) -> Result < ( ) , StreamError > {
503+ let url = format ! (
504+ "http://{}/torrents/{}/stream/{}" ,
505+ self . http_addr, torrent_id, file_idx
506+ ) ;
507+
508+ // Make a small range request to trigger prioritization
509+ let result = self
510+ . http_client
511+ . get ( & url)
512+ . header ( "Range" , "bytes=0-1024" )
513+ . send ( )
514+ . await ;
515+
516+ match result {
517+ Ok ( resp) if resp. status ( ) . is_success ( ) || resp. status ( ) . as_u16 ( ) == 206 => {
518+ info ! ( torrent_id, file_idx, "file prioritized for pre-download" ) ;
519+ Ok ( ( ) )
520+ }
521+ Ok ( resp) => {
522+ debug ! ( status = %resp. status( ) , "prioritize request returned non-success" ) ;
523+ Ok ( ( ) ) // Don't fail on this, it's best-effort
524+ }
525+ Err ( e) => {
526+ debug ! ( error = %e, "failed to prioritize file" ) ;
527+ Ok ( ( ) ) // Don't fail on this either
528+ }
529+ }
530+ }
531+
500532 /// Get download stats for a torrent
501533 pub async fn get_stats ( & self , torrent_id : usize ) -> Option < TorrentStats > {
502534 let url = format ! ( "http://{}/torrents/{}/stats/v1" , self . http_addr, torrent_id) ;
@@ -662,6 +694,41 @@ pub struct VideoFile {
662694 pub stream_url : String ,
663695}
664696
697+ impl VideoFile {
698+ /// Extract season and episode numbers from filename for sorting
699+ pub fn episode_sort_key ( & self ) -> ( u32 , u32 ) {
700+ use regex:: Regex ;
701+
702+ // S01E02 format
703+ let sxex_re = Regex :: new ( r"(?i)[Ss](\d{1,2})[Ee](\d{1,3})" ) . unwrap ( ) ;
704+ if let Some ( caps) = sxex_re. captures ( & self . name ) {
705+ if let ( Some ( s) , Some ( e) ) = ( caps. get ( 1 ) , caps. get ( 2 ) ) {
706+ if let ( Ok ( season) , Ok ( episode) ) = ( s. as_str ( ) . parse ( ) , e. as_str ( ) . parse ( ) ) {
707+ return ( season, episode) ;
708+ }
709+ }
710+ }
711+
712+ // 1x02 format
713+ let x_re = Regex :: new ( r"(?i)(\d{1,2})x(\d{1,3})" ) . unwrap ( ) ;
714+ if let Some ( caps) = x_re. captures ( & self . name ) {
715+ if let ( Some ( s) , Some ( e) ) = ( caps. get ( 1 ) , caps. get ( 2 ) ) {
716+ if let ( Ok ( season) , Ok ( episode) ) = ( s. as_str ( ) . parse ( ) , e. as_str ( ) . parse ( ) ) {
717+ return ( season, episode) ;
718+ }
719+ }
720+ }
721+
722+ // If no episode pattern found, use large values to sort at end
723+ ( u32:: MAX , u32:: MAX )
724+ }
725+ }
726+
727+ /// Sort video files by episode number (for season packs)
728+ pub fn sort_episodes ( files : & mut [ VideoFile ] ) {
729+ files. sort_by_key ( |f| f. episode_sort_key ( ) ) ;
730+ }
731+
665732#[ derive( Debug , Clone ) ]
666733pub struct TorrentInfo {
667734 pub id : usize ,
@@ -811,4 +878,72 @@ mod tests {
811878 assert ! ( !is_subtitle_file( "movie.txt" ) ) ;
812879 assert ! ( !is_subtitle_file( "movie.nfo" ) ) ;
813880 }
881+
882+ #[ test]
883+ fn test_episode_sort_key ( ) {
884+ let file1 = VideoFile {
885+ name : "Show.S01E01.720p.mkv" . to_string ( ) ,
886+ file_idx : 0 ,
887+ size : 1000 ,
888+ stream_url : String :: new ( ) ,
889+ } ;
890+ let file2 = VideoFile {
891+ name : "Show.S01E02.720p.mkv" . to_string ( ) ,
892+ file_idx : 1 ,
893+ size : 1000 ,
894+ stream_url : String :: new ( ) ,
895+ } ;
896+ let file10 = VideoFile {
897+ name : "Show.S01E10.720p.mkv" . to_string ( ) ,
898+ file_idx : 2 ,
899+ size : 1000 ,
900+ stream_url : String :: new ( ) ,
901+ } ;
902+ let file_s2 = VideoFile {
903+ name : "Show.S02E01.720p.mkv" . to_string ( ) ,
904+ file_idx : 3 ,
905+ size : 1000 ,
906+ stream_url : String :: new ( ) ,
907+ } ;
908+
909+ assert_eq ! ( file1. episode_sort_key( ) , ( 1 , 1 ) ) ;
910+ assert_eq ! ( file2. episode_sort_key( ) , ( 1 , 2 ) ) ;
911+ assert_eq ! ( file10. episode_sort_key( ) , ( 1 , 10 ) ) ;
912+ assert_eq ! ( file_s2. episode_sort_key( ) , ( 2 , 1 ) ) ;
913+
914+ // Verify sorting order
915+ assert ! ( file1. episode_sort_key( ) < file2. episode_sort_key( ) ) ;
916+ assert ! ( file2. episode_sort_key( ) < file10. episode_sort_key( ) ) ;
917+ assert ! ( file10. episode_sort_key( ) < file_s2. episode_sort_key( ) ) ;
918+ }
919+
920+ #[ test]
921+ fn test_sort_episodes ( ) {
922+ let mut files = vec ! [
923+ VideoFile {
924+ name: "Show.S01E03.mkv" . to_string( ) ,
925+ file_idx: 0 ,
926+ size: 1000 ,
927+ stream_url: String :: new( ) ,
928+ } ,
929+ VideoFile {
930+ name: "Show.S01E01.mkv" . to_string( ) ,
931+ file_idx: 1 ,
932+ size: 1000 ,
933+ stream_url: String :: new( ) ,
934+ } ,
935+ VideoFile {
936+ name: "Show.S01E02.mkv" . to_string( ) ,
937+ file_idx: 2 ,
938+ size: 1000 ,
939+ stream_url: String :: new( ) ,
940+ } ,
941+ ] ;
942+
943+ sort_episodes ( & mut files) ;
944+
945+ assert ! ( files[ 0 ] . name. contains( "E01" ) ) ;
946+ assert ! ( files[ 1 ] . name. contains( "E02" ) ) ;
947+ assert ! ( files[ 2 ] . name. contains( "E03" ) ) ;
948+ }
814949}
0 commit comments