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
This commit is contained in:
jopejoe1 2022-02-04 00:07:16 +13:00 committed by GitHub
parent 20d7e522c7
commit f4928ebd7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 364 additions and 807 deletions

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -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))

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View File

@ -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()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -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()
}

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

View File

@ -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))

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -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()
}

View File

@ -1,9 +1,10 @@
package eu.kanade.tachiyomi.extension.all.hentaihand package eu.kanade.tachiyomi.multisrc.hentaihand
import android.app.Application import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import android.text.InputType import android.text.InputType
import android.widget.Toast import android.widget.Toast
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
@ -20,13 +21,13 @@ import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.int import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonNull
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
@ -36,22 +37,19 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale import java.util.Locale
abstract class HentaiHand( abstract class HentaiHand(
override val name: String,
override val baseUrl: String,
override val lang: String, override val lang: String,
private val hhLangId: Int? = null, private val chapters: Boolean,
extraName: String = "" private val hhLangId: List<Int> = emptyList(),
) : ConfigurableSource, HttpSource() { ) : ConfigurableSource, HttpSource() {
override val baseUrl: String = "https://hentaihand.com"
override val name: String = "HentaiHand$extraName"
override val supportsLatest = true override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.addInterceptor { authIntercept(it) }
.build()
private val json: Json by injectLazy() private val json: Json by injectLazy()
private fun slugToUrl(json: JsonObject) = json["slug"]!!.jsonPrimitive.content.prependIndent("/en/comic/") private fun slugToUrl(json: JsonObject) = json["slug"]!!.jsonPrimitive.content.prependIndent("/en/comic/")
@ -73,7 +71,7 @@ abstract class HentaiHand(
SManga.create().apply { SManga.create().apply {
url = slugToUrl(obj) url = slugToUrl(obj)
title = obj["title"]!!.jsonPrimitive.content 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() val hasNextPage = jsonResponse.jsonObject["next_page_url"]!!.jsonPrimitive.content.isNotEmpty()
@ -81,8 +79,16 @@ abstract class HentaiHand(
} }
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
val url = "$baseUrl/api/comics?page=$page&sort=popularity&order=desc&duration=all" val url = "$baseUrl/api/comics".toHttpUrlOrNull()!!.newBuilder()
return GET(if (hhLangId == null) url else ("$url&languages=$hhLangId")) .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 // Latest
@ -90,8 +96,15 @@ abstract class HentaiHand(
override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response) override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request {
val url = "$baseUrl/api/comics?page=$page&sort=uploaded_at&order=desc&duration=week" val url = "$baseUrl/api/comics".toHttpUrlOrNull()!!.newBuilder()
return GET(if (hhLangId == null) url else ("$url&languages=$hhLangId")) .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 // Search
@ -119,8 +132,9 @@ abstract class HentaiHand(
.addQueryParameter("page", page.toString()) .addQueryParameter("page", page.toString())
.addQueryParameter("q", query) .addQueryParameter("q", query)
if (hhLangId != null) hhLangId.forEachIndexed() {index, it ->
url.addQueryParameter("languages", hhLangId.toString()) url.addQueryParameter("languages[${-index - 1}]", it.toString())
}
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter -> (if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
when (filter) { when (filter) {
@ -130,12 +144,15 @@ abstract class HentaiHand(
is AttributesGroupFilter -> filter.state.forEach { is AttributesGroupFilter -> filter.state.forEach {
if (it.state) url.addQueryParameter("attributes", it.value) if (it.state) url.addQueryParameter("attributes", it.value)
} }
is StatusGroupFilter -> filter.state.forEach {
if (it.state) url.addQueryParameter("statuses", it.value)
}
is LookupFilter -> { is LookupFilter -> {
filter.state.split(",").map { it.trim() }.filter { it.isNotBlank() }.map { filter.state.split(",").map { it.trim() }.filter { it.isNotBlank() }.map {
lookupFilterId(it, filter.uri) ?: throw Exception("No ${filter.singularName} \"$it\" was found") lookupFilterId(it, filter.uri) ?: throw Exception("No ${filter.singularName} \"$it\" was found")
}.forEach { }.forEachIndexed() {index, it ->
if (!(filter.uri == "languages" && it == hhLangId)) if (!(filter.uri == "languages" && hhLangId.contains(it)))
url.addQueryParameter(filter.uri, it.toString()) url.addQueryParameter(filter.uri + "[$index]", it.toString())
} }
} }
else -> {} else -> {}
@ -163,19 +180,27 @@ abstract class HentaiHand(
return SManga.create().apply { return SManga.create().apply {
url = slugToUrl(obj) url = slugToUrl(obj)
title = obj["title"]!!.jsonPrimitive.content title = obj["title"]!!.jsonPrimitive.content
thumbnail_url = obj["thumb_url"]!!.jsonPrimitive.content thumbnail_url = obj["image_url"]!!.jsonPrimitive.content
artist = jsonArrayToString("artists", obj) artist = jsonArrayToString("artists", obj)
author = jsonArrayToString("authors", obj) ?: artist author = jsonArrayToString("authors", obj) ?: artist
genre = listOfNotNull(jsonArrayToString("tags", obj), jsonArrayToString("relationships", obj)).joinToString(", ") 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( description = listOf(
Pair("Alternative Title", obj["alternative_title"]!!.jsonPrimitive.content), Pair("Alternative Title", obj["alternative_title"]!!.jsonPrimitive.content),
Pair("Groups", jsonArrayToString("groups", obj)), Pair("Groups", jsonArrayToString("groups", obj)),
Pair("Description", obj["description"]!!.jsonPrimitive.content), Pair("Description", obj["description"]!!.jsonPrimitive.content),
Pair("Pages", obj["pages"]!!.jsonPrimitive.content), Pair("Pages", obj["pages"]!!.jsonPrimitive.content),
Pair("Category", obj["category"]!!.jsonObject["name"]!!.jsonPrimitive.content), Pair("Category", try { obj["category"]!!.jsonObject["name"]!!.jsonPrimitive.content } catch (_: Exception) { null }),
Pair("Language", obj["language"]!!.jsonObject["name"]!!.jsonPrimitive.content), Pair("Language", try { obj["language"]!!.jsonObject["name"]!!.jsonPrimitive.content } catch (_: Exception) { null }),
Pair("Parodies", jsonArrayToString("parodies", obj)), Pair("Parodies", jsonArrayToString("parodies", obj)),
Pair("Characters", jsonArrayToString("characters", obj)) Pair("Characters", jsonArrayToString("characters", obj))
).filter { !it.second.isNullOrEmpty() }.joinToString("\n\n") { "${it.first}: ${it.second}" } ).filter { !it.second.isNullOrEmpty() }.joinToString("\n\n") { "${it.first}: ${it.second}" }
@ -184,24 +209,59 @@ abstract class HentaiHand(
// Chapters // 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> { override fun chapterListParse(response: Response): List<SChapter> {
val obj = json.parseToJsonElement(response.body!!.string()).jsonObject val slug = response.request.url.toString().substringAfter("/api/comics/").removeSuffix("/chapters")
return listOf( return if (chapters) {
SChapter.create().apply { val array = json.parseToJsonElement(response.body!!.string()).jsonArray
url = "/en/comic/${obj["slug"]!!.jsonPrimitive.content}/reader/1" array.map {
name = "Chapter" SChapter.create().apply {
date_upload = DATE_FORMAT.parse(obj["uploaded_at"]!!.jsonPrimitive.content)?.time ?: 0 url = "$slug/${it.jsonObject["slug"]!!.jsonPrimitive.content}"
chapter_number = 1f 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"
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 // Pages
override fun pageListRequest(chapter: SChapter): Request { 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") return GET("$baseUrl/api/comics/$slug/images")
} }
@ -217,7 +277,7 @@ abstract class HentaiHand(
// Authorization // Authorization
private fun authIntercept(chain: Interceptor.Chain): Response { protected fun authIntercept(chain: Interceptor.Chain): Response {
val request = chain.request() val request = chain.request()
if (username.isEmpty() or password.isEmpty()) { if (username.isEmpty() or password.isEmpty()) {
return chain.proceed(request) return chain.proceed(request)
@ -264,12 +324,12 @@ abstract class HentaiHand(
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) 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(USERNAME_TITLE, USERNAME_DEFAULT, username))
screen.addPreference(screen.editTextPreference(PASSWORD_TITLE, PASSWORD_DEFAULT, password, true)) 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 { return androidx.preference.EditTextPreference(context).apply {
key = title key = title
this.title = 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 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 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 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 CategoriesFilter : LookupFilter("Categories", "categories", "category")
private class TagsFilter : LookupFilter("Tags", "tags", "tag") private class TagsFilter : LookupFilter("Tags", "tags", "tag")
@ -327,7 +389,8 @@ abstract class HentaiHand(
CharactersFilter(), CharactersFilter(),
ParodiesFilter(), ParodiesFilter(),
LanguagesFilter(), LanguagesFilter(),
AttributesGroupFilter(getAttributePairs()) AttributesGroupFilter(getAttributePairs()),
StatusGroupFilter(getStatusPairs())
) )
private fun getSortPairs() = listOf( private fun getSortPairs() = listOf(
@ -357,8 +420,15 @@ abstract class HentaiHand(
Pair("Rewritten", "rewritten") Pair("Rewritten", "rewritten")
) )
private fun getStatusPairs() = listOf(
Pair("Ongoing", "ongoing"),
Pair("Complete", "complete"),
Pair("On Hold", "onhold"),
Pair("Canceled", "canceled")
)
companion object { 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 val MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
private const val USERNAME_TITLE = "Username" private const val USERNAME_TITLE = "Username"
private const val USERNAME_DEFAULT = "" private const val USERNAME_DEFAULT = ""

View File

@ -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()
}
}
}

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" />

View File

@ -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"

View File

@ -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)

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" />

View File

@ -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"

View File

@ -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"),
)
}

View File

@ -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"
)

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" />

View File

@ -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"

View File

@ -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
}
}