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:
Secozzi 2025-03-05 13:39:10 +01:00 committed by Draff
parent 5e4c156a27
commit d5acdde1d9
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
12 changed files with 118 additions and 70 deletions

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 6
baseVersionCode = 7

View File

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

View File

@ -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\\":\\"([^"']+)\\"""")

View File

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

View File

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

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