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}