From 5856681753868e990dfe223097a99739c1b0b933 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 24 May 2025 13:53:53 +0300 Subject: [PATCH] [LibSocial] Fixes --- .../parsers/site/ru/rulib/LibSocialParser.kt | 710 +++++++++--------- .../parsers/site/ru/rulib/MangaLibParser.kt | 15 +- .../parsers/site/ru/rulib/SlashLibParser.kt | 2 +- .../kotatsu/parsers/util/FaviconParser.kt | 4 + .../kotatsu/parsers/util/WebViewHelper.kt | 13 + 5 files changed, 399 insertions(+), 345 deletions(-) create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/WebViewHelper.kt diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/LibSocialParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/LibSocialParser.kt index 500ca0ad..fb9a8509 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/LibSocialParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/LibSocialParser.kt @@ -3,12 +3,15 @@ package org.koitharu.kotatsu.parsers.site.ru.rulib import androidx.collection.* import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.runBlocking import okhttp3.HttpUrl import org.json.JSONArray import org.json.JSONObject import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaParserAuthProvider import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.core.LegacyPagedMangaParser +import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.json.* @@ -17,346 +20,369 @@ import java.text.SimpleDateFormat import java.util.* internal abstract class LibSocialParser( - context: MangaLoaderContext, - source: MangaParserSource, - protected val siteDomain: String, - protected val siteId: Int, -) : LegacyPagedMangaParser(context, source, pageSize = 60) { - - override val availableSortOrders: Set = EnumSet.of( - SortOrder.UPDATED, - SortOrder.POPULARITY, - SortOrder.RATING, - SortOrder.NEWEST, - SortOrder.ALPHABETICAL, - SortOrder.ALPHABETICAL_DESC, - ) - - final override val configKeyDomain = ConfigKey.Domain(siteDomain) - - override val filterCapabilities: MangaListFilterCapabilities - get() = MangaListFilterCapabilities( - isMultipleTagsSupported = true, - isTagsExclusionSupported = true, - isSearchSupported = true, - isSearchWithFiltersSupported = true, - ) - - override suspend fun getFilterOptions() = MangaListFilterOptions( - availableTags = fetchAvailableTags(), - availableStates = EnumSet.allOf(MangaState::class.java), - ) - - private val statesMap = intObjectMapOf( - 1, MangaState.ONGOING, - 2, MangaState.FINISHED, - 3, MangaState.UPCOMING, - 4, MangaState.PAUSED, - 5, MangaState.ABANDONED, - ) - private val imageServers = suspendLazy(initializer = ::fetchServers) - private val splitTranslationsKey = ConfigKey.SplitByTranslations(true) - private val preferredServerKey = ConfigKey.PreferredImageServer( - presetValues = mapOf( - null to null, - SERVER_MAIN to "Первый", - SERVER_SECONDARY to "Второй", - SERVER_COMPRESS to "Сжатия", - SERVER_DOWNLOAD to "Загрузки", - SERVER_CROP to "Обрезки", - ), - defaultValue = null, - ) - - override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val urlBuilder = HttpUrl.Builder() - .scheme("https") - .host("api.lib.social") - .addPathSegment("api") - .addPathSegment("manga") - .addQueryParameter("site_id[]", siteId.toString()) - .addQueryParameter("fields[]", "rate") - .addQueryParameter("fields[]", "rate_avg") - .addQueryParameter("page", page.toString()) - for (state in filter.states) { - urlBuilder.addQueryParameter("status[]", statesMap.keyOf(state).toString()) - } - for (tag in filter.tags) { - urlBuilder.addQueryParameter("${tag.typeKey()}[]", tag.key.drop(1)) - } - for (tag in filter.tagsExclude) { - urlBuilder.addQueryParameter("${tag.typeKey()}_exclude[]", tag.key.drop(1)) - } - if (!filter.query.isNullOrEmpty()) { - urlBuilder.addQueryParameter("q", filter.query) - } - urlBuilder.addQueryParameter( - "sort_by", - when (order) { - SortOrder.UPDATED -> "last_chapter_at" - SortOrder.POPULARITY -> "views" - SortOrder.RATING -> "rate_avg" - SortOrder.NEWEST -> "created_at" - SortOrder.ALPHABETICAL, - SortOrder.ALPHABETICAL_DESC, - -> "rus_name" - - else -> null - }, - ) - urlBuilder.addQueryParameter( - "sort_type", - when (order) { - SortOrder.UPDATED, - SortOrder.POPULARITY, - SortOrder.RATING, - SortOrder.NEWEST, - SortOrder.ALPHABETICAL_DESC, - -> "desc" - - SortOrder.ALPHABETICAL -> "asc" - else -> null - }, - ) - val json = webClient.httpGet(urlBuilder.build()).parseJson() - val data = json.getJSONArray("data") - return data.mapJSON(::parseManga) - } - - override suspend fun getDetails(manga: Manga): Manga = coroutineScope { - val chapters = async { fetchChapters(manga) } - val url = HttpUrl.Builder() - .scheme("https") - .host("api.lib.social") - .addPathSegment("api") - .addPathSegment("manga") - .addPathSegment(manga.url) - .addQueryParameter("fields[]", "summary") - .addQueryParameter("fields[]", "genres") - .addQueryParameter("fields[]", "tags") - .addQueryParameter("fields[]", "authors") - .build() - val json = webClient.httpGet(url).parseJson().getJSONObject("data") - val genres = json.getJSONArray("genres").mapJSON { jo -> - MangaTag(title = jo.getString("name"), key = "g" + jo.getInt("id"), source = source) - } - val tags = json.getJSONArray("genres").mapJSON { jo -> - MangaTag(title = jo.getString("name"), key = "t" + jo.getInt("id"), source = source) - } - val author = json.getJSONArray("authors").optJSONObject(0)?.getStringOrNull("name") - manga.copy( - title = json.getStringOrNull("rus_name") ?: manga.title, - altTitles = setOfNotNull(json.getStringOrNull("name")), - tags = tagsSetOf(tags, genres), - authors = setOfNotNull(author), - description = json.getString("summary").nl2br(), - chapters = chapters.await(), - ) - } - - override suspend fun getPages(chapter: MangaChapter): List = coroutineScope { - val pages = async { - webClient.httpGet( - concatUrl("https://api.lib.social/api/manga/", chapter.url), - ).parseJson().getJSONObject("data") - } - val servers = imageServers.get() - val json = pages.await() - val primaryServer = getPrimaryImageServer(servers) - json.getJSONArray("pages").mapJSON { jo -> - val url = jo.getString("url") - MangaPage( - id = generateUid(jo.getLong("id")), - url = concatUrl(primaryServer, url), - preview = servers[SERVER_COMPRESS]?.let { concatUrl(it, url) }, - source = source, - ) - } - } - - private suspend fun fetchAvailableTags(): Set = coroutineScope { - val tags = async { fetchTags("tags") } - val genres = async { fetchTags("genres") } - tagsSetOf(tags.await(), genres.await()) - } - - override suspend fun getRelatedManga(seed: Manga): List { - val json = webClient.httpGet( - HttpUrl.Builder() - .scheme("https") - .host("api.lib.social") - .addPathSegment("api") - .addPathSegment("manga") - .addPathSegment(seed.url) - .addPathSegment("similar") - .build(), - ).parseJson().getJSONArray("data") - return json.mapJSON { jo -> - parseManga(jo.getJSONObject("media")) - } - } - - override fun onCreateConfig(keys: MutableCollection>) { - super.onCreateConfig(keys) - keys.remove(configKeyDomain) - keys.add(splitTranslationsKey) - keys.add(preferredServerKey) - } - - private fun parseManga(jo: JSONObject): Manga { - val cover = jo.getJSONObject("cover") - val isNsfwSource = jo.getJSONObject("ageRestriction").getIntOrDefault("id", 0) >= 3 - return Manga( - id = generateUid(jo.getLong("id")), - title = jo.getString("rus_name").ifEmpty { jo.getString("name") }, - altTitles = setOfNotNull(jo.getString("name")), - url = jo.getString("slug_url"), - publicUrl = "https://$siteDomain/ru/manga/" + jo.getString("slug_url"), - rating = jo.optJSONObject("rating") - ?.getFloatOrDefault("average", RATING_UNKNOWN * 10f)?.div(10f) ?: RATING_UNKNOWN, - contentRating = if (isNsfwSource) ContentRating.ADULT else null, - coverUrl = cover.getString("thumbnail"), - tags = setOf(), - state = statesMap[jo.optJSONObject("status")?.getIntOrDefault("id", -1) ?: -1], - authors = emptySet(), - largeCoverUrl = cover.getString("default"), - source = source, - ) - } - - private fun getPrimaryImageServer(servers: ScatterMap): String { - val preferred = config[preferredServerKey] - if (preferred != null) { - servers[preferred]?.let { return it } - } - return checkNotNull(servers[SERVER_MAIN] ?: servers[SERVER_DOWNLOAD] ?: servers[SERVER_SECONDARY]) { - "No available images servers" - } - } - - private suspend fun fetchChapters(manga: Manga): List { - val url = HttpUrl.Builder() - .scheme("https") - .host("api.lib.social") - .addPathSegment("api") - .addPathSegment("manga") - .addPathSegment(manga.url) - .addPathSegment("chapters") - .build() - val json = webClient.httpGet(url).parseJson().getJSONArray("data") - val builder = ChaptersListBuilder(json.length()) - val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US) - val useBranching = config[splitTranslationsKey] - for (i in 0 until json.length()) { - val jo = json.getJSONObject(i) - val volume = jo.getIntOrDefault("volume", 0) - val number = jo.getFloatOrDefault("number", 0f) - val numberString = number.formatSimple() - val name = jo.getStringOrNull("name") ?: buildString { - if (volume > 0) append("Том ").append(volume).append(' ') - append("Глава ").append(numberString) - } - val branches = jo.getJSONArray("branches") - for (j in 0 until branches.length()) { - val bjo = branches.getJSONObject(j) - val id = bjo.getLong("id") - val team = bjo.getJSONArray("teams").optJSONObject(0)?.getStringOrNull("name") - builder += MangaChapter( - id = generateUid(id), - title = name, - number = number, - volume = volume, - url = "${manga.url}/chapter?number=$numberString&volume=$volume", - scanlator = team, - uploadDate = dateFormat.tryParse(bjo.getStringOrNull("created_at")), - branch = if (useBranching) team else null, - source = source, - ) - } - } - return builder.toList() - } - - private suspend fun fetchTags(type: String): List { - val data = webClient.httpGet( - HttpUrl.Builder() - .scheme("https") - .host("api.lib.social") - .addPathSegment("api").addPathSegment(type).build(), - ).parseJson().getJSONArray("data") - val prefix = type.first().toString() - return data.mapJSONNotNull { jo -> - val sites = jo.getJSONArray("site_ids").toIntSet() - if (siteId !in sites) { - return@mapJSONNotNull null - } - MangaTag( - title = jo.getString("name"), - key = prefix + jo.getInt("id"), - source = source, - ) - } - } - - private suspend fun fetchServers(): ScatterMap { - val json = webClient.httpGet( - HttpUrl.Builder() - .scheme("https") - .host("api.lib.social") - .addPathSegment("api") - .addPathSegment("constants") - .addQueryParameter("fields[]", "imageServers") - .build(), - ).parseJson().getJSONObject("data").getJSONArray("imageServers") - val result = MutableScatterMap() - for (i in 0 until json.length()) { - val jo = json.getJSONObject(i) - val sites = jo.getJSONArray("site_ids").toIntSet() - if (siteId !in sites) { - continue - } - result[jo.getString("id")] = jo.getString("url") - } - return result - } - - private fun IntObjectMap.keyOf(value: V): Int { - forEach { k, v -> - if (v == value) { - return k - } - } - throw NoSuchElementException("No key associated with value $value") - } - - private fun JSONArray.toIntSet(): IntSet { - val result = MutableIntSet(length()) - for (i in 0 until length()) { - result.add(getInt(i)) - } - return result - } - - private fun MangaTag.typeKey() = when (key.firstOrNull()) { - 'g' -> "genres" - 't' -> "tags" - else -> throw IllegalArgumentException("Tag $key($title) is of unknown type") - } - - private fun tagsSetOf(tags: Collection, genres: Collection): Set { - val result = ArraySet(tags.size + genres.size) - val names = HashSet(tags.size + genres.size) - genres.forEach { x -> if (names.add(x.title)) result.add(x) } - tags.forEach { x -> if (names.add(x.title)) result.add(x) } - return result - } - - protected companion object { - - const val SERVER_MAIN = "main" - const val SERVER_SECONDARY = "secondary" - const val SERVER_COMPRESS = "compress" - const val SERVER_DOWNLOAD = "download" - const val SERVER_CROP = "crop" - } + context: MangaLoaderContext, + source: MangaParserSource, + siteDomain: String, + protected val siteId: Int, +) : LegacyPagedMangaParser(context, source, pageSize = 60), MangaParserAuthProvider { + + protected val apiHost = "api.cdnlibs.org" + + override val authUrl: String + get() = "https://$domain/ru/front/auth" + + override val isAuthorized: Boolean + get() = runBlocking { + runCatchingCancellable { getAuthData() }.getOrNull() != null + } + + override suspend fun getUsername(): String = getAuthData() + ?.getJSONObject("auth") + ?.getString("username") + ?: throw AuthRequiredException(source) + + override val availableSortOrders: Set = EnumSet.of( + SortOrder.UPDATED, + SortOrder.POPULARITY, + SortOrder.RATING, + SortOrder.NEWEST, + SortOrder.ALPHABETICAL, + SortOrder.ALPHABETICAL_DESC, + ) + + final override val configKeyDomain = ConfigKey.Domain(siteDomain) + + override val filterCapabilities: MangaListFilterCapabilities + get() = MangaListFilterCapabilities( + isMultipleTagsSupported = true, + isTagsExclusionSupported = true, + isSearchSupported = true, + isSearchWithFiltersSupported = true, + ) + + override suspend fun getFilterOptions() = MangaListFilterOptions( + availableTags = fetchAvailableTags(), + availableStates = EnumSet.allOf(MangaState::class.java), + ) + + private val statesMap = intObjectMapOf( + 1, MangaState.ONGOING, + 2, MangaState.FINISHED, + 3, MangaState.UPCOMING, + 4, MangaState.PAUSED, + 5, MangaState.ABANDONED, + ) + private val imageServers = suspendLazy(initializer = ::fetchServers) + private val splitTranslationsKey = ConfigKey.SplitByTranslations(true) + private val preferredServerKey = ConfigKey.PreferredImageServer( + presetValues = mapOf( + null to null, + SERVER_MAIN to "Первый", + SERVER_SECONDARY to "Второй", + SERVER_COMPRESS to "Сжатия", + SERVER_DOWNLOAD to "Загрузки", + SERVER_CROP to "Обрезки", + ), + defaultValue = null, + ) + + override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val urlBuilder = HttpUrl.Builder() + .scheme(SCHEME_HTTPS) + .host(apiHost) + .addPathSegment("api") + .addPathSegment("manga") + .addQueryParameter("site_id[]", siteId.toString()) + .addQueryParameter("fields[]", "rate") + .addQueryParameter("fields[]", "rate_avg") + .addQueryParameter("page", page.toString()) + for (state in filter.states) { + urlBuilder.addQueryParameter("status[]", statesMap.keyOf(state).toString()) + } + for (tag in filter.tags) { + urlBuilder.addQueryParameter("${tag.typeKey()}[]", tag.key.drop(1)) + } + for (tag in filter.tagsExclude) { + urlBuilder.addQueryParameter("${tag.typeKey()}_exclude[]", tag.key.drop(1)) + } + if (!filter.query.isNullOrEmpty()) { + urlBuilder.addQueryParameter("q", filter.query) + } + urlBuilder.addQueryParameter( + "sort_by", + when (order) { + SortOrder.UPDATED -> "last_chapter_at" + SortOrder.POPULARITY -> "views" + SortOrder.RATING -> "rate_avg" + SortOrder.NEWEST -> "created_at" + SortOrder.ALPHABETICAL, + SortOrder.ALPHABETICAL_DESC, + -> "rus_name" + + else -> null + }, + ) + urlBuilder.addQueryParameter( + "sort_type", + when (order) { + SortOrder.UPDATED, + SortOrder.POPULARITY, + SortOrder.RATING, + SortOrder.NEWEST, + SortOrder.ALPHABETICAL_DESC, + -> "desc" + + SortOrder.ALPHABETICAL -> "asc" + else -> null + }, + ) + val json = webClient.httpGet(urlBuilder.build()).parseJson() + val data = json.getJSONArray("data") + return data.mapJSON(::parseManga) + } + + override suspend fun getDetails(manga: Manga): Manga = coroutineScope { + val chapters = async { fetchChapters(manga) } + val url = HttpUrl.Builder() + .scheme(SCHEME_HTTPS) + .host(apiHost) + .addPathSegment("api") + .addPathSegment("manga") + .addPathSegment(manga.url) + .addQueryParameter("fields[]", "summary") + .addQueryParameter("fields[]", "genres") + .addQueryParameter("fields[]", "tags") + .addQueryParameter("fields[]", "authors") + .build() + val json = webClient.httpGet(url).parseJson().getJSONObject("data") + val genres = json.getJSONArray("genres").mapJSON { jo -> + MangaTag(title = jo.getString("name"), key = "g" + jo.getInt("id"), source = source) + } + val tags = json.getJSONArray("genres").mapJSON { jo -> + MangaTag(title = jo.getString("name"), key = "t" + jo.getInt("id"), source = source) + } + val author = json.getJSONArray("authors").optJSONObject(0)?.getStringOrNull("name") + manga.copy( + title = json.getStringOrNull("rus_name") ?: manga.title, + altTitles = setOfNotNull(json.getStringOrNull("name")), + tags = tagsSetOf(tags, genres), + authors = setOfNotNull(author), + description = json.getString("summary").nl2br(), + chapters = chapters.await(), + ) + } + + override suspend fun getPages(chapter: MangaChapter): List = coroutineScope { + val pages = async { + webClient.httpGet( + concatUrl("https://$apiHost/api/manga/", chapter.url), + ).parseJson().getJSONObject("data") + } + val servers = imageServers.get() + val json = pages.await() + val primaryServer = getPrimaryImageServer(servers) + json.getJSONArray("pages").mapJSON { jo -> + val url = jo.getString("url") + MangaPage( + id = generateUid(jo.getLong("id")), + url = concatUrl(primaryServer, url), + preview = servers[SERVER_COMPRESS]?.let { concatUrl(it, url) }, + source = source, + ) + } + } + + private suspend fun fetchAvailableTags(): Set = coroutineScope { + val tags = async { fetchTags("tags") } + val genres = async { fetchTags("genres") } + tagsSetOf(tags.await(), genres.await()) + } + + override suspend fun getRelatedManga(seed: Manga): List { + val json = webClient.httpGet( + HttpUrl.Builder() + .scheme(SCHEME_HTTPS) + .host(apiHost) + .addPathSegment("api") + .addPathSegment("manga") + .addPathSegment(seed.url) + .addPathSegment("similar") + .build(), + ).parseJson().getJSONArray("data") + return json.mapJSON { jo -> + parseManga(jo.getJSONObject("media")) + } + } + + override suspend fun resolveLink(resolver: LinkResolver, link: HttpUrl): Manga? { + return resolver.resolveManga(this, link.pathSegments.lastOrNull() ?: return null) + } + + override fun onCreateConfig(keys: MutableCollection>) { + super.onCreateConfig(keys) + keys.remove(configKeyDomain) + keys.add(splitTranslationsKey) + keys.add(preferredServerKey) + } + + private fun parseManga(jo: JSONObject): Manga { + val cover = jo.getJSONObject("cover") + val isNsfwSource = jo.getJSONObject("ageRestriction").getIntOrDefault("id", 0) >= 3 + return Manga( + id = generateUid(jo.getLong("id")), + title = jo.getString("rus_name").ifEmpty { jo.getString("name") }, + altTitles = setOfNotNull(jo.getString("name")), + url = jo.getString("slug_url"), + publicUrl = "https://$domain/ru/manga/" + jo.getString("slug_url"), + rating = jo.optJSONObject("rating") + ?.getFloatOrDefault("average", RATING_UNKNOWN * 10f)?.div(10f) ?: RATING_UNKNOWN, + contentRating = if (isNsfwSource) ContentRating.ADULT else null, + coverUrl = cover.getString("thumbnail"), + tags = setOf(), + state = statesMap[jo.optJSONObject("status")?.getIntOrDefault("id", -1) ?: -1], + authors = emptySet(), + largeCoverUrl = cover.getString("default"), + source = source, + ) + } + + private fun getPrimaryImageServer(servers: ScatterMap): String { + val preferred = config[preferredServerKey] + if (preferred != null) { + servers[preferred]?.let { return it } + } + return checkNotNull(servers[SERVER_MAIN] ?: servers[SERVER_DOWNLOAD] ?: servers[SERVER_SECONDARY]) { + "No available images servers" + } + } + + private suspend fun fetchChapters(manga: Manga): List { + val url = HttpUrl.Builder() + .scheme(SCHEME_HTTPS) + .host(apiHost) + .addPathSegment("api") + .addPathSegment("manga") + .addPathSegment(manga.url) + .addPathSegment("chapters") + .build() + val json = webClient.httpGet(url).parseJson().getJSONArray("data") + val builder = ChaptersListBuilder(json.length()) + val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US) + val useBranching = config[splitTranslationsKey] + for (i in 0 until json.length()) { + val jo = json.getJSONObject(i) + val volume = jo.getIntOrDefault("volume", 0) + val number = jo.getFloatOrDefault("number", 0f) + val numberString = number.formatSimple() + val name = jo.getStringOrNull("name") ?: buildString { + if (volume > 0) append("Том ").append(volume).append(' ') + append("Глава ").append(numberString) + } + val branches = jo.getJSONArray("branches") + for (j in 0 until branches.length()) { + val bjo = branches.getJSONObject(j) + val id = bjo.getLong("id") + val team = bjo.getJSONArray("teams").optJSONObject(0)?.getStringOrNull("name") + builder += MangaChapter( + id = generateUid(id), + title = name, + number = number, + volume = volume, + url = "${manga.url}/chapter?number=$numberString&volume=$volume", + scanlator = team, + uploadDate = dateFormat.tryParse(bjo.getStringOrNull("created_at")), + branch = if (useBranching) team else null, + source = source, + ) + } + } + return builder.toList() + } + + private suspend fun fetchTags(type: String): List { + val data = webClient.httpGet( + HttpUrl.Builder() + .scheme(SCHEME_HTTPS) + .host(apiHost) + .addPathSegment("api").addPathSegment(type).build(), + ).parseJson().getJSONArray("data") + val prefix = type.first().toString() + return data.mapJSONNotNull { jo -> + val sites = jo.getJSONArray("site_ids").toIntSet() + if (siteId !in sites) { + return@mapJSONNotNull null + } + MangaTag( + title = jo.getString("name"), + key = prefix + jo.getInt("id"), + source = source, + ) + } + } + + private suspend fun fetchServers(): ScatterMap { + val json = webClient.httpGet( + HttpUrl.Builder() + .scheme(SCHEME_HTTPS) + .host(apiHost) + .addPathSegment("api") + .addPathSegment("constants") + .addQueryParameter("fields[]", "imageServers") + .build(), + ).parseJson().getJSONObject("data").getJSONArray("imageServers") + val result = MutableScatterMap() + for (i in 0 until json.length()) { + val jo = json.getJSONObject(i) + val sites = jo.getJSONArray("site_ids").toIntSet() + if (siteId !in sites) { + continue + } + result[jo.getString("id")] = jo.getString("url") + } + return result + } + + private fun IntObjectMap.keyOf(value: V): Int { + forEach { k, v -> + if (v == value) { + return k + } + } + throw NoSuchElementException("No key associated with value $value") + } + + private fun JSONArray.toIntSet(): IntSet { + val result = MutableIntSet(length()) + for (i in 0 until length()) { + result.add(getInt(i)) + } + return result + } + + private fun MangaTag.typeKey() = when (key.firstOrNull()) { + 'g' -> "genres" + 't' -> "tags" + else -> throw IllegalArgumentException("Tag $key($title) is of unknown type") + } + + private fun tagsSetOf(tags: Collection, genres: Collection): Set { + val result = ArraySet(tags.size + genres.size) + val names = HashSet(tags.size + genres.size) + genres.forEach { x -> if (names.add(x.title)) result.add(x) } + tags.forEach { x -> if (names.add(x.title)) result.add(x) } + return result + } + + private suspend fun getAuthData(): JSONObject? { + return JSONObject(WebViewHelper(context, domain).getLocalStorageValue("auth") ?: return null) + } + + protected companion object { + + const val SERVER_MAIN = "main" + const val SERVER_SECONDARY = "secondary" + const val SERVER_COMPRESS = "compress" + const val SERVER_DOWNLOAD = "download" + const val SERVER_CROP = "crop" + } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/MangaLibParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/MangaLibParser.kt index 5780bef3..695e6984 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/MangaLibParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/MangaLibParser.kt @@ -2,6 +2,10 @@ package org.koitharu.kotatsu.parsers.site.ru.rulib import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.exception.AuthRequiredException +import org.koitharu.kotatsu.parsers.exception.NotFoundException +import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaParserSource @MangaSourceParser("MANGALIB", "MangaLib", "ru") @@ -11,5 +15,12 @@ internal class MangaLibParser( context = context, source = MangaParserSource.MANGALIB, siteId = 1, - siteDomain = "test-front.mangalib.me", -) + siteDomain = "mangalib.me", +) { + + override suspend fun getPages(chapter: MangaChapter): List = try { + super.getPages(chapter) + } catch (e: NotFoundException) { + throw AuthRequiredException(source, e) + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/SlashLibParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/SlashLibParser.kt index a697bff2..4c8c1c65 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/SlashLibParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/SlashLibParser.kt @@ -9,5 +9,5 @@ internal class SlashLibParser(context: MangaLoaderContext) : LibSocialParser( context = context, source = MangaParserSource.YAOILIB, siteId = 2, - siteDomain = "test-front.slashlib.me", + siteDomain = "v2.slashlib.me", ) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/FaviconParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/FaviconParser.kt index 24296048..b01fc794 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/FaviconParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/FaviconParser.kt @@ -30,6 +30,10 @@ public class FaviconParser( links.mapNotNullTo(result) { link -> parseLink(link) } + val touchIcons = doc.getElementsByAttributeValue("rel", "apple-touch-icon") + touchIcons.mapNotNullTo(result) { link -> + parseLink(link) + } if (result.isEmpty()) { result.add(createFallback()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/WebViewHelper.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/WebViewHelper.kt new file mode 100644 index 00000000..63d36494 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/WebViewHelper.kt @@ -0,0 +1,13 @@ +package org.koitharu.kotatsu.parsers.util + +import org.koitharu.kotatsu.parsers.MangaLoaderContext + +public class WebViewHelper( + private val context: MangaLoaderContext, + private val domain: String, +) { + + public suspend fun getLocalStorageValue(key: String): String? { + return context.evaluateJs("window.localStorage.getItem(\"$key\")") + } +}