use reqwest::Method;
use super::response::{SearchResponse, TrackDetailResponse};
use crate::error::SearchError;
use crate::providers::web::base_api;
use rand::RngExt;
use std::sync::LazyLock;
const SEARCH_USER_AGENT: &str = "com.luna.music/100198030 (Linux; U; Android 15; zh_CN_#Hans; ABR-AL80; Build/V417IR;tt-ok/3.12.13.19)";
const WEB_USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
static SEARCH_DEVICE_ID: LazyLock<String> = LazyLock::new(generate_client_id);
static SEARCH_INSTALL_ID: LazyLock<String> = LazyLock::new(generate_client_id);
fn generate_client_id() -> String {
let mut rng = rand::rng();
let a = rng.random_range(10_000_000..99_999_999);
let b = rng.random_range(10_000_000..99_999_999);
format!("{}{}", a, b)
}
const API_BASE: &str = "https://api.qishui.com/luna/";
const H5_BASE: &str = "https://beta-luna.douyin.com/luna/h5/";
fn build_url(base: &str, path: &str, query: &[(&str, String)]) -> String {
let qs: Vec<String> = query
.iter()
.map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
.collect();
format!("{}{}?{}", base, path, qs.join("&"))
}
pub async fn get_lyrics(track_id: &str) -> Result<(Option<String>, Option<String>), SearchError> {
let query = [
("track_id", track_id.to_string()),
("device_platform", "web".to_string()),
];
let url = build_url(H5_BASE, "seo_track", &query);
let headers = [
("Accept", "application/json"),
("User-Agent", WEB_USER_AGENT),
];
let response = base_api::send(Method::GET, &url, &headers).await?;
let detail: TrackDetailResponse = base_api::json(response).await?;
let Some(lyric) = detail
.lyric
.or_else(|| detail.seo_track.and_then(|seo_track| seo_track.lyric))
else {
return Ok((None, None));
};
Ok((
lyric.content.filter(|content| !content.is_empty()),
lyric
.translations
.and_then(|translations| translations.cn)
.filter(|content| !content.is_empty()),
))
}
pub(crate) async fn search(keyword: &str) -> Result<SearchResponse, SearchError> {
let query = [
("device_platform", "android".to_string()),
("os", "android".to_string()),
("ssmix", "a".to_string()),
("cdid", "46556f98-1720-4248-83da-62b74b60b46a".to_string()),
("channel", "xiaomi_8478_64".to_string()),
("aid", "386088".to_string()),
("app_name", "luna".to_string()),
("version_code", "100198030".to_string()),
("version_name", "19.8.0".to_string()),
("manifest_version_code", "100198030".to_string()),
("update_version_code", "100198030".to_string()),
("resolution", "1080*1920".to_string()),
("dpi", "480".to_string()),
("device_type", "ABR-AL80".to_string()),
("device_brand", "HUAWEI".to_string()),
("language", "zh".to_string()),
("os_api", "35".to_string()),
("os_version", "15".to_string()),
("ac", "wifi".to_string()),
("device_model", "ABR-AL80".to_string()),
("tz_name", "Asia/Shanghai".to_string()),
("tz_offset", "28800".to_string()),
("package", "com.luna.music".to_string()),
("sim_region", "cn".to_string()),
("iid", SEARCH_INSTALL_ID.clone()),
("device_id", SEARCH_DEVICE_ID.clone()),
("_rticket", base_api::unix_millis().to_string()),
("q", keyword.to_string()),
("cursor", "0".to_string()),
("count", "20".to_string()),
];
let url = build_url(API_BASE, "search/track", &query);
let headers = [("Accept", "*/*"), ("User-Agent", SEARCH_USER_AGENT)];
let response = base_api::send(Method::GET, &url, &headers).await?;
base_api::json(response).await
}