aboutsummaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/src/api.rs1
-rw-r--r--server/src/compat/jellyfin/mod.rs16
-rw-r--r--server/src/config.rs2
-rw-r--r--server/src/helper/cache.rs15
-rw-r--r--server/src/helper/mod.rs1
-rw-r--r--server/src/helper/picture.rs32
-rw-r--r--server/src/routes.rs11
-rw-r--r--server/src/ui/admin/user.rs5
-rw-r--r--server/src/ui/assets.rs118
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))))
}