Add DeviantArt (#6694)
* Add DeviantArt * Slight cleanup * Use .absUrl(), remove not-null asserts * Use less volatile selectors * Use better selector for gallery name * Remove not-null assert on subFolderGallery * Remove autoVerify from manifest * Remove unnecessary RSS request, simplify query parsing * Fetch HQ image * Account for gallery:{username} and gallery:{username}/all * Reword search fallback message * Allow parseDate() to accept null
This commit is contained in:
parent
0c399f549e
commit
9ead615784
|
@ -0,0 +1,24 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<application>
|
||||||
|
<activity
|
||||||
|
android:name=".all.deviantart.DeviantArtUrlActivity"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:theme="@android:style/Theme.NoDisplay">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
|
|
||||||
|
<data android:scheme="http"/>
|
||||||
|
<data android:scheme="https"/>
|
||||||
|
|
||||||
|
<data android:host="www.deviantart.com"/>
|
||||||
|
<data android:host="deviantart.com"/>
|
||||||
|
|
||||||
|
<data android:pathPattern="/..*/gallery/..*"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
|
@ -0,0 +1,8 @@
|
||||||
|
ext {
|
||||||
|
extName = 'DeviantArt'
|
||||||
|
extClass = '.DeviantArt'
|
||||||
|
extVersionCode = 1
|
||||||
|
isNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 5.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
|
@ -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<SChapter> {
|
||||||
|
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<SChapter> {
|
||||||
|
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<SChapter>): List<SChapter> {
|
||||||
|
// 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<Page> {
|
||||||
|
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}"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue