diff --git a/src/en/kouhaiwork/AndroidManifest.xml b/src/en/kouhaiwork/AndroidManifest.xml
new file mode 100644
index 000000000..30deb7f79
--- /dev/null
+++ b/src/en/kouhaiwork/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/en/kouhaiwork/build.gradle b/src/en/kouhaiwork/build.gradle
new file mode 100644
index 000000000..7092fad4b
--- /dev/null
+++ b/src/en/kouhaiwork/build.gradle
@@ -0,0 +1,12 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlinx-serialization'
+
+ext {
+ extName = 'Kouhai Scanlations'
+ pkgNameSuffix = 'en.kouhaiwork'
+ extClass = '.KouhaiWork'
+ extVersionCode = 1
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/en/kouhaiwork/res/mipmap-hdpi/ic_launcher.png b/src/en/kouhaiwork/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..90295216c
Binary files /dev/null and b/src/en/kouhaiwork/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/en/kouhaiwork/res/mipmap-mdpi/ic_launcher.png b/src/en/kouhaiwork/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..682fe5c69
Binary files /dev/null and b/src/en/kouhaiwork/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/en/kouhaiwork/res/mipmap-xhdpi/ic_launcher.png b/src/en/kouhaiwork/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..cedcf2010
Binary files /dev/null and b/src/en/kouhaiwork/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/en/kouhaiwork/res/mipmap-xxhdpi/ic_launcher.png b/src/en/kouhaiwork/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..72b5c4cc9
Binary files /dev/null and b/src/en/kouhaiwork/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/en/kouhaiwork/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/kouhaiwork/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..ef1ca9f06
Binary files /dev/null and b/src/en/kouhaiwork/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/en/kouhaiwork/res/web_hi_res_512.png b/src/en/kouhaiwork/res/web_hi_res_512.png
new file mode 100644
index 000000000..c868c5942
Binary files /dev/null and b/src/en/kouhaiwork/res/web_hi_res_512.png differ
diff --git a/src/en/kouhaiwork/src/eu/kanade/tachiyomi/extension/en/kouhaiwork/KouhaiFilters.kt b/src/en/kouhaiwork/src/eu/kanade/tachiyomi/extension/en/kouhaiwork/KouhaiFilters.kt
new file mode 100644
index 000000000..01f52eeeb
--- /dev/null
+++ b/src/en/kouhaiwork/src/eu/kanade/tachiyomi/extension/en/kouhaiwork/KouhaiFilters.kt
@@ -0,0 +1,56 @@
+package eu.kanade.tachiyomi.extension.en.kouhaiwork
+
+import eu.kanade.tachiyomi.source.model.Filter
+
+class Genre(val id: Int, name: String) : Filter.CheckBox(name)
+
+private val genres: List
+ get() = listOf(
+ Genre(1, "Romance"),
+ Genre(2, "Comedy"),
+ Genre(3, "Slice of Life"),
+ Genre(4, "Fantasy"),
+ Genre(5, "Sci-Fi"),
+ Genre(6, "Psychological"),
+ Genre(7, "Horror"),
+ Genre(8, "Mystery"),
+ Genre(9, "Girls' Love"),
+ Genre(10, "Drama"),
+ Genre(11, "Action"),
+ Genre(12, "Ecchi"),
+ Genre(13, "Adventure"),
+ )
+
+class GenresFilter(values: List = genres) :
+ Filter.Group("Genres", values)
+
+class Theme(val id: Int, name: String) : Filter.CheckBox(name)
+
+private val themes: List
+ get() = listOf(
+ Theme(1, "Office Workers"),
+ Theme(2, "Family"),
+ Theme(3, "Supernatural"),
+ Theme(4, "Demons"),
+ Theme(5, "Magic"),
+ Theme(6, "Aliens"),
+ Theme(7, "Suggestive"),
+ Theme(8, "Doujinshi"),
+ Theme(9, "School Life"),
+ Theme(10, "Police"),
+ )
+
+class ThemesFilter(values: List = themes) :
+ Filter.Group("Themes", values)
+
+private val demographics: Array
+ get() = arrayOf("Any", "Shounen", "Shoujo", "Seinen")
+
+class DemographicsFilter(values: Array = demographics) :
+ Filter.Select("Demographic", values)
+
+private val statuses: Array
+ get() = arrayOf("Any", "Ongoing", "Finished", "Axed/Dropped")
+
+class StatusFilter(values: Array = statuses) :
+ Filter.Select("Status", values)
diff --git a/src/en/kouhaiwork/src/eu/kanade/tachiyomi/extension/en/kouhaiwork/KouhaiModels.kt b/src/en/kouhaiwork/src/eu/kanade/tachiyomi/extension/en/kouhaiwork/KouhaiModels.kt
new file mode 100644
index 000000000..59fc77caf
--- /dev/null
+++ b/src/en/kouhaiwork/src/eu/kanade/tachiyomi/extension/en/kouhaiwork/KouhaiModels.kt
@@ -0,0 +1,39 @@
+package eu.kanade.tachiyomi.extension.en.kouhaiwork
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class KouhaiSeries(
+ val id: Int,
+ val title: String,
+ val cover: String,
+ val synopsis: String,
+ val status: String,
+ val artists: List,
+ val authors: List,
+ val alternative_titles: List,
+ val genres: List? = null,
+ val themes: List? = null,
+ val demographics: List? = null,
+ val chapters: List
+)
+
+@Serializable
+data class KouhaiChapter(
+ val id: Int,
+ val group: String,
+ val number: Float,
+ val updated_at: String,
+ val name: String? = null
+)
+
+@Serializable
+data class KouhaiTag(val id: Int)
+
+@Serializable
+data class KouhaiTagList(
+ val genres: List,
+ val themes: List,
+ val demographics: List,
+ val status: KouhaiTag?
+)
diff --git a/src/en/kouhaiwork/src/eu/kanade/tachiyomi/extension/en/kouhaiwork/KouhaiWork.kt b/src/en/kouhaiwork/src/eu/kanade/tachiyomi/extension/en/kouhaiwork/KouhaiWork.kt
new file mode 100644
index 000000000..18e4f628a
--- /dev/null
+++ b/src/en/kouhaiwork/src/eu/kanade/tachiyomi/extension/en/kouhaiwork/KouhaiWork.kt
@@ -0,0 +1,151 @@
+package eu.kanade.tachiyomi.extension.en.kouhaiwork
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.POST
+import eu.kanade.tachiyomi.network.asObservableSuccess
+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 kotlinx.serialization.json.Json
+import kotlinx.serialization.json.decodeFromJsonElement
+import kotlinx.serialization.json.encodeToJsonElement
+import kotlinx.serialization.json.jsonArray
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import okhttp3.FormBody
+import okhttp3.Response
+import uy.kohesive.injekt.injectLazy
+import java.text.DecimalFormat
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+class KouhaiWork : HttpSource() {
+ override val name = "Kouhai Scanlations"
+
+ override val baseUrl = "https://kouhai.work"
+
+ override val lang = "en"
+
+ override val supportsLatest = true
+
+ private val json by injectLazy()
+
+ override fun latestUpdatesRequest(page: Int) =
+ GET("$API_URL/manga/week", headers)
+
+ override fun latestUpdatesParse(response: Response) =
+ response.parse()["data"]?.jsonArray?.map {
+ val arr = it.jsonArray
+ SManga.create().apply {
+ url = arr[0].jsonPrimitive.content
+ title = arr[1].jsonPrimitive.content
+ thumbnail_url = arr.last().jsonPrimitive.content
+ }
+ }.let { MangasPage(it ?: emptyList(), false) }
+
+ override fun popularMangaRequest(page: Int) =
+ GET("$API_URL/manga/all", headers)
+
+ override fun popularMangaParse(response: Response) =
+ latestUpdatesParse(response)
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
+ FormBody.Builder().add("search", query).add("tags", filters.json())
+ .let { POST("$API_URL/manga/search", headers, it.build()) }
+
+ override fun searchMangaParse(response: Response) =
+ latestUpdatesParse(response)
+
+ // Request the actual manga URL for the webview
+ override fun mangaDetailsRequest(manga: SManga) =
+ GET("$baseUrl/series/${manga.url}", headers)
+
+ override fun fetchMangaDetails(manga: SManga) =
+ client.newCall(chapterListRequest(manga)).asObservableSuccess().map {
+ val series = it.data()
+ manga.description = series.synopsis
+ manga.author = series.authors.joinToString()
+ manga.artist = series.artists.joinToString()
+ manga.genre = series.genres.orEmpty()
+ .plus(series.themes.orEmpty())
+ .plus(series.demographics.orEmpty())
+ .joinToString()
+ manga.status = when (series.status) {
+ "ongoing" -> SManga.ONGOING
+ "finished" -> SManga.COMPLETED
+ else -> SManga.UNKNOWN
+ }
+ manga.initialized = true
+ return@map manga
+ }!!
+
+ override fun chapterListRequest(manga: SManga) =
+ GET("$API_URL/mangas/${manga.url}", headers)
+
+ override fun chapterListParse(response: Response) =
+ response.data().chapters.map {
+ SChapter.create().apply {
+ url = it.id.toString()
+ scanlator = it.group
+ chapter_number = it.number
+ name = "Chapter ${decimalFormat.format(it.number)}" +
+ if (it.name == null) "" else " - ${it.name}"
+ date_upload = dateFormat.parse(it.updated_at)?.time ?: 0L
+ }
+ }
+
+ override fun pageListRequest(chapter: SChapter) =
+ GET("$API_URL/chapters/${chapter.url}", headers)
+
+ override fun pageListParse(response: Response) =
+ response.parse()["chapter"]!!.jsonObject["pages"]!!
+ .jsonArray.mapIndexed { idx, obj ->
+ Page(idx, "", obj.jsonObject["media"]!!.jsonPrimitive.content)
+ }
+
+ override fun getFilterList() =
+ FilterList(GenresFilter(), ThemesFilter(), DemographicsFilter(), StatusFilter())
+
+ override fun mangaDetailsParse(response: Response) =
+ throw UnsupportedOperationException("Not used")
+
+ override fun imageUrlParse(response: Response) =
+ throw UnsupportedOperationException("Not used")
+
+ @Suppress("NOTHING_TO_INLINE")
+ private inline fun FilterList.json() =
+ json.encodeToJsonElement(
+ KouhaiTagList(
+ find()?.state?.filter { it.state }
+ ?.map { KouhaiTag(it.id) } ?: emptyList(),
+ find()?.state?.filter { it.state }
+ ?.map { KouhaiTag(it.id) } ?: emptyList(),
+ find()?.state?.takeIf { it != 0 }
+ ?.let { listOf(KouhaiTag(it)) } ?: emptyList(),
+ find()?.state?.takeIf { it != 0 }
+ ?.let { KouhaiTag(it - 1) }
+ )
+ ).toString()
+
+ @Suppress("NOTHING_TO_INLINE")
+ private inline fun Response.parse() =
+ json.parseToJsonElement(body!!.string()).jsonObject
+
+ private inline fun Response.data() =
+ json.decodeFromJsonElement(parse()["data"]!!)
+
+ private inline fun FilterList.find() = find { it is T } as? T
+
+ companion object {
+ private const val API_URL = "https://api.kouhai.work/v2"
+
+ private const val ISO_DATE = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"
+
+ private val dateFormat = SimpleDateFormat(ISO_DATE, Locale.ROOT)
+
+ private val decimalFormat = DecimalFormat("#.##")
+ }
+}