feat(media): Implement directory listing
Work on tokenizing file names for TMDB API calls in future.
This commit is contained in:
parent
2b47cf5321
commit
cb687fa808
39
Cargo.lock
generated
39
Cargo.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
|
@ -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)
|
||||||
|
*/
|
10
src/main.rs
10
src/main.rs
|
@ -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
109
src/media.rs
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue