diff --git a/src/all/akuma/AndroidManifest.xml b/src/all/akuma/AndroidManifest.xml
new file mode 100644
index 000000000..3d6f16b29
--- /dev/null
+++ b/src/all/akuma/AndroidManifest.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/all/akuma/build.gradle b/src/all/akuma/build.gradle
new file mode 100644
index 000000000..17d7c8650
--- /dev/null
+++ b/src/all/akuma/build.gradle
@@ -0,0 +1,12 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+ext {
+ extName = 'Akuma'
+ pkgNameSuffix = 'all.akuma'
+ extClass = '.Akuma'
+ extVersionCode = 1
+ isNsfw = true
+}
+
+apply from: "$rootDir/common.gradle"
\ No newline at end of file
diff --git a/src/all/akuma/res/mipmap-hdpi/ic_launcher.png b/src/all/akuma/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..ea33ed801
Binary files /dev/null and b/src/all/akuma/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/all/akuma/res/mipmap-mdpi/ic_launcher.png b/src/all/akuma/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..04904fe7d
Binary files /dev/null and b/src/all/akuma/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/all/akuma/res/mipmap-xhdpi/ic_launcher.png b/src/all/akuma/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..3ae326ed4
Binary files /dev/null and b/src/all/akuma/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/all/akuma/res/mipmap-xxhdpi/ic_launcher.png b/src/all/akuma/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..76f579687
Binary files /dev/null and b/src/all/akuma/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/all/akuma/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/akuma/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..4fe936b67
Binary files /dev/null and b/src/all/akuma/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/all/akuma/res/web_hi_res_512.png b/src/all/akuma/res/web_hi_res_512.png
new file mode 100644
index 000000000..74af0b3ce
Binary files /dev/null and b/src/all/akuma/res/web_hi_res_512.png differ
diff --git a/src/all/akuma/src/eu/kanade/tachiyomi/extension/all/akuma/Akuma.kt b/src/all/akuma/src/eu/kanade/tachiyomi/extension/all/akuma/Akuma.kt
new file mode 100644
index 000000000..71ab2bf1f
--- /dev/null
+++ b/src/all/akuma/src/eu/kanade/tachiyomi/extension/all/akuma/Akuma.kt
@@ -0,0 +1,221 @@
+package eu.kanade.tachiyomi.extension.all.akuma
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.POST
+import eu.kanade.tachiyomi.network.interceptor.rateLimit
+import eu.kanade.tachiyomi.source.model.FilterList
+import eu.kanade.tachiyomi.source.model.MangasPage
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.model.UpdateStrategy
+import eu.kanade.tachiyomi.source.online.ParsedHttpSource
+import eu.kanade.tachiyomi.util.asJsoup
+import okhttp3.FormBody
+import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
+import okhttp3.Interceptor
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import rx.Observable
+import java.io.IOException
+
+class Akuma : ParsedHttpSource() {
+
+ override val name = "Akuma"
+
+ override val baseUrl = "https://akuma.moe"
+
+ override val lang = "all"
+
+ override val supportsLatest = false
+
+ private var nextHash: String? = null
+
+ private var storedToken: String? = null
+
+ private val ddosGuardIntercept = DDosGuardInterceptor(network.client)
+
+ override val client: OkHttpClient = network.client.newBuilder()
+ .addInterceptor(ddosGuardIntercept)
+ .addInterceptor(::tokenInterceptor)
+ .rateLimit(2)
+ .build()
+
+ override fun headersBuilder() = super.headersBuilder()
+ .add("Referer", "$baseUrl/")
+
+ private fun tokenInterceptor(chain: Interceptor.Chain): Response {
+ val request = chain.request()
+
+ if (request.method == "POST" && request.header("X-CSRF-TOKEN") == null) {
+ val modifiedRequest = request.newBuilder()
+ .addHeader("X-Requested-With", "XMLHttpRequest")
+
+ val token = getToken()
+ val response = chain.proceed(
+ modifiedRequest
+ .addHeader("X-CSRF-TOKEN", token)
+ .build(),
+ )
+
+ if (!response.isSuccessful && response.code == 419) {
+ response.close()
+ storedToken = null // reset the token
+ val newToken = getToken()
+ return chain.proceed(
+ modifiedRequest
+ .addHeader("X-CSRF-TOKEN", newToken)
+ .build(),
+ )
+ }
+
+ return response
+ }
+
+ return chain.proceed(request)
+ }
+
+ private fun getToken(): String {
+ if (storedToken.isNullOrEmpty()) {
+ val request = GET(baseUrl, headers)
+ val response = client.newCall(request).execute()
+
+ val document = response.asJsoup()
+ val token = document.select("head meta[name*=csrf-token]")
+ .attr("content")
+
+ if (token.isEmpty()) {
+ throw IOException("Unable to find CSRF token")
+ }
+
+ storedToken = token
+ }
+
+ return storedToken!!
+ }
+
+ override fun popularMangaRequest(page: Int): Request {
+ val payload = FormBody.Builder()
+ .add("view", "3")
+ .build()
+
+ return if (page == 1) {
+ nextHash = null
+ POST(baseUrl, headers, payload)
+ } else {
+ POST("$baseUrl/?cursor=$nextHash", headers, payload)
+ }
+ }
+
+ override fun popularMangaSelector() = ".post-loop li"
+ override fun popularMangaNextPageSelector() = ".page-item a[rel*=next]"
+
+ override fun popularMangaParse(response: Response): MangasPage {
+ val document = response.asJsoup()
+
+ val mangas = document.select(popularMangaSelector()).map { element ->
+ popularMangaFromElement(element)
+ }
+
+ val nextUrl = document.select(popularMangaNextPageSelector()).first()?.attr("href")
+
+ nextHash = nextUrl?.toHttpUrlOrNull()?.queryParameter("cursor")
+
+ return MangasPage(mangas, !nextHash.isNullOrEmpty())
+ }
+
+ override fun popularMangaFromElement(element: Element): SManga {
+ return SManga.create().apply {
+ setUrlWithoutDomain(element.select("a").attr("href"))
+ title = element.select(".overlay-title").text()
+ thumbnail_url = element.select("img").attr("abs:src")
+ }
+ }
+
+ override fun fetchSearchManga(
+ page: Int,
+ query: String,
+ filters: FilterList,
+ ): Observable {
+ return if (query.startsWith(PREFIX_ID)) {
+ val url = "/g/${query.substringAfter(PREFIX_ID)}"
+ val manga = SManga.create().apply { this.url = url }
+ fetchMangaDetails(manga).map {
+ MangasPage(listOf(it.apply { this.url = url }), false)
+ }
+ } else {
+ super.fetchSearchManga(page, query, filters)
+ }
+ }
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ val request = popularMangaRequest(page)
+
+ val url = request.url.newBuilder()
+ .addQueryParameter("q", query.trim())
+ .build()
+
+ return request.newBuilder()
+ .url(url)
+ .build()
+ }
+
+ override fun searchMangaSelector() = popularMangaSelector()
+ override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
+ override fun searchMangaParse(response: Response) = popularMangaParse(response)
+ override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
+
+ override fun mangaDetailsParse(document: Document) = SManga.create().apply {
+ title = document.select(".entry-title").text()
+ thumbnail_url = document.select(".img-thumbnail").attr("abs:src")
+ author = document.select("li.meta-data > span.artist + span.value").text()
+ genre = document.select(".info-list a").joinToString { it.text() }
+ description = document.select(".pages span.value").text() + " Pages"
+ update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
+ status = SManga.COMPLETED
+ }
+
+ override fun fetchChapterList(manga: SManga): Observable> {
+ return Observable.just(
+ listOf(
+ SChapter.create().apply {
+ url = "${manga.url}/1"
+ name = "Chapter"
+ },
+ ),
+ )
+ }
+
+ override fun pageListParse(document: Document): List {
+ val totalPages = document.select(".nav-select option").last()
+ ?.attr("value")?.toIntOrNull() ?: return emptyList()
+
+ val url = document.location().substringBeforeLast("/")
+
+ val pageList = mutableListOf()
+
+ for (i in 1..totalPages) {
+ pageList.add(Page(i, "$url/$i"))
+ }
+
+ return pageList
+ }
+
+ override fun imageUrlParse(document: Document): String {
+ return document.select(".entry-content img").attr("abs:src")
+ }
+
+ companion object {
+ const val PREFIX_ID = "id:"
+ }
+
+ override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
+ override fun latestUpdatesSelector() = throw UnsupportedOperationException()
+ override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
+ override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
+ override fun chapterFromElement(element: Element) = throw UnsupportedOperationException()
+ override fun chapterListSelector() = throw UnsupportedOperationException()
+}
diff --git a/src/all/akuma/src/eu/kanade/tachiyomi/extension/all/akuma/AkumaUrlActivity.kt b/src/all/akuma/src/eu/kanade/tachiyomi/extension/all/akuma/AkumaUrlActivity.kt
new file mode 100644
index 000000000..e58be72e8
--- /dev/null
+++ b/src/all/akuma/src/eu/kanade/tachiyomi/extension/all/akuma/AkumaUrlActivity.kt
@@ -0,0 +1,34 @@
+package eu.kanade.tachiyomi.extension.all.akuma
+
+import android.app.Activity
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import kotlin.system.exitProcess
+
+class AkumaUrlActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val pathSegments = intent?.data?.pathSegments
+ if (pathSegments != null && pathSegments.size > 1) {
+ val id = pathSegments[1]
+ val mainIntent = Intent().apply {
+ action = "eu.kanade.tachiyomi.SEARCH"
+ putExtra("query", "${Akuma.PREFIX_ID}$id")
+ putExtra("filter", packageName)
+ }
+
+ try {
+ startActivity(mainIntent)
+ } catch (e: ActivityNotFoundException) {
+ Log.e("AkumaUrlActivity", e.toString())
+ }
+ } else {
+ Log.e("AkumaUrlActivity", "could not parse uri from intent $intent")
+ }
+
+ finish()
+ exitProcess(0)
+ }
+}
diff --git a/src/all/akuma/src/eu/kanade/tachiyomi/extension/all/akuma/DDosGuardInterceptor.kt b/src/all/akuma/src/eu/kanade/tachiyomi/extension/all/akuma/DDosGuardInterceptor.kt
new file mode 100644
index 000000000..1731b08ef
--- /dev/null
+++ b/src/all/akuma/src/eu/kanade/tachiyomi/extension/all/akuma/DDosGuardInterceptor.kt
@@ -0,0 +1,67 @@
+package eu.kanade.tachiyomi.extension.all.akuma
+
+import android.webkit.CookieManager
+import eu.kanade.tachiyomi.network.GET
+import okhttp3.Cookie
+import okhttp3.HttpUrl
+import okhttp3.Interceptor
+import okhttp3.OkHttpClient
+import okhttp3.Response
+
+class DDosGuardInterceptor(private val client: OkHttpClient) : Interceptor {
+
+ private val cookieManager by lazy { CookieManager.getInstance() }
+
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val originalRequest = chain.request()
+ val response = chain.proceed(originalRequest)
+
+ // Check if DDos-GUARD is on
+ if (response.code !in ERROR_CODES || response.header("Server") !in SERVER_CHECK) {
+ return response
+ }
+
+ val cookies = cookieManager.getCookie(originalRequest.url.toString())
+ val oldCookie = if (cookies != null && cookies.isNotEmpty()) {
+ cookies.split(";").mapNotNull { Cookie.parse(originalRequest.url, it) }
+ } else {
+ emptyList()
+ }
+ val ddg2Cookie = oldCookie.firstOrNull { it.name == "__ddg2_" }
+ if (!ddg2Cookie?.value.isNullOrEmpty()) {
+ return response
+ }
+
+ response.close()
+
+ val newCookie = getNewCookie(originalRequest.url)
+ ?: return chain.proceed(originalRequest)
+
+ val newCookieHeader = (oldCookie + newCookie)
+ .joinToString("; ") { "${it.name}=${it.value}" }
+
+ val modifiedRequest = originalRequest.newBuilder()
+ .header("cookie", newCookieHeader)
+ .build()
+
+ return chain.proceed(modifiedRequest)
+ }
+
+ private fun getNewCookie(url: HttpUrl): Cookie? {
+ val wellKnown = client.newCall(GET(wellKnownUrl))
+ .execute().body.string()
+ .substringAfter("'", "")
+ .substringBefore("'", "")
+ val checkUrl = "${url.scheme}://${url.host + wellKnown}"
+ val response = client.newCall(GET(checkUrl)).execute()
+ return response.header("set-cookie")?.let {
+ Cookie.parse(url, it)
+ }
+ }
+
+ companion object {
+ private const val wellKnownUrl = "https://check.ddos-guard.net/check.js"
+ private val ERROR_CODES = listOf(403)
+ private val SERVER_CHECK = listOf("ddos-guard")
+ }
+}