Compare commits

..

2 Commits

Author SHA1 Message Date
2f2c0c6bf5 fix compile errs 2025-08-14 23:51:15 +02:00
8daa4f0863 use deserializing properly 2025-08-14 23:42:05 +02:00
9 changed files with 375 additions and 362 deletions

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2021"
[dependencies]
chrono = { version = "0.4.38", default-features = false }
chrono = { version = "0.4.38", default-features = false, features = ["serde"] }
crossterm = { version = "0.29", default-features = false, features = ["events"] }
futures = { version = "0.3.30", default-features = false }
icy_sixel = { version = "0.1.2", default-features = false }

View File

@@ -1,6 +1,6 @@
use crate::{
response_deserializer,
response_deserializer::{Chapter, ChapterFeed, Id, Manga},
types::{Chapter, ChapterFeed, Id, Manga},
util::{ConfigSelectionType, CoverSize, Selection, SelectionRange, SelectionType},
BASE,
};

View File

@@ -176,6 +176,13 @@ pub enum PublicationDemographicError {
InvalidValue,
}
#[derive(Error, Debug)]
pub enum ParseLangErr {
#[error("found invalid lang")]
Invalid,
}
#[derive(Debug, Error, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiError {

View File

@@ -2,6 +2,7 @@
use serde::{Serialize, Serializer};
use std::fmt;
use crate::error::ParseLangErr;
#[derive(Debug, Serialize)]
pub enum Language {
@@ -62,13 +63,19 @@ impl ExtraLang {
}
}
impl Language {
pub fn from(s: &str) -> Self {
use std::str::FromStr;
impl FromStr for Language {
type Err = ParseLangErr;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(extra) = ExtraLang::from(s) {
Language::Extra(extra)
} else {
Language::Normal(isolang::Language::from_639_1(s).unwrap())
return Ok(Language::Extra(extra));
}
if let Some(lang) = isolang::Language::from_639_1(s) {
return Ok(Language::Normal(lang));
}
Err(Self::Err::Invalid)
}
}

View File

@@ -1,7 +1,7 @@
// TODO: Only use a single struct for deserializing
use client::{MangaClient, MangaClientBuilder, Search, SearchOptionsBuilder};
use language::Language;
use response_deserializer::{Chapter, DataType, Id, Manga};
use types::{Chapter, DataType, Id, Manga};
use select::Entry;
use std::{fs, future::Future, path::Path, pin::Pin, process};
use util::{ConfigSearch, CoverSize, SelectionType};
@@ -14,6 +14,7 @@ mod select;
mod test;
mod util;
mod language;
mod types;
const BASE: &str = "https://api.mangadex.org";
const ONLY_METADATA: bool = true;

View File

@@ -1,7 +1,7 @@
#![allow(unused)]
use serde::Serialize;
use crate::response_deserializer::{
use crate::types::{
Chapter, ContentRating, Manga, PublicationDemographic, State, Status, Titles,
};
use crate::language::Language;

View File

@@ -1,311 +1,9 @@
// TODO: Remove this
#![allow(unused)]
use crate::error::*;
use crate::types::*;
use crate::language::Language;
use chrono::{DateTime, FixedOffset};
use serde::{Deserialize, Deserializer, Serialize};
use serde::{Deserialize, Deserializer};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Id(pub String);
#[derive(Deserialize, Debug, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ResponseResult {
Ok,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Status {
Completed,
Ongoing,
Hiatus,
Cancelled,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ContentRating {
Safe,
Suggestive,
Erotica,
Pornographic,
}
#[derive(Debug, Serialize)]
pub enum PublicationDemographic {
Shounen,
Shoujo,
Seinen,
Josei,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum State {
Published,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Response {
Collection,
Entity,
}
#[derive(Debug, PartialEq, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DataType {
Manga,
Chapter,
CoverArt,
Author,
Artist,
ScanlationGroup,
Tag,
User,
CustomList,
Creator,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct ChapterImages {
pub result: ResponseResult,
pub base_url: String,
pub chapter: ChapterImageData,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct ChapterImageData {
pub hash: String,
pub data: Vec<String>,
pub data_saver: Vec<String>,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct ChapterFeed {
pub result: ResponseResult,
pub response: Response,
pub data: Vec<Chapter>,
pub limit: u32,
pub offset: u32,
pub total: u32,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct Chapter {
pub id: Id,
#[serde(rename = "type")]
pub data_type: DataType,
pub attributes: ChapterAttributes,
pub relationships: Vec<Relationship>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct ChapterAttributes {
#[serde(deserialize_with = "opt_string_to_opt_f32")]
pub volume: Option<f32>,
#[serde(deserialize_with = "opt_string_to_opt_f32")]
pub chapter: Option<f32>,
pub title: Option<String>,
#[serde(deserialize_with = "string_to_language")]
pub translated_language: Language,
pub external_url: Option<String>,
pub is_unavailable: bool,
#[serde(deserialize_with = "string_to_datetime")]
#[serde(rename = "publishAt")]
pub published_at: DateTime<FixedOffset>,
#[serde(deserialize_with = "string_to_datetime")]
pub readable_at: DateTime<FixedOffset>,
#[serde(deserialize_with = "string_to_datetime")]
pub created_at: DateTime<FixedOffset>,
#[serde(deserialize_with = "string_to_datetime")]
pub updated_at: DateTime<FixedOffset>,
pub pages: u32,
pub version: u32,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct SearchResult {
pub result: ResponseResult,
pub response: Response,
pub data: Vec<Manga>,
pub limit: u32,
pub offset: u32,
pub total: u32,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct IdQueryResult {
pub result: ResponseResult,
pub response: Response,
pub data: Manga,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct Manga {
pub id: Id,
#[serde(rename = "type")]
pub data_type: DataType,
pub attributes: MangaAttributes,
pub relationships: Vec<Relationship>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct MangaAttributes {
pub title: Titles,
pub alt_titles: Vec<AltTitles>,
pub description: Description,
pub is_locked: bool,
pub links: Option<Links>,
pub official_links: Option<Vec<String>>,
#[serde(deserialize_with = "string_to_language")]
pub original_language: Language,
#[serde(deserialize_with = "opt_string_to_opt_f32")]
pub last_volume: Option<f32>,
#[serde(deserialize_with = "opt_string_to_opt_f32")]
pub last_chapter: Option<f32>,
#[serde(deserialize_with = "opt_string_to_opt_demographic")]
pub publication_demographic: Option<PublicationDemographic>,
pub status: Status,
pub year: Option<u32>,
pub content_rating: ContentRating,
pub tags: Vec<Tag>,
pub state: State,
pub chapter_numbers_reset_on_new_volume: bool,
#[serde(deserialize_with = "string_to_datetime")]
pub created_at: DateTime<FixedOffset>,
#[serde(deserialize_with = "string_to_datetime")]
pub updated_at: DateTime<FixedOffset>,
pub version: u32,
#[serde(deserialize_with = "vec_opt_string_to_vec_opt_language")]
pub available_translated_languages: Vec<Option<Language>>,
#[serde(deserialize_with = "opt_string_to_opt_id")]
pub latest_uploaded_chapter: Option<Id>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct CoverAttributes {
pub description: String,
#[serde(deserialize_with = "opt_string_to_opt_f32")]
pub volume: Option<f32>,
pub file_name: Id,
#[serde(deserialize_with = "string_to_language")]
pub locale: Language,
#[serde(deserialize_with = "string_to_datetime")]
pub created_at: DateTime<FixedOffset>,
#[serde(deserialize_with = "string_to_datetime")]
pub updated_at: DateTime<FixedOffset>,
pub version: u32,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct Tag {
pub id: Id,
#[serde(rename = "type")]
pub data_type: DataType,
pub attributes: TagAttributes,
pub relationships: Vec<Relationship>,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct Relationship {
pub id: Id,
#[serde(rename = "type")]
pub data_type: DataType,
pub related: Option<String>,
pub attributes: Option<CoverAttributes>,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct TagAttributes {
pub name: TagName,
pub description: Description,
pub group: String,
pub version: u32,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct TagName {
pub en: String,
}
#[derive(Deserialize, Debug, Default)]
#[serde(deny_unknown_fields)]
pub struct Links {
al: Option<String>,
ap: Option<String>,
bw: Option<String>,
kt: Option<String>,
mu: Option<String>,
amz: Option<String>,
cdj: Option<String>,
ebj: Option<String>,
mal: Option<String>,
engtl: Option<String>,
raw: Option<String>,
nu: Option<String>,
}
#[derive(Deserialize, Debug)]
// #[serde(deny_unknown_fields)]
// TODO: Fill
pub struct Description {
pub en: Option<String>,
pub ru: Option<String>,
}
#[derive(Deserialize, Debug)]
// #[serde(deny_unknown_fields)]
// TODO: Fill
pub struct AltTitles {
en: Option<String>,
ja: Option<String>,
ru: Option<String>,
de: Option<String>,
lt: Option<String>,
ar: Option<String>,
zh: Option<String>,
hi: Option<String>,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct Titles {
pub en: String,
}
impl TryFrom<&str> for State {
type Error = ();
fn try_from(s: &str) -> Result<Self, Self::Error> {
Ok(match s {
"published" => Self::Published,
_ => return Err(()),
})
}
}
impl TryFrom<&str> for ContentRating {
type Error = ();
@@ -447,30 +145,7 @@ pub fn deserialize_chapter_images(json: &str) -> ChapterImages {
serde_json::from_str(json).unwrap()
}
fn opt_string_to_opt_demographic<'de, D>(
deserializer: D,
) -> Result<Option<PublicationDemographic>, D::Error>
where
D: Deserializer<'de>,
{
let opt = Option::<String>::deserialize(deserializer)?;
match opt {
Some(n) => Ok(Some(
n.as_str().try_into().map_err(serde::de::Error::custom)?,
)),
None => Ok(None),
}
}
fn string_to_datetime<'de, D>(deserializer: D) -> Result<DateTime<FixedOffset>, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
DateTime::parse_from_rfc3339(&s).map_err(serde::de::Error::custom)
}
fn opt_string_to_opt_f32<'de, D>(deserializer: D) -> Result<Option<f32>, D::Error>
pub fn opt_string_to_opt_f32<'de, D>(deserializer: D) -> Result<Option<f32>, D::Error>
where
D: Deserializer<'de>,
{
@@ -487,34 +162,65 @@ where
}
}
fn string_to_language<'de, D>(deserializer: D) -> Result<Language, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(Language::from(s.as_str()))
use serde::de::{self, Visitor};
struct LanguageVisitor;
impl<'de> Visitor<'de> for LanguageVisitor {
type Value = Language;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an integer or string for for converting to Language")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
value.parse().map_err(|_| {
E::custom(format!(
"invalid str for converting to Language, got `{value}`"
))
})
}
}
fn vec_opt_string_to_vec_opt_language<'de, D>(
deserializer: D,
) -> Result<Vec<Option<Language>>, D::Error>
where
D: Deserializer<'de>,
{
Ok(Vec::<Option<String>>::deserialize(deserializer)?
.into_iter()
.map(|m| m.map(|v| Language::from(v.as_str())))
.collect())
impl<'de> Deserialize<'de> for Language {
fn deserialize<D>(deserializer: D) -> Result<Language, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(LanguageVisitor)
}
}
fn opt_string_to_opt_id<'de, D>(deserializer: D) -> Result<Option<Id>, D::Error>
where
D: Deserializer<'de>,
{
let opt = Option::<String>::deserialize(deserializer)?;
match opt {
Some(n) => Ok(Some(Id(n))),
None => Ok(None),
struct PublicationDemographicVisitor;
impl<'de> Visitor<'de> for PublicationDemographicVisitor {
type Value = PublicationDemographic;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an integer or string for for converting to PublicationDemographic")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
value.try_into().map_err(|_| {
E::custom(format!(
"invalid str for converting to PublicationDemographic, got `{value}`"
))
})
}
}
impl<'de> Deserialize<'de> for PublicationDemographic {
fn deserialize<D>(deserializer: D) -> Result<PublicationDemographic, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(PublicationDemographicVisitor)
}
}

292
src/types.rs Normal file
View File

@@ -0,0 +1,292 @@
#![allow(unused)]
use crate::language::Language;
use crate::response_deserializer::opt_string_to_opt_f32;
use chrono::{DateTime, FixedOffset};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Id(pub String);
#[derive(Deserialize, Debug, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ResponseResult {
Ok,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Status {
Completed,
Ongoing,
Hiatus,
Cancelled,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ContentRating {
Safe,
Suggestive,
Erotica,
Pornographic,
}
#[derive(Debug, Serialize)]
pub enum PublicationDemographic {
Shounen,
Shoujo,
Seinen,
Josei,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum State {
Published,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Response {
Collection,
Entity,
}
#[derive(Debug, PartialEq, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DataType {
Manga,
Chapter,
CoverArt,
Author,
Artist,
ScanlationGroup,
Tag,
User,
CustomList,
Creator,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct ChapterImages {
pub result: ResponseResult,
pub base_url: String,
pub chapter: ChapterImageData,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct ChapterImageData {
pub hash: String,
pub data: Vec<String>,
pub data_saver: Vec<String>,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct ChapterFeed {
pub result: ResponseResult,
pub response: Response,
pub data: Vec<Chapter>,
pub limit: u32,
pub offset: u32,
pub total: u32,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct Chapter {
pub id: Id,
#[serde(rename = "type")]
pub data_type: DataType,
pub attributes: ChapterAttributes,
pub relationships: Vec<Relationship>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct ChapterAttributes {
#[serde(deserialize_with = "opt_string_to_opt_f32")]
pub volume: Option<f32>,
#[serde(deserialize_with = "opt_string_to_opt_f32")]
pub chapter: Option<f32>,
pub title: Option<String>,
pub translated_language: Language,
pub external_url: Option<String>,
pub is_unavailable: bool,
#[serde(rename = "publishAt")]
pub published_at: DateTime<FixedOffset>,
pub readable_at: DateTime<FixedOffset>,
pub created_at: DateTime<FixedOffset>,
pub updated_at: DateTime<FixedOffset>,
pub pages: u32,
pub version: u32,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct SearchResult {
pub result: ResponseResult,
pub response: Response,
pub data: Vec<Manga>,
pub limit: u32,
pub offset: u32,
pub total: u32,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct IdQueryResult {
pub result: ResponseResult,
pub response: Response,
pub data: Manga,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct Manga {
pub id: Id,
#[serde(rename = "type")]
pub data_type: DataType,
pub attributes: MangaAttributes,
pub relationships: Vec<Relationship>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct MangaAttributes {
pub title: Titles,
pub alt_titles: Vec<AltTitles>,
pub description: Description,
pub is_locked: bool,
pub links: Option<Links>,
pub official_links: Option<Vec<String>>,
pub original_language: Language,
#[serde(deserialize_with = "opt_string_to_opt_f32")]
pub last_volume: Option<f32>,
#[serde(deserialize_with = "opt_string_to_opt_f32")]
pub last_chapter: Option<f32>,
pub publication_demographic: Option<PublicationDemographic>,
pub status: Status,
pub year: Option<u32>,
pub content_rating: ContentRating,
pub tags: Vec<Tag>,
pub state: State,
pub chapter_numbers_reset_on_new_volume: bool,
pub created_at: DateTime<FixedOffset>,
pub updated_at: DateTime<FixedOffset>,
pub version: u32,
pub available_translated_languages: Vec<Option<Language>>,
pub latest_uploaded_chapter: Option<Id>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct CoverAttributes {
pub description: String,
#[serde(deserialize_with = "opt_string_to_opt_f32")]
pub volume: Option<f32>,
pub file_name: Id,
pub locale: Language,
pub created_at: DateTime<FixedOffset>,
pub updated_at: DateTime<FixedOffset>,
pub version: u32,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct Tag {
pub id: Id,
#[serde(rename = "type")]
pub data_type: DataType,
pub attributes: TagAttributes,
pub relationships: Vec<Relationship>,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct Relationship {
pub id: Id,
#[serde(rename = "type")]
pub data_type: DataType,
pub related: Option<String>,
pub attributes: Option<CoverAttributes>,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct TagAttributes {
pub name: TagName,
pub description: Description,
pub group: String,
pub version: u32,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct TagName {
pub en: String,
}
#[derive(Deserialize, Debug, Default)]
#[serde(deny_unknown_fields)]
pub struct Links {
al: Option<String>,
ap: Option<String>,
bw: Option<String>,
kt: Option<String>,
mu: Option<String>,
amz: Option<String>,
cdj: Option<String>,
ebj: Option<String>,
mal: Option<String>,
engtl: Option<String>,
raw: Option<String>,
nu: Option<String>,
}
#[derive(Deserialize, Debug)]
// #[serde(deny_unknown_fields)]
// TODO: Fill
pub struct Description {
pub en: Option<String>,
pub ru: Option<String>,
}
#[derive(Deserialize, Debug)]
// #[serde(deny_unknown_fields)]
// TODO: Fill
pub struct AltTitles {
en: Option<String>,
ja: Option<String>,
ru: Option<String>,
de: Option<String>,
lt: Option<String>,
ar: Option<String>,
zh: Option<String>,
hi: Option<String>,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct Titles {
pub en: String,
}
impl TryFrom<&str> for State {
type Error = ();
fn try_from(s: &str) -> Result<Self, Self::Error> {
Ok(match s {
"published" => Self::Published,
_ => return Err(()),
})
}
}

View File

@@ -1,6 +1,6 @@
use crate::{
response_deserializer,
response_deserializer::ChapterImages,
types::ChapterImages,
Chapter, Id, BASE,
};
use icy_sixel::{DiffusionMethod, MethodForLargest, MethodForRep, PixelFormat, Quality};