feat: subtitle support, allow chosing between vidstack and the browser-native player
This commit is contained in:
parent
3dda57baad
commit
9f1e46dbee
4 changed files with 134 additions and 13 deletions
13
src/main.rs
13
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<()> {
|
||||
|
|
65
src/nfo.rs
65
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<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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue