novel-api 0.19.1

Novel APIs from various sources
Documentation
use std::time::{SystemTime, UNIX_EPOCH};

use hex_simd::AsciiCase;
use http::HeaderValue;
use reqwest::Response;
use serde::Serialize;
use serde::de::DeserializeOwned;
use tokio::sync::OnceCell;
use url::Url;
use uuid::Uuid;

use crate::{Error, HTTPClient, NovelDB, SfacgClient};

include!(concat!(env!("OUT_DIR"), "/codegen.rs"));

impl SfacgClient {
    const APP_NAME: &'static str = "sfacg";

    const HOST: &'static str = "https://api.sfacg.com";
    const USER_AGENT: &'static str = "boluobao/5.2.64(iOS;26.5)/appStore/{}/appStore";
    const USER_AGENT_RSS: &'static str = "SFReader/5.2.64 (iPhone; iOS 26.5; Scale/3.00)";

    const USERNAME: &'static str = "apiuser";
    const PASSWORD: &'static str = "3s#1-yt6e*Acv@qer";

    const SALT: &'static str = "a@Lk7Tf4gh8TUPoX";

    /// Create a sfacg client
    pub async fn new() -> Result<Self, Error> {
        Ok(Self {
            proxy: None,
            no_proxy: false,
            cert_path: None,
            client: OnceCell::new(),
            client_rss: OnceCell::new(),
            db: OnceCell::new(),
        })
    }

    pub(crate) async fn db(&self) -> Result<&NovelDB, Error> {
        self.db
            .get_or_try_init(|| async { NovelDB::new(SfacgClient::APP_NAME).await })
            .await
    }

    pub(crate) async fn client(&self) -> Result<&HTTPClient, Error> {
        self.client
            .get_or_try_init(|| async {
                let device_token = crate::uid().to_string().to_uppercase();
                let user_agent = SfacgClient::USER_AGENT.replace("{}", &device_token);

                HTTPClient::builder()
                    .app_name(SfacgClient::APP_NAME)
                    .accept(HeaderValue::from_static(
                        "application/vnd.sfacg.api+json;version=1",
                    ))
                    .accept_language(HeaderValue::from_static("zh-Hans-CN;q=1"))
                    .cookie(true)
                    .user_agent(user_agent)
                    .maybe_proxy(self.proxy.clone())
                    .no_proxy(self.no_proxy)
                    .maybe_cert_path(self.cert_path.clone())
                    .retry_url(Url::parse(SfacgClient::HOST)?)
                    .build()
                    .await
            })
            .await
    }

    pub(crate) async fn client_rss(&self) -> Result<&HTTPClient, Error> {
        self.client_rss
            .get_or_try_init(|| async {
                HTTPClient::builder()
                    .app_name(SfacgClient::APP_NAME)
                    .accept(HeaderValue::from_static("image/*,*/*;q=0.8"))
                    .accept_language(HeaderValue::from_static("zh-CN,zh-Hans;q=0.9"))
                    .user_agent(SfacgClient::USER_AGENT_RSS.to_string())
                    .maybe_proxy(self.proxy.clone())
                    .no_proxy(self.no_proxy)
                    .maybe_cert_path(self.cert_path.clone())
                    .build()
                    .await
            })
            .await
    }

    pub(crate) async fn get<T, R>(&self, url: T) -> Result<R, Error>
    where
        T: AsRef<str>,
        R: DeserializeOwned,
    {
        let response = self
            .client()
            .await?
            .get(SfacgClient::HOST.to_string() + url.as_ref())
            .basic_auth(SfacgClient::USERNAME, Some(SfacgClient::PASSWORD))
            .header("sfsecurity", self.sf_security()?)
            .send()
            .await?;

        Ok(sonic_rs::from_slice(&response.bytes().await?)?)
    }

    pub(crate) async fn get_query<T, E, R>(&self, url: T, query: E) -> Result<R, Error>
    where
        T: AsRef<str>,
        E: Serialize,
        R: DeserializeOwned,
    {
        let mut count = 0;

        let response = loop {
            let response = self
                .client()
                .await?
                .get(SfacgClient::HOST.to_string() + url.as_ref())
                .query(&query)
                .basic_auth(SfacgClient::USERNAME, Some(SfacgClient::PASSWORD))
                .header("sfsecurity", self.sf_security()?)
                .send()
                .await;

            if let Ok(response) = response {
                break response;
            } else {
                tracing::info!(
                    "HTTP request failed: `{}`, retry, number of times: `{}`",
                    response.as_ref().unwrap_err(),
                    count + 1
                );

                count += 1;
                if count > 3 {
                    response?;
                }
            }
        };

        Ok(sonic_rs::from_slice(&response.bytes().await?)?)
    }

    pub(crate) async fn post<T, E, R>(&self, url: T, json: E) -> Result<R, Error>
    where
        T: AsRef<str>,
        E: Serialize,
        R: DeserializeOwned,
    {
        let response = self
            .client()
            .await?
            .post(SfacgClient::HOST.to_string() + url.as_ref())
            .basic_auth(SfacgClient::USERNAME, Some(SfacgClient::PASSWORD))
            .header("sfsecurity", self.sf_security()?)
            .json(&json)
            .send()
            .await?;

        Ok(sonic_rs::from_slice(&response.bytes().await?)?)
    }

    pub(crate) async fn put<T, E, R>(&self, url: T, json: E) -> Result<R, Error>
    where
        T: AsRef<str>,
        E: Serialize,
        R: DeserializeOwned,
    {
        let response = self
            .client()
            .await?
            .put(SfacgClient::HOST.to_string() + url.as_ref())
            .basic_auth(SfacgClient::USERNAME, Some(SfacgClient::PASSWORD))
            .header("sfsecurity", self.sf_security()?)
            .json(&json)
            .send()
            .await?;

        Ok(sonic_rs::from_slice(&response.bytes().await?)?)
    }

    pub(crate) async fn get_rss(&self, url: &Url) -> Result<Response, Error> {
        let response = self.client_rss().await?.get(url.clone()).send().await?;
        crate::check_status(response.status(), format!("HTTP request failed: `{url}`"))?;

        Ok(response)
    }

    fn sf_security(&self) -> Result<String, Error> {
        let uuid = Uuid::new_v4();
        let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis();
        let device_token = crate::uid().to_string().to_uppercase();

        let sign = crate::md5_hex(
            format!("{uuid}{timestamp}{device_token}{}", SfacgClient::SALT),
            AsciiCase::Upper,
        );

        Ok(format!(
            "nonce={uuid}&timestamp={timestamp}&devicetoken={device_token}&sign={sign}"
        ))
    }

    pub(crate) fn convert(content: String) -> String {
        let mut result = String::with_capacity(content.len());

        for c in content.chars() {
            result.push(*CHARACTER_MAPPER.get(&c).unwrap_or(&c));
        }

        result
    }
}