[Grouple] Multiple translations support

master
Koitharu 2 years ago
parent 20685598e3
commit 0aa4ea01f7
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -4,6 +4,7 @@ import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
@ -18,6 +19,7 @@ import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.exception.ParseException import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
import org.koitharu.kotatsu.parsers.util.json.mapJSON import org.koitharu.kotatsu.parsers.util.json.mapJSON
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -113,6 +115,11 @@ internal abstract class GroupleParser(
val root = doc.body().requireElementById("mangaBox").selectFirstOrThrow("div.leftContent") val root = doc.body().requireElementById("mangaBox").selectFirstOrThrow("div.leftContent")
val dateFormat = SimpleDateFormat("dd.MM.yy", Locale.US) val dateFormat = SimpleDateFormat("dd.MM.yy", Locale.US)
val coverImg = root.selectFirst("div.subject-cover")?.selectFirst("img") val coverImg = root.selectFirst("div.subject-cover")?.selectFirst("img")
val translations = root.selectFirst("div.translator-selection")
?.select(".translator-selection-item")
?.associate {
it.id().removePrefix("tr-").toLong() to it.selectFirst(".translator-selection-name")?.textOrNull()
}
return manga.copy( return manga.copy(
description = root.selectFirst("div.manga-description")?.html(), description = root.selectFirst("div.manga-description")?.html(),
largeCoverUrl = coverImg?.attr("data-full"), largeCoverUrl = coverImg?.attr("data-full"),
@ -128,24 +135,48 @@ internal abstract class GroupleParser(
author = root.selectFirst("a.person-link")?.text() ?: manga.author, author = root.selectFirst("a.person-link")?.text() ?: manga.author,
isNsfw = manga.isNsfw || root.select(".alert-warning").any { it.ownText().contains(NSFW_ALERT) }, isNsfw = manga.isNsfw || root.select(".alert-warning").any { it.ownText().contains(NSFW_ALERT) },
chapters = root.requireElementById("chapters-list").select("a.chapter-link") chapters = root.requireElementById("chapters-list").select("a.chapter-link")
.mapChapters(reversed = true) { i, a -> .flatMapChapters(reversed = true) { a ->
val tr = a.selectFirstParent("tr") ?: return@mapChapters null val tr = a.selectFirstParent("tr") ?: return@flatMapChapters emptyList()
val href = a.attrAsRelativeUrl("href") val href = a.attrAsRelativeUrl("href")
var translators = "" val number = tr.attr("data-num").toFloatOrNull()?.div(10f) ?: 0f
val translatorElement = a.attr("title") val volume = tr.attr("data-vol").toIntOrNull() ?: 0
if (!translatorElement.isNullOrBlank()) { if (translations.isNullOrEmpty() || a.attr("data-translations").isEmpty()) {
translators = translatorElement.replace("(Переводчик),", "&").removeSuffix(" (Переводчик)") var translators = ""
val translatorElement = a.attr("title")
if (!translatorElement.isNullOrBlank()) {
translators = translatorElement.replace("(Переводчик),", "&").removeSuffix(" (Переводчик)")
}
listOf(
MangaChapter(
id = generateUid(href),
name = a.text().removePrefix(manga.title).trim(),
number = number,
volume = volume,
url = href,
uploadDate = dateFormat.tryParse(tr.selectFirst("td.date")?.text()),
scanlator = translators,
source = source,
branch = null,
),
)
} else {
val translationData = JSONArray(a.attr("data-translations"))
translationData.mapJSON { jo ->
val personId = jo.getLong("personId")
val link = href.setQueryParam("tran", personId.toString())
MangaChapter(
id = generateUid(link),
name = a.text().removePrefix(manga.title).trim(),
number = number,
volume = volume,
url = link,
uploadDate = dateFormat.tryParse(jo.getStringOrNull("dateCreated")),
scanlator = null,
source = source,
branch = translations[personId],
)
}
} }
MangaChapter(
id = generateUid(href),
name = a.text().removePrefix(manga.title).trim(),
number = i + 1,
url = href,
uploadDate = dateFormat.tryParse(tr.selectFirst("td.date")?.text()),
scanlator = translators,
source = source,
branch = null,
)
}, },
) )
} }
@ -402,4 +433,14 @@ internal abstract class GroupleParser(
) )
} }
} }
private fun String.setQueryParam(name: String, value: String): String {
return toAbsoluteUrl(domain)
.toHttpUrl()
.newBuilder()
.setQueryParameter(name, value)
.build()
.toString()
.toRelativeUrl(domain)
}
} }

