Skip to main content

yt_dlp/client/deps/
mod.rs

1//! The fetchers for required dependencies.
2
3use 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/// Installs required libraries.
24///
25/// # Examples
26///
27/// ```rust,no_run
28/// # use yt_dlp::client::deps::LibraryInstaller;
29/// # use std::path::PathBuf;
30/// # #[tokio::main]
31/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
32/// let destination = PathBuf::from("libs");
33/// let installer = LibraryInstaller::new(destination);
34///
35/// let youtube = installer.install_youtube(None).await.unwrap();
36/// let ffmpeg = installer.install_ffmpeg(None).await.unwrap();
37/// # Ok(())
38/// # }
39/// ```
40#[derive(Constructor, Clone, Debug)]
41pub struct LibraryInstaller {
42    /// The destination directory for the libraries.
43    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/// The installed libraries.
53///
54/// # Examples
55///
56/// ```rust,no_run
57/// # use yt_dlp::client::deps::Libraries;
58/// # use std::path::PathBuf;
59/// # #[tokio::main]
60/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
61/// let destination = PathBuf::from("libs");
62///
63/// let youtube = destination.join("yt-dlp");
64/// let ffmpeg = destination.join("ffmpeg");
65///
66/// let libraries = Libraries::new(youtube, ffmpeg);
67/// # Ok(())
68/// # }
69/// ```
70#[derive(Constructor, Clone, Debug)]
71pub struct Libraries {
72    /// The path to the installed yt-dlp binary.
73    pub youtube: PathBuf,
74    /// The path to the installed ffmpeg binary.
75    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    /// Install yt-dlp from the main repository.
91    ///
92    /// # Arguments
93    ///
94    /// * `custom_name` - Optional custom name for the executable.
95    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    /// Install yt-dlp from a custom repository, assuming releases assets are named correctly.
101    ///
102    /// # Arguments
103    ///
104    /// * `owner` - The owner of the repository.
105    /// * `repo` - The name of the repository.
106    /// * `auth_token` - Optional GitHub token to avoid rate limits.
107    /// * `custom_name` - Optional custom name for the executable.
108    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    /// Install ffmpeg from static builds.
141    ///
142    /// # Arguments
143    ///
144    /// * `custom_name` - Optional custom name for the executable.
145    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    /// Install the required dependencies.
174    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    /// Install the required dependencies with an authentication token.
187    ///
188    /// # Arguments
189    ///
190    /// * `auth_token` - The authentication token to use for downloading the dependencies.
191    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    /// Install yt-dlp.
207    pub async fn install_youtube(&self) -> Result<PathBuf> {
208        self.install_youtube_internal(None).await
209    }
210
211    /// Install yt-dlp with an authentication token.
212    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    /// Install ffmpeg.
240    pub async fn install_ffmpeg(&self) -> Result<PathBuf> {
241        self.install_ffmpeg_internal(None).await
242    }
243
244    /// Install ffmpeg with an authentication token.
245    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/// A GitHub release.
271#[derive(Debug, Deserialize)]
272pub struct Release {
273    /// The tag name of the release.
274    pub tag_name: String,
275    /// The assets of the release.
276    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/// A release asset.
286#[derive(Debug, Deserialize)]
287pub struct Asset {
288    /// The name of the asset.
289    pub name: String,
290    /// The download URL of the asset.
291    #[serde(rename = "browser_download_url")]
292    pub download_url: String,
293    /// The digest of the asset (if available via API, e.g. sha256:...).
294    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/// A release that has been selected for the current platform.
304#[derive(Debug)]
305pub struct WantedRelease {
306    /// The URL of the release asset.
307    pub url: String,
308    /// The name of the release asset.
309    pub name: String,
310    /// The expected SHA256 checksum of the asset.
311    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    /// Download the release asset to the given destination.
328    ///
329    /// # Arguments
330    ///
331    /// * `destination` - The path to write the asset to.
332    ///
333    /// # Errors
334    ///
335    /// This function will return an error if the asset could not be downloaded, written to the destination,
336    /// or if the checksum verification fails.
337    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                // Delete the invalid file
387                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}