novel-api 0.19.1

Novel APIs from various sources
Documentation
use std::fmt::{self, Display};
use std::ops::{Range, RangeFrom, RangeTo};
use std::path::PathBuf;

use chrono::NaiveDateTime;
use image::DynamicImage;
use url::Url;

use crate::Error;

/// Logged-in user information
#[must_use]
#[derive(Debug)]
pub struct UserInfo {
    /// User's nickname
    pub nickname: String,
    /// User's avatar
    pub avatar: Option<Url>,
}

/// Novel information
#[must_use]
#[derive(Debug, Default)]
pub struct NovelInfo {
    /// Novel id
    pub id: u32,
    /// Novel name
    pub name: String,
    /// Author name
    pub author_name: String,
    /// Url of the novel cover
    pub cover_url: Option<Url>,
    /// Novel introduction
    pub introduction: Option<Vec<String>>,
    /// Novel word count
    pub word_count: Option<u32>,
    /// Is the novel a VIP
    pub is_vip: Option<bool>,
    /// Is the novel finished
    pub is_finished: Option<bool>,
    /// Novel creation time
    pub create_time: Option<NaiveDateTime>,
    /// Novel last update time
    pub update_time: Option<NaiveDateTime>,
    /// Novel category
    pub category: Option<Category>,
    /// Novel tags
    pub tags: Option<Vec<Tag>>,
}

impl PartialEq for NovelInfo {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
    }
}

/// Novel category
#[must_use]
#[derive(Debug, Clone, PartialEq)]
pub struct Category {
    /// Category id
    pub id: Option<u16>,
    /// Parent category id
    pub parent_id: Option<u16>,
    /// Category name
    pub name: String,
}

impl Display for Category {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.name)
    }
}

/// Novel tag
#[must_use]
#[derive(Debug, Clone, PartialEq)]
pub struct Tag {
    /// Tag id
    pub id: Option<u16>,
    /// Tag name
    pub name: String,
}

impl Display for Tag {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.name)
    }
}

/// Volume information
pub type VolumeInfos = Vec<VolumeInfo>;

/// Volume information
#[must_use]
#[derive(Debug)]
pub struct VolumeInfo {
    /// Volume id
    pub id: u32,
    /// Volume title
    pub title: String,
    /// Chapter information
    pub chapter_infos: Vec<ChapterInfo>,
}

/// Chapter information
#[must_use]
#[derive(Debug, Default, Clone)]
pub struct ChapterInfo {
    /// Novel id
    pub novel_id: Option<u32>,
    /// Chapter id
    pub id: u32,
    /// Chapter title
    pub title: String,
    /// Whether this chapter can only be read by VIP users
    pub is_vip: Option<bool>,
    /// Chapter price
    pub price: Option<u16>,
    /// Is the chapter accessible
    pub payment_required: Option<bool>,
    /// Is the chapter valid
    pub is_valid: Option<bool>,
    /// Word count
    pub word_count: Option<u32>,
    /// Chapter creation time
    pub create_time: Option<NaiveDateTime>,
    /// Chapter last update time
    pub update_time: Option<NaiveDateTime>,
}

impl ChapterInfo {
    /// Is this chapter available
    pub fn payment_required(&self) -> bool {
        self.payment_required.as_ref().is_some_and(|x| *x)
    }

    /// Is this chapter valid
    pub fn is_valid(&self) -> bool {
        self.is_valid.as_ref().is_none_or(|x| *x)
    }

    /// Is this chapter available for download
    pub fn can_download(&self) -> bool {
        !self.payment_required() && self.is_valid()
    }
}

/// Content information
pub type ContentInfos = Vec<ContentInfo>;

/// Content information
#[must_use]
#[derive(Debug)]
pub enum ContentInfo {
    /// Text content
    Text(String),
    /// Image content
    Image(Url),
}

/// Options used by the search
#[derive(Debug, Default)]
pub struct Options {
    /// Keyword
    pub keyword: Option<String>,
    /// Is it finished
    pub is_finished: Option<bool>,
    /// Whether this chapter can only be read by VIP users
    pub is_vip: Option<bool>,
    /// Category
    pub category: Option<Category>,
    /// Included tags
    pub tags: Option<Vec<Tag>>,
    /// Excluded tags
    pub excluded_tags: Option<Vec<Tag>>,
    /// The number of days since the last update
    pub update_days: Option<u8>,
    /// Word count
    pub word_count: Option<WordCountRange>,
}

/// Word count range
#[derive(Debug)]
pub enum WordCountRange {
    /// Set minimum and maximum word count
    Range(Range<u32>),
    /// Set minimum word count
    RangeFrom(RangeFrom<u32>),
    /// Set maximum word count
    RangeTo(RangeTo<u32>),
}

