BiliManga: add more filter options (#10166)

* add more filter options

* fix pagination
This commit is contained in:
Hualiang 2025-08-19 00:56:43 +08:00 committed by Draff
parent 4587ac2c1d
commit 88407d64af
Signed by: Draff
GPG Key ID: E8A89F3211677653
3 changed files with 122 additions and 40 deletions

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'BiliManga' extName = 'BiliManga'
extClass = '.BiliManga' extClass = '.BiliManga'
extVersionCode = 3 extVersionCode = 4
isNsfw = true isNsfw = true
} }

View File

@ -16,6 +16,7 @@ import keiyoushi.utils.tryParse
import okhttp3.HttpUrl.Companion.toHttpUrl 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.Element import org.jsoup.nodes.Element
import org.jsoup.select.Elements import org.jsoup.select.Elements
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -53,13 +54,29 @@ class BiliManga : HttpSource(), ConfigurableSource {
} }
companion object { companion object {
const val PAGE_SIZE = 50
val META_REGEX = Regex("連載|完結|收藏|推薦|热度") val META_REGEX = Regex("連載|完結|收藏|推薦|热度")
val DATE_REGEX = Regex("\\d{4}-\\d{1,2}-\\d{1,2}") val DATE_REGEX = Regex("\\d{4}-\\d{1,2}-\\d{1,2}")
val PAGE_REGEX = Regex("第(\\d+)/(\\d+)页")
val MANGA_ID_REGEX = Regex("/detail/(\\d+)\\.html") val MANGA_ID_REGEX = Regex("/detail/(\\d+)\\.html")
val DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd", Locale.CHINESE) val DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd", Locale.CHINESE)
} }
private fun hasNextPage(doc: Document, size: Int): Boolean {
val url = doc.location()
return when {
url.contains("filter") -> {
val total = doc.selectFirst("#pagelink > .last")!!.text().toInt()
val cur = doc.selectFirst("#pagelink > strong")!!.text().toInt()
cur < total
}
url.contains("search") -> {
val find = PAGE_REGEX.find(doc.selectFirst("#pagelink > span")!!.text())!!
find.groups[1]!!.value.toInt() < find.groups[1]!!.value.toInt()
}
else -> size == 50
}
}
private fun getChapterUrlByContext(i: Int, els: Elements) = when (i) { private fun getChapterUrlByContext(i: Int, els: Elements) = when (i) {
0 -> "${els[1].attr("href")}#prev" 0 -> "${els[1].attr("href")}#prev"
else -> "${els[i - 1].attr("href")}#next" else -> "${els[i - 1].attr("href")}#next"
@ -72,8 +89,8 @@ class BiliManga : HttpSource(), ConfigurableSource {
return GET(baseUrl + String.format(suffix, page), headers) return GET(baseUrl + String.format(suffix, page), headers)
} }
override fun popularMangaParse(response: Response) = response.asJsoup().let { override fun popularMangaParse(response: Response) = response.asJsoup().let { doc ->
val mangas = it.select(".book-layout").map { val mangas = doc.select(".book-layout").map {
SManga.create().apply { SManga.create().apply {
setUrlWithoutDomain(it.absUrl("href")) setUrlWithoutDomain(it.absUrl("href"))
val img = it.selectFirst("img")!! val img = it.selectFirst("img")!!
@ -81,12 +98,13 @@ class BiliManga : HttpSource(), ConfigurableSource {
title = img.attr("alt") title = img.attr("alt")
} }
} }
MangasPage(mangas, mangas.size >= PAGE_SIZE) MangasPage(mangas, hasNextPage(doc, mangas.size))
} }
// Latest Page // Latest Page
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/top/lastupdate/$page.html", headers) override fun latestUpdatesRequest(page: Int) =
GET("$baseUrl/top/lastupdate/$page.html", headers)
override fun latestUpdatesParse(response: Response) = popularMangaParse(response) override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
@ -94,13 +112,14 @@ class BiliManga : HttpSource(), ConfigurableSource {
override fun getFilterList() = buildFilterList() override fun getFilterList() = buildFilterList()
// https://www.bilimanga.net/filter/lastupdate_1_0_0_0_0_0_0_1_0.html
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder() val url = baseUrl.toHttpUrl().newBuilder()
if (query.isNotBlank()) { if (query.isNotBlank()) {
url.addPathSegment("search").addPathSegment("${query}_$page.html") url.addPathSegment("search").addPathSegment("${query}_$page.html")
} else { } else {
url.addPathSegment("top").addPathSegment(filters[1].toString()) url.addPathSegment("filter")
.addPathSegment("$page.html") .addPathSegment("${filters[4]}_${filters[1]}_${filters[7]}_${filters[5]}_${filters[3]}_${filters[2]}_${filters[8]}_${filters[6]}_${page}_0.html")
} }
return GET(url.build(), headers) return GET(url.build(), headers)
} }
@ -118,13 +137,11 @@ class BiliManga : HttpSource(), ConfigurableSource {
val doc = response.asJsoup() val doc = response.asJsoup()
val meta = doc.selectFirst(".book-meta")!!.text().split("|") val meta = doc.selectFirst(".book-meta")!!.text().split("|")
val extra = meta.filterNot(META_REGEX::containsMatchIn) val extra = meta.filterNot(META_REGEX::containsMatchIn)
val backupname = doc.selectFirst(".backupname")?.let { val backupname = doc.selectFirst(".backupname")?.let { "【別名:${it.text()}\n\n" } ?: ""
"\n\n漫畫別名:\n${it.text().split("、").joinToString("\n• ")}"
}
setUrlWithoutDomain(doc.location()) setUrlWithoutDomain(doc.location())
title = doc.selectFirst(".book-title")!!.text() title = doc.selectFirst(".book-title")!!.text()
thumbnail_url = doc.selectFirst(".book-cover")!!.attr("src") thumbnail_url = doc.selectFirst(".book-cover")!!.attr("src")
description = doc.selectFirst("#bookSummary")?.text() + backupname description = backupname + doc.selectFirst("#bookSummary > content")?.wholeText()?.trim()
artist = doc.selectFirst(".authorname")?.text() artist = doc.selectFirst(".authorname")?.text()
author = doc.selectFirst(".illname")?.text() ?: artist author = doc.selectFirst(".illname")?.text() ?: artist
status = when (meta.firstOrNull()) { status = when (meta.firstOrNull()) {
@ -138,7 +155,8 @@ class BiliManga : HttpSource(), ConfigurableSource {
// Catalog Page // Catalog Page
override fun chapterListRequest(manga: SManga) = GET("$baseUrl/read/${manga.id}/catalog", headers) override fun chapterListRequest(manga: SManga) =
GET("$baseUrl/read/${manga.id}/catalog", headers)
override fun chapterListParse(response: Response) = response.asJsoup().let { override fun chapterListParse(response: Response) = response.asJsoup().let {
val info = it.selectFirst(".chapter-sub-title")!!.text() val info = it.selectFirst(".chapter-sub-title")!!.text()
@ -162,7 +180,7 @@ class BiliManga : HttpSource(), ConfigurableSource {
override fun pageListParse(response: Response) = response.asJsoup().let { override fun pageListParse(response: Response) = response.asJsoup().let {
val images = it.select(".imagecontent") val images = it.select(".imagecontent")
check(images.size > 0) { check(images.isNotEmpty()) {
it.selectFirst("#acontentz")?.let { e -> it.selectFirst("#acontentz")?.let { e ->
if ("電腦端" in e.text()) "不支持電腦端查看請在高級設置中更換移動端UA標識" else "漫畫可能已下架或需要登錄查看" if ("電腦端" in e.text()) "不支持電腦端查看請在高級設置中更換移動端UA標識" else "漫畫可能已下架或需要登錄查看"
} ?: "章节鏈接错误" } ?: "章节鏈接错误"

View File

@ -4,39 +4,103 @@ import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
fun buildFilterList() = FilterList( fun buildFilterList() = FilterList(
Filter.Header("篩選條件(搜尋時無效)"), Filter.Header("篩選條件(搜尋關鍵字時無效)"),
RankFilter(), ThemeFilter(), // 1
TypeFilter(), // 5
RegionFilter(), // 4
SortFilter(), // 0
AnimeFilter(), // 3
NovelFilter(), // 7
StatusFilter(), // 2
TimeFilter(), // 6
) )
class RankFilter : Filter.Select<String>( class ThemeFilter : Filter.Select<String>(
"排行榜", "作品主題",
arrayOf( arrayOf(
"月點擊榜", "不限", "奇幻", "冒險", "異世界", "龍傲天", "魔法",
"周點擊榜", "仙俠", "戰爭", "熱血", "戰鬥", "競技", "懸疑",
"月推薦榜", "驚悚", "獵奇", "神鬼", "偵探", "校園", "日常",
"周推薦榜", "JK", "JC", "青梅竹馬", "妹妹", "大小姐", "女兒",
"月鮮花榜", "愛情", "耽美", "百合", "NTR", "後宮", "職場",
"周鮮花榜", "經營", "犯罪", "旅行", "群像", "女性視角",
"月雞蛋榜", "歷史", "武俠", "東方", "勵志", "宅系", "科幻",
"周雞蛋榜", "機戰", "遊戲", "異能", "腦洞", "病嬌", "人外",
"最新入庫", "復仇", "鬥智", "惡役", "間諜", "治癒", "歡樂",
"收藏榜", "萌系", "末日", "大逃殺", "音樂", "美食", "性轉",
"新書榜", "偽娘", "穿越", "童話", "轉生", "黑暗", "溫馨",
"超自然",
), ),
) { ) {
override fun toString(): String { override fun toString(): String {
return arrayOf( return arrayOf(
"monthvisit", "0", "1", "2", "3", "4", "5", "6", "7", "8",
"weekvisit", "9", "10", "11", "12", "13", "14", "15", "16",
"monthvote", "17", "18", "19", "20", "21", "22", "23", "24",
"weekvote", "25", "26", "27", "28", "29", "30", "31", "32",
"monthflower", "33", "34", "35", "36", "37", "38", "39", "40",
"weekflower", "41", "42", "43", "44", "45", "46", "47", "48",
"monthegg", "49", "50", "51", "52", "53", "54", "55", "56",
"weekegg", "57", "58", "59", "60", "61", "62", "63", "64",
"postdate", "65",
"goodnum",
"newhot",
)[state] )[state]
} }
} }
class TypeFilter : Filter.Select<String>(
"作品分類",
arrayOf(
"全部",
"奇幻冒險", "戰鬥熱血", "懸疑驚悚", "校園青春",
"愛情浪漫", "職場都市", "歷史文化", "科幻未來",
"奇異幻想", "治癒溫馨", "末日生存", "其他分類",
),
) {
override fun toString(): String {
return arrayOf(
"0", "1", "2", "3", "4", "5", "6", "7", "8",
"9", "10", "11", "12",
)[state]
}
}
class RegionFilter : Filter.Select<String>(
"作品地區",
arrayOf("不限", "日本", "韓國", "港台", "歐美", "大陸"),
) {
override fun toString() = arrayOf("0", "1", "2", "3", "4", "5")[state]
}
class SortFilter : Filter.Select<String>(
"排序方式",
arrayOf(
"最近更新", "月點擊", "周推薦", "月推薦", "周鮮花",
"月鮮花", "字數", "收藏數", "周點擊", "最新入庫",
),
) {
override fun toString(): String {
return arrayOf(
"lastupdate", "monthvisit", "weekvote", "monthvote", "weekflower",
"monthflower", "words", "goodnum", "weekvisit", "postdate",
)[state]
}
}
class AnimeFilter : Filter.Select<String>("是否動畫", arrayOf("不限", "已動畫化", "未動畫化")) {
override fun toString() = arrayOf("0", "1", "2")[state]
}
class NovelFilter : Filter.Select<String>("是否輕改", arrayOf("不限", "輕改漫畫", "普通漫畫")) {
override fun toString() = arrayOf("0", "1", "2")[state]
}
class StatusFilter : Filter.Select<String>("連載狀態", arrayOf("不限", "連載", "完結")) {
override fun toString() = arrayOf("0", "1", "2")[state]
}
class TimeFilter : Filter.Select<String>(
"更新時間",
arrayOf("不限", "三日內", "七日內", "半月內", "一月內"),
) {
override fun toString() = arrayOf("0", "1", "2", "3", "4")[state]
}