Add Viz Shonen Jump (#3560)
* Add Viz Shonen Jump. * Update User Agent. * Switch to AndroidX dependency. * Add support to premium chapters if logged in (#1) * Fix some parsing issues, works with premium chapters * Slight code reuse reduction * CacheControl force network and simplify parsing for Viz * Hide locked chapters when not logged in Co-authored-by: Unlocked <10186337+TheUnlocked@users.noreply.github.com>
This commit is contained in:
parent
60e16cf119
commit
8874e85b92
|
@ -17,3 +17,6 @@ org.gradle.jvmargs=-Xmx2048m
|
|||
org.gradle.parallel=true
|
||||
|
||||
org.gradle.caching=true
|
||||
|
||||
# Enable AndroidX dependencies
|
||||
android.useAndroidX=true
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
appName = 'Tachiyomi: VIZ Shonen Jump'
|
||||
pkgNameSuffix = 'en.vizshonenjump'
|
||||
extClass = '.VizShonenJump'
|
||||
extVersionCode = 1
|
||||
libVersion = '1.2'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.exifinterface:exifinterface:1.2.0'
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 8.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
|
@ -0,0 +1,99 @@
|
|||
package eu.kanade.tachiyomi.extension.en.vizshonenjump
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody
|
||||
|
||||
class VizImageInterceptor : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val response = chain.proceed(chain.request())
|
||||
|
||||
if (chain.request().url().queryParameter(SIGNATURE) == null)
|
||||
return response
|
||||
|
||||
val image = decodeImage(response.body()!!.byteStream())
|
||||
val body = ResponseBody.create(MEDIA_TYPE, image)
|
||||
return response.newBuilder()
|
||||
.body(body)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun decodeImage(image: InputStream): ByteArray {
|
||||
// See: https://stackoverflow.com/a/5924132
|
||||
// See: https://github.com/inorichi/tachiyomi-extensions/issues/2678#issuecomment-645857603
|
||||
val byteOutputStream = ByteArrayOutputStream()
|
||||
image.copyTo(byteOutputStream)
|
||||
val byteInputStreamForImage = ByteArrayInputStream(byteOutputStream.toByteArray())
|
||||
val byteInputStreamForExif = ByteArrayInputStream(byteOutputStream.toByteArray())
|
||||
|
||||
val input = BitmapFactory.decodeStream(byteInputStreamForImage)
|
||||
val width = input.width
|
||||
val height = input.height
|
||||
val newWidth = width - WIDTH_CUT
|
||||
val newHeight = height - HEIGHT_CUT
|
||||
val blockWidth = newWidth / CELL_WIDTH_COUNT
|
||||
val blockHeight = newHeight / CELL_HEIGHT_COUNT
|
||||
|
||||
val result = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(result)
|
||||
|
||||
// Draw the borders.
|
||||
canvas.copyCell(input, Pair(0, 0), Pair(0, 0), newWidth, blockHeight)
|
||||
canvas.copyCell(input,
|
||||
Pair(0, blockHeight + 10), Pair(0, blockHeight),
|
||||
blockWidth, newHeight - 2 * blockHeight)
|
||||
canvas.copyCell(input,
|
||||
Pair(0, (CELL_HEIGHT_COUNT - 1) * (blockHeight + 10)),
|
||||
Pair(0, (CELL_HEIGHT_COUNT - 1) * blockHeight),
|
||||
newWidth, height - (CELL_HEIGHT_COUNT - 1) * (blockHeight + 10))
|
||||
canvas.copyCell(input,
|
||||
Pair((CELL_WIDTH_COUNT - 1) * (blockWidth + 10), blockHeight + 10),
|
||||
Pair((CELL_WIDTH_COUNT - 1) * blockWidth, blockHeight),
|
||||
blockWidth + (newWidth - CELL_WIDTH_COUNT * blockWidth),
|
||||
newHeight - 2 * blockHeight)
|
||||
|
||||
// Get the key from the EXIF tag.
|
||||
val exifInterface = ExifInterface(byteInputStreamForExif)
|
||||
val uniqueId = exifInterface.getAttribute(ExifInterface.TAG_IMAGE_UNIQUE_ID)!!
|
||||
val key = uniqueId.split(":")
|
||||
.map { it.toInt(16) }
|
||||
|
||||
// Draw the inner cells.
|
||||
for ((m, y) in key.iterator().withIndex()) {
|
||||
canvas.copyCell(input,
|
||||
Pair((m % (CELL_WIDTH_COUNT - 2) + 1) * (blockWidth + 10), (m / (CELL_WIDTH_COUNT - 2) + 1) * (blockHeight + 10)),
|
||||
Pair((y % (CELL_WIDTH_COUNT - 2) + 1) * blockWidth, (y / (CELL_WIDTH_COUNT - 2) + 1) * blockHeight),
|
||||
blockWidth, blockHeight)
|
||||
}
|
||||
|
||||
val output = ByteArrayOutputStream()
|
||||
result.compress(Bitmap.CompressFormat.PNG, 100, output)
|
||||
return output.toByteArray()
|
||||
}
|
||||
|
||||
private fun Canvas.copyCell(from: Bitmap, src: Pair<Int, Int>, dst: Pair<Int, Int>, width: Int, height: Int) {
|
||||
val srcRect = Rect(src.first, src.second, src.first + width, src.second + height)
|
||||
val dstRect = Rect(dst.first, dst.second, dst.first + width, dst.second + height)
|
||||
drawBitmap(from, srcRect, dstRect, null)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SIGNATURE = "Signature"
|
||||
private val MEDIA_TYPE = MediaType.parse("image/png")
|
||||
|
||||
private const val CELL_WIDTH_COUNT = 10
|
||||
private const val CELL_HEIGHT_COUNT = 15
|
||||
private const val WIDTH_CUT = 90
|
||||
private const val HEIGHT_CUT = 140
|
||||
}
|
||||
}
|
|
@ -0,0 +1,250 @@
|
|||
package eu.kanade.tachiyomi.extension.en.vizshonenjump
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
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 java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
|
||||
class VizShonenJump : ParsedHttpSource() {
|
||||
|
||||
override val name = "VIZ Shonen Jump"
|
||||
|
||||
override val baseUrl = "https://www.viz.com"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.client.newBuilder()
|
||||
.addInterceptor(VizImageInterceptor())
|
||||
.build()
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||
.add("User-Agent", USER_AGENT)
|
||||
.add("Origin", baseUrl)
|
||||
.add("Referer", "$baseUrl/shonenjump")
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val newHeaders = headersBuilder()
|
||||
.set("Referer", baseUrl)
|
||||
.build()
|
||||
|
||||
return GET("$baseUrl/shonenjump", newHeaders, CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val mangas = super.popularMangaParse(response).mangas
|
||||
|
||||
if (mangas.isEmpty())
|
||||
throw Exception(COUNTRY_NOT_SUPPORTED)
|
||||
|
||||
return MangasPage(mangas.sortedBy { it.title }, false)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector(): String = "section.section_chapters div.o_sort_container div.o_sortable > a"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||
title = element.select("div.pad-x-rg").first().text()
|
||||
thumbnail_url = element.select("div.pos-r img.disp-bl").first()?.attr("src")
|
||||
url = element.attr("href")
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector(): String? = null
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = popularMangaRequest(page)
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val mangasPage = super.latestUpdatesParse(response)
|
||||
|
||||
if (mangasPage.mangas.isEmpty())
|
||||
throw Exception(COUNTRY_NOT_SUPPORTED)
|
||||
|
||||
return mangasPage
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String? = null
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return super.fetchSearchManga(page, query, filters)
|
||||
.map { MangasPage(it.mangas.filter { m -> m.title.contains(query, true) }, it.hasNextPage) }
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = popularMangaRequest(page)
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val mangasPage = super.searchMangaParse(response)
|
||||
|
||||
if (mangasPage.mangas.isEmpty())
|
||||
throw Exception(COUNTRY_NOT_SUPPORTED)
|
||||
|
||||
return mangasPage
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector(): String? = null
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val seriesIntro = document.select("section#series-intro").first()
|
||||
|
||||
return SManga.create().apply {
|
||||
author = seriesIntro.select("div.type-rg span").first()?.text()
|
||||
?.replace("Created by ", "")
|
||||
artist = author
|
||||
status = SManga.ONGOING
|
||||
description = seriesIntro.select("h2").first().text()
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val allChapters = super.chapterListParse(response)
|
||||
|
||||
val newHeaders = headersBuilder()
|
||||
.add("X-Requested-With", "XMLHttpRequest")
|
||||
.set("Referer", response.request().url().toString())
|
||||
.build()
|
||||
val loginCheckRequest = GET(REFRESH_LOGIN_LINKS_URL, newHeaders)
|
||||
val document = client.newCall(loginCheckRequest).execute().asJsoup()
|
||||
val isLoggedIn = document.select("div#o_account-links-content").first()!!.attr("logged_in")!!.toBoolean()
|
||||
|
||||
if (isLoggedIn) {
|
||||
return allChapters.map { oldChapter ->
|
||||
oldChapter.apply { url = url.substringAfter("'").substringBeforeLast("'") }
|
||||
}
|
||||
}
|
||||
|
||||
return allChapters.filter { !it.url.startsWith("javascript") }
|
||||
}
|
||||
|
||||
override fun chapterListSelector() =
|
||||
"section.section_chapters div.o_sortable > a.o_chapter-container, " +
|
||||
"section.section_chapters div.o_sortable div.o_chapter-vol-container tr.o_chapter a.o_chapter-container.pad-r-0"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||
val isVolume = element.select("div:nth-child(1) table").first() == null
|
||||
|
||||
if (isVolume) {
|
||||
name = element.text()
|
||||
} else {
|
||||
val leftSide = element.select("div:nth-child(1) table").first()!!
|
||||
val rightSide = element.select("div:nth-child(2) table").first()!!
|
||||
|
||||
name = rightSide.select("td.ch-num-list-spacing").first()!!.text()
|
||||
date_upload = DATE_FORMATTER.tryParseTime(leftSide.select("td[align=right]").first()!!.text())
|
||||
}
|
||||
|
||||
scanlator = "VIZ Media"
|
||||
|
||||
url = element.attr("data-target-url")
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val newHeaders = headersBuilder()
|
||||
.set("Referer", baseUrl + chapter.url.substringBefore("-chapter"))
|
||||
.build()
|
||||
|
||||
return GET(baseUrl + chapter.url, newHeaders)
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val pageCount = document.select("script:containsData(var pages)").first().data()
|
||||
.substringAfter("= ")
|
||||
.substringBefore(";")
|
||||
.toInt()
|
||||
val mangaId = document.location()
|
||||
.substringAfterLast("/")
|
||||
.substringBefore("?")
|
||||
|
||||
return IntRange(1, pageCount)
|
||||
.map {
|
||||
val imageUrl = HttpUrl.parse("$baseUrl/manga/get_manga_url")!!.newBuilder()
|
||||
.addQueryParameter("device_id", "3")
|
||||
.addQueryParameter("manga_id", mangaId)
|
||||
.addQueryParameter("page", it.toString())
|
||||
.addEncodedQueryParameter("referer", document.location())
|
||||
.toString()
|
||||
|
||||
Page(it, imageUrl)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlRequest(page: Page): Request {
|
||||
val url = HttpUrl.parse(page.url)!!
|
||||
val referer = url.queryParameter("referer")!!
|
||||
val newUrl = url.newBuilder()
|
||||
.removeAllEncodedQueryParameters("referer")
|
||||
.toString()
|
||||
|
||||
val newHeaders = headersBuilder()
|
||||
.add("X-Requested-With", "XMLHttpRequest")
|
||||
.set("Referer", referer)
|
||||
.build()
|
||||
|
||||
return GET(newUrl, newHeaders)
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
val cdnUrl = response.body()!!.string()
|
||||
val referer = response.request().header("Referer")!!
|
||||
|
||||
return HttpUrl.parse(cdnUrl)!!.newBuilder()
|
||||
.addEncodedQueryParameter("referer", referer)
|
||||
.toString()
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val imageUrl = HttpUrl.parse(page.imageUrl!!)!!
|
||||
val referer = imageUrl.queryParameter("referer")!!
|
||||
val newImageUrl = imageUrl.newBuilder()
|
||||
.removeAllEncodedQueryParameters("referer")
|
||||
.toString()
|
||||
|
||||
val newHeaders = headersBuilder()
|
||||
.set("Referer", referer)
|
||||
.build()
|
||||
|
||||
return GET(newImageUrl, newHeaders)
|
||||
}
|
||||
|
||||
private fun SimpleDateFormat.tryParseTime(date: String): Long {
|
||||
return try {
|
||||
parse(date).time
|
||||
} catch (e: ParseException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36"
|
||||
|
||||
private val DATE_FORMATTER by lazy { SimpleDateFormat("MMMM d, yyyy", Locale.ENGLISH) }
|
||||
|
||||
private const val COUNTRY_NOT_SUPPORTED = "Your country is not supported, try using a VPN."
|
||||
|
||||
private const val REFRESH_LOGIN_LINKS_URL = "https://www.viz.com/account/refresh_login_links"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue