Add bilimanga source (#9552)
* init * optimize * adjust * little modify * modify extName * modify prompt * apply commit Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com> * apply commit Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com> * add request rate limit * apply commit * apply commit --------- Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
This commit is contained in:
parent
2142cb32c2
commit
a5a62a2d4e
8
src/zh/bilimanga/build.gradle
Normal file
8
src/zh/bilimanga/build.gradle
Normal file
@ -0,0 +1,8 @@
|
||||
ext {
|
||||
extName = 'BiliManga'
|
||||
extClass = '.BiliManga'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/zh/bilimanga/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/zh/bilimanga/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.1 KiB |
BIN
src/zh/bilimanga/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/zh/bilimanga/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
BIN
src/zh/bilimanga/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/zh/bilimanga/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.0 KiB |
BIN
src/zh/bilimanga/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/zh/bilimanga/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
src/zh/bilimanga/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/zh/bilimanga/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
@ -0,0 +1,174 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.bilimanga
|
||||
|
||||
import androidx.preference.PreferenceScreen
|
||||
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.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import keiyoushi.utils.tryParse
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Elements
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class BiliManga : HttpSource(), ConfigurableSource {
|
||||
|
||||
override val baseUrl = "https://www.bilimanga.net"
|
||||
|
||||
override val lang = "zh"
|
||||
|
||||
override val name = "Bilimanga.net"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val preferences by getPreferencesLazy()
|
||||
|
||||
override val client = super.client.newBuilder()
|
||||
.rateLimit(10, 10).addNetworkInterceptor(MangaInterceptor()).build()
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
.add("Accept-Language", "zh")
|
||||
.add("Accept", "*/*")
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
preferencesInternal(screen.context).forEach(screen::addPreference)
|
||||
}
|
||||
|
||||
// Customize
|
||||
|
||||
private val SManga.id get() = MANGA_ID_REGEX.find(url)!!.groups[1]!!.value
|
||||
private fun String.toHalfWidthDigits(): String {
|
||||
return this.map { if (it in '0'..'9') it - 65248 else it }.joinToString("")
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PAGE_SIZE = 50
|
||||
val META_REGEX = Regex("連載|完結|收藏|推薦|热度")
|
||||
val DATE_REGEX = Regex("\\d{4}-\\d{1,2}-\\d{1,2}")
|
||||
val MANGA_ID_REGEX = Regex("/detail/(\\d+)\\.html")
|
||||
val DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd", Locale.CHINESE)
|
||||
}
|
||||
|
||||
private fun getChapterUrlByContext(i: Int, els: Elements) = when (i) {
|
||||
0 -> "${els[1].attr("href")}#prev"
|
||||
else -> "${els[i - 1].attr("href")}#next"
|
||||
}
|
||||
|
||||
// Popular Page
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val suffix = preferences.getString(PREF_POPULAR_MANGA_DISPLAY, "/top/weekvisit/%d.html")!!
|
||||
return GET(baseUrl + String.format(suffix, page), headers)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response) = response.asJsoup().let {
|
||||
val mangas = it.select(".book-layout").map {
|
||||
SManga.create().apply {
|
||||
setUrlWithoutDomain(it.absUrl("href"))
|
||||
val img = it.selectFirst("img")!!
|
||||
thumbnail_url = img.absUrl("data-src")
|
||||
title = img.attr("alt")
|
||||
}
|
||||
}
|
||||
MangasPage(mangas, mangas.size >= PAGE_SIZE)
|
||||
}
|
||||
|
||||
// Latest Page
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
GET("$baseUrl/top/lastupdate/$page.html", headers)
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
||||
|
||||
// Search Page
|
||||
|
||||
override fun getFilterList() = buildFilterList()
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = baseUrl.toHttpUrl().newBuilder()
|
||||
if (query.isNotBlank()) {
|
||||
url.addPathSegment("search").addPathSegment("${query}_$page.html")
|
||||
} else {
|
||||
url.addPathSegment("top").addPathSegment(filters[1].toString())
|
||||
.addPathSegment("$page.html")
|
||||
}
|
||||
return GET(url.build(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
if (response.request.url.pathSegments.contains("detail")) {
|
||||
return MangasPage(listOf(mangaDetailsParse(response)), false)
|
||||
}
|
||||
return popularMangaParse(response)
|
||||
}
|
||||
|
||||
// Manga Detail Page
|
||||
|
||||
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
|
||||
val doc = response.asJsoup()
|
||||
val meta = doc.selectFirst(".book-meta")!!.text().split("|")
|
||||
val extra = meta.filterNot(META_REGEX::containsMatchIn)
|
||||
val backupname = doc.selectFirst(".backupname")?.let { "漫畫別名:${it.text()}\n\n" }
|
||||
url = doc.location()
|
||||
title = doc.selectFirst(".book-title")!!.text()
|
||||
thumbnail_url = doc.selectFirst(".book-cover")!!.attr("src")
|
||||
description = backupname + doc.selectFirst("#bookSummary")?.text()
|
||||
artist = doc.selectFirst(".authorname")?.text()
|
||||
author = doc.selectFirst(".illname")?.text() ?: artist
|
||||
status = when (meta.firstOrNull()) {
|
||||
"連載" -> SManga.ONGOING
|
||||
"完結" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
genre = (doc.select(".tag-small").map(Element::text) + extra).joinToString()
|
||||
initialized = true
|
||||
}
|
||||
|
||||
// Catalog Page
|
||||
|
||||
override fun chapterListRequest(manga: SManga) =
|
||||
GET("$baseUrl/read/${manga.id}/catalog", headers)
|
||||
|
||||
override fun chapterListParse(response: Response) = response.asJsoup().let {
|
||||
val info = it.selectFirst(".chapter-sub-title")!!.text()
|
||||
val date = DATE_FORMAT.tryParse(DATE_REGEX.find(info)?.value)
|
||||
val elements = it.select(".chapter-li-a")
|
||||
elements.mapIndexed { i, e ->
|
||||
val url = e.absUrl("href").takeUnless("javascript:cid(1)"::equals)
|
||||
SChapter.create().apply {
|
||||
name = e.text().toHalfWidthDigits()
|
||||
date_upload = date
|
||||
setUrlWithoutDomain(url ?: getChapterUrlByContext(i, elements))
|
||||
}
|
||||
}.reversed()
|
||||
}
|
||||
|
||||
// Manga View Page
|
||||
|
||||
override fun pageListParse(response: Response) = response.asJsoup().let {
|
||||
val images = it.select(".imagecontent")
|
||||
check(images.size > 0) {
|
||||
it.selectFirst("#acontentz")?.let { e ->
|
||||
if ("電腦端" in e.text()) "章節不支持桌面電腦端瀏覽器顯示" else "漫畫可能已下架或需要登錄查看"
|
||||
} ?: "章节鏈接错误"
|
||||
}
|
||||
images.mapIndexed { i, image ->
|
||||
Page(i, imageUrl = image.attr("data-src"))
|
||||
}
|
||||
}
|
||||
|
||||
// Image
|
||||
|
||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.bilimanga
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
|
||||
fun buildFilterList() = FilterList(
|
||||
Filter.Header("篩選條件(搜尋時無效)"),
|
||||
RankFilter(),
|
||||
)
|
||||
|
||||
class RankFilter : Filter.Select<String>(
|
||||
"排行榜",
|
||||
arrayOf(
|
||||
"月點擊榜",
|
||||
"周點擊榜",
|
||||
"月推薦榜",
|
||||
"周推薦榜",
|
||||
"月鮮花榜",
|
||||
"周鮮花榜",
|
||||
"月雞蛋榜",
|
||||
"周雞蛋榜",
|
||||
"最新入庫",
|
||||
"收藏榜",
|
||||
"新書榜",
|
||||
),
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return arrayOf(
|
||||
"monthvisit",
|
||||
"weekvisit",
|
||||
"monthvote",
|
||||
"weekvote",
|
||||
"monthflower",
|
||||
"weekflower",
|
||||
"monthegg",
|
||||
"weekegg",
|
||||
"postdate",
|
||||
"goodnum",
|
||||
"newhot",
|
||||
)[state]
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.bilimanga
|
||||
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import okio.GzipSource
|
||||
import okio.buffer
|
||||
|
||||
class MangaInterceptor : Interceptor {
|
||||
|
||||
companion object {
|
||||
val PREV_URL_REGEX = Regex("url_previous:'(.*?)'")
|
||||
val NEXT_URL_REGEX = Regex("url_next:'(.*?)'")
|
||||
val CHAPTER_ID_REGEX = Regex("/read/(\\d+)/(\\d+)\\.html")
|
||||
}
|
||||
|
||||
private fun regexOf(str: String?) = when (str) {
|
||||
"prev" -> PREV_URL_REGEX
|
||||
"next" -> NEXT_URL_REGEX
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun predictUrlByContext(url: HttpUrl) = when (url.fragment) {
|
||||
"prev" -> {
|
||||
val groups = CHAPTER_ID_REGEX.find(url.toString())?.groups
|
||||
"/read/${groups?.get(1)?.value}/${groups?.get(2)?.value?.toInt()?.plus(1)}.html"
|
||||
}
|
||||
"next" -> {
|
||||
val groups = CHAPTER_ID_REGEX.find(url.toString())?.groups
|
||||
"/read/${groups?.get(1)?.value}/${groups?.get(2)?.value?.toInt()?.minus(1)}.html"
|
||||
}
|
||||
else -> "/read/0/0.html"
|
||||
} + "?predict"
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val origin = chain.request()
|
||||
regexOf(origin.url.fragment)?.let {
|
||||
val response = chain.proceed(origin)
|
||||
val html = GzipSource(response.body.source()).buffer().readUtf8()
|
||||
val url = it.find(html)?.groups?.get(1)?.value?.plus("?match")
|
||||
return response.newBuilder().code(302)
|
||||
.header("Location", url ?: predictUrlByContext(origin.url)).build()
|
||||
}
|
||||
return chain.proceed(origin.newBuilder().addHeader("Cookie", "night=1").build())
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.bilimanga
|
||||
|
||||
import android.content.Context
|
||||
import androidx.preference.ListPreference
|
||||
|
||||
const val PREF_POPULAR_MANGA_DISPLAY = "POPULAR_MANGA_DISPLAY"
|
||||
|
||||
fun preferencesInternal(context: Context) = arrayOf(
|
||||
ListPreference(context).apply {
|
||||
key = PREF_POPULAR_MANGA_DISPLAY
|
||||
title = "熱門漫畫顯示内容"
|
||||
summary = "%s"
|
||||
entries = arrayOf(
|
||||
"月点击榜",
|
||||
"周点击榜",
|
||||
"月推荐榜",
|
||||
"周推荐榜",
|
||||
"月鲜花榜",
|
||||
"周鲜花榜",
|
||||
"月鸡蛋榜",
|
||||
"周鸡蛋榜",
|
||||
"最新入库",
|
||||
"收藏榜",
|
||||
"新书榜",
|
||||
)
|
||||
entryValues = arrayOf(
|
||||
"/top/monthvisit/%d.html",
|
||||
"/top/weekvisit/%d.html",
|
||||
"/top/monthvote/%d.html",
|
||||
"/top/weekvote/%d.html",
|
||||
"/top/monthflower/%d.html",
|
||||
"/top/weekflower/%d.html",
|
||||
"/top/monthegg/%d.html",
|
||||
"/top/weekegg/%d.html",
|
||||
"/top/postdate/%d.html",
|
||||
"/top/goodnum/%d.html",
|
||||
"/top/newhot/%d.html",
|
||||
)
|
||||
setDefaultValue("/top/weekvisit/%d.html")
|
||||
},
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user