diff --git a/src/en/aniplay/build.gradle b/src/en/aniplay/build.gradle
new file mode 100644
index 00000000..f8264495
--- /dev/null
+++ b/src/en/aniplay/build.gradle
@@ -0,0 +1,12 @@
+ext {
+ extName = 'AniPlay'
+ extClass = '.AniPlay'
+ extVersionCode = 1
+}
+
+apply from: "$rootDir/common.gradle"
+
+dependencies {
+ implementation(project(":lib-multisrc:anilist"))
+ implementation(project(":lib:playlist-utils"))
+}
diff --git a/src/en/aniplay/ic_launcher-playstore.png b/src/en/aniplay/ic_launcher-playstore.png
new file mode 100644
index 00000000..7e70c67f
Binary files /dev/null and b/src/en/aniplay/ic_launcher-playstore.png differ
diff --git a/src/en/aniplay/res/drawable/ic_launcher_foreground.xml b/src/en/aniplay/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 00000000..d150c0b4
--- /dev/null
+++ b/src/en/aniplay/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/en/aniplay/res/mipmap-anydpi-v26/ic_launcher.xml b/src/en/aniplay/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..7353dbd1
--- /dev/null
+++ b/src/en/aniplay/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/en/aniplay/res/mipmap-anydpi-v26/ic_launcher_round.xml b/src/en/aniplay/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..7353dbd1
--- /dev/null
+++ b/src/en/aniplay/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/en/aniplay/res/mipmap-hdpi/ic_launcher.webp b/src/en/aniplay/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 00000000..940e0da3
Binary files /dev/null and b/src/en/aniplay/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/src/en/aniplay/res/mipmap-hdpi/ic_launcher_round.webp b/src/en/aniplay/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..2387abfc
Binary files /dev/null and b/src/en/aniplay/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/src/en/aniplay/res/mipmap-mdpi/ic_launcher.webp b/src/en/aniplay/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 00000000..87d7ecc9
Binary files /dev/null and b/src/en/aniplay/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/src/en/aniplay/res/mipmap-mdpi/ic_launcher_round.webp b/src/en/aniplay/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..4f02183c
Binary files /dev/null and b/src/en/aniplay/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/src/en/aniplay/res/mipmap-xhdpi/ic_launcher.webp b/src/en/aniplay/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 00000000..16243651
Binary files /dev/null and b/src/en/aniplay/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/src/en/aniplay/res/mipmap-xhdpi/ic_launcher_round.webp b/src/en/aniplay/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..d883f306
Binary files /dev/null and b/src/en/aniplay/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/src/en/aniplay/res/mipmap-xxhdpi/ic_launcher.webp b/src/en/aniplay/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..faf5bdf2
Binary files /dev/null and b/src/en/aniplay/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/src/en/aniplay/res/mipmap-xxhdpi/ic_launcher_round.webp b/src/en/aniplay/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..712e3abf
Binary files /dev/null and b/src/en/aniplay/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/src/en/aniplay/res/mipmap-xxxhdpi/ic_launcher.webp b/src/en/aniplay/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..04d7c07b
Binary files /dev/null and b/src/en/aniplay/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/src/en/aniplay/res/mipmap-xxxhdpi/ic_launcher_round.webp b/src/en/aniplay/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..ad17c1ba
Binary files /dev/null and b/src/en/aniplay/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/src/en/aniplay/res/values/ic_launcher_background.xml b/src/en/aniplay/res/values/ic_launcher_background.xml
new file mode 100644
index 00000000..de1841dc
--- /dev/null
+++ b/src/en/aniplay/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #05010D
+
\ No newline at end of file
diff --git a/src/en/aniplay/res/web_hi_res_512.png b/src/en/aniplay/res/web_hi_res_512.png
new file mode 100644
index 00000000..11b79914
Binary files /dev/null and b/src/en/aniplay/res/web_hi_res_512.png differ
diff --git a/src/en/aniplay/src/eu/kanade/tachiyomi/animeextension/en/aniplay/AniPlay.kt b/src/en/aniplay/src/eu/kanade/tachiyomi/animeextension/en/aniplay/AniPlay.kt
new file mode 100644
index 00000000..d86dfaed
--- /dev/null
+++ b/src/en/aniplay/src/eu/kanade/tachiyomi/animeextension/en/aniplay/AniPlay.kt
@@ -0,0 +1,375 @@
+package eu.kanade.tachiyomi.animeextension.en.aniplay
+
+import android.app.Application
+import android.util.Base64
+import android.widget.Toast
+import androidx.preference.ListPreference
+import androidx.preference.PreferenceScreen
+import androidx.preference.SwitchPreferenceCompat
+import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
+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.lib.playlistutils.PlaylistUtils
+import eu.kanade.tachiyomi.multisrc.anilist.AniListAnimeHttpSource
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.POST
+import eu.kanade.tachiyomi.util.parallelFlatMapBlocking
+import eu.kanade.tachiyomi.util.parseAs
+import kotlinx.serialization.encodeToString
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.Request
+import okhttp3.RequestBody.Companion.toRequestBody
+import okhttp3.Response
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
+ override val name = "AniPlay"
+ override val lang = "en"
+
+ override val baseUrl: String
+ get() = "https://${preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)}"
+
+ private val playlistUtils by lazy { PlaylistUtils(client, headers) }
+
+ private val preferences by lazy {
+ Injekt.get().getSharedPreferences("source_$id", 0x0000)
+ }
+
+ /* ================================= AniList configurations ================================= */
+
+ override fun mapAnimeDetailUrl(animeId: Int): String {
+ return "$baseUrl/anime/info/$animeId"
+ }
+
+ override fun mapAnimeId(animeDetailUrl: String): Int {
+ val httpUrl = animeDetailUrl.toHttpUrl()
+
+ return httpUrl.pathSegments[2].toInt()
+ }
+
+ override fun getPreferredTitleLanguage(): TitleLanguage {
+ val preferredLanguage = preferences.getString(PREF_TITLE_LANGUAGE_KEY, PREF_TITLE_LANGUAGE_DEFAULT)
+
+ return when (preferredLanguage) {
+ "romaji" -> TitleLanguage.ROMAJI
+ "english" -> TitleLanguage.ENGLISH
+ "native" -> TitleLanguage.NATIVE
+ else -> TitleLanguage.ROMAJI
+ }
+ }
+
+ /* ====================================== Episode List ====================================== */
+
+ override fun episodeListRequest(anime: SAnime): Request {
+ val httpUrl = anime.url.toHttpUrl()
+ val animeId = httpUrl.pathSegments[2]
+
+ return GET("$baseUrl/api/anime/episode/$animeId")
+ }
+
+ override fun episodeListParse(response: Response): List {
+ val isMarkFiller = preferences.getBoolean(PREF_MARK_FILLER_EPISODE_KEY, PREF_MARK_FILLER_EPISODE_DEFAULT)
+ val episodeListUrl = response.request.url
+ val animeId = episodeListUrl.pathSegments[3]
+ val providers = response.parseAs>()
+ val episodes = mutableMapOf()
+ val episodeExtras = mutableMapOf>()
+
+ providers.forEach { provider ->
+ provider.episodes.forEach { episode ->
+ if (!episodes.containsKey(episode.number)) {
+ episodes[episode.number] = episode
+ }
+ val existingEpisodeExtras = episodeExtras.getOrElse(episode.number) { emptyList() }
+ val episodeExtra = EpisodeExtra(
+ source = provider.providerId,
+ episodeId = episode.id,
+ hasDub = episode.hasDub,
+ )
+ episodeExtras[episode.number] = existingEpisodeExtras + listOf(episodeExtra)
+ }
+ }
+
+ return episodes.map { episodeMap ->
+ val episode = episodeMap.value
+ val episodeNumber = episode.number
+ val episodeExtra = episodeExtras.getValue(episodeNumber)
+ val episodeExtraString = json.encodeToString(episodeExtra)
+ .let { Base64.encode(it.toByteArray(), Base64.DEFAULT) }
+ .toString(Charsets.UTF_8)
+
+ val url = baseUrl.toHttpUrl().newBuilder()
+ .addPathSegment("anime")
+ .addPathSegment("watch")
+ .addQueryParameter("id", animeId)
+ .addQueryParameter("ep", episodeNumber.toString())
+ .addQueryParameter("extras", episodeExtraString)
+ .build()
+
+ val name = parseEpisodeName(episodeNumber, episode.title)
+ val uploadDate = parseDate(episode.createdAt)
+ val dub = when {
+ episodeExtra.any { it.hasDub } -> ", Dub"
+ else -> ""
+ }
+ val filler = when {
+ episode.isFiller && isMarkFiller -> " • Filler Episode"
+ else -> ""
+ }
+ val scanlator = "Sub$dub$filler"
+
+ SEpisode.create().apply {
+ this.url = url.toString()
+ this.name = name
+ this.date_upload = uploadDate
+ this.episode_number = episodeNumber.toFloat()
+ this.scanlator = scanlator
+ }
+ }.reversed()
+ }
+
+ /* ======================================= Video List ======================================= */
+
+ override suspend fun getVideoList(episode: SEpisode): List