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