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 {
extName = 'BiliManga'
extClass = '.BiliManga'
extVersionCode = 3
extVersionCode = 4
isNsfw = true
}

View File

@ -16,6 +16,7 @@ import keiyoushi.utils.tryParse
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.select.Elements
import java.text.SimpleDateFormat
@ -53,13 +54,29 @@ class BiliManga : HttpSource(), ConfigurableSource {
}
companion object {
const val PAGE_SIZE = 50
val META_REGEX = Regex("連載|完結|收藏|推薦|热度")
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 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) {
0 -> "${els[1].attr("href")}#prev"
else -> "${els[i - 1].attr("href")}#next"
@ -72,8 +89,8 @@ class BiliManga : HttpSource(), ConfigurableSource {
return GET(baseUrl + String.format(suffix, page), headers)
}
override fun popularMangaParse(response: Response) = response.asJsoup().let {
val mangas = it.select(".book-layout").map {
override fun popularMangaParse(response: Response) = response.asJsoup().let { doc ->
val mangas = doc.select(".book-layout").map {
SManga.create().apply {
setUrlWithoutDomain(it.absUrl("href"))
val img = it.selectFirst("img")!!
@ -81,12 +98,13 @@ class BiliManga : HttpSource(), ConfigurableSource {
title = img.attr("alt")
}
}
MangasPage(mangas, mangas.size >= PAGE_SIZE)
MangasPage(mangas, hasNextPage(doc, mangas.size))
}
// 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)
@ -94,13 +112,14 @@ class BiliManga : HttpSource(), ConfigurableSource {
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 {
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")
url.addPathSegment("filter")
.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)
}
@ -118,13 +137,11 @@ class BiliManga : HttpSource(), ConfigurableSource {
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 {
"\n\n漫畫別名:\n${it.text().split("、").joinToString("\n• ")}"
}
val backupname = doc.selectFirst(".backupname")?.let { "【別名:${it.text()}\n\n" } ?: ""
setUrlWithoutDomain(doc.location())
title = doc.selectFirst(".book-title")!!.text()
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()
author = doc.selectFirst(".illname")?.text() ?: artist
status = when (meta.firstOrNull()) {
@ -138,7 +155,8 @@ class BiliManga : HttpSource(), ConfigurableSource {
// 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 {
val info = it.selectFirst(".chapter-sub-title")!!.text()
@ -162,7 +180,7 @@ class BiliManga : HttpSource(), ConfigurableSource {
override fun pageListParse(response: Response) = response.asJsoup().let {
val images = it.select(".imagecontent")
check(images.size > 0) {
check(images.isNotEmpty()) {
it.selectFirst("#acontentz")?.let { e ->
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
fun buildFilterList() = FilterList(
Filter.Header("篩選條件(搜尋時無效)"),
RankFilter(),
Filter.Header("篩選條件(搜尋關鍵字時無效)"),
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(
"月點擊榜",
"周點擊榜",
"月推薦榜",
"周推薦榜",
"月鮮花榜",
"周鮮花榜",
"月雞蛋榜",
"周雞蛋榜",
"最新入庫",
"收藏榜",
"新書榜",
"不限", "奇幻", "冒險", "異世界", "龍傲天", "魔法",
"仙俠", "戰爭", "熱血", "戰鬥", "競技", "懸疑",
"驚悚", "獵奇", "神鬼", "偵探", "校園", "日常",
"JK", "JC", "青梅竹馬", "妹妹", "大小姐", "女兒",
"愛情", "耽美", "百合", "NTR", "後宮", "職場",
"經營", "犯罪", "旅行", "群像", "女性視角",
"歷史", "武俠", "東方", "勵志", "宅系", "科幻",
"機戰", "遊戲", "異能", "腦洞", "病嬌", "人外",
"復仇", "鬥智", "惡役", "間諜", "治癒", "歡樂",
"萌系", "末日", "大逃殺", "音樂", "美食", "性轉",
"偽娘", "穿越", "童話", "轉生", "黑暗", "溫馨",
"超自然",
),
) {
override fun toString(): String {
return arrayOf(
"monthvisit",
"weekvisit",
"monthvote",
"weekvote",
"monthflower",
"weekflower",
"monthegg",
"weekegg",
"postdate",
"goodnum",
"newhot",
"0", "1", "2", "3", "4", "5", "6", "7", "8",
"9", "10", "11", "12", "13", "14", "15", "16",
"17", "18", "19", "20", "21", "22", "23", "24",
"25", "26", "27", "28", "29", "30", "31", "32",
"33", "34", "35", "36", "37", "38", "39", "40",
"41", "42", "43", "44", "45", "46", "47", "48",
"49", "50", "51", "52", "53", "54", "55", "56",
"57", "58", "59", "60", "61", "62", "63", "64",
"65",
)[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]
}