@ -19,6 +19,19 @@ inline fun <T> List<T>.mapChapters(
return builder.toList() return builder.toList()
} }
@InternalParsersApi
inline fun <T> List<T>.flatMapChapters(
reversed: Boolean = false,
transform: (T) -> Iterable<MangaChapter?>,
): List<MangaChapter> {
val builder = ChaptersListBuilder(collectionSize())
val elements = if (reversed) this.asReversed() else this
for (item in elements) {
builder.addAll(transform(item))
}
return builder.toList()
}
@PublishedApi @PublishedApi
internal fun <T> Iterable<T>.collectionSize(): Int { internal fun <T> Iterable<T>.collectionSize(): Int {
return if (this is Collection<*>) this.size else 10 return if (this is Collection<*>) this.size else 10

@ -3,11 +3,9 @@ package org.koitharu.kotatsu.parsers
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import okhttp3.HttpUrl import okhttp3.HttpUrl
import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.ParameterizedTest
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.domain import org.koitharu.kotatsu.parsers.util.domain
import org.koitharu.kotatsu.parsers.util.medianOrNull import org.koitharu.kotatsu.parsers.util.medianOrNull
import org.koitharu.kotatsu.parsers.util.mimeType import org.koitharu.kotatsu.parsers.util.mimeType
@ -21,6 +19,14 @@ internal class MangaParserTest {
private val context = MangaLoaderContextMock private val context = MangaLoaderContextMock
private val timeout = 2.minutes private val timeout = 2.minutes
@Test
fun singleTest() = runTest {
val manga = mangaOf(MangaSource.READMANGA_RU, "https://readmanga.live/podniatie_urovnia_v_odinochku__A5e4e")
val parser = context.newParserInstance(manga.source)
val details = parser.getDetails(manga)
assert(!details.chapters.isNullOrEmpty())
}
@ParameterizedTest(name = "{index}|list|{0}") @ParameterizedTest(name = "{index}|list|{0}")
@MangaSources @MangaSources
fun list(source: MangaSource) = runTest(timeout = timeout) { fun list(source: MangaSource) = runTest(timeout = timeout) {
@ -162,8 +168,9 @@ internal class MangaParserTest {
assert(c.isDistinctBy { it.id }) { assert(c.isDistinctBy { it.id }) {
"Chapters are not distinct by id: ${c.maxDuplicates { it.id }} for $publicUrl" "Chapters are not distinct by id: ${c.maxDuplicates { it.id }} for $publicUrl"
} }
assert(c.isDistinctByNotNull { if (it.number > 0f) it.number to it.branch else null }) { assert(c.isDistinctByNotNull { it.key() }) {
"Chapters are not distinct by number: ${c.maxDuplicates { it.number to it.branch }} for $publicUrl" val dup = c.mapNotNull { it.key() }.maxDuplicates { it }
"Chapters are not distinct by branch/volume/number: $dup for $publicUrl"
} }
assert(c.all { it.source == source }) assert(c.all { it.source == source })
checkImageRequest(coverUrl, source) checkImageRequest(coverUrl, source)
@ -266,4 +273,10 @@ internal class MangaParserTest {
private fun String.isCapitalized(): Boolean { private fun String.isCapitalized(): Boolean {
return !first().isLowerCase() return !first().isLowerCase()
} }
private fun MangaChapter.key(): Any? = when {
number > 0f && volume > 0 -> Triple(branch, volume, number)
number > 0f -> Pair(branch, number)
else -> null
}
} }

Loading…
Cancel
Save