Add MangaDig and make ColaManga a multisrc (#1139)
* Add MangaDig and make ColaManga a multisrc * Make MangaDig NSFW * Fix linting * Fix next page selector
21
multisrc/overrides/colamanga/default/AndroidManifest.xml
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<activity
|
||||
android:name="eu.kanade.tachiyomi.multisrc.colamanga.ColaMangaUrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data
|
||||
android:host="${SOURCEHOST}"
|
||||
android:scheme="${SOURCESCHEME}"
|
||||
android:pathPattern="/manga-..*/" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
3
multisrc/overrides/colamanga/default/additional.gradle
Normal file
@ -0,0 +1,3 @@
|
||||
dependencies {
|
||||
implementation(project(":lib:synchrony"))
|
||||
}
|
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 6.0 KiB |
123
multisrc/overrides/colamanga/mangadig/src/MangaDig.kt
Normal file
@ -0,0 +1,123 @@
|
||||
package eu.kanade.tachiyomi.extension.en.mangadig
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.colamanga.ColaManga
|
||||
import eu.kanade.tachiyomi.multisrc.colamanga.UriPartFilter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
|
||||
class MangaDig : ColaManga("MangaDig", "https://mangadig.com", "en") {
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a:contains(Next):not(.fed-btns-disad)"
|
||||
|
||||
override val statusTitle = "Status"
|
||||
override val authorTitle = "Author"
|
||||
override val genreTitle = "Category"
|
||||
override val statusOngoing = "OnGoing"
|
||||
override val statusCompleted = "Complete"
|
||||
|
||||
override fun getFilterList(): FilterList {
|
||||
val filters = buildList {
|
||||
addAll(super.getFilterList().list)
|
||||
add(SortFilter())
|
||||
add(CategoryFilter())
|
||||
add(CharFilter())
|
||||
add(StatusFilter())
|
||||
}
|
||||
|
||||
return FilterList(filters)
|
||||
}
|
||||
|
||||
private class StatusFilter : UriPartFilter(
|
||||
"Status",
|
||||
"status",
|
||||
arrayOf(
|
||||
Pair("All", ""),
|
||||
Pair("Ongoing", "1"),
|
||||
Pair("Complete", "2"),
|
||||
),
|
||||
)
|
||||
|
||||
private class SortFilter : UriPartFilter(
|
||||
"Order by",
|
||||
"orderBy",
|
||||
arrayOf(
|
||||
Pair("Last updated", "update"),
|
||||
Pair("Recently added", "create"),
|
||||
Pair("Most popular today", "dailyCount"),
|
||||
Pair("Most popular this week", "weeklyCount"),
|
||||
Pair("Most popular this month", "monthlyCount"),
|
||||
),
|
||||
2,
|
||||
)
|
||||
|
||||
private class CategoryFilter : UriPartFilter(
|
||||
"Genre",
|
||||
"mainCategoryId",
|
||||
arrayOf(
|
||||
Pair("All", ""),
|
||||
Pair("Romance", "10008"),
|
||||
Pair("Drama", "10005"),
|
||||
Pair("Comedy", "10004"),
|
||||
Pair("Fantasy", "10006"),
|
||||
Pair("Action", "10002"),
|
||||
Pair("CEO", "10142"),
|
||||
Pair("Webtoons", "10012"),
|
||||
Pair("Historical", "10021"),
|
||||
Pair("Adventure", "10003"),
|
||||
Pair("Josei", "10059"),
|
||||
Pair("Smut", "10047"),
|
||||
Pair("Supernatural", "10018"),
|
||||
Pair("School life", "10017"),
|
||||
Pair("Completed", "10423"),
|
||||
Pair("Possessive", "10284"),
|
||||
Pair("Manhua", "10010"),
|
||||
Pair("Sweet", "10282"),
|
||||
Pair("Harem", "10007"),
|
||||
Pair("Slice of life", "10026"),
|
||||
Pair("Girl Power", "10144"),
|
||||
Pair("Martial arts", "10013"),
|
||||
Pair("Chinese Classic", "10243"),
|
||||
Pair("BL", "10262"),
|
||||
Pair("Manhwa", "10039"),
|
||||
Pair("Adult", "10030"),
|
||||
Pair("Shounen", "10009"),
|
||||
Pair("TimeTravel", "10143"),
|
||||
Pair("Shoujo", "10054"),
|
||||
Pair("Ecchi", "10027"),
|
||||
Pair("Revenge", "10556"),
|
||||
),
|
||||
)
|
||||
|
||||
private class CharFilter : UriPartFilter(
|
||||
"Alphabet",
|
||||
"charCategoryId",
|
||||
arrayOf(
|
||||
Pair("All", ""),
|
||||
Pair("A", "10015"),
|
||||
Pair("B", "10028"),
|
||||
Pair("C", "10055"),
|
||||
Pair("D", "10034"),
|
||||
Pair("E", "10049"),
|
||||
Pair("F", "10056"),
|
||||
Pair("G", "10023"),
|
||||
Pair("H", "10037"),
|
||||
Pair("I", "10035"),
|
||||
Pair("J", "10060"),
|
||||
Pair("K", "10022"),
|
||||
Pair("L", "10046"),
|
||||
Pair("M", "10020"),
|
||||
Pair("N", "10044"),
|
||||
Pair("O", "10024"),
|
||||
Pair("P", "10048"),
|
||||
Pair("Q", "10051"),
|
||||
Pair("R", "10025"),
|
||||
Pair("S", "10011"),
|
||||
Pair("T", "10001"),
|
||||
Pair("U", "10058"),
|
||||
Pair("V", "10016"),
|
||||
Pair("W", "10052"),
|
||||
Pair("X", "10061"),
|
||||
Pair("Y", "10036"),
|
||||
Pair("Z", "10101"),
|
||||
),
|
||||
)
|
||||
}
|
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 8.6 KiB |
120
multisrc/overrides/colamanga/onemanhua/src/Onemanhua.kt
Normal file
@ -0,0 +1,120 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.onemanhua
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.colamanga.ColaManga
|
||||
import eu.kanade.tachiyomi.multisrc.colamanga.UriPartFilter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
|
||||
class Onemanhua : ColaManga("COLAMANGA", "https://www.colamanga.com", "zh") {
|
||||
override val id = 8252565807829914103 // name used to be "One漫画"
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a:contains(下页):not(.fed-btns-disad)"
|
||||
|
||||
override val statusTitle = "状态"
|
||||
override val authorTitle = "作者"
|
||||
override val genreTitle = "类别"
|
||||
override val statusOngoing = "连载中"
|
||||
override val statusCompleted = "已完结"
|
||||
|
||||
override fun getFilterList(): FilterList {
|
||||
val filters = buildList {
|
||||
addAll(super.getFilterList().list)
|
||||
add(SortFilter())
|
||||
add(CategoryFilter())
|
||||
add(CharFilter())
|
||||
add(StatusFilter())
|
||||
}
|
||||
|
||||
return FilterList(filters)
|
||||
}
|
||||
|
||||
private class StatusFilter : UriPartFilter(
|
||||
"状态",
|
||||
"status",
|
||||
arrayOf(
|
||||
Pair("全部", ""),
|
||||
Pair("连载中", "1"),
|
||||
Pair("已完结", "2"),
|
||||
),
|
||||
)
|
||||
private class SortFilter : UriPartFilter(
|
||||
"排序",
|
||||
"orderBy",
|
||||
arrayOf(
|
||||
Pair("更新日", "update"),
|
||||
Pair("日点击", "dailyCount"),
|
||||
Pair("周点击", "weeklyCount"),
|
||||
Pair("月点击", "monthlyCount"),
|
||||
),
|
||||
1,
|
||||
)
|
||||
private class CategoryFilter : UriPartFilter(
|
||||
"类型",
|
||||
"mainCategoryId",
|
||||
arrayOf(
|
||||
Pair("全部", ""),
|
||||
Pair("热血", "10023"),
|
||||
Pair("玄幻", "10024"),
|
||||
Pair("恋爱", "10126"),
|
||||
Pair("冒险", "10210"),
|
||||
Pair("古风", "10143"),
|
||||
Pair("都市", "10124"),
|
||||
Pair("穿越", "10129"),
|
||||
Pair("奇幻", "10242"),
|
||||
Pair("其他", "10560"),
|
||||
Pair("少男", "10641"),
|
||||
Pair("搞笑", "10122"),
|
||||
Pair("战斗", "10309"),
|
||||
Pair("冒险热血", "11224"),
|
||||
Pair("重生", "10461"),
|
||||
Pair("爆笑", "10201"),
|
||||
Pair("逆袭", "10943"),
|
||||
Pair("后宫", "10138"),
|
||||
Pair("少年", "10321"),
|
||||
Pair("少女", "10301"),
|
||||
Pair("熱血", "12044"),
|
||||
Pair("系统", "10722"),
|
||||
Pair("动作", "10125"),
|
||||
Pair("校园", "10131"),
|
||||
Pair("冒險", "12123"),
|
||||
Pair("修真", "10133"),
|
||||
Pair("修仙", "10453"),
|
||||
Pair("剧情", "10480"),
|
||||
Pair("霸总", "10127"),
|
||||
Pair("大女主", "10706"),
|
||||
Pair("生活", "10142"),
|
||||
),
|
||||
)
|
||||
private class CharFilter : UriPartFilter(
|
||||
"字母",
|
||||
"charCategoryId",
|
||||
arrayOf(
|
||||
Pair("全部", ""),
|
||||
Pair("A", "10182"),
|
||||
Pair("B", "10081"),
|
||||
Pair("C", "10134"),
|
||||
Pair("D", "10001"),
|
||||
Pair("E", "10238"),
|
||||
Pair("F", "10161"),
|
||||
Pair("G", "10225"),
|
||||
Pair("H", "10137"),
|
||||
Pair("I", "10284"),
|
||||
Pair("J", "10141"),
|
||||
Pair("K", "10283"),
|
||||
Pair("L", "10132"),
|
||||
Pair("M", "10136"),
|
||||
Pair("N", "10130"),
|
||||
Pair("O", "10282"),
|
||||
Pair("P", "10262"),
|
||||
Pair("Q", "10164"),
|
||||
Pair("R", "10240"),
|
||||
Pair("S", "10121"),
|
||||
Pair("T", "10123"),
|
||||
Pair("U", "11184"),
|
||||
Pair("V", "11483"),
|
||||
Pair("W", "10135"),
|
||||
Pair("X", "10061"),
|
||||
Pair("Y", "10082"),
|
||||
Pair("Z", "10128"),
|
||||
),
|
||||
)
|
||||
}
|
@ -0,0 +1,334 @@
|
||||
package eu.kanade.tachiyomi.multisrc.colamanga
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.View
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebView
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.lib.synchrony.Deobfuscator
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
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.ParsedHttpSource
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
abstract class ColaManga(
|
||||
final override val name: String,
|
||||
final override val baseUrl: String,
|
||||
final override val lang: String,
|
||||
) : ParsedHttpSource(), ConfigurableSource {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val intl = ColaMangaIntl(lang)
|
||||
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.rateLimitHost(
|
||||
baseUrl.toHttpUrl(),
|
||||
preferences.getString(RATE_LIMIT_PREF_KEY, RATE_LIMIT_PREF_DEFAULT)!!.toInt(),
|
||||
preferences.getString(RATE_LIMIT_PERIOD_PREF_KEY, RATE_LIMIT_PERIOD_PREF_DEFAULT)!!.toLong(),
|
||||
TimeUnit.MILLISECONDS,
|
||||
)
|
||||
.addInterceptor(ColaMangaImageInterceptor())
|
||||
.build()
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Origin", baseUrl)
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
GET("$baseUrl/show?orderBy=dailyCount&page=$page", headers)
|
||||
|
||||
override fun popularMangaSelector() = "li.fed-list-item"
|
||||
|
||||
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
||||
element.selectFirst("a.fed-list-title")!!.let {
|
||||
setUrlWithoutDomain(it.attr("href"))
|
||||
title = it.text()
|
||||
}
|
||||
thumbnail_url = element.selectFirst("a.fed-list-pics")?.absUrl("data-original")
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
GET("$baseUrl/show?orderBy=update&page=$page", headers)
|
||||
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = if (query.isNotEmpty()) {
|
||||
"$baseUrl/search".toHttpUrl().newBuilder().apply {
|
||||
filters.ifEmpty { getFilterList() }
|
||||
.firstOrNull { it is SearchTypeFilter }
|
||||
?.let { (it as SearchTypeFilter).addToUri(this) }
|
||||
|
||||
addQueryParameter("searchString", query)
|
||||
addQueryParameter("page", page.toString())
|
||||
}.build()
|
||||
} else {
|
||||
"$baseUrl/show".toHttpUrl().newBuilder().apply {
|
||||
filters.ifEmpty { getFilterList() }
|
||||
.filterIsInstance<UriFilter>()
|
||||
.filterNot { it is SearchTypeFilter }
|
||||
.forEach { it.addToUri(this) }
|
||||
|
||||
addQueryParameter("page", page.toString())
|
||||
}.build()
|
||||
}
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun fetchSearchManga(
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: FilterList,
|
||||
): Observable<MangasPage> {
|
||||
return if (query.startsWith(PREFIX_SLUG_SEARCH)) {
|
||||
val slug = query.removePrefix(PREFIX_SLUG_SEARCH)
|
||||
val url = "/$slug/"
|
||||
|
||||
fetchMangaDetails(SManga.create().apply { this.url = url })
|
||||
.map { MangasPage(listOf(it.apply { this.url = url }), false) }
|
||||
} else {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "dl.fed-data-info, ${popularMangaSelector()}"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
if (element.tagName() == "li") {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
return SManga.create().apply {
|
||||
element.selectFirst("h1.fed-part-eone a")!!.let {
|
||||
setUrlWithoutDomain(it.attr("href"))
|
||||
title = it.text()
|
||||
}
|
||||
|
||||
thumbnail_url = element.selectFirst("a.fed-list-pics")?.absUrl("data-original")
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = popularMangaSelector()
|
||||
|
||||
protected abstract val statusTitle: String
|
||||
protected abstract val authorTitle: String
|
||||
protected abstract val genreTitle: String
|
||||
protected abstract val statusOngoing: String
|
||||
protected abstract val statusCompleted: String
|
||||
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
title = document.selectFirst("h1.fed-part-eone")!!.text()
|
||||
thumbnail_url = document.selectFirst("a.fed-list-pics")?.absUrl("data-orignal")
|
||||
author = document.selectFirst("span.fed-text-muted:contains($authorTitle) + a")?.text()
|
||||
genre = document.select("span.fed-text-muted:contains($genreTitle) ~ a").joinToString { it.text() }
|
||||
description = document
|
||||
.selectFirst("ul.fed-part-rows li.fed-col-xs12.fed-show-md-block .fed-part-esan")
|
||||
?.ownText()
|
||||
status = when (document.selectFirst("span.fed-text-muted:contains($statusTitle) + a")?.text()) {
|
||||
statusOngoing -> SManga.ONGOING
|
||||
statusCompleted -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListSelector(): String = "div:not(.fed-hidden) > div.all_data_list > ul.fed-part-rows a"
|
||||
|
||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
name = element.attr("title")
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val interfaceName = randomString()
|
||||
|
||||
document.body().prepend(
|
||||
"""
|
||||
<script>
|
||||
!function () {
|
||||
__cr.init();
|
||||
__cad.setCookieValue();
|
||||
|
||||
const pageCountKey = __cad.getCookieValue()[1] + mh_info.pageid.toString();
|
||||
const pageCount = parseInt($.cookie(pageCountKey) || "0");
|
||||
const images = [...Array(pageCount).keys()].map((i) => __cr.getPicUrl(i + 1));
|
||||
|
||||
__cr.isfromMangaRead = 1;
|
||||
|
||||
const key = CryptoJS.enc.Utf8.stringify(__js.getDataParse());
|
||||
|
||||
window.$interfaceName.passData(JSON.stringify({ images, key }), window.image_info.keyType || "0");
|
||||
}();
|
||||
</script>
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
val latch = CountDownLatch(1)
|
||||
val jsInterface = JsInterface(latch, json)
|
||||
var webView: WebView? = null
|
||||
|
||||
handler.post {
|
||||
val innerWv = WebView(Injekt.get<Application>())
|
||||
webView = innerWv
|
||||
innerWv.settings.javaScriptEnabled = true
|
||||
innerWv.settings.domStorageEnabled = true
|
||||
innerWv.settings.blockNetworkImage = true
|
||||
innerWv.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
|
||||
innerWv.addJavascriptInterface(jsInterface, interfaceName)
|
||||
|
||||
innerWv.loadDataWithBaseURL(document.location(), document.outerHtml(), "text/html", "UTF-8", null)
|
||||
}
|
||||
|
||||
latch.await(30L, TimeUnit.SECONDS)
|
||||
handler.post { webView?.destroy() }
|
||||
|
||||
if (latch.count == 1L) {
|
||||
throw Exception(intl.timedOutDecryptingImageLinks)
|
||||
}
|
||||
|
||||
val key = if (jsInterface.keyType.isNotEmpty()) {
|
||||
keyMapping[jsInterface.keyType]
|
||||
?: throw Exception(intl.couldNotFindKey(jsInterface.keyType))
|
||||
} else {
|
||||
jsInterface.key
|
||||
}
|
||||
|
||||
return jsInterface.images.mapIndexed { i, it ->
|
||||
val imageUrl = buildString(it.length + 6) {
|
||||
if (it.startsWith("//")) {
|
||||
append("https:")
|
||||
}
|
||||
|
||||
append(it)
|
||||
|
||||
if (key.isNotEmpty()) {
|
||||
append("#")
|
||||
append(ColaMangaImageInterceptor.KEY_PREFIX)
|
||||
append(key)
|
||||
}
|
||||
}
|
||||
|
||||
Page(i, imageUrl = imageUrl)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
SearchTypeFilter(intl),
|
||||
)
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = RATE_LIMIT_PREF_KEY
|
||||
title = intl.rateLimitPrefTitle
|
||||
summary = intl.rateLimitPrefSummary(RATE_LIMIT_PREF_DEFAULT)
|
||||
entries = RATE_LIMIT_PREF_ENTRIES
|
||||
entryValues = RATE_LIMIT_PREF_ENTRIES
|
||||
|
||||
setDefaultValue(RATE_LIMIT_PREF_DEFAULT)
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = RATE_LIMIT_PERIOD_PREF_KEY
|
||||
title = intl.rateLimitPeriodPrefTitle
|
||||
summary = intl.rateLimitPeriodPrefSummary(RATE_LIMIT_PERIOD_PREF_DEFAULT)
|
||||
entries = RATE_LIMIT_PERIOD_PREF_ENTRIES
|
||||
entryValues = RATE_LIMIT_PERIOD_PREF_ENTRIES
|
||||
|
||||
setDefaultValue(RATE_LIMIT_PERIOD_PREF_DEFAULT)
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
private val keyMappingRegex = Regex("""[0-9A-Za-z_]+\s*==\s*['"](?<keyType>\d+)['"]\s*&&\s*\([0-9A-Za-z_]+\s*=\s*['"](?<key>[a-zA-Z0-9]+)['"]\)""")
|
||||
|
||||
private val keyMapping by lazy {
|
||||
val obfuscatedReadJs = client.newCall(GET("$baseUrl/js/manga.read.js")).execute().body.string()
|
||||
val readJs = Deobfuscator.deobfuscateScript(obfuscatedReadJs)
|
||||
?: throw Exception(intl.couldNotDeobufscateScript)
|
||||
|
||||
keyMappingRegex.findAll(readJs).associate { it.groups["keyType"]!!.value to it.groups["key"]!!.value }
|
||||
}
|
||||
|
||||
private fun randomString() = buildString(15) {
|
||||
val charPool = ('a'..'z') + ('A'..'Z')
|
||||
|
||||
for (i in 0 until 15) {
|
||||
append(charPool.random())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNUSED")
|
||||
private class JsInterface(private val latch: CountDownLatch, private val json: Json) {
|
||||
var images: List<String> = listOf()
|
||||
private set
|
||||
|
||||
var key: String = ""
|
||||
private set
|
||||
|
||||
var keyType: String = ""
|
||||
private set
|
||||
|
||||
@JavascriptInterface
|
||||
fun passData(rawData: String, keyType: String) {
|
||||
val data = json.parseToJsonElement(rawData).jsonObject
|
||||
|
||||
images = data["images"]!!.jsonArray.map { it.jsonPrimitive.content }
|
||||
key = data["key"]!!.jsonPrimitive.content
|
||||
|
||||
if (keyType != "0") {
|
||||
this.keyType = keyType
|
||||
}
|
||||
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal const val PREFIX_SLUG_SEARCH = "slug:"
|
||||
}
|
||||
}
|
||||
|
||||
private const val RATE_LIMIT_PREF_KEY = "mainSiteRatePermitsPreference"
|
||||
private const val RATE_LIMIT_PREF_DEFAULT = "1"
|
||||
private val RATE_LIMIT_PREF_ENTRIES = (1..10).map { i -> i.toString() }.toTypedArray()
|
||||
|
||||
private const val RATE_LIMIT_PERIOD_PREF_KEY = "mainSiteRatePeriodMillisPreference"
|
||||
private const val RATE_LIMIT_PERIOD_PREF_DEFAULT = "2500"
|
||||
private val RATE_LIMIT_PERIOD_PREF_ENTRIES = (2000..6000 step 500).map { i -> i.toString() }.toTypedArray()
|
@ -0,0 +1,32 @@
|
||||
package eu.kanade.tachiyomi.multisrc.colamanga
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
interface UriFilter {
|
||||
fun addToUri(builder: HttpUrl.Builder)
|
||||
}
|
||||
|
||||
open class UriPartFilter(
|
||||
name: String,
|
||||
private val param: String,
|
||||
private val vals: Array<Pair<String, String>>,
|
||||
state: Int = 0,
|
||||
) : Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state), UriFilter {
|
||||
override fun addToUri(builder: HttpUrl.Builder) {
|
||||
val uriPart = vals[state].second
|
||||
|
||||
if (uriPart.isNotEmpty()) {
|
||||
builder.addQueryParameter(param, uriPart)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SearchTypeFilter(intl: ColaMangaIntl) : UriPartFilter(
|
||||
intl.searchType,
|
||||
"type",
|
||||
arrayOf(
|
||||
intl.searchTypeFuzzy to "1",
|
||||
intl.searchTypeExact to "2",
|
||||
),
|
||||
)
|
@ -0,0 +1,25 @@
|
||||
package eu.kanade.tachiyomi.multisrc.colamanga
|
||||
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
class ColaMangaGenerator : ThemeSourceGenerator {
|
||||
|
||||
override val themePkg = "colamanga"
|
||||
|
||||
override val themeClass = "ColaManga"
|
||||
|
||||
override val baseVersionCode = 1
|
||||
|
||||
override val sources = listOf(
|
||||
SingleLang("COLAMANGA", "https://www.colamanga.com", "zh", overrideVersionCode = 15, className = "Onemanhua"),
|
||||
SingleLang("MangaDig", "https://mangadig.com", "en", isNsfw = true),
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
ColaMangaGenerator().createAll()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package eu.kanade.tachiyomi.multisrc.colamanga
|
||||
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class ColaMangaImageInterceptor : Interceptor {
|
||||
private val iv = "0000000000000000".toByteArray()
|
||||
private val mediaType = "image/jpeg".toMediaType()
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val response = chain.proceed(request)
|
||||
|
||||
if (request.url.fragment?.startsWith(KEY_PREFIX) != true) {
|
||||
return response
|
||||
}
|
||||
|
||||
val key = request.url.fragment!!.substringAfter(KEY_PREFIX).toByteArray()
|
||||
val output = Cipher.getInstance("AES/CBC/PKCS7Padding").let {
|
||||
it.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
||||
it.doFinal(response.body.bytes())
|
||||
}
|
||||
|
||||
return response.newBuilder()
|
||||
.body(output.toResponseBody(mediaType))
|
||||
.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal const val KEY_PREFIX = "key="
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package eu.kanade.tachiyomi.multisrc.colamanga
|
||||
|
||||
class ColaMangaIntl(private val lang: String) {
|
||||
|
||||
val rateLimitPrefTitle = when (lang) {
|
||||
"zh" -> "主站连接限制"
|
||||
else -> "Rate limit"
|
||||
}
|
||||
|
||||
fun rateLimitPrefSummary(defaultValue: String) = when (lang) {
|
||||
"zh" -> "此值影响主站的连接请求量。降低此值可以减少获得HTTP 403错误的几率,但加载速度也会变慢。需要重启软件以生效。\n默认值:$defaultValue\n当前值:%s"
|
||||
else -> "Number of requests made to the website. Lowering this value may reduce the chance of getting HTTP 403. Tachiyomi restart required.\nDefault value: $defaultValue\nCurrent value: %s"
|
||||
}
|
||||
|
||||
val rateLimitPeriodPrefTitle = when (lang) {
|
||||
"zh" -> "主站连接限制期"
|
||||
else -> "Rate limit period"
|
||||
}
|
||||
|
||||
fun rateLimitPeriodPrefSummary(defaultValue: String) = when (lang) {
|
||||
"zh" -> "此值影响主站点连接限制时的延迟(毫秒)。增加这个值可能会减少出现HTTP 403错误的机会,但加载速度也会变慢。需要重启软件以生效。\n默认值:$defaultValue\n当前值:%s"
|
||||
else -> "Time in milliseconds to wait after using up all allowed requests. Lowering this value may reduce the chance of getting HTTP 403. Tachiyomi restart required.\nDefault value: $defaultValue\nCurrent value: %s"
|
||||
}
|
||||
|
||||
val timedOutDecryptingImageLinks = when (lang) {
|
||||
else -> "Timed out decrypting image links"
|
||||
}
|
||||
|
||||
val couldNotDeobufscateScript = when (lang) {
|
||||
else -> "Could not deobfuscate script"
|
||||
}
|
||||
|
||||
fun couldNotFindKey(forKeyType: String) = when (lang) {
|
||||
else -> "Could not find key for keyType $forKeyType"
|
||||
}
|
||||
|
||||
val searchType = when (lang) {
|
||||
"zh" -> "搜索类型"
|
||||
else -> "Search type"
|
||||
}
|
||||
|
||||
val searchTypeFuzzy = when (lang) {
|
||||
"zh" -> "模糊"
|
||||
else -> "Fuzzy"
|
||||
}
|
||||
|
||||
val searchTypeExact = when (lang) {
|
||||
"zh" -> "精确"
|
||||
else -> "Exact"
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package eu.kanade.tachiyomi.multisrc.colamanga
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class ColaMangaUrlActivity : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val pathSegments = intent?.data?.pathSegments
|
||||
|
||||
if (pathSegments != null && pathSegments.size > 0) {
|
||||
val intent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", "${ColaManga.PREFIX_SLUG_SEARCH}${pathSegments[0]}")
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
||||
try {
|
||||
startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e("ColaMangaUrlActivity", "Could not start activity", e)
|
||||
}
|
||||
} else {
|
||||
Log.e("ColaMangaUrlActivity", "Could not parse URI from intent $intent")
|
||||
}
|
||||
|
||||
finish()
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
ext {
|
||||
extName = 'COLAMANGA'
|
||||
extClass = '.Onemanhua'
|
||||
extVersionCode = 15
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib:synchrony"))
|
||||
}
|
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 11 KiB |
@ -1,496 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.onemanhua
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.webkit.ConsoleMessage
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebView
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.lib.synchrony.Deobfuscator
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
// Originally, the site was called One漫画. The name has been changing every once in awhile
|
||||
class Onemanhua : ConfigurableSource, ParsedHttpSource() {
|
||||
override val id = 8252565807829914103 // name used to be "One漫画"
|
||||
override val lang = "zh"
|
||||
override val supportsLatest = true
|
||||
override val name = "COLAMANGA"
|
||||
override val baseUrl = "https://www.colamanga.com"
|
||||
|
||||
// Preference setting
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
private val cookieManager by lazy { CookieManager.getInstance() }
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.rateLimitHost(
|
||||
baseUrl.toHttpUrl(),
|
||||
preferences.getString(MAINSITE_RATEPERMITS_PREF, MAINSITE_RATEPERMITS_PREF_DEFAULT)!!.toInt(),
|
||||
preferences.getString(MAINSITE_RATEPERIOD_PREF, MAINSITE_RATEPERIOD_PREF_DEFAULT)!!.toLong(),
|
||||
TimeUnit.MILLISECONDS,
|
||||
)
|
||||
.addInterceptor { chain ->
|
||||
val response = chain.proceed(chain.request())
|
||||
|
||||
if (response.request.url.fragment?.contains("key") != true) {
|
||||
return@addInterceptor response
|
||||
}
|
||||
|
||||
val keyStr = response.request.url.fragment!!.substringAfter("key=")
|
||||
val key = keyStr.toByteArray()
|
||||
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding")
|
||||
cipher.init(
|
||||
Cipher.DECRYPT_MODE,
|
||||
SecretKeySpec(key, "AES"),
|
||||
IvParameterSpec("0000000000000000".toByteArray()),
|
||||
)
|
||||
|
||||
val output = cipher.doFinal(response.body.bytes())
|
||||
response.newBuilder()
|
||||
.body(output.toResponseBody("image/jpeg".toMediaType()))
|
||||
.build()
|
||||
}
|
||||
.build()
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||
.add("Origin", baseUrl)
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
// Common
|
||||
private var commonSelector = "li.fed-list-item"
|
||||
private var commonNextPageSelector = "a:contains(下页):not(.fed-btns-disad)"
|
||||
private fun commonMangaFromElement(element: Element): SManga {
|
||||
val picElement = element.selectFirst("a.fed-list-pics")!!
|
||||
val manga = SManga.create().apply {
|
||||
title = element.selectFirst("a.fed-list-title")!!.text()
|
||||
thumbnail_url = picElement.attr("data-original")
|
||||
}
|
||||
|
||||
manga.setUrlWithoutDomain(picElement.attr("href"))
|
||||
|
||||
return manga
|
||||
}
|
||||
|
||||
// Popular Manga
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/show?orderBy=dailyCount&page=$page", headers)
|
||||
override fun popularMangaNextPageSelector() = commonNextPageSelector
|
||||
override fun popularMangaSelector() = commonSelector
|
||||
override fun popularMangaFromElement(element: Element) = commonMangaFromElement(element)
|
||||
|
||||
// Latest Updates
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/show?orderBy=update&page=$page", headers)
|
||||
override fun latestUpdatesNextPageSelector() = commonNextPageSelector
|
||||
override fun latestUpdatesSelector() = commonSelector
|
||||
override fun latestUpdatesFromElement(element: Element) = commonMangaFromElement(element)
|
||||
|
||||
// Filter
|
||||
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
private class StatusFilter : UriPartFilter(
|
||||
"状态",
|
||||
arrayOf(
|
||||
Pair("全部", ""),
|
||||
Pair("连载中", "1"),
|
||||
Pair("已完结", "2"),
|
||||
),
|
||||
)
|
||||
private class SortFilter : UriPartFilter(
|
||||
"排序",
|
||||
arrayOf(
|
||||
Pair("更新日", "update"),
|
||||
Pair("日点击", "dailyCount"),
|
||||
Pair("周点击", "weeklyCount"),
|
||||
Pair("月点击", "monthlyCount"),
|
||||
),
|
||||
)
|
||||
private class CategoryFilter : UriPartFilter(
|
||||
"类型",
|
||||
arrayOf(
|
||||
Pair("全部", ""),
|
||||
Pair("热血", "10023"),
|
||||
Pair("玄幻", "10024"),
|
||||
Pair("恋爱", "10126"),
|
||||
Pair("冒险", "10210"),
|
||||
Pair("古风", "10143"),
|
||||
Pair("都市", "10124"),
|
||||
Pair("穿越", "10129"),
|
||||
Pair("奇幻", "10242"),
|
||||
Pair("其他", "10560"),
|
||||
Pair("少男", "10641"),
|
||||
Pair("搞笑", "10122"),
|
||||
Pair("战斗", "10309"),
|
||||
Pair("冒险热血", "11224"),
|
||||
Pair("重生", "10461"),
|
||||
Pair("爆笑", "10201"),
|
||||
Pair("逆袭", "10943"),
|
||||
Pair("后宫", "10138"),
|
||||
Pair("少年", "10321"),
|
||||
Pair("少女", "10301"),
|
||||
Pair("熱血", "12044"),
|
||||
Pair("系统", "10722"),
|
||||
Pair("动作", "10125"),
|
||||
Pair("校园", "10131"),
|
||||
Pair("冒險", "12123"),
|
||||
Pair("修真", "10133"),
|
||||
Pair("修仙", "10453"),
|
||||
Pair("剧情", "10480"),
|
||||
Pair("霸总", "10127"),
|
||||
Pair("大女主", "10706"),
|
||||
Pair("生活", "10142"),
|
||||
),
|
||||
)
|
||||
private class CharFilter : UriPartFilter(
|
||||
"字母",
|
||||
arrayOf(
|
||||
Pair("全部", ""),
|
||||
Pair("A", "10182"),
|
||||
Pair("B", "10081"),
|
||||
Pair("C", "10134"),
|
||||
Pair("D", "10001"),
|
||||
Pair("E", "10238"),
|
||||
Pair("F", "10161"),
|
||||
Pair("G", "10225"),
|
||||
Pair("H", "10137"),
|
||||
Pair("I", "10284"),
|
||||
Pair("J", "10141"),
|
||||
Pair("K", "10283"),
|
||||
Pair("L", "10132"),
|
||||
Pair("M", "10136"),
|
||||
Pair("N", "10130"),
|
||||
Pair("O", "10282"),
|
||||
Pair("P", "10262"),
|
||||
Pair("Q", "10164"),
|
||||
Pair("R", "10240"),
|
||||
Pair("S", "10121"),
|
||||
Pair("T", "10123"),
|
||||
Pair("U", "11184"),
|
||||
Pair("V", "11483"),
|
||||
Pair("W", "10135"),
|
||||
Pair("X", "10061"),
|
||||
Pair("Y", "10082"),
|
||||
Pair("Z", "10128"),
|
||||
),
|
||||
)
|
||||
override fun getFilterList() = FilterList(
|
||||
SortFilter(),
|
||||
CategoryFilter(),
|
||||
CharFilter(),
|
||||
StatusFilter(),
|
||||
)
|
||||
|
||||
// Search
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
return if (query.isNotBlank()) {
|
||||
GET("$baseUrl/search?searchString=$query&page=$page", headers)
|
||||
} else {
|
||||
val url = "$baseUrl/show".toHttpUrl().newBuilder()
|
||||
url.addQueryParameter("page", page.toString())
|
||||
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is StatusFilter -> {
|
||||
if (filter.state != 0) {
|
||||
url.addQueryParameter("status", filter.toUriPart())
|
||||
}
|
||||
}
|
||||
is SortFilter -> {
|
||||
url.addQueryParameter("orderBy", filter.toUriPart())
|
||||
}
|
||||
is CategoryFilter -> {
|
||||
if (filter.state != 0) {
|
||||
url.addQueryParameter("mainCategoryId", filter.toUriPart())
|
||||
}
|
||||
}
|
||||
is CharFilter -> {
|
||||
if (filter.state != 0) {
|
||||
url.addQueryParameter("charCategoryId", filter.toUriPart())
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
GET(url.toString(), headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = commonNextPageSelector
|
||||
override fun searchMangaSelector() = "dl.fed-deta-info, $commonSelector"
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
if (element.tagName() == "li") {
|
||||
return commonMangaFromElement(element)
|
||||
}
|
||||
|
||||
val picElement = element.selectFirst("a.fed-list-pics")!!
|
||||
val manga = SManga.create().apply {
|
||||
title = element.selectFirst("h1.fed-part-eone a")!!.text()
|
||||
thumbnail_url = picElement.attr("data-original")
|
||||
}
|
||||
|
||||
manga.setUrlWithoutDomain(picElement.attr("href"))
|
||||
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val picElement = document.selectFirst("a.fed-list-pics")!!
|
||||
val detailElements = document.select("ul.fed-part-rows li.fed-col-xs12")
|
||||
return SManga.create().apply {
|
||||
title = document.selectFirst("h1.fed-part-eone")!!.text().trim()
|
||||
thumbnail_url = picElement.attr("data-original")
|
||||
|
||||
status = when (
|
||||
detailElements.firstOrNull {
|
||||
it.children().firstOrNull { it2 ->
|
||||
it2.hasClass("fed-text-muted") && it2.ownText() == "状态"
|
||||
} != null
|
||||
}?.select("a")?.first()?.text()
|
||||
) {
|
||||
"连载中" -> SManga.ONGOING
|
||||
"已完结" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
author = detailElements.firstOrNull {
|
||||
it.children().firstOrNull { it2 ->
|
||||
it2.hasClass("fed-text-muted") && it2.ownText() == "作者"
|
||||
} != null
|
||||
}?.select("a")?.first()?.text()
|
||||
|
||||
genre = detailElements.firstOrNull {
|
||||
it.children().firstOrNull { it2 ->
|
||||
it2.hasClass("fed-text-muted") && it2.ownText() == "类别"
|
||||
} != null
|
||||
}?.select("a")?.joinToString { it.text() }
|
||||
|
||||
description = document.select("ul.fed-part-rows li.fed-col-xs12.fed-show-md-block .fed-part-esan")
|
||||
.firstOrNull()?.text()?.trim()
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListSelector(): String = "div:not(.fed-hidden) > div.all_data_list > ul.fed-part-rows a"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val chapter = SChapter.create().apply {
|
||||
name = element.attr("title")
|
||||
}
|
||||
chapter.setUrlWithoutDomain(element.attr("href"))
|
||||
return chapter
|
||||
}
|
||||
|
||||
private fun randomString(length: Int = 10): String {
|
||||
val charPool = ('a'..'z') + ('A'..'Z')
|
||||
return List(length) { charPool.random() }.joinToString("")
|
||||
}
|
||||
|
||||
internal class JsObject(val latch: CountDownLatch, val cookieManager: CookieManager) {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
var images: List<String> = listOf()
|
||||
private set
|
||||
|
||||
var key: String = ""
|
||||
private set
|
||||
|
||||
var keyType: String = ""
|
||||
private set
|
||||
|
||||
@JavascriptInterface
|
||||
fun passJsonData(rawData: String) {
|
||||
val data = json.parseToJsonElement(rawData).jsonObject
|
||||
images = data["images"]!!.jsonArray.map { it.jsonPrimitive.content }
|
||||
key = data["key"]!!.jsonPrimitive.content
|
||||
|
||||
latch.countDown()
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun passKeyType(key: String) {
|
||||
keyType = key
|
||||
}
|
||||
}
|
||||
|
||||
private val keyMappingRegex = Regex("""[0-9A-Za-z_]+\s*==\s*['"](?<keyType>\d+)['"]\s*&&\s*\([0-9A-Za-z_]+\s*=\s*['"](?<key>[a-zA-Z0-9]+)['"]\)""")
|
||||
|
||||
private val keyMapping by lazy {
|
||||
val obfuscatedReadJs = client.newCall(GET("$baseUrl/js/manga.read.js")).execute().body.string()
|
||||
val readJs = Deobfuscator.deobfuscateScript(obfuscatedReadJs)
|
||||
?: throw Exception("Could not deobufuscate manga.read.js")
|
||||
|
||||
keyMappingRegex.findAll(readJs).associate { it.groups["keyType"]!!.value to it.groups["key"]!!.value }
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val interfaceName = randomString()
|
||||
document.body().prepend(
|
||||
"""
|
||||
<script>
|
||||
(function () {
|
||||
__cr.init();
|
||||
__cad.setCookieValue();
|
||||
|
||||
const pageCountKey = __cad.getCookieValue()[1] + mh_info.pageid.toString();
|
||||
const pageCount = parseInt($.cookie(pageCountKey) || "0");
|
||||
|
||||
const images = [...Array(pageCount).keys()].map((i) => __cr.getPicUrl(i + 1));
|
||||
|
||||
__cr.isfromMangaRead = 1
|
||||
const key = CryptoJS.enc.Utf8.stringify(__js.getDataParse())
|
||||
|
||||
if (!window.image_info.keyType || window.image_info.keyType != "0") {
|
||||
window.$interfaceName.passKeyType(window.image_info.keyType)
|
||||
}
|
||||
window.$interfaceName.passJsonData(JSON.stringify({ images, key }))
|
||||
})();
|
||||
</script>
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
val latch = CountDownLatch(1)
|
||||
val jsInterface = JsObject(latch, cookieManager)
|
||||
var webView: WebView? = null
|
||||
|
||||
handler.post {
|
||||
val webview = WebView(Injekt.get<Application>())
|
||||
webView = webview
|
||||
webview.settings.javaScriptEnabled = true
|
||||
webview.settings.domStorageEnabled = true
|
||||
webview.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
|
||||
webview.settings.useWideViewPort = false
|
||||
webview.settings.loadWithOverviewMode = false
|
||||
webview.settings.userAgentString = webview.settings.userAgentString.replace("Mobile", "eliboM").replace("Android", "diordnA")
|
||||
webview.addJavascriptInterface(jsInterface, interfaceName)
|
||||
|
||||
webview.webChromeClient = object : WebChromeClient() {
|
||||
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
|
||||
if (consoleMessage == null) { return false }
|
||||
val logContent = "wv: ${consoleMessage.message()} (${consoleMessage.sourceId()}, line ${consoleMessage.lineNumber()})"
|
||||
when (consoleMessage.messageLevel()) {
|
||||
ConsoleMessage.MessageLevel.DEBUG -> Log.d("onemanhua", logContent)
|
||||
ConsoleMessage.MessageLevel.ERROR -> Log.e("onemanhua", logContent)
|
||||
ConsoleMessage.MessageLevel.LOG -> Log.i("onemanhua", logContent)
|
||||
ConsoleMessage.MessageLevel.TIP -> Log.i("onemanhua", logContent)
|
||||
ConsoleMessage.MessageLevel.WARNING -> Log.w("onemanhua", logContent)
|
||||
else -> Log.d("onemanhua", logContent)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
webview.loadDataWithBaseURL(document.location(), document.toString(), "text/html", "UTF-8", null)
|
||||
}
|
||||
|
||||
latch.await()
|
||||
handler.post { webView?.destroy() }
|
||||
|
||||
val key = if (jsInterface.keyType.isNotEmpty()) {
|
||||
keyMapping[jsInterface.keyType]
|
||||
?: throw Exception("Could not find key mapping for keyType ${jsInterface.keyType}")
|
||||
} else {
|
||||
jsInterface.key
|
||||
}
|
||||
|
||||
return jsInterface.images.mapIndexed { i, url ->
|
||||
var imageUrl = url
|
||||
if (imageUrl.startsWith("//")) {
|
||||
imageUrl = "https:$imageUrl"
|
||||
}
|
||||
// Empty key means image is not encrypted
|
||||
if (key != "") {
|
||||
imageUrl = "$imageUrl#key=$key"
|
||||
}
|
||||
Page(i, imageUrl = imageUrl)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
|
||||
val mainSiteRatePermitsPreference = androidx.preference.ListPreference(screen.context).apply {
|
||||
key = MAINSITE_RATEPERMITS_PREF
|
||||
title = MAINSITE_RATEPERMITS_PREF_TITLE
|
||||
entries = MAINSITE_RATEPERMITS_PREF_ENTRIES_ARRAY
|
||||
entryValues = MAINSITE_RATEPERMITS_PREF_ENTRIES_ARRAY
|
||||
summary = MAINSITE_RATEPERMITS_PREF_SUMMARY
|
||||
|
||||
setDefaultValue(MAINSITE_RATEPERMITS_PREF_DEFAULT)
|
||||
}
|
||||
|
||||
val mainSiteRatePeriodPreference = androidx.preference.ListPreference(screen.context).apply {
|
||||
key = MAINSITE_RATEPERIOD_PREF
|
||||
title = MAINSITE_RATEPERIOD_PREF_TITLE
|
||||
entries = MAINSITE_RATEPERIOD_PREF_ENTRIES_ARRAY
|
||||
entryValues = MAINSITE_RATEPERIOD_PREF_ENTRIES_ARRAY
|
||||
summary = MAINSITE_RATEPERIOD_PREF_SUMMARY
|
||||
|
||||
setDefaultValue(MAINSITE_RATEPERIOD_PREF_DEFAULT)
|
||||
}
|
||||
|
||||
screen.addPreference(mainSiteRatePermitsPreference)
|
||||
screen.addPreference(mainSiteRatePeriodPreference)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAINSITE_RATEPERMITS_PREF = "mainSiteRatePermitsPreference"
|
||||
private const val MAINSITE_RATEPERMITS_PREF_DEFAULT = "1"
|
||||
|
||||
/** main site's connection limit */
|
||||
private const val MAINSITE_RATEPERMITS_PREF_TITLE = "主站连接限制"
|
||||
|
||||
/** This value affects connection request amount to main site. Lowering this value may reduce the chance to get HTTP 403 error, but loading speed will be slower too. Tachiyomi restart required. Current value: %s" */
|
||||
private const val MAINSITE_RATEPERMITS_PREF_SUMMARY = "此值影响主站的连接请求量。降低此值可以减少获得HTTP 403错误的几率,但加载速度也会变慢。需要重启软件以生效。\n默认值:$MAINSITE_RATEPERMITS_PREF_DEFAULT \n当前值:%s"
|
||||
private val MAINSITE_RATEPERMITS_PREF_ENTRIES_ARRAY = (1..10).map { i -> i.toString() }.toTypedArray()
|
||||
|
||||
private const val MAINSITE_RATEPERIOD_PREF = "mainSiteRatePeriodMillisPreference"
|
||||
private const val MAINSITE_RATEPERIOD_PREF_DEFAULT = "2500"
|
||||
|
||||
/** main site's connection limit period */
|
||||
private const val MAINSITE_RATEPERIOD_PREF_TITLE = "主站连接限制期"
|
||||
|
||||
/** This value affects the delay when hitting the connection limit to main site. Increasing this value may reduce the chance to get HTTP 403 error, but loading speed will be slower too. Tachiyomi restart required. Current value: %s" */
|
||||
private const val MAINSITE_RATEPERIOD_PREF_SUMMARY = "此值影响主站点连接限制时的延迟(毫秒)。增加这个值可能会减少出现HTTP 403错误的机会,但加载速度也会变慢。需要重启软件以生效。\n默认值:$MAINSITE_RATEPERIOD_PREF_DEFAULT\n当前值:%s"
|
||||
private val MAINSITE_RATEPERIOD_PREF_ENTRIES_ARRAY = (2000..6000 step 500).map { i -> i.toString() }.toTypedArray()
|
||||
}
|
||||
}
|