feat: add fallback to player query
This commit is contained in:
parent
01b9c8e310
commit
bbaa6cdb90
7 changed files with 204 additions and 31 deletions
|
|
@ -15,6 +15,7 @@ inspired by [NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor).
|
||||||
- [X] **Search** (with filters)
|
- [X] **Search** (with filters)
|
||||||
- [ ] **Search suggestions**
|
- [ ] **Search suggestions**
|
||||||
- [ ] **Trending**
|
- [ ] **Trending**
|
||||||
|
- [ ] **URL resolver**
|
||||||
|
|
||||||
### YouTube Music
|
### YouTube Music
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,7 @@ use clap::{Parser, Subcommand};
|
||||||
use futures::stream::{self, StreamExt};
|
use futures::stream::{self, StreamExt};
|
||||||
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
||||||
use reqwest::{Client, ClientBuilder};
|
use reqwest::{Client, ClientBuilder};
|
||||||
use rustypipe::{
|
use rustypipe::{client::RustyPipe, param::StreamFilter};
|
||||||
client::{ClientType, RustyPipe},
|
|
||||||
param::StreamFilter,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[clap(author, version, about, long_about = None)]
|
#[clap(author, version, about, long_about = None)]
|
||||||
|
|
@ -58,14 +55,10 @@ async fn download_single_video(
|
||||||
pb.set_message(format!("Fetching player data for {}", video_title));
|
pb.set_message(format!("Fetching player data for {}", video_title));
|
||||||
|
|
||||||
let res = async {
|
let res = async {
|
||||||
let player_data = rp
|
let player_data = rp.query().player(video_id.as_str()).await.context(format!(
|
||||||
.query()
|
"Failed to fetch player data for video {}",
|
||||||
.player(video_id.as_str(), ClientType::TvHtml5Embed)
|
video_id
|
||||||
.await
|
))?;
|
||||||
.context(format!(
|
|
||||||
"Failed to fetch player data for video {}",
|
|
||||||
video_id
|
|
||||||
))?;
|
|
||||||
|
|
||||||
let mut filter = StreamFilter::default();
|
let mut filter = StreamFilter::default();
|
||||||
if let Some(res) = resolution {
|
if let Some(res) = resolution {
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,10 @@ async fn player(testfiles: &Path) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let rp = rp_testfile(&json_path);
|
let rp = rp_testfile(&json_path);
|
||||||
rp.query().player(video_id, client_type).await.unwrap();
|
rp.query()
|
||||||
|
.player_from_client(video_id, client_type)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,7 +108,11 @@ async fn player_model(testfiles: &Path) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let player_data = rp.query().player(id, ClientType::Desktop).await.unwrap();
|
let player_data = rp
|
||||||
|
.query()
|
||||||
|
.player_from_client(id, ClientType::Desktop)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let file = File::create(&json_path).unwrap();
|
let file = File::create(&json_path).unwrap();
|
||||||
serde_json::to_writer_pretty(file, &player_data).unwrap();
|
serde_json::to_writer_pretty(file, &player_data).unwrap();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
CC Video: pPvd8UxmSbQ
|
CCommons Video: pPvd8UxmSbQ
|
||||||
Video: ZeerrnuLi5E
|
Video: ZeerrnuLi5E
|
||||||
4K HDR: LXb3EKWsInQ
|
4K HDR: LXb3EKWsInQ
|
||||||
8K: Zv11L-ZfrSg
|
8K: Zv11L-ZfrSg
|
||||||
|
|
@ -6,7 +6,7 @@ Music: ihUZMeYFZHA
|
||||||
Multilanguage: tVWWp1PqDus
|
Multilanguage: tVWWp1PqDus
|
||||||
|
|
||||||
# Livestreams
|
# Livestreams
|
||||||
Live: 64DYi_8ESh0
|
Live: 86YLFOog4GM
|
||||||
Was live: pxY4OXVyMe4
|
Was live: pxY4OXVyMe4
|
||||||
|
|
||||||
# Errors
|
# Errors
|
||||||
|
|
@ -15,6 +15,7 @@ Censored: 6SJNVb0GnPI
|
||||||
Geoblocked: sJL6WA-aGkQ (Japan only)
|
Geoblocked: sJL6WA-aGkQ (Japan only)
|
||||||
Private: s7_qI6_mIXc
|
Private: s7_qI6_mIXc
|
||||||
DRM: 1bfOsni7EgI
|
DRM: 1bfOsni7EgI
|
||||||
|
Deleted: 64DYi_8ESh0
|
||||||
|
|
||||||
Album with unknown artists: https://music.youtube.com/playlist?list=OLAK5uy_mEX9ljZeeEWgTM1xLL1isyiGaWXoPyoOk
|
Album with unknown artists: https://music.youtube.com/playlist?list=OLAK5uy_mEX9ljZeeEWgTM1xLL1isyiGaWXoPyoOk
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,23 @@ struct QContentPlaybackContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
pub async fn player(
|
pub async fn player(self, video_id: &str) -> Result<VideoPlayer, Error> {
|
||||||
|
let q1 = self.clone();
|
||||||
|
let android_res = q1.player_from_client(video_id, ClientType::Android).await;
|
||||||
|
|
||||||
|
match android_res {
|
||||||
|
Ok(res) => Ok(res),
|
||||||
|
Err(Error::Extraction(
|
||||||
|
ExtractionError::VideoAgeRestricted | ExtractionError::WrongResult(_),
|
||||||
|
)) => {
|
||||||
|
self.player_from_client(video_id, ClientType::TvHtml5Embed)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn player_from_client(
|
||||||
self,
|
self,
|
||||||
video_id: &str,
|
video_id: &str,
|
||||||
client_type: ClientType,
|
client_type: ClientType,
|
||||||
|
|
@ -129,7 +145,11 @@ impl MapResponse<VideoPlayer> for response::Player {
|
||||||
}
|
}
|
||||||
response::player::PlayabilityStatus::LoginRequired { reason } => {
|
response::player::PlayabilityStatus::LoginRequired { reason } => {
|
||||||
// reason: "Sign in to confirm your age"
|
// reason: "Sign in to confirm your age"
|
||||||
if reason.split_whitespace().any(|word| word == "age") {
|
// or: "This video may be inappropriate for some users."
|
||||||
|
if reason
|
||||||
|
.split_whitespace()
|
||||||
|
.any(|word| word == "age" || word == "inappropriate")
|
||||||
|
{
|
||||||
return Err(ExtractionError::VideoAgeRestricted);
|
return Err(ExtractionError::VideoAgeRestricted);
|
||||||
}
|
}
|
||||||
return Err(ExtractionError::VideoUnavailable("private video", reason));
|
return Err(ExtractionError::VideoUnavailable("private video", reason));
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,8 @@ pub struct VideoPlayerDetails {
|
||||||
/// Video description in plaintext format
|
/// Video description in plaintext format
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
/// Video length in seconds
|
/// Video length in seconds
|
||||||
|
///
|
||||||
|
/// Is zero for livestreams
|
||||||
pub length: u32,
|
pub length: u32,
|
||||||
/// Video thumbnail
|
/// Video thumbnail
|
||||||
pub thumbnail: Vec<Thumbnail>,
|
pub thumbnail: Vec<Thumbnail>,
|
||||||
|
|
|
||||||
175
tests/youtube.rs
175
tests/youtube.rs
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use chrono::Datelike;
|
use chrono::Datelike;
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
|
|
@ -20,9 +22,13 @@ use rustypipe::param::{
|
||||||
#[case::android(ClientType::Android)]
|
#[case::android(ClientType::Android)]
|
||||||
#[case::ios(ClientType::Ios)]
|
#[case::ios(ClientType::Ios)]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn get_player(#[case] client_type: ClientType) {
|
async fn get_player_from_client(#[case] client_type: ClientType) {
|
||||||
let rp = RustyPipe::builder().strict().build();
|
let rp = RustyPipe::builder().strict().build();
|
||||||
let player_data = rp.query().player("n4tK7LYFxI0", client_type).await.unwrap();
|
let player_data = rp
|
||||||
|
.query()
|
||||||
|
.player_from_client("n4tK7LYFxI0", client_type)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// dbg!(&player_data);
|
// dbg!(&player_data);
|
||||||
|
|
||||||
|
|
@ -39,7 +45,7 @@ async fn get_player(#[case] client_type: ClientType) {
|
||||||
assert!(!player_data.details.thumbnail.is_empty());
|
assert!(!player_data.details.thumbnail.is_empty());
|
||||||
assert_eq!(player_data.details.channel.id, "UC_aEa8K-EOJ3D6gOs7HcyNg");
|
assert_eq!(player_data.details.channel.id, "UC_aEa8K-EOJ3D6gOs7HcyNg");
|
||||||
assert_eq!(player_data.details.channel.name, "NoCopyrightSounds");
|
assert_eq!(player_data.details.channel.name, "NoCopyrightSounds");
|
||||||
assert!(player_data.details.view_count > 146818808);
|
assert!(player_data.details.view_count > 146_818_808);
|
||||||
assert_eq!(player_data.details.keywords[0], "spektrem");
|
assert_eq!(player_data.details.keywords[0], "spektrem");
|
||||||
assert_eq!(player_data.details.is_live_content, false);
|
assert_eq!(player_data.details.is_live_content, false);
|
||||||
|
|
||||||
|
|
@ -111,20 +117,163 @@ async fn get_player(#[case] client_type: ClientType) {
|
||||||
assert!(player_data.expires_in_seconds > 10000);
|
assert!(player_data.expires_in_seconds > 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case::desktop(ClientType::Desktop)]
|
#[case::music(
|
||||||
// #[case::tv_html5_embed(ClientType::TvHtml5Embed)]
|
"ihUZMeYFZHA",
|
||||||
// #[case::android(ClientType::Android)]
|
"Oonagh - Nan Úye",
|
||||||
// #[case::ios(ClientType::Ios)]
|
"Offizielle AlbumPlaylist:",
|
||||||
#[test_log::test(tokio::test)]
|
260,
|
||||||
async fn get_player_live(#[case] client_type: ClientType) {
|
"UC2llNlEM62gU-_fXPHfgbDg",
|
||||||
|
"Oonagh",
|
||||||
|
830900,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
)]
|
||||||
|
#[case::hdr(
|
||||||
|
"LXb3EKWsInQ",
|
||||||
|
"COSTA RICA IN 4K 60fps HDR (ULTRA HD)",
|
||||||
|
"We've re-mastered and re-uploaded our favorite video in HDR!",
|
||||||
|
314,
|
||||||
|
"UCYq-iAOSZBvoUxvfzwKIZWA",
|
||||||
|
"Jacob + Katie Schwarz",
|
||||||
|
220_000_000,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
)]
|
||||||
|
#[case::multilanguage(
|
||||||
|
"tVWWp1PqDus",
|
||||||
|
"100 Girls Vs 100 Boys For $500,000",
|
||||||
|
"Giving away $25k on Current!",
|
||||||
|
1013,
|
||||||
|
"UCX6OQ3DkcsbYNE6H8uQQuVA",
|
||||||
|
"MrBeast",
|
||||||
|
82_000_000,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
)]
|
||||||
|
#[case::live(
|
||||||
|
"86YLFOog4GM",
|
||||||
|
"🌎 Nasa Live Stream - Earth From Space : Live Views from the ISS",
|
||||||
|
"Live NASA - Views Of Earth from Space",
|
||||||
|
0,
|
||||||
|
"UCakgsb0w7QB0VHdnCc-OVEA",
|
||||||
|
"Space Videos",
|
||||||
|
10,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
)]
|
||||||
|
#[case::was_live(
|
||||||
|
"pxY4OXVyMe4",
|
||||||
|
"Minecraft GENESIS LIVESTREAM!!",
|
||||||
|
"FÜR MEHR LIVESTREAMS AUF YOUTUBE MACHT FOLGENDES",
|
||||||
|
5535,
|
||||||
|
"UCQM0bS4_04-Y4JuYrgmnpZQ",
|
||||||
|
"Chaosflo44",
|
||||||
|
500_000,
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
)]
|
||||||
|
#[case::agelimit(
|
||||||
|
"laru0QoJUmI",
|
||||||
|
"DJ Robin x Schürze - Layla (Official Video)",
|
||||||
|
"Endlich ist es soweit! Zwei Männer aus dem Schwabenland",
|
||||||
|
188,
|
||||||
|
"UCkJfSrMnLonOZWh-q5os5bg",
|
||||||
|
"Summerfield Records",
|
||||||
|
10_000_000,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
)]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_player(
|
||||||
|
#[case] id: &str,
|
||||||
|
#[case] title: &str,
|
||||||
|
#[case] description: &str,
|
||||||
|
#[case] length: u32,
|
||||||
|
#[case] channel_id: &str,
|
||||||
|
#[case] channel_name: &str,
|
||||||
|
#[case] views: u64,
|
||||||
|
#[case] is_live: bool,
|
||||||
|
#[case] is_live_content: bool,
|
||||||
|
) {
|
||||||
let rp = RustyPipe::builder().strict().build();
|
let rp = RustyPipe::builder().strict().build();
|
||||||
let player_data = rp.query().player("86YLFOog4GM", client_type).await.unwrap();
|
let player_data = rp.query().player(id).await.unwrap();
|
||||||
|
let details = player_data.details;
|
||||||
|
|
||||||
dbg!(&player_data);
|
assert_eq!(details.id, id);
|
||||||
|
assert_eq!(details.title, title);
|
||||||
|
let desc = details.description.unwrap();
|
||||||
|
assert!(desc.contains(description), "description: {}", desc);
|
||||||
|
assert_eq!(details.length, length);
|
||||||
|
assert_eq!(details.channel.id, channel_id);
|
||||||
|
assert_eq!(details.channel.name, channel_name);
|
||||||
|
assert!(
|
||||||
|
details.view_count > views,
|
||||||
|
"expected > {} views, got {}",
|
||||||
|
views,
|
||||||
|
details.view_count
|
||||||
|
);
|
||||||
|
assert_eq!(details.is_live, is_live);
|
||||||
|
assert_eq!(details.is_live_content, is_live_content);
|
||||||
|
|
||||||
|
if is_live {
|
||||||
|
assert!(player_data.hls_manifest_url.is_some());
|
||||||
|
assert!(player_data.dash_manifest_url.is_some());
|
||||||
|
} else {
|
||||||
|
assert!(!player_data.video_only_streams.is_empty());
|
||||||
|
assert!(!player_data.audio_streams.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
match id {
|
||||||
|
// HDR
|
||||||
|
"LXb3EKWsInQ" => {
|
||||||
|
assert!(
|
||||||
|
player_data
|
||||||
|
.video_only_streams
|
||||||
|
.iter()
|
||||||
|
.any(|stream| stream.hdr),
|
||||||
|
"no hdr streams"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Multilanguage
|
||||||
|
"tVWWp1PqDus" => {
|
||||||
|
let langs = player_data
|
||||||
|
.audio_streams
|
||||||
|
.iter()
|
||||||
|
.filter_map(|stream| {
|
||||||
|
stream
|
||||||
|
.track
|
||||||
|
.as_ref()
|
||||||
|
.map(|t| t.lang.as_ref().unwrap().to_owned())
|
||||||
|
})
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
|
for l in ["en", "es", "fr", "pt", "ru"] {
|
||||||
|
assert!(langs.contains(l), "missing lang: {}", l);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(player_data.expires_in_seconds > 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::not_found("86abcdefghi", "extraction error: Video cant be played because of deletion/censorship. Reason (from YT): This video is unavailable")]
|
||||||
|
#[case::deleted("64DYi_8ESh0", "extraction error: Video cant be played because of deletion/censorship. Reason (from YT): This video is unavailable")]
|
||||||
|
#[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.")]
|
||||||
|
// 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::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.")]
|
||||||
|
#[case::private("s7_qI6_mIXc", "extraction error: Video cant be played because of private video. Reason (from YT): This video is private")]
|
||||||
|
#[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")]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_player_error(#[case] id: &str, #[case] msg: &str) {
|
||||||
|
let rp = RustyPipe::builder().strict().build();
|
||||||
|
let err = rp.query().player(id).await.unwrap_err();
|
||||||
|
|
||||||
|
assert_eq!(err.to_string(), msg);
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
//#PLAYLIST
|
//#PLAYLIST
|
||||||
|
|
||||||
|
|
|
||||||
Reference in a new issue