MangaPark: fixes & improvements (#8483)

* cover absolute url & uncensored cover for hentai

* utils

* nsfw pref and thumbnail baseurl

* lint

* try upload status when original status is unknown

* include extra info in description

* off by default

* bump

* clean title

* nullable

* status set using nullability

* review changes

* revert

* actually set to off
This commit is contained in:
AwkwardPeak7 2025-04-17 18:57:14 +05:00 committed by Draff
parent 1393a25fbb
commit 219ceaac1e
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
4 changed files with 160 additions and 40 deletions

View File

@ -1,7 +1,7 @@
ext {
extName = 'MangaPark'
extClass = '.MangaParkFactory'
extVersionCode = 21
extVersionCode = 22
isNsfw = true
}

View File

@ -17,20 +17,19 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.firstInstanceOrNull
import keiyoushi.utils.getPreferences
import keiyoushi.utils.parseAs
import keiyoushi.utils.toJsonString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicBoolean
@ -54,13 +53,15 @@ class MangaPark(
private val apiUrl = "$baseUrl/apo/"
private val json: Json by injectLazy()
override val client = network.cloudflareClient.newBuilder()
.addInterceptor(::siteSettingsInterceptor)
.addNetworkInterceptor(CookieInterceptor(domain, "nsfw" to "2"))
.rateLimitHost(apiUrl.toHttpUrl(), 1)
.build()
override val client = network.cloudflareClient.newBuilder().apply {
if (preference.getBoolean(ENABLE_NSFW, true)) {
addInterceptor(::siteSettingsInterceptor)
addNetworkInterceptor(CookieInterceptor(domain, "nsfw" to "2"))
}
rateLimitHost(apiUrl.toHttpUrl(), 1)
// intentionally after rate limit interceptor so thumbnails are not rate limited
addInterceptor(::thumbnailDomainInterceptor)
}.build()
override fun headersBuilder() = super.headersBuilder()
.set("Referer", "$baseUrl/")
@ -96,8 +97,10 @@ class MangaPark(
override fun searchMangaParse(response: Response): MangasPage {
val result = response.parseAs<SearchResponse>()
val pageAsCover = preference.getString(UNCENSORED_COVER_PREF, "off")!!
val shortenTitle = preference.getBoolean(SHORTEN_TITLE_PREF, false)
val entries = result.data.searchComics.items.map { it.data.toSManga() }
val entries = result.data.searchComics.items.map { it.data.toSManga(shortenTitle, pageAsCover) }
val hasNextPage = entries.size == size
return MangasPage(entries, hasNextPage)
@ -164,8 +167,10 @@ class MangaPark(
override fun mangaDetailsParse(response: Response): SManga {
val result = response.parseAs<DetailsResponse>()
val pageAsCover = preference.getString(UNCENSORED_COVER_PREF, "off")!!
val shortenTitle = preference.getBoolean(SHORTEN_TITLE_PREF, false)
return result.data.comic.data.toSManga()
return result.data.comic.data.toSManga(shortenTitle, pageAsCover)
}
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.substringBeforeLast("#")
@ -220,7 +225,7 @@ class MangaPark(
summary = "%s"
setOnPreferenceChangeListener { _, _ ->
Toast.makeText(screen.context, "Restart Tachiyomi to apply changes", Toast.LENGTH_LONG).show()
Toast.makeText(screen.context, "Restart the app to apply changes", Toast.LENGTH_LONG).show()
true
}
}.also(screen::addPreference)
@ -231,16 +236,34 @@ class MangaPark(
summary = "Refresh chapter list to apply changes"
setDefaultValue(false)
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = ENABLE_NSFW
title = "Enable NSFW content"
summary = "Clear Cookies & Restart the app to apply changes."
setDefaultValue(true)
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = SHORTEN_TITLE_PREF
title = "Remove extra information from title"
summary = "Clear database to apply changes\n\n" +
"Note: doesn't not work for entries in library"
setDefaultValue(false)
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = UNCENSORED_COVER_PREF
title = "Attempt to use Uncensored Cover for Hentai"
summary = "Uses first or last chapter page as cover"
entries = arrayOf("Off", "First Chapter", "Last Chapter")
entryValues = arrayOf("off", "first", "last")
setDefaultValue("off")
}.also(screen::addPreference)
}
private inline fun <reified T> Response.parseAs(): T =
use { body.string() }.let(json::decodeFromString)
private inline fun <reified T> List<*>.firstInstanceOrNull(): T? =
filterIsInstance<T>().firstOrNull()
private inline fun <reified T : Any> T.toJsonRequestBody() =
json.encodeToString(this).toRequestBody(JSON_MEDIA_TYPE)
toJsonString().toRequestBody(JSON_MEDIA_TYPE)
private val cookiesNotSet = AtomicBoolean(true)
private val latch = CountDownLatch(1)
@ -271,6 +294,25 @@ class MangaPark(
return chain.proceed(request)
}
private fun thumbnailDomainInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request()
val url = request.url
return if (url.host == THUMBNAIL_LOOPBACK_HOST) {
val newUrl = url.newBuilder()
.host(domain)
.build()
val newRequest = request.newBuilder()
.url(newUrl)
.build()
chain.proceed(newRequest)
} else {
chain.proceed(request)
}
}
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException()
}
@ -298,6 +340,11 @@ class MangaPark(
"mpark.to",
)
private const val ENABLE_NSFW = "pref_nsfw"
private const val DUPLICATE_CHAPTER_PREF_KEY = "pref_dup_chapters"
private const val SHORTEN_TITLE_PREF = "pref_shorten_title"
private const val UNCENSORED_COVER_PREF = "pref_uncensored_cover"
}
}
const val THUMBNAIL_LOOPBACK_HOST = "127.0.0.1"

