diff --git a/src/pt/sukianimes/AndroidManifest.xml b/src/pt/sukianimes/AndroidManifest.xml new file mode 100644 index 000000000..6a0cda01c --- /dev/null +++ b/src/pt/sukianimes/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + diff --git a/src/pt/sukianimes/build.gradle b/src/pt/sukianimes/build.gradle new file mode 100644 index 000000000..c15a13c50 --- /dev/null +++ b/src/pt/sukianimes/build.gradle @@ -0,0 +1,14 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' + +ext { + extName = 'SukiAnimes' + pkgNameSuffix = 'pt.sukianimes' + extClass = '.SukiAnimes' + extVersionCode = 1 + libVersion = '13' +} + + +apply from: "$rootDir/common.gradle" diff --git a/src/pt/sukianimes/res/mipmap-hdpi/ic_launcher.png b/src/pt/sukianimes/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..726f2db9c Binary files /dev/null and b/src/pt/sukianimes/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/pt/sukianimes/res/mipmap-mdpi/ic_launcher.png b/src/pt/sukianimes/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..8d77007e0 Binary files /dev/null and b/src/pt/sukianimes/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/pt/sukianimes/res/mipmap-xhdpi/ic_launcher.png b/src/pt/sukianimes/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..b5f961251 Binary files /dev/null and b/src/pt/sukianimes/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/pt/sukianimes/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/sukianimes/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..c0caa463d Binary files /dev/null and b/src/pt/sukianimes/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/pt/sukianimes/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/sukianimes/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..27529bd4b Binary files /dev/null and b/src/pt/sukianimes/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/pt/sukianimes/src/eu/kanade/tachiyomi/animeextension/pt/sukianimes/SKFilters.kt b/src/pt/sukianimes/src/eu/kanade/tachiyomi/animeextension/pt/sukianimes/SKFilters.kt new file mode 100644 index 000000000..818374a09 --- /dev/null +++ b/src/pt/sukianimes/src/eu/kanade/tachiyomi/animeextension/pt/sukianimes/SKFilters.kt @@ -0,0 +1,170 @@ +package eu.kanade.tachiyomi.animeextension.pt.sukianimes + +import eu.kanade.tachiyomi.animesource.model.AnimeFilter +import eu.kanade.tachiyomi.animesource.model.AnimeFilterList + +object SKFilters { + + 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("Formato", SKFiltersData.formats) + class StatusFilter : QueryPartFilter("Status do anime", SKFiltersData.status) + class TypeFilter : QueryPartFilter("Tipo de vídeo", SKFiltersData.types) + + class GenresFilter : CheckBoxFilterList( + "Gêneros", + SKFiltersData.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 { + val genres = filters.getFirst().state + .mapNotNull { genre -> + if (genre.state) { + SKFiltersData.genres.find { it.first == genre.name }!!.second + } else { null } + }.toList() + + return FilterSearchParams( + filters.getFirst().state, + filters.asQueryPart(), + genres, + filters.asQueryPart(), + filters.asQueryPart() + ) + } + + private object SKFiltersData { + val every = Pair("Qualquer um", "") + + val types = arrayOf( + every, + Pair("Legendado", "1"), + Pair("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("Artes Marciais", "12"), + Pair("Aventura", "13"), + Pair("Ação", "5"), + Pair("Boys Love", "1125"), + Pair("Carros", "945"), + Pair("Chinês", "1032"), + Pair("Comédia Romântica", "15"), + Pair("Comédia", "14"), + Pair("Corrida", "1690"), + Pair("Culinária", "576"), + Pair("Dementia", "164"), + Pair("Demônios", "35"), + Pair("Drama", "9"), + Pair("Ecchi", "16"), + Pair("Erótico", "1203"), + Pair("Escolar", "812"), + Pair("Espaço", "429"), + Pair("Esporte", "17"), + Pair("Fantasia", "10"), + Pair("Ficção Científica", "18"), + Pair("Game", "156"), + Pair("Girls Love", "1228"), + Pair("Gore", "1708"), + Pair("Harém", "69"), + Pair("Histórico", "88"), + Pair("Horror", "165"), + Pair("Idols", "1702"), + Pair("Insanidade", "891"), + Pair("Isekai", "1138"), + Pair("Jogos", "19"), + Pair("Josei", "1345"), + Pair("Kids", "847"), + Pair("Magia", "20"), + Pair("Maid", "1677"), + Pair("Mecha", "21"), + Pair("Militar", "6"), + Pair("Mistério", "7"), + Pair("Munyuu", "1074"), + Pair("Musical", "22"), + Pair("Novel", "252"), + Pair("Parody", "1197"), + Pair("Paródia", "207"), + Pair("Performing Arts", "1564"), + Pair("Piratas", "172"), + Pair("Polícia", "229"), + Pair("Psicológico", "50"), + Pair("RPG", "94"), + Pair("Romance", "23"), + Pair("Samurai", "439"), + Pair("School", "1065"), + Pair("Sci-Fi", "42"), + Pair("Seinen", "24"), + Pair("Shoujo", "210"), + Pair("Shoujo-ai", "25"), + Pair("Shounen", "11"), + Pair("Shounen-AI", "322"), + Pair("Slice of Life", "26"), + Pair("Sobrenatural", "27"), + Pair("Super Poder", "8"), + Pair("Suspense", "230"), + Pair("Terror", "28"), + Pair("Thriller", "51"), + Pair("Tragedia", "269"), + Pair("Vampiro", "134"), + Pair("Vida Diaria", "253"), + Pair("Vida Escolar", "29"), + Pair("Violência", "440"), + Pair("Yaoi", "612"), + Pair("Yuri", "1497") + ) + } +} diff --git a/src/pt/sukianimes/src/eu/kanade/tachiyomi/animeextension/pt/sukianimes/SKUrlActivity.kt b/src/pt/sukianimes/src/eu/kanade/tachiyomi/animeextension/pt/sukianimes/SKUrlActivity.kt new file mode 100644 index 000000000..5829ab1b9 --- /dev/null +++ b/src/pt/sukianimes/src/eu/kanade/tachiyomi/animeextension/pt/sukianimes/SKUrlActivity.kt @@ -0,0 +1,41 @@ +package eu.kanade.tachiyomi.animeextension.pt.sukianimes + +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://sukianimes.com/anime/ intents + * and redirects them to the main Aniyomi process. + */ +class SKUrlActivity : Activity() { + + private val TAG = "SKUrlActivity" + + 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", "${SukiAnimes.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/sukianimes/src/eu/kanade/tachiyomi/animeextension/pt/sukianimes/SukiAnimes.kt b/src/pt/sukianimes/src/eu/kanade/tachiyomi/animeextension/pt/sukianimes/SukiAnimes.kt new file mode 100644 index 000000000..1a02a793c --- /dev/null +++ b/src/pt/sukianimes/src/eu/kanade/tachiyomi/animeextension/pt/sukianimes/SukiAnimes.kt @@ -0,0 +1,274 @@ +package eu.kanade.tachiyomi.animeextension.pt.sukianimes + +import eu.kanade.tachiyomi.animeextension.pt.sukianimes.dto.AnimeDto +import eu.kanade.tachiyomi.animeextension.pt.sukianimes.dto.SearchResultDto +import eu.kanade.tachiyomi.animeextension.pt.sukianimes.extractors.BloggerExtractor +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 SukiAnimes : ParsedAnimeHttpSource() { + + override val name = "SukiAnimes" + + override val baseUrl = "https://sukianimes.com" + private val API_URL = "$baseUrl/wp-admin/admin-ajax.php" + private val NONCE_URL = "$baseUrl/?js_global=1&ver=6.1.1" + + override val lang = "pt-BR" + + override val supportsLatest = true + + override val client: OkHttpClient = network.client + + private val json = Json { + ignoreUnknownKeys = true + } + + // ============================== Popular =============================== + // This source doesn't have a popular anime page, so we'll grab + // the latest anime additions instead. + override fun popularAnimeSelector() = "section.animeslancamentos div.aniItem > a" + override fun popularAnimeRequest(page: Int) = GET(baseUrl) + override fun popularAnimeNextPageSelector() = null // disable it + override fun popularAnimeFromElement(element: Element): SAnime { + return SAnime.create().apply { + setUrlWithoutDomain(element.attr("href")) + title = element.attr("title") + thumbnail_url = element.selectFirst("img").attr("src") + } + } + + // ============================== Episodes ============================== + override fun episodeListSelector() = "div.ultEpsContainerItem > 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