Zerobyw: auto update URL and clean up (#15884)
* Zerobyw: auto update URL and clean up * Use System.getenv() to check CI * Use HashMap
This commit is contained in:
parent
4290347286
commit
f43811902b
|
@ -1,11 +1,12 @@
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlinx-serialization'
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
extName = 'Zerobyw'
|
extName = 'Zerobyw'
|
||||||
pkgNameSuffix = 'zh.zerobyw'
|
pkgNameSuffix = 'zh.zerobyw'
|
||||||
extClass = '.Zerobyw'
|
extClass = '.Zerobyw'
|
||||||
extVersionCode = 13
|
extVersionCode = 14
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.zh.zerobyw
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.preference.EditTextPreference
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Response
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
private const val DEFAULT_BASE_URL = "http://www.zerobyw4090.com"
|
||||||
|
|
||||||
|
private const val BASE_URL_PREF = "ZEROBYW_BASEURL"
|
||||||
|
private const val DEFAULT_BASE_URL_PREF = "defaultBaseUrl"
|
||||||
|
private const val JSON_URL = "https://cdn.jsdelivr.net/gh/zerozzz123456/1/url.json"
|
||||||
|
|
||||||
|
var SharedPreferences.baseUrl: String
|
||||||
|
get() = getString(BASE_URL_PREF, DEFAULT_BASE_URL)!!
|
||||||
|
set(value) = edit().putString(BASE_URL_PREF, value).apply()
|
||||||
|
|
||||||
|
fun SharedPreferences.clearOldBaseUrl(): SharedPreferences {
|
||||||
|
if (getString(DEFAULT_BASE_URL_PREF, "")!! == DEFAULT_BASE_URL) return this
|
||||||
|
edit()
|
||||||
|
.remove(BASE_URL_PREF)
|
||||||
|
.putString(DEFAULT_BASE_URL_PREF, DEFAULT_BASE_URL)
|
||||||
|
.apply()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBaseUrlPreference(context: Context) = EditTextPreference(context).apply {
|
||||||
|
key = BASE_URL_PREF
|
||||||
|
title = "网址"
|
||||||
|
summary = "正常情况下会自动更新。" +
|
||||||
|
"如果出现错误,请在 GitHub 上报告,并且可以在 $JSON_URL 找到最新网址手动填写。" +
|
||||||
|
"填写时按照 $DEFAULT_BASE_URL 格式。"
|
||||||
|
setDefaultValue(DEFAULT_BASE_URL)
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
try {
|
||||||
|
checkBaseUrl(newValue as String)
|
||||||
|
true
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
Toast.makeText(context, "网址格式错误", Toast.LENGTH_LONG).show()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ciGetUrl(client: OkHttpClient): String {
|
||||||
|
println("[Zerobyw] CI detected, getting latest URL...")
|
||||||
|
return try {
|
||||||
|
val response = client.newCall(GET(JSON_URL)).execute()
|
||||||
|
parseJson(response).also { println("[Zerobyw] Latest URL is $it") }
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
println("[Zerobyw] Failed to fetch latest URL")
|
||||||
|
e.printStackTrace()
|
||||||
|
DEFAULT_BASE_URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseJson(response: Response): String {
|
||||||
|
val string = response.body.string()
|
||||||
|
val json: HashMap<String, String> = Json.decodeFromString(string)
|
||||||
|
val newUrl = json["url"]!!.trim()
|
||||||
|
checkBaseUrl(newUrl)
|
||||||
|
return newUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkBaseUrl(url: String) {
|
||||||
|
require(url == url.trim() && !url.endsWith('/'))
|
||||||
|
val pathSegments = url.toHttpUrl().pathSegments
|
||||||
|
require(pathSegments.size == 1 && pathSegments[0].isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
class UpdateUrlInterceptor(
|
||||||
|
private val preferences: SharedPreferences,
|
||||||
|
) : Interceptor {
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val request = chain.request()
|
||||||
|
val url = request.url
|
||||||
|
val baseUrl = preferences.baseUrl
|
||||||
|
if (!url.toString().startsWith(baseUrl)) return chain.proceed(request)
|
||||||
|
|
||||||
|
val failedResult = try {
|
||||||
|
val response = chain.proceed(request)
|
||||||
|
if (response.code < 500) return response
|
||||||
|
Result.success(response)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
if (chain.call().isCanceled()) throw e
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
val newUrl = try {
|
||||||
|
val response = chain.proceed(GET(JSON_URL))
|
||||||
|
val newUrl = parseJson(response)
|
||||||
|
require(newUrl != baseUrl)
|
||||||
|
newUrl
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
return failedResult.getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences.baseUrl = newUrl
|
||||||
|
val (scheme, host) = newUrl.split("://")
|
||||||
|
val retryUrl = url.newBuilder().scheme(scheme).host(host).build()
|
||||||
|
val retryRequest = request.newBuilder().url(retryUrl).build()
|
||||||
|
return chain.proceed(retryRequest)
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,8 +2,6 @@ package eu.kanade.tachiyomi.extension.zh.zerobyw
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.net.Uri
|
|
||||||
import androidx.preference.EditTextPreference
|
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
|
@ -13,6 +11,7 @@ import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
|
@ -23,76 +22,79 @@ import uy.kohesive.injekt.api.get
|
||||||
class Zerobyw : ParsedHttpSource(), ConfigurableSource {
|
class Zerobyw : ParsedHttpSource(), ConfigurableSource {
|
||||||
override val name: String = "zero搬运网"
|
override val name: String = "zero搬运网"
|
||||||
override val lang: String = "zh"
|
override val lang: String = "zh"
|
||||||
override val supportsLatest: Boolean = false
|
override val supportsLatest: Boolean get() = false
|
||||||
private val preferences: SharedPreferences =
|
private val preferences: SharedPreferences =
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
|
||||||
// Url can be found at https://cdn.jsdelivr.net/gh/zerozzz123456/1/url.json
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
// or just search for "zerobyw" in google
|
.addInterceptor(UpdateUrlInterceptor(preferences))
|
||||||
private val defaultBaseUrl = "http://www.zerobywblac.com"
|
.build()
|
||||||
|
|
||||||
override val baseUrl = preferences.getString("ZEROBYW_BASEURL", defaultBaseUrl)!!
|
override val baseUrl get() = when {
|
||||||
|
isCi -> ciGetUrl(client)
|
||||||
|
else -> preferences.baseUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
private val isCi = System.getenv("CI") == "true"
|
||||||
|
|
||||||
// Popular
|
// Popular
|
||||||
// Website does not provide popular manga, this is actually latest manga
|
// Website does not provide popular manga, this is actually latest manga
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/plugin.php?id=jameson_manhua&c=index&a=ku&&page=$page", headers)
|
override fun popularMangaRequest(page: Int) = GET("$baseUrl/plugin.php?id=jameson_manhua&c=index&a=ku&page=$page", headers)
|
||||||
override fun popularMangaNextPageSelector(): String = "div.pg > a.nxt"
|
override fun popularMangaNextPageSelector(): String = "div.pg > a.nxt"
|
||||||
override fun popularMangaSelector(): String = "div.uk-card"
|
override fun popularMangaSelector(): String = "div.uk-card"
|
||||||
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||||
title = getTitle(element.select("p.mt5 > a").text())
|
val link = element.selectFirst("p.mt5 > a")!!
|
||||||
setUrlWithoutDomain(element.select("p.mt5 > a").attr("abs:href"))
|
title = getTitle(link.text())
|
||||||
thumbnail_url = element.select("img").attr("src")
|
setUrlWithoutDomain(link.absUrl("href"))
|
||||||
|
thumbnail_url = element.selectFirst("img")!!.attr("src")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Latest
|
// Latest
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int) = throw Exception("Not used")
|
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
||||||
override fun latestUpdatesNextPageSelector() = throw Exception("Not used")
|
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
|
||||||
override fun latestUpdatesSelector() = throw Exception("Not used")
|
override fun latestUpdatesSelector() = throw UnsupportedOperationException()
|
||||||
override fun latestUpdatesFromElement(element: Element) = throw Exception("Not used")
|
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val uri = Uri.parse(baseUrl).buildUpon()
|
val builder = "$baseUrl/plugin.php".toHttpUrl().newBuilder()
|
||||||
|
.addEncodedQueryParameter("id", "jameson_manhua")
|
||||||
if (query.isNotBlank()) {
|
if (query.isNotBlank()) {
|
||||||
uri.appendPath("plugin.php")
|
builder
|
||||||
.appendQueryParameter("id", "jameson_manhua")
|
.addEncodedQueryParameter("a", "search")
|
||||||
.appendQueryParameter("a", "search")
|
.addEncodedQueryParameter("c", "index")
|
||||||
.appendQueryParameter("c", "index")
|
.addQueryParameter("keyword", query)
|
||||||
.appendQueryParameter("keyword", query)
|
|
||||||
.appendQueryParameter("page", page.toString())
|
|
||||||
} else {
|
} else {
|
||||||
uri.appendPath("plugin.php")
|
builder
|
||||||
.appendQueryParameter("id", "jameson_manhua")
|
.addEncodedQueryParameter("c", "index")
|
||||||
.appendQueryParameter("c", "index")
|
.addEncodedQueryParameter("a", "ku")
|
||||||
.appendQueryParameter("a", "ku")
|
|
||||||
filters.forEach {
|
filters.forEach {
|
||||||
if (it is UriSelectFilterPath && it.toUri().second.isNotEmpty()) {
|
if (it is UriSelectFilterPath && it.toUri().second.isNotEmpty()) {
|
||||||
uri.appendQueryParameter(it.toUri().first, it.toUri().second)
|
builder.addQueryParameter(it.toUri().first, it.toUri().second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
uri.appendQueryParameter("page", page.toString())
|
|
||||||
}
|
}
|
||||||
return GET(uri.toString(), headers)
|
builder.addEncodedQueryParameter("page", page.toString())
|
||||||
|
return GET(builder.build(), headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector(): String = "div.pg > a.nxt"
|
override fun searchMangaNextPageSelector(): String = "div.pg > a.nxt"
|
||||||
override fun searchMangaSelector(): String = "a.uk-card, div.uk-card"
|
override fun searchMangaSelector(): String = "a.uk-card, div.uk-card"
|
||||||
override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
|
override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||||
title = getTitle(element.select("p.mt5").text())
|
title = getTitle(element.selectFirst("p.mt5")!!.text())
|
||||||
setUrlWithoutDomain(element.select("a").attr("abs:href"))
|
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
||||||
thumbnail_url = element.select("img").attr("src")
|
thumbnail_url = element.selectFirst("img")!!.attr("src")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Details
|
// Details
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||||
title = getTitle(document.select("li.uk-active > h3.uk-heading-line").text())
|
title = getTitle(document.selectFirst("h3.uk-heading-line")!!.text())
|
||||||
thumbnail_url = document.select("div.uk-width-medium > img").attr("abs:src")
|
thumbnail_url = document.selectFirst("div.uk-width-medium > img")!!.absUrl("src")
|
||||||
author = document.selectFirst("div.cl > a.uk-label")!!.text().substring(3)
|
author = document.selectFirst("div.cl > a.uk-label")!!.text().substring(3)
|
||||||
artist = author
|
|
||||||
genre = document.select("div.cl > a.uk-label, div.cl > span.uk-label").eachText().joinToString(", ")
|
genre = document.select("div.cl > a.uk-label, div.cl > span.uk-label").eachText().joinToString(", ")
|
||||||
description = document.select("li > div.uk-alert").html().replace("<br>", "")
|
description = document.select("li > div.uk-alert").html().replace("<br>", "")
|
||||||
status = when (document.select("div.cl > span.uk-label").last()!!.text()) {
|
status = when (document.select("div.cl > span.uk-label").last()!!.text()) {
|
||||||
|
@ -106,16 +108,16 @@ class Zerobyw : ParsedHttpSource(), ConfigurableSource {
|
||||||
|
|
||||||
override fun chapterListSelector(): String = "div.uk-grid-collapse > div.muludiv"
|
override fun chapterListSelector(): String = "div.uk-grid-collapse > div.muludiv"
|
||||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||||
setUrlWithoutDomain(element.select("a.uk-button-default").attr("abs:href"))
|
setUrlWithoutDomain(element.selectFirst("a.uk-button-default")!!.absUrl("href"))
|
||||||
name = element.select("a.uk-button-default").text()
|
name = element.selectFirst("a.uk-button-default")!!.text()
|
||||||
}
|
}
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
return super.chapterListParse(response).reversed()
|
return super.chapterListParse(response).asReversed()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pages
|
// Pages
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> = mutableListOf<Page>().apply {
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
val images = document.select("div.uk-text-center > img")
|
val images = document.select("div.uk-text-center > img")
|
||||||
if (images.size == 0) {
|
if (images.size == 0) {
|
||||||
var message = document.select("div#messagetext > p")
|
var message = document.select("div#messagetext > p")
|
||||||
|
@ -126,12 +128,12 @@ class Zerobyw : ParsedHttpSource(), ConfigurableSource {
|
||||||
throw Exception(message.text())
|
throw Exception(message.text())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
images.forEach {
|
return images.mapIndexed { index, img ->
|
||||||
add(Page(size, "", it.attr("src")))
|
Page(index, imageUrl = img.attr("src"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document): String = throw Exception("Not Used")
|
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
|
|
||||||
|
@ -199,23 +201,18 @@ class Zerobyw : ParsedHttpSource(), ConfigurableSource {
|
||||||
fun toUri() = Pair(key, vals[state].first)
|
fun toUri() = Pair(key, vals[state].first)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val commentRegex = Regex("【\\d+")
|
||||||
|
|
||||||
private fun getTitle(title: String): String {
|
private fun getTitle(title: String): String {
|
||||||
val result = Regex("【\\d+").find(title)
|
val result = commentRegex.find(title)
|
||||||
return if (result != null) {
|
return if (result != null) {
|
||||||
title.substringBefore(result.value)
|
title.substring(0, result.range.first)
|
||||||
} else {
|
} else {
|
||||||
title.substringBefore("【")
|
title.substringBefore('【')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
EditTextPreference(screen.context)
|
screen.addPreference(getBaseUrlPreference(screen.context))
|
||||||
.apply {
|
|
||||||
key = "ZEROBYW_BASEURL"
|
|
||||||
title = "zerobyw网址"
|
|
||||||
setDefaultValue(defaultBaseUrl)
|
|
||||||
summary = "可在 https://cdn.jsdelivr.net/gh/zerozzz123456/1/url.json 中找到网址,或者通过google搜索\"zerobyw\"得到"
|
|
||||||
}
|
|
||||||
.let { screen.addPreference(it) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue