MDB multisrc theme (#12237)

* MDB multisrc theme

* Maofly: optimize assets

* ManhuaDB: new icon

* minor change (rerun CI)

* rename additional gradle file

* add rate limit
This commit is contained in:
stevenyomi 2022-06-20 03:38:25 +08:00 committed by GitHub
parent ab0d2b396d
commit 76da4dda30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 340 additions and 235 deletions

17
.run/MDBGenerator.run.xml Normal file
View File

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -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 }
}
}

View File

@ -0,0 +1,3 @@
dependencies {
implementation 'com.github.softwarevidal:lz-string4java:lz-string4java-1.0.0'
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@ -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
}
}

View File

@ -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")
}
}

View File

@ -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()
}
}
}

View File

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

View File

@ -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"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 756 KiB

View File

@ -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"
}

View File

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

View File

@ -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"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

View File

@ -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);"""
}
}