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::sync::Arc;

use anyhow::{Result, bail};

use crate::{
    cache::{CacheAccess, PlayerCacheHandle},
    cipher::js::SignatureJsHandle,
    utils::{parse_query_string, replace_n_sig_query_param},
};

pub enum SignatureType {
    Nsignature,
    Signature,
}

impl SignatureType {
    pub fn as_str(&self) -> &str {
        match self {
            Self::Nsignature => "n",
            Self::Signature => "sig",
        }
    }
}

pub struct SignatureDecipher<P, C>
where
    P: CacheAccess<(String, String)>,
    C: CacheAccess,
{
    pub player_cache: Arc<P>,
    pub code_cache: Arc<C>,
}

impl<P, C> SignatureDecipher<P, C>
where
    P: CacheAccess<(String, String)> + PlayerCacheHandle,
    C: CacheAccess,
{
    pub fn new(player_cache: Arc<P>, code_cache: Arc<C>) -> Self {
        Self {
            player_cache,
            code_cache,
        }
    }
}

pub trait SignatureDecipherHandle {
    async fn extract_signature_function(
        &self,
        player_url: String,
        example_sig: String,
        signature_type: SignatureType,
    ) -> Result<String>;
    async fn decrypt_signature(
        &self,
        signature_type: SignatureType,
        encrypted_signature: String,
        player_url: String,
    ) -> Result<String>;
    async fn decipher(&self, signature: String, player_url: String) -> Result<String>;
}

impl<P, C> SignatureDecipherHandle for SignatureDecipher<P, C>
where
    P: CacheAccess<(String, String)> + PlayerCacheHandle,
    C: CacheAccess,
{
    async fn extract_signature_function(
        &self,
        player_url: String,
        example_sig: String,
        signature_type: SignatureType,
    ) -> Result<String> {
        let player_js_code_key = self.player_cache.player_js_cache_key(&player_url)?;

        if let Some(code) = self.code_cache.get(&player_js_code_key)? {
            let res = self
                .parse_signature_js(code, example_sig, signature_type)
                .await?;
            return Ok(res);
        }

        bail!(
            "The player.js was not downloaded before, deciphering failed because the code was not found."
        )
    }

    async fn decrypt_signature(
        &self,
        signature_type: SignatureType,
        encrypted_signature: String,
        player_url: String,
    ) -> Result<String> {
        let cache_id = (
            format!("{}-{}", signature_type.as_str(), player_url),
            encrypted_signature.clone(),
        );

        if let Some(cached_deciphered_value) = self.player_cache.get(&cache_id)? {
            return Ok(cached_deciphered_value);
        }

        let extracted_signature = self
            .extract_signature_function(player_url, encrypted_signature, signature_type)
            .await?;
        Ok(extracted_signature)
    }

    async fn decipher(&self, signature: String, player_url: String) -> Result<String> {
        #[cfg(feature = "logging")]
        log::info!("Deciphering signature: \"{}\"", signature);
        let sc = parse_query_string(&signature).unwrap_or_default();

        let (Some(fmt_url), Some(encrypted_sig)) = (sc.get("url").cloned(), sc.get("s").cloned())
        else {
            bail!("The provided signature cannot be deciphered because it is missing `url`.")
        };

        let decrypted_signature = self
            .decrypt_signature(SignatureType::Signature, encrypted_sig, player_url.clone())
            .await?;
        let url_with_sig = format!(
            "{}&{}={}",
            fmt_url,
            sc.get("sp").map(String::as_str).unwrap_or("signature"),
            decrypted_signature,
        );

        Ok(
            match parse_query_string(&url_with_sig)
                .unwrap_or_default()
                .get("n")
            {
                Some(nsig) => replace_n_sig_query_param(
                    &url_with_sig,
                    self.decrypt_signature(SignatureType::Nsignature, nsig.clone(), player_url)
                        .await?,
                )?,
                None => url_with_sig,
            },
        )
    }
}