diff options
Diffstat (limited to 'server')
| -rw-r--r-- | server/src/routes/mod.rs | 3 | ||||
| -rw-r--r-- | server/src/routes/stream.rs | 47 |
2 files changed, 47 insertions, 3 deletions
diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index e93aac2..5b1c799 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -47,7 +47,7 @@ use self::{ }; use crate::{ State, - routes::{admin::r_admin_debug, search::r_search}, + routes::{admin::r_admin_debug, search::r_search, stream::r_thumbnail}, }; use rocket::{ Build, Config, Rocket, catchers, fairing::AdHoc, fs::FileServer, http::Header, routes, @@ -120,6 +120,7 @@ pub(super) fn build_rocket(state: Arc<State>) -> Rocket<Build> { r_image_fallback_person, r_image, r_index, + r_thumbnail, r_items, r_node, r_player, diff --git a/server/src/routes/stream.rs b/server/src/routes/stream.rs index 45b86a9..0baaca6 100644 --- a/server/src/routes/stream.rs +++ b/server/src/routes/stream.rs @@ -3,11 +3,15 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin <metamuffin.org> */ -use crate::{request_info::RequestInfo, routes::error::MyError}; +use crate::{ + request_info::RequestInfo, + routes::error::{MyError, MyResult}, +}; use anyhow::{Result, anyhow}; -use jellycommon::{jellyobject::Path, stream::StreamSpec, *}; +use jellycommon::{jellyobject::Path, routes::u_image, stream::StreamSpec, *}; use jellydb::{Filter, Query}; use jellystream::SMediaInfo; +use jellytranscoder::thumbnail::create_thumbnail; use log::{info, warn}; use rocket::{ Either, Request, Response, get, head, @@ -55,6 +59,7 @@ pub async fn r_stream( range: Option<RequestRange>, path: StringPath<'_>, ) -> Result<StreamResponse, MyError> { + ri.require_user()?; let spec = StreamSpec::from_path(&path.0).map_err(|x| anyhow!("media path invalid: {x}"))?; let mut node = None; @@ -127,6 +132,44 @@ pub async fn r_stream( }) } +#[get("/n/<slug>/thumbnail?<t>&<size>")] +pub async fn r_thumbnail( + ri: RequestInfo<'_>, + slug: &str, + t: f64, + size: usize, +) -> MyResult<Redirect> { + ri.require_user()?; + let mut node = None; + ri.state.database.transaction(&mut |txn| { + if let Some(row) = txn.query_single(Query { + filter: Filter::Match(Path(vec![NO_SLUG.0]), slug.into()), + ..Default::default() + })? { + node = txn.get(row)?; + } + Ok(()) + })?; + + let Some(node) = node else { + Err(anyhow!("node not found"))? + }; + + let t = (t / 10.).floor() * 10.; // quantize to 10s + + let media_path = node + .iter(NO_TRACK) + .filter(|t| t.get(TR_KIND) == Some(TRKIND_VIDEO)) + .filter_map(|t| t.get(TR_SOURCE)) + .filter_map(|ts| ts.get(TRSOURCE_LOCAL_PATH)) + .next() + .ok_or(anyhow!("node has no suitable video track"))?; + + let thumb_path = create_thumbnail(&ri.state.cache, media_path.as_ref(), t)?; + + Ok(Redirect::temporary(u_image(&thumb_path, size))) +} + pub struct RedirectResponse(String); #[rocket::async_trait] |