Migrate models to data class and update Manga class

master
Koitharu 1 year ago
parent a94adf4d90
commit 5df1445e29
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -2,7 +2,7 @@ package org.koitharu.kotatsu.parsers.model
import okhttp3.HttpUrl.Companion.toHttpUrl
public class Favicon(
public data class Favicon(
@JvmField public val url: String,
@JvmField public val size: Int,
@JvmField internal val rel: String?,
@ -20,30 +20,6 @@ public class Favicon(
return relWeightOf(rel).compareTo(relWeightOf(other.rel))
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Favicon
if (url != other.url) return false
if (size != other.size) return false
if (rel != other.rel) return false
return true
}
override fun hashCode(): Int {
var result = url.hashCode()
result = 31 * result + size
result = 31 * result + rel.hashCode()
return result
}
override fun toString(): String {
return "Favicon(size=$size, type='$type', rel='$rel', url='$url')"
}
private fun relWeightOf(rel: String?) = when (rel) {
"apple-touch-icon" -> 1 // Prefer apple-touch-icon because it has a better quality
"mask-icon" -> -1

@ -1,10 +1,11 @@
package org.koitharu.kotatsu.parsers.model
import androidx.collection.ArrayMap
import org.koitharu.kotatsu.parsers.InternalParsersApi
import org.koitharu.kotatsu.parsers.util.findById
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
public class Manga(
@ExposedCopyVisibility
public data class Manga private constructor(
/**
* Unique identifier for manga
*/
@ -34,12 +35,12 @@ public class Manga(
/**
* Indicates that manga may contain sensitive information (18+, NSFW)
*/
@JvmField public val isNsfw: Boolean,
@JvmField public val contentRating: ContentRating?,
/**
* Absolute link to the cover
* @see largeCoverUrl
*/
@JvmField public val coverUrl: String,
@JvmField public val coverUrl: String?,
/**
* Tags (genres) of the manga
*/
@ -78,15 +79,17 @@ public class Manga(
public val hasRating: Boolean
get() = rating > 0f && rating <= 1f
public val isNsfw: Boolean
get() = contentRating == ContentRating.ADULT
public fun getChapters(branch: String?): List<MangaChapter> {
return chapters?.filter { x -> x.branch == branch }.orEmpty()
}
public fun findChapterById(id: Long): MangaChapter? = chapters?.findById(id)
public fun requireChapterById(id: Long): MangaChapter = requireNotNull(findChapterById(id)) {
"Chapter with id $id not found"
}
public fun requireChapterById(id: Long): MangaChapter = findChapterById(id)
?: throw NoSuchElementException("Chapter with id $id not found")
public fun getBranches(): Map<String?, Int> {
if (chapters.isNullOrEmpty()) {
@ -100,85 +103,173 @@ public class Manga(
return result
}
@InternalParsersApi
public fun copy(
url: String = this.url,
title: String = this.title,
altTitle: String? = this.altTitle,
publicUrl: String = this.publicUrl,
rating: Float = this.rating,
isNsfw: Boolean = this.isNsfw,
coverUrl: String = this.coverUrl,
tags: Set<MangaTag> = this.tags,
state: MangaState? = this.state,
author: String? = this.author,
largeCoverUrl: String? = this.largeCoverUrl,
description: String? = this.description,
chapters: List<MangaChapter>? = this.chapters,
source: MangaSource = this.source,
): Manga = Manga(
id = id,
title = title,
altTitle = altTitle,
url = url,
publicUrl = publicUrl,
rating = rating,
isNsfw = isNsfw,
coverUrl = coverUrl,
tags = tags,
state = state,
author = author,
largeCoverUrl = largeCoverUrl,
description = description,
chapters = chapters,
source = source,
)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Manga
if (id != other.id) return false
if (title != other.title) return false
if (altTitle != other.altTitle) return false
if (url != other.url) return false
if (publicUrl != other.publicUrl) return false
if (rating != other.rating) return false
if (isNsfw != other.isNsfw) return false
if (coverUrl != other.coverUrl) return false
if (tags != other.tags) return false
if (state != other.state) return false
if (author != other.author) return false
if (largeCoverUrl != other.largeCoverUrl) return false
if (description != other.description) return false
if (chapters != other.chapters) return false
if (source != other.source) return false
public companion object {
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + title.hashCode()
result = 31 * result + (altTitle?.hashCode() ?: 0)
result = 31 * result + url.hashCode()
result = 31 * result + publicUrl.hashCode()
result = 31 * result + rating.hashCode()
result = 31 * result + isNsfw.hashCode()
result = 31 * result + coverUrl.hashCode()
result = 31 * result + tags.hashCode()
result = 31 * result + (state?.hashCode() ?: 0)
result = 31 * result + (author?.hashCode() ?: 0)
result = 31 * result + (largeCoverUrl?.hashCode() ?: 0)
result = 31 * result + (description?.hashCode() ?: 0)
result = 31 * result + (chapters?.hashCode() ?: 0)
result = 31 * result + source.hashCode()
return result
}
@Deprecated("")
public operator fun invoke(
/**
* Unique identifier for manga
*/
id: Long,
/**
* Manga title, human-readable
*/
title: String,
/**
* Alternative title (for example on other language), may be null
*/
altTitle: String?,
/**
* Relative url to manga (**without** a domain) or any other uri.
* Used principally in parsers
*/
url: String,
/**
* Absolute url to manga, must be ready to open in browser
*/
publicUrl: String,
/**
* Normalized manga rating, must be in range of 0..1 or [RATING_UNKNOWN] if rating s unknown
* @see hasRating
*/
rating: Float,
/**
* Indicates that manga may contain sensitive information (18+, NSFW)
*/
isNsfw: Boolean,
/**
* Absolute link to the cover
* @see largeCoverUrl
*/
coverUrl: String?,
/**
* Tags (genres) of the manga
*/
tags: Set<MangaTag>,
/**
* Manga status (ongoing, finished) or null if unknown
*/
state: MangaState?,
/**
* Author of the manga, may be null
*/
author: String?,
/**
* Large cover url (absolute), null if is no large cover
* @see coverUrl
*/
largeCoverUrl: String? = null,
/**
* Manga description, may be html or null
*/
description: String? = null,
/**
* List of chapters
*/
chapters: List<MangaChapter>? = null,
/**
* Manga source
*/
source: MangaSource,
): Manga = invoke(
id = id,
title = title,
altTitle = altTitle,
url = url,
publicUrl = publicUrl,
rating = rating,
contentRating = if (isNsfw) ContentRating.ADULT else ContentRating.SAFE,
coverUrl = coverUrl,
tags = tags,
state = state,
author = author,
largeCoverUrl = largeCoverUrl,
description = description,
chapters = chapters,
source = source,
)
override fun toString(): String {
return "Manga($id - \"$title\" [$url] - $source)"
public operator fun invoke(
/**
* Unique identifier for manga
*/
id: Long,
/**
* Manga title, human-readable
*/
title: String,
/**
* Alternative title (for example on other language), may be null
*/
altTitle: String?,
/**
* Relative url to manga (**without** a domain) or any other uri.
* Used principally in parsers
*/
url: String,
/**
* Absolute url to manga, must be ready to open in browser
*/
publicUrl: String,
/**
* Normalized manga rating, must be in range of 0..1 or [RATING_UNKNOWN] if rating s unknown
* @see hasRating
*/
rating: Float,
/**
* Indicates that manga may contain sensitive information (18+, NSFW)
*/
contentRating: ContentRating?,
/**
* Absolute link to the cover
* @see largeCoverUrl
*/
coverUrl: String?,
/**
* Tags (genres) of the manga
*/
tags: Set<MangaTag>,
/**
* Manga status (ongoing, finished) or null if unknown
*/
state: MangaState?,
/**
* Author of the manga, may be null
*/
author: String?,
/**
* Large cover url (absolute), null if is no large cover
* @see coverUrl
*/
largeCoverUrl: String? = null,
/**
* Manga description, may be html or null
*/
description: String? = null,
/**
* List of chapters
*/
chapters: List<MangaChapter>? = null,
/**
* Manga source
*/
source: MangaSource,
): Manga = Manga(
id = id,
title = title,
altTitle = altTitle?.nullIfEmpty(),
url = url,
publicUrl = publicUrl,
rating = rating,
contentRating = contentRating,
coverUrl = coverUrl?.nullIfEmpty(),
tags = tags,
state = state,
author = author?.nullIfEmpty(),
largeCoverUrl = largeCoverUrl?.nullIfEmpty(),
description = description?.nullIfEmpty(),
chapters = chapters,
source = source,
)
}
}

@ -2,7 +2,7 @@ package org.koitharu.kotatsu.parsers.model
import org.koitharu.kotatsu.parsers.util.formatSimple
public class MangaChapter(
public data class MangaChapter(
/**
* An unique id of chapter
*/
@ -51,52 +51,4 @@ public class MangaChapter(
} else {
null
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MangaChapter
if (id != other.id) return false
if (name != other.name) return false
if (number != other.number) return false
if (volume != other.volume) return false
if (url != other.url) return false
if (scanlator != other.scanlator) return false
if (uploadDate != other.uploadDate) return false
if (branch != other.branch) return false
if (source != other.source) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + number.hashCode()
result = 31 * result + volume
result = 31 * result + url.hashCode()
result = 31 * result + (scanlator?.hashCode() ?: 0)
result = 31 * result + uploadDate.hashCode()
result = 31 * result + (branch?.hashCode() ?: 0)
result = 31 * result + source.hashCode()
return result
}
override fun toString(): String {
return "MangaChapter($id - #$number [$url] - $source)"
}
internal fun copy(volume: Int, number: Float) = MangaChapter(
id = id,
name = name,
number = number,
volume = volume,
url = url,
scanlator = scanlator,
uploadDate = uploadDate,
branch = branch,
source = source,
)
}

@ -2,7 +2,7 @@ package org.koitharu.kotatsu.parsers.model
import org.koitharu.kotatsu.parsers.MangaParser
public class MangaPage(
public data class MangaPage(
/**
* Unique identifier for page
*/
@ -19,29 +19,4 @@ public class MangaPage(
*/
@JvmField public val preview: String?,
@JvmField public val source: MangaSource,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MangaPage
if (id != other.id) return false
if (url != other.url) return false
if (preview != other.preview) return false
return source == other.source
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + url.hashCode()
result = 31 * result + (preview?.hashCode() ?: 0)
result = 31 * result + source.hashCode()
return result
}
override fun toString(): String {
return "MangaPage($id [$url] - $source)"
}
}
)

@ -2,7 +2,7 @@ package org.koitharu.kotatsu.parsers.model
import org.koitharu.kotatsu.parsers.MangaParser
public class MangaTag(
public data class MangaTag(
/**
* User-readable tag title, should be in Title case
*/
@ -13,29 +13,4 @@ public class MangaTag(
*/
@JvmField public val key: String,
@JvmField public val source: MangaSource,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MangaTag
if (title != other.title) return false
if (key != other.key) return false
if (source != other.source) return false
return true
}
override fun hashCode(): Int {
var result = title.hashCode()
result = 31 * result + key.hashCode()
result = 31 * result + source.hashCode()
return result
}
override fun toString(): String {
return "MangaTag($key \"$title\" - $source)"
}
}
)

@ -214,7 +214,11 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
}.orEmpty()
return manga.copy(
title = root.selectFirst("h3.item-title")?.text() ?: manga.title,
isNsfw = !root.selectFirst("alert")?.getElementsContainingOwnText("NSFW").isNullOrEmpty(),
contentRating = if (root.selectFirst("alert")?.getElementsContainingOwnText("NSFW").isNullOrEmpty()) {
ContentRating.ADULT
} else {
ContentRating.SAFE
},
largeCoverUrl = details.selectFirst("img[src]")?.absUrl("src"),
description = details.getElementById("limit-height-body-summary")
?.selectFirst(".limit-html")

@ -183,7 +183,13 @@ internal class ComickFunParser(context: MangaLoaderContext) :
comic.getJSONArray("md_titles").mapJSON { alt += it.getString("title") + " - " }
return manga.copy(
altTitle = alt.ifEmpty { comic.getStringOrNull("title") },
isNsfw = jo.getBooleanOrDefault("matureContent", false) || comic.getBooleanOrDefault("hentai", false),
contentRating = if (jo.getBooleanOrDefault("matureContent", false)
|| comic.getBooleanOrDefault("hentai", false)
) {
ContentRating.ADULT
} else {
ContentRating.SAFE
},
description = comic.getStringOrNull("parsed") ?: comic.getStringOrNull("desc"),
tags = manga.tags + comic.getJSONArray("md_comic_md_genres").mapJSONToSet {
val g = it.getJSONObject("md_genres")

@ -166,7 +166,7 @@ internal abstract class MangaFireParser(
altTitle = null,
largeCoverUrl = null,
author = null,
isNsfw = false,
contentRating = null,
rating = RATING_UNKNOWN,
state = null,
tags = emptySet(),
@ -177,7 +177,8 @@ internal abstract class MangaFireParser(
override suspend fun getDetails(manga: Manga): Manga {
val document = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val availableTags = tags.get()
var isNsfw = false
var isAdult = false
var isSuggestive = false
return manga.copy(
title = document.selectFirstOrThrow(".info > h1").ownText(),
@ -188,12 +189,18 @@ internal abstract class MangaFireParser(
.attrAsAbsoluteUrl("src"),
tags = document.select("div.meta a[href*=/genre/]").mapNotNullToSet {
val tag = it.ownText()
if (tag == "Hentai" || tag == "Ecchi") {
isNsfw = true
if (tag == "Hentai") {
isAdult = true
} else if (tag == "Ecchi") {
isSuggestive = true
}
availableTags[tag.toTitleCase(sourceLocale)]
},
isNsfw = isNsfw,
contentRating = when {
isAdult -> ContentRating.ADULT
isSuggestive -> ContentRating.SUGGESTIVE
else -> ContentRating.SAFE
},
state = document.selectFirst(".info > p")?.ownText()?.let {
when (it.lowercase()) {
"releasing" -> MangaState.ONGOING
@ -326,7 +333,7 @@ internal abstract class MangaFireParser(
altTitle = null,
largeCoverUrl = null,
author = null,
isNsfw = false,
contentRating = null,
rating = RATING_UNKNOWN,
state = null,
tags = emptySet(),
@ -349,7 +356,7 @@ internal abstract class MangaFireParser(
altTitle = null,
largeCoverUrl = null,
author = null,
isNsfw = false,
contentRating = null,
rating = RATING_UNKNOWN,
state = null,
tags = emptySet(),

@ -190,7 +190,7 @@ internal class MangaPark(context: MangaLoaderContext) :
else -> null
},
tags = tags,
isNsfw = nsfw,
contentRating = if (nsfw) ContentRating.ADULT else ContentRating.SAFE,
chapters = doc.body().select("div.group.flex div.px-2").mapChapters(reversed = true) { i, div ->
val a = div.selectFirstOrThrow("a")
val href = a.attrAsRelativeUrl("href")

@ -167,7 +167,8 @@ internal class MangaReaderToParser(context: MangaLoaderContext) :
override suspend fun getDetails(manga: Manga): Manga {
val document = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val availableTags = tags.get()
var isNsfw = false
var isAdult = false
var isSuggestive = false
return manga.copy(
title = document.selectFirst("h2.manga-name")!!.ownText(),
@ -177,12 +178,18 @@ internal class MangaReaderToParser(context: MangaLoaderContext) :
coverUrl = document.selectFirst(".manga-poster > img")!!.attr("src"),
tags = document.select("div.genres > a[href*=/genre/]").mapNotNullToSet {
val tag = it.ownText()
if (tag == "Hentai" || tag == "Ecchi") {
isNsfw = true
if (tag == "Hentai") {
isAdult = true
} else if (tag == "Ecchi") {
isSuggestive = true
}
availableTags[tag]
},
isNsfw = isNsfw,
contentRating = when {
isAdult -> ContentRating.ADULT
isSuggestive -> ContentRating.SUGGESTIVE
else -> ContentRating.SAFE
},
state = document.selectFirst("div.anisc-info .item:contains(status:) > .name")
?.text()?.let {
when (it) {

@ -207,7 +207,7 @@ internal class NineNineNineHentaiParser(context: MangaLoaderContext) :
else -> "https://${cdnHost.get()}/$cover"
},
author = null,
isNsfw = true,
contentRating = ContentRating.ADULT,
url = id,
publicUrl = "/hchapter/$id".toAbsoluteUrl(domain),
tags = emptySet(),
@ -270,7 +270,7 @@ internal class NineNineNineHentaiParser(context: MangaLoaderContext) :
coverUrl = cover.first,
largeCoverUrl = cover.second,
author = tags?.filter { it.type == "artist" }?.joinToString { it.name.toCamelCase() },
isNsfw = true,
contentRating = ContentRating.ADULT,
tags = tags?.mapToSet {
MangaTag(
title = it.name.toCamelCase(),

@ -173,7 +173,7 @@ internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context
)
},
rating = rating?.toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN,
isNsfw = nsfw,
contentRating = if (nsfw) ContentRating.ADULT else ContentRating.SAFE,
chapters = chaptersDeferred.await(),
)
}

@ -152,7 +152,7 @@ internal class FlixScansOrg(context: MangaLoaderContext) :
)
},
rating = rating?.toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN,
isNsfw = nsfw,
contentRating = if (nsfw) ContentRating.ADULT else ContentRating.SAFE,
chapters = chaptersDeferred.await(),
)
}

@ -89,7 +89,11 @@ internal class FuryoSociety(context: MangaLoaderContext) :
manga.copy(
description = doc.selectFirst("div.fs-comic-description")?.html().orEmpty(),
chapters = chaptersDeferred,
isNsfw = doc.selectFirst(".adult-text") != null,
contentRating = if (doc.selectFirst(".adult-text") != null) {
ContentRating.ADULT
} else {
ContentRating.SAFE
},
)
}

@ -112,9 +112,9 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
val fullUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val chaptersDeferred = getChapters(doc)
val desc = doc.selectFirstOrThrow("div.desc").html()
val alt = doc.body().select("div.infox span.alter").text()
val aut = doc.select("div.spe span")[2].text().replace("Auteur:", "")
val desc = doc.selectFirstOrThrow("div.desc").html().nullIfEmpty()
val alt = doc.body().select("div.infox span.alter").text().nullIfEmpty()
val aut = doc.select("div.spe span")[2].text().replace("Auteur:", "").nullIfEmpty()
manga.copy(
tags = doc.select("div.spe span:contains(Genres) a").mapToSet { a ->
MangaTag(
@ -133,7 +133,6 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
else -> null
},
chapters = chaptersDeferred,
isNsfw = manga.isNsfw,
)
}

@ -126,7 +126,11 @@ internal class NicovideoSeigaParser(context: MangaLoaderContext) :
STATUS_FINISHED -> MangaState.FINISHED
else -> null
},
isNsfw = contents.select(".icon_adult").isNotEmpty(),
contentRating = if (contents.select(".icon_adult").isNotEmpty()) {
ContentRating.ADULT
} else {
ContentRating.SAFE
},
chapters = contents.select("#episode_list > ul > li").mapChapters { i, li ->
val href = li.selectFirst("div > div.description > div.title > a")
?.attrAsRelativeUrl("href") ?: li.parseFailed()

@ -577,7 +577,11 @@ internal abstract class MadaraParser(
altTitle = alt,
state = state,
chapters = chaptersDeferred.await(),
isNsfw = doc.selectFirst(".adult-confirm") != null,
contentRating = if (doc.selectFirst(".adult-confirm") != null) {
ContentRating.ADULT
} else {
ContentRating.SAFE
},
)
}

@ -190,7 +190,11 @@ internal abstract class MadthemeParser(
altTitle = alt.orEmpty(),
state = state,
chapters = chaptersDeferred.await(),
isNsfw = nsfw || manga.isNsfw,
contentRating = if (nsfw || manga.isNsfw) {
ContentRating.ADULT
} else {
ContentRating.SAFE
},
)
}

@ -174,8 +174,8 @@ internal abstract class MangaboxParser(
else -> null
}
}
val alt = doc.body().select(selectAlt).text().replace("Alternative : ", "")
val aut = doc.body().select(selectAut).eachText().joinToString()
val alt = doc.body().select(selectAlt).text().replace("Alternative : ", "").nullIfEmpty()
val aut = doc.body().select(selectAut).eachText().joinToString().nullIfEmpty()
manga.copy(
tags = doc.body().select(selectTag).mapToSet { a ->
MangaTag(
@ -189,7 +189,6 @@ internal abstract class MangaboxParser(
author = aut,
state = state,
chapters = chaptersDeferred.await(),
isNsfw = manga.isNsfw,
)
}

@ -126,7 +126,7 @@ internal class Mangairo(context: MangaLoaderContext) :
val fullUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val chaptersDeferred = async { getChapters(doc) }
val desc = doc.selectFirst(selectDesc)?.html()
val desc = doc.selectFirst(selectDesc)?.html()?.nullIfEmpty()
val stateDiv = doc.select(selectState).text()
val state = stateDiv.let {
when (it) {
@ -136,8 +136,8 @@ internal class Mangairo(context: MangaLoaderContext) :
}
}
val alt = doc.body().select(selectAlt).text().replace("Alternative : ", "")
val aut = doc.body().select(selectAut).eachText().joinToString()
val alt = doc.body().select(selectAlt).text().replace("Alternative : ", "").nullIfEmpty()
val aut = doc.body().select(selectAut).eachText().joinToString().nullIfEmpty()
manga.copy(
tags = doc.body().select(selectTag).mapToSet { a ->
MangaTag(
@ -152,7 +152,6 @@ internal class Mangairo(context: MangaLoaderContext) :
author = aut,
state = state,
chapters = chaptersDeferred.await(),
isNsfw = manga.isNsfw,
)
}
}

@ -108,8 +108,8 @@ internal class MangakakalotTv(context: MangaLoaderContext) :
else -> null
}
}
val alt = doc.body().select(selectAlt).text().replace("Alternative : ", "")
val aut = doc.body().select(selectAut).eachText().joinToString()
val alt = doc.body().select(selectAlt).text().replace("Alternative : ", "").nullIfEmpty()
val aut = doc.body().select(selectAut).eachText().joinToString().nullIfEmpty()
manga.copy(
tags = doc.body().select(selectTag).mapToSet { a ->
MangaTag(
@ -123,7 +123,6 @@ internal class MangakakalotTv(context: MangaLoaderContext) :
author = aut,
state = state,
chapters = chaptersDeferred.await(),
isNsfw = manga.isNsfw,
)
}

@ -262,11 +262,11 @@ internal abstract class MangaReaderParser(
}
}
val author = tableMode?.selectFirst(".infotable td:contains(Author)")?.lastElementSibling()?.text()
?: docs.selectFirst(".tsinfo div:contains(Author)")?.lastElementChild()?.text()
?: docs.selectFirst(".tsinfo div:contains(Auteur)")?.lastElementChild()?.text()
?: docs.selectFirst(".tsinfo div:contains(Artist)")?.lastElementChild()?.text()
?: docs.selectFirst(".tsinfo div:contains(Durum)")?.lastElementChild()?.text()
val author = tableMode?.selectFirst(".infotable td:contains(Author)")?.lastElementSibling()?.textOrNull()
?: docs.selectFirst(".tsinfo div:contains(Author)")?.lastElementChild()?.textOrNull()
?: docs.selectFirst(".tsinfo div:contains(Auteur)")?.lastElementChild()?.textOrNull()
?: docs.selectFirst(".tsinfo div:contains(Artist)")?.lastElementChild()?.textOrNull()
?: docs.selectFirst(".tsinfo div:contains(Durum)")?.lastElementChild()?.textOrNull()
val nsfw = docs.selectFirst(".restrictcontainer") != null
|| docs.selectFirst(".info-right .alr") != null
@ -276,7 +276,11 @@ internal abstract class MangaReaderParser(
description = docs.selectFirst(detailsDescriptionSelector)?.text(),
state = mangaState,
author = author,
isNsfw = manga.isNsfw || nsfw,
contentRating = if (manga.isNsfw || nsfw) {
ContentRating.ADULT
} else {
ContentRating.SAFE
},
tags = tags,
chapters = chapters,
)
@ -348,14 +352,13 @@ internal abstract class MangaReaderParser(
protected open suspend fun getOrCreateTagMap(): Map<String, MangaTag> = mutex.withLock {
tagCache?.let { return@withLock it }
val tagMap = ArrayMap<String, MangaTag>()
val url = listUrl.toAbsoluteUrl(domain)
val tagElements = webClient.httpGet(url).parseHtml().select("ul.genrez > li")
val tagMap = ArrayMap<String, MangaTag>(tagElements.size)
for (el in tagElements) {
if (el.text().isEmpty()) continue
tagMap[el.text()] = MangaTag(
title = el.text(),
key = el.selectFirst("input")?.attr("value") ?: continue,
title = el.textOrNull()?.toTitleCase(sourceLocale) ?: continue,
key = el.selectFirst("input")?.attrOrNull("value") ?: continue,
source = source,
)
}

@ -101,7 +101,7 @@ internal class Normoyun(context: MangaLoaderContext) :
} else {
MangaState.ONGOING
}
val author = docs.selectFirst("span.author i")?.text()
val author = docs.selectFirst("span.author i")?.textOrNull()
val nsfw = docs.selectFirst(".restrictcontainer") != null
|| docs.selectFirst(".info-right .alr") != null
@ -111,7 +111,11 @@ internal class Normoyun(context: MangaLoaderContext) :
description = docs.selectFirst("span.desc")?.html(),
state = mangaState,
author = author,
isNsfw = manga.isNsfw || nsfw,
contentRating = if (manga.isNsfw || nsfw) {
ContentRating.ADULT
} else {
ContentRating.SAFE
},
tags = emptySet(),
chapters = chapters,
)

@ -91,14 +91,17 @@ internal class TuManhwas(context: MangaLoaderContext) :
|| docs.selectFirst(".postbody .alr") != null
return manga.copy(
description = docs.selectFirst("div.entry-content")?.text(),
description = docs.selectFirst("div.entry-content")?.html(),
state = mangaState,
author = null,
isNsfw = manga.isNsfw || nsfw,
contentRating = if (manga.isNsfw || nsfw) {
ContentRating.ADULT
} else {
ContentRating.SAFE
},
tags = docs.select(".wd-full .mgen > a").mapToSet { a ->
MangaTag(
key = a.attr("href").substringAfterLast('='),
title = a.text().toTitleCase(),
title = a.text().toTitleCase(sourceLocale),
source = source,
)
},

@ -113,7 +113,8 @@ internal class Komikcast(context: MangaLoaderContext) :
} else {
MangaState.FINISHED
}
val author = docs.selectFirst(".komik_info-content-meta span:contains(Author)")?.lastElementChild()?.text()
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",
@ -123,7 +124,11 @@ internal class Komikcast(context: MangaLoaderContext) :
description = docs.selectFirst("div.komik_info-description-sinopsis")?.text(),
state = mangaState,
author = author,
isNsfw = manga.isNsfw || nsfw,
contentRating = if (manga.isNsfw || nsfw) {
ContentRating.ADULT
} else {
ContentRating.SAFE
},
tags = tags,
chapters = chapters,
)

@ -113,7 +113,7 @@ internal class BrMangas(context: MangaLoaderContext) : PagedMangaParser(context,
return doc.select(".genres_page a").mapToSet { a ->
MangaTag(
key = a.attr("href").removeSuffix('/').substringAfterLast('/'),
title = a.text(),
title = a.text().toTitleCase(sourceLocale),
source = source,
)
}
@ -127,13 +127,17 @@ internal class BrMangas(context: MangaLoaderContext) : PagedMangaParser(context,
tags = doc.select("div.serie-infos li:contains(Categorias:) a").mapToSet { a ->
MangaTag(
key = a.attr("href").removeSuffix('/').substringAfterLast('/'),
title = a.text(),
title = a.text().toTitleCase(sourceLocale),
source = source,
)
},
author = doc.select("div.serie-infos li:contains(Autor:)").text().replace("Autor:", ""),
author = doc.select("div.serie-infos li:contains(Autor:)").text().replace("Autor:", "").nullIfEmpty(),
description = doc.select(".serie-texto p").text(),
isNsfw = doc.select("div.serie-infos li:contains(Categorias:)").text().contains("Hentai"),
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")

@ -124,7 +124,11 @@ internal class LerManga(context: MangaLoaderContext) : PagedMangaParser(context,
source = source,
)
}.orEmpty(),
isNsfw = doc.select("ul.genre-list li").text().contains("Adulto"),
contentRating = if (doc.select("ul.genre-list li").text().contains("Adulto")) {
ContentRating.ADULT
} else {
manga.contentRating
},
chapters = doc.select("div.manga-chapters div.single-chapter").mapChapters(reversed = true) { i, div ->
val a = div.selectFirstOrThrow("a")
val href = a.attrAsAbsoluteUrl("href")

@ -140,11 +140,12 @@ internal abstract class GroupleParser(
source = newSource,
title = doc.metaValue("name") ?: manga.title,
altTitle = root.selectFirst(".all-names-popover")?.select(".name")?.joinToString { it.text() }
?.nullIfEmpty()
?: manga.altTitle,
publicUrl = response.request.url.toString(),
description = root.selectFirst("div.manga-description")?.html(),
largeCoverUrl = coverImg?.attr("data-full"),
coverUrl = coverImg?.attr("data-thumb") ?: manga.coverUrl,
largeCoverUrl = coverImg?.attrAsAbsoluteUrlOrNull("data-full"),
coverUrl = coverImg?.attrAsAbsoluteUrlOrNull("data-thumb") ?: manga.coverUrl,
tags = root.selectFirstOrThrow("div.subject-meta")
.getElementsByAttributeValueContaining("href", "/list/genre/").mapTo(manga.tags.toMutableSet()) { a ->
MangaTag(
@ -153,8 +154,14 @@ internal abstract class GroupleParser(
source = source,
)
},
author = root.selectFirst("a.person-link")?.text() ?: manga.author,
isNsfw = manga.isNsfw || root.select(".alert-warning").any { it.ownText().contains(NSFW_ALERT) },
author = root.selectFirst("a.person-link")?.textOrNull() ?: manga.author,
contentRating = if (manga.isNsfw || root.select(".alert-warning")
.any { it.ownText().contains(NSFW_ALERT) }
) {
ContentRating.ADULT
} else {
manga.contentRating
},
chapters = chaptersList?.select("a.chapter-link")
?.flatMapChapters(reversed = true) { a ->
val tr = a.selectFirstParent("tr") ?: return@flatMapChapters emptyList()

@ -128,13 +128,17 @@ internal class BlogTruyenParser(context: MangaLoaderContext) :
return manga.copy(
tags = tags,
author = descriptionElement.selectFirst("p:contains(Tác giả) > a")?.text(),
author = descriptionElement.selectFirst("p:contains(Tác giả) > a")?.textOrNull(),
description = doc.selectFirst(".detail .content")?.html(),
chapters = parseChapterList(doc),
largeCoverUrl = doc.selectLast("div.thumbnail > img")?.src().orEmpty(),
largeCoverUrl = doc.selectLast("div.thumbnail > img")?.src(),
state = state,
rating = rating ?: RATING_UNKNOWN,
isNsfw = doc.getElementById("warningCategory") != null,
contentRating = if (doc.getElementById("warningCategory") != null) {
ContentRating.ADULT
} else {
manga.contentRating
},
)
}

@ -180,13 +180,17 @@ internal class BlogTruyenVN(context: MangaLoaderContext) :
return manga.copy(
tags = tags ?: emptySet(),
author = descriptionElement.selectFirst("p:contains(Tác giả) > a")?.text(),
author = descriptionElement.selectFirst("p:contains(Tác giả) > a")?.textOrNull(),
description = doc.selectFirst(".detail .content")?.html(),
chapters = parseChapterList(doc),
largeCoverUrl = doc.selectLast("div.thumbnail > img")?.src().orEmpty(),
largeCoverUrl = doc.selectLast("div.thumbnail > img")?.src(),
state = state,
rating = rating ?: RATING_UNKNOWN,
isNsfw = doc.getElementById("warningCategory") != null,
contentRating = if (doc.getElementById("warningCategory") != null) {
ContentRating.ADULT
} else {
manga.contentRating
},
)
}

@ -127,37 +127,41 @@ internal class CuuTruyenParser(context: MangaLoaderContext) :
}
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val url = "https://" + domain + manga.url
val chapters = async {
webClient.httpGet("$url/chapters").parseJson().getJSONArray("data")
}
val json = webClient.httpGet(url).parseJson().getJSONObject("data")
val chapterDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.ROOT).apply {
timeZone = TimeZone.getTimeZone("GMT+7")
}
val tags = json.optJSONArray("tags")?.mapJSONToSet { jo ->
MangaTag(
title = jo.getString("name").toTitleCase(sourceLocale),
key = jo.getString("slug"),
source = source,
)
}.orEmpty()
val url = "https://" + domain + manga.url
val chapters = async {
webClient.httpGet("$url/chapters").parseJson().getJSONArray("data")
}
val json = webClient.httpGet(url).parseJson().getJSONObject("data")
val chapterDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.ROOT).apply {
timeZone = TimeZone.getTimeZone("GMT+7")
}
val tags = json.optJSONArray("tags")?.mapJSONToSet { jo ->
MangaTag(
title = jo.getString("name").toTitleCase(sourceLocale),
key = jo.getString("slug"),
source = source,
)
}.orEmpty()
// Testing: Add custom manga status using available tags
val state = when {
tags.any { it.key == "da-hoan-thanh" } -> MangaState.FINISHED
tags.any { it.key == "dang-tien-hanh" } -> MangaState.ONGOING
else -> null
}
// Testing: Add custom manga status using available tags
val state = when {
tags.any { it.key == "da-hoan-thanh" } -> MangaState.FINISHED
tags.any { it.key == "dang-tien-hanh" } -> MangaState.ONGOING
else -> null
}
// Remove old manga status from "tags"
val newTags = tags.filter { it.key != "da-hoan-thanh" && it.key != "dang-tien-hanh" }.toSet()
// Remove old manga status from "tags"
val newTags = tags.filter { it.key != "da-hoan-thanh" && it.key != "dang-tien-hanh" }.toSet()
manga.copy(
title = json.getStringOrNull("name") ?: manga.title,
isNsfw = json.getBooleanOrDefault("is_nsfw", manga.isNsfw),
author = json.optJSONObject("author")?.getStringOrNull("name")?.substringBefore(','),
description = json.getString("full_description"),
manga.copy(
title = json.getStringOrNull("name") ?: manga.title,
contentRating = if (json.getBooleanOrDefault("is_nsfw", manga.isNsfw)) {
ContentRating.ADULT
} else {
ContentRating.SAFE
},
author = json.optJSONObject("author")?.getStringOrNull("name")?.substringBefore(',')?.nullIfEmpty(),
description = json.getStringOrNull("full_description"),
tags = newTags,
state = state,
chapters = chapters.await().mapJSON { jo ->

@ -1,14 +1,13 @@
package org.koitharu.kotatsu.parsers.site.wpcomics.vi
import kotlinx.coroutines.async
import androidx.collection.ArraySet
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.wpcomics.WpComicsParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.wpcomics.WpComicsParser
import org.koitharu.kotatsu.parsers.util.*
import java.util.*
@ -18,32 +17,31 @@ internal class MeHentaiVN(context: MangaLoaderContext) :
override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("www.mehentaivn.xyz", "www.hentaivnx.autos")
override suspend fun getFilterOptions() = MangaListFilterOptions(
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED),
)
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val fullUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val chaptersDeferred = async { getChapters(doc) }
val tagMap = getOrCreateTagMap()
val tagsElement = doc.select("li.kind p.col-xs-8 a")
val mangaTags = tagsElement.mapNotNullToSet {
val tagTitle = it.text()
if (tagTitle.isNotEmpty())
MangaTag(
title = tagTitle,
key = tagsElement.attr("href").substringAfterLast('/').trim(),
source = source
)
else null
}
val mangaTags = tagsElement.mapNotNullToSet {
val tagTitle = it.text()
if (tagTitle.isNotEmpty())
MangaTag(
title = tagTitle.toTitleCase(sourceLocale),
key = tagsElement.attr("href").substringAfterLast('/').trim(),
source = source,
)
else null
}
manga.copy(
description = doc.selectFirst(selectDesc)?.html().orEmpty(),
altTitle = doc.selectFirst("h2.other-name")?.text().orEmpty(),
author = doc.body().select(selectAut).text(),
description = doc.selectFirst(selectDesc)?.html(),
altTitle = doc.selectFirst("h2.other-name")?.textOrNull(),
author = doc.body().selectFirst(selectAut)?.textOrNull(),
state = doc.selectFirst(selectState)?.let {
when (it.text()) {
in ongoing -> MangaState.ONGOING
@ -54,21 +52,21 @@ internal class MeHentaiVN(context: MangaLoaderContext) :
tags = mangaTags,
rating = doc.selectFirst("div.star input")?.attr("value")?.toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN,
chapters = chaptersDeferred.await(),
isNsfw = true
contentRating = ContentRating.ADULT,
)
}
private suspend fun fetchTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/").parseHtml()
val tagItems = doc.select("ul.dropdown-menu.megamenu li a")
val tagSet = ArraySet<MangaTag>(tagItems.size)
for (item in tagItems) {
val title = item.attr("data-title").trim()
val key = item.attr("href").substringAfterLast('/').trim()
if (key.isNotEmpty() && title.isNotEmpty()) {
tagSet.add(MangaTag(title = title, key = key, source = source))
}
}
return tagSet
}
private suspend fun fetchTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/").parseHtml()
val tagItems = doc.select("ul.dropdown-menu.megamenu li a")
val tagSet = ArraySet<MangaTag>(tagItems.size)
for (item in tagItems) {
val title = item.attr("data-title").toTitleCase(sourceLocale)
val key = item.attr("href").substringAfterLast('/').trim()
if (key.isNotEmpty() && title.isNotEmpty()) {
tagSet.add(MangaTag(title = title, key = key, source = source))
}
}
return tagSet
}
}

@ -220,9 +220,9 @@ internal abstract class ZMangaParser(
}
}
val alt = doc.body().select(selectAlt).text()
val alt = doc.body().selectFirst(selectAlt)?.textOrNull()
val aut = doc.body().select(selectAut).text()
val aut = doc.body().selectFirst(selectAut)?.textOrNull()
manga.copy(
tags = doc.body().select(selectTag).mapToSet { a ->
@ -237,7 +237,11 @@ internal abstract class ZMangaParser(
author = aut,
state = state,
chapters = chaptersDeferred.await(),
isNsfw = manga.isNsfw || doc.getElementById("adt-warning") != null,
contentRating = if (doc.getElementById("adt-warning") != null) {
ContentRating.ADULT
} else {
manga.contentRating
},
)
}

@ -50,7 +50,7 @@ public class LinkResolver internal constructor(
url = url,
publicUrl = link.toString(),
rating = RATING_UNKNOWN,
isNsfw = false,
contentRating = null,
coverUrl = "",
tags = emptySet(),
state = null,
@ -94,7 +94,7 @@ public class LinkResolver internal constructor(
author = seed.author ?: resolved.author,
tags = seed.tags + resolved.tags,
state = seed.state ?: resolved.state,
coverUrl = seed.coverUrl.ifEmpty { resolved.coverUrl },
coverUrl = seed.coverUrl ?: resolved.coverUrl,
largeCoverUrl = seed.largeCoverUrl ?: resolved.largeCoverUrl,
altTitle = seed.altTitle ?: resolved.altTitle,
)

@ -248,8 +248,8 @@ internal class MangaParserTest {
for (item in list) {
assert(item.url.isNotEmpty()) { "Url is empty" }
assert(!item.url.isUrlAbsolute()) { "Url looks like absolute: ${item.url}" }
if (item.coverUrl.isNotEmpty()) { // TODO nullable cover
assert(item.coverUrl.isUrlAbsolute()) { "Cover url is not absolute: ${item.coverUrl}" }
item.coverUrl?.let {
assert(it.isUrlAbsolute()) { "Cover url is not absolute: ${item.coverUrl}" }
}
assert(item.title.isNotEmpty()) { "Title for ${item.publicUrl} is empty" }
assert(item.publicUrl.isUrlAbsolute())
@ -258,7 +258,10 @@ internal class MangaParserTest {
checkImageRequest(testItem.coverUrl, testItem.source)
}
private suspend fun checkImageRequest(url: String, source: MangaSource) {
private suspend fun checkImageRequest(url: String?, source: MangaSource) {
if (url == null) {
return
}
context.doRequest(url, source).use {
assert(it.isSuccessful) { "Request failed: ${it.code}(${it.message}): $url" }
assert(it.mimeType?.startsWith("image/") == true) {

Loading…
Cancel
Save