1use std::fmt;
4use std::fs::File;
5use std::io::{BufReader, Read};
6use std::path::PathBuf;
7
8use derive_more::Constructor;
9use serde::Deserialize;
10use sha2::{Digest, Sha256};
11
12use crate::client::deps::ffmpeg::BuildFetcher;
13use crate::client::deps::ytdlp::YoutubeFetcher;
14use crate::download::Fetcher;
15use crate::error::Result;
16use crate::utils::fs;
17use crate::{ternary, utils};
18
19pub mod ffmpeg;
20pub mod github;
21pub mod ytdlp;
22
23#[derive(Constructor, Clone, Debug)]
41pub struct LibraryInstaller {
42 pub destination: PathBuf,
44}
45
46impl fmt::Display for LibraryInstaller {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 write!(f, "LibraryInstaller(destination={})", self.destination.display())
49 }
50}
51
52#[derive(Constructor, Clone, Debug)]
71pub struct Libraries {
72 pub youtube: PathBuf,
74 pub ffmpeg: PathBuf,
76}
77
78impl fmt::Display for Libraries {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 write!(
81 f,
82 "Libraries(youtube={}, ffmpeg={})",
83 self.youtube.display(),
84 self.ffmpeg.display()
85 )
86 }
87}
88
89impl LibraryInstaller {
90 pub async fn install_youtube(&self, custom_name: Option<String>) -> Result<PathBuf> {
96 self.install_youtube_from_repo("yt-dlp", "yt-dlp", None, custom_name)
97 .await
98 }
99
100 pub async fn install_youtube_from_repo(
109 &self,
110 owner: impl Into<String>,
111 repo: impl Into<String>,
112 auth_token: Option<String>,
113 custom_name: Option<String>,
114 ) -> Result<PathBuf> {
115 let owner: String = owner.into();
116 let repo: String = repo.into();
117
118 tracing::debug!(
119 owner = %owner,
120 repo = %repo,
121 custom_name = ?custom_name,
122 destination = ?self.destination,
123 "📦 Installing yt-dlp from repository"
124 );
125
126 fs::create_dir(self.destination.clone()).await?;
127
128 let fetcher = YoutubeFetcher::new(owner, repo);
129
130 let name = custom_name.unwrap_or(String::from("yt-dlp"));
131 let path = self.destination.join(utils::find_executable(&name));
132
133 let release = fetcher.fetch_release(auth_token).await?;
134 release.download(path.clone()).await?;
135 fs::set_executable(path.clone()).await?;
136
137 Ok(path)
138 }
139
140 pub async fn install_ffmpeg(&self, custom_name: Option<String>) -> Result<PathBuf> {
146 tracing::debug!(
147 custom_name = ?custom_name,
148 destination = ?self.destination,
149 "📦 Installing ffmpeg from static builds"
150 );
151
152 fs::create_dir(self.destination.clone()).await?;
153
154 let fetcher = BuildFetcher::new();
155 let archive = self.destination.join("ffmpeg-release.zip");
156
157 let release = fetcher.fetch_binary().await?;
158 release.download(archive.clone()).await?;
159 let path = fetcher.extract_binary(archive).await?;
160
161 if let Some(name) = custom_name {
162 let new_path = self.destination.join(utils::find_executable(&name));
163 tokio::fs::rename(&path, &new_path).await?;
164
165 return Ok(new_path);
166 }
167
168 Ok(path)
169 }
170}
171
172impl Libraries {
173 pub async fn install_dependencies(&self) -> Result<Self> {
175 tracing::info!(
176 youtube_path = ?self.youtube,
177 ffmpeg_path = ?self.ffmpeg,
178 "📦 Installing required dependencies"
179 );
180
181 let (youtube, ffmpeg) = tokio::join!(self.install_youtube(), self.install_ffmpeg());
182
183 Ok(Self::new(youtube?, ffmpeg?))
184 }
185
186 pub async fn install_dependencies_with_token(&self, auth_token: impl Into<String>) -> Result<Self> {
192 tracing::info!(
193 youtube_path = ?self.youtube,
194 ffmpeg_path = ?self.ffmpeg,
195 has_token = true,
196 "📦 Installing required dependencies with authentication token"
197 );
198
199 let token = auth_token.into();
200 let youtube = self.install_youtube_with_token(token.clone()).await?;
201 let ffmpeg = self.install_ffmpeg_with_token(token).await?;
202
203 Ok(Self::new(youtube, ffmpeg))
204 }
205
206 pub async fn install_youtube(&self) -> Result<PathBuf> {
208 self.install_youtube_internal(None).await
209 }
210
211 pub async fn install_youtube_with_token(&self, auth_token: impl Into<String>) -> Result<PathBuf> {
213 self.install_youtube_internal(Some(auth_token.into())).await
214 }
215
216 async fn install_youtube_internal(&self, auth_token: Option<String>) -> Result<PathBuf> {
217 tracing::debug!(
218 youtube_path = ?self.youtube,
219 has_token = auth_token.is_some(),
220 "📦 Installing yt-dlp binary"
221 );
222
223 let parent = fs::try_parent(self.youtube.clone())?;
224 let installer = LibraryInstaller::new(parent);
225
226 if self.youtube.exists() {
227 return Ok(self.youtube.clone());
228 }
229
230 let name = utils::find_executable("yt-dlp");
231 let file_name = fs::try_name(self.youtube.clone())?;
232
233 let custom_name = ternary!(file_name == name, None, Some(file_name));
234 installer
235 .install_youtube_from_repo("yt-dlp", "yt-dlp", auth_token, custom_name)
236 .await
237 }
238
239 pub async fn install_ffmpeg(&self) -> Result<PathBuf> {
241 self.install_ffmpeg_internal(None).await
242 }
243
244 pub async fn install_ffmpeg_with_token(&self, auth_token: impl Into<String>) -> Result<PathBuf> {
246 self.install_ffmpeg_internal(Some(auth_token.into())).await
247 }
248
249 async fn install_ffmpeg_internal(&self, _auth_token: Option<String>) -> Result<PathBuf> {
250 tracing::debug!(
251 ffmpeg_path = ?self.ffmpeg,
252 "📦 Installing ffmpeg binary"
253 );
254
255 let parent = fs::try_parent(self.ffmpeg.clone())?;
256 let installer = LibraryInstaller::new(parent);
257
258 if self.ffmpeg.exists() {
259 return Ok(self.ffmpeg.clone());
260 }
261
262 let name = utils::find_executable("ffmpeg");
263 let file_name = fs::try_name(self.ffmpeg.clone())?;
264
265 let custom_name = ternary!(file_name == name, None, Some(file_name));
266 installer.install_ffmpeg(custom_name).await
267 }
268}
269
270#[derive(Debug, Deserialize)]
272pub struct Release {
273 pub tag_name: String,
275 pub assets: Vec<Asset>,
277}
278
279impl fmt::Display for Release {
280 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281 write!(f, "Release(tag={}, assets={})", self.tag_name, self.assets.len())
282 }
283}
284
285#[derive(Debug, Deserialize)]
287pub struct Asset {
288 pub name: String,
290 #[serde(rename = "browser_download_url")]
292 pub download_url: String,
293 pub digest: Option<String>,
295}
296
297impl fmt::Display for Asset {
298 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299 write!(f, "Asset(name={}, url={})", self.name, self.download_url)
300 }
301}
302
303#[derive(Debug)]
305pub struct WantedRelease {
306 pub url: String,
308 pub name: String,
310 pub checksum: Option<String>,
312}
313
314impl fmt::Display for WantedRelease {
315 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
316 write!(
317 f,
318 "WantedRelease(asset={}, url={}, checksum={})",
319 self.name,
320 self.url,
321 self.checksum.as_deref().unwrap_or("none")
322 )
323 }
324}
325
326impl WantedRelease {
327 pub async fn download(&self, destination: impl Into<PathBuf>) -> Result<()> {
338 let destination: PathBuf = destination.into();
339 tracing::debug!(
340 url = %self.url,
341 destination = ?destination,
342 asset_name = %self.name,
343 has_checksum = self.checksum.is_some(),
344 "📦 Downloading release asset"
345 );
346
347 let fetcher = Fetcher::new(&self.url, None, None)?;
348 fetcher.fetch_asset(destination.clone()).await?;
349
350 if let Some(expected_checksum) = &self.checksum {
351 tracing::debug!(
352 destination = ?destination,
353 expected_checksum = %expected_checksum,
354 "⚙️ Verifying asset checksum"
355 );
356
357 let dest_path = destination.clone();
358 let actual_checksum = tokio::task::spawn_blocking(move || {
359 let file = File::open(&dest_path)
360 .map_err(|e| crate::error::Error::io_with_path("open file for checksum", dest_path.clone(), e))?;
361 let mut reader = BufReader::new(file);
362 let mut hasher = Sha256::new();
363 let mut buffer = [0; 8192];
364
365 loop {
366 let count = reader.read(&mut buffer).map_err(|e| {
367 crate::error::Error::io_with_path("read file for checksum", dest_path.clone(), e)
368 })?;
369 if count == 0 {
370 break;
371 }
372 hasher.update(&buffer[..count]);
373 }
374
375 let result = hasher.finalize();
376 Ok::<_, crate::error::Error>(result.iter().fold(String::new(), |mut acc, b| {
377 use std::fmt::Write;
378 let _ = write!(acc, "{:02x}", b);
379 acc
380 }))
381 })
382 .await
383 .map_err(|e| crate::error::Error::runtime("checksum computation", e))??;
384
385 if actual_checksum != *expected_checksum {
386 let _ = tokio::fs::remove_file(&destination).await;
388 return Err(crate::error::Error::ChecksumMismatch {
389 path: destination.clone(),
390 expected: expected_checksum.to_string(),
391 actual: actual_checksum.clone(),
392 });
393 }
394
395 tracing::debug!(
396 expected = %expected_checksum,
397 actual = %actual_checksum,
398 "✅ Checksum verification passed"
399 );
400 }
401
402 Ok(())
403 }
404}