use reqwest_middleware::{
reqwest::{
header::{ACCEPT_LANGUAGE, USER_AGENT},
ClientBuilder, Response, StatusCode,
},
ClientWithMiddleware,
};
use serde::Deserialize;
use std::error::Error;
use std::{collections::HashMap, sync::Arc};
use crate::{
tokens::{Session, SessionManager},
PixivAppError,
};
#[derive(Debug, Clone)]
pub struct PixivAppClient {
pub(crate) session: Arc<SessionManager>,
pub(crate) http_client: ClientWithMiddleware,
pub(crate) host: String,
pub(crate) platform: params::Platform,
}
pub mod params {
use serde::{Deserialize, Serialize};
use strum::Display;
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Display, Clone)]
pub enum Platform {
#[strum(to_string = "ios")]
IOS,
#[strum(to_string = "android")]
Android,
}
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Display, Clone)]
pub enum Visibility {
#[strum(to_string = "public")]
Public,
#[strum(to_string = "private")]
Private,
}
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Display, Clone)]
pub enum SortMode {
#[strum(to_string = "date_asc")]
DateAscending,
#[strum(to_string = "date_desc")]
DateDescending,
#[strum(to_string = "popular_desc")]
PopularDescending,
}
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Display, Clone)]
pub enum SearchMode {
#[strum(to_string = "partial_match_for_tags")]
PartialMatchForTags,
#[strum(to_string = "exact_match_for_tags")]
ExactMatchForTags,
#[strum(to_string = "title_and_caption")]
TitleAndCaption,
}
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Display, Clone)]
pub enum RankingMode {
#[strum(to_string = "day")]
Day,
#[strum(to_string = "week")]
Week,
#[strum(to_string = "month")]
Month,
}
}
impl PixivAppClient {
pub fn new(token: String) -> Self {
Self::_new(Arc::new(SessionManager::new(token)))
}
pub fn restore(session: Session) -> Self {
Self::_new(Arc::new(SessionManager::restore(session)))
}
fn _new(sessionman: Arc<SessionManager>) -> Self {
Self {
session: sessionman.clone(),
http_client: reqwest_middleware::ClientBuilder::new(
ClientBuilder::new()
.default_headers(
(&HashMap::from([
("app-os".to_string(), "ios".to_string()),
("app-os-version".to_string(), "12.2".to_string()),
("app-version".to_string(), "7.6.2".to_string()),
(
USER_AGENT.to_string(),
"PixivIOSApp/7.6.2 (iOS 12.2; iPhone9,1)".to_string(),
),
]))
.try_into()
.unwrap(),
)
.build()
.unwrap(),
)
.with_arc(sessionman.clone())
.build(),
host: String::from("https://app-api.pixiv.net"),
platform: params::Platform::IOS,
}
}
pub async fn session(&self) -> Session {
self.session.session.read().await.clone()
}
pub(crate) async fn process_request<T: for<'de> Deserialize<'de>>(
&self,
reqb: reqwest_middleware::RequestBuilder,
) -> Result<T, Box<dyn Error + Send + Sync>> {
Ok(reqb
.header(ACCEPT_LANGUAGE, "en-US")
.send()
.await
.map_or_else(
|e| Err::<Response, Box<dyn Error + Send + Sync>>(Box::new(e)),
|r| match r.status() {
x if StatusCode::is_success(&x) || StatusCode::is_redirection(&x) => Ok(r),
x if StatusCode::is_client_error(&x) => Err(Box::new(match x {
StatusCode::BAD_REQUEST => PixivAppError::RequestFailed,
StatusCode::UNAUTHORIZED => PixivAppError::MissingLogin,
StatusCode::NOT_FOUND => PixivAppError::TargetNotFound,
StatusCode::TOO_MANY_REQUESTS => PixivAppError::RateLimitReached,
s => PixivAppError::UnhandledStatus(s),
})),
x => Err(Box::new(PixivAppError::UnhandledStatus(x))),
},
)?
.text()
.await
.map(|r| async move {
if cfg!(test) {
eprintln!("{}", r);
}
let d = &mut serde_json::Deserializer::from_str(&r);
let r = serde_path_to_error::deserialize(d);
r
})
.map_err(Box::new)?
.await?)
}
}
#[cfg(test)]
pub mod tests {
use once_cell::sync::Lazy;
use super::*;
use tokio::test;
pub static USER: &str = "Aio";
pub static USER_ID: u32 = 25308802;
pub static ILLUST_ID: u32 = 132610892;
pub static ILLUST_SERIES_ID: u32 = 144416706;
pub static SERIES_ID: u32 = 280609;
pub static _STUB: Lazy<Arc<()>> = Lazy::new(|| Arc::new(env_logger::init()));
pub fn get_client() -> PixivAppClient {
let _ = _STUB.clone();
let client = PixivAppClient::new(env!("PIXIV_REFRESH_TOKEN").to_string());
client
}
#[test]
async fn test_bad_client() {
let client = PixivAppClient::new("".to_string());
assert!(client.illust_details(ILLUST_ID).await.is_err());
}
#[test]
async fn test_session_restore() {
let client = get_client();
assert!(client.illust_details(ILLUST_ID).await.is_ok());
let client2 = PixivAppClient::restore(client.session().await);
assert!(client2.illust_details(ILLUST_ID).await.is_ok());
}
#[test]
async fn test_bad_session_restore() {
let _ = get_client(); let client = PixivAppClient::restore(
serde_json::from_str(&format!(
r#"{{
"access_token":"",
"refresh_token":"{}",
"expiry":"2026-05-22T02:42:52.554084912+02:00"}}"#,
env!("PIXIV_REFRESH_TOKEN")
))
.unwrap(),
);
assert!(client.illust_details(ILLUST_ID).await.is_ok());
}
}