MDB multisrc theme (#12237)
* MDB multisrc theme * Maofly: optimize assets * ManhuaDB: new icon * minor change (rerun CI) * rename additional gradle file * add rate limit
|
@ -0,0 +1,17 @@
|
||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="MDBGenerator" type="JetRunConfigurationType" nameIsGenerated="true">
|
||||||
|
<module name="tachiyomi-extensions.multisrc" />
|
||||||
|
<option name="VM_PARAMETERS" value="" />
|
||||||
|
<option name="PROGRAM_PARAMETERS" value="" />
|
||||||
|
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||||
|
<option name="ALTERNATIVE_JRE_PATH" />
|
||||||
|
<option name="PASS_PARENT_ENVS" value="true" />
|
||||||
|
<option name="MAIN_CLASS_NAME" value="eu.kanade.tachiyomi.multisrc.mdb.MDBGenerator" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Make" enabled="true" />
|
||||||
|
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktFormat" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="" />
|
||||||
|
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktLint" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 5.8 KiB |
After Width: | Height: | Size: 7.1 KiB |
After Width: | Height: | Size: 26 KiB |
|
@ -0,0 +1,45 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.zh.manhuadb
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import eu.kanade.tachiyomi.multisrc.mdb.MDB
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import org.jsoup.select.Evaluator
|
||||||
|
import org.jsoup.select.QueryParser
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class ManhuaDB : MDB("漫画DB", "https://www.manhuadb.com") {
|
||||||
|
|
||||||
|
override val supportsLatest = false
|
||||||
|
|
||||||
|
override fun listUrl(params: String) = "$baseUrl/manhua/list-$params.html"
|
||||||
|
override fun extractParams(listUrl: String) = listUrl.substringAfter("/list-").removeSuffix(".html")
|
||||||
|
override fun searchUrl(page: Int, query: String) = "$baseUrl/search?q=$query&p=$page"
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = "nav > div.form-inline > :nth-last-child(2):not(.disabled)"
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException("Not used.")
|
||||||
|
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException("Not used.")
|
||||||
|
override fun latestUpdatesSelector() = throw UnsupportedOperationException("Not used.")
|
||||||
|
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException("Not used.")
|
||||||
|
|
||||||
|
override val authorSelector: Evaluator = QueryParser.parse("a.comic-creator")
|
||||||
|
override fun transformDescription(description: String) = description.substringBeforeLast("欢迎在漫画DB观看")
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response) = super.chapterListParse(response).asReversed()
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
// https://www.manhuadb.com/assets/js/vg-read.js
|
||||||
|
override fun parseImages(imgData: String, readerConfig: Element): List<String> {
|
||||||
|
val list: List<JsonObject> = Base64.decode(imgData, Base64.DEFAULT)
|
||||||
|
.let { json.decodeFromString(String(it)) }
|
||||||
|
val host = readerConfig.attr("data-host")
|
||||||
|
val dir = readerConfig.attr("data-img_pre")
|
||||||
|
return list.map { host + dir + it["img"]!!.jsonPrimitive.content }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
dependencies {
|
||||||
|
implementation 'com.github.softwarevidal:lz-string4java:lz-string4java-1.0.0'
|
||||||
|
}
|
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 9.4 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 67 KiB |
|
@ -0,0 +1,57 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.zh.maofly
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.AppInfo
|
||||||
|
import eu.kanade.tachiyomi.multisrc.mdb.MDB
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import org.jsoup.select.Evaluator
|
||||||
|
import org.jsoup.select.QueryParser
|
||||||
|
import rufus.lzstring4java.LZString
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class Maofly : MDB("漫画猫", "https://www.maofly.com") {
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override fun listUrl(params: String) = "$baseUrl/list/$params.html"
|
||||||
|
override fun extractParams(listUrl: String) = listUrl.substringAfter("/list/").removeSuffix(".html")
|
||||||
|
override fun searchUrl(page: Int, query: String) = "$baseUrl/search.html?q=$query&page=$page"
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = "div.pagination > li:last-child" // in the last page it's a span
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/update-page-$page.html", headers)
|
||||||
|
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
override fun latestUpdatesSelector() = searchMangaSelector()
|
||||||
|
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
|
||||||
|
|
||||||
|
override fun transformTitle(title: String) = title.run { substring(1, length - 1) } // 《title》
|
||||||
|
override val authorSelector: Evaluator = QueryParser.parse("td.pub-duration")
|
||||||
|
override fun transformDescription(description: String) =
|
||||||
|
description.substringAfter("的漫画作品。").substringBeforeLast(" 。。欢迎您到漫画猫畅快阅读。")
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
return document.select(chapterListSelector()).map { chapterFromElement(it) }.apply {
|
||||||
|
if (!isNewDateLogic) return@apply
|
||||||
|
this[0].date_upload = document.selectFirst(dateSelector).text()
|
||||||
|
.let { dateFormat.parse(it)!!.time }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.maofly.com/static/js/vg-read-v1.js
|
||||||
|
override fun parseImages(imgData: String, readerConfig: Element): List<String> {
|
||||||
|
val list = LZString.decompressFromBase64(imgData).split(',')
|
||||||
|
val host = readerConfig.attr("data-chapter-domain")
|
||||||
|
return list.map { "$host/uploads/$it" }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val dateSelector = QueryParser.parse("th:contains(上次更新) + td")
|
||||||
|
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH)
|
||||||
|
private val isNewDateLogic = AppInfo.getVersionCode() >= 81
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,197 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.mdb
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
|
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
|
||||||
|
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.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import org.jsoup.select.Evaluator
|
||||||
|
import org.jsoup.select.QueryParser
|
||||||
|
import rx.Observable
|
||||||
|
|
||||||
|
/** ManhuaDB: https://www.manhuadb.com/ */
|
||||||
|
abstract class MDB(
|
||||||
|
override val name: String,
|
||||||
|
override val baseUrl: String,
|
||||||
|
override val lang: String = "zh",
|
||||||
|
) : ParsedHttpSource() {
|
||||||
|
|
||||||
|
override val client = network.client.newBuilder().rateLimit(2).build()
|
||||||
|
|
||||||
|
override fun headersBuilder() = super.headersBuilder().add("Referer", baseUrl)
|
||||||
|
|
||||||
|
protected abstract fun listUrl(params: String): String
|
||||||
|
protected abstract fun extractParams(listUrl: String): String
|
||||||
|
protected abstract fun searchUrl(page: Int, query: String): String
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int) = GET(listUrl("page-$page"), headers)
|
||||||
|
override fun popularMangaSelector() = "div.comic-main-section > div.comic-book-unit"
|
||||||
|
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
||||||
|
val link = element.selectFirst(listComicLinkSelector)
|
||||||
|
setUrlWithoutDomain(link.attr("href"))
|
||||||
|
title = link.text()
|
||||||
|
thumbnail_url = element.selectFirst(imgSelector).absUrl("src")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
parseCategories(document) // parse categories here
|
||||||
|
val mangas = document.select(popularMangaSelector()).map { popularMangaFromElement(it) }
|
||||||
|
val hasNextPage = popularMangaNextPageSelector()?.let { document.selectFirst(it) } != null
|
||||||
|
return MangasPage(mangas, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||||
|
if (query.isNotEmpty()) {
|
||||||
|
val request = GET(searchUrl(page, query), headers)
|
||||||
|
client.newCall(request).asObservableSuccess().map { searchMangaParse(it) }
|
||||||
|
} else {
|
||||||
|
val params = filters.filterIsInstance<CategoryFilter>().map { it.getParam() }
|
||||||
|
.filterTo(mutableListOf()) { it.isNotEmpty() }.apply { add("page-$page") }
|
||||||
|
val request = GET(listUrl(params.joinToString("-")), headers)
|
||||||
|
client.newCall(request).asObservableSuccess().map { popularMangaParse(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
override fun searchMangaSelector() = "div.comic-main-section > div.row > div"
|
||||||
|
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
||||||
|
final override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||||
|
throw UnsupportedOperationException("Not used.")
|
||||||
|
|
||||||
|
protected open fun transformTitle(title: String) = title
|
||||||
|
protected abstract val authorSelector: Evaluator
|
||||||
|
protected open fun transformDescription(description: String) = description
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||||
|
title = document.selectFirst(h1Selector).text().let { transformTitle(it) }
|
||||||
|
author = document.selectFirst(authorSelector).text()
|
||||||
|
description = document.selectFirst(descriptionSelector).text().let { transformDescription(it) }
|
||||||
|
genre = parseGenre(document).joinToString(", ")
|
||||||
|
status = when (document.selectFirst(statusSelector).text()) {
|
||||||
|
"连载中" -> SManga.ONGOING
|
||||||
|
"已完结" -> SManga.COMPLETED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
thumbnail_url = document.selectFirst(coverSelector).attr("src")
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun parseGenre(document: Document): List<String> {
|
||||||
|
val list = mutableListOf<String>()
|
||||||
|
list.add(document.selectFirst(regionSelector).text())
|
||||||
|
list.add(document.selectFirst(audienceSelector).text().removeSuffix("漫画"))
|
||||||
|
val tags = document.select(tagSelector)
|
||||||
|
for (i in 1 until tags.size) { // skip status
|
||||||
|
list.add(tags[i].text())
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "#comic-book-list li > a"
|
||||||
|
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||||
|
setUrlWithoutDomain(element.attr("href"))
|
||||||
|
name = element.attr("title")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
val imgData = document.selectFirst(scriptSelector).data()
|
||||||
|
.substringAfter("img_data = ").run {
|
||||||
|
val endIndex = indexOf(this[0], startIndex = 1) // find end quote
|
||||||
|
substring(1, endIndex)
|
||||||
|
}
|
||||||
|
val readerConfig = document.selectFirst(readerConfigSelector)
|
||||||
|
return parseImages(imgData, readerConfig).mapIndexed { i, it ->
|
||||||
|
Page(i, imageUrl = it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract fun parseImages(imgData: String, readerConfig: Element): List<String>
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not used.")
|
||||||
|
|
||||||
|
protected data class Category(val name: String, val values: Array<String>, val params: List<String>) {
|
||||||
|
fun toFilter() = CategoryFilter(name, values, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected class CategoryFilter(name: String, values: Array<String>, val params: List<String>) :
|
||||||
|
Filter.Select<String>(name, values) {
|
||||||
|
fun getParam() = params[state]
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var categories: List<Category>
|
||||||
|
|
||||||
|
protected open fun parseCategories(document: Document) {
|
||||||
|
if (::categories.isInitialized) return
|
||||||
|
val filters = document.select(filterSelector)
|
||||||
|
val list = ArrayList<Category>(filters.size + 1)
|
||||||
|
for (filter in filters) {
|
||||||
|
val children = filter.children()
|
||||||
|
val filterContainer = children[1]
|
||||||
|
if (filterContainer.hasClass("row")) { // Normal filter
|
||||||
|
val tags = filterContainer.children()
|
||||||
|
val values = ArrayList<String>(tags.size + 1).apply { add("全部") }
|
||||||
|
val params = ArrayList<String>(tags.size + 1).apply { add("") }
|
||||||
|
for (tag in tags) {
|
||||||
|
val link = tag.child(0).child(0)
|
||||||
|
values.add(link.text())
|
||||||
|
params.add(link.attr("href").let(::extractParams).let(::parseParam))
|
||||||
|
}
|
||||||
|
list.add(Category(children[0].selectFirst(spanSelector).text(), values.toTypedArray(), params))
|
||||||
|
} else if (filterContainer.hasClass("form-row")) { // Dropdown filter
|
||||||
|
for (select in filterContainer.select(selectSelector)) {
|
||||||
|
val options = select.children()
|
||||||
|
val values = ArrayList<String>(options.size).apply { add("全部") }
|
||||||
|
val params = ArrayList<String>(options.size).apply { add("") }
|
||||||
|
for (i in 1 until options.size) {
|
||||||
|
values.add(options[i].text())
|
||||||
|
params.add(options[i].attr("value").let(::extractParams).let(::parseParam))
|
||||||
|
}
|
||||||
|
list.add(Category(options[0].text(), values.toTypedArray(), params))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
categories = list
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseParam(params: String): String {
|
||||||
|
val parts = params.split('-')
|
||||||
|
for (i in 1 until parts.size step 2) {
|
||||||
|
if (parts[i] != "0") return "${parts[i - 1]}-${parts[i]}"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilterList() =
|
||||||
|
if (::categories.isInitialized) FilterList(
|
||||||
|
Filter.Header("如果使用文本搜索,将会忽略分类筛选"),
|
||||||
|
*categories.map { it.toFilter() }.toTypedArray()
|
||||||
|
) else FilterList(
|
||||||
|
Filter.Header("点击“重置”即可刷新分类,如果失败,"),
|
||||||
|
Filter.Header("请尝试重新从图源列表点击进入图源"),
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val listComicLinkSelector = QueryParser.parse("h2 > a")
|
||||||
|
private val imgSelector = Evaluator.Tag("img")
|
||||||
|
private val h1Selector = Evaluator.Tag("h1")
|
||||||
|
private val coverSelector = QueryParser.parse("td.comic-cover > img")
|
||||||
|
private val descriptionSelector = QueryParser.parse("p.comic_story")
|
||||||
|
private val tagSelector = QueryParser.parse("ul.tags > li > a")
|
||||||
|
private val statusSelector = QueryParser.parse("a.comic-pub-state")
|
||||||
|
private val regionSelector = QueryParser.parse("th:contains(地区) + td")
|
||||||
|
private val audienceSelector = QueryParser.parse("th:contains(面向读者) + td")
|
||||||
|
private val scriptSelector = QueryParser.parse("body > script:containsData(img_data)")
|
||||||
|
private val readerConfigSelector = Evaluator.Class("vg-r-data")
|
||||||
|
private val filterSelector = QueryParser.parse("div.search_div > div")
|
||||||
|
private val spanSelector = Evaluator.Tag("span")
|
||||||
|
private val selectSelector = Evaluator.Tag("select")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.mdb
|
||||||
|
|
||||||
|
import generator.ThemeSourceData.SingleLang
|
||||||
|
import generator.ThemeSourceGenerator
|
||||||
|
|
||||||
|
class MDBGenerator : ThemeSourceGenerator {
|
||||||
|
override val themeClass = "MDB"
|
||||||
|
override val themePkg = "mdb"
|
||||||
|
override val baseVersionCode = 1
|
||||||
|
override val sources = listOf(
|
||||||
|
SingleLang("ManhuaDB", "https://www.manhuadb.com", "zh", sourceName = "漫画DB", overrideVersionCode = 3),
|
||||||
|
SingleLang("Maofly", "https://www.maofly.com", "zh", sourceName = "漫画猫", overrideVersionCode = 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
MDBGenerator().createAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,2 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest package="eu.kanade.tachiyomi.extension" />
|
|
|
@ -1,11 +0,0 @@
|
||||||
apply plugin: 'com.android.application'
|
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
|
|
||||||
ext {
|
|
||||||
extName = 'ManhuaDB'
|
|
||||||
pkgNameSuffix = 'zh.manhuadb'
|
|
||||||
extClass = '.ManhuaDB'
|
|
||||||
extVersionCode = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 756 KiB |
|
@ -1,111 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.zh.manhuadb
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
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 okhttp3.Headers
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import java.util.regex.Pattern
|
|
||||||
|
|
||||||
class ManhuaDB : ParsedHttpSource() {
|
|
||||||
|
|
||||||
override val baseUrl = "https://www.manhuadb.com"
|
|
||||||
|
|
||||||
override val lang = "zh"
|
|
||||||
|
|
||||||
override val name = "漫画DB"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override fun headersBuilder(): Headers.Builder =
|
|
||||||
super.headersBuilder().add("Referer", "https://www.manhuadb.com")
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
|
||||||
name = element.attr("title")
|
|
||||||
url = element.attr("href")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rewrite the method to ensure consistency with previous format orders
|
|
||||||
*/
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
|
||||||
return super.chapterListParse(response).reversed()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListSelector() = "#comic-book-list > div > ol > li > a"
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document): String {
|
|
||||||
return document.select("div.text-center > img.img-fluid").attr("abs:src")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request = popularMangaRequest(page)
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
|
||||||
title = document.select("h1.comic-title").text()
|
|
||||||
thumbnail_url = document.select("td.comic-cover > img").attr("abs:src")
|
|
||||||
author = document.select("a.comic-creator").text()
|
|
||||||
description = document.select("p.comic_story").text()
|
|
||||||
status = when (document.select("td > a.comic-pub-state").text()) {
|
|
||||||
"连载中" -> SManga.ONGOING
|
|
||||||
"已完结" -> SManga.COMPLETED
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
genre = document.select("ul.tags > li a").joinToString { it.text() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
|
||||||
val pages = mutableListOf<Page>()
|
|
||||||
val pageStr = document.select("ol.breadcrumb > li:eq(2)").text()
|
|
||||||
val pageNumMatcher = Pattern.compile("共\\s*(\\d+)").matcher(pageStr)
|
|
||||||
if (pageNumMatcher.find()) {
|
|
||||||
val page = Integer.parseInt(pageNumMatcher.group(1)!!)
|
|
||||||
var path = document.select("ol.breadcrumb > li:eq(2) > a").attr("href")
|
|
||||||
path = path.substring(1, path.length - 5)
|
|
||||||
for (i in 0 until page)
|
|
||||||
pages.add(Page(i, "$baseUrl/${path}_p${i + 1}.html"))
|
|
||||||
}
|
|
||||||
return pages
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element): SManga {
|
|
||||||
val manga = SManga.create()
|
|
||||||
element.select("h2").first().let {
|
|
||||||
manga.setUrlWithoutDomain(it.select("a").first().attr("href"))
|
|
||||||
manga.title = it.text()
|
|
||||||
}
|
|
||||||
manga.thumbnail_url = element.select("a > img").attr("abs:src")
|
|
||||||
return manga
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "a:contains(下页):not(.disabled)"
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/manhua/list-page-$page.html")
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.comic-book-unit"
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
|
|
||||||
title = element.attr("title")
|
|
||||||
setUrlWithoutDomain(element.attr("href"))
|
|
||||||
thumbnail_url = element.select("img").attr("abs:src")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
return GET("$baseUrl/search?q=$query&p=$page", headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = "a.d-block"
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest package="eu.kanade.tachiyomi.extension" />
|
|
|
@ -1,13 +0,0 @@
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
|
|
||||||
ext {
|
|
||||||
extName = 'Maofly'
|
|
||||||
pkgNameSuffix = 'zh.maofly'
|
|
||||||
extClass = '.Maofly'
|
|
||||||
extVersionCode = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 90 KiB |
|
@ -1,96 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.zh.maofly
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import com.squareup.duktape.Duktape
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
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 okhttp3.Headers
|
|
||||||
import okhttp3.Request
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
|
|
||||||
class Maofly : ParsedHttpSource() {
|
|
||||||
override val name: String = "漫画猫"
|
|
||||||
override val lang: String = "zh"
|
|
||||||
override val supportsLatest: Boolean = false
|
|
||||||
override val baseUrl: String = "https://www.maofly.com"
|
|
||||||
override fun headersBuilder(): Headers.Builder = super.headersBuilder().add("Referer", baseUrl)
|
|
||||||
|
|
||||||
// Popular
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/list-page-$page.html", headers)
|
|
||||||
override fun popularMangaNextPageSelector(): String? = "li.page-item > a:contains(下一页)"
|
|
||||||
override fun popularMangaSelector(): String = "div.comic-main-section > div.comic-book-unit"
|
|
||||||
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
|
||||||
title = element.selectFirst("h2 > a").text()
|
|
||||||
setUrlWithoutDomain(element.selectFirst("h2 > a").attr("href"))
|
|
||||||
thumbnail_url = element.selectFirst("img").attr("src")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Latest
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException("Not used.")
|
|
||||||
override fun latestUpdatesNextPageSelector(): String? = throw UnsupportedOperationException("Not used.")
|
|
||||||
override fun latestUpdatesSelector(): String = throw UnsupportedOperationException("Not used.")
|
|
||||||
override fun latestUpdatesFromElement(element: Element): SManga = throw UnsupportedOperationException("Not used.")
|
|
||||||
|
|
||||||
// Search
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val uri = Uri.parse(baseUrl).buildUpon()
|
|
||||||
.appendPath("search.html")
|
|
||||||
.appendQueryParameter("q", query)
|
|
||||||
.appendQueryParameter("page", page.toString())
|
|
||||||
return GET(uri.toString(), headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector(): String? = popularMangaNextPageSelector()
|
|
||||||
override fun searchMangaSelector(): String = "div.comic-main-section > div > div"
|
|
||||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
|
||||||
|
|
||||||
// Details
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
|
||||||
title = document.selectFirst("h1.comic-title").text().dropLast(1).drop(1)
|
|
||||||
thumbnail_url = document.selectFirst("td.comic-cover > img").attr("abs:src")
|
|
||||||
author = document.selectFirst("td.pub-duration").text()
|
|
||||||
artist = author
|
|
||||||
description = document.selectFirst("div.comic-info > p.comic_story").text()
|
|
||||||
genre = document.select("div.comic-info > ul.tags > li").eachText().joinToString(", ")
|
|
||||||
status = when {
|
|
||||||
!document.select("td.comic-titles > a:contains(已完结)").isEmpty() -> SManga.COMPLETED
|
|
||||||
!document.select("td.comic-titles > a:contains(连载中)").isEmpty() -> SManga.ONGOING
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chapters
|
|
||||||
|
|
||||||
override fun chapterListSelector(): String = "div#comic-book-list li.sort_div"
|
|
||||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
|
||||||
setUrlWithoutDomain(element.select("a").attr("href"))
|
|
||||||
name = element.select("a").text()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pages
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
|
||||||
val script = document.selectFirst("script:containsData(img_data)").data()
|
|
||||||
val imgData = script.substringAfter("img_data = ").substringBefore("\n")
|
|
||||||
val decoded = Duktape.create().use { it.evaluate("${LZSTRING}LZString.decompressFromBase64($imgData)").toString() }
|
|
||||||
val imgServer = document.selectFirst("div.vg-r-data").attr("data-chapter-domain")
|
|
||||||
return decoded.split(",").mapIndexed { i, url ->
|
|
||||||
Page(i, "", "$imgServer/uploads/$url")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used.")
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val LZSTRING = """var LZString=function(){function o(o,r){if(!t[o]){t[o]={};for(var n=0;n<o.length;n++)t[o][o.charAt(n)]=n}return t[o][r]}var r=String.fromCharCode,n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-${'$'}",t={},i={compressToBase64:function(o){if(null==o)return"";var r=i._compress(o,6,function(o){return n.charAt(o)});switch(r.length%4){default:case 0:return r;case 1:return r+"===";case 2:return r+"==";case 3:return r+"="}},decompressFromBase64:function(r){return null==r?"":""==r?null:i._decompress(r.length,32,function(e){return o(n,r.charAt(e))})},compressToUTF16:function(o){return null==o?"":i._compress(o,15,function(o){return r(o+32)})+" "},decompressFromUTF16:function(o){return null==o?"":""==o?null:i._decompress(o.length,16384,function(r){return o.charCodeAt(r)-32})},compressToUint8Array:function(o){for(var r=i.compress(o),n=new Uint8Array(2*r.length),e=0,t=r.length;t>e;e++){var s=r.charCodeAt(e);n[2*e]=s>>>8,n[2*e+1]=s%256}return n},decompressFromUint8Array:function(o){if(null===o||void 0===o)return i.decompress(o);for(var n=new Array(o.length/2),e=0,t=n.length;t>e;e++)n[e]=256*o[2*e]+o[2*e+1];var s=[];return n.forEach(function(o){s.push(r(o))}),i.decompress(s.join(""))},compressToEncodedURIComponent:function(o){return null==o?"":i._compress(o,6,function(o){return e.charAt(o)})},decompressFromEncodedURIComponent:function(r){return null==r?"":""==r?null:(r=r.replace(/ /g,"+"),i._decompress(r.length,32,function(n){return o(e,r.charAt(n))}))},compress:function(o){return i._compress(o,16,function(o){return r(o)})},_compress:function(o,r,n){if(null==o)return"";var e,t,i,s={},p={},u="",c="",a="",l=2,f=3,h=2,d=[],m=0,v=0;for(i=0;i<o.length;i+=1)if(u=o.charAt(i),Object.prototype.hasOwnProperty.call(s,u)||(s[u]=f++,p[u]=!0),c=a+u,Object.prototype.hasOwnProperty.call(s,c))a=c;else{if(Object.prototype.hasOwnProperty.call(p,a)){if(a.charCodeAt(0)<256){for(e=0;h>e;e++)m<<=1,v==r-1?(v=0,d.push(n(m)),m=0):v++;for(t=a.charCodeAt(0),e=0;8>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;h>e;e++)m=m<<1|t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=a.charCodeAt(0),e=0;16>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}l--,0==l&&(l=Math.pow(2,h),h++),delete p[a]}else for(t=s[a],e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;l--,0==l&&(l=Math.pow(2,h),h++),s[c]=f++,a=String(u)}if(""!==a){if(Object.prototype.hasOwnProperty.call(p,a)){if(a.charCodeAt(0)<256){for(e=0;h>e;e++)m<<=1,v==r-1?(v=0,d.push(n(m)),m=0):v++;for(t=a.charCodeAt(0),e=0;8>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;h>e;e++)m=m<<1|t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=a.charCodeAt(0),e=0;16>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}l--,0==l&&(l=Math.pow(2,h),h++),delete p[a]}else for(t=s[a],e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;l--,0==l&&(l=Math.pow(2,h),h++)}for(t=2,e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;for(;;){if(m<<=1,v==r-1){d.push(n(m));break}v++}return d.join("")},decompress:function(o){return null==o?"":""==o?null:i._decompress(o.length,32768,function(r){return o.charCodeAt(r)})},_decompress:function(o,n,e){var t,i,s,p,u,c,a,l,f=[],h=4,d=4,m=3,v="",w=[],A={val:e(0),position:n,index:1};for(i=0;3>i;i+=1)f[i]=i;for(p=0,c=Math.pow(2,2),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;switch(t=p){case 0:for(p=0,c=Math.pow(2,8),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;l=r(p);break;case 1:for(p=0,c=Math.pow(2,16),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;l=r(p);break;case 2:return""}for(f[3]=l,s=l,w.push(l);;){if(A.index>o)return"";for(p=0,c=Math.pow(2,m),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;switch(l=p){case 0:for(p=0,c=Math.pow(2,8),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;f[d++]=r(p),l=d-1,h--;break;case 1:for(p=0,c=Math.pow(2,16),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;f[d++]=r(p),l=d-1,h--;break;case 2:return w.join("")}if(0==h&&(h=Math.pow(2,m),m++),f[l])v=f[l];else{if(l!==d)return null;v=s+s.charAt(0)}w.push(v),f[d++]=s+v.charAt(0),h--,s=v,0==h&&(h=Math.pow(2,m),m++)}}};return i}();"function"==typeof define&&define.amd?define(function(){return LZString}):"undefined"!=typeof module&&null!=module&&(module.exports=LZString);"""
|
|
||||||
}
|
|
||||||
}
|
|