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 if is_twitter_url(url) {
182 debug!("Detected Twitter URL, using specialized handler");
183 self.twitter_generator.generate_preview(url).await
184 } else if is_github_url(url) {
185 debug!("Detected GitHub URL, using specialized handler");
186 self.generate_github_preview(url).await
187 } else {
188 debug!("Using default URL handler");
189 self.default_generator.generate_preview(url).await
190 }
191 }
192
193 pub async fn generate_github_basic_preview(&self, url: &str) -> Result<Preview, PreviewError> {
194 let (owner, repo) = Self::extract_github_info(url)
195 .ok_or_else(|| PreviewError::ExtractError("Invalid GitHub URL format".into()))?;
196
197 let basic_info = self
198 .github_generator
199 .fetcher
200 .fetch_github_basic_preview(&owner, &repo)
201 .await?;
202
203 Ok(Preview {
204 url: url.to_string(),
205 title: Some(basic_info.title),
206 description: basic_info.description,
207 image_url: basic_info.image_url,
208 site_name: Some("GitHub".to_string()),
209 favicon: Some("https://github.githubassets.com/favicons/favicon.svg".to_string()),
210 })
211 }
212
213 pub async fn get_github_detailed_info(
214 &self,
215 url: &str,
216 ) -> Result<GitHubDetailedInfo, PreviewError> {
217 let (owner, repo) = Self::extract_github_info(url)
218 .ok_or_else(|| PreviewError::ExtractError("Invalid GitHub URL format".into()))?;
219
220 self.github_generator
221 .fetcher
222 .fetch_github_detailed_info(&owner, &repo)
223 .await
224 }
225}
226
227impl PreviewService {
228 pub fn new_with_concurrency(config: PreviewServiceConfig) -> Self {
229 let semaphore = Arc::new(Semaphore::new(config.max_concurrent_requests));
230 let default_generator = Arc::new(UrlPreviewGenerator::new(config.cache_capacity));
231 let twitter_generator = Arc::new(UrlPreviewGenerator::new(config.cache_capacity));
232 let github_generator = Arc::new(UrlPreviewGenerator::new(config.cache_capacity));
233
234 PreviewService {
235 default_generator,
236 twitter_generator,
237 github_generator,
238 semaphore,
239 }
240 }
241
242 #[instrument(level = "debug", skip(self))]
243 pub async fn generate_preview_with_concurrency(
244 &self,
245 url: &str,
246 ) -> Result<Preview, PreviewError> {
247 let permit = self.semaphore.clone().acquire_owned().await;
248 let preview = self.generate_preview(url).await;
249 drop(permit);
250 preview
251 }
252}
253
254#[derive(Default, Clone)]
255pub struct PreviewServiceConfig {
256 pub cache_capacity: usize,
257 pub default_fetcher: Option<Fetcher>,
258 pub twitter_fetcher: Option<Fetcher>,
259 pub github_fetcher: Option<Fetcher>,
260 pub max_concurrent_requests: usize,
261}
262
263impl PreviewServiceConfig {
264 pub fn new(cache_capacity: usize) -> Self {
265 Self {
266 cache_capacity,
267 default_fetcher: None,
268 twitter_fetcher: None,
269 github_fetcher: None,
270 max_concurrent_requests: MAX_CONCURRENT_REQUESTS,
271 }
272 }
273
274 pub fn with_github_fetcher(mut self, fetcher: Fetcher) -> Self {
275 self.github_fetcher = Some(fetcher);
276 self
277 }
278
279 pub fn with_default_fetcher(mut self, fetcher: Fetcher) -> Self {
280 self.default_fetcher = Some(fetcher);
281 self
282 }
283
284 pub fn with_twitter_fetcher(mut self, fetcher: Fetcher) -> Self {
285 self.twitter_fetcher = Some(fetcher);
286 self
287 }
288}