Compare commits

..

No commits in common. "289b1cdbf4cba534767445c721edfccfb94ed796" and "d7caba81d0a12918ff5141fb37d8a81c54312af3" have entirely different histories.

7 changed files with 147 additions and 204 deletions

View file

@ -1141,7 +1141,7 @@ impl RustyPipeQuery {
Ok(mapres.c) Ok(mapres.c)
} }
Err(e) => { Err(e) => {
if e.should_report() || self.opts.report { if e.should_report() {
create_report(Level::ERR, Some(e.to_string()), Vec::new()); create_report(Level::ERR, Some(e.to_string()), Vec::new());
} }
Err(e.into()) Err(e.into())

View file

@ -10,7 +10,7 @@ use url::Url;
use crate::{ use crate::{
deobfuscate::Deobfuscator, deobfuscate::Deobfuscator,
error::{internal::DeobfError, Error, ExtractionError, UnavailabilityReason}, error::{DeobfError, Error, ExtractionError},
model::{ model::{
traits::QualityOrd, AudioCodec, AudioFormat, AudioStream, AudioTrack, ChannelId, Subtitle, traits::QualityOrd, AudioCodec, AudioFormat, AudioStream, AudioTrack, ChannelId, Subtitle,
VideoCodec, VideoFormat, VideoPlayer, VideoPlayerDetails, VideoStream, VideoCodec, VideoFormat, VideoPlayer, VideoPlayerDetails, VideoStream,
@ -73,10 +73,9 @@ impl RustyPipeQuery {
match tv_res { match tv_res {
// Output desktop client error if the tv client is unsupported // Output desktop client error if the tv client is unsupported
Err(Error::Extraction(ExtractionError::VideoUnavailable { Err(Error::Extraction(ExtractionError::VideoClientUnsupported(_))) => {
reason: UnavailabilityReason::UnsupportedClient, Err(Error::Extraction(e))
.. }
})) => Err(Error::Extraction(e)),
_ => tv_res, _ => tv_res,
} }
} else { } else {
@ -144,7 +143,8 @@ impl MapResponse<VideoPlayer> for response::Player {
_lang: Language, _lang: Language,
deobf: Option<&crate::deobfuscate::DeobfData>, deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<super::MapResult<VideoPlayer>, ExtractionError> { ) -> Result<super::MapResult<VideoPlayer>, ExtractionError> {
let deobf = Deobfuscator::new(deobf.unwrap())?; let deobf = Deobfuscator::new(deobf.unwrap())
.map_err(|e| ExtractionError::InvalidData(e.to_string().into()))?;
let mut warnings = vec![]; let mut warnings = vec![];
// Check playability status // Check playability status
@ -162,54 +162,47 @@ impl MapResponse<VideoPlayer> for response::Player {
msg.push_str(&error_screen.player_error_message_renderer.subreason); msg.push_str(&error_screen.player_error_message_renderer.subreason);
} }
let reason = msg for word in msg.split_whitespace() {
.split_whitespace() match word {
.find_map(|word| match word { // reason: "This video requires payment to watch."
"payment" => Some(UnavailabilityReason::Paid), "payment" => return Err(ExtractionError::VideoUnavailable("DRM", msg)),
"Premium" => Some(UnavailabilityReason::Premium), // reason: "The uploader has not made this video available in your country."
"members-only" => Some(UnavailabilityReason::MembersOnly), "country" => return Err(ExtractionError::VideoGeoblocked),
"country" => Some(UnavailabilityReason::Geoblocked), // reason (Android): "This video can only be played on newer versions of Android or other supported devices."
"Android" | "websites" => Some(UnavailabilityReason::UnsupportedClient), // reason (TV client): "Playback on other websites has been disabled by the video owner."
_ => None, "Android" | "websites" => {
}) return Err(ExtractionError::VideoClientUnsupported(msg))
.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(' ');
} }
msg.push_str(m); }
}); return Err(ExtractionError::VideoUnavailable("being unplayable", msg));
}
response::player::PlayabilityStatus::LoginRequired { reason } => {
// reason (age restriction): "Sign in to confirm your age" // reason (age restriction): "Sign in to confirm your age"
// or: "This video may be inappropriate for some users." // or: "This video may be inappropriate for some users."
// reason (private): "This video is private" // reason (private): "This video is private"
let reason = msg if reason
.split_whitespace() .split_whitespace()
.find_map(|word| match word { .any(|word| word == "age" || word == "inappropriate")
"age" | "inappropriate" => Some(UnavailabilityReason::AgeRestricted), {
"private" => Some(UnavailabilityReason::Private), return Err(ExtractionError::VideoAgeRestricted);
_ => None, }
}) return Err(ExtractionError::VideoUnavailable("being private", reason));
.unwrap_or_default();
return Err(ExtractionError::VideoUnavailable { reason, msg });
} }
response::player::PlayabilityStatus::LiveStreamOffline { reason } => { response::player::PlayabilityStatus::LiveStreamOffline { reason } => {
return Err(ExtractionError::VideoUnavailable { return Err(ExtractionError::VideoUnavailable(
reason: UnavailabilityReason::OfflineLivestream, "offline livestream",
msg: reason, reason,
}); ))
} }
response::player::PlayabilityStatus::Error { 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 (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" // reason: "This video is unavailable"
return Err(ExtractionError::VideoUnavailable { return Err(ExtractionError::VideoUnavailable(
reason: UnavailabilityReason::Deleted, "deletion/censorship",
msg: reason, reason,
}); ));
} }
}; };

View file

@ -37,8 +37,6 @@ pub(crate) enum PlayabilityStatus {
LoginRequired { LoginRequired {
#[serde(default)] #[serde(default)]
reason: String, reason: String,
#[serde(default)]
messages: Vec<String>,
}, },
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
LiveStreamOffline { LiveStreamOffline {

View file

@ -4,10 +4,9 @@ use regex::Regex;
use reqwest::Client; use reqwest::Client;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{error::DeobfError, util};
error::{internal::DeobfError, Error},
util, type Result<T> = core::result::Result<T, DeobfError>;
};
pub struct Deobfuscator { pub struct Deobfuscator {
ctx: quick_js::Context, ctx: quick_js::Context,
@ -22,7 +21,7 @@ pub struct DeobfData {
} }
impl DeobfData { impl DeobfData {
pub async fn download(http: Client) -> Result<Self, Error> { pub async fn download(http: Client) -> Result<Self> {
let js_url = get_player_js_url(&http).await?; let js_url = get_player_js_url(&http).await?;
let player_js = get_response(&http, &js_url).await?; let player_js = get_response(&http, &js_url).await?;
@ -42,7 +41,7 @@ impl DeobfData {
} }
impl Deobfuscator { impl Deobfuscator {
pub fn new(data: &DeobfData) -> Result<Self, DeobfError> { pub fn new(data: &DeobfData) -> Result<Self> {
let ctx = let ctx =
quick_js::Context::new().or(Err(DeobfError::Other("could not create QuickJS rt")))?; quick_js::Context::new().or(Err(DeobfError::Other("could not create QuickJS rt")))?;
ctx.eval(&data.sig_fn)?; ctx.eval(&data.sig_fn)?;
@ -51,7 +50,7 @@ impl Deobfuscator {
Ok(Self { ctx }) Ok(Self { ctx })
} }
pub fn deobfuscate_sig(&self, sig: &str) -> Result<String, DeobfError> { pub fn deobfuscate_sig(&self, sig: &str) -> Result<String> {
let res = self.ctx.call_function(DEOBF_SIG_FUNC_NAME, vec![sig])?; let res = self.ctx.call_function(DEOBF_SIG_FUNC_NAME, vec![sig])?;
res.as_str().map_or( res.as_str().map_or(
@ -63,7 +62,7 @@ impl Deobfuscator {
) )
} }
pub fn deobfuscate_nsig(&self, nsig: &str) -> Result<String, DeobfError> { pub fn deobfuscate_nsig(&self, nsig: &str) -> Result<String> {
let res = self.ctx.call_function(DEOBF_NSIG_FUNC_NAME, vec![nsig])?; let res = self.ctx.call_function(DEOBF_NSIG_FUNC_NAME, vec![nsig])?;
res.as_str().map_or( res.as_str().map_or(
@ -79,7 +78,7 @@ impl Deobfuscator {
const DEOBF_SIG_FUNC_NAME: &str = "deobf_sig"; const DEOBF_SIG_FUNC_NAME: &str = "deobf_sig";
const DEOBF_NSIG_FUNC_NAME: &str = "deobf_nsig"; const DEOBF_NSIG_FUNC_NAME: &str = "deobf_nsig";
fn get_sig_fn_name(player_js: &str) -> Result<String, DeobfError> { fn get_sig_fn_name(player_js: &str) -> Result<String> {
static FUNCTION_REGEXES: Lazy<[FancyRegex; 6]> = Lazy::new(|| { 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(), 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(),
@ -99,7 +98,7 @@ fn caller_function(mapped_name: &str, fn_name: &str) -> String {
format!("var {mapped_name}={fn_name};") format!("var {mapped_name}={fn_name};")
} }
fn get_sig_fn(player_js: &str) -> Result<String, DeobfError> { fn get_sig_fn(player_js: &str) -> Result<String> {
let dfunc_name = get_sig_fn_name(player_js)?; let dfunc_name = get_sig_fn_name(player_js)?;
let function_pattern_str = let function_pattern_str =
@ -142,7 +141,7 @@ fn get_sig_fn(player_js: &str) -> Result<String, DeobfError> {
+ &caller_function(DEOBF_SIG_FUNC_NAME, &dfunc_name)) + &caller_function(DEOBF_SIG_FUNC_NAME, &dfunc_name))
} }
fn get_nsig_fn_name(player_js: &str) -> Result<String, DeobfError> { fn get_nsig_fn_name(player_js: &str) -> Result<String> {
static FUNCTION_NAME_REGEX: Lazy<Regex> = Lazy::new(|| { 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$_]\\)") Regex::new("\\.get\\(\"n\"\\)\\)&&\\([a-zA-Z0-9$_]=([a-zA-Z0-9$_]+)(?:\\[(\\d+)])?\\([a-zA-Z0-9$_]\\)")
.unwrap() .unwrap()
@ -184,7 +183,7 @@ fn get_nsig_fn_name(player_js: &str) -> Result<String, DeobfError> {
Ok(name.to_owned()) Ok(name.to_owned())
} }
fn extract_js_fn(js: &str, name: &str) -> Result<String, DeobfError> { fn extract_js_fn(js: &str, name: &str) -> Result<String> {
let scan = ress::Scanner::new(js); let scan = ress::Scanner::new(js);
let mut state = 0; let mut state = 0;
let mut level = 0; let mut level = 0;
@ -236,7 +235,7 @@ fn extract_js_fn(js: &str, name: &str) -> Result<String, DeobfError> {
Ok(js[start..end].to_owned()) Ok(js[start..end].to_owned())
} }
fn get_nsig_fn(player_js: &str) -> Result<String, DeobfError> { fn get_nsig_fn(player_js: &str) -> Result<String> {
let function_name = get_nsig_fn_name(player_js)?; let function_name = get_nsig_fn_name(player_js)?;
let function_base = function_name.to_owned() + "=function"; let function_base = function_name.to_owned() + "=function";
let offset = player_js.find(&function_base).unwrap_or_default(); let offset = player_js.find(&function_base).unwrap_or_default();
@ -245,7 +244,7 @@ fn get_nsig_fn(player_js: &str) -> Result<String, DeobfError> {
.map(|s| s + ";" + &caller_function(DEOBF_NSIG_FUNC_NAME, &function_name)) .map(|s| s + ";" + &caller_function(DEOBF_NSIG_FUNC_NAME, &function_name))
} }
async fn get_player_js_url(http: &Client) -> Result<String, Error> { async fn get_player_js_url(http: &Client) -> Result<String> {
let resp = http let resp = http
.get("https://www.youtube.com/iframe_api") .get("https://www.youtube.com/iframe_api")
.send() .send()
@ -268,12 +267,12 @@ async fn get_player_js_url(http: &Client) -> Result<String, Error> {
)) ))
} }
async fn get_response(http: &Client, url: &str) -> Result<String, Error> { async fn get_response(http: &Client, url: &str) -> Result<String> {
let resp = http.get(url).send().await?.error_for_status()?; let resp = http.get(url).send().await?.error_for_status()?;
Ok(resp.text().await?) Ok(resp.text().await?)
} }
fn get_sts(player_js: &str) -> Result<String, DeobfError> { fn get_sts(player_js: &str) -> Result<String> {
static STS_PATTERN: Lazy<Regex> = static STS_PATTERN: Lazy<Regex> =
Lazy::new(|| Regex::new("signatureTimestamp[=:](\\d+)").unwrap()); Lazy::new(|| Regex::new("signatureTimestamp[=:](\\d+)").unwrap());

View file

@ -1,17 +1,23 @@
//! RustyPipe error types //! RustyPipe error types
use std::{borrow::Cow, fmt::Display}; use std::borrow::Cow;
/// Error type for the RustyPipe library /// Custom error type for the RustyPipe library
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
#[non_exhaustive] #[non_exhaustive]
pub enum Error { pub enum Error {
/// Error extracting content from YouTube /// Error extracting content from YouTube
#[error("extraction error: {0}")] #[error("extraction error: {0}")]
Extraction(#[from] ExtractionError), 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 from the HTTP client
#[error("http error: {0}")] #[error("http error: {0}")]
Http(Cow<'static, str>), Http(#[from] reqwest::Error),
/// Erroneous HTTP status code received /// Erroneous HTTP status code received
#[error("http status code: {0} message: {1}")] #[error("http status code: {0} message: {1}")]
HttpStatus(u16, Cow<'static, str>), HttpStatus(u16, Cow<'static, str>),
@ -20,6 +26,28 @@ pub enum Error {
Other(Cow<'static, str>), 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 /// Error extracting content from YouTube
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
#[non_exhaustive] #[non_exhaustive]
@ -30,25 +58,31 @@ pub enum ExtractionError {
/// - Deletion/Censorship /// - Deletion/Censorship
/// - Private video that requires a Google account /// - Private video that requires a Google account
/// - DRM (Movies and TV shows) /// - DRM (Movies and TV shows)
#[error("Video cant be played because it is {reason}. Reason (from YT): {msg}")] #[error("Video cant be played because of {0}. Reason (from YT): {1}")]
VideoUnavailable { VideoUnavailable(&'static str, String),
/// Reason why the video could not be extracted /// Video cannot be extracted because it is age restricted.
reason: UnavailabilityReason, ///
/// The error message as returned from YouTube /// Age restriction may be circumvented with the [`crate::client::ClientType::TvHtml5Embed`] client.
msg: String, #[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),
/// Content is not available / does not exist /// Content is not available / does not exist
#[error("Content is not available. Reason: {0}")] #[error("Content is not available. Reason: {0}")]
ContentUnavailable(Cow<'static, str>), ContentUnavailable(Cow<'static, str>),
/// Bad request (Error 400 from YouTube), probably invalid input parameters /// Bad request (Error 400 from YouTube), probably invalid input parameters
#[error("Bad request. Reason: {0}")] #[error("Bad request. Reason: {0}")]
BadRequest(Cow<'static, str>), BadRequest(Cow<'static, str>),
/// YouTube returned data that could not be deserialized or parsed /// Error deserializing YouTube's response JSON
#[error("deserialization error: {0}")]
Deserialization(#[from] serde_json::Error),
/// YouTube returned invalid data
#[error("got invalid data from YT: {0}")] #[error("got invalid data from YT: {0}")]
InvalidData(Cow<'static, str>), 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 /// YouTube returned data that does not match the queried ID
/// ///
/// Specifically YouTube may return this video <https://www.youtube.com/watch?v=aQvGIIdgFDM>, /// Specifically YouTube may return this video <https://www.youtube.com/watch?v=aQvGIIdgFDM>,
@ -68,120 +102,22 @@ pub enum ExtractionError {
DeserializationWarnings, 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 { impl ExtractionError {
pub(crate) fn should_report(&self) -> bool { pub(crate) fn should_report(&self) -> bool {
matches!( matches!(
self, self,
ExtractionError::InvalidData(_) | ExtractionError::WrongResult(_) ExtractionError::Deserialization(_)
| ExtractionError::InvalidData(_)
| ExtractionError::WrongResult(_)
) )
} }
pub(crate) fn switch_client(&self) -> bool { pub(crate) fn switch_client(&self) -> bool {
matches!( matches!(
self, self,
ExtractionError::VideoUnavailable { ExtractionError::VideoClientUnsupported(_)
reason: UnavailabilityReason::AgeRestricted | ExtractionError::VideoAgeRestricted
| UnavailabilityReason::UnsupportedClient, | ExtractionError::WrongResult(_)
..
} | ExtractionError::WrongResult(_)
) )
} }
} }

View file

@ -19,14 +19,15 @@
use std::{ use std::{
collections::BTreeMap, collections::BTreeMap,
fs::File, fs::File,
io::Error,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use log::error; use log::error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use time::{macros::format_description, OffsetDateTime}; use time::macros::format_description;
use time::OffsetDateTime;
use crate::error::Error;
use crate::{deobfuscate::DeobfData, util}; use crate::{deobfuscate::DeobfData, util};
const FILENAME_FORMAT: &[time::format_description::FormatItem] = const FILENAME_FORMAT: &[time::format_description::FormatItem] =
@ -126,10 +127,10 @@ impl FileReporter {
} }
} }
fn _report(&self, report: &Report) -> Result<(), String> { fn _report(&self, report: &Report) -> Result<(), Error> {
let report_path = get_report_path(&self.path, report, "json").map_err(|e| e.to_string())?; let report_path = get_report_path(&self.path, report, "json")?;
let file = File::create(report_path).map_err(|e| e.to_string())?; serde_json::to_writer_pretty(&File::create(report_path)?, &report)
serde_json::to_writer_pretty(&file, &report).map_err(|e| e.to_string())?; .map_err(|e| Error::Other(format!("could not serialize report. err: {e}").into()))?;
Ok(()) Ok(())
} }
} }

View file

@ -10,7 +10,7 @@ use time::macros::date;
use time::OffsetDateTime; use time::OffsetDateTime;
use rustypipe::client::{ClientType, RustyPipe, RustyPipeQuery}; use rustypipe::client::{ClientType, RustyPipe, RustyPipeQuery};
use rustypipe::error::{Error, ExtractionError, UnavailabilityReason}; use rustypipe::error::{Error, ExtractionError};
use rustypipe::model::{ use rustypipe::model::{
paginator::Paginator, paginator::Paginator,
richtext::ToPlaintext, richtext::ToPlaintext,
@ -280,25 +280,41 @@ fn get_player(
} }
#[rstest] #[rstest]
#[case::not_found("86abcdefghi", UnavailabilityReason::Deleted)] #[case::not_found(
#[case::deleted("64DYi_8ESh0", UnavailabilityReason::Deleted)] "86abcdefghi",
#[case::censored("6SJNVb0GnPI", UnavailabilityReason::Deleted)] "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): "
)]
// This video is geoblocked outside of Japan, so expect this test case to fail when using a Japanese IP address. // This video is geoblocked outside of Japan, so expect this test case to fail when using a Japanese IP address.
#[case::geoblock("sJL6WA-aGkQ", UnavailabilityReason::Geoblocked)] #[case::geoblock(
#[case::drm("1bfOsni7EgI", UnavailabilityReason::Paid)] "sJL6WA-aGkQ",
#[case::private("s7_qI6_mIXc", UnavailabilityReason::Private)] "extraction error: Video is not available in your country"
#[case::age_restricted("CUO8secmc0g", UnavailabilityReason::AgeRestricted)] )]
#[case::premium_only("3LvozjEOUxU", UnavailabilityReason::Premium)] #[case::drm(
#[case::members_only("vYmAhoZYg64", UnavailabilityReason::MembersOnly)] "1bfOsni7EgI",
fn get_player_error(#[case] id: &str, #[case] expect: UnavailabilityReason, rp: RustyPipe) { "extraction error: Video cant be played because of DRM. Reason (from YT): "
let err = tokio_test::block_on(rp.query().player(id)).unwrap_err(); )]
#[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();
match err { assert!(
Error::Extraction(ExtractionError::VideoUnavailable { reason, .. }) => { err.starts_with(msg),
assert_eq!(reason, expect, "got {err}") "got error msg: `{err}`, expected: `{msg}`"
} );
_ => panic!("got {err}"),
}
} }
//#PLAYLIST //#PLAYLIST