-
Notifications
You must be signed in to change notification settings - Fork 527
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
base: main
Are you sure you want to change the base?
Add DeviantArt #6694
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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> |
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" |
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() | ||
author = document.title().substringBefore(" ") | ||
description = document.selectFirst(".py0Gw._3urCH")?.wholeText() | ||
thumbnail_url = document.selectFirst("._1xcj5._1QdgI img")!!.attr("src") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||
} | ||
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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
} | ||
} | ||
|
||
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) | ||
} | ||
} |
There was a problem hiding this comment.
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: