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:
AwkwardPeak7 2025-04-07 17:41:58 +05:00 committed by Draff
parent 8dcfac5ba8
commit d0ea9fadc6
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
18 changed files with 984 additions and 659 deletions

View File

@ -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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,8 @@
ext {
extName = 'Dynasty'
extClass = '.DynastyFactory'
extVersionCode = 25
extVersionCode = 26
isNsfw = true
}
apply from: "$rootDir/common.gradle"

View File

@ -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")

View File

@ -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,
)

View File

@ -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()
}

View File

@ -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
}
}

View File

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

View File

@ -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")
}
}

View File

@ -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),
)
}

View File

@ -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
}
}

View File

@ -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"

View File

@ -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()
}
}

View File

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

View File

@ -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
}
}

View File

@ -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()

View File

@ -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")