From 4254b88c4059b297bcba096cc482c7a713141120 Mon Sep 17 00:00:00 2001 From: Claudemirovsky <63046606+Claudemirovsky@users.noreply.github.com> Date: Thu, 25 Jan 2024 11:24:19 -0300 Subject: [PATCH] =?UTF-8?q?New=20source:=20pt/Tsuki=20Mang=C3=A1s=20(#597)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- src/pt/tsukimangas/AndroidManifest.xml | 22 + src/pt/tsukimangas/build.gradle | 8 + .../res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3401 bytes .../res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1706 bytes .../res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3935 bytes .../res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6432 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9164 bytes .../extension/pt/tsukimangas/TsukiMangas.kt | 252 ++++++++++ .../pt/tsukimangas/TsukiMangasFilters.kt | 475 ++++++++++++++++++ .../pt/tsukimangas/TsukiMangasUrlActivity.kt | 41 ++ .../pt/tsukimangas/dto/TsukiMangasDto.kt | 70 +++ 11 files changed, 868 insertions(+) create mode 100644 src/pt/tsukimangas/AndroidManifest.xml create mode 100644 src/pt/tsukimangas/build.gradle create mode 100644 src/pt/tsukimangas/res/mipmap-hdpi/ic_launcher.png create mode 100644 src/pt/tsukimangas/res/mipmap-mdpi/ic_launcher.png create mode 100644 src/pt/tsukimangas/res/mipmap-xhdpi/ic_launcher.png create mode 100644 src/pt/tsukimangas/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 src/pt/tsukimangas/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/TsukiMangas.kt create mode 100644 src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/TsukiMangasFilters.kt create mode 100644 src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/TsukiMangasUrlActivity.kt create mode 100644 src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/dto/TsukiMangasDto.kt diff --git a/src/pt/tsukimangas/AndroidManifest.xml b/src/pt/tsukimangas/AndroidManifest.xml new file mode 100644 index 000000000..825716396 --- /dev/null +++ b/src/pt/tsukimangas/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/src/pt/tsukimangas/build.gradle b/src/pt/tsukimangas/build.gradle new file mode 100644 index 000000000..7e38c9877 --- /dev/null +++ b/src/pt/tsukimangas/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = 'Tsuki Mangás' + extClass = '.TsukiMangas' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/pt/tsukimangas/res/mipmap-hdpi/ic_launcher.png b/src/pt/tsukimangas/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..8be15d7923e57b475c97f29e6b260edde4673177 GIT binary patch literal 3401 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 literal 0 HcmV?d00001 diff --git a/src/pt/tsukimangas/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/tsukimangas/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..652fffad6eb46ca8aeb892dc97504eace7a6e226 GIT binary patch literal 6432 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 literal 0 HcmV?d00001 diff --git a/src/pt/tsukimangas/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/tsukimangas/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..82085d6546b5e3106b72638d960e6d6c39cffcad GIT binary patch literal 9164 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 P9q?FL>%THZ%dr0gznxd- literal 0 HcmV?d00001 diff --git a/src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/TsukiMangas.kt b/src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/TsukiMangas.kt new file mode 100644 index 000000000..ea3e2a2fb --- /dev/null +++ b/src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/TsukiMangas.kt @@ -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() + 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 { + 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() + 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 { + val parsed = response.parseAs() + + 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 { + val data = response.parseAs() + 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 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" + } +} diff --git a/src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/TsukiMangasFilters.kt b/src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/TsukiMangasFilters.kt new file mode 100644 index 000000000..77c5c6c5e --- /dev/null +++ b/src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/TsukiMangasFilters.kt @@ -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>) : + Filter.Group(name, pairs.map { CheckBoxVal(it.first) }) + + private class CheckBoxVal(name: String) : Filter.CheckBox(name, false) + + private inline fun FilterList.parseCheckbox( + options: Array>, + ): Sequence { + 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>, + ) : Filter.Select( + displayName, + vals.map { it.first }.toTypedArray(), + ) { + val selected get() = vals[state].second + } + + private inline fun 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 = emptySequence(), + val tags: Sequence = 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(GENRES), + filters.parseCheckbox(TAGS), + + filters.getSelected(), + filters.getSelected(), + filters.getSelected(), + filters.getSelected(), + ) + } + + 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"), + ) +} diff --git a/src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/TsukiMangasUrlActivity.kt b/src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/TsukiMangasUrlActivity.kt new file mode 100644 index 000000000..1722468b1 --- /dev/null +++ b/src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/TsukiMangasUrlActivity.kt @@ -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// 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) + } +} diff --git a/src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/dto/TsukiMangasDto.kt b/src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/dto/TsukiMangasDto.kt new file mode 100644 index 000000000..7207a0150 --- /dev/null +++ b/src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/dto/TsukiMangasDto.kt @@ -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, + 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 = emptyList(), + val titles: List = 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)