| use anyhow::{Result, anyhow}; |
| use reqwest::blocking::Client; |
| use reqwest::header::{ACCEPT, ACCEPT_ENCODING, CONNECTION, HeaderMap, HeaderValue, USER_AGENT}; |
| use serde_json::Value; |
| use std::time::Duration; |
|
|
| const AID: &str = "1967"; |
|
|
| fn normalize_base(base: &str) -> String { |
| base.trim().trim_end_matches('/').to_string() |
| } |
|
|
| fn ensure_trailing_query_base(url: &str) -> String { |
| let u = url.trim(); |
| if u.ends_with('?') || u.ends_with('&') { |
| return u.to_string(); |
| } |
| if u.contains('?') { |
| return format!("{}&", u); |
| } |
| format!("{}?", u) |
| } |
|
|
| fn derive_batch_full_base(endpoint: &str) -> String { |
| let base = normalize_base(endpoint); |
|
|
| |
| if base.contains("/reading/reader/batch_full") { |
| return ensure_trailing_query_base(&base); |
| } |
|
|
| ensure_trailing_query_base(&format!("{}/reading/reader/batch_full/v", base)) |
| } |
|
|
| fn join_params(params: &[(String, String)]) -> String { |
| |
| let mut out = String::new(); |
| for (i, (k, v)) in params.iter().enumerate() { |
| if i > 0 { |
| out.push('&'); |
| } |
| out.push_str(k); |
| out.push('='); |
| out.push_str(v); |
| } |
| out |
| } |
|
|
| |
| |
| |
| |
| #[derive(Clone)] |
| pub(crate) struct ThirdPartyContentClient { |
| client: Client, |
| batch_full_base: String, |
| } |
|
|
| impl ThirdPartyContentClient { |
| pub(crate) fn new( |
| endpoint: &str, |
| timeout_ms: Option<u64>, |
| connect_timeout_ms: Option<u64>, |
| ) -> Result<Self> { |
| let mut headers = HeaderMap::new(); |
| headers.insert(ACCEPT_ENCODING, HeaderValue::from_static("identity")); |
| headers.insert(CONNECTION, HeaderValue::from_static("keep-alive")); |
| headers.insert( |
| ACCEPT, |
| HeaderValue::from_static("application/json, text/plain, */*"), |
| ); |
| headers.insert( |
| USER_AGENT, |
| HeaderValue::from_static( |
| "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36", |
| ), |
| ); |
|
|
| let mut builder = Client::builder().default_headers(headers); |
| if let Some(ms) = timeout_ms { |
| builder = builder.timeout(Duration::from_millis(ms.max(50))); |
| } |
| if let Some(ms) = connect_timeout_ms { |
| builder = builder.connect_timeout(Duration::from_millis(ms.max(50))); |
| } |
|
|
| let client = builder.build()?; |
| Ok(Self { |
| client, |
| batch_full_base: derive_batch_full_base(endpoint), |
| }) |
| } |
|
|
| pub(crate) fn get_contents_unthrottled(&self, item_ids: &str, epub: bool) -> Result<Value> { |
| let item_ids = item_ids.trim(); |
| if item_ids.is_empty() { |
| return Err(anyhow!("item_ids 不能为空")); |
| } |
|
|
| |
| |
| let update_version_code = "0"; |
| let mut params: Vec<(String, String)> = vec![ |
| ("item_ids".to_string(), item_ids.to_string()), |
| ( |
| "update_version_code".to_string(), |
| update_version_code.to_string(), |
| ), |
| ("aid".to_string(), AID.to_string()), |
| ("key_register_ts".to_string(), "0".to_string()), |
| ("device_platform".to_string(), "android".to_string()), |
| ("iid".to_string(), "0".to_string()), |
| ]; |
| if epub { |
| params.push(("version_code".to_string(), update_version_code.to_string())); |
| params.push(("epub".to_string(), "1".to_string())); |
| } else { |
| params.push(("epub".to_string(), "0".to_string())); |
| } |
|
|
| let query = join_params(¶ms); |
| let url = format!("{}{}", self.batch_full_base, query); |
|
|
| let resp = self.client.get(&url).send()?; |
| let resp = resp.error_for_status()?; |
| let v: Value = resp.json()?; |
| Ok(v) |
| } |
| } |
|
|