diff --git a/src/pt/goyabu/AndroidManifest.xml b/src/pt/goyabu/AndroidManifest.xml new file mode 100644 index 000000000..94339ee7a --- /dev/null +++ b/src/pt/goyabu/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/src/pt/goyabu/build.gradle b/src/pt/goyabu/build.gradle new file mode 100644 index 000000000..1263411a7 --- /dev/null +++ b/src/pt/goyabu/build.gradle @@ -0,0 +1,14 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' + +ext { + extName = 'Goyabu' + pkgNameSuffix = 'pt.goyabu' + extClass = '.Goyabu' + extVersionCode = 1 + libVersion = '12' +} + + +apply from: "$rootDir/common.gradle" diff --git a/src/pt/goyabu/res/mipmap-hdpi/ic_launcher.png b/src/pt/goyabu/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..b2b90b7a7 Binary files /dev/null and b/src/pt/goyabu/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/pt/goyabu/res/mipmap-mdpi/ic_launcher.png b/src/pt/goyabu/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..5adebc468 Binary files /dev/null and b/src/pt/goyabu/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/pt/goyabu/res/mipmap-xhdpi/ic_launcher.png b/src/pt/goyabu/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..8c8548ae0 Binary files /dev/null and b/src/pt/goyabu/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/pt/goyabu/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/goyabu/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..d60fc7a76 Binary files /dev/null and b/src/pt/goyabu/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/pt/goyabu/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/goyabu/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..186a5f1e6 Binary files /dev/null and b/src/pt/goyabu/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/pt/goyabu/src/eu/kanade/tachiyomi/animeextension/pt/goyabu/GYConstants.kt b/src/pt/goyabu/src/eu/kanade/tachiyomi/animeextension/pt/goyabu/GYConstants.kt new file mode 100644 index 000000000..b669615ed --- /dev/null +++ b/src/pt/goyabu/src/eu/kanade/tachiyomi/animeextension/pt/goyabu/GYConstants.kt @@ -0,0 +1,11 @@ +package eu.kanade.tachiyomi.animeextension.pt.goyabu + +object GYConstants { + const val ACCEPT_LANGUAGE = "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7" + const val USER_AGENT = "Mozilla/5.0 (iPad; CPU OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.4 Mobile/15E148 Safari/604.1" + const val PREFERRED_QUALITY = "preferred_quality" + const val PREFERRED_PLAYER = "preferred_player" + val QUALITY_LIST = arrayOf("SD", "HD") + val PLAYER_NAMES = arrayOf("Player 1", "Player 2") + val PLAYER_REGEX = Regex("""label: "(\w+)",.*file: "(.*?)"""") +} diff --git a/src/pt/goyabu/src/eu/kanade/tachiyomi/animeextension/pt/goyabu/GYFilters.kt b/src/pt/goyabu/src/eu/kanade/tachiyomi/animeextension/pt/goyabu/GYFilters.kt new file mode 100644 index 000000000..809ef7687 --- /dev/null +++ b/src/pt/goyabu/src/eu/kanade/tachiyomi/animeextension/pt/goyabu/GYFilters.kt @@ -0,0 +1,230 @@ +package eu.kanade.tachiyomi.animeextension.pt.goyabu + +import eu.kanade.tachiyomi.animesource.model.AnimeFilter +import eu.kanade.tachiyomi.animesource.model.AnimeFilterList + +object GYFilters { + + open class QueryPartFilter( + displayName: String, + val vals: Array> + ) : AnimeFilter.Select( + displayName, + vals.map { it.first }.toTypedArray() + ) { + fun toQueryPart() = vals[state].second + } + + open class TriStateFilterList(name: String, values: List) : AnimeFilter.Group(name, values) + private class TriStateVal(name: String) : AnimeFilter.TriState(name) + + private inline fun AnimeFilterList.getFirst(): R { + return this.filterIsInstance().first() + } + + private inline fun AnimeFilterList.asQueryPart(): String { + return this.getFirst().let { + (it as QueryPartFilter).toQueryPart() + } + } + + class LanguageFilter : QueryPartFilter("Idioma", GYFiltersData.languages) + class InitialLetterFilter : QueryPartFilter("Primeira letra", GYFiltersData.initialLetter) + + class EpisodeFilter : AnimeFilter.Text("Episódios") + class EpisodeFilterMode : QueryPartFilter("Modo de filtro", GYFiltersData.episodeFilterMode) + class SortFilter : AnimeFilter.Sort( + "Ordenar", + GYFiltersData.orders.map { it.first }.toTypedArray(), + Selection(0, true) + ) + + class GenresFilter : TriStateFilterList( + "Gêneros", + GYFiltersData.genres.map { TriStateVal(it) } + ) + + val filterList = AnimeFilterList( + LanguageFilter(), + InitialLetterFilter(), + SortFilter(), + AnimeFilter.Separator(), + EpisodeFilter(), + EpisodeFilterMode(), + AnimeFilter.Separator(), + GenresFilter(), + ) + + data class FilterSearchParams( + val language: String = "", + val initialLetter: String = "", + val episodesFilterMode: String = ">=", + var numEpisodes: Int = 0, + var orderAscending: Boolean = true, + var sortBy: String = "", + val blackListedGenres: ArrayList = ArrayList(), + val includedGenres: ArrayList = ArrayList(), + var animeName: String = "" + ) + + internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams { + val searchParams = FilterSearchParams( + filters.asQueryPart(), + filters.asQueryPart(), + filters.asQueryPart(), + ) + + searchParams.numEpisodes = try { + filters.getFirst().state.toInt() + } catch (e: NumberFormatException) { 0 } + + filters.getFirst().state?.let { + val order = GYFiltersData.orders[it.index].second + searchParams.orderAscending = it.ascending + searchParams.sortBy = order + } + + filters.getFirst() + .state.forEach { genre -> + if (genre.isIncluded()) { + searchParams.includedGenres.add(genre.name) + } else if (genre.isExcluded()) { + searchParams.blackListedGenres.add(genre.name) + } + } + + return searchParams + } + + private fun mustRemove(anime: SearchResultDto, params: FilterSearchParams): Boolean { + val epFilterMode = params.episodesFilterMode + return when { + params.animeName != "" && params.animeName.lowercase() !in anime.title.lowercase() -> true + anime.title == "null" -> true + params.language != "" && params.language !in anime.type -> true + params.initialLetter != "" && !anime.title.startsWith(params.initialLetter) -> true + params.blackListedGenres.size > 0 && params.blackListedGenres.any { + it.lowercase() in anime.genre.lowercase() + } -> true + params.includedGenres.size > 0 && params.includedGenres.any { + it.lowercase() !in anime.genre.lowercase() + } -> true + params.numEpisodes > 0 -> { + when (epFilterMode) { + "==" -> params.numEpisodes != anime.videos + ">=" -> params.numEpisodes >= anime.videos + "<=" -> params.numEpisodes <= anime.videos + else -> false + } + } + else -> false + } + } + + fun MutableList.applyFilterParams(params: FilterSearchParams) { + this.removeAll { anime -> mustRemove(anime, params) } + when (params.sortBy) { + "A-Z" -> { + if (!params.orderAscending) + this.reverse() + } + "num" -> { + if (params.orderAscending) + this.sortBy { it.videos } + else + this.sortByDescending { it.videos } + } + } + } + + private object GYFiltersData { + + val languages = arrayOf( + Pair("Todos", ""), + Pair("Legendado", "Leg"), + Pair("Dublado", "Dub") + ) + + val orders = arrayOf( + Pair("Alfabeticamente", "A-Z"), + Pair("Por número de eps", "num") + ) + + val initialLetter = arrayOf(Pair("Qualquer uma", "")) + ('A'..'Z').map { + Pair(it.toString(), it.toString()) + }.toTypedArray() + + val episodeFilterMode = arrayOf( + Pair("Maior ou igual", ">="), + Pair("Menor ou igual", "<="), + Pair("Igual", "=="), + ) + + val genres = arrayOf( + "Alien", + "Animação Chinesa", + "Anjos", + "Artes Marciais", + "Astronautas", + "Aventura", + "Ação", + "Carros", + "Comédia", + "Crianças", + "Demência", + "Demônios", + "Drama", + "Ecchi", + "Escolar", + "Espacial", + "Espaço", + "Esporte", + "Fantasia", + "Fantasmas", + "Ficção Científica", + "Harém", + "Histórico", + "Horror", + "Idol", + "Infantil", + "Isekai", + "Jogo", + "Josei", + "Magia", + "Mecha", + "Militar", + "Mistério", + "Monstros", + "Magia", + "Música", + "Otaku", + "Paródia", + "Piratas", + "Policial", + "Psicológico", + "RPG", + "Realidade Virtual", + "Romance", + "Samurai", + "Sci-Fi", + "Seinen", + "Shoujo", + "Shoujo Ai", + "Shounen", + "Shounen Ai", + "Slice of life", + "Sobrenatural", + "Super Poder", + "Supernatural", + "Superpotência", + "Suspense", + "Teatro", + "Terror", + "Thriller", + "Vampiro", + "Vida Escolar", + "Yaoi", + "Yuri" + ) + } +} diff --git a/src/pt/goyabu/src/eu/kanade/tachiyomi/animeextension/pt/goyabu/Goyabu.kt b/src/pt/goyabu/src/eu/kanade/tachiyomi/animeextension/pt/goyabu/Goyabu.kt new file mode 100644 index 000000000..156f922f5 --- /dev/null +++ b/src/pt/goyabu/src/eu/kanade/tachiyomi/animeextension/pt/goyabu/Goyabu.kt @@ -0,0 +1,305 @@ +package eu.kanade.tachiyomi.animeextension.pt.goyabu + +import android.app.Application +import android.content.SharedPreferences +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.animeextension.pt.goyabu.GYFilters.applyFilterParams +import eu.kanade.tachiyomi.animeextension.pt.goyabu.extractors.PlayerOneExtractor +import eu.kanade.tachiyomi.animeextension.pt.goyabu.extractors.PlayerTwoExtractor +import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource +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.util.asJsoup +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import okhttp3.Headers +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.Injekt +import uy.kohesive.injekt.api.get +import java.lang.Exception + +class Goyabu : ConfigurableAnimeSource, ParsedAnimeHttpSource() { + + override val name = "Goyabu" + + override val baseUrl = "https://goyabu.com" + + override val lang = "pt-BR" + + override val supportsLatest = true + + override val client: OkHttpClient = network.cloudflareClient + + private val json = Json { + ignoreUnknownKeys = true + } + + private var searchJson: List? = null + + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + override fun headersBuilder(): Headers.Builder = Headers.Builder() + .add("Accept-Language", GYConstants.ACCEPT_LANGUAGE) + .add("Referer", baseUrl) + + // ============================== Popular =============================== + override fun popularAnimeSelector(): String = "div.item > div.anime-episode" + override fun popularAnimeRequest(page: Int): Request = GET(baseUrl) + + override fun popularAnimeFromElement(element: Element): SAnime { + val anime: SAnime = SAnime.create() + anime.setUrlWithoutDomain(element.selectFirst("a").attr("href")) + anime.title = element.selectFirst("h3").text() + anime.thumbnail_url = element.selectFirst("img").attr("src") + return anime + } + + override fun popularAnimeNextPageSelector() = throw Exception("not used") + + override fun popularAnimeParse(response: Response): AnimesPage { + val document = response.asJsoup() + val content = document.select("div.episodes-container").get(2) + val animes = content.select(popularAnimeSelector()).map { element -> + popularAnimeFromElement(element) + } + return AnimesPage(animes, false) + } + + // ============================== Episodes ============================== + override fun episodeListSelector(): String = "div.episodes-container > div.anime-episode" + + private fun getAllEps(response: Response): List { + val epList = mutableListOf() + val url = response.request.url.toString() + val doc = if (url.contains("/videos/")) { + getRealDoc(response.asJsoup()) + } else { response.asJsoup() } + + val epElementList = doc.select(episodeListSelector()) + epList.addAll(epElementList.map { episodeFromElement(it) }) + + val next = doc.selectFirst("div.naco > a.next") + if (next != null) { + val newResponse = client.newCall(GET(next.attr("href"))).execute() + epList.addAll(getAllEps(newResponse)) + } + return epList + } + + override fun episodeListParse(response: Response): List { + return getAllEps(response).reversed() + } + + override fun episodeFromElement(element: Element): SEpisode { + val episode = SEpisode.create() + + episode.setUrlWithoutDomain(element.selectFirst("a").attr("href")) + val epName = element.selectFirst("h3").text().substringAfter("– ") + episode.name = epName + episode.episode_number = try { + epName.substringAfter(" ").substringBefore(" ").toFloat() + } catch (e: NumberFormatException) { 0F } + return episode + } + + // ============================ Video Links ============================= + override fun videoListParse(response: Response): List