pub mod utils {
    use crate::{
        common::types::AudioFormat,
        sources::{
            http::reader::HttpReader,
            youtube::{cipher::YouTubeCipherManager, hls::HlsReader, reader::YoutubeReader},
        },
    };
    use std::sync::Arc;
    use symphonia::core::io::MediaSource;
    pub const DEFAULT_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";
    pub fn detect_audio_kind(url: &str, is_hls: bool) -> AudioFormat {
        if is_hls {
            AudioFormat::Aac
        } else {
            AudioFormat::from_url(url)
        }
    }
    pub async fn create_reader(
        url: &str,
        client_name: &str,
        local_addr: Option<std::net::IpAddr>,
        proxy: Option<crate::config::HttpProxyConfig>,
        cipher_manager: Arc<YouTubeCipherManager>,
    ) -> AnyResult<Box<dyn MediaSource>> {
        if url.contains(".m3u8") || url.contains("/playlist") {
            Ok(Box::new(
                HlsReader::new(url, local_addr, Some(cipher_manager), None, proxy).await?,
            ))
        } else if client_name == "TV" {
            Ok(Box::new(YoutubeReader::new(url, local_addr, proxy).await?))
        } else {
            Ok(Box::new(HttpReader::new(url, local_addr, proxy).await?))
        }
    }
    type AnyResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>;
    pub fn parse_playability_status(body: &serde_json::Value) -> Result<(), String> {
        let playability = body
            .get("playabilityStatus")
            .and_then(|p| p.get("status"))
            .and_then(|s| s.as_str())
            .unwrap_or("UNKNOWN");
        if playability == "OK" {
            return Ok(());
        }
        let p = body.get("playabilityStatus");
        let reason = p
            .and_then(|p| p.get("reason"))
            .and_then(|r| r.as_str())
            .unwrap_or("unknown reason");
        match playability {
            "ERROR" => Err(reason.to_string()),
            "UNPLAYABLE" => {
                if reason == "unknown reason" {
                    Err("This video is unplayable.".to_string())
                } else {
                    Err(reason.to_string())
                }
            }
            "LOGIN_REQUIRED" => {
                if reason.contains("This video is private") {
                    Err("This is a private video.".to_string())
                } else if reason.contains("This video may be inappropriate for some users") {
                    Err("This video requires age verification.".to_string())
                } else {
                    Err("This video requires login.".to_string())
                }
            }
            "CONTENT_CHECK_REQUIRED" => Err(reason.to_string()),
            "LIVE_STREAM_OFFLINE" => {
                if let Some(err_screen) = p.and_then(|p| p.get("errorScreen"))
                    && err_screen.get("ypcTrailerRenderer").is_some()
                {
                    return Err("This trailer cannot be loaded.".to_string());
                }
                Err(reason.to_string())
            }
            _ => Err("This video cannot be viewed anonymously.".to_string()),
        }
    }
}
pub mod ua {
    pub mod yt_ua {
        pub const IOS: &str =
            "com.google.ios.youtube/21.02.1 (iPhone16,2; U; CPU iOS 18_2 like Mac OS X;)";
        pub const ANDROID: &str =
            "com.google.android.youtube/20.01.35 (Linux; U; Android 14) identity";
        pub const ANDROID_VR: &str = "Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro Build/UQ1A.240205.002; wv) \
         AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 \
         Chrome/121.0.6167.164 Mobile Safari/537.36 YouTubeVR/1.42.15 (gzip)";
        pub const TVHTML5: &str = "Mozilla/5.0 (Fuchsia) AppleWebKit/537.36 (KHTML, like Gecko) \
         Chrome/140.0.0.0 Safari/537.36 CrKey/1.56.500000";
        pub const MWEB: &str = "Mozilla/5.0 (iPhone; CPU iPhone OS 18_2 like Mac OS X) \
         AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1";
        pub const WEB_EMBEDDED: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \
         (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36";
        pub const TVHTML5_SIMPLY: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \
         (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36";
        pub const TVHTML5_UNPLUGGED: &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";
    }
    pub fn get_youtube_ua(url: &str) -> Option<&'static str> {
        if !(url.contains("googlevideo.com") || url.contains("youtube.com")) {
            return None;
        }
        extract_param(url, "c=").and_then(|client| match client {
            "IOS" => Some(yt_ua::IOS),
            "ANDROID" => Some(yt_ua::ANDROID),
            "ANDROID_VR" => Some(yt_ua::ANDROID_VR),
            "TVHTML5" => Some(yt_ua::TVHTML5),
            "MWEB" => Some(yt_ua::MWEB),
            "WEB_EMBEDDED_PLAYER" => Some(yt_ua::WEB_EMBEDDED),
            "TVHTML5_SIMPLY" => Some(yt_ua::TVHTML5_SIMPLY),
            "TVHTML5_UNPLUGGED" => Some(yt_ua::TVHTML5_UNPLUGGED),
            _ => None,
        })
    }
    fn extract_param<'a>(url: &'a str, key: &str) -> Option<&'a str> {
        let query_start = url.find('?')?;
        let query = &url[query_start + 1..];
        for part in query.split('&') {
            if let Some(val) = part.strip_prefix(key) {
                return Some(val.split('#').next().unwrap_or(val));
            }
        }
        None
    }
}
pub mod oauth {
    use crate::common::types::AnyResult;
    use serde_json::{Value, json};
    use std::time::{SystemTime, UNIX_EPOCH};
    use tokio::sync::RwLock;
    use uuid::Uuid;
    const CLIENT_ID: &str =
        "861556708454-d6dlm3lh05idd8npek18k6be8ba3oc68.apps.googleusercontent.com";
    const CLIENT_SECRET: &str = "SboVhoG9s0rNafixCSGGKXAT";
    const SCOPES: &str = "http://gdata.youtube.com https://www.googleapis.com/auth/youtube";
    pub struct YouTubeOAuth {
        refresh_tokens: RwLock<Vec<String>>,
        current_token_index: RwLock<usize>,
        access_token: RwLock<Option<String>>,
        token_expiry: RwLock<u64>,
        client: reqwest::Client,
    }
    impl YouTubeOAuth {
        pub fn new(refresh_tokens: Vec<String>) -> Self {
            Self {
                refresh_tokens: RwLock::new(refresh_tokens),
                current_token_index: RwLock::new(0),
                access_token: RwLock::new(None),
                token_expiry: RwLock::new(0),
                client: reqwest::Client::new(),
            }
        }
        pub async fn initialize_access_token(self: std::sync::Arc<Self>) {
            if !self.refresh_tokens.read().await.is_empty() {
                return;
            }
            match self.fetch_device_code().await {
                Ok(response) => {
                    let verification_url =
                        response["verification_url"].as_str().unwrap_or_default();
                    let user_code = response["user_code"].as_str().unwrap_or_default();
                    let device_code = response["device_code"]
                        .as_str()
                        .unwrap_or_default()
                        .to_string();
                    let interval = response["interval"].as_u64().unwrap_or(5);
                    let inner_width = 60;
                    let top_border = format!("  ┌{}┐", "─".repeat(inner_width));
                    let sep_border = format!("  ├{}┤", "─".repeat(inner_width));
                    let bot_border = format!("  └{}┘", "─".repeat(inner_width));
                    let warning = "!!! USE A BURNER ACCOUNT FOR YOUTUBE OAUTH !!!";
                    let warning_pad = inner_width.saturating_sub(warning.len());
                    let warning_left = warning_pad / 2;
                    let warning_right = warning_pad - warning_left;
                    crate::log_println!("\n\x1b[1;33m{}\x1b[0m", top_border);
                    crate::log_println!(
                        "\x1b[1;33m  │\x1b[0m{:left$}\x1b[1;31m{}\x1b[0m{:right$}\x1b[1;33m│\x1b[0m",
                        "",
                        warning,
                        "",
                        left = warning_left,
                        right = warning_right
                    );
                    crate::log_println!("\x1b[1;33m{}\x1b[0m", sep_border);
                    let s1_prefix = " 1. Visit: ";
                    let s1_padding =
                        inner_width.saturating_sub(s1_prefix.len() + verification_url.len());
                    crate::log_println!(
                        "\x1b[1;33m  │\x1b[0m\x1b[1;36m{}\x1b[0m\x1b[4;34m{}\x1b[0m{:pad$}\x1b[1;33m│\x1b[0m",
                        s1_prefix,
                        verification_url,
                        "",
                        pad = s1_padding
                    );
                    let s2_prefix = " 2. Enter code: ";
                    let s2_code = format!(" {} ", user_code);
                    let s2_padding = inner_width.saturating_sub(s2_prefix.len() + s2_code.len());
                    crate::log_println!(
                        "\x1b[1;33m  │\x1b[0m\x1b[1;36m{}\x1b[0m\x1b[1;42;30m{}\x1b[0m{:pad$}\x1b[1;33m│\x1b[0m",
                        s2_prefix,
                        s2_code,
                        "",
                        pad = s2_padding
                    );
                    crate::log_println!("\x1b[1;33m{}\x1b[0m\n", bot_border);
                    let oauth = self.clone();
                    tokio::spawn(async move {
                        oauth.poll_for_token(device_code, interval).await;
                    });
                }
                Err(e) => {
                    tracing::error!("Failed to fetch YouTube device code: {}", e);
                }
            }
        }
        async fn fetch_device_code(&self) -> AnyResult<Value> {
            let res = self
                .client
                .post("https://www.youtube.com/o/oauth2/device/code")
                .json(&json!({
                    "client_id": CLIENT_ID,
                    "scope": SCOPES,
                    "device_id": Uuid::new_v4().to_string().replace("-", ""),
                    "device_model": "ytlr::"
                }))
                .send()
                .await?;
            Ok(res.json().await?)
        }
        async fn poll_for_token(&self, device_code: String, interval: u64) {
            let mut interval_timer =
                tokio::time::interval(std::time::Duration::from_secs(interval));
            loop {
                interval_timer.tick().await;
                match self
                    .fetch_refresh_token_from_device_code(&device_code)
                    .await
                {
                    Ok(response) => {
                        if let Some(error) = response["error"].as_str() {
                            match error {
                                "authorization_pending" => continue,
                                "slow_down" => {
                                    interval_timer = tokio::time::interval(
                                        std::time::Duration::from_secs(interval + 5),
                                    );
                                    continue;
                                }
                                "expired_token" => {
                                    tracing::error!(
                                        "OAUTH INTEGRATION: The device token has expired. OAuth integration has been canceled."
                                    );
                                    break;
                                }
                                "access_denied" => {
                                    tracing::error!(
                                        "OAUTH INTEGRATION: Account linking was denied. OAuth integration has been canceled."
                                    );
                                    break;
                                }
                                _ => {
                                    tracing::error!("Unhandled OAuth2 error: {}", error);
                                    break;
                                }
                            }
                        }
                        if let Some(refresh_token) = response["refresh_token"].as_str() {
                            let mut tokens = self.refresh_tokens.write().await;
                            tokens.push(refresh_token.to_string());
                            crate::log_println!(
                                "\x1b[1;32mOAUTH INTEGRATION: Token retrieved successfully!\x1b[0m"
                            );
                            crate::log_println!(
                                "\x1b[1;32mRefresh token:\x1b[0m {}",
                                refresh_token
                            );
                            break;
                        }
                    }
                    Err(e) => {
                        crate::log_println!(
                            "\x1b[1;31mFailed to fetch YouTube OAuth2 token:\x1b[0m {}",
                            e
                        );
                        break;
                    }
                }
            }
        }
        async fn fetch_refresh_token_from_device_code(
            &self,
            device_code: &str,
        ) -> AnyResult<Value> {
            let res = self
                .client
                .post("https://www.youtube.com/o/oauth2/token")
                .json(&json!({
                    "client_id": CLIENT_ID,
                    "client_secret": CLIENT_SECRET,
                    "code": device_code,
                    "grant_type": "http://oauth.net/grant_type/device/1.0"
                }))
                .send()
                .await?;
            Ok(res.json().await?)
        }
        pub async fn get_access_token(&self, idx: usize) -> Option<String> {
            let tokens = self.refresh_tokens.read().await;
            let max_tokens = tokens.len();
            if max_tokens == 0 {
                return None;
            }
            let now = SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap()
                .as_secs();
            {
                let expiry = self.token_expiry.read().await;
                let token = self.access_token.read().await;
                if let Some(t) = token.as_ref()
                    && now < *expiry
                {
                    return Some(t.clone());
                }
            }
            let refresh_token = &tokens[idx % max_tokens];
            if refresh_token.is_empty() {
                return None;
            }
            match self.refresh_token_request(refresh_token).await {
                Ok((new_token, expires_in)) => {
                    let mut token_store = self.access_token.write().await;
                    let mut expiry_store = self.token_expiry.write().await;
                    *token_store = Some(new_token.clone());
                    *expiry_store = now + expires_in - 30;
                    Some(new_token)
                }
                Err(e) => {
                    tracing::error!(
                        "Failed to refresh YouTube token for index {}: {}",
                        idx % max_tokens,
                        e
                    );
                    None
                }
            }
        }
        async fn refresh_token_request(&self, refresh_token: &str) -> AnyResult<(String, u64)> {
            let res = self
                .client
                .post("https://www.youtube.com/o/oauth2/token")
                .json(&json!({
                    "client_id": CLIENT_ID,
                    "client_secret": CLIENT_SECRET,
                    "refresh_token": refresh_token,
                    "grant_type": "refresh_token"
                }))
                .send()
                .await?;
            let status = res.status();
            if status == 200 {
                let body: Value = res.json().await?;
                if let Some(access_token) = body.get("access_token").and_then(|t| t.as_str()) {
                    let expires_in = body
                        .get("expires_in")
                        .and_then(|e| e.as_u64())
                        .unwrap_or(3600);
                    return Ok((access_token.to_string(), expires_in));
                }
            }
            Err(format!("OAuth refresh failed with status: {}", status).into())
        }
        pub async fn get_auth_header(&self) -> Option<String> {
            let tokens = self.refresh_tokens.read().await;
            if tokens.is_empty() {
                return None;
            }
            let num_tokens = tokens.len();
            let idx = {
                let mut current_idx = self.current_token_index.write().await;
                let val = *current_idx;
                *current_idx = (val + 1) % num_tokens;
                val
            };
            drop(tokens);
            self.get_access_token(idx)
                .await
                .map(|t| format!("Bearer {}", t))
        }
        pub async fn get_refresh_tokens(&self) -> Vec<String> {
            self.refresh_tokens.read().await.clone()
        }
        pub async fn refresh_with_token(
            &self,
            refresh_token: &str,
        ) -> AnyResult<serde_json::Value> {
            let res = self
                .client
                .post("https://www.youtube.com/o/oauth2/token")
                .json(&json!({
                    "client_id": CLIENT_ID,
                    "client_secret": CLIENT_SECRET,
                    "refresh_token": refresh_token,
                    "grant_type": "refresh_token"
                }))
                .send()
                .await?;
            let status = res.status();
            let body: serde_json::Value = res.json().await?;
            if status.is_success() {
                Ok(body)
            } else {
                Err(format!("OAuth refresh failed: status={}, body={}", status, body).into())
            }
        }
    }
}
pub mod cipher {
    use crate::{common::types::AnyResult, config::sources::YouTubeCipherConfig};
    use serde_json::{Value, json};
    use std::time::{Duration, Instant};
    use tokio::sync::RwLock;
    #[derive(Clone)]
    pub struct CachedPlayerScript {
        pub url: String,
        pub signature_timestamp: String,
        pub expire_timestamp_ms: Instant,
    }
    pub struct YouTubeCipherManager {
        config: YouTubeCipherConfig,
        client: reqwest::Client,
        cached_player_script: RwLock<Option<CachedPlayerScript>>,
    }
    impl YouTubeCipherManager {
        pub fn new(config: YouTubeCipherConfig) -> Self {
            Self {
                config,
                client: reqwest::Client::new(),
                cached_player_script: RwLock::new(None),
            }
        }
        pub async fn get_cached_player_script(&self) -> AnyResult<CachedPlayerScript> {
            {
                let cache = self.cached_player_script.read().await;
                if let Some(script) = &*cache
                    && Instant::now() < script.expire_timestamp_ms
                {
                    return Ok(script.clone());
                }
            }
            let mut cache = self.cached_player_script.write().await;
            if let Some(script) = &*cache
                && Instant::now() < script.expire_timestamp_ms
            {
                return Ok(script.clone());
            }
            let script = self.get_player_script().await?;
            *cache = Some(script.clone());
            Ok(script)
        }
        async fn get_player_script(&self) -> AnyResult<CachedPlayerScript> {
            let res = self
            .client
            .get("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
            .header(reqwest::header::USER_AGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36")
            .send()
            .await?;
            let text = res.text().await?;
            let re = regex::Regex::new(r#""jsUrl":"([^"]+)""#)?;
            let mut script_url = if let Some(caps) = re.captures(&text) {
                caps[1].to_string()
            } else {
                let res = self
                .client
                .get("https://www.youtube.com/embed/")
                .header(reqwest::header::USER_AGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36")
                .send()
                .await?;
                let text = res.text().await?;
                if let Some(caps) = re.captures(&text) {
                    caps[1].to_string()
                } else {
                    return Err("Could not find jsUrl in player script".into());
                }
            };
            let locale_re = regex::Regex::new(r"/([a-z]{2}_[A-Z]{2})/")?;
            script_url = locale_re.replace(&script_url, "/en_US/").to_string();
            let full_url = if script_url.starts_with("http") {
                script_url
            } else {
                format!("https://www.youtube.com{}", script_url)
            };
            let signature_timestamp = self.get_timestamp(&full_url).await?;
            Ok(CachedPlayerScript {
                url: full_url,
                signature_timestamp,
                expire_timestamp_ms: Instant::now() + Duration::from_secs(12 * 60 * 60),
            })
        }
        pub async fn get_timestamp(&self, source_url: &str) -> AnyResult<String> {
            if let Some(url) = &self.config.url {
                let mut headers = reqwest::header::HeaderMap::new();
                if let Some(token) = &self.config.token {
                    headers.insert(reqwest::header::AUTHORIZATION, token.parse()?);
                }
                if let Ok(res) = self
                    .client
                    .post(format!("{}/get_sts", url.trim_end_matches('/')))
                    .headers(headers)
                    .json(&json!({ "player_url": source_url }))
                    .send()
                    .await
                {
                    if res.status() == 200 {
                        if let Ok(body) = res.json::<Value>().await {
                            if let Some(sts) = body.get("sts").and_then(|v| v.as_str()) {
                                return Ok(sts.to_string());
                            }
                        }
                    }
                }
            }
            let res = self.client.get(source_url).send().await?;
            let text = res.text().await?;
            let re = regex::Regex::new(r#"(?:signatureTimestamp|sts):(\d+)"#)?;
            if let Some(caps) = re.captures(&text) {
                Ok(caps[1].to_string())
            } else {
                Err("Could not find STS in player script".into())
            }
        }
        pub async fn get_signature_timestamp(&self) -> AnyResult<u32> {
            let script = self.get_cached_player_script().await?;
            script
                .signature_timestamp
                .parse::<u32>()
                .map_err(|e| e.into())
        }
        pub async fn resolve_url(
            &self,
            stream_url: &str,
            player_url: &str,
            n_param: Option<&str>,
            sig: Option<&str>,
        ) -> AnyResult<String> {
            let url = self
                .config
                .url
                .as_ref()
                .ok_or("Remote cipher URL not configured")?;
            let player_url = if let Ok(script) = self.get_cached_player_script().await {
                script.url
            } else {
                player_url.to_string()
            };
            let mut headers = reqwest::header::HeaderMap::new();
            if let Some(token) = &self.config.token {
                headers.insert(reqwest::header::AUTHORIZATION, token.parse()?);
            }
            let mut body = json!({
                "stream_url": stream_url,
                "player_url": player_url,
            });
            if let Some(n) = n_param {
                body["n_param"] = json!(n);
            }
            if let Some(s) = sig {
                body["encrypted_signature"] = json!(s);
                body["signature_key"] = json!("sig");
            }
            let res = self
                .client
                .post(format!("{}/resolve_url", url.trim_end_matches('/')))
                .headers(headers)
                .json(&body)
                .send()
                .await?;
            let status = res.status();
            if status == 200 {
                let body: Value = res.json().await?;
                if let Some(resolved) = body.get("resolved_url").and_then(|v| v.as_str()) {
                    return Ok(resolved.to_string());
                }
                return Err("Resolved URL missing in response".into());
            }
            let err_body = res.text().await?;
            Err(format!("Failed to resolve URL with status {}: {}", status, err_body).into())
        }
    }
}
pub mod reader {
    use super::ua::get_youtube_ua;
    use crate::{
        audio::source::{SegmentedSource, create_client},
        common::types::AnyResult,
    };
    use std::io::{Read, Seek, SeekFrom};
    use symphonia::core::io::MediaSource;
    pub struct YoutubeReader {
        inner: SegmentedSource,
    }
    impl YoutubeReader {
        pub async fn new(
            url: &str,
            local_addr: Option<std::net::IpAddr>,
            proxy: Option<crate::config::HttpProxyConfig>,
        ) -> AnyResult<Self> {
            let user_agent = get_youtube_ua(url)
                .map(str::to_string)
                .unwrap_or_else(crate::common::utils::default_user_agent);
            let client = create_client(user_agent, local_addr, proxy, None)?;
            let inner = SegmentedSource::new(client, url).await?;
            Ok(Self { inner })
        }
    }
    impl Read for YoutubeReader {
        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
            self.inner.read(buf)
        }
    }
    impl Seek for YoutubeReader {
        fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
            self.inner.seek(pos)
        }
    }
    impl MediaSource for YoutubeReader {
        fn is_seekable(&self) -> bool {
            self.inner.is_seekable()
        }
        fn byte_len(&self) -> Option<u64> {
            self.inner.byte_len()
        }
    }
}
pub mod extractor {
    use crate::protocol::tracks::{Track, TrackInfo};
    use serde_json::Value;
    pub fn extract_from_player(body: &Value, source_name: &str) -> Option<Track> {
        let details = body
            .get("videoDetails")
            .or_else(|| body.get("video_details"))?;
        let video_id = details
            .get("videoId")
            .or_else(|| details.get("video_id"))?
            .as_str()?;
        let title = details.get("title")?.as_str()?.to_string();
        let author = details.get("author")?.as_str()?.to_string();
        let is_stream = details
            .get("isLiveContent")
            .or_else(|| details.get("is_live_content"))
            .and_then(|v| v.as_bool())
            .unwrap_or(false);
        let length_seconds = details
            .get("lengthSeconds")
            .or_else(|| details.get("length_seconds"))
            .and_then(|v| v.as_str())
            .and_then(|s| s.parse::<u64>().ok())
            .unwrap_or(0);
        let artwork_url = details
            .get("thumbnail")
            .and_then(|t| t.get("thumbnails"))
            .and_then(|arr| arr.as_array())
            .and_then(|arr| arr.last())
            .and_then(|thumb| thumb.get("url"))
            .and_then(|url| url.as_str())
            .map(|s| s.to_string());
        let track = Track::new(TrackInfo {
            identifier: video_id.to_string(),
            is_seekable: !is_stream,
            author,
            length: if is_stream {
                9223372036854775807
            } else {
                length_seconds * 1000
            },
            is_stream,
            position: 0,
            title,
            uri: Some(format!("https://www.youtube.com/watch?v={}", video_id)),
            artwork_url,
            isrc: None,
            source_name: source_name.to_string(),
        });
        Some(track)
    }
    pub fn extract_from_next(body: &Value, source_name: &str) -> Option<(Vec<Track>, String)> {
        let contents_root = body.get("contents").and_then(|c| {
            c.get("singleColumnWatchNextResults")
                .or_else(|| c.get("singleColumnMusicWatchNextResultsRenderer"))
                .or_else(|| c.get("twoColumnWatchNextResults"))
        })?;
        let playlist_content = contents_root
            .get("playlist")
            .and_then(|p| p.get("playlist"))
            .and_then(|p| p.get("contents"))
            .and_then(|c| c.as_array())
            .or_else(|| {
                contents_root
                    .get("tabbedRenderer")
                    .and_then(|t| t.get("watchNextTabbedResultsRenderer"))
                    .and_then(|w| w.get("tabs"))
                    .and_then(|t| t.get(0))
                    .and_then(|t| t.get("tabRenderer"))
                    .and_then(|t| t.get("content"))
                    .and_then(|c| c.get("musicQueueRenderer"))
                    .and_then(|music_queue| {
                        music_queue
                            .get("content")
                            .and_then(|c| c.get("playlistPanelRenderer"))
                            .and_then(|p| p.get("contents"))
                            .or_else(|| music_queue.get("contents"))
                            .and_then(|c| c.as_array())
                    })
            })?;
        if playlist_content.is_empty() {
            return None;
        }
        let mut tracks = Vec::new();
        for item in playlist_content {
            if let Some(track) = extract_track(item, source_name) {
                tracks.push(track);
            }
        }
        if tracks.is_empty() {
            return None;
        }
        let title = contents_root
            .get("tabbedRenderer")
            .and_then(|t| t.get("watchNextTabbedResultsRenderer"))
            .and_then(|t| t.get("tabs"))
            .and_then(|t| t.get(0))
            .and_then(|t| t.get("tabRenderer"))
            .and_then(|t| t.get("content"))
            .and_then(|c| c.get("musicQueueRenderer"))
            .and_then(|m| m.get("header"))
            .and_then(|h| h.get("musicQueueHeaderRenderer"))
            .and_then(|m| m.get("subtitle"))
            .and_then(get_text)
            .or_else(|| {
                contents_root
                    .get("playlist")
                    .and_then(|p| p.get("playlist"))
                    .and_then(|p| p.get("title"))
                    .and_then(|t| t.as_str())
                    .map(|s| s.to_string())
            })
            .unwrap_or_else(|| "Unknown Playlist".to_string());
        Some((tracks, title))
    }
    pub fn extract_from_browse(body: &Value, source_name: &str) -> Option<(Vec<Track>, String)> {
        let title = body
            .get("header")
            .and_then(|h| {
                h.get("playlistHeaderRenderer")
                    .or_else(|| h.get("musicAlbumReleaseHeaderRenderer"))
                    .or_else(|| h.get("musicDetailHeaderRenderer"))
                    .or_else(|| {
                        h.get("musicEditablePlaylistDetailHeaderRenderer")
                            .and_then(|m| m.get("header"))
                            .and_then(|h| h.get("musicDetailHeaderRenderer"))
                    })
            })
            .and_then(|h| h.get("title"))
            .and_then(get_text)
            .unwrap_or_else(|| "Unknown Playlist".to_string());
        let mut tracks = Vec::new();
        if let Some(section_list) = find_section_list(body)
            && let Some(contents) = section_list.get("contents").and_then(|c| c.as_array())
        {
            for section in contents {
                if let Some(list) = section
                    .get("itemSectionRenderer")
                    .and_then(|i| i.get("contents"))
                    .and_then(|c| c.as_array())
                    .and_then(|arr| arr.first())
                    .and_then(|first| first.get("playlistVideoListRenderer"))
                    .and_then(|p| p.get("contents"))
                    .and_then(|c| c.as_array())
                {
                    for item in list {
                        if let Some(track) = extract_track(item, source_name) {
                            tracks.push(track);
                        }
                    }
                }
                if let Some(list) = section
                    .get("musicShelfRenderer")
                    .and_then(|s| s.get("contents"))
                    .and_then(|c| c.as_array())
                {
                    for item in list {
                        if let Some(track) = extract_track(item, source_name) {
                            tracks.push(track);
                        }
                    }
                }
                if let Some(shelf) = section.get("musicPlaylistShelfRenderer")
                    && let Some(list) = shelf.get("contents").and_then(|c| c.as_array())
                {
                    for item in list {
                        if let Some(track) = extract_track(item, source_name) {
                            tracks.push(track);
                        }
                    }
                }
            }
        }
        if tracks.is_empty()
            && let Some(contents) = body
                .get("contents")
                .and_then(|c| c.get("singleColumnBrowseResultsRenderer"))
                .and_then(|s| s.get("tabs"))
                .and_then(|t| t.as_array())
                .and_then(|t| t.first())
                .and_then(|t| t.get("tabRenderer"))
                .and_then(|t| t.get("content"))
                .and_then(|c| c.get("sectionListRenderer"))
                .and_then(|s| s.get("contents"))
                .and_then(|c| c.as_array())
                .and_then(|c| c.first())
                .and_then(|c| c.get("musicPlaylistShelfRenderer"))
            && let Some(list) = contents.get("contents").and_then(|c| c.as_array())
        {
            for item in list {
                if let Some(track) = extract_track(item, source_name) {
                    tracks.push(track);
                }
            }
        }
        if tracks.is_empty()
            && let Some(list) = find_music_playlist_shelf(body)
        {
            for item in list {
                if let Some(track) = extract_track(item, source_name) {
                    tracks.push(track);
                }
            }
        }
        if tracks.is_empty()
            && let Some(continuation_contents) = body
                .get("onResponseReceivedActions")
                .and_then(|a| a.as_array())
                .and_then(|arr| arr.first())
                .and_then(|a| a.get("appendContinuationItemsAction"))
                .and_then(|a| a.get("continuationItems"))
                .and_then(|c| c.as_array())
        {
            for item in continuation_contents {
                if let Some(track) = extract_track(item, source_name) {
                    tracks.push(track);
                }
            }
        }
        if tracks.is_empty() {
            return None;
        }
        Some((tracks, title))
    }
    fn find_music_playlist_shelf(value: &Value) -> Option<&Vec<Value>> {
        if let Some(shelf) = value.get("musicPlaylistShelfRenderer") {
            return shelf.get("contents").and_then(|c| c.as_array());
        }
        if let Some(obj) = value.as_object() {
            for (_, val) in obj {
                if let Some(list) = find_music_playlist_shelf(val) {
                    return Some(list);
                }
            }
        }
        if let Some(arr) = value.as_array() {
            for item in arr {
                if let Some(list) = find_music_playlist_shelf(item) {
                    return Some(list);
                }
            }
        }
        None
    }
    pub fn find_section_list(value: &Value) -> Option<&Value> {
        if let Some(list) = value.get("sectionListRenderer") {
            return Some(list);
        }
        if let Some(contents) = value.get("contents")
            && let Some(list) = find_section_list(contents)
        {
            return Some(list);
        }
        if let Some(arr) = value.as_array() {
            for item in arr {
                if let Some(list) = find_section_list(item) {
                    return Some(list);
                }
            }
        }
        if let Some(tabs) = value.get("tabs").and_then(|t| t.as_array()) {
            for tab in tabs {
                if let Some(content) = tab.get("tabRenderer").and_then(|tr| tr.get("content"))
                    && let Some(list) = find_section_list(content)
                {
                    return Some(list);
                }
            }
        }
        if let Some(primary) = value
            .get("twoColumnSearchResultsRenderer")
            .and_then(|t| t.get("primaryContents"))
        {
            return find_section_list(primary);
        }
        None
    }
    pub fn extract_track(item: &Value, source_name: &str) -> Option<Track> {
        let renderer = item
            .get("videoRenderer")
            .or_else(|| item.get("compactVideoRenderer"))
            .or_else(|| item.get("playlistVideoRenderer"))
            .or_else(|| item.get("musicResponsiveListItemRenderer"))
            .or_else(|| item.get("musicTwoColumnItemRenderer"))
            .or_else(|| item.get("playlistPanelVideoRenderer"))
            .or_else(|| item.get("gridVideoRenderer"))?;
        let video_id = renderer
            .get("videoId")
            .and_then(|v| v.as_str())
            .or_else(|| {
                renderer
                    .get("playlistItemData")
                    .and_then(|d| d.get("videoId"))
                    .and_then(|v| v.as_str())
            })
            .or_else(|| {
                renderer
                    .get("doubleTapCommand")
                    .and_then(|c| c.get("watchEndpoint"))
                    .and_then(|w| w.get("videoId"))
                    .and_then(|v| v.as_str())
            })
            .or_else(|| {
                renderer
                    .get("navigationEndpoint")
                    .and_then(|n| n.get("watchEndpoint"))
                    .and_then(|w| w.get("videoId"))
                    .and_then(|v| v.as_str())
            })?;
        let title = get_text(renderer.get("title").or_else(|| {
            renderer
                .get("flexColumns")
                .and_then(|c| c.get(0))
                .and_then(|c| c.get("musicResponsiveListItemFlexColumnRenderer"))
                .and_then(|r| r.get("text"))
        })?)
        .unwrap_or_else(|| "Unknown Title".to_string());
        let author = extract_author(renderer).unwrap_or_else(|| "Unknown Artist".to_string());
        let is_stream = renderer
            .get("isLive")
            .and_then(|v| v.as_bool())
            .unwrap_or(false)
            || renderer
                .get("badges")
                .and_then(|b| b.as_array())
                .map(|arr| {
                    arr.iter().any(|badge| {
                        badge
                            .get("metadataBadgeRenderer")
                            .and_then(|mbr| mbr.get("label"))
                            .and_then(|l| l.as_str())
                            .map(|s| s == "LIVE")
                            .unwrap_or(false)
                    })
                })
                .unwrap_or(false);
        let length_ms = if is_stream {
            9223372036854775807
        } else {
            renderer
                .get("lengthText")
                .and_then(get_text)
                .map(|s| parse_duration(&s))
                .or_else(|| {
                    renderer
                        .get("lengthSeconds")
                        .and_then(|v| v.as_str())
                        .and_then(|s| s.parse::<i64>().ok())
                        .map(|s| s * 1000)
                })
                .unwrap_or(0)
        };
        Some(Track::new(TrackInfo {
            identifier: video_id.to_string(),
            is_seekable: !is_stream,
            author,
            length: length_ms as u64,
            is_stream,
            position: 0,
            title,
            uri: Some(format!("https://www.youtube.com/watch?v={}", video_id)),
            artwork_url: get_thumbnail(renderer),
            isrc: None,
            source_name: source_name.to_string(),
        }))
    }
    fn extract_author(renderer: &Value) -> Option<String> {
        if let Some(subtitle) = renderer.get("subtitle")
            && let Some(text) = get_first_subtitle_run(subtitle)
        {
            let artist = text.split(" • ").next().unwrap_or(&text).trim();
            if !artist.is_empty() {
                return Some(artist.to_string());
            }
        }
        if let Some(author) = renderer
            .get("menu")
            .and_then(|m| m.get("menuRenderer"))
            .and_then(|m| m.get("title"))
            .and_then(|t| t.get("musicMenuTitleRenderer"))
            .and_then(|m| m.get("secondaryText"))
            .and_then(get_first_text)
        {
            return Some(author);
        }
        if let Some(text) = renderer.get("longBylineText").and_then(get_first_text) {
            return Some(text);
        }
        if let Some(text) = renderer.get("shortBylineText").and_then(get_first_text) {
            return Some(text);
        }
        if let Some(text) = renderer.get("ownerText").and_then(get_first_text) {
            return Some(text);
        }
        if let Some(flex) = renderer
            .get("flexColumns")
            .and_then(|c| c.get(1))
            .and_then(|c| c.get("musicResponsiveListItemFlexColumnRenderer"))
            .and_then(|r| r.get("text"))
            && let Some(runs) = flex.get("runs").and_then(|r| r.as_array())
            && let Some(text) = runs
                .first()
                .and_then(|r| r.get("text"))
                .and_then(|t| t.as_str())
        {
            return Some(text.to_string());
        }
        None
    }
    fn get_first_subtitle_run(subtitle: &Value) -> Option<String> {
        if let Some(runs) = subtitle.get("runs").and_then(|r| r.as_array()) {
            return runs
                .first()
                .and_then(|r| r.get("text"))
                .and_then(|t| t.as_str())
                .map(|s| s.to_string());
        }
        if let Some(simple_text) = subtitle.get("simpleText").and_then(|v| v.as_str()) {
            return Some(simple_text.to_string());
        }
        if let Some(s) = subtitle.as_str() {
            return Some(s.to_string());
        }
        None
    }
    fn get_text(obj: &Value) -> Option<String> {
        if let Some(s) = obj.as_str() {
            return Some(s.to_string());
        }
        if let Some(simple_text) = obj.get("simpleText").and_then(|v| v.as_str()) {
            return Some(simple_text.to_string());
        }
        if let Some(runs) = obj.get("runs").and_then(|v| v.as_array()) {
            let mut text = String::new();
            for run in runs {
                if let Some(t) = run.get("text").and_then(|v| v.as_str()) {
                    text.push_str(t);
                }
            }
            return Some(text);
        }
        None
    }
    fn get_first_text(obj: &Value) -> Option<String> {
        if let Some(s) = obj.as_str() {
            return Some(s.to_string());
        }
        if let Some(simple_text) = obj.get("simpleText").and_then(|v| v.as_str()) {
            return Some(simple_text.to_string());
        }
        if let Some(runs) = obj.get("runs").and_then(|v| v.as_array()) {
            return runs
                .first()
                .and_then(|run| run.get("text"))
                .and_then(|t| t.as_str())
                .map(|s| s.to_string());
        }
        None
    }
    fn parse_duration(s: &str) -> i64 {
        let parts: Vec<&str> = s.split(':').collect();
        let mut seconds = 0;
        for part in parts {
            seconds = seconds * 60 + part.parse::<i64>().unwrap_or(0);
        }
        seconds * 1000
    }
    fn get_thumbnail(renderer: &Value) -> Option<String> {
        renderer
            .get("thumbnail")
            .and_then(|t| t.get("thumbnails"))
            .and_then(|arr| arr.as_array())
            .and_then(|arr| arr.last())
            .and_then(|thumb| thumb.get("url"))
            .and_then(|url| url.as_str())
            .map(|s| s.split('?').next().unwrap_or(s).to_string())
    }
}
pub mod track {
    use crate::{
        config::HttpProxyConfig,
        sources::{
            playable_track::{PlayableTrack, ResolvedTrack},
            youtube::{
                cipher::YouTubeCipherManager,
                clients::YouTubeClient,
                oauth::YouTubeOAuth,
                utils::{create_reader, detect_audio_kind},
            },
        },
    };
    use async_trait::async_trait;
    use std::{net::IpAddr, sync::Arc};
    use tracing::{debug, error, info, warn};
    pub struct YoutubeTrack {
        pub identifier: String,
        pub clients: Vec<Arc<dyn YouTubeClient>>,
        pub oauth: Arc<YouTubeOAuth>,
        pub cipher_manager: Arc<YouTubeCipherManager>,
        pub visitor_data: Option<String>,
        pub local_addr: Option<IpAddr>,
        pub proxy: Option<HttpProxyConfig>,
    }
    #[async_trait]
    impl PlayableTrack for YoutubeTrack {
        fn supports_seek(&self) -> bool {
            true
        }
        async fn resolve(&self) -> Result<ResolvedTrack, String> {
            let context = serde_json::json!({ "visitorData": self.visitor_data });
            let mut last_error = String::from("No clients available");
            for client in &self.clients {
                let name = client.name().to_string();
                let url = match client
                    .get_track_url(
                        &self.identifier,
                        &context,
                        self.cipher_manager.clone(),
                        self.oauth.clone(),
                    )
                    .await
                {
                    Ok(Some(url)) => {
                        info!(
                            "YoutubeTrack: resolved '{}' using '{name}'",
                            self.identifier
                        );
                        url
                    }
                    Ok(None) => {
                        debug!(
                            "YoutubeTrack: client '{name}' returned no URL for '{}'",
                            self.identifier
                        );
                        continue;
                    }
                    Err(e) => {
                        warn!(
                            "YoutubeTrack: client '{name}' failed for '{}': {e}",
                            self.identifier
                        );
                        last_error = e.to_string();
                        continue;
                    }
                };
                let is_hls = url.contains(".m3u8") || url.contains("/playlist");
                let hint = Some(detect_audio_kind(&url, is_hls));
                let proxy = self.proxy.clone();
                let local_addr = self.local_addr;
                let cipher = self.cipher_manager.clone();
                let url_clone = url.clone();
                let name_clone = name.clone();
                match create_reader(&url_clone, &name_clone, local_addr, proxy, cipher).await {
                    Ok(reader) => return Ok(ResolvedTrack::new(reader, hint)),
                    Err(e) => {
                        warn!("YoutubeTrack: reader failed for '{name}': {e} — trying next client");
                        last_error = e.to_string();
                        continue;
                    }
                }
            }
            error!(
                "YoutubeTrack: all clients failed for '{}': {last_error}",
                self.identifier
            );
            Err(format!("All clients failed: {last_error}"))
        }
    }

    #[allow(dead_code)]
    fn is_playability_error(msg: &str) -> bool {
        msg.contains("This video ")
            || msg.contains("This is a private video")
            || msg.contains("This trailer cannot be loaded")
    }
}
use crate::{
    common::types::SharedRw,
    config::sources::YouTubeConfig,
    protocol::tracks::*,
    sources::{SourcePlugin, playable_track::BoxedTrack},
};
use async_trait::async_trait;
use regex::Regex;
use serde_json::{Value, json};
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{debug, warn};
pub mod clients;
pub mod hls;
use cipher::YouTubeCipherManager;
use clients::{
    YouTubeClient, android::AndroidClient, android_vr::AndroidVrClient, ios::IosClient,
    music_android::MusicAndroidClient, mweb::MWebClient, tv::TvClient, tv_cast::TvCastClient,
    tv_embedded::TvEmbeddedClient, tv_simply::TvSimplyClient, tv_unplugged::TvUnpluggedClient,
    web::WebClient, web_embedded::WebEmbeddedClient, web_parent_tools::WebParentToolsClient,
    web_remix::WebRemixClient,
};
use oauth::YouTubeOAuth;
pub struct YouTubeSource {
    search_prefixes: Vec<String>,
    rec_prefixes: Vec<String>,
    url_regex: Regex,
    search_clients: Vec<Arc<dyn YouTubeClient>>,
    music_search_clients: Vec<Arc<dyn YouTubeClient>>,
    playback_clients: Vec<Arc<dyn YouTubeClient>>,
    resolve_clients: Vec<Arc<dyn YouTubeClient>>,
    oauth: Arc<YouTubeOAuth>,
    cipher_manager: Arc<YouTubeCipherManager>,
    visitor_data: SharedRw<Option<String>>,
    #[allow(dead_code)]
    http: Arc<reqwest::Client>,
}
pub struct YoutubeStreamContext {
    pub clients: Vec<Arc<dyn YouTubeClient>>,
    pub oauth: Arc<YouTubeOAuth>,
    pub cipher_manager: Arc<YouTubeCipherManager>,
    pub visitor_data: SharedRw<Option<String>>,
    pub http: Arc<reqwest::Client>,
}
impl YouTubeSource {
    pub fn new(config: Option<YouTubeConfig>, http: Arc<reqwest::Client>) -> Self {
        let config = config.unwrap_or_default();
        let oauth = Arc::new(YouTubeOAuth::new(config.refresh_tokens.clone()));
        let cipher_manager = Arc::new(YouTubeCipherManager::new(config.cipher.clone()));
        if config.get_oauth_token && config.refresh_tokens.is_empty() {
            let oauth_clone = oauth.clone();
            tokio::spawn(async move {
                oauth_clone.initialize_access_token().await;
            });
        }
        let cm_clone = cipher_manager.clone();
        tokio::spawn(async move {
            debug!("YouTubeSource: Warming cipher cache...");
            if let Err(e) = cm_clone.get_cached_player_script().await {
                warn!("YouTubeSource: Failed to warm cipher cache: {}", e);
            } else {
                debug!("YouTubeSource: Cipher cache warmed.");
            }
        });
        let visitor_data = Arc::new(RwLock::new(None));
        let vd_clone = visitor_data.clone();
        let http_clone = http.clone();
        tokio::spawn(async move {
            loop {
                if let Some(vd) = Self::refresh_visitor_data(&http_clone).await {
                    let mut lock = vd_clone.write().await;
                    *lock = Some(vd);
                    tracing::debug!("YouTube visitorData refreshed.");
                } else {
                    tracing::warn!("Failed to refresh YouTube visitorData.");
                }
                tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
            }
        });
        let cipher_url = config.cipher.url.clone();
        let cipher_token = config.cipher.token.clone();
        let create_client = |name: &str| -> Option<Arc<dyn YouTubeClient>> {
            match name.to_uppercase().as_str() {
                "WEB" => Some(Arc::new(WebClient::with_cipher_url(
                    http.clone(),
                    cipher_url.clone(),
                    cipher_token.clone(),
                ))),
                "WEB_REMIX" | "REMIX" | "MUSIC_WEB" => {
                    Some(Arc::new(WebRemixClient::new(http.clone())))
                }
                "MWEB" => Some(Arc::new(MWebClient::new(http.clone()))),
                "ANDROID" => Some(Arc::new(AndroidClient::new(http.clone()))),
                "IOS" => Some(Arc::new(IosClient::new(http.clone()))),
                "TV" | "TVHTML5" => Some(Arc::new(TvClient::new(http.clone()))),
                "TV_CAST" | "TVHTML5_CAST" => Some(Arc::new(TvCastClient::new(http.clone()))),
                "TVHTML5_SIMPLY_EMBEDDED_PLAYER" | "TV_EMBEDDED" => {
                    Some(Arc::new(TvEmbeddedClient::new(http.clone())))
                }
                "TVHTML5_SIMPLY" | "TV_SIMPLY" => Some(Arc::new(TvSimplyClient::new(http.clone()))),
                "TVHTML5_UNPLUGGED" | "TV_UNPLUGGED" => {
                    Some(Arc::new(TvUnpluggedClient::new(http.clone())))
                }
                "MUSIC" | "MUSIC_ANDROID" | "ANDROID_MUSIC" => {
                    Some(Arc::new(MusicAndroidClient::new(http.clone())))
                }
                "ANDROID_VR" | "ANDROIDVR" => Some(Arc::new(AndroidVrClient::new(http.clone()))),
                "WEB_EMBEDDED" | "WEBEMBEDDED" => {
                    Some(Arc::new(WebEmbeddedClient::new(http.clone())))
                }
                "WEB_PARENT_TOOLS" | "WEBPARENTTOOLS" => {
                    Some(Arc::new(WebParentToolsClient::new(http.clone())))
                }
                _ => {
                    tracing::warn!("Unknown YouTube client: {}", name);
                    None
                }
            }
        };
        let mut search_clients = Vec::new();
        for name in &config.clients.search {
            if let Some(client) = create_client(name) {
                search_clients.push(client);
            }
        }
        if search_clients.is_empty() {
            tracing::warn!("No valid YouTube search clients configured! Fallback to Web.");
            search_clients.push(Arc::new(WebClient::new(http.clone())));
        }
        let search_client_names: Vec<String> = search_clients
            .iter()
            .map(|c| c.name().to_string())
            .collect();
        tracing::debug!(
            "YouTube Search Clients initialized: {:?}",
            search_client_names
        );
        let mut playback_clients = Vec::new();
        for name in &config.clients.playback {
            if let Some(client) = create_client(name) {
                playback_clients.push(client);
            }
        }
        if playback_clients.is_empty() {
            tracing::warn!("No valid YouTube playback clients configured! Fallback to Web.");
            playback_clients.push(Arc::new(WebClient::new(http.clone())));
        }
        let mut resolve_clients = Vec::new();
        for name in &config.clients.resolve {
            if let Some(client) = create_client(name) {
                resolve_clients.push(client);
            }
        }
        if resolve_clients.is_empty() {
            tracing::warn!("No valid YouTube resolve clients configured! Fallback to Web.");
            resolve_clients.push(Arc::new(WebClient::new(http.clone())));
        }
        let music_search_clients: Vec<Arc<dyn YouTubeClient>> = vec![
            Arc::new(MusicAndroidClient::new(http.clone())),
            Arc::new(WebRemixClient::new(http.clone())),
        ];
        tracing::info!(
            "YouTube source initialized with {} search, {} playback, and {} resolve clients.",
            search_clients.len(),
            playback_clients.len(),
            resolve_clients.len()
        );
        Self {
            search_prefixes: vec!["ytsearch:".to_string(), "ytmsearch:".to_string()],
            rec_prefixes: vec!["ytrec:".to_string()],
            url_regex: Regex::new(r"(?:youtube\.com|youtu\.be)").unwrap(),
            search_clients,
            music_search_clients,
            playback_clients,
            resolve_clients,
            oauth,
            cipher_manager,
            visitor_data,
            http,
        }
    }
    pub fn stream_context(&self) -> Arc<YoutubeStreamContext> {
        Arc::new(YoutubeStreamContext {
            clients: self.playback_clients.clone(),
            oauth: self.oauth.clone(),
            cipher_manager: self.cipher_manager.clone(),
            visitor_data: self.visitor_data.clone(),
            http: self.http.clone(),
        })
    }
    async fn refresh_visitor_data(http: &reqwest::Client) -> Option<String> {
        match http
            .get("https://www.youtube.com/embed")
            .header("Cookie", "YSC=cz5kYp3ZuIE; VISITOR_INFO1_LIVE=U-0T5oUyzf8;")
            .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
            .send()
            .await
        {
            Ok(res) if res.status().is_success() => {
                if let Ok(text) = res.text().await {
                    let re = regex::Regex::new(r#""VISITOR_DATA":"([^"]+)""#).ok();
                    if let Some(vd) = re.as_ref().and_then(|r| r.captures(&text)).and_then(|c| c.get(1)) {
                        let raw = vd.as_str();
                        let decoded = urlencoding::decode(raw)
                            .map(|s| s.into_owned())
                            .unwrap_or_else(|_| raw.to_string());
                        tracing::debug!("YouTube: visitorData refreshed from embed page.");
                        return Some(decoded);
                    }
                }
            }
            Ok(res) => {
                tracing::warn!("YouTube embed page returned status {}; falling back to guide API.", res.status());
            }
            Err(e) => {
                tracing::warn!("YouTube embed page request failed: {}; falling back to guide API.", e);
            }
        }
        let body = json!({
            "context": {
                "client": {
                    "clientName": "WEB",
                    "clientVersion": "2.20260114.01.00",
                    "hl": "en",
                    "gl": "US"
                }
            }
        });
        match http
            .post("https://www.youtube.com/youtubei/v1/guide")
            .json(&body)
            .send()
            .await
        {
            Ok(res) => {
                if let Ok(json) = res.json::<Value>().await
                    && let Some(vd) = json
                        .get("responseContext")
                        .and_then(|rc| rc.get("visitorData"))
                        .and_then(|vd| vd.as_str())
                {
                    let decoded = urlencoding::decode(vd)
                        .map(|s| s.into_owned())
                        .unwrap_or_else(|_| vd.to_string());
                    tracing::debug!("YouTube: visitorData refreshed via guide API fallback.");
                    return Some(decoded);
                }
            }
            Err(e) => {
                tracing::warn!("Failed to fetch visitor data via guide API: {}", e);
            }
        }
        None
    }
    fn extract_playlist_id(&self, identifier: &str) -> Option<String> {
        if identifier.contains("list=") {
            return Some(
                identifier
                    .split("list=")
                    .nth(1)
                    .unwrap_or(identifier)
                    .split('&')
                    .next()
                    .unwrap_or(identifier)
                    .to_string(),
            );
        }
        None
    }
    fn extract_id(&self, identifier: &str) -> String {
        if identifier.contains("v=") {
            identifier
                .split("v=")
                .nth(1)
                .unwrap_or(identifier)
                .split('&')
                .next()
                .unwrap_or(identifier)
                .to_string()
        } else if identifier.contains("youtu.be/") {
            identifier
                .split("youtu.be/")
                .nth(1)
                .unwrap_or(identifier)
                .split('?')
                .next()
                .unwrap_or(identifier)
                .to_string()
        } else if identifier.contains("/live/") {
            identifier
                .split("/live/")
                .nth(1)
                .unwrap_or(identifier)
                .split('?')
                .next()
                .unwrap_or(identifier)
                .to_string()
        } else if identifier.contains("/shorts/") {
            identifier
                .split("/shorts/")
                .nth(1)
                .unwrap_or(identifier)
                .split('?')
                .next()
                .unwrap_or(identifier)
                .to_string()
        } else {
            identifier.to_string()
        }
    }
    fn prioritize_clients<'a>(
        &'a self,
        clients: &'a [Arc<dyn YouTubeClient>],
        prefer_music: bool,
    ) -> Vec<&'a Arc<dyn YouTubeClient>> {
        let is_music =
            |c: &&Arc<dyn YouTubeClient>| c.name().contains("Music") || c.name().contains("Remix");
        let mut ordered = Vec::with_capacity(clients.len());
        if prefer_music {
            ordered.extend(clients.iter().filter(|c| is_music(c)));
            ordered.extend(clients.iter().filter(|c| !is_music(c)));
        } else {
            ordered.extend(clients.iter().filter(|c| !is_music(c)));
            ordered.extend(clients.iter().filter(|c| is_music(c)));
        }
        ordered
    }
    fn fallback_clients<'a>(
        &'a self,
        tried: &[&Arc<dyn YouTubeClient>],
        prefer_music: bool,
    ) -> Vec<&'a Arc<dyn YouTubeClient>> {
        let tried_names: std::collections::HashSet<&str> = tried.iter().map(|c| c.name()).collect();
        let all_pools: &[&[Arc<dyn YouTubeClient>]] = &[
            &self.resolve_clients,
            &self.playback_clients,
            &self.search_clients,
        ];
        let mut seen = tried_names.clone();
        let mut fallback: Vec<&Arc<dyn YouTubeClient>> = Vec::new();
        for pool in all_pools {
            for client in pool.iter() {
                if seen.insert(client.name()) {
                    fallback.push(client);
                }
            }
        }
        self.prioritize_clients_slice(&fallback, prefer_music)
    }
    fn prioritize_clients_slice<'a>(
        &self,
        clients: &[&'a Arc<dyn YouTubeClient>],
        prefer_music: bool,
    ) -> Vec<&'a Arc<dyn YouTubeClient>> {
        let is_music =
            |c: &&&Arc<dyn YouTubeClient>| c.name().contains("Music") || c.name().contains("Remix");
        let mut ordered = Vec::with_capacity(clients.len());
        if prefer_music {
            ordered.extend(clients.iter().filter(|c| is_music(c)).copied());
            ordered.extend(clients.iter().filter(|c| !is_music(c)).copied());
        } else {
            ordered.extend(clients.iter().filter(|c| !is_music(c)).copied());
            ordered.extend(clients.iter().filter(|c| is_music(c)).copied());
        }
        ordered
    }
    pub fn cipher_manager(&self) -> Arc<YouTubeCipherManager> {
        self.cipher_manager.clone()
    }
}
#[async_trait]
impl SourcePlugin for YouTubeSource {
    fn name(&self) -> &str {
        "youtube"
    }
    fn can_handle(&self, identifier: &str) -> bool {
        self.search_prefixes
            .iter()
            .any(|p| identifier.starts_with(p))
            || self.rec_prefixes.iter().any(|p| identifier.starts_with(p))
            || self.url_regex.is_match(identifier)
    }
    fn search_prefixes(&self) -> Vec<&str> {
        self.search_prefixes.iter().map(|s| s.as_str()).collect()
    }
    async fn load(
        &self,
        identifier: &str,
        _routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
    ) -> LoadResult {
        let visitor_data = self.visitor_data.read().await.clone();
        let context = if let Some(vd) = visitor_data {
            json!({ "visitorData": vd })
        } else {
            json!({})
        };
        if let Some(prefix) = self
            .search_prefixes
            .iter()
            .find(|p| identifier.starts_with(*p))
        {
            return self.handle_search(identifier, prefix, &context).await;
        }
        if let Some(prefix) = self
            .rec_prefixes
            .iter()
            .find(|p| identifier.starts_with(*p))
        {
            return self
                .handle_recommendations(identifier, prefix, &context)
                .await;
        }
        if self.url_regex.is_match(identifier) {
            return self.handle_url(identifier, &context).await;
        }
        LoadResult::Empty {}
    }
    async fn get_track(
        &self,
        identifier: &str,
        routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
    ) -> Option<BoxedTrack> {
        let visitor_data = self.visitor_data.read().await.clone();
        let id = self.extract_id(identifier);
        let is_music_url = identifier.contains("music.youtube.com");
        let clients_to_try = self.prioritize_clients(&self.playback_clients, is_music_url);
        let clients = clients_to_try.into_iter().cloned().collect();
        Some(Arc::new(track::YoutubeTrack {
            identifier: id,
            clients,
            oauth: self.oauth.clone(),
            cipher_manager: self.cipher_manager.clone(),
            visitor_data,
            local_addr: routeplanner.and_then(|rp| rp.get_address()),
            proxy: None,
        }))
    }
}
impl YouTubeSource {
    async fn handle_search(&self, identifier: &str, prefix: &str, context: &Value) -> LoadResult {
        let prefer_music = prefix == "ytmsearch:";
        let query = &identifier[prefix.len()..];
        let is_music =
            |c: &Arc<dyn YouTubeClient>| c.name().contains("Music") || c.name().contains("Remix");
        if prefer_music {
            let mut music_clients: Vec<&Arc<dyn YouTubeClient>> = Vec::new();
            let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
            tracing::debug!(
                "Search: prefer_music=true. search_clients count: {}",
                self.search_clients.len()
            );
            for c in &self.music_search_clients {
                if seen.insert(c.name()) {
                    music_clients.push(c);
                }
            }
            for pool in [
                &self.search_clients[..],
                &self.resolve_clients[..],
                &self.playback_clients[..],
            ] {
                for c in pool {
                    if is_music(c) && seen.insert(c.name()) {
                        music_clients.push(c);
                    }
                }
            }
            for client in &music_clients {
                if !client.can_handle_request(identifier) {
                    continue;
                }
                tracing::debug!("Searching '{}' with {}", query, client.name());
                match client.search(query, context, self.oauth.clone()).await {
                    Ok(tracks) if !tracks.is_empty() => return LoadResult::Search(tracks),
                    Ok(_) => continue,
                    Err(e) => tracing::warn!("Music search error with {}: {}", client.name(), e),
                }
            }
            tracing::debug!(
                "All music clients returned empty for '{}', falling back to regular search",
                query
            );
        }
        let primary: Vec<&Arc<dyn YouTubeClient>> = self
            .search_clients
            .iter()
            .filter(|c| !is_music(c))
            .collect();
        for client in &primary {
            if !client.can_handle_request(identifier) {
                continue;
            }
            tracing::debug!("Searching '{}' with {}", query, client.name());
            match client.search(query, context, self.oauth.clone()).await {
                Ok(tracks) if !tracks.is_empty() => return LoadResult::Search(tracks),
                Ok(_) => continue,
                Err(e) => tracing::warn!("Search error with {}: {}", client.name(), e),
            }
        }
        let mut seen_search: std::collections::HashSet<&str> =
            primary.iter().map(|c| c.name()).collect();
        let secondary_search: Vec<&Arc<dyn YouTubeClient>> = self
            .search_clients
            .iter()
            .filter(|c| is_music(c) && seen_search.insert(c.name()))
            .collect();
        for client in &secondary_search {
            if !client.can_handle_request(identifier) {
                continue;
            }
            tracing::debug!("Secondary search '{}' with {}", query, client.name());
            match client.search(query, context, self.oauth.clone()).await {
                Ok(tracks) if !tracks.is_empty() => return LoadResult::Search(tracks),
                Ok(_) => continue,
                Err(e) => tracing::warn!("Secondary search error with {}: {}", client.name(), e),
            }
        }
        let tried: Vec<&Arc<dyn YouTubeClient>> =
            primary.into_iter().chain(secondary_search).collect();
        let fallback = self.fallback_clients(&tried, false);
        if !fallback.is_empty() {
            tracing::debug!(
                "All search clients failed for '{}', trying fallback clients",
                query
            );
        }
        for client in fallback {
            if !client.can_handle_request(identifier) {
                continue;
            }
            tracing::debug!("Fallback search '{}' with {}", query, client.name());
            match client.search(query, context, self.oauth.clone()).await {
                Ok(tracks) if !tracks.is_empty() => return LoadResult::Search(tracks),
                Ok(_) => continue,
                Err(e) => tracing::warn!("Fallback search error with {}: {}", client.name(), e),
            }
        }
        LoadResult::Empty {}
    }
    async fn handle_recommendations(
        &self,
        identifier: &str,
        prefix: &str,
        context: &Value,
    ) -> LoadResult {
        let seed_id = &identifier[prefix.len()..];
        let playlist_id = format!("RD{}", seed_id);
        let clients = self.prioritize_clients(&self.resolve_clients, true);
        for client in clients {
            if !client.can_handle_request(&playlist_id) {
                continue;
            }
            match client
                .get_playlist(&playlist_id, context, self.oauth.clone())
                .await
            {
                Ok(Some((tracks, title))) => {
                    let filtered: Vec<Track> = tracks
                        .into_iter()
                        .filter(|t| t.info.identifier != seed_id)
                        .collect();
                    return LoadResult::Playlist(PlaylistData {
                        info: PlaylistInfo {
                            name: format!("Recommendations: {}", title),
                            selected_track: -1,
                        },
                        plugin_info: json!({
                          "type": "recommendations",
                          "totalTracks": filtered.len()
                        }),
                        tracks: filtered,
                    });
                }
                _ => continue,
            }
        }
        LoadResult::Empty {}
    }
    async fn handle_url(&self, identifier: &str, context: &Value) -> LoadResult {
        let is_music_url = identifier.contains("music.youtube.com");
        if let Some(playlist_id) = self.extract_playlist_id(identifier) {
            let mut playlist_clients: Vec<&Arc<dyn YouTubeClient>> = Vec::new();
            for c in self.prioritize_clients(&self.resolve_clients, is_music_url) {
                if !playlist_clients.iter().any(|x| x.name() == c.name()) {
                    playlist_clients.push(c);
                }
            }
            for c in self.fallback_clients(&playlist_clients, is_music_url) {
                if !playlist_clients.iter().any(|x| x.name() == c.name()) {
                    playlist_clients.push(c);
                }
            }
            for client in &playlist_clients {
                if !client.can_handle_request(identifier) {
                    continue;
                }
                tracing::debug!("Fetching playlist '{}' with {}", playlist_id, client.name());
                match client
                    .get_playlist(&playlist_id, context, self.oauth.clone())
                    .await
                {
                    Ok(Some((tracks, title))) => {
                        return LoadResult::Playlist(PlaylistData {
                            info: PlaylistInfo {
                                name: title,
                                selected_track: -1,
                            },
                            plugin_info: json!({
                                "type": "playlist",
                                "url": format!("https://www.youtube.com/playlist?list={}", playlist_id),
                                "artworkUrl": tracks.first().and_then(|t| t.info.artwork_url.clone()),
                                "totalTracks": tracks.len()
                            }),
                            tracks,
                        });
                    }
                    _ => continue,
                }
            }
        }
        let id = self.extract_id(identifier);
        let resolve_clients: Vec<&Arc<dyn YouTubeClient>> = self.resolve_clients.iter().collect();
        for client in &resolve_clients {
            if !client.can_handle_request(identifier) {
                continue;
            }
            tracing::debug!("Resolving track '{}' with {}", id, client.name());
            match client
                .get_track_info(&id, context, self.oauth.clone())
                .await
            {
                Ok(Some(mut track)) => {
                    if is_music_url {
                        track.info.uri = Some(format!("https://music.youtube.com/watch?v={}", id));
                    }
                    return LoadResult::Track(track);
                }
                Ok(None) => continue,
                Err(e) => tracing::warn!("Resolve error with {}: {}", client.name(), e),
            }
        }
        let fallback = self.fallback_clients(&resolve_clients, false);
        if !fallback.is_empty() {
            tracing::debug!(
                "All resolve clients failed for '{}', trying {} fallback client(s)",
                id,
                fallback.len()
            );
        }
        for client in fallback {
            if !client.can_handle_request(identifier) {
                continue;
            }
            tracing::debug!("Fallback resolve '{}' with {}", id, client.name());
            if let Ok(Some(mut track)) = client
                .get_track_info(&id, context, self.oauth.clone())
                .await
            {
                if is_music_url {
                    track.info.uri = Some(format!("https://music.youtube.com/watch?v={}", id));
                }
                return LoadResult::Track(track);
            }
        }
        LoadResult::Empty {}
    }
}
