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#[derive(Clone)]
14pub struct PreviewService {
15 pub default_generator: Arc<UrlPreviewGenerator>,
16 pub twitter_generator: Arc<UrlPreviewGenerator>,
17 pub github_generator: Arc<UrlPreviewGenerator>,
18 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 pub fn new() -> Self {
33 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 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 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}