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")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 6
|
baseVersionCode = 7
|
||||||
|
@ -97,16 +97,21 @@ class Chapter(
|
|||||||
private val createdAt: String,
|
private val createdAt: String,
|
||||||
private val chapterStatus: String,
|
private val chapterStatus: String,
|
||||||
private val isAccessible: Boolean,
|
private val isAccessible: Boolean,
|
||||||
|
private val isLocked: Boolean? = false,
|
||||||
|
private val isTimeLocked: Boolean? = false,
|
||||||
private val mangaPost: ChapterPostDetails,
|
private val mangaPost: ChapterPostDetails,
|
||||||
) {
|
) {
|
||||||
fun isPublic() = chapterStatus == "PUBLIC"
|
fun isPublic() = chapterStatus == "PUBLIC"
|
||||||
|
|
||||||
fun isAccessible() = isAccessible
|
fun isAccessible() = isAccessible
|
||||||
|
|
||||||
|
fun isLocked() = (isLocked == true) || (isTimeLocked == true)
|
||||||
|
|
||||||
fun toSChapter(mangaSlug: String?) = SChapter.create().apply {
|
fun toSChapter(mangaSlug: String?) = SChapter.create().apply {
|
||||||
|
val prefix = if (isLocked()) "🔒 " else ""
|
||||||
val seriesSlug = mangaSlug ?: mangaPost.slug
|
val seriesSlug = mangaSlug ?: mangaPost.slug
|
||||||
url = "/series/$seriesSlug/$slug#$id"
|
url = "/series/$seriesSlug/$slug#$id"
|
||||||
name = "Chapter $number"
|
name = "${prefix}Chapter $number"
|
||||||
scanlator = createdBy.name
|
scanlator = createdBy.name
|
||||||
date_upload = try {
|
date_upload = try {
|
||||||
dateFormat.parse(createdAt)!!.time
|
dateFormat.parse(createdAt)!!.time
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.iken
|
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.network.GET
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
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.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import kotlinx.serialization.decodeFromString
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
import kotlinx.serialization.json.Json
|
import keiyoushi.utils.parseAs
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
abstract class Iken(
|
abstract class Iken(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
override val baseUrl: String,
|
override val baseUrl: String,
|
||||||
) : HttpSource() {
|
) : HttpSource(), ConfigurableSource {
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
override val client = network.cloudflareClient
|
override val client = network.cloudflareClient
|
||||||
|
|
||||||
private val json by injectLazy<Json>()
|
private val preferences: SharedPreferences by getPreferencesLazy()
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
.set("Referer", "$baseUrl/")
|
.set("Referer", "$baseUrl/")
|
||||||
@ -114,35 +119,76 @@ abstract class Iken(
|
|||||||
throw UnsupportedOperationException()
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga): Request {
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
val id = manga.url.substringAfterLast("#")
|
return GET("$baseUrl/series/${manga.url}", headers)
|
||||||
val url = "$baseUrl/api/chapters?postId=$id&skip=0&take=1000&order=desc&userid="
|
|
||||||
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
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" }
|
assert(!data.post.isNovel) { "Novels are unsupported" }
|
||||||
|
|
||||||
return data.post.chapters
|
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) }
|
.map { it.toSChapter(data.post.slug) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
|
|
||||||
return document.select("main section img").mapIndexed { idx, img ->
|
if (document.selectFirst("svg.lucide-lock") != null) {
|
||||||
Page(idx, imageUrl = img.absUrl("src"))
|
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) =
|
override fun imageUrlParse(response: Response) =
|
||||||
throw UnsupportedOperationException()
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
private inline fun <reified T> Response.parseAs(): T =
|
protected fun Document.getNextJson(key: String): String {
|
||||||
json.decodeFromString(body.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 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
|
package eu.kanade.tachiyomi.extension.en.arvenscans
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.iken.Iken
|
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(
|
class VortexScans : Iken(
|
||||||
"Vortex Scans",
|
"Vortex Scans",
|
||||||
"en",
|
"en",
|
||||||
"https://vortexscans.org",
|
"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
|
package eu.kanade.tachiyomi.extension.en.infernalvoidscans
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.iken.Iken
|
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(
|
class HiveScans : Iken(
|
||||||
"Hive Scans",
|
"Hive Scans",
|
||||||
"en",
|
"en",
|
||||||
"https://hivetoon.com",
|
"https://hivetoon.com",
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val json by injectLazy<Json>()
|
|
||||||
|
|
||||||
override val versionId = 2
|
override val versionId = 2
|
||||||
|
|
||||||
override val client = super.client.newBuilder()
|
override val client = super.client.newBuilder()
|
||||||
@ -28,20 +19,6 @@ class HiveScans : Iken(
|
|||||||
}
|
}
|
||||||
.build()
|
.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()
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
.set("Cache-Control", "max-age=0")
|
.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