kick_rust/fetch/
strategies.rs

1//! Different fetching strategies for Kick API data
2//!
3//! This module provides various strategies for fetching channel information,
4//! focusing on the official API endpoint with fallback mechanisms.
5
6use async_trait::async_trait;
7use crate::fetch::{
8    types::{ChannelInfo, FetchResult, FetchError, StrategyConfig},
9    parsers::JsonParser,
10    useragent::generate_browser_fingerprint,
11};
12use reqwest::Client;
13use std::time::Duration;
14use tokio::time::sleep;
15use tracing::{info, warn, debug};
16
17
18/// Base strategy trait for all fetching approaches
19#[async_trait]
20pub trait FetchStrategy {
21    /// Get the name of this strategy
22    fn name(&self) -> &'static str;
23
24    /// Fetch channel information using this strategy
25    async fn fetch_channel(&self, client: &Client, channel_name: &str, config: &StrategyConfig) -> FetchResult<ChannelInfo>;
26
27    /// Check if this strategy is available/enabled
28    fn is_enabled(&self, _config: &StrategyConfig) -> bool {
29        true
30    }
31}
32
33
34
35
36
37
38
39
40
41/// Curl-based strategy for better Cloudflare compatibility
42pub struct CurlStrategy {
43    _parser: JsonParser, // Keep for potential future use
44}
45
46impl CurlStrategy {
47    pub fn new() -> Self {
48        Self {
49            _parser: JsonParser, // Keep for potential future use
50        }
51    }
52
53    async fn make_curl_request(&self, url: &str) -> Result<String, FetchError> {
54        // Generate realistic browser fingerprint
55        let fingerprint = generate_browser_fingerprint();
56
57        let mut headers = curl::easy::List::new();
58        for header in fingerprint.get_curl_headers() {
59            headers.append(&header).unwrap();
60        }
61
62        let mut easy = curl::easy::Easy::new();
63        easy.url(url).unwrap();
64        easy.http_headers(headers).unwrap();
65        easy.follow_location(true).unwrap();
66        easy.timeout(Duration::from_secs(30)).unwrap();
67
68        let mut data = Vec::new();
69        {
70            let mut transfer = easy.transfer();
71
72            transfer.write_function(|new_data| {
73                data.extend_from_slice(new_data);
74                Ok(new_data.len())
75            }).unwrap();
76
77            match transfer.perform() {
78                Ok(_) => {
79                    // Transfer is dropped here, releasing the borrow on easy
80                }
81                Err(e) => {
82                    warn!("Curl request failed: {}", e);
83                    return Err(FetchError::Network(format!("Curl request failed: {}", e)));
84                }
85            }
86        }
87
88        // Now we can safely use easy again
89        let response_code = easy.response_code().unwrap_or(0);
90        debug!("Curl response code: {}", response_code);
91
92        if response_code == 200 {
93            // Try to decompress the response if needed
94            let decompressed_data = self.try_decompress(&data).await?;
95            let response_text = String::from_utf8(decompressed_data)
96                .map_err(|e| FetchError::Network(format!("Invalid UTF-8 response: {}", e)))?;
97            Ok(response_text)
98        } else if response_code == 404 {
99            Err(FetchError::ChannelNotFound("Channel not found".to_string()))
100        } else if response_code == 429 {
101            Err(FetchError::RateLimit)
102        } else {
103            let error_text = String::from_utf8(data).unwrap_or_default();
104            Err(FetchError::Http(format!("HTTP {}: {}", response_code, error_text)))
105        }
106    }
107}
108
109#[async_trait]
110impl FetchStrategy for CurlStrategy {
111    fn name(&self) -> &'static str {
112        "curl"
113    }
114
115    fn is_enabled(&self, _config: &StrategyConfig) -> bool {
116        true
117    }
118
119    async fn fetch_channel(&self, _client: &Client, channel_name: &str, _config: &StrategyConfig) -> FetchResult<ChannelInfo> {
120        let api_url = format!("https://kick.com/api/v2/channels/{}", channel_name);
121        debug!("Fetching channel {} using curl: {}", channel_name, api_url);
122
123        let response_text = self.make_curl_request(&api_url).await?;
124        JsonParser::parse_channel_response(&response_text)
125    }
126
127}
128
129impl CurlStrategy {
130    async fn try_decompress(&self, data: &[u8]) -> FetchResult<Vec<u8>> {
131        // Try to detect and decompress gzip/deflate/brotli compressed data
132        if data.len() >= 2 {
133            // Check for gzip magic number
134            if data[0] == 0x1f && data[1] == 0x8b {
135                debug!("Detected gzip compression, decompressing...");
136                return self.decompress_gzip(data).await;
137            }
138            // Check for brotli magic number
139            if data.len() >= 6 && &data[0..6] == b"\x8b\x02\x80\x00\x00\x00" {
140                debug!("Detected brotli compression, decompressing...");
141                return self.decompress_brotli(data).await;
142            }
143        }
144
145        // Try to decompress as gzip anyway (some responses don't have proper magic numbers)
146        if let Ok(decompressed) = self.decompress_gzip(data).await {
147            debug!("Successfully decompressed as gzip");
148            return Ok(decompressed);
149        }
150
151        // Try brotli
152        if let Ok(decompressed) = self.decompress_brotli(data).await {
153            debug!("Successfully decompressed as brotli");
154            return Ok(decompressed);
155        }
156
157        // If all decompression attempts fail, assume it's not compressed
158        debug!("No compression detected or decompression failed, using raw data");
159        Ok(data.to_vec())
160    }
161
162    async fn decompress_gzip(&self, data: &[u8]) -> FetchResult<Vec<u8>> {
163        use flate2::read::GzDecoder;
164        use std::io::Read;
165
166        let mut decoder = GzDecoder::new(data);
167        let mut decompressed = Vec::new();
168        decoder.read_to_end(&mut decompressed)
169            .map_err(|e| FetchError::Network(format!("Gzip decompression failed: {}", e)))?;
170        Ok(decompressed)
171    }
172
173    async fn decompress_brotli(&self, data: &[u8]) -> FetchResult<Vec<u8>> {
174        use brotli::Decompressor;
175        use std::io::Read;
176
177        let mut decompressed = Vec::new();
178        let mut decoder = Decompressor::new(data, 4096);
179        decoder.read_to_end(&mut decompressed)
180            .map_err(|e| FetchError::Network(format!("Brotli decompression failed: {}", e)))?;
181        Ok(decompressed)
182    }
183}
184
185/// Strategy manager that handles multiple fetching strategies
186pub struct StrategyManager {
187    strategies: Vec<Box<dyn FetchStrategy + Send + Sync>>,
188}
189
190impl StrategyManager {
191    /// Create a new strategy manager with default strategies
192    pub fn new() -> Result<Self, FetchError> {
193        let strategies: Vec<Box<dyn FetchStrategy + Send + Sync>> = vec![
194            Box::new(CurlStrategy::new()), // Only use curl strategy as it works reliably
195        ];
196
197        Ok(Self { strategies })
198    }
199
200    /// Create a strategy manager with custom configuration
201    pub fn with_config(_config: &StrategyConfig) -> Result<Self, FetchError> {
202        let strategies: Vec<Box<dyn FetchStrategy + Send + Sync>> = vec![
203            Box::new(CurlStrategy::new()), // Only use curl strategy as it works reliably
204        ];
205
206        Ok(Self { strategies })
207    }
208
209    /// Fetch channel information using available strategies
210    pub async fn fetch_channel(
211        &self,
212        client: &Client,
213        channel_name: &str,
214        config: &StrategyConfig,
215    ) -> FetchResult<ChannelInfo> {
216        let mut last_error = None;
217
218        for strategy in &self.strategies {
219            if !strategy.is_enabled(config) {
220                debug!("Skipping disabled strategy: {}", strategy.name());
221                continue;
222            }
223
224            info!("Trying strategy: {}", strategy.name());
225
226            match strategy.fetch_channel(client, channel_name, config).await {
227                Ok(channel) => {
228                    info!("Successfully fetched channel {} using strategy: {}", channel_name, strategy.name());
229                    return Ok(channel);
230                }
231                Err(e) => {
232                    warn!("Strategy {} failed for channel {}: {}", strategy.name(), channel_name, e);
233                    last_error = Some(e);
234
235                    // Add delay between strategies
236                    sleep(Duration::from_millis(500)).await;
237                }
238            }
239        }
240
241        Err(last_error.unwrap_or_else(|| FetchError::ChannelNotFound(format!("No strategies succeeded for channel: {}", channel_name))))
242    }
243
244    /// Get the names of all available strategies
245    pub fn strategy_names(&self) -> Vec<&'static str> {
246        self.strategies.iter().map(|s| s.name()).collect()
247    }
248}
249
250impl Default for StrategyManager {
251    fn default() -> Self {
252        Self::new().unwrap_or_else(|_| {
253            warn!("Failed to create StrategyManager, using curl strategy only");
254            Self {
255                strategies: vec![
256                    Box::new(CurlStrategy::new()),
257                ],
258            }
259        })
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266    use crate::fetch::types::StrategyConfig;
267
268    #[tokio::test]
269    async fn test_strategy_manager_creation() {
270        let manager = StrategyManager::new();
271        assert!(manager.is_ok());
272
273        if let Ok(manager) = manager {
274            let names = manager.strategy_names();
275            assert!(!names.is_empty());
276            assert!(names.contains(&"curl"));
277        }
278    }
279
280    #[tokio::test]
281    async fn test_strategy_manager_with_config() {
282        let config = StrategyConfig::default();
283
284        let manager = StrategyManager::with_config(&config);
285        assert!(manager.is_ok());
286
287        if let Ok(manager) = manager {
288            let names = manager.strategy_names();
289            assert_eq!(names.len(), 1);
290            assert_eq!(names[0], "curl");
291        }
292    }
293
294    #[test]
295    fn test_curl_strategy() {
296        let strategy = CurlStrategy::new();
297        assert_eq!(strategy.name(), "curl");
298
299        let config = StrategyConfig::default();
300        assert!(strategy.is_enabled(&config));
301    }
302}