[Hentalk] Add source (#1665)
parent
a5c70f0b51
commit
826c948260
@ -1 +1 @@
|
|||||||
total: 1199
|
total: 1200
|
||||||
@ -0,0 +1,337 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.en
|
||||||
|
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.jsoup.HttpStatusException
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.*
|
||||||
|
import org.koitharu.kotatsu.parsers.core.LegacyPagedMangaParser
|
||||||
|
import org.koitharu.kotatsu.parsers.exception.ParseException
|
||||||
|
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||||
|
import org.koitharu.kotatsu.parsers.network.UserAgents
|
||||||
|
import org.koitharu.kotatsu.parsers.util.*
|
||||||
|
import org.koitharu.kotatsu.parsers.util.json.*
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
private const val SERVER_DATA_SAVER = "?type="
|
||||||
|
private const val SERVER_DATA = ""
|
||||||
|
|
||||||
|
@MangaSourceParser("HENTALK", "Hentalk", "en", type = ContentType.HENTAI)
|
||||||
|
internal class Hentalk(context: MangaLoaderContext) :
|
||||||
|
LegacyPagedMangaParser(context, MangaParserSource.HENTALK, 24) {
|
||||||
|
|
||||||
|
override val configKeyDomain = ConfigKey.Domain("hentalk.pw")
|
||||||
|
override val userAgentKey = ConfigKey.UserAgent(UserAgents.KOTATSU)
|
||||||
|
|
||||||
|
private val preferredServerKey = ConfigKey.PreferredImageServer(
|
||||||
|
presetValues = mapOf(
|
||||||
|
SERVER_DATA to "Original quality",
|
||||||
|
SERVER_DATA_SAVER to "Compressed quality",
|
||||||
|
),
|
||||||
|
defaultValue = SERVER_DATA,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
|
||||||
|
super.onCreateConfig(keys)
|
||||||
|
keys.add(userAgentKey)
|
||||||
|
keys.add(preferredServerKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
||||||
|
SortOrder.UPDATED,
|
||||||
|
SortOrder.NEWEST,
|
||||||
|
SortOrder.NEWEST_ASC,
|
||||||
|
SortOrder.ALPHABETICAL,
|
||||||
|
SortOrder.ALPHABETICAL_DESC,
|
||||||
|
)
|
||||||
|
|
||||||
|
override val filterCapabilities: MangaListFilterCapabilities
|
||||||
|
get() = MangaListFilterCapabilities(
|
||||||
|
isSearchSupported = true,
|
||||||
|
isMultipleTagsSupported = true,
|
||||||
|
isSearchWithFiltersSupported = true,
|
||||||
|
isAuthorSearchSupported = true
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getFilterOptions(): MangaListFilterOptions {
|
||||||
|
return MangaListFilterOptions( availableTags = emptySet() ) // not found any URLs for it
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
|
||||||
|
val url = buildString {
|
||||||
|
append("https://")
|
||||||
|
append(domain)
|
||||||
|
append("/__data.json?x-sveltekit-trailing-slash=1&x-sveltekit-invalidated=001")
|
||||||
|
|
||||||
|
when {
|
||||||
|
!filter.query.isNullOrEmpty() || filter.tags.isNotEmpty() || !filter.author.isNullOrEmpty() -> {
|
||||||
|
append("&q=")
|
||||||
|
|
||||||
|
if (!filter.author.isNullOrEmpty()) {
|
||||||
|
append("artist:\"${space2plus(filter.author)}\"")
|
||||||
|
append("+")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.tags.isNotEmpty()) {
|
||||||
|
filter.tags.forEach { tag ->
|
||||||
|
append("tag:\"${space2plus(tag.key)}\"")
|
||||||
|
append("+")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filter.query.isNullOrEmpty()) {
|
||||||
|
append(space2plus(filter.query))
|
||||||
|
} else {
|
||||||
|
append("+")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (order) {
|
||||||
|
SortOrder.UPDATED -> append("&sort=released_at")
|
||||||
|
SortOrder.NEWEST_ASC -> append("&sort=created_at&order=asc")
|
||||||
|
SortOrder.NEWEST -> append("&sort=created_at&order=desc")
|
||||||
|
SortOrder.ALPHABETICAL -> append("&sort=title&order=asc")
|
||||||
|
SortOrder.ALPHABETICAL_DESC -> append("&sort=title&order=desc")
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page > 1) {
|
||||||
|
append("&page=")
|
||||||
|
append(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val json = try {
|
||||||
|
webClient.httpGet(url).parseJson()
|
||||||
|
} catch (e: HttpStatusException) {
|
||||||
|
if (e.statusCode == HttpURLConnection.HTTP_INTERNAL_ERROR) {
|
||||||
|
return emptyList()
|
||||||
|
} else {
|
||||||
|
throw ParseException("Can't get data from source!", url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val mangaList = mutableListOf<Manga>()
|
||||||
|
val dataValues = mutableMapOf<Int, Any>()
|
||||||
|
|
||||||
|
val dataArray = json.getJSONArray("nodes")
|
||||||
|
.optJSONObject(2)
|
||||||
|
?.optJSONArray("data")
|
||||||
|
?: return emptyList()
|
||||||
|
|
||||||
|
for (i in 0 until dataArray.length()) {
|
||||||
|
dataValues[i] = dataArray.get(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
val archiveH = mutableListOf<Int>()
|
||||||
|
for (i in 0 until dataArray.length()) {
|
||||||
|
val item = dataArray.opt(i)
|
||||||
|
if (item is JSONObject && item.has("id") && item.has("hash") &&
|
||||||
|
item.has("title") && item.has("thumbnail") && item.has("tags")) {
|
||||||
|
archiveH.add(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (tempIndex in archiveH) {
|
||||||
|
val temp = dataArray.getJSONObject(tempIndex)
|
||||||
|
val idRef = temp.getInt("id")
|
||||||
|
val hashRef = temp.getInt("hash")
|
||||||
|
val titleRef = temp.getInt("title")
|
||||||
|
val thumbnailRef = temp.getInt("thumbnail")
|
||||||
|
val tagsRef = temp.getInt("tags")
|
||||||
|
|
||||||
|
val mangaId = dataArray.getLong(idRef)
|
||||||
|
|
||||||
|
val key = dataArray.getString(hashRef)
|
||||||
|
val title = dataArray.getString(titleRef)
|
||||||
|
val idThumbnail = dataArray.getInt(thumbnailRef)
|
||||||
|
|
||||||
|
val tagsList = dataArray.optJSONArray(tagsRef)
|
||||||
|
val tags = mutableSetOf<MangaTag>()
|
||||||
|
var author: String? = null
|
||||||
|
|
||||||
|
if (tagsList != null) {
|
||||||
|
var i = 0
|
||||||
|
while (i < tagsList.length()) {
|
||||||
|
val tagRefIndex = tagsList.getInt(i)
|
||||||
|
|
||||||
|
if (dataValues.containsKey(tagRefIndex) &&
|
||||||
|
dataValues[tagRefIndex] is JSONObject &&
|
||||||
|
(dataValues[tagRefIndex] as JSONObject).has("namespace")) {
|
||||||
|
|
||||||
|
val nsObj = dataValues[tagRefIndex] as JSONObject
|
||||||
|
val nsIndex = nsObj.getInt("namespace")
|
||||||
|
val nameIndex = nsObj.getInt("name")
|
||||||
|
|
||||||
|
val nsValue = if (dataValues.containsKey(nsIndex)) dataValues[nsIndex].toString() else ""
|
||||||
|
val nameValue = if (dataValues.containsKey(nameIndex)) dataValues[nameIndex].toString() else ""
|
||||||
|
|
||||||
|
if (nsValue == "artist") {
|
||||||
|
author = nameValue
|
||||||
|
} else if (nsValue == "tag") {
|
||||||
|
tags.add(MangaTag(
|
||||||
|
key = nameValue,
|
||||||
|
title = nameValue,
|
||||||
|
source = source
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mangaList.add(Manga(
|
||||||
|
id = generateUid(mangaId),
|
||||||
|
url = "/g/$mangaId/__data.json?x-sveltekit-invalidated=001",
|
||||||
|
publicUrl = "https://$domain/g/$mangaId",
|
||||||
|
title = title,
|
||||||
|
altTitles = emptySet(),
|
||||||
|
coverUrl = "https://$domain/image/$key/$idThumbnail?type=cover",
|
||||||
|
largeCoverUrl = null,
|
||||||
|
authors = setOfNotNull(author),
|
||||||
|
tags = tags,
|
||||||
|
state = null,
|
||||||
|
description = null,
|
||||||
|
contentRating = ContentRating.ADULT,
|
||||||
|
source = source,
|
||||||
|
rating = RATING_UNKNOWN,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
return mangaList
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getDetails(manga: Manga): Manga {
|
||||||
|
val json = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseJson()
|
||||||
|
val mangaId = manga.url.substringAfter("/g/").substringBefore("/")
|
||||||
|
|
||||||
|
val dataArray = json.getJSONArray("nodes")
|
||||||
|
.optJSONObject(2)
|
||||||
|
?.optJSONArray("data")
|
||||||
|
?: return manga.copy()
|
||||||
|
|
||||||
|
var createdAt = ""
|
||||||
|
|
||||||
|
for (i in 0 until dataArray.length()) {
|
||||||
|
val item = dataArray.opt(i)
|
||||||
|
if (item is JSONObject && item.has("createdAt")) {
|
||||||
|
val addedAt = item.getInt("createdAt")
|
||||||
|
if (dataArray.length() > addedAt) {
|
||||||
|
createdAt = dataArray.optString(addedAt, "")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
|
||||||
|
val parseTime = dateFormat.tryParse(createdAt)
|
||||||
|
val chapter = MangaChapter(
|
||||||
|
id = generateUid("/g/$mangaId/read/1"),
|
||||||
|
url = "/g/$mangaId/read/1/__data.json?x-sveltekit-invalidated=011",
|
||||||
|
title = "Oneshot", // for all, just has 1 chapter
|
||||||
|
number = 0f,
|
||||||
|
uploadDate = parseTime,
|
||||||
|
volume = 0,
|
||||||
|
branch = null,
|
||||||
|
scanlator = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
|
||||||
|
return manga.copy(
|
||||||
|
chapters = listOf(chapter)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||||
|
val json = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseJson()
|
||||||
|
val dataArray = json.getJSONArray("nodes")
|
||||||
|
.optJSONObject(2)
|
||||||
|
?.optJSONArray("data")
|
||||||
|
?: return emptyList()
|
||||||
|
|
||||||
|
var compressID = ""
|
||||||
|
for (i in 0 until dataArray.length()) {
|
||||||
|
val item = dataArray.opt(i)
|
||||||
|
if (item is JSONObject && item.has("hash")) {
|
||||||
|
if (i < 20) { // search in first 20 items
|
||||||
|
val hashValue = dataArray.getString(item.getInt("hash"))
|
||||||
|
if (hashValue.length == 8) { // hash is a key that has 8 chars
|
||||||
|
compressID = hashValue
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
throw ParseException("Can't find type ID in this chapter!", chapter.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var hashID = ""
|
||||||
|
for (i in 0 until dataArray.length()) {
|
||||||
|
val item = dataArray.opt(i)
|
||||||
|
if (item is JSONObject && item.has("hash") && item.has("id")) {
|
||||||
|
val hashIndex = item.getInt("hash")
|
||||||
|
hashID = dataArray.getString(hashIndex)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hashID.isEmpty()) {
|
||||||
|
for (i in 0 until dataArray.length()) {
|
||||||
|
val item = dataArray.opt(i)
|
||||||
|
if (item is JSONObject && item.has("gallery")) {
|
||||||
|
val galleryIndex = item.getInt("gallery")
|
||||||
|
val galleryTemp = dataArray.optJSONObject(galleryIndex)
|
||||||
|
if (galleryTemp != null && galleryTemp.has("hash")) {
|
||||||
|
val hashIndex = galleryTemp.getInt("hash")
|
||||||
|
hashID = dataArray.getString(hashIndex)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw ParseException("Can't find hash ID in this chapter!", chapter.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
val imgList = mutableListOf<String>()
|
||||||
|
for (i in 0 until dataArray.length()) {
|
||||||
|
val item = dataArray.opt(i)
|
||||||
|
if (item is JSONObject && item.has("filename")) {
|
||||||
|
val filenameIndex = item.getInt("filename")
|
||||||
|
if (dataArray.length() > filenameIndex) {
|
||||||
|
val filename = dataArray.optString(filenameIndex, "")
|
||||||
|
if (filename.isNotEmpty()) {
|
||||||
|
imgList.add(filename)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw ParseException("Can't find imageUrls in this chapter!", chapter.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val server = config[preferredServerKey] ?: SERVER_DATA
|
||||||
|
return imgList.map { imgEx ->
|
||||||
|
val baseUrl = "https://$domain/image/$hashID/$imgEx"
|
||||||
|
val imageUrl = when (server) {
|
||||||
|
SERVER_DATA -> baseUrl
|
||||||
|
SERVER_DATA_SAVER -> baseUrl + SERVER_DATA_SAVER + compressID
|
||||||
|
else -> baseUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
MangaPage(
|
||||||
|
id = generateUid(imageUrl),
|
||||||
|
url = imageUrl,
|
||||||
|
preview = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun space2plus(input: String): String {
|
||||||
|
return input.replace(" ", "+")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue