Skip to content

Commit d4798aa

Browse files
committed
10.08.2024
* if PO token is detected fallback to the IOS client to fetch stream data * `boa_engine` updated to 0.19.0 * MSRV now 1.79.0 * Code quality improved
1 parent dd8beae commit d4798aa

15 files changed

+218
-105
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/target
22
Cargo.lock
3-
flamegraph.html
3+
flamegraph.html
4+
*.mp3

Cargo.toml

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rusty_ytdl"
3-
version = "0.7.3"
3+
version = "0.7.4"
44
authors = ["Mithronn"]
55
edition = "2021"
66
description = "A Rust library for Youtube video searcher and downloader"
@@ -11,7 +11,7 @@ keywords = ["youtube", "downloader", "ytdl", "youtube-dl", "searcher"]
1111
include = ["src/**/*", "Cargo.toml"]
1212
exclude = ["examples/**/*", "tests/**/*", ".github/**/*"]
1313
categories = ["multimedia::video", "multimedia::audio"]
14-
rust-version = "1.70.0"
14+
rust-version = "1.79.0"
1515

1616
[package.metadata.docs.rs]
1717
all-features = true
@@ -26,29 +26,29 @@ reqwest = { version = "0.12.5", features = [
2626
"cookies",
2727
"gzip",
2828
], default-features = false }
29-
scraper = "0.19.1"
30-
serde = "1.0.204"
31-
serde_json = "1.0.120"
29+
scraper = "0.20.0"
30+
serde = "1.0.205"
31+
serde_json = "1.0.122"
3232
serde_qs = "0.13.0"
33-
regex = "1.10.5"
33+
regex = "1.10.6"
3434
url = "2.5.2"
3535
urlencoding = "2.1.3"
3636
thiserror = "1.0.63"
37-
derive_more = "0.99.18"
37+
derive_more = { version = "1.0.0", features = ["display"] }
3838
derivative = "2.2.0"
3939
once_cell = "1.19.0"
4040
tokio = { version = "1.39.2", default-features = false, features = ["sync"] }
4141
rand = "0.8.5"
42-
reqwest-middleware = { version = "0.3.2", features = ["json"] }
43-
reqwest-retry = "0.6.0"
42+
reqwest-middleware = { version = "0.3.3", features = ["json"] }
43+
reqwest-retry = "0.6.1"
4444
m3u8-rs = "6.0.0"
4545
async-trait = "0.1.81"
4646
aes = "0.8.4"
4747
cbc = { version = "0.1.2", features = ["std"] }
4848
hex = "0.4.3"
49-
boa_engine = "0.17.3"
49+
boa_engine = "0.19.0"
5050
mime = "0.3.17"
51-
bytes = "1.6.1"
51+
bytes = "1.7.1"
5252
flame = { version = "0.2.2", optional = true }
5353
flamer = { version = "0.5.0", optional = true }
5454

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,5 +158,5 @@ Or add the following to your `Cargo.toml` file:
158158

159159
```toml
160160
[dependencies]
161-
rusty_ytdl = "0.7.3"
161+
rusty_ytdl = "0.7.4"
162162
```

src/blocking/info.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::block_async;
44
#[cfg(feature = "live")]
55
use crate::blocking::stream::LiveStream;
66
use crate::blocking::stream::NonLiveStream;
7-
use crate::info::DEFAULT_DL_CHUNK_SIZE;
7+
use crate::constants::DEFAULT_DL_CHUNK_SIZE;
88
use crate::structs::{VideoError, VideoInfo, VideoOptions};
99
use crate::utils::choose_format;
1010
use crate::Video as AsyncVideo;

src/blocking/search/youtube.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::{block_async, VideoError};
77
use serde::Serialize;
88

99
#[derive(Clone, derive_more::Display, derivative::Derivative)]
10-
#[display(fmt = "YouTube()")]
10+
#[display("YouTube()")]
1111
#[derivative(Debug, PartialEq, Eq)]
1212
pub struct YouTube(AsyncYouTube);
1313

