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"]
 | 
			
		||||
 | 
			
		||||
[features]
 | 
			
		||||
default = ["default-tls"]
 | 
			
		||||
default = ["default-tls", "rss"]
 | 
			
		||||
all = ["rss", "html"]
 | 
			
		||||
 | 
			
		||||
rss = ["quick-xml"]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -39,7 +39,7 @@ enum Params {
 | 
			
		|||
 | 
			
		||||
impl RustyPipeQuery {
 | 
			
		||||
    pub async fn channel_videos(
 | 
			
		||||
        self,
 | 
			
		||||
        &self,
 | 
			
		||||
        channel_id: &str,
 | 
			
		||||
    ) -> Result<Channel<Paginator<ChannelVideo>>, Error> {
 | 
			
		||||
        self.channel_videos_ordered(channel_id, ChannelOrder::default())
 | 
			
		||||
| 
						 | 
				
			
			@ -47,7 +47,7 @@ impl RustyPipeQuery {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn channel_videos_ordered(
 | 
			
		||||
        self,
 | 
			
		||||
        &self,
 | 
			
		||||
        channel_id: &str,
 | 
			
		||||
        order: ChannelOrder,
 | 
			
		||||
    ) -> Result<Channel<Paginator<ChannelVideo>>, Error> {
 | 
			
		||||
| 
						 | 
				
			
			@ -73,7 +73,7 @@ impl RustyPipeQuery {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn channel_videos_continuation(
 | 
			
		||||
        self,
 | 
			
		||||
        &self,
 | 
			
		||||
        ctoken: &str,
 | 
			
		||||
    ) -> Result<Paginator<ChannelVideo>, Error> {
 | 
			
		||||
        let context = self.get_context(ClientType::Desktop, true).await;
 | 
			
		||||
| 
						 | 
				
			
			@ -93,7 +93,7 @@ impl RustyPipeQuery {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn channel_playlists(
 | 
			
		||||
        self,
 | 
			
		||||
        &self,
 | 
			
		||||
        channel_id: &str,
 | 
			
		||||
    ) -> Result<Channel<Paginator<ChannelPlaylist>>, Error> {
 | 
			
		||||
        let context = self.get_context(ClientType::Desktop, true).await;
 | 
			
		||||
| 
						 | 
				
			
			@ -114,7 +114,7 @@ impl RustyPipeQuery {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn channel_playlists_continuation(
 | 
			
		||||
        self,
 | 
			
		||||
        &self,
 | 
			
		||||
        ctoken: &str,
 | 
			
		||||
    ) -> Result<Paginator<ChannelPlaylist>, Error> {
 | 
			
		||||
        let context = self.get_context(ClientType::Desktop, true).await;
 | 
			
		||||
| 
						 | 
				
			
			@ -383,13 +383,11 @@ fn map_channel<T>(
 | 
			
		|||
    id: &str,
 | 
			
		||||
    lang: Language,
 | 
			
		||||
) -> Result<Channel<T>, ExtractionError> {
 | 
			
		||||
    let header =
 | 
			
		||||
        header.ok_or_else(|| ExtractionError::ContentUnavailable("channel not found".into()))?;
 | 
			
		||||
    let header = header.ok_or(ExtractionError::NoData)?;
 | 
			
		||||
    let metadata = metadata
 | 
			
		||||
        .ok_or_else(|| ExtractionError::ContentUnavailable("channel not found".into()))?
 | 
			
		||||
        .ok_or(ExtractionError::NoData)?
 | 
			
		||||
        .channel_metadata_renderer;
 | 
			
		||||
    let microformat = microformat
 | 
			
		||||
        .ok_or_else(|| ExtractionError::ContentUnavailable("channel not found".into()))?;
 | 
			
		||||
    let microformat = microformat.ok_or(ExtractionError::NoData)?;
 | 
			
		||||
 | 
			
		||||
    if metadata.external_id != id {
 | 
			
		||||
        return Err(ExtractionError::WrongResult(format!(
 | 
			
		||||
| 
						 | 
				
			
			@ -465,9 +463,7 @@ fn map_channel_content(
 | 
			
		|||
        Some(contents) => {
 | 
			
		||||
            let tabs = contents.two_column_browse_results_renderer.tabs;
 | 
			
		||||
            if tabs.is_empty() {
 | 
			
		||||
                return Err(ExtractionError::ContentUnavailable(
 | 
			
		||||
                    "channel not found".into(),
 | 
			
		||||
                ));
 | 
			
		||||
                return Err(ExtractionError::NoData);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let (channel_content, target_id) = tabs
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,7 @@ use crate::{
 | 
			
		|||
use super::{response, 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!(
 | 
			
		||||
            "https://www.youtube.com/feeds/videos.xml?channel_id={}",
 | 
			
		||||
            channel_id
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,7 +27,7 @@ use tokio::sync::RwLock;
 | 
			
		|||
use crate::{
 | 
			
		||||
    cache::{CacheStorage, FileStorage},
 | 
			
		||||
    deobfuscate::{DeobfData, Deobfuscator},
 | 
			
		||||
    error::{Error, ExtractionError},
 | 
			
		||||
    error::{Error, ExtractionError, Result},
 | 
			
		||||
    param::{Country, Language},
 | 
			
		||||
    report::{FileReporter, Level, Report, Reporter},
 | 
			
		||||
    serializer::MapResult,
 | 
			
		||||
| 
						 | 
				
			
			@ -467,7 +467,10 @@ impl RustyPipe {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    /// 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;
 | 
			
		||||
        for n in 0..self.inner.n_http_retries {
 | 
			
		||||
            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
 | 
			
		||||
    /// 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 status = res.status();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -513,12 +516,12 @@ impl RustyPipe {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    /// 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?)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// 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 swjs = self
 | 
			
		||||
                .http_request_txt(
 | 
			
		||||
| 
						 | 
				
			
			@ -565,7 +568,7 @@ impl RustyPipe {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    /// 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 swjs = self
 | 
			
		||||
                .http_request_txt(
 | 
			
		||||
| 
						 | 
				
			
			@ -676,7 +679,7 @@ impl RustyPipe {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    /// 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
 | 
			
		||||
        let mut deobf = self.inner.cache.deobf.write().await;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -957,7 +960,7 @@ impl RustyPipeQuery {
 | 
			
		|||
        endpoint: &str,
 | 
			
		||||
        body: &B,
 | 
			
		||||
        deobf: Option<&Deobfuscator>,
 | 
			
		||||
    ) -> Result<M, Error> {
 | 
			
		||||
    ) -> Result<M> {
 | 
			
		||||
        for n in 0..self.client.inner.n_query_retries.saturating_sub(1) {
 | 
			
		||||
            let res = self
 | 
			
		||||
                ._try_execute_request_deobf::<R, M, B>(
 | 
			
		||||
| 
						 | 
				
			
			@ -1013,7 +1016,7 @@ impl RustyPipeQuery {
 | 
			
		|||
        body: &B,
 | 
			
		||||
        deobf: Option<&Deobfuscator>,
 | 
			
		||||
        report: bool,
 | 
			
		||||
    ) -> Result<M, Error> {
 | 
			
		||||
    ) -> Result<M> {
 | 
			
		||||
        let request = self
 | 
			
		||||
            .request_builder(ctype, endpoint)
 | 
			
		||||
            .await
 | 
			
		||||
| 
						 | 
				
			
			@ -1092,6 +1095,7 @@ impl RustyPipeQuery {
 | 
			
		|||
                        ExtractionError::VideoUnavailable(_, _)
 | 
			
		||||
                        | ExtractionError::VideoAgeRestricted
 | 
			
		||||
                        | ExtractionError::ContentUnavailable(_)
 | 
			
		||||
                        | ExtractionError::NoData
 | 
			
		||||
                        | ExtractionError::Retry => (),
 | 
			
		||||
                        _ => create_report(Level::ERR, Some(e.to_string()), Vec::new()),
 | 
			
		||||
                    }
 | 
			
		||||
| 
						 | 
				
			
			@ -1129,7 +1133,7 @@ impl RustyPipeQuery {
 | 
			
		|||
        id: &str,
 | 
			
		||||
        endpoint: &str,
 | 
			
		||||
        body: &B,
 | 
			
		||||
    ) -> Result<M, Error> {
 | 
			
		||||
    ) -> Result<M> {
 | 
			
		||||
        self.execute_request_deobf::<R, M, B>(ctype, operation, id, endpoint, body, None)
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -1155,7 +1159,7 @@ trait MapResponse<T> {
 | 
			
		|||
        id: &str,
 | 
			
		||||
        lang: Language,
 | 
			
		||||
        deobf: Option<&Deobfuscator>,
 | 
			
		||||
    ) -> Result<MapResult<T>, ExtractionError>;
 | 
			
		||||
    ) -> core::result::Result<MapResult<T>, crate::error::ExtractionError>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,72 +1,271 @@
 | 
			
		|||
use crate::error::Error;
 | 
			
		||||
use crate::error::Result;
 | 
			
		||||
 | 
			
		||||
use crate::model::{
 | 
			
		||||
    ChannelPlaylist, ChannelVideo, Comment, Paginator, PlaylistVideo, RecommendedVideo, SearchItem,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use super::RustyPipeQuery;
 | 
			
		||||
 | 
			
		||||
macro_rules! paginator {
 | 
			
		||||
    ($entity_type:ty, $cont_function:path) => {
 | 
			
		||||
        impl Paginator<$entity_type> {
 | 
			
		||||
            pub async fn next(&self, query: RustyPipeQuery) -> Result<Option<Self>, Error> {
 | 
			
		||||
                Ok(match &self.ctoken {
 | 
			
		||||
                    Some(ctoken) => Some($cont_function(query, ctoken).await?),
 | 
			
		||||
                    None => None,
 | 
			
		||||
                })
 | 
			
		||||
            }
 | 
			
		||||
impl Paginator<PlaylistVideo> {
 | 
			
		||||
    pub async fn next(&self, query: RustyPipeQuery) -> Result<Option<Self>> {
 | 
			
		||||
        Ok(match &self.ctoken {
 | 
			
		||||
            Some(ctoken) => Some(query.playlist_continuation(ctoken).await?),
 | 
			
		||||
            None => None,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
            pub async fn extend(&mut self, query: RustyPipeQuery) -> Result<bool, Error> {
 | 
			
		||||
                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(&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<(), Error> {
 | 
			
		||||
                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<(), Error> {
 | 
			
		||||
                while self.items.len() < n_items {
 | 
			
		||||
                    match self.extend(query.clone()).await {
 | 
			
		||||
                        Ok(false) => break,
 | 
			
		||||
                        Err(e) => return Err(e),
 | 
			
		||||
                        _ => {}
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                Ok(())
 | 
			
		||||
    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(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
paginator!(PlaylistVideo, RustyPipeQuery::playlist_continuation);
 | 
			
		||||
paginator!(RecommendedVideo, RustyPipeQuery::video_recommendations);
 | 
			
		||||
paginator!(Comment, RustyPipeQuery::video_comments);
 | 
			
		||||
paginator!(ChannelVideo, RustyPipeQuery::channel_videos_continuation);
 | 
			
		||||
paginator!(
 | 
			
		||||
    ChannelPlaylist,
 | 
			
		||||
    RustyPipeQuery::channel_playlists_continuation
 | 
			
		||||
);
 | 
			
		||||
paginator!(SearchItem, RustyPipeQuery::search_continuation);
 | 
			
		||||
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,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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::util::{self, TryRemove};
 | 
			
		||||
 | 
			
		||||
use self::search::ChannelRenderer;
 | 
			
		||||
use self::search::PlaylistRenderer;
 | 
			
		||||
use self::search::VideoRenderer;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Deserialize)]
 | 
			
		||||
#[serde(rename_all = "camelCase")]
 | 
			
		||||
pub struct ContentRenderer<T> {
 | 
			
		||||
| 
						 | 
				
			
			@ -510,10 +506,9 @@ pub fn alerts_to_err(alerts: Option<Vec<Alert>>) -> ExtractionError {
 | 
			
		|||
                .into_iter()
 | 
			
		||||
                .map(|a| a.alert_renderer.text)
 | 
			
		||||
                .collect::<Vec<_>>()
 | 
			
		||||
                .join(" ")
 | 
			
		||||
                .into(),
 | 
			
		||||
                .join(" "),
 | 
			
		||||
        ),
 | 
			
		||||
        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 {
 | 
			
		||||
    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 {
 | 
			
		||||
| 
						 | 
				
			
			@ -582,7 +577,7 @@ impl TryFromWLang<CompactVideoRenderer> for model::RecommendedVideo {
 | 
			
		|||
    fn from_w_lang(
 | 
			
		||||
        video: CompactVideoRenderer,
 | 
			
		||||
        lang: Language,
 | 
			
		||||
    ) -> Result<Self, util::MappingError> {
 | 
			
		||||
    ) -> core::result::Result<Self, util::MappingError> {
 | 
			
		||||
        let channel = model::ChannelId::try_from(video.channel)?;
 | 
			
		||||
 | 
			
		||||
        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::{
 | 
			
		||||
    deobfuscate::Deobfuscator,
 | 
			
		||||
    error::{Error, ExtractionError},
 | 
			
		||||
    model::{Paginator, SearchItem, SearchResult, SearchVideo},
 | 
			
		||||
    model::{
 | 
			
		||||
        ChannelId, ChannelTag, Paginator, SearchChannel, SearchItem, SearchPlaylist,
 | 
			
		||||
        SearchPlaylistVideo, SearchResult, SearchVideo,
 | 
			
		||||
    },
 | 
			
		||||
    param::{search_filter::SearchFilter, Language},
 | 
			
		||||
    util::TryRemove,
 | 
			
		||||
    timeago,
 | 
			
		||||
    util::{self, TryRemove},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use super::{
 | 
			
		||||
    response::{self, TryFromWLang},
 | 
			
		||||
    response::{self, IsLive, IsShort},
 | 
			
		||||
    ClientType, MapResponse, MapResult, QContinuation, RustyPipeQuery, YTContext,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -179,20 +183,86 @@ fn map_search_items(
 | 
			
		|||
    let mapped_items = items
 | 
			
		||||
        .into_iter()
 | 
			
		||||
        .filter_map(|item| match item {
 | 
			
		||||
            response::search::SearchItem::VideoRenderer(video) => {
 | 
			
		||||
                match SearchVideo::from_w_lang(video, lang) {
 | 
			
		||||
                    Ok(video) => Some(SearchItem::Video(video)),
 | 
			
		||||
            response::search::SearchItem::VideoRenderer(mut video) => {
 | 
			
		||||
                match ChannelId::try_from(video.channel) {
 | 
			
		||||
                    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) => {
 | 
			
		||||
                        warnings.push(e.to_string());
 | 
			
		||||
                        None
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            response::search::SearchItem::PlaylistRenderer(playlist) => {
 | 
			
		||||
                Some(SearchItem::Playlist(playlist.into()))
 | 
			
		||||
            response::search::SearchItem::PlaylistRenderer(mut playlist) => {
 | 
			
		||||
                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) => {
 | 
			
		||||
                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 } => {
 | 
			
		||||
                c_query = Some(corrected_query);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -516,7 +516,11 @@ fn map_comment(
 | 
			
		|||
                }),
 | 
			
		||||
                _ => 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,
 | 
			
		||||
            like_count: util::parse_numeric_or_warn(
 | 
			
		||||
                &c.action_buttons
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,7 @@
 | 
			
		|||
use std::borrow::Cow;
 | 
			
		||||
 | 
			
		||||
pub(crate) type Result<T> = core::result::Result<T, Error>;
 | 
			
		||||
 | 
			
		||||
/// Custom error type for the RustyPipe library
 | 
			
		||||
#[derive(thiserror::Error, Debug)]
 | 
			
		||||
#[non_exhaustive]
 | 
			
		||||
| 
						 | 
				
			
			@ -73,8 +75,10 @@ pub enum ExtractionError {
 | 
			
		|||
    VideoUnavailable(&'static str, String),
 | 
			
		||||
    #[error("Video is age restricted")]
 | 
			
		||||
    VideoAgeRestricted,
 | 
			
		||||
    #[error("Content is not available. Reason: {0}")]
 | 
			
		||||
    ContentUnavailable(Cow<'static, str>),
 | 
			
		||||
    #[error("Content is not available. Reason (from YT): {0}")]
 | 
			
		||||
    ContentUnavailable(String),
 | 
			
		||||
    #[error("Got no data from YouTube")]
 | 
			
		||||
    NoData,
 | 
			
		||||
    #[error("deserialization error: {0}")]
 | 
			
		||||
    Deserialization(#[from] serde_json::Error),
 | 
			
		||||
    #[error("got invalid data from YT: {0}")]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,7 +11,7 @@ use log::error;
 | 
			
		|||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
use crate::deobfuscate::DeobfData;
 | 
			
		||||
use crate::error::Error;
 | 
			
		||||
use crate::error::{Error, Result};
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
#[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")?;
 | 
			
		||||
        serde_json::to_writer_pretty(&File::create(report_path)?, &report)
 | 
			
		||||
            .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() {
 | 
			
		||||
        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())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
///
 | 
			
		||||
/// Returns None if the date could not be parsed.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,7 +15,7 @@ use once_cell::sync::Lazy;
 | 
			
		|||
use rand::Rng;
 | 
			
		||||
use url::Url;
 | 
			
		||||
 | 
			
		||||
use crate::{error::Error, param::Language};
 | 
			
		||||
use crate::{error::Error, error::Result, param::Language};
 | 
			
		||||
 | 
			
		||||
const CONTENT_PLAYBACK_NONCE_ALPHABET: &[u8; 64] =
 | 
			
		||||
    b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
 | 
			
		||||
| 
						 | 
				
			
			@ -57,7 +57,7 @@ pub fn generate_content_playback_nonce() -> String {
 | 
			
		|||
/// Example:
 | 
			
		||||
///
 | 
			
		||||
/// `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)
 | 
			
		||||
        .map_err(|e| Error::Other(format!("could not parse url `{}` err: {}", url, e).into()))?;
 | 
			
		||||
    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
 | 
			
		||||
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
 | 
			
		||||
    F: FromStr,
 | 
			
		||||
{
 | 
			
		||||
| 
						 | 
				
			
			@ -147,6 +147,14 @@ where
 | 
			
		|||
    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(
 | 
			
		||||
    n_past_retries: u32,
 | 
			
		||||
    min_retry_interval: u32,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										124
									
								
								tests/youtube.rs
									
										
									
									
									
								
							
							
						
						
									
										124
									
								
								tests/youtube.rs
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
use chrono::Datelike;
 | 
			
		||||
use chrono::{Datelike, Timelike};
 | 
			
		||||
use rstest::rstest;
 | 
			
		||||
 | 
			
		||||
use rustypipe::client::{ClientType, RustyPipe};
 | 
			
		||||
| 
						 | 
				
			
			@ -218,14 +218,10 @@ async fn playlist_not_found() {
 | 
			
		|||
        .await
 | 
			
		||||
        .unwrap_err();
 | 
			
		||||
 | 
			
		||||
    assert!(
 | 
			
		||||
        matches!(
 | 
			
		||||
            err,
 | 
			
		||||
            Error::Extraction(ExtractionError::ContentUnavailable(_))
 | 
			
		||||
        ),
 | 
			
		||||
        "got: {}",
 | 
			
		||||
        err
 | 
			
		||||
    );
 | 
			
		||||
    assert!(matches!(
 | 
			
		||||
        err,
 | 
			
		||||
        Error::Extraction(ExtractionError::ContentUnavailable(_))
 | 
			
		||||
    ));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//#VIDEO DETAILS
 | 
			
		||||
| 
						 | 
				
			
			@ -328,7 +324,6 @@ async fn get_video_details_music() {
 | 
			
		|||
    assert!(!details.is_live);
 | 
			
		||||
    assert!(!details.is_ccommons);
 | 
			
		||||
 | 
			
		||||
    assert!(!details.recommended.items.is_empty());
 | 
			
		||||
    assert!(!details.recommended.is_exhausted());
 | 
			
		||||
 | 
			
		||||
    // Comments are disabled for this video
 | 
			
		||||
| 
						 | 
				
			
			@ -386,7 +381,6 @@ async fn get_video_details_ccommons() {
 | 
			
		|||
    assert!(!details.is_live);
 | 
			
		||||
    assert!(details.is_ccommons);
 | 
			
		||||
 | 
			
		||||
    assert!(!details.recommended.items.is_empty());
 | 
			
		||||
    assert!(!details.recommended.is_exhausted());
 | 
			
		||||
 | 
			
		||||
    assert!(
 | 
			
		||||
| 
						 | 
				
			
			@ -526,7 +520,6 @@ async fn get_video_details_chapters() {
 | 
			
		|||
        "###);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    assert!(!details.recommended.items.is_empty());
 | 
			
		||||
    assert!(!details.recommended.is_exhausted());
 | 
			
		||||
 | 
			
		||||
    assert!(
 | 
			
		||||
| 
						 | 
				
			
			@ -586,7 +579,6 @@ async fn get_video_details_live() {
 | 
			
		|||
    assert!(details.is_live);
 | 
			
		||||
    assert!(!details.is_ccommons);
 | 
			
		||||
 | 
			
		||||
    assert!(!details.recommended.items.is_empty());
 | 
			
		||||
    assert!(!details.recommended.is_exhausted());
 | 
			
		||||
 | 
			
		||||
    // No comments because livestream
 | 
			
		||||
| 
						 | 
				
			
			@ -645,14 +637,10 @@ async fn get_video_details_not_found() {
 | 
			
		|||
    let rp = RustyPipe::builder().strict().build();
 | 
			
		||||
    let err = rp.query().video_details("abcdefgLi5X").await.unwrap_err();
 | 
			
		||||
 | 
			
		||||
    assert!(
 | 
			
		||||
        matches!(
 | 
			
		||||
            err,
 | 
			
		||||
            Error::Extraction(ExtractionError::ContentUnavailable(_))
 | 
			
		||||
        ),
 | 
			
		||||
        "got: {}",
 | 
			
		||||
        err
 | 
			
		||||
    )
 | 
			
		||||
    assert!(matches!(
 | 
			
		||||
        err,
 | 
			
		||||
        Error::Extraction(ExtractionError::ContentUnavailable(_))
 | 
			
		||||
    ))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[tokio::test]
 | 
			
		||||
| 
						 | 
				
			
			@ -849,15 +837,13 @@ fn assert_channel_eevblog<T>(channel: &Channel<T>) {
 | 
			
		|||
#[rstest]
 | 
			
		||||
#[case::artist("UC_vmjW5e1xEHhYjY2a0kK1A", "Oonagh - Topic", false, false)]
 | 
			
		||||
#[case::shorts("UCh8gHdtzO2tXd593_bjErWg", "Doobydobap", true, true)]
 | 
			
		||||
#[case::livestream(
 | 
			
		||||
#[case::live(
 | 
			
		||||
    "UChs0pSaEoNLV4mevBFGaoKA",
 | 
			
		||||
    "The Good Life Radio x Sensual Musique",
 | 
			
		||||
    true,
 | 
			
		||||
    true
 | 
			
		||||
)]
 | 
			
		||||
#[case::music("UC-9-kyTW8ZkZNDHQJ6FgpwQ", "Music", false, false)]
 | 
			
		||||
#[case::live("UC4R8DWoMoI7CAwX8_LjQHig", "Live", false, false)]
 | 
			
		||||
#[case::news("UCYfdidRxbB8Qhf0Nx7ioOYw", "News", false, false)]
 | 
			
		||||
#[tokio::test]
 | 
			
		||||
async fn channel_more(
 | 
			
		||||
    #[case] id: &str,
 | 
			
		||||
| 
						 | 
				
			
			@ -896,72 +882,58 @@ async fn channel_more(
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
#[rstest]
 | 
			
		||||
#[case::not_exist("UCOpNcN46UbXVtpKMrmU4Abx")]
 | 
			
		||||
#[case::gaming("UCOpNcN46UbXVtpKMrmU4Abg")]
 | 
			
		||||
#[case::movies("UCuJcl0Ju-gPDoksRjK1ya-w")]
 | 
			
		||||
#[case::sports("UCEgdi0XIXXZ-qJOFPf4JSKw")]
 | 
			
		||||
#[case::learning("UCtFRv9O2AHqOZjjynzrv-xg")]
 | 
			
		||||
#[case::gaming("UCOpNcN46UbXVtpKMrmU4Abg", false)]
 | 
			
		||||
#[case::not_found("UCOpNcN46UbXVtpKMrmU4Abx", true)]
 | 
			
		||||
#[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 err = rp.query().channel_videos(&id).await.unwrap_err();
 | 
			
		||||
 | 
			
		||||
    assert!(
 | 
			
		||||
        matches!(
 | 
			
		||||
    if not_found {
 | 
			
		||||
        assert!(matches!(
 | 
			
		||||
            err,
 | 
			
		||||
            Error::Extraction(ExtractionError::ContentUnavailable(_))
 | 
			
		||||
        ),
 | 
			
		||||
        "got: {}",
 | 
			
		||||
        err
 | 
			
		||||
    );
 | 
			
		||||
        ));
 | 
			
		||||
    } else {
 | 
			
		||||
        assert!(matches!(err, Error::Extraction(ExtractionError::NoData)));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//#CHANNEL_RSS
 | 
			
		||||
 | 
			
		||||
#[cfg(feature = "rss")]
 | 
			
		||||
mod channel_rss {
 | 
			
		||||
    use super::*;
 | 
			
		||||
#[tokio::test]
 | 
			
		||||
async fn get_channel_rss() {
 | 
			
		||||
    let rp = RustyPipe::builder().strict().build();
 | 
			
		||||
    let channel = rp
 | 
			
		||||
        .query()
 | 
			
		||||
        .channel_rss("UCHnyfMqiRRG1u-2MsSQLbXA")
 | 
			
		||||
        .await
 | 
			
		||||
        .unwrap();
 | 
			
		||||
 | 
			
		||||
    use chrono::Timelike;
 | 
			
		||||
    assert_eq!(channel.id, "UCHnyfMqiRRG1u-2MsSQLbXA");
 | 
			
		||||
    assert_eq!(channel.name, "Veritasium");
 | 
			
		||||
    assert_eq!(channel.create_date.year(), 2010);
 | 
			
		||||
    assert_eq!(channel.create_date.month(), 7);
 | 
			
		||||
    assert_eq!(channel.create_date.day(), 21);
 | 
			
		||||
    assert_eq!(channel.create_date.hour(), 7);
 | 
			
		||||
    assert_eq!(channel.create_date.minute(), 18);
 | 
			
		||||
 | 
			
		||||
    #[tokio::test]
 | 
			
		||||
    async fn get_channel_rss() {
 | 
			
		||||
        let rp = RustyPipe::builder().strict().build();
 | 
			
		||||
        let channel = rp
 | 
			
		||||
            .query()
 | 
			
		||||
            .channel_rss("UCHnyfMqiRRG1u-2MsSQLbXA")
 | 
			
		||||
            .await
 | 
			
		||||
            .unwrap();
 | 
			
		||||
    assert!(!channel.videos.is_empty());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
        assert_eq!(channel.id, "UCHnyfMqiRRG1u-2MsSQLbXA");
 | 
			
		||||
        assert_eq!(channel.name, "Veritasium");
 | 
			
		||||
        assert_eq!(channel.create_date.year(), 2010);
 | 
			
		||||
        assert_eq!(channel.create_date.month(), 7);
 | 
			
		||||
        assert_eq!(channel.create_date.day(), 21);
 | 
			
		||||
        assert_eq!(channel.create_date.hour(), 7);
 | 
			
		||||
        assert_eq!(channel.create_date.minute(), 18);
 | 
			
		||||
#[tokio::test]
 | 
			
		||||
async fn get_channel_rss_not_found() {
 | 
			
		||||
    let rp = RustyPipe::builder().strict().build();
 | 
			
		||||
    let err = rp
 | 
			
		||||
        .query()
 | 
			
		||||
        .channel_rss("UCHnyfMqiRRG1u-2MsSQLbXZ")
 | 
			
		||||
        .await
 | 
			
		||||
        .unwrap_err();
 | 
			
		||||
 | 
			
		||||
        assert!(!channel.videos.is_empty());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[tokio::test]
 | 
			
		||||
    async fn get_channel_rss_not_found() {
 | 
			
		||||
        let rp = RustyPipe::builder().strict().build();
 | 
			
		||||
        let err = rp
 | 
			
		||||
            .query()
 | 
			
		||||
            .channel_rss("UCHnyfMqiRRG1u-2MsSQLbXZ")
 | 
			
		||||
            .await
 | 
			
		||||
            .unwrap_err();
 | 
			
		||||
 | 
			
		||||
        assert!(
 | 
			
		||||
            matches!(
 | 
			
		||||
                err,
 | 
			
		||||
                Error::Extraction(ExtractionError::ContentUnavailable(_))
 | 
			
		||||
            ),
 | 
			
		||||
            "got: {}",
 | 
			
		||||
            err
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    assert!(matches!(
 | 
			
		||||
        err,
 | 
			
		||||
        Error::Extraction(ExtractionError::ContentUnavailable(_))
 | 
			
		||||
    ));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//#SEARCH
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue