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) + } +}