tosho_mplus/proto/
chapters.rs

1//! A module containing information related to manga chapter.
2//!
3//! If something is missing, please [open an issue](https://github.com/noaione/tosho-mango/issues/new/choose) or a [pull request](https://github.com/noaione/tosho-mango/compare).
4
5use std::str::FromStr;
6
7use tosho_macros::AutoGetter;
8
9use crate::helper::SubscriptionPlan;
10
11use super::ChapterPosition;
12
13/// A single chapter information
14#[derive(Clone, AutoGetter, PartialEq, ::prost::Message)]
15pub struct Chapter {
16    /// Title ID
17    #[prost(uint64, tag = "1")]
18    title_id: u64,
19    /// Chapter ID
20    #[prost(uint64, tag = "2")]
21    chapter_id: u64,
22    /// Chapter title
23    #[prost(string, tag = "3")]
24    title: ::prost::alloc::string::String,
25    /// Chapter subtitle
26    #[prost(string, optional, tag = "4")]
27    #[skip_field]
28    subtitle: ::core::option::Option<::prost::alloc::string::String>,
29    /// Chapter thumbnail URL
30    #[prost(string, tag = "5")]
31    thumbnail: ::prost::alloc::string::String,
32    /// Chapter published/start UNIX timestamp
33    #[prost(int64, tag = "6")]
34    published_at: i64,
35    /// Chapter end viewing period UNIX timestamp
36    #[prost(int64, optional, tag = "7")]
37    #[skip_field]
38    end_at: ::core::option::Option<i64>,
39    /// Is the chapter already viewed?
40    #[prost(bool, tag = "8")]
41    viewed: bool,
42    /// Is the chapter can be read in vertical mode only?
43    #[prost(bool, tag = "9")]
44    vertical_only: bool,
45    /// Chapter end viewing by ticket timestamp
46    #[prost(int64, optional, tag = "10")]
47    #[skip_field]
48    ticket_end_at: ::core::option::Option<i64>,
49    /// Is the chapter can be read for free?
50    #[prost(bool, tag = "11")]
51    free: bool,
52    /// Is the chapter can be read in horizontal mode only?
53    #[prost(bool, tag = "12")]
54    horizontal_only: bool,
55    /// Chapter view count
56    #[prost(uint64, tag = "13")]
57    view_count: u64,
58    /// Chapter comment count
59    #[prost(uint64, tag = "14")]
60    comment_count: u64,
61    /// Chapter position in the group
62    ///
63    /// This is assigned client side.
64    #[prost(enumeration = "super::ChapterPosition", tag = "999")]
65    #[skip_field]
66    position: i32,
67}
68
69impl Chapter {
70    /// Can this chapter be read for free?
71    pub fn is_free(&self) -> bool {
72        if self.free {
73            return true;
74        }
75
76        if self.position() == ChapterPosition::First {
77            return true;
78        }
79
80        if self.position() != ChapterPosition::Middle {
81            if let Some(end_at) = self.end_at {
82                let current_time = chrono::Utc::now().timestamp();
83                current_time < end_at
84            } else {
85                false
86            }
87        } else {
88            false
89        }
90    }
91
92    /// Can this chapter be read with ticket?
93    pub fn is_ticketed(&self) -> bool {
94        if let Some(ticket_end_at) = self.ticket_end_at {
95            let current_time = chrono::Utc::now().timestamp();
96            current_time < ticket_end_at
97        } else {
98            false
99        }
100    }
101
102    /// Get the default viewing mode
103    pub fn default_view_mode(&self) -> &'static str {
104        if self.vertical_only {
105            "vertical"
106        } else {
107            "horizontal"
108        }
109    }
110
111    /// Format the chapter title and subtitle into a single string.
112    ///
113    /// If the subtitle is [`None`], the title will be returned as is.
114    pub fn as_chapter_title(&self) -> String {
115        let base_title = self.title.clone();
116        if let Some(subtitle) = self.subtitle.clone() {
117            format!("{base_title} — {subtitle}")
118        } else {
119            base_title
120        }
121    }
122}
123
124/// A group of chapters
125#[derive(Clone, AutoGetter, PartialEq, ::prost::Message)]
126pub struct ChapterGroup {
127    /// The chapter numbers range
128    #[prost(string, tag = "1")]
129    chapters: ::prost::alloc::string::String,
130    /// The first chapters list, all of them should be free to read
131    #[prost(message, repeated, tag = "2")]
132    first_chapters: ::prost::alloc::vec::Vec<Chapter>,
133    /// The mid chapters list, this chapter is locked behind subscriptions or tickets
134    #[prost(message, repeated, tag = "3")]
135    mid_chapters: ::prost::alloc::vec::Vec<Chapter>,
136    /// The last chapters list, all of them should be free to read
137    #[prost(message, repeated, tag = "4")]
138    last_chapters: ::prost::alloc::vec::Vec<Chapter>,
139}
140
141impl ChapterGroup {
142    /// Group the chapters into a single list
143    pub fn flatten(&self) -> Vec<Chapter> {
144        let mut chapters = Vec::new();
145        chapters.extend_from_slice(&self.first_chapters);
146        chapters.extend_from_slice(&self.mid_chapters);
147        chapters.extend_from_slice(&self.last_chapters);
148        chapters
149    }
150
151    /// Get the mutable reference to first chapters
152    pub fn first_chapters_mut(&mut self) -> &mut Vec<Chapter> {
153        &mut self.first_chapters
154    }
155
156    /// Get the mutable reference to mid chapters
157    pub fn mid_chapters_mut(&mut self) -> &mut Vec<Chapter> {
158        &mut self.mid_chapters
159    }
160
161    /// Get the mutable reference to last chapters
162    pub fn last_chapters_mut(&mut self) -> &mut Vec<Chapter> {
163        &mut self.last_chapters
164    }
165}
166
167/// A page of a chapter
168#[derive(Clone, AutoGetter, PartialEq, ::prost::Message)]
169pub struct ChapterPage {
170    /// The page url
171    #[prost(string, tag = "1")]
172    url: ::prost::alloc::string::String,
173    /// The image width
174    #[prost(uint64, tag = "2")]
175    width: u64,
176    /// The image height
177    #[prost(uint64, tag = "3")]
178    height: u64,
179    /// The image type/kind
180    #[prost(enumeration = "super::PageType", tag = "4")]
181    #[skip_field]
182    kind: i32,
183    /// The image encryption key
184    #[prost(string, optional, tag = "5")]
185    #[skip_field]
186    key: ::core::option::Option<::prost::alloc::string::String>,
187}
188
189impl ChapterPage {
190    /// The file name of the image.
191    ///
192    /// When you have the URL of `https://example.com/image.webp?ignore=me`,
193    /// the filename would become `image.webp` including the extension.
194    pub fn file_name(&self) -> String {
195        let url = self.url.clone();
196        // split at the last slash
197        let split: Vec<&str> = url.rsplitn(2, '/').collect();
198        // Remove extra URL parameters
199        let file_name: Vec<&str> = split[0].split('?').collect();
200        file_name[0].to_string()
201    }
202
203    /// The file extension of the image.
204    ///
205    /// When you have the URL of `https://example.com/image.webp?ignore=me`,
206    /// the extension would become `webp`, when there is no extension it
207    /// would return an empty string.
208    pub fn extension(&self) -> String {
209        let file_name = self.file_name();
210        // split at the last dot
211        let split: Vec<&str> = file_name.rsplitn(2, '.').collect();
212
213        if split.len() == 2 {
214            split[0].to_string()
215        } else {
216            "".to_string()
217        }
218    }
219
220    /// The file stem of the image.
221    ///
222    /// When you have the URL of `https://example.com/image.webp?ignore=me`,
223    /// the file stem would become `image`.
224    pub fn file_stem(&self) -> String {
225        let file_name = self.file_name();
226        // split at the last dot
227        let split: Vec<&str> = file_name.rsplitn(2, '.').collect();
228
229        if split.len() == 2 {
230            split[1].to_string()
231        } else {
232            file_name
233        }
234    }
235}
236
237/// A chapter page of a banners
238#[derive(Clone, AutoGetter, PartialEq, ::prost::Message)]
239pub struct ChapterPageBanner {
240    /// Banner title
241    #[prost(string, optional, tag = "1")]
242    #[skip_field]
243    title: ::core::option::Option<::prost::alloc::string::String>,
244    /// Banner list
245    #[prost(message, repeated, tag = "2")]
246    banners: ::prost::alloc::vec::Vec<super::common::Banner>,
247}
248
249/// A chapter last page response
250#[derive(Clone, AutoGetter, PartialEq, ::prost::Message)]
251pub struct ChapterPageLastPage {
252    /// Current chapter
253    #[prost(message, optional, tag = "1")]
254    chapter: ::core::option::Option<Chapter>,
255    /// Next chapter
256    #[prost(message, optional, tag = "2")]
257    next_chapter: ::core::option::Option<Chapter>,
258    /// Top comments of this chapter
259    #[prost(message, repeated, tag = "3")]
260    top_comments: ::prost::alloc::vec::Vec<super::comments::Comment>,
261    /// Is the user subscribed
262    #[prost(bool, tag = "4")]
263    subscribed: bool,
264    /// The next chapter timestamp
265    #[prost(int64, optional, tag = "5")]
266    #[skip_field]
267    next_chapter_at: ::core::option::Option<i64>,
268    /// The chapter type
269    #[prost(enumeration = "super::ChapterType", tag = "6")]
270    #[skip_field]
271    chapter_type: i32,
272    /// Movie reward of the chapter
273    // #[prost(message, optional, tag = "8")]
274    // movie_reward: ::core::option::Option<super::common::PopupMessage>,
275    /// Banner list
276    #[prost(message, optional, tag = "9")]
277    banner: ::core::option::Option<super::common::Banner>,
278    /// Title ticket list
279    #[prost(message, repeated, tag = "10")]
280    title_tickets: ::prost::alloc::vec::Vec<super::titles::Title>,
281    /// Publisher banner
282    #[prost(message, optional, tag = "11")]
283    publisher_banner: ::core::option::Option<super::common::Banner>,
284    /// User tickets
285    #[prost(message, optional, tag = "12")]
286    #[copyable]
287    user_tickets: ::core::option::Option<super::accounts::UserTickets>,
288    /// Is next chapter can be read by ticket?
289    #[prost(bool, tag = "13")]
290    next_chapter_ticket: bool,
291    /// Is next chapter can be read for free one time only?
292    #[prost(bool, tag = "14")]
293    next_chapter_free: bool,
294    /// Is next chapter can be read only with subscription?
295    #[prost(bool, tag = "16")]
296    next_chapter_subscription: bool,
297}
298
299/// A chapter page response
300#[derive(Clone, AutoGetter, PartialEq, ::prost::Message)]
301pub struct ChapterPageResponse {
302    /// A response to a chapter page (a.k.a the manga page)
303    #[prost(message, optional, tag = "1")]
304    page: ::core::option::Option<ChapterPage>,
305    /// A response to a banner page
306    #[prost(message, optional, tag = "2")]
307    banner: ::core::option::Option<ChapterPageBanner>,
308    /// A response to a last page
309    #[prost(message, optional, tag = "3")]
310    last_page: ::core::option::Option<ChapterPageLastPage>,
311    /// A response to an insert banner
312    #[prost(message, optional, tag = "5")]
313    insert_banner: ::core::option::Option<ChapterPageBanner>,
314}
315
316/// A chapter viewer response
317#[derive(Clone, AutoGetter, PartialEq, ::prost::Message)]
318pub struct ChapterViewer {
319    /// Chapter pages
320    #[prost(message, repeated, tag = "1")]
321    pages: ::prost::alloc::vec::Vec<ChapterPageResponse>,
322    /// Chapter ID
323    #[prost(uint64, tag = "2")]
324    chapter_id: u64,
325    /// All available chapters
326    #[prost(message, repeated, tag = "3")]
327    chapters: ::prost::alloc::vec::Vec<Chapter>,
328    // SNS: 4
329    /// Manga title
330    #[prost(string, tag = "5")]
331    title: ::prost::alloc::string::String,
332    /// Chapter title
333    #[prost(string, tag = "6")]
334    chapter_title: ::prost::alloc::string::String,
335    /// Number of comments
336    #[prost(uint64, tag = "7")]
337    comment_count: u64,
338    /// Is vertical only?
339    #[prost(bool, tag = "8")]
340    vertical_only: bool,
341    /// Title ID
342    #[prost(uint64, tag = "9")]
343    title_id: u64,
344    /// Is the first page on the right side (first page is odd number)
345    #[prost(bool, tag = "10")]
346    first_page_right: bool,
347    /// Region code of the title
348    #[prost(string, tag = "11")]
349    region_code: ::prost::alloc::string::String,
350    /// Is horizontal only?
351    #[prost(bool, tag = "12")]
352    horizontal_only: bool,
353    /// User subscription info
354    #[prost(message, optional, tag = "13")]
355    user_subscription: ::core::option::Option<super::accounts::UserSubscription>,
356    /// User plan type
357    #[prost(string, tag = "14")]
358    #[skip_field]
359    plan_type: ::prost::alloc::string::String,
360}
361
362impl ChapterViewer {
363    /// Get the actual subscriptions plan type
364    ///
365    /// This will return the actual [`SubscriptionPlan`] type
366    /// and fallback to [`SubscriptionPlan::Basic`] if the plan is not recognized.
367    pub fn plan_type(&self) -> SubscriptionPlan {
368        match SubscriptionPlan::from_str(&self.plan_type) {
369            Ok(plan) => plan,
370            Err(_) => SubscriptionPlan::Basic,
371        }
372    }
373}