From cb687fa80862adec66e9e9b224d1c7c6a8798931 Mon Sep 17 00:00:00 2001 From: Andreas Mieke Date: Sun, 5 Nov 2023 17:19:01 +0100 Subject: [PATCH] feat(media): Implement directory listing Work on tokenizing file names for TMDB API calls in future. --- Cargo.lock | 39 +++++++++++++++++ Cargo.toml | 1 + src/directory.rs | 60 +++++++++++++++++++++++--- src/main.rs | 10 +++-- src/media.rs | 109 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 210 insertions(+), 9 deletions(-) create mode 100644 src/media.rs diff --git a/Cargo.lock b/Cargo.lock index aa348d1..583aa15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,6 +94,12 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "cc" version = "1.0.83" @@ -103,6 +109,17 @@ dependencies = [ "libc", ] +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -206,6 +223,12 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "heck" version = "0.4.1" @@ -244,6 +267,15 @@ dependencies = [ "cc", ] +[[package]] +name = "infer" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb33622da908807a06f9513c19b3c1ad50fab3e4137d82a78107d502075aa199" +dependencies = [ + "cfb", +] + [[package]] name = "inquire" version = "0.6.2" @@ -367,6 +399,7 @@ name = "plex-media-ingest" version = "0.1.0" dependencies = [ "clap", + "infer", "inquire", "log", "serde", @@ -583,6 +616,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "uuid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" + [[package]] name = "walkdir" version = "2.4.0" diff --git a/Cargo.toml b/Cargo.toml index 1490eab..feb8452 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] clap = { version = "4.4.7", features = ["derive"] } +infer = "0.15.0" inquire = "0.6.2" log = "0.4.20" serde = { version = "1.0.190", features = ["derive"] } diff --git a/src/directory.rs b/src/directory.rs index 795fb88..0f7a710 100644 --- a/src/directory.rs +++ b/src/directory.rs @@ -1,16 +1,16 @@ -use std::path::PathBuf; +use std::{path::PathBuf, fs::{self, DirEntry}, error::Error}; -use walkdir::{DirEntry, WalkDir}; +use crate::media::handle_media; -fn is_not_hidden(entry: &DirEntry) -> bool { +/*fn is_not_hidden(entry: &DirEntry) -> bool { entry .file_name() .to_str() - .map(|s| entry.depth() == 0 || !s.starts_with(".")) + .map(|s| entry.depth() == 0 || (!s.starts_with(".") && !s.starts_with("@"))) // todo!: Allow ignored chars to be configured, here, @ is QNAP special folders .unwrap_or(false) } -pub fn list_files(path: PathBuf) -> Vec{ +pub fn walk_path(path: PathBuf) -> Vec { let mut entries: Vec = vec![]; WalkDir::new(path) .into_iter() @@ -18,4 +18,52 @@ pub fn list_files(path: PathBuf) -> Vec{ .filter_map(|v| v.ok()) .for_each(|x| entries.push(x.into_path())); entries -} \ No newline at end of file +}*/ + +pub fn search_path(path: PathBuf) -> Result<(), Box> { + let entries = fs::read_dir(path)?; + let mut files: Vec = Vec::new(); + let mut dirs: Vec = Vec::new(); + + // Put all files and folders in corresponding vectors + for entry in entries { + if let Ok(entry) = entry { + if let Ok(file_type) = entry.file_type() { + if file_type.is_dir() { + dirs.push(entry); + } else if file_type.is_file() { + files.push(entry); + } + } + } + } + + if dirs.len() == 0 { + // No folders present, assuming there are only distinct media files + for file in files { + handle_media(file); + } + } + + Ok(()) +} + +/* +Look at current directory: + Only directories, no media files -> + Media must be in subfolders, look at name of folder (in case media file has cryptic name) and traverse into it, look at media files + + Media files present, folders as well -> + Treat media as media to add, traverse into subfolders and look for eventual extra content + + Media file(s), but no folders present -> + Treat media as media to add +*/ + +/* +Use folder/file name as name to look up on tmdb (replace . with ' ' till first occurence of non alphanumeric symbol ([]())) (For shows, look for SxxEyy or similar tokens, if single file assume movie by default) +If there is a token with only 4 digits (and maybe parantheses), assume this is a year, add it to tmdb search, retry search without 'year' if result is empty +Show user file with title(s) found on tmdb, make user select one +Remember selection and look in current folder for extra content (deleted scenes, trailers, featurettes) -> show user what files were found and have them select which files they want +For each selection show file name plus try to match to extras category, show user selection of which kind of extra it is, then allow them to enter a arbitary name (prefill from file if possible) +*/ \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index b7a8c42..a18f888 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ mod config; mod directory; +mod media; use log::*; use clap::Parser; @@ -62,10 +63,13 @@ fn main() { args.path.unwrap() }; - let files = directory::list_files(search_path); + //let files = directory::walk_path(search_path); + directory::search_path(search_path).unwrap(); - for file in files { + /*for file in files.clone() { info!("Found: {}", file.to_str().unwrap()); - } + }*/ + + //search_media(files).unwrap(); } \ No newline at end of file diff --git a/src/media.rs b/src/media.rs new file mode 100644 index 0000000..b777469 --- /dev/null +++ b/src/media.rs @@ -0,0 +1,109 @@ +use std::{path::PathBuf, error::Error, io::Read, fs::{File, DirEntry}, cmp}; +use infer; +use log::{info, warn, error, trace, debug}; + +#[derive(Debug)] +struct MediaName { + name: String, + year: String +} + +fn get_file_header(path: PathBuf) -> Result, Box> { + let f = File::open(path)?; + + let limit = f + .metadata() + .map(|m| cmp::min(m.len(), 8192) as usize + 1) + .unwrap_or(0); + let mut bytes = Vec::with_capacity(limit); + f.take(8192).read_to_end(&mut bytes)?; + Ok(bytes) +} + +fn token_year_likely(t: &&str) -> bool { + if t.len() == 6 && + (t.starts_with('[') && t.ends_with(']')) || + (t.starts_with('(') && t.ends_with(')')) || + (t.starts_with('{') && t.ends_with('}')) + { + return true; + } + return false +} + +fn token_valid(t: &&str) -> bool { + if + t.eq_ignore_ascii_case("dvd") || + t.eq_ignore_ascii_case("bluray") || + t.eq_ignore_ascii_case("webrip") || + t.eq_ignore_ascii_case("web") || + t.eq_ignore_ascii_case("uhd") || + t.eq_ignore_ascii_case("hd") || + t.eq_ignore_ascii_case("tv") || + t.eq_ignore_ascii_case("tvrip") || + t.eq_ignore_ascii_case("1080p") || + t.eq_ignore_ascii_case("1080i") || + t.eq_ignore_ascii_case("2160p") || + (t.len() != 6 && t.starts_with('[') && t.ends_with(']')) || + (t.len() != 6 && t.starts_with('(') && t.ends_with(')')) || + (t.len() != 6 && t.starts_with('{') && t.ends_with('}')) + { + return false; + } + true +} + +fn find_media_name(file_name: String) -> MediaName { + let mut tokens: Vec<&str> = file_name.split(&['-', ' ', ':', '@', '.'][..]).filter(|t| token_valid(t)).collect(); + trace!("Tokens are: {:#?}", tokens); + + // Remove last token (file ext) + _ = tokens.pop(); + + let mut year = String::new(); + let mut name = String::new(); + + for token in tokens { + if token_year_likely(&token) { + year = token.strip_prefix(['(', '[', '{']).unwrap().strip_suffix([')', ']', '}']).unwrap().to_string(); + } else if token.len() != 0 { + name.push_str(token); + name.push(' '); + } + } + + // Remove last added space + name.pop(); + + let media_name = MediaName { name: name, year: year }; + debug!("Name is now: {:#?}", media_name); + media_name +} + +fn video_file_handler(entry: DirEntry) { + let path = entry.path(); + info!("Found video file: {:#?}", path); + + let file_name = path.file_name().unwrap_or_default(); + trace!("File name is: {:#?}", file_name); + + let name = find_media_name(file_name.to_str().unwrap_or_default().to_string()); + todo!("Do TMDB API calls"); +} + +pub fn handle_media(entry: DirEntry) { + if entry.file_type().is_ok_and(|t| t.is_dir()) { + warn!("Directory passed to handle_media, {:#?} will be skipped", entry); + return + } + + match get_file_header(entry.path()) { + Ok(header) => { + // Handle video files + if infer::is_video(&header) { + video_file_handler(entry); + } + }, + Err(error) => error!("Can not get file header for {:#?}, Error: {:#?}", entry, error), + } +} \ No newline at end of file