sunox 0.0.9

Generate AI music from your terminal via direct Suno web workflows
use reqwest::header::{ACCEPT, ACCEPT_LANGUAGE, HeaderMap, USER_AGENT};

use super::SunoClient;
use crate::auth;
use crate::net::http;

impl SunoClient {
    pub(crate) fn headers(&self) -> HeaderMap {
        let mut headers = HeaderMap::new();
        let (jwt, device, browser_environment) = {
            let auth = self.auth.lock().expect("auth mutex poisoned");
            (
                auth.jwt.clone(),
                auth.device_id
                    .clone()
                    .unwrap_or_else(|| "00000000-0000-0000-0000-000000000000".to_string()),
                auth.browser_environment.clone(),
            )
        };

        if let Some(jwt) = jwt
            && let Ok(val) = format!("Bearer {jwt}").parse()
        {
            headers.insert("authorization", val);
        }
        if let Ok(val) = device.parse() {
            headers.insert("device-id", val);
        }
        if let Ok(val) = "*/*".parse() {
            headers.insert(ACCEPT, val);
        }
        if let Ok(val) = auth::browser_token().parse() {
            headers.insert("browser-token", val);
        }
        let browser_environment = browser_environment.as_ref();
        let user_agent = browser_environment
            .and_then(|env| env.user_agent.as_deref())
            .unwrap_or(http::BROWSER_USER_AGENT);
        if let Ok(val) = user_agent.parse() {
            headers.insert(USER_AGENT, val);
        }
        let accept_language = browser_environment
            .and_then(|env| env.accept_language.as_deref())
            .unwrap_or(http::BROWSER_ACCEPT_LANGUAGE);
        if let Ok(val) = accept_language.parse() {
            headers.insert(ACCEPT_LANGUAGE, val);
        }
        if let Some(client_hints) = chromium_client_hints(user_agent) {
            if let Ok(val) = client_hints.sec_ch_ua.parse() {
                headers.insert("sec-ch-ua", val);
            }
            if let Ok(val) = client_hints.sec_ch_ua_mobile.parse() {
                headers.insert("sec-ch-ua-mobile", val);
            }
            if let Ok(val) = client_hints.sec_ch_ua_platform.parse() {
                headers.insert("sec-ch-ua-platform", val);
            }
        }
        if let Ok(val) = "cors".parse() {
            headers.insert("sec-fetch-mode", val);
        }
        if let Ok(val) = "empty".parse() {
            headers.insert("sec-fetch-dest", val);
        }
        if let Ok(val) = "same-site".parse() {
            headers.insert("sec-fetch-site", val);
        }
        if let Ok(val) = "u=1, i".parse() {
            headers.insert("priority", val);
        }
        if let Ok(val) = "https://suno.com".parse() {
            headers.insert("origin", val);
        }
        if let Ok(val) = "https://suno.com/".parse() {
            headers.insert("referer", val);
        }
        headers
    }
}

struct ChromiumClientHints {
    sec_ch_ua: String,
    sec_ch_ua_mobile: &'static str,
    sec_ch_ua_platform: &'static str,
}

fn chromium_client_hints(user_agent: &str) -> Option<ChromiumClientHints> {
    let (brand, major) = if let Some(major) = major_version_after(user_agent, "Edg/") {
        ("Microsoft Edge", major)
    } else if let Some(major) = major_version_after(user_agent, "Chrome/") {
        ("Google Chrome", major)
    } else {
        return None;
    };

    Some(ChromiumClientHints {
        sec_ch_ua: format!(
            r#""{brand}";v="{major}", "Chromium";v="{major}", "Not)A;Brand";v="24""#
        ),
        sec_ch_ua_mobile: if user_agent.contains("Mobile") {
            "?1"
        } else {
            "?0"
        },
        sec_ch_ua_platform: sec_ch_ua_platform(user_agent),
    })
}

fn major_version_after(user_agent: &str, marker: &str) -> Option<String> {
    let version = user_agent.split_once(marker)?.1;
    let end = version
        .find(|ch: char| !ch.is_ascii_digit())
        .unwrap_or(version.len());
    if end == 0 {
        None
    } else {
        Some(version[..end].to_string())
    }
}

fn sec_ch_ua_platform(user_agent: &str) -> &'static str {
    if user_agent.contains("Android") {
        r#""Android""#
    } else if user_agent.contains("iPhone") || user_agent.contains("iPad") {
        r#""iOS""#
    } else if user_agent.contains("Windows") {
        r#""Windows""#
    } else if user_agent.contains("Macintosh") || user_agent.contains("Mac OS X") {
        r#""macOS""#
    } else if user_agent.contains("Linux") || user_agent.contains("X11") {
        r#""Linux""#
    } else {
        r#""Unknown""#
    }
}

#[cfg(test)]
mod tests {
    use super::chromium_client_hints;

    #[test]
    fn chrome_user_agent_becomes_current_web_client_hints() {
        let hints = chromium_client_hints(
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/149.0.0.0 Safari/537.36",
        )
        .expect("client hints");

        assert_eq!(
            hints.sec_ch_ua,
            r#""Google Chrome";v="149", "Chromium";v="149", "Not)A;Brand";v="24""#
        );
        assert_eq!(hints.sec_ch_ua_mobile, "?0");
        assert_eq!(hints.sec_ch_ua_platform, r#""macOS""#);
    }

    #[test]
    fn edge_user_agent_keeps_edge_brand() {
        let hints =
            chromium_client_hints("Mozilla/5.0 Chrome/149.0.0.0 Safari/537.36 Edg/149.0.0.0")
                .expect("client hints");

        assert_eq!(
            hints.sec_ch_ua,
            r#""Microsoft Edge";v="149", "Chromium";v="149", "Not)A;Brand";v="24""#
        );
    }

    #[test]
    fn windows_user_agent_sets_windows_platform_hint() {
        let hints = chromium_client_hints(
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/149.0.0.0 Safari/537.36",
        )
        .expect("client hints");

        assert_eq!(hints.sec_ch_ua_platform, r#""Windows""#);
    }

    #[test]
    fn non_chromium_user_agent_omits_client_hints() {
        assert!(chromium_client_hints("Mozilla/5.0 Firefox/140.0").is_none());
    }
}