diff --git a/src/zh/happymh/AndroidManifest.xml b/src/zh/happymh/AndroidManifest.xml new file mode 100644 index 000000000..30deb7f79 --- /dev/null +++ b/src/zh/happymh/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/src/zh/happymh/build.gradle b/src/zh/happymh/build.gradle new file mode 100644 index 000000000..934f3d473 --- /dev/null +++ b/src/zh/happymh/build.gradle @@ -0,0 +1,11 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'Happymh' + pkgNameSuffix = 'zh.happymh' + extClass = '.Happymh' + extVersionCode = 1 +} + +apply from: "$rootDir/common.gradle" diff --git a/src/zh/happymh/res/mipmap-hdpi/ic_launcher.png b/src/zh/happymh/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..35c1e9718 Binary files /dev/null and b/src/zh/happymh/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/zh/happymh/res/mipmap-mdpi/ic_launcher.png b/src/zh/happymh/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..8fbacdcc5 Binary files /dev/null and b/src/zh/happymh/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/zh/happymh/res/mipmap-xhdpi/ic_launcher.png b/src/zh/happymh/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..40ce467d0 Binary files /dev/null and b/src/zh/happymh/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/zh/happymh/res/mipmap-xxhdpi/ic_launcher.png b/src/zh/happymh/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..03b76796d Binary files /dev/null and b/src/zh/happymh/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/zh/happymh/res/mipmap-xxxhdpi/ic_launcher.png b/src/zh/happymh/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..9c1450b84 Binary files /dev/null and b/src/zh/happymh/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/zh/happymh/res/web_hi_res_512.png b/src/zh/happymh/res/web_hi_res_512.png new file mode 100644 index 000000000..a86063d71 Binary files /dev/null and b/src/zh/happymh/res/web_hi_res_512.png differ diff --git a/src/zh/happymh/src/eu/kanade/tachiyomi/extension/zh/happymh/Happymh.kt b/src/zh/happymh/src/eu/kanade/tachiyomi/extension/zh/happymh/Happymh.kt new file mode 100644 index 000000000..6ef2c096b --- /dev/null +++ b/src/zh/happymh/src/eu/kanade/tachiyomi/extension/zh/happymh/Happymh.kt @@ -0,0 +1,117 @@ +package eu.kanade.tachiyomi.extension.zh.happymh + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +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 kotlinx.serialization.json.Json +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import okhttp3.FormBody +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import uy.kohesive.injekt.injectLazy + +class Happymh : HttpSource() { + override val name: String = "嗨皮漫画" + override val lang: String = "zh" + override val supportsLatest: Boolean = true + override val baseUrl: String = "https://m.happymh.com" + override val client: OkHttpClient = network.cloudflareClient + private val json: Json by injectLazy() + + // Popular + + // Requires login, otherwise result is the same as latest updates + override fun popularMangaRequest(page: Int): Request { + val header = headersBuilder().add("referer", "$baseUrl/latest").build() + return GET("$baseUrl/apis/c/index?pn=$page&series_status=-1&order=views", header) + } + + override fun popularMangaParse(response: Response): MangasPage { + val data = json.parseToJsonElement(response.body!!.string()).jsonObject["data"]!!.jsonObject + val items = data["items"]!!.jsonArray.map { + SManga.create().apply { + val obj = it.jsonObject + title = obj["name"]!!.jsonPrimitive.content + url = "/manga/${obj["manga_code"]!!.jsonPrimitive.content}" + thumbnail_url = obj["cover"]!!.jsonPrimitive.content + } + } + val isEnd = data["isEnd"]!!.jsonPrimitive.boolean + return MangasPage(items, !isEnd) + } + + // Latest + + override fun latestUpdatesRequest(page: Int): Request { + val header = headersBuilder().add("referer", "$baseUrl/latest").build() + return GET("$baseUrl/apis/c/index?pn=$page&series_status=-1&order=last_date", header) + } + + override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response) + + // Search + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val body = FormBody.Builder().addEncoded("searchkey", query).build() + val header = headersBuilder().add("referer", "$baseUrl/ssearch").build() + return POST("$baseUrl/apis/m/ssearch?pn=$page", header, body) + } + + override fun searchMangaParse(response: Response): MangasPage { + // I do not find a way to go to next page, so I always set hasNextPage to false + return MangasPage(popularMangaParse(response).mangas, false) + } + + // Details + + override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply { + val document = response.asJsoup() + title = document.selectFirst("div.mg-property > h2.mg-title").text() + thumbnail_url = document.selectFirst("div.mg-cover > mip-img").attr("abs:src") + author = document.selectFirst("div.mg-property > p.mg-sub-title:nth-of-type(2)").text() + artist = author + genre = document.select("div.mg-property > p.mg-cate > a").eachText().joinToString(", ") + description = document.selectFirst("div.manga-introduction > mip-showmore#showmore").text() + } + + // Chapters + + override fun chapterListParse(response: Response): List { + val comicId = response.request.url.pathSegments.last() + val document = response.asJsoup() + val script = document.selectFirst("mip-data > script:containsData(chapterList)").html() + return json.parseToJsonElement(script).jsonObject["chapterList"]!!.jsonArray.map { + SChapter.create().apply { + val chapterId = it.jsonObject["id"]!!.jsonPrimitive.content + url = "/v2.0/apis/manga/read?code=$comicId&cid=$chapterId" + name = it.jsonObject["chapterName"]!!.jsonPrimitive.content + } + } + } + + // Pages + + override fun pageListRequest(chapter: SChapter): Request { + // Some chapters return 403 without this header + val header = headersBuilder().add("X-Requested-With", "XMLHttpRequest").build() + return GET(baseUrl + chapter.url, header) + } + + override fun pageListParse(response: Response): List = mutableListOf().apply { + json.parseToJsonElement(response.body!!.string()).jsonObject["data"]!!.jsonObject["scans"]!!.jsonArray.mapIndexed() { index, it -> + add(Page(index, "", it.jsonObject["url"]!!.jsonPrimitive.content)) + } + } + + override fun imageUrlParse(response: Response): String = throw Exception("Not Used") +}