Fix Baozimh.org and rename to GoDa Manhua (#3087)

* Fix Baozimh

* Cleanup

* Fix author info

* Use parent request

* Bump versionID

* Update

* Update code
* Rename source to GoDa Manhua and update icon (to distinguish from the "legit" Baozi Manhua)
* Add English source

* Update

* Update

* Keep the Chinese source only for now

* Add comments to explain why the English source is not added

* Error message on old chapter keys

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
This commit is contained in:
Chopper 2024-05-30 02:54:39 -03:00 committed by GitHub
parent 17adef35a2
commit 70592b29ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 234 additions and 157 deletions

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Baozimh.org' extName = 'GoDa'
extClass = '.BaozimhOrg' extClass = '.GoDaManhua'
extVersionCode = 28 extVersionCode = 29
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -1,10 +1,6 @@
package eu.kanade.tachiyomi.extension.zh.baozimhorg package eu.kanade.tachiyomi.extension.zh.baozimhorg
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
@ -13,140 +9,133 @@ 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 okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Entities
import org.jsoup.select.Evaluator import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
// Uses WPManga + GeneratePress/Blocksy Child open class BaozimhOrg(
class BaozimhOrg : HttpSource(), ConfigurableSource { override val name: String,
override val baseUrl: String,
override val lang: String,
) : HttpSource() {
override val name get() = "包子漫画导航"
override val lang get() = "zh"
override val supportsLatest get() = true override val supportsLatest get() = true
override val baseUrl: String private val enableGenres = true
private val baseHttpUrl: HttpUrl
private val enableGenres: Boolean
init { override fun headersBuilder() = super.headersBuilder().add("Referer", "$baseUrl/")
val mirrors = MIRRORS
val mirrorIndex = Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
.getString(MIRROR_PREF, "0")!!.toInt().coerceAtMost(mirrors.size - 1)
baseUrl = "https://" + mirrors[mirrorIndex]
baseHttpUrl = baseUrl.toHttpUrl()
enableGenres = mirrorIndex == 0
}
override val client = network.client.newBuilder() override val client = network.cloudflareClient
.addInterceptor(UrlInterceptor)
.build()
private fun getKey(link: String): String { private fun getKey(link: String): String {
val pathSegments = baseHttpUrl.resolve(link)!!.pathSegments return link.substringAfter("/manga/").removeSuffix("/")
val fromIndex = if (pathSegments[0] == "manga") 1 else 0
val toIndex = if (pathSegments.last().isEmpty()) pathSegments.size - 1 else pathSegments.size
val list = pathSegments.subList(fromIndex, toIndex).toMutableList()
list[0] = list[0].split("-").take(2).joinToString("-")
return list.joinToString("/")
} }
override fun popularMangaRequest(page: Int) = GET("$baseUrl/hots/page/$page/", headers) override fun popularMangaRequest(page: Int) = GET("$baseUrl/hots/page/$page", headers)
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup().also(::parseGenres) val document = response.asJsoup().also(::parseGenres)
val mangas = document.select("article.wp-manga").map { element -> val mangas = document.select(".cardlist .pb-2 a").map { element ->
SManga.create().apply { SManga.create().apply {
val link = element.selectFirst(Evaluator.Tag("h2"))!!.child(0) val imgSrc = element.selectFirst("img")!!.attr("src")
url = getKey(link.attr("href")) url = getKey(element.attr("href"))
title = link.ownText() title = element.selectFirst("h3")!!.ownText()
thumbnail_url = element.selectFirst(Evaluator.Tag("img"))!!.imgSrc thumbnail_url = if ("url=" in imgSrc) imgSrc.toHttpUrl().queryParameter("url")!! else imgSrc
} }
} }
val hasNextPage = document.selectFirst(Evaluator.Class("next"))?.tagName() == "a" || val nextPage = if (lang == "zh") "下一頁" else "NEXT"
document.selectFirst(".gb-button[aria-label=Next page]") != null val hasNextPage = document.selectFirst("a[aria-label=$nextPage] button") != null
return MangasPage(mangas, hasNextPage) return MangasPage(mangas, hasNextPage)
} }
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/newss/page/$page/", headers) override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/newss/page/$page", headers)
override fun latestUpdatesParse(response: Response) = popularMangaParse(response) override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isNotEmpty()) { if (query.isNotEmpty()) {
val url = "$baseUrl/page/$page/".toHttpUrl().newBuilder() val url = "$baseUrl/s".toHttpUrl().newBuilder()
.addQueryParameter("s", query) .addPathSegment(query)
return Request.Builder().url(url.build()).headers(headers).build() .addEncodedQueryParameter("page", "$page")
.build()
return GET(url, headers)
} }
for (filter in filters) { for (filter in filters) {
if (filter is UriPartFilter) return GET(baseUrl + filter.toUriPart() + "page/$page/", headers) if (filter is UriPartFilter) return GET(baseUrl + filter.toUriPart() + "/page/$page", headers)
} }
return popularMangaRequest(page) return popularMangaRequest(page)
} }
override fun searchMangaParse(response: Response) = popularMangaParse(response) override fun searchMangaParse(response: Response) = popularMangaParse(response)
override fun getMangaUrl(manga: SManga) = "$baseUrl/manga/${manga.url}"
override fun mangaDetailsRequest(manga: SManga): Request { override fun mangaDetailsRequest(manga: SManga): Request {
val url = manga.url return GET(getMangaUrl(manga), headers)
if (url[0] == '/') throw Exception(MIGRATE)
return GET("$baseUrl/manga/$url/", headers)
} }
private fun Document.getMangaId() = selectFirst("#mangachapters")!!.attr("data-mid")
override fun mangaDetailsParse(response: Response) = SManga.create().apply { override fun mangaDetailsParse(response: Response) = SManga.create().apply {
val document = response.asJsoup() val document = response.asJsoup()
title = document.selectFirst(Evaluator.Tag("h1"))!!.ownText() val titleElement = document.selectFirst("h1")!!
author = document.selectFirst(Evaluator.Class("author-content"))!!.children().joinToString { it.ownText() } val elements = titleElement.parent()!!.parent()!!.children()
description = document.selectFirst(".descrip_manga_info, .wp-block-stackable-text")!!.text() check(elements.size == 6)
thumbnail_url = document.selectFirst("img.wp-post-image")!!.imgSrc
val genreList = document.selectFirst(Evaluator.Class("genres-content"))!! title = titleElement.ownText()
.children().eachText().toMutableSet() status = SManga.UNKNOWN // Everything is marked as ongoing
if ("连载中" in genreList) { author = Entities.unescape(elements[1].children().drop(1).joinToString { it.text().removeSuffix(" ,") })
genreList.remove("连载中") genre = buildList {
status = SManga.ONGOING elements[2].children().drop(1).mapTo(this) { it.text().removeSuffix(" ,") }
} else if ("已完结" in genreList) { elements[3].children().mapTo(this) { it.text().removePrefix("#") }
genreList.remove("已完结") }.joinToString()
status = SManga.COMPLETED description = elements[4].text() + "\n\nID: ${document.getMangaId()}"
} thumbnail_url = document.selectFirst("img.object-cover")!!.attr("src")
genre = genreList.joinToString()
} }
override fun chapterListRequest(manga: SManga): Request { override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.fromCallable {
val url = manga.url val mangaId = manga.description
if (url[0] == '/') throw Exception(MIGRATE) ?.substringAfterLast("\nID: ", "")
return GET("$baseUrl/chapterlist/$url/", headers) ?.takeIf { it.isNotEmpty() && it.all(Character::isDigit) }
?: client.newCall(mangaDetailsRequest(manga)).execute().asJsoup().getMangaId()
fetchChapterList(mangaId)
} }
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup() throw UnsupportedOperationException()
return document.selectFirst(Evaluator.Class("version-chaps"))!!.children().map { }
open fun fetchChapterList(mangaId: String): List<SChapter> {
val response = client.newCall(GET("$baseUrl/manga/get?mid=$mangaId&mode=all", headers)).execute()
return response.asJsoup().select(".chapteritem").asReversed().map { element ->
val anchor = element.selectFirst("a")!!
SChapter.create().apply { SChapter.create().apply {
url = getKey(it.attr("href")) url = getKey(anchor.attr("href")) + "#$mangaId/" + anchor.attr("data-cs")
name = it.ownText() name = anchor.attr("data-ct")
date_upload = parseChapterDate(it.child(0).text())
} }
} }
} }
override fun getChapterUrl(chapter: SChapter) = "$baseUrl/manga/" + chapter.url.substringBeforeLast('#')
override fun pageListRequest(chapter: SChapter): Request { override fun pageListRequest(chapter: SChapter): Request {
val url = chapter.url val id = chapter.url.substringAfterLast('#', "")
if (url[0] == '/') throw Exception(MIGRATE) val mangaId = id.substringBefore('/')
return GET("$baseUrl/manga/$url/", headers) val chapterId = id.substringAfter('/')
return pageListRequest(mangaId, chapterId)
} }
open fun pageListRequest(mangaId: String, chapterId: String) = GET("$baseUrl/chapter/getcontent?m=$mangaId&c=$chapterId", headers)
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup() val document = response.asJsoup()
// Jsoup won't ignore duplicates inside <noscript> tag return document.select("noscript > img").mapIndexed { index, element ->
document.select(Evaluator.Tag("noscript")).remove() Page(index, imageUrl = element.attr("src"))
return document.select("img[decoding=async]").mapIndexed { index, element ->
Page(index, imageUrl = element.imgSrc)
} }
} }
@ -156,26 +145,23 @@ class BaozimhOrg : HttpSource(), ConfigurableSource {
private fun parseGenres(document: Document) { private fun parseGenres(document: Document) {
if (!enableGenres || genres.isNotEmpty()) return if (!enableGenres || genres.isNotEmpty()) return
val box = document.selectFirst(Evaluator.Class("wp-block-navigation__container")) ?: return val box = document.selectFirst("h2")?.parent()?.parent() ?: return
val items = box.children() val items = box.select("a")
genres = buildList(items.size + 1) { genres = Array(items.size) { i ->
add(Pair("全部", "/allmanga/")) val item = items[i]
items.mapTo(this) { Pair(item.text().removePrefix("#"), item.attr("href"))
val link = it.child(0)
Pair(link.text(), link.attr("href"))
} }
}.toTypedArray()
} }
override fun getFilterList(): FilterList = override fun getFilterList(): FilterList =
if (!enableGenres) { if (!enableGenres) {
FilterList() FilterList()
} else if (genres.isEmpty()) { } else if (genres.isEmpty()) {
FilterList(listOf(Filter.Header("点击“重置”刷新分类"))) FilterList(listOf(Filter.Header(if (lang == "zh") "点击“重置”刷新分类" else "Tap 'Reset' to load genres")))
} else { } else {
val list = listOf( val list = listOf(
Filter.Header("分类(搜索文本时无效)"), Filter.Header(if (lang == "zh") "分类(搜索文本时无效)" else "Filters are ignored when using text search."),
UriPartFilter("分类", genres), UriPartFilter(if (lang == "zh") "分类" else "Genre", genres),
) )
FilterList(list) FilterList(list)
} }
@ -184,33 +170,4 @@ class BaozimhOrg : HttpSource(), ConfigurableSource {
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) { Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second fun toUriPart() = vals[state].second
} }
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
val mirrors = MIRRORS
key = MIRROR_PREF
title = "镜像网址"
summary = "%s\n重启生效暂未适配GoDa漫画的分类筛选功能"
entries = mirrors
entryValues = Array(mirrors.size) { it.toString() }
setDefaultValue("0")
}.let(screen::addPreference)
}
companion object {
private const val MIRROR_PREF = "MIRROR"
private val MIRRORS get() = arrayOf("baozimh.org", "cn.godamanga.com")
const val MIGRATE = "请将此漫画重新迁移到本图源"
val Element.imgSrc: String get() = attr("data-src").ifEmpty { attr("src") }
private val dateFormat by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) }
fun parseChapterDate(text: String): Long = try {
dateFormat.parse(text)!!.time
} catch (_: Throwable) {
0
}
}
} }

