1use crate::github_types::{is_github_url, GitHubDetailedInfo};
2use crate::{
3 is_twitter_url, Fetcher, Preview, PreviewError, PreviewGenerator, UrlPreviewGenerator,
4};
5use std::sync::Arc;
6use tokio::sync::Semaphore;
7use tracing::{debug, instrument, warn};
8use url::Url;
9
10#[derive(Clone)]
13pub struct PreviewService {
14 pub default_generator: Arc<UrlPreviewGenerator>,
15 pub twitter_generator: Arc<UrlPreviewGenerator>,
16 pub github_generator: Arc<UrlPreviewGenerator>,
17 semaphore: Arc<Semaphore>,
19}
20
21pub const MAX_CONCURRENT_REQUESTS: usize = 500;
22
23impl Default for PreviewService {
24 fn default() -> Self {
25 Self::new()
26 }
27}
28
29impl PreviewService {
30 pub fn new() -> Self {
32 Self::with_cache_cap(1000)
37 }
38
39 pub fn with_cache_cap(cache_capacity: usize) -> Self {
40 debug!(
41 "Initializing PreviewService with cache capacity: {}",
42 cache_capacity
43 );
44
45 let default_generator = Arc::new(UrlPreviewGenerator::new_with_fetcher(
46 cache_capacity,
47 Fetcher::new(),
48 ));
49
50 let twitter_generator = Arc::new(UrlPreviewGenerator::new_with_fetcher(
51 cache_capacity,
52 Fetcher::new_twitter_client(),
53 ));
54
55 let github_generator = Arc::new(UrlPreviewGenerator::new_with_fetcher(
56 cache_capacity,
57 Fetcher::new_github_client(),
58 ));
59
60 let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_REQUESTS));
61
62 debug!("PreviewService initialized successfully");
63
64 Self {
65 default_generator,
66 twitter_generator,
67 github_generator,
68 semaphore,
69 }
70 }
71
72 pub fn new_with_config(config: PreviewServiceConfig) -> Self {
73 debug!("Initializing PreviewService with custom configuration");
74
75 let default_generator = Arc::new(UrlPreviewGenerator::new_with_fetcher(
76 config.cache_capacity,
77 config.default_fetcher.unwrap_or_default(),
78 ));
79
80 let twitter_generator = Arc::new(UrlPreviewGenerator::new_with_fetcher(
81 config.cache_capacity,
82 config
83 .twitter_fetcher
84 .unwrap_or_else(Fetcher::new_twitter_client),
85 ));
86
87 let github_generator = Arc::new(UrlPreviewGenerator::new_with_fetcher(
88 config.cache_capacity,
89 config
90 .github_fetcher
91 .unwrap_or_else(Fetcher::new_github_client),
92 ));
93
94 let semaphore = Arc::new(Semaphore::new(config.max_concurrent_requests));
95
96 debug!("PreviewService initialized with custom configuration");
97
98 Self {
99 default_generator,
100 twitter_generator,
101 github_generator,
102 semaphore,
103 }
104 }
105
106 fn extract_github_info(url: &str) -> Option<(String, String)> {
107 let url = Url::parse(url).ok()?;
108 if !url.host_str()?.contains("github.com") {
109 return None;
110 }
111
112 let path_segments: Vec<&str> = url.path_segments()?.collect();
113 if path_segments.len() >= 2 {
114 Some((path_segments[0].to_string(), path_segments[1].to_string()))
115 } else {
116 None
117 }
118 }
119
120 #[instrument(level = "debug", skip(self))]
121 async fn generate_github_preview(&self, url: &str) -> Result<Preview, PreviewError> {
122 if let Some(cached) = self.github_generator.cache.get(url).await {
123 return Ok(cached);
124 }
125
126 let (owner, repo_name) = Self::extract_github_info(url).ok_or_else(|| {
127 warn!("GitHub URL parsing failed: {}", url);
128 PreviewError::ExtractError("Invalid GitHub URL format".into())
129 })?;
130
131 match self
132 .github_generator
133 .fetcher
134 .fetch_github_basic_preview(&owner, &repo_name)
135 .await
136 {
137 Ok(basic_info) => {
138 debug!("Found GitHub Repo {}/{} basic infos", owner, repo_name);
139
140 let preview = Preview {
141 url: url.to_string(),
142 title: Some(basic_info.title),
143 description: basic_info.description,
144 image_url: basic_info.image_url,
145 site_name: Some("GitHub".to_string()),
146 favicon: Some(
147 "https://github.githubassets.com/favicons/favicon.svg".to_string(),
148 ),
149 };
150
151 self.github_generator
152 .cache
153 .set(url.to_string(), preview.clone())
154 .await;
155
156 Ok(preview)
157 }
158 Err(e) => {
159 warn!(
160 error = %e,
161 "Failed to get GitHub basic preview, will use general preview generator as fallback"
162 );
163 self.github_generator.generate_preview(url).await
164 }
165 }
166 }
167
168 #[instrument(level = "debug", skip(self))]
169 pub async fn generate_preview(&self, url: &str) -> Result<Preview, PreviewError> {
170 debug!("Starting preview generation for URL: {}", url);
171
172
173
174 if is_twitter_url(url) {
184 debug!("Detected Twitter URL, using specialized handler");
185 self.twitter_generator.generate_preview(url).await
186 } else if is_github_url(url) {
187 debug!("Detected GitHub URL, using specialized handler");
188 self.generate_github_preview(url).await
189 } else {
190 debug!("Using default URL handler");
191 self.default_generator.generate_preview(url).await
192 }
193 }
194
195 pub async fn generate_github_basic_preview(&self, url: &str) -> Result<Preview, PreviewError> {
196 let (owner, repo) = Self::extract_github_info(url)
197 .ok_or_else(|| PreviewError::ExtractError("Invalid GitHub URL format".into()))?;
198
199 let basic_info = self
200 .github_generator
201 .fetcher
202 .fetch_github_basic_preview(&owner, &repo)
203 .await?;
204
205 Ok(Preview {
206 url: url.to_string(),
207 title: Some(basic_info.title),
208 description: basic_info.description,
209 image_url: basic_info.image_url,
210 site_name: Some("GitHub".to_string()),
211 favicon: Some("https://github.githubassets.com/favicons/favicon.svg".to_string()),
212 })
213 }
214
215 pub async fn get_github_detailed_info(
216 &self,
217 url: &str,
218 ) -> Result<GitHubDetailedInfo, PreviewError> {
219 let (owner, repo) = Self::extract_github_info(url)
220 .ok_or_else(|| PreviewError::ExtractError("Invalid GitHub URL format".into()))?;
221
222 self.github_generator
223 .fetcher
224 .fetch_github_detailed_info(&owner, &repo)
225 .await
226 }
227}
228
229impl PreviewService {
230 pub fn new_with_concurrency(config: PreviewServiceConfig) -> Self {
231 let semaphore = Arc::new(Semaphore::new(config.max_concurrent_requests));
232 let default_generator = Arc::new(UrlPreviewGenerator::new(config.cache_capacity));
233 let twitter_generator = Arc::new(UrlPreviewGenerator::new(config.cache_capacity));
234 let github_generator = Arc::new(UrlPreviewGenerator::new(config.cache_capacity));
235
236 PreviewService {
237 default_generator,
238 twitter_generator,
239 github_generator,
240 semaphore,
241 }
242 }
243
244 #[instrument(level = "debug", skip(self))]
245 pub async fn generate_preview_with_concurrency(
246 &self,
247 url: &str,
248 ) -> Result<Preview, PreviewError> {
249 let permit = self.semaphore.clone().acquire_owned().await;
250 let preview = self.generate_preview(url).await;
251 drop(permit);
252 preview
253 }
254}
255
256#[derive(Default, Clone)]
257pub struct PreviewServiceConfig {
258 pub cache_capacity: usize,
259 pub default_fetcher: Option<Fetcher>,
260 pub twitter_fetcher: Option<Fetcher>,
261 pub github_fetcher: Option<Fetcher>,
262 pub max_concurrent_requests: usize,
263}
264
265impl PreviewServiceConfig {
266 pub fn new(cache_capacity: usize) -> Self {
267 Self {
268 cache_capacity,
269 default_fetcher: None,
270 twitter_fetcher: None,
271 github_fetcher: None,
272 max_concurrent_requests: MAX_CONCURRENT_REQUESTS,
273 }
274 }
275
276 pub fn with_github_fetcher(mut self, fetcher: Fetcher) -> Self {
277 self.github_fetcher = Some(fetcher);
278 self
279 }
280
281 pub fn with_default_fetcher(mut self, fetcher: Fetcher) -> Self {
282 self.default_fetcher = Some(fetcher);
283 self
284 }
285
286 pub fn with_twitter_fetcher(mut self, fetcher: Fetcher) -> Self {
287 self.twitter_fetcher = Some(fetcher);
288 self
289 }
290}