New J-Novel API Module, should be finished
All checks were successful
Run Tests on Code / run-tests (push) Successful in 0s
All checks were successful
Run Tests on Code / run-tests (push) Successful in 0s
This commit is contained in:
parent
b128f243fc
commit
30e5fcbf72
1 changed files with 359 additions and 0 deletions
359
src/jnovel/mod.rs
Normal file
359
src/jnovel/mod.rs
Normal file
|
@ -0,0 +1,359 @@
|
|||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::ops::Sub;
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use lemmy_db_schema::source::post::Post;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
use crate::{HTTP_CLIENT};
|
||||
use crate::jnovel::PartInfo::{NoParts, Part};
|
||||
use crate::jnovel::PostInfo::{Chapter, Volume};
|
||||
|
||||
static PAST_DAYS_ELIGIBLE: u8 = 4;
|
||||
|
||||
macro_rules! api_url {
|
||||
() => {
|
||||
"https://labs.j-novel.club/app/v1".to_string()
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! jnc_base_url {
|
||||
() => {
|
||||
"https://j-novel.club".to_string()
|
||||
};
|
||||
}
|
||||
|
||||
#[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,
|
||||
pub(crate) number: u8,
|
||||
pub(crate) publishing: String,
|
||||
#[serde(alias = "shortDescription")]
|
||||
pub(crate) short_description: String,
|
||||
pub(crate) cover: Cover,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub(crate) struct ChapterDetail {
|
||||
pub(crate) title: String,
|
||||
pub(crate) slug: String,
|
||||
pub(crate) launch: String,
|
||||
pub(crate) cover: Option<Cover>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct LemmyPostInfo {
|
||||
title: String,
|
||||
url: Url,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub(crate) enum PartInfo {
|
||||
NoParts,
|
||||
Part(u8),
|
||||
}
|
||||
|
||||
impl PartInfo {
|
||||
pub(crate) fn is_parts(&self) -> bool {
|
||||
match self {
|
||||
Part(_) => true,
|
||||
NoParts => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_no_parts(&self) -> bool {
|
||||
!self.is_parts()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for PartInfo {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
let self_numeric = match self {
|
||||
Part(number) => number,
|
||||
NoParts => &0,
|
||||
};
|
||||
|
||||
let other_numeric = match other {
|
||||
Part(number) => number,
|
||||
NoParts => &0,
|
||||
};
|
||||
|
||||
self_numeric == other_numeric
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for PartInfo {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
if self.gt(other) { Some(Ordering::Greater) }
|
||||
else if self.eq(other) { Some(Ordering::Equal) }
|
||||
else { Some(Ordering::Less) }
|
||||
}
|
||||
|
||||
fn lt(&self, other: &Self) -> bool {
|
||||
let self_numeric = match self {
|
||||
Part(number) => number,
|
||||
NoParts => &0,
|
||||
};
|
||||
|
||||
let other_numeric = match other {
|
||||
Part(number) => number,
|
||||
NoParts => &0,
|
||||
};
|
||||
|
||||
self_numeric < other_numeric
|
||||
}
|
||||
|
||||
fn le(&self, other: &Self) -> bool {
|
||||
!self.gt(other)
|
||||
}
|
||||
|
||||
fn gt(&self, other: &Self) -> bool {
|
||||
let self_numeric = match self {
|
||||
Part(number) => number,
|
||||
NoParts => &0,
|
||||
};
|
||||
|
||||
let other_numeric = match other {
|
||||
Part(number) => number,
|
||||
NoParts => &0,
|
||||
};
|
||||
|
||||
self_numeric > other_numeric
|
||||
}
|
||||
|
||||
fn ge(&self, other: &Self) -> bool {
|
||||
!self.lt(other)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum PostInfo {
|
||||
Chapter { part: PartInfo, volume: u8, lemmy_info: LemmyPostInfo },
|
||||
Volume { part: PartInfo, lemmy_info: LemmyPostInfo },
|
||||
}
|
||||
|
||||
impl PartialEq for PostInfo {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
let self_part = match self {
|
||||
Chapter {part, ..} => part,
|
||||
Volume {part, ..} => part,
|
||||
};
|
||||
|
||||
let other_part = match other {
|
||||
Chapter {part, ..} => part,
|
||||
Volume {part, ..} => part,
|
||||
};
|
||||
|
||||
self_part.eq(other_part)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for PostInfo {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
if self.gt(other) { Some(Ordering::Greater) }
|
||||
else if self.eq(other) { Some(Ordering::Equal) }
|
||||
else { Some(Ordering::Less) }
|
||||
}
|
||||
|
||||
fn lt(&self, other: &Self) -> bool {
|
||||
let self_part = match self {
|
||||
Chapter {part, ..} => part,
|
||||
Volume {part, ..} => part,
|
||||
};
|
||||
|
||||
let other_part = match other {
|
||||
Chapter {part, ..} => part,
|
||||
Volume {part, ..} => part,
|
||||
};
|
||||
|
||||
self_part < other_part
|
||||
}
|
||||
|
||||
fn le(&self, other: &Self) -> bool {
|
||||
!self.gt(other)
|
||||
}
|
||||
|
||||
fn gt(&self, other: &Self) -> bool {
|
||||
let self_part = match self {
|
||||
Chapter {part, ..} => part,
|
||||
Volume {part, ..} => part,
|
||||
};
|
||||
|
||||
let other_part = match other {
|
||||
Chapter {part, ..} => part,
|
||||
Volume {part, ..} => part,
|
||||
};
|
||||
|
||||
self_part > other_part
|
||||
}
|
||||
|
||||
fn ge(&self, other: &Self) -> bool {
|
||||
!self.lt(other)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn check_feed(series_slug: &str, series_has_parts: bool) -> Result<Vec<PostInfo>, Box<dyn Error>> {
|
||||
let response = HTTP_CLIENT
|
||||
.get(api_url!() + "/series/" + series_slug + "/volumes?format=json")
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
let mut volume_brief_data: VolumesWrapper = serde_json::from_str(&response)?;
|
||||
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 chapter_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 series_has_parts {
|
||||
true => continue,
|
||||
false => break,
|
||||
}
|
||||
}
|
||||
let new_part_info: PartInfo;
|
||||
|
||||
if 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 => {
|
||||
println!("No Part found, assuming 1");
|
||||
new_part_info = Part(1);
|
||||
},
|
||||
}
|
||||
}
|
||||
else {
|
||||
new_part_info = NoParts;
|
||||
}
|
||||
|
||||
let post_url = format!("{}/series/{series_slug}#volume-{}", jnc_base_url!(), volume.number);
|
||||
let post_details = LemmyPostInfo {
|
||||
title: volume.title.clone(),
|
||||
url: Url::parse(&post_url).unwrap(),
|
||||
};
|
||||
|
||||
let new_post_info = Volume {
|
||||
part: new_part_info,
|
||||
lemmy_info: post_details,
|
||||
};
|
||||
|
||||
let part_id = match new_part_info {
|
||||
Part(number) => number,
|
||||
NoParts => 0,
|
||||
};
|
||||
|
||||
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(part_info) = get_latest_part(&volume.slug).await? {
|
||||
let part_post_info = Chapter {
|
||||
part: new_part_info,
|
||||
volume: volume.number,
|
||||
lemmy_info: part_info
|
||||
};
|
||||
|
||||
chapter_map
|
||||
.entry(part_id)
|
||||
.and_modify(|val| {
|
||||
if *val < part_post_info {
|
||||
*val = part_post_info.clone()
|
||||
}
|
||||
})
|
||||
.or_insert(part_post_info);
|
||||
}
|
||||
}
|
||||
|
||||
let mut result_vec: Vec<PostInfo> = volume_map.values().cloned().collect();
|
||||
let mut chapter_vec: Vec<PostInfo> = chapter_map.values().cloned().collect();
|
||||
result_vec.append(&mut chapter_vec);
|
||||
|
||||
Ok(result_vec)
|
||||
}
|
||||
|
||||
async fn get_latest_part(volume_slug: &str) -> Result<Option<LemmyPostInfo>, Box<dyn Error>> {
|
||||
let response = HTTP_CLIENT
|
||||
.get(api_url!() + "/volumes/" + volume_slug + "/parts?format=json")
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
let mut volume_parts_data: ChapterWrapper = serde_json::from_str(&response)?;
|
||||
volume_parts_data.parts.reverse(); // Makes breaking out of the parts loop easier
|
||||
|
||||
let mut post_details: Option<LemmyPostInfo> = None;
|
||||
|
||||
for part in volume_parts_data.parts.iter() {
|
||||
let publishing_date = DateTime::parse_from_rfc3339(&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 post_url = format!("{}/read/{}", jnc_base_url!(), part.slug);
|
||||
post_details = Some(LemmyPostInfo {
|
||||
title: part.title.clone(),
|
||||
url: Url::parse(&post_url).unwrap(),
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
Ok(post_details)
|
||||
}
|
Loading…
Reference in a new issue