Rework the Dynasty Source (#8326)
* exact match * single search * single search fixed * rm * thumbnail fast * details * chapter details in browse * chapter details in browse * man * chapters and page * chapters and page * cleanup * small cleanup * enforce type filter manually * enforce type filter manually * reversed list or not * newlines in description adjustment * pairing filter * remove single fetching logic not worth it * add other dynasty factories for legacy compatibility * cleanup * fix status and header being null in some cases * update covers * status * unused * inline function * selector, reorder etc * lint * no empty types * author in chapter name * add InputStream parseAs utils function * Review Changes * dont include all authors prevent https://imgur.com/dJ9LI4z * Revert "add InputStream parseAs utils function" This reverts commit 1b6bdc45aa6cfcb1ee046924a8c1ba68ec35789a. * revert * use encodedPath for covers * more constants * update covers
This commit is contained in:
parent
8dcfac5ba8
commit
d0ea9fadc6
@ -14,22 +14,13 @@
|
||||
|
||||
<data android:host="*.dynasty-scans.com" />
|
||||
<data android:host="dynasty-scans.com" />
|
||||
<data android:scheme="https" />
|
||||
|
||||
<data
|
||||
android:pathPattern="/anthologies/..*"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:pathPattern="/chapters/..*"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:pathPattern="/doujins/..*"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:pathPattern="/issues/..*"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:pathPattern="/series/..*"
|
||||
android:scheme="https" />
|
||||
<data android:pathPattern="/anthologies/..*" />
|
||||
<data android:pathPattern="/chapters/..*" />
|
||||
<data android:pathPattern="/doujins/..*" />
|
||||
<data android:pathPattern="/issues/..*" />
|
||||
<data android:pathPattern="/series/..*" />
|
||||
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
1
src/en/dynasty/assets/covers.json
Normal file
1
src/en/dynasty/assets/covers.json
Normal file
File diff suppressed because one or more lines are too long
1
src/en/dynasty/assets/tags.json
Normal file
1
src/en/dynasty/assets/tags.json
Normal file
File diff suppressed because one or more lines are too long
@ -1,7 +1,8 @@
|
||||
ext {
|
||||
extName = 'Dynasty'
|
||||
extClass = '.DynastyFactory'
|
||||
extVersionCode = 25
|
||||
extVersionCode = 26
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
@ -0,0 +1,30 @@
|
||||
package eu.kanade.tachiyomi.extension.en.dynasty
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
const val SERIES_TYPE = "Series"
|
||||
const val CHAPTER_TYPE = "Chapter"
|
||||
const val ANTHOLOGY_TYPE = "Anthology"
|
||||
const val DOUJIN_TYPE = "Doujin"
|
||||
const val ISSUE_TYPE = "Issue"
|
||||
|
||||
const val SERIES_DIR = "series"
|
||||
const val CHAPTERS_DIR = "chapters"
|
||||
const val ANTHOLOGIES_DIR = "anthologies"
|
||||
const val DOUJINS_DIR = "doujins"
|
||||
const val ISSUES_DIR = "issues"
|
||||
|
||||
const val COVER_FETCH_HOST = "keiyoushi-chapter-cover"
|
||||
const val COVER_URL_FRAGMENT = "thumbnail"
|
||||
|
||||
val CHAPTER_SLUG_REGEX = Regex("""(.*?)_(ch[0-9_]+|volume_[0-9_\w]+)""")
|
||||
|
||||
val UNICODE_REGEX = Regex("\\\\u([0-9A-Fa-f]{4})")
|
||||
|
||||
const val AUTHORS_UPPER_LIMIT = 15
|
||||
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
|
||||
|
||||
const val CHAPTER_FETCH_LIMIT_PREF = "chapterFetchLimit"
|
||||
val CHAPTER_FETCH_LIMITS = arrayOf("2", "5", "10", "all")
|
@ -0,0 +1,133 @@
|
||||
package eu.kanade.tachiyomi.extension.en.dynasty
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.JsonTransformingSerializer
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
|
||||
@Serializable
|
||||
class BrowseResponse(
|
||||
val chapters: List<BrowseChapter>,
|
||||
@SerialName("current_page") private val currentPage: Int,
|
||||
@SerialName("total_pages") private val totalPages: Int,
|
||||
) {
|
||||
fun hasNextPage() = currentPage <= totalPages
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class BrowseChapter(
|
||||
val title: String,
|
||||
val permalink: String,
|
||||
val tags: List<BrowseTag>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class BrowseTag(
|
||||
val type: String,
|
||||
val name: String,
|
||||
val permalink: String,
|
||||
) {
|
||||
val directory get() = when (type) {
|
||||
SERIES_TYPE -> SERIES_DIR
|
||||
ANTHOLOGY_TYPE -> ANTHOLOGIES_DIR
|
||||
DOUJIN_TYPE -> DOUJINS_DIR
|
||||
ISSUE_TYPE -> ISSUES_DIR
|
||||
else -> throw Exception("Unsupported Type for directory: $type")
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class TagSuggest(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val type: String,
|
||||
)
|
||||
|
||||
class MangaEntry(
|
||||
private val title: String,
|
||||
val url: String,
|
||||
private val cover: String?,
|
||||
) {
|
||||
fun toSManga() = SManga.create().apply {
|
||||
url = this@MangaEntry.url
|
||||
title = this@MangaEntry.title
|
||||
thumbnail_url = cover
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return this.url == (other as MangaEntry?)?.url
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return this.url.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class MangaResponse(
|
||||
val name: String,
|
||||
val type: String,
|
||||
val tags: List<BrowseTag>,
|
||||
val cover: String?,
|
||||
val description: String?,
|
||||
val aliases: List<String>,
|
||||
@Serializable(with = ChapterItemListSerializer::class)
|
||||
val taggings: List<ChapterItem>,
|
||||
@SerialName("total_pages") val totalPages: Int = 0,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
sealed class ChapterItem
|
||||
|
||||
@Serializable
|
||||
@SerialName("header")
|
||||
class MangaChapterHeader(
|
||||
val header: String?,
|
||||
) : ChapterItem()
|
||||
|
||||
@Serializable
|
||||
@SerialName("chapter")
|
||||
class MangaChapter(
|
||||
val title: String,
|
||||
val permalink: String,
|
||||
@SerialName("released_on") val releasedOn: String,
|
||||
val tags: List<BrowseTag>,
|
||||
) : ChapterItem()
|
||||
|
||||
object ChapterItemListSerializer : JsonTransformingSerializer<List<ChapterItem>>(ListSerializer(ChapterItem.serializer())) {
|
||||
override fun transformDeserialize(element: JsonElement): JsonElement {
|
||||
return JsonArray(
|
||||
element.jsonArray.map { jsonElement ->
|
||||
val jsonObject = jsonElement.jsonObject
|
||||
when {
|
||||
"header" in jsonObject -> JsonObject(
|
||||
jsonObject + ("type" to JsonPrimitive("header")),
|
||||
)
|
||||
else -> JsonObject(
|
||||
jsonObject + ("type" to JsonPrimitive("chapter")),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ChapterResponse(
|
||||
val title: String,
|
||||
val tags: List<BrowseTag>,
|
||||
val pages: List<Page>,
|
||||
@SerialName("released_on") val releasedOn: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Page(
|
||||
val url: String,
|
||||
)
|
@ -0,0 +1,695 @@
|
||||
package eu.kanade.tachiyomi.extension.en.dynasty
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.util.LruCache
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
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.model.UpdateStrategy
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import keiyoushi.utils.firstInstance
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import keiyoushi.utils.parseAs
|
||||
import keiyoushi.utils.tryParse
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okio.use
|
||||
import org.jsoup.Jsoup
|
||||
import rx.Observable
|
||||
|
||||
open class Dynasty : HttpSource(), ConfigurableSource {
|
||||
|
||||
override val name = "Dynasty Scans"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val baseUrl = "https://dynasty-scans.com"
|
||||
|
||||
override val supportsLatest = false
|
||||
|
||||
private val preferences by getPreferencesLazy()
|
||||
|
||||
// Dynasty-Series
|
||||
override val id = 669095474988166464
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor(::fetchCoverUrlInterceptor)
|
||||
.addInterceptor(::coverInterceptor)
|
||||
.rateLimit(1, 2)
|
||||
.build()
|
||||
|
||||
private val coverClient = network.cloudflareClient
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/$CHAPTERS_DIR/added.json?page=$page", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val data = response.parseAs<BrowseResponse>()
|
||||
val entries = LinkedHashSet<MangaEntry>()
|
||||
|
||||
data.chapters.forEach { chapter ->
|
||||
var isSeries = false
|
||||
|
||||
chapter.tags.forEach { tag ->
|
||||
if (tag.type in listOf(SERIES_TYPE, ANTHOLOGY_TYPE, DOUJIN_TYPE, ISSUE_TYPE)) {
|
||||
MangaEntry(
|
||||
url = "/${tag.directory}/${tag.permalink}",
|
||||
title = tag.name,
|
||||
cover = getCoverUrl(tag.directory, tag.permalink),
|
||||
).also(entries::add)
|
||||
|
||||
// true if an associated series is found
|
||||
isSeries = isSeries || tag.type == SERIES_TYPE
|
||||
}
|
||||
}
|
||||
|
||||
// individual chapter if no linked series
|
||||
// mostly the case for uploaded doujins
|
||||
if (!isSeries) {
|
||||
MangaEntry(
|
||||
url = "/$CHAPTERS_DIR/${chapter.permalink}",
|
||||
title = chapter.title,
|
||||
cover = buildChapterCoverFetchUrl(chapter.permalink),
|
||||
).also(entries::add)
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(
|
||||
mangas = entries.map(MangaEntry::toSManga),
|
||||
hasNextPage = data.hasNextPage(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
if (query.startsWith("deeplink:")) {
|
||||
var (_, directory, permalink) = query.split(":", limit = 3)
|
||||
|
||||
if (directory == CHAPTERS_DIR) {
|
||||
val seriesPermalink = CHAPTER_SLUG_REGEX.find(permalink)?.groupValues?.get(1)
|
||||
|
||||
if (seriesPermalink != null) {
|
||||
directory = SERIES_DIR
|
||||
permalink = seriesPermalink
|
||||
}
|
||||
}
|
||||
|
||||
val entry = MangaEntry(
|
||||
url = "/$directory/$permalink",
|
||||
title = permalink.permalinkToTitle(),
|
||||
cover = getCoverUrl(directory, permalink),
|
||||
).toSManga()
|
||||
|
||||
return Observable.just(
|
||||
MangasPage(
|
||||
mangas = listOf(entry),
|
||||
hasNextPage = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return client.newCall(searchMangaRequest(page, query, filters))
|
||||
.asObservableSuccess()
|
||||
.map { searchMangaParse(it, filters) }
|
||||
}
|
||||
|
||||
override fun getFilterList(): FilterList {
|
||||
val tags = this::class.java
|
||||
.getResourceAsStream("/assets/tags.json")!!
|
||||
.bufferedReader().use { it.readText() }
|
||||
.parseAs<List<Tag>>()
|
||||
|
||||
return FilterList(
|
||||
SortFilter(),
|
||||
TypeFilter(),
|
||||
Filter.Header("Note: Sort and Type may not always work"),
|
||||
Filter.Separator(),
|
||||
TagFilter(tags),
|
||||
AuthorFilter(),
|
||||
ScanlatorFilter(),
|
||||
PairingFilter(),
|
||||
Filter.Header("Note: Author, Scanlator and Pairing filters require exact name. You can add multiple by comma (,) separation"),
|
||||
)
|
||||
}
|
||||
|
||||
// lazy because extension inspector doesn't have implementation
|
||||
private val lruCache by lazy { LruCache<String, Int>(15) }
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val typeFilter = filters.firstInstance<TypeFilter>()
|
||||
.also {
|
||||
if (it.checked.isEmpty()) {
|
||||
throw Exception("Select at least one type")
|
||||
}
|
||||
}
|
||||
|
||||
val authors = filters.firstInstance<AuthorFilter>().values.map { author ->
|
||||
lruCache[author]
|
||||
?: fetchTagId(author, "Author")
|
||||
?.also { lruCache.put(author, it) }
|
||||
?: throw Exception("Unknown Author: $author")
|
||||
}
|
||||
val scanlators = filters.firstInstance<ScanlatorFilter>().values.map { scanlator ->
|
||||
lruCache[scanlator]
|
||||
?: fetchTagId(scanlator, "Scanlator")
|
||||
?.also { lruCache.put(scanlator, it) }
|
||||
?: throw Exception("Unknown Scanlator: $scanlator")
|
||||
}
|
||||
val pairing = filters.firstInstance<PairingFilter>().values.map { pairing ->
|
||||
lruCache[pairing]
|
||||
?: fetchTagId(pairing, "Pairing")
|
||||
?.also { lruCache.put(pairing, it) }
|
||||
?: throw Exception("Unknown Pairing: $pairing")
|
||||
}
|
||||
|
||||
// series and doujin results are best when chapters are included as type so keep track of this
|
||||
var seriesSelected = false
|
||||
var doujinSelected = false
|
||||
var chapterSelected = false
|
||||
|
||||
val url = "$baseUrl/search".toHttpUrl().newBuilder().apply {
|
||||
addQueryParameter("q", query.trim())
|
||||
filters.firstInstance<SortFilter>().also {
|
||||
addQueryParameter("sort", it.sort)
|
||||
}
|
||||
typeFilter.also {
|
||||
it.checked.forEach { type ->
|
||||
seriesSelected = seriesSelected || type == SERIES_TYPE
|
||||
doujinSelected = doujinSelected || type == DOUJIN_TYPE
|
||||
chapterSelected = chapterSelected || type == CHAPTER_TYPE
|
||||
|
||||
addQueryParameter("classes[]", type)
|
||||
}
|
||||
}
|
||||
|
||||
// series and doujin results are best when chapters are included
|
||||
// they will be filtered client side in `searchMangaParse`
|
||||
if ((seriesSelected || doujinSelected) && !chapterSelected) {
|
||||
addQueryParameter("classes[]", CHAPTER_TYPE)
|
||||
}
|
||||
|
||||
filters.firstInstance<TagFilter>().also {
|
||||
it.included.forEach { with ->
|
||||
addQueryParameter("with[]", with.id.toString())
|
||||
}
|
||||
it.excluded.forEach { without ->
|
||||
addQueryParameter("without[]", without.id.toString())
|
||||
}
|
||||
}
|
||||
authors.forEach { author ->
|
||||
addQueryParameter("with[]", author.toString())
|
||||
}
|
||||
scanlators.forEach { scanlator ->
|
||||
addQueryParameter("with[]", scanlator.toString())
|
||||
}
|
||||
pairing.forEach { pairing ->
|
||||
addQueryParameter("with[]", pairing.toString())
|
||||
}
|
||||
if (page > 1) {
|
||||
addQueryParameter("page", page.toString())
|
||||
}
|
||||
}.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
private fun fetchTagId(query: String, type: String): Int? {
|
||||
val url = "$baseUrl/tags/suggest"
|
||||
val body = FormBody.Builder()
|
||||
.add("query", query)
|
||||
.build()
|
||||
|
||||
val data = client.newCall(POST(url, headers, body)).execute()
|
||||
.parseAs<List<TagSuggest>>()
|
||||
|
||||
return data.firstOrNull {
|
||||
it.type == type && it.name.trim().lowercase() == query
|
||||
}?.id
|
||||
}
|
||||
|
||||
private fun searchMangaParse(response: Response, filters: FilterList): MangasPage {
|
||||
val typeFilter = filters.firstInstance<TypeFilter>()
|
||||
val includedSeries = typeFilter.checked.contains(SERIES_TYPE)
|
||||
val includedChapters = typeFilter.checked.contains(CHAPTER_TYPE)
|
||||
val includedDoujins = typeFilter.checked.contains(DOUJIN_TYPE)
|
||||
|
||||
val document = response.asJsoup()
|
||||
val entries = LinkedHashSet<MangaEntry>()
|
||||
|
||||
// saves the first entry found
|
||||
// returned if everything was filtered out to avoid "No Results found" error
|
||||
var firstEntry: MangaEntry? = null
|
||||
|
||||
document.select(
|
||||
".chapter-list a.name[href~=/($SERIES_DIR|$ANTHOLOGIES_DIR|$CHAPTERS_DIR|$DOUJINS_DIR|$ISSUES_DIR)/], " +
|
||||
".chapter-list .doujin_tags a[href~=/$DOUJINS_DIR/]",
|
||||
).forEach { element ->
|
||||
var (directory, permalink) = element.absUrl("href")
|
||||
.toHttpUrl().pathSegments
|
||||
.let { it[0] to it[1] }
|
||||
var title = element.ownText()
|
||||
|
||||
if (directory == CHAPTERS_DIR) {
|
||||
val seriesPermalink = CHAPTER_SLUG_REGEX.find(permalink)?.groupValues?.get(1)
|
||||
|
||||
if (seriesPermalink != null) {
|
||||
directory = SERIES_DIR
|
||||
permalink = seriesPermalink
|
||||
title = seriesPermalink.permalinkToTitle()
|
||||
}
|
||||
}
|
||||
|
||||
val entry = MangaEntry(
|
||||
url = "/$directory/$permalink",
|
||||
title = title,
|
||||
cover = getCoverUrl(directory, permalink),
|
||||
)
|
||||
|
||||
if (firstEntry == null) {
|
||||
firstEntry = entry
|
||||
}
|
||||
|
||||
// since we convert chapters to their series counterpart, and select doujins from chapters
|
||||
// it is possible to get a certain type even if it is unselected from filters
|
||||
// so don't include in that case
|
||||
if ((!includedSeries && directory == SERIES_DIR) ||
|
||||
(!includedChapters && directory == CHAPTERS_DIR) ||
|
||||
(!includedDoujins && directory == DOUJINS_DIR)
|
||||
) {
|
||||
return@forEach
|
||||
}
|
||||
|
||||
entries.add(entry)
|
||||
}
|
||||
|
||||
// avoid "No Results found" error in case everything was filtered out from above check
|
||||
if (entries.isEmpty()) {
|
||||
firstEntry?.also { entries.add(it) }
|
||||
}
|
||||
|
||||
val hasNextPage = document.selectFirst(".pagination [rel=next]") != null
|
||||
|
||||
return MangasPage(
|
||||
mangas = entries.map(MangaEntry::toSManga),
|
||||
hasNextPage = hasNextPage,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getMangaUrl(manga: SManga): String {
|
||||
return baseUrl + manga.url
|
||||
}
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
val mangaPath = "$baseUrl${manga.url}".toHttpUrl().pathSegments
|
||||
|
||||
assert(
|
||||
mangaPath.size == 2 &&
|
||||
mangaPath[0] in listOf(SERIES_DIR, ANTHOLOGIES_DIR, DOUJINS_DIR, ISSUES_DIR, CHAPTERS_DIR),
|
||||
) { "Migrate to Dynasty Scans to update url" }
|
||||
|
||||
val (directory, permalink) = mangaPath.let { it[0] to it[1] }
|
||||
|
||||
val url = baseUrl.toHttpUrl().newBuilder()
|
||||
.addPathSegment(directory)
|
||||
.addPathSegment("$permalink.json")
|
||||
.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
if (response.request.url.pathSegments[0] == CHAPTERS_DIR) {
|
||||
return chapterDetailsParse(response)
|
||||
}
|
||||
|
||||
val data = response.parseAs<MangaResponse>()
|
||||
|
||||
val authors = LinkedHashSet<String>()
|
||||
val tags = LinkedHashSet<String>()
|
||||
val others = LinkedHashSet<Pair<String, String>>()
|
||||
val publishingStatus = LinkedHashSet<String>()
|
||||
|
||||
data.tags.forEach { tag ->
|
||||
when (tag.type) {
|
||||
"Author" -> authors.add(tag.name)
|
||||
"General" -> tags.add(tag.name)
|
||||
"Status" -> {
|
||||
publishingStatus.add(tag.name)
|
||||
others.add(tag.type to tag.name)
|
||||
}
|
||||
else -> others.add(tag.type to tag.name)
|
||||
}
|
||||
}
|
||||
|
||||
data.taggings.filterIsInstance<MangaChapter>().forEach { tagging ->
|
||||
tagging.tags.forEach { tag ->
|
||||
when (tag.type) {
|
||||
"Author" -> authors.add(tag.name)
|
||||
"General" -> tags.add(tag.name)
|
||||
SERIES_TYPE, DOUJIN_TYPE, ANTHOLOGY_TYPE, ISSUE_TYPE, "Scanlator" -> {}
|
||||
else -> others.add(tag.type to tag.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SManga.create().apply {
|
||||
title = data.name
|
||||
author = if (authors.size > AUTHORS_UPPER_LIMIT) {
|
||||
authors.take(AUTHORS_UPPER_LIMIT)
|
||||
.joinToString(postfix = "...")
|
||||
} else {
|
||||
authors.joinToString()
|
||||
}
|
||||
artist = author
|
||||
description = buildString {
|
||||
val prefChapterFetchLimit = preferences.chapterFetchLimit
|
||||
if (prefChapterFetchLimit < data.totalPages) {
|
||||
append("IMPORTANT: Only first $prefChapterFetchLimit pages of chapter list will be fetched. You can change this in extension settings.\n\n")
|
||||
}
|
||||
|
||||
data.description?.let {
|
||||
val desc = Jsoup.parseBodyFragment(
|
||||
decodeUnicode(it),
|
||||
)
|
||||
desc.select("a").remove()
|
||||
|
||||
append(desc.wholeText().trim())
|
||||
append("\n\n")
|
||||
}
|
||||
|
||||
append("Type: ", data.type, "\n\n")
|
||||
|
||||
if (authors.size > AUTHORS_UPPER_LIMIT) {
|
||||
others.addAll(authors.map { "Author" to it })
|
||||
}
|
||||
|
||||
for ((type, values) in others.groupBy { it.first }) {
|
||||
append(type, ":\n")
|
||||
values.forEach { append("• ", it.second, "\n") }
|
||||
append("\n")
|
||||
}
|
||||
if (data.aliases.isNotEmpty()) {
|
||||
append("Aliases:\n")
|
||||
data.aliases.forEach { append("• ", it, "\n") }
|
||||
append("\n")
|
||||
}
|
||||
}.trim()
|
||||
genre = tags.joinToString()
|
||||
status = when {
|
||||
publishingStatus.contains("Ongoing") -> SManga.ONGOING
|
||||
publishingStatus.contains("Completed") -> SManga.COMPLETED
|
||||
publishingStatus.contains("On Hiatus") -> SManga.ON_HIATUS
|
||||
publishingStatus.contains("Licensed") -> SManga.LICENSED
|
||||
listOf("Dropped", "Cancelled", "Not Updated", "Abandoned", "Removed")
|
||||
.any { publishingStatus.contains(it) } -> SManga.CANCELLED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
thumbnail_url = data.cover?.let { buildCoverUrl(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeUnicode(input: String): String {
|
||||
return UNICODE_REGEX.replace(input) { matchResult ->
|
||||
matchResult.groupValues[1]
|
||||
.toInt(16)
|
||||
.toChar()
|
||||
.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun chapterDetailsParse(response: Response): SManga {
|
||||
val data = response.parseAs<ChapterResponse>()
|
||||
|
||||
val authors = LinkedHashSet<String>()
|
||||
val tags = LinkedHashSet<String>()
|
||||
val others = LinkedHashSet<Pair<String, String>>()
|
||||
|
||||
data.tags.forEach { tag ->
|
||||
when (tag.type) {
|
||||
"Author" -> authors.add(tag.name)
|
||||
"General" -> tags.add(tag.name)
|
||||
else -> others.add(tag.type to tag.name)
|
||||
}
|
||||
}
|
||||
|
||||
return SManga.create().apply {
|
||||
title = data.title
|
||||
author = authors.joinToString()
|
||||
artist = author
|
||||
description = buildString {
|
||||
append("Type: ", CHAPTER_TYPE, "\n\n")
|
||||
for ((type, values) in others.groupBy { it.first }) {
|
||||
append(type, ":\n")
|
||||
values.forEach { append("• ", it.second, "\n") }
|
||||
append("\n")
|
||||
}
|
||||
append("Released: ", data.releasedOn)
|
||||
}.trim()
|
||||
genre = tags.joinToString()
|
||||
thumbnail_url = buildCoverUrl(data.pages.first().url)
|
||||
status = SManga.COMPLETED
|
||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return if (manga.url.contains("/$CHAPTERS_DIR/")) {
|
||||
Observable.just(
|
||||
listOf(
|
||||
SChapter.create().apply {
|
||||
url = manga.url
|
||||
name = "Chapter"
|
||||
date_upload = dateFormat.tryParse(
|
||||
manga.description
|
||||
?.substringAfter("Released:", ""),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
} else {
|
||||
super.fetchChapterList(manga)
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
return mangaDetailsRequest(manga)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val data = response.parseAs<MangaResponse>()
|
||||
val chapters = data.taggings.toMutableList()
|
||||
|
||||
var page = 2
|
||||
val limit = preferences.chapterFetchLimit
|
||||
|
||||
while (page <= data.totalPages && page <= limit) {
|
||||
val url = response.request.url.newBuilder()
|
||||
.addQueryParameter("page", page.toString())
|
||||
.build()
|
||||
|
||||
chapters += client.newCall(GET(url, headers)).execute()
|
||||
.parseAs<MangaResponse>().taggings
|
||||
page += 1
|
||||
}
|
||||
|
||||
var header: String? = null
|
||||
|
||||
val chapterList = mutableListOf<SChapter>()
|
||||
|
||||
chapters.forEach { item ->
|
||||
if (item is MangaChapterHeader) {
|
||||
header = item.header
|
||||
return@forEach
|
||||
}
|
||||
|
||||
with(item as MangaChapter) {
|
||||
var chapterName = header?.let { "$it $title" } ?: title
|
||||
if (data.type != SERIES_TYPE) {
|
||||
chapterName += tags.filter { it.type == "Author" }
|
||||
.joinToString(prefix = " by ", separator = " and ") { it.name }
|
||||
}
|
||||
SChapter.create().apply {
|
||||
url = "/$CHAPTERS_DIR/$permalink"
|
||||
name = chapterName
|
||||
scanlator = tags.filter { it.type == "Scanlator" }.joinToString { it.name }
|
||||
date_upload = dateFormat.tryParse(releasedOn)
|
||||
}.also(chapterList::add)
|
||||
}
|
||||
}
|
||||
|
||||
return if (data.type != DOUJIN_TYPE) {
|
||||
chapterList.asReversed()
|
||||
} else {
|
||||
chapterList
|
||||
}
|
||||
}
|
||||
|
||||
override fun getChapterUrl(chapter: SChapter): String {
|
||||
return baseUrl + chapter.url
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val chapterPath = "$baseUrl${chapter.url}".toHttpUrl().pathSegments
|
||||
|
||||
assert(
|
||||
chapterPath.size == 2 &&
|
||||
chapterPath[0] == CHAPTERS_DIR,
|
||||
) { "Refresh Chapter List" }
|
||||
|
||||
val permalink = chapterPath[1]
|
||||
|
||||
val url = baseUrl.toHttpUrl().newBuilder()
|
||||
.addPathSegment(CHAPTERS_DIR)
|
||||
.addPathSegment("$permalink.json")
|
||||
.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val data = response.parseAs<ChapterResponse>()
|
||||
|
||||
return data.pages.mapIndexed { index, page ->
|
||||
Page(index, imageUrl = baseUrl + page.url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = CHAPTER_FETCH_LIMIT_PREF
|
||||
title = "Chapters Fetch Limit"
|
||||
entries = CHAPTER_FETCH_LIMITS.map { "$it pages" }.toTypedArray()
|
||||
entryValues = CHAPTER_FETCH_LIMITS
|
||||
setDefaultValue(CHAPTER_FETCH_LIMITS[0])
|
||||
summary = """
|
||||
Limits how many pages of an entry are fetched for chapter list
|
||||
Mostly applies to Doujins
|
||||
|
||||
More pages mean slower loading of chapter list
|
||||
|
||||
Currently fetching %s
|
||||
""".trimIndent()
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
private val SharedPreferences.chapterFetchLimit: Int
|
||||
get() = getString(CHAPTER_FETCH_LIMIT_PREF, CHAPTER_FETCH_LIMITS[0])!!.let {
|
||||
if (it == "all") {
|
||||
Int.MAX_VALUE
|
||||
} else {
|
||||
it.toInt()
|
||||
}
|
||||
}
|
||||
|
||||
private val covers: Map<String, Map<String, String>> by lazy {
|
||||
this::class.java
|
||||
.getResourceAsStream("/assets/covers.json")!!
|
||||
.bufferedReader().use { it.readText() }
|
||||
.parseAs()
|
||||
}
|
||||
|
||||
private fun getCoverUrl(directory: String?, permalink: String): String? {
|
||||
directory ?: return null
|
||||
|
||||
if (directory == CHAPTERS_DIR) {
|
||||
return buildChapterCoverFetchUrl(permalink)
|
||||
}
|
||||
|
||||
val file = covers[directory]?.get(permalink)
|
||||
?: return null
|
||||
|
||||
return buildCoverUrl(file)
|
||||
}
|
||||
|
||||
private fun buildCoverUrl(file: String): String {
|
||||
val path = "$baseUrl$file".toHttpUrl()
|
||||
.encodedPath
|
||||
.removePrefix("/")
|
||||
|
||||
return baseUrl.toHttpUrl()
|
||||
.newBuilder()
|
||||
.addEncodedPathSegments(path)
|
||||
.fragment(COVER_URL_FRAGMENT)
|
||||
.build()
|
||||
.toString()
|
||||
}
|
||||
|
||||
private fun buildChapterCoverFetchUrl(permalink: String): String {
|
||||
return HttpUrl.Builder().apply {
|
||||
scheme("https")
|
||||
host(COVER_FETCH_HOST)
|
||||
addQueryParameter("permalink", permalink)
|
||||
}.build().toString()
|
||||
}
|
||||
|
||||
private fun fetchCoverUrlInterceptor(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
|
||||
if (request.url.host != COVER_FETCH_HOST) {
|
||||
return chain.proceed(request)
|
||||
}
|
||||
|
||||
val permalink = request.url.queryParameter("permalink")!!
|
||||
|
||||
val chapterUrl = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment(CHAPTERS_DIR)
|
||||
addPathSegments("$permalink.json")
|
||||
}.build()
|
||||
|
||||
val page = client.newCall(GET(chapterUrl, headers)).execute()
|
||||
.parseAs<ChapterResponse>()
|
||||
.pages.first()
|
||||
|
||||
val url = buildCoverUrl(page.url)
|
||||
|
||||
val newRequest = request.newBuilder()
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
return chain.proceed(newRequest)
|
||||
}
|
||||
|
||||
private fun coverInterceptor(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
|
||||
return if (request.url.fragment == COVER_URL_FRAGMENT) {
|
||||
coverClient.newCall(request).execute()
|
||||
} else {
|
||||
chain.proceed(request)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.permalinkToTitle(): String {
|
||||
return split('_')
|
||||
.joinToString(" ") { word ->
|
||||
word.replaceFirstChar { it.uppercase() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response) =
|
||||
throw UnsupportedOperationException()
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
throw UnsupportedOperationException()
|
||||
override fun latestUpdatesParse(response: Response) =
|
||||
throw UnsupportedOperationException()
|
||||
override fun searchMangaParse(response: Response) =
|
||||
throw UnsupportedOperationException()
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.en.dynasty
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
|
||||
class DynastyAnthologies : DynastyScans() {
|
||||
|
||||
override val name = "Dynasty-Anthologies"
|
||||
|
||||
override val searchPrefix = "anthologies"
|
||||
|
||||
override val categoryPrefix = "Anthology"
|
||||
|
||||
override fun popularMangaInitialUrl() = ""
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
return GET("$baseUrl/search?q=$query&classes%5B%5D=Anthology&sort=&page=$page", headers)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val manga = SManga.create()
|
||||
manga.thumbnail_url = baseUrl + document.select("div.span2 > img").attr("src")
|
||||
parseHeader(document, manga)
|
||||
parseGenres(document, manga)
|
||||
parseDescription(document, manga)
|
||||
return manga
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.en.dynasty
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
class DynastyChapters : DynastyScans() {
|
||||
override val name = "Dynasty-Chapters"
|
||||
override val searchPrefix = "chapters"
|
||||
override val categoryPrefix = "Chapter"
|
||||
override fun popularMangaInitialUrl() = ""
|
||||
|
||||
private fun latestUpdatesInitialUrl(page: Int) = "$baseUrl/search?q=&classes%5B%5D=Chapter&page=$page=$&sort=created_at"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
return GET("$baseUrl/search?q=$query&classes%5B%5D=Chapter&sort=&page=$page", headers)
|
||||
}
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val manga = SManga.create()
|
||||
|
||||
manga.thumbnail_url = document.select("img").last()!!.absUrl("src")
|
||||
manga.title = document.select("h3 b").text()
|
||||
manga.status = SManga.COMPLETED
|
||||
val artistAuthorElements = document.select("a[href*=author]")
|
||||
if (!artistAuthorElements.isEmpty()) {
|
||||
if (artistAuthorElements.size == 1) {
|
||||
manga.author = artistAuthorElements[0].text()
|
||||
} else {
|
||||
manga.artist = artistAuthorElements[0].text()
|
||||
manga.author = artistAuthorElements[1].text()
|
||||
}
|
||||
}
|
||||
|
||||
val genreElements = document.select(".tags a")
|
||||
val doujinElements = document.select("a[href*=doujins]")
|
||||
genreElements.addAll(doujinElements)
|
||||
parseGenres(genreElements, manga)
|
||||
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "dd"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
val titleSelect = element.select("a.name")
|
||||
manga.title = titleSelect.text()
|
||||
manga.setUrlWithoutDomain(titleSelect.attr("href"))
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
|
||||
return document.select(chapterListSelector()).map {
|
||||
chapterFromElement(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = ".chapters.show#main"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(element.baseUri())
|
||||
chapter.name = element.select("h3").text()
|
||||
chapter.date_upload = element.select("span.released").firstOrNull()?.text().toDate("MMM dd, yyyy")
|
||||
return chapter
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET(latestUpdatesInitialUrl(page), headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = searchMangaNextPageSelector()
|
||||
|
||||
override fun latestUpdatesSelector() = searchMangaSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = searchMangaFromElement(element)
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||
}
|
@ -1,119 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.en.dynasty
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
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.util.asJsoup
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
class DynastyDoujins : DynastyScans() {
|
||||
|
||||
override val name = "Dynasty-Doujins"
|
||||
|
||||
override val searchPrefix = "doujins"
|
||||
|
||||
override val categoryPrefix = "Doujin"
|
||||
override fun popularMangaInitialUrl() = ""
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
return super.popularMangaFromElement(element).apply {
|
||||
thumbnail_url = element.select("img").attr("abs:src").let {
|
||||
if (it.contains("cover_missing")) {
|
||||
null
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
return GET("$baseUrl/search?q=$query&classes%5B%5D=Doujin&sort=&page=$page", headers)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val manga = SManga.create().apply {
|
||||
title = document.selectFirst("div#main > h2 > b")!!.text().substringAfter("Doujins › ")
|
||||
description = document.select("div#main > div.description").text()
|
||||
thumbnail_url = document.select("a.thumbnail img").firstOrNull()?.attr("abs:src")
|
||||
?.replace("/thumb/", "/medium/")
|
||||
}
|
||||
parseGenres(document, manga)
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div#main > dl.chapter-list > dd"
|
||||
|
||||
private fun doujinChapterParse(document: Document): List<SChapter> {
|
||||
return try {
|
||||
document.select(chapterListSelector()).map { chapterFromElement(it) }
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
val chapters = mutableListOf<SChapter>()
|
||||
var page = 1
|
||||
|
||||
document.select("a.thumbnail img").let { images ->
|
||||
if (images.isNotEmpty()) {
|
||||
chapters.add(
|
||||
SChapter.create().apply {
|
||||
name = "Images"
|
||||
setUrlWithoutDomain(document.location() + "/images")
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
chapters.addAll(doujinChapterParse(document))
|
||||
|
||||
var hasNextPage = popularMangaNextPageSelector().let { selector ->
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
|
||||
while (hasNextPage) {
|
||||
page += 1
|
||||
val doujinURL = document.location() + "?page=$page"
|
||||
|
||||
val newRequest = GET(doujinURL, headers)
|
||||
val newResponse = client.newCall(newRequest).execute()
|
||||
|
||||
if (!newResponse.isSuccessful) {
|
||||
/*
|
||||
TODO: Toast to notify chapter parsing aborted.
|
||||
Add possible retry logic.
|
||||
*/
|
||||
return chapters
|
||||
}
|
||||
|
||||
val newDocument = newResponse.asJsoup()
|
||||
chapters.addAll(doujinChapterParse(newDocument))
|
||||
|
||||
hasNextPage = popularMangaNextPageSelector().let { selector ->
|
||||
newDocument.select(selector).first()
|
||||
} != null
|
||||
}
|
||||
return chapters
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return if (document.location().endsWith("/images")) {
|
||||
document.select("a.thumbnail").mapIndexed { i, element ->
|
||||
Page(i, element.attr("abs:href"))
|
||||
}
|
||||
} else {
|
||||
super.pageListParse(document)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String {
|
||||
return document.select("div.image img").attr("abs:src")
|
||||
}
|
||||
}
|
@ -1,18 +1,13 @@
|
||||
package eu.kanade.tachiyomi.extension.en.dynasty
|
||||
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
class DynastyFactory : SourceFactory {
|
||||
override fun createSources(): List<Source> = getAllDynasty()
|
||||
}
|
||||
|
||||
fun getAllDynasty() =
|
||||
listOf(
|
||||
DynastyAnthologies(),
|
||||
DynastyChapters(),
|
||||
DynastyDoujins(),
|
||||
DynastyIssues(),
|
||||
DynastySeries(),
|
||||
DynastyScanlator(),
|
||||
override fun createSources() = listOf(
|
||||
Dynasty(),
|
||||
DynastyLegacy("Dynasty-Anthologies (Deprecated)", 738706855355689486),
|
||||
DynastyLegacy("Dynasty-Chapters (Deprecated)", 4399127807078496448),
|
||||
DynastyLegacy("Dynasty-Doujins (Deprecated)", 6243685045159195166),
|
||||
DynastyLegacy("Dynasty-Issues (Deprecated)", 2548005429321146934),
|
||||
)
|
||||
}
|
||||
|
@ -1,31 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.en.dynasty
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
|
||||
class DynastyIssues : DynastyScans() {
|
||||
|
||||
override val name = "Dynasty-Issues"
|
||||
|
||||
override val searchPrefix = "issues"
|
||||
|
||||
override val categoryPrefix = "Issue"
|
||||
|
||||
override fun popularMangaInitialUrl() = ""
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
return GET("$baseUrl/search?q=$query&classes%5B%5D=Issue&sort=&page=$page", headers)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val manga = SManga.create()
|
||||
manga.thumbnail_url = baseUrl + document.select("div.span2 > img").attr("src")
|
||||
parseHeader(document, manga)
|
||||
parseGenres(document, manga)
|
||||
parseDescription(document, manga)
|
||||
return manga
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package eu.kanade.tachiyomi.extension.en.dynasty
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import rx.Observable
|
||||
|
||||
class DynastyLegacy(
|
||||
override val name: String,
|
||||
override val id: Long,
|
||||
) : Dynasty() {
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
throw Exception(LEGACY_DYNASTY_ERROR)
|
||||
}
|
||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||
throw Exception(LEGACY_DYNASTY_ERROR)
|
||||
}
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
throw Exception(LEGACY_DYNASTY_ERROR)
|
||||
}
|
||||
override fun getFilterList(): FilterList {
|
||||
return FilterList()
|
||||
}
|
||||
}
|
||||
|
||||
private const val LEGACY_DYNASTY_ERROR = "Use the `Dynasty Scans` source instead"
|
@ -1,44 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.en.dynasty
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
class DynastyScanlator : DynastyScans() {
|
||||
override val name = "Dynasty-Scanlator"
|
||||
override val searchPrefix = "scanlators"
|
||||
override val categoryPrefix = "Scanlator"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
return GET(
|
||||
"$baseUrl/search?q=$query&classes%5B%5D=$categoryPrefix&page=$page&sort=",
|
||||
headers,
|
||||
)
|
||||
}
|
||||
|
||||
override fun popularMangaInitialUrl() = ""
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
manga.setUrlWithoutDomain(element.select("a").attr("href"))
|
||||
manga.title = element.select("div.caption").text()
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val manga = SManga.create()
|
||||
parseHeader(document, manga)
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "dl.chapter-list > dd"
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
return super.chapterListParse(response).reversed()
|
||||
}
|
||||
}
|
@ -1,269 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.en.dynasty
|
||||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
||||
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.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.nodes.Node
|
||||
import org.jsoup.nodes.TextNode
|
||||
import org.jsoup.select.Elements
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.ArrayList
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
abstract class DynastyScans : ParsedHttpSource() {
|
||||
|
||||
override val baseUrl = "https://dynasty-scans.com"
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.rateLimitHost(baseUrl.toHttpUrl(), 1, 1, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
abstract fun popularMangaInitialUrl(): String
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = false
|
||||
|
||||
open val searchPrefix = ""
|
||||
|
||||
open val categoryPrefix = ""
|
||||
|
||||
private var parent: List<Node> = ArrayList()
|
||||
|
||||
private var list = InternalList(ArrayList(), "")
|
||||
|
||||
private var imgList = InternalList(ArrayList(), "")
|
||||
|
||||
private var _valid: Validate = Validate(false, -1)
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
protected fun popularMangaInitialUrl(page: Int) = "$baseUrl/search?q=&classes%5B%5D=$categoryPrefix&page=$page=$&sort="
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET(popularMangaInitialUrl(page), headers)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = searchMangaSelector()
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
manga.setUrlWithoutDomain(element.select("a").attr("href"))
|
||||
manga.title = element.select("div.caption").text()
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
if (query.startsWith("manga:")) {
|
||||
return if (query.startsWith("manga:$searchPrefix:")) {
|
||||
val newQuery = query.removePrefix("manga:$searchPrefix:")
|
||||
client.newCall(GET("$baseUrl/$searchPrefix/$newQuery"))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
val details = mangaDetailsParse(response)
|
||||
details.url = "/$searchPrefix/$newQuery"
|
||||
MangasPage(listOf(details), false)
|
||||
}
|
||||
} else {
|
||||
return Observable.just(MangasPage(ArrayList<SManga>(0), false))
|
||||
}
|
||||
}
|
||||
return super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "a.name"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
manga.setUrlWithoutDomain(element.attr("href"))
|
||||
manga.title = element.text()
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = "div.pagination > ul > li.active + li > a"
|
||||
|
||||
private fun buildListfromResponse(): List<Node> {
|
||||
return client.newCall(
|
||||
Request.Builder().headers(headers)
|
||||
.url(popularMangaInitialUrl()).build(),
|
||||
).execute().asJsoup()
|
||||
.select("div#main").first { it.hasText() }.childNodes()
|
||||
}
|
||||
|
||||
protected fun parseHeader(document: Document, manga: SManga): Boolean {
|
||||
manga.title = document.selectFirst("div.tags > h2.tag-title > b")!!.text()
|
||||
val elements = document.selectFirst("div.tags > h2.tag-title")!!.getElementsByTag("a")
|
||||
if (elements.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
if (elements.lastIndex == 0) {
|
||||
manga.author = elements[0].text()
|
||||
} else {
|
||||
manga.artist = elements[0].text()
|
||||
manga.author = elements[1].text()
|
||||
}
|
||||
manga.status = document.select("div.tags > h2.tag-title > small").text().let {
|
||||
when {
|
||||
it.contains("Ongoing") -> SManga.ONGOING
|
||||
it.contains("Completed") -> SManga.COMPLETED
|
||||
it.contains("Licensed") -> SManga.LICENSED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
protected fun parseGenres(document: Document, manga: SManga, select: String = "div.tags > div.tag-tags a") {
|
||||
val tagElements = document.select(select)
|
||||
val doujinElements = document.select("div.tags > h2.tag-title > small > a[href*=doujins]")
|
||||
tagElements.addAll(doujinElements)
|
||||
parseGenres(tagElements, manga)
|
||||
}
|
||||
|
||||
protected fun parseGenres(elements: Elements, manga: SManga) {
|
||||
if (!elements.isEmpty()) {
|
||||
val genres = mutableListOf<String>()
|
||||
elements.forEach {
|
||||
genres.add(it.text())
|
||||
}
|
||||
manga.genre = genres.joinToString(", ")
|
||||
}
|
||||
}
|
||||
|
||||
protected fun parseDescription(document: Document, manga: SManga) {
|
||||
manga.description = document.select("div.tags > div.row div.description").text()
|
||||
}
|
||||
|
||||
private fun getValid(manga: SManga): Validate {
|
||||
if (parent.isEmpty()) parent = buildListfromResponse()
|
||||
if (list.isEmpty()) list = InternalList(parent, "href")
|
||||
if (imgList.isEmpty()) imgList = InternalList(parent, "src")
|
||||
val pos = list.indexOf(manga.url.substringBeforeLast("/") + "/" + Uri.encode(manga.url.substringAfterLast("/")))
|
||||
return Validate((pos > -1), pos)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val manga = SManga.create()
|
||||
_valid = getValid(manga)
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div.span10 > dl.chapter-list > dd"
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
return super.chapterListParse(response).asReversed()
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val chapter = SChapter.create()
|
||||
val nodes = InternalList(element.childNodes(), "text")
|
||||
|
||||
chapter.setUrlWithoutDomain(element.select("a.name").attr("href"))
|
||||
chapter.name = nodes[0]
|
||||
if (nodes.contains(" by ")) {
|
||||
chapter.name += " by ${nodes[nodes.indexOfPartial(" by ") + 1]}"
|
||||
if (nodes.contains(" and ")) {
|
||||
chapter.name += " and ${nodes[nodes.indexOfPartial(" and ") + 1]}"
|
||||
}
|
||||
}
|
||||
chapter.date_upload = nodes[nodes.indexOfPartial("released")]
|
||||
.substringAfter("released ")
|
||||
.replace("\'", "")
|
||||
.toDate("MMM dd yy")
|
||||
return chapter
|
||||
}
|
||||
|
||||
protected fun String?.toDate(pattern: String): Long {
|
||||
this ?: return 0
|
||||
return try {
|
||||
SimpleDateFormat(pattern, Locale.ENGLISH).parse(this)?.time ?: 0
|
||||
} catch (_: Exception) {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return try {
|
||||
val imageUrl = document.select("script").last()!!.html().substringAfter("var pages = [").substringBefore("];")
|
||||
|
||||
json.parseToJsonElement("[$imageUrl]").jsonArray.mapIndexed { index, it ->
|
||||
Page(index, imageUrl = "$baseUrl${it.jsonObject["image"]!!.jsonPrimitive.content}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
class InternalList(nodes: List<Node>, type: String) : ArrayList<String>() {
|
||||
|
||||
init {
|
||||
if (type == "text") {
|
||||
for (node in nodes) {
|
||||
if (node is TextNode) {
|
||||
if (node.text() != " " && !node.text().contains("\n")) {
|
||||
this.add(node.text())
|
||||
}
|
||||
} else if (node is Element) this.add(node.text())
|
||||
}
|
||||
}
|
||||
if (type == "src") {
|
||||
nodes
|
||||
.filter { it is Element && it.hasClass("thumbnails") }
|
||||
.flatMap { it.childNodes() }
|
||||
.filterIsInstance<Element>()
|
||||
.filter { it.hasClass("span2") }
|
||||
.forEach { this.add(it.child(0).child(0).attr(type)) }
|
||||
}
|
||||
if (type == "href") {
|
||||
nodes
|
||||
.filter { it is Element && it.hasClass("thumbnails") }
|
||||
.flatMap { it.childNodes() }
|
||||
.filterIsInstance<Element>()
|
||||
.filter { it.hasClass("span2") }
|
||||
.forEach { this.add(it.child(0).attr(type)) }
|
||||
}
|
||||
}
|
||||
|
||||
fun indexOfPartial(partial: String): Int {
|
||||
return (0..this.lastIndex).firstOrNull { this[it].contains(partial) }
|
||||
?: -1
|
||||
}
|
||||
}
|
||||
|
||||
data class Validate(val _isManga: Boolean, val _pos: Int)
|
||||
|
||||
override fun popularMangaNextPageSelector() = searchMangaNextPageSelector()
|
||||
override fun latestUpdatesSelector() = ""
|
||||
override fun latestUpdatesNextPageSelector() = ""
|
||||
override fun imageUrlParse(document: Document): String = ""
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return popularMangaRequest(page)
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.en.dynasty
|
||||
|
||||
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.SManga
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import rx.Observable
|
||||
|
||||
class DynastySeries : DynastyScans() {
|
||||
|
||||
override val name = "Dynasty-Series"
|
||||
|
||||
override val searchPrefix = "series"
|
||||
|
||||
override val categoryPrefix = "Series"
|
||||
|
||||
override fun popularMangaInitialUrl() = ""
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
return GET("$baseUrl/search?q=$query&classes%5B%5D=Series&sort=&page=$page", headers)
|
||||
}
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
if (query.startsWith("manga:chapters:")) {
|
||||
val seriesName = Regex("""manga:chapters:(.*?)_ch[0-9_]+""").matchEntire(query)?.groups?.get(1)?.value
|
||||
if (seriesName != null) {
|
||||
return super.fetchSearchManga(page, "manga:$searchPrefix:$seriesName", filters)
|
||||
}
|
||||
}
|
||||
return super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val manga = SManga.create()
|
||||
manga.thumbnail_url = baseUrl + document.select("div.span2 > img").attr("src")
|
||||
parseHeader(document, manga)
|
||||
parseGenres(document, manga)
|
||||
parseDescription(document, manga)
|
||||
return manga
|
||||
}
|
||||
}
|
@ -8,24 +8,27 @@ import android.util.Log
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class DynastyUrlActivity : Activity() {
|
||||
private val name = javaClass.getSimpleName()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val pathSegments = intent?.data?.pathSegments
|
||||
if (pathSegments != null && pathSegments.size > 1) {
|
||||
val id = pathSegments[1]
|
||||
val directory = pathSegments[0]
|
||||
val permalink = pathSegments[1]
|
||||
val mainIntent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", "manga:${pathSegments[0]}:$id")
|
||||
putExtra("query", "deeplink:$directory:$permalink")
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
||||
try {
|
||||
startActivity(mainIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e("DynastyUrlActivity", e.toString())
|
||||
Log.e(name, "Activity Not Found", e)
|
||||
}
|
||||
} else {
|
||||
Log.e("DynastyUrlActivity", "could not parse uri from intent $intent")
|
||||
Log.e(name, "could not parse uri from intent $intent")
|
||||
}
|
||||
|
||||
finish()
|
||||
|
@ -0,0 +1,77 @@
|
||||
package eu.kanade.tachiyomi.extension.en.dynasty
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
class SortFilter : Filter.Select<String>(
|
||||
name = "Sort",
|
||||
values = selectOptions.map { it.first }.toTypedArray(),
|
||||
state = 3,
|
||||
) {
|
||||
val sort get() = selectOptions[state].second
|
||||
}
|
||||
|
||||
private val selectOptions = listOf(
|
||||
"Best Match" to "",
|
||||
"Alphabetical" to "name",
|
||||
"Date Added" to "created_at",
|
||||
"Release Date" to "released_on",
|
||||
)
|
||||
|
||||
class TypeOption(name: String) : Filter.CheckBox(name, true)
|
||||
|
||||
class TypeFilter : Filter.Group<TypeOption>(
|
||||
name = "Type",
|
||||
state = typeOptions.map { TypeOption(it) },
|
||||
) {
|
||||
val checked get() = state.filter { it.state }.map { it.name }
|
||||
}
|
||||
|
||||
private val typeOptions = listOf(
|
||||
SERIES_TYPE,
|
||||
CHAPTER_TYPE,
|
||||
ANTHOLOGY_TYPE,
|
||||
DOUJIN_TYPE,
|
||||
ISSUE_TYPE,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Tag(
|
||||
private val id: Int,
|
||||
private val name: String,
|
||||
private val permalink: String,
|
||||
) {
|
||||
val checkBoxOption get() = TagCheckBox(id, name, permalink)
|
||||
}
|
||||
|
||||
class TagCheckBox(
|
||||
val id: Int,
|
||||
name: String,
|
||||
val permalink: String,
|
||||
) : Filter.TriState(name)
|
||||
|
||||
class TagFilter(
|
||||
tags: List<Tag>,
|
||||
) : Filter.Group<TagCheckBox>(
|
||||
name = "Tags",
|
||||
state = tags.map(Tag::checkBoxOption),
|
||||
) {
|
||||
val included get() = state.filter { it.isIncluded() }
|
||||
val excluded get() = state.filter { it.isExcluded() }
|
||||
|
||||
fun isEmpty() = included.isEmpty() && excluded.isEmpty()
|
||||
}
|
||||
|
||||
abstract class TextFilter(name: String) : Filter.Text(name) {
|
||||
val values get() = state
|
||||
.split(",")
|
||||
.filterNot(String::isBlank)
|
||||
.map(String::trim)
|
||||
.map(String::lowercase)
|
||||
}
|
||||
|
||||
class AuthorFilter : TextFilter("Author")
|
||||
|
||||
class ScanlatorFilter : TextFilter("Scanlator")
|
||||
|
||||
class PairingFilter : TextFilter("Pairing")
|
Loading…
x
Reference in New Issue
Block a user