fix(zh/happymh): Fix http 403 in page lists (#19470)

* fix: Fix page list - Set API version

* refactor: Use serializable data classes

* chore: Bump version
This commit is contained in:
Claudemirovsky 2023-12-29 12:20:09 -03:00 committed by GitHub
parent 4f22d3f064
commit 5c71163ef6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 75 additions and 26 deletions

View File

@ -1,11 +1,12 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext { ext {
extName = 'Happymh' extName = 'Happymh'
pkgNameSuffix = 'zh.happymh' pkgNameSuffix = 'zh.happymh'
extClass = '.Happymh' extClass = '.Happymh'
extVersionCode = 5 extVersionCode = 6
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -4,6 +4,9 @@ import android.app.Application
import android.widget.Toast import android.widget.Toast
import androidx.preference.EditTextPreference import androidx.preference.EditTextPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.extension.zh.happymh.dto.ChapterListDto
import eu.kanade.tachiyomi.extension.zh.happymh.dto.PageListResponseDto
import eu.kanade.tachiyomi.extension.zh.happymh.dto.PopularResponseDto
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
@ -14,12 +17,9 @@ 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 kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.boolean import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Headers import okhttp3.Headers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -37,9 +37,12 @@ class Happymh : HttpSource(), ConfigurableSource {
override val client: OkHttpClient = network.cloudflareClient override val client: OkHttpClient = network.cloudflareClient
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun headersBuilder(): Headers.Builder { override fun headersBuilder(): Headers.Builder {
val builder = super.headersBuilder() val builder = super.headersBuilder()
val preferences = Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
val userAgent = preferences.getString(USER_AGENT_PREF, "")!! val userAgent = preferences.getString(USER_AGENT_PREF, "")!!
return if (userAgent.isNotBlank()) { return if (userAgent.isNotBlank()) {
builder.set("User-Agent", userAgent) builder.set("User-Agent", userAgent)
@ -57,17 +60,18 @@ class Happymh : HttpSource(), ConfigurableSource {
} }
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response): MangasPage {
val data = json.parseToJsonElement(response.body.string()).jsonObject["data"]!!.jsonObject val data = response.parseAs<PopularResponseDto>().data
val items = data["items"]!!.jsonArray.map {
val items = data.items.map {
SManga.create().apply { SManga.create().apply {
val obj = it.jsonObject title = it.name
title = obj["name"]!!.jsonPrimitive.content url = it.url
url = "/manga/${obj["manga_code"]!!.jsonPrimitive.content}" thumbnail_url = it.cover
thumbnail_url = obj["cover"]!!.jsonPrimitive.content
} }
} }
val isEnd = data["isEnd"]!!.jsonPrimitive.boolean val hasNextPage = data.isEnd.not()
return MangasPage(items, !isEnd)
return MangasPage(items, hasNextPage)
} }
// Latest // Latest
@ -108,12 +112,12 @@ class Happymh : HttpSource(), ConfigurableSource {
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val comicId = response.request.url.pathSegments.last() val comicId = response.request.url.pathSegments.last()
val document = response.asJsoup() val document = response.asJsoup()
val script = document.selectFirst("mip-data > script:containsData(chapterList)")!!.html() val script = document.selectFirst("mip-data > script:containsData(chapterList)")!!.data()
return json.parseToJsonElement(script).jsonObject["chapterList"]!!.jsonArray.map { return json.decodeFromString<ChapterListDto>(script).chapterList.map {
SChapter.create().apply { SChapter.create().apply {
val chapterId = it.jsonObject["id"]!!.jsonPrimitive.content val chapterId = it.id
url = "/v2.0/apis/manga/read?code=$comicId&cid=$chapterId" url = "/v2.0/apis/manga/read?code=$comicId&cid=$chapterId&v=v2.13"
name = it.jsonObject["chapterName"]!!.jsonPrimitive.content name = it.chapterName
} }
} }
} }
@ -121,18 +125,21 @@ class Happymh : HttpSource(), ConfigurableSource {
// Pages // Pages
override fun pageListRequest(chapter: SChapter): Request { override fun pageListRequest(chapter: SChapter): Request {
val url = baseUrl + chapter.url
// Some chapters return 403 without this header // Some chapters return 403 without this header
val header = headersBuilder().add("X-Requested-With", "XMLHttpRequest").build() val header = headersBuilder()
return GET(baseUrl + chapter.url, header) .add("X-Requested-With", "XMLHttpRequest")
.set("Referer", url)
.build()
return GET(url, header)
} }
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
return json.parseToJsonElement(response.body.string()) return response.parseAs<PageListResponseDto>().data.scans
.jsonObject["data"]!!.jsonObject["scans"]!!.jsonArray
// If n == 1, the image is from next chapter // If n == 1, the image is from next chapter
.filter { it.jsonObject["n"]!!.jsonPrimitive.int == 0 } .filter { it.n == 0 }
.mapIndexed { index, it -> .mapIndexed { index, it ->
Page(index, "", it.jsonObject["url"]!!.jsonPrimitive.content) Page(index, "", it.url)
} }
} }
@ -158,6 +165,10 @@ class Happymh : HttpSource(), ConfigurableSource {
}.let(screen::addPreference) }.let(screen::addPreference)
} }
private inline fun <reified T> Response.parseAs(): T = use {
json.decodeFromStream(it.body.byteStream())
}
companion object { companion object {
private const val USER_AGENT_PREF = "userAgent" private const val USER_AGENT_PREF = "userAgent"
} }

View File

@ -0,0 +1,37 @@
package eu.kanade.tachiyomi.extension.zh.happymh.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
// Popular / Latest / Search pages
@Serializable
data class PopularResponseDto(val data: PopularData)
@Serializable
data class PopularData(val items: List<MangaDto>, val isEnd: Boolean)
@Serializable
data class MangaDto(
val name: String,
@SerialName("manga_code") val code: String,
val cover: String,
) {
val url = "/manga/$code"
}
// Chapters
@Serializable
data class ChapterListDto(val chapterList: List<ChapterDto>) {
@Serializable
data class ChapterDto(val id: String, val chapterName: String)
}
// Pages
@Serializable
data class PageListResponseDto(val data: PageListData)
@Serializable
data class PageListData(val scans: List<PageDto>) {
@Serializable
data class PageDto(val n: Int, val url: String)
}