Add support for additional media types
This commit is contained in:
parent
333cd0c38b
commit
0b3c88d601
5 changed files with 124 additions and 43 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
|
@ -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"
|
106
src/main.rs
106
src/main.rs
|
@ -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, ®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(())
|
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() {
|
if episode_name {
|
||||||
return ComplianceStatus::NotMatched;
|
check_rules(SERIES_RULES_WITH_NAME, DetectedFileType::SeriesEpisode, filename, &captures)
|
||||||
}
|
} else {
|
||||||
|
check_rules(SERIES_RULES_WITHOUT_NAME, DetectedFileType::SeriesEpisode, filename, &captures)
|
||||||
let captures = captures.unwrap();
|
}
|
||||||
|
}
|
||||||
if episode_name {
|
FileType::Movie(captures) => check_rules(MOVIE_RULES, DetectedFileType::Movie, filename, &captures),
|
||||||
check_rules(SERIES_RULES_WITH_NAME, filename, &captures)
|
FileType::Unknown => ComplianceStatus::NotMatched
|
||||||
} else {
|
|
||||||
check_rules(SERIES_RULES_WITHOUT_NAME, filename, &captures)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use crate::DetectedFileType;
|
||||||
|
|
||||||
pub enum StatusIcon {
|
pub enum StatusIcon {
|
||||||
Failure,
|
Failure,
|
||||||
Warning,
|
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::Success => { if no_emoji { "S -".to_string() } else { "✔️".to_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() } }
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue