stevenyomi 205d95cc07 Migrate to lib-multisrc (#1237)
* adjustments

* Run generator

* Remove multisrc project

* add lib dependencies

* Remove multisrc from build scripts

* Remove build condition
2024-02-18 20:10:37 +00:00

334 lines
13 KiB
Kotlin

package eu.kanade.tachiyomi.multisrc.gigaviewer
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Rect
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservable
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
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 kotlinx.serialization.SerializationException
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Call
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
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
abstract class GigaViewer(
override val name: String,
override val baseUrl: String,
override val lang: String,
private val cdnUrl: String = "",
) : ParsedHttpSource() {
override val supportsLatest = true
protected val dayOfWeek: String by lazy {
Calendar.getInstance()
.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG, Locale.US)!!
.lowercase(Locale.US)
}
protected open val publisher: String = ""
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Origin", baseUrl)
.add("Referer", baseUrl)
private val json: Json by injectLazy()
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/series", headers)
override fun popularMangaSelector(): String = "ul.series-list li a"
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
title = element.selectFirst("h2.series-list-title")!!.text()
thumbnail_url = element.selectFirst("div.series-list-thumb img")!!
.attr("data-src")
setUrlWithoutDomain(element.attr("href"))
}
override fun popularMangaNextPageSelector(): String? = null
override fun latestUpdatesRequest(page: Int): Request = popularMangaRequest(page)
override fun latestUpdatesSelector(): String = "h2.series-list-date-week.$dayOfWeek + ul.series-list li a"
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun latestUpdatesNextPageSelector(): String? = null
// The search returns 404 when there's no results.
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return client.newCall(searchMangaRequest(page, query, filters))
.asObservableIgnoreCode(404)
.map(::searchMangaParse)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isNotEmpty()) {
val url = "$baseUrl/search".toHttpUrl().newBuilder()
.addQueryParameter("q", query)
return GET(url.build(), headers)
}
val collectionSelected = (filters[0] as CollectionFilter).selected
val collectionPath = if (collectionSelected.path.isBlank()) "" else "/" + collectionSelected.path
return GET("$baseUrl/series$collectionPath", headers)
}
override fun searchMangaParse(response: Response): MangasPage {
if (response.request.url.toString().contains("search")) {
return super.searchMangaParse(response)
}
return popularMangaParse(response)
}
override fun searchMangaSelector() = "ul.search-series-list li, ul.series-list li"
override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
title = element.selectFirst("div.title-box p.series-title")!!.text()
thumbnail_url = element.selectFirst("div.thmb-container a img")!!.attr("src")
setUrlWithoutDomain(element.selectFirst("div.thmb-container a")!!.attr("href"))
}
override fun searchMangaNextPageSelector(): String? = null
protected open fun mangaDetailsInfoSelector(): String = "section.series-information div.series-header"
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
val infoElement = document.selectFirst(mangaDetailsInfoSelector())!!
title = infoElement.selectFirst("h1.series-header-title")!!.text()
author = infoElement.selectFirst("h2.series-header-author")!!.text()
artist = author
description = infoElement.selectFirst("p.series-header-description")!!.text()
thumbnail_url = infoElement.selectFirst("div.series-header-image-wrapper img")!!
.attr("data-src")
}
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
val readableProductList = document.selectFirst("div.js-readable-product-list")!!
val firstListEndpoint = readableProductList.attr("data-first-list-endpoint")
.toHttpUrl()
val latestListEndpoint = readableProductList.attr("data-latest-list-endpoint")
.toHttpUrlOrNull() ?: firstListEndpoint
val numberSince = latestListEndpoint.queryParameter("number_since")!!.toFloat()
.coerceAtLeast(firstListEndpoint.queryParameter("number_since")!!.toFloat())
val newHeaders = headers.newBuilder()
.set("Referer", response.request.url.toString())
.build()
var readMoreEndpoint = firstListEndpoint.newBuilder()
.setQueryParameter("number_since", numberSince.toString())
.toString()
val chapters = mutableListOf<SChapter>()
var request = GET(readMoreEndpoint, newHeaders)
var result = client.newCall(request).execute()
while (result.code != 404) {
val jsonResult = json.parseToJsonElement(result.body.string()).jsonObject
readMoreEndpoint = jsonResult["nextUrl"]!!.jsonPrimitive.content
val tempDocument = Jsoup.parse(
jsonResult["html"]!!.jsonPrimitive.content,
response.request.url.toString(),
)
tempDocument
.select("ul.series-episode-list " + chapterListSelector())
.mapTo(chapters) { element -> chapterFromElement(element) }
request = GET(readMoreEndpoint, newHeaders)
result = client.newCall(request).execute()
}
result.close()
return chapters
}
override fun chapterListSelector() = "li.episode"
protected open val chapterListMode = CHAPTER_LIST_PAID
override fun chapterFromElement(element: Element): SChapter {
val info = element.selectFirst("a.series-episode-list-container") ?: element
val mangaUrl = element.ownerDocument()!!.location()
return SChapter.create().apply {
name = info.selectFirst("h4.series-episode-list-title")!!.text()
if (chapterListMode == CHAPTER_LIST_PAID && element.selectFirst("span.series-episode-list-is-free") == null) {
name = YEN_BANKNOTE + name
} else if (chapterListMode == CHAPTER_LIST_LOCKED && element.hasClass("private")) {
name = LOCK + name
}
date_upload = info.selectFirst("span.series-episode-list-date")
?.text().orEmpty()
.toDate()
scanlator = publisher
setUrlWithoutDomain(if (info.tagName() == "a") info.attr("href") else mangaUrl)
}
}
override fun pageListParse(document: Document): List<Page> {
val episode = document.selectFirst("script#episode-json")!!
.attr("data-value")
.let {
try {
json.decodeFromString<GigaViewerEpisodeDto>(it)
} catch (e: SerializationException) {
throw Exception("このチャプターは非公開です\nChapter is not available!")
}
}
return episode.readableProduct.pageStructure.pages
.filter { it.type == "main" }
.mapIndexed { i, page ->
val imageUrl = page.src.toHttpUrl().newBuilder()
.addQueryParameter("width", page.width.toString())
.addQueryParameter("height", page.height.toString())
.toString()
Page(i, document.location(), imageUrl)
}
}
override fun imageUrlParse(document: Document) = ""
override fun imageRequest(page: Page): Request {
val newHeaders = headersBuilder()
.set("Referer", page.url)
.build()
return GET(page.imageUrl!!, newHeaders)
}
protected data class Collection(val name: String, val path: String) {
override fun toString(): String = name
}
private class CollectionFilter(val collections: List<Collection>) : Filter.Select<Collection>(
"コレクション",
collections.toTypedArray(),
) {
val selected: Collection
get() = collections[state]
}
override fun getFilterList(): FilterList = FilterList(CollectionFilter(getCollections()))
protected open fun getCollections(): List<Collection> = emptyList()
protected open fun imageIntercept(chain: Interceptor.Chain): Response {
var request = chain.request()
if (!request.url.toString().startsWith(cdnUrl)) {
return chain.proceed(request)
}
val width = request.url.queryParameter("width")!!.toInt()
val height = request.url.queryParameter("height")!!.toInt()
val newUrl = request.url.newBuilder()
.removeAllQueryParameters("width")
.removeAllQueryParameters("height")
.build()
request = request.newBuilder().url(newUrl).build()
val response = chain.proceed(request)
val image = decodeImage(response.body.byteStream(), width, height)
val body = image.toResponseBody(jpegMediaType)
response.close()
return response.newBuilder().body(body).build()
}
protected open fun decodeImage(image: InputStream, width: Int, height: Int): ByteArray {
val input = BitmapFactory.decodeStream(image)
val cWidth = (floor(width.toDouble() / (DIVIDE_NUM * MULTIPLE)) * MULTIPLE).toInt()
val cHeight = (floor(height.toDouble() / (DIVIDE_NUM * MULTIPLE)) * MULTIPLE).toInt()
val result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(result)
val imageRect = Rect(0, 0, width, height)
canvas.drawBitmap(input, imageRect, imageRect, null)
for (e in 0 until DIVIDE_NUM * DIVIDE_NUM) {
val x = e % DIVIDE_NUM * cWidth
val y = (floor(e.toFloat() / DIVIDE_NUM) * cHeight).toInt()
val cellSrc = Rect(x, y, x + cWidth, y + cHeight)
val row = floor(e.toFloat() / DIVIDE_NUM).toInt()
val dstE = e % DIVIDE_NUM * DIVIDE_NUM + row
val dstX = dstE % DIVIDE_NUM * cWidth
val dstY = (floor(dstE.toFloat() / DIVIDE_NUM) * cHeight).toInt()
val cellDst = Rect(dstX, dstY, dstX + cWidth, dstY + cHeight)
canvas.drawBitmap(input, cellSrc, cellDst, null)
}
val output = ByteArrayOutputStream()
result.compress(Bitmap.CompressFormat.JPEG, 90, output)
return output.toByteArray()
}
private fun Call.asObservableIgnoreCode(code: Int): Observable<Response> {
return asObservable().doOnNext { response ->
if (!response.isSuccessful && response.code != code) {
response.close()
throw Exception("HTTP error ${response.code}")
}
}
}
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()
const val CHAPTER_LIST_PAID = 0
const val CHAPTER_LIST_LOCKED = 1
private const val YEN_BANKNOTE = "💴 "
private const val LOCK = "🔒 "
}
}