Happymh: Fix chapter list parsing (#6572)

* Happymh: Fix chapter list parsing

* Update src/zh/happymh/src/eu/kanade/tachiyomi/extension/zh/happymh/Happymh.kt

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
This commit is contained in:
AlphaBoom 2024-12-12 14:23:53 +08:00 committed by Draff
parent 4cc9b24137
commit eb41438fb3
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
3 changed files with 79 additions and 23 deletions

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Happymh' extName = 'Happymh'
extClass = '.Happymh' extClass = '.Happymh'
extVersionCode = 14 extVersionCode = 15
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -4,7 +4,8 @@ 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.ChapterByPageResponse
import eu.kanade.tachiyomi.extension.zh.happymh.dto.ChapterByPageResponseData
import eu.kanade.tachiyomi.extension.zh.happymh.dto.PageListResponseDto import eu.kanade.tachiyomi.extension.zh.happymh.dto.PageListResponseDto
import eu.kanade.tachiyomi.extension.zh.happymh.dto.PopularResponseDto import eu.kanade.tachiyomi.extension.zh.happymh.dto.PopularResponseDto
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
@ -17,17 +18,18 @@ 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.decodeFromStream import kotlinx.serialization.json.decodeFromStream
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody.Companion.asResponseBody import okhttp3.ResponseBody.Companion.asResponseBody
import rx.Observable
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -38,6 +40,7 @@ class Happymh : HttpSource(), ConfigurableSource {
override val name: String = "嗨皮漫画" override val name: String = "嗨皮漫画"
override val lang: String = "zh" override val lang: String = "zh"
override val supportsLatest: Boolean = true override val supportsLatest: Boolean = true
override val baseUrl: String = "https://m.happymh.com" override val baseUrl: String = "https://m.happymh.com"
private val json: Json by injectLazy() private val json: Json by injectLazy()
@ -54,7 +57,10 @@ class Happymh : HttpSource(), ConfigurableSource {
private val rewriteOctetStream: Interceptor = Interceptor { chain -> private val rewriteOctetStream: Interceptor = Interceptor { chain ->
val originalResponse: Response = chain.proceed(chain.request()) val originalResponse: Response = chain.proceed(chain.request())
if (originalResponse.headers("Content-Type").contains("application/octet-stream") && originalResponse.request.url.toString().contains(".jpg")) { if (originalResponse.headers("Content-Type")
.contains("application/octet-stream") && originalResponse.request.url.toString()
.contains(".jpg")
) {
val orgBody = originalResponse.body.source() val orgBody = originalResponse.body.source()
val newBody = orgBody.asResponseBody("image/jpeg".toMediaType()) val newBody = orgBody.asResponseBody("image/jpeg".toMediaType())
originalResponse.newBuilder() originalResponse.newBuilder()
@ -139,24 +145,62 @@ class Happymh : HttpSource(), ConfigurableSource {
author = document.selectFirst("div.mg-property > p.mg-sub-title:nth-of-type(2)")!!.text() author = document.selectFirst("div.mg-property > p.mg-sub-title:nth-of-type(2)")!!.text()
artist = author artist = author
genre = document.select("div.mg-property > p.mg-cate > a").eachText().joinToString(", ") genre = document.select("div.mg-property > p.mg-cate > a").eachText().joinToString(", ")
description = document.selectFirst("div.manga-introduction > mip-showmore#showmore")!!.text() description =
document.selectFirst("div.manga-introduction > mip-showmore#showmore")!!.text()
} }
// Chapters // Chapters
override fun chapterListParse(response: Response): List<SChapter> { private fun fetchChapterByPage(manga: SManga, page: Int): ChapterByPageResponseData {
val comicId = response.request.url.pathSegments.last() val code = manga.url.substringAfterLast("/")
val document = response.asJsoup() val url = "$baseUrl/v2.0/apis/manga/chapterByPage".toHttpUrl().newBuilder()
val script = document.selectFirst("mip-data > script:containsData(chapterList)")!!.data() .addQueryParameter("code", code)
return json.decodeFromString<ChapterListDto>(script).chapterList.map { .addQueryParameter("lang", "cn")
SChapter.create().apply { .addQueryParameter("order", "asc")
val chapterId = it.id .addQueryParameter("page", "$page")
url = "/reads/$comicId/$chapterId" .build()
name = it.chapterName return client.newCall(GET(url, headers)).execute().parseAs<ChapterByPageResponse>().data
}
private fun fetchChapterByPageObservable(
manga: SManga,
page: Int,
): Observable<ChapterByPageResponseData> {
return Observable.just(fetchChapterByPage(manga, page)).concatMap {
if (it.isEnd == 1 || it.items.isEmpty()) {
Observable.just(it)
} else {
Observable.just(it).concatWith(fetchChapterByPageObservable(manga, page + 1))
} }
} }
} }
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return Observable
.defer {
fetchChapterByPageObservable(manga, 1)
}
.map {
it.items.map {
SChapter.create().apply {
name = it.chapterName
url = "/mangaread/${it.codes}"
chapter_number = it.order.toFloat()
}
}
}
.toList()
.map { it.flatten().sortedByDescending { chapter -> chapter.chapter_number } }
.map {
// remove order mark
it.onEach { chapter ->
chapter.chapter_number = 0f
}
}
}
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
override fun getChapterUrl(chapter: SChapter): String { override fun getChapterUrl(chapter: SChapter): String {
return baseUrl + chapter.url return baseUrl + chapter.url
} }
@ -164,13 +208,12 @@ class Happymh : HttpSource(), ConfigurableSource {
// Pages // Pages
override fun pageListRequest(chapter: SChapter): Request { override fun pageListRequest(chapter: SChapter): Request {
if (chapter.url.startsWith("/v2.0/apis/manga/read")) { if (!chapter.url.contains("mangaread")) {
// Old format is detected // Old format is detected
throw Exception("请刷新章节列表") throw Exception("请刷新章节列表")
} }
val comicId = chapter.url.substringAfter("/reads/").substringBefore("/") val code = chapter.url.substringAfterLast("/")
val chapterId = chapter.url.substringAfterLast("/") val url = "$baseUrl/v2.0/apis/manga/reading?code=$code&v=v3.1818134"
val url = "$baseUrl/v2.0/apis/manga/reading?code=$comicId&cid=$chapterId&v=v3.1613134"
// Some chapters return 403 without this header // Some chapters return 403 without this header
val header = headersBuilder() val header = headersBuilder()
.add("X-Requested-With", "XMLHttpRequest") .add("X-Requested-With", "XMLHttpRequest")
@ -210,7 +253,8 @@ class Happymh : HttpSource(), ConfigurableSource {
Headers.headersOf("User-Agent", newValue as String) Headers.headersOf("User-Agent", newValue as String)
true true
} catch (e: Throwable) { } catch (e: Throwable) {
Toast.makeText(context, "User Agent 无效:${e.message}", Toast.LENGTH_LONG).show() Toast.makeText(context, "User Agent 无效:${e.message}", Toast.LENGTH_LONG)
.show()
false false
} }
} }

View File

@ -21,10 +21,22 @@ data class MangaDto(
// Chapters // Chapters
@Serializable @Serializable
data class ChapterListDto(val chapterList: List<ChapterDto>) { class ChapterByPageResponseDataItem(
@Serializable val chapterName: String,
data class ChapterDto(val id: String, val chapterName: String) val order: Int,
} val codes: String,
)
@Serializable
class ChapterByPageResponseData(
val items: List<ChapterByPageResponseDataItem>,
val isEnd: Int,
)
@Serializable
class ChapterByPageResponse(
val data: ChapterByPageResponseData,
)
// Pages // Pages
@Serializable @Serializable