diff --git a/src/ca/fansubscat/AndroidManifest.xml b/src/ca/fansubscat/AndroidManifest.xml
new file mode 100644
index 000000000..30deb7f79
--- /dev/null
+++ b/src/ca/fansubscat/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/ca/fansubscat/build.gradle b/src/ca/fansubscat/build.gradle
new file mode 100644
index 000000000..6c723cb91
--- /dev/null
+++ b/src/ca/fansubscat/build.gradle
@@ -0,0 +1,13 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+ext {
+ extName = 'Fansubs.cat'
+ pkgNameSuffix = 'ca.fansubscat'
+ extClass = '.FansubsCat'
+ extVersionCode = 1
+ libVersion = '1.2'
+ containsNsfw = true
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/ca/fansubscat/res/mipmap-hdpi/ic_launcher.png b/src/ca/fansubscat/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..8e0468783
Binary files /dev/null and b/src/ca/fansubscat/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/ca/fansubscat/res/mipmap-mdpi/ic_launcher.png b/src/ca/fansubscat/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..e0702ecea
Binary files /dev/null and b/src/ca/fansubscat/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/ca/fansubscat/res/mipmap-xhdpi/ic_launcher.png b/src/ca/fansubscat/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..b0027ad57
Binary files /dev/null and b/src/ca/fansubscat/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/ca/fansubscat/res/mipmap-xxhdpi/ic_launcher.png b/src/ca/fansubscat/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..2424e711f
Binary files /dev/null and b/src/ca/fansubscat/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/ca/fansubscat/res/mipmap-xxxhdpi/ic_launcher.png b/src/ca/fansubscat/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..ccc47ea5f
Binary files /dev/null and b/src/ca/fansubscat/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/ca/fansubscat/res/web_hi_res_512.png b/src/ca/fansubscat/res/web_hi_res_512.png
new file mode 100644
index 000000000..8e8555a10
Binary files /dev/null and b/src/ca/fansubscat/res/web_hi_res_512.png differ
diff --git a/src/ca/fansubscat/src/eu/kanade/tachiyomi/extension/ca/fansubscat/FansubsCat.kt b/src/ca/fansubscat/src/eu/kanade/tachiyomi/extension/ca/fansubscat/FansubsCat.kt
new file mode 100644
index 000000000..2f9d34e86
--- /dev/null
+++ b/src/ca/fansubscat/src/eu/kanade/tachiyomi/extension/ca/fansubscat/FansubsCat.kt
@@ -0,0 +1,160 @@
+package eu.kanade.tachiyomi.extension.ca.fansubscat
+
+import com.github.salomonbrys.kotson.float
+import com.github.salomonbrys.kotson.fromJson
+import com.github.salomonbrys.kotson.get
+import com.github.salomonbrys.kotson.long
+import com.github.salomonbrys.kotson.nullString
+import com.github.salomonbrys.kotson.string
+import com.google.gson.Gson
+import com.google.gson.JsonObject
+import eu.kanade.tachiyomi.extension.BuildConfig
+import eu.kanade.tachiyomi.network.GET
+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 okhttp3.Headers
+import okhttp3.HttpUrl
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import rx.Observable
+
+class FansubsCat : HttpSource() {
+
+ override val name = "Fansubs.cat"
+
+ override val baseUrl = "https://manga.fansubs.cat"
+
+ override val lang = "ca"
+
+ override val supportsLatest = true
+
+ override fun headersBuilder(): Headers.Builder = Headers.Builder()
+ .add("User-Agent", "Tachiyomi/FansubsCat/${BuildConfig.VERSION_NAME}")
+
+ override val client: OkHttpClient = network.client
+
+ private val gson = Gson()
+
+ private val apiBaseUrl = "https://api.fansubs.cat"
+
+ private fun parseMangaFromJson(response: Response): MangasPage {
+ val jsonObject = gson.fromJson(response.body()!!.string())
+
+ val mangas = jsonObject["result"].asJsonArray.map { json ->
+ SManga.create().apply {
+ url = json["slug"].string
+ title = json["name"].string
+ thumbnail_url = json["thumbnail_url"].string
+ author = json["author"].nullString
+ description = json["synopsis"].nullString
+ status = json["status"].string.toStatus()
+ genre = json["genres"].nullString
+ }
+ }
+
+ return MangasPage(mangas, mangas.size >= 20)
+ }
+
+ private fun parseChapterListFromJson(response: Response): List {
+ val jsonObject = gson.fromJson(response.body()!!.string())
+
+ return jsonObject["result"].asJsonArray.map { json ->
+ SChapter.create().apply {
+ url = json["id"].string
+ name = json["title"].string
+ chapter_number = json["number"].float
+ scanlator = json["fansub"].string
+ date_upload = json["created"].long
+ }
+ }
+ }
+
+ private fun parsePageListFromJson(response: Response): List {
+ val jsonObject = gson.fromJson(response.body()!!.string())
+
+ return jsonObject["result"].asJsonArray.mapIndexed { i, it ->
+ Page(i, it["url"].asString, it["url"].asString)
+ }
+ }
+
+ // Popular
+
+ override fun popularMangaRequest(page: Int): Request {
+ return GET("$apiBaseUrl/manga/popular/$page", headers)
+ }
+
+ override fun popularMangaParse(response: Response): MangasPage = parseMangaFromJson(response)
+
+ // Latest
+
+ override fun latestUpdatesRequest(page: Int): Request {
+ return GET("$apiBaseUrl/manga/recent/$page", headers)
+ }
+
+ override fun latestUpdatesParse(response: Response): MangasPage = parseMangaFromJson(response)
+
+ // Search
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ val url = HttpUrl.parse("$apiBaseUrl/manga/search/$page")!!.newBuilder()
+ .addQueryParameter("query", query)
+ return GET(url.toString(), headers)
+ }
+
+ override fun searchMangaParse(response: Response): MangasPage = parseMangaFromJson(response)
+
+ // Details
+
+ // Workaround to allow "Open in browser" to use the real URL
+ override fun fetchMangaDetails(manga: SManga): Observable =
+ client.newCall(apiMangaDetailsRequest(manga)).asObservableSuccess()
+ .map { mangaDetailsParse(it).apply { initialized = true } }
+
+ // Return the real URL for "Open in browser"
+ override fun mangaDetailsRequest(manga: SManga) = GET("$baseUrl/${manga.url}", headers)
+
+ private fun apiMangaDetailsRequest(manga: SManga): Request {
+ return GET("$apiBaseUrl/manga/details/${manga.url.substringAfterLast('/')}", headers)
+ }
+
+ override fun mangaDetailsParse(response: Response): SManga {
+ val jsonObject = gson.fromJson(response.body()!!.string())
+
+ return SManga.create().apply {
+ url = jsonObject["result"]["slug"].string
+ title = jsonObject["result"]["name"].string
+ thumbnail_url = jsonObject["result"]["thumbnail_url"].string
+ author = jsonObject["result"]["author"].nullString
+ description = jsonObject["result"]["synopsis"].nullString
+ status = jsonObject["result"]["status"].string.toStatus()
+ genre = jsonObject["result"]["genres"].nullString
+ }
+ }
+
+ private fun String?.toStatus() = when {
+ this == null -> SManga.UNKNOWN
+ this.contains("ongoing", ignoreCase = true) -> SManga.ONGOING
+ this.contains("finished", ignoreCase = true) -> SManga.COMPLETED
+ else -> SManga.UNKNOWN
+ }
+
+ // Chapters
+
+ override fun chapterListRequest(manga: SManga): Request = GET("$apiBaseUrl/manga/chapters/${manga.url.substringAfterLast('/')}", headers)
+
+ override fun chapterListParse(response: Response): List = parseChapterListFromJson(response)
+
+ // Pages
+
+ override fun pageListRequest(chapter: SChapter): Request = GET("$apiBaseUrl/manga/pages/${chapter.url}", headers)
+
+ override fun pageListParse(response: Response): List = parsePageListFromJson(response)
+
+ override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not used")
+}