Mangabz: rewrite, make multisrc, add mirror and new source (#13378)
* Mangabz: rewrite, make multisrc, add mirror and new source * use HttpUrl.Builder for search query
|
@ -0,0 +1,11 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="MangabzGenerator" type="JetRunConfigurationType" nameIsGenerated="true">
|
||||
<module name="tachiyomi-extensions.multisrc.main" />
|
||||
<option name="MAIN_CLASS_NAME" value="eu.kanade.tachiyomi.multisrc.mangabz.MangabzGenerator" />
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktFormat" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=mangabz" />
|
||||
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktLint" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=mangabz" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
|
@ -18,6 +18,10 @@
|
|||
android:host="mangabz.com"
|
||||
android:pathPattern="/..*"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:host="xmanhua.com"
|
||||
android:pathPattern="/..*"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
|
@ -0,0 +1,3 @@
|
|||
dependencies {
|
||||
implementation 'com.github.stevenyomi:unpacker:2948449d0c' // 1.2
|
||||
}
|
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 8.3 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 62 KiB |
|
@ -0,0 +1,36 @@
|
|||
package eu.kanade.tachiyomi.extension.zh.mangabz
|
||||
|
||||
import android.webkit.CookieManager
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
class CookieInterceptor(
|
||||
private val domain: String,
|
||||
private val key: String,
|
||||
private val value: String
|
||||
) : Interceptor {
|
||||
|
||||
init {
|
||||
val url = "https://$domain/"
|
||||
val cookie = "$key=$value; Domain=$domain; Path=/"
|
||||
CookieManager.getInstance().setCookie(url, cookie)
|
||||
}
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
if (!request.url.host.endsWith(domain)) return chain.proceed(request)
|
||||
|
||||
val cookie = "$key=$value"
|
||||
val cookieList = request.header("Cookie")?.split("; ") ?: emptyList()
|
||||
if (cookie in cookieList) return chain.proceed(request)
|
||||
|
||||
CookieManager.getInstance().setCookie("https://$domain/", "$cookie; Domain=$domain; Path=/")
|
||||
val prefix = "$key="
|
||||
val newCookie = buildList(cookieList.size + 1) {
|
||||
cookieList.filterNotTo(this) { it.startsWith(prefix) }
|
||||
add(cookie)
|
||||
}.joinToString("; ")
|
||||
val newRequest = request.newBuilder().header("Cookie", newCookie).build()
|
||||
return chain.proceed(newRequest)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package eu.kanade.tachiyomi.extension.zh.mangabz
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
fun parseDateInternal(source: String): Long {
|
||||
// 今天 00:00
|
||||
if (recentRegex.matches(source)) {
|
||||
val date = fullDateFormat.format(Date())
|
||||
val time = timeFormat.parse(date + ' ' + source.substring(3))!!.time
|
||||
val offset = when (source[0]) {
|
||||
'今' -> 0L
|
||||
'昨' -> 86400000L
|
||||
'前' -> 86400000L * 2
|
||||
else -> 0L // impossible
|
||||
}
|
||||
return time - offset
|
||||
}
|
||||
|
||||
// 01月01号, 01月01號
|
||||
if (source.length >= 6 && source[2] == '月') {
|
||||
val year = fullDateFormat.format(Date()).substringBefore('-')
|
||||
return shortDateFormat.parse("$year $source")!!.time
|
||||
}
|
||||
|
||||
// 2021-01-01
|
||||
return fullDateFormat.parse(source)!!.time
|
||||
}
|
||||
|
||||
private val recentRegex by lazy { Regex("""[今昨前]天 \d{2}:\d{2}""") }
|
||||
private val timeFormat by lazy { cstFormat("yyyy-MM-dd hh:mm") }
|
||||
private val shortDateFormat by lazy { cstFormat("yyyy MM月dd") }
|
||||
private val fullDateFormat by lazy { cstFormat("yyyy-MM-dd") }
|
||||
|
||||
private fun cstFormat(pattern: String) =
|
||||
SimpleDateFormat(pattern, Locale.ENGLISH).apply { timeZone = TimeZone.getTimeZone("GMT+8") }
|
|
@ -0,0 +1,66 @@
|
|||
package eu.kanade.tachiyomi.extension.zh.mangabz
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.select.Evaluator
|
||||
|
||||
fun getFilterListInternal(categories: List<CategoryData>): FilterList {
|
||||
val list: List<Filter<*>> = if (categories.isEmpty()) {
|
||||
listOf(Filter.Header("点击“重置”刷新分类"))
|
||||
} else buildList(categories.size + 1) {
|
||||
add(Filter.Header("分类(搜索文本时无效)"))
|
||||
categories.mapTo(this, CategoryData::toFilter)
|
||||
}
|
||||
return FilterList(list)
|
||||
}
|
||||
|
||||
fun parseFilterList(filters: FilterList): String =
|
||||
filters.filterIsInstance<CategoryFilter>().joinToString("-") { it.id.toString() }
|
||||
|
||||
fun parseCategories(document: Document): List<CategoryData> {
|
||||
val lines = document.select(Evaluator.Class("class-line")).ifEmpty { return emptyList() }
|
||||
val defaultIds = IntArray(lines.size)
|
||||
val idArrays = arrayOfNulls<IntArray>(lines.size)
|
||||
|
||||
val result = lines.mapIndexed { filterIndex, line ->
|
||||
val options = line.select(Evaluator.Tag("a")).mapIndexed { optionIndex, option ->
|
||||
val optionName = option.ownText()!!
|
||||
if (optionIndex == 0) {
|
||||
Pair(optionName, 0) // id is unknown
|
||||
} else {
|
||||
val idTuple = option.attr("href")
|
||||
.removePrefix("/manga-list-").removeSuffix("/").split("-")
|
||||
for ((indexInTuple, id) in idTuple.withIndex()) {
|
||||
if (indexInTuple != filterIndex) defaultIds[indexInTuple] = id.toInt()
|
||||
}
|
||||
Pair(optionName, idTuple[filterIndex].toInt())
|
||||
}
|
||||
}
|
||||
|
||||
val name = line.child(0).ownText().removeSuffix(":")
|
||||
val values = Array(options.size) { options[it].first }
|
||||
val ids = IntArray(options.size) { options[it].second }
|
||||
idArrays[filterIndex] = ids
|
||||
CategoryData(name, values, ids)
|
||||
}
|
||||
|
||||
for ((i, idArray) in idArrays.withIndex()) {
|
||||
idArray!![0] = defaultIds[i]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
class CategoryData(
|
||||
private val name: String,
|
||||
private val values: Array<String>,
|
||||
private val ids: IntArray
|
||||
) {
|
||||
fun toFilter() = CategoryFilter(name, values, ids)
|
||||
}
|
||||
|
||||
class CategoryFilter(name: String, values: Array<String>, private val ids: IntArray) :
|
||||
Filter.Select<String>(name, values) {
|
||||
val id get() = ids[state]
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
package eu.kanade.tachiyomi.extension.zh.mangabz
|
||||
|
||||
import android.app.Application
|
||||
import androidx.preference.PreferenceScreen
|
||||
import com.github.stevenyomi.unpacker.ProgressiveParser
|
||||
import com.github.stevenyomi.unpacker.Unpacker
|
||||
import eu.kanade.tachiyomi.multisrc.mangabz.MangabzTheme
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
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 okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Elements
|
||||
import org.jsoup.select.Evaluator
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class Mangabz : MangabzTheme("Mangabz", ""), ConfigurableSource {
|
||||
|
||||
override val baseUrl: String
|
||||
override val client: OkHttpClient
|
||||
|
||||
private val urlSuffix: String
|
||||
|
||||
init {
|
||||
val preferences = Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
val mirror = preferences.mirror
|
||||
baseUrl = "https://" + mirror.domain
|
||||
urlSuffix = mirror.urlSuffix
|
||||
|
||||
val cookieInterceptor = CookieInterceptor(mirror.domain, mirror.langCookie, preferences.lang)
|
||||
client = network.client.newBuilder()
|
||||
.rateLimit(5)
|
||||
.addNetworkInterceptor(cookieInterceptor)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun SManga.stripMirror() = apply {
|
||||
val old = url
|
||||
url = buildString(old.length) {
|
||||
append(old, 0, old.length - urlSuffix.length).append("bz/")
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toMirror() = buildString {
|
||||
val old = this@toMirror // ...bz/
|
||||
append(old, 0, old.length - 3).append(urlSuffix)
|
||||
}
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
if (query.isEmpty()) {
|
||||
val ids = parseFilterList(filters)
|
||||
if (ids.isEmpty()) return fetchPopularManga(page)
|
||||
|
||||
return client.newCall(GET("$baseUrl/manga-list-$ids-p$page/", headers))
|
||||
.asObservableSuccess().map(::searchMangaParse)
|
||||
}
|
||||
|
||||
val path = when {
|
||||
query.startsWith(PREFIX_ID_SEARCH) -> query.removePrefix(PREFIX_ID_SEARCH)
|
||||
query.startsWith(baseUrl) -> query.removePrefix(baseUrl).trim('/')
|
||||
else -> return super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
val mirrorPath = "$path/".toMirror()
|
||||
|
||||
return client.newCall(GET("$baseUrl/$mirrorPath", headers))
|
||||
.asObservableSuccess().map { MangasPage(listOf(mangaDetailsParse(it)), false) }
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response) = super.searchMangaParse(response).apply {
|
||||
for (manga in mangas) manga.stripMirror()
|
||||
}
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga) = GET(baseUrl + manga.url.toMirror(), headers)
|
||||
override fun mangaDetailsParse(response: Response) = super.mangaDetailsParse(response).stripMirror()
|
||||
|
||||
override fun parseDescription(element: Element, title: String, details: Elements): String {
|
||||
val text = element.ownText()
|
||||
val start = if (text.startsWith(title)) title.length + 4 else 0
|
||||
val collapsed = element.selectFirst(Evaluator.Tag("span"))?.ownText()
|
||||
?: return text.substring(start)
|
||||
return buildString { append(text, start, text.length - 1).append(collapsed) }
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga) = GET(baseUrl + manga.url.toMirror(), headers)
|
||||
|
||||
override fun parseDate(listTitle: String) = parseDateInternal(listTitle.substringAfterLast(", "))
|
||||
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
val chapterId = chapter.url.removePrefix("/m").removeSuffix("/")
|
||||
val pageCount = chapter.name.substringAfterLast('(').removeSuffix("P)").toInt()
|
||||
val prefix = "$baseUrl${chapter.url}chapterimage.ashx?cid=$chapterId&page="
|
||||
// 1 request returns 2 pages, or 15 if server cache is ready, so we manually cache them below
|
||||
val list = List(pageCount) { Page(it, "$prefix${it + 1}#$pageCount") }
|
||||
return Observable.just(list)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
// key is chapterId, value[0] is URL prefix, value[1..pageCount] are paths
|
||||
private val imageUrlCache = object : LinkedHashMap<Int, Array<String?>>() {
|
||||
// limit cache to 10 chapters
|
||||
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<Int, Array<String?>>?) = size > 10
|
||||
}
|
||||
|
||||
override fun fetchImageUrl(page: Page): Observable<String> {
|
||||
val url = page.url.toHttpUrl()
|
||||
|
||||
var cache: Array<String?>? = null
|
||||
url.fragment?.run {
|
||||
val pageCount = toInt()
|
||||
val chapterId = url.queryParameter("cid")!!.toInt()
|
||||
val realCache = imageUrlCache.getOrPut(chapterId) { arrayOfNulls(pageCount + 1) }
|
||||
val path = realCache[page.index + 1]
|
||||
if (path != null) return Observable.just(realCache[0]!! + path)
|
||||
cache = realCache
|
||||
}
|
||||
|
||||
return client.newCall(GET(page.url, headers)).asObservableSuccess().map {
|
||||
val script = Unpacker.unpack(it.body!!.string())
|
||||
val parser = ProgressiveParser(script)
|
||||
val prefix = parser.substringBetween("pix=\"", "\"")
|
||||
// 2 pages, or 15 if server cache is ready
|
||||
val paths = parser.substringBetween("[\"", "\"]").split("\",\"")
|
||||
val pageNumber = page.index + 1
|
||||
cache?.run {
|
||||
this[0] = prefix
|
||||
for ((offset, path) in paths.withIndex()) this[pageNumber + offset] = path
|
||||
}
|
||||
prefix + paths[0]
|
||||
}
|
||||
}
|
||||
|
||||
var categories = emptyList<CategoryData>()
|
||||
|
||||
override fun parseFilters(document: Document) {
|
||||
if (categories.isEmpty()) categories = parseCategories(document)
|
||||
}
|
||||
|
||||
override fun getFilterList() = getFilterListInternal(categories)
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
getPreferencesInternal(screen.context).forEach(screen::addPreference)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PREFIX_ID_SEARCH = "id:"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package eu.kanade.tachiyomi.extension.zh.mangabz
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
|
||||
fun getPreferencesInternal(context: Context) = arrayOf(
|
||||
ListPreference(context).apply {
|
||||
val mirrors = MIRRORS
|
||||
val size = mirrors.size
|
||||
|
||||
key = MIRROR_PREF
|
||||
title = "镜像站点"
|
||||
summary = "%s\n重启生效,不同站点的数据有细微差异"
|
||||
entries = Array(size) { mirrors[it].domain }
|
||||
entryValues = Array(size) { it.toString() }
|
||||
setDefaultValue("0")
|
||||
},
|
||||
|
||||
SwitchPreferenceCompat(context).apply {
|
||||
key = ZH_HANT_PREF
|
||||
title = "使用繁体中文"
|
||||
summary = "重启生效,已添加的漫画需要迁移才能更新标题"
|
||||
setDefaultValue(false)
|
||||
},
|
||||
)
|
||||
|
||||
val SharedPreferences.mirror: Mirror
|
||||
get() {
|
||||
val mirrors = MIRRORS
|
||||
val mirrorPref = getString(MIRROR_PREF, "0")!!
|
||||
val mirrorIndex = mirrorPref.toInt().coerceAtMost(mirrors.size - 1)
|
||||
return mirrors[mirrorIndex]
|
||||
}
|
||||
|
||||
val SharedPreferences.lang get() = if (getBoolean(ZH_HANT_PREF, false)) "1" else "2"
|
||||
|
||||
// Legacy preferences:
|
||||
// "mainSiteRatelimitPreference" -> 1..10 default "5"
|
||||
// "imgCDNRatelimitPreference" -> 1..10 default "5"
|
||||
|
||||
const val MIRROR_PREF = "mirror"
|
||||
const val ZH_HANT_PREF = "showZhHantWebsite"
|
||||
|
||||
val MIRRORS
|
||||
get() = arrayOf(
|
||||
Mirror("mangabz.com", "bz/", "mangabz_lang"),
|
||||
Mirror("xmanhua.com", "xm/", "xmanhua_lang"),
|
||||
)
|
||||
|
||||
class Mirror(
|
||||
val domain: String,
|
||||
val urlSuffix: String,
|
||||
val langCookie: String,
|
||||
)
|
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,150 @@
|
|||
package eu.kanade.tachiyomi.extension.zh.vomic
|
||||
|
||||
import android.app.Application
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.multisrc.mangabz.MangabzTheme
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
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.util.asJsoup
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Elements
|
||||
import org.jsoup.select.Evaluator
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class Vomic : MangabzTheme("vomic", ""), ConfigurableSource {
|
||||
|
||||
override val supportsLatest = false
|
||||
|
||||
override val baseUrl: String
|
||||
|
||||
init {
|
||||
val mirrors = MIRRORS
|
||||
val mirrorIndex = Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
.getString(MIRROR_PREF, "0")!!.toInt().coerceAtMost(mirrors.size - 1)
|
||||
baseUrl = "http://" + mirrors[mirrorIndex]
|
||||
}
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder().removeAll("Referer")
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
|
||||
|
||||
// original credit: https://github.com/tachiyomiorg/tachiyomi-extensions/pull/5628
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val link = Evaluator.Tag("a")
|
||||
val image = Evaluator.Tag("img")
|
||||
val paragraph = Evaluator.Tag("p")
|
||||
|
||||
/* top banner - no thumbnail
|
||||
document.selectFirst(Evaluator.Class("banner-con")).select(link).mapTo(mangas) { element ->
|
||||
SManga.create().apply {
|
||||
title = element.attr("title")
|
||||
url = element.attr("href")
|
||||
thumbnail_url = element.selectFirst(image).attr("src")
|
||||
.takeIf { !it.endsWith("/static/images/bg/banner_info_a.jpg") }
|
||||
}
|
||||
} */
|
||||
|
||||
val mangas = buildList {
|
||||
// ranking sidebar
|
||||
addAll(document.selectFirst(Evaluator.Class("rank-list")).children())
|
||||
// carousel list
|
||||
addAll(document.selectFirst(Evaluator.Class("carousel-right-list")).children())
|
||||
// recommend list
|
||||
addAll(document.select(Evaluator.Class("index-manga-item")))
|
||||
}.map { element ->
|
||||
SManga.create().apply {
|
||||
title = element.selectFirst(paragraph).text()
|
||||
url = element.selectFirst(link).attr("href")
|
||||
thumbnail_url = element.selectFirst(image).attr("src")
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(mangas.distinctBy { it.url }, false)
|
||||
}
|
||||
|
||||
override fun parseDescription(element: Element, title: String, details: Elements): String {
|
||||
val text = element.ownText()
|
||||
val collapsed = element.selectFirst(Evaluator.Tag("span"))?.ownText() ?: ""
|
||||
val source = details[3].text()
|
||||
return "$source\n\n$text$collapsed"
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
val chapterId = manga.url.removePrefix("/").removeSuffix("_c/")
|
||||
return super.fetchChapterList(manga).doOnNext {
|
||||
for (chapter in it) chapter.url = chapter.url + "chapterimage.ashx?mid=" + chapterId
|
||||
}
|
||||
}
|
||||
|
||||
override fun getChapterElements(document: Document): Elements {
|
||||
val chapterId = document.location().removeSuffix("_c/").substringAfterLast('/')
|
||||
val response = client.newCall(GET("$baseUrl/chapter-$chapterId-s2/", headers)).execute()
|
||||
return Jsoup.parseBodyFragment(response.body!!.string()).body().children()
|
||||
}
|
||||
|
||||
override val needPageCount = false
|
||||
|
||||
override fun parseDate(listTitle: String): Long {
|
||||
val date = listTitle.split("|")[2].trim()
|
||||
return dateFormat.parse(date)!!.time
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val urls = response.body!!.string().run {
|
||||
val left = indexOf('[')
|
||||
val right = lastIndexOf(']')
|
||||
if (left + 1 == right) return emptyList()
|
||||
substring(left + 1, right).split(", ")
|
||||
}
|
||||
return urls.mapIndexed { index, rawUrl ->
|
||||
val url = rawUrl.trim('"')
|
||||
val imageUrl = when {
|
||||
url.startsWith("http://127.0.0.1") -> url.toHttpUrl().queryParameter("url")
|
||||
else -> url
|
||||
}
|
||||
Page(index, imageUrl = imageUrl)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val url = page.imageUrl!!
|
||||
val host = url.toHttpUrl().host
|
||||
val headers = headersBuilder().set("Referer", "https://$host/").build()
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
val mirrors = MIRRORS
|
||||
key = MIRROR_PREF
|
||||
title = "镜像网址"
|
||||
summary = "%s\n重启生效"
|
||||
entries = mirrors
|
||||
entryValues = Array(mirrors.size) { it.toString() }
|
||||
setDefaultValue("0")
|
||||
}.let(screen::addPreference)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MIRROR_PREF = "MIRROR"
|
||||
private val MIRRORS get() = arrayOf("www.vomicmh.com", "www.iewoai.com")
|
||||
|
||||
private val dateFormat by lazy { SimpleDateFormat("yyyy-MM-dd hh:mm:ss", Locale.ENGLISH) }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package eu.kanade.tachiyomi.multisrc.mangabz
|
||||
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
class MangabzGenerator : ThemeSourceGenerator {
|
||||
override val themeClass = "MangabzTheme"
|
||||
override val themePkg = "mangabz"
|
||||
override val baseVersionCode = 1
|
||||
override val sources = listOf(
|
||||
SingleLang("Mangabz", "https://mangabz.com", "zh", overrideVersionCode = 5),
|
||||
SingleLang("vomic", "http://www.vomicmh.com", "zh", className = "Vomic"),
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
MangabzGenerator().createAll()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
package eu.kanade.tachiyomi.multisrc.mangabz
|
||||
|
||||
import android.util.Log
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Elements
|
||||
import org.jsoup.select.Evaluator
|
||||
|
||||
abstract class MangabzTheme(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String = "zh"
|
||||
) : HttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder().add("Referer", baseUrl)
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/manga-list-p$page/", headers)
|
||||
|
||||
override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/manga-list-0-0-2-p$page/", headers)
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||
if (query.isEmpty()) {
|
||||
popularMangaRequest(page)
|
||||
} else {
|
||||
val url = "$baseUrl/search".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("title", query)
|
||||
.addQueryParameter("page", page.toString())
|
||||
Request.Builder().url(url.build()).headers(headers).build()
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup().also(::parseFilters)
|
||||
val mangas = document.selectFirst(Evaluator.Class("mh-list")).children().map { element ->
|
||||
SManga.create().apply {
|
||||
title = element.selectFirst(Evaluator.Tag("h2")).text()
|
||||
url = element.selectFirst(Evaluator.Tag("a")).attr("href")
|
||||
thumbnail_url = element.selectFirst(Evaluator.Tag("img")).attr("src")
|
||||
}
|
||||
}
|
||||
val hasNextPage = document.run {
|
||||
val pagination = selectFirst(Evaluator.Class("page-pagination"))
|
||||
pagination != null && pagination.select(Evaluator.Tag("a")).last().text() == ">"
|
||||
}
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val document = response.asJsoup()
|
||||
val details = document.selectFirst(Evaluator.Class("detail-info-tip")).children()!!
|
||||
return SManga.create().apply {
|
||||
url = document.location().removePrefix(baseUrl)
|
||||
title = document.selectFirst(Evaluator.Class("detail-info-title")).ownText()
|
||||
thumbnail_url = document.selectFirst(Evaluator.Class("detail-info-cover")).attr("src")
|
||||
status = when (details[1].child(0).ownText()) {
|
||||
"连载中" -> SManga.ONGOING
|
||||
"已完结" -> SManga.COMPLETED
|
||||
"連載中" -> SManga.ONGOING
|
||||
"已完結" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
author = details[0].children().joinToString { it.ownText() }
|
||||
genre = details[2].children().joinToString { it.ownText() }
|
||||
description = parseDescription(document.selectFirst(Evaluator.Class("detail-info-content")), title, details)
|
||||
initialized = true
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun parseDescription(element: Element, title: String, details: Elements): String
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
val needPageCount = needPageCount
|
||||
val list = getChapterElements(document).map { element ->
|
||||
val chapterName = element.ownText()
|
||||
SChapter.create().apply {
|
||||
url = element.attr("href")
|
||||
if (needPageCount) {
|
||||
name = chapterName + element.child(0).ownText()
|
||||
chapter_number = when (val result = floatRegex.find(chapterName)) {
|
||||
null -> -2f
|
||||
else -> result.value.toFloat()
|
||||
}
|
||||
} else {
|
||||
name = chapterName
|
||||
}
|
||||
}
|
||||
}
|
||||
if (list.isEmpty()) return emptyList()
|
||||
|
||||
val listTitle = document.selectFirst(Evaluator.Class("detail-list-form-title")).ownText()
|
||||
try {
|
||||
list[0].date_upload = parseDate(listTitle)
|
||||
} catch (e: Throwable) {
|
||||
Log.e("Mangabz/$name", "failed to parse date from '$listTitle'", e)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
protected open fun getChapterElements(document: Document): Elements =
|
||||
document.selectFirst(Evaluator.Id("chapterlistload")).children()
|
||||
|
||||
protected open val needPageCount = true
|
||||
|
||||
protected abstract fun parseDate(listTitle: String): Long
|
||||
|
||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
protected open fun parseFilters(document: Document) = Unit
|
||||
|
||||
private val floatRegex by lazy { Regex("""\d+(?:\.\d+)?""") }
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
extName = 'Mangabz'
|
||||
pkgNameSuffix = 'zh.mangabz'
|
||||
extClass = '.Mangabz'
|
||||
extVersionCode = 5
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
|
||||
}
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
multiDexEnabled true
|
||||
}
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 70 KiB |
|
@ -1,393 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.zh.mangabz
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import app.cash.quickjs.QuickJs
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
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.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.json.JSONArray
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.ArrayList
|
||||
|
||||
class Mangabz : ConfigurableSource, HttpSource() {
|
||||
|
||||
override val lang = "zh"
|
||||
override val supportsLatest = false
|
||||
override val name = "Mangabz"
|
||||
override val baseUrl = "https://mangabz.com"
|
||||
private val imageServer = arrayOf("https://cover.mangabz.com", "https://image.mangabz.com")
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
|
||||
.set("Referer", "https://mangabz.com")
|
||||
.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.104 Safari/537.36")
|
||||
|
||||
private val showZhHantWebsite = preferences.getBoolean(SHOW_ZH_HANT_WEBSITE_PREF, false)
|
||||
|
||||
override val client: OkHttpClient = network.client.newBuilder()
|
||||
.rateLimitHost(baseUrl.toHttpUrlOrNull()!!, preferences.getString(MAINSITE_RATELIMIT_PREF, "5")!!.toInt())
|
||||
.rateLimitHost(imageServer[0].toHttpUrlOrNull()!!, preferences.getString(IMAGE_CDN_RATELIMIT_PREF, "5")!!.toInt())
|
||||
.rateLimitHost(imageServer[1].toHttpUrlOrNull()!!, preferences.getString(IMAGE_CDN_RATELIMIT_PREF, "5")!!.toInt())
|
||||
.addNetworkInterceptor { chain ->
|
||||
val cookies = chain.request().header("Cookie")?.replace(replaceCookiesRegex, "") ?: ""
|
||||
val newReq = chain
|
||||
.request()
|
||||
.newBuilder()
|
||||
.header("Cookie", if (showZhHantWebsite) cookies else "$cookies; mangabz_lang=2")
|
||||
.build()
|
||||
chain.proceed(newReq)
|
||||
}.build()!!
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
val mangasList = ArrayList<SManga>(0)
|
||||
|
||||
// top banner
|
||||
document.select("div.banner-con a").map { element ->
|
||||
mangasList.add(
|
||||
SManga.create().apply {
|
||||
title = element.attr("title")
|
||||
url = element.attr("href")
|
||||
thumbnail_url = element.select("img").first().attr("src")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// ranking sidebar
|
||||
document.select(".rank-list .list").map { element ->
|
||||
mangasList.add(
|
||||
SManga.create().apply {
|
||||
title = element.select(".rank-item-title").first().text()
|
||||
url = element.select("a").first().attr("href")
|
||||
thumbnail_url = element.select("a img").first().attr("src")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// carousel list
|
||||
document.select(".carousel-right-item").map { element ->
|
||||
mangasList.add(
|
||||
SManga.create().apply {
|
||||
title = element.select(".carousel-right-item-title a").first().text()
|
||||
url = element.select(".carousel-right-item-title a").first().attr("href")
|
||||
thumbnail_url = element.select("a img").first().attr("src")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// recommend list
|
||||
document.select(".index-manga-item").map { element ->
|
||||
mangasList.add(
|
||||
SManga.create().apply {
|
||||
title = element.select(".index-manga-item-title").first().text()
|
||||
url = element.select(".index-manga-item-title a").first().attr("href")
|
||||
thumbnail_url = element.select("a img").first().attr("src")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return MangasPage(mangasList.distinctBy { it.url }, false)
|
||||
}
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return if (query.startsWith(PREFIX_ID_SEARCH) && query.contains(extractMangaIdRegex)) {
|
||||
val id = query.removePrefix(PREFIX_ID_SEARCH)
|
||||
client.newCall(GET("$baseUrl/$id", headers))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
val sManga = mangaDetailsParse(response)
|
||||
sManga.url = "/$id"
|
||||
return@map MangasPage(listOf(sManga), false)
|
||||
}
|
||||
} else if (query.startsWith(baseUrl) && query.contains(extractMangaIdRegex)) {
|
||||
val id = extractMangaIdRegex.find(query)?.value
|
||||
client.newCall(GET("$baseUrl/$id", headers))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
val sManga = mangaDetailsParse(response)
|
||||
sManga.url = "/$id"
|
||||
return@map MangasPage(listOf(sManga), false)
|
||||
}
|
||||
} else {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = GET("$baseUrl/search?title=$query&page=$page")
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
val mangas = document.select(".mh-list .mh-item").map { element ->
|
||||
SManga.create().apply {
|
||||
title = element.select(".mh-item-detali h2.title a").first().text()
|
||||
url = element.select(".mh-item-detali h2.title a").first().attr("href")
|
||||
thumbnail_url = element.select("a img.mh-cover").first().attr("src")
|
||||
}
|
||||
}
|
||||
val hasNextPage = document.select(".page-pagination li:contains(>)").first() != null
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException("Not used.")
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage = throw UnsupportedOperationException("Not used.")
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
return GET(baseUrl + manga.url, headers.newBuilder().set("Referer", baseUrl + manga.url).build())
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val document = response.asJsoup()
|
||||
return SManga.create().apply {
|
||||
title = document.select(".detail-info-title").first().text()
|
||||
thumbnail_url = document.select("img.detail-info-cover").first().attr("src")
|
||||
status = when (document.select("span:contains(状态)>span, span:contains(狀態)>span").first().text()) {
|
||||
"连载中" -> SManga.ONGOING
|
||||
"連載中" -> SManga.ONGOING
|
||||
"已完结" -> SManga.COMPLETED
|
||||
"已完結" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
author = document.select("span:contains(作者) a")?.first()?.text() ?: ""
|
||||
genre = document.select(".item")?.first()?.text() ?: ""
|
||||
description = document.select(".detail-info-content")?.first()?.text() ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request = mangaDetailsRequest(manga)
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
val latestChapter = document.select(".s a").first().attr("href")
|
||||
val chapterInfo = document.select(".detail-list-form-title").first().text()
|
||||
val latestUploadDate = parseDate(chapterInfo)
|
||||
|
||||
return document.select("a.detail-list-form-item").map { element ->
|
||||
SChapter.create().apply {
|
||||
url = element.attr("href")
|
||||
name = element.text()
|
||||
chapter_number = chapterNumRegex.find(name)?.value?.toFloatOrNull() ?: -1F
|
||||
if (url == latestChapter) {
|
||||
date_upload = latestUploadDate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseDate(string: String): Long {
|
||||
val rightNow = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"))
|
||||
// today
|
||||
if (string.contains("今天")) {
|
||||
return rightNow.toInstant().toEpochMilli()
|
||||
}
|
||||
// yesterday
|
||||
if (string.contains("昨天")) {
|
||||
return rightNow.minusDays(1).toInstant().toEpochMilli()
|
||||
}
|
||||
// the day before yesterday
|
||||
if (string.contains("前天")) {
|
||||
return rightNow.minusDays(2).toInstant().toEpochMilli()
|
||||
}
|
||||
// 2021-01-01
|
||||
val result1 = dateRegex1.find(string)?.value
|
||||
if (result1 != null) {
|
||||
return LocalDate.parse("$result1").atTime(0, 0).atZone(ZoneId.of("Asia/Shanghai")).toInstant().toEpochMilli()
|
||||
}
|
||||
// 1月1号 or 1月1號 -> (1, 1)
|
||||
val result2 = dateRegex2.find(string)?.groupValues
|
||||
if (result2 != null && result2.size > 1) {
|
||||
val d = rightNow.withMonth(result2[1].toInt()).withDayOfMonth(result2[2].toInt())
|
||||
return d.toInstant().toEpochMilli()
|
||||
}
|
||||
return rightNow.toInstant().toEpochMilli()
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
return GET(baseUrl + chapter.url, headers.newBuilder().set("Referer", baseUrl + chapter.url).build())
|
||||
}
|
||||
|
||||
// Special thanks to Cimoc project.
|
||||
// https://github.com/feilongfl/Cimoc/blob/03d378ddb5fe8684ef85cae673624afdb68fcf46/app/src/main/java/com/hiroshi/cimoc/source/MangaBZ.kt#L95
|
||||
private fun getJSVar(html: String, keyword: String, searchFor: String): String? {
|
||||
val re = Regex("var\\s+$keyword\\s*=\\s*$searchFor\\s*;")
|
||||
val match = re.find(html)
|
||||
return match?.groups?.get(1)?.value
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val document = response.asJsoup()
|
||||
val scriptTag = (document.select("head script").filter { it.data().isNotBlank() })[0].data()
|
||||
val chapterUrl = response.request.url.toString()
|
||||
|
||||
val mid = getJSVar(scriptTag, "MANGABZ_MID", "(\\w+)")!!
|
||||
val cid = getJSVar(scriptTag, "MANGABZ_CID", "(\\w+)")!!
|
||||
val sign = getJSVar(scriptTag, "MANGABZ_VIEWSIGN", """\"(\w+)\"""")!!
|
||||
val pageCount = getJSVar(scriptTag, "MANGABZ_IMAGE_COUNT", "(\\d+)")!!.toInt()
|
||||
val path = getJSVar(scriptTag, "MANGABZ_CURL", "\"/(\\w+)/\"")!!
|
||||
|
||||
// Page list return by webpage's API maybe incomplete, so we store API url and
|
||||
// chapter url in page.url to build header and fetch API when needed.
|
||||
val pagesList = MutableList(
|
||||
pageCount,
|
||||
init = { index ->
|
||||
Page(
|
||||
index,
|
||||
url = "$baseUrl/$path/chapterimage.ashx?cid=$cid&page=${index + 1}&key=&_cid=$cid&_mid=$mid&_sign=$sign&_dt=\n" +
|
||||
chapterUrl
|
||||
)
|
||||
}
|
||||
) // Fill the list at first.
|
||||
|
||||
// Page 1 may return 1~2 image urls.
|
||||
val apiUrlInPage1 = "$baseUrl/$path/chapterimage.ashx?cid=$cid&page=1&key=&_cid=$cid&_mid=$mid&_sign=$sign&_dt="
|
||||
val imgUrlList = fetchImageUrlListFromAPI(apiUrlInPage1, response.request.headers)
|
||||
for (i in 0 until imgUrlList.length()) {
|
||||
val imgUrl = imgUrlList[i] as String
|
||||
val pageNum = extractPageNumFromImageUrlRegex.find(imgUrl)!!.groups[1]!!.value.toInt() - 1
|
||||
pagesList[pageNum] = Page(pageNum, "$apiUrlInPage1\n$chapterUrl", imgUrl)
|
||||
}
|
||||
|
||||
return pagesList
|
||||
}
|
||||
|
||||
private fun fetchImageUrlListFromAPI(apiUrl: String, requestHeaders: Headers = headers): JSONArray {
|
||||
val jsEvalPayload = client.newCall(GET(apiUrl, requestHeaders)).execute().body!!.string()
|
||||
val imgUrlDecode = QuickJs.create().use {
|
||||
it.evaluate("$jsEvalPayload; JSON.stringify(d);") as String
|
||||
}
|
||||
return JSONArray(imgUrlDecode)
|
||||
}
|
||||
|
||||
override fun fetchImageUrl(page: Page): Observable<String> {
|
||||
if (page.imageUrl != null) {
|
||||
return Observable.just(page.imageUrl)
|
||||
} else {
|
||||
val urls = page.url.split("\n")
|
||||
val imgUrlList = fetchImageUrlListFromAPI(urls[0], headers.newBuilder().set("Referer", urls[1]).build())
|
||||
|
||||
for (i in 0 until imgUrlList.length()) {
|
||||
val imgUrl = imgUrlList[i] as String
|
||||
val pageNum = extractPageNumFromImageUrlRegex.find(imgUrl)!!.groups[1]!!.value.toInt() - 1
|
||||
if (page.index == pageNum) {
|
||||
return Observable.just(imgUrl)
|
||||
}
|
||||
}
|
||||
return Observable.error(Exception("Can't find image urls"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlRequest(page: Page): Request = throw UnsupportedOperationException("Not used.")
|
||||
|
||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not used.")
|
||||
|
||||
override fun imageRequest(page: Page): Request = GET(page.imageUrl!!, headers)
|
||||
|
||||
override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
|
||||
val mainSiteRateLimitPreference = androidx.preference.ListPreference(screen.context).apply {
|
||||
key = MAINSITE_RATELIMIT_PREF
|
||||
title = MAINSITE_RATELIMIT_PREF_TITLE
|
||||
entries = ENTRIES_ARRAY
|
||||
entryValues = ENTRIES_ARRAY
|
||||
summary = MAINSITE_RATELIMIT_PREF_SUMMARY
|
||||
|
||||
setDefaultValue("5")
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
try {
|
||||
val setting = preferences.edit().putString(MAINSITE_RATELIMIT_PREF, newValue as String).commit()
|
||||
setting
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val imgCDNRateLimitPreference = androidx.preference.ListPreference(screen.context).apply {
|
||||
key = IMAGE_CDN_RATELIMIT_PREF
|
||||
title = IMAGE_CDN_RATELIMIT_PREF_TITLE
|
||||
entries = ENTRIES_ARRAY
|
||||
entryValues = ENTRIES_ARRAY
|
||||
summary = IMAGE_CDN_RATELIMIT_PREF_SUMMARY
|
||||
|
||||
setDefaultValue("5")
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
try {
|
||||
val setting = preferences.edit().putString(IMAGE_CDN_RATELIMIT_PREF, newValue as String).commit()
|
||||
setting
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val zhHantPreference = androidx.preference.CheckBoxPreference(screen.context).apply {
|
||||
key = SHOW_ZH_HANT_WEBSITE_PREF
|
||||
title = SHOW_ZH_HANT_WEBSITE_PREF_TITLE
|
||||
summary = SHOW_ZH_HANT_WEBSITE_PREF_SUMMARY
|
||||
|
||||
setDefaultValue(false)
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
try {
|
||||
val setting = preferences.edit().putBoolean(SHOW_ZH_HANT_WEBSITE_PREF, newValue as Boolean).commit()
|
||||
setting
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
screen.addPreference(mainSiteRateLimitPreference)
|
||||
screen.addPreference(imgCDNRateLimitPreference)
|
||||
screen.addPreference(zhHantPreference)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAINSITE_RATELIMIT_PREF = "mainSiteRatelimitPreference"
|
||||
private const val MAINSITE_RATELIMIT_PREF_TITLE = "主站每秒连接数限制" // "Ratelimit permits per second for main website"
|
||||
private const val MAINSITE_RATELIMIT_PREF_SUMMARY = "此值影响向网站发起连接请求的数量。调低此值可能减少发生HTTP 429(连接请求过多)错误的几率,但加载速度也会变慢。需要重启软件以生效。\n当前值:%s" // "This value affects network request amount to main website url. Lower this value may reduce the chance to get HTTP 429 error, but loading speed will be slower too. Tachiyomi restart required. Current value: %s"
|
||||
|
||||
private const val IMAGE_CDN_RATELIMIT_PREF = "imgCDNRatelimitPreference"
|
||||
private const val IMAGE_CDN_RATELIMIT_PREF_TITLE = "图片CDN每秒连接数限制" // "Ratelimit permits per second for image CDN"
|
||||
private const val IMAGE_CDN_RATELIMIT_PREF_SUMMARY = "此值影响加载图片时发起连接请求的数量。调低此值可能减小IP被屏蔽的几率,但加载速度也会变慢。需要重启软件以生效。\n当前值:%s" // "This value affects network request amount for loading image. Lower this value may reduce the chance to get IP Ban, but loading speed will be slower too. Tachiyomi restart required."
|
||||
|
||||
private const val SHOW_ZH_HANT_WEBSITE_PREF = "showZhHantWebsite"
|
||||
private const val SHOW_ZH_HANT_WEBSITE_PREF_TITLE = "使用繁体版网站" // "Use traditional chinese version website"
|
||||
private const val SHOW_ZH_HANT_WEBSITE_PREF_SUMMARY = "需要重启软件以生效。" // "You need to restart Tachiyomi"
|
||||
|
||||
private val replaceCookiesRegex = Regex("""mangabz_lang=\d[;\s]*""")
|
||||
private val extractMangaIdRegex = Regex("""\d+bz""")
|
||||
private val chapterNumRegex = Regex("""\d+""")
|
||||
private val dateRegex1 = Regex("""\d{4}-\d{1,2}-\d{1,2}""")
|
||||
private val dateRegex2 = Regex("""(\d{1,2})月(\d{1,2})[号號]?""")
|
||||
private val extractPageNumFromImageUrlRegex = Regex("""/(\d+)_\d+\.""")
|
||||
|
||||
private val ENTRIES_ARRAY = (1..10).map { i -> i.toString() }.toTypedArray()
|
||||
const val PREFIX_ID_SEARCH = "id:"
|
||||
}
|
||||
}
|