diff --git a/Cargo.lock b/Cargo.lock index 111be8d..1934e38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,6 +120,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.132" @@ -132,6 +138,7 @@ version = "0.1.0" dependencies = [ "clap", "eyre", + "lazy_static", "regex", "walkdir", ] diff --git a/Cargo.toml b/Cargo.toml index 2a23bbe..1675f3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,4 @@ eyre = "0.6" clap = { version = "3.2", features = ["derive"] } regex = "1.6" walkdir = "2" +lazy_static = "1.4" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index a55e457..c609931 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ mod utils; use std::fmt::{Display, Formatter}; use eyre::Result; use clap::Parser; +use lazy_static::lazy_static; use regex::{Captures, Regex}; use walkdir::{DirEntry, WalkDir}; @@ -14,7 +15,12 @@ use utils::status_icon::*; const VIDEO_EXTENSIONS: [&str; 14] = ["mkv", "mp4", "avi", "webm", "mov", "wmv", "flv", "ogg", "ogv", "yuv", "amv", "mpg", "mpeg", "m4v"]; pub(crate) const SERIES_REGEX: &str = r"^(?P.*?)(?P<titleSeparator>\s-\s?)?(?P<seasonPrefix>[Ss]|\s|\.)(?P<season>\d{1,3})(?P<episodePrefix>[Ee]|[Xx]|[Ss])(?P<episode>\d{1,3})([Ee](?P<episode2>\d{2,3}))?((?P<nameSeparator>\s-\s)?(?P<name>.+))?\.(?P<ext>...)$"; -// const MOVIE_REGEX: &str = r"^(?P<title>.+)\s(?P<year>\(\d{4}\))\s(?P<resolution>\[.+\])\.(?P<ext>...)$"; +pub(crate) const MOVIE_REGEX: &str = r"^(?P<title>.*?)\s(?P<year>\(\d{4}\))\s(?P<resolution>\[.+\])\.(?P<ext>...)$"; + +lazy_static! { + static ref SERIES_PARSER: Regex = Regex::new(SERIES_REGEX).unwrap(); + static ref MOVIE_PARSER: Regex = Regex::new(MOVIE_REGEX).unwrap(); +} struct Stats { success: usize, @@ -43,8 +49,6 @@ struct Context { fn main() -> Result<()> { let params = Args::parse(); - println!("{}", params.path); - let mut stats = Stats { files: 0, success: 0, @@ -57,11 +61,9 @@ fn main() -> Result<()> { has_printed_folder: false, }; - let regex = Regex::new(SERIES_REGEX).unwrap(); - for entry in WalkDir::new(params.path) { if let Ok(file) = entry { - check_file(file, &mut stats, &mut ctx, ®ex, params.no_emoji, params.show_success, params.episode_name); + check_file(file, &mut stats, &mut ctx, params.no_emoji, params.show_success, params.episode_name); } } @@ -70,11 +72,27 @@ fn main() -> Result<()> { Ok(()) } -fn check_file(file: DirEntry, stats: &mut Stats, ctx: &mut Context, regex: &Regex, no_emoji: bool, show_success: bool, episode_name: bool) -> () { +fn print_result(media_icon: MediaIcon, status_icon: StatusIcon, filename: &str, reason: Option<String>, no_emoji: bool) { + match reason { + None => println!( + "{} {} {}", + get_media_icon(media_icon, no_emoji), + get_status_icon(status_icon, no_emoji), + filename), + Some(r) => println!( + "{} {} {} --- ({})", + get_media_icon(media_icon, no_emoji), + get_status_icon(StatusIcon::Warning, no_emoji), + filename, + r) + } +} + +fn check_file(file: DirEntry, stats: &mut Stats, ctx: &mut Context, no_emoji: bool, show_success: bool, episode_name: bool) -> () { let file_type = file.file_type(); if file_type.is_file() { let filename = file.file_name().to_str().unwrap(); - match lint_file_name(&file, filename, regex, episode_name) { + match lint_file_name(&file, filename, episode_name) { ComplianceStatus::NotMatched => { stats.files += 1; stats.error += 1; @@ -82,22 +100,26 @@ fn check_file(file: DirEntry, stats: &mut Stats, ctx: &mut Context, regex: &Rege println!("\n\n=== {} {}", get_status_icon(StatusIcon::Directory, no_emoji), ctx.current_folder); ctx.has_printed_folder = true; } - println!("{} {} --- (Failed to match)", get_status_icon(StatusIcon::Failure, no_emoji), filename); + print_result(MediaIcon::Unknown, StatusIcon::Failure, filename, Some("Failed to match".to_string()), no_emoji); } - ComplianceStatus::NonCompliant(reason) => { + ComplianceStatus::NonCompliant(detected_file_type, reason) => { stats.files += 1; if !ctx.has_printed_folder { println!("\n\n=== {} {}", get_status_icon(StatusIcon::Directory, no_emoji), ctx.current_folder); ctx.has_printed_folder = true; } stats.warning += 1; - println!("{} {} --- ({})", get_status_icon(StatusIcon::Warning, no_emoji), filename, reason); + print_result(MediaIcon::from(detected_file_type), StatusIcon::Warning, filename, Some(reason.to_string()), no_emoji); } - ComplianceStatus::Compliant => { + ComplianceStatus::Compliant(detected_file_type) => { stats.files += 1; stats.success += 1; if show_success { - println!("{} {}", get_status_icon(StatusIcon::Success, no_emoji), filename); + if !ctx.has_printed_folder { + println!("\n\n=== {} {}", get_status_icon(StatusIcon::Directory, no_emoji), ctx.current_folder); + ctx.has_printed_folder = true; + } + print_result(MediaIcon::from(detected_file_type), StatusIcon::Success, filename, None, no_emoji); } } ComplianceStatus::Ignored => {} @@ -108,21 +130,25 @@ fn check_file(file: DirEntry, stats: &mut Stats, ctx: &mut Context, regex: &Rege } } -const SERIES_RULES_WITHOUT_NAME: [fn(&str, &Captures) -> Option<NonCompliantReason>; 3] = [ - DashInTitle::check, - EpisodeMarker::check, - HasFluff::check, -]; +enum FileType<'lt> { + SeriesEpisode(Captures<'lt>), + Movie(Captures<'lt>), + Unknown +} -const SERIES_RULES_WITH_NAME: [fn(&str, &Captures) -> Option<NonCompliantReason>; 5] = [ - DashInTitle::check, - EpisodeMarker::check, - HasFluff::check, - HasEpisodeName::check, - MissingNameSeparator::check -]; +fn parse_filename(filename: &str) -> FileType { + if let Some(captures) = SERIES_PARSER.captures(filename) { + return FileType::SeriesEpisode(captures) + } -fn lint_file_name(file: &DirEntry, filename: &str, regex: &Regex, episode_name: bool) -> ComplianceStatus { + if let Some(captures) = MOVIE_PARSER.captures(filename) { + return FileType::Movie(captures) + } + + return FileType::Unknown +} + +fn lint_file_name(file: &DirEntry, filename: &str, episode_name: bool) -> ComplianceStatus { if let Some(ext) = file.path().extension() { if !VIDEO_EXTENSIONS.contains(&ext.to_str().unwrap()) { return ComplianceStatus::Ignored; @@ -131,27 +157,25 @@ fn lint_file_name(file: &DirEntry, filename: &str, regex: &Regex, episode_name: return ComplianceStatus::Ignored; } - let captures = regex.captures(filename); - - if captures.is_none() { - return ComplianceStatus::NotMatched; - } - - let captures = captures.unwrap(); - - if episode_name { - check_rules(SERIES_RULES_WITH_NAME, filename, &captures) - } else { - check_rules(SERIES_RULES_WITHOUT_NAME, filename, &captures) + match parse_filename(filename) { + FileType::SeriesEpisode(captures) => { + if episode_name { + check_rules(SERIES_RULES_WITH_NAME, DetectedFileType::SeriesEpisode, filename, &captures) + } else { + check_rules(SERIES_RULES_WITHOUT_NAME, DetectedFileType::SeriesEpisode, filename, &captures) + } + } + FileType::Movie(captures) => check_rules(MOVIE_RULES, DetectedFileType::Movie, filename, &captures), + FileType::Unknown => ComplianceStatus::NotMatched } } -fn check_rules<const N: usize>(validation_rules: [fn(&str, &Captures) -> Option<NonCompliantReason>; N], filename: &str, captures: &Captures) -> ComplianceStatus { +fn check_rules<const N: usize>(validation_rules: [fn(&str, &Captures) -> Option<NonCompliantReason>; N], file_type: DetectedFileType, filename: &str, captures: &Captures) -> ComplianceStatus { for rule in validation_rules { if let Some(reason) = rule(filename, &captures) { - return ComplianceStatus::NonCompliant(reason); + return ComplianceStatus::NonCompliant(file_type, reason); } } - ComplianceStatus::Compliant + ComplianceStatus::Compliant(file_type) } \ No newline at end of file diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 350e9b5..a6b8146 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -14,6 +14,24 @@ pub use has_episode_name::HasEpisodeName; pub use has_dash_in_title::DashInTitle; pub use missing_name_separator::MissingNameSeparator; +pub const SERIES_RULES_WITHOUT_NAME: [fn(&str, &Captures) -> Option<NonCompliantReason>; 3] = [ + DashInTitle::check, + EpisodeMarker::check, + HasFluff::check, +]; + +pub const SERIES_RULES_WITH_NAME: [fn(&str, &Captures) -> Option<NonCompliantReason>; 5] = [ + DashInTitle::check, + EpisodeMarker::check, + HasFluff::check, + HasEpisodeName::check, + MissingNameSeparator::check +]; + +pub const MOVIE_RULES: [fn(&str, &Captures) -> Option<NonCompliantReason>; 1] = [ + HasFluff::check, +]; + #[derive(Debug, PartialEq)] pub enum NonCompliantReason { DashInTitle, @@ -62,10 +80,15 @@ impl Display for EpisodeMarkerReason { } } +pub enum DetectedFileType { + SeriesEpisode, + Movie +} + pub enum ComplianceStatus { - NonCompliant(NonCompliantReason), + NonCompliant(DetectedFileType, NonCompliantReason), NotMatched, - Compliant, + Compliant(DetectedFileType), Ignored } diff --git a/src/utils/status_icon.rs b/src/utils/status_icon.rs index 10ba0b9..c757641 100644 --- a/src/utils/status_icon.rs +++ b/src/utils/status_icon.rs @@ -1,3 +1,5 @@ +use crate::DetectedFileType; + pub enum StatusIcon { Failure, Warning, @@ -12,4 +14,28 @@ pub fn get_status_icon(icon: StatusIcon, no_emoji: bool) -> String { StatusIcon::Success => { if no_emoji { "S -".to_string() } else { "✔️".to_string() } } StatusIcon::Directory => { if no_emoji { "".to_string() } else { "📂".to_string() } } } +} + + +pub enum MediaIcon { + SeriesEpisode, + Movie, + Unknown, +} + +impl From<DetectedFileType> for MediaIcon { + fn from(d: DetectedFileType) -> Self { + match d { + DetectedFileType::SeriesEpisode => Self::SeriesEpisode, + DetectedFileType::Movie => Self::Movie + } + } +} + +pub fn get_media_icon(icon: MediaIcon, no_emoji: bool) -> String { + match icon { + MediaIcon::SeriesEpisode => { if no_emoji { "S -".to_string() } else { "🎞️".to_string() } } + MediaIcon::Movie => { if no_emoji { "M -".to_string() } else { "🎬".to_string() } } + MediaIcon::Unknown => { if no_emoji { "U -".to_string() } else { "❓".to_string() } } + } } \ No newline at end of file