/* 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 */ use super::{error::MyResult, CacheControlFile}; use crate::logic::session::Session; use anyhow::{anyhow, bail, Context}; use base64::Engine; use jellybase::{assetfed::AssetInner, database::Database, federation::Federation, CONF}; use jellycache::async_cache_file; 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/?")] pub async fn r_asset( _session: Session, fed: &State, token: &str, width: Option, ) -> 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 { 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//poster?")] pub async fn r_item_poster( _session: Session, db: &State, id: NodeID, width: Option, ) -> MyResult { // 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//backdrop?")] pub async fn r_item_backdrop( _session: Session, db: &State, id: NodeID, width: Option, ) -> MyResult { // 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//person//asset?&")] pub async fn r_person_asset( _session: Session, db: &State, id: NodeID, index: usize, group: String, width: Option, ) -> MyResult { // 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//thumbnail?&")] pub async fn r_node_thumbnail( _session: Session, db: &State, fed: &State, id: NodeID, t: f64, width: Option, ) -> MyResult { 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 )))) }