diff --git a/src/zh/zerobyw/build.gradle b/src/zh/zerobyw/build.gradle index eb4069e48..1d5a3518a 100644 --- a/src/zh/zerobyw/build.gradle +++ b/src/zh/zerobyw/build.gradle @@ -1,11 +1,12 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' ext { extName = 'Zerobyw' pkgNameSuffix = 'zh.zerobyw' extClass = '.Zerobyw' - extVersionCode = 13 + extVersionCode = 14 } apply from: "$rootDir/common.gradle" diff --git a/src/zh/zerobyw/src/eu/kanade/tachiyomi/extension/zh/zerobyw/UpdateUrl.kt b/src/zh/zerobyw/src/eu/kanade/tachiyomi/extension/zh/zerobyw/UpdateUrl.kt new file mode 100644 index 000000000..ef83f0e25 --- /dev/null +++ b/src/zh/zerobyw/src/eu/kanade/tachiyomi/extension/zh/zerobyw/UpdateUrl.kt @@ -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 = 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) + } +} diff --git a/src/zh/zerobyw/src/eu/kanade/tachiyomi/extension/zh/zerobyw/Zerobyw.kt b/src/zh/zerobyw/src/eu/kanade/tachiyomi/extension/zh/zerobyw/Zerobyw.kt index cecb8b854..5b832d6ab 100644 --- a/src/zh/zerobyw/src/eu/kanade/tachiyomi/extension/zh/zerobyw/Zerobyw.kt +++ b/src/zh/zerobyw/src/eu/kanade/tachiyomi/extension/zh/zerobyw/Zerobyw.kt @@ -2,8 +2,6 @@ package eu.kanade.tachiyomi.extension.zh.zerobyw import android.app.Application import android.content.SharedPreferences -import android.net.Uri -import androidx.preference.EditTextPreference import androidx.preference.PreferenceScreen import eu.kanade.tachiyomi.network.GET 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.SManga import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Request import okhttp3.Response import org.jsoup.nodes.Document @@ -23,76 +22,79 @@ import uy.kohesive.injekt.api.get class Zerobyw : ParsedHttpSource(), ConfigurableSource { override val name: String = "zero搬运网" override val lang: String = "zh" - override val supportsLatest: Boolean = false + override val supportsLatest: Boolean get() = false private val preferences: SharedPreferences = Injekt.get().getSharedPreferences("source_$id", 0x0000) - // Url can be found at https://cdn.jsdelivr.net/gh/zerozzz123456/1/url.json - // or just search for "zerobyw" in google - private val defaultBaseUrl = "http://www.zerobywblac.com" + override val client = network.cloudflareClient.newBuilder() + .addInterceptor(UpdateUrlInterceptor(preferences)) + .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 // 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 popularMangaSelector(): String = "div.uk-card" override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply { - title = getTitle(element.select("p.mt5 > a").text()) - setUrlWithoutDomain(element.select("p.mt5 > a").attr("abs:href")) - thumbnail_url = element.select("img").attr("src") + val link = element.selectFirst("p.mt5 > a")!! + title = getTitle(link.text()) + setUrlWithoutDomain(link.absUrl("href")) + thumbnail_url = element.selectFirst("img")!!.attr("src") } // Latest - override fun latestUpdatesRequest(page: Int) = throw Exception("Not used") - override fun latestUpdatesNextPageSelector() = throw Exception("Not used") - override fun latestUpdatesSelector() = throw Exception("Not used") - override fun latestUpdatesFromElement(element: Element) = throw Exception("Not used") + override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException() + override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException() + override fun latestUpdatesSelector() = throw UnsupportedOperationException() + override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException() // Search 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()) { - uri.appendPath("plugin.php") - .appendQueryParameter("id", "jameson_manhua") - .appendQueryParameter("a", "search") - .appendQueryParameter("c", "index") - .appendQueryParameter("keyword", query) - .appendQueryParameter("page", page.toString()) + builder + .addEncodedQueryParameter("a", "search") + .addEncodedQueryParameter("c", "index") + .addQueryParameter("keyword", query) } else { - uri.appendPath("plugin.php") - .appendQueryParameter("id", "jameson_manhua") - .appendQueryParameter("c", "index") - .appendQueryParameter("a", "ku") + builder + .addEncodedQueryParameter("c", "index") + .addEncodedQueryParameter("a", "ku") filters.forEach { 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 searchMangaSelector(): String = "a.uk-card, div.uk-card" override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply { - title = getTitle(element.select("p.mt5").text()) - setUrlWithoutDomain(element.select("a").attr("abs:href")) - thumbnail_url = element.select("img").attr("src") + title = getTitle(element.selectFirst("p.mt5")!!.text()) + setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href")) + thumbnail_url = element.selectFirst("img")!!.attr("src") } // Details override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply { - title = getTitle(document.select("li.uk-active > h3.uk-heading-line").text()) - thumbnail_url = document.select("div.uk-width-medium > img").attr("abs:src") + title = getTitle(document.selectFirst("h3.uk-heading-line")!!.text()) + thumbnail_url = document.selectFirst("div.uk-width-medium > img")!!.absUrl("src") 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(", ") description = document.select("li > div.uk-alert").html().replace("
", "") 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 chapterFromElement(element: Element): SChapter = SChapter.create().apply { - setUrlWithoutDomain(element.select("a.uk-button-default").attr("abs:href")) - name = element.select("a.uk-button-default").text() + setUrlWithoutDomain(element.selectFirst("a.uk-button-default")!!.absUrl("href")) + name = element.selectFirst("a.uk-button-default")!!.text() } override fun chapterListParse(response: Response): List { - return super.chapterListParse(response).reversed() + return super.chapterListParse(response).asReversed() } // Pages - override fun pageListParse(document: Document): List = mutableListOf().apply { + override fun pageListParse(document: Document): List { val images = document.select("div.uk-text-center > img") if (images.size == 0) { var message = document.select("div#messagetext > p") @@ -126,12 +128,12 @@ class Zerobyw : ParsedHttpSource(), ConfigurableSource { throw Exception(message.text()) } } - images.forEach { - add(Page(size, "", it.attr("src"))) + return images.mapIndexed { index, img -> + 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 @@ -199,23 +201,18 @@ class Zerobyw : ParsedHttpSource(), ConfigurableSource { fun toUri() = Pair(key, vals[state].first) } + private val commentRegex = Regex("【\\d+") + private fun getTitle(title: String): String { - val result = Regex("【\\d+").find(title) + val result = commentRegex.find(title) return if (result != null) { - title.substringBefore(result.value) + title.substring(0, result.range.first) } else { - title.substringBefore("【") + title.substringBefore('【') } } override fun setupPreferenceScreen(screen: PreferenceScreen) { - EditTextPreference(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) } + screen.addPreference(getBaseUrlPreference(screen.context)) } }