use std::{borrow::Cow, sync::Arc};
use anyhow::Result;
use base64::{Engine, engine::general_purpose};
use regex::Regex;
use reqwest::{Client, cookie::Jar, header::HeaderMap};
use crate::api::endpoint::{LOGIN_CHECK_URL, RadikoEndpoint};
pub const USER_AGENT_VALUE: &str =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:140.0) Gecko/20100101 Firefox/140.0";
#[derive(Debug, Clone)]
pub struct RadikoAuthManager {
inner: Arc<RadikoAuthManagerRef>,
}
#[derive(Debug, Clone)]
struct RadikoAuthManagerRef {
area_id: String,
http_client: Client,
auth_token: String,
stream_lsid: String,
}
impl RadikoAuthManager {
pub async fn new() -> Self {
Self::init().await.unwrap()
}
pub fn area_id(&self) -> Cow<str> {
Cow::Borrowed(&self.inner.area_id)
}
pub fn http_client(&self) -> Client {
self.inner.http_client.clone()
}
pub fn auth_token(&self) -> Cow<str> {
Cow::Borrowed(&self.inner.auth_token)
}
pub fn lsid(&self) -> Cow<str> {
Cow::Borrowed(&self.inner.stream_lsid)
}
pub async fn refresh_auth(&mut self) -> Result<Self> {
Self::init().await
}
async fn init() -> Result<Self> {
let auth1_url = RadikoEndpoint::auth1_endpoint();
let auth2_url = RadikoEndpoint::auth2_endpoint();
let auth_key = Self::get_public_auth_key().await;
let cookie_jar = Arc::new(Jar::default());
let client = Client::builder()
.cookie_provider(cookie_jar.clone())
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.build()?;
let response_body = client
.get(RadikoEndpoint::area_id_endpoint())
.send()
.await?
.text()
.await?;
let area_id_pattern = Regex::new(r"[A-Z]{2}[0-9]{2}")?;
let Some(area_id_caps) = area_id_pattern.captures(&response_body) else {
panic!("not found pattern area_id");
};
let area_id = &area_id_caps[0];
let _ = client.get(LOGIN_CHECK_URL).send().await?;
let mut headers = HeaderMap::new();
headers.insert("X-Radiko-App", "pc_html5".parse()?);
headers.insert("X-Radiko-App-Version", "0.0.1".parse()?);
headers.insert("X-Radiko-User", "dummy_user".parse()?);
headers.insert("X-Radiko-Device", "pc".parse()?);
let res_auth1 = client.get(auth1_url).headers(headers).send().await?;
let auth_token = res_auth1
.headers()
.get("X-Radiko-Authtoken")
.unwrap()
.to_str()?;
let offset = res_auth1
.headers()
.get("X-Radiko-KeyOffset")
.unwrap()
.to_str()?
.parse::<usize>()?;
let length = res_auth1
.headers()
.get("X-Radiko-KeyLength")
.unwrap()
.to_str()?
.parse::<usize>()?;
let partial_key = general_purpose::STANDARD.encode(&auth_key[offset..offset + length]);
let mut headers = HeaderMap::new();
headers.insert("X-Radiko-Authtoken", auth_token.parse()?);
headers.insert("X-Radiko-Partialkey", partial_key.parse()?);
headers.insert("X-Radiko-User", "dummy_user".parse()?);
headers.insert("X-Radiko-Device", "pc".parse()?);
let _res_auth2 = client
.get(&auth2_url)
.headers(headers.clone())
.send()
.await?;
let lsid = crate::utils::generate_md5_hash();
let authed_client = Client::builder()
.default_headers(headers.clone())
.cookie_provider(cookie_jar.clone())
.build()?;
Ok(Self {
inner: Arc::new(RadikoAuthManagerRef {
area_id: area_id.to_string(),
http_client: authed_client,
auth_token: auth_token.to_string(),
stream_lsid: lsid,
}),
})
}
async fn get_public_auth_key() -> String {
let url = "https://radiko.jp/apps/js/playerCommon.js";
let response_body = reqwest::get(url).await.unwrap().text().await.unwrap();
let auth_key_pattern =
regex::Regex::new(r"new RadikoJSPlayer\(.*?,.*?,.'(?P<auth_key>\w+)'").unwrap();
let Some(auth_key_caps) = auth_key_pattern.captures(&response_body) else {
panic!("failed get auth_key ")
};
auth_key_caps["auth_key"].to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn init_radiko_auth_manager_test() -> Result<()> {
let radiko_auth_manager = RadikoAuthManager::new().await;
println!("radiko_auth_manager: {:#?}", radiko_auth_manager);
Ok(())
}
}