use crate::error::{PixivError, Result};
use crate::models::app::{
ContentType, Duration, Filter, IllustDetail, RankingMode, RankingResponse, RecommendedResponse,
SearchIllustResponse, SearchTarget, Sort,
};
use crate::network::bypass_sni::BypassSniClient;
use serde::Deserialize;
use std::collections::HashMap;
use tracing::debug;
#[derive(Debug, Clone)]
pub struct BypassSniAppClient {
http_client: BypassSniClient,
base_url: String,
}
impl BypassSniAppClient {
pub fn new(http_client: BypassSniClient) -> Self {
Self {
http_client,
base_url: "https://app-api.pixiv.net".to_string(),
}
}
pub fn with_ip(ip: &str) -> Result<Self> {
let http_client = BypassSniClient::new(ip)?;
Ok(Self {
http_client,
base_url: "https://app-api.pixiv.net".to_string(),
})
}
pub fn set_base_url(&mut self, url: String) {
self.base_url = url;
}
pub fn base_url(&self) -> &str {
&self.base_url
}
pub async fn illust_detail(&self, illust_id: u64) -> Result<IllustDetail> {
debug!(illust_id = %illust_id, "Fetching illustration detail with SNI bypass");
let url = format!("{}/v1/illust/detail", self.base_url);
let params = [("illust_id", illust_id.to_string())];
let response = self
.http_client
.send_request(reqwest::Method::GET, &url, Some(¶ms))
.await?;
let text = response.text().await?;
let detail: IllustDetail = serde_json::from_str(&text)?;
Ok(detail)
}
pub async fn illust_ranking(
&self,
mode: RankingMode,
filter: Filter,
date: Option<&str>,
offset: Option<u32>,
) -> Result<RankingResponse> {
debug!(
mode = %mode.to_string(),
filter = %filter.to_string(),
date = ?date,
offset = ?offset,
"Fetching illustration ranking with SNI bypass"
);
let url = format!("{}/v1/illust/ranking", self.base_url);
let mut params = Vec::new();
params.push(("mode", mode.to_string()));
params.push(("filter", filter.to_string()));
if let Some(date) = date {
params.push(("date", date.to_string()));
}
if let Some(offset) = offset {
params.push(("offset", offset.to_string()));
}
let response = self
.http_client
.send_request(reqwest::Method::GET, &url, Some(¶ms))
.await?;
let text = response.text().await?;
let ranking: RankingResponse = serde_json::from_str(&text)?;
Ok(ranking)
}
pub async fn illust_recommended(
&self,
content_type: ContentType,
include_ranking_label: bool,
filter: Filter,
max_bookmark_id_for_recommend: Option<u64>,
min_bookmark_id_for_recent_illust: Option<u64>,
offset: Option<u32>,
include_ranking_illusts: Option<bool>,
bookmark_illust_ids: Option<Vec<u64>>,
viewed: Option<Vec<String>>,
) -> Result<RecommendedResponse> {
debug!(
content_type = %content_type.to_string(),
include_ranking_label = %include_ranking_label,
filter = %filter.to_string(),
max_bookmark_id_for_recommend = ?max_bookmark_id_for_recommend,
min_bookmark_id_for_recent_illust = ?min_bookmark_id_for_recent_illust,
offset = ?offset,
include_ranking_illusts = ?include_ranking_illusts,
bookmark_illust_ids = ?bookmark_illust_ids,
viewed = ?viewed,
"Fetching recommended illustrations with SNI bypass"
);
let url = format!("{}/v1/illust/recommended", self.base_url);
let mut params = Vec::new();
params.push(("content_type".to_string(), content_type.to_string()));
params.push(("include_ranking_label".to_string(), include_ranking_label.to_string()));
params.push(("filter".to_string(), filter.to_string()));
if let Some(max_bookmark_id_for_recommend) = max_bookmark_id_for_recommend {
params.push(("max_bookmark_id_for_recommend".to_string(), max_bookmark_id_for_recommend.to_string()));
}
if let Some(min_bookmark_id_for_recent_illust) = min_bookmark_id_for_recent_illust {
params.push(("min_bookmark_id_for_recent_illust".to_string(), min_bookmark_id_for_recent_illust.to_string()));
}
if let Some(offset) = offset {
params.push(("offset".to_string(), offset.to_string()));
}
if let Some(include_ranking_illusts) = include_ranking_illusts {
params.push(("include_ranking_illusts".to_string(), include_ranking_illusts.to_string()));
}
if let Some(bookmark_illust_ids) = bookmark_illust_ids {
let ids = bookmark_illust_ids
.iter()
.map(|id| id.to_string())
.collect::<Vec<_>>()
.join(",");
params.push(("bookmark_illust_ids".to_string(), ids));
}
if let Some(viewed) = viewed {
for (i, viewed_id) in viewed.iter().enumerate() {
let key = format!("viewed[{}]", i);
params.push((key, viewed_id.to_string()));
}
}
let response = self
.http_client
.send_request(reqwest::Method::GET, &url, Some(¶ms))
.await?;
let text = response.text().await?;
let recommended: RecommendedResponse = serde_json::from_str(&text)?;
Ok(recommended)
}
pub async fn search_illust(
&self,
word: &str,
search_target: SearchTarget,
sort: Sort,
duration: Option<Duration>,
start_date: Option<&str>,
end_date: Option<&str>,
filter: Filter,
search_ai_type: Option<u32>,
offset: Option<u32>,
) -> Result<SearchIllustResponse> {
debug!(
word = %word,
search_target = %search_target.to_string(),
sort = %sort.to_string(),
duration = ?duration,
start_date = ?start_date,
end_date = ?end_date,
filter = %filter.to_string(),
search_ai_type = ?search_ai_type,
offset = ?offset,
"Searching illustrations with SNI bypass"
);
let url = format!("{}/v1/search/illust", self.base_url);
let mut params = Vec::new();
params.push(("word", word.to_string()));
params.push(("search_target", search_target.to_string()));
params.push(("sort", sort.to_string()));
params.push(("filter", filter.to_string()));
if let Some(duration) = duration {
params.push(("duration", duration.to_string()));
}
if let Some(start_date) = start_date {
params.push(("start_date", start_date.to_string()));
}
if let Some(end_date) = end_date {
params.push(("end_date", end_date.to_string()));
}
if let Some(search_ai_type) = search_ai_type {
params.push(("search_ai_type", search_ai_type.to_string()));
}
if let Some(offset) = offset {
params.push(("offset", offset.to_string()));
}
let response = self
.http_client
.send_request(reqwest::Method::GET, &url, Some(¶ms))
.await?;
let text = response.text().await?;
let search_result: SearchIllustResponse = serde_json::from_str(&text)?;
Ok(search_result)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bypass_sni_app_client_creation() {
let result = BypassSniAppClient::with_ip("210.140.131.145");
assert!(result.is_ok());
}
#[test]
fn test_invalid_ip() {
let result = BypassSniAppClient::with_ip("invalid_ip");
assert!(result.is_err());
}
}