[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.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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user