diff --git a/src/all/imhentai/AndroidManifest.xml b/src/all/imhentai/AndroidManifest.xml
new file mode 100644
index 000000000..30deb7f79
--- /dev/null
+++ b/src/all/imhentai/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/all/imhentai/build.gradle b/src/all/imhentai/build.gradle
new file mode 100644
index 000000000..14667eff9
--- /dev/null
+++ b/src/all/imhentai/build.gradle
@@ -0,0 +1,13 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+ext {
+ extName = 'IMHentai'
+ pkgNameSuffix = 'all.imhentai'
+ extClass = '.IMHentaiFactory'
+ extVersionCode = 1
+ libVersion = '1.2'
+ containsNsfw = true
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/all/imhentai/res/mipmap-hdpi/ic_launcher.png b/src/all/imhentai/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..a29fb9208
Binary files /dev/null and b/src/all/imhentai/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/all/imhentai/res/mipmap-mdpi/ic_launcher.png b/src/all/imhentai/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..2d3866bd1
Binary files /dev/null and b/src/all/imhentai/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/all/imhentai/res/mipmap-xhdpi/ic_launcher.png b/src/all/imhentai/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..57eac862e
Binary files /dev/null and b/src/all/imhentai/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/all/imhentai/res/mipmap-xxhdpi/ic_launcher.png b/src/all/imhentai/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..66b298b74
Binary files /dev/null and b/src/all/imhentai/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/all/imhentai/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/imhentai/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..6d4d71984
Binary files /dev/null and b/src/all/imhentai/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/all/imhentai/res/web_hi_res_512.png b/src/all/imhentai/res/web_hi_res_512.png
new file mode 100644
index 000000000..cfe8b5960
Binary files /dev/null and b/src/all/imhentai/res/web_hi_res_512.png differ
diff --git a/src/all/imhentai/src/eu/kanade/tachiyomi/extension/all/imhentai/IMHentai.kt b/src/all/imhentai/src/eu/kanade/tachiyomi/extension/all/imhentai/IMHentai.kt
new file mode 100644
index 000000000..259890a98
--- /dev/null
+++ b/src/all/imhentai/src/eu/kanade/tachiyomi/extension/all/imhentai/IMHentai.kt
@@ -0,0 +1,267 @@
+package eu.kanade.tachiyomi.extension.all.imhentai
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.POST
+import eu.kanade.tachiyomi.network.asObservableSuccess
+import eu.kanade.tachiyomi.source.model.Filter
+import eu.kanade.tachiyomi.source.model.FilterList
+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.online.ParsedHttpSource
+import eu.kanade.tachiyomi.util.asJsoup
+import okhttp3.Headers
+import okhttp3.HttpUrl
+import okhttp3.MediaType
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.RequestBody
+import okhttp3.Response
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import org.jsoup.select.Elements
+import rx.Observable
+
+class IMHentai(override val lang: String, private val imhLang: String) : ParsedHttpSource() {
+
+ private val pageLoadHeaders: Headers = Headers.Builder().apply {
+ add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
+ add("X-Requested-With", "XMLHttpRequest")
+ }.build()
+
+ override val baseUrl: String = "https://imhentai.com"
+ override val name: String = "IMHentai"
+ override val supportsLatest = true
+
+ override val client: OkHttpClient = network.cloudflareClient
+
+ // Popular
+
+ override fun popularMangaFromElement(element: Element): SManga {
+ return SManga.create().apply {
+ thumbnail_url = element.select(".inner_thumb img").attr("src")
+ with(element.select(".caption a")) {
+ url = this.attr("href")
+ title = this.text()
+ }
+ }
+ }
+
+ override fun popularMangaNextPageSelector(): String = ".pagination li a:contains(Next):not([tabindex])"
+
+ override fun popularMangaSelector(): String = ".thumbs_container .thumb"
+
+ override fun popularMangaRequest(page: Int): Request = searchMangaRequest(page, "", getFilterList(SORT_ORDER_POPULAR))
+
+ // Latest
+
+ override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
+
+ override fun latestUpdatesNextPageSelector(): String = popularMangaNextPageSelector()
+
+ override fun latestUpdatesRequest(page: Int): Request = searchMangaRequest(page, "", getFilterList(SORT_ORDER_LATEST))
+
+ override fun latestUpdatesSelector(): String = popularMangaSelector()
+
+ // Search
+
+ override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
+
+ override fun searchMangaNextPageSelector(): String = popularMangaNextPageSelector()
+
+ private fun toBinary(boolean: Boolean) = if (boolean) "1" else "0"
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ val url = HttpUrl.parse("$baseUrl/search")!!.newBuilder()
+ .addQueryParameter("key", query)
+ .addQueryParameter("page", page.toString())
+ .addQueryParameter(getLanguageURIByName(imhLang).uri, toBinary(true)) // main language always enabled
+
+ (if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
+ when (filter) {
+ is LanguageFilters -> {
+ filter.state.forEach {
+ url.addQueryParameter(it.uri, toBinary(it.state))
+ }
+ }
+ is CategoryFilters -> {
+ filter.state.forEach {
+ url.addQueryParameter(it.uri, toBinary(it.state))
+ }
+ }
+ is SortOrderFilter -> {
+ getSortOrderURIs().forEachIndexed { index, pair ->
+ url.addQueryParameter(pair.second, toBinary(filter.state == index))
+ }
+ }
+ else -> { }
+ }
+ }
+
+ return GET(url.toString())
+ }
+
+ override fun searchMangaSelector(): String = popularMangaSelector()
+
+ // Details
+
+ private fun Elements.csvText(splitTagSeparator: String = ", "): String {
+ return this.joinToString {
+ listOf(
+ it.ownText(),
+ it.select(".split_tag")?.text()
+ ?.trim()
+ ?.removePrefix("| ")
+ )
+ .filter { s -> !s.isNullOrBlank() }
+ .joinToString(splitTagSeparator)
+ }
+ }
+
+ override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
+ val mangaInfoElement = document.select(".galleries_info")
+ val infoMap = mangaInfoElement.select("li:not(.pages)").map {
+ it.select("span.tags_text").text().removeSuffix(":") to it.select(".tag")
+ }.toMap()
+
+ artist = infoMap["Artists"]?.csvText(" | ")
+
+ author = artist
+
+ genre = infoMap["Tags"]?.csvText()
+
+ status = SManga.COMPLETED
+
+ val pages = mangaInfoElement.select("li.pages").text().substringAfter("Pages: ")
+ val altTitle = document.select(".subtitle").text().ifBlank { null }
+
+ description = listOf(
+ "Parodies",
+ "Characters",
+ "Groups",
+ "Languages",
+ "Category"
+ ).map { it to infoMap[it]?.csvText() }
+ .let { listOf(Pair("Alternate Title", altTitle)) + it + listOf(Pair("Pages", pages)) }
+ .filter { !it.second.isNullOrEmpty() }
+ .joinToString("\n\n") { "${it.first}:\n${it.second}" }
+ }
+
+ // Chapters
+
+ private fun pageLoadMetaParse(document: Document): String {
+ return document.select(".gallery_divider ~ input[type=\"hidden\"]").map { m ->
+ m.attr("id") to m.attr("value")
+ }.toMap().let {
+ listOf(
+ Pair("server", "load_server"),
+ Pair("u_id", "gallery_id"),
+ Pair("g_id", "load_id"),
+ Pair("img_dir", "load_dir"),
+ Pair("total_pages", "load_pages")
+ ).map { meta -> "${meta.first}=${it[meta.second]}" }
+ .let { payload -> payload + listOf("type=2", "visible_pages=0") }
+ .joinToString("&")
+ }
+ }
+
+ override fun chapterListParse(response: Response): List {
+ return listOf(
+ SChapter.create().apply {
+ setUrlWithoutDomain(response.request().url().toString())
+ name = "Chapter"
+ chapter_number = 1f
+ }
+ )
+ }
+
+ override fun chapterFromElement(element: Element): SChapter = throw UnsupportedOperationException("Not used")
+
+ override fun chapterListSelector(): String = throw UnsupportedOperationException("Not used")
+
+ // Pages
+
+ override fun fetchPageList(chapter: SChapter): Observable> {
+ return client.newCall(GET("$baseUrl${chapter.url}"))
+ .asObservableSuccess()
+ .map { pageLoadMetaParse(it.asJsoup()) }
+ .map { RequestBody.create(MediaType.parse("application/x-www-form-urlencoded; charset=UTF-8"), it) }
+ .concatMap { client.newCall(POST(PAEG_LOAD_URL, pageLoadHeaders, it)).asObservableSuccess() }
+ .map { pageListParse(it) }
+ }
+
+ override fun pageListParse(document: Document): List {
+ return document.select("a").mapIndexed { i, element ->
+ Page(i, element.attr("href"), element.select(".lazy.preloader[src]").attr("src").replace("t.", "."))
+ }
+ }
+
+ override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
+
+ // Filters
+
+ private class SortOrderFilter(sortOrderURIs: List>, state: Int) :
+ Filter.Select("Sort By", sortOrderURIs.map { it.first }.toTypedArray(), state)
+ private open class SearchFlagFilter(name: String, val uri: String, state: Boolean = true) : Filter.CheckBox(name, state)
+ private class LanguageFilter(name: String, uri: String = name) : SearchFlagFilter(name, uri, false)
+ private class LanguageFilters(flags: List) : Filter.Group("Other Languages", flags)
+ private class CategoryFilters(flags: List) : Filter.Group("Categories", flags)
+
+ override fun getFilterList() = getFilterList(SORT_ORDER_DEFAULT)
+
+ private fun getFilterList(sortOrderState: Int) = FilterList(
+ SortOrderFilter(getSortOrderURIs(), sortOrderState),
+ CategoryFilters(getCategoryURIs()),
+ LanguageFilters(getLanguageURIs().filter { it.name != imhLang }) // exclude main lang
+ )
+
+ private fun getCategoryURIs() = listOf(
+ SearchFlagFilter("Manga", "manga"),
+ SearchFlagFilter("Doujinshi", "doujinshi"),
+ SearchFlagFilter("Western", "western"),
+ SearchFlagFilter("Image Set", "imageset"),
+ SearchFlagFilter("Artist CG", "artistcg"),
+ SearchFlagFilter("Game CG", "gamecg")
+ )
+
+ // update sort order indices in companion object if order is changed
+ private fun getSortOrderURIs() = listOf(
+ Pair("Popular", "pp"),
+ Pair("Latest", "lt"),
+ Pair("Downloads", "dl"),
+ Pair("Top Rated", "tr")
+ )
+
+ private fun getLanguageURIs() = listOf(
+ LanguageFilter(LANGUAGE_ENGLISH, "en"),
+ LanguageFilter(LANGUAGE_JAPANESE, "jp"),
+ LanguageFilter(LANGUAGE_SPANISH, "es"),
+ LanguageFilter(LANGUAGE_FRENCH, "fr"),
+ LanguageFilter(LANGUAGE_KOREAN, "kr"),
+ LanguageFilter(LANGUAGE_GERMAN, "de"),
+ LanguageFilter(LANGUAGE_RUSSIAN, "ru")
+ )
+
+ private fun getLanguageURIByName(name: String): LanguageFilter {
+ return getLanguageURIs().first { it.name == name }
+ }
+
+ companion object {
+
+ // references to sort order indices
+ private const val SORT_ORDER_POPULAR = 0
+ private const val SORT_ORDER_LATEST = 1
+ private const val SORT_ORDER_DEFAULT = SORT_ORDER_POPULAR
+
+ // references to be used in factory
+ const val LANGUAGE_ENGLISH = "English"
+ const val LANGUAGE_JAPANESE = "Japanese"
+ const val LANGUAGE_SPANISH = "Spanish"
+ const val LANGUAGE_FRENCH = "French"
+ const val LANGUAGE_KOREAN = "Korean"
+ const val LANGUAGE_GERMAN = "German"
+ const val LANGUAGE_RUSSIAN = "Russian"
+
+ private const val PAEG_LOAD_URL: String = "https://imhentai.com/inc/thumbs_loader.php"
+ }
+}
diff --git a/src/all/imhentai/src/eu/kanade/tachiyomi/extension/all/imhentai/IMHentaiFactory.kt b/src/all/imhentai/src/eu/kanade/tachiyomi/extension/all/imhentai/IMHentaiFactory.kt
new file mode 100644
index 000000000..38541894d
--- /dev/null
+++ b/src/all/imhentai/src/eu/kanade/tachiyomi/extension/all/imhentai/IMHentaiFactory.kt
@@ -0,0 +1,19 @@
+package eu.kanade.tachiyomi.extension.all.imhentai
+
+import eu.kanade.tachiyomi.annotations.Nsfw
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.source.SourceFactory
+
+@Nsfw
+class IMHentaiFactory : SourceFactory {
+
+ override fun createSources(): List = listOf(
+ IMHentai("en", IMHentai.LANGUAGE_ENGLISH),
+ IMHentai("ja", IMHentai.LANGUAGE_JAPANESE),
+ IMHentai("es", IMHentai.LANGUAGE_SPANISH),
+ IMHentai("fr", IMHentai.LANGUAGE_FRENCH),
+ IMHentai("ko", IMHentai.LANGUAGE_KOREAN),
+ IMHentai("de", IMHentai.LANGUAGE_GERMAN),
+ IMHentai("ru", IMHentai.LANGUAGE_RUSSIAN)
+ )
+}