Initial Commit

This commit is contained in:
Daan Boerlage 2022-08-27 20:15:31 +02:00
commit 7a06c4cea7
Signed by: daan
GPG key ID: FCE070E1E4956606
14 changed files with 1064 additions and 0 deletions

14
src/args.rs Normal file
View file

@ -0,0 +1,14 @@
use clap::Parser;
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
pub struct Args {
#[clap(long, value_parser)]
pub no_emoji: bool,
#[clap(long, value_parser)]
pub show_success: bool,
#[clap(long, value_parser)]
pub path: String,
}

120
src/main.rs Normal file
View file

@ -0,0 +1,120 @@
mod args;
mod rules;
mod utils;
use std::fmt::{Display, Formatter};
use eyre::Result;
use clap::Parser;
use walkdir::{DirEntry, WalkDir};
use args::Args;
use rules::*;
use utils::status_icon::*;
const VIDEO_EXTENSIONS: [&str; 14] = ["mkv", "mp4", "avi", "webm", "mov", "wmv", "flv", "ogg", "ogv", "yuv", "amv", "mpg", "mpeg", "m4v"];
const SERIES_REGEX: &str = r"^(?P<title>.+)\sS(?P<season>\d\d)E(?P<episode>\d\d\d?)(E(?P<episode2>\d\d\d?))?((?P<nameSeparator>\s-\s)?(?P<name>.+))?\.(?P<ext>...)$";
// const FILE_EXT_REGEX: &str = r"^(?P<title>.+)\.(?P<ext>...)$";
// const MOVIE_REGEX: &str = r"^(?P<title>.+)\s(?P<year>\(\d{4}\))\s(?P<resolution>\[.+\])\.(?P<ext>...)$";
struct Stats {
success: usize,
warning: usize,
error: usize,
files: usize,
}
impl Display for Stats {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Files: {} - Success: {} - Warnings: {} - Errors: {}",
self.files,
self.success,
self.warning,
self.error)
}
}
#[tokio::main]
async fn main() -> Result<()> {
let params = Args::parse();
println!("{}", params.path);
let mut stats = Stats {
files: 0,
success: 0,
warning: 0,
error: 0,
};
for entry in WalkDir::new(params.path) {
if let Ok(file) = entry {
check_file(file, &mut stats, params.no_emoji, params.show_success);
}
}
println!("{}", stats);
Ok(())
}
fn check_file(file: DirEntry, stats: &mut Stats, no_emoji: bool, show_success: bool) -> () {
let file_type = file.file_type();
if file_type.is_file() {
stats.files += 1;
let filename = file.file_name().to_str().unwrap();
match lint_file_name(&file, filename) {
ComplianceStatus::NotMatched => {
stats.error += 1;
println!("{} {} --- (Failed to match)", get_status_icon(StatusIcon::Failure, no_emoji), filename)
}
ComplianceStatus::NonCompliant(reason) => {
stats.warning += 1;
println!("{} {} --- ({})", get_status_icon(StatusIcon::Warning, no_emoji), filename, reason);
}
ComplianceStatus::Compliant => {
stats.success += 1;
if show_success {
println!("{} {}", get_status_icon(StatusIcon::Success, no_emoji), filename)
}
}
}
} else if file_type.is_dir() {
println!("\n\n=== {} {}", get_status_icon(StatusIcon::Directory, no_emoji), file.path().display())
}
}
fn lint_file_name(file: &DirEntry, filename: &str) -> ComplianceStatus {
if let Some(ext) = file.path().extension() {
if !VIDEO_EXTENSIONS.contains(&ext.to_str().unwrap()) {
return ComplianceStatus::Compliant;
}
}
let captures = regex::Regex::new(SERIES_REGEX).unwrap().captures(filename);
if captures.is_none() {
return ComplianceStatus::NotMatched;
}
let captures = captures.unwrap();
if let Some(reason) = HasEpisodeName::check(filename, &captures) {
return ComplianceStatus::NonCompliant(reason);
}
if let Some(reason) = MissingSeparator::check(filename, &captures) {
return ComplianceStatus::NonCompliant(reason);
}
if let Some(reason) = DashInTitle::check(filename, &captures) {
return ComplianceStatus::NonCompliant(reason);
}
if let Some(reason) = HasFluff::check(filename, &captures) {
return ComplianceStatus::NonCompliant(reason);
}
ComplianceStatus::Compliant
}

View file