View File

@ -0,0 +1,61 @@
package eu.kanade.tachiyomi.extension.zh.baozimhorg
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import kotlinx.serialization.Serializable
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
@Serializable
class ResponseDto<T>(val data: T)
@Serializable
class ChapterListDto(
private val id: Int,
private val slug: String,
private val chapters: List<ChapterDto>,
) {
fun toChapterList(): List<SChapter> {
val mangaId = id.toString()
val mangaSlug = slug
return chapters.asReversed().map { it.toSChapter(mangaSlug, mangaId) }
}
}
@Serializable
class ChapterDto(
private val id: Int,
private val attributes: AttributesDto,
) {
fun toSChapter(mangaSlug: String, mangaId: String) = attributes.toSChapter(mangaSlug, mangaId, id.toString())
}
@Serializable
class AttributesDto(
private val title: String,
private val slug: String,
private val updatedAt: String,
) {
fun toSChapter(mangaSlug: String, mangaId: String, chapterId: String) = SChapter.create().apply {
url = "$mangaSlug/$slug#$mangaId/$chapterId"
name = title
date_upload = dateFormat.parse(updatedAt)!!.time
}
}
// Static field, no need for lazy
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
@Serializable
class PageListDto(val info: PageListInfoDto)
@Serializable
class PageListInfoDto(val images: List<ImageDto>)
@Serializable
class ImageDto(private val url: String, private val order: Int) {
fun toPage() = Page(order, imageUrl = url)
}

