diff options
Diffstat (limited to 'tool/src')
-rw-r--r-- | tool/src/add.rs | 181 | ||||
-rw-r--r-- | tool/src/main.rs | 75 |
2 files changed, 195 insertions, 61 deletions
diff --git a/tool/src/add.rs b/tool/src/add.rs new file mode 100644 index 0000000..9cb1180 --- /dev/null +++ b/tool/src/add.rs @@ -0,0 +1,181 @@ +use std::{ + fmt::Display, + path::{Path, PathBuf}, +}; + +use crate::Action; +use anyhow::{anyhow, bail, Context}; +use dialoguer::{theme::ColorfulTheme, Confirm, FuzzySelect, Input, MultiSelect}; +use jellybase::{CONF, SECRETS}; +use jellycommon::{AssetLocation, ImportOptions, ImportSource, TraktKind}; +use jellyimport::trakt::Trakt; +use tokio::{fs::File, io::AsyncWriteExt}; + +pub(crate) async fn add(action: Action) -> anyhow::Result<()> { + match action { + Action::Add { + id, + media, + library_path, + } => { + let theme = ColorfulTheme::default(); + + let possible_kinds = [ + TraktKind::Movie, + TraktKind::Season, + TraktKind::Show, + TraktKind::Episode, + ]; + let trakt_kind: Vec<usize> = MultiSelect::with_theme(&theme) + .with_prompt("Media Kind") + .items(&possible_kinds) + .defaults(&[true, false, false, false]) + .interact() + .unwrap(); + let search_kinds = trakt_kind + .iter() + .map(|&i| possible_kinds[i]) + .collect::<Vec<_>>(); + + let library_path = if let Some(library_path) = library_path { + library_path + } else { + let mut directories = Vec::new(); + find_folders(&CONF.library_path, &PathBuf::new(), &mut directories) + .context("listing library directories")?; + + let target_dir_index = FuzzySelect::with_theme(&theme) + .items(&directories) + .interact() + .unwrap(); + directories[target_dir_index].0.clone() + }; + + let (last_search, trakt_object, trakt_kind) = loop { + let name: String = Input::with_theme(&theme) + .with_prompt("Search by title") + .default(media.as_ref().map(path_to_query).unwrap_or_default()) + .interact_text() + .unwrap(); + + let trakt = Trakt::new( + SECRETS + .api + .trakt + .as_ref() + .ok_or(anyhow!("no trakt api key configured"))?, + ); + + let results = trakt.search(&search_kinds, &name, false).await?; + + let correct = FuzzySelect::with_theme(&theme) + .items(&results) + .with_prompt("Metadata Source") + .interact_opt() + .unwrap(); + + if let Some(o) = correct { + break (name, results[o].inner.inner().to_owned(), results[o].r#type); + } + }; + + let id = id.unwrap_or_else(|| { + trakt_object.ids.slug.unwrap_or_else(|| { + let o: String = Input::with_theme(&theme) + .with_prompt("Node ID") + .validate_with(validate_id) + .default(make_id(&last_search)) + .interact_text() + .unwrap(); + o + }) + }); + + let mut sources = Vec::new(); + sources.push(ImportSource::Trakt { + id: trakt_object.ids.trakt, + kind: trakt_kind, + }); + if let Some(media) = media { + sources.push(ImportSource::Media { + location: AssetLocation::Media(media), + ignore_metadata: true, + ignore_attachments: false, + ignore_chapters: false, + }) + } + + let impo = ImportOptions { id, sources }; + + let ypath = CONF + .library_path + .join(library_path) + .join(&impo.id) + .with_extension("yaml"); + + if Confirm::with_theme(&theme) + .with_prompt(format!("Write {:?}?", ypath)) + .interact() + .unwrap() + { + File::create(ypath) + .await? + .write_all(serde_yaml::to_string(&impo)?.as_bytes()) + .await?; + } + + Ok(()) + } + _ => unreachable!(), + } +} + +fn validate_id(s: &String) -> anyhow::Result<()> { + if &make_id(&s) == s { + Ok(()) + } else { + bail!("invalid id") + } +} +fn make_id(s: &str) -> String { + let mut out = String::new(); + for s in s.chars() { + match s { + 'a'..='z' | '0'..='9' => out.push(s), + 'A'..='Z' => out.push(s.to_ascii_lowercase()), + '-' | ' ' | '_' | ':' => out.push('-'), + _ => (), + } + } + out +} + +fn path_to_query(path: &PathBuf) -> String { + path.file_stem() + .unwrap() + .to_str() + .unwrap() + .to_string() + .replace("-", " ") + .replace(".", " ") +} + +fn find_folders(base: &Path, path: &Path, out: &mut Vec<PathDisplay>) -> anyhow::Result<()> { + out.push(PathDisplay(path.to_owned())); + for entry in base.join(path).read_dir()? { + let entry = entry?; + let child_path = path.join(entry.file_name()); + if entry.path().is_dir() { + find_folders(base, &child_path, out)?; + } + } + Ok(()) +} + +pub struct PathDisplay(PathBuf); +impl Display for PathDisplay { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("/")?; + f.write_str(self.0.to_str().unwrap()) + } +} diff --git a/tool/src/main.rs b/tool/src/main.rs index 58559dc..68513d1 100644 --- a/tool/src/main.rs +++ b/tool/src/main.rs @@ -3,14 +3,17 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2023 metamuffin <metamuffin.org> */ + +pub mod add; pub mod migrate; +use add::add; use anyhow::anyhow; use clap::{Parser, Subcommand, ValueEnum}; use jellybase::{CONF, SECRETS}; use jellyclient::Instance; use jellycommon::user::CreateSessionParams; -use log::{error, info}; +use log::info; use migrate::migrate; use std::{fmt::Debug, path::PathBuf}; @@ -25,14 +28,13 @@ struct Args { #[derive(Subcommand)] enum Action { - /// Initialize a new jellything instance - Init { - /// Base path of the instance, must either be absolute or relative to the servers pwd - base_path: PathBuf, + Add { + #[arg(short, long)] + id: Option<String>, #[arg(short, long)] - brand: String, + media: Option<PathBuf>, #[arg(short, long)] - hostname: String, + library_path: Option<PathBuf>, }, Migrate { database: PathBuf, @@ -63,50 +65,11 @@ fn main() -> anyhow::Result<()> { let args = Args::parse(); match args.action { - Action::Init { .. } => { - // info!("creating new instance..."); - // std::fs::create_dir_all(path.join("library"))?; - // std::fs::create_dir_all(path.join("cache"))?; - // std::fs::create_dir_all(path.join("assets"))?; - // std::fs::create_dir_all(path.join("media"))?; - // File::create_new(path.join("assets/front.htm"))? - // .write_fmt(format_args!("<h1>My very own jellything instance</h1>"))?; - - // // TODO: dont fill that - // serde_yaml::to_writer( - // File::create_new(path.join("config.yaml"))?, - // &GlobalConfig { - // brand: brand.clone(), - // hostname, - // slogan: "Creative slogan here".to_string(), - // asset_path: path.join("assets"), - // cache_path: path.join("cache"), - // library_path: path.join("library"), - // database_path: path.join("database"), - // temp_path: "/tmp".into(), - - // login_expire: 10, - // ..Default::default() - // }, - // )?; - // serde_json::to_writer( - // File::create_new(path.join("library/directory.json"))?, - // &Node { - // public: NodePublic { - // kind: Some(NodeKind::Collection), - // title: Some("My Library".to_string()), - // ..Default::default() - // }, - // private: NodePrivate { - // ..Default::default() - // }, - // }, - // )?; - // info!("{brand:?} is ready!"); - // warn!("please add an admin password to login."); - error!("init is currently disabled"); - Ok(()) - } + a @ Action::Add { .. } => tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() + .block_on(add(a)), a @ Action::Migrate { .. } => migrate(a), Action::Reimport { hostname, no_tls } => tokio::runtime::Builder::new_multi_thread() .enable_all() @@ -135,13 +98,3 @@ fn main() -> anyhow::Result<()> { }), } } - -// fn ok_or_warn<T, E: Debug>(r: Result<T, E>) -> Option<T> { -// match r { -// Ok(t) => Some(t), -// Err(e) => { -// warn!("{e:?}"); -// None -// } -// } -// } |