diff --git a/src/en/uniquestream/AndroidManifest.xml b/src/en/uniquestream/AndroidManifest.xml
new file mode 100644
index 000000000..acb4de356
--- /dev/null
+++ b/src/en/uniquestream/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/src/en/uniquestream/build.gradle b/src/en/uniquestream/build.gradle
new file mode 100644
index 000000000..c9b90b3c7
--- /dev/null
+++ b/src/en/uniquestream/build.gradle
@@ -0,0 +1,13 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlinx-serialization'
+
+ext {
+ extName = 'UniqueStream'
+ pkgNameSuffix = 'en.uniquestream'
+ extClass = '.UniqueStream'
+ extVersionCode = 1
+ libVersion = '13'
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/en/uniquestream/res/mipmap-hdpi/ic_launcher.png b/src/en/uniquestream/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..1d6b6bafa
Binary files /dev/null and b/src/en/uniquestream/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/en/uniquestream/res/mipmap-mdpi/ic_launcher.png b/src/en/uniquestream/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..1b4cc4938
Binary files /dev/null and b/src/en/uniquestream/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/en/uniquestream/res/mipmap-xhdpi/ic_launcher.png b/src/en/uniquestream/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..06ecde079
Binary files /dev/null and b/src/en/uniquestream/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/en/uniquestream/res/mipmap-xxhdpi/ic_launcher.png b/src/en/uniquestream/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..514c366e6
Binary files /dev/null and b/src/en/uniquestream/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/en/uniquestream/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/uniquestream/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..d58a80893
Binary files /dev/null and b/src/en/uniquestream/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/en/uniquestream/res/web_hi_res_512.png b/src/en/uniquestream/res/web_hi_res_512.png
new file mode 100644
index 000000000..73a3a7b0b
Binary files /dev/null and b/src/en/uniquestream/res/web_hi_res_512.png differ
diff --git a/src/en/uniquestream/src/eu/kanade/tachiyomi/animeextension/en/uniquestream/UniqueStream.kt b/src/en/uniquestream/src/eu/kanade/tachiyomi/animeextension/en/uniquestream/UniqueStream.kt
new file mode 100644
index 000000000..c7cede411
--- /dev/null
+++ b/src/en/uniquestream/src/eu/kanade/tachiyomi/animeextension/en/uniquestream/UniqueStream.kt
@@ -0,0 +1,509 @@
+package eu.kanade.tachiyomi.animeextension.en.uniquestream
+
+import android.app.Application
+import android.content.SharedPreferences
+import androidx.preference.ListPreference
+import androidx.preference.PreferenceScreen
+import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
+import eu.kanade.tachiyomi.animesource.model.AnimeFilter
+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.ParsedAnimeHttpSource
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.POST
+import eu.kanade.tachiyomi.network.asObservableSuccess
+import eu.kanade.tachiyomi.util.asJsoup
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+import okhttp3.Headers
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.RequestBody.Companion.toRequestBody
+import okhttp3.Response
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import rx.Observable
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import uy.kohesive.injekt.injectLazy
+
+class UniqueStream : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
+
+ override val name = "UniqueStream"
+
+ override val baseUrl by lazy { preferences.getString("preferred_domain", "https://uniquestreaming.net")!! }
+
+ override val lang = "en"
+
+ override val supportsLatest = false
+
+ override val client: OkHttpClient = network.cloudflareClient
+
+ private val json: Json by injectLazy()
+
+ private val preferences: SharedPreferences by lazy {
+ Injekt.get().getSharedPreferences("source_$id", 0x0000)
+ }
+
+ // ============================== Popular ===============================
+
+ override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/ratings/")
+
+ override fun popularAnimeSelector(): String = "div.content > div.items > article"
+
+ override fun popularAnimeNextPageSelector(): String? = null
+
+ override fun popularAnimeFromElement(element: Element): SAnime {
+ return SAnime.create().apply {
+ setUrlWithoutDomain(element.selectFirst("a").attr("href").toHttpUrl().encodedPath)
+ thumbnail_url = if (element.selectFirst("img").hasAttr("data-wpfc-original-src")) {
+ element.selectFirst("img").attr("data-wpfc-original-src")
+ } else {
+ element.selectFirst("img").attr("src")
+ }
+ title = element.selectFirst("div.data > h3").text()
+ }
+ }
+
+ // =============================== Latest ===============================
+
+ override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not Used")
+
+ override fun latestUpdatesSelector(): String = throw Exception("Not Used")
+
+ override fun latestUpdatesNextPageSelector(): String = throw Exception("Not Used")
+
+ override fun latestUpdatesFromElement(element: Element): SAnime = throw Exception("Not Used")
+
+ // =============================== Search ===============================
+
+ override fun searchAnimeParse(response: Response): AnimesPage = throw Exception("Not used")
+
+ override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("Not used")
+
+ override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable {
+ val (request, isExact) = searchAnimeRequestExact(page, query, filters)
+ return client.newCall(request)
+ .asObservableSuccess()
+ .map { response ->
+ searchAnimeParse(response, isExact)
+ }
+ }
+
+ private fun searchAnimeParse(response: Response, isExact: Boolean): AnimesPage {
+ val document = response.asJsoup()
+
+ if (isExact) {
+ val anime = SAnime.create()
+ anime.title = document.selectFirst("div.data > h1").text()
+ anime.thumbnail_url = if (document.selectFirst("div.poster > img").hasAttr("data-wpfc-original-src")) {
+ document.selectFirst("div.poster > img").attr("data-wpfc-original-src")
+ } else {
+ document.selectFirst("div.poster > img").attr("src")
+ }
+ anime.setUrlWithoutDomain(response.request.url.encodedPath)
+ return AnimesPage(listOf(anime), false)
+ }
+
+ val animes = document.select(searchAnimeSelector()).map { element ->
+ searchAnimeFromElement(element)
+ }
+
+ val hasNextPage = searchAnimeNextPageSelector()?.let { selector ->
+ document.select(selector).first()
+ } != null
+
+ return AnimesPage(animes, hasNextPage)
+ }
+
+ private fun searchAnimeRequestExact(page: Int, query: String, filters: AnimeFilterList): Pair {
+ val cleanQuery = query.replace(" ", "+").lowercase()
+
+ val filterList = if (filters.isEmpty()) getFilterList() else filters
+ val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
+ val recentFilter = filterList.find { it is RecentFilter } as RecentFilter
+ val yearFilter = filterList.find { it is YearFilter } as YearFilter
+ val urlFilter = filterList.find { it is URLFilter } as URLFilter
+
+ return when {
+ query.isNotBlank() -> Pair(GET("$baseUrl/page/$page/?s=$cleanQuery", headers = headers), false)
+ genreFilter.state != 0 -> Pair(GET("$baseUrl/genre/${genreFilter.toUriPart()}/page/$page/", headers = headers), false)
+ recentFilter.state != 0 -> Pair(GET("$baseUrl/${recentFilter.toUriPart()}/page/$page/", headers = headers), false)
+ yearFilter.state != 0 -> Pair(GET("$baseUrl/release/${yearFilter.toUriPart()}/page/$page/", headers = headers), false)
+ urlFilter.state.isNotEmpty() -> Pair(GET(urlFilter.state, headers = headers), true)
+ else -> Pair(popularAnimeRequest(page), false)
+ }
+ }
+
+ override fun searchAnimeSelector(): String = popularAnimeSelector()
+
+ override fun searchAnimeNextPageSelector(): String? = "div.pagination > span.current ~ a"
+
+ override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
+
+ // ============================== FILTERS ===============================
+
+ override fun getFilterList(): AnimeFilterList = AnimeFilterList(
+ AnimeFilter.Header("Text search ignores filters"),
+ GenreFilter(),
+ RecentFilter(),
+ YearFilter(),
+ AnimeFilter.Separator(),
+ AnimeFilter.Header("Get item url from webview"),
+ URLFilter(),
+ )
+
+ private class GenreFilter : UriPartFilter(
+ "Genres",
+ arrayOf(
+ Pair("