aboutsummaryrefslogtreecommitdiff
path: root/import
diff options
context:
space:
mode:
Diffstat (limited to 'import')
-rw-r--r--import/Cargo.toml19
-rw-r--r--import/src/main.rs324
-rw-r--r--import/src/tmdb.rs95
3 files changed, 438 insertions, 0 deletions
diff --git a/import/Cargo.toml b/import/Cargo.toml
new file mode 100644
index 0000000..4e351c6
--- /dev/null
+++ b/import/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "jellything-import"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+jellycommon = { path = "../common" }
+jellymatroska = { path = "../matroska" }
+jellyremuxer = { path = "../remuxer" }
+
+log = "0.4.19"
+env_logger = "0.10.0"
+anyhow = "1.0.72"
+clap = { version = "4.3.19", features = ["derive"] }
+reqwest = { version = "0.11.18", features = ["blocking", "json"] }
+
+serde = { version = "1.0.180", features = ["derive"] }
+serde_json = "1.0.104"
+bincode = { version = "2.0.0-rc.3", features = ["serde"] }
diff --git a/import/src/main.rs b/import/src/main.rs
new file mode 100644
index 0000000..6cf2ef3
--- /dev/null
+++ b/import/src/main.rs
@@ -0,0 +1,324 @@
+/*
+ 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>
+*/
+pub mod tmdb;
+
+use anyhow::Context;
+use clap::{Parser, Subcommand};
+use jellycommon::{AssetLocation, MediaInfo, MediaSource, Node, NodeKind, NodePrivate, NodePublic};
+use jellymatroska::read::EbmlReader;
+use jellyremuxer::import::{import_metadata, seek_index::import_seek_index};
+use log::info;
+use std::{
+ collections::BTreeMap,
+ fs::{remove_file, File},
+ io::stdin,
+ path::PathBuf,
+ process::exit,
+};
+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 {
+ Create {
+ path: PathBuf,
+ #[arg(short = 't', long)]
+ tmdb_search: Option<String>,
+ #[arg(short = 'T', long)]
+ tmdb_id: Option<String>,
+ #[arg(long)]
+ copy: bool,
+ #[arg(long)]
+ r#move: bool,
+ #[arg(short, long)]
+ input: Option<PathBuf>,
+ #[arg(short, long)]
+ series: bool,
+ },
+ Set {
+ #[arg(short = 'I', long)]
+ item: PathBuf,
+ #[arg(short, long)]
+ poster: Option<PathBuf>,
+ #[arg(short, long)]
+ title: Option<String>,
+ #[arg(short = 'D', long)]
+ tagline: Option<String>,
+ #[arg(short = 'd', long)]
+ description: Option<String>,
+ #[arg(short = 'c', long)]
+ clear_inputs: bool,
+ #[arg(short = 'i', long, num_args(0..))]
+ input: Vec<PathBuf>,
+ },
+}
+
+fn main() -> anyhow::Result<()> {
+ env_logger::builder()
+ .filter_level(log::LevelFilter::Info)
+ .parse_env("LOG")
+ .init();
+ let args = Args::parse();
+
+ match args.action {
+ Action::Create {
+ path,
+ tmdb_id,
+ tmdb_search,
+ input,
+ series,
+ copy,
+ r#move,
+ } => {
+ let tmdb_kind = if series { "tv" } else { "movie" };
+ let tmdb_key = std::env::var("TMDB_API_KEY").context("tmdb api key required")?;
+ let tmdb_id = if let Some(id) = tmdb_id {
+ Some(id.parse().unwrap())
+ } else {
+ let title = tmdb_search.as_ref().unwrap();
+ 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)
+ };
+
+ let tmdb_details = tmdb_id.map(|id| {
+ let td = tmdb_details(tmdb_kind, id, &tmdb_key)
+ .context("fetching details")
+ .unwrap();
+ if td.title.is_some() {
+ info!("is this correct? [y/n]");
+ if stdin().lines().next().unwrap().unwrap() != "y" {
+ exit(0)
+ }
+ }
+ td
+ });
+
+ let title = tmdb_details
+ .as_ref()
+ .map(|d| d.title.clone().or(d.name.clone()))
+ .flatten()
+ .unwrap();
+ let ident = make_ident(&title);
+ let path = path.join(&ident);
+
+ let poster = 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();
+
+ let 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 kind;
+ let media;
+ let source;
+ let mut seek_index = BTreeMap::new();
+ let mut source_path_e = None;
+
+ if let Some(input) = input {
+ let source_path = path.join(format!("source.mkv"));
+ 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 (tracks, local_tracks, duration) = {
+ let input = File::open(&source_path).unwrap();
+ let mut input = EbmlReader::new(input);
+ import_metadata(&source_path.to_path_buf(), &mut input)?
+ };
+ seek_index = {
+ let input = File::open(&source_path).unwrap();
+ let mut input = EbmlReader::new(input);
+ import_seek_index(&mut input)?
+ };
+
+ kind = NodeKind::Movie;
+ media = Some(MediaInfo { duration, tracks });
+ source = Some(MediaSource::Local {
+ tracks: local_tracks,
+ });
+ source_path_e = Some(source_path)
+ } else {
+ kind = NodeKind::Series;
+ media = None;
+ source = None;
+ };
+
+ let node = Node {
+ private: NodePrivate {
+ import: None,
+ backdrop: backdrop.clone().map(AssetLocation::Library),
+ poster: poster.clone().map(AssetLocation::Library),
+ source,
+ },
+ public: NodePublic {
+ parent: None,
+ federated: None,
+ description: tmdb_details.as_ref().map(|d| d.overview.to_owned()),
+ tagline: tmdb_details
+ .as_ref()
+ .map(|d| d.tagline.to_owned())
+ .flatten(),
+ title,
+ index: None,
+ kind,
+ children: Vec::new(),
+ media,
+ },
+ };
+
+ if args.dry {
+ println!("{node:?}")
+ } else {
+ std::fs::create_dir_all(&path)?;
+ for (tn, index) in seek_index {
+ info!("writing index {tn} with {} blocks", index.blocks.len());
+ bincode::encode_into_std_write(
+ index,
+ &mut File::create(
+ source_path_e
+ .as_ref()
+ .unwrap()
+ .with_extension(&format!("si.{tn}")),
+ )?,
+ bincode::config::standard(),
+ )?;
+ }
+ let f = File::create(path.join(if series {
+ "directory.json".to_string()
+ } else {
+ format!("{ident}.jelly")
+ }))?;
+ serde_json::to_writer_pretty(f, &node)?;
+ }
+
+ Ok(())
+ }
+ Action::Set { .. } => {
+ // let mut iteminfo: ItemInfo = match File::open(item.clone()) {
+ // Ok(f) => serde_json::from_reader(f)?,
+ // Err(e) => {
+ // warn!("could not load item info: {e}");
+ // warn!("using the default instead");
+ // ItemInfo {
+ // common: CommmonInfo {
+ // poster: None,
+ // backdrop: None,
+ // tagline: None,
+ // description: None,
+ // title: item.to_str().unwrap().to_string(),
+ // index: None,
+ // },
+ // duration: 0.0,
+ // tracks: Default::default(),
+ // }
+ // }
+ // };
+
+ // if let Some(title) = title {
+ // iteminfo.title = title;
+ // }
+ // if let Some(poster) = poster {
+ // iteminfo.poster = Some(poster);
+ // }
+ // if let Some(d) = description {
+ // iteminfo.description = Some(d);
+ // }
+ // if let Some(d) = tagline {
+ // iteminfo.tagline = Some(d);
+ // }
+ // if clear_inputs {
+ // iteminfo.tracks = Default::default()
+ // }
+
+ // // for input_path in input {
+ // // let input = File::open(input_path.clone()).unwrap();
+ // // let mut input = EbmlReader::new(input);
+ // // import_read(&input_path, &mut input, &mut iteminfo)?;
+ // // }
+
+ // let k = serde_json::to_string_pretty(&iteminfo)?;
+ // if args.dry {
+ // println!("{k}")
+ // } else {
+ // let mut f = File::create(item)?;
+ // f.write_all(k.as_bytes())?;
+ // }
+ Ok(())
+ }
+ }
+}
+
+fn make_ident(s: &str) -> String {
+ let mut out = String::new();
+ for s in s.chars() {
+ match s {
+ 'a'..='z' => out.push(s),
+ 'A'..='Z' => out.push(s.to_ascii_lowercase()),
+ '-' | ' ' | '_' => out.push('-'),
+ _ => (),
+ }
+ }
+ out
+}
diff --git a/import/src/tmdb.rs b/import/src/tmdb.rs
new file mode 100644
index 0000000..5f21afd
--- /dev/null
+++ b/import/src/tmdb.rs
@@ -0,0 +1,95 @@
+/*
+ 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>
+*/
+use log::info;
+use serde::Deserialize;
+use std::io::Write;
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct TmdbQuery {
+ pub page: usize,
+ pub results: Vec<TmdbQueryResult>,
+ pub total_pages: usize,
+ pub total_results: usize,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct TmdbQueryResult {
+ pub adult: bool,
+ pub backdrop_path: Option<String>,
+ pub genre_ids: Vec<u64>,
+ pub id: u64,
+ pub original_language: Option<String>,
+ pub original_title: Option<String>,
+ pub overview: String,
+ pub popularity: f64,
+ pub poster_path: Option<String>,
+ pub release_date: Option<String>,
+ pub title: Option<String>,
+ pub name: Option<String>,
+ pub vote_average: f64,
+ pub vote_count: usize,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct TmdbDetails {
+ pub adult: bool,
+ pub backdrop_path: Option<String>,
+ pub genres: Vec<TmdbGenre>,
+ pub id: u64,
+ pub original_language: Option<String>,
+ pub original_title: Option<String>,
+ pub overview: String,
+ pub popularity: f64,
+ pub poster_path: Option<String>,
+ pub release_date: Option<String>,
+ pub title: Option<String>,
+ pub name: Option<String>,
+ pub vote_average: f64,
+ pub vote_count: usize,
+ pub budget: Option<usize>,
+ pub homepage: Option<String>,
+ pub imdb_id: Option<String>,
+ pub production_companies: Vec<TmdbProductionCompany>,
+ pub revenue: Option<usize>,
+ pub tagline: Option<String>,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct TmdbGenre {
+ pub id: u64,
+ pub name: String,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct TmdbProductionCompany {
+ pub id: u64,
+ pub name: String,
+ pub logo_path: Option<String>,
+}
+
+pub fn tmdb_search(kind: &str, query: &str, key: &str) -> anyhow::Result<TmdbQuery> {
+ info!("searching tmdb: {query:?}");
+ Ok(reqwest::blocking::get(&format!(
+ "https://api.themoviedb.org/3/search/{kind}?query={}&api_key={key}",
+ query.replace(" ", "+")
+ ))?
+ .json::<TmdbQuery>()?)
+}
+
+pub fn tmdb_details(kind: &str, id: u64, key: &str) -> anyhow::Result<TmdbDetails> {
+ info!("fetching details: {id:?}");
+ Ok(reqwest::blocking::get(&format!(
+ "https://api.themoviedb.org/3/{kind}/{id}?api_key={key}"
+ ))?
+ .json()?)
+}
+
+pub fn tmdb_image(path: &str, out: &mut impl Write) -> anyhow::Result<()> {
+ info!("downloading image {path:?}");
+ let mut res = reqwest::blocking::get(&format!("https://image.tmdb.org/t/p/original{path}"))?;
+ res.copy_to(out)?;
+ Ok(())
+}