-
Notifications
You must be signed in to change notification settings - Fork 543
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Migration * Remove data class
- Loading branch information
Showing
8 changed files
with
333 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
291 changes: 281 additions & 10 deletions
291
src/pt/readmangas/src/eu/kanade/tachiyomi/extension/pt/readmangas/ReadMangas.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} | ||
} | ||
} |
51 changes: 51 additions & 0 deletions
51
src/pt/readmangas/src/eu/kanade/tachiyomi/extension/pt/readmangas/ReadMangasDto.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) |