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 }