novel_api/common/
client.rs

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