parent
9058ead462
commit
309b6405f9
@ -1 +1 @@
|
|||||||
total: 1201
|
total: 1202
|
||||||
@ -0,0 +1,475 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.all
|
||||||
|
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.jsoup.HttpStatusException
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
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.*
|
||||||
|
import org.koitharu.kotatsu.parsers.Broken
|
||||||
|
|
||||||
|
@Broken("Need fix getPages + fetchTags + fetchAuthors")
|
||||||
|
@MangaSourceParser("KOHARU", "Schale.network", type = ContentType.HENTAI)
|
||||||
|
internal class Koharu(context: MangaLoaderContext) :
|
||||||
|
LegacyPagedMangaParser(context, MangaParserSource.KOHARU, 24) {
|
||||||
|
|
||||||
|
override val configKeyDomain = ConfigKey.Domain("niyaniya.moe")
|
||||||
|
private val apiSuffix = "api.schale.network"
|
||||||
|
|
||||||
|
private var authorMap: Map<String, String>? = null
|
||||||
|
|
||||||
|
private val preferredImageResolutionKey = ConfigKey.PreferredImageServer(
|
||||||
|
presetValues = mapOf(
|
||||||
|
"0" to "Lowest Quality",
|
||||||
|
"780" to "Low Quality (780px)",
|
||||||
|
"980" to "Medium Quality (980px)",
|
||||||
|
"1280" to "High Quality (1280px)",
|
||||||
|
"1600" to "Highest Quality (1600px)",
|
||||||
|
),
|
||||||
|
defaultValue = "1280",
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
|
||||||
|
super.onCreateConfig(keys)
|
||||||
|
keys.add(userAgentKey)
|
||||||
|
keys.add(preferredImageResolutionKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRequestHeaders() = super.getRequestHeaders().newBuilder()
|
||||||
|
.add("referer", "https://$domain/")
|
||||||
|
.add("origin", "https://$domain")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
||||||
|
SortOrder.NEWEST,
|
||||||
|
SortOrder.POPULARITY,
|
||||||
|
SortOrder.POPULARITY_TODAY,
|
||||||
|
SortOrder.POPULARITY_WEEK,
|
||||||
|
SortOrder.ALPHABETICAL,
|
||||||
|
SortOrder.ALPHABETICAL_DESC,
|
||||||
|
SortOrder.RATING
|
||||||
|
)
|
||||||
|
|
||||||
|
override val filterCapabilities: MangaListFilterCapabilities
|
||||||
|
get() = MangaListFilterCapabilities(
|
||||||
|
isMultipleTagsSupported = true,
|
||||||
|
isSearchSupported = true,
|
||||||
|
isAuthorSearchSupported = true,
|
||||||
|
isSearchWithFiltersSupported = true,
|
||||||
|
isTagsExclusionSupported = true
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getFilterOptions() = MangaListFilterOptions(
|
||||||
|
availableTags = fetchTags()
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
|
||||||
|
val baseUrl = "https://$apiSuffix/books"
|
||||||
|
val url = buildString {
|
||||||
|
append(baseUrl)
|
||||||
|
|
||||||
|
val terms: MutableList<String> = mutableListOf()
|
||||||
|
val includedTags: MutableList<String> = mutableListOf()
|
||||||
|
val excludedTags: MutableList<String> = mutableListOf()
|
||||||
|
|
||||||
|
if (!filter.query.isNullOrEmpty() && filter.query.startsWith("id:")) {
|
||||||
|
val ipk = filter.query.removePrefix("id:")
|
||||||
|
val response = webClient.httpGet("$baseUrl/detail/$ipk").parseJson()
|
||||||
|
return listOf(parseMangaDetail(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
val sortValue = when (order) {
|
||||||
|
SortOrder.POPULARITY, SortOrder.POPULARITY_TODAY -> "8"
|
||||||
|
SortOrder.POPULARITY_WEEK -> "9"
|
||||||
|
SortOrder.ALPHABETICAL -> "2"
|
||||||
|
SortOrder.ALPHABETICAL_DESC -> "2"
|
||||||
|
SortOrder.RATING -> "3"
|
||||||
|
SortOrder.NEWEST -> "4"
|
||||||
|
else -> "4"
|
||||||
|
}
|
||||||
|
append("?sort=").append(sortValue)
|
||||||
|
|
||||||
|
if (!filter.query.isNullOrEmpty()) {
|
||||||
|
terms.add("title:\"${filter.query.urlEncoded()}\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filter.author.isNullOrEmpty()) {
|
||||||
|
val authors = fetchAuthors()
|
||||||
|
val authorId = authors[filter.author] ?:
|
||||||
|
authors.entries.find { it.key.equals(filter.author, ignoreCase = true) }?.value
|
||||||
|
|
||||||
|
if (authorId != null) {
|
||||||
|
includedTags.add(authorId)
|
||||||
|
} else {
|
||||||
|
terms.add("artist:\"${filter.author.urlEncoded()}\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.tags.forEach { tag ->
|
||||||
|
if (tag.key.startsWith("-")) {
|
||||||
|
excludedTags.add(tag.key.substring(1))
|
||||||
|
} else {
|
||||||
|
includedTags.add(tag.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludedTags.isNotEmpty()) {
|
||||||
|
append("&exclude=").append(excludedTags.joinToString(","))
|
||||||
|
append("&e=1")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includedTags.isNotEmpty()) {
|
||||||
|
append("&include=").append(includedTags.joinToString(","))
|
||||||
|
append("&i=1")
|
||||||
|
}
|
||||||
|
|
||||||
|
append("&page=").append(page)
|
||||||
|
|
||||||
|
if (terms.isNotEmpty()) {
|
||||||
|
append("&s=").append(terms.joinToString(" ").urlEncoded())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val json = webClient.httpGet(url).parseJson()
|
||||||
|
if (json.has("error") || json.has("message")) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
return parseMangaList(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseMangaList(json: JSONObject): List<Manga> {
|
||||||
|
val entries = json.optJSONArray("entries") ?: return emptyList()
|
||||||
|
val results = ArrayList<Manga>(entries.length())
|
||||||
|
|
||||||
|
for (i in 0 until entries.length()) {
|
||||||
|
val entry = entries.getJSONObject(i)
|
||||||
|
val id = entry.getInt("id")
|
||||||
|
val key = entry.getString("key")
|
||||||
|
val url = "$id/$key"
|
||||||
|
|
||||||
|
results.add(
|
||||||
|
Manga(
|
||||||
|
id = generateUid(url),
|
||||||
|
url = url,
|
||||||
|
publicUrl = "https://$domain/g/$url",
|
||||||
|
title = entry.getString("title"),
|
||||||
|
altTitles = emptySet(),
|
||||||
|
authors = emptySet(),
|
||||||
|
tags = emptySet(),
|
||||||
|
rating = RATING_UNKNOWN,
|
||||||
|
state = MangaState.FINISHED,
|
||||||
|
coverUrl = entry.getJSONObject("thumbnail").getString("path"),
|
||||||
|
contentRating = ContentRating.ADULT,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseMangaDetail(json: JSONObject): Manga {
|
||||||
|
val data = json.getJSONObject("data")
|
||||||
|
val id = data.getInt("id")
|
||||||
|
val key = data.getString("key")
|
||||||
|
val url = "$id/$key"
|
||||||
|
|
||||||
|
var author: String? = null
|
||||||
|
val tags = data.optJSONArray("tags")
|
||||||
|
if (tags != null) {
|
||||||
|
for (i in 0 until tags.length()) {
|
||||||
|
val tag = tags.getJSONObject(i)
|
||||||
|
if (tag.getInt("namespace") == 1) {
|
||||||
|
author = tag.getString("name")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Manga(
|
||||||
|
id = generateUid(url),
|
||||||
|
url = url,
|
||||||
|
publicUrl = "https://$domain/g/$url",
|
||||||
|
title = data.getString("title"),
|
||||||
|
altTitles = emptySet(),
|
||||||
|
authors = setOfNotNull(author),
|
||||||
|
tags = emptySet(),
|
||||||
|
rating = RATING_UNKNOWN,
|
||||||
|
state = MangaState.FINISHED,
|
||||||
|
coverUrl = data.getJSONObject("thumbnails").getJSONObject("main").getString("path"),
|
||||||
|
contentRating = ContentRating.ADULT,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getDetails(manga: Manga): Manga {
|
||||||
|
val url = manga.url
|
||||||
|
val response = webClient.httpGet("https://$apiSuffix/books/detail/$url").parseJson()
|
||||||
|
|
||||||
|
val id = response.getInt("id")
|
||||||
|
val key = response.getString("key")
|
||||||
|
val mangaUrl = "$id/$key"
|
||||||
|
|
||||||
|
val tagsList = mutableSetOf<MangaTag>()
|
||||||
|
var author: String? = null
|
||||||
|
val tags = response.optJSONArray("tags")
|
||||||
|
|
||||||
|
if (tags != null) {
|
||||||
|
for (i in 0 until tags.length()) {
|
||||||
|
val tag = tags.getJSONObject(i)
|
||||||
|
if (tag.has("namespace")) {
|
||||||
|
val namespace = tag.getInt("namespace")
|
||||||
|
val tagName = tag.getString("name")
|
||||||
|
|
||||||
|
when (namespace) {
|
||||||
|
1 -> {
|
||||||
|
author = tagName
|
||||||
|
}
|
||||||
|
0, 3, 8, 9, 10, 12 -> {
|
||||||
|
tagsList.add(
|
||||||
|
MangaTag(
|
||||||
|
key = tagName,
|
||||||
|
title = tagName.toTitleCase(sourceLocale),
|
||||||
|
source = source
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val tagName = tag.getString("name")
|
||||||
|
tagsList.add(
|
||||||
|
MangaTag(
|
||||||
|
key = tagName,
|
||||||
|
title = tagName.toTitleCase(sourceLocale),
|
||||||
|
source = source
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val description = buildString {
|
||||||
|
val created = response.getLongOrDefault("created_at", 0L)
|
||||||
|
if (created > 0) {
|
||||||
|
append("Posted: ").append(SimpleDateFormat("yyyy-MM-dd", Locale.US).format(created)).append("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
val thumbnails = response.getJSONObject("thumbnails")
|
||||||
|
val pageCount = thumbnails.optJSONArray("entries")?.length() ?: 0
|
||||||
|
append("Pages: ").append(pageCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
val thumbnails = response.getJSONObject("thumbnails")
|
||||||
|
val base = thumbnails.getString("base")
|
||||||
|
val mainPath = thumbnails.getJSONObject("main").getString("path")
|
||||||
|
val coverUrl = base + mainPath
|
||||||
|
|
||||||
|
return Manga(
|
||||||
|
id = generateUid(mangaUrl),
|
||||||
|
url = mangaUrl,
|
||||||
|
publicUrl = "https://$domain/g/$mangaUrl",
|
||||||
|
title = response.getString("title"),
|
||||||
|
altTitles = emptySet(),
|
||||||
|
authors = setOfNotNull(author),
|
||||||
|
tags = tagsList,
|
||||||
|
rating = RATING_UNKNOWN,
|
||||||
|
state = MangaState.FINISHED,
|
||||||
|
description = description,
|
||||||
|
coverUrl = coverUrl,
|
||||||
|
contentRating = ContentRating.ADULT,
|
||||||
|
source = source,
|
||||||
|
chapters = listOf(
|
||||||
|
MangaChapter(
|
||||||
|
id = generateUid("$mangaUrl/chapter"),
|
||||||
|
title = "Oneshot",
|
||||||
|
number = 1f,
|
||||||
|
url = mangaUrl,
|
||||||
|
scanlator = null,
|
||||||
|
uploadDate = response.getLongOrDefault("created_at", 0L),
|
||||||
|
branch = null,
|
||||||
|
source = source,
|
||||||
|
volume = 0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var cachedToken: String? = null
|
||||||
|
|
||||||
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||||
|
val mangaUrl = chapter.url
|
||||||
|
val parts = mangaUrl.split("/")
|
||||||
|
if (parts.size < 2) {
|
||||||
|
throw Exception("Invalid URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
val id = parts[0]
|
||||||
|
val key = parts[1]
|
||||||
|
|
||||||
|
val detailResponse = webClient.httpGet("https://$apiSuffix/books/detail/$id/$key").parseJson()
|
||||||
|
|
||||||
|
var currentToken = detailResponse.optString("crt", cachedToken ?: "")
|
||||||
|
|
||||||
|
if (currentToken.isEmpty()) {
|
||||||
|
try {
|
||||||
|
val noTokenResponse = webClient.httpPost(
|
||||||
|
url = "https://$apiSuffix/books/detail/$id/$key".toHttpUrl(),
|
||||||
|
form = emptyMap(),
|
||||||
|
extraHeaders = getRequestHeaders()
|
||||||
|
).parseJson()
|
||||||
|
|
||||||
|
currentToken = noTokenResponse.optString("crt", "")
|
||||||
|
if (currentToken.isNotEmpty()) {
|
||||||
|
cachedToken = currentToken
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw Exception("Cant get auth token!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentToken.isEmpty()) {
|
||||||
|
throw Exception("Cant get auth token!")
|
||||||
|
}
|
||||||
|
|
||||||
|
val dataResponse = try {
|
||||||
|
webClient.httpPost(
|
||||||
|
url = "https://$apiSuffix/books/detail/$id/$key?crt=$currentToken".toHttpUrl(),
|
||||||
|
form = emptyMap(),
|
||||||
|
extraHeaders = getRequestHeaders()
|
||||||
|
).parseJson()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw Exception("Cant send POST request: ${e.message}. Token may be expired.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val data = try {
|
||||||
|
dataResponse.getJSONObject("data").getJSONObject("data")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw Exception("Cant parse image data. Token may be invalid or expired: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val preferredRes = config[preferredImageResolutionKey] ?: "1280"
|
||||||
|
val resolutionOrder = when (preferredRes) {
|
||||||
|
"1600" -> listOf("1600", "1280", "0", "980", "780")
|
||||||
|
"1280" -> listOf("1280", "1600", "0", "980", "780")
|
||||||
|
"980" -> listOf("980", "1280", "0", "1600", "780")
|
||||||
|
"780" -> listOf("780", "980", "0", "1280", "1600")
|
||||||
|
else -> listOf("0", "1600", "1280", "980", "780")
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedImageId: Int? = null
|
||||||
|
var selectedPublicKey: String? = null
|
||||||
|
var selectedQuality = "0"
|
||||||
|
|
||||||
|
for (res in resolutionOrder) {
|
||||||
|
if (data.has(res) && !data.isNull(res)) {
|
||||||
|
val resData = data.getJSONObject(res)
|
||||||
|
if (resData.has("id") && resData.has("key")) {
|
||||||
|
selectedImageId = resData.getInt("id")
|
||||||
|
selectedPublicKey = resData.getString("key")
|
||||||
|
selectedQuality = res
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedImageId == null || selectedPublicKey == null) {
|
||||||
|
throw Exception("Cant find image data")
|
||||||
|
}
|
||||||
|
|
||||||
|
val imagesResponse = try {
|
||||||
|
webClient.httpGet(
|
||||||
|
"https://$apiSuffix/books/data/$id/$key/$selectedImageId/$selectedPublicKey/$selectedQuality?crt=$currentToken"
|
||||||
|
).parseJson()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw Exception("Cant fetch image data: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val base = imagesResponse.getString("base")
|
||||||
|
val entries = imagesResponse.getJSONArray("entries")
|
||||||
|
|
||||||
|
val pages = ArrayList<MangaPage>(entries.length())
|
||||||
|
for (i in 0 until entries.length()) {
|
||||||
|
val imagePath = entries.getJSONObject(i).getString("path")
|
||||||
|
val fullImageUrl = "$base$imagePath"
|
||||||
|
|
||||||
|
pages.add(
|
||||||
|
MangaPage(
|
||||||
|
id = generateUid(fullImageUrl),
|
||||||
|
url = fullImageUrl,
|
||||||
|
preview = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchTags(): Set<MangaTag> {
|
||||||
|
val url = "https://$apiSuffix/books/tags/filters"
|
||||||
|
val json = try {
|
||||||
|
webClient.httpGet(url).parseJson()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return emptySet()
|
||||||
|
}
|
||||||
|
|
||||||
|
val tagsArray = json.optJSONArray("tags") ?: return emptySet()
|
||||||
|
|
||||||
|
return tagsArray.mapJSONNotNullToSet { tagObject ->
|
||||||
|
if (tagObject.has("namespace")) {
|
||||||
|
return@mapJSONNotNullToSet null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tagObject.has("id") && tagObject.has("name")) {
|
||||||
|
MangaTag(
|
||||||
|
key = tagObject.getString("id"),
|
||||||
|
title = tagObject.getString("name").toTitleCase(sourceLocale),
|
||||||
|
source = source
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchAuthors(): Map<String, String> {
|
||||||
|
if (authorMap != null) {
|
||||||
|
return authorMap!!
|
||||||
|
}
|
||||||
|
|
||||||
|
val url = "https://$apiSuffix/books/tags/filters"
|
||||||
|
val json = try {
|
||||||
|
webClient.httpGet(url).parseJson()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
val tagsArray = json.optJSONArray("tags") ?: return emptyMap()
|
||||||
|
val result = mutableMapOf<String, String>()
|
||||||
|
|
||||||
|
for (i in 0 until tagsArray.length()) {
|
||||||
|
val tagObject = tagsArray.getJSONObject(i)
|
||||||
|
|
||||||
|
if (tagObject.has("namespace") && tagObject.getInt("namespace") == 1) {
|
||||||
|
if (tagObject.has("id") && tagObject.has("name")) {
|
||||||
|
val id = tagObject.getString("id")
|
||||||
|
val name = tagObject.getString("name")
|
||||||
|
result[name] = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authorMap = result
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue