feat: add fallback to player query

This commit is contained in:
ThetaDev 2022-10-12 23:53:48 +02:00
parent 01b9c8e310
commit bbaa6cdb90
7 changed files with 204 additions and 31 deletions

View file

@ -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

View file

@ -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 {

View file

@ -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();

View file

@ -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

View file

@ -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));

View file

@ -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>,

View file

@ -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