yt_transcript_rs/proxies.rs
1use std::any::Any;
2use std::collections::HashMap;
3use std::fmt::Debug;
4
5/// # InvalidProxyConfig
6///
7/// Error type for invalid proxy configurations.
8///
9/// This error is returned when a proxy configuration is deemed invalid,
10/// such as when required fields are missing or values are not in the expected format.
11#[derive(Debug, thiserror::Error)]
12#[error("Invalid proxy configuration: {0}")]
13pub struct InvalidProxyConfig(pub String);
14
15/// # ProxyConfig
16///
17/// Trait for defining proxy configurations to route YouTube requests through proxies.
18///
19/// This trait provides methods for configuring how HTTP requests are routed through
20/// proxies, which is essential for bypassing geographical restrictions or IP blocks
21/// that YouTube might impose.
22///
23/// ## Implementing Types
24///
25/// The library provides two built-in implementations:
26/// - `GenericProxyConfig`: For standard HTTP/HTTPS proxies
27/// - `WebshareProxyConfig`: For Webshare's rotating residential proxies
28///
29/// ## Custom Implementations
30///
31/// You can implement this trait for your own proxy providers by:
32/// 1. Creating a struct with your proxy configuration details
33/// 2. Implementing the required methods to generate proxy URLs
34/// 3. Pass your custom implementation to `YouTubeTranscriptApi::new`
35///
36/// ## Example
37///
38/// ```rust,no_run
39/// # use std::any::Any;
40/// # use std::collections::HashMap;
41/// # use yt_transcript_rs::proxies::{ProxyConfig, InvalidProxyConfig};
42/// #[derive(Debug)]
43/// struct MyCustomProxy {
44/// server: String,
45/// port: u16,
46/// username: String,
47/// password: String,
48/// }
49///
50/// impl ProxyConfig for MyCustomProxy {
51/// fn to_requests_dict(&self) -> HashMap<String, String> {
52/// let url = format!(
53/// "http://{}:{}@{}:{}",
54/// self.username, self.password, self.server, self.port
55/// );
56///
57/// let mut map = HashMap::new();
58/// map.insert("http".to_string(), url.clone());
59/// map.insert("https".to_string(), url);
60/// map
61/// }
62///
63/// fn prevent_keeping_connections_alive(&self) -> bool {
64/// false // We want persistent connections
65/// }
66///
67/// fn retries_when_blocked(&self) -> i32 {
68/// 3 // Retry up to 3 times if blocked
69/// }
70///
71/// fn as_any(&self) -> &dyn Any {
72/// self
73/// }
74/// }
75/// ```
76pub trait ProxyConfig: Debug + Send + Sync {
77 /// Converts the proxy configuration to a reqwest-compatible proxy URL map.
78 ///
79 /// This method should return a HashMap with keys "http" and/or "https"
80 /// containing the formatted proxy URLs for each protocol.
81 ///
82 /// # Returns
83 ///
84 /// * `HashMap<String, String>` - Map of protocol names to proxy URLs
85 ///
86 /// # Expected Format
87 ///
88 /// The URL format typically follows: `protocol://[username:password@]host:port`
89 fn to_requests_dict(&self) -> HashMap<String, String>;
90
91 /// Controls whether HTTP connections should be closed after each request.
92 ///
93 /// Setting this to `true` ensures you get a new connection (and potentially
94 /// a new IP address) for each request, which is useful for rotating proxies
95 /// to prevent IP-based blocking.
96 ///
97 /// # Returns
98 ///
99 /// * `bool` - `true` to close connections after each request, `false` to reuse connections
100 ///
101 /// # Default Implementation
102 ///
103 /// The default implementation returns `false`, which keeps connections alive.
104 fn prevent_keeping_connections_alive(&self) -> bool {
105 false
106 }
107
108 /// Specifies how many retries to attempt when a request is blocked by YouTube.
109 ///
110 /// When using rotating residential proxies, this allows the library to
111 /// retry the request with different IPs when YouTube blocks a request.
112 ///
113 /// # Returns
114 ///
115 /// * `i32` - The number of retries to attempt when blocked (0 = no retries)
116 ///
117 /// # Default Implementation
118 ///
119 /// The default implementation returns `0`, meaning no retries.
120 fn retries_when_blocked(&self) -> i32 {
121 0
122 }
123
124 /// Type conversion for dynamic dispatch and type identification.
125 ///
126 /// This method is used internally to determine the concrete type of the proxy
127 /// configuration, which is needed for specific error handling and behavior.
128 ///
129 /// # Returns
130 ///
131 /// * `&dyn Any` - Reference to the concrete type as `Any`
132 fn as_any(&self) -> &dyn Any;
133}
134
135/// # GenericProxyConfig
136///
137/// A generic proxy configuration for standard HTTP/HTTPS proxies.
138///
139/// This configuration allows you to specify separate proxies for HTTP and HTTPS
140/// requests, or use the same proxy for both. It's suitable for most standard
141/// proxy services.
142///
143/// ## Features
144///
145/// - Support for separate HTTP and HTTPS proxies
146/// - Simple configuration with minimal required fields
147/// - Compatible with most proxy services
148///
149/// ## Example Usage
150///
151/// ```rust,no_run
152/// # use yt_transcript_rs::proxies::GenericProxyConfig;
153/// # use yt_transcript_rs::YouTubeTranscriptApi;
154/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
155/// // Create a proxy configuration with the same proxy for both HTTP and HTTPS
156/// let proxy = GenericProxyConfig::new(
157/// Some("http://username:password@proxy.example.com:8080".to_string()),
158/// None
159/// )?;
160///
161/// // Use it with the YouTube Transcript API
162/// let api = YouTubeTranscriptApi::new(
163/// None,
164/// Some(Box::new(proxy)),
165/// None
166/// )?;
167/// # Ok(())
168/// # }
169/// ```
170#[derive(Debug, Clone)]
171pub struct GenericProxyConfig {
172 /// URL for HTTP proxy (format: "http://[username:password@]host:port")
173 pub http_url: Option<String>,
174 /// URL for HTTPS proxy (format: "https://[username:password@]host:port")
175 pub https_url: Option<String>,
176}
177
178impl GenericProxyConfig {
179 /// Creates a new generic proxy configuration.
180 ///
181 /// You can specify different proxies for HTTP and HTTPS requests, or use
182 /// the same proxy for both by specifying only one. At least one of the
183 /// proxy URLs must be provided.
184 ///
185 /// # Parameters
186 ///
187 /// * `http_url` - Optional URL for HTTP proxy
188 /// * `https_url` - Optional URL for HTTPS proxy
189 ///
190 /// # Returns
191 ///
192 /// * `Result<Self, InvalidProxyConfig>` - A new proxy configuration or an error
193 ///
194 /// # Errors
195 ///
196 /// Returns `InvalidProxyConfig` if both `http_url` and `https_url` are `None`.
197 ///
198 /// # Example
199 ///
200 /// ```rust,no_run
201 /// # use yt_transcript_rs::proxies::GenericProxyConfig;
202 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
203 /// // Configure different proxies for HTTP and HTTPS
204 /// let proxy = GenericProxyConfig::new(
205 /// Some("http://user:pass@proxy1.example.com:8080".to_string()),
206 /// Some("http://user:pass@proxy2.example.com:8443".to_string())
207 /// )?;
208 /// # Ok(())
209 /// # }
210 /// ```
211 pub fn new(
212 http_url: Option<String>,
213 https_url: Option<String>,
214 ) -> Result<Self, InvalidProxyConfig> {
215 if http_url.is_none() && https_url.is_none() {
216 return Err(InvalidProxyConfig(
217 "GenericProxyConfig requires you to define at least one of the two: http or https"
218 .to_string(),
219 ));
220 }
221
222 Ok(Self {
223 http_url,
224 https_url,
225 })
226 }
227}
228
229impl ProxyConfig for GenericProxyConfig {
230 /// Converts the generic proxy configuration to a reqwest-compatible dictionary.
231 ///
232 /// If either HTTP or HTTPS URL is missing, the other is used as a fallback.
233 ///
234 /// # Returns
235 ///
236 /// * `HashMap<String, String>` - Map with "http" and "https" keys and their proxy URLs
237 fn to_requests_dict(&self) -> HashMap<String, String> {
238 let mut map = HashMap::new();
239
240 let http = match &self.http_url {
241 Some(url) => url.clone(),
242 None => self.https_url.clone().unwrap_or_default(),
243 };
244
245 let https = match &self.https_url {
246 Some(url) => url.clone(),
247 None => self.http_url.clone().unwrap_or_default(),
248 };
249
250 map.insert("http".to_string(), http);
251 map.insert("https".to_string(), https);
252
253 map
254 }
255
256 fn as_any(&self) -> &dyn Any {
257 self
258 }
259}
260
261/// # WebshareProxyConfig
262///
263/// Specialized proxy configuration for Webshare's rotating residential proxies.
264///
265/// Webshare provides residential proxies that rotate IPs automatically, which is
266/// extremely useful for accessing YouTube without being blocked. This configuration
267/// is optimized for Webshare's API format.
268///
269/// ## Features
270///
271/// - Automatic IP rotation for each request
272/// - Configurable retry mechanism for handling blocks
273/// - Optimized for Webshare's proxy service
274///
275/// ## Important Note
276///
277/// For reliable YouTube access, use Webshare's "Residential" proxies, not the
278/// "Proxy Server" or "Static Residential" options, as YouTube often blocks those IPs.
279///
280/// ## Example Usage
281///
282/// ```rust,no_run
283/// # use yt_transcript_rs::proxies::WebshareProxyConfig;
284/// # use yt_transcript_rs::YouTubeTranscriptApi;
285/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
286/// // Create a Webshare proxy configuration with credentials
287/// let proxy = WebshareProxyConfig::new(
288/// "your_username".to_string(),
289/// "your_password".to_string(),
290/// 5, // Retry up to 5 times if blocked
291/// None, // Use default domain
292/// None // Use default port
293/// );
294///
295/// // Use it with the YouTube Transcript API
296/// let api = YouTubeTranscriptApi::new(
297/// None,
298/// Some(Box::new(proxy)),
299/// None
300/// )?;
301/// # Ok(())
302/// # }
303/// ```
304#[derive(Debug, Clone)]
305pub struct WebshareProxyConfig {
306 /// Your Webshare proxy username
307 pub proxy_username: String,
308 /// Your Webshare proxy password
309 pub proxy_password: String,
310 /// The proxy domain name (default: "p.webshare.io")
311 pub domain_name: String,
312 /// The port number to use (default: 80)
313 pub proxy_port: u16,
314 /// Number of retries to attempt when blocked
315 pub retries: i32,
316}
317
318impl WebshareProxyConfig {
319 /// Default domain name for Webshare proxies
320 pub const DEFAULT_DOMAIN_NAME: &'static str = "p.webshare.io";
321 /// Default port for Webshare proxies
322 pub const DEFAULT_PORT: u16 = 80;
323
324 /// Creates a new Webshare proxy configuration.
325 ///
326 /// This configuration is specifically designed for Webshare's rotating proxy service.
327 /// It automatically adds the rotation feature to your proxy.
328 ///
329 /// # Parameters
330 ///
331 /// * `proxy_username` - Your Webshare proxy username
332 /// * `proxy_password` - Your Webshare proxy password
333 /// * `retries_when_blocked` - Number of retries to attempt if blocked (recommended: 3-5)
334 /// * `domain_name` - Optional custom domain name (default: "p.webshare.io")
335 /// * `proxy_port` - Optional custom port (default: 80)
336 ///
337 /// # Returns
338 ///
339 /// * `Self` - A new Webshare proxy configuration
340 ///
341 /// # Example
342 ///
343 /// ```rust,no_run
344 /// # use yt_transcript_rs::proxies::WebshareProxyConfig;
345 /// // Basic configuration
346 /// let proxy = WebshareProxyConfig::new(
347 /// "username".to_string(),
348 /// "password".to_string(),
349 /// 3, // Retry 3 times
350 /// None, // Use default domain
351 /// None // Use default port
352 /// );
353 ///
354 /// // Custom domain and port
355 /// let proxy_custom = WebshareProxyConfig::new(
356 /// "username".to_string(),
357 /// "password".to_string(),
358 /// 5,
359 /// Some("custom.webshare.io".to_string()),
360 /// Some(8080)
361 /// );
362 /// ```
363 pub fn new(
364 proxy_username: String,
365 proxy_password: String,
366 retries_when_blocked: i32,
367 domain_name: Option<String>,
368 proxy_port: Option<u16>,
369 ) -> Self {
370 Self {
371 proxy_username,
372 proxy_password,
373 domain_name: domain_name.unwrap_or_else(|| Self::DEFAULT_DOMAIN_NAME.to_string()),
374 proxy_port: proxy_port.unwrap_or(Self::DEFAULT_PORT),
375 retries: retries_when_blocked,
376 }
377 }
378
379 /// Generates the complete proxy URL for Webshare.
380 ///
381 /// This formats the proxy URL with rotation enabled by appending "-rotate"
382 /// to the username, which tells Webshare to provide a new IP for each request.
383 ///
384 /// # Returns
385 ///
386 /// * `String` - The formatted proxy URL
387 ///
388 /// # Example (internal)
389 ///
390 /// ```rust,no_run
391 /// # use yt_transcript_rs::proxies::WebshareProxyConfig;
392 /// # fn example() {
393 /// let proxy = WebshareProxyConfig::new(
394 /// "user123".to_string(),
395 /// "pass456".to_string(),
396 /// 3,
397 /// None,
398 /// None
399 /// );
400 ///
401 /// // Generates: "http://user123-rotate:pass456@p.webshare.io:80/"
402 /// let url = proxy.url();
403 /// # }
404 /// ```
405 pub fn url(&self) -> String {
406 format!(
407 "http://{}-rotate:{}@{}:{}/",
408 self.proxy_username, self.proxy_password, self.domain_name, self.proxy_port
409 )
410 }
411}
412
413impl ProxyConfig for WebshareProxyConfig {
414 /// Converts the Webshare proxy configuration to a reqwest-compatible dictionary.
415 ///
416 /// Uses the same URL for both HTTP and HTTPS requests.
417 ///
418 /// # Returns
419 ///
420 /// * `HashMap<String, String>` - Map with "http" and "https" keys and proxy URLs
421 fn to_requests_dict(&self) -> HashMap<String, String> {
422 let url = self.url();
423 let mut map = HashMap::new();
424
425 map.insert("http".to_string(), url.clone());
426 map.insert("https".to_string(), url);
427
428 map
429 }
430
431 /// Always returns `true` to ensure connection rotation.
432 ///
433 /// Webshare rotating proxies work best when a new connection is established
434 /// for each request, ensuring you get a fresh IP address each time.
435 ///
436 /// # Returns
437 ///
438 /// * `bool` - Always `true` for Webshare proxies
439 fn prevent_keeping_connections_alive(&self) -> bool {
440 true
441 }
442
443 /// Returns the configured number of retries.
444 ///
445 /// This determines how many times the library will retry a request with
446 /// a new IP address if YouTube blocks the request.
447 ///
448 /// # Returns
449 ///
450 /// * `i32` - The number of retries to attempt
451 fn retries_when_blocked(&self) -> i32 {
452 self.retries
453 }
454
455 fn as_any(&self) -> &dyn Any {
456 self
457 }
458}