1use serde::{Deserialize, Serialize};
2use std::time::Duration;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6pub enum SubtitleType {
7 Srt,
9 Vtt,
11 Txt,
13 Json,
15 Raw,
17}
18
19impl SubtitleType {
20 pub fn extension(&self) -> &'static str {
22 match self {
23 SubtitleType::Srt => "srt",
24 SubtitleType::Vtt => "vtt",
25 SubtitleType::Txt => "txt",
26 SubtitleType::Json => "json",
27 SubtitleType::Raw => "xml",
28 }
29 }
30
31 pub fn mime_type(&self) -> &'static str {
33 match self {
34 SubtitleType::Srt => "application/x-subrip",
35 SubtitleType::Vtt => "text/vtt",
36 SubtitleType::Txt => "text/plain",
37 SubtitleType::Json => "application/json",
38 SubtitleType::Raw => "application/xml",
39 }
40 }
41}
42
43impl std::str::FromStr for SubtitleType {
44 type Err = crate::error::YdlError;
45
46 fn from_str(s: &str) -> Result<Self, Self::Err> {
47 match s.to_lowercase().as_str() {
48 "srt" => Ok(SubtitleType::Srt),
49 "vtt" => Ok(SubtitleType::Vtt),
50 "txt" => Ok(SubtitleType::Txt),
51 "json" => Ok(SubtitleType::Json),
52 "raw" | "xml" => Ok(SubtitleType::Raw),
53 _ => Err(crate::error::YdlError::UnsupportedFormat {
54 format: s.to_string(),
55 }),
56 }
57 }
58}
59
60impl std::fmt::Display for SubtitleType {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 match self {
63 SubtitleType::Srt => write!(f, "srt"),
64 SubtitleType::Vtt => write!(f, "vtt"),
65 SubtitleType::Txt => write!(f, "txt"),
66 SubtitleType::Json => write!(f, "json"),
67 SubtitleType::Raw => write!(f, "raw"),
68 }
69 }
70}
71
72#[derive(Debug, Clone)]
74pub struct YdlOptions {
75 pub language: Option<String>,
77
78 pub allow_auto_generated: bool,
80
81 pub prefer_manual: bool,
83
84 pub max_retries: u32,
86
87 pub timeout_seconds: u64,
89
90 pub user_agent: Option<String>,
92
93 pub proxy: Option<String>,
95
96 pub clean_content: bool,
98
99 pub validate_timing: bool,
101}
102
103impl Default for YdlOptions {
104 fn default() -> Self {
105 Self {
106 language: None, allow_auto_generated: true, prefer_manual: true,
109 max_retries: 3,
110 timeout_seconds: 30,
111 user_agent: None, proxy: None,
113 clean_content: true,
114 validate_timing: true,
115 }
116 }
117}
118
119impl YdlOptions {
120 pub fn new() -> Self {
122 Self::default()
123 }
124
125 pub fn language(mut self, lang: &str) -> Self {
127 self.language = Some(lang.to_string());
128 self
129 }
130
131 pub fn allow_auto_generated(mut self, allow: bool) -> Self {
132 self.allow_auto_generated = allow;
133 self
134 }
135
136 pub fn prefer_manual(mut self, prefer: bool) -> Self {
137 self.prefer_manual = prefer;
138 self
139 }
140
141 pub fn max_retries(mut self, retries: u32) -> Self {
142 self.max_retries = retries;
143 self
144 }
145
146 pub fn timeout(mut self, seconds: u64) -> Self {
147 self.timeout_seconds = seconds;
148 self
149 }
150
151 pub fn user_agent(mut self, ua: &str) -> Self {
152 self.user_agent = Some(ua.to_string());
153 self
154 }
155
156 pub fn proxy(mut self, proxy_url: &str) -> Self {
157 self.proxy = Some(proxy_url.to_string());
158 self
159 }
160
161 pub fn clean_content(mut self, clean: bool) -> Self {
162 self.clean_content = clean;
163 self
164 }
165
166 pub fn validate_timing(mut self, validate: bool) -> Self {
167 self.validate_timing = validate;
168 self
169 }
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
174pub enum SubtitleTrackType {
175 Manual,
177 AutoGenerated,
179 Community,
181}
182
183impl std::fmt::Display for SubtitleTrackType {
184 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185 match self {
186 SubtitleTrackType::Manual => write!(f, "manual"),
187 SubtitleTrackType::AutoGenerated => write!(f, "auto-generated"),
188 SubtitleTrackType::Community => write!(f, "community"),
189 }
190 }
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct SubtitleTrack {
196 pub language_code: String,
197 pub language_name: String,
198 pub track_type: SubtitleTrackType,
199 pub is_translatable: bool,
200 pub url: Option<String>,
201}
202
203impl SubtitleTrack {
204 pub fn new(
205 language_code: String,
206 language_name: String,
207 track_type: SubtitleTrackType,
208 ) -> Self {
209 Self {
210 language_code,
211 language_name,
212 track_type,
213 is_translatable: false,
214 url: None,
215 }
216 }
217
218 pub fn with_url(mut self, url: String) -> Self {
219 self.url = Some(url);
220 self
221 }
222
223 pub fn with_translatable(mut self, translatable: bool) -> Self {
224 self.is_translatable = translatable;
225 self
226 }
227}
228
229#[derive(Debug, Clone)]
231pub struct SubtitleResult {
232 pub content: String,
233 pub format: SubtitleType,
234 pub language: String,
235 pub track_type: SubtitleTrackType,
236}
237
238impl SubtitleResult {
239 pub fn new(
240 content: String,
241 format: SubtitleType,
242 language: String,
243 track_type: SubtitleTrackType,
244 ) -> Self {
245 Self {
246 content,
247 format,
248 language,
249 track_type,
250 }
251 }
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize, Default)]
256pub struct VideoMetadata {
257 pub video_id: String,
258 pub title: String,
259 pub duration: Option<Duration>,
260 pub available_subtitles: Vec<SubtitleTrack>,
261}
262
263impl VideoMetadata {
264 pub fn new(video_id: String, title: String) -> Self {
265 Self {
266 video_id,
267 title,
268 duration: None,
269 available_subtitles: Vec::new(),
270 }
271 }
272
273 pub fn with_duration(mut self, duration: Duration) -> Self {
274 self.duration = Some(duration);
275 self
276 }
277
278 pub fn with_subtitles(mut self, subtitles: Vec<SubtitleTrack>) -> Self {
279 self.available_subtitles = subtitles;
280 self
281 }
282}
283
284#[derive(Debug, Deserialize)]
286pub struct PlayerResponse {
287 pub captions: Option<CaptionTracks>,
288 #[serde(rename = "videoDetails")]
289 pub video_details: Option<VideoDetails>,
290}
291
292#[derive(Debug, Deserialize)]
294pub struct CaptionTracks {
295 #[serde(rename = "playerCaptionsTracklistRenderer")]
296 pub player_captions_tracklist_renderer: Option<TrackListRenderer>,
297}
298
299#[derive(Debug, Deserialize)]
301pub struct TrackListRenderer {
302 #[serde(rename = "captionTracks")]
303 pub caption_tracks: Option<Vec<CaptionTrack>>,
304 #[serde(rename = "audioTracks")]
305 pub audio_tracks: Option<Vec<AudioTrack>>,
306}
307
308#[derive(Debug, Deserialize)]
310pub struct CaptionTrack {
311 #[serde(rename = "baseUrl")]
312 pub base_url: String,
313 #[serde(rename = "languageCode")]
314 pub language_code: String,
315 pub name: Option<CaptionTrackName>,
316 #[serde(rename = "vssId")]
317 pub vss_id: String,
318 #[serde(rename = "isTranslatable")]
319 pub is_translatable: Option<bool>,
320 pub kind: Option<String>,
321}
322
323#[derive(Debug, Deserialize)]
325pub struct CaptionTrackName {
326 #[serde(rename = "simpleText")]
327 pub simple_text: Option<String>,
328 pub runs: Option<Vec<Run>>,
329}
330
331#[derive(Debug, Deserialize)]
333pub struct Run {
334 pub text: String,
335}
336
337#[derive(Debug, Deserialize)]
339pub struct AudioTrack {
340 #[serde(rename = "captionTrackIndices")]
341 pub caption_track_indices: Option<Vec<i32>>,
342}
343
344#[derive(Debug, Deserialize)]
346pub struct VideoDetails {
347 #[serde(rename = "videoId")]
348 pub video_id: String,
349 pub title: String,
350 #[serde(rename = "lengthSeconds")]
351 pub length_seconds: Option<String>,
352 #[serde(rename = "isLiveContent")]
353 pub is_live_content: Option<bool>,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize)]
358pub struct SubtitleEntry {
359 pub start: Duration,
360 pub end: Duration,
361 pub text: String,
362}
363
364impl SubtitleEntry {
365 pub fn new(start: Duration, end: Duration, text: String) -> Self {
366 Self { start, end, text }
367 }
368
369 pub fn duration(&self) -> Duration {
371 self.end.saturating_sub(self.start)
372 }
373
374 pub fn start_as_srt(&self) -> String {
376 format_duration_as_srt(self.start)
377 }
378
379 pub fn end_as_srt(&self) -> String {
381 format_duration_as_srt(self.end)
382 }
383
384 pub fn start_as_vtt(&self) -> String {
386 format_duration_as_vtt(self.start)
387 }
388
389 pub fn end_as_vtt(&self) -> String {
391 format_duration_as_vtt(self.end)
392 }
393}
394
395#[derive(Debug, Clone)]
397pub struct ParsedSubtitles {
398 pub entries: Vec<SubtitleEntry>,
399 pub language: String,
400 pub original_format: SubtitleType,
401}
402
403impl ParsedSubtitles {
404 pub fn new(entries: Vec<SubtitleEntry>, language: String) -> Self {
405 Self {
406 entries,
407 language,
408 original_format: SubtitleType::Raw,
409 }
410 }
411
412 pub fn with_format(mut self, format: SubtitleType) -> Self {
413 self.original_format = format;
414 self
415 }
416
417 pub fn total_duration(&self) -> Duration {
419 self.entries
420 .last()
421 .map(|e| e.end)
422 .unwrap_or_else(|| Duration::from_secs(0))
423 }
424
425 pub fn entry_count(&self) -> usize {
427 self.entries.len()
428 }
429}
430
431fn format_duration_as_srt(duration: Duration) -> String {
433 let total_secs = duration.as_secs();
434 let hours = total_secs / 3600;
435 let minutes = (total_secs % 3600) / 60;
436 let seconds = total_secs % 60;
437 let millis = duration.subsec_millis();
438
439 format!("{:02}:{:02}:{:02},{:03}", hours, minutes, seconds, millis)
440}
441
442fn format_duration_as_vtt(duration: Duration) -> String {
444 let total_secs = duration.as_secs();
445 let hours = total_secs / 3600;
446 let minutes = (total_secs % 3600) / 60;
447 let seconds = total_secs % 60;
448 let millis = duration.subsec_millis();
449
450 format!("{:02}:{:02}:{:02}.{:03}", hours, minutes, seconds, millis)
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456
457 #[test]
458 fn test_subtitle_type_from_str() {
459 assert_eq!("srt".parse::<SubtitleType>().unwrap(), SubtitleType::Srt);
460 assert_eq!("vtt".parse::<SubtitleType>().unwrap(), SubtitleType::Vtt);
461 assert_eq!("txt".parse::<SubtitleType>().unwrap(), SubtitleType::Txt);
462 assert_eq!("json".parse::<SubtitleType>().unwrap(), SubtitleType::Json);
463 assert_eq!("raw".parse::<SubtitleType>().unwrap(), SubtitleType::Raw);
464 assert_eq!("xml".parse::<SubtitleType>().unwrap(), SubtitleType::Raw);
465
466 assert!("invalid".parse::<SubtitleType>().is_err());
467 }
468
469 #[test]
470 fn test_subtitle_type_extensions() {
471 assert_eq!(SubtitleType::Srt.extension(), "srt");
472 assert_eq!(SubtitleType::Vtt.extension(), "vtt");
473 assert_eq!(SubtitleType::Txt.extension(), "txt");
474 assert_eq!(SubtitleType::Json.extension(), "json");
475 assert_eq!(SubtitleType::Raw.extension(), "xml");
476 }
477
478 #[test]
479 fn test_ydl_options_builder() {
480 let options = YdlOptions::new()
481 .language("en")
482 .timeout(60)
483 .allow_auto_generated(false)
484 .user_agent("custom-agent");
485
486 assert_eq!(options.language, Some("en".to_string()));
487 assert_eq!(options.timeout_seconds, 60);
488 assert!(!options.allow_auto_generated);
489 assert_eq!(options.user_agent, Some("custom-agent".to_string()));
490 }
491
492 #[test]
493 fn test_subtitle_entry_timing() {
494 let entry = SubtitleEntry::new(
495 Duration::from_secs(1),
496 Duration::from_millis(3500),
497 "Test subtitle".to_string(),
498 );
499
500 assert_eq!(entry.duration(), Duration::from_millis(2500));
501 assert_eq!(entry.start_as_srt(), "00:00:01,000");
502 assert_eq!(entry.end_as_srt(), "00:00:03,500");
503 assert_eq!(entry.start_as_vtt(), "00:00:01.000");
504 assert_eq!(entry.end_as_vtt(), "00:00:03.500");
505 }
506
507 #[test]
508 fn test_duration_formatting() {
509 let duration = Duration::from_secs(3661) + Duration::from_millis(250);
510 assert_eq!(format_duration_as_srt(duration), "01:01:01,250");
511 assert_eq!(format_duration_as_vtt(duration), "01:01:01.250");
512 }
513
514 #[test]
515 fn test_parsed_subtitles() {
516 let entries = vec![
517 SubtitleEntry::new(
518 Duration::from_secs(0),
519 Duration::from_secs(2),
520 "First".to_string(),
521 ),
522 SubtitleEntry::new(
523 Duration::from_secs(2),
524 Duration::from_secs(5),
525 "Second".to_string(),
526 ),
527 ];
528
529 let subtitles = ParsedSubtitles::new(entries, "en".to_string());
530 assert_eq!(subtitles.entry_count(), 2);
531 assert_eq!(subtitles.total_duration(), Duration::from_secs(5));
532 assert_eq!(subtitles.language, "en");
533 }
534}