parent
eb6eaf28ca
commit
c3330537fb
|
@ -0,0 +1,12 @@
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
|
ext {
|
||||||
|
appName = 'Tachiyomi: Manhuaren'
|
||||||
|
pkgNameSuffix = 'zh.manhuaren'
|
||||||
|
extClass = '.Manhuaren'
|
||||||
|
extVersionCode = 1
|
||||||
|
libVersion = '1.2'
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 7.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
|
@ -0,0 +1,336 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.zh.manhuaren
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import kotlin.collections.mutableMapOf
|
||||||
|
import kotlin.collections.MutableMap
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.net.URLEncoder
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
class Manhuaren : HttpSource() {
|
||||||
|
override val lang = "zh"
|
||||||
|
override val supportsLatest = true
|
||||||
|
override val name = "漫画人"
|
||||||
|
override val baseUrl = "http://mangaapi.manhuaren.com"
|
||||||
|
|
||||||
|
private val pageSize = 20
|
||||||
|
|
||||||
|
private val c = "4e0a48e1c0b54041bce9c8f0e036124d"
|
||||||
|
|
||||||
|
private fun myGet(url: String): Request {
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun headersBuilder() = super.headersBuilder().add("X-Yq-Yqci", "{\"le\": \"zh\"}")
|
||||||
|
|
||||||
|
private fun hashString(type: String, input: String): String {
|
||||||
|
val HEX_CHARS = "0123456789abcdef"
|
||||||
|
val bytes = MessageDigest
|
||||||
|
.getInstance(type)
|
||||||
|
.digest(input.toByteArray())
|
||||||
|
val result = StringBuilder(bytes.size * 2)
|
||||||
|
|
||||||
|
bytes.forEach {
|
||||||
|
val i = it.toInt()
|
||||||
|
result.append(HEX_CHARS[i shr 4 and 0x0f])
|
||||||
|
result.append(HEX_CHARS[i and 0x0f])
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun urlEncode(str: String?): String {
|
||||||
|
return URLEncoder.encode(str, "UTF-8")
|
||||||
|
.replace("+", "%20")
|
||||||
|
.replace("%7E", "~")
|
||||||
|
.replace("*", "%2A")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateGSNHash(params: MutableMap<String, String>): String {
|
||||||
|
var s = c + "GET"
|
||||||
|
val keys = params.toSortedMap().keys
|
||||||
|
keys.forEach {
|
||||||
|
s += it
|
||||||
|
s += urlEncode(params[it])
|
||||||
|
}
|
||||||
|
s += c
|
||||||
|
return hashString("MD5", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateApiRequestUrl(path: String, params: MutableMap<String, String>): String {
|
||||||
|
val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd+HH:mm:ss")
|
||||||
|
val now = LocalDateTime.now().format(dateFormatter)
|
||||||
|
|
||||||
|
params["gsm"] = "md5"
|
||||||
|
params["gft"] = "json"
|
||||||
|
params["gts"] = now
|
||||||
|
params["gak"] = "android_manhuaren2"
|
||||||
|
params["gat"] = ""
|
||||||
|
params["gaui"] = "191909801"
|
||||||
|
params["gui"] = "191909801"
|
||||||
|
params["gut"] = "0"
|
||||||
|
params["gsn"] = generateGSNHash(params)
|
||||||
|
val queryString = params.map { (key, value) -> "$key=${urlEncode(value)}" }.joinToString("&")
|
||||||
|
return "$path?$queryString"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mangasFromJSONArray(arr: JSONArray): MangasPage {
|
||||||
|
val ret = ArrayList<SManga>(arr.length())
|
||||||
|
for (i in 0 until arr.length()) {
|
||||||
|
var obj = arr.getJSONObject(i)
|
||||||
|
var id = obj.getInt("mangaId")
|
||||||
|
ret.add(SManga.create().apply {
|
||||||
|
title = obj.getString("mangaName")
|
||||||
|
thumbnail_url = obj.getString("mangaCoverimageUrl")
|
||||||
|
author = obj.optString("mangaAuthor")
|
||||||
|
status = when (obj.getInt("mangaIsOver")) {
|
||||||
|
1 -> SManga.COMPLETED
|
||||||
|
0 -> SManga.ONGOING
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
url = generateApiRequestUrl(
|
||||||
|
"/v1/manga/getDetail",
|
||||||
|
mutableMapOf<String, String>("mangaId" to id.toString())
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return MangasPage(ret, arr.length() != 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mangasPageParse(response: Response): MangasPage {
|
||||||
|
val res = response.body()!!.string()
|
||||||
|
val arr = JSONObject(res).getJSONObject("response").getJSONArray("mangas")
|
||||||
|
return mangasFromJSONArray(arr)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
val params = mutableMapOf<String, String>(
|
||||||
|
"subCategoryType" to "0",
|
||||||
|
"subCategoryId" to "0",
|
||||||
|
"start" to (pageSize * (page - 1)).toString(),
|
||||||
|
"limit" to pageSize.toString(),
|
||||||
|
"sort" to "0"
|
||||||
|
)
|
||||||
|
return myGet(baseUrl + generateApiRequestUrl("/v2/manga/getCategoryMangas", params))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
val params = mutableMapOf<String, String>(
|
||||||
|
"subCategoryType" to "0",
|
||||||
|
"subCategoryId" to "0",
|
||||||
|
"start" to (pageSize * (page - 1)).toString(),
|
||||||
|
"limit" to pageSize.toString(),
|
||||||
|
"sort" to "1"
|
||||||
|
)
|
||||||
|
return myGet(baseUrl + generateApiRequestUrl("/v2/manga/getCategoryMangas", params))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
|
return mangasPageParse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
|
return mangasPageParse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
if (query != "") {
|
||||||
|
return myGet(baseUrl + generateApiRequestUrl(
|
||||||
|
"/v1/search/getSearchManga",
|
||||||
|
mutableMapOf<String, String>(
|
||||||
|
"keywords" to query,
|
||||||
|
"start" to (pageSize * (page - 1)).toString(),
|
||||||
|
"limit" to pageSize.toString()
|
||||||
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
val params = mutableMapOf<String, String>(
|
||||||
|
"start" to (pageSize * (page - 1)).toString(),
|
||||||
|
"limit" to pageSize.toString()
|
||||||
|
)
|
||||||
|
filters.forEach { filter ->
|
||||||
|
when (filter) {
|
||||||
|
is SortFilter -> params["sort"] = filter.getId()
|
||||||
|
is CategoryFilter -> {
|
||||||
|
params["subCategoryId"] = filter.getId()
|
||||||
|
params["subCategoryType"] = filter.getType()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return myGet(baseUrl + generateApiRequestUrl("/v2/manga/getCategoryMangas", params))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
|
val res = response.body()!!.string()
|
||||||
|
val obj = JSONObject(res).getJSONObject("response")
|
||||||
|
if (obj.has("result")) {
|
||||||
|
return mangasFromJSONArray(obj.getJSONArray("result"))
|
||||||
|
}
|
||||||
|
return mangasFromJSONArray(obj.getJSONArray("mangas"))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
|
||||||
|
val res = response.body()!!.string()
|
||||||
|
val obj = JSONObject(res).getJSONObject("response")
|
||||||
|
title = obj.getString("mangaName")
|
||||||
|
thumbnail_url = obj.getString("mangaCoverimageUrl")
|
||||||
|
|
||||||
|
var arr = obj.getJSONArray("mangaAuthors")
|
||||||
|
var tmparr = ArrayList<String>(arr.length())
|
||||||
|
for (i in 0 until arr.length()) {
|
||||||
|
tmparr.add(arr.getString(i))
|
||||||
|
}
|
||||||
|
author = tmparr.joinToString(", ")
|
||||||
|
|
||||||
|
genre = obj.getString("mangaTheme").replace(" ", ", ")
|
||||||
|
|
||||||
|
status = when (obj.getInt("mangaIsOver")) {
|
||||||
|
1 -> SManga.COMPLETED
|
||||||
|
0 -> SManga.ONGOING
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
description = obj.getString("mangaIntro")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getChapterName(type: String, name: String, title: String): String {
|
||||||
|
return (if (type == "mangaEpisode") "[番外] " else "") + name + (if (title == "") "" else ": $title")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun chaptersFromJSONArray(type: String, arr: JSONArray): List<SChapter> {
|
||||||
|
val ret = ArrayList<SChapter>()
|
||||||
|
for (i in 0 until arr.length()) {
|
||||||
|
val obj = arr.getJSONObject(i)
|
||||||
|
ret.add(SChapter.create().apply {
|
||||||
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd")
|
||||||
|
name = getChapterName(type, obj.getString("sectionName"), obj.getString("sectionTitle"))
|
||||||
|
date_upload = dateFormat.parse(obj.getString("releaseTime")).getTime()
|
||||||
|
chapter_number = obj.getInt("sectionSort").toFloat()
|
||||||
|
url = generateApiRequestUrl(
|
||||||
|
"/v1/manga/getRead",
|
||||||
|
mutableMapOf(
|
||||||
|
"mangaSectionId" to obj.getInt("sectionId").toString(),
|
||||||
|
"netType" to "4",
|
||||||
|
"loadreal" to "1",
|
||||||
|
"imageQuality" to "2"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
val res = response.body()!!.string()
|
||||||
|
val obj = JSONObject(res).getJSONObject("response")
|
||||||
|
val ret = ArrayList<SChapter>()
|
||||||
|
listOf("mangaEpisode", "mangaWords", "mangaRolls").forEach {
|
||||||
|
if (obj.has(it)) {
|
||||||
|
ret.addAll(chaptersFromJSONArray(it, obj.getJSONArray(it)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val res = response.body()!!.string()
|
||||||
|
val obj = JSONObject(res).getJSONObject("response")
|
||||||
|
val ret = ArrayList<Page>()
|
||||||
|
val host = obj.getJSONArray("hostList").getString(0)
|
||||||
|
val arr = obj.getJSONArray("mangaSectionImages")
|
||||||
|
for (i in 0 until arr.length()) {
|
||||||
|
ret.add(Page(i, "$host${arr.getString(i)}", "$host${arr.getString(i)}"))
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException("This method should not be called!")
|
||||||
|
|
||||||
|
override fun getFilterList() = FilterList(
|
||||||
|
SortFilter("状态", arrayOf(
|
||||||
|
Pair("热门", "0"),
|
||||||
|
Pair("更新", "1"),
|
||||||
|
Pair("新作", "2"),
|
||||||
|
Pair("完结", "3")
|
||||||
|
)),
|
||||||
|
CategoryFilter("分类", arrayOf(
|
||||||
|
Category("全部", "0", "0"),
|
||||||
|
Category("热血", "0", "31"),
|
||||||
|
Category("恋爱", "0", "26"),
|
||||||
|
Category("校园", "0", "1"),
|
||||||
|
Category("百合", "0", "3"),
|
||||||
|
Category("耽美", "0", "27"),
|
||||||
|
Category("伪娘", "0", "5"),
|
||||||
|
Category("冒险", "0", "2"),
|
||||||
|
Category("职场", "0", "6"),
|
||||||
|
Category("后宫", "0", "8"),
|
||||||
|
Category("治愈", "0", "9"),
|
||||||
|
Category("科幻", "0", "25"),
|
||||||
|
Category("励志", "0", "10"),
|
||||||
|
Category("生活", "0", "11"),
|
||||||
|
Category("战争", "0", "12"),
|
||||||
|
Category("悬疑", "0", "17"),
|
||||||
|
Category("推理", "0", "33"),
|
||||||
|
Category("搞笑", "0", "37"),
|
||||||
|
Category("奇幻", "0", "14"),
|
||||||
|
Category("魔法", "0", "15"),
|
||||||
|
Category("恐怖", "0", "29"),
|
||||||
|
Category("神鬼", "0", "20"),
|
||||||
|
Category("萌系", "0", "21"),
|
||||||
|
Category("历史", "0", "4"),
|
||||||
|
Category("美食", "0", "7"),
|
||||||
|
Category("同人", "0", "30"),
|
||||||
|
Category("运动", "0", "34"),
|
||||||
|
Category("绅士", "0", "36"),
|
||||||
|
Category("机甲", "0", "40"),
|
||||||
|
Category("限制级", "0", "61"),
|
||||||
|
Category("少年向", "1", "1"),
|
||||||
|
Category("少女向", "1", "2"),
|
||||||
|
Category("青年向", "1", "3"),
|
||||||
|
Category("港台", "2", "35"),
|
||||||
|
Category("日韩", "2", "36"),
|
||||||
|
Category("大陆", "2", "37"),
|
||||||
|
Category("欧美", "2", "52")
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class Category(val name: String, val type: String, val id: String)
|
||||||
|
|
||||||
|
private class SortFilter(
|
||||||
|
name: String,
|
||||||
|
val vals: Array<Pair<String, String>>,
|
||||||
|
state: Int = 0
|
||||||
|
) : Filter.Select<String>(
|
||||||
|
name,
|
||||||
|
vals.map { it.first }.toTypedArray(),
|
||||||
|
state
|
||||||
|
) {
|
||||||
|
fun getId() = vals[state].second
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CategoryFilter(
|
||||||
|
name: String,
|
||||||
|
val vals: Array<Category>,
|
||||||
|
state: Int = 0
|
||||||
|
) : Filter.Select<String>(
|
||||||
|
name,
|
||||||
|
vals.map { it.name }.toTypedArray(),
|
||||||
|
state
|
||||||
|
) {
|
||||||
|
fun getId() = vals[state].id
|
||||||
|
fun getType() = vals[state].type
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue