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,
 | 
				
			||||||
| 
						 | 
					@ -58,15 +49,223 @@ macro_rules! paginator {
 | 
				
			||||||
        Ok(())
 | 
					        Ok(())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
    };
 | 
					
 | 
				
			||||||
 | 
					impl Paginator<RecommendedVideo> {
 | 
				
			||||||
 | 
					    pub async fn next(&self, query: RustyPipeQuery) -> Result<Option<Self>> {
 | 
				
			||||||
 | 
					        Ok(match &self.ctoken {
 | 
				
			||||||
 | 
					            Some(ctoken) => Some(query.video_recommendations(ctoken).await?),
 | 
				
			||||||
 | 
					            None => None,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
paginator!(PlaylistVideo, RustyPipeQuery::playlist_continuation);
 | 
					    pub async fn extend(&mut self, query: RustyPipeQuery) -> Result<bool> {
 | 
				
			||||||
paginator!(RecommendedVideo, RustyPipeQuery::video_recommendations);
 | 
					        match self.next(query).await {
 | 
				
			||||||
paginator!(Comment, RustyPipeQuery::video_comments);
 | 
					            Ok(Some(paginator)) => {
 | 
				
			||||||
paginator!(ChannelVideo, RustyPipeQuery::channel_videos_continuation);
 | 
					                let mut items = paginator.items;
 | 
				
			||||||
paginator!(
 | 
					                self.items.append(&mut items);
 | 
				
			||||||
    ChannelPlaylist,
 | 
					                self.ctoken = paginator.ctoken;
 | 
				
			||||||
    RustyPipeQuery::channel_playlists_continuation
 | 
					                Ok(true)
 | 
				
			||||||
);
 | 
					            }
 | 
				
			||||||
paginator!(SearchItem, RustyPipeQuery::search_continuation);
 | 
					            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,34 +882,25 @@ 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")]
 | 
					 | 
				
			||||||
mod channel_rss {
 | 
					 | 
				
			||||||
    use super::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    use chrono::Timelike;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[tokio::test]
 | 
					#[tokio::test]
 | 
				
			||||||
async fn get_channel_rss() {
 | 
					async fn get_channel_rss() {
 | 
				
			||||||
    let rp = RustyPipe::builder().strict().build();
 | 
					    let rp = RustyPipe::builder().strict().build();
 | 
				
			||||||
| 
						 | 
					@ -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
		Add a link
		
	
		Reference in a new issue