1#![allow(unused)]
2
3use crate::{PixivError, PixivImage};
4use rand::{
5 prelude::{IndexedRandom, ThreadRng},
6 Rng,
7};
8use reqwest::{
9 header::{COOKIE, REFERER, USER_AGENT},
10 Client,
11};
12use serde::{
13 de::{MapAccess, Visitor},
14 ser::SerializeStruct,
15 Deserialize, Deserializer, Serialize, Serializer,
16};
17use serde_json::Value;
18use std::{
19 cell::RefCell,
20 collections::BTreeMap,
21 fmt::Formatter,
22 ops::{DerefMut, Range},
23 path::{Path, PathBuf},
24 time::Duration,
25};
26use tokio::{fs::File, io::AsyncWriteExt, time::Sleep};
27
28pub mod images;
29pub mod tags;
30
31#[derive(Debug, Deserialize)]
32pub struct PixivResponse<T> {
33 pub error: bool,
34 #[serde(default)]
35 pub message: String,
36 pub body: T,
37}
38
39impl<T> PixivResponse<T> {
40 pub fn throw(self, context: impl Into<String>) -> Result<T, PixivError> {
41 match self.error {
42 true => Err(PixivError::request_error(self.message, context)),
43 false => Ok(self.body),
44 }
45 }
46}
47
48
49#[derive(Copy, Clone, Debug)]
50pub enum PixivImageRatio {
51 Landscape,
52 Portrait,
53 Square,
54 All,
55}
56
57
58#[derive(Debug, Serialize, Deserialize)]
59pub struct Meta {
60 pub title: String,
61 pub description: String,
62 pub canonical: String,
63 #[serde(rename = "descriptionHeader")]
64 pub description_header: String,
65}
66
67#[derive(Debug, Serialize, Deserialize)]
68pub struct ExtraData {
69 pub meta: Meta,
70}
71
72#[derive(Debug, Serialize, Deserialize)]
73pub struct Struct3 {
74 pub url: String,
75}
76
77#[derive(Debug, Serialize, Deserialize)]
78pub struct ZoneConfig {
79 pub logo: Struct3,
80 pub header: Struct3,
81 pub footer: Struct3,
82 pub infeed: Struct3,
83}
84
85#[derive(Debug, Serialize, Deserialize)]
86pub struct Popular {
87 pub recent: Value,
88 pub permanent: Value,
89}
90
91#[derive(Debug, Serialize, Deserialize)]
92pub struct Struct1 {
93 pub min: Option<i64>,
94 pub max: Option<Value>,
95}
96
97#[derive(Debug, Serialize, Deserialize)]
98pub struct TitleCaptionTranslation {
99 #[serde(rename = "workTitle")]
100 pub work_title: Value,
101 #[serde(rename = "workCaption")]
102 pub work_caption: Value,
103}
104
105#[derive(Clone, Debug, Serialize, Default)]
106pub struct IllustData {
107 pub id: u64,
108 pub tags: Vec<String>,
109 pub title: String,
110 pub description: String,
111 pub width: u32,
112 pub height: u32,
113 #[serde(flatten)]
114 pub unknown_fields: BTreeMap<String, Value>,
115}
116
117impl<'de> Deserialize<'de> for IllustData {
118 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
119 where
120 D: Deserializer<'de>,
121 {
122 let mut data = IllustData::default();
123 let visitor = IllustDataVisitor { data: &mut data };
124 deserializer.deserialize_map(visitor)?;
125 Ok(data)
126 }
127}
128
129struct IllustDataVisitor<'i> {
130 data: &'i mut IllustData,
131}
132
133impl<'i, 'de> Visitor<'de> for IllustDataVisitor<'i> {
134 type Value = ();
135
136 fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
137 todo!()
138 }
139 fn visit_map<A>(mut self, mut map: A) -> Result<Self::Value, A::Error>
140 where
141 A: MapAccess<'de>,
142 {
143 while let Some(key) = map.next_key::<String>()? {
144 match key.as_str() {
145 "id" => {
146 let id = map.next_value::<String>()?;
147 match id.parse() {
148 Ok(id) => self.data.id = id,
149 Err(..) => {}
150 }
151 }
152 "tags" => self.data.tags = map.next_value()?,
153 "title" => self.data.title = map.next_value()?,
154 "description" => self.data.description = map.next_value()?,
155 "width" => self.data.width = map.next_value()?,
156 "height" => self.data.height = map.next_value()?,
157 unknown => {
158 let value = map.next_value::<Value>()?;
159 self.data.unknown_fields.insert(key, value);
160 }
161 }
162 }
163 Ok(())
164 }
165}
166
167
168#[derive(Debug, Serialize, Deserialize)]
169pub struct SearchTagPage {
170 #[serde(rename = "illust")]
171 pub illust: Illust,
172 pub popular: Popular,
173 #[serde(rename = "relatedTags")]
174 pub related_tags: Vec<String>,
175 #[serde(rename = "tagTranslation")]
176 pub tag_translation: Value,
177 #[serde(rename = "zoneConfig")]
178 pub zone_config: ZoneConfig,
179 #[serde(rename = "extraData")]
180 pub extra_data: ExtraData,
181}
182
183#[derive(Debug, Serialize, Deserialize)]
184pub struct Illust {
185 pub data: Vec<IllustData>,
186 pub total: u64,
187 #[serde(rename = "lastPage")]
188 pub last_page: u32,
189 #[serde(rename = "bookmarkRanges")]
190 pub bookmark_ranges: Vec<Struct1>,
191}
192
193impl SearchTagPage {
194 pub async fn count_pages(&self) -> Result<u32, PixivError> {
195 Ok(self.illust.last_page)
196 }
197}
198
199pub struct PixivClient {
200 pub rng: RefCell<ThreadRng>,
201 pub root: PathBuf,
202 pub agents: Vec<String>,
203 pub cookie: String,
204 pub wait: Range<f32>,
205}
206
207impl PixivClient {
208 pub fn user_agent(&self) -> &str {
209 self.agents.choose(self.rng.borrow_mut().deref_mut()).unwrap()
210 }
211 pub fn cooldown(&self) -> Sleep {
212 let time = self.rng.borrow_mut().gen_range(self.wait.clone());
213 tokio::time::sleep(Duration::from_secs_f32(time))
214 }
215}
216
217pub struct PixivArtwork {
218 pub id: u64,
219}
220
221impl PixivArtwork {
222 pub async fn download_original(&self, client: &PixivClient) -> Result<usize, PixivError> {
223 if self.get_skip_mark(&client.root) {
224 println!("Skip downloading artwork {}", self.id);
225 return Ok(0);
226 }
227 let data = self.get_image_urls(&client.cookie, client.user_agent()).await?;
228 client.cooldown().await;
230 let download_tasks = data.iter().cloned().map(|image| {
232 let path = client.root.clone();
233 tokio::task::spawn(async move { image.download_original(&path).await })
234 });
235 let tasks = futures::future::join_all(download_tasks).await;
236 for task in tasks {
237 task??;
238 }
239 self.set_skip_mark(&client.root).await?;
240 println!("Downloaded artwork {}", self.id);
241 Ok(data.len())
242 }
243
244 pub async fn get_image_urls(&self, cookie: &str, agent: &str) -> Result<Vec<PixivImage>, PixivError> {
245 let url = format!("https://www.pixiv.net/ajax/illust/{0}/pages?lang=zh", self.id);
246 let client = Client::new();
247 let response = client.get(url).header(USER_AGENT, agent).header(COOKIE, cookie).send().await?;
248 let json_data: PixivResponse<Vec<PixivImage>> = response.json().await?;
249 json_data.throw(format!("PixivArtwork::get_image_urls({})", self.id))
250 }
251
252 pub fn get_skip_mark(&self, folder: &Path) -> bool {
253 let path = folder.join("skip").join(self.id.to_string());
254 path.exists()
255 }
256 pub async fn set_skip_mark(&self, folder: &Path) -> Result<PathBuf, PixivError> {
257 let path = folder.join("skip");
258 if !path.exists() {
259 std::fs::create_dir_all(&path)?;
260 }
261 File::create(path.join(self.id.to_string())).await?;
262 Ok(path)
263 }
264}