diff options
author | metamuffin <metamuffin@disroot.org> | 2025-04-27 19:25:11 +0200 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2025-04-27 19:25:11 +0200 |
commit | 11a585b3dbe620dcc8772e713b22f1d9ba80d598 (patch) | |
tree | 44f8d97137412aefc79a2425a489c34fa3e5f6c5 /server/src/ui/assets.rs | |
parent | d871aa7c5bba49ff55170b5d2dac9cd440ae7170 (diff) | |
download | jellything-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.rs | 201 |
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 + )))) +} |