Migrate models to data class and update Manga class
This commit is contained in:
@@ -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,
|
||||
)
|
||||
public companion object {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
@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,
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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,49 +1,47 @@
|
||||
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.*
|
||||
|
||||
@MangaSourceParser("MEHENTAIVN", "MeHentaiVN", "vi", ContentType.HENTAI)
|
||||
internal class MeHentaiVN(context: MangaLoaderContext) :
|
||||
WpComicsParser(context, MangaParserSource.MEHENTAIVN, "www.mehentaivn.xyz", 44) {
|
||||
|
||||
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user