Compare commits

..

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

16 changed files with 1720 additions and 230 deletions

View file

@ -6,5 +6,5 @@ pipeline:
commands: commands:
- rustup component add rustfmt clippy - rustup component add rustfmt clippy
- cargo fmt --all --check - cargo fmt --all --check
- cargo clippy --all --features=rss -- -D warnings - cargo clippy --all --all-features -- -D warnings
- cargo test --features=rss --workspace - cargo test --workspace

View file

@ -17,11 +17,8 @@ default = ["default-tls"]
rss = ["quick-xml"] rss = ["quick-xml"]
# Reqwest TLS options # Reqwest TLS
default-tls = ["reqwest/default-tls"] default-tls = ["reqwest/default-tls"]
native-tls = ["reqwest/native-tls"]
native-tls-alpn = ["reqwest/native-tls-alpn"]
native-tls-vendored = ["reqwest/native-tls-vendored"]
rustls-tls-webpki-roots = ["reqwest/rustls-tls-webpki-roots"] rustls-tls-webpki-roots = ["reqwest/rustls-tls-webpki-roots"]
rustls-tls-native-roots = ["reqwest/rustls-tls-native-roots"] rustls-tls-native-roots = ["reqwest/rustls-tls-native-roots"]

View file

@ -1,18 +1,18 @@
test: test:
cargo test --features=rss cargo test --all-features
unittest: unittest:
cargo test --features=rss --lib cargo test --all-features --lib
testyt: testyt:
cargo test --features=rss --test youtube cargo test --all-features --test youtube
testyt10: testyt10:
#!/usr/bin/env bash #!/usr/bin/env bash
set -e set -e
for i in {1..10}; do \ for i in {1..10}; do \
echo "---TEST RUN $i---"; \ echo "---TEST RUN $i---"; \
cargo test --features=rss --test youtube; \ cargo test --all-features --test youtube; \
done done
testintl: testintl:

1524
cli/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -3,40 +3,16 @@ name = "rustypipe-cli"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
[features]
default = ["rustls-tls-native-roots"]
# Reqwest TLS options
native-tls = [
"reqwest/native-tls",
"rustypipe/native-tls",
"rustypipe-downloader/native-tls",
]
native-tls-alpn = [
"reqwest/native-tls-alpn",
"rustypipe/native-tls-alpn",
"rustypipe-downloader/native-tls-alpn",
]
native-tls-vendored = [
"reqwest/native-tls-vendored",
"rustypipe/native-tls-vendored",
"rustypipe-downloader/native-tls-vendored",
]
rustls-tls-webpki-roots = [
"reqwest/rustls-tls-webpki-roots",
"rustypipe/rustls-tls-webpki-roots",
"rustypipe-downloader/rustls-tls-webpki-roots",
]
rustls-tls-native-roots = [
"reqwest/rustls-tls-native-roots",
"rustypipe/rustls-tls-native-roots",
"rustypipe-downloader/rustls-tls-native-roots",
]
[dependencies] [dependencies]
rustypipe = { path = "../", default-features = false } rustypipe = { path = "../", default-features = false, features = [
rustypipe-downloader = { path = "../downloader", default-features = false } "rustls-tls-native-roots",
reqwest = { version = "0.11.11", default_features = false } ] }
rustypipe-downloader = { path = "../downloader", default-features = false, features = [
"rustls-tls-native-roots",
] }
reqwest = { version = "0.11.11", default_features = false, features = [
"rustls-tls-native-roots",
] }
tokio = { version = "1.20.0", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.20.0", features = ["macros", "rt-multi-thread"] }
indicatif = "0.17.0" indicatif = "0.17.0"
futures = "0.3.21" futures = "0.3.21"

View file

@ -4,16 +4,8 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[features] [features]
default = ["default-tls"] # Reqwest TLS
# Reqwest TLS options
default-tls = ["reqwest/default-tls", "rustypipe/default-tls"] default-tls = ["reqwest/default-tls", "rustypipe/default-tls"]
native-tls = ["reqwest/native-tls", "rustypipe/native-tls"]
native-tls-alpn = ["reqwest/native-tls-alpn", "rustypipe/native-tls-alpn"]
native-tls-vendored = [
"reqwest/native-tls-vendored",
"rustypipe/native-tls-vendored",
]
rustls-tls-webpki-roots = [ rustls-tls-webpki-roots = [
"reqwest/rustls-tls-webpki-roots", "reqwest/rustls-tls-webpki-roots",
"rustypipe/rustls-tls-webpki-roots", "rustypipe/rustls-tls-webpki-roots",

View file

@ -164,7 +164,7 @@ impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel {
lang: Language, lang: Language,
_deobf: Option<&crate::deobfuscate::DeobfData>, _deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<Channel<Paginator<VideoItem>>>, ExtractionError> { ) -> Result<MapResult<Channel<Paginator<VideoItem>>>, ExtractionError> {
let content = map_channel_content(id, self.contents, self.alerts)?; let content = map_channel_content(self.contents, self.alerts)?;
let channel_data = map_channel( let channel_data = map_channel(
MapChannelData { MapChannelData {
@ -207,7 +207,7 @@ impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel {
lang: Language, lang: Language,
_deobf: Option<&crate::deobfuscate::DeobfData>, _deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<Channel<Paginator<PlaylistItem>>>, ExtractionError> { ) -> Result<MapResult<Channel<Paginator<PlaylistItem>>>, ExtractionError> {
let content = map_channel_content(id, self.contents, self.alerts)?; let content = map_channel_content(self.contents, self.alerts)?;
let channel_data = map_channel( let channel_data = map_channel(
MapChannelData { MapChannelData {
@ -244,7 +244,7 @@ impl MapResponse<Channel<ChannelInfo>> for response::Channel {
lang: Language, lang: Language,
_deobf: Option<&crate::deobfuscate::DeobfData>, _deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<Channel<ChannelInfo>>, ExtractionError> { ) -> Result<MapResult<Channel<ChannelInfo>>, ExtractionError> {
let content = map_channel_content(id, self.contents, self.alerts)?; let content = map_channel_content(self.contents, self.alerts)?;
let channel_data = map_channel( let channel_data = map_channel(
MapChannelData { MapChannelData {
header: self.header, header: self.header,
@ -304,21 +304,22 @@ fn map_channel(
id: &str, id: &str,
lang: Language, lang: Language,
) -> Result<MapResult<Channel<()>>, ExtractionError> { ) -> Result<MapResult<Channel<()>>, ExtractionError> {
let header = d.header.ok_or_else(|| ExtractionError::NotFound { let header = d
id: id.to_owned(), .header
msg: "no header".into(), .ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
})?; "channel not found",
)))?;
let metadata = d let metadata = d
.metadata .metadata
.ok_or_else(|| ExtractionError::NotFound { .ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
id: id.to_owned(), "channel not found",
msg: "no metadata".into(), )))?
})?
.channel_metadata_renderer; .channel_metadata_renderer;
let microformat = d.microformat.ok_or_else(|| ExtractionError::NotFound { let microformat = d
id: id.to_owned(), .microformat
msg: "no microformat".into(), .ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
})?; "channel not found",
)))?;
if metadata.external_id != id { if metadata.external_id != id {
return Err(ExtractionError::WrongResult(format!( return Err(ExtractionError::WrongResult(format!(
@ -404,7 +405,6 @@ struct MappedChannelContent {
} }
fn map_channel_content( fn map_channel_content(
id: &str,
contents: Option<response::channel::Contents>, contents: Option<response::channel::Contents>,
alerts: Option<Vec<response::Alert>>, alerts: Option<Vec<response::Alert>>,
) -> Result<MappedChannelContent, ExtractionError> { ) -> Result<MappedChannelContent, ExtractionError> {
@ -412,10 +412,9 @@ fn map_channel_content(
Some(contents) => { Some(contents) => {
let tabs = contents.two_column_browse_results_renderer.contents; let tabs = contents.two_column_browse_results_renderer.contents;
if tabs.is_empty() { if tabs.is_empty() {
return Err(ExtractionError::NotFound { return Err(ExtractionError::ContentUnavailable(
id: id.to_owned(), "channel not found".into(),
msg: "no tabs".into(), ));
});
} }
let cmp_url_suffix = |endpoint: &response::channel::ChannelTabEndpoint, let cmp_url_suffix = |endpoint: &response::channel::ChannelTabEndpoint,
@ -471,7 +470,7 @@ fn map_channel_content(
has_live, has_live,
}) })
} }
None => Err(response::alerts_to_err(id, alerts)), None => Err(response::alerts_to_err(alerts)),
} }
} }

View file

@ -16,20 +16,18 @@ impl RustyPipeQuery {
/// Fetching RSS feeds is a lot faster than querying the InnerTube API, so this method is great /// Fetching RSS feeds is a lot faster than querying the InnerTube API, so this method is great
/// for checking a lot of channels or implementing a subscription feed. /// for checking a lot of channels or implementing a subscription feed.
pub async fn channel_rss<S: AsRef<str>>(&self, channel_id: S) -> Result<ChannelRss, Error> { pub async fn channel_rss<S: AsRef<str>>(&self, channel_id: S) -> Result<ChannelRss, Error> {
let channel_id = channel_id.as_ref();
let url = format!( let url = format!(
"https://www.youtube.com/feeds/videos.xml?channel_id={}", "https://www.youtube.com/feeds/videos.xml?channel_id={}",
channel_id, channel_id.as_ref()
); );
let xml = self let xml = self
.client .client
.http_request_txt(self.client.inner.http.get(&url).build()?) .http_request_txt(self.client.inner.http.get(&url).build()?)
.await .await
.map_err(|e| match e { .map_err(|e| match e {
Error::HttpStatus(404, _) => Error::Extraction(ExtractionError::NotFound { Error::HttpStatus(404, _) => Error::Extraction(
id: channel_id.to_owned(), ExtractionError::ContentUnavailable("Channel not found".into()),
msg: "404".into(), ),
}),
_ => e, _ => e,
})?; })?;

View file

@ -23,14 +23,14 @@ mod video_details;
mod channel_rss; mod channel_rss;
use std::sync::Arc; use std::sync::Arc;
use std::{borrow::Cow, fmt::Debug, time::Duration}; use std::{borrow::Cow, fmt::Debug};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use rand::Rng; use rand::Rng;
use regex::Regex; use regex::Regex;
use reqwest::{header, Client, ClientBuilder, Request, RequestBuilder, Response, StatusCode}; use reqwest::{header, Client, ClientBuilder, Request, RequestBuilder, Response, StatusCode};
use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde::{de::DeserializeOwned, Deserialize, Serialize};
use time::OffsetDateTime; use time::{Duration, OffsetDateTime};
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::{ use crate::{
@ -241,30 +241,15 @@ struct RustyPipeOpts {
/// Builder to construct a new RustyPipe client /// Builder to construct a new RustyPipe client
pub struct RustyPipeBuilder { pub struct RustyPipeBuilder {
storage: DefaultOpt<Box<dyn CacheStorage>>, storage: Option<Box<dyn CacheStorage>>,
reporter: DefaultOpt<Box<dyn Reporter>>, no_storage: bool,
reporter: Option<Box<dyn Reporter>>,
no_reporter: bool,
n_http_retries: u32, n_http_retries: u32,
timeout: DefaultOpt<Duration>,
user_agent: Option<String>, user_agent: Option<String>,
default_opts: RustyPipeOpts, default_opts: RustyPipeOpts,
} }
enum DefaultOpt<T> {
Some(T),
None,
Default,
}
impl<T> DefaultOpt<T> {
fn or_default<F: FnOnce() -> T>(self, f: F) -> Option<T> {
match self {
DefaultOpt::Some(x) => Some(x),
DefaultOpt::None => None,
DefaultOpt::Default => Some(f()),
}
}
}
/// RustyPipe query object /// RustyPipe query object
/// ///
/// Contains a reference to the RustyPipe client as well as query-specific /// Contains a reference to the RustyPipe client as well as query-specific
@ -323,7 +308,7 @@ impl<T> CacheEntry<T> {
fn get(&self) -> Option<&T> { fn get(&self) -> Option<&T> {
match self { match self {
CacheEntry::Some { last_update, data } => { CacheEntry::Some { last_update, data } => {
if last_update < &(OffsetDateTime::now_utc() - time::Duration::hours(24)) { if last_update < &(OffsetDateTime::now_utc() - Duration::hours(24)) {
None None
} else { } else {
Some(data) Some(data)
@ -356,9 +341,10 @@ impl RustyPipeBuilder {
pub fn new() -> Self { pub fn new() -> Self {
RustyPipeBuilder { RustyPipeBuilder {
default_opts: RustyPipeOpts::default(), default_opts: RustyPipeOpts::default(),
storage: DefaultOpt::Default, storage: None,
reporter: DefaultOpt::Default, no_storage: false,
timeout: DefaultOpt::Default, reporter: None,
no_reporter: false,
n_http_retries: 2, n_http_retries: 2,
user_agent: None, user_agent: None,
} }
@ -366,19 +352,15 @@ impl RustyPipeBuilder {
/// Returns a new, configured RustyPipe instance. /// Returns a new, configured RustyPipe instance.
pub fn build(self) -> RustyPipe { pub fn build(self) -> RustyPipe {
let mut client_builder = ClientBuilder::new() let http = ClientBuilder::new()
.user_agent(self.user_agent.unwrap_or_else(|| DEFAULT_UA.to_owned())) .user_agent(self.user_agent.unwrap_or_else(|| DEFAULT_UA.to_owned()))
.gzip(true) .gzip(true)
.brotli(true) .brotli(true)
.redirect(reqwest::redirect::Policy::none()); .redirect(reqwest::redirect::Policy::none())
.build()
.unwrap();
if let Some(timeout) = self.timeout.or_default(|| Duration::from_secs(10)) { let cdata = if let Some(storage) = &self.storage {
client_builder = client_builder.timeout(timeout);
}
let http = client_builder.build().unwrap();
let cdata = if let DefaultOpt::Some(storage) = &self.storage {
if let Some(data) = storage.read() { if let Some(data) = storage.read() {
match serde_json::from_str::<CacheData>(&data) { match serde_json::from_str::<CacheData>(&data) {
Ok(data) => data, Ok(data) => data,
@ -397,8 +379,22 @@ impl RustyPipeBuilder {
RustyPipe { RustyPipe {
inner: Arc::new(RustyPipeRef { inner: Arc::new(RustyPipeRef {
http, http,
storage: self.storage.or_default(|| Box::<FileStorage>::default()), storage: if self.no_storage {
reporter: self.reporter.or_default(|| Box::<FileReporter>::default()), None
} else {
Some(
self.storage
.unwrap_or_else(|| Box::<FileStorage>::default()),
)
},
reporter: if self.no_reporter {
None
} else {
Some(
self.reporter
.unwrap_or_else(|| Box::<FileReporter>::default()),
)
},
n_http_retries: self.n_http_retries, n_http_retries: self.n_http_retries,
consent_cookie: format!( consent_cookie: format!(
"{}={}{}", "{}={}{}",
@ -422,13 +418,15 @@ impl RustyPipeBuilder {
/// ///
/// **Default value**: [`FileStorage`] in `rustypipe_cache.json` /// **Default value**: [`FileStorage`] in `rustypipe_cache.json`
pub fn storage(mut self, storage: Box<dyn CacheStorage>) -> Self { pub fn storage(mut self, storage: Box<dyn CacheStorage>) -> Self {
self.storage = DefaultOpt::Some(storage); self.storage = Some(storage);
self.no_storage = false;
self self
} }
/// Disable cache storage /// Disable cache storage
pub fn no_storage(mut self) -> Self { pub fn no_storage(mut self) -> Self {
self.storage = DefaultOpt::None; self.storage = None;
self.no_storage = true;
self self
} }
@ -436,30 +434,15 @@ impl RustyPipeBuilder {
/// ///
/// **Default value**: [`FileReporter`] creating reports in `./rustypipe_reports` /// **Default value**: [`FileReporter`] creating reports in `./rustypipe_reports`
pub fn reporter(mut self, reporter: Box<dyn Reporter>) -> Self { pub fn reporter(mut self, reporter: Box<dyn Reporter>) -> Self {
self.reporter = DefaultOpt::Some(reporter); self.reporter = Some(reporter);
self.no_reporter = false;
self self
} }
/// Disable the creation of report files in case of errors and warnings. /// Disable the creation of report files in case of errors and warnings.
pub fn no_reporter(mut self) -> Self { pub fn no_reporter(mut self) -> Self {
self.reporter = DefaultOpt::None; self.reporter = None;
self self.no_reporter = true;
}
/// Enable a HTTP request timeout
///
/// The timeout is applied from when the request starts connecting until the
/// response body has finished.
///
/// **Default value**: 10s
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = DefaultOpt::Some(timeout);
self
}
/// Disable the HTTP request timeout.
pub fn no_timeout(mut self) -> Self {
self.timeout = DefaultOpt::None;
self self
} }
@ -593,7 +576,7 @@ impl RustyPipe {
let ms = util::retry_delay(n, 1000, 60000, 3); let ms = util::retry_delay(n, 1000, 60000, 3);
log::warn!("Retry attempt #{}. Error: {}. Waiting {} ms", n, emsg, ms); log::warn!("Retry attempt #{}. Error: {}. Waiting {} ms", n, emsg, ms);
tokio::time::sleep(Duration::from_millis(ms.into())).await; tokio::time::sleep(std::time::Duration::from_millis(ms.into())).await;
last_res = Some(res); last_res = Some(res);
} }
@ -1123,20 +1106,17 @@ impl RustyPipeQuery {
if status.is_client_error() || status.is_server_error() { if status.is_client_error() || status.is_server_error() {
let error_msg = serde_json::from_str::<response::ErrorResponse>(&resp_str) let error_msg = serde_json::from_str::<response::ErrorResponse>(&resp_str)
.map(|r| Cow::from(r.error.message)); .map(|r| r.error.message)
.unwrap_or_default();
return match status { return match status {
StatusCode::NOT_FOUND => Err(Error::Extraction(ExtractionError::NotFound { StatusCode::NOT_FOUND => Err(Error::Extraction(
id: id.to_owned(), ExtractionError::ContentUnavailable(error_msg.into()),
msg: error_msg.unwrap_or("404".into()),
})),
StatusCode::BAD_REQUEST => Err(Error::Extraction(ExtractionError::BadRequest(
error_msg.unwrap_or_default(),
))),
_ => Err(Error::HttpStatus(
status.as_u16(),
error_msg.unwrap_or_default(),
)), )),
StatusCode::BAD_REQUEST => Err(Error::Extraction(ExtractionError::BadRequest(
error_msg.into(),
))),
_ => Err(Error::HttpStatus(status.as_u16(), error_msg.into())),
}; };
} }

View file

@ -193,10 +193,9 @@ impl MapResponse<TrackDetails> for response::MusicDetails {
} }
} }
let content = content.ok_or_else(|| ExtractionError::NotFound { let content = content.ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
id: id.to_owned(), "track not found",
msg: "no content".into(), )))?;
})?;
let track_item = content let track_item = content
.contents .contents
.c .c
@ -234,7 +233,7 @@ impl MapResponse<TrackDetails> for response::MusicDetails {
impl MapResponse<Paginator<TrackItem>> for response::MusicDetails { impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
fn map_response( fn map_response(
self, self,
id: &str, _id: &str,
lang: Language, lang: Language,
_deobf: Option<&crate::deobfuscate::DeobfData>, _deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<Paginator<TrackItem>>, ExtractionError> { ) -> Result<MapResult<Paginator<TrackItem>>, ExtractionError> {
@ -248,10 +247,9 @@ impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
let content = tabs let content = tabs
.into_iter() .into_iter()
.find_map(|t| t.tab_renderer.content) .find_map(|t| t.tab_renderer.content)
.ok_or_else(|| ExtractionError::NotFound { .ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
id: id.to_owned(), "radio unavailable",
msg: "no content".into(), )))?
})?
.music_queue_renderer .music_queue_renderer
.content .content
.playlist_panel_renderer; .playlist_panel_renderer;
@ -294,7 +292,7 @@ impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
impl MapResponse<Lyrics> for response::MusicLyrics { impl MapResponse<Lyrics> for response::MusicLyrics {
fn map_response( fn map_response(
self, self,
id: &str, _id: &str,
_lang: Language, _lang: Language,
_deobf: Option<&crate::deobfuscate::DeobfData>, _deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<Lyrics>, ExtractionError> { ) -> Result<MapResult<Lyrics>, ExtractionError> {
@ -307,10 +305,7 @@ impl MapResponse<Lyrics> for response::MusicLyrics {
.find_map(|item| item.music_description_shelf_renderer) .find_map(|item| item.music_description_shelf_renderer)
}) })
.ok_or(match self.contents.message_renderer { .ok_or(match self.contents.message_renderer {
Some(msg) => ExtractionError::NotFound { Some(msg) => ExtractionError::ContentUnavailable(Cow::Owned(msg.text)),
id: id.to_owned(),
msg: msg.text.into(),
},
None => ExtractionError::InvalidData(Cow::Borrowed("no content")), None => ExtractionError::InvalidData(Cow::Borrowed("no content")),
})?; })?;

View file

@ -62,7 +62,7 @@ impl MapResponse<Playlist> for response::Playlist {
) -> Result<MapResult<Playlist>, ExtractionError> { ) -> Result<MapResult<Playlist>, ExtractionError> {
let (contents, header) = match (self.contents, self.header) { let (contents, header) = match (self.contents, self.header) {
(Some(contents), Some(header)) => (contents, header), (Some(contents), Some(header)) => (contents, header),
_ => return Err(response::alerts_to_err(id, self.alerts)), _ => return Err(response::alerts_to_err(self.alerts)),
}; };
let video_items = contents let video_items = contents

View file

@ -353,18 +353,16 @@ impl From<Icon> for crate::model::Verification {
} }
} }
pub(crate) fn alerts_to_err(id: &str, alerts: Option<Vec<Alert>>) -> ExtractionError { pub(crate) fn alerts_to_err(alerts: Option<Vec<Alert>>) -> ExtractionError {
ExtractionError::NotFound { match alerts {
id: id.to_owned(), Some(alerts) => ExtractionError::ContentUnavailable(
msg: alerts
.map(|alerts| {
alerts alerts
.into_iter() .into_iter()
.map(|a| a.alert_renderer.text) .map(|a| a.alert_renderer.text)
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(" ") .join(" ")
.into() .into(),
}) ),
.unwrap_or_default(), None => ExtractionError::ContentUnavailable("content not found".into()),
} }
} }

View file

@ -163,22 +163,18 @@ impl RustyPipeQuery {
.await .await
{ {
Ok(target) => Ok(target), Ok(target) => Ok(target),
Err(e) => { Err(Error::Extraction(ExtractionError::ContentUnavailable(e))) => {
if matches!(
e,
Error::Extraction(ExtractionError::NotFound { .. })
) {
match util::VIDEO_ID_REGEX.is_match(id) { match util::VIDEO_ID_REGEX.is_match(id) {
true => Ok(UrlTarget::Video { true => Ok(UrlTarget::Video {
id: id.to_owned(), id: id.to_owned(),
start_time: get_start_time(), start_time: get_start_time(),
}), }),
false => Err(e), false => Err(Error::Extraction(
} ExtractionError::ContentUnavailable(e),
} else { )),
Err(e)
} }
} }
Err(e) => Err(e),
} }
} else if util::VIDEO_ID_REGEX.is_match(id) { } else if util::VIDEO_ID_REGEX.is_match(id) {
Ok(UrlTarget::Video { Ok(UrlTarget::Video {

View file

@ -1,3 +1,5 @@
use std::borrow::Cow;
use serde::Serialize; use serde::Serialize;
use crate::{ use crate::{
@ -85,16 +87,16 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
) -> Result<MapResult<VideoDetails>, ExtractionError> { ) -> Result<MapResult<VideoDetails>, ExtractionError> {
let mut warnings = Vec::new(); let mut warnings = Vec::new();
let contents = self.contents.ok_or_else(|| ExtractionError::NotFound { let contents = self
id: id.to_owned(), .contents
msg: "no content".into(), .ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
})?; "Video not found",
)))?;
let current_video_endpoint = let current_video_endpoint =
self.current_video_endpoint self.current_video_endpoint
.ok_or_else(|| ExtractionError::NotFound { .ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
id: id.to_owned(), "Video not found",
msg: "no current_video_endpoint".into(), )))?;
})?;
let video_id = current_video_endpoint.watch_endpoint.video_id; let video_id = current_video_endpoint.watch_endpoint.video_id;
if id != video_id { if id != video_id {
@ -108,10 +110,9 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
.results .results
.results .results
.contents .contents
.ok_or_else(|| ExtractionError::NotFound { .ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
id: id.into(), "Video not found",
msg: "no primary_results".into(), )))?;
})?;
warnings.append(&mut primary_results.warnings); warnings.append(&mut primary_results.warnings);
let mut primary_info = None; let mut primary_info = None;
@ -584,7 +585,7 @@ mod tests {
let err = details.map_response("", Language::En, None).unwrap_err(); let err = details.map_response("", Language::En, None).unwrap_err();
assert!(matches!( assert!(matches!(
err, err,
crate::error::ExtractionError::NotFound { .. } crate::error::ExtractionError::ContentUnavailable(_)
)) ))
} }

View file

@ -30,26 +30,21 @@ 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 it is {reason}. Reason (from YT): {msg}")]
VideoUnavailable { VideoUnavailable {
/// Reason why the video could not be extracted /// Reason why the video could not be extracted
reason: UnavailabilityReason, reason: UnavailabilityReason,
/// The error message as returned from YouTube /// The error message as returned from YouTube
msg: String, msg: String,
}, },
/// Content with the given ID does not exist /// Content is not available / does not exist
#[error("content `{id}` was not found ({msg})")] #[error("Content is not available. Reason: {0}")]
NotFound { ContentUnavailable(Cow<'static, str>),
/// ID of the requested content
id: String,
/// Error message
msg: 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 ({0})")] #[error("Bad request. Reason: {0}")]
BadRequest(Cow<'static, str>), BadRequest(Cow<'static, str>),
/// YouTube returned data that could not be deserialized or parsed /// YouTube returned data that could not be deserialized or parsed
#[error("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 deobfuscating YouTube's URL signatures
#[error("deobfuscation error: {0}")] #[error("deobfuscation error: {0}")]
@ -59,7 +54,7 @@ pub enum ExtractionError {
/// 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>,
/// which is a 5 minute error message, instead of the requested video when using an outdated /// which is a 5 minute error message, instead of the requested video when using an outdated
/// Android client. /// Android client.
#[error("wrong result from YT: {0}")] #[error("got wrong result from YT: {0}")]
WrongResult(String), WrongResult(String),
/// YouTube redirects you to another content ID /// YouTube redirects you to another content ID
/// ///
@ -69,7 +64,7 @@ pub enum ExtractionError {
/// Warnings occurred during deserialization/mapping /// Warnings occurred during deserialization/mapping
/// ///
/// This error is only returned in strict mode. /// This error is only returned in strict mode.
#[error("warnings during deserialization/mapping")] #[error("Warnings during deserialization/mapping")]
DeserializationWarnings, DeserializationWarnings,
} }

View file

@ -383,7 +383,10 @@ fn playlist_not_found(rp: RustyPipe) {
.unwrap_err(); .unwrap_err();
assert!( assert!(
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })), matches!(
err,
Error::Extraction(ExtractionError::ContentUnavailable(_))
),
"got: {err}" "got: {err}"
); );
} }
@ -726,7 +729,10 @@ fn get_video_details_not_found(rp: RustyPipe) {
let err = tokio_test::block_on(rp.query().video_details("abcdefgLi5X")).unwrap_err(); let err = tokio_test::block_on(rp.query().video_details("abcdefgLi5X")).unwrap_err();
assert!( assert!(
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })), matches!(
err,
Error::Extraction(ExtractionError::ContentUnavailable(_))
),
"got: {err}" "got: {err}"
) )
} }
@ -967,7 +973,10 @@ fn channel_not_found(#[case] id: &str, rp: RustyPipe) {
let err = tokio_test::block_on(rp.query().channel_videos(&id)).unwrap_err(); let err = tokio_test::block_on(rp.query().channel_videos(&id)).unwrap_err();
assert!( assert!(
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })), matches!(
err,
Error::Extraction(ExtractionError::ContentUnavailable(_))
),
"got: {err}" "got: {err}"
); );
} }
@ -1008,7 +1017,10 @@ mod channel_rss {
tokio_test::block_on(rp.query().channel_rss("UCHnyfMqiRRG1u-2MsSQLbXZ")).unwrap_err(); tokio_test::block_on(rp.query().channel_rss("UCHnyfMqiRRG1u-2MsSQLbXZ")).unwrap_err();
assert!( assert!(
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })), matches!(
err,
Error::Extraction(ExtractionError::ContentUnavailable(_))
),
"got: {}", "got: {}",
err err
); );
@ -1152,7 +1164,7 @@ fn resolve_channel_not_found(rp: RustyPipe) {
assert!(matches!( assert!(matches!(
err, err,
Error::Extraction(ExtractionError::NotFound { .. }) Error::Extraction(ExtractionError::ContentUnavailable(_))
)); ));
} }
@ -1276,7 +1288,10 @@ fn music_playlist_not_found(rp: RustyPipe) {
.unwrap_err(); .unwrap_err();
assert!( assert!(
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })), matches!(
err,
Error::Extraction(ExtractionError::ContentUnavailable(_))
),
"got: {err}" "got: {err}"
); );
} }
@ -1322,7 +1337,10 @@ fn music_album_not_found(rp: RustyPipe) {
let err = tokio_test::block_on(rp.query().music_album("MPREb_nlBWQROfvjoz")).unwrap_err(); let err = tokio_test::block_on(rp.query().music_album("MPREb_nlBWQROfvjoz")).unwrap_err();
assert!( assert!(
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })), matches!(
err,
Error::Extraction(ExtractionError::ContentUnavailable(_))
),
"got: {err}" "got: {err}"
); );
} }
@ -1412,7 +1430,10 @@ fn music_artist_not_found(rp: RustyPipe) {
.unwrap_err(); .unwrap_err();
assert!( assert!(
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })), matches!(
err,
Error::Extraction(ExtractionError::ContentUnavailable(_))
),
"got: {err}" "got: {err}"
); );
} }
@ -1836,7 +1857,10 @@ fn music_lyrics_not_found(rp: RustyPipe) {
let err = tokio_test::block_on(rp.query().music_lyrics(&track.lyrics_id.unwrap())).unwrap_err(); let err = tokio_test::block_on(rp.query().music_lyrics(&track.lyrics_id.unwrap())).unwrap_err();
assert!( assert!(
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })), matches!(
err,
Error::Extraction(ExtractionError::ContentUnavailable(_))
),
"got: {err}" "got: {err}"
); );
} }
@ -1946,7 +1970,10 @@ fn music_details_not_found(rp: RustyPipe) {
let err = tokio_test::block_on(rp.query().music_details("7nigXQS1XbZ")).unwrap_err(); let err = tokio_test::block_on(rp.query().music_details("7nigXQS1XbZ")).unwrap_err();
assert!( assert!(
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })), matches!(
err,
Error::Extraction(ExtractionError::ContentUnavailable(_))
),
"got: {err}" "got: {err}"
); );
} }
@ -1962,7 +1989,10 @@ fn music_radio_track_not_found(rp: RustyPipe) {
let err = tokio_test::block_on(rp.query().music_radio_track("7nigXQS1XbZ")).unwrap_err(); let err = tokio_test::block_on(rp.query().music_radio_track("7nigXQS1XbZ")).unwrap_err();
assert!( assert!(
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })), matches!(
err,
Error::Extraction(ExtractionError::ContentUnavailable(_))
),
"got: {err}" "got: {err}"
); );
} }
@ -1986,7 +2016,10 @@ fn music_radio_playlist_not_found(rp: RustyPipe) {
if let Err(err) = res { if let Err(err) = res {
assert!( assert!(
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })), matches!(
err,
Error::Extraction(ExtractionError::ContentUnavailable(_))
),
"got: {err}" "got: {err}"
); );
} }
@ -2005,7 +2038,10 @@ fn music_radio_not_found(rp: RustyPipe) {
tokio_test::block_on(rp.query().music_radio("RDEM_Ktu-TilkxtLvmc9wXZZZZ")).unwrap_err(); tokio_test::block_on(rp.query().music_radio("RDEM_Ktu-TilkxtLvmc9wXZZZZ")).unwrap_err();
assert!( assert!(
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })), matches!(
err,
Error::Extraction(ExtractionError::ContentUnavailable(_))
),
"got: {err}" "got: {err}"
); );
} }
@ -2159,7 +2195,10 @@ fn music_genre_not_found(rp: RustyPipe) {
let err = tokio_test::block_on(rp.query().music_genre("ggMPOg1uX1JOQWZFeDByc2zz")).unwrap_err(); let err = tokio_test::block_on(rp.query().music_genre("ggMPOg1uX1JOQWZFeDByc2zz")).unwrap_err();
assert!( assert!(
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })), matches!(
err,
Error::Extraction(ExtractionError::ContentUnavailable(_))
),
"got: {err}" "got: {err}"
); );
} }