diff --git a/src/all/sudatchi/AndroidManifest.xml b/src/all/sudatchi/AndroidManifest.xml
new file mode 100644
index 0000000000..dc4be11abe
--- /dev/null
+++ b/src/all/sudatchi/AndroidManifest.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/all/sudatchi/build.gradle b/src/all/sudatchi/build.gradle
new file mode 100644
index 0000000000..cbd13790e6
--- /dev/null
+++ b/src/all/sudatchi/build.gradle
@@ -0,0 +1,11 @@
+ext {
+ extName = 'Sudatchi'
+ extClass = '.Sudatchi'
+ extVersionCode = 1
+}
+
+apply from: "$rootDir/common.gradle"
+
+dependencies {
+ implementation(project(":lib:playlist-utils"))
+}
diff --git a/src/all/sudatchi/res/mipmap-hdpi/ic_launcher.png b/src/all/sudatchi/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..0e72fb45f0
Binary files /dev/null and b/src/all/sudatchi/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/all/sudatchi/res/mipmap-mdpi/ic_launcher.png b/src/all/sudatchi/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..fb9e1024af
Binary files /dev/null and b/src/all/sudatchi/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/all/sudatchi/res/mipmap-xhdpi/ic_launcher.png b/src/all/sudatchi/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..199c55fbef
Binary files /dev/null and b/src/all/sudatchi/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/all/sudatchi/res/mipmap-xxhdpi/ic_launcher.png b/src/all/sudatchi/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..5bed2af8b2
Binary files /dev/null and b/src/all/sudatchi/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/all/sudatchi/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/sudatchi/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..4022b54833
Binary files /dev/null and b/src/all/sudatchi/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/all/sudatchi/src/eu/kanade/tachiyomi/animeextension/all/sudatchi/Sudatchi.kt b/src/all/sudatchi/src/eu/kanade/tachiyomi/animeextension/all/sudatchi/Sudatchi.kt
new file mode 100644
index 0000000000..7e485873ba
--- /dev/null
+++ b/src/all/sudatchi/src/eu/kanade/tachiyomi/animeextension/all/sudatchi/Sudatchi.kt
@@ -0,0 +1,292 @@
+package eu.kanade.tachiyomi.animeextension.all.sudatchi
+
+import android.app.Application
+import android.content.SharedPreferences
+import androidx.preference.ListPreference
+import androidx.preference.PreferenceScreen
+import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.DirectoryDto
+import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.HomeListDto
+import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.LongAnimeDto
+import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.ShortAnimeDto
+import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.SubtitleDto
+import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.WatchDto
+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.Track
+import eu.kanade.tachiyomi.animesource.model.Video
+import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
+import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.awaitSuccess
+import eu.kanade.tachiyomi.util.asJsoup
+import eu.kanade.tachiyomi.util.parseAs
+import kotlinx.serialization.json.Json
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.Request
+import okhttp3.Response
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import uy.kohesive.injekt.injectLazy
+
+class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
+
+ override val name = "Sudatchi"
+
+ override val baseUrl = "https://sudatchi.com"
+
+ override val lang = "all"
+
+ override val supportsLatest = true
+
+ private val codeRegex by lazy { Regex("""\((.*)\)""") }
+
+ private val json: Json by injectLazy()
+
+ private val sudatchiFilters: SudatchiFilters by lazy { SudatchiFilters(baseUrl, client) }
+
+ private val preferences: SharedPreferences by lazy {
+ Injekt.get().getSharedPreferences("source_$id", 0x0000)
+ }
+
+ // ============================== Popular ===============================
+ override fun popularAnimeRequest(page: Int) = GET("$baseUrl/api/home-list", headers)
+
+ private fun Int.parseStatus() = when (this) {
+ 1 -> SAnime.UNKNOWN // Not Yet Released
+ 2 -> SAnime.ONGOING
+ 3 -> SAnime.COMPLETED
+ else -> SAnime.UNKNOWN
+ }
+
+ private fun ShortAnimeDto.toSAnime(titleLang: String) = SAnime.create().apply {
+ url = "/anime/$slug"
+ title = when (titleLang) {
+ "romaji" -> titleRomanji
+ "japanese" -> titleJapanese
+ else -> titleEnglish
+ } ?: arrayOf(titleEnglish, titleRomanji, titleJapanese, "").firstNotNullOf { it }
+ description = synopsis
+ status = statusId.parseStatus()
+ thumbnail_url = "$baseUrl$imgUrl"
+ genre = animeGenres?.joinToString { it.genre.name }
+ }
+
+ override fun popularAnimeParse(response: Response): AnimesPage {
+ sudatchiFilters.fetchFilters()
+ val titleLang = preferences.title
+ return AnimesPage(response.parseAs().animeSpotlight.map { it.toSAnime(titleLang) }, false)
+ }
+
+ // =============================== Latest ===============================
+ override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/api/directory?page=$page&genres=&status=2,3", headers)
+
+ override fun latestUpdatesParse(response: Response): AnimesPage {
+ sudatchiFilters.fetchFilters()
+ val titleLang = preferences.title
+ return response.parseAs().let {
+ AnimesPage(it.animes.map { it.toSAnime(titleLang) }, it.page != it.pages)
+ }
+ }
+
+ // =============================== Search ===============================
+ override fun getFilterList() = sudatchiFilters.getFilterList()
+
+ override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
+ return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
+ val id = query.removePrefix(PREFIX_SEARCH)
+ client.newCall(GET("$baseUrl/api/anime/$id", headers))
+ .awaitSuccess()
+ .use(::searchAnimeByIdParse)
+ } else {
+ super.getSearchAnime(page, query, filters)
+ }
+ }
+
+ private fun searchAnimeByIdParse(response: Response) = AnimesPage(listOf(animeDetailsParse(response)), false)
+
+ override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
+ val url = "$baseUrl/api/directory".toHttpUrl().newBuilder()
+ url.addQueryParameter("page", page.toString())
+ url.addQueryParameter("title", query)
+ filters.filterIsInstance().forEach {
+ val (name, value) = it.toQueryParameter()
+ if (value != null) url.addQueryParameter(name, value)
+ }
+ return GET(url.build(), headers)
+ }
+
+ override fun searchAnimeParse(response: Response) = latestUpdatesParse(response)
+
+ // =========================== Anime Details ============================
+ override fun getAnimeUrl(anime: SAnime) = "$baseUrl${anime.url}"
+
+ override fun animeDetailsRequest(anime: SAnime) = GET("$baseUrl/api${anime.url}", headers)
+
+ override fun animeDetailsParse(response: Response) = response.parseAs().toSAnime(preferences.title)
+
+ // ============================== Episodes ==============================
+ override fun episodeListRequest(anime: SAnime) = animeDetailsRequest(anime)
+
+ override fun episodeListParse(response: Response): List {
+ val anime = response.parseAs()
+ return anime.episodes.map {
+ SEpisode.create().apply {
+ name = it.title
+ episode_number = it.number.toFloat()
+ url = "/watch/${anime.slug}/${it.number}"
+ }
+ }.reversed()
+ }
+
+ // ============================ Video Links =============================
+ override fun videoListRequest(episode: SEpisode) = GET("$baseUrl${episode.url}", headers)
+
+ private val playlistUtils: PlaylistUtils by lazy { PlaylistUtils(client, headers) }
+
+ override fun videoListParse(response: Response): List