From edc515fc033d6007a0d8cf6ff333295bf640a169 Mon Sep 17 00:00:00 2001 From: Pablo Date: Fri, 2 Jan 2026 19:55:26 +0100 Subject: [PATCH 1/2] New Source: Es.manhwaweb --- sources/es.manhwaweb/.cargo/config.toml | 5 + sources/es.manhwaweb/Cargo.lock | 376 ++++++++++++++++++++++++ sources/es.manhwaweb/Cargo.toml | 28 ++ sources/es.manhwaweb/res/filters.json | 151 ++++++++++ sources/es.manhwaweb/res/icon.png | Bin 0 -> 16062 bytes sources/es.manhwaweb/res/source.json | 26 ++ sources/es.manhwaweb/src/helper.rs | 15 + sources/es.manhwaweb/src/imp.rs | 294 ++++++++++++++++++ sources/es.manhwaweb/src/lib.rs | 14 + sources/es.manhwaweb/src/models.rs | 175 +++++++++++ 10 files changed, 1084 insertions(+) create mode 100644 sources/es.manhwaweb/.cargo/config.toml create mode 100644 sources/es.manhwaweb/Cargo.lock create mode 100644 sources/es.manhwaweb/Cargo.toml create mode 100644 sources/es.manhwaweb/res/filters.json create mode 100644 sources/es.manhwaweb/res/icon.png create mode 100644 sources/es.manhwaweb/res/source.json create mode 100644 sources/es.manhwaweb/src/helper.rs create mode 100644 sources/es.manhwaweb/src/imp.rs create mode 100644 sources/es.manhwaweb/src/lib.rs create mode 100644 sources/es.manhwaweb/src/models.rs diff --git a/sources/es.manhwaweb/.cargo/config.toml b/sources/es.manhwaweb/.cargo/config.toml new file mode 100644 index 000000000..f137b5a99 --- /dev/null +++ b/sources/es.manhwaweb/.cargo/config.toml @@ -0,0 +1,5 @@ +[build] +target = "wasm32-unknown-unknown" + +[target.wasm32-unknown-unknown] +runner = "aidoku-test-runner" diff --git a/sources/es.manhwaweb/Cargo.lock b/sources/es.manhwaweb/Cargo.lock new file mode 100644 index 000000000..7c5c64e4e --- /dev/null +++ b/sources/es.manhwaweb/Cargo.lock @@ -0,0 +1,376 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aidoku" +version = "0.3.0" +source = "git+https://github.com/Aidoku/aidoku-rs.git#c5a7e53d0c586a06056c76854017ccb220328193" +dependencies = [ + "euclid", + "hashbrown", + "itoa", + "num-traits", + "paste", + "postcard", + "serde", + "serde_json", + "talc", + "thiserror", +] + +[[package]] +name = "aidoku-test" +version = "1.0.0" +source = "git+https://github.com/Aidoku/aidoku-rs.git#c5a7e53d0c586a06056c76854017ccb220328193" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "num-traits", +] + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "euclid" +version = "0.22.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" +dependencies = [ + "num-traits", +] + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", + "serde", + "serde_core", +] + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "iken" +version = "0.1.0" +dependencies = [ + "aidoku", + "chrono", + "serde", + "serde_json", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "manhwaweb" +version = "1.0.0" +dependencies = [ + "aidoku", + "aidoku-test", + "chrono", + "iken", + "serde", + "serde_json", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] + +[[package]] +name = "proc-macro2" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "talc" +version = "4.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3ae828aa394de34c7de08f522d1b86bd1c182c668d27da69caadda00590f26d" +dependencies = [ + "lock_api", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "zmij" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de9211a9f64b825911bdf0240f58b7a8dac217fe260fc61f080a07f61372fbd5" diff --git a/sources/es.manhwaweb/Cargo.toml b/sources/es.manhwaweb/Cargo.toml new file mode 100644 index 000000000..6d9ad7e2d --- /dev/null +++ b/sources/es.manhwaweb/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "manhwaweb" +version = "1.0.0" +edition = "2024" + +[dependencies] +aidoku = { git = "https://github.com/Aidoku/aidoku-rs.git", version = "0.3.0", features = ["json"] } +iken = { path = "../../templates/iken" } +serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } +serde_json = { version = "1.0", default-features = false, features = ["alloc"] } +chrono = { version = "0.4", default-features = false, features = ["alloc"] } + +[dev-dependencies] +aidoku = { git = "https://github.com/Aidoku/aidoku-rs.git", features = ["test"] } +aidoku-test = { git = "https://github.com/Aidoku/aidoku-rs.git" } + + +[lib] +crate-type = ["cdylib"] + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" +opt-level = "s" +strip = true +lto = true diff --git a/sources/es.manhwaweb/res/filters.json b/sources/es.manhwaweb/res/filters.json new file mode 100644 index 000000000..244325628 --- /dev/null +++ b/sources/es.manhwaweb/res/filters.json @@ -0,0 +1,151 @@ +[ + { + "type": "sort", + "title": "Ordenar por", + "id": "sortBy", + "options": [ + "Alfabetico", + "Creacion", + "Num. Capitulos" + ] + }, + { + "type": "select", + "title": "Tipo", + "id": "tipo", + "options": [ + "Ver todo", + "Manhwa", + "Manga", + "Manhua", + "Doujinshi", + "Novela", + "One shot" + ], + "ids": [ + "", + "manhwa", + "manga", + "manhua", + "doujinshi", + "novela", + "one_shot" + ] + }, + { + "type": "select", + "title": "Demografía", + "id": "demografia", + "options": [ + "Ver todo", + "Seinen", + "Shonen", + "Josei", + "Shojo" + ], + "ids": [ + "", + "seinen", + "shonen", + "josei", + "shojo" + ] + }, + { + "type": "select", + "title": "Estado", + "id": "estado", + "options": [ + "Ver todo", + "Publicandose", + "Pausado", + "Finalizado" + ], + "ids": [ + "", + "publicandose", + "pausado", + "finalizado" + ] + }, + { + "type": "select", + "title": "Erótico", + "id": "erotico", + "options": [ + "No", + "Si", + "Ver todo" + ], + "ids": [ + "no", + "si", + "" + ] + }, + { + "type": "multi-select", + "title": "Géneros", + "id": "genres", + "options": [ + "Acción", + "Aventura", + "Comedia", + "Drama", + "Recuentos de la vida", + "Romance", + "Venganza", + "Harem", + "Fantasía", + "Sobrenatural", + "Tragedia", + "Psicológico", + "Horror", + "Thriller", + "Historias cortas", + "Ecchi", + "Gore", + "Girls love", + "Boys love", + "Reencarnación", + "Sistema de niveles", + "Ciencia ficción", + "Apocalíptico", + "Artes marciales", + "Superpoderes", + "Cultivación (cultivo)", + "Milf" + ], + "ids": [ + "3", + "29", + "18", + "1", + "42", + "2", + "5", + "6", + "23", + "31", + "25", + "43", + "32", + "44", + "28", + "30", + "34", + "37", + "27", + "45", + "41", + "37", + "33", + "38", + "39", + "40", + "35", + "8" + ], + "defaults": [] + } +] \ No newline at end of file diff --git a/sources/es.manhwaweb/res/icon.png b/sources/es.manhwaweb/res/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c0c5f1881a2ca61a31053bdf5f75c873f9cf4e93 GIT binary patch literal 16062 zcmZ|$1#lfPvn>kmz0J(b%*@Qp7<0@VGcz+YGuyEpGjq($j+vR+j_sH4ob#W1>#z5w zMx$2u>XurnnW~X$MJg*wA;99o0ssI68EJ9Ve|5lr3N+~7Q(o^W=U)YMR+SP3)K24{ z{%eSvX~~!?C;({x$uzxl1Ke__Y|5^hI!2XZ?AE0y` zxcfiYgq6CMi3pN9}fQk0Qfz4|A{}$T#Sf4e%RVM^Lhx7{uhGx zpZp&+6Dje3L0oJENVOD{iN)-l%!oM|nHiZ$1!0MaiTRyO&3RSDCI83$UrT`0(#6Gr zmx;;U-JQ{$jnUr8f{BHPhlh!om5G&=;U9v*+0)L&$b-Sone4xV{Qu&Jn>m{}Svj~^ z+1nBSC$5pPy{n4=Dd~SY`rqrn$7$wa^?!P@bN-*S{w2ut9}5!;BQw+g#{TEZ{~s-{ zn7ysNlbVB(iJ2e^|9=7hZ`J?l<-gI&Rvu=yTH;ne%

1jYyD%mHU63{{Joiub$ff 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 M2}SW*QNy7B2iihh5C8xG literal 0 HcmV?d00001 diff --git a/sources/es.manhwaweb/res/source.json b/sources/es.manhwaweb/res/source.json new file mode 100644 index 000000000..9b5e79442 --- /dev/null +++ b/sources/es.manhwaweb/res/source.json @@ -0,0 +1,26 @@ +{ + "info": { + "id": "es.manhwaweb", + "name": "ManhwaWeb", + "version": 1, + "url": "https://manhwaweb.com", + "contentRating": 2, + "languages": [ + "es" + ] + }, + "listings": [ + { + "id": "Latest", + "name": "Últimas Actualizaciones" + }, + { + "id": "New", + "name": "Recién Agregados" + }, + { + "id": "Erotic", + "name": "+18 (Erotic)" + } + ] +} \ No newline at end of file diff --git a/sources/es.manhwaweb/src/helper.rs b/sources/es.manhwaweb/src/helper.rs new file mode 100644 index 000000000..39a6bb286 --- /dev/null +++ b/sources/es.manhwaweb/src/helper.rs @@ -0,0 +1,15 @@ +use aidoku::{ + imports::{net::Request, std::sleep}, + Result, +}; + +/// Enforces a rate limit by sleeping before returning the request object. +/// This prevents spamming the server and getting IP banned. +pub fn request_with_limits(url: &str, method: &str) -> Result { + // Sleep for 1 second to respect rate limits. + sleep(1); + match method { + "POST" => Request::post(url).map_err(|e| e.into()), + _ => Request::get(url).map_err(|e| e.into()), + } +} diff --git a/sources/es.manhwaweb/src/imp.rs b/sources/es.manhwaweb/src/imp.rs new file mode 100644 index 000000000..3a73a56e2 --- /dev/null +++ b/sources/es.manhwaweb/src/imp.rs @@ -0,0 +1,294 @@ +use crate::models::*; +use aidoku::{ + alloc::{String, Vec, vec}, + helpers::uri::QueryParameters, + prelude::*, + Chapter, DeepLinkResult, FilterValue, HomeComponent, HomeComponentValue, HomeLayout, Listing, ListingKind, Manga, + MangaPageResult, Page, Result, Source, Home, ListingProvider, DeepLinkHandler, PageContent, +}; + +const PER_PAGE: i32 = 18; +const BASE_URL: &str = "https://manhwaweb.com"; +const BACKEND_URL: &str = "https://manhwawebbackend-production.up.railway.app"; + +pub struct ManhwaWeb; + +impl Source for ManhwaWeb { + fn new() -> Self { + Self + } + + fn get_search_manga_list( + &self, + query: Option, + page: i32, + filters: Vec, + ) -> Result { + // API is 0-based, Aidoku is 1-based. + let api_page = if page > 0 { page - 1 } else { 0 }; + let mut url = format!("{BACKEND_URL}/manhwa/library?page={}&perPage={}", api_page, PER_PAGE); + let mut qs = QueryParameters::new(); + + // Handle search query + if let Some(q) = query { + let trimmed = q.trim(); + if !trimmed.is_empty() { + qs.push("buscar", Some(trimmed)); + } + } + + let mut erotic_filter_set = false; + let mut genre_values: Vec = Vec::new(); + + for filter in filters { + match filter { + FilterValue::Select { id, value } => { + if !value.is_empty() { + // Pass ID directly as API expects (e.g., 'tipo', 'demografia') + qs.push(&id, Some(&value)); + + if id == "erotico" { + erotic_filter_set = true; + } + } + } + FilterValue::MultiSelect { id, included, .. } => { + // For genres + if id == "genres" || id == "genreIds" || id == "generes" { + for val in included { + genre_values.push(val); + } + } + } + FilterValue::Sort { index, .. } => { + let sort_val = match index { + 0 => "alfabetico", + 2 => "num_chapter", + _ => "creacion", // Default to creation date + }; + qs.push("order_item", Some(sort_val)); + } + _ => {} + } + } + + // Handle Genres: joined by 'a' (e.g., "1a2a3") + if !genre_values.is_empty() { + let joined_genres = genre_values.join("a"); + qs.push("generes", Some(&joined_genres)); + } + + // Default to "no" erotic content if the filter wasn't explicitly set + if !erotic_filter_set { + qs.push("erotico", Some("no")); + } + + // Ensure qs is appended with '&' prefix because base URL might have query params already. + let qs_str = format!("{}", qs); + if !qs_str.is_empty() { + url.push_str(&format!("&{}", qs_str)); + } + + let mut response = crate::helper::request_with_limits(&url, "GET")? + .header("Referer", &format!("{}/", BASE_URL)) + .send()?; + + let data = response.get_json::()?; + let entries = data.data.iter().map(|m| m.to_manga(BASE_URL)).collect(); + // Pagination check: API returns 'next': boolean. + let has_next_page = data.next; + + Ok(MangaPageResult { + entries, + has_next_page, + }) + } + + fn get_manga_update( + &self, + mut manga: Manga, + needs_details: bool, + needs_chapters: bool, + ) -> Result { + let url = format!("{BACKEND_URL}/manhwa/see/{}", manga.key); + let mut response = crate::helper::request_with_limits(&url, "GET")? + .header("Referer", &format!("{}/", BASE_URL)) + .send()?; + let data = response.get_json::()?; + + if needs_details { + manga.copy_from(data.parse_manga(BASE_URL)); + } + + if needs_chapters { + manga.chapters = Some(data.parse_chapters(BASE_URL)); + } + + Ok(manga) + } + + fn get_page_list(&self, _manga: Manga, chapter: Chapter) -> Result> { + let url = format!("{BACKEND_URL}/chapters/see/{}", chapter.key); + let mut response = crate::helper::request_with_limits(&url, "GET")? + .send()?; + let data = response.get_json::()?; + + Ok(data.chapter.img.into_iter().enumerate().map(|(_i, url)| Page { + content: PageContent::Url(url, None), + ..Default::default() + }).collect()) + } +} + +impl DeepLinkHandler for ManhwaWeb { + fn handle_deep_link(&self, url: String) -> Result> { + if let Some(path) = url.strip_prefix(BASE_URL) { + if path.starts_with("/manhwa/") { + let id = path.trim_start_matches("/manhwa/"); + return Ok(Some(DeepLinkResult::Manga { + key: id.into(), + })); + } + } + Ok(None) + } +} + +use aidoku::alloc::string::ToString; + +/// Helper function to map genre IDs to Spanish names. +fn get_genre_name(id: &str) -> String { + match id { + "3" => "Acción", "29" => "Aventura", "18" => "Comedia", "1" => "Drama", + "42" => "Recuentos de la vida", "2" => "Romance", "5" => "Venganza", "6" => "Harem", + "23" => "Fantasía", "31" => "Sobrenatural", "25" => "Tragedia", "43" => "Psicológico", + "32" => "Horror", "44" => "Thriller", "28" => "Historias cortas", "30" => "Ecchi", + "34" => "Gore", "37" => "Girls love", "27" => "Boys love", "45" => "Reencarnación", + "41" => "Sistema de niveles", "33" => "Ciencia ficción", "38" => "Apocalíptico", + "39" => "Artes marciales", "40" => "Superpoderes", "35" => "Cultivación (cultivo)", + "8" => "Milf", _ => "Desconocido", + }.to_string() +} + +impl Home for ManhwaWeb { + fn get_home(&self) -> Result { + let mut components = Vec::new(); + + // 1. Hero: Latest Chapters ("Nuevos Capitulos") - BigScroller + let url_latest = format!("{BACKEND_URL}/manhwa/nuevos"); + if let Ok(response) = crate::helper::request_with_limits(&url_latest, "GET") { + if let Ok(mut resp) = response.send() { + if let Ok(data) = resp.get_json::() { + let latest_entries: Vec = data.manhwas.spanish_manhwas.iter().map(|m| { + let group = m.gru_name.clone().unwrap_or_default(); + let subtitle = format!("Cap. {} • {}", m.chapter, group); + + Manga { + key: m.id_manhwa.clone().into(), + title: m.name_manhwa.clone(), + authors: Some(vec![subtitle]), + cover: m.img.clone().map(|s| s.into()), + url: Some(format!("{}/manhwa/{}", BASE_URL, m.id_manhwa)), + ..Default::default() + } + }).collect(); + + components.push(HomeComponent { + title: Some("Nuevos Capítulos".into()), + subtitle: None, + value: HomeComponentValue::BigScroller { + entries: latest_entries, + auto_scroll_interval: Some(5.0), + }, + }); + } + } + } + + // 2. New Works ("Nuevas Obras") - Scroller + let url_new = format!("{BACKEND_URL}/manhwa/library?page=0&perPage=12&order_item=creacion&order_dir=desc"); + if let Ok(resp) = crate::helper::request_with_limits(&url_new, "GET") { + if let Ok(mut resp) = resp.send() { + if let Ok(data) = resp.get_json::() { + let entries: Vec = data.data + .iter() + .filter(|m| m.erotic.as_deref() != Some("si")) + .map(|m| { + let mut manga = m.to_manga(BASE_URL); + if let Some(cats) = &m.categories { + let tags: Vec = cats.iter().map(|id| get_genre_name(&id.to_string())).collect(); + manga.tags = Some(tags); + } + manga.into() + }) + .collect(); + + components.push(HomeComponent { + title: Some("Nuevas Obras".into()), + subtitle: None, + value: HomeComponentValue::Scroller { + entries, + listing: Some(Listing { + id: "New".into(), + name: "Nuevas Obras".into(), + kind: ListingKind::Default, + }), + }, + }); + } + } + } + + Ok(HomeLayout { components }) + } +} + +impl ListingProvider for ManhwaWeb { + fn get_manga_list(&self, listing: Listing, page: i32) -> Result { + let name = listing.id.as_str(); + let api_page = if page > 0 { page - 1 } else { 0 }; + + if name.starts_with("Genre:") { + let id = name.split(':').nth(1).unwrap_or(""); + // Ensure 'creacion' (creation date) sort is properly applied for these listings too + let url = format!("{BACKEND_URL}/manhwa/library?page={}&perPage={}&erotico=no&generes={}&order_item=creacion&order_dir=desc", api_page, PER_PAGE, id); + let resp = crate::helper::request_with_limits(&url, "GET")?.send()?.get_json::()?; + let entries: Vec = resp.data.iter().map(|m| m.to_manga(BASE_URL)).collect(); + return Ok(MangaPageResult { entries, has_next_page: resp.next }); + } + + match name { + // "Latest" and "Popular" standard tabs fallback + // Note: The API does not strictly support a "Popular" endpoint that differs significantly from "Nuevos" in this context without specific implementation. + "Latest" | "Popular" => { + if page > 1 { + return Ok(MangaPageResult { entries: vec![], has_next_page: false }); + } + let url = format!("{BACKEND_URL}/manhwa/nuevos"); + let resp = crate::helper::request_with_limits(&url, "GET")?.send()?.get_json::()?; + let entries: Vec = resp.manhwas.spanish_manhwas.iter().map(|m| Manga { + key: m.id_manhwa.clone().into(), + title: m.name_manhwa.clone(), + cover: m.img.clone().map(|s| s.into()), + url: Some(format!("{}/manhwa/{}", BASE_URL, m.id_manhwa)), + ..Default::default() + }).collect(); + Ok(MangaPageResult { entries, has_next_page: false }) + } + "New" => { + self.get_search_manga_list(None, page, vec![FilterValue::Sort { index: 3, ascending: false, id: "sortBy".into() }]) + } + "Erotic" | "+18 (Erotic)" => { + let url = format!("{BACKEND_URL}/manhwa/library?page={}&perPage={}&erotico=si", api_page, PER_PAGE); + let data = crate::helper::request_with_limits(&url, "GET")?.header("Referer", &format!("{}/", BASE_URL)).send()?.get_json::()?; + let entries = data.data.iter().map(|m| m.to_manga(BASE_URL)).collect(); + Ok(MangaPageResult { entries, has_next_page: data.next }) + } + // Search fallback + _ => { + let filters = vec![]; + self.get_search_manga_list(None, page, filters) + } + } + } +} diff --git a/sources/es.manhwaweb/src/lib.rs b/sources/es.manhwaweb/src/lib.rs new file mode 100644 index 000000000..f17638dde --- /dev/null +++ b/sources/es.manhwaweb/src/lib.rs @@ -0,0 +1,14 @@ +#![no_std] + +use aidoku::{ + prelude::*, + Source, +}; + +mod helper; +mod imp; +mod models; + +use imp::ManhwaWeb; + +register_source!(ManhwaWeb, Home, DeepLinkHandler, ListingProvider); diff --git a/sources/es.manhwaweb/src/models.rs b/sources/es.manhwaweb/src/models.rs new file mode 100644 index 000000000..8b94c94c2 --- /dev/null +++ b/sources/es.manhwaweb/src/models.rs @@ -0,0 +1,175 @@ +use aidoku::{ + alloc::{string::ToString, String, Vec}, + prelude::*, + Chapter, Manga, MangaStatus, Viewer, +}; +use serde::Deserialize; + +#[derive(Debug, Clone, Deserialize)] +pub struct NuevosResponse { + // 'utimos_mangas_creados' (latest_mangas) and 'top' are never used by logic, + // so we can omit them. If the JSON structure requires them to be present but ignored, + // we can keep them or use `serde::IgnoredAny` if we want to be strict, + // but typically just omitting them from the struct works if `deny_unknown_fields` isn't on. + + // However, the `ManhwaWeb` logic accesses `data.manhwas.spanish_manhwas`. + pub manhwas: ManhwasCollection, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct ManhwasCollection { + #[serde(rename = "manhwas_esp")] + pub spanish_manhwas: Vec, +} + +// TopCollection and TopManga were seemingly unused. Removed. + +#[derive(Debug, Clone, Deserialize)] +pub struct UpdateManga { + pub name_manhwa: String, + pub img: Option, + pub id_manhwa: String, + pub chapter: f32, + // 'create' was unused. + pub gru_name: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct LibraryResponse { + pub data: Vec, + pub next: bool, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct LibraryManga { + #[serde(rename = "_id")] + pub id: String, + pub the_real_name: String, + #[serde(rename = "_imagen")] + pub image: Option, + #[serde(rename = "_status")] + pub status: Option, + #[serde(rename = "_erotico")] + pub erotic: Option, + #[serde(rename = "_categoris")] + pub categories: Option>, +} + +impl LibraryManga { + pub fn to_manga(&self, base_url: &str) -> Manga { + Manga { + key: self.id.clone().into(), + title: self.the_real_name.clone(), + cover: self.image.clone().map(|s| s.into()), + url: Some(format!("{}/manhwa/{}", base_url, self.id)), + status: self.status.as_ref().map(|s| match s.as_str() { + "publicandose" => MangaStatus::Ongoing, + "finalizado" => MangaStatus::Completed, + "pausado" => MangaStatus::Hiatus, + _ => MangaStatus::Unknown, + }).unwrap_or(MangaStatus::Unknown), + ..Default::default() + } + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct SeeResponse { + #[serde(rename = "_id")] + pub id: String, + pub the_real_name: String, + #[serde(rename = "_sinopsis")] + pub synopsis: Option, + #[serde(rename = "_imagen")] + pub image: Option, + #[serde(rename = "_status")] + pub status: Option, + #[serde(rename = "_tipo")] + pub serie_type: Option, + // Removed unused 'demography' and 'erotic' + #[serde(rename = "_categoris")] + pub categories: Option>, + pub chapters: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct RawSeeChapter { + pub chapter: f32, + pub create: i64, + pub link: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct ChapterSeeResponse { + pub chapter: ChapterImgData, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct ChapterImgData { + pub img: Vec, +} + +impl SeeResponse { + pub fn parse_manga(&self, base_url: &str) -> Manga { + Manga { + key: self.id.clone().into(), + title: self.the_real_name.clone(), + description: self.synopsis.as_ref().map(|s| s.trim().into()), + cover: self.image.as_ref().map(|s| s.into()), + url: Some(format!("{base_url}/manhwa/{}", self.id)), + status: self + .status + .as_ref() + .map(|s| match s.as_str() { + "publicandose" => MangaStatus::Ongoing, + "finalizado" => MangaStatus::Completed, + "pausado" => MangaStatus::Hiatus, + _ => MangaStatus::Unknown, + }) + .unwrap_or(MangaStatus::Unknown), + viewer: self + .serie_type + .as_ref() + .map(|s| match s.as_str() { + "manhwa" | "manhua" => Viewer::Webtoon, + _ => Viewer::RightToLeft, + }) + .unwrap_or(Viewer::Unknown), + tags: self.categories.as_ref().map(|cats: &Vec| { + cats.iter() + .filter_map(|cat: &serde_json::Value| { + cat.as_object() + .and_then(|obj: &serde_json::Map| obj.values().next()) + .and_then(|v: &serde_json::Value| v.as_str()) + .map(|s: &str| s.into()) + }) + .collect() + }), + ..Default::default() + } + } + + pub fn parse_chapters(&self, _base_url: &str) -> Vec { + let mut chapters: Vec = self.chapters + .iter() + .map(|c| Chapter { + // The URL is like /leer/slug-number + // But the API for images uses the whole slug-number as ID (KEY) + key: c.link.rsplit('/').next().unwrap_or(&self.id).to_string(), + chapter_number: Some(c.chapter), + date_uploaded: Some(c.create / 1000), // convert ms to s + url: Some(c.link.clone()), + ..Default::default() + }) + .collect(); + + // Sort by chapter number descending + chapters.sort_by(|a, b| { + b.chapter_number + .partial_cmp(&a.chapter_number) + .unwrap_or(core::cmp::Ordering::Equal) + }); + + chapters + } +} From dd0d2f3d8926eaeb9f24464757952527a47978d0 Mon Sep 17 00:00:00 2001 From: Llavesuke Date: Sat, 3 Jan 2026 12:52:57 +0100 Subject: [PATCH 2/2] fix(es.manhwaweb): address PR review feedback --- sources/es.manhwaweb/Cargo.lock | 21 - sources/es.manhwaweb/Cargo.toml | 2 - sources/es.manhwaweb/res/filters.json | 20 +- sources/es.manhwaweb/res/icon.png | Bin 16062 -> 4951 bytes sources/es.manhwaweb/res/source.json | 4 +- sources/es.manhwaweb/src/helper.rs | 15 - sources/es.manhwaweb/src/helpers.rs | 55 +++ sources/es.manhwaweb/src/imp.rs | 551 ++++++++++++++------------ sources/es.manhwaweb/src/lib.rs | 7 +- sources/es.manhwaweb/src/models.rs | 270 +++++++------ 10 files changed, 509 insertions(+), 436 deletions(-) delete mode 100644 sources/es.manhwaweb/src/helper.rs create mode 100644 sources/es.manhwaweb/src/helpers.rs diff --git a/sources/es.manhwaweb/Cargo.lock b/sources/es.manhwaweb/Cargo.lock index 7c5c64e4e..276d462eb 100644 --- a/sources/es.manhwaweb/Cargo.lock +++ b/sources/es.manhwaweb/Cargo.lock @@ -55,15 +55,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "num-traits", -] - [[package]] name = "cobs" version = "0.3.0" @@ -148,16 +139,6 @@ dependencies = [ "stable_deref_trait", ] -[[package]] -name = "iken" -version = "0.1.0" -dependencies = [ - "aidoku", - "chrono", - "serde", - "serde_json", -] - [[package]] name = "itoa" version = "1.0.17" @@ -185,8 +166,6 @@ version = "1.0.0" dependencies = [ "aidoku", "aidoku-test", - "chrono", - "iken", "serde", "serde_json", ] diff --git a/sources/es.manhwaweb/Cargo.toml b/sources/es.manhwaweb/Cargo.toml index 6d9ad7e2d..c59e04516 100644 --- a/sources/es.manhwaweb/Cargo.toml +++ b/sources/es.manhwaweb/Cargo.toml @@ -5,10 +5,8 @@ edition = "2024" [dependencies] aidoku = { git = "https://github.com/Aidoku/aidoku-rs.git", version = "0.3.0", features = ["json"] } -iken = { path = "../../templates/iken" } serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } serde_json = { version = "1.0", default-features = false, features = ["alloc"] } -chrono = { version = "0.4", default-features = false, features = ["alloc"] } [dev-dependencies] aidoku = { git = "https://github.com/Aidoku/aidoku-rs.git", features = ["test"] } diff --git a/sources/es.manhwaweb/res/filters.json b/sources/es.manhwaweb/res/filters.json index 244325628..65308339c 100644 --- a/sources/es.manhwaweb/res/filters.json +++ b/sources/es.manhwaweb/res/filters.json @@ -34,7 +34,7 @@ }, { "type": "select", - "title": "Demografía", + "title": "Demografia", "id": "demografia", "options": [ "Ver todo", @@ -70,7 +70,7 @@ }, { "type": "select", - "title": "Erótico", + "title": "Erotico", "id": "erotico", "options": [ "No", @@ -85,10 +85,10 @@ }, { "type": "multi-select", - "title": "Géneros", + "title": "Generos", "id": "genres", "options": [ - "Acción", + "Accion", "Aventura", "Comedia", "Drama", @@ -96,10 +96,10 @@ "Romance", "Venganza", "Harem", - "Fantasía", + "Fantasia", "Sobrenatural", "Tragedia", - "Psicológico", + "Psicologico", "Horror", "Thriller", "Historias cortas", @@ -107,13 +107,13 @@ "Gore", "Girls love", "Boys love", - "Reencarnación", + "Reencarnacion", "Sistema de niveles", - "Ciencia ficción", - "Apocalíptico", + "Ciencia ficcion", + "Apocaliptico", "Artes marciales", "Superpoderes", - "Cultivación (cultivo)", + "Cultivacion (cultivo)", "Milf" ], "ids": [ diff --git a/sources/es.manhwaweb/res/icon.png b/sources/es.manhwaweb/res/icon.png index c0c5f1881a2ca61a31053bdf5f75c873f9cf4e93..f528898e91a11ba5d7504d06538fb2360ef1ffb1 100644 GIT binary patch literal 4951 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

1jYyD%mHU63{{Joiub$ff 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 M2}SW*QNy7B2iihh5C8xG diff --git a/sources/es.manhwaweb/res/source.json b/sources/es.manhwaweb/res/source.json index 9b5e79442..3ab6e49b6 100644 --- a/sources/es.manhwaweb/res/source.json +++ b/sources/es.manhwaweb/res/source.json @@ -12,11 +12,11 @@ "listings": [ { "id": "Latest", - "name": "Últimas Actualizaciones" + "name": "Ultimas Actualizaciones" }, { "id": "New", - "name": "Recién Agregados" + "name": "Recien Agregados" }, { "id": "Erotic", diff --git a/sources/es.manhwaweb/src/helper.rs b/sources/es.manhwaweb/src/helper.rs deleted file mode 100644 index 39a6bb286..000000000 --- a/sources/es.manhwaweb/src/helper.rs +++ /dev/null @@ -1,15 +0,0 @@ -use aidoku::{ - imports::{net::Request, std::sleep}, - Result, -}; - -/// Enforces a rate limit by sleeping before returning the request object. -/// This prevents spamming the server and getting IP banned. -pub fn request_with_limits(url: &str, method: &str) -> Result { - // Sleep for 1 second to respect rate limits. - sleep(1); - match method { - "POST" => Request::post(url).map_err(|e| e.into()), - _ => Request::get(url).map_err(|e| e.into()), - } -} diff --git a/sources/es.manhwaweb/src/helpers.rs b/sources/es.manhwaweb/src/helpers.rs new file mode 100644 index 000000000..684278301 --- /dev/null +++ b/sources/es.manhwaweb/src/helpers.rs @@ -0,0 +1,55 @@ +use aidoku::{ + Result, + alloc::String, + imports::net::{Request, TimeUnit, set_rate_limit}, +}; + +/// Initializes the rate limiter for this source. +/// Call this once during source initialization to enforce rate limits globally. +pub fn setup_rate_limit() { + // Allow 1 request every 2 seconds to avoid IP bans. + set_rate_limit(1, 2, TimeUnit::Seconds); +} + +/// Creates a request object for the given URL and method. +pub fn create_request(url: &str, method: &str) -> Result { + match method { + "POST" => Request::post(url).map_err(Into::into), + _ => Request::get(url).map_err(Into::into), + } +} + +/// Helper function to map genre IDs to Spanish names. +pub fn get_genre_name(id: &str) -> String { + match id { + "3" => "Accion", + "29" => "Aventura", + "18" => "Comedia", + "1" => "Drama", + "42" => "Recuentos de la vida", + "2" => "Romance", + "5" => "Venganza", + "6" => "Harem", + "23" => "Fantasia", + "31" => "Sobrenatural", + "25" => "Tragedia", + "43" => "Psicologico", + "32" => "Horror", + "44" => "Thriller", + "28" => "Historias cortas", + "30" => "Ecchi", + "34" => "Gore", + "37" => "Girls love", + "27" => "Boys love", + "45" => "Reencarnacion", + "41" => "Sistema de niveles", + "33" => "Ciencia ficcion", + "38" => "Apocaliptico", + "39" => "Artes marciales", + "40" => "Superpoderes", + "35" => "Cultivacion (cultivo)", + "8" => "Milf", + _ => "Desconocido", + } + .into() +} diff --git a/sources/es.manhwaweb/src/imp.rs b/sources/es.manhwaweb/src/imp.rs index 3a73a56e2..353f05409 100644 --- a/sources/es.manhwaweb/src/imp.rs +++ b/sources/es.manhwaweb/src/imp.rs @@ -1,10 +1,11 @@ use crate::models::*; use aidoku::{ - alloc::{String, Vec, vec}, - helpers::uri::QueryParameters, - prelude::*, - Chapter, DeepLinkResult, FilterValue, HomeComponent, HomeComponentValue, HomeLayout, Listing, ListingKind, Manga, - MangaPageResult, Page, Result, Source, Home, ListingProvider, DeepLinkHandler, PageContent, + Chapter, DeepLinkHandler, DeepLinkResult, FilterValue, Home, HomeComponent, HomeComponentValue, + HomeLayout, Listing, ListingKind, ListingProvider, Manga, MangaPageResult, Page, PageContent, + Result, Source, + alloc::{String, Vec, string::ToString, vec}, + helpers::uri::QueryParameters, + prelude::*, }; const PER_PAGE: i32 = 18; @@ -14,281 +15,325 @@ const BACKEND_URL: &str = "https://manhwawebbackend-production.up.railway.app"; pub struct ManhwaWeb; impl Source for ManhwaWeb { - fn new() -> Self { - Self - } + fn new() -> Self { + crate::helpers::setup_rate_limit(); + Self + } - fn get_search_manga_list( - &self, - query: Option, - page: i32, - filters: Vec, - ) -> Result { - // API is 0-based, Aidoku is 1-based. - let api_page = if page > 0 { page - 1 } else { 0 }; - let mut url = format!("{BACKEND_URL}/manhwa/library?page={}&perPage={}", api_page, PER_PAGE); - let mut qs = QueryParameters::new(); - - // Handle search query - if let Some(q) = query { - let trimmed = q.trim(); - if !trimmed.is_empty() { - qs.push("buscar", Some(trimmed)); - } - } + fn get_search_manga_list( + &self, + query: Option, + page: i32, + filters: Vec, + ) -> Result { + // API is 0-based, Aidoku is 1-based. + let api_page = page - 1; + let mut url = format!( + "{BACKEND_URL}/manhwa/library?page={}&perPage={}", + api_page, PER_PAGE + ); + let mut qs = QueryParameters::new(); - let mut erotic_filter_set = false; - let mut genre_values: Vec = Vec::new(); + // Handle search query + if let Some(q) = query { + let trimmed = q.trim(); + if !trimmed.is_empty() { + qs.push("buscar", Some(trimmed)); + } + } - for filter in filters { - match filter { - FilterValue::Select { id, value } => { - if !value.is_empty() { - // Pass ID directly as API expects (e.g., 'tipo', 'demografia') - qs.push(&id, Some(&value)); - - if id == "erotico" { - erotic_filter_set = true; - } - } - } - FilterValue::MultiSelect { id, included, .. } => { - // For genres - if id == "genres" || id == "genreIds" || id == "generes" { - for val in included { - genre_values.push(val); - } - } - } - FilterValue::Sort { index, .. } => { - let sort_val = match index { - 0 => "alfabetico", - 2 => "num_chapter", - _ => "creacion", // Default to creation date - }; - qs.push("order_item", Some(sort_val)); - } - _ => {} - } - } - - // Handle Genres: joined by 'a' (e.g., "1a2a3") - if !genre_values.is_empty() { - let joined_genres = genre_values.join("a"); - qs.push("generes", Some(&joined_genres)); - } + let mut erotic_filter_set = false; + let mut genre_values: Vec = Vec::new(); - // Default to "no" erotic content if the filter wasn't explicitly set - if !erotic_filter_set { - qs.push("erotico", Some("no")); - } - - // Ensure qs is appended with '&' prefix because base URL might have query params already. - let qs_str = format!("{}", qs); - if !qs_str.is_empty() { - url.push_str(&format!("&{}", qs_str)); - } + for filter in filters { + match filter { + FilterValue::Select { id, value } => { + if !value.is_empty() { + // Pass ID directly as API expects (e.g., 'tipo', 'demografia') + qs.push(&id, Some(&value)); - let mut response = crate::helper::request_with_limits(&url, "GET")? - .header("Referer", &format!("{}/", BASE_URL)) - .send()?; + if id == "erotico" { + erotic_filter_set = true; + } + } + } + FilterValue::MultiSelect { id, included, .. } => { + // For genres + if id == "genres" || id == "genreIds" || id == "generes" { + for val in included { + genre_values.push(val); + } + } + } + FilterValue::Sort { index, .. } => { + let sort_val = match index { + 0 => "alfabetico", + 2 => "num_chapter", + _ => "creacion", // Default to creation date + }; + qs.push("order_item", Some(sort_val)); + } + _ => {} + } + } - let data = response.get_json::()?; - let entries = data.data.iter().map(|m| m.to_manga(BASE_URL)).collect(); - // Pagination check: API returns 'next': boolean. - let has_next_page = data.next; + // Handle Genres: joined by 'a' (e.g., "1a2a3") + if !genre_values.is_empty() { + let joined_genres = genre_values.join("a"); + qs.push("generes", Some(&joined_genres)); + } - Ok(MangaPageResult { - entries, - has_next_page, - }) - } + // Default to "no" erotic content if the filter wasn't explicitly set + if !erotic_filter_set { + qs.push("erotico", Some("no")); + } - fn get_manga_update( - &self, - mut manga: Manga, - needs_details: bool, - needs_chapters: bool, - ) -> Result { - let url = format!("{BACKEND_URL}/manhwa/see/{}", manga.key); - let mut response = crate::helper::request_with_limits(&url, "GET")? - .header("Referer", &format!("{}/", BASE_URL)) - .send()?; - let data = response.get_json::()?; + // Ensure qs is appended with '&' prefix because base URL might have query params already. + if !qs.is_empty() { + url.push_str(&format!("&{}", qs)); + } - if needs_details { - manga.copy_from(data.parse_manga(BASE_URL)); - } + let data = crate::helpers::create_request(&url, "GET")? + .header("Referer", &format!("{}/", BASE_URL)) + .json_owned::()?; + let entries = data + .data + .into_iter() + .map(|m| m.to_manga(BASE_URL)) + .collect(); + // Pagination check: API returns 'next': boolean. + let has_next_page = data.next; - if needs_chapters { - manga.chapters = Some(data.parse_chapters(BASE_URL)); - } + Ok(MangaPageResult { + entries, + has_next_page, + }) + } - Ok(manga) - } + fn get_manga_update( + &self, + mut manga: Manga, + needs_details: bool, + needs_chapters: bool, + ) -> Result { + let url = format!("{BACKEND_URL}/manhwa/see/{}", manga.key); + let data = crate::helpers::create_request(&url, "GET")? + .header("Referer", &format!("{}/", BASE_URL)) + .json_owned::()?; - fn get_page_list(&self, _manga: Manga, chapter: Chapter) -> Result> { - let url = format!("{BACKEND_URL}/chapters/see/{}", chapter.key); - let mut response = crate::helper::request_with_limits(&url, "GET")? - .send()?; - let data = response.get_json::()?; - - Ok(data.chapter.img.into_iter().enumerate().map(|(_i, url)| Page { - content: PageContent::Url(url, None), - ..Default::default() - }).collect()) - } -} + if needs_details { + manga.copy_from(data.parse_manga(BASE_URL)); + } -impl DeepLinkHandler for ManhwaWeb { - fn handle_deep_link(&self, url: String) -> Result> { - if let Some(path) = url.strip_prefix(BASE_URL) { - if path.starts_with("/manhwa/") { - let id = path.trim_start_matches("/manhwa/"); - return Ok(Some(DeepLinkResult::Manga { - key: id.into(), - })); - } - } - Ok(None) - } -} + if needs_chapters { + manga.chapters = Some(data.parse_chapters(BASE_URL)); + } + + Ok(manga) + } -use aidoku::alloc::string::ToString; + fn get_page_list(&self, _manga: Manga, chapter: Chapter) -> Result> { + let url = format!("{BACKEND_URL}/chapters/see/{}", chapter.key); + let data = + crate::helpers::create_request(&url, "GET")?.json_owned::()?; -/// Helper function to map genre IDs to Spanish names. -fn get_genre_name(id: &str) -> String { - match id { - "3" => "Acción", "29" => "Aventura", "18" => "Comedia", "1" => "Drama", - "42" => "Recuentos de la vida", "2" => "Romance", "5" => "Venganza", "6" => "Harem", - "23" => "Fantasía", "31" => "Sobrenatural", "25" => "Tragedia", "43" => "Psicológico", - "32" => "Horror", "44" => "Thriller", "28" => "Historias cortas", "30" => "Ecchi", - "34" => "Gore", "37" => "Girls love", "27" => "Boys love", "45" => "Reencarnación", - "41" => "Sistema de niveles", "33" => "Ciencia ficción", "38" => "Apocalíptico", - "39" => "Artes marciales", "40" => "Superpoderes", "35" => "Cultivación (cultivo)", - "8" => "Milf", _ => "Desconocido", - }.to_string() + Ok(data + .chapter + .img + .into_iter() + .enumerate() + .map(|(_i, url)| Page { + content: PageContent::Url(url, None), + ..Default::default() + }) + .collect()) + } +} + +impl DeepLinkHandler for ManhwaWeb { + fn handle_deep_link(&self, url: String) -> Result> { + if let Some(path) = url.strip_prefix(BASE_URL) { + if path.starts_with("/manhwa/") { + let id = path.trim_start_matches("/manhwa/"); + return Ok(Some(DeepLinkResult::Manga { key: id.into() })); + } + } + Ok(None) + } } impl Home for ManhwaWeb { - fn get_home(&self) -> Result { - let mut components = Vec::new(); + fn get_home(&self) -> Result { + let mut components = Vec::new(); - // 1. Hero: Latest Chapters ("Nuevos Capitulos") - BigScroller - let url_latest = format!("{BACKEND_URL}/manhwa/nuevos"); - if let Ok(response) = crate::helper::request_with_limits(&url_latest, "GET") { - if let Ok(mut resp) = response.send() { - if let Ok(data) = resp.get_json::() { - let latest_entries: Vec = data.manhwas.spanish_manhwas.iter().map(|m| { - let group = m.gru_name.clone().unwrap_or_default(); - let subtitle = format!("Cap. {} • {}", m.chapter, group); + // 1. Hero: Latest Chapters ("Nuevos Capitulos") - BigScroller + let url_latest = format!("{BACKEND_URL}/manhwa/nuevos"); + if let Ok(data) = crate::helpers::create_request(&url_latest, "GET") + .and_then(|r| r.json_owned::()) + { + let latest_entries: Vec = data + .manhwas + .spanish_manhwas + .into_iter() + .map(|m| { + let group = m.gru_name.unwrap_or_default(); + let subtitle = format!("Cap. {} - {}", m.chapter, group); + let url = Some(format!("{}/manhwa/{}", BASE_URL, m.id_manhwa)); - Manga { - key: m.id_manhwa.clone().into(), - title: m.name_manhwa.clone(), - authors: Some(vec![subtitle]), - cover: m.img.clone().map(|s| s.into()), - url: Some(format!("{}/manhwa/{}", BASE_URL, m.id_manhwa)), - ..Default::default() - } - }).collect(); + Manga { + key: m.id_manhwa.into(), + title: m.name_manhwa, + authors: Some(vec![subtitle]), + cover: m.img.map(|s| s.into()), + url, + ..Default::default() + } + }) + .collect(); - components.push(HomeComponent { - title: Some("Nuevos Capítulos".into()), - subtitle: None, - value: HomeComponentValue::BigScroller { - entries: latest_entries, - auto_scroll_interval: Some(5.0), - }, - }); - } - } - } + components.push(HomeComponent { + title: Some("Nuevos Capitulos".into()), + subtitle: None, + value: HomeComponentValue::BigScroller { + entries: latest_entries, + auto_scroll_interval: Some(5.0), + }, + }); + } - // 2. New Works ("Nuevas Obras") - Scroller - let url_new = format!("{BACKEND_URL}/manhwa/library?page=0&perPage=12&order_item=creacion&order_dir=desc"); - if let Ok(resp) = crate::helper::request_with_limits(&url_new, "GET") { - if let Ok(mut resp) = resp.send() { - if let Ok(data) = resp.get_json::() { - let entries: Vec = data.data - .iter() - .filter(|m| m.erotic.as_deref() != Some("si")) - .map(|m| { - let mut manga = m.to_manga(BASE_URL); - if let Some(cats) = &m.categories { - let tags: Vec = cats.iter().map(|id| get_genre_name(&id.to_string())).collect(); - manga.tags = Some(tags); - } - manga.into() - }) - .collect(); + // 2. New Works ("Nuevas Obras") - Scroller + let url_new = format!( + "{BACKEND_URL}/manhwa/library?page=0&perPage=12&order_item=creacion&order_dir=desc" + ); + if let Ok(data) = crate::helpers::create_request(&url_new, "GET") + .and_then(|r| r.json_owned::()) + { + let entries: Vec = data + .data + .into_iter() + .filter(|m| m.erotic.as_deref() != Some("si")) + .map(|m| { + let categories = m.categories.clone(); + let mut manga = m.to_manga(BASE_URL); + if let Some(cats) = categories { + let tags: Vec = cats + .iter() + .map(|id| crate::helpers::get_genre_name(&id.to_string())) + .collect(); + manga.tags = Some(tags); + } + manga.into() + }) + .collect(); - components.push(HomeComponent { - title: Some("Nuevas Obras".into()), - subtitle: None, - value: HomeComponentValue::Scroller { - entries, - listing: Some(Listing { - id: "New".into(), - name: "Nuevas Obras".into(), - kind: ListingKind::Default, - }), - }, - }); - } - } - } + components.push(HomeComponent { + title: Some("Nuevas Obras".into()), + subtitle: None, + value: HomeComponentValue::Scroller { + entries, + listing: Some(Listing { + id: "New".into(), + name: "Nuevas Obras".into(), + kind: ListingKind::Default, + }), + }, + }); + } - Ok(HomeLayout { components }) - } + Ok(HomeLayout { components }) + } } impl ListingProvider for ManhwaWeb { - fn get_manga_list(&self, listing: Listing, page: i32) -> Result { - let name = listing.id.as_str(); - let api_page = if page > 0 { page - 1 } else { 0 }; + fn get_manga_list(&self, listing: Listing, page: i32) -> Result { + let name = listing.id.as_str(); + let api_page = page - 1; - if name.starts_with("Genre:") { - let id = name.split(':').nth(1).unwrap_or(""); - // Ensure 'creacion' (creation date) sort is properly applied for these listings too - let url = format!("{BACKEND_URL}/manhwa/library?page={}&perPage={}&erotico=no&generes={}&order_item=creacion&order_dir=desc", api_page, PER_PAGE, id); - let resp = crate::helper::request_with_limits(&url, "GET")?.send()?.get_json::()?; - let entries: Vec = resp.data.iter().map(|m| m.to_manga(BASE_URL)).collect(); - return Ok(MangaPageResult { entries, has_next_page: resp.next }); - } + if name.starts_with("Genre:") { + let id = name.split(':').nth(1).unwrap_or(""); + // Ensure 'creacion' (creation date) sort is properly applied for these listings too + let url = format!( + "{BACKEND_URL}/manhwa/library?page={}&perPage={}&erotico=no&generes={}&order_item=creacion&order_dir=desc", + api_page, PER_PAGE, id + ); + let resp = + crate::helpers::create_request(&url, "GET")?.json_owned::()?; + let entries: Vec = resp + .data + .into_iter() + .map(|m| m.to_manga(BASE_URL)) + .collect(); + return Ok(MangaPageResult { + entries, + has_next_page: resp.next, + }); + } - match name { - // "Latest" and "Popular" standard tabs fallback - // Note: The API does not strictly support a "Popular" endpoint that differs significantly from "Nuevos" in this context without specific implementation. - "Latest" | "Popular" => { - if page > 1 { - return Ok(MangaPageResult { entries: vec![], has_next_page: false }); - } - let url = format!("{BACKEND_URL}/manhwa/nuevos"); - let resp = crate::helper::request_with_limits(&url, "GET")?.send()?.get_json::()?; - let entries: Vec = resp.manhwas.spanish_manhwas.iter().map(|m| Manga { - key: m.id_manhwa.clone().into(), - title: m.name_manhwa.clone(), - cover: m.img.clone().map(|s| s.into()), - url: Some(format!("{}/manhwa/{}", BASE_URL, m.id_manhwa)), - ..Default::default() - }).collect(); - Ok(MangaPageResult { entries, has_next_page: false }) - } - "New" => { - self.get_search_manga_list(None, page, vec![FilterValue::Sort { index: 3, ascending: false, id: "sortBy".into() }]) - } - "Erotic" | "+18 (Erotic)" => { - let url = format!("{BACKEND_URL}/manhwa/library?page={}&perPage={}&erotico=si", api_page, PER_PAGE); - let data = crate::helper::request_with_limits(&url, "GET")?.header("Referer", &format!("{}/", BASE_URL)).send()?.get_json::()?; - let entries = data.data.iter().map(|m| m.to_manga(BASE_URL)).collect(); - Ok(MangaPageResult { entries, has_next_page: data.next }) - } - // Search fallback - _ => { - let filters = vec![]; - self.get_search_manga_list(None, page, filters) - } - } - } + match name { + // "Latest" and "Popular" standard tabs fallback + // Note: The API does not strictly support a "Popular" endpoint that differs significantly from "Nuevos" in this context without specific implementation. + "Latest" | "Popular" => { + if page > 1 { + return Ok(MangaPageResult { + entries: vec![], + has_next_page: false, + }); + } + let url = format!("{BACKEND_URL}/manhwa/nuevos"); + let resp = + crate::helpers::create_request(&url, "GET")?.json_owned::()?; + let entries: Vec = resp + .manhwas + .spanish_manhwas + .into_iter() + .map(|m| { + let url = Some(format!("{}/manhwa/{}", BASE_URL, m.id_manhwa)); + Manga { + key: m.id_manhwa.into(), + title: m.name_manhwa, + cover: m.img.map(|s| s.into()), + url, + ..Default::default() + } + }) + .collect(); + Ok(MangaPageResult { + entries, + has_next_page: false, + }) + } + "New" => self.get_search_manga_list( + None, + page, + vec![FilterValue::Sort { + index: 3, + ascending: false, + id: "sortBy".into(), + }], + ), + "Erotic" | "+18 (Erotic)" => { + let url = format!( + "{BACKEND_URL}/manhwa/library?page={}&perPage={}&erotico=si", + api_page, PER_PAGE + ); + let data = crate::helpers::create_request(&url, "GET")? + .header("Referer", &format!("{}/", BASE_URL)) + .json_owned::()?; + let entries = data + .data + .into_iter() + .map(|m| m.to_manga(BASE_URL)) + .collect(); + Ok(MangaPageResult { + entries, + has_next_page: data.next, + }) + } + // Search fallback + _ => { + let filters = vec![]; + self.get_search_manga_list(None, page, filters) + } + } + } } diff --git a/sources/es.manhwaweb/src/lib.rs b/sources/es.manhwaweb/src/lib.rs index f17638dde..385ccc72d 100644 --- a/sources/es.manhwaweb/src/lib.rs +++ b/sources/es.manhwaweb/src/lib.rs @@ -1,11 +1,8 @@ #![no_std] -use aidoku::{ - prelude::*, - Source, -}; +use aidoku::{Source, prelude::*}; -mod helper; +mod helpers; mod imp; mod models; diff --git a/sources/es.manhwaweb/src/models.rs b/sources/es.manhwaweb/src/models.rs index 8b94c94c2..07a764f15 100644 --- a/sources/es.manhwaweb/src/models.rs +++ b/sources/es.manhwaweb/src/models.rs @@ -1,175 +1,189 @@ use aidoku::{ - alloc::{string::ToString, String, Vec}, - prelude::*, - Chapter, Manga, MangaStatus, Viewer, + Chapter, Manga, MangaStatus, Viewer, + alloc::{String, Vec}, + prelude::*, }; use serde::Deserialize; #[derive(Debug, Clone, Deserialize)] pub struct NuevosResponse { - // 'utimos_mangas_creados' (latest_mangas) and 'top' are never used by logic, - // so we can omit them. If the JSON structure requires them to be present but ignored, - // we can keep them or use `serde::IgnoredAny` if we want to be strict, - // but typically just omitting them from the struct works if `deny_unknown_fields` isn't on. - - // However, the `ManhwaWeb` logic accesses `data.manhwas.spanish_manhwas`. - pub manhwas: ManhwasCollection, + // 'utimos_mangas_creados' (latest_mangas) and 'top' are never used by logic, + // so we can omit them. If the JSON structure requires them to be present but ignored, + // we can keep them or use `serde::IgnoredAny` if we want to be strict, + // but typically just omitting them from the struct works if `deny_unknown_fields` isn't on. + + // However, the `ManhwaWeb` logic accesses `data.manhwas.spanish_manhwas`. + pub manhwas: ManhwasCollection, } #[derive(Debug, Clone, Deserialize)] pub struct ManhwasCollection { - #[serde(rename = "manhwas_esp")] - pub spanish_manhwas: Vec, + #[serde(rename = "manhwas_esp")] + pub spanish_manhwas: Vec, } // TopCollection and TopManga were seemingly unused. Removed. #[derive(Debug, Clone, Deserialize)] pub struct UpdateManga { - pub name_manhwa: String, - pub img: Option, - pub id_manhwa: String, - pub chapter: f32, - // 'create' was unused. - pub gru_name: Option, + pub name_manhwa: String, + pub img: Option, + pub id_manhwa: String, + pub chapter: f32, + // 'create' was unused. + pub gru_name: Option, } #[derive(Debug, Clone, Deserialize)] pub struct LibraryResponse { - pub data: Vec, - pub next: bool, + pub data: Vec, + pub next: bool, } #[derive(Debug, Clone, Deserialize)] pub struct LibraryManga { - #[serde(rename = "_id")] - pub id: String, - pub the_real_name: String, - #[serde(rename = "_imagen")] - pub image: Option, - #[serde(rename = "_status")] - pub status: Option, - #[serde(rename = "_erotico")] - pub erotic: Option, - #[serde(rename = "_categoris")] - pub categories: Option>, + #[serde(rename = "_id")] + pub id: String, + pub the_real_name: String, + #[serde(rename = "_imagen")] + pub image: Option, + #[serde(rename = "_status")] + pub status: Option, + #[serde(rename = "_erotico")] + pub erotic: Option, + #[serde(rename = "_categoris")] + pub categories: Option>, } impl LibraryManga { - pub fn to_manga(&self, base_url: &str) -> Manga { - Manga { - key: self.id.clone().into(), - title: self.the_real_name.clone(), - cover: self.image.clone().map(|s| s.into()), - url: Some(format!("{}/manhwa/{}", base_url, self.id)), - status: self.status.as_ref().map(|s| match s.as_str() { - "publicandose" => MangaStatus::Ongoing, - "finalizado" => MangaStatus::Completed, - "pausado" => MangaStatus::Hiatus, - _ => MangaStatus::Unknown, - }).unwrap_or(MangaStatus::Unknown), - ..Default::default() - } - } + pub fn to_manga(self, base_url: &str) -> Manga { + let url = Some(format!("{}/manhwa/{}", base_url, self.id)); + Manga { + key: self.id.into(), + title: self.the_real_name, + cover: self.image.map(|s| s.into()), + url, + status: self + .status + .as_ref() + .map(|s| match s.as_str() { + "publicandose" => MangaStatus::Ongoing, + "finalizado" => MangaStatus::Completed, + "pausado" => MangaStatus::Hiatus, + _ => MangaStatus::Unknown, + }) + .unwrap_or(MangaStatus::Unknown), + ..Default::default() + } + } } #[derive(Debug, Clone, Deserialize)] pub struct SeeResponse { - #[serde(rename = "_id")] - pub id: String, - pub the_real_name: String, - #[serde(rename = "_sinopsis")] - pub synopsis: Option, - #[serde(rename = "_imagen")] - pub image: Option, - #[serde(rename = "_status")] - pub status: Option, - #[serde(rename = "_tipo")] - pub serie_type: Option, - // Removed unused 'demography' and 'erotic' - #[serde(rename = "_categoris")] - pub categories: Option>, - pub chapters: Vec, + #[serde(rename = "_id")] + pub id: String, + pub the_real_name: String, + #[serde(rename = "_sinopsis")] + pub synopsis: Option, + #[serde(rename = "_imagen")] + pub image: Option, + #[serde(rename = "_status")] + pub status: Option, + #[serde(rename = "_tipo")] + pub serie_type: Option, + // Removed unused 'demography' and 'erotic' + #[serde(rename = "_categoris")] + pub categories: Option>, + pub chapters: Vec, } #[derive(Debug, Clone, Deserialize)] pub struct RawSeeChapter { - pub chapter: f32, - pub create: i64, - pub link: String, + pub chapter: f32, + pub create: i64, + pub link: String, } #[derive(Debug, Clone, Deserialize)] pub struct ChapterSeeResponse { - pub chapter: ChapterImgData, + pub chapter: ChapterImgData, } #[derive(Debug, Clone, Deserialize)] pub struct ChapterImgData { - pub img: Vec, + pub img: Vec, } impl SeeResponse { - pub fn parse_manga(&self, base_url: &str) -> Manga { - Manga { - key: self.id.clone().into(), - title: self.the_real_name.clone(), - description: self.synopsis.as_ref().map(|s| s.trim().into()), - cover: self.image.as_ref().map(|s| s.into()), - url: Some(format!("{base_url}/manhwa/{}", self.id)), - status: self - .status - .as_ref() - .map(|s| match s.as_str() { - "publicandose" => MangaStatus::Ongoing, - "finalizado" => MangaStatus::Completed, - "pausado" => MangaStatus::Hiatus, - _ => MangaStatus::Unknown, - }) - .unwrap_or(MangaStatus::Unknown), - viewer: self - .serie_type - .as_ref() - .map(|s| match s.as_str() { - "manhwa" | "manhua" => Viewer::Webtoon, - _ => Viewer::RightToLeft, - }) - .unwrap_or(Viewer::Unknown), - tags: self.categories.as_ref().map(|cats: &Vec| { - cats.iter() - .filter_map(|cat: &serde_json::Value| { - cat.as_object() - .and_then(|obj: &serde_json::Map| obj.values().next()) - .and_then(|v: &serde_json::Value| v.as_str()) - .map(|s: &str| s.into()) - }) - .collect() - }), - ..Default::default() - } - } - - pub fn parse_chapters(&self, _base_url: &str) -> Vec { - let mut chapters: Vec = self.chapters - .iter() - .map(|c| Chapter { - // The URL is like /leer/slug-number - // But the API for images uses the whole slug-number as ID (KEY) - key: c.link.rsplit('/').next().unwrap_or(&self.id).to_string(), - chapter_number: Some(c.chapter), - date_uploaded: Some(c.create / 1000), // convert ms to s - url: Some(c.link.clone()), - ..Default::default() - }) - .collect(); - - // Sort by chapter number descending - chapters.sort_by(|a, b| { - b.chapter_number - .partial_cmp(&a.chapter_number) - .unwrap_or(core::cmp::Ordering::Equal) - }); - - chapters - } + pub fn parse_manga(&self, base_url: &str) -> Manga { + Manga { + key: self.id.clone().into(), + title: self.the_real_name.clone(), + description: self.synopsis.as_ref().map(|s| s.trim().into()), + cover: self.image.as_ref().map(|s| s.into()), + url: Some(format!("{base_url}/manhwa/{}", self.id)), + status: self + .status + .as_ref() + .map(|s| match s.as_str() { + "publicandose" => MangaStatus::Ongoing, + "finalizado" => MangaStatus::Completed, + "pausado" => MangaStatus::Hiatus, + _ => MangaStatus::Unknown, + }) + .unwrap_or(MangaStatus::Unknown), + viewer: self + .serie_type + .as_ref() + .map(|s| match s.as_str() { + "manhwa" | "manhua" => Viewer::Webtoon, + _ => Viewer::RightToLeft, + }) + .unwrap_or(Viewer::Unknown), + tags: self + .categories + .as_ref() + .map(|cats: &Vec| { + cats.iter() + .filter_map(|cat: &serde_json::Value| { + cat.as_object() + .and_then(|obj: &serde_json::Map| { + obj.values().next() + }) + .and_then(|v: &serde_json::Value| v.as_str()) + .map(|s: &str| s.into()) + }) + .collect() + }), + ..Default::default() + } + } + + pub fn parse_chapters(&self, _base_url: &str) -> Vec { + let mut chapters: Vec = self + .chapters + .iter() + .map(|c| { + let url = Some(c.link.clone()); + Chapter { + // The URL is like /leer/slug-number + // But the API for images uses the whole slug-number as ID (KEY) + key: c.link.rsplit('/').next().unwrap_or(&self.id).into(), + chapter_number: Some(c.chapter), + date_uploaded: Some(c.create / 1000), // convert ms to s + url, + ..Default::default() + } + }) + .collect(); + + // Sort by chapter number descending + chapters.sort_by(|a, b| { + b.chapter_number + .partial_cmp(&a.chapter_number) + .unwrap_or(core::cmp::Ordering::Equal) + }); + + chapters + } }