/* 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 */ use super::{ account::session::{token, Session}, layout::LayoutPage, }; use crate::{ database::Database, routes::{ stream::rocket_uri_macro_r_stream, ui::{assets::rocket_uri_macro_r_item_backdrop, error::MyResult, layout::DynLayoutPage}, }, uri, }; use anyhow::anyhow; use jellybase::{permission::PermissionSetExt, CONF}; use jellycommon::{ stream::{StreamFormat, StreamSpec}, user::{PermissionSet, PlayerKind, UserPermission}, Node, SourceTrackKind, TrackID, }; 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, pub v: Option, pub s: Option, pub t: Option, pub kind: Option, } 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 = uri!(r_stream( node, StreamSpec { format: StreamFormat::HlsMaster, ..Default::default() } )); format!("jellynative://{action}/{secret}/{session}/{seek}/{protocol}://{host}{stream_url}",) } #[get("/n//player?", rank = 4)] pub fn r_player<'a>( sess: Session, db: &'a State, id: &'a str, conf: PlayerConfig, ) -> MyResult, Redirect>> { let item = db .get_node_slug(id)? .ok_or(anyhow!("node does not exist"))?; let native_session = |action: &str| { let perm = [ UserPermission::StreamFormat(StreamFormat::HlsMaster), UserPermission::StreamFormat(StreamFormat::HlsVariant), UserPermission::StreamFormat(StreamFormat::Fragment), ]; for perm in &perm { sess.user.permissions.assert(perm)?; } Ok(Either::Right(Redirect::temporary(jellynative_url( action, conf.t.unwrap_or(0.), &sess.user.native_secret, id, &token::create( sess.user.name, PermissionSet(perm.map(|e| (e, true)).into()), chrono::Duration::hours(24), ), )))) }; match conf.kind.unwrap_or(sess.user.player_preference) { PlayerKind::Browser => (), PlayerKind::Native => { return native_session("player-v2"); } PlayerKind::NativeFullscreen => { return native_session("player-fullscreen-v2"); } } let spec = StreamSpec { track: None .into_iter() .chain(conf.v) .chain(conf.a) .chain(conf.s) .collect::>(), format: StreamFormat::Matroska, webm: Some(true), ..Default::default() }; let playing = !spec.track.is_empty(); let conf = player_conf(item.clone(), playing)?; Ok(Either::Left(LayoutPage { title: item.title.to_owned().unwrap_or_default(), class: Some("player"), content: markup::new! { @if playing { video[src=uri!(r_stream(&id, &spec)), controls, preload="auto"]{} } else { img.backdrop[src=uri!(r_item_backdrop(&id, Some(2048))).to_string()]; } @conf }, })) } pub fn player_conf<'a>(item: Arc, playing: bool) -> anyhow::Result> { 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" }]; } }) }