1pub mod error;
2pub mod extractor;
3pub mod parser;
4pub mod processor;
5pub mod types;
6pub mod youtube_client;
7
8pub use error::{YdlError, YdlResult};
9pub use types::{
10 ParsedSubtitles, SubtitleEntry, SubtitleResult, SubtitleTrack, SubtitleTrackType, SubtitleType,
11 VideoMetadata, YdlOptions,
12};
13
14use extractor::SubtitleExtractor;
15use parser::YouTubeParser;
16use processor::ContentProcessor;
17use std::sync::Arc;
18use tracing::{debug, error, info};
19
20pub struct Ydl {
22 url: String,
23 video_id: String,
24 options: YdlOptions,
25 extractor: Arc<SubtitleExtractor>,
26 processor: ContentProcessor,
27}
28
29impl Ydl {
30 pub fn new(url: &str, options: YdlOptions) -> YdlResult<Self> {
32 info!("Initializing Ydl for URL: {}", url);
33
34 let parser = YouTubeParser::new();
35 let video_id = parser.parse_url(url)?;
36
37 debug!("Extracted video ID: {}", video_id);
38
39 let extractor = Arc::new(SubtitleExtractor::new(options.clone())?);
40 let processor = ContentProcessor::new();
41
42 Ok(Self {
43 url: url.to_string(),
44 video_id,
45 options,
46 extractor,
47 processor,
48 })
49 }
50
51 pub async fn subtitle(&self, subtitle_type: SubtitleType) -> YdlResult<String> {
53 info!("Downloading subtitle in format: {:?}", subtitle_type);
54
55 let tracks = self.extractor.discover_tracks(&self.video_id).await?;
57
58 if tracks.is_empty() {
59 return Err(YdlError::NoSubtitlesAvailable {
60 video_id: self.video_id.clone(),
61 });
62 }
63
64 let selected_track = self.extractor.select_best_track(&tracks).ok_or_else(|| {
66 YdlError::NoSubtitlesAvailable {
67 video_id: self.video_id.clone(),
68 }
69 })?;
70
71 debug!(
72 "Selected track: {} ({})",
73 selected_track.language_name, selected_track.track_type
74 );
75
76 let raw_content = self
78 .extractor
79 .download_content(selected_track, &self.video_id)
80 .await?;
81
82 let processed_content = self.processor.process_content(
84 &raw_content,
85 subtitle_type,
86 &selected_track.language_code,
87 self.options.clean_content,
88 self.options.validate_timing,
89 )?;
90
91 Ok(processed_content)
92 }
93
94 pub async fn subtitle_async(&self, subtitle_type: SubtitleType) -> YdlResult<String> {
96 self.subtitle(subtitle_type).await
97 }
98
99 pub async fn available_subtitles(&self) -> YdlResult<Vec<SubtitleTrack>> {
101 info!("Discovering available subtitle tracks");
102 self.extractor.discover_tracks(&self.video_id).await
103 }
104
105 pub async fn subtitles(&self, types: &[SubtitleType]) -> YdlResult<Vec<SubtitleResult>> {
107 info!("Downloading multiple subtitle formats: {:?}", types);
108
109 let tracks = self.extractor.discover_tracks(&self.video_id).await?;
111
112 if tracks.is_empty() {
113 return Err(YdlError::NoSubtitlesAvailable {
114 video_id: self.video_id.clone(),
115 });
116 }
117
118 let selected_track = self.extractor.select_best_track(&tracks).ok_or_else(|| {
119 YdlError::NoSubtitlesAvailable {
120 video_id: self.video_id.clone(),
121 }
122 })?;
123
124 let raw_content = self
126 .extractor
127 .download_content(selected_track, &self.video_id)
128 .await?;
129
130 let mut results = Vec::new();
132
133 for &subtitle_type in types {
134 match self.processor.process_content(
135 &raw_content,
136 subtitle_type,
137 &selected_track.language_code,
138 self.options.clean_content,
139 self.options.validate_timing,
140 ) {
141 Ok(content) => {
142 results.push(SubtitleResult::new(
143 content,
144 subtitle_type,
145 selected_track.language_code.clone(),
146 selected_track.track_type.clone(),
147 ));
148 }
149 Err(e) => {
150 error!("Failed to process format {:?}: {}", subtitle_type, e);
151 return Err(e);
152 }
153 }
154 }
155
156 Ok(results)
157 }
158
159 pub async fn metadata(&self) -> YdlResult<VideoMetadata> {
161 info!("Getting video metadata");
162 self.extractor.get_video_metadata(&self.video_id).await
163 }
164
165 pub fn video_id(&self) -> &str {
167 &self.video_id
168 }
169
170 pub fn url(&self) -> &str {
172 &self.url
173 }
174
175 pub fn normalized_url(&self) -> String {
177 format!("https://www.youtube.com/watch?v={}", self.video_id)
178 }
179
180 pub async fn has_subtitles(&self) -> bool {
182 match self.extractor.discover_tracks(&self.video_id).await {
183 Ok(tracks) => !tracks.is_empty(),
184 Err(_) => false,
185 }
186 }
187
188 pub async fn subtitle_with_retry(&self, subtitle_type: SubtitleType) -> YdlResult<String> {
190 let mut retries = 0;
191 let max_retries = self.options.max_retries;
192
193 loop {
194 match self.subtitle(subtitle_type).await {
195 Ok(content) => return Ok(content),
196 Err(e) => {
197 if retries >= max_retries {
198 return Err(e);
199 }
200
201 if e.is_retryable() {
202 retries += 1;
203 let delay = e.retry_delay().unwrap_or(1);
204
205 debug!(
206 "Retrying in {}s (attempt {} of {})",
207 delay, retries, max_retries
208 );
209 tokio::time::sleep(std::time::Duration::from_secs(delay)).await;
210 } else {
211 return Err(e);
212 }
213 }
214 }
215 }
216 }
217
218 pub fn with_language(mut self, lang: &str) -> Self {
220 self.options.language = Some(lang.to_string());
221 self
222 }
223
224 pub fn with_auto_generated(mut self, allow: bool) -> Self {
226 self.options.allow_auto_generated = allow;
227 self
228 }
229}
230
231pub async fn download_subtitle(url: &str, format: SubtitleType) -> YdlResult<String> {
235 let downloader = Ydl::new(url, YdlOptions::default())?;
236 downloader.subtitle(format).await
237}
238
239pub async fn list_subtitles(url: &str) -> YdlResult<Vec<SubtitleTrack>> {
241 let downloader = Ydl::new(url, YdlOptions::default())?;
242 downloader.available_subtitles().await
243}
244
245pub async fn get_metadata(url: &str) -> YdlResult<VideoMetadata> {
247 let downloader = Ydl::new(url, YdlOptions::default())?;
248 downloader.metadata().await
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[tokio::test]
256 async fn test_ydl_creation() {
257 let options = YdlOptions::default();
258 let result = Ydl::new("https://www.youtube.com/watch?v=dQw4w9WgXcQ", options);
259 assert!(result.is_ok());
260
261 let ydl = result.unwrap();
262 assert_eq!(ydl.video_id(), "dQw4w9WgXcQ");
263 assert_eq!(ydl.url(), "https://www.youtube.com/watch?v=dQw4w9WgXcQ");
264 }
265
266 #[test]
267 fn test_ydl_invalid_url() {
268 let options = YdlOptions::default();
269 let result = Ydl::new("https://www.google.com/", options);
270 assert!(result.is_err());
271 }
272
273 #[test]
274 fn test_ydl_fluent_interface() {
275 let options = YdlOptions::default();
276 let ydl = Ydl::new("https://www.youtube.com/watch?v=dQw4w9WgXcQ", options)
277 .unwrap()
278 .with_language("en")
279 .with_auto_generated(false);
280
281 assert_eq!(ydl.options.language, Some("en".to_string()));
282 assert!(!ydl.options.allow_auto_generated);
283 }
284
285 #[test]
286 fn test_normalized_url() {
287 let options = YdlOptions::default();
288 let ydl = Ydl::new("https://youtu.be/dQw4w9WgXcQ", options).unwrap();
289 assert_eq!(
290 ydl.normalized_url(),
291 "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
292 );
293 }
294
295 }