use lazy_regex::regex;
pub(crate) fn extract_filename_from_subject(subject: &str) -> Option<&str> {
if let Some(captured) = regex!(r#""(.*)""#).captures(subject) {
let trimmed = captured.get(1).map(|m| m.as_str().trim());
if let Some(s) = trimmed
&& !s.is_empty()
{
return Some(s);
}
}
if let Some(captured) =
regex!(r"^(?:\[|\()(?:\d+/\d+)(?:\]|\))\s-\s(.*)\syEnc\s(?:\[|\()(?:\d+/\d+)(?:\]|\))\s\d+").captures(subject)
{
let trimmed = captured.get(1).map(|m| m.as_str().trim());
if let Some(s) = trimmed
&& !s.is_empty()
{
return Some(s);
}
}
if let Some(captured) =
regex!(r"\b([\w\-+()' .,]+(?:\[[\w\-/+()' .,]*][\w\-+()' .,]*)*\.[A-Za-z0-9]{2,4})\b").captures(subject)
{
let trimmed = captured.get(1).map(|m| m.as_str().trim());
if let Some(s) = trimmed
&& !s.is_empty()
{
return Some(s);
}
}
None
}
pub(crate) fn split_filename_at_extension(filename: &str) -> (&str, Option<&str>) {
let re = regex!(r"(\.[a-z]\w{2,5})$"i);
if let Some(found) = re.find(filename) {
let start = found.start();
let extension = &filename[start + 1..];
let stem = &filename[..start];
(stem, Some(extension))
} else {
(filename, None)
}
}
pub(crate) fn sort_key_from_subject(subject: &str) -> String {
let pattern = regex!(r"^\[(\d+)\/(\d+)\]");
if let Some((_, [current, total])) = pattern.captures(subject).map(|caps| caps.extract::<2>()) {
let width = total.len();
let current = format!("{current:0>width$}");
pattern.replace(subject, format!("[{current}/{total}]")).to_string()
} else {
subject.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::seq::SliceRandom;
use rstest::rstest;
#[rstest]
#[case(
"[011/116] - [AC-FFF] Highschool DxD BorN - 02 [BD][1080p-Hi10p] FLAC][Dual-Audio][442E5446].mkv yEnc (1/2401) 1720916370",
"[AC-FFF] Highschool DxD BorN - 02 [BD][1080p-Hi10p] FLAC][Dual-Audio][442E5446].mkv",
"[AC-FFF] Highschool DxD BorN - 02 [BD][1080p-Hi10p] FLAC][Dual-Audio][442E5446]",
Some("mkv")
)]
#[case(
"[010/108] - [SubsPlease] Ijiranaide, Nagatoro-san - 02 (1080p) [6E8E8065].mkv yEnc (1/2014) 1443366873",
"[SubsPlease] Ijiranaide, Nagatoro-san - 02 (1080p) [6E8E8065].mkv",
"[SubsPlease] Ijiranaide, Nagatoro-san - 02 (1080p) [6E8E8065]",
Some("mkv")
)]
#[case(
r#"[1/8] - "TenPuru - No One Can Live on Loneliness v05 {+ "Book of Earthly Desires" pamphlet} (2021) (Digital) (KG Manga).cbz" yEnc (1/230) 164676947"#,
r#"TenPuru - No One Can Live on Loneliness v05 {+ "Book of Earthly Desires" pamphlet} (2021) (Digital) (KG Manga).cbz"#,
r#"TenPuru - No One Can Live on Loneliness v05 {+ "Book of Earthly Desires" pamphlet} (2021) (Digital) (KG Manga)"#,
Some("cbz"),
)]
#[case(
r#"[1/10] - "ONE.PIECE.S01E1109.1080p.NF.WEB-DL.AAC2.0.H.264-VARYG" yEnc (1/1277) 915318101"#,
"ONE.PIECE.S01E1109.1080p.NF.WEB-DL.AAC2.0.H.264-VARYG",
"ONE.PIECE.S01E1109.1080p.NF.WEB-DL.AAC2.0.H.264-VARYG",
None
)]
#[case(
r#"[1/10] - "ONE.PIECE.S01E1109.1080p.NF.WEB-DL.AAC2.0.H.264-VARYG.mkv" yEnc (1/1277) 915318101"#,
"ONE.PIECE.S01E1109.1080p.NF.WEB-DL.AAC2.0.H.264-VARYG.mkv",
"ONE.PIECE.S01E1109.1080p.NF.WEB-DL.AAC2.0.H.264-VARYG",
Some("mkv")
)]
#[case(r#"[27/141] - "index.bdmv" yEnc (1/1) 280"#, "index.bdmv", "index", Some("bdmv"))]
fn test_name_stem_extension_extraction(
#[case] subject: &str,
#[case] filename: &str,
#[case] stem: &str,
#[case] extension: Option<&str>,
) {
assert_eq!(extract_filename_from_subject(subject), Some(filename));
assert_eq!(split_filename_at_extension(filename), (stem, extension));
}
#[test]
fn test_sort_key_from_subject() {
assert_eq!(
sort_key_from_subject(r#"[10/141] - "00010.clpi" yEnc (1/1) 1000"#),
r#"[010/141] - "00010.clpi" yEnc (1/1) 1000"#
);
assert_eq!(
sort_key_from_subject(r#""00010.clpi" yEnc (1/1) 1000"#),
r#""00010.clpi" yEnc (1/1) 1000"#
);
let control = vec![
r#"[1/141] - "00001.clpi" yEnc (1/1) 24248"#,
r#"[2/141] - "00002.clpi" yEnc (1/1) 860"#,
r#"[3/141] - "00003.clpi" yEnc (1/1) 480"#,
r#"[4/141] - "00004.clpi" yEnc (1/1) 1136"#,
r#"[5/141] - "00005.clpi" yEnc (1/1) 480"#,
r#"[6/141] - "00006.clpi" yEnc (1/1) 480"#,
r#"[7/141] - "00007.clpi" yEnc (1/1) 712"#,
r#"[8/141] - "00008.clpi" yEnc (1/1) 392"#,
r#"[9/141] - "00009.clpi" yEnc (1/1) 1016"#,
r#"[10/141] - "00010.clpi" yEnc (1/1) 1000"#,
r#"[11/141] - "00011.clpi" yEnc (1/1) 296"#,
r#"[12/141] - "00012.clpi" yEnc (1/1) 296"#,
r#"[13/141] - "00013.clpi" yEnc (1/1) 332"#,
r#"[14/141] - "00014.clpi" yEnc (1/1) 332"#,
r#"[15/141] - "MovieObject.bdmv" yEnc (1/1) 39430"#,
r#"[16/141] - "00001.mpls" yEnc (1/1) 708"#,
r#"[17/141] - "00002.mpls" yEnc (1/1) 188"#,
r#"[18/141] - "00003.mpls" yEnc (1/1) 188"#,
r#"[19/141] - "00004.mpls" yEnc (1/1) 188"#,
r#"[20/141] - "00005.mpls" yEnc (1/1) 188"#,
r#"[21/141] - "00006.mpls" yEnc (1/1) 188"#,
r#"[22/141] - "00007.mpls" yEnc (1/1) 188"#,
r#"[23/141] - "00008.mpls" yEnc (1/1) 174"#,
r#"[24/141] - "00009.mpls" yEnc (1/1) 13046"#,
r#"[25/141] - "00010.mpls" yEnc (1/1) 158"#,
r#"[26/141] - "00011.mpls" yEnc (1/1) 158"#,
r#"[27/141] - "index.bdmv" yEnc (1/1) 280"#,
r#"[28/141] - "00001.clpi" yEnc (1/1) 24248"#,
r#"[29/141] - "00002.clpi" yEnc (1/1) 860"#,
r#"[30/141] - "00003.clpi" yEnc (1/1) 480"#,
r#"[31/141] - "00004.clpi" yEnc (1/1) 1136"#,
r#"[32/141] - "00005.clpi" yEnc (1/1) 480"#,
r#"[33/141] - "00006.clpi" yEnc (1/1) 480"#,
r#"[34/141] - "00007.clpi" yEnc (1/1) 712"#,
r#"[35/141] - "00008.clpi" yEnc (1/1) 392"#,
r#"[36/141] - "00009.clpi" yEnc (1/1) 1016"#,
r#"[37/141] - "00010.clpi" yEnc (1/1) 1000"#,
r#"[38/141] - "00011.clpi" yEnc (1/1) 296"#,
r#"[39/141] - "00012.clpi" yEnc (1/1) 296"#,
r#"[40/141] - "00013.clpi" yEnc (1/1) 332"#,
r#"[41/141] - "00014.clpi" yEnc (1/1) 332"#,
r#"[42/141] - "bdmt_jpn.xml" yEnc (1/1) 446"#,
r#"[43/141] - "fafner_BTL_L_xmb.jpg" yEnc (1/1) 267785"#,
r#"[44/141] - "fafner_BTL_S_xmb.jpg" yEnc (1/1) 137371"#,
r#"[45/141] - "MovieObject.bdmv" yEnc (1/1) 39430"#,
r#"[46/141] - "00001.mpls" yEnc (1/1) 708"#,
r#"[47/141] - "00002.mpls" yEnc (1/1) 188"#,
r#"[48/141] - "00003.mpls" yEnc (1/1) 188"#,
r#"[49/141] - "00004.mpls" yEnc (1/1) 188"#,
r#"[50/141] - "00005.mpls" yEnc (1/1) 188"#,
r#"[51/141] - "00006.mpls" yEnc (1/1) 188"#,
r#"[52/141] - "00007.mpls" yEnc (1/1) 188"#,
r#"[53/141] - "00008.mpls" yEnc (1/1) 174"#,
r#"[54/141] - "00009.mpls" yEnc (1/1) 13046"#,
r#"[55/141] - "00010.mpls" yEnc (1/1) 158"#,
r#"[56/141] - "00011.mpls" yEnc (1/1) 158"#,
r#"[57/141] - "00001.m2ts" yEnc (1/23594) 16911587328"#,
r#"[58/141] - "00002.m2ts" yEnc (1/378) 270790656"#,
r#"[59/141] - "00003.m2ts" yEnc (1/99) 70281216"#,
r#"[60/141] - "00004.m2ts" yEnc (1/649) 464873472"#,
r#"[61/141] - "00005.m2ts" yEnc (1/101) 71995392"#,
r#"[62/141] - "00006.m2ts" yEnc (1/101) 71995392"#,
r#"[63/141] - "00007.m2ts" yEnc (1/240) 171515904"#,
r#"[64/141] - "00008.m2ts" yEnc (1/11) 7606272"#,
r#"[65/141] - "00009.m2ts" yEnc (1/308) 220569600"#,
r#"[66/141] - "00010.m2ts" yEnc (1/281) 201308160"#,
r#"[67/141] - "00011.m2ts" yEnc (1/13) 8742912"#,
r#"[68/141] - "00012.m2ts" yEnc (1/3) 2033664"#,
r#"[69/141] - "00013.m2ts" yEnc (1/2) 1062912"#,
r#"[70/141] - "00014.m2ts" yEnc (1/1) 110592"#,
r#"[71/141] - "index.bdmv" yEnc (1/1) 280"#,
];
let mut randomized = control.clone();
let mut rng = rand::rng();
randomized.shuffle(&mut rng);
let mut sorted = randomized.clone();
sorted.sort_by_key(|s| sort_key_from_subject(s));
assert_ne!(randomized, control);
assert_eq!(sorted, control);
}
}