From 66daaad6a79ae9c3a5a702355c84408c2efabdf9 Mon Sep 17 00:00:00 2001 From: Vegard Matthey Date: Wed, 9 Jul 2025 18:43:34 +0200 Subject: [PATCH] some more tests and metadata --- Cargo.toml | 1 + src/client.rs | 29 +++++++++++++++--- src/main.rs | 11 +++++-- src/metadata.rs | 59 ++++++++++++++++++++++++++++++++++++ src/response_deserializer.rs | 46 +++++++++++++++++++++++----- src/test.rs | 8 +---- src/util.rs | 15 +++++---- 7 files changed, 140 insertions(+), 29 deletions(-) create mode 100644 src/metadata.rs diff --git a/Cargo.toml b/Cargo.toml index 0e3f196..a39aaf3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ image = { version = "0.25.2", default-features = false, features = ["jpeg", "png 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 } +ron = {version = "0.10.1", default-features = false } serde = { version = "1.0.204", default-features = false, features = ["derive"] } serde_json = { version = "1.0.121", default-features = false, features = ["std"] } thiserror = { version = "2.0.12", default-features = false } diff --git a/src/client.rs b/src/client.rs index 4ce8e17..6c4cbe1 100644 --- a/src/client.rs +++ b/src/client.rs @@ -222,7 +222,7 @@ impl MangaClient { .text() .await .unwrap(); - crate::response_deserializer::deserializer(&json) + crate::response_deserializer::deserialize_search(&json) .unwrap() .data } @@ -307,6 +307,7 @@ mod tests { use super::*; #[tokio::test] + #[ignore] async fn ensure_getting_all_chapters() { let client = MangaClientBuilder::new() .selection_type(crate::util::ConfigSelectionType::Volume) @@ -315,7 +316,7 @@ mod tests { // These tests should only be done for completed manga because last chapter might change // otherwise - test_chapters_for_manga( + chapters_for_manga( &client, &Id(String::from("5a547d1d-576b-477f-8cb3-70a3b4187f8a")), "JoJo's Bizarre Adventure Part 1 - Phantom Blood", @@ -323,7 +324,7 @@ mod tests { ) .await; - test_chapters_for_manga( + chapters_for_manga( &client, &Id(String::from("0d545e62-d4cd-4e65-a65c-a5c46b794918")), "JoJo's Bizarre Adventure Part 3 - Stardust Crusaders", @@ -331,7 +332,7 @@ mod tests { ) .await; - test_chapters_for_manga( + chapters_for_manga( &client, &Id(String::from("319df2e2-e6a6-4e3a-a31c-68539c140a84")), "Slam Dunk!", @@ -340,7 +341,7 @@ mod tests { .await; } - async fn test_chapters_for_manga(client: &MangaClient, id: &Id, title: &str, len: usize) { + async fn chapters_for_manga(client: &MangaClient, id: &Id, title: &str, len: usize) { let chapters = client.get_chapters(id).await.unwrap(); let mut test = vec![false; len]; for chapter in &chapters { @@ -356,4 +357,22 @@ mod tests { } assert!(!test.iter().any(|m| *m == false)); } + + #[tokio::test] + #[ignore] + async fn get_manga() { + 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))) + .build(); + + let manga: Vec = client.get_manga().await; + assert!(manga.iter().any(|m| m.id.0 == String::from(id))); + } } diff --git a/src/main.rs b/src/main.rs index 7a5b571..080ccd1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ mod response_deserializer; mod select; mod test; mod util; +mod metadata; const BASE: &str = "https://api.mangadex.org"; @@ -38,7 +39,7 @@ async fn main() { .search_filter(SearchFilter::default()) .language(Language::default()) .build(); - let search_result = manga_client.get_manga().await; + let mut search_result = manga_client.get_manga().await; if search_result.is_empty() { eprintln!("Found no search results"); std::process::exit(1); @@ -48,7 +49,7 @@ async fn main() { if search_result.len() > 1 { choice = select_manga_from_search(&manga_client, &search_result).await; } - let manga = &search_result[choice as usize]; + let manga = search_result.remove(choice as usize); let mut chapters = match manga_client.get_chapters(&manga.id).await { Ok(v) => v, Err(e) => { @@ -64,13 +65,14 @@ 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 title = &manga.attributes.title.en; + let title = &manga.attributes.title.en.clone(); util::download_selected_chapters( &manga_client.client, @@ -81,6 +83,9 @@ async fn main() { .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(); } async fn select_manga_from_search(client: &MangaClient, results: &[Manga]) -> u16 { diff --git a/src/metadata.rs b/src/metadata.rs new file mode 100644 index 0000000..fb6eae4 --- /dev/null +++ b/src/metadata.rs @@ -0,0 +1,59 @@ +#![allow(unused)] + +use serde::Serialize; +use crate::response_deserializer::{ + Chapter, ContentRating, Language, Manga, PublicationDemographic, State, Status, Titles, +}; +use chrono::{DateTime, FixedOffset}; + +#[derive(Serialize)] +pub struct Metadata { + title: String, + original_language: Language, + last_volume: f32, + last_chapter: f32, + publication_demographic: Option, + status: Status, + year: Option, + content_rating: ContentRating, + state: State, + created_at: String, + updated_at: String, + chapters: Vec, +} + +#[derive(Clone, Serialize)] +struct ChapterMetadata { + chapter: f32, + volume: f32, + title: String, + pages: u32, +} + +impl Metadata { + pub fn new(manga: Manga, chapters: Vec) -> Self { + let attributes = manga.attributes; + Self { + title: attributes.title.en, + original_language: attributes.original_language, + last_volume: attributes.last_volume.unwrap(), + last_chapter: attributes.last_chapter.unwrap(), + publication_demographic: attributes.publication_demographic, + status: attributes.status, + year: attributes.year, + content_rating: attributes.content_rating, + state: attributes.state, + created_at: attributes.created_at.to_string(), + updated_at: attributes.updated_at.to_string(), + chapters: chapters + .iter() + .map(|m| ChapterMetadata { + chapter: m.attributes.chapter.unwrap(), + volume: m.attributes.volume.unwrap(), + title: m.attributes.title.clone(), + pages: m.attributes.pages, + }) + .collect(), + } + } +} diff --git a/src/response_deserializer.rs b/src/response_deserializer.rs index b85c4d1..e885973 100644 --- a/src/response_deserializer.rs +++ b/src/response_deserializer.rs @@ -2,7 +2,7 @@ #![allow(unused)] use crate::error::*; use chrono::{DateTime, FixedOffset}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::fmt; #[derive(Debug, Clone)] @@ -26,7 +26,7 @@ pub enum ResponseResult { // 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)] +#[derive(Debug, Default, Serialize)] pub enum Language { Abkhazian, Afar, @@ -252,7 +252,7 @@ pub enum Language { Zulu, } -#[derive(Debug)] +#[derive(Debug, Serialize)] pub enum Status { Completed, Ongoing, @@ -260,7 +260,7 @@ pub enum Status { Cancelled, } -#[derive(Debug)] +#[derive(Debug, Serialize)] pub enum ContentRating { Safe, Suggestive, @@ -268,7 +268,7 @@ pub enum ContentRating { Pornographic, } -#[derive(Debug)] +#[derive(Debug, Serialize)] pub enum PublicationDemographic { Shounen, Shoujo, @@ -276,7 +276,7 @@ pub enum PublicationDemographic { Josei, } -#[derive(Debug)] +#[derive(Debug, Serialize)] pub enum State { Published, } @@ -408,6 +408,7 @@ struct ChapterAttributesContent { version: u32, } +#[derive(Debug)] pub struct Chapter { pub id: Id, pub data_type: DataType, @@ -1409,7 +1410,7 @@ pub fn deserialize_chapter_feed(json: &str) -> Result Result { +pub fn deserialize_search(json: &str) -> Result { let search_response: SearchResponse = serde_json::from_str(json) .map_err(|e| SearchResultError::Serde(JsonError::Message(e.to_string())))?; search_response @@ -1598,7 +1599,7 @@ impl TryFrom for Manga { ) })?, // TODO: Something should probably be done here - description: String::new(), + description: attributes.description, file_name: Id(attributes.file_name.clone()), locale: (attributes.locale.as_str()) .try_into() @@ -1629,3 +1630,32 @@ impl TryFrom for Manga { }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn id_query() { + let manga = std::fs::read_to_string("test_data/id_query_hunter_x_hunter.json").unwrap(); + assert_eq!(deserialize_id_query(&manga).unwrap().result , ResponseResult::Ok); + } + + #[test] + fn search() { + let search_result = std::fs::read_to_string("test_data/search_result.json").unwrap(); + assert_eq!(deserialize_search(&search_result).unwrap().result , ResponseResult::Ok); + } + + #[test] + fn chapter_feed() { + let chapter_feed = std::fs::read_to_string("test_data/chapter_feed_hunter_x_hunter.json").unwrap(); + deserialize_chapter_feed(&chapter_feed).unwrap(); + } + + #[test] + 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(); + } +} diff --git a/src/test.rs b/src/test.rs index 2e438ce..529a471 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,10 +1,4 @@ #[cfg(test)] mod tests { - use crate::{response_deserializer, response_deserializer::ResponseResult}; - - #[test] - fn loops() { - let search_result = std::fs::read_to_string("test_data/search_result.json").unwrap(); - assert_eq!(response_deserializer::deserializer(&search_result).unwrap().result , ResponseResult::Ok); - } + // } diff --git a/src/util.rs b/src/util.rs index b0839d4..2052b0d 100644 --- a/src/util.rs +++ b/src/util.rs @@ -401,13 +401,13 @@ fn save_images( String::from("_none") }; let chapter_path = format!( - "images/{}/chapter_{:0>3}_image_{:0>3}.png", + "images/{}/chapter_{:0>4}_image_{:0>4}.png", title, chapter_text, n.index ); let path = if let Some(v) = n.volume { if create_volumes { format!( - "images/{}/volume_{:0>3}/chapter_{:0>3}_image_{:0>3}.png", + "images/{}/volume_{:0>4}/chapter_{:0>4}_image_{:0>4}.png", title, v, chapter_text, n.index ) } else { @@ -427,6 +427,9 @@ 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, @@ -443,10 +446,10 @@ pub fn create_volumes_or_chapters( } }); for volume in volumes { - let image_paths = format!("images/{}/volume_{:0>3}", title, volume); + let image_paths = format!("images/{}/volume_{:0>4}", title, volume); let image_paths = Path::new(&image_paths); - let zip_file_path = format!("{} - Volume {:0>3}.cbz", title, volume); + let zip_file_path = format!("{} - Volume {:0>4}.cbz", title, volume); let zip_file_path = Path::new(&zip_file_path); let zip_file = File::create(zip_file_path).unwrap(); @@ -471,7 +474,7 @@ pub fn create_volumes_or_chapters( let image_paths = format!("images/{}", title); let image_paths = Path::new(&image_paths); - let zip_file_path = format!("{} - Chapter {:0>3}.cbz", title, chapter); + let zip_file_path = format!("{} - Chapter {:0>4}.cbz", title, chapter); println!("creating cbz chapter at: {zip_file_path}"); let zip_file_path = Path::new(&zip_file_path); let zip_file = File::create(zip_file_path).unwrap(); @@ -486,7 +489,7 @@ pub fn create_volumes_or_chapters( continue; } if entry.file_name().to_str().unwrap()[..18] - == *format!("chapter_{:0>3}_image_", chapter).as_str() + == *format!("chapter_{:0>4}_image_", chapter).as_str() { zip.start_file(entry.file_name().to_str().unwrap(), options) .unwrap();