use regex::Regex;
use std::sync::LazyLock;
static RAR_EXT_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\.r(\d+)$").expect("rar ext regex"));
static MULTIPART_MKV_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\.\d{3}$").expect("multipart mkv regex"));
pub fn is_rar_file(filename: &str) -> bool {
filename.to_ascii_lowercase().ends_with(".rar") || RAR_EXT_RE.is_match(filename)
}
pub fn is_7z_file(filename: &str) -> bool {
let lower = filename.to_ascii_lowercase();
lower.ends_with(".7z") || lower.contains(".7z.")
}
pub fn is_multipart_mkv(filename: &str) -> bool {
let lower = filename.to_ascii_lowercase();
(lower.contains(".mkv.") || lower.contains(".avi.") || lower.contains(".mp4."))
&& MULTIPART_MKV_RE.is_match(&lower)
}
pub fn is_video_file(filename: &str) -> bool {
let lower = filename.to_ascii_lowercase();
matches!(
std::path::Path::new(&lower)
.extension()
.and_then(|e| e.to_str()),
Some("mkv" | "avi" | "mp4" | "wmv" | "ts" | "m4v" | "mov" | "webm")
)
}
pub fn is_par2_file(filename: &str) -> bool {
filename.to_ascii_lowercase().ends_with(".par2")
}
pub fn is_important_file_type(filename: &str) -> bool {
is_video_file(filename) || is_rar_file(filename) || is_7z_file(filename)
}
pub fn is_probably_obfuscated(filename: &str) -> bool {
let stem = std::path::Path::new(filename)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or(filename);
if stem.len() >= 32 && stem.chars().all(|c| c.is_ascii_hexdigit()) {
return true;
}
if uuid::Uuid::try_parse(stem).is_ok() {
return true;
}
if stem.chars().all(|c| c.is_ascii_digit()) {
return true;
}
false
}
pub fn get_nzb_password(filename: &str) -> Option<String> {
static PW_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\{\{(.+?)\}\}").expect("password regex"));
PW_RE.captures(filename).map(|c| c[1].to_string())
}
pub fn get_job_name(filename: &str) -> String {
let name = filename
.trim_end_matches(".nzb")
.trim_end_matches(".gz")
.trim_end_matches(".nzb");
static PW_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\s*\{\{.+?\}\}\s*").expect("password strip regex"));
PW_RE.replace_all(name, "").trim().to_string()
}
pub fn matches_blocklist(filename: &str, patterns: &[String]) -> bool {
let lower = filename.to_ascii_lowercase();
patterns.iter().any(|pattern| {
glob::Pattern::new(&pattern.to_ascii_lowercase())
.map(|p| p.matches(&lower))
.unwrap_or(false)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_rar_file() {
assert!(is_rar_file("archive.rar"));
assert!(is_rar_file("archive.RAR"));
assert!(is_rar_file("archive.r00"));
assert!(is_rar_file("archive.r01"));
assert!(is_rar_file("movie.part01.rar"));
assert!(!is_rar_file("readme.txt"));
assert!(!is_rar_file("video.mkv"));
}
#[test]
fn test_is_par2_file() {
assert!(is_par2_file("file.par2"));
assert!(is_par2_file("file.vol01+02.par2"));
assert!(is_par2_file("FILE.PAR2"));
assert!(!is_par2_file("file.txt"));
assert!(!is_par2_file("file.par"));
}
#[test]
fn test_is_video_file() {
assert!(is_video_file("movie.mkv"));
assert!(is_video_file("movie.mp4"));
assert!(is_video_file("movie.avi"));
assert!(is_video_file("movie.MKV"));
assert!(is_video_file("movie.wmv"));
assert!(is_video_file("movie.ts"));
assert!(!is_video_file("info.nfo"));
assert!(!is_video_file("readme.txt"));
assert!(!is_video_file("archive.rar"));
}
#[test]
fn test_is_probably_obfuscated() {
assert!(is_probably_obfuscated(
"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4.mkv"
));
assert!(is_probably_obfuscated(
"550e8400-e29b-41d4-a716-446655440000.mkv"
));
assert!(is_probably_obfuscated("1234567890.mkv"));
assert!(!is_probably_obfuscated("movie.name.mkv"));
assert!(!is_probably_obfuscated("Some.Movie.2024.1080p.mkv"));
}
#[test]
fn test_get_job_name() {
assert_eq!(get_job_name("release.nzb"), "release");
assert_eq!(get_job_name("release {{password}}.nzb"), "release");
assert_eq!(get_job_name("file.nzb.gz"), "file");
assert_eq!(get_job_name("noext"), "noext");
}
#[test]
fn test_get_nzb_password_double_braces() {
assert_eq!(
get_nzb_password("release {{mypass}}.nzb"),
Some("mypass".to_string())
);
}
#[test]
fn test_get_nzb_password_no_password() {
assert_eq!(get_nzb_password("release.nzb"), None);
}
#[test]
fn test_get_nzb_password_empty_braces() {
assert_eq!(get_nzb_password("release {{}}.nzb"), None);
}
#[test]
fn test_matches_blocklist() {
let patterns = vec!["*.nfo".to_string(), "*.txt".to_string()];
assert!(matches_blocklist("info.nfo", &patterns));
assert!(matches_blocklist("readme.txt", &patterns));
assert!(matches_blocklist("INFO.NFO", &patterns));
assert!(!matches_blocklist("movie.mkv", &patterns));
assert!(!matches_blocklist("archive.rar", &patterns));
}
}