GigaViewer: Add paginated chapter list parse support (#9911)

* Support for paginated readable products API

* Simplify date handling

* Chapter status labels

* Fix type

* Handle null display_open_at value

* Additional chapter status label

* Mark GigaViewer paginated sources

* Implement requested changes from feedback

* Remove unused fields

* Use tryParse for date handling

* Remove label constants

* Remove extra whitespace
This commit is contained in:
hatozuki-programmer 2025-08-03 10:12:47 -04:00 committed by Draff
parent fd494a9fa7
commit 286ccd2f53
Signed by: Draff
GPG Key ID: E8A89F3211677653
11 changed files with 107 additions and 15 deletions

View File

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

View File

@ -14,6 +14,8 @@ 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 keiyoushi.utils.parseAs
import keiyoushi.utils.tryParse
import kotlinx.serialization.SerializationException
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
@ -34,7 +36,6 @@ import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import kotlin.math.floor
@ -44,6 +45,7 @@ abstract class GigaViewer(
override val baseUrl: String,
override val lang: String,
private val cdnUrl: String = "",
private val isPaginated: Boolean = false,
) : ParsedHttpSource() {
override val supportsLatest = true
@ -134,7 +136,7 @@ abstract class GigaViewer(
.attr("data-src")
}
override fun chapterListParse(response: Response): List<SChapter> {
protected fun chapterListParseSinglePage(response: Response): List<SChapter> {
val document = response.asJsoup()
val aggregateId = document.selectFirst("script.js-valve")!!.attr("data-giga_series")
@ -180,6 +182,61 @@ abstract class GigaViewer(
return chapters
}
protected fun paginatedChaptersRequest(referer: String, aggregateId: String, offset: Int): Response {
val headers = headers.newBuilder()
.set("Referer", referer)
.build()
val apiUrl = baseUrl.toHttpUrl().newBuilder()
.addPathSegment("api")
.addPathSegment("viewer")
.addPathSegment("pagination_readable_products")
.addQueryParameter("type", "episode")
.addQueryParameter("aggregate_id", aggregateId)
.addQueryParameter("sort_order", "desc")
.addQueryParameter("offset", offset.toString())
.build()
.toString()
val request = GET(apiUrl, headers)
return client.newCall(request).execute()
}
protected fun chapterListParsePaginated(response: Response): List<SChapter> {
val document = response.asJsoup()
val referer = response.request.url.toString()
val aggregateId = document.selectFirst("script.js-valve")!!.attr("data-giga_series")
val chapters = mutableListOf<SChapter>()
var offset = 0
// repeat until the offset is too large to return any chapters, resulting in an empty list
while (true) {
// make request
val result = paginatedChaptersRequest(referer, aggregateId, offset)
val resultData = result.parseAs<List<GigaViewerPaginationReadableProduct>>()
if (resultData.isEmpty()) {
break
}
resultData.mapTo(chapters) { element ->
element.toSChapter(chapterListMode, publisher)
}
// increase offset
offset += resultData.size
}
return chapters
}
override fun chapterListParse(response: Response): List<SChapter> {
return if (isPaginated) {
chapterListParsePaginated(response)
} else {
chapterListParseSinglePage(response)
}
}
override fun chapterListSelector() = "li.episode"
protected open val chapterListMode = CHAPTER_LIST_PAID
@ -195,9 +252,7 @@ abstract class GigaViewer(
} else if (chapterListMode == CHAPTER_LIST_LOCKED && element.hasClass("private")) {
name = LOCK + name
}
date_upload = info.selectFirst("span.series-episode-list-date")
?.text().orEmpty()
.toDate()
date_upload = DATE_PARSER_SIMPLE.tryParse(info.selectFirst("span.series-episode-list-date")?.text().orEmpty())
scanlator = publisher
setUrlWithoutDomain(if (info.tagName() == "a") info.attr("href") else mangaUrl)
}
@ -314,14 +369,7 @@ abstract class GigaViewer(
}
}
private fun String.toDate(): Long {
return runCatching { DATE_PARSER.parse(this)?.time }
.getOrNull() ?: 0L
}
companion object {
private val DATE_PARSER by lazy { SimpleDateFormat("yyyy/MM/dd", Locale.ENGLISH) }
private const val DIVIDE_NUM = 4
private const val MULTIPLE = 8
private val jpegMediaType = "image/jpeg".toMediaType()
@ -329,7 +377,7 @@ abstract class GigaViewer(
const val CHAPTER_LIST_PAID = 0
const val CHAPTER_LIST_LOCKED = 1
private const val YEN_BANKNOTE = "💴 "
private const val LOCK = "🔒 "
const val YEN_BANKNOTE = "💴 "
const val LOCK = "🔒 "
}
}

View File

@ -1,6 +1,14 @@
package eu.kanade.tachiyomi.multisrc.gigaviewer
import eu.kanade.tachiyomi.multisrc.gigaviewer.GigaViewer.Companion.CHAPTER_LIST_LOCKED
import eu.kanade.tachiyomi.multisrc.gigaviewer.GigaViewer.Companion.CHAPTER_LIST_PAID
import eu.kanade.tachiyomi.multisrc.gigaviewer.GigaViewer.Companion.LOCK
import eu.kanade.tachiyomi.multisrc.gigaviewer.GigaViewer.Companion.YEN_BANKNOTE
import eu.kanade.tachiyomi.source.model.SChapter
import keiyoushi.utils.tryParse
import kotlinx.serialization.Serializable
import java.text.SimpleDateFormat
import java.util.Locale
@Serializable
data class GigaViewerEpisodeDto(
@ -24,3 +32,31 @@ data class GigaViewerPage(
val type: String = "",
val width: Int = 0,
)
@Serializable
class GigaViewerPaginationReadableProduct(
private val display_open_at: String?,
private val readable_product_id: String = "",
private val status: GigaViewerPaginationReadableProductStatus?,
private val title: String = "",
) {
fun toSChapter(chapterListMode: Int, publisher: String) = SChapter.create().apply {
name = title
if (chapterListMode == CHAPTER_LIST_PAID && status?.label != "is_free") {
name = YEN_BANKNOTE + name
} else if (chapterListMode == CHAPTER_LIST_LOCKED && status?.label == "unpublished") {
name = LOCK + name
}
date_upload = DATE_PARSER_COMPLEX.tryParse(display_open_at)
scanlator = publisher
url = "/episode/$readable_product_id"
}
}
@Serializable
class GigaViewerPaginationReadableProductStatus(
val label: String?, // is_free, is_rentable, is_purchasable, unpublished
)
val DATE_PARSER_SIMPLE = SimpleDateFormat("yyyy/MM/dd", Locale.ENGLISH)
val DATE_PARSER_COMPLEX = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH)

View File

@ -10,6 +10,7 @@ class ComicDays : GigaViewer(
"https://comic-days.com",
"ja",
"https://cdn-img.comic-days.com/public/page",
isPaginated = true,
) {
override val client: OkHttpClient = super.client.newBuilder()

View File

@ -10,6 +10,7 @@ class ComicGardo : GigaViewer(
"https://comic-gardo.com",
"ja",
"https://cdn-img.comic-gardo.com/public/page",
isPaginated = true,
) {
override val supportsLatest: Boolean = false

View File

@ -12,6 +12,7 @@ class Comiplex : GigaViewer(
"https://viewer.heros-web.com",
"ja",
"https://cdn-img.viewer.heros-web.com/public/page",
isPaginated = true,
) {
override val supportsLatest: Boolean = false

View File

@ -12,6 +12,7 @@ class KurageBunch : GigaViewer(
"https://kuragebunch.com",
"ja",
"https://cdn-img.kuragebunch.com",
isPaginated = true,
) {
override val supportsLatest: Boolean = false

View File

@ -10,6 +10,7 @@ class MagComi : GigaViewer(
"https://magcomi.com",
"ja",
"https://cdn-img.magcomi.com/public/page",
isPaginated = true,
) {
override val supportsLatest: Boolean = false

View File

@ -8,6 +8,7 @@ class ShonenJumpPlus : GigaViewer(
"https://shonenjumpplus.com",
"ja",
"https://cdn-ak-img.shonenjumpplus.com",
isPaginated = true,
) {
override val client: OkHttpClient = super.client.newBuilder()

View File

@ -10,6 +10,7 @@ class SundayWebEvery : GigaViewer(
"https://www.sunday-webry.com",
"ja",
"https://cdn-img.www.sunday-webry.com/public/page",
isPaginated = true,
) {
override val client: OkHttpClient = super.client.newBuilder()

View File

@ -10,6 +10,7 @@ class TonariNoYoungJump : GigaViewer(
"https://tonarinoyj.jp",
"ja",
"https://cdn-img.tonarinoyj.jp/public/page",
isPaginated = true,
) {
override val supportsLatest: Boolean = false