use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SubtitleType {
Srt,
Vtt,
Txt,
Json,
Raw,
}
impl SubtitleType {
pub fn extension(&self) -> &'static str {
match self {
SubtitleType::Srt => "srt",
SubtitleType::Vtt => "vtt",
SubtitleType::Txt => "txt",
SubtitleType::Json => "json",
SubtitleType::Raw => "xml",
}
}
pub fn mime_type(&self) -> &'static str {
match self {
SubtitleType::Srt => "application/x-subrip",
SubtitleType::Vtt => "text/vtt",
SubtitleType::Txt => "text/plain",
SubtitleType::Json => "application/json",
SubtitleType::Raw => "application/xml",
}
}
}
impl std::str::FromStr for SubtitleType {
type Err = crate::error::YdlError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"srt" => Ok(SubtitleType::Srt),
"vtt" => Ok(SubtitleType::Vtt),
"txt" => Ok(SubtitleType::Txt),
"json" => Ok(SubtitleType::Json),
"raw" | "xml" => Ok(SubtitleType::Raw),
_ => Err(crate::error::YdlError::UnsupportedFormat {
format: s.to_string(),
}),
}
}
}
impl std::fmt::Display for SubtitleType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SubtitleType::Srt => write!(f, "srt"),
SubtitleType::Vtt => write!(f, "vtt"),
SubtitleType::Txt => write!(f, "txt"),
SubtitleType::Json => write!(f, "json"),
SubtitleType::Raw => write!(f, "raw"),
}
}
}
#[derive(Debug, Clone)]
pub struct YdlOptions {
pub language: Option<String>,
pub allow_auto_generated: bool,
pub prefer_manual: bool,
pub max_retries: u32,
pub timeout_seconds: u64,
pub user_agent: Option<String>,
pub proxy: Option<String>,
pub clean_content: bool,
pub validate_timing: bool,
}
impl Default for YdlOptions {
fn default() -> Self {
Self {
language: None, allow_auto_generated: true, prefer_manual: true,
max_retries: 3,
timeout_seconds: 30,
user_agent: None, proxy: None,
clean_content: true,
validate_timing: true,
}
}
}
impl YdlOptions {
pub fn new() -> Self {
Self::default()
}
pub fn language(mut self, lang: &str) -> Self {
self.language = Some(lang.to_string());
self
}
pub fn allow_auto_generated(mut self, allow: bool) -> Self {
self.allow_auto_generated = allow;
self
}
pub fn prefer_manual(mut self, prefer: bool) -> Self {
self.prefer_manual = prefer;
self
}
pub fn max_retries(mut self, retries: u32) -> Self {
self.max_retries = retries;
self
}
pub fn timeout(mut self, seconds: u64) -> Self {
self.timeout_seconds = seconds;
self
}
pub fn user_agent(mut self, ua: &str) -> Self {
self.user_agent = Some(ua.to_string());
self
}
pub fn proxy(mut self, proxy_url: &str) -> Self {
self.proxy = Some(proxy_url.to_string());
self
}
pub fn clean_content(mut self, clean: bool) -> Self {
self.clean_content = clean;
self
}
pub fn validate_timing(mut self, validate: bool) -> Self {
self.validate_timing = validate;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SubtitleTrackType {
Manual,
AutoGenerated,
Community,
}
impl std::fmt::Display for SubtitleTrackType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SubtitleTrackType::Manual => write!(f, "manual"),
SubtitleTrackType::AutoGenerated => write!(f, "auto-generated"),
SubtitleTrackType::Community => write!(f, "community"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubtitleTrack {
pub language_code: String,
pub language_name: String,
pub track_type: SubtitleTrackType,
pub is_translatable: bool,
pub url: Option<String>,
}
impl SubtitleTrack {
pub fn new(
language_code: String,
language_name: String,
track_type: SubtitleTrackType,
) -> Self {
Self {
language_code,
language_name,
track_type,
is_translatable: false,
url: None,
}
}
pub fn with_url(mut self, url: String) -> Self {
self.url = Some(url);
self
}
pub fn with_translatable(mut self, translatable: bool) -> Self {
self.is_translatable = translatable;
self
}
}
#[derive(Debug, Clone)]
pub struct SubtitleResult {
pub content: String,
pub format: SubtitleType,
pub language: String,
pub track_type: SubtitleTrackType,
}
impl SubtitleResult {
pub fn new(
content: String,
format: SubtitleType,
language: String,
track_type: SubtitleTrackType,
) -> Self {
Self {
content,
format,
language,
track_type,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct VideoMetadata {
pub video_id: String,
pub title: String,
pub duration: Option<Duration>,
pub available_subtitles: Vec<SubtitleTrack>,
}
impl VideoMetadata {
pub fn new(video_id: String, title: String) -> Self {
Self {
video_id,
title,
duration: None,
available_subtitles: Vec::new(),
}
}
pub fn with_duration(mut self, duration: Duration) -> Self {
self.duration = Some(duration);
self
}
pub fn with_subtitles(mut self, subtitles: Vec<SubtitleTrack>) -> Self {
self.available_subtitles = subtitles;
self
}
}
#[derive(Debug, Deserialize)]
pub struct PlayerResponse {
pub captions: Option<CaptionTracks>,
#[serde(rename = "videoDetails")]
pub video_details: Option<VideoDetails>,
}
#[derive(Debug, Deserialize)]
pub struct CaptionTracks {
#[serde(rename = "playerCaptionsTracklistRenderer")]
pub player_captions_tracklist_renderer: Option<TrackListRenderer>,
}
#[derive(Debug, Deserialize)]
pub struct TrackListRenderer {
#[serde(rename = "captionTracks")]
pub caption_tracks: Option<Vec<CaptionTrack>>,
#[serde(rename = "audioTracks")]
pub audio_tracks: Option<Vec<AudioTrack>>,
}
#[derive(Debug, Deserialize)]
pub struct CaptionTrack {
#[serde(rename = "baseUrl")]
pub base_url: String,
#[serde(rename = "languageCode")]
pub language_code: String,
pub name: Option<CaptionTrackName>,
#[serde(rename = "vssId")]
pub vss_id: String,
#[serde(rename = "isTranslatable")]
pub is_translatable: Option<bool>,
pub kind: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct CaptionTrackName {
#[serde(rename = "simpleText")]
pub simple_text: Option<String>,
pub runs: Option<Vec<Run>>,
}
#[derive(Debug, Deserialize)]
pub struct Run {
pub text: String,
}
#[derive(Debug, Deserialize)]
pub struct AudioTrack {
#[serde(rename = "captionTrackIndices")]
pub caption_track_indices: Option<Vec<i32>>,
}
#[derive(Debug, Deserialize)]
pub struct VideoDetails {
#[serde(rename = "videoId")]
pub video_id: String,
pub title: String,
#[serde(rename = "lengthSeconds")]
pub length_seconds: Option<String>,
#[serde(rename = "isLiveContent")]
pub is_live_content: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubtitleEntry {
pub start: Duration,
pub end: Duration,
pub text: String,
}
impl SubtitleEntry {
pub fn new(start: Duration, end: Duration, text: String) -> Self {
Self { start, end, text }
}
pub fn duration(&self) -> Duration {
self.end.saturating_sub(self.start)
}
pub fn start_as_srt(&self) -> String {
format_duration_as_srt(self.start)
}
pub fn end_as_srt(&self) -> String {
format_duration_as_srt(self.end)
}
pub fn start_as_vtt(&self) -> String {
format_duration_as_vtt(self.start)
}
pub fn end_as_vtt(&self) -> String {
format_duration_as_vtt(self.end)
}
}
#[derive(Debug, Clone)]
pub struct ParsedSubtitles {
pub entries: Vec<SubtitleEntry>,
pub language: String,
pub original_format: SubtitleType,
}
impl ParsedSubtitles {
pub fn new(entries: Vec<SubtitleEntry>, language: String) -> Self {
Self {
entries,
language,
original_format: SubtitleType::Raw,
}
}
pub fn with_format(mut self, format: SubtitleType) -> Self {
self.original_format = format;
self
}
pub fn total_duration(&self) -> Duration {
self.entries
.last()
.map(|e| e.end)
.unwrap_or_else(|| Duration::from_secs(0))
}
pub fn entry_count(&self) -> usize {
self.entries.len()
}
}
fn format_duration_as_srt(duration: Duration) -> String {
let total_secs = duration.as_secs();
let hours = total_secs / 3600;
let minutes = (total_secs % 3600) / 60;
let seconds = total_secs % 60;
let millis = duration.subsec_millis();
format!("{:02}:{:02}:{:02},{:03}", hours, minutes, seconds, millis)
}
fn format_duration_as_vtt(duration: Duration) -> String {
let total_secs = duration.as_secs();
let hours = total_secs / 3600;
let minutes = (total_secs % 3600) / 60;
let seconds = total_secs % 60;
let millis = duration.subsec_millis();
format!("{:02}:{:02}:{:02}.{:03}", hours, minutes, seconds, millis)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_subtitle_type_from_str() {
assert_eq!("srt".parse::<SubtitleType>().unwrap(), SubtitleType::Srt);
assert_eq!("vtt".parse::<SubtitleType>().unwrap(), SubtitleType::Vtt);
assert_eq!("txt".parse::<SubtitleType>().unwrap(), SubtitleType::Txt);
assert_eq!("json".parse::<SubtitleType>().unwrap(), SubtitleType::Json);
assert_eq!("raw".parse::<SubtitleType>().unwrap(), SubtitleType::Raw);
assert_eq!("xml".parse::<SubtitleType>().unwrap(), SubtitleType::Raw);
assert!("invalid".parse::<SubtitleType>().is_err());
}
#[test]
fn test_subtitle_type_extensions() {
assert_eq!(SubtitleType::Srt.extension(), "srt");
assert_eq!(SubtitleType::Vtt.extension(), "vtt");
assert_eq!(SubtitleType::Txt.extension(), "txt");
assert_eq!(SubtitleType::Json.extension(), "json");
assert_eq!(SubtitleType::Raw.extension(), "xml");
}
#[test]
fn test_ydl_options_builder() {
let options = YdlOptions::new()
.language("en")
.timeout(60)
.allow_auto_generated(false)
.user_agent("custom-agent");
assert_eq!(options.language, Some("en".to_string()));
assert_eq!(options.timeout_seconds, 60);
assert!(!options.allow_auto_generated);
assert_eq!(options.user_agent, Some("custom-agent".to_string()));
}
#[test]
fn test_subtitle_entry_timing() {
let entry = SubtitleEntry::new(
Duration::from_secs(1),
Duration::from_millis(3500),
"Test subtitle".to_string(),
);
assert_eq!(entry.duration(), Duration::from_millis(2500));
assert_eq!(entry.start_as_srt(), "00:00:01,000");
assert_eq!(entry.end_as_srt(), "00:00:03,500");
assert_eq!(entry.start_as_vtt(), "00:00:01.000");
assert_eq!(entry.end_as_vtt(), "00:00:03.500");
}
#[test]
fn test_duration_formatting() {
let duration = Duration::from_secs(3661) + Duration::from_millis(250);
assert_eq!(format_duration_as_srt(duration), "01:01:01,250");
assert_eq!(format_duration_as_vtt(duration), "01:01:01.250");
}
#[test]
fn test_parsed_subtitles() {
let entries = vec![
SubtitleEntry::new(
Duration::from_secs(0),
Duration::from_secs(2),
"First".to_string(),
),
SubtitleEntry::new(
Duration::from_secs(2),
Duration::from_secs(5),
"Second".to_string(),
),
];
let subtitles = ParsedSubtitles::new(entries, "en".to_string());
assert_eq!(subtitles.entry_count(), 2);
assert_eq!(subtitles.total_duration(), Duration::from_secs(5));
assert_eq!(subtitles.language, "en");
}
}