aboutsummaryrefslogtreecommitdiff
path: root/server/src/ui/player.rs
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/ui/player.rs')
-rw-r--r--server/src/ui/player.rs198
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" }];
+ }
+ })
+}