diff options
Diffstat (limited to 'server/src/ui/player.rs')
-rw-r--r-- | server/src/ui/player.rs | 198 |
1 files changed, 198 insertions, 0 deletions
diff --git a/server/src/ui/player.rs b/server/src/ui/player.rs new file mode 100644 index 0000000..cd4d03c --- /dev/null +++ b/server/src/ui/player.rs @@ -0,0 +1,198 @@ +/* + 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 super::{ + layout::LayoutPage, + node::{get_similar_media, DatabaseNodeUserDataExt, NodePage}, + sort::NodeFilterSort, +}; +use crate::{ + database::Database, + locale::AcceptLanguage, + logic::session::{self, Session}, + ui::{error::MyResult, layout::DynLayoutPage}, +}; +use anyhow::anyhow; +use jellybase::CONF; +use jellycommon::{ + stream::{StreamContainer, StreamSpec}, + user::{PermissionSet, PlayerKind}, + Node, NodeID, SourceTrackKind, TrackID, Visibility, +}; +use markup::DynRender; +use rocket::{get, response::Redirect, Either, FromForm, State, UriDisplayQuery}; +use std::sync::Arc; + +#[derive(FromForm, Default, Clone, Debug, UriDisplayQuery)] +pub struct PlayerConfig { + pub a: Option<TrackID>, + pub v: Option<TrackID>, + pub s: Option<TrackID>, + pub t: Option<f64>, + pub kind: Option<PlayerKind>, +} + +impl PlayerConfig { + pub fn seek(t: f64) -> Self { + Self { + t: Some(t), + ..Default::default() + } + } +} + +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 = format!( + "/n/{node}/stream{}", + StreamSpec::HlsMultiVariant { + segment: 0, + container: StreamContainer::Matroska + } + .to_query() + ); + format!("jellynative://{action}/{secret}/{session}/{seek}/{protocol}://{host}{stream_url}",) +} + +#[get("/n/<id>/player?<conf..>", rank = 4)] +pub fn r_player( + session: Session, + lang: AcceptLanguage, + db: &State<Database>, + id: NodeID, + conf: PlayerConfig, +) -> MyResult<Either<DynLayoutPage<'_>, Redirect>> { + let AcceptLanguage(lang) = lang; + let (node, udata) = db.get_node_with_userdata(id, &session)?; + + let mut parents = node + .parents + .iter() + .map(|pid| db.get_node_with_userdata(*pid, &session)) + .collect::<anyhow::Result<Vec<_>>>()?; + + let mut similar = get_similar_media(&node, db, &session)?; + + similar.retain(|(n, _)| n.visibility >= Visibility::Reduced); + parents.retain(|(n, _)| n.visibility >= Visibility::Reduced); + + let native_session = |action: &str| { + Ok(Either::Right(Redirect::temporary(jellynative_url( + action, + conf.t.unwrap_or(0.), + &session.user.native_secret, + &id.to_string(), + &session::create( + session.user.name, + PermissionSet::default(), // TODO + chrono::Duration::hours(24), + ), + )))) + }; + + match conf.kind.unwrap_or(session.user.player_preference) { + PlayerKind::Browser => (), + PlayerKind::Native => { + return native_session("player-v2"); + } + PlayerKind::NativeFullscreen => { + return native_session("player-fullscreen-v2"); + } + } + + // 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 = 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! { + // @if playing { + // // video[src=uri!(r_stream(&node.slug, &spec)), controls, preload="auto"]{} + // } + // @conf + @NodePage { + children: &[], + parents: &parents, + filter: &NodeFilterSort::default(), + node: &node, + udata: &udata, + player: true, + similar: &similar, + lang: &lang + } + }, + })) +} + +pub fn player_conf<'a>(item: Arc<Node>, playing: bool) -> anyhow::Result<DynRender<'a>> { + let mut audio_tracks = vec![]; + let mut video_tracks = vec![]; + let mut sub_tracks = vec![]; + let tracks = item + .media + .clone() + .ok_or(anyhow!("node does not have media"))? + .tracks + .clone(); + for (tid, track) in tracks.into_iter().enumerate() { + match &track.kind { + SourceTrackKind::Audio { .. } => audio_tracks.push((tid, track)), + SourceTrackKind::Video { .. } => video_tracks.push((tid, track)), + SourceTrackKind::Subtitles => sub_tracks.push((tid, track)), + } + } + + Ok(markup::new! { + form.playerconf[method = "GET", action = ""] { + h2 { "Select tracks for " @item.title } + + fieldset.video { + legend { "Video" } + @for (i, (tid, track)) in video_tracks.iter().enumerate() { + input[type="radio", id=tid, name="v", value=tid, checked=i==0]; + label[for=tid] { @format!("{track}") } br; + } + input[type="radio", id="v-none", name="v", value=""]; + label[for="v-none"] { "No video" } + } + + fieldset.audio { + legend { "Audio" } + @for (i, (tid, track)) in audio_tracks.iter().enumerate() { + input[type="radio", id=tid, name="a", value=tid, checked=i==0]; + label[for=tid] { @format!("{track}") } br; + } + input[type="radio", id="a-none", name="a", value=""]; + label[for="a-none"] { "No audio" } + } + + fieldset.subtitles { + legend { "Subtitles" } + @for (_i, (tid, track)) in sub_tracks.iter().enumerate() { + input[type="radio", id=tid, name="s", value=tid]; + label[for=tid] { @format!("{track}") } br; + } + input[type="radio", id="s-none", name="s", value="", checked=true]; + label[for="s-none"] { "No subtitles" } + } + + input[type="submit", value=if playing { "Change tracks" } else { "Start playback" }]; + } + }) +} |