Skip to content

Commit

Permalink
ReadMangas: Theme change (#6525)
Browse files Browse the repository at this point in the history
* Migration

* Remove data class
  • Loading branch information
choppeh authored Dec 9, 2024
1 parent 8a51465 commit 5c4f754
Show file tree
Hide file tree
Showing 8 changed files with 333 additions and 13 deletions.
4 changes: 1 addition & 3 deletions src/pt/readmangas/build.gradle
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
ext {
extName = 'Read Mangas'
extClass = '.ReadMangas'
themePkg = 'mangathemesia'
baseUrl = 'https://readmangas.org'
overrideVersionCode = 0
extVersionCode = 31
}

apply from: "$rootDir/common.gradle"
Binary file added src/pt/readmangas/res/mipmap-hdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/pt/readmangas/res/mipmap-mdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,18 +1,289 @@
package eu.kanade.tachiyomi.extension.pt.readmangas

import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import android.annotation.SuppressLint
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import okhttp3.OkHttpClient
import eu.kanade.tachiyomi.source.model.FilterList
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 eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.TimeZone

class ReadMangas : MangaThemesia(
"Read Mangas",
"https://readmangas.org",
"pt-BR",
dateFormat = SimpleDateFormat("MMM dd, yyyy", Locale("pt", "BR")),
) {
override val client: OkHttpClient = super.client.newBuilder()
.rateLimit(3)
class ReadMangas() : HttpSource() {

override val name = "Read Mangas"

override val baseUrl = "https://readmangas.org"

override val lang = "pt-BR"

override val supportsLatest = true

private val json: Json by injectLazy()

override val client = network.cloudflareClient.newBuilder()
.rateLimit(2)
.build()

override val versionId = 2

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

private var popularNextCursorPage = ""

override fun popularMangaRequest(page: Int): Request {
if (page == 1) {
popularNextCursorPage = ""
}

val input = buildJsonObject {
put(
"0",
buildJsonObject {
put(
"json",
buildJsonObject {
put("direction", "forward")
if (popularNextCursorPage.isNotBlank()) {
put("cursor", popularNextCursorPage)
}
},
)
},
)
}

val url = "$baseUrl/api/trpc/manga.getAllManga?batch=1".toHttpUrl().newBuilder()
.addQueryParameter("batch", "1")
.addQueryParameter("input", input.toString())
.build()
return GET(url, headers)
}

override fun popularMangaParse(response: Response): MangasPage {
val (mangaPage, nextCursor) = mangasPageParse(response)
popularNextCursorPage = nextCursor
return mangaPage
}

// =========================== Latest ===================================

private var latestNextCursorPage = ""

override fun latestUpdatesRequest(page: Int): Request {
if (page == 1) {
latestNextCursorPage = Date().let { latestUpdateDateFormat.format(it) }
}

val input = buildJsonObject {
put(
"0",
buildJsonObject {
put(
"json",
buildJsonObject {
put("direction", "forward")
put("limit", 20)
put("cursor", latestNextCursorPage)
},
)
},
)
}

val url = "$baseUrl/api/trpc/discover.updated".toHttpUrl().newBuilder()
.addQueryParameter("batch", "1")
.addQueryParameter("input", input.toString())
.build()
return GET(url, headers)
}

override fun latestUpdatesParse(response: Response): MangasPage {
val (mangaPage, nextCursor) = mangasPageParse(response)
latestNextCursorPage = nextCursor
return mangaPage
}

// =========================== Search =================================

override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/api/trpc/discover.search?batch=1"
val payload = buildJsonObject {
put(
"0",
buildJsonObject {
put(
"json",
buildJsonObject {
put("name", query)
},
)
},
)
}.toString().toRequestBody("application/json".toMediaType())

return POST(url, headers, payload)
}

override fun searchMangaParse(response: Response) = latestUpdatesParse(response)

// =========================== Details =================================

override fun mangaDetailsParse(response: Response): SManga {
val document = response.asJsoup()
return SManga.create().apply {
title = document.selectFirst("h1")!!.text()
thumbnail_url = document.selectFirst("img.w-full")?.absUrl("src")
genre = document.select("div > label + div > div").joinToString { it.text() }

description = document.select("script").map { it.data() }
.firstOrNull { MANGA_DETAILS_DESCRIPTION_REGEX.containsMatchIn(it) }
?.let {
MANGA_DETAILS_DESCRIPTION_REGEX.find(it)?.groups?.get("description")?.value
}

document.selectFirst("div.flex > div.inline-flex.items-center:last-child")?.text()?.let {
status = it.toStatus()
}
}
}

// =========================== Chapter =================================

override fun chapterListRequest(manga: SManga) = throw UnsupportedOperationException()

override fun chapterListParse(response: Response) = throw UnsupportedOperationException()

private fun chapterListRequest(manga: SManga, page: Int): Request {
val id = manga.url.substringAfterLast("#")
val input = buildJsonObject {
put(
"1",
buildJsonObject {
put(
"json",
buildJsonObject {
put("id", id)
put("page", page)
put("limit", 10)
put("sort", "desc")
put("search", "")
},
)
},
)
}

val url = "$baseUrl/api/trpc/manga.getLiked,chapter.publicAllChapters".toHttpUrl().newBuilder()
.addQueryParameter("batch", "1")
.addQueryParameter("input", input.toString())
.build()

return GET(url, headers)
}

override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val chapters = mutableListOf<SChapter>()
var page = 1
do {
val response = client.newCall(this.chapterListRequest(manga, page++)).execute()
val dto = response
.parseAs<List<WrapperResult<ChapterListDto>>>()
.firstNotNullOf { it.result }
.data.json
chapters += chapterListParse(dto.chapters)
} while (dto.hasNext())

return Observable.just(chapters)
}

private fun chapterListParse(chapters: List<ChapterDto>): List<SChapter> {
return chapters.map {
SChapter.create().apply {
name = it.title
chapter_number = it.number.toFloat()
date_upload = it.createdAt.toDate()
url = "/readme/${it.id}"
}
}
}

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

override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
val script = document.select("script").map { it.data() }
.firstOrNull { IMAGE_URL_REGEX.containsMatchIn(it) }
?: return emptyList()

return IMAGE_URL_REGEX.findAll(script).mapIndexed { index, match ->
Page(index, imageUrl = match.groups["imageUrl"]!!.value)
}.toList()
}

