aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock5
-rw-r--r--client/src/lib.rs23
-rw-r--r--common/src/lib.rs12
-rw-r--r--server/Cargo.toml1
-rw-r--r--server/src/import.rs48
-rw-r--r--server/src/routes/ui/assets.rs23
-rw-r--r--tools/src/bin/import.rs6
7 files changed, 95 insertions, 23 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 2bdb234..00a0a27 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1276,6 +1276,7 @@ dependencies = [
"rocket",
"serde",
"serde_json",
+ "sha2",
"sled",
"tokio",
"tokio-util",
@@ -2198,9 +2199,9 @@ dependencies = [
[[package]]
name = "sha2"
-version = "0.10.6"
+version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
+checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8"
dependencies = [
"cfg-if",
"cpufeatures",
diff --git a/client/src/lib.rs b/client/src/lib.rs
index 39bd1fd..b208525 100644
--- a/client/src/lib.rs
+++ b/client/src/lib.rs
@@ -5,7 +5,7 @@ use reqwest::{
Client,
};
use serde_json::json;
-use std::time::Duration;
+use std::{io::Write, time::Duration};
#[derive(Debug, Clone)]
pub struct Instance {
@@ -79,15 +79,18 @@ impl Session {
.await?)
}
- // pub async fn node_asset(&self, id: &str, role: AssetRole) -> Result<Node> {
- // Ok(self
- // .client
- // .get(format!("/n/{id}"))
- // .send()
- // .await?
- // .bytes()
- // .await?)
- // }
+ // TODO use AssetRole instead of str
+ pub async fn node_asset(&self, id: &str, role: &str, mut writer: impl Write) -> Result<()> {
+ let mut r = self
+ .client
+ .get(format!("{}/n/{id}/asset?role={role}", self.instance.base()))
+ .send()
+ .await?;
+ while let Some(chunk) = r.chunk().await? {
+ writer.write_all(&chunk)?;
+ }
+ Ok(())
+ }
pub fn stream(&self, id: &str, tracks: &[usize], webm: bool) -> String {
format!(
diff --git a/common/src/lib.rs b/common/src/lib.rs
index 6f6caf5..be25157 100644
--- a/common/src/lib.rs
+++ b/common/src/lib.rs
@@ -20,8 +20,8 @@ pub struct Node {
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct NodePrivate {
#[serde(default)] pub import: Option<RemoteImportOptions>,
- #[serde(default)] pub poster: Option<PathBuf>,
- #[serde(default)] pub backdrop: Option<PathBuf>,
+ #[serde(default)] pub poster: Option<AssetLocation>,
+ #[serde(default)] pub backdrop: Option<AssetLocation>,
#[serde(default)] pub source: Option<MediaSource>,
}
@@ -48,6 +48,14 @@ pub struct RemoteImportOptions {
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
+pub enum AssetLocation {
+ Cache(PathBuf),
+ Library(PathBuf),
+ Assets(PathBuf),
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+#[serde(rename_all = "snake_case")]
pub enum NodeKind {
Movie,
Collection,
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 6118240..6648b80 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -22,6 +22,7 @@ base64 = "0.21.2"
chrono = { version = "0.4.26", features = ["serde"] }
argon2 = "0.5.1"
+sha2 = "0.10.7"
aes-gcm-siv = "0.11.1"
async-std = "1.12.0"
diff --git a/server/src/import.rs b/server/src/import.rs
index 2cd1998..a059694 100644
--- a/server/src/import.rs
+++ b/server/src/import.rs
@@ -6,10 +6,13 @@
use crate::{database::Database, federation::Federation, CONF};
use anyhow::{anyhow, bail, Context, Ok};
use async_recursion::async_recursion;
+use base64::Engine;
use jellyclient::Session;
-use jellycommon::{MediaSource, Node, NodePrivate, RemoteImportOptions};
+use jellycommon::{AssetLocation, MediaSource, Node, NodePrivate, RemoteImportOptions};
use log::{error, info};
-use std::{ffi::OsStr, fs::File, os::unix::prelude::OsStrExt, path::PathBuf, sync::LazyLock};
+use std::{
+ ffi::OsStr, fs::File, os::unix::prelude::OsStrExt, path::PathBuf, str::FromStr, sync::LazyLock,
+};
use tokio::sync::Semaphore;
static IMPORT_SEM: LazyLock<Semaphore> = LazyLock::new(|| Semaphore::new(1));
@@ -125,11 +128,15 @@ async fn import_remote(
.node(&opts.id)
.await
.context("fetching remote node")?;
+
+ let poster = cache_federation_asset(session, &opts.id, "poster").await?;
+ let backdrop = cache_federation_asset(session, &opts.id, "backdrop").await?;
+
let mut node = Node {
public: node,
private: NodePrivate {
- backdrop: None,
- poster: None,
+ backdrop: Some(AssetLocation::Cache(backdrop)),
+ poster: Some(AssetLocation::Cache(poster)),
import: None,
source: Some(MediaSource::Remote {
host: opts.host.clone(),
@@ -160,3 +167,36 @@ async fn import_remote(
Ok(())
}
+
+async fn cache_federation_asset(
+ session: &Session,
+ identifier: &String,
+ role: &str,
+) -> anyhow::Result<PathBuf> {
+ let (poster, download) = cache_file(&["federation-asset", role, identifier]);
+ if let Some(d) = download {
+ session.node_asset(&identifier, role, d).await?;
+ }
+ Ok(poster)
+}
+
+fn cache_file(seed: &[&str]) -> (PathBuf, Option<File>) {
+ use sha2::Digest;
+ let mut d = sha2::Sha512::new();
+ for s in seed {
+ d.update(s.as_bytes());
+ d.update(b"\0");
+ }
+ let d = d.finalize();
+ let fname = base64::engine::general_purpose::URL_SAFE.encode(d);
+ let fname = &fname[..22]; // about 128 bits
+ let fullpath = CONF.cache_path.join(fname);
+ let cachepath = PathBuf::from_str(fname).unwrap();
+
+ let f = if !fullpath.exists() {
+ Some(File::create(&fullpath).unwrap())
+ } else {
+ None
+ };
+ (cachepath, f)
+}
diff --git a/server/src/routes/ui/assets.rs b/server/src/routes/ui/assets.rs
index 7fa083c..4b5c64a 100644
--- a/server/src/routes/ui/assets.rs
+++ b/server/src/routes/ui/assets.rs
@@ -9,6 +9,7 @@ use crate::{
CONF,
};
use anyhow::anyhow;
+use jellycommon::AssetLocation;
use log::info;
use rocket::{get, http::ContentType, FromFormField, State, UriDisplayQuery};
use std::{path::PathBuf, str::FromStr};
@@ -16,7 +17,9 @@ use tokio::fs::File;
#[derive(FromFormField, UriDisplayQuery)]
pub enum AssetRole {
+ #[field(value = "poster")]
Poster,
+ #[field(value = "backdrop")]
Backdrop,
}
@@ -32,15 +35,31 @@ pub async fn r_item_assets(
AssetRole::Backdrop => node.private.backdrop,
AssetRole::Poster => node.private.poster,
}
- .map(|e| CONF.library_path.join(e))
+ .map(|e| e.path())
.unwrap_or_else(|| {
CONF.asset_path
.join(PathBuf::from_str("fallback.jpeg").unwrap())
});
info!("loading asset from {path:?}");
- let ext = path.extension().unwrap().to_str().unwrap();
+ let ext = path
+ .extension()
+ .map(|e| e.to_str().unwrap())
+ .unwrap_or("jpeg");
Ok((
ContentType::from_extension(ext).unwrap(),
CacheControlFile::new(File::open(path).await?).await,
))
}
+
+pub trait AssetLocationExt {
+ fn path(&self) -> PathBuf;
+}
+impl AssetLocationExt for AssetLocation {
+ fn path(&self) -> PathBuf {
+ match self {
+ AssetLocation::Assets(p) => CONF.asset_path.join(p),
+ AssetLocation::Cache(p) => CONF.cache_path.join(p),
+ AssetLocation::Library(p) => CONF.library_path.join(p),
+ }
+ }
+}
diff --git a/tools/src/bin/import.rs b/tools/src/bin/import.rs
index 9e3dc4e..78c8646 100644
--- a/tools/src/bin/import.rs
+++ b/tools/src/bin/import.rs
@@ -5,7 +5,7 @@
*/
use anyhow::Context;
use clap::{Parser, Subcommand};
-use jellycommon::{MediaInfo, Node, NodeKind, NodePrivate, NodePublic};
+use jellycommon::{AssetLocation, MediaInfo, Node, NodeKind, NodePrivate, NodePublic};
use jellymatroska::read::EbmlReader;
use jellyremuxer::import::import_read;
use jellytools::tmdb::{tmdb_details, tmdb_image, tmdb_search};
@@ -167,8 +167,8 @@ fn main() -> anyhow::Result<()> {
let node = Node {
private: NodePrivate {
import: None,
- backdrop: backdrop.clone(),
- poster: poster.clone(),
+ backdrop: backdrop.clone().map(AssetLocation::Library),
+ poster: poster.clone().map(AssetLocation::Library),
source,
},
public: NodePublic {