aob-lemmy-bot/src/fetchers/jnovel.rs

307 lines
9.2 KiB
Rust

use crate::{HTTP_CLIENT};
use chrono::{DateTime, Duration, Utc};
use serde_derive::{Deserialize, Serialize};
use std::collections::HashMap;
use std::ops::Sub;
use async_trait::async_trait;
use crate::fetchers::{FetcherTrait};
use crate::lemmy::{PartInfo, PostInfo, PostInfoInner, PostType};
use systemd_journal_logger::connected_to_journal;
use crate::lemmy::PartInfo::{NoParts, Part};
macro_rules! error {
($msg:tt) => {
match connected_to_journal() {
true => log::error!("[ERROR] {}", $msg),
false => eprintln!("[ERROR] {}", $msg),
}
};
}
macro_rules! info {
($msg:tt) => {
match connected_to_journal() {
true => log::info!("[INFO] {}", $msg),
false => println!("[INFO] {}", $msg),
}
};
}
static PAST_DAYS_ELIGIBLE: u8 = 4;
macro_rules! api_url {
() => {
"https://labs.j-novel.club/app/v1".to_owned()
};
}
macro_rules! jnc_base_url {
() => {
"https://j-novel.club".to_owned()
};
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct VolumesWrapper {
volumes: Vec<VolumeDetail>,
pagination: PaginationInfo,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct ChapterWrapper {
parts: Vec<ChapterDetail>,
pagination: PaginationInfo,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct PaginationInfo {
limit: usize,
skip: usize,
#[serde(alias = "lastPage")]
last_page: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub(crate) struct Cover {
#[serde(alias = "coverUrl")]
pub(crate) cover: String,
#[serde(alias = "thumbnailUrl")]
pub(crate) thumbnail: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub(crate) struct VolumeDetail {
pub(crate) title: String,
pub(crate) slug: String,
number: u8,
publishing: String,
#[serde(alias = "shortDescription")]
short_description: String,
cover: Cover,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub(crate) struct ChapterDetail {
pub(crate) title: String,
pub(crate) slug: String,
launch: String,
pub(crate) cover: Option<Cover>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub(crate) struct JNovelFetcher {
series_slug: String,
series_has_parts: bool
}
impl Default for JNovelFetcher {
fn default() -> Self {
Self {
series_slug: "".to_owned(),
series_has_parts: false,
}
}
}
impl JNovelFetcher {
pub(crate) fn set_series(&mut self, series: String) {
self.series_slug = series;
}
pub(crate) fn set_part_option(&mut self, has_parts: bool) {
self.series_has_parts = has_parts;
}
}
#[async_trait]
impl FetcherTrait for JNovelFetcher {
fn new() -> Self {
JNovelFetcher {
series_slug: "".to_owned(),
series_has_parts: false
}
}
async fn check_feed(&self) -> Result<Vec<PostInfo>, ()> {
let response = match HTTP_CLIENT
.get(api_url!() + "/series/" + self.series_slug.as_str() + "/volumes?format=json")
.send()
.await
{
Ok(data) => match data.text().await {
Ok(data) => data,
Err(e) => {
let err_msg = format!("While checking feed: {e}");
error!(err_msg);
return Err(());
}
},
Err(e) => {
let err_msg = format!("{e}");
error!(err_msg);
return Err(());
}
};
let mut volume_brief_data: VolumesWrapper = match serde_json::from_str(&response) {
Ok(data) => data,
Err(e) => {
let err_msg = format!("{e}");
error!(err_msg);
return Err(());
}
};
volume_brief_data.volumes.reverse(); // Makes breaking out of the volume loop easier
// If no parts just use 0 as Part indicator as no Series with Parts has a Part 0
let mut volume_map: HashMap<u8, PostInfo> = HashMap::new();
let mut prepub_map: HashMap<u8, PostInfo> = HashMap::new();
for volume in volume_brief_data.volumes.iter() {
let publishing_date = DateTime::parse_from_rfc3339(&volume.publishing).unwrap();
if publishing_date < Utc::now().sub(Duration::days(PAST_DAYS_ELIGIBLE as i64)) {
match self.series_has_parts {
true => continue,
false => break,
}
}
let new_part_info: PartInfo;
if self.series_has_parts {
let mut part_number: Option<u8> = None;
let splits: Vec<&str> = volume.slug.split('-').collect();
for (index, split) in splits.clone().into_iter().enumerate() {
if split == "part" {
part_number = Some(
splits[index + 1]
.parse::<u8>()
.expect("Split Element after 'Part' should always be a number"),
);
break;
}
}
match part_number {
Some(number) => new_part_info = Part(number),
None => {
info!("No Part found, assuming 1");
new_part_info = Part(1);
}
}
} else {
new_part_info = NoParts;
}
let post_url = format!(
"{}/series/{}#volume-{}",
jnc_base_url!(),
self.series_slug.as_str(),
volume.number
);
let post_details = PostInfoInner {
title: volume.title.clone(),
url: post_url.clone(),
thumbnail: Some(volume.cover.thumbnail.clone())
};
let new_post_info = PostInfo {
post_type: Some(PostType::Volume),
part: Some(new_part_info),
description: Some(volume.short_description.clone()),
lemmy_info: post_details,
};
let part_id = new_part_info.as_u8();
if publishing_date <= Utc::now() {
volume_map
.entry(part_id)
.and_modify(|val| {
if *val < new_post_info {
*val = new_post_info.clone()
}
})
.or_insert(new_post_info);
}
if let Some(prepub_info) = get_latest_prepub(&volume.slug).await {
let prepub_post_info = PostInfo {
post_type: Some(PostType::Chapter),
part: Some(new_part_info),
lemmy_info: prepub_info,
description: None,
};
prepub_map
.entry(part_id)
.and_modify(|val| {
if *val < prepub_post_info {
*val = prepub_post_info.clone()
}
})
.or_insert(prepub_post_info);
}
}
let mut result_vec: Vec<PostInfo> = volume_map.values().cloned().collect();
let mut prepub_vec: Vec<PostInfo> = prepub_map.values().cloned().collect();
result_vec.append(&mut prepub_vec);
Ok(result_vec)
}
}
async fn get_latest_prepub(volume_slug: &str) -> Option<PostInfoInner> {
let response = match HTTP_CLIENT
.get(api_url!() + "/volumes/" + volume_slug + "/parts?format=json")
.send()
.await
{
Ok(data) => match data.text().await {
Ok(data) => data,
Err(e) => {
let err_msg = format!("While getting latest PrePub: {e}");
error!(err_msg);
return None;
}
},
Err(e) => {
let err_msg = format!("{e}");
error!(err_msg);
return None;
}
};
let mut volume_prepub_parts_data: ChapterWrapper = match serde_json::from_str(&response) {
Ok(data) => data,
Err(e) => {
let err_msg = format!("{e}");
error!(err_msg);
return None;
}
};
volume_prepub_parts_data.parts.reverse(); // Makes breaking out of the parts loop easier
let mut post_details: Option<PostInfoInner> = None;
for prepub_part in volume_prepub_parts_data.parts.iter() {
let publishing_date = DateTime::parse_from_rfc3339(&prepub_part.launch).unwrap();
if publishing_date > Utc::now() {
break;
} else if publishing_date < Utc::now().sub(Duration::days(PAST_DAYS_ELIGIBLE as i64)) {
continue;
}
let thumbnail = prepub_part.cover.as_ref().map(|cover| cover.thumbnail.clone());
let post_url = format!("{}/read/{}", jnc_base_url!(), prepub_part.slug);
post_details = Some(PostInfoInner {
title: prepub_part.title.clone(),
url: post_url.clone(),
thumbnail
});
}
post_details
}