override fun imageUrlParse(response: Response) = ""

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

private fun mangasPageParse(response: Response): Pair<MangasPage, String> {
val dto = response.parseAs<List<WrapperResult<MangaListDto>>>().first()
val data = dto.result?.data?.json ?: return MangasPage(emptyList(), false) to ""

val mangas = data.mangas.map {
SManga.create().apply {
title = it.title
thumbnail_url = it.thumbnailUrl
author = it.author
status = it.status.toStatus()
url = "/title/${it.slug}#${it.id}"
}
}
return MangasPage(mangas, data.nextCursor != null) to (data.nextCursor ?: "")
}

private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}

private fun String.toDate() =
try { dateFormat.parse(this)!!.time } catch (_: Exception) { 0L }

private fun String.toStatus() = when (lowercase()) {
"ongoing" -> SManga.ONGOING
"hiatus" -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}

@SuppressLint("SimpleDateFormat")
companion object {
val MANGA_DETAILS_DESCRIPTION_REGEX = """description":(?<description>"[^"]+)""".toRegex()
val IMAGE_URL_REGEX = """url\\":\\"(?<imageUrl>[^(\\")]+)""".toRegex()

val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")

val latestUpdateDateFormat = SimpleDateFormat(
"EEE MMM dd yyyy HH:mm:ss 'GMT'Z '(Coordinated Universal Time)'",
Locale.ENGLISH,
).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package eu.kanade.tachiyomi.extension.pt.readmangas

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames

@Serializable
class WrapperResult<T>(
val result: Result<T>? = null,
) {
@Serializable
class Result<T>(val `data`: Data<T>)

@Serializable
class Data<T>(val json: T)
}

@Serializable
class MangaListDto(
@JsonNames("items")
val mangas: List<MangaDto>,
val nextCursor: String?,
)

@Serializable
class MangaDto(
val author: String,
@SerialName("coverImage")
val thumbnailUrl: String,
val id: String,
val slug: String,
val status: String,
val title: String,
)

@Serializable
class ChapterListDto(
val currentPage: Int,
val chapters: List<ChapterDto>,
val totalPages: Int,
) {
fun hasNext() = currentPage < totalPages
}

@Serializable
class ChapterDto(
val id: String,
val title: String,
val number: String,
val createdAt: String,
)

0 comments on commit 5c4f754

Please sign in to comment.