diff --git a/src/id/nimegami/AndroidManifest.xml b/src/id/nimegami/AndroidManifest.xml
new file mode 100644
index 000000000..dc72cdf93
--- /dev/null
+++ b/src/id/nimegami/AndroidManifest.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/id/nimegami/build.gradle b/src/id/nimegami/build.gradle
new file mode 100644
index 000000000..a81bb2375
--- /dev/null
+++ b/src/id/nimegami/build.gradle
@@ -0,0 +1,19 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.serialization)
+}
+
+ext {
+ extName = 'NimeGami'
+ pkgNameSuffix = 'id.nimegami'
+ extClass = '.NimeGami'
+ extVersionCode = 1
+}
+
+dependencies {
+ implementation("dev.datlag.jsunpacker:jsunpacker:1.0.1")
+ implementation(project(":lib-synchrony"))
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/id/nimegami/res/mipmap-hdpi/ic_launcher.png b/src/id/nimegami/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..591180bcf
Binary files /dev/null and b/src/id/nimegami/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/id/nimegami/res/mipmap-mdpi/ic_launcher.png b/src/id/nimegami/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..80325dbe3
Binary files /dev/null and b/src/id/nimegami/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/id/nimegami/res/mipmap-xhdpi/ic_launcher.png b/src/id/nimegami/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..00bfa0a6e
Binary files /dev/null and b/src/id/nimegami/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/id/nimegami/res/mipmap-xxhdpi/ic_launcher.png b/src/id/nimegami/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..960f27134
Binary files /dev/null and b/src/id/nimegami/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/id/nimegami/res/mipmap-xxxhdpi/ic_launcher.png b/src/id/nimegami/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..019e15738
Binary files /dev/null and b/src/id/nimegami/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/id/nimegami/src/eu/kanade/tachiyomi/animeextension/id/nimegami/NimeGami.kt b/src/id/nimegami/src/eu/kanade/tachiyomi/animeextension/id/nimegami/NimeGami.kt
new file mode 100644
index 000000000..1621e0265
--- /dev/null
+++ b/src/id/nimegami/src/eu/kanade/tachiyomi/animeextension/id/nimegami/NimeGami.kt
@@ -0,0 +1,270 @@
+package eu.kanade.tachiyomi.animeextension.id.nimegami
+
+import android.util.Base64
+import dev.datlag.jsunpacker.JsUnpacker
+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.asObservableSuccess
+import eu.kanade.tachiyomi.util.asJsoup
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.runBlocking
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+import okhttp3.Response
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import rx.Observable
+import uy.kohesive.injekt.injectLazy
+import eu.kanade.tachiyomi.lib.synchrony.Deobfuscator as Synchrony
+
+class NimeGami : ParsedAnimeHttpSource() {
+
+ override val name = "NimeGami"
+
+ override val baseUrl = "https://nimegami.id"
+
+ override val lang = "id"
+
+ override val supportsLatest = true
+
+ private val json: Json by injectLazy()
+
+ // ============================== Popular ===============================
+ override fun popularAnimeRequest(page: Int) = GET(baseUrl)
+
+ override fun popularAnimeSelector() = "div.wrapper-2-a > article > a"
+
+ override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
+ setUrlWithoutDomain(element.attr("href"))
+ thumbnail_url = element.selectFirst("img")!!.attr("data-lazy-src")
+ title = element.selectFirst("div.title-post2")!!.text()
+ }
+
+ override fun popularAnimeNextPageSelector() = null
+
+ // =============================== Latest ===============================
+ override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/page/$page")
+
+ override fun latestUpdatesSelector() = "div.post article"
+
+ override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
+ element.selectFirst("h2 > a")!!.let {
+ setUrlWithoutDomain(it.attr("href"))
+ title = it.text()
+ }
+ thumbnail_url = element.selectFirst("img")!!.attr("srcset").substringBefore(" ")
+ }
+
+ override fun latestUpdatesNextPageSelector() = "ul.pagination > li > a:contains(Next)"
+
+ // =============================== Search ===============================
+ override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable {
+ return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
+ val id = query.removePrefix(PREFIX_SEARCH)
+ client.newCall(GET("$baseUrl/$id"))
+ .asObservableSuccess()
+ .map(::searchAnimeByIdParse)
+ } else {
+ super.fetchSearchAnime(page, query, filters)
+ }
+ }
+
+ private fun searchAnimeByIdParse(response: Response): AnimesPage {
+ val details = animeDetailsParse(response.asJsoup())
+ return AnimesPage(listOf(details), false)
+ }
+
+ // TODO: Add support for search filters
+ override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) =
+ GET("$baseUrl/page/$page/?s=$query&post_type=post")
+
+ override fun searchAnimeSelector() = "div.archive > div > article"
+
+ override fun searchAnimeFromElement(element: Element) = latestUpdatesFromElement(element)
+
+ override fun searchAnimeNextPageSelector() = latestUpdatesNextPageSelector()
+
+ // =========================== Anime Details ============================
+ override fun animeDetailsParse(document: Document) = SAnime.create().apply {
+ setUrlWithoutDomain(document.location())
+ thumbnail_url = document.selectFirst("div.coverthumbnail img")!!.attr("src")
+ val infosDiv = document.selectFirst("div.info2 > table > tbody")!!
+ title = infosDiv.getInfo("Judul:")
+ ?: document.selectFirst("h2[itemprop=name]")!!.text()
+ genre = infosDiv.getInfo("Kategori")
+ artist = infosDiv.getInfo("Studio")
+ status = with(document.selectFirst("h1.title")?.text().orEmpty()) {
+ when {
+ contains("(On-Going)") -> SAnime.ONGOING
+ contains("(End)") || contains("(Movie)") -> SAnime.COMPLETED
+ else -> SAnime.UNKNOWN
+ }
+ }
+
+ description = buildString {
+ document.select("div#Sinopsis p").eachText().forEach {
+ append("$it\n")
+ }
+
+ val nonNeeded = listOf("Judul:", "Kategori", "Studio")
+ infosDiv.select("tr")
+ .eachText()
+ .filterNot(nonNeeded::contains)
+ .forEach { append("\n$it") }
+ }
+ }
+
+ private fun Element.getInfo(info: String) =
+ selectFirst("tr:has(td.tablex:contains($info))")?.text()?.substringAfter(": ")
+
+ // ============================== Episodes ==============================
+ override fun episodeListParse(response: Response) = super.episodeListParse(response).reversed()
+
+ override fun episodeListSelector() = "div.list_eps_stream > li.select-eps"
+
+ override fun episodeFromElement(element: Element) = SEpisode.create().apply {
+ val num = element.attr("id").substringAfterLast("_")
+ episode_number = num.toFloatOrNull() ?: 1F
+ name = "Episode $num"
+ url = element.attr("data")
+ }
+
+ // ============================ Video Links =============================
+ override fun fetchVideoList(episode: SEpisode): Observable> {
+ val qualities = json.decodeFromString>(episode.url.b64Decode())
+ val episodeIndex = episode.episode_number.toInt() - 1
+ var usedBunga = false // to prevent repeating the same request to bunga.nimegami
+ return qualities.flatMap {
+ val quality = it.format
+ it.url.mapNotNull { url ->
+ if (url.contains("bunga.nimegami")) {
+ if (usedBunga) {
+ return@mapNotNull null
+ } else usedBunga = true
+ }
+ runCatching {
+ extractVideos(url, quality, episodeIndex)
+ }.getOrElse { emptyList() }
+ }.flatten()
+ }.let { Observable.just(it) }
+ }
+
+ private fun extractVideos(url: String, quality: String, episodeIndex: Int): List