@ -0,0 +1,20 @@
use regex::Captures;
use eyre::Result;
use super::*;
pub struct DashInTitle {}
impl Rule for DashInTitle {
fn check(_filename: &str, captures: &Captures) -> Option<NonCompliantReason> {
let title = captures.name("title").unwrap();
if title.as_str().contains("-") {
return Some(NonCompliantReason::DashInTitle)
}
return None
}
fn fix(_filename: &str, _captures: &Captures) -> Result<FixStatus> {
return Ok(FixStatus::NotImplemented)
}
}

View file

@ -0,0 +1,18 @@
use regex::Captures;
use eyre::Result;
use super::*;
pub struct HasEpisodeName {}
impl Rule for HasEpisodeName {
fn check(_filename: &str, captures: &Captures) -> Option<NonCompliantReason> {
if captures.name("name").is_none() {
return Some(NonCompliantReason::MissingName)
}
return None
}
fn fix(_filename: &str, _captures: &Captures) -> Result<FixStatus> {
return Ok(FixStatus::NotImplemented)
}
}

24
src/rules/has_fluff.rs Normal file
View file

@ -0,0 +1,24 @@
use regex::Captures;
use eyre::Result;
use super::*;
const KNOWN_FLUFF: [&str; 7] = ["[", "xvid", "BluRay", "1080p", "264", "dvdrip", "web-dl"];
pub struct HasFluff {}
impl Rule for HasFluff {
fn check(filename: &str, _captures: &Captures) -> Option<NonCompliantReason> {
let lowered_filename = filename.to_lowercase();
for fluff in KNOWN_FLUFF {
if lowered_filename.contains(fluff) {
return Some(NonCompliantReason::HasFluff)
}
}
return None
}
fn fix(_filename: &str, _captures: &Captures) -> Result<FixStatus> {
return Ok(FixStatus::NotImplemented)
}
}

View file

@ -0,0 +1,18 @@
use regex::Captures;
use eyre::Result;
use super::*;
pub struct MissingSeparator {}
impl Rule for MissingSeparator {
fn check(_filename: &str, captures: &Captures) -> Option<NonCompliantReason> {
if captures.name("nameSeparator").is_none() {
return Some(NonCompliantReason::MissingNameSeparator)
}
return None
}
fn fix(_filename: &str, _captures: &Captures) -> Result<FixStatus> {
return Ok(FixStatus::NotImplemented)
}
}

48
src/rules/mod.rs Normal file
View file

@ -0,0 +1,48 @@
use std::fmt::{Display, Formatter};
use regex::Captures;
use eyre::Result;
mod has_fluff;
mod has_dash_in_title;
mod has_episode_name;
mod missing_separator;
pub use has_fluff::HasFluff;
pub use has_episode_name::HasEpisodeName;
pub use has_dash_in_title::DashInTitle;
pub use missing_separator::MissingSeparator;
pub enum NonCompliantReason {
DashInTitle,
MissingName,
HasFluff,
MissingNameSeparator
}
impl Display for NonCompliantReason {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
NonCompliantReason::DashInTitle => write!(f, "Title contains dash"),
NonCompliantReason::MissingName => write!(f, "Missing episode name"),
NonCompliantReason::HasFluff => write!(f, "Has fluff"),
NonCompliantReason::MissingNameSeparator => write!(f, "Missing episode name separator"),
}
}
}
pub enum ComplianceStatus {
NonCompliant(NonCompliantReason),
NotMatched,
Compliant,
}
pub enum FixStatus {
// Fixed,
NotImplemented,
// NotFixable
}
pub trait Rule {
fn check(filename: &str, captures: &Captures) -> Option<NonCompliantReason>;
fn fix(filename: &str, captures: &Captures) -> Result<FixStatus>;
}

View file

@ -0,0 +1,4 @@
#[allow(dead_code)]
pub fn episode_name(title: String, season: String, episode: String, name: String, format: String) -> String {
format!("${title} S${season}E${episode} - ${name}.${format}")
}

2
src/utils/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod constructors;
pub mod status_icon;

15
src/utils/status_icon.rs Normal file
View file

@ -0,0 +1,15 @@
pub enum StatusIcon {
Failure,
Warning,
Success,
Directory,
}
pub fn get_status_icon(icon: StatusIcon, no_emoji: bool) -> String {
match icon {
StatusIcon::Failure => { if no_emoji { "F -".to_string() } else { "".to_string() } }
StatusIcon::Warning => { if no_emoji { "W -".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() } }
}
}