[Koharu] Fixes

master
Koitharu 1 year ago
parent fecb1db3be
commit b24741678c
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -1,32 +1,33 @@
package org.koitharu.kotatsu.parsers.site.all 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 okhttp3.HttpUrl.Companion.toHttpUrl
import org.json.JSONObject
import org.koitharu.kotatsu.parsers.Broken
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.LegacyPagedMangaParser import org.koitharu.kotatsu.parsers.core.LegacyPagedMangaParser
import org.koitharu.kotatsu.parsers.exception.ParseException import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.* import org.koitharu.kotatsu.parsers.util.json.getIntOrDefault
import java.net.HttpURLConnection import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNullToSet
import org.koitharu.kotatsu.parsers.util.suspendlazy.getOrDefault
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import org.koitharu.kotatsu.parsers.Broken
@Broken("Need fix getPages + fetchTags + fetchAuthors") @Broken("Need fix getPages")
@MangaSourceParser("KOHARU", "Schale.network", type = ContentType.HENTAI) @MangaSourceParser("KOHARU", "Schale Network", type = ContentType.HENTAI)
internal class Koharu(context: MangaLoaderContext) : internal class Koharu(context: MangaLoaderContext) :
LegacyPagedMangaParser(context, MangaParserSource.KOHARU, 24) { LegacyPagedMangaParser(context, MangaParserSource.KOHARU, 24) {
override val configKeyDomain = ConfigKey.Domain("niyaniya.moe") override val configKeyDomain = ConfigKey.Domain("niyaniya.moe")
private val apiSuffix = "api.schale.network" private val apiSuffix = "api.schale.network"
private var authorMap: Map<String, String>? = null private val authorsIds = suspendLazy { fetchAuthorsIds() }
private val preferredImageResolutionKey = ConfigKey.PreferredImageServer( private val preferredImageResolutionKey = ConfigKey.PreferredImageServer(
presetValues = mapOf( presetValues = mapOf(
@ -57,7 +58,7 @@ internal class Koharu(context: MangaLoaderContext) :
SortOrder.POPULARITY_WEEK, SortOrder.POPULARITY_WEEK,
SortOrder.ALPHABETICAL, SortOrder.ALPHABETICAL,
SortOrder.ALPHABETICAL_DESC, SortOrder.ALPHABETICAL_DESC,
SortOrder.RATING SortOrder.RATING,
) )
override val filterCapabilities: MangaListFilterCapabilities override val filterCapabilities: MangaListFilterCapabilities
@ -66,11 +67,11 @@ internal class Koharu(context: MangaLoaderContext) :
isSearchSupported = true, isSearchSupported = true,
isAuthorSearchSupported = true, isAuthorSearchSupported = true,
isSearchWithFiltersSupported = true, isSearchWithFiltersSupported = true,
isTagsExclusionSupported = true isTagsExclusionSupported = true,
) )
override suspend fun getFilterOptions() = MangaListFilterOptions( override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchTags() availableTags = fetchTags(namespace = 0),
) )
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
@ -104,9 +105,8 @@ internal class Koharu(context: MangaLoaderContext) :
} }
if (!filter.author.isNullOrEmpty()) { if (!filter.author.isNullOrEmpty()) {
val authors = fetchAuthors() val authors = authorsIds.getOrDefault(emptyMap())
val authorId = authors[filter.author] ?: val authorId = authors[filter.author.lowercase()]
authors.entries.find { it.key.equals(filter.author, ignoreCase = true) }?.value
if (authorId != null) { if (authorId != null) {
includedTags.add(authorId) includedTags.add(authorId)
@ -141,8 +141,11 @@ internal class Koharu(context: MangaLoaderContext) :
} }
val json = webClient.httpGet(url).parseJson() val json = webClient.httpGet(url).parseJson()
if (json.has("error") || json.has("message")) { json.getStringOrNull("error")?.let {
return emptyList() throw ParseException(it, url)
}
json.getStringOrNull("message")?.let {
throw ParseException(it, url)
} }
return parseMangaList(json) return parseMangaList(json)
} }
@ -153,13 +156,13 @@ internal class Koharu(context: MangaLoaderContext) :
for (i in 0 until entries.length()) { for (i in 0 until entries.length()) {
val entry = entries.getJSONObject(i) val entry = entries.getJSONObject(i)
val id = entry.getInt("id") val id = entry.getLong("id")
val key = entry.getString("key") val key = entry.getString("key")
val url = "$id/$key" val url = "$id/$key"
results.add( results.add(
Manga( Manga(
id = generateUid(url), id = generateUid(id),
url = url, url = url,
publicUrl = "https://$domain/g/$url", publicUrl = "https://$domain/g/$url",
title = entry.getString("title"), title = entry.getString("title"),
@ -167,11 +170,11 @@ internal class Koharu(context: MangaLoaderContext) :
authors = emptySet(), authors = emptySet(),
tags = emptySet(), tags = emptySet(),
rating = RATING_UNKNOWN, rating = RATING_UNKNOWN,
state = MangaState.FINISHED, state = null,
coverUrl = entry.getJSONObject("thumbnail").getString("path"), coverUrl = entry.getJSONObject("thumbnail").getString("path"),
contentRating = ContentRating.ADULT, contentRating = ContentRating.ADULT,
source = source, source = source,
) ),
) )
} }
@ -180,7 +183,7 @@ internal class Koharu(context: MangaLoaderContext) :
private fun parseMangaDetail(json: JSONObject): Manga { private fun parseMangaDetail(json: JSONObject): Manga {
val data = json.getJSONObject("data") val data = json.getJSONObject("data")
val id = data.getInt("id") val id = data.getLong("id")
val key = data.getString("key") val key = data.getString("key")
val url = "$id/$key" val url = "$id/$key"
@ -197,7 +200,7 @@ internal class Koharu(context: MangaLoaderContext) :
} }
return Manga( return Manga(
id = generateUid(url), id = generateUid(id),
url = url, url = url,
publicUrl = "https://$domain/g/$url", publicUrl = "https://$domain/g/$url",
title = data.getString("title"), title = data.getString("title"),
@ -205,7 +208,7 @@ internal class Koharu(context: MangaLoaderContext) :
authors = setOfNotNull(author), authors = setOfNotNull(author),
tags = emptySet(), tags = emptySet(),
rating = RATING_UNKNOWN, rating = RATING_UNKNOWN,
state = MangaState.FINISHED, state = null,
coverUrl = data.getJSONObject("thumbnails").getJSONObject("main").getString("path"), coverUrl = data.getJSONObject("thumbnails").getJSONObject("main").getString("path"),
contentRating = ContentRating.ADULT, contentRating = ContentRating.ADULT,
source = source, source = source,
@ -216,7 +219,7 @@ internal class Koharu(context: MangaLoaderContext) :
val url = manga.url val url = manga.url
val response = webClient.httpGet("https://$apiSuffix/books/detail/$url").parseJson() val response = webClient.httpGet("https://$apiSuffix/books/detail/$url").parseJson()
val id = response.getInt("id") val id = response.getLong("id")
val key = response.getString("key") val key = response.getString("key")
val mangaUrl = "$id/$key" val mangaUrl = "$id/$key"
@ -235,13 +238,14 @@ internal class Koharu(context: MangaLoaderContext) :
1 -> { 1 -> {
author = tagName author = tagName
} }
0, 3, 8, 9, 10, 12 -> { 0, 3, 8, 9, 10, 12 -> {
tagsList.add( tagsList.add(
MangaTag( MangaTag(
key = tagName, key = tagName,
title = tagName.toTitleCase(sourceLocale), title = tagName.toTitleCase(sourceLocale),
source = source source = source,
) ),
) )
} }
} }
@ -251,8 +255,8 @@ internal class Koharu(context: MangaLoaderContext) :
MangaTag( MangaTag(
key = tagName, key = tagName,
title = tagName.toTitleCase(sourceLocale), title = tagName.toTitleCase(sourceLocale),
source = source source = source,
) ),
) )
} }
} }
@ -261,12 +265,12 @@ internal class Koharu(context: MangaLoaderContext) :
val description = buildString { val description = buildString {
val created = response.getLongOrDefault("created_at", 0L) val created = response.getLongOrDefault("created_at", 0L)
if (created > 0) { if (created > 0) {
append("Posted: ").append(SimpleDateFormat("yyyy-MM-dd", Locale.US).format(created)).append("\n") append("<b>Posted:</b> ").append(SimpleDateFormat("yyyy-MM-dd", Locale.US).format(created)).append("\n")
} }
val thumbnails = response.getJSONObject("thumbnails") val thumbnails = response.getJSONObject("thumbnails")
val pageCount = thumbnails.optJSONArray("entries")?.length() ?: 0 val pageCount = thumbnails.optJSONArray("entries")?.length() ?: 0
append("Pages: ").append(pageCount) append("<b>Pages:</b> ").append(pageCount)
} }
val thumbnails = response.getJSONObject("thumbnails") val thumbnails = response.getJSONObject("thumbnails")
@ -275,7 +279,7 @@ internal class Koharu(context: MangaLoaderContext) :
val coverUrl = base + mainPath val coverUrl = base + mainPath
return Manga( return Manga(
id = generateUid(mangaUrl), id = generateUid(id),
url = mangaUrl, url = mangaUrl,
publicUrl = "https://$domain/g/$mangaUrl", publicUrl = "https://$domain/g/$mangaUrl",
title = response.getString("title"), title = response.getString("title"),
@ -291,7 +295,7 @@ internal class Koharu(context: MangaLoaderContext) :
chapters = listOf( chapters = listOf(
MangaChapter( MangaChapter(
id = generateUid("$mangaUrl/chapter"), id = generateUid("$mangaUrl/chapter"),
title = "Oneshot", title = null,
number = 1f, number = 1f,
url = mangaUrl, url = mangaUrl,
scanlator = null, scanlator = null,
@ -299,18 +303,19 @@ internal class Koharu(context: MangaLoaderContext) :
branch = null, branch = null,
source = source, source = source,
volume = 0, volume = 0,
) ),
) ),
) )
} }
@Volatile
private var cachedToken: String? = null private var cachedToken: String? = null
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val mangaUrl = chapter.url val mangaUrl = chapter.url
val parts = mangaUrl.split("/") val parts = mangaUrl.split('/')
if (parts.size < 2) { if (parts.size < 2) {
throw Exception("Invalid URL") throw ParseException("Invalid URL", mangaUrl)
} }
val id = parts[0] val id = parts[0]
@ -318,43 +323,40 @@ internal class Koharu(context: MangaLoaderContext) :
val detailResponse = webClient.httpGet("https://$apiSuffix/books/detail/$id/$key").parseJson() val detailResponse = webClient.httpGet("https://$apiSuffix/books/detail/$id/$key").parseJson()
var currentToken = detailResponse.optString("crt", cachedToken ?: "") var currentToken = detailResponse.getStringOrNull("crt") ?: cachedToken
if (currentToken.isEmpty()) { if (currentToken.isNullOrEmpty()) {
try { try {
val noTokenResponse = webClient.httpPost( val noTokenResponse = webClient.httpPost(
url = "https://$apiSuffix/books/detail/$id/$key".toHttpUrl(), url = "https://$apiSuffix/books/detail/$id/$key".toHttpUrl(),
form = emptyMap(), form = emptyMap(),
extraHeaders = getRequestHeaders() extraHeaders = getRequestHeaders(),
).parseJson() ).parseJson()
currentToken = noTokenResponse.optString("crt", "") currentToken = noTokenResponse.getStringOrNull("crt")
if (currentToken.isNotEmpty()) { if (!currentToken.isNullOrEmpty()) {
cachedToken = currentToken cachedToken = currentToken
} }
} catch (e: Exception) { } catch (e: Exception) {
throw Exception("Cant get auth token!") throw IllegalStateException("Cant get auth token", e)
} }
} }
if (currentToken.isEmpty()) { if (currentToken.isNullOrEmpty()) {
throw Exception("Cant get auth token!") throw IllegalStateException("Cant get auth token")
} }
val dataResponse = try { val dataUrl = "https://$apiSuffix/books/detail/$id/$key?crt=$currentToken"
webClient.httpPost( val dataResponse = webClient.httpPost(
url = "https://$apiSuffix/books/detail/$id/$key?crt=$currentToken".toHttpUrl(), url = dataUrl.toHttpUrl(),
form = emptyMap(), form = emptyMap(),
extraHeaders = getRequestHeaders() extraHeaders = getRequestHeaders(),
).parseJson() ).parseJson()
} catch (e: Exception) {
throw Exception("Cant send POST request: ${e.message}. Token may be expired.")
}
val data = try { val data = try {
dataResponse.getJSONObject("data").getJSONObject("data") dataResponse.getJSONObject("data").getJSONObject("data")
} catch (e: Exception) { } catch (e: Exception) {
throw Exception("Cant parse image data. Token may be invalid or expired: ${e.message}") throw ParseException("Cant parse image data. Token may be invalid or expired: ${e.message}", dataUrl, e)
} }
val preferredRes = config[preferredImageResolutionKey] ?: "1280" val preferredRes = config[preferredImageResolutionKey] ?: "1280"
@ -383,16 +385,12 @@ internal class Koharu(context: MangaLoaderContext) :
} }
if (selectedImageId == null || selectedPublicKey == null) { if (selectedImageId == null || selectedPublicKey == null) {
throw Exception("Cant find image data") throw ParseException("Cant find image data", dataUrl)
} }
val imagesResponse = try { val imagesResponse = webClient.httpGet(
webClient.httpGet( "https://$apiSuffix/books/data/$id/$key/$selectedImageId/$selectedPublicKey/$selectedQuality?crt=$currentToken",
"https://$apiSuffix/books/data/$id/$key/$selectedImageId/$selectedPublicKey/$selectedQuality?crt=$currentToken"
).parseJson() ).parseJson()
} catch (e: Exception) {
throw Exception("Cant fetch image data: ${e.message}")
}
val base = imagesResponse.getString("base") val base = imagesResponse.getString("base")
val entries = imagesResponse.getJSONArray("entries") val entries = imagesResponse.getJSONArray("entries")
@ -408,68 +406,27 @@ internal class Koharu(context: MangaLoaderContext) :
url = fullImageUrl, url = fullImageUrl,
preview = null, preview = null,
source = source, source = source,
) ),
) )
} }
return pages return pages
} }
private suspend fun fetchTags(): Set<MangaTag> { private suspend fun fetchTags(namespace: Int): Set<MangaTag> =
val url = "https://$apiSuffix/books/tags/filters" webClient.httpGet("https://$apiSuffix/books/tags/filters").parseJsonArray().mapJSONNotNullToSet {
val json = try { if (it.getIntOrDefault("namespace", 0) != namespace) {
webClient.httpGet(url).parseJson() null
} catch (e: Exception) { } else {
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( MangaTag(
key = tagObject.getString("id"), title = it.getStringOrNull("name")
title = tagObject.getString("name").toTitleCase(sourceLocale), ?.toTitleCase(sourceLocale) ?: return@mapJSONNotNullToSet null,
source = source key = it.getStringOrNull("id") ?: return@mapJSONNotNullToSet null,
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()) { private suspend fun fetchAuthorsIds(): Map<String, String> = fetchTags(namespace = 1)
val tagObject = tagsArray.getJSONObject(i) .associate { it.title.lowercase() to it.key }
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…
Cancel
Save