diff --git a/src/pt/readmangas/build.gradle b/src/pt/readmangas/build.gradle index 33db403f5..41f761131 100644 --- a/src/pt/readmangas/build.gradle +++ b/src/pt/readmangas/build.gradle @@ -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" diff --git a/src/pt/readmangas/res/mipmap-hdpi/ic_launcher.png b/src/pt/readmangas/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..ccece56dc Binary files /dev/null and b/src/pt/readmangas/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/pt/readmangas/res/mipmap-mdpi/ic_launcher.png b/src/pt/readmangas/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..abeb65a97 Binary files /dev/null and b/src/pt/readmangas/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/pt/readmangas/res/mipmap-xhdpi/ic_launcher.png b/src/pt/readmangas/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..f9acd4971 Binary files /dev/null and b/src/pt/readmangas/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/pt/readmangas/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/readmangas/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..042365f36 Binary files /dev/null and b/src/pt/readmangas/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/pt/readmangas/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/readmangas/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..47ecc5098 Binary files /dev/null and b/src/pt/readmangas/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/pt/readmangas/src/eu/kanade/tachiyomi/extension/pt/readmangas/ReadMangas.kt b/src/pt/readmangas/src/eu/kanade/tachiyomi/extension/pt/readmangas/ReadMangas.kt index ad6d9dc03..e629b54b9 100644 --- a/src/pt/readmangas/src/eu/kanade/tachiyomi/extension/pt/readmangas/ReadMangas.kt +++ b/src/pt/readmangas/src/eu/kanade/tachiyomi/extension/pt/readmangas/ReadMangas.kt @@ -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> { + val chapters = mutableListOf() + var page = 1 + do { + val response = client.newCall(this.chapterListRequest(manga, page++)).execute() + val dto = response + .parseAs>>() + .firstNotNullOf { it.result } + .data.json + chapters += chapterListParse(dto.chapters) + } while (dto.hasNext()) + + return Observable.just(chapters) + } + + private fun chapterListParse(chapters: List): List { + 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 { + 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 { + val dto = response.parseAs>>().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 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":(?"[^"]+)""".toRegex() + val IMAGE_URL_REGEX = """url\\":\\"(?[^(\\")]+)""".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") + } + } } diff --git a/src/pt/readmangas/src/eu/kanade/tachiyomi/extension/pt/readmangas/ReadMangasDto.kt b/src/pt/readmangas/src/eu/kanade/tachiyomi/extension/pt/readmangas/ReadMangasDto.kt new file mode 100644 index 000000000..2eb2ea3c1 --- /dev/null +++ b/src/pt/readmangas/src/eu/kanade/tachiyomi/extension/pt/readmangas/ReadMangasDto.kt @@ -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( + val result: Result? = null, +) { + @Serializable + class Result(val `data`: Data) + + @Serializable + class Data(val json: T) +} + +@Serializable +class MangaListDto( + @JsonNames("items") + val mangas: List, + 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, + val totalPages: Int, +) { + fun hasNext() = currentPage < totalPages +} + +@Serializable +class ChapterDto( + val id: String, + val title: String, + val number: String, + val createdAt: String, +)