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
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 68 KiB |
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 7.6 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 54 KiB |
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 7.7 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 39 KiB |
|
@ -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
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest package="eu.kanade.tachiyomi.extension" />
|
|
|
@ -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"
|
|
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 56 KiB |
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|