Add support for additional media types

This commit is contained in:
Daan Boerlage 2022-09-01 23:18:43 +02:00
parent 333cd0c38b
commit 0b3c88d601
Signed by: daan
GPG key ID: FCE070E1E4956606
5 changed files with 124 additions and 43 deletions

7
Cargo.lock generated
View file

@ -120,6 +120,12 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.132" version = "0.2.132"
@ -132,6 +138,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"clap", "clap",
"eyre", "eyre",
"lazy_static",
"regex", "regex",
"walkdir", "walkdir",
] ]

View file

@ -10,3 +10,4 @@ eyre = "0.6"
clap = { version = "3.2", features = ["derive"] } clap = { version = "3.2", features = ["derive"] }
regex = "1.6" regex = "1.6"
walkdir = "2" walkdir = "2"
lazy_static = "1.4"

View file

@ -5,6 +5,7 @@ mod utils;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use eyre::Result; use eyre::Result;
use clap::Parser; use clap::Parser;
use lazy_static::lazy_static;
use regex::{Captures, Regex}; use regex::{Captures, Regex};
use walkdir::{DirEntry, WalkDir}; 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"]; 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<title>.*?)(?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>...)$"; pub(crate) const SERIES_REGEX: &str = r"^(?P<title>.*?)(?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 { struct Stats {
success: usize, success: usize,
@ -43,8 +49,6 @@ struct Context {
fn main() -> Result<()> { fn main() -> Result<()> {
let params = Args::parse(); let params = Args::parse();
println!("{}", params.path);
let mut stats = Stats { let mut stats = Stats {
files: 0, files: 0,
success: 0, success: 0,
@ -57,11 +61,9 @@ fn main() -> Result<()> {
has_printed_folder: false, has_printed_folder: false,
}; };
let regex = Regex::new(SERIES_REGEX).unwrap();
for entry in WalkDir::new(params.path) { for entry in WalkDir::new(params.path) {
if let Ok(file) = entry { if let Ok(file) = entry {
check_file(file, &mut stats, &mut ctx, &regex, 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(()) 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(); let file_type = file.file_type();
if file_type.is_file() { if file_type.is_file() {
let filename = file.file_name().to_str().unwrap(); 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 => { ComplianceStatus::NotMatched => {
stats.files += 1; stats.files += 1;
stats.error += 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); println!("\n\n=== {} {}", get_status_icon(StatusIcon::Directory, no_emoji), ctx.current_folder);
ctx.has_printed_folder = true; 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; stats.files += 1;
if !ctx.has_printed_folder { if !ctx.has_printed_folder {
println!("\n\n=== {} {}", get_status_icon(StatusIcon::Directory, no_emoji), ctx.current_folder); println!("\n\n=== {} {}", get_status_icon(StatusIcon::Directory, no_emoji), ctx.current_folder);
ctx.has_printed_folder = true; ctx.has_printed_folder = true;
} }
stats.warning += 1; 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.files += 1;
stats.success += 1; stats.success += 1;
if show_success { 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 => {} 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] = [ enum FileType<'lt> {
DashInTitle::check, SeriesEpisode(Captures<'lt>),
EpisodeMarker::check, Movie(Captures<'lt>),
HasFluff::check, Unknown
]; }
const SERIES_RULES_WITH_NAME: [fn(&str, &Captures) -> Option<NonCompliantReason>; 5] = [ fn parse_filename(filename: &str) -> FileType {
DashInTitle::check, if let Some(captures) = SERIES_PARSER.captures(filename) {
EpisodeMarker::check, return FileType::SeriesEpisode(captures)
HasFluff::check, }
HasEpisodeName::check,
MissingNameSeparator::check
];
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 let Some(ext) = file.path().extension() {
if !VIDEO_EXTENSIONS.contains(&ext.to_str().unwrap()) { if !VIDEO_EXTENSIONS.contains(&ext.to_str().unwrap()) {
return ComplianceStatus::Ignored; return ComplianceStatus::Ignored;
@ -131,27 +157,25 @@ fn lint_file_name(file: &DirEntry, filename: &str, regex: &Regex, episode_name:
return ComplianceStatus::Ignored; return ComplianceStatus::Ignored;
} }
let captures = regex.captures(filename); match parse_filename(filename) {
FileType::SeriesEpisode(captures) => {
if captures.is_none() {
return ComplianceStatus::NotMatched;
}
let captures = captures.unwrap();
if episode_name { if episode_name {
check_rules(SERIES_RULES_WITH_NAME, filename, &captures) check_rules(SERIES_RULES_WITH_NAME, DetectedFileType::SeriesEpisode, filename, &captures)
} else { } else {
check_rules(SERIES_RULES_WITHOUT_NAME, filename, &captures) 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 { for rule in validation_rules {
if let Some(reason) = rule(filename, &captures) { if let Some(reason) = rule(filename, &captures) {
return ComplianceStatus::NonCompliant(reason); return ComplianceStatus::NonCompliant(file_type, reason);
} }
} }
ComplianceStatus::Compliant ComplianceStatus::Compliant(file_type)
} }

View file

@ -14,6 +14,24 @@ pub use has_episode_name::HasEpisodeName;
pub use has_dash_in_title::DashInTitle; pub use has_dash_in_title::DashInTitle;
pub use missing_name_separator::MissingNameSeparator; 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)] #[derive(Debug, PartialEq)]
pub enum NonCompliantReason { pub enum NonCompliantReason {
DashInTitle, DashInTitle,
@ -62,10 +80,15 @@ impl Display for EpisodeMarkerReason {
} }
} }
pub enum DetectedFileType {
SeriesEpisode,
Movie
}
pub enum ComplianceStatus { pub enum ComplianceStatus {
NonCompliant(NonCompliantReason), NonCompliant(DetectedFileType, NonCompliantReason),
NotMatched, NotMatched,
Compliant, Compliant(DetectedFileType),
Ignored Ignored
} }

View file

@ -1,3 +1,5 @@
use crate::DetectedFileType;
pub enum StatusIcon { pub enum StatusIcon {
Failure, Failure,
Warning, Warning,
@ -13,3 +15,27 @@ pub fn get_status_icon(icon: StatusIcon, no_emoji: bool) -> String {
StatusIcon::Directory => { if no_emoji { "".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() } }
}
}