1pub mod android {
2 use super::{YouTubeClient, core};
3 use crate::{
4 common::types::AnyResult,
5 protocol::tracks::Track,
6 sources::youtube::{
7 cipher::YouTubeCipherManager, clients::common::ClientConfig, oauth::YouTubeOAuth,
8 },
9 };
10 use async_trait::async_trait;
11 use serde_json::Value;
12 use std::sync::Arc;
13 const CLIENT_NAME: &str = "ANDROID";
14 const CLIENT_ID: &str = "3";
15 const CLIENT_VERSION: &str = "20.01.35";
16 const USER_AGENT: &str = "com.google.android.youtube/20.01.35 (Linux; U; Android 14) identity";
17 pub struct AndroidClient {
18 http: Arc<reqwest::Client>,
19 }
20 impl AndroidClient {
21 pub fn new(http: Arc<reqwest::Client>) -> Self {
22 Self { http }
23 }
24 fn config(&self) -> ClientConfig<'static> {
25 ClientConfig {
26 client_name: CLIENT_NAME,
27 client_version: CLIENT_VERSION,
28 client_id: CLIENT_ID,
29 user_agent: USER_AGENT,
30 device_make: Some("Google"),
31 device_model: Some("Pixel 6"),
32 os_name: Some("Android"),
33 os_version: Some("14"),
34 android_sdk_version: Some("34"),
35 ..Default::default()
36 }
37 }
38 }
39 #[async_trait]
40 impl YouTubeClient for AndroidClient {
41 fn name(&self) -> &str {
42 "Android"
43 }
44 fn client_name(&self) -> &str {
45 CLIENT_NAME
46 }
47 fn client_version(&self) -> &str {
48 CLIENT_VERSION
49 }
50 fn user_agent(&self) -> &str {
51 USER_AGENT
52 }
53 async fn search(
54 &self,
55 query: &str,
56 context: &Value,
57 oauth: Arc<YouTubeOAuth>,
58 ) -> AnyResult<Vec<Track>> {
59 core::standard_search(self, &self.http, query, context, oauth, || self.config()).await
60 }
61 async fn get_track_info(
62 &self,
63 track_id: &str,
64 context: &Value,
65 oauth: Arc<YouTubeOAuth>,
66 ) -> AnyResult<Option<Track>> {
67 core::standard_get_track_info(
68 self,
69 core::StandardPlayerOptions {
70 http: &self.http,
71 track_id,
72 context,
73 oauth,
74 signature_timestamp: None,
75 encrypted_host_flags: None,
76 config_builder: || self.config(),
77 },
78 )
79 .await
80 }
81 async fn get_playlist(
82 &self,
83 playlist_id: &str,
84 context: &Value,
85 oauth: Arc<YouTubeOAuth>,
86 ) -> AnyResult<Option<(Vec<Track>, String)>> {
87 core::standard_get_playlist(self, &self.http, playlist_id, context, oauth, || {
88 self.config()
89 })
90 .await
91 }
92 async fn resolve_url(
93 &self,
94 _url: &str,
95 _context: &Value,
96 _oauth: Arc<YouTubeOAuth>,
97 ) -> AnyResult<Option<Track>> {
98 Ok(None)
99 }
100 async fn get_track_url(
101 &self,
102 track_id: &str,
103 context: &Value,
104 cipher_manager: Arc<YouTubeCipherManager>,
105 oauth: Arc<YouTubeOAuth>,
106 ) -> AnyResult<Option<String>> {
107 core::standard_get_track_url(
108 self,
109 core::StandardUrlOptions {
110 http: &self.http,
111 track_id,
112 context,
113 cipher_manager,
114 oauth,
115 signature_timestamp: None,
116 encrypted_host_flags: None,
117 config_builder: || self.config(),
118 },
119 )
120 .await
121 }
122 async fn get_player_body(
123 &self,
124 track_id: &str,
125 visitor_data: Option<&str>,
126 _oauth: Arc<YouTubeOAuth>,
127 ) -> Option<serde_json::Value> {
128 crate::sources::youtube::clients::common::make_player_request(
129 crate::sources::youtube::clients::common::PlayerRequestOptions {
130 http: &self.http,
131 config: &self.config(),
132 video_id: track_id,
133 params: None,
134 visitor_data,
135 signature_timestamp: None,
136 auth_header: None,
137 referer: None,
138 origin: None,
139 po_token: None,
140 encrypted_host_flags: None,
141 attestation_request: None,
142 serialized_third_party_embed_config: false,
143 },
144 )
145 .await
146 .ok()
147 }
148 }
149}
150pub mod android_vr {
151 use super::{YouTubeClient, core};
152 use crate::{
153 common::types::AnyResult,
154 protocol::tracks::Track,
155 sources::youtube::{
156 cipher::YouTubeCipherManager, clients::common::ClientConfig, oauth::YouTubeOAuth,
157 },
158 };
159 use async_trait::async_trait;
160 use serde_json::Value;
161 use std::sync::Arc;
162 const CLIENT_NAME: &str = "ANDROID_VR";
163 const CLIENT_ID: &str = "28";
164 const CLIENT_VERSION: &str = "1.71.26";
165 const USER_AGENT: &str = "com.google.android.apps.youtube.vr.oculus/1.71.26 (Linux; U; Android 15; eureka-user Build/AP4A.250205.002) gzip";
166 pub struct AndroidVrClient {
167 http: Arc<reqwest::Client>,
168 }
169 impl AndroidVrClient {
170 pub fn new(http: Arc<reqwest::Client>) -> Self {
171 Self { http }
172 }
173 fn config(&self) -> ClientConfig<'static> {
174 ClientConfig {
175 client_name: CLIENT_NAME,
176 client_version: CLIENT_VERSION,
177 client_id: CLIENT_ID,
178 user_agent: USER_AGENT,
179 device_make: Some("Google"),
180 os_name: Some("Android"),
181 os_version: Some("15"),
182 android_sdk_version: Some("35"),
183 ..Default::default()
184 }
185 }
186 }
187 #[async_trait]
188 impl YouTubeClient for AndroidVrClient {
189 fn name(&self) -> &str {
190 "AndroidVR"
191 }
192 fn client_name(&self) -> &str {
193 CLIENT_NAME
194 }
195 fn client_version(&self) -> &str {
196 CLIENT_VERSION
197 }
198 fn user_agent(&self) -> &str {
199 USER_AGENT
200 }
201 async fn search(
202 &self,
203 query: &str,
204 context: &Value,
205 oauth: Arc<YouTubeOAuth>,
206 ) -> AnyResult<Vec<Track>> {
207 core::standard_search(self, &self.http, query, context, oauth, || self.config()).await
208 }
209 async fn get_track_info(
210 &self,
211 _track_id: &str,
212 _context: &Value,
213 _oauth: Arc<YouTubeOAuth>,
214 ) -> AnyResult<Option<Track>> {
215 Ok(None)
216 }
217 async fn get_playlist(
218 &self,
219 _playlist_id: &str,
220 _context: &Value,
221 _oauth: Arc<YouTubeOAuth>,
222 ) -> AnyResult<Option<(Vec<Track>, String)>> {
223 Ok(None)
224 }
225 async fn resolve_url(
226 &self,
227 _url: &str,
228 _context: &Value,
229 _oauth: Arc<YouTubeOAuth>,
230 ) -> AnyResult<Option<Track>> {
231 Ok(None)
232 }
233 async fn get_track_url(
234 &self,
235 track_id: &str,
236 context: &Value,
237 cipher_manager: Arc<YouTubeCipherManager>,
238 oauth: Arc<YouTubeOAuth>,
239 ) -> AnyResult<Option<String>> {
240 let signature_timestamp = cipher_manager.get_signature_timestamp().await.ok();
241 core::standard_get_track_url(
242 self,
243 core::StandardUrlOptions {
244 http: &self.http,
245 track_id,
246 context,
247 cipher_manager,
248 oauth,
249 signature_timestamp,
250 encrypted_host_flags: None,
251 config_builder: || self.config(),
252 },
253 )
254 .await
255 }
256 async fn get_player_body(
257 &self,
258 track_id: &str,
259 visitor_data: Option<&str>,
260 _oauth: Arc<YouTubeOAuth>,
261 ) -> Option<serde_json::Value> {
262 crate::sources::youtube::clients::common::make_player_request(
263 crate::sources::youtube::clients::common::PlayerRequestOptions {
264 http: &self.http,
265 config: &self.config(),
266 video_id: track_id,
267 params: None,
268 visitor_data,
269 signature_timestamp: None,
270 auth_header: None,
271 referer: None,
272 origin: None,
273 po_token: None,
274 encrypted_host_flags: None,
275 attestation_request: None,
276 serialized_third_party_embed_config: false,
277 },
278 )
279 .await
280 .ok()
281 }
282 }
283}
284pub mod common {
285 use super::YouTubeCipherManager;
286 use crate::common::types::AnyResult;
287 use regex::Regex;
288 use serde_json::{Value, json};
289 use std::sync::{Arc, OnceLock};
290 pub const INNERTUBE_API: &str = "https://youtubei.googleapis.com";
291 #[derive(Debug, Clone)]
292 pub struct ClientConfig<'a> {
293 pub client_name: &'a str,
294 pub client_version: &'a str,
295 pub client_id: &'a str,
296 pub user_agent: &'a str,
297 pub os_name: Option<&'a str>,
298 pub os_version: Option<&'a str>,
299 pub device_make: Option<&'a str>,
300 pub device_model: Option<&'a str>,
301 pub platform: Option<&'a str>,
302 pub android_sdk_version: Option<&'a str>,
303 pub hl: &'a str,
304 pub gl: &'a str,
305 pub utc_offset_minutes: Option<i32>,
306 pub third_party_embed_url: Option<&'a str>,
307 pub client_screen: Option<&'a str>,
308 pub attestation_request: Option<Value>,
309 }
310 impl<'a> Default for ClientConfig<'a> {
311 fn default() -> Self {
312 Self {
313 client_name: "",
314 client_version: "",
315 client_id: "",
316 user_agent: "",
317 os_name: None,
318 os_version: None,
319 device_make: None,
320 device_model: None,
321 platform: None,
322 android_sdk_version: None,
323 hl: "en",
324 gl: "US",
325 utc_offset_minutes: None,
326 third_party_embed_url: None,
327 client_screen: None,
328 attestation_request: None,
329 }
330 }
331 }
332 impl<'a> ClientConfig<'a> {
333 pub fn build_context(&self, visitor_data: Option<&str>) -> Value {
334 let mut client = json!({
335 "clientName": self.client_name,
336 "clientVersion": self.client_version,
337 "userAgent": self.user_agent,
338 "hl": self.hl,
339 "gl": self.gl,
340 });
341 if let Some(obj) = client.as_object_mut() {
342 if let Some(v) = self.os_name {
343 obj.insert("osName".to_string(), v.into());
344 }
345 if let Some(v) = self.os_version {
346 obj.insert("osVersion".to_string(), v.into());
347 }
348 if let Some(v) = self.device_make {
349 obj.insert("deviceMake".to_string(), v.into());
350 }
351 if let Some(v) = self.device_model {
352 obj.insert("deviceModel".to_string(), v.into());
353 }
354 if let Some(v) = self.platform {
355 obj.insert("platform".to_string(), v.into());
356 }
357 if let Some(v) = self.android_sdk_version {
358 obj.insert("androidSdkVersion".to_string(), v.into());
359 }
360 if let Some(v) = self.utc_offset_minutes {
361 obj.insert("utcOffsetMinutes".to_string(), v.into());
362 }
363 if let Some(v) = self.client_screen {
364 obj.insert("clientScreen".to_string(), v.into());
365 }
366 if let Some(vd) = visitor_data {
367 obj.insert("visitorData".to_string(), vd.into());
368 }
369 }
370 let mut context = json!({
371 "client": client,
372 "user": { "lockedSafetyMode": false },
373 "request": { "useSsl": true }
374 });
375 if let Some(url) = self.third_party_embed_url
376 && let Some(obj) = context.as_object_mut()
377 {
378 obj.insert("thirdParty".to_string(), json!({ "embedUrl": url }));
379 }
380 if let Some(att) = self.attestation_request.clone()
381 && let Some(obj) = context.as_object_mut()
382 {
383 obj.insert("attestationRequest".to_string(), att);
384 }
385 context
386 }
387 }
388 pub const AUDIO_ITAG_PRIORITY: &[i64] = &[251, 250, 140];
389 pub const ITAG_FALLBACK: i64 = 18;
390 pub fn decode_signature_cipher(cipher_str: &str) -> Option<(String, String)> {
391 let mut url = None;
392 let mut sig = None;
393 for part in cipher_str.split('&') {
394 if let Some((k, v)) = part.split_once('=') {
395 let decoded = urlencoding::decode(v).ok()?.to_string();
396 match k {
397 "url" => url = Some(decoded),
398 "s" => sig = Some(decoded),
399 _ => {}
400 }
401 }
402 }
403 match (url, sig) {
404 (Some(u), Some(s)) => Some((u, s)),
405 _ => None,
406 }
407 }
408 pub fn select_best_audio_format<'a>(
409 adaptive_formats: Option<&'a Vec<Value>>,
410 formats: Option<&'a Vec<Value>>,
411 ) -> Option<&'a Value> {
412 let all: Vec<&Value> = adaptive_formats
413 .into_iter()
414 .flatten()
415 .chain(formats.into_iter().flatten())
416 .collect();
417 for &target_itag in AUDIO_ITAG_PRIORITY {
418 for f in &all {
419 let itag = f.get("itag").and_then(|v| v.as_i64()).unwrap_or(-1);
420 let mime = f.get("mimeType").and_then(|v| v.as_str()).unwrap_or("");
421 if itag == target_itag && mime.starts_with("audio/") {
422 return Some(f);
423 }
424 }
425 }
426 for f in &all {
427 let itag = f.get("itag").and_then(|v| v.as_i64()).unwrap_or(-1);
428 if itag == ITAG_FALLBACK {
429 return Some(f);
430 }
431 }
432 let mut best: Option<&Value> = None;
433 let mut best_bitrate = 0i64;
434 for f in all {
435 let mime = f.get("mimeType").and_then(|v| v.as_str()).unwrap_or("");
436 if mime.starts_with("audio/") {
437 let bitrate = f.get("bitrate").and_then(|v| v.as_i64()).unwrap_or(0);
438 if bitrate > best_bitrate {
439 best = Some(f);
440 best_bitrate = bitrate;
441 }
442 }
443 }
444 best
445 }
446 pub async fn resolve_format_url(
447 format: &Value,
448 player_page_url: &str,
449 cipher_manager: &Arc<YouTubeCipherManager>,
450 ) -> AnyResult<Option<String>> {
451 if let Some(url) = format.get("url").and_then(|u| u.as_str()) {
452 let n_param = url
453 .split("&n=")
454 .nth(1)
455 .or_else(|| url.split("?n=").nth(1))
456 .and_then(|s| s.split('&').next());
457 if n_param.is_none() {
458 return Ok(Some(url.to_string()));
459 }
460 let resolved = cipher_manager
461 .resolve_url(url, player_page_url, n_param, None)
462 .await?;
463 return Ok(Some(resolved));
464 }
465 let cipher_str = format
466 .get("signatureCipher")
467 .or_else(|| format.get("cipher"))
468 .and_then(|c| c.as_str());
469 if let Some(cipher_str) = cipher_str
470 && let Some((url, sig)) = decode_signature_cipher(cipher_str)
471 {
472 let n_param = url
473 .split("&n=")
474 .nth(1)
475 .or_else(|| url.split("?n=").nth(1))
476 .and_then(|s| s.split('&').next());
477 let resolved = cipher_manager
478 .resolve_url(&url, player_page_url, n_param, Some(&sig))
479 .await?;
480 return Ok(Some(resolved));
481 }
482 Ok(None)
483 }
484 static DURATION_REGEX: OnceLock<Regex> = OnceLock::new();
485 pub fn is_duration(text: &str) -> bool {
486 let re = DURATION_REGEX.get_or_init(|| Regex::new(r"^\d{1,2}:\d{2}(:\d{2})?$").unwrap());
487 re.is_match(text)
488 }
489 pub fn parse_duration(duration: &str) -> u64 {
490 let parts: Vec<&str> = duration.split(':').collect();
491 let mut ms = 0u64;
492 for part in parts {
493 if let Ok(num) = part.parse::<u64>() {
494 ms = ms * 60 + num;
495 }
496 }
497 ms * 1000
498 }
499 pub fn extract_thumbnail(renderer: &Value, video_id: Option<&str>) -> Option<String> {
500 let thumbnails = renderer
501 .get("thumbnail")
502 .and_then(|t| t.get("thumbnails"))
503 .or_else(|| {
504 renderer
505 .get("thumbnail")
506 .and_then(|t| t.get("musicThumbnailRenderer"))
507 .and_then(|t| t.get("thumbnail"))
508 .and_then(|t| t.get("thumbnails"))
509 });
510 if let Some(list) = thumbnails.and_then(|t| t.as_array())
511 && !list.is_empty()
512 {
513 let lh3 = list.iter().rev().find_map(|t| {
514 t.get("url")
515 .and_then(|u| u.as_str())
516 .filter(|u| u.contains("lh3.googleusercontent.com"))
517 .map(|u| u.split('?').next().unwrap_or(u).to_string())
518 });
519 if let Some(url) = lh3 {
520 return Some(url);
521 }
522 let best = list
523 .iter()
524 .max_by_key(|t| t.get("width").and_then(|w| w.as_u64()).unwrap_or(0));
525 if let Some(url) = best.and_then(|t| t.get("url")).and_then(|u| u.as_str()) {
526 let clean = url.split('?').next().unwrap_or(url);
527 if clean.contains("i.ytimg.com") {
528 let upgraded = clean
529 .replace("mqdefault", "maxresdefault")
530 .replace("sddefault", "maxresdefault")
531 .replace("hqdefault", "maxresdefault");
532 return Some(upgraded);
533 }
534 return Some(clean.to_string());
535 }
536 }
537 if let Some(id) = video_id {
538 return Some(format!("https://i.ytimg.com/vi/{}/maxresdefault.jpg", id));
539 }
540 None
541 }
542 pub struct PlayerRequestOptions<'a> {
543 pub http: &'a reqwest::Client,
544 pub config: &'a ClientConfig<'a>,
545 pub video_id: &'a str,
546 pub params: Option<&'a str>,
547 pub visitor_data: Option<&'a str>,
548 pub signature_timestamp: Option<u32>,
549 pub auth_header: Option<String>,
550 pub referer: Option<&'a str>,
551 pub origin: Option<&'a str>,
552 pub po_token: Option<&'a str>,
553 pub encrypted_host_flags: Option<String>,
554 pub attestation_request: Option<Value>,
555 pub serialized_third_party_embed_config: bool,
556 }
557 pub async fn make_player_request(opts: PlayerRequestOptions<'_>) -> AnyResult<Value> {
558 let mut body = json!({
559 "context": opts.config.build_context(opts.visitor_data),
560 "videoId": opts.video_id,
561 "contentCheckOk": true,
562 "racyCheckOk": true
563 });
564 if opts.serialized_third_party_embed_config
565 && let Some(obj) = body.as_object_mut()
566 {
567 obj.insert(
568 "serializedThirdPartyEmbedConfig".to_string(),
569 "{\"hideInfoBar\":true,\"disableRelatedVideos\":true}".into(),
570 );
571 }
572 if let Some(token) = opts.po_token
573 && let Some(obj) = body.as_object_mut()
574 {
575 obj.insert(
576 "serviceIntegrityDimensions".to_string(),
577 json!({ "poToken": token }),
578 );
579 }
580 if let Some(p) = opts.params
581 && let Some(obj) = body.as_object_mut()
582 {
583 obj.insert("params".to_string(), p.into());
584 }
585 if let Some(sts) = opts.signature_timestamp
586 && let Some(obj) = body.as_object_mut()
587 {
588 obj.insert(
589 "playbackContext".to_string(),
590 json!({
591 "contentPlaybackContext": {
592 "signatureTimestamp": sts
593 }
594 }),
595 );
596 }
597 if let Some(flags) = opts.encrypted_host_flags
598 && let Some(obj) = body.as_object_mut()
599 {
600 let playback_context = obj
601 .entry("playbackContext".to_string())
602 .or_insert_with(|| json!({}));
603 let content_playback_context = playback_context
604 .as_object_mut()
605 .unwrap()
606 .entry("contentPlaybackContext".to_string())
607 .or_insert_with(|| json!({}));
608 content_playback_context
609 .as_object_mut()
610 .unwrap()
611 .insert("encryptedHostFlags".to_string(), flags.into());
612 }
613 if let Some(att) = opts.attestation_request
614 && let Some(obj) = body.as_object_mut()
615 {
616 obj.insert("attestationRequest".to_string(), att);
617 }
618 let url = format!("{}/youtubei/v1/player?prettyPrint=false", INNERTUBE_API);
619 let mut req = opts
620 .http
621 .post(&url)
622 .header("User-Agent", opts.config.user_agent)
623 .header("X-YouTube-Client-Name", opts.config.client_id)
624 .header("X-YouTube-Client-Version", opts.config.client_version)
625 .header("X-Goog-Api-Format-Version", "2");
626 if let Some(vd) = opts.visitor_data {
627 req = req.header("X-Goog-Visitor-Id", vd);
628 }
629 if let Some(auth) = opts.auth_header {
630 req = req.header("Authorization", auth);
631 }
632 if let Some(ref_url) = opts.referer {
633 req = req.header("Referer", ref_url);
634 }
635 if let Some(orig_url) = opts.origin {
636 req = req.header("Origin", orig_url);
637 }
638 let res = req.json(&body).send().await?;
639 let status = res.status();
640 if !status.is_success() {
641 let text = res
642 .text()
643 .await
644 .unwrap_or_else(|_| "Unknown error".to_string());
645 return Err(format!("Player request failed (status={}): {}", status, text).into());
646 }
647 Ok(res.json().await?)
648 }
649 pub async fn make_next_request(
650 http: &reqwest::Client,
651 config: &ClientConfig<'_>,
652 video_id: Option<&str>,
653 playlist_id: Option<&str>,
654 visitor_data: Option<&str>,
655 auth_header: Option<String>,
656 ) -> AnyResult<Value> {
657 let mut body = json!({
658 "context": config.build_context(visitor_data),
659 });
660 if let Some(vid) = video_id
661 && let Some(obj) = body.as_object_mut()
662 {
663 obj.insert("videoId".to_string(), vid.into());
664 }
665 if let Some(pid) = playlist_id
666 && let Some(obj) = body.as_object_mut()
667 {
668 obj.insert("playlistId".to_string(), pid.into());
669 }
670 let url = format!("{}/youtubei/v1/next?prettyPrint=false", INNERTUBE_API);
671 let mut req = http
672 .post(&url)
673 .header("User-Agent", config.user_agent)
674 .header("X-YouTube-Client-Name", config.client_id)
675 .header("X-YouTube-Client-Version", config.client_version);
676 if let Some(vd) = visitor_data {
677 req = req.header("X-Goog-Visitor-Id", vd);
678 }
679 if let Some(auth) = auth_header {
680 req = req.header("Authorization", auth);
681 }
682 let res = req.json(&body).send().await?;
683 let status = res.status();
684 if !status.is_success() {
685 let text = res
686 .text()
687 .await
688 .unwrap_or_else(|_| "Unknown error".to_string());
689 return Err(format!("Next request failed (status={}): {}", status, text).into());
690 }
691 Ok(res.json().await?)
692 }
693}
694pub mod core {
695 use super::{
696 YouTubeClient,
697 common::{
698 INNERTUBE_API, PlayerRequestOptions, make_player_request, resolve_format_url,
699 select_best_audio_format,
700 },
701 };
702 use crate::{
703 common::types::AnyResult,
704 protocol::tracks::Track,
705 sources::youtube::{
706 cipher::YouTubeCipherManager,
707 clients::common::ClientConfig,
708 extractor::{extract_from_player, extract_track},
709 oauth::YouTubeOAuth,
710 },
711 };
712 use serde_json::{Value, json};
713 use std::sync::Arc;
714 pub fn extract_visitor_data(context: &Value) -> Option<&str> {
715 context
716 .get("client")
717 .and_then(|c| c.get("visitorData"))
718 .and_then(|v| v.as_str())
719 .or_else(|| context.get("visitorData").and_then(|v| v.as_str()))
720 }
721 pub async fn standard_search<T: YouTubeClient>(
722 client: &T,
723 http: &Arc<reqwest::Client>,
724 query: &str,
725 context: &Value,
726 _oauth: Arc<YouTubeOAuth>,
727 config_builder: impl FnOnce() -> ClientConfig<'static>,
728 ) -> AnyResult<Vec<Track>> {
729 let visitor_data = extract_visitor_data(context);
730 let config = config_builder();
731 let body = json!({
732 "context": config.build_context(visitor_data),
733 "query": query,
734 "params": "EgIQAQ%3D%3D"
735 });
736 let url = format!("{}/youtubei/v1/search", INNERTUBE_API);
737 let mut req = http
738 .post(&url)
739 .header("User-Agent", client.user_agent())
740 .header("X-Goog-Api-Format-Version", "2")
741 .header("X-YouTube-Client-Name", client.client_name())
742 .header("X-YouTube-Client-Version", client.client_version());
743 if let Some(vd) = visitor_data {
744 req = req.header("X-Goog-Visitor-Id", vd);
745 }
746 let res = req.json(&body).send().await?;
747 let status = res.status();
748 if !status.is_success() {
749 let text = res.text().await.unwrap_or_default();
750 return Err(format!("{} search failed: {} - {}", client.name(), status, text).into());
751 }
752 let response: Value = res.json().await?;
753 let mut tracks = Vec::new();
754 if let Some(contents) = response.get("contents") {
755 let sections = contents
756 .get("sectionListRenderer")
757 .and_then(|s| s.get("contents"))
758 .and_then(|c| c.as_array())
759 .or_else(|| {
760 contents
761 .get("twoColumnSearchResultsRenderer")
762 .and_then(|t| t.get("primaryContents"))
763 .and_then(|p| p.get("sectionListRenderer"))
764 .and_then(|s| s.get("contents"))
765 .and_then(|c| c.as_array())
766 });
767 if let Some(sections) = sections {
768 for section in sections {
769 let items_opt = section
770 .get("itemSectionRenderer")
771 .and_then(|i| i.get("contents"))
772 .and_then(|c| c.as_array());
773 let shelf_items_opt = items_opt
774 .is_none()
775 .then(|| {
776 let shelf = section
777 .get("shelfRenderer")
778 .or_else(|| section.get("richShelfRenderer"))
779 .or_else(|| section.get("reelShelfRenderer"));
780 shelf.and_then(|s| {
781 s.get("content")
782 .and_then(|c| {
783 c.get("verticalListRenderer")
784 .or_else(|| c.get("horizontalListRenderer"))
785 })
786 .and_then(|v| v.get("items"))
787 .or_else(|| {
788 s.get("content")
789 .and_then(|c| c.get("richGridRenderer"))
790 .and_then(|r| r.get("contents"))
791 })
792 .and_then(|c| c.as_array())
793 })
794 })
795 .flatten();
796 let items = items_opt.or(shelf_items_opt);
797 if let Some(items) = items {
798 for item in items {
799 let inner = item
800 .get("richItemRenderer")
801 .and_then(|r| r.get("content"))
802 .unwrap_or(item);
803 if let Some(track) = extract_track(inner, "youtube") {
804 tracks.push(track);
805 }
806 }
807 }
808 }
809 } else if let Some(contents) = contents
810 .get("twoColumnSearchResultsRenderer")
811 .and_then(|t| t.get("primaryContents"))
812 .and_then(|p| p.get("richGridRenderer"))
813 .and_then(|r| r.get("contents"))
814 .and_then(|c| c.as_array())
815 {
816 for item in contents {
817 let inner = item
818 .get("richItemRenderer")
819 .and_then(|r| r.get("content"))
820 .unwrap_or(item);
821 if let Some(track) = extract_track(inner, "youtube") {
822 tracks.push(track);
823 }
824 }
825 } else {
826 tracing::debug!(
827 "Search: No standard sections found in response contents. keys: {:?}",
828 contents.as_object().map(|o| o.keys().collect::<Vec<_>>())
829 );
830 }
831 } else {
832 tracing::debug!(
833 "Search: No contents found in response. keys: {:?}",
834 response.as_object().map(|o| o.keys().collect::<Vec<_>>())
835 );
836 }
837 Ok(tracks)
838 }
839 pub struct StandardPlayerOptions<'a, F>
840 where
841 F: FnOnce() -> ClientConfig<'static>,
842 {
843 pub http: &'a Arc<reqwest::Client>,
844 pub track_id: &'a str,
845 pub context: &'a Value,
846 pub oauth: Arc<YouTubeOAuth>,
847 pub signature_timestamp: Option<u32>,
848 pub encrypted_host_flags: Option<String>,
849 pub config_builder: F,
850 }
851 pub async fn standard_get_track_info<T, F>(
852 client: &T,
853 opts: StandardPlayerOptions<'_, F>,
854 ) -> AnyResult<Option<Track>>
855 where
856 T: YouTubeClient,
857 F: FnOnce() -> ClientConfig<'static>,
858 {
859 let visitor_data = extract_visitor_data(opts.context);
860 let config = (opts.config_builder)();
861 let body = make_player_request(PlayerRequestOptions {
862 http: opts.http,
863 config: &config,
864 video_id: opts.track_id,
865 params: None,
866 visitor_data,
867 signature_timestamp: opts.signature_timestamp,
868 auth_header: if client.supports_oauth() {
869 opts.oauth.get_auth_header().await
870 } else {
871 None
872 },
873 referer: None,
874 origin: None,
875 po_token: None,
876 encrypted_host_flags: opts.encrypted_host_flags,
877 attestation_request: None,
878 serialized_third_party_embed_config: client.is_embedded(),
879 })
880 .await?;
881 Ok(extract_from_player(&body, "youtube"))
882 }
883 pub struct StandardUrlOptions<'a, F>
884 where
885 F: FnOnce() -> ClientConfig<'static>,
886 {
887 pub http: &'a Arc<reqwest::Client>,
888 pub track_id: &'a str,
889 pub context: &'a Value,
890 pub cipher_manager: Arc<YouTubeCipherManager>,
891 pub oauth: Arc<YouTubeOAuth>,
892 pub signature_timestamp: Option<u32>,
893 pub encrypted_host_flags: Option<String>,
894 pub config_builder: F,
895 }
896 pub async fn standard_get_track_url<T, F>(
897 client: &T,
898 opts: StandardUrlOptions<'_, F>,
899 ) -> AnyResult<Option<String>>
900 where
901 T: YouTubeClient,
902 F: FnOnce() -> ClientConfig<'static>,
903 {
904 let visitor_data = extract_visitor_data(opts.context);
905 let config = (opts.config_builder)();
906 let body = make_player_request(PlayerRequestOptions {
907 http: opts.http,
908 config: &config,
909 video_id: opts.track_id,
910 params: None,
911 visitor_data,
912 signature_timestamp: opts.signature_timestamp,
913 auth_header: if client.supports_oauth() {
914 opts.oauth.get_auth_header().await
915 } else {
916 None
917 },
918 referer: None,
919 origin: None,
920 po_token: None,
921 encrypted_host_flags: opts.encrypted_host_flags,
922 attestation_request: None,
923 serialized_third_party_embed_config: client.is_embedded(),
924 })
925 .await?;
926 if let Err(e) = crate::sources::youtube::utils::parse_playability_status(&body) {
927 tracing::warn!(
928 "{} player: video {} not playable: {}",
929 client.name(),
930 opts.track_id,
931 e
932 );
933 return Err(e.into());
934 }
935 let streaming_data = match body.get("streamingData") {
936 Some(sd) => sd,
937 None => {
938 tracing::error!(
939 "{} player: no streamingData for {}",
940 client.name(),
941 opts.track_id
942 );
943 return Ok(None);
944 }
945 };
946 if let Some(hls) = streaming_data
947 .get("hlsManifestUrl")
948 .and_then(|v| v.as_str())
949 {
950 tracing::debug!(
951 "{} player: using HLS manifest for {}",
952 client.name(),
953 opts.track_id
954 );
955 return Ok(Some(hls.to_string()));
956 }
957 let adaptive = streaming_data
958 .get("adaptiveFormats")
959 .and_then(|v| v.as_array());
960 let formats = streaming_data.get("formats").and_then(|v| v.as_array());
961 let player_page_url = format!("https://www.youtube.com/watch?v={}", opts.track_id);
962 if let Some(best) = select_best_audio_format(adaptive, formats) {
963 match resolve_format_url(best, &player_page_url, &opts.cipher_manager).await {
964 Ok(Some(url)) => {
965 return Ok(Some(url));
966 }
967 Ok(None) => {
968 tracing::warn!(
969 "{} player: best format had no resolvable URL for {}",
970 client.name(),
971 opts.track_id
972 );
973 }
974 Err(e) => {
975 tracing::error!(
976 "{} player: cipher resolution failed for {}: {}",
977 client.name(),
978 opts.track_id,
979 e
980 );
981 return Err(e);
982 }
983 }
984 }
985 Ok(None)
986 }
987 pub async fn standard_get_playlist<F>(
988 client: &dyn YouTubeClient,
989 http: &reqwest::Client,
990 playlist_id: &str,
991 context: &Value,
992 oauth: Arc<YouTubeOAuth>,
993 config_builder: F,
994 ) -> AnyResult<Option<(Vec<Track>, String)>>
995 where
996 F: Fn() -> ClientConfig<'static>,
997 {
998 let visitor_data = extract_visitor_data(context);
999 let config = config_builder();
1000 let browse_body = json!({
1001 "context": config.build_context(visitor_data),
1002 "browseId": if playlist_id.starts_with("VL") { playlist_id.to_string() } else { format!("VL{}", playlist_id) },
1003 });
1004 let browse_url = "https://www.youtube.com/youtubei/v1/browse?prettyPrint=false";
1005 let mut browse_req = http
1006 .post(browse_url)
1007 .header("User-Agent", client.user_agent())
1008 .header("X-YouTube-Client-Name", client.client_name())
1009 .header("X-YouTube-Client-Version", client.client_version());
1010 if let Some(auth) = oauth.get_auth_header().await {
1011 browse_req = browse_req.header("Authorization", auth);
1012 }
1013 if let Some(vd) = visitor_data {
1014 browse_req = browse_req.header("X-Goog-Visitor-Id", vd);
1015 }
1016 if let Ok(res) = browse_req.json(&browse_body).send().await
1017 && res.status().is_success()
1018 {
1019 let body: Value = res.json().await?;
1020 if let Some(result) =
1021 crate::sources::youtube::extractor::extract_from_browse(&body, "youtube")
1022 {
1023 return Ok(Some(result));
1024 }
1025 }
1026 let next_body = json!({
1027 "context": config.build_context(visitor_data),
1028 "playlistId": playlist_id,
1029 "enablePersistentPlaylistPanel": true,
1030 });
1031 let next_url = "https://www.youtube.com/youtubei/v1/next?prettyPrint=false";
1032 let mut next_req = http
1033 .post(next_url)
1034 .header("User-Agent", client.user_agent())
1035 .header("X-YouTube-Client-Name", client.client_name())
1036 .header("X-YouTube-Client-Version", client.client_version());
1037 if let Some(auth) = oauth.get_auth_header().await {
1038 next_req = next_req.header("Authorization", auth);
1039 }
1040 if let Some(vd) = visitor_data {
1041 next_req = next_req.header("X-Goog-Visitor-Id", vd);
1042 }
1043 if let Ok(res) = next_req.json(&next_body).send().await
1044 && res.status().is_success()
1045 {
1046 let body: Value = res.json().await?;
1047 if let Some(result) =
1048 crate::sources::youtube::extractor::extract_from_next(&body, "youtube")
1049 {
1050 return Ok(Some(result));
1051 }
1052 }
1053 Ok(None)
1054 }
1055}
1056pub mod ios {
1057 use super::{YouTubeClient, core};
1058 use crate::{
1059 common::types::AnyResult,
1060 protocol::tracks::Track,
1061 sources::youtube::{
1062 cipher::YouTubeCipherManager, clients::common::ClientConfig, oauth::YouTubeOAuth,
1063 },
1064 };
1065 use async_trait::async_trait;
1066 use serde_json::Value;
1067 use std::sync::Arc;
1068 const CLIENT_NAME: &str = "IOS";
1069 const CLIENT_VERSION: &str = "21.02.1";
1070 const USER_AGENT: &str =
1071 "com.google.ios.youtube/21.02.1 (iPhone16,2; U; CPU iOS 18_2 like Mac OS X;)";
1072 pub struct IosClient {
1073 http: Arc<reqwest::Client>,
1074 }
1075 impl IosClient {
1076 pub fn new(http: Arc<reqwest::Client>) -> Self {
1077 Self { http }
1078 }
1079 fn config(&self) -> ClientConfig<'static> {
1080 ClientConfig {
1081 client_name: CLIENT_NAME,
1082 client_version: CLIENT_VERSION,
1083 client_id: "5",
1084 user_agent: USER_AGENT,
1085 device_make: Some("Apple"),
1086 device_model: Some("iPhone16,2"),
1087 os_name: Some("iPhone"),
1088 os_version: Some("18.2.22C152"),
1089 utc_offset_minutes: Some(0),
1090 ..Default::default()
1091 }
1092 }
1093 }
1094 #[async_trait]
1095 impl YouTubeClient for IosClient {
1096 fn name(&self) -> &str {
1097 "IOS"
1098 }
1099 fn client_name(&self) -> &str {
1100 CLIENT_NAME
1101 }
1102 fn client_version(&self) -> &str {
1103 CLIENT_VERSION
1104 }
1105 fn user_agent(&self) -> &str {
1106 USER_AGENT
1107 }
1108 async fn search(
1109 &self,
1110 query: &str,
1111 context: &Value,
1112 oauth: Arc<YouTubeOAuth>,
1113 ) -> AnyResult<Vec<Track>> {
1114 core::standard_search(self, &self.http, query, context, oauth, || self.config()).await
1115 }
1116 async fn get_track_info(
1117 &self,
1118 track_id: &str,
1119 context: &Value,
1120 oauth: Arc<YouTubeOAuth>,
1121 ) -> AnyResult<Option<Track>> {
1122 core::standard_get_track_info(
1123 self,
1124 core::StandardPlayerOptions {
1125 http: &self.http,
1126 track_id,
1127 context,
1128 oauth,
1129 signature_timestamp: None,
1130 encrypted_host_flags: None,
1131 config_builder: || self.config(),
1132 },
1133 )
1134 .await
1135 }
1136 async fn get_playlist(
1137 &self,
1138 _playlist_id: &str,
1139 _context: &Value,
1140 _oauth: Arc<YouTubeOAuth>,
1141 ) -> AnyResult<Option<(Vec<Track>, String)>> {
1142 Ok(None)
1143 }
1144 async fn resolve_url(
1145 &self,
1146 _url: &str,
1147 _context: &Value,
1148 _oauth: Arc<YouTubeOAuth>,
1149 ) -> AnyResult<Option<Track>> {
1150 Ok(None)
1151 }
1152 async fn get_track_url(
1153 &self,
1154 track_id: &str,
1155 context: &Value,
1156 cipher_manager: Arc<YouTubeCipherManager>,
1157 oauth: Arc<YouTubeOAuth>,
1158 ) -> AnyResult<Option<String>> {
1159 core::standard_get_track_url(
1160 self,
1161 core::StandardUrlOptions {
1162 http: &self.http,
1163 track_id,
1164 context,
1165 cipher_manager,
1166 oauth,
1167 signature_timestamp: None,
1168 encrypted_host_flags: None,
1169 config_builder: || self.config(),
1170 },
1171 )
1172 .await
1173 }
1174 async fn get_player_body(
1175 &self,
1176 track_id: &str,
1177 visitor_data: Option<&str>,
1178 _oauth: Arc<YouTubeOAuth>,
1179 ) -> Option<serde_json::Value> {
1180 crate::sources::youtube::clients::common::make_player_request(
1181 crate::sources::youtube::clients::common::PlayerRequestOptions {
1182 http: &self.http,
1183 config: &self.config(),
1184 video_id: track_id,
1185 params: None,
1186 visitor_data,
1187 signature_timestamp: None,
1188 auth_header: None,
1189 referer: None,
1190 origin: None,
1191 po_token: None,
1192 encrypted_host_flags: None,
1193 attestation_request: None,
1194 serialized_third_party_embed_config: false,
1195 },
1196 )
1197 .await
1198 .ok()
1199 }
1200 }
1201}
1202pub mod music_android {
1203 use super::{YouTubeClient, core};
1204 use crate::{
1205 common::types::AnyResult,
1206 protocol::tracks::{Track, TrackInfo},
1207 sources::youtube::{
1208 cipher::YouTubeCipherManager,
1209 clients::common::{ClientConfig, extract_thumbnail, is_duration, parse_duration},
1210 oauth::YouTubeOAuth,
1211 },
1212 };
1213 use async_trait::async_trait;
1214 use serde_json::{Value, json};
1215 use std::sync::Arc;
1216 const CLIENT_NAME: &str = "ANDROID_MUSIC";
1217 const CLIENT_VERSION: &str = "8.47.54";
1218 const USER_AGENT: &str =
1219 "com.google.android.apps.youtube.music/8.47.54 (Linux; U; Android 14 gzip)";
1220 const INNERTUBE_API: &str = "https://music.youtube.com";
1221 pub struct MusicAndroidClient {
1222 http: Arc<reqwest::Client>,
1223 }
1224 impl MusicAndroidClient {
1225 pub fn new(http: Arc<reqwest::Client>) -> Self {
1226 Self { http }
1227 }
1228 fn config(&self) -> ClientConfig<'static> {
1229 ClientConfig {
1230 client_name: CLIENT_NAME,
1231 client_version: CLIENT_VERSION,
1232 client_id: "67",
1233 user_agent: USER_AGENT,
1234 device_make: Some("Google"),
1235 device_model: Some("Pixel 6"),
1236 os_name: Some("Android"),
1237 os_version: Some("14"),
1238 android_sdk_version: Some("30"),
1239 ..Default::default()
1240 }
1241 }
1242 }
1243 #[async_trait]
1244 impl YouTubeClient for MusicAndroidClient {
1245 fn name(&self) -> &str {
1246 "MusicAndroid"
1247 }
1248 fn client_name(&self) -> &str {
1249 CLIENT_NAME
1250 }
1251 fn client_version(&self) -> &str {
1252 CLIENT_VERSION
1253 }
1254 fn user_agent(&self) -> &str {
1255 USER_AGENT
1256 }
1257 fn can_handle_request(&self, identifier: &str) -> bool {
1258 !identifier.contains("list=") || identifier.contains("list=RD")
1259 }
1260 async fn search(
1261 &self,
1262 query: &str,
1263 context: &Value,
1264 _oauth: Arc<YouTubeOAuth>,
1265 ) -> AnyResult<Vec<Track>> {
1266 let visitor_data = core::extract_visitor_data(context);
1267 let body = json!({
1268 "context": self.config().build_context(None),
1269 "query": query,
1270 "params": "EgWKAQIIAWoQEAMQBBAJEAoQBRAREBAQFQ%3D%3D"
1271 });
1272 let url = format!("{}/youtubei/v1/search?prettyPrint=false", INNERTUBE_API);
1273 let mut req = self
1274 .http
1275 .post(&url)
1276 .header("X-Goog-Api-Format-Version", "2")
1277 .header("User-Agent", USER_AGENT);
1278 if let Some(vd) = visitor_data {
1279 req = req.header("X-Goog-Visitor-Id", vd);
1280 }
1281 let req = req.json(&body);
1282 let res = req.send().await?;
1283 if !res.status().is_success() {
1284 let status = res.status();
1285 let err_body = res.text().await.unwrap_or_default();
1286 return Err(
1287 format!("Music Android search failed: {} - {}", status, err_body).into(),
1288 );
1289 }
1290 let response: Value = res.json().await.unwrap_or_default();
1291 let mut tracks = Vec::new();
1292 let tab_content = response
1293 .get("contents")
1294 .and_then(|c| c.get("tabbedSearchResultsRenderer"))
1295 .and_then(|t| t.get("tabs"))
1296 .and_then(|t| t.get(0))
1297 .and_then(|t| t.get("tabRenderer"))
1298 .and_then(|t| t.get("content"));
1299 let mut videos = None;
1300 fn find_shelf(contents: &Value) -> Option<&Vec<Value>> {
1301 if let Some(sections) = contents.as_array() {
1302 for section in sections {
1303 if let Some(shelf) = section.get("musicShelfRenderer") {
1304 return shelf.get("contents").and_then(|c| c.as_array());
1305 }
1306 }
1307 }
1308 None
1309 }
1310 if let Some(tab) = tab_content {
1311 if let Some(section_list) = tab.get("sectionListRenderer")
1312 && let Some(contents) = section_list.get("contents")
1313 {
1314 videos = find_shelf(contents);
1315 }
1316 if videos.is_none()
1317 && let Some(split_view) = tab.get("musicSplitViewRenderer")
1318 && let Some(main_content) = split_view.get("mainContent")
1319 && let Some(section_list) = main_content.get("sectionListRenderer")
1320 && let Some(contents) = section_list.get("contents")
1321 {
1322 videos = find_shelf(contents);
1323 }
1324 }
1325 if let Some(items) = videos {
1326 for item in items {
1327 let renderer = item
1328 .get("musicResponsiveListItemRenderer")
1329 .or_else(|| item.get("musicTwoColumnItemRenderer"))
1330 .or_else(|| {
1331 if item.get("videoId").is_some() {
1332 Some(item)
1333 } else {
1334 None
1335 }
1336 });
1337 if let Some(renderer) = renderer {
1338 let id = renderer
1339 .get("playlistItemData")
1340 .and_then(|d| d.get("videoId"))
1341 .and_then(|v| v.as_str())
1342 .or_else(|| {
1343 renderer
1344 .get("navigationEndpoint")
1345 .and_then(|n| n.get("watchEndpoint"))
1346 .and_then(|w| w.get("videoId"))
1347 .and_then(|v| v.as_str())
1348 })
1349 .or_else(|| {
1350 renderer
1351 .get("doubleTapCommand")
1352 .and_then(|c| c.get("watchEndpoint"))
1353 .and_then(|w| w.get("videoId"))
1354 .and_then(|v| v.as_str())
1355 })
1356 .or_else(|| renderer.get("videoId").and_then(|v| v.as_str()));
1357 if let Some(id) = id {
1358 let mut title = renderer
1359 .get("title")
1360 .and_then(|t| t.get("runs"))
1361 .and_then(|r| r.get(0))
1362 .and_then(|r| r.get("text"))
1363 .and_then(|t| t.as_str())
1364 .or_else(|| {
1365 renderer
1366 .get("title")
1367 .and_then(|t| t.get("simpleText"))
1368 .and_then(|t| t.as_str())
1369 })
1370 .or_else(|| renderer.get("title").and_then(|t| t.as_str()))
1371 .unwrap_or("Unknown Title");
1372 if title == "Unknown Title"
1373 && let Some(flex_cols) =
1374 renderer.get("flexColumns").and_then(|c| c.as_array())
1375 && !flex_cols.is_empty()
1376 && let Some(t) = flex_cols[0]
1377 .get("musicResponsiveListItemFlexColumnRenderer")
1378 .and_then(|r| r.get("text"))
1379 .and_then(|t| t.get("runs"))
1380 .and_then(|r| r.get(0))
1381 .and_then(|r| r.get("text"))
1382 .and_then(|t| t.as_str())
1383 {
1384 title = t;
1385 }
1386 let mut author = "Unknown Artist".to_string();
1387 let subtitle_runs = renderer
1388 .get("subtitle")
1389 .and_then(|s| s.get("runs"))
1390 .and_then(|r| r.as_array());
1391 let long_byline_runs = renderer
1392 .get("longBylineText")
1393 .and_then(|l| l.get("runs"))
1394 .and_then(|r| r.as_array());
1395 let short_byline_runs = renderer
1396 .get("shortBylineText")
1397 .and_then(|s| s.get("runs"))
1398 .and_then(|r| r.as_array());
1399 if let Some(runs) = subtitle_runs {
1400 if !runs.is_empty()
1401 && let Some(a) = runs[0].get("text").and_then(|t| t.as_str())
1402 {
1403 author = a.to_string();
1404 }
1405 } else if let Some(runs) = long_byline_runs {
1406 if !runs.is_empty()
1407 && let Some(a) = runs[0].get("text").and_then(|t| t.as_str())
1408 {
1409 author = a.to_string();
1410 }
1411 } else if let Some(runs) = short_byline_runs {
1412 if !runs.is_empty()
1413 && let Some(a) = runs[0].get("text").and_then(|t| t.as_str())
1414 {
1415 author = a.to_string();
1416 }
1417 } else if let Some(a) = renderer.get("author").and_then(|a| a.as_str())
1418 {
1419 author = a.to_string();
1420 }
1421 if author == "Unknown Artist"
1422 && let Some(flex_cols) =
1423 renderer.get("flexColumns").and_then(|c| c.as_array())
1424 && flex_cols.len() > 1
1425 && let Some(a) = flex_cols[1]
1426 .get("musicResponsiveListItemFlexColumnRenderer")
1427 .and_then(|r| r.get("text"))
1428 .and_then(|t| t.get("runs"))
1429 .and_then(|r| r.get(0))
1430 .and_then(|r| r.get("text"))
1431 .and_then(|t| t.as_str())
1432 {
1433 author = a.to_string();
1434 }
1435 let mut length_ms = 0u64;
1436 if let Some(runs) = subtitle_runs {
1437 for run in runs {
1438 if let Some(text) = run.get("text").and_then(|t| t.as_str())
1439 && is_duration(text)
1440 {
1441 length_ms = parse_duration(text);
1442 break;
1443 }
1444 }
1445 }
1446 if length_ms == 0
1447 && let Some(text) = renderer
1448 .get("lengthText")
1449 .and_then(|l| l.get("simpleText"))
1450 .and_then(|t| t.as_str())
1451 && is_duration(text)
1452 {
1453 length_ms = parse_duration(text);
1454 }
1455 if length_ms == 0
1456 && let Some(runs) = renderer
1457 .get("lengthText")
1458 .and_then(|l| l.get("runs"))
1459 .and_then(|r| r.as_array())
1460 {
1461 for run in runs {
1462 if let Some(text) = run.get("text").and_then(|t| t.as_str())
1463 && is_duration(text)
1464 {
1465 length_ms = parse_duration(text);
1466 break;
1467 }
1468 }
1469 }
1470 if length_ms == 0
1471 && let Some(flex_cols) =
1472 renderer.get("flexColumns").and_then(|c| c.as_array())
1473 {
1474 for column in flex_cols {
1475 if let Some(runs) = column
1476 .get("musicResponsiveListItemFlexColumnRenderer")
1477 .and_then(|r| r.get("text"))
1478 .and_then(|t| t.get("runs"))
1479 .and_then(|r| r.as_array())
1480 {
1481 for run in runs {
1482 if let Some(text) =
1483 run.get("text").and_then(|t| t.as_str())
1484 && is_duration(text)
1485 {
1486 length_ms = parse_duration(text);
1487 break;
1488 }
1489 }
1490 }
1491 if length_ms > 0 {
1492 break;
1493 }
1494 }
1495 }
1496 let artwork_url = extract_thumbnail(renderer, Some(id));
1497 let info = TrackInfo {
1498 identifier: id.to_string(),
1499 is_seekable: true,
1500 title: title.to_string(),
1501 author,
1502 length: length_ms,
1503 is_stream: false,
1504 uri: Some(format!("https://music.youtube.com/watch?v={}", id)),
1505 source_name: "youtube".to_string(),
1506 isrc: None,
1507 artwork_url,
1508 position: 0,
1509 };
1510 tracks.push(Track::new(info));
1511 }
1512 }
1513 }
1514 }
1515 Ok(tracks)
1516 }
1517 async fn get_track_info(
1518 &self,
1519 track_id: &str,
1520 context: &Value,
1521 oauth: Arc<YouTubeOAuth>,
1522 ) -> AnyResult<Option<Track>> {
1523 core::standard_get_track_info(
1524 self,
1525 core::StandardPlayerOptions {
1526 http: &self.http,
1527 track_id,
1528 context,
1529 oauth,
1530 signature_timestamp: None,
1531 encrypted_host_flags: None,
1532 config_builder: || self.config(),
1533 },
1534 )
1535 .await
1536 }
1537 async fn get_playlist(
1538 &self,
1539 playlist_id: &str,
1540 context: &Value,
1541 _oauth: Arc<YouTubeOAuth>,
1542 ) -> AnyResult<Option<(Vec<Track>, String)>> {
1543 let visitor_data = core::extract_visitor_data(context);
1544 let next_body = json!({
1545 "context": self.config().build_context(visitor_data),
1546 "playlistId": playlist_id,
1547 "enablePersistentPlaylistPanel": true,
1548 "isAudioOnly": true
1549 });
1550 let next_url = format!("{}/youtubei/v1/next?prettyPrint=false", INNERTUBE_API);
1551 let mut next_req = self
1552 .http
1553 .post(&next_url)
1554 .header("User-Agent", USER_AGENT)
1555 .header("X-YouTube-Client-Name", "67")
1556 .header("X-YouTube-Client-Version", CLIENT_VERSION);
1557 if let Some(vd) = visitor_data {
1558 next_req = next_req.header("X-Goog-Visitor-Id", vd);
1559 }
1560 let next_req = next_req.json(&next_body);
1561 if let Ok(res) = next_req.send().await
1562 && res.status().is_success()
1563 {
1564 let body: Value = res.json().await?;
1565 if let Some(result) =
1566 crate::sources::youtube::extractor::extract_from_next(&body, "youtube")
1567 {
1568 return Ok(Some(result));
1569 }
1570 tracing::debug!(
1571 "MusicAndroid: /next endpoint returned but extraction failed for playlist {}",
1572 playlist_id
1573 );
1574 }
1575 let browse_body = json!({
1576 "context": self.config().build_context(visitor_data),
1577 "browseId": if playlist_id.starts_with("VL") { playlist_id.to_string() } else { format!("VL{}", playlist_id) },
1578 });
1579 let browse_url = format!("{}/youtubei/v1/browse?prettyPrint=false", INNERTUBE_API);
1580 let mut browse_req = self
1581 .http
1582 .post(&browse_url)
1583 .header("User-Agent", USER_AGENT)
1584 .header("X-YouTube-Client-Name", "67")
1585 .header("X-YouTube-Client-Version", CLIENT_VERSION);
1586 if let Some(vd) = visitor_data {
1587 browse_req = browse_req.header("X-Goog-Visitor-Id", vd);
1588 }
1589 if let Ok(res) = browse_req.json(&browse_body).send().await
1590 && res.status().is_success()
1591 {
1592 let body: Value = res.json().await?;
1593 if let Some(result) =
1594 crate::sources::youtube::extractor::extract_from_browse(&body, "youtube")
1595 {
1596 return Ok(Some(result));
1597 }
1598 tracing::debug!(
1599 "MusicAndroid: /browse endpoint returned but extraction failed for playlist {}",
1600 playlist_id
1601 );
1602 }
1603 tracing::warn!(
1604 "MusicAndroid: Both /next and /browse endpoints failed for playlist {}",
1605 playlist_id
1606 );
1607 Ok(None)
1608 }
1609 async fn resolve_url(
1610 &self,
1611 _url: &str,
1612 _context: &Value,
1613 _oauth: Arc<YouTubeOAuth>,
1614 ) -> AnyResult<Option<Track>> {
1615 Ok(None)
1616 }
1617 async fn get_track_url(
1618 &self,
1619 track_id: &str,
1620 context: &Value,
1621 cipher_manager: Arc<YouTubeCipherManager>,
1622 oauth: Arc<YouTubeOAuth>,
1623 ) -> AnyResult<Option<String>> {
1624 let signature_timestamp = cipher_manager.get_signature_timestamp().await.ok();
1625 core::standard_get_track_url(
1626 self,
1627 core::StandardUrlOptions {
1628 http: &self.http,
1629 track_id,
1630 context,
1631 cipher_manager,
1632 oauth,
1633 signature_timestamp,
1634 encrypted_host_flags: None,
1635 config_builder: || self.config(),
1636 },
1637 )
1638 .await
1639 }
1640 async fn get_player_body(
1641 &self,
1642 track_id: &str,
1643 visitor_data: Option<&str>,
1644 _oauth: Arc<YouTubeOAuth>,
1645 ) -> Option<serde_json::Value> {
1646 crate::sources::youtube::clients::common::make_player_request(
1647 crate::sources::youtube::clients::common::PlayerRequestOptions {
1648 http: &self.http,
1649 config: &self.config(),
1650 video_id: track_id,
1651 params: None,
1652 visitor_data,
1653 signature_timestamp: None,
1654 auth_header: None,
1655 referer: None,
1656 origin: Some(INNERTUBE_API),
1657 po_token: None,
1658 encrypted_host_flags: None,
1659 attestation_request: None,
1660 serialized_third_party_embed_config: false,
1661 },
1662 )
1663 .await
1664 .ok()
1665 }
1666 }
1667}
1668pub mod mweb {
1669 use super::{YouTubeClient, core};
1670 use crate::{
1671 common::types::AnyResult,
1672 protocol::tracks::Track,
1673 sources::youtube::{
1674 cipher::YouTubeCipherManager, clients::common::ClientConfig, oauth::YouTubeOAuth,
1675 },
1676 };
1677 use async_trait::async_trait;
1678 use serde_json::Value;
1679 use std::sync::Arc;
1680 const CLIENT_NAME: &str = "MWEB";
1681 const CLIENT_ID: &str = "2";
1682 const CLIENT_VERSION: &str = "2.20241022.01.00";
1683 const USER_AGENT: &str = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36";
1684 pub struct MWebClient {
1685 http: Arc<reqwest::Client>,
1686 }
1687 impl MWebClient {
1688 pub fn new(http: Arc<reqwest::Client>) -> Self {
1689 Self { http }
1690 }
1691 fn config(&self) -> ClientConfig<'static> {
1692 ClientConfig {
1693 client_name: CLIENT_NAME,
1694 client_version: CLIENT_VERSION,
1695 client_id: CLIENT_ID,
1696 user_agent: USER_AGENT,
1697 ..Default::default()
1698 }
1699 }
1700 }
1701 #[async_trait]
1702 impl YouTubeClient for MWebClient {
1703 fn name(&self) -> &str {
1704 "MWeb"
1705 }
1706 fn client_name(&self) -> &str {
1707 CLIENT_NAME
1708 }
1709 fn client_version(&self) -> &str {
1710 CLIENT_VERSION
1711 }
1712 fn user_agent(&self) -> &str {
1713 USER_AGENT
1714 }
1715 async fn search(
1716 &self,
1717 query: &str,
1718 context: &Value,
1719 oauth: Arc<YouTubeOAuth>,
1720 ) -> AnyResult<Vec<Track>> {
1721 core::standard_search(self, &self.http, query, context, oauth, || self.config()).await
1722 }
1723 async fn get_track_info(
1724 &self,
1725 track_id: &str,
1726 context: &Value,
1727 oauth: Arc<YouTubeOAuth>,
1728 ) -> AnyResult<Option<Track>> {
1729 core::standard_get_track_info(
1730 self,
1731 core::StandardPlayerOptions {
1732 http: &self.http,
1733 track_id,
1734 context,
1735 oauth,
1736 signature_timestamp: None,
1737 encrypted_host_flags: None,
1738 config_builder: || self.config(),
1739 },
1740 )
1741 .await
1742 }
1743 async fn get_playlist(
1744 &self,
1745 playlist_id: &str,
1746 context: &Value,
1747 oauth: Arc<YouTubeOAuth>,
1748 ) -> AnyResult<Option<(Vec<Track>, String)>> {
1749 core::standard_get_playlist(self, &self.http, playlist_id, context, oauth, || {
1750 self.config()
1751 })
1752 .await
1753 }
1754 async fn resolve_url(
1755 &self,
1756 _url: &str,
1757 _context: &Value,
1758 _oauth: Arc<YouTubeOAuth>,
1759 ) -> AnyResult<Option<Track>> {
1760 Ok(None)
1761 }
1762 async fn get_track_url(
1763 &self,
1764 track_id: &str,
1765 context: &Value,
1766 cipher_manager: Arc<YouTubeCipherManager>,
1767 oauth: Arc<YouTubeOAuth>,
1768 ) -> AnyResult<Option<String>> {
1769 let signature_timestamp = cipher_manager.get_signature_timestamp().await.ok();
1770 core::standard_get_track_url(
1771 self,
1772 core::StandardUrlOptions {
1773 http: &self.http,
1774 track_id,
1775 context,
1776 cipher_manager,
1777 oauth,
1778 signature_timestamp,
1779 encrypted_host_flags: None,
1780 config_builder: || self.config(),
1781 },
1782 )
1783 .await
1784 }
1785 async fn get_player_body(
1786 &self,
1787 track_id: &str,
1788 visitor_data: Option<&str>,
1789 _oauth: Arc<YouTubeOAuth>,
1790 ) -> Option<serde_json::Value> {
1791 crate::sources::youtube::clients::common::make_player_request(
1792 crate::sources::youtube::clients::common::PlayerRequestOptions {
1793 http: &self.http,
1794 config: &self.config(),
1795 video_id: track_id,
1796 params: None,
1797 visitor_data,
1798 signature_timestamp: None,
1799 auth_header: None,
1800 referer: Some("https://m.youtube.com"),
1801 origin: None,
1802 po_token: None,
1803 encrypted_host_flags: None,
1804 attestation_request: None,
1805 serialized_third_party_embed_config: false,
1806 },
1807 )
1808 .await
1809 .ok()
1810 }
1811 }
1812}
1813pub mod tv {
1814 use super::{YouTubeClient, core};
1815 use crate::{
1816 common::types::AnyResult,
1817 protocol::tracks::Track,
1818 sources::youtube::{
1819 cipher::YouTubeCipherManager, clients::common::ClientConfig, oauth::YouTubeOAuth,
1820 },
1821 };
1822 use async_trait::async_trait;
1823 use serde_json::Value;
1824 use std::sync::Arc;
1825 const CLIENT_NAME: &str = "TVHTML5";
1826 const CLIENT_ID: &str = "7";
1827 const CLIENT_VERSION: &str = "7.20260113.16.00";
1828 const USER_AGENT: &str = "Mozilla/5.0 (Fuchsia) AppleWebKit/537.36 (KHTML, like Gecko) \
1829 Chrome/140.0.0.0 Safari/537.36 CrKey/1.56.500000";
1830 pub struct TvClient {
1831 http: Arc<reqwest::Client>,
1832 }
1833 impl TvClient {
1834 pub fn new(http: Arc<reqwest::Client>) -> Self {
1835 Self { http }
1836 }
1837 fn config(&self) -> ClientConfig<'static> {
1838 ClientConfig {
1839 client_name: CLIENT_NAME,
1840 client_version: CLIENT_VERSION,
1841 client_id: CLIENT_ID,
1842 user_agent: USER_AGENT,
1843 ..Default::default()
1844 }
1845 }
1846 }
1847 #[async_trait]
1848 impl YouTubeClient for TvClient {
1849 fn name(&self) -> &str {
1850 "TV"
1851 }
1852 fn client_name(&self) -> &str {
1853 CLIENT_NAME
1854 }
1855 fn client_version(&self) -> &str {
1856 CLIENT_VERSION
1857 }
1858 fn user_agent(&self) -> &str {
1859 USER_AGENT
1860 }
1861 fn supports_oauth(&self) -> bool {
1862 true
1863 }
1864 fn can_handle_request(&self, _identifier: &str) -> bool {
1865 false
1866 }
1867 async fn search(
1868 &self,
1869 query: &str,
1870 context: &Value,
1871 oauth: Arc<YouTubeOAuth>,
1872 ) -> AnyResult<Vec<Track>> {
1873 core::standard_search(self, &self.http, query, context, oauth, || self.config()).await
1874 }
1875 async fn get_track_info(
1876 &self,
1877 track_id: &str,
1878 context: &Value,
1879 oauth: Arc<YouTubeOAuth>,
1880 ) -> AnyResult<Option<Track>> {
1881 core::standard_get_track_info(
1882 self,
1883 core::StandardPlayerOptions {
1884 http: &self.http,
1885 track_id,
1886 context,
1887 oauth,
1888 signature_timestamp: None,
1889 encrypted_host_flags: None,
1890 config_builder: || self.config(),
1891 },
1892 )
1893 .await
1894 }
1895 async fn get_playlist(
1896 &self,
1897 playlist_id: &str,
1898 context: &Value,
1899 oauth: Arc<YouTubeOAuth>,
1900 ) -> AnyResult<Option<(Vec<Track>, String)>> {
1901 core::standard_get_playlist(self, &self.http, playlist_id, context, oauth, || {
1902 self.config()
1903 })
1904 .await
1905 }
1906 async fn resolve_url(
1907 &self,
1908 _url: &str,
1909 _context: &Value,
1910 _oauth: Arc<YouTubeOAuth>,
1911 ) -> AnyResult<Option<Track>> {
1912 Ok(None)
1913 }
1914 async fn get_track_url(
1915 &self,
1916 track_id: &str,
1917 context: &Value,
1918 cipher_manager: Arc<YouTubeCipherManager>,
1919 oauth: Arc<YouTubeOAuth>,
1920 ) -> AnyResult<Option<String>> {
1921 let signature_timestamp = cipher_manager.get_signature_timestamp().await.ok();
1922 core::standard_get_track_url(
1923 self,
1924 core::StandardUrlOptions {
1925 http: &self.http,
1926 track_id,
1927 context,
1928 cipher_manager,
1929 oauth,
1930 signature_timestamp,
1931 encrypted_host_flags: None,
1932 config_builder: || self.config(),
1933 },
1934 )
1935 .await
1936 }
1937 async fn get_player_body(
1938 &self,
1939 track_id: &str,
1940 visitor_data: Option<&str>,
1941 oauth: Arc<YouTubeOAuth>,
1942 ) -> Option<serde_json::Value> {
1943 crate::sources::youtube::clients::common::make_player_request(
1944 crate::sources::youtube::clients::common::PlayerRequestOptions {
1945 http: &self.http,
1946 config: &self.config(),
1947 video_id: track_id,
1948 params: None,
1949 visitor_data,
1950 signature_timestamp: None,
1951 auth_header: oauth.get_auth_header().await,
1952 referer: None,
1953 origin: None,
1954 po_token: None,
1955 encrypted_host_flags: None,
1956 attestation_request: None,
1957 serialized_third_party_embed_config: false,
1958 },
1959 )
1960 .await
1961 .ok()
1962 }
1963 }
1964}
1965pub mod tv_cast {
1966 use super::{YouTubeClient, core};
1967 use crate::{
1968 common::types::AnyResult,
1969 protocol::tracks::Track,
1970 sources::youtube::{
1971 cipher::YouTubeCipherManager, clients::common::ClientConfig, oauth::YouTubeOAuth,
1972 },
1973 };
1974 use async_trait::async_trait;
1975 use serde_json::Value;
1976 use std::sync::Arc;
1977 const CLIENT_NAME_OVERRIDE: &str = "TVHTML5_CAST";
1978 const CLIENT_VERSION: &str = "7.20190924";
1979 const USER_AGENT: &str = "Mozilla/5.0 (Linux; Android) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 CrKey/1.54.248666";
1980 pub struct TvCastClient {
1981 http: Arc<reqwest::Client>,
1982 }
1983 impl TvCastClient {
1984 pub fn new(http: Arc<reqwest::Client>) -> Self {
1985 Self { http }
1986 }
1987 fn config(&self) -> ClientConfig<'static> {
1988 ClientConfig {
1989 client_name: CLIENT_NAME_OVERRIDE,
1990 client_version: CLIENT_VERSION,
1991 client_id: "7",
1992 user_agent: USER_AGENT,
1993 ..Default::default()
1994 }
1995 }
1996 }
1997 #[async_trait]
1998 impl YouTubeClient for TvCastClient {
1999 fn name(&self) -> &str {
2000 "TV Cast"
2001 }
2002 fn client_name(&self) -> &str {
2003 CLIENT_NAME_OVERRIDE
2004 }
2005 fn client_version(&self) -> &str {
2006 CLIENT_VERSION
2007 }
2008 fn user_agent(&self) -> &str {
2009 USER_AGENT
2010 }
2011 fn can_handle_request(&self, _identifier: &str) -> bool {
2012 false
2013 }
2014 async fn search(
2015 &self,
2016 query: &str,
2017 context: &Value,
2018 oauth: Arc<YouTubeOAuth>,
2019 ) -> AnyResult<Vec<Track>> {
2020 core::standard_search(self, &self.http, query, context, oauth, || self.config()).await
2021 }
2022 async fn get_track_info(
2023 &self,
2024 track_id: &str,
2025 context: &Value,
2026 oauth: Arc<YouTubeOAuth>,
2027 ) -> AnyResult<Option<Track>> {
2028 core::standard_get_track_info(
2029 self,
2030 core::StandardPlayerOptions {
2031 http: &self.http,
2032 track_id,
2033 context,
2034 oauth,
2035 signature_timestamp: None,
2036 encrypted_host_flags: None,
2037 config_builder: || self.config(),
2038 },
2039 )
2040 .await
2041 }
2042 async fn get_playlist(
2043 &self,
2044 playlist_id: &str,
2045 context: &Value,
2046 oauth: Arc<YouTubeOAuth>,
2047 ) -> AnyResult<Option<(Vec<Track>, String)>> {
2048 core::standard_get_playlist(self, &self.http, playlist_id, context, oauth, || {
2049 self.config()
2050 })
2051 .await
2052 }
2053 async fn resolve_url(
2054 &self,
2055 _url: &str,
2056 _context: &Value,
2057 _oauth: Arc<YouTubeOAuth>,
2058 ) -> AnyResult<Option<Track>> {
2059 Ok(None)
2060 }
2061 async fn get_track_url(
2062 &self,
2063 track_id: &str,
2064 context: &Value,
2065 cipher_manager: Arc<YouTubeCipherManager>,
2066 oauth: Arc<YouTubeOAuth>,
2067 ) -> AnyResult<Option<String>> {
2068 let signature_timestamp = cipher_manager.get_signature_timestamp().await.ok();
2069 core::standard_get_track_url(
2070 self,
2071 core::StandardUrlOptions {
2072 http: &self.http,
2073 track_id,
2074 context,
2075 cipher_manager,
2076 oauth,
2077 signature_timestamp,
2078 encrypted_host_flags: None,
2079 config_builder: || self.config(),
2080 },
2081 )
2082 .await
2083 }
2084 async fn get_player_body(
2085 &self,
2086 track_id: &str,
2087 visitor_data: Option<&str>,
2088 _oauth: Arc<YouTubeOAuth>,
2089 ) -> Option<serde_json::Value> {
2090 crate::sources::youtube::clients::common::make_player_request(
2091 crate::sources::youtube::clients::common::PlayerRequestOptions {
2092 http: &self.http,
2093 config: &self.config(),
2094 video_id: track_id,
2095 params: None,
2096 visitor_data,
2097 signature_timestamp: None,
2098 auth_header: None,
2099 referer: None,
2100 origin: None,
2101 po_token: None,
2102 encrypted_host_flags: None,
2103 attestation_request: None,
2104 serialized_third_party_embed_config: false,
2105 },
2106 )
2107 .await
2108 .ok()
2109 }
2110 }
2111}
2112pub mod tv_embedded {
2113 use super::{YouTubeClient, core};
2114 use crate::{
2115 common::types::AnyResult,
2116 protocol::tracks::Track,
2117 sources::youtube::{
2118 cipher::YouTubeCipherManager, clients::common::ClientConfig, oauth::YouTubeOAuth,
2119 },
2120 };
2121 use async_trait::async_trait;
2122 use serde_json::Value;
2123 use std::sync::Arc;
2124 const CLIENT_NAME: &str = "TVHTML5_SIMPLY_EMBEDDED_PLAYER";
2125 const CLIENT_ID: &str = "85";
2126 const CLIENT_VERSION: &str = "2.0";
2127 const USER_AGENT: &str = "Mozilla/5.0 (Linux armeabi-v7a; Android 7.1.2; Fire OS 6.0) Cobalt/22.lts.3.306369-gold (unlike Gecko) v8/8.8.278.8-jit gles Starboard/13, Amazon_ATV_mediatek8695_2019/NS6294 (Amazon, AFTMM, Wireless) com.amazon.firetv.youtube/22.3.r2.v66.0";
2128 pub struct TvEmbeddedClient {
2129 http: Arc<reqwest::Client>,
2130 }
2131 impl TvEmbeddedClient {
2132 pub fn new(http: Arc<reqwest::Client>) -> Self {
2133 Self { http }
2134 }
2135 fn config(&self) -> ClientConfig<'static> {
2136 ClientConfig {
2137 client_name: CLIENT_NAME,
2138 client_version: CLIENT_VERSION,
2139 client_id: CLIENT_ID,
2140 user_agent: USER_AGENT,
2141 third_party_embed_url: Some("https://www.youtube.com/tv"),
2142 ..Default::default()
2143 }
2144 }
2145 }
2146 #[async_trait]
2147 impl YouTubeClient for TvEmbeddedClient {
2148 fn name(&self) -> &str {
2149 "TvEmbedded"
2150 }
2151 fn client_name(&self) -> &str {
2152 CLIENT_NAME
2153 }
2154 fn client_version(&self) -> &str {
2155 CLIENT_VERSION
2156 }
2157 fn user_agent(&self) -> &str {
2158 USER_AGENT
2159 }
2160 fn supports_oauth(&self) -> bool {
2161 true
2162 }
2163 fn can_handle_request(&self, identifier: &str) -> bool {
2164 !identifier.contains("list=")
2165 }
2166 async fn search(
2167 &self,
2168 query: &str,
2169 context: &Value,
2170 oauth: Arc<YouTubeOAuth>,
2171 ) -> AnyResult<Vec<Track>> {
2172 core::standard_search(self, &self.http, query, context, oauth, || self.config()).await
2173 }
2174 async fn get_track_info(
2175 &self,
2176 track_id: &str,
2177 context: &Value,
2178 oauth: Arc<YouTubeOAuth>,
2179 ) -> AnyResult<Option<Track>> {
2180 core::standard_get_track_info(
2181 self,
2182 core::StandardPlayerOptions {
2183 http: &self.http,
2184 track_id,
2185 context,
2186 oauth,
2187 signature_timestamp: None,
2188 encrypted_host_flags: None,
2189 config_builder: || self.config(),
2190 },
2191 )
2192 .await
2193 }
2194 async fn get_playlist(
2195 &self,
2196 playlist_id: &str,
2197 context: &Value,
2198 oauth: Arc<YouTubeOAuth>,
2199 ) -> AnyResult<Option<(Vec<Track>, String)>> {
2200 core::standard_get_playlist(self, &self.http, playlist_id, context, oauth, || {
2201 self.config()
2202 })
2203 .await
2204 }
2205 async fn resolve_url(
2206 &self,
2207 _url: &str,
2208 _context: &Value,
2209 _oauth: Arc<YouTubeOAuth>,
2210 ) -> AnyResult<Option<Track>> {
2211 Ok(None)
2212 }
2213 async fn get_track_url(
2214 &self,
2215 track_id: &str,
2216 context: &Value,
2217 cipher_manager: Arc<YouTubeCipherManager>,
2218 oauth: Arc<YouTubeOAuth>,
2219 ) -> AnyResult<Option<String>> {
2220 let signature_timestamp = cipher_manager.get_signature_timestamp().await.ok();
2221 core::standard_get_track_url(
2222 self,
2223 core::StandardUrlOptions {
2224 http: &self.http,
2225 track_id,
2226 context,
2227 cipher_manager,
2228 oauth,
2229 signature_timestamp,
2230 encrypted_host_flags: None,
2231 config_builder: || self.config(),
2232 },
2233 )
2234 .await
2235 }
2236 async fn get_player_body(
2237 &self,
2238 track_id: &str,
2239 visitor_data: Option<&str>,
2240 oauth: Arc<YouTubeOAuth>,
2241 ) -> Option<serde_json::Value> {
2242 crate::sources::youtube::clients::common::make_player_request(
2243 crate::sources::youtube::clients::common::PlayerRequestOptions {
2244 http: &self.http,
2245 config: &self.config(),
2246 video_id: track_id,
2247 params: Some("2AMB"),
2248 visitor_data,
2249 signature_timestamp: None,
2250 auth_header: oauth.get_auth_header().await,
2251 referer: None,
2252 origin: None,
2253 po_token: None,
2254 encrypted_host_flags: None,
2255 attestation_request: None,
2256 serialized_third_party_embed_config: false,
2257 },
2258 )
2259 .await
2260 .ok()
2261 }
2262 }
2263}
2264pub mod tv_simply {
2265 use super::{YouTubeClient, core};
2266 use crate::{
2267 common::types::AnyResult,
2268 protocol::tracks::Track,
2269 sources::youtube::{
2270 cipher::YouTubeCipherManager, clients::common::ClientConfig, oauth::YouTubeOAuth,
2271 },
2272 };
2273 use async_trait::async_trait;
2274 use serde_json::{Value, json};
2275 use std::sync::Arc;
2276 const CLIENT_NAME: &str = "TVHTML5_SIMPLY";
2277 const CLIENT_VERSION: &str = "1.0";
2278 const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36";
2279 pub struct TvSimplyClient {
2280 http: Arc<reqwest::Client>,
2281 }
2282 impl TvSimplyClient {
2283 pub fn new(http: Arc<reqwest::Client>) -> Self {
2284 Self { http }
2285 }
2286 fn config(&self) -> ClientConfig<'static> {
2287 ClientConfig {
2288 client_name: CLIENT_NAME,
2289 client_version: CLIENT_VERSION,
2290 client_id: "TVHTML5_SIMPLY",
2291 user_agent: USER_AGENT,
2292 attestation_request: Some(json!({ "omitBotguardData": true })),
2293 ..Default::default()
2294 }
2295 }
2296 async fn fetch_encrypted_host_flags(&self, video_id: &str) -> Option<String> {
2297 let url = format!("https://www.youtube.com/embed/{}", video_id);
2298 let res = self
2299 .http
2300 .get(&url)
2301 .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
2302 .send()
2303 .await
2304 .ok()?;
2305 let html = res.text().await.ok()?;
2306 let re = regex::Regex::new(r#""encryptedHostFlags":"([^"]+)""#).ok()?;
2307 re.captures(&html).map(|caps| caps[1].to_string())
2308 }
2309 }
2310 #[async_trait]
2311 impl YouTubeClient for TvSimplyClient {
2312 fn name(&self) -> &str {
2313 "TvSimply"
2314 }
2315 fn client_name(&self) -> &str {
2316 CLIENT_NAME
2317 }
2318 fn client_version(&self) -> &str {
2319 CLIENT_VERSION
2320 }
2321 fn user_agent(&self) -> &str {
2322 USER_AGENT
2323 }
2324 async fn search(
2325 &self,
2326 query: &str,
2327 context: &Value,
2328 oauth: Arc<YouTubeOAuth>,
2329 ) -> AnyResult<Vec<Track>> {
2330 core::standard_search(self, &self.http, query, context, oauth, || self.config()).await
2331 }
2332 async fn get_track_info(
2333 &self,
2334 track_id: &str,
2335 context: &Value,
2336 oauth: Arc<YouTubeOAuth>,
2337 ) -> AnyResult<Option<Track>> {
2338 let encrypted_host_flags = self.fetch_encrypted_host_flags(track_id).await;
2339 core::standard_get_track_info(
2340 self,
2341 core::StandardPlayerOptions {
2342 http: &self.http,
2343 track_id,
2344 context,
2345 oauth,
2346 signature_timestamp: None,
2347 encrypted_host_flags,
2348 config_builder: || self.config(),
2349 },
2350 )
2351 .await
2352 }
2353 async fn get_playlist(
2354 &self,
2355 playlist_id: &str,
2356 context: &Value,
2357 oauth: Arc<YouTubeOAuth>,
2358 ) -> AnyResult<Option<(Vec<Track>, String)>> {
2359 core::standard_get_playlist(self, &self.http, playlist_id, context, oauth, || {
2360 self.config()
2361 })
2362 .await
2363 }
2364 async fn resolve_url(
2365 &self,
2366 _url: &str,
2367 _context: &Value,
2368 _oauth: Arc<YouTubeOAuth>,
2369 ) -> AnyResult<Option<Track>> {
2370 Ok(None)
2371 }
2372 async fn get_track_url(
2373 &self,
2374 track_id: &str,
2375 context: &Value,
2376 cipher_manager: Arc<YouTubeCipherManager>,
2377 oauth: Arc<YouTubeOAuth>,
2378 ) -> AnyResult<Option<String>> {
2379 let signature_timestamp = cipher_manager.get_signature_timestamp().await.ok();
2380 let encrypted_host_flags = self.fetch_encrypted_host_flags(track_id).await;
2381 core::standard_get_track_url(
2382 self,
2383 core::StandardUrlOptions {
2384 http: &self.http,
2385 track_id,
2386 context,
2387 cipher_manager,
2388 oauth,
2389 signature_timestamp,
2390 encrypted_host_flags,
2391 config_builder: || self.config(),
2392 },
2393 )
2394 .await
2395 }
2396 async fn get_player_body(
2397 &self,
2398 track_id: &str,
2399 visitor_data: Option<&str>,
2400 _oauth: Arc<YouTubeOAuth>,
2401 ) -> Option<serde_json::Value> {
2402 let encrypted_host_flags = self.fetch_encrypted_host_flags(track_id).await;
2403 crate::sources::youtube::clients::common::make_player_request(
2404 crate::sources::youtube::clients::common::PlayerRequestOptions {
2405 http: &self.http,
2406 config: &self.config(),
2407 video_id: track_id,
2408 params: Some("2AMB"),
2409 visitor_data,
2410 signature_timestamp: None,
2411 auth_header: None,
2412 referer: None,
2413 origin: Some("https://www.youtube.com"),
2414 po_token: None,
2415 encrypted_host_flags,
2416 attestation_request: Some(json!({ "omitBotguardData": true })),
2417 serialized_third_party_embed_config: false,
2418 },
2419 )
2420 .await
2421 .ok()
2422 }
2423 }
2424 #[cfg(test)]
2425 mod tests {
2426 use super::*;
2427 use crate::{
2428 config::sources::YouTubeCipherConfig, sources::youtube::cipher::YouTubeCipherManager,
2429 };
2430 #[tokio::test]
2431 async fn test_search() {
2432 let http = Arc::new(reqwest::Client::new());
2433 let _cipher = Arc::new(YouTubeCipherManager::new(YouTubeCipherConfig::default()));
2434 let client = TvSimplyClient::new(http);
2435 let oauth = Arc::new(YouTubeOAuth::new(vec![]));
2436 let result = client.search("test", &json!({}), oauth).await.unwrap();
2437 assert!(!result.is_empty(), "Search should return tracks");
2438 }
2439 #[tokio::test]
2440 async fn test_playlist() {
2441 let http = Arc::new(reqwest::Client::new());
2442 let _cipher = Arc::new(YouTubeCipherManager::new(YouTubeCipherConfig::default()));
2443 let client = TvSimplyClient::new(http);
2444 let oauth = Arc::new(YouTubeOAuth::new(vec![]));
2445 let result = client
2446 .get_playlist("PLFsQleAWXsj_4yDeebiIADdH5FMayBiJo", &json!({}), oauth)
2447 .await
2448 .unwrap();
2449 assert!(result.is_some(), "Playlist should return tracks");
2450 assert!(
2451 !result.unwrap().0.is_empty(),
2452 "Playlist should not be empty"
2453 );
2454 }
2455 }
2456 #[cfg(test)]
2457 mod get_track_tests {
2458 use super::*;
2459 use crate::{
2460 config::sources::YouTubeCipherConfig, sources::youtube::cipher::YouTubeCipherManager,
2461 };
2462 #[tokio::test]
2463 async fn test_get_track_url() {
2464 let http = Arc::new(reqwest::Client::new());
2465 let _cipher = Arc::new(YouTubeCipherManager::new(YouTubeCipherConfig::default()));
2466 let client = TvSimplyClient::new(http);
2467 let body = client
2468 .get_player_body("3Z_x7vBqr6E", None, Arc::new(YouTubeOAuth::new(vec![])))
2469 .await;
2470 assert!(body.is_some());
2471 println!(
2472 "Body: {}",
2473 serde_json::to_string_pretty(&body.unwrap()).unwrap()
2474 );
2475 }
2476 }
2477}
2478pub mod tv_unplugged {
2479 use super::{YouTubeClient, core};
2480 use crate::{
2481 common::types::AnyResult,
2482 protocol::tracks::Track,
2483 sources::youtube::{
2484 cipher::YouTubeCipherManager, clients::common::ClientConfig, oauth::YouTubeOAuth,
2485 },
2486 };
2487 use async_trait::async_trait;
2488 use serde_json::Value;
2489 use std::sync::Arc;
2490 const CLIENT_NAME: &str = "TVHTML5_UNPLUGGED";
2491 const CLIENT_VERSION: &str = "6.13";
2492 const USER_AGENT: &str = "Mozilla/5.0 (Linux armeabi-v7a; Android 7.1.2; Fire OS 6.0) Cobalt/22.lts.3.306369-gold (unlike Gecko) v8/8.8.278.8-jit gles Starboard/13, Amazon_ATV_mediatek8695_2019/NS6294 (Amazon, AFTMM, Wireless) com.amazon.firetv.youtube/22.3.r2.v66.0";
2493 pub struct TvUnpluggedClient {
2494 http: Arc<reqwest::Client>,
2495 }
2496 impl TvUnpluggedClient {
2497 pub fn new(http: Arc<reqwest::Client>) -> Self {
2498 Self { http }
2499 }
2500 fn config(&self) -> ClientConfig<'static> {
2501 ClientConfig {
2502 client_name: CLIENT_NAME,
2503 client_version: CLIENT_VERSION,
2504 user_agent: USER_AGENT,
2505 ..Default::default()
2506 }
2507 }
2508 async fn fetch_encrypted_host_flags(&self, video_id: &str) -> Option<String> {
2509 let url = format!("https://www.youtube.com/embed/{}", video_id);
2510 let res = self
2511 .http
2512 .get(&url)
2513 .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
2514 .send()
2515 .await
2516 .ok()?;
2517 let html = res.text().await.ok()?;
2518 let re = regex::Regex::new(r#""encryptedHostFlags":"([^"]+)""#).ok()?;
2519 re.captures(&html).map(|caps| caps[1].to_string())
2520 }
2521 }
2522 #[async_trait]
2523 impl YouTubeClient for TvUnpluggedClient {
2524 fn name(&self) -> &str {
2525 "TvUnplugged"
2526 }
2527 fn client_name(&self) -> &str {
2528 CLIENT_NAME
2529 }
2530 fn client_version(&self) -> &str {
2531 CLIENT_VERSION
2532 }
2533 fn user_agent(&self) -> &str {
2534 USER_AGENT
2535 }
2536 fn can_handle_request(&self, identifier: &str) -> bool {
2537 if identifier.contains("list=") && !identifier.contains("list=RD") {
2538 return false;
2539 }
2540 true
2541 }
2542 async fn search(
2543 &self,
2544 _query: &str,
2545 _context: &Value,
2546 _oauth: Arc<YouTubeOAuth>,
2547 ) -> AnyResult<Vec<Track>> {
2548 Err("TvUnplugged client does not support search".into())
2549 }
2550 async fn get_track_info(
2551 &self,
2552 track_id: &str,
2553 context: &Value,
2554 oauth: Arc<YouTubeOAuth>,
2555 ) -> AnyResult<Option<Track>> {
2556 let encrypted_host_flags = self.fetch_encrypted_host_flags(track_id).await;
2557 core::standard_get_track_info(
2558 self,
2559 core::StandardPlayerOptions {
2560 http: &self.http,
2561 track_id,
2562 context,
2563 oauth,
2564 signature_timestamp: None,
2565 encrypted_host_flags,
2566 config_builder: || {
2567 let mut cfg = self.config();
2568 cfg.client_screen = Some("EMBED");
2569 cfg
2570 },
2571 },
2572 )
2573 .await
2574 }
2575 async fn get_playlist(
2576 &self,
2577 _playlist_id: &str,
2578 _context: &Value,
2579 _oauth: Arc<YouTubeOAuth>,
2580 ) -> AnyResult<Option<(Vec<Track>, String)>> {
2581 Ok(None)
2582 }
2583 async fn resolve_url(
2584 &self,
2585 _url: &str,
2586 _context: &Value,
2587 _oauth: Arc<YouTubeOAuth>,
2588 ) -> AnyResult<Option<Track>> {
2589 Ok(None)
2590 }
2591 async fn get_track_url(
2592 &self,
2593 track_id: &str,
2594 context: &Value,
2595 cipher_manager: Arc<YouTubeCipherManager>,
2596 oauth: Arc<YouTubeOAuth>,
2597 ) -> AnyResult<Option<String>> {
2598 let signature_timestamp = cipher_manager.get_signature_timestamp().await.ok();
2599 let encrypted_host_flags = self.fetch_encrypted_host_flags(track_id).await;
2600 core::standard_get_track_url(
2601 self,
2602 core::StandardUrlOptions {
2603 http: &self.http,
2604 track_id,
2605 context,
2606 cipher_manager,
2607 oauth,
2608 signature_timestamp,
2609 encrypted_host_flags,
2610 config_builder: || {
2611 let mut cfg = self.config();
2612 cfg.client_screen = Some("EMBED");
2613 cfg
2614 },
2615 },
2616 )
2617 .await
2618 }
2619 async fn get_player_body(
2620 &self,
2621 track_id: &str,
2622 visitor_data: Option<&str>,
2623 _oauth: Arc<YouTubeOAuth>,
2624 ) -> Option<serde_json::Value> {
2625 let encrypted_host_flags = self.fetch_encrypted_host_flags(track_id).await;
2626 crate::sources::youtube::clients::common::make_player_request(
2627 crate::sources::youtube::clients::common::PlayerRequestOptions {
2628 http: &self.http,
2629 config: &{
2630 let mut cfg = self.config();
2631 cfg.client_screen = Some("EMBED");
2632 cfg
2633 },
2634 video_id: track_id,
2635 params: Some("2AMB"),
2636 visitor_data,
2637 signature_timestamp: None,
2638 auth_header: None,
2639 referer: None,
2640 origin: Some("https://www.youtube.com/"),
2641 po_token: None,
2642 encrypted_host_flags,
2643 attestation_request: None,
2644 serialized_third_party_embed_config: false,
2645 },
2646 )
2647 .await
2648 .ok()
2649 }
2650 }
2651}
2652pub mod web {
2653 use super::{YouTubeClient, core};
2654 use crate::{
2655 common::types::AnyResult,
2656 protocol::tracks::Track,
2657 sources::youtube::{
2658 cipher::YouTubeCipherManager, clients::common::ClientConfig, oauth::YouTubeOAuth,
2659 },
2660 };
2661 use async_trait::async_trait;
2662 use serde_json::Value;
2663 use std::sync::Arc;
2664 const CLIENT_NAME: &str = "WEB";
2665 const CLIENT_ID: &str = "1";
2666 const CLIENT_VERSION: &str = "2.20260114.01.00";
2667 const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
2668 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36";
2669 pub struct WebClient {
2670 http: Arc<reqwest::Client>,
2671 pub yt_cipher_url: Option<String>,
2672 pub yt_cipher_token: Option<String>,
2673 }
2674 impl WebClient {
2675 pub fn new(http: Arc<reqwest::Client>) -> Self {
2676 Self {
2677 http,
2678 yt_cipher_url: None,
2679 yt_cipher_token: None,
2680 }
2681 }
2682 pub fn with_cipher_url(
2683 http: Arc<reqwest::Client>,
2684 yt_cipher_url: Option<String>,
2685 yt_cipher_token: Option<String>,
2686 ) -> Self {
2687 let mut client = Self::new(http);
2688 client.yt_cipher_url = yt_cipher_url;
2689 client.yt_cipher_token = yt_cipher_token;
2690 client
2691 }
2692 fn config(&self) -> ClientConfig<'static> {
2693 ClientConfig {
2694 client_name: CLIENT_NAME,
2695 client_version: CLIENT_VERSION,
2696 client_id: CLIENT_ID,
2697 user_agent: USER_AGENT,
2698 platform: Some("DESKTOP"),
2699 ..Default::default()
2700 }
2701 }
2702 }
2703 #[async_trait]
2704 impl YouTubeClient for WebClient {
2705 fn name(&self) -> &str {
2706 "Web"
2707 }
2708 fn client_name(&self) -> &str {
2709 CLIENT_NAME
2710 }
2711 fn client_version(&self) -> &str {
2712 CLIENT_VERSION
2713 }
2714 fn user_agent(&self) -> &str {
2715 USER_AGENT
2716 }
2717 async fn search(
2718 &self,
2719 query: &str,
2720 context: &Value,
2721 oauth: Arc<YouTubeOAuth>,
2722 ) -> AnyResult<Vec<Track>> {
2723 core::standard_search(self, &self.http, query, context, oauth, || self.config()).await
2724 }
2725 async fn get_track_info(
2726 &self,
2727 track_id: &str,
2728 context: &Value,
2729 oauth: Arc<YouTubeOAuth>,
2730 ) -> AnyResult<Option<Track>> {
2731 core::standard_get_track_info(
2732 self,
2733 core::StandardPlayerOptions {
2734 http: &self.http,
2735 track_id,
2736 context,
2737 oauth,
2738 signature_timestamp: None,
2739 encrypted_host_flags: None,
2740 config_builder: || self.config(),
2741 },
2742 )
2743 .await
2744 }
2745 async fn get_playlist(
2746 &self,
2747 playlist_id: &str,
2748 context: &Value,
2749 oauth: Arc<YouTubeOAuth>,
2750 ) -> AnyResult<Option<(Vec<Track>, String)>> {
2751 core::standard_get_playlist(self, &self.http, playlist_id, context, oauth, || {
2752 self.config()
2753 })
2754 .await
2755 }
2756 async fn resolve_url(
2757 &self,
2758 _url: &str,
2759 _context: &Value,
2760 _oauth: Arc<YouTubeOAuth>,
2761 ) -> AnyResult<Option<Track>> {
2762 Ok(None)
2763 }
2764 async fn get_track_url(
2765 &self,
2766 track_id: &str,
2767 context: &Value,
2768 cipher_manager: Arc<YouTubeCipherManager>,
2769 oauth: Arc<YouTubeOAuth>,
2770 ) -> AnyResult<Option<String>> {
2771 let signature_timestamp = cipher_manager.get_signature_timestamp().await.ok();
2772 core::standard_get_track_url(
2773 self,
2774 core::StandardUrlOptions {
2775 http: &self.http,
2776 track_id,
2777 context,
2778 cipher_manager,
2779 oauth,
2780 signature_timestamp,
2781 encrypted_host_flags: None,
2782 config_builder: || self.config(),
2783 },
2784 )
2785 .await
2786 }
2787 async fn get_player_body(
2788 &self,
2789 track_id: &str,
2790 visitor_data: Option<&str>,
2791 _oauth: Arc<YouTubeOAuth>,
2792 ) -> Option<serde_json::Value> {
2793 crate::sources::youtube::clients::common::make_player_request(
2794 crate::sources::youtube::clients::common::PlayerRequestOptions {
2795 http: &self.http,
2796 config: &self.config(),
2797 video_id: track_id,
2798 params: None,
2799 visitor_data,
2800 signature_timestamp: None,
2801 auth_header: None,
2802 referer: None,
2803 origin: None,
2804 po_token: None,
2805 encrypted_host_flags: None,
2806 attestation_request: None,
2807 serialized_third_party_embed_config: false,
2808 },
2809 )
2810 .await
2811 .ok()
2812 }
2813 }
2814}
2815pub mod web_embedded {
2816 use super::{YouTubeClient, core};
2817 use crate::{
2818 common::types::AnyResult,
2819 protocol::tracks::Track,
2820 sources::youtube::{
2821 cipher::YouTubeCipherManager, clients::common::ClientConfig, oauth::YouTubeOAuth,
2822 },
2823 };
2824 use async_trait::async_trait;
2825 use serde_json::Value;
2826 use std::sync::Arc;
2827 const CLIENT_NAME: &str = "WEB_EMBEDDED_PLAYER";
2828 const CLIENT_ID: &str = "56";
2829 const CLIENT_VERSION: &str = "1.20260128.01.00";
2830 const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36,gzip(gfe)";
2831 pub struct WebEmbeddedClient {
2832 http: Arc<reqwest::Client>,
2833 }
2834 impl WebEmbeddedClient {
2835 pub fn new(http: Arc<reqwest::Client>) -> Self {
2836 Self { http }
2837 }
2838 fn config(&self) -> ClientConfig<'static> {
2839 ClientConfig {
2840 client_name: CLIENT_NAME,
2841 client_version: CLIENT_VERSION,
2842 client_id: CLIENT_ID,
2843 user_agent: USER_AGENT,
2844 platform: Some("DESKTOP"),
2845 third_party_embed_url: Some("https://www.google.com/"),
2846 ..Default::default()
2847 }
2848 }
2849 async fn fetch_encrypted_host_flags(&self, video_id: &str) -> Option<String> {
2850 let url = format!("https://www.youtube.com/embed/{}", video_id);
2851 let res = self
2852 .http
2853 .get(&url)
2854 .header("Referer", "https://www.google.com")
2855 .header(
2856 "User-Agent",
2857 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
2858 )
2859 .send()
2860 .await
2861 .ok()?;
2862 if !res.status().is_success() {
2863 return None;
2864 }
2865 let body = res.text().await.ok()?;
2866 let re = regex::Regex::new(r#""encryptedHostFlags":"([^"]+)""#).ok()?;
2867 re.captures(&body)
2868 .and_then(|caps| caps.get(1))
2869 .map(|m| m.as_str().to_string())
2870 }
2871 }
2872 #[async_trait]
2873 impl YouTubeClient for WebEmbeddedClient {
2874 fn name(&self) -> &str {
2875 "WebEmbedded"
2876 }
2877 fn client_name(&self) -> &str {
2878 CLIENT_NAME
2879 }
2880 fn client_version(&self) -> &str {
2881 CLIENT_VERSION
2882 }
2883 fn user_agent(&self) -> &str {
2884 USER_AGENT
2885 }
2886 fn is_embedded(&self) -> bool {
2887 true
2888 }
2889 fn can_handle_request(&self, identifier: &str) -> bool {
2890 !identifier.contains("list=")
2891 }
2892 async fn search(
2893 &self,
2894 query: &str,
2895 context: &Value,
2896 oauth: Arc<YouTubeOAuth>,
2897 ) -> AnyResult<Vec<Track>> {
2898 core::standard_search(self, &self.http, query, context, oauth, || self.config()).await
2899 }
2900 async fn get_track_info(
2901 &self,
2902 _track_id: &str,
2903 _context: &Value,
2904 _oauth: Arc<YouTubeOAuth>,
2905 ) -> AnyResult<Option<Track>> {
2906 Ok(None)
2907 }
2908 async fn get_playlist(
2909 &self,
2910 _playlist_id: &str,
2911 _context: &Value,
2912 _oauth: Arc<YouTubeOAuth>,
2913 ) -> AnyResult<Option<(Vec<Track>, String)>> {
2914 Ok(None)
2915 }
2916 async fn resolve_url(
2917 &self,
2918 _url: &str,
2919 _context: &Value,
2920 _oauth: Arc<YouTubeOAuth>,
2921 ) -> AnyResult<Option<Track>> {
2922 Ok(None)
2923 }
2924 async fn get_track_url(
2925 &self,
2926 track_id: &str,
2927 context: &Value,
2928 cipher_manager: Arc<YouTubeCipherManager>,
2929 oauth: Arc<YouTubeOAuth>,
2930 ) -> AnyResult<Option<String>> {
2931 let signature_timestamp = cipher_manager.get_signature_timestamp().await.ok();
2932 let encrypted_host_flags = self.fetch_encrypted_host_flags(track_id).await;
2933 core::standard_get_track_url(
2934 self,
2935 core::StandardUrlOptions {
2936 http: &self.http,
2937 track_id,
2938 context,
2939 cipher_manager,
2940 oauth,
2941 signature_timestamp,
2942 encrypted_host_flags,
2943 config_builder: || self.config(),
2944 },
2945 )
2946 .await
2947 }
2948 async fn get_player_body(
2949 &self,
2950 track_id: &str,
2951 visitor_data: Option<&str>,
2952 _oauth: Arc<YouTubeOAuth>,
2953 ) -> Option<serde_json::Value> {
2954 let encrypted_host_flags = self.fetch_encrypted_host_flags(track_id).await;
2955 crate::sources::youtube::clients::common::make_player_request(
2956 crate::sources::youtube::clients::common::PlayerRequestOptions {
2957 http: &self.http,
2958 config: &self.config(),
2959 video_id: track_id,
2960 params: None,
2961 visitor_data,
2962 signature_timestamp: None,
2963 auth_header: None,
2964 referer: Some("https://www.youtube.com"),
2965 origin: None,
2966 po_token: None,
2967 encrypted_host_flags,
2968 attestation_request: None,
2969 serialized_third_party_embed_config: true,
2970 },
2971 )
2972 .await
2973 .ok()
2974 }
2975 }
2976}
2977pub mod web_parent_tools {
2978 use super::{YouTubeClient, core};
2979 use crate::{
2980 common::types::AnyResult,
2981 protocol::tracks::Track,
2982 sources::youtube::{
2983 cipher::YouTubeCipherManager, clients::common::ClientConfig, oauth::YouTubeOAuth,
2984 },
2985 };
2986 use async_trait::async_trait;
2987 use serde_json::Value;
2988 use std::sync::Arc;
2989 const CLIENT_NAME: &str = "WEB_PARENT_TOOLS";
2990 const CLIENT_ID: &str = "88";
2991 const CLIENT_VERSION: &str = "1.20220918";
2992 const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36,gzip(gfe)";
2993 pub struct WebParentToolsClient {
2994 http: Arc<reqwest::Client>,
2995 }
2996 impl WebParentToolsClient {
2997 pub fn new(http: Arc<reqwest::Client>) -> Self {
2998 Self { http }
2999 }
3000 fn config(&self) -> ClientConfig<'static> {
3001 ClientConfig {
3002 client_name: CLIENT_NAME,
3003 client_version: CLIENT_VERSION,
3004 client_id: CLIENT_ID,
3005 user_agent: USER_AGENT,
3006 third_party_embed_url: Some("https://www.youtube.com/"),
3007 ..Default::default()
3008 }
3009 }
3010 }
3011 #[async_trait]
3012 impl YouTubeClient for WebParentToolsClient {
3013 fn name(&self) -> &str {
3014 "WebParentTools"
3015 }
3016 fn client_name(&self) -> &str {
3017 CLIENT_NAME
3018 }
3019 fn client_version(&self) -> &str {
3020 CLIENT_VERSION
3021 }
3022 fn user_agent(&self) -> &str {
3023 USER_AGENT
3024 }
3025 fn supports_oauth(&self) -> bool {
3026 true
3027 }
3028 async fn search(
3029 &self,
3030 query: &str,
3031 context: &Value,
3032 oauth: Arc<YouTubeOAuth>,
3033 ) -> AnyResult<Vec<Track>> {
3034 core::standard_search(self, &self.http, query, context, oauth, || self.config()).await
3035 }
3036 async fn get_track_info(
3037 &self,
3038 track_id: &str,
3039 context: &Value,
3040 oauth: Arc<YouTubeOAuth>,
3041 ) -> AnyResult<Option<Track>> {
3042 core::standard_get_track_info(
3043 self,
3044 core::StandardPlayerOptions {
3045 http: &self.http,
3046 track_id,
3047 context,
3048 oauth,
3049 signature_timestamp: None,
3050 encrypted_host_flags: None,
3051 config_builder: || self.config(),
3052 },
3053 )
3054 .await
3055 }
3056 async fn get_playlist(
3057 &self,
3058 playlist_id: &str,
3059 context: &Value,
3060 oauth: Arc<YouTubeOAuth>,
3061 ) -> AnyResult<Option<(Vec<Track>, String)>> {
3062 core::standard_get_playlist(self, &self.http, playlist_id, context, oauth, || {
3063 self.config()
3064 })
3065 .await
3066 }
3067 async fn resolve_url(
3068 &self,
3069 _url: &str,
3070 _context: &Value,
3071 _oauth: Arc<YouTubeOAuth>,
3072 ) -> AnyResult<Option<Track>> {
3073 Ok(None)
3074 }
3075 async fn get_track_url(
3076 &self,
3077 track_id: &str,
3078 context: &Value,
3079 cipher_manager: Arc<YouTubeCipherManager>,
3080 oauth: Arc<YouTubeOAuth>,
3081 ) -> AnyResult<Option<String>> {
3082 let signature_timestamp = cipher_manager.get_signature_timestamp().await.ok();
3083 core::standard_get_track_url(
3084 self,
3085 core::StandardUrlOptions {
3086 http: &self.http,
3087 track_id,
3088 context,
3089 cipher_manager,
3090 oauth,
3091 signature_timestamp,
3092 encrypted_host_flags: None,
3093 config_builder: || self.config(),
3094 },
3095 )
3096 .await
3097 }
3098 async fn get_player_body(
3099 &self,
3100 track_id: &str,
3101 visitor_data: Option<&str>,
3102 oauth: Arc<YouTubeOAuth>,
3103 ) -> Option<serde_json::Value> {
3104 crate::sources::youtube::clients::common::make_player_request(
3105 crate::sources::youtube::clients::common::PlayerRequestOptions {
3106 http: &self.http,
3107 config: &self.config(),
3108 video_id: track_id,
3109 params: Some("2AMB"),
3110 visitor_data,
3111 signature_timestamp: None,
3112 auth_header: oauth.get_auth_header().await,
3113 referer: Some("https://www.youtube.com/"),
3114 origin: None,
3115 po_token: None,
3116 encrypted_host_flags: None,
3117 attestation_request: None,
3118 serialized_third_party_embed_config: false,
3119 },
3120 )
3121 .await
3122 .ok()
3123 }
3124 }
3125}
3126pub mod web_remix {
3127 use super::{YouTubeClient, core};
3128 use crate::{
3129 common::types::AnyResult,
3130 protocol::tracks::{Track, TrackInfo},
3131 sources::youtube::{
3132 cipher::YouTubeCipherManager,
3133 clients::common::{ClientConfig, extract_thumbnail, is_duration, parse_duration},
3134 oauth::YouTubeOAuth,
3135 },
3136 };
3137 use async_trait::async_trait;
3138 use serde_json::{Value, json};
3139 use std::sync::Arc;
3140 const CLIENT_NAME: &str = "WEB_REMIX";
3141 const CLIENT_VERSION: &str = "1.20260121.03.00";
3142 const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
3143 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36";
3144 const MUSIC_API: &str = "https://music.youtube.com";
3145 pub struct WebRemixClient {
3146 http: Arc<reqwest::Client>,
3147 }
3148 impl WebRemixClient {
3149 pub fn new(http: Arc<reqwest::Client>) -> Self {
3150 Self { http }
3151 }
3152 fn config(&self) -> ClientConfig<'static> {
3153 ClientConfig {
3154 client_name: CLIENT_NAME,
3155 client_version: CLIENT_VERSION,
3156 client_id: "26",
3157 user_agent: USER_AGENT,
3158 ..Default::default()
3159 }
3160 }
3161 }
3162 #[async_trait]
3163 impl YouTubeClient for WebRemixClient {
3164 fn name(&self) -> &str {
3165 "MusicWeb"
3166 }
3167 fn client_name(&self) -> &str {
3168 CLIENT_NAME
3169 }
3170 fn client_version(&self) -> &str {
3171 CLIENT_VERSION
3172 }
3173 fn user_agent(&self) -> &str {
3174 USER_AGENT
3175 }
3176 async fn search(
3177 &self,
3178 query: &str,
3179 context: &Value,
3180 _oauth: Arc<YouTubeOAuth>,
3181 ) -> AnyResult<Vec<Track>> {
3182 let visitor_data = core::extract_visitor_data(context);
3183 let body = json!({
3184 "context": self.config().build_context(visitor_data),
3185 "query": query,
3186 "params": "EgWKAQIIAWoQEAMQBBAFEBAQCRAKEBUQEQ%3D%3D"
3187 });
3188 let url = format!("{}/youtubei/v1/search?prettyPrint=false", MUSIC_API);
3189 let mut req = self
3190 .http
3191 .post(&url)
3192 .header("User-Agent", USER_AGENT)
3193 .header("X-Goog-Api-Format-Version", "2")
3194 .header("Origin", MUSIC_API);
3195 if let Some(vd) = visitor_data {
3196 req = req.header("X-Goog-Visitor-Id", vd);
3197 }
3198 let req = req.json(&body);
3199 let res = req.send().await?;
3200 if !res.status().is_success() {
3201 return Err(format!("Music search failed: {}", res.status()).into());
3202 }
3203 let response: Value = res.json().await?;
3204 let mut tracks = Vec::new();
3205 let tab_content = response
3206 .get("contents")
3207 .and_then(|c| c.get("tabbedSearchResultsRenderer"))
3208 .and_then(|t| t.get("tabs"))
3209 .and_then(|t| t.get(0))
3210 .and_then(|t| t.get("tabRenderer"))
3211 .and_then(|t| t.get("content"));
3212 let mut shelf_contents = None;
3213 fn find_shelf(content: &Value) -> Option<&Vec<Value>> {
3214 if let Some(section_list) = content.get("sectionListRenderer")
3215 && let Some(sections) = section_list.get("contents").and_then(|c| c.as_array())
3216 {
3217 for section in sections {
3218 if let Some(shelf) = section.get("musicShelfRenderer")
3219 && let Some(items) = shelf.get("contents").and_then(|c| c.as_array())
3220 {
3221 return Some(items);
3222 }
3223 }
3224 }
3225 None
3226 }
3227 if let Some(tab) = tab_content {
3228 shelf_contents = find_shelf(tab);
3229 if shelf_contents.is_none()
3230 && let Some(split_view) = tab.get("musicSplitViewRenderer")
3231 && let Some(main_content) = split_view.get("mainContent")
3232 {
3233 shelf_contents = find_shelf(main_content);
3234 }
3235 }
3236 if let Some(items) = shelf_contents {
3237 for item in items {
3238 let renderer = item
3239 .get("musicResponsiveListItemRenderer")
3240 .or_else(|| item.get("musicTwoColumnItemRenderer"));
3241 if let Some(renderer) = renderer {
3242 let id = renderer
3243 .get("playlistItemData")
3244 .and_then(|d| d.get("videoId"))
3245 .and_then(|v| v.as_str())
3246 .or_else(|| {
3247 renderer
3248 .get("doubleTapCommand")
3249 .and_then(|c| c.get("watchEndpoint"))
3250 .and_then(|w| w.get("videoId"))
3251 .and_then(|v| v.as_str())
3252 })
3253 .or_else(|| renderer.get("videoId").and_then(|v| v.as_str()));
3254 if let Some(id) = id {
3255 let title = renderer
3256 .get("flexColumns")
3257 .and_then(|c| c.get(0))
3258 .and_then(|c| c.get("musicResponsiveListItemFlexColumnRenderer"))
3259 .and_then(|r| r.get("text"))
3260 .and_then(|t| t.get("runs"))
3261 .and_then(|r| r.get(0))
3262 .and_then(|r| r.get("text"))
3263 .and_then(|t| t.as_str())
3264 .unwrap_or("Unknown Title");
3265 let mut author = "Unknown Artist".to_string();
3266 let mut length_ms = 0u64;
3267 if let Some(flex_cols) =
3268 renderer.get("flexColumns").and_then(|c| c.as_array())
3269 {
3270 if flex_cols.len() > 1
3271 && let Some(a) = flex_cols[1]
3272 .get("musicResponsiveListItemFlexColumnRenderer")
3273 .and_then(|r| r.get("text"))
3274 .and_then(|t| t.get("runs"))
3275 .and_then(|r| r.get(0))
3276 .and_then(|r| r.get("text"))
3277 .and_then(|t| t.as_str())
3278 {
3279 author = a.to_string();
3280 }
3281 for col in flex_cols {
3282 if let Some(runs) = col
3283 .get("musicResponsiveListItemFlexColumnRenderer")
3284 .and_then(|r| r.get("text"))
3285 .and_then(|t| t.get("runs"))
3286 .and_then(|r| r.as_array())
3287 {
3288 for run in runs {
3289 if let Some(text) =
3290 run.get("text").and_then(|t| t.as_str())
3291 && is_duration(text)
3292 {
3293 length_ms = parse_duration(text);
3294 break;
3295 }
3296 }
3297 }
3298 if length_ms > 0 {
3299 break;
3300 }
3301 }
3302 }
3303 if author == "Unknown Artist"
3304 && let Some(subtitle_runs) = renderer
3305 .get("subtitle")
3306 .and_then(|s| s.get("runs"))
3307 .and_then(|r| r.as_array())
3308 && !subtitle_runs.is_empty()
3309 && let Some(a) =
3310 subtitle_runs[0].get("text").and_then(|t| t.as_str())
3311 {
3312 author = a.to_string();
3313 }
3314 let artwork_url = extract_thumbnail(renderer, Some(id));
3315 let info = TrackInfo {
3316 identifier: id.to_string(),
3317 is_seekable: true,
3318 title: title.to_string(),
3319 author,
3320 length: length_ms,
3321 is_stream: false,
3322 uri: Some(format!("https://music.youtube.com/watch?v={}", id)),
3323 source_name: "youtube".to_string(),
3324 isrc: None,
3325 artwork_url,
3326 position: 0,
3327 };
3328 tracks.push(Track::new(info));
3329 }
3330 }
3331 }
3332 }
3333 Ok(tracks)
3334 }
3335 async fn get_track_info(
3336 &self,
3337 track_id: &str,
3338 context: &Value,
3339 oauth: Arc<YouTubeOAuth>,
3340 ) -> AnyResult<Option<Track>> {
3341 core::standard_get_track_info(
3342 self,
3343 core::StandardPlayerOptions {
3344 http: &self.http,
3345 track_id,
3346 context,
3347 oauth,
3348 signature_timestamp: None,
3349 encrypted_host_flags: None,
3350 config_builder: || self.config(),
3351 },
3352 )
3353 .await
3354 }
3355 async fn get_playlist(
3356 &self,
3357 playlist_id: &str,
3358 context: &Value,
3359 oauth: Arc<YouTubeOAuth>,
3360 ) -> AnyResult<Option<(Vec<Track>, String)>> {
3361 let visitor_data = core::extract_visitor_data(context);
3362 let is_mix_playlist = playlist_id.starts_with("RDCLAK5uy_")
3363 || playlist_id.starts_with("RDAMVM")
3364 || playlist_id.starts_with("RDMM")
3365 || playlist_id.starts_with("RD");
3366 if is_mix_playlist {
3367 let next_body = json!({
3368 "context": self.config().build_context(visitor_data),
3369 "playlistId": playlist_id,
3370 "enablePersistentPlaylistPanel": true,
3371 "isAudioOnly": true
3372 });
3373 let next_url = format!("{}/youtubei/v1/next?prettyPrint=false", MUSIC_API);
3374 let mut next_req = self
3375 .http
3376 .post(&next_url)
3377 .header("User-Agent", USER_AGENT)
3378 .header("X-YouTube-Client-Name", "26")
3379 .header("X-YouTube-Client-Version", CLIENT_VERSION);
3380 if let Some(vd) = visitor_data {
3381 next_req = next_req.header("X-Goog-Visitor-Id", vd);
3382 }
3383 if let Ok(res) = next_req.json(&next_body).send().await
3384 && res.status().is_success()
3385 {
3386 let body: Value = res.json().await?;
3387 if let Some(result) =
3388 crate::sources::youtube::extractor::extract_from_next(&body, "youtube")
3389 {
3390 return Ok(Some(result));
3391 }
3392 tracing::debug!(
3393 "WebRemix: /next endpoint returned but extraction failed for playlist {}",
3394 playlist_id
3395 );
3396 }
3397 }
3398 core::standard_get_playlist(self, &self.http, playlist_id, context, oauth, || {
3399 self.config()
3400 })
3401 .await
3402 }
3403 async fn resolve_url(
3404 &self,
3405 _url: &str,
3406 _context: &Value,
3407 _oauth: Arc<YouTubeOAuth>,
3408 ) -> AnyResult<Option<Track>> {
3409 Ok(None)
3410 }
3411 async fn get_track_url(
3412 &self,
3413 track_id: &str,
3414 context: &Value,
3415 cipher_manager: Arc<YouTubeCipherManager>,
3416 oauth: Arc<YouTubeOAuth>,
3417 ) -> AnyResult<Option<String>> {
3418 let signature_timestamp = cipher_manager.get_signature_timestamp().await.ok();
3419 core::standard_get_track_url(
3420 self,
3421 core::StandardUrlOptions {
3422 http: &self.http,
3423 track_id,
3424 context,
3425 cipher_manager,
3426 oauth,
3427 signature_timestamp,
3428 encrypted_host_flags: None,
3429 config_builder: || self.config(),
3430 },
3431 )
3432 .await
3433 }
3434 async fn get_player_body(
3435 &self,
3436 track_id: &str,
3437 visitor_data: Option<&str>,
3438 _oauth: Arc<YouTubeOAuth>,
3439 ) -> Option<serde_json::Value> {
3440 crate::sources::youtube::clients::common::make_player_request(
3441 crate::sources::youtube::clients::common::PlayerRequestOptions {
3442 http: &self.http,
3443 config: &self.config(),
3444 video_id: track_id,
3445 params: None,
3446 visitor_data,
3447 signature_timestamp: None,
3448 auth_header: None,
3449 referer: None,
3450 origin: Some(MUSIC_API),
3451 po_token: None,
3452 encrypted_host_flags: None,
3453 attestation_request: None,
3454 serialized_third_party_embed_config: false,
3455 },
3456 )
3457 .await
3458 .ok()
3459 }
3460 }
3461}
3462use crate::{
3463 common::types::AnyResult,
3464 protocol::tracks::Track,
3465 sources::youtube::{cipher::YouTubeCipherManager, oauth::YouTubeOAuth},
3466};
3467use async_trait::async_trait;
3468use serde_json::Value;
3469use std::sync::Arc;
3470#[async_trait]
3471pub trait YouTubeClient: Send + Sync {
3472 fn name(&self) -> &str;
3473 fn client_name(&self) -> &str;
3474 fn client_version(&self) -> &str;
3475 fn user_agent(&self) -> &str;
3476 fn supports_oauth(&self) -> bool {
3477 false
3478 }
3479 fn is_embedded(&self) -> bool {
3480 false
3481 }
3482 fn requires_embed_workaround(&self) -> bool {
3483 !self.supports_oauth()
3484 }
3485 fn can_handle_request(&self, _identifier: &str) -> bool {
3486 true
3487 }
3488 async fn search(
3489 &self,
3490 query: &str,
3491 context: &Value,
3492 oauth: Arc<YouTubeOAuth>,
3493 ) -> AnyResult<Vec<Track>>;
3494 async fn get_track_info(
3495 &self,
3496 track_id: &str,
3497 context: &Value,
3498 oauth: Arc<YouTubeOAuth>,
3499 ) -> AnyResult<Option<Track>>;
3500 async fn resolve_url(
3501 &self,
3502 url: &str,
3503 context: &Value,
3504 oauth: Arc<YouTubeOAuth>,
3505 ) -> AnyResult<Option<Track>>;
3506 async fn get_track_url(
3507 &self,
3508 track_id: &str,
3509 context: &Value,
3510 cipher_manager: Arc<YouTubeCipherManager>,
3511 oauth: Arc<YouTubeOAuth>,
3512 ) -> AnyResult<Option<String>>;
3513 async fn get_playlist(
3514 &self,
3515 playlist_id: &str,
3516 context: &Value,
3517 oauth: Arc<YouTubeOAuth>,
3518 ) -> AnyResult<Option<(Vec<Track>, String)>>;
3519 async fn get_player_body(
3520 &self,
3521 _track_id: &str,
3522 _visitor_data: Option<&str>,
3523 _oauth: Arc<YouTubeOAuth>,
3524 ) -> Option<serde_json::Value> {
3525 None
3526 }
3527}