Add the Tsuki Mangás extension again with permission from site owner. (#4916)
Add the Tsuki Mangás extension again with permission from site owner
This commit is contained in:
parent
d7edce780a
commit
bc7350c76d
|
@ -0,0 +1,17 @@
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
|
ext {
|
||||||
|
extName = 'Tsuki Mangás'
|
||||||
|
pkgNameSuffix = 'pt.tsukimangas'
|
||||||
|
extClass = '.TsukiMangas'
|
||||||
|
extVersionCode = 6
|
||||||
|
libVersion = '1.2'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(':lib-ratelimit')
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 7.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
|
@ -0,0 +1,349 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.pt.tsukimangas
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.array
|
||||||
|
import com.github.salomonbrys.kotson.int
|
||||||
|
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.lib.ratelimit.RateLimitInterceptor
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import rx.Observable
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class TsukiMangas : HttpSource() {
|
||||||
|
|
||||||
|
override val name = "Tsuki Mangás"
|
||||||
|
|
||||||
|
override val baseUrl = "https://tsukimangas.com"
|
||||||
|
|
||||||
|
override val lang = "pt-BR"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
private val rateLimitInterceptor = RateLimitInterceptor(150, 1, TimeUnit.MINUTES)
|
||||||
|
|
||||||
|
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||||
|
.addInterceptor(rateLimitInterceptor)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||||
|
.add("Accept", ACCEPT)
|
||||||
|
.add("Accept-Language", ACCEPT_LANGUAGE)
|
||||||
|
.add("User-Agent", USER_AGENT)
|
||||||
|
.add("Referer", baseUrl)
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
return GET("$baseUrl/api/melhores", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
|
val result = response.asJson().array
|
||||||
|
|
||||||
|
val popularMangas = result.map { popularMangaItemParse(it.obj) }
|
||||||
|
|
||||||
|
return MangasPage(popularMangas, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun popularMangaItemParse(obj: JsonObject) = SManga.create().apply {
|
||||||
|
title = obj["TITULO"].string
|
||||||
|
thumbnail_url = baseUrl + "/imgs/" + obj["CAPA"].string.substringBefore("?")
|
||||||
|
url = "/manga/" + obj["URL"].string
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
return GET("$baseUrl/api/lancamentos/$page", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
|
val json = response.asJson().array
|
||||||
|
|
||||||
|
if (json.size() == 0)
|
||||||
|
return MangasPage(emptyList(), false)
|
||||||
|
|
||||||
|
val result = json[0].obj
|
||||||
|
|
||||||
|
val latestMangas = result["mangas"].array
|
||||||
|
.map { latestMangaItemParse(it.obj) }
|
||||||
|
|
||||||
|
// Latest pagination doesn't seen to have a lower end.
|
||||||
|
return MangasPage(latestMangas, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun latestMangaItemParse(obj: JsonObject) = SManga.create().apply {
|
||||||
|
title = obj["TITULO"].string
|
||||||
|
thumbnail_url = baseUrl + "/imgs/" + obj["CAPA"].string
|
||||||
|
url = "/manga/" + obj["URL"].string
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
|
return client.newCall(searchMangaRequest(page, query, filters))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response -> searchMangaParse(response) }
|
||||||
|
.onErrorReturn {
|
||||||
|
if (it.message!!.contains("404")) {
|
||||||
|
return@onErrorReturn MangasPage(emptyList(), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val newHeaders = headersBuilder()
|
||||||
|
.set("Referer", "$baseUrl/lista-mangas")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val pathQuery = if (query.isEmpty()) "all" else query
|
||||||
|
|
||||||
|
val genreFilter = if (filters.isEmpty()) null else filters[0] as GenreFilter
|
||||||
|
val genreQuery = genreFilter?.state
|
||||||
|
?.filter { it.state }
|
||||||
|
?.joinToString(",") { it.name } ?: "all"
|
||||||
|
|
||||||
|
val url = HttpUrl.parse("$baseUrl/api/generos")!!.newBuilder()
|
||||||
|
.addEncodedPathSegment(genreQuery)
|
||||||
|
.addPathSegment(page.toString())
|
||||||
|
.addEncodedPathSegment(pathQuery)
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
return GET(url, newHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
|
val result = response.asJson().array
|
||||||
|
|
||||||
|
if (result.size() == 0)
|
||||||
|
return MangasPage(emptyList(), false)
|
||||||
|
|
||||||
|
val searchMangas = result.map { searchMangaItemParse(it.obj) }
|
||||||
|
|
||||||
|
val currentPage = response.request().url().toString()
|
||||||
|
.substringBeforeLast("/")
|
||||||
|
.substringAfterLast("/")
|
||||||
|
.toInt()
|
||||||
|
val lastPage = result[0].obj["page"].array[0].int
|
||||||
|
val hasNextPage = currentPage < lastPage
|
||||||
|
|
||||||
|
return MangasPage(searchMangas, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchMangaItemParse(obj: JsonObject) = SManga.create().apply {
|
||||||
|
title = obj["TITULO"].string
|
||||||
|
thumbnail_url = baseUrl + "/imgs/" + obj["CAPA"].string.substringBefore("?")
|
||||||
|
url = "/manga/" + obj["URL"].string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workaround to allow "Open in browser" use the real URL.
|
||||||
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
|
return client.newCall(mangaDetailsApiRequest(manga))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
mangaDetailsParse(response).apply { initialized = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mangaDetailsApiRequest(manga: SManga): Request {
|
||||||
|
val newHeaders = headersBuilder()
|
||||||
|
.set("Referer", baseUrl + manga.url)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val mangaSlug = manga.url.substringAfterLast("/")
|
||||||
|
|
||||||
|
return GET("$baseUrl/api/mangas/$mangaSlug", newHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||||
|
val newHeaders = headersBuilder()
|
||||||
|
.removeAll("Accept")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return GET(baseUrl + manga.url, newHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
|
val result = response.asJson().obj["manga"].array[0].obj
|
||||||
|
|
||||||
|
return SManga.create().apply {
|
||||||
|
title = result["TITULO"].string
|
||||||
|
thumbnail_url = baseUrl + "/imgs/" + result["CAPA"].string.substringBefore("?")
|
||||||
|
description = result["SINOPSE"].string
|
||||||
|
status = SManga.ONGOING
|
||||||
|
author = result["AUTOR"].string
|
||||||
|
artist = result["ARTISTA"].string
|
||||||
|
genre = result["GENEROS"].string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListRequest(manga: SManga): Request = chapterListRequestPaginated(manga.url, 1)
|
||||||
|
|
||||||
|
private fun chapterListRequestPaginated(mangaUrl: String, page: Int): Request {
|
||||||
|
val slug = mangaUrl.substringAfterLast("/")
|
||||||
|
|
||||||
|
val newHeaders = headersBuilder()
|
||||||
|
.set("Referer", baseUrl + mangaUrl)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return GET("$baseUrl/api/capitulospag/$slug/DESC/$page", newHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
var result = response.asJson().array
|
||||||
|
|
||||||
|
if (result.size() == 0)
|
||||||
|
return emptyList()
|
||||||
|
|
||||||
|
val mangaUrl = response.request().header("Referer")!!.substringAfter(baseUrl)
|
||||||
|
val mangaSlug = mangaUrl.substringAfterLast("/")
|
||||||
|
var page = 1
|
||||||
|
|
||||||
|
val chapters = mutableListOf<SChapter>()
|
||||||
|
|
||||||
|
while (result.size() != 0) {
|
||||||
|
chapters += result
|
||||||
|
.map { chapterListItemParse(it.obj, mangaSlug) }
|
||||||
|
.toMutableList()
|
||||||
|
|
||||||
|
val newRequest = chapterListRequestPaginated(mangaUrl, ++page)
|
||||||
|
result = client.newCall(newRequest).execute().asJson().array
|
||||||
|
}
|
||||||
|
|
||||||
|
return chapters
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun chapterListItemParse(obj: JsonObject, slug: String): SChapter = SChapter.create().apply {
|
||||||
|
name = "Cap. " + obj["NUMERO"].string +
|
||||||
|
(if (obj["TITULO"].string.isNotEmpty()) " - " + obj["TITULO"].string else "")
|
||||||
|
chapter_number = obj["NUMERO"].string.toFloatOrNull() ?: -1f
|
||||||
|
scanlator = obj["scans"].array.joinToString { it.obj["NOME"].string }
|
||||||
|
date_upload = obj["DATA"].string.substringBefore("T").toDate()
|
||||||
|
url = "/leitor/$slug/" + obj["NUMERO"].string
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
|
val newHeaders = headersBuilder()
|
||||||
|
.set("Referer", baseUrl + chapter.url)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return GET("$baseUrl/api" + chapter.url, newHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val result = response.asJson().array
|
||||||
|
|
||||||
|
return result.mapIndexed { i, page -> Page(i, baseUrl + "/", page.obj["IMG"].string) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchImageUrl(page: Page): Observable<String> = Observable.just(page.imageUrl!!)
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response): String = ""
|
||||||
|
|
||||||
|
override fun imageRequest(page: Page): Request {
|
||||||
|
val newHeaders = headersBuilder()
|
||||||
|
.set("Accept", ACCEPT_IMAGE)
|
||||||
|
.set("Accept-Language", ACCEPT_LANGUAGE)
|
||||||
|
.set("Referer", page.url)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return GET(page.imageUrl!!, newHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Genre(name: String) : Filter.CheckBox(name)
|
||||||
|
|
||||||
|
private class GenreFilter(genres: List<Genre>) : Filter.Group<Genre>("Gêneros", genres)
|
||||||
|
|
||||||
|
override fun getFilterList(): FilterList = FilterList(GenreFilter(getGenreList()))
|
||||||
|
|
||||||
|
// [...document.querySelectorAll(".multiselect__element span span")]
|
||||||
|
// .map(i => `Genre("${i.innerHTML}")`).join(",\n")
|
||||||
|
private fun getGenreList(): List<Genre> = listOf(
|
||||||
|
Genre("4-koma"),
|
||||||
|
Genre("Adulto"),
|
||||||
|
Genre("Artes Marciais"),
|
||||||
|
Genre("Aventura"),
|
||||||
|
Genre("Ação"),
|
||||||
|
Genre("Bender"),
|
||||||
|
Genre("Comédia"),
|
||||||
|
Genre("Drama"),
|
||||||
|
Genre("Ecchi"),
|
||||||
|
Genre("Esporte"),
|
||||||
|
Genre("Fantasia"),
|
||||||
|
Genre("Ficção"),
|
||||||
|
Genre("Gastronomia"),
|
||||||
|
Genre("Gender"),
|
||||||
|
Genre("Guerra"),
|
||||||
|
Genre("Harém"),
|
||||||
|
Genre("Histórico"),
|
||||||
|
Genre("Horror"),
|
||||||
|
Genre("Isekai"),
|
||||||
|
Genre("Josei"),
|
||||||
|
Genre("Magia"),
|
||||||
|
Genre("Manhua"),
|
||||||
|
Genre("Manhwa"),
|
||||||
|
Genre("Mecha"),
|
||||||
|
Genre("Medicina"),
|
||||||
|
Genre("Militar"),
|
||||||
|
Genre("Mistério"),
|
||||||
|
Genre("Musical"),
|
||||||
|
Genre("One-Shot"),
|
||||||
|
Genre("Psicológico"),
|
||||||
|
Genre("Romance"),
|
||||||
|
Genre("Sci-fi"),
|
||||||
|
Genre("Seinen"),
|
||||||
|
Genre("Shoujo"),
|
||||||
|
Genre("Shoujo Ai"),
|
||||||
|
Genre("Shounen"),
|
||||||
|
Genre("Shounen Ai"),
|
||||||
|
Genre("Slice of Life"),
|
||||||
|
Genre("Sobrenatural"),
|
||||||
|
Genre("Super Poderes"),
|
||||||
|
Genre("Suspense"),
|
||||||
|
Genre("Terror"),
|
||||||
|
Genre("Thriller"),
|
||||||
|
Genre("Tragédia"),
|
||||||
|
Genre("Vida Escolar"),
|
||||||
|
Genre("Webtoon"),
|
||||||
|
Genre("Yaoi"),
|
||||||
|
Genre("Yuri"),
|
||||||
|
Genre("Zumbi")
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun String.toDate(): Long {
|
||||||
|
return try {
|
||||||
|
DATE_FORMATTER.parse(this)?.time ?: 0L
|
||||||
|
} catch (e: ParseException) {
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Response.asJson(): JsonElement = JSON_PARSER.parse(body()!!.string())
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ACCEPT = "application/json, text/plain, */*"
|
||||||
|
private const val ACCEPT_IMAGE = "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"
|
||||||
|
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"
|
||||||
|
// By request of site owner. Detailed at Issue #4912 (in Portuguese).
|
||||||
|
private val USER_AGENT = "Tachiyomi " + System.getProperty("http.agent")
|
||||||
|
|
||||||
|
private val JSON_PARSER by lazy { JsonParser() }
|
||||||
|
|
||||||
|
private val DATE_FORMATTER by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) }
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue