url_preview/
preview_service.rs

1use crate::github_types::{is_github_url, GitHubDetailedInfo};
2use crate::{
3    is_twitter_url, CacheStrategy, Fetcher, Preview, PreviewError, PreviewGenerator,
4    UrlPreviewGenerator,
5};
6use std::sync::Arc;
7use tokio::sync::Semaphore;
8use tracing::{debug, instrument, warn};
9use url::Url;
10
11/// PreviewService provides a unified preview generation service
12/// It can automatically identify different types of URLs and use appropriate processing strategies
13#[derive(Clone)]
14pub struct PreviewService {
15    pub default_generator: Arc<UrlPreviewGenerator>,
16    pub twitter_generator: Arc<UrlPreviewGenerator>,
17    pub github_generator: Arc<UrlPreviewGenerator>,
18    // Max Concurrent Requests
19    semaphore: Arc<Semaphore>,
20}
21
22pub const MAX_CONCURRENT_REQUESTS: usize = 500;
23
24impl Default for PreviewService {
25    fn default() -> Self {
26        Self::new()
27    }
28}
29
30impl PreviewService {
31    /// Creates a new preview service instance with default cache capacity
32    pub fn new() -> Self {
33        // Set 1000 cache entries for each generator
34        // This means that up to 1000 different URL previews can be cached for each type (Normal/Twitter/GitHub)
35        // 1000 cache entries take about 1-2MB memory
36        // Total of 3-6MB for three generators is reasonable for modern systems
37        Self::with_cache_cap(1000)
38    }
39
40    pub fn with_cache_cap(cache_capacity: usize) -> Self {
41        debug!(
42            "Initializing PreviewService with cache capacity: {}",
43            cache_capacity
44        );
45
46        let default_generator = Arc::new(UrlPreviewGenerator::new_with_fetcher(
47            cache_capacity,
48            CacheStrategy::UseCache,
49            Fetcher::new(),
50        ));
51
52        let twitter_generator = Arc::new(UrlPreviewGenerator::new_with_fetcher(
53            cache_capacity,
54            CacheStrategy::UseCache,
55            Fetcher::new_twitter_client(),
56        ));
57
58        let github_generator = Arc::new(UrlPreviewGenerator::new_with_fetcher(
59            cache_capacity,
60            CacheStrategy::UseCache,
61            Fetcher::new_github_client(),
62        ));
63
64        let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_REQUESTS));
65
66        debug!("PreviewService initialized successfully");
67
68        Self {
69            default_generator,
70            twitter_generator,
71            github_generator,
72            semaphore,
73        }
74    }
75
76    pub fn with_no_cache() -> Self {
77        debug!("Initializing PreviewService with cache capacity: {}", 0);
78
79        let default_generator = Arc::new(UrlPreviewGenerator::new_with_fetcher(
80            0,
81            CacheStrategy::NoCache,
82            Fetcher::new(),
83        ));
84
85        let twitter_generator = Arc::new(UrlPreviewGenerator::new_with_fetcher(
86            0,
87            CacheStrategy::NoCache,
88            Fetcher::new_twitter_client(),
89        ));
90
91        let github_generator = Arc::new(UrlPreviewGenerator::new_with_fetcher(
92            0,
93            CacheStrategy::NoCache,
94            Fetcher::new_github_client(),
95        ));
96
97        let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_REQUESTS));
98
99        debug!("PreviewService initialized successfully");
100
101        Self {
102            default_generator,
103            twitter_generator,
104            github_generator,
105            semaphore,
106        }
107    }
108
109    pub fn new_with_config(config: PreviewServiceConfig) -> Self {
110        debug!("Initializing PreviewService with custom configuration");
111
112        let default_generator = Arc::new(UrlPreviewGenerator::new_with_fetcher(
113            config.cache_capacity,
114            config.cache_strategy.clone(),
115            config.default_fetcher.unwrap_or_default(),
116        ));
117
118        let twitter_generator = Arc::new(UrlPreviewGenerator::new_with_fetcher(
119            config.cache_capacity,
120            config.cache_strategy.clone(),
121            config
122                .twitter_fetcher
123                .unwrap_or_else(Fetcher::new_twitter_client),
124        ));
125
126        let github_generator = Arc::new(UrlPreviewGenerator::new_with_fetcher(
127            config.cache_capacity,
128            config.cache_strategy,
129            config
130                .github_fetcher
131                .unwrap_or_else(Fetcher::new_github_client),
132        ));
133
134        let semaphore = Arc::new(Semaphore::new(config.max_concurrent_requests));
135
136        debug!("PreviewService initialized with custom configuration");
137
138        Self {
139            default_generator,
140            twitter_generator,
141            github_generator,
142            semaphore,
143        }
144    }
145
146    fn extract_github_info(url: &str) -> Option<(String, String)> {
147        let url = Url::parse(url).ok()?;
148        if !url.host_str()?.contains("github.com") {
149            return None;
150        }
151
152        let path_segments: Vec<&str> = url.path_segments()?.collect();
153        if path_segments.len() >= 2 {
154            Some((path_segments[0].to_string(), path_segments[1].to_string()))
155        } else {
156            None
157        }
158    }
159
160    #[instrument(level = "debug", skip(self))]
161    async fn generate_github_preview(&self, url: &str) -> Result<Preview, PreviewError> {
162        if let CacheStrategy::UseCache = self.github_generator.cache_strategy {
163            if let Some(cached) = self.github_generator.cache.get(url).await {
164                return Ok(cached);
165            };
166        };
167
168        let (owner, repo_name) = Self::extract_github_info(url).ok_or_else(|| {
169            warn!("GitHub URL parsing failed: {}", url);
170            PreviewError::ExtractError("Invalid GitHub URL format".into())
171        })?;
172
173        match self
174            .github_generator
175            .fetcher
176            .fetch_github_basic_preview(&owner, &repo_name)
177            .await
178        {
179            Ok(basic_info) => {
180                debug!("Found GitHub Repo {}/{} basic infos", owner, repo_name);
181
182                let preview = Preview {
183                    url: url.to_string(),
184                    title: Some(basic_info.title),
185                    description: basic_info.description,
186                    image_url: basic_info.image_url,
187                    site_name: Some("GitHub".to_string()),
188                    favicon: Some(
189                        "https://github.githubassets.com/favicons/favicon.svg".to_string(),
190                    ),
191                };
192
193                if let CacheStrategy::UseCache = self.github_generator.cache_strategy {
194                    self.github_generator
195                        .cache
196                        .set(url.to_string(), preview.clone())
197                        .await;
198                }
199
200                Ok(preview)
201            }
202            Err(e) => {
203                warn!(
204                    error = %e,
205                    "Failed to get GitHub basic preview, will use general preview generator as fallback"
206                );
207                self.github_generator.generate_preview(url).await
208            }
209        }
210    }
211
212    #[instrument(level = "debug", skip(self))]
213    pub async fn generate_preview(&self, url: &str) -> Result<Preview, PreviewError> {
214        debug!("Starting preview generation for URL: {}", url);
215
216        // match &result {
217        //     Ok(preview) => {
218        //         log_preview_card(preview, url);
219        //     }
220        //     Err(e) => {
221        //         log_error_card(url, e);
222        //     }
223        // }
224
225        if is_twitter_url(url) {
226            debug!("Detected Twitter URL, using specialized handler");
227            self.twitter_generator.generate_preview(url).await
228        } else if is_github_url(url) {
229            debug!("Detected GitHub URL, using specialized handler");
230            self.generate_github_preview(url).await
231        } else {
232            debug!("Using default URL handler");
233            self.default_generator.generate_preview(url).await
234        }
235    }
236
237    #[instrument(level = "debug", skip(self))]
238    pub async fn generate_preview_no_cache(&self, url: &str) -> Result<Preview, PreviewError> {
239        debug!("Starting preview generation for URL: {}", url);
240
241        // match &result {
242        //     Ok(preview) => {
243        //         log_preview_card(preview, url);
244        //     }
245        //     Err(e) => {
246        //         log_error_card(url, e);
247        //     }
248        // }
249
250        if is_twitter_url(url) {
251            debug!("Detected Twitter URL, using specialized handler");
252            self.twitter_generator.generate_preview(url).await
253        } else if is_github_url(url) {
254            debug!("Detected GitHub URL, using specialized handler");
255            self.generate_github_preview(url).await
256        } else {
257            debug!("Using default URL handler");
258            self.default_generator.generate_preview(url).await
259        }
260    }
261
262    pub async fn generate_github_basic_preview(&self, url: &str) -> Result<Preview, PreviewError> {
263        let (owner, repo) = Self::extract_github_info(url)
264            .ok_or_else(|| PreviewError::ExtractError("Invalid GitHub URL format".into()))?;
265
266        let basic_info = self
267            .github_generator
268            .fetcher
269            .fetch_github_basic_preview(&owner, &repo)
270            .await?;
271
272        Ok(Preview {
273            url: url.to_string(),
274            title: Some(basic_info.title),
275            description: basic_info.description,
276            image_url: basic_info.image_url,
277            site_name: Some("GitHub".to_string()),
278            favicon: Some("https://github.githubassets.com/favicons/favicon.svg".to_string()),
279        })
280    }
281
282    pub async fn get_github_detailed_info(
283        &self,
284        url: &str,
285    ) -> Result<GitHubDetailedInfo, PreviewError> {
286        let (owner, repo) = Self::extract_github_info(url)
287            .ok_or_else(|| PreviewError::ExtractError("Invalid GitHub URL format".into()))?;
288
289        self.github_generator
290            .fetcher
291            .fetch_github_detailed_info(&owner, &repo)
292            .await
293    }
294}
295
296impl PreviewService {
297    pub fn new_with_concurrency(config: PreviewServiceConfig) -> Self {
298        let semaphore = Arc::new(Semaphore::new(config.max_concurrent_requests));
299        let default_generator = Arc::new(UrlPreviewGenerator::new(
300            config.cache_capacity,
301            config.cache_strategy.clone(),
302        ));
303        let twitter_generator = Arc::new(UrlPreviewGenerator::new(
304            config.cache_capacity,
305            config.cache_strategy.clone(),
306        ));
307        let github_generator = Arc::new(UrlPreviewGenerator::new(
308            config.cache_capacity,
309            config.cache_strategy,
310        ));
311
312        PreviewService {
313            default_generator,
314            twitter_generator,
315            github_generator,
316            semaphore,
317        }
318    }
319
320    #[instrument(level = "debug", skip(self))]
321    pub async fn generate_preview_with_concurrency(
322        &self,
323        url: &str,
324    ) -> Result<Preview, PreviewError> {
325        let permit = self.semaphore.clone().acquire_owned().await;
326        let preview = self.generate_preview(url).await;
327        drop(permit);
328        preview
329    }
330}
331
332#[derive(Default, Clone)]
333pub struct PreviewServiceConfig {
334    pub cache_capacity: usize,
335    pub cache_strategy: CacheStrategy,
336    pub default_fetcher: Option<Fetcher>,
337    pub twitter_fetcher: Option<Fetcher>,
338    pub github_fetcher: Option<Fetcher>,
339    pub max_concurrent_requests: usize,
340}
341
342impl PreviewServiceConfig {
343    pub fn new(cache_capacity: usize, cache_strategy: CacheStrategy) -> Self {
344        Self {
345            cache_capacity,
346            cache_strategy,
347            default_fetcher: None,
348            twitter_fetcher: None,
349            github_fetcher: None,
350            max_concurrent_requests: MAX_CONCURRENT_REQUESTS,
351        }
352    }
353
354    pub fn with_github_fetcher(mut self, fetcher: Fetcher) -> Self {
355        self.github_fetcher = Some(fetcher);
356        self
357    }
358
359    pub fn with_default_fetcher(mut self, fetcher: Fetcher) -> Self {
360        self.default_fetcher = Some(fetcher);
361        self
362    }
363
364    pub fn with_twitter_fetcher(mut self, fetcher: Fetcher) -> Self {
365        self.twitter_fetcher = Some(fetcher);
366        self
367    }
368}