Add HWTManga extension (#9365)
This commit is contained in:
parent
1c53f50265
commit
2d7556ba8d
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="eu.kanade.tachiyomi.extension" />
|
|
@ -0,0 +1,13 @@
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlinx-serialization'
|
||||||
|
|
||||||
|
ext {
|
||||||
|
extName = 'Hardworking Translations'
|
||||||
|
pkgNameSuffix = 'en.hwtmanga'
|
||||||
|
extClass = '.HWTManga'
|
||||||
|
extVersionCode = 1
|
||||||
|
containsNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
Binary file not shown.
After Width: | Height: | Size: 160 KiB |
|
@ -0,0 +1,132 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.hwtmanga
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
|
||||||
|
class Tag(name: String, private val id: String) : Filter.CheckBox(name) {
|
||||||
|
override fun toString() = id
|
||||||
|
}
|
||||||
|
|
||||||
|
private val tags: List<Tag>
|
||||||
|
get() = listOf(
|
||||||
|
Tag("Action", "action"),
|
||||||
|
Tag("Adventure", "adventure"),
|
||||||
|
Tag("Comedy", "comedy"),
|
||||||
|
Tag("Cooking", "cooking"),
|
||||||
|
Tag("Drama", "drama"),
|
||||||
|
Tag("Fantasy", "fantasy"),
|
||||||
|
Tag("Horror", "horror"),
|
||||||
|
Tag("Mystery", "mystery"),
|
||||||
|
Tag("Martial Arts", "martialarts"),
|
||||||
|
Tag("Romance", "romance"),
|
||||||
|
Tag("School Life", "school"),
|
||||||
|
Tag("Shoujo", "shoujo"),
|
||||||
|
Tag("Shounen", "shounen"),
|
||||||
|
Tag("Supernatural", "supernatural"),
|
||||||
|
Tag("Sci-fi", "sci-fi"),
|
||||||
|
Tag("Slice of Life", "slice of life"),
|
||||||
|
Tag("Adult", "adult"),
|
||||||
|
Tag("Ancient era", "ancient era"),
|
||||||
|
Tag("Arranged Marriage", "arranged_marriage"),
|
||||||
|
Tag("Age gap", "age_gap"),
|
||||||
|
Tag("Betrayal", "betrayal"),
|
||||||
|
Tag("Clan", "clan"),
|
||||||
|
Tag("Childhood Friends", "childhood_friends"),
|
||||||
|
Tag("Couple", "couple"),
|
||||||
|
Tag("Crime", "crime"),
|
||||||
|
Tag("Cultivation", "cultivation"),
|
||||||
|
Tag("Comic", "comic"),
|
||||||
|
Tag("Delinquent", "delinquent"),
|
||||||
|
Tag("Doujinshi", "doujinshi"),
|
||||||
|
Tag("Ecchi", "ecchi"),
|
||||||
|
Tag("Family", "family"),
|
||||||
|
Tag("Fetishes", "fetish"),
|
||||||
|
Tag("Gender Bender", "gender_bender"),
|
||||||
|
Tag("Gyaru", "gyaru"),
|
||||||
|
Tag("Harem", "harem"),
|
||||||
|
Tag("Historical", "historical"),
|
||||||
|
Tag("Isekai", "isekai"),
|
||||||
|
Tag("Josei", "josei"),
|
||||||
|
Tag("Lolicon", "lolicon"),
|
||||||
|
Tag("Leader or Politician", "leader_politician"),
|
||||||
|
Tag("Mature", "mature"),
|
||||||
|
Tag("Magic", "magic"),
|
||||||
|
Tag("Mangaka", "mangaka"),
|
||||||
|
Tag("Masochist", "masochist"),
|
||||||
|
Tag("Monsters", "monsters"),
|
||||||
|
Tag("Mecha", "mecha"),
|
||||||
|
Tag("Music", "music"),
|
||||||
|
Tag("Medical", "medical"),
|
||||||
|
Tag("Misunderstands", "misunderstands"),
|
||||||
|
Tag("OneShot", "oneshot"),
|
||||||
|
Tag("Public figure", "public figure"),
|
||||||
|
Tag("Psychological", "psychological"),
|
||||||
|
Tag("Powerful Lead Character", "powerful"),
|
||||||
|
Tag("Rushed ending", "rushed end"),
|
||||||
|
Tag("Revenge", "revenge"),
|
||||||
|
Tag("Reverse Harem", "reverse_harem"),
|
||||||
|
Tag("Sadist", "sadist"),
|
||||||
|
Tag("Seinen", "seinen"),
|
||||||
|
Tag("Shotacon", "shotacon"),
|
||||||
|
Tag("Secret Crush", "secret_crush"),
|
||||||
|
Tag("Secret Relationship", "secret_relationship"),
|
||||||
|
Tag("Smart MC", "smart_mc"),
|
||||||
|
Tag("Sports", "sports"),
|
||||||
|
Tag("Smut", "smut"),
|
||||||
|
Tag("Tragedy", "tragedy"),
|
||||||
|
Tag("Tomboy", "tomboy"),
|
||||||
|
Tag("Triangles", "triangles"),
|
||||||
|
Tag("Unusual Pupils", "unusual_pupils"),
|
||||||
|
Tag("Vampires", "vampires"),
|
||||||
|
Tag("Webtoon", "webtoon"),
|
||||||
|
Tag("Work", "work"),
|
||||||
|
Tag("Zombies", "zombies"),
|
||||||
|
Tag("4-Koma", "4koma"),
|
||||||
|
Tag("Manga", "manga"),
|
||||||
|
Tag("Manhwa", "manhwa"),
|
||||||
|
Tag("Manhua", "manhua"),
|
||||||
|
)
|
||||||
|
|
||||||
|
class TagFilter(
|
||||||
|
values: List<Tag> = tags
|
||||||
|
) : Filter.Group<Tag>("Tag Match", values) {
|
||||||
|
override fun toString() =
|
||||||
|
state.filter { it.state }.joinToString(";").ifEmpty { "all;" }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val states: Array<String>
|
||||||
|
get() = arrayOf("ALL", "Completed", "Ongoing")
|
||||||
|
|
||||||
|
class StateFilter(
|
||||||
|
values: Array<String> = states
|
||||||
|
) : Filter.Select<String>("State", values) {
|
||||||
|
private val ids = arrayOf("all", "complete", "ongoing")
|
||||||
|
|
||||||
|
override fun toString() = ids[state]
|
||||||
|
}
|
||||||
|
|
||||||
|
private val orders: Array<String>
|
||||||
|
get() = arrayOf(
|
||||||
|
"A~Z",
|
||||||
|
"Z~A",
|
||||||
|
"Newest",
|
||||||
|
"Oldest",
|
||||||
|
"Most Liked",
|
||||||
|
"Most Viewed",
|
||||||
|
"Most Favourite"
|
||||||
|
)
|
||||||
|
|
||||||
|
class OrderFilter(
|
||||||
|
values: Array<String> = orders
|
||||||
|
) : Filter.Select<String>("Order By", values) {
|
||||||
|
private val ids = arrayOf(
|
||||||
|
"az",
|
||||||
|
"za",
|
||||||
|
"newest",
|
||||||
|
"oldest",
|
||||||
|
"liked",
|
||||||
|
"viewed",
|
||||||
|
"fav"
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun toString() = ids[state]
|
||||||
|
}
|
|
@ -0,0 +1,215 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.hwtmanga
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.annotations.Nsfw
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
|
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 kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import okhttp3.Cookie
|
||||||
|
import okhttp3.CookieJar
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@Nsfw class HWTManga : HttpSource() {
|
||||||
|
override val name = "Hardworking Translations"
|
||||||
|
|
||||||
|
override val baseUrl = "https://www.hwtmanga.com/hwt/"
|
||||||
|
|
||||||
|
override val lang = "en"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override val client = network.client.newBuilder().cookieJar(
|
||||||
|
object : CookieJar {
|
||||||
|
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {}
|
||||||
|
|
||||||
|
override fun loadForRequest(url: HttpUrl) =
|
||||||
|
listOf(
|
||||||
|
Cookie.Builder()
|
||||||
|
.domain("www.hwtmanga.com")
|
||||||
|
.path("/hwt")
|
||||||
|
.name("PHPSESSID")
|
||||||
|
.value(sessionID)
|
||||||
|
.build(),
|
||||||
|
Cookie.Builder()
|
||||||
|
.domain("www.hwtmanga.com")
|
||||||
|
.path("/")
|
||||||
|
.name("manga_security_id")
|
||||||
|
.value(postID)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
).build()
|
||||||
|
|
||||||
|
private var postID = ""
|
||||||
|
|
||||||
|
private var sessionID = ""
|
||||||
|
|
||||||
|
private val json by injectLazy<Json>()
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int) =
|
||||||
|
FormBody.Builder().search(order = "newest", pid = page)
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response) =
|
||||||
|
searchMangaParse(response)
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int) =
|
||||||
|
FormBody.Builder().search(order = "viewed", pid = page)
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response) =
|
||||||
|
searchMangaParse(response)
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||||
|
FormBody.Builder().search(
|
||||||
|
query = query,
|
||||||
|
pid = page,
|
||||||
|
tags = filters.get<TagFilter>("all;"),
|
||||||
|
state = filters.get<StateFilter>("all"),
|
||||||
|
order = filters.get<OrderFilter>("az")
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response) =
|
||||||
|
response.parse<List<HWTQuery>>("query").map {
|
||||||
|
SManga.create().apply {
|
||||||
|
title = it.title
|
||||||
|
thumbnail_url = it.cimage
|
||||||
|
url = "?page=manga&vid=${it.postID}"
|
||||||
|
}
|
||||||
|
}.let { MangasPage(it, false) }
|
||||||
|
|
||||||
|
override fun fetchMangaDetails(manga: SManga) =
|
||||||
|
FormBody.Builder().post("GET_MANGA_INFO") {
|
||||||
|
add("scom", "0")
|
||||||
|
add("pageid", "1")
|
||||||
|
add("pid", manga.id)
|
||||||
|
}.let(client::newCall).asObservableSuccess().map { res ->
|
||||||
|
// Session cookie is required to view pages
|
||||||
|
if (sessionID == "") {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(baseUrl)
|
||||||
|
.headers(headers)
|
||||||
|
.head().build()
|
||||||
|
client.newCall(request).execute().header("Set-Cookie")?.let {
|
||||||
|
sessionID = Cookie.parse(request.url, it)?.value ?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val info = res.parse<HWTMangaInfo>("mangaInfo")
|
||||||
|
info.tags[0].value = info.mtag.value
|
||||||
|
manga.title = info.title
|
||||||
|
manga.thumbnail_url = info.cover
|
||||||
|
manga.description = info.desc + "\n\n\n" +
|
||||||
|
info.onames.replace(",", " | ")
|
||||||
|
manga.genre = info.tags.joinToString { it.value!! }
|
||||||
|
manga.status = when (info.statue) {
|
||||||
|
1 -> SManga.ONGOING
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
manga.initialized = true
|
||||||
|
return@map manga
|
||||||
|
}!!
|
||||||
|
|
||||||
|
override fun chapterListRequest(manga: SManga) =
|
||||||
|
FormBody.Builder().post("GET_CHAPTER_LIST") {
|
||||||
|
add("pageid", "1")
|
||||||
|
add("pid", manga.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response) =
|
||||||
|
response.parse<HWTChapterList>("all_data").mapIndexed { idx, ch ->
|
||||||
|
SChapter.create().apply {
|
||||||
|
chapter_number = idx + 1f
|
||||||
|
url = "?page=watch_manga&cid=${ch.fid}&pid=${ch.pid}"
|
||||||
|
date_upload = dateFormat.parse(ch.cdate)?.time ?: 0L
|
||||||
|
name = buildString {
|
||||||
|
append("Chapter %.0f".format(chapter_number))
|
||||||
|
if (ch.name != "-") append(" | ${ch.name}")
|
||||||
|
if (ch.is_locked != "false") append(LOCK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListRequest(chapter: SChapter) =
|
||||||
|
FormBody.Builder().post("GET_CHA_DATA", "manga_viewer") {
|
||||||
|
val tokens = chapter.tokens
|
||||||
|
postID = tokens[5]
|
||||||
|
add("pageid", "1")
|
||||||
|
add("cid", tokens[3])
|
||||||
|
add("pid", postID)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response) =
|
||||||
|
response.parse<List<HWTPage>>("clist")
|
||||||
|
.mapIndexed { idx, page -> Page(idx, "", page.image) }
|
||||||
|
|
||||||
|
override fun getFilterList() =
|
||||||
|
FilterList(TagFilter(), StateFilter(), OrderFilter())
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response) =
|
||||||
|
throw UnsupportedOperationException("Not used")
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response) =
|
||||||
|
throw UnsupportedOperationException("Not used")
|
||||||
|
|
||||||
|
private inline val SManga.id: String
|
||||||
|
get() = url.substringAfterLast('=')
|
||||||
|
|
||||||
|
private inline val SChapter.tokens: List<String>
|
||||||
|
get() = url.split('&', '=')
|
||||||
|
|
||||||
|
private inline val HWTPage.image: String
|
||||||
|
get() = if (base.startsWith("http")) base else baseUrl + base
|
||||||
|
|
||||||
|
private fun FormBody.Builder.post(
|
||||||
|
subpage: String,
|
||||||
|
page: String = "mangaData",
|
||||||
|
block: FormBody.Builder.() -> FormBody.Builder
|
||||||
|
) = add("page", page).add("subpage", subpage).run {
|
||||||
|
POST(baseUrl + "callback.php", headers, block().build())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun FormBody.Builder.search(
|
||||||
|
query: String = "",
|
||||||
|
tags: String = "all;",
|
||||||
|
state: String = "all",
|
||||||
|
order: String = "az",
|
||||||
|
pid: Int = 1
|
||||||
|
) = post("MANGASEARCH") {
|
||||||
|
add("searchbox", query)
|
||||||
|
add("byg", tags)
|
||||||
|
add("bys", state)
|
||||||
|
add("byo", order)
|
||||||
|
add("pid", pid.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> Response.parse(key: String) =
|
||||||
|
body!!.string().let { body ->
|
||||||
|
if ("success" !in body) error(body)
|
||||||
|
json.decodeFromJsonElement<T>(
|
||||||
|
json.parseToJsonElement(body).jsonObject[key]!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> FilterList.get(default: String) =
|
||||||
|
find { it is T }?.toString() ?: default
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val LOCK = " \uD83D\uDD12"
|
||||||
|
|
||||||
|
private val dateFormat by lazy {
|
||||||
|
SimpleDateFormat("MMM dd, yyyy", Locale.ROOT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.hwtmanga
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class HWTQuery(
|
||||||
|
val cimage: String,
|
||||||
|
val postID: String,
|
||||||
|
val title: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class HWTMangaInfo(
|
||||||
|
val cover: String,
|
||||||
|
val desc: String,
|
||||||
|
val mtag: HWTTag,
|
||||||
|
val onames: String,
|
||||||
|
val statue: Int,
|
||||||
|
val postID: Int,
|
||||||
|
val tags: List<HWTTag>,
|
||||||
|
val title: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class HWTTag(var value: String?)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class HWTChapterList(
|
||||||
|
private val chapterList: List<HWTChapter>
|
||||||
|
) : List<HWTChapter> by chapterList
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class HWTChapter(
|
||||||
|
val fid: String,
|
||||||
|
val pid: String,
|
||||||
|
val name: String,
|
||||||
|
val cdate: String,
|
||||||
|
val is_locked: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class HWTPage(val base: String)
|
Loading…
Reference in New Issue