src/constants.rs

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,74 @@ pub(crate) static IPV6_REGEX: Lazy<Regex> = Lazy::new(|| {
4646
pub(crate) static PARSE_INT_REGEX: Lazy<Regex> =
4747
Lazy::new(|| Regex::new(r"(?m)^\s*((\-|\+)?[0-9]+)\s*").unwrap());
4848

49+
// 10485760 -> Default is 10MB to avoid Youtube throttle (Bigger than this value can be throttle by Youtube)
50+
pub(crate) const DEFAULT_DL_CHUNK_SIZE: u64 = 10485760;
51+
52+
/// Default max number of retries for a web reqwest.
53+
pub(crate) const DEFAULT_MAX_RETRIES: u32 = 3;
54+
55+
pub(crate) const POTOKEN_EXPERIMENTS: &[&str] = &["51217476", "51217102"];
56+
57+
pub static INNERTUBE_CLIENT: Lazy<HashMap<&str, (&str, &str, &str)>> =
58+
// (clientVersion, clientName, json value)
59+
Lazy::new(|| {
60+
HashMap::from([
61+
(
62+
"web",
63+
(
64+
"2.20240726.00.00",
65+
"1",
66+
r#""context": {
67+
"client": {
68+
"clientName": "WEB",
69+
"clientVersion": "2.20240726.00.00",
70+
"hl": "en"
71+
}
72+
},"#,
73+
),
74+
),
75+
(
76+
"ios",
77+
(
78+
"19.29.1",
79+
"5",
80+
r#""context": {
81+
"client": {
82+
"clientName": "IOS",
83+
"clientVersion": "19.29.1",
84+
"deviceMake": "Apple",
85+
"deviceModel": "iPhone16,2",
86+
"userAgent": "com.google.ios.youtube/19.29.1 (iPhone16,2; U; CPU iOS 17_5_1 like Mac OS X;)",
87+
"osName": "iPhone",
88+
"osVersion": "17.5.1.21F90",
89+
"hl": "en"
90+
}
91+
},"#,
92+
),
93+
),
94+
(
95+
// This client can access age restricted videos (unless the uploader has disabled the 'allow embedding' option)
96+
// See: https://github.com/yt-dlp/yt-dlp/blob/28d485714fef88937c82635438afba5db81f9089/yt_dlp/extractor/youtube.py#L231
97+
"tv_embedded",
98+
(
99+
"2.0",
100+
"85",
101+
r#""context": {
102+
"client": {
103+
"clientName": "TVHTML5_SIMPLY_EMBEDDED_PLAYER",
104+
"clientVersion": "2.0",
105+
"hl": "en",
106+
"clientScreen": "EMBED"
107+
},
108+
"thirdParty": {
109+
"embedUrl": "https://google.com"
110+
}
111+
},"#,
112+
),
113+
),
114+
])
115+
});
116+
49117
pub static FORMATS: Lazy<HashMap<&str, StaticFormat>> = Lazy::new(|| {
50118
HashMap::from([
51119
(
@@ -1234,6 +1302,3 @@ pub static FORMATS: Lazy<HashMap<&str, StaticFormat>> = Lazy::new(|| {
12341302
),
12351303
])
12361304
});
1237-
1238-
/// Default max number of retries for a web reqwest.
1239-
pub const DEFAULT_MAX_RETRIES: u32 = 3;

src/info.rs

Lines changed: 77 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,22 @@ use crate::stream::{LiveStream, LiveStreamOptions};
1515
use crate::structs::FFmpegArgs;
1616

1717
use crate::{
18-
constants::{BASE_URL, DEFAULT_MAX_RETRIES},
18+
constants::{BASE_URL, DEFAULT_DL_CHUNK_SIZE, DEFAULT_MAX_RETRIES, INNERTUBE_CLIENT},
1919
info_extras::{get_media, get_related_videos},
2020
stream::{NonLiveStream, NonLiveStreamOptions, Stream},
2121
structs::{
2222
CustomRetryableStrategy, PlayerResponse, VideoError, VideoInfo, VideoOptions, YTConfig,
2323
},
2424
utils::{
25-
between, choose_format, clean_video_details, get_functions, get_html, get_html5player,
26-
get_random_v6_ip, get_video_id, get_ytconfig, is_age_restricted_from_html,
25+
between, check_experiments, choose_format, clean_video_details, get_functions, get_html,
26+
get_html5player, get_random_v6_ip, get_video_id, get_ytconfig, is_age_restricted_from_html,
2727
is_not_yet_broadcasted, is_play_error, is_private_video, is_rental,
2828
parse_live_video_formats, parse_video_formats, sort_formats,
2929
},
3030
};
3131

32-
// 10485760 -> Default is 10MB to avoid Youtube throttle (Bigger than this value can be throttle by Youtube)
33-
pub(crate) const DEFAULT_DL_CHUNK_SIZE: u64 = 10485760;
34-
3532
#[derive(Clone, derive_more::Display, derivative::Derivative)]
36-
#[display(fmt = "Video({video_id})")]
33+
#[display("Video({video_id})")]
3734
#[derivative(Debug, PartialEq, Eq)]
3835
pub struct Video {
3936
video_id: String,
@@ -102,7 +99,10 @@ impl Video {
10299
}
103100
};
104101

105-
let max_retries = options.request_options.max_retries.unwrap_or(DEFAULT_MAX_RETRIES);
102+
let max_retries = options
103+
.request_options
104+
.max_retries
105+
.unwrap_or(DEFAULT_MAX_RETRIES);
106106

107107
let retry_policy = ExponentialBackoff::builder()
108108
.retry_bounds(Duration::from_millis(1000), Duration::from_millis(30000))
@@ -177,8 +177,31 @@ impl Video {
177177
return Err(VideoError::VideoIsPrivate);
178178
}
179179

180+
// POToken experiment detected fallback to ios client (Webpage contains broken formats)
181+
if check_experiments(&response) {
182+
let ios_ytconfig = self
183+
.get_player_ytconfig(
184+
&response,
185+
INNERTUBE_CLIENT.get("ios").cloned().unwrap_or_default(),
186+
)
187+
.await?;
188+
189+
let player_response_new =
190+
serde_json::from_str::<PlayerResponse>(&ios_ytconfig).unwrap_or_default();
191+
192+
player_response.streaming_data = player_response_new.streaming_data;
193+
}
194+
180195
if is_age_restricted {
181-
let embed_ytconfig = self.get_embeded_ytconfig(&response).await?;
196+
let embed_ytconfig = self
197+
.get_player_ytconfig(
198+
&response,
199+
INNERTUBE_CLIENT
200+
.get("tv_embedded")
201+
.cloned()
202+
.unwrap_or_default(),
203+
)
204+
.await?;
182205

183206
let player_response_new =
184207
serde_json::from_str::<PlayerResponse>(&embed_ytconfig).unwrap_or_default();
@@ -484,39 +507,36 @@ impl Video {
484507
}
485508

486509
#[cfg_attr(feature = "performance_analysis", flamer::flame)]
487-
async fn get_embeded_ytconfig(&self, html: &str) -> Result<String, VideoError> {
510+
async fn get_player_ytconfig(
511+
&self,
512+
html: &str,
513+
configs: (&str, &str, &str),
514+
) -> Result<String, VideoError> {
515+
use std::str::FromStr;
516+
488517
let ytcfg = get_ytconfig(html)?;
489518

490-
// This client can access age restricted videos (unless the uploader has disabled the 'allow embedding' option)
491-
// See: https://github.com/yt-dlp/yt-dlp/blob/28d485714fef88937c82635438afba5db81f9089/yt_dlp/extractor/youtube.py#L231
492-
let query = serde_json::json!({
493-
"context": {
494-
"client": {
495-
"clientName": "TVHTML5_SIMPLY_EMBEDDED_PLAYER",
496-
"clientVersion": "2.0",
497-
"hl": "en",
498-
"clientScreen": "EMBED",
499-
},
500-
"thirdParty": {
501-
"embedUrl": "https://google.com",
502-
},
503-
},
504-
"playbackContext": {
505-
"contentPlaybackContext": {
506-
"signatureTimestamp": ytcfg.sts.unwrap_or(0),
507-
"html5Preference": "HTML5_PREF_WANTS",
508-
},
509-
},
510-
"videoId": self.get_video_id(),
511-
});
519+
let client = configs.2;
520+
let sts = ytcfg.sts.unwrap_or(0);
521+
let video_id = self.get_video_id();
522+
523+
let query = serde_json::from_str::<serde_json::Value>(&format!(
524+
r#"{{
525+
{client}
526+
"playbackContext": {{
527+
"contentPlaybackContext": {{
528+
"signatureTimestamp": {sts},
529+
"html5Preference": "HTML5_PREF_WANTS"
530+
}}
531+
}},
532+
"videoId": "{video_id}"
533+
}}"#
534+
))
535+
.unwrap_or_default();
512536

513537
static CONFIGS: Lazy<(HeaderMap, &str)> = Lazy::new(|| {
514-
use std::str::FromStr;
515-
516538
(HeaderMap::from_iter([
517539
(HeaderName::from_str("content-type").unwrap(), HeaderValue::from_str("application/json").unwrap()),
518-
(HeaderName::from_str("X-Youtube-Client-Name").unwrap(), HeaderValue::from_str("85").unwrap()),
519-
(HeaderName::from_str("X-Youtube-Client-Version").unwrap(), HeaderValue::from_str("2.0").unwrap()),
520540
(HeaderName::from_str("Origin").unwrap(), HeaderValue::from_str("https://www.youtube.com").unwrap()),
521541
(HeaderName::from_str("User-Agent").unwrap(), HeaderValue::from_str("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3513.0 Safari/537.36").unwrap()),
522542
(HeaderName::from_str("Referer").unwrap(), HeaderValue::from_str("https://www.youtube.com/").unwrap()),
@@ -526,10 +546,20 @@ impl Video {
526546
]),"AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8")
527547
});
528548

549+
let mut headers = CONFIGS.0.clone();
550+
headers.insert(
551+
HeaderName::from_str("X-Youtube-Client-Version").unwrap(),
552+
HeaderValue::from_str(configs.0).unwrap(),
553+
);
554+
headers.insert(
555+
HeaderName::from_str("X-Youtube-Client-Name").unwrap(),
556+
HeaderValue::from_str(configs.1).unwrap(),
557+
);
558+
529559
let response = self
530560
.client
531561
.post("https://www.youtube.com/youtubei/v1/player")
532-
.headers(CONFIGS.0.clone())
562+
.headers(headers)
533563
.query(&[("key", CONFIGS.1)])
534564
.json(&query)
535565
.send()
@@ -550,15 +580,12 @@ async fn get_m3u8(
550580
url: &str,
551581
client: &reqwest_middleware::ClientWithMiddleware,
552582
) -> Result<Vec<(String, String)>, VideoError> {
553-
let base_url = Url::parse(BASE_URL).expect("BASE_URL corrapt");
554-
let base_url_host = base_url.host_str().expect("BASE_URL host corrapt");
583+
let base_url = Url::parse(BASE_URL)?;
584+
let base_url_host = base_url.host_str();
555585

556586
let url = Url::parse(url)
557587
.and_then(|mut x| {
558-
let set_host_result = x.set_host(Some(base_url_host));
559-
if set_host_result.is_err() {
560-
return Err(set_host_result.expect_err("How can be posible"));
561-
}
588+
x.set_host(base_url_host)?;
562589
Ok(x)
563590
})
564591
.map(|x| x.as_str().to_string())
@@ -574,19 +601,12 @@ async fn get_m3u8(
574601
.split('\n')
575602
.filter(|x| HTTP_REGEX.is_match(x) && ITAG_REGEX.is_match(x));
576603

577-
let itag_and_url: Vec<(String, String)> = itag_and_url
578-
.map(|line| {
579-
let itag = ITAG_REGEX
580-
.captures(line)
581-
.expect("IMPOSSIBLE")
582-
.get(1)
583-
.map(|x| x.as_str())
584-
.unwrap_or("");
585-
586-
// println!("itag: {}, url: {}", itag, line);
587-
(itag.to_string(), line.to_string())
604+
Ok(itag_and_url
605+
.filter_map(|line| {
606+
ITAG_REGEX.captures(line).and_then(|caps| {
607+
caps.get(1)
608+
.map(|itag| (itag.as_str().to_string(), line.to_string()))
609+
})
588610
})
589-
.collect();
590-
591-
Ok(itag_and_url)
611+
.collect::<Vec<(String, String)>>())
592612
}

0 commit comments

Comments
 (0)