feat(media): Implement directory listing

Work on tokenizing file names for TMDB API calls in future.
This commit is contained in:
Andreas Mieke 2023-11-05 17:19:01 +01:00
parent 2b47cf5321
commit cb687fa808
5 changed files with 210 additions and 9 deletions

39
Cargo.lock generated
View file

@ -94,6 +94,12 @@ version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.83" version = "1.0.83"
@ -103,6 +109,17 @@ dependencies = [
"libc", "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]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
@ -206,6 +223,12 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd" checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.4.1" version = "0.4.1"
@ -244,6 +267,15 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "infer"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb33622da908807a06f9513c19b3c1ad50fab3e4137d82a78107d502075aa199"
dependencies = [
"cfb",
]
[[package]] [[package]]
name = "inquire" name = "inquire"
version = "0.6.2" version = "0.6.2"
@ -367,6 +399,7 @@ name = "plex-media-ingest"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"clap", "clap",
"infer",
"inquire", "inquire",
"log", "log",
"serde", "serde",
@ -583,6 +616,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc"
[[package]] [[package]]
name = "walkdir" name = "walkdir"
version = "2.4.0" version = "2.4.0"

View file

@ -7,6 +7,7 @@ edition = "2021"
[dependencies] [dependencies]
clap = { version = "4.4.7", features = ["derive"] } clap = { version = "4.4.7", features = ["derive"] }
infer = "0.15.0"
inquire = "0.6.2" inquire = "0.6.2"
log = "0.4.20" log = "0.4.20"
serde = { version = "1.0.190", features = ["derive"] } serde = { version = "1.0.190", features = ["derive"] }

View file

@ -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 entry
.file_name() .file_name()
.to_str() .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) .unwrap_or(false)
} }
pub fn list_files(path: PathBuf) -> Vec<PathBuf>{ pub fn walk_path(path: PathBuf) -> Vec<PathBuf> {
let mut entries: Vec<PathBuf> = vec![]; let mut entries: Vec<PathBuf> = vec![];
WalkDir::new(path) WalkDir::new(path)
.into_iter() .into_iter()
@ -18,4 +18,52 @@ pub fn list_files(path: PathBuf) -> Vec<PathBuf>{
.filter_map(|v| v.ok()) .filter_map(|v| v.ok())
.for_each(|x| entries.push(x.into_path())); .for_each(|x| entries.push(x.into_path()));
entries entries
}*/
pub fn search_path(path: PathBuf) -> Result<(), Box<dyn Error>> {
let entries = fs::read_dir(path)?;
let mut files: Vec<DirEntry> = Vec::new();
let mut dirs: Vec<DirEntry> = 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)
*/

View file

@ -1,5 +1,6 @@
mod config; mod config;
mod directory; mod directory;
mod media;
use log::*; use log::*;
use clap::Parser; use clap::Parser;
@ -62,10 +63,13 @@ fn main() {
args.path.unwrap() 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()); info!("Found: {}", file.to_str().unwrap());
} }*/
//search_media(files).unwrap();
} }

109
src/media.rs Normal file
View file

@ -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<Vec<u8>, Box<dyn Error>> {
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),
}
}