From 352ac3bd1af02da21d1c1c4172798e0768e4c441 Mon Sep 17 00:00:00 2001 From: Vegard Matthey Date: Sat, 26 Jul 2025 17:15:43 +0200 Subject: [PATCH] use serde deserializer functions to remove duplicated structs, remove TryFrom impls, replace Language enum with wrapper around isolang Language type --- .gitignore | 1 + Cargo.toml | 1 + src/client.rs | 203 +++-- src/error.rs | 13 + src/language.rs | 89 ++ src/main.rs | 222 +++-- src/metadata.rs | 5 +- src/response_deserializer.rs | 1495 +++++----------------------------- src/util.rs | 65 +- 9 files changed, 630 insertions(+), 1464 deletions(-) create mode 100644 src/language.rs diff --git a/.gitignore b/.gitignore index 7e5ca36..8a47a9b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ Cargo.lock *.png *.cbz *.json +*.sh diff --git a/Cargo.toml b/Cargo.toml index a39aaf3..fc90cb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ 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 } image = { version = "0.25.2", default-features = false, features = ["jpeg", "png"] } +isolang = { version = "2.4.0", features = ["serde"] } reqwest = { version = "0.12.5", default-features = false, features = ["default-tls"] } reqwest-middleware = { version = "0.4", default-features = false } reqwest-retry = { version = "0.7", default-features = false } diff --git a/src/client.rs b/src/client.rs index 6c4cbe1..74dedb5 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,34 +1,78 @@ -use crate::response_deserializer::{Chapter, ChapterFeed, Id, Language, Manga}; -use crate::util::{ConfigSearch, ConfigSelectionType, CoverSize, SelectionRange}; -use crate::BASE; +use crate::{ + response_deserializer, + response_deserializer::{Chapter, ChapterFeed, Id, Manga}, + util::{ConfigSelectionType, CoverSize, Selection, SelectionRange, SelectionType}, + BASE, +}; +use crate::language::Language; use reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware}; type Client = ClientWithMiddleware; +// An alternate design would use different builders depending on if .search_by_query() or +// .search_by_id() was called or search(Search::Query(query)) / search(Search::Id(id)) +// This would allow for flat building rather than nested +// Example: +// MangaClientBuilder::new() +// .search_by_query("query example") +// You now have a struct like MangaClientQueryBuilder +// .result_limit(3) +// .search_filter(SearchFilter::default()) +// .build() +// And end up with a MangaClient struct +// MangaClientBuilder::new() +// .search_by_id("id example") +// You now have a struct like MangaClientIdBuilder +// .build() +// And end up with a MangaClient struct +// +// Example implementation: +// impl MangaClientBuilder { +// fn search_by_query(mut self, query: &str) -> MangaClientQueryBuilder { +// todo!() +// } +// fn search_by_id(mut self, id: Id) -> MangaClientIdBuilder { +// todo!() +// } +// } pub struct MangaClientBuilder { client: Client, - search_filter: Option, + search: Option, bonus: Option, - result_limit: Option, - cover_size: Option, selection_type: Option, selection_range: Option, - search: Option, - cover: Option, language: Option, } #[derive(Debug)] pub struct MangaClient { pub client: Client, + pub search: Search, + pub selection: Selection, + pub language: Language, +} + +#[derive(Debug)] +pub enum Search { + Id(Id), + Query(SearchOptions), +} + +#[derive(Debug)] +pub struct SearchOptions { + pub query: String, pub search_filter: SearchFilter, pub result_limit: u32, - pub cover_size: CoverSize, - pub search: ConfigSearch, - pub cover: bool, - pub selection: crate::util::Selection, - pub language: Language, + pub cover: Option, +} + +#[derive(Debug, Default)] +pub struct SearchOptionsBuilder { + pub query: Option, + pub search_filter: Option, + pub result_limit: Option, + pub cover: Option, } #[derive(Default, Clone, Debug)] @@ -111,9 +155,40 @@ impl Default for PublicationDemographicFilter { } } +#[allow(unused)] +impl SearchOptionsBuilder { + pub fn new() -> Self { + Self::default() + } + pub fn query(mut self, query: String) -> Self { + self.query = Some(query); + self + } + pub fn search_filter(mut self, search_filter: SearchFilter) -> Self { + self.search_filter = Some(search_filter); + self + } + pub fn result_limit(mut self, result_limit: u32) -> Self { + self.result_limit = Some(result_limit); + self + } + pub fn cover(mut self, cover_size: Option) -> Self { + self.cover = cover_size; + self + } + pub fn build(self) -> SearchOptions { + SearchOptions { + query: self.query.unwrap(), + search_filter: self.search_filter.unwrap_or_default(), + result_limit: self.result_limit.unwrap_or(5), + cover: self.cover, + } + } +} + impl MangaClientBuilder { - pub fn new() -> MangaClientBuilder { - MangaClientBuilder { + pub fn new() -> Self { + Self { client: ClientBuilder::new( reqwest::Client::builder() .user_agent("manga-cli/version-0.1") @@ -124,50 +199,30 @@ impl MangaClientBuilder { ExponentialBackoff::builder().build_with_max_retries(3), )) .build(), - search_filter: None, - bonus: None, - result_limit: None, - cover_size: None, selection_type: None, selection_range: None, search: None, - cover: None, language: None, + bonus: None, } } - pub fn search_filter(mut self, filter: SearchFilter) -> MangaClientBuilder { - self.search_filter = Some(filter); - self - } - pub fn bonus(mut self, bonus: bool) -> MangaClientBuilder { + pub fn bonus(mut self, bonus: bool) -> Self { self.bonus = Some(bonus); self } - pub fn result_limit(mut self, result_limit: u32) -> MangaClientBuilder { - self.result_limit = Some(result_limit); - self - } - pub fn cover_size(mut self, cover_size: CoverSize) -> MangaClientBuilder { - self.cover_size = Some(cover_size); - self - } - pub fn selection_type(mut self, selection_type: ConfigSelectionType) -> MangaClientBuilder { + pub fn selection_type(mut self, selection_type: ConfigSelectionType) -> Self { self.selection_type = Some(selection_type); self } - pub fn selection_range(mut self, selection_range: SelectionRange) -> MangaClientBuilder { + pub fn selection_range(mut self, selection_range: SelectionRange) -> Self { self.selection_range = Some(selection_range); self } - pub fn search(mut self, search: ConfigSearch) -> MangaClientBuilder { + pub fn search(mut self, search: Search) -> Self { self.search = Some(search); self } - pub fn cover(mut self, cover: bool) -> MangaClientBuilder { - self.cover = Some(cover); - self - } - pub fn language(mut self, language: Language) -> MangaClientBuilder { + pub fn language(mut self, language: Language) -> Self { self.language = Some(language); self } @@ -175,21 +230,15 @@ impl MangaClientBuilder { pub fn build(self) -> MangaClient { MangaClient { client: self.client, - search_filter: self.search_filter.unwrap_or_default(), - result_limit: self.result_limit.unwrap_or(5), - cover_size: self.cover_size.unwrap_or_default(), - search: self - .search - .unwrap_or(crate::util::ConfigSearch::Query(String::new())), - cover: self.cover.unwrap_or(true), - selection: crate::util::Selection::new( + search: self.search.unwrap(), + selection: Selection::new( match self.selection_type { Some(a) => match a { ConfigSelectionType::Chapter => { - crate::util::SelectionType::Chapter(self.selection_range.unwrap()) + SelectionType::Chapter(self.selection_range.unwrap()) } ConfigSelectionType::Volume => { - crate::util::SelectionType::Volume(self.selection_range.unwrap()) + SelectionType::Volume(self.selection_range.unwrap()) } }, None => panic!(), @@ -204,13 +253,13 @@ impl MangaClientBuilder { impl MangaClient { pub async fn get_manga(&self) -> Vec { match &self.search { - ConfigSearch::Query(query) => { - let limit = self.result_limit.to_string(); - let mut params = vec![("title", query.as_str()), ("limit", limit.as_str())]; - if self.cover { - params.push(("includes[]", "cover_art")); - } - let search_filter: Vec<(&str, &str)> = self.search_filter.clone().into(); + Search::Query(search) => { + let limit = search.result_limit.to_string(); + let mut params = vec![("title", search.query.as_str()), ("limit", limit.as_str())]; + // if self.cover { + params.push(("includes[]", "cover_art")); + // } + let search_filter: Vec<(&str, &str)> = search.search_filter.clone().into(); let json = self .client .get(format!("{BASE}/manga")) @@ -222,11 +271,11 @@ impl MangaClient { .text() .await .unwrap(); - crate::response_deserializer::deserialize_search(&json) + response_deserializer::deserialize_search(&json) .unwrap() .data } - ConfigSearch::Id(id) => { + Search::Id(id) => { let json = self .client .get(format!("{BASE}/manga/{id}")) @@ -237,7 +286,7 @@ impl MangaClient { .await .unwrap(); vec![ - crate::response_deserializer::deserialize_id_query(&json) + response_deserializer::deserialize_id_query(&json) .unwrap() .data, ] @@ -254,6 +303,7 @@ impl MangaClient { ("limit", limit_s.as_str()), // These are apparently necessary, or else you may miss some chapters ("order[chapter]", "asc"), + ("includeUnavailable", "1"), ("offset", "0"), ]; let url = format!("{BASE}/manga/{id}/feed"); @@ -265,7 +315,7 @@ impl MangaClient { .await? .text() .await?; - let mut result = crate::response_deserializer::deserialize_chapter_feed(&json).unwrap(); + let mut result = response_deserializer::deserialize_chapter_feed(&json).unwrap(); let mut chapters = Vec::new(); chapters.append(&mut result.data); @@ -276,7 +326,13 @@ impl MangaClient { let url = &url; async move { let offset = (i * limit).to_string(); - let params = [params[0], params[1], params[2], ("offset", offset.as_str())]; + let params = [ + params[0], + params[1], + params[2], + params[3], + ("offset", offset.as_str()), + ]; let json = self .client .get(url) @@ -286,7 +342,7 @@ impl MangaClient { .text() .await?; Ok::( - crate::response_deserializer::deserialize_chapter_feed(&json).unwrap(), + response_deserializer::deserialize_chapter_feed(&json).unwrap(), ) } })) @@ -310,8 +366,9 @@ mod tests { #[ignore] async fn ensure_getting_all_chapters() { let client = MangaClientBuilder::new() - .selection_type(crate::util::ConfigSelectionType::Volume) - .selection_range(crate::util::SelectionRange::All) + .search(Search::Id(Id(String::new()))) + .selection_type(ConfigSelectionType::Volume) + .selection_range(SelectionRange::All) .build(); // These tests should only be done for completed manga because last chapter might change @@ -361,15 +418,21 @@ mod tests { #[tokio::test] #[ignore] async fn get_manga() { - search("JoJo's Bizarre Adventure Part 1 - Phantom Blood", "5a547d1d-576b-477f-8cb3-70a3b4187f8a").await; + search( + "JoJo's Bizarre Adventure Part 1 - Phantom Blood", + "5a547d1d-576b-477f-8cb3-70a3b4187f8a", + ) + .await; search("Slam Dunk!", "319df2e2-e6a6-4e3a-a31c-68539c140a84").await; } async fn search(query: &str, id: &str) { let client = MangaClientBuilder::new() - .selection_type(crate::util::ConfigSelectionType::Volume) - .selection_range(crate::util::SelectionRange::All) - .search(ConfigSearch::Query(String::from(query))) + .selection_type(ConfigSelectionType::Volume) + .selection_range(SelectionRange::All) + .search(Search::Query( + SearchOptionsBuilder::new().query(query.to_owned()).build(), + )) .build(); let manga: Vec = client.get_manga().await; diff --git a/src/error.rs b/src/error.rs index 557ec9b..2d47ebe 100644 --- a/src/error.rs +++ b/src/error.rs @@ -171,6 +171,11 @@ pub enum IdQueryResultError { IdQueryResult(#[from] IdQueryResponseError), } +#[derive(Debug, Error)] +pub enum PublicationDemographicError { + InvalidValue, +} + #[derive(Debug, Error, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ApiError { @@ -208,6 +213,14 @@ impl fmt::Display for ChapterImagesContentError { } } +impl fmt::Display for PublicationDemographicError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidValue => "InvalidValue".fmt(f), + } + } +} + impl serde::de::Error for JsonError { fn custom(msg: T) -> Self { JsonError::Message(msg.to_string()) diff --git a/src/language.rs b/src/language.rs new file mode 100644 index 0000000..73e5cf5 --- /dev/null +++ b/src/language.rs @@ -0,0 +1,89 @@ +// Wrapper for including extras from https://en.wikipedia.org/wiki/IETF_language_tag + +use serde::{Serialize, Serializer}; +use std::fmt; + +#[derive(Debug, Serialize)] +pub enum Language { + Normal(isolang::Language), + #[serde(serialize_with = "serialize_extralang_as_str")] + Extra(ExtraLang), +} + +#[derive(Debug, Serialize)] +pub enum ExtraLang { + SimplifiedChinese, + TraditionalChinese, + BrazilianPortugese, + CastilianSpanish, + LatinAmericanSpanish, + RomanizedJapanese, + RomanizedKorean, + RomanizedChinese, +} + +fn serialize_extralang_as_str(x: &ExtraLang, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(x.to_string().as_str()) +} + +impl fmt::Display for ExtraLang { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use ExtraLang::*; + match self { + SimplifiedChinese => "zh".fmt(f), + TraditionalChinese => "zh-hk".fmt(f), + BrazilianPortugese => "pt-br".fmt(f), + CastilianSpanish => "es".fmt(f), + LatinAmericanSpanish => "es-la".fmt(f), + RomanizedJapanese => "ja-ro".fmt(f), + RomanizedKorean => "ko-ro".fmt(f), + RomanizedChinese => "zh-ro".fmt(f), + } + } +} + +impl ExtraLang { + fn from(s: &str) -> Option { + use ExtraLang::*; + Some(match s { + "zh" => SimplifiedChinese, + "zh-hk" => TraditionalChinese, + "pt-br" => BrazilianPortugese, + "es" => CastilianSpanish, + "es-la" => LatinAmericanSpanish, + "ja-ro" => RomanizedJapanese, + "ko-ro" => RomanizedKorean, + "zh-ro" => RomanizedChinese, + _ => return None, + }) + } +} + +impl Language { + pub fn from(s: &str) -> Self { + if let Some(extra) = ExtraLang::from(s) { + Language::Extra(extra) + } else { + Language::Normal(isolang::Language::from_639_1(s).unwrap()) + } + } +} + +impl Default for Language { + fn default() -> Self { + Self::Normal(isolang::Language::from_name("English").unwrap()) + } +} + +impl fmt::Display for Language { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use Language::*; + match self { + Extra(extra) => extra.fmt(f), + Normal(normal) => normal.to_639_1().unwrap().fmt(f), + } + } +} diff --git a/src/main.rs b/src/main.rs index 080ccd1..e547bae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,48 +1,60 @@ -use client::{MangaClient, MangaClientBuilder, SearchFilter}; -use response_deserializer::{Chapter, Id, Language, Manga}; +// 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 select::Entry; -use std::{future::Future, pin::Pin}; -use util::SelectionType; +use std::{fs, future::Future, path::Path, pin::Pin, process}; +use util::{ConfigSearch, CoverSize, SelectionType}; mod client; mod error; +mod metadata; mod response_deserializer; mod select; mod test; mod util; -mod metadata; +mod language; const BASE: &str = "https://api.mangadex.org"; +const ONLY_METADATA: bool = true; #[tokio::main] async fn main() { let config = util::args(); - let manga_client = - MangaClientBuilder::new() - .bonus(config.bonus.unwrap_or_else(util::ask_bonus)) - .cover(config.cover.unwrap_or_else(util::ask_cover)) - .result_limit(config.result_limit.unwrap_or(5)) - .cover_size(util::CoverSize::Full) - .selection_type( - config - .selection_type - .unwrap_or_else(util::ask_selection_type), - ) - .selection_range( - config - .selection_range - .unwrap_or_else(util::ask_selection_range), - ) - .search(config.search.unwrap_or_else(|| { - util::ConfigSearch::Query(util::get_input("Enter search query: ")) - })) - .search_filter(SearchFilter::default()) - .language(Language::default()) - .build(); + let manga_client = MangaClientBuilder::new() + .search(match config.search { + None => Search::Query( + SearchOptionsBuilder::new() + .query(util::get_input("Enter search query: ")) + .build(), + ), + Some(ConfigSearch::Id(id)) => Search::Id(id), + Some(ConfigSearch::Query(query)) => Search::Query( + SearchOptionsBuilder::new() + .query(query) + .result_limit(config.result_limit.unwrap_or(5)) + .build(), + ), + }) + .selection_type( + config + .selection_type + .unwrap_or_else(util::ask_selection_type), + ) + .selection_range( + config + .selection_range + .unwrap_or_else(util::ask_selection_range), + ) + .language(Language::Normal( + isolang::Language::from_name("English").unwrap(), + )) + .bonus(config.bonus.unwrap_or_else(util::ask_bonus)) + .build(); let mut search_result = manga_client.get_manga().await; if search_result.is_empty() { eprintln!("Found no search results"); - std::process::exit(1); + process::exit(1); } let mut choice = 0; @@ -50,11 +62,35 @@ async fn main() { choice = select_manga_from_search(&manga_client, &search_result).await; } let manga = search_result.remove(choice as usize); + if let Some(cover_data) = &manga.relationships[2].attributes { + let image_url = format!( + "https://uploads.mangadex.org/covers/{}/{}", + &manga.id.0, &cover_data.file_name + ); + let dir = format!("images/{}", manga.attributes.title.en); + if !Path::new(&dir).exists() { + fs::create_dir(&dir).unwrap(); + } + let path = format!("{}/cover.jpg", &dir); + fs::write( + path, + manga_client + .client + .get(&image_url) + .send() + .await + .unwrap() + .bytes() + .await + .unwrap(), + ) + .unwrap(); + } let mut chapters = match manga_client.get_chapters(&manga.id).await { Ok(v) => v, Err(e) => { eprintln!("ERROR: {e:#?}"); - std::process::exit(1); + process::exit(1); } }; println!("Downloading {} chapters", chapters.len()); @@ -65,34 +101,69 @@ async fn main() { .partial_cmp(&b.attributes.chapter.unwrap_or(-1.)) .unwrap() }); - - let create_volumes = matches!( - manga_client.selection.selection_type, - SelectionType::Volume(_) - ); - let selected_chapters = util::get_chapters_from_selection(manga_client.selection, &chapters); + let mut actual_chapters: Vec = Vec::new(); + for chapter in chapters { + let mut should_push = false; + for relation in &chapter.relationships { + println!( + "relation type: {:?}, id: {:?}", + relation.data_type, relation.id + ); + if relation.data_type == DataType::ScanlationGroup + && (relation.id == Id(String::from("13e87775-f8b2-4d5f-a814-8dc245820e5a")) + || relation.id == Id(String::from("e2ffaa79-1bef-4b7d-b382-a2b98221a310"))) + { + should_push = true; + } + } + if should_push { + actual_chapters.push(chapter); + } + } + let chapters = actual_chapters; let title = &manga.attributes.title.en.clone(); - util::download_selected_chapters( - &manga_client.client, - &selected_chapters, - title, - create_volumes, - ) - .await; - - util::create_volumes_or_chapters(&selected_chapters, create_volumes, title); + if !ONLY_METADATA { + let create_volumes = matches!( + manga_client.selection.selection_type, + SelectionType::Volume(_) + ); + let selected_chapters = + util::get_chapters_from_selection(manga_client.selection, &chapters); + util::download_selected_chapters( + &manga_client.client, + &selected_chapters, + title, + create_volumes, + ) + .await; + util::create_volumes_or_chapters(&selected_chapters, create_volumes, title); + } let metadata = metadata::Metadata::new(manga, chapters); - std::fs::write(format!("images/{}/metadata.json", title), serde_json::to_string(&metadata).unwrap()).unwrap(); + let dir = format!("images/{}", title); + if !Path::new(&dir).exists() { + fs::create_dir(Path::new(&dir)).unwrap(); + } + fs::write( + format!("images/{}/metadata.json", title), + serde_json::to_string(&metadata).unwrap(), + ) + .unwrap(); } async fn select_manga_from_search(client: &MangaClient, results: &[Manga]) -> u16 { - let cover_ex = match client.cover_size { - util::CoverSize::Full => "", - util::CoverSize::W256 => ".256.jpg", - util::CoverSize::W512 => ".512.jpg", + let cover_ex = match &client.search { + Search::Query(q) => match &q.cover { + Some(s) => match s { + CoverSize::Full => "", + CoverSize::W256 => ".256.jpg", + CoverSize::W512 => ".512.jpg", + }, + _ => "", + }, + _ => "", }; let mut entry_futures: Vec>>> = Vec::new(); @@ -115,31 +186,34 @@ async fn select_manga_from_search(client: &MangaClient, results: &[Manga]) -> u1 if let Some(volumes) = result.attributes.last_volume { entry.add_info("volumes", volumes); } - if let Some(cover_data) = &result.relationships[2].attributes { - // The lib used for converting to sixel is abysmally slow for larger images, this - // should be in a future to allow for multithreaded work - // Cover data should only be present if used - assert!(client.cover); - let future = async move { - let image_url = format!( - "https://uploads.mangadex.org/covers/{id}/{}{cover_ex}", - &cover_data.file_name - ); - let data = client - .client - .get(&image_url) - .send() - .await - .unwrap() - .bytes() - .await - .unwrap(); - let result = util::convert_to_sixel(&data); - entry.set_image(result); - entry - }; - entry_futures.push(Box::pin(future)); + if let Some(cover_data) = &result.relationships[2].attributes { + if matches!(&client.search, Search::Query(q) if q.cover.is_some()) { + // The lib used for converting to sixel is abysmally slow for larger images, this + // should be in a future to allow for multithreaded work + let future = async move { + let image_url = format!( + "https://uploads.mangadex.org/covers/{id}/{}{cover_ex}", + cover_data.file_name + ); + let data = client + .client + .get(&image_url) + .send() + .await + .unwrap() + .bytes() + .await + .unwrap(); + let result = util::convert_to_sixel(&data); + + entry.set_image(result); + entry + }; + entry_futures.push(Box::pin(future)); + } else { + entry_futures.push(Box::pin(async move { entry })); + } } else { entry_futures.push(Box::pin(async move { entry })); } @@ -154,7 +228,7 @@ async fn select_manga_from_search(client: &MangaClient, results: &[Manga]) -> u1 Ok(v) => v, Err(e) => { eprintln!("ERROR: Failed to select: {e:?}"); - std::process::exit(1); + process::exit(1); } }; choice diff --git a/src/metadata.rs b/src/metadata.rs index fb6eae4..033dad1 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -2,8 +2,9 @@ use serde::Serialize; use crate::response_deserializer::{ - Chapter, ContentRating, Language, Manga, PublicationDemographic, State, Status, Titles, + Chapter, ContentRating, Manga, PublicationDemographic, State, Status, Titles, }; +use crate::language::Language; use chrono::{DateTime, FixedOffset}; #[derive(Serialize)] @@ -26,7 +27,7 @@ pub struct Metadata { struct ChapterMetadata { chapter: f32, volume: f32, - title: String, + title: Option, pages: u32, } diff --git a/src/response_deserializer.rs b/src/response_deserializer.rs index c8de26a..444dc05 100644 --- a/src/response_deserializer.rs +++ b/src/response_deserializer.rs @@ -1,258 +1,22 @@ // TODO: Remove this #![allow(unused)] use crate::error::*; +use crate::language::Language; use chrono::{DateTime, FixedOffset}; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use std::fmt; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Deserialize)] pub struct Id(pub String); -#[derive(Debug, PartialEq)] +#[derive(Deserialize, Debug, PartialEq)] +#[serde(rename_all = "lowercase")] pub enum ResponseResult { Ok, } -// https://api.mangadex.org/docs/3-enumerations/ -// https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes -// https://www.loc.gov/standards/iso639-2/php/English_list.php -// This is a fucking mess, I hate this. I could have just used a library, but I would have to adapt -// it to the special cases which mangadex require. -// The two-letter codes are not unique, and the code esentially just choose the first one in the -// list. Why have identifiers if they are not unique? Who knows what they were smoking when making -// this. Also Why is there Bouth SouthNdebele and Ndebele, South? Who knows? Why is there Bokmål -// and Norwegian Bokmål, many questions to ask, but few answers to get. Best part of this is that -// the updated ISO pdf is behind a paywall. https://www.iso.org/standard/74575.html -// If the code works 98% of the time and is 95% correct I guess that is "good enough". -// There are a bunch of duplicates, I don't know if I wan't to fix it even. -#[allow(unused)] -#[derive(Debug, Default, Serialize)] -pub enum Language { - Abkhazian, - Afar, - Afrikaans, - Akan, - Albanian, - Amharic, - Arabic, - Aragonese, - Armenian, - Assamese, - Avaric, - Avestan, - Aymara, - Azerbaijani, - Bambara, - Bashkir, - Basque, - Belarusian, - Bengali, - Bislama, - Bosnian, - BrazilianPortugese, - Breton, - Bulgarian, - Burmese, - Castilian, - CastilianSpanish, - Catalan, - Central, - Chamorro, - Chechen, - Chewa, - Chichewa, - SimplifiedChinese, - TraditionalChinese, - Chuang, - ChurchSlavic, - ChurchSlavonic, - Chuvash, - Cornish, - Corsican, - Cree, - Croatian, - Czech, - Danish, - Dhivehi, - Divehi, - Dutch, - Dzongkha, - #[default] - English, - Esperanto, - Estonian, - Ewe, - Faroese, - Fijian, - Finnish, - Flemish, - French, - Fulah, - Gaelic, - Galician, - Ganda, - Georgian, - German, - Gikuyu, - Greek, - Greenlandic, - Guarani, - Gujarati, - Haitian, - Hausa, - Hebrew, - Herero, - Hindi, - Hiri, - Hungarian, - Icelandic, - Ido, - Igbo, - Indonesian, - Interlingua, - Interlingue, - Inuktitut, - Inupiaq, - Irish, - Italian, - Japanese, - Javanese, - Kalaallisut, - Kannada, - Kanuri, - Kashmiri, - Kazakh, - Kikuyu, - Kinyarwanda, - Kirghiz, - Komi, - Kongo, - Korean, - Kuanyama, - Kurdish, - Kwanyama, - Kyrgyz, - Lao, - Latin, - LatinAmericanSpanish, - Latvian, - Letzeburgesch, - Limburgan, - Limburger, - Limburgish, - Lingala, - Lithuanian, - LubaKatanga, - Luxembourgish, - Macedonian, - Malagasy, - Malay, - Malayalam, - Maldivian, - Maltese, - Manx, - Maori, - Marathi, - Marshallese, - MiMoldavian, - Moldovan, - Mongolian, - NNauru, - Navaho, - Navajo, - NdebeleNorth, - NdebeleSouth, - Ndonga, - Nepali, - North, - Northern, - Norwegian, - NorwegianBokmål, - NorwegianNynorsk, - Nuosu, - Nyanja, - Occidental, - Occitan, - Ojibwa, - OldBulgarian, - OldChurchSlavonic, - OldSlavonic, - Oriya, - Oromo, - Ossetian, - Ossetic, - Pali, - Panjabi, - Pashto, - Persian, - Polish, - Portuguese, - ProvençPunjabi, - Pushto, - Quechua, - Romanian, - RomanizedJapanese, - RomanizedKorean, - RomanizedChinese, - Romansh, - Rundi, - Russian, - Samoan, - Sango, - Sanskrit, - Sardinian, - Scottish, - Serbian, - Shona, - Sichuan, - Sindhi, - Sinhala, - Sinhalese, - Slovak, - Slovenian, - Somali, - Sotho, - Spanish, - Sundanese, - Swahili, - Swati, - Swedish, - Tagalog, - Tahitian, - Tajik, - Tamil, - Tatar, - Telugu, - Thai, - Tibetan, - Tigrinya, - Tonga, - Tsonga, - Tswana, - Turkish, - Turkmen, - Twi, - Uighur, - Ukrainian, - Urdu, - Uyghur, - Uzbek, - Valencian, - Venda, - Vietnamese, - Volapük, - Walloon, - Welsh, - Western, - Wolof, - Xhosa, - Yiddish, - Yoruba, - Zhuang, - Zulu, -} - -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] pub enum Status { Completed, Ongoing, @@ -260,7 +24,8 @@ pub enum Status { Cancelled, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] pub enum ContentRating { Safe, Suggestive, @@ -276,18 +41,21 @@ pub enum PublicationDemographic { Josei, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] pub enum State { Published, } -#[derive(Debug)] +#[derive(Debug, Deserialize)] +#[serde(rename_all = "lowercase")] pub enum Response { Collection, Entity, } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Deserialize)] +#[serde(rename_all = "snake_case")] pub enum DataType { Manga, Chapter, @@ -301,45 +69,26 @@ pub enum DataType { Creator, } -#[derive(Debug)] -pub struct SearchResult { - pub result: ResponseResult, - pub response: Response, - pub data: Vec, -} - -#[derive(Debug)] -pub struct IdQueryResult { - pub result: ResponseResult, - pub response: Response, - pub data: Manga, -} - #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] -struct ChapterImagesContent { - result: String, - base_url: String, - chapter: ChapterImageDataContent, -} - -#[derive(Debug, Deserialize)] -struct ChapterImageDataContent { - hash: String, - data: Vec, -} - -pub struct ChapterImageData { - pub hash: String, - pub data: Vec, -} - +#[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, + pub data_saver: Vec, +} + +#[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] pub struct ChapterFeed { pub result: ResponseResult, pub response: Response, @@ -350,25 +99,87 @@ pub struct ChapterFeed { } #[derive(Deserialize, Debug)] -struct ChapterFeedResponse { - result: String, - response: String, - data: Vec, - limit: u32, - offset: u32, - total: u32, +#[serde(deny_unknown_fields)] +pub struct Chapter { + pub id: Id, + #[serde(rename = "type")] + pub data_type: DataType, + pub attributes: ChapterAttributes, + pub relationships: Vec, } -#[derive(Debug)] +#[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, + #[serde(deserialize_with = "opt_string_to_opt_f32")] + pub chapter: Option, + pub title: Option, + #[serde(deserialize_with = "string_to_language")] + pub translated_language: Language, + pub external_url: Option, + pub is_unavailable: bool, + #[serde(deserialize_with = "string_to_datetime")] + #[serde(rename = "publishAt")] + pub published_at: DateTime, + #[serde(deserialize_with = "string_to_datetime")] + pub readable_at: DateTime, + #[serde(deserialize_with = "string_to_datetime")] + pub created_at: DateTime, + #[serde(deserialize_with = "string_to_datetime")] + pub updated_at: DateTime, + 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, + 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, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] pub struct MangaAttributes { pub title: Titles, pub alt_titles: Vec, pub description: Description, pub is_locked: bool, pub links: Option, + pub official_links: Option>, + #[serde(deserialize_with = "string_to_language")] pub original_language: Language, + #[serde(deserialize_with = "opt_string_to_opt_f32")] pub last_volume: Option, + #[serde(deserialize_with = "opt_string_to_opt_f32")] pub last_chapter: Option, + #[serde(deserialize_with = "opt_string_to_opt_demographic")] pub publication_demographic: Option, pub status: Status, pub year: Option, @@ -376,183 +187,56 @@ pub struct MangaAttributes { pub tags: Vec, pub state: State, pub chapter_numbers_reset_on_new_volume: bool, + #[serde(deserialize_with = "string_to_datetime")] pub created_at: DateTime, + #[serde(deserialize_with = "string_to_datetime")] pub updated_at: DateTime, pub version: u32, + #[serde(deserialize_with = "vec_opt_string_to_vec_opt_language")] pub available_translated_languages: Vec>, + #[serde(deserialize_with = "opt_string_to_opt_id")] pub latest_uploaded_chapter: Option, } -#[derive(Deserialize, Debug)] -struct ChapterContent { - id: String, - #[serde(rename = "type")] - type_name: String, - attributes: ChapterAttributesContent, - relationships: Vec, -} - #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] -struct ChapterAttributesContent { - volume: Option, - chapter: Option, - title: Option, - translated_language: String, - external_url: Option, - publish_at: String, - readable_at: String, - created_at: String, - updated_at: String, - pages: u32, - version: u32, -} - -#[derive(Debug)] -pub struct Chapter { - pub id: Id, - pub data_type: DataType, - pub attributes: ChapterAttributes, - pub relationships: Vec, -} - -#[derive(Debug)] -pub struct ChapterAttributes { - pub volume: Option, - pub chapter: Option, - pub title: Option, - pub translated_language: Language, - pub external_url: Option, - pub published_at: DateTime, - pub created_at: DateTime, - pub updated_at: DateTime, - pub pages: u32, - pub version: u32, -} - -#[derive(Debug)] -pub struct Manga { - pub id: Id, - pub data_type: DataType, - pub attributes: MangaAttributes, - pub relationships: Vec, -} - -#[derive(Deserialize, Debug)] -struct SearchResponse { - result: String, - response: String, - data: Vec, - limit: u32, - offset: u32, - total: u32, -} - -#[derive(Deserialize, Debug)] -struct IdQueryResponse { - result: String, - response: String, - data: ContentData, -} - -#[derive(Deserialize, Debug)] -struct ContentData { - id: String, - #[serde(rename = "type")] - type_name: String, - attributes: ContentAttributes, - relationships: Vec, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct ContentAttributes { - title: Titles, - alt_titles: Vec, - description: Description, - is_locked: bool, - links: Option, - original_language: String, - last_volume: Option, - last_chapter: Option, - publication_demographic: Option, - status: String, - year: Option, - content_rating: String, - tags: Vec, - state: String, - chapter_numbers_reset_on_new_volume: bool, - created_at: String, - updated_at: String, - version: u32, - available_translated_languages: Vec>, - latest_uploaded_chapter: Option, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct ContentCoverAttributes { - description: String, - volume: Option, - file_name: String, - locale: String, - created_at: String, - updated_at: String, - version: u32, -} - -#[derive(Debug)] +#[serde(deny_unknown_fields)] pub struct CoverAttributes { pub description: String, + #[serde(deserialize_with = "opt_string_to_opt_f32")] pub volume: Option, pub file_name: Id, + #[serde(deserialize_with = "string_to_language")] pub locale: Language, + #[serde(deserialize_with = "string_to_datetime")] pub created_at: DateTime, + #[serde(deserialize_with = "string_to_datetime")] pub updated_at: DateTime, pub version: u32, } #[derive(Deserialize, Debug)] -pub struct ContentTag { - id: String, - #[serde(rename = "type")] - type_name: String, - attributes: TagAttributes, - relationships: Vec, -} - -#[derive(Debug)] +#[serde(deny_unknown_fields)] pub struct Tag { pub id: Id, + #[serde(rename = "type")] pub data_type: DataType, pub attributes: TagAttributes, pub relationships: Vec, } #[derive(Deserialize, Debug)] -struct ContentRelationship { - id: String, - #[serde(rename = "type")] - type_name: String, - related: Option, - attributes: Option, -} - -#[derive(Debug)] -pub struct ChapterRelationship { - pub id: Id, - pub data_type: DataType, -} - -#[derive(Debug)] +#[serde(deny_unknown_fields)] pub struct Relationship { pub id: Id, + #[serde(rename = "type")] pub data_type: DataType, pub related: Option, pub attributes: Option, } #[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] pub struct TagAttributes { pub name: TagName, pub description: Description, @@ -561,11 +245,13 @@ pub struct TagAttributes { } #[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, ap: Option, @@ -577,22 +263,34 @@ pub struct Links { ebj: Option, mal: Option, engtl: Option, + raw: Option, + nu: Option, } #[derive(Deserialize, Debug)] +// #[serde(deny_unknown_fields)] +// TODO: Fill pub struct Description { pub en: Option, pub ru: Option, } #[derive(Deserialize, Debug)] +// #[serde(deny_unknown_fields)] +// TODO: Fill pub struct AltTitles { en: Option, ja: Option, ru: Option, + de: Option, + lt: Option, + ar: Option, + zh: Option, + hi: Option, } #[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] pub struct Titles { pub en: String, } @@ -602,7 +300,7 @@ impl TryFrom<&str> for State { fn try_from(s: &str) -> Result { Ok(match s { - "published" => State::Published, + "published" => Self::Published, _ => return Err(()), }) } @@ -613,263 +311,25 @@ impl TryFrom<&str> for ContentRating { fn try_from(s: &str) -> Result { Ok(match s { - "safe" => ContentRating::Safe, - "suggestive" => ContentRating::Suggestive, - "erotica" => ContentRating::Erotica, - "pornographic" => ContentRating::Pornographic, - _ => return Err(()), - }) - } -} - -impl TryFrom<&str> for Language { - type Error = (); - - fn try_from(s: &str) -> Result { - #[allow(unused)] - Ok(match s { - "ab" => Language::Abkhazian, - "aa" => Language::Afar, - "af" => Language::Afrikaans, - "ak" => Language::Akan, - "sq" => Language::Albanian, - "am" => Language::Amharic, - "ar" => Language::Arabic, - "an" => Language::Aragonese, - "hy" => Language::Armenian, - "as" => Language::Assamese, - "av" => Language::Avaric, - "ae" => Language::Avestan, - "ay" => Language::Aymara, - "az" => Language::Azerbaijani, - "bm" => Language::Bambara, - "ba" => Language::Bashkir, - "eu" => Language::Basque, - "be" => Language::Belarusian, - "bn" => Language::Bengali, - "bi" => Language::Bislama, - "nb" => Language::NorwegianBokmål, - "bs" => Language::Bosnian, - "br" => Language::Breton, - "bg" => Language::Bulgarian, - "my" => Language::Burmese, - "es" => Language::Castilian, - "ca" => Language::Catalan, - "km" => Language::Central, - "ch" => Language::Chamorro, - "ce" => Language::Chechen, - "ny" => Language::Chewa, - "ny" => Language::Chichewa, - "zh" => Language::SimplifiedChinese, - "za" => Language::Chuang, - "cu" => Language::ChurchSlavic, - "cu" => Language::ChurchSlavonic, - "cv" => Language::Chuvash, - "kw" => Language::Cornish, - "co" => Language::Corsican, - "cr" => Language::Cree, - "hr" => Language::Croatian, - "cs" => Language::Czech, - "da" => Language::Danish, - "dv" => Language::Dhivehi, - "dv" => Language::Divehi, - "nl" => Language::Dutch, - "dz" => Language::Dzongkha, - "en" => Language::English, - "eo" => Language::Esperanto, - "et" => Language::Estonian, - "ee" => Language::Ewe, - "fo" => Language::Faroese, - "fj" => Language::Fijian, - "fi" => Language::Finnish, - "nl" => Language::Flemish, - "fr" => Language::French, - "ff" => Language::Fulah, - "gd" => Language::Gaelic, - "gl" => Language::Galician, - "lg" => Language::Ganda, - "ka" => Language::Georgian, - "de" => Language::German, - "ki" => Language::Gikuyu, - "el" => Language::Greek, - "kl" => Language::Greenlandic, - "gn" => Language::Guarani, - "gu" => Language::Gujarati, - "ht" => Language::Haitian, - "ha" => Language::Hausa, - "he" => Language::Hebrew, - "hz" => Language::Herero, - "hi" => Language::Hindi, - "ho" => Language::Hiri, - "hu" => Language::Hungarian, - "is" => Language::Icelandic, - "io" => Language::Ido, - "ig" => Language::Igbo, - "id" => Language::Indonesian, - "ia" => Language::Interlingua, - "ie" => Language::Interlingue, - "iu" => Language::Inuktitut, - "ik" => Language::Inupiaq, - "ga" => Language::Irish, - "it" => Language::Italian, - "ja" => Language::Japanese, - "jv" => Language::Javanese, - "kl" => Language::Kalaallisut, - "kn" => Language::Kannada, - "kr" => Language::Kanuri, - "ks" => Language::Kashmiri, - "kk" => Language::Kazakh, - "ki" => Language::Kikuyu, - "rw" => Language::Kinyarwanda, - "ky" => Language::Kirghiz, - "kv" => Language::Komi, - "kg" => Language::Kongo, - "ko" => Language::Korean, - "kj" => Language::Kuanyama, - "ku" => Language::Kurdish, - "kj" => Language::Kwanyama, - "ky" => Language::Kyrgyz, - "lo" => Language::Lao, - "la" => Language::Latin, - "lv" => Language::Latvian, - "lb" => Language::Letzeburgesch, - "li" => Language::Limburgan, - "li" => Language::Limburger, - "li" => Language::Limburgish, - "ln" => Language::Lingala, - "lt" => Language::Lithuanian, - "lu" => Language::LubaKatanga, - "lb" => Language::Luxembourgish, - "mk" => Language::Macedonian, - "mg" => Language::Malagasy, - "ms" => Language::Malay, - "ml" => Language::Malayalam, - "dv" => Language::Maldivian, - "mt" => Language::Maltese, - "gv" => Language::Manx, - "mi" => Language::Maori, - "mr" => Language::Marathi, - "mh" => Language::Marshallese, - "ro" => Language::MiMoldavian, - "ro" => Language::Moldovan, - "mn" => Language::Mongolian, - "na" => Language::NNauru, - "nv" => Language::Navaho, - "nv" => Language::Navajo, - "nd" => Language::NdebeleNorth, - "nr" => Language::NdebeleSouth, - "ng" => Language::Ndonga, - "ne" => Language::Nepali, - "nd" => Language::North, - "se" => Language::Northern, - "no" => Language::Norwegian, - "nb" => Language::NorwegianBokmål, - "nn" => Language::NorwegianNynorsk, - "ii" => Language::Nuosu, - "ny" => Language::Nyanja, - "nn" => Language::NorwegianNynorsk, - "ie" => Language::Occidental, - "oc" => Language::Occitan, - "oj" => Language::Ojibwa, - "cu" => Language::OldBulgarian, - "cu" => Language::OldChurchSlavonic, - "cu" => Language::OldSlavonic, - "or" => Language::Oriya, - "om" => Language::Oromo, - "os" => Language::Ossetian, - "os" => Language::Ossetic, - "pi" => Language::Pali, - "pa" => Language::Panjabi, - "ps" => Language::Pashto, - "fa" => Language::Persian, - "pl" => Language::Polish, - "pt" => Language::Portuguese, - "pa" => Language::ProvençPunjabi, - "ps" => Language::Pushto, - "qu" => Language::Quechua, - "ro" => Language::Romanian, - "rm" => Language::Romansh, - "rn" => Language::Rundi, - "ru" => Language::Russian, - "sm" => Language::Samoan, - "sg" => Language::Sango, - "sa" => Language::Sanskrit, - "sc" => Language::Sardinian, - "gd" => Language::Scottish, - "sr" => Language::Serbian, - "sn" => Language::Shona, - "ii" => Language::Sichuan, - "sd" => Language::Sindhi, - "si" => Language::Sinhala, - "si" => Language::Sinhalese, - "sk" => Language::Slovak, - "sl" => Language::Slovenian, - "so" => Language::Somali, - "st" => Language::Sotho, - "es" => Language::Spanish, - "su" => Language::Sundanese, - "sw" => Language::Swahili, - "ss" => Language::Swati, - "sv" => Language::Swedish, - "tl" => Language::Tagalog, - "ty" => Language::Tahitian, - "tg" => Language::Tajik, - "ta" => Language::Tamil, - "tt" => Language::Tatar, - "te" => Language::Telugu, - "th" => Language::Thai, - "bo" => Language::Tibetan, - "ti" => Language::Tigrinya, - "to" => Language::Tonga, - "ts" => Language::Tsonga, - "tn" => Language::Tswana, - "tr" => Language::Turkish, - "tk" => Language::Turkmen, - "tw" => Language::Twi, - "ug" => Language::Uighur, - "uk" => Language::Ukrainian, - "ur" => Language::Urdu, - "ug" => Language::Uyghur, - "uz" => Language::Uzbek, - "ca" => Language::Valencian, - "ve" => Language::Venda, - "vi" => Language::Vietnamese, - "vo" => Language::Volapük, - "wa" => Language::Walloon, - "cy" => Language::Welsh, - "fy" => Language::Western, - "wo" => Language::Wolof, - "xh" => Language::Xhosa, - "yi" => Language::Yiddish, - "yo" => Language::Yoruba, - "za" => Language::Zhuang, - "zu" => Language::Zulu, - - "zh-ro" => Language::RomanizedChinese, - "zh" => Language::SimplifiedChinese, - "zh-hk" => Language::TraditionalChinese, - "ko-ro" => Language::RomanizedKorean, - "pt" => Language::Portuguese, - "es" => Language::CastilianSpanish, - "es-la" => Language::LatinAmericanSpanish, - "ja-ro" => Language::RomanizedJapanese, - "pt-br" => Language::BrazilianPortugese, - + "safe" => Self::Safe, + "suggestive" => Self::Suggestive, + "erotica" => Self::Erotica, + "pornographic" => Self::Pornographic, _ => return Err(()), }) } } impl TryFrom<&str> for PublicationDemographic { - type Error = (); + type Error = PublicationDemographicError; fn try_from(s: &str) -> Result { Ok(match s { - "shounen" => PublicationDemographic::Shounen, - "josei" => PublicationDemographic::Josei, - "shoujo" => PublicationDemographic::Shoujo, - "seinen" => PublicationDemographic::Seinen, - _ => return Err(()), + "shounen" => Self::Shounen, + "josei" => Self::Josei, + "shoujo" => Self::Shoujo, + "seinen" => Self::Seinen, + _ => return Err(Self::Error::InvalidValue), }) } } @@ -879,16 +339,16 @@ impl TryFrom<&str> for DataType { fn try_from(s: &str) -> Result { Ok(match s { - "manga" => DataType::Manga, - "chapter" => DataType::Chapter, - "cover_art" => DataType::CoverArt, - "author" => DataType::Author, - "artist" => DataType::Artist, - "scanlation_group" => DataType::ScanlationGroup, - "tag" => DataType::Tag, - "user" => DataType::User, - "custom_list" => DataType::CustomList, - "creator" => DataType::Creator, + "manga" => Self::Manga, + "chapter" => Self::Chapter, + "cover_art" => Self::CoverArt, + "author" => Self::Author, + "artist" => Self::Artist, + "scanlation_group" => Self::ScanlationGroup, + "tag" => Self::Tag, + "user" => Self::User, + "custom_list" => Self::CustomList, + "creator" => Self::Creator, _ => return Err(()), }) } @@ -899,10 +359,10 @@ impl TryFrom<&str> for Status { fn try_from(s: &str) -> Result { Ok(match s { - "ongoing" => Status::Ongoing, - "completed" => Status::Completed, - "hiatus" => Status::Hiatus, - "cancelled" => Status::Cancelled, + "ongoing" => Self::Ongoing, + "completed" => Self::Completed, + "hiatus" => Self::Hiatus, + "cancelled" => Self::Cancelled, _ => return Err(()), }) } @@ -913,7 +373,7 @@ impl TryFrom<&str> for ResponseResult { fn try_from(s: &str) -> Result { match s { - "ok" => Ok(ResponseResult::Ok), + "ok" => Ok(Self::Ok), _ => Err(()), } } @@ -924,270 +384,13 @@ impl TryFrom<&str> for Response { fn try_from(s: &str) -> Result { match s { - "collection" => Ok(Response::Collection), - "entity" => Ok(Response::Entity), + "collection" => Ok(Self::Collection), + "entity" => Ok(Self::Entity), _ => Err(()), } } } -impl TryFrom for SearchResult { - type Error = ResponseConversionError; - fn try_from(search_response: SearchResponse) -> Result { - let response = (search_response.response.as_str()) - .try_into() - .map_err(|_| ResponseConversionError::Result(search_response.response))?; - let result: ResponseResult = (search_response.result.as_str()) - .try_into() - .map_err(|_| ResponseConversionError::Result(search_response.result))?; - - let data: Result, Self::Error> = search_response - .data - .into_iter() - .map(|m| m.try_into()) - .collect(); - Ok(SearchResult { - response, - result, - data: data?, - }) - } -} - -impl fmt::Display for Language { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - #[allow(unused)] - match self { - Language::Abkhazian => "ab".fmt(f), - Language::Afar => "aa".fmt(f), - Language::Afrikaans => "af".fmt(f), - Language::Akan => "ak".fmt(f), - Language::Albanian => "sq".fmt(f), - Language::Amharic => "am".fmt(f), - Language::Arabic => "ar".fmt(f), - Language::Aragonese => "an".fmt(f), - Language::Armenian => "hy".fmt(f), - Language::Assamese => "as".fmt(f), - Language::Avaric => "av".fmt(f), - Language::Avestan => "ae".fmt(f), - Language::Aymara => "ay".fmt(f), - Language::Azerbaijani => "az".fmt(f), - Language::Bambara => "bm".fmt(f), - Language::Bashkir => "ba".fmt(f), - Language::Basque => "eu".fmt(f), - Language::Belarusian => "be".fmt(f), - Language::Bengali => "bn".fmt(f), - Language::Bislama => "bi".fmt(f), - Language::NorwegianBokmål => "nb".fmt(f), - Language::Bosnian => "bs".fmt(f), - Language::Breton => "br".fmt(f), - Language::Bulgarian => "bg".fmt(f), - Language::Burmese => "my".fmt(f), - Language::Castilian => "es".fmt(f), - Language::Catalan => "ca".fmt(f), - Language::Central => "km".fmt(f), - Language::Chamorro => "ch".fmt(f), - Language::Chechen => "ce".fmt(f), - Language::Chewa => "ny".fmt(f), - Language::Chichewa => "ny".fmt(f), - Language::SimplifiedChinese => "zh".fmt(f), - Language::Chuang => "za".fmt(f), - Language::ChurchSlavic => "cu".fmt(f), - Language::ChurchSlavonic => "cu".fmt(f), - Language::Chuvash => "cv".fmt(f), - Language::Cornish => "kw".fmt(f), - Language::Corsican => "co".fmt(f), - Language::Cree => "cr".fmt(f), - Language::Croatian => "hr".fmt(f), - Language::Czech => "cs".fmt(f), - Language::Danish => "da".fmt(f), - Language::Dhivehi => "dv".fmt(f), - Language::Divehi => "dv".fmt(f), - Language::Dutch => "nl".fmt(f), - Language::Dzongkha => "dz".fmt(f), - Language::English => "en".fmt(f), - Language::Esperanto => "eo".fmt(f), - Language::Estonian => "et".fmt(f), - Language::Ewe => "ee".fmt(f), - Language::Faroese => "fo".fmt(f), - Language::Fijian => "fj".fmt(f), - Language::Finnish => "fi".fmt(f), - Language::Flemish => "nl".fmt(f), - Language::French => "fr".fmt(f), - Language::Fulah => "ff".fmt(f), - Language::Gaelic => "gd".fmt(f), - Language::Galician => "gl".fmt(f), - Language::Ganda => "lg".fmt(f), - Language::Georgian => "ka".fmt(f), - Language::German => "de".fmt(f), - Language::Gikuyu => "ki".fmt(f), - Language::Greek => "el".fmt(f), - Language::Greenlandic => "kl".fmt(f), - Language::Guarani => "gn".fmt(f), - Language::Gujarati => "gu".fmt(f), - Language::Haitian => "ht".fmt(f), - Language::Hausa => "ha".fmt(f), - Language::Hebrew => "he".fmt(f), - Language::Herero => "hz".fmt(f), - Language::Hindi => "hi".fmt(f), - Language::Hiri => "ho".fmt(f), - Language::Hungarian => "hu".fmt(f), - Language::Icelandic => "is".fmt(f), - Language::Ido => "io".fmt(f), - Language::Igbo => "ig".fmt(f), - Language::Indonesian => "id".fmt(f), - Language::Interlingua => "ia".fmt(f), - Language::Interlingue => "ie".fmt(f), - Language::Inuktitut => "iu".fmt(f), - Language::Inupiaq => "ik".fmt(f), - Language::Irish => "ga".fmt(f), - Language::Italian => "it".fmt(f), - Language::Japanese => "ja".fmt(f), - Language::Javanese => "jv".fmt(f), - Language::Kalaallisut => "kl".fmt(f), - Language::Kannada => "kn".fmt(f), - Language::Kanuri => "kr".fmt(f), - Language::Kashmiri => "ks".fmt(f), - Language::Kazakh => "kk".fmt(f), - Language::Kikuyu => "ki".fmt(f), - Language::Kinyarwanda => "rw".fmt(f), - Language::Kirghiz => "ky".fmt(f), - Language::Komi => "kv".fmt(f), - Language::Kongo => "kg".fmt(f), - Language::Korean => "ko".fmt(f), - Language::Kuanyama => "kj".fmt(f), - Language::Kurdish => "ku".fmt(f), - Language::Kwanyama => "kj".fmt(f), - Language::Kyrgyz => "ky".fmt(f), - Language::Lao => "lo".fmt(f), - Language::Latin => "la".fmt(f), - Language::Latvian => "lv".fmt(f), - Language::Letzeburgesch => "lb".fmt(f), - Language::Limburgan => "li".fmt(f), - Language::Limburger => "li".fmt(f), - Language::Limburgish => "li".fmt(f), - Language::Lingala => "ln".fmt(f), - Language::Lithuanian => "lt".fmt(f), - Language::LubaKatanga => "lu".fmt(f), - Language::Luxembourgish => "lb".fmt(f), - Language::Macedonian => "mk".fmt(f), - Language::Malagasy => "mg".fmt(f), - Language::Malay => "ms".fmt(f), - Language::Malayalam => "ml".fmt(f), - Language::Maldivian => "dv".fmt(f), - Language::Maltese => "mt".fmt(f), - Language::Manx => "gv".fmt(f), - Language::Maori => "mi".fmt(f), - Language::Marathi => "mr".fmt(f), - Language::Marshallese => "mh".fmt(f), - Language::MiMoldavian => "ro".fmt(f), - Language::Moldovan => "ro".fmt(f), - Language::Mongolian => "mn".fmt(f), - Language::NNauru => "na".fmt(f), - Language::Navaho => "nv".fmt(f), - Language::Navajo => "nv".fmt(f), - Language::NdebeleNorth => "nd".fmt(f), - Language::NdebeleSouth => "nr".fmt(f), - Language::Ndonga => "ng".fmt(f), - Language::Nepali => "ne".fmt(f), - Language::North => "nd".fmt(f), - Language::Northern => "se".fmt(f), - Language::Norwegian => "no".fmt(f), - Language::NorwegianBokmål => "nb".fmt(f), - Language::NorwegianNynorsk => "nn".fmt(f), - Language::Nuosu => "ii".fmt(f), - Language::Nyanja => "ny".fmt(f), - Language::NorwegianNynorsk => "nn".fmt(f), - Language::Occidental => "ie".fmt(f), - Language::Occitan => "oc".fmt(f), - Language::Ojibwa => "oj".fmt(f), - Language::OldBulgarian => "cu".fmt(f), - Language::OldChurchSlavonic => "cu".fmt(f), - Language::OldSlavonic => "cu".fmt(f), - Language::Oriya => "or".fmt(f), - Language::Oromo => "om".fmt(f), - Language::Ossetian => "os".fmt(f), - Language::Ossetic => "os".fmt(f), - Language::Pali => "pi".fmt(f), - Language::Panjabi => "pa".fmt(f), - Language::Pashto => "ps".fmt(f), - Language::Persian => "fa".fmt(f), - Language::Polish => "pl".fmt(f), - Language::Portuguese => "pt".fmt(f), - Language::ProvençPunjabi => "pa".fmt(f), - Language::Pushto => "ps".fmt(f), - Language::Quechua => "qu".fmt(f), - Language::Romanian => "ro".fmt(f), - Language::Romansh => "rm".fmt(f), - Language::Rundi => "rn".fmt(f), - Language::Russian => "ru".fmt(f), - Language::Samoan => "sm".fmt(f), - Language::Sango => "sg".fmt(f), - Language::Sanskrit => "sa".fmt(f), - Language::Sardinian => "sc".fmt(f), - Language::Scottish => "gd".fmt(f), - Language::Serbian => "sr".fmt(f), - Language::Shona => "sn".fmt(f), - Language::Sichuan => "ii".fmt(f), - Language::Sindhi => "sd".fmt(f), - Language::Sinhala => "si".fmt(f), - Language::Sinhalese => "si".fmt(f), - Language::Slovak => "sk".fmt(f), - Language::Slovenian => "sl".fmt(f), - Language::Somali => "so".fmt(f), - Language::Sotho => "st".fmt(f), - Language::Spanish => "es".fmt(f), - Language::Sundanese => "su".fmt(f), - Language::Swahili => "sw".fmt(f), - Language::Swati => "ss".fmt(f), - Language::Swedish => "sv".fmt(f), - Language::Tagalog => "tl".fmt(f), - Language::Tahitian => "ty".fmt(f), - Language::Tajik => "tg".fmt(f), - Language::Tamil => "ta".fmt(f), - Language::Tatar => "tt".fmt(f), - Language::Telugu => "te".fmt(f), - Language::Thai => "th".fmt(f), - Language::Tibetan => "bo".fmt(f), - Language::Tigrinya => "ti".fmt(f), - Language::Tonga => "to".fmt(f), - Language::Tsonga => "ts".fmt(f), - Language::Tswana => "tn".fmt(f), - Language::Turkish => "tr".fmt(f), - Language::Turkmen => "tk".fmt(f), - Language::Twi => "tw".fmt(f), - Language::Uighur => "ug".fmt(f), - Language::Ukrainian => "uk".fmt(f), - Language::Urdu => "ur".fmt(f), - Language::Uyghur => "ug".fmt(f), - Language::Uzbek => "uz".fmt(f), - Language::Valencian => "ca".fmt(f), - Language::Venda => "ve".fmt(f), - Language::Vietnamese => "vi".fmt(f), - Language::Volapük => "vo".fmt(f), - Language::Walloon => "wa".fmt(f), - Language::Welsh => "cy".fmt(f), - Language::Western => "fy".fmt(f), - Language::Wolof => "wo".fmt(f), - Language::Xhosa => "xh".fmt(f), - Language::Yiddish => "yi".fmt(f), - Language::Yoruba => "yo".fmt(f), - Language::Zhuang => "za".fmt(f), - Language::Zulu => "zu".fmt(f), - - Language::RomanizedChinese => "zh-ro".fmt(f), - Language::SimplifiedChinese => "zh".fmt(f), - Language::TraditionalChinese => "zh-hk".fmt(f), - Language::RomanizedKorean => "ko-ro".fmt(f), - Language::Portuguese => "pt".fmt(f), - Language::CastilianSpanish => "es".fmt(f), - Language::LatinAmericanSpanish => "es-la".fmt(f), - Language::RomanizedJapanese => "ja-ro".fmt(f), - Language::BrazilianPortugese => "pt-br".fmt(f), - } - } -} - impl fmt::Display for Id { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) @@ -1216,356 +419,102 @@ impl fmt::Display for ContentRating { } } -impl TryFrom for MangaAttributes { - type Error = AttributeConversionError; - fn try_from(attributes: ContentAttributes) -> Result { - Ok(MangaAttributes { - title: attributes.title, - alt_titles: attributes.alt_titles, - description: attributes.description, - is_locked: attributes.is_locked, - links: attributes.links, - original_language: (attributes.original_language.as_str()) - .try_into() - .map_err(|_| Self::Error::Language(attributes.original_language))?, - last_volume: match attributes.last_volume { - Some(s) => match s.parse() { - Ok(v) => Some(v), - Err(_) => { - if s.is_empty() { - None - } else { - return Err(Self::Error::LastVolume(s)); - } - } - }, - None => None, - }, - last_chapter: match attributes.last_chapter { - Some(n) => match n.parse() { - Ok(v) => Some(v), - Err(_) => { - if n.is_empty() { - None - } else { - return Err(Self::Error::LastVolume(n)); - } - } - }, - None => None, - }, - publication_demographic: match attributes.publication_demographic { - Some(s) => Some( - (s.as_str()) - .try_into() - .map_err(|_| Self::Error::PublicationDemographic(s))?, - ), - None => None, - }, - status: (attributes.status.as_str()) - .try_into() - .map_err(|_| Self::Error::Status(attributes.status))?, - year: attributes.year, - content_rating: attributes - .content_rating - .as_str() - .try_into() - .map_err(|_| Self::Error::ContentRating(attributes.content_rating))?, - tags: attributes - .tags - .into_iter() - .map(|m| { - Ok(Tag { - data_type: (m.type_name.as_str()) - .try_into() - .map_err(|_| Self::Error::DataType(m.type_name))?, - id: Id(m.id), - relationships: m - .relationships - .into_iter() - .map(|r| { - Ok::(Relationship { - id: Id(r.id), - data_type: (r.type_name.as_str()) - .try_into() - .map_err(|_| Self::Error::DataType(r.type_name))?, - // TODO: Do this - attributes: None, - related: r.related, - }) - }) - .collect::, Self::Error>>()?, - attributes: TagAttributes { - name: m.attributes.name, - group: m.attributes.group, - version: m.attributes.version, - description: Description { - en: m.attributes.description.en, - ru: m.attributes.description.ru, - }, - }, - }) - }) - .collect::, Self::Error>>()?, - state: (attributes.state.as_str()) - .try_into() - .map_err(|_| Self::Error::State(attributes.state))?, - chapter_numbers_reset_on_new_volume: attributes.chapter_numbers_reset_on_new_volume, - created_at: DateTime::parse_from_rfc3339(&attributes.created_at) - .map_err(|_| Self::Error::CreatedAtDateTime(attributes.created_at.clone()))?, - updated_at: DateTime::parse_from_rfc3339(&attributes.updated_at) - .map_err(|_| Self::Error::UpdatedAtDateTime(attributes.created_at))?, - version: attributes.version, - available_translated_languages: attributes - .available_translated_languages - .into_iter() - .map(|m| { - Ok(match m { - Some(s) => Some( - (s.as_str()) - .try_into() - .map_err(|_| Self::Error::Language(s))?, - ), - None => None, - }) - }) - .collect::>, Self::Error>>()?, - latest_uploaded_chapter: attributes - .latest_uploaded_chapter - .as_ref() - .map(|m| Id(m.clone())), - }) - } -} - pub fn deserialize_id_query(json: &str) -> Result { std::fs::write("id_query.json", json).unwrap(); - let id_query_response: IdQueryResponse = serde_json::from_str(json) + let id_query_response: IdQueryResult = serde_json::from_str(json) .map_err(|e| IdQueryResultError::Serde(JsonError::Message(e.to_string())))?; - id_query_response - .try_into() - .map_err(IdQueryResultError::IdQueryResult) -} - -impl TryFrom for IdQueryResult { - type Error = IdQueryResponseError; - fn try_from(response: IdQueryResponse) -> Result { - Ok(IdQueryResult { - result: response - .result - .as_str() - .try_into() - .map_err(|_| Self::Error::Result)?, - response: response - .response - .as_str() - .try_into() - .map_err(|_| Self::Error::Response)?, - data: response.data.try_into().map_err(Self::Error::Data)?, - }) - } + Ok(id_query_response) } pub fn deserialize_chapter_feed(json: &str) -> Result { - let chapter_feed_response: ChapterFeedResponse = match serde_json::from_str(json) { + let chapter_feed_response: ChapterFeed = match serde_json::from_str(json) { Ok(v) => v, Err(e) => { // TODO: Actually do error handling here return Err(ChapterFeedError::Serde(JsonError::Message(e.to_string()))); } }; - chapter_feed_response - .try_into() - .map_err(ChapterFeedError::Conversion) + Ok(chapter_feed_response) } pub fn deserialize_search(json: &str) -> Result { - let search_response: SearchResponse = serde_json::from_str(json) + let search_response: SearchResult = serde_json::from_str(json) .map_err(|e| SearchResultError::Serde(JsonError::Message(e.to_string())))?; - search_response - .try_into() - .map_err(SearchResultError::SearchResult) + Ok(search_response) } -impl TryFrom for ChapterFeed { - type Error = ChapterFeedConversionError; +pub fn deserialize_chapter_images(json: &str) -> ChapterImages { + serde_json::from_str(json).unwrap() +} - fn try_from(feed: ChapterFeedResponse) -> Result { - Ok(ChapterFeed { - result: (feed.result.as_str()) - .try_into() - .map_err(|_| Self::Error::Result(feed.result))?, - response: (feed.response.as_str()) - .try_into() - .map_err(|_| Self::Error::Result(feed.response))?, - data: feed - .data - .into_iter() - .map(|m| { - Ok(Chapter { - data_type: (m.type_name.as_str()) - .try_into() - .map_err(|_| ChapterConversionError::DataType(m.type_name))?, - id: Id(m.id), - attributes: m - .attributes - .try_into() - .map_err(ChapterConversionError::Attributes)?, - relationships: m - .relationships - .into_iter() - .map(|r| { - Ok(ChapterRelationship { - data_type: (r.type_name.as_str()).try_into().map_err(|_| { - ChapterRelationshipError::TypeData(r.type_name) - })?, - id: Id(r.id), - }) - }) - .collect::, ChapterRelationshipError>>() - .map_err(ChapterConversionError::Relationship)?, - }) - }) - .collect::, Self::Error>>()?, - limit: feed.limit, - offset: feed.offset, - total: feed.total, - }) +fn opt_string_to_opt_demographic<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let opt = Option::::deserialize(deserializer)?; + match opt { + Some(n) => Ok(Some( + n.as_str().try_into().map_err(serde::de::Error::custom)?, + )), + None => Ok(None), } } -impl TryFrom for ChapterAttributes { - type Error = ChapterAttributeConversionError; - fn try_from(attributes: ChapterAttributesContent) -> Result { - Ok(ChapterAttributes { - volume: match &attributes.volume { - Some(v) => match v.parse() { - Ok(n) => Some(n), - Err(_) => return Err(Self::Error::Volume(v.to_owned())), - }, - None => None, - }, - chapter: match &attributes.chapter { - Some(v) => match v.parse() { - Ok(v) => Some(v), - Err(_) => return Err(Self::Error::Chapter(v.to_owned())), - }, - None => None, - }, - created_at: DateTime::parse_from_rfc3339(&attributes.created_at) - .map_err(|_| Self::Error::CreatedAt(attributes.created_at))?, - published_at: DateTime::parse_from_rfc3339(&attributes.publish_at) - .map_err(|_| Self::Error::CreatedAt(attributes.publish_at))?, - updated_at: DateTime::parse_from_rfc3339(&attributes.updated_at) - .map_err(|_| Self::Error::CreatedAt(attributes.updated_at))?, - external_url: attributes.external_url, - title: attributes.title, - pages: attributes.pages, - translated_language: (attributes.translated_language.as_str()) - .try_into() - .map_err(|_| Self::Error::TranslatedLanguage(attributes.translated_language))?, - version: attributes.version, - }) - } +fn string_to_datetime<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + DateTime::parse_from_rfc3339(&s).map_err(serde::de::Error::custom) } -impl TryFrom for ChapterImages { - type Error = ChapterImageError; - fn try_from(data: ChapterImagesContent) -> Result { - Ok(ChapterImages { - result: (data.result.as_str()) - .try_into() - .map_err(|_| Self::Error::Result(data.result))?, - base_url: data.base_url, - chapter: ChapterImageData { - hash: data.chapter.hash, - data: data.chapter.data, - }, - }) - } -} - -pub fn deserialize_chapter_images(json: &str) -> Result { - let chapter_images: ChapterImagesContent = match serde_json::from_str(json) { - Ok(v) => v, - Err(e) => { - match serde_json::from_str::(json) { - Ok(v) => return Err(ChapterImagesError::Content(v)), - Err(f) => { - // If you can't parse the error then there is no point in continuing. - eprintln!("ERROR: {e:#?}, with {f:#?}"); - std::process::exit(1); - } +fn opt_string_to_opt_f32<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let opt = Option::::deserialize(deserializer)?; + match opt { + Some(s) => { + if s.is_empty() { + Ok(None) + } else { + Ok(Some(s.parse::().map_err(serde::de::Error::custom)?)) } } - }; - chapter_images.try_into().map_err(ChapterImagesError::Image) + None => Ok(None), + } } -impl TryFrom for Manga { - type Error = ResponseConversionError; - fn try_from(m: ContentData) -> Result { - Ok(Manga { - id: Id(m.id), - data_type: (m.type_name.as_str()).try_into().map_err(|_| { - Self::Error::Attribute(AttributeConversionError::DataType(m.type_name)) - })?, - attributes: m.attributes.try_into().map_err(Self::Error::Attribute)?, - relationships: m - .relationships - .into_iter() - .map(|m| { - Ok(Relationship { - id: Id(m.id), - data_type: m - .type_name - .as_str() - .try_into() - .map_err(|_| AttributeConversionError::DataType(m.type_name))?, - attributes: { - if let Some(attributes) = m.attributes { - Some(CoverAttributes { - created_at: DateTime::parse_from_rfc3339( - &attributes.created_at, - ) - .map_err(|_| { - AttributeConversionError::CreatedAtDateTime( - attributes.created_at.clone(), - ) - })?, - updated_at: DateTime::parse_from_rfc3339( - &attributes.created_at, - ) - .map_err(|_| { - AttributeConversionError::CreatedAtDateTime( - attributes.created_at.clone(), - ) - })?, - // TODO: Something should probably be done here - description: attributes.description, - file_name: Id(attributes.file_name.clone()), - locale: (attributes.locale.as_str()).try_into().map_err( - |_| { - AttributeConversionError::Locale( - attributes.locale.clone(), - ) - }, - )?, - version: attributes.version, - volume: match &attributes.volume { - Some(v) => v.parse().ok(), - None => None, - }, - }) - } else { - None - } - }, - related: m.related.clone(), - }) - }) - .collect::, AttributeConversionError>>()?, - }) +fn string_to_language<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + Ok(Language::from(s.as_str())) +} + +fn vec_opt_string_to_vec_opt_language<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + Ok(Vec::>::deserialize(deserializer)? + .into_iter() + .map(|m| m.map(|v| Language::from(v.as_str()))) + .collect()) +} + +fn opt_string_to_opt_id<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let opt = Option::::deserialize(deserializer)?; + match opt { + Some(n) => Ok(Some(Id(n))), + None => Ok(None), } } @@ -1602,6 +551,6 @@ mod tests { fn chapter_images() { let chapter_images = std::fs::read_to_string("test_data/chapter_images_hunter_x_hunter.json").unwrap(); - deserialize_chapter_images(&chapter_images).unwrap(); + let _ = deserialize_chapter_images(&chapter_images); } } diff --git a/src/util.rs b/src/util.rs index 2052b0d..f68921d 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,6 +1,8 @@ -use crate::error::{ChapterImageError, ChapterImagesError}; -use crate::response_deserializer::ChapterImages; -use crate::{Chapter, Id, BASE}; +use crate::{ + response_deserializer, + response_deserializer::ChapterImages, + Chapter, Id, BASE, +}; use icy_sixel::{DiffusionMethod, MethodForLargest, MethodForRep, PixelFormat, Quality}; use image::DynamicImage; use std::{fs::File, io, io::Write, path::Path}; @@ -72,7 +74,7 @@ pub struct Config { #[derive(Debug)] pub enum ConfigSearch { Query(String), - Id(crate::Id), + Id(Id), } fn filter_bonus(bonus: bool, volume: Option, chapter: Option) -> bool { @@ -311,39 +313,15 @@ pub async fn download_selected_chapters( ) { let mut items = Vec::new(); for (i, chapter) in selected_chapters.iter().enumerate() { - let chapter_image_data = loop { - let json = client - .get(format!("{BASE}/at-home/server/{}", chapter.id)) - .send() - .await - .unwrap() - .text() - .await - .unwrap(); - let result = crate::response_deserializer::deserialize_chapter_images(&json); - match result { - Ok(v) => break v, - Err(e) => match e { - ChapterImagesError::Image(i) => match i { - ChapterImageError::Result(s) => { - eprintln!("chapter image error: {s}"); - std::process::exit(1); - } - }, - ChapterImagesError::Content(e) => { - if e.result != "error" { - panic!("brotha, api gone wrong (wild)"); - } - for error in e.errors { - if error.status == 429 { - eprintln!("you sent too many requests"); - } - std::thread::sleep(std::time::Duration::from_millis(20000)); - } - } - }, - } - }; + let json = client + .get(format!("{BASE}/at-home/server/{}", chapter.id)) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + let chapter_image_data = response_deserializer::deserialize_chapter_images(&json); println!( "\x1b[1A\x1b[2Kdownloaded chapter json image data: [{i}/{}]", selected_chapters.len() @@ -360,7 +338,7 @@ pub async fn download_selected_chapters( .await .unwrap(); println!( - "\x1b[1A\x1b[2KDownloaded volume: {:?}, chapter: {:?}, title: {}, [{}/{}]", + "\x1b[1A\x1b[2KDownloaded volume: {:?}, chapter: {:?}, title: {:?}, [{}/{}]", chapter.attributes.volume, chapter.attributes.chapter, chapter.attributes.title, @@ -401,13 +379,13 @@ fn save_images( String::from("_none") }; let chapter_path = format!( - "images/{}/chapter_{:0>4}_image_{:0>4}.png", + "images/{}/chapter_{:0>4}_page_{:0>4}.png", title, chapter_text, n.index ); let path = if let Some(v) = n.volume { if create_volumes { format!( - "images/{}/volume_{:0>4}/chapter_{:0>4}_image_{:0>4}.png", + "images/{}/volume_{:0>4}/chapter_{:0>4}_page_{:0>4}.png", title, v, chapter_text, n.index ) } else { @@ -427,9 +405,6 @@ fn save_images( } } -// Path structure -// /images/Manga Name/Volume 00XX - Volume Name/Chapter 00XX - Chapter Name/00XX.png -// /images/Manga Name/Volume 00XX - Volume Name/Chapter 00XX - page 00XX - Chapter Name.png pub fn create_volumes_or_chapters( selected_chapters: &Vec<&Chapter>, create_volumes: bool, @@ -489,7 +464,7 @@ pub fn create_volumes_or_chapters( continue; } if entry.file_name().to_str().unwrap()[..18] - == *format!("chapter_{:0>4}_image_", chapter).as_str() + == *format!("chapter_{:0>4}_page_", chapter).as_str() { zip.start_file(entry.file_name().to_str().unwrap(), options) .unwrap(); @@ -517,7 +492,7 @@ fn construct_image_items(image_data: &ChapterImages, chapter: &Chapter) -> Vec