diff --git a/multisrc/overrides/sinmh/imitui/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/sinmh/imitui/res/mipmap-hdpi/ic_launcher.png index 66eb5b99f..82b26861b 100644 Binary files a/multisrc/overrides/sinmh/imitui/res/mipmap-hdpi/ic_launcher.png and b/multisrc/overrides/sinmh/imitui/res/mipmap-hdpi/ic_launcher.png differ diff --git a/multisrc/overrides/sinmh/imitui/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/sinmh/imitui/res/mipmap-mdpi/ic_launcher.png index 0974201c4..44ea984e3 100644 Binary files a/multisrc/overrides/sinmh/imitui/res/mipmap-mdpi/ic_launcher.png and b/multisrc/overrides/sinmh/imitui/res/mipmap-mdpi/ic_launcher.png differ diff --git a/multisrc/overrides/sinmh/imitui/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/sinmh/imitui/res/mipmap-xhdpi/ic_launcher.png index 9564df603..fd1afe84f 100644 Binary files a/multisrc/overrides/sinmh/imitui/res/mipmap-xhdpi/ic_launcher.png and b/multisrc/overrides/sinmh/imitui/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/sinmh/imitui/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/sinmh/imitui/res/mipmap-xxhdpi/ic_launcher.png index 11a0425ca..ddcba2cec 100644 Binary files a/multisrc/overrides/sinmh/imitui/res/mipmap-xxhdpi/ic_launcher.png and b/multisrc/overrides/sinmh/imitui/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/sinmh/imitui/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/sinmh/imitui/res/mipmap-xxxhdpi/ic_launcher.png index 49a80c5e5..6eca42c6c 100644 Binary files a/multisrc/overrides/sinmh/imitui/res/mipmap-xxxhdpi/ic_launcher.png and b/multisrc/overrides/sinmh/imitui/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/sinmh/imitui/res/web_hi_res_512.png b/multisrc/overrides/sinmh/imitui/res/web_hi_res_512.png index cb3aa0a47..5e56f22d1 100644 Binary files a/multisrc/overrides/sinmh/imitui/res/web_hi_res_512.png and b/multisrc/overrides/sinmh/imitui/res/web_hi_res_512.png differ diff --git a/multisrc/overrides/sinmh/imitui/src/Imitui.kt b/multisrc/overrides/sinmh/imitui/src/Imitui.kt index 673351963..4833bbf31 100644 --- a/multisrc/overrides/sinmh/imitui/src/Imitui.kt +++ b/multisrc/overrides/sinmh/imitui/src/Imitui.kt @@ -1,17 +1,31 @@ package eu.kanade.tachiyomi.extension.zh.imitui 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.SChapter +import eu.kanade.tachiyomi.util.asJsoup import org.jsoup.nodes.Document +import rx.Observable class Imitui : SinMH("爱米推漫画", "https://www.imitui.com") { - override fun pageListParse(document: Document): List { + override fun fetchPageList(chapter: SChapter): Observable> = + 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 { val pageCount = document.select("div.image-content > p").text().removePrefix("1/").toInt() val prefix = document.location().removeSuffix(".html") return (0 until pageCount).map { Page(it, url = "$prefix-${it + 1}.html") } } + // mobile override fun imageUrlParse(document: Document): String = document.select("div.image-content > img#image").attr("src") } diff --git a/multisrc/overrides/sinmh/manhuadui/src/YKMH.kt b/multisrc/overrides/sinmh/manhuadui/src/YKMH.kt index 80f73b304..275138de3 100644 --- a/multisrc/overrides/sinmh/manhuadui/src/YKMH.kt +++ b/multisrc/overrides/sinmh/manhuadui/src/YKMH.kt @@ -2,30 +2,13 @@ package eu.kanade.tachiyomi.extension.zh.manhuadui import eu.kanade.tachiyomi.multisrc.sinmh.SinMH import eu.kanade.tachiyomi.source.model.SChapter -import eu.kanade.tachiyomi.source.model.SManga import org.jsoup.nodes.Document class YKMH : SinMH("优酷漫画", "http://www.ykmh.com") { override val id = 1637952806167036168 override val mobileUrl = "http://wap.ykmh.com" - override val comicItemSelector = "li.list-comic" - 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 mangaDetailsParse(document: Document) = mangaDetailsParseDMZJStyle(document, hasBreadcrumb = false) override fun List.sortedDescending() = this } diff --git a/multisrc/overrides/sinmh/qinqin/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/sinmh/qinqin/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..9a6b358b8 Binary files /dev/null and b/multisrc/overrides/sinmh/qinqin/res/mipmap-hdpi/ic_launcher.png differ diff --git a/multisrc/overrides/sinmh/qinqin/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/sinmh/qinqin/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..5b665aae0 Binary files /dev/null and b/multisrc/overrides/sinmh/qinqin/res/mipmap-mdpi/ic_launcher.png differ diff --git a/multisrc/overrides/sinmh/qinqin/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/sinmh/qinqin/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..c3183acf1 Binary files /dev/null and b/multisrc/overrides/sinmh/qinqin/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/sinmh/qinqin/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/sinmh/qinqin/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..9bd1f802b Binary files /dev/null and b/multisrc/overrides/sinmh/qinqin/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/sinmh/qinqin/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/sinmh/qinqin/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..d7eae56f0 Binary files /dev/null and b/multisrc/overrides/sinmh/qinqin/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/sinmh/qinqin/res/web_hi_res_512.png b/multisrc/overrides/sinmh/qinqin/res/web_hi_res_512.png new file mode 100644 index 000000000..1bd1ed09a Binary files /dev/null and b/multisrc/overrides/sinmh/qinqin/res/web_hi_res_512.png differ diff --git a/multisrc/overrides/sinmh/qinqin/src/Qinqin.kt b/multisrc/overrides/sinmh/qinqin/src/Qinqin.kt new file mode 100644 index 000000000..0300da80f --- /dev/null +++ b/multisrc/overrides/sinmh/qinqin/src/Qinqin.kt @@ -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 { + 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)) + } +} diff --git a/multisrc/overrides/sinmh/wuqimanga/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/sinmh/wuqimanga/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..b1f78cba3 Binary files /dev/null and b/multisrc/overrides/sinmh/wuqimanga/res/mipmap-hdpi/ic_launcher.png differ diff --git a/multisrc/overrides/sinmh/wuqimanga/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/sinmh/wuqimanga/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..8959228e3 Binary files /dev/null and b/multisrc/overrides/sinmh/wuqimanga/res/mipmap-mdpi/ic_launcher.png differ diff --git a/multisrc/overrides/sinmh/wuqimanga/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/sinmh/wuqimanga/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..bf6741b83 Binary files /dev/null and b/multisrc/overrides/sinmh/wuqimanga/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/sinmh/wuqimanga/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/sinmh/wuqimanga/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..bd63f2d50 Binary files /dev/null and b/multisrc/overrides/sinmh/wuqimanga/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/sinmh/wuqimanga/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/sinmh/wuqimanga/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..aa1fcec0d Binary files /dev/null and b/multisrc/overrides/sinmh/wuqimanga/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/sinmh/wuqimanga/res/web_hi_res_512.png b/multisrc/overrides/sinmh/wuqimanga/res/web_hi_res_512.png new file mode 100644 index 000000000..b7ed7273d Binary files /dev/null and b/multisrc/overrides/sinmh/wuqimanga/res/web_hi_res_512.png differ diff --git a/multisrc/overrides/sinmh/wuqimanga/src/WuqiManga.kt b/multisrc/overrides/sinmh/wuqimanga/src/WuqiManga.kt new file mode 100644 index 000000000..aa9a9c545 --- /dev/null +++ b/multisrc/overrides/sinmh/wuqimanga/src/WuqiManga.kt @@ -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() + if (isSearch) params.add(query) + filters.filterIsInstance().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.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 { + 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>(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> + 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 +} diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/sinmh/SinMH.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/sinmh/SinMH.kt index 96cd03d18..48aa05893 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/sinmh/SinMH.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/sinmh/SinMH.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.multisrc.sinmh import eu.kanade.tachiyomi.AppInfo 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.FilterList import eu.kanade.tachiyomi.source.model.MangasPage @@ -14,6 +15,7 @@ import okhttp3.Headers import okhttp3.Response import org.jsoup.nodes.Document import org.jsoup.nodes.Element +import org.jsoup.select.Evaluator import java.text.SimpleDateFormat import java.util.Locale @@ -31,17 +33,20 @@ abstract class SinMH( protected open val mobileUrl = _baseUrl.replace("www", "m") override val supportsLatest = true + override val client = network.client.newBuilder().rateLimit(2).build() + override fun headersBuilder(): Headers.Builder = Headers.Builder() .add("Referer", baseUrl) protected open val nextPageSelector = "ul.pagination > li.next:not(.disabled)" - protected open val comicItemSelector = "#contList > li" - protected open val comicItemTitleSelector = "p > a" + protected open val comicItemSelector = "#contList > li, li.list-comic" + protected open val comicItemTitleSelector = "p > a, h3 > a" protected open fun mangaFromElement(element: Element) = SManga.create().apply { val titleElement = element.selectFirst(comicItemTitleSelector) title = titleElement.text() - setUrlWithoutDomain(titleElement.attr("abs:href")) - thumbnail_url = element.selectFirst("img").attr("abs:src") + setUrlWithoutDomain(titleElement.attr("href")) + val image = element.selectFirst(Evaluator.Tag("img")) + thumbnail_url = image.attr("src").ifEmpty { image.attr("data-src") } } // Popular @@ -80,30 +85,61 @@ abstract class SinMH( override fun searchMangaSelector(): String = comicItemSelector override fun searchMangaFromElement(element: Element) = mangaFromElement(element) override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = - if (query.isNotBlank()) { + if (query.isNotEmpty()) { GET("$baseUrl/search/?keywords=$query&page=$page", headers) } else { val categories = filters.filterIsInstance().map { it.toUriPart() } - .filter { it.isNotEmpty() }.joinToString("-") + "-" - GET("$baseUrl/list/$categories/", headers) + .filter { it.isNotEmpty() } + val sort = filters.filterIsInstance().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 override fun mangaDetailsParse(document: Document) = SManga.create().apply { - title = document.selectFirst(".book-title > h1 > span").text() - author = document.selectFirst(".detail-list strong:contains(作者) + a").text() - description = document.selectFirst("#intro-all").text().trim() + title = document.selectFirst(".book-title > h1").text() + val detailsList = document.selectFirst(Evaluator.Class("detail-list")) + author = detailsList.select("strong:contains(作者) ~ *").text() + description = document.selectFirst(Evaluator.Id("intro-all")).text().trim() .removePrefix("漫画简介:").trim() .removePrefix("漫画简介:").trim() // some sources have double prefix - genre = document.selectFirst(".detail-list strong:contains(类型) + a").text() + ", " + - document.select(".breadcrumb-bar a[href*=/list/]").joinToString(", ") { it.text() } - status = when (document.selectFirst(".detail-list strong:contains(状态) + a").text()) { + genre = mangaDetailsParseDefaultGenre(document, detailsList) + status = when (detailsList.selectFirst("strong:contains(状态) + *").text()) { "连载中" -> SManga.ONGOING "已完结" -> SManga.COMPLETED 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().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 @@ -117,7 +153,7 @@ abstract class SinMH( override fun chapterListParse(response: Response): List { val document = response.asJsoup() return document.select(chapterListSelector()).map { chapterFromElement(it) }.sortedDescending().apply { - if (isNewDateLogic) { + if (isNewDateLogic && this.isNotEmpty()) { val date = document.selectFirst(dateSelector).textNodes().last().text() 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 chapterFromElement(element: Element) = SChapter.create().apply { - setUrlWithoutDomain(element.attr("abs:href")) - name = element.text() + setUrlWithoutDomain(element.attr("href")) + val children = element.children() + name = if (children.isEmpty()) element.text() else children[0].text() } // Pages 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 { - val script = document.selectFirst("body > script").html() - val images = script.substringAfter("chapterImages = [\"").substringBefore("\"]").split("\",\"") - val path = script.substringAfter("chapterPath = \"").substringBefore("\";") - // assume cover images are on the page image server - val server = script.substringAfter("pageImage = \"").substringBefore("/images/cover") - return images.mapIndexed { i, image -> - val unescapedImage = image.replace("""\/""", "/") - val imageUrl = if (unescapedImage.startsWith("/")) { - "$server$unescapedImage" - } else { - "$server/$path$unescapedImage" + val script = document.selectFirst("body > script").html().let(::ProgressiveParser) + val images = script.substringBetween("chapterImages = ", ";") + if (images.length <= 2) return emptyList() // [] or "" + val path = script.substringBetween("chapterPath = \"", "\";") + return images.let(::parsePageImages).mapIndexed { i, image -> + val imageUrl = when { + image.startsWith("https://") -> image + image.startsWith("/") -> "$imageHost$image" + else -> "$imageHost/$path$image" } 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 = + if (chapterImages.length > 2) { + chapterImages.run { substring(2, length - 2) }.replace("""\/""", "/").split("\",\"") + } else emptyList() // [] + override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not Used.") protected class UriPartFilter(displayName: String, values: Array, private val uriParts: Array) : @@ -158,38 +218,52 @@ abstract class SinMH( fun toUriPart(): String = uriParts[state] } - protected data class Category(val name: String, val values: Array, val uriParts: Array) { + private class SortFilter : Filter.Select("排序方式", sortNames) { + fun toUriPart(): String = sortKeys[state] + } + + protected class Category(private val name: String, private val values: Array, private val uriParts: Array) { fun toUriPartFilter() = UriPartFilter(name, values, uriParts) } - private lateinit var categories: List + protected var categories: List = emptyList() protected open fun parseCategories(document: Document) { - if (::categories.isInitialized) return - categories = document.selectFirst(".filter-nav").children().map { element -> - val name = element.selectFirst("label").text() - val tags = element.select("a") + if (categories.isNotEmpty()) return + val labelSelector = Evaluator.Tag("label") + val linkSelector = Evaluator.Tag("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 uriParts = tags.map { it.attr("href").removePrefix("/list/").removeSuffix("/") }.toTypedArray() Category(name, values, uriParts) } } - override fun getFilterList() = - if (::categories.isInitialized) FilterList( - Filter.Header("如果使用文本搜索,将会忽略分类筛选"), - *categories.map(Category::toUriPartFilter).toTypedArray() - ) else FilterList( - Filter.Header("点击“重置”即可刷新分类,如果失败,"), - Filter.Header("请尝试重新从图源列表点击进入图源"), - ) + override fun getFilterList(): FilterList { + val list: ArrayList> + if (categories.isNotEmpty()) { + list = ArrayList(categories.size + 2) + with(list) { + add(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) - private val isNewDateLogic = run { - val commitCount = AppInfo.getVersionName().substringAfter('-', "") - if (commitCount.isNotEmpty()) // Preview - commitCount.toInt() >= 4442 - else // Stable - AppInfo.getVersionCode() >= 81 + companion object { + private val DATE_FORMAT by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) } + private val isNewDateLogic = AppInfo.getVersionCode() >= 81 + private val sortNames = arrayOf("按发布排序", "按发布排序(逆序)", "按更新排序", "按更新排序(逆序)", "按点击排序", "按点击排序(逆序)") + private val sortKeys = arrayOf("post/", "-post/", "update/", "-update/", "click/", "-click/") } } diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/sinmh/SinMHGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/sinmh/SinMHGenerator.kt index 486602c1c..ea0bface2 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/sinmh/SinMHGenerator.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/sinmh/SinMHGenerator.kt @@ -6,7 +6,7 @@ import generator.ThemeSourceGenerator class SinMHGenerator : ThemeSourceGenerator { override val themeClass = "SinMH" override val themePkg = "sinmh" - override val baseVersionCode = 4 + override val baseVersionCode = 5 override val sources = listOf( SingleLang( 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", 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 { diff --git a/src/zh/wuqimanga/AndroidManifest.xml b/src/zh/wuqimanga/AndroidManifest.xml deleted file mode 100644 index 30deb7f79..000000000 --- a/src/zh/wuqimanga/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/src/zh/wuqimanga/build.gradle b/src/zh/wuqimanga/build.gradle deleted file mode 100644 index 1624ac982..000000000 --- a/src/zh/wuqimanga/build.gradle +++ /dev/null @@ -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" diff --git a/src/zh/wuqimanga/res/mipmap-hdpi/ic_launcher.png b/src/zh/wuqimanga/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 90438cd32..000000000 Binary files a/src/zh/wuqimanga/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/src/zh/wuqimanga/res/mipmap-mdpi/ic_launcher.png b/src/zh/wuqimanga/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 8363d3ff0..000000000 Binary files a/src/zh/wuqimanga/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/src/zh/wuqimanga/res/mipmap-xhdpi/ic_launcher.png b/src/zh/wuqimanga/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 9d08867db..000000000 Binary files a/src/zh/wuqimanga/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/src/zh/wuqimanga/res/mipmap-xxhdpi/ic_launcher.png b/src/zh/wuqimanga/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index e5401866f..000000000 Binary files a/src/zh/wuqimanga/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/zh/wuqimanga/res/mipmap-xxxhdpi/ic_launcher.png b/src/zh/wuqimanga/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index a3fe98e29..000000000 Binary files a/src/zh/wuqimanga/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/zh/wuqimanga/res/web_hi_res_512.png b/src/zh/wuqimanga/res/web_hi_res_512.png deleted file mode 100644 index 975429ec4..000000000 Binary files a/src/zh/wuqimanga/res/web_hi_res_512.png and /dev/null differ diff --git a/src/zh/wuqimanga/src/eu/kanade/tachiyomi/extension/zh/wuqimanga/WuqiManga.kt b/src/zh/wuqimanga/src/eu/kanade/tachiyomi/extension/zh/wuqimanga/WuqiManga.kt deleted file mode 100644 index 4de12df7f..000000000 --- a/src/zh/wuqimanga/src/eu/kanade/tachiyomi/extension/zh/wuqimanga/WuqiManga.kt +++ /dev/null @@ -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().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 { - val chapters = mutableListOf() - 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 { - 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) - } -}