View File

@ -0,0 +1,12 @@
package eu.kanade.tachiyomi.extension.zh.baozimhorg
import eu.kanade.tachiyomi.source.SourceFactory
// This is not used because ideally the extension language should be updated to "Multi" (all).
// Chinese users don't receive status updates from Discord, so I'll keep the package name unchanged for now.
class GoDaFactory : SourceFactory {
override fun createSources() = listOf(
GoDaManhua(),
BaozimhOrg("Goda", "https://manhuascans.org", "en"),
)
}

View File

@ -0,0 +1,79 @@
package eu.kanade.tachiyomi.extension.zh.baozimhorg
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import okio.IOException
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class GoDaManhua : BaozimhOrg("GoDa漫画", "", "zh"), ConfigurableSource {
override val id get() = 774030471139699415
override val baseUrl: String
init {
val mirrors = MIRRORS
if (System.getenv("CI") == "true") {
baseUrl = mirrors.joinToString("#, ") { "https://$it" }
} else {
val mirrorIndex = Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
.getString(MIRROR_PREF, "0")!!.toInt().coerceAtMost(mirrors.size - 1)
baseUrl = "https://" + mirrors[mirrorIndex]
}
}
override val client = super.client.newBuilder().addInterceptor(NotFoundInterceptor()).build()
private val json: Json = Injekt.get()
override fun fetchChapterList(mangaId: String): List<SChapter> {
val response = client.newCall(GET("https://api-get.mgsearcher.com/api/manga/get?mid=$mangaId&mode=all", headers)).execute()
return json.decodeFromString<ResponseDto<ChapterListDto>>(response.body.string()).data.toChapterList()
}
override fun pageListRequest(mangaId: String, chapterId: String): Request {
if (mangaId.isEmpty() || chapterId.isEmpty()) throw Exception("请刷新漫画")
return GET("https://api-get.mgsearcher.com/api/chapter/getinfo?m=$mangaId&c=$chapterId", headers)
}
override fun pageListParse(response: Response): List<Page> {
return json.decodeFromString<ResponseDto<PageListDto>>(response.body.string()).data.info.images.map { it.toPage() }
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
val mirrors = MIRRORS
key = MIRROR_PREF
title = "镜像网址"
summary = "%s\n重启生效"
entries = mirrors
entryValues = Array(mirrors.size, Int::toString)
setDefaultValue("0")
}.let(screen::addPreference)
}
}
private const val MIRROR_PREF = "MIRROR"
// https://nav.telltome.net/
private val MIRRORS get() = arrayOf("baozimh.org", "godamh.com", "m.baozimh.one")
private class NotFoundInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
if (response.code != 404) return response
response.close()
throw IOException("请将此漫画重新迁移到本图源")
}
}

View File

@ -1,32 +0,0 @@
package eu.kanade.tachiyomi.extension.zh.baozimhorg
import okhttp3.Interceptor
import okhttp3.Response
// Temporary interceptor to handle URL redirections
object UrlInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val url = request.url
val (type, slug) = url.pathSegments
when (type) {
"manga", "chapterlist" -> {}
else -> return chain.proceed(request)
}
val mangaUrl = "/manga/$slug/"
val headRequest = request.newBuilder()
.head()
.url(url.resolve(mangaUrl)!!)
.build()
// might redirect multiple times
val headResponse = chain.proceed(headRequest)
if (headResponse.priorResponse == null) return chain.proceed(request)
val realSlug = headResponse.request.url.pathSegments[1]
val newUrl = url.newBuilder().setEncodedPathSegment(1, realSlug).build()
val newRequest = request.newBuilder().url(newUrl).build()
return chain.proceed(newRequest)
}
}