Add kotlinx.serialization to more sources. (#7391)
This commit is contained in:
parent
7cc0764041
commit
10eb030895
|
@ -4,9 +4,9 @@ import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
|
|||
import eu.kanade.tachiyomi.multisrc.mangasproject.MangasProject
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.OkHttpClient
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
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 client: OkHttpClient = super.client.newBuilder()
|
||||
.addInterceptor(RateLimitInterceptor(5, 1, TimeUnit.SECONDS))
|
||||
.addInterceptor(RateLimitInterceptor(2, 1, TimeUnit.SECONDS))
|
||||
.build()
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,9 +6,9 @@ import eu.kanade.tachiyomi.network.GET
|
|||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.OkHttpClient
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
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 client: OkHttpClient = super.client.newBuilder()
|
||||
.addInterceptor(RateLimitInterceptor(5, 1, TimeUnit.SECONDS))
|
||||
.addInterceptor(RateLimitInterceptor(2, 1, TimeUnit.SECONDS))
|
||||
.build()
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
|
|
|
@ -2,14 +2,14 @@ package eu.kanade.tachiyomi.extension.pt.toonei
|
|||
|
||||
import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
|
||||
import eu.kanade.tachiyomi.multisrc.mangasproject.MangasProject
|
||||
import org.jsoup.nodes.Document
|
||||
import okhttp3.OkHttpClient
|
||||
import org.jsoup.nodes.Document
|
||||
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()
|
||||
.addInterceptor(RateLimitInterceptor(5, 1, TimeUnit.SECONDS))
|
||||
.addInterceptor(RateLimitInterceptor(2, 1, TimeUnit.SECONDS))
|
||||
.build()
|
||||
|
||||
override fun getReaderToken(document: Document): String? {
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
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.POST
|
||||
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.online.HttpSource
|
||||
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.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
|
@ -23,6 +23,7 @@ import okhttp3.Response
|
|||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
@ -54,25 +55,26 @@ abstract class MangasProject(
|
|||
|
||||
protected val sourceHeaders: Headers by lazy { sourceHeadersBuilder().build() }
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/home/most_read?page=$page&type=", sourceHeaders)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val result = response.asJsonObject()
|
||||
val result = json.decodeFromString<MangasProjectMostReadDto>(response.body!!.string())
|
||||
|
||||
val popularMangas = result["most_read"].array
|
||||
.map { popularMangaItemParse(it.obj) }
|
||||
val popularMangas = result.mostRead.map(::popularMangaFromObject)
|
||||
|
||||
val hasNextPage = response.request.url.queryParameter("page")!!.toInt() < 10
|
||||
|
||||
return MangasPage(popularMangas, hasNextPage)
|
||||
}
|
||||
|
||||
private fun popularMangaItemParse(obj: JsonObject) = SManga.create().apply {
|
||||
title = obj["serie_name"].string
|
||||
thumbnail_url = obj["cover"].string
|
||||
url = obj["link"].string
|
||||
private fun popularMangaFromObject(serie: MangasProjectSerieDto) = SManga.create().apply {
|
||||
title = serie.serieName
|
||||
thumbnail_url = serie.cover
|
||||
url = serie.link
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
|
@ -80,20 +82,19 @@ abstract class MangasProject(
|
|||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val result = response.asJsonObject()
|
||||
val result = json.decodeFromString<MangasProjectReleasesDto>(response.body!!.string())
|
||||
|
||||
val latestMangas = result["releases"].array
|
||||
.map { latestMangaItemParse(it.obj) }
|
||||
val latestMangas = result.releases.map(::latestMangaFromObject)
|
||||
|
||||
val hasNextPage = response.request.url.queryParameter("page")!!.toInt() < 5
|
||||
|
||||
return MangasPage(latestMangas, hasNextPage)
|
||||
}
|
||||
|
||||
private fun latestMangaItemParse(obj: JsonObject) = SManga.create().apply {
|
||||
title = obj["name"].string
|
||||
thumbnail_url = obj["image"].string
|
||||
url = obj["link"].string
|
||||
private fun latestMangaFromObject(serie: MangasProjectSerieDto) = SManga.create().apply {
|
||||
title = serie.name
|
||||
thumbnail_url = serie.image
|
||||
url = serie.link
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
|
@ -110,22 +111,22 @@ abstract class MangasProject(
|
|||
}
|
||||
|
||||
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 (!result["series"]!!.isJsonArray)
|
||||
if (result.series is JsonPrimitive)
|
||||
return MangasPage(emptyList(), false)
|
||||
|
||||
val searchMangas = result["series"].array
|
||||
.map { searchMangaItemParse(it.obj) }
|
||||
val searchMangas = json.decodeFromJsonElement<List<MangasProjectSerieDto>>(result.series)
|
||||
.map(::searchMangaFromObject)
|
||||
|
||||
return MangasPage(searchMangas, false)
|
||||
}
|
||||
|
||||
private fun searchMangaItemParse(obj: JsonObject) = SManga.create().apply {
|
||||
title = obj["name"].string
|
||||
thumbnail_url = obj["cover"].string
|
||||
url = obj["link"].string
|
||||
private fun searchMangaFromObject(serie: MangasProjectSerieDto) = SManga.create().apply {
|
||||
title = serie.name
|
||||
thumbnail_url = serie.cover
|
||||
url = serie.link
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
|
@ -202,41 +203,41 @@ abstract class MangasProject(
|
|||
var page = 1
|
||||
|
||||
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()
|
||||
|
||||
val chapters = mutableListOf<SChapter>()
|
||||
|
||||
while (result["chapters"]!!.isJsonArray) {
|
||||
chapters += result["chapters"].array
|
||||
.flatMap { chapterListItemParse(it.obj) }
|
||||
while (result.chapters is JsonArray) {
|
||||
chapters += json.decodeFromJsonElement<List<MangasProjectChapterDto>>(result.chapters)
|
||||
.flatMap(::chaptersFromObject)
|
||||
.toMutableList()
|
||||
|
||||
chapterListRequest = chapterListRequestPaginated(mangaUrl, mangaId, ++page)
|
||||
result = client.newCall(chapterListRequest).execute().asJsonObject()
|
||||
result = client.newCall(chapterListRequest).execute().let {
|
||||
json.decodeFromString(it.body!!.string())
|
||||
}
|
||||
}
|
||||
|
||||
return chapters
|
||||
}
|
||||
|
||||
private fun chapterListItemParse(obj: JsonObject): List<SChapter> {
|
||||
val chapterName = obj["chapter_name"]!!.string
|
||||
|
||||
return obj["releases"].obj.entrySet().map {
|
||||
val release = it.value.obj
|
||||
|
||||
private fun chaptersFromObject(chapter: MangasProjectChapterDto): List<SChapter> {
|
||||
return chapter.releases.values.map { release ->
|
||||
SChapter.create().apply {
|
||||
name = "Cap. ${obj["number"].string}" +
|
||||
(if (chapterName == "") "" else " - $chapterName")
|
||||
date_upload = obj["date_created"].string.substringBefore("T").toDate()
|
||||
scanlator = release["scanlators"]!!.array
|
||||
.mapNotNull { scanObj -> scanObj.obj["name"].string.ifEmpty { null } }
|
||||
name = "Cap. ${chapter.number}" +
|
||||
(if (chapter.name.isEmpty()) "" else " - ${chapter.name}")
|
||||
date_upload = chapter.dateCreated.substringBefore("T").toDate()
|
||||
scanlator = release.scanlators
|
||||
.mapNotNull { scan -> scan.name.ifEmpty { null } }
|
||||
.sorted()
|
||||
.joinToString()
|
||||
url = release["link"].string
|
||||
chapter_number = obj["number"].string.toFloatOrNull() ?: -1f
|
||||
url = release.link
|
||||
chapter_number = chapter.number.toFloatOrNull() ?: -1f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -269,11 +270,13 @@ abstract class MangasProject(
|
|||
val chapterUrl = getChapterUrl(response)
|
||||
|
||||
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
|
||||
.filter { it.string.startsWith("http") }
|
||||
.mapIndexed { i, obj -> Page(i, chapterUrl, obj.string) }
|
||||
return apiResponse.images
|
||||
.filter { it.startsWith("http") }
|
||||
.mapIndexed { i, imageUrl -> Page(i, chapterUrl, imageUrl) }
|
||||
}
|
||||
|
||||
open fun getChapterUrl(response: Response): String {
|
||||
|
@ -299,14 +302,6 @@ abstract class MangasProject(
|
|||
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 {
|
||||
return try {
|
||||
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_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) " +
|
||||
"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) }
|
||||
|
||||
|
|
|
@ -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()
|
||||
)
|
|
@ -9,12 +9,12 @@ class MangasProjectGenerator : ThemeSourceGenerator {
|
|||
|
||||
override val themeClass = "MangasProject"
|
||||
|
||||
override val baseVersionCode: Int = 2
|
||||
override val baseVersionCode: Int = 3
|
||||
|
||||
override val sources = listOf(
|
||||
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("Toonei", "https://toonei.com", "pt-BR", isNsfw = true, overrideVersionCode = 1),
|
||||
SingleLang("Toonei", "https://toonei.net", "pt-BR", isNsfw = true, overrideVersionCode = 1),
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -63,6 +63,7 @@ interface ThemeSourceGenerator {
|
|||
// THIS FILE IS AUTO-GENERATED; DO NOT EDIT
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = '${source.name}'
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'VIZ Shonen Jump'
|
||||
pkgNameSuffix = 'en.vizshonenjump'
|
||||
extClass = '.VizShonenJump'
|
||||
extVersionCode = 10
|
||||
extVersionCode = 11
|
||||
libVersion = '1.2'
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,5 @@
|
|||
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.network.GET
|
||||
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.online.ParsedHttpSource
|
||||
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.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
|
@ -26,6 +26,7 @@ import okhttp3.Response
|
|||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
@ -53,6 +54,8 @@ class VizShonenJump : ParsedHttpSource() {
|
|||
.add("Origin", baseUrl)
|
||||
.add("Referer", "$baseUrl/shonenjump")
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private var mangaList: List<SManga>? = null
|
||||
|
||||
private var loggedIn: Boolean? = null
|
||||
|
@ -318,11 +321,14 @@ class VizShonenJump : ParsedHttpSource() {
|
|||
.toString()
|
||||
val authCheckRequest = GET(authCheckUrl, authCheckHeaders)
|
||||
val authCheckResponse = chain.proceed(authCheckRequest)
|
||||
val authCheckJson = JsonParser.parseString(authCheckResponse.body!!.string()).obj
|
||||
val authCheckJson = Json.parseToJsonElement(authCheckResponse.body!!.string()).jsonObject
|
||||
|
||||
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()
|
||||
.removeAllQueryParameters("locked")
|
||||
.build()
|
||||
|
@ -334,16 +340,17 @@ class VizShonenJump : ParsedHttpSource() {
|
|||
}
|
||||
|
||||
if (
|
||||
authCheckJson["archive_info"]["err"].isJsonObject &&
|
||||
authCheckJson["archive_info"]["err"]["code"].nullInt == 4 &&
|
||||
authCheckJson["archive_info"]!!.jsonObject["err"] is JsonObject &&
|
||||
authCheckJson["archive_info"]!!.jsonObject["err"]!!.jsonObject["code"]?.jsonPrimitive?.intOrNull == 4 &&
|
||||
loggedIn == true
|
||||
) {
|
||||
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 {
|
||||
|
@ -363,7 +370,7 @@ class VizShonenJump : ParsedHttpSource() {
|
|||
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 AUTH_CHECK_FAILED = "Something went wrong in the auth check."
|
||||
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'HQ Now!'
|
||||
pkgNameSuffix = 'pt.hqnow'
|
||||
extClass = '.HQNow'
|
||||
extVersionCode = 3
|
||||
extVersionCode = 4
|
||||
libVersion = '1.2'
|
||||
}
|
||||
|
||||
|
|
|
@ -1,200 +1,364 @@
|
|||
package eu.kanade.tachiyomi.extension.pt.hqnow
|
||||
|
||||
import com.github.salomonbrys.kotson.fromJson
|
||||
import com.github.salomonbrys.kotson.get
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonObject
|
||||
import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
|
||||
import eu.kanade.tachiyomi.lib.ratelimit.SpecificHostRateLimitInterceptor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
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.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 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.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
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() {
|
||||
|
||||
override val name = "HQ Now!"
|
||||
|
||||
// Website is http://www.hq-now.com
|
||||
override val baseUrl = "http://admin.hq-now.com/graphql"
|
||||
override val baseUrl = "http://www.hq-now.com"
|
||||
|
||||
override val lang = "pt-BR"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
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()
|
||||
|
||||
private val gson = Gson()
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val jsonHeaders = headersBuilder().add("content-type", "application/json").build()
|
||||
|
||||
private fun mangaFromResponse(response: Response, selector: String, coversAvailable: Boolean = true): List<SManga> {
|
||||
return gson.fromJson<JsonObject>(response.body!!.string())["data"][selector].asJsonArray
|
||||
.map {
|
||||
private fun genericComicBookFromObject(comicBook: HqNowComicBookDto): SManga =
|
||||
SManga.create().apply {
|
||||
url = it["id"].asString
|
||||
title = it["name"].asString
|
||||
if (coversAvailable) thumbnail_url = it["hqCover"].asString
|
||||
title = comicBook.name
|
||||
url = "/hq/${comicBook.id}/${comicBook.name.toSlug()}"
|
||||
thumbnail_url = comicBook.cover
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Popular
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
return if (query.isNotBlank()) {
|
||||
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))
|
||||
} else {
|
||||
queryIsTitle = false
|
||||
var searchLetter = ""
|
||||
val queryStr = buildQuery {
|
||||
"""
|
||||
query getHqsByName(%name: String!) {
|
||||
getHqsByName(name: %name) {
|
||||
id
|
||||
name
|
||||
editoraId
|
||||
status
|
||||
publisherName
|
||||
impressionsCount
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is LetterFilter -> {
|
||||
searchLetter = filter.toUriPart()
|
||||
val payload = buildJsonObject {
|
||||
put("operationName", "getHqsByName")
|
||||
put("query", queryStr)
|
||||
putJsonObject("variables") {
|
||||
put("name", query)
|
||||
}
|
||||
}
|
||||
}
|
||||
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 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 {
|
||||
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
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
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))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
// Workaround to allow "Open in browser" use the real URL.
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsApiRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
mangaDetailsParse(response).apply { initialized = true }
|
||||
}
|
||||
}
|
||||
|
||||
// Chapters
|
||||
private fun mangaDetailsApiRequest(manga: SManga): Request {
|
||||
val comicBookId = manga.url.substringAfter("/hq/").substringBefore("/")
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
return mangaDetailsRequest(manga)
|
||||
val query = buildQuery {
|
||||
"""
|
||||
query getHqsById(%id: Int!) {
|
||||
getHqsById(id: %id) {
|
||||
id
|
||||
name
|
||||
synopsis
|
||||
editoraId
|
||||
status
|
||||
publisherName
|
||||
hqCover
|
||||
impressionsCount
|
||||
capitulos {
|
||||
name
|
||||
id
|
||||
number
|
||||
}
|
||||
}
|
||||
}
|
||||
""".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> {
|
||||
return gson.fromJson<JsonObject>(response.body!!.string())["data"]["getHqsById"][0]["capitulos"].asJsonArray
|
||||
.map {
|
||||
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 {
|
||||
url = it["id"].asString
|
||||
name = it["name"].asString.let { jsonName ->
|
||||
if (jsonName.isNotEmpty()) jsonName.trim() else "Capitulo: " + it["number"].asString
|
||||
}
|
||||
}
|
||||
}.reversed()
|
||||
name = "#" + chapter.number +
|
||||
(if (chapter.name.isNotEmpty()) " - " + chapter.name else "")
|
||||
url = "/hq-reader/${comicBook.id}/${comicBook.name.toSlug()}" +
|
||||
"/chapter/${chapter.id}/page/1"
|
||||
}
|
||||
|
||||
// Pages
|
||||
|
||||
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> {
|
||||
return gson.fromJson<JsonObject>(response.body!!.string())["data"]["getChapterById"]["pictures"].asJsonArray
|
||||
.mapIndexed { i, json -> Page(i, "", json["pictureUrl"].asString) }
|
||||
}
|
||||
val result = json.parseToJsonElement(response.body!!.string()).jsonObject
|
||||
|
||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not used")
|
||||
val chapterDto = result["data"]!!.jsonObject["getChapterById"]!!
|
||||
.let { json.decodeFromJsonElement<HqNowChapterDto>(it) }
|
||||
|
||||
// Filters
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
Filter.Header("NOTA: Ignorado se estiver usando"),
|
||||
Filter.Header("a pesquisa de texto!"),
|
||||
Filter.Separator(),
|
||||
LetterFilter()
|
||||
)
|
||||
|
||||
private class LetterFilter : UriPartFilter(
|
||||
"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>>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].first
|
||||
return chapterDto.pictures.mapIndexed { i, page ->
|
||||
Page(i, baseUrl, page.pictureUrl)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String = ""
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val newHeaders = headersBuilder()
|
||||
.set("Referer", page.url)
|
||||
.build()
|
||||
|
||||
return GET(page.imageUrl!!, newHeaders)
|
||||
}
|
||||
|
||||
private fun buildQuery(queryAction: () -> String) = queryAction().replace("%", "$")
|
||||
|
||||
private fun String.toSlug(): String {
|
||||
return Normalizer
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -1,11 +1,12 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'MangaTube'
|
||||
pkgNameSuffix = 'pt.mangatube'
|
||||
extClass = '.MangaTube'
|
||||
extVersionCode = 1
|
||||
extVersionCode = 2
|
||||
libVersion = '1.2'
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,5 @@
|
|||
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.network.GET
|
||||
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.online.HttpSource
|
||||
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.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
|
@ -27,6 +25,7 @@ import okhttp3.Request
|
|||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
@ -58,6 +57,8 @@ class MangaTube : HttpSource() {
|
|||
|
||||
private val apiHeaders: Headers by lazy { apiHeadersBuilder().build() }
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET(baseUrl, headers)
|
||||
}
|
||||
|
@ -91,20 +92,20 @@ class MangaTube : HttpSource() {
|
|||
}
|
||||
|
||||
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)
|
||||
|
||||
val hasNextPage = result["page"].string.toInt() < result["total_page"].int
|
||||
val hasNextPage = result.page.toInt() < result.totalPage
|
||||
|
||||
return MangasPage(latestMangas, hasNextPage)
|
||||
}
|
||||
|
||||
private fun latestUpdatesFromObject(obj: JsonElement) = SManga.create().apply {
|
||||
title = obj["name"].string
|
||||
thumbnail_url = obj["image"].string
|
||||
url = obj["link"].string
|
||||
private fun latestUpdatesFromObject(release: MangaTubeReleaseDto) = SManga.create().apply {
|
||||
title = release.name
|
||||
thumbnail_url = release.image
|
||||
url = release.link
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
|
@ -117,18 +118,17 @@ class MangaTube : HttpSource() {
|
|||
}
|
||||
|
||||
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()
|
||||
.map { searchMangaFromObject(it.value) }
|
||||
val searchResults = result.values.map(::searchMangaFromObject)
|
||||
|
||||
return MangasPage(searchResults, hasNextPage = false)
|
||||
}
|
||||
|
||||
private fun searchMangaFromObject(obj: JsonElement) = SManga.create().apply {
|
||||
title = obj["title"].string
|
||||
thumbnail_url = obj["img"].string
|
||||
setUrlWithoutDomain(obj["url"].string)
|
||||
private fun searchMangaFromObject(manga: MangaTubeTitleDto) = SManga.create().apply {
|
||||
title = manga.title
|
||||
thumbnail_url = manga.image
|
||||
setUrlWithoutDomain(manga.url)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
|
@ -154,6 +154,7 @@ class MangaTube : HttpSource() {
|
|||
|
||||
val url = "$baseUrl/jsons/series/chapters_list.json".toHttpUrlOrNull()!!.newBuilder()
|
||||
.addQueryParameter("page", page.toString())
|
||||
.addQueryParameter("order", "desc")
|
||||
.addQueryParameter("id_s", mangaId)
|
||||
.toString()
|
||||
|
||||
|
@ -163,24 +164,26 @@ class MangaTube : HttpSource() {
|
|||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
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()
|
||||
}
|
||||
|
||||
val chapters = result["chapters"].array
|
||||
val chapters = result.chapters!!
|
||||
.map(::chapterFromObject)
|
||||
.toMutableList()
|
||||
|
||||
var page = result["pagina"].int + 1
|
||||
val lastPage = result["total_pags"].int
|
||||
var page = result.page + 1
|
||||
val lastPage = result.totalPages
|
||||
|
||||
while (++page <= lastPage) {
|
||||
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)
|
||||
.toMutableList()
|
||||
}
|
||||
|
@ -188,12 +191,12 @@ class MangaTube : HttpSource() {
|
|||
return chapters
|
||||
}
|
||||
|
||||
private fun chapterFromObject(obj: JsonElement): SChapter = SChapter.create().apply {
|
||||
name = "Cap. " + (if (obj["number"].string == "false") "0" else obj["number"].string) +
|
||||
(if (obj["chapter_name"].asJsonPrimitive.isString) " - " + obj["chapter_name"].string else "")
|
||||
chapter_number = obj["number"].string.toFloatOrNull() ?: -1f
|
||||
date_upload = obj["date_created"].string.substringBefore("T").toDate()
|
||||
setUrlWithoutDomain(obj["link"].string)
|
||||
private fun chapterFromObject(chapter: MangaTubeChapterDto): SChapter = SChapter.create().apply {
|
||||
name = "Cap. " + (if (chapter.number.booleanOrNull != null) "0" else chapter.number.content) +
|
||||
(if (chapter.name.isString) " - " + chapter.name.content else "")
|
||||
chapter_number = chapter.number.floatOrNull ?: -1f
|
||||
date_upload = chapter.dateCreated.substringBefore("T").toDate()
|
||||
setUrlWithoutDomain(chapter.link)
|
||||
}
|
||||
|
||||
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 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
|
||||
.filter { it["url"].string.startsWith("http") }
|
||||
.mapIndexed { i, obj -> Page(i, chapterUrl, obj["url"].string) }
|
||||
return apiResponse.images
|
||||
.filter { it.url.startsWith("http") }
|
||||
.mapIndexed { i, page -> Page(i, chapterUrl, page.url) }
|
||||
}
|
||||
|
||||
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()
|
||||
.substringAfter("pAPI = ")
|
||||
.substringBeforeLast(";")
|
||||
.let { JsonParser.parseString(it) }
|
||||
.let { json.parseToJsonElement(it) }
|
||||
.jsonObject
|
||||
|
||||
val newUrl = chain.request().url.newBuilder()
|
||||
.addQueryParameter("nonce", apiParams["nonce"].string)
|
||||
.addQueryParameter("nonce", apiParams["nonce"]!!.jsonPrimitive.content)
|
||||
.build()
|
||||
|
||||
val newRequest = chain.request().newBuilder()
|
||||
|
@ -272,8 +278,6 @@ class MangaTube : HttpSource() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun Response.asJson(): JsonElement = JsonParser.parseString(body!!.string())
|
||||
|
||||
companion object {
|
||||
private const val ACCEPT = "application/json, text/plain, */*"
|
||||
private const val ACCEPT_HTML = "text/html,application/xhtml+xml,application/xml;q=0.9," +
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -1,11 +1,12 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'Muito Mangá'
|
||||
pkgNameSuffix = 'pt.muitomanga'
|
||||
extClass = '.MuitoManga'
|
||||
extVersionCode = 1
|
||||
extVersionCode = 2
|
||||
libVersion = '1.2'
|
||||
containsNsfw = true
|
||||
}
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
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.lib.ratelimit.RateLimitInterceptor
|
||||
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.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Interceptor
|
||||
|
@ -26,6 +22,7 @@ import okhttp3.Response
|
|||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
@ -53,6 +50,8 @@ class MuitoManga : ParsedHttpSource() {
|
|||
.add("Accept-Language", ACCEPT_LANGUAGE)
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val directoryCache: MutableMap<Int, String> = mutableMapOf()
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
|
@ -67,11 +66,11 @@ class MuitoManga : ParsedHttpSource() {
|
|||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val result = response.asJson().obj
|
||||
val totalPages = ceil(result["encontrado"].array.size().toDouble() / ITEMS_PER_PAGE)
|
||||
val directory = json.decodeFromString<MuitoMangaDirectoryDto>(response.body!!.string())
|
||||
val totalPages = ceil(directory.results.size.toDouble() / ITEMS_PER_PAGE)
|
||||
val currentPage = response.request.header("X-Page")!!.toInt()
|
||||
|
||||
val mangaList = result["encontrado"].array
|
||||
val mangaList = directory.results
|
||||
.drop(ITEMS_PER_PAGE * (currentPage - 1))
|
||||
.take(ITEMS_PER_PAGE)
|
||||
.map(::popularMangaFromObject)
|
||||
|
@ -79,10 +78,10 @@ class MuitoManga : ParsedHttpSource() {
|
|||
return MangasPage(mangaList, hasNextPage = currentPage < totalPages)
|
||||
}
|
||||
|
||||
private fun popularMangaFromObject(obj: JsonElement): SManga = SManga.create().apply {
|
||||
title = obj["titulo"].string
|
||||
thumbnail_url = obj["imagem"].string
|
||||
url = "/manga/" + obj["url"].string
|
||||
private fun popularMangaFromObject(manga: MuitoMangaTitleDto): SManga = SManga.create().apply {
|
||||
title = manga.title
|
||||
thumbnail_url = manga.image
|
||||
url = "/manga/" + manga.url
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
|
@ -210,8 +209,6 @@ class MuitoManga : ParsedHttpSource() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun Response.asJson(): JsonElement = JsonParser.parseString(body!!.string())
|
||||
|
||||
companion object {
|
||||
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"
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -1,11 +1,12 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'Mundo Mangá-Kun'
|
||||
pkgNameSuffix = 'pt.mundomangakun'
|
||||
extClass = '.MundoMangaKun'
|
||||
extVersionCode = 2
|
||||
extVersionCode = 3
|
||||
libVersion = '1.2'
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
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.network.GET
|
||||
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.SManga
|
||||
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.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
|
@ -19,6 +19,7 @@ import okhttp3.Request
|
|||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class MundoMangaKun : ParsedHttpSource() {
|
||||
|
@ -40,6 +41,8 @@ class MundoMangaKun : ParsedHttpSource() {
|
|||
.add("Origin", baseUrl)
|
||||
.add("Referer", baseUrl)
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val refererPath = if (page <= 2) "" else "/leitor-online/${page - 1}"
|
||||
val newHeaders = headersBuilder()
|
||||
|
@ -124,20 +127,21 @@ class MundoMangaKun : ParsedHttpSource() {
|
|||
val link = element.attr("onclick")
|
||||
.substringAfter("this,")
|
||||
.substringBeforeLast(")")
|
||||
.let { JsonParser.parseString(it) }
|
||||
.array
|
||||
.first { it.obj["tipo"].string == "LEITOR" }
|
||||
.replace("'", "\"")
|
||||
.let { json.parseToJsonElement(it) }
|
||||
.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> {
|
||||
return document.select("script:containsData(var paginas)").first().data()
|
||||
.substringAfter("var paginas=")
|
||||
.substringBefore(";var")
|
||||
.let { JsonParser.parseString(it) }
|
||||
.array
|
||||
.mapIndexed { i, page -> Page(i, document.location(), page.string) }
|
||||
.let { json.parseToJsonElement(it) }
|
||||
.jsonArray
|
||||
.mapIndexed { i, page -> Page(i, document.location(), page.jsonPrimitive.content) }
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'One Piece Ex'
|
||||
pkgNameSuffix = 'pt.opex'
|
||||
extClass = '.OnePieceEx'
|
||||
extVersionCode = 1
|
||||
extVersionCode = 2
|
||||
libVersion = '1.2'
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
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.network.GET
|
||||
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.online.ParsedHttpSource
|
||||
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.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
|
@ -20,6 +20,7 @@ import okhttp3.Response
|
|||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
@ -42,6 +43,8 @@ class OnePieceEx : ParsedHttpSource() {
|
|||
.add("Accept-Language", ACCEPT_LANGUAGE)
|
||||
.add("Referer", "$baseUrl/mangas")
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/mangas", headers)
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
|
@ -195,10 +198,9 @@ class OnePieceEx : ParsedHttpSource() {
|
|||
.replace("\\\"", "\"")
|
||||
.replace("\\\\\\/", "/")
|
||||
.replace("//", "/")
|
||||
.let { JsonParser.parseString(it).obj }
|
||||
.entrySet()
|
||||
.let { json.parseToJsonElement(it).jsonObject.entries }
|
||||
.mapIndexed { i, entry ->
|
||||
Page(i, document.location(), "$baseUrl/${entry.value.string}")
|
||||
Page(i, document.location(), "$baseUrl/${entry.value.jsonPrimitive.content}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'Tsuki Mangás'
|
||||
pkgNameSuffix = 'pt.tsukimangas'
|
||||
extClass = '.TsukiMangas'
|
||||
extVersionCode = 15
|
||||
extVersionCode = 16
|
||||
libVersion = '1.2'
|
||||
containsNsfw = true
|
||||
}
|
||||
|
|
|
@ -1,14 +1,5 @@
|
|||
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.lib.ratelimit.RateLimitInterceptor
|
||||
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.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
@ -52,24 +46,26 @@ class TsukiMangas : HttpSource() {
|
|||
.add("User-Agent", USER_AGENT)
|
||||
.add("Referer", baseUrl)
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/api/v2/mangas?page=$page&title=&filter=0", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val result = response.asJson().obj
|
||||
val result = json.decodeFromString<TsukiPaginatedDto>(response.body!!.string())
|
||||
|
||||
val popularMangas = result["data"].array
|
||||
.map { popularMangaItemParse(it.obj) }
|
||||
val popularMangas = result.data.map(::popularMangaItemParse)
|
||||
|
||||
val hasNextPage = result.page < result.lastPage
|
||||
|
||||
val hasNextPage = result["page"].int < result["lastPage"].int
|
||||
return MangasPage(popularMangas, hasNextPage)
|
||||
}
|
||||
|
||||
private fun popularMangaItemParse(obj: JsonObject) = SManga.create().apply {
|
||||
title = obj["title"].string
|
||||
thumbnail_url = baseUrl + "/imgs/" + obj["poster"].string.substringBefore("?")
|
||||
url = "/obra/${obj["id"].int}/${obj["url"].string}"
|
||||
private fun popularMangaItemParse(manga: TsukiMangaDto) = SManga.create().apply {
|
||||
title = manga.title
|
||||
thumbnail_url = baseUrl + "/imgs/" + manga.poster.substringBefore("?")
|
||||
url = "/obra/${manga.id}/${manga.url}"
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
|
@ -77,19 +73,19 @@ class TsukiMangas : HttpSource() {
|
|||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val result = response.asJson().obj
|
||||
val result = json.decodeFromString<TsukiPaginatedDto>(response.body!!.string())
|
||||
|
||||
val latestMangas = result["data"].array
|
||||
.map { latestMangaItemParse(it.obj) }
|
||||
val latestMangas = result.data.map(::latestMangaItemParse)
|
||||
|
||||
val hasNextPage = result.page < result.lastPage
|
||||
|
||||
val hasNextPage = result["page"].int < result["lastPage"].int
|
||||
return MangasPage(latestMangas, hasNextPage)
|
||||
}
|
||||
|
||||
private fun latestMangaItemParse(obj: JsonObject) = SManga.create().apply {
|
||||
title = obj["title"].string
|
||||
thumbnail_url = baseUrl + "/imgs/" + obj["poster"].string.substringBefore("?")
|
||||
url = "/obra/${obj["id"].int}/${obj["url"].string}"
|
||||
private fun latestMangaItemParse(manga: TsukiMangaDto) = SManga.create().apply {
|
||||
title = manga.title
|
||||
thumbnail_url = baseUrl + "/imgs/" + manga.poster.substringBefore("?")
|
||||
url = "/obra/${manga.id}/${manga.url}"
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
|
@ -149,20 +145,19 @@ class TsukiMangas : HttpSource() {
|
|||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val result = response.asJson().obj
|
||||
val result = json.decodeFromString<TsukiPaginatedDto>(response.body!!.string())
|
||||
|
||||
val searchResults = result["data"].array
|
||||
.map { searchMangaItemParse(it.obj) }
|
||||
val searchResults = result.data.map(::searchMangaItemParse)
|
||||
|
||||
val hasNextPage = result["page"].int < result["lastPage"].int
|
||||
val hasNextPage = result.page < result.lastPage
|
||||
|
||||
return MangasPage(searchResults, hasNextPage)
|
||||
}
|
||||
|
||||
private fun searchMangaItemParse(obj: JsonObject) = SManga.create().apply {
|
||||
title = obj["title"].string
|
||||
thumbnail_url = baseUrl + "/imgs/" + obj["poster"].string.substringBefore("?")
|
||||
url = "/obra/${obj["id"].int}/${obj["url"].string}"
|
||||
private fun searchMangaItemParse(manga: TsukiMangaDto) = SManga.create().apply {
|
||||
title = manga.title
|
||||
thumbnail_url = baseUrl + "/imgs/" + manga.poster.substringBefore("?")
|
||||
url = "/obra/${manga.id}/${manga.url}"
|
||||
}
|
||||
|
||||
// Workaround to allow "Open in browser" use the real URL.
|
||||
|
@ -192,18 +187,16 @@ class TsukiMangas : HttpSource() {
|
|||
return GET(baseUrl + manga.url, newHeaders)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val result = response.asJson().obj
|
||||
override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply {
|
||||
val mangaDto = json.decodeFromString<TsukiMangaDto>(response.body!!.string())
|
||||
|
||||
return SManga.create().apply {
|
||||
title = result["title"].string
|
||||
thumbnail_url = baseUrl + "/imgs/" + result["poster"].string.substringBefore("?")
|
||||
description = result["synopsis"].nullString.orEmpty()
|
||||
status = result["status"].nullString.orEmpty().toStatus()
|
||||
author = result["author"].nullString.orEmpty()
|
||||
artist = result["artist"].nullString.orEmpty()
|
||||
genre = result["genres"].array.joinToString { it.obj["genre"].string }
|
||||
}
|
||||
title = mangaDto.title
|
||||
thumbnail_url = baseUrl + "/imgs/" + mangaDto.poster.substringBefore("?")
|
||||
description = mangaDto.synopsis.orEmpty()
|
||||
status = mangaDto.status.orEmpty().toStatus()
|
||||
author = mangaDto.author.orEmpty()
|
||||
artist = mangaDto.artist.orEmpty()
|
||||
genre = mangaDto.genres.joinToString { it.genre }
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
|
@ -219,25 +212,26 @@ class TsukiMangas : HttpSource() {
|
|||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val mangaUrl = response.request.header("Referer")!!.substringAfter(baseUrl)
|
||||
|
||||
return response.asJson().array
|
||||
.flatMap { chapterListItemParse(it.obj, mangaUrl) }
|
||||
return json
|
||||
.decodeFromString<List<TsukiChapterDto>>(response.body!!.string())
|
||||
.flatMap { chapterListItemParse(it, mangaUrl) }
|
||||
.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 mangaSlug = mangaUrl.substringAfterLast("/")
|
||||
|
||||
return obj["versions"].array.map { version ->
|
||||
return chapter.versions.map { version ->
|
||||
SChapter.create().apply {
|
||||
name = "Cap. " + obj["number"].string +
|
||||
(if (!obj["title"].nullString.isNullOrEmpty()) " - " + obj["title"].string else "")
|
||||
chapter_number = obj["number"].string.toFloatOrNull() ?: -1f
|
||||
scanlator = version.obj["scans"].array
|
||||
.sortedBy { it.obj["scan"].obj["name"].string }
|
||||
.joinToString { it.obj["scan"].obj["name"].string }
|
||||
date_upload = version.obj["created_at"].string.substringBefore(" ").toDate()
|
||||
url = "/leitor/$mangaId/${version.obj["id"].int}/$mangaSlug/${obj["number"].string}"
|
||||
name = "Cap. " + chapter.number +
|
||||
(if (!chapter.title.isNullOrEmpty()) " - " + chapter.title else "")
|
||||
chapter_number = chapter.number.toFloatOrNull() ?: -1f
|
||||
scanlator = version.scans
|
||||
.sortedBy { it.scan.name }
|
||||
.joinToString { it.scan.name }
|
||||
date_upload = version.createdAt.substringBefore(" ").toDate()
|
||||
url = "/leitor/$mangaId/${version.id}/$mangaSlug/${chapter.number}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -258,12 +252,11 @@ class TsukiMangas : HttpSource() {
|
|||
}
|
||||
|
||||
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 ->
|
||||
val server = page["server"].string
|
||||
val cdnUrl = "https://cdn$server.tsukimangas.com"
|
||||
Page(i, "$baseUrl/", cdnUrl + page.obj["url"].string)
|
||||
return result.pages.mapIndexed { i, page ->
|
||||
val cdnUrl = "https://cdn${page.server}.tsukimangas.com"
|
||||
Page(i, "$baseUrl/", cdnUrl + page.url)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -426,8 +419,6 @@ class TsukiMangas : HttpSource() {
|
|||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
private fun Response.asJson(): JsonElement = JsonParser.parseString(body!!.string())
|
||||
|
||||
companion object {
|
||||
private const val ACCEPT = "application/json, text/plain, */*"
|
||||
private const val ACCEPT_IMAGE = "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"
|
||||
|
|
|
@ -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
|
||||
)
|
Loading…
Reference in New Issue