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:host="dynasty-scans.com" />
|
<data android:host="dynasty-scans.com" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
|
||||||
<data
|
<data android:pathPattern="/anthologies/..*" />
|
||||||
android:pathPattern="/anthologies/..*"
|
<data android:pathPattern="/chapters/..*" />
|
||||||
android:scheme="https" />
|
<data android:pathPattern="/doujins/..*" />
|
||||||
<data
|
<data android:pathPattern="/issues/..*" />
|
||||||
android:pathPattern="/chapters/..*"
|
<data android:pathPattern="/series/..*" />
|
||||||
android:scheme="https" />
|
|
||||||
<data
|
|
||||||
android:pathPattern="/doujins/..*"
|
|
||||||
android:scheme="https" />
|
|
||||||
<data
|
|
||||||
android:pathPattern="/issues/..*"
|
|
||||||
android:scheme="https" />
|
|
||||||
<data
|
|
||||||
android:pathPattern="/series/..*"
|
|
||||||
android:scheme="https" />
|
|
||||||
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</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 {
|
ext {
|
||||||
extName = 'Dynasty'
|
extName = 'Dynasty'
|
||||||
extClass = '.DynastyFactory'
|
extClass = '.DynastyFactory'
|
||||||
extVersionCode = 25
|
extVersionCode = 26
|
||||||
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
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
|
package eu.kanade.tachiyomi.extension.en.dynasty
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
|
|
||||||
class DynastyFactory : SourceFactory {
|
class DynastyFactory : SourceFactory {
|
||||||
override fun createSources(): List<Source> = getAllDynasty()
|
override fun createSources() = listOf(
|
||||||
}
|
Dynasty(),
|
||||||
|
DynastyLegacy("Dynasty-Anthologies (Deprecated)", 738706855355689486),
|
||||||
fun getAllDynasty() =
|
DynastyLegacy("Dynasty-Chapters (Deprecated)", 4399127807078496448),
|
||||||
listOf(
|
DynastyLegacy("Dynasty-Doujins (Deprecated)", 6243685045159195166),
|
||||||
DynastyAnthologies(),
|
DynastyLegacy("Dynasty-Issues (Deprecated)", 2548005429321146934),
|
||||||
DynastyChapters(),
|
|
||||||
DynastyDoujins(),
|
|
||||||
DynastyIssues(),
|
|
||||||
DynastySeries(),
|
|
||||||
DynastyScanlator(),
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
@ -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
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
class DynastyUrlActivity : Activity() {
|
class DynastyUrlActivity : Activity() {
|
||||||
|
private val name = javaClass.getSimpleName()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
val pathSegments = intent?.data?.pathSegments
|
val pathSegments = intent?.data?.pathSegments
|
||||||
if (pathSegments != null && pathSegments.size > 1) {
|
if (pathSegments != null && pathSegments.size > 1) {
|
||||||
val id = pathSegments[1]
|
val directory = pathSegments[0]
|
||||||
|
val permalink = pathSegments[1]
|
||||||
val mainIntent = Intent().apply {
|
val mainIntent = Intent().apply {
|
||||||
action = "eu.kanade.tachiyomi.SEARCH"
|
action = "eu.kanade.tachiyomi.SEARCH"
|
||||||
putExtra("query", "manga:${pathSegments[0]}:$id")
|
putExtra("query", "deeplink:$directory:$permalink")
|
||||||
putExtra("filter", packageName)
|
putExtra("filter", packageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
startActivity(mainIntent)
|
startActivity(mainIntent)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Log.e("DynastyUrlActivity", e.toString())
|
Log.e(name, "Activity Not Found", e)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.e("DynastyUrlActivity", "could not parse uri from intent $intent")
|
Log.e(name, "could not parse uri from intent $intent")
|
||||||
}
|
}
|
||||||
|
|
||||||
finish()
|
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