Merge pull request #404 from AwkwardPeak7/999
commit
75cc0716fd
@ -0,0 +1,400 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.all
|
||||||
|
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
||||||
|
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
|
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
|
||||||
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
|
import org.koitharu.kotatsu.parsers.util.SuspendLazy
|
||||||
|
import org.koitharu.kotatsu.parsers.util.domain
|
||||||
|
import org.koitharu.kotatsu.parsers.util.generateUid
|
||||||
|
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
|
||||||
|
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
||||||
|
import org.koitharu.kotatsu.parsers.util.json.mapJSONToSet
|
||||||
|
import org.koitharu.kotatsu.parsers.util.json.toJSONList
|
||||||
|
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||||
|
import org.koitharu.kotatsu.parsers.util.oneOrThrowIfMany
|
||||||
|
import org.koitharu.kotatsu.parsers.util.parseHtml
|
||||||
|
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
|
||||||
|
import org.koitharu.kotatsu.parsers.util.toCamelCase
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.EnumSet
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@MangaSourceParser("NINENINENINEHENTAI", "999Hentai", type = ContentType.HENTAI)
|
||||||
|
internal class NineNineNineHentaiParser(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.NINENINENINEHENTAI, size), Interceptor {
|
||||||
|
|
||||||
|
override val configKeyDomain = ConfigKey.Domain("999hentai.net")
|
||||||
|
|
||||||
|
override val availableSortOrders: EnumSet<SortOrder> = EnumSet.of(
|
||||||
|
SortOrder.POPULARITY,
|
||||||
|
SortOrder.NEWEST
|
||||||
|
)
|
||||||
|
|
||||||
|
override val isMultipleTagsSupported = false
|
||||||
|
|
||||||
|
override suspend fun getAvailableLocales() = setOf(
|
||||||
|
Locale.ENGLISH,
|
||||||
|
Locale.CHINESE,
|
||||||
|
Locale.JAPANESE,
|
||||||
|
Locale("es")
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun Locale?.getSiteLang(): String {
|
||||||
|
if (this == null) return "all"
|
||||||
|
|
||||||
|
return when {
|
||||||
|
equals(Locale.ENGLISH) -> "en"
|
||||||
|
equals(Locale.CHINESE) -> "cn"
|
||||||
|
equals(Locale.JAPANESE) -> "jp"
|
||||||
|
equals(Locale("es")) -> "es"
|
||||||
|
else -> "all"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need for disable encoding (with encoding not working)
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val request = chain.request()
|
||||||
|
val newRequest = if (request.header("Content-Encoding") != null) {
|
||||||
|
request.newBuilder().removeHeader("Content-Encoding").build()
|
||||||
|
} else {
|
||||||
|
request
|
||||||
|
}
|
||||||
|
return chain.proceed(newRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val cdnHost = SuspendLazy(::getUpdatedCdnHost)
|
||||||
|
|
||||||
|
private suspend fun getUpdatedCdnHost(): String {
|
||||||
|
val url = "https://$domain/manga-home"
|
||||||
|
val response = webClient.httpGet(url).parseHtml()
|
||||||
|
val cdn = response.selectFirst("img.v-thumbnail")?.attr("data-src")
|
||||||
|
return cdn?.toHttpUrlOrNull()?.host ?: "edge.fast4speed.rsvp"
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||||
|
val query = """
|
||||||
|
queryTags(
|
||||||
|
search: {format:"tagchapter",sortBy:Popular}
|
||||||
|
page: 1
|
||||||
|
limit: 100
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val tags = apiCall(query)
|
||||||
|
.getJSONObject("queryTags")
|
||||||
|
.getJSONArray("edges")
|
||||||
|
|
||||||
|
return tags.mapJSONToSet {
|
||||||
|
val name = it.getString("name")
|
||||||
|
MangaTag(
|
||||||
|
title = name.toCamelCase(),
|
||||||
|
key = name,
|
||||||
|
source = source
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||||
|
return when (filter) {
|
||||||
|
is MangaListFilter.Advanced -> {
|
||||||
|
if (filter.tags.isEmpty() && filter.sortOrder == SortOrder.POPULARITY) {
|
||||||
|
getPopularList(page, filter.locale)
|
||||||
|
} else {
|
||||||
|
getSearchList(page, null, filter.locale, filter.tags, filter.sortOrder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is MangaListFilter.Search -> {
|
||||||
|
getSearchList(page, filter.query, null, null, filter.sortOrder)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
getPopularList(page, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getPopularList(
|
||||||
|
page: Int,
|
||||||
|
locale: Locale?
|
||||||
|
): List<Manga> {
|
||||||
|
val query = """
|
||||||
|
queryPopularChapters(
|
||||||
|
size: $size
|
||||||
|
language: "${locale.getSiteLang()}"
|
||||||
|
dateRange: 1
|
||||||
|
page: $page
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
_id
|
||||||
|
name
|
||||||
|
firstPics
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
return apiCall(query)
|
||||||
|
.getJSONObject("queryPopularChapters")
|
||||||
|
.getJSONArray("edges")
|
||||||
|
.toMangaList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getSearchList(
|
||||||
|
page: Int,
|
||||||
|
search: String?,
|
||||||
|
locale: Locale?,
|
||||||
|
tags: Set<MangaTag>?,
|
||||||
|
sort: SortOrder?,
|
||||||
|
): List<Manga> {
|
||||||
|
val searchPayload = buildString {
|
||||||
|
if (!search.isNullOrEmpty()) {
|
||||||
|
append("query:\"$search\",")
|
||||||
|
}
|
||||||
|
append("language:\"${locale.getSiteLang()}\"")
|
||||||
|
if (sort == SortOrder.POPULARITY) {
|
||||||
|
append(",sortBy:Popular")
|
||||||
|
}
|
||||||
|
if (!tags.isNullOrEmpty()) {
|
||||||
|
val tag = tags.oneOrThrowIfMany()!!.key
|
||||||
|
append(",tags:[\"$tag\"]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val query = """
|
||||||
|
queryChapters(
|
||||||
|
limit: $size
|
||||||
|
search: {$searchPayload}
|
||||||
|
page: $page
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
_id
|
||||||
|
name
|
||||||
|
firstPics
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
return apiCall(query)
|
||||||
|
.getJSONObject("queryChapters")
|
||||||
|
.getJSONArray("edges")
|
||||||
|
.toMangaList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun JSONArray.toMangaList(): List<Manga> = mapJSON { entry ->
|
||||||
|
val id = entry.getString("_id")
|
||||||
|
val name = entry.getString("name")
|
||||||
|
val cover = runCatching {
|
||||||
|
entry.getJSONArray("firstPics")
|
||||||
|
.getJSONObject(0)
|
||||||
|
.getString("url")
|
||||||
|
}.getOrNull()
|
||||||
|
|
||||||
|
Manga(
|
||||||
|
id = generateUid(id),
|
||||||
|
title = name.replace(shortenTitleRegex, "").trim(),
|
||||||
|
altTitle = name,
|
||||||
|
coverUrl = when {
|
||||||
|
cover?.startsWith("http") == true -> cover
|
||||||
|
cover == null -> ""
|
||||||
|
else -> "https://${cdnHost.get()}/$cover"
|
||||||
|
},
|
||||||
|
author = null,
|
||||||
|
isNsfw = true,
|
||||||
|
url = id,
|
||||||
|
publicUrl = "/hchapter/$id".toAbsoluteUrl(domain),
|
||||||
|
tags = emptySet(),
|
||||||
|
source = source,
|
||||||
|
state = null,
|
||||||
|
rating = RATING_UNKNOWN,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override suspend fun getDetails(manga: Manga): Manga {
|
||||||
|
val query = """
|
||||||
|
queryChapter(
|
||||||
|
chapterId: "${manga.url}"
|
||||||
|
) {
|
||||||
|
_id
|
||||||
|
name
|
||||||
|
uploadDate
|
||||||
|
format
|
||||||
|
description
|
||||||
|
language
|
||||||
|
pages
|
||||||
|
tags
|
||||||
|
pictureUrls {
|
||||||
|
picCdn
|
||||||
|
pics
|
||||||
|
picsS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val entry = apiCall(query)
|
||||||
|
.getJSONObject("queryChapter")
|
||||||
|
|
||||||
|
val id = entry.getString("_id")
|
||||||
|
val name = entry.getString("name")
|
||||||
|
val cover = entry.getJSONArray("pictureUrls")
|
||||||
|
.getJSONObject(0)
|
||||||
|
.let { pics ->
|
||||||
|
val cdn = pics.getString("picCdn").let {
|
||||||
|
if (it.startsWith("http")) {
|
||||||
|
"$it/"
|
||||||
|
} else {
|
||||||
|
"https://${cdnHost.get()}/$it/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val img = pics.getJSONArray("pics").getJSONObject(0).getString("url")
|
||||||
|
val imgS = pics.getJSONArray("picsS").getJSONObject(0).getString("url")
|
||||||
|
Pair(cdn + imgS, cdn + img)
|
||||||
|
}
|
||||||
|
val tags = entry.optJSONArray("tags")?.mapJSON {
|
||||||
|
SiteTag(
|
||||||
|
name = it.getString("tagName"),
|
||||||
|
type = it.getStringOrNull("tagType")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return manga.copy(
|
||||||
|
title = name.replace(shortenTitleRegex, "").trim(),
|
||||||
|
altTitle = name,
|
||||||
|
coverUrl = cover.first,
|
||||||
|
largeCoverUrl = cover.second,
|
||||||
|
author = tags?.filter { it.type == "artist" }?.joinToString { it.name.toCamelCase() },
|
||||||
|
isNsfw = true,
|
||||||
|
tags = tags?.mapToSet {
|
||||||
|
MangaTag(
|
||||||
|
title = it.name.toCamelCase(),
|
||||||
|
key = it.name,
|
||||||
|
source = source
|
||||||
|
)
|
||||||
|
}.orEmpty(),
|
||||||
|
state = null,
|
||||||
|
description = entry.getStringOrNull("description"),
|
||||||
|
chapters = listOf(
|
||||||
|
MangaChapter(
|
||||||
|
id = generateUid(id),
|
||||||
|
name = name,
|
||||||
|
number = 1,
|
||||||
|
url = id,
|
||||||
|
uploadDate = runCatching {
|
||||||
|
dateFormat.parse(entry.getString("uploadDate"))!!.time
|
||||||
|
}.getOrDefault(0L),
|
||||||
|
branch = entry.getStringOrNull("language")?.let {
|
||||||
|
val locale = when (it) {
|
||||||
|
"en" -> Locale.ENGLISH
|
||||||
|
"jp" -> Locale.JAPANESE
|
||||||
|
"cn" -> Locale.CHINESE
|
||||||
|
"es" -> Locale("es")
|
||||||
|
else -> Locale.ROOT
|
||||||
|
}
|
||||||
|
|
||||||
|
return@let locale.getDisplayLanguage(locale)
|
||||||
|
},
|
||||||
|
scanlator = when(entry.getStringOrNull("format")) {
|
||||||
|
"artistcg" -> "ArtistCG"
|
||||||
|
"gamecg" -> "GameCG"
|
||||||
|
"imageset" -> "ImageSet"
|
||||||
|
else -> entry.getStringOrNull("format")?.toCamelCase()
|
||||||
|
},
|
||||||
|
source = source
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class SiteTag(
|
||||||
|
val name: String,
|
||||||
|
val type: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getRelatedManga(seed: Manga): List<Manga> {
|
||||||
|
val query = """
|
||||||
|
queryRecommendations(
|
||||||
|
type: "chapter"
|
||||||
|
_id: "${seed.url}"
|
||||||
|
search: {sortBy:Popular}
|
||||||
|
page: 1
|
||||||
|
size: $size
|
||||||
|
) {
|
||||||
|
chapters {
|
||||||
|
_id
|
||||||
|
name
|
||||||
|
firstPics
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
return apiCall(query)
|
||||||
|
.getJSONObject("queryRecommendations")
|
||||||
|
.getJSONArray("chapters")
|
||||||
|
.toMangaList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||||
|
val query = """
|
||||||
|
queryChapter(
|
||||||
|
chapterId: "${chapter.url}"
|
||||||
|
) {
|
||||||
|
pictureUrls {
|
||||||
|
picCdn
|
||||||
|
pics
|
||||||
|
picsS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val pages = apiCall(query)
|
||||||
|
.getJSONObject("queryChapter")
|
||||||
|
.getJSONArray("pictureUrls")
|
||||||
|
.getJSONObject(0)
|
||||||
|
|
||||||
|
val cdn = pages.getString("picCdn").let {
|
||||||
|
if (it.startsWith("http")) {
|
||||||
|
"$it/"
|
||||||
|
} else {
|
||||||
|
"https://${cdnHost.get()}/$it/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val pics = pages.getJSONArray("pics").toJSONList()
|
||||||
|
val picsS = pages.getJSONArray("picsS").toJSONList()
|
||||||
|
|
||||||
|
return pics.zip(picsS).map {
|
||||||
|
val img = it.first.getString("url")
|
||||||
|
val imgS = it.second.getString("url")
|
||||||
|
MangaPage(
|
||||||
|
id = generateUid(img),
|
||||||
|
url = cdn + img,
|
||||||
|
preview = cdn + imgS,
|
||||||
|
source = source
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun apiCall(query: String): JSONObject {
|
||||||
|
return webClient.graphQLQuery("https://api.$domain/api", query).getJSONObject("data")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val size = 20
|
||||||
|
private val shortenTitleRegex = Regex("""(\[[^]]*]|[({][^)}]*[)}])""")
|
||||||
|
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue