Add HentaiHand multisrc (#10664)
* Delete src/all/hentaihand directory * Delete src/all/nhentaicom directory * Delete src/en/readmanhwa directory * Add HH multisrc * Update version numbers * Update HentaiHandGenerator.kt * Add overrides and icons
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
@ -0,0 +1,97 @@
|
|||
package eu.kanade.tachiyomi.extension.all.hentaihand
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.hentaihand.HentaiHand
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class HentaiHandFactory : SourceFactory {
|
||||
override fun createSources(): List<Source> = listOf(
|
||||
// https://hentaihand.com/api/languages?per_page=50
|
||||
HentaiHandOther(),
|
||||
HentaiHandEn(),
|
||||
HentaiHandZh(),
|
||||
HentaiHandJa(),
|
||||
HentaiHandNoText(),
|
||||
HentaiHandEo(),
|
||||
HentaiHandCeb(),
|
||||
HentaiHandCs(),
|
||||
HentaiHandAr(),
|
||||
HentaiHandSk(),
|
||||
HentaiHandMn(),
|
||||
HentaiHandUk(),
|
||||
HentaiHandLa(),
|
||||
HentaiHandTl(),
|
||||
HentaiHandEs(),
|
||||
HentaiHandIt(),
|
||||
HentaiHandKo(),
|
||||
HentaiHandTh(),
|
||||
HentaiHandPl(),
|
||||
HentaiHandFr(),
|
||||
HentaiHandPtBr(),
|
||||
HentaiHandDe(),
|
||||
HentaiHandFi(),
|
||||
HentaiHandRu(),
|
||||
HentaiHandHu(),
|
||||
HentaiHandId(),
|
||||
HentaiHandVi(),
|
||||
HentaiHandNl(),
|
||||
HentaiHandHi(),
|
||||
HentaiHandTr(),
|
||||
HentaiHandEl(),
|
||||
HentaiHandSr(),
|
||||
HentaiHandJv(),
|
||||
HentaiHandBg(),
|
||||
)
|
||||
}
|
||||
abstract class HentaiHandCommon(
|
||||
override val lang: String,
|
||||
hhLangId: List<Int> = emptyList(),
|
||||
//altLangId: Int? = null
|
||||
) : HentaiHand("HentaiHand", "https://hentaihand.com", lang, false, hhLangId) {
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor { authIntercept(it) }
|
||||
.build()
|
||||
}
|
||||
|
||||
class HentaiHandOther : HentaiHandCommon("all") {
|
||||
override val id: Long = 1235047015955289468
|
||||
}
|
||||
class HentaiHandJa : HentaiHandCommon("ja", listOf(1, 29))
|
||||
class HentaiHandEn : HentaiHandCommon("en", listOf(2, 27))
|
||||
class HentaiHandZh : HentaiHandCommon("zh", listOf(3, 50))
|
||||
class HentaiHandBg : HentaiHandCommon("bg", listOf(4))
|
||||
class HentaiHandCeb : HentaiHandCommon("ceb", listOf(5, 44))
|
||||
class HentaiHandNoText : HentaiHandCommon("other", listOf(6)) {
|
||||
override val id: Long = 7302549142935671434
|
||||
}
|
||||
class HentaiHandTl : HentaiHandCommon("tl", listOf(7, 55))
|
||||
class HentaiHandAr : HentaiHandCommon("ar", listOf(8, 49))
|
||||
class HentaiHandEl : HentaiHandCommon("el", listOf(9))
|
||||
class HentaiHandSr : HentaiHandCommon("sr", listOf(10))
|
||||
class HentaiHandJv : HentaiHandCommon("jv", listOf(11, 51))
|
||||
class HentaiHandUk : HentaiHandCommon("uk", listOf(12, 46))
|
||||
class HentaiHandTr : HentaiHandCommon("tr", listOf(13, 41))
|
||||
class HentaiHandFi : HentaiHandCommon("fi", listOf(14, 54))
|
||||
class HentaiHandLa : HentaiHandCommon("la", listOf(15))
|
||||
class HentaiHandMn : HentaiHandCommon("mn", listOf(16))
|
||||
class HentaiHandEo : HentaiHandCommon("eo", listOf(17, 47))
|
||||
class HentaiHandSk : HentaiHandCommon("sk", listOf(18))
|
||||
class HentaiHandCs : HentaiHandCommon("cs", listOf(19, 52))
|
||||
class HentaiHandKo : HentaiHandCommon("ko", listOf(30, 39))
|
||||
class HentaiHandRu : HentaiHandCommon("ru", listOf(31))
|
||||
class HentaiHandIt : HentaiHandCommon("it", listOf(32))
|
||||
class HentaiHandEs : HentaiHandCommon("es", listOf(33, 37))
|
||||
class HentaiHandPtBr : HentaiHandCommon("pt-BR", listOf(34)) {
|
||||
// Hardcode the id because the language wasn't specific.
|
||||
override val id: Long = 2516244587139644000
|
||||
}
|
||||
class HentaiHandTh : HentaiHandCommon("th", listOf(35, 40))
|
||||
class HentaiHandFr : HentaiHandCommon("fr", listOf(36))
|
||||
class HentaiHandId : HentaiHandCommon("id", listOf(38))
|
||||
class HentaiHandVi : HentaiHandCommon("vi", listOf(42))
|
||||
class HentaiHandDe : HentaiHandCommon("de", listOf(43))
|
||||
class HentaiHandPl : HentaiHandCommon("pl", listOf(45))
|
||||
class HentaiHandHu : HentaiHandCommon("hu", listOf(48))
|
||||
class HentaiHandNl : HentaiHandCommon("nl", listOf(53))
|
||||
class HentaiHandHi : HentaiHandCommon("hi", listOf(56))
|
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 5.7 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 83 KiB |
|
@ -0,0 +1,10 @@
|
|||
package eu.kanade.tachiyomi.extension.en.hentaisphere
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.hentaihand.HentaiHand
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class HentaiSphere : HentaiHand("HentaiSphere", "https://hentaisphere.com", "en", false) {
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor { authIntercept(it) }
|
||||
.build()
|
||||
}
|
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 5.8 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 65 KiB |
|
@ -0,0 +1,10 @@
|
|||
package eu.kanade.tachiyomi.extension.en.manhwaclub
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.hentaihand.HentaiHand
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class ManhwaClub : HentaiHand("ManhwaClub", "https://manhwa.club", "en", true) {
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor { authIntercept(it) }
|
||||
.build()
|
||||
}
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
@ -0,0 +1,99 @@
|
|||
package eu.kanade.tachiyomi.extension.all.nhentaicom
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.hentaihand.HentaiHand
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class NHentaiComFactory : SourceFactory {
|
||||
override fun createSources(): List<Source> = listOf(
|
||||
// https://nhentai.com/api/languages?per_page=50
|
||||
NHentaiComAll(),
|
||||
NHentaiComEn(),
|
||||
NHentaiComZh(),
|
||||
NHentaiComJa(),
|
||||
NHentaiComNoText(),
|
||||
NHentaiComEo(),
|
||||
NHentaiComCeb(),
|
||||
NHentaiComCs(),
|
||||
NHentaiComAr(),
|
||||
NHentaiComSk(),
|
||||
NHentaiComMn(),
|
||||
NHentaiComUk(),
|
||||
NHentaiComLa(),
|
||||
NHentaiComTl(),
|
||||
NHentaiComEs(),
|
||||
NHentaiComIt(),
|
||||
NHentaiComKo(),
|
||||
NHentaiComTh(),
|
||||
NHentaiComPl(),
|
||||
NHentaiComFr(),
|
||||
NHentaiComPtBr(),
|
||||
NHentaiComDe(),
|
||||
NHentaiComFi(),
|
||||
NHentaiComRu(),
|
||||
NHentaiComHu(),
|
||||
NHentaiComId(),
|
||||
NHentaiComVi(),
|
||||
NHentaiComNl(),
|
||||
NHentaiComTr(),
|
||||
NHentaiComEl(),
|
||||
NHentaiComBg(),
|
||||
NHentaiComSr(),
|
||||
NHentaiComJv(),
|
||||
NHentaiComHi(),
|
||||
)
|
||||
}
|
||||
abstract class NHentaiComCommon(
|
||||
override val lang: String,
|
||||
hhLangId: List<Int> = emptyList(),
|
||||
//altLangId: Int? = null
|
||||
) : HentaiHand("nHentai.com (unoriginal)", "https://nhentai.com", lang, false, hhLangId) {
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor { authIntercept(it) }
|
||||
.build()
|
||||
}
|
||||
|
||||
class NHentaiComAll : NHentaiComCommon("all") {
|
||||
override val id: Long = 9165839893600661480
|
||||
}
|
||||
|
||||
class NHentaiComJa : NHentaiComCommon("ja", listOf(1, 29))
|
||||
class NHentaiComEn : NHentaiComCommon("en", listOf(2, 27)) {
|
||||
override val id: Long = 5591830863732393712
|
||||
}
|
||||
class NHentaiComZh : NHentaiComCommon("zh", listOf(3, 50))
|
||||
class NHentaiComBg : NHentaiComCommon("bg", listOf(4))
|
||||
class NHentaiComCeb : NHentaiComCommon("ceb", listOf(5, 44))
|
||||
class NHentaiComNoText : NHentaiComCommon("other", listOf(6)) {
|
||||
override val id: Long = 5817327335315373850
|
||||
}
|
||||
class NHentaiComTl : NHentaiComCommon("tl", listOf(7, 55))
|
||||
class NHentaiComAr : NHentaiComCommon("ar", listOf(8, 49))
|
||||
class NHentaiComEl : NHentaiComCommon("el", listOf(9))
|
||||
class NHentaiComSr : NHentaiComCommon("sr", listOf(10))
|
||||
class NHentaiComJv : NHentaiComCommon("jv", listOf(11, 51))
|
||||
class NHentaiComUk : NHentaiComCommon("uk", listOf(12, 46))
|
||||
class NHentaiComTr : NHentaiComCommon("tr", listOf(13, 41))
|
||||
class NHentaiComFi : NHentaiComCommon("fi", listOf(14, 54))
|
||||
class NHentaiComLa : NHentaiComCommon("la", listOf(15))
|
||||
class NHentaiComMn : NHentaiComCommon("mn", listOf(16))
|
||||
class NHentaiComEo : NHentaiComCommon("eo", listOf(17, 47))
|
||||
class NHentaiComSk : NHentaiComCommon("sk", listOf(18))
|
||||
class NHentaiComCs : NHentaiComCommon("cs", listOf(19, 52)) {
|
||||
override val id: Long = 1144495813995437124
|
||||
}
|
||||
class NHentaiComKo : NHentaiComCommon("ko", listOf(30, 39))
|
||||
class NHentaiComRu : NHentaiComCommon("ru", listOf(31))
|
||||
class NHentaiComIt : NHentaiComCommon("it", listOf(32))
|
||||
class NHentaiComEs : NHentaiComCommon("es", listOf(33, 37))
|
||||
class NHentaiComPtBr : NHentaiComCommon("pt-BR", listOf(34))
|
||||
class NHentaiComTh : NHentaiComCommon("th", listOf(35, 40))
|
||||
class NHentaiComFr : NHentaiComCommon("fr", listOf(36))
|
||||
class NHentaiComId : NHentaiComCommon("id", listOf(38))
|
||||
class NHentaiComVi : NHentaiComCommon("vi", listOf(42))
|
||||
class NHentaiComDe : NHentaiComCommon("de", listOf(43))
|
||||
class NHentaiComPl : NHentaiComCommon("pl", listOf(45))
|
||||
class NHentaiComHu : NHentaiComCommon("hu", listOf(48))
|
||||
class NHentaiComNl : NHentaiComCommon("nl", listOf(53))
|
||||
class NHentaiComHi : NHentaiComCommon("hi", listOf(56))
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
@ -0,0 +1,10 @@
|
|||
package eu.kanade.tachiyomi.extension.en.readmanhwa
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.hentaihand.HentaiHand
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class ReadManhwa : HentaiHand("ReadManhwa", "https://readmanhwa.com", "en", true) {
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor { authIntercept(it) }
|
||||
.build()
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
package eu.kanade.tachiyomi.extension.all.hentaihand
|
||||
package eu.kanade.tachiyomi.multisrc.hentaihand
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.text.InputType
|
||||
import android.widget.Toast
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
|
@ -20,13 +21,13 @@ import kotlinx.serialization.json.JsonObject
|
|||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonNull
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.put
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
|
@ -36,22 +37,19 @@ import uy.kohesive.injekt.Injekt
|
|||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
abstract class HentaiHand(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String,
|
||||
private val hhLangId: Int? = null,
|
||||
extraName: String = ""
|
||||
private val chapters: Boolean,
|
||||
private val hhLangId: List<Int> = emptyList(),
|
||||
) : ConfigurableSource, HttpSource() {
|
||||
|
||||
override val baseUrl: String = "https://hentaihand.com"
|
||||
override val name: String = "HentaiHand$extraName"
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor { authIntercept(it) }
|
||||
.build()
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private fun slugToUrl(json: JsonObject) = json["slug"]!!.jsonPrimitive.content.prependIndent("/en/comic/")
|
||||
|
@ -73,7 +71,7 @@ abstract class HentaiHand(
|
|||
SManga.create().apply {
|
||||
url = slugToUrl(obj)
|
||||
title = obj["title"]!!.jsonPrimitive.content
|
||||
thumbnail_url = obj["thumb_url"]!!.jsonPrimitive.content
|
||||
thumbnail_url = obj["image_url"]!!.jsonPrimitive.content
|
||||
}
|
||||
}
|
||||
val hasNextPage = jsonResponse.jsonObject["next_page_url"]!!.jsonPrimitive.content.isNotEmpty()
|
||||
|
@ -81,8 +79,16 @@ abstract class HentaiHand(
|
|||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val url = "$baseUrl/api/comics?page=$page&sort=popularity&order=desc&duration=all"
|
||||
return GET(if (hhLangId == null) url else ("$url&languages=$hhLangId"))
|
||||
val url = "$baseUrl/api/comics".toHttpUrlOrNull()!!.newBuilder()
|
||||
.addQueryParameter("page", page.toString())
|
||||
.addQueryParameter("sort", "popularity")
|
||||
.addQueryParameter("order", "desc")
|
||||
.addQueryParameter("duration", "all")
|
||||
hhLangId.forEachIndexed() {index, it ->
|
||||
url.addQueryParameter("languages[${-index - 1}]", it.toString())
|
||||
}
|
||||
// if (altLangId != null) url.addQueryParameter("languages", altLangId.toString())
|
||||
return GET(url.toString())
|
||||
}
|
||||
|
||||
// Latest
|
||||
|
@ -90,8 +96,15 @@ abstract class HentaiHand(
|
|||
override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val url = "$baseUrl/api/comics?page=$page&sort=uploaded_at&order=desc&duration=week"
|
||||
return GET(if (hhLangId == null) url else ("$url&languages=$hhLangId"))
|
||||
val url = "$baseUrl/api/comics".toHttpUrlOrNull()!!.newBuilder()
|
||||
.addQueryParameter("page", page.toString())
|
||||
.addQueryParameter("sort", "uploaded_at")
|
||||
.addQueryParameter("order", "desc")
|
||||
.addQueryParameter("duration", "all")
|
||||
hhLangId.forEachIndexed() {index, it ->
|
||||
url.addQueryParameter("languages[${-index - 1}]", it.toString())
|
||||
}
|
||||
return GET(url.toString())
|
||||
}
|
||||
|
||||
// Search
|
||||
|
@ -119,8 +132,9 @@ abstract class HentaiHand(
|
|||
.addQueryParameter("page", page.toString())
|
||||
.addQueryParameter("q", query)
|
||||
|
||||
if (hhLangId != null)
|
||||
url.addQueryParameter("languages", hhLangId.toString())
|
||||
hhLangId.forEachIndexed() {index, it ->
|
||||
url.addQueryParameter("languages[${-index - 1}]", it.toString())
|
||||
}
|
||||
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
|
@ -130,12 +144,15 @@ abstract class HentaiHand(
|
|||
is AttributesGroupFilter -> filter.state.forEach {
|
||||
if (it.state) url.addQueryParameter("attributes", it.value)
|
||||
}
|
||||
is StatusGroupFilter -> filter.state.forEach {
|
||||
if (it.state) url.addQueryParameter("statuses", it.value)
|
||||
}
|
||||
is LookupFilter -> {
|
||||
filter.state.split(",").map { it.trim() }.filter { it.isNotBlank() }.map {
|
||||
lookupFilterId(it, filter.uri) ?: throw Exception("No ${filter.singularName} \"$it\" was found")
|
||||
}.forEach {
|
||||
if (!(filter.uri == "languages" && it == hhLangId))
|
||||
url.addQueryParameter(filter.uri, it.toString())
|
||||
}.forEachIndexed() {index, it ->
|
||||
if (!(filter.uri == "languages" && hhLangId.contains(it)))
|
||||
url.addQueryParameter(filter.uri + "[$index]", it.toString())
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
|
@ -163,19 +180,27 @@ abstract class HentaiHand(
|
|||
return SManga.create().apply {
|
||||
url = slugToUrl(obj)
|
||||
title = obj["title"]!!.jsonPrimitive.content
|
||||
thumbnail_url = obj["thumb_url"]!!.jsonPrimitive.content
|
||||
thumbnail_url = obj["image_url"]!!.jsonPrimitive.content
|
||||
artist = jsonArrayToString("artists", obj)
|
||||
author = jsonArrayToString("authors", obj) ?: artist
|
||||
genre = listOfNotNull(jsonArrayToString("tags", obj), jsonArrayToString("relationships", obj)).joinToString(", ")
|
||||
status = SManga.COMPLETED
|
||||
status = when (obj["status"]!!.jsonPrimitive.content) {
|
||||
"complete" -> SManga.COMPLETED
|
||||
"ongoing" -> SManga.ONGOING
|
||||
"onhold" -> SManga.ONGOING
|
||||
"canceled" -> SManga.COMPLETED
|
||||
else -> SManga.COMPLETED
|
||||
}
|
||||
|
||||
|
||||
|
||||
description = listOf(
|
||||
Pair("Alternative Title", obj["alternative_title"]!!.jsonPrimitive.content),
|
||||
Pair("Groups", jsonArrayToString("groups", obj)),
|
||||
Pair("Description", obj["description"]!!.jsonPrimitive.content),
|
||||
Pair("Pages", obj["pages"]!!.jsonPrimitive.content),
|
||||
Pair("Category", obj["category"]!!.jsonObject["name"]!!.jsonPrimitive.content),
|
||||
Pair("Language", obj["language"]!!.jsonObject["name"]!!.jsonPrimitive.content),
|
||||
Pair("Category", try { obj["category"]!!.jsonObject["name"]!!.jsonPrimitive.content } catch (_: Exception) { null }),
|
||||
Pair("Language", try { obj["language"]!!.jsonObject["name"]!!.jsonPrimitive.content } catch (_: Exception) { null }),
|
||||
Pair("Parodies", jsonArrayToString("parodies", obj)),
|
||||
Pair("Characters", jsonArrayToString("characters", obj))
|
||||
).filter { !it.second.isNullOrEmpty() }.joinToString("\n\n") { "${it.first}: ${it.second}" }
|
||||
|
@ -184,24 +209,59 @@ abstract class HentaiHand(
|
|||
|
||||
// Chapters
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request = mangaDetailsApiRequest(manga)
|
||||
private fun chapterListApiRequest(manga: SManga): Request {
|
||||
val slug = manga.url.removePrefix("/en/comic/")
|
||||
return if (chapters) {
|
||||
GET("$baseUrl/api/comics/$slug/chapters")
|
||||
} else {
|
||||
GET("$baseUrl/api/comics/$slug")
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request = chapterListApiRequest(manga)
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val obj = json.parseToJsonElement(response.body!!.string()).jsonObject
|
||||
return listOf(
|
||||
val slug = response.request.url.toString().substringAfter("/api/comics/").removeSuffix("/chapters")
|
||||
return if (chapters) {
|
||||
val array = json.parseToJsonElement(response.body!!.string()).jsonArray
|
||||
array.map {
|
||||
SChapter.create().apply {
|
||||
url = "/en/comic/${obj["slug"]!!.jsonPrimitive.content}/reader/1"
|
||||
url = "$slug/${it.jsonObject["slug"]!!.jsonPrimitive.content}"
|
||||
name = it.jsonObject["name"]!!.jsonPrimitive.content
|
||||
val date = it.jsonObject["added_at"]!!.jsonPrimitive.content
|
||||
date_upload = if (date.contains("day")) {
|
||||
Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, date.filter { it.isDigit() }.toInt() * -1)
|
||||
}.timeInMillis
|
||||
} else {
|
||||
DATE_FORMAT.parse(it.jsonObject["added_at"]!!.jsonPrimitive.content)?.time ?: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val obj = json.parseToJsonElement(response.body!!.string()).jsonObject
|
||||
listOf(
|
||||
SChapter.create().apply {
|
||||
url = obj["slug"]!!.jsonPrimitive.content
|
||||
name = "Chapter"
|
||||
date_upload = DATE_FORMAT.parse(obj["uploaded_at"]!!.jsonPrimitive.content)?.time ?: 0
|
||||
val date = obj.jsonObject["uploaded_at"]!!.jsonPrimitive.content
|
||||
date_upload = if (date.contains("day")) {
|
||||
Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, date.filter { it.isDigit() }.toInt() * -1)
|
||||
}.timeInMillis
|
||||
} else {
|
||||
DATE_FORMAT.parse(obj.jsonObject["uploaded_at"]!!.jsonPrimitive.content)?.time ?: 0
|
||||
}
|
||||
chapter_number = 1f
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Pages
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val slug = chapter.url.removePrefix("/en/comic/").removeSuffix("/reader/1")
|
||||
val slug = chapter.url
|
||||
return GET("$baseUrl/api/comics/$slug/images")
|
||||
}
|
||||
|
||||
|
@ -217,7 +277,7 @@ abstract class HentaiHand(
|
|||
|
||||
// Authorization
|
||||
|
||||
private fun authIntercept(chain: Interceptor.Chain): Response {
|
||||
protected fun authIntercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
if (username.isEmpty() or password.isEmpty()) {
|
||||
return chain.proceed(request)
|
||||
|
@ -264,12 +324,12 @@ abstract class HentaiHand(
|
|||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
screen.addPreference(screen.editTextPreference(USERNAME_TITLE, USERNAME_DEFAULT, username))
|
||||
screen.addPreference(screen.editTextPreference(PASSWORD_TITLE, PASSWORD_DEFAULT, password, true))
|
||||
}
|
||||
|
||||
private fun androidx.preference.PreferenceScreen.editTextPreference(title: String, default: String, value: String, isPassword: Boolean = false): androidx.preference.EditTextPreference {
|
||||
private fun PreferenceScreen.editTextPreference(title: String, default: String, value: String, isPassword: Boolean = false): androidx.preference.EditTextPreference {
|
||||
return androidx.preference.EditTextPreference(context).apply {
|
||||
key = title
|
||||
this.title = title
|
||||
|
@ -305,6 +365,8 @@ abstract class HentaiHand(
|
|||
private class DurationFilter(durationPairs: List<Pair<String, String>>) : Filter.Select<String>("Duration", durationPairs.map { it.first }.toTypedArray())
|
||||
private class AttributeFilter(name: String, val value: String) : Filter.CheckBox(name)
|
||||
private class AttributesGroupFilter(attributePairs: List<Pair<String, String>>) : Filter.Group<AttributeFilter>("Attributes", attributePairs.map { AttributeFilter(it.first, it.second) })
|
||||
private class StatusFilter(name: String, val value: String) : Filter.CheckBox(name)
|
||||
private class StatusGroupFilter(attributePairs: List<Pair<String, String>>) : Filter.Group<StatusFilter>("Status", attributePairs.map { StatusFilter(it.first, it.second) })
|
||||
|
||||
private class CategoriesFilter : LookupFilter("Categories", "categories", "category")
|
||||
private class TagsFilter : LookupFilter("Tags", "tags", "tag")
|
||||
|
@ -327,7 +389,8 @@ abstract class HentaiHand(
|
|||
CharactersFilter(),
|
||||
ParodiesFilter(),
|
||||
LanguagesFilter(),
|
||||
AttributesGroupFilter(getAttributePairs())
|
||||
AttributesGroupFilter(getAttributePairs()),
|
||||
StatusGroupFilter(getStatusPairs())
|
||||
)
|
||||
|
||||
private fun getSortPairs() = listOf(
|
||||
|
@ -357,8 +420,15 @@ abstract class HentaiHand(
|
|||
Pair("Rewritten", "rewritten")
|
||||
)
|
||||
|
||||
private fun getStatusPairs() = listOf(
|
||||
Pair("Ongoing", "ongoing"),
|
||||
Pair("Complete", "complete"),
|
||||
Pair("On Hold", "onhold"),
|
||||
Pair("Canceled", "canceled")
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val DATE_FORMAT = SimpleDateFormat("yyyy-dd-MM", Locale.US)
|
||||
private val DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||
private val MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
|
||||
private const val USERNAME_TITLE = "Username"
|
||||
private const val USERNAME_DEFAULT = ""
|
|
@ -0,0 +1,29 @@
|
|||
package eu.kanade.tachiyomi.multisrc.hentaihand
|
||||
|
||||
import generator.ThemeSourceData.MultiLang
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
class HentaiHandGenerator : ThemeSourceGenerator {
|
||||
|
||||
override val themePkg = "hentaihand"
|
||||
|
||||
override val themeClass = "HentaiHand"
|
||||
|
||||
override val baseVersionCode: Int = 1
|
||||
|
||||
override val sources = listOf(
|
||||
MultiLang("HentaiHand", "https://hentaihand.com", listOf("all", "ja", "en", "zh", "bg", "ceb", "other", "tl", "ar", "el", "sr", "jv", "uk", "tr", "fi", "la", "mn", "eo", "sk", "cs", "ko", "ru", "it", "es", "pt-BR", "th", "fr", "id", "vi", "de", "pl", "hu", "nl", "hi"), isNsfw = true, overrideVersionCode = 5),
|
||||
MultiLang("nHentai.com (unoriginal)", "https://nhentai.com", listOf("all", "ja", "en", "zh", "bg", "ceb", "other", "tl", "ar", "el", "sr", "jv", "uk", "tr", "fi", "la", "mn", "eo", "sk", "cs", "ko", "ru", "it", "es", "pt-BR", "th", "fr", "id", "vi", "de", "pl", "hu", "nl", "hi"), isNsfw = true, className = "NHentaiComFactory", overrideVersionCode = 4),
|
||||
SingleLang("HentaiSphere", "https://hentaisphere.com", "en", isNsfw = true),
|
||||
SingleLang("ManhwaClub", "https://manhwa.club", "en", isNsfw = true, overrideVersionCode = 3),
|
||||
SingleLang("ReadManhwa", "https://readmanhwa.com", "en", isNsfw = true, overrideVersionCode = 10),
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
HentaiHandGenerator().createAll()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="eu.kanade.tachiyomi.extension" />
|
|
@ -1,13 +0,0 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'HentaiHand'
|
||||
pkgNameSuffix = 'all.hentaihand'
|
||||
extClass = '.HentaiHandFactory'
|
||||
extVersionCode = 4
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
|
@ -1,91 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.all.hentaihand
|
||||
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
class HentaiHandFactory : SourceFactory {
|
||||
|
||||
override fun createSources(): List<Source> = listOf(
|
||||
// https://hentaihand.com/api/languages?per_page=50
|
||||
HentaiHandOther(),
|
||||
HentaiHandEn(),
|
||||
HentaiHandZh(),
|
||||
HentaiHandJa(),
|
||||
HentaiHandNoText(),
|
||||
HentaiHandEo(),
|
||||
HentaiHandCeb(),
|
||||
HentaiHandCs(),
|
||||
HentaiHandAr(),
|
||||
HentaiHandSk(),
|
||||
HentaiHandMn(),
|
||||
HentaiHandUk(),
|
||||
HentaiHandLa(),
|
||||
HentaiHandTl(),
|
||||
HentaiHandEs(),
|
||||
HentaiHandIt(),
|
||||
HentaiHandKo(),
|
||||
HentaiHandTh(),
|
||||
HentaiHandPl(),
|
||||
HentaiHandFr(),
|
||||
HentaiHandPtBr(),
|
||||
HentaiHandDe(),
|
||||
HentaiHandFi(),
|
||||
HentaiHandRu(),
|
||||
HentaiHandSv(),
|
||||
HentaiHandHu(),
|
||||
HentaiHandId(),
|
||||
HentaiHandVi(),
|
||||
HentaiHandDa(),
|
||||
HentaiHandRo(),
|
||||
HentaiHandEt(),
|
||||
HentaiHandNl(),
|
||||
HentaiHandCa(),
|
||||
HentaiHandTr(),
|
||||
HentaiHandEl(),
|
||||
HentaiHandNo(),
|
||||
HentaiHandSq(),
|
||||
HentaiHandBg(),
|
||||
)
|
||||
}
|
||||
|
||||
class HentaiHandOther : HentaiHand("all", extraName = " (Unfiltered)")
|
||||
class HentaiHandEn : HentaiHand("en", 1)
|
||||
class HentaiHandZh : HentaiHand("zh", 2)
|
||||
class HentaiHandJa : HentaiHand("ja", 3)
|
||||
class HentaiHandNoText : HentaiHand("other", 4, extraName = " (Text Cleaned)")
|
||||
class HentaiHandEo : HentaiHand("eo", 5)
|
||||
class HentaiHandCeb : HentaiHand("ceb", 6)
|
||||
class HentaiHandCs : HentaiHand("cs", 7)
|
||||
class HentaiHandAr : HentaiHand("ar", 8)
|
||||
class HentaiHandSk : HentaiHand("sk", 9)
|
||||
class HentaiHandMn : HentaiHand("mn", 10)
|
||||
class HentaiHandUk : HentaiHand("uk", 11)
|
||||
class HentaiHandLa : HentaiHand("la", 12)
|
||||
class HentaiHandTl : HentaiHand("tl", 13)
|
||||
class HentaiHandEs : HentaiHand("es", 14)
|
||||
class HentaiHandIt : HentaiHand("it", 15)
|
||||
class HentaiHandKo : HentaiHand("ko", 16)
|
||||
class HentaiHandTh : HentaiHand("th", 17)
|
||||
class HentaiHandPl : HentaiHand("pl", 18)
|
||||
class HentaiHandFr : HentaiHand("fr", 19)
|
||||
class HentaiHandPtBr : HentaiHand("pt-BR", 20) {
|
||||
// Hardcode the id because the language wasn't specific.
|
||||
override val id: Long = 2516244587139644000
|
||||
}
|
||||
class HentaiHandDe : HentaiHand("de", 21)
|
||||
class HentaiHandFi : HentaiHand("fi", 22)
|
||||
class HentaiHandRu : HentaiHand("ru", 23)
|
||||
class HentaiHandSv : HentaiHand("sv", 24)
|
||||
class HentaiHandHu : HentaiHand("hu", 25)
|
||||
class HentaiHandId : HentaiHand("id", 26)
|
||||
class HentaiHandVi : HentaiHand("vi", 27)
|
||||
class HentaiHandDa : HentaiHand("da", 28)
|
||||
class HentaiHandRo : HentaiHand("ro", 29)
|
||||
class HentaiHandEt : HentaiHand("et", 30)
|
||||
class HentaiHandNl : HentaiHand("nl", 31)
|
||||
class HentaiHandCa : HentaiHand("ca", 32)
|
||||
class HentaiHandTr : HentaiHand("tr", 33)
|
||||
class HentaiHandEl : HentaiHand("el", 34)
|
||||
class HentaiHandNo : HentaiHand("no", 35)
|
||||
class HentaiHandSq : HentaiHand("sq", 1501)
|
||||
class HentaiHandBg : HentaiHand("bg", 1502)
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="eu.kanade.tachiyomi.extension" />
|
|
@ -1,12 +0,0 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
extName = 'nHentai.com (unoriginal)'
|
||||
pkgNameSuffix = 'all.nhentaicom'
|
||||
extClass = '.NHentaiComFactory'
|
||||
extVersionCode = 3
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
|
@ -1,231 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.all.nhentaicom
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
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.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class NHentaiCom(override val lang: String) : HttpSource() {
|
||||
|
||||
override val name = when (lang) {
|
||||
"other" -> "nHentai.com (unoriginal)(Text Cleaned)"
|
||||
"all" -> "nHentai.com (unoriginal)(Unfiltered)"
|
||||
else -> "nHentai.com (unoriginal)"
|
||||
}
|
||||
|
||||
override val id = when (lang) {
|
||||
"en" -> 5591830863732393712
|
||||
"cs" -> 1144495813995437124
|
||||
else -> super.id
|
||||
}
|
||||
|
||||
override val baseUrl = "https://nhentai.com"
|
||||
|
||||
private val langId = toLangId(lang)
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private fun toLangId(langCode: String): String {
|
||||
return when (langCode) {
|
||||
"en" -> "1"
|
||||
"zh" -> "2"
|
||||
"ja" -> "3"
|
||||
"other" -> "4"
|
||||
"cs" -> "5"
|
||||
"ar" -> "6"
|
||||
"sk" -> "7"
|
||||
"eo" -> "8"
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||
.add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
|
||||
|
||||
private fun parseMangaFromJson(response: Response): MangasPage {
|
||||
val jsonRaw = response.body!!.string()
|
||||
val jsonResult = json.parseToJsonElement(jsonRaw).jsonObject
|
||||
|
||||
val mangas = jsonResult["data"]!!.jsonArray.map { jsonEl ->
|
||||
SManga.create().apply {
|
||||
val jsonObj = jsonEl.jsonObject
|
||||
title = jsonObj["title"]!!.jsonPrimitive.content
|
||||
thumbnail_url = jsonObj["image_url"]!!.jsonPrimitive.content
|
||||
url = jsonObj["slug"]!!.jsonPrimitive.content
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(mangas, jsonResult["current_page"]!!.jsonPrimitive.content.toInt() < jsonResult["last_page"]!!.jsonPrimitive.content.toInt())
|
||||
}
|
||||
|
||||
private fun getMangaUrl(page: Int, sort: String): String {
|
||||
val url = "$baseUrl/api/comics".toHttpUrlOrNull()!!.newBuilder()
|
||||
if (langId.isNotBlank()) {
|
||||
url.setQueryParameter("languages[]", langId)
|
||||
}
|
||||
url.setQueryParameter("page", "$page")
|
||||
url.setQueryParameter("sort", sort)
|
||||
url.setQueryParameter("nsfw", "false")
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
// Popular
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET(getMangaUrl(page, "popularity"), headers)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage = parseMangaFromJson(response)
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET(getMangaUrl(page, "uploaded_at"), headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage = parseMangaFromJson(response)
|
||||
|
||||
// Search
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = "$baseUrl/api/comics".toHttpUrlOrNull()!!.newBuilder()
|
||||
.addQueryParameter("per_page", "18")
|
||||
.addQueryParameter("page", page.toString())
|
||||
.addQueryParameter("q", query)
|
||||
.addQueryParameter("nsfw", "false")
|
||||
|
||||
if (langId.isNotBlank()) {
|
||||
url.setQueryParameter("languages[]", langId)
|
||||
}
|
||||
url.setQueryParameter("page", "$page")
|
||||
url.setQueryParameter("nsfw", "false")
|
||||
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is SortFilter -> url.addQueryParameter("sort", filter.toUriPart())
|
||||
is DurationFilter -> url.addQueryParameter("duration", filter.toUriPart())
|
||||
is SortOrderFilter -> url.addQueryParameter("order", filter.toUriPart())
|
||||
}
|
||||
}
|
||||
return GET(url.toString(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage = parseMangaFromJson(response)
|
||||
|
||||
// Details
|
||||
|
||||
// Workaround to allow "Open in browser" to use the real URL
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> =
|
||||
client.newCall(apiMangaDetailsRequest(manga)).asObservableSuccess()
|
||||
.map { mangaDetailsParse(it).apply { initialized = true } }
|
||||
|
||||
// Return the real URL for "Open in browser"
|
||||
override fun mangaDetailsRequest(manga: SManga) = GET("$baseUrl/en/comic/${manga.url}", headers)
|
||||
|
||||
private fun apiMangaDetailsRequest(manga: SManga): Request {
|
||||
return GET("$baseUrl/api/comics/${manga.url}", headers)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val jsonRaw = response.body!!.string()
|
||||
val jsonObject = json.parseToJsonElement(jsonRaw).jsonObject
|
||||
|
||||
return SManga.create().apply {
|
||||
description = jsonObject["description"]!!.jsonPrimitive.content
|
||||
status = SManga.COMPLETED
|
||||
thumbnail_url = jsonObject["image_url"]!!.jsonPrimitive.content
|
||||
genre = runCatching { jsonObject["tags"]!!.jsonArray.joinToString { it.jsonObject["name"]!!.jsonPrimitive.content } }.getOrNull()
|
||||
artist = runCatching { jsonObject["artists"]!!.jsonArray.joinToString { it.jsonObject["name"]!!.jsonPrimitive.content } }.getOrNull()
|
||||
author = runCatching { jsonObject["authors"]!!.jsonArray.joinToString { it.jsonObject["name"]!!.jsonPrimitive.content } }.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
// Chapters
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return Observable.just(
|
||||
listOf(
|
||||
SChapter.create().apply {
|
||||
name = "chapter"
|
||||
url = manga.url
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request = throw Exception("not used")
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> = throw UnsupportedOperationException("Not used")
|
||||
|
||||
// Pages
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
return GET("$baseUrl/api/comics/${chapter.url}/images", headers)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
return json.parseToJsonElement(response.body!!.string()).jsonObject["images"]!!.jsonArray.mapIndexed { i, jsonEl ->
|
||||
val jsonObj = jsonEl.jsonObject
|
||||
Page(i, "", jsonObj["source_url"]!!.jsonPrimitive.content)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not used")
|
||||
|
||||
// Filters
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
DurationFilter(getDurationList()),
|
||||
SortFilter(getSortList()),
|
||||
SortOrderFilter(getSortOrder())
|
||||
)
|
||||
|
||||
private class DurationFilter(pairs: Array<Pair<String, String>>) : UriPartFilter("Duration", pairs)
|
||||
|
||||
private class SortFilter(pairs: Array<Pair<String, String>>) : UriPartFilter("Sorted by", pairs)
|
||||
|
||||
private class SortOrderFilter(pairs: Array<Pair<String, String>>) : UriPartFilter("Order", pairs)
|
||||
|
||||
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
private fun getSortOrder() = arrayOf(
|
||||
Pair("Descending", "desc"),
|
||||
Pair("Ascending", "asc"),
|
||||
)
|
||||
|
||||
private fun getDurationList() = arrayOf(
|
||||
Pair("All time", "all"),
|
||||
Pair("Year", "year"),
|
||||
Pair("Month", "month"),
|
||||
Pair("Week", "week"),
|
||||
Pair("Day", "day")
|
||||
)
|
||||
|
||||
private fun getSortList() = arrayOf(
|
||||
Pair("Upload date", "uploaded_at"),
|
||||
Pair("Title", "title"),
|
||||
Pair("Pages", "pages"),
|
||||
Pair("Favorites", "favorites"),
|
||||
Pair("Popularity", "popularity"),
|
||||
)
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.all.nhentaicom
|
||||
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
class NHentaiComFactory : SourceFactory {
|
||||
override fun createSources(): List<Source> = languages.map { NHentaiCom(it) }
|
||||
}
|
||||
|
||||
private val languages = listOf(
|
||||
"all", "en", "zh", "ja", "other", "eo", "cs", "ar", "sk"
|
||||
)
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="eu.kanade.tachiyomi.extension" />
|
|
@ -1,13 +0,0 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'ReadManhwa'
|
||||
pkgNameSuffix = 'en.readmanhwa'
|
||||
extClass = '.ReadManhwa'
|
||||
extVersionCode = 9
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
|
@ -1,390 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.en.readmanhwa
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
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.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
class ReadManhwa : ConfigurableSource, HttpSource() {
|
||||
|
||||
override val name = "ReadManhwa"
|
||||
|
||||
override val baseUrl = "https://www.readmanhwa.com"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = headersBuilder(true)
|
||||
|
||||
private fun headersBuilder(enableNsfw: Boolean) = Headers.Builder()
|
||||
.add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
|
||||
.add("X-NSFW", enableNsfw.toString())
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private fun parseMangaFromJson(response: Response): MangasPage {
|
||||
val jsonObject = json.decodeFromString<JsonObject>(response.body!!.string())
|
||||
|
||||
val mangas = jsonObject["data"]!!.jsonArray.map { json ->
|
||||
SManga.create().apply {
|
||||
title = json.jsonObject["title"]!!.jsonPrimitive.content
|
||||
thumbnail_url = json.jsonObject["image_url"]!!.jsonPrimitive.content
|
||||
url = json.jsonObject["slug"]!!.jsonPrimitive.content
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(mangas, jsonObject["current_page"]!!.jsonPrimitive.int < jsonObject["last_page"]!!.jsonPrimitive.int)
|
||||
}
|
||||
private fun getMangaUrl(url: String): String {
|
||||
return url.toHttpUrlOrNull()!!.newBuilder()
|
||||
.setQueryParameter("nsfw", isNSFWEnabledInPref().toString()).toString()
|
||||
}
|
||||
|
||||
// Popular
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET(getMangaUrl("$baseUrl/api/comics?per_page=36&page=$page&q=&sort=popularity&order=desc&duration=all"), headers)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage = parseMangaFromJson(response)
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET(getMangaUrl("$baseUrl/api/comics?per_page=36&page=$page&q=&sort=uploaded_at&order=desc&duration=day"), headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage = parseMangaFromJson(response)
|
||||
|
||||
// Search
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val enableNsfw = (filters.find { it is NSFWFilter } as? Filter.CheckBox)?.state ?: true
|
||||
|
||||
val url = "$baseUrl/api/comics".toHttpUrlOrNull()!!.newBuilder()
|
||||
.addQueryParameter("per_page", "36")
|
||||
.addQueryParameter("page", page.toString())
|
||||
.addQueryParameter("q", query)
|
||||
.addQueryParameter("nsfw", enableNsfw.toString())
|
||||
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is GenreFilter -> {
|
||||
|
||||
val genreInclude = mutableListOf<String>()
|
||||
filter.state.forEach {
|
||||
if (it.state == 1) {
|
||||
genreInclude.add(it.id)
|
||||
}
|
||||
}
|
||||
if (genreInclude.isNotEmpty()) {
|
||||
genreInclude.forEach { genre ->
|
||||
url.addQueryParameter("tags[]", genre)
|
||||
}
|
||||
}
|
||||
}
|
||||
is StatusFilter -> {
|
||||
val statusInclude = mutableListOf<String>()
|
||||
filter.state.forEach {
|
||||
if (it.state == 1) {
|
||||
statusInclude.add(it.id)
|
||||
}
|
||||
}
|
||||
if (statusInclude.isNotEmpty()) {
|
||||
statusInclude.forEach { status ->
|
||||
url.addQueryParameter("statuses[]", status)
|
||||
}
|
||||
}
|
||||
}
|
||||
is OrderBy -> {
|
||||
val orderby = if (filter.state!!.ascending) "asc" else "desc"
|
||||
val sort = arrayOf("uploaded_at", "title", "pages", "favorites", "popularity")[filter.state!!.index]
|
||||
url.addQueryParameter("sort", sort)
|
||||
url.addQueryParameter("order", orderby)
|
||||
}
|
||||
is DurationFilter -> url.addQueryParameter("duration", filter.toUriPart())
|
||||
}
|
||||
}
|
||||
return GET(url.toString(), headersBuilder(enableNsfw).build())
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage = parseMangaFromJson(response)
|
||||
|
||||
// Details
|
||||
|
||||
// Workaround to allow "Open in browser" to use the real URL
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> =
|
||||
client.newCall(apiMangaDetailsRequest(manga)).asObservableSuccess()
|
||||
.map { mangaDetailsParse(it).apply { initialized = true } }
|
||||
|
||||
// Return the real URL for "Open in browser"
|
||||
override fun mangaDetailsRequest(manga: SManga) = GET(getMangaUrl("$baseUrl/en/webtoon/${manga.url}"), headers)
|
||||
|
||||
private fun apiMangaDetailsRequest(manga: SManga): Request {
|
||||
return GET(getMangaUrl("$baseUrl/api/comics/${manga.url}"), headers)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val jsonObject = json.decodeFromString<JsonObject>(response.body!!.string())
|
||||
|
||||
return SManga.create().apply {
|
||||
description = jsonObject["description"]!!.jsonPrimitive.contentOrNull
|
||||
status = jsonObject["status"]!!.jsonPrimitive.contentOrNull.toStatus()
|
||||
thumbnail_url = jsonObject["image_url"]!!.jsonPrimitive.contentOrNull
|
||||
genre = try { jsonObject["tags"]!!.jsonArray.joinToString { it.jsonObject["name"]!!.jsonPrimitive.content } } catch (_: Exception) { null }
|
||||
artist = try { jsonObject["artists"]!!.jsonArray.joinToString { it.jsonObject["name"]!!.jsonPrimitive.content } } catch (_: Exception) { null }
|
||||
author = try { jsonObject["authors"]!!.jsonArray.joinToString { it.jsonObject["name"]!!.jsonPrimitive.content } } catch (_: Exception) { null }
|
||||
}
|
||||
}
|
||||
|
||||
private fun String?.toStatus() = when {
|
||||
this == null -> SManga.UNKNOWN
|
||||
this.contains("ongoing", ignoreCase = true) -> SManga.ONGOING
|
||||
this.contains("complete", ignoreCase = true) -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
// Chapters
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return client.newCall(chapterListRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
chapterListParse(response, manga.url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
return GET(getMangaUrl("$baseUrl/api/comics/${manga.url}/chapters"), headers)
|
||||
}
|
||||
|
||||
private fun chapterListParse(response: Response, titleSlug: String): List<SChapter> {
|
||||
return json.decodeFromString<JsonArray>(response.body!!.string()).map { json ->
|
||||
SChapter.create().apply {
|
||||
name = json.jsonObject["name"]!!.jsonPrimitive.content
|
||||
url = "$titleSlug/${json.jsonObject["slug"]!!.jsonPrimitive.content}"
|
||||
date_upload = json.jsonObject["added_at"]!!.jsonPrimitive.content.let { dateString ->
|
||||
if (dateString.contains("ago")) {
|
||||
val trimmedDate = dateString.substringBefore(" ago").removeSuffix("s").split(" ")
|
||||
val calendar = Calendar.getInstance()
|
||||
when (trimmedDate[1]) {
|
||||
"day" -> calendar.apply { add(Calendar.DAY_OF_MONTH, -trimmedDate[0].toInt()) }.timeInMillis
|
||||
"hour" -> calendar.apply { add(Calendar.HOUR_OF_DAY, -trimmedDate[0].toInt()) }.timeInMillis
|
||||
"minute" -> calendar.apply { add(Calendar.MINUTE, -trimmedDate[0].toInt()) }.timeInMillis
|
||||
"second" -> calendar.apply { add(Calendar.SECOND, -trimmedDate[0].toInt()) }.timeInMillis
|
||||
else -> 0L
|
||||
}
|
||||
} else {
|
||||
SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(dateString)?.time ?: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> = throw UnsupportedOperationException("Not used")
|
||||
|
||||
// Pages
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
return GET(getMangaUrl("$baseUrl/api/comics/${chapter.url}/images"), headers)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
return json.decodeFromString<JsonObject>(response.body!!.string())["images"]!!.jsonArray.mapIndexed { i, json ->
|
||||
Page(i, "", json.jsonObject["source_url"]!!.jsonPrimitive.content)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not used")
|
||||
|
||||
// Filters
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
NSFWFilter().apply { state = isNSFWEnabledInPref() },
|
||||
GenreFilter(getGenreList()),
|
||||
StatusFilter(getStatusList()),
|
||||
DurationFilter(getDurationList()),
|
||||
OrderBy()
|
||||
)
|
||||
|
||||
private class NSFWFilter : Filter.CheckBox("Show NSFW", true)
|
||||
private class Genre(name: String, val id: String = name) : Filter.TriState(name)
|
||||
private class GenreFilter(genres: List<Genre>) : Filter.Group<Genre>("GENRES", genres)
|
||||
private class Status(name: String, val id: String = name) : Filter.TriState(name)
|
||||
private class StatusFilter(status: List<Status>) : Filter.Group<Status>("STATUS", status)
|
||||
private class DurationFilter(pairs: Array<Pair<String, String>>) : UriPartFilter("DURATION", pairs)
|
||||
|
||||
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
private fun getGenreList() = listOf(
|
||||
Genre("Action", "14"),
|
||||
Genre("Adventure", "6"),
|
||||
Genre("All Ages", "73"),
|
||||
Genre("Angst", "50"),
|
||||
Genre("BL", "20"),
|
||||
Genre("Boxing", "58"),
|
||||
Genre("College", "82"),
|
||||
Genre("Comedy", "1"),
|
||||
Genre("Comic", "70"),
|
||||
Genre("Completed", "53"),
|
||||
Genre("Cooking", "67"),
|
||||
Genre("Crime", "18"),
|
||||
Genre("Cultivation", "37"),
|
||||
Genre("Demons", "65"),
|
||||
Genre("Drama", "2"),
|
||||
Genre("Ecchi", "46"),
|
||||
Genre("Fantasy", "8"),
|
||||
Genre("Gender Bender", "35"),
|
||||
Genre("GL", "42"),
|
||||
Genre("Goshiwon", "80"),
|
||||
Genre("Gossip", "12"),
|
||||
Genre("Harem", "7"),
|
||||
Genre("Historical", "33"),
|
||||
Genre("Horror", "19"),
|
||||
Genre("Incest", "10"),
|
||||
Genre("Isekai", "28"),
|
||||
Genre("Josei", "48"),
|
||||
Genre("Long Strip", "78"),
|
||||
Genre("M", "43"),
|
||||
Genre("Magic", "59"),
|
||||
Genre("Magical", "69"),
|
||||
Genre("Magical Girls", "77"),
|
||||
Genre("Manga", "56"),
|
||||
Genre("Manhua", "38"),
|
||||
Genre("Manhwa", "40"),
|
||||
Genre("Manhwa18", "81"),
|
||||
Genre("Martial arts", "26"),
|
||||
Genre("Mature", "30"),
|
||||
Genre("Mecha", "54"),
|
||||
Genre("Medical", "24"),
|
||||
Genre("Moder", "64"),
|
||||
Genre("Modern", "51"),
|
||||
Genre("Monster/Tentacle", "57"),
|
||||
Genre("Music", "75"),
|
||||
Genre("Mystery", "15"),
|
||||
Genre("NTR", "32"),
|
||||
Genre("Office", "84"),
|
||||
Genre("Office Life", "79"),
|
||||
Genre("One shot", "61"),
|
||||
Genre("Philosophical", "44"),
|
||||
Genre("Post Apocalyptic", "49"),
|
||||
Genre("Psychological", "16"),
|
||||
Genre("Revenge", "74"),
|
||||
Genre("Reverse harem", "72"),
|
||||
Genre("Romance", "3"),
|
||||
Genre("Rpg", "41"),
|
||||
Genre("School LIfe", "11"),
|
||||
Genre("Sci Fi", "9"),
|
||||
Genre("Seinen", "31"),
|
||||
Genre("Shoujo", "36"),
|
||||
Genre("Shoujo Ai", "62"),
|
||||
Genre("Shounen", "29"),
|
||||
Genre("Shounen Ai", "63"),
|
||||
Genre("Slice of Life", "4"),
|
||||
Genre("Smut", "13"),
|
||||
Genre("Sports", "5"),
|
||||
Genre("Super power", "71"),
|
||||
Genre("Superhero", "45"),
|
||||
Genre("Supernatural", "22"),
|
||||
Genre("Suspense", "47"),
|
||||
Genre("Thriller", "17"),
|
||||
Genre("Time Travel", "55"),
|
||||
Genre("TimeTravel", "52"),
|
||||
Genre("ToonPoint", "83"),
|
||||
Genre("Tragedy", "23"),
|
||||
Genre("Uncensored", "85"),
|
||||
Genre("Vampire", "68"),
|
||||
Genre("Vanilla", "34"),
|
||||
Genre("Web Comic", "76"),
|
||||
Genre("Webtoon", "39"),
|
||||
Genre("Webtoons", "60"),
|
||||
Genre("Yaoi", "21"),
|
||||
Genre("Youkai", "66"),
|
||||
Genre("Yuri", "25")
|
||||
)
|
||||
|
||||
private fun getStatusList() = listOf(
|
||||
Status("Ongoing", "ongoing"),
|
||||
Status("Complete", "complete"),
|
||||
Status("On Hold", "onhold"),
|
||||
Status("Canceled", "canceled")
|
||||
)
|
||||
|
||||
private fun getDurationList() = arrayOf(
|
||||
Pair("All time", "all"),
|
||||
Pair("Year", "year"),
|
||||
Pair("Month", "month"),
|
||||
Pair("Week", "week"),
|
||||
Pair("Day", "day")
|
||||
)
|
||||
|
||||
private class OrderBy : Filter.Sort(
|
||||
"Order by",
|
||||
arrayOf("Date", "Title", "Pages", "Favorites", "Popularity"),
|
||||
Selection(0, false)
|
||||
)
|
||||
|
||||
override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
|
||||
val nsfw = androidx.preference.CheckBoxPreference(screen.context).apply {
|
||||
key = NSFW
|
||||
title = NSFW_TITLE
|
||||
setDefaultValue(NSFW_DEFAULT)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as Boolean
|
||||
preferences.edit().putBoolean(NSFW, selected).commit()
|
||||
}
|
||||
}
|
||||
screen.addPreference(nsfw)
|
||||
}
|
||||
|
||||
private fun isNSFWEnabledInPref(): Boolean {
|
||||
return preferences.getBoolean(NSFW, NSFW_DEFAULT)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NSFW = "NSFW"
|
||||
private const val NSFW_TITLE = "Show NSFW"
|
||||
private const val NSFW_DEFAULT = true
|
||||
}
|
||||
}
|