use client::{MangaClient, MangaClientBuilder, SearchFilter}; use response_deserializer::{Chapter, Id, Language, Manga}; use select::Entry; use std::future::Future; use std::pin::Pin; use util::SelectionType; mod client; mod error; mod response_deserializer; mod select; mod test; mod util; const BASE: &str = "https://api.mangadex.dev"; #[tokio::main] async fn main() { let config = util::args(); let manga_client = MangaClientBuilder::new() .bonus(config.bonus.unwrap_or_else(util::ask_bonus)) .cover(config.cover.unwrap_or_else(util::ask_cover)) .result_limit(config.result_limit.unwrap_or(5)) .cover_size(util::CoverSize::W512) .selection_type( config .selection_type .unwrap_or_else(util::ask_selection_type), ) .selection_range( config .selection_range .unwrap_or_else(util::ask_selection_range), ) .search(config.search.unwrap_or_else(|| { util::ConfigSearch::Query(util::get_input("Enter search query: ")) })) .search_filter(SearchFilter::default()) .language(Language::default()) .build(); let search_result = manga_client.get_manga().await; let mut choice = 0; if search_result.len() > 1 { choice = select_manga_from_search(&manga_client, &search_result).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 .unwrap_or(-1.) .partial_cmp(&b.attributes.chapter.unwrap_or(-1.)) .unwrap() }); let create_volumes = matches!( manga_client.selection.selection_type, SelectionType::Volume(_) ); let selected_chapters = util::get_chapters_from_selection(manga_client.selection, &chapters); let title = &manga.attributes.title.en; util::download_the_stuff( &manga_client.client, &selected_chapters, title, create_volumes, ) .await; util::create_volumes_or_chapters(&selected_chapters, create_volumes, title); } async fn select_manga_from_search(client: &MangaClient, results: &[Manga]) -> u16 { let cover_ex = match client.cover_size { util::CoverSize::Full => "", util::CoverSize::W256 => ".256.jpg", util::CoverSize::W512 => ".512.jpg", }; let mut entry_futures: Vec>>> = Vec::new(); 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); } 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 // Cover data should only be present if used assert!(client.cover); let future = async move { let image_url = format!( "https://uploads.mangadex.org/covers/{id}/{}{cover_ex}", &cover_data.file_name ); let data = client .client .get(&image_url) .send() .await .unwrap() .bytes() .await .unwrap(); let result = util::convert_to_sixel(&data); entry.set_image(result); entry }; entry_futures.push(Box::pin(future)); } else { entry_futures.push(Box::pin(async move { entry })); } } let entries: Vec = futures::future::join_all(entry_futures).await; assert!(!entries.is_empty()); if entries.len() == 1 { return 0; } let choice = match select::select(&entries) { Ok(v) => v, Err(e) => { eprintln!("ERROR: Failed to select: {e:?}"); std::process::exit(1); } }; choice }