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 { ext {
extName = 'MangaPark' extName = 'MangaPark'
extClass = '.MangaParkFactory' extClass = '.MangaParkFactory'
extVersionCode = 21 extVersionCode = 22
isNsfw = true 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.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.firstInstanceOrNull
import keiyoushi.utils.getPreferences import keiyoushi.utils.getPreferences
import keiyoushi.utils.parseAs
import keiyoushi.utils.toJsonString
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -54,13 +53,15 @@ class MangaPark(
private val apiUrl = "$baseUrl/apo/" private val apiUrl = "$baseUrl/apo/"
private val json: Json by injectLazy() override val client = network.cloudflareClient.newBuilder().apply {
if (preference.getBoolean(ENABLE_NSFW, true)) {
override val client = network.cloudflareClient.newBuilder() addInterceptor(::siteSettingsInterceptor)
.addInterceptor(::siteSettingsInterceptor) addNetworkInterceptor(CookieInterceptor(domain, "nsfw" to "2"))
.addNetworkInterceptor(CookieInterceptor(domain, "nsfw" to "2")) }
.rateLimitHost(apiUrl.toHttpUrl(), 1) rateLimitHost(apiUrl.toHttpUrl(), 1)
.build() // intentionally after rate limit interceptor so thumbnails are not rate limited
addInterceptor(::thumbnailDomainInterceptor)
}.build()
override fun headersBuilder() = super.headersBuilder() override fun headersBuilder() = super.headersBuilder()
.set("Referer", "$baseUrl/") .set("Referer", "$baseUrl/")
@ -96,8 +97,10 @@ class MangaPark(
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
val result = response.parseAs<SearchResponse>() 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 val hasNextPage = entries.size == size
return MangasPage(entries, hasNextPage) return MangasPage(entries, hasNextPage)
@ -164,8 +167,10 @@ class MangaPark(
override fun mangaDetailsParse(response: Response): SManga { override fun mangaDetailsParse(response: Response): SManga {
val result = response.parseAs<DetailsResponse>() 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("#") override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.substringBeforeLast("#")
@ -220,7 +225,7 @@ class MangaPark(
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, _ -> 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 true
} }
}.also(screen::addPreference) }.also(screen::addPreference)
@ -231,16 +236,34 @@ class MangaPark(
summary = "Refresh chapter list to apply changes" summary = "Refresh chapter list to apply changes"
setDefaultValue(false) setDefaultValue(false)
}.also(screen::addPreference) }.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() = 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 cookiesNotSet = AtomicBoolean(true)
private val latch = CountDownLatch(1) private val latch = CountDownLatch(1)
@ -271,6 +294,25 @@ class MangaPark(
return chain.proceed(request) 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 { override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException() throw UnsupportedOperationException()
} }
@ -298,6 +340,11 @@ class MangaPark(
"mpark.to", "mpark.to",
) )
private const val ENABLE_NSFW = "pref_nsfw"
private const val DUPLICATE_CHAPTER_PREF_KEY = "pref_dup_chapters" 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 originalStatus: String? = null,
private val uploadStatus: String? = null, private val uploadStatus: String? = null,
private val summary: String? = null, private val summary: String? = null,
private val extraInfo: String? = null,
@SerialName("urlCoverOri") private val cover: String? = null, @SerialName("urlCoverOri") private val cover: String? = null,
private val urlPath: String, 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" url = "$urlPath#$id"
title = name title = if (shortenTitle) {
thumbnail_url = cover 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() author = authors?.joinToString()
artist = artists?.joinToString() artist = artists?.joinToString()
description = buildString { description = buildString {
val desc = summary?.let { Jsoup.parse(it).text() } if (shortenTitle) {
val names = altNames?.takeUnless { it.isEmpty() } append(name)
?.joinToString("\n") { "${it.trim()}" } append("\n\n")
if (desc.isNullOrEmpty()) {
if (!names.isNullOrEmpty()) {
append("Alternative Names:\n", names)
}
} else {
append(desc)
if (!names.isNullOrEmpty()) {
append("\n\nAlternative Names:\n", names)
} }
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() } genre = genres?.joinToString { it.replace("_", " ").toCamelCase() }
status = when (originalStatus) { status = when (originalStatus ?: uploadStatus) {
"ongoing" -> SManga.ONGOING "ongoing" -> SManga.ONGOING
"completed" -> { "completed" -> {
if (uploadStatus == "ongoing") { if (uploadStatus == "ongoing") {
@ -96,6 +131,14 @@ class MangaParkComic(
} }
return result.toString() 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 originalStatus
uploadStatus uploadStatus
summary summary
extraInfo
urlCoverOri urlCoverOri
urlPath urlPath
max_chapterNode {
data {
imageFile {
urlList
}
}
}
first_chapterNode {
data {
imageFile {
urlList
}
}
}
} }
} }
} }
@ -52,8 +67,23 @@ val DETAILS_QUERY = buildQuery {
originalStatus originalStatus
uploadStatus uploadStatus
summary summary
extraInfo
urlCoverOri urlCoverOri
urlPath urlPath
max_chapterNode {
data {
imageFile {
urlList
}
}
}
first_chapterNode {
data {
imageFile {
urlList
}
}
}
} }
} }
} }