Compare commits
No commits in common. "01b9c8e310dc1fce4d934d823748f8cf5d77c048" and "de118c59c457bff5d198f46ffee095397a3ec8ae" have entirely different histories.
01b9c8e310
...
de118c59c4
13 changed files with 452 additions and 274 deletions
|
@ -13,7 +13,7 @@ include = ["/src", "README.md", "LICENSE", "!snapshots"]
|
||||||
members = [".", "codegen", "cli"]
|
members = [".", "codegen", "cli"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["default-tls"]
|
default = ["default-tls", "rss"]
|
||||||
all = ["rss", "html"]
|
all = ["rss", "html"]
|
||||||
|
|
||||||
rss = ["quick-xml"]
|
rss = ["quick-xml"]
|
||||||
|
|
|
@ -39,7 +39,7 @@ enum Params {
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
pub async fn channel_videos(
|
pub async fn channel_videos(
|
||||||
self,
|
&self,
|
||||||
channel_id: &str,
|
channel_id: &str,
|
||||||
) -> Result<Channel<Paginator<ChannelVideo>>, Error> {
|
) -> Result<Channel<Paginator<ChannelVideo>>, Error> {
|
||||||
self.channel_videos_ordered(channel_id, ChannelOrder::default())
|
self.channel_videos_ordered(channel_id, ChannelOrder::default())
|
||||||
|
@ -47,7 +47,7 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn channel_videos_ordered(
|
pub async fn channel_videos_ordered(
|
||||||
self,
|
&self,
|
||||||
channel_id: &str,
|
channel_id: &str,
|
||||||
order: ChannelOrder,
|
order: ChannelOrder,
|
||||||
) -> Result<Channel<Paginator<ChannelVideo>>, Error> {
|
) -> Result<Channel<Paginator<ChannelVideo>>, Error> {
|
||||||
|
@ -73,7 +73,7 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn channel_videos_continuation(
|
pub async fn channel_videos_continuation(
|
||||||
self,
|
&self,
|
||||||
ctoken: &str,
|
ctoken: &str,
|
||||||
) -> Result<Paginator<ChannelVideo>, Error> {
|
) -> Result<Paginator<ChannelVideo>, Error> {
|
||||||
let context = self.get_context(ClientType::Desktop, true).await;
|
let context = self.get_context(ClientType::Desktop, true).await;
|
||||||
|
@ -93,7 +93,7 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn channel_playlists(
|
pub async fn channel_playlists(
|
||||||
self,
|
&self,
|
||||||
channel_id: &str,
|
channel_id: &str,
|
||||||
) -> Result<Channel<Paginator<ChannelPlaylist>>, Error> {
|
) -> Result<Channel<Paginator<ChannelPlaylist>>, Error> {
|
||||||
let context = self.get_context(ClientType::Desktop, true).await;
|
let context = self.get_context(ClientType::Desktop, true).await;
|
||||||
|
@ -114,7 +114,7 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn channel_playlists_continuation(
|
pub async fn channel_playlists_continuation(
|
||||||
self,
|
&self,
|
||||||
ctoken: &str,
|
ctoken: &str,
|
||||||
) -> Result<Paginator<ChannelPlaylist>, Error> {
|
) -> Result<Paginator<ChannelPlaylist>, Error> {
|
||||||
let context = self.get_context(ClientType::Desktop, true).await;
|
let context = self.get_context(ClientType::Desktop, true).await;
|
||||||
|
@ -383,13 +383,11 @@ fn map_channel<T>(
|
||||||
id: &str,
|
id: &str,
|
||||||
lang: Language,
|
lang: Language,
|
||||||
) -> Result<Channel<T>, ExtractionError> {
|
) -> Result<Channel<T>, ExtractionError> {
|
||||||
let header =
|
let header = header.ok_or(ExtractionError::NoData)?;
|
||||||
header.ok_or_else(|| ExtractionError::ContentUnavailable("channel not found".into()))?;
|
|
||||||
let metadata = metadata
|
let metadata = metadata
|
||||||
.ok_or_else(|| ExtractionError::ContentUnavailable("channel not found".into()))?
|
.ok_or(ExtractionError::NoData)?
|
||||||
.channel_metadata_renderer;
|
.channel_metadata_renderer;
|
||||||
let microformat = microformat
|
let microformat = microformat.ok_or(ExtractionError::NoData)?;
|
||||||
.ok_or_else(|| ExtractionError::ContentUnavailable("channel not found".into()))?;
|
|
||||||
|
|
||||||
if metadata.external_id != id {
|
if metadata.external_id != id {
|
||||||
return Err(ExtractionError::WrongResult(format!(
|
return Err(ExtractionError::WrongResult(format!(
|
||||||
|
@ -465,9 +463,7 @@ fn map_channel_content(
|
||||||
Some(contents) => {
|
Some(contents) => {
|
||||||
let tabs = contents.two_column_browse_results_renderer.tabs;
|
let tabs = contents.two_column_browse_results_renderer.tabs;
|
||||||
if tabs.is_empty() {
|
if tabs.is_empty() {
|
||||||
return Err(ExtractionError::ContentUnavailable(
|
return Err(ExtractionError::NoData);
|
||||||
"channel not found".into(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let (channel_content, target_id) = tabs
|
let (channel_content, target_id) = tabs
|
||||||
|
|
|
@ -9,7 +9,7 @@ use crate::{
|
||||||
use super::{response, RustyPipeQuery};
|
use super::{response, RustyPipeQuery};
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
pub async fn channel_rss(self, channel_id: &str) -> Result<ChannelRss, Error> {
|
pub async fn channel_rss(&self, channel_id: &str) -> Result<ChannelRss, Error> {
|
||||||
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
|
||||||
|
|
|
@ -27,7 +27,7 @@ use tokio::sync::RwLock;
|
||||||
use crate::{
|
use crate::{
|
||||||
cache::{CacheStorage, FileStorage},
|
cache::{CacheStorage, FileStorage},
|
||||||
deobfuscate::{DeobfData, Deobfuscator},
|
deobfuscate::{DeobfData, Deobfuscator},
|
||||||
error::{Error, ExtractionError},
|
error::{Error, ExtractionError, Result},
|
||||||
param::{Country, Language},
|
param::{Country, Language},
|
||||||
report::{FileReporter, Level, Report, Reporter},
|
report::{FileReporter, Level, Report, Reporter},
|
||||||
serializer::MapResult,
|
serializer::MapResult,
|
||||||
|
@ -467,7 +467,10 @@ impl RustyPipe {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute the given http request.
|
/// Execute the given http request.
|
||||||
async fn http_request(&self, request: Request) -> Result<Response, reqwest::Error> {
|
async fn http_request(
|
||||||
|
&self,
|
||||||
|
request: Request,
|
||||||
|
) -> core::result::Result<Response, reqwest::Error> {
|
||||||
let mut last_res = None;
|
let mut last_res = None;
|
||||||
for n in 0..self.inner.n_http_retries {
|
for n in 0..self.inner.n_http_retries {
|
||||||
let res = self.inner.http.execute(request.try_clone().unwrap()).await;
|
let res = self.inner.http.execute(request.try_clone().unwrap()).await;
|
||||||
|
@ -501,7 +504,7 @@ impl RustyPipe {
|
||||||
|
|
||||||
/// Execute the given http request, returning an error in case of a
|
/// Execute the given http request, returning an error in case of a
|
||||||
/// non-successful status code.
|
/// non-successful status code.
|
||||||
async fn http_request_estatus(&self, request: Request) -> Result<Response, Error> {
|
async fn http_request_estatus(&self, request: Request) -> Result<Response> {
|
||||||
let res = self.http_request(request).await?;
|
let res = self.http_request(request).await?;
|
||||||
let status = res.status();
|
let status = res.status();
|
||||||
|
|
||||||
|
@ -513,12 +516,12 @@ impl RustyPipe {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute the given http request, returning the response body as a string.
|
/// Execute the given http request, returning the response body as a string.
|
||||||
async fn http_request_txt(&self, request: Request) -> Result<String, Error> {
|
async fn http_request_txt(&self, request: Request) -> Result<String> {
|
||||||
Ok(self.http_request_estatus(request).await?.text().await?)
|
Ok(self.http_request_estatus(request).await?.text().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract the current version of the YouTube desktop client from the website.
|
/// Extract the current version of the YouTube desktop client from the website.
|
||||||
async fn extract_desktop_client_version(&self) -> Result<String, Error> {
|
async fn extract_desktop_client_version(&self) -> Result<String> {
|
||||||
let from_swjs = async {
|
let from_swjs = async {
|
||||||
let swjs = self
|
let swjs = self
|
||||||
.http_request_txt(
|
.http_request_txt(
|
||||||
|
@ -565,7 +568,7 @@ impl RustyPipe {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract the current version of the YouTube Music desktop client from the website.
|
/// Extract the current version of the YouTube Music desktop client from the website.
|
||||||
async fn extract_music_client_version(&self) -> Result<String, Error> {
|
async fn extract_music_client_version(&self) -> Result<String> {
|
||||||
let from_swjs = async {
|
let from_swjs = async {
|
||||||
let swjs = self
|
let swjs = self
|
||||||
.http_request_txt(
|
.http_request_txt(
|
||||||
|
@ -676,7 +679,7 @@ impl RustyPipe {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Instantiate a new deobfuscator from either cached or extracted YouTube JavaScript code.
|
/// Instantiate a new deobfuscator from either cached or extracted YouTube JavaScript code.
|
||||||
async fn get_deobf(&self) -> Result<Deobfuscator, Error> {
|
async fn get_deobf(&self) -> Result<Deobfuscator> {
|
||||||
// Write lock here to prevent concurrent tasks from fetching the same data
|
// Write lock here to prevent concurrent tasks from fetching the same data
|
||||||
let mut deobf = self.inner.cache.deobf.write().await;
|
let mut deobf = self.inner.cache.deobf.write().await;
|
||||||
|
|
||||||
|
@ -957,7 +960,7 @@ impl RustyPipeQuery {
|
||||||
endpoint: &str,
|
endpoint: &str,
|
||||||
body: &B,
|
body: &B,
|
||||||
deobf: Option<&Deobfuscator>,
|
deobf: Option<&Deobfuscator>,
|
||||||
) -> Result<M, Error> {
|
) -> Result<M> {
|
||||||
for n in 0..self.client.inner.n_query_retries.saturating_sub(1) {
|
for n in 0..self.client.inner.n_query_retries.saturating_sub(1) {
|
||||||
let res = self
|
let res = self
|
||||||
._try_execute_request_deobf::<R, M, B>(
|
._try_execute_request_deobf::<R, M, B>(
|
||||||
|
@ -1013,7 +1016,7 @@ impl RustyPipeQuery {
|
||||||
body: &B,
|
body: &B,
|
||||||
deobf: Option<&Deobfuscator>,
|
deobf: Option<&Deobfuscator>,
|
||||||
report: bool,
|
report: bool,
|
||||||
) -> Result<M, Error> {
|
) -> Result<M> {
|
||||||
let request = self
|
let request = self
|
||||||
.request_builder(ctype, endpoint)
|
.request_builder(ctype, endpoint)
|
||||||
.await
|
.await
|
||||||
|
@ -1092,6 +1095,7 @@ impl RustyPipeQuery {
|
||||||
ExtractionError::VideoUnavailable(_, _)
|
ExtractionError::VideoUnavailable(_, _)
|
||||||
| ExtractionError::VideoAgeRestricted
|
| ExtractionError::VideoAgeRestricted
|
||||||
| ExtractionError::ContentUnavailable(_)
|
| ExtractionError::ContentUnavailable(_)
|
||||||
|
| ExtractionError::NoData
|
||||||
| ExtractionError::Retry => (),
|
| ExtractionError::Retry => (),
|
||||||
_ => create_report(Level::ERR, Some(e.to_string()), Vec::new()),
|
_ => create_report(Level::ERR, Some(e.to_string()), Vec::new()),
|
||||||
}
|
}
|
||||||
|
@ -1129,7 +1133,7 @@ impl RustyPipeQuery {
|
||||||
id: &str,
|
id: &str,
|
||||||
endpoint: &str,
|
endpoint: &str,
|
||||||
body: &B,
|
body: &B,
|
||||||
) -> Result<M, Error> {
|
) -> Result<M> {
|
||||||
self.execute_request_deobf::<R, M, B>(ctype, operation, id, endpoint, body, None)
|
self.execute_request_deobf::<R, M, B>(ctype, operation, id, endpoint, body, None)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
@ -1155,7 +1159,7 @@ trait MapResponse<T> {
|
||||||
id: &str,
|
id: &str,
|
||||||
lang: Language,
|
lang: Language,
|
||||||
deobf: Option<&Deobfuscator>,
|
deobf: Option<&Deobfuscator>,
|
||||||
) -> Result<MapResult<T>, ExtractionError>;
|
) -> core::result::Result<MapResult<T>, crate::error::ExtractionError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -1,21 +1,20 @@
|
||||||
use crate::error::Error;
|
use crate::error::Result;
|
||||||
|
|
||||||
use crate::model::{
|
use crate::model::{
|
||||||
ChannelPlaylist, ChannelVideo, Comment, Paginator, PlaylistVideo, RecommendedVideo, SearchItem,
|
ChannelPlaylist, ChannelVideo, Comment, Paginator, PlaylistVideo, RecommendedVideo, SearchItem,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::RustyPipeQuery;
|
use super::RustyPipeQuery;
|
||||||
|
|
||||||
macro_rules! paginator {
|
impl Paginator<PlaylistVideo> {
|
||||||
($entity_type:ty, $cont_function:path) => {
|
pub async fn next(&self, query: RustyPipeQuery) -> Result<Option<Self>> {
|
||||||
impl Paginator<$entity_type> {
|
|
||||||
pub async fn next(&self, query: RustyPipeQuery) -> Result<Option<Self>, Error> {
|
|
||||||
Ok(match &self.ctoken {
|
Ok(match &self.ctoken {
|
||||||
Some(ctoken) => Some($cont_function(query, ctoken).await?),
|
Some(ctoken) => Some(query.playlist_continuation(ctoken).await?),
|
||||||
None => None,
|
None => None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn extend(&mut self, query: RustyPipeQuery) -> Result<bool, Error> {
|
pub async fn extend(&mut self, query: RustyPipeQuery) -> Result<bool> {
|
||||||
match self.next(query).await {
|
match self.next(query).await {
|
||||||
Ok(Some(paginator)) => {
|
Ok(Some(paginator)) => {
|
||||||
let mut items = paginator.items;
|
let mut items = paginator.items;
|
||||||
|
@ -28,11 +27,7 @@ macro_rules! paginator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn extend_pages(
|
pub async fn extend_pages(&mut self, query: RustyPipeQuery, n_pages: usize) -> Result<()> {
|
||||||
&mut self,
|
|
||||||
query: RustyPipeQuery,
|
|
||||||
n_pages: usize,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
for _ in 0..n_pages {
|
for _ in 0..n_pages {
|
||||||
match self.extend(query.clone()).await {
|
match self.extend(query.clone()).await {
|
||||||
Ok(false) => break,
|
Ok(false) => break,
|
||||||
|
@ -43,11 +38,7 @@ macro_rules! paginator {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn extend_limit(
|
pub async fn extend_limit(&mut self, query: RustyPipeQuery, n_items: usize) -> Result<()> {
|
||||||
&mut self,
|
|
||||||
query: RustyPipeQuery,
|
|
||||||
n_items: usize,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
while self.items.len() < n_items {
|
while self.items.len() < n_items {
|
||||||
match self.extend(query.clone()).await {
|
match self.extend(query.clone()).await {
|
||||||
Ok(false) => break,
|
Ok(false) => break,
|
||||||
|
@ -57,16 +48,224 @@ macro_rules! paginator {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
paginator!(PlaylistVideo, RustyPipeQuery::playlist_continuation);
|
impl Paginator<RecommendedVideo> {
|
||||||
paginator!(RecommendedVideo, RustyPipeQuery::video_recommendations);
|
pub async fn next(&self, query: RustyPipeQuery) -> Result<Option<Self>> {
|
||||||
paginator!(Comment, RustyPipeQuery::video_comments);
|
Ok(match &self.ctoken {
|
||||||
paginator!(ChannelVideo, RustyPipeQuery::channel_videos_continuation);
|
Some(ctoken) => Some(query.video_recommendations(ctoken).await?),
|
||||||
paginator!(
|
None => None,
|
||||||
ChannelPlaylist,
|
})
|
||||||
RustyPipeQuery::channel_playlists_continuation
|
}
|
||||||
);
|
|
||||||
paginator!(SearchItem, RustyPipeQuery::search_continuation);
|
pub async fn extend(&mut self, query: RustyPipeQuery) -> Result<bool> {
|
||||||
|
match self.next(query).await {
|
||||||
|
Ok(Some(paginator)) => {
|
||||||
|
let mut items = paginator.items;
|
||||||
|
self.items.append(&mut items);
|
||||||
|
self.ctoken = paginator.ctoken;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
Ok(None) => Ok(false),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn extend_pages(&mut self, query: RustyPipeQuery, n_pages: usize) -> Result<()> {
|
||||||
|
for _ in 0..n_pages {
|
||||||
|
match self.extend(query.clone()).await {
|
||||||
|
Ok(false) => break,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn extend_limit(&mut self, query: RustyPipeQuery, n_items: usize) -> Result<()> {
|
||||||
|
while self.items.len() < n_items {
|
||||||
|
match self.extend(query.clone()).await {
|
||||||
|
Ok(false) => break,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Paginator<ChannelVideo> {
|
||||||
|
pub async fn next(&self, query: RustyPipeQuery) -> Result<Option<Self>> {
|
||||||
|
Ok(match &self.ctoken {
|
||||||
|
Some(ctoken) => Some(query.channel_videos_continuation(ctoken).await?),
|
||||||
|
None => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn extend(&mut self, query: RustyPipeQuery) -> Result<bool> {
|
||||||
|
match self.next(query).await {
|
||||||
|
Ok(Some(paginator)) => {
|
||||||
|
let mut items = paginator.items;
|
||||||
|
self.items.append(&mut items);
|
||||||
|
self.ctoken = paginator.ctoken;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
Ok(None) => Ok(false),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn extend_pages(&mut self, query: RustyPipeQuery, n_pages: usize) -> Result<()> {
|
||||||
|
for _ in 0..n_pages {
|
||||||
|
match self.extend(query.clone()).await {
|
||||||
|
Ok(false) => break,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn extend_limit(&mut self, query: RustyPipeQuery, n_items: usize) -> Result<()> {
|
||||||
|
while self.items.len() < n_items {
|
||||||
|
match self.extend(query.clone()).await {
|
||||||
|
Ok(false) => break,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Paginator<ChannelPlaylist> {
|
||||||
|
pub async fn next(&self, query: RustyPipeQuery) -> Result<Option<Self>> {
|
||||||
|
Ok(match &self.ctoken {
|
||||||
|
Some(ctoken) => Some(query.channel_playlists_continuation(ctoken).await?),
|
||||||
|
None => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn extend(&mut self, query: RustyPipeQuery) -> Result<bool> {
|
||||||
|
match self.next(query).await {
|
||||||
|
Ok(Some(paginator)) => {
|
||||||
|
let mut items = paginator.items;
|
||||||
|
self.items.append(&mut items);
|
||||||
|
self.ctoken = paginator.ctoken;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
Ok(None) => Ok(false),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn extend_pages(&mut self, query: RustyPipeQuery, n_pages: usize) -> Result<()> {
|
||||||
|
for _ in 0..n_pages {
|
||||||
|
match self.extend(query.clone()).await {
|
||||||
|
Ok(false) => break,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn extend_limit(&mut self, query: RustyPipeQuery, n_items: usize) -> Result<()> {
|
||||||
|
while self.items.len() < n_items {
|
||||||
|
match self.extend(query.clone()).await {
|
||||||
|
Ok(false) => break,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Paginator<Comment> {
|
||||||
|
pub async fn next(&self, query: RustyPipeQuery) -> Result<Option<Self>> {
|
||||||
|
Ok(match &self.ctoken {
|
||||||
|
Some(ctoken) => Some(query.video_comments(ctoken).await?),
|
||||||
|
None => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn extend(&mut self, query: RustyPipeQuery) -> Result<bool> {
|
||||||
|
match self.next(query).await {
|
||||||
|
Ok(Some(paginator)) => {
|
||||||
|
let mut items = paginator.items;
|
||||||
|
self.items.append(&mut items);
|
||||||
|
self.ctoken = paginator.ctoken;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
Ok(None) => Ok(false),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn extend_pages(&mut self, query: RustyPipeQuery, n_pages: usize) -> Result<()> {
|
||||||
|
for _ in 0..n_pages {
|
||||||
|
match self.extend(query.clone()).await {
|
||||||
|
Ok(false) => break,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn extend_limit(&mut self, query: RustyPipeQuery, n_items: usize) -> Result<()> {
|
||||||
|
while self.items.len() < n_items {
|
||||||
|
match self.extend(query.clone()).await {
|
||||||
|
Ok(false) => break,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Paginator<SearchItem> {
|
||||||
|
pub async fn next(&self, query: RustyPipeQuery) -> Result<Option<Self>> {
|
||||||
|
Ok(match &self.ctoken {
|
||||||
|
Some(ctoken) => Some(query.search_continuation(ctoken).await?),
|
||||||
|
None => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn extend(&mut self, query: RustyPipeQuery) -> Result<bool> {
|
||||||
|
match self.next(query).await {
|
||||||
|
Ok(Some(paginator)) => {
|
||||||
|
let mut items = paginator.items;
|
||||||
|
self.items.append(&mut items);
|
||||||
|
self.ctoken = paginator.ctoken;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
Ok(None) => Ok(false),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn extend_pages(&mut self, query: RustyPipeQuery, n_pages: usize) -> Result<()> {
|
||||||
|
for _ in 0..n_pages {
|
||||||
|
match self.extend(query.clone()).await {
|
||||||
|
Ok(false) => break,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn extend_limit(&mut self, query: RustyPipeQuery, n_items: usize) -> Result<()> {
|
||||||
|
while self.items.len() < n_items {
|
||||||
|
match self.extend(query.clone()).await {
|
||||||
|
Ok(false) => break,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -38,10 +38,6 @@ use crate::serializer::{
|
||||||
use crate::timeago;
|
use crate::timeago;
|
||||||
use crate::util::{self, TryRemove};
|
use crate::util::{self, TryRemove};
|
||||||
|
|
||||||
use self::search::ChannelRenderer;
|
|
||||||
use self::search::PlaylistRenderer;
|
|
||||||
use self::search::VideoRenderer;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ContentRenderer<T> {
|
pub struct ContentRenderer<T> {
|
||||||
|
@ -510,10 +506,9 @@ pub fn alerts_to_err(alerts: Option<Vec<Alert>>) -> ExtractionError {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|a| a.alert_renderer.text)
|
.map(|a| a.alert_renderer.text)
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(" ")
|
.join(" "),
|
||||||
.into(),
|
|
||||||
),
|
),
|
||||||
None => ExtractionError::ContentUnavailable("content not found".into()),
|
None => ExtractionError::InvalidData("no contents".into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -522,7 +517,7 @@ pub trait FromWLang<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait TryFromWLang<T>: Sized {
|
pub trait TryFromWLang<T>: Sized {
|
||||||
fn from_w_lang(from: T, lang: Language) -> Result<Self, util::MappingError>;
|
fn from_w_lang(from: T, lang: Language) -> core::result::Result<Self, util::MappingError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromWLang<GridVideoRenderer> for model::ChannelVideo {
|
impl FromWLang<GridVideoRenderer> for model::ChannelVideo {
|
||||||
|
@ -582,7 +577,7 @@ impl TryFromWLang<CompactVideoRenderer> for model::RecommendedVideo {
|
||||||
fn from_w_lang(
|
fn from_w_lang(
|
||||||
video: CompactVideoRenderer,
|
video: CompactVideoRenderer,
|
||||||
lang: Language,
|
lang: Language,
|
||||||
) -> Result<Self, util::MappingError> {
|
) -> core::result::Result<Self, util::MappingError> {
|
||||||
let channel = model::ChannelId::try_from(video.channel)?;
|
let channel = model::ChannelId::try_from(video.channel)?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
@ -613,89 +608,3 @@ impl TryFromWLang<CompactVideoRenderer> for model::RecommendedVideo {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFromWLang<VideoRenderer> for model::SearchVideo {
|
|
||||||
fn from_w_lang(video: VideoRenderer, lang: Language) -> Result<Self, util::MappingError> {
|
|
||||||
let channel = model::ChannelId::try_from(video.channel)?;
|
|
||||||
let mut metadata_snippets = video.detailed_metadata_snippets;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
id: video.video_id,
|
|
||||||
title: video.title,
|
|
||||||
length: video
|
|
||||||
.length_text
|
|
||||||
.and_then(|txt| util::parse_video_length(&txt)),
|
|
||||||
thumbnail: video.thumbnail.into(),
|
|
||||||
channel: model::ChannelTag {
|
|
||||||
id: channel.id,
|
|
||||||
name: channel.name,
|
|
||||||
avatar: video
|
|
||||||
.channel_thumbnail_supported_renderers
|
|
||||||
.channel_thumbnail_with_link_renderer
|
|
||||||
.thumbnail
|
|
||||||
.into(),
|
|
||||||
verification: video.owner_badges.into(),
|
|
||||||
subscriber_count: None,
|
|
||||||
},
|
|
||||||
publish_date: video
|
|
||||||
.published_time_text
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|txt| timeago::parse_timeago_to_dt(lang, txt)),
|
|
||||||
publish_date_txt: video.published_time_text,
|
|
||||||
view_count: video
|
|
||||||
.view_count_text
|
|
||||||
.and_then(|txt| util::parse_numeric(&txt).ok())
|
|
||||||
.unwrap_or_default(),
|
|
||||||
is_live: video.thumbnail_overlays.is_live(),
|
|
||||||
is_short: video.thumbnail_overlays.is_short(),
|
|
||||||
short_description: metadata_snippets
|
|
||||||
.try_swap_remove(0)
|
|
||||||
.map(|s| s.snippet_text)
|
|
||||||
.unwrap_or_default(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<PlaylistRenderer> for model::SearchPlaylist {
|
|
||||||
fn from(playlist: PlaylistRenderer) -> Self {
|
|
||||||
let mut thumbnails = playlist.thumbnails;
|
|
||||||
|
|
||||||
Self {
|
|
||||||
id: playlist.playlist_id,
|
|
||||||
name: playlist.title,
|
|
||||||
thumbnail: thumbnails.try_swap_remove(0).unwrap_or_default().into(),
|
|
||||||
video_count: playlist.video_count,
|
|
||||||
first_videos: playlist
|
|
||||||
.videos
|
|
||||||
.into_iter()
|
|
||||||
.map(|v| model::SearchPlaylistVideo {
|
|
||||||
id: v.child_video_renderer.video_id,
|
|
||||||
title: v.child_video_renderer.title,
|
|
||||||
length: v
|
|
||||||
.child_video_renderer
|
|
||||||
.length_text
|
|
||||||
.and_then(|txt| util::parse_video_length(&txt)),
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ChannelRenderer> for model::SearchChannel {
|
|
||||||
fn from(channel: ChannelRenderer) -> Self {
|
|
||||||
Self {
|
|
||||||
id: channel.channel_id,
|
|
||||||
name: channel.title,
|
|
||||||
avatar: channel.thumbnail.into(),
|
|
||||||
verification: channel.owner_badges.into(),
|
|
||||||
subscriber_count: channel
|
|
||||||
.subscriber_count_text
|
|
||||||
.and_then(|txt| util::parse_numeric(&txt).ok()),
|
|
||||||
video_count: channel
|
|
||||||
.video_count_text
|
|
||||||
.and_then(|txt| util::parse_numeric(&txt).ok())
|
|
||||||
.unwrap_or_default(),
|
|
||||||
short_description: channel.description_snippet,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,13 +3,17 @@ use serde::Serialize;
|
||||||
use crate::{
|
use crate::{
|
||||||
deobfuscate::Deobfuscator,
|
deobfuscate::Deobfuscator,
|
||||||
error::{Error, ExtractionError},
|
error::{Error, ExtractionError},
|
||||||
model::{Paginator, SearchItem, SearchResult, SearchVideo},
|
model::{
|
||||||
|
ChannelId, ChannelTag, Paginator, SearchChannel, SearchItem, SearchPlaylist,
|
||||||
|
SearchPlaylistVideo, SearchResult, SearchVideo,
|
||||||
|
},
|
||||||
param::{search_filter::SearchFilter, Language},
|
param::{search_filter::SearchFilter, Language},
|
||||||
util::TryRemove,
|
timeago,
|
||||||
|
util::{self, TryRemove},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
response::{self, TryFromWLang},
|
response::{self, IsLive, IsShort},
|
||||||
ClientType, MapResponse, MapResult, QContinuation, RustyPipeQuery, YTContext,
|
ClientType, MapResponse, MapResult, QContinuation, RustyPipeQuery, YTContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -179,20 +183,86 @@ fn map_search_items(
|
||||||
let mapped_items = items
|
let mapped_items = items
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|item| match item {
|
.filter_map(|item| match item {
|
||||||
response::search::SearchItem::VideoRenderer(video) => {
|
response::search::SearchItem::VideoRenderer(mut video) => {
|
||||||
match SearchVideo::from_w_lang(video, lang) {
|
match ChannelId::try_from(video.channel) {
|
||||||
Ok(video) => Some(SearchItem::Video(video)),
|
Ok(channel) => Some(SearchItem::Video(SearchVideo {
|
||||||
|
id: video.video_id,
|
||||||
|
title: video.title,
|
||||||
|
length: video
|
||||||
|
.length_text
|
||||||
|
.and_then(|txt| util::parse_video_length_or_warn(&txt, &mut warnings)),
|
||||||
|
thumbnail: video.thumbnail.into(),
|
||||||
|
channel: ChannelTag {
|
||||||
|
id: channel.id,
|
||||||
|
name: channel.name,
|
||||||
|
avatar: video
|
||||||
|
.channel_thumbnail_supported_renderers
|
||||||
|
.channel_thumbnail_with_link_renderer
|
||||||
|
.thumbnail
|
||||||
|
.into(),
|
||||||
|
verification: video.owner_badges.into(),
|
||||||
|
subscriber_count: None,
|
||||||
|
},
|
||||||
|
publish_date: video.published_time_text.as_ref().and_then(|txt| {
|
||||||
|
timeago::parse_timeago_or_warn(lang, txt, &mut warnings)
|
||||||
|
}),
|
||||||
|
publish_date_txt: video.published_time_text,
|
||||||
|
view_count: video
|
||||||
|
.view_count_text
|
||||||
|
.and_then(|txt| util::parse_numeric(&txt).ok())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
is_live: video.thumbnail_overlays.is_live(),
|
||||||
|
is_short: video.thumbnail_overlays.is_short(),
|
||||||
|
short_description: video
|
||||||
|
.detailed_metadata_snippets
|
||||||
|
.try_swap_remove(0)
|
||||||
|
.map(|s| s.snippet_text)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
})),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warnings.push(e.to_string());
|
warnings.push(e.to_string());
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
response::search::SearchItem::PlaylistRenderer(playlist) => {
|
response::search::SearchItem::PlaylistRenderer(mut playlist) => {
|
||||||
Some(SearchItem::Playlist(playlist.into()))
|
Some(SearchItem::Playlist(SearchPlaylist {
|
||||||
|
id: playlist.playlist_id,
|
||||||
|
name: playlist.title,
|
||||||
|
thumbnail: playlist
|
||||||
|
.thumbnails
|
||||||
|
.try_swap_remove(0)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into(),
|
||||||
|
video_count: playlist.video_count,
|
||||||
|
first_videos: playlist
|
||||||
|
.videos
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| SearchPlaylistVideo {
|
||||||
|
id: v.child_video_renderer.video_id,
|
||||||
|
title: v.child_video_renderer.title,
|
||||||
|
length: v.child_video_renderer.length_text.and_then(|txt| {
|
||||||
|
util::parse_video_length_or_warn(&txt, &mut warnings)
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
response::search::SearchItem::ChannelRenderer(channel) => {
|
response::search::SearchItem::ChannelRenderer(channel) => {
|
||||||
Some(SearchItem::Channel(channel.into()))
|
Some(SearchItem::Channel(SearchChannel {
|
||||||
|
id: channel.channel_id,
|
||||||
|
name: channel.title,
|
||||||
|
avatar: channel.thumbnail.into(),
|
||||||
|
verification: channel.owner_badges.into(),
|
||||||
|
subscriber_count: channel
|
||||||
|
.subscriber_count_text
|
||||||
|
.and_then(|txt| util::parse_numeric_or_warn(&txt, &mut warnings)),
|
||||||
|
video_count: channel
|
||||||
|
.video_count_text
|
||||||
|
.and_then(|txt| util::parse_numeric(&txt).ok())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
short_description: channel.description_snippet,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
response::search::SearchItem::ShowingResultsForRenderer { corrected_query } => {
|
response::search::SearchItem::ShowingResultsForRenderer { corrected_query } => {
|
||||||
c_query = Some(corrected_query);
|
c_query = Some(corrected_query);
|
||||||
|
|
|
@ -516,7 +516,11 @@ fn map_comment(
|
||||||
}),
|
}),
|
||||||
_ => None,
|
_ => None,
|
||||||
},
|
},
|
||||||
publish_date: timeago::parse_timeago_to_dt(lang, &c.published_time_text),
|
publish_date: timeago::parse_timeago_or_warn(
|
||||||
|
lang,
|
||||||
|
&c.published_time_text,
|
||||||
|
&mut warnings,
|
||||||
|
),
|
||||||
publish_date_txt: c.published_time_text,
|
publish_date_txt: c.published_time_text,
|
||||||
like_count: util::parse_numeric_or_warn(
|
like_count: util::parse_numeric_or_warn(
|
||||||
&c.action_buttons
|
&c.action_buttons
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
pub(crate) type Result<T> = core::result::Result<T, Error>;
|
||||||
|
|
||||||
/// Custom 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]
|
||||||
|
@ -73,8 +75,10 @@ pub enum ExtractionError {
|
||||||
VideoUnavailable(&'static str, String),
|
VideoUnavailable(&'static str, String),
|
||||||
#[error("Video is age restricted")]
|
#[error("Video is age restricted")]
|
||||||
VideoAgeRestricted,
|
VideoAgeRestricted,
|
||||||
#[error("Content is not available. Reason: {0}")]
|
#[error("Content is not available. Reason (from YT): {0}")]
|
||||||
ContentUnavailable(Cow<'static, str>),
|
ContentUnavailable(String),
|
||||||
|
#[error("Got no data from YouTube")]
|
||||||
|
NoData,
|
||||||
#[error("deserialization error: {0}")]
|
#[error("deserialization error: {0}")]
|
||||||
Deserialization(#[from] serde_json::Error),
|
Deserialization(#[from] serde_json::Error),
|
||||||
#[error("got invalid data from YT: {0}")]
|
#[error("got invalid data from YT: {0}")]
|
||||||
|
|
|
@ -11,7 +11,7 @@ use log::error;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::deobfuscate::DeobfData;
|
use crate::deobfuscate::DeobfData;
|
||||||
use crate::error::Error;
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
|
@ -96,7 +96,7 @@ impl FileReporter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _report(&self, report: &Report) -> Result<(), Error> {
|
fn _report(&self, report: &Report) -> Result<()> {
|
||||||
let report_path = get_report_path(&self.path, report, "json")?;
|
let report_path = get_report_path(&self.path, report, "json")?;
|
||||||
serde_json::to_writer_pretty(&File::create(report_path)?, &report)
|
serde_json::to_writer_pretty(&File::create(report_path)?, &report)
|
||||||
.map_err(|e| Error::Other(format!("could not serialize report. err: {}", e).into()))?;
|
.map_err(|e| Error::Other(format!("could not serialize report. err: {}", e).into()))?;
|
||||||
|
@ -119,7 +119,7 @@ impl Reporter for FileReporter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_report_path(root: &Path, report: &Report, ext: &str) -> Result<PathBuf, Error> {
|
fn get_report_path(root: &Path, report: &Report, ext: &str) -> Result<PathBuf> {
|
||||||
if !root.is_dir() {
|
if !root.is_dir() {
|
||||||
std::fs::create_dir_all(root)?;
|
std::fs::create_dir_all(root)?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -184,6 +184,18 @@ pub fn parse_timeago_to_dt(lang: Language, textual_date: &str) -> Option<DateTim
|
||||||
parse_timeago(lang, textual_date).map(|ta| ta.into())
|
parse_timeago(lang, textual_date).map(|ta| ta.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse_timeago_or_warn(
|
||||||
|
lang: Language,
|
||||||
|
textual_date: &str,
|
||||||
|
warnings: &mut Vec<String>,
|
||||||
|
) -> Option<DateTime<Local>> {
|
||||||
|
let res = parse_timeago_to_dt(lang, textual_date);
|
||||||
|
if res.is_none() {
|
||||||
|
warnings.push(format!("could not parse timeago `{}`", textual_date));
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse a textual date (e.g. "29 minutes ago" or "Jul 2, 2014") into a ParsedDate object.
|
/// Parse a textual date (e.g. "29 minutes ago" or "Jul 2, 2014") into a ParsedDate object.
|
||||||
///
|
///
|
||||||
/// Returns None if the date could not be parsed.
|
/// Returns None if the date could not be parsed.
|
||||||
|
|
|
@ -15,7 +15,7 @@ use once_cell::sync::Lazy;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{error::Error, param::Language};
|
use crate::{error::Error, error::Result, param::Language};
|
||||||
|
|
||||||
const CONTENT_PLAYBACK_NONCE_ALPHABET: &[u8; 64] =
|
const CONTENT_PLAYBACK_NONCE_ALPHABET: &[u8; 64] =
|
||||||
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||||
|
@ -57,7 +57,7 @@ pub fn generate_content_playback_nonce() -> String {
|
||||||
/// Example:
|
/// Example:
|
||||||
///
|
///
|
||||||
/// `example.com/api?k1=v1&k2=v2 => example.com/api; {k1: v1, k2: v2}`
|
/// `example.com/api?k1=v1&k2=v2 => example.com/api; {k1: v1, k2: v2}`
|
||||||
pub fn url_to_params(url: &str) -> Result<(String, BTreeMap<String, String>), Error> {
|
pub fn url_to_params(url: &str) -> Result<(String, BTreeMap<String, String>)> {
|
||||||
let mut parsed_url = Url::parse(url)
|
let mut parsed_url = Url::parse(url)
|
||||||
.map_err(|e| Error::Other(format!("could not parse url `{}` err: {}", url, e).into()))?;
|
.map_err(|e| Error::Other(format!("could not parse url `{}` err: {}", url, e).into()))?;
|
||||||
let url_params: BTreeMap<String, String> = parsed_url
|
let url_params: BTreeMap<String, String> = parsed_url
|
||||||
|
@ -77,7 +77,7 @@ pub fn urlencode(string: &str) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a string after removing all non-numeric characters
|
/// Parse a string after removing all non-numeric characters
|
||||||
pub fn parse_numeric<F>(string: &str) -> Result<F, F::Err>
|
pub fn parse_numeric<F>(string: &str) -> core::result::Result<F, F::Err>
|
||||||
where
|
where
|
||||||
F: FromStr,
|
F: FromStr,
|
||||||
{
|
{
|
||||||
|
@ -147,6 +147,14 @@ where
|
||||||
res.ok()
|
res.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_video_length_or_warn(text: &str, warnings: &mut Vec<String>) -> Option<u32> {
|
||||||
|
let res = parse_video_length(text);
|
||||||
|
if res.is_none() {
|
||||||
|
warnings.push(format!("could not parse video length `{}`", text));
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
pub fn retry_delay(
|
pub fn retry_delay(
|
||||||
n_past_retries: u32,
|
n_past_retries: u32,
|
||||||
min_retry_interval: u32,
|
min_retry_interval: u32,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use chrono::Datelike;
|
use chrono::{Datelike, Timelike};
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
use rustypipe::client::{ClientType, RustyPipe};
|
use rustypipe::client::{ClientType, RustyPipe};
|
||||||
|
@ -218,14 +218,10 @@ async fn playlist_not_found() {
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
|
||||||
assert!(
|
assert!(matches!(
|
||||||
matches!(
|
|
||||||
err,
|
err,
|
||||||
Error::Extraction(ExtractionError::ContentUnavailable(_))
|
Error::Extraction(ExtractionError::ContentUnavailable(_))
|
||||||
),
|
));
|
||||||
"got: {}",
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//#VIDEO DETAILS
|
//#VIDEO DETAILS
|
||||||
|
@ -328,7 +324,6 @@ async fn get_video_details_music() {
|
||||||
assert!(!details.is_live);
|
assert!(!details.is_live);
|
||||||
assert!(!details.is_ccommons);
|
assert!(!details.is_ccommons);
|
||||||
|
|
||||||
assert!(!details.recommended.items.is_empty());
|
|
||||||
assert!(!details.recommended.is_exhausted());
|
assert!(!details.recommended.is_exhausted());
|
||||||
|
|
||||||
// Comments are disabled for this video
|
// Comments are disabled for this video
|
||||||
|
@ -386,7 +381,6 @@ async fn get_video_details_ccommons() {
|
||||||
assert!(!details.is_live);
|
assert!(!details.is_live);
|
||||||
assert!(details.is_ccommons);
|
assert!(details.is_ccommons);
|
||||||
|
|
||||||
assert!(!details.recommended.items.is_empty());
|
|
||||||
assert!(!details.recommended.is_exhausted());
|
assert!(!details.recommended.is_exhausted());
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
|
@ -526,7 +520,6 @@ async fn get_video_details_chapters() {
|
||||||
"###);
|
"###);
|
||||||
}
|
}
|
||||||
|
|
||||||
assert!(!details.recommended.items.is_empty());
|
|
||||||
assert!(!details.recommended.is_exhausted());
|
assert!(!details.recommended.is_exhausted());
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
|
@ -586,7 +579,6 @@ async fn get_video_details_live() {
|
||||||
assert!(details.is_live);
|
assert!(details.is_live);
|
||||||
assert!(!details.is_ccommons);
|
assert!(!details.is_ccommons);
|
||||||
|
|
||||||
assert!(!details.recommended.items.is_empty());
|
|
||||||
assert!(!details.recommended.is_exhausted());
|
assert!(!details.recommended.is_exhausted());
|
||||||
|
|
||||||
// No comments because livestream
|
// No comments because livestream
|
||||||
|
@ -645,14 +637,10 @@ async fn get_video_details_not_found() {
|
||||||
let rp = RustyPipe::builder().strict().build();
|
let rp = RustyPipe::builder().strict().build();
|
||||||
let err = rp.query().video_details("abcdefgLi5X").await.unwrap_err();
|
let err = rp.query().video_details("abcdefgLi5X").await.unwrap_err();
|
||||||
|
|
||||||
assert!(
|
assert!(matches!(
|
||||||
matches!(
|
|
||||||
err,
|
err,
|
||||||
Error::Extraction(ExtractionError::ContentUnavailable(_))
|
Error::Extraction(ExtractionError::ContentUnavailable(_))
|
||||||
),
|
))
|
||||||
"got: {}",
|
|
||||||
err
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
@ -849,15 +837,13 @@ fn assert_channel_eevblog<T>(channel: &Channel<T>) {
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case::artist("UC_vmjW5e1xEHhYjY2a0kK1A", "Oonagh - Topic", false, false)]
|
#[case::artist("UC_vmjW5e1xEHhYjY2a0kK1A", "Oonagh - Topic", false, false)]
|
||||||
#[case::shorts("UCh8gHdtzO2tXd593_bjErWg", "Doobydobap", true, true)]
|
#[case::shorts("UCh8gHdtzO2tXd593_bjErWg", "Doobydobap", true, true)]
|
||||||
#[case::livestream(
|
#[case::live(
|
||||||
"UChs0pSaEoNLV4mevBFGaoKA",
|
"UChs0pSaEoNLV4mevBFGaoKA",
|
||||||
"The Good Life Radio x Sensual Musique",
|
"The Good Life Radio x Sensual Musique",
|
||||||
true,
|
true,
|
||||||
true
|
true
|
||||||
)]
|
)]
|
||||||
#[case::music("UC-9-kyTW8ZkZNDHQJ6FgpwQ", "Music", false, false)]
|
#[case::music("UC-9-kyTW8ZkZNDHQJ6FgpwQ", "Music", false, false)]
|
||||||
#[case::live("UC4R8DWoMoI7CAwX8_LjQHig", "Live", false, false)]
|
|
||||||
#[case::news("UCYfdidRxbB8Qhf0Nx7ioOYw", "News", false, false)]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn channel_more(
|
async fn channel_more(
|
||||||
#[case] id: &str,
|
#[case] id: &str,
|
||||||
|
@ -896,36 +882,27 @@ async fn channel_more(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case::not_exist("UCOpNcN46UbXVtpKMrmU4Abx")]
|
#[case::gaming("UCOpNcN46UbXVtpKMrmU4Abg", false)]
|
||||||
#[case::gaming("UCOpNcN46UbXVtpKMrmU4Abg")]
|
#[case::not_found("UCOpNcN46UbXVtpKMrmU4Abx", true)]
|
||||||
#[case::movies("UCuJcl0Ju-gPDoksRjK1ya-w")]
|
|
||||||
#[case::sports("UCEgdi0XIXXZ-qJOFPf4JSKw")]
|
|
||||||
#[case::learning("UCtFRv9O2AHqOZjjynzrv-xg")]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn channel_not_found(#[case] id: &str) {
|
async fn channel_error(#[case] id: &str, #[case] not_found: bool) {
|
||||||
let rp = RustyPipe::builder().strict().build();
|
let rp = RustyPipe::builder().strict().build();
|
||||||
let err = rp.query().channel_videos(&id).await.unwrap_err();
|
let err = rp.query().channel_videos(&id).await.unwrap_err();
|
||||||
|
|
||||||
assert!(
|
if not_found {
|
||||||
matches!(
|
assert!(matches!(
|
||||||
err,
|
err,
|
||||||
Error::Extraction(ExtractionError::ContentUnavailable(_))
|
Error::Extraction(ExtractionError::ContentUnavailable(_))
|
||||||
),
|
));
|
||||||
"got: {}",
|
} else {
|
||||||
err
|
assert!(matches!(err, Error::Extraction(ExtractionError::NoData)));
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//#CHANNEL_RSS
|
//#CHANNEL_RSS
|
||||||
|
|
||||||
#[cfg(feature = "rss")]
|
#[tokio::test]
|
||||||
mod channel_rss {
|
async fn get_channel_rss() {
|
||||||
use super::*;
|
|
||||||
|
|
||||||
use chrono::Timelike;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn get_channel_rss() {
|
|
||||||
let rp = RustyPipe::builder().strict().build();
|
let rp = RustyPipe::builder().strict().build();
|
||||||
let channel = rp
|
let channel = rp
|
||||||
.query()
|
.query()
|
||||||
|
@ -942,10 +919,10 @@ mod channel_rss {
|
||||||
assert_eq!(channel.create_date.minute(), 18);
|
assert_eq!(channel.create_date.minute(), 18);
|
||||||
|
|
||||||
assert!(!channel.videos.is_empty());
|
assert!(!channel.videos.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn get_channel_rss_not_found() {
|
async fn get_channel_rss_not_found() {
|
||||||
let rp = RustyPipe::builder().strict().build();
|
let rp = RustyPipe::builder().strict().build();
|
||||||
let err = rp
|
let err = rp
|
||||||
.query()
|
.query()
|
||||||
|
@ -953,15 +930,10 @@ mod channel_rss {
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
|
||||||
assert!(
|
assert!(matches!(
|
||||||
matches!(
|
|
||||||
err,
|
err,
|
||||||
Error::Extraction(ExtractionError::ContentUnavailable(_))
|
Error::Extraction(ExtractionError::ContentUnavailable(_))
|
||||||
),
|
));
|
||||||
"got: {}",
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//#SEARCH
|
//#SEARCH
|
||||||
|
|
Loading…
Add table
Reference in a new issue