aboutsummaryrefslogtreecommitdiff
path: root/server/src/ui/assets.rs
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2025-04-27 19:25:11 +0200
committermetamuffin <metamuffin@disroot.org>2025-04-27 19:25:11 +0200
commit11a585b3dbe620dcc8772e713b22f1d9ba80d598 (patch)
tree44f8d97137412aefc79a2425a489c34fa3e5f6c5 /server/src/ui/assets.rs
parentd871aa7c5bba49ff55170b5d2dac9cd440ae7170 (diff)
downloadjellything-11a585b3dbe620dcc8772e713b22f1d9ba80d598.tar
jellything-11a585b3dbe620dcc8772e713b22f1d9ba80d598.tar.bz2
jellything-11a585b3dbe620dcc8772e713b22f1d9ba80d598.tar.zst
move files around
Diffstat (limited to 'server/src/ui/assets.rs')
-rw-r--r--server/src/ui/assets.rs201
1 files changed, 201 insertions, 0 deletions
diff --git a/server/src/ui/assets.rs b/server/src/ui/assets.rs
new file mode 100644
index 0000000..ce2a8e2
--- /dev/null
+++ b/server/src/ui/assets.rs
@@ -0,0 +1,201 @@
+/*
+ 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) 2025 metamuffin <metamuffin.org>
+*/
+use super::{error::MyResult, CacheControlFile};
+use crate::logic::session::Session;
+use anyhow::{anyhow, bail, Context};
+use base64::Engine;
+use jellybase::{
+ assetfed::AssetInner, cache::async_cache_file, database::Database, federation::Federation, CONF,
+};
+use jellycommon::{LocalTrack, NodeID, PeopleGroup, SourceTrackKind, TrackSource};
+use log::info;
+use rocket::{get, http::ContentType, response::Redirect, State};
+use std::{path::PathBuf, str::FromStr};
+
+pub const AVIF_QUALITY: f32 = 50.;
+pub const AVIF_SPEED: u8 = 5;
+
+#[get("/asset/<token>?<width>")]
+pub async fn r_asset(
+ _session: Session,
+ fed: &State<Federation>,
+ token: &str,
+ width: Option<usize>,
+) -> MyResult<(ContentType, CacheControlFile)> {
+ let width = width.unwrap_or(2048);
+ let asset = AssetInner::deser(token)?;
+
+ let path = if let AssetInner::Federated { host, asset } = asset {
+ let session = fed.get_session(&host).await?;
+
+ let asset = base64::engine::general_purpose::URL_SAFE.encode(asset);
+ async_cache_file("fed-asset", &asset, |out| async {
+ session.asset(out, &asset, width).await
+ })
+ .await?
+ } else {
+ let source = resolve_asset(asset).await.context("resolving asset")?;
+
+ // fit the resolution into a finite set so the maximum cache is finite too.
+ let width = 2usize.pow(width.clamp(128, 2048).ilog2());
+ jellytranscoder::image::transcode(&source, AVIF_QUALITY, AVIF_SPEED, width)
+ .await
+ .context("transcoding asset")?
+ };
+ info!("loading asset from {path:?}");
+ Ok((
+ ContentType::AVIF,
+ CacheControlFile::new_cachekey(&path.abs()).await?,
+ ))
+}
+
+pub async fn resolve_asset(asset: AssetInner) -> anyhow::Result<PathBuf> {
+ match asset {
+ AssetInner::Cache(c) => Ok(c.abs()),
+ AssetInner::Assets(c) => Ok(CONF.asset_path.join(c)),
+ AssetInner::Media(c) => Ok(CONF.media_path.join(c)),
+ _ => bail!("wrong asset type"),
+ }
+}
+
+#[get("/n/<id>/poster?<width>")]
+pub async fn r_item_poster(
+ _session: Session,
+ db: &State<Database>,
+ id: NodeID,
+ width: Option<usize>,
+) -> MyResult<Redirect> {
+ // TODO perm
+ let node = db.get_node(id)?.ok_or(anyhow!("node does not exist"))?;
+
+ let mut asset = node.poster.clone();
+ if asset.is_none() {
+ if let Some(parent) = node.parents.last().copied() {
+ let parent = db.get_node(parent)?.ok_or(anyhow!("node does not exist"))?;
+ asset = parent.poster.clone();
+ }
+ };
+ let asset = asset.unwrap_or_else(|| {
+ AssetInner::Assets(format!("fallback-{:?}.avif", node.kind).into()).ser()
+ });
+ Ok(Redirect::permanent(rocket::uri!(r_asset(asset.0, width))))
+}
+
+#[get("/n/<id>/backdrop?<width>")]
+pub async fn r_item_backdrop(
+ _session: Session,
+ db: &State<Database>,
+ id: NodeID,
+ width: Option<usize>,
+) -> MyResult<Redirect> {
+ // TODO perm
+ let node = db.get_node(id)?.ok_or(anyhow!("node does not exist"))?;
+
+ let mut asset = node.backdrop.clone();
+ if asset.is_none() {
+ if let Some(parent) = node.parents.last().copied() {
+ let parent = db.get_node(parent)?.ok_or(anyhow!("node does not exist"))?;
+ asset = parent.backdrop.clone();
+ }
+ };
+ let asset = asset.unwrap_or_else(|| {
+ AssetInner::Assets(format!("fallback-{:?}.avif", node.kind).into()).ser()
+ });
+ Ok(Redirect::permanent(rocket::uri!(r_asset(asset.0, width))))
+}
+
+#[get("/n/<id>/person/<index>/asset?<group>&<width>")]
+pub async fn r_person_asset(
+ _session: Session,
+ db: &State<Database>,
+ id: NodeID,
+ index: usize,
+ group: String,
+ width: Option<usize>,
+) -> MyResult<Redirect> {
+ // TODO perm
+
+ let node = db.get_node(id)?.ok_or(anyhow!("node does not exist"))?;
+ let app = node
+ .people
+ .get(&PeopleGroup::from_str(&group).map_err(|()| anyhow!("unknown people group"))?)
+ .ok_or(anyhow!("group has no members"))?
+ .get(index)
+ .ok_or(anyhow!("person does not exist"))?;
+
+ let asset = app
+ .person
+ .headshot
+ .to_owned()
+ .unwrap_or(AssetInner::Assets("fallback-Person.avif".into()).ser());
+ Ok(Redirect::permanent(rocket::uri!(r_asset(asset.0, width))))
+}
+
+// TODO this can create "federation recursion" because track selection cannot be relied on.
+//? TODO is this still relevant?
+
+#[get("/n/<id>/thumbnail?<t>&<width>")]
+pub async fn r_node_thumbnail(
+ _session: Session,
+ db: &State<Database>,
+ fed: &State<Federation>,
+ id: NodeID,
+ t: f64,
+ width: Option<usize>,
+) -> MyResult<Redirect> {
+ let node = db.get_node(id)?.ok_or(anyhow!("node does not exist"))?;
+
+ let media = node.media.as_ref().ok_or(anyhow!("no media"))?;
+ let (thumb_track_index, thumb_track) = media
+ .tracks
+ .iter()
+ .enumerate()
+ .find(|(_i, t)| matches!(t.kind, SourceTrackKind::Video { .. }))
+ .ok_or(anyhow!("no video track to create a thumbnail of"))?;
+ let source = media
+ .tracks
+ .get(thumb_track_index)
+ .ok_or(anyhow!("no source"))?;
+ let thumb_track_source = source.source.clone();
+
+ if t < 0. || t > media.duration {
+ Err(anyhow!("thumbnail instant not within media duration"))?
+ }
+
+ let step = 8.;
+ let t = (t / step).floor() * step;
+
+ let asset = match thumb_track_source {
+ TrackSource::Local(a) => {
+ let AssetInner::LocalTrack(LocalTrack { path, .. }) = AssetInner::deser(&a.0)? else {
+ return Err(anyhow!("track set to wrong asset type").into());
+ };
+ // the track selected might be different from thumb_track
+ jellytranscoder::thumbnail::create_thumbnail(&CONF.media_path.join(path), t).await?
+ }
+ TrackSource::Remote(_) => {
+ // TODO in the new system this is preferrably a property of node ext for regular fed
+ let session = fed
+ .get_session(
+ thumb_track
+ .federated
+ .last()
+ .ok_or(anyhow!("federation broken"))?,
+ )
+ .await?;
+
+ async_cache_file("fed-thumb", (id, t as i64), |out| {
+ session.node_thumbnail(out, id.into(), 2048, t)
+ })
+ .await?
+ }
+ };
+
+ Ok(Redirect::temporary(rocket::uri!(r_asset(
+ AssetInner::Cache(asset).ser().0,
+ width
+ ))))
+}