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
10 changes: 7 additions & 3 deletions src/pt/sssscanlator/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ plugins {

keiyoushi {
name = "Yomu Comics"
versionCode = 52
contentWarning = ContentWarning.NSFW // or MIXED, please confirm
libVersion = "1.4"
versionCode = 53
contentWarning = ContentWarning.MIXED
libVersion = "1.6"

source {
lang = "pt-BR"
baseUrl = "https://yomu.com.br"
id = 1497838059713668619L
}
}

dependencies {
implementation(project(":lib:cryptoaes"))
}
Original file line number Diff line number Diff line change
@@ -1,95 +1,188 @@
package eu.kanade.tachiyomi.extension.pt.sssscanlator

import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import keiyoushi.lib.cryptoaes.CryptoAES
import keiyoushi.utils.extractNextJsRsc
import keiyoushi.utils.parseAs
import keiyoushi.utils.tryParse
import keiyoushi.utils.tryParseDate
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.text.SimpleDateFormat
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.put
import java.time.format.DateTimeFormatter
import java.util.Locale
import java.util.TimeZone
import kotlin.time.Instant

@Serializable
class LibraryResponseDto(
val pagination: LibraryPaginationDto = LibraryPaginationDto(),
)
private const val PASSPHRASE = "yomu_trolling_scrapers_v1"
private const val ENCRYPTED_PREFIX = "U2FsdGVkX1"

internal fun String.decryptPayload(): String = CryptoAES.decrypt(this, PASSPHRASE)

@Serializable
class LibraryPaginationDto(
val page: Int = 1,
val totalPages: Int = 1,
)
private val page: Int = 1,
private val totalPages: Int = 1,
) {
val hasNextPage get() = page < totalPages
}

@Serializable
class LibraryMangaDto(
val title: String,
val cover: String? = null,
val slug: String,
val type: String? = null,
private val title: String,
private val slug: String,
private val cover: String? = null,
) {
fun toSManga(): SManga = SManga.create().apply {
title = this@LibraryMangaDto.title
thumbnail_url = cover?.takeUnless(String::isBlank)
fun toSManga() = SManga.create().apply {
url = "/obra/$slug"
title = this@LibraryMangaDto.title
thumbnail_url = cover?.takeIf(String::isNotBlank)
}
}

internal fun JsonObject.toMangasPage(): MangasPage {
val mangas = values.asSequence()
.mapNotNull { (it as? JsonPrimitive)?.contentOrNull }
.filter { it.startsWith(ENCRYPTED_PREFIX) }
.map { it.decryptPayload() }
.firstOrNull(String::isNotEmpty)
?.parseAs<List<LibraryMangaDto>>()
?: throw Exception("Não foi possível ler a lista de obras")

val pagination = get("pagination")?.parseAs<LibraryPaginationDto>() ?: LibraryPaginationDto()
return MangasPage(mangas.map(LibraryMangaDto::toSManga), pagination.hasNextPage)
}

@Serializable
class SeriesPayloadDto(
val slug: String,
private val encryptedChapters: String,
val description: String? = null,
val author: String? = null,
val artist: String? = null,
val coverImage: String? = null,
@SerialName("capitulos_lista")
val chapters: List<SeriesChapterDto> = emptyList(),
private val slug: String? = null,
)
val status: String? = null,
) {
val chapters: List<SChapter>
get() = encryptedChapters.decryptPayload()
.takeIf(String::isNotEmpty)
?.parseAs<List<SeriesChapterDto>>()
?.map { it.toSChapter(slug) }
?: emptyList()
}

