/* 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) 2024 metamuffin */ use super::{account::session::Session, layout::LayoutPage}; use crate::{ database::DataAcid, routes::{ stream::rocket_uri_macro_r_stream, ui::{ assets::{rocket_uri_macro_r_item_assets, AssetRole}, error::MyResult, layout::DynLayoutPage, }, }, uri, }; use anyhow::anyhow; use jellybase::{ database::{TableExt, T_NODE}, CONF, }; use jellycommon::{ stream::{StreamFormat, StreamSpec}, user::PlayerKind, Node, SourceTrackKind, TrackID, }; use markup::DynRender; use rocket::{get, response::Redirect, Either, FromForm, State, UriDisplayQuery}; #[derive(FromForm, Default, Clone, Debug, UriDisplayQuery)] pub struct PlayerConfig { pub a: Option, pub v: Option, pub s: Option, pub t: Option, } impl PlayerConfig { pub fn seek(t: f64) -> Self { Self { t: Some(t), ..Default::default() } } } fn jellynative_url(action: &str, node: &str) -> String { format!( "jellynative://{action}/http://{}{}", CONF.hostname, uri!(r_stream( node, StreamSpec { format: StreamFormat::HlsMaster, ..Default::default() } )) ) } #[get("/n//player?", rank = 4)] pub fn r_player<'a>( sess: Session, db: &'a State, id: &'a str, conf: PlayerConfig, ) -> MyResult, Redirect>> { let item = T_NODE.get(db, id)?.ok_or(anyhow!("node does not exist"))?; match sess.user.player_preference { PlayerKind::Browser => (), PlayerKind::Native => { return Ok(Either::Right(Redirect::temporary(jellynative_url( "player", id, )))) } PlayerKind::NativeFullscreen => { return Ok(Either::Right(Redirect::temporary(jellynative_url( "player-fullscreen", id, )))) } } let spec = StreamSpec { track: None .into_iter() .chain(conf.v.into_iter()) .chain(conf.a.into_iter()) .chain(conf.s.into_iter()) .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.public.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_assets(&id, AssetRole::Backdrop, Some(2048))).to_string()]; } @conf }, ..Default::default() })) } pub fn player_conf<'a>(item: Node, playing: bool) -> anyhow::Result> { let mut audio_tracks = vec![]; let mut video_tracks = vec![]; let mut sub_tracks = vec![]; let tracks = item .public .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.public.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" }]; } }) }