novel_api/common/
client.rs

1use std::fmt::{self, Display};
2use std::ops::{Range, RangeFrom, RangeTo};
3use std::path::PathBuf;
4
5use chrono::NaiveDateTime;
6use image::DynamicImage;
7use url::Url;
8
9use crate::Error;
10
11/// Logged-in user information
12#[must_use]
13#[derive(Debug)]
14pub struct UserInfo {
15    /// User's nickname
16    pub nickname: String,
17    /// User's avatar
18    pub avatar: Option<Url>,
19}
20
21/// Novel information
22#[must_use]
23#[derive(Debug, Default)]
24pub struct NovelInfo {
25    /// Novel id
26    pub id: u32,
27    /// Novel name
28    pub name: String,
29    /// Author name
30    pub author_name: String,
31    /// Url of the novel cover
32    pub cover_url: Option<Url>,
33    /// Novel introduction
34    pub introduction: Option<Vec<String>>,
35    /// Novel word count
36    pub word_count: Option<u32>,
37    /// Is the novel a VIP
38    pub is_vip: Option<bool>,
39    /// Is the novel finished
40    pub is_finished: Option<bool>,
41    /// Novel creation time
42    pub create_time: Option<NaiveDateTime>,
43    /// Novel last update time
44    pub update_time: Option<NaiveDateTime>,
45    /// Novel category
46    pub category: Option<Category>,
47    /// Novel tags
48    pub tags: Option<Vec<Tag>>,
49}
50
51impl PartialEq for NovelInfo {
52    fn eq(&self, other: &Self) -> bool {
53        self.id == other.id
54    }
55}
56
57/// Novel category
58#[must_use]
59#[derive(Debug, Clone, PartialEq)]
60pub struct Category {
61    /// Category id
62    pub id: Option<u16>,
63    /// Parent category id
64    pub parent_id: Option<u16>,
65    /// Category name
66    pub name: String,
67}
68
69impl Display for Category {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        write!(f, "{}", self.name)
72    }
73}
74
75/// Novel tag
76#[must_use]
77#[derive(Debug, Clone, PartialEq)]
78pub struct Tag {
79    /// Tag id
80    pub id: Option<u16>,
81    /// Tag name
82    pub name: String,
83}
84
85impl Display for Tag {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        write!(f, "{}", self.name)
88    }
89}
90
91/// Volume information
92pub type VolumeInfos = Vec<VolumeInfo>;
93
94/// Volume information
95#[must_use]
96#[derive(Debug)]
97pub struct VolumeInfo {
98    /// Volume id
99    pub id: u32,
100    /// Volume title
101    pub title: String,
102    /// Chapter information
103    pub chapter_infos: Vec<ChapterInfo>,
104}
105
106/// Chapter information
107#[must_use]
108#[derive(Debug, Default, Clone)]
109pub struct ChapterInfo {
110    /// Novel id
111    pub novel_id: Option<u32>,
112    /// Chapter id
113    pub id: u32,
114    /// Chapter title
115    pub title: String,
116    /// Whether this chapter can only be read by VIP users
117    pub is_vip: Option<bool>,
118    /// Chapter price
119    pub price: Option<u16>,
120    /// Is the chapter accessible
121    pub payment_required: Option<bool>,
122    /// Is the chapter valid
123    pub is_valid: Option<bool>,
124    /// Word count
125    pub word_count: Option<u32>,
126    /// Chapter creation time
127    pub create_time: Option<NaiveDateTime>,
128    /// Chapter last update time
129    pub update_time: Option<NaiveDateTime>,
130}
131
132impl ChapterInfo {
133    /// Is this chapter available
134    pub fn payment_required(&self) -> bool {
135        self.payment_required.as_ref().is_some_and(|x| *x)
136    }
137
138    /// Is this chapter valid
139    pub fn is_valid(&self) -> bool {
140        self.is_valid.as_ref().is_none_or(|x| *x)
141    }
142
143    /// Is this chapter available for download
144    pub fn can_download(&self) -> bool {
145        !self.payment_required() && self.is_valid()
146    }
147}
148
149/// Content information
150pub type ContentInfos = Vec<ContentInfo>;
151
152/// Content information
153#[must_use]
154#[derive(Debug)]
155pub enum ContentInfo {
156    /// Text content
157    Text(String),
158    /// Image content
159    Image(Url),
160}
161
162/// Options used by the search
163#[derive(Debug, Default)]
164pub struct Options {
165    /// Keyword
166    pub keyword: Option<String>,
167    /// Is it finished
168    pub is_finished: Option<bool>,
169    /// Whether this chapter can only be read by VIP users
170    pub is_vip: Option<bool>,
171    /// Category
172    pub category: Option<Category>,
173    /// Included tags
174    pub tags: Option<Vec<Tag>>,
175    /// Excluded tags
176    pub excluded_tags: Option<Vec<Tag>>,
177    /// The number of days since the last update
178    pub update_days: Option<u8>,
179    /// Word count
180    pub word_count: Option<WordCountRange>,
181}
182
183/// Word count range
184#[derive(Debug)]
185pub enum WordCountRange {
186    /// Set minimum and maximum word count
187    Range(Range<u32>),
188    /// Set minimum word count
189    RangeFrom(RangeFrom<u32>),
190    /// Set maximum word count
191    RangeTo(RangeTo<u32>),
192}
193
194#[derive(Debug)]
195pub enum Comment {
196    Short(ShortComment),
197    Long(LongComment),
198}
199
200#[derive(Debug)]
201pub struct ShortComment {
202    pub id: u32,
203    pub user: UserInfo,
204    pub content: Vec<String>,
205    pub create_time: Option<NaiveDateTime>,
206    pub like_count: Option<u16>,
207    pub replies: Option<Vec<ShortComment>>,
208}
209
210impl PartialEq for ShortComment {
211    fn eq(&self, other: &Self) -> bool {
212        self.id == other.id
213    }
214}
215
216#[derive(Debug)]
217pub struct LongComment {
218    pub id: u32,
219    pub user: UserInfo,
220    pub title: String,
221    pub content: Vec<String>,
222    pub create_time: Option<NaiveDateTime>,
223    pub like_count: Option<u16>,
224    pub replies: Option<Vec<ShortComment>>,
225}
226
227impl PartialEq for LongComment {
228    fn eq(&self, other: &Self) -> bool {
229        self.id == other.id
230    }
231}
232
233#[derive(Clone, Copy)]
234pub enum CommentType {
235    Short,
236    Long,
237}
238
239/// Traits that abstract client behavior
240#[trait_variant::make(Send)]
241pub trait Client {
242    /// set proxy
243    fn proxy(&mut self, proxy: Url);
244
245    /// Do not use proxy (environment variables used to set proxy are ignored)
246    fn no_proxy(&mut self);
247
248    /// Set the certificate path for use with packet capture tools
249    fn cert(&mut self, cert_path: PathBuf);
250
251    /// Stop the client, save the data
252    async fn shutdown(&self) -> Result<(), Error>;
253
254    /// Add cookie
255    async fn add_cookie(&self, cookie_str: &str, url: &Url) -> Result<(), Error>;
256
257    /// Login in
258    async fn log_in(&self, username: String, password: Option<String>) -> Result<(), Error>;
259
260    /// Check if you are logged in
261    async fn logged_in(&self) -> Result<bool, Error>;
262
263    /// Get the information of the logged-in user
264    async fn user_info(&self) -> Result<UserInfo, Error>;
265
266    /// Get user's existing money
267    async fn money(&self) -> Result<u32, Error>;
268
269    /// Sign in
270    async fn sign_in(&self) -> Result<(), Error>;
271
272    /// Get the favorite novel of the logged-in user and return the novel id
273    async fn bookshelf_infos(&self) -> Result<Vec<u32>, Error>;
274
275    /// Get novel Information
276    async fn novel_info(&self, id: u32) -> Result<Option<NovelInfo>, Error>;
277
278    /// Get comments of the novel
279    async fn comments(
280        &self,
281        id: u32,
282        comment_type: CommentType,
283        need_replies: bool,
284        page: u16,
285        size: u16,
286    ) -> Result<Option<Vec<Comment>>, Error>;
287
288    /// Get volume Information
289    async fn volume_infos(&self, id: u32) -> Result<Option<VolumeInfos>, Error>;
290
291    /// Get content Information
292    async fn content_infos(&self, info: &ChapterInfo) -> Result<ContentInfos, Error>;
293
294    /// Get multiple content Information
295    async fn content_infos_multiple(
296        &self,
297        infos: &[ChapterInfo],
298    ) -> Result<Vec<ContentInfos>, Error>;
299
300    /// Order chapter
301    async fn order_chapter(&self, info: &ChapterInfo) -> Result<(), Error>;
302
303    /// Order the whole novel
304    async fn order_novel(&self, id: u32, infos: &VolumeInfos) -> Result<(), Error>;
305
306    /// Download image
307    async fn image(&self, url: &Url) -> Result<DynamicImage, Error>;
308
309    /// Get all categories
310    async fn categories(&self) -> Result<&Vec<Category>, Error>;
311
312    /// Get all tags
313    async fn tags(&self) -> Result<&Vec<Tag>, Error>;
314
315    /// Search all matching novels
316    async fn search_infos(
317        &self,
318        option: &Options,
319        page: u16,
320        size: u16,
321    ) -> Result<Option<Vec<u32>>, Error>;
322
323    /// Does the app have comment in this type
324    fn has_this_type_of_comments(comment_type: CommentType) -> bool;
325}
326
327mod tests {
328    #[test]
329    fn test_payment_required() {
330        use crate::ChapterInfo;
331
332        assert!(
333            ChapterInfo {
334                payment_required: Some(true),
335                ..Default::default()
336            }
337            .payment_required()
338        );
339
340        assert!(
341            !ChapterInfo {
342                payment_required: Some(false),
343                ..Default::default()
344            }
345            .payment_required()
346        );
347
348        assert!(
349            !ChapterInfo {
350                payment_required: None,
351                ..Default::default()
352            }
353            .payment_required()
354        );
355    }
356
357    #[test]
358    fn test_is_valid() {
359        use crate::ChapterInfo;
360
361        assert!(
362            ChapterInfo {
363                is_valid: Some(true),
364                ..Default::default()
365            }
366            .is_valid()
367        );
368
369        assert!(
370            !ChapterInfo {
371                is_valid: Some(false),
372                ..Default::default()
373            }
374            .is_valid()
375        );
376
377        assert!(
378            ChapterInfo {
379                is_valid: None,
380                ..Default::default()
381            }
382            .is_valid()
383        );
384    }
385}