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 {
extName = 'Baozimh.org'
extClass = '.BaozimhOrg'
extVersionCode = 28
extName = 'GoDa'
extClass = '.GoDaManhua'
extVersionCode = 29
}
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
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.Filter
import eu.kanade.tachiyomi.source.model.FilterList
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.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.select.Evaluator
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
import org.jsoup.nodes.Entities
import rx.Observable
// Uses WPManga + GeneratePress/Blocksy Child
class BaozimhOrg : HttpSource(), ConfigurableSource {
open class BaozimhOrg(
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 baseUrl: String
private val baseHttpUrl: HttpUrl
private val enableGenres: Boolean
private val enableGenres = true
init {
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 fun headersBuilder() = super.headersBuilder().add("Referer", "$baseUrl/")
override val client = network.client.newBuilder()
.addInterceptor(UrlInterceptor)
.build()
override val client = network.cloudflareClient
private fun getKey(link: String): String {
val pathSegments = baseHttpUrl.resolve(link)!!.pathSegments
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("/")
return link.substringAfter("/manga/").removeSuffix("/")
}
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 {
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 {
val link = element.selectFirst(Evaluator.Tag("h2"))!!.child(0)
url = getKey(link.attr("href"))
title = link.ownText()
thumbnail_url = element.selectFirst(Evaluator.Tag("img"))!!.imgSrc
val imgSrc = element.selectFirst("img")!!.attr("src")
url = getKey(element.attr("href"))
title = element.selectFirst("h3")!!.ownText()
thumbnail_url = if ("url=" in imgSrc) imgSrc.toHttpUrl().queryParameter("url")!! else imgSrc
}
}
val hasNextPage = document.selectFirst(Evaluator.Class("next"))?.tagName() == "a" ||
document.selectFirst(".gb-button[aria-label=Next page]") != null
val nextPage = if (lang == "zh") "下一頁" else "NEXT"
val hasNextPage = document.selectFirst("a[aria-label=$nextPage] button") != null
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 searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isNotEmpty()) {
val url = "$baseUrl/page/$page/".toHttpUrl().newBuilder()
.addQueryParameter("s", query)
return Request.Builder().url(url.build()).headers(headers).build()
val url = "$baseUrl/s".toHttpUrl().newBuilder()
.addPathSegment(query)
.addEncodedQueryParameter("page", "$page")
.build()
return GET(url, headers)
}
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)
}
override fun searchMangaParse(response: Response) = popularMangaParse(response)
override fun getMangaUrl(manga: SManga) = "$baseUrl/manga/${manga.url}"
override fun mangaDetailsRequest(manga: SManga): Request {
val url = manga.url
if (url[0] == '/') throw Exception(MIGRATE)
return GET("$baseUrl/manga/$url/", headers)
return GET(getMangaUrl(manga), headers)
}
private fun Document.getMangaId() = selectFirst("#mangachapters")!!.attr("data-mid")
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
val document = response.asJsoup()
title = document.selectFirst(Evaluator.Tag("h1"))!!.ownText()
author = document.selectFirst(Evaluator.Class("author-content"))!!.children().joinToString { it.ownText() }
description = document.selectFirst(".descrip_manga_info, .wp-block-stackable-text")!!.text()
thumbnail_url = document.selectFirst("img.wp-post-image")!!.imgSrc
val titleElement = document.selectFirst("h1")!!
val elements = titleElement.parent()!!.parent()!!.children()
check(elements.size == 6)
val genreList = document.selectFirst(Evaluator.Class("genres-content"))!!
.children().eachText().toMutableSet()
if ("连载中" in genreList) {
genreList.remove("连载中")
status = SManga.ONGOING
} else if ("已完结" in genreList) {
genreList.remove("已完结")
status = SManga.COMPLETED
}
genre = genreList.joinToString()
title = titleElement.ownText()
status = SManga.UNKNOWN // Everything is marked as ongoing
author = Entities.unescape(elements[1].children().drop(1).joinToString { it.text().removeSuffix(" ,") })
genre = buildList {
elements[2].children().drop(1).mapTo(this) { it.text().removeSuffix(" ,") }
elements[3].children().mapTo(this) { it.text().removePrefix("#") }
}.joinToString()
description = elements[4].text() + "\n\nID: ${document.getMangaId()}"
thumbnail_url = document.selectFirst("img.object-cover")!!.attr("src")
}
override fun chapterListRequest(manga: SManga): Request {
val url = manga.url
if (url[0] == '/') throw Exception(MIGRATE)
return GET("$baseUrl/chapterlist/$url/", headers)
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.fromCallable {
val mangaId = manga.description
?.substringAfterLast("\nID: ", "")
?.takeIf { it.isNotEmpty() && it.all(Character::isDigit) }
?: client.newCall(mangaDetailsRequest(manga)).execute().asJsoup().getMangaId()
fetchChapterList(mangaId)
}
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
return document.selectFirst(Evaluator.Class("version-chaps"))!!.children().map {
throw UnsupportedOperationException()
}
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 {
url = getKey(it.attr("href"))
name = it.ownText()
date_upload = parseChapterDate(it.child(0).text())
url = getKey(anchor.attr("href")) + "#$mangaId/" + anchor.attr("data-cs")
name = anchor.attr("data-ct")
}
}
}
override fun getChapterUrl(chapter: SChapter) = "$baseUrl/manga/" + chapter.url.substringBeforeLast('#')
override fun pageListRequest(chapter: SChapter): Request {
val url = chapter.url
if (url[0] == '/') throw Exception(MIGRATE)
return GET("$baseUrl/manga/$url/", headers)
val id = chapter.url.substringAfterLast('#', "")
val mangaId = id.substringBefore('/')
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> {
val document = response.asJsoup()
// Jsoup won't ignore duplicates inside <noscript> tag
document.select(Evaluator.Tag("noscript")).remove()
return document.select("img[decoding=async]").mapIndexed { index, element ->
Page(index, imageUrl = element.imgSrc)
return document.select("noscript > img").mapIndexed { index, element ->
Page(index, imageUrl = element.attr("src"))
}
}
@ -156,26 +145,23 @@ class BaozimhOrg : HttpSource(), ConfigurableSource {
private fun parseGenres(document: Document) {
if (!enableGenres || genres.isNotEmpty()) return
val box = document.selectFirst(Evaluator.Class("wp-block-navigation__container")) ?: return
val items = box.children()
genres = buildList(items.size + 1) {
add(Pair("全部", "/allmanga/"))
items.mapTo(this) {
val link = it.child(0)
Pair(link.text(), link.attr("href"))
}
}.toTypedArray()
val box = document.selectFirst("h2")?.parent()?.parent() ?: return
val items = box.select("a")
genres = Array(items.size) { i ->
val item = items[i]
Pair(item.text().removePrefix("#"), item.attr("href"))
}
}
override fun getFilterList(): FilterList =
if (!enableGenres) {
FilterList()
} else if (genres.isEmpty()) {
FilterList(listOf(Filter.Header("点击“重置”刷新分类")))
FilterList(listOf(Filter.Header(if (lang == "zh") "点击“重置”刷新分类" else "Tap 'Reset' to load genres")))
} else {
val list = listOf(
Filter.Header("分类(搜索文本时无效)"),
UriPartFilter("分类", genres),
Filter.Header(if (lang == "zh") "分类(搜索文本时无效)" else "Filters are ignored when using text search."),
UriPartFilter(if (lang == "zh") "分类" else "Genre", genres),
)
FilterList(list)
}
@ -184,33 +170,4 @@ class BaozimhOrg : HttpSource(), ConfigurableSource {
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
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)
}
}