diff --git a/src/all/deviantart/AndroidManifest.xml b/src/all/deviantart/AndroidManifest.xml
new file mode 100644
index 000000000..7ceef95ab
--- /dev/null
+++ b/src/all/deviantart/AndroidManifest.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/all/deviantart/build.gradle b/src/all/deviantart/build.gradle
new file mode 100644
index 000000000..2d28101d2
--- /dev/null
+++ b/src/all/deviantart/build.gradle
@@ -0,0 +1,8 @@
+ext {
+ extName = 'DeviantArt'
+ extClass = '.DeviantArt'
+ extVersionCode = 1
+ isNsfw = true
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/all/deviantart/res/mipmap-hdpi/ic_launcher.png b/src/all/deviantart/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..66c4fd94d
Binary files /dev/null and b/src/all/deviantart/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/all/deviantart/res/mipmap-mdpi/ic_launcher.png b/src/all/deviantart/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..4baedaeca
Binary files /dev/null and b/src/all/deviantart/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/all/deviantart/res/mipmap-xhdpi/ic_launcher.png b/src/all/deviantart/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..330f51bbf
Binary files /dev/null and b/src/all/deviantart/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/all/deviantart/res/mipmap-xxhdpi/ic_launcher.png b/src/all/deviantart/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..f3e42c0bf
Binary files /dev/null and b/src/all/deviantart/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/all/deviantart/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/deviantart/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..564beb16f
Binary files /dev/null and b/src/all/deviantart/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/all/deviantart/src/eu/kanade/tachiyomi/extension/all/deviantart/DeviantArt.kt b/src/all/deviantart/src/eu/kanade/tachiyomi/extension/all/deviantart/DeviantArt.kt
new file mode 100644
index 000000000..b52a281da
--- /dev/null
+++ b/src/all/deviantart/src/eu/kanade/tachiyomi/extension/all/deviantart/DeviantArt.kt
@@ -0,0 +1,167 @@
+package eu.kanade.tachiyomi.extension.all.deviantart
+
+import eu.kanade.tachiyomi.network.GET
+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.HttpUrl.Companion.toHttpUrl
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.Jsoup
+import org.jsoup.nodes.Document
+import org.jsoup.parser.Parser
+import java.text.ParseException
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+class DeviantArt : HttpSource() {
+ override val name = "DeviantArt"
+ override val baseUrl = "https://deviantart.com"
+ override val lang = "all"
+ override val supportsLatest = false
+
+ private val backendBaseUrl = "https://backend.deviantart.com"
+ private fun backendBuilder() = backendBaseUrl.toHttpUrl().newBuilder()
+
+ private val dateFormat by lazy {
+ SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH)
+ }
+
+ private fun parseDate(dateStr: String?): Long {
+ return try {
+ dateFormat.parse(dateStr ?: "")!!.time
+ } catch (_: ParseException) {
+ 0L
+ }
+ }
+
+ override fun popularMangaRequest(page: Int): Request {
+ throw UnsupportedOperationException(SEARCH_FORMAT_MSG)
+ }
+
+ override fun popularMangaParse(response: Response): MangasPage {
+ throw UnsupportedOperationException(SEARCH_FORMAT_MSG)
+ }
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ require(query.startsWith("gallery:")) { SEARCH_FORMAT_MSG }
+ val querySegments = query.substringAfter(":").split("/")
+ val username = querySegments[0]
+ val folderId = querySegments.getOrElse(1) { "all" }
+ return GET("$baseUrl/$username/gallery/$folderId", headers)
+ }
+
+ override fun searchMangaParse(response: Response): MangasPage {
+ val manga = mangaDetailsParse(response)
+ return MangasPage(listOf(manga), false)
+ }
+
+ override fun latestUpdatesRequest(page: Int): Request {
+ throw UnsupportedOperationException()
+ }
+
+ override fun latestUpdatesParse(response: Response): MangasPage {
+ throw UnsupportedOperationException()
+ }
+
+ override fun mangaDetailsParse(response: Response): SManga {
+ val document = response.asJsoup()
+ val subFolderGallery = document.selectFirst("#sub-folder-gallery")
+ val manga = SManga.create().apply {
+ // If manga is sub-gallery then use sub-gallery name, else use gallery name
+ title = subFolderGallery?.selectFirst("._2vMZg + ._2vMZg")?.text()?.substringBeforeLast(" ")
+ ?: document.selectFirst(".ds-card-selected h2")!!.text()
+ author = document.title().substringBefore(" ")
+ description = subFolderGallery?.selectFirst(".legacy-journal")?.wholeText()
+ thumbnail_url = subFolderGallery?.selectFirst("img[property=contentUrl]")?.absUrl("src")
+ }
+ manga.setUrlWithoutDomain(response.request.url.toString())
+ return manga
+ }
+
+ override fun chapterListRequest(manga: SManga): Request {
+ val pathSegments = getMangaUrl(manga).toHttpUrl().pathSegments
+ val username = pathSegments[0]
+ val folderId = pathSegments[2]
+
+ val query = if (folderId == "all") {
+ "gallery:$username"
+ } else {
+ "gallery:$username/$folderId"
+ }
+
+ val url = backendBuilder()
+ .addPathSegment("rss.xml")
+ .addQueryParameter("q", query)
+ .build()
+
+ return GET(url, headers)
+ }
+
+ override fun chapterListParse(response: Response): List {
+ val document = response.asJsoupXml()
+ val chapterList = parseToChapterList(document).toMutableList()
+ var nextUrl = document.selectFirst("[rel=next]")?.absUrl("href")
+
+ while (nextUrl != null) {
+ val newRequest = GET(nextUrl, headers)
+ val newResponse = client.newCall(newRequest).execute()
+ val newDocument = newResponse.asJsoupXml()
+ val newChapterList = parseToChapterList(newDocument)
+ chapterList.addAll(newChapterList)
+
+ nextUrl = newDocument.selectFirst("[rel=next]")?.absUrl("href")
+ }
+
+ return indexChapterList(chapterList.toList())
+ }
+
+ private fun parseToChapterList(document: Document): List {
+ val items = document.select("item")
+ return items.map {
+ val chapter = SChapter.create()
+ chapter.setUrlWithoutDomain(it.selectFirst("link")!!.text())
+ chapter.apply {
+ name = it.selectFirst("title")!!.text()
+ date_upload = parseDate(it.selectFirst("pubDate")?.text())
+ scanlator = it.selectFirst("media|credit")?.text()
+ }
+ }
+ }
+
+ private fun indexChapterList(chapterList: List): List {
+ // DeviantArt allows users to arrange galleries arbitrarily so we will
+ // primitively index the list by checking the first and last dates
+ return if (chapterList.first().date_upload > chapterList.last().date_upload) {
+ chapterList.mapIndexed { i, chapter ->
+ chapter.apply { chapter_number = chapterList.size - i.toFloat() }
+ }
+ } else {
+ chapterList.mapIndexed { i, chapter ->
+ chapter.apply { chapter_number = i.toFloat() + 1 }
+ }
+ }
+ }
+
+ override fun pageListParse(response: Response): List {
+ val document = response.asJsoup()
+ val imageUrl = document.selectFirst("img[fetchpriority=high]")?.absUrl("src")
+ return listOf(Page(0, imageUrl = imageUrl))
+ }
+
+ override fun imageUrlParse(response: Response): String {
+ throw UnsupportedOperationException()
+ }
+
+ private fun Response.asJsoupXml(): Document {
+ return Jsoup.parse(body.string(), request.url.toString(), Parser.xmlParser())
+ }
+
+ companion object {
+ const val SEARCH_FORMAT_MSG = "Please enter a query in the format of gallery:{username} or gallery:{username}/{folderId}"
+ }
+}
diff --git a/src/all/deviantart/src/eu/kanade/tachiyomi/extension/all/deviantart/DeviantArtUrlActivity.kt b/src/all/deviantart/src/eu/kanade/tachiyomi/extension/all/deviantart/DeviantArtUrlActivity.kt
new file mode 100644
index 000000000..9a4b0c625
--- /dev/null
+++ b/src/all/deviantart/src/eu/kanade/tachiyomi/extension/all/deviantart/DeviantArtUrlActivity.kt
@@ -0,0 +1,37 @@
+package eu.kanade.tachiyomi.extension.all.deviantart
+
+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 DeviantArtUrlActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val pathSegments = intent?.data?.pathSegments
+
+ if (pathSegments != null && pathSegments.size >= 3) {
+ val username = pathSegments[0]
+ val folderId = pathSegments[2]
+
+ val mainIntent = Intent().apply {
+ action = "eu.kanade.tachiyomi.SEARCH"
+ putExtra("query", "gallery:$username/$folderId")
+ putExtra("filter", packageName)
+ }
+
+ try {
+ startActivity(mainIntent)
+ } catch (e: ActivityNotFoundException) {
+ Log.e("DeviantArtUrlActivity", e.toString())
+ }
+ } else {
+ Log.e("DeviantArtUrlActivity", "Could not parse URI from intent $intent")
+ }
+
+ finish()
+ exitProcess(0)
+ }
+}