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 import okhttp3.HttpUrl.Companion.toHttpUrl
public class Favicon( public data class Favicon(
@JvmField public val url: String, @JvmField public val url: String,
@JvmField public val size: Int, @JvmField public val size: Int,
@JvmField internal val rel: String?, @JvmField internal val rel: String?,
@ -20,30 +20,6 @@ public class Favicon(
return relWeightOf(rel).compareTo(relWeightOf(other.rel)) 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) { private fun relWeightOf(rel: String?) = when (rel) {
"apple-touch-icon" -> 1 // Prefer apple-touch-icon because it has a better quality "apple-touch-icon" -> 1 // Prefer apple-touch-icon because it has a better quality
"mask-icon" -> -1 "mask-icon" -> -1

@ -1,10 +1,11 @@
package org.koitharu.kotatsu.parsers.model package org.koitharu.kotatsu.parsers.model
import androidx.collection.ArrayMap import androidx.collection.ArrayMap
import org.koitharu.kotatsu.parsers.InternalParsersApi
import org.koitharu.kotatsu.parsers.util.findById 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 * Unique identifier for manga
*/ */
@ -34,12 +35,12 @@ public class Manga(
/** /**
* Indicates that manga may contain sensitive information (18+, NSFW) * Indicates that manga may contain sensitive information (18+, NSFW)
*/ */
@JvmField public val isNsfw: Boolean, @JvmField public val contentRating: ContentRating?,
/** /**
* Absolute link to the cover * Absolute link to the cover
* @see largeCoverUrl * @see largeCoverUrl
*/ */
@JvmField public val coverUrl: String, @JvmField public val coverUrl: String?,
/** /**
* Tags (genres) of the manga * Tags (genres) of the manga
*/ */
@ -78,15 +79,17 @@ public class Manga(
public val hasRating: Boolean public val hasRating: Boolean
get() = rating > 0f && rating <= 1f get() = rating > 0f && rating <= 1f
public val isNsfw: Boolean
get() = contentRating == ContentRating.ADULT
public fun getChapters(branch: String?): List<MangaChapter> { public fun getChapters(branch: String?): List<MangaChapter> {
return chapters?.filter { x -> x.branch == branch }.orEmpty() return chapters?.filter { x -> x.branch == branch }.orEmpty()
} }
public fun findChapterById(id: Long): MangaChapter? = chapters?.findById(id) public fun findChapterById(id: Long): MangaChapter? = chapters?.findById(id)
public fun requireChapterById(id: Long): MangaChapter = requireNotNull(findChapterById(id)) { public fun requireChapterById(id: Long): MangaChapter = findChapterById(id)
"Chapter with id $id not found" ?: throw NoSuchElementException("Chapter with id $id not found")
}
public fun getBranches(): Map<String?, Int> { public fun getBranches(): Map<String?, Int> {
if (chapters.isNullOrEmpty()) { if (chapters.isNullOrEmpty()) {
@ -100,85 +103,173 @@ public class Manga(
return result return result
} }
@InternalParsersApi public companion object {
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
return true @Deprecated("")
} public operator fun invoke(
/**
override fun hashCode(): Int { * Unique identifier for manga
var result = id.hashCode() */
result = 31 * result + title.hashCode() id: Long,
result = 31 * result + (altTitle?.hashCode() ?: 0) /**
result = 31 * result + url.hashCode() * Manga title, human-readable
result = 31 * result + publicUrl.hashCode() */
result = 31 * result + rating.hashCode() title: String,
result = 31 * result + isNsfw.hashCode() /**
result = 31 * result + coverUrl.hashCode() * Alternative title (for example on other language), may be null
result = 31 * result + tags.hashCode() */
result = 31 * result + (state?.hashCode() ?: 0) altTitle: String?,
result = 31 * result + (author?.hashCode() ?: 0) /**
result = 31 * result + (largeCoverUrl?.hashCode() ?: 0) * Relative url to manga (**without** a domain) or any other uri.
result = 31 * result + (description?.hashCode() ?: 0) * Used principally in parsers
result = 31 * result + (chapters?.hashCode() ?: 0) */
result = 31 * result + source.hashCode() url: String,
return result /**
} * 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 { public operator fun invoke(
return "Manga($id - \"$title\" [$url] - $source)" /**
* 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 import org.koitharu.kotatsu.parsers.util.formatSimple
public class MangaChapter( public data class MangaChapter(
/** /**
* An unique id of chapter * An unique id of chapter
*/ */
@ -51,52 +51,4 @@ public class MangaChapter(
} else { } else {
null 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 import org.koitharu.kotatsu.parsers.MangaParser
public class MangaPage( public data class MangaPage(
/** /**
* Unique identifier for page * Unique identifier for page
*/ */
@ -19,29 +19,4 @@ public class MangaPage(
*/ */
@JvmField public val preview: String?, @JvmField public val preview: String?,
@JvmField public val source: MangaSource, @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 import org.koitharu.kotatsu.parsers.MangaParser
public class MangaTag( public data class MangaTag(
/** /**
* User-readable tag title, should be in Title case * User-readable tag title, should be in Title case
*/ */
@ -13,29 +13,4 @@ public class MangaTag(
*/ */
@JvmField public val key: String, @JvmField public val key: String,
@JvmField public val source: MangaSource, @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() }.orEmpty()
return manga.copy( return manga.copy(
title = root.selectFirst("h3.item-title")?.text() ?: manga.title, 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"), largeCoverUrl = details.selectFirst("img[src]")?.absUrl("src"),
description = details.getElementById("limit-height-body-summary") description = details.getElementById("limit-height-body-summary")
?.selectFirst(".limit-html") ?.selectFirst(".limit-html")

@ -183,7 +183,13 @@ internal class ComickFunParser(context: MangaLoaderContext) :
comic.getJSONArray("md_titles").mapJSON { alt += it.getString("title") + " - " } comic.getJSONArray("md_titles").mapJSON { alt += it.getString("title") + " - " }
return manga.copy( return manga.copy(
altTitle = alt.ifEmpty { comic.getStringOrNull("title") }, 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"), description = comic.getStringOrNull("parsed") ?: comic.getStringOrNull("desc"),
tags = manga.tags + comic.getJSONArray("md_comic_md_genres").mapJSONToSet { tags = manga.tags + comic.getJSONArray("md_comic_md_genres").mapJSONToSet {
val g = it.getJSONObject("md_genres") val g = it.getJSONObject("md_genres")

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

@ -190,7 +190,7 @@ internal class MangaPark(context: MangaLoaderContext) :
else -> null else -> null
}, },
tags = tags, 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 -> chapters = doc.body().select("div.group.flex div.px-2").mapChapters(reversed = true) { i, div ->
val a = div.selectFirstOrThrow("a") val a = div.selectFirstOrThrow("a")
val href = a.attrAsRelativeUrl("href") val href = a.attrAsRelativeUrl("href")

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

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

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

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

@ -89,7 +89,11 @@ internal class FuryoSociety(context: MangaLoaderContext) :
manga.copy( manga.copy(
description = doc.selectFirst("div.fs-comic-description")?.html().orEmpty(), description = doc.selectFirst("div.fs-comic-description")?.html().orEmpty(),
chapters = chaptersDeferred, 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 fullUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml() val doc = webClient.httpGet(fullUrl).parseHtml()
val chaptersDeferred = getChapters(doc) val chaptersDeferred = getChapters(doc)
val desc = doc.selectFirstOrThrow("div.desc").html() val desc = doc.selectFirstOrThrow("div.desc").html().nullIfEmpty()
val alt = doc.body().select("div.infox span.alter").text() val alt = doc.body().select("div.infox span.alter").text().nullIfEmpty()
val aut = doc.select("div.spe span")[2].text().replace("Auteur:", "") val aut = doc.select("div.spe span")[2].text().replace("Auteur:", "").nullIfEmpty()
manga.copy( manga.copy(
tags = doc.select("div.spe span:contains(Genres) a").mapToSet { a -> tags = doc.select("div.spe span:contains(Genres) a").mapToSet { a ->
MangaTag( MangaTag(
@ -133,7 +133,6 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
else -> null else -> null
}, },
chapters = chaptersDeferred, chapters = chaptersDeferred,
isNsfw = manga.isNsfw,
) )
} }

@ -126,7 +126,11 @@ internal class NicovideoSeigaParser(context: MangaLoaderContext) :
STATUS_FINISHED -> MangaState.FINISHED STATUS_FINISHED -> MangaState.FINISHED
else -> null 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 -> chapters = contents.select("#episode_list > ul > li").mapChapters { i, li ->
val href = li.selectFirst("div > div.description > div.title > a") val href = li.selectFirst("div > div.description > div.title > a")
?.attrAsRelativeUrl("href") ?: li.parseFailed() ?.attrAsRelativeUrl("href") ?: li.parseFailed()

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

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

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

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

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

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

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

@ -113,7 +113,7 @@ internal class BrMangas(context: MangaLoaderContext) : PagedMangaParser(context,
return doc.select(".genres_page a").mapToSet { a -> return doc.select(".genres_page a").mapToSet { a ->
MangaTag( MangaTag(
key = a.attr("href").removeSuffix('/').substringAfterLast('/'), key = a.attr("href").removeSuffix('/').substringAfterLast('/'),
title = a.text(), title = a.text().toTitleCase(sourceLocale),
source = source, 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 -> tags = doc.select("div.serie-infos li:contains(Categorias:) a").mapToSet { a ->
MangaTag( MangaTag(
key = a.attr("href").removeSuffix('/').substringAfterLast('/'), key = a.attr("href").removeSuffix('/').substringAfterLast('/'),
title = a.text(), title = a.text().toTitleCase(sourceLocale),
source = source, 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(), 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") chapters = doc.select(".capitulos li a")
.mapChapters { i, a -> .mapChapters { i, a ->
val url = a.attrAsRelativeUrl("href") val url = a.attrAsRelativeUrl("href")

@ -124,7 +124,11 @@ internal class LerManga(context: MangaLoaderContext) : PagedMangaParser(context,
source = source, source = source,
) )
}.orEmpty(), }.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 -> chapters = doc.select("div.manga-chapters div.single-chapter").mapChapters(reversed = true) { i, div ->
val a = div.selectFirstOrThrow("a") val a = div.selectFirstOrThrow("a")
val href = a.attrAsAbsoluteUrl("href") val href = a.attrAsAbsoluteUrl("href")

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

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

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

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

Loading…
Cancel
Save