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::Result;
use maplit::hashmap;
use reqwest::Url;
use serde_json::{Value, json};

use crate::{
    cache::CacheAccess,
    extractor::{
        auth::ExtractorAuthHandle, client::INNERTUBE_CLIENTS, cookies::ExtractorCookieHandle,
        extract::YtExtractor, ytcfg::ExtractorYtCfgHandle,
    },
    yt_interface::{YtClient, YtEndpoint},
};

pub trait ExtractorApiHandle {
    fn generate_api_headers(
        &self,
        ytcfg: HashMap<String, Value>,
        delegated_session_id: Option<String>,
        user_session_id: Option<String>,
        session_index: Option<i32>,
        visitor_id: Option<String>,
        default_client: Option<&YtClient>,
    ) -> Result<HashMap<&str, String>>;
    async fn call_api(
        &self,
        endpoint: YtEndpoint,
        query: HashMap<String, Value>,
        headers: Option<HashMap<&str, String>>,
        context: Option<HashMap<String, Value>>,
        api_key: Option<String>,
        default_client: Option<&YtClient>,
    ) -> Result<HashMap<String, Value>>;
}

impl<P, C> ExtractorApiHandle for YtExtractor<P, C>
where
    P: CacheAccess<(String, String)> + Send + Sync,
    C: CacheAccess,
{
    fn generate_api_headers(
        &self,
        ytcfg: HashMap<String, Value>,
        delegated_session_id: Option<String>,
        user_session_id: Option<String>,
        session_index: Option<i32>,
        visitor_id: Option<String>,
        default_client: Option<&YtClient>,
    ) -> Result<HashMap<&str, String>> {
        let client = default_client.unwrap_or(&self.tydle_options.default_client);
        let innertube_client = INNERTUBE_CLIENTS.get(client).unwrap();
        let host_name = self.select_api_hostname(Some(client));

        let origin = format!("https://{}", host_name);

        let cookie_headers = self.generate_cookie_auth_headers(
            &ytcfg,
            delegated_session_id,
            user_session_id,
            session_index,
            &origin,
        )?;

        let mut headers = hashmap! {
            "X-YouTube-Client-Name" => innertube_client.innertube_context_client_name.to_string(),
            "X-YouTube-Client-Version" => self.select_client_version(Some(client)).to_string(),
            "Origin" => origin,
        };

        if let Some(available_visitor_id) = visitor_id {
            headers.insert("X-Goog-Visitor-Id", available_visitor_id);
        } else if let Some(selected_visitor_id) = self.select_visitor_data(&[&ytcfg]) {
            headers.insert("X-Goog-Visitor-Id", selected_visitor_id);
        }

        let innertube_client_context = innertube_client.innertube_context.get("client").unwrap();

        if let Some(user_agent) = innertube_client_context.get("userAgent") {
            headers.insert("User-Agent", user_agent.as_str().unwrap_or_default().into());
        }

        headers.extend(cookie_headers);

        Ok(headers)
    }

    async fn call_api(
        &self,
        endpoint: YtEndpoint,
        query: HashMap<String, Value>,
        headers: Option<HashMap<&str, String>>,
        context: Option<HashMap<String, Value>>,
        api_key: Option<String>,
        default_client: Option<&YtClient>,
    ) -> Result<HashMap<String, Value>> {
        let client = default_client.unwrap_or(&self.tydle_options.default_client);

        let host_name = self.select_api_hostname(Some(client));
        let ep = endpoint.as_str();
        let api_url = format!("https://{}/youtubei/v1/{}", host_name, ep);
        let yt_url = Url::parse(api_url.as_str())?;

        #[cfg(feature = "logging")]
        log::info!("Requesting YouTube API at {}", api_url);

        let http_client = reqwest::Client::new();
        let mut real_headers =
            self.generate_api_headers(Default::default(), None, None, None, None, Some(client))?;
        let mut data: HashMap<String, Value> = HashMap::new();

        if let Some(ctx) = context {
            data.insert(
                "context".into(),
                json!({
                    "client": ctx
                }),
            );
        } else {
            data.insert(
                "context".into(),
                json!({
                    "client": self.select_context(None, Some(client))?,
                }),
            );
        };

        data.extend(query);

        if let Some(availabe_headers) = headers {
            real_headers.extend(availabe_headers);
        }

        let mut request_builder = http_client
            .post(yt_url)
            .json(&data)
            .query(&[("prettyPrint", "false")]);

        let yt_cookies = self.get_youtube_cookies()?;

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

        if let Some(available_api_key) = api_key {
            request_builder = request_builder.query(&[("key", available_api_key)]);
        }

        for (k, v) in real_headers {
            request_builder = request_builder.header(k, v);
        }

        request_builder = request_builder.header("Content-Type", "application/json");

        let response = request_builder.send().await?;
        Ok(response.json().await?)
    }
}