From f62ea43de83615f83b4cf4b0b7bf9336a37670f0 Mon Sep 17 00:00:00 2001 From: devi Date: Sun, 26 Nov 2023 18:44:34 +0100 Subject: [PATCH] fix getlist on AnimeBootstrapParser, ImHentai, beetoon, CloneManga, MangaGeko, Po2Scans, Pururin add filter FlixScans, TeamXNovel, ComicExtra, MangaOwl.to, MangaTown, Manhwa18.net, ManhwasMen fix getlist and add tag list on DynastyScans fix getlist and add SortOrder.UPDATED on MangaStorm --- .../kotatsu/parsers/site/all/ImHentai.kt | 67 +++--- .../animebootstrap/AnimeBootstrapParser.kt | 51 ++--- .../parsers/site/animebootstrap/fr/PapScan.kt | 57 ++--- .../site/animebootstrap/id/KomikzoId.kt | 2 +- .../site/animebootstrap/id/NeuManga.kt | 4 +- .../site/animebootstrap/id/SekteKomik.kt | 1 - .../kotatsu/parsers/site/ar/FlixScans.kt | 84 +++++-- .../kotatsu/parsers/site/ar/MangaStorm.kt | 59 ++--- .../kotatsu/parsers/site/ar/TeamXNovel.kt | 91 +++++--- .../kotatsu/parsers/site/en/BeeToon.kt | 48 ++-- .../parsers/site/en/CloneMangaParser.kt | 22 +- .../kotatsu/parsers/site/en/ComicExtra.kt | 83 ++++--- .../kotatsu/parsers/site/en/DynastyScans.kt | 205 +++++++++++------- .../koitharu/kotatsu/parsers/site/en/Fakku.kt | 148 ------------- .../kotatsu/parsers/site/en/KskMoe.kt | 187 ---------------- .../kotatsu/parsers/site/en/MangaGeko.kt | 69 +++--- .../parsers/site/en/MangaTownParser.kt | 97 ++++++--- .../kotatsu/parsers/site/en/Mangaowl.kt | 73 ++++--- .../kotatsu/parsers/site/en/Manhwa18Parser.kt | 147 ++++++++----- .../kotatsu/parsers/site/en/ManhwasMen.kt | 45 ++-- .../kotatsu/parsers/site/en/Po2Scans.kt | 20 +- .../kotatsu/parsers/site/en/Pururin.kt | 60 ++--- 22 files changed, 767 insertions(+), 853 deletions(-) delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Fakku.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/KskMoe.kt diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ImHentai.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ImHentai.kt index 845259d3..3c9e0e39 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ImHentai.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ImHentai.kt @@ -24,43 +24,50 @@ internal class ImHentai(context: MangaLoaderContext) : override val isMultipleTagsSupported = false - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) + when (filter) { + is MangaListFilter.Search -> { + append("/search/?key=") + append(filter.query.urlEncoded()) + append("&page=") + append(page) + } + + is MangaListFilter.Advanced -> { - if (!query.isNullOrEmpty()) { - append("/search/?key=") - append(query.urlEncoded()) - append("&page=") - append(page) - } else if (!tags.isNullOrEmpty()) { - append("/tag/") - append(tag?.key.orEmpty()) - append("/") - when (sortOrder) { - SortOrder.UPDATED -> append("") - SortOrder.POPULARITY -> append("popular/") - else -> append("") + val tag = filter.tags.oneOrThrowIfMany() + if (filter.tags.isNotEmpty()) { + append("/tag/") + append(tag?.key.orEmpty()) + append("/") + when (filter.sortOrder) { + SortOrder.UPDATED -> append("") + SortOrder.POPULARITY -> append("popular/") + else -> append("") + } + append("?page=") + append(page) + } else { + append("/search/?page=") + append(page) + when (filter.sortOrder) { + SortOrder.UPDATED -> append("<=1&pp=0") + SortOrder.POPULARITY -> append("<=0&pp=1") + SortOrder.RATING -> append("<=0&pp=0") + else -> append("<=1&pp=0") + } + } } - append("?page=") - append(page) - } else { - append("/search/?page=") - append(page) - when (sortOrder) { - SortOrder.UPDATED -> append("<=1&pp=0") - SortOrder.POPULARITY -> append("<=0&pp=1") - SortOrder.RATING -> append("<=0&pp=0") - else -> append("<=1&pp=0") + + null -> { + append("/search/?lt=1&pp=0&page=") + append(page) } } + } val doc = webClient.httpGet(url).parseHtml() return doc.select("div.galleries div.thumb").map { div -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/AnimeBootstrapParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/AnimeBootstrapParser.kt index c5a89e1d..28a02ff6 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/AnimeBootstrapParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/AnimeBootstrapParser.kt @@ -11,8 +11,6 @@ import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.util.* import java.util.* -// see https://themewagon.com/themes/free-bootstrap-4-html5-gaming-anime-website-template-anime/ - internal abstract class AnimeBootstrapParser( context: MangaLoaderContext, source: MangaSource, @@ -40,13 +38,7 @@ internal abstract class AnimeBootstrapParser( searchPaginator.firstPage = 1 } - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) @@ -55,23 +47,31 @@ internal abstract class AnimeBootstrapParser( append(page.toString()) append("&type=all") - if (!query.isNullOrEmpty()) { - append("&search=") - append(query.urlEncoded()) - } + when (filter) { + is MangaListFilter.Search -> { + append("&search=") + append(filter.query.urlEncoded()) + } - if (!tags.isNullOrEmpty()) { - append("&categorie=") - append(tag?.key.orEmpty()) - } + is MangaListFilter.Advanced -> { + + filter.tags.oneOrThrowIfMany()?.let { + append("&categorie=") + append(it.key) + } + + append("&sort=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("view") + SortOrder.UPDATED -> append("updated") + SortOrder.ALPHABETICAL -> append("default") + SortOrder.NEWEST -> append("published") + else -> append("updated") + } - append("&sort=") - when (sortOrder) { - SortOrder.POPULARITY -> append("view") - SortOrder.UPDATED -> append("updated") - SortOrder.ALPHABETICAL -> append("default") - SortOrder.NEWEST -> append("published") - else -> append("updated") + } + + null -> append("&sort=updated") } } val doc = webClient.httpGet(url).parseHtml() @@ -115,11 +115,8 @@ internal abstract class AnimeBootstrapParser( override suspend fun getDetails(manga: Manga): Manga = coroutineScope { val fullUrl = manga.url.toAbsoluteUrl(domain) val doc = webClient.httpGet(fullUrl).parseHtml() - val chaptersDeferred = async { getChapters(doc) } - val desc = doc.selectFirstOrThrow(selectDesc).html() - val state = if (doc.select(selectState).isNullOrEmpty()) { MangaState.FINISHED } else { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/fr/PapScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/fr/PapScan.kt index 0833785d..667cc56a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/fr/PapScan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/fr/PapScan.kt @@ -1,6 +1,5 @@ package org.koitharu.kotatsu.parsers.site.animebootstrap.fr - import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import org.jsoup.nodes.Document @@ -13,20 +12,14 @@ import java.text.SimpleDateFormat import java.util.EnumSet import java.util.Locale - @MangaSourceParser("PAPSCAN", "PapScan", "fr") internal class PapScan(context: MangaLoaderContext) : AnimeBootstrapParser(context, MangaSource.PAPSCAN, "papscan.com") { - override val sourceLocale: Locale = Locale.ENGLISH - override val isMultipleTagsSupported = false - override val listUrl = "/liste-manga" - override val selectState = "div.anime__details__widget li:contains(En cours)" override val selectTag = "div.anime__details__widget li:contains(Genre) a" - override val selectChapter = "ul.chapters li" override val availableSortOrders: Set = EnumSet.of( @@ -34,40 +27,39 @@ internal class PapScan(context: MangaLoaderContext) : SortOrder.ALPHABETICAL, ) - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - - val tag = tags.oneOrThrowIfMany() - + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) append("/filterList") append("?page=") append(page.toString()) + when (filter) { + is MangaListFilter.Search -> { + append("&alpha=") + append(filter.query.urlEncoded()) + } - if (!query.isNullOrEmpty()) { - append("&alpha=") - append(query.urlEncoded()) - } + is MangaListFilter.Advanced -> { - if (!tags.isNullOrEmpty()) { - append("&cat=") - append(tag?.key.orEmpty()) - } - append("&sortBy=") - when (sortOrder) { - SortOrder.POPULARITY -> append("views") - SortOrder.ALPHABETICAL -> append("name") - else -> append("updated") + filter.tags.oneOrThrowIfMany()?.let { + append("&cat=") + append(it.key) + } + + append("&sortBy=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("views") + SortOrder.ALPHABETICAL -> append("name") + else -> append("updated") + } + + } + + null -> append("&sortBy=updated") } } val doc = webClient.httpGet(url).parseHtml() - return doc.select("div.product__item").map { div -> val href = div.selectFirstOrThrow("h5 a").attrAsRelativeUrl("href") Manga( @@ -103,17 +95,13 @@ internal class PapScan(context: MangaLoaderContext) : override suspend fun getDetails(manga: Manga): Manga = coroutineScope { val fullUrl = manga.url.toAbsoluteUrl(domain) val doc = webClient.httpGet(fullUrl).parseHtml() - val chaptersDeferred = async { getChapters(doc) } - val desc = doc.selectFirstOrThrow(selectDesc).html() - val state = if (doc.select(selectState).isNullOrEmpty()) { MangaState.FINISHED } else { MangaState.ONGOING } - manga.copy( tags = doc.body().select(selectTag).mapNotNullToSet { a -> MangaTag( @@ -145,5 +133,4 @@ internal class PapScan(context: MangaLoaderContext) : ) } } - } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/id/KomikzoId.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/id/KomikzoId.kt index cd7d64a1..6e2da189 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/id/KomikzoId.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/id/KomikzoId.kt @@ -5,6 +5,6 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.site.animebootstrap.AnimeBootstrapParser -@MangaSourceParser("KOMIKZOID", "Komikzo Id", "id") +@MangaSourceParser("KOMIKZOID", "KomikzoId", "id") internal class KomikzoId(context: MangaLoaderContext) : AnimeBootstrapParser(context, MangaSource.KOMIKZOID, "komikzoid.xyz") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/id/NeuManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/id/NeuManga.kt index 625459f6..4912109e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/id/NeuManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/id/NeuManga.kt @@ -1,12 +1,10 @@ package org.koitharu.kotatsu.parsers.site.animebootstrap.id - import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.site.animebootstrap.AnimeBootstrapParser - -@MangaSourceParser("NEUMANGA", "NeuManga", "id") +@MangaSourceParser("NEUMANGA", "NeuManga.xyz", "id") internal class NeuManga(context: MangaLoaderContext) : AnimeBootstrapParser(context, MangaSource.NEUMANGA, "neumanga.xyz") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/id/SekteKomik.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/id/SekteKomik.kt index 86f48685..574cba24 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/id/SekteKomik.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/id/SekteKomik.kt @@ -1,6 +1,5 @@ package org.koitharu.kotatsu.parsers.site.animebootstrap.id - import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.model.MangaSource diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/FlixScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/FlixScans.kt index 95a12125..54d4e545 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/FlixScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/FlixScans.kt @@ -19,32 +19,66 @@ import java.util.* internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.FLIXSCANS, 18) { override val availableSortOrders: Set = EnumSet.of(SortOrder.UPDATED) + override val availableStates: Set = EnumSet.allOf(MangaState::class.java) override val configKeyDomain = ConfigKey.Domain("flixscans.com") - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val json = if (!query.isNullOrEmpty()) { - if (page > 1) { - return emptyList() + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + + val json = when (filter) { + is MangaListFilter.Search -> { + if (page > 1) { + return emptyList() + } + val url = "https://api.$domain/api/v1/search/serie" + val body = JSONObject() + body.put("title", filter.query.urlEncoded()) + webClient.httpPost(url, body).parseJson().getJSONArray("data") } - val url = "https://api.$domain/api/v1/search/serie" - val body = JSONObject() - body.put("title", query.urlEncoded()) - webClient.httpPost(url, body).parseJson().getJSONArray("data") - } else if (!tags.isNullOrEmpty()) { - if (page > 1) { - return emptyList() + + is MangaListFilter.Advanced -> { + val url = buildString { + append("https://api.") + append(domain) + append("/api/v1/") + + if (filter.tags.isNotEmpty() || filter.states.isNotEmpty()) { + if (page > 1) { + return emptyList() + } + append("search/advance?=") + if (filter.tags.isNotEmpty()) { + val tagQuery = filter.tags.joinToString(separator = ",") { it.key } + append("&genres=") + append(tagQuery) + } + if (filter.states.isNotEmpty()) { + filter.states.oneOrThrowIfMany()?.let { + append("&status=") + append( + when (it) { + MangaState.ONGOING -> "ongoing" + MangaState.FINISHED -> "completed" + MangaState.ABANDONED -> "droped" + MangaState.PAUSED -> "onhold" + }, + ) + } + } + append("&serie_type=webtoon") + + } else { + append("webtoon/homepage/latest/home?page=") + append(page.toString()) + } + } + + webClient.httpGet(url).parseJson().getJSONArray("data") + } + + null -> { + val url = "https://api.$domain/api/v1/webtoon/homepage/latest/home?page=$page" + webClient.httpGet(url).parseJson().getJSONArray("data") } - val tagQuery = tags.joinToString(separator = ",") { it.key } - val url = "https://api.$domain/api/v1/search/advance?=&genres=$tagQuery&serie_type=webtoon" - webClient.httpGet(url).parseJson().getJSONArray("data") - } else { - val url = "https://api.$domain/api/v1/webtoon/homepage/latest/home?page=$page" - webClient.httpGet(url).parseJson().getJSONArray("data") } return json.mapJSON { j -> val href = "https://$domain/series/${j.getString("prefix")}-${j.getString("id")}-${j.getString("slug")}" @@ -62,6 +96,8 @@ internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context state = when (j.getString("status")) { "ongoing" -> MangaState.ONGOING "completed" -> MangaState.FINISHED + "onhold" -> MangaState.PAUSED + "droped" -> MangaState.ABANDONED else -> null }, author = null, @@ -77,9 +113,9 @@ internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context return tagsList.mapNotNullToSet { idTag -> val id = idTag.toInt() val idKey = json.getJSONObject(id).getInt("id") - val key = json.get(idKey).toString() + val key = json.getInt(idKey).toString() val idName = json.getJSONObject(id).getInt("name") - val name = json.get(idName).toString() + val name = json.getString(idName) MangaTag( key = key, title = name, diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/MangaStorm.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/MangaStorm.kt index 74aec220..4859e841 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/MangaStorm.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/MangaStorm.kt @@ -13,7 +13,7 @@ import java.util.* @MangaSourceParser("MANGASTORM", "MangaStorm", "ar") internal class MangaStorm(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.MANGASTORM, 30) { - override val availableSortOrders: Set = EnumSet.of(SortOrder.POPULARITY) + override val availableSortOrders: Set = EnumSet.of(SortOrder.POPULARITY, SortOrder.UPDATED) override val configKeyDomain = ConfigKey.Domain("mangastorm.org") override val isMultipleTagsSupported = false @@ -21,35 +21,44 @@ internal class MangaStorm(context: MangaLoaderContext) : PagedMangaParser(contex .add("User-Agent", UserAgents.CHROME_DESKTOP) .build() - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() - val url = - if (!tags.isNullOrEmpty()) { - buildString { - append("https://") - append(domain) - append("/categories/") - append(tag?.key.orEmpty()) - append("?page=") + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + val url = buildString { + append("https://") + append(domain) + when (filter) { + is MangaListFilter.Search -> { + append("/mangas?page=") append(page) + append("&query=") + append(filter.query.urlEncoded()) + } + + is MangaListFilter.Advanced -> { + + if (filter.tags.isNotEmpty()) { + val tag = filter.tags.oneOrThrowIfMany() + append("/categories/") + append(tag?.key.orEmpty()) + append("?page=") + append(page) + } else { + if (filter.sortOrder == SortOrder.POPULARITY) { + append("/mangas?page=") + append(page) + } else { + if (page > 1) { + return emptyList() + } + } + } } - } else { - buildString { - append("https://") - append(domain) + + null -> { append("/mangas?page=") append(page) - if (!query.isNullOrEmpty()) { - append("&query=") - append(query.urlEncoded()) - } } } + } val doc = webClient.httpGet(url).parseHtml() return doc.select("div.row div.col").map { div -> val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") @@ -74,9 +83,7 @@ internal class MangaStorm(context: MangaLoaderContext) : PagedMangaParser(contex override suspend fun getDetails(manga: Manga): Manga { val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() - val root = doc.selectFirstOrThrow(".card-body .col-lg-9") - return manga.copy( altTitle = null, state = null, diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/TeamXNovel.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/TeamXNovel.kt index a2805601..252c6fcb 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/TeamXNovel.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/TeamXNovel.kt @@ -17,46 +17,71 @@ import java.util.* internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.TEAMXNOVEL, 10) { override val availableSortOrders: Set = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY) + override val availableStates: Set = + EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED) + override val configKeyDomain = ConfigKey.Domain("team11x11.com") override val isMultipleTagsSupported = false - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + val url = buildString { append("https://") append(domain) - if (!tags.isNullOrEmpty()) { - append("/series?genre=") - append(tag?.key.orEmpty()) - if (page > 1) { - append("&page=") - append(page) - } - } else if (!query.isNullOrEmpty()) { - append("/series?search=") - append(query.urlEncoded()) - if (page > 1) { - append("&page=") - append(page) - } - } else { - when (sortOrder) { - SortOrder.POPULARITY -> append("/series") - SortOrder.UPDATED -> append("/") - else -> append("/") + when (filter) { + + is MangaListFilter.Search -> { + append("/series?search=") + append(filter.query.urlEncoded()) + if (page > 1) { + append("&page=") + append(page.toString()) + } } - if (page > 1) { - append("?page=") - append(page) + + is MangaListFilter.Advanced -> { + if (filter.tags.isNotEmpty()) { + val tag = filter.tags.oneOrThrowIfMany() + append("/series?genre=") + append(tag?.key.orEmpty()) + if (page > 1) { + append("&page=") + append(page.toString()) + } + append("&") + } else { + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("/series") + SortOrder.UPDATED -> append("/") + else -> append("/") + } + if (page > 1) { + append("?page=") + append(page.toString()) + append("&") + } else { + append("?") + } + } + + if (filter.sortOrder == SortOrder.POPULARITY || filter.tags.isNotEmpty()) { + filter.states.oneOrThrowIfMany()?.let { + append("status=") + append( + when (it) { + MangaState.ONGOING -> "مستمرة" + MangaState.FINISHED -> "مكتمل" + MangaState.ABANDONED -> "متوقف" + else -> "مستمرة" + }, + ) + } + } } + + null -> append("/?page=$page") } } - val doc = webClient.httpGet(url).parseHtml() return doc.select("div.listupd .bs .bsx").ifEmpty { doc.select("div.post-body .box") @@ -74,7 +99,8 @@ internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(contex tags = emptySet(), state = when (div.selectFirst(".status")?.text()) { "مستمرة" -> MangaState.ONGOING - "متوقف", "مكتمل" -> MangaState.FINISHED + "مكتمل" -> MangaState.FINISHED + "متوقف" -> MangaState.ABANDONED else -> null }, author = null, @@ -111,7 +137,8 @@ internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(contex altTitle = null, state = when (doc.selectFirstOrThrow(".full-list-info:contains(الحالة:) a").text()) { "مستمرة" -> MangaState.ONGOING - "متوقف", "مكتمل" -> MangaState.FINISHED + "مكتمل" -> MangaState.FINISHED + "متوقف" -> MangaState.ABANDONED else -> null }, tags = doc.select(".review-author-info a").mapNotNullToSet { a -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/BeeToon.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/BeeToon.kt index cb073d18..c5b6591d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/BeeToon.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/BeeToon.kt @@ -19,43 +19,41 @@ internal class BeeToon(context: MangaLoaderContext) : override val isMultipleTagsSupported = false - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) - when { - !query.isNullOrEmpty() -> { + when (filter) { + is MangaListFilter.Search -> { if (page > 1) { return emptyList() } append("/?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } - !tags.isNullOrEmpty() -> { - append("/genre/") - append(tag?.key.orEmpty()) - append("/page-") - append(page) - append("/") - } + is MangaListFilter.Advanced -> { - else -> { - when (sortOrder) { - SortOrder.UPDATED -> append("/latest-update/") - SortOrder.POPULARITY -> append("/popular-manga/") - else -> append("/latest-update/") + if (filter.tags.isNotEmpty()) { + val tag = filter.tags.oneOrThrowIfMany() + append("/genre/") + append(tag?.key.orEmpty()) + append("/page-") + append(page) + append("/") + } else { + when (filter.sortOrder) { + SortOrder.UPDATED -> append("/latest-update/") + SortOrder.POPULARITY -> append("/popular-manga/") + else -> append("/latest-update/") + } + append("page-") + append(page) + append("/") } - append("page-") - append(page) - append("/") } + + null -> append("/latest-update/page-$page/") } } val doc = webClient.httpGet(url).parseHtml() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/CloneMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/CloneMangaParser.kt index 7d36dab3..e01e0060 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/CloneMangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/CloneMangaParser.kt @@ -19,11 +19,25 @@ internal class CloneMangaParser(context: MangaLoaderContext) : MangaParser(conte override val configKeyDomain = ConfigKey.Domain("manga.clone-army.org") @InternalParsersApi - override suspend fun getList(offset: Int, query: String?, tags: Set?, sortOrder: SortOrder): List { - if (query != null || offset > 0) { - return emptyList() + override suspend fun getList(offset: Int, filter: MangaListFilter?): List { + + val link = when (filter) { + is MangaListFilter.Search -> { + return emptyList() + } + + is MangaListFilter.Advanced -> { + if (offset > 0) { + return emptyList() + } + + "https://$domain/viewer_landing.php" + } + + null -> "https://$domain/viewer_landing.php" + } - val link = "https://$domain/viewer_landing.php" + val doc = webClient.httpGet(link).parseHtml() val mangas = doc.getElementsByClass("comicPreviewContainer") return mangas.mapNotNull { item -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ComicExtra.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ComicExtra.kt index 1b1b40e2..042beaef 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ComicExtra.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ComicExtra.kt @@ -8,6 +8,7 @@ import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.network.UserAgents import org.koitharu.kotatsu.parsers.util.* +import java.lang.IllegalArgumentException import java.text.SimpleDateFormat import java.util.* @@ -17,6 +18,8 @@ internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(contex override val availableSortOrders: Set = EnumSet.of(SortOrder.POPULARITY, SortOrder.UPDATED, SortOrder.NEWEST) + override val availableStates: Set = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED) + override val configKeyDomain = ConfigKey.Domain("comicextra.me") override val isMultipleTagsSupported = false @@ -25,38 +28,62 @@ internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(contex .add("User-Agent", UserAgents.CHROME_DESKTOP) .build() - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { - append("https://$domain/") - if (!tags.isNullOrEmpty()) { - append(tag?.key.orEmpty()) - if (page > 1) { - append("/") - append(page) - } - } else if (!query.isNullOrEmpty()) { - append("comic-search?key=") - append(query.urlEncoded()) - if (page > 1) { - append("&page=") - append(page) + append("https://") + append(domain) + append("/") + when (filter) { + is MangaListFilter.Search -> { + append("comic-search?key=") + append(filter.query.urlEncoded()) + if (page > 1) { + append("&page=") + append(page.toString()) + } } - } else { - when (sortOrder) { - SortOrder.POPULARITY -> append("popular-comic/") - SortOrder.UPDATED -> append("new-comic/") - SortOrder.NEWEST -> append("recent-comic/") - else -> append("new-comic/") + + is MangaListFilter.Advanced -> { + if (filter.tags.isNotEmpty() && filter.states.isEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + append(it.key) + } + } else if (filter.tags.isEmpty() && filter.states.isNotEmpty()) { + filter.states.oneOrThrowIfMany()?.let { + append( + when (it) { + MangaState.ONGOING -> "/ongoing-comic" + MangaState.FINISHED -> "/completed-comic" + else -> "/ongoing-comic" + }, + ) + } + + } else if (filter.tags.isNotEmpty() && filter.states.isNotEmpty()) { + throw IllegalArgumentException("Source does not support tag + states filters") + } else { + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("popular-comic") + SortOrder.UPDATED -> append("new-comic") + SortOrder.NEWEST -> append("recent-comic") + else -> append("new-comic") + } + } + + if (page > 1) { + append("/") + append(page.toString()) + } } - if (page > 1) { - append(page) + + null -> { + append("popular-comic") + if (page > 1) { + append("/") + append(page.toString()) + } } + } } val doc = webClient.httpGet(url).parseHtml() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/DynastyScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/DynastyScans.kt index 634dc9ec..2200b663 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/DynastyScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/DynastyScans.kt @@ -1,8 +1,13 @@ package org.koitharu.kotatsu.parsers.site.en +import androidx.collection.ArraySet +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import okhttp3.Headers import org.json.JSONArray import org.jsoup.nodes.Document +import org.jsoup.nodes.Element import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.PagedMangaParser @@ -22,85 +27,128 @@ internal class DynastyScans(context: MangaLoaderContext) : PagedMangaParser(cont .add("User-Agent", UserAgents.CHROME_DESKTOP) .build() - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val url = buildString { - append("https://") - append(domain) - if (!query.isNullOrEmpty()) { - append("/search?q=") - append(query.urlEncoded()) - append("&") - append("classes[]".urlEncoded()) - append("=Serie&page=") - append(page.toString()) - } else if (!tags.isNullOrEmpty()) { - append("/tags/") - for (tag in tags) { - append(tag.key) + override val isMultipleTagsSupported = false + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + when (filter) { + is MangaListFilter.Search -> { + val url = buildString { + append("https://") + append(domain) + append("/search?q=") + append(filter.query.urlEncoded()) + append("&") + append("classes[]".urlEncoded()) + append("=Serie&page=") + append(page.toString()) } - append("?view=groupings&page=") - append(page.toString()) - } else { - append("/series?view=cover&page=") - append(page.toString()) + return parseMangaListQuery(webClient.httpGet(url).parseHtml()) } - } - val doc = webClient.httpGet(url).parseHtml() - - // There are no images on the search page - if (!query.isNullOrEmpty()) { - return doc.select("dl.chapter-list dd") - .map { div -> - val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") - Manga( - id = generateUid(href), - title = div.selectFirstOrThrow("a").text(), - altTitle = null, - url = href, - publicUrl = href.toAbsoluteUrl(domain), - rating = RATING_UNKNOWN, - isNsfw = false, - coverUrl = "", - tags = div.select("span.tags a").mapNotNullToSet { a -> - MangaTag( - key = a.attr("href").removeSuffix('/').substringAfterLast('/'), - title = a.text(), - source = source, - ) - }, - state = null, - author = null, - source = source, - ) + + is MangaListFilter.Advanced -> { + + val url = buildString { + append("https://") + append(domain) + if (filter.tags.isNotEmpty()) { + append("/tags/") + filter.tags.oneOrThrowIfMany()?.let { + append(it.key) + } + append("?view=groupings") + } else { + append("/series?view=cover") + + } + + append("&page=") + append(page.toString()) } - } else { - return doc.select("li.span2") - .map { div -> - val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") - Manga( - id = generateUid(href), - title = div.selectFirstOrThrow("div.caption").text(), - altTitle = null, - url = href, - publicUrl = href.toAbsoluteUrl(domain), - rating = RATING_UNKNOWN, - isNsfw = false, - coverUrl = div.selectFirstOrThrow("img").attrAsAbsoluteUrl("src"), - tags = setOf(), - state = null, - author = null, - source = source, - ) + return parseMangaList(webClient.httpGet(url).parseHtml()) + } + + null -> { + val url = buildString { + append("https://") + append(domain) + append("/series?view=cover&page=") + append(page.toString()) } + return parseMangaList(webClient.httpGet(url).parseHtml()) + } } } - override suspend fun getAvailableTags(): Set = emptySet() + + private fun parseMangaList(doc: Document): List { + return doc.select("li.span2") + .map { div -> + val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") + Manga( + id = generateUid(href), + title = div.selectFirstOrThrow("div.caption").text(), + altTitle = null, + url = href, + publicUrl = href.toAbsoluteUrl(domain), + rating = RATING_UNKNOWN, + isNsfw = false, + coverUrl = div.selectFirstOrThrow("img").attrAsAbsoluteUrl("src"), + tags = setOf(), + state = null, + author = null, + source = source, + ) + } + } + + private fun parseMangaListQuery(doc: Document): List { + return doc.select("dl.chapter-list dd") + .map { div -> + val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") + Manga( + id = generateUid(href), + title = div.selectFirstOrThrow("a").text(), + altTitle = null, + url = href, + publicUrl = href.toAbsoluteUrl(domain), + rating = RATING_UNKNOWN, + isNsfw = false, + coverUrl = "", + tags = div.select("span.tags a").mapNotNullToSet { a -> + MangaTag( + key = a.attr("href").removeSuffix('/').substringAfterLast('/'), + title = a.text(), + source = source, + ) + }, + state = null, + author = null, + source = source, + ) + } + } + + override suspend fun getAvailableTags(): Set { + return coroutineScope { + (1..3).map { page -> + async { getTags(page) } + } + }.awaitAll().flattenTo(ArraySet(360)) + } + + private suspend fun getTags(page: Int): Set { + val url = "https://$domain/tags?page=$page" + val root = webClient.httpGet(url).parseHtml() + return root.selectFirstOrThrow(".tag-list ").parseTags() + } + + private fun Element.parseTags() = select("a").mapToSet { + MangaTag( + key = it.attr("href").removeSuffix('/').substringAfterLast('/'), + title = it.text(), + source = source, + ) + } override suspend fun getDetails(manga: Manga): Manga { val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() @@ -110,19 +158,14 @@ internal class DynastyScans(context: MangaLoaderContext) : PagedMangaParser(cont altTitle = null, state = when (root.select("h2.tag-title small").last()?.text()) { "— Ongoing" -> MangaState.ONGOING - "— Completed" -> MangaState.FINISHED + "— Completed", "— Completed and Licensed" -> MangaState.FINISHED + "— Dropped", "— Licensed and Removed", "— Abandoned" -> MangaState.ABANDONED + "— On Hiatus" -> MangaState.PAUSED else -> null }, coverUrl = root.selectFirst("img.thumbnail")?.src() .orEmpty(), // It is needed if the manga was found via the search. - tags = root.select("div.tag-tags a").mapNotNullToSet { a -> - val href = a.attr("href").removeSuffix('/').substringAfterLast('/') - MangaTag( - key = href, - title = a.text(), - source = source, - ) - }, + tags = root.selectFirstOrThrow("div.tag-tags").parseTags(), author = null, description = null, chapters = chapters, diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Fakku.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Fakku.kt deleted file mode 100644 index b231da15..00000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Fakku.kt +++ /dev/null @@ -1,148 +0,0 @@ -package org.koitharu.kotatsu.parsers.site.en - -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope -import org.koitharu.kotatsu.parsers.MangaLoaderContext -import org.koitharu.kotatsu.parsers.MangaSourceParser -import org.koitharu.kotatsu.parsers.PagedMangaParser -import org.koitharu.kotatsu.parsers.config.ConfigKey -import org.koitharu.kotatsu.parsers.model.* -import org.koitharu.kotatsu.parsers.util.* -import org.koitharu.kotatsu.parsers.util.json.mapJSONToSet -import java.util.* - -@MangaSourceParser("FAKKU", "Fakku", "en", ContentType.HENTAI) -internal class Fakku(context: MangaLoaderContext) : - PagedMangaParser(context, MangaSource.FAKKU, pageSize = 25) { - - override val availableSortOrders: Set = - EnumSet.of(SortOrder.ALPHABETICAL, SortOrder.NEWEST, SortOrder.UPDATED) - - override val configKeyDomain = ConfigKey.Domain("fakku.cc") - - override val isMultipleTagsSupported = false - - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() - val url = buildString { - append("https://") - append(domain) - when { - !query.isNullOrEmpty() -> { - append("/search?q=") - append(query.urlEncoded()) - append("&") - } - - !tags.isNullOrEmpty() -> { - append("/tags/") - append(tag?.key.orEmpty()) - append("?") - } - - else -> { - append("?") - } - } - append("page=") - append(page) - append("&sort=") - when (sortOrder) { - SortOrder.ALPHABETICAL -> append("title") - SortOrder.NEWEST -> append("created_at") - SortOrder.UPDATED -> append("published_at") - else -> append("published_at") - } - } - val doc = webClient.httpGet(url).parseHtml() - return doc.select("div.entries .entry a").map { a -> - val href = a.attrAsRelativeUrl("href") - Manga( - id = generateUid(href), - url = href, - publicUrl = href.toAbsoluteUrl(domain), - coverUrl = a.selectFirst("img")?.src().orEmpty(), - title = a.selectFirst(".title")?.text().orEmpty(), - altTitle = null, - rating = RATING_UNKNOWN, - tags = emptySet(), - author = null, - state = null, - source = source, - isNsfw = isNsfwSource, - ) - } - } - - override suspend fun getAvailableTags(): Set { - val root = webClient.httpGet("https://$domain/tags").parseHtml() - return root.select("div.entries .entry a").mapToSet { - MangaTag( - key = it.attr("href").substringAfterLast("/"), - title = it.selectFirstOrThrow(".name").text(), - source = source, - ) - } - } - - override suspend fun getDetails(manga: Manga): Manga = coroutineScope { - val fullUrl = manga.url.toAbsoluteUrl(domain) - val doc = webClient.httpGet(fullUrl).parseHtml() - val genreDeferred = async { - webClient.httpGet(manga.url.toAbsoluteUrl(domain) + ".json").parseJson() - } - val genre = genreDeferred.await() - manga.copy( - author = doc.selectFirst("tr.artists a")?.text(), - tags = if (genre.toString().contains("tags")) { - genre.getJSONArray("tags").mapJSONToSet { - MangaTag( - key = it.getString("slug"), - title = it.getString("name"), - source = source, - ) - } - } else { - emptySet() - }, - chapters = listOf( - MangaChapter( - id = manga.id, - name = manga.title, - number = 1, - url = manga.url + "/1", - scanlator = null, - uploadDate = 0, - branch = null, - source = source, - ), - ), - ) - } - - override suspend fun getPages(chapter: MangaChapter): List { - val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml() - val totalPages = doc.selectFirstOrThrow(".total").text().toInt() - val rawUrl = chapter.url.substringBeforeLast("/") - return (1..totalPages).map { - val url = "$rawUrl/$it" - MangaPage( - id = generateUid(url), - url = url, - preview = null, - source = source, - ) - } - } - - override suspend fun getPageUrl(page: MangaPage): String { - val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml() - val root = doc.body() - return root.selectFirstOrThrow(".page img").attrAsAbsoluteUrl("src") - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/KskMoe.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/KskMoe.kt deleted file mode 100644 index e9c22fe8..00000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/KskMoe.kt +++ /dev/null @@ -1,187 +0,0 @@ -package org.koitharu.kotatsu.parsers.site.en - -import androidx.collection.ArraySet -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import org.json.JSONArray -import org.jsoup.nodes.Element -import org.koitharu.kotatsu.parsers.MangaLoaderContext -import org.koitharu.kotatsu.parsers.MangaSourceParser -import org.koitharu.kotatsu.parsers.PagedMangaParser -import org.koitharu.kotatsu.parsers.config.ConfigKey -import org.koitharu.kotatsu.parsers.model.* -import org.koitharu.kotatsu.parsers.util.* -import org.koitharu.kotatsu.parsers.util.json.mapJSON -import java.text.SimpleDateFormat -import java.util.* - -@MangaSourceParser("KSKMOE", "Ksk.moe", "en", ContentType.HENTAI) -internal class KskMoe(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.KSKMOE, 35) { - - override val availableSortOrders: Set = - EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.NEWEST, SortOrder.ALPHABETICAL) - override val configKeyDomain = ConfigKey.Domain("ksk.moe") - override val isMultipleTagsSupported = false - - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() - - val url = buildString { - append("https://") - append(domain) - - if (!tags.isNullOrEmpty()) { - append("/tags/") - append(tag?.key.orEmpty()) - } else { - append("/browse") - } - - if (page > 1) { - append("/page/") - append(page) - } - - when (sortOrder) { - SortOrder.POPULARITY -> append("?sort=32") - SortOrder.UPDATED -> append("") - SortOrder.NEWEST -> append("?sort=16") - SortOrder.ALPHABETICAL -> append("?sort=1") - else -> append("") - } - - if (!query.isNullOrEmpty()) { - append("?s=") - append(query.urlEncoded()) - } - } - val doc = webClient.httpGet(url).parseHtml() - - if (!doc.html().contains("pagination") && page > 1) { - return emptyList() - } - return doc.requireElementById("galleries").select("article").map { div -> - val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") - Manga( - id = generateUid(href), - title = div.selectLastOrThrow("h3 span").text(), - altTitle = null, - url = href, - publicUrl = href.toAbsoluteUrl(domain), - rating = RATING_UNKNOWN, - isNsfw = true, - coverUrl = div.selectFirstOrThrow("img").src()?.toAbsoluteUrl(domain).orEmpty(), - tags = div.select("footer span").mapNotNullToSet { span -> - MangaTag( - key = span.text().urlEncoded(), - title = span.text(), - source = source, - ) - }, - state = null, - author = null, - source = source, - ) - } - } - - override suspend fun getAvailableTags(): Set { - return coroutineScope { - (1..2).map { page -> - async { getTags(page) } - } - }.awaitAll().flattenTo(ArraySet(360)) - } - - private suspend fun getTags(page: Int): Set { - val url = if (page == 1) { - "https://$domain/tags" - } else { - "https://$domain/tags/page/$page" - } - val root = webClient.httpGet(url).parseHtml().body().getElementById("tags") - return root?.parseTags().orEmpty() - } - - private fun Element.parseTags() = select("section.tags div a").mapToSet { a -> - MangaTag( - key = a.attr("href").substringAfterLast("/tags/"), - title = a.selectFirstOrThrow("span").text(), - source = source, - ) - } - - private val date = SimpleDateFormat("dd.MM.yyyy hh:mm 'UTC'", Locale.US) - override suspend fun getDetails(manga: Manga): Manga { - val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() - - return manga.copy( - tags = doc.requireElementById("metadata").select("main div:contains(Tag) a").mapNotNullToSet { a -> - MangaTag( - key = a.attr("href").substringAfterLast("/tags/"), - title = a.selectFirstOrThrow("span").text(), - source = source, - ) - }, - author = doc.requireElementById("metadata").selectFirstOrThrow("main div:contains(Artist) a span").text(), - chapters = listOf( - MangaChapter( - id = generateUid(manga.id), - name = manga.title, - number = 1, - url = manga.url, - scanlator = null, - uploadDate = date.tryParse(doc.selectFirstOrThrow("time.updated").text()), - branch = null, - source = source, - ), - ), - ) - } - - override suspend fun getPages(chapter: MangaChapter): List { - val fullUrl = chapter.url - .replace("/view/", "/read/") - .let { "$it/1" } - .toAbsoluteUrl(domain) - val document = webClient.httpGet(fullUrl).parseHtml() - - val id = fullUrl - .substringAfter("/read/") - .substringBeforeLast("/") - - val cdnUrl = document.selectFirst("meta[itemprop=image]") - ?.attr("content") - ?.toHttpUrlOrNull() - ?.host - .let { "https://" + (it ?: domain) } - - val script = document.select("script:containsData(window.metadata)").html() - - val rawJson = script - .substringAfter("original:") - .substringBefore("resampled:") - .substringBeforeLast(",") - - return JSONArray(rawJson).mapJSON { - val fileName = it.getString("n") - - val url = "$cdnUrl/original/$id/$fileName" - val preview = "$cdnUrl/t/$id/320/$fileName" - - MangaPage( - id = generateUid(url), - url = url, - preview = preview, - source = source, - ) - } - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaGeko.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaGeko.kt index 81327a2d..c1cf6a1e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaGeko.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaGeko.kt @@ -25,42 +25,49 @@ internal class MangaGeko(context: MangaLoaderContext) : PagedMangaParser(context .add("User-Agent", UserAgents.CHROME_DESKTOP) .build() - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() - - val url = if (!query.isNullOrEmpty()) { - if (page > 1) { - return emptyList() - } - buildString { - append("https://$domain/search/?search=") - append(query.urlEncoded()) - } - } else { - buildString { - append("https://$domain/browse-comics/?results=") - append(page) - append("&filter=") - when (sortOrder) { - SortOrder.POPULARITY -> append("views") - SortOrder.UPDATED -> append("Updated") - SortOrder.NEWEST -> append("New") - else -> append("Updated") + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + + val url = buildString { + append("https://") + append(domain) + when (filter) { + is MangaListFilter.Search -> { + if (page > 1) { + return emptyList() + } + append("/search/?search=") + append(filter.query.urlEncoded()) } - if (!tags.isNullOrEmpty()) { - append("&genre=") - append(tag?.key.orEmpty()) + + is MangaListFilter.Advanced -> { + + append("/browse-comics/?results=") + append(page) + + append("&filter=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("views") + SortOrder.UPDATED -> append("Updated") + SortOrder.NEWEST -> append("New") + else -> append("Updated") + } + + if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + append("&genre=") + append(it.key) + } + } + } + + null -> { + append("/browse-comics/?results=") + append(page) + append("&filter=Updated") } } } - val doc = webClient.httpGet(url).parseHtml() - return doc.select("li.novel-item").map { div -> val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") Manga( diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaTownParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaTownParser.kt index 4cb4ed8f..6aad3269 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaTownParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaTownParser.kt @@ -22,35 +22,69 @@ internal class MangaTownParser(context: MangaLoaderContext) : MangaParser(contex SortOrder.UPDATED, ) - private val regexTag = Regex("[^\\-]+-[^\\-]+-[^\\-]+-[^\\-]+-[^\\-]+-[^\\-]+") - - override suspend fun getList( - offset: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val sortKey = when (sortOrder) { - SortOrder.ALPHABETICAL -> "?name.az" - SortOrder.RATING -> "?rating.za" - SortOrder.UPDATED -> "?last_chapter_time.za" - else -> "" - } + override val availableStates: Set = EnumSet.of( + MangaState.ONGOING, + MangaState.FINISHED, + ) + + override val isMultipleTagsSupported = false + + override suspend fun getList(offset: Int, filter: MangaListFilter?): List { val page = (offset / 30) + 1 - val url = when { - !query.isNullOrEmpty() -> { - if (offset != 0) { - return emptyList() + val url = buildString { + append("https://") + append(domain) + when (filter) { + is MangaListFilter.Search -> { + append("/search?name=") + append(filter.query.urlEncoded()) + append("&page=") + append(page.toString()) } - "/search?name=${query.urlEncoded()}".toAbsoluteUrl(domain) - } - tags.isNullOrEmpty() -> "/directory/$page.htm$sortKey".toAbsoluteUrl(domain) - tags.size == 1 -> "/directory/${tags.first().key}/$page.htm$sortKey".toAbsoluteUrl(domain) - else -> tags.joinToString( - prefix = "/search?page=$page".toAbsoluteUrl(domain), - ) { tag -> - "&genres[${tag.key}]=1" + is MangaListFilter.Advanced -> { + append("/directory/") + append("0-") + + if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + append(it.key) + } + } else { + append("0") + } + append("-0-") + + if (filter.states.isNotEmpty()) { + filter.states.oneOrThrowIfMany()?.let { + append( + when (it) { + MangaState.ONGOING -> "ongoing" + MangaState.FINISHED -> "completed" + else -> "0" + }, + ) + } + } else { + append("0") + } + + append("-0-0/") + append(page.toString()) + append(".htm") + + append( + when (filter.sortOrder) { + SortOrder.POPULARITY -> "" + SortOrder.UPDATED -> "?last_chapter_time.za" + SortOrder.ALPHABETICAL -> "?name.az" + SortOrder.RATING -> "?rating.za" + else -> "?last_chapter_time.za" + }, + ) + } + + null -> append("/directory/$page.htm?last_chapter_time.za") } } val doc = webClient.httpGet(url).parseHtml() @@ -81,7 +115,7 @@ internal class MangaTownParser(context: MangaLoaderContext) : MangaParser(contex tags = li.selectFirst("p.keyWord")?.select("a")?.mapNotNullToSet tags@{ x -> MangaTag( title = x.attr("title").toTitleCase(), - key = x.attr("href").parseTagKey() ?: return@tags null, + key = x.attr("href").substringAfter("/directory/0-").substringBefore("-0-"), source = MangaSource.MANGATOWN, ) }.orEmpty(), @@ -106,7 +140,7 @@ internal class MangaTownParser(context: MangaLoaderContext) : MangaParser(contex }?.select("a")?.mapNotNull { a -> MangaTag( title = a.attr("title").toTitleCase(), - key = a.attr("href").parseTagKey() ?: return@mapNotNull null, + key = a.attr("href").substringAfter("/directory/0-").substringBefore("-0-"), source = MangaSource.MANGATOWN, ) }.orEmpty(), @@ -165,10 +199,7 @@ internal class MangaTownParser(context: MangaLoaderContext) : MangaParser(contex ?.nextElementSibling() ?: doc.parseFailed("Root not found") return root.select("li").mapNotNullToSet { li -> val a = li.selectFirst("a") ?: return@mapNotNullToSet null - val key = a.attr("href").parseTagKey() - if (key.isNullOrEmpty()) { - return@mapNotNullToSet null - } + val key = a.attr("href").substringAfter("/directory/0-").substringBefore("-0-") MangaTag( source = MangaSource.MANGATOWN, key = key, @@ -211,6 +242,4 @@ internal class MangaTownParser(context: MangaLoaderContext) : MangaParser(contex ) } } - - private fun String.parseTagKey() = split('/').findLast { regexTag matches it } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Mangaowl.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Mangaowl.kt index 71f1d23a..a5e6fe0e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Mangaowl.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Mangaowl.kt @@ -24,6 +24,7 @@ internal class Mangaowl(context: MangaLoaderContext) : SortOrder.UPDATED, SortOrder.RATING, ) + override val availableStates: Set = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED) override val configKeyDomain = ConfigKey.Domain("mangaowl.to") @@ -31,46 +32,56 @@ internal class Mangaowl(context: MangaLoaderContext) : .add("User-Agent", UserAgents.CHROME_DESKTOP) .build() - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val sort = when (sortOrder) { - SortOrder.POPULARITY -> "view_count" - SortOrder.UPDATED -> "-modified_at" - SortOrder.NEWEST -> "created_at" - SortOrder.RATING -> "rating" - else -> "modified_at" - } + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + val url = buildString { append("https://") append(domain) - when { - !query.isNullOrEmpty() -> { - append("/8-search") - append("?q=") - append(query.urlEncoded()) + when (filter) { + is MangaListFilter.Search -> { + append("/10-search?q=") + append(filter.query.urlEncoded()) append("&page=") append(page.toString()) } - !tags.isNullOrEmpty() -> { - append("/8-genres/") - for (tag in tags) { - append(tag.key) - } + is MangaListFilter.Advanced -> { + + append("/10-comics") append("?page=") append(page.toString()) + + filter.tags.forEach { tag -> + append("&genres=") + append(tag.key) + } + + filter.states.oneOrThrowIfMany()?.let { + append("&status=") + append( + when (it) { + MangaState.ONGOING -> "ongoing" + MangaState.FINISHED -> "completed" + else -> "" + }, + ) + } + + append("&ordering=") + append( + when (filter.sortOrder) { + SortOrder.POPULARITY -> "view_count" + SortOrder.UPDATED -> "-modified_at" + SortOrder.NEWEST -> "created_at" + SortOrder.RATING -> "rating" + else -> "modified_at" + }, + ) } - else -> { - append("/8-comics") - append("?page=") + null -> { + append("/10-comics?ordering=-modified_at&page=") append(page.toString()) - append("&ordering=") - append(sort) } } } @@ -95,9 +106,9 @@ internal class Mangaowl(context: MangaLoaderContext) : } override suspend fun getAvailableTags(): Set { - val doc = webClient.httpGet("https://$domain/8-genres").parseHtml() + val doc = webClient.httpGet("https://$domain/10-genres").parseHtml() return doc.select("div.genres-container span.genre-item a").mapNotNullToSet { a -> - val key = a.attr("href").substringAfterLast("/") + val key = a.attr("href").removeSuffix('/').substringAfterLast('/').substringBefore("-") MangaTag( key = key, title = a.text(), @@ -112,7 +123,7 @@ internal class Mangaowl(context: MangaLoaderContext) : manga.copy( tags = doc.body().select("div.comic-attrs div.column.my-2:contains(Genres) a").mapNotNullToSet { a -> MangaTag( - key = a.attr("href").removeSuffix("/").substringAfterLast('/'), + key = a.attr("href").removeSuffix("/").substringAfterLast('/').substringBefore("-"), title = a.text().toTitleCase().replace(",", ""), source = source, ) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manhwa18Parser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manhwa18Parser.kt index 76f77453..27b1d5e9 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manhwa18Parser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manhwa18Parser.kt @@ -9,14 +9,26 @@ import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.util.* import java.util.* -@MangaSourceParser("MANHWA18", "Manhwa18", "en", type = ContentType.HENTAI) +@MangaSourceParser("MANHWA18", "Manhwa18.net", "en", type = ContentType.HENTAI) class Manhwa18Parser(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.MANHWA18, pageSize = 18, searchPageSize = 18) { override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("manhwa18.net") override val availableSortOrders: Set - get() = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.ALPHABETICAL, SortOrder.NEWEST) + get() = EnumSet.of( + SortOrder.UPDATED, + SortOrder.POPULARITY, + SortOrder.ALPHABETICAL, + SortOrder.NEWEST, + SortOrder.RATING, + ) + + override val availableStates: Set = EnumSet.of( + MangaState.ONGOING, + MangaState.FINISHED, + MangaState.PAUSED, + ) private val tagsMap = SuspendLazy(::parseTags) @@ -29,6 +41,82 @@ class Manhwa18Parser(context: MangaLoaderContext) : ) } + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + + val url = buildString { + append("https://") + append(domain) + append("/tim-kiem?page=") + append(page.toString()) + + when (filter) { + is MangaListFilter.Search -> { + append("&q=") + append(filter.query.urlEncoded()) + } + + is MangaListFilter.Advanced -> { + + append("&accept_genres=") + if (filter.tags.isNotEmpty()) { + append( + filter.tags.joinToString(",") { it.key }, + ) + } + + append("&sort=") + append( + when (filter.sortOrder) { + SortOrder.ALPHABETICAL -> "az" + SortOrder.POPULARITY -> "top" + SortOrder.UPDATED -> "update" + SortOrder.NEWEST -> "new" + SortOrder.RATING -> "like" + }, + ) + + filter.states.oneOrThrowIfMany()?.let { + append("&status=") + append( + when (it) { + MangaState.ONGOING -> "1" + MangaState.FINISHED -> "3" + MangaState.PAUSED -> "2" + else -> "" + }, + ) + } + } + + null -> append("&sort=update") + } + } + + val docs = webClient.httpGet(url).parseHtml() + + return docs.select(".card-body .thumb-item-flow") + .map { + val titleElement = it.selectFirstOrThrow(".thumb_attr.series-title > a") + val absUrl = titleElement.attrAsAbsoluteUrl("href") + Manga( + id = generateUid(absUrl.toRelativeUrl(domain)), + title = titleElement.text(), + altTitle = null, + url = absUrl.toRelativeUrl(domain), + publicUrl = absUrl, + rating = RATING_UNKNOWN, + isNsfw = true, + coverUrl = it.selectFirst("div.img-in-ratio")?.attrAsAbsoluteUrl("data-bg").orEmpty(), + tags = emptySet(), + state = null, + author = null, + largeCoverUrl = null, + description = null, + source = MangaSource.MANHWA18, + ) + } + } + override suspend fun getDetails(manga: Manga): Manga { val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() val cardInfoElement = docs.selectFirst("div.series-information") @@ -45,6 +133,7 @@ class Manhwa18Parser(context: MangaLoaderContext) : when (it.text().lowercase()) { "on going" -> MangaState.ONGOING "completed" -> MangaState.FINISHED + "on hold" -> MangaState.PAUSED else -> null } } @@ -99,60 +188,6 @@ class Manhwa18Parser(context: MangaLoaderContext) : return cal.time.time } - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val sortQuery = when (sortOrder) { - SortOrder.ALPHABETICAL -> "az" - SortOrder.POPULARITY -> "top" - SortOrder.UPDATED -> "update" - SortOrder.NEWEST -> "new" - else -> "" - } - - val tagQuery = tags?.joinToString(",") { it.key }.orEmpty() - val url = buildString { - append("https://") - append(domain) - append("/tim-kiem?page=") - append(page) - if (!query.isNullOrEmpty()) { - append("&q=") - append(query.urlEncoded()) - } - append("&accept_genres=$tagQuery") - append("&sort=") - append(sortQuery) - } - - val docs = webClient.httpGet(url).parseHtml() - - return docs.select(".card-body .thumb-item-flow") - .map { - val titleElement = it.selectFirstOrThrow(".thumb_attr.series-title > a") - val absUrl = titleElement.attrAsAbsoluteUrl("href") - Manga( - id = generateUid(absUrl.toRelativeUrl(domain)), - title = titleElement.text(), - altTitle = null, - url = absUrl.toRelativeUrl(domain), - publicUrl = absUrl, - rating = RATING_UNKNOWN, - isNsfw = true, - coverUrl = it.selectFirst("div.img-in-ratio")?.attrAsAbsoluteUrl("data-bg").orEmpty(), - tags = emptySet(), - state = null, - author = null, - largeCoverUrl = null, - description = null, - source = MangaSource.MANHWA18, - ) - } - } - override suspend fun getPages(chapter: MangaChapter): List { val chapterUrl = chapter.url.toAbsoluteUrl(domain) val doc = webClient.httpGet(chapterUrl).parseHtml() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ManhwasMen.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ManhwasMen.kt index 8bb514ca..970bd688 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ManhwasMen.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ManhwasMen.kt @@ -21,29 +21,42 @@ class ManhwasMen(context: MangaLoaderContext) : override val availableSortOrders: Set get() = EnumSet.of(SortOrder.POPULARITY) - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() + override val availableStates: Set = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED) + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + val url = buildString { append("https://") append(domain) append("/manga-list") append("?page=") - append(page) - when { - !query.isNullOrEmpty() -> { + append(page.toString()) + when (filter) { + is MangaListFilter.Search -> { append("&search=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } - !tags.isNullOrEmpty() -> { - append("&genero=") - append(tag?.key.orEmpty()) + is MangaListFilter.Advanced -> { + + filter.tags.oneOrThrowIfMany()?.let { + append("&genero=") + append(it.key) + } + + filter.states.oneOrThrowIfMany()?.let { + append("&estado=") + append( + when (it) { + MangaState.ONGOING -> "ongoing" + MangaState.FINISHED -> "complete" + else -> "" + }, + ) + } } + + null -> {} } } val doc = webClient.httpGet(url).parseHtml() @@ -89,9 +102,9 @@ class ManhwasMen(context: MangaLoaderContext) : ) }, description = doc.select(".sinopsis").html(), - state = when (doc.selectLast(".anime-type-peli")?.text()?.lowercase()) { + state = when (doc.selectLast("span.anime-type-peli")?.text()?.lowercase()) { "ongoing" -> MangaState.ONGOING - "completed" -> MangaState.FINISHED + "complete" -> MangaState.FINISHED else -> null }, chapters = doc.select(".episodes-list li").mapChapters(reversed = true) { i, li -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Po2Scans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Po2Scans.kt index 1659a2c5..bbd415dd 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Po2Scans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Po2Scans.kt @@ -15,20 +15,28 @@ internal class Po2Scans(context: MangaLoaderContext) : MangaParser(context, Mang override val availableSortOrders: Set = EnumSet.of(SortOrder.ALPHABETICAL) override val configKeyDomain = ConfigKey.Domain("po2scans.com") - override suspend fun getList(offset: Int, query: String?, tags: Set?, sortOrder: SortOrder): List { + override suspend fun getList(offset: Int, filter: MangaListFilter?): List { if (offset > 0) { return emptyList() } val url = buildString { - append("https://$domain/series") - if (!query.isNullOrEmpty()) { - append("?search=") - append(query.urlEncoded()) + append("https://") + append(domain) + append("/series") + when (filter) { + is MangaListFilter.Search -> { + append("?search=") + append(filter.query.urlEncoded()) + } + + is MangaListFilter.Advanced -> {} + + null -> {} } } val doc = webClient.httpGet(url).parseHtml() return doc.select(".series-list").map { div -> - val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") + val href = "/" + div.selectFirstOrThrow("a").attrAsRelativeUrl("href") Manga( id = generateUid(href), title = div.selectFirstOrThrow("h2").text(), diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Pururin.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Pururin.kt index 1940df62..ce48030f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Pururin.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Pururin.kt @@ -24,37 +24,43 @@ internal class Pururin(context: MangaLoaderContext) : override val isMultipleTagsSupported = false - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) - if (!query.isNullOrEmpty()) { - append("/search?q=") - append(query.urlEncoded()) - append("&page=") - append(page) - } else { - append("/browse") - if (!tags.isNullOrEmpty()) { - append("/tags/content/") - append(tag?.key.orEmpty()) - append("/") + when (filter) { + is MangaListFilter.Search -> { + append("/search?q=") + append(filter.query.urlEncoded()) + append("&page=") + append(page.toString()) } - append("?page=") - append(page) - append("&sort=") - when (sortOrder) { - SortOrder.UPDATED -> append("") - SortOrder.POPULARITY -> append("most-viewed") - SortOrder.RATING -> append("highest-rated") - SortOrder.ALPHABETICAL -> append("title") - else -> append("") + + is MangaListFilter.Advanced -> { + append("/browse") + + filter.tags.oneOrThrowIfMany()?.let { + append("/tags/content/") + append(it.key) + append("/") + } + + append("?page=") + append(page) + + append("&sort=") + when (filter.sortOrder) { + SortOrder.UPDATED -> append("") + SortOrder.POPULARITY -> append("most-viewed") + SortOrder.RATING -> append("highest-rated") + SortOrder.ALPHABETICAL -> append("title") + else -> append("") + } + } + + null -> { + append("/browse?page=") + append(page) } } }