diff --git a/src/all/netflixmirror/AndroidManifest.xml b/src/all/netflixmirror/AndroidManifest.xml
new file mode 100644
index 000000000..8072ee00d
--- /dev/null
+++ b/src/all/netflixmirror/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/all/netflixmirror/build.gradle b/src/all/netflixmirror/build.gradle
new file mode 100644
index 000000000..21044713a
--- /dev/null
+++ b/src/all/netflixmirror/build.gradle
@@ -0,0 +1,18 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.serialization)
+}
+
+ext {
+ extName = 'NetFlix Mirror'
+ pkgNameSuffix = 'all.netflixmirror'
+ extClass = '.NetFlixMirror'
+ extVersionCode = 1
+}
+
+dependencies {
+ implementation(project(':lib-playlist-utils'))
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/all/netflixmirror/res/mipmap-hdpi/ic_launcher.png b/src/all/netflixmirror/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..68e5ac658
Binary files /dev/null and b/src/all/netflixmirror/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/all/netflixmirror/res/mipmap-mdpi/ic_launcher.png b/src/all/netflixmirror/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..b5e1811ca
Binary files /dev/null and b/src/all/netflixmirror/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/all/netflixmirror/res/mipmap-xhdpi/ic_launcher.png b/src/all/netflixmirror/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..838d63e3a
Binary files /dev/null and b/src/all/netflixmirror/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/all/netflixmirror/res/mipmap-xxhdpi/ic_launcher.png b/src/all/netflixmirror/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..f9599bf70
Binary files /dev/null and b/src/all/netflixmirror/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/all/netflixmirror/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/netflixmirror/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..ca41fc42a
Binary files /dev/null and b/src/all/netflixmirror/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/all/netflixmirror/res/web_hi_res_512.png b/src/all/netflixmirror/res/web_hi_res_512.png
new file mode 100644
index 000000000..fd4971093
Binary files /dev/null and b/src/all/netflixmirror/res/web_hi_res_512.png differ
diff --git a/src/all/netflixmirror/src/eu/kanade/tachiyomi/animeextension/all/netflixmirror/CookieInterceptor.kt b/src/all/netflixmirror/src/eu/kanade/tachiyomi/animeextension/all/netflixmirror/CookieInterceptor.kt
new file mode 100644
index 000000000..01fd66e7e
--- /dev/null
+++ b/src/all/netflixmirror/src/eu/kanade/tachiyomi/animeextension/all/netflixmirror/CookieInterceptor.kt
@@ -0,0 +1,45 @@
+package eu.kanade.tachiyomi.animeextension.all.netflixmirror
+
+import android.util.Log
+import android.webkit.CookieManager
+import okhttp3.Interceptor
+import okhttp3.Response
+
+class CookieInterceptor(
+ private val domain: String,
+ private val key: String,
+ private val value: String,
+) : Interceptor {
+
+ init {
+ val url = "https://$domain/"
+ val cookie = "$key=$value; Domain=$domain; Path=/"
+ setCookie(url, cookie)
+ }
+
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val request = chain.request()
+ if (!request.url.host.endsWith(domain)) return chain.proceed(request)
+
+ val cookie = "$key=$value"
+ val cookieList = request.header("Cookie")?.split("; ") ?: emptyList()
+ if (cookie in cookieList) return chain.proceed(request)
+
+ setCookie("https://$domain/", "$cookie; Domain=$domain; Path=/")
+ val prefix = "$key="
+ val newCookie = buildList(cookieList.size + 1) {
+ cookieList.filterNotTo(this) { it.startsWith(prefix) }
+ add(cookie)
+ }.joinToString("; ")
+ val newRequest = request.newBuilder().header("Cookie", newCookie).build()
+ return chain.proceed(newRequest)
+ }
+
+ private fun setCookie(url: String, value: String) {
+ try {
+ CookieManager.getInstance().setCookie(url, value)
+ } catch (e: Exception) {
+ Log.e(domain, "failed to set cookie", e)
+ }
+ }
+}
diff --git a/src/all/netflixmirror/src/eu/kanade/tachiyomi/animeextension/all/netflixmirror/NetFlixMirror.kt b/src/all/netflixmirror/src/eu/kanade/tachiyomi/animeextension/all/netflixmirror/NetFlixMirror.kt
new file mode 100644
index 000000000..fb5ad0e36
--- /dev/null
+++ b/src/all/netflixmirror/src/eu/kanade/tachiyomi/animeextension/all/netflixmirror/NetFlixMirror.kt
@@ -0,0 +1,271 @@
+package eu.kanade.tachiyomi.animeextension.all.netflixmirror
+
+import android.app.Application
+import androidx.preference.ListPreference
+import androidx.preference.PreferenceScreen
+import eu.kanade.tachiyomi.animeextension.all.netflixmirror.dto.DetailsDto
+import eu.kanade.tachiyomi.animeextension.all.netflixmirror.dto.EpisodeUrl
+import eu.kanade.tachiyomi.animeextension.all.netflixmirror.dto.EpisodesDto
+import eu.kanade.tachiyomi.animeextension.all.netflixmirror.dto.SearchDto
+import eu.kanade.tachiyomi.animeextension.all.netflixmirror.dto.SeasonEpisodesDto
+import eu.kanade.tachiyomi.animeextension.all.netflixmirror.dto.VideoList
+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.lib.playlistutils.PlaylistUtils
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.asObservableSuccess
+import eu.kanade.tachiyomi.util.asJsoup
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.select.Elements
+import rx.Observable
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import uy.kohesive.injekt.injectLazy
+import kotlin.math.min
+
+class NetFlixMirror : AnimeHttpSource(), ConfigurableAnimeSource {
+
+ override val name = "NetFlix Mirror"
+
+ override val baseUrl = "https://m.netflixmirror.com"
+
+ override val lang = "all"
+
+ override val supportsLatest = false
+
+ private val json: Json by injectLazy()
+
+ override val client = network.cloudflareClient.newBuilder()
+ .addNetworkInterceptor(
+ CookieInterceptor(baseUrl.toHttpUrl().host, "hd", "on"),
+ )
+ .build()
+
+ override fun headersBuilder() = super.headersBuilder()
+ .add("Referer", "$baseUrl/")
+
+ private val xhrHeaders by lazy {
+ headersBuilder()
+ .add("X-Requested-With", "XMLHttpRequest")
+ .build()
+ }
+
+ private val preferences by lazy {
+ Injekt.get().getSharedPreferences("source_$id", 0x0000)
+ }
+
+ private val playListUtils by lazy {
+ PlaylistUtils(client, headers)
+ }
+
+ private lateinit var pageElements: Elements
+
+ override fun fetchPopularAnime(page: Int): Observable {
+ return if (page == 1) {
+ super.fetchPopularAnime(page)
+ } else {
+ Observable.just(paginatedAnimePageParse(page))
+ }
+ }
+
+ override fun popularAnimeRequest(page: Int): Request {
+ return GET("$baseUrl/home", headers)
+ }
+
+ override fun popularAnimeParse(response: Response): AnimesPage {
+ pageElements = response.asJsoup().select("article > a.post-data")
+
+ return paginatedAnimePageParse(1)
+ }
+
+ private fun paginatedAnimePageParse(page: Int): AnimesPage {
+ val end = min(page * 20, pageElements.size)
+ val entries = pageElements.subList((page - 1) * 20, end).map {
+ SAnime.create().apply {
+ title = "" // no title here
+ url = it.attr("data-post")
+ thumbnail_url = it.selectFirst("img")?.attr("abs:data-src")
+ }
+ }
+
+ return AnimesPage(entries, end < pageElements.size)
+ }
+
+ override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable {
+ return if (query.isNotEmpty()) {
+ super.fetchSearchAnime(page, query, filters)
+ } else {
+ if (page == 1) {
+ val pageFilter = filters.filterIsInstance().firstOrNull()?.selected ?: "/home"
+ val request = GET(baseUrl + pageFilter, headers)
+
+ client.newCall(request)
+ .asObservableSuccess()
+ .map(::popularAnimeParse)
+ } else {
+ Observable.just(paginatedAnimePageParse(page))
+ }
+ }
+ }
+
+ override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
+ val url = "$baseUrl/search.php".toHttpUrl().newBuilder().apply {
+ addQueryParameter("s", query.trim())
+ addQueryParameter("t", System.currentTimeMillis().toString())
+ }.build().toString()
+
+ return GET(url, xhrHeaders)
+ }
+
+ override fun getFilterList() = getFilters()
+
+ override fun searchAnimeParse(response: Response): AnimesPage {
+ val result = response.parseAs()
+
+ val entries = result.searchResult?.map {
+ SAnime.create().apply {
+ url = it.id
+ title = it.title
+ thumbnail_url = idToThumbnailUrl(it.id)
+ }
+ } ?: emptyList()
+
+ return AnimesPage(entries, false)
+ }
+
+ override fun animeDetailsRequest(anime: SAnime): Request {
+ val url = "$baseUrl/post.php?id=${anime.url}&t=${System.currentTimeMillis()}"
+
+ return GET(url, xhrHeaders)
+ }
+
+ override fun animeDetailsParse(response: Response): SAnime {
+ val result = response.parseAs()
+ val id = response.request.url.queryParameter("id")!!
+
+ return SAnime.create().apply {
+ title = result.title
+ url = id
+ thumbnail_url = idToThumbnailUrl(id)
+ genre = "${result.genre}, ${result.cast}"
+ author = result.creator
+ artist = result.director
+ description = result.desc
+ if (!result.lang.isNullOrEmpty()) {
+ description += "\n\nAvailable Language(s): ${result.lang.joinToString { it.language }}"
+ }
+ status = result.status
+ }
+ }
+
+ override fun episodeListRequest(anime: SAnime) = animeDetailsRequest(anime)
+
+ override fun episodeListParse(response: Response): List {
+ val result = response.parseAs()
+ val id = response.request.url.queryParameter("id")!!
+
+ if (result.episodes?.firstOrNull() == null) {
+ return SEpisode.create().apply {
+ name = "Movie"
+ url = EpisodeUrl(id, result.title).let(json::encodeToString)
+ }.let(::listOf)
+ }
+
+ val episodes = result.episodes.mapNotNull {
+ if (it == null) return@mapNotNull null
+
+ it.toSEpisode(result.title)
+ }.toMutableList()
+
+ result.season?.reversed()?.drop(1)?.forEach { season ->
+ val seasonRequest = GET("$baseUrl/episodes.php?s=${season.id}&series=$id&t=${System.currentTimeMillis()}", xhrHeaders)
+ val seasonResponse = client.newCall(seasonRequest).execute().parseAs()
+
+ episodes.addAll(
+ index = 0,
+ elements = seasonResponse.episodes?.map {
+ it.toSEpisode(result.title)
+ } ?: emptyList(),
+ )
+ }
+
+ return episodes.reversed()
+ }
+
+ override fun videoListRequest(episode: SEpisode): Request {
+ val episodeUrl = episode.url.parseAs()
+
+ val url = "$baseUrl/playlist.php".toHttpUrl().newBuilder().apply {
+ addQueryParameter("id", episodeUrl.id)
+ addQueryParameter("t", episodeUrl.title)
+ addQueryParameter("tm", System.currentTimeMillis().toString())
+ }.build().toString()
+
+ return GET(url, xhrHeaders)
+ }
+
+ override fun videoListParse(response: Response): List