diff --git a/src/zh/bainianmanga/build.gradle b/src/zh/bainianmanga/build.gradle index c265ef757..b71d8728b 100755 --- a/src/zh/bainianmanga/build.gradle +++ b/src/zh/bainianmanga/build.gradle @@ -1,12 +1,11 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply plugin: 'kotlinx-serialization' ext { - extName = 'BainianManga' + extName = 'Bainian Manhua' pkgNameSuffix = 'zh.bainianmanga' - extClass = '.BainianManga' - extVersionCode = 6 + extClass = '.Bainian' + extVersionCode = 7 } apply from: "$rootDir/common.gradle" diff --git a/src/zh/bainianmanga/res/mipmap-hdpi/ic_launcher.png b/src/zh/bainianmanga/res/mipmap-hdpi/ic_launcher.png index b8ac02076..5810a4e10 100755 Binary files a/src/zh/bainianmanga/res/mipmap-hdpi/ic_launcher.png and b/src/zh/bainianmanga/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/zh/bainianmanga/res/mipmap-ldpi/ic_launcher.png b/src/zh/bainianmanga/res/mipmap-ldpi/ic_launcher.png index 5e51f7a05..cc25546fc 100755 Binary files a/src/zh/bainianmanga/res/mipmap-ldpi/ic_launcher.png and b/src/zh/bainianmanga/res/mipmap-ldpi/ic_launcher.png differ diff --git a/src/zh/bainianmanga/res/mipmap-mdpi/ic_launcher.png b/src/zh/bainianmanga/res/mipmap-mdpi/ic_launcher.png index 1c1c335c1..f9e635e3f 100755 Binary files a/src/zh/bainianmanga/res/mipmap-mdpi/ic_launcher.png and b/src/zh/bainianmanga/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/zh/bainianmanga/res/mipmap-xhdpi/ic_launcher.png b/src/zh/bainianmanga/res/mipmap-xhdpi/ic_launcher.png index 5fea520b2..5c5187649 100755 Binary files a/src/zh/bainianmanga/res/mipmap-xhdpi/ic_launcher.png and b/src/zh/bainianmanga/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/zh/bainianmanga/res/mipmap-xxhdpi/ic_launcher.png b/src/zh/bainianmanga/res/mipmap-xxhdpi/ic_launcher.png index 41feb5806..72d2a4566 100755 Binary files a/src/zh/bainianmanga/res/mipmap-xxhdpi/ic_launcher.png and b/src/zh/bainianmanga/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/zh/bainianmanga/res/mipmap-xxxhdpi/ic_launcher.png b/src/zh/bainianmanga/res/mipmap-xxxhdpi/ic_launcher.png index 23f8f8986..ec3958c94 100755 Binary files a/src/zh/bainianmanga/res/mipmap-xxxhdpi/ic_launcher.png and b/src/zh/bainianmanga/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/zh/bainianmanga/res/web_hi_res_512.png b/src/zh/bainianmanga/res/web_hi_res_512.png index 5b1d711db..59f627bf4 100755 Binary files a/src/zh/bainianmanga/res/web_hi_res_512.png and b/src/zh/bainianmanga/res/web_hi_res_512.png differ diff --git a/src/zh/bainianmanga/src/eu/kanade/tachiyomi/extension/zh/bainianmanga/Bainian.kt b/src/zh/bainianmanga/src/eu/kanade/tachiyomi/extension/zh/bainianmanga/Bainian.kt new file mode 100644 index 000000000..044eb4f7b --- /dev/null +++ b/src/zh/bainianmanga/src/eu/kanade/tachiyomi/extension/zh/bainianmanga/Bainian.kt @@ -0,0 +1,207 @@ +package eu.kanade.tachiyomi.extension.zh.bainianmanga + +import android.app.Application +import android.content.SharedPreferences +import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat +import eu.kanade.tachiyomi.AppInfo +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.interceptor.rateLimit +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.MangasPage +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import org.jsoup.select.Evaluator +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.text.SimpleDateFormat +import java.util.Locale + +class Bainian : ParsedHttpSource(), ConfigurableSource { + + override val name = "百年漫画" + override val lang = "zh" + override val supportsLatest = true + + private val preferences: SharedPreferences = + Injekt.get().getSharedPreferences("source_$id", 0x0000) + + private val useMirror = preferences.getBoolean(USE_MIRROR_PREF, false) + override val baseUrl = if (useMirror) "https://www.mhqj.com" else "https://www.bnman.net" + private fun String.stripMirror() = if (useMirror) "/comic" + removePrefix("/manhuadaquan") else this + private fun String.toMirror() = if (useMirror) baseUrl + "/manhuadaquan" + removePrefix("/comic") else baseUrl + this + + override val client: OkHttpClient = network.client.newBuilder().rateLimit(2).build() + + override fun popularMangaRequest(page: Int) = GET("$baseUrl/page/hot/$page.html", headers) + override fun popularMangaNextPageSelector() = ".pagination > li:last-child > a" + override fun popularMangaSelector() = "ul#list_img > li > a" + override fun popularMangaFromElement(element: Element) = SManga.create().apply { + url = element.attr("href").stripMirror() + val children = element.children() + title = children[2].text() + thumbnail_url = children[0].attr("src") + } + + override fun popularMangaParse(response: Response): MangasPage { + val document = response.asJsoup() + if (filters.isEmpty()) parseFilters(document) // parse filters here + val mangas = document.select(popularMangaSelector()).map(::popularMangaFromElement) + val hasNextPage = document.selectFirst(popularMangaNextPageSelector()) != null + return MangasPage(mangas, hasNextPage) + } + + override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/page/new/$page.html", headers) + override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() + override fun latestUpdatesSelector() = popularMangaSelector() + override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element) + + override fun latestUpdatesParse(response: Response): MangasPage { + val document = response.asJsoup() + if (filters.isEmpty()) parseFilters(document) // parse filters here + val mangas = document.select(latestUpdatesSelector()).map(::latestUpdatesFromElement) + val hasNextPage = document.selectFirst(latestUpdatesNextPageSelector()) != null + return MangasPage(mangas, hasNextPage) + } + + override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() + override fun searchMangaSelector() = popularMangaSelector() + override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element) + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + if (query.isNotEmpty()) { + return GET("$baseUrl/search/$query/$page.html", headers) + } + for (filter in filters) if (filter is CategoryFilter) { + val path = filter.getPath() + if (path.isNotEmpty()) return GET("$baseUrl$path/$page.html", headers) + } + return popularMangaRequest(page) + } + + override fun mangaDetailsRequest(manga: SManga) = GET(manga.url.toMirror(), headers) + override fun mangaDetailsParse(document: Document) = SManga.create().apply { + val details = document.selectFirst(Evaluator.Class("info")).child(0).children() + title = details[0].text() + author = details[3].child(1).text() + description = document.selectFirst(Evaluator.Class("mt10")).text() + genre = "${details[2].child(1).text()}, ${details[4].child(1).text()}" + status = when (document.selectFirst(".title01 > h2").text()) { + "连载中" -> SManga.ONGOING + "已完结" -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + thumbnail_url = document.selectFirst(".bpic > img").attr("src") + initialized = true + } + + override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga) + override fun chapterListSelector() = "ul.jslist01 > li > a:not([href^=http])" + override fun chapterFromElement(element: Element) = SChapter.create().apply { + url = element.attr("href").stripMirror() + name = element.text() + } + + override fun chapterListParse(response: Response): List { + val document = response.asJsoup() + val list = document.select(chapterListSelector()).map { chapterFromElement(it) } + if (list.isNotEmpty() && isNewDateLogic) { + // div.info > ul:eq(0) > li:eq(5) > p:eq(1) + val date = document.selectFirst(Evaluator.Class("info")).child(0).child(5).child(1).text() + list[0].date_upload = dateFormat.parse(date)?.time ?: 0 + } + return list + } + + override fun pageListRequest(chapter: SChapter) = GET(chapter.url.toMirror(), headers) + override fun pageListParse(document: Document): List { + val script = document.selectFirst("body > script").data() + val leftIndex = script.indexOf('[') + val rightIndex = script.lastIndexOf(']') + if (rightIndex - leftIndex <= 1) return emptyList() // empty string or empty list + // '["...","..."]' - check baseUrl/static/manhua/comic.js + val images = script.substring(leftIndex + 2, rightIndex - 1).replace("\\", "").split("\",\"") + return images.mapIndexed { i, it -> + val imageUrl = when { + it.startsWith("http") -> it.replace("cxcyfhpt.com", "ygeol.net") + else -> "https://img.jiequegongchang.com/$it" + } + // some hosts have invalid SSL certificates + val imageUrlInHttp = "http:" + imageUrl.substringAfter(':') + Page(i, imageUrl = imageUrlInHttp) + } + } + + override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not used.") + + private class CategoryFilter(name: String, values: Array, private val paths: List) : + Filter.Select(name, values) { + fun getPath() = paths[state] + } + + private class FilterData(private val name: String, private val values: Array, private val paths: List) { + fun toFilter() = CategoryFilter(name, values, paths) + } + + private var filters: List = emptyList() + + private fun parseFilters(document: Document) { + val categories = document.selectFirst(Evaluator.Class("select")).child(0).children().filter { it.tagName() == "dl" } + filters = categories.map { element -> + val children = element.children() + val size = children.size + val values = ArrayList(size).apply { add("全部") } + val paths = ArrayList(size).apply { add("") } + val iterator = children.iterator().apply { next() } // skip first + while (iterator.hasNext()) { + val next = iterator.next() + values.add(next.text()) + paths.add(next.child(0).attr("href").removeSuffix(".html")) + } + FilterData(children[0].text(), values.toTypedArray(), paths) + } + } + + override fun getFilterList(): FilterList { + val list: List> = + if (filters.isNotEmpty()) buildList(filters.size + 3) { + add(Filter.Header("如果使用文本搜索,将会忽略分类筛选")) + add(Filter.Header("最多使用一个分类筛选,多选时只有第一个有效")) + add(Filter.Header("提示:分类筛选非常不准")) + filters.forEach { add(it.toFilter()) } + } else buildList(2) { + add(Filter.Header("点击“重置”即可刷新分类,如果失败,")) + add(Filter.Header("请尝试重新从图源列表点击进入图源")) + } + return FilterList(list) + } + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + SwitchPreferenceCompat(screen.context).apply { + key = USE_MIRROR_PREF + title = "使用镜像网站" + summary = "使用“漫画全集”网站,重启生效" + setDefaultValue(false) + setOnPreferenceChangeListener { _, newValue -> + preferences.edit().putBoolean(USE_MIRROR_PREF, newValue as Boolean).apply() + true + } + }.let { screen.addPreference(it) } + } + + companion object { + private const val USE_MIRROR_PREF = "USE_MIRROR" + + private val isNewDateLogic = AppInfo.getVersionCode() >= 81 + private val dateFormat by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) } + } +} diff --git a/src/zh/bainianmanga/src/eu/kanade/tachiyomi/extension/zh/bainianmanga/BainianManga.kt b/src/zh/bainianmanga/src/eu/kanade/tachiyomi/extension/zh/bainianmanga/BainianManga.kt deleted file mode 100755 index 18428f804..000000000 --- a/src/zh/bainianmanga/src/eu/kanade/tachiyomi/extension/zh/bainianmanga/BainianManga.kt +++ /dev/null @@ -1,124 +0,0 @@ -package eu.kanade.tachiyomi.extension.zh.bainianmanga - -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.source.model.FilterList -import eu.kanade.tachiyomi.source.model.Page -import eu.kanade.tachiyomi.source.model.SChapter -import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.source.online.ParsedHttpSource -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json -import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.Interceptor -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.Response -import okhttp3.ResponseBody.Companion.toResponseBody -import org.jsoup.nodes.Document -import org.jsoup.nodes.Element -import uy.kohesive.injekt.injectLazy - -class BainianManga : ParsedHttpSource() { - - override val name = "百年漫画" - override val baseUrl = "https://www.bnman.net" - override val lang = "zh" - override val supportsLatest = true - - override val client: OkHttpClient - get() = network.client.newBuilder() - .addNetworkInterceptor(rewriteOctetStream) - .build() - - // Based on Pufei ext - private val rewriteOctetStream: Interceptor = Interceptor { chain -> - val originalResponse: Response = chain.proceed(chain.request()) - if (originalResponse.headers("Content-Type").contains("application/octet-stream") && originalResponse.request.url.toString().contains(".jpg")) { - val orgBody = originalResponse.body!!.bytes() - val newBody = orgBody.toResponseBody("image/jpeg".toMediaTypeOrNull()) - originalResponse.newBuilder() - .body(newBody) - .build() - } else originalResponse - } - - private val json: Json by injectLazy() - - override fun popularMangaRequest(page: Int) = GET("$baseUrl/page/hot/$page.html", headers) - override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/page/new/$page.html", headers) - override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - val url = "$baseUrl/search.html?".toHttpUrl().newBuilder() - .addQueryParameter("keyword", query) - .addQueryParameter("page", page.toString()) - return GET(url.toString(), headers) - } - - override fun mangaDetailsRequest(manga: SManga) = GET(baseUrl + manga.url, headers) - override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga) - override fun pageListRequest(chapter: SChapter) = GET(baseUrl + chapter.url, headers) - - override fun popularMangaSelector() = "ul#list_img > li" - override fun latestUpdatesSelector() = popularMangaSelector() - override fun searchMangaSelector() = popularMangaSelector() - // Ignore first item, which is link to another comic site - override fun chapterListSelector() = "ul.jslist01 > li:not(:first-child)" - - override fun searchMangaNextPageSelector() = ".pagination > li:last-child > a" - override fun popularMangaNextPageSelector() = searchMangaNextPageSelector() - override fun latestUpdatesNextPageSelector() = searchMangaNextPageSelector() - - override fun headersBuilder() = super.headersBuilder() - .add("Referer", baseUrl) - - override fun popularMangaFromElement(element: Element) = mangaFromElement(element) - override fun latestUpdatesFromElement(element: Element) = mangaFromElement(element) - override fun searchMangaFromElement(element: Element) = mangaFromElement(element) - private fun mangaFromElement(element: Element): SManga { - val manga = SManga.create() - element.select("a").first().let { - manga.setUrlWithoutDomain(it.attr("href")) - manga.title = it.select("p").first().text() - } - manga.thumbnail_url = element.select("img").attr("src") - return manga - } - - override fun chapterFromElement(element: Element): SChapter { - val urlElement = element.select("a") - - val chapter = SChapter.create() - chapter.setUrlWithoutDomain(urlElement.attr("href")) - chapter.name = urlElement.text() - return chapter - } - - override fun mangaDetailsParse(document: Document): SManga { - val infoElement = document.select(".info") - - val manga = SManga.create() - manga.description = document.select(".mt10").first().text() - manga.author = infoElement.select("ul > li > span:contains(漫画作者) + p").first().text() - return manga - } - - override fun pageListParse(response: Response): List { - return json.decodeFromString>( - response.body!!.string() - .substringAfter("var z_img='") - .substringBefore("';") - ).mapIndexed { i, imageUrl -> - when { - imageUrl.startsWith("http") -> Page(i, "", imageUrl) - else -> Page(i, "", "https://img.hngxgt.net/$imageUrl") - } - } - } - - override fun pageListParse(document: Document): List = - throw UnsupportedOperationException("Not used.") - - override fun imageUrlParse(document: Document) = "" - - override fun getFilterList() = FilterList() -}