diff --git a/src/en/doujins/AndroidManifest.xml b/src/en/doujins/AndroidManifest.xml
new file mode 100644
index 000000000..30deb7f79
--- /dev/null
+++ b/src/en/doujins/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/en/doujins/build.gradle b/src/en/doujins/build.gradle
new file mode 100644
index 000000000..0647229d2
--- /dev/null
+++ b/src/en/doujins/build.gradle
@@ -0,0 +1,13 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+ext {
+ extName = 'Doujins'
+ pkgNameSuffix = 'en.doujins'
+ extClass = '.Doujins'
+ extVersionCode = 1
+ libVersion = '1.2'
+ containsNsfw = true
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/en/doujins/res/mipmap-hdpi/ic_launcher.png b/src/en/doujins/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..21396b51e
Binary files /dev/null and b/src/en/doujins/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/en/doujins/res/mipmap-mdpi/ic_launcher.png b/src/en/doujins/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..d00cba10c
Binary files /dev/null and b/src/en/doujins/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/en/doujins/res/mipmap-xhdpi/ic_launcher.png b/src/en/doujins/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..7efb531c3
Binary files /dev/null and b/src/en/doujins/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/en/doujins/res/mipmap-xxhdpi/ic_launcher.png b/src/en/doujins/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..f440235e5
Binary files /dev/null and b/src/en/doujins/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/en/doujins/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/doujins/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..e33bbc81a
Binary files /dev/null and b/src/en/doujins/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/en/doujins/res/web_hi_res_512.png b/src/en/doujins/res/web_hi_res_512.png
new file mode 100644
index 000000000..221eaf878
Binary files /dev/null and b/src/en/doujins/res/web_hi_res_512.png differ
diff --git a/src/en/doujins/src/eu/kanade/tachiyomi/extension/en/doujins/Doujins.kt b/src/en/doujins/src/eu/kanade/tachiyomi/extension/en/doujins/Doujins.kt
new file mode 100644
index 000000000..b3e1a444e
--- /dev/null
+++ b/src/en/doujins/src/eu/kanade/tachiyomi/extension/en/doujins/Doujins.kt
@@ -0,0 +1,253 @@
+package eu.kanade.tachiyomi.extension.en.doujins
+
+import android.app.Application
+import android.content.SharedPreferences
+import com.github.salomonbrys.kotson.fromJson
+import com.github.salomonbrys.kotson.get
+import com.google.gson.Gson
+import com.google.gson.JsonObject
+import eu.kanade.tachiyomi.annotations.Nsfw
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.source.model.Filter
+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.online.HttpSource
+import eu.kanade.tachiyomi.util.asJsoup
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.nodes.Document
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.text.ParseException
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Date
+import java.util.Locale
+
+@Nsfw
+class Doujins : HttpSource() {
+
+ override val baseUrl: String = "https://doujins.com"
+
+ override val lang: String = "en"
+
+ override val name: String = "Doujins"
+
+ override val supportsLatest: Boolean = true
+
+ private val gson = Gson()
+
+ private val preferences: SharedPreferences by lazy {
+ Injekt.get().getSharedPreferences("source_$id", 0x0000)
+ }
+
+ override fun chapterListParse(response: Response): List {
+ return listOf(
+ SChapter.create().apply {
+ name = "Chapter"
+ setUrlWithoutDomain(response.request().url().toString())
+
+ val date = response.asJsoup().select(".folder-message").last().text().substringBefore(" • ")
+ for (dateFormat in MANGA_DETAILS_DATE_FORMAT) {
+ if (date_upload == 0L)
+ date_upload = dateFormat.parseOrNull(date)?.time ?: 0L
+ else
+ break
+ }
+ }
+ )
+ }
+
+ override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not used")
+
+ override fun latestUpdatesParse(response: Response): MangasPage {
+ return MangasPage(
+ gson.fromJson(response.body()!!.string())["folders"].asJsonArray.map {
+ SManga.create().apply {
+ setUrlWithoutDomain(it["link"].asString)
+ title = it["name"].asString
+ artist = it["artistList"].asString
+ author = artist
+ genre = it["tags"].asJsonArray.joinToString(", ") { it["tag"].asString }
+ thumbnail_url = it["thumbnail2"].asString
+ }
+ },
+ true
+ )
+ }
+
+ private fun getLatestPageUrl(page: Int): String {
+ val endDate = Calendar.getInstance().apply {
+ add(Calendar.DATE, 1)
+ set(Calendar.HOUR_OF_DAY, 0)
+ set(Calendar.MINUTE, 0)
+ set(Calendar.SECOND, 0)
+ set(Calendar.MILLISECOND, 0)
+ add(Calendar.DATE, -1 * PAGE_DAYS * (page - 1))
+ }
+
+ val endDateSec = endDate.timeInMillis / 1000
+ val startDateSec = endDate.apply {
+ add(Calendar.DATE, -1 * PAGE_DAYS)
+ }.timeInMillis / 1000
+
+ return "$baseUrl/folders?start=$startDateSec&end=$endDateSec"
+ }
+
+ override fun latestUpdatesRequest(page: Int) = GET(getLatestPageUrl(page))
+
+ override fun mangaDetailsParse(response: Response): SManga {
+ val document = response.asJsoup()
+ return SManga.create().apply {
+ title = document.select(".folder-title a").last().text()
+ artist = document.select(".gallery-artist a").joinToString(", ") { it.text() }
+ author = artist
+ }
+ }
+
+ override fun pageListParse(response: Response): List {
+ val document = response.asJsoup()
+ val pageUrl = response.request().url().toString()
+ return document.select(".doujin").mapIndexed { i, page ->
+ Page(i, "$pageUrl${page.attr("data-link")}", page.attr("data-file").replace("amp;", ""))
+ }
+ }
+
+ override fun popularMangaParse(response: Response) = parseGalleryPage(response.asJsoup())
+
+ override fun popularMangaRequest(page: Int) = GET("$baseUrl/top/month")
+
+ override fun searchMangaParse(response: Response) = parseGalleryPage(response.asJsoup())
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ val filterList = if (filters.isEmpty()) getFilterList() else filters
+ val seriesFilter = filterList.findInstance()!!
+ val sortFilter = filterList.findInstance()!!
+ val popularityPeriodFilter = filterList.findInstance()!!
+
+ return when {
+ query != "" -> {
+ GET("$baseUrl/searches?words=$query&page=$page&sort=${sortFilter.toUriPart()}")
+ }
+ seriesFilter.toUriPart() != "" -> {
+ GET("$baseUrl${seriesFilter.toUriPart()}?sort=${sortFilter.toUriPart()}")
+ }
+ else -> {
+ GET("$baseUrl${popularityPeriodFilter.toUriPart()}")
+ }
+ }
+ }
+
+ private fun parseGalleryPage(document: Document): MangasPage {
+
+ val pagination = document.select(".pagination").first()
+ return MangasPage(
+ document.select("a.gallery-visited-from-favorites").map {
+ SManga.create().apply {
+ setUrlWithoutDomain(it.attr("href"))
+ title = it.select("div.title .text").text()
+ artist = it.parent().nextElementSibling().select(".single-line strong")?.last()?.text()?.substringAfter("Artist: ")
+ author = artist
+ thumbnail_url = it.select("img").attr("srcset")
+ }
+ },
+ if (pagination != null) {
+ !pagination.select("li.page-item:last-child").hasClass("disabled")
+ } else {
+ false
+ }
+ )
+ }
+
+ override fun getFilterList(): FilterList = FilterList(
+ Filter.Header("Text search ignores series and period filters"),
+ Filter.Separator(),
+
+ Filter.Header("Series filter overrides period filter"),
+ SeriesFilter(),
+ Filter.Separator(),
+
+ Filter.Header("Period filter only applies at initial page"),
+ PopularityPeriodFilter(),
+ Filter.Separator(),
+
+ Filter.Header("Sort only works with text search and series filter"),
+ SortFilter()
+ )
+
+ private class SeriesFilter : UriPartFilter(
+ "Series",
+ arrayOf(
+ Pair("None", ""),
+ Pair("Doujins - Original Series", "/doujins-original-series-19934"),
+ Pair("Hentai Magazine Chapters", "/hentai-magazine-chapters-2766"),
+ Pair("Hentai Manga", "/hentai-manga-19"),
+ Pair("Fate Grand Order", "/fate-grand-order-doujins-28615"),
+ Pair("CG Sets - Original Series", "/cg-sets-original-series-14865"),
+ Pair("Touhou", "/touhou-doujins-7748"),
+ Pair("Naruto", "/naruto-doujins-5761"),
+ Pair("Kantai Collection", "/kantai-collection-doujins-22720"),
+ Pair("Hentai Game CG-Sets", "/hentai-game-cg-sets-2422"),
+ Pair("One Piece", "/one-piece-doujins-6080"),
+ Pair("Granblue Fantasy", "/granblue-fantasy-doujins-28177"),
+ Pair("Azur Lane", "/azur-lane-doujins-34298"),
+ Pair("Sword Art Online", "/sword-art-online-doujins-7246"),
+ Pair("Idolmaster", "/idolmaster-4281"),
+ Pair("My Hero Academia", "/my-hero-academia-doujins-28744"),
+ Pair("Love Live", "/love-live-doujins-21865"),
+ Pair("Pokemon", "/pokemon-doujins-6393"),
+ Pair("Dragon Ball", "/dragon-ball-doujins-1238"),
+ Pair("CGs - Mixed Series", "/cgs-mixed-series-35311"),
+ Pair("Doujins - Mixed Series", "/doujins-mixed-series-20091"),
+ Pair("Hentai Magazine Chapters", "/hentai-magazine-chapters-2766"),
+ Pair("Hentai Magazine Chapters - Super-Shorts", "/hentai-magazine-chapters-super-shorts-19933"),
+ Pair("Hentai Manga", "/hentai-manga-19")
+ )
+ )
+
+ private class SortFilter : UriPartFilter(
+ "Sort",
+ arrayOf(
+ Pair("Newest First", ""),
+ Pair("Oldest First", "created_at"),
+ Pair("Alphabetical", "name"),
+ Pair("Rating", "-cached_score"),
+ Pair("Popularity", "-cached_views")
+ )
+ )
+
+ private class PopularityPeriodFilter : UriPartFilter(
+ "Period",
+ arrayOf(
+ Pair("This Month", "/top"),
+ Pair("This Year", "/top/year"),
+ Pair("All Time", "/top/all"),
+ )
+ )
+
+ private open class UriPartFilter(displayName: String, val vals: Array>) :
+ Filter.Select(displayName, vals.map { it.first }.toTypedArray()) {
+ fun toUriPart() = vals[state].second
+ }
+
+ private fun SimpleDateFormat.parseOrNull(string: String): Date? {
+ return try {
+ parse(string)
+ } catch (e: ParseException) {
+ null
+ }
+ }
+
+ private inline fun Iterable<*>.findInstance() = find { it is T } as? T
+
+ companion object {
+ private const val PAGE_DAYS = 3
+ private val ORDINAL_SUFFIXES = listOf("th", "st", "nd", "rd")
+ private val MANGA_DETAILS_DATE_FORMAT = ORDINAL_SUFFIXES.map {
+ SimpleDateFormat("MMMM dd'$it', yyyy", Locale.US)
+ }
+ }
+}