aboutsummaryrefslogtreecommitdiff
path: root/tool/src
diff options
context:
space:
mode:
Diffstat (limited to 'tool/src')
-rw-r--r--tool/src/add.rs181
-rw-r--r--tool/src/main.rs75
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
-// }
-// }
-// }