diff --git a/src/pt/betteranime/AndroidManifest.xml b/src/pt/betteranime/AndroidManifest.xml new file mode 100644 index 000000000..94339ee7a --- /dev/null +++ b/src/pt/betteranime/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/src/pt/betteranime/build.gradle b/src/pt/betteranime/build.gradle new file mode 100644 index 000000000..d0952c558 --- /dev/null +++ b/src/pt/betteranime/build.gradle @@ -0,0 +1,17 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' + +ext { + extName = 'Better Anime' + pkgNameSuffix = 'pt.betteranime' + extClass = '.BetterAnime' + extVersionCode = 1 + libVersion = '12' +} + +dependencies { + compileOnly 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2' +} + +apply from: "$rootDir/common.gradle" diff --git a/src/pt/betteranime/res/mipmap-hdpi/ic_launcher.png b/src/pt/betteranime/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..4f2f2aecc Binary files /dev/null and b/src/pt/betteranime/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/pt/betteranime/res/mipmap-mdpi/ic_launcher.png b/src/pt/betteranime/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..9160b10a4 Binary files /dev/null and b/src/pt/betteranime/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/pt/betteranime/res/mipmap-xhdpi/ic_launcher.png b/src/pt/betteranime/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..78c2ef2a8 Binary files /dev/null and b/src/pt/betteranime/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/pt/betteranime/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/betteranime/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..dfec36fad Binary files /dev/null and b/src/pt/betteranime/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/pt/betteranime/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/betteranime/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..474fdb69a Binary files /dev/null and b/src/pt/betteranime/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/pt/betteranime/src/eu/kanade/tachiyomi/animeextension/pt/betteranime/BAFilters.kt b/src/pt/betteranime/src/eu/kanade/tachiyomi/animeextension/pt/betteranime/BAFilters.kt new file mode 100644 index 000000000..bb230de26 --- /dev/null +++ b/src/pt/betteranime/src/eu/kanade/tachiyomi/animeextension/pt/betteranime/BAFilters.kt @@ -0,0 +1,130 @@ +package eu.kanade.tachiyomi.animeextension.pt.betteranime + +import eu.kanade.tachiyomi.animesource.model.AnimeFilter +import eu.kanade.tachiyomi.animesource.model.AnimeFilterList + +object BAFilters { + + open class QueryPartFilter( + displayName: String, + val vals: Array> + ) : AnimeFilter.Select( + displayName, + vals.map { it.first }.toTypedArray() + ) { + + fun toQueryPart() = vals[state].second + } + + open class CheckBoxFilterList(name: String, values: List) : AnimeFilter.Group(name, values) + private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state) + + 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 LanguageFilter : QueryPartFilter("Idioma", BAFiltersData.languages) + class YearFilter : QueryPartFilter("Ano", BAFiltersData.years) + + class GenresFilter : CheckBoxFilterList( + "Gêneros", + BAFiltersData.genres.map { CheckBoxVal(it.first, false) } + ) + + val filterList = AnimeFilterList( + LanguageFilter(), + YearFilter(), + GenresFilter() + ) + + data class FilterSearchParams( + val language: String = "", + val year: String = "", + val genres: List = emptyList() + ) + + internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams { + val genres = listOf("") + filters.getFirst().state + .mapNotNull { genre -> + if (genre.state) { + BAFiltersData.genres.find { it.first == genre.name }!!.second + } else { null } + }.toList() + + return FilterSearchParams( + filters.asQueryPart(), + filters.asQueryPart(), + genres + ) + } + + private object BAFiltersData { + val every = Pair("Qualquer um", "") + + val languages = arrayOf( + every, + Pair("Legendado", "legendado"), + Pair("Dublado", "dublado") + ) + + val years = arrayOf(every) + (2022 downTo 1976).map { + Pair(it.toString(), it.toString()) + }.toTypedArray() + + val genres = arrayOf( + Pair("Ação", "acao"), + Pair("Artes Marciais", "artes-marciais"), + Pair("Aventura", "aventura"), + Pair("Comédia", "comedia"), + Pair("Cotidiano", "cotidiano"), + Pair("Demência", "demencia"), + Pair("Demônios", "demonios"), + Pair("Drama", "drama"), + Pair("Ecchi", "ecchi"), + Pair("Escolar", "escolar"), + Pair("Espacial", "espacial"), + Pair("Esportes", "esportes"), + Pair("Fantasia", "fantasia"), + Pair("Ficção Científica", "ficcao-cientifica"), + Pair("Game", "game"), + Pair("Harém", "harem"), + Pair("Histórico", "historico"), + Pair("Horror", "horror"), + Pair("Infantil", "infantil"), + Pair("Josei", "josei"), + Pair("Magia", "magia"), + Pair("Mecha", "mecha"), + Pair("Militar", "militar"), + Pair("Mistério", "misterio"), + Pair("Musical", "musical"), + Pair("Paródia", "parodia"), + Pair("Policial", "policial"), + Pair("Psicológico", "psicologico"), + Pair("Romance", "romance"), + Pair("Samurai", "samurai"), + Pair("Sci-Fi", "sci-fi"), + Pair("Seinen", "seinen"), + Pair("Shoujo-Ai", "shoujo-ai"), + Pair("Shoujo", "shoujo"), + Pair("Shounen-Ai", "shounen-ai"), + Pair("Shounen", "shounen"), + Pair("Slice of Life", "slice-of-life"), + Pair("Sobrenatural", "sobrenatural"), + Pair("Super Poderes", "super-poderes"), + Pair("Suspense", "suspense"), + Pair("Terror", "terror"), + Pair("Thriller", "thriller"), + Pair("Tragédia", "tragedia"), + Pair("Vampiros", "vampiros"), + Pair("Vida Escolar", "vida-escolar"), + Pair("Yaoi", "yaoi"), + Pair("Yuri", "yuri"), + ) + } +} diff --git a/src/pt/betteranime/src/eu/kanade/tachiyomi/animeextension/pt/betteranime/BAUtils.kt b/src/pt/betteranime/src/eu/kanade/tachiyomi/animeextension/pt/betteranime/BAUtils.kt new file mode 100644 index 000000000..bb0d3b200 --- /dev/null +++ b/src/pt/betteranime/src/eu/kanade/tachiyomi/animeextension/pt/betteranime/BAUtils.kt @@ -0,0 +1,12 @@ +package eu.kanade.tachiyomi.animeextension.pt.betteranime + +// Terrible way to reinvent the wheel, i just didnt wanted to use apache commons. +fun String.unescape(): String { + return UNICODE_REGEX.replace(this) { + it.groupValues[1] + .toInt(16) + .toChar() + .toString() + }.replace("\\", "") +} +private val UNICODE_REGEX = "\\\\u(\\d+)".toRegex() diff --git a/src/pt/betteranime/src/eu/kanade/tachiyomi/animeextension/pt/betteranime/BetterAnime.kt b/src/pt/betteranime/src/eu/kanade/tachiyomi/animeextension/pt/betteranime/BetterAnime.kt new file mode 100644 index 000000000..1eb79671d --- /dev/null +++ b/src/pt/betteranime/src/eu/kanade/tachiyomi/animeextension/pt/betteranime/BetterAnime.kt @@ -0,0 +1,319 @@ +package eu.kanade.tachiyomi.animeextension.pt.betteranime + +import android.app.Application +import android.content.SharedPreferences +import android.util.Log +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.animeextension.pt.betteranime.dto.LivewireResponseDto +import eu.kanade.tachiyomi.animeextension.pt.betteranime.dto.PayloadData +import eu.kanade.tachiyomi.animeextension.pt.betteranime.dto.PayloadItem +import eu.kanade.tachiyomi.animeextension.pt.betteranime.extractors.BetterAnimeExtractor +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.network.POST +import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonPrimitive +import okhttp3.Headers +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import org.jsoup.Jsoup +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 BetterAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() { + + override val name = "Better Anime" + + override val baseUrl = "https://betteranime.net" + + override val lang = "pt-BR" + + override val supportsLatest = true + + override val client: OkHttpClient = network.cloudflareClient + + private val json = Json { + ignoreUnknownKeys = true + } + + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + override fun headersBuilder(): Headers.Builder = Headers.Builder() + .add("Referer", baseUrl) + .add("Accept-Language", ACCEPT_LANGUAGE) + + // ============================== Popular =============================== + private fun nextPageSelector(): String = "ul.pagination li.page-item:contains(›)" + override fun popularAnimeNextPageSelector() = throw Exception("not used") + override fun popularAnimeSelector(): String = "div.list-animes article" + + override fun popularAnimeFromElement(element: Element): SAnime { + val anime = SAnime.create() + val img = element.selectFirst("img")!! + val url = element.selectFirst("a")?.attr("href")!! + anime.setUrlWithoutDomain(url) + anime.title = element.selectFirst("h3")?.text()!! + anime.thumbnail_url = "https:" + img.attr("src") + return anime + } + + // The site doesn't have a popular anime tab, so we use the latest anime page instead. + override fun popularAnimeRequest(page: Int): Request = + GET("$baseUrl/ultimosAdicionados?page=$page") + + override fun popularAnimeParse(response: Response): AnimesPage { + val document = response.asJsoup() + val animes = document.select(popularAnimeSelector()).map { element -> + popularAnimeFromElement(element) + } + val hasNextPage = hasNextPage(document) + return AnimesPage(animes, hasNextPage) + } + + // ============================== Episodes ============================== + override fun episodeListSelector(): String = "ul#episodesList > li.list-group-item-action > a" + + override fun episodeListParse(response: Response): List { + val document = response.asJsoup() + val episodes = document.select(episodeListSelector()).map { element -> + episodeFromElement(element) + } + return episodes.reversed() + } + + override fun episodeFromElement(element: Element): SEpisode { + val episode = SEpisode.create() + val episodeName = element.text() + episode.setUrlWithoutDomain(element.attr("href")) + episode.name = episodeName + episode.episode_number = try { + episodeName.substringAfterLast(" ").toFloat() + } catch (e: NumberFormatException) { 0F } + return episode + } + + // ============================ Video Links ============================= + override fun videoListParse(response: Response): List