diff --git a/src/config.rs b/src/config.rs index c4e2c70..aadc294 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,20 +3,24 @@ use inquire::{Text, CustomUserError, Autocomplete, autocompletion::Replacement}; use log::{warn, info, error}; use serde::{Serialize, Deserialize}; +// Struct to hold the config values #[derive(Clone, Serialize, Deserialize, Debug)] pub struct Config { pub tmdb_key: String, pub plex_library: PathBuf, } +// Load config, or trigger first run wizard pub fn load(path: &PathBuf, first: bool) -> Result> { if first { + // If first run wizard should be re-run don't bother with the existing config, run wizard and save it info!("Running first run wizard..."); let cfg = first_run()?; save(cfg.clone(), path)?; return Ok(cfg); } + // Find and read config file, deserialise it into a config object let f = fs::read_to_string(path); let f = match f { Ok(file) => file, @@ -36,6 +40,7 @@ pub fn load(path: &PathBuf, first: bool) -> Result> { Ok(cfg) } +// First run wizard pub fn first_run() -> Result> { let tmdb_key = Text::new("Enter your TMDB API Read Access Token:") .with_help_message("The API key can be found at https://www.themoviedb.org/settings/api (you must be logged in).") @@ -64,6 +69,7 @@ pub fn first_run() -> Result> { Ok(Config { tmdb_key: tmdb_key, plex_library: plex_library}) } +// Serialise and save config object to disk pub fn save(cfg: Config, path: &PathBuf) -> Result<(), Box> { let serialized = serde_json::to_string_pretty(&cfg)?; fs::create_dir_all(path.parent().unwrap())?; diff --git a/src/directory.rs b/src/directory.rs index 5abf2ef..ed44f3e 100644 --- a/src/directory.rs +++ b/src/directory.rs @@ -4,24 +4,8 @@ use log::trace; use crate::{movie::handle_movie_files_and_folders, config::Config, media::Move, show::handle_show_files_and_folders}; -/*fn is_not_hidden(entry: &DirEntry) -> bool { - entry - .file_name() - .to_str() - .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 walk_path(path: PathBuf) -> Vec { - let mut entries: Vec = vec![]; - WalkDir::new(path) - .into_iter() - .filter_entry(|e| is_not_hidden(e)) - .filter_map(|v| v.ok()) - .for_each(|x| entries.push(x.into_path())); - entries -}*/ - +// Search a given path for movies or shows +// TODO: Add support for single file as well pub fn search_path(path: PathBuf, cfg: Config, shows: bool) -> Result, Box> { let entries = fs::read_dir(path.clone())?; let mut files: Vec = Vec::new(); @@ -40,6 +24,7 @@ pub fn search_path(path: PathBuf, cfg: Config, shows: bool) -> Result, } } + // Sort the files and directory vectors by size, so the main movie file (the biggest usually) is the first folders.sort_by(|a, b| b.metadata().unwrap().len().cmp(&a.metadata().unwrap().len())); files.sort_by(|a, b| b.metadata().unwrap().len().cmp(&a.metadata().unwrap().len())); trace!("Sorted Dirs: {:#?}", folders); @@ -47,14 +32,17 @@ pub fn search_path(path: PathBuf, cfg: Config, shows: bool) -> Result, let mut moves: Vec = Vec::new(); if shows { + // Find shows in directory (only one show per run supported right now) moves.append(&mut handle_show_files_and_folders(path, files, folders, cfg.clone())); } else { + // Find movies in directory or subdirectories, find extras moves.append(&mut handle_movie_files_and_folders(files, folders, cfg.clone())); } Ok(moves) } +// Some lgecy documentation, rough description of the algorithm /* Look at current directory: Only directories, no media files -> diff --git a/src/main.rs b/src/main.rs index e77ba56..1c85a6f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -47,6 +47,7 @@ struct Args { fn main() { let args = Args::parse(); + // Initialise error logger to use `stderr` and verbosity/quiet mode from command line flags stderrlog::new() .module(module_path!()) .quiet(args.quiet) @@ -54,12 +55,7 @@ fn main() { .init() .unwrap(); - trace!("trace message"); - debug!("debug message"); - info!("info message"); - warn!("warn message"); - error!("error message"); - + // Set config path config to home folder, or if provided to specified file let config_path = if args.config.is_none() { PathBuf::from(std::env::var("HOME").unwrap()).join(".plex-media-ingest").join("config.json") } else { @@ -68,16 +64,19 @@ fn main() { info!("Loading config from \"{}\"", config_path.to_str().unwrap()); + // Read config, or run first run wizard and write config, if none can be found let cfg = config::load(&config_path, args.first_run).unwrap(); info!("Found config: {:#?}", cfg); + // Use either provided or current path as search path for movies/shows let search_path = if args.path.is_none() { env::current_dir().unwrap() } else { args.path.unwrap() }; + // Search path and put everything in vector to hold all the file moves (or copies) let moves = directory::search_path(search_path, cfg, args.shows).unwrap(); for move_file in moves { @@ -117,13 +116,4 @@ fn main() { } } } - - //let files = directory::walk_path(search_path); - - /*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 index 5cc7111..bc9f7b4 100644 --- a/src/media.rs +++ b/src/media.rs @@ -2,12 +2,14 @@ use std::{path::PathBuf, error::Error, fs::File, cmp, io::Read}; use log::trace; +// Struct holding two paths for the move/copy command #[derive(Debug, Clone)] pub struct Move { pub from: PathBuf, pub to: PathBuf } +// Extract the header/magic bytes from a file pub fn get_file_header(path: PathBuf) -> Result, Box> { let f = File::open(path)?; @@ -20,6 +22,7 @@ pub fn get_file_header(path: PathBuf) -> Result, Box> { Ok(bytes) } +// Check validity of a file-/foldername token (strip common torrent parts) fn token_valid(t: &&str) -> bool { if t.eq_ignore_ascii_case("dvd") || @@ -64,6 +67,7 @@ fn token_valid(t: &&str) -> bool { true } +// Separate file-/foldernames into a vector of tokens, stripping of whitespace or other separation characters pub fn tokenize_media_name(file_name: String) -> Vec { let tokens: Vec = file_name.split(&['-', ' ', ':', '@', '.'][..]).filter(|t| token_valid(t)).map(String::from).collect(); trace!("Tokens are: {:#?}", tokens); diff --git a/src/movie.rs b/src/movie.rs index 9a088ea..325f459 100644 --- a/src/movie.rs +++ b/src/movie.rs @@ -11,12 +11,14 @@ use walkdir::WalkDir; use crate::{config::Config, directory::search_path, media::{self, Move, get_file_header}}; +// Struct to hold the TMDB API response #[derive(Deserialize, Debug)] struct TMDBResponse { results: Vec, total_results: i32 } +// Struct to hold a movie from the TMDB API response #[derive(Deserialize, Debug, Clone)] struct TMDBEntry { id: i32, @@ -25,12 +27,14 @@ struct TMDBEntry { release_date: Option, } +// Display implementation for the inquire selection dialog impl fmt::Display for TMDBEntry { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} ({}, {}) (ID: {})", self.title, self.release_date.clone().unwrap_or("unknown".to_string()), self.original_language.as_ref().unwrap(), self.id) } } +// Look up movie on the TMDB API fn lookup_movie(file_name: PathBuf, mut name_tokens: Vec, cfg: Config) -> Option { let mut h = HeaderMap::new(); h.insert("Accept", HeaderValue::from_static("application/json")); @@ -85,6 +89,7 @@ fn lookup_movie(file_name: PathBuf, mut name_tokens: Vec, cfg: Config) - } } +// Handle single video file fn movie_video_file_handler(entry: PathBuf, cfg: Config) -> Option { info!("Found movie video file: {:#?}", entry); @@ -99,6 +104,7 @@ fn movie_video_file_handler(entry: PathBuf, cfg: Config) -> Option { lookup_movie(entry, name_tokens, cfg) } +// Handler for the sorted vectors of files and folders, gets called recursively for subfolders, if no primary media can be found pub fn handle_movie_files_and_folders(files: Vec, folders: Vec, cfg: Config) -> Vec { let mut moves: Vec = Vec::new(); let mut primary_media: Option = None; // Assuming first file (biggest file) is primary media, store the information of this, for the rest, do lazy matching for extra content/subs and so on @@ -140,6 +146,7 @@ pub fn handle_movie_files_and_folders(files: Vec, folders: Vec, cfg: &Config, moves: &mut Vec) { trace!("Checking {:#?}", file); match get_file_header(file.clone()) { diff --git a/src/show.rs b/src/show.rs index f08de8c..7ea7b77 100644 --- a/src/show.rs +++ b/src/show.rs @@ -12,12 +12,14 @@ use regex::RegexBuilder; use crate::{config::Config, media::{Move, self, get_file_header}, directory::search_path}; +// Struct to hold the TMDB API response #[derive(Deserialize, Debug)] struct TMDBResponse { results: Vec, total_results: i32 } +// Struct to hold a show from the TMDB API response #[derive(Deserialize, Debug, Clone)] struct TMDBEntry { id: i32, @@ -26,12 +28,14 @@ struct TMDBEntry { first_air_date: Option, } +// Display implementation for the inquire selection dialog impl fmt::Display for TMDBEntry { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} ({}, {}) (ID: {})", self.name, self.first_air_date.clone().unwrap_or("unknown".to_string()), self.original_language.as_ref().unwrap(), self.id) } } +// Use directory name to find out show name, as opposed to file name for movies fn check_show_name(entry: PathBuf, cfg: Config) -> Option { info!("Found folder: {:#?}", entry); @@ -42,6 +46,7 @@ fn check_show_name(entry: PathBuf, cfg: Config) -> Option { lookup_show(entry, name_tokens, cfg) } +// Look up show on the TMDB API fn lookup_show(folder_name: PathBuf, mut name_tokens: Vec, cfg: Config) -> Option { if name_tokens.first().unwrap_or(&"".to_string()).eq_ignore_ascii_case("season") { // Is a season folder most likely, skip useless TMDB requests @@ -100,14 +105,13 @@ fn lookup_show(folder_name: PathBuf, mut name_tokens: Vec, cfg: Config) } } +// Handler for the sorted vectors of files and folders, gets called recursively for subfolders, if no primary media can be found pub fn handle_show_files_and_folders(directory: PathBuf, files: Vec, folders: Vec, cfg: Config) -> Vec { let mut moves: Vec = Vec::new(); let mut primary_media: Option; // Check current directory for possible name primary_media = check_show_name(directory, cfg.clone()); - - //check_show_file(file.path(), &mut primary_media, &cfg, &mut moves); match primary_media { Some(_) => { // There is already primary media, check files and directories for more media for same show @@ -148,6 +152,7 @@ pub fn handle_show_files_and_folders(directory: PathBuf, files: Vec, f moves } +// Check files for episodes or subtitles, show required inquire dialoges fn check_show_file(file: PathBuf, primary_media: &mut Option, cfg: &Config, moves: &mut Vec) { trace!("Checking {:#?}", file); match get_file_header(file.clone()) {