From 6d4ffa209a8db7333326b7651a13b9301e400f29 Mon Sep 17 00:00:00 2001 From: Vegard Matthey Date: Tue, 3 Jun 2025 00:25:00 +0200 Subject: [PATCH] replace zip command with zip lib, do languages properly i suppose, and other stuff i do not remember --- .gitignore | 1 + src/main.rs | 368 +++++++++++++-------- src/response_deserializer.rs | 601 ++++++++++++++++++++++++++++++++--- src/util.rs | 33 +- 4 files changed, 824 insertions(+), 179 deletions(-) diff --git a/.gitignore b/.gitignore index bca458a..ac76f28 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ Cargo.lock /cba *.jpg *.png +*.cbz diff --git a/src/main.rs b/src/main.rs index 13e154e..c4d02a3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,11 @@ use reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware}; use response_deserializer::{ChapterImages, SearchResult}; +use std::fs::File; +use std::io::Write; +use std::path::Path; +use zip::write::{SimpleFileOptions, ZipWriter}; +use zip::CompressionMethod; mod response_deserializer; mod select; @@ -15,7 +20,7 @@ type Client = ClientWithMiddleware; #[tokio::main] async fn main() { let config = util::args(); - let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3); + let retry_policy = ExponentialBackoff::builder().build_with_max_retries(10); let client = ClientBuilder::new( reqwest::Client::builder() .user_agent("manga-cli/version-0.1") @@ -32,73 +37,26 @@ async fn main() { ]; let limit = config.result_limit; - let results = if let Some(query) = config.search { - match query { - util::ConfigSearch::Query(query) => search(client, &query, &filters, limit).await, - util::ConfigSearch::Id(_) => todo!(), - } + let mut choice = 0; + let results = if let Some(util::ConfigSearch::Id(set_id)) = config.search { + let id_query_result: response_deserializer::IdQueryResult = + id_query_get_info(client, &set_id).await; + vec![id_query_result.data] } else { - let input = util::get_input("Enter search query: "); - search(client, &input, &filters, limit).await - }; - let cover_ex = match config.cover_size { - util::CoverSize::Full => "", - util::CoverSize::W256 => ".256.jpg", - util::CoverSize::W512 => ".512.jpg", + let results = if let Some(ref query) = config.search { + match query { + util::ConfigSearch::Query(query) => search(client, query, &filters, limit).await, + _ => unreachable!(), + } + } else { + let input = util::get_input("Enter search query: "); + search(client, &input, &filters, limit).await + }; + choice = select_manga_from_search(client, &config, &results).await; + results.data }; + let manga = &results[choice as usize]; - let mut entry_futures = Vec::new(); - for result in results.data.iter() { - let mut entry = Entry::new(result.attributes.title.en.clone()); - if let Some(year) = result.attributes.year { - entry.add_info("year", year); - } - let id = result.id.to_string(); - entry.add_info("id", &id); - entry.add_info("status", result.attributes.status.to_string()); - entry.add_info( - "content rating", - result.attributes.content_rating.to_string(), - ); - if let Some(chapters) = result.attributes.last_chapter { - entry.add_info("chapters", chapters); - } - 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 - let future = async move { - let image_url = format!( - "https://uploads.mangadex.org/covers/{id}/{}{cover_ex}", - &cover_data.file_name - ); - let data = 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(future); - } - } - let entries = futures::future::join_all(entry_futures).await; - let choice = match select::select(&entries) { - Ok(v) => v, - Err(e) => { - eprintln!("ERROR: Failed to select: {:?}", e); - std::process::exit(1); - } - }; - let choice_id = &results.data[choice as usize].id; let bonus = if let Some(bonus) = config.bonus { bonus } else { @@ -111,33 +69,72 @@ async fn main() { } }; let selection_type = if let Some(selection_type) = config.selection_type { - match selection_type { - util::ConfigSelectionType::Volume => loop { - let input = util::get_input("Choose volumes: ").replace(" ", ""); - if let Some(selection) = util::choose_volumes(input.as_str()) { - break util::SelectionType::Volume(selection); + if let Some(selection_range) = config.selection_range { + match selection_type { + util::ConfigSelectionType::Volume => { + if let Some(selection) = util::choose_volumes(selection_range.as_str()) { + util::SelectionType::Volume(selection) + } else { + std::process::exit(1) + } } - }, - util::ConfigSelectionType::Chapter => loop { - let input = util::get_input("Choose chapters: ").replace(" ", ""); - if let Some(selection) = util::choose_chapters(input.as_str()) { - break util::SelectionType::Chapter(selection); + util::ConfigSelectionType::Chapter => { + if let Some(selection) = util::choose_chapters(selection_range.as_str()) { + util::SelectionType::Chapter(selection) + } else { + std::process::exit(1) + } } - }, + } + } else { + match selection_type { + util::ConfigSelectionType::Volume => loop { + let input = util::get_input("Choose volumes: ").replace(" ", ""); + if let Some(selection) = util::choose_volumes(input.as_str()) { + break util::SelectionType::Volume(selection); + } + }, + util::ConfigSelectionType::Chapter => loop { + let input = util::get_input("Choose chapters: ").replace(" ", ""); + if let Some(selection) = util::choose_chapters(input.as_str()) { + break util::SelectionType::Chapter(selection); + } + }, + } } } else { 'outer: loop { match util::get_input("Select by volume or chapter? [v/c] : ").as_str() { - "v" | "volume" => loop { - let input = util::get_input("Choose volumes: ").replace(" ", ""); - if let Some(selection) = util::choose_volumes(input.as_str()) { - break 'outer util::SelectionType::Volume(selection); + "v" | "volume" => { + if let Some(ref selection_range) = config.selection_range { + if let Some(selection) = util::choose_volumes(selection_range.as_str()) { + break util::SelectionType::Volume(selection); + } else { + std::process::exit(1); + } + } else { + loop { + let input = util::get_input("Choose volumes: ").replace(" ", ""); + if let Some(selection) = util::choose_volumes(input.as_str()) { + break 'outer util::SelectionType::Volume(selection); + } + } } - }, + } "c" | "chapter" => { - let input = util::get_input("Choose chapters: ").replace(" ", ""); - if let Some(selection) = util::choose_chapters(input.as_str()) { - break 'outer util::SelectionType::Chapter(selection); + if let Some(ref selection_range) = config.selection_range { + if let Some(selection) = util::choose_chapters(selection_range.as_str()) { + break util::SelectionType::Chapter(selection); + } else { + std::process::exit(1); + } + } else { + loop { + let input = util::get_input("Choose chapters: ").replace(" ", ""); + if let Some(selection) = util::choose_chapters(input.as_str()) { + break 'outer util::SelectionType::Chapter(selection); + } + } } } _ => { @@ -146,7 +143,7 @@ async fn main() { } } }; - let mut chapters = match get_chapters(client, choice_id).await { + let mut chapters = match get_chapters(client, &manga.id).await { Ok(v) => v, Err(e) => { eprintln!("ERROR: {:#?}", e); @@ -166,11 +163,9 @@ async fn main() { let selected_chapters = util::get_chapters_from_selection(util::Selection::new(selection_type, bonus), &chapters); - let mut chapters_image_data = Vec::new(); - let mut i = 0; - for chapter in &selected_chapters { - // rate limits beware - let r = loop { + let title = &manga.attributes.title; + 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() @@ -195,40 +190,40 @@ async fn main() { } } }; - std::thread::sleep(std::time::Duration::from_millis(800)); - println!("downloaded chapter json image data: {i}"); - i += 1; - chapters_image_data.push(r); - } - - let chapters = futures::future::join_all( - chapters_image_data - .iter() - .enumerate() - .map(|(i, image_data)| { - download_chapter_images(client, image_data, selected_chapters[i]) - }), - ) - .await; - - for (i, chapter) in chapters.iter().enumerate() { + println!( + "\x1b[1A\x1b[2Kdownloaded chapter json image data: [{i}/{}]", + selected_chapters.len() + ); + let chapter = + download_chapter_images(client, &chapter_image_data, selected_chapters[i]).await; match chapter { Ok(chapter) => { for (j, image) in chapter.iter().enumerate() { - let chapter_n = selected_chapters[i].attributes.chapter.unwrap(); - let path = if let Some(v) = selected_chapters[i].attributes.volume { - format!( - "images/{}/volume_{:0>3}/chapter{:0>3}_image_{:0>3}.png", - results.data[choice as usize].attributes.title.en, v, chapter_n, j - ) + let chapter_n = selected_chapters[i].attributes.chapter; + let chapter_text = if let Some(n) = chapter_n { + n.to_string() } else { - format!( - "images/{}/chapter{:0>3}_image_{:0>3}.png", - results.data[choice as usize].attributes.title.en, chapter_n, j - ) + String::from("_none") + }; + let chapter_path = format!( + "images/{}/chapter_{:0>3}_image_{:0>3}.png", + title.en, chapter_text, j + ); + let path = if let Some(v) = selected_chapters[i].attributes.volume { + if create_volumes { + format!( + "images/{}/volume_{:0>3}/chapter_{:0>3}_image_{:0>3}.png", + title.en, v, chapter_text, j + ) + } else { + chapter_path + } + } else { + chapter_path }; let path = std::path::Path::new(&path); - if selected_chapters[i].attributes.volume.is_some() + if create_volumes + && selected_chapters[i].attributes.volume.is_some() && !&path.parent().unwrap().exists() { if !path.parent().unwrap().parent().unwrap().exists() { @@ -245,7 +240,6 @@ async fn main() { } } - let title = &results.data[choice as usize].attributes.title; if create_volumes { let mut volumes = Vec::new(); selected_chapters @@ -257,14 +251,55 @@ async fn main() { } }); for volume in volumes { - let path = format!("images/{}/volume_{:0>3}", title.en, volume); - let file_name = format!("{} - Volume {:0>3}.cbz", title.en, volume); - std::process::Command::new("/usr/bin/zip") - .args(["-j", "-r", &file_name, &path]) - .spawn() - .unwrap() - .wait() - .unwrap(); + let image_paths = format!("images/{}/volume_{:0>3}", title.en, volume); + let image_paths = Path::new(&image_paths); + + let zip_file_path = format!("{} - Volume {:0>3}.cbz", title.en, volume); + let zip_file_path = Path::new(&zip_file_path); + let zip_file = File::create(&zip_file_path).unwrap(); + + let mut zip = ZipWriter::new(zip_file); + let options = + SimpleFileOptions::default().compression_method(CompressionMethod::Deflated); + + for entry in std::fs::read_dir(image_paths).unwrap() { + let entry = entry.unwrap(); + zip.start_file(entry.file_name().to_str().unwrap(), options) + .unwrap(); + + let buffer = std::fs::read(entry.path()).unwrap(); + zip.write_all(&buffer).unwrap(); + } + + zip.finish().unwrap(); + } + } else { + for chapter in selected_chapters { + let chapter = chapter.attributes.chapter.unwrap(); + let image_paths = format!("images/{}", title.en); + let image_paths = Path::new(&image_paths); + + let zip_file_path = format!("{} - Chapter {:0>3}.cbz", title.en, chapter); + let zip_file_path = Path::new(&zip_file_path); + let zip_file = File::create(&zip_file_path).unwrap(); + + let mut zip = ZipWriter::new(zip_file); + let options = + SimpleFileOptions::default().compression_method(CompressionMethod::Deflated); + + for entry in std::fs::read_dir(image_paths).unwrap() { + let entry = entry.unwrap(); + if entry.path().is_dir() { + continue; + } + zip.start_file(entry.file_name().to_str().unwrap(), options) + .unwrap(); + + let buffer = std::fs::read(entry.path()).unwrap(); + zip.write_all(&buffer).unwrap(); + } + + zip.finish().unwrap(); } } } @@ -289,7 +324,7 @@ async fn download_chapter_images( .await .unwrap(); println!( - "Downloaded volume: {:?}, chapter: {:?}, title: {}, [{}/{}]", + "\x1b[1A\x1b[2KDownloaded volume: {:?}, chapter: {:?}, title: {}, [{}/{}]", chapter.attributes.volume, chapter.attributes.chapter, chapter.attributes.title, @@ -356,3 +391,80 @@ async fn search( .unwrap(); response_deserializer::deserializer(&json) } + +async fn select_manga_from_search( + client: &Client, + config: &util::Config, + results: &SearchResult, +) -> u16 { + let cover_ex = match config.cover_size { + util::CoverSize::Full => "", + util::CoverSize::W256 => ".256.jpg", + util::CoverSize::W512 => ".512.jpg", + }; + + let mut entry_futures = Vec::new(); + for result in results.data.iter() { + let mut entry = Entry::new(result.attributes.title.en.clone()); + if let Some(year) = result.attributes.year { + entry.add_info("year", year); + } + let id = result.id.to_string(); + entry.add_info("id", &id); + entry.add_info("status", result.attributes.status.to_string()); + entry.add_info( + "content rating", + result.attributes.content_rating.to_string(), + ); + if let Some(chapters) = result.attributes.last_chapter { + entry.add_info("chapters", chapters); + } + 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 + let future = async move { + let image_url = format!( + "https://uploads.mangadex.org/covers/{id}/{}{cover_ex}", + &cover_data.file_name + ); + let data = 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(future); + } + } + let entries = futures::future::join_all(entry_futures).await; + let choice = match select::select(&entries) { + Ok(v) => v, + Err(e) => { + eprintln!("ERROR: Failed to select: {:?}", e); + std::process::exit(1); + } + }; + choice +} + +async fn id_query_get_info(client: &Client, id: &Id) -> response_deserializer::IdQueryResult { + let json = client + .get(format!("{BASE}/manga/{id}")) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + response_deserializer::deserialize_id_query(&json) +} diff --git a/src/response_deserializer.rs b/src/response_deserializer.rs index 0b06159..4272837 100644 --- a/src/response_deserializer.rs +++ b/src/response_deserializer.rs @@ -12,36 +12,243 @@ 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". #[derive(Debug)] pub enum Language { - Tagalog, - SimplifiedChinese, - Greek, - Persian, - TraditionalChinese, - Ukranian, - Romanian, + Abkhazian, + Afar, + Afrikaans, + Akan, + Albanian, + Amharic, Arabic, - German, - Vietnamese, - French, - Turkish, - Korean, - SpanishLatinAmerican, - Hungarian, + Aragonese, + Armenian, + Assamese, + Avaric, + Avestan, + Aymara, + Azerbaijani, + Bambara, + Bashkir, + Basque, + Belarusian, + Bengali, + Bislama, + Bosnian, BrazilianPortugese, - English, - Japanese, - JapaneseRomaji, - Italian, - Russian, - Indonesian, + Breton, Bulgarian, - Hebrew, - Spanish, - Esperanto, - Polish, + 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, + English, + Esperanto, + Estonian, + Ewe, + Faroese, + Fijian, + Finnish, + Flemish, + French, + Fulah, + Gaelic, + Galician, + Ganda, + Georgian, + German, + Gikuyu, + Greek, + Greenlandic, + Guarani, + Gujarati, + Gwic, + 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, + Nynors, + 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, + SouthNdebele, + 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)] @@ -76,6 +283,7 @@ pub enum State { #[derive(Debug)] pub enum Response { Collection, + Entity, } #[derive(Debug)] @@ -99,6 +307,13 @@ pub struct SearchResult { 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 { @@ -216,6 +431,7 @@ pub struct Chapter { pub relationships: Vec, } +#[derive(Debug)] pub struct ChapterAttributes { pub volume: Option, pub chapter: Option, @@ -247,6 +463,13 @@ struct SearchResponse { total: u32, } +#[derive(Deserialize, Debug)] +struct IdQueryResponse { + result: String, + response: String, + data: ContentData, +} + #[derive(Deserialize, Debug)] struct ContentData { id: String, @@ -430,34 +653,232 @@ impl TryInto for &str { fn try_into(self) -> Result { Ok(match self { - "ja" => Language::Japanese, - "ja-ro" => Language::JapaneseRomaji, - "en" => Language::English, - "ru" => Language::Russian, - "pt-br" => Language::BrazilianPortugese, - "tr" => Language::Turkish, - "it" => Language::Italian, - "es-la" => Language::SpanishLatinAmerican, - "hu" => Language::Hungarian, - "bg" => Language::Bulgarian, - "id" => Language::Indonesian, - "he" => Language::Hebrew, - "es" => Language::Spanish, - "eo" => Language::Esperanto, - "pl" => Language::Polish, - "ko" => Language::Korean, - "fr" => Language::French, - "vi" => Language::Vietnamese, - "de" => Language::German, + "ab" => Language::Abkhazian, + "aa" => Language::Afar, + "af" => Language::Afrikaans, + "ak" => Language::Akan, + "sq" => Language::Albanian, + "am" => Language::Amharic, "ar" => Language::Arabic, - "ro" => Language::Romanian, - "uk" => Language::Ukranian, - "zh-hk" => Language::TraditionalChinese, - "fa" => Language::Persian, - "el" => Language::Greek, + "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, - "tl" => Language::Tagalog, + "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, + "pt" => Language::Portuguese, + "es" => Language::CastilianSpanish, + "es-la" => Language::LatinAmericanSpanish, + "ja-ro" => Language::RomanizedJapanese, + "pt-br" => Language::BrazilianPortugese, + _ => return Err(()), }) } @@ -523,6 +944,7 @@ impl TryInto for &str { fn try_into(self) -> Result { match self { "collection" => Ok(Response::Collection), + "entity" => Ok(Response::Entity), _ => Err(()), } } @@ -781,6 +1203,29 @@ fn convert_attributes( }) } +// ***** +pub fn deserialize_id_query(json: &str) -> IdQueryResult { + let id_query_response: IdQueryResponse = match serde_json::from_str(json) { + Ok(v) => v, + Err(e) => { + std::fs::write("out.json", json).unwrap(); + eprintln!("ERROR: {:#?}", e); + std::process::exit(1); + } + }; + convert_some_test(id_query_response).unwrap() +} + +fn convert_some_test(input: IdQueryResponse) -> Result { + Ok(IdQueryResult { + result: input.result.as_str().try_into().unwrap(), + response: input.response.as_str().try_into().unwrap(), + data: convert_data_to_manga(input.data).unwrap(), + }) +} + +// ***** + pub fn deserialize_chapter_feed(json: &str) -> ChapterFeed { let chapter_feed_response: ChapterFeedResponse = match serde_json::from_str(json) { Ok(v) => v, @@ -957,3 +1402,61 @@ pub fn deserialize_chapter_images(json: &str) -> Result Result { + Ok(Manga { + id: Id(m.id.clone()), + data_type: (m.type_name.as_str()).try_into().map_err(|_| { + ResponseConversionError::AttributeError(AttributeConversionError::DataType( + m.type_name.clone(), + )) + })?, + attributes: convert_attributes(&m.attributes) + .map_err(ResponseConversionError::AttributeError)?, + relationships: m + .relationships + .iter() + .map(|m| { + Ok(RelationShip { + id: Id(m.id.clone()), + data_type: (m.type_name.as_str()) + .try_into() + .map_err(|_| AttributeConversionError::DataType(m.type_name.clone()))?, + 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: String::new(), + 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>>() + .map_err(ResponseConversionError::AttributeError)?, + }) +} diff --git a/src/util.rs b/src/util.rs index ad4d136..69836a5 100644 --- a/src/util.rs +++ b/src/util.rs @@ -320,11 +320,28 @@ pub fn args() -> Config { let mut config = Config::new(); while args.len() != 0 { match args.next().unwrap().as_ref() { + "--selection-range" => { + if config.selection_range.is_some() { + eprintln!("Value selection range already set."); + std::process::exit(1); + } + config.selection_range = match args.next() { + Some(selection_type) => Some(selection_type), + None => { + eprintln!("Missing value for selection range, type: String"); + std::process::exit(1); + } + }; + } "-t" | "--selection-type" => { + if config.selection_type.is_some() { + eprintln!("Value selection type already set."); + std::process::exit(1); + } config.selection_type = match args.next() { Some(selection_type) => Some(match selection_type.as_str() { - "volume" => ConfigSelectionType::Volume, - "chapter" => ConfigSelectionType::Chapter, + "v" | "volume" => ConfigSelectionType::Volume, + "c" | "chapter" => ConfigSelectionType::Chapter, _ => { eprintln!("Invalid value for selection type, type: SelectionType"); std::process::exit(1); @@ -337,6 +354,10 @@ pub fn args() -> Config { }; } "-i" | "--id" => { + if config.search.is_some() { + eprintln!("Conflicting arguments for search."); + std::process::exit(1); + } if let Some(id) = args.next() { config.search = Some(ConfigSearch::Id(Id(id))); } else { @@ -345,6 +366,10 @@ pub fn args() -> Config { } } "-s" | "--search" => { + if config.search.is_some() { + eprintln!("Conflicting arguments for search."); + std::process::exit(1); + } if let Some(query) = args.next() { config.search = Some(ConfigSearch::Query(query)); } else { @@ -353,6 +378,10 @@ pub fn args() -> Config { } } "-b" | "--bonus" => { + if config.bonus.is_some() { + eprintln!("Value bonus already set."); + std::process::exit(1); + } config.bonus = match args.next() { Some(a) => Some(match a.as_str() { "true" => true,