mirror of
https://github.com/keiyoushi/extensions-source.git
synced 2024-11-22 10:22:47 +01:00
New source: pt/Tsuki Mangás (#597)
* feat: Create Tsuki Mangás base * feat: Implement popular manga page * feat: Implement latest updates page * feat: Implement search page * feat: Implement manga details page * fix: Fix URL intent handler * fix: Fix webview url * feat: Implement chapter list page * feat: Implement page list * fix: Fix chapter URLs Kotlinx-serialization moment * feat: Apply rate limit to image CDNs * refactor: Make the API path a separate constant * chore: Add source icon ... Actually they don't have a icon yet, they're just using the "TSUKI" text, so I did the same in the icon. it may be updated later, when they create a proper icon. * fix: Fix random http 404 in pages * fix: Prevent multiple wrong requests * refactor: Apply suggestion - set custom interceptor before ratelimit
This commit is contained in:
parent
646334b6b2
commit
2ea74d8170
22
src/pt/tsukimangas/AndroidManifest.xml
Normal file
22
src/pt/tsukimangas/AndroidManifest.xml
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<activity
|
||||
android:name=".pt.tsukimangas.TsukiMangasUrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="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="tsuki-mangas.com"
|
||||
android:pathPattern="/obra/..*"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
8
src/pt/tsukimangas/build.gradle
Normal file
8
src/pt/tsukimangas/build.gradle
Normal file
@ -0,0 +1,8 @@
|
||||
ext {
|
||||
extName = 'Tsuki Mangás'
|
||||
extClass = '.TsukiMangas'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/pt/tsukimangas/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/pt/tsukimangas/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
BIN
src/pt/tsukimangas/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/pt/tsukimangas/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
BIN
src/pt/tsukimangas/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/pt/tsukimangas/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
BIN
src/pt/tsukimangas/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/pt/tsukimangas/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.3 KiB |
BIN
src/pt/tsukimangas/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/pt/tsukimangas/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.9 KiB |
@ -0,0 +1,252 @@
|
||||
package eu.kanade.tachiyomi.extension.pt.tsukimangas
|
||||
|
||||
import eu.kanade.tachiyomi.extension.pt.tsukimangas.dto.ChapterListDto
|
||||
import eu.kanade.tachiyomi.extension.pt.tsukimangas.dto.CompleteMangaDto
|
||||
import eu.kanade.tachiyomi.extension.pt.tsukimangas.dto.MangaListDto
|
||||
import eu.kanade.tachiyomi.extension.pt.tsukimangas.dto.PageListDto
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class TsukiMangas : HttpSource() {
|
||||
|
||||
override val name = "Tsuki Mangás"
|
||||
|
||||
override val baseUrl = "https://tsuki-mangas.com"
|
||||
|
||||
private val API_URL = baseUrl + API_PATH
|
||||
|
||||
override val lang = "pt-BR"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client by lazy {
|
||||
network.client.newBuilder()
|
||||
.addInterceptor(::imageCdnSwapper)
|
||||
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
||||
.rateLimitHost(MAIN_CDN.toHttpUrl(), 1)
|
||||
.rateLimitHost(SECONDARY_CDN.toHttpUrl(), 1)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder().add("Referer", "$baseUrl/")
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
// ============================== Popular ===============================
|
||||
override fun popularMangaRequest(page: Int) = GET("$API_URL/mangas?page=$page&filter=0", headers)
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val item = response.parseAs<MangaListDto>()
|
||||
val mangas = item.data.map {
|
||||
SManga.create().apply {
|
||||
url = "/obra" + it.entryPath
|
||||
thumbnail_url = baseUrl + it.imagePath
|
||||
title = it.title
|
||||
}
|
||||
}
|
||||
val hasNextPage = item.page < item.lastPage
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
// Yes, "lastests". High IQ move.
|
||||
// Also yeah, there's a "?format=0" glued to the page number. Without this,
|
||||
// the request will blow up with a HTTP 500.
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$API_URL/home/lastests?page=$page%3Fformat%3D0", headers)
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
|
||||
val id = query.removePrefix(PREFIX_SEARCH)
|
||||
client.newCall(GET("$API_URL/mangas/$id", headers))
|
||||
.asObservableSuccess()
|
||||
.map(::searchMangaByIdParse)
|
||||
} else {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchMangaByIdParse(response: Response): MangasPage {
|
||||
val details = mangaDetailsParse(response)
|
||||
return MangasPage(listOf(details), false)
|
||||
}
|
||||
|
||||
override fun getFilterList() = TsukiMangasFilters.FILTER_LIST
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val params = TsukiMangasFilters.getSearchParameters(filters)
|
||||
val url = "$API_URL/mangas".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("page", page.toString())
|
||||
.addQueryParameter("title", query.trim())
|
||||
.addIfNotBlank("filter", params.filter)
|
||||
.addIfNotBlank("format", params.format)
|
||||
.addIfNotBlank("status", params.status)
|
||||
.addIfNotBlank("adult_content", params.adult)
|
||||
.apply {
|
||||
params.genres.forEach { addQueryParameter("genres[]", it) }
|
||||
params.tags.forEach { addQueryParameter("tags[]", it) }
|
||||
}.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response) = popularMangaParse(response)
|
||||
|
||||
// =========================== Manga Details ============================
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
val id = manga.url.getMangaId()
|
||||
return GET("$API_URL/mangas/$id", headers)
|
||||
}
|
||||
|
||||
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url
|
||||
|
||||
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
|
||||
val mangaDto = response.parseAs<CompleteMangaDto>()
|
||||
url = "/obra" + mangaDto.entryPath
|
||||
thumbnail_url = baseUrl + mangaDto.imagePath
|
||||
title = mangaDto.title
|
||||
artist = mangaDto.staff
|
||||
genre = mangaDto.genres.joinToString { it.genre }
|
||||
status = parseStatus(mangaDto.status.orEmpty())
|
||||
description = buildString {
|
||||
mangaDto.synopsis?.also { append("$it\n\n") }
|
||||
if (mangaDto.titles.isNotEmpty()) {
|
||||
append("Títulos alternativos: ${mangaDto.titles.joinToString { it.title }}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String) = when (status) {
|
||||
"Ativo" -> SManga.ONGOING
|
||||
"Completo" -> SManga.COMPLETED
|
||||
"Hiato" -> SManga.ON_HIATUS
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
// ============================== Chapters ==============================
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
val id = manga.url.getMangaId()
|
||||
return GET("$API_URL/chapters/$id/all", headers)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val parsed = response.parseAs<ChapterListDto>()
|
||||
|
||||
return parsed.chapters.reversed().map {
|
||||
SChapter.create().apply {
|
||||
name = "Capítulo ${it.number}"
|
||||
// Sometimes the "number" attribute have letters or other characters,
|
||||
// which could ruin the automatic chapter number recognition system.
|
||||
chapter_number = it.number.trim { char -> !char.isDigit() }.toFloatOrNull() ?: 1F
|
||||
|
||||
url = "$API_PATH/chapter/versions/${it.versionId}"
|
||||
|
||||
date_upload = it.created_at.orEmpty().toDate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================== Pages ================================
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val data = response.parseAs<PageListDto>()
|
||||
val sortedPages = data.pages.sortedBy { it.url.substringAfterLast("/") }
|
||||
val host = getImageHost(sortedPages.first().url)
|
||||
|
||||
return sortedPages.mapIndexed { index, item ->
|
||||
Page(index, imageUrl = host + item.url)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The source normally uses only one CDN per chapter, so we'll try to get
|
||||
* the correct CDN before loading all pages, leaving the [imageCdnSwapper]
|
||||
* as the last choice.
|
||||
*/
|
||||
private fun getImageHost(path: String): String {
|
||||
val pageCheck = super.client.newCall(GET(MAIN_CDN + path, headers)).execute()
|
||||
pageCheck.close()
|
||||
return when {
|
||||
!pageCheck.isSuccessful -> SECONDARY_CDN
|
||||
else -> MAIN_CDN
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
private inline fun <reified T> Response.parseAs(): T = use {
|
||||
json.decodeFromStream(it.body.byteStream())
|
||||
}
|
||||
|
||||
private fun HttpUrl.Builder.addIfNotBlank(query: String, value: String): HttpUrl.Builder {
|
||||
if (value.isNotBlank()) addQueryParameter(query, value)
|
||||
return this
|
||||
}
|
||||
|
||||
private fun String.getMangaId() = substringAfter("/obra/").substringBefore("/")
|
||||
|
||||
private fun String.toDate(): Long {
|
||||
return runCatching { DATE_FORMATTER.parse(trim())?.time }
|
||||
.getOrNull() ?: 0L
|
||||
}
|
||||
|
||||
/**
|
||||
* This may sound stupid (because it is), but a similar approach exists
|
||||
* in the source itself, because they somehow don't know to which server
|
||||
* each page belongs to. I thought the `server` attribute returned by page
|
||||
* objects would be enough, but it turns out that it isn't. Day ruined.
|
||||
*/
|
||||
private fun imageCdnSwapper(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val response = chain.proceed(request)
|
||||
|
||||
return if (response.code != 404) {
|
||||
response
|
||||
} else {
|
||||
response.close()
|
||||
val url = request.url.toString()
|
||||
val newUrl = when {
|
||||
url.startsWith(MAIN_CDN) -> url.replace("$MAIN_CDN/tsuki", SECONDARY_CDN)
|
||||
url.startsWith(SECONDARY_CDN) -> url.replace(SECONDARY_CDN, "$MAIN_CDN/tsuki")
|
||||
else -> url
|
||||
}
|
||||
|
||||
val newRequest = GET(newUrl, request.headers)
|
||||
chain.proceed(newRequest)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PREFIX_SEARCH = "id:"
|
||||
|
||||
private val DATE_FORMATTER by lazy {
|
||||
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH)
|
||||
}
|
||||
|
||||
private const val MAIN_CDN = "https://cdn.tsuki-mangas.com/tsuki"
|
||||
private const val SECONDARY_CDN = "https://cdn2.tsuki-mangas.com"
|
||||
private const val API_PATH = "/api/v2"
|
||||
}
|
||||
}
|
@ -0,0 +1,475 @@
|
||||
package eu.kanade.tachiyomi.extension.pt.tsukimangas
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
|
||||
object TsukiMangasFilters {
|
||||
open class CheckBoxFilterList(name: String, val pairs: Array<Pair<String, String>>) :
|
||||
Filter.Group<Filter.CheckBox>(name, pairs.map { CheckBoxVal(it.first) })
|
||||
|
||||
private class CheckBoxVal(name: String) : Filter.CheckBox(name, false)
|
||||
|
||||
private inline fun <reified R> FilterList.parseCheckbox(
|
||||
options: Array<Pair<String, String>>,
|
||||
): Sequence<String> {
|
||||
return (first { it is R } as CheckBoxFilterList).state
|
||||
.asSequence()
|
||||
.filter { it.state }
|
||||
.map { checkbox -> options.find { it.first == checkbox.name }!!.second }
|
||||
}
|
||||
|
||||
open class SelectFilter(
|
||||
displayName: String,
|
||||
val vals: Array<Pair<String, String>>,
|
||||
) : Filter.Select<String>(
|
||||
displayName,
|
||||
vals.map { it.first }.toTypedArray(),
|
||||
) {
|
||||
val selected get() = vals[state].second
|
||||
}
|
||||
|
||||
private inline fun <reified R> FilterList.getSelected(): String {
|
||||
return (first { it is R } as SelectFilter).selected
|
||||
}
|
||||
|
||||
internal class GenresFilter : CheckBoxFilterList("Gêneros", GENRES)
|
||||
internal class TagsFilter : CheckBoxFilterList("Tags", TAGS)
|
||||
|
||||
internal class FormatFilter : SelectFilter("Formato", FORMATS)
|
||||
internal class AdultFilter : SelectFilter("Mostrar conteúdo adulto", ADULT_OPTIONS)
|
||||
internal class ContentFilter : SelectFilter("Filtro", CONTENT_FILTER)
|
||||
internal class StatusFilter : SelectFilter("Status", STATUS)
|
||||
|
||||
internal val FILTER_LIST get() = FilterList(
|
||||
GenresFilter(),
|
||||
TagsFilter(),
|
||||
|
||||
FormatFilter(),
|
||||
AdultFilter(),
|
||||
ContentFilter(),
|
||||
StatusFilter(),
|
||||
)
|
||||
|
||||
internal data class FilterSearchParams(
|
||||
val genres: Sequence<String> = emptySequence(),
|
||||
val tags: Sequence<String> = emptySequence(),
|
||||
|
||||
val format: String = "",
|
||||
val adult: String = "",
|
||||
val filter: String = "",
|
||||
val status: String = "",
|
||||
)
|
||||
|
||||
internal fun getSearchParameters(filters: FilterList): FilterSearchParams {
|
||||
if (filters.isEmpty()) return FilterSearchParams()
|
||||
|
||||
return FilterSearchParams(
|
||||
filters.parseCheckbox<GenresFilter>(GENRES),
|
||||
filters.parseCheckbox<TagsFilter>(TAGS),
|
||||
|
||||
filters.getSelected<FormatFilter>(),
|
||||
filters.getSelected<AdultFilter>(),
|
||||
filters.getSelected<ContentFilter>(),
|
||||
filters.getSelected<StatusFilter>(),
|
||||
)
|
||||
}
|
||||
|
||||
private val GENRES = arrayOf(
|
||||
"4-Koma",
|
||||
"Adaptação",
|
||||
"Aliens",
|
||||
"Animais",
|
||||
"Antologia",
|
||||
"Artes Marciais",
|
||||
"Aventura",
|
||||
"Ação",
|
||||
"Colorido por fã",
|
||||
"Comédia",
|
||||
"Criado pelo Usuário",
|
||||
"Crime",
|
||||
"Cross-dressing",
|
||||
"Deliquentes",
|
||||
"Demônios",
|
||||
"Doujinshi",
|
||||
"Drama",
|
||||
"Ecchi",
|
||||
"Esportes",
|
||||
"Fantasia",
|
||||
"Fantasmas",
|
||||
"Filosófico",
|
||||
"Gals",
|
||||
"Ganhador de Prêmio",
|
||||
"Garotas Monstro",
|
||||
"Garotas Mágicas",
|
||||
"Gastronomia",
|
||||
"Gore",
|
||||
"Harém",
|
||||
"Harém Reverso",
|
||||
"Hentai",
|
||||
"Histórico",
|
||||
"Horror",
|
||||
"Incesto",
|
||||
"Isekai",
|
||||
"Jogos Tradicionais",
|
||||
"Lolis",
|
||||
"Long Strip",
|
||||
"Mafia",
|
||||
"Magia",
|
||||
"Mecha",
|
||||
"Medicina",
|
||||
"Militar",
|
||||
"Mistério",
|
||||
"Monstros",
|
||||
"Música",
|
||||
"Ninjas",
|
||||
"Obscenidade",
|
||||
"Oficialmente Colorido",
|
||||
"One-shot",
|
||||
"Policial",
|
||||
"Psicológico",
|
||||
"Pós-apocalíptico",
|
||||
"Realidade Virtual",
|
||||
"Reencarnação",
|
||||
"Romance",
|
||||
"Samurais",
|
||||
"Sci-Fi",
|
||||
"Shotas",
|
||||
"Shoujo Ai",
|
||||
"Shounen Ai",
|
||||
"Slice of Life",
|
||||
"Sobrenatural",
|
||||
"Sobrevivência",
|
||||
"Super Herói",
|
||||
"Thriller",
|
||||
"Todo Colorido",
|
||||
"Trabalho de Escritório",
|
||||
"Tragédia",
|
||||
"Troca de Gênero",
|
||||
"Vampiros",
|
||||
"Viagem no Tempo",
|
||||
"Vida Escolar",
|
||||
"Violência Sexual",
|
||||
"Vídeo Games",
|
||||
"Webcomic",
|
||||
"Wuxia",
|
||||
"Yaoi",
|
||||
"Yuri",
|
||||
"Zumbis",
|
||||
).map { Pair(it, it) }.toTypedArray()
|
||||
|
||||
private val TAGS = arrayOf(
|
||||
Pair("4-Koma", "4-Koma"),
|
||||
Pair("Acromático", "Achromatic"),
|
||||
Pair("Adoção", "Adoption"),
|
||||
Pair("Agricultura", "Agriculture"),
|
||||
Pair("Airsoft", "Airsoft"),
|
||||
Pair("Alienígenas", "Aliens"),
|
||||
Pair("Alquimia", "Alchemy"),
|
||||
Pair("Amadurecimento", "Coming Of Age"),
|
||||
Pair("Ambiental", "Environmental"),
|
||||
Pair("Amnésia", "Amnesia"),
|
||||
Pair("Amor entre Adolescentes", "Teens' Love"),
|
||||
Pair("Amor entre Homens", "Boys' Love"),
|
||||
Pair("Anacronismo", "Anachronism"),
|
||||
Pair("Animais", "Animals"),
|
||||
Pair("Anjos", "Angels"),
|
||||
Pair("Anti-Herói", "Anti-Hero"),
|
||||
Pair("Antologia", "Anthology"),
|
||||
Pair("Antropomorfismo", "Anthropomorphism"),
|
||||
Pair("Anúncio Publicitário", "Advertisement"),
|
||||
Pair("Ao Ar Livre", "Outdoor"),
|
||||
Pair("Arco e Flecha", "Archery"),
|
||||
Pair("Armas", "Guns"),
|
||||
Pair("Artes Marciais", "Martial Arts"),
|
||||
Pair("Assassinos", "Assassins"),
|
||||
Pair("Assexual", "Asexual"),
|
||||
Pair("Astronomia", "Astronomy"),
|
||||
Pair("Atletismo", "Athletics"),
|
||||
Pair("Atuação", "Acting"),
|
||||
Pair("Autobiográfico", "Autobiographical"),
|
||||
Pair("Aviação", "Aviation"),
|
||||
Pair("Badminton", "Badminton"),
|
||||
Pair("Banda", "Band"),
|
||||
Pair("Bar", "Bar"),
|
||||
Pair("Barreira de Idioma Estrangeiro", "Foreign Language Barrier"),
|
||||
Pair("Basquete", "Basketball"),
|
||||
Pair("Batalha Real", "Battle Royale"),
|
||||
Pair("Batalha de Cartas", "Card Battle"),
|
||||
Pair("Beisebol", "Baseball"),
|
||||
Pair("Biográfico", "Biographical"),
|
||||
Pair("Bissexual", "Bisexual"),
|
||||
Pair("Bombeiros", "Firefighters"),
|
||||
Pair("Boxe", "Boxing"),
|
||||
Pair("Bruxa", "Witch"),
|
||||
Pair("Bullying", "Bullying"),
|
||||
Pair("CGI Completo", "Full CGI"),
|
||||
Pair("CGI", "CGI"),
|
||||
Pair("Caligrafia", "Calligraphy"),
|
||||
Pair("Canibalismo", "Cannibalism"),
|
||||
Pair("Carros", "Cars"),
|
||||
Pair("Casamento", "Marriage"),
|
||||
Pair("Centauro", "Centaur"),
|
||||
Pair("Chibi", "Chibi"),
|
||||
Pair("Chuunibyou", "Chuunibyou"),
|
||||
Pair("Ciborgue", "Cyborg"),
|
||||
Pair("Ciclismo", "Cycling"),
|
||||
Pair("Ciclomotores", "Mopeds"),
|
||||
Pair("Circo", "Circus"),
|
||||
Pair("Civilização Perdida", "Lost Civilization"),
|
||||
Pair("Clone", "Clone"),
|
||||
Pair("Clube Escolar", "School Club"),
|
||||
Pair("Comida", "Food"),
|
||||
Pair("Comédia Surrealista", "Surreal Comedy"),
|
||||
Pair("Conspiração", "Conspiracy"),
|
||||
Pair("Conto de Fadas", "Fairy Tale"),
|
||||
Pair("Cor Completa", "Full Color"),
|
||||
Pair("Cosplay", "Cosplay"),
|
||||
Pair("Crime", "Crime"),
|
||||
Pair("Crossover", "Crossover"),
|
||||
Pair("Cultivo", "Cultivation"),
|
||||
Pair("Culto", "Cult"),
|
||||
Pair("Cultura Otaku", "Otaku Culture"),
|
||||
Pair("Cyberpunk", "Cyberpunk"),
|
||||
Pair("Dança", "Dancing"),
|
||||
Pair("Deficiência", "Disability"),
|
||||
Pair("Delinquentes", "Delinquents"),
|
||||
Pair("Demônios", "Demons"),
|
||||
Pair("Denpa", "Denpa"),
|
||||
Pair("Desenho", "Drawing"),
|
||||
Pair("Desenvolvimento de Software", "Software Development"),
|
||||
Pair("Deserto", "Desert"),
|
||||
Pair("Detetive", "Detective"),
|
||||
Pair("Deuses", "Gods"),
|
||||
Pair("Diferença de Idade", "Age Gap"),
|
||||
Pair("Dinossauros", "Dinosaurs"),
|
||||
Pair("Distópico", "Dystopian"),
|
||||
Pair("Donzela do Santuário", "Shrine Maiden"),
|
||||
Pair("Dragões", "Dragons"),
|
||||
Pair("Drogas", "Drugs"),
|
||||
Pair("Dullahan", "Dullahan"),
|
||||
Pair("E-Sports", "E-Sports"),
|
||||
Pair("Economia", "Economics"),
|
||||
Pair("Educacional", "Educational"),
|
||||
Pair("Elenco Conjunto", "Ensemble Cast"),
|
||||
Pair("Elenco Principalmente Adolescente", "Primarily Teen Cast"),
|
||||
Pair("Elenco Principalmente Adulto", "Primarily Adult Cast"),
|
||||
Pair("Elenco Principalmente Feminino", "Primarily Female Cast"),
|
||||
Pair("Elenco Principalmente Infantil", "Primarily Child Cast"),
|
||||
Pair("Elenco Principalmente Masculino", "Primarily Male Cast"),
|
||||
Pair("Elfo", "Elf"),
|
||||
Pair("Empregadas", "Maids"),
|
||||
Pair("Episódico", "Episodic"),
|
||||
Pair("Ero Guro", "Ero Guro"),
|
||||
Pair("Escola", "School"),
|
||||
Pair("Escravidão", "Slavery"),
|
||||
Pair("Escrita", "Writing"),
|
||||
Pair("Esgrima", "Fencing"),
|
||||
Pair("Espaço", "Space"),
|
||||
Pair("Espionagem", "Espionage"),
|
||||
Pair("Esqueleto", "Skeleton"),
|
||||
Pair("Faculdade", "College"),
|
||||
Pair("Fada", "Fairy"),
|
||||
Pair("Família Encontrada", "Found Family"),
|
||||
Pair("Fantasia Urbana", "Urban Fantasy"),
|
||||
Pair("Fantasma", "Ghost"),
|
||||
Pair("Filosofia", "Philosophy"),
|
||||
Pair("Fitness", "Fitness"),
|
||||
Pair("Flash", "Flash"),
|
||||
Pair("Fotografia", "Photography"),
|
||||
Pair("Freira", "Nun"),
|
||||
Pair("Fugitivo", "Fugitive"),
|
||||
Pair("Futebol Americano", "American Football"),
|
||||
Pair("Futebol", "Football"),
|
||||
Pair("Gangues", "Gangs"),
|
||||
Pair("Garota Monstro", "Monster Girl"),
|
||||
Pair("Garotas Bonitinhas Fazendo Coisas Bonitinhas", "Cute Girls Doing Cute Things"),
|
||||
Pair("Garoto Feminino", "Femboy"),
|
||||
Pair("Garoto Monstro", "Monster Boy"),
|
||||
Pair("Garotos Bonitinhos Fazendo Coisas Bonitinhas", "Cute Boys Doing Cute Things"),
|
||||
Pair("Go", "Go"),
|
||||
Pair("Goblin", "Goblin"),
|
||||
Pair("Golfe", "Golf"),
|
||||
Pair("Gore", "Gore"),
|
||||
Pair("Guerra", "War"),
|
||||
Pair("Gyaru", "Gyaru"),
|
||||
Pair("Gêmeos", "Twins"),
|
||||
Pair("Handebol", "Handball"),
|
||||
Pair("Harém Feminino", "Female Harem"),
|
||||
Pair("Harém Masculino", "Male Harem"),
|
||||
Pair("Harém com Gêneros Mistos", "Mixed Gender Harem"),
|
||||
Pair("Henshin", "Henshin"),
|
||||
Pair("Heterossexual", "Heterosexual"),
|
||||
Pair("Hikikomori", "Hikikomori"),
|
||||
Pair("Histórico", "Historical"),
|
||||
Pair("Horror Corporal", "Body Horror"),
|
||||
Pair("Horror Cósmico", "Cosmic Horror"),
|
||||
Pair("Identidades Dissociativas", "Dissociative Identities"),
|
||||
Pair("Inteligência Artificial", "Artificial Intelligence"),
|
||||
Pair("Isekai", "Isekai"),
|
||||
Pair("Iyashikei", "Iyashikei"),
|
||||
Pair("Jogo da Morte", "Death Game"),
|
||||
Pair("Jogos Eletrônicos", "Video Games"),
|
||||
Pair("Jogos de Azar", "Gambling"),
|
||||
Pair("Judô", "Judo"),
|
||||
Pair("Kaiju", "Kaiju"),
|
||||
Pair("Karuta", "Karuta"),
|
||||
Pair("Kemonomimi", "Kemonomimi"),
|
||||
Pair("Kuudere", "Kuudere"),
|
||||
Pair("Lacrosse", "Lacrosse"),
|
||||
Pair("Literatura Clássica", "Classic Literature"),
|
||||
Pair("Lobisomem", "Werewolf"),
|
||||
Pair("Luta Livre", "Wrestling"),
|
||||
Pair("Luta com Espada", "Swordplay"),
|
||||
Pair("Luta com Lança", "Spearplay"),
|
||||
Pair("Líder de Torcida", "Cheerleading"),
|
||||
Pair("Magia", "Magic"),
|
||||
Pair("Mahjong", "Mahjong"),
|
||||
Pair("Manipulação de Memória", "Memory Manipulation"),
|
||||
Pair("Manipulação do Tempo", "Time Manipulation"),
|
||||
Pair("Maquiagem", "Makeup"),
|
||||
Pair("Maria-rapaz", "Tomboy"),
|
||||
Pair("Masmorra", "Dungeon"),
|
||||
Pair("Medicina", "Medicine"),
|
||||
Pair("Mergulho", "Scuba Diving"),
|
||||
Pair("Meta", "Meta"),
|
||||
Pair("Militar", "Military"),
|
||||
Pair("Mitologia", "Mythology"),
|
||||
Pair("Moda", "Fashion"),
|
||||
Pair("Mordomo", "Butler"),
|
||||
Pair("Motocicletas", "Motorcycles"),
|
||||
Pair("Mudança de Forma", "Shapeshifting"),
|
||||
Pair("Mulher de Escritório", "Office Lady"),
|
||||
Pair("Mundo Virtual", "Virtual World"),
|
||||
Pair("Musical", "Musical"),
|
||||
Pair("Máfia", "Mafia"),
|
||||
Pair("Natação", "Swimming"),
|
||||
Pair("Navios", "Ships"),
|
||||
Pair("Necromancia", "Necromancy"),
|
||||
Pair("Nekomimi", "Nekomimi"),
|
||||
Pair("Ninja", "Ninja"),
|
||||
Pair("Noir", "Noir"),
|
||||
Pair("Nudez", "Nudity"),
|
||||
Pair("Não Ficção", "Non-Fiction"),
|
||||
Pair("Oiran", "Oiran"),
|
||||
Pair("Ojou-Sama", "Ojou-Sama"),
|
||||
Pair("Ordem Acrônica", "Achronological Order"),
|
||||
Pair("Pandemia", "Pandemic"),
|
||||
Pair("Parkour", "Parkour"),
|
||||
Pair("Paródia", "Parody"),
|
||||
Pair("Patinagem no Gelo", "Ice Skating"),
|
||||
Pair("Pele Bronzeada", "Tanned Skin"),
|
||||
Pair("Pesca", "Fishing"),
|
||||
Pair("Piratas", "Pirates"),
|
||||
Pair("Polícia", "Police"),
|
||||
Pair("Política", "Politics"),
|
||||
Pair("Ponto de Vista", "POV"),
|
||||
Pair("Prisão", "Prison"),
|
||||
Pair("Professor(a)", "Teacher"),
|
||||
Pair("Protagonista Feminina", "Female Protagonist"),
|
||||
Pair("Protagonista Masculino", "Male Protagonist"),
|
||||
Pair("Pular no Tempo", "Time Skip"),
|
||||
Pair("Puppetry", "Puppetry"),
|
||||
Pair("Pós-Apocalíptico", "Post-Apocalyptic"),
|
||||
Pair("Pós-Vida", "Afterlife"),
|
||||
Pair("Pôquer", "Poker"),
|
||||
Pair("Quimera", "Chimera"),
|
||||
Pair("Rakugo", "Rakugo"),
|
||||
Pair("Reabilitação", "Rehabilitation"),
|
||||
Pair("Realidade Aumentada", "Augmented Reality"),
|
||||
Pair("Reencarnação", "Reincarnation"),
|
||||
Pair("Regressão de Idade", "Age Regression"),
|
||||
Pair("Religião", "Religion"),
|
||||
Pair("Robô Real", "Real Robot"),
|
||||
Pair("Robôs", "Robots"),
|
||||
Pair("Rotoscopia", "Rotoscoping"),
|
||||
Pair("Rugby", "Rugby"),
|
||||
Pair("Rural", "Rural"),
|
||||
Pair("Samurai", "Samurai"),
|
||||
Pair("Sem Diálogo", "No Dialogue"),
|
||||
Pair("Sem Gênero", "Agender"),
|
||||
Pair("Sem-teto", "Homeless"),
|
||||
Pair("Sereia", "Mermaid"),
|
||||
Pair("Shogi", "Shogi"),
|
||||
Pair("Skateboarding", "Skateboarding"),
|
||||
Pair("Slapstick", "Slapstick"),
|
||||
Pair("Sobrevivência", "Survival"),
|
||||
Pair("Steampunk", "Steampunk"),
|
||||
Pair("Stop Motion", "Stop Motion"),
|
||||
Pair("Suicídio", "Suicide"),
|
||||
Pair("Sumô", "Sumo"),
|
||||
Pair("Super Robô", "Super Robot"),
|
||||
Pair("Super-herói", "Superhero"),
|
||||
Pair("Superpoder", "Super Power"),
|
||||
Pair("Surf", "Surfing"),
|
||||
Pair("Sátira", "Satire"),
|
||||
Pair("Súcubo", "Succubus"),
|
||||
Pair("Tanques", "Tanks"),
|
||||
Pair("Temas LGBTQ+", "LGBTQ+ Themes"),
|
||||
Pair("Terrorismo", "Terrorism"),
|
||||
Pair("Tokusatsu", "Tokusatsu"),
|
||||
Pair("Tortura", "Torture"),
|
||||
Pair("Trabalho", "Work"),
|
||||
Pair("Tragédia", "Tragedy"),
|
||||
Pair("Transgênero", "Transgender"),
|
||||
Pair("Travestismo", "Crossdressing"),
|
||||
Pair("Trens", "Trains"),
|
||||
Pair("Triângulo Amoroso", "Love Triangle"),
|
||||
Pair("Troca de Corpos", "Body Swapping"),
|
||||
Pair("Troca de Gênero", "Gender Bending"),
|
||||
Pair("Tríades", "Triads"),
|
||||
Pair("Tsundere", "Tsundere"),
|
||||
Pair("Tênis de Mesa", "Table Tennis"),
|
||||
Pair("Tênis", "Tennis"),
|
||||
Pair("Universo Alternativo", "Alternate Universe"),
|
||||
Pair("Urbano", "Urban"),
|
||||
Pair("VTuber", "VTuber"),
|
||||
Pair("Vampiro", "Vampire"),
|
||||
Pair("Viagem", "Travel"),
|
||||
Pair("Vida Familiar", "Family Life"),
|
||||
Pair("Vikings", "Vikings"),
|
||||
Pair("Vilã", "Villainess"),
|
||||
Pair("Vingança", "Revenge"),
|
||||
Pair("Vôlei", "Volleyball"),
|
||||
Pair("Wuxia", "Wuxia"),
|
||||
Pair("Yakuza", "Yakuza"),
|
||||
Pair("Yandere", "Yandere"),
|
||||
Pair("Youkai", "Youkai"),
|
||||
Pair("Yuri", "Yuri"),
|
||||
Pair("Zumbi", "Zombie"),
|
||||
Pair("Ídolo", "Idol"),
|
||||
Pair("Ópera Espacial", "Space Opera"),
|
||||
Pair("Órfão/Órfã", "Orphan"),
|
||||
)
|
||||
|
||||
private val ANY = Pair("Qualquer um", "")
|
||||
|
||||
private val FORMATS = arrayOf(
|
||||
ANY,
|
||||
Pair("Mangá", "1"),
|
||||
Pair("Manhwa", "2"),
|
||||
Pair("Manhua", "3"),
|
||||
Pair("Novel", "4"),
|
||||
)
|
||||
|
||||
private val ADULT_OPTIONS = arrayOf(
|
||||
ANY,
|
||||
Pair("Sim", "1"),
|
||||
Pair("Não", "0"),
|
||||
)
|
||||
|
||||
private val CONTENT_FILTER = arrayOf(
|
||||
ANY,
|
||||
Pair("Mais popular", "0"),
|
||||
Pair("Menos popular", "1"),
|
||||
Pair("Melhores notas", "2"),
|
||||
Pair("Piores notas", "3"),
|
||||
)
|
||||
|
||||
private val STATUS = arrayOf(
|
||||
ANY,
|
||||
Pair("Ativo", "0"),
|
||||
Pair("Completo", "1"),
|
||||
Pair("Cancelado", "2"),
|
||||
Pair("Hiato", "3"),
|
||||
)
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
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
|
||||
|
||||
/**
|
||||
* Springboard that accepts https://tsuki-mangas.com/obra/<id>/<item> intents
|
||||
* and redirects them to the main Tachiyomi process.
|
||||
*/
|
||||
class TsukiMangasUrlActivity : Activity() {
|
||||
|
||||
private val tag = javaClass.simpleName
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val pathSegments = intent?.data?.pathSegments
|
||||
if (pathSegments != null && pathSegments.size > 1) {
|
||||
val id = pathSegments[1]
|
||||
val mainIntent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", "${TsukiMangas.PREFIX_SEARCH}$id")
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
||||
try {
|
||||
startActivity(mainIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e(tag, e.toString())
|
||||
}
|
||||
} else {
|
||||
Log.e(tag, "could not parse uri from intent $intent")
|
||||
}
|
||||
|
||||
finish()
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
package eu.kanade.tachiyomi.extension.pt.tsukimangas.dto
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class MangaListDto(
|
||||
val data: List<SimpleMangaDto>,
|
||||
val page: Int,
|
||||
val lastPage: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SimpleMangaDto(
|
||||
val id: Int,
|
||||
@SerialName("url") val slug: String,
|
||||
val title: String,
|
||||
val poster: String? = null,
|
||||
val cover: String? = null,
|
||||
) {
|
||||
val imagePath = "/img/imgs/${poster ?: cover ?: "nobackground.jpg"}"
|
||||
val entryPath = "/$id/$slug"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class CompleteMangaDto(
|
||||
val id: Int,
|
||||
@SerialName("url") val slug: String,
|
||||
|
||||
val title: String,
|
||||
val poster: String? = null,
|
||||
val cover: String? = null,
|
||||
val status: String? = null,
|
||||
val synopsis: String? = null,
|
||||
val staff: String? = null,
|
||||
val genres: List<Genre> = emptyList(),
|
||||
val titles: List<Title> = emptyList(),
|
||||
) {
|
||||
val entryPath = "/$id/$slug"
|
||||
|
||||
val imagePath = "/img/imgs/${poster ?: cover ?: "nobackground.jpg"}"
|
||||
|
||||
@Serializable
|
||||
data class Genre(val genre: String)
|
||||
|
||||
@Serializable
|
||||
data class Title(val title: String)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ChapterListDto(val chapters: List<ChapterDto>)
|
||||
|
||||
@Serializable
|
||||
data class ChapterDto(
|
||||
val number: String,
|
||||
val title: String? = null,
|
||||
val created_at: String? = null,
|
||||
private val versions: List<Version>,
|
||||
) {
|
||||
@Serializable
|
||||
data class Version(val id: Int)
|
||||
|
||||
val versionId = versions.first().id
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class PageListDto(val pages: List<PageDto>)
|
||||
|
||||
@Serializable
|
||||
data class PageDto(val url: String)
|
Loading…
Reference in New Issue
Block a user