1use std::time::{SystemTime, UNIX_EPOCH};
2
3use hex_simd::AsciiCase;
4use http::HeaderValue;
5use reqwest::Response;
6use serde::Serialize;
7use serde::de::DeserializeOwned;
8use tokio::sync::OnceCell;
9use url::Url;
10use uuid::Uuid;
11
12use crate::{Error, HTTPClient, NovelDB, SfacgClient};
13
14include!(concat!(env!("OUT_DIR"), "/codegen.rs"));
15
16impl SfacgClient {
17 const APP_NAME: &'static str = "sfacg";
18
19 const HOST: &'static str = "https://api.sfacg.com";
20 const USER_AGENT: &'static str = "boluobao/5.2.20(iOS;26.1)/appStore/{}/appStore";
21 const USER_AGENT_RSS: &'static str = "SFReader/5.2.20 (iPhone; iOS 26.1; Scale/3.00)";
22
23 const USERNAME: &'static str = "apiuser";
24 const PASSWORD: &'static str = "3s#1-yt6e*Acv@qer";
25
26 const SALT: &'static str = "a@Lk7Tf4gh8TUPoX";
27
28 pub async fn new() -> Result<Self, Error> {
30 Ok(Self {
31 proxy: None,
32 no_proxy: false,
33 cert_path: None,
34 client: OnceCell::new(),
35 client_rss: OnceCell::new(),
36 db: OnceCell::new(),
37 })
38 }
39
40 pub(crate) async fn db(&self) -> Result<&NovelDB, Error> {
41 self.db
42 .get_or_try_init(|| async { NovelDB::new(SfacgClient::APP_NAME).await })
43 .await
44 }
45
46 pub(crate) async fn client(&self) -> Result<&HTTPClient, Error> {
47 self.client
48 .get_or_try_init(|| async {
49 let device_token = crate::uid().to_string().to_uppercase();
50 let user_agent = SfacgClient::USER_AGENT.replace("{}", &device_token);
51
52 HTTPClient::builder()
53 .app_name(SfacgClient::APP_NAME)
54 .accept(HeaderValue::from_static(
55 "application/vnd.sfacg.api+json;version=1",
56 ))
57 .accept_language(HeaderValue::from_static("zh-Hans-CN;q=1"))
58 .cookie(true)
59 .user_agent(user_agent)
60 .maybe_proxy(self.proxy.clone())
61 .no_proxy(self.no_proxy)
62 .maybe_cert_path(self.cert_path.clone())
63 .retry_host(SfacgClient::HOST)
64 .build()
65 .await
66 })
67 .await
68 }
69
70 pub(crate) async fn client_rss(&self) -> Result<&HTTPClient, Error> {
71 self.client_rss
72 .get_or_try_init(|| async {
73 HTTPClient::builder()
74 .app_name(SfacgClient::APP_NAME)
75 .accept(HeaderValue::from_static("image/*,*/*;q=0.8"))
76 .accept_language(HeaderValue::from_static("zh-CN,zh-Hans;q=0.9"))
77 .user_agent(SfacgClient::USER_AGENT_RSS.to_string())
78 .maybe_proxy(self.proxy.clone())
79 .no_proxy(self.no_proxy)
80 .maybe_cert_path(self.cert_path.clone())
81 .build()
82 .await
83 })
84 .await
85 }
86
87 pub(crate) async fn get<T, R>(&self, url: T) -> Result<R, Error>
88 where
89 T: AsRef<str>,
90 R: DeserializeOwned,
91 {
92 let response = self
93 .client()
94 .await?
95 .get(SfacgClient::HOST.to_string() + url.as_ref())
96 .basic_auth(SfacgClient::USERNAME, Some(SfacgClient::PASSWORD))
97 .header("sfsecurity", self.sf_security()?)
98 .send()
99 .await?;
100
101 Ok(sonic_rs::from_slice(&response.bytes().await?)?)
102 }
103
104 pub(crate) async fn get_query<T, E, R>(&self, url: T, query: E) -> Result<R, Error>
105 where
106 T: AsRef<str>,
107 E: Serialize,
108 R: DeserializeOwned,
109 {
110 let mut count = 0;
111
112 let response = loop {
113 let response = self
114 .client()
115 .await?
116 .get(SfacgClient::HOST.to_string() + url.as_ref())
117 .query(&query)
118 .basic_auth(SfacgClient::USERNAME, Some(SfacgClient::PASSWORD))
119 .header("sfsecurity", self.sf_security()?)
120 .send()
121 .await;
122
123 if let Ok(response) = response {
124 break response;
125 } else {
126 tracing::info!(
127 "HTTP request failed: `{}`, retry, number of times: `{}`",
128 response.as_ref().unwrap_err(),
129 count + 1
130 );
131
132 count += 1;
133 if count > 3 {
134 response?;
135 }
136 }
137 };
138
139 Ok(sonic_rs::from_slice(&response.bytes().await?)?)
140 }
141
142 pub(crate) async fn post<T, E, R>(&self, url: T, json: E) -> Result<R, Error>
143 where
144 T: AsRef<str>,
145 E: Serialize,
146 R: DeserializeOwned,
147 {
148 let response = self
149 .client()
150 .await?
151 .post(SfacgClient::HOST.to_string() + url.as_ref())
152 .basic_auth(SfacgClient::USERNAME, Some(SfacgClient::PASSWORD))
153 .header("sfsecurity", self.sf_security()?)
154 .json(&json)
155 .send()
156 .await?;
157
158 Ok(sonic_rs::from_slice(&response.bytes().await?)?)
159 }
160
161 pub(crate) async fn put<T, E, R>(&self, url: T, json: E) -> Result<R, Error>
162 where
163 T: AsRef<str>,
164 E: Serialize,
165 R: DeserializeOwned,
166 {
167 let response = self
168 .client()
169 .await?
170 .put(SfacgClient::HOST.to_string() + url.as_ref())
171 .basic_auth(SfacgClient::USERNAME, Some(SfacgClient::PASSWORD))
172 .header("sfsecurity", self.sf_security()?)
173 .json(&json)
174 .send()
175 .await?;
176
177 Ok(sonic_rs::from_slice(&response.bytes().await?)?)
178 }
179
180 pub(crate) async fn get_rss(&self, url: &Url) -> Result<Response, Error> {
181 let response = self.client_rss().await?.get(url.clone()).send().await?;
182 crate::check_status(response.status(), format!("HTTP request failed: `{url}`"))?;
183
184 Ok(response)
185 }
186
187 fn sf_security(&self) -> Result<String, Error> {
188 let uuid = Uuid::new_v4();
189 let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis();
190 let device_token = crate::uid().to_string().to_uppercase();
191
192 let sign = crate::md5_hex(
193 format!("{uuid}{timestamp}{device_token}{}", SfacgClient::SALT),
194 AsciiCase::Upper,
195 );
196
197 Ok(format!(
198 "nonce={uuid}×tamp={timestamp}&devicetoken={device_token}&sign={sign}"
199 ))
200 }
201
202 pub(crate) fn convert(content: String) -> String {
203 let mut result = String::with_capacity(content.len());
204
205 for c in content.chars() {
206 result.push(*CHARACTER_MAPPER.get(&c).unwrap_or(&c));
207 }
208
209 result
210 }
211}