/* 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) 2023 metamuffin */ use crate::{ database::Database, routes::ui::{account::session::Session, error::MyResult, CacheControlFile}, }; use anyhow::{anyhow, Context}; use jellybase::{ cache::async_cache_file, federation::Federation, permission::NodePermissionExt, AssetLocationExt, }; pub use jellycommon::AssetRole; use jellycommon::{AssetLocation, LocalTrack, SourceTrackKind, TrackSource}; use log::info; use rocket::{get, http::ContentType, State}; use std::{path::PathBuf, str::FromStr}; use tokio::fs::File; #[get("/n//asset?&")] pub async fn r_item_assets( session: Session, db: &State, id: &str, role: AssetRole, width: Option, ) -> MyResult<(ContentType, CacheControlFile)> { let node = db .node .get(&id.to_string())? .only_if_permitted(&session.user.permissions) .ok_or(anyhow!("node does not exist"))?; let mut asset = match role { AssetRole::Backdrop => node.private.backdrop, AssetRole::Poster => node.private.poster, }; if let None = asset { if let Some(parent) = &node.public.path.last() { let parent = db.node.get(parent)?.ok_or(anyhow!("node does not exist"))?; asset = match role { AssetRole::Backdrop => parent.private.backdrop, AssetRole::Poster => parent.private.poster, }; } }; let asset = asset.unwrap_or(AssetLocation::Assets( PathBuf::from_str("fallback.avif").unwrap(), )); Ok(asset_with_res(asset, width).await?) } // TODO this can create "federation recursion" because track selection cannot be relied on. #[get("/n//thumbnail?&")] pub async fn r_node_thumbnail( session: Session, db: &State, fed: &State, id: &str, t: f64, width: Option, ) -> MyResult<(ContentType, CacheControlFile)> { let node = db .node .get(&id.to_string())? .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(_) => { 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(asset_with_res(asset, width).await?) } async fn asset_with_res( asset: AssetLocation, width: Option, ) -> MyResult<(ContentType, CacheControlFile)> { // 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()); let path = jellytranscoder::image::transcode(asset, 50., 5, width) .await .context("transcoding asset")?; info!("loading asset from {path:?}"); Ok(( ContentType::AVIF, CacheControlFile::new(File::open(path.path()).await.context("opening file")?).await, )) }