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