pixiv_api/artworks/
mod.rs

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        // WAIT!!! error-429 here!
229        client.cooldown().await;
230        // TODO: no limit but need retry
231        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}