ibdl_common/post/
mod.rs

1//! Main representation of an imageboard post
2//!
3//! # Post
4//! A [`Post` struct](Post) is a generic representation of an imageboard post.
5//!
6//! Most imageboard APIs have a common set of info from the files we want to download.
7use reqwest::Client;
8use serde::{Deserialize, Serialize};
9
10use std::{cmp::Ordering, fmt::Debug, ops::Not};
11
12use crate::ImageBoards;
13
14use self::{extension::Extension, rating::Rating, tags::Tag};
15
16pub mod error;
17pub mod extension;
18pub mod rating;
19pub mod tags;
20
21/// Special enum to simplify the selection of the output file name when downloading a [`Post`]
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23pub enum NameType {
24    ID,
25    MD5,
26}
27
28impl Not for NameType {
29    type Output = Self;
30
31    fn not(self) -> Self::Output {
32        match self {
33            Self::ID => Self::MD5,
34            Self::MD5 => Self::ID,
35        }
36    }
37}
38
39/// Queue that combines all posts collected, with which tags and with a user-defined blacklist in case an Extractor implements [Auth](ibdl-extractors::websites::Auth).
40#[derive(Debug)]
41pub struct PostQueue {
42    /// The imageboard where the posts come from.
43    pub imageboard: ImageBoards,
44    /// The internal `Client` used by the extractor.
45    pub client: Client,
46    /// A list containing all `Post`s collected.
47    pub posts: Vec<Post>,
48    /// The tags used to search the collected posts.
49    pub tags: Vec<String>,
50}
51
52impl PostQueue {
53    pub fn prepare(&mut self, limit: Option<u16>) {
54        if let Some(max) = limit {
55            self.posts.truncate(max as usize);
56        } else {
57            self.posts.shrink_to_fit()
58        }
59    }
60}
61
62/// Catchall model for the necessary parts of the imageboard post to properly identify, download and save it.
63#[derive(Clone, Serialize, Deserialize, Eq)]
64pub struct Post {
65    /// ID number of the post given by the imageboard
66    pub id: u64,
67    /// The imageboard where this post was extracted from
68    pub website: ImageBoards,
69    /// Direct URL of the original image file located inside the imageboard's server
70    pub url: String,
71    /// Instead of calculating the downloaded file's MD5 hash on the fly, it uses the one provided by the API.
72    pub md5: String,
73    /// The original file extension provided by the imageboard.
74    ///
75    /// ```https://konachan.com``` (Moebooru) and some other imageboards don't provide this field. So, additional work is required to get the file extension from the url
76    pub extension: Extension,
77    /// Rating of the post. Can be:
78    ///
79    /// * `Rating::Safe` for SFW posts
80    /// * `Rating::Questionable` for a not necessarily SFW post
81    /// * `Rating::Explicit` for NSFW posts
82    /// * `Rating::Unknown` in case none of the above are correctly parsed
83    pub rating: Rating,
84    /// Set of tags associated with the post.
85    ///
86    /// Used to exclude posts according to a blacklist
87    pub tags: Vec<Tag>,
88}
89
90impl Debug for Post {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        f.debug_struct("Post")
93            .field("Post ID", &self.id)
94            .field("Website", &self.website)
95            .field("Download URL", &self.url)
96            .field("MD5 Hash", &self.md5)
97            .field("File Extension", &self.extension)
98            .field("Rating", &self.rating)
99            .field("Tag List", &self.tags)
100            .finish()
101    }
102}
103
104impl Ord for Post {
105    fn cmp(&self, other: &Self) -> Ordering {
106        self.id.cmp(&other.id)
107    }
108}
109
110impl PartialOrd for Post {
111    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
112        Some(self.cmp(other))
113    }
114}
115
116impl PartialEq for Post {
117    fn eq(&self, other: &Self) -> bool {
118        self.id == other.id
119    }
120}
121
122impl Post {
123    /// Get the final file name of the post for saving.
124    #[inline]
125    pub fn file_name(&self, name_type: NameType) -> String {
126        let name = match name_type {
127            NameType::ID => self.id.to_string(),
128            NameType::MD5 => self.md5.to_string(),
129        };
130
131        format!("{}.{}", name, self.extension)
132    }
133
134    /// Get the generic name of the post. Can be it's MD5 hash or ID
135    #[inline]
136    pub fn name(&self, name_type: NameType) -> String {
137        match name_type {
138            NameType::ID => self.id.to_string(),
139            NameType::MD5 => self.md5.to_string(),
140        }
141    }
142
143    #[inline]
144    pub fn seq_file_name(&self, num_digits: usize) -> String {
145        format!("{:0num_digits$}.{}", self.id, self.extension)
146    }
147}