Remove TM extension. (#8609)

This commit is contained in:
Alessandro Jean 2021-08-17 22:17:58 -03:00 committed by GitHub
parent 51ff911e0e
commit c9651735f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 0 additions and 639 deletions

View File

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi.extension">
<application>
<activity
android:name=".pt.tsukimangas.TsukiMangasUrlActivity"
android:excludeFromRecents="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="tsukimangas.com"
android:pathPattern="/obra/..*/..*"
android:scheme="https" />
<data
android:host="www.tsukimangas.com"
android:pathPattern="/obra/..*/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,19 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Tsuki Mangás'
pkgNameSuffix = 'pt.tsukimangas'
extClass = '.TsukiMangas'
extVersionCode = 27
libVersion = '1.2'
containsNsfw = true
}
dependencies {
implementation project(':lib-ratelimit')
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

View File

@ -1,478 +0,0 @@
package eu.kanade.tachiyomi.extension.pt.tsukimangas
import eu.kanade.tachiyomi.annotations.Nsfw
import eu.kanade.tachiyomi.lib.ratelimit.SpecificHostRateLimitInterceptor
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 kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.IOException
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.TimeUnit
@Nsfw
class TsukiMangas : HttpSource() {
override val name = "Tsuki Mangás"
override val baseUrl = "https://tsukimangas.com"
override val lang = "pt-BR"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.addInterceptor(::tsukiAuthIntercept)
.addInterceptor(SpecificHostRateLimitInterceptor(baseUrl.toHttpUrl(), 1, 1, TimeUnit.SECONDS))
.build()
private val json: Json by injectLazy()
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Accept", ACCEPT)
.add("Accept-Language", ACCEPT_LANGUAGE)
.add("Referer", baseUrl)
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/api/v2/mangas?page=$page&title=&adult_content=false&filter=0", headers)
}
override fun popularMangaParse(response: Response): MangasPage {
val result = json.decodeFromString<TsukiPaginatedDto>(response.body!!.string())
val popularMangas = result.data.map(::popularMangaItemParse)
val hasNextPage = result.page < result.lastPage
return MangasPage(popularMangas, hasNextPage)
}
private fun popularMangaItemParse(manga: TsukiMangaDto) = SManga.create().apply {
val poster = manga.poster?.substringBefore("?")
title = manga.title + (if (manga.format == NOVEL_FORMAT_ID) " (Novel)" else "")
thumbnail_url = baseUrl + (if (poster.isNullOrEmpty()) EMPTY_COVER else "/imgs/$poster")
url = "/obra/${manga.id}/${manga.url}"
}
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/api/v2/home/lastests?page=$page", headers)
}
override fun latestUpdatesParse(response: Response): MangasPage {
val result = json.decodeFromString<TsukiPaginatedDto>(response.body!!.string())
val latestMangas = result.data.map(::latestMangaItemParse)
val hasNextPage = result.page < result.lastPage
return MangasPage(latestMangas, hasNextPage)
}
private fun latestMangaItemParse(manga: TsukiMangaDto) = SManga.create().apply {
val poster = manga.poster?.substringBefore("?")
title = manga.title + (if (manga.format == NOVEL_FORMAT_ID) " (Novel)" else "")
thumbnail_url = baseUrl + (if (poster.isNullOrEmpty()) EMPTY_COVER else "/imgs/$poster")
url = "/obra/${manga.id}/${manga.url}"
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.startsWith(PREFIX_ID_SEARCH) && query.matches(ID_SEARCH_PATTERN)) {
return mangaDetailsApiRequest(query.removePrefix(PREFIX_ID_SEARCH))
}
val newHeaders = headersBuilder()
.set("Referer", "$baseUrl/lista-completa")
.build()
val url = "$baseUrl/api/v2/mangas?page=$page".toHttpUrlOrNull()!!.newBuilder()
url.addQueryParameter("title", query)
// Some filters have to follow an order in the URL.
filters.filterIsInstance<GenreFilter>().firstOrNull()?.state
?.filter { it.state }
?.forEach { url.addQueryParameter("genres[]", it.name) }
filters.filterIsInstance<AdultFilter>().firstOrNull()
?.let {
if (it.state == Filter.TriState.STATE_INCLUDE) {
url.addQueryParameter("adult_content", "1")
} else if (it.state == Filter.TriState.STATE_EXCLUDE) {
url.addQueryParameter("adult_content", "false")
}
return@let null
}
filters.filterIsInstance<SortByFilter>().firstOrNull()
?.let { filter ->
if (filter.state!!.index == 0) {
url.addQueryParameter("filter", if (filter.state!!.ascending) "1" else "0")
} else {
url.addQueryParameter("filter", if (filter.state!!.ascending) "3" else "2")
}
}
filters.forEach { filter ->
when (filter) {
is DemographyFilter -> {
if (filter.state > 0) {
url.addQueryParameter("demography", filter.state.toString())
}
}
is FormatFilter -> {
if (filter.state > 0) {
url.addQueryParameter("format", filter.state.toString())
}
}
is StatusFilter -> {
if (filter.state > 0) {
url.addQueryParameter("status", (filter.state - 1).toString())
}
}
}
}
return GET(url.toString(), newHeaders)
}
override fun searchMangaParse(response: Response): MangasPage {
if (response.request.url.toString().contains("/mangas/")) {
val manga = mangaDetailsParse(response)
return MangasPage(listOf(manga), hasNextPage = false)
}
val result = json.decodeFromString<TsukiPaginatedDto>(response.body!!.string())
val searchResults = result.data.map(::searchMangaItemParse)
val hasNextPage = result.page < result.lastPage
return MangasPage(searchResults, hasNextPage)
}
private fun searchMangaItemParse(manga: TsukiMangaDto) = SManga.create().apply {
val poster = manga.poster?.substringBefore("?")
title = manga.title + (if (manga.format == NOVEL_FORMAT_ID) " (Novel)" else "")
thumbnail_url = baseUrl + (if (poster.isNullOrEmpty()) EMPTY_COVER else "/imgs/$poster")
url = "/obra/${manga.id}/${manga.url}"
}
// Workaround to allow "Open in browser" use the real URL.
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsApiRequest(manga.url))
.asObservableSuccess()
.map { response ->
mangaDetailsParse(response).apply { initialized = true }
}
}
private fun mangaDetailsApiRequest(mangaUrl: String): Request {
val mangaId = mangaUrl.substringAfter("obra/").substringBefore("/")
return GET("$baseUrl/api/v2/mangas/$mangaId", headers)
}
override fun mangaDetailsRequest(manga: SManga): Request {
val newHeaders = headersBuilder()
.removeAll("Accept")
.build()
return GET(baseUrl + manga.url, newHeaders)
}
override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply {
val mangaDto = json.decodeFromString<TsukiMangaDto>(response.body!!.string())
val poster = mangaDto.poster?.substringBefore("?")
title = mangaDto.title + (if (mangaDto.format == NOVEL_FORMAT_ID) " (Novel)" else "")
thumbnail_url = baseUrl + (if (poster.isNullOrEmpty()) EMPTY_COVER else "/imgs/$poster")
description = mangaDto.synopsis.orEmpty()
status = mangaDto.status.orEmpty().toStatus()
author = mangaDto.author.orEmpty()
artist = mangaDto.artist.orEmpty()
genre = mangaDto.genres.joinToString { it.genre }
url = "/obra/${mangaDto.id}/${mangaDto.url}"
}
override fun chapterListRequest(manga: SManga): Request {
val mangaId = manga.url.substringAfter("obra/").substringBefore("/")
val newHeaders = headersBuilder()
.set("Referer", baseUrl + manga.url)
.build()
return GET("$baseUrl/api/v2/chapters/$mangaId/all", newHeaders)
}
override fun chapterListParse(response: Response): List<SChapter> {
val mangaUrl = response.request.header("Referer")!!.substringAfter(baseUrl)
return json
.decodeFromString<List<TsukiChapterDto>>(response.body!!.string())
.flatMap { chapterListItemParse(it, mangaUrl) }
.reversed()
}
private fun chapterListItemParse(chapter: TsukiChapterDto, mangaUrl: String): List<SChapter> {
val mangaId = mangaUrl.substringAfter("obra/").substringBefore("/")
val mangaSlug = mangaUrl.substringAfterLast("/")
return chapter.versions.map { version ->
SChapter.create().apply {
name = "Cap. " + chapter.number +
(if (!chapter.title.isNullOrEmpty()) " - " + chapter.title else "")
chapter_number = chapter.number.toFloatOrNull() ?: -1f
scanlator = version.scans
.sortedBy { it.scan.name }
.joinToString { it.scan.name }
date_upload = version.createdAt.substringBefore(" ").toDate()
url = "/leitor/$mangaId/${version.id}/$mangaSlug/${chapter.number}"
}
}
}
override fun pageListRequest(chapter: SChapter): Request {
val newHeaders = headersBuilder()
.set("Referer", baseUrl + chapter.url)
.build()
val mangaId = chapter.url
.substringAfter("leitor/")
.substringBefore("/")
val versionId = chapter.url
.substringAfter("$mangaId/")
.substringBefore("/")
return GET("$baseUrl/api/v2/chapter/versions/$versionId", newHeaders)
}
override fun pageListParse(response: Response): List<Page> {
val result = json.decodeFromString<TsukiReaderDto>(response.body!!.string())
return result.pages.mapIndexed { i, page ->
val cdnUrl = "https://cdn${page.server}.tsukimangas.com"
Page(i, "$baseUrl/", cdnUrl + page.url)
}
}
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 fun tsukiAuthIntercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
// API returns 403, 429 or 1020 when the extension is getting blocked.
if (BLOCKING_CODES.contains(response.code)) {
response.close()
throw IOException(UA_DISABLED_MESSAGE)
}
return response
}
private class Genre(name: String) : Filter.CheckBox(name)
private class DemographyFilter(demographies: List<String>) : Filter.Select<String>("Demografia", demographies.toTypedArray())
private class FormatFilter(types: List<String>) : Filter.Select<String>("Formato", types.toTypedArray())
private class StatusFilter(statusList: List<String>) : Filter.Select<String>("Status", statusList.toTypedArray())
private class AdultFilter : Filter.TriState("Conteúdo adulto", STATE_EXCLUDE)
private class SortByFilter : Filter.Sort("Ordenar por", arrayOf("Visualizações", "Nota"), Selection(0, false))
private class GenreFilter(genres: List<Genre>) : Filter.Group<Genre>("Gêneros", genres)
override fun getFilterList(): FilterList = FilterList(
DemographyFilter(getDemographiesList()),
FormatFilter(getSerieFormats()),
StatusFilter(getStatusList()),
AdultFilter(),
SortByFilter(),
GenreFilter(getGenreList()),
)
private fun getDemographiesList(): List<String> = listOf(
"Todas",
"Shounen",
"Shoujo",
"Seinen",
"Josei"
)
private fun getSerieFormats(): List<String> = listOf(
"Todos",
"Mangá",
"Manhwa",
"Manhua",
"Novel"
)
private fun getStatusList(): List<String> = listOf(
"Todos",
"Ativo",
"Completo",
"Cancelado",
"Hiato"
)
// [...document.querySelectorAll(".multiselect:first-of-type .multiselect__element span span")]
// .map(i => `Genre("${i.innerHTML}")`).join(",\n")
private fun getGenreList(): List<Genre> = listOf(
Genre("4-Koma"),
Genre("Adaptação"),
Genre("Aliens"),
Genre("Animais"),
Genre("Antologia"),
Genre("Artes Marciais"),
Genre("Aventura"),
Genre("Ação"),
Genre("Colorido por fã"),
Genre("Comédia"),
Genre("Crime"),
Genre("Cross-dressing"),
Genre("Deliquentes"),
Genre("Demônios"),
Genre("Doujinshi"),
Genre("Drama"),
Genre("Ecchi"),
Genre("Esportes"),
Genre("Fantasia"),
Genre("Fantasmas"),
Genre("Filosófico"),
Genre("Gals"),
Genre("Ganhador de Prêmio"),
Genre("Garotas Monstro"),
Genre("Garotas Mágicas"),
Genre("Gastronomia"),
Genre("Gore"),
Genre("Harém"),
Genre("Harém Reverso"),
Genre("Hentai"),
Genre("Histórico"),
Genre("Horror"),
Genre("Incesto"),
Genre("Isekai"),
Genre("Jogos Tradicionais"),
Genre("Lolis"),
Genre("Long Strip"),
Genre("Mafia"),
Genre("Magia"),
Genre("Mecha"),
Genre("Medicina"),
Genre("Militar"),
Genre("Mistério"),
Genre("Monstros"),
Genre("Música"),
Genre("Ninjas"),
Genre("Obscenidade"),
Genre("Oficialmente Colorido"),
Genre("One-shot"),
Genre("Policial"),
Genre("Psicológico"),
Genre("Pós-apocalíptico"),
Genre("Realidade Virtual"),
Genre("Reencarnação"),
Genre("Romance"),
Genre("Samurais"),
Genre("Sci-Fi"),
Genre("Shotas"),
Genre("Shoujo Ai"),
Genre("Shounen Ai"),
Genre("Slice of Life"),
Genre("Sobrenatural"),
Genre("Sobrevivência"),
Genre("Super Herói"),
Genre("Thriller"),
Genre("Todo Colorido"),
Genre("Trabalho de Escritório"),
Genre("Tragédia"),
Genre("Troca de Gênero"),
Genre("Vampiros"),
Genre("Viagem no Tempo"),
Genre("Vida Escolar"),
Genre("Violência Sexual"),
Genre("Vídeo Games"),
Genre("Webcomic"),
Genre("Wuxia"),
Genre("Yaoi"),
Genre("Yuri"),
Genre("Zumbis")
)
private fun String.toDate(): Long {
return try {
DATE_FORMATTER.parse(this)?.time ?: 0L
} catch (e: ParseException) {
0L
}
}
private fun String.toStatus() = when {
contains("Ativo") -> SManga.ONGOING
contains("Completo") -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
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 CDN_1_URL = "https://cdn1.tsukimangas.com".toHttpUrl()
// private val CDN_2_URL = "https://cdn2.tsukimangas.com".toHttpUrl()
private val BLOCKING_CODES = arrayOf(403, 429, 1020)
private const val UA_DISABLED_MESSAGE = "O site está bloqueando o Tachiyomi. " +
"Aguarde com que eles parem de bloquear ou migre para outras fontes."
private const val EMPTY_COVER = "/ext/errorcapa.jpg"
private const val NOVEL_FORMAT_ID = 4
private val DATE_FORMATTER by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) }
const val PREFIX_ID_SEARCH = "id:"
private val ID_SEARCH_PATTERN = "^id:(\\d+)$".toRegex()
}
}

View File

@ -1,78 +0,0 @@
package eu.kanade.tachiyomi.extension.pt.tsukimangas
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class TsukiAuthRequestDto(
val username: String,
val password: String
)
@Serializable
data class TsukiAuthResultDto(
val token: String? = null
)
@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 format: Int? = 1,
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
)

View File

@ -1,37 +0,0 @@
package eu.kanade.tachiyomi.extension.pt.tsukimangas
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
class TsukiMangasUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val titleId = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", TsukiMangas.PREFIX_ID_SEARCH + titleId)
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("TsukiMangasUrlActivity", e.toString())
}
} else {
Log.e("TsukiMangasUrlActivity", "Could not parse URI from intent $intent")
}
finish()
exitProcess(0)
}
}