use std::cmp::Ordering;
use std::fmt::Debug;
use regex::Regex;
use reqwest::StatusCode;
use strum::{EnumCount, EnumIter, IntoEnumIterator};
#[cfg(feature = "freewebnovel")]
pub use crate::backends::freewebnovel::FreeWebNovel;
#[cfg(feature = "libread")]
pub use crate::backends::libread::LibRead;
#[cfg(feature = "lightnovelworld")]
pub use crate::backends::lightnovelworld::LightNovelWorld;
#[cfg(feature = "royalroad")]
pub use crate::backends::royalroad::RoyalRoad;
use crate::utils::get;
use crate::Chapter;
#[cfg(feature = "libread")]
mod libread;
#[cfg(feature = "royalroad")]
mod royalroad;
#[cfg(feature = "freewebnovel")]
mod freewebnovel;
#[cfg(feature = "lightnovelworld")]
mod lightnovelworld;
#[derive(thiserror::Error, Debug)]
pub enum BackendError {
#[error("No backend has been found capable of handling the url {0}.")]
NoMatchingBackendFound(String),
#[error("An error has been encountered while trying to access fiction page: {0}")]
NetError(#[from] reqwest::Error),
#[error("the given url could not be found")]
UrlNotFound,
#[error("We could not access the fiction page: {message}: {status}: {content}")]
RequestFailed {
message: String,
status: StatusCode,
content: String,
},
#[error("An error occured while parsing the fiction page: {0}")]
ParseError(String),
#[error("An error occured while trying to make sense of a date: {0}")]
DateParseError(#[from] chrono::format::ParseError),
#[error("Could not find chapter {0}")]
UnknownChapter(usize),
#[error("{msg} on Chapter {chapter_url}:", chapter_url=chapter.chapter_url())]
MissingChapterInformation {
msg: String,
chapter: Box<Chapter>,
},
}
type ChapterOrderingFn = Box<dyn Fn(&Chapter, &Chapter) -> Ordering>;
pub(crate) type ChapterListElem = (usize, String);
impl TryFrom<&Chapter> for ChapterListElem {
type Error = BackendError;
fn try_from(value: &Chapter) -> Result<Self, Self::Error> {
Ok((
value.index,
value
.title()
.as_ref()
.ok_or(BackendError::MissingChapterInformation {
msg: "Could not find a valid title".to_string(),
chapter: Box::new(value.clone()),
})?
.to_string(),
))
}
}
pub trait Backend: Default + Debug
where
Self: Sized,
{
fn get_backend_regexps() -> Vec<Regex>;
fn get_backend_name() -> &'static str;
fn get_ordering_function() -> ChapterOrderingFn {
Box::new(|c1: &Chapter, c2: &Chapter| c1.published_at().cmp(c2.published_at()))
}
fn new(url: &str) -> Result<Self, BackendError>;
fn title(&self) -> Result<String, BackendError>;
fn immutable_identifier(&self) -> Result<String, BackendError>;
fn url(&self) -> String;
fn cover_url(&self) -> Result<String, BackendError>;
fn get_authors(&self) -> Result<Vec<String>, BackendError>;
fn get_chapter_list(&self) -> Result<Vec<ChapterListElem>, BackendError>;
fn get_chapter(&self, chapter_number: usize) -> Result<Chapter, BackendError>;
fn get_chapter_count(&self) -> Result<usize, BackendError> {
Ok(self.get_chapter_list()?.len())
}
fn get_chapters(&self) -> Result<Vec<Chapter>, BackendError> {
let mut chapters = Vec::new();
for i in 1..self.get_chapter_count()? {
let chapter = self.get_chapter(i)?;
chapters.push(chapter);
}
Ok(chapters)
}
fn cover(&self) -> Result<Vec<u8>, BackendError> {
let resp = get(self.cover_url()?)?;
if !resp.status().is_success() {
return Err(BackendError::RequestFailed {
message: "Could not download cover image".to_string(),
status: resp.status(),
content: resp.text()?,
});
}
let image_bytes = resp.bytes()?;
Ok(image_bytes.to_vec())
}
}
#[derive(EnumCount, EnumIter, Debug, Default)]
pub enum Backends {
#[default]
Dumb,
#[cfg(feature = "royalroad")]
RoyalRoad(RoyalRoad),
#[cfg(feature = "libread")]
LibRead(LibRead),
#[cfg(feature = "freewebnovel")]
FreeWebNovel(FreeWebNovel),
#[cfg(feature = "lightnovelworld")]
LightNovelWorld(LightNovelWorld),
}
impl Backends {
pub fn get_ordering_function(&self) -> ChapterOrderingFn {
match self {
Backends::Dumb => {
unimplemented!()
}
#[cfg(feature = "royalroad")]
Backends::RoyalRoad(_) => RoyalRoad::get_ordering_function(),
#[cfg(feature = "libread")]
Backends::LibRead(_) => LibRead::get_ordering_function(),
#[cfg(feature = "freewebnovel")]
Backends::FreeWebNovel(_) => FreeWebNovel::get_ordering_function(),
#[cfg(feature = "lightnovelworld")]
Backends::LightNovelWorld(_) => LightNovelWorld::get_ordering_function(),
}
}
pub(crate) fn new_from_url(&self, url: &str) -> Result<Backends, BackendError> {
match self {
Backends::Dumb => Ok(Self::Dumb),
#[cfg(feature = "royalroad")]
Backends::RoyalRoad(_) => Ok(Self::RoyalRoad(RoyalRoad::new(url)?)),
#[cfg(feature = "libread")]
Backends::LibRead(_) => Ok(Self::LibRead(LibRead::new(url)?)),
#[cfg(feature = "freewebnovel")]
Backends::FreeWebNovel(_) => Ok(Self::FreeWebNovel(FreeWebNovel::new(url)?)),
#[cfg(feature = "lightnovelworld")]
Backends::LightNovelWorld(_) => Ok(Self::LightNovelWorld(LightNovelWorld::new(url)?)),
}
}
pub fn get_backend_regexps(&self) -> Vec<Regex> {
match self {
Backends::Dumb => Vec::new(),
#[cfg(feature = "royalroad")]
Backends::RoyalRoad(_) => RoyalRoad::get_backend_regexps(),
#[cfg(feature = "libread")]
Backends::LibRead(_) => LibRead::get_backend_regexps(),
#[cfg(feature = "freewebnovel")]
Backends::FreeWebNovel(_) => FreeWebNovel::get_backend_regexps(),
#[cfg(feature = "lightnovelworld")]
Backends::LightNovelWorld(_) => LightNovelWorld::get_backend_regexps(),
}
}
pub fn get_backend_name(&self) -> &'static str {
match self {
Backends::Dumb => "dummy",
#[cfg(feature = "royalroad")]
Backends::RoyalRoad(_) => RoyalRoad::get_backend_name(),
#[cfg(feature = "libread")]
Backends::LibRead(_) => LibRead::get_backend_name(),
#[cfg(feature = "freewebnovel")]
Backends::FreeWebNovel(_) => FreeWebNovel::get_backend_name(),
#[cfg(feature = "lightnovelworld")]
Backends::LightNovelWorld(_) => LightNovelWorld::get_backend_name(),
}
}
}
impl Backend for Backends {
fn get_backend_regexps() -> Vec<Regex> {
unimplemented!()
}
fn get_backend_name() -> &'static str {
unimplemented!()
}
fn get_ordering_function() -> ChapterOrderingFn {
unimplemented!()
}
fn new(url: &str) -> Result<Self, BackendError> {
for backend_variant in Backends::iter() {
for regex in backend_variant.get_backend_regexps() {
if regex.is_match(url) {
return backend_variant.new_from_url(url);
}
}
}
Err(BackendError::NoMatchingBackendFound(url.to_string()))
}
fn title(&self) -> Result<String, BackendError> {
match self {
Backends::Dumb => {
unimplemented!()
}
#[cfg(feature = "royalroad")]
Backends::RoyalRoad(b) => b.title(),
#[cfg(feature = "libread")]
Backends::LibRead(b) => b.title(),
#[cfg(feature = "freewebnovel")]
Backends::FreeWebNovel(b) => b.title(),
#[cfg(feature = "lightnovelworld")]
Backends::LightNovelWorld(b) => b.title(),
}
}
fn immutable_identifier(&self) -> Result<String, BackendError> {
match self {
Backends::Dumb => {
unimplemented!()
}
#[cfg(feature = "royalroad")]
Backends::RoyalRoad(b) => b.immutable_identifier(),
#[cfg(feature = "libread")]
Backends::LibRead(b) => b.immutable_identifier(),
#[cfg(feature = "freewebnovel")]
Backends::FreeWebNovel(b) => b.immutable_identifier(),
#[cfg(feature = "lightnovelworld")]
Backends::LightNovelWorld(b) => b.immutable_identifier(),
}
}
fn url(&self) -> String {
match self {
Backends::Dumb => {
unimplemented!()
}
#[cfg(feature = "royalroad")]
Backends::RoyalRoad(b) => b.url(),
#[cfg(feature = "libread")]
Backends::LibRead(b) => b.url(),
#[cfg(feature = "freewebnovel")]
Backends::FreeWebNovel(b) => b.url(),
#[cfg(feature = "lightnovelworld")]
Backends::LightNovelWorld(b) => b.url(),
}
}
fn cover_url(&self) -> Result<String, BackendError> {
match self {
Backends::Dumb => {
unimplemented!()
}
#[cfg(feature = "royalroad")]
Backends::RoyalRoad(backend) => backend.cover_url(),
#[cfg(feature = "libread")]
Backends::LibRead(backend) => backend.cover_url(),
#[cfg(feature = "freewebnovel")]
Backends::FreeWebNovel(backend) => backend.cover_url(),
#[cfg(feature = "lightnovelworld")]
Backends::LightNovelWorld(b) => b.cover_url(),
}
}
fn get_authors(&self) -> Result<Vec<String>, BackendError> {
match self {
Backends::Dumb => {
unimplemented!()
}
#[cfg(feature = "royalroad")]
Backends::RoyalRoad(b) => b.get_authors(),
#[cfg(feature = "libread")]
Backends::LibRead(b) => b.get_authors(),
#[cfg(feature = "freewebnovel")]
Backends::FreeWebNovel(b) => b.get_authors(),
#[cfg(feature = "lightnovelworld")]
Backends::LightNovelWorld(b) => b.get_authors(),
}
}
fn get_chapter_list(&self) -> Result<Vec<ChapterListElem>, BackendError> {
match self {
Backends::Dumb => {
unimplemented!()
}
#[cfg(feature = "royalroad")]
Backends::RoyalRoad(b) => b.get_chapter_list(),
#[cfg(feature = "libread")]
Backends::LibRead(b) => b.get_chapter_list(),
#[cfg(feature = "freewebnovel")]
Backends::FreeWebNovel(b) => b.get_chapter_list(),
#[cfg(feature = "lightnovelworld")]
Backends::LightNovelWorld(b) => b.get_chapter_list(),
}
}
fn get_chapter(&self, chapter_number: usize) -> Result<Chapter, BackendError> {
match self {
Backends::Dumb => {
unimplemented!()
}
#[cfg(feature = "royalroad")]
Backends::RoyalRoad(b) => b.get_chapter(chapter_number),
#[cfg(feature = "libread")]
Backends::LibRead(b) => b.get_chapter(chapter_number),
#[cfg(feature = "freewebnovel")]
Backends::FreeWebNovel(b) => b.get_chapter(chapter_number),
#[cfg(feature = "lightnovelworld")]
Backends::LightNovelWorld(b) => b.get_chapter(chapter_number),
}
}
fn get_chapter_count(&self) -> Result<usize, BackendError> {
match self {
Backends::Dumb => {
unimplemented!()
}
#[cfg(feature = "royalroad")]
Backends::RoyalRoad(b) => b.get_chapter_count(),
#[cfg(feature = "libread")]
Backends::LibRead(b) => b.get_chapter_count(),
#[cfg(feature = "freewebnovel")]
Backends::FreeWebNovel(b) => b.get_chapter_count(),
#[cfg(feature = "lightnovelworld")]
Backends::LightNovelWorld(b) => b.get_chapter_count(),
}
}
}