Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DeviantArt #6694

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/all/deviantart/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -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 android:autoVerify="true">
<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>
8 changes: 8 additions & 0 deletions src/all/deviantart/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
ext {
extName = 'DeviantArt'
extClass = '.DeviantArt'
extVersionCode = 1
isNsfw = true
}

apply from: "$rootDir/common.gradle"
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
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 kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
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_ERROR_MSG)
}

override fun popularMangaParse(response: Response): MangasPage {
throw UnsupportedOperationException(SEARCH_ERROR_MSG)
}

override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = backendBuilder()
.addPathSegment("rss.xml")
.addQueryParameter("q", query)
.build()

return GET(url, headers)
}

override fun searchMangaParse(response: Response): MangasPage {
val document = response.asJsoupXml()
requireNotNull(document.selectFirst("item")) { SEARCH_ERROR_MSG }

val (username, folderId) = response.request.url.queryParameter("q")!!.split(" ")
.find { it.startsWith("gallery:") }
?.substringAfter(":")
?.split("/")
?: throw IllegalArgumentException(SEARCH_ERROR_MSG)

val newRequest = GET("$baseUrl/$username/gallery/$folderId", headers)
val newResponse = client.newCall(newRequest).execute()
val manga = mangaDetailsParse(newResponse)

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 manga = SManga.create().apply {
// If manga is sub-gallery then use sub-gallery name, else use gallery name
title = document.selectFirst("._2vMZg + ._2vMZg")?.text()?.substringBeforeLast(" ")
?: document.selectFirst(".kyUNb")!!.ownText()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These selectors will break quite fast. Use multiple criteria if you need to, like:

div[class][aria-haspopup=listbox]

#sub-folder-gallery img[property=contentUrl]

author = document.title().substringBefore(" ")
description = document.selectFirst(".py0Gw._3urCH")?.wholeText()
thumbnail_url = document.selectFirst("._1xcj5._1QdgI img")!!.attr("src")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use absUrl, also, thumbnail_url is not critical so you should remove the non-null assert.

}
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 url = backendBuilder()
.addPathSegment("rss.xml")
.addQueryParameter("q", "gallery:$username/$folderId")
.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]")?.attr("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]")?.attr("href")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

absUrl

}

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()
Comment on lines +139 to +140
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

date_upload and scanlator are not critical, remove non-null assert.

}
}
}

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 pageListRequest(chapter: SChapter): Request {
val url = backendBuilder()
.addPathSegment("oembed")
.addQueryParameter("url", baseUrl + chapter.url)
.build()
return GET(url, headers)
}

override fun pageListParse(response: Response): List<Page> {
val imageUrl = Json.parseToJsonElement(response.body.string())
.jsonObject["url"]!!
.jsonPrimitive
.content
vetleledaal marked this conversation as resolved.
Show resolved Hide resolved
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_ERROR_MSG = "No results found. Is your query in the format of gallery:{username}/{folderId}?"
}
}
Original file line number Diff line number Diff line change
@@ -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