diff options
Diffstat (limited to 'import/src/main.rs')
-rw-r--r-- | import/src/main.rs | 415 |
1 files changed, 0 insertions, 415 deletions
diff --git a/import/src/main.rs b/import/src/main.rs deleted file mode 100644 index c274e54..0000000 --- a/import/src/main.rs +++ /dev/null @@ -1,415 +0,0 @@ -/* - 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 <metamuffin.org> -*/ -#![feature(file_create_new)] - -pub mod infojson; -pub mod tmdb; - -use anyhow::Context; -use base64::Engine; -use clap::{Parser, Subcommand}; -use infojson::{parse_upload_date, YVideo}; -use jellycommon::{ - config::GlobalConfig, AssetLocation, LocalTrack, MediaInfo, MediaSource, Node, NodeKind, - NodePrivate, NodePublic, Rating, -}; -use jellymatroska::read::EbmlReader; -use jellyremuxer::import::import_metadata; -use log::{info, warn}; -use rand::random; -use std::{ - collections::BTreeMap, - fmt::Debug, - fs::{remove_file, File}, - io::{stdin, BufReader, Write}, - path::PathBuf, -}; -use tmdb::{tmdb_details, tmdb_image, tmdb_search}; - -#[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<String>, - /// Search the node by id on TMDB - #[arg(short = 'T', long)] - tmdb_id: Option<String>, - #[arg(long)] - /// Prefix the inferred id with something to avoid collisions - ident_prefix: Option<String>, - /// 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<PathBuf>, - /// Marks node as a series - #[arg(short, long)] - series: bool, - }, -} - -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!("<h1>My very own jellything instance</h1>"))?; - 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(()) - } - Action::New { - path, - tmdb_id, - tmdb_search, - input, - series, - ident_prefix, - copy, - video, - r#move, - } => { - if std::env::current_dir().unwrap().file_name().unwrap() != "library" { - warn!("new command can only be used in the library directory; what you are doing right now probably wont work.") - } - - let tmdb_kind = if series { "tv" } else { "movie" }; - let tmdb_id = if let Some(id) = tmdb_id { - Some(id.parse().unwrap()) - } else if let Some(title) = tmdb_search { - let tmdb_key = std::env::var("TMDB_API_KEY").context("tmdb api key required")?; - let results = crate::tmdb_search(tmdb_kind, &title, &tmdb_key)?; - info!("results:"); - for (i, r) in results.results.iter().enumerate() { - info!( - "\t[{i}] {}: {} ({})", - r.id, - r.title.as_ref().or(r.name.as_ref()).unwrap(), - r.overview.chars().take(100).collect::<String>() - ); - } - let res_index = if results.results.len() > 1 { - stdin() - .lines() - .next() - .unwrap() - .unwrap() - .parse::<usize>() - .unwrap() - } else { - 0 - }; - Some(results.results[res_index].id) - } else { - None - }; - - let tmdb_details = tmdb_id - .map(|id| { - let tmdb_key = - std::env::var("TMDB_API_KEY").context("tmdb api key required")?; - let td = tmdb_details(tmdb_kind, id, &tmdb_key) - .context("fetching details") - .unwrap(); - Ok::<_, anyhow::Error>(td) - }) - .transpose()?; - - let mut kind = NodeKind::Series; - let mut file_meta = None; - let mut infojson = None; - - if let Some(input_path) = &input { - file_meta = Some({ - let input = BufReader::new(File::open(&input_path).unwrap()); - let mut input = EbmlReader::new(input); - import_metadata(&mut input)? - }); - - if let Some(ij) = &file_meta.as_ref().unwrap().infojson { - infojson = - Some(serde_json::from_str::<YVideo>(ij).context("parsing info.json")?); - } - - kind = if video { - NodeKind::Video - } else { - NodeKind::Movie - }; - } - - let title = tmdb_details - .as_ref() - .map(|d| d.title.clone().or(d.name.clone())) - .flatten() - .or(file_meta.as_ref().map(|m| m.title.clone()).flatten()) - .expect("no title detected"); - - let ident = format!( - "{}{}", - ident_prefix.unwrap_or(String::new()), - make_ident( - &infojson - .as_ref() - .map(|i| i.id.clone()) - .unwrap_or(title.clone()) - ), - ); - let path = path.join(&ident); - let source_path = input.as_ref().map(|_| path.join(format!("source.mkv"))); - - let (mut poster, mut backdrop) = (None, None); - if !args.dry { - std::fs::create_dir_all(&path)?; - - poster = file_meta - .as_ref() - .map(|m| { - m.cover - .as_ref() - .map(|(mime, data)| { - let pu = path.join(format!( - "cover.{}", - match mime.as_str() { - "image/webp" => "webp", - "image/jpeg" => "jpeg", - "image/png" => "png", - _ => { - warn!("unknown mime, just using webp"); - "webp" - } - } - )); - if !pu.exists() { - let mut f = File::create(&pu)?; - f.write_all(&data)?; - } - Ok::<_, anyhow::Error>(pu) - }) - .transpose() - }) - .transpose()? - .flatten() - .or(tmdb_details - .as_ref() - .map(|d| { - d.poster_path - .as_ref() - .map(|p| { - let pu = path.join("poster.jpeg"); - let mut f = File::create(&pu)?; - tmdb_image(&p, &mut f)?; - Ok::<_, anyhow::Error>(pu) - }) - .transpose() - }) - .transpose()? - .flatten()); - - backdrop = tmdb_details - .as_ref() - .map(|d| { - d.backdrop_path - .as_ref() - .map(|p| { - let pu = path.join("backdrop.jpeg"); - let mut f = File::create(&pu)?; - tmdb_image(&p, &mut f)?; - Ok::<_, anyhow::Error>(pu) - }) - .transpose() - }) - .transpose()? - .flatten(); - } - - let mut ratings = BTreeMap::new(); - - ratings.extend( - infojson - .as_ref() - .map(|i| (Rating::YoutubeViews, i.view_count as f64)), - ); - ratings.extend( - infojson - .as_ref() - .map(|i| i.like_count.map(|l| (Rating::YoutubeLikes, l as f64))) - .flatten(), - ); - - let node = Node { - private: NodePrivate { - id: Some(ident.clone()), - import: None, - backdrop: backdrop.clone().map(AssetLocation::Library), - poster: poster.clone().map(AssetLocation::Library), - source: file_meta.as_ref().map(|m| MediaSource::Local { - tracks: m - .track_sources - .clone() - .into_iter() - .map(|t| LocalTrack { - path: source_path.clone().unwrap(), - ..t - }) - .collect(), - }), - }, - public: NodePublic { - federated: None, - ratings, - description: file_meta - .as_ref() - .map(|m| m.description.clone()) - .flatten() - .or(tmdb_details.as_ref().map(|d| d.overview.to_owned())), - tagline: file_meta.as_ref().map(|m| m.tagline.clone()).flatten().or( - tmdb_details - .as_ref() - .map(|d| d.tagline.to_owned()) - .flatten(), - ), - title, - index: None, - kind, - children: Vec::new(), - media: file_meta.as_ref().map(|m| MediaInfo { - duration: m.duration, - tracks: m.tracks.clone(), - }), - release_date: infojson - .as_ref() - .and_then(|j| ok_or_warn(parse_upload_date(&j.upload_date))), - ..Default::default() - }, - }; - - if args.dry { - println!("{}", serde_json::to_string_pretty(&node)?); - } else { - if let Some(source_path) = source_path { - let input = input.clone().unwrap(); - if r#move { - std::fs::rename(&input, &source_path)?; - } else if copy { - std::fs::copy(&input, &source_path)?; - } else { - if source_path.is_symlink() { - remove_file(&source_path)?; - } - std::os::unix::fs::symlink(&input, &source_path)?; - } - } - let f = File::create(path.join(if series { - "directory.json" - } else { - "item.jelly" - }))?; - serde_json::to_writer_pretty(f, &node)?; - } - - 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<T, E: Debug>(r: Result<T, E>) -> Option<T> { - match r { - Ok(t) => Some(t), - Err(e) => { - warn!("{e:?}"); - None - } - } -} |