/* 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, node::{get_similar_media, DatabaseNodeUserDataExt, NodePage}, sort::NodeFilterSort, }; use crate::{ database::Database, routes::{ locale::AcceptLanguage, 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, 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 = 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//player?", rank = 4)] pub fn r_player( session: Session, lang: AcceptLanguage, db: &State, id: NodeID, conf: PlayerConfig, ) -> MyResult, 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::>>()?; 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(), &token::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::>(), // 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, 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" }]; } }) }