1#[cfg(feature = "github")]
2use crate::github_types::{is_github_url, GitHubDetailedInfo};
3use crate::{
4 is_twitter_url, CacheStrategy, Fetcher, Preview, PreviewError, PreviewGenerator,
5 UrlPreviewGenerator,
6};
7use std::sync::Arc;
8use tokio::sync::Semaphore;
9#[cfg(all(feature = "logging", feature = "github"))]
10use tracing::warn;
11#[cfg(feature = "logging")]
12use tracing::{debug, instrument};
13use url::Url;
14
15#[derive(Clone)]
18pub struct PreviewService {
19 pub default_generator: Arc<UrlPreviewGenerator>,
20 #[cfg(feature = "twitter")]
21 pub twitter_generator: Arc<UrlPreviewGenerator>,
22 #[cfg(feature = "github")]
23 pub github_generator: Arc<UrlPreviewGenerator>,
24 semaphore: Arc<Semaphore>,
26}
27
28pub const MAX_CONCURRENT_REQUESTS: usize = 500;
29
30impl Default for PreviewService {
31 fn default() -> Self {
32 Self::new()
33 }
34}
35
36impl PreviewService {
37 pub fn new() -> Self {
39 Self::with_cache_cap(1000)
44 }
45
46 pub fn with_cache_cap(cache_capacity: usize) -> Self {
47 #[cfg(feature = "logging")]
48 debug!(
49 "Initializing PreviewService with cache capacity: {}",
50 cache_capacity
51 );
52
53 let default_generator = Arc::new(UrlPreviewGenerator::new_with_fetcher(
54 cache_capacity,
55 CacheStrategy::UseCache,
56 Fetcher::new(),
57 ));
58
59 #[cfg(feature = "twitter")]
60 let twitter_generator = Arc::new(UrlPreviewGenerator::new_with_fetcher(
61 cache_capacity,
62 CacheStrategy::UseCache,
63 Fetcher::new_twitter_client(),
64 ));
65
66 #[cfg(feature = "github")]
67 let github_generator = Arc::new(UrlPreviewGenerator::new_with_fetcher(
68 cache_capacity,
69 CacheStrategy::UseCache,
70 Fetcher::new_github_client(),
71 ));
72
73 let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_REQUESTS));
74
75 #[cfg(feature = "logging")]
76 debug!("PreviewService initialized successfully");
77
78 Self {
79 default_generator,
80 #[cfg(feature = "twitter")]
81 twitter_generator,
82 #[cfg(feature = "github")]
83 github_generator,
84 semaphore,
85 }
86 }
87
88 pub fn no_cache() -> Self {
89 #[cfg(feature = "logging")]
90 debug!("Initializing PreviewService with cache capacity: {}", 0);
91
92 let default_generator = Arc::new(UrlPreviewGenerator::new_with_fetcher(
93 0,
94 CacheStrategy::NoCache,
95 Fetcher::new(),
96 ));
97
98 #[cfg(feature = "twitter")]
99 let twitter_generator = Arc::new(UrlPreviewGenerator::new_with_fetcher(
100 0,
101 CacheStrategy::NoCache,
102 Fetcher::new_twitter_client(),
103 ));
104
105 #[cfg(feature = "github")]
106 let github_generator = Arc::new(UrlPreviewGenerator::new_with_fetcher(
107 0,
108 CacheStrategy::NoCache,
109 Fetcher::new_github_client(),
110 ));
111
112 let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_REQUESTS));
113
114 #[cfg(feature = "logging")]
115 debug!("PreviewService initialized successfully");
116
117 Self {
118 default_generator,
119 #[cfg(feature = "twitter")]
120 twitter_generator,
121 #[cfg(feature = "github")]
122 github_generator,
123 semaphore,
124 }
125 }
126
127 pub fn new_with_config(config: PreviewServiceConfig) -> Self {
128 #[cfg(feature = "logging")]
129 debug!("Initializing PreviewService with custom configuration");
130
131 let default_generator = Arc::new(UrlPreviewGenerator::new_with_fetcher(
132 config.cache_capacity,
133 config.cache_strategy,
134 config.default_fetcher.unwrap_or_else(Fetcher::new),
135 ));
136
137 #[cfg(feature = "twitter")]
138 let twitter_generator = Arc::new(UrlPreviewGenerator::new_with_fetcher(
139 config.cache_capacity,
140 config.cache_strategy,
141 config
142 .twitter_fetcher
143 .unwrap_or_else(Fetcher::new_twitter_client),
144 ));
145
146 #[cfg(feature = "github")]
147 let github_generator = Arc::new(UrlPreviewGenerator::new_with_fetcher(
148 config.cache_capacity,
149 config.cache_strategy,
150 config
151 .github_fetcher
152 .unwrap_or_else(Fetcher::new_github_client),
153 ));
154
155 let semaphore = Arc::new(Semaphore::new(config.max_concurrent_requests));
156
157 #[cfg(feature = "logging")]
158 debug!("PreviewService initialized with custom configuration");
159
160 Self {
161 default_generator,
162 #[cfg(feature = "twitter")]
163 twitter_generator,
164 #[cfg(feature = "github")]
165 github_generator,
166 semaphore,
167 }
168 }
169
170 #[cfg(feature = "github")]
171 fn extract_github_info(url: &str) -> Option<(String, String)> {
172 let parsed_url = Url::parse(url).ok()?;
173 if !parsed_url.host_str()?.contains("github.com") {
174 return None;
175 }
176
177 let path_segments: Vec<&str> = parsed_url.path_segments()?.collect();
178 if path_segments.len() >= 2 {
179 return Some((path_segments[0].to_string(), path_segments[1].to_string()));
180 }
181 None
182 }
183
184 #[cfg(feature = "github")]
185 #[cfg_attr(feature = "logging", instrument(level = "debug", skip(self)))]
186 async fn generate_github_preview(&self, url: &str) -> Result<Preview, PreviewError> {
187 #[cfg(feature = "cache")]
188 if let CacheStrategy::UseCache = self.github_generator.cache_strategy {
189 if let Some(cached) = self.github_generator.cache.get(url).await {
190 return Ok(cached);
191 }
192 }
193
194 let (owner, repo_name) = Self::extract_github_info(url).ok_or_else(|| {
195 #[cfg(feature = "logging")]
196 warn!("GitHub URL parsing failed: {}", url);
197 PreviewError::ExtractError("Invalid GitHub URL format".into())
198 })?;
199
200 match self
201 .github_generator
202 .fetcher
203 .fetch_github_basic_preview(&owner, &repo_name)
204 .await
205 {
206 Ok(basic_info) => {
207 #[cfg(feature = "logging")]
208 debug!("Found GitHub Repo {}/{} basic infos", owner, repo_name);
209
210 let preview = Preview {
211 url: url.to_string(),
212 title: basic_info.title,
213 description: basic_info.description,
214 image_url: basic_info.image_url,
215 site_name: Some("GitHub".to_string()),
216 favicon: Some(
217 "https://github.githubassets.com/favicons/favicon.svg".to_string(),
218 ),
219 };
220
221 #[cfg(feature = "cache")]
222 if let CacheStrategy::UseCache = self.github_generator.cache_strategy {
223 self.github_generator
224 .cache
225 .set(url.to_string(), preview.clone())
226 .await;
227 }
228
229 Ok(preview)
230 }
231 Err(_e) => {
232 #[cfg(feature = "logging")]
233 warn!(
234 error = ?_e,
235 "Failed to get GitHub basic preview, will use general preview generator as fallback"
236 );
237 self.github_generator.generate_preview(url).await
238 }
239 }
240 }
241
242 #[cfg_attr(feature = "logging", instrument(level = "debug", skip(self)))]
243 pub async fn generate_preview(&self, url: &str) -> Result<Preview, PreviewError> {
244 #[cfg(feature = "logging")]
245 debug!("Starting preview generation for URL: {}", url);
246
247 let _permit = self
248 .semaphore
249 .acquire()
250 .await
251 .map_err(|_| PreviewError::ConcurrencyLimitError)?;
252
253 let _ = Url::parse(url)
254 .map_err(|e| PreviewError::ParseError(format!("Invalid URL format: {}", e)))?;
255
256 if is_twitter_url(url) {
257 #[cfg(feature = "logging")]
258 debug!("Detected Twitter URL, using specialized handler");
259 #[cfg(feature = "twitter")]
260 {
261 self.twitter_generator.generate_preview(url).await
262 }
263 #[cfg(not(feature = "twitter"))]
264 {
265 self.default_generator.generate_preview(url).await
266 }
267 } else if cfg!(feature = "github") && {
268 #[cfg(feature = "github")]
269 {
270 is_github_url(url)
271 }
272 #[cfg(not(feature = "github"))]
273 {
274 false
275 }
276 } {
277 #[cfg(feature = "logging")]
278 debug!("Detected GitHub URL, using specialized handler");
279 #[cfg(feature = "github")]
280 {
281 self.generate_github_preview(url).await
282 }
283 #[cfg(not(feature = "github"))]
284 {
285 self.default_generator.generate_preview(url).await
286 }
287 } else {
288 #[cfg(feature = "logging")]
289 debug!("Using default URL handler");
290 self.default_generator.generate_preview(url).await
291 }
292 }
293
294 #[cfg_attr(feature = "logging", instrument(level = "debug", skip(self)))]
295 pub async fn generate_preview_with_concurrency(
296 &self,
297 url: &str,
298 ) -> Result<Preview, PreviewError> {
299 #[cfg(feature = "logging")]
300 debug!("Starting preview generation for URL: {}", url);
301
302 let _permit = self
303 .semaphore
304 .acquire()
305 .await
306 .map_err(|_| PreviewError::ConcurrencyLimitError)?;
307
308 let _ = Url::parse(url)
309 .map_err(|e| PreviewError::ParseError(format!("Invalid URL format: {}", e)))?;
310
311 if is_twitter_url(url) {
312 #[cfg(feature = "logging")]
313 debug!("Detected Twitter URL, using specialized handler");
314 #[cfg(feature = "twitter")]
315 {
316 self.twitter_generator.generate_preview(url).await
317 }
318 #[cfg(not(feature = "twitter"))]
319 {
320 self.default_generator.generate_preview(url).await
321 }
322 } else if cfg!(feature = "github") && {
323 #[cfg(feature = "github")]
324 {
325 is_github_url(url)
326 }
327 #[cfg(not(feature = "github"))]
328 {
329 false
330 }
331 } {
332 #[cfg(feature = "logging")]
333 debug!("Detected GitHub URL, using specialized handler");
334 #[cfg(feature = "github")]
335 {
336 self.generate_github_preview(url).await
337 }
338 #[cfg(not(feature = "github"))]
339 {
340 self.default_generator.generate_preview(url).await
341 }
342 } else {
343 #[cfg(feature = "logging")]
344 debug!("Using default URL handler");
345 self.default_generator.generate_preview(url).await
346 }
347 }
348
349 #[cfg(feature = "github")]
350 pub async fn generate_github_basic_preview(&self, url: &str) -> Result<Preview, PreviewError> {
351 let (owner, repo) = Self::extract_github_info(url)
352 .ok_or_else(|| PreviewError::ExtractError("Invalid GitHub URL format".into()))?;
353
354 let basic_info = self
355 .github_generator
356 .fetcher
357 .fetch_github_basic_preview(&owner, &repo)
358 .await?;
359
360 Ok(Preview {
361 url: url.to_string(),
362 title: basic_info.title,
363 description: basic_info.description,
364 image_url: basic_info.image_url,
365 site_name: Some("GitHub".to_string()),
366 favicon: Some("https://github.githubassets.com/favicons/favicon.svg".to_string()),
367 })
368 }
369
370 #[cfg(feature = "github")]
371 pub async fn get_github_detailed_info(
372 &self,
373 url: &str,
374 ) -> Result<GitHubDetailedInfo, PreviewError> {
375 let (owner, repo) = Self::extract_github_info(url)
376 .ok_or_else(|| PreviewError::ExtractError("Invalid GitHub URL format".into()))?;
377
378 self.github_generator
379 .fetcher
380 .fetch_github_detailed_info(&owner, &repo)
381 .await
382 }
383}
384
385impl PreviewService {
387 pub fn new_minimal() -> Self {
389 let default_generator = Arc::new(UrlPreviewGenerator::new(100, CacheStrategy::UseCache));
390 #[cfg(feature = "twitter")]
391 let twitter_generator = Arc::new(UrlPreviewGenerator::new(100, CacheStrategy::UseCache));
392 #[cfg(feature = "github")]
393 let github_generator = Arc::new(UrlPreviewGenerator::new(100, CacheStrategy::UseCache));
394
395 Self {
396 default_generator,
397 #[cfg(feature = "twitter")]
398 twitter_generator,
399 #[cfg(feature = "github")]
400 github_generator,
401 semaphore: Arc::new(Semaphore::new(10)),
402 }
403 }
404
405 #[cfg_attr(feature = "logging", instrument(level = "debug", skip(self)))]
406 pub async fn generate_preview_no_cache(&self, url: &str) -> Result<Preview, PreviewError> {
407 let generator = UrlPreviewGenerator::new_with_fetcher(
408 0,
409 CacheStrategy::NoCache,
410 self.default_generator.fetcher.clone(),
411 );
412 generator.generate_preview(url).await
413 }
414}
415
416pub struct PreviewServiceConfig {
417 pub cache_capacity: usize,
418 pub cache_strategy: CacheStrategy,
419 pub max_concurrent_requests: usize,
420 pub default_fetcher: Option<Fetcher>,
421 #[cfg(feature = "twitter")]
422 pub twitter_fetcher: Option<Fetcher>,
423 #[cfg(feature = "github")]
424 pub github_fetcher: Option<Fetcher>,
425}
426
427impl PreviewServiceConfig {
428 pub fn new(cache_capacity: usize) -> Self {
429 Self {
430 cache_capacity,
431 cache_strategy: CacheStrategy::UseCache,
432 max_concurrent_requests: MAX_CONCURRENT_REQUESTS,
433 default_fetcher: None,
434 #[cfg(feature = "twitter")]
435 twitter_fetcher: None,
436 #[cfg(feature = "github")]
437 github_fetcher: None,
438 }
439 }
440
441 #[cfg(feature = "github")]
442 pub fn with_github_fetcher(mut self, fetcher: Fetcher) -> Self {
443 self.github_fetcher = Some(fetcher);
444 self
445 }
446
447 pub fn with_default_fetcher(mut self, fetcher: Fetcher) -> Self {
448 self.default_fetcher = Some(fetcher);
449 self
450 }
451
452 #[cfg(feature = "twitter")]
453 pub fn with_twitter_fetcher(mut self, fetcher: Fetcher) -> Self {
454 self.twitter_fetcher = Some(fetcher);
455 self
456 }
457
458 pub fn with_max_concurrent_requests(mut self, max_concurrent_requests: usize) -> Self {
459 self.max_concurrent_requests = max_concurrent_requests;
460 self
461 }
462
463 pub fn with_cache_strategy(mut self, cache_strategy: CacheStrategy) -> Self {
464 self.cache_strategy = cache_strategy;
465 self
466 }
467}