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