doc: Adding some comments for documentation

This commit is contained in:
Andreas Mieke 2023-11-14 17:34:18 +01:00
parent 72baadbec7
commit 676c922a3a
6 changed files with 35 additions and 35 deletions

View file

@ -3,20 +3,24 @@ use inquire::{Text, CustomUserError, Autocomplete, autocompletion::Replacement};
use log::{warn, info, error}; use log::{warn, info, error};
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
// Struct to hold the config values
#[derive(Clone, Serialize, Deserialize, Debug)] #[derive(Clone, Serialize, Deserialize, Debug)]
pub struct Config { pub struct Config {
pub tmdb_key: String, pub tmdb_key: String,
pub plex_library: PathBuf, pub plex_library: PathBuf,
} }
// Load config, or trigger first run wizard
pub fn load(path: &PathBuf, first: bool) -> Result<Config, Box<dyn Error>> { pub fn load(path: &PathBuf, first: bool) -> Result<Config, Box<dyn Error>> {
if first { 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..."); info!("Running first run wizard...");
let cfg = first_run()?; let cfg = first_run()?;
save(cfg.clone(), path)?; save(cfg.clone(), path)?;
return Ok(cfg); return Ok(cfg);
} }
// Find and read config file, deserialise it into a config object
let f = fs::read_to_string(path); let f = fs::read_to_string(path);
let f = match f { let f = match f {
Ok(file) => file, Ok(file) => file,
@ -36,6 +40,7 @@ pub fn load(path: &PathBuf, first: bool) -> Result<Config, Box<dyn Error>> {
Ok(cfg) Ok(cfg)
} }
// First run wizard
pub fn first_run() -> Result<Config, Box<dyn Error>> { pub fn first_run() -> Result<Config, Box<dyn Error>> {
let tmdb_key = Text::new("Enter your TMDB API Read Access Token:") 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).") .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<Config, Box<dyn Error>> {
Ok(Config { tmdb_key: tmdb_key, plex_library: plex_library}) 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<dyn Error>> { pub fn save(cfg: Config, path: &PathBuf) -> Result<(), Box<dyn Error>> {
let serialized = serde_json::to_string_pretty(&cfg)?; let serialized = serde_json::to_string_pretty(&cfg)?;
fs::create_dir_all(path.parent().unwrap())?; fs::create_dir_all(path.parent().unwrap())?;

View file

@ -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}; 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 { // Search a given path for movies or shows
entry // TODO: Add support for single file as well
.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<PathBuf> {
let mut entries: Vec<PathBuf> = 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
}*/
pub fn search_path(path: PathBuf, cfg: Config, shows: bool) -> Result<Vec<Move>, Box<dyn Error>> { pub fn search_path(path: PathBuf, cfg: Config, shows: bool) -> Result<Vec<Move>, Box<dyn Error>> {
let entries = fs::read_dir(path.clone())?; let entries = fs::read_dir(path.clone())?;
let mut files: Vec<DirEntry> = Vec::new(); let mut files: Vec<DirEntry> = Vec::new();
@ -40,6 +24,7 @@ pub fn search_path(path: PathBuf, cfg: Config, shows: bool) -> Result<Vec<Move>,
} }
} }
// 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())); 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())); files.sort_by(|a, b| b.metadata().unwrap().len().cmp(&a.metadata().unwrap().len()));
trace!("Sorted Dirs: {:#?}", folders); trace!("Sorted Dirs: {:#?}", folders);
@ -47,14 +32,17 @@ pub fn search_path(path: PathBuf, cfg: Config, shows: bool) -> Result<Vec<Move>,
let mut moves: Vec<Move> = Vec::new(); let mut moves: Vec<Move> = Vec::new();
if shows { 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())); moves.append(&mut handle_show_files_and_folders(path, files, folders, cfg.clone()));
} else { } else {
// Find movies in directory or subdirectories, find extras
moves.append(&mut handle_movie_files_and_folders(files, folders, cfg.clone())); moves.append(&mut handle_movie_files_and_folders(files, folders, cfg.clone()));
} }
Ok(moves) Ok(moves)
} }
// Some lgecy documentation, rough description of the algorithm
/* /*
Look at current directory: Look at current directory:
Only directories, no media files -> Only directories, no media files ->

View file

@ -47,6 +47,7 @@ struct Args {
fn main() { fn main() {
let args = Args::parse(); let args = Args::parse();
// Initialise error logger to use `stderr` and verbosity/quiet mode from command line flags
stderrlog::new() stderrlog::new()
.module(module_path!()) .module(module_path!())
.quiet(args.quiet) .quiet(args.quiet)
@ -54,12 +55,7 @@ fn main() {
.init() .init()
.unwrap(); .unwrap();
trace!("trace message"); // Set config path config to home folder, or if provided to specified file
debug!("debug message");
info!("info message");
warn!("warn message");
error!("error message");
let config_path = if args.config.is_none() { let config_path = if args.config.is_none() {
PathBuf::from(std::env::var("HOME").unwrap()).join(".plex-media-ingest").join("config.json") PathBuf::from(std::env::var("HOME").unwrap()).join(".plex-media-ingest").join("config.json")
} else { } else {
@ -68,16 +64,19 @@ fn main() {
info!("Loading config from \"{}\"", config_path.to_str().unwrap()); 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(); let cfg = config::load(&config_path, args.first_run).unwrap();
info!("Found config: {:#?}", cfg); info!("Found config: {:#?}", cfg);
// Use either provided or current path as search path for movies/shows
let search_path = if args.path.is_none() { let search_path = if args.path.is_none() {
env::current_dir().unwrap() env::current_dir().unwrap()
} else { } else {
args.path.unwrap() 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(); let moves = directory::search_path(search_path, cfg, args.shows).unwrap();
for move_file in moves { 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();
} }

View file

@ -2,12 +2,14 @@ use std::{path::PathBuf, error::Error, fs::File, cmp, io::Read};
use log::trace; use log::trace;
// Struct holding two paths for the move/copy command
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Move { pub struct Move {
pub from: PathBuf, pub from: PathBuf,
pub to: PathBuf pub to: PathBuf
} }
// Extract the header/magic bytes from a file
pub fn get_file_header(path: PathBuf) -> Result<Vec<u8>, Box<dyn Error>> { pub fn get_file_header(path: PathBuf) -> Result<Vec<u8>, Box<dyn Error>> {
let f = File::open(path)?; let f = File::open(path)?;
@ -20,6 +22,7 @@ pub fn get_file_header(path: PathBuf) -> Result<Vec<u8>, Box<dyn Error>> {
Ok(bytes) Ok(bytes)
} }
// Check validity of a file-/foldername token (strip common torrent parts)
fn token_valid(t: &&str) -> bool { fn token_valid(t: &&str) -> bool {
if if
t.eq_ignore_ascii_case("dvd") || t.eq_ignore_ascii_case("dvd") ||
@ -64,6 +67,7 @@ fn token_valid(t: &&str) -> bool {
true 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<String> { pub fn tokenize_media_name(file_name: String) -> Vec<String> {
let tokens: Vec<String> = file_name.split(&['-', ' ', ':', '@', '.'][..]).filter(|t| token_valid(t)).map(String::from).collect(); let tokens: Vec<String> = file_name.split(&['-', ' ', ':', '@', '.'][..]).filter(|t| token_valid(t)).map(String::from).collect();
trace!("Tokens are: {:#?}", tokens); trace!("Tokens are: {:#?}", tokens);

View file

@ -11,12 +11,14 @@ use walkdir::WalkDir;
use crate::{config::Config, directory::search_path, media::{self, Move, get_file_header}}; use crate::{config::Config, directory::search_path, media::{self, Move, get_file_header}};
// Struct to hold the TMDB API response
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct TMDBResponse { struct TMDBResponse {
results: Vec<TMDBEntry>, results: Vec<TMDBEntry>,
total_results: i32 total_results: i32
} }
// Struct to hold a movie from the TMDB API response
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
struct TMDBEntry { struct TMDBEntry {
id: i32, id: i32,
@ -25,12 +27,14 @@ struct TMDBEntry {
release_date: Option<String>, release_date: Option<String>,
} }
// Display implementation for the inquire selection dialog
impl fmt::Display for TMDBEntry { impl fmt::Display for TMDBEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 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) 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<String>, cfg: Config) -> Option<TMDBEntry> { fn lookup_movie(file_name: PathBuf, mut name_tokens: Vec<String>, cfg: Config) -> Option<TMDBEntry> {
let mut h = HeaderMap::new(); let mut h = HeaderMap::new();
h.insert("Accept", HeaderValue::from_static("application/json")); h.insert("Accept", HeaderValue::from_static("application/json"));
@ -85,6 +89,7 @@ fn lookup_movie(file_name: PathBuf, mut name_tokens: Vec<String>, cfg: Config) -
} }
} }
// Handle single video file
fn movie_video_file_handler(entry: PathBuf, cfg: Config) -> Option<TMDBEntry> { fn movie_video_file_handler(entry: PathBuf, cfg: Config) -> Option<TMDBEntry> {
info!("Found movie video file: {:#?}", entry); info!("Found movie video file: {:#?}", entry);
@ -99,6 +104,7 @@ fn movie_video_file_handler(entry: PathBuf, cfg: Config) -> Option<TMDBEntry> {
lookup_movie(entry, name_tokens, cfg) 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<DirEntry>, folders: Vec<DirEntry>, cfg: Config) -> Vec<Move> { pub fn handle_movie_files_and_folders(files: Vec<DirEntry>, folders: Vec<DirEntry>, cfg: Config) -> Vec<Move> {
let mut moves: Vec<Move> = Vec::new(); let mut moves: Vec<Move> = Vec::new();
let mut primary_media: Option<TMDBEntry> = 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 let mut primary_media: Option<TMDBEntry> = 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<DirEntry>, folders: Vec<DirEntr
moves moves
} }
// Check files for movie, or if primary media has been marked as found for extras, show required inquire dialoges
fn check_movie_file(file: PathBuf, primary_media: &mut Option<TMDBEntry>, cfg: &Config, moves: &mut Vec<Move>) { fn check_movie_file(file: PathBuf, primary_media: &mut Option<TMDBEntry>, cfg: &Config, moves: &mut Vec<Move>) {
trace!("Checking {:#?}", file); trace!("Checking {:#?}", file);
match get_file_header(file.clone()) { match get_file_header(file.clone()) {

View file

@ -12,12 +12,14 @@ use regex::RegexBuilder;
use crate::{config::Config, media::{Move, self, get_file_header}, directory::search_path}; use crate::{config::Config, media::{Move, self, get_file_header}, directory::search_path};
// Struct to hold the TMDB API response
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct TMDBResponse { struct TMDBResponse {
results: Vec<TMDBEntry>, results: Vec<TMDBEntry>,
total_results: i32 total_results: i32
} }
// Struct to hold a show from the TMDB API response
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
struct TMDBEntry { struct TMDBEntry {
id: i32, id: i32,
@ -26,12 +28,14 @@ struct TMDBEntry {
first_air_date: Option<String>, first_air_date: Option<String>,
} }
// Display implementation for the inquire selection dialog
impl fmt::Display for TMDBEntry { impl fmt::Display for TMDBEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 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) 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<TMDBEntry> { fn check_show_name(entry: PathBuf, cfg: Config) -> Option<TMDBEntry> {
info!("Found folder: {:#?}", entry); info!("Found folder: {:#?}", entry);
@ -42,6 +46,7 @@ fn check_show_name(entry: PathBuf, cfg: Config) -> Option<TMDBEntry> {
lookup_show(entry, name_tokens, cfg) lookup_show(entry, name_tokens, cfg)
} }
// Look up show on the TMDB API
fn lookup_show(folder_name: PathBuf, mut name_tokens: Vec<String>, cfg: Config) -> Option<TMDBEntry> { fn lookup_show(folder_name: PathBuf, mut name_tokens: Vec<String>, cfg: Config) -> Option<TMDBEntry> {
if name_tokens.first().unwrap_or(&"".to_string()).eq_ignore_ascii_case("season") { if name_tokens.first().unwrap_or(&"".to_string()).eq_ignore_ascii_case("season") {
// Is a season folder most likely, skip useless TMDB requests // Is a season folder most likely, skip useless TMDB requests
@ -100,14 +105,13 @@ fn lookup_show(folder_name: PathBuf, mut name_tokens: Vec<String>, 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<DirEntry>, folders: Vec<DirEntry>, cfg: Config) -> Vec<Move> { pub fn handle_show_files_and_folders(directory: PathBuf, files: Vec<DirEntry>, folders: Vec<DirEntry>, cfg: Config) -> Vec<Move> {
let mut moves: Vec<Move> = Vec::new(); let mut moves: Vec<Move> = Vec::new();
let mut primary_media: Option<TMDBEntry>; let mut primary_media: Option<TMDBEntry>;
// Check current directory for possible name // Check current directory for possible name
primary_media = check_show_name(directory, cfg.clone()); primary_media = check_show_name(directory, cfg.clone());
//check_show_file(file.path(), &mut primary_media, &cfg, &mut moves);
match primary_media { match primary_media {
Some(_) => { Some(_) => {
// There is already primary media, check files and directories for more media for same show // 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<DirEntry>, f
moves moves
} }
// Check files for episodes or subtitles, show required inquire dialoges
fn check_show_file(file: PathBuf, primary_media: &mut Option<TMDBEntry>, cfg: &Config, moves: &mut Vec<Move>) { fn check_show_file(file: PathBuf, primary_media: &mut Option<TMDBEntry>, cfg: &Config, moves: &mut Vec<Move>) {
trace!("Checking {:#?}", file); trace!("Checking {:#?}", file);
match get_file_header(file.clone()) { match get_file_header(file.clone()) {