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:
@@ -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,14 +197,23 @@ 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(
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -28,15 +28,13 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
var favoritesCount: Long? = null
|
||||
|
||||
var mediaId: String? = null
|
||||
var mediaServer: Int? = null
|
||||
|
||||
var japaneseTitle by titleDelegate(TITLE_TYPE_JAPANESE)
|
||||
var englishTitle by titleDelegate(TITLE_TYPE_ENGLISH)
|
||||
var shortTitle by titleDelegate(TITLE_TYPE_SHORT)
|
||||
|
||||
var coverImageType: String? = null
|
||||
var pageImageTypes: List<String> = emptyList()
|
||||
var thumbnailImageType: String? = null
|
||||
var coverImageUrl: String? = null
|
||||
var pageImagePreviewUrls: List<String> = emptyList()
|
||||
|
||||
var scanlator: String? = null
|
||||
|
||||
@@ -45,14 +43,6 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
override fun createMangaInfo(manga: SManga): SManga {
|
||||
val key = nhId?.let { nhIdToPath(it) }
|
||||
|
||||
val cover = if (mediaId != null) {
|
||||
typeToExtension(coverImageType)?.let {
|
||||
"https://t${mediaServer ?: 1}.nhentai.net/galleries/$mediaId/cover.$it"
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val title = when (preferredTitle) {
|
||||
TITLE_TYPE_SHORT -> shortTitle ?: englishTitle ?: japaneseTitle ?: manga.title
|
||||
0, TITLE_TYPE_ENGLISH -> englishTitle ?: japaneseTitle ?: shortTitle ?: manga.title
|
||||
@@ -85,7 +75,7 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
|
||||
return manga.copy(
|
||||
url = key ?: manga.url,
|
||||
thumbnail_url = cover ?: manga.thumbnail_url,
|
||||
thumbnail_url = coverImageUrl ?: manga.thumbnail_url,
|
||||
title = title,
|
||||
artist = group ?: manga.artist,
|
||||
author = artist ?: manga.artist,
|
||||
@@ -113,9 +103,8 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
getItem(japaneseTitle) { stringResource(SYMR.strings.japanese_title) },
|
||||
getItem(englishTitle) { stringResource(SYMR.strings.english_title) },
|
||||
getItem(shortTitle) { stringResource(SYMR.strings.short_title) },
|
||||
getItem(coverImageType) { stringResource(SYMR.strings.cover_image_file_type) },
|
||||
getItem(pageImageTypes.size) { stringResource(SYMR.strings.page_count) },
|
||||
getItem(thumbnailImageType) { stringResource(SYMR.strings.thumbnail_image_file_type) },
|
||||
getItem(coverImageUrl) { stringResource(SYMR.strings.thumbnail_url) },
|
||||
getItem(pageImagePreviewUrls.size) { stringResource(SYMR.strings.page_count) },
|
||||
getItem(scanlator) { stringResource(MR.strings.scanlator) },
|
||||
)
|
||||
}
|
||||
@@ -134,15 +123,6 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
private const val NHENTAI_GROUP_NAMESPACE = "group"
|
||||
const val NHENTAI_CATEGORIES_NAMESPACE = "category"
|
||||
|
||||
fun typeToExtension(t: String?) =
|
||||
when (t) {
|
||||
"w" -> "webp"
|
||||
"p" -> "png"
|
||||
"j" -> "jpg"
|
||||
"g" -> "gif"
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun nhUrlToId(url: String) =
|
||||
url.split("/").last { it.isNotBlank() }.toLong()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user