Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/pt/mangalivre/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ plugins {

keiyoushi {
name = "Manga Livre"
versionCode = 79
versionCode = 80
contentWarning = ContentWarning.SAFE
libVersion = "1.4"

Expand Down
Comment thread
atiladpribeiro marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,46 @@ package eu.kanade.tachiyomi.extension.pt.mangalivre
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import keiyoushi.utils.toJsonString
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.longOrNull
import kotlinx.serialization.json.put
import java.text.Normalizer

@Serializable
class WrapperDto(
val mangas: List<MangaDto>,
val pagination: PaginationDto,
) {
val hasNextPage get() = pagination.hasNextPage

companion object {
fun fromJson(json: JsonObject) = WrapperDto(
mangas = json.requiredArray("mangas").mapObjects(MangaDto::fromJson),
pagination = PaginationDto.fromJson(json.requiredObject("pagination")),
)
}
}

@Serializable
class PaginationDto(
val hasNextPage: Boolean,
)
) {
companion object {
fun fromJson(json: JsonObject) = PaginationDto(json.requiredBoolean("hasNextPage"))
}
}

@Serializable
class MangaDto(
val id: String,
val title: String,
@SerialName("coverUrl")
val thumbnailUrl: String? = null,
val authors: List<String>? = null,
val artists: List<String>? = null,
Expand Down Expand Up @@ -73,16 +90,40 @@ class MangaDto(
companion object {
private val MARKS_REGEX = "\\p{InCombiningDiacriticalMarks}+".toRegex()
private val NON_ALPHA_REGEX = "[^a-z0-9]+".toRegex()

fun fromJson(json: JsonObject) = MangaDto(
id = json.requiredString("id"),
title = json.requiredString("title"),
thumbnailUrl = json.optionalString("coverUrl"),
authors = json.optionalStringList("authors"),
artists = json.optionalStringList("artists"),
genres = json.optionalStringList("genres"),
description = json.optionalString("description"),
alternativeTitle = json.optionalString("alternativeTitle"),
chapters = json["chapters"]?.takeUnless { it === JsonNull }
?.jsonArray
?.mapObjects(ChapterDto::fromJson),
status = json.optionalString("status"),
)
}
}

@Serializable
class ChapterReferenceDto(
val mangaId: String,
val chapterId: String,
)
) {
fun toJson(): String = buildJsonObject {
put("mangaId", mangaId)
put("chapterId", chapterId)
}.toString()

companion object {
fun fromJson(value: String) = Json.parseToJsonElement(value).jsonObject.let {
ChapterReferenceDto(it.requiredString("mangaId"), it.requiredString("chapterId"))
}
}
}

@Serializable
class ChapterDto(
val id: String,
val number: String,
Expand All @@ -91,15 +132,55 @@ class ChapterDto(
fun toSChapter(slug: String, mangaId: String) = SChapter.create().apply {
name = "Capítulo $number"
date_upload = timestamp
url = "/$slug/$number#${ChapterReferenceDto(mangaId, id).toJsonString()}"
url = "/$slug/$number#${ChapterReferenceDto(mangaId, id).toJson()}"
}

companion object {
fun fromJson(json: JsonObject) = ChapterDto(
id = json.requiredString("id"),
number = json.requiredString("number"),
timestamp = json.requiredLong("timestamp"),
)
}
}

@Serializable
class PageDto(
val pages: List<String>,
) {
fun toPageList() = pages.mapIndexed { index, imageUrl ->
Page(index, imageUrl = imageUrl)
}

companion object {
fun fromJson(json: JsonObject) = PageDto(json.requiredArray("pages").toStringList())
}
}

private fun JsonObject.requiredElement(key: String) = this[key]
?: throw SerializationException("Missing required field: $key")

private fun JsonObject.requiredString(key: String): String = requiredElement(key).jsonPrimitive.content

private fun JsonObject.requiredBoolean(key: String): Boolean = requiredElement(key).jsonPrimitive.booleanOrNull
?: throw SerializationException("Field is not a boolean: $key")

private fun JsonObject.requiredLong(key: String): Long = requiredElement(key).jsonPrimitive.longOrNull
?: throw SerializationException("Field is not a long: $key")

private fun JsonObject.requiredArray(key: String): JsonArray = requiredElement(key).jsonArray

private fun JsonObject.requiredObject(key: String): JsonObject = requiredElement(key).jsonObject

private fun JsonObject.optionalString(key: String): String? = this[key]
?.jsonPrimitive
?.contentOrNull
?.takeIf { it.isNotBlank() }

private fun JsonObject.optionalStringList(key: String): List<String>? = this[key]
?.takeUnless { it === JsonNull }
?.jsonArray
?.toStringList()

private fun JsonArray.toStringList(): List<String> = map { it.jsonPrimitive.content }

private fun <T> JsonArray.mapObjects(transform: (JsonObject) -> T): List<T> = map { transform(it.jsonObject) }
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.extension.pt.mangalivre

import android.webkit.WebSettings
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.network.GET
Expand All @@ -13,8 +14,11 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import keiyoushi.annotation.Source
import keiyoushi.network.rateLimit
import keiyoushi.utils.applicationContext
import keiyoushi.utils.getPreferencesLazy
import keiyoushi.utils.parseAs
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonObject
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
Expand Down Expand Up @@ -43,13 +47,20 @@ abstract class MangaLivre :

private val preferences by getPreferencesLazy()

override fun headersBuilder(): Headers.Builder = super.headersBuilder()
.add("Accept", "*/*")
.add("Accept-Language", "pt-BR,en-US;q=0.9,en;q=0.8")
.add("Referer", "$baseUrl/")
.add("Sec-Fetch-Dest", "empty")
.add("Sec-Fetch-Mode", "cors")
.add("Sec-Fetch-Site", "same-origin")
private val webViewUserAgent: String? by lazy {
runCatching { WebSettings.getDefaultUserAgent(applicationContext) }
.getOrNull()
}

override fun headersBuilder(): Headers.Builder = super.headersBuilder().apply {
webViewUserAgent?.takeIf { it.isNotBlank() }?.let { set("User-Agent", it) }
add("Accept", "*/*")
add("Accept-Language", "pt-BR,en-US;q=0.9,en;q=0.8")
add("Referer", "$baseUrl/")
add("Sec-Fetch-Dest", "empty")
add("Sec-Fetch-Mode", "cors")
add("Sec-Fetch-Site", "same-origin")
}

// ============================== Popular =======================================

Expand Down Expand Up @@ -103,7 +114,7 @@ abstract class MangaLivre :
}

override fun searchMangaParse(response: Response): MangasPage {
val dto = response.parseJson<WrapperDto>()
val dto = WrapperDto.fromJson(response.parseJson())
val mangas = dto.mangas.map { it.toSManga(useAlternativeTitle) }
return MangasPage(mangas, dto.hasNextPage)
}
Expand All @@ -114,26 +125,26 @@ abstract class MangaLivre :

override fun mangaDetailsRequest(manga: SManga): Request = GET("$apiUrl/manga-by-slug/${manga.url}", headers)

override fun mangaDetailsParse(response: Response): SManga = response.parseJson<MangaDto>().toSManga(useAlternativeTitle)
override fun mangaDetailsParse(response: Response): SManga = MangaDto.fromJson(response.parseJson()).toSManga(useAlternativeTitle)

// ============================== Chapters =======================================

override fun chapterListRequest(manga: SManga): Request = mangaDetailsRequest(manga)

override fun chapterListParse(response: Response): List<SChapter> = response.parseJson<MangaDto>().toSChapterList()
override fun chapterListParse(response: Response): List<SChapter> = MangaDto.fromJson(response.parseJson()).toSChapterList()

// ============================== Pages =======================================

override fun pageListRequest(chapter: SChapter): Request {
val readerPath = chapter.url.substringBeforeLast("#")
val ref = chapter.url.substringAfterLast("#").parseAs<ChapterReferenceDto>()
val ref = ChapterReferenceDto.fromJson(chapter.url.substringAfterLast("#"))
return GET("$apiUrl/mangas/${ref.mangaId}/chapters/${ref.chapterId}", headers)
.newBuilder()
.tag(ReadingGateInterceptor.ReaderPath::class.java, ReadingGateInterceptor.ReaderPath(readerPath))
.build()
}

override fun pageListParse(response: Response): List<Page> = response.parseJson<PageDto>().toPageList()
override fun pageListParse(response: Response): List<Page> = PageDto.fromJson(response.parseJson()).toPageList()

override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()

Expand Down Expand Up @@ -179,18 +190,18 @@ abstract class MangaLivre :

// ============================== Utilities =======================================

private inline fun <reified T> Response.parseJson(): T {
val peek = peekBody(MAX_PEEK).string().trimStart()
if (peek.isEmpty() || peek.startsWith("<")) {
private fun Response.parseJson(): JsonObject {
val responseBody = body.string().trimStart()
if (responseBody.isEmpty() || responseBody.startsWith("<")) {
close()
throw IOException(NON_JSON_MESSAGE)
}
return parseAs<T>()
return runCatching { Json.parseToJsonElement(responseBody).jsonObject }
.getOrElse { throw IOException(NON_JSON_MESSAGE, it) }
}

companion object {
private const val ALTERNATIVE_TITLE_PREF = "alternativeTitlePref"
private const val MAX_PEEK = 1024L
private const val NON_JSON_MESSAGE =
"Resposta não-JSON (Cloudflare ou header desatualizado). Abra a fonte na WebView do app e tente de novo."

Expand Down
Loading