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:
Alessandro Jean 2020-11-21 19:37:56 -03:00 committed by GitHub
parent d7edce780a
commit bc7350c76d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 366 additions and 0 deletions

View File

@ -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

View File

@ -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) }
}
}