Compare commits

...

7 Commits
v2.1 ... v2.1.2

Author SHA1 Message Date
Koitharu
197393fbd1 Fix webtoon scroll 2022-01-15 17:21:03 +02:00
J. Lavoie
51ef6e3c78 Translated using Weblate (French)
Currently translated at 100.0% (249 of 249 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
2022-01-15 08:37:11 +02:00
J. Lavoie
663277fe6f Translated using Weblate (Italian)
Currently translated at 100.0% (249 of 249 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
2022-01-15 08:37:11 +02:00
J. Lavoie
332a38d674 Translated using Weblate (German)
Currently translated at 100.0% (249 of 249 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
2022-01-15 08:37:11 +02:00
Zakhar Timoshenko
e9410a2f54 [MangaOwl] Fix missing pages 2022-01-15 08:20:17 +02:00
Koitharu
b5fa2bd660 Fix MangaDex pages extraction 2022-01-14 08:50:04 +02:00
Koitharu
e56c61d834 Update manga parsers 2022-01-10 18:36:52 +02:00
14 changed files with 76 additions and 43 deletions

View File

@@ -13,8 +13,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 31 targetSdkVersion 31
versionCode 376 versionCode 378
versionName '2.1' versionName '2.1.2'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View File

@@ -93,14 +93,14 @@ class DesuMeRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor
description = json.getString("description"), description = json.getString("description"),
chapters = chaptersList.mapIndexed { i, it -> chapters = chaptersList.mapIndexed { i, it ->
val chid = it.getLong("id") val chid = it.getLong("id")
val volChap = "Том " + it.getString("vol") + ". " + "Глава " + it.getString("ch") val volChap = "Том " + it.optString("vol", "0") + ". " + "Глава " + it.optString("ch", "0")
val title = if (it.getString("title") == "null") "" else it.getString("title") val title = it.optString("title", "null").takeUnless { it == "null" }
MangaChapter( MangaChapter(
id = generateUid(chid), id = generateUid(chid),
source = manga.source, source = manga.source,
url = "$baseChapterUrl$chid", url = "$baseChapterUrl$chid",
uploadDate = it.getLong("date") * 1000, uploadDate = it.getLong("date") * 1000,
name = if (title.isEmpty()) volChap else "$volChap: $title", name = if (title.isNullOrEmpty()) volChap else "$volChap: $title",
number = totalChapters - i, number = totalChapters - i,
scanlator = null, scanlator = null,
branch = null, branch = null,

View File

@@ -148,7 +148,7 @@ class MangaDexRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposit
chapters = feed.mapNotNull { jo -> chapters = feed.mapNotNull { jo ->
val id = jo.getString("id") val id = jo.getString("id")
val attrs = jo.getJSONObject("attributes") val attrs = jo.getJSONObject("attributes")
if (attrs.optJSONArray("data").isNullOrEmpty()) { if (!attrs.isNull("externalUrl")) {
return@mapNotNull null return@mapNotNull null
} }
val locale = Locale.forLanguageTag(attrs.getString("translatedLanguage")) val locale = Locale.forLanguageTag(attrs.getString("translatedLanguage"))
@@ -171,15 +171,14 @@ class MangaDexRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposit
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val domain = getDomain() val domain = getDomain()
val attrs = loaderContext.httpGet("https://api.$domain/chapter/${chapter.url}") val chapter = loaderContext.httpGet("https://api.$domain/at-home/server/${chapter.url}?forcePort443=false")
.parseJson() .parseJson()
.getJSONObject("data") .getJSONObject("chapter")
.getJSONObject("attributes") val pages = chapter.getJSONArray("data")
val data = attrs.getJSONArray("data") val prefix = "https://uploads.$domain/data/${chapter.getString("hash")}/"
val prefix = "https://uploads.$domain/data/${attrs.getString("hash")}/"
val referer = "https://$domain/" val referer = "https://$domain/"
return List(data.length()) { i -> return List(pages.length()) { i ->
val url = prefix + data.getString(i) val url = prefix + pages.getString(i)
MangaPage( MangaPage(
id = generateUid(url), id = generateUid(url),
url = url, url = url,

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.core.parser.site package org.koitharu.kotatsu.core.parser.site
import android.util.Base64
import org.koitharu.kotatsu.base.domain.MangaLoaderContext import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.exceptions.ParseException import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.* import org.koitharu.kotatsu.core.model.*
@@ -75,6 +76,10 @@ class MangaOwlRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposit
val info = doc.body().selectFirst("div.single_detail") ?: parseFailed("An error occurred while parsing") val info = doc.body().selectFirst("div.single_detail") ?: parseFailed("An error occurred while parsing")
val table = doc.body().selectFirst("div.single-grid-right") ?: parseFailed("An error occurred while parsing") val table = doc.body().selectFirst("div.single-grid-right") ?: parseFailed("An error occurred while parsing")
val dateFormat = SimpleDateFormat("MM/dd/yyyy", Locale.US) val dateFormat = SimpleDateFormat("MM/dd/yyyy", Locale.US)
val trRegex = "window\\['tr'] = '([^']*)';".toRegex(RegexOption.IGNORE_CASE)
val trElement = doc.getElementsByTag("script").find { trRegex.find(it.data()) != null } ?: parseFailed("Oops, tr not found")
val tr = trRegex.find(trElement.data())!!.groups[1]!!.value
val s = Base64.encodeToString(defaultDomain.toByteArray(), Base64.NO_PADDING)
return manga.copy( return manga.copy(
description = info.selectFirst(".description")?.html(), description = info.selectFirst(".description")?.html(),
largeCoverUrl = info.select("img").first()?.let { img -> largeCoverUrl = info.select("img").first()?.let { img ->
@@ -100,7 +105,7 @@ class MangaOwlRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposit
id = generateUid(href), id = generateUid(href),
name = a.select("label").text(), name = a.select("label").text(),
number = i + 1, number = i + 1,
url = href, url = "$href?tr=$tr&s=$s",
scanlator = null, scanlator = null,
branch = null, branch = null,
uploadDate = dateFormat.tryParse(li.selectFirst("small:last-of-type")?.text()), uploadDate = dateFormat.tryParse(li.selectFirst("small:last-of-type")?.text()),
@@ -120,7 +125,7 @@ class MangaOwlRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposit
id = generateUid(url), id = generateUid(url),
url = url, url = url,
preview = null, preview = null,
referer = fullUrl, referer = url,
source = MangaSource.MANGAOWL, source = MangaSource.MANGAOWL,
) )
} }

View File

@@ -55,7 +55,7 @@ class MangareadRepository(
id = generateUid(href), id = generateUid(href),
url = href, url = href,
publicUrl = href.inContextOf(div), publicUrl = href.inContextOf(div),
coverUrl = div.selectFirst("img")?.absUrl("src").orEmpty(), coverUrl = div.selectFirst("img")?.absUrl("data-src").orEmpty(),
title = summary?.selectFirst("h3")?.text().orEmpty(), title = summary?.selectFirst("h3")?.text().orEmpty(),
rating = div.selectFirst("span.total_votes")?.ownText() rating = div.selectFirst("span.total_votes")?.ownText()
?.toFloatOrNull()?.div(5f) ?: -1f, ?.toFloatOrNull()?.div(5f) ?: -1f,
@@ -107,16 +107,6 @@ class MangareadRepository(
val root2 = doc.body().selectFirst("div.content-area") val root2 = doc.body().selectFirst("div.content-area")
?.selectFirst("div.c-page") ?.selectFirst("div.c-page")
?: throw ParseException("Root2 not found") ?: throw ParseException("Root2 not found")
val mangaId = doc.getElementsByAttribute("data-post").firstOrNull()
?.attr("data-post")?.toLongOrNull()
?: throw ParseException("Cannot obtain manga id")
val doc2 = loaderContext.httpPost(
"https://${getDomain()}/wp-admin/admin-ajax.php",
mapOf(
"action" to "manga_get_chapters",
"manga" to mangaId.toString()
)
).parseHtml()
val dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale.US) val dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale.US)
return manga.copy( return manga.copy(
tags = root.selectFirst("div.genres-content")?.select("a") tags = root.selectFirst("div.genres-content")?.select("a")
@@ -132,7 +122,7 @@ class MangareadRepository(
?.select("p") ?.select("p")
?.filterNot { it.ownText().startsWith("A brief description") } ?.filterNot { it.ownText().startsWith("A brief description") }
?.joinToString { it.html() }, ?.joinToString { it.html() },
chapters = doc2.select("li").asReversed().mapIndexed { i, li -> chapters = root2.select("li").asReversed().mapIndexed { i, li ->
val a = li.selectFirst("a") val a = li.selectFirst("a")
val href = a?.relUrl("href").orEmpty().ifEmpty { val href = a?.relUrl("href").orEmpty().ifEmpty {
parseFailed("Link is missing") parseFailed("Link is missing")
@@ -144,7 +134,7 @@ class MangareadRepository(
url = href, url = href,
uploadDate = parseChapterDate( uploadDate = parseChapterDate(
dateFormat, dateFormat,
doc2.selectFirst("span.chapter-release-date i")?.text() li.selectFirst("span.chapter-release-date i")?.text()
), ),
source = MangaSource.MANGAREAD, source = MangaSource.MANGAREAD,
scanlator = null, scanlator = null,

View File

@@ -125,10 +125,10 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
number = chapters.length() - i, number = chapters.length() - i,
name = buildString { name = buildString {
append("Том ") append("Том ")
append(jo.getString("tome")) append(jo.optString("tome", "0"))
append(". ") append(". ")
append("Глава ") append("Глава ")
append(jo.getString("chapter")) append(jo.optString("chapter", "0"))
if (name.isNotEmpty()) { if (name.isNotEmpty()) {
append(" - ") append(" - ")
append(name) append(name)

View File

@@ -1,16 +1,19 @@
package org.koitharu.kotatsu.reader.ui.pager.webtoon package org.koitharu.kotatsu.reader.ui.pager.webtoon
import android.app.Activity
import android.content.Context import android.content.Context
import android.graphics.PointF import android.graphics.PointF
import android.util.AttributeSet import android.util.AttributeSet
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import org.koitharu.kotatsu.utils.ext.toIntUp import org.koitharu.kotatsu.utils.ext.toIntUp
class WebtoonImageView @JvmOverloads constructor(context: Context, attr: AttributeSet? = null) : class WebtoonImageView @JvmOverloads constructor(
SubsamplingScaleImageView(context, attr) { context: Context,
attr: AttributeSet? = null,
) : SubsamplingScaleImageView(context, attr) {
private val ct = PointF() private val ct = PointF()
private val displayHeight = resources.displayMetrics.heightPixels private val displayHeight = (context as Activity).window.decorView.height
private var scrollPos = 0 private var scrollPos = 0
private var scrollRange = SCROLL_UNKNOWN private var scrollRange = SCROLL_UNKNOWN
@@ -55,6 +58,30 @@ class WebtoonImageView @JvmOverloads constructor(context: Context, attr: Attribu
return desiredHeight return desiredHeight
} }
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
val parentWidth = MeasureSpec.getSize(widthMeasureSpec)
val parentHeight = MeasureSpec.getSize(heightMeasureSpec)
val resizeWidth = widthSpecMode != MeasureSpec.EXACTLY
val resizeHeight = heightSpecMode != MeasureSpec.EXACTLY
var width = parentWidth
var height = parentHeight
if (sWidth > 0 && sHeight > 0) {
if (resizeWidth && resizeHeight) {
width = sWidth
height = sHeight
} else if (resizeHeight) {
height = (sHeight.toDouble() / sWidth.toDouble() * width).toInt()
} else if (resizeWidth) {
width = (sWidth.toDouble() / sHeight.toDouble() * height).toInt()
}
}
width = width.coerceAtLeast(suggestedMinimumWidth)
height = height.coerceIn(suggestedMinimumHeight, displayHeight)
setMeasuredDimension(width, height)
}
private fun scrollToInternal(pos: Int) { private fun scrollToInternal(pos: Int) {
scrollPos = pos scrollPos = pos
ct.set(sWidth / 2f, (height / 2f + pos.toFloat()) / minScale) ct.set(sWidth / 2f, (height / 2f + pos.toFloat()) / minScale)

View File

@@ -34,7 +34,7 @@ class WebtoonRecyclerView @JvmOverloads constructor(
consumed[0] = 0 consumed[0] = 0
consumed[1] = consumedY consumed[1] = consumedY
} }
return consumedY != 0 return consumedY != 0 || dy == 0
} }
private fun consumeVerticalScroll(dy: Int): Int { private fun consumeVerticalScroll(dy: Int): Int {

View File

@@ -246,4 +246,7 @@
<string name="system_default">Standard</string> <string name="system_default">Standard</string>
<string name="exclude_nsfw_from_history">NSFW-Manga aus dem Verlauf ausschließen</string> <string name="exclude_nsfw_from_history">NSFW-Manga aus dem Verlauf ausschließen</string>
<string name="error_empty_name">Der Name sollte nicht leer sein</string> <string name="error_empty_name">Der Name sollte nicht leer sein</string>
<string name="show_pages_numbers">Seitenzahlen anzeigen</string>
<string name="enabled_sources">Freigegebene Quellen</string>
<string name="available_sources">Verfügbare Quellen</string>
</resources> </resources>

View File

@@ -246,4 +246,7 @@
<string name="system_default">Par défaut</string> <string name="system_default">Par défaut</string>
<string name="exclude_nsfw_from_history">Exclure les mangas osés de l\'historique</string> <string name="exclude_nsfw_from_history">Exclure les mangas osés de l\'historique</string>
<string name="error_empty_name">Le nom ne doit pas être vide</string> <string name="error_empty_name">Le nom ne doit pas être vide</string>
<string name="show_pages_numbers">Afficher les numéros de pages</string>
<string name="enabled_sources">Sources activées</string>
<string name="available_sources">Sources disponibles</string>
</resources> </resources>

View File

@@ -246,4 +246,7 @@
<string name="date_format">Formato della data</string> <string name="date_format">Formato della data</string>
<string name="exclude_nsfw_from_history">Escludi i manga NSFW dalla storia</string> <string name="exclude_nsfw_from_history">Escludi i manga NSFW dalla storia</string>
<string name="error_empty_name">Il nome non dovrebbe essere vuoto</string> <string name="error_empty_name">Il nome non dovrebbe essere vuoto</string>
<string name="show_pages_numbers">Mostra i numeri delle pagine</string>
<string name="enabled_sources">Fonti abilitate</string>
<string name="available_sources">Fonti disponibili</string>
</resources> </resources>

View File

@@ -6,6 +6,7 @@ import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.Parameterized import org.junit.runners.Parameterized
import org.koin.core.component.inject import org.koin.core.component.inject
import org.koin.core.logger.Level
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
import org.koin.test.KoinTest import org.koin.test.KoinTest
import org.koin.test.KoinTestRule import org.koin.test.KoinTestRule
@@ -18,7 +19,7 @@ import org.koitharu.kotatsu.utils.TestResponse
import org.koitharu.kotatsu.utils.ext.mapToSet import org.koitharu.kotatsu.utils.ext.mapToSet
import org.koitharu.kotatsu.utils.ext.medianOrNull import org.koitharu.kotatsu.utils.ext.medianOrNull
import org.koitharu.kotatsu.utils.isAbsoluteUrl import org.koitharu.kotatsu.utils.isAbsoluteUrl
import org.koitharu.kotatsu.utils.isRelativeUrl import org.koitharu.kotatsu.utils.isNotAbsoluteUrl
@RunWith(Parameterized::class) @RunWith(Parameterized::class)
class RemoteMangaRepositoryTest(private val source: MangaSource) : KoinTest { class RemoteMangaRepositoryTest(private val source: MangaSource) : KoinTest {
@@ -29,7 +30,7 @@ class RemoteMangaRepositoryTest(private val source: MangaSource) : KoinTest {
@get:Rule @get:Rule
val koinTestRule = KoinTestRule.create { val koinTestRule = KoinTestRule.create {
printLogger() printLogger(Level.ERROR)
modules(repositoryTestModule) modules(repositoryTestModule)
} }
@@ -112,7 +113,7 @@ class RemoteMangaRepositoryTest(private val source: MangaSource) : KoinTest {
Truth.assertThat(list.map { it.id }).containsNoDuplicates() Truth.assertThat(list.map { it.id }).containsNoDuplicates()
for (item in list) { for (item in list) {
Truth.assertThat(item.url).isNotEmpty() Truth.assertThat(item.url).isNotEmpty()
Truth.assertThat(item.url).isRelativeUrl() Truth.assertThat(item.url).isNotAbsoluteUrl()
Truth.assertThat(item.coverUrl).isAbsoluteUrl() Truth.assertThat(item.coverUrl).isAbsoluteUrl()
Truth.assertThat(item.title).isNotEmpty() Truth.assertThat(item.title).isNotEmpty()
Truth.assertThat(item.publicUrl).isAbsoluteUrl() Truth.assertThat(item.publicUrl).isAbsoluteUrl()

View File

@@ -3,25 +3,25 @@ package org.koitharu.kotatsu.utils
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineDispatcher import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher import org.junit.rules.TestWatcher
import org.junit.runner.Description import org.junit.runner.Description
class CoroutineTestRule( class CoroutineTestRule(
private val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher(), private val testDispatcher: TestDispatcher = StandardTestDispatcher(),
) : TestWatcher() { ) : TestWatcher() {
override fun starting(description: Description?) { override fun starting(description: Description) {
super.starting(description) super.starting(description)
Dispatchers.setMain(testDispatcher) Dispatchers.setMain(testDispatcher)
} }
override fun finished(description: Description?) { override fun finished(description: Description) {
super.finished(description) super.finished(description)
Dispatchers.resetMain() Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
} }
fun runBlockingTest(block: suspend CoroutineScope.() -> Unit) { fun runBlockingTest(block: suspend CoroutineScope.() -> Unit) {

View File

@@ -9,3 +9,5 @@ private val PATTERN_URL_RELATIVE = Pattern.compile("^/[^\\s]+", Pattern.CASE_INS
fun StringSubject.isRelativeUrl() = matches(PATTERN_URL_RELATIVE) fun StringSubject.isRelativeUrl() = matches(PATTERN_URL_RELATIVE)
fun StringSubject.isAbsoluteUrl() = matches(PATTERN_URL_ABSOLUTE) fun StringSubject.isAbsoluteUrl() = matches(PATTERN_URL_ABSOLUTE)
fun StringSubject.isNotAbsoluteUrl() = doesNotMatch(PATTERN_URL_ABSOLUTE)