diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/DesuMeParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/DesuMeParser.kt index 3b76baed..3004dcc4 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/DesuMeParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/DesuMeParser.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.parsers.site +import androidx.collection.ArrayMap import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.PagedMangaParser @@ -15,147 +16,165 @@ import java.util.* @MangaSourceParser("DESUME", "Desu.me", "ru") internal class DesuMeParser(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.DESUME, 20) { - override val configKeyDomain = ConfigKey.Domain("desu.me", null) + override val configKeyDomain = ConfigKey.Domain("desu.me", null) - override val sortOrders: Set = EnumSet.of( - SortOrder.UPDATED, - SortOrder.POPULARITY, - SortOrder.NEWEST, - SortOrder.ALPHABETICAL, - ) + override val sortOrders: Set = EnumSet.of( + SortOrder.UPDATED, + SortOrder.POPULARITY, + SortOrder.NEWEST, + SortOrder.ALPHABETICAL, + ) - override suspend fun getListPage( - page: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, - ): List { - if (query != null && page != searchPaginator.firstPage) { - return emptyList() - } - val domain = domain - val url = buildString { - append("https://") - append(domain) - append("/manga/api/?limit=20&order=") - append(getSortKey(sortOrder)) - append("&page=") - append(page) - if (!tags.isNullOrEmpty()) { - append("&genres=") - appendAll(tags, ",") { it.key } - } - if (query != null) { - append("&search=") - append(query) - } - } - val json = webClient.httpGet(url).parseJson().getJSONArray("response") - ?: throw ParseException("Invalid response", url) - val total = json.length() - val list = ArrayList(total) - for (i in 0 until total) { - val jo = json.getJSONObject(i) - val cover = jo.getJSONObject("image") - val id = jo.getLong("id") - list += Manga( - url = "/manga/api/$id", - publicUrl = jo.getString("url"), - source = MangaSource.DESUME, - title = jo.getString("russian"), - altTitle = jo.getString("name"), - coverUrl = cover.getString("preview"), - largeCoverUrl = cover.getString("original"), - state = when { - jo.getInt("ongoing") == 1 -> MangaState.ONGOING - else -> null - }, - rating = jo.getDouble("score").toFloat().coerceIn(0f, 1f), - id = generateUid(id), - isNsfw = false, - tags = emptySet(), - author = null, - description = jo.getString("description"), - ) - } - return list - } + private val tagsCache = SuspendLazy(::fetchTags) - override suspend fun getDetails(manga: Manga): Manga { - val url = manga.url.toAbsoluteUrl(domain) - val json = webClient.httpGet(url).parseJson().getJSONObject("response") - ?: throw ParseException("Invalid response", url) - val baseChapterUrl = manga.url + "/chapter/" - val chaptersList = json.getJSONObject("chapters").getJSONArray("list") - val totalChapters = chaptersList.length() - return manga.copy( - tags = json.getJSONArray("genres").mapJSONToSet { - MangaTag( - key = it.getString("text"), - title = it.getString("russian").toTitleCase(), - source = manga.source, - ) - }, - publicUrl = json.getString("url"), - description = json.getString("description"), - chapters = chaptersList.mapJSONIndexed { i, it -> - val chid = it.getLong("id") - val volChap = "Том " + it.optString("vol", "0") + ". " + "Глава " + it.optString("ch", "0") - val title = it.optString("title", "null").takeUnless { it == "null" } - MangaChapter( - id = generateUid(chid), - source = manga.source, - url = "$baseChapterUrl$chid", - uploadDate = it.getLong("date") * 1000, - name = if (title.isNullOrEmpty()) volChap else "$volChap: $title", - number = totalChapters - i, - scanlator = null, - branch = null, - ) - }.reversed(), - ) - } + override suspend fun getListPage( + page: Int, + query: String?, + tags: Set?, + sortOrder: SortOrder, + ): List { + if (query != null && page != searchPaginator.firstPage) { + return emptyList() + } + val domain = domain + val url = buildString { + append("https://") + append(domain) + append("/manga/api/?limit=20&order=") + append(getSortKey(sortOrder)) + append("&page=") + append(page) + if (!tags.isNullOrEmpty()) { + append("&genres=") + appendAll(tags, ",") { it.key } + } + if (query != null) { + append("&search=") + append(query) + } + } + val json = webClient.httpGet(url).parseJson().getJSONArray("response") + ?: throw ParseException("Invalid response", url) + val total = json.length() + val list = ArrayList(total) + val tagsMap = tagsCache.tryGet().getOrNull() + for (i in 0 until total) { + val jo = json.getJSONObject(i) + val cover = jo.getJSONObject("image") + val id = jo.getLong("id") + val genres = jo.getString("genres").split(',') + list += Manga( + url = "/manga/api/$id", + publicUrl = jo.getString("url"), + source = MangaSource.DESUME, + title = jo.getString("russian"), + altTitle = jo.getString("name"), + coverUrl = cover.getString("preview"), + largeCoverUrl = cover.getString("original"), + state = when { + jo.getInt("ongoing") == 1 -> MangaState.ONGOING + else -> null + }, + rating = jo.getDouble("score").toFloat().coerceIn(0f, 1f), + id = generateUid(id), + isNsfw = false, + tags = if (!tagsMap.isNullOrEmpty()) { + genres.mapNotNullToSet { g -> + tagsMap[g.trim().toTitleCase()] + } + } else { + emptySet() + }, + author = null, + description = jo.getString("description"), + ) + } + return list + } - override suspend fun getPages(chapter: MangaChapter): List { - val fullUrl = chapter.url.toAbsoluteUrl(domain) - val json = webClient.httpGet(fullUrl) - .parseJson() - .getJSONObject("response") ?: throw ParseException("Invalid response", fullUrl) - return json.getJSONObject("pages").getJSONArray("list").mapJSON { jo -> - MangaPage( - id = generateUid(jo.getLong("id")), - referer = fullUrl, - preview = null, - source = chapter.source, - url = jo.getString("img"), - ) - } - } + override suspend fun getDetails(manga: Manga): Manga { + val url = manga.url.toAbsoluteUrl(domain) + val json = webClient.httpGet(url).parseJson().getJSONObject("response") + ?: throw ParseException("Invalid response", url) + val baseChapterUrl = manga.url + "/chapter/" + val chaptersList = json.getJSONObject("chapters").getJSONArray("list") + val totalChapters = chaptersList.length() + return manga.copy( + tags = json.getJSONArray("genres").mapJSONToSet { + MangaTag( + key = it.getString("text"), + title = it.getString("russian").toTitleCase(), + source = manga.source, + ) + }, + publicUrl = json.getString("url"), + description = json.getString("description"), + chapters = chaptersList.mapJSONIndexed { i, it -> + val chid = it.getLong("id") + val volChap = "Том " + it.optString("vol", "0") + ". " + "Глава " + it.optString("ch", "0") + val title = it.optString("title", "null").takeUnless { it == "null" } + MangaChapter( + id = generateUid(chid), + source = manga.source, + url = "$baseChapterUrl$chid", + uploadDate = it.getLong("date") * 1000, + name = if (title.isNullOrEmpty()) volChap else "$volChap: $title", + number = totalChapters - i, + scanlator = null, + branch = null, + ) + }.reversed(), + ) + } - override suspend fun getTags(): Set { - val doc = webClient.httpGet("https://${domain}/manga/").parseHtml() - val root = doc.body().requireElementById("animeFilter") - .selectFirstOrThrow(".catalog-genres") - return root.select("li").mapToSet { - val input = it.selectFirstOrThrow("input") - MangaTag( - source = source, - key = input.attr("data-genre-slug").ifEmpty { - it.parseFailed("data-genre-slug is empty") - }, - title = input.attr("data-genre-name").toTitleCase().ifEmpty { - it.parseFailed("data-genre-name is empty") - }, - ) - } - } + override suspend fun getPages(chapter: MangaChapter): List { + val fullUrl = chapter.url.toAbsoluteUrl(domain) + val json = webClient.httpGet(fullUrl) + .parseJson() + .getJSONObject("response") ?: throw ParseException("Invalid response", fullUrl) + return json.getJSONObject("pages").getJSONArray("list").mapJSON { jo -> + MangaPage( + id = generateUid(jo.getLong("id")), + referer = fullUrl, + preview = null, + source = chapter.source, + url = jo.getString("img"), + ) + } + } - private fun getSortKey(sortOrder: SortOrder) = - when (sortOrder) { - SortOrder.ALPHABETICAL -> "name" - SortOrder.POPULARITY -> "popular" - SortOrder.UPDATED -> "updated" - SortOrder.NEWEST -> "id" - else -> "updated" - } + override suspend fun getTags(): Set { + return tagsCache.get().values.toSet() + } + + private fun getSortKey(sortOrder: SortOrder) = + when (sortOrder) { + SortOrder.ALPHABETICAL -> "name" + SortOrder.POPULARITY -> "popular" + SortOrder.UPDATED -> "updated" + SortOrder.NEWEST -> "id" + else -> "updated" + } + + private suspend fun fetchTags(): Map { + val doc = webClient.httpGet("https://${domain}/manga/").parseHtml() + val root = doc.body().requireElementById("animeFilter") + .selectFirstOrThrow(".catalog-genres") + val li = root.select("li") + val result = ArrayMap(li.size) + li.forEach { + val input = it.selectFirstOrThrow("input") + val tag = MangaTag( + source = source, + key = input.attr("data-genre-slug").ifEmpty { + it.parseFailed("data-genre-slug is empty") + }, + title = input.attr("data-genre-name").toTitleCase().ifEmpty { + it.parseFailed("data-genre-name is empty") + }, + ) + result[tag.title] = tag + } + return result + } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/SuspendLazy.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/SuspendLazy.kt new file mode 100644 index 00000000..cb9a911a --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/SuspendLazy.kt @@ -0,0 +1,36 @@ +package org.koitharu.kotatsu.parsers.util + +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +class SuspendLazy( + private val initializer: suspend () -> T, +) { + + private val mutex = Mutex() + private var cachedValue: Any? = Uninitialized + + @Suppress("UNCHECKED_CAST") + suspend fun get(): T { + // fast way + cachedValue.let { + if (it !== Uninitialized) { + return it as T + } + } + return mutex.withLock { + cachedValue.let { + if (it !== Uninitialized) { + return it as T + } + } + val result = initializer() + cachedValue = result + result + } + } + + suspend fun tryGet() = runCatchingCancellable { get() } + + private object Uninitialized +}