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
Reference in a new issue