Compare commits
3 commits
289b1cdbf4
...
c15d46e0c4
Author | SHA1 | Date | |
---|---|---|---|
c15d46e0c4 | |||
a51e42f563 | |||
c021496a55 |
16 changed files with 229 additions and 1719 deletions
|
@ -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 --all-features -- -D warnings
|
- cargo clippy --all --features=rss -- -D warnings
|
||||||
- cargo test --workspace
|
- cargo test --features=rss --workspace
|
||||||
|
|
|
@ -17,8 +17,11 @@ default = ["default-tls"]
|
||||||
|
|
||||||
rss = ["quick-xml"]
|
rss = ["quick-xml"]
|
||||||
|
|
||||||
# Reqwest TLS
|
# Reqwest TLS options
|
||||||
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"]
|
||||||
|
|
||||||
|
|
8
Justfile
8
Justfile
|
@ -1,18 +1,18 @@
|
||||||
test:
|
test:
|
||||||
cargo test --all-features
|
cargo test --features=rss
|
||||||
|
|
||||||
unittest:
|
unittest:
|
||||||
cargo test --all-features --lib
|
cargo test --features=rss --lib
|
||||||
|
|
||||||
testyt:
|
testyt:
|
||||||
cargo test --all-features --test youtube
|
cargo test --features=rss --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 --all-features --test youtube; \
|
cargo test --features=rss --test youtube; \
|
||||||
done
|
done
|
||||||
|
|
||||||
testintl:
|
testintl:
|
||||||
|
|
1524
cli/Cargo.lock
generated
1524
cli/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -3,16 +3,40 @@ 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, features = [
|
rustypipe = { path = "../", default-features = false }
|
||||||
"rustls-tls-native-roots",
|
rustypipe-downloader = { path = "../downloader", default-features = false }
|
||||||
] }
|
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"
|
||||||
|
|
|
@ -4,8 +4,16 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# Reqwest TLS
|
default = ["default-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",
|
||||||
|
|
|
@ -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(self.contents, self.alerts)?;
|
let content = map_channel_content(id, 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(self.contents, self.alerts)?;
|
let content = map_channel_content(id, 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(self.contents, self.alerts)?;
|
let content = map_channel_content(id, self.contents, self.alerts)?;
|
||||||
let channel_data = map_channel(
|
let channel_data = map_channel(
|
||||||
MapChannelData {
|
MapChannelData {
|
||||||
header: self.header,
|
header: self.header,
|
||||||
|
@ -304,22 +304,21 @@ fn map_channel(
|
||||||
id: &str,
|
id: &str,
|
||||||
lang: Language,
|
lang: Language,
|
||||||
) -> Result<MapResult<Channel<()>>, ExtractionError> {
|
) -> Result<MapResult<Channel<()>>, ExtractionError> {
|
||||||
let header = d
|
let header = d.header.ok_or_else(|| ExtractionError::NotFound {
|
||||||
.header
|
id: id.to_owned(),
|
||||||
.ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
|
msg: "no header".into(),
|
||||||
"channel not found",
|
})?;
|
||||||
)))?;
|
|
||||||
let metadata = d
|
let metadata = d
|
||||||
.metadata
|
.metadata
|
||||||
.ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
|
.ok_or_else(|| ExtractionError::NotFound {
|
||||||
"channel not found",
|
id: id.to_owned(),
|
||||||
)))?
|
msg: "no metadata".into(),
|
||||||
|
})?
|
||||||
.channel_metadata_renderer;
|
.channel_metadata_renderer;
|
||||||
let microformat = d
|
let microformat = d.microformat.ok_or_else(|| ExtractionError::NotFound {
|
||||||
.microformat
|
id: id.to_owned(),
|
||||||
.ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
|
msg: "no microformat".into(),
|
||||||
"channel not found",
|
})?;
|
||||||
)))?;
|
|
||||||
|
|
||||||
if metadata.external_id != id {
|
if metadata.external_id != id {
|
||||||
return Err(ExtractionError::WrongResult(format!(
|
return Err(ExtractionError::WrongResult(format!(
|
||||||
|
@ -405,6 +404,7 @@ 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,9 +412,10 @@ 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::ContentUnavailable(
|
return Err(ExtractionError::NotFound {
|
||||||
"channel not found".into(),
|
id: id.to_owned(),
|
||||||
));
|
msg: "no tabs".into(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let cmp_url_suffix = |endpoint: &response::channel::ChannelTabEndpoint,
|
let cmp_url_suffix = |endpoint: &response::channel::ChannelTabEndpoint,
|
||||||
|
@ -470,7 +471,7 @@ fn map_channel_content(
|
||||||
has_live,
|
has_live,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
None => Err(response::alerts_to_err(alerts)),
|
None => Err(response::alerts_to_err(id, alerts)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,18 +16,20 @@ 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.as_ref()
|
channel_id,
|
||||||
);
|
);
|
||||||
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(
|
Error::HttpStatus(404, _) => Error::Extraction(ExtractionError::NotFound {
|
||||||
ExtractionError::ContentUnavailable("Channel not found".into()),
|
id: channel_id.to_owned(),
|
||||||
),
|
msg: "404".into(),
|
||||||
|
}),
|
||||||
_ => e,
|
_ => e,
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|
|
@ -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};
|
use std::{borrow::Cow, fmt::Debug, time::Duration};
|
||||||
|
|
||||||
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::{Duration, OffsetDateTime};
|
use time::OffsetDateTime;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -241,15 +241,30 @@ struct RustyPipeOpts {
|
||||||
|
|
||||||
/// Builder to construct a new RustyPipe client
|
/// Builder to construct a new RustyPipe client
|
||||||
pub struct RustyPipeBuilder {
|
pub struct RustyPipeBuilder {
|
||||||
storage: Option<Box<dyn CacheStorage>>,
|
storage: DefaultOpt<Box<dyn CacheStorage>>,
|
||||||
no_storage: bool,
|
reporter: DefaultOpt<Box<dyn Reporter>>,
|
||||||
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
|
||||||
|
@ -308,7 +323,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() - Duration::hours(24)) {
|
if last_update < &(OffsetDateTime::now_utc() - time::Duration::hours(24)) {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(data)
|
Some(data)
|
||||||
|
@ -341,10 +356,9 @@ impl RustyPipeBuilder {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
RustyPipeBuilder {
|
RustyPipeBuilder {
|
||||||
default_opts: RustyPipeOpts::default(),
|
default_opts: RustyPipeOpts::default(),
|
||||||
storage: None,
|
storage: DefaultOpt::Default,
|
||||||
no_storage: false,
|
reporter: DefaultOpt::Default,
|
||||||
reporter: None,
|
timeout: DefaultOpt::Default,
|
||||||
no_reporter: false,
|
|
||||||
n_http_retries: 2,
|
n_http_retries: 2,
|
||||||
user_agent: None,
|
user_agent: None,
|
||||||
}
|
}
|
||||||
|
@ -352,15 +366,19 @@ 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 http = ClientBuilder::new()
|
let mut client_builder = 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();
|
|
||||||
|
|
||||||
let cdata = if let Some(storage) = &self.storage {
|
if let Some(timeout) = self.timeout.or_default(|| Duration::from_secs(10)) {
|
||||||
|
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,
|
||||||
|
@ -379,22 +397,8 @@ impl RustyPipeBuilder {
|
||||||
RustyPipe {
|
RustyPipe {
|
||||||
inner: Arc::new(RustyPipeRef {
|
inner: Arc::new(RustyPipeRef {
|
||||||
http,
|
http,
|
||||||
storage: if self.no_storage {
|
storage: self.storage.or_default(|| Box::<FileStorage>::default()),
|
||||||
None
|
reporter: self.reporter.or_default(|| Box::<FileReporter>::default()),
|
||||||
} 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!(
|
||||||
"{}={}{}",
|
"{}={}{}",
|
||||||
|
@ -418,15 +422,13 @@ 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 = Some(storage);
|
self.storage = DefaultOpt::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 = None;
|
self.storage = DefaultOpt::None;
|
||||||
self.no_storage = true;
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -434,15 +436,30 @@ 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 = Some(reporter);
|
self.reporter = DefaultOpt::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 = None;
|
self.reporter = DefaultOpt::None;
|
||||||
self.no_reporter = true;
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -576,7 +593,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(std::time::Duration::from_millis(ms.into())).await;
|
tokio::time::sleep(Duration::from_millis(ms.into())).await;
|
||||||
|
|
||||||
last_res = Some(res);
|
last_res = Some(res);
|
||||||
}
|
}
|
||||||
|
@ -1106,17 +1123,20 @@ 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| r.error.message)
|
.map(|r| Cow::from(r.error.message));
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
return match status {
|
return match status {
|
||||||
StatusCode::NOT_FOUND => Err(Error::Extraction(
|
StatusCode::NOT_FOUND => Err(Error::Extraction(ExtractionError::NotFound {
|
||||||
ExtractionError::ContentUnavailable(error_msg.into()),
|
id: id.to_owned(),
|
||||||
)),
|
msg: error_msg.unwrap_or("404".into()),
|
||||||
|
})),
|
||||||
StatusCode::BAD_REQUEST => Err(Error::Extraction(ExtractionError::BadRequest(
|
StatusCode::BAD_REQUEST => Err(Error::Extraction(ExtractionError::BadRequest(
|
||||||
error_msg.into(),
|
error_msg.unwrap_or_default(),
|
||||||
))),
|
))),
|
||||||
_ => Err(Error::HttpStatus(status.as_u16(), error_msg.into())),
|
_ => Err(Error::HttpStatus(
|
||||||
|
status.as_u16(),
|
||||||
|
error_msg.unwrap_or_default(),
|
||||||
|
)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -193,9 +193,10 @@ impl MapResponse<TrackDetails> for response::MusicDetails {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = content.ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
|
let content = content.ok_or_else(|| ExtractionError::NotFound {
|
||||||
"track not found",
|
id: id.to_owned(),
|
||||||
)))?;
|
msg: "no content".into(),
|
||||||
|
})?;
|
||||||
let track_item = content
|
let track_item = content
|
||||||
.contents
|
.contents
|
||||||
.c
|
.c
|
||||||
|
@ -233,7 +234,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> {
|
||||||
|
@ -247,9 +248,10 @@ 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(ExtractionError::ContentUnavailable(Cow::Borrowed(
|
.ok_or_else(|| ExtractionError::NotFound {
|
||||||
"radio unavailable",
|
id: id.to_owned(),
|
||||||
)))?
|
msg: "no content".into(),
|
||||||
|
})?
|
||||||
.music_queue_renderer
|
.music_queue_renderer
|
||||||
.content
|
.content
|
||||||
.playlist_panel_renderer;
|
.playlist_panel_renderer;
|
||||||
|
@ -292,7 +294,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> {
|
||||||
|
@ -305,7 +307,10 @@ 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::ContentUnavailable(Cow::Owned(msg.text)),
|
Some(msg) => ExtractionError::NotFound {
|
||||||
|
id: id.to_owned(),
|
||||||
|
msg: msg.text.into(),
|
||||||
|
},
|
||||||
None => ExtractionError::InvalidData(Cow::Borrowed("no content")),
|
None => ExtractionError::InvalidData(Cow::Borrowed("no content")),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|
|
@ -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(self.alerts)),
|
_ => return Err(response::alerts_to_err(id, self.alerts)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let video_items = contents
|
let video_items = contents
|
||||||
|
|
|
@ -353,16 +353,18 @@ impl From<Icon> for crate::model::Verification {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn alerts_to_err(alerts: Option<Vec<Alert>>) -> ExtractionError {
|
pub(crate) fn alerts_to_err(id: &str, alerts: Option<Vec<Alert>>) -> ExtractionError {
|
||||||
match alerts {
|
ExtractionError::NotFound {
|
||||||
Some(alerts) => ExtractionError::ContentUnavailable(
|
id: id.to_owned(),
|
||||||
alerts
|
msg: alerts
|
||||||
.into_iter()
|
.map(|alerts| {
|
||||||
.map(|a| a.alert_renderer.text)
|
alerts
|
||||||
.collect::<Vec<_>>()
|
.into_iter()
|
||||||
.join(" ")
|
.map(|a| a.alert_renderer.text)
|
||||||
.into(),
|
.collect::<Vec<_>>()
|
||||||
),
|
.join(" ")
|
||||||
None => ExtractionError::ContentUnavailable("content not found".into()),
|
.into()
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -163,18 +163,22 @@ impl RustyPipeQuery {
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(target) => Ok(target),
|
Ok(target) => Ok(target),
|
||||||
Err(Error::Extraction(ExtractionError::ContentUnavailable(e))) => {
|
Err(e) => {
|
||||||
match util::VIDEO_ID_REGEX.is_match(id) {
|
if matches!(
|
||||||
true => Ok(UrlTarget::Video {
|
e,
|
||||||
id: id.to_owned(),
|
Error::Extraction(ExtractionError::NotFound { .. })
|
||||||
start_time: get_start_time(),
|
) {
|
||||||
}),
|
match util::VIDEO_ID_REGEX.is_match(id) {
|
||||||
false => Err(Error::Extraction(
|
true => Ok(UrlTarget::Video {
|
||||||
ExtractionError::ContentUnavailable(e),
|
id: id.to_owned(),
|
||||||
)),
|
start_time: get_start_time(),
|
||||||
|
}),
|
||||||
|
false => Err(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 {
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -87,16 +85,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
|
let contents = self.contents.ok_or_else(|| ExtractionError::NotFound {
|
||||||
.contents
|
id: id.to_owned(),
|
||||||
.ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
|
msg: "no content".into(),
|
||||||
"Video not found",
|
})?;
|
||||||
)))?;
|
|
||||||
let current_video_endpoint =
|
let current_video_endpoint =
|
||||||
self.current_video_endpoint
|
self.current_video_endpoint
|
||||||
.ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
|
.ok_or_else(|| ExtractionError::NotFound {
|
||||||
"Video not found",
|
id: id.to_owned(),
|
||||||
)))?;
|
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 {
|
||||||
|
@ -110,9 +108,10 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
||||||
.results
|
.results
|
||||||
.results
|
.results
|
||||||
.contents
|
.contents
|
||||||
.ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
|
.ok_or_else(|| ExtractionError::NotFound {
|
||||||
"Video not found",
|
id: id.into(),
|
||||||
)))?;
|
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;
|
||||||
|
@ -585,7 +584,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::ContentUnavailable(_)
|
crate::error::ExtractionError::NotFound { .. }
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
21
src/error.rs
21
src/error.rs
|
@ -30,21 +30,26 @@ 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 is not available / does not exist
|
/// Content with the given ID does not exist
|
||||||
#[error("Content is not available. Reason: {0}")]
|
#[error("content `{id}` was not found ({msg})")]
|
||||||
ContentUnavailable(Cow<'static, str>),
|
NotFound {
|
||||||
|
/// 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. Reason: {0}")]
|
#[error("bad request ({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("got invalid data from YT: {0}")]
|
#[error("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}")]
|
||||||
|
@ -54,7 +59,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("got wrong result from YT: {0}")]
|
#[error("wrong result from YT: {0}")]
|
||||||
WrongResult(String),
|
WrongResult(String),
|
||||||
/// YouTube redirects you to another content ID
|
/// YouTube redirects you to another content ID
|
||||||
///
|
///
|
||||||
|
@ -64,7 +69,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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -383,10 +383,7 @@ fn playlist_not_found(rp: RustyPipe) {
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
matches!(
|
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })),
|
||||||
err,
|
|
||||||
Error::Extraction(ExtractionError::ContentUnavailable(_))
|
|
||||||
),
|
|
||||||
"got: {err}"
|
"got: {err}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -729,10 +726,7 @@ 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!(
|
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })),
|
||||||
err,
|
|
||||||
Error::Extraction(ExtractionError::ContentUnavailable(_))
|
|
||||||
),
|
|
||||||
"got: {err}"
|
"got: {err}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -973,10 +967,7 @@ 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!(
|
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })),
|
||||||
err,
|
|
||||||
Error::Extraction(ExtractionError::ContentUnavailable(_))
|
|
||||||
),
|
|
||||||
"got: {err}"
|
"got: {err}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1017,10 +1008,7 @@ 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!(
|
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })),
|
||||||
err,
|
|
||||||
Error::Extraction(ExtractionError::ContentUnavailable(_))
|
|
||||||
),
|
|
||||||
"got: {}",
|
"got: {}",
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
|
@ -1164,7 +1152,7 @@ fn resolve_channel_not_found(rp: RustyPipe) {
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
err,
|
err,
|
||||||
Error::Extraction(ExtractionError::ContentUnavailable(_))
|
Error::Extraction(ExtractionError::NotFound { .. })
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1288,10 +1276,7 @@ fn music_playlist_not_found(rp: RustyPipe) {
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
matches!(
|
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })),
|
||||||
err,
|
|
||||||
Error::Extraction(ExtractionError::ContentUnavailable(_))
|
|
||||||
),
|
|
||||||
"got: {err}"
|
"got: {err}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1337,10 +1322,7 @@ 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!(
|
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })),
|
||||||
err,
|
|
||||||
Error::Extraction(ExtractionError::ContentUnavailable(_))
|
|
||||||
),
|
|
||||||
"got: {err}"
|
"got: {err}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1430,10 +1412,7 @@ fn music_artist_not_found(rp: RustyPipe) {
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
matches!(
|
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })),
|
||||||
err,
|
|
||||||
Error::Extraction(ExtractionError::ContentUnavailable(_))
|
|
||||||
),
|
|
||||||
"got: {err}"
|
"got: {err}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1857,10 +1836,7 @@ 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!(
|
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })),
|
||||||
err,
|
|
||||||
Error::Extraction(ExtractionError::ContentUnavailable(_))
|
|
||||||
),
|
|
||||||
"got: {err}"
|
"got: {err}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1970,10 +1946,7 @@ 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!(
|
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })),
|
||||||
err,
|
|
||||||
Error::Extraction(ExtractionError::ContentUnavailable(_))
|
|
||||||
),
|
|
||||||
"got: {err}"
|
"got: {err}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1989,10 +1962,7 @@ 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!(
|
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })),
|
||||||
err,
|
|
||||||
Error::Extraction(ExtractionError::ContentUnavailable(_))
|
|
||||||
),
|
|
||||||
"got: {err}"
|
"got: {err}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2016,10 +1986,7 @@ fn music_radio_playlist_not_found(rp: RustyPipe) {
|
||||||
|
|
||||||
if let Err(err) = res {
|
if let Err(err) = res {
|
||||||
assert!(
|
assert!(
|
||||||
matches!(
|
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })),
|
||||||
err,
|
|
||||||
Error::Extraction(ExtractionError::ContentUnavailable(_))
|
|
||||||
),
|
|
||||||
"got: {err}"
|
"got: {err}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2038,10 +2005,7 @@ 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!(
|
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })),
|
||||||
err,
|
|
||||||
Error::Extraction(ExtractionError::ContentUnavailable(_))
|
|
||||||
),
|
|
||||||
"got: {err}"
|
"got: {err}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2195,10 +2159,7 @@ 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!(
|
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })),
|
||||||
err,
|
|
||||||
Error::Extraction(ExtractionError::ContentUnavailable(_))
|
|
||||||
),
|
|
||||||
"got: {err}"
|
"got: {err}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue