Add kotlinx.serialization to more sources. (#7391)

This commit is contained in:
Alessandro Jean 2021-06-02 17:28:10 -03:00 committed by GitHub
parent 7cc0764041
commit 10eb030895
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 747 additions and 356 deletions

View File

@ -4,9 +4,9 @@ import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
import eu.kanade.tachiyomi.multisrc.mangasproject.MangasProject import eu.kanade.tachiyomi.multisrc.mangasproject.MangasProject
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class LeitorNet : MangasProject("Leitor.net", "https://leitor.net", "pt-BR") { class LeitorNet : MangasProject("Leitor.net", "https://leitor.net", "pt-BR") {
@ -18,7 +18,7 @@ class LeitorNet : MangasProject("Leitor.net", "https://leitor.net", "pt-BR") {
override val id: Long = 2225174659569980836 override val id: Long = 2225174659569980836
override val client: OkHttpClient = super.client.newBuilder() override val client: OkHttpClient = super.client.newBuilder()
.addInterceptor(RateLimitInterceptor(5, 1, TimeUnit.SECONDS)) .addInterceptor(RateLimitInterceptor(2, 1, TimeUnit.SECONDS))
.build() .build()
/** /**

View File

@ -6,9 +6,9 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class MangaLivre : MangasProject("Mangá Livre", "https://mangalivre.net", "pt-BR") { class MangaLivre : MangasProject("Mangá Livre", "https://mangalivre.net", "pt-BR") {
@ -17,7 +17,7 @@ class MangaLivre : MangasProject("Mangá Livre", "https://mangalivre.net", "pt-B
override val id: Long = 4762777556012432014 override val id: Long = 4762777556012432014
override val client: OkHttpClient = super.client.newBuilder() override val client: OkHttpClient = super.client.newBuilder()
.addInterceptor(RateLimitInterceptor(5, 1, TimeUnit.SECONDS)) .addInterceptor(RateLimitInterceptor(2, 1, TimeUnit.SECONDS))
.build() .build()
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {

View File

@ -2,14 +2,14 @@ package eu.kanade.tachiyomi.extension.pt.toonei
import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
import eu.kanade.tachiyomi.multisrc.mangasproject.MangasProject import eu.kanade.tachiyomi.multisrc.mangasproject.MangasProject
import org.jsoup.nodes.Document
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.jsoup.nodes.Document
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class Toonei : MangasProject("Toonei", "https://toonei.com", "pt-BR") { class Toonei : MangasProject("Toonei", "https://toonei.net", "pt-BR") {
override val client: OkHttpClient = super.client.newBuilder() override val client: OkHttpClient = super.client.newBuilder()
.addInterceptor(RateLimitInterceptor(5, 1, TimeUnit.SECONDS)) .addInterceptor(RateLimitInterceptor(2, 1, TimeUnit.SECONDS))
.build() .build()
override fun getReaderToken(document: Document): String? { override fun getReaderToken(document: Document): String? {

View File

@ -1,10 +1,5 @@
package eu.kanade.tachiyomi.multisrc.mangasproject package eu.kanade.tachiyomi.multisrc.mangasproject
import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.obj
import com.github.salomonbrys.kotson.string
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
@ -14,6 +9,11 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.decodeFromJsonElement
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
@ -23,6 +23,7 @@ import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@ -54,25 +55,26 @@ abstract class MangasProject(
protected val sourceHeaders: Headers by lazy { sourceHeadersBuilder().build() } protected val sourceHeaders: Headers by lazy { sourceHeadersBuilder().build() }
private val json: Json by injectLazy()
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/home/most_read?page=$page&type=", sourceHeaders) return GET("$baseUrl/home/most_read?page=$page&type=", sourceHeaders)
} }
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response): MangasPage {
val result = response.asJsonObject() val result = json.decodeFromString<MangasProjectMostReadDto>(response.body!!.string())
val popularMangas = result["most_read"].array val popularMangas = result.mostRead.map(::popularMangaFromObject)
.map { popularMangaItemParse(it.obj) }
val hasNextPage = response.request.url.queryParameter("page")!!.toInt() < 10 val hasNextPage = response.request.url.queryParameter("page")!!.toInt() < 10
return MangasPage(popularMangas, hasNextPage) return MangasPage(popularMangas, hasNextPage)
} }
private fun popularMangaItemParse(obj: JsonObject) = SManga.create().apply { private fun popularMangaFromObject(serie: MangasProjectSerieDto) = SManga.create().apply {
title = obj["serie_name"].string title = serie.serieName
thumbnail_url = obj["cover"].string thumbnail_url = serie.cover
url = obj["link"].string url = serie.link
} }
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request {
@ -80,20 +82,19 @@ abstract class MangasProject(
} }
override fun latestUpdatesParse(response: Response): MangasPage { override fun latestUpdatesParse(response: Response): MangasPage {
val result = response.asJsonObject() val result = json.decodeFromString<MangasProjectReleasesDto>(response.body!!.string())
val latestMangas = result["releases"].array val latestMangas = result.releases.map(::latestMangaFromObject)
.map { latestMangaItemParse(it.obj) }
val hasNextPage = response.request.url.queryParameter("page")!!.toInt() < 5 val hasNextPage = response.request.url.queryParameter("page")!!.toInt() < 5
return MangasPage(latestMangas, hasNextPage) return MangasPage(latestMangas, hasNextPage)
} }
private fun latestMangaItemParse(obj: JsonObject) = SManga.create().apply { private fun latestMangaFromObject(serie: MangasProjectSerieDto) = SManga.create().apply {
title = obj["name"].string title = serie.name
thumbnail_url = obj["image"].string thumbnail_url = serie.image
url = obj["link"].string url = serie.link
} }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
@ -110,22 +111,22 @@ abstract class MangasProject(
} }
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
val result = response.asJsonObject() val result = json.decodeFromString<MangasProjectSearchDto>(response.body!!.string())
// If "series" have boolean false value, then it doesn't have results. // If "series" have boolean false value, then it doesn't have results.
if (!result["series"]!!.isJsonArray) if (result.series is JsonPrimitive)
return MangasPage(emptyList(), false) return MangasPage(emptyList(), false)
val searchMangas = result["series"].array val searchMangas = json.decodeFromJsonElement<List<MangasProjectSerieDto>>(result.series)
.map { searchMangaItemParse(it.obj) } .map(::searchMangaFromObject)
return MangasPage(searchMangas, false) return MangasPage(searchMangas, false)
} }
private fun searchMangaItemParse(obj: JsonObject) = SManga.create().apply { private fun searchMangaFromObject(serie: MangasProjectSerieDto) = SManga.create().apply {
title = obj["name"].string title = serie.name
thumbnail_url = obj["cover"].string thumbnail_url = serie.cover
url = obj["link"].string url = serie.link
} }
override fun mangaDetailsParse(response: Response): SManga { override fun mangaDetailsParse(response: Response): SManga {
@ -202,41 +203,41 @@ abstract class MangasProject(
var page = 1 var page = 1
var chapterListRequest = chapterListRequestPaginated(mangaUrl, mangaId, page) var chapterListRequest = chapterListRequestPaginated(mangaUrl, mangaId, page)
var result = client.newCall(chapterListRequest).execute().asJsonObject() var result = client.newCall(chapterListRequest).execute().let {
json.decodeFromString<MangasProjectChapterListDto>(it.body!!.string())
}
if (!result["chapters"]!!.isJsonArray) if (result.chapters is JsonPrimitive)
return emptyList() return emptyList()
val chapters = mutableListOf<SChapter>() val chapters = mutableListOf<SChapter>()
while (result["chapters"]!!.isJsonArray) { while (result.chapters is JsonArray) {
chapters += result["chapters"].array chapters += json.decodeFromJsonElement<List<MangasProjectChapterDto>>(result.chapters)
.flatMap { chapterListItemParse(it.obj) } .flatMap(::chaptersFromObject)
.toMutableList() .toMutableList()
chapterListRequest = chapterListRequestPaginated(mangaUrl, mangaId, ++page) chapterListRequest = chapterListRequestPaginated(mangaUrl, mangaId, ++page)
result = client.newCall(chapterListRequest).execute().asJsonObject() result = client.newCall(chapterListRequest).execute().let {
json.decodeFromString(it.body!!.string())
}
} }
return chapters return chapters
} }
private fun chapterListItemParse(obj: JsonObject): List<SChapter> { private fun chaptersFromObject(chapter: MangasProjectChapterDto): List<SChapter> {
val chapterName = obj["chapter_name"]!!.string return chapter.releases.values.map { release ->
return obj["releases"].obj.entrySet().map {
val release = it.value.obj
SChapter.create().apply { SChapter.create().apply {
name = "Cap. ${obj["number"].string}" + name = "Cap. ${chapter.number}" +
(if (chapterName == "") "" else " - $chapterName") (if (chapter.name.isEmpty()) "" else " - ${chapter.name}")
date_upload = obj["date_created"].string.substringBefore("T").toDate() date_upload = chapter.dateCreated.substringBefore("T").toDate()
scanlator = release["scanlators"]!!.array scanlator = release.scanlators
.mapNotNull { scanObj -> scanObj.obj["name"].string.ifEmpty { null } } .mapNotNull { scan -> scan.name.ifEmpty { null } }
.sorted() .sorted()
.joinToString() .joinToString()
url = release["link"].string url = release.link
chapter_number = obj["number"].string.toFloatOrNull() ?: -1f chapter_number = chapter.number.toFloatOrNull() ?: -1f
} }
} }
} }
@ -269,11 +270,13 @@ abstract class MangasProject(
val chapterUrl = getChapterUrl(response) val chapterUrl = getChapterUrl(response)
val apiRequest = pageListApiRequest(chapterUrl, readerToken) val apiRequest = pageListApiRequest(chapterUrl, readerToken)
val apiResponse = client.newCall(apiRequest).execute().asJsonObject() val apiResponse = client.newCall(apiRequest).execute().let {
json.decodeFromString<MangasProjectReaderDto>(it.body!!.string())
}
return apiResponse["images"].array return apiResponse.images
.filter { it.string.startsWith("http") } .filter { it.startsWith("http") }
.mapIndexed { i, obj -> Page(i, chapterUrl, obj.string) } .mapIndexed { i, imageUrl -> Page(i, chapterUrl, imageUrl) }
} }
open fun getChapterUrl(response: Response): String { open fun getChapterUrl(response: Response): String {
@ -299,14 +302,6 @@ abstract class MangasProject(
return GET(page.imageUrl!!, newHeaders) return GET(page.imageUrl!!, newHeaders)
} }
private fun Response.asJsonObject(): JsonObject {
if (!isSuccessful) {
throw Exception("HTTP error $code")
}
return JsonParser.parseString(body!!.string()).obj
}
private fun String.toDate(): Long { private fun String.toDate(): Long {
return try { return try {
DATE_FORMATTER.parse(this)?.time ?: 0L DATE_FORMATTER.parse(this)?.time ?: 0L
@ -321,7 +316,7 @@ abstract class MangasProject(
private const val ACCEPT_JSON = "application/json, text/javascript, */*; q=0.01" private const val ACCEPT_JSON = "application/json, text/javascript, */*; q=0.01"
private const val ACCEPT_LANGUAGE = "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7,es;q=0.6,gl;q=0.5" private const val ACCEPT_LANGUAGE = "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7,es;q=0.6,gl;q=0.5"
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36" "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36"
private val DATE_FORMATTER by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) } private val DATE_FORMATTER by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) }

View File

@ -0,0 +1,58 @@
package eu.kanade.tachiyomi.multisrc.mangasproject
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
@Serializable
data class MangasProjectMostReadDto(
@SerialName("most_read") val mostRead: List<MangasProjectSerieDto> = emptyList()
)
@Serializable
data class MangasProjectReleasesDto(
val releases: List<MangasProjectSerieDto> = emptyList()
)
@Serializable
data class MangasProjectSearchDto(
val series: JsonElement
)
@Serializable
data class MangasProjectSerieDto(
val cover: String = "",
val image: String = "",
val link: String,
val name: String = "",
@SerialName("serie_name") val serieName: String = ""
)
@Serializable
data class MangasProjectChapterListDto(
val chapters: JsonElement
)
@Serializable
data class MangasProjectChapterDto(
@SerialName("date_created") val dateCreated: String,
@SerialName("chapter_name") val name: String,
val number: String,
val releases: Map<String, MangasProjectChapterReleaseDto> = emptyMap()
)
@Serializable
data class MangasProjectChapterReleaseDto(
val link: String,
val scanlators: List<MangasProjectScanlatorDto> = emptyList()
)
@Serializable
data class MangasProjectScanlatorDto(
val name: String
)
@Serializable
data class MangasProjectReaderDto(
val images: List<String> = emptyList()
)

View File

@ -9,12 +9,12 @@ class MangasProjectGenerator : ThemeSourceGenerator {
override val themeClass = "MangasProject" override val themeClass = "MangasProject"
override val baseVersionCode: Int = 2 override val baseVersionCode: Int = 3
override val sources = listOf( override val sources = listOf(
SingleLang("Leitor.net", "https://leitor.net", "pt-BR", className = "LeitorNet", isNsfw = true, overrideVersionCode = 1), SingleLang("Leitor.net", "https://leitor.net", "pt-BR", className = "LeitorNet", isNsfw = true, overrideVersionCode = 1),
SingleLang("Mangá Livre", "https://mangalivre.net", "pt-BR", className = "MangaLivre", isNsfw = true, overrideVersionCode = 1), SingleLang("Mangá Livre", "https://mangalivre.net", "pt-BR", className = "MangaLivre", isNsfw = true, overrideVersionCode = 1),
SingleLang("Toonei", "https://toonei.com", "pt-BR", isNsfw = true, overrideVersionCode = 1), SingleLang("Toonei", "https://toonei.net", "pt-BR", isNsfw = true, overrideVersionCode = 1),
) )
companion object { companion object {

View File

@ -63,6 +63,7 @@ interface ThemeSourceGenerator {
// THIS FILE IS AUTO-GENERATED; DO NOT EDIT // THIS FILE IS AUTO-GENERATED; DO NOT EDIT
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext { ext {
extName = '${source.name}' extName = '${source.name}'

View File

@ -1,11 +1,12 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext { ext {
extName = 'VIZ Shonen Jump' extName = 'VIZ Shonen Jump'
pkgNameSuffix = 'en.vizshonenjump' pkgNameSuffix = 'en.vizshonenjump'
extClass = '.VizShonenJump' extClass = '.VizShonenJump'
extVersionCode = 10 extVersionCode = 11
libVersion = '1.2' libVersion = '1.2'
} }

View File

@ -1,12 +1,5 @@
package eu.kanade.tachiyomi.extension.en.vizshonenjump package eu.kanade.tachiyomi.extension.en.vizshonenjump
import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.int
import com.github.salomonbrys.kotson.nullInt
import com.github.salomonbrys.kotson.nullObj
import com.github.salomonbrys.kotson.nullString
import com.github.salomonbrys.kotson.obj
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
@ -16,6 +9,13 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.int
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
@ -26,6 +26,7 @@ import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@ -53,6 +54,8 @@ class VizShonenJump : ParsedHttpSource() {
.add("Origin", baseUrl) .add("Origin", baseUrl)
.add("Referer", "$baseUrl/shonenjump") .add("Referer", "$baseUrl/shonenjump")
private val json: Json by injectLazy()
private var mangaList: List<SManga>? = null private var mangaList: List<SManga>? = null
private var loggedIn: Boolean? = null private var loggedIn: Boolean? = null
@ -318,11 +321,14 @@ class VizShonenJump : ParsedHttpSource() {
.toString() .toString()
val authCheckRequest = GET(authCheckUrl, authCheckHeaders) val authCheckRequest = GET(authCheckUrl, authCheckHeaders)
val authCheckResponse = chain.proceed(authCheckRequest) val authCheckResponse = chain.proceed(authCheckRequest)
val authCheckJson = JsonParser.parseString(authCheckResponse.body!!.string()).obj val authCheckJson = Json.parseToJsonElement(authCheckResponse.body!!.string()).jsonObject
authCheckResponse.close() authCheckResponse.close()
if (authCheckJson["ok"].int == 1 && authCheckJson["archive_info"]["ok"].int == 1) { if (
authCheckJson["ok"]!!.jsonPrimitive.int == 1 &&
authCheckJson["archive_info"]!!.jsonObject["ok"]!!.jsonPrimitive.int == 1
) {
val newChapterUrl = chain.request().url.newBuilder() val newChapterUrl = chain.request().url.newBuilder()
.removeAllQueryParameters("locked") .removeAllQueryParameters("locked")
.build() .build()
@ -334,16 +340,17 @@ class VizShonenJump : ParsedHttpSource() {
} }
if ( if (
authCheckJson["archive_info"]["err"].isJsonObject && authCheckJson["archive_info"]!!.jsonObject["err"] is JsonObject &&
authCheckJson["archive_info"]["err"]["code"].nullInt == 4 && authCheckJson["archive_info"]!!.jsonObject["err"]!!.jsonObject["code"]?.jsonPrimitive?.intOrNull == 4 &&
loggedIn == true loggedIn == true
) { ) {
throw Exception(SESSION_EXPIRED) throw Exception(SESSION_EXPIRED)
} }
val errorMessage = authCheckJson["archive_info"]["err"].nullObj?.get("msg")?.nullString val errorMessage = authCheckJson["archive_info"]!!.jsonObject["err"]?.jsonObject
?.get("msg")?.jsonPrimitive?.contentOrNull ?: AUTH_CHECK_FAILED
throw Exception(errorMessage ?: AUTH_CHECK_FAILED) throw Exception(errorMessage)
} }
private fun String.toDate(): Long { private fun String.toDate(): Long {
@ -363,7 +370,7 @@ class VizShonenJump : ParsedHttpSource() {
SimpleDateFormat("MMMM d, yyyy", Locale.ENGLISH) SimpleDateFormat("MMMM d, yyyy", Locale.ENGLISH)
} }
private const val COUNTRY_NOT_SUPPORTED = "Your country is not supported, try using a VPN." private const val COUNTRY_NOT_SUPPORTED = "Your country is not supported by the service."
private const val SESSION_EXPIRED = "Your session has expired, please log in through WebView again." private const val SESSION_EXPIRED = "Your session has expired, please log in through WebView again."
private const val AUTH_CHECK_FAILED = "Something went wrong in the auth check." private const val AUTH_CHECK_FAILED = "Something went wrong in the auth check."

View File

@ -1,11 +1,12 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext { ext {
extName = 'HQ Now!' extName = 'HQ Now!'
pkgNameSuffix = 'pt.hqnow' pkgNameSuffix = 'pt.hqnow'
extClass = '.HQNow' extClass = '.HQNow'
extVersionCode = 3 extVersionCode = 4
libVersion = '1.2' libVersion = '1.2'
} }

View File

@ -1,200 +1,364 @@
package eu.kanade.tachiyomi.extension.pt.hqnow package eu.kanade.tachiyomi.extension.pt.hqnow
import com.github.salomonbrys.kotson.fromJson import eu.kanade.tachiyomi.lib.ratelimit.SpecificHostRateLimitInterceptor
import com.github.salomonbrys.kotson.get import eu.kanade.tachiyomi.network.GET
import com.google.gson.Gson
import com.google.gson.JsonObject
import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import java.util.concurrent.TimeUnit import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.Normalizer
import java.util.Locale
class HQNow : HttpSource() { class HQNow : HttpSource() {
override val name = "HQ Now!" override val name = "HQ Now!"
// Website is http://www.hq-now.com override val baseUrl = "http://www.hq-now.com"
override val baseUrl = "http://admin.hq-now.com/graphql"
override val lang = "pt-BR" override val lang = "pt-BR"
override val supportsLatest = true override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient.newBuilder() override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.addInterceptor(RateLimitInterceptor(1, 1, TimeUnit.SECONDS)) .addInterceptor(SpecificHostRateLimitInterceptor(GRAPHQL_URL.toHttpUrl(), 1))
.addInterceptor(SpecificHostRateLimitInterceptor(STATIC_URL.toHttpUrl(), 2))
.build() .build()
private val gson = Gson() private val json: Json by injectLazy()
private val jsonHeaders = headersBuilder().add("content-type", "application/json").build() private fun genericComicBookFromObject(comicBook: HqNowComicBookDto): SManga =
SManga.create().apply {
private fun mangaFromResponse(response: Response, selector: String, coversAvailable: Boolean = true): List<SManga> { title = comicBook.name
return gson.fromJson<JsonObject>(response.body!!.string())["data"][selector].asJsonArray url = "/hq/${comicBook.id}/${comicBook.name.toSlug()}"
.map { thumbnail_url = comicBook.cover
SManga.create().apply { }
url = it["id"].asString
title = it["name"].asString
if (coversAvailable) thumbnail_url = it["hqCover"].asString
}
}
}
// Popular
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
return POST(baseUrl, jsonHeaders, "{\"operationName\":\"getHqsByFilters\",\"variables\":{\"orderByViews\":true,\"loadCovers\":true,\"limit\":30},\"query\":\"query getHqsByFilters(\$orderByViews: Boolean, \$limit: Int, \$publisherId: Int, \$loadCovers: Boolean) {\\n getHqsByFilters(orderByViews: \$orderByViews, limit: \$limit, publisherId: \$publisherId, loadCovers: \$loadCovers) {\\n id\\n name\\n editoraId\\n status\\n publisherName\\n hqCover\\n synopsis\\n updatedAt\\n }\\n}\\n\"}".toRequestBody(null)) val query = buildQuery {
"""
query getHqsByFilters(
%orderByViews: Boolean,
%limit: Int,
%publisherId: Int,
%loadCovers: Boolean
) {
getHqsByFilters(
orderByViews: %orderByViews,
limit: %limit,
publisherId: %publisherId,
loadCovers: %loadCovers
) {
id
name
editoraId
status
publisherName
hqCover
synopsis
updatedAt
}
}
""".trimIndent()
}
val payload = buildJsonObject {
put("operationName", "getHqsByFilters")
put("query", query)
putJsonObject("variables") {
put("orderByViews", true)
put("loadCovers", true)
put("limit", 300)
}
}
val body = payload.toString().toRequestBody(JSON_MEDIA_TYPE)
val newHeaders = headersBuilder()
.add("Content-Length", body.contentLength().toString())
.add("Content-Type", body.contentType().toString())
.build()
return POST(GRAPHQL_URL, newHeaders, body)
} }
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response): MangasPage {
return MangasPage(mangaFromResponse(response, "getHqsByFilters"), false) val result = json.parseToJsonElement(response.body!!.string()).jsonObject
val comicList = result["data"]!!.jsonObject["getHqsByFilters"]!!
.let { json.decodeFromJsonElement<List<HqNowComicBookDto>>(it) }
.map(::genericComicBookFromObject)
return MangasPage(comicList, hasNextPage = false)
} }
// Latest
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request {
return POST(baseUrl, jsonHeaders, "{\"operationName\":\"getRecentlyUpdatedHqs\",\"variables\":{},\"query\":\"query getRecentlyUpdatedHqs {\\n getRecentlyUpdatedHqs {\\n name\\n hqCover\\n synopsis\\n id\\n updatedAt\\n updatedChapters\\n }\\n}\\n\"}".toRequestBody(null)) val query = buildQuery {
"""
query getRecentlyUpdatedHqs {
getRecentlyUpdatedHqs {
name
hqCover
synopsis
id
updatedAt
updatedChapters
}
}
""".trimIndent()
}
val payload = buildJsonObject {
put("operationName", "getRecentlyUpdatedHqs")
put("query", query)
}
val body = payload.toString().toRequestBody(JSON_MEDIA_TYPE)
val newHeaders = headersBuilder()
.add("Content-Length", body.contentLength().toString())
.add("Content-Type", body.contentType().toString())
.build()
return POST(GRAPHQL_URL, newHeaders, body)
} }
override fun latestUpdatesParse(response: Response): MangasPage { override fun latestUpdatesParse(response: Response): MangasPage {
return MangasPage(mangaFromResponse(response, "getRecentlyUpdatedHqs"), false) val result = json.parseToJsonElement(response.body!!.string()).jsonObject
val comicList = result["data"]!!.jsonObject["getRecentlyUpdatedHqs"]!!
.let { json.decodeFromJsonElement<List<HqNowComicBookDto>>(it) }
.map(::genericComicBookFromObject)
return MangasPage(comicList, hasNextPage = false)
} }
// Search
private var queryIsTitle = true
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return if (query.isNotBlank()) { val queryStr = buildQuery {
queryIsTitle = true """
POST(baseUrl, jsonHeaders, "{\"operationName\":\"getHqsByName\",\"variables\":{\"name\":\"$query\"},\"query\":\"query getHqsByName(\$name: String!) {\\n getHqsByName(name: \$name) {\\n id\\n name\\n editoraId\\n status\\n publisherName\\n impressionsCount\\n }\\n}\\n\"}".toRequestBody(null)) query getHqsByName(%name: String!) {
} else { getHqsByName(name: %name) {
queryIsTitle = false id
var searchLetter = "" name
editoraId
filters.forEach { filter -> status
when (filter) { publisherName
is LetterFilter -> { impressionsCount
searchLetter = filter.toUriPart()
} }
} }
} """.trimIndent()
POST(baseUrl, jsonHeaders, "{\"operationName\":\"getHqsByNameStartingLetter\",\"variables\":{\"letter\":\"$searchLetter-$searchLetter\"},\"query\":\"query getHqsByNameStartingLetter(\$letter: String!) {\\n getHqsByNameStartingLetter(letter: \$letter) {\\n id\\n name\\n editoraId\\n status\\n publisherName\\n impressionsCount\\n }\\n}\\n\"}".toRequestBody(null))
} }
val payload = buildJsonObject {
put("operationName", "getHqsByName")
put("query", queryStr)
putJsonObject("variables") {
put("name", query)
}
}
val body = payload.toString().toRequestBody(JSON_MEDIA_TYPE)
val newHeaders = headersBuilder()
.add("Content-Length", body.contentLength().toString())
.add("Content-Type", body.contentType().toString())
.build()
return POST(GRAPHQL_URL, newHeaders, body)
} }
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
return MangasPage(mangaFromResponse(response, if (queryIsTitle) "getHqsByName" else "getHqsByNameStartingLetter", false), false) val result = json.parseToJsonElement(response.body!!.string()).jsonObject
val comicList = result["data"]!!.jsonObject["getHqsByName"]!!
.let { json.decodeFromJsonElement<List<HqNowComicBookDto>>(it) }
.map(::genericComicBookFromObject)
return MangasPage(comicList, hasNextPage = false)
} }
// Details // Workaround to allow "Open in browser" use the real URL.
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
override fun mangaDetailsRequest(manga: SManga): Request { return client.newCall(mangaDetailsApiRequest(manga))
return POST(baseUrl, jsonHeaders, "{\"operationName\":\"getHqsById\",\"variables\":{\"id\":${manga.url}},\"query\":\"query getHqsById(\$id: Int!) {\\n getHqsById(id: \$id) {\\n id\\n name\\n synopsis\\n editoraId\\n status\\n publisherName\\n hqCover\\n impressionsCount\\n capitulos {\\n name\\n id\\n number\\n }\\n }\\n}\\n\"}".toRequestBody(null)) .asObservableSuccess()
} .map { response ->
mangaDetailsParse(response).apply { initialized = true }
override fun mangaDetailsParse(response: Response): SManga {
return gson.fromJson<JsonObject>(response.body!!.string())["data"]["getHqsById"][0]
.let {
SManga.create().apply {
title = it["name"].asString
thumbnail_url = it["hqCover"].asString
description = it["synopsis"].asString
author = it["publisherName"].asString
status = when (it["status"].asString) {
"Concluído" -> SManga.COMPLETED
"Em Andamento" -> SManga.ONGOING
else -> SManga.UNKNOWN
}
}
} }
} }
// Chapters private fun mangaDetailsApiRequest(manga: SManga): Request {
val comicBookId = manga.url.substringAfter("/hq/").substringBefore("/")
override fun chapterListRequest(manga: SManga): Request { val query = buildQuery {
return mangaDetailsRequest(manga) """
} query getHqsById(%id: Int!) {
getHqsById(id: %id) {
override fun chapterListParse(response: Response): List<SChapter> { id
return gson.fromJson<JsonObject>(response.body!!.string())["data"]["getHqsById"][0]["capitulos"].asJsonArray name
.map { synopsis
SChapter.create().apply { editoraId
url = it["id"].asString status
name = it["name"].asString.let { jsonName -> publisherName
if (jsonName.isNotEmpty()) jsonName.trim() else "Capitulo: " + it["number"].asString hqCover
impressionsCount
capitulos {
name
id
number
}
} }
} }
}.reversed() """.trimIndent()
}
val payload = buildJsonObject {
put("operationName", "getHqsById")
put("query", query)
putJsonObject("variables") {
put("id", comicBookId.toInt())
}
}
val body = payload.toString().toRequestBody(JSON_MEDIA_TYPE)
val newHeaders = headersBuilder()
.add("Content-Length", body.contentLength().toString())
.add("Content-Type", body.contentType().toString())
.build()
return POST(GRAPHQL_URL, newHeaders, body)
} }
override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply {
val result = json.parseToJsonElement(response.body!!.string()).jsonObject
val comicBook = result["data"]!!.jsonObject["getHqsById"]!!.jsonArray[0].jsonObject
.let { json.decodeFromJsonElement<HqNowComicBookDto>(it) }
title = comicBook.name
thumbnail_url = comicBook.cover
description = comicBook.synopsis.orEmpty()
author = comicBook.publisherName.orEmpty()
status = comicBook.status.orEmpty().toStatus()
}
override fun chapterListRequest(manga: SManga): Request = mangaDetailsApiRequest(manga)
override fun chapterListParse(response: Response): List<SChapter> {
val result = json.parseToJsonElement(response.body!!.string()).jsonObject
val comicBook = result["data"]!!.jsonObject["getHqsById"]!!.jsonArray[0].jsonObject
.let { json.decodeFromJsonElement<HqNowComicBookDto>(it) }
return comicBook.chapters
.map { chapter -> chapterFromObject(chapter, comicBook) }
.reversed()
}
private fun chapterFromObject(chapter: HqNowChapterDto, comicBook: HqNowComicBookDto): SChapter =
SChapter.create().apply {
name = "#" + chapter.number +
(if (chapter.name.isNotEmpty()) " - " + chapter.name else "")
url = "/hq-reader/${comicBook.id}/${comicBook.name.toSlug()}" +
"/chapter/${chapter.id}/page/1"
}
// Pages // Pages
override fun pageListRequest(chapter: SChapter): Request { override fun pageListRequest(chapter: SChapter): Request {
return POST(baseUrl, jsonHeaders, "{\"operationName\":\"getChapterById\",\"variables\":{\"chapterId\":${chapter.url}},\"query\":\"query getChapterById(\$chapterId: Int!) {\\n getChapterById(chapterId: \$chapterId) {\\n name\\n number\\n oneshot\\n pictures {\\n pictureUrl\\n }\\n hq {\\n id\\n name\\n capitulos {\\n id\\n number\\n }\\n }\\n }\\n}\\n\"}".toRequestBody(null)) val chapterId = chapter.url.substringAfter("/chapter/").substringBefore("/")
val query = buildQuery {
"""
query getChapterById(%chapterId: Int!) {
getChapterById(chapterId: %chapterId) {
name
number
oneshot
pictures {
pictureUrl
}
}
}
""".trimIndent()
}
val payload = buildJsonObject {
put("operationName", "getChapterById")
put("query", query)
putJsonObject("variables") {
put("chapterId", chapterId.toInt())
}
}
val body = payload.toString().toRequestBody(JSON_MEDIA_TYPE)
val newHeaders = headersBuilder()
.add("Content-Length", body.contentLength().toString())
.add("Content-Type", body.contentType().toString())
.build()
return POST(GRAPHQL_URL, newHeaders, body)
} }
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
return gson.fromJson<JsonObject>(response.body!!.string())["data"]["getChapterById"]["pictures"].asJsonArray val result = json.parseToJsonElement(response.body!!.string()).jsonObject
.mapIndexed { i, json -> Page(i, "", json["pictureUrl"].asString) }
val chapterDto = result["data"]!!.jsonObject["getChapterById"]!!
.let { json.decodeFromJsonElement<HqNowChapterDto>(it) }
return chapterDto.pictures.mapIndexed { i, page ->
Page(i, baseUrl, page.pictureUrl)
}
} }
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not used") override fun imageUrlParse(response: Response): String = ""
// Filters override fun imageRequest(page: Page): Request {
val newHeaders = headersBuilder()
.set("Referer", page.url)
.build()
override fun getFilterList() = FilterList( return GET(page.imageUrl!!, newHeaders)
Filter.Header("NOTA: Ignorado se estiver usando"), }
Filter.Header("a pesquisa de texto!"),
Filter.Separator(),
LetterFilter()
)
private class LetterFilter : UriPartFilter( private fun buildQuery(queryAction: () -> String) = queryAction().replace("%", "$")
"Letra",
arrayOf(
Pair("---", "<Selecione>"),
Pair("a", "A"),
Pair("b", "B"),
Pair("c", "C"),
Pair("d", "D"),
Pair("e", "E"),
Pair("f", "F"),
Pair("g", "G"),
Pair("h", "H"),
Pair("i", "I"),
Pair("j", "J"),
Pair("k", "K"),
Pair("l", "L"),
Pair("m", "M"),
Pair("n", "N"),
Pair("o", "O"),
Pair("p", "P"),
Pair("q", "Q"),
Pair("r", "R"),
Pair("s", "S"),
Pair("t", "T"),
Pair("u", "U"),
Pair("v", "V"),
Pair("w", "W"),
Pair("x", "X"),
Pair("y", "Y"),
Pair("z", "Z")
)
)
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) : private fun String.toSlug(): String {
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray()) { return Normalizer
fun toUriPart() = vals[state].first .normalize(this, Normalizer.Form.NFD)
.replace("[^\\p{ASCII}]".toRegex(), "")
.replace("[^a-zA-Z0-9\\s]+".toRegex(), "").trim()
.replace("\\s+".toRegex(), "-")
.toLowerCase(Locale("pt", "BR"))
}
private fun String.toStatus(): Int = when (this) {
"Concluído" -> SManga.COMPLETED
"Em Andamento" -> SManga.ONGOING
else -> SManga.UNKNOWN
}
companion object {
private const val STATIC_URL = "http://static.hq-now.com/"
private const val GRAPHQL_URL = "http://admin.hq-now.com/graphql"
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
} }
} }

View File

@ -0,0 +1,28 @@
package eu.kanade.tachiyomi.extension.pt.hqnow
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class HqNowComicBookDto(
@SerialName("capitulos") val chapters: List<HqNowChapterDto> = emptyList(),
@SerialName("hqCover") val cover: String? = "",
val id: Int,
val name: String,
val publisherName: String? = "",
val status: String? = "",
val synopsis: String? = ""
)
@Serializable
data class HqNowChapterDto(
val id: Int = 0,
val name: String,
val number: String,
val pictures: List<HqNowPageDto> = emptyList()
)
@Serializable
data class HqNowPageDto(
val pictureUrl: String
)

View File

@ -1,11 +1,12 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext { ext {
extName = 'MangaTube' extName = 'MangaTube'
pkgNameSuffix = 'pt.mangatube' pkgNameSuffix = 'pt.mangatube'
extClass = '.MangaTube' extClass = '.MangaTube'
extVersionCode = 1 extVersionCode = 2
libVersion = '1.2' libVersion = '1.2'
} }

View File

@ -1,13 +1,5 @@
package eu.kanade.tachiyomi.extension.pt.mangatube package eu.kanade.tachiyomi.extension.pt.mangatube
import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.int
import com.github.salomonbrys.kotson.nullArray
import com.github.salomonbrys.kotson.obj
import com.github.salomonbrys.kotson.string
import com.google.gson.JsonElement
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
@ -18,6 +10,12 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.floatOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
@ -27,6 +25,7 @@ import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@ -58,6 +57,8 @@ class MangaTube : HttpSource() {
private val apiHeaders: Headers by lazy { apiHeadersBuilder().build() } private val apiHeaders: Headers by lazy { apiHeadersBuilder().build() }
private val json: Json by injectLazy()
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
return GET(baseUrl, headers) return GET(baseUrl, headers)
} }
@ -91,20 +92,20 @@ class MangaTube : HttpSource() {
} }
override fun latestUpdatesParse(response: Response): MangasPage { override fun latestUpdatesParse(response: Response): MangasPage {
val result = response.asJson().obj val result = json.decodeFromString<MangaTubeLatestDto>(response.body!!.string())
val latestMangas = result["releases"].array val latestMangas = result.releases
.map(::latestUpdatesFromObject) .map(::latestUpdatesFromObject)
val hasNextPage = result["page"].string.toInt() < result["total_page"].int val hasNextPage = result.page.toInt() < result.totalPage
return MangasPage(latestMangas, hasNextPage) return MangasPage(latestMangas, hasNextPage)
} }
private fun latestUpdatesFromObject(obj: JsonElement) = SManga.create().apply { private fun latestUpdatesFromObject(release: MangaTubeReleaseDto) = SManga.create().apply {
title = obj["name"].string title = release.name
thumbnail_url = obj["image"].string thumbnail_url = release.image
url = obj["link"].string url = release.link
} }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
@ -117,18 +118,17 @@ class MangaTube : HttpSource() {
} }
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
val result = response.asJson().obj val result = json.decodeFromString<Map<String, MangaTubeTitleDto>>(response.body!!.string())
val searchResults = result.entrySet() val searchResults = result.values.map(::searchMangaFromObject)
.map { searchMangaFromObject(it.value) }
return MangasPage(searchResults, hasNextPage = false) return MangasPage(searchResults, hasNextPage = false)
} }
private fun searchMangaFromObject(obj: JsonElement) = SManga.create().apply { private fun searchMangaFromObject(manga: MangaTubeTitleDto) = SManga.create().apply {
title = obj["title"].string title = manga.title
thumbnail_url = obj["img"].string thumbnail_url = manga.image
setUrlWithoutDomain(obj["url"].string) setUrlWithoutDomain(manga.url)
} }
override fun mangaDetailsParse(response: Response): SManga { override fun mangaDetailsParse(response: Response): SManga {
@ -154,6 +154,7 @@ class MangaTube : HttpSource() {
val url = "$baseUrl/jsons/series/chapters_list.json".toHttpUrlOrNull()!!.newBuilder() val url = "$baseUrl/jsons/series/chapters_list.json".toHttpUrlOrNull()!!.newBuilder()
.addQueryParameter("page", page.toString()) .addQueryParameter("page", page.toString())
.addQueryParameter("order", "desc")
.addQueryParameter("id_s", mangaId) .addQueryParameter("id_s", mangaId)
.toString() .toString()
@ -163,24 +164,26 @@ class MangaTube : HttpSource() {
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val mangaUrl = response.request.header("Referer")!!.substringAfter(baseUrl) val mangaUrl = response.request.header("Referer")!!.substringAfter(baseUrl)
var result = response.asJson().obj var result = json.decodeFromString<MangaTubePaginatedChaptersDto>(response.body!!.string())
if (result["chapters"].nullArray == null || result["chapters"].array.size() == 0) { if (result.chapters.isNullOrEmpty()) {
return emptyList() return emptyList()
} }
val chapters = result["chapters"].array val chapters = result.chapters!!
.map(::chapterFromObject) .map(::chapterFromObject)
.toMutableList() .toMutableList()
var page = result["pagina"].int + 1 var page = result.page + 1
val lastPage = result["total_pags"].int val lastPage = result.totalPages
while (++page <= lastPage) { while (++page <= lastPage) {
val nextPageRequest = chapterListPaginatedRequest(mangaUrl, page) val nextPageRequest = chapterListPaginatedRequest(mangaUrl, page)
result = client.newCall(nextPageRequest).execute().asJson().obj result = client.newCall(nextPageRequest).execute().let {
json.decodeFromString(it.body!!.string())
}
chapters += result["chapters"].array chapters += result.chapters!!
.map(::chapterFromObject) .map(::chapterFromObject)
.toMutableList() .toMutableList()
} }
@ -188,12 +191,12 @@ class MangaTube : HttpSource() {
return chapters return chapters
} }
private fun chapterFromObject(obj: JsonElement): SChapter = SChapter.create().apply { private fun chapterFromObject(chapter: MangaTubeChapterDto): SChapter = SChapter.create().apply {
name = "Cap. " + (if (obj["number"].string == "false") "0" else obj["number"].string) + name = "Cap. " + (if (chapter.number.booleanOrNull != null) "0" else chapter.number.content) +
(if (obj["chapter_name"].asJsonPrimitive.isString) " - " + obj["chapter_name"].string else "") (if (chapter.name.isString) " - " + chapter.name.content else "")
chapter_number = obj["number"].string.toFloatOrNull() ?: -1f chapter_number = chapter.number.floatOrNull ?: -1f
date_upload = obj["date_created"].string.substringBefore("T").toDate() date_upload = chapter.dateCreated.substringBefore("T").toDate()
setUrlWithoutDomain(obj["link"].string) setUrlWithoutDomain(chapter.link)
} }
private fun pageListApiRequest(chapterUrl: String, serieId: String, token: String): Request { private fun pageListApiRequest(chapterUrl: String, serieId: String, token: String): Request {
@ -220,11 +223,13 @@ class MangaTube : HttpSource() {
val token = TOKEN_REGEX.find(apiParams)!!.groupValues[1] val token = TOKEN_REGEX.find(apiParams)!!.groupValues[1]
val apiRequest = pageListApiRequest(chapterUrl, serieId, token) val apiRequest = pageListApiRequest(chapterUrl, serieId, token)
val apiResponse = client.newCall(apiRequest).execute().asJson().obj val apiResponse = client.newCall(apiRequest).execute().let {
json.decodeFromString<MangaTubeReaderDto>(it.body!!.string())
}
return apiResponse["images"].array return apiResponse.images
.filter { it["url"].string.startsWith("http") } .filter { it.url.startsWith("http") }
.mapIndexed { i, obj -> Page(i, chapterUrl, obj["url"].string) } .mapIndexed { i, page -> Page(i, chapterUrl, page.url) }
} }
override fun fetchImageUrl(page: Page): Observable<String> = Observable.just(page.imageUrl!!) override fun fetchImageUrl(page: Page): Observable<String> = Observable.just(page.imageUrl!!)
@ -248,10 +253,11 @@ class MangaTube : HttpSource() {
val apiParams = document.select("script:containsData(pAPI)").first()!!.data() val apiParams = document.select("script:containsData(pAPI)").first()!!.data()
.substringAfter("pAPI = ") .substringAfter("pAPI = ")
.substringBeforeLast(";") .substringBeforeLast(";")
.let { JsonParser.parseString(it) } .let { json.parseToJsonElement(it) }
.jsonObject
val newUrl = chain.request().url.newBuilder() val newUrl = chain.request().url.newBuilder()
.addQueryParameter("nonce", apiParams["nonce"].string) .addQueryParameter("nonce", apiParams["nonce"]!!.jsonPrimitive.content)
.build() .build()
val newRequest = chain.request().newBuilder() val newRequest = chain.request().newBuilder()
@ -272,8 +278,6 @@ class MangaTube : HttpSource() {
} }
} }
private fun Response.asJson(): JsonElement = JsonParser.parseString(body!!.string())
companion object { companion object {
private const val ACCEPT = "application/json, text/plain, */*" private const val ACCEPT = "application/json, text/plain, */*"
private const val ACCEPT_HTML = "text/html,application/xhtml+xml,application/xml;q=0.9," + private const val ACCEPT_HTML = "text/html,application/xhtml+xml,application/xml;q=0.9," +

View File

@ -0,0 +1,51 @@
package eu.kanade.tachiyomi.extension.pt.mangatube
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonPrimitive
@Serializable
data class MangaTubeLatestDto(
val page: String,
val releases: List<MangaTubeReleaseDto> = emptyList(),
@SerialName("total_page") val totalPage: Int
)
@Serializable
data class MangaTubeReleaseDto(
val image: String,
val link: String,
val name: String
)
@Serializable
data class MangaTubeTitleDto(
@SerialName("img") val image: String,
val title: String,
val url: String
)
@Serializable
data class MangaTubePaginatedChaptersDto(
val chapters: List<MangaTubeChapterDto>? = emptyList(),
@SerialName("pagina") val page: Int,
@SerialName("total_pags") val totalPages: Int
)
@Serializable
data class MangaTubeChapterDto(
@SerialName("date_created") val dateCreated: String,
val link: String,
@SerialName("chapter_name") val name: JsonPrimitive,
val number: JsonPrimitive
)
@Serializable
data class MangaTubeReaderDto(
val images: List<MangaTubePageDto> = emptyList()
)
@Serializable
data class MangaTubePageDto(
val url: String
)

View File

@ -1,11 +1,12 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext { ext {
extName = 'Muito Mangá' extName = 'Muito Mangá'
pkgNameSuffix = 'pt.muitomanga' pkgNameSuffix = 'pt.muitomanga'
extClass = '.MuitoManga' extClass = '.MuitoManga'
extVersionCode = 1 extVersionCode = 2
libVersion = '1.2' libVersion = '1.2'
containsNsfw = true containsNsfw = true
} }

View File

@ -1,11 +1,5 @@
package eu.kanade.tachiyomi.extension.pt.muitomanga package eu.kanade.tachiyomi.extension.pt.muitomanga
import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.obj
import com.github.salomonbrys.kotson.string
import com.google.gson.JsonElement
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.annotations.Nsfw import eu.kanade.tachiyomi.annotations.Nsfw
import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
@ -15,6 +9,8 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor import okhttp3.Interceptor
@ -26,6 +22,7 @@ import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody import okhttp3.ResponseBody.Companion.toResponseBody
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import uy.kohesive.injekt.injectLazy
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@ -53,6 +50,8 @@ class MuitoManga : ParsedHttpSource() {
.add("Accept-Language", ACCEPT_LANGUAGE) .add("Accept-Language", ACCEPT_LANGUAGE)
.add("Referer", "$baseUrl/") .add("Referer", "$baseUrl/")
private val json: Json by injectLazy()
private val directoryCache: MutableMap<Int, String> = mutableMapOf() private val directoryCache: MutableMap<Int, String> = mutableMapOf()
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
@ -67,11 +66,11 @@ class MuitoManga : ParsedHttpSource() {
} }
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response): MangasPage {
val result = response.asJson().obj val directory = json.decodeFromString<MuitoMangaDirectoryDto>(response.body!!.string())
val totalPages = ceil(result["encontrado"].array.size().toDouble() / ITEMS_PER_PAGE) val totalPages = ceil(directory.results.size.toDouble() / ITEMS_PER_PAGE)
val currentPage = response.request.header("X-Page")!!.toInt() val currentPage = response.request.header("X-Page")!!.toInt()
val mangaList = result["encontrado"].array val mangaList = directory.results
.drop(ITEMS_PER_PAGE * (currentPage - 1)) .drop(ITEMS_PER_PAGE * (currentPage - 1))
.take(ITEMS_PER_PAGE) .take(ITEMS_PER_PAGE)
.map(::popularMangaFromObject) .map(::popularMangaFromObject)
@ -79,10 +78,10 @@ class MuitoManga : ParsedHttpSource() {
return MangasPage(mangaList, hasNextPage = currentPage < totalPages) return MangasPage(mangaList, hasNextPage = currentPage < totalPages)
} }
private fun popularMangaFromObject(obj: JsonElement): SManga = SManga.create().apply { private fun popularMangaFromObject(manga: MuitoMangaTitleDto): SManga = SManga.create().apply {
title = obj["titulo"].string title = manga.title
thumbnail_url = obj["imagem"].string thumbnail_url = manga.image
url = "/manga/" + obj["url"].string url = "/manga/" + manga.url
} }
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request {
@ -210,8 +209,6 @@ class MuitoManga : ParsedHttpSource() {
} }
} }
private fun Response.asJson(): JsonElement = JsonParser.parseString(body!!.string())
companion object { companion object {
private const val ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9," + private const val ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9," +
"image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" "image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"

View File

@ -0,0 +1,16 @@
package eu.kanade.tachiyomi.extension.pt.muitomanga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class MuitoMangaDirectoryDto(
@SerialName("encontrado") val results: List<MuitoMangaTitleDto> = emptyList()
)
@Serializable
data class MuitoMangaTitleDto(
@SerialName("imagem") val image: String,
@SerialName("titulo") val title: String,
val url: String
)

View File

@ -1,11 +1,12 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext { ext {
extName = 'Mundo Mangá-Kun' extName = 'Mundo Mangá-Kun'
pkgNameSuffix = 'pt.mundomangakun' pkgNameSuffix = 'pt.mundomangakun'
extClass = '.MundoMangaKun' extClass = '.MundoMangaKun'
extVersionCode = 2 extVersionCode = 3
libVersion = '1.2' libVersion = '1.2'
} }

View File

@ -1,9 +1,5 @@
package eu.kanade.tachiyomi.extension.pt.mundomangakun package eu.kanade.tachiyomi.extension.pt.mundomangakun
import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.obj
import com.github.salomonbrys.kotson.string
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
@ -12,6 +8,10 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -19,6 +19,7 @@ import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class MundoMangaKun : ParsedHttpSource() { class MundoMangaKun : ParsedHttpSource() {
@ -40,6 +41,8 @@ class MundoMangaKun : ParsedHttpSource() {
.add("Origin", baseUrl) .add("Origin", baseUrl)
.add("Referer", baseUrl) .add("Referer", baseUrl)
private val json: Json by injectLazy()
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
val refererPath = if (page <= 2) "" else "/leitor-online/${page - 1}" val refererPath = if (page <= 2) "" else "/leitor-online/${page - 1}"
val newHeaders = headersBuilder() val newHeaders = headersBuilder()
@ -124,20 +127,21 @@ class MundoMangaKun : ParsedHttpSource() {
val link = element.attr("onclick") val link = element.attr("onclick")
.substringAfter("this,") .substringAfter("this,")
.substringBeforeLast(")") .substringBeforeLast(")")
.let { JsonParser.parseString(it) } .replace("'", "\"")
.array .let { json.parseToJsonElement(it) }
.first { it.obj["tipo"].string == "LEITOR" } .jsonArray
.first { it.jsonObject["tipo"]!!.jsonPrimitive.content == "LEITOR" }
setUrlWithoutDomain(link.obj["link"].string) setUrlWithoutDomain(link.jsonObject["link"]!!.jsonPrimitive.content)
} }
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
return document.select("script:containsData(var paginas)").first().data() return document.select("script:containsData(var paginas)").first().data()
.substringAfter("var paginas=") .substringAfter("var paginas=")
.substringBefore(";var") .substringBefore(";var")
.let { JsonParser.parseString(it) } .let { json.parseToJsonElement(it) }
.array .jsonArray
.mapIndexed { i, page -> Page(i, document.location(), page.string) } .mapIndexed { i, page -> Page(i, document.location(), page.jsonPrimitive.content) }
} }
override fun imageUrlParse(document: Document) = "" override fun imageUrlParse(document: Document) = ""

View File

@ -1,11 +1,12 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext { ext {
extName = 'One Piece Ex' extName = 'One Piece Ex'
pkgNameSuffix = 'pt.opex' pkgNameSuffix = 'pt.opex'
extClass = '.OnePieceEx' extClass = '.OnePieceEx'
extVersionCode = 1 extVersionCode = 2
libVersion = '1.2' libVersion = '1.2'
} }

View File

@ -1,8 +1,5 @@
package eu.kanade.tachiyomi.extension.pt.opex package eu.kanade.tachiyomi.extension.pt.opex
import com.github.salomonbrys.kotson.obj
import com.github.salomonbrys.kotson.string
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
@ -12,6 +9,9 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -20,6 +20,7 @@ import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -42,6 +43,8 @@ class OnePieceEx : ParsedHttpSource() {
.add("Accept-Language", ACCEPT_LANGUAGE) .add("Accept-Language", ACCEPT_LANGUAGE)
.add("Referer", "$baseUrl/mangas") .add("Referer", "$baseUrl/mangas")
private val json: Json by injectLazy()
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/mangas", headers) override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/mangas", headers)
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response): MangasPage {
@ -195,10 +198,9 @@ class OnePieceEx : ParsedHttpSource() {
.replace("\\\"", "\"") .replace("\\\"", "\"")
.replace("\\\\\\/", "/") .replace("\\\\\\/", "/")
.replace("//", "/") .replace("//", "/")
.let { JsonParser.parseString(it).obj } .let { json.parseToJsonElement(it).jsonObject.entries }
.entrySet()
.mapIndexed { i, entry -> .mapIndexed { i, entry ->
Page(i, document.location(), "$baseUrl/${entry.value.string}") Page(i, document.location(), "$baseUrl/${entry.value.jsonPrimitive.content}")
} }
} }

View File

@ -1,11 +1,12 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext { ext {
extName = 'Tsuki Mangás' extName = 'Tsuki Mangás'
pkgNameSuffix = 'pt.tsukimangas' pkgNameSuffix = 'pt.tsukimangas'
extClass = '.TsukiMangas' extClass = '.TsukiMangas'
extVersionCode = 15 extVersionCode = 16
libVersion = '1.2' libVersion = '1.2'
containsNsfw = true containsNsfw = true
} }

View File

@ -1,14 +1,5 @@
package eu.kanade.tachiyomi.extension.pt.tsukimangas package eu.kanade.tachiyomi.extension.pt.tsukimangas
import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.int
import com.github.salomonbrys.kotson.nullString
import com.github.salomonbrys.kotson.obj
import com.github.salomonbrys.kotson.string
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.annotations.Nsfw import eu.kanade.tachiyomi.annotations.Nsfw
import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
@ -20,12 +11,15 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@ -52,24 +46,26 @@ class TsukiMangas : HttpSource() {
.add("User-Agent", USER_AGENT) .add("User-Agent", USER_AGENT)
.add("Referer", baseUrl) .add("Referer", baseUrl)
private val json: Json by injectLazy()
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/api/v2/mangas?page=$page&title=&filter=0", headers) return GET("$baseUrl/api/v2/mangas?page=$page&title=&filter=0", headers)
} }
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response): MangasPage {
val result = response.asJson().obj val result = json.decodeFromString<TsukiPaginatedDto>(response.body!!.string())
val popularMangas = result["data"].array val popularMangas = result.data.map(::popularMangaItemParse)
.map { popularMangaItemParse(it.obj) }
val hasNextPage = result.page < result.lastPage
val hasNextPage = result["page"].int < result["lastPage"].int
return MangasPage(popularMangas, hasNextPage) return MangasPage(popularMangas, hasNextPage)
} }
private fun popularMangaItemParse(obj: JsonObject) = SManga.create().apply { private fun popularMangaItemParse(manga: TsukiMangaDto) = SManga.create().apply {
title = obj["title"].string title = manga.title
thumbnail_url = baseUrl + "/imgs/" + obj["poster"].string.substringBefore("?") thumbnail_url = baseUrl + "/imgs/" + manga.poster.substringBefore("?")
url = "/obra/${obj["id"].int}/${obj["url"].string}" url = "/obra/${manga.id}/${manga.url}"
} }
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request {
@ -77,19 +73,19 @@ class TsukiMangas : HttpSource() {
} }
override fun latestUpdatesParse(response: Response): MangasPage { override fun latestUpdatesParse(response: Response): MangasPage {
val result = response.asJson().obj val result = json.decodeFromString<TsukiPaginatedDto>(response.body!!.string())
val latestMangas = result["data"].array val latestMangas = result.data.map(::latestMangaItemParse)
.map { latestMangaItemParse(it.obj) }
val hasNextPage = result.page < result.lastPage
val hasNextPage = result["page"].int < result["lastPage"].int
return MangasPage(latestMangas, hasNextPage) return MangasPage(latestMangas, hasNextPage)
} }
private fun latestMangaItemParse(obj: JsonObject) = SManga.create().apply { private fun latestMangaItemParse(manga: TsukiMangaDto) = SManga.create().apply {
title = obj["title"].string title = manga.title
thumbnail_url = baseUrl + "/imgs/" + obj["poster"].string.substringBefore("?") thumbnail_url = baseUrl + "/imgs/" + manga.poster.substringBefore("?")
url = "/obra/${obj["id"].int}/${obj["url"].string}" url = "/obra/${manga.id}/${manga.url}"
} }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
@ -149,20 +145,19 @@ class TsukiMangas : HttpSource() {
} }
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
val result = response.asJson().obj val result = json.decodeFromString<TsukiPaginatedDto>(response.body!!.string())
val searchResults = result["data"].array val searchResults = result.data.map(::searchMangaItemParse)
.map { searchMangaItemParse(it.obj) }
val hasNextPage = result["page"].int < result["lastPage"].int val hasNextPage = result.page < result.lastPage
return MangasPage(searchResults, hasNextPage) return MangasPage(searchResults, hasNextPage)
} }
private fun searchMangaItemParse(obj: JsonObject) = SManga.create().apply { private fun searchMangaItemParse(manga: TsukiMangaDto) = SManga.create().apply {
title = obj["title"].string title = manga.title
thumbnail_url = baseUrl + "/imgs/" + obj["poster"].string.substringBefore("?") thumbnail_url = baseUrl + "/imgs/" + manga.poster.substringBefore("?")
url = "/obra/${obj["id"].int}/${obj["url"].string}" url = "/obra/${manga.id}/${manga.url}"
} }
// Workaround to allow "Open in browser" use the real URL. // Workaround to allow "Open in browser" use the real URL.
@ -192,18 +187,16 @@ class TsukiMangas : HttpSource() {
return GET(baseUrl + manga.url, newHeaders) return GET(baseUrl + manga.url, newHeaders)
} }
override fun mangaDetailsParse(response: Response): SManga { override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply {
val result = response.asJson().obj val mangaDto = json.decodeFromString<TsukiMangaDto>(response.body!!.string())
return SManga.create().apply { title = mangaDto.title
title = result["title"].string thumbnail_url = baseUrl + "/imgs/" + mangaDto.poster.substringBefore("?")
thumbnail_url = baseUrl + "/imgs/" + result["poster"].string.substringBefore("?") description = mangaDto.synopsis.orEmpty()
description = result["synopsis"].nullString.orEmpty() status = mangaDto.status.orEmpty().toStatus()
status = result["status"].nullString.orEmpty().toStatus() author = mangaDto.author.orEmpty()
author = result["author"].nullString.orEmpty() artist = mangaDto.artist.orEmpty()
artist = result["artist"].nullString.orEmpty() genre = mangaDto.genres.joinToString { it.genre }
genre = result["genres"].array.joinToString { it.obj["genre"].string }
}
} }
override fun chapterListRequest(manga: SManga): Request { override fun chapterListRequest(manga: SManga): Request {
@ -219,25 +212,26 @@ class TsukiMangas : HttpSource() {
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val mangaUrl = response.request.header("Referer")!!.substringAfter(baseUrl) val mangaUrl = response.request.header("Referer")!!.substringAfter(baseUrl)
return response.asJson().array return json
.flatMap { chapterListItemParse(it.obj, mangaUrl) } .decodeFromString<List<TsukiChapterDto>>(response.body!!.string())
.flatMap { chapterListItemParse(it, mangaUrl) }
.reversed() .reversed()
} }
private fun chapterListItemParse(obj: JsonObject, mangaUrl: String): List<SChapter> { private fun chapterListItemParse(chapter: TsukiChapterDto, mangaUrl: String): List<SChapter> {
val mangaId = mangaUrl.substringAfter("obra/").substringBefore("/") val mangaId = mangaUrl.substringAfter("obra/").substringBefore("/")
val mangaSlug = mangaUrl.substringAfterLast("/") val mangaSlug = mangaUrl.substringAfterLast("/")
return obj["versions"].array.map { version -> return chapter.versions.map { version ->
SChapter.create().apply { SChapter.create().apply {
name = "Cap. " + obj["number"].string + name = "Cap. " + chapter.number +
(if (!obj["title"].nullString.isNullOrEmpty()) " - " + obj["title"].string else "") (if (!chapter.title.isNullOrEmpty()) " - " + chapter.title else "")
chapter_number = obj["number"].string.toFloatOrNull() ?: -1f chapter_number = chapter.number.toFloatOrNull() ?: -1f
scanlator = version.obj["scans"].array scanlator = version.scans
.sortedBy { it.obj["scan"].obj["name"].string } .sortedBy { it.scan.name }
.joinToString { it.obj["scan"].obj["name"].string } .joinToString { it.scan.name }
date_upload = version.obj["created_at"].string.substringBefore(" ").toDate() date_upload = version.createdAt.substringBefore(" ").toDate()
url = "/leitor/$mangaId/${version.obj["id"].int}/$mangaSlug/${obj["number"].string}" url = "/leitor/$mangaId/${version.id}/$mangaSlug/${chapter.number}"
} }
} }
} }
@ -258,12 +252,11 @@ class TsukiMangas : HttpSource() {
} }
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
val result = response.asJson().obj val result = json.decodeFromString<TsukiReaderDto>(response.body!!.string())
return result["pages"].array.mapIndexed { i, page -> return result.pages.mapIndexed { i, page ->
val server = page["server"].string val cdnUrl = "https://cdn${page.server}.tsukimangas.com"
val cdnUrl = "https://cdn$server.tsukimangas.com" Page(i, "$baseUrl/", cdnUrl + page.url)
Page(i, "$baseUrl/", cdnUrl + page.obj["url"].string)
} }
} }
@ -426,8 +419,6 @@ class TsukiMangas : HttpSource() {
else -> SManga.UNKNOWN else -> SManga.UNKNOWN
} }
private fun Response.asJson(): JsonElement = JsonParser.parseString(body!!.string())
companion object { companion object {
private const val ACCEPT = "application/json, text/plain, */*" private const val ACCEPT = "application/json, text/plain, */*"
private const val ACCEPT_IMAGE = "image/avif,image/webp,image/apng,image/*,*/*;q=0.8" private const val ACCEPT_IMAGE = "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"

View File

@ -0,0 +1,66 @@
package eu.kanade.tachiyomi.extension.pt.tsukimangas
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class TsukiPaginatedDto(
val data: List<TsukiMangaDto> = emptyList(),
val lastPage: Int,
val page: Int,
val perPage: Int,
val total: Int
)
@Serializable
data class TsukiMangaDto(
val artist: String? = "",
val author: String? = "",
val genres: List<TsukiGenreDto> = emptyList(),
val id: Int,
val poster: String,
val status: String? = "",
val synopsis: String? = "",
val title: String,
val url: String
)
@Serializable
data class TsukiGenreDto(
val genre: String
)
@Serializable
data class TsukiChapterDto(
val number: String,
val title: String? = "",
val versions: List<TsukiChapterVersionDto> = emptyList()
)
@Serializable
data class TsukiChapterVersionDto(
@SerialName("created_at") val createdAt: String,
val id: Int,
val scans: List<TsukiScanlatorDto> = emptyList()
)
@Serializable
data class TsukiScanlatorDto(
val scan: TsukiScanlatorDetailDto
)
@Serializable
data class TsukiScanlatorDetailDto(
val name: String
)
@Serializable
data class TsukiReaderDto(
val pages: List<TsukiPageDto> = emptyList()
)
@Serializable
data class TsukiPageDto(
val server: Int,
val url: String
)