Skip to main content

lavende_core/sources/youtube/
clients.rs

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}