diff options
Diffstat (limited to 'server')
| -rw-r--r-- | server/src/api.rs | 1 | ||||
| -rw-r--r-- | server/src/compat/jellyfin/mod.rs | 16 | ||||
| -rw-r--r-- | server/src/config.rs | 2 | ||||
| -rw-r--r-- | server/src/helper/cache.rs | 15 | ||||
| -rw-r--r-- | server/src/helper/mod.rs | 1 | ||||
| -rw-r--r-- | server/src/helper/picture.rs | 32 | ||||
| -rw-r--r-- | server/src/routes.rs | 11 | ||||
| -rw-r--r-- | server/src/ui/admin/user.rs | 5 | ||||
| -rw-r--r-- | server/src/ui/assets.rs | 118 |
9 files changed, 102 insertions, 99 deletions
diff --git a/server/src/api.rs b/server/src/api.rs index 217cd9f..2cfdbbd 100644 --- a/server/src/api.rs +++ b/server/src/api.rs @@ -54,7 +54,6 @@ pub fn r_api_account_login(data: Json<CreateSessionParams>) -> MyResult<Value> { Ok(json!(token)) } - #[get("/nodes_modified?<since>")] pub fn r_nodes_modified_since(session: A<Session>, since: u64) -> MyResult<Json<Vec<NodeID>>> { let nodes = get_nodes_modified_since(&session.0, since)?; diff --git a/server/src/compat/jellyfin/mod.rs b/server/src/compat/jellyfin/mod.rs index 27df1aa..b18d304 100644 --- a/server/src/compat/jellyfin/mod.rs +++ b/server/src/compat/jellyfin/mod.rs @@ -9,10 +9,10 @@ use crate::{helper::A, ui::error::MyResult}; use anyhow::anyhow; use jellycommon::{ api::{NodeFilterSort, SortOrder, SortProperty}, - routes::{u_asset, u_node_slug_backdrop, u_node_slug_poster}, + routes::{u_asset, u_node_image}, stream::{StreamContainer, StreamSpec}, user::{NodeUserData, WatchedState}, - MediaInfo, Node, NodeID, NodeKind, SourceTrack, SourceTrackKind, Visibility, + MediaInfo, Node, NodeID, NodeKind, PictureSlot, SourceTrack, SourceTrackKind, Visibility, }; use jellylogic::{ login::login_logic, @@ -169,7 +169,11 @@ pub fn r_jellyfin_items_image_primary( tag: String, ) -> Redirect { if tag == "poster" { - Redirect::permanent(u_node_slug_poster(id, fillWidth.unwrap_or(1024))) + Redirect::permanent(u_node_image( + id, + PictureSlot::Cover, + fillWidth.unwrap_or(1024), + )) } else { Redirect::permanent(u_asset(&tag, fillWidth.unwrap_or(1024))) } @@ -182,7 +186,11 @@ pub fn r_jellyfin_items_images_backdrop( id: &str, maxWidth: Option<usize>, ) -> Redirect { - Redirect::permanent(u_node_slug_backdrop(id, maxWidth.unwrap_or(1024))) + Redirect::permanent(u_node_image( + id, + PictureSlot::Backdrop, + maxWidth.unwrap_or(1024), + )) } #[get("/Items/<id>")] diff --git a/server/src/config.rs b/server/src/config.rs index b663c78..bcb3de6 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -5,6 +5,7 @@ */ use anyhow::{anyhow, Context, Result}; +use jellycache::init_cache; use jellylogic::init_database; use serde::Deserialize; use std::env::{args, var}; @@ -40,6 +41,7 @@ pub async fn load_config() -> Result<()> { *crate::CONF_PRELOAD.lock().unwrap() = Some(config.server); *jellyui::CONF_PRELOAD.lock().unwrap() = Some(config.ui); + init_cache()?; init_database()?; Ok(()) diff --git a/server/src/helper/cache.rs b/server/src/helper/cache.rs index d4c0595..93743e7 100644 --- a/server/src/helper/cache.rs +++ b/server/src/helper/cache.rs @@ -12,6 +12,7 @@ use rocket::{ }; use std::{ hash::{DefaultHasher, Hash, Hasher}, + io::Cursor, os::unix::fs::MetadataExt, path::Path, }; @@ -54,3 +55,17 @@ impl<'r> Responder<'r, 'static> for CacheControlFile { } } } + +pub struct CacheControlImage(pub Vec<u8>); +impl<'r> Responder<'r, 'static> for CacheControlImage { + fn respond_to(self, _req: &'r Request<'_>) -> response::Result<'static> { + Response::build() + .status(Status::Ok) + .header(Header::new( + "cache-control", + "private, immutable, maxage=86400", + )) + .sized_body(self.0.len(), Cursor::new(self.0)) + .ok() + } +} diff --git a/server/src/helper/mod.rs b/server/src/helper/mod.rs index a4e0e1f..6d1c834 100644 --- a/server/src/helper/mod.rs +++ b/server/src/helper/mod.rs @@ -10,6 +10,7 @@ pub mod filter_sort; pub mod language; pub mod node_id; pub mod session; +pub mod picture; use crate::ui::error::{MyError, MyResult}; use accept::Accept; diff --git a/server/src/helper/picture.rs b/server/src/helper/picture.rs new file mode 100644 index 0000000..d5887e3 --- /dev/null +++ b/server/src/helper/picture.rs @@ -0,0 +1,32 @@ +/* + 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 crate::helper::A; +use jellycommon::Picture; +use rocket::{ + http::uri::fmt::{FromUriParam, Path, UriDisplay}, + request::FromParam, +}; +use std::fmt::Write; +use std::str::FromStr; + +impl<'a> FromParam<'a> for A<Picture> { + type Error = (); + fn from_param(param: &'a str) -> Result<Self, Self::Error> { + Picture::from_str(param).map_err(|_| ()).map(A) + } +} +impl UriDisplay<Path> for A<Picture> { + fn fmt(&self, f: &mut rocket::http::uri::fmt::Formatter<'_, Path>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} +impl FromUriParam<Path, Picture> for A<Picture> { + type Target = A<Picture>; + fn from_uri_param(param: Picture) -> Self::Target { + A(param) + } +} diff --git a/server/src/routes.rs b/server/src/routes.rs index 9a35105..b777788 100644 --- a/server/src/routes.rs +++ b/server/src/routes.rs @@ -16,7 +16,7 @@ use crate::ui::{ r_admin_update_search, user::{r_admin_remove_user, r_admin_user, r_admin_user_permission, r_admin_users}, }, - assets::{r_asset, r_item_backdrop, r_item_poster, r_node_thumbnail, r_person_asset}, + assets::{r_image, r_item_poster, r_node_thumbnail}, error::{r_api_catch, r_catch}, home::r_home, items::r_items, @@ -29,10 +29,7 @@ use crate::ui::{ }; use crate::CONF; use crate::{ - api::{ - r_api_account_login, r_api_root, r_nodes_modified_since, - r_translations, r_version, - }, + api::{r_api_account_login, r_api_root, r_nodes_modified_since, r_translations, r_version}, compat::{ jellyfin::{ r_jellyfin_artists, r_jellyfin_branding_configuration, r_jellyfin_branding_css, @@ -136,7 +133,7 @@ pub fn build_rocket() -> Rocket<Build> { r_admin_user, r_admin_users, r_items, - r_asset, + r_image, r_assets_font, r_assets_js_map, r_assets_js, @@ -144,7 +141,6 @@ pub fn build_rocket() -> Rocket<Build> { r_favicon, r_home, r_index, - r_item_backdrop, r_item_poster, r_node, r_node_thumbnail, @@ -152,7 +148,6 @@ pub fn build_rocket() -> Rocket<Build> { r_node_userdata_rating, r_node_userdata_watched, r_node_userdata, - r_person_asset, r_player, r_playersync, r_search, diff --git a/server/src/ui/admin/user.rs b/server/src/ui/admin/user.rs index dd68383..4df5e80 100644 --- a/server/src/ui/admin/user.rs +++ b/server/src/ui/admin/user.rs @@ -87,10 +87,7 @@ pub fn r_admin_user_permission( Ok(Flash::success( Redirect::to(u_admin_user(name)), - tr( - ri.lang, - "admin.users.permission_update_success", - ), + tr(ri.lang, "admin.users.permission_update_success"), )) } diff --git a/server/src/ui/assets.rs b/server/src/ui/assets.rs index 969f3ed..d7663c3 100644 --- a/server/src/ui/assets.rs +++ b/server/src/ui/assets.rs @@ -4,108 +4,62 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use super::error::MyResult; -use crate::{ - helper::{cache::CacheControlFile, A}, - CONF, -}; -use anyhow::{anyhow, bail, Context}; -use jellycommon::NodeID; -use jellylogic::session::Session; -use log::info; +use crate::helper::{cache::CacheControlImage, A}; +use anyhow::{anyhow, Context}; +use jellycache::{CacheContentType, CacheKey}; +use jellycommon::{api::NodeFilterSort, NodeID, Picture, PictureSlot}; +use jellylogic::{assets::get_node_thumbnail, node::get_node, session::Session}; use rocket::{get, http::ContentType, response::Redirect}; -use std::path::PathBuf; +use std::str::FromStr; pub const AVIF_QUALITY: f32 = 50.; pub const AVIF_SPEED: u8 = 5; -#[get("/asset/<token>?<width>")] -pub async fn r_asset( +#[get("/image/<key>?<size>")] +pub async fn r_image( _session: A<Session>, - token: &str, - width: Option<usize>, -) -> MyResult<(ContentType, CacheControlFile)> { - // let width = width.unwrap_or(2048); - // let asset = AssetInner::deser(token)?; + key: A<Picture>, + size: Option<usize>, +) -> MyResult<(ContentType, CacheControlImage)> { + let size = size.unwrap_or(2048); - // // if let AssetInner::Federated { host, asset } = asset { - // // let session = fed.get_session(&host).await?; + let key = CacheKey(key.0 .0); + if !matches!(key.content_type(), CacheContentType::Image) { + Err(anyhow!("request to non-image"))? + } - // // 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 path = { - // 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(size.clamp(128, 2048).ilog2()); + let encoded = jellytranscoder::image::transcode(key, AVIF_QUALITY, AVIF_SPEED, width) + .context("transcoding 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?, - // )) - todo!() + Ok((ContentType::AVIF, CacheControlImage(encoded))) } -// 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(c), -// _ => bail!("wrong asset type"), -// } -// } - -#[get("/n/<id>/poster?<width>")] +#[get("/n/<id>/image/<slot>?<size>")] pub async fn r_item_poster( session: A<Session>, id: A<NodeID>, - width: Option<usize>, -) -> MyResult<Redirect> { - // let asset = get_node_poster(&session.0, id.0)?; - // Ok(Redirect::permanent(rocket::uri!(r_asset(asset.0, width)))) - Err(anyhow!("a").into()) -} - -#[get("/n/<id>/backdrop?<width>")] -pub async fn r_item_backdrop( - session: A<Session>, - id: A<NodeID>, - width: Option<usize>, -) -> MyResult<Redirect> { - // let asset = get_node_backdrop(&session.0, id.0)?; - // Ok(Redirect::permanent(rocket::uri!(r_asset(asset.0, width)))) - Err(anyhow!("a").into()) -} - -#[get("/n/<id>/person/<index>/asset?<group>&<width>")] -pub async fn r_person_asset( - session: A<Session>, - id: A<NodeID>, - index: usize, - group: String, - width: Option<usize>, + slot: &str, + size: Option<usize>, ) -> MyResult<Redirect> { - // let group = PeopleGroup::from_str_opt(&group).ok_or(anyhow!("unknown people group"))?; - // let asset = get_node_person_asset(&session.0, id.0, group, index)?; - // Ok(Redirect::permanent(rocket::uri!(r_asset(asset.0, width)))) - Err(anyhow!("a").into()) + let slot = PictureSlot::from_str(slot).map_err(|_| anyhow!("slot invalid"))?; + let node = get_node(&session.0, id.0, false, false, NodeFilterSort::default())?; + let picture = node + .node + .pictures + .get(&slot) + .ok_or(anyhow!("no pic todo"))?; + Ok(Redirect::permanent(rocket::uri!(r_image(*picture, size)))) } -#[get("/n/<id>/thumbnail?<t>&<width>")] +#[get("/n/<id>/thumbnail?<t>&<size>")] pub async fn r_node_thumbnail( session: A<Session>, id: A<NodeID>, t: f64, - width: Option<usize>, + size: Option<usize>, ) -> MyResult<Redirect> { - // let asset = get_node_thumbnail(&session.0, id.0, t).await?; - // Ok(Redirect::temporary(rocket::uri!(r_asset(asset.0, width)))) - Err(anyhow!("a").into()) + let picture = get_node_thumbnail(&session.0, id.0, t).await?; + Ok(Redirect::permanent(rocket::uri!(r_image(picture, size)))) } |