@@ -4,6 +4,16 @@ pub mod trakt;
44pub use discord:: DiscordExtension ;
55pub use trakt:: TraktExtension ;
66
7+ use once_cell:: sync:: Lazy ;
8+ use regex:: Regex ;
9+
10+ // Compiled regexes for episode parsing (compiled once at startup)
11+ static SXEX_RE : Lazy < Regex > = Lazy :: new ( || Regex :: new ( r"(?i)[Ss](\d{1,2})[Ee](\d{1,3})" ) . unwrap ( ) ) ;
12+ static X_RE : Lazy < Regex > = Lazy :: new ( || Regex :: new ( r"(?i)(\d{1,2})x(\d{1,3})" ) . unwrap ( ) ) ;
13+ static FULL_RE : Lazy < Regex > =
14+ Lazy :: new ( || Regex :: new ( r"(?i)season[.\s]*(\d{1,2}).*episode[.\s]*(\d{1,3})" ) . unwrap ( ) ) ;
15+ static COMPACT_RE : Lazy < Regex > = Lazy :: new ( || Regex :: new ( r"[.\s](\d{1,2})(\d{2})[.\s]" ) . unwrap ( ) ) ;
16+
717/// Information about the currently playing media
818#[ derive( Debug , Clone ) ]
919pub struct MediaInfo {
@@ -32,29 +42,24 @@ pub struct MediaInfo {
3242/// - Season 1 Episode 2
3343/// - .102. (season 1, episode 02)
3444pub fn parse_episode_info ( filename : & str ) -> ( Option < u32 > , Option < u32 > ) {
35- use regex:: Regex ;
36-
3745 // S01E02, S1E2 format (most common)
38- let sxex_re = Regex :: new ( r"(?i)[Ss](\d{1,2})[Ee](\d{1,3})" ) . unwrap ( ) ;
39- if let Some ( caps) = sxex_re. captures ( filename)
46+ if let Some ( caps) = SXEX_RE . captures ( filename)
4047 && let ( Some ( s) , Some ( e) ) = ( caps. get ( 1 ) , caps. get ( 2 ) )
4148 && let ( Ok ( season) , Ok ( episode) ) = ( s. as_str ( ) . parse ( ) , e. as_str ( ) . parse ( ) )
4249 {
4350 return ( Some ( season) , Some ( episode) ) ;
4451 }
4552
4653 // 1x02, 01x02 format
47- let x_re = Regex :: new ( r"(?i)(\d{1,2})x(\d{1,3})" ) . unwrap ( ) ;
48- if let Some ( caps) = x_re. captures ( filename)
54+ if let Some ( caps) = X_RE . captures ( filename)
4955 && let ( Some ( s) , Some ( e) ) = ( caps. get ( 1 ) , caps. get ( 2 ) )
5056 && let ( Ok ( season) , Ok ( episode) ) = ( s. as_str ( ) . parse ( ) , e. as_str ( ) . parse ( ) )
5157 {
5258 return ( Some ( season) , Some ( episode) ) ;
5359 }
5460
5561 // Season 1 Episode 2 format (also handles dots instead of spaces)
56- let full_re = Regex :: new ( r"(?i)season[.\s]*(\d{1,2}).*episode[.\s]*(\d{1,3})" ) . unwrap ( ) ;
57- if let Some ( caps) = full_re. captures ( filename)
62+ if let Some ( caps) = FULL_RE . captures ( filename)
5863 && let ( Some ( s) , Some ( e) ) = ( caps. get ( 1 ) , caps. get ( 2 ) )
5964 && let ( Ok ( season) , Ok ( episode) ) = ( s. as_str ( ) . parse ( ) , e. as_str ( ) . parse ( ) )
6065 {
@@ -63,13 +68,17 @@ pub fn parse_episode_info(filename: &str) -> (Option<u32>, Option<u32>) {
6368
6469 // .102. or .1002. format (season 1, episode 02 or season 10, episode 02)
6570 // Must be surrounded by dots/spaces to avoid matching years
66- let compact_re = Regex :: new ( r"[.\s](\d)(\d{2})[.\s]" ) . unwrap ( ) ;
67- if let Some ( caps) = compact_re. captures ( filename)
71+ if let Some ( caps) = COMPACT_RE . captures ( filename)
6872 && let ( Some ( s) , Some ( e) ) = ( caps. get ( 1 ) , caps. get ( 2 ) )
6973 && let ( Ok ( season) , Ok ( episode) ) = ( s. as_str ( ) . parse :: < u32 > ( ) , e. as_str ( ) . parse :: < u32 > ( ) )
7074 {
71- // Only valid if episode isn't too high (avoid matching years like 1999)
72- if ( 1 ..=99 ) . contains ( & season) && ( 1 ..=99 ) . contains ( & episode) {
75+ // Reconstruct the full matched number to check if it's a year
76+ let full_number = season * 100 + episode;
77+ // Only valid if episode isn't too high and the full number isn't a year (1900-2099)
78+ if ( 1 ..=99 ) . contains ( & season)
79+ && ( 1 ..=99 ) . contains ( & episode)
80+ && !( 1900 ..=2099 ) . contains ( & full_number)
81+ {
7382 return ( Some ( season) , Some ( episode) ) ;
7483 }
7584 }
@@ -214,4 +223,38 @@ mod tests {
214223 assert_eq ! ( parse_episode_info( "show.S01e02.mkv" ) , ( Some ( 1 ) , Some ( 2 ) ) ) ;
215224 assert_eq ! ( parse_episode_info( "show.s01E02.mkv" ) , ( Some ( 1 ) , Some ( 2 ) ) ) ;
216225 }
226+
227+ #[ test]
228+ fn test_compact_format_season_10_episode_02 ( ) {
229+ // The comment in parse_episode_info states this format is supported
230+ let result = parse_episode_info ( "Show.Name.1002.mkv" ) ;
231+
232+ assert_eq ! (
233+ result,
234+ ( Some ( 10 ) , Some ( 2 ) ) ,
235+ "According to the comment, .1002. should parse as season 10, episode 2"
236+ ) ;
237+ }
238+
239+ #[ test]
240+ fn test_compact_format_single_digit_season ( ) {
241+ // Verify single-digit seasons still work
242+ assert_eq ! ( parse_episode_info( "Show.Name.102.mkv" ) , ( Some ( 1 ) , Some ( 2 ) ) ) ;
243+ assert_eq ! ( parse_episode_info( "Show.Name.315.mkv" ) , ( Some ( 3 ) , Some ( 15 ) ) ) ;
244+ }
245+
246+ #[ test]
247+ fn test_compact_format_rejects_years ( ) {
248+ // Should not match years that look like they could be season/episode
249+ assert_eq ! (
250+ parse_episode_info( "Movie.1999.BluRay.mkv" ) ,
251+ ( None , None ) ,
252+ "1999 should be treated as a year, not season 19 episode 99"
253+ ) ;
254+ assert_eq ! (
255+ parse_episode_info( "Movie.2020.1080p.mkv" ) ,
256+ ( None , None ) ,
257+ "2020 should be treated as a year, not season 20 episode 20"
258+ ) ;
259+ }
217260}
0 commit comments