diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/BatoToParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/BatoToParser.kt index c9da08f5..39c95502 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/BatoToParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/BatoToParser.kt @@ -33,6 +33,8 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser( SortOrder.ALPHABETICAL, ) + override val availableStates: Set = EnumSet.allOf(MangaState::class.java) + override val configKeyDomain = ConfigKey.Domain( "bato.to", "batocomic.com", @@ -61,37 +63,64 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser( "zbato.org", ) - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - if (!query.isNullOrEmpty()) { - return search(page, query) - } - @Suppress("NON_EXHAUSTIVE_WHEN_STATEMENT") - val url = buildString { - append("https://") - append(domain) - append("/browse?sort=") - when (sortOrder) { - SortOrder.UPDATED, - -> append("update.za") + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { - SortOrder.POPULARITY -> append("views_a.za") - SortOrder.NEWEST -> append("create.za") - SortOrder.ALPHABETICAL -> append("title.az") - SortOrder.RATING -> Unit + when (filter) { + is MangaListFilter.Search -> { + return search(page, filter.query) } - if (!tags.isNullOrEmpty()) { - append("&genres=") - appendAll(tags, ",") { it.key } + + is MangaListFilter.Advanced -> { + + val url = buildString { + append("https://") + append(domain) + + append("/browse?sort=") + when (filter.sortOrder) { + SortOrder.UPDATED -> append("update.za") + SortOrder.POPULARITY -> append("views_a.za") + SortOrder.NEWEST -> append("create.za") + SortOrder.ALPHABETICAL -> append("title.az") + else -> append("update.za") + } + + filter.states.oneOrThrowIfMany()?.let { + append("&release=") + append( + when (it) { + MangaState.ONGOING -> "ongoing" + MangaState.FINISHED -> "completed" + MangaState.ABANDONED -> "cancelled" + MangaState.PAUSED -> "hiatus" + }, + ) + } + // langs= en ... + + if (filter.tags.isNotEmpty()) { + append("&genres=") + appendAll(filter.tags, ",") { it.key } + } + + append("&page=") + append(page) + } + + return parseList(url, page) + } + + null -> { + val url = buildString { + append("https://") + append(domain) + append("/browse?sort=update.za") + append("&page=") + append(page) + } + return parseList(url, page) } - append("&page=") - append(page) } - return parseList(url, page) } override suspend fun getDetails(manga: Manga): Manga { @@ -109,9 +138,11 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser( ?.selectFirst(".limit-html") ?.html(), tags = manga.tags + attrs["Genres:"]?.parseTags().orEmpty(), - state = when (attrs["Release status:"]?.text()) { + state = when (attrs["Original work:"]?.text()) { "Ongoing" -> MangaState.ONGOING "Completed" -> MangaState.FINISHED + "Cancelled" -> MangaState.ABANDONED + "Hiatus" -> MangaState.PAUSED else -> manga.state }, author = attrs["Authors:"]?.text()?.trim() ?: manga.author, diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ComickFunParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ComickFunParser.kt index 9e579518..30a4778d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ComickFunParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ComickFunParser.kt @@ -114,9 +114,11 @@ internal class ComickFunParser(context: MangaLoaderContext) : PagedMangaParser(c val url = "https://api.$domain/comic/${manga.url}?tachiyomi=true" val jo = webClient.httpGet(url).parseJson() val comic = jo.getJSONObject("comic") + var alt = "" + comic.getJSONArray("md_titles").mapJSON { alt += it.getString("title") + " - " } return manga.copy( title = comic.getString("title"), - altTitle = null, // TODO + altTitle = alt, isNsfw = jo.getBoolean("matureContent") || comic.getBoolean("hentai"), description = comic.getStringOrNull("parsed") ?: comic.getStringOrNull("desc"), tags = manga.tags + comic.getJSONArray("md_comic_md_genres").mapJSONToSet { 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/all/LineWebtoonsParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/LineWebtoonsParser.kt index 33cbe5af..80120a6d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/LineWebtoonsParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/LineWebtoonsParser.kt @@ -132,83 +132,117 @@ internal abstract class LineWebtoonsParser( } } - override suspend fun getList( - offset: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val genre = tags.oneOrThrowIfMany()?.key ?: "ALL" - - val sortOrderStr = when (sortOrder) { - SortOrder.UPDATED -> "UPDATE" - SortOrder.POPULARITY -> "READ_COUNT" - SortOrder.RATING -> "LIKEIT" - else -> throw IllegalArgumentException("Unsupported sort order: $sortOrder") - } - - val manga = if (query != null) { - if (!tags.isNullOrEmpty()) { - throw IllegalArgumentException("This source does not support search with tags") - } + override suspend fun getList(offset: Int, filter: MangaListFilter?): List { + val manga = + when (filter) { + is MangaListFilter.Search -> { + makeRequest("/lineWebtoon/webtoon/searchChallenge?query=${filter.query.urlEncoded()}&startIndex=${offset + 1}&pageSize=20") + .getJSONObject("challengeSearch") + .getJSONArray("titleList") + .mapJSON { jo -> + val titleNo = jo.getLong("titleNo") + + Manga( + id = generateUid(titleNo), + title = jo.getString("title"), + altTitle = null, + url = titleNo.toString(), + publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo", + rating = RATING_UNKNOWN, + isNsfw = isNsfwSource, + coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain), + largeCoverUrl = null, + tags = emptySet(), + author = jo.getStringOrNull("writingAuthorName"), + description = null, + state = null, + source = source, + ) + } + } - makeRequest("/lineWebtoon/webtoon/searchChallenge?query=${query.urlEncoded()}&startIndex=${offset + 1}&pageSize=20") - .getJSONObject("challengeSearch") - .getJSONArray("titleList") - .mapJSON { jo -> - val titleNo = jo.getLong("titleNo") - - Manga( - id = generateUid(titleNo), - title = jo.getString("title"), - altTitle = null, - url = titleNo.toString(), - publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo", - rating = RATING_UNKNOWN, - isNsfw = isNsfwSource, - coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain), - largeCoverUrl = null, - tags = emptySet(), - author = jo.getStringOrNull("writingAuthorName"), - description = null, - state = null, - source = source, - ) + is MangaListFilter.Advanced -> { + + val genre = filter.tags.oneOrThrowIfMany()?.key ?: "ALL" + + val sortOrderStr = when (filter.sortOrder) { + SortOrder.UPDATED -> "UPDATE" + SortOrder.POPULARITY -> "READ_COUNT" + SortOrder.RATING -> "LIKEIT" + else -> throw IllegalArgumentException("Unsupported sort order: ${filter.sortOrder}") + } + + val result = + makeRequest("/lineWebtoon/webtoon/challengeGenreTitleList.json?genre=$genre&sortOrder=$sortOrderStr&startIndex=${offset + 1}&pageSize=20") + + val genres = result.getJSONObject("genreList") + .getJSONArray("challengeGenres") + .mapJSON { jo -> parseTag(jo) } + .associateBy { tag -> tag.key } + + result + .getJSONObject("titleList") + .getJSONArray("titles") + .mapJSON { jo -> + val titleNo = jo.getLong("titleNo") + + Manga( + id = generateUid(titleNo), + title = jo.getString("title"), + altTitle = null, + url = titleNo.toString(), + publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo", + rating = jo.getFloatOrDefault("starScoreAverage", -10f) / 10f, + isNsfw = jo.getBooleanOrDefault("ageGradeNotice", isNsfwSource), + coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain), + largeCoverUrl = jo.getStringOrNull("thumbnailVertical")?.toAbsoluteUrl(staticDomain), + tags = setOfNotNull(genres[jo.getString("representGenre")]), + author = jo.getStringOrNull("writingAuthorName"), + description = jo.getString("synopsis"), + // I don't think the API provides this info + state = null, + source = source, + ) + } } - } else { - val result = - makeRequest("/lineWebtoon/webtoon/challengeGenreTitleList.json?genre=$genre&sortOrder=$sortOrderStr&startIndex=${offset + 1}&pageSize=20") - - val genres = result.getJSONObject("genreList") - .getJSONArray("challengeGenres") - .mapJSON { jo -> parseTag(jo) } - .associateBy { tag -> tag.key } - - result - .getJSONObject("titleList") - .getJSONArray("titles") - .mapJSON { jo -> - val titleNo = jo.getLong("titleNo") - - Manga( - id = generateUid(titleNo), - title = jo.getString("title"), - altTitle = null, - url = titleNo.toString(), - publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo", - rating = jo.getFloatOrDefault("starScoreAverage", -10f) / 10f, - isNsfw = jo.getBooleanOrDefault("ageGradeNotice", isNsfwSource), - coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain), - largeCoverUrl = jo.getStringOrNull("thumbnailVertical")?.toAbsoluteUrl(staticDomain), - tags = setOfNotNull(genres[jo.getString("representGenre")]), - author = jo.getStringOrNull("writingAuthorName"), - description = jo.getString("synopsis"), - // I don't think the API provides this info - state = null, - source = source, - ) + + null -> { + + val result = + makeRequest("/lineWebtoon/webtoon/challengeGenreTitleList.json?genre=ALL&sortOrder=UPDATE&startIndex=${offset + 1}&pageSize=20") + + val genres = result.getJSONObject("genreList") + .getJSONArray("challengeGenres") + .mapJSON { jo -> parseTag(jo) } + .associateBy { tag -> tag.key } + + result + .getJSONObject("titleList") + .getJSONArray("titles") + .mapJSON { jo -> + val titleNo = jo.getLong("titleNo") + + Manga( + id = generateUid(titleNo), + title = jo.getString("title"), + altTitle = null, + url = titleNo.toString(), + publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo", + rating = jo.getFloatOrDefault("starScoreAverage", -10f) / 10f, + isNsfw = jo.getBooleanOrDefault("ageGradeNotice", isNsfwSource), + coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain), + largeCoverUrl = jo.getStringOrNull("thumbnailVertical")?.toAbsoluteUrl(staticDomain), + tags = setOfNotNull(genres[jo.getString("representGenre")]), + author = jo.getStringOrNull("writingAuthorName"), + description = jo.getString("synopsis"), + // I don't think the API provides this info + state = null, + source = source, + ) + } } - } + } + return manga } @@ -309,10 +343,10 @@ internal abstract class LineWebtoonsParser( } fun makeEncryptUrl(urlBuilder: HttpUrl.Builder) { - val msgpad = Calendar.getInstance().timeInMillis.toString() - val digest = getMessageDigest(getMessage(urlBuilder.build().toString(), msgpad)) + val msgPad = Calendar.getInstance().timeInMillis.toString() + val digest = getMessageDigest(getMessage(urlBuilder.build().toString(), msgPad)) urlBuilder - .addQueryParameter("msgpad", msgpad) + .addQueryParameter("msgpad", msgPad) .addQueryParameter("md", digest) // .addEncodedQueryParameter("md", digest.urlEncoded()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaDexParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaDexParser.kt index 7edcff84..53e2b361 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaDexParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaDexParser.kt @@ -30,19 +30,12 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context override val configKeyDomain = ConfigKey.Domain("mangadex.org") - override val availableSortOrders: EnumSet = EnumSet.of( - SortOrder.UPDATED, - SortOrder.ALPHABETICAL, - SortOrder.NEWEST, - SortOrder.POPULARITY, - ) + override val availableSortOrders: EnumSet = EnumSet.allOf(SortOrder::class.java) - override suspend fun getList( - offset: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { + override val availableStates: Set = EnumSet.allOf(MangaState::class.java) + + + override suspend fun getList(offset: Int, filter: MangaListFilter?): List { val domain = domain val url = buildString { append("https://api.") @@ -52,29 +45,46 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context append("&offset=") append(offset) append("&includes[]=cover_art&includes[]=author&includes[]=artist&") - tags?.forEach { tag -> - append("includedTags[]=") - append(tag.key) - append('&') - } - if (!query.isNullOrEmpty()) { - append("title=") - append(query.urlEncoded()) - append('&') - } - append(CONTENT_RATING) - append("&order") - append( - when (sortOrder) { - SortOrder.UPDATED, - -> "[latestUploadedChapter]=desc" + when (filter) { + is MangaListFilter.Search -> { + append("title=") + append(filter.query) + append('&') + } - SortOrder.ALPHABETICAL -> "[title]=asc" - SortOrder.NEWEST -> "[createdAt]=desc" - SortOrder.POPULARITY -> "[followedCount]=desc" - else -> "[followedCount]=desc" - }, - ) + is MangaListFilter.Advanced -> { + filter.tags.forEach { tag -> + append("includedTags[]=") + append(tag.key) + append('&') + } + + append(CONTENT_RATING) + append("&order") + append( + when (filter.sortOrder) { + SortOrder.UPDATED -> "[latestUploadedChapter]=desc" + SortOrder.RATING -> "[rating]=desc" + SortOrder.ALPHABETICAL -> "[title]=asc" + SortOrder.NEWEST -> "[createdAt]=desc" + SortOrder.POPULARITY -> "[followedCount]=desc" + }, + ) + filter.states.forEach { + append("&status[]=") + when (it) { + MangaState.ONGOING -> append("ongoing") + MangaState.FINISHED -> append("completed") + MangaState.ABANDONED -> append("cancelled") + MangaState.PAUSED -> append("hiatus") + } + } + } + + null -> { + append("&order[latestUploadedChapter]=desc") + } + } } val json = webClient.httpGet(url).parseJson().getJSONArray("data") return json.mapJSON { jo -> @@ -110,9 +120,11 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context source = source, ) }, - state = when (jo.getStringOrNull("status")) { + state = when (attrs.getStringOrNull("status")) { "ongoing" -> MangaState.ONGOING "completed" -> MangaState.FINISHED + "hiatus" -> MangaState.PAUSED + "cancelled" -> MangaState.ABANDONED else -> null }, author = (relations["author"] ?: relations["artist"]) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NineMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NineMangaParser.kt index 5d9ef843..4ef6b192 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NineMangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NineMangaParser.kt @@ -35,6 +35,11 @@ internal abstract class NineMangaParser( SortOrder.POPULARITY, ) + override val availableStates: Set = EnumSet.of( + MangaState.ONGOING, + MangaState.FINISHED, + ) + override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() val newRequest = if (request.url.host == domain) { @@ -45,37 +50,60 @@ internal abstract class NineMangaParser( return chain.proceed(newRequest) } - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) - when { - !query.isNullOrEmpty() -> { + when (filter) { + is MangaListFilter.Search -> { append("/search/?name_sel=&wd=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") + append(page) + append(".html") } - !tags.isNullOrEmpty() -> { - append("/search/?category_id=") - for (tag in tags) { - append(tag.key) - append(',') + is MangaListFilter.Advanced -> { + if (filter.tags.isNotEmpty()) { + append("/search/?category_id=") + for (tag in filter.tags) { + append(tag.key) + append(',') + } + + filter.states.oneOrThrowIfMany()?.let { + append("&completed_series=") + when (it) { + MangaState.ONGOING -> append("no") + MangaState.FINISHED -> append("yes") + else -> append("either") + } + } + append("&page=") + } else { + append("/category/") + if (filter.states.isNotEmpty()) { + filter.states.oneOrThrowIfMany()?.let { + when (it) { + MangaState.ONGOING -> append("updated_") + MangaState.FINISHED -> append("completed_") + else -> append("either") + } + } + } else { + append("index_") + } } - append("&page=") + append(page) + append(".html") } - else -> { + null -> { append("/category/index_") + append(page) + append(".html") } } - append(page) - append(".html") } val doc = webClient.httpGet(url).parseHtml() val root = doc.body().selectFirst("ul.direlist") ?: doc.parseFailed("Cannot find root") 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) } } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/TempleScanEsp.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/TempleScanEsp.kt index 1413e7b1..ec17b58d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/TempleScanEsp.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/TempleScanEsp.kt @@ -10,6 +10,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.util.* @MangaSourceParser("TEMPLESCANESP", "TempleScanEsp", "es", ContentType.HENTAI) @@ -24,22 +25,29 @@ internal class TempleScanEsp(context: MangaLoaderContext) : .add("User-Agent", UserAgents.CHROME_DESKTOP) .build() - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) - if (sortOrder == SortOrder.NEWEST) { - append("/comics") - append("?page=") - append(page.toString()) - } else { - if (page > 1) { - return emptyList() + when (filter) { + is MangaListFilter.Search -> { + throw IllegalArgumentException("Search is not supported by this source") + } + + is MangaListFilter.Advanced -> { + if (filter.sortOrder == SortOrder.NEWEST) { + append("/comics?page=") + append(page.toString()) + } else { + if (page > 1) { + return emptyList() + } + } + } + + null -> { + append("/comics?page=") + append(page.toString()) } } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/TuMangaOnlineParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/TuMangaOnlineParser.kt index 595831ea..02ba7388 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/TuMangaOnlineParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/TuMangaOnlineParser.kt @@ -32,41 +32,47 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser( SortOrder.RATING, ) - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - - val order = - when (sortOrder) { - SortOrder.POPULARITY -> "likes_count" - SortOrder.UPDATED -> "release_date" - SortOrder.NEWEST -> "creation" - SortOrder.ALPHABETICAL -> "alphabetically" - SortOrder.RATING -> "score" - - } + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { + append("https://") + append(domain) append("/library") - if (query.isNullOrEmpty()) { - append("?order_item=") - append(order) - append("&order_dir=desc") - append("&filter_by=title") - if (tags != null) { - for (tag in tags) { - append("&genders[]=${tag.key}") + when (filter) { + + is MangaListFilter.Search -> { + append("?title=") + append(filter.query.urlEncoded()) + } + + is MangaListFilter.Advanced -> { + append("?order_item=") + append( + when (filter.sortOrder) { + SortOrder.POPULARITY -> "likes_count" + SortOrder.UPDATED -> "release_date" + SortOrder.NEWEST -> "creation" + SortOrder.ALPHABETICAL -> "alphabetically" + SortOrder.RATING -> "score" + + }, + ) + append("&order_dir=desc") + append("&filter_by=title") + if (filter.tags.isNotEmpty()) { + for (tag in filter.tags) { + append("&genders[]=") + append(tag.key) + } } } - } else { - append("?title=$query") - } - append("&_pg=1") - append("&page=$page") - }.toAbsoluteUrl(domain) + null -> { + append("?order_item=release_date&order_dir=desc&filter_by=title") + } + } + append("&_pg=1&page=") + append(page.toString()) + } val doc = webClient.httpGet(url, headers).parseHtml() val items = doc.body().select("div.element") return items.mapNotNull { item -> @@ -95,6 +101,13 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser( val contents = doc.body().selectFirstOrThrow("section.element-header-content") return manga.copy( description = contents.selectFirst("p.element-description")?.html(), + tags = contents.select("h6 a").mapNotNullToSet { a -> + MangaTag( + key = a.attr("href").substringBefore("&").substringAfterLast("="), + title = a.text(), + source = source, + ) + }, largeCoverUrl = contents.selectFirst(".book-thumbnail")?.attrAsAbsoluteUrlOrNull("src"), state = parseStatus(contents.select("span.book-status").text().orEmpty()), author = contents.selectFirst("h5.card-title")?.attr("title")?.substringAfter(", "), @@ -105,9 +118,9 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser( } else { val chapters = ChaptersListBuilder(10) doc.select(regularChapterListSelector).reversed().forEachIndexed { i, item -> - val chaptername = item.select("div.col-10.text-truncate").text().replace(" ", " ").trim() - val scanelement = item.select("ul.chapter-list > li") - scanelement.forEach { chapters.add(regularChapterFromElement(it, chaptername, i)) } + val chapterName = item.select("div.col-10.text-truncate").text().replace(" ", " ").trim() + val scanElement = item.select("ul.chapter-list > li") + scanElement.forEach { chapters.add(regularChapterFromElement(it, chapterName, i)) } } chapters.toList() }, diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fmreader/FmreaderParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fmreader/FmreaderParser.kt index 3dbceae2..7fa7a03d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fmreader/FmreaderParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fmreader/FmreaderParser.kt @@ -27,7 +27,11 @@ internal abstract class FmreaderParser( SortOrder.ALPHABETICAL, ) - override val isMultipleTagsSupported = false + override val availableStates: Set = EnumSet.of( + MangaState.ONGOING, + MangaState.FINISHED, + MangaState.ABANDONED, + ) protected open val listUrl = "/manga-list.html" protected open val datePattern = "MMMM d, yyyy" @@ -40,58 +44,73 @@ internal abstract class FmreaderParser( @JvmField protected val ongoing: Set = setOf( - "On going", - "Incomplete", - "En curso", + "on going", + "incomplete", + "en curso", ) @JvmField protected val finished: Set = setOf( - "Completed", - "Completado", + "completed", + "completado", ) @JvmField protected val abandoned: Set = hashSetOf( - "Canceled", - "Cancelled", - "Drop", + "canceled", + "cancelled", + "drop", ) - 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(listUrl) append("?page=") append(page.toString()) - when { - !query.isNullOrEmpty() -> { - + when (filter) { + is MangaListFilter.Search -> { append("&name=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } - !tags.isNullOrEmpty() -> { + is MangaListFilter.Advanced -> { + append("&genre=") - append(tag?.key.orEmpty()) + append(filter.tags.joinToString(",") { it.key }) + + + append("&sort=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("views") + SortOrder.UPDATED -> append("last_update") + SortOrder.ALPHABETICAL -> append("name") + else -> append("last_update") + } + + append("&m_status=") + filter.states.oneOrThrowIfMany()?.let { + append( + when (it) { + MangaState.ONGOING -> "2" + MangaState.FINISHED -> "1" + MangaState.ABANDONED -> "3" + else -> "" + }, + ) + } + } - } - append("&sort=") - when (sortOrder) { - SortOrder.POPULARITY -> append("views") - SortOrder.UPDATED -> append("last_update") - SortOrder.ALPHABETICAL -> append("name") - else -> append("last_update") + + null -> append("&sort=last_update") } } - val doc = webClient.httpGet(url).parseHtml() + return parseMangaList(webClient.httpGet(url).parseHtml()) + + } + + protected open fun parseMangaList(doc: Document): List { return doc.select("div.thumb-item-flow").map { div -> val href = div.selectFirstOrThrow("div.series-title a").attrAsRelativeUrl("href") Manga( @@ -99,8 +118,8 @@ internal abstract class FmreaderParser( url = href, publicUrl = href.toAbsoluteUrl(div.host ?: domain), coverUrl = div.selectFirstOrThrow("div.img-in-ratio").attr("data-bg") - ?: div.selectFirstOrThrow("div.img-in-ratio").attr("style").substringAfter("('") - .substringBeforeLast("')"), + ?: div.selectFirstOrThrow("div.img-in-ratio").attr("style").substringAfter("(") + .substringBefore(")"), title = div.selectFirstOrThrow("div.series-title").text().orEmpty(), altTitle = null, rating = RATING_UNKNOWN, @@ -140,7 +159,7 @@ internal abstract class FmreaderParser( val desc = doc.selectFirst(selectDesc)?.html() val stateDiv = doc.selectFirst(selectState) val state = stateDiv?.let { - when (it.text()) { + when (it.text().lowercase()) { in ongoing -> MangaState.ONGOING in finished -> MangaState.FINISHED in abandoned -> MangaState.ABANDONED diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fmreader/en/Manhwa18Com.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fmreader/en/Manhwa18Com.kt index e3a86ef5..676c5564 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fmreader/en/Manhwa18Com.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fmreader/en/Manhwa18Com.kt @@ -20,79 +20,67 @@ internal class Manhwa18Com(context: MangaLoaderContext) : override val selectTag = "div.info-item:contains(Genre) span.info-value a" override val datePattern = "dd/MM/yyyy" override val selectPage = "div#chapter-content img" - override val selectBodyTag = "div.genres-menu a" + override val selectBodyTag = "div.advanced-wrapper .genre_label" - 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("/genre/") - append(tag?.key.orEmpty()) - append("?page=") - append(page.toString()) - append("&sort=") - when (sortOrder) { - SortOrder.POPULARITY -> append("views") - SortOrder.UPDATED -> append("last_update") - SortOrder.ALPHABETICAL -> append("name") - else -> append("last_update") + append("/tim-kiem?page=") + append(page.toString()) + + when (filter) { + is MangaListFilter.Search -> { + append("&q=") + append(filter.query.urlEncoded()) } - } else { - append(listUrl) - append("?page=") - append(page.toString()) - when { - !query.isNullOrEmpty() -> { - append("&q=") - append(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 -> "" + }, + ) } - } - append("&sort=") - when (sortOrder) { - SortOrder.POPULARITY -> append("views") - SortOrder.UPDATED -> append("last_update") - SortOrder.ALPHABETICAL -> append("name") - else -> append("last_update") } + null -> append("&sort=update") } } - val doc = webClient.httpGet(url).parseHtml() - return doc.select("div.thumb-item-flow").map { div -> - val href = div.selectFirstOrThrow("div.series-title a").attrAsRelativeUrl("href") - Manga( - id = generateUid(href), - url = href, - publicUrl = href.toAbsoluteUrl(div.host ?: domain), - coverUrl = div.selectFirstOrThrow("div.img-in-ratio").attr("data-bg") - ?: div.selectFirstOrThrow("div.img-in-ratio").attr("style").substringAfter("('") - .substringBeforeLast("')"), - title = div.selectFirstOrThrow("div.series-title").text().orEmpty(), - altTitle = null, - rating = RATING_UNKNOWN, - tags = emptySet(), - author = null, - state = null, - source = source, - isNsfw = isNsfwSource, - ) - } + return parseMangaList(webClient.httpGet(url).parseHtml()) } override suspend fun getAvailableTags(): Set { val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml() - return doc.select(selectBodyTag).mapNotNullToSet { a -> - val href = a.attr("href").substringAfterLast("/") + return doc.select(selectBodyTag).mapNotNullToSet { label -> + val key = label.attr("data-genre-id") MangaTag( - key = href, - title = a.text(), + key = key, + title = label.selectFirstOrThrow(".gerne-name").text(), source = source, ) } @@ -105,7 +93,7 @@ internal class Manhwa18Com(context: MangaLoaderContext) : val desc = doc.selectFirstOrThrow(selectDesc).html() val stateDiv = doc.selectFirst(selectState) val state = stateDiv?.let { - when (it.text()) { + when (it.text().lowercase()) { in ongoing -> MangaState.ONGOING in finished -> MangaState.FINISHED else -> null diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fmreader/es/OlimpoScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fmreader/es/OlimpoScans.kt index 0b603f13..2ed70663 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fmreader/es/OlimpoScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fmreader/es/OlimpoScans.kt @@ -14,37 +14,60 @@ internal class OlimpoScans(context: MangaLoaderContext) : override val selectAlt = "ul.manga-info li:contains(Otros nombres)" override val selectTag = "ul.manga-info li:contains(Género) a" override val tagPrefix = "lista-de-comics-genero-" + 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) - append(listUrl) - append("?page=") - append(page.toString()) - when { - !query.isNullOrEmpty() -> { + when (filter) { + is MangaListFilter.Search -> { + append(listUrl) + append("?page=") + append(page.toString()) append("&name=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } - !tags.isNullOrEmpty() -> { - append("&genre=") - append(tag?.key.orEmpty()) + is MangaListFilter.Advanced -> { + if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + append("/lista-de-comics-genero-") + append(it.key) + append(".html") + } + } else { + append(listUrl) + append("?page=") + append(page.toString()) + append("&sort=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("views") + SortOrder.UPDATED -> append("last_update") + SortOrder.ALPHABETICAL -> append("name") + else -> append("last_update") + } + } + + append("&m_status=") + filter.states.oneOrThrowIfMany()?.let { + append( + when (it) { + MangaState.ONGOING -> "2" + MangaState.FINISHED -> "1" + MangaState.ABANDONED -> "3" + else -> "" + }, + ) + } + } + + null -> { + append(listUrl) + append("?page=") + append(page.toString()) + append("&sort=last_update") } - } - append("&sort=") - when (sortOrder) { - SortOrder.POPULARITY -> append("views") - SortOrder.UPDATED -> append("last_update") - SortOrder.ALPHABETICAL -> append("name") - else -> append("last_update") } } val doc = webClient.httpGet(url).parseHtml() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fmreader/ja/Klz9.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fmreader/ja/Klz9.kt index 9d363040..4ab0ee63 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fmreader/ja/Klz9.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fmreader/ja/Klz9.kt @@ -3,13 +3,7 @@ package org.koitharu.kotatsu.parsers.site.fmreader.ja import org.jsoup.nodes.Document import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser -import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaChapter -import org.koitharu.kotatsu.parsers.model.MangaPage -import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN -import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.site.fmreader.FmreaderParser import org.koitharu.kotatsu.parsers.util.* import java.text.SimpleDateFormat @@ -27,40 +21,7 @@ internal class Klz9(context: MangaLoaderContext) : override val selectPage = "img" override val selectBodyTag = "div.panel-body a" - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() - val url = buildString { - append("https://") - append(domain) - append("/$listUrl") - append("?page=") - append(page.toString()) - when { - !query.isNullOrEmpty() -> { - - append("&name=") - append(query.urlEncoded()) - } - - !tags.isNullOrEmpty() -> { - append("&genre=") - append(tag?.key.orEmpty()) - } - } - append("&sort=") - when (sortOrder) { - SortOrder.POPULARITY -> append("views") - SortOrder.UPDATED -> append("last_update") - SortOrder.ALPHABETICAL -> append("name") - else -> append("last_update") - } - } - val doc = webClient.httpGet(url).parseHtml() + override fun parseMangaList(doc: Document): List { return doc.select("div.thumb-item-flow").map { div -> val href = "/" + div.selectFirstOrThrow("a").attrAsRelativeUrl("href") Manga( @@ -112,7 +73,6 @@ internal class Klz9(context: MangaLoaderContext) : val docLoad = webClient.httpGet("https://$domain/app/manga/controllers/cont.listImg.php?cid=$cid").parseHtml() return docLoad.select(selectPage).map { img -> val url = img.src()?.toRelativeUrl(domain) ?: img.parseFailed("Image src not found") - MangaPage( id = generateUid(url), url = url, diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/FoolSlideParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/FoolSlideParser.kt index f0b5211c..70bf7e34 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/FoolSlideParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/FoolSlideParser.kt @@ -32,35 +32,63 @@ internal abstract class FoolSlideParser( searchPaginator.firstPage = 1 } - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val doc = if (!query.isNullOrEmpty()) { - val url = buildString { - append("https://$domain/$searchUrl") - if (page > 1) { - return emptyList() - } - } - val q = query.urlEncoded() - webClient.httpPost(url, "search=$q").parseHtml() - } else { - val url = buildString { - append("https://$domain/$listUrl") - // For some sites that don't have enough manga and page 2 links to page 1 - if (!pagination) { + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + val doc = + when (filter) { + is MangaListFilter.Search -> { if (page > 1) { return emptyList() } - } else { - append(page.toString()) + + val url = buildString { + append("https://") + append(domain) + append("/") + append(searchUrl) + } + + webClient.httpPost(url, "search=${filter.query.urlEncoded()}").parseHtml() + } + + is MangaListFilter.Advanced -> { + + val url = buildString { + append("https://") + append(domain) + append("/") + append(listUrl) + // For some sites that don't have enough manga and page 2 links to page 1 + if (!pagination) { + if (page > 1) { + return emptyList() + } + } else { + append(page.toString()) + } + } + webClient.httpGet(url).parseHtml() + + } + + null -> { + val url = buildString { + append("https://") + append(domain) + append("/") + append(listUrl) + if (!pagination) { + if (page > 1) { + return emptyList() + } + } else { + append(page.toString()) + } + } + webClient.httpGet(url).parseHtml() + } } - webClient.httpGet(url).parseHtml() - } + return doc.select("div.list div.group").map { div -> val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") Manga( diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/en/AssortedScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/en/AssortedScans.kt index 957cab69..4dbf8916 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/en/AssortedScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/en/AssortedScans.kt @@ -16,40 +16,30 @@ internal class AssortedScans(context: MangaLoaderContext) : override val pagination = false override val selectInfo = "div.#series-info" - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + if (page > 1) { + return emptyList() + } - val doc = if (!query.isNullOrEmpty()) { - if (page > 1) { - return emptyList() - } - val url = buildString { - append("https://") - append(domain) - append('/') - append(searchUrl) - append("?q=") - append(query.urlEncoded()) - } - webClient.httpGet(url).parseHtml() - } else { - val url = buildString { - append("https://$domain/$listUrl") - // For some sites that don't have enough manga and page 2 links to page 1 - if (!pagination) { - if (page > 1) { - return emptyList() - } - } else { - append(page.toString()) + val url = buildString { + append("https://") + append(domain) + append('/') + when (filter) { + is MangaListFilter.Search -> { + append(searchUrl) + append("?q=") + append(filter.query.urlEncoded()) } + + is MangaListFilter.Advanced -> { + append(listUrl) + } + + null -> append(listUrl) } - webClient.httpGet(url).parseHtml() } + val doc = webClient.httpGet(url).parseHtml() return doc.select("section.series, tr.result").map { div -> val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") Manga( diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/fr/HniScantrad.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/fr/HniScantrad.kt index 0debd791..bd069d28 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/fr/HniScantrad.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/fr/HniScantrad.kt @@ -5,7 +5,7 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.site.foolslide.FoolSlideParser -@MangaSourceParser("HNISCANTRAD", "Hni Scantrad", "fr") +@MangaSourceParser("HNISCANTRAD", "HniScantrad", "fr") internal class HniScantrad(context: MangaLoaderContext) : FoolSlideParser(context, MangaSource.HNISCANTRAD, "hni-scantrad.com") { override val pagination = false diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/it/PowerManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/it/PowerManga.kt index e400f8a8..39f6844d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/it/PowerManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/it/PowerManga.kt @@ -1,12 +1,10 @@ package org.koitharu.kotatsu.parsers.site.foolslide.it - 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.foolslide.FoolSlideParser - @MangaSourceParser("POWERMANGA", "PowerManga", "it") internal class PowerManga(context: MangaLoaderContext) : FoolSlideParser(context, MangaSource.POWERMANGA, "reader.powermanga.org") { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/it/Ramareader.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/it/Ramareader.kt index 8a704477..cc0a6906 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/it/Ramareader.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/it/Ramareader.kt @@ -1,12 +1,10 @@ package org.koitharu.kotatsu.parsers.site.foolslide.it - 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.foolslide.FoolSlideParser - @MangaSourceParser("RAMAREADER", "RamaReader", "it") internal class Ramareader(context: MangaLoaderContext) : FoolSlideParser(context, MangaSource.RAMAREADER, "www.ramareader.it") { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/it/ReadNifteam.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/it/ReadNifteam.kt index 1f567e69..5aee24f1 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/it/ReadNifteam.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/it/ReadNifteam.kt @@ -1,12 +1,10 @@ package org.koitharu.kotatsu.parsers.site.foolslide.it - 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.foolslide.FoolSlideParser - @MangaSourceParser("READNIFTEAM", "ReadNifTeam", "it") internal class ReadNifteam(context: MangaLoaderContext) : FoolSlideParser(context, MangaSource.READNIFTEAM, "read-nifteam.info") { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/BentomangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/BentomangaParser.kt index 7b029cd5..ae86a442 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/BentomangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/BentomangaParser.kt @@ -28,35 +28,55 @@ internal class BentomangaParser(context: MangaLoaderContext) : PagedMangaParser( override val configKeyDomain = ConfigKey.Domain("bentomanga.com", "www.bentomanga.com") + override val availableStates: Set = EnumSet.allOf(MangaState::class.java) + init { paginator.firstPage = 0 searchPaginator.firstPage = 0 } - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = urlBuilder() + .host(domain) .addPathSegment("manga_list") .addQueryParameter("limit", page.toString()) - .addQueryParameter( - "order_by", - when (sortOrder) { - SortOrder.UPDATED -> "update" - SortOrder.POPULARITY -> "views" - SortOrder.RATING -> "top" - SortOrder.NEWEST -> "create" - SortOrder.ALPHABETICAL -> "name" - }, - ) - if (!tags.isNullOrEmpty()) { - url.addQueryParameter("withCategories", tags.joinToString(",") { it.key }) - } - if (!query.isNullOrEmpty()) { - url.addQueryParameter("search", query) + when (filter) { + is MangaListFilter.Search -> { + url.addQueryParameter("search", filter.query) + } + + is MangaListFilter.Advanced -> { + + url.addQueryParameter( + "order_by", + when (filter.sortOrder) { + SortOrder.UPDATED -> "update" + SortOrder.POPULARITY -> "views" + SortOrder.RATING -> "top" + SortOrder.NEWEST -> "create" + SortOrder.ALPHABETICAL -> "name" + }, + ) + + if (filter.tags.isNotEmpty()) { + url.addQueryParameter("withCategories", filter.tags.joinToString(",") { it.key }) + } + + filter.states.oneOrThrowIfMany()?.let { + url.addQueryParameter( + "state", + when (it) { + MangaState.ONGOING -> "1" + MangaState.FINISHED -> "2" + MangaState.PAUSED -> "3" + MangaState.ABANDONED -> "5" + }, + ) + } + + } + + null -> url.addQueryParameter("order_by", "update") } val root = webClient.httpGet(url.build()).parseHtml().requireElementById("mangas_content") return root.select(".manga[data-manga]").map { div -> @@ -109,7 +129,7 @@ internal class BentomangaParser(context: MangaLoaderContext) : PagedMangaParser( "En pause" -> MangaState.PAUSED else -> null }, - author = root.selectFirst(".datas_more-authors-people")?.textOrNull().assertNotNull("author"), + author = root.selectFirst(".datas_more-authors-people")?.textOrNull(), chapters = run { val input = root.selectFirst("input[name=\"limit\"]") ?: return@run parseChapters(root) val max = input.attr("max").toInt() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/FmTeam.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/FmTeam.kt index 73f68a4f..8b81136b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/FmTeam.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/FmTeam.kt @@ -18,51 +18,83 @@ internal class FmTeam(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.FMTEAM, 0) { override val availableSortOrders: Set = EnumSet.of(SortOrder.ALPHABETICAL) + override val availableStates: Set = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED) override val configKeyDomain = ConfigKey.Domain("fmteam.fr") - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { if (page > 1) { return emptyList() } - val jsonManga = if (!query.isNullOrEmpty()) { - //3 letters minimum - webClient.httpGet("https://$domain/api/search/${query.urlEncoded()}").parseJson().getJSONArray("comics") - } else { - webClient.httpGet("https://$domain/api/comics").parseJson().getJSONArray("comics") - } + var foundTag = true + var foundState = true + + val manga = ArrayList() + + when (filter) { + is MangaListFilter.Search -> { + val jsonManga = webClient.httpGet("https://$domain/api/search/${filter.query.urlEncoded()}").parseJson() + .getJSONArray("comics") + for (i in 0 until jsonManga.length()) { + val j = jsonManga.getJSONObject(i) + val href = "/api" + j.getString("url") + manga.add(addManga(href, j)) + } + } + + is MangaListFilter.Advanced -> { + val jsonManga = webClient.httpGet("https://$domain/api/comics").parseJson().getJSONArray("comics") + for (i in 0 until jsonManga.length()) { + + val j = jsonManga.getJSONObject(i) + val href = "/api" + j.getString("url") - val manga = ArrayList(jsonManga.length()) - for (i in 0 until jsonManga.length()) { - val j = jsonManga.getJSONObject(i) - val href = "/api" + j.getString("url") - when { - !tags.isNullOrEmpty() -> { - val a = j.getJSONArray("genres").toString() - var found = true - tags.forEach { - if (!a.contains(it.key, ignoreCase = true)) { - found = false + if (filter.tags.isNotEmpty() && filter.states.isEmpty()) { + val a = j.getJSONArray("genres").toString() + foundTag = false + filter.tags.forEach { + if (a.contains(it.key, ignoreCase = true)) { + foundTag = true + } } } - if (found) { - manga.add( - addManga(href, j), - ) + + if (filter.states.isNotEmpty()) { + val a = j.getString("status") + foundState = false + filter.states.oneOrThrowIfMany()?.let { + if (a.contains( + when (it) { + MangaState.ONGOING -> "En cours" + MangaState.FINISHED -> "Terminé" + else -> "" + }, + ignoreCase = true, + ) + ) { + foundState = true + } + } + + } + + if (foundState && foundTag) { + manga.add(addManga(href, j)) } } + } - else -> { + null -> { + val jsonManga = webClient.httpGet("https://$domain/api/comics").parseJson().getJSONArray("comics") + for (i in 0 until jsonManga.length()) { + val j = jsonManga.getJSONObject(i) + val href = "/api" + j.getString("url") manga.add( addManga(href, j), ) } } } + return manga } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/FuryoSociety.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/FuryoSociety.kt index 2363c994..047e2eaa 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/FuryoSociety.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/FuryoSociety.kt @@ -10,6 +10,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.DateFormat import java.text.SimpleDateFormat import java.util.* @@ -35,21 +36,27 @@ internal class FuryoSociety(context: MangaLoaderContext) : ) } - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + if (page > 1) { + return emptyList() + } + val url = buildString { append("https://") append(domain) - if (page == 1) { - if (sortOrder == SortOrder.ALPHABETICAL) { - append("/mangas") + when (filter) { + is MangaListFilter.Search -> { + throw IllegalArgumentException("Search is not supported by this source") + } + + is MangaListFilter.Advanced -> { + + if (filter.sortOrder == SortOrder.ALPHABETICAL) { + append("/mangas") + } } - } else { - return emptyList() + + null -> {} } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LegacyScansParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LegacyScansParser.kt index cdb87743..3817021d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LegacyScansParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LegacyScansParser.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.parsers.site.fr import okhttp3.Headers +import org.json.JSONObject import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.PagedMangaParser @@ -16,9 +17,7 @@ import java.util.* internal class LegacyScansParser(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.LEGACY_SCANS, 18) { - override val availableSortOrders: Set = EnumSet.of( - SortOrder.POPULARITY, - ) + override val availableSortOrders: Set = EnumSet.of(SortOrder.POPULARITY) override val configKeyDomain = ConfigKey.Domain("legacy-scans.com") @@ -26,86 +25,96 @@ internal class LegacyScansParser(context: MangaLoaderContext) : .add("User-Agent", UserAgents.CHROME_MOBILE) .build() - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val end = page * pageSize val start = end - (pageSize - 1) - val url = if (!query.isNullOrEmpty()) { - if (page > 1) { - return emptyList() - } - buildString { - append("https://api.$domain/misc/home/search?title=") - append(query.urlEncoded()) - } - } else { - buildString { - append("https://api.$domain/misc/comic/search/query?status=&order=&genreNames=") - if (!tags.isNullOrEmpty()) { - for (tag in tags) { - append(tag.key) - append(',') - } + + when (filter) { + is MangaListFilter.Search -> { + if (page > 1) { + return emptyList() } - append("&type=&start=") - append(start) - append("&end=") - append(end) + val url = buildString { + append("https://api.$domain/misc/home/search?title=") + append(filter.query.urlEncoded()) + } + return parseMangaListQuery(webClient.httpGet(url).parseJson()) } - } - val json = webClient.httpGet(url).parseJson() - return if (!query.isNullOrEmpty()) { - json.getJSONArray("results").mapJSON { j -> - val slug = j.getString("slug") - val urlManga = "https://$domain/comics/$slug" - Manga( - id = generateUid(urlManga), - title = j.getString("title"), - altTitle = null, - url = urlManga, - publicUrl = urlManga, - rating = RATING_UNKNOWN, - isNsfw = false, - coverUrl = "", - tags = setOf(), - state = null, - author = null, - source = source, - ) + is MangaListFilter.Advanced -> { + val url = buildString { + append("https://api.") + append(domain) + append("/misc/comic/search/query?status=&order=&genreNames=") + append(filter.tags.joinToString(",") { it.key }) + append("&type=&start=") + append(start) + append("&end=") + append(end) + } + return parseMangaList(webClient.httpGet(url).parseJson()) } - } else { - json.getJSONArray("comics").mapJSON { j -> - val slug = j.getString("slug") - val urlManga = "https://$domain/comics/$slug" - Manga( - id = generateUid(urlManga), - title = j.getString("title"), - altTitle = null, - url = urlManga, - publicUrl = urlManga, - rating = RATING_UNKNOWN, - isNsfw = false, - coverUrl = "https://api.$domain/" + j.getString("cover"), - tags = setOf(), - state = null, - author = null, - source = source, - ) + + null -> { + val url = buildString { + append("https://api.") + append(domain) + append("/misc/comic/search/query?status=&order=&genreNames=&type=&start=") + append(start) + append("&end=") + append(end) + } + return parseMangaList(webClient.httpGet(url).parseJson()) } } + } + + + private fun parseMangaList(json: JSONObject): List { + return json.getJSONArray("comics").mapJSON { j -> + val slug = j.getString("slug") + val urlManga = "https://$domain/comics/$slug" + Manga( + id = generateUid(urlManga), + title = j.getString("title"), + altTitle = null, + url = urlManga, + publicUrl = urlManga, + rating = RATING_UNKNOWN, + isNsfw = false, + coverUrl = "https://api.$domain/" + j.getString("cover"), + tags = setOf(), + state = null, + author = null, + source = source, + ) + } + } + private fun parseMangaListQuery(json: JSONObject): List { + return json.getJSONArray("results").mapJSON { j -> + val slug = j.getString("slug") + val urlManga = "https://$domain/comics/$slug" + Manga( + id = generateUid(urlManga), + title = j.getString("title"), + altTitle = null, + url = urlManga, + publicUrl = urlManga, + rating = RATING_UNKNOWN, + isNsfw = false, + coverUrl = "", + tags = setOf(), + state = null, + author = null, + source = source, + ) + } } override suspend fun getDetails(manga: Manga): Manga { val root = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.FRENCH) - return manga.copy( altTitle = null, tags = root.select("div.serieGenre span").mapNotNullToSet { span -> @@ -156,7 +165,6 @@ internal class LegacyScansParser(context: MangaLoaderContext) : val script = doc.requireElementById("__NUXT_DATA__").data() .substringAfterLast("\"genres\"").substringBeforeLast("\"comics\"") .split("\",\"").drop(1) - return script.mapNotNullToSet { tag -> MangaTag( key = tag.substringBeforeLast("\",{"), diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LireScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LireScan.kt index 6b3ca531..593d4470 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LireScan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LireScan.kt @@ -24,36 +24,50 @@ internal class LireScan(context: MangaLoaderContext) : PagedMangaParser(context, .add("User-Agent", UserAgents.CHROME_MOBILE) .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 doc = - if (!query.isNullOrEmpty()) { // search only works with 4 or more letters - if (page > 1) { - return emptyList() + when (filter) { + is MangaListFilter.Search -> { + if (page > 1) { + return emptyList() + } + val q = filter.query.urlEncoded().replace("%20", "+") + val post = "do=search&subaction=search&search_start=0&full_search=0&result_from=1&story=$q" + webClient.httpPost("https://$domain/index.php?do=search", post).parseHtml() } - val q = query.urlEncoded().replace("%20", "+") - val post = "do=search&subaction=search&search_start=0&full_search=0&result_from=1&story=$q" - webClient.httpPost("https://$domain/index.php?do=search", post).parseHtml() - } else { - val url = buildString { - append("https://") - append(domain) - if (!tags.isNullOrEmpty()) { - append("/manga/") - append(tag?.key.orEmpty()) + + is MangaListFilter.Advanced -> { + val url = buildString { + append("https://") + append(domain) + + filter.tags.oneOrThrowIfMany()?.let { + append("/manga/") + append(it.key) + } + + if (page > 1) { + append("/page/") + append(page) + append('/') + } } - if (page > 1) { - append("/page/") - append(page) - append('/') + webClient.httpGet(url).parseHtml() + } + + null -> { + val url = buildString { + append("https://") + append(domain) + if (page > 1) { + append("/page/") + append(page) + append('/') + } } + webClient.httpGet(url).parseHtml() } - webClient.httpGet(url).parseHtml() } return doc.select("div.sect__content.grid-items div.item-poster").map { div -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LugnicaScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LugnicaScans.kt index 7ad9c279..3545a377 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LugnicaScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LugnicaScans.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.parsers.site.fr import okhttp3.Headers +import org.json.JSONArray import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.PagedMangaParser @@ -21,6 +22,8 @@ internal class LugnicaScans(context: MangaLoaderContext) : PagedMangaParser(cont SortOrder.UPDATED, ) + override val availableStates: Set = EnumSet.allOf(MangaState::class.java) + override val configKeyDomain = ConfigKey.Domain("lugnica-scans.com") override val headers: Headers = Headers.Builder() @@ -43,77 +46,102 @@ internal class LugnicaScans(context: MangaLoaderContext) : PagedMangaParser(cont ) } - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - if (!query.isNullOrEmpty()) { - throw IllegalArgumentException("Search is not supported by this source") - } - if (sortOrder == SortOrder.ALPHABETICAL) { - if (page > 1) { - return emptyList() - } - val url = buildString { - append("https://") - append(domain) - append("/api/get/catalog?page=0&filter=all") - } - val json = webClient.httpGet(url).parseJsonArray() - return json.mapJSON { j -> - val urlManga = "https://$domain/api/get/card/${j.getString("slug")}" - val img = "https://$domain/upload/min_cover/${j.getString("image")}" - Manga( - id = generateUid(urlManga), - title = j.getString("title"), - altTitle = null, - url = urlManga, - publicUrl = urlManga.toAbsoluteUrl(domain), - rating = j.getString("rate").toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN, - isNsfw = false, - coverUrl = img, - tags = setOf(), - state = when (j.getString("status")) { - "0" -> MangaState.ONGOING - "1" -> MangaState.FINISHED - "3" -> MangaState.ABANDONED - else -> null - }, - author = null, - source = source, - ) + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + when (filter) { + is MangaListFilter.Search -> { + throw IllegalArgumentException("Search is not supported by this source") } - } else { - val url = buildString { - append("https://") - append(domain) - append("/api/get/homegrid/") - append(page) + + is MangaListFilter.Advanced -> { + + if (filter.sortOrder == SortOrder.ALPHABETICAL) { + if (page > 1) { + return emptyList() + } + val url = buildString { + append("https://") + append(domain) + append("/api/get/catalog?page=0&filter=") + filter.states.oneOrThrowIfMany()?.let { + when (it) { + MangaState.ONGOING -> append("0") + MangaState.FINISHED -> append("1") + MangaState.PAUSED -> append("4") + MangaState.ABANDONED -> append("3") + } + } + + + } + return parseMangaListAlpha(webClient.httpGet(url).parseJsonArray()) + } else { + val url = buildString { + append("https://") + append(domain) + append("/api/get/homegrid/") + append(page) + } + return parseMangaList(webClient.httpGet(url).parseJsonArray()) + } } - val json = webClient.httpGet(url).parseJsonArray() - return json.mapJSON { j -> - val urlManga = "https://$domain/api/get/card/${j.getString("manga_slug")}" - val img = "https://$domain/upload/min_cover/${j.getString("manga_image")}" - Manga( - id = generateUid(urlManga), - title = j.getString("manga_title"), - altTitle = null, - url = urlManga, - publicUrl = urlManga.toAbsoluteUrl(domain), - rating = j.getString("manga_rate").toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN, - isNsfw = false, - coverUrl = img, - tags = setOf(), - state = null, - author = null, - source = source, - ) + + null -> { + val url = buildString { + append("https://") + append(domain) + append("/api/get/homegrid/") + append(page) + } + return parseMangaList(webClient.httpGet(url).parseJsonArray()) } + } + } + private fun parseMangaList(json: JSONArray): List { + return json.mapJSON { j -> + val urlManga = "https://$domain/api/get/card/${j.getString("manga_slug")}" + val img = "https://$domain/upload/min_cover/${j.getString("manga_image")}" + Manga( + id = generateUid(urlManga), + title = j.getString("manga_title"), + altTitle = null, + url = urlManga, + publicUrl = urlManga.toAbsoluteUrl(domain), + rating = j.getString("manga_rate").toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN, + isNsfw = false, + coverUrl = img, + tags = setOf(), + state = null, + author = null, + source = source, + ) } + } + private fun parseMangaListAlpha(json: JSONArray): List { + return json.mapJSON { j -> + val urlManga = "https://$domain/api/get/card/${j.getString("slug")}" + val img = "https://$domain/upload/min_cover/${j.getString("image")}" + Manga( + id = generateUid(urlManga), + title = j.getString("title"), + altTitle = null, + url = urlManga, + publicUrl = urlManga.toAbsoluteUrl(domain), + rating = j.getString("rate").toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN, + isNsfw = false, + coverUrl = img, + tags = setOf(), + state = when (j.getString("status")) { + "0" -> MangaState.ONGOING + "1" -> MangaState.FINISHED + "3" -> MangaState.ABANDONED + else -> null + }, + author = null, + source = source, + ) + } } override suspend fun getDetails(manga: Manga): Manga { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/ScansMangasMe.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/ScansMangasMe.kt index 15c285c2..1e9e892e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/ScansMangasMe.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/ScansMangasMe.kt @@ -30,46 +30,45 @@ internal class ScansMangasMe(context: MangaLoaderContext) : .add("User-Agent", UserAgents.CHROME_DESKTOP) .build() + override val isMultipleTagsSupported = false - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + if (page > 1) { + return emptyList() + } val url = buildString { append("https://") append(domain) - if (page == 1) { - if (!query.isNullOrEmpty()) { + when (filter) { + is MangaListFilter.Search -> { append("/?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&post_type=manga") + } - } else if (!tags.isNullOrEmpty()) { - append("/genres/") - for (tag in tags) { - append(tag.key) - } - } else { - append("/tous-nos-mangas/") - append("?order=") - when (sortOrder) { - SortOrder.POPULARITY -> append("popular") - SortOrder.UPDATED -> append("update") - SortOrder.ALPHABETICAL -> append("title") - SortOrder.NEWEST -> append("create") - else -> append("update") + is MangaListFilter.Advanced -> { + if (filter.tags.isNotEmpty()) { + append("/genres/") + filter.tags.oneOrThrowIfMany()?.let { + append(it.key) + } + } else { + append("/tous-nos-mangas/?order=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("popular") + SortOrder.UPDATED -> append("update") + SortOrder.ALPHABETICAL -> append("title") + SortOrder.NEWEST -> append("create") + else -> append("update") + } } } - } else { - return emptyList() - } + null -> append("/tous-nos-mangas/?order=update") + } } val doc = webClient.httpGet(url).parseHtml() - return doc.select("div.postbody .bs .bsx").map { div -> val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") Manga( @@ -90,7 +89,6 @@ internal class ScansMangasMe(context: MangaLoaderContext) : } } - override suspend fun getAvailableTags(): Set { val doc = webClient.httpGet("https://$domain/tous-nos-mangas/").parseHtml() return doc.select("ul.genre li").mapNotNullToSet { li -> @@ -104,25 +102,18 @@ internal class ScansMangasMe(context: MangaLoaderContext) : } } - override suspend fun getDetails(manga: Manga): Manga = coroutineScope { val fullUrl = manga.url.toAbsoluteUrl(domain) val doc = webClient.httpGet(fullUrl).parseHtml() - val chaptersDeferred = getChapters(doc) - val desc = doc.selectFirstOrThrow("div.desc").html() - val state = if (doc.select("div.spe span:contains(En cours)").isNullOrEmpty()) { MangaState.FINISHED } else { MangaState.ONGOING } - val alt = doc.body().select("div.infox span.alter").text() - val aut = doc.select("div.spe span")[2].text().replace("Auteur:", "") - manga.copy( tags = doc.select("div.spe span:contains(Genres) a").mapNotNullToSet { a -> MangaTag( @@ -140,7 +131,6 @@ internal class ScansMangasMe(context: MangaLoaderContext) : ) } - private fun getChapters(doc: Document): List { return doc.body().requireElementById("chapter_list").select("li").mapChapters(reversed = true) { i, li -> val a = li.selectFirstOrThrow("a") @@ -161,10 +151,8 @@ internal class ScansMangasMe(context: MangaLoaderContext) : override suspend fun getPages(chapter: MangaChapter): List { val fullUrl = chapter.url.toAbsoluteUrl(domain) val doc = webClient.httpGet(fullUrl).parseHtml() - val script = doc.selectFirstOrThrow("script:containsData(page_image)") val images = JSONArray(script.data().substringAfterLast("var pages = ").substringBefore(';')) - val pages = ArrayList(images.length()) for (i in 0 until images.length()) { val pageTake = images.getJSONObject(i) @@ -177,7 +165,6 @@ internal class ScansMangasMe(context: MangaLoaderContext) : ), ) } - return pages } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/ScantradUnion.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/ScantradUnion.kt index 07f8dcc2..b5c13aff 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/ScantradUnion.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/ScantradUnion.kt @@ -20,49 +20,54 @@ internal class ScantradUnion(context: MangaLoaderContext) : PagedMangaParser(con SortOrder.UPDATED, ) + override val isMultipleTagsSupported = false + override val configKeyDomain = ConfigKey.Domain("scantrad-union.com") override val headers: Headers = Headers.Builder() .add("User-Agent", UserAgents.CHROME_DESKTOP) .build() - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) - when { - !query.isNullOrEmpty() -> { + when (filter) { + is MangaListFilter.Search -> { append("/page/") append(page.toString()) append("/?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } - !tags.isNullOrEmpty() -> { - append("/tag/") - for (tag in tags) { - append(tag.key) - append(',') - } - append("/page/") - append(page.toString()) - } + is MangaListFilter.Advanced -> { + if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + append("/tag/") + append(it.key) + append("/page/") + append(page.toString()) + append("/") + } + } else { + if (filter.sortOrder == SortOrder.ALPHABETICAL) { + append("/manga/page/") + append(page.toString()) + append("/") + } - else -> { - if (sortOrder == SortOrder.ALPHABETICAL) { - append("/manga/") - append("/page/") - append(page.toString()) - } + if (filter.sortOrder == SortOrder.UPDATED && page > 1) { + return emptyList() + } - if (sortOrder == SortOrder.UPDATED) { - append("") } + + } + + null -> { + append("/manga/page/") + append(page.toString()) + append("/") } } } @@ -180,9 +185,8 @@ internal class ScantradUnion(context: MangaLoaderContext) : PagedMangaParser(con val root = body.select(".asp_gochosen")[1] val list = root?.select("option").orEmpty() return list.mapToSet { li -> - MangaTag( - key = li.text(), + key = li.text().lowercase().replace(" ", "-"), title = li.text(), source = source, ) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/GalleryAdultsParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/GalleryAdultsParser.kt index 9def242d..5b69fd57 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/GalleryAdultsParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/GalleryAdultsParser.kt @@ -24,36 +24,42 @@ internal abstract class GalleryAdultsParser( override val configKeyDomain = ConfigKey.Domain(domain) 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()) { - if (tag?.key == "languageKey") { - append("/language") - append(tag.title) - append("/?") - } else { - append("/tag/") - append(tag?.key.orEmpty()) - append("/?") + when (filter) { + is MangaListFilter.Search -> { + append("/search/?q=") + append(filter.query.urlEncoded()) + append("&") + } + + is MangaListFilter.Advanced -> { + if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + if (it.key == "languageKey") { + append("/language") + append(it.title) + append("/?") + } else { + append("/tag/") + append(it.key) + append("/?") + } + } + } else { + append("/?") + } } - } else if (!query.isNullOrEmpty()) { - append("/search/?q=") - append(query.urlEncoded()) - append("&") - } else { - append("/?") + + null -> append("/?") } append("page=") append(page) } + return parseMangaList(webClient.httpGet(url).parseHtml()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/Hentai3.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/Hentai3.kt index 65b653b5..ed39facf 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/Hentai3.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/Hentai3.kt @@ -31,45 +31,58 @@ internal class Hentai3(context: MangaLoaderContext) : "/japanese", ) - override val sortOrders: Set = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY) + override val isMultipleTagsSupported = true - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - if (query.isNullOrEmpty() && tags != null && tags.size > 1) { - return getListPage(page, buildQuery(tags), emptySet(), sortOrder) - } + override val availableSortOrders: Set = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY) + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) - if (!tags.isNullOrEmpty()) { - val tag = tags.single() - if (tag.key == "languageKey") { - append("/language") - append(tag.title) - } else { - append("/tags/") - append(tag.key) + when (filter) { + + is MangaListFilter.Search -> { + append("/search?q=") + append(filter.query.urlEncoded()) + append("&page=") + append(page.toString()) } - append("/") - append(page) - if (sortOrder == SortOrder.POPULARITY) { - append("?sort=popular") + + is MangaListFilter.Advanced -> { + + if (filter.tags.isNotEmpty() && filter.tags.size > 1) { + append("/search?q=") + append(buildQuery(filter.tags)) + if (filter.sortOrder == SortOrder.POPULARITY) { + append("&sort=popular") + } + append("&page=") + append(page.toString()) + } else if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + if (it.key == "languageKey") { + append("/language") + append(it.title) + } else { + append("/tags/") + append(it.key) + } + } + append("/") + append(page.toString()) + if (filter.sortOrder == SortOrder.POPULARITY) { + append("?sort=popular") + } + } else { + append("/") + append(page) + } } - } else if (!query.isNullOrEmpty()) { - append("/search?q=") - append(query.urlEncoded()) - if (sortOrder == SortOrder.POPULARITY) { - append("&sort=popular") + + null -> { + append("/") + append(page) } - append("&page=") - append(page) - } else { - append("/") - append(page) } } return parseMangaList(webClient.httpGet(url).parseHtml()) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEnvy.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEnvy.kt index cb8a199a..95407f77 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEnvy.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEnvy.kt @@ -32,40 +32,45 @@ internal class HentaiEnvy(context: MangaLoaderContext) : "/portuguese", ) - override val sortOrders: Set = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY) + override val availableSortOrders: Set = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY) - 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()) { - if (tag?.key == "languageKey") { - append("/language") - append(tag.title) - append("/?") - } else { - append("/tag/") - append(tag?.key.orEmpty()) - if (sortOrder == SortOrder.POPULARITY) { - append("/popular") + when (filter) { + is MangaListFilter.Search -> { + append("/search/?s_key=") + append(filter.query.urlEncoded()) + append("&") + } + + is MangaListFilter.Advanced -> { + if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + if (it.key == "languageKey") { + append("/language") + append(it.title) + append("/?") + } else { + append("/tag/") + append(it.key) + if (filter.sortOrder == SortOrder.POPULARITY) { + append("/popular") + } + append("/?") + } + } + } else { + append("/?") } - append("/?") } - } else if (!query.isNullOrEmpty()) { - append("/search/?s_key=") - append(query.urlEncoded()) - append("&") - } else { - append("/?") + + null -> append("/?") } append("page=") append(page) + } return parseMangaList(webClient.httpGet(url).parseHtml()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEra.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEra.kt index 8d4628eb..0b83be26 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEra.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEra.kt @@ -25,7 +25,9 @@ internal class HentaiEra(context: MangaLoaderContext) : "/russian", ) - override val sortOrders: Set = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY) + override val availableSortOrders: Set = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY) + + override val isMultipleTagsSupported = true override fun Element.parseTags() = select("a.tag, .gallery_title a").mapToSet { val key = it.attr("href").removeSuffix('/').substringAfterLast('/') @@ -37,46 +39,54 @@ internal class HentaiEra(context: MangaLoaderContext) : ) } - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - if (query.isNullOrEmpty() && tags != null && tags.size > 1) { - return getListPage(page, buildQuery(tags), emptySet(), sortOrder) - } + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) - if (!tags.isNullOrEmpty()) { - val tag = tags.single() - if (tag.key == "languageKey") { - append("/language") - append(tag.title) - } else { - append("/tag/") - append(tag.key) - } - append("/") - if (sortOrder == SortOrder.POPULARITY) { - append("popular/") + when (filter) { + + is MangaListFilter.Search -> { + append("/search/?key=") + append(filter.query.urlEncoded()) + append("&") } - append("?") - } else if (!query.isNullOrEmpty()) { - append("/search/?key=") - if (sortOrder == SortOrder.POPULARITY) { - append(query.replace("<=1&dl=0&pp=0&tr=0", "<=0&dl=0&pp=1&tr=0")) - } else { - append(query) + + is MangaListFilter.Advanced -> { + if (filter.tags.isNotEmpty() && filter.tags.size > 1) { + append("/search/?key=") + if (filter.sortOrder == SortOrder.POPULARITY) { + append(buildQuery(filter.tags).replace("<=1&dl=0&pp=0&tr=0", "<=0&dl=0&pp=1&tr=0")) + } else { + append(buildQuery(filter.tags)) + } + append("&") + } else if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + if (it.key == "languageKey") { + append("/language") + append(it.title) + } else { + append("/tag/") + append(it.key) + } + } + append("/") + + if (filter.sortOrder == SortOrder.POPULARITY) { + append("popular/") + } + append("?") + } else { + append("/?") + } } - append("&") - } else { - append("/?") + + null -> append("/?") } append("page=") - append(page) + append(page.toString()) } + return parseMangaList(webClient.httpGet(url).parseHtml()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiForce.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiForce.kt index dc5a76c7..ed97854f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiForce.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiForce.kt @@ -35,50 +35,60 @@ internal class HentaiForce(context: MangaLoaderContext) : "/vietnamese", ) - override val sortOrders: Set = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY) + override val isMultipleTagsSupported = true + + override val availableSortOrders: Set = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY) override suspend fun getPageUrl(page: MangaPage): String { val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml() return doc.selectFirstOrThrow(idImg).src() ?: doc.parseFailed("Image src not found") } - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - if (query.isNullOrEmpty() && tags != null && tags.size > 1) { - return getListPage(page, buildQuery(tags), emptySet(), sortOrder) - } + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) - if (!tags.isNullOrEmpty()) { - val tag = tags.single() - if (tag.key == "languageKey") { - append("/language") - append(tag.title) - } else { - append("/tag/") - append(tag.key) + when (filter) { + is MangaListFilter.Search -> { + append("/search?q=") + append(filter.query.urlEncoded()) + append("&page=") } - if (sortOrder == SortOrder.POPULARITY) { - append("/popular") - } - append("/") - } else if (!query.isNullOrEmpty()) { - append("/search?q=") - append(query.urlEncoded()) - if (sortOrder == SortOrder.POPULARITY) { - append("&sort=popular") + + is MangaListFilter.Advanced -> { + if (filter.tags.isNotEmpty() && filter.tags.size > 1) { + append("/search?q=") + append(buildQuery(filter.tags)) + if (filter.sortOrder == SortOrder.POPULARITY) { + append("&sort=popular") + } + append("&page=") + } else if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + if (it.key == "languageKey") { + append("/language") + append(it.title) + } else { + append("/tag/") + append(it.key) + } + } + append("/") + + if (filter.sortOrder == SortOrder.POPULARITY) { + append("popular/") + } + append("?") + } else { + append("/page/") + } } - append("&page=") - } else { - append("/page/") + + null -> append("/page/") } - append(page) + append(page.toString()) } + return parseMangaList(webClient.httpGet(url).parseHtml()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiFox.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiFox.kt index 8bcde87c..02c108c0 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiFox.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiFox.kt @@ -32,59 +32,83 @@ internal class HentaiFox(context: MangaLoaderContext) : "/vietnamese", ) - override val sortOrders: Set = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY) + override val isMultipleTagsSupported = true - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - if (query.isNullOrEmpty() && tags != null && tags.size > 1) { - return getListPage(page, buildQuery(tags), emptySet(), sortOrder) - } + override val availableSortOrders: Set = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY) + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) - if (!tags.isNullOrEmpty()) { - val tag = tags.single() - if (tag.key == "languageKey") { - append("/language") - append(tag.title) - } else { - append("/tag/") - append(tag.key) - } - if (sortOrder == SortOrder.POPULARITY) { - append("/popular") + when (filter) { + is MangaListFilter.Search -> { + append("/search/?q=") + append(filter.query.urlEncoded()) + if (page > 1) { + append("&page=") + append(page.toString()) + } } - if (page > 1) { - append("/pag/") - append(page) - append("/") - } - } else if (!query.isNullOrEmpty()) { - append("/search/?q=") - append(query.urlEncoded()) - if (sortOrder == SortOrder.POPULARITY) { - append("&sort=popular") - } - if (page > 1) { - append("&page=") - append(page) + + is MangaListFilter.Advanced -> { + if (filter.tags.isNotEmpty() && filter.tags.size > 1) { + append("/search/?q=") + append(buildQuery(filter.tags)) + if (page > 1) { + append("&page=") + append(page.toString()) + } + + if (filter.sortOrder == SortOrder.POPULARITY) { + append("&sort=popular") + } + } else if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + if (it.key == "languageKey") { + append("/language") + append(it.title) + } else { + append("/tag/") + append(it.key) + } + } + append("/") + if (filter.sortOrder == SortOrder.POPULARITY) { + append("popular/") + } + + if (page > 1) { + append("/pag/") + append(page.toString()) + append("/") + } + } else { + if (page > 2) { + append("/pag/") + append(page.toString()) + append("/") + } else if (page > 1) { + append("/page/") + append(page.toString()) + append("/") + } + } } - } else { - if (page > 2) { - append("/pag/") - append(page) - append("/") - } else if (page > 1) { - append("/page/") - append(page) - append("/") + + null -> { + if (page > 2) { + append("/pag/") + append(page.toString()) + append("/") + } else if (page > 1) { + append("/page/") + append(page.toString()) + append("/") + } } } } + return parseMangaList(webClient.httpGet(url).parseHtml()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiParser.kt index 078d5eaf..c7ecb9ab 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiParser.kt @@ -27,50 +27,60 @@ internal class NHentaiParser(context: MangaLoaderContext) : "/chinese", ) - override val sortOrders: Set = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY) + override val availableSortOrders: Set = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY) + + override val isMultipleTagsSupported = true + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - if (query.isNullOrEmpty() && tags != null && tags.size > 1) { - return getListPage(page, buildQuery(tags), emptySet(), sortOrder) - } val url = buildString { append("https://") append(domain) - if (!tags.isNullOrEmpty()) { - val tag = tags.single() - if (tag.key == "languageKey") { - append("/language") - append(tag.title) - } else { - append("/tag/") - append(tag.key) - } - append("/") - if (sortOrder == SortOrder.POPULARITY) { - append("popular") - } - append("?") - } else if (!query.isNullOrEmpty()) { - append("/search/?q=") - append(query.urlEncoded()) - if (sortOrder == SortOrder.POPULARITY) { - append("&sort=popular") + when (filter) { + + is MangaListFilter.Search -> { + append("/search/?q=pages:>0 ") + append(filter.query.urlEncoded()) + append("&") } - append("&") - } else { - if (sortOrder == SortOrder.POPULARITY) { - append("/?sort=popular&") - } else { - append("/?") + + is MangaListFilter.Advanced -> { + if (filter.tags.isNotEmpty() && filter.tags.size > 1) { + append("/search/?q=") + append(buildQuery(filter.tags)) + if (filter.sortOrder == SortOrder.POPULARITY) { + append("&sort=popular") + } + append("&") + } else if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + if (it.key == "languageKey") { + append("/language") + append(it.title) + } else { + append("/tag/") + append(it.key) + } + } + append("/") + if (filter.sortOrder == SortOrder.POPULARITY) { + append("popular/") + } + + append("?") + } else { + if (filter.sortOrder == SortOrder.POPULARITY) { + append("/?sort=popular&") + } else { + append("/?") + } + } } + + null -> append("/?") } append("page=") - append(page) + append(page.toString()) } return parseMangaList(webClient.httpGet(url).parseHtml()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiUk.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiUk.kt index 0a8b8403..cc434d16 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiUk.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiUk.kt @@ -33,33 +33,39 @@ internal class NHentaiUk(context: MangaLoaderContext) : "/turkish", ) - 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()) { - if (tag?.key == "languageKey") { - append("/language") - append(tag.title) - append("/?p=") - } else { - append("/tag/") - append(tag?.key) - append("/?p=") + when (filter) { + + is MangaListFilter.Search -> { + throw IllegalArgumentException("Search is not supported by this source") + } + + is MangaListFilter.Advanced -> { + if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + if (it.key == "languageKey") { + append("/language") + append(it.title) + } else { + append("/tag/") + append(it.key) + } + } + append("/?p=") + } else { + append("/home?p=") + } } - } else if (!query.isNullOrEmpty()) { - throw IllegalArgumentException("Search is not supported by this source") - } else { - append("/home?p=") + + null -> append("/?") } - append(page) + append(page.toString()) } + return parseMangaList(webClient.httpGet(url).parseHtml()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/HeanCms.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/HeanCms.kt index e196f8d9..70bea32a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/HeanCms.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/HeanCms.kt @@ -27,6 +27,8 @@ internal abstract class HeanCms( SortOrder.POPULARITY, ) + override val availableStates: Set = EnumSet.allOf(MangaState::class.java) + override val headers: Headers = Headers.Builder() .add("User-Agent", UserAgents.CHROME_DESKTOP) .build() @@ -34,50 +36,54 @@ internal abstract class HeanCms( protected open val pathManga = "series" //For some sources, you need to send a json. For the moment, this part only works in Get. ( ex source need json gloriousscan.com , omegascans.org ) - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - - var firstTag = false + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://api.") append(domain) append("/query?query_string=") + when (filter) { + is MangaListFilter.Search -> { + append(filter.query.urlEncoded()) + } - if (!query.isNullOrEmpty()) { - append(query.urlEncoded()) - } + is MangaListFilter.Advanced -> { - append("&series_status=All&order=desc&orderBy=") - when (sortOrder) { - SortOrder.POPULARITY -> append("total_views") - SortOrder.UPDATED -> append("latest") - SortOrder.NEWEST -> append("created_at") - SortOrder.ALPHABETICAL -> append("title") - else -> append("latest") - } + filter.states.oneOrThrowIfMany()?.let { + append("&series_status=") + append( + when (it) { + MangaState.ONGOING -> "Ongoing" + MangaState.FINISHED -> "Completed" + MangaState.ABANDONED -> "Dropped" + MangaState.PAUSED -> "Hiatus" + }, + ) - append("&series_type=Comic&page=") - append(page) - append("&perPage=12&tags_ids=") - append("[".urlEncoded()) - if (!tags.isNullOrEmpty()) { - for (tag in tags) { - // Just to make it fit [1,2,44] ect - if (!firstTag) { - firstTag = true - } else { - append(",") } - append(tag.key) + append("&order=desc") + append("&orderBy=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("total_views") + SortOrder.UPDATED -> append("latest") + SortOrder.NEWEST -> append("created_at") + SortOrder.ALPHABETICAL -> append("title") + else -> append("latest") + } + append("&series_type=Comic&perPage=12") + append("&tags_ids=") + append("[".urlEncoded()) + append(filter.tags.joinToString(",") { it.key }) + append("]".urlEncoded()) + } + + null -> {} } - append("]".urlEncoded()) + append("&page=") + append(page.toString()) } val json = webClient.httpGet(url).parseJson() + return json.getJSONArray("data").mapJSON { j -> val slug = j.getString("series_slug") val urlManga = "https://$domain/$pathManga/$slug" @@ -107,8 +113,10 @@ internal abstract class HeanCms( source = source, ) } + } + protected open val datePattern = "yyyy-MM-dd" override suspend fun getDetails(manga: Manga): Manga { val root = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/es/YugenMangasEs.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/es/YugenMangasEs.kt index e1700bbb..64b7349a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/es/YugenMangasEs.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/es/YugenMangasEs.kt @@ -2,19 +2,10 @@ package org.koitharu.kotatsu.parsers.site.heancms.es import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser -import org.koitharu.kotatsu.parsers.model.ContentType -import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.model.MangaState -import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN -import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.site.heancms.HeanCms -import org.koitharu.kotatsu.parsers.util.domain -import org.koitharu.kotatsu.parsers.util.generateUid import org.koitharu.kotatsu.parsers.util.json.mapJSON -import org.koitharu.kotatsu.parsers.util.parseJson -import org.koitharu.kotatsu.parsers.util.urlEncoded +import org.koitharu.kotatsu.parsers.util.* @MangaSourceParser("YUGEN_MANGAS_ES", "YugenMangas.lat", "es", ContentType.HENTAI) internal class YugenMangasEs(context: MangaLoaderContext) : @@ -22,46 +13,55 @@ internal class YugenMangasEs(context: MangaLoaderContext) : private val domainAlt = "yugenmangas.net" - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - var firstTag = false + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://api.") append(domainAlt) append("/query?query_string=") - if (!query.isNullOrEmpty()) { - append(query.urlEncoded()) - } - append("&series_status=All&order=desc&orderBy=") - when (sortOrder) { - SortOrder.POPULARITY -> append("total_views") - SortOrder.UPDATED -> append("latest") - SortOrder.NEWEST -> append("created_at") - SortOrder.ALPHABETICAL -> append("title") - else -> append("latest") - } - append("&series_type=Comic&page=") - append(page) - append("&perPage=12&tags_ids=") - append("[".urlEncoded()) - if (!tags.isNullOrEmpty()) { - for (tag in tags) { - // Just to make it fit [1,2,44] ect - if (!firstTag) { - firstTag = true - } else { - append(",") + when (filter) { + is MangaListFilter.Search -> { + append(filter.query.urlEncoded()) + } + + is MangaListFilter.Advanced -> { + + filter.states.oneOrThrowIfMany()?.let { + append("&series_status=") + append( + when (it) { + MangaState.ONGOING -> "Ongoing" + MangaState.FINISHED -> "Completed" + MangaState.ABANDONED -> "Dropped" + MangaState.PAUSED -> "Hiatus" + }, + ) + } - append(tag.key) + append("&order=desc") + append("&orderBy=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("total_views") + SortOrder.UPDATED -> append("latest") + SortOrder.NEWEST -> append("created_at") + SortOrder.ALPHABETICAL -> append("title") + else -> append("latest") + } + append("&series_type=Comic&perPage=12") + append("&tags_ids=") + append("[".urlEncoded()) + append(filter.tags.joinToString(",") { it.key }) + append("]".urlEncoded()) } + + null -> {} } - append("]".urlEncoded()) + + append("&page=") + append(page.toString()) } + val json = webClient.httpGet(url).parseJson() + return json.getJSONArray("data").mapJSON { j -> val slug = j.getString("series_slug") val urlManga = "https://$domain/series/$slug" diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancmsalt/HeanCmsAlt.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancmsalt/HeanCmsAlt.kt index a6768be4..588e23f3 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancmsalt/HeanCmsAlt.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancmsalt/HeanCmsAlt.kt @@ -5,6 +5,7 @@ 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 java.lang.IllegalArgumentException import java.text.DateFormat import java.text.SimpleDateFormat import java.util.* @@ -33,23 +34,24 @@ internal abstract class HeanCmsAlt( protected open val selectManga = "div.grid.grid-cols-2 div:not([class]):contains(M)" protected open val selectMangaTitle = "h5" - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - // No search or tag - if (!query.isNullOrEmpty()) { - return emptyList() - } + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) append(listUrl) + when (filter) { + is MangaListFilter.Search -> { + throw IllegalArgumentException("Search is not supported by this source") + } + + is MangaListFilter.Advanced -> { + } + + null -> {} + } if (page > 1) { append("?page=") - append(page) + append(page.toString()) } } val doc = webClient.httpGet(url).parseHtml() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/DoujinDesuParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/DoujinDesuParser.kt index babd84fc..69ef6220 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/DoujinDesuParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/DoujinDesuParser.kt @@ -18,6 +18,78 @@ class DoujinDesuParser(context: MangaLoaderContext) : PagedMangaParser(context, override val availableSortOrders: Set get() = EnumSet.of(SortOrder.UPDATED, SortOrder.NEWEST, SortOrder.ALPHABETICAL, SortOrder.POPULARITY) + override val availableStates: Set = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED) + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + val url = urlBuilder().apply { + addPathSegment("manga") + addPathSegment("page") + addPathSegment("$page/") + + when (filter) { + is MangaListFilter.Search -> { + addQueryParameter("title", filter.query) + } + + is MangaListFilter.Advanced -> { + addQueryParameter("title", "") + addQueryParameter( + "order", + when (filter.sortOrder) { + SortOrder.UPDATED -> "update" + SortOrder.POPULARITY -> "popular" + SortOrder.ALPHABETICAL -> "title" + SortOrder.NEWEST -> "latest" + else -> "latest" + }, + ) + + filter.tags.forEach { + addEncodedQueryParameter("genre[]".urlEncoded(), it.key.urlEncoded()) + } + + filter.states.oneOrThrowIfMany()?.let { + addEncodedQueryParameter( + "statusx", + when (it) { + MangaState.ONGOING -> "Publishing" + MangaState.FINISHED -> "Finished" + else -> "" + }, + ) + } + } + + null -> addQueryParameter("order", "update") + } + }.build() + + return webClient.httpGet(url).parseHtml() + .requireElementById("archives") + .selectFirstOrThrow("div.entries") + .select(".entry") + .map { + val titleTag = it.selectFirstOrThrow(".metadata > a") + val relativeUrl = titleTag.attrAsRelativeUrl("href") + Manga( + id = generateUid(relativeUrl), + title = titleTag.attr("title"), + altTitle = null, + url = relativeUrl, + publicUrl = relativeUrl.toAbsoluteUrl(domain), + rating = RATING_UNKNOWN, + isNsfw = true, + coverUrl = it.selectFirst(".thumbnail > img")?.src().orEmpty(), + tags = emptySet(), + state = null, + author = null, + largeCoverUrl = null, + description = null, + source = source, + ) + } + } + override suspend fun getDetails(manga: Manga): Manga { val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml().selectFirstOrThrow("#archive") val chapterDateFormat = SimpleDateFormat("EEEE, dd MMMM yyyy", sourceLocale) @@ -58,56 +130,6 @@ class DoujinDesuParser(context: MangaLoaderContext) : PagedMangaParser(context, ) } - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val url = urlBuilder().apply { - addPathSegment("manga") - addPathSegment("page") - addPathSegment("$page/") - val order = when (sortOrder) { - SortOrder.UPDATED -> "update" - SortOrder.POPULARITY -> "popular" - SortOrder.ALPHABETICAL -> "title" - SortOrder.NEWEST -> "latest" - else -> throw IllegalArgumentException("Sort order not supported") - } - addQueryParameter("order", order) - addQueryParameter("title", query.orEmpty()) - tags?.forEach { - addEncodedQueryParameter("genre[]".urlEncoded(), it.key.urlEncoded()) - } - }.build() - - return webClient.httpGet(url).parseHtml() - .requireElementById("archives") - .selectFirstOrThrow("div.entries") - .select(".entry") - .map { - val titleTag = it.selectFirstOrThrow(".metadata > a") - val relativeUrl = titleTag.attrAsRelativeUrl("href") - Manga( - id = generateUid(relativeUrl), - title = titleTag.attr("title"), - altTitle = null, - url = relativeUrl, - publicUrl = relativeUrl.toAbsoluteUrl(domain), - rating = RATING_UNKNOWN, - isNsfw = true, - coverUrl = it.selectFirst(".thumbnail > img")?.src().orEmpty(), - tags = emptySet(), - state = null, - author = null, - largeCoverUrl = null, - description = null, - source = source, - ) - } - } - override suspend fun getPages(chapter: MangaChapter): List { val id = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml() .requireElementById("reader") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/NicovideoSeigaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/NicovideoSeigaParser.kt index 17cff2ea..06eafe33 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/NicovideoSeigaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/NicovideoSeigaParser.kt @@ -39,26 +39,37 @@ class NicovideoSeigaParser(context: MangaLoaderContext) : SortOrder.POPULARITY, ) + override val isMultipleTagsSupported = false + override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("nicovideo.jp") @InternalParsersApi - override suspend fun getList( - offset: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { + override suspend fun getList(offset: Int, filter: MangaListFilter?): List { val page = (offset / 20f).toIntUp().inc() val domain = getDomain("seiga") - val url = when { - !query.isNullOrEmpty() -> return if (offset == 0) getSearchList(query, page) else emptyList() - tags.isNullOrEmpty() -> "https://$domain/manga/list?page=$page&sort=${getSortKey(sortOrder)}" - tags.size == 1 -> "https://$domain/manga/list?category=${tags.first().key}&page=$page" + - "&sort=${getSortKey(sortOrder)}" - - tags.size > 1 -> throw IllegalArgumentException("This source supports only 1 category") - else -> "https://$domain/manga/list?page=$page&sort=${getSortKey(sortOrder)}" - } + val url = + when (filter) { + is MangaListFilter.Search -> { + return if (offset == 0) getSearchList(filter.query, page) else emptyList() + } + + is MangaListFilter.Advanced -> { + + + if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany().let { + "https://$domain/manga/list?category=${it?.key}&page=$page&sort=${getSortKey(filter.sortOrder)}" + } + + } else { + "https://$domain/manga/list?page=$page&sort=${getSortKey(filter.sortOrder)}" + } + + } + + null -> "https://$domain/manga/list?page=$page" + } + val doc = webClient.httpGet(url).parseHtml() val comicList = doc.body().select("#comic_list > ul > li") ?: doc.parseFailed("Container not found") val items = comicList.select("div > .description > div > div") @@ -145,12 +156,12 @@ class NicovideoSeigaParser(context: MangaLoaderContext) : override suspend fun getAvailableTags(): Set { val doc = webClient.httpGet("https://${getDomain("seiga")}/manga/list").parseHtml() - val root = doc.body().selectOrThrow("#mg_category_list > ul > li") + val root = doc.body().selectOrThrow("#mg_category_list > ul > li").drop(1) return root.mapToSet { li -> val a = li.selectFirstOrThrow("a") MangaTag( title = a.text(), - key = a.attrAsRelativeUrlOrNull("href").orEmpty(), + key = a.attrAsRelativeUrl("href").substringAfter("category=").substringBefore("&"), source = source, ) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/likemanga/LikeMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/likemanga/LikeMangaParser.kt index 10f3df59..8c359f1a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/likemanga/LikeMangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/likemanga/LikeMangaParser.kt @@ -23,44 +23,75 @@ internal abstract class LikeMangaParser( override val availableSortOrders: Set = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.NEWEST) + + override val availableStates: Set = + EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED) + override val configKeyDomain = ConfigKey.Domain(domain) + 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) - append("/?act=search&f") - append("[sortby]".urlEncoded()) - append("=") - when (sortOrder) { - SortOrder.POPULARITY -> append("hot") - SortOrder.UPDATED -> append("lastest-chap") - SortOrder.NEWEST -> append("lastest-manga") - else -> append("lastest-chap") + append("/?act=search") + + when (filter) { + is MangaListFilter.Search -> { + append("&f") + append("[keyword]".urlEncoded()) + append("=") + append(filter.query.urlEncoded()) + } + + is MangaListFilter.Advanced -> { + append("&f") + append("[sortby]".urlEncoded()) + append("=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("hot") + SortOrder.UPDATED -> append("lastest-chap") + SortOrder.NEWEST -> append("lastest-manga") + else -> append("lastest-chap") + } + + if (filter.tags.isNotEmpty()) { + append("&f") + append("[genres]".urlEncoded()) + append("=") + filter.tags.oneOrThrowIfMany()?.let { + append(it.key) + } + } + + filter.states.oneOrThrowIfMany()?.let { + append("&f") + append("[status]".urlEncoded()) + append("=") + append( + when (it) { + MangaState.ONGOING -> "in-process" + MangaState.FINISHED -> "complete" + MangaState.PAUSED -> "pause" + else -> "all" + }, + ) + } + } + + null -> { + append("&f") + append("[sortby]".urlEncoded()) + append("=lastest-chap") + } } + if (page > 1) { append("&pageNum=") append(page) } - if (!tags.isNullOrEmpty()) { - append("&f") - append("[genres]".urlEncoded()) - append("=") - append(tag?.key.orEmpty()) - } - if (!query.isNullOrEmpty()) { - append("&f") - append("[keyword]".urlEncoded()) - append("=") - append(query.urlEncoded()) - } + } val doc = webClient.httpGet(url).parseHtml() return doc.select("div.card-body div.video").map { div -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt index 1efb0be5..405afa56 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt @@ -33,6 +33,8 @@ internal abstract class MadaraParser( SortOrder.RATING, ) + override val availableStates: Set = EnumSet.allOf(MangaState::class.java) + protected open val tagPrefix = "manga-genre/" protected open val datePattern = "MMMM d, yyyy" protected open val stylePage = "?style=list" @@ -155,67 +157,123 @@ internal abstract class MadaraParser( // can be changed to retrieve tags see getTags protected open val listUrl = "manga/" - 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 doc = if (withoutAjax) { + val pages = page + 1 + val url = buildString { append("https://") append(domain) - val pages = page + 1 - if (!tags.isNullOrEmpty()) { - append("/$tagPrefix") - append(tag?.key.orEmpty()) - if (pages > 1) { - append("/page/") - append(pages.toString()) + + when (filter) { + + is MangaListFilter.Search -> { + if (pages > 1) { + append("/page/") + append(pages.toString()) + } + append("/?s=") + append(filter.query.urlEncoded()) + append("&post_type=wp-manga") } - append("?") - } else { - append("/page/") - append(pages) - append("/?s=") - if (!query.isNullOrEmpty()) { - append(query.urlEncoded()) + + is MangaListFilter.Advanced -> { + + if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + append("/$tagPrefix") + append(it.key) + if (pages > 1) { + append("/page/") + append(pages.toString()) + } + append("/?") + } + } else { + + if (pages > 1) { + append("/page/") + append(pages.toString()) + } + append("/?s=&post_type=wp-manga") + filter.states.forEach { + append("&status[]=") + when (it) { + MangaState.ONGOING -> append("on-going") + MangaState.FINISHED -> append("end") + MangaState.ABANDONED -> append("canceled") + MangaState.PAUSED -> append("on-hold") + } + } + append("&") + } + + append("m_orderby=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("views") + SortOrder.UPDATED -> append("latest") + SortOrder.NEWEST -> append("new-manga") + SortOrder.ALPHABETICAL -> append("alphabet") + SortOrder.RATING -> append("rating") + } + } + + null -> { + append("?s&post_type=wp-manga&m_orderby=latest") } - append("&post_type=wp-manga&") - /// &status[]= ( on-going - end - canceled - on-hold - upcoming ) - } - append("m_orderby=") - when (sortOrder) { - SortOrder.POPULARITY -> append("views") - SortOrder.UPDATED -> append("latest") - SortOrder.NEWEST -> append("new-manga") - SortOrder.ALPHABETICAL -> append("alphabet") - SortOrder.RATING -> append("rating") } } webClient.httpGet(url).parseHtml() } else { - val payload = if (sortOrder == SortOrder.RATING) { + val payload = if (filter?.sortOrder == SortOrder.RATING) { createRequestTemplate(ratingRequest) } else { createRequestTemplate(defaultRequest) } - when (sortOrder) { - SortOrder.POPULARITY -> payload["vars[meta_key]"] = "_wp_manga_views" - SortOrder.UPDATED -> payload["vars[meta_key]"] = "_latest_update" - SortOrder.NEWEST -> payload["vars[meta_key]"] = "" - SortOrder.ALPHABETICAL -> { - payload["vars[orderby]"] = "post_title" - payload["vars[order]"] = "ASC" + + payload["page"] = page.toString() + + when (filter) { + + is MangaListFilter.Search -> { + payload["vars[s]"] = filter.query.urlEncoded() } - SortOrder.RATING -> {} + is MangaListFilter.Advanced -> { + + filter.tags.oneOrThrowIfMany()?.let { + payload["vars[wp-manga-genre]"] = it.key + } + + when (filter.sortOrder) { + SortOrder.POPULARITY -> payload["vars[meta_key]"] = "_wp_manga_views" + SortOrder.UPDATED -> payload["vars[meta_key]"] = "_latest_update" + SortOrder.NEWEST -> payload["vars[meta_key]"] = "" + SortOrder.ALPHABETICAL -> { + payload["vars[orderby]"] = "post_title" + payload["vars[order]"] = "ASC" + } + + SortOrder.RATING -> {} + } + + filter.states.forEach { + payload["vars[meta_query][0][0][value][]"] = + when (it) { + MangaState.ONGOING -> "on-going" + MangaState.FINISHED -> "end" + MangaState.ABANDONED -> "canceled" + MangaState.PAUSED -> "on-hold" + } + } + } + + null -> { + payload["vars[meta_key]"] = "_latest_update" + } } - payload["page"] = page.toString() - payload["vars[wp-manga-genre]"] = tag?.key.orEmpty() - payload["vars[s]"] = query?.urlEncoded().orEmpty() - /// payload["vars[meta_query][0][0][value][]"] = ( on-going - end - canceled - on-hold - upcoming ) + webClient.httpPost( "https://$domain/wp-admin/admin-ajax.php", payload, diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/all/Manga18Fx.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/all/Manga18Fx.kt index 5547d042..60451400 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/all/Manga18Fx.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/all/Manga18Fx.kt @@ -19,39 +19,49 @@ internal class Manga18Fx(context: MangaLoaderContext) : override val selectDate = "span.chapter-time" override val selectChapter = "li.a-h" override val selectBodyPage = "div.read-content" - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() + + init { + paginator.firstPage = 1 + searchPaginator.firstPage = 1 + } + + override val availableStates: Set get() = emptySet() + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + val url = buildString { append("https://") append(domain) - val pages = page + 1 - when { - !query.isNullOrEmpty() -> { - + when (filter) { + is MangaListFilter.Search -> { append("/search?q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") - append(pages) + append(page.toString()) } - !tags.isNullOrEmpty() -> { - append("/$tagPrefix") - append(tag?.key.orEmpty()) - if (pages > 1) { - append("/") - append(pages) + is MangaListFilter.Advanced -> { + + val tag = filter.tags.oneOrThrowIfMany() + if (filter.tags.isNotEmpty()) { + append("/$tagPrefix") + append(tag?.key.orEmpty()) + if (page > 1) { + append("/") + append(page.toString()) + } + } else { + if (page > 1) { + append("/page/") + append(page) + } } } - else -> { - if (pages > 1) { + null -> { + if (page > 1) { append("/page/") - append(pages) + append(page) } } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/all/Manhwa18Cc.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/all/Manhwa18Cc.kt index 83274803..8e5ca159 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/all/Manhwa18Cc.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/all/Manhwa18Cc.kt @@ -26,48 +26,48 @@ internal class Manhwa18Cc(context: MangaLoaderContext) : searchPaginator.firstPage = 1 } - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() + override val availableStates: Set get() = emptySet() + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) - when { - !query.isNullOrEmpty() -> { + when (filter) { + + is MangaListFilter.Search -> { append("/search?q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page.toString()) } - !tags.isNullOrEmpty() -> { - append("/$tagPrefix") - append(tag?.key.orEmpty()) + is MangaListFilter.Advanced -> { + + val tag = filter.tags.oneOrThrowIfMany() + if (filter.tags.isNotEmpty()) { + append("/$tagPrefix") + append(tag?.key.orEmpty()) + } else { + append("/$listUrl") + } + if (page > 1) { append(page.toString()) } - append("?") - } - else -> { - append("/$listUrl") - if (page > 1) { - append(page) + append("?orderby=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("trending") + SortOrder.UPDATED -> append("latest") + SortOrder.ALPHABETICAL -> append("alphabet") + SortOrder.RATING -> append("rating") + else -> append("latest") } - append("?") } - } - append("m_orderby=") - when (sortOrder) { - SortOrder.POPULARITY -> append("trending") - SortOrder.UPDATED -> append("latest") - SortOrder.ALPHABETICAL -> append("alphabet") - SortOrder.RATING -> append("rating") - else -> append("latest") + + null -> { + append("?s&post_type=wp-manga&m_orderby=latest") + } } } val doc = webClient.httpGet(url).parseHtml() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Hentai4Free.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Hentai4Free.kt index 0a0e3334..fdca0963 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Hentai4Free.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Hentai4Free.kt @@ -5,6 +5,7 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaTag @@ -20,48 +21,48 @@ internal class Hentai4Free(context: MangaLoaderContext) : override val listUrl = "" override val withoutAjax = true override val datePattern = "MMMM dd, yyyy" + override val selectGenre = "div.tags-content a" + override val availableStates: Set get() = emptySet() - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - - val tag = tags.oneOrThrowIfMany() + init { + paginator.firstPage = 1 + searchPaginator.firstPage = 1 + } + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) - val pages = page + 1 - when { - !query.isNullOrEmpty() -> { + when (filter) { + is MangaListFilter.Search -> { append("/page/") - append(pages.toString()) + append(page.toString()) append("/?s=") - append(query.urlEncoded()) - append("&post_type=wp-manga&") + append(filter.query.urlEncoded()) + append("&post_type=wp-manga") } - !tags.isNullOrEmpty() -> { - append("/$tagPrefix") - append(tag?.key.orEmpty()) - append("/") - if (pages > 1) { - append("page/") - append(pages.toString()) - } - } + is MangaListFilter.Advanced -> { - else -> { - - if (pages > 1) { - append("/page/") - append(pages.toString()) + val tag = filter.tags.oneOrThrowIfMany() + if (filter.tags.isNotEmpty()) { + append("/$tagPrefix") + append(tag?.key.orEmpty()) + append("/") + if (page > 1) { + append("page/") + append(page.toString()) + } + } else { + if (page > 1) { + append("/page/") + append(page.toString()) + } } + append("/?m_orderby=") - when (sortOrder) { + when (filter.sortOrder) { SortOrder.POPULARITY -> append("views") SortOrder.UPDATED -> append("latest") SortOrder.NEWEST -> append("new-manga") @@ -69,9 +70,17 @@ internal class Hentai4Free(context: MangaLoaderContext) : SortOrder.RATING -> append("rating") } } - } + null -> { + if (page > 1) { + append("/page/") + append(page.toString()) + } + append("/?m_orderby=latest") + } + } } + val doc = webClient.httpGet(url).parseHtml() return doc.select("div.row.c-tabs-item__content").ifEmpty { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/InstaManhwa.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/InstaManhwa.kt index 1febb799..cde6e581 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/InstaManhwa.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/InstaManhwa.kt @@ -23,46 +23,48 @@ internal class InstaManhwa(context: MangaLoaderContext) : SortOrder.NEWEST, ) - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() + override val availableStates: Set get() = emptySet() + + init { + paginator.firstPage = 1 + searchPaginator.firstPage = 1 + } + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) - val pages = page + 1 - - when { - !query.isNullOrEmpty() -> { - - append("/?search=") - append(query.urlEncoded()) + when (filter) { + is MangaListFilter.Search -> { + append("/search?q=") + append(filter.query.urlEncoded()) append("&page=") - append(pages.toString()) - append("&post_type=wp-manga&post_type=wp-manga") + append(page.toString()) } - !tags.isNullOrEmpty() -> { - append("/genre/") - append(tag?.key.orEmpty()) - append("?page=") - append(pages.toString()) + is MangaListFilter.Advanced -> { + val tag = filter.tags.oneOrThrowIfMany() + if (filter.tags.isNotEmpty()) { + append("/genre/") + append(tag?.key.orEmpty()) + append("?page=") + append(page.toString()) + } else { + when (filter.sortOrder) { + SortOrder.UPDATED -> append("/latest") + SortOrder.NEWEST -> append("/new") + SortOrder.ALPHABETICAL -> append("/alphabet") + else -> append("/latest") + } + append("?page=") + append(page.toString()) + } } - else -> { - - when (sortOrder) { - SortOrder.UPDATED -> append("/latest") - SortOrder.NEWEST -> append("/new") - SortOrder.ALPHABETICAL -> append("/alphabet") - else -> append("/latest") - } - append("?page=") - append(pages.toString()) + null -> { + append("/latest?page=") + append(page.toString()) } } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/IsekaiScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/IsekaiScan.kt index 6464a2de..c0a9278f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/IsekaiScan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/IsekaiScan.kt @@ -22,50 +22,58 @@ internal class IsekaiScan(context: MangaLoaderContext) : SortOrder.UPDATED, ) - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() + override val availableStates: Set get() = emptySet() + + init { + paginator.firstPage = 1 + searchPaginator.firstPage = 1 + } + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + val url = buildString { append("https://") append(domain) - val pages = page + 1 - - when { - !query.isNullOrEmpty() -> { - + when (filter) { + is MangaListFilter.Search -> { append("/?search=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") - append(pages.toString()) + append(page.toString()) append("&post_type=wp-manga") } - !tags.isNullOrEmpty() -> { - append("/mangas/") - append(tag?.key.orEmpty()) - append("?orderby=2&page=") - append(pages.toString()) - + is MangaListFilter.Advanced -> { + + val tag = filter.tags.oneOrThrowIfMany() + if (filter.tags.isNotEmpty()) { + append("/$tagPrefix") + append(tag?.key.orEmpty()) + append("?orderby=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("2") + SortOrder.UPDATED -> append("3") + else -> append("3") + } + append("&page=") + append(page.toString()) + } else { + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("/popular-manga") + SortOrder.UPDATED -> append("/latest-manga") + else -> append("/latest-manga") + } + append("?page=") + append(page.toString()) + } } - else -> { - - if (sortOrder == SortOrder.POPULARITY) { - append("/popular-manga") - } - if (sortOrder == SortOrder.UPDATED) { - append("/latest-manga") - } - append("?page=") - append(pages.toString()) + null -> { + append("/latest-manga?page=") + append(page.toString()) } } } - val doc = webClient.httpGet(url).parseHtml() return doc.select("div.row.c-tabs-item__content").ifEmpty { @@ -91,8 +99,8 @@ internal class IsekaiScan(context: MangaLoaderContext) : author = summary?.selectFirst(".mg_author")?.selectFirst("a")?.ownText(), state = when (summary?.selectFirst(".mg_status")?.selectFirst(".summary-content")?.ownText()?.trim() ?.lowercase()) { - "Ongoing" -> MangaState.ONGOING - "Completed " -> MangaState.FINISHED + "ongoing" -> MangaState.ONGOING + "completed " -> MangaState.FINISHED else -> null }, source = source, @@ -133,8 +141,8 @@ internal class IsekaiScan(context: MangaLoaderContext) : override suspend fun getPages(chapter: MangaChapter): List { val fullUrl = chapter.url.toAbsoluteUrl(domain) val doc = webClient.httpGet(fullUrl).parseHtml() - val urlarray = doc.select("p#arraydata").text().split(",").toTypedArray() - return urlarray.map { url -> + val urlArray = doc.select("p#arraydata").text().split(",").toTypedArray() + return urlArray.map { url -> MangaPage( id = generateUid(url), url = url, diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/IsekaiScanEuParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/IsekaiScanEuParser.kt index 01657f53..c5b561e3 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/IsekaiScanEuParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/IsekaiScanEuParser.kt @@ -14,50 +14,71 @@ internal class IsekaiScanEuParser(context: MangaLoaderContext) : override val withoutAjax = true override val listUrl = "mangax/" - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() + init { + paginator.firstPage = 1 + searchPaginator.firstPage = 1 + } + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + val url = buildString { append("https://") append(domain) - val pages = page + 1 - when { - !query.isNullOrEmpty() -> { - append("/page/") - append(pages.toString()) + when (filter) { + + is MangaListFilter.Search -> { + if (page > 1) { + append("/page/") + append(page.toString()) + } append("/?s=") - append(query.urlEncoded()) - append("&post_type=wp-manga&") + append(filter.query.urlEncoded()) + append("&post_type=wp-manga") } - !tags.isNullOrEmpty() -> { - append("/$tagPrefix") - append(tag?.key.orEmpty()) - append("/page/") - append(pages.toString()) - append("?") - } + is MangaListFilter.Advanced -> { - else -> { + val tag = filter.tags.oneOrThrowIfMany() + if (filter.tags.isNotEmpty()) { + append("/$tagPrefix") + append(tag?.key.orEmpty()) + if (page > 1) { + append("/page/") + append(page.toString()) + } + append("/?") + } else { + if (page > 1) { + append("/page/") + append(page.toString()) + } + append("/?s=&post_type=wp-manga") + filter.states.forEach { + append("&status[]=") + when (it) { + MangaState.ONGOING -> append("on-going") + MangaState.FINISHED -> append("end") + MangaState.ABANDONED -> append("canceled") + MangaState.PAUSED -> append("on-hold") + } + } + append("&") + } - append("/$listUrl") - append("/page/") - append(pages.toString()) - append("?") + append("m_orderby=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("views") + SortOrder.UPDATED -> append("latest") + SortOrder.NEWEST -> append("new-manga") + SortOrder.ALPHABETICAL -> append("alphabet") + SortOrder.RATING -> append("rating") + } + } + + null -> { + append("/?s&post_type=wp-manga&m_orderby=latest") } - } - append("m_orderby=") - when (sortOrder) { - SortOrder.POPULARITY -> append("views") - SortOrder.UPDATED -> append("latest") - SortOrder.NEWEST -> append("new-manga") - SortOrder.ALPHABETICAL -> append("alphabet") - SortOrder.RATING -> append("rating") } } val doc = webClient.httpGet(url).parseHtml() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaDass.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaDass.kt index 3520f9ff..d17a4199 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaDass.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaDass.kt @@ -19,55 +19,61 @@ internal class MangaDass(context: MangaLoaderContext) : override val selectChapter = "li.a-h" override val selectDesc = "div.ss-manga" - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() + override val availableStates: Set get() = emptySet() + + init { + paginator.firstPage = 1 + searchPaginator.firstPage = 1 + } + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + val url = buildString { append("https://") append(domain) - val pages = page + 1 - - when { - !query.isNullOrEmpty() -> { - - append("/?s=") - append(query.urlEncoded()) + when (filter) { + is MangaListFilter.Search -> { + append("/search?q=") + append(filter.query.urlEncoded()) append("&page=") - append(pages.toString()) + append(page.toString()) } - !tags.isNullOrEmpty() -> { - append("/$tagPrefix") - append(tag?.key.orEmpty()) - append("/") - append(pages.toString()) - append("?") + is MangaListFilter.Advanced -> { + + val tag = filter.tags.oneOrThrowIfMany() + if (filter.tags.isNotEmpty()) { + append("/$tagPrefix") + append(tag?.key.orEmpty()) + append("/") + append(page.toString()) + append("?") + } else { + append("/$listUrl") + append("/") + append(page.toString()) + append("?") + } + + append("orderby=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("views") + SortOrder.UPDATED -> append("latest") + SortOrder.NEWEST -> append("new-manga") + SortOrder.ALPHABETICAL -> append("alphabet") + SortOrder.RATING -> append("rating") + } } - else -> { - + null -> { append("/$listUrl") append("/") - append(pages.toString()) - append("?") + append(page.toString()) + append("?orderby=latest") } } - append("orderby=") - when (sortOrder) { - SortOrder.POPULARITY -> append("views") - SortOrder.UPDATED -> append("latest") - SortOrder.NEWEST -> append("new-manga") - SortOrder.ALPHABETICAL -> append("alphabet") - SortOrder.RATING -> append("rating") - } } val doc = webClient.httpGet(url).parseHtml() - - return doc.select("div.row.c-tabs-item__content").ifEmpty { doc.select("div.page-item-detail") }.map { div -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaDna.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaDna.kt index 4e8d905c..e3416f46 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaDna.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaDna.kt @@ -18,53 +18,53 @@ internal class MangaDna(context: MangaLoaderContext) : override val withoutAjax = true override val selectDesc = "div.dsct" override val selectChapter = "li.a-h" + override val availableStates: Set get() = emptySet() - 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) - val pages = page + 1 - - when { - !query.isNullOrEmpty() -> { - append("/page/") - append(pages.toString()) - append("/?s=") - append(query.urlEncoded()) - append("&post_type=wp-manga&") + when (filter) { + is MangaListFilter.Search -> { + append("/search?q=") + append(filter.query.urlEncoded()) + append("&page=") + append(page.toString()) } - !tags.isNullOrEmpty() -> { - append("/$tagPrefix") - append(tag?.key.orEmpty()) - append("/page/") - append(pages.toString()) - append("?") + is MangaListFilter.Advanced -> { + + val tag = filter.tags.oneOrThrowIfMany() + if (filter.tags.isNotEmpty()) { + append("/$tagPrefix") + append(tag?.key.orEmpty()) + append("/") + append(page.toString()) + } else { + append("/$listUrl") + append("/page/") + append(page.toString()) + } + + append("?orderby=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("trending") + SortOrder.UPDATED -> append("latest") + SortOrder.ALPHABETICAL -> append("alphabet") + SortOrder.RATING -> append("rating") + else -> append("latest") + } } - else -> { - + null -> { append("/$listUrl") append("/page/") - append(pages.toString()) - append("?") + append(page.toString()) + append("?orderby=latest") } } - append("m_orderby=") - when (sortOrder) { - SortOrder.POPULARITY -> append("views") - SortOrder.UPDATED -> append("latest") - SortOrder.NEWEST -> append("new-manga") - SortOrder.ALPHABETICAL -> append("alphabet") - SortOrder.RATING -> append("rating") - } } + val doc = webClient.httpGet(url).parseHtml() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaPure.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaPure.kt index 44e53ff6..6277b7ce 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaPure.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaPure.kt @@ -21,46 +21,54 @@ internal class MangaPure(context: MangaLoaderContext) : SortOrder.UPDATED, ) - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() + override val availableStates: Set get() = emptySet() + + init { + paginator.firstPage = 1 + searchPaginator.firstPage = 1 + } + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) - val pages = page + 1 - - when { - !query.isNullOrEmpty() -> { - - append("/?search=") - append(query.urlEncoded()) + when (filter) { + is MangaListFilter.Search -> { + append("/search?s=") + append(filter.query.urlEncoded()) append("&page=") - append(pages.toString()) + append(page.toString()) append("&post_type=wp-manga") } - !tags.isNullOrEmpty() -> { - append("/mangas/") - append(tag?.key.orEmpty()) - append("?orderby=2&page=") - append(pages.toString()) - + is MangaListFilter.Advanced -> { + + val tag = filter.tags.oneOrThrowIfMany() + if (filter.tags.isNotEmpty()) { + append("/$tagPrefix") + append(tag?.key.orEmpty()) + append("?orderby=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("2") + SortOrder.UPDATED -> append("3") + else -> append("3") + } + append("&page=") + append(page.toString()) + } else { + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("/popular-manga") + SortOrder.UPDATED -> append("/latest-manga") + else -> append("/latest-manga") + } + append("?page=") + append(page.toString()) + } } - else -> { - - if (sortOrder == SortOrder.POPULARITY) { - append("/popular-manga") - } - if (sortOrder == SortOrder.UPDATED) { - append("/latest-manga") - } - append("?page=") - append(pages.toString()) + null -> { + append("/latest-manga?page=") + append(page.toString()) } } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Manhwaz.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Manhwaz.kt index b2ada718..4e8bc2fe 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Manhwaz.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Manhwaz.kt @@ -5,6 +5,7 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.site.madara.MadaraParser import org.koitharu.kotatsu.parsers.util.* +import java.util.EnumSet @MangaSourceParser("MANHWAZ", "ManhwaZ", "en") internal class Manhwaz(context: MangaLoaderContext) : @@ -15,51 +16,65 @@ internal class Manhwaz(context: MangaLoaderContext) : override val withoutAjax = true override val selectTestAsync = "div.list-chapter" - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() + override val availableStates: Set get() = emptySet() + + override val availableSortOrders: Set = EnumSet.of( + SortOrder.UPDATED, + SortOrder.POPULARITY, + SortOrder.NEWEST, + SortOrder.RATING, + ) + + init { + paginator.firstPage = 1 + searchPaginator.firstPage = 1 + } + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) - val pages = page + 1 - - when { - !query.isNullOrEmpty() -> { - + when (filter) { + is MangaListFilter.Search -> { append("/search?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") - append(pages.toString()) + append(page.toString()) } - !tags.isNullOrEmpty() -> { - append("/$tagPrefix") - append(tag?.key.orEmpty()) - append("?page=") - append(pages.toString()) - append("&") - } + is MangaListFilter.Advanced -> { - else -> { + val tag = filter.tags.oneOrThrowIfMany() + if (filter.tags.isNotEmpty()) { + append("/$tagPrefix") + append(tag?.key.orEmpty()) + append("?page=") + append(page.toString()) + append("&") + } else { + append("/$listUrl") + append("?page=") + append(page.toString()) + append("&") + } + append("m_orderby=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("views") + SortOrder.UPDATED -> append("latest") + SortOrder.NEWEST -> append("new") + SortOrder.RATING -> append("rating") + else -> append("latest") + } + } + + null -> { append("/$listUrl") append("?page=") - append(pages.toString()) - append("&") + append(page.toString()) + append("&m_orderby=latest") } } - append("m_orderby=") - when (sortOrder) { - SortOrder.POPULARITY -> append("views") - SortOrder.UPDATED -> append("latest") - SortOrder.NEWEST -> append("new-manga") - SortOrder.ALPHABETICAL -> append("alphabet") - SortOrder.RATING -> append("rating") - } } val doc = webClient.httpGet(url).parseHtml() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/DragonTranslationParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/DragonTranslationParser.kt index a4afe517..b86bcbb8 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/DragonTranslationParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/DragonTranslationParser.kt @@ -3,8 +3,9 @@ package org.koitharu.kotatsu.parsers.site.madara.es import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.site.madara.MadaraParser import org.koitharu.kotatsu.parsers.util.* @@ -13,48 +14,46 @@ import java.util.* @MangaSourceParser("DRAGONTRANSLATION", "Dragon Translation", "es") internal class DragonTranslationParser(context: MangaLoaderContext) : MadaraParser(context, MangaSource.DRAGONTRANSLATION, "dragontranslation.net", 30) { - - override val availableSortOrders: Set = EnumSet.of(SortOrder.UPDATED) - override val selectPage = "div#chapter_imgs img" + override val availableSortOrders: Set = EnumSet.of(SortOrder.UPDATED) + override val availableStates: Set get() = emptySet() - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - - val tag = tags.oneOrThrowIfMany() + init { + paginator.firstPage = 1 + searchPaginator.firstPage = 1 + } + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) - val pages = page + 1 - - when { - !query.isNullOrEmpty() -> { + when (filter) { + is MangaListFilter.Search -> { append("/mangas?buscar=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") - append(pages.toString()) + append(page.toString()) } - !tags.isNullOrEmpty() -> { - append("/mangas?tag=") - append(tag?.key.orEmpty()) - append("&page=") - append(pages.toString()) - } + is MangaListFilter.Advanced -> { - else -> { + append("/mangas?page=") + append(page.toString()) - append("/mangas") - append("?page=") - append(pages.toString()) + val tag = filter.tags.oneOrThrowIfMany() + if (filter.tags.isNotEmpty()) { + append("&tag=") + append(tag?.key.orEmpty()) + } + } + + null -> { + append("/mangas?page=") + append(page.toString()) } } } + val doc = webClient.httpGet(url).parseHtml() return doc.select("div.video-bg div.col-6 ").map { div -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/MonarcaManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/MonarcaManga.kt index 9f4d6186..48b58636 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/MonarcaManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/MonarcaManga.kt @@ -4,93 +4,9 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.site.madara.MadaraParser -import org.koitharu.kotatsu.parsers.util.* @MangaSourceParser("MONARCAMANGA", "MonarcaManga", "es") internal class MonarcaManga(context: MangaLoaderContext) : MadaraParser(context, MangaSource.MONARCAMANGA, "monarcamanga.com") { - override val tagPrefix = "manga-generos/" - override val withoutAjax = true - - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() - val url = buildString { - append("https://") - append(domain) - val pages = page + 1 - - when { - !query.isNullOrEmpty() -> { - append("/page/") - append(pages.toString()) - append("/?s=") - append(query.urlEncoded()) - append("&post_type=wp-manga&") - } - - !tags.isNullOrEmpty() -> { - append("/$tagPrefix") - append(tag?.key.orEmpty()) - append("/page/") - append(pages.toString()) - append("?") - } - - else -> { - - append("/$listUrl") - append("/page/") - append(pages.toString()) - append("?") - } - } - append("m_orderby=") - when (sortOrder) { - SortOrder.POPULARITY -> append("views") - SortOrder.UPDATED -> append("latest") - SortOrder.NEWEST -> append("new-manga") - SortOrder.ALPHABETICAL -> append("alphabet") - SortOrder.RATING -> append("rating") - } - } - val doc = webClient.httpGet(url).parseHtml() - - return doc.select("div.row.c-tabs-item__content").ifEmpty { - doc.select("div.page-item-detail") - }.map { div -> - val href = div.selectFirst("a")?.attrAsRelativeUrlOrNull("href") ?: div.parseFailed("Link not found") - val summary = div.selectFirst(".tab-summary") ?: div.selectFirst(".item-summary") - Manga( - id = generateUid(href), - url = href, - publicUrl = href.toAbsoluteUrl(div.host ?: domain), - coverUrl = div.selectFirst("img")?.src().orEmpty(), - title = (div?.selectFirst("h3") ?: div?.selectFirst("h4"))?.text().orEmpty(), - altTitle = null, - rating = div.selectFirst("span.total_votes")?.ownText()?.toFloatOrNull()?.div(5f) ?: -1f, - tags = summary?.selectFirst(".mg_genres")?.select("a")?.mapNotNullToSet { a -> - MangaTag( - key = a.attr("href").removeSuffix('/').substringAfterLast('/'), - title = a.text().ifEmpty { return@mapNotNullToSet null }.toTitleCase(), - source = source, - ) - }.orEmpty(), - author = summary?.selectFirst(".mg_author")?.selectFirst("a")?.ownText(), - state = when (summary?.selectFirst(".mg_status")?.selectFirst(".summary-content")?.ownText() - ?.lowercase()) { - in ongoing -> MangaState.ONGOING - in finished -> MangaState.FINISHED - else -> null - }, - source = source, - isNsfw = isNsfwSource, - ) - } - } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/TmoManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/TmoManga.kt index 641b79f7..1ec9a6df 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/TmoManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/TmoManga.kt @@ -16,44 +16,49 @@ internal class TmoManga(context: MangaLoaderContext) : override val listUrl = "biblioteca/" override val selectGenre = "div.summary-content a.tags_manga" override val withoutAjax = true + override val availableSortOrders: Set = EnumSet.of(SortOrder.POPULARITY) + override val availableStates: Set get() = emptySet() init { paginator.firstPage = 1 searchPaginator.firstPage = 1 } - override val availableSortOrders: Set = EnumSet.of(SortOrder.POPULARITY) - 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 -> { append("/$listUrl") append("?search=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) if (page > 1) { append("&page=") append(page) } } - !tags.isNullOrEmpty() -> { - append("/$tagPrefix") - append(tag?.key.orEmpty()) - if (page > 1) { - append("?page=") - append(page) + is MangaListFilter.Advanced -> { + + val tag = filter.tags.oneOrThrowIfMany() + if (filter.tags.isNotEmpty()) { + append("/$tagPrefix") + append(tag?.key.orEmpty()) + if (page > 1) { + append("?page=") + append(page) + } + } else { + append("/$listUrl") + if (page > 1) { + append("?page=") + append(page) + } } } - else -> { + null -> { append("/$listUrl") if (page > 1) { append("?page=") @@ -62,6 +67,7 @@ internal class TmoManga(context: MangaLoaderContext) : } } } + val doc = webClient.httpGet(url).parseHtml() return doc.select("div.page-item-detail").map { div -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/id/ManhwaHub.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/id/ManhwaHub.kt index 36f06b33..a4c1991f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/id/ManhwaHub.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/id/ManhwaHub.kt @@ -17,56 +17,47 @@ internal class ManhwaHub(context: MangaLoaderContext) : override val withoutAjax = true override val listUrl = "genre/manhwa" override val selectTestAsync = "ul.box-list-chapter" + override val availableStates: Set get() = emptySet() + override val availableSortOrders: Set = EnumSet.of(SortOrder.UPDATED) init { paginator.firstPage = 1 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) - when { - !query.isNullOrEmpty() -> { + when (filter) { + is MangaListFilter.Search -> { append("/search?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") - append(page) - append("&") + append(page.toString()) } - !tags.isNullOrEmpty() -> { - append("/$tagPrefix") - append(tag?.key.orEmpty()) - append("?page=") - append(page) - append("&") - } + is MangaListFilter.Advanced -> { - else -> { + val tag = filter.tags.oneOrThrowIfMany() + if (filter.tags.isNotEmpty()) { + append("/$tagPrefix") + append(tag?.key.orEmpty()) + append("?page=") + append(page.toString()) + append("&m_orderby=latest") + } else { + append("/?page=") + append(page.toString()) + } - append("/$listUrl") - append("?page=") - append(page) - append("&") - } - } + } - append("m_orderby=") - when (sortOrder) { - SortOrder.POPULARITY -> append("views") - SortOrder.UPDATED -> append("latest") - SortOrder.NEWEST -> append("new-manga") - SortOrder.ALPHABETICAL -> append("alphabet") - SortOrder.RATING -> append("rating") + null -> { + append("/?page=") + append(page.toString()) + } } } val doc = webClient.httpGet(url).parseHtml() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/Saytruyenhay.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/Saytruyenhay.kt index c69b31b1..6a67b7e2 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/Saytruyenhay.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/Saytruyenhay.kt @@ -7,6 +7,7 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.site.madara.MadaraParser import org.koitharu.kotatsu.parsers.util.* +import java.util.EnumSet @MangaSourceParser("SAYTRUYENHAY", "Saytruyenhay", "vi") internal class Saytruyenhay(context: MangaLoaderContext) : @@ -15,50 +16,57 @@ internal class Saytruyenhay(context: MangaLoaderContext) : override val tagPrefix = "genre/" override val withoutAjax = true override val listUrl = "public/genre/manga/" + override val availableStates: Set get() = emptySet() + override val availableSortOrders: Set = + EnumSet.of(SortOrder.POPULARITY, SortOrder.UPDATED, SortOrder.RATING, SortOrder.NEWEST) - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() + init { + paginator.firstPage = 1 + searchPaginator.firstPage = 1 + } + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) - val pages = page + 1 - - when { - !query.isNullOrEmpty() -> { - - append("/public/search?s=") - append(query.urlEncoded()) + when (filter) { + is MangaListFilter.Search -> { + append("/search?s=") + append(filter.query.urlEncoded()) append("&page=") - append(pages.toString()) + append(page.toString()) } - !tags.isNullOrEmpty() -> { - append("/$tagPrefix") - append(tag?.key.orEmpty()) + is MangaListFilter.Advanced -> { + + val tag = filter.tags.oneOrThrowIfMany() + if (filter.tags.isNotEmpty()) { + append("/$tagPrefix") + append(tag?.key.orEmpty()) + } else { + append("/$listUrl") + + } append("?page=") - append(pages.toString()) + append(page.toString()) + append("&m_orderby=") + when (filter.sortOrder) { + SortOrder.UPDATED -> append("latest") + SortOrder.RATING -> append("rating") + SortOrder.POPULARITY -> append("views") + SortOrder.NEWEST -> append("new") + else -> append("latest") + } } - else -> { + null -> { append("/$listUrl") append("?page=") - append(pages.toString()) + append(page.toString()) } } - append("&m_orderby=") - when (sortOrder) { - SortOrder.POPULARITY -> append("views") - SortOrder.UPDATED -> append("latest") - SortOrder.NEWEST -> append("new-manga") - SortOrder.ALPHABETICAL -> append("alphabet") - SortOrder.RATING -> append("rating") - } } + val doc = webClient.httpGet(url).parseHtml() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/MadthemeParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/MadthemeParser.kt index 2e795f13..0c7d3665 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/MadthemeParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/MadthemeParser.kt @@ -29,6 +29,8 @@ internal abstract class MadthemeParser( SortOrder.RATING, ) + override val availableStates: Set = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED) + protected open val listUrl = "search/" protected open val datePattern = "MMM dd, yyyy" @@ -52,35 +54,52 @@ internal abstract class MadthemeParser( "COMPLETED", ) - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) - append("/$listUrl?sort=") - when (sortOrder) { - SortOrder.POPULARITY -> append("views") - SortOrder.UPDATED -> append("updated_at") - SortOrder.ALPHABETICAL -> append("name") // On some sites without tags or searches, the alphabetical option is empty. - SortOrder.NEWEST -> append("created_at") - SortOrder.RATING -> append("rating") - } - if (!query.isNullOrEmpty()) { - append("&q=") - append(query.urlEncoded()) - } + append('/') + append(listUrl) + when (filter) { - if (!tags.isNullOrEmpty()) { - for (tag in tags) { - append("&") - append("genre[]".urlEncoded()) - append("=") - append(tag.key) + is MangaListFilter.Search -> { + append("?sort=updated_at&q=") + append(filter.query.urlEncoded()) } + + is MangaListFilter.Advanced -> { + + append("?sort=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("views") + SortOrder.UPDATED -> append("updated_at") + SortOrder.ALPHABETICAL -> append("name") // On some sites without tags or searches, the alphabetical option is empty. + SortOrder.NEWEST -> append("created_at") + SortOrder.RATING -> append("rating") + } + if (filter.tags.isNotEmpty()) { + filter.tags.forEach { + append("&") + append("genre[]".urlEncoded()) + append("=") + append(it.key) + } + } + + filter.states.oneOrThrowIfMany()?.let { + append("&status=") + append( + when (it) { + MangaState.ONGOING -> "ongoing" + MangaState.FINISHED -> "completed" + else -> "all" + }, + ) + } + + } + + null -> append("?sort=updated_at") } append("&page=") @@ -117,7 +136,7 @@ internal abstract class MadthemeParser( override suspend fun getAvailableTags(): Set { val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml() - return doc.select("div.genres label.checkbox").mapNotNullToSet { checkbox -> + return doc.select("div.genres .checkbox").mapNotNullToSet { checkbox -> val key = checkbox.selectFirstOrThrow("input").attr("value") ?: return@mapNotNullToSet null val name = checkbox.selectFirstOrThrow("span.radio__label").text() MangaTag( diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/all/ManhuaScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/all/ManhuaScan.kt index 031861c5..341b086b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/all/ManhuaScan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/all/ManhuaScan.kt @@ -7,44 +7,58 @@ import org.koitharu.kotatsu.parsers.site.madtheme.MadthemeParser import org.koitharu.kotatsu.parsers.util.* import java.util.Locale -@MangaSourceParser("MANHUASCAN", "ManhuaScan", "") +@MangaSourceParser("MANHUASCAN", "ManhuaScan.io", "") internal class ManhuaScan(context: MangaLoaderContext) : MadthemeParser(context, MangaSource.MANHUASCAN, "manhuascan.io") { override val sourceLocale: Locale = Locale.ENGLISH override val listUrl = "search" - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) append('/') append(listUrl) - append("?sort=") - when (sortOrder) { - SortOrder.POPULARITY -> append("views") - SortOrder.UPDATED -> append("updated_at") - SortOrder.ALPHABETICAL -> append("name") - SortOrder.NEWEST -> append("created_at") - SortOrder.RATING -> append("rating") - } + when (filter) { - if (!query.isNullOrEmpty()) { - append("&q=") - append(query.urlEncoded()) - } + is MangaListFilter.Search -> { + append("?sort=updated_at&q=") + append(filter.query.urlEncoded()) + } + + is MangaListFilter.Advanced -> { + + append("?sort=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("views") + SortOrder.UPDATED -> append("updated_at") + SortOrder.ALPHABETICAL -> append("name") + SortOrder.NEWEST -> append("created_at") + SortOrder.RATING -> append("rating") + } + if (filter.tags.isNotEmpty()) { + filter.tags.forEach { + append("&") + append("include[]".urlEncoded()) + append("=") + append(it.key) + } + } + + filter.states.oneOrThrowIfMany()?.let { + append("&status=") + append( + when (it) { + MangaState.ONGOING -> "ongoing" + MangaState.FINISHED -> "completed" + else -> "all" + }, + ) + } - if (!tags.isNullOrEmpty()) { - for (tag in tags) { - append("&") - append("include[]".urlEncoded()) - append("=") - append(tag.key) } + + null -> append("?sort=updated_at") } append("&page=") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/en/MangaBuddy.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/en/MangaBuddy.kt index e08c1bd8..23a8331b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/en/MangaBuddy.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/en/MangaBuddy.kt @@ -34,6 +34,5 @@ internal class MangaBuddy(context: MangaLoaderContext) : ) } return pages - } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/en/TooniTube.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/en/TooniTube.kt index a00a728d..34aa29ea 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/en/TooniTube.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/en/TooniTube.kt @@ -9,6 +9,5 @@ import org.koitharu.kotatsu.parsers.site.madtheme.MadthemeParser @MangaSourceParser("TOONITUBE", "TooniTube", "en", ContentType.HENTAI) internal class TooniTube(context: MangaLoaderContext) : MadthemeParser(context, MangaSource.TOONITUBE, "toonitube.com") { - override val selectDesc = "div.summary div.section-body p.content" } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/en/ToonilyMe.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/en/ToonilyMe.kt index 4dd02782..ef25c4b7 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/en/ToonilyMe.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/en/ToonilyMe.kt @@ -9,6 +9,5 @@ import org.koitharu.kotatsu.parsers.site.madtheme.MadthemeParser @MangaSourceParser("TOONILY_ME", "Toonily.Me", "en", ContentType.HENTAI) internal class ToonilyMe(context: MangaLoaderContext) : MadthemeParser(context, MangaSource.TOONILY_ME, "toonily.me") { - override val selectDesc = "div.summary div.section-body p.content" } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/Manga18Parser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/Manga18Parser.kt index 3ec81dc2..7703908d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/Manga18Parser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/Manga18Parser.kt @@ -26,6 +26,8 @@ internal abstract class Manga18Parser( SortOrder.ALPHABETICAL, ) + override val isMultipleTagsSupported = false + protected open val listUrl = "list-manga/" protected open val tagUrl = "manga-list/" protected open val datePattern = "dd-MM-yyyy" @@ -47,49 +49,53 @@ internal abstract class Manga18Parser( "Completed", ) - 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() -> { - append("/$listUrl") + append('/') + when (filter) { + + is MangaListFilter.Search -> { + append(listUrl) append(page.toString()) append("?search=") - append(query.urlEncoded()) - append("&") + append(filter.query.urlEncoded()) + append("&order_by=latest") } - !tags.isNullOrEmpty() -> { - append("/$tagUrl") - append(tag?.key.orEmpty()) - append("/") + is MangaListFilter.Advanced -> { + if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + append(tagUrl) + append(it.key) + append("/") + } + } else { + append(listUrl) + } + append(page.toString()) - append("?") + append("?order_by=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("views") + SortOrder.UPDATED -> append("lastest") + SortOrder.ALPHABETICAL -> append("name") + else -> append("latest") + } } - else -> { - append("/$listUrl") + null -> { + append(listUrl) append(page.toString()) - append("?") + append("?order_by=latest") } } - append("order_by=") - when (sortOrder) { - SortOrder.POPULARITY -> append("views") - SortOrder.UPDATED -> append("lastest") - SortOrder.ALPHABETICAL -> append("name") - else -> append("latest") - } } - val doc = webClient.httpGet(url).parseHtml() + return parseMangaList(webClient.httpGet(url).parseHtml()) + } + protected open fun parseMangaList(doc: Document): List { return doc.select("div.story_item").map { div -> val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") Manga( diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/en/Hentai3zCc.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/en/Hentai3zCc.kt index d7016dd5..71a72bc8 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/en/Hentai3zCc.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/en/Hentai3zCc.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.parsers.site.manga18.en +import org.jsoup.nodes.Document import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.model.* @@ -10,51 +11,7 @@ import org.koitharu.kotatsu.parsers.util.* internal class Hentai3zCc(context: MangaLoaderContext) : Manga18Parser(context, MangaSource.HENTAI3ZCC, "hentai3z.cc") { - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() - val url = buildString { - append("https://") - append(domain) - val pages = page + 1 - when { - !query.isNullOrEmpty() -> { - append("/$listUrl") - append(pages.toString()) - append("?search=") - append(query.urlEncoded()) - append("&") - } - - !tags.isNullOrEmpty() -> { - append("/$tagUrl") - append(tag?.key.orEmpty()) - append("/") - append(pages.toString()) - append("?") - } - - else -> { - append("/$listUrl") - append(pages.toString()) - append("?") - } - } - append("order_by=") - when (sortOrder) { - SortOrder.POPULARITY -> append("views") - SortOrder.UPDATED -> append("lastest") - SortOrder.ALPHABETICAL -> append("name") - else -> append("latest") - } - } - val doc = webClient.httpGet(url).parseHtml() - - + override fun parseMangaList(doc: Document): List { return doc.select("div.story_item").map { div -> val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") Manga( diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/es/Tumanhwas.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/es/Tumanhwas.kt index ee087215..8c8101dc 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/es/Tumanhwas.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/es/Tumanhwas.kt @@ -9,7 +9,6 @@ import org.koitharu.kotatsu.parsers.site.manga18.Manga18Parser @MangaSourceParser("TUMANHWAS", "Tumanhwas", "es", ContentType.HENTAI) internal class Tumanhwas(context: MangaLoaderContext) : Manga18Parser(context, MangaSource.TUMANHWAS, "tumanhwas.club") { - override val selectTag = "div.item:contains(Géneros) div.info_value a" override val selectAlt = "div.item:contains(Títulos alternativos) div.info_value" } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/MangaboxParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/MangaboxParser.kt index 0954d696..f0f5383f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/MangaboxParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/MangaboxParser.kt @@ -21,9 +21,12 @@ internal abstract class MangaboxParser( SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.NEWEST, + SortOrder.ALPHABETICAL, ) - protected open val listUrl = "/genre-all" + override val availableStates: Set = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED) + + protected open val listUrl = "/advanced_search" protected open val searchUrl = "/search/story/" protected open val datePattern = "MMM dd,yy" @@ -36,50 +39,64 @@ internal abstract class MangaboxParser( @JvmField protected val ongoing: Set = setOf( - "Ongoing", + "ongoing", ) @JvmField protected val finished: Set = setOf( - "Completed", + "completed", ) - 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(listUrl) + append("/?s=all") + when (filter) { - if (!query.isNullOrEmpty()) { - append(searchUrl) - append(query.urlEncoded()) - append("?page=") - append(page.toString()) - - } else if (!tags.isNullOrEmpty()) { - append("/") - append(tag?.key.orEmpty()) - append("/") - append(page.toString()) - } else { - append("$listUrl/") - if (page > 1) { - append(page.toString()) + is MangaListFilter.Search -> { + append("&keyw=") + append(filter.query.urlEncoded()) } - when (sortOrder) { - SortOrder.POPULARITY -> append("?type=topview") - SortOrder.UPDATED -> append("") - SortOrder.NEWEST -> append("?type=newest") - else -> append("") + + is MangaListFilter.Advanced -> { + + if (filter.tags.isNotEmpty()) { + append("&g_i=") + filter.tags.forEach { + append("_") + append(it.key) + append("_") + } + } + + filter.states.oneOrThrowIfMany()?.let { + append("&sts=") + append( + when (it) { + MangaState.ONGOING -> "ongoing" + MangaState.FINISHED -> "completed" + else -> "" + }, + ) + } + + append("&orby=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("topview") + SortOrder.UPDATED -> append("") + SortOrder.NEWEST -> append("newest") + SortOrder.ALPHABETICAL -> append("az") + else -> append("") + } } - } + null -> {} + } + append("&page=") + append(page.toString()) } val doc = webClient.httpGet(url).parseHtml() @@ -109,7 +126,8 @@ internal abstract class MangaboxParser( override suspend fun getAvailableTags(): Set { val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml() - return doc.select(selectTagMap).mapNotNullToSet { a -> + val tags = doc.select(selectTagMap).drop(1) // remove all tags + return tags.mapNotNullToSet { a -> val key = a.attr("href").removeSuffix('/').substringAfterLast('/') val name = a.attr("title").replace(" Manga", "") MangaTag( @@ -129,25 +147,18 @@ internal abstract class MangaboxParser( 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 stateDiv = doc.select(selectState).text() - val state = stateDiv.let { - when (it) { + when (it.lowercase()) { in ongoing -> MangaState.ONGOING in finished -> MangaState.FINISHED else -> null } } - val alt = doc.body().select(selectAlt).text().replace("Alternative : ", "") - val aut = doc.body().select(selectAut).eachText().joinToString() - manga.copy( tags = doc.body().select(selectTag).mapNotNullToSet { a -> MangaTag( diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Mangabat.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Mangabat.kt index a4e6b1da..23efaaba 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Mangabat.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Mangabat.kt @@ -9,13 +9,7 @@ import org.koitharu.kotatsu.parsers.site.mangabox.MangaboxParser @MangaSourceParser("HMANGABAT", "MangaBat", "en") internal class Mangabat(context: MangaLoaderContext) : MangaboxParser(context, MangaSource.HMANGABAT) { - override val configKeyDomain = ConfigKey.Domain("h.mangabat.com", "readmangabat.com") - override val otherDomain = "readmangabat.com" - - override val searchUrl = "/search/manga/" - - override val listUrl = "/manga-list-all" override val selectTagMap = "div.panel-category p.pn-category-row:not(.pn-category-row-border) a" } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Mangairo.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Mangairo.kt index 51eb5e02..70241a79 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Mangairo.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Mangairo.kt @@ -6,6 +6,7 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaTag @@ -13,73 +14,87 @@ import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.site.mangabox.MangaboxParser import org.koitharu.kotatsu.parsers.util.* +import java.util.EnumSet @MangaSourceParser("MANGAIRO", "MangaIro", "en") internal class Mangairo(context: MangaLoaderContext) : MangaboxParser(context, MangaSource.MANGAIRO) { - override val configKeyDomain = ConfigKey.Domain("w.mangairo.com", "chap.mangairo.com") - override val otherDomain = "chap.mangairo.com" - override val datePattern = "MMM-dd-yy" override val listUrl = "/manga-list" override val searchUrl = "/list/search/" - override val selectDesc = "div#story_discription p" override val selectState = "ul.story_info_right li:contains(Status) a" override val selectAlt = "ul.story_info_right li:contains(Alter) h2" override val selectAut = "ul.story_info_right li:contains(Author) a" override val selectTag = "ul.story_info_right li:contains(Genres) a" - override val selectChapter = "div.chapter_list li" override val selectDate = "p" - override val selectPage = "div.panel-read-story img" - - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() + override val availableSortOrders: Set = EnumSet.of( + SortOrder.UPDATED, + SortOrder.POPULARITY, + SortOrder.NEWEST, + ) + override val isMultipleTagsSupported = false + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) + when (filter) { - if (!query.isNullOrEmpty()) { - append(searchUrl) - append(query.urlEncoded()) - append("?page=") - append(page.toString()) - - - } else { + is MangaListFilter.Search -> { + append(searchUrl) + append(filter.query.urlEncoded()) + append("?page=") + } - append("$listUrl/") + is MangaListFilter.Advanced -> { + append(listUrl) + append("/type-") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("topview") + SortOrder.UPDATED -> append("latest") + SortOrder.NEWEST -> append("newest") + else -> append("latest") + } - append("/type-") - when (sortOrder) { - SortOrder.POPULARITY -> append("topview") - SortOrder.UPDATED -> append("latest") - SortOrder.NEWEST -> append("newest") - else -> append("latest") + append("/ctg-") + if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + append(it.key) + } + } else { + append("all") + } + + append("/state-") + if (filter.states.isNotEmpty()) { + filter.states.oneOrThrowIfMany()?.let { + append( + when (it) { + MangaState.ONGOING -> "ongoing" + MangaState.FINISHED -> "completed" + else -> "all" + }, + ) + } + } else { + append("all") + } + + append("/page-") } - if (!tags.isNullOrEmpty()) { - append("/ctg-") - append(tag?.key.orEmpty()) - } else { - append("/ctg-all") + null -> { + append(listUrl) + append("/type-latest/ctg-all/state-all/page-") } - append("/state-all/page-") - append(page.toString()) } + append(page.toString()) } - val doc = webClient.httpGet(url).parseHtml() - return doc.select("div.story-item").map { div -> val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") Manga( @@ -115,13 +130,9 @@ internal class Mangairo(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 stateDiv = doc.select(selectState).text() - val state = stateDiv.let { when (it) { in ongoing -> MangaState.ONGOING @@ -131,9 +142,7 @@ internal class Mangairo(context: MangaLoaderContext) : } val alt = doc.body().select(selectAlt).text().replace("Alternative : ", "") - val aut = doc.body().select(selectAut).eachText().joinToString() - manga.copy( tags = doc.body().select(selectTag).mapNotNullToSet { a -> MangaTag( @@ -151,6 +160,4 @@ internal class Mangairo(context: MangaLoaderContext) : isNsfw = manga.isNsfw, ) } - - } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Mangakakalot.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Mangakakalot.kt index 99324654..ec3c171a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Mangakakalot.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Mangakakalot.kt @@ -8,54 +8,69 @@ import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.site.mangabox.MangaboxParser import org.koitharu.kotatsu.parsers.util.* import java.text.SimpleDateFormat +import java.util.EnumSet -@MangaSourceParser("MANGAKAKALOT", "Mangakakalot", "en") +@MangaSourceParser("MANGAKAKALOT", "Mangakakalot.com", "en") internal class Mangakakalot(context: MangaLoaderContext) : MangaboxParser(context, MangaSource.MANGAKAKALOT) { - override val configKeyDomain = ConfigKey.Domain("mangakakalot.com", "chapmanganato.com") - + override val availableSortOrders: Set = EnumSet.of( + SortOrder.UPDATED, + SortOrder.POPULARITY, + SortOrder.NEWEST, + ) + override val isMultipleTagsSupported = false override val otherDomain = "chapmanganato.com" - override val listUrl = "/manga_list" - 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) { - if (!query.isNullOrEmpty()) { - append(searchUrl) - append(query.replace(" ", "_").urlEncoded()) - append("?page=") - append(page.toString()) - - } else { - append("$listUrl/") - when (sortOrder) { - SortOrder.POPULARITY -> append("?type=topview") - SortOrder.UPDATED -> append("?type=latest") - SortOrder.NEWEST -> append("?type=newest") - else -> append("?type=latest") + is MangaListFilter.Search -> { + append(searchUrl) + append(filter.query.urlEncoded()) + append("?page=") } - if (!tags.isNullOrEmpty()) { - append("&category=") - append(tag?.key.orEmpty()) - } else { - append("&category=all") + + is MangaListFilter.Advanced -> { + append(listUrl) + append("?type=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("topview") + SortOrder.UPDATED -> append("latest") + SortOrder.NEWEST -> append("newest") + else -> append("latest") + } + if (filter.tags.isNotEmpty()) { + append("&category=") + filter.tags.oneOrThrowIfMany()?.let { + append(it.key) + } + } + + filter.states.oneOrThrowIfMany()?.let { + append("&state=") + append( + when (it) { + MangaState.ONGOING -> "ongoing" + MangaState.FINISHED -> "completed" + else -> "all" + }, + ) + } + + append("&page=") } - append("&state=all&page=") - append(page) + null -> { + append(listUrl) + append("?type=latest&page=") + } } - - + append(page.toString()) } val doc = webClient.httpGet(url).parseHtml() @@ -81,8 +96,21 @@ internal class Mangakakalot(context: MangaLoaderContext) : } } - override suspend fun getChapters(doc: Document): List { + override suspend fun getAvailableTags(): Set { + val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml() + val tags = doc.select("ul.tag li a").drop(1) + return tags.mapNotNullToSet { a -> + val key = a.attr("href").substringAfterLast("category=").substringBefore("&") + val name = a.attr("title").replace(" Manga", "") + MangaTag( + key = key, + title = name, + source = source, + ) + } + } + override suspend fun getChapters(doc: Document): List { return doc.body().select(selectChapter).mapChapters(reversed = true) { i, li -> val a = li.selectFirstOrThrow("a") val href = a.attrAsRelativeUrl("href") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/MangakakalotTv.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/MangakakalotTv.kt index 0531c005..2c545e17 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/MangakakalotTv.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/MangakakalotTv.kt @@ -1,11 +1,14 @@ package org.koitharu.kotatsu.parsers.site.mangabox.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.config.ConfigKey import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.site.mangabox.MangaboxParser import org.koitharu.kotatsu.parsers.util.* +import java.util.EnumSet @MangaSourceParser("MANGAKAKALOTTV", "Mangakakalot.tv", "en") internal class MangakakalotTv(context: MangaLoaderContext) : @@ -14,40 +17,61 @@ internal class MangakakalotTv(context: MangaLoaderContext) : override val configKeyDomain = ConfigKey.Domain("ww6.mangakakalot.tv") override val searchUrl = "/search/" override val listUrl = "/manga_list" + override val availableSortOrders: Set = EnumSet.of( + SortOrder.UPDATED, + SortOrder.POPULARITY, + SortOrder.NEWEST, + ) + 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(searchUrl) - append(query.urlEncoded()) - append("?page=") - append(page.toString()) - } else { - append("$listUrl/") - append("?type=") - when (sortOrder) { - SortOrder.POPULARITY -> append("topview") - SortOrder.UPDATED -> append("latest") - SortOrder.NEWEST -> append("newest") - else -> append("latest") - } - if (!tags.isNullOrEmpty()) { - append("&category=") - append(tag?.key.orEmpty()) + when (filter) { + + is MangaListFilter.Search -> { + append(searchUrl) + append(filter.query.urlEncoded()) + append("?page=") } - if (page > 1) { + + is MangaListFilter.Advanced -> { + append(listUrl) + append("?type=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("topview") + SortOrder.UPDATED -> append("latest") + SortOrder.NEWEST -> append("newest") + else -> append("latest") + } + if (filter.tags.isNotEmpty()) { + append("&category=") + filter.tags.oneOrThrowIfMany()?.let { + append(it.key) + } + } + + filter.states.oneOrThrowIfMany()?.let { + append("&state=") + append( + when (it) { + MangaState.ONGOING -> "Ongoing" + MangaState.FINISHED -> "Completed" + else -> "all" + }, + ) + } + append("&page=") - append(page.toString()) + } + + null -> { + append(listUrl) + append("?type=latest&page=") } } + append(page.toString()) } val doc = webClient.httpGet(url).parseHtml() return doc.select("div.list-truyen-item-wrap").ifEmpty { @@ -71,6 +95,38 @@ internal class MangakakalotTv(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 stateDiv = doc.select(selectState).text().replace("Status : ", "") + val state = stateDiv.let { + when (it.lowercase()) { + in ongoing -> MangaState.ONGOING + in finished -> MangaState.FINISHED + else -> null + } + } + val alt = doc.body().select(selectAlt).text().replace("Alternative : ", "") + val aut = doc.body().select(selectAut).eachText().joinToString() + manga.copy( + tags = doc.body().select(selectTag).mapNotNullToSet { a -> + MangaTag( + key = a.attr("href").substringAfterLast("category=").substringBefore("&"), + title = a.text().toTitleCase(), + source = source, + ) + }, + description = desc, + altTitle = alt, + author = aut, + state = state, + chapters = chaptersDeferred.await(), + isNsfw = manga.isNsfw, + ) + } + override val selectTagMap = "ul.tag li a" override suspend fun getAvailableTags(): Set { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Manganato.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Manganato.kt index c9f09346..3022f7d4 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Manganato.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Manganato.kt @@ -9,9 +9,6 @@ import org.koitharu.kotatsu.parsers.site.mangabox.MangaboxParser @MangaSourceParser("MANGANATO", "Manganato", "en") internal class Manganato(context: MangaLoaderContext) : MangaboxParser(context, MangaSource.MANGANATO) { - override val configKeyDomain = ConfigKey.Domain("chapmanganato.com", "manganato.com") - override val otherDomain = "chapmanganato.com" - } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/MangaReaderParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/MangaReaderParser.kt index a28442d6..861de4eb 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/MangaReaderParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/MangaReaderParser.kt @@ -32,60 +32,72 @@ internal abstract class MangaReaderParser( override val availableSortOrders: Set get() = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.ALPHABETICAL, SortOrder.NEWEST) + override val availableStates: Set + get() = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED) + protected open val listUrl = "/manga" protected open val datePattern = "MMMM d, yyyy" protected open val isNetShieldProtected = false private var tagCache: ArrayMap? = null private val mutex = Mutex() - protected open var lastSearchPage = 1 - - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - if (!query.isNullOrEmpty()) { - if (page > lastSearchPage) { - return emptyList() - } - val url = buildString { - append("https://") - append(domain) - append("/page/") - append(page) - append("/?s=") - append(query.urlEncoded()) - } - - val docs = webClient.httpGet(url).parseHtml() - lastSearchPage = docs.selectFirst(".pagination .next") - ?.previousElementSibling() - ?.text()?.toIntOrNull() ?: 1 - return parseMangaList(docs) - } - - val sortQuery = when (sortOrder) { - SortOrder.ALPHABETICAL -> "title" - SortOrder.NEWEST -> "latest" - SortOrder.POPULARITY -> "popular" - SortOrder.UPDATED -> "update" - else -> "" - } - val tagKey = "genre[]".urlEncoded() - val tagQuery = - if (tags.isNullOrEmpty()) "" else tags.joinToString(separator = "&", prefix = "&") { "$tagKey=${it.key}" } + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) - append(listUrl) - append("/?order=") - append(sortQuery) - append(tagQuery) - append("&page=") - append(page) + + when (filter) { + + is MangaListFilter.Search -> { + append("/page/") + append(page.toString()) + append("/?s=") + append(filter.query.urlEncoded()) + } + + is MangaListFilter.Advanced -> { + append(listUrl) + + append("/?order=") + append( + when (filter.sortOrder) { + SortOrder.ALPHABETICAL -> "title" + SortOrder.NEWEST -> "latest" + SortOrder.POPULARITY -> "popular" + SortOrder.UPDATED -> "update" + else -> "" + }, + ) + + val tagKey = "genre[]".urlEncoded() + val tagQuery = + if (filter.tags.isEmpty()) "" + else filter.tags.joinToString(separator = "&", prefix = "&") { "$tagKey=${it.key}" } + append(tagQuery) + + if (filter.states.isNotEmpty()) { + filter.states.oneOrThrowIfMany()?.let { + append("&status=") + when (it) { + MangaState.ONGOING -> append("ongoing") + MangaState.FINISHED -> append("completed") + MangaState.PAUSED -> append("hiatus") + else -> append("") + } + } + } + + append("&page=") + append(page.toString()) + } + + null -> { + append(listUrl) + append("/?order=update&page=") + append(page.toString()) + } + } } return parseMangaList(webClient.httpGet(url).parseHtml()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/ar/SwaTeam.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/ar/SwaTeam.kt index eb82d9f9..f672d1c5 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/ar/SwaTeam.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/ar/SwaTeam.kt @@ -17,52 +17,58 @@ internal class SwaTeam(context: MangaLoaderContext) : override val selectMangaListImg = "img" override val isNetShieldProtected = true - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - if (!query.isNullOrEmpty()) { - if (page > lastSearchPage) { - return emptyList() - } - - val url = buildString { - append("https://") - append(domain) - append("/?s=") - append(query.urlEncoded()) - append("&page=") - append(page) - } - - val docs = webClient.httpGet(url).parseHtml() - lastSearchPage = docs.selectFirst(".pagination .next") - ?.previousElementSibling() - ?.text()?.toIntOrNull() ?: 1 - return parseMangaList(docs) - } - - val sortQuery = when (sortOrder) { - SortOrder.ALPHABETICAL -> "a-z" - SortOrder.NEWEST -> "added" - SortOrder.POPULARITY -> "popular" - SortOrder.UPDATED -> "update" - else -> "" - } - val tagKey = "genre[]".urlEncoded() - val tagQuery = - if (tags.isNullOrEmpty()) "" else tags.joinToString(separator = "&", prefix = "&") { "$tagKey=${it.key}" } + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) - append(listUrl) - append("/?order=") - append(sortQuery) - append(tagQuery) + + when (filter) { + + is MangaListFilter.Search -> { + append("/?s=") + append(filter.query.urlEncoded()) + } + + is MangaListFilter.Advanced -> { + append(listUrl) + + append("/?order=") + append( + when (filter.sortOrder) { + SortOrder.ALPHABETICAL -> "a-z" + SortOrder.NEWEST -> "added" + SortOrder.POPULARITY -> "popular" + SortOrder.UPDATED -> "update" + else -> "" + }, + ) + + val tagKey = "genre[]".urlEncoded() + val tagQuery = + if (filter.tags.isEmpty()) "" + else filter.tags.joinToString(separator = "&", prefix = "&") { "$tagKey=${it.key}" } + append(tagQuery) + + if (filter.states.isNotEmpty()) { + filter.states.oneOrThrowIfMany()?.let { + append("&status=") + when (it) { + MangaState.ONGOING -> append("ongoing") + MangaState.FINISHED -> append("completed") + MangaState.PAUSED -> append("hiatus") + else -> append("") + } + } + } + } + + null -> { + append(listUrl) + append("/?order=update") + } + } append("&page=") - append(page) + append(page.toString()) } return parseMangaList(webClient.httpGet(url).parseHtml()) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/ManhwaFreak.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/ManhwaFreak.kt index ae686339..1c9ba41c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/ManhwaFreak.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/ManhwaFreak.kt @@ -17,60 +17,52 @@ internal class ManhwaFreak(context: MangaLoaderContext) : override val selectMangaList = ".listupd .lastest-serie" override val selectMangaListImg = "img" + override val availableStates: Set = emptySet() + override val isMultipleTagsSupported = false - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - if (!query.isNullOrEmpty()) { - if (page > lastSearchPage) { - return emptyList() - } - val url = buildString { - append("https://") - append(domain) - append("/page/") - append(page) - append("/?s=") - append(query.urlEncoded()) - } - val docs = webClient.httpGet(url).parseHtml() - lastSearchPage = docs.selectFirst(".pagination .next") - ?.previousElementSibling() - ?.text()?.toIntOrNull() ?: 1 - return parseMangaList(docs) - } - if (!tags.isNullOrEmpty()) { - if (page > 1) { - return emptyList() - } - val tag = tags.oneOrThrowIfMany() - val url = buildString { - append("https://") - append(domain) - append("/genres/?genre=") - append(tag?.key.orEmpty()) - } - return parseMangaList(webClient.httpGet(url).parseHtml()) - } - if (page > 1) { - return emptyList() - } - val sortQuery = when (sortOrder) { - SortOrder.ALPHABETICAL -> "az" - SortOrder.NEWEST -> "new" - SortOrder.POPULARITY -> "views" - SortOrder.UPDATED -> "" - else -> "" - } + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) - append(listUrl) - append("/?order=") - append(sortQuery) + + when (filter) { + + is MangaListFilter.Search -> { + append("/page/") + append(page.toString()) + append("/?s=") + append(filter.query.urlEncoded()) + } + + is MangaListFilter.Advanced -> { + if (page > 1) { + return emptyList() + } + + if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + append("/genres/?genre=") + append(it.key) + } + } else { + append(listUrl) + append("/?order=") + append( + when (filter.sortOrder) { + SortOrder.ALPHABETICAL -> "az" + SortOrder.NEWEST -> "new" + SortOrder.POPULARITY -> "views" + SortOrder.UPDATED -> "" + else -> "" + }, + ) + } + } + + null -> { + append(listUrl) + } + } } return parseMangaList(webClient.httpGet(url).parseHtml()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/RizzComic.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/RizzComic.kt index c1eb518d..48ffe06c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/RizzComic.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/RizzComic.kt @@ -3,8 +3,9 @@ package org.koitharu.kotatsu.parsers.site.mangareader.en import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser import org.koitharu.kotatsu.parsers.util.domain @@ -19,31 +20,40 @@ internal class RizzComic(context: MangaLoaderContext) : override val datePattern = "dd MMM yyyy" override val listUrl = "/series" - override val availableSortOrders: Set - get() = EnumSet.of(SortOrder.ALPHABETICAL) + override val availableSortOrders: Set = EnumSet.of(SortOrder.ALPHABETICAL) + override val availableStates: Set = emptySet() + override val isMultipleTagsSupported = false - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() + // TODO Query created in json + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { if (page > 1) { return emptyList() } - if (!query.isNullOrEmpty()) { - throw IllegalArgumentException("Search is not supported by this source") - } - val url = if (!tags.isNullOrEmpty()) { - buildString { - append("https://") - append(domain) - append("/genre/") - append(tag?.key.orEmpty()) + val url = buildString { + append("https://") + append(domain) + when (filter) { + + is MangaListFilter.Search -> { + throw IllegalArgumentException("Search is not supported by this source") + } + + is MangaListFilter.Advanced -> { + + if (filter.tags.isNotEmpty()) { + append("/genre/") + filter.tags.oneOrThrowIfMany()?.let { + append(it.key) + } + } else { + append(listUrl) + } + } + + null -> { + append(listUrl) + } } - } else { - "https://$domain$listUrl" } return parseMangaList(webClient.httpGet(url).parseHtml()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/Zahard.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/Zahard.kt index ef1064ce..7d57f9a9 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/Zahard.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/Zahard.kt @@ -3,11 +3,13 @@ package org.koitharu.kotatsu.parsers.site.mangareader.en import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser import org.koitharu.kotatsu.parsers.util.domain +import org.koitharu.kotatsu.parsers.util.oneOrThrowIfMany import org.koitharu.kotatsu.parsers.util.parseHtml import org.koitharu.kotatsu.parsers.util.urlEncoded import java.util.* @@ -19,53 +21,34 @@ internal class Zahard(context: MangaLoaderContext) : override val listUrl = "/library" override val selectChapter = "#chapterlist > ul > a" override val selectPage = "div#chapter_imgs img" + override val availableSortOrders: Set = EnumSet.of(SortOrder.NEWEST) + override val availableStates: Set = emptySet() + override val isMultipleTagsSupported = false - - override val availableSortOrders: Set - get() = EnumSet.of(SortOrder.NEWEST) - - - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - if (!query.isNullOrEmpty()) { - if (page > lastSearchPage) { - return emptyList() - } - - val url = buildString { - append("https://") - append(domain) - append(listUrl) - append("?search=") - append(query.urlEncoded()) - append("&page=") - append(page) - } - - val docs = webClient.httpGet(url).parseHtml() - lastSearchPage = docs.selectFirst("a[rel=next]") - ?.previousElementSibling() - ?.text()?.toIntOrNull() ?: 1 - return parseMangaList(docs) - } - - val tagKey = "tag".urlEncoded() - val tagQuery = - if (tags.isNullOrEmpty()) "" else tags.joinToString(separator = "&", prefix = "&") { "$tagKey=${it.key}" } + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) append(listUrl) append("?page=") - append(page) - append(tagQuery) - + append(page.toString()) + when (filter) { + + is MangaListFilter.Search -> { + append("&search=") + append(filter.query.urlEncoded()) + } + + is MangaListFilter.Advanced -> { + filter.tags.oneOrThrowIfMany()?.let { + append("tag=") + append(it.key) + } + } + + null -> {} + } } - return parseMangaList(webClient.httpGet(url).parseHtml()) } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/TuManhwas.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/TuManhwas.kt index 06afc82b..d8e2af53 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/TuManhwas.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/TuManhwas.kt @@ -12,41 +12,41 @@ import java.util.ArrayList import java.util.Calendar import java.util.EnumSet -@MangaSourceParser("TU_MANHWAS", "TuManhwas", "es") +@MangaSourceParser("TU_MANHWAS", "TuManhwas.com", "es") internal class TuManhwas(context: MangaLoaderContext) : MangaReaderParser(context, MangaSource.TU_MANHWAS, "tumanhwas.com", 20, 20) { override val listUrl = "/biblioteca" override val selectPage = "div#readerarea img" - - - override val availableSortOrders: Set - get() = EnumSet.of(SortOrder.NEWEST) + override val availableSortOrders: Set = EnumSet.of(SortOrder.NEWEST) + override val availableStates: Set = emptySet() + override val isMultipleTagsSupported = false override suspend fun getAvailableTags(): Set = emptySet() - 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(listUrl) append("?page=") - append(page) - if (!tags.isNullOrEmpty()) { - append("&genero=") - append(tag?.key.orEmpty()) + append(page.toString()) + when (filter) { + + is MangaListFilter.Search -> { + append("&search=") + append(filter.query.urlEncoded()) + } + + is MangaListFilter.Advanced -> { + filter.tags.oneOrThrowIfMany()?.let { + append("&genero=") + append(it.key) + } + } + + null -> {} } - if (!query.isNullOrEmpty()) { - append("&search=") - append(query.urlEncoded()) - } - } - return parseMangaList(webClient.httpGet(url).parseHtml()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/fr/ManhwaFreakFr.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/fr/ManhwaFreakFr.kt index ac5a6929..ff49525b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/fr/ManhwaFreakFr.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/fr/ManhwaFreakFr.kt @@ -18,61 +18,52 @@ internal class ManhwaFreakFr(context: MangaLoaderContext) : override val selectMangaList = ".listupd .lastest-serie" override val selectMangaListImg = "img" override val sourceLocale: Locale = Locale.ENGLISH + override val availableStates: Set = emptySet() + override val isMultipleTagsSupported = false - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - if (!query.isNullOrEmpty()) { - if (page > lastSearchPage) { - return emptyList() - } - val url = buildString { - append("https://") - append(domain) - append("/page/") - append(page) - append("/?s=") - append(query.urlEncoded()) - } - val docs = webClient.httpGet(url).parseHtml() - lastSearchPage = docs.selectFirst(".pagination .next") - ?.previousElementSibling() - ?.text()?.toIntOrNull() ?: 1 - return parseMangaList(docs) - } - if (!tags.isNullOrEmpty()) { - - if (page > 1) { - return emptyList() - } - val tag = tags.oneOrThrowIfMany() - val url = buildString { - append("https://") - append(domain) - append("/genres/?genre=") - append(tag?.key.orEmpty()) - } - return parseMangaList(webClient.httpGet(url).parseHtml()) - } - if (page > 1) { - return emptyList() - } - val sortQuery = when (sortOrder) { - SortOrder.ALPHABETICAL -> "az" - SortOrder.NEWEST -> "new" - SortOrder.POPULARITY -> "views" - SortOrder.UPDATED -> "" - else -> "" - } + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) - append(listUrl) - append("/?order=") - append(sortQuery) + + when (filter) { + + is MangaListFilter.Search -> { + append("/page/") + append(page.toString()) + append("/?s=") + append(filter.query.urlEncoded()) + } + + is MangaListFilter.Advanced -> { + if (page > 1) { + return emptyList() + } + + if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + append("/genres/?genre=") + append(it.key) + } + } else { + append(listUrl) + append("/?order=") + append( + when (filter.sortOrder) { + SortOrder.ALPHABETICAL -> "az" + SortOrder.NEWEST -> "new" + SortOrder.POPULARITY -> "views" + SortOrder.UPDATED -> "" + else -> "" + }, + ) + } + } + + null -> { + append(listUrl) + } + } } return parseMangaList(webClient.httpGet(url).parseHtml()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/CosmicScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/CosmicScans.kt index 52526dc1..f979bece 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/CosmicScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/CosmicScans.kt @@ -3,13 +3,16 @@ package org.koitharu.kotatsu.parsers.site.mangareader.id import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser import org.koitharu.kotatsu.parsers.util.domain +import org.koitharu.kotatsu.parsers.util.oneOrThrowIfMany import org.koitharu.kotatsu.parsers.util.parseHtml import org.koitharu.kotatsu.parsers.util.urlEncoded +import java.util.EnumSet import java.util.Locale @MangaSourceParser("COSMIC_SCANS", "CosmicScans.id", "id") @@ -17,54 +20,50 @@ internal class CosmicScans(context: MangaLoaderContext) : MangaReaderParser(context, MangaSource.COSMIC_SCANS, "cosmicscans.id", pageSize = 30, searchPageSize = 30) { override val sourceLocale: Locale = Locale.ENGLISH + override val availableSortOrders: Set = EnumSet.of(SortOrder.UPDATED) + override val availableStates: Set = emptySet() + override val isMultipleTagsSupported = false override val listUrl = "/semua-komik" - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - if (!query.isNullOrEmpty()) { - if (page > lastSearchPage) { - return emptyList() - } - val url = buildString { - append("https://") - append(domain) - append("/page/") - append(page) - append("/?s=") - append(query.urlEncoded()) - } - val docs = webClient.httpGet(url).parseHtml() - lastSearchPage = docs.selectFirst(".pagination .next") - ?.previousElementSibling() - ?.text()?.toIntOrNull() ?: 1 - return parseMangaList(docs) - } - if (page > 1) { - return emptyList() - } - val sortQuery = when (sortOrder) { - SortOrder.ALPHABETICAL -> "title" - SortOrder.NEWEST -> "latest" - SortOrder.POPULARITY -> "popular" - SortOrder.UPDATED -> "update" - else -> "" - } - val tagKey = "genre[]".urlEncoded() - val tagQuery = - if (tags.isNullOrEmpty()) "" else tags.joinToString(separator = "&", prefix = "&") { "$tagKey=${it.key}" } + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) - append(listUrl) - append("/?order=") - append(sortQuery) - append(tagQuery) - append("&page=") - append(page) + + when (filter) { + + is MangaListFilter.Search -> { + append("/page/") + append(page.toString()) + append("/?s=") + append(filter.query.urlEncoded()) + } + + is MangaListFilter.Advanced -> { + if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + append("/genres/") + append(it.key) + append("/page/") + append(page.toString()) + append('/') + } + } else { + if (page > 1) { + return emptyList() + } + append(listUrl) + } + + } + + null -> { + if (page > 1) { + return emptyList() + } + append(listUrl) + } + } } return parseMangaList(webClient.httpGet(url).parseHtml()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/KomikSan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/KomikSan.kt index 10efd3fa..9074f62c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/KomikSan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/KomikSan.kt @@ -3,11 +3,13 @@ package org.koitharu.kotatsu.parsers.site.mangareader.id import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser import org.koitharu.kotatsu.parsers.util.domain +import org.koitharu.kotatsu.parsers.util.oneOrThrowIfMany import org.koitharu.kotatsu.parsers.util.parseHtml import org.koitharu.kotatsu.parsers.util.urlEncoded @@ -19,50 +21,61 @@ internal class KomikSan(context: MangaLoaderContext) : override val listUrl = "/list" override val datePattern = "MMM d, yyyy" - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - if (!query.isNullOrEmpty()) { - if (page > lastSearchPage) { - return emptyList() - } - - val url = buildString { - append("https://") - append(domain) - append("/search?search=") - append(query.urlEncoded()) - append("&page=") - append(page) - } - val docs = webClient.httpGet(url).parseHtml() - lastSearchPage = docs.selectFirst(".pagination .next") - ?.previousElementSibling() - ?.text()?.toIntOrNull() ?: 1 - return parseMangaList(docs) - } - val sortQuery = when (sortOrder) { - SortOrder.ALPHABETICAL -> "title" - SortOrder.NEWEST -> "latest" - SortOrder.POPULARITY -> "popular" - SortOrder.UPDATED -> "update" - else -> "" - } - val tagKey = "genre[]".urlEncoded() - val tagQuery = - if (tags.isNullOrEmpty()) "" else tags.joinToString(separator = "&", prefix = "&") { "$tagKey=${it.key}" } + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) - append(listUrl) - append("/?order=") - append(sortQuery) - append(tagQuery) - append("&page=") - append(page) + when (filter) { + + is MangaListFilter.Search -> { + append("/search?search=") + append(filter.query.urlEncoded()) + append("&page=") + append(page.toString()) + } + + is MangaListFilter.Advanced -> { + append(listUrl) + + append("/?order=") + append( + when (filter.sortOrder) { + SortOrder.ALPHABETICAL -> "title" + SortOrder.NEWEST -> "latest" + SortOrder.POPULARITY -> "popular" + SortOrder.UPDATED -> "update" + else -> "" + }, + ) + + val tagKey = "genre[]".urlEncoded() + val tagQuery = + if (filter.tags.isEmpty()) "" + else filter.tags.joinToString(separator = "&", prefix = "&") { "$tagKey=${it.key}" } + append(tagQuery) + + if (filter.states.isNotEmpty()) { + filter.states.oneOrThrowIfMany()?.let { + append("&status=") + when (it) { + MangaState.ONGOING -> append("ongoing") + MangaState.FINISHED -> append("completed") + MangaState.PAUSED -> append("hiatus") + else -> append("") + } + } + } + + append("&page=") + append(page.toString()) + } + + null -> { + append(listUrl) + append("/?order=update&page=") + append(page.toString()) + } + } } return parseMangaList(webClient.httpGet(url).parseHtml()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/Komikcast.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/Komikcast.kt index 63fd834f..989284df 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/Komikcast.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/Komikcast.kt @@ -13,55 +13,70 @@ import java.util.* @MangaSourceParser("KOMIKCAST", "KomikCast", "id") internal class Komikcast(context: MangaLoaderContext) : - MangaReaderParser(context, MangaSource.KOMIKCAST, "komikcast.ch", pageSize = 60, searchPageSize = 28) { + MangaReaderParser(context, MangaSource.KOMIKCAST, "komikcast.lol", pageSize = 60, searchPageSize = 28) { override val listUrl = "/daftar-komik" override val datePattern = "MMM d, yyyy" override val sourceLocale: Locale = Locale.ENGLISH - override val availableSortOrders: Set - get() = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.ALPHABETICAL) - - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - if (!query.isNullOrEmpty()) { - if (page > lastSearchPage) { - return emptyList() - } - val url = buildString { - append("https://") - append(domain) - append("/page/") - append(page) - append("/?s=") - append(query.urlEncoded()) - } - val docs = webClient.httpGet(url).parseHtml() - lastSearchPage = docs.selectFirst(".pagination .next") - ?.previousElementSibling() - ?.text()?.toIntOrNull() ?: 1 - return parseMangaList(docs) - } - val tagKey = "genre[]".urlEncoded() - val tagQuery = - if (tags.isNullOrEmpty()) "" else tags.joinToString(separator = "&", prefix = "&") { "$tagKey=${it.key}" } + override val availableSortOrders: Set = + EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.ALPHABETICAL) + 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(listUrl) - append("/page/") - append(page) - when (sortOrder) { - SortOrder.ALPHABETICAL -> append("/?orderby=titleasc") - SortOrder.POPULARITY -> append("/?orderby=popular") - SortOrder.UPDATED -> append("/?sortby=update") - else -> append("/?sortby=update") + + when (filter) { + + is MangaListFilter.Search -> { + append("/page/") + append(page.toString()) + append("/?s=") + append(filter.query.urlEncoded()) + } + + is MangaListFilter.Advanced -> { + append(listUrl) + append("/page/") + append(page.toString()) + + append("/?orderby=") + append( + when (filter.sortOrder) { + SortOrder.ALPHABETICAL -> append("titleasc") + SortOrder.POPULARITY -> append("popular") + SortOrder.UPDATED -> append("update") + else -> append("update") + }, + ) + + val tagKey = "genre[]".urlEncoded() + val tagQuery = + if (filter.tags.isEmpty()) "" + else filter.tags.joinToString(separator = "&", prefix = "&") { "$tagKey=${it.key}" } + append(tagQuery) + + if (filter.states.isNotEmpty()) { + filter.states.oneOrThrowIfMany()?.let { + append("&status=") + when (it) { + MangaState.ONGOING -> append("Ongoing") + MangaState.FINISHED -> append("Completed") + else -> append("") + } + } + } + } + + null -> { + append(listUrl) + append("/?orderby=update&page=") + append(page.toString()) + } } - append(tagQuery) } + return parseMangaList(webClient.httpGet(url).parseHtml()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/MmrcmsParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/MmrcmsParser.kt index 86385d29..9d025b62 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/MmrcmsParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/MmrcmsParser.kt @@ -55,87 +55,114 @@ internal abstract class MmrcmsParser( "مكتملة", ) + override val isMultipleTagsSupported = false + protected open val imgUpdated = "/cover/cover_250x350.jpg" - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() - val url = if (!query.isNullOrEmpty() || !tags.isNullOrEmpty() || sortOrder != SortOrder.UPDATED) { - buildString { - append("https://") - append(domain) - append("/$listUrl/") - append("?page=") - append(page.toString()) - append("&asc=true&author=&tag=") - append("&alpha=") - if (!query.isNullOrEmpty()) { - append(query.urlEncoded()) - } - append("&cat=") - if (!tags.isNullOrEmpty()) { - append(tag?.key.orEmpty()) + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + + when (filter) { + + is MangaListFilter.Search -> { + val url = buildString { + append("https://") + append(domain) + append('/') + append(listUrl) + append("/?page=") + append(page.toString()) + append("&asc=true&author=&tag=&alpha=") + append(filter.query.urlEncoded()) + append("&cat=&sortBy=views") } - append("&sortBy=") - when (sortOrder) { - SortOrder.POPULARITY -> append("views") - SortOrder.ALPHABETICAL -> append("name") - else -> append("name") + return parseMangaList(webClient.httpGet(url).parseHtml()) + } + + is MangaListFilter.Advanced -> { + + if (filter.sortOrder == SortOrder.UPDATED) { + val url = buildString { + append("https://") + append(domain) + append("/latest-release?page=") + append(page.toString()) + } + return parseMangaListUpdated(webClient.httpGet(url).parseHtml()) + + } else { + val url = buildString { + append("https://") + append(domain) + append('/') + append(listUrl) + append("/?page=") + append(page.toString()) + append("&asc=true&author=&tag=&alpha=&cat=") + filter.tags.oneOrThrowIfMany()?.let { + append(it.key) + } + append("&sortBy=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("views") + SortOrder.ALPHABETICAL -> append("name") + else -> append("name") + } + } + return parseMangaList(webClient.httpGet(url).parseHtml()) } } - } else { - buildString { - append("https://") - append(domain) - append("/latest-release") - append("?page=") - append(page.toString()) + + null -> { + val url = buildString { + append("https://") + append(domain) + append("/latest-release?page=") + append(page.toString()) + } + return parseMangaList(webClient.httpGet(url).parseHtml()) } } - val doc = webClient.httpGet(url).parseHtml() - if (!query.isNullOrEmpty() || !tags.isNullOrEmpty() || sortOrder != SortOrder.UPDATED) { - return doc.select("div.media").map { div -> - val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") - Manga( - id = generateUid(href), - url = href, - publicUrl = href.toAbsoluteUrl(div.host ?: domain), - coverUrl = div.selectFirst("img")?.src().orEmpty(), - title = div.selectFirstOrThrow("div.media-body h5").text().orEmpty(), - altTitle = null, - rating = div.selectFirstOrThrow("span").ownText().toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN, - tags = emptySet(), - author = null, - state = null, - source = source, - isNsfw = isNsfwSource, - ) - } - } else { - return doc.select("div.manga-item").map { div -> - val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") - val deeplink = href.substringAfterLast("/") - Manga( - id = generateUid(href), - url = href, - publicUrl = href.toAbsoluteUrl(div.host ?: domain), - coverUrl = "https://$domain/uploads/manga/$deeplink$imgUpdated", - title = div.selectFirstOrThrow("h3 a").text().orEmpty(), - altTitle = null, - rating = RATING_UNKNOWN, - tags = emptySet(), - author = null, - state = null, - source = source, - isNsfw = isNsfwSource, - ) - } + } + + protected open fun parseMangaList(doc: Document): List { + return doc.select("div.media").map { div -> + val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") + Manga( + id = generateUid(href), + url = href, + publicUrl = href.toAbsoluteUrl(div.host ?: domain), + coverUrl = div.selectFirst("img")?.src().orEmpty(), + title = div.selectFirstOrThrow("div.media-body h5").text().orEmpty(), + altTitle = null, + rating = div.selectFirstOrThrow("span").ownText().toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN, + tags = emptySet(), + author = null, + state = null, + source = source, + isNsfw = isNsfwSource, + ) } + } + protected open fun parseMangaListUpdated(doc: Document): List { + return doc.select("div.manga-item").map { div -> + val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") + val deeplink = href.substringAfterLast("/") + Manga( + id = generateUid(href), + url = href, + publicUrl = href.toAbsoluteUrl(div.host ?: domain), + coverUrl = "https://$domain/uploads/manga/$deeplink$imgUpdated", + title = div.selectFirstOrThrow("h3 a").text().orEmpty(), + altTitle = null, + rating = RATING_UNKNOWN, + tags = emptySet(), + author = null, + state = null, + source = source, + isNsfw = isNsfwSource, + ) + } } override suspend fun getAvailableTags(): Set { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/ar/Onma.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/ar/Onma.kt index 5101e05a..787f2bcb 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/ar/Onma.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/ar/Onma.kt @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.parsers.site.mmrcms.ar import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope +import org.jsoup.nodes.Document import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.model.* @@ -19,83 +20,24 @@ internal class Onma(context: MangaLoaderContext) : override val selectAut = "h3:contains(المؤلف) .text" override val selectTag = "h3:contains(التصنيفات) .text" - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() - val url = if (sortOrder == SortOrder.UPDATED) { - buildString { - append("https://") - append(domain) - append("/latest-release") - append("?page=") - append(page.toString()) - } - } else { - buildString { - append("https://") - append(domain) - append("/$listUrl/") - append("?page=") - append(page.toString()) - append("&asc=true&author=&tag=") - append("&alpha=") - if (!query.isNullOrEmpty()) { - append(query.urlEncoded()) - } - append("&cat=") - if (!tags.isNullOrEmpty()) { - append(tag?.key.orEmpty()) - } - append("&sortBy=") - when (sortOrder) { - SortOrder.POPULARITY -> append("views") - SortOrder.ALPHABETICAL -> append("name") - else -> append("views") - } - } - } - val doc = webClient.httpGet(url).parseHtml() - if (sortOrder == SortOrder.UPDATED) { - return doc.select("div.manga-item").map { div -> - val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") - val deeplink = href.substringAfterLast('/') - Manga( - id = generateUid(href), - url = href, - publicUrl = href.toAbsoluteUrl(div.host ?: domain), - coverUrl = "https://$domain/uploads/manga/$deeplink$imgUpdated", - title = div.selectFirstOrThrow("div.content-left a").text().orEmpty(), - altTitle = null, - rating = RATING_UNKNOWN, - tags = emptySet(), - author = null, - state = null, - source = source, - isNsfw = isNsfwSource, - ) - } - } else { - return doc.select("div.chapter-container").map { div -> - val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") - Manga( - id = generateUid(href), - url = href, - publicUrl = href.toAbsoluteUrl(div.host ?: domain), - coverUrl = div.selectFirst("img")?.src().orEmpty(), - title = div.selectFirstOrThrow("h5.media-heading").text().orEmpty(), - altTitle = null, - rating = div.selectFirstOrThrow("span").ownText().toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN, - tags = emptySet(), - author = null, - state = null, - source = source, - isNsfw = isNsfwSource, - ) - } + + override fun parseMangaList(doc: Document): List { + return doc.select("div.chapter-container").map { div -> + val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") + Manga( + id = generateUid(href), + url = href, + publicUrl = href.toAbsoluteUrl(div.host ?: domain), + coverUrl = div.selectFirst("img")?.src().orEmpty(), + title = div.selectFirstOrThrow("h5.media-heading").text().orEmpty(), + altTitle = null, + rating = div.selectFirstOrThrow("span").ownText().toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN, + tags = emptySet(), + author = null, + state = null, + source = source, + isNsfw = isNsfwSource, + ) } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/id/Mangaid.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/id/Mangaid.kt index dff1955a..f0a0bfa7 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/id/Mangaid.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/id/Mangaid.kt @@ -1,18 +1,14 @@ package org.koitharu.kotatsu.parsers.site.mmrcms.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.mmrcms.MmrcmsParser import java.util.Locale - @MangaSourceParser("MANGAID", "MangaId", "id") internal class Mangaid(context: MangaLoaderContext) : MmrcmsParser(context, MangaSource.MANGAID, "mangaid.click") { - - override val selectState = "dt:contains(Status)" override val selectAlt = "dt:contains(Other names)" override val selectAut = "dt:contains(Author(s))" diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/nepnep/NepnepParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/nepnep/NepnepParser.kt index 2845daec..f175cbe1 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/nepnep/NepnepParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/nepnep/NepnepParser.kt @@ -24,15 +24,20 @@ internal abstract class NepnepParser( override val configKeyDomain = ConfigKey.Domain(domain) override val availableSortOrders: Set = EnumSet.of(SortOrder.ALPHABETICAL) + override val availableStates: Set = EnumSet.allOf(MangaState::class.java) override val headers: Headers = Headers.Builder() .add("User-Agent", UserAgents.CHROME_DESKTOP) .build() - 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() } + + var foundTag = true + var foundState = true + val doc = webClient.httpGet("https://$domain/search/").parseHtml() val json = JSONArray( doc.selectFirstOrThrow("script:containsData(MainFunction)").data() @@ -41,18 +46,18 @@ internal abstract class NepnepParser( .trim() .replace(';', ' '), ) - - val manga = ArrayList(json.length()) + for (i in 0 until json.length()) { val m = json.getJSONObject(i) val href = "/manga/" + m.getString("i") val imgUrl = "https://temp.compsci88.com/cover/" + m.getString("i") + ".jpg" - when { - !query.isNullOrEmpty() -> { - if (m.getString("s").contains(query, ignoreCase = true) || m.getString("al") - .contains(query, ignoreCase = true) + when (filter) { + + is MangaListFilter.Search -> { + if (m.getString("s").contains(filter.query, ignoreCase = true) || m.getString("al") + .contains(filter.query, ignoreCase = true) ) { manga.add( addManga(href, imgUrl, m), @@ -60,29 +65,49 @@ internal abstract class NepnepParser( } } - !tags.isNullOrEmpty() -> { - val a = m.getJSONArray("g").toString() - var found = true - tags.forEach { - if (!a.contains(it.key, ignoreCase = true)) { - found = false + is MangaListFilter.Advanced -> { + + if (filter.tags.isNotEmpty()) { + val tagsJon = m.getJSONArray("g").toString() + filter.tags.forEach { + foundTag = false + if (tagsJon.contains(it.key, ignoreCase = true)) { + foundTag = true + } } } - if (found) { - manga.add( - addManga(href, imgUrl, m), - ) + + if (filter.states.isNotEmpty()) { + val stateJson = m.getString("ps") + filter.states.oneOrThrowIfMany().let { + foundState = false + if (stateJson.contains( + when (it) { + MangaState.ONGOING -> "Ongoing" + MangaState.FINISHED -> "Complete" + MangaState.ABANDONED -> "Cancelled" + MangaState.PAUSED -> "Hiatus" + else -> "" + }, + ignoreCase = true, + ) + ) { + foundState = true + } + } + } + + if (foundTag && foundState) { + manga.add(addManga(href, imgUrl, m)) } } - else -> { + null -> { manga.add( addManga(href, imgUrl, m), ) } } - - } return manga } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/otakusanctuary/OtakuSanctuaryParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/otakusanctuary/OtakuSanctuaryParser.kt index b4d6918c..8d454c84 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/otakusanctuary/OtakuSanctuaryParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/otakusanctuary/OtakuSanctuaryParser.kt @@ -25,7 +25,7 @@ internal abstract class OtakuSanctuaryParser( SortOrder.NEWEST, ) - protected open val listeurl = "Manga/Newest" + protected open val listUrl = "Manga/Newest" protected open val datePattern = "dd/MM/yyyy" protected open val lang = "" @@ -40,60 +40,71 @@ internal abstract class OtakuSanctuaryParser( "Done", ) - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() - - val doc = if (!tags.isNullOrEmpty()) { - val url = buildString { - - append("https://$domain/Genre/MangaGenrePartial?id=") - append(tag?.key.orEmpty()) - append("&lang=$lang") - append("&offset=") - append(page * pageSize) - append("&pagesize=$pageSize") - } + override val isMultipleTagsSupported = false + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + val doc = + when (filter) { + + is MangaListFilter.Search -> { + if (page > 1) { + return emptyList() + } + val url = buildString { + append("https://") + append(domain) + append("/Home/Search?search=") + append(filter.query.urlEncoded()) + } + webClient.httpGet(url).parseHtml().requireElementById("collection-manga") + } - webClient.httpGet(url).parseHtml() - } else if (!query.isNullOrEmpty()) { - if (page > 1) { - return emptyList() - } + is MangaListFilter.Advanced -> { + + if (filter.tags.isNotEmpty()) { + val url = buildString { + append("https://") + append(domain) + append("/Genre/MangaGenrePartial?id=") + filter.tags.oneOrThrowIfMany()?.let { + append(it.key) + } + append("&lang=") + append(lang) + append("&offset=") + append(page * pageSize) + append("&pagesize=") + append(pageSize) + } + webClient.httpGet(url).parseHtml() + + } else { + val payload = HashMap() + payload["Lang"] = lang + payload["Page"] = page.toString() + payload["Type"] = "Include" + when (filter.sortOrder) { + SortOrder.NEWEST -> payload["Dir"] = "CreatedDate" + SortOrder.UPDATED -> payload["Dir"] = "NewPostedDate" + else -> payload["Dir"] = "NewPostedDate" + } + webClient.httpPost("https://$domain/$listUrl", payload).parseHtml() + } - val url = buildString { - append("https://$domain/Home/Search?search=") - append(query.urlEncoded()) - } + } - webClient.httpGet(url).parseHtml() - } else { - val url = "https://$domain/$listeurl" - val payload = HashMap() - payload["Lang"] = lang - payload["Page"] = page.toString() - payload["Type"] = "Include" - when (sortOrder) { - SortOrder.NEWEST -> payload["Dir"] = "CreatedDate" - SortOrder.UPDATED -> payload["Dir"] = "NewPostedDate" - else -> payload["Dir"] = "NewPostedDate" + null -> { + val payload = HashMap() + payload["Lang"] = lang + payload["Page"] = page.toString() + payload["Type"] = "Include" + payload["Dir"] = "NewPostedDate" + webClient.httpPost("https://$domain/$listUrl", payload).parseHtml() + } } - webClient.httpPost(url, payload).parseHtml() - } - - - val mangas = if (!query.isNullOrEmpty()) { - doc.requireElementById("collection-manga").select("div.picture-card") - } else { - doc.select("div.picture-card") - } - return mangas.map { div -> + return doc.select("div.picture-card").map { div -> val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") Manga( id = generateUid(href), diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/Bakai.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/Bakai.kt index aee33c96..3ecd17ad 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/Bakai.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/Bakai.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.parsers.site.pt import okhttp3.Headers +import org.jsoup.nodes.Document import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.PagedMangaParser @@ -16,79 +17,102 @@ internal class Bakai(context: MangaLoaderContext) : PagedMangaParser(context, Ma override val availableSortOrders: Set = EnumSet.of(SortOrder.UPDATED) override val configKeyDomain = ConfigKey.Domain("bakai.org") override val headers: Headers = Headers.Builder() - .add("User-Agent", UserAgents.CHROME_MOBILE) + .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("&quick=1&type=cms_records1&page=") - append(page.toString()) - } else if (!tags.isNullOrEmpty()) { - append("/search/?tags=") - for (tag in tags) { - append(tag.key) - append(",") + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + + when (filter) { + + is MangaListFilter.Search -> { + val url = buildString { + append("https://") + append(domain) + append("/search1/?q=") + append(filter.query.urlEncoded()) + append("&quick=1&type=cms_records1&updated_after=any&sortby=newest&page=") + append(page.toString()) } - append("&quick=1&type=cms_records1&page=") - append(page.toString()) - } else { - append("/hentai/") - append("page/") - append(page.toString()) + return parseMangaListQueryOrTags(webClient.httpGet(url).parseHtml()) } - } - val doc = webClient.httpGet(url).parseHtml() - if (!tags.isNullOrEmpty() or !query.isNullOrEmpty()) { - return doc.select("ol.ipsStream li.ipsStreamItem") - .map { div -> - val href = div.selectFirstOrThrow("div.ipsStreamItem_snippet a").attrAsRelativeUrl("href") - Manga( - id = generateUid(href), - title = div.selectFirstOrThrow("h2.ipsStreamItem_title").text(), - altTitle = null, - url = href, - publicUrl = href.toAbsoluteUrl(domain), - rating = RATING_UNKNOWN, - isNsfw = true, - coverUrl = div.selectFirstOrThrow("span.ipsThumb img").attrAsAbsoluteUrl("src"), - tags = setOf(), - state = null, - author = null, - source = source, - ) + + is MangaListFilter.Advanced -> { + if (filter.tags.isNotEmpty()) { + val url = buildString { + append("https://") + append(domain) + append("/search1/?tags=") + append(filter.tags.joinToString(separator = ",") { it.key }) + append("&updated_after=any&sortby=newest&search_and_or=and&page=") + append(page.toString()) + } + return parseMangaListQueryOrTags(webClient.httpGet(url).parseHtml()) + } else { + val url = buildString { + append("https://") + append(domain) + append("/hentai/page/") + append(page.toString()) + } + return parseMangaList(webClient.httpGet(url).parseHtml()) } - } else { - return doc.select("section.ipsType_normal li.ipsGrid_span4") - .map { div -> - val href = div.selectFirstOrThrow("h2.ipsType_pageTitle a").attrAsRelativeUrl("href") - Manga( - id = generateUid(href), - title = div.selectFirstOrThrow("h2.ipsType_pageTitle").text(), - altTitle = null, - url = href, - publicUrl = href.toAbsoluteUrl(domain), - rating = RATING_UNKNOWN, - isNsfw = true, - coverUrl = div.selectFirstOrThrow("img").attrAsAbsoluteUrl("src"), - tags = setOf(), - state = null, - author = null, - source = source, - ) + } + + null -> { + val url = buildString { + append("https://") + append(domain) + append("/hentai/page/") + append(page.toString()) } + return parseMangaList(webClient.httpGet(url).parseHtml()) + } } } + private fun parseMangaList(doc: Document): List { + return doc.select("section.ipsType_normal li.ipsGrid_span4") + .map { div -> + val href = div.selectFirstOrThrow("h2.ipsType_pageTitle a").attrAsRelativeUrl("href") + Manga( + id = generateUid(href), + title = div.selectFirstOrThrow("h2.ipsType_pageTitle").text(), + altTitle = null, + url = href, + publicUrl = href.toAbsoluteUrl(domain), + rating = RATING_UNKNOWN, + isNsfw = true, + coverUrl = div.selectFirst("img")?.src().orEmpty(), + tags = setOf(), + state = null, + author = null, + source = source, + ) + } + } + + private fun parseMangaListQueryOrTags(doc: Document): List { + return doc.select("ol.ipsStream li.ipsStreamItem") + .mapNotNull { div -> + val href = + div.selectFirst(".ipsStreamItem_snippet a")?.attrAsRelativeUrl("href") ?: return@mapNotNull null + Manga( + id = generateUid(href), + title = div.selectFirstOrThrow("h2.ipsStreamItem_title").text(), + altTitle = null, + url = href, + publicUrl = href.toAbsoluteUrl(domain), + rating = RATING_UNKNOWN, + isNsfw = true, + coverUrl = div.selectFirst(".ipsStreamItem_snippet img")?.src().orEmpty(), + tags = setOf(), + state = null, + author = null, + source = source, + ) + } + } + override suspend fun getAvailableTags(): Set { val doc = webClient.httpGet("https://$domain").parseHtml() return doc.requireElementById("elNavigation_17_menu").select("li.ipsMenu_item a").mapNotNullToSet { a -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/BrMangas.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/BrMangas.kt index 688b63bb..f51e1d62 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/BrMangas.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/BrMangas.kt @@ -17,51 +17,78 @@ internal class BrMangas(context: MangaLoaderContext) : PagedMangaParser(context, override val configKeyDomain = ConfigKey.Domain("www.brmangas.net") + override val isMultipleTagsSupported = false + override val headers: Headers = Headers.Builder() .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("category/") - append(tag?.key.orEmpty()) - if (page > 1) { - append("/page/$page/") - } - } else if (!query.isNullOrEmpty()) { - if (page > 1) { - append("/page/$page/") + append("https://") + append(domain) + append('/') + when (filter) { + is MangaListFilter.Search -> { + if (page > 1) { + append("/page/$page/") + } + append("/?s=") + append(filter.query.urlEncoded()) } - append("/?s=") - append(query.urlEncoded()) - } else { - when (sortOrder) { - SortOrder.POPULARITY -> append("/") - SortOrder.UPDATED -> append("manga/") - else -> append("manga/") + + is MangaListFilter.Advanced -> { + if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + append("category/") + append(it.key) + if (page > 1) { + append("/page/$page/") + } + } + } else { + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("/") + SortOrder.UPDATED -> append("manga/") + else -> append("manga/") + } + if (page > 1) { + append("page/$page/") + } + } + } - if (page > 1) { - append("page/$page/") + + null -> { + append("manga/") + if (page > 1) { + append("page/$page/") + } } } + } val doc = webClient.httpGet(url).parseHtml() - val item = if (sortOrder == SortOrder.POPULARITY) { - doc.select("div.listagem")[1].select("div.item") // To remove the 6 mangas updated on the home page - } else { - doc.select("div.listagem div.item") - } + val item = + when (filter) { + is MangaListFilter.Search -> { + doc.select("div.listagem div.item") + } + + is MangaListFilter.Advanced -> { + if (filter.sortOrder == SortOrder.POPULARITY && filter.tags.isEmpty()) { + doc.select("div.listagem")[1].select("div.item") // To remove the 6 mangas updated on the home page + } else { + doc.select("div.listagem div.item") + } + } + + null -> doc.select("div.listagem div.item") + + } return item.map { div -> val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") Manga( diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LerMangaOnline.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LerMangaOnline.kt index 2f548ea3..9a8e824f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LerMangaOnline.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LerMangaOnline.kt @@ -7,7 +7,6 @@ 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 java.lang.IllegalArgumentException import java.text.SimpleDateFormat import java.util.* @@ -18,30 +17,48 @@ class LerMangaOnline(context: MangaLoaderContext) : PagedMangaParser(context, Ma override val configKeyDomain = ConfigKey.Domain("lermangaonline.com.br") - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - if (!query.isNullOrEmpty()) { - throw IllegalArgumentException("Search is not supported by this source") - } - val tag = tags.oneOrThrowIfMany() + override val isMultipleTagsSupported = false + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + val url = buildString { append("https://") append(domain) - append("/") - if (!tags.isNullOrEmpty()) { - append(tag?.key.orEmpty()) - append("/") - } - if (page > 1) { - append("page/") - append(page) - append("/") + append('/') + when (filter) { + is MangaListFilter.Search -> { + if (page > 1) { + append("page/") + append(page.toString()) + append("/") + } + append("?s=") + append(filter.query.urlEncoded()) + } + + is MangaListFilter.Advanced -> { + filter.tags.oneOrThrowIfMany()?.let { + append(it.key) + append('/') + } + + if (page > 1) { + append("page/") + append(page.toString()) + append('/') + } + } + + null -> { + if (page > 1) { + append("page/") + append(page.toString()) + append('/') + } + } } } + return parseManga(webClient.httpGet(url).parseHtml()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MangaOnline.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MangaOnline.kt index dd41b478..c7f4a735 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MangaOnline.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MangaOnline.kt @@ -16,37 +16,40 @@ class MangaOnline(context: MangaLoaderContext) : PagedMangaParser(context, Manga override val configKeyDomain = ConfigKey.Domain("mangaonline.biz") - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() + override val isMultipleTagsSupported = false + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + val url = buildString { append("https://") append(domain) - when { - !tags.isNullOrEmpty() -> { - append("/genero/") - append(tag?.key.orEmpty()) - append("/") - } + when (filter) { - !query.isNullOrEmpty() -> { + is MangaListFilter.Search -> { append("/search/") - append(query.urlEncoded()) - append("/") + append(filter.query.urlEncoded()) + append('/') } - else -> { - append("/manga/") + is MangaListFilter.Advanced -> { + if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + append("/genero/") + append(it.key) + append('/') + } + } else { + append("/manga/") + } } + + null -> append("/manga/") + } if (page > 1) { append("page/") - append(page) - append("/") + append(page.toString()) + append('/') } } val doc = webClient.httpGet(url).parseHtml() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/YugenMangas.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/YugenMangas.kt index 55564083..3dc110c3 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/YugenMangas.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/YugenMangas.kt @@ -8,7 +8,6 @@ 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 org.koitharu.kotatsu.parsers.util.json.mapJSONIndexed import java.text.DateFormat import java.text.SimpleDateFormat import java.util.* @@ -19,44 +18,71 @@ class YugenMangas(context: MangaLoaderContext) : PagedMangaParser(context, Manga override val availableSortOrders: Set = EnumSet.of(SortOrder.ALPHABETICAL, SortOrder.UPDATED) override val configKeyDomain = ConfigKey.Domain("yugenmangas.net.br") - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + + if (page > 1) { + return emptyList() + } + val json = - if (!query.isNullOrEmpty()) { - if (page > 1) { - return emptyList() - } - val url = buildString { - append("https://api.") - append(domain) - append("/api/series/list/?query=") - append(query.urlEncoded()) + when (filter) { + + is MangaListFilter.Search -> { + + val url = buildString { + append("https://api.") + append(domain) + append("/api/series/list/?query=") + append(filter.query.urlEncoded()) + } + webClient.httpGet(url).parseJsonArray() } - webClient.httpGet(url).parseJsonArray() - } else { - if (page > 1) { - return emptyList() + + is MangaListFilter.Advanced -> { + + + if (filter.sortOrder == SortOrder.UPDATED) { + val url = buildString { + append("https://api.") + append(domain) + append("/api/latest_updates/") + } + webClient.httpGet(url).parseJsonArray() + } else { + val url = buildString { + append("https://api.") + append(domain) + append("/api/all_series/") + } + webClient.httpGet(url).parseJson().getJSONArray("series") + } + } - val url = buildString { - append("https://api.") - append(domain) - append("/api/all_series/?page=1") + + null -> { + val url = buildString { + append("https://api.") + append(domain) + append("/api/latest_updates/") + } + webClient.httpGet(url).parseJsonArray() } - webClient.httpGet(url).parseJson().getJSONArray("series") } return json.mapJSON { j -> val slug = j.getString("slug") + val cover = if (!j.getString("cover").startsWith("https://")) { + // Some covers don't have the "/" so we ensure that the URL will be spelled correctly. + "https://$domain/media/" + j.getString("cover").removePrefix("/") + } else { + j.getString("cover") + } Manga( id = generateUid(slug), url = slug, publicUrl = slug, title = j.getString("name"), - coverUrl = j.getString("cover"), + coverUrl = cover, altTitle = null, rating = RATING_UNKNOWN, tags = emptySet(), @@ -90,7 +116,7 @@ class YugenMangas(context: MangaLoaderContext) : PagedMangaParser(context, Manga else -> null } }, - chapters = chapterManga.mapJSONIndexed { i, j -> + chapters = chapterManga.mapJSON { j -> val url = "https://api.$domain/api/serie/${manga.url}/chapter/${j.getString("slug")}/images/imgs/" MangaChapter( id = generateUid(url), diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/sinmh/SinmhParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/sinmh/SinmhParser.kt index 3af8d4ac..ebbf48ed 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/sinmh/SinmhParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/sinmh/SinmhParser.kt @@ -23,6 +23,10 @@ internal abstract class SinmhParser( SortOrder.POPULARITY, ) + override val availableStates: Set = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED) + + override val isMultipleTagsSupported = false + protected open val searchUrl = "search/" protected open val listUrl = "list/" @@ -41,40 +45,55 @@ internal abstract class SinmhParser( "已完结", ) - 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() -> { - append("/$searchUrl?keywords=") - append(query.urlEncoded()) + append('/') + when (filter) { + + is MangaListFilter.Search -> { + append(searchUrl) + append("?keywords=") + append(filter.query.urlEncoded()) append("&page=") append(page) } - !tags.isNullOrEmpty() -> { - append("/$listUrl") - append(tag?.key.orEmpty()) - append("/$page/") - } + is MangaListFilter.Advanced -> { + append(listUrl) + filter.tags.oneOrThrowIfMany()?.let { + append(it.key) + } - else -> { + filter.states.oneOrThrowIfMany()?.let { + append( + when (it) { + MangaState.ONGOING -> "-lianzai" + MangaState.FINISHED -> "-wanjie" + else -> "" + }, + ) + } - append("/$listUrl") - when (sortOrder) { + if (filter.tags.isNotEmpty() && filter.states.isNotEmpty()) { + append('/') + } + + when (filter.sortOrder) { SortOrder.POPULARITY -> append("click/") SortOrder.UPDATED -> append("update/") - else -> append("") + else -> append("/") } - append("?page=") - append(page) + append(page.toString()) + append('/') + } + + null -> { + append(listUrl) + append("update/") + append(page.toString()) + append('/') } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/MangaAy.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/MangaAy.kt index d55cb8a3..8e3f0a6d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/MangaAy.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/MangaAy.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.parsers.site.tr import androidx.collection.ArrayMap import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import org.jsoup.nodes.Document import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.PagedMangaParser @@ -19,75 +20,111 @@ class MangaAy(context: MangaLoaderContext) : PagedMangaParser(context, MangaSour override val configKeyDomain = ConfigKey.Domain("manga-ay.com") - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() - if (!query.isNullOrEmpty() || !tags.isNullOrEmpty()) { - if (page > 1) { - return emptyList() - } - val url = "https://$domain/arama" - val doc = webClient.httpPost( - url, - mapOf( - "title" to query?.urlEncoded().orEmpty(), - "genres" to tag?.key.orEmpty(), - ), - ).parseHtml() - return doc.select(".table tr").map { tr -> - val a = tr.selectFirstOrThrow("a") - val href = a.attrAsRelativeUrl("href") - Manga( - id = generateUid(href), - url = href, - publicUrl = a.attrAsAbsoluteUrl("href"), - title = a.text(), - coverUrl = "", - altTitle = null, - rating = RATING_UNKNOWN, - tags = emptySet(), - description = null, - state = null, - author = null, - isNsfw = isNsfwSource, - source = source, + override val isMultipleTagsSupported = false + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + + when (filter) { + is MangaListFilter.Search -> { + if (page > 1) { + return emptyList() + } + return parseMangaListQueryOrTags( + webClient.httpPost( + "https://$domain/arama", + mapOf("title" to filter.query.urlEncoded(), "genres" to ""), + ).parseHtml(), ) } - } else { - val url = buildString { - append("https://") - append(domain) - append("/seriler") - if (page > 1) { - append("/") - append(page) + + is MangaListFilter.Advanced -> { + + if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + if (page > 1) { + return emptyList() + } + return parseMangaListQueryOrTags( + webClient.httpPost( + "https://$domain/arama", + mapOf("title" to "", "genres" to it.key), + ).parseHtml(), + ) + } + } else { + val url = buildString { + append("https://") + append(domain) + append("/seriler") + if (page > 1) { + append("/") + append(page) + } + } + return parseMangaList(webClient.httpGet(url).parseHtml()) } + } - val doc = webClient.httpGet(url).parseHtml().requireElementById("ecommerce-products") - return doc.select(".card").map { div -> - val a = div.selectFirstOrThrow("a") - val href = a.attrAsRelativeUrl("href") - Manga( - id = generateUid(href), - url = href, - publicUrl = a.attrAsAbsoluteUrl("href"), - title = div.selectLastOrThrow(".item-name").text(), - coverUrl = div.selectFirst("img")?.src().orEmpty(), - altTitle = null, - rating = RATING_UNKNOWN, - tags = emptySet(), - description = null, - state = null, - author = null, - isNsfw = isNsfwSource, - source = source, - ) + + null -> { + val url = buildString { + append("https://") + append(domain) + append("/seriler") + if (page > 1) { + append("/") + append(page) + } + } + return parseMangaList(webClient.httpGet(url).parseHtml()) } } + + return emptyList() + } + + private fun parseMangaList(doc: Document): List { + return doc.requireElementById("ecommerce-products").select(".card").map { div -> + val a = div.selectFirstOrThrow("a") + val href = a.attrAsRelativeUrl("href") + Manga( + id = generateUid(href), + url = href, + publicUrl = a.attrAsAbsoluteUrl("href"), + title = div.selectLastOrThrow(".item-name").text(), + coverUrl = div.selectFirst("img")?.src().orEmpty(), + altTitle = null, + rating = RATING_UNKNOWN, + tags = emptySet(), + description = null, + state = null, + author = null, + isNsfw = isNsfwSource, + source = source, + ) + } + } + + private fun parseMangaListQueryOrTags(doc: Document): List { + return doc.select(".table tr").map { tr -> + val a = tr.selectFirstOrThrow("a") + val href = a.attrAsRelativeUrl("href") + Manga( + id = generateUid(href), + url = href, + publicUrl = a.attrAsAbsoluteUrl("href"), + title = a.text(), + coverUrl = "", + altTitle = null, + rating = RATING_UNKNOWN, + tags = emptySet(), + description = null, + state = null, + author = null, + isNsfw = isNsfwSource, + source = source, + ) + } } private var tagCache: ArrayMap? = null diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/SadScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/SadScans.kt index 381331bf..5c686374 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/SadScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/SadScans.kt @@ -15,20 +15,29 @@ internal class SadScans(context: MangaLoaderContext) : MangaParser(context, Mang override val availableSortOrders: Set = EnumSet.of(SortOrder.ALPHABETICAL) override val configKeyDomain = ConfigKey.Domain("sadscans.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(), @@ -64,7 +73,7 @@ internal class SadScans(context: MangaLoaderContext) : MangaParser(context, Mang chapters = doc.select(".chap-section .chap") .mapChapters(reversed = true) { i, div -> val a = div.selectFirstOrThrow("a") - val url = a.attrAsRelativeUrl("href").toAbsoluteUrl(domain) + val url = "/" + a.attrAsRelativeUrl("href").toAbsoluteUrl(domain) MangaChapter( id = generateUid(url), name = a.text(), diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/TrWebtoon.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/TrWebtoon.kt index 1d16a585..0d732396 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/TrWebtoon.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/TrWebtoon.kt @@ -1,11 +1,13 @@ package org.koitharu.kotatsu.parsers.site.tr +import org.jsoup.nodes.Document 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 java.lang.IllegalArgumentException import java.text.DateFormat import java.text.SimpleDateFormat import java.util.* @@ -16,100 +18,134 @@ class TrWebtoon(context: MangaLoaderContext) : override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("trwebtoon.com") - override val availableSortOrders: Set - get() = EnumSet.of(SortOrder.POPULARITY, SortOrder.ALPHABETICAL, SortOrder.UPDATED) - - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() - val url = if (sortOrder == SortOrder.UPDATED && query.isNullOrEmpty() && tags.isNullOrEmpty()) { - buildString { - append("https://") - append(domain) - append("/son-eklenenler") - append("?page=") - append(page) + override val availableSortOrders: Set = + EnumSet.of(SortOrder.POPULARITY, SortOrder.ALPHABETICAL, SortOrder.UPDATED) + + override val availableStates: Set = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED) + + 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("/webtoon-listesi?page=") + append(page.toString()) + append("&q=") + append(filter.query.urlEncoded()) + append("&sort=views&short_type=DESC") + } + return parseMangaList(webClient.httpGet(url).parseHtml()) } - } else { - buildString { - append("https://") - append(domain) - append("/webtoon-listesi") - append("?page=") - append(page) - when { - !query.isNullOrEmpty() -> { - append("&q=") - append(query.urlEncoded()) - } - !tags.isNullOrEmpty() -> { - append("&genre=") - append(tag?.key.orEmpty()) + is MangaListFilter.Advanced -> { + + if (filter.sortOrder == SortOrder.UPDATED) { + if (filter.tags.isNotEmpty()) { + throw IllegalArgumentException("Sort order updated + Tags or States is not supported by this source") + } + val url = buildString { + append("https://") + append(domain) + append("/son-eklenenler?page=") + append(page.toString()) } + return parseMangaListUpdated(webClient.httpGet(url).parseHtml()) + } else { + val url = buildString { + append("https://") + append(domain) + append("/webtoon-listesi?page=") + append(page.toString()) + filter.tags.oneOrThrowIfMany()?.let { + append("&genre=") + append(it.key) + } + filter.states.oneOrThrowIfMany()?.let { + append("&status=") + append( + when (it) { + MangaState.ONGOING -> "continues" + MangaState.FINISHED -> "complated" + else -> "" + }, + ) + } + append("&sort=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("views&short_type=DESC") + SortOrder.ALPHABETICAL -> append("name&short_type=ASC") + else -> append("views&short_type=DESC") + } + } + + return parseMangaList(webClient.httpGet(url).parseHtml()) } - append("&sort=") - when (sortOrder) { - SortOrder.POPULARITY -> append("views&short_type=DESC") - SortOrder.ALPHABETICAL -> append("name&short_type=ASC") - else -> append("views&short_type=DESC") + + } + + null -> { + val url = buildString { + append("https://") + append(domain) + append("/son-eklenenler?page=") + append(page.toString()) } + return parseMangaListUpdated(webClient.httpGet(url).parseHtml()) } } + } - val doc = webClient.httpGet(url).parseHtml() - val mangas = if (sortOrder == SortOrder.UPDATED && query.isNullOrEmpty() && tags.isNullOrEmpty()) { - doc.select(".page-content div.bslist_item").map { li -> - val href = li.selectFirstOrThrow("a").attrAsRelativeUrl("href") - Manga( - id = generateUid(href), - url = href, - publicUrl = href.toAbsoluteUrl(domain), - coverUrl = li.selectFirst(".figure img")?.src().orEmpty(), - title = li.selectFirst(".title")?.text().orEmpty(), - altTitle = null, - rating = RATING_UNKNOWN, - tags = emptySet(), - author = null, - state = when (doc.selectFirst("d-inline .badge")?.text()) { - "Devam Ediyor", "Güncel" -> MangaState.ONGOING - "Tamamlandı" -> MangaState.FINISHED - else -> null - }, - source = source, - isNsfw = isNsfwSource, - ) - } - } else { - doc.select(".row .col-xl-4 .card-body").map { li -> - val href = li.selectFirstOrThrow("a").attrAsRelativeUrl("href") - Manga( - id = generateUid(href), - url = href, - publicUrl = href.toAbsoluteUrl(domain), - coverUrl = li.selectFirst("img")?.src().orEmpty(), - title = li.selectFirst(".table-responsive a")?.text().orEmpty(), - altTitle = null, - rating = li.selectFirst(".row .col-xl-4 .mt-2 .my-1 .text-muted")?.text()?.substringBefore("/") - ?.toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN, - tags = emptySet(), - author = null, - state = when (doc.selectLast(".row .col-xl-4 .mt-2 .rounded-pill")?.text()) { - "Devam Ediyor", "Güncel" -> MangaState.ONGOING - "Tamamlandı" -> MangaState.FINISHED - else -> null - }, - source = source, - isNsfw = isNsfwSource, - ) - } + + private fun parseMangaList(doc: Document): List { + return doc.select(".row .col-xl-4 .card-body").map { li -> + val href = li.selectFirstOrThrow("a").attrAsRelativeUrl("href") + Manga( + id = generateUid(href), + url = href, + publicUrl = href.toAbsoluteUrl(domain), + coverUrl = li.selectFirst("img")?.src().orEmpty(), + title = li.selectFirst(".table-responsive a")?.text().orEmpty(), + altTitle = null, + rating = li.selectFirst(".row .col-xl-4 .mt-2 .my-1 .text-muted")?.text()?.substringBefore("/") + ?.toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN, + tags = emptySet(), + author = null, + state = when (doc.selectLast(".row .col-xl-4 .mt-2 .rounded-pill")?.text()) { + "Devam Ediyor", "Güncel" -> MangaState.ONGOING + "Tamamlandı" -> MangaState.FINISHED + else -> null + }, + source = source, + isNsfw = isNsfwSource, + ) } + } - return mangas + private fun parseMangaListUpdated(doc: Document): List { + return doc.select(".page-content div.bslist_item").map { li -> + val href = li.selectFirstOrThrow("a").attrAsRelativeUrl("href") + Manga( + id = generateUid(href), + url = href, + publicUrl = href.toAbsoluteUrl(domain), + coverUrl = li.selectFirst(".figure img")?.src().orEmpty(), + title = li.selectFirst(".title")?.text().orEmpty(), + altTitle = null, + rating = RATING_UNKNOWN, + tags = emptySet(), + author = null, + state = when (doc.selectFirst("d-inline .badge")?.text()) { + "Devam Ediyor", "Güncel" -> MangaState.ONGOING + "Tamamlandı" -> MangaState.FINISHED + else -> null + }, + source = source, + isNsfw = isNsfwSource, + ) + } } override suspend fun getAvailableTags(): Set { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/YaoiFlix.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/YaoiFlix.kt index ef915b6f..cb41dc3a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/YaoiFlix.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/YaoiFlix.kt @@ -16,43 +16,50 @@ class YaoiFlix(context: MangaLoaderContext) : PagedMangaParser(context, MangaSou override val configKeyDomain = ConfigKey.Domain("www.yaoiflix.pro") - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() + override val isMultipleTagsSupported = false + + 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) { append("/page/") - append(page) + append(page.toString()) } append("/?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } - !tags.isNullOrEmpty() -> { - append("/dizi-kategori/") - append(tag?.key.orEmpty()) - append("/") - if (page > 1) { - append("page/") - append(page) - append("/") + is MangaListFilter.Advanced -> { + if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + append("/dizi-kategori/") + append(it.key) + append("/") + if (page > 1) { + append("page/") + append(page.toString()) + append('/') + } + } + } else { + append("/tum-seriler/") + if (page > 1) { + append("page/") + append(page.toString()) + append('/') + } } } - else -> { + null -> { append("/tum-seriler/") if (page > 1) { append("page/") - append(page) - append("/") + append(page.toString()) + append('/') } } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BlogTruyenParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BlogTruyenParser.kt index 3ec7263b..f5bee237 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BlogTruyenParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BlogTruyenParser.kt @@ -29,98 +29,38 @@ class BlogTruyenParser(context: MangaLoaderContext) : .add("User-Agent", UserAgents.CHROME_DESKTOP) .build() + override val isMultipleTagsSupported = false + private val dateFormat = SimpleDateFormat("dd/MM/yyyy HH:mm", Locale.US) private var cacheTags = SuspendLazy(::fetchTags) - override suspend fun getDetails(manga: Manga): Manga { - val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() - val descriptionElement = doc.selectFirstOrThrow("div.description") - val statusText = descriptionElement - .selectFirst("p:contains(Trạng thái) > span.color-red") - ?.text() - val state = when (statusText) { - "Đang tiến hành" -> MangaState.ONGOING - "Đã hoàn thành" -> MangaState.FINISHED - else -> null - } + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { - val rating = doc.selectFirst("span.total-vote")?.attr("ng-init")?.let { text -> - val like = text.substringAfter("TotalLike=") - .substringBefore(';') - .toIntOrNull() ?: return@let RATING_UNKNOWN - val dislike = text.substringAfter("TotalDisLike=") - .toIntOrNull() ?: return@let RATING_UNKNOWN + return when (filter) { - when { - like == 0 && dislike == 0 -> RATING_UNKNOWN - else -> like.toFloat() / (like + dislike) - } - } - - val tags = cacheTags.tryGet().getOrNull()?.let { tagMap -> - descriptionElement.select("p > span.category").mapNotNullToSet { - val tagName = it.selectFirst("a")?.text()?.trim() ?: return@mapNotNullToSet null - tagMap[tagName] - } - } - - return manga.copy( - tags = tags ?: emptySet(), - author = descriptionElement.selectFirst("p:contains(Tác giả) > a")?.text(), - description = doc.selectFirst(".detail .content")?.html(), - chapters = parseChapterList(doc), - largeCoverUrl = doc.selectLast("div.thumbnail > img")?.imageUrl(), - state = state, - rating = rating ?: RATING_UNKNOWN, - isNsfw = doc.getElementById("warningCategory") != null, - ) - } - - private fun parseChapterList(doc: Document): List { - val chapterList = doc.select("#list-chapters > p") - return chapterList.mapChapters(reversed = true) { index, element -> - val titleElement = element.selectFirst("span.title > a") ?: return@mapChapters null - val name = titleElement.text() - val relativeUrl = titleElement.attrAsRelativeUrl("href") - val id = relativeUrl.substringAfter('/').substringBefore('/') - val uploadDate = dateFormat.tryParse(element.select("span.publishedDate").text()) - MangaChapter( - id = generateUid(id), - name = name, - number = index + 1, - url = relativeUrl, - scanlator = null, - uploadDate = uploadDate, - branch = null, - source = source, - ) - } - } - - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - return when { - !query.isNullOrEmpty() -> { - val searchUrl = "https://${domain}/timkiem/nangcao/1/0/-1/-1?txt=${query.urlEncoded()}&p=$page" + is MangaListFilter.Search -> { + val searchUrl = "https://${domain}/timkiem/nangcao/1/0/-1/-1?txt=${filter.query.urlEncoded()}&p=$page" val searchContent = webClient.httpGet(searchUrl).parseHtml() .selectFirst("section.list-manga-bycate > div.list") parseMangaList(searchContent) } - !tags.isNullOrEmpty() -> { - val tag = tags.oneOrThrowIfMany()!! - val categoryAjax = - "https://${domain}/ajax/Category/AjaxLoadMangaByCategory?id=${tag.key}&orderBy=5&p=$page" - val listContent = webClient.httpGet(categoryAjax).parseHtml().selectFirst("div.list") - parseMangaList(listContent) + is MangaListFilter.Advanced -> { + + if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany().let { + val categoryAjax = + "https://${domain}/ajax/Category/AjaxLoadMangaByCategory?id=${it?.key}&orderBy=5&p=$page" + val listContent = webClient.httpGet(categoryAjax).parseHtml().selectFirst("div.list") + parseMangaList(listContent) + } + } else { + getNormalList(page) + } } - else -> getNormalList(page) + null -> getNormalList(page) } } @@ -146,7 +86,7 @@ class BlogTruyenParser(context: MangaLoaderContext) : description = el.selectFirst("p.al-j.break.line-height-15")?.text(), url = relativeUrl, publicUrl = relativeUrl.toAbsoluteUrl(domain), - coverUrl = linkTag.selectLast("img")?.imageUrl().orEmpty(), + coverUrl = linkTag.selectLast("img")?.src().orEmpty(), source = source, tags = tags ?: emptySet(), isNsfw = false, @@ -171,7 +111,7 @@ class BlogTruyenParser(context: MangaLoaderContext) : description = mangaInfo.select("div.al-j.fs-12").text(), url = relativeUrl, publicUrl = relativeUrl.toAbsoluteUrl(domain), - coverUrl = mangaInfo.selectFirst("div > img.img")?.imageUrl().orEmpty(), + coverUrl = mangaInfo.selectFirst("div > img.img")?.src().orEmpty(), isNsfw = false, rating = RATING_UNKNOWN, tags = emptySet(), @@ -182,6 +122,91 @@ class BlogTruyenParser(context: MangaLoaderContext) : } } + override suspend fun getAvailableTags(): Set { + return cacheTags.get().values.toSet() + } + + private suspend fun fetchTags(): Map { + val doc = webClient.httpGet("/timkiem/nangcao".toAbsoluteUrl(domain)).parseHtml() + val tagItems = doc.select("li[data-id]") + val tagMap = ArrayMap(tagItems.size) + for (tag in tagItems) { + val title = tag.text().trim() + tagMap[tag.text().trim()] = MangaTag( + title = title, + key = tag.attr("data-id"), + source = source, + ) + } + return tagMap + } + + override suspend fun getDetails(manga: Manga): Manga { + val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() + val descriptionElement = doc.selectFirstOrThrow("div.description") + val statusText = descriptionElement + .selectFirst("p:contains(Trạng thái) > span.color-red") + ?.text() + + val state = when (statusText) { + "Đang tiến hành" -> MangaState.ONGOING + "Đã hoàn thành" -> MangaState.FINISHED + else -> null + } + + val rating = doc.selectFirst("span.total-vote")?.attr("ng-init")?.let { text -> + val like = text.substringAfter("TotalLike=") + .substringBefore(';') + .toIntOrNull() ?: return@let RATING_UNKNOWN + val dislike = text.substringAfter("TotalDisLike=") + .toIntOrNull() ?: return@let RATING_UNKNOWN + + when { + like == 0 && dislike == 0 -> RATING_UNKNOWN + else -> like.toFloat() / (like + dislike) + } + } + + val tags = cacheTags.tryGet().getOrNull()?.let { tagMap -> + descriptionElement.select("p > span.category").mapNotNullToSet { + val tagName = it.selectFirst("a")?.text()?.trim() ?: return@mapNotNullToSet null + tagMap[tagName] + } + } + + return manga.copy( + tags = tags ?: emptySet(), + author = descriptionElement.selectFirst("p:contains(Tác giả) > a")?.text(), + description = doc.selectFirst(".detail .content")?.html(), + chapters = parseChapterList(doc), + largeCoverUrl = doc.selectLast("div.thumbnail > img")?.src().orEmpty(), + state = state, + rating = rating ?: RATING_UNKNOWN, + isNsfw = doc.getElementById("warningCategory") != null, + ) + } + + private fun parseChapterList(doc: Document): List { + val chapterList = doc.select("#list-chapters > p") + return chapterList.mapChapters(reversed = true) { index, element -> + val titleElement = element.selectFirst("span.title > a") ?: return@mapChapters null + val name = titleElement.text() + val relativeUrl = titleElement.attrAsRelativeUrl("href") + val id = relativeUrl.substringAfter('/').substringBefore('/') + val uploadDate = dateFormat.tryParse(element.select("span.publishedDate").text()) + MangaChapter( + id = generateUid(id), + name = name, + number = index + 1, + url = relativeUrl, + scanlator = null, + uploadDate = uploadDate, + branch = null, + source = source, + ) + } + } + override suspend fun getPages(chapter: MangaChapter): List { fun generateImageId(index: Int) = generateUid("${chapter.url}/$index") @@ -191,7 +216,7 @@ class BlogTruyenParser(context: MangaLoaderContext) : pages.add( MangaPage( id = generateImageId(pages.size), - url = img.imageUrl(), + url = img.src().orEmpty(), preview = null, source = source, ), @@ -218,30 +243,4 @@ class BlogTruyenParser(context: MangaLoaderContext) : return pages } - - override suspend fun getAvailableTags(): Set { - return cacheTags.get().values.toSet() - } - - - private suspend fun fetchTags(): Map { - val doc = webClient.httpGet("/timkiem/nangcao".toAbsoluteUrl(domain)).parseHtml() - val tagItems = doc.select("li[data-id]") - val tagMap = ArrayMap(tagItems.size) - for (tag in tagItems) { - val title = tag.text().trim() - tagMap[tag.text().trim()] = MangaTag( - title = title, - key = tag.attr("data-id"), - source = source, - ) - } - return tagMap - } - - private fun Element.imageUrl(): String { - return attrAsAbsoluteUrlOrNull("src") - ?: attrAsAbsoluteUrlOrNull("data-cfsrc") - ?: "" - } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVNParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVNParser.kt index 54edb7a6..5b398558 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVNParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVNParser.kt @@ -35,6 +35,57 @@ class HentaiVNParser(context: MangaLoaderContext) : MangaParser(context, MangaSo SortOrder.NEWEST, ) + override suspend fun getList(offset: Int, filter: MangaListFilter?): List { + return when (filter) { + + is MangaListFilter.Search -> { + val page = (offset / PAGE_SIZE.toFloat()).toIntUp() + 1 + urlBuilder() + val searchUrl = + "/tim-kiem-truyen.html?key=${filter.query.urlEncoded()}&page=$page".toAbsoluteUrl(domain) + val docs = webClient.httpGet(searchUrl).parseHtml() + parseMainList(docs, page) + } + + is MangaListFilter.Advanced -> { + val pageSize = if (filter.tags.isEmpty()) PAGE_SIZE else SEARCH_PAGE_SIZE + val page = (offset / pageSize.toFloat()).toIntUp() + 1 + + if (filter.tags.isNotEmpty()) { + val url = buildString { + val tagKey = "tag[]".urlEncoded() + append("/forum/search-plus.php?name=") + append("&dou=&char=") + filter.tags.forEach { tag -> + append("&") + append(tagKey) + append("=") + append(tag.key) + } + append("&search=") + append("&page=") + append(page) + }.toAbsoluteUrl(domain) + + val docs = webClient.httpGet(url).parseHtml() + return parseAdvanceSearch(docs, page) + } else { + val site = if (filter.sortOrder == SortOrder.UPDATED) "/chap-moi" else "/danh-sach" + val url = "$site.html?page=$page".toAbsoluteUrl(domain) + context.cookieJar.insertCookies(domain, *getSortCookies(filter.sortOrder)) + val docs = webClient.httpGet(url).parseHtml() + parseMainList(docs, page) + } + } + + null -> { + val page = (offset / PAGE_SIZE.toFloat()).toIntUp() + 1 + val url = "/chap-moi.html?page=$page".toAbsoluteUrl(domain) + parseMainList(webClient.httpGet(url).parseHtml(), page) + } + } + } + override suspend fun getDetails(manga: Manga): Manga = coroutineScope { val chapterDeferred = async { fetchChapters(manga.url) } val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() @@ -79,48 +130,6 @@ class HentaiVNParser(context: MangaLoaderContext) : MangaParser(context, MangaSo ) } - override suspend fun getList(offset: Int, query: String?, tags: Set?, sortOrder: SortOrder): List { - val pageSize = if (tags.isNullOrEmpty()) PAGE_SIZE else SEARCH_PAGE_SIZE - val page = (offset / pageSize.toFloat()).toIntUp() + 1 - return when { - !tags.isNullOrEmpty() -> { - val url = buildString { - val tagKey = "tag[]".urlEncoded() - append("/forum/search-plus.php?name=") - append(query?.urlEncoded().orEmpty()) - append("&dou=&char=") - tags.forEach { tag -> - append("&") - append(tagKey) - append("=") - append(tag.key) - } - append("&search=") - append("&page=") - append(page) - }.toAbsoluteUrl(domain) - - val docs = webClient.httpGet(url).parseHtml() - return parseAdvanceSearch(docs, page) - } - - !query.isNullOrEmpty() -> { - urlBuilder() - val searchUrl = "/tim-kiem-truyen.html?key=${query.urlEncoded()}&page=$page".toAbsoluteUrl(domain) - val docs = webClient.httpGet(searchUrl).parseHtml() - parseMainList(docs, page) - } - - else -> { - val site = if (sortOrder == SortOrder.UPDATED) "/chap-moi" else "/danh-sach" - val url = "$site.html?page=$page".toAbsoluteUrl(domain) - context.cookieJar.insertCookies(domain, *getSortCookies(sortOrder)) - val docs = webClient.httpGet(url).parseHtml() - parseMainList(docs, page) - } - } - } - override suspend fun getPages(chapter: MangaChapter): List { val docs = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml() return docs.select("#image > img").map { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/LxManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/LxManga.kt index 6fdd9b0a..bd61e0aa 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/LxManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/LxManga.kt @@ -20,57 +20,76 @@ internal class LxManga(context: MangaLoaderContext) : PagedMangaParser(context, SortOrder.NEWEST, SortOrder.POPULARITY, ) + override val availableStates: Set = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED) override val configKeyDomain = ConfigKey.Domain("lxmanga.net") + override val isMultipleTagsSupported = false + override val headers: Headers = Headers.Builder() .add("User-Agent", UserAgents.CHROME_DESKTOP) .build() - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) - when { - !query.isNullOrEmpty() -> { + + when (filter) { + + is MangaListFilter.Search -> { val skey = "filter[name]=".urlEncoded() append("/tim-kiem?$skey") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) + append("&page=") + append(page.toString()) } - !tags.isNullOrEmpty() -> { - append("/the-loai/") - for (tag in tags) { - append(tag.key) + is MangaListFilter.Advanced -> { + if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + append("/the-loai/") + append(it.key) + } + } else { + append("/danh-sach") + } + append("?page=") + append(page.toString()) + + if (filter.states.isNotEmpty()) { + append("&filter[status]=") + filter.states.forEach { + append( + when (it) { + MangaState.ONGOING -> "2," + MangaState.FINISHED -> "1," + else -> "1,2" + }, + ) + } + } + + append("&sort=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("-views") + SortOrder.UPDATED -> append("-updated_at") + SortOrder.NEWEST -> append("-created_at") + SortOrder.ALPHABETICAL -> append("name") + else -> append("-updated_at") } } - else -> { - append("/danh-sach") + null -> { + append("/danh-sach?sort=-updated_at&page=") + append(page.toString()) } } - append("?page=") - append(page.toString()) - append("&sort=") - when (sortOrder) { - SortOrder.POPULARITY -> append("-views") - SortOrder.UPDATED -> append("-updated_at") - SortOrder.NEWEST -> append("-created_at") - SortOrder.ALPHABETICAL -> append("name") - - else -> append("-updated_at") - } - } + } val doc = webClient.httpGet(url).parseHtml() - return doc.select("div.grid div.manga-vertical") .map { div -> val href = div.selectFirstOrThrow("a").attr("href") @@ -95,7 +114,6 @@ internal class LxManga(context: MangaLoaderContext) : PagedMangaParser(context, override suspend fun getDetails(manga: Manga): Manga { val root = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US) - return manga.copy( altTitle = root.select(".divider2:contains(Noms associés :)").firstOrNull()?.text(), state = when (root.select("div.grow div.mt-2:contains(Tình trạng) a").first()!!.text()) { @@ -135,9 +153,7 @@ internal class LxManga(context: MangaLoaderContext) : PagedMangaParser(context, override suspend fun getPages(chapter: MangaChapter): List { val fullUrl = chapter.url.toAbsoluteUrl(domain) - val doc = webClient.httpGet(fullUrl).parseHtml() - return doc.select("div.text-center img.lazy").map { img -> val url = img.attrAsRelativeUrlOrNull("data-src") ?: img.attrAsRelativeUrlOrNull("src") ?: img.parseFailed("Image src not found") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/NetTruyenParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/NetTruyenParser.kt deleted file mode 100644 index db799ac0..00000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/NetTruyenParser.kt +++ /dev/null @@ -1,228 +0,0 @@ -package org.koitharu.kotatsu.parsers.site.vi - -import androidx.collection.ArrayMap -import androidx.collection.ArraySet -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -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.exception.NotFoundException -import org.koitharu.kotatsu.parsers.model.* -import org.koitharu.kotatsu.parsers.util.* -import java.text.SimpleDateFormat -import java.util.* - -@MangaSourceParser("NETTRUYEN", "NetTruyen", "vi") -class NetTruyenParser(context: MangaLoaderContext) : - PagedMangaParser(context, MangaSource.NETTRUYEN, pageSize = 36) { - - override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain( - "www.nettruyenlive.com", - "www.nettruyenio.com", - "www.nettruyento.com", - "nettruyento.com", - "nettruyenin.com", - ) - - override val availableSortOrders: Set - get() = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.NEWEST, SortOrder.RATING) - - private val mutex = Mutex() - private val dateFormat = SimpleDateFormat("dd/MM/yy", Locale.US) - private var tagCache: ArrayMap? = null - - override suspend fun getDetails(manga: Manga): Manga { - val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() - val rating = doc.selectFirst("span[itemprop=ratingValue]") - ?.ownText() - ?.toFloatOrNull() ?: 0f - - val chapterElements = doc.getElementById("nt_listchapter")?.select("ul > li") ?: doc.parseFailed() - val chapters = chapterElements.mapChapters(reversed = true) { index, element -> - val a = element.selectFirst("div.chapter > a") ?: return@mapChapters null - val relativeUrl = a.attrAsRelativeUrlOrNull("href") ?: return@mapChapters null - val timeText = element.selectFirst("div.col-xs-4.text-center.no-wrap.small")?.text() - - MangaChapter( - id = generateUid(relativeUrl), - name = a.text(), - number = index + 1, - url = relativeUrl, - scanlator = null, - uploadDate = parseChapterTime(timeText), - branch = null, - source = source, - ) - } - - return manga.copy( - rating = rating / 5, - chapters = chapters, - description = doc.selectFirst("div.detail-content > p")?.html(), - isNsfw = doc.selectFirst("div.alert.alert-danger > strong:contains(Cảnh báo độ tuổi)") != null, - ) - } - - // 20 giây trước - // 52 phút trước - // 6 giờ trước - // 2 ngày trước - // 19:09 30/07 - // 23/12/21 - private fun parseChapterTime(timeText: String?): Long { - if (timeText.isNullOrEmpty()) { - return 0L - } - - val timeWords = arrayOf("giây", "phút", "giờ", "ngày") - val calendar = Calendar.getInstance() - val timeArr = timeText.split(' ') - if (WordSet(*timeWords).anyWordIn(timeText)) { - val timeSuffix = timeArr.getOrNull(1) - val timeDiff = timeArr.getOrNull(0)?.toIntOrNull() ?: return 0L - when (timeSuffix) { - timeWords[0] -> calendar.add(Calendar.SECOND, -timeDiff) - timeWords[1] -> calendar.add(Calendar.MINUTE, -timeDiff) - timeWords[2] -> calendar.add(Calendar.HOUR, -timeDiff) - timeWords[3] -> calendar.add(Calendar.DATE, -timeDiff) - else -> return 0L - } - } else { - val relativeDate = timeArr.lastOrNull() ?: return 0L - val dateString = when (relativeDate.split('/').size) { - 2 -> { - val currentYear = calendar.get(Calendar.YEAR).toString().takeLast(2) - "$relativeDate/$currentYear" - } - - 3 -> relativeDate - else -> return 0L - } - - calendar.timeInMillis = dateFormat.tryParse(dateString) - } - - - return calendar.time.time - } - - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val isSearching = !query.isNullOrEmpty() - val url = buildString { - append("https://") - append(domain) - if (isSearching) { - append("/tim-truyen?keyword=") - append(query!!.urlEncoded()) - append("&page=") - append(page) - } else { - val tagQuery = tags.orEmpty().joinToString(",") { it.key } - append("/tim-truyen-nang-cao?genres=$tagQuery") - append("¬genres=&gender=-1&status=-1&minchapter=1&sort=${getSortOrderKey(sortOrder)}") - append("&page=$page") - } - } - - val response = if (isSearching) { - val result = runCatchingCancellable { webClient.httpGet(url) } - val exception = result.exceptionOrNull() - if (exception is NotFoundException) { - return emptyList() - } - - result.getOrThrow() - } else { - webClient.httpGet(url) - } - - val itemsElements = response.parseHtml() - .select("div.ModuleContent > div.items") - .select("div.item") - return itemsElements.mapNotNull { item -> - val tooltipElement = item.selectFirst("div.box_tootip") ?: return@mapNotNull null - val absUrl = item.selectFirst("div.image > a")?.attrAsAbsoluteUrlOrNull("href") ?: return@mapNotNull null - val slug = absUrl.substringAfterLast('/') - val mangaState = when (tooltipElement.selectFirst("div.message_main > p:contains(Tình trạng)")?.ownText()) { - "Đang tiến hành" -> MangaState.ONGOING - "Hoàn thành" -> MangaState.FINISHED - else -> null - } - - val tagMap = getOrCreateTagMap() - val tagsElement = tooltipElement.selectFirst("div.message_main > p:contains(Thể loại)")?.ownText().orEmpty() - val mangaTags = tagsElement.split(',').mapNotNullToSet { tagMap[it.trim()] } - Manga( - id = generateUid(slug), - title = tooltipElement.selectFirst("div.title")?.text().orEmpty(), - altTitle = null, - url = absUrl.toRelativeUrl(domain), - publicUrl = absUrl, - rating = RATING_UNKNOWN, - isNsfw = false, - coverUrl = item.selectFirst("div.image a img")?.absUrl("data-original").orEmpty(), - largeCoverUrl = null, - tags = mangaTags, - state = mangaState, - author = tooltipElement.selectFirst("div.message_main > p:contains(Tác giả)")?.ownText(), - description = tooltipElement.selectFirst("div.box_text")?.text(), - chapters = null, - source = source, - ) - } - } - - - override suspend fun getPages(chapter: MangaChapter): List { - val pageElements = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml() - .select("div.reading-detail.box_doc > div img") - return pageElements.map { element -> - val url = element.attrAsAbsoluteUrl("data-original") - MangaPage( - id = generateUid(url), - url = url, - preview = null, - source = source, - ) - } - } - - override suspend fun getAvailableTags(): Set { - val map = getOrCreateTagMap() - val tagSet = ArraySet(map.size) - for (entry in map) { - tagSet.add(entry.value) - } - - return tagSet - } - - private suspend fun getOrCreateTagMap(): ArrayMap = mutex.withLock { - tagCache?.let { return@withLock it } - val doc = webClient.httpGet("/tim-truyen-nang-cao".toAbsoluteUrl(domain)).parseHtml() - val tagItems = doc.select("div.genre-item") - val result = ArrayMap(tagItems.size) - for (item in tagItems) { - val title = item.text().trim() - val key = item.select("span[data-id]").attr("data-id") - result[title] = MangaTag(title = title, key = key, source = source) - } - tagCache = result - result - } - - private fun getSortOrderKey(sortOrder: SortOrder) = when (sortOrder) { - SortOrder.UPDATED -> 0 - SortOrder.POPULARITY -> 10 - SortOrder.NEWEST -> 15 - SortOrder.RATING -> 20 - else -> throw IllegalArgumentException("Sort order ${sortOrder.name} not supported") - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/Truyenqq.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/Truyenqq.kt index 6341e2be..256892d7 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/Truyenqq.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/Truyenqq.kt @@ -14,40 +14,70 @@ internal class Truyenqq(context: MangaLoaderContext) : PagedMangaParser(context, override val availableSortOrders: Set = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.NEWEST) + + override val availableStates: Set = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED) + override val configKeyDomain = ConfigKey.Domain("truyenqqvn.com") - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tagQuery = if (tags.isNullOrEmpty()) "" else tags.joinToString(separator = ",") { it.key } - val url = if (!query.isNullOrEmpty()) { - buildString { - append("https://") - append(domain) - append("/tim-kiem/trang-$page.html") - append("?q=") - append(query.urlEncoded()) - } - } else { - buildString { - append("https://") - append(domain) - append("/tim-kiem-nang-cao/trang-$page.html") - append("?status=-1&country=0&sort=") - when (sortOrder) { - SortOrder.POPULARITY -> append("4") - SortOrder.UPDATED -> append("2") - SortOrder.NEWEST -> append("0") - else -> append("2") + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + + val url = + when (filter) { + is MangaListFilter.Search -> { + buildString { + append("https://") + append(domain) + append("/tim-kiem/trang-$page.html") + append("?q=") + append(filter.query.urlEncoded()) + } + } + + is MangaListFilter.Advanced -> { + buildString { + append("https://") + append(domain) + append("/tim-kiem-nang-cao/trang-") + append(page.toString()) + append(".html?country=0&sort=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("4") + SortOrder.UPDATED -> append("2") + SortOrder.NEWEST -> append("0") + else -> append("2") + } + if (filter.states.isNotEmpty()) { + filter.states.oneOrThrowIfMany()?.let { + append("&status=") + append( + when (it) { + MangaState.ONGOING -> "0" + MangaState.FINISHED -> "1" + else -> "-1" + }, + ) + } + } else { + append("&status=-1") + } + + append("&category=") + append(filter.tags.joinToString(separator = ",") { it.key }) + append("¬category=&minchapter=0") + } + } + + null -> { + buildString { + append("https://") + append(domain) + append("/tim-kiem-nang-cao/trang-") + append(page.toString()) + append(".html?status=-1&country=0&sort=2&category=¬category=&minchapter=0") + } + } - append("&category=") - append(tagQuery) - append("¬category=&minchapter=0") } - } val doc = webClient.httpGet(url).parseHtml() return doc.requireElementById("main_homepage").select("li").map { li -> val href = li.selectFirstOrThrow("a").attrAsRelativeUrl("href") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyentranhLHParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyentranhLHParser.kt index 640a089b..02fa5fc0 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyentranhLHParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyentranhLHParser.kt @@ -19,10 +19,84 @@ class TruyentranhLHParser(context: MangaLoaderContext) : override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("truyentranhlh.net") override val availableSortOrders: Set = EnumSet.allOf(SortOrder::class.java) + override val availableStates: Set = + EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED) private val mutex = Mutex() private var tagCache: Map? = null + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + + val url = urlBuilder().apply { + addPathSegment("tim-kiem") + addQueryParameter("page", page.toString()) + when (filter) { + + is MangaListFilter.Search -> { + addQueryParameter("q", filter.query) + } + + is MangaListFilter.Advanced -> { + + addQueryParameter( + "sort", + when (filter.sortOrder) { + SortOrder.UPDATED -> "update" + SortOrder.NEWEST -> "new" + SortOrder.RATING -> "like" + SortOrder.POPULARITY -> "top" + SortOrder.ALPHABETICAL -> "az" + }, + ) + + if (filter.states.isNotEmpty()) { + filter.states.oneOrThrowIfMany()?.let { + addQueryParameter( + "status", + when (it) { + MangaState.ONGOING -> "1" + MangaState.FINISHED -> "3" + MangaState.PAUSED -> "2" + else -> "0" + }, + ) + } + } + + if (filter.tags.isNotEmpty()) { + val tagsQuery = filter.tags.joinToString(separator = ",") { it.key } + addEncodedQueryParameter("accept_genres", tagsQuery) + } + } + + null -> { + addQueryParameter("sort", "update") + } + } + + }.build() + + return webClient.httpGet(url).parseHtml() + .select(".container .card.card-dark .row > .thumb-item-flow") + .mapNotNull { + val a = it.selectFirstOrThrow(".thumb-wrapper a") + Manga( + id = generateUid(a.attrAsRelativeUrl("href")), + url = a.attrAsRelativeUrl("href"), + publicUrl = a.attrAsAbsoluteUrl("href"), + title = it.select(".thumb_attr.series-title").text(), + altTitle = null, + rating = RATING_UNKNOWN, + isNsfw = false, + coverUrl = a.selectFirst("div[data-bg]")?.attrAsAbsoluteUrl("data-bg").orEmpty(), + tags = emptySet(), + state = null, + author = null, + source = source, + ) + } + } + override suspend fun getDetails(manga: Manga): Manga { val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() val infoHeaderEl = docs.selectFirst("main.section-body") @@ -33,6 +107,7 @@ class TruyentranhLHParser(context: MangaLoaderContext) : val state = when (infoEl?.selectFirst(".info-item:contains(Tình trạng) > .info-value")?.text()) { "Đang tiến hành" -> MangaState.ONGOING "Đã hoàn thành" -> MangaState.FINISHED + "Tạm ngưng" -> MangaState.PAUSED else -> null } val rating = infoHeaderEl?.let { @@ -68,53 +143,6 @@ class TruyentranhLHParser(context: MangaLoaderContext) : ) } - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val sortQuery = when (sortOrder) { - SortOrder.UPDATED -> "update" - SortOrder.NEWEST -> "new" - SortOrder.RATING -> "like" - SortOrder.POPULARITY -> "top" - SortOrder.ALPHABETICAL -> "az" - } - val url = urlBuilder().apply { - addPathSegment("tim-kiem") - addQueryParameter("sort", sortQuery) - addQueryParameter("page", page.toString()) - if (!query.isNullOrEmpty()) { - addQueryParameter("q", query) - } - if (!tags.isNullOrEmpty()) { - val tagsQuery = tags.joinToString(separator = ",") { it.key } - addEncodedQueryParameter("accept_genres", tagsQuery) - } - }.build() - - return webClient.httpGet(url).parseHtml() - .select(".container .card.card-dark .row > .thumb-item-flow") - .mapNotNull { - val a = it.selectFirstOrThrow(".thumb-wrapper a") - Manga( - id = generateUid(a.attrAsRelativeUrl("href")), - url = a.attrAsRelativeUrl("href"), - publicUrl = a.attrAsAbsoluteUrl("href"), - title = it.select(".thumb_attr.series-title").text(), - altTitle = null, - rating = RATING_UNKNOWN, - isNsfw = false, - coverUrl = a.selectFirst("div[data-bg]")?.attrAsAbsoluteUrl("data-bg").orEmpty(), - tags = emptySet(), - state = null, - author = null, - source = source, - ) - } - } - override suspend fun getPages(chapter: MangaChapter): List { val url = chapter.url.toAbsoluteUrl(domain) return webClient.httpGet(url).parseHtml().select("#chapter-content > img").mapNotNull { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/YurinekoParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/YurinekoParser.kt index 1cdd0b6e..336e7e88 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/YurinekoParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/YurinekoParser.kt @@ -5,30 +5,12 @@ 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.ContentType -import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaChapter -import org.koitharu.kotatsu.parsers.model.MangaPage -import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.model.MangaState -import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN -import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.util.domain import org.koitharu.kotatsu.parsers.util.generateUid -import org.koitharu.kotatsu.parsers.util.json.asIterable -import org.koitharu.kotatsu.parsers.util.json.getStringOrNull -import org.koitharu.kotatsu.parsers.util.json.mapJSON -import org.koitharu.kotatsu.parsers.util.json.mapJSONToSet -import org.koitharu.kotatsu.parsers.util.json.toJSONList +import org.koitharu.kotatsu.parsers.util.json.* import org.koitharu.kotatsu.parsers.util.mapChapters -import org.koitharu.kotatsu.parsers.util.parseHtml -import org.koitharu.kotatsu.parsers.util.parseJson -import org.koitharu.kotatsu.parsers.util.parseJsonArray -import org.koitharu.kotatsu.parsers.util.requireElementById -import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl -import org.koitharu.kotatsu.parsers.util.tryParse -import org.koitharu.kotatsu.parsers.util.urlEncoded +import org.koitharu.kotatsu.parsers.util.* import java.text.SimpleDateFormat import java.util.EnumSet import java.util.Locale @@ -44,45 +26,25 @@ class YurinekoParser(context: MangaLoaderContext) : PagedMangaParser(context, Ma private val apiDomain get() = "api.$domain" - override suspend fun getDetails(manga: Manga): Manga { - val response = webClient.httpGet(manga.url.toAbsoluteUrl(apiDomain)).parseJson() - val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US) - return manga.copy( - chapters = response.getJSONArray("chapters") - .toJSONList() - .mapChapters(true) { i, jo -> - val mangaId = jo.getInt("mangaID") - val chapterId = jo.getInt("id") - MangaChapter( - id = generateUid(chapterId.toLong()), - name = jo.getString("name"), - number = i + 1, - scanlator = null, - url = "/read/$mangaId/$chapterId", - uploadDate = df.tryParse(jo.getString("date")), - branch = null, - source = source, - ) - }, - ) - } + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + val listUrl = + when (filter) { + + is MangaListFilter.Search -> { + "/search?query=${filter.query.urlEncoded()}&page=$page" + } + + is MangaListFilter.Advanced -> { + if (filter.tags.isNotEmpty()) { + val tagKeys = filter.tags.joinToString(separator = ",") { it.key } + "/advancedSearch?genre=$tagKeys¬Genre=&sort=7&minChapter=1&status=0&page=$page" + } else { + "/lastest2?page=$page" + } + } - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val listUrl = when { - !query.isNullOrEmpty() -> "/search?query=${query.urlEncoded()}&page=$page" - tags.isNullOrEmpty() -> "/lastest2?page=$page" - tags.size == 1 -> "/searchType?type=tag&id=${tags.first().key}&page=$page" - else -> { - // Sort order is different when filter with multiple tags - val tagKeys = tags.joinToString(separator = ",") { it.key } - "/advancedSearch?genre=$tagKeys¬Genre=&sort=7&minChapter=1&status=0&page=$page" + null -> "/lastest2?page=$page" } - } val jsonResponse = webClient.httpGet(listUrl.toAbsoluteUrl(apiDomain)).parseJson() return jsonResponse.getJSONArray("result") .mapJSON { jo -> @@ -119,6 +81,29 @@ class YurinekoParser(context: MangaLoaderContext) : PagedMangaParser(context, Ma } } + override suspend fun getDetails(manga: Manga): Manga { + val response = webClient.httpGet(manga.url.toAbsoluteUrl(apiDomain)).parseJson() + val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US) + return manga.copy( + chapters = response.getJSONArray("chapters") + .toJSONList() + .mapChapters(true) { i, jo -> + val mangaId = jo.getInt("mangaID") + val chapterId = jo.getInt("id") + MangaChapter( + id = generateUid(chapterId.toLong()), + name = jo.getString("name"), + number = i + 1, + scanlator = null, + url = "/read/$mangaId/$chapterId", + uploadDate = df.tryParse(jo.getString("date")), + branch = null, + source = source, + ) + }, + ) + } + override suspend fun getPages(chapter: MangaChapter): List { val jsonData = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml() .requireElementById("__NEXT_DATA__") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vmp/VmpParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vmp/VmpParser.kt index 5cbd5844..69b9c256 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vmp/VmpParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vmp/VmpParser.kt @@ -16,8 +16,11 @@ internal abstract class VmpParser( ) : PagedMangaParser(context, source, pageSize) { override val configKeyDomain = ConfigKey.Domain(domain) + override val availableSortOrders: Set = EnumSet.of(SortOrder.UPDATED) + override val isMultipleTagsSupported = false + protected open val listUrl = "xxx/" protected open val geneUrl = "genero/" @@ -26,27 +29,42 @@ internal abstract class VmpParser( 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://$domain/") - if (!tags.isNullOrEmpty()) { - append(geneUrl) - append(tag?.key.orEmpty()) - append("/page/") - append(page.toString()) - } else { - append(listUrl) - append("/page/") - append(page.toString()) - if (!query.isNullOrEmpty()) { + append("https://") + append(domain) + append('/') + when (filter) { + + is MangaListFilter.Search -> { + append(listUrl) + append("/page/") + append(page.toString()) append("?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) + } + + is MangaListFilter.Advanced -> { + + if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + append(geneUrl) + append(it.key) + append("/page/") + append(page.toString()) + } + } else { + append(listUrl) + append("/page/") + append(page.toString()) + } + } + + null -> { + append(listUrl) + append("/page/") + append(page.toString()) } } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/WpComicsParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/WpComicsParser.kt index 952f365f..b6c3c5b4 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/WpComicsParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/WpComicsParser.kt @@ -1,11 +1,16 @@ package org.koitharu.kotatsu.parsers.site.wpcomics +import androidx.collection.ArrayMap +import androidx.collection.ArraySet import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import org.jsoup.nodes.Document import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.PagedMangaParser import org.koitharu.kotatsu.parsers.config.ConfigKey +import org.koitharu.kotatsu.parsers.exception.NotFoundException import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.util.* import java.text.DateFormat @@ -21,15 +26,16 @@ internal abstract class WpComicsParser( override val configKeyDomain = ConfigKey.Domain(domain) - override val isMultipleTagsSupported = false - override val availableSortOrders: Set = EnumSet.of( SortOrder.UPDATED, SortOrder.NEWEST, SortOrder.POPULARITY, + SortOrder.RATING, ) - protected open val listUrl = "/the-loai" + override val availableStates: Set = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED) + + protected open val listUrl = "/tim-truyen-nang-cao" protected open val datePattern = "dd/MM/yy" @@ -48,75 +54,136 @@ internal abstract class WpComicsParser( @JvmField protected val finished: Set = setOf( "Hoàn thành", - "Completed ", + "Completed", ) - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - val tag = tags.oneOrThrowIfMany() - val url = buildString { - append("https://") - append(domain) - append(listUrl) - - if (!tags.isNullOrEmpty()) { - append("/") - append(tag?.key.orEmpty()) - } + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + val response = + when (filter) { + is MangaListFilter.Search -> { + val url = buildString { + append("https://") + append(domain) + append("/tim-truyen?keyword=") + append(filter.query.urlEncoded()) + append("&page=") + append(page.toString()) + } + + val result = runCatchingCancellable { webClient.httpGet(url) } + val exception = result.exceptionOrNull() + if (exception is NotFoundException) { + return emptyList() + } + result.getOrThrow() + } - append("?page=") - append(page.toString()) + is MangaListFilter.Advanced -> { + val url = buildString { + append("https://") + append(domain) + val tagQuery = filter.tags.joinToString(",") { it.key } + append("/tim-truyen-nang-cao?genres=") + append(tagQuery) + append("¬genres=&gender=-1&minchapter=1&sort=") + append( + when (filter.sortOrder) { + SortOrder.UPDATED -> 0 + SortOrder.POPULARITY -> 10 + SortOrder.NEWEST -> 15 + SortOrder.RATING -> 20 + else -> throw IllegalArgumentException("Sort order ${filter.sortOrder.name} not supported") + }, + ) + filter.states.oneOrThrowIfMany()?.let { + append("&status=") + append( + when (it) { + MangaState.ONGOING -> "1" + MangaState.FINISHED -> "2" + else -> "-1" + }, + ) + } + append("&page=") + append(page.toString()) + } + + webClient.httpGet(url) + } - if (!query.isNullOrEmpty()) { - append("&keyword=") - append(query.urlEncoded()) + null -> { + val url = buildString { + append("https://") + append(domain) + append("/tim-truyen-nang-cao?genres=¬genres=&gender=-1&status=-1&minchapter=1&sort=0&page=") + append(page.toString()) + } + webClient.httpGet(url) + } } - - append("&sort=") - when (sortOrder) { - SortOrder.POPULARITY -> append("10") - SortOrder.UPDATED -> append("") - SortOrder.NEWEST -> append("15") - else -> append("") + val itemsElements = response.parseHtml() + .select("div.ModuleContent > div.items") + .select("div.item") + return itemsElements.mapNotNull { item -> + val tooltipElement = item.selectFirst("div.box_tootip") ?: return@mapNotNull null + val absUrl = item.selectFirst("div.image > a")?.attrAsAbsoluteUrlOrNull("href") ?: return@mapNotNull null + val slug = absUrl.substringAfterLast('/') + val mangaState = when (tooltipElement.selectFirst("div.message_main > p:contains(Tình trạng)")?.ownText()) { + "Đang tiến hành" -> MangaState.ONGOING + "Hoàn thành" -> MangaState.FINISHED + else -> null } - } - val doc = webClient.httpGet(url).parseHtml() - return doc.select("div.item").map { div -> - val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") + val tagMap = getOrCreateTagMap() + val tagsElement = tooltipElement.selectFirst("div.message_main > p:contains(Thể loại)")?.ownText().orEmpty() + val mangaTags = tagsElement.split(',').mapNotNullToSet { tagMap[it.trim()] } Manga( - id = generateUid(href), - url = href, - publicUrl = href.toAbsoluteUrl(div.host ?: domain), - coverUrl = div.selectFirst("img")?.src().orEmpty(), - title = div.selectFirstOrThrow("h3").text().orEmpty(), + id = generateUid(slug), + title = tooltipElement.selectFirst("div.title")?.text().orEmpty(), altTitle = null, + url = absUrl.toRelativeUrl(domain), + publicUrl = absUrl, rating = RATING_UNKNOWN, - tags = emptySet(), - author = null, - state = null, + isNsfw = false, + coverUrl = item.selectFirst("div.image a img")?.absUrl("data-original").orEmpty(), + largeCoverUrl = null, + tags = mangaTags, + state = mangaState, + author = tooltipElement.selectFirst("div.message_main > p:contains(Tác giả)")?.ownText(), + description = tooltipElement.selectFirst("div.box_text")?.text(), + chapters = null, source = source, - isNsfw = isNsfwSource, ) } } override suspend fun getAvailableTags(): Set { - val doc = webClient.httpGet("https://$domain$listUrl").parseHtml() - return doc.select("div.genres ul li:not(.active)").mapNotNullToSet { li -> - val a = li.selectFirst("a") ?: return@mapNotNullToSet null - val href = a.attr("href").removeSuffix('/').substringAfterLast('/') - MangaTag( - key = href, - title = a.text(), - source = source, - ) + val map = getOrCreateTagMap() + val tagSet = ArraySet(map.size) + for (entry in map) { + tagSet.add(entry.value) } + return tagSet + } + + + private val mutex = Mutex() + private var tagCache: ArrayMap? = null + + private suspend fun getOrCreateTagMap(): ArrayMap = mutex.withLock { + tagCache?.let { return@withLock it } + val doc = webClient.httpGet("/tim-truyen-nang-cao".toAbsoluteUrl(domain)).parseHtml() + val tagItems = doc.select("div.genre-item") + val result = ArrayMap(tagItems.size) + for (item in tagItems) { + val title = item.text().trim() + val key = item.select("span[data-id]").attr("data-id") + result[title] = MangaTag(title = title, key = key, source = source) + } + tagCache = result + result } protected open val selectDesc = "div.detail-content p" @@ -127,13 +194,9 @@ internal abstract class WpComicsParser( 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 stateDiv = doc.selectFirst(selectState) - val state = stateDiv?.let { when (it.text()) { in ongoing -> MangaState.ONGOING @@ -141,17 +204,8 @@ internal abstract class WpComicsParser( else -> null } } - val aut = doc.body().select(selectAut).text() - manga.copy( - tags = doc.body().select(selectTag).mapNotNullToSet { a -> - MangaTag( - key = a.attr("href").removeSuffix('/').substringAfterLast('/'), - title = a.text().toTitleCase(), - source = source, - ) - }, description = desc, altTitle = null, author = aut, @@ -165,19 +219,16 @@ internal abstract class WpComicsParser( protected open val selectChapter = "div#nt_listchapter li:not(.heading)" protected open suspend fun getChapters(doc: Document): List { - return doc.body().select(selectChapter).mapChapters(reversed = true) { i, li -> val a = li.selectFirstOrThrow("a") val href = a.attrAsRelativeUrl("href") val dateText = li.selectFirst(selectDate)?.text() - val findHours = dateText?.contains(":") val dateFormat = if (findHours == true) { SimpleDateFormat("HH:mm dd/MM", sourceLocale) } else { SimpleDateFormat(datePattern, sourceLocale) } - MangaChapter( id = generateUid(href), name = a.text(), @@ -295,5 +346,4 @@ internal abstract class WpComicsParser( else -> 0 } } - } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/en/XoxoComics.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/en/XoxoComics.kt index 581cacfa..047b3cbf 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/en/XoxoComics.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/en/XoxoComics.kt @@ -1,5 +1,7 @@ package org.koitharu.kotatsu.parsers.site.wpcomics.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.model.* @@ -9,11 +11,13 @@ import java.util.EnumSet @MangaSourceParser("XOXOCOMICS", "XoxoComics", "en", ContentType.COMICS) internal class XoxoComics(context: MangaLoaderContext) : - WpComicsParser(context, MangaSource.XOXOCOMICS, "xoxocomics.net", 50) { + WpComicsParser(context, MangaSource.XOXOCOMICS, "xoxocomic.com", 50) { - override val listUrl = "/genre" + override val listUrl = "/comic-list" override val datePattern = "MM/dd/yyyy" + override val isMultipleTagsSupported = false + override val availableSortOrders: Set = EnumSet.of( SortOrder.UPDATED, SortOrder.NEWEST, @@ -21,46 +25,66 @@ internal class XoxoComics(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) + when (filter) { - if (!query.isNullOrEmpty()) { - append("/search?keyword=") - append(query.urlEncoded()) - append("&page=") - append(page.toString()) - } else { - append(listUrl) - if (!tags.isNullOrEmpty()) { - append("/") - append(tag?.key.orEmpty()) + is MangaListFilter.Search -> { + append("/search-comic?keyword=") + append(filter.query.urlEncoded()) + append("&page=") + append(page.toString()) } - append("/") - when (sortOrder) { - SortOrder.POPULARITY -> append("popular") - SortOrder.UPDATED -> append("") - SortOrder.NEWEST -> append("newest") - SortOrder.ALPHABETICAL -> append("alphabet") - else -> append("") - } + is MangaListFilter.Advanced -> { + + if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + append("/") + append(it.key) + } + } + + filter.states.oneOrThrowIfMany()?.let { + append( + when (it) { + MangaState.ONGOING -> "/ongoing" + MangaState.FINISHED -> "/completed" + else -> "" + }, + ) + if (filter.tags.isEmpty()) { + append("-comic") + } + } + + if (filter.states.isEmpty() && filter.tags.isEmpty()) { + append(listUrl) + } - append("?page=") - append(page.toString()) + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("/popular") + SortOrder.UPDATED -> append("/latest") + SortOrder.NEWEST -> append("/newest") + SortOrder.ALPHABETICAL -> append("") + else -> append("/latest") + } + append("?page=") + append(page.toString()) + } + null -> { + append(listUrl) + append("/?page=") + append(page.toString()) + } } } val doc = webClient.httpGet(url).parseHtml() - return doc.select("div.item").map { div -> + return doc.select("div.item, #nt_listchapter nav ul li").map { div -> val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") Manga( id = generateUid(href), @@ -79,12 +103,53 @@ internal class XoxoComics(context: MangaLoaderContext) : } } + override suspend fun getAvailableTags(): Set { + val doc = webClient.httpGet("https://$domain$listUrl").parseHtml() + return doc.select("div.genres ul li:not(.active)").mapNotNullToSet { li -> + val a = li.selectFirst("a") ?: return@mapNotNullToSet null + val href = a.attr("href").removeSuffix('/').substringAfterLast('/') + MangaTag( + key = href, + title = a.text(), + source = source, + ) + } + } - override suspend fun getPages(chapter: MangaChapter): List { - val fullUrl = chapter.url.toAbsoluteUrl(domain) + "/all" + 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 stateDiv = doc.selectFirst(selectState) + val state = stateDiv?.let { + when (it.text()) { + in ongoing -> MangaState.ONGOING + in finished -> MangaState.FINISHED + else -> null + } + } + val aut = doc.body().select(selectAut).text() + manga.copy( + tags = doc.body().select(selectTag).mapNotNullToSet { a -> + MangaTag( + key = a.attr("href").removeSuffix('/').substringAfterLast('/'), + title = a.text().toTitleCase(), + source = source, + ) + }, + description = desc, + altTitle = null, + author = aut, + state = state, + chapters = chaptersDeferred.await(), + ) + } + override suspend fun getPages(chapter: MangaChapter): List { + val fullUrl = chapter.url.toAbsoluteUrl(domain) + "/all" + val doc = webClient.httpGet(fullUrl).parseHtml() return doc.select(selectPage).map { url -> val img = url.src()?.toRelativeUrl(domain) ?: url.parseFailed("Image src not found") MangaPage( diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyen.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyen.kt new file mode 100644 index 00000000..81691cf7 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyen.kt @@ -0,0 +1,19 @@ +package org.koitharu.kotatsu.parsers.site.wpcomics.vi + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.config.ConfigKey +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.site.wpcomics.WpComicsParser + +@MangaSourceParser("NETTRUYEN", "NetTruyen", "vi") +internal class NetTruyen(context: MangaLoaderContext) : + WpComicsParser(context, MangaSource.NETTRUYEN, "www.nettruyenlive.com", 36) { + override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain( + "www.nettruyenlive.com", + "www.nettruyenio.com", + "www.nettruyento.com", + "nettruyento.com", + "nettruyenin.com", + ) +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/Nettruyenmax.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/Nettruyenmax.kt index 37868bde..070093fb 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/Nettruyenmax.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/Nettruyenmax.kt @@ -7,6 +7,4 @@ import org.koitharu.kotatsu.parsers.site.wpcomics.WpComicsParser @MangaSourceParser("NETTRUYENMAX", "NettruyenMax", "vi") internal class Nettruyenmax(context: MangaLoaderContext) : - WpComicsParser(context, MangaSource.NETTRUYENMAX, "www.nettruyenus.com", 36) { - override val listUrl = "/tim-truyen" -} + WpComicsParser(context, MangaSource.NETTRUYENMAX, "www.nettruyenus.com", 36) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zmanga/ZMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zmanga/ZMangaParser.kt index 486f0b25..6c928a53 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zmanga/ZMangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zmanga/ZMangaParser.kt @@ -21,13 +21,9 @@ internal abstract class ZMangaParser( override val configKeyDomain = ConfigKey.Domain(domain) - override val availableSortOrders: Set = EnumSet.of( - SortOrder.UPDATED, - SortOrder.POPULARITY, - SortOrder.ALPHABETICAL, - SortOrder.NEWEST, - SortOrder.RATING, - ) + override val availableSortOrders: Set = EnumSet.allOf(SortOrder::class.java) + + override val availableStates: Set = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED) protected open val listUrl = "advanced-search/" protected open val datePattern = "MMMM d, yyyy" @@ -50,42 +46,56 @@ internal abstract class ZMangaParser( "Completed", ) - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { val url = buildString { append("https://") append(domain) - append("/$listUrl") + append('/') + append(listUrl) if (page > 1) { append("page/") append(page.toString()) - append("/") + append('/') } - append("?order=") - when (sortOrder) { - SortOrder.POPULARITY -> append("popular") - SortOrder.UPDATED -> append("update") - SortOrder.ALPHABETICAL -> append("title") - SortOrder.NEWEST -> append("latest") - SortOrder.RATING -> append("rating") - } - if (!query.isNullOrEmpty()) { - append("&title=") - append(query.urlEncoded()) - } + when (filter) { - if (!tags.isNullOrEmpty()) { - for (tag in tags) { - append("&") - append("genre[]".urlEncoded()) - append("=") - append(tag.key) + is MangaListFilter.Search -> { + append("&title=") + append(filter.query.urlEncoded()) } + + is MangaListFilter.Advanced -> { + + append("?order=") + when (filter.sortOrder) { + SortOrder.POPULARITY -> append("popular") + SortOrder.UPDATED -> append("update") + SortOrder.ALPHABETICAL -> append("title") + SortOrder.NEWEST -> append("latest") + SortOrder.RATING -> append("rating") + } + + filter.tags.forEach { + append("&") + append("genre[]".urlEncoded()) + append("=") + append(it.key) + } + + filter.states.oneOrThrowIfMany()?.let { + append("&status=") + append( + when (it) { + MangaState.ONGOING -> "ongoing" + MangaState.FINISHED -> "completed" + else -> "" + }, + ) + } + } + + null -> append("?order=update") } }