SinMH: add Qinqin Manhua, rewrite 57Manhua, refactor (#12270)

* SinMH: add Qinqin Manhua and refactor

* minor refactor

* add sorting filter

* some tiny optimizations

* rewrite Wuqi Manhua

* optimize assets of Wuqi Manhua

* [skip ci] rename extension to 57Manhua
This commit is contained in:
stevenyomi 2022-06-22 17:41:55 +08:00 committed by GitHub
parent 0253ff3513
commit 3dc8e83d79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 321 additions and 250 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -1,17 +1,31 @@
package eu.kanade.tachiyomi.extension.zh.imitui package eu.kanade.tachiyomi.extension.zh.imitui
import eu.kanade.tachiyomi.multisrc.sinmh.SinMH import eu.kanade.tachiyomi.multisrc.sinmh.SinMH
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.util.asJsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import rx.Observable
class Imitui : SinMH("爱米推漫画", "https://www.imitui.com") { class Imitui : SinMH("爱米推漫画", "https://www.imitui.com") {
override fun pageListParse(document: Document): List<Page> { override fun fetchPageList(chapter: SChapter): Observable<List<Page>> =
client.newCall(GET(baseUrl + chapter.url, headers)).asObservableSuccess().map {
val pcResult = pageListParse(it)
if (pcResult.isNotEmpty()) return@map pcResult
val response = client.newCall(GET(mobileUrl + chapter.url, headers)).execute()
mobilePageListParse(response.asJsoup())
}
private fun mobilePageListParse(document: Document): List<Page> {
val pageCount = document.select("div.image-content > p").text().removePrefix("1/").toInt() val pageCount = document.select("div.image-content > p").text().removePrefix("1/").toInt()
val prefix = document.location().removeSuffix(".html") val prefix = document.location().removeSuffix(".html")
return (0 until pageCount).map { Page(it, url = "$prefix-${it + 1}.html") } return (0 until pageCount).map { Page(it, url = "$prefix-${it + 1}.html") }
} }
// mobile
override fun imageUrlParse(document: Document): String = override fun imageUrlParse(document: Document): String =
document.select("div.image-content > img#image").attr("src") document.select("div.image-content > img#image").attr("src")
} }

View File

@ -2,30 +2,13 @@ package eu.kanade.tachiyomi.extension.zh.manhuadui
import eu.kanade.tachiyomi.multisrc.sinmh.SinMH import eu.kanade.tachiyomi.multisrc.sinmh.SinMH
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
class YKMH : SinMH("优酷漫画", "http://www.ykmh.com") { class YKMH : SinMH("优酷漫画", "http://www.ykmh.com") {
override val id = 1637952806167036168 override val id = 1637952806167036168
override val mobileUrl = "http://wap.ykmh.com" override val mobileUrl = "http://wap.ykmh.com"
override val comicItemSelector = "li.list-comic" override fun mangaDetailsParse(document: Document) = mangaDetailsParseDMZJStyle(document, hasBreadcrumb = false)
override val comicItemTitleSelector = "h3 > a, p > a"
// DMZJ style
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.selectFirst("h1").text()
val details = document.selectFirst("ul.comic_deCon_liO").children()
author = details[0].selectFirst("a").text()
status = when (details[1].selectFirst("a").text()) {
"连载中" -> SManga.ONGOING
"已完结" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
genre = (details[2].select("a") + details[3].select("a")).joinToString(", ") { it.text() }
description = document.selectFirst("p.comic_deCon_d").text()
thumbnail_url = document.selectFirst("div.comic_i_img > img").attr("src")
}
override fun List<SChapter>.sortedDescending() = this override fun List<SChapter>.sortedDescending() = this
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@ -0,0 +1,27 @@
package eu.kanade.tachiyomi.extension.zh.qinqin
import android.util.Base64
import eu.kanade.tachiyomi.multisrc.sinmh.SinMH
import eu.kanade.tachiyomi.network.GET
import org.jsoup.nodes.Document
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
class Qinqin : SinMH("亲亲漫画", "https://www.acgqd.com") {
override fun popularMangaRequest(page: Int) = GET("$baseUrl/list/post/?page=$page", headers)
override fun mangaDetailsParse(document: Document) = mangaDetailsParseDMZJStyle(document, hasBreadcrumb = true)
// https://www.acgqd.com/js/jmzz20191018.js
override fun parsePageImages(chapterImages: String): List<String> {
val key = SecretKeySpec("cxNB23W8xzKJV26O".toByteArray(), "AES")
val iv = IvParameterSpec("opb4x7z21vg1f3gI".toByteArray())
val result = Cipher.getInstance("AES/CBC/PKCS7Padding").run {
init(Cipher.DECRYPT_MODE, key, iv)
doFinal(Base64.decode(chapterImages, Base64.DEFAULT))
}
return super.parsePageImages(String(result))
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -0,0 +1,146 @@
package eu.kanade.tachiyomi.extension.zh.wuqimanga
import eu.kanade.tachiyomi.multisrc.sinmh.SinMH
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.select.Evaluator
// Memo: the old implementation had a string preference with key "IMAGE_SERVER"
class WuqiManga : SinMH("57漫画", "http://www.wuqimh.net") {
override val nextPageSelector = "span.pager > a:last-child" // in the last page it's a span
override val comicItemSelector = "#contList > li, .book-result > li"
override val comicItemTitleSelector = "p > a, dt > a"
// 人气排序的漫画全是 404所以就用默认的最新发布了
override fun popularMangaRequest(page: Int) = GET("$baseUrl/list/order-id-p-$page", headers)
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/list/order-addtime-p-$page", headers)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val isSearch = query.isNotEmpty()
val params = arrayListOf<String>()
if (isSearch) params.add(query)
filters.filterIsInstance<UriPartFilter>().mapTo(params) { it.toUriPart() }
params.add("p-")
val url = buildString(120) {
append(baseUrl)
append(if (isSearch) "/search/q_" else "/list/")
params.joinTo(this, separator = "-", postfix = page.toString())
}
return GET(url, headers)
}
override fun mangaDetailsParse(document: Document) = super.mangaDetailsParse(document).apply {
val comment = document.selectFirst(".book-title > h2").text()
if (comment.isNotEmpty()) description = "$comment\n\n$description"
}
override fun mangaDetailsParseDefaultGenre(document: Document, detailsList: Element): String =
document.selectFirst("div.crumb").select("a[href^=/list/]")
.map { it.text().removeSuffix("").removeSuffix("漫画") }
.filter { it.isNotEmpty() }.joinToString(", ")
override fun chapterListSelector() = ".chapter-list li > a"
override fun List<SChapter>.sortedDescending() = this
override val dateSelector = ".cont-list dt:contains(更新于) + dd"
override val imageHost: String by lazy {
client.newCall(GET("$baseUrl/templates/wuqi/default/scripts/configs.js", headers)).execute().use {
Regex("""\['(.+?)']""").find(it.body!!.string())!!.groupValues[1].run { "http://$this" }
}
}
override fun pageListRequest(chapter: SChapter) = GET(baseUrl + chapter.url, headers)
// Reference: https://github.com/evanw/packer/blob/master/packer.js
override fun pageListParse(document: Document): List<Page> {
val script = document.selectFirst("body > script").html().let(::ProgressiveParser)
val imageList = script.substringBetween(":[", "]").replace("\\", "")
if (imageList.isEmpty()) return emptyList()
script.consumeUntil("""};',""")
val dictionary = script.substringBetween("'", "'").split('|')
val size = dictionary.size
val unpacked = Regex("""\b\w+\b""").replace(imageList) {
with(it.value) {
val key = parseRadix62()
return@replace if (key < size) dictionary[key] else this
}
}.removeSurrounding("'").split("','")
return unpacked.filterNot { it.endsWith("/ManHuaKu/222.jpg") }.mapIndexed { i, image ->
val imageUrl = if (image.startsWith("http")) image else imageHost + image
Page(i, imageUrl = imageUrl)
}.also { list ->
if (list.isEmpty()) return emptyList()
client.newCall(GET(list[0].imageUrl!!, headers)).execute().use {
if (!it.isSuccessful) throw Exception("该章节的图片加载出错:${it.code}")
}
}
}
override fun parseCategories(document: Document) {
if (categories.isNotEmpty()) return
val labelSelector = Evaluator.Tag("label")
val linkSelector = Evaluator.Tag("a")
val filterMap = LinkedHashMap<String, LinkedHashMap<String, String>>(8)
document.select(Evaluator.Class("filter")).forEach { row ->
val tags = row.select(linkSelector)
if (tags.isEmpty()) return@forEach
val name = row.selectFirst(labelSelector).text().removeSuffix("")
if (!filterMap.containsKey(name)) {
filterMap[name] = LinkedHashMap(tags.size * 2)
}
val tagMap = filterMap[name]!!
for (tag in tags) {
val tagName = tag.text()
if (!tagMap.containsKey(tagName))
tagMap[tagName] = tag.attr("href").removePrefix("/list/").substringBeforeLast("-order-")
}
}
categories = filterMap.map {
val tagMap = it.value
Category(it.key, tagMap.keys.toTypedArray(), tagMap.values.toTypedArray())
}
}
override fun getFilterList(): FilterList {
val list: ArrayList<Filter<*>>
if (categories.isNotEmpty()) {
list = ArrayList(categories.size + 2)
with(list) {
add(Filter.Header("使用文本搜索时,只有地区、年份、字母选项有效"))
categories.forEach { add(it.toUriPartFilter()) }
}
} else {
list = ArrayList(4)
with(list) {
add(Filter.Header("点击“重置”即可刷新分类,如果失败,"))
add(Filter.Header("请尝试重新从图源列表点击进入图源"))
add(Filter.Header("使用文本搜索时,只有地区、年份、字母选项有效"))
}
}
list.add(UriPartFilter("排序方式", sortNames, sortKeys))
return FilterList(list)
}
private val sortNames = arrayOf("最新发布", "最近更新", "人气最旺", "评分最高")
private val sortKeys = arrayOf("order-id", "order-addtime", "order-hits", "order-gold")
}
private fun String.parseRadix62(): Int {
var result = 0
for (char in this) {
result = result * 62 + when {
char <= '9' -> char.code - '0'.code
char >= 'a' -> char.code - 'a'.code + 10
else -> char.code - 'A'.code + 36
}
}
return result
}

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.multisrc.sinmh
import eu.kanade.tachiyomi.AppInfo import eu.kanade.tachiyomi.AppInfo
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit
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
@ -14,6 +15,7 @@ import okhttp3.Headers
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.Element
import org.jsoup.select.Evaluator
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@ -31,17 +33,20 @@ abstract class SinMH(
protected open val mobileUrl = _baseUrl.replace("www", "m") protected open val mobileUrl = _baseUrl.replace("www", "m")
override val supportsLatest = true override val supportsLatest = true
override val client = network.client.newBuilder().rateLimit(2).build()
override fun headersBuilder(): Headers.Builder = Headers.Builder() override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Referer", baseUrl) .add("Referer", baseUrl)
protected open val nextPageSelector = "ul.pagination > li.next:not(.disabled)" protected open val nextPageSelector = "ul.pagination > li.next:not(.disabled)"
protected open val comicItemSelector = "#contList > li" protected open val comicItemSelector = "#contList > li, li.list-comic"
protected open val comicItemTitleSelector = "p > a" protected open val comicItemTitleSelector = "p > a, h3 > a"
protected open fun mangaFromElement(element: Element) = SManga.create().apply { protected open fun mangaFromElement(element: Element) = SManga.create().apply {
val titleElement = element.selectFirst(comicItemTitleSelector) val titleElement = element.selectFirst(comicItemTitleSelector)
title = titleElement.text() title = titleElement.text()
setUrlWithoutDomain(titleElement.attr("abs:href")) setUrlWithoutDomain(titleElement.attr("href"))
thumbnail_url = element.selectFirst("img").attr("abs:src") val image = element.selectFirst(Evaluator.Tag("img"))
thumbnail_url = image.attr("src").ifEmpty { image.attr("data-src") }
} }
// Popular // Popular
@ -80,30 +85,61 @@ abstract class SinMH(
override fun searchMangaSelector(): String = comicItemSelector override fun searchMangaSelector(): String = comicItemSelector
override fun searchMangaFromElement(element: Element) = mangaFromElement(element) override fun searchMangaFromElement(element: Element) = mangaFromElement(element)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
if (query.isNotBlank()) { if (query.isNotEmpty()) {
GET("$baseUrl/search/?keywords=$query&page=$page", headers) GET("$baseUrl/search/?keywords=$query&page=$page", headers)
} else { } else {
val categories = filters.filterIsInstance<UriPartFilter>().map { it.toUriPart() } val categories = filters.filterIsInstance<UriPartFilter>().map { it.toUriPart() }
.filter { it.isNotEmpty() }.joinToString("-") + "-" .filter { it.isNotEmpty() }
GET("$baseUrl/list/$categories/", headers) val sort = filters.filterIsInstance<SortFilter>().firstOrNull()?.toUriPart().orEmpty()
val url = StringBuilder(baseUrl).append("/list/").apply {
categories.joinTo(this, separator = "-", postfix = "-/")
}.append(sort).append("?page=").append(page).toString()
GET(url, headers)
} }
// Details // Details
override fun mangaDetailsParse(document: Document) = SManga.create().apply { override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.selectFirst(".book-title > h1 > span").text() title = document.selectFirst(".book-title > h1").text()
author = document.selectFirst(".detail-list strong:contains(作者) + a").text() val detailsList = document.selectFirst(Evaluator.Class("detail-list"))
description = document.selectFirst("#intro-all").text().trim() author = detailsList.select("strong:contains(作者) ~ *").text()
description = document.selectFirst(Evaluator.Id("intro-all")).text().trim()
.removePrefix("漫画简介:").trim() .removePrefix("漫画简介:").trim()
.removePrefix("漫画简介:").trim() // some sources have double prefix .removePrefix("漫画简介:").trim() // some sources have double prefix
genre = document.selectFirst(".detail-list strong:contains(类型) + a").text() + ", " + genre = mangaDetailsParseDefaultGenre(document, detailsList)
document.select(".breadcrumb-bar a[href*=/list/]").joinToString(", ") { it.text() } status = when (detailsList.selectFirst("strong:contains(状态) + *").text()) {
status = when (document.selectFirst(".detail-list strong:contains(状态) + a").text()) {
"连载中" -> SManga.ONGOING "连载中" -> SManga.ONGOING
"已完结" -> SManga.COMPLETED "已完结" -> SManga.COMPLETED
else -> SManga.UNKNOWN else -> SManga.UNKNOWN
} }
thumbnail_url = document.selectFirst("p.cover > img").attr("abs:src") thumbnail_url = document.selectFirst("div.book-cover img").attr("src")
}
protected open fun mangaDetailsParseDefaultGenre(document: Document, detailsList: Element): String {
val category = detailsList.selectFirst("strong:contains(类型) + a")
val breadcrumbs = document.selectFirst("div.breadcrumb-bar").select("a[href^=/list/]")
breadcrumbs.add(0, category)
return breadcrumbs.joinToString(", ") { it.text() }
}
protected fun mangaDetailsParseDMZJStyle(document: Document, hasBreadcrumb: Boolean) = SManga.create().apply {
val detailsDiv = document.selectFirst("div.comic_deCon")
title = detailsDiv.selectFirst(Evaluator.Tag("h1")).text()
val details = detailsDiv.select("> ul > li")
val linkSelector = Evaluator.Tag("a")
author = details[0].selectFirst(linkSelector).text()
status = when (details[1].selectFirst(linkSelector).text()) {
"连载中" -> SManga.ONGOING
"已完结" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
genre = mutableListOf<Element>().apply {
add(details[2].selectFirst(linkSelector)) // 类别
addAll(details[3].select(linkSelector)) // 类型
if (hasBreadcrumb) addAll(document.selectFirst("div.mianbao").select("a[href^=/list/]"))
}.mapTo(mutableSetOf()) { it.text() }.joinToString(", ")
description = detailsDiv.selectFirst("> p.comic_deCon_d").text()
thumbnail_url = document.selectFirst("div.comic_i_img > img").attr("src")
} }
// Chapters // Chapters
@ -117,7 +153,7 @@ abstract class SinMH(
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup() val document = response.asJsoup()
return document.select(chapterListSelector()).map { chapterFromElement(it) }.sortedDescending().apply { return document.select(chapterListSelector()).map { chapterFromElement(it) }.sortedDescending().apply {
if (isNewDateLogic) { if (isNewDateLogic && this.isNotEmpty()) {
val date = document.selectFirst(dateSelector).textNodes().last().text() val date = document.selectFirst(dateSelector).textNodes().last().text()
this[0].date_upload = DATE_FORMAT.parse(date)?.time ?: 0L this[0].date_upload = DATE_FORMAT.parse(date)?.time ?: 0L
} }
@ -126,31 +162,55 @@ abstract class SinMH(
override fun chapterListSelector() = ".chapter-body li > a:not([href^=/comic/app/])" override fun chapterListSelector() = ".chapter-body li > a:not([href^=/comic/app/])"
override fun chapterFromElement(element: Element) = SChapter.create().apply { override fun chapterFromElement(element: Element) = SChapter.create().apply {
setUrlWithoutDomain(element.attr("abs:href")) setUrlWithoutDomain(element.attr("href"))
name = element.text() val children = element.children()
name = if (children.isEmpty()) element.text() else children[0].text()
} }
// Pages // Pages
override fun pageListRequest(chapter: SChapter) = GET(mobileUrl + chapter.url, headers) override fun pageListRequest(chapter: SChapter) = GET(mobileUrl + chapter.url, headers)
protected open val imageHost: String by lazy {
client.newCall(GET("$baseUrl/js/config.js", headers)).execute().use {
Regex("""resHost:.+?"domain":\["(.+?)"""").find(it.body!!.string())!!
.groupValues[1].substringAfter(':').run { "https:$this" }
}
}
// baseUrl/js/common.js/getChapterImage()
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
val script = document.selectFirst("body > script").html() val script = document.selectFirst("body > script").html().let(::ProgressiveParser)
val images = script.substringAfter("chapterImages = [\"").substringBefore("\"]").split("\",\"") val images = script.substringBetween("chapterImages = ", ";")
val path = script.substringAfter("chapterPath = \"").substringBefore("\";") if (images.length <= 2) return emptyList() // [] or ""
// assume cover images are on the page image server val path = script.substringBetween("chapterPath = \"", "\";")
val server = script.substringAfter("pageImage = \"").substringBefore("/images/cover") return images.let(::parsePageImages).mapIndexed { i, image ->
return images.mapIndexed { i, image -> val imageUrl = when {
val unescapedImage = image.replace("""\/""", "/") image.startsWith("https://") -> image
val imageUrl = if (unescapedImage.startsWith("/")) { image.startsWith("/") -> "$imageHost$image"
"$server$unescapedImage" else -> "$imageHost/$path$image"
} else {
"$server/$path$unescapedImage"
} }
Page(i, imageUrl = imageUrl) Page(i, imageUrl = imageUrl)
} }
} }
protected class ProgressiveParser(private val text: String) {
private var startIndex = 0
fun consumeUntil(string: String) = with(text) { startIndex = indexOf(string, startIndex) + string.length }
fun substringBetween(left: String, right: String): String = with(text) {
val leftIndex = indexOf(left, startIndex) + left.length
val rightIndex = indexOf(right, leftIndex)
startIndex = rightIndex + right.length
return substring(leftIndex, rightIndex)
}
}
// default parsing of ["...","..."]
protected open fun parsePageImages(chapterImages: String): List<String> =
if (chapterImages.length > 2) {
chapterImages.run { substring(2, length - 2) }.replace("""\/""", "/").split("\",\"")
} else emptyList() // []
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not Used.") override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not Used.")
protected class UriPartFilter(displayName: String, values: Array<String>, private val uriParts: Array<String>) : protected class UriPartFilter(displayName: String, values: Array<String>, private val uriParts: Array<String>) :
@ -158,38 +218,52 @@ abstract class SinMH(
fun toUriPart(): String = uriParts[state] fun toUriPart(): String = uriParts[state]
} }
protected data class Category(val name: String, val values: Array<String>, val uriParts: Array<String>) { private class SortFilter : Filter.Select<String>("排序方式", sortNames) {
fun toUriPart(): String = sortKeys[state]
}
protected class Category(private val name: String, private val values: Array<String>, private val uriParts: Array<String>) {
fun toUriPartFilter() = UriPartFilter(name, values, uriParts) fun toUriPartFilter() = UriPartFilter(name, values, uriParts)
} }
private lateinit var categories: List<Category> protected var categories: List<Category> = emptyList()
protected open fun parseCategories(document: Document) { protected open fun parseCategories(document: Document) {
if (::categories.isInitialized) return if (categories.isNotEmpty()) return
categories = document.selectFirst(".filter-nav").children().map { element -> val labelSelector = Evaluator.Tag("label")
val name = element.selectFirst("label").text() val linkSelector = Evaluator.Tag("a")
val tags = element.select("a") categories = document.selectFirst(Evaluator.Class("filter-nav")).children().map { element ->
val name = element.selectFirst(labelSelector).text()
val tags = element.select(linkSelector)
val values = tags.map { it.text() }.toTypedArray() val values = tags.map { it.text() }.toTypedArray()
val uriParts = tags.map { it.attr("href").removePrefix("/list/").removeSuffix("/") }.toTypedArray() val uriParts = tags.map { it.attr("href").removePrefix("/list/").removeSuffix("/") }.toTypedArray()
Category(name, values, uriParts) Category(name, values, uriParts)
} }
} }
override fun getFilterList() = override fun getFilterList(): FilterList {
if (::categories.isInitialized) FilterList( val list: ArrayList<Filter<*>>
Filter.Header("如果使用文本搜索,将会忽略分类筛选"), if (categories.isNotEmpty()) {
*categories.map(Category::toUriPartFilter).toTypedArray() list = ArrayList(categories.size + 2)
) else FilterList( with(list) {
Filter.Header("点击“重置”即可刷新分类,如果失败,"), add(Filter.Header("如果使用文本搜索,将会忽略分类筛选"))
Filter.Header("请尝试重新从图源列表点击进入图源"), categories.forEach { add(it.toUriPartFilter()) }
) }
} else {
list = ArrayList(3)
with(list) {
add(Filter.Header("点击“重置”即可刷新分类,如果失败,"))
add(Filter.Header("请尝试重新从图源列表点击进入图源"))
}
}
list.add(SortFilter())
return FilterList(list)
}
private val DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) companion object {
private val isNewDateLogic = run { private val DATE_FORMAT by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) }
val commitCount = AppInfo.getVersionName().substringAfter('-', "") private val isNewDateLogic = AppInfo.getVersionCode() >= 81
if (commitCount.isNotEmpty()) // Preview private val sortNames = arrayOf("按发布排序", "按发布排序(逆序)", "按更新排序", "按更新排序(逆序)", "按点击排序", "按点击排序(逆序)")
commitCount.toInt() >= 4442 private val sortKeys = arrayOf("post/", "-post/", "update/", "-update/", "click/", "-click/")
else // Stable
AppInfo.getVersionCode() >= 81
} }
} }

View File

@ -6,7 +6,7 @@ import generator.ThemeSourceGenerator
class SinMHGenerator : ThemeSourceGenerator { class SinMHGenerator : ThemeSourceGenerator {
override val themeClass = "SinMH" override val themeClass = "SinMH"
override val themePkg = "sinmh" override val themePkg = "sinmh"
override val baseVersionCode = 4 override val baseVersionCode = 5
override val sources = listOf( override val sources = listOf(
SingleLang( SingleLang(
name = "Gufeng Manhua", baseUrl = "https://www.gufengmh9.com", lang = "zh", name = "Gufeng Manhua", baseUrl = "https://www.gufengmh9.com", lang = "zh",
@ -20,6 +20,14 @@ class SinMHGenerator : ThemeSourceGenerator {
name = "YKMH", baseUrl = "http://www.ykmh.com", lang = "zh", className = "YKMH", name = "YKMH", baseUrl = "http://www.ykmh.com", lang = "zh", className = "YKMH",
pkgName = "manhuadui", sourceName = "优酷漫画", overrideVersionCode = 17 pkgName = "manhuadui", sourceName = "优酷漫画", overrideVersionCode = 17
), ),
SingleLang(
name = "Qinqin Manhua", baseUrl = "https://www.acgqd.com", lang = "zh",
className = "Qinqin", sourceName = "亲亲漫画", overrideVersionCode = 0
),
SingleLang(
name = "57Manhua", baseUrl = "http://www.wuqimh.net", lang = "zh",
className = "WuqiManga", sourceName = "57漫画", overrideVersionCode = 3
),
) )
companion object { companion object {

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" />

View File

@ -1,12 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'WuqiManga'
pkgNameSuffix = 'zh.wuqimanga'
extClass = '.WuqiManga'
extVersionCode = 3
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

View File

@ -1,167 +0,0 @@
package eu.kanade.tachiyomi.extension.zh.wuqimanga
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import com.squareup.duktape.Duktape
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class WuqiManga : ParsedHttpSource(), ConfigurableSource {
override val name = "57漫画"
override val baseUrl = "http://www.wuqimh.net"
override val lang = "zh"
override val supportsLatest = true
// Server list can be found from baseUrl/templates/wuqi/default/scripts/configs.js?v=1.0.3
private val imageServer = arrayOf("http://imagesold.benzidui.com", "http://images.tingliu.cc")
private val preferences: SharedPreferences =
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/list/order-addtime-p-$page", headers)
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
override fun latestUpdatesSelector() = popularMangaSelector()
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
override fun popularMangaRequest(page: Int) = GET("$baseUrl/list/order-hits-p-$page", headers)
override fun popularMangaSelector() = "ul#contList > li"
override fun popularMangaNextPageSelector(): String? = "span.pager > a.prev:contains(下一页)"
override fun popularMangaFromElement(element: Element): SManga {
val coverEl = element.select("a img").first()
val cover = if (coverEl.hasAttr("data-src")) {
coverEl.attr("data-src")
} else {
coverEl.attr("src")
}
val title = element.select("a").attr("title")
val url = element.select("a").attr("href")
val manga = SManga.create()
manga.thumbnail_url = cover
manga.title = title
manga.url = url
return manga
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return GET("$baseUrl/search/q_$query-p-$page", headers)
}
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
override fun searchMangaSelector() = "div.book-result li.cf"
override fun searchMangaFromElement(element: Element): SManga {
val manga = SManga.create()
element.select("div.book-detail").first().let {
val titleEl = it.select("dl > dt > a")
manga.setUrlWithoutDomain(titleEl.attr("href"))
manga.title = titleEl.attr("title").trim()
manga.description = it.select("dd.intro").text()
val status = it.select("dd.tags.status")
manga.status = if (status.select("span.red").first().text().contains("连载中")) {
SManga.ONGOING
} else {
SManga.COMPLETED
}
for (el in it.select("dd.tags")) {
if (el.select("span strong").text().contains("作者")) {
manga.author = el.select("span a").text()
}
}
}
manga.thumbnail_url = element.select("a.bcover > img").attr("src")
return manga
}
override fun chapterListSelector() = throw Exception("Not used")
override fun headersBuilder() = Headers.Builder().add("Referer", "$baseUrl/")
.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36")
override fun chapterFromElement(element: Element): SChapter {
val urlElement = element.select("a")
val chapter = SChapter.create()
chapter.url = urlElement.attr("href")
chapter.name = urlElement.attr("alt").trim()
return chapter
}
override fun mangaDetailsParse(document: Document): SManga {
val manga = SManga.create()
manga.description = document.selectFirst("div#intro-all > p").text()
manga.title = document.select(".book-title h1").text().trim()
manga.thumbnail_url = document.select(".hcover img").attr("src")
for (element in document.select("ul.detail-list li span")) {
if (element.select("strong").text().contains("漫画作者")) {
manga.author = element.select("a").text()
break
}
}
return manga
}
override fun imageUrlParse(document: Document): String = ""
override fun chapterListParse(response: Response): List<SChapter> {
val chapters = mutableListOf<SChapter>()
response.asJsoup().select("div.chapter div.chapter-list>ul").asReversed().forEach {
it.select("li a").forEach {
chapters.add(
SChapter.create().apply {
url = it.attr("href")
name = it.attr("title")
}
)
}
}
return chapters
}
override fun pageListParse(document: Document): List<Page> {
val html = document.html()
val packed = Regex("eval(.*?)\\n").find(html)?.groups?.get(1)?.value
val result = Duktape.create().use {
it.evaluate(packed!!) as String
}
val re2 = Regex("""\{.*\}""")
val imgJsonStr = re2.find(result)?.groups?.get(0)?.value
val server = preferences.getString("IMAGE_SERVER", imageServer[0])
// The website is using single quotes in json, so kotlinx.serialization doesn't work
return imgJsonStr!!.substringAfter("'fs':['").substringBefore("'],").split("','").mapIndexed { index, it ->
Page(index, "", if (it.startsWith("http")) it else "$server$it")
}
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = "IMAGE_SERVER"
title = "线路"
summary = "需要清除章节缓存以生效"
entries = arrayOf("主线", "备用")
entryValues = imageServer
setDefaultValue(imageServer[0])
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putString(key, newValue as String).commit()
}
}.let(screen::addPreference)
}
}