tydle 0.1.15

YouTube video extractor written in Rust that can be used anywhere in web or native environments, based on an extremely small subset of yt-dlp.
Documentation
use std::collections::HashMap;

use anyhow::{Error, Result};
use fancy_regex::Regex;
use reqwest::Url;
use serde_json::Value;

use crate::{
    cache::{CacheAccess, PlayerCacheHandle},
    extractor::{
        api::ExtractorApiHandle,
        client::INNERTUBE_CLIENTS,
        cookies::ExtractorCookieHandle,
        extract::{InfoExtractor, YtExtractor},
        player::ExtractorPlayerHandle,
        ytcfg::ExtractorYtCfgHandle,
    },
    yt_interface::{PlayerIdentifier, VideoId, YtClient, YtEndpoint},
};

pub trait ExtractorDownloadHandle {
    async fn download_initial_data(
        &self,
        video_id: &VideoId,
        webpage_content: &String,
        webpage_client: &YtClient,
        webpage_ytcfg: &HashMap<String, Value>,
    ) -> Result<HashMap<String, Value>>;
    async fn download_player_url(&self, video_id: &VideoId) -> Result<Option<String>>;
    async fn download_webpage(
        &self,
        webpage_url: &str,
        webpage_client: &YtClient,
        video_id: &VideoId,
    ) -> Result<String>;
    async fn download_initial_webpage(
        &self,
        webpage_url: Url,
        webpage_client: &YtClient,
        video_id: &VideoId,
    ) -> Result<String>;
}

impl<P, C> ExtractorDownloadHandle for YtExtractor<P, C>
where
    P: CacheAccess<(String, String)> + PlayerCacheHandle + Send + Sync,
    C: CacheAccess,
{
    async fn download_initial_data(
        &self,
        video_id: &VideoId,
        webpage_content: &String,
        webpage_client: &YtClient,
        webpage_ytcfg: &HashMap<String, Value>,
    ) -> Result<HashMap<String, Value>> {
        let mut initial_data: Option<HashMap<String, Value>> = if !webpage_content.is_empty() {
            Some(self.extract_yt_initial_data(webpage_content)?)
        } else {
            None
        };

        if initial_data.is_none() {
            let mut query = self.generate_checkok_params();
            query.insert("videoId".into(), video_id.as_str().into());

            initial_data = Some(
                self.call_api(
                    YtEndpoint::Next,
                    query,
                    None,
                    Some(self.select_context(Some(webpage_ytcfg), Some(webpage_client))?),
                    None,
                    Some(webpage_client),
                )
                .await?,
            );
        }

        Ok(initial_data.unwrap())
    }

    async fn download_player_url(&self, video_id: &VideoId) -> Result<Option<String>> {
        let formatted_url = Url::parse("https://www.youtube.com/iframe_api")?;

        #[cfg(feature = "logging")]
        log::info!("Downloading player iFrame API {}", formatted_url);

        let iframe_webpage = self
            .download_initial_webpage(formatted_url, &YtClient::Web, video_id)
            .await?;

        let player_version_re = Regex::new(r"player\\?/([0-9a-fA-F]{8})\\?/")?;
        let player_version = player_version_re.captures(&iframe_webpage)?;

        if let Some(caps) = player_version {
            if let Some(m) = caps.get(1) {
                return Ok(Some(self.construct_player_url(
                    PlayerIdentifier::PlayerId(m.as_str().to_string()),
                )?));
            }
        }

        Ok(None)
    }

    async fn download_webpage(
        &self,
        webpage_url: &str,
        webpage_client: &YtClient,
        video_id: &VideoId,
    ) -> Result<String> {
        let formatted_url = Url::parse(webpage_url)?;
        self.download_initial_webpage(formatted_url, webpage_client, video_id)
            .await
    }

    async fn download_initial_webpage(
        &self,
        webpage_url: Url,
        webpage_client: &YtClient,
        video_id: &VideoId,
    ) -> Result<String> {
        #[cfg(feature = "logging")]
        log::info!("{}: Downloading webpage {}", video_id.as_str(), webpage_url);
        let mut webpage_request = self.http_client.get(webpage_url).query(&[
            ("bpctr", "9999999999"),
            ("has_verified", "1"),
            ("v", video_id.as_str()),
        ]);
        let innertube_client = INNERTUBE_CLIENTS.get(webpage_client).unwrap();

        let client = innertube_client.innertube_context.get("client").unwrap();
        if let Some(user_agent) = client.get("userAgent") {
            webpage_request =
                webpage_request.header("User-Agent", user_agent.as_str().unwrap_or_default());
        }

        let yt_cookies = self.get_youtube_cookies()?;

        if !yt_cookies.is_empty() {
            webpage_request = webpage_request.header("Cookie", yt_cookies.header_value());
        }

        if !self.tydle_options.source_address.is_empty() {
            webpage_request = webpage_request.header(
                "X-Forwarded-For",
                self.tydle_options.source_address.as_str(),
            );
        }

        let response = webpage_request.send().await?;

        let webpage = response.text().await.map_err(|e| Error::new(e))?;

        Ok(webpage)
    }
}