[DesuMe] Genres in list
This commit is contained in:
@@ -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<SortOrder> = EnumSet.of(
|
||||
SortOrder.UPDATED,
|
||||
SortOrder.POPULARITY,
|
||||
SortOrder.NEWEST,
|
||||
SortOrder.ALPHABETICAL,
|
||||
)
|
||||
override val sortOrders: Set<SortOrder> = EnumSet.of(
|
||||
SortOrder.UPDATED,
|
||||
SortOrder.POPULARITY,
|
||||
SortOrder.NEWEST,
|
||||
SortOrder.ALPHABETICAL,
|
||||
)
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
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<Manga>(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<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
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<Manga>(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<MangaPage> {
|
||||
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<MangaTag> {
|
||||
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<MangaPage> {
|
||||
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<MangaTag> {
|
||||
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<String, MangaTag> {
|
||||
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<String, MangaTag>(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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.koitharu.kotatsu.parsers.util
|
||||
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
class SuspendLazy<T>(
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user