use builder pattern for getting manga
This commit is contained in:
@@ -9,10 +9,11 @@ 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"] }
|
||||
reqwest = { version = "0.12.5", default-features = false }
|
||||
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 }
|
||||
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 }
|
||||
tokio = { version = "1.39.2", default-features = false, features = ["macros", "rt-multi-thread"] }
|
||||
zip = { version = "4.0.0", default-features = false, features = ["deflate"] }
|
||||
|
||||
287
src/client.rs
Normal file
287
src/client.rs
Normal file
@@ -0,0 +1,287 @@
|
||||
use crate::response_deserializer::{Chapter, Id, Language};
|
||||
use crate::util::{ConfigSearch, ConfigSelectionType, CoverSize, SelectionRange};
|
||||
use crate::BASE;
|
||||
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
|
||||
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
|
||||
|
||||
pub struct MangaClientBuilder {
|
||||
client: ClientWithMiddleware,
|
||||
search_filter: Option<SearchFilter>,
|
||||
bonus: Option<bool>,
|
||||
result_limit: Option<u32>,
|
||||
cover_size: Option<CoverSize>,
|
||||
selection_type: Option<ConfigSelectionType>,
|
||||
selection_range: Option<SelectionRange>,
|
||||
search: Option<ConfigSearch>,
|
||||
cover: Option<bool>,
|
||||
language: Option<Language>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MangaClient {
|
||||
pub client: ClientWithMiddleware,
|
||||
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,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct SearchFilter {
|
||||
pub content_rating: ContentRatingFilter,
|
||||
pub publication_demographic: PublicationDemographicFilter,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PublicationDemographicFilter {
|
||||
pub shounen: bool,
|
||||
pub shoujo: bool,
|
||||
pub seinen: bool,
|
||||
pub josei: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ContentRatingFilter {
|
||||
pub safe: bool,
|
||||
pub suggestive: bool,
|
||||
pub erotica: bool,
|
||||
pub pornographic: bool,
|
||||
}
|
||||
|
||||
impl From<SearchFilter> for Vec<(&str, &str)> {
|
||||
fn from(s: SearchFilter) -> Self {
|
||||
let mut v = Vec::new();
|
||||
|
||||
let p = s.publication_demographic;
|
||||
if p.shounen {
|
||||
v.push(("publicationDemographic[]", "shounen"));
|
||||
}
|
||||
if p.shoujo {
|
||||
v.push(("publicationDemographic[]", "shoujo"));
|
||||
}
|
||||
if p.seinen {
|
||||
v.push(("publicationDemographic[]", "seinen"));
|
||||
}
|
||||
if p.josei {
|
||||
v.push(("publicationDemographic[]", "josei"));
|
||||
}
|
||||
|
||||
let c = s.content_rating;
|
||||
if c.safe {
|
||||
v.push(("contentRating[]", "safe"));
|
||||
}
|
||||
if c.suggestive {
|
||||
v.push(("contentRating[]", "suggestive"));
|
||||
}
|
||||
if c.erotica {
|
||||
v.push(("contentRating[]", "erotica"));
|
||||
}
|
||||
if c.pornographic {
|
||||
v.push(("contentRating[]", "pornographic"));
|
||||
}
|
||||
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ContentRatingFilter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
safe: true,
|
||||
suggestive: true,
|
||||
erotica: true,
|
||||
pornographic: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PublicationDemographicFilter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
shounen: true,
|
||||
shoujo: true,
|
||||
seinen: true,
|
||||
josei: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MangaClientBuilder {
|
||||
pub fn new() -> MangaClientBuilder {
|
||||
MangaClientBuilder {
|
||||
client: ClientBuilder::new(
|
||||
reqwest::Client::builder()
|
||||
.user_agent("manga-cli/version-0.1")
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.with(RetryTransientMiddleware::new_with_policy(
|
||||
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,
|
||||
}
|
||||
}
|
||||
pub fn search_filter(mut self, filter: SearchFilter) -> MangaClientBuilder {
|
||||
self.search_filter = Some(filter);
|
||||
self
|
||||
}
|
||||
pub fn bonus(mut self, bonus: bool) -> MangaClientBuilder {
|
||||
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 {
|
||||
self.selection_type = Some(selection_type);
|
||||
self
|
||||
}
|
||||
pub fn selection_range(mut self, selection_range: SelectionRange) -> MangaClientBuilder {
|
||||
self.selection_range = Some(selection_range);
|
||||
self
|
||||
}
|
||||
pub fn search(mut self, search: ConfigSearch) -> MangaClientBuilder {
|
||||
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 {
|
||||
self.language = Some(language);
|
||||
self
|
||||
}
|
||||
|
||||
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(
|
||||
match self.selection_type {
|
||||
Some(a) => match a {
|
||||
ConfigSelectionType::Chapter => crate::util::SelectionType::Chapter(
|
||||
self.selection_range.unwrap(),
|
||||
),
|
||||
ConfigSelectionType::Volume => {
|
||||
crate::util::SelectionType::Volume(self.selection_range.unwrap())
|
||||
}
|
||||
},
|
||||
None => panic!(),
|
||||
},
|
||||
self.bonus.unwrap_or(true),
|
||||
),
|
||||
language: self.language.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use crate::response_deserializer::Manga;
|
||||
|
||||
impl MangaClient {
|
||||
pub async fn get_manga(&self) -> Vec<Manga> {
|
||||
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();
|
||||
let json = self
|
||||
.client
|
||||
.get(format!("{BASE}/manga"))
|
||||
.query(¶ms)
|
||||
.query(&search_filter)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.text()
|
||||
.await
|
||||
.unwrap();
|
||||
crate::response_deserializer::deserializer(&json)
|
||||
.unwrap()
|
||||
.data
|
||||
}
|
||||
ConfigSearch::Id(id) => {
|
||||
let json = self
|
||||
.client
|
||||
.get(format!("{BASE}/manga/{id}"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.text()
|
||||
.await
|
||||
.unwrap();
|
||||
vec![
|
||||
crate::response_deserializer::deserialize_id_query(&json)
|
||||
.unwrap()
|
||||
.data,
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_chapters(&self, id: &Id) -> Result<Vec<Chapter>, reqwest_middleware::Error> {
|
||||
let limit = 50;
|
||||
let limit = limit.to_string();
|
||||
let params = [
|
||||
("limit", limit.as_str()),
|
||||
("translatedLanguage[]", &self.language.to_string()),
|
||||
];
|
||||
let url = format!("{BASE}/manga/{id}/feed");
|
||||
let json = self
|
||||
.client
|
||||
.get(url)
|
||||
.query(¶ms)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
let mut result = crate::response_deserializer::deserialize_chapter_feed(&json).unwrap();
|
||||
|
||||
let mut total_chapters_received = result.limit;
|
||||
while total_chapters_received < result.total {
|
||||
let offset = total_chapters_received.to_string();
|
||||
let params = [params[0], params[1], ("offset", offset.as_str())];
|
||||
let url = format!("{BASE}/manga/{id}/feed");
|
||||
let json = self
|
||||
.client
|
||||
.get(url)
|
||||
.query(¶ms)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
let mut new_result =
|
||||
crate::response_deserializer::deserialize_chapter_feed(&json).unwrap();
|
||||
result.data.append(&mut new_result.data);
|
||||
total_chapters_received += result.limit;
|
||||
}
|
||||
assert_eq!(result.data.len(), result.total as usize);
|
||||
Ok(result.data)
|
||||
}
|
||||
}
|
||||
457
src/main.rs
457
src/main.rs
@@ -1,160 +1,59 @@
|
||||
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
|
||||
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
|
||||
use response_deserializer::{ChapterImages, SearchResult};
|
||||
use error::{ChapterImageError, ChapterImagesError};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use client::{MangaClient, MangaClientBuilder, SearchFilter};
|
||||
use response_deserializer::{Chapter, Id, Language, Manga};
|
||||
use select::Entry;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use zip::write::{SimpleFileOptions, ZipWriter};
|
||||
use zip::CompressionMethod;
|
||||
use util::SelectionType;
|
||||
|
||||
mod client;
|
||||
mod error;
|
||||
mod response_deserializer;
|
||||
mod select;
|
||||
mod test;
|
||||
mod util;
|
||||
mod error;
|
||||
mod client;
|
||||
|
||||
use response_deserializer::{Chapter, Id};
|
||||
use select::Entry;
|
||||
|
||||
const BASE: &str = "https://api.mangadex.org";
|
||||
type Client = ClientWithMiddleware;
|
||||
const BASE: &str = "https://api.mangadex.dev";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let config = util::args();
|
||||
let retry_policy = ExponentialBackoff::builder().build_with_max_retries(10);
|
||||
let client = ClientBuilder::new(
|
||||
reqwest::Client::builder()
|
||||
.user_agent("manga-cli/version-0.1")
|
||||
.build()
|
||||
.unwrap(),
|
||||
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::W512)
|
||||
.selection_type(
|
||||
config
|
||||
.selection_type
|
||||
.unwrap_or_else(util::ask_selection_type),
|
||||
)
|
||||
.with(RetryTransientMiddleware::new_with_policy(retry_policy))
|
||||
.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 client = &client;
|
||||
let filters = [
|
||||
// ("publicationDemographic[]", "seinen"),
|
||||
// ("status[]", "completed"),
|
||||
// ("contentRating[]", "suggestive"),
|
||||
];
|
||||
let limit = config.result_limit;
|
||||
let search_result = manga_client.get_manga().await;
|
||||
|
||||
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 results = if let Some(ref query) = config.search {
|
||||
match query {
|
||||
util::ConfigSearch::Query(query) => search(client, query, &filters, limit).await,
|
||||
_ => unreachable!(),
|
||||
if search_result.len() > 1 {
|
||||
choice = select_manga_from_search(&manga_client, &search_result).await;
|
||||
}
|
||||
} 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 bonus = if let Some(bonus) = config.bonus {
|
||||
bonus
|
||||
} else {
|
||||
loop {
|
||||
match util::get_input("Read bonus chapters? [y/n] : ").as_str() {
|
||||
"y" | "yes" => break true,
|
||||
"n" | "no" => break false,
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
};
|
||||
let selection_type = if let Some(selection_type) = config.selection_type {
|
||||
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 => {
|
||||
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" => {
|
||||
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" => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
eprintln!("Invalid input");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let mut chapters = match get_chapters(client, &manga.id).await {
|
||||
let manga = &search_result[choice as usize];
|
||||
let mut chapters = match manga_client.get_chapters(&manga.id).await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
eprintln!("ERROR: {e:#?}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
println!("Downloading {} chapters", chapters.len());
|
||||
chapters.sort_by(|a, b| {
|
||||
a.attributes
|
||||
.chapter
|
||||
@@ -162,263 +61,35 @@ async fn main() {
|
||||
.partial_cmp(&b.attributes.chapter.unwrap_or(-1.))
|
||||
.unwrap()
|
||||
});
|
||||
dbg!("got chapters");
|
||||
|
||||
let create_volumes = matches!(selection_type, util::SelectionType::Volume(_));
|
||||
let selected_chapters =
|
||||
util::get_chapters_from_selection(util::Selection::new(selection_type, bonus), &chapters);
|
||||
|
||||
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()
|
||||
.await
|
||||
.unwrap()
|
||||
.text()
|
||||
.await
|
||||
.unwrap();
|
||||
let result = 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));
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
println!(
|
||||
"\x1b[1A\x1b[2Kdownloaded chapter json image data: [{i}/{}]",
|
||||
selected_chapters.len()
|
||||
let create_volumes = matches!(
|
||||
manga_client.selection.selection_type,
|
||||
SelectionType::Volume(_)
|
||||
);
|
||||
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;
|
||||
let chapter_text = if let Some(n) = chapter_n {
|
||||
n.to_string()
|
||||
} else {
|
||||
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
|
||||
let selected_chapters = util::get_chapters_from_selection(manga_client.selection, &chapters);
|
||||
|
||||
let title = &manga.attributes.title.en;
|
||||
|
||||
util::download_the_stuff(
|
||||
&manga_client.client,
|
||||
&selected_chapters,
|
||||
title,
|
||||
create_volumes,
|
||||
)
|
||||
} else {
|
||||
chapter_path
|
||||
}
|
||||
} else {
|
||||
chapter_path
|
||||
};
|
||||
let path = std::path::Path::new(&path);
|
||||
if create_volumes
|
||||
&& selected_chapters[i].attributes.volume.is_some()
|
||||
&& !&path.parent().unwrap().exists()
|
||||
{
|
||||
if !path.parent().unwrap().parent().unwrap().exists() {
|
||||
std::fs::create_dir(path.parent().unwrap().parent().unwrap()).unwrap();
|
||||
}
|
||||
std::fs::create_dir(path.parent().unwrap()).unwrap();
|
||||
}
|
||||
image.save(path).unwrap();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
panic!("{e}");
|
||||
}
|
||||
}
|
||||
.await;
|
||||
|
||||
util::create_volumes_or_chapters(&selected_chapters, create_volumes, title);
|
||||
}
|
||||
|
||||
if create_volumes {
|
||||
let mut volumes = Vec::new();
|
||||
selected_chapters
|
||||
.iter()
|
||||
.filter_map(|m| m.attributes.volume)
|
||||
.for_each(|v| {
|
||||
if !volumes.contains(&v) {
|
||||
volumes.push(v);
|
||||
}
|
||||
});
|
||||
for volume in volumes {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn download_chapter_images(
|
||||
client: &Client,
|
||||
image_data: &ChapterImages,
|
||||
chapter: &Chapter,
|
||||
) -> Result<Vec<image::DynamicImage>, reqwest_middleware::Error> {
|
||||
let mut data_futures = vec![];
|
||||
for (i, file_name) in image_data.chapter.data.iter().enumerate() {
|
||||
let base_url: &str = image_data.base_url.as_str();
|
||||
let hash: &str = image_data.chapter.hash.as_str();
|
||||
let future = async move {
|
||||
let data = client
|
||||
.clone()
|
||||
.get(format!("{base_url}/data/{hash}/{file_name}"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.bytes()
|
||||
.await
|
||||
.unwrap();
|
||||
println!(
|
||||
"\x1b[1A\x1b[2KDownloaded volume: {:?}, chapter: {:?}, title: {}, [{}/{}]",
|
||||
chapter.attributes.volume,
|
||||
chapter.attributes.chapter,
|
||||
chapter.attributes.title,
|
||||
i,
|
||||
chapter.attributes.pages
|
||||
);
|
||||
data
|
||||
};
|
||||
data_futures.push(future);
|
||||
}
|
||||
Ok(futures::future::join_all(data_futures)
|
||||
.await
|
||||
.iter()
|
||||
.map(|m| image::load_from_memory(m).unwrap())
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn get_chapters(client: &Client, id: &Id) -> Result<Vec<Chapter>, reqwest_middleware::Error> {
|
||||
let limit = 50;
|
||||
let limit = limit.to_string();
|
||||
let params = [("limit", limit.as_str()), ("translatedLanguage[]", "en")];
|
||||
let url = format!("{BASE}/manga/{id}/feed");
|
||||
let json = client.get(url).query(¶ms).send().await?.text().await?;
|
||||
let mut result = response_deserializer::deserialize_chapter_feed(&json).unwrap();
|
||||
|
||||
let mut total_chapters_received = result.limit;
|
||||
while total_chapters_received < result.total {
|
||||
let offset = total_chapters_received.to_string();
|
||||
let params = [
|
||||
("limit", limit.as_str()),
|
||||
("translatedLanguage[]", "en"),
|
||||
("offset", offset.as_str()),
|
||||
];
|
||||
let url = format!("{BASE}/manga/{id}/feed");
|
||||
let json = client.get(url).query(¶ms).send().await?.text().await?;
|
||||
let mut new_result = response_deserializer::deserialize_chapter_feed(&json).unwrap();
|
||||
result.data.append(&mut new_result.data);
|
||||
total_chapters_received += result.limit;
|
||||
}
|
||||
assert_eq!(result.data.len(), result.total as usize);
|
||||
Ok(result.data)
|
||||
}
|
||||
|
||||
async fn search(
|
||||
client: &Client,
|
||||
query: &str,
|
||||
filters: &[(&str, &str)],
|
||||
limit: u32,
|
||||
) -> SearchResult {
|
||||
let params = [
|
||||
("title", query),
|
||||
("limit", &limit.to_string()),
|
||||
("includes[]", "cover_art"),
|
||||
];
|
||||
let json = client
|
||||
.get(format!("{BASE}/manga"))
|
||||
.query(¶ms)
|
||||
.query(filters)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.text()
|
||||
.await
|
||||
.unwrap();
|
||||
response_deserializer::deserializer(&json).unwrap()
|
||||
}
|
||||
|
||||
async fn select_manga_from_search(
|
||||
client: &Client,
|
||||
config: &util::Config,
|
||||
results: &SearchResult,
|
||||
) -> u16 {
|
||||
let cover_ex = match config.cover_size {
|
||||
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",
|
||||
};
|
||||
|
||||
use std::future::Future;
|
||||
let mut entry_futures: Vec<Pin<Box<dyn Future<Output = Entry>>>> = Vec::new();
|
||||
for result in results.data.iter() {
|
||||
assert!(!results.is_empty());
|
||||
for result in results.iter() {
|
||||
let mut entry = Entry::new(result.attributes.title.en.clone());
|
||||
if let Some(year) = result.attributes.year {
|
||||
entry.add_info("year", year);
|
||||
@@ -439,13 +110,15 @@ async fn select_manga_from_search(
|
||||
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
|
||||
if config.cover != Some(false) {
|
||||
// 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
|
||||
@@ -463,8 +136,12 @@ async fn select_manga_from_search(
|
||||
entry_futures.push(Box::pin(async move { entry }));
|
||||
}
|
||||
}
|
||||
let entries: Vec<Entry> = futures::future::join_all(entry_futures).await;
|
||||
assert!(!entries.is_empty());
|
||||
if entries.len() == 1 {
|
||||
return 0;
|
||||
}
|
||||
let entries = futures::future::join_all(entry_futures).await;
|
||||
|
||||
let choice = match select::select(&entries) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
@@ -474,15 +151,3 @@ async fn select_manga_from_search(
|
||||
};
|
||||
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).unwrap()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#![allow(unused)]
|
||||
// TODO: Remove this
|
||||
#![allow(unused)]
|
||||
use crate::error::*;
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use serde::Deserialize;
|
||||
@@ -8,7 +8,7 @@ use std::fmt;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Id(pub String);
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ResponseResult {
|
||||
Ok,
|
||||
}
|
||||
@@ -24,7 +24,9 @@ pub enum ResponseResult {
|
||||
// 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)]
|
||||
// There are a bunch of duplicates, I don't know if I wan't to fix it even.
|
||||
#[allow(unused)]
|
||||
#[derive(Debug, Default)]
|
||||
pub enum Language {
|
||||
Abkhazian,
|
||||
Afar,
|
||||
@@ -75,6 +77,7 @@ pub enum Language {
|
||||
Divehi,
|
||||
Dutch,
|
||||
Dzongkha,
|
||||
#[default]
|
||||
English,
|
||||
Esperanto,
|
||||
Estonian,
|
||||
@@ -364,8 +367,8 @@ pub struct MangaAttributes {
|
||||
pub is_locked: bool,
|
||||
pub links: Option<Links>,
|
||||
pub original_language: Language,
|
||||
pub last_volume: Option<u32>,
|
||||
pub last_chapter: Option<u32>,
|
||||
pub last_volume: Option<f32>,
|
||||
pub last_chapter: Option<f32>,
|
||||
pub publication_demographic: Option<PublicationDemographic>,
|
||||
pub status: Status,
|
||||
pub year: Option<u32>,
|
||||
@@ -414,7 +417,7 @@ pub struct Chapter {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ChapterAttributes {
|
||||
pub volume: Option<u32>,
|
||||
pub volume: Option<f32>,
|
||||
pub chapter: Option<f32>,
|
||||
pub title: String,
|
||||
pub translated_language: Language,
|
||||
@@ -500,7 +503,7 @@ struct ContentCoverAttributes {
|
||||
#[derive(Debug)]
|
||||
pub struct CoverAttributes {
|
||||
pub description: String,
|
||||
pub volume: Option<u32>,
|
||||
pub volume: Option<f32>,
|
||||
pub file_name: Id,
|
||||
pub locale: Language,
|
||||
pub created_at: DateTime<FixedOffset>,
|
||||
@@ -622,6 +625,7 @@ impl TryFrom<&str> for Language {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||
#[allow(unused)]
|
||||
Ok(match s {
|
||||
"ab" => Language::Abkhazian,
|
||||
"aa" => Language::Afar,
|
||||
@@ -957,14 +961,248 @@ impl TryFrom<SearchResponse> for SearchResult {
|
||||
}
|
||||
}
|
||||
|
||||
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 std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Status {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Ongoing => "ongoing".fmt(f),
|
||||
Self::Completed => "completed".fmt(f),
|
||||
@@ -1168,7 +1406,7 @@ pub fn deserialize_chapter_feed(json: &str) -> Result<ChapterFeed, ChapterFeedEr
|
||||
};
|
||||
chapter_feed_response
|
||||
.try_into()
|
||||
.map_err(|e| ChapterFeedError::Conversion(e))
|
||||
.map_err(ChapterFeedError::Conversion)
|
||||
}
|
||||
|
||||
pub fn deserializer(json: &str) -> Result<SearchResult, SearchResultError> {
|
||||
|
||||
@@ -6,14 +6,13 @@ use crossterm::{
|
||||
terminal::{Clear, ClearType},
|
||||
QueueableCommand,
|
||||
};
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
fmt::Display,
|
||||
io,
|
||||
io::{Stdout, Write},
|
||||
time::Duration,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
const CURRENT: char = '>';
|
||||
const NON_CURRENT: char = ' ';
|
||||
@@ -22,10 +21,10 @@ enum Action {
|
||||
MoveDown,
|
||||
MoveUp,
|
||||
Select,
|
||||
Exit,
|
||||
// Exit,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Entry {
|
||||
title: String,
|
||||
info: Vec<(String, String)>,
|
||||
@@ -60,7 +59,7 @@ fn get_input() -> Option<Action> {
|
||||
KeyCode::Char('j') => Action::MoveDown,
|
||||
KeyCode::Char('k') => Action::MoveUp,
|
||||
KeyCode::Enter => Action::Select,
|
||||
KeyCode::Char('q') => Action::Exit,
|
||||
// KeyCode::Char('q') => Action::Exit,
|
||||
_ => return None,
|
||||
}),
|
||||
Err(e) => {
|
||||
@@ -81,15 +80,39 @@ fn get_input() -> Option<Action> {
|
||||
fn exit() -> ! {
|
||||
io::stdout().queue(Show).unwrap().flush().unwrap();
|
||||
terminal::disable_raw_mode().unwrap();
|
||||
std::process::exit(1);
|
||||
std::process::exit(1)
|
||||
}
|
||||
|
||||
pub fn select(entries: &[Entry]) -> Result<u16, std::io::Error> {
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SelectionError {
|
||||
#[error("found empty list of entries")]
|
||||
NoEntries,
|
||||
#[error("found singular entry in list")]
|
||||
SingularEntry,
|
||||
#[error("io error")]
|
||||
IO(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
pub fn select(entries: &[Entry]) -> Result<u16, SelectionError> {
|
||||
if entries.is_empty() {
|
||||
return Err(SelectionError::NoEntries);
|
||||
}
|
||||
if entries.len() == 1 {
|
||||
return Err(SelectionError::SingularEntry);
|
||||
}
|
||||
actual_selection(entries).map_err(SelectionError::IO)
|
||||
}
|
||||
|
||||
fn actual_selection(entries: &[Entry]) -> Result<u16, std::io::Error> {
|
||||
let (width, height) = terminal::size()?;
|
||||
let mut stdout = io::stdout();
|
||||
stdout.queue(Hide)?;
|
||||
let mut selected: u16 = 0;
|
||||
let offset = width / 3;
|
||||
let offset = if entries.iter().any(|m| m.image.is_some()) {
|
||||
width / 3
|
||||
} else {
|
||||
width / 2
|
||||
};
|
||||
let mut should_render = true;
|
||||
loop {
|
||||
if should_render {
|
||||
@@ -123,14 +146,14 @@ pub fn select(entries: &[Entry]) -> Result<u16, std::io::Error> {
|
||||
.flush()?;
|
||||
return Ok(selected);
|
||||
}
|
||||
Action::Exit => {
|
||||
stdout
|
||||
.queue(MoveTo(0, 0))?
|
||||
.queue(Clear(ClearType::All))?
|
||||
.queue(Show)?
|
||||
.flush()?;
|
||||
exit();
|
||||
}
|
||||
// Action::Exit => {
|
||||
// stdout
|
||||
// .queue(MoveTo(0, 0))?
|
||||
// .queue(Clear(ClearType::All))?
|
||||
// .queue(Show)?
|
||||
// .flush()?;
|
||||
// exit();
|
||||
// }
|
||||
}
|
||||
stdout.queue(MoveTo(0, selected))?;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::response_deserializer;
|
||||
use crate::{response_deserializer, response_deserializer::ResponseResult};
|
||||
|
||||
#[test]
|
||||
fn loops() {
|
||||
let search_result = std::fs::read_to_string("test_data/search_result.json").unwrap();
|
||||
response_deserializer::deserializer(&search_result);
|
||||
assert_eq!(response_deserializer::deserializer(&search_result).unwrap().result , ResponseResult::Ok);
|
||||
}
|
||||
}
|
||||
|
||||
386
src/util.rs
386
src/util.rs
@@ -1,8 +1,14 @@
|
||||
use crate::response_deserializer::ChapterImages;
|
||||
use crate::Chapter;
|
||||
use crate::BASE;
|
||||
use icy_sixel::{DiffusionMethod, MethodForLargest, MethodForRep, PixelFormat, Quality};
|
||||
use image::DynamicImage;
|
||||
use reqwest_middleware::ClientWithMiddleware;
|
||||
use std::{io, io::Write};
|
||||
|
||||
type Client = ClientWithMiddleware;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Selection {
|
||||
pub selection_type: SelectionType,
|
||||
pub bonus: bool, // Allows including or excluding bonus chapters and volumes
|
||||
@@ -17,7 +23,7 @@ impl Selection {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Debug)]
|
||||
pub enum CoverSize {
|
||||
#[default]
|
||||
Full,
|
||||
@@ -25,49 +31,48 @@ pub enum CoverSize {
|
||||
W512,
|
||||
}
|
||||
|
||||
pub enum SelectionType {
|
||||
Volume(VolumeSelection),
|
||||
Chapter(ChapterSelection),
|
||||
}
|
||||
|
||||
pub enum VolumeSelection {
|
||||
All,
|
||||
Range(u32, u32),
|
||||
List(Vec<u32>),
|
||||
Single(u32),
|
||||
}
|
||||
|
||||
pub enum ChapterSelection {
|
||||
#[derive(Default, Debug)]
|
||||
pub enum SelectionRange {
|
||||
Range(f32, f32),
|
||||
List(Vec<f32>),
|
||||
#[default]
|
||||
All,
|
||||
Single(f32),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SelectionType {
|
||||
Volume(SelectionRange),
|
||||
Chapter(SelectionRange),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Config {
|
||||
pub bonus: Option<bool>,
|
||||
pub result_limit: u32,
|
||||
pub result_limit: Option<u32>,
|
||||
pub cover_size: CoverSize,
|
||||
pub selection_type: Option<ConfigSelectionType>,
|
||||
pub selection_range: Option<String>,
|
||||
pub selection_range: Option<SelectionRange>,
|
||||
pub search: Option<ConfigSearch>,
|
||||
pub cover: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ConfigSearch {
|
||||
Query(String),
|
||||
Id(crate::Id),
|
||||
}
|
||||
|
||||
fn filter_bonus(bonus: bool, volume: Option<u32>, chapter: Option<f32>) -> bool {
|
||||
fn filter_bonus(bonus: bool, volume: Option<f32>, chapter: Option<f32>) -> bool {
|
||||
if bonus {
|
||||
return true;
|
||||
}
|
||||
if let Some(volume) = volume {
|
||||
if volume > 0 {
|
||||
if let Some(chapter) = chapter {
|
||||
return chapter.round() == chapter;
|
||||
}
|
||||
return chapter.round() == chapter
|
||||
&& volume.round() == volume
|
||||
&& chapter >= 1.
|
||||
&& volume >= 1.;
|
||||
}
|
||||
}
|
||||
false
|
||||
@@ -77,11 +82,11 @@ pub fn get_chapters_from_selection(selection: Selection, chapters: &[Chapter]) -
|
||||
let bonus = selection.bonus;
|
||||
match selection.selection_type {
|
||||
SelectionType::Volume(v) => match v {
|
||||
VolumeSelection::All => chapters
|
||||
SelectionRange::All => chapters
|
||||
.iter()
|
||||
.filter(|m| filter_bonus(bonus, m.attributes.volume, m.attributes.chapter))
|
||||
.collect(),
|
||||
VolumeSelection::Single(a) => chapters
|
||||
SelectionRange::Single(a) => chapters
|
||||
.iter()
|
||||
.filter(|m| {
|
||||
if let Some(n) = m.attributes.volume {
|
||||
@@ -91,7 +96,7 @@ pub fn get_chapters_from_selection(selection: Selection, chapters: &[Chapter]) -
|
||||
false
|
||||
})
|
||||
.collect(),
|
||||
VolumeSelection::List(list) => chapters
|
||||
SelectionRange::List(list) => chapters
|
||||
.iter()
|
||||
.filter(|m| {
|
||||
if let Some(v) = m.attributes.volume {
|
||||
@@ -101,7 +106,7 @@ pub fn get_chapters_from_selection(selection: Selection, chapters: &[Chapter]) -
|
||||
false
|
||||
})
|
||||
.collect(),
|
||||
VolumeSelection::Range(a, b) => chapters
|
||||
SelectionRange::Range(a, b) => chapters
|
||||
.iter()
|
||||
.filter(|m| {
|
||||
if let Some(v) = m.attributes.volume {
|
||||
@@ -114,11 +119,11 @@ pub fn get_chapters_from_selection(selection: Selection, chapters: &[Chapter]) -
|
||||
.collect(),
|
||||
},
|
||||
SelectionType::Chapter(c) => match c {
|
||||
ChapterSelection::All => chapters
|
||||
SelectionRange::All => chapters
|
||||
.iter()
|
||||
.filter(|m| filter_bonus(bonus, m.attributes.volume, m.attributes.chapter))
|
||||
.collect(),
|
||||
ChapterSelection::Single(a) => chapters
|
||||
SelectionRange::Single(a) => chapters
|
||||
.iter()
|
||||
.filter(|m| {
|
||||
if let Some(c) = m.attributes.chapter {
|
||||
@@ -128,7 +133,7 @@ pub fn get_chapters_from_selection(selection: Selection, chapters: &[Chapter]) -
|
||||
false
|
||||
})
|
||||
.collect(),
|
||||
ChapterSelection::List(list) => chapters
|
||||
SelectionRange::List(list) => chapters
|
||||
.iter()
|
||||
.filter(|m| {
|
||||
if let Some(n) = m.attributes.chapter {
|
||||
@@ -138,7 +143,7 @@ pub fn get_chapters_from_selection(selection: Selection, chapters: &[Chapter]) -
|
||||
false
|
||||
})
|
||||
.collect(),
|
||||
ChapterSelection::Range(a, b) => chapters
|
||||
SelectionRange::Range(a, b) => chapters
|
||||
.iter()
|
||||
.filter(|m| {
|
||||
if let Some(c) = m.attributes.chapter {
|
||||
@@ -153,54 +158,16 @@ pub fn get_chapters_from_selection(selection: Selection, chapters: &[Chapter]) -
|
||||
}
|
||||
}
|
||||
|
||||
pub fn choose_volumes(input: &str) -> Option<VolumeSelection> {
|
||||
if let Some(x) = input.find("..") {
|
||||
match (input[0..x].parse(), input[x + 2..].parse::<u32>()) {
|
||||
(Ok(a), Ok(b)) => {
|
||||
if a > b {
|
||||
eprintln!("Invalid range: a > b");
|
||||
None
|
||||
} else {
|
||||
Some(VolumeSelection::Range(a, b))
|
||||
pub fn ask_selection_range() -> SelectionRange {
|
||||
loop {
|
||||
match parse_selection_range(&get_input("Choose chapters/volumes: ")) {
|
||||
Some(v) => return v,
|
||||
None => eprintln!("Invalid selection"),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
eprintln!("Invalid range");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else if let Some(x) = input.find(":") {
|
||||
match (input[0..x].parse(), input[x + 2..].parse::<u32>()) {
|
||||
(Ok(a), Ok(b)) => Some(VolumeSelection::Range(a, b)),
|
||||
_ => {
|
||||
eprintln!("Invalid range");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else if input.contains(",") {
|
||||
match input
|
||||
.split(",")
|
||||
.map(|m| m.parse::<u32>())
|
||||
.collect::<Result<Vec<u32>, _>>()
|
||||
{
|
||||
Ok(v) => Some(VolumeSelection::List(v)),
|
||||
Err(e) => {
|
||||
eprintln!("Invalid number in list: {e:#?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else if input == "all" {
|
||||
Some(VolumeSelection::All)
|
||||
} else {
|
||||
if let Ok(n) = input.parse() {
|
||||
return Some(VolumeSelection::Single(n));
|
||||
}
|
||||
eprintln!("Invalid input");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn choose_chapters(input: &str) -> Option<ChapterSelection> {
|
||||
fn parse_selection_range(input: &str) -> Option<SelectionRange> {
|
||||
if let Some(x) = input.find("..") {
|
||||
match (input[0..x].parse(), input[x + 2..].parse()) {
|
||||
// Inclusive range
|
||||
@@ -209,7 +176,7 @@ pub fn choose_chapters(input: &str) -> Option<ChapterSelection> {
|
||||
eprintln!("Invalid range: a > b");
|
||||
None
|
||||
} else {
|
||||
Some(ChapterSelection::Range(a, b))
|
||||
Some(SelectionRange::Range(a, b))
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
@@ -233,19 +200,31 @@ pub fn choose_chapters(input: &str) -> Option<ChapterSelection> {
|
||||
if invalid {
|
||||
None
|
||||
} else {
|
||||
Some(ChapterSelection::List(list))
|
||||
Some(SelectionRange::List(list))
|
||||
}
|
||||
} else if input == "all" {
|
||||
Some(ChapterSelection::All)
|
||||
Some(SelectionRange::All)
|
||||
} else if let Ok(n) = input.parse() {
|
||||
Some(SelectionRange::Single(n))
|
||||
} else {
|
||||
if let Ok(n) = input.parse() {
|
||||
return Some(ChapterSelection::Single(n));
|
||||
}
|
||||
eprintln!("Invalid input");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ask_selection_type() -> ConfigSelectionType {
|
||||
loop {
|
||||
match get_input("Select by volume or chapter? [v/c] : ").as_str() {
|
||||
"v" | "volume" => break ConfigSelectionType::Volume,
|
||||
"c" | "chapter" => break ConfigSelectionType::Chapter,
|
||||
_ => {
|
||||
eprintln!("Invalid response");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_input(msg: &str) -> String {
|
||||
print!("{msg}");
|
||||
io::stdout().flush().expect("failed to flush stdout");
|
||||
@@ -295,8 +274,10 @@ pub fn convert_to_sixel(data: &[u8]) -> String {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub enum ConfigSelectionType {
|
||||
Volume,
|
||||
#[default]
|
||||
Chapter,
|
||||
}
|
||||
|
||||
@@ -306,7 +287,7 @@ impl Config {
|
||||
bonus: None,
|
||||
search: None,
|
||||
cover_size: CoverSize::default(),
|
||||
result_limit: 5,
|
||||
result_limit: None,
|
||||
selection_type: None,
|
||||
selection_range: None,
|
||||
cover: None,
|
||||
@@ -316,6 +297,230 @@ impl Config {
|
||||
|
||||
use crate::Id;
|
||||
|
||||
use crate::error::{ChapterImageError, ChapterImagesError};
|
||||
pub async fn download_the_stuff(
|
||||
client: &Client,
|
||||
selected_chapters: &Vec<&Chapter>,
|
||||
title: &str,
|
||||
create_volumes: bool,
|
||||
) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
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;
|
||||
let chapter_text = if let Some(n) = chapter_n {
|
||||
n.to_string()
|
||||
} else {
|
||||
String::from("_none")
|
||||
};
|
||||
let chapter_path = format!(
|
||||
"images/{}/chapter_{:0>3}_image_{:0>3}.png",
|
||||
title, 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, 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()
|
||||
&& !&path.parent().unwrap().exists()
|
||||
{
|
||||
if create_volumes && !path.parent().unwrap().parent().unwrap().exists() {
|
||||
std::fs::create_dir(path.parent().unwrap().parent().unwrap()).unwrap();
|
||||
}
|
||||
std::fs::create_dir(path.parent().unwrap()).unwrap();
|
||||
}
|
||||
image.save(path).unwrap();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
panic!("{e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_volumes_or_chapters(
|
||||
selected_chapters: &Vec<&Chapter>,
|
||||
create_volumes: bool,
|
||||
title: &str,
|
||||
) {
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use zip::write::{SimpleFileOptions, ZipWriter};
|
||||
use zip::CompressionMethod;
|
||||
|
||||
if create_volumes {
|
||||
let mut volumes = Vec::new();
|
||||
selected_chapters
|
||||
.iter()
|
||||
.filter_map(|m| m.attributes.volume)
|
||||
.for_each(|v| {
|
||||
if !volumes.contains(&v) {
|
||||
volumes.push(v);
|
||||
}
|
||||
});
|
||||
for volume in volumes {
|
||||
let image_paths = format!("images/{}/volume_{:0>3}", title, volume);
|
||||
let image_paths = Path::new(&image_paths);
|
||||
|
||||
let zip_file_path = format!("{} - Volume {:0>3}.cbz", title, 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);
|
||||
let image_paths = Path::new(&image_paths);
|
||||
|
||||
let zip_file_path = format!("{} - Chapter {:0>3}.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();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn download_chapter_images(
|
||||
client: &Client,
|
||||
image_data: &ChapterImages,
|
||||
chapter: &Chapter,
|
||||
) -> Result<Vec<image::DynamicImage>, reqwest_middleware::Error> {
|
||||
let mut data_futures = vec![];
|
||||
for (i, file_name) in image_data.chapter.data.iter().enumerate() {
|
||||
let base_url: &str = image_data.base_url.as_str();
|
||||
let hash: &str = image_data.chapter.hash.as_str();
|
||||
let future = async move {
|
||||
let data = client
|
||||
.clone()
|
||||
.get(format!("{base_url}/data/{hash}/{file_name}"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.bytes()
|
||||
.await
|
||||
.unwrap();
|
||||
println!(
|
||||
"\x1b[1A\x1b[2KDownloaded volume: {:?}, chapter: {:?}, title: {}, [{}/{}]",
|
||||
chapter.attributes.volume,
|
||||
chapter.attributes.chapter,
|
||||
chapter.attributes.title,
|
||||
i,
|
||||
chapter.attributes.pages
|
||||
);
|
||||
data
|
||||
};
|
||||
data_futures.push(future);
|
||||
}
|
||||
Ok(futures::future::join_all(data_futures)
|
||||
.await
|
||||
.iter()
|
||||
.map(|m| image::load_from_memory(m).unwrap())
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn ask_cover() -> bool {
|
||||
loop {
|
||||
match get_input("Search with covers? [y/n] : ").as_str() {
|
||||
"y" | "yes" => break true,
|
||||
"n" | "no" => break false,
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn ask_bonus() -> bool {
|
||||
loop {
|
||||
match get_input("Read bonus chapters? [y/n] : ").as_str() {
|
||||
"y" | "yes" => break true,
|
||||
"n" | "no" => break false,
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn args() -> Config {
|
||||
let mut args = std::env::args().skip(1);
|
||||
|
||||
@@ -328,7 +533,7 @@ pub fn args() -> Config {
|
||||
std::process::exit(1);
|
||||
}
|
||||
config.selection_range = match args.next() {
|
||||
Some(selection_type) => Some(selection_type),
|
||||
Some(selection_range) => parse_selection_range(&selection_range),
|
||||
None => {
|
||||
eprintln!("Missing value for selection range, type: String");
|
||||
std::process::exit(1);
|
||||
@@ -402,7 +607,7 @@ pub fn args() -> Config {
|
||||
"-r" | "--result-limit" => {
|
||||
config.result_limit = match args.next() {
|
||||
Some(a) => match a.parse() {
|
||||
Ok(v) => v,
|
||||
Ok(v) => Some(v),
|
||||
Err(e) => {
|
||||
eprintln!("Failed to parse value for result-limit: {e:?}, type: u32");
|
||||
std::process::exit(1);
|
||||
@@ -455,3 +660,22 @@ pub fn args() -> Config {
|
||||
}
|
||||
config
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn filter() {
|
||||
assert!(filter_bonus(false, Some(1.), Some(1.)));
|
||||
assert!(filter_bonus(false, Some(2.), Some(1.)));
|
||||
assert!(filter_bonus(false, Some(1.), Some(2.)));
|
||||
assert!(filter_bonus(false, Some(2.), Some(2.)));
|
||||
assert!(!filter_bonus(false, Some(1.), Some(0.)));
|
||||
assert!(!filter_bonus(false, Some(0.), Some(1.)));
|
||||
assert!(!filter_bonus(false, Some(1.), Some(-1.)));
|
||||
assert!(!filter_bonus(false, Some(-1.), Some(1.)));
|
||||
assert!(!filter_bonus(false, Some(1.5), Some(1.)));
|
||||
assert!(!filter_bonus(false, Some(1.), Some(1.5)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user