* adjustments * Run generator * Remove multisrc project * add lib dependencies * Remove multisrc from build scripts * Remove build condition
334 lines
13 KiB
Kotlin
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 = "🔒 "
|
|
}
|
|
}
|