feat: subtitle support, allow chosing between vidstack and the browser-native player

This commit is contained in:
Ashhhleyyy 2024-01-20 13:55:15 +00:00
parent 3dda57baad
commit 9f1e46dbee
Signed by: ash
GPG key ID: 83B789081A0878FB
4 changed files with 134 additions and 13 deletions

View file

@ -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<()> {

View file

@ -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<i32>,
pub episode: Option<i32>,
#[yaserde(rename = "fileinfo")]
pub file_info: FileInfo,
}
impl ItemData {
@ -119,8 +121,69 @@ pub struct Actor {
pub thumb: Option<String>,
}
#[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<Subtitle>,
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()
}
}

View file

@ -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<maud::Markup> {
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::<maud::Markup>,
))
}
pub fn tv_show(details: &ItemMeta, args: &Args, children: &[ItemMeta]) -> Result<maud::Markup> {
let fanart_url = details.fanart_url().map(|s| get_url(s, args)).transpose()?;
let poster_url = details

View file

@ -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::<PathBuf, ItemMeta>::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;