aboutsummaryrefslogtreecommitdiff
path: root/tool/src/add.rs
diff options
context:
space:
mode:
Diffstat (limited to 'tool/src/add.rs')
-rw-r--r--tool/src/add.rs181
1 files changed, 181 insertions, 0 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())
+ }
+}