/* 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 import; pub mod migrate; use base64::Engine; use clap::{Parser, Subcommand, ValueEnum}; use import::import; 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, }, /// 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, /// Path to the media of the node, required for non-series #[arg(short, long)] input: Option, /// Marks node as a series #[arg(short, long)] series: bool, }, Migrate { database: PathBuf, mode: MigrateMode, save_location: PathBuf, }, } #[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, } => { 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"))?; File::create_new(path.join("assets/front.htm"))? .write_fmt(format_args!("

My very own jellything instance

"))?; serde_yaml::to_writer( File::create_new(path.join("config.yaml"))?, &GlobalConfig { brand: brand.clone(), 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: NodeKind::Collection, title: "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), } } 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 } } }