diff --git a/lib-multisrc/iken/build.gradle.kts b/lib-multisrc/iken/build.gradle.kts index ede652be5..f0ad544d5 100644 --- a/lib-multisrc/iken/build.gradle.kts +++ b/lib-multisrc/iken/build.gradle.kts @@ -2,4 +2,4 @@ plugins { id("lib-multisrc") } -baseVersionCode = 6 +baseVersionCode = 7 diff --git a/lib-multisrc/iken/src/eu/kanade/tachiyomi/multisrc/iken/Dto.kt b/lib-multisrc/iken/src/eu/kanade/tachiyomi/multisrc/iken/Dto.kt index 056842d7b..a2916cc60 100644 --- a/lib-multisrc/iken/src/eu/kanade/tachiyomi/multisrc/iken/Dto.kt +++ b/lib-multisrc/iken/src/eu/kanade/tachiyomi/multisrc/iken/Dto.kt @@ -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 diff --git a/lib-multisrc/iken/src/eu/kanade/tachiyomi/multisrc/iken/Iken.kt b/lib-multisrc/iken/src/eu/kanade/tachiyomi/multisrc/iken/Iken.kt index 67fc19353..bd610a1d3 100644 --- a/lib-multisrc/iken/src/eu/kanade/tachiyomi/multisrc/iken/Iken.kt +++ b/lib-multisrc/iken/src/eu/kanade/tachiyomi/multisrc/iken/Iken.kt @@ -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() + 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 { - val data = response.parseAs>() + 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>() 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 { 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>().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 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() + } } private const val perPage = 18 +private const val showLockedChapterPrefKey = "pref_show_locked_chapters" +private val userIdRegex = Regex(""""user\\":\{\\"id\\":\\"([^"']+)\\"""") diff --git a/src/en/arvenscans/src/eu/kanade/tachiyomi/extension/en/arvenscans/VortexScans.kt b/src/en/arvenscans/src/eu/kanade/tachiyomi/extension/en/arvenscans/VortexScans.kt index f987d6c25..fb8955dca 100644 --- a/src/en/arvenscans/src/eu/kanade/tachiyomi/extension/en/arvenscans/VortexScans.kt +++ b/src/en/arvenscans/src/eu/kanade/tachiyomi/extension/en/arvenscans/VortexScans.kt @@ -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() - - private val regexImages = """\\"images\\":(.*?)\\"next""".toRegex() - - override fun pageListParse(response: Response): List { - val document = response.asJsoup() - - val images = document.selectFirst("script:containsData(images)") - ?.data() - ?.let { regexImages.find(it)!!.groupValues[1].trim(',') } - ?.let { json.decodeFromString("\"$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) - } - } -} +) diff --git a/src/en/infernalvoidscans/src/eu/kanade/tachiyomi/extension/en/infernalvoidscans/HiveScans.kt b/src/en/infernalvoidscans/src/eu/kanade/tachiyomi/extension/en/infernalvoidscans/HiveScans.kt index 4c78ef23e..d1b9d90c3 100644 --- a/src/en/infernalvoidscans/src/eu/kanade/tachiyomi/extension/en/infernalvoidscans/HiveScans.kt +++ b/src/en/infernalvoidscans/src/eu/kanade/tachiyomi/extension/en/infernalvoidscans/HiveScans.kt @@ -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() - 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 { - val pageDataArray = pageRegex.find(response.body.string())?.destructured?.component1()?.replace("\\", "") ?: return listOf() - return json.decodeFromString>(pageDataArray).mapIndexed { idx, page -> - Page(idx, imageUrl = page.url) - } - } - override fun headersBuilder() = super.headersBuilder() .set("Cache-Control", "max-age=0") } diff --git a/src/en/nyxscans/build.gradle b/src/en/nyxscans/build.gradle new file mode 100644 index 000000000..11d93f7d9 --- /dev/null +++ b/src/en/nyxscans/build.gradle @@ -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" diff --git a/src/en/nyxscans/res/mipmap-hdpi/ic_launcher.png b/src/en/nyxscans/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..a5073c493 Binary files /dev/null and b/src/en/nyxscans/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/nyxscans/res/mipmap-mdpi/ic_launcher.png b/src/en/nyxscans/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..c0bc5ae16 Binary files /dev/null and b/src/en/nyxscans/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/nyxscans/res/mipmap-xhdpi/ic_launcher.png b/src/en/nyxscans/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..4697c1b5f Binary files /dev/null and b/src/en/nyxscans/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/nyxscans/res/mipmap-xxhdpi/ic_launcher.png b/src/en/nyxscans/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..6ff028a14 Binary files /dev/null and b/src/en/nyxscans/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/nyxscans/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/nyxscans/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..81764f3d0 Binary files /dev/null and b/src/en/nyxscans/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/nyxscans/src/eu/kanade/tachiyomi/extension/en/nyxscans/NyxScans.kt b/src/en/nyxscans/src/eu/kanade/tachiyomi/extension/en/nyxscans/NyxScans.kt new file mode 100644 index 000000000..1583840ca --- /dev/null +++ b/src/en/nyxscans/src/eu/kanade/tachiyomi/extension/en/nyxscans/NyxScans.kt @@ -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>().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, + ) +}