diff options
author | metamuffin <metamuffin@disroot.org> | 2025-04-16 20:06:01 +0200 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2025-04-16 20:06:01 +0200 |
commit | d26849375c70c795fdf664f9dfea68c273b6d483 (patch) | |
tree | 53ad4f0eff3604e80b27ff0abf0438ea6c69d432 /server/src/routes | |
parent | 1cd966f7454f052fda6c6c9ae1597479f05e23d9 (diff) | |
parent | cdf95d7b80bd2b78895671da8f462145bb5db522 (diff) | |
download | jellything-d26849375c70c795fdf664f9dfea68c273b6d483.tar jellything-d26849375c70c795fdf664f9dfea68c273b6d483.tar.bz2 jellything-d26849375c70c795fdf664f9dfea68c273b6d483.tar.zst |
Merge branch 'rewrite-stream'
Diffstat (limited to 'server/src/routes')
-rw-r--r-- | server/src/routes/compat/jellyfin/mod.rs | 45 | ||||
-rw-r--r-- | server/src/routes/stream.rs | 136 | ||||
-rw-r--r-- | server/src/routes/ui/player.rs | 72 |
3 files changed, 129 insertions, 124 deletions
diff --git a/server/src/routes/compat/jellyfin/mod.rs b/server/src/routes/compat/jellyfin/mod.rs index ab36a8c..7393c5f 100644 --- a/server/src/routes/compat/jellyfin/mod.rs +++ b/server/src/routes/compat/jellyfin/mod.rs @@ -5,23 +5,19 @@ */ pub mod models; -use crate::routes::{ - stream::rocket_uri_macro_r_stream, - ui::{ - account::{login_logic, session::Session}, - assets::{ - rocket_uri_macro_r_asset, rocket_uri_macro_r_item_backdrop, - rocket_uri_macro_r_item_poster, - }, - error::MyResult, - node::{aspect_class, DatabaseNodeUserDataExt}, - sort::{filter_and_sort_nodes, FilterProperty, NodeFilterSort, SortOrder, SortProperty}, +use crate::routes::ui::{ + account::{login_logic, session::Session}, + assets::{ + rocket_uri_macro_r_asset, rocket_uri_macro_r_item_backdrop, rocket_uri_macro_r_item_poster, }, + error::MyResult, + node::{aspect_class, DatabaseNodeUserDataExt}, + sort::{filter_and_sort_nodes, FilterProperty, NodeFilterSort, SortOrder, SortProperty}, }; use anyhow::{anyhow, Context}; use jellybase::{database::Database, CONF}; use jellycommon::{ - stream::{StreamFormat, StreamSpec}, + stream::{StreamContainer, StreamSpec}, user::{NodeUserData, WatchedState}, MediaInfo, Node, NodeID, NodeKind, SourceTrack, SourceTrackKind, Visibility, }; @@ -446,16 +442,12 @@ pub fn r_jellyfin_video_stream( .get_node_slug(id)? .ok_or(anyhow!("node does not exist"))?; let media = node.media.as_ref().ok_or(anyhow!("node has no media"))?; - Ok(Redirect::temporary(rocket::uri!(r_stream( - id, - StreamSpec { - format: StreamFormat::Matroska, - webm: Some(true), - track: (0..media.tracks.len()).collect(), - index: None, - profile: None, - } - )))) + let params = StreamSpec::Remux { + tracks: (0..media.tracks.len()).collect(), + container: StreamContainer::WebM, + } + .to_query(); + Ok(Redirect::temporary(format!("/n/{id}/stream{params}"))) } #[derive(Deserialize)] @@ -498,9 +490,7 @@ pub fn r_jellyfin_playback_bitratetest(_session: Session, Size: usize) -> Vec<u8 } #[post("/Sessions/Capabilities/Full")] -pub fn r_jellyfin_sessions_capabilities_full(_session: Session) { - -} +pub fn r_jellyfin_sessions_capabilities_full(_session: Session) {} #[derive(Deserialize)] #[serde(rename_all = "PascalCase")] @@ -796,7 +786,10 @@ fn item_object(node: &Node, userdata: &NodeUserData) -> JellyfinItem { location_type: node.media.as_ref().map(|_| "FileSystem".to_owned()), play_access: node.media.as_ref().map(|_| "Full".to_owned()), container: node.media.as_ref().map(|_| "webm".to_owned()), - run_time_ticks: node.media.as_ref().map(|m| (m.duration * 10_000_000.) as i64), + run_time_ticks: node + .media + .as_ref() + .map(|m| (m.duration * 10_000_000.) as i64), media_sources: media_source.as_ref().map(|s| vec![s.clone()]), media_streams: media_source.as_ref().map(|s| s.media_streams.clone()), path: node diff --git a/server/src/routes/stream.rs b/server/src/routes/stream.rs index 1fb136c..0fbeb3a 100644 --- a/server/src/routes/stream.rs +++ b/server/src/routes/stream.rs @@ -6,13 +6,9 @@ use super::ui::{account::session::Session, error::MyError}; use crate::database::Database; use anyhow::{anyhow, Result}; -use jellybase::{federation::Federation, permission::PermissionSetExt, SECRETS}; -use jellycommon::{ - config::FederationAccount, - stream::StreamSpec, - user::{CreateSessionParams, UserPermission}, - TrackSource, -}; +use jellybase::{assetfed::AssetInner, federation::Federation}; +use jellycommon::{stream::StreamSpec, TrackSource}; +use jellystream::SMediaInfo; use log::{info, warn}; use rocket::{ get, head, @@ -21,15 +17,20 @@ use rocket::{ response::{self, Redirect, Responder}, Either, Request, Response, State, }; -use std::{collections::HashSet, ops::Range}; +use std::{ + collections::{BTreeMap, BTreeSet}, + ops::Range, + sync::Arc, +}; use tokio::io::{duplex, DuplexStream}; -#[head("/n/<_id>/stream?<spec>")] +#[head("/n/<_id>/stream?<spec..>")] pub async fn r_stream_head( _sess: Session, _id: &str, - spec: StreamSpec, + spec: BTreeMap<String, String>, ) -> Result<Either<StreamResponse, Redirect>, MyError> { + let spec = StreamSpec::from_query_kv(&spec).map_err(|x| anyhow!("spec invalid: {x}"))?; let head = jellystream::stream_head(&spec); Ok(Either::Left(StreamResponse { stream: duplex(0).0, @@ -41,70 +42,72 @@ pub async fn r_stream_head( #[get("/n/<id>/stream?<spec..>")] pub async fn r_stream( - session: Session, - federation: &State<Federation>, + _session: Session, + _federation: &State<Federation>, db: &State<Database>, id: &str, range: Option<RequestRange>, - spec: StreamSpec, + spec: BTreeMap<String, String>, ) -> Result<Either<StreamResponse, RedirectResponse>, MyError> { + let spec = StreamSpec::from_query_kv(&spec).map_err(|x| anyhow!("spec invalid: {x}"))?; // TODO perm let node = db .get_node_slug(id)? .ok_or(anyhow!("node does not exist"))?; - let media = node - .media - .as_ref() - .ok_or(anyhow!("item does not contain media"))?; + let media = Arc::new( + node.media + .clone() + .ok_or(anyhow!("item does not contain media"))?, + ); // TODO its unclear how requests with multiple tracks should be handled. - if spec.track.len() == 1 { - let ti = spec.track[0]; - if let TrackSource::Remote(remote_index) = media.tracks[ti].source { - session - .user - .permissions - .assert(&UserPermission::FederatedContent)?; + // if spec.track.len() == 1 { + // let ti = spec.track[0]; + // if let TrackSource::Remote(remote_index) = media.tracks[ti].source { + // session + // .user + // .permissions + // .assert(&UserPermission::FederatedContent)?; - let track = &node.media.as_ref().ok_or(anyhow!("no media"))?.tracks[ti]; - let host = track - .federated - .last() - .ok_or(anyhow!("federation inconsistent"))?; + // let track = &node.media.as_ref().ok_or(anyhow!("no media"))?.tracks[ti]; + // let host = track + // .federated + // .last() + // .ok_or(anyhow!("federation inconsistent"))?; - let FederationAccount { - password, username, .. - } = SECRETS - .federation - .get(host) - .ok_or(anyhow!("no credentials on the server-side"))?; + // let FederationAccount { + // password, username, .. + // } = SECRETS + // .federation + // .get(host) + // .ok_or(anyhow!("no credentials on the server-side"))?; - info!("creating session on {host}"); - let instance = federation.get_instance(host)?.to_owned(); - let session = instance - .login(CreateSessionParams { - username: username.to_owned(), - password: password.to_owned(), - expire: Some(60), - drop_permissions: Some(HashSet::from_iter([ - UserPermission::ManageSelf, - UserPermission::Admin, // in case somebody federated the admin :))) - ])), - }) - .await?; + // info!("creating session on {host}"); + // let instance = federation.get_instance(host)?.to_owned(); + // let session = instance + // .login(CreateSessionParams { + // username: username.to_owned(), + // password: password.to_owned(), + // expire: Some(60), + // drop_permissions: Some(HashSet::from_iter([ + // UserPermission::ManageSelf, + // UserPermission::Admin, // in case somebody federated the admin :))) + // ])), + // }) + // .await?; - let uri = session.stream_url( - node.slug.clone().into(), - &StreamSpec { - track: vec![remote_index], - ..spec - }, - ); - info!("federation redirect"); - return Ok(Either::Right(RedirectResponse(uri))); - } - } + // let uri = session.stream_url( + // node.slug.clone().into(), + // &StreamSpec { + // track: vec![remote_index], + // ..spec + // }, + // ); + // info!("federation redirect"); + // return Ok(Either::Right(RedirectResponse(uri))); + // } + // } info!( "stream request (range={})", @@ -124,7 +127,20 @@ pub async fn r_stream( let head = jellystream::stream_head(&spec); - match jellystream::stream(node, spec, urange, &session.user.permissions).await { + let mut sources = BTreeSet::new(); + for t in &media.tracks { + if let TrackSource::Local(x) = &t.source { + if let AssetInner::LocalTrack(m) = AssetInner::deser(&x.0)? { + sources.insert(m.path); + } + } + } + let media = Arc::new(SMediaInfo { + files: sources, + info: node, + }); + + match jellystream::stream(media, spec, urange).await { Ok(stream) => Ok(Either::Left(StreamResponse { stream, range, diff --git a/server/src/routes/ui/player.rs b/server/src/routes/ui/player.rs index c2188a8..d2a8236 100644 --- a/server/src/routes/ui/player.rs +++ b/server/src/routes/ui/player.rs @@ -6,22 +6,20 @@ use super::{ account::session::{token, Session}, layout::LayoutPage, - node::{get_similar_media, DatabaseNodeUserDataExt, NodePage}, - sort::NodeFilterSort, + node::{get_similar_media, DatabaseNodeUserDataExt}, }; use crate::{ database::Database, - routes::{ - stream::rocket_uri_macro_r_stream, - ui::{error::MyResult, layout::DynLayoutPage}, + routes::ui::{ + assets::rocket_uri_macro_r_item_backdrop, error::MyResult, layout::DynLayoutPage, }, uri, }; use anyhow::anyhow; -use jellybase::{permission::PermissionSetExt, CONF}; +use jellybase::CONF; use jellycommon::{ - stream::{StreamFormat, StreamSpec}, - user::{PermissionSet, PlayerKind, UserPermission}, + stream::{StreamContainer, StreamSpec}, + user::{PermissionSet, PlayerKind}, Node, NodeID, SourceTrackKind, TrackID, Visibility, }; use markup::DynRender; @@ -49,13 +47,14 @@ impl PlayerConfig { fn jellynative_url(action: &str, seek: f64, secret: &str, node: &str, session: &str) -> String { let protocol = if CONF.tls { "https" } else { "http" }; let host = &CONF.hostname; - let stream_url = uri!(r_stream( - node, - StreamSpec { - format: StreamFormat::HlsMaster, - ..Default::default() + let stream_url = format!( + "/n/{node}/stream{}", + StreamSpec::HlsMultiVariant { + segment: 0, + container: StreamContainer::Matroska } - )); + .to_query() + ); format!("jellynative://{action}/{secret}/{session}/{seek}/{protocol}://{host}{stream_url}",) } @@ -66,7 +65,7 @@ pub fn r_player( id: NodeID, conf: PlayerConfig, ) -> MyResult<Either<DynLayoutPage<'_>, Redirect>> { - let (node, udata) = db.get_node_with_userdata(id, &session)?; + let (node, _udata) = db.get_node_with_userdata(id, &session)?; let mut parents = node .parents @@ -80,14 +79,6 @@ pub fn r_player( parents.retain(|(n, _)| n.visibility >= Visibility::Reduced); let native_session = |action: &str| { - let perm = [ - UserPermission::StreamFormat(StreamFormat::HlsMaster), - UserPermission::StreamFormat(StreamFormat::HlsVariant), - UserPermission::StreamFormat(StreamFormat::Fragment), - ]; - for perm in &perm { - session.user.permissions.assert(perm)?; - } Ok(Either::Right(Redirect::temporary(jellynative_url( action, conf.t.unwrap_or(0.), @@ -95,7 +86,7 @@ pub fn r_player( &id.to_string(), &token::create( session.user.name, - PermissionSet(perm.map(|e| (e, true)).into()), + PermissionSet::default(), // TODO chrono::Duration::hours(24), ), )))) @@ -111,27 +102,32 @@ pub fn r_player( } } - let spec = StreamSpec { - track: None - .into_iter() - .chain(conf.v) - .chain(conf.a) - .chain(conf.s) - .collect::<Vec<_>>(), - format: StreamFormat::Matroska, - webm: Some(true), - ..Default::default() - }; + // TODO + // let spec = StreamSpec { + // track: None + // .into_iter() + // .chain(conf.v) + // .chain(conf.a) + // .chain(conf.s) + // .collect::<Vec<_>>(), + // format: StreamFormat::Matroska, + // webm: Some(true), + // ..Default::default() + // }; - let playing = !spec.track.is_empty(); + let playing = false; // !spec.track.is_empty(); let conf = player_conf(node.clone(), playing)?; Ok(Either::Left(LayoutPage { title: node.title.to_owned().unwrap_or_default(), class: Some("player"), content: markup::new! { - video[id="player", src=uri!(r_stream(&node.slug, &spec)), controls, preload="auto"]{} - @NodePage { children: &[], parents: &parents, filter: &NodeFilterSort::default(), node: &node, udata: &udata, player: true, similar: &similar } + @if playing { + // TODO + // video[src=uri!(r_stream(&node.slug, &spec)), controls, preload="auto"]{} + } else { + img.backdrop[src=uri!(r_item_backdrop(&node.slug, Some(2048))).to_string()]; + } @conf }, })) |