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.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()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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? {
|
||||||
|
|
|
@ -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) }
|
||||||
|
|
||||||
|
|
|
@ -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 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 {
|
||||||
|
|
|
@ -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}'
|
||||||
|
|
|
@ -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'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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."
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: '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'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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," +
|
||||||
|
|
|
@ -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: '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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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: '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'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) = ""
|
||||||
|
|
|
@ -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'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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