Compare commits
	
		
			2 commits
		
	
	
		
			
				d7caba81d0
			
			...
			
				289b1cdbf4
			
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 289b1cdbf4 | |||
| 6ab7b2415a | 
					 7 changed files with 204 additions and 147 deletions
				
			
		| 
						 | 
				
			
			@ -1141,7 +1141,7 @@ impl RustyPipeQuery {
 | 
			
		|||
                    Ok(mapres.c)
 | 
			
		||||
                }
 | 
			
		||||
                Err(e) => {
 | 
			
		||||
                    if e.should_report() {
 | 
			
		||||
                    if e.should_report() || self.opts.report {
 | 
			
		||||
                        create_report(Level::ERR, Some(e.to_string()), Vec::new());
 | 
			
		||||
                    }
 | 
			
		||||
                    Err(e.into())
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,7 @@ use url::Url;
 | 
			
		|||
 | 
			
		||||
use crate::{
 | 
			
		||||
    deobfuscate::Deobfuscator,
 | 
			
		||||
    error::{DeobfError, Error, ExtractionError},
 | 
			
		||||
    error::{internal::DeobfError, Error, ExtractionError, UnavailabilityReason},
 | 
			
		||||
    model::{
 | 
			
		||||
        traits::QualityOrd, AudioCodec, AudioFormat, AudioStream, AudioTrack, ChannelId, Subtitle,
 | 
			
		||||
        VideoCodec, VideoFormat, VideoPlayer, VideoPlayerDetails, VideoStream,
 | 
			
		||||
| 
						 | 
				
			
			@ -73,9 +73,10 @@ impl RustyPipeQuery {
 | 
			
		|||
 | 
			
		||||
                    match tv_res {
 | 
			
		||||
                        // Output desktop client error if the tv client is unsupported
 | 
			
		||||
                        Err(Error::Extraction(ExtractionError::VideoClientUnsupported(_))) => {
 | 
			
		||||
                            Err(Error::Extraction(e))
 | 
			
		||||
                        }
 | 
			
		||||
                        Err(Error::Extraction(ExtractionError::VideoUnavailable {
 | 
			
		||||
                            reason: UnavailabilityReason::UnsupportedClient,
 | 
			
		||||
                            ..
 | 
			
		||||
                        })) => Err(Error::Extraction(e)),
 | 
			
		||||
                        _ => tv_res,
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
| 
						 | 
				
			
			@ -143,8 +144,7 @@ impl MapResponse<VideoPlayer> for response::Player {
 | 
			
		|||
        _lang: Language,
 | 
			
		||||
        deobf: Option<&crate::deobfuscate::DeobfData>,
 | 
			
		||||
    ) -> Result<super::MapResult<VideoPlayer>, ExtractionError> {
 | 
			
		||||
        let deobf = Deobfuscator::new(deobf.unwrap())
 | 
			
		||||
            .map_err(|e| ExtractionError::InvalidData(e.to_string().into()))?;
 | 
			
		||||
        let deobf = Deobfuscator::new(deobf.unwrap())?;
 | 
			
		||||
        let mut warnings = vec![];
 | 
			
		||||
 | 
			
		||||
        // Check playability status
 | 
			
		||||
| 
						 | 
				
			
			@ -162,47 +162,54 @@ impl MapResponse<VideoPlayer> for response::Player {
 | 
			
		|||
                    msg.push_str(&error_screen.player_error_message_renderer.subreason);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                for word in msg.split_whitespace() {
 | 
			
		||||
                    match word {
 | 
			
		||||
                        // reason: "This video requires payment to watch."
 | 
			
		||||
                        "payment" => return Err(ExtractionError::VideoUnavailable("DRM", msg)),
 | 
			
		||||
                        // reason: "The uploader has not made this video available in your country."
 | 
			
		||||
                        "country" => return Err(ExtractionError::VideoGeoblocked),
 | 
			
		||||
                        // reason (Android): "This video can only be played on newer versions of Android or other supported devices."
 | 
			
		||||
                        // reason (TV client): "Playback on other websites has been disabled by the video owner."
 | 
			
		||||
                        "Android" | "websites" => {
 | 
			
		||||
                            return Err(ExtractionError::VideoClientUnsupported(msg))
 | 
			
		||||
                let reason = msg
 | 
			
		||||
                    .split_whitespace()
 | 
			
		||||
                    .find_map(|word| match word {
 | 
			
		||||
                        "payment" => Some(UnavailabilityReason::Paid),
 | 
			
		||||
                        "Premium" => Some(UnavailabilityReason::Premium),
 | 
			
		||||
                        "members-only" => Some(UnavailabilityReason::MembersOnly),
 | 
			
		||||
                        "country" => Some(UnavailabilityReason::Geoblocked),
 | 
			
		||||
                        "Android" | "websites" => Some(UnavailabilityReason::UnsupportedClient),
 | 
			
		||||
                        _ => None,
 | 
			
		||||
                    })
 | 
			
		||||
                    .unwrap_or_default();
 | 
			
		||||
                return Err(ExtractionError::VideoUnavailable { reason, msg });
 | 
			
		||||
            }
 | 
			
		||||
                        _ => {}
 | 
			
		||||
            response::player::PlayabilityStatus::LoginRequired { reason, messages } => {
 | 
			
		||||
                let mut msg = reason;
 | 
			
		||||
                messages.iter().for_each(|m| {
 | 
			
		||||
                    if !msg.is_empty() {
 | 
			
		||||
                        msg.push(' ');
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                return Err(ExtractionError::VideoUnavailable("being unplayable", msg));
 | 
			
		||||
            }
 | 
			
		||||
            response::player::PlayabilityStatus::LoginRequired { reason } => {
 | 
			
		||||
                    msg.push_str(m);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                // reason (age restriction): "Sign in to confirm your age"
 | 
			
		||||
                // or: "This video may be inappropriate for some users."
 | 
			
		||||
                // reason (private): "This video is private"
 | 
			
		||||
                if reason
 | 
			
		||||
                let reason = msg
 | 
			
		||||
                    .split_whitespace()
 | 
			
		||||
                    .any(|word| word == "age" || word == "inappropriate")
 | 
			
		||||
                {
 | 
			
		||||
                    return Err(ExtractionError::VideoAgeRestricted);
 | 
			
		||||
                }
 | 
			
		||||
                return Err(ExtractionError::VideoUnavailable("being private", reason));
 | 
			
		||||
                    .find_map(|word| match word {
 | 
			
		||||
                        "age" | "inappropriate" => Some(UnavailabilityReason::AgeRestricted),
 | 
			
		||||
                        "private" => Some(UnavailabilityReason::Private),
 | 
			
		||||
                        _ => None,
 | 
			
		||||
                    })
 | 
			
		||||
                    .unwrap_or_default();
 | 
			
		||||
                return Err(ExtractionError::VideoUnavailable { reason, msg });
 | 
			
		||||
            }
 | 
			
		||||
            response::player::PlayabilityStatus::LiveStreamOffline { reason } => {
 | 
			
		||||
                return Err(ExtractionError::VideoUnavailable(
 | 
			
		||||
                    "offline livestream",
 | 
			
		||||
                    reason,
 | 
			
		||||
                ))
 | 
			
		||||
                return Err(ExtractionError::VideoUnavailable {
 | 
			
		||||
                    reason: UnavailabilityReason::OfflineLivestream,
 | 
			
		||||
                    msg: reason,
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            response::player::PlayabilityStatus::Error { reason } => {
 | 
			
		||||
                // reason (censored): "This video has been removed for violating YouTube's policy on hate speech. Learn more about combating hate speech in your country."
 | 
			
		||||
                // reason: "This video is unavailable"
 | 
			
		||||
                return Err(ExtractionError::VideoUnavailable(
 | 
			
		||||
                    "deletion/censorship",
 | 
			
		||||
                    reason,
 | 
			
		||||
                ));
 | 
			
		||||
                return Err(ExtractionError::VideoUnavailable {
 | 
			
		||||
                    reason: UnavailabilityReason::Deleted,
 | 
			
		||||
                    msg: reason,
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -37,6 +37,8 @@ pub(crate) enum PlayabilityStatus {
 | 
			
		|||
    LoginRequired {
 | 
			
		||||
        #[serde(default)]
 | 
			
		||||
        reason: String,
 | 
			
		||||
        #[serde(default)]
 | 
			
		||||
        messages: Vec<String>,
 | 
			
		||||
    },
 | 
			
		||||
    #[serde(rename_all = "camelCase")]
 | 
			
		||||
    LiveStreamOffline {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,9 +4,10 @@ use regex::Regex;
 | 
			
		|||
use reqwest::Client;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
use crate::{error::DeobfError, util};
 | 
			
		||||
 | 
			
		||||
type Result<T> = core::result::Result<T, DeobfError>;
 | 
			
		||||
use crate::{
 | 
			
		||||
    error::{internal::DeobfError, Error},
 | 
			
		||||
    util,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
pub struct Deobfuscator {
 | 
			
		||||
    ctx: quick_js::Context,
 | 
			
		||||
| 
						 | 
				
			
			@ -21,7 +22,7 @@ pub struct DeobfData {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
impl DeobfData {
 | 
			
		||||
    pub async fn download(http: Client) -> Result<Self> {
 | 
			
		||||
    pub async fn download(http: Client) -> Result<Self, Error> {
 | 
			
		||||
        let js_url = get_player_js_url(&http).await?;
 | 
			
		||||
        let player_js = get_response(&http, &js_url).await?;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -41,7 +42,7 @@ impl DeobfData {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
impl Deobfuscator {
 | 
			
		||||
    pub fn new(data: &DeobfData) -> Result<Self> {
 | 
			
		||||
    pub fn new(data: &DeobfData) -> Result<Self, DeobfError> {
 | 
			
		||||
        let ctx =
 | 
			
		||||
            quick_js::Context::new().or(Err(DeobfError::Other("could not create QuickJS rt")))?;
 | 
			
		||||
        ctx.eval(&data.sig_fn)?;
 | 
			
		||||
| 
						 | 
				
			
			@ -50,7 +51,7 @@ impl Deobfuscator {
 | 
			
		|||
        Ok(Self { ctx })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn deobfuscate_sig(&self, sig: &str) -> Result<String> {
 | 
			
		||||
    pub fn deobfuscate_sig(&self, sig: &str) -> Result<String, DeobfError> {
 | 
			
		||||
        let res = self.ctx.call_function(DEOBF_SIG_FUNC_NAME, vec![sig])?;
 | 
			
		||||
 | 
			
		||||
        res.as_str().map_or(
 | 
			
		||||
| 
						 | 
				
			
			@ -62,7 +63,7 @@ impl Deobfuscator {
 | 
			
		|||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn deobfuscate_nsig(&self, nsig: &str) -> Result<String> {
 | 
			
		||||
    pub fn deobfuscate_nsig(&self, nsig: &str) -> Result<String, DeobfError> {
 | 
			
		||||
        let res = self.ctx.call_function(DEOBF_NSIG_FUNC_NAME, vec![nsig])?;
 | 
			
		||||
 | 
			
		||||
        res.as_str().map_or(
 | 
			
		||||
| 
						 | 
				
			
			@ -78,7 +79,7 @@ impl Deobfuscator {
 | 
			
		|||
const DEOBF_SIG_FUNC_NAME: &str = "deobf_sig";
 | 
			
		||||
const DEOBF_NSIG_FUNC_NAME: &str = "deobf_nsig";
 | 
			
		||||
 | 
			
		||||
fn get_sig_fn_name(player_js: &str) -> Result<String> {
 | 
			
		||||
fn get_sig_fn_name(player_js: &str) -> Result<String, DeobfError> {
 | 
			
		||||
    static FUNCTION_REGEXES: Lazy<[FancyRegex; 6]> = Lazy::new(|| {
 | 
			
		||||
        [
 | 
			
		||||
        FancyRegex::new("(?:\\b|[^a-zA-Z0-9$])([a-zA-Z0-9$]{2,})\\s*=\\s*function\\(\\s*a\\s*\\)\\s*\\{\\s*a\\s*=\\s*a\\.split\\(\\s*\"\"\\s*\\)").unwrap(),
 | 
			
		||||
| 
						 | 
				
			
			@ -98,7 +99,7 @@ fn caller_function(mapped_name: &str, fn_name: &str) -> String {
 | 
			
		|||
    format!("var {mapped_name}={fn_name};")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn get_sig_fn(player_js: &str) -> Result<String> {
 | 
			
		||||
fn get_sig_fn(player_js: &str) -> Result<String, DeobfError> {
 | 
			
		||||
    let dfunc_name = get_sig_fn_name(player_js)?;
 | 
			
		||||
 | 
			
		||||
    let function_pattern_str =
 | 
			
		||||
| 
						 | 
				
			
			@ -141,7 +142,7 @@ fn get_sig_fn(player_js: &str) -> Result<String> {
 | 
			
		|||
        + &caller_function(DEOBF_SIG_FUNC_NAME, &dfunc_name))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn get_nsig_fn_name(player_js: &str) -> Result<String> {
 | 
			
		||||
fn get_nsig_fn_name(player_js: &str) -> Result<String, DeobfError> {
 | 
			
		||||
    static FUNCTION_NAME_REGEX: Lazy<Regex> = Lazy::new(|| {
 | 
			
		||||
        Regex::new("\\.get\\(\"n\"\\)\\)&&\\([a-zA-Z0-9$_]=([a-zA-Z0-9$_]+)(?:\\[(\\d+)])?\\([a-zA-Z0-9$_]\\)")
 | 
			
		||||
            .unwrap()
 | 
			
		||||
| 
						 | 
				
			
			@ -183,7 +184,7 @@ fn get_nsig_fn_name(player_js: &str) -> Result<String> {
 | 
			
		|||
    Ok(name.to_owned())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn extract_js_fn(js: &str, name: &str) -> Result<String> {
 | 
			
		||||
fn extract_js_fn(js: &str, name: &str) -> Result<String, DeobfError> {
 | 
			
		||||
    let scan = ress::Scanner::new(js);
 | 
			
		||||
    let mut state = 0;
 | 
			
		||||
    let mut level = 0;
 | 
			
		||||
| 
						 | 
				
			
			@ -235,7 +236,7 @@ fn extract_js_fn(js: &str, name: &str) -> Result<String> {
 | 
			
		|||
    Ok(js[start..end].to_owned())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn get_nsig_fn(player_js: &str) -> Result<String> {
 | 
			
		||||
fn get_nsig_fn(player_js: &str) -> Result<String, DeobfError> {
 | 
			
		||||
    let function_name = get_nsig_fn_name(player_js)?;
 | 
			
		||||
    let function_base = function_name.to_owned() + "=function";
 | 
			
		||||
    let offset = player_js.find(&function_base).unwrap_or_default();
 | 
			
		||||
| 
						 | 
				
			
			@ -244,7 +245,7 @@ fn get_nsig_fn(player_js: &str) -> Result<String> {
 | 
			
		|||
        .map(|s| s + ";" + &caller_function(DEOBF_NSIG_FUNC_NAME, &function_name))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn get_player_js_url(http: &Client) -> Result<String> {
 | 
			
		||||
async fn get_player_js_url(http: &Client) -> Result<String, Error> {
 | 
			
		||||
    let resp = http
 | 
			
		||||
        .get("https://www.youtube.com/iframe_api")
 | 
			
		||||
        .send()
 | 
			
		||||
| 
						 | 
				
			
			@ -267,12 +268,12 @@ async fn get_player_js_url(http: &Client) -> Result<String> {
 | 
			
		|||
    ))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn get_response(http: &Client, url: &str) -> Result<String> {
 | 
			
		||||
async fn get_response(http: &Client, url: &str) -> Result<String, Error> {
 | 
			
		||||
    let resp = http.get(url).send().await?.error_for_status()?;
 | 
			
		||||
    Ok(resp.text().await?)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn get_sts(player_js: &str) -> Result<String> {
 | 
			
		||||
fn get_sts(player_js: &str) -> Result<String, DeobfError> {
 | 
			
		||||
    static STS_PATTERN: Lazy<Regex> =
 | 
			
		||||
        Lazy::new(|| Regex::new("signatureTimestamp[=:](\\d+)").unwrap());
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										172
									
								
								src/error.rs
									
										
									
									
									
								
							
							
						
						
									
										172
									
								
								src/error.rs
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,23 +1,17 @@
 | 
			
		|||
//! RustyPipe error types
 | 
			
		||||
 | 
			
		||||
use std::borrow::Cow;
 | 
			
		||||
use std::{borrow::Cow, fmt::Display};
 | 
			
		||||
 | 
			
		||||
/// Custom error type for the RustyPipe library
 | 
			
		||||
/// Error type for the RustyPipe library
 | 
			
		||||
#[derive(thiserror::Error, Debug)]
 | 
			
		||||
#[non_exhaustive]
 | 
			
		||||
pub enum Error {
 | 
			
		||||
    /// Error extracting content from YouTube
 | 
			
		||||
    #[error("extraction error: {0}")]
 | 
			
		||||
    Extraction(#[from] ExtractionError),
 | 
			
		||||
    /// Error from the deobfuscater
 | 
			
		||||
    #[error("deobfuscator error: {0}")]
 | 
			
		||||
    Deobfuscation(#[from] DeobfError),
 | 
			
		||||
    /// File IO error
 | 
			
		||||
    #[error(transparent)]
 | 
			
		||||
    Io(#[from] std::io::Error),
 | 
			
		||||
    /// Error from the HTTP client
 | 
			
		||||
    #[error("http error: {0}")]
 | 
			
		||||
    Http(#[from] reqwest::Error),
 | 
			
		||||
    Http(Cow<'static, str>),
 | 
			
		||||
    /// Erroneous HTTP status code received
 | 
			
		||||
    #[error("http status code: {0} message: {1}")]
 | 
			
		||||
    HttpStatus(u16, Cow<'static, str>),
 | 
			
		||||
| 
						 | 
				
			
			@ -26,28 +20,6 @@ pub enum Error {
 | 
			
		|||
    Other(Cow<'static, str>),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Error that occurred during the initialization
 | 
			
		||||
/// or use of the YouTube URL signature deobfuscator.
 | 
			
		||||
#[derive(thiserror::Error, Debug)]
 | 
			
		||||
#[non_exhaustive]
 | 
			
		||||
pub enum DeobfError {
 | 
			
		||||
    /// Error from the HTTP client
 | 
			
		||||
    #[error("http error: {0}")]
 | 
			
		||||
    Http(#[from] reqwest::Error),
 | 
			
		||||
    /// Error during JavaScript execution
 | 
			
		||||
    #[error("js execution error: {0}")]
 | 
			
		||||
    JavaScript(#[from] quick_js::ExecutionError),
 | 
			
		||||
    /// Error during JavaScript parsing
 | 
			
		||||
    #[error("js parsing: {0}")]
 | 
			
		||||
    JsParser(#[from] ress::error::Error),
 | 
			
		||||
    /// Could not extract certain data
 | 
			
		||||
    #[error("could not extract {0}")]
 | 
			
		||||
    Extraction(&'static str),
 | 
			
		||||
    /// Unspecified error
 | 
			
		||||
    #[error("error: {0}")]
 | 
			
		||||
    Other(&'static str),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Error extracting content from YouTube
 | 
			
		||||
#[derive(thiserror::Error, Debug)]
 | 
			
		||||
#[non_exhaustive]
 | 
			
		||||
| 
						 | 
				
			
			@ -58,31 +30,25 @@ pub enum ExtractionError {
 | 
			
		|||
    /// - Deletion/Censorship
 | 
			
		||||
    /// - Private video that requires a Google account
 | 
			
		||||
    /// - DRM (Movies and TV shows)
 | 
			
		||||
    #[error("Video cant be played because of {0}. Reason (from YT): {1}")]
 | 
			
		||||
    VideoUnavailable(&'static str, String),
 | 
			
		||||
    /// Video cannot be extracted because it is age restricted.
 | 
			
		||||
    ///
 | 
			
		||||
    /// Age restriction may be circumvented with the [`crate::client::ClientType::TvHtml5Embed`] client.
 | 
			
		||||
    #[error("Video is age restricted")]
 | 
			
		||||
    VideoAgeRestricted,
 | 
			
		||||
    /// Video cannot be extracted because it is not available in your country
 | 
			
		||||
    #[error("Video is not available in your country")]
 | 
			
		||||
    VideoGeoblocked,
 | 
			
		||||
    /// Video cannot be extracted with the specified client
 | 
			
		||||
    #[error("Video cant be played with this client. Reason (from YT): {0}")]
 | 
			
		||||
    VideoClientUnsupported(String),
 | 
			
		||||
    #[error("Video cant be played because it is {reason}. Reason (from YT): {msg}")]
 | 
			
		||||
    VideoUnavailable {
 | 
			
		||||
        /// Reason why the video could not be extracted
 | 
			
		||||
        reason: UnavailabilityReason,
 | 
			
		||||
        /// The error message as returned from YouTube
 | 
			
		||||
        msg: String,
 | 
			
		||||
    },
 | 
			
		||||
    /// Content is not available / does not exist
 | 
			
		||||
    #[error("Content is not available. Reason: {0}")]
 | 
			
		||||
    ContentUnavailable(Cow<'static, str>),
 | 
			
		||||
    /// Bad request (Error 400 from YouTube), probably invalid input parameters
 | 
			
		||||
    #[error("Bad request. Reason: {0}")]
 | 
			
		||||
    BadRequest(Cow<'static, str>),
 | 
			
		||||
    /// Error deserializing YouTube's response JSON
 | 
			
		||||
    #[error("deserialization error: {0}")]
 | 
			
		||||
    Deserialization(#[from] serde_json::Error),
 | 
			
		||||
    /// YouTube returned invalid data
 | 
			
		||||
    /// YouTube returned data that could not be deserialized or parsed
 | 
			
		||||
    #[error("got invalid data from YT: {0}")]
 | 
			
		||||
    InvalidData(Cow<'static, str>),
 | 
			
		||||
    /// Error deobfuscating YouTube's URL signatures
 | 
			
		||||
    #[error("deobfuscation error: {0}")]
 | 
			
		||||
    Deobfuscation(Cow<'static, str>),
 | 
			
		||||
    /// YouTube returned data that does not match the queried ID
 | 
			
		||||
    ///
 | 
			
		||||
    /// Specifically YouTube may return this video <https://www.youtube.com/watch?v=aQvGIIdgFDM>,
 | 
			
		||||
| 
						 | 
				
			
			@ -102,22 +68,120 @@ pub enum ExtractionError {
 | 
			
		|||
    DeserializationWarnings,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Reason why a video cannot be extracted
 | 
			
		||||
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
 | 
			
		||||
#[non_exhaustive]
 | 
			
		||||
pub enum UnavailabilityReason {
 | 
			
		||||
    /// Video is age restricted.
 | 
			
		||||
    ///
 | 
			
		||||
    /// Age restriction may be circumvented with the [`crate::client::ClientType::TvHtml5Embed`] client.
 | 
			
		||||
    AgeRestricted,
 | 
			
		||||
    /// Video was deleted or censored
 | 
			
		||||
    Deleted,
 | 
			
		||||
    /// Video is not available in your country
 | 
			
		||||
    Geoblocked,
 | 
			
		||||
    /// Video cannot be extracted with the specified client
 | 
			
		||||
    UnsupportedClient,
 | 
			
		||||
    /// Video is private
 | 
			
		||||
    Private,
 | 
			
		||||
    /// Video needs to be purchased and is protected by digital restrictions management
 | 
			
		||||
    /// (e.g. movies and TV shows)
 | 
			
		||||
    Paid,
 | 
			
		||||
    /// Video is only available to YouTube Premium users
 | 
			
		||||
    Premium,
 | 
			
		||||
    /// Video is only available to channel members
 | 
			
		||||
    MembersOnly,
 | 
			
		||||
    /// Livestream has gone offline
 | 
			
		||||
    OfflineLivestream,
 | 
			
		||||
    /// Video cant be played for other reasons
 | 
			
		||||
    #[default]
 | 
			
		||||
    Unplayable,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Display for UnavailabilityReason {
 | 
			
		||||
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 | 
			
		||||
        match self {
 | 
			
		||||
            UnavailabilityReason::AgeRestricted => f.write_str("age restriction"),
 | 
			
		||||
            UnavailabilityReason::Deleted => f.write_str("deleted"),
 | 
			
		||||
            UnavailabilityReason::Geoblocked => f.write_str("geoblocking"),
 | 
			
		||||
            UnavailabilityReason::UnsupportedClient => f.write_str("unsupported by client"),
 | 
			
		||||
            UnavailabilityReason::Private => f.write_str("private"),
 | 
			
		||||
            UnavailabilityReason::Paid => f.write_str("paid"),
 | 
			
		||||
            UnavailabilityReason::Premium => f.write_str("premium-only"),
 | 
			
		||||
            UnavailabilityReason::MembersOnly => f.write_str("members-only"),
 | 
			
		||||
            UnavailabilityReason::OfflineLivestream => f.write_str("an offline stream"),
 | 
			
		||||
            UnavailabilityReason::Unplayable => f.write_str("unplayable"),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub(crate) mod internal {
 | 
			
		||||
    use super::*;
 | 
			
		||||
 | 
			
		||||
    /// Error that occurred during the initialization
 | 
			
		||||
    /// or use of the YouTube URL signature deobfuscator.
 | 
			
		||||
    #[derive(thiserror::Error, Debug)]
 | 
			
		||||
    pub enum DeobfError {
 | 
			
		||||
        /// Error during JavaScript execution
 | 
			
		||||
        #[error("js execution error: {0}")]
 | 
			
		||||
        JavaScript(#[from] quick_js::ExecutionError),
 | 
			
		||||
        /// Error during JavaScript parsing
 | 
			
		||||
        #[error("js parsing: {0}")]
 | 
			
		||||
        JsParser(#[from] ress::error::Error),
 | 
			
		||||
        /// Could not extract certain data
 | 
			
		||||
        #[error("could not extract {0}")]
 | 
			
		||||
        Extraction(&'static str),
 | 
			
		||||
        /// Unspecified error
 | 
			
		||||
        #[error("error: {0}")]
 | 
			
		||||
        Other(&'static str),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    impl From<DeobfError> for Error {
 | 
			
		||||
        fn from(value: DeobfError) -> Self {
 | 
			
		||||
            Self::Extraction(value.into())
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    impl From<DeobfError> for ExtractionError {
 | 
			
		||||
        fn from(value: DeobfError) -> Self {
 | 
			
		||||
            Self::Deobfuscation(value.to_string().into())
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<serde_json::Error> for ExtractionError {
 | 
			
		||||
    fn from(value: serde_json::Error) -> Self {
 | 
			
		||||
        Self::InvalidData(value.to_string().into())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<reqwest::Error> for Error {
 | 
			
		||||
    fn from(value: reqwest::Error) -> Self {
 | 
			
		||||
        if value.is_status() {
 | 
			
		||||
            if let Some(status) = value.status() {
 | 
			
		||||
                return Self::HttpStatus(status.as_u16(), Default::default());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Self::Http(value.to_string().into())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ExtractionError {
 | 
			
		||||
    pub(crate) fn should_report(&self) -> bool {
 | 
			
		||||
        matches!(
 | 
			
		||||
            self,
 | 
			
		||||
            ExtractionError::Deserialization(_)
 | 
			
		||||
                | ExtractionError::InvalidData(_)
 | 
			
		||||
                | ExtractionError::WrongResult(_)
 | 
			
		||||
            ExtractionError::InvalidData(_) | ExtractionError::WrongResult(_)
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn switch_client(&self) -> bool {
 | 
			
		||||
        matches!(
 | 
			
		||||
            self,
 | 
			
		||||
            ExtractionError::VideoClientUnsupported(_)
 | 
			
		||||
                | ExtractionError::VideoAgeRestricted
 | 
			
		||||
                | ExtractionError::WrongResult(_)
 | 
			
		||||
            ExtractionError::VideoUnavailable {
 | 
			
		||||
                reason: UnavailabilityReason::AgeRestricted
 | 
			
		||||
                    | UnavailabilityReason::UnsupportedClient,
 | 
			
		||||
                ..
 | 
			
		||||
            } | ExtractionError::WrongResult(_)
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,15 +19,14 @@
 | 
			
		|||
use std::{
 | 
			
		||||
    collections::BTreeMap,
 | 
			
		||||
    fs::File,
 | 
			
		||||
    io::Error,
 | 
			
		||||
    path::{Path, PathBuf},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use log::error;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use time::macros::format_description;
 | 
			
		||||
use time::OffsetDateTime;
 | 
			
		||||
use time::{macros::format_description, OffsetDateTime};
 | 
			
		||||
 | 
			
		||||
use crate::error::Error;
 | 
			
		||||
use crate::{deobfuscate::DeobfData, util};
 | 
			
		||||
 | 
			
		||||
const FILENAME_FORMAT: &[time::format_description::FormatItem] =
 | 
			
		||||
| 
						 | 
				
			
			@ -127,10 +126,10 @@ impl FileReporter {
 | 
			
		|||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn _report(&self, report: &Report) -> Result<(), Error> {
 | 
			
		||||
        let report_path = get_report_path(&self.path, report, "json")?;
 | 
			
		||||
        serde_json::to_writer_pretty(&File::create(report_path)?, &report)
 | 
			
		||||
            .map_err(|e| Error::Other(format!("could not serialize report. err: {e}").into()))?;
 | 
			
		||||
    fn _report(&self, report: &Report) -> Result<(), String> {
 | 
			
		||||
        let report_path = get_report_path(&self.path, report, "json").map_err(|e| e.to_string())?;
 | 
			
		||||
        let file = File::create(report_path).map_err(|e| e.to_string())?;
 | 
			
		||||
        serde_json::to_writer_pretty(&file, &report).map_err(|e| e.to_string())?;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,7 @@ use time::macros::date;
 | 
			
		|||
use time::OffsetDateTime;
 | 
			
		||||
 | 
			
		||||
use rustypipe::client::{ClientType, RustyPipe, RustyPipeQuery};
 | 
			
		||||
use rustypipe::error::{Error, ExtractionError};
 | 
			
		||||
use rustypipe::error::{Error, ExtractionError, UnavailabilityReason};
 | 
			
		||||
use rustypipe::model::{
 | 
			
		||||
    paginator::Paginator,
 | 
			
		||||
    richtext::ToPlaintext,
 | 
			
		||||
| 
						 | 
				
			
			@ -280,41 +280,25 @@ fn get_player(
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
#[rstest]
 | 
			
		||||
#[case::not_found(
 | 
			
		||||
    "86abcdefghi",
 | 
			
		||||
    "extraction error: Video cant be played because of deletion/censorship. Reason (from YT): "
 | 
			
		||||
)]
 | 
			
		||||
#[case::deleted(
 | 
			
		||||
    "64DYi_8ESh0",
 | 
			
		||||
    "extraction error: Video cant be played because of deletion/censorship. Reason (from YT): "
 | 
			
		||||
)]
 | 
			
		||||
#[case::censored(
 | 
			
		||||
    "6SJNVb0GnPI",
 | 
			
		||||
    "extraction error: Video cant be played because of deletion/censorship. Reason (from YT): "
 | 
			
		||||
)]
 | 
			
		||||
#[case::not_found("86abcdefghi", UnavailabilityReason::Deleted)]
 | 
			
		||||
#[case::deleted("64DYi_8ESh0", UnavailabilityReason::Deleted)]
 | 
			
		||||
#[case::censored("6SJNVb0GnPI", UnavailabilityReason::Deleted)]
 | 
			
		||||
// This video is geoblocked outside of Japan, so expect this test case to fail when using a Japanese IP address.
 | 
			
		||||
#[case::geoblock(
 | 
			
		||||
    "sJL6WA-aGkQ",
 | 
			
		||||
    "extraction error: Video is not available in your country"
 | 
			
		||||
)]
 | 
			
		||||
#[case::drm(
 | 
			
		||||
    "1bfOsni7EgI",
 | 
			
		||||
    "extraction error: Video cant be played because of DRM. Reason (from YT): "
 | 
			
		||||
)]
 | 
			
		||||
#[case::private(
 | 
			
		||||
    "s7_qI6_mIXc",
 | 
			
		||||
    "extraction error: Video cant be played because of being private. Reason (from YT): "
 | 
			
		||||
)]
 | 
			
		||||
#[case::age_restricted("CUO8secmc0g", "extraction error: Video is age restricted")]
 | 
			
		||||
fn get_player_error(#[case] id: &str, #[case] msg: &str, rp: RustyPipe) {
 | 
			
		||||
    let err = tokio_test::block_on(rp.query().player(id))
 | 
			
		||||
        .unwrap_err()
 | 
			
		||||
        .to_string();
 | 
			
		||||
#[case::geoblock("sJL6WA-aGkQ", UnavailabilityReason::Geoblocked)]
 | 
			
		||||
#[case::drm("1bfOsni7EgI", UnavailabilityReason::Paid)]
 | 
			
		||||
#[case::private("s7_qI6_mIXc", UnavailabilityReason::Private)]
 | 
			
		||||
#[case::age_restricted("CUO8secmc0g", UnavailabilityReason::AgeRestricted)]
 | 
			
		||||
#[case::premium_only("3LvozjEOUxU", UnavailabilityReason::Premium)]
 | 
			
		||||
#[case::members_only("vYmAhoZYg64", UnavailabilityReason::MembersOnly)]
 | 
			
		||||
fn get_player_error(#[case] id: &str, #[case] expect: UnavailabilityReason, rp: RustyPipe) {
 | 
			
		||||
    let err = tokio_test::block_on(rp.query().player(id)).unwrap_err();
 | 
			
		||||
 | 
			
		||||
    assert!(
 | 
			
		||||
        err.starts_with(msg),
 | 
			
		||||
        "got error msg: `{err}`, expected: `{msg}`"
 | 
			
		||||
    );
 | 
			
		||||
    match err {
 | 
			
		||||
        Error::Extraction(ExtractionError::VideoUnavailable { reason, .. }) => {
 | 
			
		||||
            assert_eq!(reason, expect, "got {err}")
 | 
			
		||||
        }
 | 
			
		||||
        _ => panic!("got {err}"),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//#PLAYLIST
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue