fix(nhentaicom): fix image and language loading errors. (#10412)
* fix(nhentaicom): lang code error * fix(nhentaicom): token parse error * chore(nhentaicom): update version code * fix(nhentaicom): fix 403 error * fix(nhentaicom): Fix HentaiHand image loading when logged in The token interceptor was incorrectly trying to add a token to image requests, which are not on the source's domain, causing image loading to fail when logged in. * fix(nhentai): fix null description * wip: restore code from the crashed rog laptop. * refactor: use @Serializable * fix: parse error * wip: use keiyoushi.utils.parseAs
This commit is contained in:
parent
d4d2785853
commit
6a29aa1afd
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 3
|
||||
baseVersionCode = 4
|
||||
|
||||
@ -0,0 +1,135 @@
|
||||
@file:Suppress("PrivatePropertyName", "PropertyName")
|
||||
|
||||
package eu.kanade.tachiyomi.multisrc.hentaihand
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Created by ipcjs on 2025/9/23.
|
||||
*/
|
||||
@Serializable
|
||||
class ResponseDto<T>(
|
||||
val data: T,
|
||||
val next_page_url: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class LoginResponseDto(val auth: AuthDto) {
|
||||
@Serializable
|
||||
class AuthDto(val access_token: String)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class PageListResponseDto(val images: List<PageDto>) {
|
||||
fun toPageList() = images.map { Page(it.page, "", it.source_url) }
|
||||
|
||||
@Serializable
|
||||
class PageDto(
|
||||
val page: Int,
|
||||
val source_url: String,
|
||||
)
|
||||
}
|
||||
|
||||
typealias ChapterListResponseDto = List<ChapterDto>
|
||||
typealias ChapterResponseDto = ChapterDto
|
||||
|
||||
@Serializable
|
||||
class ChapterDto(
|
||||
private val slug: String,
|
||||
private val name: String?,
|
||||
private val added_at: String?,
|
||||
private val updated_at: String?,
|
||||
) {
|
||||
companion object {
|
||||
private val DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||
}
|
||||
|
||||
private fun parseDate(date: String?): Long =
|
||||
if (date == null) {
|
||||
0
|
||||
} else if (date.contains("day")) {
|
||||
Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, -date.filter { it.isDigit() }.toInt())
|
||||
}.timeInMillis
|
||||
} else {
|
||||
DATE_FORMAT.parse(date)?.time ?: 0
|
||||
}
|
||||
|
||||
fun toSChapter(slug: String) = SChapter.create().also { chapter ->
|
||||
chapter.url = "$slug/${this.slug}"
|
||||
chapter.name = name ?: "Chapter"
|
||||
chapter.date_upload = parseDate(added_at)
|
||||
}
|
||||
|
||||
fun toSChapter() = SChapter.create().also { chapter ->
|
||||
chapter.url = slug
|
||||
chapter.name = "Chapter"
|
||||
chapter.date_upload = parseDate(updated_at)
|
||||
chapter.chapter_number = 1f
|
||||
}
|
||||
}
|
||||
|
||||
typealias MangaDetailsResponseDto = MangaDto
|
||||
|
||||
@Serializable
|
||||
class MangaDto(
|
||||
private val slug: String,
|
||||
private val title: String,
|
||||
private val image_url: String?,
|
||||
private val artists: List<NameDto>?,
|
||||
private val authors: List<NameDto>?,
|
||||
private val tags: List<NameDto>?,
|
||||
private val relationships: List<NameDto>?,
|
||||
private val status: String?,
|
||||
private val alternative_title: String?,
|
||||
private val groups: List<NameDto>?,
|
||||
private val description: String?,
|
||||
private val pages: Int?,
|
||||
private val category: NameDto?,
|
||||
private val language: NameDto?,
|
||||
private val parodies: List<NameDto>?,
|
||||
private val characters: List<NameDto>?,
|
||||
) {
|
||||
fun toSManga() = SManga.create().also { manga ->
|
||||
manga.url = slug.prependIndent("/en/comic/")
|
||||
manga.title = title
|
||||
manga.thumbnail_url = image_url
|
||||
}
|
||||
|
||||
fun toSMangaDetails() = toSManga().also { manga ->
|
||||
manga.artist = artists?.toNames()
|
||||
manga.author = authors?.toNames() ?: manga.artist
|
||||
manga.genre = listOfNotNull(tags, relationships).flatten().toNames()
|
||||
manga.status = when (status) {
|
||||
"complete" -> SManga.COMPLETED
|
||||
"ongoing" -> SManga.ONGOING
|
||||
"onhold" -> SManga.ONGOING
|
||||
"canceled" -> SManga.COMPLETED
|
||||
else -> SManga.COMPLETED
|
||||
}
|
||||
manga.description = listOf(
|
||||
Pair("Alternative Title", alternative_title),
|
||||
Pair("Groups", groups?.toNames()),
|
||||
Pair("Description", description),
|
||||
Pair("Pages", pages?.toString()),
|
||||
Pair("Category", category?.name),
|
||||
Pair("Language", language?.name),
|
||||
Pair("Parodies", parodies?.toNames()),
|
||||
Pair("Characters", characters?.toNames()),
|
||||
).filter { !it.second.isNullOrEmpty() }.joinToString("\n\n") { "${it.first}: ${it.second}" }
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class NameDto(val name: String)
|
||||
|
||||
fun List<NameDto>.toNames() = if (this.isEmpty()) null else this.joinToString { it.name }
|
||||
|
||||
@Serializable
|
||||
class IdDto(val id: String)
|
||||
@ -16,13 +16,8 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import keiyoushi.utils.parseAs
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.put
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
@ -32,10 +27,8 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
abstract class HentaiHand(
|
||||
@ -48,32 +41,15 @@ abstract class HentaiHand(
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private fun slugToUrl(json: JsonObject) = json["slug"]!!.jsonPrimitive.content.prependIndent("/en/comic/")
|
||||
|
||||
private fun jsonArrayToString(arrayKey: String, obj: JsonObject): String? {
|
||||
val array = obj[arrayKey]!!.jsonArray
|
||||
if (array.isEmpty()) return null
|
||||
return array.joinToString(", ") {
|
||||
it.jsonObject["name"]!!.jsonPrimitive.content
|
||||
}
|
||||
}
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
// Popular
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val jsonResponse = json.parseToJsonElement(response.body.string())
|
||||
val mangaList = jsonResponse.jsonObject["data"]!!.jsonArray.map {
|
||||
val obj = it.jsonObject
|
||||
SManga.create().apply {
|
||||
url = slugToUrl(obj)
|
||||
title = obj["title"]!!.jsonPrimitive.content
|
||||
thumbnail_url = obj["image_url"]!!.jsonPrimitive.content
|
||||
}
|
||||
}
|
||||
val hasNextPage = jsonResponse.jsonObject["next_page_url"]!!.jsonPrimitive.content.isNotEmpty()
|
||||
return MangasPage(mangaList, hasNextPage)
|
||||
val resp = response.parseAs<ResponseDto<List<MangaDto>>>()
|
||||
val hasNextPage = !resp.next_page_url.isNullOrEmpty()
|
||||
return MangasPage(resp.data.map { it.toSManga() }, hasNextPage)
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
@ -116,9 +92,7 @@ abstract class HentaiHand(
|
||||
.subscribeOn(Schedulers.io())
|
||||
.map { response ->
|
||||
// Returns the first matched id, or null if there are no results
|
||||
val idList = json.parseToJsonElement(response.body.string()).jsonObject["data"]!!.jsonArray.map {
|
||||
it.jsonObject["id"]!!.jsonPrimitive.content
|
||||
}
|
||||
val idList = response.parseAs<ResponseDto<List<IdDto>>>().data.map { it.id }
|
||||
if (idList.isEmpty()) {
|
||||
return@map null
|
||||
} else {
|
||||
@ -177,33 +151,7 @@ abstract class HentaiHand(
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val obj = json.parseToJsonElement(response.body.string()).jsonObject
|
||||
return SManga.create().apply {
|
||||
url = slugToUrl(obj)
|
||||
title = obj["title"]!!.jsonPrimitive.content
|
||||
thumbnail_url = obj["image_url"]!!.jsonPrimitive.content
|
||||
artist = jsonArrayToString("artists", obj)
|
||||
author = jsonArrayToString("authors", obj) ?: artist
|
||||
genre = listOfNotNull(jsonArrayToString("tags", obj), jsonArrayToString("relationships", obj)).joinToString(", ")
|
||||
status = when (obj["status"]!!.jsonPrimitive.content) {
|
||||
"complete" -> SManga.COMPLETED
|
||||
"ongoing" -> SManga.ONGOING
|
||||
"onhold" -> SManga.ONGOING
|
||||
"canceled" -> SManga.COMPLETED
|
||||
else -> SManga.COMPLETED
|
||||
}
|
||||
|
||||
description = listOf(
|
||||
Pair("Alternative Title", obj["alternative_title"]!!.jsonPrimitive.content),
|
||||
Pair("Groups", jsonArrayToString("groups", obj)),
|
||||
Pair("Description", obj["description"]!!.jsonPrimitive.content),
|
||||
Pair("Pages", obj["pages"]!!.jsonPrimitive.content),
|
||||
Pair("Category", try { obj["category"]!!.jsonObject["name"]!!.jsonPrimitive.content } catch (_: Exception) { null }),
|
||||
Pair("Language", try { obj["language"]!!.jsonObject["name"]!!.jsonPrimitive.content } catch (_: Exception) { null }),
|
||||
Pair("Parodies", jsonArrayToString("parodies", obj)),
|
||||
Pair("Characters", jsonArrayToString("characters", obj)),
|
||||
).filter { !it.second.isNullOrEmpty() }.joinToString("\n\n") { "${it.first}: ${it.second}" }
|
||||
}
|
||||
return response.parseAs<MangaDetailsResponseDto>().toSMangaDetails()
|
||||
}
|
||||
|
||||
// Chapters
|
||||
@ -220,40 +168,13 @@ abstract class HentaiHand(
|
||||
override fun chapterListRequest(manga: SManga): Request = chapterListApiRequest(manga)
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val slug = response.request.url.toString().substringAfter("/api/comics/").removeSuffix("/chapters")
|
||||
return if (chapters) {
|
||||
val array = json.parseToJsonElement(response.body.string()).jsonArray
|
||||
array.map {
|
||||
SChapter.create().apply {
|
||||
url = "$slug/${it.jsonObject["slug"]!!.jsonPrimitive.content}"
|
||||
name = it.jsonObject["name"]!!.jsonPrimitive.content
|
||||
val date = it.jsonObject["added_at"]!!.jsonPrimitive.content
|
||||
date_upload = if (date.contains("day")) {
|
||||
Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, -date.filter { it.isDigit() }.toInt())
|
||||
}.timeInMillis
|
||||
} else {
|
||||
DATE_FORMAT.parse(it.jsonObject["added_at"]!!.jsonPrimitive.content)?.time ?: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
return if (this.chapters) {
|
||||
val slug = response.request.url.toString()
|
||||
.substringAfter("/api/comics/")
|
||||
.removeSuffix("/chapters")
|
||||
response.parseAs<ChapterListResponseDto>().map { it.toSChapter(slug) }
|
||||
} else {
|
||||
val obj = json.parseToJsonElement(response.body.string()).jsonObject
|
||||
listOf(
|
||||
SChapter.create().apply {
|
||||
url = obj["slug"]!!.jsonPrimitive.content
|
||||
name = "Chapter"
|
||||
val date = obj.jsonObject["uploaded_at"]!!.jsonPrimitive.content
|
||||
date_upload = if (date.contains("day")) {
|
||||
Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, -date.filter { it.isDigit() }.toInt())
|
||||
}.timeInMillis
|
||||
} else {
|
||||
DATE_FORMAT.parse(obj.jsonObject["uploaded_at"]!!.jsonPrimitive.content)?.time ?: 0
|
||||
}
|
||||
chapter_number = 1f
|
||||
},
|
||||
)
|
||||
listOf(response.parseAs<ChapterResponseDto>().toSChapter())
|
||||
}
|
||||
}
|
||||
|
||||
@ -264,13 +185,7 @@ abstract class HentaiHand(
|
||||
return GET("$baseUrl/api/comics/$slug/images")
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> =
|
||||
json.parseToJsonElement(response.body.string()).jsonObject["images"]!!.jsonArray.map {
|
||||
val imgObj = it.jsonObject
|
||||
val index = imgObj["page"]!!.jsonPrimitive.int
|
||||
val imgUrl = imgObj["source_url"]!!.jsonPrimitive.content
|
||||
Page(index, "", imgUrl)
|
||||
}
|
||||
override fun pageListParse(response: Response): List<Page> = response.parseAs<PageListResponseDto>().toPageList()
|
||||
|
||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
||||
|
||||
@ -278,7 +193,10 @@ abstract class HentaiHand(
|
||||
|
||||
protected fun authIntercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
if (username.isEmpty() or password.isEmpty()) {
|
||||
if (username.isEmpty() or password.isEmpty()
|
||||
// image request doesn't need token
|
||||
or !request.url.toString().startsWith(baseUrl)
|
||||
) {
|
||||
return chain.proceed(request)
|
||||
}
|
||||
|
||||
@ -304,7 +222,7 @@ abstract class HentaiHand(
|
||||
}
|
||||
try {
|
||||
// Returns access token as a string, unless unparseable
|
||||
return json.parseToJsonElement(response.body.string()).jsonObject["auth"]!!.jsonObject["access-token"]!!.jsonPrimitive.content
|
||||
return response.parseAs<LoginResponseDto>().auth.access_token
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw IOException("Cannot parse login response body")
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ ext {
|
||||
extClass = '.NHentaiComFactory'
|
||||
themePkg = 'hentaihand'
|
||||
baseUrl = 'https://nhentai.com'
|
||||
overrideVersionCode = 4
|
||||
overrideVersionCode = 5
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
||||
@ -9,39 +9,27 @@ class NHentaiComFactory : SourceFactory {
|
||||
override fun createSources(): List<Source> = listOf(
|
||||
// https://nhentai.com/api/languages?per_page=50
|
||||
NHentaiComAll(),
|
||||
NHentaiComEn(),
|
||||
NHentaiComZh(),
|
||||
NHentaiComEn(),
|
||||
NHentaiComJa(),
|
||||
NHentaiComNoText(),
|
||||
NHentaiComEo(),
|
||||
NHentaiComCeb(),
|
||||
NHentaiComCs(),
|
||||
NHentaiComAr(),
|
||||
NHentaiComSk(),
|
||||
NHentaiComMn(),
|
||||
NHentaiComUk(),
|
||||
NHentaiComLa(),
|
||||
NHentaiComTl(),
|
||||
NHentaiComEs(),
|
||||
NHentaiComIt(),
|
||||
NHentaiComKo(),
|
||||
NHentaiComTh(),
|
||||
NHentaiComPl(),
|
||||
NHentaiComFr(),
|
||||
NHentaiComPtBr(),
|
||||
NHentaiComDe(),
|
||||
NHentaiComFi(),
|
||||
NHentaiComRu(),
|
||||
NHentaiComHu(),
|
||||
NHentaiComId(),
|
||||
NHentaiComVi(),
|
||||
NHentaiComNl(),
|
||||
NHentaiComTr(),
|
||||
NHentaiComEl(),
|
||||
NHentaiComBg(),
|
||||
NHentaiComSr(),
|
||||
NHentaiComJv(),
|
||||
NHentaiComHi(),
|
||||
NHentaiComBg(),
|
||||
NHentaiComCs(),
|
||||
NHentaiComUk(),
|
||||
NHentaiComSk(),
|
||||
NHentaiComEo(),
|
||||
NHentaiComMn(),
|
||||
NHentaiComLa(),
|
||||
NHentaiComCeb(),
|
||||
NHentaiComTl(),
|
||||
NHentaiComFi(),
|
||||
NHentaiComTr(),
|
||||
NHentaiComSr(),
|
||||
NHentaiComEl(),
|
||||
NHentaiComKo(),
|
||||
NHentaiComRo(),
|
||||
)
|
||||
}
|
||||
abstract class NHentaiComCommon(
|
||||
@ -58,42 +46,30 @@ class NHentaiComAll : NHentaiComCommon("all") {
|
||||
override val id: Long = 9165839893600661480
|
||||
}
|
||||
|
||||
class NHentaiComJa : NHentaiComCommon("ja", listOf(1, 29))
|
||||
class NHentaiComEn : NHentaiComCommon("en", listOf(2, 27)) {
|
||||
class NHentaiComZh : NHentaiComCommon("zh", listOf(1))
|
||||
class NHentaiComEn : NHentaiComCommon("en", listOf(2)) {
|
||||
override val id: Long = 5591830863732393712
|
||||
}
|
||||
class NHentaiComZh : NHentaiComCommon("zh", listOf(3, 50))
|
||||
class NHentaiComBg : NHentaiComCommon("bg", listOf(4))
|
||||
class NHentaiComCeb : NHentaiComCommon("ceb", listOf(5, 44))
|
||||
class NHentaiComNoText : NHentaiComCommon("other", listOf(6)) {
|
||||
class NHentaiComJa : NHentaiComCommon("ja", listOf(3))
|
||||
class NHentaiComNoText : NHentaiComCommon("other", listOf(4)) {
|
||||
override val id: Long = 5817327335315373850
|
||||
}
|
||||
class NHentaiComTl : NHentaiComCommon("tl", listOf(7, 55))
|
||||
class NHentaiComAr : NHentaiComCommon("ar", listOf(8, 49))
|
||||
class NHentaiComEl : NHentaiComCommon("el", listOf(9))
|
||||
class NHentaiComSr : NHentaiComCommon("sr", listOf(10))
|
||||
class NHentaiComJv : NHentaiComCommon("jv", listOf(11, 51))
|
||||
class NHentaiComUk : NHentaiComCommon("uk", listOf(12, 46))
|
||||
class NHentaiComTr : NHentaiComCommon("tr", listOf(13, 41))
|
||||
class NHentaiComFi : NHentaiComCommon("fi", listOf(14, 54))
|
||||
class NHentaiComLa : NHentaiComCommon("la", listOf(15))
|
||||
class NHentaiComMn : NHentaiComCommon("mn", listOf(16))
|
||||
class NHentaiComEo : NHentaiComCommon("eo", listOf(17, 47))
|
||||
class NHentaiComSk : NHentaiComCommon("sk", listOf(18))
|
||||
class NHentaiComCs : NHentaiComCommon("cs", listOf(19, 52)) {
|
||||
class NHentaiComAr : NHentaiComCommon("ar", listOf(5))
|
||||
class NHentaiComJv : NHentaiComCommon("jv", listOf(6))
|
||||
class NHentaiComBg : NHentaiComCommon("bg", listOf(7))
|
||||
class NHentaiComCs : NHentaiComCommon("cs", listOf(8)) {
|
||||
override val id: Long = 1144495813995437124
|
||||
}
|
||||
class NHentaiComKo : NHentaiComCommon("ko", listOf(30, 39))
|
||||
class NHentaiComRu : NHentaiComCommon("ru", listOf(31))
|
||||
class NHentaiComIt : NHentaiComCommon("it", listOf(32))
|
||||
class NHentaiComEs : NHentaiComCommon("es", listOf(33, 37))
|
||||
class NHentaiComPtBr : NHentaiComCommon("pt-BR", listOf(34))
|
||||
class NHentaiComTh : NHentaiComCommon("th", listOf(35, 40))
|
||||
class NHentaiComFr : NHentaiComCommon("fr", listOf(36))
|
||||
class NHentaiComId : NHentaiComCommon("id", listOf(38))
|
||||
class NHentaiComVi : NHentaiComCommon("vi", listOf(42))
|
||||
class NHentaiComDe : NHentaiComCommon("de", listOf(43))
|
||||
class NHentaiComPl : NHentaiComCommon("pl", listOf(45))
|
||||
class NHentaiComHu : NHentaiComCommon("hu", listOf(48))
|
||||
class NHentaiComNl : NHentaiComCommon("nl", listOf(53))
|
||||
class NHentaiComHi : NHentaiComCommon("hi", listOf(56))
|
||||
class NHentaiComUk : NHentaiComCommon("uk", listOf(9))
|
||||
class NHentaiComSk : NHentaiComCommon("sk", listOf(10))
|
||||
class NHentaiComEo : NHentaiComCommon("eo", listOf(11))
|
||||
class NHentaiComMn : NHentaiComCommon("mn", listOf(12))
|
||||
class NHentaiComLa : NHentaiComCommon("la", listOf(13))
|
||||
class NHentaiComCeb : NHentaiComCommon("ceb", listOf(14))
|
||||
class NHentaiComTl : NHentaiComCommon("tl", listOf(15))
|
||||
class NHentaiComFi : NHentaiComCommon("fi", listOf(16))
|
||||
class NHentaiComTr : NHentaiComCommon("tr", listOf(17))
|
||||
class NHentaiComSr : NHentaiComCommon("sr", listOf(18))
|
||||
class NHentaiComEl : NHentaiComCommon("el", listOf(19))
|
||||
class NHentaiComKo : NHentaiComCommon("ko", listOf(20))
|
||||
class NHentaiComRo : NHentaiComCommon("ro", listOf(21))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user