diff --git a/src/en/nollyverse/AndroidManifest.xml b/src/en/nollyverse/AndroidManifest.xml
new file mode 100644
index 0000000000..568741e54f
--- /dev/null
+++ b/src/en/nollyverse/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/src/en/nollyverse/build.gradle b/src/en/nollyverse/build.gradle
new file mode 100644
index 0000000000..2bd94f69c6
--- /dev/null
+++ b/src/en/nollyverse/build.gradle
@@ -0,0 +1,11 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+ext {
+ extName = 'NollyVerse'
+ pkgNameSuffix = 'en.nollyverse'
+ extClass = '.NollyVerse'
+ extVersionCode = 3
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/en/nollyverse/res/mipmap-hdpi/ic_launcher.png b/src/en/nollyverse/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..01b41f68ad
Binary files /dev/null and b/src/en/nollyverse/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/en/nollyverse/res/mipmap-mdpi/ic_launcher.png b/src/en/nollyverse/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..505832322c
Binary files /dev/null and b/src/en/nollyverse/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/en/nollyverse/res/mipmap-xhdpi/ic_launcher.png b/src/en/nollyverse/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..62f929f72c
Binary files /dev/null and b/src/en/nollyverse/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/en/nollyverse/res/mipmap-xxhdpi/ic_launcher.png b/src/en/nollyverse/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..f5e1c241b0
Binary files /dev/null and b/src/en/nollyverse/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/en/nollyverse/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/nollyverse/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..beeb4e3c62
Binary files /dev/null and b/src/en/nollyverse/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/en/nollyverse/res/web_hi_res_512.png b/src/en/nollyverse/res/web_hi_res_512.png
new file mode 100644
index 0000000000..cd31026f19
Binary files /dev/null and b/src/en/nollyverse/res/web_hi_res_512.png differ
diff --git a/src/en/nollyverse/src/eu/kanade/tachiyomi/animeextension/en/nollyverse/NollyVerse.kt b/src/en/nollyverse/src/eu/kanade/tachiyomi/animeextension/en/nollyverse/NollyVerse.kt
new file mode 100644
index 0000000000..7fea856d69
--- /dev/null
+++ b/src/en/nollyverse/src/eu/kanade/tachiyomi/animeextension/en/nollyverse/NollyVerse.kt
@@ -0,0 +1,552 @@
+package eu.kanade.tachiyomi.animeextension.en.nollyverse
+
+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.Video
+import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.POST
+import eu.kanade.tachiyomi.util.asJsoup
+import okhttp3.FormBody
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.lang.Exception
+
+class NollyVerse : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
+
+ override val name = "NollyVerse"
+
+ override val baseUrl = "https://www.thenollyverse.com"
+
+ override val lang = "en"
+
+ override val supportsLatest = true
+
+ private val preferences: SharedPreferences by lazy {
+ Injekt.get().getSharedPreferences("source_$id", 0x0000)
+ }
+
+ // ============================== Popular ===============================
+
+ override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/category/trending-movies/page/$page/")
+
+ override fun popularAnimeParse(response: Response): AnimesPage {
+ val document = response.asJsoup()
+
+ val animes = document.select(popularAnimeSelector()).map { element ->
+ popularAnimeFromElement(element)
+ }
+
+ val hasNextPage = popularAnimeNextPageSelector()?.let { selector ->
+ if (document.select(selector).text() != ">") {
+ return AnimesPage(animes, false)
+ }
+ document.select(selector).first()
+ } != null
+
+ return AnimesPage(animes, hasNextPage)
+ }
+
+ override fun popularAnimeSelector(): String = "div.col-md-8 div.row div.col-md-6"
+
+ override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
+ title = element.select("div.post-body h3 a").text()
+ thumbnail_url = element.select("a.post-img img").attr("data-src")
+ setUrlWithoutDomain(element.select("a.post-img").attr("href"))
+ }
+
+ override fun popularAnimeNextPageSelector(): String = "div.loadmore ul.pagination.pagination-md li:nth-last-child(2)"
+
+ // =============================== Latest ===============================
+
+ override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/category/new-series/page/$page/")
+
+ override fun latestUpdatesParse(response: Response): AnimesPage {
+ val document = response.asJsoup()
+
+ val animes = document.select(latestUpdatesSelector()).map { element ->
+ latestUpdatesFromElement(element)
+ }
+
+ val hasNextPage = latestUpdatesNextPageSelector()?.let { selector ->
+ if (document.select(selector).text() != ">") {
+ return AnimesPage(animes, false)
+ }
+ document.select(selector).first()
+ } != null
+
+ return AnimesPage(animes, hasNextPage)
+ }
+
+ override fun latestUpdatesSelector(): String = "div.section div.container div.row div.post.post-row"
+
+ override fun latestUpdatesFromElement(element: Element): SAnime = SAnime.create().apply {
+ title = element.select("div.post-body h3 a").text()
+ thumbnail_url = element.select("a.post-img img").attr("data-src").ifEmpty {
+ element.select("a.post-img img").attr("src")
+ }
+ setUrlWithoutDomain(element.select("a.post-img").attr("href"))
+ }
+
+ override fun latestUpdatesNextPageSelector(): String = "div.loadmore ul.pagination.pagination-md li:nth-last-child(2)"
+
+ // =============================== Search ===============================
+
+ override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
+ return if (query.isNotBlank()) {
+ val body = FormBody.Builder()
+ .add("name", "$query")
+ .build()
+ POST("$baseUrl/livesearch.php", body = body)
+ } else {
+ var searchPath = ""
+ filters.filter { it.state != 0 }.forEach { filter ->
+ when (filter) {
+ is CategoryFilter -> searchPath = if (filter.toUriPart() == "/series/") filter.toUriPart() else "${filter.toUriPart()}page/$page"
+ is MovieGenreFilter -> searchPath = "/movies/genre/${filter.toUriPart()}/page/$page"
+ is SeriesGenreFilter -> searchPath = "/series/genre/${filter.toUriPart()}/page/$page"
+ else -> ""
+ }
+ }
+
+ require(searchPath.isNotEmpty()) { "Search must not be empty" }
+
+ GET(baseUrl + searchPath)
+ }
+ }
+
+ override fun searchAnimeParse(response: Response): AnimesPage {
+ val document = response.asJsoup()
+ val path = response.request.url.encodedPath
+
+ var hasNextPage: Boolean
+ var animes: List
+
+ when {
+ path.startsWith("/livesearch") -> {
+ hasNextPage = false
+ animes = document.select(searchAnimeSelector()).map { element ->
+ searchAnimeFromElement(element)
+ }
+ }
+
+ path.startsWith("/movies/genre/") -> {
+ animes = document.select(movieGenreSelector()).map { element ->
+ movieGenreFromElement(element)
+ }
+ hasNextPage = nextPageSelector()?.let { selector ->
+ if (document.select(selector).text() != ">") {
+ return AnimesPage(animes, false)
+ }
+ document.select(selector).first()
+ } != null
+ }
+
+ path.startsWith("/series/genre/") ||
+ path.startsWith("/category/popular-movies/") ||
+ path.startsWith("/category/trending-movies/") -> {
+ animes = document.select(seriesGenreSelector()).map { element ->
+ seriesGenreFromElement(element)
+ }
+ hasNextPage = nextPageSelector()?.let { selector ->
+ if (document.select(selector).text() != ">") {
+ return AnimesPage(animes, false)
+ }
+ document.select(selector).first()
+ } != null
+ }
+
+ path.startsWith("/category/korean-movies/") || path.startsWith("/category/korean-series/") -> {
+ animes = document.select(koreanSelector()).map { element ->
+ koreanFromElement(element)
+ }
+ hasNextPage = nextPageSelector()?.let { selector ->
+ if (document.select(selector).text() != ">") {
+ return AnimesPage(animes, false)
+ }
+ document.select(selector).first()
+ } != null
+ }
+
+ path.startsWith("/category/latest-movies/") ||
+ path.startsWith("/category/new-series/") ||
+ path.startsWith("/category/latest-uploads/") -> {
+ animes = document.select(latestSelector()).map { element ->
+ latestFromElement(element)
+ }
+ hasNextPage = nextPageSelector()?.let { selector ->
+ if (document.select(selector).text() != ">") {
+ return AnimesPage(animes, false)
+ }
+ document.select(selector).first()
+ } != null
+ }
+
+ path.startsWith("/series/") -> {
+ animes = document.select(seriesSelector()).map { element ->
+ seriesFromElement(element)
+ }
+ hasNextPage = false
+ }
+
+ else -> {
+ animes = document.select(movieSelector()).map { element ->
+ movieFromElement(element)
+ }
+ hasNextPage = nextPageSelector()?.let { selector ->
+ if (document.select(selector).text() != ">") {
+ return AnimesPage(animes, false)
+ }
+ document.select(selector).first()
+ } != null
+ }
+ }
+
+ return AnimesPage(animes, hasNextPage)
+ }
+
+ private fun movieGenreSelector(): String = "div.container > div.row > div.col-md-4"
+
+ private fun movieGenreFromElement(element: Element): SAnime = latestUpdatesFromElement(element)
+
+ private fun seriesGenreSelector(): String = "div.row div.col-md-8 div.col-md-6"
+
+ private fun seriesGenreFromElement(element: Element): SAnime = latestUpdatesFromElement(element)
+
+ private fun koreanSelector(): String = "div.col-md-8 div.row div.col-md-6"
+
+ private fun koreanFromElement(element: Element): SAnime = latestUpdatesFromElement(element)
+
+ private fun latestSelector(): String = latestUpdatesSelector()
+
+ private fun latestFromElement(element: Element): SAnime = latestUpdatesFromElement(element)
+
+ private fun seriesSelector(): String = "div.section-row ul.list-style li"
+
+ private fun seriesFromElement(element: Element): SAnime = SAnime.create().apply {
+ title = element.select("a").text()
+ thumbnail_url = toImgUrl(element.select("a").attr("href"))
+ setUrlWithoutDomain(element.select("a").attr("href"))
+ }
+
+ private fun movieSelector(): String = "div.container div.row div.col-md-12 div.col-md-4"
+
+ private fun movieFromElement(element: Element): SAnime = SAnime.create().apply {
+ title = element.select("h3 a").text()
+ thumbnail_url = element.select("a img").attr("src")
+ setUrlWithoutDomain(element.select("a.post-img").attr("href"))
+ }
+
+ override fun searchAnimeSelector(): String = "a"
+
+ override fun searchAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
+ title = element.text()
+ thumbnail_url = toImgUrl(element.attr("href"))
+ setUrlWithoutDomain(element.attr("href"))
+ }
+
+ private fun nextPageSelector(): String = "ul.pagination.pagination-md li:nth-last-child(2)"
+
+ override fun searchAnimeNextPageSelector(): String = throw Exception("Not used")
+
+ // =========================== Anime Details ============================
+
+ override fun animeDetailsParse(document: Document): SAnime = SAnime.create().apply {
+ title = document.select("div.page-header div.container div.row div.text-center h1").text()
+ description = document.select("blockquote.blockquote small").text()
+ genre = document.select("div.col-md-8 ul.list-style li").firstOrNull {
+ it.text().startsWith("Genre: ")
+ }?.text()?.substringAfter("Genre: ")?.replace(",", ", ")
+ }
+
+ // ============================== Episodes ==============================
+
+ override fun episodeListRequest(anime: SAnime): Request {
+ return if (anime.url.startsWith("/movie/")) {
+ GET(baseUrl + anime.url + "/download/", headers)
+ } else {
+ GET(baseUrl + anime.url + "/seasons/", headers)
+ }
+ }
+
+ override fun episodeListParse(response: Response): List {
+ val path = response.request.url.encodedPath
+
+ val document = response.asJsoup()
+ val episodeList = mutableListOf()
+
+ if (path.startsWith("/movie/")) {
+ episodeList.add(
+ SEpisode.create().apply {
+ name = "Movie"
+ episode_number = 1F
+ setUrlWithoutDomain(path)
+ },
+ )
+ } else {
+ var counter = 1
+ for (season in document.select("table.table.table-striped tbody tr").reversed()) {
+ val seasonUrl = season.select("td a[href]").attr("href")
+ val seasonSoup = client.newCall(
+ GET(seasonUrl, headers),
+ ).execute().asJsoup()
+
+ val episodeTable = seasonSoup.select("table.table.table-striped")
+ val seasonNumber = episodeTable.select("thead th").eachText().find {
+ t ->
+ """Season (\d+)""".toRegex().matches(t)
+ }?.split(" ")!![1]
+
+ for (ep in episodeTable.select("tbody tr")) {
+ episodeList.add(
+ SEpisode.create().apply {
+ name = "Episode S${seasonNumber}E${ep.selectFirst("td")!!.text().split(" ")!![1]}"
+ episode_number = counter.toFloat()
+ setUrlWithoutDomain(seasonUrl + "#$counter")
+ },
+ )
+ counter++
+ }
+
+ // Stop abuse
+ Thread.sleep(500)
+ }
+ }
+
+ return episodeList.reversed()
+ }
+
+ override fun episodeFromElement(element: Element): SEpisode {
+ val seasonNum = element.ownerDocument()!!.select("div.Title span").text()
+
+ return SEpisode.create().apply {
+ name = "Season $seasonNum" + "x" + element.select("td span.Num").text() + " : " + element.select("td.MvTbTtl > a").text()
+ episode_number = element.select("td > span.Num").text().toFloat()
+ setUrlWithoutDomain(element.select("td.MvTbPly > a.ClA").attr("abs:href"))
+ }
+ }
+
+ override fun episodeListSelector() = throw Exception("not used")
+
+ // ============================ Video Links =============================
+
+ override fun videoListRequest(episode: SEpisode): Request {
+ return if (episode.name == "Movie") {
+ GET(baseUrl + episode.url + "#movie", headers)
+ } else {
+ val episodeIndex = """Episode S(\d+)E(?\d+)""".toRegex().matchEntire(
+ episode.name,
+ )!!.groups["num"]!!.value
+ GET(baseUrl + episode.url.replaceAfterLast("#", "") + episodeIndex, headers)
+ }
+ }
+
+ override fun videoListParse(response: Response): List