feat(config): Add config loading/saving
Also adding a first time run wizard, which creates the config if it does not exist yet.
This commit is contained in:
commit
b7a47519b5
5 changed files with 1000 additions and 0 deletions
187
src/config.rs
Normal file
187
src/config.rs
Normal file
|
@ -0,0 +1,187 @@
|
|||
use std::{fs, error::Error, path::PathBuf, io::ErrorKind, str::FromStr};
|
||||
use inquire::{Text, CustomUserError, Autocomplete, autocompletion::Replacement};
|
||||
use log::warn;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct Config {
|
||||
pub tmdb_key: String,
|
||||
pub plex_library: PathBuf,
|
||||
}
|
||||
|
||||
pub fn load(path: &PathBuf) -> Result<Config, Box<dyn Error>> {
|
||||
let f = fs::read_to_string(path);
|
||||
let f = match f {
|
||||
Ok(file) => file,
|
||||
Err(e) => {
|
||||
if e.kind() == ErrorKind::NotFound {
|
||||
warn!("Config not found, assuming first run!");
|
||||
let cfg = first_run()?;
|
||||
save(cfg.clone(), path)?;
|
||||
return Ok(cfg);
|
||||
} else {
|
||||
panic!("There was an error reading the config file!");
|
||||
}
|
||||
}
|
||||
};
|
||||
let cfg: Config = serde_json::from_str(&f)?;
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
pub fn first_run() -> Result<Config, Box<dyn Error>> {
|
||||
let tmdb_key = Text::new("Enter your TMDB API Key:")
|
||||
.with_help_message("The API key can be found at https://www.themoviedb.org/settings/api (you must be logged in).")
|
||||
.prompt();
|
||||
|
||||
let tmdb_key = match tmdb_key {
|
||||
Ok(tmdb_key) => tmdb_key,
|
||||
Err(e) => panic!("Error retrieving TMDB key from inquire: {}", e.to_string())
|
||||
};
|
||||
|
||||
let plex_library = Text::new("Enter your Plex Media Library path:")
|
||||
.with_help_message("Enter the full path of your Plex Media Library, or the path you plan to use for it.")
|
||||
.with_autocomplete(FilePathCompleter::default())
|
||||
.prompt();
|
||||
|
||||
let plex_library = match plex_library {
|
||||
Ok(plex_library) => plex_library,
|
||||
Err(e) => panic!("Error retrieving Plex Library from inquire: {}", e.to_string())
|
||||
};
|
||||
|
||||
let plex_library = match PathBuf::from_str(&plex_library) {
|
||||
Ok(plex_library) => plex_library,
|
||||
Err(e) => panic!("Path is not valid: {}", e.to_string())
|
||||
};
|
||||
|
||||
Ok(Config { tmdb_key: tmdb_key, plex_library: plex_library})
|
||||
}
|
||||
|
||||
pub fn save(cfg: Config, path: &PathBuf) -> Result<(), Box<dyn Error>> {
|
||||
let serialized = serde_json::to_string_pretty(&cfg)?;
|
||||
fs::create_dir_all(path.parent().unwrap())?;
|
||||
fs::write(path, serialized)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/*
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; ;;
|
||||
;; ----==| A U T O C O M P L E T E P A T H |==---- ;;
|
||||
;; ;;
|
||||
;; From https://github.com/mikaelmello/inquire/blob/main/inquire/examples/complex_autocompletion.rs ;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
*/
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct FilePathCompleter {
|
||||
input: String,
|
||||
paths: Vec<String>,
|
||||
lcp: String,
|
||||
}
|
||||
|
||||
impl FilePathCompleter {
|
||||
fn update_input(&mut self, input: &str) -> Result<(), CustomUserError> {
|
||||
if input == self.input {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.input = input.to_owned();
|
||||
self.paths.clear();
|
||||
|
||||
let input_path = std::path::PathBuf::from(input);
|
||||
|
||||
let fallback_parent = input_path
|
||||
.parent()
|
||||
.map(|p| {
|
||||
if p.to_string_lossy() == "" {
|
||||
std::path::PathBuf::from(".")
|
||||
} else {
|
||||
p.to_owned()
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."));
|
||||
|
||||
let scan_dir = if input.ends_with('/') {
|
||||
input_path
|
||||
} else {
|
||||
fallback_parent.clone()
|
||||
};
|
||||
|
||||
let entries = match std::fs::read_dir(scan_dir) {
|
||||
Ok(read_dir) => Ok(read_dir),
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => std::fs::read_dir(fallback_parent),
|
||||
Err(err) => Err(err),
|
||||
}?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let mut idx = 0;
|
||||
let limit = 15;
|
||||
|
||||
while idx < entries.len() && self.paths.len() < limit {
|
||||
let entry = entries.get(idx).unwrap();
|
||||
|
||||
let path = entry.path();
|
||||
let path_str = if path.is_dir() {
|
||||
format!("{}/", path.to_string_lossy())
|
||||
} else {
|
||||
path.to_string_lossy().to_string()
|
||||
};
|
||||
|
||||
if path_str.starts_with(&self.input) && path_str.len() != self.input.len() {
|
||||
self.paths.push(path_str);
|
||||
}
|
||||
|
||||
idx = idx.saturating_add(1);
|
||||
}
|
||||
|
||||
self.lcp = self.longest_common_prefix();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn longest_common_prefix(&self) -> String {
|
||||
let mut ret: String = String::new();
|
||||
|
||||
let mut sorted = self.paths.clone();
|
||||
sorted.sort();
|
||||
if sorted.is_empty() {
|
||||
return ret;
|
||||
}
|
||||
|
||||
let mut first_word = sorted.first().unwrap().chars();
|
||||
let mut last_word = sorted.last().unwrap().chars();
|
||||
|
||||
loop {
|
||||
match (first_word.next(), last_word.next()) {
|
||||
(Some(c1), Some(c2)) if c1 == c2 => {
|
||||
ret.push(c1);
|
||||
}
|
||||
_ => return ret,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Autocomplete for FilePathCompleter {
|
||||
fn get_suggestions(&mut self, input: &str) -> Result<Vec<String>, CustomUserError> {
|
||||
self.update_input(input)?;
|
||||
|
||||
Ok(self.paths.clone())
|
||||
}
|
||||
|
||||
fn get_completion(
|
||||
&mut self,
|
||||
input: &str,
|
||||
highlighted_suggestion: Option<String>,
|
||||
) -> Result<Replacement, CustomUserError> {
|
||||
self.update_input(input)?;
|
||||
|
||||
Ok(match highlighted_suggestion {
|
||||
Some(suggestion) => Replacement::Some(suggestion),
|
||||
None => match self.lcp.is_empty() {
|
||||
true => Replacement::None,
|
||||
false => Replacement::Some(self.lcp.clone()),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
58
src/main.rs
Normal file
58
src/main.rs
Normal file
|
@ -0,0 +1,58 @@
|
|||
mod config;
|
||||
|
||||
use log::*;
|
||||
use clap::Parser;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Args {
|
||||
/// Quiet mode
|
||||
#[arg(short, long)]
|
||||
quiet: bool,
|
||||
|
||||
/// Verbosity
|
||||
#[arg(short, long, action = clap::ArgAction::Count)]
|
||||
verbose: u8,
|
||||
|
||||
/// First run mode
|
||||
#[arg(short, long)]
|
||||
first_run: bool,
|
||||
|
||||
/// Custom config file
|
||||
#[arg(short, long, value_name = "FILE")]
|
||||
config: Option<PathBuf>,
|
||||
|
||||
/// Path to look for media in
|
||||
path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = Args::parse();
|
||||
|
||||
stderrlog::new()
|
||||
.module(module_path!())
|
||||
.quiet(args.quiet)
|
||||
.verbosity(args.verbose as usize + 1)
|
||||
.init()
|
||||
.unwrap();
|
||||
|
||||
trace!("trace message");
|
||||
debug!("debug message");
|
||||
info!("info message");
|
||||
warn!("warn message");
|
||||
error!("error message");
|
||||
|
||||
let config_path = if args.config.is_none() {
|
||||
PathBuf::from(std::env::var("HOME").unwrap()).join(".plex-media-ingest").join("config.json")
|
||||
} else {
|
||||
args.config.unwrap()
|
||||
};
|
||||
|
||||
info!("Loading config from \"{}\"", config_path.to_str().unwrap());
|
||||
|
||||
let cfg = config::load(&config_path).unwrap();
|
||||
|
||||
info!("Found config: {:#?}", cfg);
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue