diff --git a/src/pt/subanimes/AndroidManifest.xml b/src/pt/subanimes/AndroidManifest.xml new file mode 100644 index 000000000..a5f321261 --- /dev/null +++ b/src/pt/subanimes/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + diff --git a/src/pt/subanimes/build.gradle b/src/pt/subanimes/build.gradle new file mode 100644 index 000000000..e89700e43 --- /dev/null +++ b/src/pt/subanimes/build.gradle @@ -0,0 +1,14 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' + +ext { + extName = 'SubAnimes' + pkgNameSuffix = 'pt.subanimes' + extClass = '.SubAnimes' + extVersionCode = 1 + libVersion = '13' +} + + +apply from: "$rootDir/common.gradle" diff --git a/src/pt/subanimes/res/mipmap-hdpi/ic_launcher.png b/src/pt/subanimes/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..1ff98d629 Binary files /dev/null and b/src/pt/subanimes/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/pt/subanimes/res/mipmap-mdpi/ic_launcher.png b/src/pt/subanimes/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..71f7b1a2e Binary files /dev/null and b/src/pt/subanimes/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/pt/subanimes/res/mipmap-xhdpi/ic_launcher.png b/src/pt/subanimes/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..88745a5ce Binary files /dev/null and b/src/pt/subanimes/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/pt/subanimes/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/subanimes/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..4d12d3187 Binary files /dev/null and b/src/pt/subanimes/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/pt/subanimes/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/subanimes/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..b4341dba4 Binary files /dev/null and b/src/pt/subanimes/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/pt/subanimes/src/eu/kanade/tachiyomi/animeextension/pt/subanimes/SBFilters.kt b/src/pt/subanimes/src/eu/kanade/tachiyomi/animeextension/pt/subanimes/SBFilters.kt new file mode 100644 index 000000000..dc299088b --- /dev/null +++ b/src/pt/subanimes/src/eu/kanade/tachiyomi/animeextension/pt/subanimes/SBFilters.kt @@ -0,0 +1,177 @@ +package eu.kanade.tachiyomi.animeextension.pt.subanimes + +import eu.kanade.tachiyomi.animesource.model.AnimeFilter +import eu.kanade.tachiyomi.animesource.model.AnimeFilterList + +object SBFilters { + + open class QueryPartFilter( + displayName: String, + val vals: Array> + ) : AnimeFilter.Select( + displayName, + vals.map { it.first }.toTypedArray() + ) { + + fun toQueryPart() = vals[state].second + } + + private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state) + open class CheckBoxFilterList(name: String, values: List) : AnimeFilter.Group(name, values) + + private inline fun AnimeFilterList.getFirst(): R { + return this.filterIsInstance().first() + } + + private inline fun AnimeFilterList.asQueryPart(): String { + return this.filterIsInstance().joinToString("") { + (it as QueryPartFilter).toQueryPart() + } + } + + class AdultFilter : AnimeFilter.CheckBox("Exibir animes adultos", true) + + class FormatFilter : QueryPartFilter("Tipo de série", SBFiltersData.formats) + class StatusFilter : QueryPartFilter("Status do anime", SBFiltersData.status) + class TypeFilter : QueryPartFilter("Tipo de áudio", SBFiltersData.types) + + class GenresFilter : CheckBoxFilterList( + "Gêneros", + SBFiltersData.genres.map { CheckBoxVal(it.first, false) } + ) + + // Mimicking the order of filters on the source + val filterList = AnimeFilterList( + TypeFilter(), + StatusFilter(), + AdultFilter(), + FormatFilter(), + GenresFilter() + ) + + data class FilterSearchParams( + val adult: Boolean = true, + val format: String = "", + val genres: List = emptyList(), + val status: String = "", + val type: String = "", + ) + + internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams { + + if (filters.isEmpty()) return FilterSearchParams() + + val genres = filters.getFirst().state + .mapNotNull { genre -> + if (genre.state) { + SBFiltersData.genres.find { it.first == genre.name }!!.second + } else { null } + }.toList() + + return FilterSearchParams( + filters.getFirst().state, + filters.asQueryPart(), + genres, + filters.asQueryPart(), + filters.asQueryPart() + ) + } + + private object SBFiltersData { + val every = Pair("Qualquer um", "") + + val types = arrayOf( + every, + Pair("Japonês/Legendado", "1"), + Pair("Português/Dublado", "2") + ) + + val status = arrayOf( + every, + Pair("Completo", "Completo"), + Pair("Em lançamento", "Lançamento") + ) + + val formats = arrayOf( + every, + Pair("Anime", "Anime"), + Pair("Filme", "Filme") + ) + + val genres = arrayOf( + Pair("Adulto", "334"), + Pair("Animação", "2374"), + Pair("Arte Marcial", "16"), + Pair("Avant Garde", "2846"), + Pair("Avant", "2845"), + Pair("Aventura", "4"), + Pair("Ação", "15"), + Pair("Boys Love", "2435"), + Pair("Card Battles", "1157"), + Pair("Carro", "605"), + Pair("China", "865"), + Pair("Comédia Romântica", "1254"), + Pair("Comédia", "5"), + Pair("Corridas", "1514"), + Pair("Crime", "1962"), + Pair("Culinária", "925"), + Pair("Cultivo", "1133"), + Pair("Demônio", "19"), + Pair("Drama", "36"), + Pair("Ecchi", "49"), + Pair("Escolar", "140"), + Pair("Espacial", "646"), + Pair("Esporte", "106"), + Pair("Família", "1431"), + Pair("Fantasia", "6"), + Pair("Ficção Científica", "99"), + Pair("Ficção Mítica", "1575"), + Pair("Gathering", "2756"), + Pair("Gourmet", "2813"), + Pair("Harém", "189"), + Pair("Histórico", "20"), + Pair("Horror", "256"), + Pair("Insanidade", "387"), + Pair("Isekai", "10"), + Pair("Jogos", "63"), + Pair("Josei", "733"), + Pair("Magia", "82"), + Pair("Maid", "2772"), + Pair("Mecha", "200"), + Pair("Militar", "58"), + Pair("Mistério", "50"), + Pair("Musical", "112"), + Pair("Novel", "951"), + Pair("Paródia", "171"), + Pair("Policial", "249"), + Pair("Psicológico", "66"), + Pair("Pós-Apocalíptico", "470"), + Pair("Reencarnação", "1134"), + Pair("Romance", "7"), + Pair("Samurai", "127"), + Pair("Sci-fi", "203"), + Pair("Seinen", "51"), + Pair("Seven", "1449"), + Pair("Shoujo Ai", "507"), + Pair("Shoujo", "78"), + Pair("Shounen Ai", "1326"), + Pair("Shounen", "17"), + Pair("Slice of Life", "79"), + Pair("Sobrenatural", "8"), + Pair("Studio Deen", "2451"), + Pair("Sunrise", "318"), + Pair("Super Poder", "18"), + Pair("Suspense", "134"), + Pair("Terror", "42"), + Pair("Thriller", "960"), + Pair("Tragédia", "264"), + Pair("Vampiros", "358"), + Pair("Vida Diaria", "1518"), + Pair("Vida Escolar", "67"), + Pair("Violência", "59"), + Pair("Yaoi", "1386"), + Pair("Yuri", "243"), + Pair("Zumbi", "574") + ) + } +} diff --git a/src/pt/subanimes/src/eu/kanade/tachiyomi/animeextension/pt/subanimes/SBUrlActivity.kt b/src/pt/subanimes/src/eu/kanade/tachiyomi/animeextension/pt/subanimes/SBUrlActivity.kt new file mode 100644 index 000000000..4d8287680 --- /dev/null +++ b/src/pt/subanimes/src/eu/kanade/tachiyomi/animeextension/pt/subanimes/SBUrlActivity.kt @@ -0,0 +1,41 @@ +package eu.kanade.tachiyomi.animeextension.pt.subanimes + +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://subanimes.cc/anime/ intents + * and redirects them to the main Aniyomi process. + */ +class SBUrlActivity : Activity() { + + private val TAG = "SBUrlActivity" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val pathSegments = intent?.data?.pathSegments + if (pathSegments != null && pathSegments.size > 1) { + val item = pathSegments[1] + val mainIntent = Intent().apply { + action = "eu.kanade.tachiyomi.ANIMESEARCH" + putExtra("query", "${SubAnimes.PREFIX_SEARCH}$item") + 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/subanimes/src/eu/kanade/tachiyomi/animeextension/pt/subanimes/SubAnimes.kt b/src/pt/subanimes/src/eu/kanade/tachiyomi/animeextension/pt/subanimes/SubAnimes.kt new file mode 100644 index 000000000..9811858e6 --- /dev/null +++ b/src/pt/subanimes/src/eu/kanade/tachiyomi/animeextension/pt/subanimes/SubAnimes.kt @@ -0,0 +1,252 @@ +package eu.kanade.tachiyomi.animeextension.pt.subanimes + +import eu.kanade.tachiyomi.animeextension.pt.subanimes.dto.AnimeDataDto +import eu.kanade.tachiyomi.animeextension.pt.subanimes.dto.SearchResultDto +import eu.kanade.tachiyomi.animeextension.pt.subanimes.extractors.SubAnimesExtractor +import eu.kanade.tachiyomi.animesource.model.AnimeFilterList +import eu.kanade.tachiyomi.animesource.model.AnimesPage +import eu.kanade.tachiyomi.animesource.model.SAnime +import eu.kanade.tachiyomi.animesource.model.SEpisode +import eu.kanade.tachiyomi.animesource.model.Video +import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import okhttp3.FormBody +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import rx.Observable +import uy.kohesive.injekt.api.get +import kotlin.Exception + +class SubAnimes : ParsedAnimeHttpSource() { + + override val name = "SubAnimes" + + override val baseUrl = "https://subanimes.cc" + private val API_URL = "$baseUrl/wp-admin/admin-ajax.php" + + override val lang = "pt-BR" + + override val supportsLatest = true + + override val client: OkHttpClient = network.client + + private val json = Json { + ignoreUnknownKeys = true + } + + // ============================== Popular =============================== + override fun popularAnimeSelector() = "div#hype div.aniItem > a" + override fun popularAnimeRequest(page: Int) = GET(baseUrl) + override fun popularAnimeNextPageSelector() = null // disable it + override fun popularAnimeFromElement(element: Element): SAnime = + latestUpdatesFromElement(element) + + // ============================== Episodes ============================== + override fun episodeListSelector() = "div#episodios div.animeVideosItem > a" + private fun episodeListNextPageSelector() = latestUpdatesNextPageSelector() + + override fun fetchEpisodeList(anime: SAnime): Observable> { + return client.newCall(episodeListRequest(anime)) + .asObservableSuccess() + .map { response -> + val realDoc = getRealDoc(response.asJsoup()) + episodeListParse(realDoc).reversed() + } + } + + override fun episodeListParse(response: Response): List { + return episodeListParse(response.asJsoup()) + } + + private fun episodeListParse(doc: Document): List { + val episodeList = mutableListOf() + val eps = doc.select(episodeListSelector()).map(::episodeFromElement) + episodeList.addAll(eps) + val nextPageElement = doc.selectFirst(episodeListNextPageSelector()) + if (nextPageElement != null) { + val nextUrl = nextPageElement.attr("href") + val res = client.newCall(GET(nextUrl)).execute() + episodeList.addAll(episodeListParse(res)) + } + return episodeList + } + + override fun episodeFromElement(element: Element): SEpisode { + return SEpisode.create().apply { + setUrlWithoutDomain(element.attr("href")) + val title = element.attr("title") + name = title + episode_number = runCatching { + title.trim().substringAfterLast(" ").toFloat() + }.getOrDefault(0F) + } + } + + // ============================ Video Links ============================= + override fun videoListParse(response: Response): List