diff --git a/src/en/nollyverse/AndroidManifest.xml b/src/en/nollyverse/AndroidManifest.xml
new file mode 100644
index 000000000..acb4de356
--- /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 000000000..f81958038
--- /dev/null
+++ b/src/en/nollyverse/build.gradle
@@ -0,0 +1,12 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+ext {
+ extName = 'NollyVerse'
+ pkgNameSuffix = 'en.nollyverse'
+ extClass = '.NollyVerse'
+ extVersionCode = 1
+ libVersion = '13'
+}
+
+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 000000000..01b41f68a
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 000000000..505832322
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 000000000..62f929f72
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 000000000..f5e1c241b
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 000000000..beeb4e3c6
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 000000000..cd31026f1
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 000000000..946a30ce5
--- /dev/null
+++ b/src/en/nollyverse/src/eu/kanade/tachiyomi/animeextension/en/nollyverse/NollyVerse.kt
@@ -0,0 +1,579 @@
+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.OkHttpClient
+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.nollyverse.com"
+
+ override val lang = "en"
+
+ override val supportsLatest = true
+
+ override val client: OkHttpClient = network.cloudflareClient
+
+ private val preferences: SharedPreferences by lazy {
+ Injekt.get().getSharedPreferences("source_$id", 0x0000)
+ }
+
+ // Popular Anime
+
+ private fun toImgUrl(inputUrl: String): String {
+ val url = inputUrl.removeSuffix("/").toHttpUrl()
+ val pathSeg = url.encodedPathSegments.toMutableList()
+ pathSeg.add(1, "img")
+ return url.scheme +
+ "://" +
+ url.host +
+ "/" +
+ pathSeg.joinToString(separator = "/") +
+ ".jpg"
+ }
+
+ override fun popularAnimeNextPageSelector(): String = "div.loadmore ul.pagination.pagination-md li:nth-last-child(2)"
+
+ 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 popularAnimeRequest(page: Int): Request {
+ return GET("$baseUrl/category/trending-movies/page/$page/")
+ }
+
+ override fun popularAnimeFromElement(element: Element): SAnime {
+ val anime = SAnime.create()
+ anime.title = element.select("div.post-body h3 a").text()
+ anime.thumbnail_url = element.select("a.post-img img").attr("data-src")
+ anime.setUrlWithoutDomain(element.select("a.post-img").attr("href").toHttpUrl().encodedPath)
+ return anime
+ }
+
+ // 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 episodeListSelector() = throw Exception("not used")
+
+ override fun episodeListParse(response: Response): List {
+ val path = response.request.url.encodedPath
+
+ val document = response.asJsoup()
+ val episodeList = mutableListOf()
+
+ if (path.startsWith("/movie/")) {
+ val episode = SEpisode.create()
+ episode.name = "Movie"
+ episode.episode_number = 1F
+ episode.setUrlWithoutDomain(path)
+ episodeList.add(episode)
+ } 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")) {
+ val episode = SEpisode.create()
+
+ episode.name = "Episode S${seasonNumber}E${ep.selectFirst("td").text().split(" ")!![1]}"
+ episode.episode_number = counter.toFloat()
+ episode.setUrlWithoutDomain(seasonUrl + "#$counter")
+ episodeList.add(episode)
+
+ counter++
+ }
+
+ // Stop API abuse
+ Thread.sleep(500)
+ }
+ }
+
+ return episodeList.reversed()
+ }
+
+ override fun episodeFromElement(element: Element): SEpisode {
+ val episode = SEpisode.create()
+ episode.episode_number = element.select("td > span.Num").text().toFloat()
+ val seasonNum = element.ownerDocument().select("div.Title span").text()
+ episode.name = "Season $seasonNum" + "x" + element.select("td span.Num").text() + " : " + element.select("td.MvTbTtl > a").text()
+ episode.setUrlWithoutDomain(element.select("td.MvTbPly > a.ClA").attr("abs:href"))
+ return episode
+ }
+
+ // Video urls
+ 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