aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2023-12-21 23:57:42 +0100
committermetamuffin <metamuffin@disroot.org>2023-12-21 23:57:42 +0100
commit3a29113e965a94bdef06655f1583cc6e86edd606 (patch)
treea0910fa9687a9935ba1ca85a9cb5def1a0bc9069
parenta8b2480e898e269e7e0d41dbd46d9a18c7d1e4ba (diff)
downloadjellything-3a29113e965a94bdef06655f1583cc6e86edd606.tar
jellything-3a29113e965a94bdef06655f1583cc6e86edd606.tar.bz2
jellything-3a29113e965a94bdef06655f1583cc6e86edd606.tar.zst
rework import system pt. 1
-rw-r--r--Cargo.lock21
-rw-r--r--Cargo.toml3
-rw-r--r--base/Cargo.toml1
-rw-r--r--base/src/database.rs3
-rw-r--r--base/src/federation.rs (renamed from server/src/federation.rs)2
-rw-r--r--base/src/lib.rs2
-rw-r--r--common/src/config.rs4
-rw-r--r--common/src/lib.rs45
-rw-r--r--import/Cargo.toml24
-rw-r--r--import/src/infojson.rs (renamed from tool/src/import/infojson.rs)0
-rw-r--r--import/src/lib.rs376
-rw-r--r--import/src/mod.rs (renamed from tool/src/import/mod.rs)9
-rw-r--r--import/src/tmdb.rs (renamed from tool/src/import/tmdb.rs)0
-rw-r--r--remuxer/src/import/mod.rs1
-rw-r--r--remuxer/src/remux.rs2
-rw-r--r--remuxer/src/snippet.rs5
-rw-r--r--server/Cargo.toml2
-rw-r--r--server/src/import.rs254
-rw-r--r--server/src/main.rs5
-rw-r--r--server/src/routes/mod.rs4
-rw-r--r--server/src/routes/stream.rs59
-rw-r--r--server/src/routes/ui/admin/mod.rs5
-rw-r--r--server/src/routes/ui/node.rs22
-rw-r--r--server/src/routes/ui/player.rs2
-rw-r--r--server/src/routes/ui/sort.rs16
-rw-r--r--stream/src/lib.rs5
-rw-r--r--stream/src/webvtt.rs8
-rw-r--r--tool/src/main.rs93
28 files changed, 585 insertions, 388 deletions
diff --git a/Cargo.lock b/Cargo.lock
index d6415b4..9a12441 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1347,6 +1347,7 @@ dependencies = [
"anyhow",
"base64",
"bincode 2.0.0-rc.3",
+ "jellyclient",
"jellycommon",
"log",
"serde",
@@ -1381,6 +1382,24 @@ dependencies = [
]
[[package]]
+name = "jellyimport"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-recursion",
+ "futures",
+ "jellybase",
+ "jellyclient",
+ "jellycommon",
+ "jellyremuxer",
+ "log",
+ "reqwest",
+ "serde_json",
+ "serde_yaml",
+ "tokio",
+]
+
+[[package]]
name = "jellymatroska"
version = "0.1.0"
dependencies = [
@@ -1434,8 +1453,8 @@ dependencies = [
"futures",
"glob",
"jellybase",
- "jellyclient",
"jellycommon",
+ "jellyimport",
"jellystream",
"jellytranscoder",
"log",
diff --git a/Cargo.toml b/Cargo.toml
index a9969e7..1b0d230 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,7 +8,8 @@ members = [
"ebml_derive",
"client",
"transcoder",
- "base",
+ "base",
+ "import",
]
resolver = "2"
diff --git a/base/Cargo.toml b/base/Cargo.toml
index ac6989c..8109998 100644
--- a/base/Cargo.toml
+++ b/base/Cargo.toml
@@ -5,6 +5,7 @@ edition = "2021"
[dependencies]
jellycommon = { path = "../common" }
+jellyclient = { path = "../client" }
serde = { version = "1.0.193", features = ["derive"] }
serde_yaml = "0.9.27"
log = { workspace = true }
diff --git a/base/src/database.rs b/base/src/database.rs
index f46f0fb..3f81cca 100644
--- a/base/src/database.rs
+++ b/base/src/database.rs
@@ -22,6 +22,8 @@ pub struct Database {
pub user_node: Tree<(String, String), NodeUserData>,
pub invite: Tree<String, ()>,
pub node: Tree<String, Node>,
+
+ pub node_import: Tree<String, Vec<(Vec<usize>, Node)>>,
}
impl Database {
@@ -34,6 +36,7 @@ impl Database {
invite: Tree::open(&db, "invite"),
node: Tree::open(&db, "node"),
user_node: Tree::open(&db, "user_node"),
+ node_import: Tree::open(&db, "node_import"),
db,
});
info!("ready");
diff --git a/server/src/federation.rs b/base/src/federation.rs
index eb2a1ac..509b87c 100644
--- a/server/src/federation.rs
+++ b/base/src/federation.rs
@@ -4,7 +4,7 @@
Copyright (C) 2023 metamuffin <metamuffin.org>
*/
use anyhow::anyhow;
-use jellybase::CONF;
+use crate::CONF;
use jellyclient::{Instance, LoginDetails, Session};
use std::{collections::HashMap, sync::Arc};
use tokio::sync::RwLock;
diff --git a/base/src/lib.rs b/base/src/lib.rs
index cfc5a11..a7b15c5 100644
--- a/base/src/lib.rs
+++ b/base/src/lib.rs
@@ -6,6 +6,7 @@
#![feature(lazy_cell)]
pub mod cache;
pub mod database;
+pub mod federation;
pub mod permission;
pub mod temp;
@@ -34,6 +35,7 @@ impl AssetLocationExt for AssetLocation {
AssetLocation::Cache(p) => CONF.cache_path.join(p),
AssetLocation::Library(p) => CONF.library_path.join(p),
AssetLocation::Temp(p) => CONF.temp_path.join(p),
+ AssetLocation::Media(p) => CONF.media_path.join(p),
}
}
}
diff --git a/common/src/config.rs b/common/src/config.rs
index 3ccf0e8..200249c 100644
--- a/common/src/config.rs
+++ b/common/src/config.rs
@@ -19,6 +19,7 @@ pub struct GlobalConfig {
#[serde(default = "default::library_path")] pub library_path: PathBuf,
#[serde(default = "default::temp_path")] pub temp_path: PathBuf,
#[serde(default = "default::cache_path")] pub cache_path: PathBuf,
+ #[serde(default = "default::media_path")] pub media_path: PathBuf,
#[serde(default = "default::admin_username")] pub admin_username: String,
#[serde(default = "default::transcoding_profiles")] pub transcoding_profiles: Vec<EncodingProfile>,
#[serde(default = "default::max_in_memory_cache_size")] pub max_in_memory_cache_size: usize,
@@ -53,6 +54,9 @@ mod default {
pub fn cache_path() -> PathBuf {
"data/cache".into()
}
+ pub fn media_path() -> PathBuf {
+ "data/media".into()
+ }
pub fn temp_path() -> PathBuf {
"/tmp".into()
}
diff --git a/common/src/lib.rs b/common/src/lib.rs
index aedcb07..e953d85 100644
--- a/common/src/lib.rs
+++ b/common/src/lib.rs
@@ -19,7 +19,7 @@ use rocket::{FromFormField, UriDisplayQuery};
use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, path::PathBuf};
-#[derive(Debug, Clone, Deserialize, Serialize)]
+#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct Node {
pub public: NodePublic,
pub private: NodePrivate,
@@ -29,17 +29,16 @@ pub struct Node {
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct NodePrivate {
#[serde(default)] pub id: Option<String>,
- #[serde(default)] pub import: Option<RemoteImportOptions>,
#[serde(default)] pub poster: Option<AssetLocation>,
#[serde(default)] pub backdrop: Option<AssetLocation>,
- #[serde(default)] pub source: Option<MediaSource>,
+ #[serde(default)] pub source: Option<Vec<TrackSource>>,
}
#[rustfmt::skip]
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct NodePublic {
- pub kind: NodeKind,
- pub title: String,
+ #[serde(default)] pub kind: Option<NodeKind>,
+ #[serde(default)] pub title: Option<String>,
#[serde(default)] pub id: Option<String>,
#[serde(default)] pub path: Vec<String>,
#[serde(default)] pub children: Vec<String>,
@@ -52,13 +51,25 @@ pub struct NodePublic {
#[serde(default)] pub federated: Option<String>,
}
-#[rustfmt::skip]
-#[derive(Debug, Clone, Deserialize, Serialize)]
-pub struct RemoteImportOptions {
- pub host: String,
+#[derive(Debug, Clone, Deserialize, Serialize, Default)]
+pub struct ImportOptions {
pub id: String,
- #[serde(default)] pub flatten: bool,
- #[serde(default)] pub prefix: Option<String>,
+ pub sources: Vec<ImportSource>,
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub enum ImportSource {
+ Override(Node),
+ Tmdb(u64),
+ AutoChildren,
+ Media {
+ location: AssetLocation,
+ // TODO ignore options
+ },
+ Federated {
+ host: String,
+ },
}
#[derive(Debug, Clone, Deserialize, Serialize, Hash, PartialEq, Eq)]
@@ -68,6 +79,7 @@ pub enum AssetLocation {
Library(PathBuf),
Assets(PathBuf),
Temp(PathBuf),
+ Media(PathBuf),
}
#[rustfmt::skip]
@@ -87,9 +99,9 @@ pub enum NodeKind {
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
-pub enum MediaSource {
- Local { tracks: Vec<LocalTrack> },
- Remote { host: String, remote_id: String },
+pub enum TrackSource {
+ Local(LocalTrack),
+ Remote,
}
pub enum PublicMediaSource {
@@ -128,18 +140,19 @@ pub struct SourceTrack {
pub codec: String,
pub language: String,
pub default_duration: Option<u64>,
+ #[serde(default)] pub federated: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum Rating {
+ Imdb,
+ Tmdb,
RottenTomatoes,
Metacritic,
- Imdb,
YoutubeViews,
YoutubeLikes,
YoutubeFollowers,
- Tmdb,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
diff --git a/import/Cargo.toml b/import/Cargo.toml
new file mode 100644
index 0000000..f2ba7af
--- /dev/null
+++ b/import/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "jellyimport"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+jellycommon = { path = "../common" }
+jellybase = { path = "../base" }
+jellyclient = { path = "../client" }
+# jellymatroska = { path = "../matroska" }
+jellyremuxer = { path = "../remuxer" }
+
+log = { workspace = true }
+anyhow = "1.0.75"
+reqwest = { version = "0.11.22", features = ["blocking", "json"] }
+
+# serde = { version = "1.0.193", features = ["derive"] }
+serde_json = "1.0.108"
+serde_yaml = "0.9.27"
+# bincode = { version = "2.0.0-rc.3", features = ["serde"] }
+
+async-recursion = "1.0.5"
+futures = "0.3.29"
+tokio = { workspace = true }
diff --git a/tool/src/import/infojson.rs b/import/src/infojson.rs
index 3f0edc9..3f0edc9 100644
--- a/tool/src/import/infojson.rs
+++ b/import/src/infojson.rs
diff --git a/import/src/lib.rs b/import/src/lib.rs
new file mode 100644
index 0000000..3698f79
--- /dev/null
+++ b/import/src/lib.rs
@@ -0,0 +1,376 @@
+/*
+ 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(lazy_cell)]
+use anyhow::{Context, Ok};
+use async_recursion::async_recursion;
+use futures::{stream::FuturesUnordered, StreamExt};
+use jellybase::{
+ cache::async_cache_file, database::Database, federation::Federation, AssetLocationExt, CONF,
+};
+use jellyclient::Session;
+use jellycommon::{
+ AssetLocation, AssetRole, ImportOptions, ImportSource, Node, NodePrivate, NodePublic,
+};
+use log::{debug, info};
+use std::{
+ cmp::Ordering,
+ ffi::OsStr,
+ fs::File,
+ os::unix::prelude::OsStrExt,
+ path::{Path, PathBuf},
+ sync::{Arc, LazyLock},
+};
+use tokio::sync::Semaphore;
+
+static IMPORT_SEM: LazyLock<Semaphore> = LazyLock::new(|| Semaphore::new(1));
+
+pub async fn import(db: &Database, fed: &Federation) -> anyhow::Result<()> {
+ let permit = IMPORT_SEM.try_acquire()?;
+ info!("loading sources...");
+ import_path(CONF.library_path.clone(), vec![], db, fed)
+ .await
+ .context("indexing")?;
+ info!("merging nodes...");
+ merge_nodes(db).context("merging nodes")?;
+ info!("clearing temporary node tree");
+ db.node_import.clear()?;
+ info!("import completed");
+ drop(permit);
+ Ok(())
+}
+
+pub fn merge_nodes(db: &Database) -> anyhow::Result<()> {
+ for r in db.node_import.iter() {
+ let (id, mut nodes) = r?;
+
+ nodes.sort_by(|(x, _), (y, _)| compare_index_path(x, y));
+
+ let node = nodes
+ .into_iter()
+ .map(|(_, x)| x)
+ .reduce(|x, y| merge_node(x, y))
+ .unwrap();
+
+ db.node.insert(&id, &node)?;
+ }
+ Ok(())
+}
+
+fn compare_index_path(x: &[usize], y: &[usize]) -> Ordering {
+ if x.is_empty() {
+ Ordering::Greater
+ } else if y.is_empty() {
+ Ordering::Less
+ } else {
+ match x[0].cmp(&y[0]) {
+ o @ (Ordering::Less | Ordering::Greater) => o,
+ Ordering::Equal => compare_index_path(&x[1..], &y[1..]),
+ }
+ }
+}
+
+#[async_recursion]
+pub async fn import_path(
+ path: PathBuf,
+ index_path: Vec<usize>,
+ db: &Database,
+ fed: &Federation,
+) -> anyhow::Result<()> {
+ if path.is_dir() {
+ let mut children_paths = path
+ .read_dir()?
+ .map(Result::unwrap)
+ .filter_map(|e| {
+ if e.path().extension() == Some(&OsStr::from_bytes(b"yaml"))
+ || e.metadata().unwrap().is_dir()
+ {
+ Some(e.path())
+ } else {
+ None
+ }
+ })
+ .collect::<Vec<_>>();
+
+ children_paths.sort();
+
+ let mut children: FuturesUnordered<_> = children_paths
+ .into_iter()
+ .enumerate()
+ .map(|(i, p)| {
+ import_path(
+ p.clone(),
+ {
+ let mut path = index_path.clone();
+ path.push(i);
+ path
+ },
+ db,
+ fed,
+ )
+ })
+ .collect();
+
+ while let Some(k) = children.next().await {
+ k?
+ }
+ } else {
+ let opts: ImportOptions = serde_yaml::from_reader(File::open(&path)?)?;
+
+ for s in opts.sources {
+ process_source(opts.id.clone(), s, &path, &index_path, db, fed).await?;
+ }
+ }
+ Ok(())
+}
+
+async fn process_source(
+ id: String,
+ s: ImportSource,
+ path: &Path,
+ index_path: &[usize],
+ db: &Database,
+ fed: &Federation,
+) -> anyhow::Result<()> {
+ let insert_node = move |id: &String, n: Node| -> anyhow::Result<()> {
+ db.node_import.fetch_and_update(id, |l| {
+ let mut l = l.unwrap_or_default();
+ l.push((index_path.to_vec(), n.clone()));
+ Some(l)
+ })?;
+ Ok(())
+ };
+ match s {
+ ImportSource::Override(n) => insert_node(&id, n)?,
+ ImportSource::Tmdb(_) => todo!(),
+ ImportSource::Media { location } => {
+ let path = location.path();
+ }
+ ImportSource::Federated { host } => {
+ let session = fed.get_session(&host).await.context("creating session")?;
+
+ import_remote(id, &host, db, &session, index_path)
+ .await
+ .context("federated import")?
+ }
+ ImportSource::AutoChildren => {
+ // TODO dont forget to update path of children
+ }
+ }
+ Ok(())
+}
+
+fn merge_node(x: Node, y: Node) -> Node {
+ Node {
+ public: NodePublic {
+ kind: x.public.kind.or(y.public.kind),
+ title: x.public.title.or(y.public.title),
+ id: x.public.id.or(y.public.id),
+ path: vec![],
+ children: if x.public.children.is_empty() {
+ x.public.children
+ } else {
+ y.public.children
+ },
+ tagline: x.public.tagline.or(y.public.tagline),
+ description: x.public.description.or(y.public.description),
+ release_date: x.public.release_date.or(y.public.release_date),
+ index: x.public.index.or(y.public.index),
+ media: x.public.media.or(y.public.media), // TODO proper media merging
+ ratings: x
+ .public
+ .ratings
+ .into_iter()
+ .chain(y.public.ratings)
+ .collect(),
+ federated: x.public.federated.or(y.public.federated),
+ },
+ private: NodePrivate {
+ id: x.private.id.or(y.private.id),
+ poster: x.private.poster.or(y.private.poster),
+ backdrop: x.private.backdrop.or(y.private.backdrop),
+ source: x.private.source.or(y.private.source), // TODO here too
+ },
+ }
+}
+
+// #[async_recursion]
+// pub async fn import_path(
+// path: PathBuf,
+// db: &Database,
+// fed: &Federation,
+// mut node_path: Vec<String>,
+// ) -> anyhow::Result<(Vec<String>, usize)> {
+// if path.is_dir() {
+// let mpath = path.join("directory.json");
+// let children_paths = path.read_dir()?.map(Result::unwrap).filter_map(|e| {
+// if e.path().extension() == Some(&OsStr::from_bytes(b"jelly"))
+// || e.metadata().unwrap().is_dir()
+// {
+// Some(e.path())
+// } else {
+// None
+// }
+// });
+// let identifier = if mpath.exists() {
+// path.file_name().unwrap().to_str().unwrap().to_string()
+// } else {
+// node_path
+// .last()
+// .cloned()
+// .ok_or(anyhow!("non-root node requires parent"))?
+// };
+
+// node_path.push(identifier.clone());
+// let mut all: FuturesUnordered<_> = children_paths
+// .into_iter()
+// .map(|p| import_path(p.clone(), db, fed, node_path.clone()).map_err(|e| (p, e)))
+// .collect();
+// node_path.pop(); // we will set the dirs path later and need it to not be included
+
+// let mut children_ids = Vec::new();
+// let mut errs = 0;
+// while let Some(k) = all.next().await {
+// match k {
+// core::result::Result::Ok((els, errs2)) => {
+// errs += errs2;
+// children_ids.extend(els)
+// }
+// Err((p, e)) => {
+// errs += 1;
+// error!("import of {p:?} failed: {e:?}")
+// }
+// }
+// }
+// if mpath.exists() {
+// let mut node: Node =
+// serde_json::from_reader(File::open(mpath).context("metadata missing")?)?;
+
+// node.public.children = children_ids;
+// node.public.path = node_path;
+// node.public.id = Some(identifier.to_owned());
+// info!("adding {identifier}");
+// db.node.insert(&identifier, &node)?;
+// Ok((vec![identifier], errs))
+// } else {
+// Ok((children_ids, errs))
+// }
+// } else if path.is_file() {
+// info!("loading {path:?}");
+// let datafile = File::open(path.clone()).context("cant load metadata")?;
+// let mut node: Node = serde_json::from_reader(datafile).context("invalid metadata")?;
+// let identifier = node.private.id.clone().unwrap_or_else(|| {
+// path.file_name()
+// .unwrap()
+// .to_str()
+// .unwrap()
+// .strip_suffix(".json")
+// .unwrap()
+// .to_string()
+// });
+
+// let idents = if let Some(io) = node.private.import.take() {
+// let session = fed
+// .get_session(&io.host)
+// .await
+// .context("creating session")?;
+
+// import_remote(io, db, &session, identifier.clone(), node_path)
+// .await
+// .context("federated import")?
+// } else {
+// debug!("adding {identifier}");
+// node.public.path = node_path;
+// node.public.id = Some(identifier.to_owned());
+// let did_insert = db.node.insert(&identifier, &node)?.is_none();
+// if did_insert {
+// vec![identifier]
+// } else {
+// vec![]
+// }
+// };
+// Ok((idents, 0))
+// } else {
+// bail!("did somebody really put a fifo or socket in the library?!")
+// }
+// }
+
+static SEM_REMOTE_IMPORT: LazyLock<Semaphore> = LazyLock::new(|| Semaphore::new(16));
+
+#[async_recursion]
+async fn import_remote(
+ id: String,
+ host: &str,
+ db: &Database,
+ session: &Arc<Session>,
+ index_path: &[usize],
+) -> anyhow::Result<()> {
+ let insert_node = move |id: &String, n: Node| -> anyhow::Result<()> {
+ db.node_import.fetch_and_update(id, |l| {
+ let mut l = l.unwrap_or_default();
+ l.push((index_path.to_vec(), n.clone()));
+ Some(l)
+ })?;
+ Ok(())
+ };
+ let _permit = SEM_REMOTE_IMPORT.acquire().await.unwrap();
+ info!("loading federated node {id:?}");
+
+ let node = session.node(&id).await.context("fetching remote node")?;
+
+ if node.federated.as_ref() == Some(&CONF.hostname) {
+ return Ok(());
+ }
+
+ // TODO maybe use lazy download
+ let poster = cache_federation_asset(session.to_owned(), id.clone(), AssetRole::Poster).await?;
+ let backdrop =
+ cache_federation_asset(session.to_owned(), id.clone(), AssetRole::Backdrop).await?;
+
+ drop(_permit);
+
+ let node = Node {
+ public: node.clone(),
+ private: NodePrivate {
+ backdrop: Some(backdrop),
+ poster: Some(poster),
+ id: None,
+ source: None, // TODO
+ },
+ };
+
+ debug!("adding {id}");
+ insert_node(&id, node.clone())?;
+
+ let mut children: FuturesUnordered<_> = node
+ .public
+ .children
+ .iter()
+ .map(|c| import_remote(c.to_owned(), host, db, session, index_path))
+ .collect();
+
+ while let Some(r) = children.next().await {
+ r?;
+ }
+
+ Ok(())
+}
+
+async fn cache_federation_asset(
+ session: Arc<Session>,
+ identifier: String,
+ role: AssetRole,
+) -> anyhow::Result<AssetLocation> {
+ async_cache_file(
+ &["fed-asset", role.as_str(), &identifier.clone()],
+ move |out| async move {
+ let session = session;
+ session
+ .node_asset(identifier.as_str(), role, 1024, out)
+ .await
+ },
+ )
+ .await
+}
diff --git a/tool/src/import/mod.rs b/import/src/mod.rs
index 973629c..0c43cde 100644
--- a/tool/src/import/mod.rs
+++ b/import/src/mod.rs
@@ -10,8 +10,8 @@ use crate::{make_ident, ok_or_warn, Action};
use anyhow::Context;
use infojson::YVideo;
use jellycommon::{
- AssetLocation, LocalTrack, MediaInfo, MediaSource, Node, NodeKind, NodePrivate, NodePublic,
- Rating,
+ AssetLocation, LocalTrack, MediaInfo, Node, NodeKind, NodePrivate, NodePublic, Rating,
+ TrackSource,
};
use jellymatroska::read::EbmlReader;
use jellyremuxer::import::import_metadata;
@@ -240,7 +240,6 @@ pub(crate) fn import(action: Action, dry: bool) -> anyhow::Result<()> {
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 {
@@ -269,9 +268,9 @@ pub(crate) fn import(action: Action, dry: bool) -> anyhow::Result<()> {
.map(|d| d.tagline.to_owned())
.flatten(),
),
- title,
+ title: Some(title),
index: None,
- kind,
+ kind: Some(kind),
children: Vec::new(),
media: file_meta.as_ref().map(|m| MediaInfo {
chapters: m.chapters.clone(),
diff --git a/tool/src/import/tmdb.rs b/import/src/tmdb.rs
index c38d50e..c38d50e 100644
--- a/tool/src/import/tmdb.rs
+++ b/import/src/tmdb.rs
diff --git a/remuxer/src/import/mod.rs b/remuxer/src/import/mod.rs
index cf37b78..92e601e 100644
--- a/remuxer/src/import/mod.rs
+++ b/remuxer/src/import/mod.rs
@@ -325,6 +325,7 @@ fn import_read_segment(segment: &mut Unflatten) -> Result<MatroskaMetadata> {
_ => bail!("invalid track type"),
};
m.tracks.push(SourceTrack {
+ federated: vec![],
default_duration,
name: name.unwrap_or_else(|| "unnamed".to_string()),
codec: codec.unwrap(),
diff --git a/remuxer/src/remux.rs b/remuxer/src/remux.rs
index 0e7877d..b705572 100644
--- a/remuxer/src/remux.rs
+++ b/remuxer/src/remux.rs
@@ -108,7 +108,7 @@ pub fn remux_stream_into(
output.write_tag(&MatroskaTag::Info(Master::Collected(vec![
MatroskaTag::TimestampScale(1_000_000),
MatroskaTag::Duration(item.media.unwrap().duration * 1000.0),
- MatroskaTag::Title(item.title.clone()),
+ MatroskaTag::Title(item.title.unwrap_or_default().clone()),
MatroskaTag::MuxingApp("jellyremux".to_string()),
MatroskaTag::WritingApp("jellything".to_string()),
])))?;
diff --git a/remuxer/src/snippet.rs b/remuxer/src/snippet.rs
index afda1f3..cd45f8b 100644
--- a/remuxer/src/snippet.rs
+++ b/remuxer/src/snippet.rs
@@ -110,7 +110,10 @@ pub fn write_snippet_into(
output.write_tag(&ebml_header(webm))?;
output.write_tag(&MatroskaTag::Segment(Master::Start))?;
output.write_tag(&ebml_segment_info(
- format!("{} (track {track}; snippet {n})", item.title),
+ format!(
+ "{} (track {track}; snippet {n})",
+ item.title.clone().unwrap_or_default()
+ ),
(last_block.pts - start_block.pts) as f64 / 1000.,
))?;
output.write_tag(&MatroskaTag::Tags(Master::Collected(vec![])))?;
diff --git a/server/Cargo.toml b/server/Cargo.toml
index da8603d..68ae861 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -7,8 +7,8 @@ edition = "2021"
jellycommon = { path = "../common", features = ["rocket"] }
jellybase = { path = "../base" }
jellystream = { path = "../stream" }
-jellyclient = { path = "../client" }
jellytranscoder = { path = "../transcoder" }
+jellyimport = { path = "../import" }
serde = { version = "1.0.193", features = ["derive"] }
bincode = { version = "2.0.0-rc.3", features = ["serde", "derive"] }
diff --git a/server/src/import.rs b/server/src/import.rs
deleted file mode 100644
index dc32fbf..0000000
--- a/server/src/import.rs
+++ /dev/null
@@ -1,254 +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>
-*/
-use crate::{database::Database, federation::Federation, CONF};
-use anyhow::{anyhow, bail, Context, Ok};
-use async_recursion::async_recursion;
-use futures::{stream::FuturesUnordered, StreamExt, TryFutureExt};
-use jellybase::cache::async_cache_file;
-use jellyclient::Session;
-use jellycommon::{AssetLocation, AssetRole, MediaSource, Node, NodePrivate, RemoteImportOptions};
-use log::{debug, error, info};
-use std::{
- ffi::OsStr,
- fs::File,
- os::unix::prelude::OsStrExt,
- path::PathBuf,
- sync::{Arc, LazyLock},
-};
-use tokio::sync::Semaphore;
-
-static IMPORT_SEM: LazyLock<Semaphore> = LazyLock::new(|| Semaphore::new(1));
-
-pub async fn import(db: &Database, fed: &Federation) -> anyhow::Result<()> {
- info!("clearing node tree");
- let permit = IMPORT_SEM.try_acquire()?;
- db.node.clear()?;
- info!("importing...");
- let (_, errs) = import_path(CONF.library_path.clone(), db, fed, vec![])
- .await
- .context("indexing")?;
- info!("import completed");
- drop(permit);
- if errs == 0 {
- Ok(())
- } else {
- Err(anyhow!(
- "partial import, {errs} errors occured; see server log"
- ))
- }
-}
-
-#[async_recursion]
-pub async fn import_path(
- path: PathBuf,
- db: &Database,
- fed: &Federation,
- mut node_path: Vec<String>,
-) -> anyhow::Result<(Vec<String>, usize)> {
- if path.is_dir() {
- let mpath = path.join("directory.json");
- let children_paths = path.read_dir()?.map(Result::unwrap).filter_map(|e| {
- if e.path().extension() == Some(&OsStr::from_bytes(b"jelly"))
- || e.metadata().unwrap().is_dir()
- {
- Some(e.path())
- } else {
- None
- }
- });
- let identifier = if mpath.exists() {
- path.file_name().unwrap().to_str().unwrap().to_string()
- } else {
- node_path
- .last()
- .cloned()
- .ok_or(anyhow!("non-root node requires parent"))?
- };
-
- node_path.push(identifier.clone());
- let mut all: FuturesUnordered<_> = children_paths
- .into_iter()
- .map(|p| import_path(p.clone(), db, fed, node_path.clone()).map_err(|e| (p, e)))
- .collect();
- node_path.pop(); // we will set the dirs path later and need it to not be included
-
- let mut children_ids = Vec::new();
- let mut errs = 0;
- while let Some(k) = all.next().await {
- match k {
- core::result::Result::Ok((els, errs2)) => {
- errs += errs2;
- children_ids.extend(els)
- }
- Err((p, e)) => {
- errs += 1;
- error!("import of {p:?} failed: {e:?}")
- }
- }
- }
- if mpath.exists() {
- let mut node: Node =
- serde_json::from_reader(File::open(mpath).context("metadata missing")?)?;
-
- node.public.children = children_ids;
- node.public.path = node_path;
- node.public.id = Some(identifier.to_owned());
- info!("adding {identifier}");
- db.node.insert(&identifier, &node)?;
- Ok((vec![identifier], errs))
- } else {
- Ok((children_ids, errs))
- }
- } else if path.is_file() {
- info!("loading {path:?}");
- let datafile = File::open(path.clone()).context("cant load metadata")?;
- let mut node: Node = serde_json::from_reader(datafile).context("invalid metadata")?;
- let identifier = node.private.id.clone().unwrap_or_else(|| {
- path.file_name()
- .unwrap()
- .to_str()
- .unwrap()
- .strip_suffix(".jelly")
- .unwrap()
- .to_string()
- });
-
- let idents = if let Some(io) = node.private.import.take() {
- let session = fed
- .get_session(&io.host)
- .await
- .context("creating session")?;
-
- import_remote(io, db, &session, identifier.clone(), node_path)
- .await
- .context("federated import")?
- } else {
- debug!("adding {identifier}");
- node.public.path = node_path;
- node.public.id = Some(identifier.to_owned());
- let did_insert = db.node.insert(&identifier, &node)?.is_none();
- if did_insert {
- vec![identifier]
- } else {
- vec![]
- }
- };
- Ok((idents, 0))
- } else {
- bail!("did somebody really put a fifo or socket in the library?!")
- }
-}
-
-static SEM_REMOTE_IMPORT: LazyLock<Semaphore> = LazyLock::new(|| Semaphore::new(16));
-
-#[async_recursion]
-async fn import_remote(
- mut opts: RemoteImportOptions,
- db: &Database,
- session: &Arc<Session>,
- identifier: String,
- mut node_path: Vec<String>,
-) -> anyhow::Result<Vec<String>> {
- let _permit = SEM_REMOTE_IMPORT.acquire().await.unwrap();
- info!("loading federated node {identifier:?}");
-
- let flatten = opts.flatten;
- opts.flatten = false;
-
- let node = session
- .node(&opts.id)
- .await
- .context("fetching remote node")?;
-
- if node.federated.as_ref() == Some(&CONF.hostname) {
- return Ok(vec![]); // node is federated from us, lets not import it
- }
-
- let poster =
- cache_federation_asset(session.to_owned(), opts.id.clone(), AssetRole::Poster).await?;
- let backdrop =
- cache_federation_asset(session.to_owned(), opts.id.clone(), AssetRole::Backdrop).await?;
-
- drop(_permit);
-
- let mut did_insert = false;
- if !flatten {
- let mut node = Node {
- public: node.clone(),
- private: NodePrivate {
- backdrop: Some(backdrop),
- poster: Some(poster),
- import: None,
- id: None,
- source: Some(MediaSource::Remote {
- host: opts.host.clone(),
- remote_id: opts.id.clone(),
- }),
- },
- };
- node.public.path = node_path.clone();
- node.public.federated = Some(opts.host.clone());
- node.public
- .children
- .iter_mut()
- .for_each(|c| *c = format!("{}{c}", opts.prefix.clone().unwrap_or(String::new())));
-
- debug!("adding {identifier}");
- node.public.id = Some(identifier.to_owned());
- did_insert = db
- .node
- .fetch_and_update(&identifier, |pnode| Some(pnode.unwrap_or(node.clone())))?
- .is_none();
- node_path.push(opts.id.clone());
- }
-
- let mut children: FuturesUnordered<_> = node
- .children
- .iter()
- .map(|c| {
- let prefixed = format!("{}{c}", opts.prefix.clone().unwrap_or(String::new()));
- import_remote(
- RemoteImportOptions {
- id: c.to_owned(),
- ..opts.clone()
- },
- db,
- session,
- prefixed,
- node_path.clone(),
- )
- })
- .collect();
-
- let mut children_idents = Vec::new();
- while let Some(r) = children.next().await {
- children_idents.extend(r?);
- }
- Ok(if flatten {
- children_idents
- } else if did_insert {
- vec![identifier]
- } else {
- vec![]
- })
-}
-
-async fn cache_federation_asset(
- session: Arc<Session>,
- identifier: String,
- role: AssetRole,
-) -> anyhow::Result<AssetLocation> {
- async_cache_file(
- &["federation-asset", role.as_str(), &identifier.clone()],
- move |out| async move {
- let session = session;
- session
- .node_asset(identifier.as_str(), role, 1024, out)
- .await
- },
- )
- .await
-}
diff --git a/server/src/main.rs b/server/src/main.rs
index 6e732bd..d9ddf8b 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -8,16 +8,13 @@
use crate::routes::ui::{account::hash_password, admin::log::enable_logging};
use database::Database;
-use federation::Federation;
-use jellybase::CONF;
+use jellybase::{CONF, federation::Federation};
use jellycommon::user::{PermissionSet, Theme, User};
use log::{error, warn};
use routes::build_rocket;
use tokio::fs::create_dir_all;
pub use jellybase::database;
-pub mod federation;
-pub mod import;
pub mod routes;
#[rocket::main]
diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs
index e95d714..4c7838d 100644
--- a/server/src/routes/mod.rs
+++ b/server/src/routes/mod.rs
@@ -3,10 +3,10 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2023 metamuffin <metamuffin.org>
*/
-use crate::{database::Database, federation::Federation, routes::ui::error::MyResult};
+use crate::{database::Database, routes::ui::error::MyResult};
use api::{r_api_account_login, r_api_node_raw, r_api_root, r_api_version};
use base64::Engine;
-use jellybase::CONF;
+use jellybase::{CONF, federation::Federation};
use log::warn;
use progress::{r_player_progress, r_player_watched};
use rand::random;
diff --git a/server/src/routes/stream.rs b/server/src/routes/stream.rs
index e7b3d54..14db462 100644
--- a/server/src/routes/stream.rs
+++ b/server/src/routes/stream.rs
@@ -4,14 +4,14 @@
Copyright (C) 2023 metamuffin <metamuffin.org>
*/
use super::ui::{account::session::Session, error::MyError};
-use crate::{database::Database, federation::Federation};
+use crate::database::Database;
use anyhow::{anyhow, Result};
use jellybase::{
+ federation::Federation,
permission::{NodePermissionExt, PermissionSetExt},
CONF,
};
-use jellyclient::LoginDetails;
-use jellycommon::{stream::StreamSpec, user::UserPermission, MediaSource};
+use jellycommon::{stream::StreamSpec, user::UserPermission, TrackSource};
use log::{info, warn};
use rocket::{
get, head,
@@ -58,35 +58,36 @@ pub async fn r_stream(
.as_ref()
.ok_or(anyhow!("item does not contain media"))?;
- if let MediaSource::Remote { host, remote_id } = source {
- session
- .user
- .permissions
- .assert(&UserPermission::FederatedContent)?;
+ // TODO federated streams
+ // if let MediaSource::Remote { host, remote_id } = source {
+ // session
+ // .user
+ // .permissions
+ // .assert(&UserPermission::FederatedContent)?;
- let (username, password, _) = CONF
- .remote_credentials
- .get(host)
- .ok_or(anyhow!("no credentials on the server-side"))?;
+ // let (username, password, _) = CONF
+ // .remote_credentials
+ // .get(host)
+ // .ok_or(anyhow!("no credentials on the server-side"))?;
- info!("creating session on {host}");
- let instance = federation.get_instance(&host)?.to_owned();
- let session = instance
- .login(LoginDetails {
- username: username.to_owned(),
- password: password.to_owned(),
- expire: Some(60),
- drop_permissions: Some(HashSet::from_iter([
- UserPermission::ManageSelf,
- UserPermission::Admin, // in case somebody federated the admin :)))
- ])),
- })
- .await?;
+ // info!("creating session on {host}");
+ // let instance = federation.get_instance(&host)?.to_owned();
+ // let session = instance
+ // .login(LoginDetails {
+ // username: username.to_owned(),
+ // password: password.to_owned(),
+ // expire: Some(60),
+ // drop_permissions: Some(HashSet::from_iter([
+ // UserPermission::ManageSelf,
+ // UserPermission::Admin, // in case somebody federated the admin :)))
+ // ])),
+ // })
+ // .await?;
- let uri = session.stream(&remote_id, &spec);
- info!("federation redirect");
- return Ok(Either::Right(RedirectResponse(uri)));
- }
+ // let uri = session.stream(&remote_id, &spec);
+ // info!("federation redirect");
+ // return Ok(Either::Right(RedirectResponse(uri)));
+ // }
info!(
"stream request (range={})",
diff --git a/server/src/routes/ui/admin/mod.rs b/server/src/routes/ui/admin/mod.rs
index 0d1ee0a..b976192 100644
--- a/server/src/routes/ui/admin/mod.rs
+++ b/server/src/routes/ui/admin/mod.rs
@@ -9,8 +9,6 @@ pub mod user;
use super::account::session::AdminSession;
use crate::{
database::Database,
- federation::Federation,
- import::import,
routes::ui::{
admin::log::rocket_uri_macro_r_admin_log,
error::MyResult,
@@ -19,7 +17,8 @@ use crate::{
uri,
};
use anyhow::anyhow;
-use jellybase::CONF;
+use jellybase::{federation::Federation, CONF};
+use jellyimport::import;
use rand::Rng;
use rocket::{form::Form, get, post, FromForm, State};
use std::time::Instant;
diff --git a/server/src/routes/ui/node.rs b/server/src/routes/ui/node.rs
index 6e2f532..bcb7362 100644
--- a/server/src/routes/ui/node.rs
+++ b/server/src/routes/ui/node.rs
@@ -72,7 +72,7 @@ pub async fn r_library_node_filter<'a>(
filter_and_sort_nodes(&filter, &mut children);
Ok(Either::Left(LayoutPage {
- title: node.title.to_string(),
+ title: node.title.clone().unwrap_or_default(),
content: markup::new! {
@NodePage { node: &node, id: &id, udata: &udata, children: &children, filter: &filter }
},
@@ -82,14 +82,14 @@ pub async fn r_library_node_filter<'a>(
markup::define! {
NodeCard<'a>(id: &'a str, node: &'a NodePublic, udata: &'a NodeUserData) {
- @let cls = format!("node card poster {}", match node.kind {NodeKind::Channel => "aspect-square", NodeKind::Video => "aspect-thumb", NodeKind::Collection => "aspect-land", _ => "aspect-port"});
+ @let cls = format!("node card poster {}", match node.kind.unwrap_or_default() {NodeKind::Channel => "aspect-square", NodeKind::Video => "aspect-thumb", NodeKind::Collection => "aspect-land", _ => "aspect-port"});
div[class=cls] {
.poster {
a[href=uri!(r_library_node(id))] {
img[src=uri!(r_item_assets(id, AssetRole::Poster, Some(1024)))];
}
.cardhover.item {
- @if !(matches!(node.kind, NodeKind::Collection | NodeKind::Channel)) {
+ @if !(matches!(node.kind.unwrap_or_default(), NodeKind::Collection | NodeKind::Channel)) {
a.play.icon[href=&uri!(r_player(id, PlayerConfig::default()))] { "play_arrow" }
}
@Props { node, udata }
@@ -103,17 +103,17 @@ markup::define! {
}
}
NodePage<'a>(id: &'a str, node: &'a NodePublic, udata: &'a NodeUserData, children: &'a Vec<(String, NodePublic, NodeUserData)>, filter: &'a NodeFilterSort) {
- @if !matches!(node.kind, NodeKind::Collection) {
+ @if !matches!(node.kind.unwrap_or_default(), NodeKind::Collection) {
img.backdrop[src=uri!(r_item_assets(id, AssetRole::Backdrop, Some(2048)))];
}
.page.node {
- @if !matches!(node.kind, NodeKind::Collection) {
+ @if !matches!(node.kind.unwrap_or_default(), NodeKind::Collection) {
div.bigposter { img[src=uri!(r_item_assets(id, AssetRole::Poster, Some(2048)))]; }
}
.title {
h1 { @node.title }
@if node.media.is_some() { a.play[href=&uri!(r_player(id, PlayerConfig::default()))] { "Watch now" }}
- @if !matches!(node.kind, NodeKind::Collection | NodeKind::Channel) {
+ @if !matches!(node.kind.unwrap_or_default(), NodeKind::Collection | NodeKind::Channel) {
@match udata.watched {
WatchedState::None |
WatchedState::Progress(_) => {
@@ -152,15 +152,15 @@ markup::define! {
}
}
}
- @if matches!(node.kind, NodeKind::Collection | NodeKind::Channel) {
- @if matches!(node.kind, NodeKind::Collection) {
+ @if matches!(node.kind.unwrap_or_default(), NodeKind::Collection | NodeKind::Channel) {
+ @if matches!(node.kind.unwrap_or_default(), NodeKind::Collection) {
@if let Some(parent) = &node.path.last().cloned() {
a.dirup[href=uri!(r_library_node(parent))] { "Go up" }
}
}
@NodeFilterSortForm { f: filter }
}
- @match node.kind {
+ @match node.kind.unwrap_or_default() {
NodeKind::Collection | NodeKind::Channel => {
ul.children {@for (id, node, udata) in children.iter() {
li { @NodeCard { id, node, udata } }
@@ -261,7 +261,9 @@ impl MediaInfoExt for MediaInfo {
let mut maxdim = 0;
for t in &self.tracks {
match &t.kind {
- SourceTrackKind::Video { width, height, .. } => maxdim = maxdim.max(*width.max(height)),
+ SourceTrackKind::Video { width, height, .. } => {
+ maxdim = maxdim.max(*width.max(height))
+ }
_ => (),
}
}
diff --git a/server/src/routes/ui/player.rs b/server/src/routes/ui/player.rs
index 177a5f6..8b8adf6 100644
--- a/server/src/routes/ui/player.rs
+++ b/server/src/routes/ui/player.rs
@@ -59,7 +59,7 @@ pub fn r_player<'a>(
let conf = player_conf(item.clone(), playing)?;
Ok(LayoutPage {
- title: item.public.title.to_owned(),
+ title: item.public.title.to_owned().unwrap_or_default(),
class: Some("player"),
content: markup::new! {
@if playing {
diff --git a/server/src/routes/ui/sort.rs b/server/src/routes/ui/sort.rs
index c7fbfc2..143a101 100644
--- a/server/src/routes/ui/sort.rs
+++ b/server/src/routes/ui/sort.rs
@@ -139,14 +139,14 @@ pub fn filter_and_sort_nodes(
o &= !match p {
FilterProperty::FederationLocal => node.federated.is_none(),
FilterProperty::FederationRemote => node.federated.is_some(),
- FilterProperty::KindMovie => node.kind == NodeKind::Movie,
- FilterProperty::KindVideo => node.kind == NodeKind::Video,
- FilterProperty::KindCollection => node.kind == NodeKind::Collection,
- FilterProperty::KindChannel => node.kind == NodeKind::Channel,
- FilterProperty::KindShow => node.kind == NodeKind::Show,
- FilterProperty::KindSeries => node.kind == NodeKind::Series,
- FilterProperty::KindSeason => node.kind == NodeKind::Season,
- FilterProperty::KindEpisode => node.kind == NodeKind::Episode,
+ FilterProperty::KindMovie => node.kind == Some(NodeKind::Movie),
+ FilterProperty::KindVideo => node.kind == Some(NodeKind::Video),
+ FilterProperty::KindCollection => node.kind == Some(NodeKind::Collection),
+ FilterProperty::KindChannel => node.kind == Some(NodeKind::Channel),
+ FilterProperty::KindShow => node.kind == Some(NodeKind::Show),
+ FilterProperty::KindSeries => node.kind == Some(NodeKind::Series),
+ FilterProperty::KindSeason => node.kind == Some(NodeKind::Season),
+ FilterProperty::KindEpisode => node.kind == Some(NodeKind::Episode),
FilterProperty::Watched => udata.watched == WatchedState::Watched,
FilterProperty::Unwatched => udata.watched == WatchedState::None,
FilterProperty::WatchProgress => {
diff --git a/stream/src/lib.rs b/stream/src/lib.rs
index 604e3eb..1ee0690 100644
--- a/stream/src/lib.rs
+++ b/stream/src/lib.rs
@@ -15,7 +15,7 @@ use jellybase::{permission::PermissionSetExt, CONF};
use jellycommon::{
stream::{StreamFormat, StreamSpec},
user::{PermissionSet, UserPermission},
- LocalTrack, MediaSource, Node,
+ LocalTrack, Node,
};
use jhls::jhls_stream;
use segment::segment_stream;
@@ -55,13 +55,14 @@ pub async fn stream(
let (a, b) = duplex(4096);
+ // TODO remux of mixed remote and local tracks?!
let track_sources = match node
.private
.source
.as_ref()
.ok_or(anyhow!("node has no media"))?
{
- MediaSource::Local { tracks } => tracks.to_owned(),
+ // MediaSource::Local { tracks } => tracks.to_owned(),
_ => bail!("node tracks are not local"),
};
diff --git a/stream/src/webvtt.rs b/stream/src/webvtt.rs
index 6ee5212..faf0cd3 100644
--- a/stream/src/webvtt.rs
+++ b/stream/src/webvtt.rs
@@ -41,8 +41,12 @@ pub async fn webvtt_stream(
})
.await??;
- let webvtt = webvtt_from_ass_blocks(node.public.title, codec_private, ass_blocks)
- .context("transcoding subtitles")?;
+ let webvtt = webvtt_from_ass_blocks(
+ node.public.title.clone().unwrap_or_default(),
+ codec_private,
+ ass_blocks,
+ )
+ .context("transcoding subtitles")?;
tokio::task::spawn(async move {
let _ = b.write_all(webvtt.as_bytes()).await;
diff --git a/tool/src/main.rs b/tool/src/main.rs
index 31e63b7..34337ce 100644
--- a/tool/src/main.rs
+++ b/tool/src/main.rs
@@ -5,12 +5,10 @@
*/
#![feature(file_create_new)]
-pub mod import;
pub mod migrate;
use base64::Engine;
use clap::{Parser, Subcommand, ValueEnum};
-use import::import;
use jellyclient::{Instance, LoginDetails};
use jellycommon::{config::GlobalConfig, Node, NodeKind, NodePrivate, NodePublic};
use log::{info, warn};
@@ -37,47 +35,47 @@ enum Action {
#[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<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,
- /// 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<PathBuf>,
- /// 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<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,
+ // /// 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<PathBuf>,
+ // /// 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<String>,
+ // },
Migrate {
database: PathBuf,
mode: MigrateMode,
@@ -118,8 +116,11 @@ fn main() -> anyhow::Result<()> {
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 {
@@ -149,8 +150,8 @@ fn main() -> anyhow::Result<()> {
File::create_new(path.join("library/directory.json"))?,
&Node {
public: NodePublic {
- kind: NodeKind::Collection,
- title: "My Library".to_string(),
+ kind: Some(NodeKind::Collection),
+ title: Some("My Library".to_string()),
..Default::default()
},
private: NodePrivate {
@@ -162,7 +163,7 @@ fn main() -> anyhow::Result<()> {
warn!("please change the admin password.");
Ok(())
}
- a @ Action::New { .. } => import(a, args.dry),
+ // a @ Action::New { .. } => import(a, args.dry),
a @ Action::Migrate { .. } => migrate(a),
Action::Reimport {
config,