diff --git a/src/main.rs b/src/main.rs index 86b3760..f2d2f3c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use clap::Parser; +use clap::{Parser, ValueEnum}; use color_eyre::Result; mod nfo; @@ -25,6 +25,17 @@ pub struct Args { /// Path to write the generated HTML files to. #[clap(short, long)] output: PathBuf, + + /// Whether to use JS-based player (Vidstack) or the browser's native video element. + #[clap(short, long)] + player: PlayerKind, +} + +#[derive(Clone, Default, ValueEnum)] +pub enum PlayerKind { + #[default] + Vidstack, + Native, } fn main() -> Result<()> { diff --git a/src/nfo.rs b/src/nfo.rs index 79f086a..0b4b531 100644 --- a/src/nfo.rs +++ b/src/nfo.rs @@ -1,6 +1,6 @@ use yaserde_derive::YaDeserialize; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct ItemMeta { pub url: String, pub ty: ContentType, @@ -86,6 +86,8 @@ pub struct ItemData { #[yaserde(rename = "seasonnumber")] pub season_number: Option, pub episode: Option, + #[yaserde(rename = "fileinfo")] + pub file_info: FileInfo, } impl ItemData { @@ -119,8 +121,69 @@ pub struct Actor { pub thumb: Option, } +#[derive(Clone, Debug, Default, YaDeserialize)] +#[yaserde(rename = "fileinfo")] +pub struct FileInfo { + #[yaserde(rename = "streamdetails")] + pub stream_details: StreamDetails, +} + +#[derive(Clone, Debug, Default, YaDeserialize)] +#[yaserde(rename = "streamdetails")] +pub struct StreamDetails { + pub subtitle: Option, + pub video: Video, +} + +#[derive(Clone, Debug, Default, YaDeserialize)] +#[yaserde(rename = "subtitle")] +pub struct Subtitle { + pub codec: String, + pub language: String, + pub forced: bool, +} + +impl Subtitle { + pub fn ext(&self) -> Option<&str> { + match &*self.codec { + "webvtt" => Some("vtt"), + _ => None, + } + } +} + +#[derive(Clone, Debug, Default, YaDeserialize)] +#[yaserde(rename = "video")] +pub struct Video { + pub width: u32, + pub height: u32, + #[yaserde(rename = "aspectratio")] + pub aspect_ratio: String, + #[yaserde(rename = "durationinseconds")] + pub duration_in_seconds: u32, +} + pub struct SubtitleTrack { pub url: String, pub label: String, pub lang: String, } + +impl SubtitleTrack { + pub fn from_nfo(subtitle: Subtitle, video_url: &str) -> Self { + Self { + url: video_url.replace(".mp4", &format!(".{}.{}", subtitle.language, subtitle.ext().unwrap())), + label: Self::label_from_lang(&subtitle.language).to_string(), + lang: subtitle.language, + } + } + + fn label_from_lang(lang: &str) -> String { + // TODO: Actually make this use proper country code lookup + match lang { + "eng" => "English", + "spa" => "Spanish", + lang => lang, + }.to_string() + } +} diff --git a/src/templates.rs b/src/templates.rs index 007b2c9..26fe08b 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -31,9 +31,10 @@ fn base_template( } } -pub fn player( +pub fn vidstack_player( title: &str, video_url: &str, + aspect: &str, poster: &str, subtitles: &[SubtitleTrack], args: &Args, @@ -45,13 +46,13 @@ pub fn player( title=(title) src=(video_url) poster=(get_url(poster, args)?) - aspect-ratio="16/9" + aspect-ratio=(aspect) style="height: 100vh" crossorigin { media-outlet { media-poster alt=(title) {} @for subtitle_track in subtitles { - track src=(get_url(&subtitle_track.url, args)?) + track src=(subtitle_track.url) label=(subtitle_track.label) srclang=(subtitle_track.lang) kind="subtitles" @@ -69,6 +70,32 @@ pub fn player( )) } +pub fn native_player( + title: &str, + video_url: &str, + aspect: &str, + poster: &str, + subtitles: &[SubtitleTrack], + args: &Args, +) -> Result { + Ok(base_template( + title, + maud::html! { + video controls style="height: 100vh" crossorigin="anonymous" { + source src=(video_url) type="video/mp4"; + @for subtitle_track in subtitles { + track src=(subtitle_track.url) + label=(subtitle_track.label) + srclang=(subtitle_track.lang) + kind="subtitles" + default; + } + } + }, + None::, + )) +} + pub fn tv_show(details: &ItemMeta, args: &Args, children: &[ItemMeta]) -> Result { let fanart_url = details.fanart_url().map(|s| get_url(s, args)).transpose()?; let poster_url = details diff --git a/src/walker.rs b/src/walker.rs index 0f3c4a4..62bed6a 100644 --- a/src/walker.rs +++ b/src/walker.rs @@ -2,7 +2,7 @@ use std::{path::{Path, PathBuf}, fs::File, collections::HashMap}; use color_eyre::{Result, eyre::eyre}; -use crate::{nfo::{ContentType, ItemMeta, ItemData}, Args, templates, util::get_url}; +use crate::{nfo::{ContentType, ItemMeta, ItemData, SubtitleTrack}, Args, templates, util::get_url, PlayerKind}; pub fn walk_metadata(args: &Args) -> Result<()> { let mut path_cache = HashMap::::new(); @@ -36,13 +36,33 @@ pub fn walk_metadata(args: &Args) -> Result<()> { let mut children = find_season_children(&path_cache, &entries, &info.data, args)?; for child in &mut children { - let rendered = templates::player( - &child.data.title, - &child.url, - child.data.art.poster.as_ref().unwrap(), - &[], - args, - )?; + let aspect = child.data.file_info.stream_details.video.aspect_ratio.replace(':', "/"); + let rendered = match args.player { + PlayerKind::Vidstack => templates::vidstack_player( + &child.data.title, + &child.url, + &aspect, + child.data.art.poster.as_ref().unwrap(), + &if let Some(subtitle) = &child.data.file_info.stream_details.subtitle { + vec![SubtitleTrack::from_nfo(subtitle.clone(), &child.url)] + } else { + vec![] + }, + args, + ), + PlayerKind::Native => templates::native_player( + &child.data.title, + &child.url, + &aspect, + child.data.art.poster.as_ref().unwrap(), + &if let Some(subtitle) = &child.data.file_info.stream_details.subtitle { + vec![SubtitleTrack::from_nfo(subtitle.clone(), &child.url)] + } else { + vec![] + }, + args, + ), + }?; let filename = format!("Episode {}.html", child.data.episode.unwrap()); std::fs::write(output_dir.join(&filename), rendered.0)?; child.url = filename;