diff --git a/src/rust/zh.boylove/Cargo.lock b/src/rust/zh.boylove/Cargo.lock index ec4a9f92f..ffd57afb2 100644 --- a/src/rust/zh.boylove/Cargo.lock +++ b/src/rust/zh.boylove/Cargo.lock @@ -5,7 +5,7 @@ version = 3 [[package]] name = "aidoku" version = "0.2.0" -source = "git+https://github.com/Aidoku/aidoku-rs/#004bddabade7b24c58cf925b08f90dd093b00c9d" +source = "git+https://github.com/Aidoku/aidoku-rs#004bddabade7b24c58cf925b08f90dd093b00c9d" dependencies = [ "aidoku_helpers", "aidoku_imports", @@ -17,7 +17,7 @@ dependencies = [ [[package]] name = "aidoku_helpers" version = "0.1.0" -source = "git+https://github.com/Aidoku/aidoku-rs/#004bddabade7b24c58cf925b08f90dd093b00c9d" +source = "git+https://github.com/Aidoku/aidoku-rs#004bddabade7b24c58cf925b08f90dd093b00c9d" dependencies = [ "aidoku_imports", ] @@ -25,28 +25,35 @@ dependencies = [ [[package]] name = "aidoku_imports" version = "0.2.0" -source = "git+https://github.com/Aidoku/aidoku-rs/#004bddabade7b24c58cf925b08f90dd093b00c9d" +source = "git+https://github.com/Aidoku/aidoku-rs#004bddabade7b24c58cf925b08f90dd093b00c9d" [[package]] name = "aidoku_macros" version = "0.1.0" -source = "git+https://github.com/Aidoku/aidoku-rs/#004bddabade7b24c58cf925b08f90dd093b00c9d" +source = "git+https://github.com/Aidoku/aidoku-rs#004bddabade7b24c58cf925b08f90dd093b00c9d" [[package]] name = "aidoku_proc_macros" version = "0.2.0" -source = "git+https://github.com/Aidoku/aidoku-rs/#004bddabade7b24c58cf925b08f90dd093b00c9d" +source = "git+https://github.com/Aidoku/aidoku-rs#004bddabade7b24c58cf925b08f90dd093b00c9d" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + [[package]] name = "boylove" version = "0.1.0" dependencies = [ "aidoku", + "base64", ] [[package]] @@ -75,9 +82,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.32" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] diff --git a/src/rust/zh.boylove/Cargo.toml b/src/rust/zh.boylove/Cargo.toml index ee72517ac..d394853ee 100644 --- a/src/rust/zh.boylove/Cargo.toml +++ b/src/rust/zh.boylove/Cargo.toml @@ -16,4 +16,5 @@ strip = true lto = true [dependencies] -aidoku = { git = "https://github.com/Aidoku/aidoku-rs/", features = ["helpers"] } +aidoku = { git = "https://github.com/Aidoku/aidoku-rs", features = ["helpers"] } +base64 = { version = "0.21.2", default-features = false, features = ["alloc"] } diff --git a/src/rust/zh.boylove/res/filters.json b/src/rust/zh.boylove/res/filters.json index 198e49669..4b0a3c7d4 100644 --- a/src/rust/zh.boylove/res/filters.json +++ b/src/rust/zh.boylove/res/filters.json @@ -2,6 +2,15 @@ { "type": "title" }, + { + "type": "select", + "name": "閱覽權限", + "options": [ + "全部", + "一般", + "VIP" + ] + }, { "type": "select", "name": "連載狀態", diff --git a/src/rust/zh.boylove/res/settings.json b/src/rust/zh.boylove/res/settings.json index 19521e753..835a91dfc 100644 --- a/src/rust/zh.boylove/res/settings.json +++ b/src/rust/zh.boylove/res/settings.json @@ -1,6 +1,7 @@ [ { "type": "group", + "title": "變更語系", "items": [ { "type": "switch", @@ -10,5 +11,47 @@ "default": true } ] + }, + { + "type": "group", + "title": "用戶登入", + "footer": "驗證碼欄位非四位數字時按登入,可取得驗證碼圖片;圖片以Base64編碼,可使用捷徑或線上工具解碼。", + "items": [ + { + "type": "text", + "key": "username", + "placeholder": "用戶名", + "autocapitalizationType": 0, + "autocorrectionType": 1, + "spellCheckingType": 1, + "keyboardType": 1 + }, + { + "type": "text", + "key": "password", + "placeholder": "密碼", + "autocapitalizationType": 0, + "autocorrectionType": 1, + "spellCheckingType": 1 + }, + { + "type": "text", + "key": "captcha", + "placeholder": "驗證碼", + "keyboardType": 4 + }, + { + "type": "button", + "title": "登入", + "action": "signIn", + "destructive": true + }, + { + "type": "switch", + "key": "autoCheckIn", + "title": "自動簽到", + "default": false + } + ] } ] diff --git a/src/rust/zh.boylove/res/source.json b/src/rust/zh.boylove/res/source.json index 3d13208c4..93d465b9f 100644 --- a/src/rust/zh.boylove/res/source.json +++ b/src/rust/zh.boylove/res/source.json @@ -3,7 +3,7 @@ "id": "zh.boylove", "lang": "zh", "name": "香香腐宅", - "version": 3, + "version": 4, "url": "https://boylove.cc", "urls": [ "https://boylove.cc", diff --git a/src/rust/zh.boylove/src/lib.rs b/src/rust/zh.boylove/src/lib.rs index 73e10bf27..f9482f929 100644 --- a/src/rust/zh.boylove/src/lib.rs +++ b/src/rust/zh.boylove/src/lib.rs @@ -1,101 +1,22 @@ #![no_std] +extern crate alloc; +mod url; + use aidoku::{ error::Result, - helpers::{substring::Substring, uri::encode_uri_component}, + helpers::substring::Substring, prelude::*, - std::{defaults::defaults_get, html::unescape_html_entities, net::Request, String, Vec}, - Chapter, DeepLink, Filter, - FilterType::{Genre, Select, Sort, Title}, - Manga, MangaContentRating, MangaPageResult, - MangaStatus::{Completed, Ongoing, Unknown}, - Page, + std::{ + defaults::defaults_get, + html::unescape_html_entities, + net::{HttpMethod, Request}, + String, ValueRef, Vec, + }, + Chapter, DeepLink, Filter, Manga, MangaContentRating, MangaPageResult, MangaStatus, Page, }; - -extern crate alloc; use alloc::string::ToString; - -enum Url<'a> { - /// https://boylove.cc{path} - Abs(&'a str), - - /// https://boylove.cc/home/api/searchk?keyword={}&type={}&pageNo={} - /// - /// --- - /// - /// `keyword` ➡️ Should be percent-encoded - /// - /// `type`: - /// - /// - **`1`: 漫畫** ➡️ Always - /// - `2`: 小說 - /// - /// `pageNo`: Start from `1` - Search(&'a str, i32), - - /// https://boylove.cc/home/api/cate/tp/1-{tags}-{status}-{sort_by}-{page}-{content_rating}-{content_type}-{viewing_permission} - /// - /// --- - /// - /// `content_type`: - /// - /// - **`1`: 漫畫** ➡️ Always - /// - `2`: 小說 - /// - /// `viewing_permission`: - /// - /// - `2`: 全部 - /// - **`0`: 一般** ➡️ Always - /// - ~~`1`: VIP~~ ➡️ Login cookie is required to view manga for VIP members - Filters { - /// - `0`: 全部 - /// - `A+B+…+Z` ➡️ Should be percent-encoded - tags: &'a str, - - /// - `2`: 全部 - /// - `0`: 連載中 - /// - `1`: 已完結 - status: u8, - - /// - `0`: 人氣 ➡️ ❗️**Not sure**❗️ - /// - `1`: 最新更新 - sort_by: u8, - - /// Start from `1` - page: i32, - - /// - `0`: 全部 - /// - `1`: 清水 - /// - `2`: 有肉 - content_rating: u8, - // // - // // viewing_permission: u8, - }, - - /// https://boylove.cc/home/api/chapter_list/tp/{manga_id}-0-0-10 - ChapterList(&'a str), - - /// https://boylove.cc/home/book/index/id/{manga_id} - Manga(&'a str), - - /// https://boylove.cc/home/book/capter/id/{chapter_id} - Chapter(&'a str), -} - -const DOMAIN: &str = "https://boylove.cc"; -const MANGA_PATH: &str = "index/id/"; -const CHAPTER_PATH: &str = "capter/id/"; - -/// Chrome 114 on macOS -const USER_AGENT: &str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36"; - -/// 連載狀態:\[全部, 連載中, 已完結\] -const FILTER_STATUS: [u8; 3] = [2, 0, 1]; - -/// 內容分級:\[全部, 清水, 有肉\] -const FILTER_CONTENT_RATING: [u8; 3] = [0, 1, 2]; - -/// 排序依據:\[最新更新, 人氣\] -const SORT: [u8; 2] = [1, 0]; +use base64::{engine::general_purpose, Engine}; +use url::{Url, CHAPTER_PATH, DOMAIN, MANGA_PATH, USER_AGENT}; #[initialize] fn initialize() { @@ -104,8 +25,11 @@ fn initialize() { #[get_manga_list] fn get_manga_list(filters: Vec, page: i32) -> Result { - let manga_list_url = get_filtered_url(filters, page)?; - let manga_list_json = request_get(&manga_list_url).json()?; + if page == 1 { + check_in(); + } + + let manga_list_json = Url::from(filters, page)?.request(HttpMethod::Get).json()?; let manga_list_obj = manga_list_json.as_object()?; let result = manga_list_obj.get("result").as_object()?; @@ -125,7 +49,7 @@ fn get_manga_list(filters: Vec, page: i32) -> Result { let manga_id = manga_obj.get("id").as_int()?.to_string(); let cover_path = manga_obj.get("image").as_string()?.read(); - let cover_url = Url::Abs(&cover_path).to_string(); + let cover_url = Url::Abs(cover_path).to_string(); let manga_title = manga_obj.get("title").as_string()?.read(); @@ -146,9 +70,9 @@ fn get_manga_list(filters: Vec, page: i32) -> Result { .collect::>(); let status = match manga_obj.get("mhstatus").as_int()? { - 0 => Ongoing, - 1 => Completed, - _ => Unknown, + 0 => MangaStatus::Ongoing, + 1 => MangaStatus::Completed, + _ => MangaStatus::Unknown, }; let content_rating = get_content_rating(&categories); @@ -175,9 +99,7 @@ fn get_manga_list(filters: Vec, page: i32) -> Result { #[get_manga_details] fn get_manga_details(manga_id: String) -> Result { - let manga_url = Url::Manga(&manga_id).to_string(); - - let manga_html = request_get(&manga_url).html()?; + let manga_html = Url::Manga(&manga_id).request(HttpMethod::Get).html()?; let cover_url = manga_html.select("a.play").attr("abs:data-original").read(); @@ -202,6 +124,8 @@ fn get_manga_details(manga_id: String) -> Result { description = description_removed_closing_tag.trim().to_string(); } + let manga_url = Url::Manga(&manga_id).to_string(); + let categories = manga_html .select("a.tag > span") .array() @@ -210,9 +134,9 @@ fn get_manga_details(manga_id: String) -> Result { .collect::>(); let status = match manga_html.select("p.data").first().text().read().as_str() { - "连载中" | "連載中" => Ongoing, - "完结" | "完結" => Completed, - _ => Unknown, + "连载中" | "連載中" => MangaStatus::Ongoing, + "完结" | "完結" => MangaStatus::Completed, + _ => MangaStatus::Unknown, }; let content_rating = get_content_rating(&categories); @@ -234,8 +158,7 @@ fn get_manga_details(manga_id: String) -> Result { #[get_chapter_list] fn get_chapter_list(manga_id: String) -> Result> { - let chapter_list_url = Url::ChapterList(&manga_id).to_string(); - let chapter_list_json = request_get(&chapter_list_url).json()?; + let chapter_list_json = Url::ChapterList(manga_id).request(HttpMethod::Get).json()?; let chapter_list_obj = chapter_list_json.as_object()?; let result = chapter_list_obj.get("result").as_object()?; @@ -268,8 +191,7 @@ fn get_chapter_list(manga_id: String) -> Result> { #[get_page_list] fn get_page_list(_manga_id: String, chapter_id: String) -> Result> { - let chapter_url = Url::Chapter(&chapter_id).to_string(); - let chapter_html = request_get(&chapter_url).html()?; + let chapter_html = Url::Chapter(&chapter_id).request(HttpMethod::Get).html()?; let mut pages = Vec::::new(); let page_nodes = chapter_html.select("img.lazy[id]"); @@ -280,7 +202,7 @@ fn get_page_list(_manga_id: String, chapter_id: String) -> Result> { .read() .trim() .to_string(); - let page_url = Url::Abs(&page_path).to_string(); + let page_url = Url::Abs(page_path).to_string(); pages.push(Page { index: page_index as i32, @@ -325,7 +247,7 @@ fn handle_url(url: String) -> Result { ..Default::default() }); - let chapter_html = request_get(&url).html()?; + let chapter_html = Url::Chapter(chapter_id).request(HttpMethod::Get).html()?; let manga_url = chapter_html .select("a.icon-only.link.back") .attr("href") @@ -343,87 +265,20 @@ fn handle_url(url: String) -> Result { #[handle_notification] fn handle_notification(notification: String) { - if notification.as_str() == "switchChineseCharSet" { - switch_chinese_char_set(); + match notification.as_str() { + "switchChineseCharSet" => switch_chinese_char_set(), + "signIn" => sign_in().unwrap_or_default(), + _ => (), } } fn switch_chinese_char_set() { - let is_tc = defaults_get("isTC").map_or(true, |value| value.as_bool().unwrap_or(true)); - let converting_url = format!( - "{}/home/user/to{}.html", - DOMAIN, - if is_tc { "T" } else { "S" } - ); - request_get(&converting_url).send(); -} + let is_tc = defaults_get("isTC") + .and_then(|value| value.as_bool()) + .unwrap_or(true); + let char_set = if is_tc { "T" } else { "S" }; -fn get_filtered_url(filters: Vec, page: i32) -> Result { - let mut filter_status = FILTER_STATUS[0]; - let mut filter_content_rating = FILTER_CONTENT_RATING[0]; - let mut filter_tags_vec = Vec::::new(); - let mut sort_by = SORT[0]; - - for filter in filters { - match filter.kind { - Select => { - let index = filter.value.as_int().unwrap_or(0) as usize; - match filter.name.as_str() { - "連載狀態" => filter_status = FILTER_STATUS[index], - "內容分級" => filter_content_rating = FILTER_CONTENT_RATING[index], - _ => continue, - } - } - - Sort => { - let obj = filter.value.as_object()?; - let index = obj.get("index").as_int().unwrap_or(0) as usize; - sort_by = SORT[index]; - } - - Title => { - let encoded_search_str = encode_uri_component(filter.value.as_string()?.read()); - - return Ok(Url::Search(&encoded_search_str, page).to_string()); - } - - Genre => { - let is_not_checked = filter.value.as_int().unwrap_or(-1) != 1; - if is_not_checked { - continue; - } - - let encoded_tag = encode_uri_component(filter.name); - filter_tags_vec.push(encoded_tag); - } - - _ => continue, - } - } - - let filter_tags_str = match filter_tags_vec.is_empty() { - // ? 全部 - true => "0".to_string(), - - false => filter_tags_vec.join("+"), - }; - - Ok(Url::Filters { - tags: &filter_tags_str, - status: filter_status, - sort_by, - page, - content_rating: filter_content_rating, - } - .to_string()) -} - -/// Start a new GET request with the given URL with headers `Referer` and -/// `User-Agent` set. -fn request_get(url: &str) -> Request { - Request::get(url) - .header("Referer", DOMAIN) - .header("User-Agent", USER_AGENT) + Url::CharSet(char_set).request(HttpMethod::Get).send(); } /// Returns [`Safe`](MangaContentRating::Safe) if the given slice contains @@ -435,41 +290,51 @@ fn get_content_rating(categories: &[String]) -> MangaContentRating { MangaContentRating::Nsfw } -impl core::fmt::Display for Url<'_> { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - let api_path = format!("{}/home/api/", DOMAIN); - let html_path = format!("{}/home/book/", DOMAIN); - - match self { - Self::Abs(path) => write!(f, "{}{}", DOMAIN, path), - - Self::Search(search_str, page) => write!( - f, - "{}searchk?keyword={}&type=1&pageNo={}", - api_path, search_str, page - ), - - Self::Filters { - tags, - status, - sort_by, - page, - content_rating, - } => write!( - f, - "{}cate/tp/1-{}-{}-{}-{}-{}-1-0", - api_path, tags, status, sort_by, page, content_rating, - ), - - Self::ChapterList(manga_id) => { - write!(f, "{}chapter_list/tp/{}-0-0-10", api_path, manga_id) - } - - Self::Manga(manga_id) => write!(f, "{}{}{}", html_path, MANGA_PATH, manga_id), - - Self::Chapter(chapter_id) => write!(f, "{}{}{}", html_path, CHAPTER_PATH, chapter_id), - } +fn sign_in() -> Result<()> { + let captcha = defaults_get("captcha")?.as_string()?.read(); + + let is_wrong_captcha_format = captcha.parse::().is_err() || captcha.chars().count() != 4; + if is_wrong_captcha_format { + let sign_in_page = Url::SignInPage.request(HttpMethod::Get).html()?; + + let captcha_img_path = sign_in_page.select("img#verifyImg").attr("src").read(); + let captcha_img = Url::Abs(captcha_img_path).request(HttpMethod::Get).data(); + let base64_img = general_purpose::STANDARD_NO_PAD.encode(captcha_img); + + return Ok(println!("{}", base64_img)); } + + let username = defaults_get("username")?.as_string()?.read(); + let password = defaults_get("password")?.as_string()?.read(); + let sign_in_data = format!( + "username={}&password={}&vfycode={}&type=login", + username, password, captcha + ); + + let response_json = Url::SignIn + .request(HttpMethod::Post) + .body(sign_in_data) + .json()?; + let reponse_obj = response_json.as_object()?; + let info = reponse_obj.get("info").as_string()?; + + Ok(println!("{}", info)) +} + +fn check_in() { + let not_auto_check_in = !defaults_get("autoCheckIn") + .and_then(|value| value.as_bool()) + .unwrap_or(false); + if not_auto_check_in { + return; + } + + let check_in_data = "auto=false&td=&type=1"; + + Url::CheckIn + .request(HttpMethod::Post) + .body(check_in_data) + .send(); } trait Parser { @@ -477,8 +342,8 @@ trait Parser { fn get_is_ok_text(self) -> Option; } -impl Parser for aidoku::std::ValueRef { +impl Parser for ValueRef { fn get_is_ok_text(self) -> Option { - self.as_node().map_or(None, |node| Some(node.text().read())) + self.as_node().map(|node| node.text().read()).ok() } } diff --git a/src/rust/zh.boylove/src/url.rs b/src/rust/zh.boylove/src/url.rs new file mode 100644 index 000000000..1ca423763 --- /dev/null +++ b/src/rust/zh.boylove/src/url.rs @@ -0,0 +1,260 @@ +use aidoku::{ + error::Result, + helpers::uri::encode_uri_component, + prelude::format, + std::{ + net::{HttpMethod, Request}, + String, Vec, + }, + Filter, FilterType, +}; +use alloc::string::ToString; +use core::fmt::Display; + +#[derive(Clone, Copy)] +enum ViewingPermission { + All = 2, + Basic = 0, + Vip = 1, +} + +#[derive(Clone, Copy)] +enum Status { + All = 2, + Ongoing = 0, + Completed = 1, +} + +#[derive(Clone, Copy)] +enum ContentRating { + All = 0, + Safe = 1, + Nsfw = 2, +} + +#[derive(Clone, Copy)] +enum Sort { + Latest = 1, + Popular = 0, +} + +pub enum Url<'a> { + /// https://boylove.cc/home/user/to{char_set}.html + /// + /// --- + /// + /// `char_set`: + /// + /// - `T`: 繁體中文 + /// - `S`: 簡體中文 + CharSet(&'a str), + + /// https://boylove.cc{path} + Abs(String), + + /// https://boylove.cc/home/api/searchk?keyword={}&type={}&pageNo={} + /// + /// --- + /// + /// `keyword`: `search_str` ➡️ Should be percent-encoded + /// + /// `type`: + /// + /// - **`1`: 漫畫** ➡️ Always + /// - `2`: 小說 + /// + /// `pageNo`: Start from `1` + Search(String, i32), + + /// https://boylove.cc/home/api/cate/tp/1-{tags}-{status}-{sort_by}-{page}-{content_rating}-{content_type}-{viewing_permission} + /// + /// --- + /// + /// `content_type`: + /// + /// - **`1`: 漫畫** ➡️ Always + /// - `2`: 小說 + Filters { + /// - `0`: 全部 + /// - `A+B+…+Z` ➡️ Should be percent-encoded + tags: String, + + /// - `2`: 全部 + /// - `0`: 連載中 + /// - `1`: 已完結 + status: u8, + + /// - `0`: 人氣 ➡️ ❗️**Not sure**❗️ + /// - `1`: 最新更新 + sort_by: u8, + + /// Start from `1` + page: i32, + + /// - `0`: 全部 + /// - `1`: 清水 + /// - `2`: 有肉 + content_rating: u8, + + /// - `2`: 全部 + /// - `0`: 一般 + /// - `1`: VIP + viewing_permission: u8, + }, + + /// https://boylove.cc/home/api/chapter_list/tp/{manga_id}-0-0-10 + ChapterList(String), + + /// https://boylove.cc/home/book/index/id/{manga_id} + Manga(&'a str), + + /// https://boylove.cc/home/book/capter/id/{chapter_id} + Chapter(&'a str), + + /// https://boylove.cc/home/auth/login/type/login.html + SignInPage, + + /// https://boylove.cc/home/auth/login.html + SignIn, + + /// https://boylove.cc/home/api/signup.html + CheckIn, +} + +pub const DOMAIN: &str = "https://boylove.cc"; +pub const MANGA_PATH: &str = "index/id/"; +pub const CHAPTER_PATH: &str = "capter/id/"; + +/// Chrome 114 on macOS +pub const USER_AGENT: &str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36"; + +const FILTER_VIEWING_PERMISSION: [ViewingPermission; 3] = [ + ViewingPermission::All, + ViewingPermission::Basic, + ViewingPermission::Vip, +]; +const FILTER_STATUS: [Status; 3] = [Status::All, Status::Ongoing, Status::Completed]; +const FILTER_CONTENT_RATING: [ContentRating; 3] = + [ContentRating::All, ContentRating::Safe, ContentRating::Nsfw]; +const SORT: [Sort; 2] = [Sort::Latest, Sort::Popular]; + +impl<'a> Url<'a> { + pub fn from(filters: Vec, page: i32) -> Result { + let mut filter_viewing_permission = ViewingPermission::All; + let mut filter_status = Status::All; + let mut filter_content_rating = ContentRating::All; + let mut filter_tags_vec = Vec::::new(); + let mut sort_by = Sort::Latest; + + for filter in filters { + match filter.kind { + FilterType::Select => { + let index = filter.value.as_int().unwrap_or(0) as usize; + match filter.name.as_str() { + "閱覽權限" => { + filter_viewing_permission = FILTER_VIEWING_PERMISSION[index]; + } + "連載狀態" => filter_status = FILTER_STATUS[index], + "內容分級" => filter_content_rating = FILTER_CONTENT_RATING[index], + _ => continue, + } + } + + FilterType::Sort => { + let obj = filter.value.as_object()?; + let index = obj.get("index").as_int().unwrap_or(0) as usize; + sort_by = SORT[index]; + } + + FilterType::Title => { + let encoded_search_str = encode_uri_component(filter.value.as_string()?.read()); + + return Ok(Url::Search(encoded_search_str, page)); + } + + FilterType::Genre => { + let is_not_checked = filter.value.as_int().unwrap_or(-1) != 1; + if is_not_checked { + continue; + } + + let encoded_tag = encode_uri_component(filter.name); + filter_tags_vec.push(encoded_tag); + } + + _ => continue, + } + } + + let filter_tags_str = if filter_tags_vec.is_empty() { + // ? 全部 + "0".to_string() + } else { + filter_tags_vec.join("+") + }; + + Ok(Url::Filters { + tags: filter_tags_str, + status: filter_status as u8, + sort_by: sort_by as u8, + page, + content_rating: filter_content_rating as u8, + viewing_permission: filter_viewing_permission as u8, + }) + } + + /// Start a new request with the given URL with headers `Referer` and + /// `User-Agent` set. + pub fn request(self, method: HttpMethod) -> Request { + Request::new(self.to_string(), method) + .header("Referer", DOMAIN) + .header("User-Agent", USER_AGENT) + } +} + +impl<'a> Display for Url<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let api_path = format!("{}/home/api/", DOMAIN); + let html_path = format!("{}/home/book/", DOMAIN); + let auth_path = format!("{}/home/auth/", DOMAIN); + + match self { + Self::CharSet(char_set) => write!(f, "{}/home/user/to{}.html", DOMAIN, char_set), + + Self::Abs(path) => write!(f, "{}{}", DOMAIN, path), + + Self::Search(search_str, page) => write!( + f, + "{}searchk?keyword={}&type=1&pageNo={}", + api_path, search_str, page + ), + + Self::Filters { + tags, + status, + sort_by, + page, + content_rating, + viewing_permission, + } => write!( + f, + "{}cate/tp/1-{}-{}-{}-{}-{}-1-{}", + api_path, tags, status, sort_by, page, content_rating, viewing_permission + ), + + Self::ChapterList(manga_id) => { + write!(f, "{}chapter_list/tp/{}-0-0-10", api_path, manga_id) + } + + Self::Manga(manga_id) => write!(f, "{}{}{}", html_path, MANGA_PATH, manga_id), + + Self::Chapter(chapter_id) => write!(f, "{}{}{}", html_path, CHAPTER_PATH, chapter_id), + + Self::SignInPage => write!(f, "{}login/type/login.html", auth_path), + + Self::SignIn => write!(f, "{}login.html", auth_path), + + Self::CheckIn => write!(f, "{}signup.html", api_path), + } + } +}