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:
Alessandro Jean 2020-06-18 23:53:30 -03:00 committed by GitHub
parent 60e16cf119
commit 8874e85b92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 368 additions and 0 deletions

View File

@ -17,3 +17,6 @@ org.gradle.jvmargs=-Xmx2048m
org.gradle.parallel=true
org.gradle.caching=true
# Enable AndroidX dependencies
android.useAndroidX=true

View File

@ -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

View File

@ -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
}
}

View File

@ -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"
}
}