/* 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) 2024 metamuffin */ use crate::routes::ui::{account::session::Session, error::MyResult, CacheControlFile}; use anyhow::{anyhow, Context}; use base64::Engine; use jellybase::{ assetfed::AssetInner, cache::async_cache_file, database::{DataAcid, TableExt, T_NODE, T_NODE_EXTENDED}, federation::Federation, permission::NodePermissionExt, CONF, }; pub use jellycommon::AssetRole; use jellycommon::{LocalTrack, PeopleGroup, SourceTrackKind, TrackSource}; use log::info; use rocket::{get, http::ContentType, response::Redirect, State}; use std::path::PathBuf; #[get("/asset/?")] pub async fn r_asset( _session: Session, fed: &State, token: &str, width: Option, ) -> MyResult<(ContentType, CacheControlFile)> { let asset = AssetInner::deser(token)?; let source = resolve_asset(asset, fed).await.context("resolving asset")?; // fit the resolution into a finite set so the maximum cache is finite too. let width = 2usize.pow(width.unwrap_or(2048).clamp(128, 2048).ilog2()); // TODO configure avif quality and speed. let path = jellytranscoder::image::transcode(source, 50., 5, 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, fed: &State) -> anyhow::Result { match asset { AssetInner::Federated { host, asset } => { let session = fed.get_session(&host).await?; let asset = base64::engine::general_purpose::URL_SAFE.encode(asset); Ok(async_cache_file(&["fed-asset", &asset], |out| async { session.asset(out, &asset, 2048).await }) .await? .abs()) } AssetInner::Cache(c) => Ok(c.abs()), AssetInner::Assets(c) => Ok(CONF.asset_path.join(c)), AssetInner::Library(c) => Ok(CONF.library_path.join(c)), } } #[get("/n//asset?&")] pub async fn r_item_assets( session: Session, db: &State, id: &str, role: AssetRole, width: Option, ) -> MyResult { let node = T_NODE .get(&db, id)? .only_if_permitted(&session.user.permissions) .ok_or(anyhow!("node does not exist"))?; let mut asset = match role { AssetRole::Backdrop => node.public.backdrop, AssetRole::Poster => node.public.poster, }; if let None = asset { if let Some(parent) = &node.public.path.last() { let parent = T_NODE .get(&db, parent.as_str())? .ok_or(anyhow!("node does not exist"))?; asset = match role { AssetRole::Backdrop => parent.public.backdrop, AssetRole::Poster => parent.public.poster, }; } }; let asset = asset.unwrap_or_else(|| { AssetInner::Assets( format!("fallback-{:?}.avif", node.public.kind.unwrap_or_default()).into(), ) .ser() }); Ok(Redirect::temporary(rocket::uri!(r_asset(asset.0, width)))) } #[get("/n//person//asset?&")] pub async fn r_person_asset( session: Session, db: &State, id: &str, index: usize, group: PeopleGroup, width: Option, ) -> MyResult { T_NODE .get(&db, id)? .only_if_permitted(&session.user.permissions) .ok_or(anyhow!("node does not exist"))?; let ext = T_NODE_EXTENDED.get(db, id)?.unwrap_or_default(); let app = ext .people .get(&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(format!("fallback-Person.avif").into()).ser()); Ok(Redirect::temporary(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: &str, t: f64, width: Option, ) -> MyResult { let node = T_NODE .get(&db, id)? .only_if_permitted(&session.user.permissions) .ok_or(anyhow!("node does not exist"))?; let media = node.public.media.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 = node.private.source.ok_or(anyhow!("no source"))?; let thumb_track_source = &source[thumb_track_index]; 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(LocalTrack { path, .. }) => { // the track selected might be different from thumb_track jellytranscoder::thumbnail::create_thumbnail(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, &format!("{t}")], |out| { session.node_thumbnail(out, id, 2048, t) }) .await? } }; Ok(Redirect::temporary(rocket::uri!(r_asset( AssetInner::Cache(asset).ser().0, width )))) }