/* This file is part of jellything (https://codeberg.org/metamuffin/jellything) which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2023 metamuffin */ #![feature(file_create_new)] pub mod migrate; use base64::Engine; use clap::{Parser, Subcommand, ValueEnum}; use jellyclient::{Instance, LoginDetails}; use jellycommon::{config::GlobalConfig, Node, NodeKind, NodePrivate, NodePublic}; use log::{info, warn}; use migrate::migrate; use rand::random; use std::{fmt::Debug, fs::File, io::Write, path::PathBuf}; #[derive(Parser)] struct Args { #[arg(short = 'N', long)] dry: bool, #[clap(subcommand)] action: Action, } #[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, #[arg(short, long)] brand: String, #[arg(short, long)] hostname: String, }, // /// Imports a movie, video or series given media and metadata sources // New { // /// Relative path to the node's parent(!). // path: PathBuf, // /// Search the node by title on TMDB // #[arg(short = 't', long)] // tmdb_search: Option, // /// Search the node by id on TMDB // #[arg(short = 'T', long)] // tmdb_id: Option, // #[arg(long)] // /// Prefix the inferred id with something to avoid collisions // ident_prefix: Option, // /// Copies media into the library // #[arg(long)] // copy: bool, // /// Moves media into the library (potentially destructive operation) // #[arg(long)] // r#move: bool, // /// Marks node as a video // #[arg(long)] // video: bool, // /// Marks node as a series // #[arg(short, long)] // series: bool, // /// Path to the media of the node, required for non-series // #[arg(short, long)] // input: Option, // /// Ignore attachments (dont use them as cover) // #[arg(long)] // ignore_attachments: bool, // /// Ignore metadate (no title, description and tagline from input) // #[arg(long)] // ignore_metadata: bool, // /// Skip any action that appears to be run already. // #[arg(long)] // skip_existing: bool, // /// Sets the title // #[arg(long)] // title: Option, // }, Migrate { database: PathBuf, mode: MigrateMode, save_location: PathBuf, }, Reimport { /// Path to global jellything config config: PathBuf, /// Custom hostname, the config's is used by default #[arg(long)] hostname: Option, /// Disable TLS. Dont use this. #[arg(long)] no_tls: bool, }, } #[derive(Debug, Clone, Copy, PartialEq, ValueEnum)] enum MigrateMode { Import, Export, } fn main() -> anyhow::Result<()> { env_logger::builder() .filter_level(log::LevelFilter::Info) .parse_env("LOG") .init(); let args = Args::parse(); match args.action { Action::Init { base_path: path, brand, hostname, } => { 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!("

My very own jellything instance

"))?; // 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(), cookie_key: Some( base64::engine::general_purpose::STANDARD .encode([(); 32].map(|_| random())), ), session_key: Some( base64::engine::general_purpose::STANDARD .encode([(); 32].map(|_| random())), ), admin_username: "admin".to_string(), admin_password: "hackme".to_string(), 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 change the admin password."); Ok(()) } // a @ Action::New { .. } => import(a, args.dry), a @ Action::Migrate { .. } => migrate(a), Action::Reimport { config, hostname, no_tls, } => tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .unwrap() .block_on(async move { let config = serde_yaml::from_reader::<_, GlobalConfig>(File::open(config)?)?; let inst = Instance::new(hostname.unwrap_or(config.hostname.clone()), !no_tls); info!("login"); let session = inst .login(LoginDetails { drop_permissions: None, expire: None, password: config.admin_password, username: config.admin_username, }) .await?; session.reimport().await?; Ok(()) }), } } fn make_ident(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 ok_or_warn(r: Result) -> Option { match r { Ok(t) => Some(t), Err(e) => { warn!("{e:?}"); None } } }