@Serializable
class SeriesChapterDto(
val number: Double,
val title: String? = null,
val releaseDate: String? = null,
@SerialName("id")
val chapterId: String,
val releaseAt: String? = null,
) {
fun toSChapter(mangaSlug: String): SChapter = SChapter.create().apply {
val chapterNumberLabel = number.toChapterNumberString()
class SeriesHeaderDto(
val seriesId: String,
val title: String,
)

url = "/ler/$mangaSlug/$chapterNumberLabel?chapterId=$chapterId"
name = title?.takeUnless { it.isBlank() } ?: "Capítulo $chapterNumberLabel"
chapter_number = number.toFloat()
date_upload = parseChapterDate(releaseAt, releaseDate)
}
class SeriesPage(val manga: SManga, val chapters: List<SChapter>)

companion object {
private val RELEASE_AT_MILLIS by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT).apply {
timeZone = TimeZone.getTimeZone("UTC")
internal fun String.parseSeriesPage(): SeriesPage {
var payload: SeriesPayloadDto? = null
var title: String? = null
val genres = mutableListOf<String>()

extractNextJsRsc<JsonElement> { element ->
when (element) {
is JsonObject -> {
if (payload == null && "encryptedChapters" in element && "slug" in element) {
payload = element.parseAs()
}
if (title == null && "seriesId" in element && "title" in element) {
title = element.parseAs<SeriesHeaderDto>().title
}
}
is JsonArray -> element.genreBadgeOrNull()?.let(genres::add)
else -> {}
}
false
}

val series = payload ?: throw Exception("Não foi possível ler os dados da obra")

private val RELEASE_DATE by lazy {
SimpleDateFormat("dd/MM/yyyy", Locale.ROOT)
val manga = SManga.create().apply {
url = "/obra/${series.slug}"
this.title = title ?: throw Exception("Título da obra não encontrado")
thumbnail_url = series.coverImage?.takeIf(String::isNotBlank)
description = series.description?.takeIf(String::isNotBlank)
author = series.author?.takeIf(String::isNotBlank)
artist = series.artist?.takeIf(String::isNotBlank)
genre = genres.distinct().joinToString()
status = when (series.status?.uppercase()) {
"ONGOING" -> SManga.ONGOING
"COMPLETED" -> SManga.COMPLETED
"HIATUS" -> SManga.ON_HIATUS
"CANCELED", "CANCELLED" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
}

return SeriesPage(manga, series.chapters)
}

private fun JsonArray.genreBadgeOrNull(): String? {
if (size != 4 || (this[1] as? JsonPrimitive)?.contentOrNull != "span") return null
if ((this[2] as? JsonPrimitive)?.contentOrNull.isNullOrEmpty()) return null

private fun parseChapterDate(releaseAt: String?, releaseDate: String?): Long {
RELEASE_AT_MILLIS.tryParse(releaseAt).takeIf { it != 0L }?.let { return it }
return RELEASE_DATE.tryParse(releaseDate)
val props = this[3] as? JsonObject ?: return null
if ((props["data-slot"] as? JsonPrimitive)?.contentOrNull != "badge") return null

return (props["children"] as? JsonPrimitive)?.contentOrNull?.takeIf(String::isNotBlank)
}

@Serializable
class SeriesChapterDto(
private val id: String,
private val number: Double,
private val title: String? = null,
private val releaseDate: String? = null,
private val releaseAt: String? = null,
) {
fun toSChapter(mangaSlug: String) = SChapter.create().apply {
val label = number.formatted()
url = id
name = title?.takeIf(String::isNotBlank) ?: "Capítulo $label"
chapter_number = number.toFloat()
date_upload = Instant.tryParse(releaseAt).takeIf { it != 0L } ?: DATE_FORMAT.tryParseDate(releaseDate)
memo = buildJsonObject {
put("slug", mangaSlug)
put("number", label)
}
}

private fun Double.formatted(): String = toString().removeSuffix(".0")

companion object {
private val DATE_FORMAT = DateTimeFormatter.ofPattern("dd/MM/yyyy", Locale.ROOT)
}
}

@Serializable
class ChapterPageDto(
val chapter: ChapterImagesDto,
)
class ChapterPayloadDto(
val seriesSlug: String,
private val encryptedChapter: String,
) {
val pages: List<Page>
get() = encryptedChapter.decryptPayload()
.takeIf(String::isNotEmpty)
?.parseAs<ChapterImagesDto>()
?.toPageList()
?: emptyList()
}

@Serializable
class ChapterImagesDto(
@SerialName("imagens_lista")
val images: List<String>,
)

private fun Double.toChapterNumberString(): String = toString().removeSuffix(".0")
@SerialName("imagens_lista") private val images: List<String> = emptyList(),
) {
fun toPageList() = images.mapIndexed { index, imageUrl -> Page(index, imageUrl = imageUrl) }
}
Loading