fix(delegate): migrate NH to the v2 api (#1581)

* fix(delegate): migrate NH to the v2 api

* remove extra comment

* remove redundant data

* linting

* Code cleanup

---------

Co-authored-by: Jobobby04 <jobobby04@users.noreply.github.com>
This commit is contained in:
MediocreLegion
2026-04-03 13:59:13 -03:00
committed by GitHub
parent ee1e783126
commit eec1236b8b
3 changed files with 47 additions and 69 deletions
@@ -29,6 +29,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.CacheControl
import okhttp3.Response
import tachiyomi.core.common.util.lang.withIOContext
class NHentai(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate),
@@ -70,12 +71,8 @@ class NHentai(delegate: HttpSource, val context: Context) :
}
override suspend fun parseIntoMetadata(metadata: NHentaiSearchMetadata, input: Response) {
val body = input.body.string()
val server = MEDIA_SERVER_REGEX.find(body)?.groupValues?.get(1)?.toInt() ?: 1
val json = GALLERY_JSON_REGEX.find(body)!!.groupValues[1].replace(
UNICODE_ESCAPE_REGEX,
) { it.groupValues[1].toInt(radix = 16).toChar().toString() }
val jsonResponse = jsonParser.decodeFromString<JsonResponse>(json)
if (nhConfig == null) getNhConfig()
val jsonResponse = jsonParser.decodeFromString<JsonResponse>(input.body.string())
with(metadata) {
nhId = jsonResponse.id
@@ -86,8 +83,6 @@ class NHentai(delegate: HttpSource, val context: Context) :
mediaId = jsonResponse.mediaId
mediaServer = server
jsonResponse.title?.let { title ->
japaneseTitle = title.japanese
shortTitle = title.pretty
@@ -96,15 +91,11 @@ class NHentai(delegate: HttpSource, val context: Context) :
preferredTitle = this@NHentai.preferredTitle
jsonResponse.images?.let { images ->
coverImageType = images.cover?.type
images.pages.mapNotNull {
it.type
}.let {
pageImageTypes = it
}
thumbnailImageType = images.thumbnail?.type
}
coverImageUrl =
jsonResponse.cover?.path?.let { "$thumbServer/$it" }
?: jsonResponse.thumbnail?.path?.let { "$thumbServer/$it" }
pageImagePreviewUrls = jsonResponse.pages.mapNotNull { it.thumbnail }
scanlator = jsonResponse.scanlator?.trimOrNull()
@@ -125,13 +116,22 @@ class NHentai(delegate: HttpSource, val context: Context) :
}
}
@Serializable
data class JsonConfig(
@SerialName("image_servers")
val imageServers: List<String> = emptyList(),
@SerialName("thumb_servers")
val thumbServers: List<String> = emptyList(),
)
@Serializable
data class JsonResponse(
val id: Long,
@SerialName("media_id")
val mediaId: String? = null,
val title: JsonTitle? = null,
val images: JsonImages? = null,
val cover: JsonPage? = null,
val thumbnail: JsonPage? = null,
val scanlator: String? = null,
@SerialName("upload_date")
val uploadDate: Long? = null,
@@ -140,6 +140,7 @@ class NHentai(delegate: HttpSource, val context: Context) :
val numPages: Int? = null,
@SerialName("num_favorites")
val numFavorites: Long? = null,
val pages: List<JsonPage> = emptyList(),
)
@Serializable
@@ -149,21 +150,12 @@ class NHentai(delegate: HttpSource, val context: Context) :
val pretty: String? = null,
)
@Serializable
data class JsonImages(
val pages: List<JsonPage> = emptyList(),
val cover: JsonPage? = null,
val thumbnail: JsonPage? = null,
)
@Serializable
data class JsonPage(
@SerialName("t")
val type: String? = null,
@SerialName("w")
val path: String? = null,
val width: Long? = null,
@SerialName("h")
val height: Long? = null,
val thumbnail: String? = null,
)
@Serializable
@@ -188,15 +180,16 @@ class NHentai(delegate: HttpSource, val context: Context) :
}
override suspend fun getPagePreviewList(manga: SManga, chapters: List<SChapter>, page: Int): PagePreviewPage {
if (nhConfig == null) getNhConfig()
val metadata = fetchOrLoadMetadata(manga.id()) {
client.newCall(mangaDetailsRequest(manga)).awaitSuccess()
}
return PagePreviewPage(
page,
metadata.pageImageTypes.mapIndexed { index, s ->
metadata.pageImagePreviewUrls.mapIndexed { index, path ->
PagePreviewInfo(
index + 1,
imageUrl = thumbnailUrlFromType(metadata.mediaId!!, metadata.mediaServer ?: 1, index + 1, s)!!,
imageUrl = "$thumbServer/$path",
)
},
false,
@@ -204,15 +197,24 @@ class NHentai(delegate: HttpSource, val context: Context) :
)
}
private fun thumbnailUrlFromType(
mediaId: String,
mediaServer: Int,
page: Int,
t: String,
) = NHentaiSearchMetadata.typeToExtension(t)?.let {
"https://t$mediaServer.nhentai.net/galleries/$mediaId/${page}t.$it"
var nhConfig: JsonConfig? = null
suspend fun getNhConfig() {
try {
val response =
withIOContext { client.newCall(GET("https://nhentai.net/api/v2/config", headers)).awaitSuccess() }
val body = response.body.string()
nhConfig = jsonParser.decodeFromString<JsonConfig>(body)
} catch (_: Exception) {
nhConfig = JsonConfig(
(1..4).map { n -> "https://i$n.nhentai.net" },
(1..4).map { n -> "https://t$n.nhentai.net" },
)
}
}
val thumbServer
get() = nhConfig?.thumbServers?.random()
override suspend fun fetchPreviewImage(page: PagePreviewInfo, cacheControl: CacheControl?): Response {
return client.newCachelessCallWithProgress(
if (cacheControl != null) {
@@ -230,10 +232,6 @@ class NHentai(delegate: HttpSource, val context: Context) :
private val jsonParser = Json {
ignoreUnknownKeys = true
}
private val GALLERY_JSON_REGEX = Regex(".parse\\(\"(.*)\"\\);")
private val MEDIA_SERVER_REGEX = Regex("media_server\\s*:\\s*(\\d+)")
private val UNICODE_ESCAPE_REGEX = Regex("\\\\u([0-9a-fA-F]{4})")
private const val TITLE_PREF = "Display manga title as:"
}
}
@@ -60,8 +60,8 @@ fun NHentaiDescription(state: State.Success, openMetadataViewer: () -> Unit) {
binding.pages.text = context.pluralStringResource(
SYMR.plurals.num_pages,
meta.pageImageTypes.size,
meta.pageImageTypes.size,
meta.pageImagePreviewUrls.size,
meta.pageImagePreviewUrls.size,
)
binding.pages.bindDrawable(context, R.drawable.ic_baseline_menu_book_24)