#[derive(Debug)]
pub enum Comment {
    Short(ShortComment),
    Long(LongComment),
}

#[derive(Debug)]
pub struct ShortComment {
    pub id: u32,
    pub user: UserInfo,
    pub content: Vec<String>,
    pub create_time: Option<NaiveDateTime>,
    pub like_count: Option<u16>,
    pub replies: Option<Vec<ShortComment>>,
}

impl PartialEq for ShortComment {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
    }
}

#[derive(Debug)]
pub struct LongComment {
    pub id: u32,
    pub user: UserInfo,
    pub title: String,
    pub content: Vec<String>,
    pub create_time: Option<NaiveDateTime>,
    pub like_count: Option<u16>,
    pub replies: Option<Vec<ShortComment>>,
}

impl PartialEq for LongComment {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
    }
}

#[derive(Clone, Copy)]
pub enum CommentType {
    Short,
    Long,
}

/// Traits that abstract client behavior
#[trait_variant::make(Send)]
pub trait Client {
    /// set proxy
    fn proxy(&mut self, proxy: Url);

    /// Do not use proxy (environment variables used to set proxy are ignored)
    fn no_proxy(&mut self);

    /// Set the certificate path for use with packet capture tools
    fn cert(&mut self, cert_path: PathBuf);

    /// Stop the client, save the data
    async fn shutdown(&self) -> Result<(), Error>;

    /// Add cookie
    async fn add_cookie(&self, cookie_str: &str, url: &Url) -> Result<(), Error>;

    /// Login in
    async fn log_in(&self, username: String, password: Option<String>) -> Result<(), Error>;

    /// Check if you are logged in
    async fn logged_in(&self) -> Result<bool, Error>;

    /// Get the information of the logged-in user
    async fn user_info(&self) -> Result<UserInfo, Error>;

    /// Get user's existing money
    async fn money(&self) -> Result<u32, Error>;

    /// Sign in
    async fn sign_in(&self) -> Result<(), Error>;

    /// Get the favorite novel of the logged-in user and return the novel id
    async fn bookshelf_infos(&self) -> Result<Vec<u32>, Error>;

    /// Get novel Information
    async fn novel_info(&self, id: u32) -> Result<Option<NovelInfo>, Error>;

    /// Get comments of the novel
    async fn comments(
        &self,
        id: u32,
        comment_type: CommentType,
        need_replies: bool,
        page: u16,
        size: u16,
    ) -> Result<Option<Vec<Comment>>, Error>;

    /// Get volume Information
    async fn volume_infos(&self, id: u32) -> Result<Option<VolumeInfos>, Error>;

    /// Get content Information
    async fn content_infos(&self, info: &ChapterInfo) -> Result<ContentInfos, Error>;

    /// Get multiple content Information
    async fn content_infos_multiple(
        &self,
        infos: &[ChapterInfo],
    ) -> Result<Vec<ContentInfos>, Error>;

    /// Order chapter
    async fn order_chapter(&self, info: &ChapterInfo) -> Result<(), Error>;

    /// Order the whole novel
    async fn order_novel(&self, id: u32, infos: &VolumeInfos) -> Result<(), Error>;

    /// Download image
    async fn image(&self, url: &Url) -> Result<DynamicImage, Error>;

    /// Get all categories
    async fn categories(&self) -> Result<&Vec<Category>, Error>;

    /// Get all tags
    async fn tags(&self) -> Result<&Vec<Tag>, Error>;

    /// Search all matching novels
    async fn search_infos(
        &self,
        option: &Options,
        page: u16,
        size: u16,
    ) -> Result<Option<Vec<u32>>, Error>;

    /// Does the app have comment in this type
    fn has_this_type_of_comments(comment_type: CommentType) -> bool;
}

mod tests {
    #[test]
    fn test_payment_required() {
        use crate::ChapterInfo;

        assert!(
            ChapterInfo {
                payment_required: Some(true),
                ..Default::default()
            }
            .payment_required()
        );

        assert!(
            !ChapterInfo {
                payment_required: Some(false),
                ..Default::default()
            }
            .payment_required()
        );

        assert!(
            !ChapterInfo {
                payment_required: None,
                ..Default::default()
            }
            .payment_required()
        );
    }

    #[test]
    fn test_is_valid() {
        use crate::ChapterInfo;

        assert!(
            ChapterInfo {
                is_valid: Some(true),
                ..Default::default()
            }
            .is_valid()
        );

        assert!(
            !ChapterInfo {
                is_valid: Some(false),
                ..Default::default()
            }
            .is_valid()
        );

        assert!(
            ChapterInfo {
                is_valid: None,
                ..Default::default()
            }
            .is_valid()
        );
    }
}