Happymh: Support search filters (#8985)

* Happymh: Fix chapter order

* Happymh: Support filters

* Rename header to headers for clarity

* Apply review suggestions
This commit is contained in:
AlphaBoom 2025-05-31 08:18:33 +08:00 committed by Draff
parent 5d970dab5a
commit af50939f4e
Signed by: Draff
GPG Key ID: E8A89F3211677653
3 changed files with 240 additions and 26 deletions

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Happymh' extName = 'Happymh'
extClass = '.Happymh' extClass = '.Happymh'
extVersionCode = 17 extVersionCode = 18
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -0,0 +1,196 @@
package eu.kanade.tachiyomi.extension.zh.happymh
import eu.kanade.tachiyomi.source.model.Filter
open class UriPartFilter(
name: String,
val key: String,
private val pairs: List<Pair<String, String>>,
) :
Filter.Select<String>(name, pairs.map { it.first }.toTypedArray()) {
val selected: String
get() = pairs[state].second
}
class GenreFilter : UriPartFilter(
"分类",
"genre",
listOf(
"全部" to "",
"热血" to "rexue",
"格斗" to "gedou",
"武侠" to "wuxia",
"魔幻" to "mohuan",
"魔法" to "mofa",
"冒险" to "maoxian",
"爱情" to "aiqing",
"搞笑" to "gaoxiao",
"校园" to "xiaoyuan",
"科幻" to "kehuan",
"后宫" to "hougong",
"励志" to "lizhi",
"职场" to "zhichang",
"美食" to "meishi",
"社会" to "shehui",
"黑道" to "heidao",
"战争" to "zhanzheng",
"历史" to "lishi",
"悬疑" to "xuanyi",
"竞技" to "jingji",
"体育" to "tiyu",
"恐怖" to "kongbu",
"推理" to "tuili",
"生活" to "shenghuo",
"伪娘" to "weiniang",
"治愈" to "zhiyu",
"神鬼" to "shengui",
"四格" to "sige",
"百合" to "baihe",
"耽美" to "danmei",
"舞蹈" to "wudao",
"侦探" to "zhentan",
"宅男" to "zhainan",
"音乐" to "yinyue",
"萌系" to "mengxi",
"古风" to "gufeng",
"恋爱" to "lianai",
"都市" to "dushi",
"性转" to "xingzhuan",
"穿越" to "chuanyue",
"游戏" to "youxi",
"其他" to "qita",
"爱妻" to "aiqi",
"日常" to "richang",
"腹黑" to "fuhei",
"古装" to "guzhuang",
"仙侠" to "xianxia",
"生化" to "shenghua",
"修仙" to "xiuxian",
"情感" to "qinggan",
"改编" to "gaibian",
"纯爱" to "chunai",
"唯美" to "weimei",
"蔷薇" to "qiangwei",
"明星" to "mingxing",
"猎奇" to "lieqi",
"青春" to "qingchun",
"幻想" to "huanxiang",
"惊奇" to "jingqi",
"彩虹" to "caihong",
"奇闻" to "qiwen",
"权谋" to "quanmou",
"宅斗" to "zhaidou",
"限制级" to "xianzhiji",
"装逼" to "zhuangbi",
"浪漫" to "langman",
"偶像" to "ouxiang",
"大女主" to "danvzhu",
"复仇" to "fuchou",
"虐心" to "nuexin",
"恶搞" to "egao",
"灵异" to "lingyi",
"惊险" to "jingxian",
"宠爱" to "chongai",
"逆袭" to "nixi",
"妖怪" to "yaoguai",
"暧昧" to "aimei",
"同人" to "tongren",
"架空" to "jiakong",
"真人" to "zhenren",
"动作" to "dongzuo",
"橘味" to "juwei",
"宫斗" to "gongdou",
"脑洞" to "naodong",
"漫改" to "mangai",
"战斗" to "zhandou",
"丧尸" to "sangshi",
"美少女" to "meishaonv",
"怪物" to "guaiwu",
"系统" to "xitong",
"智斗" to "zhidou",
"机甲" to "jijia",
"高甜" to "gaotian",
"僵尸" to "jiangshi",
"致郁" to "zhiyu",
"电竞" to "dianjing",
"神魔" to "shenmo",
"异能" to "yineng",
"末日" to "mori",
"乙女" to "yinv",
"豪快" to "haokuai",
"奇幻" to "qihuan",
"绅士" to "shenshi",
"正能量" to "zhengnengliang",
"宫廷" to "gongting",
"亲情" to "qinqing",
"养成" to "yangcheng",
"剧情" to "juqing",
"轻小说" to "qingxiaoshuo",
"暗黑" to "anhei",
"长条" to "changtiao",
"玄幻" to "xuanhuan",
"霸总" to "bazong",
"欧皇" to "ouhuang",
"生存" to "shengcun",
"异世界" to "yishijie",
"其它" to "qita",
"C99" to "C99",
"节操" to "jiecao",
"AA" to "AA",
"影视化" to "yingshihua",
"欧风" to "oufeng",
"女神" to "nvshen",
"爽感" to "shuanggan",
"转生" to "zhuansheng",
"异形" to "yixing",
"反套路" to "fantaolu",
"双男主" to "shuangnanzhu",
"无敌流" to "wudiliu",
"性转换" to "xingzhuanhuan",
"重生" to "zhongsheng",
"血腥" to "xuexing",
"奇遇" to "qiyu",
"泛爱" to "fanai",
"软萌" to "ruanmeng",
"小天使" to "xiaotianshi",
"邪恶" to "xiee",
"後宫" to "hougong",
),
)
class AreaFilter : UriPartFilter(
"地区",
"area",
listOf(
"全部" to "",
"内地" to "china",
"日本" to "japan",
"港台" to "hongkong",
"欧美" to "europe",
"韩国" to "korea",
"其他" to "other",
),
)
class AudienceFilter : UriPartFilter(
"受众",
"audience",
listOf(
"全部" to "",
"少年" to "shaonian",
"少女" to "shaonv",
"青年" to "qingnian",
"BL" to "BL",
"GL" to "GL",
),
)
class StatusFilter : UriPartFilter(
"状态",
"series_status",
listOf(
"全部" to "-1",
"连载中" to "0",
"完结" to "1",
),
)

View File

@ -88,8 +88,8 @@ class Happymh : HttpSource(), ConfigurableSource {
// Requires login, otherwise result is the same as latest updates // Requires login, otherwise result is the same as latest updates
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
val header = headersBuilder().add("referer", "$baseUrl/latest").build() val headers = headersBuilder().add("Referer", "$baseUrl/latest").build()
return GET("$baseUrl/apis/c/index?pn=$page&series_status=-1&order=views", header) return GET("$baseUrl/apis/c/index?pn=$page&series_status=-1&order=views", headers)
} }
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response): MangasPage {
@ -110,8 +110,8 @@ class Happymh : HttpSource(), ConfigurableSource {
// Latest // Latest
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request {
val header = headersBuilder().add("referer", "$baseUrl/latest").build() val headers = headersBuilder().add("Referer", "$baseUrl/latest").build()
return GET("$baseUrl/apis/c/index?pn=$page&series_status=-1&order=last_date", header) return GET("$baseUrl/apis/c/index?pn=$page&series_status=-1&order=last_date", headers)
} }
override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response) override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
@ -119,22 +119,46 @@ class Happymh : HttpSource(), ConfigurableSource {
// Search // Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isNotEmpty()) {
val body = FormBody.Builder() val body = FormBody.Builder()
.addEncoded("searchkey", query) .addEncoded("searchkey", query)
.add("v", "v2.13") .add("v", "v2.13")
.build() .build()
val header = headersBuilder() val headers = headersBuilder()
.add("referer", "$baseUrl/sssearch") .add("Referer", "$baseUrl/sssearch")
.build() .build()
return POST("$baseUrl/v2.0/apis/manga/ssearch", header, body) return POST("$baseUrl/v2.0/apis/manga/ssearch", headers, body)
}
val url = "$baseUrl/apis/c/index".toHttpUrl().newBuilder()
filters.filterIsInstance<UriPartFilter>().forEach {
if (it.selected.isNotEmpty()) {
url.addQueryParameter(it.key, it.selected)
}
}
val headers = headersBuilder().add("Referer", "$baseUrl/latest/${url.build().query}").build()
url.addQueryParameter("pn", page.toString())
return GET(url.build(), headers)
} }
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
if (response.request.url.encodedPath.contains("/apis/c/index")) {
// for filter response
return popularMangaParse(response)
}
return MangasPage(popularMangaParse(response).mangas, false) return MangasPage(popularMangaParse(response).mangas, false)
} }
override fun getFilterList(): FilterList {
return FilterList(
GenreFilter(),
AreaFilter(),
AudienceFilter(),
StatusFilter(),
)
}
// Details // Details
override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply { override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply {
@ -173,7 +197,8 @@ class Happymh : HttpSource(), ConfigurableSource {
if (it.isPageEnd()) { if (it.isPageEnd()) {
Observable.just(page to it) Observable.just(page to it)
} else { } else {
Observable.just(page to it).concatWith(fetchChapterByPageAsObservable(manga, page + 1)) Observable.just(page to it)
.concatWith(fetchChapterByPageAsObservable(manga, page + 1))
} }
} }
} }
@ -190,18 +215,11 @@ class Happymh : HttpSource(), ConfigurableSource {
name = data.chapterName name = data.chapterName
// create a dummy chapter url : /comic_id/dummy_mark/chapter_id#expect_page // create a dummy chapter url : /comic_id/dummy_mark/chapter_id#expect_page
url = "/$comicId/$DUMMY_CHAPTER_MARK/${data.id}#${it.first}" url = "/$comicId/$DUMMY_CHAPTER_MARK/${data.id}#${it.first}"
chapter_number = data.order.toFloat()
} }
} }
} }
.toList() .toList()
.map { it.flatten().sortedByDescending { chapter -> chapter.chapter_number } } .map { it.flatten().reversed() }
.map {
// remove order mark
it.onEach { chapter ->
chapter.chapter_number = -1f
}
}
} }
override fun chapterListParse(response: Response) = throw UnsupportedOperationException() override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
@ -240,11 +258,11 @@ class Happymh : HttpSource(), ConfigurableSource {
val code = fetchChapterCode(chapter) ?: throw Exception("找不到章节地址,请尝试刷新章节列表") val code = fetchChapterCode(chapter) ?: throw Exception("找不到章节地址,请尝试刷新章节列表")
val url = "$baseUrl/v2.0/apis/manga/reading?code=$code&v=v3.1818134" val url = "$baseUrl/v2.0/apis/manga/reading?code=$code&v=v3.1818134"
// Some chapters return 403 without this header // Some chapters return 403 without this header
val header = headersBuilder() val headers = headersBuilder()
.add("X-Requested-With", "XMLHttpRequest") .add("X-Requested-With", "XMLHttpRequest")
.set("Referer", baseUrl + chapter.url) .set("Referer", baseUrl + chapter.url)
.build() .build()
return GET(url, header) return GET(url, headers)
} }
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
@ -259,10 +277,10 @@ class Happymh : HttpSource(), ConfigurableSource {
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
override fun imageRequest(page: Page): Request { override fun imageRequest(page: Page): Request {
val header = headersBuilder() val headers = headersBuilder()
.set("Referer", "$baseUrl/") .set("Referer", "$baseUrl/")
.build() .build()
return GET(page.imageUrl!!, header) return GET(page.imageUrl!!, headers)
} }
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {