diff --git a/src/pt/animestc/AndroidManifest.xml b/src/pt/animestc/AndroidManifest.xml new file mode 100644 index 000000000..2480650f4 --- /dev/null +++ b/src/pt/animestc/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + diff --git a/src/pt/animestc/build.gradle b/src/pt/animestc/build.gradle new file mode 100644 index 000000000..e24fbabbe --- /dev/null +++ b/src/pt/animestc/build.gradle @@ -0,0 +1,16 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' + +ext { + extName = 'AnimesTC' + pkgNameSuffix = 'pt.animestc' + extClass = '.AnimesTC' + extVersionCode = 1 +} + +dependencies { + compileOnly(libs.bundles.coroutines) +} + +apply from: "$rootDir/common.gradle" diff --git a/src/pt/animestc/res/mipmap-hdpi/ic_launcher.png b/src/pt/animestc/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..6d4567918 Binary files /dev/null and b/src/pt/animestc/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/pt/animestc/res/mipmap-mdpi/ic_launcher.png b/src/pt/animestc/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..384d412d9 Binary files /dev/null and b/src/pt/animestc/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/pt/animestc/res/mipmap-xhdpi/ic_launcher.png b/src/pt/animestc/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..5129c6e56 Binary files /dev/null and b/src/pt/animestc/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/pt/animestc/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/animestc/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..bfbb55209 Binary files /dev/null and b/src/pt/animestc/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/pt/animestc/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/animestc/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..f5229e6a7 Binary files /dev/null and b/src/pt/animestc/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/pt/animestc/src/eu/kanade/tachiyomi/animeextension/pt/animestc/ATCFilters.kt b/src/pt/animestc/src/eu/kanade/tachiyomi/animeextension/pt/animestc/ATCFilters.kt new file mode 100644 index 000000000..2a0f405aa --- /dev/null +++ b/src/pt/animestc/src/eu/kanade/tachiyomi/animeextension/pt/animestc/ATCFilters.kt @@ -0,0 +1,188 @@ +package eu.kanade.tachiyomi.animeextension.pt.animestc + +import eu.kanade.tachiyomi.animeextension.pt.animestc.dto.AnimeDto +import eu.kanade.tachiyomi.animesource.model.AnimeFilter +import eu.kanade.tachiyomi.animesource.model.AnimeFilterList +import eu.kanade.tachiyomi.animesource.model.SAnime + +object ATCFilters { + + 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 InitialLetterFilter : QueryPartFilter("Primeira letra", ATCFiltersData.initialLetter) + class StatusFilter : QueryPartFilter("Status", ATCFiltersData.status) + + class SortFilter : AnimeFilter.Sort( + "Ordenar", + ATCFiltersData.orders.map { it.first }.toTypedArray(), + Selection(0, true) + ) + + class GenresFilter : TriStateFilterList( + "Gêneros", + ATCFiltersData.genres.map { TriStateVal(it) } + ) + + val filterList = AnimeFilterList( + InitialLetterFilter(), + StatusFilter(), + SortFilter(), + + AnimeFilter.Separator(), + GenresFilter(), + ) + + data class FilterSearchParams( + val initialLetter: String = "", + val status: String = "", + var orderAscending: Boolean = true, + var sortBy: String = "", + val blackListedGenres: ArrayList = ArrayList(), + val includedGenres: ArrayList = ArrayList(), + var animeName: String = "" + ) + + internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams { + if (filters.isEmpty()) return FilterSearchParams() + val searchParams = FilterSearchParams( + filters.asQueryPart(), + filters.asQueryPart() + ) + + filters.getFirst().state?.let { + val order = ATCFiltersData.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 compareLower(first: String, second: String): Boolean { + return first.lowercase() in second.lowercase() + } + + private fun mustRemove(anime: AnimeDto, params: FilterSearchParams): Boolean { + return when { + params.animeName != "" && !compareLower(params.animeName, anime.title) -> true + params.initialLetter != "" && !anime.title.lowercase().startsWith(params.initialLetter) -> true + params.blackListedGenres.size > 0 && params.blackListedGenres.any { + compareLower(it, anime.genres) + } -> true + params.includedGenres.size > 0 && params.includedGenres.any { + !compareLower(it, anime.genres) + } -> true + params.status != "" && anime.status != SAnime.UNKNOWN && anime.status != params.status.toInt() -> true + else -> false + } + } + + private inline fun > List.sortedByIf( + condition: Boolean, + crossinline selector: (T) -> R? + ): List { + return if (condition) sortedBy(selector) + else sortedByDescending(selector) + } + + fun List.applyFilterParams(params: FilterSearchParams): List { + return this.filterNot { mustRemove(it, params) }.let { results -> + when (params.sortBy) { + "A-Z" -> results.sortedByIf(params.orderAscending) { it.title.lowercase() } + "year" -> results.sortedByIf(params.orderAscending) { it.year ?: 0 } + else -> results + } + } + } + + private object ATCFiltersData { + + val orders = arrayOf( + Pair("Alfabeticamente", "A-Z"), + Pair("Por ano", "year") + ) + + val status = arrayOf( + Pair("Selecione", ""), + Pair("Completo", SAnime.COMPLETED.toString()), + Pair("Em Lançamento", SAnime.ONGOING.toString()) + ) + + val initialLetter = arrayOf(Pair("Selecione", "")) + ('A'..'Z').map { + Pair(it.toString(), it.toString().lowercase()) + }.toTypedArray() + + val genres = arrayOf( + "Ação", + "Action", + "Adventure", + "Artes Marciais", + "Aventura", + "Carros", + "Comédia", + "Comédia Romântica", + "Demônios", + "Drama", + "Ecchi", + "Escolar", + "Esporte", + "Fantasia", + "Historical", + "Histórico", + "Horror", + "Jogos", + "Kids", + "Live Action", + "Magia", + "Mecha", + "Militar", + "Mistério", + "Psicológico", + "Romance", + "Samurai", + "School Life", + "Sci-Fi", // Yeah + "SciFi", + "Seinen", + "Shoujo", + "Shounen", + "Sobrenatural", + "Super Poder", + "Supernatural", + "Terror", + "Tragédia", + "Vampiro", + "Vida Escolar" + ) + } +} diff --git a/src/pt/animestc/src/eu/kanade/tachiyomi/animeextension/pt/animestc/AnimesTC.kt b/src/pt/animestc/src/eu/kanade/tachiyomi/animeextension/pt/animestc/AnimesTC.kt new file mode 100644 index 000000000..19fbf7d2b --- /dev/null +++ b/src/pt/animestc/src/eu/kanade/tachiyomi/animeextension/pt/animestc/AnimesTC.kt @@ -0,0 +1,308 @@ +package eu.kanade.tachiyomi.animeextension.pt.animestc + +import android.app.Application +import android.content.SharedPreferences +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.animeextension.pt.animestc.ATCFilters.applyFilterParams +import eu.kanade.tachiyomi.animeextension.pt.animestc.dto.AnimeDto +import eu.kanade.tachiyomi.animeextension.pt.animestc.dto.EpisodeDto +import eu.kanade.tachiyomi.animeextension.pt.animestc.dto.ResponseDto +import eu.kanade.tachiyomi.animeextension.pt.animestc.dto.VideoDto +import eu.kanade.tachiyomi.animeextension.pt.animestc.extractors.AnonFilesExtractor +import eu.kanade.tachiyomi.animeextension.pt.animestc.extractors.LinkBypasser +import eu.kanade.tachiyomi.animeextension.pt.animestc.extractors.SendcmExtractor +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.AnimeHttpSource +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.asObservableSuccess +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import okhttp3.CacheControl +import okhttp3.Headers +import okhttp3.Request +import okhttp3.Response +import rx.Observable +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.concurrent.TimeUnit.DAYS + +class AnimesTC : ConfigurableAnimeSource, AnimeHttpSource() { + + override val name = "AnimesTC" + + override val baseUrl = "https://api2.animestc.com" + + override val lang = "pt-BR" + + override val supportsLatest = true + + override fun headersBuilder() = Headers.Builder() + .add("Referer", "https://www.animestc.net/") + + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + private val json = Json { + ignoreUnknownKeys = true + } + + // ============================== Popular =============================== + // This source doesnt have a popular animes page, + // so we use latest animes page instead. + override fun fetchPopularAnime(page: Int) = fetchLatestUpdates(page) + override fun popularAnimeParse(response: Response): AnimesPage = TODO() + override fun popularAnimeRequest(page: Int): Request = TODO() + + // ============================== Episodes ============================== + override fun episodeListParse(response: Response): List { + val id = response.getAnimeDto().id + return getEpisodeList(id) + } + + private fun episodeListRequest(animeId: Int, page: Int) = + GET("$baseUrl/episodes?order=id&direction=desc&page=$page&seriesId=$animeId&specialOrder=true") + + private fun getEpisodeList(animeId: Int, page: Int = 1): List { + val response = client.newCall(episodeListRequest(animeId, page)).execute() + val parsed = response.parseAs>() + val episodes = parsed.items.map { + SEpisode.create().apply { + name = it.title + setUrlWithoutDomain("/episodes?slug=${it.slug}") + episode_number = it.number.toFloat() + date_upload = it.created_at.toDate() + } + } + + if (parsed.page < parsed.lastPage) { + return episodes + getEpisodeList(animeId, page + 1) + } else { + return episodes + } + } + + // ============================ Video Links ============================= + + override fun videoListParse(response: Response): List