plex-media-ingest/src/config.rs
2023-11-14 17:34:18 +01:00

201 lines
No EOL
6.4 KiB
Rust

use std::{fs, error::Error, path::PathBuf, io::ErrorKind, str::FromStr};
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<Config, Box<dyn Error>> {
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,
Err(e) => {
if e.kind() == ErrorKind::NotFound {
warn!("Config not found, running first run wizard...");
let cfg = first_run()?;
save(cfg.clone(), path)?;
return Ok(cfg);
} else {
error!("There was an error reading the config file!");
return Err(Box::new(e));
}
}
};
let cfg: Config = serde_json::from_str(&f)?;
Ok(cfg)
}
// First run wizard
pub fn first_run() -> Result<Config, Box<dyn Error>> {
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).")
.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})
}
// Serialise and save config object to disk
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()),
},
})
}
}