pub mod api {
    use aes::{
        Aes128,
        cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray},
    };
    use md5::{Digest, Md5};
    use serde::{Deserialize, de::DeserializeOwned};
    use serde_json::Value;
    use tracing::{debug, error};
    #[derive(Debug, Deserialize)]
    pub struct NeteaseResponse<T> {
        pub code: i64,
        #[serde(flatten)]
        pub data: T,
    }
    #[derive(Debug, Deserialize)]
    pub struct NeteaseArtist {
        pub id: i64,
        pub name: String,
    }
    #[derive(Debug, Deserialize)]
    pub struct NeteaseAlbum {
        pub id: i64,
        pub name: String,
        #[serde(alias = "picUrl")]
        pub pic_url: Option<String>,
    }
    #[derive(Debug, Deserialize)]
    pub struct NeteaseSong {
        pub id: i64,
        pub name: String,
        #[serde(alias = "ar", alias = "artists")]
        pub artists: Vec<NeteaseArtist>,
        #[serde(alias = "al", alias = "album")]
        pub album: Option<NeteaseAlbum>,
        #[serde(alias = "dt", alias = "duration")]
        pub duration: u64,
    }
    #[derive(Debug, Deserialize)]
    pub struct SearchResultData {
        pub result: SearchResultInner,
    }
    #[derive(Debug, Deserialize)]
    pub struct SearchResultInner {
        #[serde(default)]
        pub songs: Vec<NeteaseSong>,
        #[serde(default)]
        pub albums: Vec<NeteaseAlbum>,
        #[serde(default)]
        pub artists: Vec<NeteaseArtist>,
        #[serde(default)]
        pub playlists: Vec<NeteasePlaylist>,
    }
    #[derive(Debug, Deserialize)]
    pub struct NeteasePlaylist {
        pub id: i64,
        pub name: String,
        #[serde(alias = "coverImgUrl")]
        pub cover_img_url: Option<String>,
        pub creator: Option<NeteaseCreator>,
        #[serde(alias = "trackCount")]
        pub track_count: Option<u64>,
    }
    #[derive(Debug, Deserialize)]
    pub struct NeteaseCreator {
        pub nickname: String,
    }
    #[derive(Debug, Deserialize)]
    pub struct SongDetailData {
        pub songs: Vec<NeteaseSong>,
    }
    #[derive(Debug, Deserialize)]
    pub struct TrackUrlData {
        pub data: Vec<TrackUrlItem>,
    }
    #[derive(Debug, Deserialize)]
    pub struct TrackUrlItem {
        pub id: i64,
        pub url: Option<String>,
        pub br: i64,
        pub code: i64,
        #[serde(rename = "freeTrialInfo")]
        pub free_trial_info: Option<serde_json::Value>,
    }
    #[derive(Debug, Deserialize)]
    pub struct SimilarSongsData {
        pub songs: Vec<NeteaseSong>,
    }
    #[derive(Debug, Deserialize)]
    pub struct AlbumDetailData {
        pub album: NeteaseAlbum,
        pub songs: Vec<NeteaseSong>,
    }
    #[derive(Debug, Deserialize)]
    pub struct PlaylistDetailData {
        pub playlist: PlaylistInfo,
    }
    #[derive(Debug, Deserialize)]
    pub struct PlaylistInfo {
        pub id: i64,
        pub name: String,
        pub tracks: Vec<NeteaseSong>,
    }
    #[derive(Debug, Deserialize)]
    pub struct ArtistDetailData {
        pub artist: NeteaseArtist,
        #[serde(rename = "hotSongs")]
        pub hot_songs: Vec<NeteaseSong>,
    }
    const EAPI_KEY: &[u8] = b"e82ckenh8dichen8";
    const EAPI_URLS: &[&str] = &[
        "https://interface3.music.163.com/eapi",
        "https://interface.music.163.com/eapi",
    ];
    pub fn aes_encrypt_ecb(data: &[u8], key: &[u8]) -> Vec<u8> {
        let cipher = Aes128::new(GenericArray::from_slice(key));
        let mut padded_data = pkcs7_pad(data, 16);
        for chunk in padded_data.chunks_mut(16) {
            let block = GenericArray::from_mut_slice(chunk);
            cipher.encrypt_block(block);
        }
        padded_data
    }
    fn pkcs7_pad(data: &[u8], block_size: usize) -> Vec<u8> {
        let padding_len = block_size - (data.len() % block_size);
        let mut padded = data.to_vec();
        padded.extend(std::iter::repeat_n(padding_len as u8, padding_len));
        padded
    }
    pub fn md5_hex(data: &str) -> String {
        let mut hasher = Md5::new();
        hasher.update(data.as_bytes());
        let result = hasher.finalize();
        hex::encode(result)
    }
    pub fn eapi_encrypt(url: &str, obj: &Value) -> String {
        let text = serde_json::to_string(obj).unwrap_or_default();
        let message = format!("nobody{}use{}md5forencrypt", url, text);
        let digest = md5_hex(&message);
        let data = format!("{}-36cd479b6b5-{}-36cd479b6b5-{}", url, text, digest);
        let encrypted = aes_encrypt_ecb(data.as_bytes(), EAPI_KEY);
        hex::encode(encrypted).to_uppercase()
    }
    pub async fn get_eapi_json<T: DeserializeOwned>(
        client: &reqwest::Client,
        path: &str,
        obj: Value,
        nuid: &str,
        device_id: &str,
    ) -> Option<T> {
        let params = eapi_encrypt(path, &obj);
        let network_path = path.strip_prefix("/api").unwrap_or(path);
        for &base_url in EAPI_URLS {
            let url = format!("{}{}", base_url, network_path);
            let resp = client.post(&url)
            .header("User-Agent", "NeteaseMusic/2.5.1 (iPhone; iOS 16.6; Scale/3.00)")
            .header("Referer", "https://music.163.com/")
            .header("Origin", "https://music.163.com")
            .header("X-Real-IP", "118.88.88.88")
            .header("X-Forwarded-For", "118.88.88.88")
            .header("X-Netease-PC-IP", "118.88.88.88")
            .header("Cookie", format!(
                "os=iOS; appver=2.5.1; _ntes_nuid={}; deviceId={}; channel=AppStore; mobilename=iPhone15,3", 
                nuid, device_id
            ))
            .form(&[("params", params.clone())])
            .send()
            .await;
            match resp {
                Ok(r) if r.status().is_success() => {
                    let text = r.text().await.ok()?;
                    match serde_json::from_str::<NeteaseResponse<T>>(&text) {
                        Ok(res) => {
                            if res.code == 200 || res.code == 0 {
                                return Some(res.data);
                            } else {
                                debug!(
                                    "Netease API {} returned application code {}: {}",
                                    path, res.code, text
                                );
                            }
                        }
                        Err(e) => {
                            error!(
                                "Netease API {} failed to parse JSON: {}. Text: {}",
                                path, e, text
                            );
                        }
                    }
                }
                _ => continue,
            }
        }
        None
    }
}
pub mod manager {
    use super::api::*;
    use crate::protocol::tracks::{
        LoadResult, PlaylistData, PlaylistInfo, SearchResult, Track, TrackInfo,
    };
    use serde_json::json;
    pub fn parse_track(song: &NeteaseSong) -> Option<Track> {
        let id = song.id.to_string();
        let artist = song
            .artists
            .first()
            .map(|a| a.name.as_str())
            .unwrap_or("Unknown Artist");
        let artwork_url = song.album.as_ref().and_then(|al| al.pic_url.clone());
        Some(Track::new(TrackInfo {
            identifier: id.clone(),
            is_seekable: true,
            author: artist.to_string(),
            length: song.duration,
            is_stream: false,
            position: 0,
            title: song.name.clone(),
            uri: Some(format!("https://music.163.com/song?id={}", id)),
            artwork_url,
            isrc: None,
            source_name: "netease".to_string(),
        }))
    }
    pub fn parse_album(album: &NeteaseAlbum) -> PlaylistData {
        PlaylistData {
            info: PlaylistInfo {
                name: album.name.clone(),
                selected_track: -1,
            },
            plugin_info: json!({
                "type": "album",
                "url": format!("https://music.163.com/album?id={}", album.id),
                "artworkUrl": album.pic_url,
                "author": "Netease Music",
                "totalTracks": 0
            }),
            tracks: Vec::new(),
        }
    }
    pub fn parse_artist(artist: &NeteaseArtist) -> PlaylistData {
        PlaylistData {
            info: PlaylistInfo {
                name: format!("{}'s Top Tracks", artist.name),
                selected_track: -1,
            },
            plugin_info: json!({
                "type": "artist",
                "url": format!("https://music.163.com/artist?id={}", artist.id),
                "artworkUrl": None::<String>,
                "author": artist.name,
                "totalTracks": 0
            }),
            tracks: Vec::new(),
        }
    }
    pub fn parse_playlist(playlist: &NeteasePlaylist) -> PlaylistData {
        PlaylistData {
            info: PlaylistInfo {
                name: playlist.name.clone(),
                selected_track: -1,
            },
            plugin_info: json!({
                "type": "playlist",
                "url": format!("https://music.163.com/playlist?id={}", playlist.id),
                "artworkUrl": playlist.cover_img_url,
                "author": playlist.creator.as_ref().map(|c| c.nickname.clone()).unwrap_or_else(|| "Netease Music".to_string()),
                "totalTracks": playlist.track_count.unwrap_or(0)
            }),
            tracks: Vec::new(),
        }
    }
    #[derive(Debug, PartialEq)]
    pub enum TrackUrlResult {
        Success(String),
        Code(i64),
        Trial,
        None,
    }
    pub async fn fetch_track_detail(
        client: &reqwest::Client,
        nuid: &str,
        device_id: &str,
        id: &str,
    ) -> Option<SongDetailData> {
        get_eapi_json(
            client,
            "/api/v3/song/detail",
            json!({
                "c": serde_json::to_string(&[json!({"id": id})]).unwrap(),
                "header": {
                    "os": "iOS",
                    "appver": "2.5.1",
                    "deviceId": device_id
                }
            }),
            nuid,
            device_id,
        )
        .await
    }
    pub async fn fetch_album(
        client: &reqwest::Client,
        nuid: &str,
        device_id: &str,
        id: &str,
    ) -> LoadResult {
        match get_eapi_json::<AlbumDetailData>(
            client,
            &format!("/api/v1/album/{}", id),
            json!({}),
            nuid,
            device_id,
        )
        .await
        {
            Some(resp) if !resp.songs.is_empty() => {
                let tracks = resp.songs.iter().filter_map(parse_track).collect();
                LoadResult::Playlist(PlaylistData {
                    info: PlaylistInfo {
                        name: resp.album.name,
                        selected_track: -1,
                    },
                    plugin_info: json!({ "type": "album", "id": id }),
                    tracks,
                })
            }
            _ => LoadResult::Empty {},
        }
    }
    pub async fn fetch_playlist(
        client: &reqwest::Client,
        nuid: &str,
        device_id: &str,
        id: &str,
    ) -> LoadResult {
        match get_eapi_json::<PlaylistDetailData>(
            client,
            "/api/v6/playlist/detail",
            json!({ "id": id, "n": 1000 }),
            nuid,
            device_id,
        )
        .await
        {
            Some(resp) if !resp.playlist.tracks.is_empty() => {
                let tracks = resp
                    .playlist
                    .tracks
                    .iter()
                    .filter_map(parse_track)
                    .collect();
                LoadResult::Playlist(PlaylistData {
                    info: PlaylistInfo {
                        name: resp.playlist.name,
                        selected_track: -1,
                    },
                    plugin_info: json!({ "type": "playlist", "id": id }),
                    tracks,
                })
            }
            _ => LoadResult::Empty {},
        }
    }
    pub async fn fetch_artist(
        client: &reqwest::Client,
        nuid: &str,
        device_id: &str,
        id: &str,
    ) -> LoadResult {
        match get_eapi_json::<ArtistDetailData>(
            client,
            &format!("/api/v1/artist/{}", id),
            json!({}),
            nuid,
            device_id,
        )
        .await
        {
            Some(resp) if !resp.hot_songs.is_empty() => {
                let tracks = resp.hot_songs.iter().filter_map(parse_track).collect();
                LoadResult::Playlist(PlaylistData {
                    info: PlaylistInfo {
                        name: format!("{}'s Top Tracks", resp.artist.name),
                        selected_track: -1,
                    },
                    plugin_info: json!({ "type": "artist", "id": id }),
                    tracks,
                })
            }
            _ => LoadResult::Empty {},
        }
    }
    pub async fn fetch_track_url(
        client: &reqwest::Client,
        nuid: &str,
        device_id: &str,
        id: &str,
        level: &str,
        encode_type: &str,
    ) -> TrackUrlResult {
        let resp: Option<TrackUrlData> = get_eapi_json(
            client,
            "/api/song/enhance/player/url/v1",
            json!({
                "ids": [id],
                "level": level,
                "encodeType": encode_type,
                "header": {
                    "os": "iOS",
                    "appver": "2.5.1",
                    "deviceId": device_id
                }
            }),
            nuid,
            device_id,
        )
        .await;
        let data_wrap = match resp {
            Some(v) => v,
            None => return TrackUrlResult::None,
        };
        let data = match data_wrap.data.first() {
            Some(d) => d,
            None => return TrackUrlResult::None,
        };
        if data.code == -110 {
            return TrackUrlResult::Code(-110);
        }
        if let Some(trial_info) = &data.free_trial_info
            && trial_info.as_object().is_some_and(|o| !o.is_empty())
        {
            return TrackUrlResult::Trial;
        }
        if let Some(url) = &data.url
            && !url.is_empty()
        {
            return TrackUrlResult::Success(url.clone());
        }
        if data.code != 200 && data.code != 0 {
            TrackUrlResult::Code(data.code)
        } else {
            TrackUrlResult::None
        }
    }
    pub async fn fetch_track_url_legacy(
        client: &reqwest::Client,
        device_id: &str,
        id: &str,
        br: &str,
    ) -> Option<String> {
        let url = format!(
            "https://music.163.com/api/song/enhance/player/url?id={}&ids=[{}]&br={}",
            id, id, br
        );
        let resp = client.get(&url)
        .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36")
        .header("Referer", "https://music.163.com/")
        .header("X-Real-IP", "118.88.88.88")
        .header("Cookie", format!("os=pc; deviceId={}", device_id))
        .send()
        .await
        .ok()?;
        if !resp.status().is_success() {
            return None;
        }
        let res: NeteaseResponse<TrackUrlData> = resp.json().await.ok()?;
        let data = res.data.data.first()?;
        if let Some(url) = &data.url
            && !url.is_empty()
        {
            return Some(url.clone());
        }
        None
    }
    pub async fn check_url(client: &reqwest::Client, url: &str) -> bool {
        let resp = match client.head(url)
        .header("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 CloudMusic/2.5.1")
        .header("Referer", "https://music.163.com/")
        .send()
        .await
    {
        Ok(r) => r,
        Err(_) => return false,
    };
        if !resp.status().is_success() {
            return false;
        }
        let content_length = resp
            .headers()
            .get(reqwest::header::CONTENT_LENGTH)
            .and_then(|h| h.to_str().ok())
            .and_then(|s| s.parse::<u64>().ok())
            .unwrap_or(0);
        content_length >= 100 * 1024
    }
    pub async fn search_tracks(
        client: &reqwest::Client,
        nuid: &str,
        device_id: &str,
        query: &str,
        limit: usize,
    ) -> LoadResult {
        match get_eapi_json::<SearchResultData>(
            client,
            "/api/cloudsearch/pc",
            json!({
                "s": query,
                "type": 1,
                "limit": limit,
                "offset": 0,
                "total": true
            }),
            nuid,
            device_id,
        )
        .await
        {
            Some(resp) if !resp.result.songs.is_empty() => {
                let tracks = resp.result.songs.iter().filter_map(parse_track).collect();
                LoadResult::Search(tracks)
            }
            _ => LoadResult::Empty {},
        }
    }
    pub async fn search_full(
        client: &reqwest::Client,
        nuid: &str,
        device_id: &str,
        query: &str,
        types: &[String],
        limit: usize,
    ) -> Option<SearchResult> {
        let mut tracks = Vec::new();
        let mut albums = Vec::new();
        let mut artists = Vec::new();
        let mut playlists = Vec::new();
        let all_types = types.is_empty();
        if (all_types || types.contains(&"track".to_owned()))
            && let Some(res) = fetch_search(client, nuid, device_id, query, 1, limit).await
        {
            tracks = res.songs.iter().filter_map(parse_track).collect();
        }
        if (all_types || types.contains(&"album".to_owned()))
            && let Some(res) = fetch_search(client, nuid, device_id, query, 10, limit).await
        {
            albums = res.albums.iter().map(parse_album).collect();
        }
        if (all_types || types.contains(&"artist".to_owned()))
            && let Some(res) = fetch_search(client, nuid, device_id, query, 100, limit).await
        {
            artists = res.artists.iter().map(parse_artist).collect();
        }
        if (all_types || types.contains(&"playlist".to_owned()))
            && let Some(res) = fetch_search(client, nuid, device_id, query, 1000, limit).await
        {
            playlists = res.playlists.iter().map(parse_playlist).collect();
        }
        Some(SearchResult {
            tracks,
            albums,
            artists,
            playlists,
            texts: Vec::new(),
            plugin: json!({}),
        })
    }
    async fn fetch_search(
        client: &reqwest::Client,
        nuid: &str,
        device_id: &str,
        query: &str,
        type_id: i32,
        limit: usize,
    ) -> Option<SearchResultInner> {
        get_eapi_json::<SearchResultData>(
            client,
            "/api/cloudsearch/pc",
            json!({
                "s": query,
                "type": type_id,
                "limit": limit,
                "offset": 0,
                "total": true
            }),
            nuid,
            device_id,
        )
        .await
        .map(|d| d.result)
    }
    pub async fn fetch_recommendations(
        client: &reqwest::Client,
        nuid: &str,
        device_id: &str,
        identifier: &str,
    ) -> LoadResult {
        let id = identifier.trim();
        if !id.chars().all(|c| c.is_ascii_digit()) || id.is_empty() {
            return LoadResult::Empty {};
        }
        match get_eapi_json::<SimilarSongsData>(
            client,
            "/api/v1/discovery/simiSong",
            json!({ "songid": id }),
            nuid,
            device_id,
        )
        .await
        {
            Some(resp) if !resp.songs.is_empty() => {
                let tracks = resp.songs.iter().filter_map(parse_track).collect();
                LoadResult::Playlist(PlaylistData {
                    info: PlaylistInfo {
                        name: format!("Similar to Track {}", id),
                        selected_track: -1,
                    },
                    plugin_info: json!({ "type": "recommendations", "seed_id": id }),
                    tracks,
                })
            }
            _ => LoadResult::Empty {},
        }
    }
}
pub mod track {
    use crate::{
        config::HttpProxyConfig,
        sources::{
            http::HttpTrack,
            playable_track::{PlayableTrack, ResolvedTrack},
        },
    };
    use async_trait::async_trait;
    use std::net::IpAddr;
    use tracing::debug;
    pub struct NeteaseTrack {
        pub stream_url: String,
        pub local_addr: Option<IpAddr>,
        pub proxy: Option<HttpProxyConfig>,
    }
    #[async_trait]
    impl PlayableTrack for NeteaseTrack {
        async fn resolve(&self) -> Result<ResolvedTrack, String> {
            let url = self.stream_url.clone();
            debug!("Netease playback URL: {url}");
            HttpTrack {
                url,
                local_addr: self.local_addr,
                proxy: None,
            }
            .resolve()
            .await
        }
    }
}
use crate::{
    protocol::tracks::{LoadResult, SearchResult},
    sources::{SourcePlugin, playable_track::BoxedTrack},
};
use async_trait::async_trait;
use rand::Rng;
use regex::Regex;
use std::sync::{Arc, OnceLock};
use tracing::debug;
fn url_regex() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        Regex::new(r"https?://music\.163\.com/(?:(?:#|m)/)?(?P<type>song|album|playlist|artist)(?:\?id=|\/)(?P<id>\d+)").unwrap()
    })
}
pub struct NeteaseSource {
    pub(crate) client: Arc<reqwest::Client>,
    pub(crate) proxy: Option<crate::config::HttpProxyConfig>,
    pub(crate) search_limit: usize,
    pub(crate) nuid: String,
    pub(crate) device_id: String,
}
impl NeteaseSource {
    pub fn new(
        config: Option<crate::config::NeteaseMusicConfig>,
        client: Arc<reqwest::Client>,
    ) -> Result<Self, String> {
        let cfg = config.ok_or("Netease Music configuration is missing")?;
        let mut rng = rand::thread_rng();
        let nuid: String = (0..16)
            .map(|_| format!("{:02x}", rng.r#gen::<u8>()))
            .collect::<Vec<String>>()
            .join("");
        let device_id: String = (0..8)
            .map(|_| format!("{:02X}", rng.r#gen::<u8>()))
            .collect::<Vec<String>>()
            .join("");
        Ok(Self {
            client,
            proxy: cfg.proxy,
            search_limit: cfg.search_limit,
            nuid,
            device_id,
        })
    }
}
#[async_trait]
impl SourcePlugin for NeteaseSource {
    fn name(&self) -> &str {
        "netease"
    }
    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))
            || url_regex().is_match(identifier)
    }
    fn search_prefixes(&self) -> Vec<&str> {
        vec!["nmsearch:", "ncsearch:"]
    }
    fn rec_prefixes(&self) -> Vec<&str> {
        vec!["nmrec:", "ncrec:"]
    }
    async fn load(
        &self,
        identifier: &str,
        _routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
    ) -> LoadResult {
        for prefix in self.search_prefixes() {
            if let Some(query) = identifier.strip_prefix(prefix) {
                return manager::search_tracks(
                    &self.client,
                    &self.nuid,
                    &self.device_id,
                    query,
                    self.search_limit,
                )
                .await;
            }
        }
        for prefix in self.rec_prefixes() {
            if let Some(query) = identifier.strip_prefix(prefix) {
                return manager::fetch_recommendations(
                    &self.client,
                    &self.nuid,
                    &self.device_id,
                    query,
                )
                .await;
            }
        }
        if let Some(caps) = url_regex().captures(identifier) {
            let type_ = caps.name("type").map(|m| m.as_str()).unwrap_or("");
            let id = caps.name("id").map(|m| m.as_str()).unwrap_or("");
            match type_ {
                "song" => {
                    if let Some(detail) =
                        manager::fetch_track_detail(&self.client, &self.nuid, &self.device_id, id)
                            .await
                        && let Some(song) = detail.songs.first()
                        && let Some(track) = manager::parse_track(song)
                    {
                        return LoadResult::Track(track);
                    }
                }
                "album" => {
                    return manager::fetch_album(&self.client, &self.nuid, &self.device_id, id)
                        .await;
                }
                "playlist" => {
                    return manager::fetch_playlist(&self.client, &self.nuid, &self.device_id, id)
                        .await;
                }
                "artist" => {
                    return manager::fetch_artist(&self.client, &self.nuid, &self.device_id, id)
                        .await;
                }
                _ => {}
            }
            return LoadResult::Empty {};
        }
        if identifier.chars().all(|c| c.is_ascii_digit())
            && !identifier.is_empty()
            && let Some(detail) =
                manager::fetch_track_detail(&self.client, &self.nuid, &self.device_id, identifier)
                    .await
            && let Some(song) = detail.songs.first()
            && let Some(track) = manager::parse_track(song)
        {
            return LoadResult::Track(track);
        }
        manager::search_tracks(
            &self.client,
            &self.nuid,
            &self.device_id,
            identifier,
            self.search_limit,
        )
        .await
    }
    async fn load_search(
        &self,
        query: &str,
        _types: &[String],
        _routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
    ) -> Option<SearchResult> {
        let mut q = query;
        for prefix in self.search_prefixes() {
            if let Some(stripped) = query.strip_prefix(prefix) {
                q = stripped;
                break;
            }
        }
        manager::search_full(
            &self.client,
            &self.nuid,
            &self.device_id,
            q,
            _types,
            self.search_limit,
        )
        .await
    }
    async fn get_track(
        &self,
        identifier: &str,
        routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
    ) -> Option<BoxedTrack> {
        let id = url_regex()
            .captures(identifier)
            .and_then(|caps| caps.name("id"))
            .map(|m| m.as_str())
            .unwrap_or(identifier);
        debug!("Netease: Resolving track ID: {}", id);
        let mut stream_url = None;
        let mut fallback_early = false;
        let qualities = [
            ("aac", "standard"),
            ("aac", "higher"),
            ("aac", "exhigh"),
            ("aac", "lossless"),
            ("aac", "hires"),
            ("aac", "jymaster"),
            ("aac", "sky"),
            ("aac", "jyeffect"),
            ("aac", "jylive"),
            ("mp3", "standard"),
            ("mp3", "higher"),
            ("mp3", "exhigh"),
            ("mp3", "lossless"),
            ("mp3", "hires"),
            ("mp3", "jymaster"),
            ("mp3", "sky"),
            ("mp3", "jyeffect"),
            ("mp3", "jylive"),
        ];
        let mut first_code: Option<i64> = None;
        for (format, level) in qualities {
            match manager::fetch_track_url(
                &self.client,
                &self.nuid,
                &self.device_id,
                id,
                level,
                format,
            )
            .await
            {
                manager::TrackUrlResult::Success(url)
                    if manager::check_url(&self.client, &url).await =>
                {
                    stream_url = Some(url);
                    break;
                }
                manager::TrackUrlResult::Code(-110) => {
                    fallback_early = true;
                    break;
                }
                manager::TrackUrlResult::Trial => {
                    debug!("Netease: Track {} is trial-only, skipping quality loop", id);
                    fallback_early = true;
                    break;
                }
                manager::TrackUrlResult::Code(c) => {
                    first_code = first_code.or(Some(c));
                    continue;
                }
                _ => continue,
            }
        }
        if stream_url.is_none() || fallback_early {
            for br in ["320000", "128000"] {
                if let Some(url) =
                    manager::fetch_track_url_legacy(&self.client, &self.device_id, id, br).await
                    && !url.is_empty()
                    && manager::check_url(&self.client, &url).await
                {
                    stream_url = Some(url);
                    break;
                }
            }
        }
        if stream_url.is_none() {
            debug!(
                "Netease: Failed to resolve playback URL for track ID: {}",
                id
            );
        }
        stream_url.map(|url| {
            Arc::new(track::NeteaseTrack {
                stream_url: url,
                proxy: self.proxy.clone(),
                local_addr: routeplanner.and_then(|rp| rp.get_address()),
            }) as BoxedTrack
        })
    }
    fn get_proxy_config(&self) -> Option<crate::config::HttpProxyConfig> {
        self.proxy.clone()
    }
}
