[Grouple] Multiple translations support
This commit is contained in:
@@ -4,6 +4,7 @@ import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Interceptor
|
||||
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.model.*
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
@@ -113,6 +115,11 @@ internal abstract class GroupleParser(
|
||||
val root = doc.body().requireElementById("mangaBox").selectFirstOrThrow("div.leftContent")
|
||||
val dateFormat = SimpleDateFormat("dd.MM.yy", Locale.US)
|
||||
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(
|
||||
description = root.selectFirst("div.manga-description")?.html(),
|
||||
largeCoverUrl = coverImg?.attr("data-full"),
|
||||
@@ -128,24 +135,48 @@ internal abstract class GroupleParser(
|
||||
author = root.selectFirst("a.person-link")?.text() ?: manga.author,
|
||||
isNsfw = manga.isNsfw || root.select(".alert-warning").any { it.ownText().contains(NSFW_ALERT) },
|
||||
chapters = root.requireElementById("chapters-list").select("a.chapter-link")
|
||||
.mapChapters(reversed = true) { i, a ->
|
||||
val tr = a.selectFirstParent("tr") ?: return@mapChapters null
|
||||
.flatMapChapters(reversed = true) { a ->
|
||||
val tr = a.selectFirstParent("tr") ?: return@flatMapChapters emptyList()
|
||||
val href = a.attrAsRelativeUrl("href")
|
||||
var translators = ""
|
||||
val translatorElement = a.attr("title")
|
||||
if (!translatorElement.isNullOrBlank()) {
|
||||
translators = translatorElement.replace("(Переводчик),", "&").removeSuffix(" (Переводчик)")
|
||||
val number = tr.attr("data-num").toFloatOrNull()?.div(10f) ?: 0f
|
||||
val volume = tr.attr("data-vol").toIntOrNull() ?: 0
|
||||
if (translations.isNullOrEmpty() || a.attr("data-translations").isEmpty()) {
|
||||
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()
|
||||
}
|
||||
|
||||
@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
|
||||
internal fun <T> Iterable<T>.collectionSize(): Int {
|
||||
return if (this is Collection<*>) this.size else 10
|
||||
|
||||
@@ -3,11 +3,9 @@ package org.koitharu.kotatsu.parsers
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.HttpUrl
|
||||
import org.junit.jupiter.api.Disabled
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
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.model.*
|
||||
import org.koitharu.kotatsu.parsers.util.domain
|
||||
import org.koitharu.kotatsu.parsers.util.medianOrNull
|
||||
import org.koitharu.kotatsu.parsers.util.mimeType
|
||||
@@ -21,6 +19,14 @@ internal class MangaParserTest {
|
||||
private val context = MangaLoaderContextMock
|
||||
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}")
|
||||
@MangaSources
|
||||
fun list(source: MangaSource) = runTest(timeout = timeout) {
|
||||
@@ -162,8 +168,9 @@ internal class MangaParserTest {
|
||||
assert(c.isDistinctBy { it.id }) {
|
||||
"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 }) {
|
||||
"Chapters are not distinct by number: ${c.maxDuplicates { it.number to it.branch }} for $publicUrl"
|
||||
assert(c.isDistinctByNotNull { it.key() }) {
|
||||
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 })
|
||||
checkImageRequest(coverUrl, source)
|
||||
@@ -266,4 +273,10 @@ internal class MangaParserTest {
|
||||
private fun String.isCapitalized(): Boolean {
|
||||
return !first().isLowerCase()
|
||||
}
|
||||
|
||||
private fun MangaChapter.key(): Any? = when {
|
||||
number > 0f && volume > 0 -> Triple(branch, volume, number)
|
||||
number > 0f -> Pair(branch, number)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user