View File

@ -38,33 +38,68 @@ class MangaParkComic(
private val originalStatus: String? = null,
private val uploadStatus: String? = null,
private val summary: String? = null,
private val extraInfo: String? = null,
@SerialName("urlCoverOri") private val cover: String? = null,
private val urlPath: String,
@SerialName("max_chapterNode") private val latestChapter: Data<ImageFiles>? = null,
@SerialName("first_chapterNode") private val firstChapter: Data<ImageFiles>? = null,
) {
fun toSManga() = SManga.create().apply {
fun toSManga(shortenTitle: Boolean, pageAsCover: String) = SManga.create().apply {
url = "$urlPath#$id"
title = name
thumbnail_url = cover
title = if (shortenTitle) {
var shortName = name
while (shortenTitleRegex.containsMatchIn(shortName)) {
shortName = shortName.replace(shortenTitleRegex, "").trim()
}
shortName
} else {
name
}
thumbnail_url = run {
val coverUrl = cover?.let {
when {
it.startsWith("http") -> it
it.startsWith("/") -> "https://$THUMBNAIL_LOOPBACK_HOST$it"
else -> null
}
}
if (pageAsCover != "off" && useLatestPageAsCover(genres)) {
if (pageAsCover == "first") {
firstChapter?.data?.imageFile?.urlList?.firstOrNull() ?: coverUrl
} else {
latestChapter?.data?.imageFile?.urlList?.firstOrNull() ?: coverUrl
}
} else {
coverUrl
}
}
author = authors?.joinToString()
artist = artists?.joinToString()
description = buildString {
val desc = summary?.let { Jsoup.parse(it).text() }
val names = altNames?.takeUnless { it.isEmpty() }
?.joinToString("\n") { "${it.trim()}" }
if (desc.isNullOrEmpty()) {
if (!names.isNullOrEmpty()) {
append("Alternative Names:\n", names)
}
} else {
append(desc)
if (!names.isNullOrEmpty()) {
append("\n\nAlternative Names:\n", names)
}
if (shortenTitle) {
append(name)
append("\n\n")
}
}
summary?.also {
append(Jsoup.parse(it).wholeText().trim())
append("\n\n")
}
extraInfo?.takeUnless(String::isBlank)?.also {
append("Extra Info:\n")
append(Jsoup.parse(it).wholeText().trim())
append("\n\n")
}
altNames?.takeUnless(List<String>::isEmpty)
?.joinToString(
prefix = "Alternative Names:\n",
separator = "\n",
) { "${it.trim()}" }
?.also(::append)
}.trim()
genre = genres?.joinToString { it.replace("_", " ").toCamelCase() }
status = when (originalStatus) {
status = when (originalStatus ?: uploadStatus) {
"ongoing" -> SManga.ONGOING
"completed" -> {
if (uploadStatus == "ongoing") {
@ -96,6 +131,14 @@ class MangaParkComic(
}
return result.toString()
}
private fun useLatestPageAsCover(genres: List<String>?): Boolean {
return genres.orEmpty().let {
it.contains("hentai") && !it.contains("webtoon")
}
}
private val shortenTitleRegex = Regex("""^(\[[^]]+\])|^(\([^)]+\))|^(\{[^}]+\})|(\[[^]]+\])${'$'}|(\([^)]+\))${'$'}|(\{[^}]+\})${'$'}""")
}
}

View File

@ -25,8 +25,23 @@ val SEARCH_QUERY = buildQuery {
originalStatus
uploadStatus
summary
extraInfo
urlCoverOri
urlPath
max_chapterNode {
data {
imageFile {
urlList
}
}
}
first_chapterNode {
data {
imageFile {
urlList
}
}
}
}
}
}
@ -52,8 +67,23 @@ val DETAILS_QUERY = buildQuery {
originalStatus
uploadStatus
summary
extraInfo
urlCoverOri
urlPath
max_chapterNode {
data {
imageFile {
urlList
}
}
}
first_chapterNode {
data {
imageFile {
urlList
}
}
}
}
}
}