feat(en): Add NyxScans (#7883)
* feat(en): Add NyxScans * naming * Always use next data for page parsing * Use functions from `utils`
This commit is contained in:
parent
5e4c156a27
commit
d5acdde1d9
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 6
|
||||
baseVersionCode = 7
|
||||
|
@ -97,16 +97,21 @@ class Chapter(
|
||||
private val createdAt: String,
|
||||
private val chapterStatus: String,
|
||||
private val isAccessible: Boolean,
|
||||
private val isLocked: Boolean? = false,
|
||||
private val isTimeLocked: Boolean? = false,
|
||||
private val mangaPost: ChapterPostDetails,
|
||||
) {
|
||||
fun isPublic() = chapterStatus == "PUBLIC"
|
||||
|
||||
fun isAccessible() = isAccessible
|
||||
|
||||
fun isLocked() = (isLocked == true) || (isTimeLocked == true)
|
||||
|
||||
fun toSChapter(mangaSlug: String?) = SChapter.create().apply {
|
||||
val prefix = if (isLocked()) "🔒 " else ""
|
||||
val seriesSlug = mangaSlug ?: mangaPost.slug
|
||||
url = "/series/$seriesSlug/$slug#$id"
|
||||
name = "Chapter $number"
|
||||
name = "${prefix}Chapter $number"
|
||||
scanlator = createdBy.name
|
||||
date_upload = try {
|
||||
dateFormat.parse(createdAt)!!.time
|
||||
|
@ -1,6 +1,10 @@
|
||||
package eu.kanade.tachiyomi.multisrc.iken
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
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
|
||||
@ -9,25 +13,26 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import keiyoushi.utils.parseAs
|
||||
import kotlinx.serialization.Serializable
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
abstract class Iken(
|
||||
override val name: String,
|
||||
override val lang: String,
|
||||
override val baseUrl: String,
|
||||
) : HttpSource() {
|
||||
) : HttpSource(), ConfigurableSource {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
private val json by injectLazy<Json>()
|
||||
private val preferences: SharedPreferences by getPreferencesLazy()
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.set("Referer", "$baseUrl/")
|
||||
@ -114,35 +119,76 @@ abstract class Iken(
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
val id = manga.url.substringAfterLast("#")
|
||||
val url = "$baseUrl/api/chapters?postId=$id&skip=0&take=1000&order=desc&userid="
|
||||
|
||||
return GET(url, headers)
|
||||
return GET("$baseUrl/series/${manga.url}", headers)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val data = response.parseAs<Post<ChapterListResponse>>()
|
||||
val userId = userIdRegex.find(response.body.string())?.groupValues?.get(1) ?: ""
|
||||
|
||||
val id = response.request.url.fragment!!
|
||||
val chapterUrl = "$baseUrl/api/chapters?postId=$id&skip=0&take=1000&order=desc&userid=$userId"
|
||||
val chapterResponse = client.newCall(GET(chapterUrl, headers)).execute()
|
||||
|
||||
val data = chapterResponse.parseAs<Post<ChapterListResponse>>()
|
||||
|
||||
assert(!data.post.isNovel) { "Novels are unsupported" }
|
||||
|
||||
return data.post.chapters
|
||||
.filter { it.isPublic() && it.isAccessible() }
|
||||
.filter { it.isPublic() && (it.isAccessible() || (preferences.getBoolean(showLockedChapterPrefKey, false) && it.isLocked())) }
|
||||
.map { it.toSChapter(data.post.slug) }
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val document = response.asJsoup()
|
||||
|
||||
return document.select("main section img").mapIndexed { idx, img ->
|
||||
Page(idx, imageUrl = img.absUrl("src"))
|
||||
if (document.selectFirst("svg.lucide-lock") != null) {
|
||||
throw Exception("Unlock chapter in webview")
|
||||
}
|
||||
|
||||
return document.getNextJson("images").parseAs<List<PageParseDto>>().mapIndexed { idx, p ->
|
||||
Page(idx, imageUrl = p.url)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class PageParseDto(
|
||||
val url: String,
|
||||
)
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = showLockedChapterPrefKey
|
||||
title = "Show locked chapters"
|
||||
setDefaultValue(false)
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response) =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T =
|
||||
json.decodeFromString(body.string())
|
||||
protected fun Document.getNextJson(key: String): String {
|
||||
val data = selectFirst("script:containsData($key)")
|
||||
?.data()
|
||||
?: throw Exception("Unable to retrieve NEXT data")
|
||||
|
||||
val keyIndex = data.indexOf(key)
|
||||
val start = data.indexOf('[', keyIndex)
|
||||
|
||||
var depth = 1
|
||||
var i = start + 1
|
||||
|
||||
while (i < data.length && depth > 0) {
|
||||
when (data[i]) {
|
||||
'[' -> depth++
|
||||
']' -> depth--
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
return "\"${data.substring(start, i)}\"".parseAs<String>()
|
||||
}
|
||||
}
|
||||
|
||||
private const val perPage = 18
|
||||
private const val showLockedChapterPrefKey = "pref_show_locked_chapters"
|
||||
private val userIdRegex = Regex(""""user\\":\{\\"id\\":\\"([^"']+)\\"""")
|
||||
|
@ -1,38 +1,9 @@
|
||||
package eu.kanade.tachiyomi.extension.en.arvenscans
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.iken.Iken
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class VortexScans : Iken(
|
||||
"Vortex Scans",
|
||||
"en",
|
||||
"https://vortexscans.org",
|
||||
) {
|
||||
|
||||
private val json by injectLazy<Json>()
|
||||
|
||||
private val regexImages = """\\"images\\":(.*?)\\"next""".toRegex()
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val images = document.selectFirst("script:containsData(images)")
|
||||
?.data()
|
||||
?.let { regexImages.find(it)!!.groupValues[1].trim(',') }
|
||||
?.let { json.decodeFromString<String>("\"$it\"") }
|
||||
?.let { json.parseToJsonElement(it).jsonArray }
|
||||
?: throw Exception("Unable to parse images")
|
||||
|
||||
return images.mapIndexed { idx, img ->
|
||||
Page(idx, imageUrl = img.jsonObject["url"]!!.jsonPrimitive.content)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -1,21 +1,12 @@
|
||||
package eu.kanade.tachiyomi.extension.en.infernalvoidscans
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.iken.Iken
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class HiveScans : Iken(
|
||||
"Hive Scans",
|
||||
"en",
|
||||
"https://hivetoon.com",
|
||||
) {
|
||||
|
||||
private val json by injectLazy<Json>()
|
||||
|
||||
override val versionId = 2
|
||||
|
||||
override val client = super.client.newBuilder()
|
||||
@ -28,20 +19,6 @@ class HiveScans : Iken(
|
||||
}
|
||||
.build()
|
||||
|
||||
private val pageRegex = Regex("""\\"images\\":(\[.*?]).*?nextChapter""")
|
||||
|
||||
@Serializable
|
||||
class PageDTO(
|
||||
val url: String,
|
||||
)
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val pageDataArray = pageRegex.find(response.body.string())?.destructured?.component1()?.replace("\\", "") ?: return listOf()
|
||||
return json.decodeFromString<List<PageDTO>>(pageDataArray).mapIndexed { idx, page ->
|
||||
Page(idx, imageUrl = page.url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.set("Cache-Control", "max-age=0")
|
||||
}
|
||||
|
10
src/en/nyxscans/build.gradle
Normal file
10
src/en/nyxscans/build.gradle
Normal file
@ -0,0 +1,10 @@
|
||||
ext {
|
||||
extName = 'Nyx Scans'
|
||||
extClass = '.NyxScans'
|
||||
themePkg = 'iken'
|
||||
baseUrl = 'https://nyxscans.com'
|
||||
overrideVersionCode = 0
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/en/nyxscans/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/en/nyxscans/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.1 KiB |
BIN
src/en/nyxscans/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/en/nyxscans/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
BIN
src/en/nyxscans/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/en/nyxscans/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.4 KiB |
BIN
src/en/nyxscans/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/en/nyxscans/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
BIN
src/en/nyxscans/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/en/nyxscans/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
@ -0,0 +1,39 @@
|
||||
package eu.kanade.tachiyomi.extension.en.nyxscans
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.iken.Iken
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import keiyoushi.utils.parseAs
|
||||
import kotlinx.serialization.Serializable
|
||||
import okhttp3.Response
|
||||
|
||||
class NyxScans : Iken(
|
||||
"Nyx Scans",
|
||||
"en",
|
||||
"https://nyxscans.com",
|
||||
) {
|
||||
// ============================== Popular ===============================
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val data = response.asJsoup().getNextJson("popularPosts")
|
||||
|
||||
val entries = data.parseAs<List<PopularParseDto>>().map { entry ->
|
||||
SManga.create().apply {
|
||||
title = entry.postTitle
|
||||
thumbnail_url = entry.featuredImage
|
||||
url = "${entry.slug}#${entry.id}"
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(entries, false)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class PopularParseDto(
|
||||
val id: Int,
|
||||
val slug: String,
|
||||
val postTitle: String,
|
||||
val featuredImage: String? = null,
|
||||
)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user