Compare commits
	
		
			2 commits
		
	
	
		
			
				9d0ae0e9c2
			
			...
			
				4ebee5856e
			
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4ebee5856e | |||
| 264f82346c | 
					 4 changed files with 59 additions and 72 deletions
				
			
		| 
						 | 
					@ -7,7 +7,7 @@ use url::Url;
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    error::{Error, ExtractionError},
 | 
					    error::{Error, ExtractionError},
 | 
				
			||||||
    model::{Channel, ChannelInfo, Paginator, PlaylistItem, VideoItem},
 | 
					    model::{Channel, ChannelInfo, Paginator, PlaylistItem, VideoItem},
 | 
				
			||||||
    param::{ChannelOrder, Language},
 | 
					    param::Language,
 | 
				
			||||||
    serializer::MapResult,
 | 
					    serializer::MapResult,
 | 
				
			||||||
    timeago,
 | 
					    timeago,
 | 
				
			||||||
    util::{self, TryRemove},
 | 
					    util::{self, TryRemove},
 | 
				
			||||||
| 
						 | 
					@ -26,11 +26,7 @@ struct QChannel<'a> {
 | 
				
			||||||
#[derive(Debug, Serialize)]
 | 
					#[derive(Debug, Serialize)]
 | 
				
			||||||
enum Params {
 | 
					enum Params {
 | 
				
			||||||
    #[serde(rename = "EgZ2aWRlb3PyBgQKAjoA")]
 | 
					    #[serde(rename = "EgZ2aWRlb3PyBgQKAjoA")]
 | 
				
			||||||
    VideosLatest,
 | 
					    Videos,
 | 
				
			||||||
    #[serde(rename = "EgZ2aWRlb3MYAiAAMAE%3D")]
 | 
					 | 
				
			||||||
    VideosOldest,
 | 
					 | 
				
			||||||
    #[serde(rename = "EgZ2aWRlb3MYASAAMAE%3D")]
 | 
					 | 
				
			||||||
    VideosPopular,
 | 
					 | 
				
			||||||
    #[serde(rename = "EglwbGF5bGlzdHMgAQ%3D%3D")]
 | 
					    #[serde(rename = "EglwbGF5bGlzdHMgAQ%3D%3D")]
 | 
				
			||||||
    Playlists,
 | 
					    Playlists,
 | 
				
			||||||
    #[serde(rename = "EgVhYm91dPIGBAoCEgA%3D")]
 | 
					    #[serde(rename = "EgVhYm91dPIGBAoCEgA%3D")]
 | 
				
			||||||
| 
						 | 
					@ -41,25 +37,12 @@ impl RustyPipeQuery {
 | 
				
			||||||
    pub async fn channel_videos(
 | 
					    pub async fn channel_videos(
 | 
				
			||||||
        &self,
 | 
					        &self,
 | 
				
			||||||
        channel_id: &str,
 | 
					        channel_id: &str,
 | 
				
			||||||
    ) -> Result<Channel<Paginator<VideoItem>>, Error> {
 | 
					 | 
				
			||||||
        self.channel_videos_ordered(channel_id, ChannelOrder::default())
 | 
					 | 
				
			||||||
            .await
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn channel_videos_ordered(
 | 
					 | 
				
			||||||
        &self,
 | 
					 | 
				
			||||||
        channel_id: &str,
 | 
					 | 
				
			||||||
        order: ChannelOrder,
 | 
					 | 
				
			||||||
    ) -> Result<Channel<Paginator<VideoItem>>, Error> {
 | 
					    ) -> Result<Channel<Paginator<VideoItem>>, Error> {
 | 
				
			||||||
        let context = self.get_context(ClientType::Desktop, true, None).await;
 | 
					        let context = self.get_context(ClientType::Desktop, true, None).await;
 | 
				
			||||||
        let request_body = QChannel {
 | 
					        let request_body = QChannel {
 | 
				
			||||||
            context,
 | 
					            context,
 | 
				
			||||||
            browse_id: channel_id,
 | 
					            browse_id: channel_id,
 | 
				
			||||||
            params: match order {
 | 
					            params: Params::Videos,
 | 
				
			||||||
                ChannelOrder::Latest => Params::VideosLatest,
 | 
					 | 
				
			||||||
                ChannelOrder::Oldest => Params::VideosOldest,
 | 
					 | 
				
			||||||
                ChannelOrder::Popular => Params::VideosPopular,
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.execute_request::<response::Channel, _, _>(
 | 
					        self.execute_request::<response::Channel, _, _>(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -183,7 +183,6 @@ struct RustyPipeRef {
 | 
				
			||||||
    reporter: Option<Box<dyn Reporter>>,
 | 
					    reporter: Option<Box<dyn Reporter>>,
 | 
				
			||||||
    n_http_retries: u32,
 | 
					    n_http_retries: u32,
 | 
				
			||||||
    consent_cookie: String,
 | 
					    consent_cookie: String,
 | 
				
			||||||
    visitor_data: Option<String>,
 | 
					 | 
				
			||||||
    cache: CacheHolder,
 | 
					    cache: CacheHolder,
 | 
				
			||||||
    default_opts: RustyPipeOpts,
 | 
					    default_opts: RustyPipeOpts,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -194,6 +193,7 @@ struct RustyPipeOpts {
 | 
				
			||||||
    country: Country,
 | 
					    country: Country,
 | 
				
			||||||
    report: bool,
 | 
					    report: bool,
 | 
				
			||||||
    strict: bool,
 | 
					    strict: bool,
 | 
				
			||||||
 | 
					    visitor_data: Option<String>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub struct RustyPipeBuilder {
 | 
					pub struct RustyPipeBuilder {
 | 
				
			||||||
| 
						 | 
					@ -201,7 +201,6 @@ pub struct RustyPipeBuilder {
 | 
				
			||||||
    reporter: Option<Box<dyn Reporter>>,
 | 
					    reporter: Option<Box<dyn Reporter>>,
 | 
				
			||||||
    n_http_retries: u32,
 | 
					    n_http_retries: u32,
 | 
				
			||||||
    user_agent: String,
 | 
					    user_agent: String,
 | 
				
			||||||
    visitor_data: Option<String>,
 | 
					 | 
				
			||||||
    default_opts: RustyPipeOpts,
 | 
					    default_opts: RustyPipeOpts,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -218,6 +217,7 @@ impl Default for RustyPipeOpts {
 | 
				
			||||||
            country: Country::Us,
 | 
					            country: Country::Us,
 | 
				
			||||||
            report: false,
 | 
					            report: false,
 | 
				
			||||||
            strict: false,
 | 
					            strict: false,
 | 
				
			||||||
 | 
					            visitor_data: None,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -294,7 +294,6 @@ impl RustyPipeBuilder {
 | 
				
			||||||
            reporter: Some(Box::new(FileReporter::default())),
 | 
					            reporter: Some(Box::new(FileReporter::default())),
 | 
				
			||||||
            n_http_retries: 2,
 | 
					            n_http_retries: 2,
 | 
				
			||||||
            user_agent: DEFAULT_UA.to_owned(),
 | 
					            user_agent: DEFAULT_UA.to_owned(),
 | 
				
			||||||
            visitor_data: None,
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -335,7 +334,6 @@ impl RustyPipeBuilder {
 | 
				
			||||||
                    CONSENT_COOKIE_YES,
 | 
					                    CONSENT_COOKIE_YES,
 | 
				
			||||||
                    rand::thread_rng().gen_range(100..1000)
 | 
					                    rand::thread_rng().gen_range(100..1000)
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                visitor_data: self.visitor_data,
 | 
					 | 
				
			||||||
                cache: CacheHolder {
 | 
					                cache: CacheHolder {
 | 
				
			||||||
                    desktop_client: RwLock::new(cdata.desktop_client),
 | 
					                    desktop_client: RwLock::new(cdata.desktop_client),
 | 
				
			||||||
                    music_client: RwLock::new(cdata.music_client),
 | 
					                    music_client: RwLock::new(cdata.music_client),
 | 
				
			||||||
| 
						 | 
					@ -439,8 +437,15 @@ impl RustyPipeBuilder {
 | 
				
			||||||
        self
 | 
					        self
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Set the default YouTube visitor data cookie
 | 
				
			||||||
    pub fn visitor_data(mut self, visitor_data: &str) -> Self {
 | 
					    pub fn visitor_data(mut self, visitor_data: &str) -> Self {
 | 
				
			||||||
        self.visitor_data = Some(visitor_data.to_owned());
 | 
					        self.default_opts.visitor_data = Some(visitor_data.to_owned());
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Set the default YouTube visitor data cookie to an optional value
 | 
				
			||||||
 | 
					    pub fn visitor_data_opt(mut self, visitor_data: Option<String>) -> Self {
 | 
				
			||||||
 | 
					        self.default_opts.visitor_data = visitor_data;
 | 
				
			||||||
        self
 | 
					        self
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -748,6 +753,18 @@ impl RustyPipeQuery {
 | 
				
			||||||
        self
 | 
					        self
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Set the YouTube visitor data cookie
 | 
				
			||||||
 | 
					    pub fn visitor_data(mut self, visitor_data: &str) -> Self {
 | 
				
			||||||
 | 
					        self.opts.visitor_data = Some(visitor_data.to_owned());
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Set the YouTube visitor data cookie to an optional value
 | 
				
			||||||
 | 
					    pub fn visitor_data_opt(mut self, visitor_data: Option<String>) -> Self {
 | 
				
			||||||
 | 
					        self.opts.visitor_data = visitor_data;
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// Create a new context object, which is included in every request to
 | 
					    /// Create a new context object, which is included in every request to
 | 
				
			||||||
    /// the YouTube API and contains language, country and device parameters.
 | 
					    /// the YouTube API and contains language, country and device parameters.
 | 
				
			||||||
    ///
 | 
					    ///
 | 
				
			||||||
| 
						 | 
					@ -768,7 +785,7 @@ impl RustyPipeQuery {
 | 
				
			||||||
            true => self.opts.country,
 | 
					            true => self.opts.country,
 | 
				
			||||||
            false => Country::Us,
 | 
					            false => Country::Us,
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        let visitor_data = self.client.inner.visitor_data.as_deref().or(visitor_data);
 | 
					        let visitor_data = self.opts.visitor_data.as_deref().or(visitor_data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        match ctype {
 | 
					        match ctype {
 | 
				
			||||||
            ClientType::Desktop => YTContext {
 | 
					            ClientType::Desktop => YTContext {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,19 +7,6 @@ pub use locale::{Country, Language};
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
pub use stream_filter::StreamFilter;
 | 
					pub use stream_filter::StreamFilter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Channel video sort order
 | 
					 | 
				
			||||||
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
 | 
					 | 
				
			||||||
#[non_exhaustive]
 | 
					 | 
				
			||||||
pub enum ChannelOrder {
 | 
					 | 
				
			||||||
    /// Output the latest videos first
 | 
					 | 
				
			||||||
    #[default]
 | 
					 | 
				
			||||||
    Latest,
 | 
					 | 
				
			||||||
    /// Output the oldest videos first
 | 
					 | 
				
			||||||
    Oldest,
 | 
					 | 
				
			||||||
    /// Output the most viewed videos first
 | 
					 | 
				
			||||||
    Popular,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// YouTube API endpoint to fetch continuations from
 | 
					/// YouTube API endpoint to fetch continuations from
 | 
				
			||||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
 | 
					#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
 | 
				
			||||||
#[serde(rename_all = "lowercase")]
 | 
					#[serde(rename_all = "lowercase")]
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,10 +10,7 @@ use rustypipe::model::richtext::ToPlaintext;
 | 
				
			||||||
use rustypipe::model::{
 | 
					use rustypipe::model::{
 | 
				
			||||||
    AudioCodec, AudioFormat, Channel, UrlTarget, Verification, VideoCodec, VideoFormat, YouTubeItem,
 | 
					    AudioCodec, AudioFormat, Channel, UrlTarget, Verification, VideoCodec, VideoFormat, YouTubeItem,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use rustypipe::param::{
 | 
					use rustypipe::param::search_filter::{self, SearchFilter};
 | 
				
			||||||
    search_filter::{self, SearchFilter},
 | 
					 | 
				
			||||||
    ChannelOrder,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
//#PLAYER
 | 
					//#PLAYER
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -260,20 +257,41 @@ async fn get_player(
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[rstest]
 | 
					#[rstest]
 | 
				
			||||||
#[case::not_found("86abcdefghi", "extraction error: Video cant be played because of deletion/censorship. Reason (from YT): This video is unavailable")]
 | 
					#[case::not_found(
 | 
				
			||||||
#[case::deleted("64DYi_8ESh0", "extraction error: Video cant be played because of deletion/censorship. Reason (from YT): This video is unavailable")]
 | 
					    "86abcdefghi",
 | 
				
			||||||
#[case::censored("6SJNVb0GnPI", "extraction error: Video cant be played because of deletion/censorship. Reason (from YT): This video has been removed for violating YouTube's policy on hate speech. Learn more about combating hate speech in your country.")]
 | 
					    "extraction error: Video cant be played because of deletion/censorship. Reason (from YT): "
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					#[case::deleted(
 | 
				
			||||||
 | 
					    "64DYi_8ESh0",
 | 
				
			||||||
 | 
					    "extraction error: Video cant be played because of deletion/censorship. Reason (from YT): "
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					#[case::censored(
 | 
				
			||||||
 | 
					    "6SJNVb0GnPI",
 | 
				
			||||||
 | 
					    "extraction error: Video cant be played because of deletion/censorship. Reason (from YT): "
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
// This video is geoblocked outside of Japan, so expect this test case to fail when using a Japanese IP address.
 | 
					// This video is geoblocked outside of Japan, so expect this test case to fail when using a Japanese IP address.
 | 
				
			||||||
#[case::geoblock("sJL6WA-aGkQ", "extraction error: Video cant be played because of DRM/Geoblock. Reason (from YT): The uploader has not made this video available in your country")]
 | 
					#[case::geoblock(
 | 
				
			||||||
#[case::drm("1bfOsni7EgI", "extraction error: Video cant be played because of DRM/Geoblock. Reason (from YT): This video can only be played on newer versions of Android or other supported devices.")]
 | 
					    "sJL6WA-aGkQ",
 | 
				
			||||||
#[case::private("s7_qI6_mIXc", "extraction error: Video cant be played because of private video. Reason (from YT): This video is private")]
 | 
					    "extraction error: Video cant be played because of DRM/Geoblock. Reason (from YT): "
 | 
				
			||||||
#[case::t1("CUO8secmc0g", "extraction error: Video cant be played because of DRM/Geoblock. Reason (from YT): Playback on other websites has been disabled by the video owner")]
 | 
					)]
 | 
				
			||||||
 | 
					#[case::drm(
 | 
				
			||||||
 | 
					    "1bfOsni7EgI",
 | 
				
			||||||
 | 
					    "extraction error: Video cant be played because of DRM/Geoblock. Reason (from YT): "
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					#[case::private(
 | 
				
			||||||
 | 
					    "s7_qI6_mIXc",
 | 
				
			||||||
 | 
					    "extraction error: Video cant be played because of private video. Reason (from YT): "
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					#[case::t1(
 | 
				
			||||||
 | 
					    "CUO8secmc0g",
 | 
				
			||||||
 | 
					    "extraction error: Video cant be played because of DRM/Geoblock. Reason (from YT): "
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
#[tokio::test]
 | 
					#[tokio::test]
 | 
				
			||||||
async fn get_player_error(#[case] id: &str, #[case] msg: &str) {
 | 
					async fn get_player_error(#[case] id: &str, #[case] msg: &str) {
 | 
				
			||||||
    let rp = RustyPipe::builder().strict().build();
 | 
					    let rp = RustyPipe::builder().strict().build();
 | 
				
			||||||
    let err = rp.query().player(id).await.unwrap_err();
 | 
					    let err = rp.query().player(id).await.unwrap_err();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    assert_eq!(err.to_string(), msg);
 | 
					    assert!(err.to_string().starts_with(msg), "got error msg: {}", err);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//#PLAYLIST
 | 
					//#PLAYLIST
 | 
				
			||||||
| 
						 | 
					@ -860,16 +878,12 @@ async fn get_video_comments() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//#CHANNEL
 | 
					//#CHANNEL
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[rstest]
 | 
					 | 
				
			||||||
#[case::latest(ChannelOrder::Latest)]
 | 
					 | 
				
			||||||
#[case::oldest(ChannelOrder::Oldest)]
 | 
					 | 
				
			||||||
#[case::popular(ChannelOrder::Popular)]
 | 
					 | 
				
			||||||
#[tokio::test]
 | 
					#[tokio::test]
 | 
				
			||||||
async fn channel_videos(#[case] order: ChannelOrder) {
 | 
					async fn channel_videos() {
 | 
				
			||||||
    let rp = RustyPipe::builder().strict().build();
 | 
					    let rp = RustyPipe::builder().strict().build();
 | 
				
			||||||
    let channel = rp
 | 
					    let channel = rp
 | 
				
			||||||
        .query()
 | 
					        .query()
 | 
				
			||||||
        .channel_videos_ordered("UC2DjFE7Xf11URZqWBigcVOQ", order)
 | 
					        .channel_videos("UC2DjFE7Xf11URZqWBigcVOQ")
 | 
				
			||||||
        .await
 | 
					        .await
 | 
				
			||||||
        .unwrap();
 | 
					        .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -885,21 +899,7 @@ async fn channel_videos(#[case] order: ChannelOrder) {
 | 
				
			||||||
    let first_video_date = first_video.publish_date.unwrap();
 | 
					    let first_video_date = first_video.publish_date.unwrap();
 | 
				
			||||||
    let age_days = (OffsetDateTime::now_utc() - first_video_date).whole_days();
 | 
					    let age_days = (OffsetDateTime::now_utc() - first_video_date).whole_days();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    match order {
 | 
					    assert!(age_days < 60, "latest video older than 60 days");
 | 
				
			||||||
        ChannelOrder::Latest => {
 | 
					 | 
				
			||||||
            assert!(age_days < 60, "latest video older than 60 days")
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        ChannelOrder::Oldest => {
 | 
					 | 
				
			||||||
            assert!(age_days > 4700, "oldest video newer than 4700 days")
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        ChannelOrder::Popular => {
 | 
					 | 
				
			||||||
            assert!(
 | 
					 | 
				
			||||||
                first_video.view_count.unwrap() > 2300000,
 | 
					 | 
				
			||||||
                "most popular video < 2.3M views"
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        _ => unimplemented!(),
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let next = channel.content.next(&rp.query()).await.unwrap().unwrap();
 | 
					    let next = channel.content.next(&rp.query()).await.unwrap().unwrap();
 | 
				
			||||||
    assert!(
 | 
					    assert!(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue