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 99573e13..b7687010 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 @@ -201,7 +201,7 @@ internal class ComickFunParser(context: MangaLoaderContext) : source = source, ) }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), chapters = getChapters(comic.getString("hid")), ) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ExHentaiParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ExHentaiParser.kt index c4d04d82..dbea6816 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ExHentaiParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ExHentaiParser.kt @@ -184,7 +184,7 @@ internal class ExHentaiParser( rawTitle.contains("(ongoing)", ignoreCase = true) -> MangaState.ONGOING else -> null }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), source = source, ) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/HitomiLaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/HitomiLaParser.kt index 2d2607bc..480da7d3 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/HitomiLaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/HitomiLaParser.kt @@ -558,7 +558,7 @@ internal class HitomiLaParser(context: MangaLoaderContext) : LegacyMangaParser(c "https://${getDomain("${subDomain}a")}/webp/$commonId$imageId/$hash.webp" }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), publicUrl = json.getString("galleryurl").toAbsoluteUrl(domain), tags = buildSet 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 31404ebd..04f32d8b 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 @@ -171,7 +171,7 @@ internal class ImHentai(context: MangaLoaderContext) : source = source, ) }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), chapters = listOf( MangaChapter( id = manga.id, 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 27a99cdb..5e9e0f9e 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 @@ -134,7 +134,7 @@ internal abstract class LineWebtoonsParser( coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain), largeCoverUrl = jo.getStringOrNull("thumbnailVertical")?.toAbsoluteUrl(staticDomain), tags = setOf(parseTag(jo.getJSONObject("genreInfo"))), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = jo.getString("synopsis"), // I don't think the API provides this info state = null, @@ -165,7 +165,7 @@ internal abstract class LineWebtoonsParser( coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain), largeCoverUrl = null, tags = emptySet(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = null, state = null, source = source, @@ -211,7 +211,7 @@ internal abstract class LineWebtoonsParser( coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain), largeCoverUrl = jo.getStringOrNull("thumbnailVertical")?.toAbsoluteUrl(staticDomain), tags = setOfNotNull(genres[jo.getString("representGenre")]), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = jo.getString("synopsis"), // I don't think the API provides this info state = null, diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaFireParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaFireParser.kt index 0a13f85d..b64a7a3d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaFireParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaFireParser.kt @@ -213,7 +213,7 @@ internal abstract class MangaFireParser( else -> null } }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = document.selectFirstOrThrow("#synopsis div.modal-content").html(), chapters = getChapters(manga.url, document), ) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPark.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPark.kt index b9b4efc7..f3f0361e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPark.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPark.kt @@ -196,7 +196,7 @@ internal class MangaPark(context: MangaLoaderContext) : val author = doc.selectFirst("div[q:key=tz_4]")?.textOrNull() manga.copy( altTitles = setOfNotNull(doc.selectFirst("div[q:key=tz_2]")?.textOrNull()), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = doc.selectFirst("react-island[q:key=0a_9]")?.html(), state = when (doc.selectFirst("span[q:key=Yn_5]")?.text()?.lowercase()) { "ongoing" -> MangaState.ONGOING diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaReaderToParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaReaderToParser.kt index 50198ca1..510f0030 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaReaderToParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaReaderToParser.kt @@ -203,7 +203,7 @@ internal class MangaReaderToParser(context: MangaLoaderContext) : else -> null } }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = document.select("div.description").html(), chapters = parseChapters(document), source = source, 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 12695333..97076f16 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 @@ -140,7 +140,7 @@ internal abstract class NineMangaParser( title = root.selectFirst("h1[itemprop=name]")?.textOrNull()?.removeSuffix("Manga")?.trimEnd() ?: manga.title, tags = tags.orEmpty(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = parseStatus(infoRoot.select("li a.red").text()), description = infoRoot.getElementsByAttributeValue("itemprop", "description").first()?.html() ?.substringAfter(""), diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NineNineNineHentaiParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NineNineNineHentaiParser.kt index 43e01f0e..1f194126 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NineNineNineHentaiParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NineNineNineHentaiParser.kt @@ -270,7 +270,7 @@ internal class NineNineNineHentaiParser(context: MangaLoaderContext) : altTitles = setOf(name), coverUrl = cover.first, largeCoverUrl = cover.second, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), contentRating = ContentRating.ADULT, tags = tags?.mapToSet { MangaTag( diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/WebtoonsParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/WebtoonsParser.kt index 9e3f9a98..8df0a8b1 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/WebtoonsParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/WebtoonsParser.kt @@ -137,7 +137,7 @@ internal abstract class WebtoonsParser( coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain), largeCoverUrl = jo.getStringOrNull("thumbnailVertical")?.toAbsoluteUrl(staticDomain), tags = setOf(parseTag(jo.getJSONObject("genreInfo"))), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = jo.getString("synopsis"), // I don't think the API provides this info, state = null, @@ -170,7 +170,7 @@ internal abstract class WebtoonsParser( title = jo.getString("title"), coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain), altTitles = emptySet(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), contentRating = if (isNsfwSource) ContentRating.ADULT else null, rating = jo.getFloatOrDefault("starScoreAverage", -10f) / 10f, tags = setOfNotNull(allGenreCache.get()[jo.getString("representGenre")]), @@ -213,7 +213,7 @@ internal abstract class WebtoonsParser( coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain), largeCoverUrl = null, tags = emptySet(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = null, state = null, source = source, diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/cupfox/CupFoxParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/cupfox/CupFoxParser.kt index e213e57d..cb7c9cd8 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/cupfox/CupFoxParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/cupfox/CupFoxParser.kt @@ -134,7 +134,7 @@ internal abstract class CupFoxParser( source = source, ) }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = doc.selectFirst(selectMangaDescription)?.html(), chapters = doc.select(selectMangaChapters) .mapChapters { i, li -> 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 45c04e00..9846202c 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 @@ -114,7 +114,7 @@ internal class BeeToon(context: MangaLoaderContext) : source = source, ) }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), chapters = doc.select(".items-chapters a").mapChapters(reversed = true) { i, a -> val url = a.attrAsRelativeUrl("href").toAbsoluteUrl(domain) MangaChapter( 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 4d1e0ad3..26f1cef7 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 @@ -119,7 +119,7 @@ internal class ComicExtra(context: MangaLoaderContext) : source = source, ) }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = doc.selectFirstOrThrow("div.detail-desc-content p").html(), chapters = doc.select("ul.basic-list li").let { elements -> elements.mapChapters { i, li -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FlameComics.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FlameComics.kt index 6e633129..10292916 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FlameComics.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FlameComics.kt @@ -154,7 +154,7 @@ internal class FlameComics(context: MangaLoaderContext) : "Ongoing" -> MangaState.ONGOING else -> null }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), largeCoverUrl = if (cover != null) { imageUrl(seriesId, cover, 640) } else { 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 c40828a9..a477e5fe 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 @@ -90,7 +90,7 @@ internal class MangaGeko(context: MangaLoaderContext) : coverUrl = div.selectFirstOrThrow("img").src(), tags = emptySet(), state = null, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), source = source, ) } @@ -125,7 +125,7 @@ internal class MangaGeko(context: MangaLoaderContext) : source = source, ) }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = doc.selectFirstOrThrow(".description").html(), chapters = chaptersDeferred.await(), ) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaKawaiiEn.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaKawaiiEn.kt index 9a58f852..a14f77f5 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaKawaiiEn.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaKawaiiEn.kt @@ -105,7 +105,7 @@ internal class MangaKawaiiEn(context: MangaLoaderContext) : altTitles = doc.select("span[itemprop*=alternativeHeadline]").mapNotNullToSet { it.textOrNull() }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = when (doc.selectFirst("span.badge.bg-success.text-uppercase")?.text()) { "Ongoing" -> MangaState.ONGOING "" -> MangaState.FINISHED 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 eccbee5e..96473551 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 @@ -121,7 +121,7 @@ internal class MangaTownParser(context: MangaLoaderContext) : altTitles = emptySet(), rating = li.selectFirst("p.score")?.selectFirst("b") ?.ownText()?.toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = when (status) { "ongoing" -> MangaState.ONGOING "completed" -> MangaState.FINISHED diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manhwa18Com.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manhwa18Com.kt index 5418dff2..8a585bd3 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manhwa18Com.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manhwa18Com.kt @@ -168,7 +168,7 @@ internal class Manhwa18Com(context: MangaLoaderContext) : cardInfoElement?.selectFirst("b:contains(Other names)")?.parent()?.ownTextOrNull() ?.removePrefix(": "), ), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = docs.selectFirst(".series-summary .summary-content")?.html(), tags = tags.orEmpty(), state = state, 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 2f9e3a9c..76b2788e 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 @@ -168,7 +168,7 @@ internal class Manhwa18Parser(context: MangaLoaderContext) : cardInfoElement?.selectFirst("b:contains(Other names)")?.parent()?.ownTextOrNull() ?.removePrefix(": "), ), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = docs.selectFirst(".series-summary .summary-content")?.html(), tags = tags.orEmpty(), state = state, diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MyComicList.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MyComicList.kt index 61013b19..c26f96d7 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MyComicList.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MyComicList.kt @@ -97,7 +97,7 @@ internal class MyComicList(context: MangaLoaderContext) : source = source, ) }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = when (doc.selectFirst("td:contains(Status:) + td a")?.text()?.lowercase()) { "ongoing" -> MangaState.ONGOING "completed" -> MangaState.FINISHED 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 f7cd5a85..27606d4b 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 @@ -71,7 +71,7 @@ internal class Po2Scans(context: MangaLoaderContext) : else -> null }, tags = emptySet(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = doc.selectFirstOrThrow(".summary").html(), chapters = doc.select(".chap-section .chap") .mapChapters(reversed = true) { i, div -> 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 8618e2a3..f5dd3736 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 @@ -145,7 +145,7 @@ internal class Pururin(context: MangaLoaderContext) : source = source, ) }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), chapters = listOf( MangaChapter( id = manga.id, diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/WeebCentral.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/WeebCentral.kt index 59d70e2e..d54737b6 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/WeebCentral.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/WeebCentral.kt @@ -211,7 +211,7 @@ internal class WeebCentral(context: MangaLoaderContext) : LegacyMangaParser(cont "Hiatus" -> PAUSED else -> null }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), largeCoverUrl = null, chapters = null, source = source, 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 37549193..857a6ac1 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 @@ -187,7 +187,7 @@ internal class TuMangaOnlineParser(context: MangaLoaderContext) : LegacyPagedMan }, largeCoverUrl = contents.selectFirst(".book-thumbnail")?.attrAsAbsoluteUrlOrNull("src"), state = parseStatus(contents.select("span.book-status").text().orEmpty()), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), chapters = if (doc.select("div.chapters").isEmpty()) { doc.select(oneShotChapterListSelector).mapChapters(reversed = true) { _, item -> oneShotChapterFromElement(item) 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 fb7e80ef..d7024c86 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 @@ -126,7 +126,7 @@ internal abstract class FoolSlideParser( manga.copy( coverUrl = doc.selectFirst(".thumbnail img")?.src() ?: manga.coverUrl, description = desc?.nullIfEmpty(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), chapters = chapters, ) } 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 eddb755d..0b004c1f 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 @@ -160,7 +160,7 @@ internal class BentomangaParser(context: MangaLoaderContext) : "En pause" -> MangaState.PAUSED else -> null }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), 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/LegacyScansParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LegacyScansParser.kt index 51aa1323..775f20ff 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 @@ -180,7 +180,7 @@ internal class LegacyScansParser(context: MangaLoaderContext) : ) }, coverUrl = root.selectFirst("div.serieImg img")?.attrAsAbsoluteUrlOrNull("src"), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = root.selectFirst("div.serieDescription div")?.html(), chapters = root.select("div.chapterList a") .mapChapters(reversed = true) { i, a -> 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 e2d6553b..0e319639 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 @@ -106,7 +106,7 @@ internal class LireScan(context: MangaLoaderContext) : LegacyPagedMangaParser(co source = source, ) }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = root.selectFirst("div.pmovie__text")?.html(), chapters = root.select("ul li div.chapter") .mapChapters(reversed = true) { i, 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 88ed9395..228d0f44 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 @@ -169,7 +169,7 @@ internal class LugnicaScans(context: MangaLoaderContext) : "3" -> MangaState.ABANDONED else -> null }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = jsonManga.getStringOrNull("description"), chapters = chapters.mapChapters { i, it -> val id = it.substringAfter("\"chapter\":").substringBefore(",") 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 08be9968..4b35c0ed 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 @@ -138,7 +138,7 @@ internal class ScantradUnion(context: MangaLoaderContext) : source = source, ) }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = root.selectFirst("p.sContent")?.html(), chapters = root.select("div.chapter-list li") .mapChapters(reversed = true) { i, li -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fuzzydoodle/FuzzyDoodleParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fuzzydoodle/FuzzyDoodleParser.kt index 175d2cb3..8222960e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fuzzydoodle/FuzzyDoodleParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fuzzydoodle/FuzzyDoodleParser.kt @@ -192,7 +192,7 @@ internal abstract class FuzzyDoodleParser( in paused -> MangaState.PAUSED else -> null }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = doc.select(selectDescription).html(), tags = doc.select(selectTagManga).mapToSet { val key = it.attr("href").substringAfterLast('=') 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 0e76a9ff..74edbcbd 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 @@ -169,7 +169,7 @@ internal abstract class GalleryAdultsParser( return manga.copy( tags = tag.orEmpty(), title = doc.selectFirst(selectTitle)?.textOrNull()?.cleanupTitle() ?: manga.title, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), chapters = listOf( MangaChapter( id = manga.id, 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 8f6dda6b..d5a35e63 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 @@ -129,7 +129,7 @@ internal class HentaiEra(context: MangaLoaderContext) : val author = doc.selectFirst(selectAuthor)?.text() return manga.copy( tags = tag.orEmpty(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), chapters = listOf( MangaChapter( id = manga.id, diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/GattsuParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/GattsuParser.kt index 6886be3f..9af0eb07 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/GattsuParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/GattsuParser.kt @@ -116,7 +116,7 @@ internal abstract class GattsuParser( description = doc.selectFirst("div.post-texto")?.html(), tags = doc.selectFirst(".post-itens li:contains(Tags), .paginaPostInfo li:contains(Categorias)") ?.parseTags().orEmpty(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), chapters = listOf( MangaChapter( id = manga.id, diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/guya/GuyaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/guya/GuyaParser.kt index 5d211586..8e4edaa9 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/guya/GuyaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/guya/GuyaParser.kt @@ -75,7 +75,7 @@ internal abstract class GuyaParser( tags = emptySet(), description = j.getString("description"), state = null, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), contentRating = if (isNsfwSource) ContentRating.ADULT else null, source = source, ) 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 ff922a5b..c30d21a4 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 @@ -143,7 +143,7 @@ internal class DoujinDesuParser(context: MangaLoaderContext) : } val author = metadataEl?.selectFirst("tr:contains(Author)")?.selectLast("td")?.text() return manga.copy( - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = docs.selectFirst(".wrapper > .metadata > .pb-2")?.selectFirst("p")?.html(), state = state, rating = metadataEl?.selectFirst(".rating-prc")?.ownText()?.toFloatOrNull()?.div(10f) ?: RATING_UNKNOWN, diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/HentaiCrot.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/HentaiCrot.kt index 79ae0a1e..2174d4b1 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/HentaiCrot.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/HentaiCrot.kt @@ -101,7 +101,7 @@ internal class HentaiCrot(context: MangaLoaderContext) : altTitles = setOfNotNull( doc.selectFirst("div.entry-content ul li:contains(Alternative Name(s) :) em")?.textOrNull(), ), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = null, chapters = listOf( MangaChapter( diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/PixHentai.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/PixHentai.kt index bac6a457..464a845f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/PixHentai.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/PixHentai.kt @@ -101,7 +101,7 @@ internal class PixHentai(context: MangaLoaderContext) : altTitles = setOfNotNull( doc.selectFirst("div.entry-content ul li:contains(Alternative Name(s) :) em")?.textOrNull(), ), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = null, chapters = listOf( MangaChapter( diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/iken/IkenParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/iken/IkenParser.kt index ed5de57d..c271b7bb 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/iken/IkenParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/iken/IkenParser.kt @@ -113,7 +113,7 @@ internal abstract class IkenParser( description = it.getString("postContent"), rating = RATING_UNKNOWN, tags = emptySet(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = when (it.getString("seriesStatus")) { "ONGOING" -> MangaState.ONGOING "COMPLETED" -> MangaState.FINISHED 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 60dd0e9b..1a10bb63 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 @@ -90,7 +90,7 @@ internal class NicovideoSeigaParser(context: MangaLoaderContext) : title = item.selectFirst(".mg_body > .title > a")?.text() ?: return@mapNotNull null, coverUrl = item.selectFirst(".comic_icon > div > a > img")?.attrAsAbsoluteUrl("src"), altTitles = emptySet(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), rating = RATING_UNKNOWN, url = href, contentRating = null, 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 1fab92f2..9263b258 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 @@ -159,7 +159,7 @@ internal abstract class LikeMangaParser( source = source, ) }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = doc.requireElementById("summary_shortened").html(), chapters = run { if (maxPageChapter == 1) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/liliana/LilianaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/liliana/LilianaParser.kt index ff1eb9b7..67ac4b58 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/liliana/LilianaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/liliana/LilianaParser.kt @@ -161,7 +161,7 @@ internal abstract class LilianaParser( source = source, ) }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = when (doc.selectFirst("div.y6x11p i.fas.fa-rss + span.dt")?.text()?.lowercase().orEmpty()) { in ongoing -> MangaState.ONGOING in finished -> MangaState.FINISHED 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 9129b570..61d24f08 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 @@ -477,7 +477,7 @@ internal abstract class MadaraParser( source = source, ) }.orEmpty(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = when ( summary?.selectFirst(".mg_status") ?.selectFirst(".summary-content") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/FireScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/FireScans.kt index f3b55cde..baf37522 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/FireScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/FireScans.kt @@ -34,7 +34,7 @@ internal class FireScans(context: MangaLoaderContext) : source = source, ) }.orEmpty(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = when ( summary?.selectFirst(".mg_status") ?.selectFirst(".summary-content") 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 70c3fb43..d99cbdef 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 @@ -119,7 +119,7 @@ internal class Hentai4Free(context: MangaLoaderContext) : source = source, ) }.orEmpty(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = when (summary?.selectFirst(".mg_status")?.selectFirst(".summary-content")?.ownText() ?.lowercase().orEmpty()) { in ongoing -> MangaState.ONGOING 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 d0379853..c8481a3b 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 @@ -93,7 +93,7 @@ internal class IsekaiScan(context: MangaLoaderContext) : source = source, ) }.orEmpty(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = when (summary?.selectFirst(".mg_status")?.selectFirst(".summary-content")?.ownText() ?.lowercase()) { "ongoing" -> MangaState.ONGOING 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 e9ef3c4f..2ef4ec3b 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 @@ -116,7 +116,7 @@ internal class IsekaiScanEuParser(context: MangaLoaderContext) : source = source, ) }.orEmpty(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = when (summary?.selectFirst(".mg_status")?.selectFirst(".summary-content")?.ownText() ?.lowercase().orEmpty()) { in ongoing -> MangaState.ONGOING 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 f165c0b9..cbbedfea 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 @@ -91,7 +91,7 @@ internal class MangaDass(context: MangaLoaderContext) : source = source, ) }.orEmpty(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = when (summary?.selectFirst(".mg_status")?.selectFirst(".summary-content")?.ownText() ?.lowercase().orEmpty()) { in ongoing -> MangaState.ONGOING 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 d0f8fd5d..3cb6cd8f 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 @@ -84,7 +84,7 @@ internal class MangaDna(context: MangaLoaderContext) : source = source, ) }.orEmpty(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = when (summary?.selectFirst(".mg_status")?.selectFirst(".summary-content")?.ownText() ?.lowercase().orEmpty()) { in ongoing -> MangaState.ONGOING 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 b50b23ad..f283582c 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 @@ -13,145 +13,145 @@ import java.util.* @Broken @MangaSourceParser("MANGAPURE", "MangaPure", "en") internal class MangaPure(context: MangaLoaderContext) : - MadaraParser(context, MangaParserSource.MANGAPURE, "mangapure.net") { - override val tagPrefix = "mangas/" - override val listUrl = "latest-manga/" - override val datePattern = "MMMM d, HH:mm" - - override val availableSortOrders: Set = EnumSet.of( - SortOrder.POPULARITY, - SortOrder.UPDATED, - ) - - init { - paginator.firstPage = 1 - searchPaginator.firstPage = 1 - } - - override suspend fun getFilterOptions(): MangaListFilterOptions { - return super.getFilterOptions().copy( - availableStates = emptySet(), - availableContentRating = emptySet(), - ) - } - - override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val url = buildString { - append("https://") - append(domain) - when { - !filter.query.isNullOrEmpty() -> { - append("/search?s=") - append(filter.query.urlEncoded()) - append("&page=") - append(page.toString()) - append("&post_type=wp-manga") - } - - else -> { - - val tag = filter.tags.oneOrThrowIfMany() - if (filter.tags.isNotEmpty()) { - append("/$tagPrefix") - append(tag?.key.orEmpty()) - append("?orderby=") - when (order) { - SortOrder.POPULARITY -> append("2") - SortOrder.UPDATED -> append("3") - else -> append("3") - } - append("&page=") - append(page.toString()) - } else { - when (order) { - SortOrder.POPULARITY -> append("/popular-manga") - SortOrder.UPDATED -> append("/latest-manga") - else -> append("/latest-manga") - } - append("?page=") - append(page.toString()) - } - } - } - } - - val doc = webClient.httpGet(url).parseHtml() - - return doc.select("div.row.c-tabs-item__content").ifEmpty { - doc.select("div.page-item-detail.manga") - }.map { div -> - val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") - val summary = div.selectFirst(".tab-summary") ?: div.selectFirst(".item-summary") - val author = summary?.selectFirst(".mg_author")?.selectFirst("a")?.ownText() - Manga( - id = generateUid(href), - url = href, - publicUrl = href.toAbsoluteUrl(div.host ?: domain), - coverUrl = div.selectFirst("img")?.src(), - title = (summary?.selectFirst("h3") ?: summary?.selectFirst("h4"))?.text().orEmpty(), - altTitles = emptySet(), - 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(), - authors = author?.let { setOf(it) } ?: emptySet(), - state = when (summary?.selectFirst(".mg_status")?.selectFirst(".summary-content")?.ownText() - ?.lowercase()) { - "ongoing" -> MangaState.ONGOING - "completed" -> MangaState.FINISHED - else -> null - }, - source = source, - contentRating = if (isNsfwSource) ContentRating.ADULT else null, - ) - } - } - - override suspend fun loadChapters(mangaUrl: String, document: Document): List { - - - val mangaId = document.select("div[id^=manga-chapters-holder]").attr("data-id") - - val doc = webClient.httpGet("https://$domain/ajax-list-chapter?mangaID=$mangaId").parseHtml() - - val dateFormat = SimpleDateFormat(datePattern, sourceLocale) - - return doc.select(selectChapter).mapChapters(reversed = true) { i, li -> - val a = li.selectFirst("a") - val href = a?.attrAsRelativeUrlOrNull("href") ?: li.parseFailed("Link is missing") - val link = href + stylePage - MangaChapter( - id = generateUid(href), - url = link, - name = a.ownText(), - number = i + 1f, - volume = 0, - branch = null, - uploadDate = parseChapterDate( - dateFormat, - li.selectFirst(selectDate)?.text(), - ), - scanlator = null, - source = source, - ) - } - } - - 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 -> - MangaPage( - id = generateUid(url), - url = url, - preview = null, - source = source, - ) - } - } + MadaraParser(context, MangaParserSource.MANGAPURE, "mangapure.net") { + override val tagPrefix = "mangas/" + override val listUrl = "latest-manga/" + override val datePattern = "MMMM d, HH:mm" + + override val availableSortOrders: Set = EnumSet.of( + SortOrder.POPULARITY, + SortOrder.UPDATED, + ) + + init { + paginator.firstPage = 1 + searchPaginator.firstPage = 1 + } + + override suspend fun getFilterOptions(): MangaListFilterOptions { + return super.getFilterOptions().copy( + availableStates = emptySet(), + availableContentRating = emptySet(), + ) + } + + override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val url = buildString { + append("https://") + append(domain) + when { + !filter.query.isNullOrEmpty() -> { + append("/search?s=") + append(filter.query.urlEncoded()) + append("&page=") + append(page.toString()) + append("&post_type=wp-manga") + } + + else -> { + + val tag = filter.tags.oneOrThrowIfMany() + if (filter.tags.isNotEmpty()) { + append("/$tagPrefix") + append(tag?.key.orEmpty()) + append("?orderby=") + when (order) { + SortOrder.POPULARITY -> append("2") + SortOrder.UPDATED -> append("3") + else -> append("3") + } + append("&page=") + append(page.toString()) + } else { + when (order) { + SortOrder.POPULARITY -> append("/popular-manga") + SortOrder.UPDATED -> append("/latest-manga") + else -> append("/latest-manga") + } + append("?page=") + append(page.toString()) + } + } + } + } + + val doc = webClient.httpGet(url).parseHtml() + + return doc.select("div.row.c-tabs-item__content").ifEmpty { + doc.select("div.page-item-detail.manga") + }.map { div -> + val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") + val summary = div.selectFirst(".tab-summary") ?: div.selectFirst(".item-summary") + val author = summary?.selectFirst(".mg_author")?.selectFirst("a")?.ownText() + Manga( + id = generateUid(href), + url = href, + publicUrl = href.toAbsoluteUrl(div.host ?: domain), + coverUrl = div.selectFirst("img")?.src(), + title = (summary?.selectFirst("h3") ?: summary?.selectFirst("h4"))?.text().orEmpty(), + altTitles = emptySet(), + 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(), + authors = setOfNotNull(author), + state = when (summary?.selectFirst(".mg_status")?.selectFirst(".summary-content")?.ownText() + ?.lowercase()) { + "ongoing" -> MangaState.ONGOING + "completed" -> MangaState.FINISHED + else -> null + }, + source = source, + contentRating = if (isNsfwSource) ContentRating.ADULT else null, + ) + } + } + + override suspend fun loadChapters(mangaUrl: String, document: Document): List { + + + val mangaId = document.select("div[id^=manga-chapters-holder]").attr("data-id") + + val doc = webClient.httpGet("https://$domain/ajax-list-chapter?mangaID=$mangaId").parseHtml() + + val dateFormat = SimpleDateFormat(datePattern, sourceLocale) + + return doc.select(selectChapter).mapChapters(reversed = true) { i, li -> + val a = li.selectFirst("a") + val href = a?.attrAsRelativeUrlOrNull("href") ?: li.parseFailed("Link is missing") + val link = href + stylePage + MangaChapter( + id = generateUid(href), + url = link, + name = a.ownText(), + number = i + 1f, + volume = 0, + branch = null, + uploadDate = parseChapterDate( + dateFormat, + li.selectFirst(selectDate)?.text(), + ), + scanlator = null, + source = source, + ) + } + } + + 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 -> + MangaPage( + id = generateUid(url), + url = url, + preview = null, + source = source, + ) + } + } } 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 c7bb9c26..df31a917 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 @@ -8,96 +8,96 @@ import org.koitharu.kotatsu.parsers.util.* @MangaSourceParser("MANHWAZ", "ManhwaZ", "en") internal class Manhwaz(context: MangaLoaderContext) : - MadaraParser(context, MangaParserSource.MANHWAZ, "manhwaz.com", 40) { + MadaraParser(context, MangaParserSource.MANHWAZ, "manhwaz.com", 40) { - override val listUrl = "genre/manhwa" - override val tagPrefix = "genre/" - override val withoutAjax = true - override val selectTestAsync = "div.list-chapter" + override val listUrl = "genre/manhwa" + override val tagPrefix = "genre/" + override val withoutAjax = true + override val selectTestAsync = "div.list-chapter" - init { - paginator.firstPage = 1 - searchPaginator.firstPage = 1 - } + init { + paginator.firstPage = 1 + searchPaginator.firstPage = 1 + } - override suspend fun getFilterOptions() = super.getFilterOptions().copy( - availableStates = emptySet(), - availableContentRating = emptySet(), - ) + override suspend fun getFilterOptions() = super.getFilterOptions().copy( + availableStates = emptySet(), + availableContentRating = emptySet(), + ) - override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val url = buildString { - append("https://") - append(domain) - when { - !filter.query.isNullOrEmpty() -> { - append("/search?s=") - append(filter.query.urlEncoded()) - append("&page=") - append(page.toString()) - } + override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val url = buildString { + append("https://") + append(domain) + when { + !filter.query.isNullOrEmpty() -> { + append("/search?s=") + append(filter.query.urlEncoded()) + append("&page=") + append(page.toString()) + } - else -> { + 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("&") - } + 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 (order) { - SortOrder.POPULARITY -> append("views") - SortOrder.UPDATED -> append("latest") - SortOrder.NEWEST -> append("new") - SortOrder.RATING -> append("rating") - else -> append("latest") - } - } - } - } - val doc = webClient.httpGet(url).parseHtml() + append("m_orderby=") + when (order) { + SortOrder.POPULARITY -> append("views") + SortOrder.UPDATED -> append("latest") + SortOrder.NEWEST -> append("new") + SortOrder.RATING -> append("rating") + else -> append("latest") + } + } + } + } + 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") - val author = summary?.selectFirst(".mg_author")?.selectFirst("a")?.ownText() - Manga( - id = generateUid(href), - url = href, - publicUrl = href.toAbsoluteUrl(div.host ?: domain), - coverUrl = div.selectFirst("img")?.src(), - title = (summary?.selectFirst("h3") ?: summary?.selectFirst("h4"))?.text().orEmpty(), - altTitles = emptySet(), - 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(), - authors = author?.let { setOf(it) } ?: emptySet(), - state = when (summary?.selectFirst(".mg_status")?.selectFirst(".summary-content")?.ownText() - ?.lowercase().orEmpty()) { - in ongoing -> MangaState.ONGOING - in finished -> MangaState.FINISHED - else -> null - }, - source = source, - contentRating = if (isNsfwSource) ContentRating.ADULT else null, - ) - } - } + 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") + val author = summary?.selectFirst(".mg_author")?.selectFirst("a")?.ownText() + Manga( + id = generateUid(href), + url = href, + publicUrl = href.toAbsoluteUrl(div.host ?: domain), + coverUrl = div.selectFirst("img")?.src(), + title = (summary?.selectFirst("h3") ?: summary?.selectFirst("h4"))?.text().orEmpty(), + altTitles = emptySet(), + 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(), + authors = setOfNotNull(author), + state = when (summary?.selectFirst(".mg_status")?.selectFirst(".summary-content")?.ownText() + ?.lowercase().orEmpty()) { + in ongoing -> MangaState.ONGOING + in finished -> MangaState.FINISHED + else -> null + }, + source = source, + contentRating = if (isNsfwSource) ContentRating.ADULT else null, + ) + } + } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/ShibaManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/ShibaManga.kt index 2adaef70..c0247223 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/ShibaManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/ShibaManga.kt @@ -35,7 +35,7 @@ internal class ShibaManga(context: MangaLoaderContext) : source = source, ) }.orEmpty(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = when ( summary?.selectFirst(".mg_status") ?.selectFirst(".summary-content") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/MangasNoSekai.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/MangasNoSekai.kt index 2f724c71..5e049035 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/MangasNoSekai.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/MangasNoSekai.kt @@ -29,7 +29,7 @@ internal class MangasNoSekai(context: MangaLoaderContext) : source = source, ) }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = body.selectFirst("#section-sinopsis p")?.text().orEmpty(), altTitles = setOfNotNull( doc.selectFirst("section#section-sinopsis div.d-flex:has(div:contains(Otros nombres)) p") 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 2ed35df0..5a222ed0 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 @@ -83,7 +83,7 @@ internal class ManhwaHub(context: MangaLoaderContext) : source = source, ) }.orEmpty(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = when (summary?.selectFirst(".mg_status")?.selectFirst(".summary-content")?.ownText() ?.lowercase().orEmpty()) { in ongoing -> MangaState.ONGOING diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/ja/MangaFenxi.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/ja/MangaFenxi.kt index 2b049650..3e4a4c8a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/ja/MangaFenxi.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/ja/MangaFenxi.kt @@ -39,7 +39,7 @@ internal class MangaFenxi(context: MangaLoaderContext) : source = source, ) }.orEmpty(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = when ( summary?.selectFirst(".mg_status") ?.selectFirst(".summary-content") 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 78caf15a..de6e7efb 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 @@ -88,7 +88,7 @@ internal class Saytruyenhay(context: MangaLoaderContext) : source = source, ) }.orEmpty(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = when (summary?.selectFirst(".mg_status")?.selectFirst(".summary-content")?.ownText() ?.lowercase().orEmpty()) { in ongoing -> MangaState.ONGOING 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 016a19f5..93e44f22 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 @@ -275,7 +275,7 @@ internal abstract class MangaReaderParser( return manga.copy( description = docs.selectFirst(detailsDescriptionSelector)?.text(), state = mangaState, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), contentRating = if (manga.isNsfw || nsfw) { ContentRating.ADULT } else { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/ar/Normoyun.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/ar/Normoyun.kt index 0c3240a6..48577a6f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/ar/Normoyun.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/ar/Normoyun.kt @@ -110,7 +110,7 @@ internal class Normoyun(context: MangaLoaderContext) : return manga.copy( description = docs.selectFirst("span.desc")?.html(), state = mangaState, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), contentRating = if (manga.isNsfw || nsfw) { ContentRating.ADULT } else { 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 9e1cb956..1d670d40 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 @@ -133,7 +133,7 @@ internal class RizzComic(context: MangaLoaderContext) : "hiatus" -> MangaState.PAUSED else -> null }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), source = source, description = j.getString("long_description"), ) 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 1bf42080..2fbd2e0e 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,229 +13,229 @@ import java.util.* @MangaSourceParser("KOMIKCAST", "KomikCast", "id") internal class Komikcast(context: MangaLoaderContext) : - MangaReaderParser(context, MangaParserSource.KOMIKCAST, "komikcast.bz", 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 = - EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.ALPHABETICAL) - - override val filterCapabilities: MangaListFilterCapabilities - get() = super.filterCapabilities.copy( - isTagsExclusionSupported = false, - ) - - override suspend fun getFilterOptions() = super.getFilterOptions().copy( - availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED), - ) - - override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val url = buildString { - append("https://") - append(domain) - - when { - - !filter.query.isNullOrEmpty() -> { - append("/page/") - append(page.toString()) - append("/?s=") - append(filter.query.urlEncoded()) - } - - else -> { - append(listUrl) - append("/page/") - append(page.toString()) - append("/?type=") - append( - when (order) { - SortOrder.ALPHABETICAL -> "&orderby=titleasc" - SortOrder.ALPHABETICAL_DESC -> "&orderby=titledesc" - SortOrder.POPULARITY -> "&orderby=popular" - SortOrder.UPDATED -> "" // To get the Updated list, you don't need "orderby" in the url. - 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") - else -> append("") - } - } - } - } - } - } - - return parseMangaList(webClient.httpGet(url).parseHtml()) - } - - override suspend fun getDetails(manga: Manga): Manga { - val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() - val dateFormat = SimpleDateFormat(datePattern, sourceLocale) - val chapters = docs.select("#chapter-wrapper > li").mapChapters(reversed = true) { index, element -> - val url = element.selectFirst("a.chapter-link-item")?.attrAsRelativeUrl("href") ?: return@mapChapters null - MangaChapter( - id = generateUid(url), - name = element.selectFirst("a.chapter-link-item")?.ownText().orEmpty(), - url = url, - number = index + 1f, - volume = 0, - scanlator = null, - uploadDate = parseChapterDate( - dateFormat, - element.selectFirst("div.chapter-link-time")?.text(), - ), - branch = null, - source = source, - ) - } - return parseInfo(docs, manga, chapters) - } - - override suspend fun parseInfo(docs: Document, manga: Manga, chapters: List): Manga { - val tagMap = getOrCreateTagMap() - val tags = docs.select(".komik_info-content-genre > a").mapNotNullToSet { tagMap[it.text()] } - val state = docs.selectFirst(".komik_info-content-meta span:contains(Status)")?.html() - val mangaState = if (state!!.contains("Ongoing")) { - MangaState.ONGOING - } else { - MangaState.FINISHED - } - val author = docs.selectFirst(".komik_info-content-meta span:contains(Author)") - ?.lastElementChild()?.textOrNull() - val nsfw = - docs.selectFirst(".restrictcontainer") != null || docs.selectFirst(".info-right .alr") != null || docs.selectFirst( - ".postbody .alr", - ) != null - - return manga.copy( - description = docs.selectFirst("div.komik_info-description-sinopsis")?.text(), - state = mangaState, - authors = author?.let { setOf(it) } ?: emptySet(), - contentRating = if (manga.isNsfw || nsfw) { - ContentRating.ADULT - } else { - ContentRating.SAFE - }, - tags = tags, - chapters = chapters, - ) - } - - override fun parseMangaList(docs: Document): List { - return docs.select("div.list-update_item").mapNotNull { - val a = it.selectFirstOrThrow("a.data-tooltip") - val relativeUrl = a.attrAsRelativeUrl("href") - val rating = it.selectFirst(".numscore")?.text()?.toFloatOrNull()?.div(10) ?: RATING_UNKNOWN - val name = it.selectFirst("h3.title")?.text().orEmpty() - Manga( - id = generateUid(relativeUrl), - url = relativeUrl, - title = name, - altTitles = emptySet(), - publicUrl = a.attrAsAbsoluteUrl("href"), - rating = rating, - contentRating = if (isNsfwSource) ContentRating.ADULT else null, - coverUrl = it.selectFirst("img.ts-post-image")?.src(), - tags = emptySet(), - state = null, - authors = emptySet(), - source = source, - ) - } - } - - override suspend fun getPages(chapter: MangaChapter): List { - val chapterUrl = chapter.url.toAbsoluteUrl(domain) - val docs = webClient.httpGet(chapterUrl).parseHtml() - val test = docs.select("script:containsData(ts_reader)") - if (test.isNullOrEmpty()) { - return docs.select("div#chapter_body img").map { img -> - val url = img.requireSrc().toRelativeUrl(domain) - MangaPage( - id = generateUid(url), - url = url, - preview = null, - source = source, - ) - } - } else { - val script = docs.selectFirstOrThrow("script:containsData(ts_reader)") - val images = JSONObject(script.data().substringAfter('(').substringBeforeLast(')')).getJSONArray("sources") - .getJSONObject(0).getJSONArray("images") - val pages = ArrayList(images.length()) - for (i in 0 until images.length()) { - pages.add( - MangaPage( - id = generateUid(images.getString(i)), - url = images.getString(i), - preview = null, - source = source, - ), - ) - } - return pages - } - } - - private fun parseChapterDate(dateFormat: DateFormat, date: String?): Long { - date ?: return 0 - return when { - date.endsWith(" ago", ignoreCase = true) -> { - parseRelativeDate(date) - } - - else -> dateFormat.tryParse(date) - } - } - - private fun parseRelativeDate(date: String): Long { - val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0 - val cal = Calendar.getInstance() - return when { - WordSet( - "day", - "days", - ).anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis - - WordSet("hour", "hours").anyWordIn(date) -> cal.apply { - add( - Calendar.HOUR, - -number, - ) - }.timeInMillis - - WordSet( - "mins", - ).anyWordIn(date) -> cal.apply { - add( - Calendar.MINUTE, - -number, - ) - }.timeInMillis - - WordSet("second").anyWordIn(date) -> cal.apply { - add( - Calendar.SECOND, - -number, - ) - }.timeInMillis - - WordSet("month", "months").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis - WordSet("year").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis - else -> 0 - } - } + MangaReaderParser(context, MangaParserSource.KOMIKCAST, "komikcast.bz", 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 = + EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.ALPHABETICAL) + + override val filterCapabilities: MangaListFilterCapabilities + get() = super.filterCapabilities.copy( + isTagsExclusionSupported = false, + ) + + override suspend fun getFilterOptions() = super.getFilterOptions().copy( + availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED), + ) + + override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val url = buildString { + append("https://") + append(domain) + + when { + + !filter.query.isNullOrEmpty() -> { + append("/page/") + append(page.toString()) + append("/?s=") + append(filter.query.urlEncoded()) + } + + else -> { + append(listUrl) + append("/page/") + append(page.toString()) + append("/?type=") + append( + when (order) { + SortOrder.ALPHABETICAL -> "&orderby=titleasc" + SortOrder.ALPHABETICAL_DESC -> "&orderby=titledesc" + SortOrder.POPULARITY -> "&orderby=popular" + SortOrder.UPDATED -> "" // To get the Updated list, you don't need "orderby" in the url. + 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") + else -> append("") + } + } + } + } + } + } + + return parseMangaList(webClient.httpGet(url).parseHtml()) + } + + override suspend fun getDetails(manga: Manga): Manga { + val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() + val dateFormat = SimpleDateFormat(datePattern, sourceLocale) + val chapters = docs.select("#chapter-wrapper > li").mapChapters(reversed = true) { index, element -> + val url = element.selectFirst("a.chapter-link-item")?.attrAsRelativeUrl("href") ?: return@mapChapters null + MangaChapter( + id = generateUid(url), + name = element.selectFirst("a.chapter-link-item")?.ownText().orEmpty(), + url = url, + number = index + 1f, + volume = 0, + scanlator = null, + uploadDate = parseChapterDate( + dateFormat, + element.selectFirst("div.chapter-link-time")?.text(), + ), + branch = null, + source = source, + ) + } + return parseInfo(docs, manga, chapters) + } + + override suspend fun parseInfo(docs: Document, manga: Manga, chapters: List): Manga { + val tagMap = getOrCreateTagMap() + val tags = docs.select(".komik_info-content-genre > a").mapNotNullToSet { tagMap[it.text()] } + val state = docs.selectFirst(".komik_info-content-meta span:contains(Status)")?.html() + val mangaState = if (state!!.contains("Ongoing")) { + MangaState.ONGOING + } else { + MangaState.FINISHED + } + val author = docs.selectFirst(".komik_info-content-meta span:contains(Author)") + ?.lastElementChild()?.textOrNull() + val nsfw = + docs.selectFirst(".restrictcontainer") != null || docs.selectFirst(".info-right .alr") != null || docs.selectFirst( + ".postbody .alr", + ) != null + + return manga.copy( + description = docs.selectFirst("div.komik_info-description-sinopsis")?.text(), + state = mangaState, + authors = setOfNotNull(author), + contentRating = if (manga.isNsfw || nsfw) { + ContentRating.ADULT + } else { + ContentRating.SAFE + }, + tags = tags, + chapters = chapters, + ) + } + + override fun parseMangaList(docs: Document): List { + return docs.select("div.list-update_item").mapNotNull { + val a = it.selectFirstOrThrow("a.data-tooltip") + val relativeUrl = a.attrAsRelativeUrl("href") + val rating = it.selectFirst(".numscore")?.text()?.toFloatOrNull()?.div(10) ?: RATING_UNKNOWN + val name = it.selectFirst("h3.title")?.text().orEmpty() + Manga( + id = generateUid(relativeUrl), + url = relativeUrl, + title = name, + altTitles = emptySet(), + publicUrl = a.attrAsAbsoluteUrl("href"), + rating = rating, + contentRating = if (isNsfwSource) ContentRating.ADULT else null, + coverUrl = it.selectFirst("img.ts-post-image")?.src(), + tags = emptySet(), + state = null, + authors = emptySet(), + source = source, + ) + } + } + + override suspend fun getPages(chapter: MangaChapter): List { + val chapterUrl = chapter.url.toAbsoluteUrl(domain) + val docs = webClient.httpGet(chapterUrl).parseHtml() + val test = docs.select("script:containsData(ts_reader)") + if (test.isNullOrEmpty()) { + return docs.select("div#chapter_body img").map { img -> + val url = img.requireSrc().toRelativeUrl(domain) + MangaPage( + id = generateUid(url), + url = url, + preview = null, + source = source, + ) + } + } else { + val script = docs.selectFirstOrThrow("script:containsData(ts_reader)") + val images = JSONObject(script.data().substringAfter('(').substringBeforeLast(')')).getJSONArray("sources") + .getJSONObject(0).getJSONArray("images") + val pages = ArrayList(images.length()) + for (i in 0 until images.length()) { + pages.add( + MangaPage( + id = generateUid(images.getString(i)), + url = images.getString(i), + preview = null, + source = source, + ), + ) + } + return pages + } + } + + private fun parseChapterDate(dateFormat: DateFormat, date: String?): Long { + date ?: return 0 + return when { + date.endsWith(" ago", ignoreCase = true) -> { + parseRelativeDate(date) + } + + else -> dateFormat.tryParse(date) + } + } + + private fun parseRelativeDate(date: String): Long { + val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0 + val cal = Calendar.getInstance() + return when { + WordSet( + "day", + "days", + ).anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis + + WordSet("hour", "hours").anyWordIn(date) -> cal.apply { + add( + Calendar.HOUR, + -number, + ) + }.timeInMillis + + WordSet( + "mins", + ).anyWordIn(date) -> cal.apply { + add( + Calendar.MINUTE, + -number, + ) + }.timeInMillis + + WordSet("second").anyWordIn(date) -> cal.apply { + add( + Calendar.SECOND, + -number, + ) + }.timeInMillis + + WordSet("month", "months").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis + WordSet("year").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis + else -> 0 + } + } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangaworld/MangaWorldParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangaworld/MangaWorldParser.kt index 2192573a..ed21b865 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangaworld/MangaWorldParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangaworld/MangaWorldParser.kt @@ -154,7 +154,7 @@ internal abstract class MangaWorldParser( altTitles = emptySet(), rating = RATING_UNKNOWN, tags = tags, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = when (div.selectFirst(".status a")?.text()?.lowercase()) { "in corso" -> MangaState.ONGOING 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 6bf8a980..0304d659 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 @@ -68,7 +68,7 @@ internal class Onma(context: MangaLoaderContext) : source = source, ) }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = desc, altTitles = setOfNotNull(alt), state = state, 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 cfb0ca72..63eb754b 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 @@ -17,294 +17,294 @@ import java.util.* internal abstract class NepnepParser( - context: MangaLoaderContext, - source: MangaParserSource, - domain: String, + context: MangaLoaderContext, + source: MangaParserSource, + domain: String, ) : LegacyMangaParser(context, source) { - override val configKeyDomain = ConfigKey.Domain(domain) - - override val userAgentKey = ConfigKey.UserAgent(UserAgents.CHROME_DESKTOP) - - override fun onCreateConfig(keys: MutableCollection>) { - super.onCreateConfig(keys) - keys.add(userAgentKey) - } - - override val availableSortOrders: Set = - EnumSet.of(SortOrder.ALPHABETICAL, SortOrder.POPULARITY, SortOrder.UPDATED) - - private val searchDoc = suspendLazy(soft = true) { - webClient.httpGet("https://$domain/search/").parseHtml() - } - - override val filterCapabilities: MangaListFilterCapabilities - get() = MangaListFilterCapabilities( - isMultipleTagsSupported = true, - isTagsExclusionSupported = true, - isSearchSupported = true, - ) - - override suspend fun getFilterOptions() = MangaListFilterOptions( - availableTags = fetchAvailableTags(), - availableStates = EnumSet.allOf(MangaState::class.java), - ) - - data class MangaWithLastUpdate( - val manga: Manga, - val lastUpdate: Long, - val views: String, - ) - - override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { - val doc = searchDoc.get() - val json = JSONArray( - doc.selectFirstOrThrow("script:containsData(MainFunction)").data() - .substringAfter("vm.Directory = ") - .substringBefore("vm.GetIntValue") - .trim() - .replace(';', ' '), - ) - val mangaWithLastUpdateList = ArrayList(json.length()) - var sort = false - - 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" - val lastUpdate = m.getLong("lt") - val views = m.getString("v") - //val viewMonth = m.getString("vm") - - when { - !filter.query.isNullOrEmpty() -> { - if (m.getString("s").contains(filter.query, ignoreCase = true) || (m.getJSONArray("al") - .length() > 0 && m.getJSONArray("al").getString(0) - .contains(filter.query, ignoreCase = true)) - ) { - mangaWithLastUpdateList.add( - MangaWithLastUpdate(addManga(href, imgUrl, m), lastUpdate, views), - ) - } - } - - else -> { - val tags = filter.tags - val tagsExcluded = filter.tagsExclude - val tagsJson = m.getJSONArray("g").toString() - - val tagsMatched = - tags.isEmpty() || tags.all { tag -> tagsJson.contains(tag.key, ignoreCase = true) } - val tagsExcludeMatched = tagsExcluded.isEmpty() || tagsExcluded.none { tag -> - tagsJson.contains( - tag.key, - ignoreCase = true, - ) - } - val statesMatched = filter.states.isEmpty() || filter.states.any { state -> - m.getString("ps").contains( - when (state) { - MangaState.ONGOING -> "Ongoing" - MangaState.FINISHED -> "Complete" - MangaState.ABANDONED -> "Cancelled" - MangaState.PAUSED -> "Hiatus" - else -> "" - }, - ignoreCase = true, - ) - } - if (tagsMatched && tagsExcludeMatched && statesMatched) { - mangaWithLastUpdateList.add( - MangaWithLastUpdate(addManga(href, imgUrl, m), lastUpdate, views), - ) - - } - sort = true - } - } - } - if (sort) { - when (order) { - SortOrder.POPULARITY -> mangaWithLastUpdateList.sortByDescending { it.views } - SortOrder.UPDATED -> mangaWithLastUpdateList.sortByDescending { it.lastUpdate } - SortOrder.ALPHABETICAL -> {} - else -> throw IllegalArgumentException("Unsupported sort order: $order") - } - } - return mangaWithLastUpdateList.map { it.manga } - .subList(offset, (offset + 30).coerceAtMost(mangaWithLastUpdateList.size)) - } - - private fun addManga(href: String, imgUrl: String, m: JSONObject): Manga { - return Manga( - id = generateUid(href), - title = m.getString("i").replace('-', ' '), - altTitles = emptySet(), - url = href, - publicUrl = href.toAbsoluteUrl(domain), - rating = RATING_UNKNOWN, - contentRating = null, - coverUrl = imgUrl, - tags = emptySet(), - state = null, - authors = emptySet(), - source = source, - ) - } - - private suspend fun fetchAvailableTags(): Set { - val doc = searchDoc.get() - val tags = doc.selectFirstOrThrow("script:containsData(vm.AvailableFilters)").data() - .substringAfter("\"Genre\"") - .substringAfter('[') - .substringBefore(']') - .replace("'", "") - .split(',') - - return tags.mapToSet { tag -> - MangaTag( - key = tag, - title = tag, - source = source, - ) - } - } - - override suspend fun getDetails(manga: Manga): Manga { - val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() - - val chapter = JSONArray( - JSONArray( - doc.selectFirstOrThrow("script:containsData(MainFunction)").data() - .substringAfter("vm.Chapters = ") - .substringBefore(';'), - ).asTypedList().reversed(), - ) - - val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:SS", sourceLocale) - val author = doc.select(".list-group-item:contains(Author(s):) a").textOrNull() - - return manga.copy( - state = when (doc.selectFirstOrThrow(".list-group-item:contains(Status:) a").text()) { - "Ongoing (Scan)", "Ongoing (Publish)", - -> MangaState.ONGOING - - "Complete (Scan)", "Complete (Publish)", - -> MangaState.FINISHED - - "Cancelled (Scan)", "Cancelled (Publish)", - "Discontinued (Scan)", "Discontinued (Publish)", - -> MangaState.ABANDONED - - "Hiatus (Scan)", "Hiatus (Publish)", - -> MangaState.PAUSED - - else -> null - }, - tags = doc.select(".list-group-item:contains(Genre(s):) a").mapToSet { a -> - MangaTag( - key = a.attr("href").substringAfterLast('='), - title = a.text().toTitleCase(sourceLocale), - source = source, - ) - }, - authors = author?.let { setOf(it) } ?: emptySet(), - description = doc.selectFirstOrThrow(".top-5.Content").textOrNull(), - - chapters = chapter.mapJSONIndexed { i, j -> - val indexChapter = j.getString("Chapter")!! - val url = "/read-online/" + manga.url.substringAfter("/manga/") + chapterURLEncode(indexChapter) - val name = j.getStringOrNull("ChapterName").let { - if (it.isNullOrEmpty() || it == "null") "${j.getString("Type")} ${ - chapterImage( - indexChapter, - true, - ) - }" else it - } - val date = j.getStringOrNull("Date") - MangaChapter( - id = generateUid(url), - name = name, - number = i + 1f, - volume = 0, - url = url, - scanlator = null, - uploadDate = dateFormat.tryParse(date), - branch = null, - source = source, - ) - }, - ) - } - - private fun chapterURLEncode(e: String): String { - var index = "" - val t = e.substring(0, 1).toInt() - if (1 != t) { - index = "-index-$t" - } - val ei = e.toInt() - val dgt = when { - ei < 100100 -> 4 - ei < 101000 -> 3 - ei < 110000 -> 2 - else -> 1 - } - val n = e.substring(dgt, e.length - 1) - var suffix = "" - val path = e.substring(e.length - 1).toInt() - if (0 != path) { - suffix = ".$path" - } - return "-chapter-$n$suffix$index.html" - } - - private val chapterImageRegex = Regex("""^0+""") - - private fun chapterImage(e: String, cleanString: Boolean = false): String { - // cleanString will result in an empty string if chapter number is 0, hence the else if below - val a = e.substring(1, e.length - 1).let { if (cleanString) it.replace(chapterImageRegex, "") else it } - // If b is not zero, indicates chapter has decimal numbering - val b = e.substring(e.length - 1).toInt() - return when { - b == 0 && a.isNotEmpty() -> a - b == 0 && a.isEmpty() -> "0" - else -> "$a.$b" - } - } - - 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(MainFunction)").data() - val curChapter = JSONObject( - doc.selectFirstOrThrow("script:containsData(MainFunction)").data() - .substringAfter("vm.CurChapter = ") - .substringBefore(';'), - ) - val pageTotal = curChapter.getString("Page")!!.toInt() - val host = "https://" + script - .substringAfter("vm.CurPathName = \"", "") - .substringBefore('"') - check(host.isNotEmpty()) { - "Manga4Life is overloaded and blocking Kotatsu right now. Wait for unblock." - } - val titleURI = script.substringAfter("vm.IndexName = \"").substringBefore("\"") - val seasonURI = curChapter.getString("Directory")!!.let { if (it.isEmpty()) "" else "$it/" } - val path = "$host/manga/$titleURI/$seasonURI" - val chNum = chapterImage(curChapter.getString("Chapter")!!) - - return IntRange(1, pageTotal).mapIndexed { i, _ -> - val imageNum = (i + 1).toString().let { "000$it" }.let { it.substring(it.length - 3) } - val url = "$path$chNum-$imageNum.png" - MangaPage( - id = generateUid(url), - url = url, - preview = null, - source = source, - ) - } - } + override val configKeyDomain = ConfigKey.Domain(domain) + + override val userAgentKey = ConfigKey.UserAgent(UserAgents.CHROME_DESKTOP) + + override fun onCreateConfig(keys: MutableCollection>) { + super.onCreateConfig(keys) + keys.add(userAgentKey) + } + + override val availableSortOrders: Set = + EnumSet.of(SortOrder.ALPHABETICAL, SortOrder.POPULARITY, SortOrder.UPDATED) + + private val searchDoc = suspendLazy(soft = true) { + webClient.httpGet("https://$domain/search/").parseHtml() + } + + override val filterCapabilities: MangaListFilterCapabilities + get() = MangaListFilterCapabilities( + isMultipleTagsSupported = true, + isTagsExclusionSupported = true, + isSearchSupported = true, + ) + + override suspend fun getFilterOptions() = MangaListFilterOptions( + availableTags = fetchAvailableTags(), + availableStates = EnumSet.allOf(MangaState::class.java), + ) + + data class MangaWithLastUpdate( + val manga: Manga, + val lastUpdate: Long, + val views: String, + ) + + override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { + val doc = searchDoc.get() + val json = JSONArray( + doc.selectFirstOrThrow("script:containsData(MainFunction)").data() + .substringAfter("vm.Directory = ") + .substringBefore("vm.GetIntValue") + .trim() + .replace(';', ' '), + ) + val mangaWithLastUpdateList = ArrayList(json.length()) + var sort = false + + 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" + val lastUpdate = m.getLong("lt") + val views = m.getString("v") + //val viewMonth = m.getString("vm") + + when { + !filter.query.isNullOrEmpty() -> { + if (m.getString("s").contains(filter.query, ignoreCase = true) || (m.getJSONArray("al") + .length() > 0 && m.getJSONArray("al").getString(0) + .contains(filter.query, ignoreCase = true)) + ) { + mangaWithLastUpdateList.add( + MangaWithLastUpdate(addManga(href, imgUrl, m), lastUpdate, views), + ) + } + } + + else -> { + val tags = filter.tags + val tagsExcluded = filter.tagsExclude + val tagsJson = m.getJSONArray("g").toString() + + val tagsMatched = + tags.isEmpty() || tags.all { tag -> tagsJson.contains(tag.key, ignoreCase = true) } + val tagsExcludeMatched = tagsExcluded.isEmpty() || tagsExcluded.none { tag -> + tagsJson.contains( + tag.key, + ignoreCase = true, + ) + } + val statesMatched = filter.states.isEmpty() || filter.states.any { state -> + m.getString("ps").contains( + when (state) { + MangaState.ONGOING -> "Ongoing" + MangaState.FINISHED -> "Complete" + MangaState.ABANDONED -> "Cancelled" + MangaState.PAUSED -> "Hiatus" + else -> "" + }, + ignoreCase = true, + ) + } + if (tagsMatched && tagsExcludeMatched && statesMatched) { + mangaWithLastUpdateList.add( + MangaWithLastUpdate(addManga(href, imgUrl, m), lastUpdate, views), + ) + + } + sort = true + } + } + } + if (sort) { + when (order) { + SortOrder.POPULARITY -> mangaWithLastUpdateList.sortByDescending { it.views } + SortOrder.UPDATED -> mangaWithLastUpdateList.sortByDescending { it.lastUpdate } + SortOrder.ALPHABETICAL -> {} + else -> throw IllegalArgumentException("Unsupported sort order: $order") + } + } + return mangaWithLastUpdateList.map { it.manga } + .subList(offset, (offset + 30).coerceAtMost(mangaWithLastUpdateList.size)) + } + + private fun addManga(href: String, imgUrl: String, m: JSONObject): Manga { + return Manga( + id = generateUid(href), + title = m.getString("i").replace('-', ' '), + altTitles = emptySet(), + url = href, + publicUrl = href.toAbsoluteUrl(domain), + rating = RATING_UNKNOWN, + contentRating = null, + coverUrl = imgUrl, + tags = emptySet(), + state = null, + authors = emptySet(), + source = source, + ) + } + + private suspend fun fetchAvailableTags(): Set { + val doc = searchDoc.get() + val tags = doc.selectFirstOrThrow("script:containsData(vm.AvailableFilters)").data() + .substringAfter("\"Genre\"") + .substringAfter('[') + .substringBefore(']') + .replace("'", "") + .split(',') + + return tags.mapToSet { tag -> + MangaTag( + key = tag, + title = tag, + source = source, + ) + } + } + + override suspend fun getDetails(manga: Manga): Manga { + val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() + + val chapter = JSONArray( + JSONArray( + doc.selectFirstOrThrow("script:containsData(MainFunction)").data() + .substringAfter("vm.Chapters = ") + .substringBefore(';'), + ).asTypedList().reversed(), + ) + + val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:SS", sourceLocale) + val author = doc.select(".list-group-item:contains(Author(s):) a").textOrNull() + + return manga.copy( + state = when (doc.selectFirstOrThrow(".list-group-item:contains(Status:) a").text()) { + "Ongoing (Scan)", "Ongoing (Publish)", + -> MangaState.ONGOING + + "Complete (Scan)", "Complete (Publish)", + -> MangaState.FINISHED + + "Cancelled (Scan)", "Cancelled (Publish)", + "Discontinued (Scan)", "Discontinued (Publish)", + -> MangaState.ABANDONED + + "Hiatus (Scan)", "Hiatus (Publish)", + -> MangaState.PAUSED + + else -> null + }, + tags = doc.select(".list-group-item:contains(Genre(s):) a").mapToSet { a -> + MangaTag( + key = a.attr("href").substringAfterLast('='), + title = a.text().toTitleCase(sourceLocale), + source = source, + ) + }, + authors = setOfNotNull(author), + description = doc.selectFirstOrThrow(".top-5.Content").textOrNull(), + + chapters = chapter.mapJSONIndexed { i, j -> + val indexChapter = j.getString("Chapter")!! + val url = "/read-online/" + manga.url.substringAfter("/manga/") + chapterURLEncode(indexChapter) + val name = j.getStringOrNull("ChapterName").let { + if (it.isNullOrEmpty() || it == "null") "${j.getString("Type")} ${ + chapterImage( + indexChapter, + true, + ) + }" else it + } + val date = j.getStringOrNull("Date") + MangaChapter( + id = generateUid(url), + name = name, + number = i + 1f, + volume = 0, + url = url, + scanlator = null, + uploadDate = dateFormat.tryParse(date), + branch = null, + source = source, + ) + }, + ) + } + + private fun chapterURLEncode(e: String): String { + var index = "" + val t = e.substring(0, 1).toInt() + if (1 != t) { + index = "-index-$t" + } + val ei = e.toInt() + val dgt = when { + ei < 100100 -> 4 + ei < 101000 -> 3 + ei < 110000 -> 2 + else -> 1 + } + val n = e.substring(dgt, e.length - 1) + var suffix = "" + val path = e.substring(e.length - 1).toInt() + if (0 != path) { + suffix = ".$path" + } + return "-chapter-$n$suffix$index.html" + } + + private val chapterImageRegex = Regex("""^0+""") + + private fun chapterImage(e: String, cleanString: Boolean = false): String { + // cleanString will result in an empty string if chapter number is 0, hence the else if below + val a = e.substring(1, e.length - 1).let { if (cleanString) it.replace(chapterImageRegex, "") else it } + // If b is not zero, indicates chapter has decimal numbering + val b = e.substring(e.length - 1).toInt() + return when { + b == 0 && a.isNotEmpty() -> a + b == 0 && a.isEmpty() -> "0" + else -> "$a.$b" + } + } + + 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(MainFunction)").data() + val curChapter = JSONObject( + doc.selectFirstOrThrow("script:containsData(MainFunction)").data() + .substringAfter("vm.CurChapter = ") + .substringBefore(';'), + ) + val pageTotal = curChapter.getString("Page")!!.toInt() + val host = "https://" + script + .substringAfter("vm.CurPathName = \"", "") + .substringBefore('"') + check(host.isNotEmpty()) { + "Manga4Life is overloaded and blocking Kotatsu right now. Wait for unblock." + } + val titleURI = script.substringAfter("vm.IndexName = \"").substringBefore("\"") + val seasonURI = curChapter.getString("Directory")!!.let { if (it.isEmpty()) "" else "$it/" } + val path = "$host/manga/$titleURI/$seasonURI" + val chNum = chapterImage(curChapter.getString("Chapter")!!) + + return IntRange(1, pageTotal).mapIndexed { i, _ -> + val imageNum = (i + 1).toString().let { "000$it" }.let { it.substring(it.length - 3) } + val url = "$path$chNum-$imageNum.png" + MangaPage( + id = generateUid(url), + url = url, + preview = null, + source = source, + ) + } + } } 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 41053e4b..d5926532 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 @@ -175,7 +175,7 @@ internal abstract class OtakuSanctuaryParser( }, description = desc, altTitles = setOfNotNull(alt), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = state, chapters = doc.body().requireElementById("chapter").select("tr.chapter") .mapChapters(reversed = true) { i, tr -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pizzareader/PizzaReaderParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pizzareader/PizzaReaderParser.kt index 38a94900..6d6f49de 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pizzareader/PizzaReaderParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pizzareader/PizzaReaderParser.kt @@ -196,7 +196,7 @@ internal abstract class PizzaReaderParser( rating = j.getString("rating").toFloatOrNull()?.div(10f) ?: RATING_UNKNOWN, tags = emptySet(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = when (j.getString("status").lowercase()) { in ongoing -> MangaState.ONGOING in finished -> MangaState.FINISHED 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 511e27d4..244761db 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 @@ -14,162 +14,162 @@ import java.util.* @MangaSourceParser("BRMANGAS", "BrMangas", "pt") internal class BrMangas(context: MangaLoaderContext) : LegacyPagedMangaParser(context, MangaParserSource.BRMANGAS, 25) { - override val configKeyDomain = ConfigKey.Domain("www.brmangas.net") - - override val userAgentKey = ConfigKey.UserAgent(UserAgents.CHROME_DESKTOP) - - override fun onCreateConfig(keys: MutableCollection>) { - super.onCreateConfig(keys) - keys.add(userAgentKey) - } - - override val availableSortOrders: Set = EnumSet.of(SortOrder.POPULARITY, SortOrder.UPDATED) - - override val filterCapabilities: MangaListFilterCapabilities - get() = MangaListFilterCapabilities( - isSearchSupported = true, - ) - - override suspend fun getFilterOptions() = MangaListFilterOptions( - availableTags = fetchAvailableTags(), - ) - - override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val url = buildString { - append("https://") - append(domain) - append('/') - when { - !filter.query.isNullOrEmpty() -> { - if (page > 1) { - append("/page/$page/") - } - append("/?s=") - append(filter.query.urlEncoded()) - } - - else -> { - if (filter.tags.isNotEmpty()) { - filter.tags.oneOrThrowIfMany()?.let { - append("category/") - append(it.key) - if (page > 1) { - append("/page/$page/") - } - } - } else { - when (order) { - SortOrder.POPULARITY -> append("/") - SortOrder.UPDATED -> append("manga/") - else -> append("manga/") - } - if (page > 1) { - append("page/$page/") - } - } - - } - } - - } - - val doc = webClient.httpGet(url).parseHtml() - - val item = when { - - !filter.query.isNullOrEmpty() -> { - doc.select("div.listagem div.item") - } - - else -> { - if (order == 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") - } - } - } - return item.map { div -> - val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") - Manga( - id = generateUid(href), - title = div.selectFirstOrThrow("h2").text(), - altTitles = emptySet(), - url = href, - publicUrl = href.toAbsoluteUrl(domain), - rating = RATING_UNKNOWN, - contentRating = null, - coverUrl = div.selectFirstOrThrow("img").src(), - tags = emptySet(), - state = null, - authors = emptySet(), - source = source, - ) - } - } - - private suspend fun fetchAvailableTags(): Set { - val doc = webClient.httpGet("https://$domain/lista-de-generos-de-manga/").parseHtml() - return doc.select(".genres_page a").mapToSet { a -> - MangaTag( - key = a.attr("href").removeSuffix('/').substringAfterLast('/'), - title = a.text().toTitleCase(sourceLocale), - source = source, - ) - } - } - - override suspend fun getDetails(manga: Manga): Manga { - val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() - val author = doc.select("div.serie-infos li:contains(Autor:)").text().replace("Autor:", "").nullIfEmpty() - return manga.copy( - tags = doc.select("div.serie-infos li:contains(Categorias:) a").mapToSet { a -> - MangaTag( - key = a.attr("href").removeSuffix('/').substringAfterLast('/'), - title = a.text().toTitleCase(sourceLocale), - source = source, - ) - }, - authors = author?.let { setOf(it) } ?: emptySet(), - description = doc.select(".serie-texto p").html(), - contentRating = if (doc.select("div.serie-infos li:contains(Categorias:)").text().contains("Hentai")) { - ContentRating.ADULT - } else { - manga.contentRating - }, - chapters = doc.select(".capitulos li a") - .mapChapters { i, a -> - val url = a.attrAsRelativeUrl("href") - val name = a.text() - MangaChapter( - id = generateUid(url), - name = name, - number = i + 1f, - volume = 0, - url = url, - scanlator = null, - uploadDate = 0, - branch = null, - source = source, - ) - }, - ) - } - - override suspend fun getPages(chapter: MangaChapter): List { - val fullUrl = chapter.url.toAbsoluteUrl(domain) - val doc = webClient.httpGet(fullUrl).parseHtml() - val scriptData = - doc.selectFirstOrThrow("script:containsData(imageArray)").data().substringAfter('[').substringBefore(']') - .split(",") - return scriptData.map { data -> - val url = data.replace("\\\"", "").replace("\\/", "/") - MangaPage( - id = generateUid(url), - url = url, - preview = null, - source = source, - ) - } - } + override val configKeyDomain = ConfigKey.Domain("www.brmangas.net") + + override val userAgentKey = ConfigKey.UserAgent(UserAgents.CHROME_DESKTOP) + + override fun onCreateConfig(keys: MutableCollection>) { + super.onCreateConfig(keys) + keys.add(userAgentKey) + } + + override val availableSortOrders: Set = EnumSet.of(SortOrder.POPULARITY, SortOrder.UPDATED) + + override val filterCapabilities: MangaListFilterCapabilities + get() = MangaListFilterCapabilities( + isSearchSupported = true, + ) + + override suspend fun getFilterOptions() = MangaListFilterOptions( + availableTags = fetchAvailableTags(), + ) + + override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val url = buildString { + append("https://") + append(domain) + append('/') + when { + !filter.query.isNullOrEmpty() -> { + if (page > 1) { + append("/page/$page/") + } + append("/?s=") + append(filter.query.urlEncoded()) + } + + else -> { + if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { + append("category/") + append(it.key) + if (page > 1) { + append("/page/$page/") + } + } + } else { + when (order) { + SortOrder.POPULARITY -> append("/") + SortOrder.UPDATED -> append("manga/") + else -> append("manga/") + } + if (page > 1) { + append("page/$page/") + } + } + + } + } + + } + + val doc = webClient.httpGet(url).parseHtml() + + val item = when { + + !filter.query.isNullOrEmpty() -> { + doc.select("div.listagem div.item") + } + + else -> { + if (order == 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") + } + } + } + return item.map { div -> + val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") + Manga( + id = generateUid(href), + title = div.selectFirstOrThrow("h2").text(), + altTitles = emptySet(), + url = href, + publicUrl = href.toAbsoluteUrl(domain), + rating = RATING_UNKNOWN, + contentRating = null, + coverUrl = div.selectFirstOrThrow("img").src(), + tags = emptySet(), + state = null, + authors = emptySet(), + source = source, + ) + } + } + + private suspend fun fetchAvailableTags(): Set { + val doc = webClient.httpGet("https://$domain/lista-de-generos-de-manga/").parseHtml() + return doc.select(".genres_page a").mapToSet { a -> + MangaTag( + key = a.attr("href").removeSuffix('/').substringAfterLast('/'), + title = a.text().toTitleCase(sourceLocale), + source = source, + ) + } + } + + override suspend fun getDetails(manga: Manga): Manga { + val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() + val author = doc.select("div.serie-infos li:contains(Autor:)").text().replace("Autor:", "").nullIfEmpty() + return manga.copy( + tags = doc.select("div.serie-infos li:contains(Categorias:) a").mapToSet { a -> + MangaTag( + key = a.attr("href").removeSuffix('/').substringAfterLast('/'), + title = a.text().toTitleCase(sourceLocale), + source = source, + ) + }, + authors = setOfNotNull(author), + description = doc.select(".serie-texto p").html(), + contentRating = if (doc.select("div.serie-infos li:contains(Categorias:)").text().contains("Hentai")) { + ContentRating.ADULT + } else { + manga.contentRating + }, + chapters = doc.select(".capitulos li a") + .mapChapters { i, a -> + val url = a.attrAsRelativeUrl("href") + val name = a.text() + MangaChapter( + id = generateUid(url), + name = name, + number = i + 1f, + volume = 0, + url = url, + scanlator = null, + uploadDate = 0, + branch = null, + source = source, + ) + }, + ) + } + + override suspend fun getPages(chapter: MangaChapter): List { + val fullUrl = chapter.url.toAbsoluteUrl(domain) + val doc = webClient.httpGet(fullUrl).parseHtml() + val scriptData = + doc.selectFirstOrThrow("script:containsData(imageArray)").data().substringAfter('[').substringBefore(']') + .split(",") + return scriptData.map { data -> + val url = data.replace("\\\"", "").replace("\\/", "/") + MangaPage( + id = generateUid(url), + url = url, + preview = null, + source = source, + ) + } + } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LuratoonScansParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LuratoonScansParser.kt index 60e647a1..3d656b04 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LuratoonScansParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LuratoonScansParser.kt @@ -20,143 +20,143 @@ import java.util.zip.ZipInputStream @Broken // Not dead but changed template @MangaSourceParser("RANDOMSCANS", "LuratoonScan", "pt") internal class LuratoonScansParser(context: MangaLoaderContext) : - LegacySinglePageMangaParser(context, MangaParserSource.RANDOMSCANS), - Interceptor { + LegacySinglePageMangaParser(context, MangaParserSource.RANDOMSCANS), + Interceptor { - override val configKeyDomain = ConfigKey.Domain("luratoons.com") + override val configKeyDomain = ConfigKey.Domain("luratoons.com") - override fun getRequestHeaders(): Headers = Headers.Builder().add("User-Agent", config[userAgentKey]).build() + override fun getRequestHeaders(): Headers = Headers.Builder().add("User-Agent", config[userAgentKey]).build() - override val availableSortOrders = setOf(SortOrder.ALPHABETICAL) + override val availableSortOrders = setOf(SortOrder.ALPHABETICAL) - override val filterCapabilities: MangaListFilterCapabilities - get() = MangaListFilterCapabilities() + override val filterCapabilities: MangaListFilterCapabilities + get() = MangaListFilterCapabilities() - override suspend fun getFilterOptions() = MangaListFilterOptions() + override suspend fun getFilterOptions() = MangaListFilterOptions() - override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { - require(filter.query.isNullOrEmpty()) { ErrorMessages.SEARCH_NOT_SUPPORTED } - val url = urlBuilder() - val tag = filter.tags.oneOrThrowIfMany() - if (tag == null) { - url.addPathSegment("todas-as-obras") - } else { - url.addPathSegment("pesquisar").addQueryParameter("category", tag.key) - } - val doc = webClient.httpGet(url.build()).parseHtml() - return doc.selectFirstOrThrow(".todas__as__obras").select(".comics__all__box").map { div -> - val a = div.selectFirstOrThrow("a") - val href = a.attrAsRelativeUrl("href") - Manga( - id = generateUid(href), - url = href, - publicUrl = href.toAbsoluteUrl(div.host ?: domain), - coverUrl = div.selectFirst("img")?.src().orEmpty(), - title = div.text(), - altTitles = emptySet(), - rating = RATING_UNKNOWN, - tags = emptySet(), - authors = emptySet(), - state = null, - source = source, - contentRating = null, - ) - } - } + override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { + require(filter.query.isNullOrEmpty()) { ErrorMessages.SEARCH_NOT_SUPPORTED } + val url = urlBuilder() + val tag = filter.tags.oneOrThrowIfMany() + if (tag == null) { + url.addPathSegment("todas-as-obras") + } else { + url.addPathSegment("pesquisar").addQueryParameter("category", tag.key) + } + val doc = webClient.httpGet(url.build()).parseHtml() + return doc.selectFirstOrThrow(".todas__as__obras").select(".comics__all__box").map { div -> + val a = div.selectFirstOrThrow("a") + val href = a.attrAsRelativeUrl("href") + Manga( + id = generateUid(href), + url = href, + publicUrl = href.toAbsoluteUrl(div.host ?: domain), + coverUrl = div.selectFirst("img")?.src().orEmpty(), + title = div.text(), + altTitles = emptySet(), + rating = RATING_UNKNOWN, + tags = emptySet(), + authors = emptySet(), + state = null, + source = source, + contentRating = null, + ) + } + } - override suspend fun getDetails(manga: Manga): Manga { - val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml().body() - val summaryContainer = doc.selectFirstOrThrow(".sumario__container") - // 1 de Maio de 2024 às 20:15 - val dateFormat = SimpleDateFormat("dd 'de' MMM 'de' YYYY 'às' HH:mm", sourceLocale) - val author = summaryContainer.getElementsContainingOwnText("Autor(es)").firstOrNull() - ?.nextElementSibling()?.textOrNull() - return manga.copy( - title = doc.selectFirst("h1.desc__titulo__comic")?.textOrNull() ?: manga.title, - altTitles = setOfNotNull( - summaryContainer.getElementsContainingOwnText("Alternativo").firstOrNull() - ?.nextElementSibling()?.textOrNull(), - ), - tags = summaryContainer.getElementsByAttributeValueContaining("href", "?category=").mapToSet { - MangaTag( - title = it.text().toTitleCase(sourceLocale), - key = it.attr("href").substringAfterLast('='), - source = source, - ) - }, - state = when (summaryContainer.getElementsContainingOwnText("Status").firstOrNull() - ?.nextElementSibling()?.text()?.lowercase()) { - "em lançamento" -> MangaState.ONGOING - "hiato" -> MangaState.PAUSED - "finalizado" -> MangaState.FINISHED - else -> null - }, - authors = author?.let { setOf(it) } ?: emptySet(), - largeCoverUrl = doc.selectFirst("img.sumario__img")?.attrAsAbsoluteUrlOrNull("src"), - description = summaryContainer.selectFirst(".sumario__sinopse__texto")?.html(), - chapters = doc.selectFirstOrThrow("ul.capitulos__lista") - .select("li") - .mapChapters(reversed = true) { _, li -> - val href = li.parent()?.attrAsRelativeUrlOrNull("href") ?: return@mapChapters null - val span = li.selectFirstOrThrow(".numero__capitulo") - MangaChapter( - id = generateUid(href), - name = span.text(), - number = 0.0f, - volume = 0, - url = href, - scanlator = null, - uploadDate = dateFormat.tryParse(span.nextElementSibling()?.text()), - branch = null, - source = source, - ) - }, - ) - } + override suspend fun getDetails(manga: Manga): Manga { + val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml().body() + val summaryContainer = doc.selectFirstOrThrow(".sumario__container") + // 1 de Maio de 2024 às 20:15 + val dateFormat = SimpleDateFormat("dd 'de' MMM 'de' YYYY 'às' HH:mm", sourceLocale) + val author = summaryContainer.getElementsContainingOwnText("Autor(es)").firstOrNull() + ?.nextElementSibling()?.textOrNull() + return manga.copy( + title = doc.selectFirst("h1.desc__titulo__comic")?.textOrNull() ?: manga.title, + altTitles = setOfNotNull( + summaryContainer.getElementsContainingOwnText("Alternativo").firstOrNull() + ?.nextElementSibling()?.textOrNull(), + ), + tags = summaryContainer.getElementsByAttributeValueContaining("href", "?category=").mapToSet { + MangaTag( + title = it.text().toTitleCase(sourceLocale), + key = it.attr("href").substringAfterLast('='), + source = source, + ) + }, + state = when (summaryContainer.getElementsContainingOwnText("Status").firstOrNull() + ?.nextElementSibling()?.text()?.lowercase()) { + "em lançamento" -> MangaState.ONGOING + "hiato" -> MangaState.PAUSED + "finalizado" -> MangaState.FINISHED + else -> null + }, + authors = setOfNotNull(author), + largeCoverUrl = doc.selectFirst("img.sumario__img")?.attrAsAbsoluteUrlOrNull("src"), + description = summaryContainer.selectFirst(".sumario__sinopse__texto")?.html(), + chapters = doc.selectFirstOrThrow("ul.capitulos__lista") + .select("li") + .mapChapters(reversed = true) { _, li -> + val href = li.parent()?.attrAsRelativeUrlOrNull("href") ?: return@mapChapters null + val span = li.selectFirstOrThrow(".numero__capitulo") + MangaChapter( + id = generateUid(href), + name = span.text(), + number = 0.0f, + volume = 0, + url = href, + scanlator = null, + uploadDate = dateFormat.tryParse(span.nextElementSibling()?.text()), + branch = null, + source = source, + ) + }, + ) + } - override suspend fun getPages(chapter: MangaChapter): List { - val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml() - val regex = Regex("const\\s+urls\\s*=\\s*(\\[.*])") - val urls = doc.select("script").firstNotNullOf { - regex.find(it.data())?.groupValues?.getOrNull(1) - } - val ja = JSONArray(urls) - return (0 until ja.length()).map { i -> - val url = ja.getString(i) - MangaPage( - id = generateUid(url), - url = url, - preview = null, - source = source, - ) - } - } + override suspend fun getPages(chapter: MangaChapter): List { + val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml() + val regex = Regex("const\\s+urls\\s*=\\s*(\\[.*])") + val urls = doc.select("script").firstNotNullOf { + regex.find(it.data())?.groupValues?.getOrNull(1) + } + val ja = JSONArray(urls) + return (0 until ja.length()).map { i -> + val url = ja.getString(i) + MangaPage( + id = generateUid(url), + url = url, + preview = null, + source = source, + ) + } + } - override fun intercept(chain: Interceptor.Chain): Response { - val response = chain.proceed(chain.request()) - if (response.mimeType == "application/octet-stream") { - val (bytes, name) = response.use { resp -> - ZipInputStream(resp.requireBody().byteStream()).use { - val entry = it.nextEntry - it.readBytes() to entry?.name - } - } - val type = if (name?.endsWith(".avif", ignoreCase = true) == true) { - "image/avif" - } else { - "image/*" - }.toMediaTypeOrNull() - return response.newBuilder() - .setHeader("Content-Type", type?.toString()) - .body(bytes.toResponseBody(type)) - .build() - } else { - return response - } - } + override fun intercept(chain: Interceptor.Chain): Response { + val response = chain.proceed(chain.request()) + if (response.mimeType == "application/octet-stream") { + val (bytes, name) = response.use { resp -> + ZipInputStream(resp.requireBody().byteStream()).use { + val entry = it.nextEntry + it.readBytes() to entry?.name + } + } + val type = if (name?.endsWith(".avif", ignoreCase = true) == true) { + "image/avif" + } else { + "image/*" + }.toMediaTypeOrNull() + return response.newBuilder() + .setHeader("Content-Type", type?.toString()) + .body(bytes.toResponseBody(type)) + .build() + } else { + return response + } + } - override fun onCreateConfig(keys: MutableCollection>) { - super.onCreateConfig(keys) - keys.add(userAgentKey) - } + override fun onCreateConfig(keys: MutableCollection>) { + super.onCreateConfig(keys) + keys.add(userAgentKey) + } } 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 e55f4af0..15392807 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 @@ -108,7 +108,7 @@ internal class YugenMangas(context: MangaLoaderContext) : description = detailManga.getString("synopsis"), coverUrl = detailManga.getString("cover"), altTitles = setOfNotNull(detailManga.getStringOrNull("alternative_names")), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = detailManga.getStringOrNull("status")?.let { when (it) { "ongoing" -> MangaState.ONGOING diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/AComics.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/AComics.kt index d74bba1b..182cb39b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/AComics.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/AComics.kt @@ -14,170 +14,170 @@ import java.util.* @MangaSourceParser("ACOMICS", "AComics", "ru", ContentType.COMICS) internal class AComics(context: MangaLoaderContext) : - LegacyPagedMangaParser(context, MangaParserSource.ACOMICS, pageSize = 10) { - - override val availableSortOrders: Set = EnumSet.of( - SortOrder.UPDATED, - SortOrder.ALPHABETICAL, - SortOrder.POPULARITY, - ) - - override val configKeyDomain = ConfigKey.Domain("acomics.ru") - - override val filterCapabilities: MangaListFilterCapabilities - get() = MangaListFilterCapabilities( - isMultipleTagsSupported = true, - isSearchSupported = true, - ) - - init { - paginator.firstPage = 0 - searchPaginator.firstPage = 0 - context.cookieJar.insertCookies(domain, "ageRestrict=18") - } - - override suspend fun getFilterOptions() = MangaListFilterOptions( - availableTags = getOrCreateTagMap().values.toSet(), - availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED), - ) - - override suspend fun getListPage( - page: Int, - order: SortOrder, - filter: MangaListFilter, - ): List { - val url = buildString { - append("https://") - append(domain) - when { - !filter.query.isNullOrEmpty() -> { - if (page > 0) { - return emptyList() - } - append("/search?keyword=") - append(filter.query) - } - - else -> { - append("/comics?ratings[]=1&ratings[]=2&ratings[]=3&ratings[]=4&ratings[]=5&ratings[]=6&skip=") - append(page * 10) - append("&sort=") - append( - when (order) { - SortOrder.UPDATED -> "last_update" - SortOrder.ALPHABETICAL -> "serial_name" - SortOrder.POPULARITY -> "subscr_count" - else -> "last_update" - }, - ) - - if (filter.tags.isNotEmpty()) { - append("&categories=") - append(filter.tags.joinToString(separator = ",") { it.key }) - } - - if (filter.states.isNotEmpty()) { - append("&updatable=") - append( - filter.states.oneOrThrowIfMany().let { - when (it) { - MangaState.ONGOING -> "yes" - MangaState.FINISHED -> "no" - else -> "0" - } - }, - ) - } - } - } - } - - return parseMangaList(webClient.httpGet(url).parseHtml()) - } - - private fun parseMangaList(docs: Document): List { - return docs.select("table.list-loadable").map { - val a = it.selectFirstOrThrow("a") - val url = a.attrAsAbsoluteUrl("href") + "/about" - Manga( - id = generateUid(url), - url = url, - title = it.selectFirstOrThrow(".title").text(), - altTitles = emptySet(), - publicUrl = url, - rating = RATING_UNKNOWN, - contentRating = if (isNsfwSource) ContentRating.ADULT else null, - coverUrl = it.selectFirstOrThrow("img").src().orEmpty(), - tags = emptySet(), - state = null, - authors = emptySet(), - source = source, - ) - } - } - - private var tagCache: ArrayMap? = null - private val mutex = Mutex() - - private suspend fun getOrCreateTagMap(): Map = mutex.withLock { - tagCache?.let { return@withLock it } - val tagMap = ArrayMap() - val tagElements = - webClient.httpGet("https://$domain/comics").parseHtml().requireElementById("catalog").select(" a.button") - for (el in tagElements) { - val name = el.html().substringAfterLast("") - if (name.isEmpty()) continue - tagMap[name] = MangaTag( - title = name, - key = el.attr("onclick").substringAfterLast("('").substringBefore("')"), - source = source, - ) - } - tagCache = tagMap - return@withLock tagMap - } - - override suspend fun getDetails(manga: Manga): Manga { - val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() - val tagMap = getOrCreateTagMap() - val tags = doc.select("p.serial-about-badges .category").mapNotNullToSet { tagMap[it.text()] } - val author = doc.selectFirst("p:contains(Автор оригинала:)")?.text()?.replace("Автор оригинала: ", "") - return manga.copy( - tags = tags, - description = doc.selectFirst("section.serial-about-text p")?.text(), - authors = author?.let { setOf(it) } ?: emptySet(), - chapters = listOf( - MangaChapter( - id = manga.id, - name = manga.title, - number = 1f, - volume = 0, - url = manga.url.replace("/about", "/"), - scanlator = null, - uploadDate = 0, - branch = null, - source = source, - ), - ), - ) - } - - override suspend fun getPages(chapter: MangaChapter): List { - val doc = webClient.httpGet(chapter.url + "1").parseHtml() - val totalPages = doc.selectFirstOrThrow("span.issueNumber").text().substringAfterLast('/').toInt() - return (1..totalPages).map { - val url = chapter.url + 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() - return doc.requireElementById("mainImage").requireSrc() - } + LegacyPagedMangaParser(context, MangaParserSource.ACOMICS, pageSize = 10) { + + override val availableSortOrders: Set = EnumSet.of( + SortOrder.UPDATED, + SortOrder.ALPHABETICAL, + SortOrder.POPULARITY, + ) + + override val configKeyDomain = ConfigKey.Domain("acomics.ru") + + override val filterCapabilities: MangaListFilterCapabilities + get() = MangaListFilterCapabilities( + isMultipleTagsSupported = true, + isSearchSupported = true, + ) + + init { + paginator.firstPage = 0 + searchPaginator.firstPage = 0 + context.cookieJar.insertCookies(domain, "ageRestrict=18") + } + + override suspend fun getFilterOptions() = MangaListFilterOptions( + availableTags = getOrCreateTagMap().values.toSet(), + availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED), + ) + + override suspend fun getListPage( + page: Int, + order: SortOrder, + filter: MangaListFilter, + ): List { + val url = buildString { + append("https://") + append(domain) + when { + !filter.query.isNullOrEmpty() -> { + if (page > 0) { + return emptyList() + } + append("/search?keyword=") + append(filter.query) + } + + else -> { + append("/comics?ratings[]=1&ratings[]=2&ratings[]=3&ratings[]=4&ratings[]=5&ratings[]=6&skip=") + append(page * 10) + append("&sort=") + append( + when (order) { + SortOrder.UPDATED -> "last_update" + SortOrder.ALPHABETICAL -> "serial_name" + SortOrder.POPULARITY -> "subscr_count" + else -> "last_update" + }, + ) + + if (filter.tags.isNotEmpty()) { + append("&categories=") + append(filter.tags.joinToString(separator = ",") { it.key }) + } + + if (filter.states.isNotEmpty()) { + append("&updatable=") + append( + filter.states.oneOrThrowIfMany().let { + when (it) { + MangaState.ONGOING -> "yes" + MangaState.FINISHED -> "no" + else -> "0" + } + }, + ) + } + } + } + } + + return parseMangaList(webClient.httpGet(url).parseHtml()) + } + + private fun parseMangaList(docs: Document): List { + return docs.select("table.list-loadable").map { + val a = it.selectFirstOrThrow("a") + val url = a.attrAsAbsoluteUrl("href") + "/about" + Manga( + id = generateUid(url), + url = url, + title = it.selectFirstOrThrow(".title").text(), + altTitles = emptySet(), + publicUrl = url, + rating = RATING_UNKNOWN, + contentRating = if (isNsfwSource) ContentRating.ADULT else null, + coverUrl = it.selectFirstOrThrow("img").src().orEmpty(), + tags = emptySet(), + state = null, + authors = emptySet(), + source = source, + ) + } + } + + private var tagCache: ArrayMap? = null + private val mutex = Mutex() + + private suspend fun getOrCreateTagMap(): Map = mutex.withLock { + tagCache?.let { return@withLock it } + val tagMap = ArrayMap() + val tagElements = + webClient.httpGet("https://$domain/comics").parseHtml().requireElementById("catalog").select(" a.button") + for (el in tagElements) { + val name = el.html().substringAfterLast("") + if (name.isEmpty()) continue + tagMap[name] = MangaTag( + title = name, + key = el.attr("onclick").substringAfterLast("('").substringBefore("')"), + source = source, + ) + } + tagCache = tagMap + return@withLock tagMap + } + + override suspend fun getDetails(manga: Manga): Manga { + val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() + val tagMap = getOrCreateTagMap() + val tags = doc.select("p.serial-about-badges .category").mapNotNullToSet { tagMap[it.text()] } + val author = doc.selectFirst("p:contains(Автор оригинала:)")?.text()?.replace("Автор оригинала: ", "") + return manga.copy( + tags = tags, + description = doc.selectFirst("section.serial-about-text p")?.text(), + authors = setOfNotNull(author), + chapters = listOf( + MangaChapter( + id = manga.id, + name = manga.title, + number = 1f, + volume = 0, + url = manga.url.replace("/about", "/"), + scanlator = null, + uploadDate = 0, + branch = null, + source = source, + ), + ), + ) + } + + override suspend fun getPages(chapter: MangaChapter): List { + val doc = webClient.httpGet(chapter.url + "1").parseHtml() + val totalPages = doc.selectFirstOrThrow("span.issueNumber").text().substringAfterLast('/').toInt() + return (1..totalPages).map { + val url = chapter.url + 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() + return doc.requireElementById("mainImage").requireSrc() + } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/NudeMoonParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/NudeMoonParser.kt index 7b9861b7..e48c1fb7 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/NudeMoonParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/NudeMoonParser.kt @@ -99,7 +99,7 @@ internal class NudeMoonParser( url = href, title = title.substringAfter(" / "), altTitles = setOfNotNull(title.substringBefore(" / ", "").takeUnless { it.isBlank() }), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), coverUrl = row.selectFirst("img")?.absUrl("src").orEmpty(), tags = row.selectFirst(".tag-links")?.select("a")?.mapToSet { MangaTag( diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/WaMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/WaMangaParser.kt index 7d1411b5..c399a361 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/WaMangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/WaMangaParser.kt @@ -62,7 +62,7 @@ internal class WaMangaParser( "закончен" -> MangaState.FINISHED else -> MangaState.UPCOMING }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), source = source, contentRating = if (doc.getIntOrDefault("adult", 0) == 0) { ContentRating.SAFE diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/GroupleParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/GroupleParser.kt index 2a28956f..48fe3a4f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/GroupleParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/GroupleParser.kt @@ -40,511 +40,511 @@ private const val RELATED_TITLE = "Связанные произведения" private const val NO_CHAPTERS = "В этой манге еще нет ни одной главы" internal abstract class GroupleParser( - context: MangaLoaderContext, - source: MangaParserSource, - private val siteId: Int, + context: MangaLoaderContext, + source: MangaParserSource, + private val siteId: Int, ) : LegacyMangaParser(context, source), MangaParserAuthProvider, Interceptor { - @Volatile - private var cachedPagesServer: String? = null - - override val userAgentKey = ConfigKey.UserAgent( - "Mozilla/5.0 (X11; U; UNICOS lcLinux; en-US) Gecko/20140730 (KHTML, like Gecko, Safari/419.3) Arora/0.8.0", - ) - private val splitTranslationsKey = ConfigKey.SplitByTranslations(false) - private val tagsIndex = suspendLazy(initializer = ::fetchTagsMap) - - override fun getRequestHeaders(): Headers = Headers.Builder() - .add("User-Agent", config[userAgentKey]) - .add("Accept-Language", "ru,en-US;q=0.7,en;q=0.3") - .build() - - override val availableSortOrders: Set = EnumSet.of( - SortOrder.UPDATED, - SortOrder.POPULARITY, - SortOrder.NEWEST, - SortOrder.RATING, - SortOrder.ALPHABETICAL, - SortOrder.ADDED, - ) - - override val authUrl: String - get() { - val targetUri = "https://${domain}/".urlEncoded() - return "https://grouple.co/internal/auth/sso?siteId=$siteId&=targetUri=$targetUri" - } - - override val isAuthorized: Boolean - get() = context.cookieJar.getCookies(domain).any { it.name == "gwt" } - - override val filterCapabilities: MangaListFilterCapabilities - get() = MangaListFilterCapabilities( - isMultipleTagsSupported = true, - isTagsExclusionSupported = true, - isSearchSupported = true, - isSearchWithFiltersSupported = true, - isYearRangeSupported = true, - ) - - override suspend fun getFilterOptions() = MangaListFilterOptions( - availableTags = fetchAvailableTags(), - availableStates = EnumSet.of(MangaState.FINISHED, MangaState.ABANDONED, MangaState.UPCOMING), - ) - - override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { - val domain = domain - val root = if (filter.isEmpty()) { - webClient.httpGet( - "https://$domain/list?sortType=${ - getSortKey(order) - }&offset=${offset upBy PAGE_SIZE}", - ).parseHtml().body().let { doc -> (doc.getElementById("mangaBox") ?: doc.getElementById("mangaResults")) } - } else { - advancedSearch(offset, order, filter).parseHtml() - } - val tiles = root.selectFirst("div.tiles.row") - if (tiles == null) { - if (!root.getElementsContainingOwnText(NOTHING_FOUND).isNullOrEmpty()) { - return emptyList() - } - root.parseFailed("No tiles found") - } - return tiles.select("div.tile").mapNotNull(::parseManga) - } - - override suspend fun getDetails(manga: Manga): Manga { - val response = webClient.httpGet(manga.url.toAbsoluteUrl(domain)) - val doc = response.parseHtml() - val root = doc.body().requireElementById("mangaBox").run { - selectFirst("div.leftContent") ?: this - } - val dateFormat = SimpleDateFormat("dd.MM.yy", Locale.US) - val coverImg = root.selectFirst("div.subject-cover")?.selectFirst("img") - val translations = if (config[splitTranslationsKey]) { - root.selectFirst("div.translator-selection") - ?.select(".translator-selection-item") - ?.associate { - it.id().removePrefix("tr-").toLong() to it.selectFirst(".translator-selection-name")?.textOrNull() - } - } else { - null - } - val newSource = getSource(response.request.url) - val chaptersList = root.getElementById("chapters-list") - if (chaptersList == null && root.getElementsContainingOwnText(NO_CHAPTERS).isEmpty()) { - root.parseFailed("No chapters found") - } - val hashRegex = Regex("window.user_hash\\s*=\\s*\'([^\']+)\'") - val userHash = doc.select("script").firstNotNullOfOrNull { it.html().findGroupValue(hashRegex) } - val hasNsfwAlert = root.select(".alert-warning").any { it.ownText().contains(NSFW_ALERT) } - return manga.copy( - source = newSource, - title = doc.metaValue("name") ?: manga.title, - altTitles = root.selectFirst(".all-names-popover")?.select(".name")?.mapNotNullToSet { - it.textOrNull() - } ?: manga.altTitles, - publicUrl = response.request.url.toString(), - description = root.selectFirst("div.manga-description")?.html(), - largeCoverUrl = coverImg?.attrAsAbsoluteUrlOrNull("data-full"), - coverUrl = manga.coverUrl - ?: coverImg?.attrAsAbsoluteUrlOrNull("data-thumb")?.replace("_p.", "."), - tags = root.selectFirstOrThrow("div.subject-meta") - .getElementsByAttributeValueContaining("href", "/list/genre/").mapTo(manga.tags.toMutableSet()) { a -> - MangaTag( - title = a.text().toTitleCase(), - key = a.attr("href").removeSuffix('/').substringAfterLast('/'), - source = source, - ) - }, - authors = root.select(".elem_author,.elem_illustrator,.elem_screenwriter") - .select("a.person-link") - .mapNotNullToSet { it.textOrNull() } + manga.authors, - contentRating = (if (hasNsfwAlert) ContentRating.SUGGESTIVE else ContentRating.SAFE) - .coerceAtLeast(manga.contentRating ?: ContentRating.SAFE), - chapters = chaptersList?.select("a.chapter-link") - ?.flatMapChapters(reversed = true) { a -> - val tr = a.selectFirstParent("tr") ?: return@flatMapChapters emptyList() - val href = a.attrAsRelativeUrl("href") - val number = tr.attr("data-num").toFloatOrNull()?.div(10f) ?: 0f - val volume = tr.attr("data-vol").toIntOrNull() ?: 0 - if (translations.isNullOrEmpty() || a.attr("data-translations").isEmpty()) { - var translators = "" - val translatorElement = a.attr("title") - if (!translatorElement.isNullOrBlank()) { - translators = translatorElement.replace("(Переводчик),", "&").removeSuffix(" (Переводчик)") - } - listOf( - MangaChapter( - id = generateUid(href), - name = a.text().removePrefix(manga.title).trim(), - number = number, - volume = volume, - url = href.withQueryParam("d", userHash), - uploadDate = dateFormat.tryParse(tr.selectFirst("td.date")?.text()), - scanlator = translators, - source = newSource, - branch = null, - ), - ) - } else { - val translationData = JSONArray(a.attr("data-translations")) - translationData.mapJSON { jo -> - val personId = jo.getLong("personId") - val link = href.withQueryParam("tran", personId.toString()) - MangaChapter( - id = generateUid(link), - name = a.text().removePrefix(manga.title).trim(), - number = number, - volume = volume, - url = link.withQueryParam("d", userHash), - uploadDate = dateFormat.tryParse(jo.getStringOrNull("dateCreated")), - scanlator = null, - source = newSource, - branch = translations[personId], - ) - } - } - }.orEmpty(), - ) - } - - override suspend fun getPages(chapter: MangaChapter): List { - if (chapter.source != source && chapter.source is MangaParserSource) { // handle redirects between websites - return context.newParserInstance(chapter.source).getPages(chapter) - } - val url = chapter.url.toAbsoluteUrl(domain).toHttpUrl().newBuilder().setQueryParameter("mtr", "1").build() - val doc = webClient.httpGet(url).parseHtml() - val scripts = doc.select("script") - for (script in scripts) { - val data = script.html() - var pos = data.indexOf("rm_h.readerDoInit(") - if (pos != -1) { - parsePagesV2(data, pos)?.let { return it } - } - pos = data.indexOf("rm_h.readerInit( 0,") - if (pos != -1) { - parsePagesV1(data, pos)?.let { return it } - } - pos = data.indexOf(".readerInit(") - if (pos != -1) { - parsePagesV3(data, pos).let { return it } - } - } - doc.parseFailed("Pages list not found at ${chapter.url}") - } - - override suspend fun getPageUrl(page: MangaPage): String { - val parts = page.url.split('|') - if (parts.size < 2) { - return page.url - } - val path = parts.last() - // fast path - cachedPagesServer?.let { host -> - val url = concatUrl("https://$host/", path) - if (tryHead(url)) { - return url - } else { - cachedPagesServer = null - } - } - // slow path - val candidates = HashSet((parts.size - 1) * 2) - for (i in 0 until parts.size - 1) { - val server = parts[i].trim().ifEmpty { "https://$domain/" } - candidates.add(concatUrl(server, path)) - candidates.add(concatUrl(server, path.substringBeforeLast('?'))) - } - return try { - channelFlow { - for (url in candidates) { - launch { - if (tryHead(url)) { - send(url) - } - } - } - }.first().also { - cachedPagesServer = it.toHttpUrlOrNull()?.host - } - } catch (e: NoSuchElementException) { - candidates.randomOrNull() ?: throw ParseException("No page url candidates", page.url, e) - } - } - - private suspend fun fetchAvailableTags(): Set { - val doc = webClient.httpGet("https://${domain}/list/genres/sort_name").parseHtml() - val root = doc.body().getElementById("mangaBox")?.selectFirst("div.leftContent")?.selectFirst("table.table") - ?: doc.parseFailed("Cannot find root") - return root.select("a.element-link").mapToSet { a -> - MangaTag( - title = a.text().toTitleCase(), - key = a.attr("href").substringAfterLast('/'), - source = source, - ) - } - } - - override suspend fun getUsername(): String { - val root = webClient.httpGet("https://grouple.co/").parseHtml().body() - val element = root.selectFirst("img.user-avatar") ?: throw AuthRequiredException(source) - val res = element.parent()?.text() - return if (res.isNullOrEmpty()) { - root.parseFailed("Cannot find username") - } else res - } - - override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request() - if (!request.header(HEADER_ACCEPT).isNullOrEmpty()) { - return chain.proceed(request).checkIfAuthRequired() - } - val ext = request.url.pathSegments.lastOrNull()?.substringAfterLast('.', "")?.lowercase(Locale.ROOT) - return if (ext == "jpg" || ext == "jpeg" || ext == "png" || ext == "webp") { - chain.proceed( - request.newBuilder().header(HEADER_ACCEPT, "image/webp,image/png;q=0.9,image/jpeg,*/*;q=0.8").build(), - ) - } else { - chain.proceed(request).checkIfAuthRequired() - } - } - - override fun onCreateConfig(keys: MutableCollection>) { - super.onCreateConfig(keys) - keys.add(userAgentKey) - keys.add(splitTranslationsKey) - } - - override suspend fun getRelatedManga(seed: Manga): List { - val doc = webClient.httpGet(seed.url.toAbsoluteUrl(domain)).parseHtml() - val root = doc.body().requireElementById("mangaBox").select("h4").first { it.ownText() == RELATED_TITLE } - .nextElementSibling() ?: doc.parseFailed("Cannot find root") - return root.select("div.tile").mapNotNull(::parseManga) - } - - protected open fun getSource(url: HttpUrl): MangaSource = when (url.host) { - in SeiMangaParser.domains -> MangaParserSource.SEIMANGA - in MintMangaParser.domains -> MangaParserSource.MINTMANGA - in ReadmangaParser.domains -> MangaParserSource.READMANGA_RU - in SelfMangaParser.domains -> MangaParserSource.SELFMANGA - in UsagiParser.domains -> MangaParserSource.USAGI - else -> source - } - - private fun getSortKey(sortOrder: SortOrder) = when (sortOrder) { - SortOrder.ALPHABETICAL -> "name" - SortOrder.POPULARITY -> "rate" - SortOrder.UPDATED -> "updated" - SortOrder.ADDED, - SortOrder.NEWEST, - -> "created" - - SortOrder.RATING -> "votes" - else -> "rate" - } - - private suspend fun advancedSearch(offset: Int, order: SortOrder, filter: MangaListFilter): Response { - val tagsMap = tagsIndex.get() - val url = urlBuilder() - .addPathSegment("search") - .addPathSegment("advancedResults") - url.addQueryParameter("q", filter.query) - url.addQueryParameter("offset", offset.toString()) - filter.tags.forEach { tag -> - val tagId = requireNotNull(tagsMap[tag.title.lowercase()]) { "Tag ${tag.title} not found" } - url.addQueryParameter(tagId, "in") - } - filter.tagsExclude.forEach { tag -> - val tagId = requireNotNull(tagsMap[tag.title.lowercase()]) { "Tag ${tag.title} not found" } - url.addQueryParameter(tagId, "ex") - } - url.addQueryParameter( - "years", - buildString { - append(filter.yearFrom.ifZero { YEAR_MIN }) - append(',') - append(filter.yearTo.ifZero { YEAR_MAX }) - }, - ) - url.addQueryParameter( - "sortType", - when (order) { - SortOrder.RATING -> "USER_RATING" - SortOrder.ALPHABETICAL -> "NAME" - SortOrder.ADDED -> "YEAR" - SortOrder.POPULARITY -> "POPULARITY" - SortOrder.NEWEST -> "DATE_CREATE" - SortOrder.UPDATED -> "DATE_UPDATE" - else -> "RATING" - }, - ) - filter.states.forEach { state -> - when (state) { - MangaState.FINISHED -> "s_completed" - MangaState.ABANDONED -> "s_abandoned_popular" - MangaState.UPCOMING -> "s_wait_upload" - else -> null - }?.let { - url.addQueryParameter(it, "in") - } - } - - return webClient.httpGet(url.build()) - } - - private suspend fun tryHead(url: String): Boolean = runCatchingCancellable { - webClient.httpHead(url).use { response -> - response.isSuccessful && !response.isPumpkin() && response.headersContentLength() >= MIN_IMAGE_SIZE - } - }.getOrDefault(false) - - private fun Response.isPumpkin(): Boolean = request.url.host == "upload.wikimedia.org" - - private fun parseManga(node: Element): Manga? { - val imgDiv = node.selectFirst("div.img") ?: return null - val descDiv = node.selectFirst("div.desc") ?: return null - if (descDiv.selectFirst("i.fa-user") != null || descDiv.selectFirst("i.fa-external-link") != null) { - return null // skip author - } - val href = imgDiv.selectFirst("a")?.attrAsAbsoluteUrlOrNull("href") ?: return null - val title = descDiv.selectFirst("h3")?.selectFirst("a")?.text() ?: return null - val tileInfo = descDiv.selectFirst("div.tile-info") - val relUrl = href.toRelativeUrl(domain) - if (relUrl.contains("://")) { - return null - } - val author = tileInfo?.selectFirst("a.person-link")?.text() - return Manga( - id = generateUid(relUrl), - url = relUrl, - publicUrl = href, - title = title, - altTitles = setOfNotNull(descDiv.selectFirst("h5")?.textOrNull()), - coverUrl = imgDiv.selectFirst("img.lazy")?.attrAsAbsoluteUrlOrNull("data-original")?.replace("_p.", "."), - rating = runCatching { - node.selectFirst(".compact-rate")?.attr("title")?.toFloatOrNull()?.div(5f) - }.getOrNull() ?: RATING_UNKNOWN, - authors = author?.let { setOf(it) } ?: emptySet(), - contentRating = if (isNsfwSource) ContentRating.ADULT else null, - tags = runCatching { - tileInfo?.select("a.element-link")?.mapToSet { - MangaTag( - title = it.text().toTitleCase(), - key = it.attr("href").substringAfterLast('/'), - source = source, - ) - } - }.getOrNull().orEmpty(), - state = when { - node.selectFirst("div.tags")?.selectFirst("span.mangaCompleted") != null -> MangaState.FINISHED - - else -> null - }, - source = source, - ) - } - - private fun parsePagesV1(data: String, pos: Int): List? { - val json = data.substring(pos).substringAfter('(').substringBefore('\n').substringBeforeLast(')') - if (json.isEmpty()) { - return null - } - val ja = JSONArray("[$json]") - val pages = ja.getJSONArray(1) - val servers = ja.getJSONArray(3).mapJSON { it.getString("path") } - val serversStr = servers.joinToString("|") - return (0 until pages.length()).map { i -> - val page = pages.getJSONArray(i) - val primaryServer = page.getString(0) - val url = page.getString(2) - MangaPage( - id = generateUid(url), - url = "$primaryServer|$serversStr|$url", - preview = null, - source = source, - ) - } - } - - private fun parsePagesV2(data: String, pos: Int): List? { - val json = data.substring(pos).substringAfter('(').substringBefore('\n').substringBeforeLast(')') - if (json.isEmpty()) { - return null - } - val ja = JSONArray("[$json]") - val pages = ja.getJSONArray(0) - val servers = ja.getJSONArray(2).mapJSON { it.getString("path") } - val serversStr = servers.joinToString("|") - return (0 until pages.length()).map { i -> - val page = pages.getJSONArray(i) - val primaryServer = page.getString(0) - val url = page.getString(2) - MangaPage( - id = generateUid(url), - url = "$primaryServer|$serversStr|$url", - preview = null, - source = source, - ) - } - } - - private fun parsePagesV3(data: String, pos: Int): List { - val json = JSONArray(data.substring(pos).substringBetween("(", ")").substringBeforeLast(',')) - return (0 until json.length()).map { i -> - val ja = json.getJSONArray(i) - val server = ja.getString(0).ifEmpty { "https://$domain" } - val url = ja.getString(2) - MangaPage( - id = generateUid(url), - url = concatUrl(server, url), - preview = null, - source = source, - ) - } - } - - private suspend fun fetchTagsMap(): ScatterMap { - val url = "https://$domain/search/advanced" - val properties = - webClient.httpGet(url).parseHtml().body().selectFirst("form.search-form")?.select("div.form-group") - ?.find { it.selectFirst("li.property") != null } - ?.select("li.property") - ?: throw ParseException("Genres filter element not found", url) - val result = MutableScatterMap(properties.size) - properties.forEach { li -> - val name = li.text().lowercase() - val id = li.selectFirstOrThrow("input").id() - result[name] = id - } - return result - } - - private fun String.withQueryParam(name: String, value: String?): String { - if (value == null) return this - return toAbsoluteUrl(domain) - .toHttpUrl() - .newBuilder() - .setQueryParameter(name, value) - .build() - .toString() - .toRelativeUrl(domain) - } - - @Throws(IOException::class) - private fun Response.checkIfAuthRequired(): Response { - val lastPathSegment = request.url.pathSegments.lastOrNull() - if (lastPathSegment == "login") { - closeQuietly() - throw AuthRequiredException(source) - } - if (code == HttpURLConnection.HTTP_NOT_FOUND) { - if (!isAuthorized) { - closeQuietly() - throw AuthRequiredException(source) - } else { - return newBuilder().code(HttpURLConnection.HTTP_OK).build() - } - } - return this - } + @Volatile + private var cachedPagesServer: String? = null + + override val userAgentKey = ConfigKey.UserAgent( + "Mozilla/5.0 (X11; U; UNICOS lcLinux; en-US) Gecko/20140730 (KHTML, like Gecko, Safari/419.3) Arora/0.8.0", + ) + private val splitTranslationsKey = ConfigKey.SplitByTranslations(false) + private val tagsIndex = suspendLazy(initializer = ::fetchTagsMap) + + override fun getRequestHeaders(): Headers = Headers.Builder() + .add("User-Agent", config[userAgentKey]) + .add("Accept-Language", "ru,en-US;q=0.7,en;q=0.3") + .build() + + override val availableSortOrders: Set = EnumSet.of( + SortOrder.UPDATED, + SortOrder.POPULARITY, + SortOrder.NEWEST, + SortOrder.RATING, + SortOrder.ALPHABETICAL, + SortOrder.ADDED, + ) + + override val authUrl: String + get() { + val targetUri = "https://${domain}/".urlEncoded() + return "https://grouple.co/internal/auth/sso?siteId=$siteId&=targetUri=$targetUri" + } + + override val isAuthorized: Boolean + get() = context.cookieJar.getCookies(domain).any { it.name == "gwt" } + + override val filterCapabilities: MangaListFilterCapabilities + get() = MangaListFilterCapabilities( + isMultipleTagsSupported = true, + isTagsExclusionSupported = true, + isSearchSupported = true, + isSearchWithFiltersSupported = true, + isYearRangeSupported = true, + ) + + override suspend fun getFilterOptions() = MangaListFilterOptions( + availableTags = fetchAvailableTags(), + availableStates = EnumSet.of(MangaState.FINISHED, MangaState.ABANDONED, MangaState.UPCOMING), + ) + + override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { + val domain = domain + val root = if (filter.isEmpty()) { + webClient.httpGet( + "https://$domain/list?sortType=${ + getSortKey(order) + }&offset=${offset upBy PAGE_SIZE}", + ).parseHtml().body().let { doc -> (doc.getElementById("mangaBox") ?: doc.getElementById("mangaResults")) } + } else { + advancedSearch(offset, order, filter).parseHtml() + } + val tiles = root.selectFirst("div.tiles.row") + if (tiles == null) { + if (!root.getElementsContainingOwnText(NOTHING_FOUND).isNullOrEmpty()) { + return emptyList() + } + root.parseFailed("No tiles found") + } + return tiles.select("div.tile").mapNotNull(::parseManga) + } + + override suspend fun getDetails(manga: Manga): Manga { + val response = webClient.httpGet(manga.url.toAbsoluteUrl(domain)) + val doc = response.parseHtml() + val root = doc.body().requireElementById("mangaBox").run { + selectFirst("div.leftContent") ?: this + } + val dateFormat = SimpleDateFormat("dd.MM.yy", Locale.US) + val coverImg = root.selectFirst("div.subject-cover")?.selectFirst("img") + val translations = if (config[splitTranslationsKey]) { + root.selectFirst("div.translator-selection") + ?.select(".translator-selection-item") + ?.associate { + it.id().removePrefix("tr-").toLong() to it.selectFirst(".translator-selection-name")?.textOrNull() + } + } else { + null + } + val newSource = getSource(response.request.url) + val chaptersList = root.getElementById("chapters-list") + if (chaptersList == null && root.getElementsContainingOwnText(NO_CHAPTERS).isEmpty()) { + root.parseFailed("No chapters found") + } + val hashRegex = Regex("window.user_hash\\s*=\\s*\'([^\']+)\'") + val userHash = doc.select("script").firstNotNullOfOrNull { it.html().findGroupValue(hashRegex) } + val hasNsfwAlert = root.select(".alert-warning").any { it.ownText().contains(NSFW_ALERT) } + return manga.copy( + source = newSource, + title = doc.metaValue("name") ?: manga.title, + altTitles = root.selectFirst(".all-names-popover")?.select(".name")?.mapNotNullToSet { + it.textOrNull() + } ?: manga.altTitles, + publicUrl = response.request.url.toString(), + description = root.selectFirst("div.manga-description")?.html(), + largeCoverUrl = coverImg?.attrAsAbsoluteUrlOrNull("data-full"), + coverUrl = manga.coverUrl + ?: coverImg?.attrAsAbsoluteUrlOrNull("data-thumb")?.replace("_p.", "."), + tags = root.selectFirstOrThrow("div.subject-meta") + .getElementsByAttributeValueContaining("href", "/list/genre/").mapTo(manga.tags.toMutableSet()) { a -> + MangaTag( + title = a.text().toTitleCase(), + key = a.attr("href").removeSuffix('/').substringAfterLast('/'), + source = source, + ) + }, + authors = root.select(".elem_author,.elem_illustrator,.elem_screenwriter") + .select("a.person-link") + .mapNotNullToSet { it.textOrNull() } + manga.authors, + contentRating = (if (hasNsfwAlert) ContentRating.SUGGESTIVE else ContentRating.SAFE) + .coerceAtLeast(manga.contentRating ?: ContentRating.SAFE), + chapters = chaptersList?.select("a.chapter-link") + ?.flatMapChapters(reversed = true) { a -> + val tr = a.selectFirstParent("tr") ?: return@flatMapChapters emptyList() + val href = a.attrAsRelativeUrl("href") + val number = tr.attr("data-num").toFloatOrNull()?.div(10f) ?: 0f + val volume = tr.attr("data-vol").toIntOrNull() ?: 0 + if (translations.isNullOrEmpty() || a.attr("data-translations").isEmpty()) { + var translators = "" + val translatorElement = a.attr("title") + if (!translatorElement.isNullOrBlank()) { + translators = translatorElement.replace("(Переводчик),", "&").removeSuffix(" (Переводчик)") + } + listOf( + MangaChapter( + id = generateUid(href), + name = a.text().removePrefix(manga.title).trim(), + number = number, + volume = volume, + url = href.withQueryParam("d", userHash), + uploadDate = dateFormat.tryParse(tr.selectFirst("td.date")?.text()), + scanlator = translators, + source = newSource, + branch = null, + ), + ) + } else { + val translationData = JSONArray(a.attr("data-translations")) + translationData.mapJSON { jo -> + val personId = jo.getLong("personId") + val link = href.withQueryParam("tran", personId.toString()) + MangaChapter( + id = generateUid(link), + name = a.text().removePrefix(manga.title).trim(), + number = number, + volume = volume, + url = link.withQueryParam("d", userHash), + uploadDate = dateFormat.tryParse(jo.getStringOrNull("dateCreated")), + scanlator = null, + source = newSource, + branch = translations[personId], + ) + } + } + }.orEmpty(), + ) + } + + override suspend fun getPages(chapter: MangaChapter): List { + if (chapter.source != source && chapter.source is MangaParserSource) { // handle redirects between websites + return context.newParserInstance(chapter.source).getPages(chapter) + } + val url = chapter.url.toAbsoluteUrl(domain).toHttpUrl().newBuilder().setQueryParameter("mtr", "1").build() + val doc = webClient.httpGet(url).parseHtml() + val scripts = doc.select("script") + for (script in scripts) { + val data = script.html() + var pos = data.indexOf("rm_h.readerDoInit(") + if (pos != -1) { + parsePagesV2(data, pos)?.let { return it } + } + pos = data.indexOf("rm_h.readerInit( 0,") + if (pos != -1) { + parsePagesV1(data, pos)?.let { return it } + } + pos = data.indexOf(".readerInit(") + if (pos != -1) { + parsePagesV3(data, pos).let { return it } + } + } + doc.parseFailed("Pages list not found at ${chapter.url}") + } + + override suspend fun getPageUrl(page: MangaPage): String { + val parts = page.url.split('|') + if (parts.size < 2) { + return page.url + } + val path = parts.last() + // fast path + cachedPagesServer?.let { host -> + val url = concatUrl("https://$host/", path) + if (tryHead(url)) { + return url + } else { + cachedPagesServer = null + } + } + // slow path + val candidates = HashSet((parts.size - 1) * 2) + for (i in 0 until parts.size - 1) { + val server = parts[i].trim().ifEmpty { "https://$domain/" } + candidates.add(concatUrl(server, path)) + candidates.add(concatUrl(server, path.substringBeforeLast('?'))) + } + return try { + channelFlow { + for (url in candidates) { + launch { + if (tryHead(url)) { + send(url) + } + } + } + }.first().also { + cachedPagesServer = it.toHttpUrlOrNull()?.host + } + } catch (e: NoSuchElementException) { + candidates.randomOrNull() ?: throw ParseException("No page url candidates", page.url, e) + } + } + + private suspend fun fetchAvailableTags(): Set { + val doc = webClient.httpGet("https://${domain}/list/genres/sort_name").parseHtml() + val root = doc.body().getElementById("mangaBox")?.selectFirst("div.leftContent")?.selectFirst("table.table") + ?: doc.parseFailed("Cannot find root") + return root.select("a.element-link").mapToSet { a -> + MangaTag( + title = a.text().toTitleCase(), + key = a.attr("href").substringAfterLast('/'), + source = source, + ) + } + } + + override suspend fun getUsername(): String { + val root = webClient.httpGet("https://grouple.co/").parseHtml().body() + val element = root.selectFirst("img.user-avatar") ?: throw AuthRequiredException(source) + val res = element.parent()?.text() + return if (res.isNullOrEmpty()) { + root.parseFailed("Cannot find username") + } else res + } + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + if (!request.header(HEADER_ACCEPT).isNullOrEmpty()) { + return chain.proceed(request).checkIfAuthRequired() + } + val ext = request.url.pathSegments.lastOrNull()?.substringAfterLast('.', "")?.lowercase(Locale.ROOT) + return if (ext == "jpg" || ext == "jpeg" || ext == "png" || ext == "webp") { + chain.proceed( + request.newBuilder().header(HEADER_ACCEPT, "image/webp,image/png;q=0.9,image/jpeg,*/*;q=0.8").build(), + ) + } else { + chain.proceed(request).checkIfAuthRequired() + } + } + + override fun onCreateConfig(keys: MutableCollection>) { + super.onCreateConfig(keys) + keys.add(userAgentKey) + keys.add(splitTranslationsKey) + } + + override suspend fun getRelatedManga(seed: Manga): List { + val doc = webClient.httpGet(seed.url.toAbsoluteUrl(domain)).parseHtml() + val root = doc.body().requireElementById("mangaBox").select("h4").first { it.ownText() == RELATED_TITLE } + .nextElementSibling() ?: doc.parseFailed("Cannot find root") + return root.select("div.tile").mapNotNull(::parseManga) + } + + protected open fun getSource(url: HttpUrl): MangaSource = when (url.host) { + in SeiMangaParser.domains -> MangaParserSource.SEIMANGA + in MintMangaParser.domains -> MangaParserSource.MINTMANGA + in ReadmangaParser.domains -> MangaParserSource.READMANGA_RU + in SelfMangaParser.domains -> MangaParserSource.SELFMANGA + in UsagiParser.domains -> MangaParserSource.USAGI + else -> source + } + + private fun getSortKey(sortOrder: SortOrder) = when (sortOrder) { + SortOrder.ALPHABETICAL -> "name" + SortOrder.POPULARITY -> "rate" + SortOrder.UPDATED -> "updated" + SortOrder.ADDED, + SortOrder.NEWEST, + -> "created" + + SortOrder.RATING -> "votes" + else -> "rate" + } + + private suspend fun advancedSearch(offset: Int, order: SortOrder, filter: MangaListFilter): Response { + val tagsMap = tagsIndex.get() + val url = urlBuilder() + .addPathSegment("search") + .addPathSegment("advancedResults") + url.addQueryParameter("q", filter.query) + url.addQueryParameter("offset", offset.toString()) + filter.tags.forEach { tag -> + val tagId = requireNotNull(tagsMap[tag.title.lowercase()]) { "Tag ${tag.title} not found" } + url.addQueryParameter(tagId, "in") + } + filter.tagsExclude.forEach { tag -> + val tagId = requireNotNull(tagsMap[tag.title.lowercase()]) { "Tag ${tag.title} not found" } + url.addQueryParameter(tagId, "ex") + } + url.addQueryParameter( + "years", + buildString { + append(filter.yearFrom.ifZero { YEAR_MIN }) + append(',') + append(filter.yearTo.ifZero { YEAR_MAX }) + }, + ) + url.addQueryParameter( + "sortType", + when (order) { + SortOrder.RATING -> "USER_RATING" + SortOrder.ALPHABETICAL -> "NAME" + SortOrder.ADDED -> "YEAR" + SortOrder.POPULARITY -> "POPULARITY" + SortOrder.NEWEST -> "DATE_CREATE" + SortOrder.UPDATED -> "DATE_UPDATE" + else -> "RATING" + }, + ) + filter.states.forEach { state -> + when (state) { + MangaState.FINISHED -> "s_completed" + MangaState.ABANDONED -> "s_abandoned_popular" + MangaState.UPCOMING -> "s_wait_upload" + else -> null + }?.let { + url.addQueryParameter(it, "in") + } + } + + return webClient.httpGet(url.build()) + } + + private suspend fun tryHead(url: String): Boolean = runCatchingCancellable { + webClient.httpHead(url).use { response -> + response.isSuccessful && !response.isPumpkin() && response.headersContentLength() >= MIN_IMAGE_SIZE + } + }.getOrDefault(false) + + private fun Response.isPumpkin(): Boolean = request.url.host == "upload.wikimedia.org" + + private fun parseManga(node: Element): Manga? { + val imgDiv = node.selectFirst("div.img") ?: return null + val descDiv = node.selectFirst("div.desc") ?: return null + if (descDiv.selectFirst("i.fa-user") != null || descDiv.selectFirst("i.fa-external-link") != null) { + return null // skip author + } + val href = imgDiv.selectFirst("a")?.attrAsAbsoluteUrlOrNull("href") ?: return null + val title = descDiv.selectFirst("h3")?.selectFirst("a")?.text() ?: return null + val tileInfo = descDiv.selectFirst("div.tile-info") + val relUrl = href.toRelativeUrl(domain) + if (relUrl.contains("://")) { + return null + } + val author = tileInfo?.selectFirst("a.person-link")?.text() + return Manga( + id = generateUid(relUrl), + url = relUrl, + publicUrl = href, + title = title, + altTitles = setOfNotNull(descDiv.selectFirst("h5")?.textOrNull()), + coverUrl = imgDiv.selectFirst("img.lazy")?.attrAsAbsoluteUrlOrNull("data-original")?.replace("_p.", "."), + rating = runCatching { + node.selectFirst(".compact-rate")?.attr("title")?.toFloatOrNull()?.div(5f) + }.getOrNull() ?: RATING_UNKNOWN, + authors = setOfNotNull(author), + contentRating = if (isNsfwSource) ContentRating.ADULT else null, + tags = runCatching { + tileInfo?.select("a.element-link")?.mapToSet { + MangaTag( + title = it.text().toTitleCase(), + key = it.attr("href").substringAfterLast('/'), + source = source, + ) + } + }.getOrNull().orEmpty(), + state = when { + node.selectFirst("div.tags")?.selectFirst("span.mangaCompleted") != null -> MangaState.FINISHED + + else -> null + }, + source = source, + ) + } + + private fun parsePagesV1(data: String, pos: Int): List? { + val json = data.substring(pos).substringAfter('(').substringBefore('\n').substringBeforeLast(')') + if (json.isEmpty()) { + return null + } + val ja = JSONArray("[$json]") + val pages = ja.getJSONArray(1) + val servers = ja.getJSONArray(3).mapJSON { it.getString("path") } + val serversStr = servers.joinToString("|") + return (0 until pages.length()).map { i -> + val page = pages.getJSONArray(i) + val primaryServer = page.getString(0) + val url = page.getString(2) + MangaPage( + id = generateUid(url), + url = "$primaryServer|$serversStr|$url", + preview = null, + source = source, + ) + } + } + + private fun parsePagesV2(data: String, pos: Int): List? { + val json = data.substring(pos).substringAfter('(').substringBefore('\n').substringBeforeLast(')') + if (json.isEmpty()) { + return null + } + val ja = JSONArray("[$json]") + val pages = ja.getJSONArray(0) + val servers = ja.getJSONArray(2).mapJSON { it.getString("path") } + val serversStr = servers.joinToString("|") + return (0 until pages.length()).map { i -> + val page = pages.getJSONArray(i) + val primaryServer = page.getString(0) + val url = page.getString(2) + MangaPage( + id = generateUid(url), + url = "$primaryServer|$serversStr|$url", + preview = null, + source = source, + ) + } + } + + private fun parsePagesV3(data: String, pos: Int): List { + val json = JSONArray(data.substring(pos).substringBetween("(", ")").substringBeforeLast(',')) + return (0 until json.length()).map { i -> + val ja = json.getJSONArray(i) + val server = ja.getString(0).ifEmpty { "https://$domain" } + val url = ja.getString(2) + MangaPage( + id = generateUid(url), + url = concatUrl(server, url), + preview = null, + source = source, + ) + } + } + + private suspend fun fetchTagsMap(): ScatterMap { + val url = "https://$domain/search/advanced" + val properties = + webClient.httpGet(url).parseHtml().body().selectFirst("form.search-form")?.select("div.form-group") + ?.find { it.selectFirst("li.property") != null } + ?.select("li.property") + ?: throw ParseException("Genres filter element not found", url) + val result = MutableScatterMap(properties.size) + properties.forEach { li -> + val name = li.text().lowercase() + val id = li.selectFirstOrThrow("input").id() + result[name] = id + } + return result + } + + private fun String.withQueryParam(name: String, value: String?): String { + if (value == null) return this + return toAbsoluteUrl(domain) + .toHttpUrl() + .newBuilder() + .setQueryParameter(name, value) + .build() + .toString() + .toRelativeUrl(domain) + } + + @Throws(IOException::class) + private fun Response.checkIfAuthRequired(): Response { + val lastPathSegment = request.url.pathSegments.lastOrNull() + if (lastPathSegment == "login") { + closeQuietly() + throw AuthRequiredException(source) + } + if (code == HttpURLConnection.HTTP_NOT_FOUND) { + if (!isAuthorized) { + closeQuietly() + throw AuthRequiredException(source) + } else { + return newBuilder().code(HttpURLConnection.HTTP_OK).build() + } + } + return this + } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/ChanParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/ChanParser.kt index 3be544c0..a29c260c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/ChanParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/ChanParser.kt @@ -60,7 +60,7 @@ internal abstract class ChanParser( publicUrl = href.toAbsoluteUrl(a.host ?: domain), altTitles = setOfNotNull(title.second), title = title.first, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), coverUrl = row.selectFirst("div.manga_images")?.selectFirst("img") ?.absUrl("src").orEmpty(), tags = runCatching { @@ -176,7 +176,7 @@ internal abstract class ChanParser( publicUrl = href.toAbsoluteUrl(a.host ?: domain), altTitles = setOfNotNull(title.second), title = title.first, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), coverUrl = div.selectFirst("img")?.absUrl("src").orEmpty(), tags = emptySet(), rating = RATING_UNKNOWN, diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/LibSocialParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/LibSocialParser.kt index 29c082b3..3238d8f1 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/LibSocialParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/LibSocialParser.kt @@ -17,346 +17,346 @@ import java.text.SimpleDateFormat import java.util.* internal abstract class LibSocialParser( - context: MangaLoaderContext, - source: MangaParserSource, - protected val siteDomain: String, - protected val siteId: Int, + context: MangaLoaderContext, + source: MangaParserSource, + protected val siteDomain: String, + protected val siteId: Int, ) : LegacyPagedMangaParser(context, source, pageSize = 60) { - override val availableSortOrders: Set = EnumSet.of( - SortOrder.UPDATED, - SortOrder.POPULARITY, - SortOrder.RATING, - SortOrder.NEWEST, - SortOrder.ALPHABETICAL, - SortOrder.ALPHABETICAL_DESC, - ) + override val availableSortOrders: Set = EnumSet.of( + SortOrder.UPDATED, + SortOrder.POPULARITY, + SortOrder.RATING, + SortOrder.NEWEST, + SortOrder.ALPHABETICAL, + SortOrder.ALPHABETICAL_DESC, + ) - final override val configKeyDomain = ConfigKey.Domain(siteDomain) + final override val configKeyDomain = ConfigKey.Domain(siteDomain) - override val filterCapabilities: MangaListFilterCapabilities - get() = MangaListFilterCapabilities( - isMultipleTagsSupported = true, - isTagsExclusionSupported = true, - isSearchSupported = true, - isSearchWithFiltersSupported = true, - ) + override val filterCapabilities: MangaListFilterCapabilities + get() = MangaListFilterCapabilities( + isMultipleTagsSupported = true, + isTagsExclusionSupported = true, + isSearchSupported = true, + isSearchWithFiltersSupported = true, + ) - override suspend fun getFilterOptions() = MangaListFilterOptions( - availableTags = fetchAvailableTags(), - availableStates = EnumSet.allOf(MangaState::class.java), - ) + override suspend fun getFilterOptions() = MangaListFilterOptions( + availableTags = fetchAvailableTags(), + availableStates = EnumSet.allOf(MangaState::class.java), + ) - private val statesMap = intObjectMapOf( - 1, MangaState.ONGOING, - 2, MangaState.FINISHED, - 3, MangaState.UPCOMING, - 4, MangaState.PAUSED, - 5, MangaState.ABANDONED, - ) - private val imageServers = suspendLazy(initializer = ::fetchServers) - private val splitTranslationsKey = ConfigKey.SplitByTranslations(true) - private val preferredServerKey = ConfigKey.PreferredImageServer( - presetValues = mapOf( - null to null, - SERVER_MAIN to "Первый", - SERVER_SECONDARY to "Второй", - SERVER_COMPRESS to "Сжатия", - SERVER_DOWNLOAD to "Загрузки", - SERVER_CROP to "Обрезки", - ), - defaultValue = null, - ) + private val statesMap = intObjectMapOf( + 1, MangaState.ONGOING, + 2, MangaState.FINISHED, + 3, MangaState.UPCOMING, + 4, MangaState.PAUSED, + 5, MangaState.ABANDONED, + ) + private val imageServers = suspendLazy(initializer = ::fetchServers) + private val splitTranslationsKey = ConfigKey.SplitByTranslations(true) + private val preferredServerKey = ConfigKey.PreferredImageServer( + presetValues = mapOf( + null to null, + SERVER_MAIN to "Первый", + SERVER_SECONDARY to "Второй", + SERVER_COMPRESS to "Сжатия", + SERVER_DOWNLOAD to "Загрузки", + SERVER_CROP to "Обрезки", + ), + defaultValue = null, + ) - override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val urlBuilder = HttpUrl.Builder() - .scheme("https") - .host("api.lib.social") - .addPathSegment("api") - .addPathSegment("manga") - .addQueryParameter("site_id[]", siteId.toString()) - .addQueryParameter("fields[]", "rate") - .addQueryParameter("fields[]", "rate_avg") - .addQueryParameter("page", page.toString()) - for (state in filter.states) { - urlBuilder.addQueryParameter("status[]", statesMap.keyOf(state).toString()) - } - for (tag in filter.tags) { - urlBuilder.addQueryParameter("${tag.typeKey()}[]", tag.key.drop(1)) - } - for (tag in filter.tagsExclude) { - urlBuilder.addQueryParameter("${tag.typeKey()}_exclude[]", tag.key.drop(1)) - } - if (!filter.query.isNullOrEmpty()) { - urlBuilder.addQueryParameter("q", filter.query) - } - urlBuilder.addQueryParameter( - "sort_by", - when (order) { - SortOrder.UPDATED -> "last_chapter_at" - SortOrder.POPULARITY -> "views" - SortOrder.RATING -> "rate_avg" - SortOrder.NEWEST -> "created_at" - SortOrder.ALPHABETICAL, - SortOrder.ALPHABETICAL_DESC, - -> "rus_name" + override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val urlBuilder = HttpUrl.Builder() + .scheme("https") + .host("api.lib.social") + .addPathSegment("api") + .addPathSegment("manga") + .addQueryParameter("site_id[]", siteId.toString()) + .addQueryParameter("fields[]", "rate") + .addQueryParameter("fields[]", "rate_avg") + .addQueryParameter("page", page.toString()) + for (state in filter.states) { + urlBuilder.addQueryParameter("status[]", statesMap.keyOf(state).toString()) + } + for (tag in filter.tags) { + urlBuilder.addQueryParameter("${tag.typeKey()}[]", tag.key.drop(1)) + } + for (tag in filter.tagsExclude) { + urlBuilder.addQueryParameter("${tag.typeKey()}_exclude[]", tag.key.drop(1)) + } + if (!filter.query.isNullOrEmpty()) { + urlBuilder.addQueryParameter("q", filter.query) + } + urlBuilder.addQueryParameter( + "sort_by", + when (order) { + SortOrder.UPDATED -> "last_chapter_at" + SortOrder.POPULARITY -> "views" + SortOrder.RATING -> "rate_avg" + SortOrder.NEWEST -> "created_at" + SortOrder.ALPHABETICAL, + SortOrder.ALPHABETICAL_DESC, + -> "rus_name" - else -> null - }, - ) - urlBuilder.addQueryParameter( - "sort_type", - when (order) { - SortOrder.UPDATED, - SortOrder.POPULARITY, - SortOrder.RATING, - SortOrder.NEWEST, - SortOrder.ALPHABETICAL_DESC, - -> "desc" + else -> null + }, + ) + urlBuilder.addQueryParameter( + "sort_type", + when (order) { + SortOrder.UPDATED, + SortOrder.POPULARITY, + SortOrder.RATING, + SortOrder.NEWEST, + SortOrder.ALPHABETICAL_DESC, + -> "desc" - SortOrder.ALPHABETICAL -> "asc" - else -> null - }, - ) - val json = webClient.httpGet(urlBuilder.build()).parseJson() - val data = json.getJSONArray("data") - return data.mapJSON(::parseManga) - } + SortOrder.ALPHABETICAL -> "asc" + else -> null + }, + ) + val json = webClient.httpGet(urlBuilder.build()).parseJson() + val data = json.getJSONArray("data") + return data.mapJSON(::parseManga) + } - override suspend fun getDetails(manga: Manga): Manga = coroutineScope { - val chapters = async { fetchChapters(manga) } - val url = HttpUrl.Builder() - .scheme("https") - .host("api.lib.social") - .addPathSegment("api") - .addPathSegment("manga") - .addPathSegment(manga.url) - .addQueryParameter("fields[]", "summary") - .addQueryParameter("fields[]", "genres") - .addQueryParameter("fields[]", "tags") - .addQueryParameter("fields[]", "authors") - .build() - val json = webClient.httpGet(url).parseJson().getJSONObject("data") - val genres = json.getJSONArray("genres").mapJSON { jo -> - MangaTag(title = jo.getString("name"), key = "g" + jo.getInt("id"), source = source) - } - val tags = json.getJSONArray("genres").mapJSON { jo -> - MangaTag(title = jo.getString("name"), key = "t" + jo.getInt("id"), source = source) - } - val author = json.getJSONArray("authors").optJSONObject(0)?.getStringOrNull("name") - manga.copy( - title = json.getStringOrNull("rus_name") ?: manga.title, - altTitles = setOfNotNull(json.getStringOrNull("name")), - tags = tagsSetOf(tags, genres), - authors = author?.let { setOf(it) } ?: emptySet(), - description = json.getString("summary").nl2br(), - chapters = chapters.await(), - ) - } + override suspend fun getDetails(manga: Manga): Manga = coroutineScope { + val chapters = async { fetchChapters(manga) } + val url = HttpUrl.Builder() + .scheme("https") + .host("api.lib.social") + .addPathSegment("api") + .addPathSegment("manga") + .addPathSegment(manga.url) + .addQueryParameter("fields[]", "summary") + .addQueryParameter("fields[]", "genres") + .addQueryParameter("fields[]", "tags") + .addQueryParameter("fields[]", "authors") + .build() + val json = webClient.httpGet(url).parseJson().getJSONObject("data") + val genres = json.getJSONArray("genres").mapJSON { jo -> + MangaTag(title = jo.getString("name"), key = "g" + jo.getInt("id"), source = source) + } + val tags = json.getJSONArray("genres").mapJSON { jo -> + MangaTag(title = jo.getString("name"), key = "t" + jo.getInt("id"), source = source) + } + val author = json.getJSONArray("authors").optJSONObject(0)?.getStringOrNull("name") + manga.copy( + title = json.getStringOrNull("rus_name") ?: manga.title, + altTitles = setOfNotNull(json.getStringOrNull("name")), + tags = tagsSetOf(tags, genres), + authors = setOfNotNull(author), + description = json.getString("summary").nl2br(), + chapters = chapters.await(), + ) + } - override suspend fun getPages(chapter: MangaChapter): List = coroutineScope { - val pages = async { - webClient.httpGet( - concatUrl("https://api.lib.social/api/manga/", chapter.url), - ).parseJson().getJSONObject("data") - } - val servers = imageServers.get() - val json = pages.await() - val primaryServer = getPrimaryImageServer(servers) - json.getJSONArray("pages").mapJSON { jo -> - val url = jo.getString("url") - MangaPage( - id = generateUid(jo.getLong("id")), - url = concatUrl(primaryServer, url), - preview = servers[SERVER_COMPRESS]?.let { concatUrl(it, url) }, - source = source, - ) - } - } + override suspend fun getPages(chapter: MangaChapter): List = coroutineScope { + val pages = async { + webClient.httpGet( + concatUrl("https://api.lib.social/api/manga/", chapter.url), + ).parseJson().getJSONObject("data") + } + val servers = imageServers.get() + val json = pages.await() + val primaryServer = getPrimaryImageServer(servers) + json.getJSONArray("pages").mapJSON { jo -> + val url = jo.getString("url") + MangaPage( + id = generateUid(jo.getLong("id")), + url = concatUrl(primaryServer, url), + preview = servers[SERVER_COMPRESS]?.let { concatUrl(it, url) }, + source = source, + ) + } + } - private suspend fun fetchAvailableTags(): Set = coroutineScope { - val tags = async { fetchTags("tags") } - val genres = async { fetchTags("genres") } - tagsSetOf(tags.await(), genres.await()) - } + private suspend fun fetchAvailableTags(): Set = coroutineScope { + val tags = async { fetchTags("tags") } + val genres = async { fetchTags("genres") } + tagsSetOf(tags.await(), genres.await()) + } - override suspend fun getRelatedManga(seed: Manga): List { - val json = webClient.httpGet( - HttpUrl.Builder() - .scheme("https") - .host("api.lib.social") - .addPathSegment("api") - .addPathSegment("manga") - .addPathSegment(seed.url) - .addPathSegment("similar") - .build(), - ).parseJson().getJSONArray("data") - return json.mapJSON { jo -> - parseManga(jo.getJSONObject("media")) - } - } + override suspend fun getRelatedManga(seed: Manga): List { + val json = webClient.httpGet( + HttpUrl.Builder() + .scheme("https") + .host("api.lib.social") + .addPathSegment("api") + .addPathSegment("manga") + .addPathSegment(seed.url) + .addPathSegment("similar") + .build(), + ).parseJson().getJSONArray("data") + return json.mapJSON { jo -> + parseManga(jo.getJSONObject("media")) + } + } - override fun onCreateConfig(keys: MutableCollection>) { - super.onCreateConfig(keys) - keys.remove(configKeyDomain) - keys.add(splitTranslationsKey) - keys.add(preferredServerKey) - } + override fun onCreateConfig(keys: MutableCollection>) { + super.onCreateConfig(keys) + keys.remove(configKeyDomain) + keys.add(splitTranslationsKey) + keys.add(preferredServerKey) + } - private fun parseManga(jo: JSONObject): Manga { - val cover = jo.getJSONObject("cover") - val isNsfwSource = jo.getJSONObject("ageRestriction").getIntOrDefault("id", 0) >= 3 - return Manga( - id = generateUid(jo.getLong("id")), - title = jo.getString("rus_name").ifEmpty { jo.getString("name") }, - altTitles = setOfNotNull(jo.getString("name")), - url = jo.getString("slug_url"), - publicUrl = "https://$siteDomain/ru/manga/" + jo.getString("slug_url"), - rating = jo.optJSONObject("rating") - ?.getFloatOrDefault("average", RATING_UNKNOWN * 10f)?.div(10f) ?: RATING_UNKNOWN, - contentRating = if (isNsfwSource) ContentRating.ADULT else null, - coverUrl = cover.getString("thumbnail"), - tags = setOf(), - state = statesMap[jo.optJSONObject("status")?.getIntOrDefault("id", -1) ?: -1], - authors = emptySet(), - largeCoverUrl = cover.getString("default"), - source = source, - ) - } + private fun parseManga(jo: JSONObject): Manga { + val cover = jo.getJSONObject("cover") + val isNsfwSource = jo.getJSONObject("ageRestriction").getIntOrDefault("id", 0) >= 3 + return Manga( + id = generateUid(jo.getLong("id")), + title = jo.getString("rus_name").ifEmpty { jo.getString("name") }, + altTitles = setOfNotNull(jo.getString("name")), + url = jo.getString("slug_url"), + publicUrl = "https://$siteDomain/ru/manga/" + jo.getString("slug_url"), + rating = jo.optJSONObject("rating") + ?.getFloatOrDefault("average", RATING_UNKNOWN * 10f)?.div(10f) ?: RATING_UNKNOWN, + contentRating = if (isNsfwSource) ContentRating.ADULT else null, + coverUrl = cover.getString("thumbnail"), + tags = setOf(), + state = statesMap[jo.optJSONObject("status")?.getIntOrDefault("id", -1) ?: -1], + authors = emptySet(), + largeCoverUrl = cover.getString("default"), + source = source, + ) + } - private fun getPrimaryImageServer(servers: ScatterMap): String { - val preferred = config[preferredServerKey] - if (preferred != null) { - servers[preferred]?.let { return it } - } - return checkNotNull(servers[SERVER_MAIN] ?: servers[SERVER_DOWNLOAD] ?: servers[SERVER_SECONDARY]) { - "No available images servers" - } - } + private fun getPrimaryImageServer(servers: ScatterMap): String { + val preferred = config[preferredServerKey] + if (preferred != null) { + servers[preferred]?.let { return it } + } + return checkNotNull(servers[SERVER_MAIN] ?: servers[SERVER_DOWNLOAD] ?: servers[SERVER_SECONDARY]) { + "No available images servers" + } + } - private suspend fun fetchChapters(manga: Manga): List { - val url = HttpUrl.Builder() - .scheme("https") - .host("api.lib.social") - .addPathSegment("api") - .addPathSegment("manga") - .addPathSegment(manga.url) - .addPathSegment("chapters") - .build() - val json = webClient.httpGet(url).parseJson().getJSONArray("data") - val builder = ChaptersListBuilder(json.length()) - val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US) - val useBranching = config[splitTranslationsKey] - for (i in 0 until json.length()) { - val jo = json.getJSONObject(i) - val volume = jo.getIntOrDefault("volume", 0) - val number = jo.getFloatOrDefault("number", 0f) - val numberString = number.formatSimple() - val name = jo.getStringOrNull("name") ?: buildString { - if (volume > 0) append("Том ").append(volume).append(' ') - append("Глава ").append(numberString) - } - val branches = jo.getJSONArray("branches") - for (j in 0 until branches.length()) { - val bjo = branches.getJSONObject(j) - val id = bjo.getLong("id") - val team = bjo.getJSONArray("teams").optJSONObject(0)?.getStringOrNull("name") - builder += MangaChapter( - id = generateUid(id), - name = name, - number = number, - volume = volume, - url = "${manga.url}/chapter?number=$numberString&volume=$volume", - scanlator = team, - uploadDate = dateFormat.tryParse(bjo.getStringOrNull("created_at")), - branch = if (useBranching) team else null, - source = source, - ) - } - } - return builder.toList() - } + private suspend fun fetchChapters(manga: Manga): List { + val url = HttpUrl.Builder() + .scheme("https") + .host("api.lib.social") + .addPathSegment("api") + .addPathSegment("manga") + .addPathSegment(manga.url) + .addPathSegment("chapters") + .build() + val json = webClient.httpGet(url).parseJson().getJSONArray("data") + val builder = ChaptersListBuilder(json.length()) + val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US) + val useBranching = config[splitTranslationsKey] + for (i in 0 until json.length()) { + val jo = json.getJSONObject(i) + val volume = jo.getIntOrDefault("volume", 0) + val number = jo.getFloatOrDefault("number", 0f) + val numberString = number.formatSimple() + val name = jo.getStringOrNull("name") ?: buildString { + if (volume > 0) append("Том ").append(volume).append(' ') + append("Глава ").append(numberString) + } + val branches = jo.getJSONArray("branches") + for (j in 0 until branches.length()) { + val bjo = branches.getJSONObject(j) + val id = bjo.getLong("id") + val team = bjo.getJSONArray("teams").optJSONObject(0)?.getStringOrNull("name") + builder += MangaChapter( + id = generateUid(id), + name = name, + number = number, + volume = volume, + url = "${manga.url}/chapter?number=$numberString&volume=$volume", + scanlator = team, + uploadDate = dateFormat.tryParse(bjo.getStringOrNull("created_at")), + branch = if (useBranching) team else null, + source = source, + ) + } + } + return builder.toList() + } - private suspend fun fetchTags(type: String): List { - val data = webClient.httpGet( - HttpUrl.Builder() - .scheme("https") - .host("api.lib.social") - .addPathSegment("api").addPathSegment(type).build(), - ).parseJson().getJSONArray("data") - val prefix = type.first().toString() - return data.mapJSONNotNull { jo -> - val sites = jo.getJSONArray("site_ids").toIntSet() - if (siteId !in sites) { - return@mapJSONNotNull null - } - MangaTag( - title = jo.getString("name"), - key = prefix + jo.getInt("id"), - source = source, - ) - } - } + private suspend fun fetchTags(type: String): List { + val data = webClient.httpGet( + HttpUrl.Builder() + .scheme("https") + .host("api.lib.social") + .addPathSegment("api").addPathSegment(type).build(), + ).parseJson().getJSONArray("data") + val prefix = type.first().toString() + return data.mapJSONNotNull { jo -> + val sites = jo.getJSONArray("site_ids").toIntSet() + if (siteId !in sites) { + return@mapJSONNotNull null + } + MangaTag( + title = jo.getString("name"), + key = prefix + jo.getInt("id"), + source = source, + ) + } + } - private suspend fun fetchServers(): ScatterMap { - val json = webClient.httpGet( - HttpUrl.Builder() - .scheme("https") - .host("api.lib.social") - .addPathSegment("api") - .addPathSegment("constants") - .addQueryParameter("fields[]", "imageServers") - .build(), - ).parseJson().getJSONObject("data").getJSONArray("imageServers") - val result = MutableScatterMap() - for (i in 0 until json.length()) { - val jo = json.getJSONObject(i) - val sites = jo.getJSONArray("site_ids").toIntSet() - if (siteId !in sites) { - continue - } - result[jo.getString("id")] = jo.getString("url") - } - return result - } + private suspend fun fetchServers(): ScatterMap { + val json = webClient.httpGet( + HttpUrl.Builder() + .scheme("https") + .host("api.lib.social") + .addPathSegment("api") + .addPathSegment("constants") + .addQueryParameter("fields[]", "imageServers") + .build(), + ).parseJson().getJSONObject("data").getJSONArray("imageServers") + val result = MutableScatterMap() + for (i in 0 until json.length()) { + val jo = json.getJSONObject(i) + val sites = jo.getJSONArray("site_ids").toIntSet() + if (siteId !in sites) { + continue + } + result[jo.getString("id")] = jo.getString("url") + } + return result + } - private fun IntObjectMap.keyOf(value: V): Int { - forEach { k, v -> - if (v == value) { - return k - } - } - throw NoSuchElementException("No key associated with value $value") - } + private fun IntObjectMap.keyOf(value: V): Int { + forEach { k, v -> + if (v == value) { + return k + } + } + throw NoSuchElementException("No key associated with value $value") + } - private fun JSONArray.toIntSet(): IntSet { - val result = MutableIntSet(length()) - for (i in 0 until length()) { - result.add(getInt(i)) - } - return result - } + private fun JSONArray.toIntSet(): IntSet { + val result = MutableIntSet(length()) + for (i in 0 until length()) { + result.add(getInt(i)) + } + return result + } - private fun MangaTag.typeKey() = when (key.firstOrNull()) { - 'g' -> "genres" - 't' -> "tags" - else -> throw IllegalArgumentException("Tag $key($title) is of unknown type") - } + private fun MangaTag.typeKey() = when (key.firstOrNull()) { + 'g' -> "genres" + 't' -> "tags" + else -> throw IllegalArgumentException("Tag $key($title) is of unknown type") + } - private fun tagsSetOf(tags: Collection, genres: Collection): Set { - val result = ArraySet(tags.size + genres.size) - val names = HashSet(tags.size + genres.size) - genres.forEach { x -> if (names.add(x.title)) result.add(x) } - tags.forEach { x -> if (names.add(x.title)) result.add(x) } - return result - } + private fun tagsSetOf(tags: Collection, genres: Collection): Set { + val result = ArraySet(tags.size + genres.size) + val names = HashSet(tags.size + genres.size) + genres.forEach { x -> if (names.add(x.title)) result.add(x) } + tags.forEach { x -> if (names.add(x.title)) result.add(x) } + return result + } - protected companion object { + protected companion object { - const val SERVER_MAIN = "main" - const val SERVER_SECONDARY = "secondary" - const val SERVER_COMPRESS = "compress" - const val SERVER_DOWNLOAD = "download" - const val SERVER_CROP = "crop" - } + const val SERVER_MAIN = "main" + const val SERVER_SECONDARY = "secondary" + const val SERVER_COMPRESS = "compress" + const val SERVER_DOWNLOAD = "download" + const val SERVER_CROP = "crop" + } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/scan/ScanParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/scan/ScanParser.kt index d12402b6..319ce521 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/scan/ScanParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/scan/ScanParser.kt @@ -148,7 +148,7 @@ internal abstract class ScanParser( ?.ownText()?.toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN, tags = tags, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), altTitles = setOfNotNull(doc.selectFirst(".card div.col-12.mb-4 h2, .card-series-about .h6")?.textOrNull()), description = doc.selectFirst(".card div.col-12.mb-4 p, .card-series-desc .mb-4 p")?.html(), chapters = doc.select(".chapters-list .col-chapter, .card-list-chapter .col-chapter") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/scan/fr/MangaFr.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/scan/fr/MangaFr.kt index c3322ea8..e6aeebcf 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/scan/fr/MangaFr.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/scan/fr/MangaFr.kt @@ -27,7 +27,7 @@ internal class MangaFr(context: MangaLoaderContext) : ?.ownText()?.toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN, tags = emptySet(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), altTitles = setOfNotNull(doc.selectFirst(".card div.col-12.mb-4 h2, .card-series-about .h6")?.textOrNull()), description = doc.selectFirst(".card div.col-12.mb-4 p, .card-series-desc .mb-4 p")?.html(), chapters = doc.select(".chapters-list .col-chapter, .card-list-chapter .col-chapter") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/scan/it/ScanIta.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/scan/it/ScanIta.kt index 675330f7..7e23374f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/scan/it/ScanIta.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/scan/it/ScanIta.kt @@ -28,7 +28,7 @@ internal class ScanIta(context: MangaLoaderContext) : rating = doc.selectFirst(".card-series-detail .rate-value span")?.ownText()?.toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN, tags = tags, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), altTitles = setOfNotNull(doc.selectFirst(".card div.col-12.mb-4 h2")?.textOrNull()), description = doc.selectFirst(".card div.col-12.mb-4 p")?.html(), chapters = chaptersDeferred.await(), 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 ade39c01..86723f1d 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 @@ -72,7 +72,7 @@ internal class SadScans(context: MangaLoaderContext) : else -> null }, tags = emptySet(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = doc.selectFirstOrThrow(".summary").html(), chapters = doc.select(".chap-section .chap") .mapChapters(reversed = true) { i, div -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/HentaiUkrParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/HentaiUkrParser.kt index 818fd5f7..e087d24d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/HentaiUkrParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/HentaiUkrParser.kt @@ -26,173 +26,173 @@ private const val PAGE_SIZE = 60 // NOTE High profile focus @MangaSourceParser("HENTAIUKR", "HentaiUkr", "uk", ContentType.HENTAI) internal class HentaiUkrParser(context: MangaLoaderContext) : LegacyMangaParser(context, MangaParserSource.HENTAIUKR), - Interceptor { - - private val date = SimpleDateFormat("yyyy-MM-dd", Locale.US) - - private val allManga = suspendLazy(soft = true) { - runCatchingCancellable { - webClient.httpGet("https://$domain/search/objects.json").parseJson() - }.recoverCatchingCancellable { - webClient.httpGet("https://$domain/search/objects2.json").parseJson() - }.recoverCatchingCancellable { - webClient.httpGet("https://$domain/search/objects69.json").parseJson() - }.getOrThrow().getJSONArray("manga").asTypedList() - } - - override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("hentaiukr.com") - - override val filterCapabilities: MangaListFilterCapabilities - get() = MangaListFilterCapabilities( - isMultipleTagsSupported = true, - isSearchSupported = true, - isSearchWithFiltersSupported = true, - ) - - override suspend fun getFilterOptions() = MangaListFilterOptions( - availableTags = fetchAvailableTags(), - ) - - override fun onCreateConfig(keys: MutableCollection>) { - super.onCreateConfig(keys) - keys.add(userAgentKey) - } - - override val availableSortOrders: Set = EnumSet.of( - SortOrder.NEWEST, - ) - - override suspend fun getDetails(manga: Manga): Manga = coroutineScope { - val jsonDeferred = async { allManga.get().first { it.getString("url") == manga.url } } - val htmlDeferred = async { webClient.httpGet("https://$domain${manga.url}").parseHtml() } - - val about = htmlDeferred.await().body().requireElementById("about").text() - - manga.copy( - description = about, - chapters = listOf( - MangaChapter( - id = generateUid(manga.id), - name = manga.title, - number = 1f, - volume = 0, - url = manga.url, - scanlator = null, - uploadDate = date.tryParse(jsonDeferred.await().getString("add_date")), - branch = null, - source = source, - ), - ), - ) - } - - override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { - // Get all manga - val json = allManga.get().toMutableList() - - if (!filter.query.isNullOrEmpty()) { - json.retainAll { item -> - item.getString("name").contains(filter.query, ignoreCase = true) || - item.getStringOrNull("eng_name")?.contains(filter.query, ignoreCase = true) == true || - item.getStringOrNull("orig_name")?.contains(filter.query, ignoreCase = true) == true || - item.getStringOrNull("author")?.contains(filter.query, ignoreCase = true) == true || - item.getStringOrNull("team")?.contains(filter.query, ignoreCase = true) == true - } - } - if (filter.tags.isNotEmpty()) { - val ids = filter.tags.mapToSet { it.key } - json.retainAll { item -> - item.getJSONArray("tags") - .mapJSON { it.getAsString() } - .any { x -> x in ids } - } - } - // Return to app - return json.drop(offset).take(PAGE_SIZE).map { jo -> - val id = jo.getAsLong() - val author = jo.getStringOrNull("author") - Manga( - id = generateUid(id), - title = jo.getString("name"), - altTitles = setOfNotNull(jo.getStringOrNull("eng_name")), - url = jo.getString("url"), - publicUrl = jo.getString("url").toAbsoluteUrl(domain), - rating = RATING_UNKNOWN, - contentRating = ContentRating.ADULT, - coverUrl = jo.getString("thumb").toAbsoluteUrl(domain), - tags = getTags(jo.optJSONArray("tags")), - state = null, - authors = author?.let { setOf(it) } ?: emptySet(), - largeCoverUrl = null, - description = null, - chapters = null, - source = source, - ) - } - } - - override suspend fun getPages(chapter: MangaChapter): List { - val htmlPages = webClient.httpGet("https://$domain${chapter.url}vertical_reader.html").parseHtml() - return htmlPages.select("img.image").mapIndexed { i, page -> - MangaPage( - id = generateUid(i.toString()), - "https://$domain${page.attr("src")}", - null, - source, - ) - } - } - - private suspend fun fetchAvailableTags(): Set { - return allManga.get().flatMapTo(HashSet()) { x -> - x.getJSONArray("tags").mapJSON { t -> - MangaTag( - title = t.getString("name"), - key = t.getAsString(), - source = source, - ) - } - } - } - - private fun getTags(jsonTags: JSONArray): Set { - val tagsSet = ArraySet(jsonTags.length()) - repeat(jsonTags.length()) { i -> - val item = jsonTags.getJSONObject(i) - tagsSet.add( - MangaTag( - title = item.getString("name"), - key = item.getAsString(), - source = source, - ), - ) - } - return tagsSet - } - - // Need for disable encoding (with encoding not working) - override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request() - val newRequest = if (request.header(HEADER_ENCODING) != null) { - request.newBuilder().removeHeader(HEADER_ENCODING).build() - } else { - request - } - return chain.proceed(newRequest) - } - - private fun JSONObject.getAsLong(): Long { - val rawValue = opt("id") - return when (rawValue) { - null, JSONObject.NULL -> null - is Long -> rawValue - is Number -> rawValue.toLong() - is String -> rawValue.toLong() - else -> null - } ?: error("Cannot read value $rawValue as Long") - } - - private fun JSONObject.getAsString(): String { - return get("id").toString() - } + Interceptor { + + private val date = SimpleDateFormat("yyyy-MM-dd", Locale.US) + + private val allManga = suspendLazy(soft = true) { + runCatchingCancellable { + webClient.httpGet("https://$domain/search/objects.json").parseJson() + }.recoverCatchingCancellable { + webClient.httpGet("https://$domain/search/objects2.json").parseJson() + }.recoverCatchingCancellable { + webClient.httpGet("https://$domain/search/objects69.json").parseJson() + }.getOrThrow().getJSONArray("manga").asTypedList() + } + + override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("hentaiukr.com") + + override val filterCapabilities: MangaListFilterCapabilities + get() = MangaListFilterCapabilities( + isMultipleTagsSupported = true, + isSearchSupported = true, + isSearchWithFiltersSupported = true, + ) + + override suspend fun getFilterOptions() = MangaListFilterOptions( + availableTags = fetchAvailableTags(), + ) + + override fun onCreateConfig(keys: MutableCollection>) { + super.onCreateConfig(keys) + keys.add(userAgentKey) + } + + override val availableSortOrders: Set = EnumSet.of( + SortOrder.NEWEST, + ) + + override suspend fun getDetails(manga: Manga): Manga = coroutineScope { + val jsonDeferred = async { allManga.get().first { it.getString("url") == manga.url } } + val htmlDeferred = async { webClient.httpGet("https://$domain${manga.url}").parseHtml() } + + val about = htmlDeferred.await().body().requireElementById("about").text() + + manga.copy( + description = about, + chapters = listOf( + MangaChapter( + id = generateUid(manga.id), + name = manga.title, + number = 1f, + volume = 0, + url = manga.url, + scanlator = null, + uploadDate = date.tryParse(jsonDeferred.await().getString("add_date")), + branch = null, + source = source, + ), + ), + ) + } + + override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { + // Get all manga + val json = allManga.get().toMutableList() + + if (!filter.query.isNullOrEmpty()) { + json.retainAll { item -> + item.getString("name").contains(filter.query, ignoreCase = true) || + item.getStringOrNull("eng_name")?.contains(filter.query, ignoreCase = true) == true || + item.getStringOrNull("orig_name")?.contains(filter.query, ignoreCase = true) == true || + item.getStringOrNull("author")?.contains(filter.query, ignoreCase = true) == true || + item.getStringOrNull("team")?.contains(filter.query, ignoreCase = true) == true + } + } + if (filter.tags.isNotEmpty()) { + val ids = filter.tags.mapToSet { it.key } + json.retainAll { item -> + item.getJSONArray("tags") + .mapJSON { it.getAsString() } + .any { x -> x in ids } + } + } + // Return to app + return json.drop(offset).take(PAGE_SIZE).map { jo -> + val id = jo.getAsLong() + val author = jo.getStringOrNull("author") + Manga( + id = generateUid(id), + title = jo.getString("name"), + altTitles = setOfNotNull(jo.getStringOrNull("eng_name")), + url = jo.getString("url"), + publicUrl = jo.getString("url").toAbsoluteUrl(domain), + rating = RATING_UNKNOWN, + contentRating = ContentRating.ADULT, + coverUrl = jo.getString("thumb").toAbsoluteUrl(domain), + tags = getTags(jo.optJSONArray("tags")), + state = null, + authors = setOfNotNull(author), + largeCoverUrl = null, + description = null, + chapters = null, + source = source, + ) + } + } + + override suspend fun getPages(chapter: MangaChapter): List { + val htmlPages = webClient.httpGet("https://$domain${chapter.url}vertical_reader.html").parseHtml() + return htmlPages.select("img.image").mapIndexed { i, page -> + MangaPage( + id = generateUid(i.toString()), + "https://$domain${page.attr("src")}", + null, + source, + ) + } + } + + private suspend fun fetchAvailableTags(): Set { + return allManga.get().flatMapTo(HashSet()) { x -> + x.getJSONArray("tags").mapJSON { t -> + MangaTag( + title = t.getString("name"), + key = t.getAsString(), + source = source, + ) + } + } + } + + private fun getTags(jsonTags: JSONArray): Set { + val tagsSet = ArraySet(jsonTags.length()) + repeat(jsonTags.length()) { i -> + val item = jsonTags.getJSONObject(i) + tagsSet.add( + MangaTag( + title = item.getString("name"), + key = item.getAsString(), + source = source, + ), + ) + } + return tagsSet + } + + // Need for disable encoding (with encoding not working) + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val newRequest = if (request.header(HEADER_ENCODING) != null) { + request.newBuilder().removeHeader(HEADER_ENCODING).build() + } else { + request + } + return chain.proceed(newRequest) + } + + private fun JSONObject.getAsLong(): Long { + val rawValue = opt("id") + return when (rawValue) { + null, JSONObject.NULL -> null + is Long -> rawValue + is Number -> rawValue.toLong() + is String -> rawValue.toLong() + else -> null + } ?: error("Cannot read value $rawValue as Long") + } + + private fun JSONObject.getAsString(): String { + return get("id").toString() + } } 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 affcaa3e..786ab60c 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 @@ -83,7 +83,7 @@ internal 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")?.src().orEmpty(), + coverUrl = mangaInfo.selectFirst("div > img.img")?.src(), contentRating = null, rating = RATING_UNKNOWN, tags = emptySet(), @@ -129,7 +129,7 @@ internal class BlogTruyenParser(context: MangaLoaderContext) : return manga.copy( tags = tags, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = doc.selectFirst(".detail .content")?.html(), chapters = parseChapterList(doc), largeCoverUrl = doc.selectLast("div.thumbnail > img")?.src(), diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BlogTruyenVN.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BlogTruyenVN.kt index 62ebbf7a..3eed2de1 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BlogTruyenVN.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BlogTruyenVN.kt @@ -116,10 +116,10 @@ internal class BlogTruyenVN(context: MangaLoaderContext) : id = generateUid(relativeUrl), title = a.text(), altTitles = emptySet(), - description = mangaInfo.select("div.al-j.fs-12").text(), + description = mangaInfo.select("div.al-j.fs-12").textOrNull(), url = relativeUrl, publicUrl = relativeUrl.toAbsoluteUrl(domain), - coverUrl = mangaInfo.selectFirst("div > img.img")?.src().orEmpty(), + coverUrl = mangaInfo.selectFirst("div > img.img")?.src(), contentRating = null, rating = RATING_UNKNOWN, tags = emptySet(), @@ -181,7 +181,7 @@ internal class BlogTruyenVN(context: MangaLoaderContext) : return manga.copy( tags = tags ?: emptySet(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = doc.selectFirst(".detail .content")?.html(), chapters = parseChapterList(doc), largeCoverUrl = doc.selectLast("div.thumbnail > img")?.src(), diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BuonDuaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BuonDuaParser.kt index 14f79bfd..dc47cd94 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BuonDuaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BuonDuaParser.kt @@ -32,7 +32,7 @@ internal class BuonDuaParser(context: MangaLoaderContext) : LegacyMangaParser(co val df = SimpleDateFormat("HH:mm dd-MM-yyyy") val time = content.selectFirst("div.article-info > small")?.text()?.trim() val chapters = content.selectFirst("nav.pagination")?.select("a.pagination-link") - ?.mapIndexed { index, element -> + ?.mapChapters { index, element -> val relUrl = element.attrAsRelativeUrl("href") MangaChapter( id = generateUid(relUrl), diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CMangaParser.kt index cbcc928d..a5b5b8c3 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CMangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CMangaParser.kt @@ -22,8 +22,7 @@ private const val PAGE_SIZE = 20 internal class CMangaParser(context: MangaLoaderContext) : LegacyPagedMangaParser(context, MangaParserSource.CMANGA, PAGE_SIZE), MangaParserAuthProvider { - override val configKeyDomain: ConfigKey.Domain - get() = ConfigKey.Domain("cmangax.com") + override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("cmangax.com") override val availableSortOrders: Set get() = EnumSet.of( @@ -47,7 +46,7 @@ internal class CMangaParser(context: MangaLoaderContext) : override suspend fun getFilterOptions(): MangaListFilterOptions { return MangaListFilterOptions( - availableTags = tags.get().values.toSet(), + availableTags = tags.get().values.toArraySet(), availableStates = arraySetOf(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED), ) } @@ -80,14 +79,14 @@ internal class CMangaParser(context: MangaLoaderContext) : chapters = webClient .httpGet("/api/chapter_list?album=$mangaId&page=1&limit=${Int.MAX_VALUE}&v=0v21".toAbsoluteUrl(domain)) .parseJsonArray() - .mapJSON { jo -> + .mapChapters(reversed = true) { _, jo -> val chapterId = jo.getLong("id_chapter") val info = jo.parseJson("info") - val chapterNumber = info.getString("num") + val chapterNumber = info.getFloatOrDefault("num", -1f) + 1f MangaChapter( id = generateUid(chapterId), name = if (info.isLocked()) "Chapter $chapterNumber - locked" else "Chapter $chapterNumber", - number = chapterNumber.toFloatOrNull()?.plus(1) ?: 0f, + number = chapterNumber, volume = 0, url = "/album/$slug/chapter-$mangaId-$chapterId", uploadDate = df.tryParse(info.getString("last_update")), @@ -95,7 +94,7 @@ internal class CMangaParser(context: MangaLoaderContext) : scanlator = null, source = source, ) - }.reversed(), + }, ) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CuuTruyenParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CuuTruyenParser.kt index 06dde7a2..c55e1af4 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CuuTruyenParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CuuTruyenParser.kt @@ -24,7 +24,7 @@ import java.util.* @MangaSourceParser("CUUTRUYEN", "Cứu Truyện", "vi") internal class CuuTruyenParser(context: MangaLoaderContext) : - LegacyPagedMangaParser(context, MangaParserSource.CUUTRUYEN, 20), Interceptor { + LegacyPagedMangaParser(context, MangaParserSource.CUUTRUYEN, 20) { override val userAgentKey = ConfigKey.UserAgent(UserAgents.KOTATSU) @@ -115,7 +115,7 @@ internal class CuuTruyenParser(context: MangaLoaderContext) : altTitles = emptySet(), coverUrl = jo.getString("cover_mobile_url"), largeCoverUrl = jo.getString("cover_url"), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), tags = emptySet(), state = null, description = null, @@ -153,19 +153,21 @@ internal class CuuTruyenParser(context: MangaLoaderContext) : // Remove old manga status from "tags" val newTags = tags.filter { it.key != "da-hoan-thanh" && it.key != "dang-tien-hanh" }.toSet() val author = json.optJSONObject("author")?.getStringOrNull("name")?.substringBefore(',')?.nullIfEmpty() + val title = json.getStringOrNull("name") ?: manga.title manga.copy( - title = json.getStringOrNull("name") ?: manga.title, + title = title, + altTitles = json.optJSONArray("titles")?.mapJSONToSet { it.getString("name") }?.minus(title).orEmpty(), contentRating = if (json.getBooleanOrDefault("is_nsfw", manga.isNsfw)) { ContentRating.ADULT } else { ContentRating.SAFE }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = json.getStringOrNull("full_description"), tags = newTags, state = state, - chapters = chapters.await().mapJSON { jo -> + chapters = chapters.await().mapChapters(reversed = true) { _, jo -> val chapterId = jo.getLong("id") val number = jo.getFloatOrDefault("number", 0f) MangaChapter( @@ -179,7 +181,7 @@ internal class CuuTruyenParser(context: MangaLoaderContext) : branch = null, source = source, ) - }.reversed(), + }, ) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/DuaLeoTruyen.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/DuaLeoTruyen.kt index f5d1e9e4..d512cf5c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/DuaLeoTruyen.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/DuaLeoTruyen.kt @@ -105,7 +105,7 @@ internal class DuaLeoTruyen(context: MangaLoaderContext) : "Full" -> MangaState.FINISHED else -> null }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = doc.selectFirst(".story-detail-info")?.html(), chapters = doc.select(".list-chapters .chapter-item").mapChapters(reversed = true) { i, div -> val a = div.selectFirstOrThrow(".chap_name a") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/Hentai18VN.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/Hentai18VN.kt index a2f0d022..031b1fb7 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/Hentai18VN.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/Hentai18VN.kt @@ -94,7 +94,7 @@ internal class Hentai18VN(context: MangaLoaderContext) : private fun parseMangaSearch(doc: Document): List { return doc.select("a.item").map { a -> val href = a.attr("href") - val mangaInfo = a.selectFirst("img") + val mangaInfo = a.selectFirstOrThrow("img") Manga( id = generateUid(href), url = href, @@ -105,7 +105,7 @@ internal class Hentai18VN(context: MangaLoaderContext) : tags = emptySet(), rating = RATING_UNKNOWN, state = null, - coverUrl = mangaInfo.requireSrc(), + coverUrl = mangaInfo.src(), contentRating = ContentRating.ADULT, source = source, ) @@ -138,13 +138,13 @@ internal class Hentai18VN(context: MangaLoaderContext) : override suspend fun getDetails(manga: Manga): Manga { val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() val tags = doc.select("div.hentai-info .line-content a.item-tag") - .mapNotNull { a -> + .mapToSet { a -> MangaTag( - title = a.text(), - key = a.attr("href").substringAfterLast("/"), + title = a.text().toTitleCase(sourceLocale), + key = a.attr("href").substringAfterLast('/'), source = source, ) - }.toSet() + } val chapters = doc.select("ul#chapter-list li.citem").mapChapters(reversed = true) { i, li -> val a = li.selectFirst("a") ?: return@mapChapters null @@ -152,7 +152,7 @@ internal class Hentai18VN(context: MangaLoaderContext) : id = generateUid(a.attr("href")), name = a.text(), number = i + 1f, - url = a.attr("href").removePrefix("https://$domain"), + url = a.attrAsRelativeUrl("href"), uploadDate = parseChapterDate(li.selectFirst(".time")?.text()), source = source, scanlator = null, 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 6a8b41b1..670d3a6a 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 @@ -115,7 +115,7 @@ internal class HentaiVNParser(context: MangaLoaderContext) : LegacyMangaParser(c altTitles = infoEl.selectFirst("span.info:contains(Tên Khác:)")?.parent()?.select("span:not(.info) > a") ?.mapNotNullToSet { it.textOrNull() } .orEmpty(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = infoEl.select("p:contains(Nội dung:) + p").html(), tags = tags, state = stateDoc.select("p:contains(Tình Trạng:) a").firstOrNull()?.text()?.let { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVnBuzz.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVnBuzz.kt index f1a3f253..4490b7cf 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVnBuzz.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVnBuzz.kt @@ -118,7 +118,7 @@ internal class HentaiVnBuzz(context: MangaLoaderContext) : private fun parseSearchManga(doc: Document): List { return doc.select(".story-item-list.d-flex.align-items-center.position-relative.mb-1").map { div -> val href = div.selectFirstOrThrow("a.story-item-list__image").attrAsRelativeUrl("href") - val coverUrl = div.selectFirst("img")?.attr("data-src").orEmpty() + val coverUrl = div.selectFirst("img")?.attr("data-src") val title = div.selectFirst("img")?.attr("alt").orEmpty() Manga( id = generateUid(href), @@ -163,11 +163,11 @@ internal class HentaiVnBuzz(context: MangaLoaderContext) : val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() val author = doc.select("p:contains(Tác giả:) a").text().nullIfEmpty() return manga.copy( - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), tags = doc.select("div.mb-1 span a").mapToSet { element -> MangaTag( key = element.attr("href").substringAfter("/the-loai/"), - title = element.text().substringBefore(",").trim(), // force trim before , symbol and space + title = element.text().substringBefore(',').trim(), // force trim before , symbol and space source = source, ) }, diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/KuroNeko.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/KuroNeko.kt index 376f45f1..81bcb551 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/KuroNeko.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/KuroNeko.kt @@ -162,7 +162,7 @@ internal class KuroNeko(context: MangaLoaderContext) : LegacyPagedMangaParser(co source = source, ) }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = root.selectFirst("meta[name=description]")?.attrOrNull("content"), chapters = root.select("div.justify-between ul.overflow-y-auto.overflow-x-hidden a") .mapChapters(reversed = true) { i, a -> 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 317079b4..7fef668b 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 @@ -163,7 +163,7 @@ internal class LxManga(context: MangaLoaderContext) : LegacyPagedMangaParser(con source = source, ) }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = root.selectFirst("meta[name=description]")?.attrOrNull("content"), chapters = root.select("div.justify-between ul.overflow-y-auto.overflow-x-hidden a") .mapChapters(reversed = true) { i, a -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/SayHentai.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/SayHentai.kt index 2dbbf09c..b3d5ca33 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/SayHentai.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/SayHentai.kt @@ -90,7 +90,7 @@ internal class SayHentai(context: MangaLoaderContext) : val author = doc.selectFirst("div.summary-heading:contains(Tác giả) + div.summary-content")?.textOrNull() return manga.copy( altTitles = setOfNotNull(doc.selectFirst("h2.other-name")?.textOrNull()), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), tags = doc.select("div.genres-content a[rel=tag]").mapToSet { a -> MangaTag( key = a.attr("href").substringAfterLast('/'), @@ -178,7 +178,7 @@ internal class SayHentai(context: MangaLoaderContext) : .mapToSet { a -> val title = a.ownText().toTitleCase(sourceLocale) MangaTag( - key = a.attr("href").substringAfterLast("/"), + key = a.attr("href").substringAfterLast('/'), title = title, source = source, ) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenGG.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenGG.kt index 4b637f2b..145cde6a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenGG.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenGG.kt @@ -127,7 +127,7 @@ internal class TruyenGG(context: MangaLoaderContext) : LegacyPagedMangaParser(co publicUrl = href.toAbsoluteUrl(domain), rating = RATING_UNKNOWN, contentRating = if (isNsfwSource) ContentRating.ADULT else null, - coverUrl = div.selectFirst(".image-cover img")?.attr("data-src").orEmpty(), + coverUrl = div.selectFirst(".image-cover img")?.attrAsAbsoluteUrlOrNull("data-src"), tags = emptySet(), state = null, authors = emptySet(), @@ -143,11 +143,11 @@ internal class TruyenGG(context: MangaLoaderContext) : LegacyPagedMangaParser(co return manga.copy( altTitles = setOfNotNull(doc.selectFirst("h2.other-name")?.textOrNull()), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), tags = doc.select("a.clblue").mapToSet { MangaTag( key = it.attr("href").substringAfterLast('-').substringBeforeLast('.'), - title = it.text(), + title = it.text().toTitleCase(sourceLocale), source = source, ) }, @@ -196,7 +196,7 @@ internal class TruyenGG(context: MangaLoaderContext) : LegacyPagedMangaParser(co return doc.select(".advsearch-form div.genre-item").mapToSet { MangaTag( key = it.selectFirstOrThrow("span").attr("data-id"), - title = it.text(), + title = it.text().toTitleCase(sourceLocale), source = source, ) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenHentaiVN.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenHentaiVN.kt index e611f350..5935c2c0 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenHentaiVN.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenHentaiVN.kt @@ -94,6 +94,7 @@ internal class TruyenHentaiVN(context: MangaLoaderContext) : override suspend fun getDetails(manga: Manga): Manga { val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() + val dateFormat = SimpleDateFormat("dd-MM-yyyy", Locale.US) return manga.copy( authors = setOfNotNull(doc.selectFirst("div.author i")?.textOrNull()), tags = doc.select("div.genre.mb-3.mgen a").mapNotNullToSet { a -> @@ -121,13 +122,7 @@ internal class TruyenHentaiVN(context: MangaLoaderContext) : val name = div.selectFirst("a .name")?.text() ?: "" val dateStr = div.selectFirst("a span:last-child")?.text() - val uploadDate = dateStr?.let { - try { - SimpleDateFormat("dd-MM-yyyy", Locale.US).parse(it)?.time ?: 0L - } catch (e: Exception) { - 0L - } - } ?: 0L + val uploadDate = dateFormat.tryParse(dateStr) MangaChapter( id = generateUid(url), @@ -147,15 +142,13 @@ internal class TruyenHentaiVN(context: MangaLoaderContext) : override suspend fun getPages(chapter: MangaChapter): List { val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml() return doc.select("div.content-text img").mapNotNull { img -> - val url = img.requireSrc().toAbsoluteUrl(domain) - if (url.isNotEmpty()) { - MangaPage( - id = generateUid(url), - url = url, - preview = null, - source = source, - ) - } else null + val url = img.src() ?: return@mapNotNull null + MangaPage( + id = generateUid(url), + url = url, + preview = null, + source = source, + ) } } 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 7acf2dc1..2e2baab7 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 @@ -166,7 +166,7 @@ internal class TruyenQQ(context: MangaLoaderContext) : LegacyPagedMangaParser(co "Hoàn Thành" -> MangaState.FINISHED else -> null }, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = doc.selectFirst(".story-detail-info")?.html(), chapters = doc.select("div.list_chapter div.works-chapter-item").mapChapters(reversed = true) { i, div -> val a = div.selectFirstOrThrow("a") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenTranh3Q.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenTranh3Q.kt index 3af44382..56b5b830 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenTranh3Q.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenTranh3Q.kt @@ -147,7 +147,7 @@ internal class TruyenTranh3Q(context: MangaLoaderContext) : return manga.copy( altTitles = setOfNotNull(doc.selectFirst("h2.other-name")?.textOrNull()), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), tags = tags, description = doc.selectFirst("div.story-detail-info")?.html(), state = when (doc.selectFirst(".status p.col-xs-9")?.text()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/VcomycsParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/VcomycsParser.kt index 9547e355..615bcea6 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/VcomycsParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/VcomycsParser.kt @@ -134,7 +134,7 @@ internal class VcomycsParser(context: MangaLoaderContext) : info.selectFirst(".comic-intro-text > strong:contains(Tên khác:)")?.nextElementSibling() ?.textOrNull(), ), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = when (info.selectFirst(".comic-stt")?.text()) { "Đang tiến hành" -> MangaState.ONGOING "Trọn bộ" -> MangaState.FINISHED 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 cd6618a1..233990ab 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 @@ -101,7 +101,6 @@ internal class YurinekoParser(context: MangaLoaderContext) : val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US) return manga.copy( chapters = response.getJSONArray("chapters") - .asTypedList() .mapChapters(true) { i, jo -> val mangaId = jo.getInt("mangaID") val chapterId = jo.getInt("id") 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 39305bd3..a98b2b1c 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 @@ -168,7 +168,7 @@ internal abstract class WpComicsParser( largeCoverUrl = null, tags = mangaTags, state = mangaState, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = tooltipElement?.selectFirst("div.box_text")?.text(), chapters = null, source = source, @@ -220,7 +220,7 @@ internal abstract class WpComicsParser( manga.copy( description = doc.selectFirst(selectDesc)?.html(), altTitles = setOfNotNull(doc.selectFirst("h2.other-name")?.textOrNull()), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = doc.selectFirst(selectState)?.let { when (it.text()) { in ongoing -> MangaState.ONGOING 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 fe5aa627..1e33118c 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 @@ -138,7 +138,7 @@ internal class XoxoComics(context: MangaLoaderContext) : ) }, description = desc, - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = state, chapters = chaptersDeferred.await(), ) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/DocTruyen3Q.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/DocTruyen3Q.kt index ac9f3fc7..cb049cd1 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/DocTruyen3Q.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/DocTruyen3Q.kt @@ -147,7 +147,7 @@ internal class DocTruyen3Q(context: MangaLoaderContext) : } return manga.copy( - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = description, state = state, tags = tags, diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/HamTruyen.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/HamTruyen.kt index f011f424..71d1b001 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/HamTruyen.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/HamTruyen.kt @@ -29,7 +29,7 @@ internal class HamTruyen(context: MangaLoaderContext) : manga.copy( description = doc.selectFirst(selectDesc)?.html(), altTitles = setOfNotNull(doc.selectFirst("h2.other-name")?.textOrNull()), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = doc.selectFirst(selectState)?.let { when (it.text()) { in ongoing -> MangaState.ONGOING diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/MeHentaiVN.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/MeHentaiVN.kt index 0d15fc8c..a2764862 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/MeHentaiVN.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/MeHentaiVN.kt @@ -42,7 +42,7 @@ internal class MeHentaiVN(context: MangaLoaderContext) : manga.copy( description = doc.selectFirst(selectDesc)?.html(), altTitles = setOfNotNull(doc.selectFirst("h2.other-name")?.textOrNull()), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = doc.selectFirst(selectState)?.let { when (it.text()) { in ongoing -> MangaState.ONGOING 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 index 5e5215d0..098cc690 100644 --- 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 @@ -31,7 +31,7 @@ internal class NetTruyen(context: MangaLoaderContext) : manga.copy( description = doc.selectFirst(selectDesc)?.html(), altTitles = setOfNotNull(doc.selectFirst("h2.other-name")?.textOrNull()), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = doc.selectFirst(selectState)?.let { when (it.text()) { in ongoing -> MangaState.ONGOING diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenVie.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenVie.kt index 0c8a9e08..a261693d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenVie.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenVie.kt @@ -25,7 +25,7 @@ internal class NetTruyenVie(context: MangaLoaderContext) : manga.copy( description = doc.selectFirst("div.detail-content > div")?.html(), altTitles = setOfNotNull(doc.selectFirst("h2.other-name")?.textOrNull()), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = doc.selectFirst(selectState)?.let { when (it.text()) { in ongoing -> MangaState.ONGOING diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NewTruyen.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NewTruyen.kt index e1b2a651..10493a08 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NewTruyen.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NewTruyen.kt @@ -39,7 +39,7 @@ internal class NewTruyen(context: MangaLoaderContext) : manga.copy( description = doc.selectFirst(selectDesc)?.html(), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = doc.selectFirst(selectState)?.let { when (it.text()) { in ongoing -> MangaState.ONGOING diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NhatTruyenVN.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NhatTruyenVN.kt index f500811d..3b6b069c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NhatTruyenVN.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NhatTruyenVN.kt @@ -14,105 +14,105 @@ import java.util.* @MangaSourceParser("NHATTRUYENVN", "NhatTruyenVN", "vi") internal class NhatTruyenVN(context: MangaLoaderContext) : - WpComicsParser(context, MangaParserSource.NHATTRUYENVN, "nhattruyenv.com", 36) { + WpComicsParser(context, MangaParserSource.NHATTRUYENVN, "nhattruyenv.com", 36) { - override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("nhattruyenv.com", "www.nhattruyenss.net") + override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("nhattruyenv.com", "www.nhattruyenss.net") - private val selectChap = "ul#asc li.row:not(.heading)" + private val selectChap = "ul#asc li.row:not(.heading)" - private fun getChaps(doc: Document): List { - return doc.body().select(selectChap).mapChapters(reversed = false) { i, li -> - val a = li.selectFirstOrThrow("a") - val href = a.attrAsRelativeUrl("href") - val chapterNumber = a.text().substringAfter("Chapter ").substringBefore(" ").toFloatOrNull() ?: (i + 1f) - val dateText = li.selectFirst(selectDate)?.text() - MangaChapter( - id = generateUid(href), - name = a.text(), - number = chapterNumber, - volume = 0, - url = href, - uploadDate = parseChapterDate(dateText), - source = source, - scanlator = null, - branch = null, - ) - } - } + private fun getChaps(doc: Document): List { + return doc.body().select(selectChap).mapChapters(reversed = false) { i, li -> + val a = li.selectFirstOrThrow("a") + val href = a.attrAsRelativeUrl("href") + val chapterNumber = a.text().substringAfter("Chapter ").substringBefore(" ").toFloatOrNull() ?: (i + 1f) + val dateText = li.selectFirst(selectDate)?.text() + MangaChapter( + id = generateUid(href), + name = a.text(), + number = chapterNumber, + volume = 0, + url = href, + uploadDate = parseChapterDate(dateText), + source = source, + scanlator = null, + branch = null, + ) + } + } - override suspend fun getDetails(manga: Manga): Manga = coroutineScope { - val fullUrl = manga.url.toAbsoluteUrl(domain) - val doc = webClient.httpGet(fullUrl).parseHtml() - val chaptersDeferred = async { getChaps(doc) } - val tagMap = getOrCreateTagMap() - val tagsElement = doc.select("li.kind p.col-xs-8 a") - val mangaTags = tagsElement.mapNotNullToSet { tagMap[it.text()] } - val author = doc.body().selectFirst(selectAut)?.textOrNull() - manga.copy( - description = doc.selectFirst(selectDesc)?.html(), - altTitles = setOfNotNull(doc.selectFirst("h2.other-name")?.textOrNull()), - authors = author?.let { setOf(it) } ?: emptySet(), - state = doc.selectFirst(selectState)?.let { - when (it.text()) { - in ongoing -> MangaState.ONGOING - in finished -> MangaState.FINISHED - else -> null - } - }, - tags = mangaTags, - rating = doc.selectFirst("div.star input")?.attr("value")?.toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN, - chapters = chaptersDeferred.await(), - ) - } + override suspend fun getDetails(manga: Manga): Manga = coroutineScope { + val fullUrl = manga.url.toAbsoluteUrl(domain) + val doc = webClient.httpGet(fullUrl).parseHtml() + val chaptersDeferred = async { getChaps(doc) } + val tagMap = getOrCreateTagMap() + val tagsElement = doc.select("li.kind p.col-xs-8 a") + val mangaTags = tagsElement.mapNotNullToSet { tagMap[it.text()] } + val author = doc.body().selectFirst(selectAut)?.textOrNull() + manga.copy( + description = doc.selectFirst(selectDesc)?.html(), + altTitles = setOfNotNull(doc.selectFirst("h2.other-name")?.textOrNull()), + authors = setOfNotNull(author), + state = doc.selectFirst(selectState)?.let { + when (it.text()) { + in ongoing -> MangaState.ONGOING + in finished -> MangaState.FINISHED + else -> null + } + }, + tags = mangaTags, + rating = doc.selectFirst("div.star input")?.attr("value")?.toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN, + chapters = chaptersDeferred.await(), + ) + } - private fun parseChapterDate(dateText: String?): Long { - if (dateText == null) return 0 + private fun parseChapterDate(dateText: String?): Long { + if (dateText == null) return 0 - val relativeTimePattern = Regex("(\\d+)\\s*(phút|giờ|ngày|tháng|năm) trước") - val absoluteTimePattern = Regex("(\\d{2}/\\d{2}/\\d{4})") + val relativeTimePattern = Regex("(\\d+)\\s*(phút|giờ|ngày|tháng|năm) trước") + val absoluteTimePattern = Regex("(\\d{2}/\\d{2}/\\d{4})") - return when { - dateText.contains("phút trước") -> { - val match = relativeTimePattern.find(dateText) - val minutes = match?.groups?.get(1)?.value?.toIntOrNull() ?: 0 - System.currentTimeMillis() - minutes * 60 * 1000 - } + return when { + dateText.contains("phút trước") -> { + val match = relativeTimePattern.find(dateText) + val minutes = match?.groups?.get(1)?.value?.toIntOrNull() ?: 0 + System.currentTimeMillis() - minutes * 60 * 1000 + } - dateText.contains("giờ trước") -> { - val match = relativeTimePattern.find(dateText) - val hours = match?.groups?.get(1)?.value?.toIntOrNull() ?: 0 - System.currentTimeMillis() - hours * 3600 * 1000 - } + dateText.contains("giờ trước") -> { + val match = relativeTimePattern.find(dateText) + val hours = match?.groups?.get(1)?.value?.toIntOrNull() ?: 0 + System.currentTimeMillis() - hours * 3600 * 1000 + } - dateText.contains("ngày trước") -> { - val match = relativeTimePattern.find(dateText) - val days = match?.groups?.get(1)?.value?.toIntOrNull() ?: 0 - System.currentTimeMillis() - days * 86400 * 1000 - } + dateText.contains("ngày trước") -> { + val match = relativeTimePattern.find(dateText) + val days = match?.groups?.get(1)?.value?.toIntOrNull() ?: 0 + System.currentTimeMillis() - days * 86400 * 1000 + } - dateText.contains("tháng trước") -> { - val match = relativeTimePattern.find(dateText) - val months = match?.groups?.get(1)?.value?.toIntOrNull() ?: 0 - System.currentTimeMillis() - months * 30 * 86400 * 1000 - } + dateText.contains("tháng trước") -> { + val match = relativeTimePattern.find(dateText) + val months = match?.groups?.get(1)?.value?.toIntOrNull() ?: 0 + System.currentTimeMillis() - months * 30 * 86400 * 1000 + } - dateText.contains("năm trước") -> { - val match = relativeTimePattern.find(dateText) - val years = match?.groups?.get(1)?.value?.toIntOrNull() ?: 0 - System.currentTimeMillis() - years * 365 * 86400 * 1000 - } + dateText.contains("năm trước") -> { + val match = relativeTimePattern.find(dateText) + val years = match?.groups?.get(1)?.value?.toIntOrNull() ?: 0 + System.currentTimeMillis() - years * 365 * 86400 * 1000 + } - absoluteTimePattern.matches(dateText) -> { - val formatter = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) - try { - val parsedDate = formatter.parse(dateText) - parsedDate?.time ?: 0L - } catch (e: Exception) { - 0L - } - } + absoluteTimePattern.matches(dateText) -> { + val formatter = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) + try { + val parsedDate = formatter.parse(dateText) + parsedDate?.time ?: 0L + } catch (e: Exception) { + 0L + } + } - else -> 0L - } - } + else -> 0L + } + } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/TopTruyen.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/TopTruyen.kt index 045d4318..57939328 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/TopTruyen.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/TopTruyen.kt @@ -149,7 +149,7 @@ internal class TopTruyen(context: MangaLoaderContext) : } return manga.copy( - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), description = description, state = state, tags = tags, diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/Baozimh.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/Baozimh.kt index fa9b2fa3..e8130108 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/Baozimh.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/Baozimh.kt @@ -16,240 +16,240 @@ import java.util.* @MangaSourceParser("BAOZIMH", "Baozimh", "zh") internal class Baozimh(context: MangaLoaderContext) : - LegacyPagedMangaParser(context, MangaParserSource.BAOZIMH, pageSize = 36) { - - override val configKeyDomain = ConfigKey.Domain("www.baozimh.com") - - override fun onCreateConfig(keys: MutableCollection>) { - super.onCreateConfig(keys) - keys.add(userAgentKey) - } - - override val availableSortOrders: Set = EnumSet.of(SortOrder.POPULARITY) - - override val filterCapabilities: MangaListFilterCapabilities - get() = MangaListFilterCapabilities( - isSearchSupported = true, - ) - - override suspend fun getFilterOptions() = MangaListFilterOptions( - availableTags = tagsMap.get().values.toSet(), - availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED), - availableContentTypes = EnumSet.of( - ContentType.MANGA, - ContentType.MANHWA, - ContentType.MANHUA, - ContentType.COMICS, - ), - ) - - private val tagsMap = suspendLazy(initializer = ::parseTags) - - override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - when { - !filter.query.isNullOrEmpty() -> { - if (page > 1) return emptyList() - val url = buildString { - append("https://") - append(domain) - append("/search?q=") - append(filter.query.urlEncoded()) - } - return parseMangaListSearch(webClient.httpGet(url).parseHtml()) - } - - else -> { - val url = buildString { - append("https://") - append(domain) - append("/api/bzmhq/amp_comic_list?filter=*®ion=") - - if (filter.types.isNotEmpty()) { - filter.types.oneOrThrowIfMany().let { - append( - when (it) { - ContentType.MANGA -> "jp" - ContentType.MANHWA -> "kr" - ContentType.MANHUA -> "cn" - ContentType.COMICS -> "en" - else -> "all" - }, - ) - } - } else append("all") - - - append("&type=") - 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 -> "serial" - MangaState.FINISHED -> "pub" - else -> "all" - }, - ) - } - } else append("all") - - append("&limit=36&page=") - append(page.toString()) - } - - return parseMangaList(webClient.httpGet(url).parseJson().getJSONArray("items")) - } - } - } - - private fun parseMangaList(json: JSONArray): List { - return json.mapJSON { j -> - val href = "https://$domain/comic/" + j.getString("comic_id") - val author = j.getString("author") - Manga( - id = generateUid(href), - url = href, - publicUrl = href, - coverUrl = "https://static-tw${domain.removePrefix("www")}/cover/" + j.getString("topic_img"), - title = j.getString("name"), - altTitles = emptySet(), - rating = RATING_UNKNOWN, - tags = emptySet(), - authors = author?.let { setOf(it) } ?: emptySet(), - state = null, - source = source, - contentRating = if (isNsfwSource) ContentRating.ADULT else null, - ) - } - } - - private fun parseMangaListSearch(doc: Document): List { - return doc.select("div.comics-card").map { div -> - val href = "https://$domain" + div.selectFirstOrThrow("a").attrAsRelativeUrl("href") - Manga( - id = generateUid(href), - url = href, - publicUrl = href, - coverUrl = div.selectFirst("amp-img")?.src().orEmpty(), - title = div.selectFirst(".comics-card__title h3")?.text().orEmpty(), - altTitles = emptySet(), - rating = RATING_UNKNOWN, - tags = emptySet(), - authors = emptySet(), - state = null, - source = source, - contentRating = if (isNsfwSource) ContentRating.ADULT else null, - ) - } - } - - private suspend fun parseTags(): Map { - val tagElements = webClient.httpGet("https://$domain/classify").parseHtml() - .select("div.nav")[3].select("a.item:not(.active)") - val tagMap = ArrayMap(tagElements.size) - for (el in tagElements) { - val name = el.text() - if (name.isEmpty()) continue - tagMap[name] = MangaTag( - key = el.attr("href").substringAfter("type=").substringBefore("&"), - title = name, - source = source, - ) - } - return tagMap - } - - override suspend fun getDetails(manga: Manga): Manga { - val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() - val state = doc.selectFirst(".tag-list span.tag")?.text() - val tagMap = tagsMap.get() - val selectTag = doc.select(".tag-list span.tag").drop(1) - val tags = selectTag.mapNotNullToSet { tagMap[it.text()] } - var chaptersReversed = false - val chapters = try { - doc.requireElementById("chapter-items") - .select("div.comics-chapters a") + doc.requireElementById("chapters_other_list") - .select("div.comics-chapters a") - } catch (e: ParseException) { - chaptersReversed = true - // If the above fails it means the manga is new, so we select the chapters using the "comics-chapters__item" query - doc.select(".comics-chapters__item") - } - return manga.copy( - description = doc.selectFirst(".comics-detail__desc")?.text().orEmpty(), - state = when (state) { - "連載中" -> MangaState.ONGOING - "已完結" -> MangaState.FINISHED - else -> null - }, - tags = tags, - chapters = chapters.mapChapters(chaptersReversed) { i, a -> - val url = a.attrAsRelativeUrl("href").toAbsoluteUrl(domain) - MangaChapter( - id = generateUid(url), - name = a.selectFirst("span")?.text() ?: "Chapter ${i + 1f}", - number = i + 1f, - volume = 0, - url = url, - 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 pagesList = doc.requireElementById("__nuxt") - var chapterLink = doc.select("link[rel=canonical]").attr("href") - var nextChapterLink = doc.select("a#next-chapter").attr("href") - var part = 2 - val idSet = HashSet() - var pages = pagesList.select("button.pure-button").map { btn -> - val urlPage = btn.attr("on").substringAfter(": '").substringBefore("?t=") - val id = generateUid(urlPage) - idSet.add(id) - MangaPage( - id = id, - url = urlPage, - preview = null, - source = source, - ) - } - - var chapterPart = chapterLink.substringAfterLast("/").substringBefore(".html") - var nexChapterPart = nextChapterLink.substringAfterLast("/").substringBefore(".html") - while (nextChapterLink != "" && (nexChapterPart == chapterPart + "_" + part.toString())) { - val doc2 = webClient.httpGet(nextChapterLink).parseHtml() - val pages2 = doc2.requireElementById("__nuxt").select("button.pure-button").mapNotNull { btn -> - val urlPage = btn.attr("on").substringAfter(": '").substringBefore("?t=") - val id = generateUid(urlPage) - if (!idSet.add(id)) { - null - } else { - MangaPage( - id = id, - url = urlPage, - preview = null, - source = source, - ) - } - } - pages = pages + pages2 - part++ - chapterLink = doc2.select("link[rel=canonical]").attr("href") - nextChapterLink = doc2.select("a#next-chapter").attr("href") - chapterPart = chapterLink.substringAfterLast("/").substringBefore(".html").substringBeforeLast("_") - nexChapterPart = nextChapterLink.substringAfterLast("/").substringBefore(".html") - } - return pages - } + LegacyPagedMangaParser(context, MangaParserSource.BAOZIMH, pageSize = 36) { + + override val configKeyDomain = ConfigKey.Domain("www.baozimh.com") + + override fun onCreateConfig(keys: MutableCollection>) { + super.onCreateConfig(keys) + keys.add(userAgentKey) + } + + override val availableSortOrders: Set = EnumSet.of(SortOrder.POPULARITY) + + override val filterCapabilities: MangaListFilterCapabilities + get() = MangaListFilterCapabilities( + isSearchSupported = true, + ) + + override suspend fun getFilterOptions() = MangaListFilterOptions( + availableTags = tagsMap.get().values.toSet(), + availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED), + availableContentTypes = EnumSet.of( + ContentType.MANGA, + ContentType.MANHWA, + ContentType.MANHUA, + ContentType.COMICS, + ), + ) + + private val tagsMap = suspendLazy(initializer = ::parseTags) + + override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + when { + !filter.query.isNullOrEmpty() -> { + if (page > 1) return emptyList() + val url = buildString { + append("https://") + append(domain) + append("/search?q=") + append(filter.query.urlEncoded()) + } + return parseMangaListSearch(webClient.httpGet(url).parseHtml()) + } + + else -> { + val url = buildString { + append("https://") + append(domain) + append("/api/bzmhq/amp_comic_list?filter=*®ion=") + + if (filter.types.isNotEmpty()) { + filter.types.oneOrThrowIfMany().let { + append( + when (it) { + ContentType.MANGA -> "jp" + ContentType.MANHWA -> "kr" + ContentType.MANHUA -> "cn" + ContentType.COMICS -> "en" + else -> "all" + }, + ) + } + } else append("all") + + + append("&type=") + 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 -> "serial" + MangaState.FINISHED -> "pub" + else -> "all" + }, + ) + } + } else append("all") + + append("&limit=36&page=") + append(page.toString()) + } + + return parseMangaList(webClient.httpGet(url).parseJson().getJSONArray("items")) + } + } + } + + private fun parseMangaList(json: JSONArray): List { + return json.mapJSON { j -> + val href = "https://$domain/comic/" + j.getString("comic_id") + val author = j.getString("author") + Manga( + id = generateUid(href), + url = href, + publicUrl = href, + coverUrl = "https://static-tw${domain.removePrefix("www")}/cover/" + j.getString("topic_img"), + title = j.getString("name"), + altTitles = emptySet(), + rating = RATING_UNKNOWN, + tags = emptySet(), + authors = setOfNotNull(author), + state = null, + source = source, + contentRating = if (isNsfwSource) ContentRating.ADULT else null, + ) + } + } + + private fun parseMangaListSearch(doc: Document): List { + return doc.select("div.comics-card").map { div -> + val href = "https://$domain" + div.selectFirstOrThrow("a").attrAsRelativeUrl("href") + Manga( + id = generateUid(href), + url = href, + publicUrl = href, + coverUrl = div.selectFirst("amp-img")?.src().orEmpty(), + title = div.selectFirst(".comics-card__title h3")?.text().orEmpty(), + altTitles = emptySet(), + rating = RATING_UNKNOWN, + tags = emptySet(), + authors = emptySet(), + state = null, + source = source, + contentRating = if (isNsfwSource) ContentRating.ADULT else null, + ) + } + } + + private suspend fun parseTags(): Map { + val tagElements = webClient.httpGet("https://$domain/classify").parseHtml() + .select("div.nav")[3].select("a.item:not(.active)") + val tagMap = ArrayMap(tagElements.size) + for (el in tagElements) { + val name = el.text() + if (name.isEmpty()) continue + tagMap[name] = MangaTag( + key = el.attr("href").substringAfter("type=").substringBefore("&"), + title = name, + source = source, + ) + } + return tagMap + } + + override suspend fun getDetails(manga: Manga): Manga { + val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() + val state = doc.selectFirst(".tag-list span.tag")?.text() + val tagMap = tagsMap.get() + val selectTag = doc.select(".tag-list span.tag").drop(1) + val tags = selectTag.mapNotNullToSet { tagMap[it.text()] } + var chaptersReversed = false + val chapters = try { + doc.requireElementById("chapter-items") + .select("div.comics-chapters a") + doc.requireElementById("chapters_other_list") + .select("div.comics-chapters a") + } catch (e: ParseException) { + chaptersReversed = true + // If the above fails it means the manga is new, so we select the chapters using the "comics-chapters__item" query + doc.select(".comics-chapters__item") + } + return manga.copy( + description = doc.selectFirst(".comics-detail__desc")?.text().orEmpty(), + state = when (state) { + "連載中" -> MangaState.ONGOING + "已完結" -> MangaState.FINISHED + else -> null + }, + tags = tags, + chapters = chapters.mapChapters(chaptersReversed) { i, a -> + val url = a.attrAsRelativeUrl("href").toAbsoluteUrl(domain) + MangaChapter( + id = generateUid(url), + name = a.selectFirst("span")?.text() ?: "Chapter ${i + 1f}", + number = i + 1f, + volume = 0, + url = url, + 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 pagesList = doc.requireElementById("__nuxt") + var chapterLink = doc.select("link[rel=canonical]").attr("href") + var nextChapterLink = doc.select("a#next-chapter").attr("href") + var part = 2 + val idSet = HashSet() + var pages = pagesList.select("button.pure-button").map { btn -> + val urlPage = btn.attr("on").substringAfter(": '").substringBefore("?t=") + val id = generateUid(urlPage) + idSet.add(id) + MangaPage( + id = id, + url = urlPage, + preview = null, + source = source, + ) + } + + var chapterPart = chapterLink.substringAfterLast("/").substringBefore(".html") + var nexChapterPart = nextChapterLink.substringAfterLast("/").substringBefore(".html") + while (nextChapterLink != "" && (nexChapterPart == chapterPart + "_" + part.toString())) { + val doc2 = webClient.httpGet(nextChapterLink).parseHtml() + val pages2 = doc2.requireElementById("__nuxt").select("button.pure-button").mapNotNull { btn -> + val urlPage = btn.attr("on").substringAfter(": '").substringBefore("?t=") + val id = generateUid(urlPage) + if (!idSet.add(id)) { + null + } else { + MangaPage( + id = id, + url = urlPage, + preview = null, + source = source, + ) + } + } + pages = pages + pages2 + part++ + chapterLink = doc2.select("link[rel=canonical]").attr("href") + nextChapterLink = doc2.select("a#next-chapter").attr("href") + chapterPart = chapterLink.substringAfterLast("/").substringBefore(".html").substringBeforeLast("_") + nexChapterPart = nextChapterLink.substringAfterLast("/").substringBefore(".html") + } + return pages + } } 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 62ad9f81..b52f59ff 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 @@ -234,7 +234,7 @@ internal abstract class ZMangaParser( }, description = desc, altTitles = setOfNotNull(alt), - authors = author?.let { setOf(it) } ?: emptySet(), + authors = setOfNotNull(author), state = state, chapters = chaptersDeferred.await(), contentRating = if (doc.getElementById("adt-warning") != null) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Chapters.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Chapters.kt index eb56ac0a..afad7166 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Chapters.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Chapters.kt @@ -1,7 +1,10 @@ package org.koitharu.kotatsu.parsers.util +import org.json.JSONArray +import org.json.JSONObject import org.koitharu.kotatsu.parsers.InternalParsersApi import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.util.json.asTypedList @InternalParsersApi public inline fun List.mapChapters( @@ -19,6 +22,12 @@ public inline fun List.mapChapters( return builder.toList() } +@InternalParsersApi +public inline fun JSONArray.mapChapters( + reversed: Boolean = false, + transform: (index: Int, JSONObject) -> MangaChapter?, +): List = asTypedList().mapChapters(reversed, transform) + @InternalParsersApi public inline fun List.flatMapChapters( reversed: Boolean = false,