Add SpeedBinb reader library (#1316)
* Add SpeedBinb reader library * Make TextInterceptor generic
This commit is contained in:
parent
93c5dbc650
commit
a799bf8a5c
|
@ -0,0 +1,24 @@
|
||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
kotlin("android")
|
||||||
|
id("kotlinx-serialization")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdk = AndroidConfig.compileSdk
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = AndroidConfig.minSdk
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace = "eu.kanade.tachiyomi.lib.speedbinb"
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly(libs.bundles.common)
|
||||||
|
implementation(project(":lib:textinterceptor"))
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
package eu.kanade.tachiyomi.lib.speedbinb
|
||||||
|
|
||||||
|
private const val URLSAFE_BASE64_LOOKUP = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||||
|
|
||||||
|
internal fun determineKeyPair(src: String?, ptbl: List<String>, ctbl: List<String>): Pair<String, String> {
|
||||||
|
val i = mutableListOf(0, 0)
|
||||||
|
|
||||||
|
if (src != null) {
|
||||||
|
val filename = src.substringAfterLast("/")
|
||||||
|
|
||||||
|
for (e in filename.indices) {
|
||||||
|
i[e % 2] = i[e % 2] + filename[e].code
|
||||||
|
}
|
||||||
|
|
||||||
|
i[0] = i[0] % 8
|
||||||
|
i[1] = i[1] % 8
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pair(ptbl[i[0]], ctbl[i[1]])
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun decodeScrambleTable(cid: String, sharedKey: String, table: String): String {
|
||||||
|
val r = "$cid:$sharedKey"
|
||||||
|
var e = r.toCharArray()
|
||||||
|
.map { it.code }
|
||||||
|
.reduceIndexed { index, acc, i -> acc + (i shl index % 16) } and 2147483647
|
||||||
|
|
||||||
|
if (e == 0) {
|
||||||
|
e = 0x12345678
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildString(table.length) {
|
||||||
|
for (s in table.indices) {
|
||||||
|
e = e ushr 1 xor (1210056708 and -(1 and e))
|
||||||
|
append(((table[s].code - 32 + e) % 94 + 32).toChar())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun generateSharedKey(cid: String): String {
|
||||||
|
val randomChars = randomChars(16)
|
||||||
|
val cidRepeatCount = (16 + cid.length - 1) / cid.length
|
||||||
|
val unk1 = buildString(cid.length * cidRepeatCount) {
|
||||||
|
for (i in 0 until cidRepeatCount) {
|
||||||
|
append(cid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val unk2 = unk1.substring(0, 16)
|
||||||
|
val unk3 = unk1.substring(unk1.length - 16, unk1.length)
|
||||||
|
var s = 0
|
||||||
|
var h = 0
|
||||||
|
var u = 0
|
||||||
|
|
||||||
|
return buildString(randomChars.length * 2) {
|
||||||
|
for (i in randomChars.indices) {
|
||||||
|
s = s xor randomChars[i].code
|
||||||
|
h = h xor unk2[i].code
|
||||||
|
u = u xor unk3[i].code
|
||||||
|
|
||||||
|
append(randomChars[i])
|
||||||
|
append(URLSAFE_BASE64_LOOKUP[(s + h + u) and 63])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun randomChars(length: Int) = buildString(length) {
|
||||||
|
for (i in 0 until length) {
|
||||||
|
append(URLSAFE_BASE64_LOOKUP.random())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
package eu.kanade.tachiyomi.lib.speedbinb
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
|
||||||
|
private val COORD_REGEX = Regex("""^i:(\d+),(\d+)\+(\d+),(\d+)>(\d+),(\d+)$""")
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class BibContentInfo(
|
||||||
|
val result: Int,
|
||||||
|
val items: List<BibContentItem>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class BibContentItem(
|
||||||
|
@SerialName("ContentID") val contentId: String,
|
||||||
|
@SerialName("ContentsServer") val contentServer: String,
|
||||||
|
@SerialName("ServerType") val serverType: Int,
|
||||||
|
val stbl: String,
|
||||||
|
val ttbl: String,
|
||||||
|
val ptbl: String,
|
||||||
|
val ctbl: String,
|
||||||
|
@SerialName("p") val requestToken: String? = null,
|
||||||
|
@SerialName("ViewMode") val viewMode: Int,
|
||||||
|
@SerialName("ContentDate") val contentDate: String? = null,
|
||||||
|
@SerialName("ShopURL") val shopUrl: String? = null,
|
||||||
|
) {
|
||||||
|
fun getSbcUrl(readerUrl: HttpUrl, cid: String) =
|
||||||
|
contentServer.toHttpUrl().newBuilder().apply {
|
||||||
|
when (serverType) {
|
||||||
|
ServerType.DIRECT -> addPathSegment("content.js")
|
||||||
|
ServerType.REST -> addPathSegment("content")
|
||||||
|
ServerType.SBC -> {
|
||||||
|
addPathSegment("sbcGetCntnt.php")
|
||||||
|
setQueryParameter("cid", cid)
|
||||||
|
requestToken?.let { setQueryParameter("p", it) }
|
||||||
|
setQueryParameter("q", "1")
|
||||||
|
setQueryParameter("vm", viewMode.toString())
|
||||||
|
setQueryParameter("dmytime", contentDate ?: System.currentTimeMillis().toString())
|
||||||
|
copyKeyParametersFrom(readerUrl)
|
||||||
|
}
|
||||||
|
else -> throw UnsupportedOperationException("Unsupported ServerType value $serverType")
|
||||||
|
}
|
||||||
|
}.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
object ServerType {
|
||||||
|
const val SBC = 0
|
||||||
|
const val DIRECT = 1
|
||||||
|
const val REST = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
object ViewMode {
|
||||||
|
const val COMMERCIAL = 1
|
||||||
|
const val NON_MEMBER_TRIAL = 2
|
||||||
|
const val MEMBER_TRIAL = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class PtImg(
|
||||||
|
@SerialName("ptimg-version") val ptImgVersion: Int,
|
||||||
|
val resources: PtImgResources,
|
||||||
|
val views: List<PtImgViews>,
|
||||||
|
) {
|
||||||
|
val translations by lazy {
|
||||||
|
views[0].coords.map { coord ->
|
||||||
|
val v = COORD_REGEX.matchEntire(coord)!!.groupValues.drop(1).map { it.toInt() }
|
||||||
|
PtImgTranslation(v[0], v[1], v[2], v[3], v[4], v[5])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class PtImgResources(
|
||||||
|
val i: PtImgImage,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class PtImgImage(
|
||||||
|
val src: String,
|
||||||
|
val width: Int,
|
||||||
|
val height: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class PtImgViews(
|
||||||
|
val width: Int,
|
||||||
|
val height: Int,
|
||||||
|
val coords: Array<String>,
|
||||||
|
)
|
||||||
|
|
||||||
|
class PtImgTranslation(val xsrc: Int, val ysrc: Int, val width: Int, val height: Int, val xdest: Int, val ydest: Int)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class SBCContent(
|
||||||
|
@SerialName("SBCVersion") val sbcVersion: String,
|
||||||
|
val result: Int,
|
||||||
|
val ttx: String,
|
||||||
|
@SerialName("ImageClass") val imageClass: String? = null,
|
||||||
|
)
|
|
@ -0,0 +1,85 @@
|
||||||
|
package eu.kanade.tachiyomi.lib.speedbinb
|
||||||
|
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import eu.kanade.tachiyomi.lib.speedbinb.descrambler.PtBinbDescramblerA
|
||||||
|
import eu.kanade.tachiyomi.lib.speedbinb.descrambler.PtBinbDescramblerF
|
||||||
|
import eu.kanade.tachiyomi.lib.speedbinb.descrambler.PtImgDescrambler
|
||||||
|
import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptor
|
||||||
|
import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptorHelper
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class SpeedBinbInterceptor(private val json: Json) : Interceptor {
|
||||||
|
|
||||||
|
private val textInterceptor by lazy { TextInterceptor() }
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val request = chain.request()
|
||||||
|
val host = request.url.host
|
||||||
|
val filename = request.url.pathSegments.last()
|
||||||
|
val fragment = request.url.fragment
|
||||||
|
|
||||||
|
return when {
|
||||||
|
host == TextInterceptorHelper.HOST -> textInterceptor.intercept(chain)
|
||||||
|
filename.endsWith(".ptimg.json") -> interceptPtImg(chain, request)
|
||||||
|
fragment == null -> chain.proceed(request)
|
||||||
|
fragment.startsWith("ptbinb,") -> interceptPtBinB(chain, request)
|
||||||
|
else -> chain.proceed(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun interceptPtImg(chain: Interceptor.Chain, request: Request): Response {
|
||||||
|
val response = chain.proceed(request)
|
||||||
|
val metadata = json.decodeFromString<PtImg>(response.body.string())
|
||||||
|
val imageUrl = request.url.newBuilder()
|
||||||
|
.setPathSegment(request.url.pathSize - 1, metadata.resources.i.src)
|
||||||
|
.build()
|
||||||
|
val imageResponse = chain.proceed(
|
||||||
|
request.newBuilder().url(imageUrl).build(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (metadata.translations.isEmpty()) {
|
||||||
|
return imageResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
val image = BitmapFactory.decodeStream(imageResponse.body.byteStream())
|
||||||
|
val descrambler = PtImgDescrambler(metadata)
|
||||||
|
return imageResponse.newBuilder()
|
||||||
|
.body(descrambler.descrambleImage(image)!!.toResponseBody(JPEG_MEDIA_TYPE))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun interceptPtBinB(chain: Interceptor.Chain, request: Request): Response {
|
||||||
|
val response = chain.proceed(request)
|
||||||
|
val fragment = request.url.fragment!!
|
||||||
|
val (s, u) = fragment.removePrefix("ptbinb,").split(",", limit = 2)
|
||||||
|
|
||||||
|
if (s.isEmpty() && u.isEmpty()) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
val imageData = response.body.bytes()
|
||||||
|
val image = BitmapFactory.decodeByteArray(imageData, 0, imageData.size)
|
||||||
|
val descrambler = if (s[0] == '=' && u[0] == '=') {
|
||||||
|
PtBinbDescramblerF(s, u, image.width, image.height)
|
||||||
|
} else if (NUMERIC_CHARACTERS.contains(s[0]) && NUMERIC_CHARACTERS.contains(u[0])) {
|
||||||
|
PtBinbDescramblerA(s, u, image.width, image.height)
|
||||||
|
} else {
|
||||||
|
throw IOException("Cannot select descrambler for key pair s=$s, u=$u")
|
||||||
|
}
|
||||||
|
val descrambled = descrambler.descrambleImage(image) ?: imageData
|
||||||
|
|
||||||
|
return response.newBuilder()
|
||||||
|
.body(descrambled.toResponseBody(JPEG_MEDIA_TYPE))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val NUMERIC_CHARACTERS = "0123456789"
|
||||||
|
private val JPEG_MEDIA_TYPE = "image/jpeg".toMediaType()
|
|
@ -0,0 +1,197 @@
|
||||||
|
package eu.kanade.tachiyomi.lib.speedbinb
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptorHelper
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SpeedBinb is a reader for various Japanese manga sites.
|
||||||
|
*
|
||||||
|
* Versions (`SpeedBinb.VERSION` in DevTools console):
|
||||||
|
* - Minimum version tested: `1.6650.0001`
|
||||||
|
* - Maximum version tested: `1.6930.1101`
|
||||||
|
*
|
||||||
|
* These versions are only for reference purposes, and does not reflect the actual range
|
||||||
|
* of versions this class can scrape.
|
||||||
|
*/
|
||||||
|
class SpeedBinbReader(
|
||||||
|
private val client: OkHttpClient,
|
||||||
|
private val headers: Headers,
|
||||||
|
private val json: Json,
|
||||||
|
private val highQualityMode: Boolean = false,
|
||||||
|
) {
|
||||||
|
private val isInterceptorAdded by lazy {
|
||||||
|
client.interceptors.filterIsInstance<SpeedBinbInterceptor>().isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pageListParse(response: Response): List<Page> =
|
||||||
|
pageListParse(response.asJsoup())
|
||||||
|
|
||||||
|
fun pageListParse(document: Document): List<Page> {
|
||||||
|
// We throw here instead of in the `init {}` block because extensions that fail
|
||||||
|
// to load just mysteriously disappears from the extension list, no errors no nothing.
|
||||||
|
if (!isInterceptorAdded) {
|
||||||
|
throw Exception("SpeedBinbInterceptor was not added to the client.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val readerUrl = document.location().toHttpUrl()
|
||||||
|
val content = document.selectFirst("#content")!!
|
||||||
|
|
||||||
|
if (!content.hasAttr("data-ptbinb")) {
|
||||||
|
return content.select("[data-ptimg]").mapIndexed { i, it ->
|
||||||
|
Page(i, imageUrl = it.absUrl("data-ptimg"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val cid = content.attr("data-ptbinb-cid")
|
||||||
|
.ifEmpty { readerUrl.queryParameter("cid") }
|
||||||
|
?: throw Exception("Could not find chapter ID")
|
||||||
|
val sharedKey = generateSharedKey(cid)
|
||||||
|
val contentInfoUrl = content.absUrl("data-ptbinb").toHttpUrl().newBuilder()
|
||||||
|
.copyKeyParametersFrom(readerUrl)
|
||||||
|
.setQueryParameter("cid", cid)
|
||||||
|
.setQueryParameter("k", sharedKey)
|
||||||
|
.setQueryParameter("dmytime", System.currentTimeMillis().toString())
|
||||||
|
.build()
|
||||||
|
val contentInfo = client.newCall(GET(contentInfoUrl, headers)).execute().parseAs<BibContentInfo>()
|
||||||
|
|
||||||
|
if (contentInfo.result != 1) {
|
||||||
|
throw Exception("Failed to execute bibGetCntntInfo API.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentInfo.items.isEmpty()) {
|
||||||
|
throw Exception("There is no item.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val contentItem = contentInfo.items[0]
|
||||||
|
val ctbl = json.decodeFromString<List<String>>(decodeScrambleTable(cid, sharedKey, contentItem.ctbl))
|
||||||
|
val ptbl = json.decodeFromString<List<String>>(decodeScrambleTable(cid, sharedKey, contentItem.ptbl))
|
||||||
|
val sbcUrl = contentItem.getSbcUrl(readerUrl, cid)
|
||||||
|
val sbcData = client.newCall(GET(sbcUrl, headers)).execute().body.string().let {
|
||||||
|
val raw = if (contentItem.serverType == ServerType.DIRECT) {
|
||||||
|
it.substringAfter("DataGet_Content(").substringBeforeLast(")")
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
|
||||||
|
json.decodeFromString<SBCContent>(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sbcData.result != 1) {
|
||||||
|
throw Exception("Failed to fetch content")
|
||||||
|
}
|
||||||
|
|
||||||
|
val isSingleQuality = sbcData.imageClass == "singlequality"
|
||||||
|
val ttx = Jsoup.parseBodyFragment(sbcData.ttx, document.location())
|
||||||
|
val pageBaseUrl = when (contentItem.serverType) {
|
||||||
|
ServerType.DIRECT, ServerType.REST -> contentItem.contentServer
|
||||||
|
ServerType.SBC -> sbcUrl.replaceFirst("/sbcGetCntnt.php", "/sbcGetImg.php")
|
||||||
|
else -> throw UnsupportedOperationException("Unsupported ServerType value ${contentItem.serverType}")
|
||||||
|
}.toHttpUrl()
|
||||||
|
val pages = ttx.select("t-case:first-of-type t-img").mapIndexed { i, it ->
|
||||||
|
val src = it.attr("src")
|
||||||
|
val keyPair = determineKeyPair(src, ptbl, ctbl)
|
||||||
|
val fragment = "ptbinb,${keyPair.first},${keyPair.second}"
|
||||||
|
val imageUrl = pageBaseUrl.newBuilder()
|
||||||
|
.buildImageUrl(
|
||||||
|
readerUrl,
|
||||||
|
src,
|
||||||
|
contentItem,
|
||||||
|
isSingleQuality,
|
||||||
|
highQualityMode,
|
||||||
|
)
|
||||||
|
.fragment(fragment)
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
Page(i, imageUrl = imageUrl)
|
||||||
|
}.toMutableList()
|
||||||
|
|
||||||
|
// This is probably the silliest use of TextInterceptor ever.
|
||||||
|
//
|
||||||
|
// If chapter purchases are enabled, and there's a link to purchase the current chapter,
|
||||||
|
// we add in the purchase URL as the last page.
|
||||||
|
val buyIconPosition = document.selectFirst("script:containsData(Config.LoginBuyIconPosition)")
|
||||||
|
?.data()
|
||||||
|
?.substringAfter("Config.LoginBuyIconPosition=")
|
||||||
|
?.substringBefore(";")
|
||||||
|
?.trim()
|
||||||
|
?: "-1"
|
||||||
|
val enableBuying = buyIconPosition != "-1"
|
||||||
|
|
||||||
|
if (enableBuying && contentItem.viewMode != ViewMode.COMMERCIAL && !contentItem.shopUrl.isNullOrEmpty()) {
|
||||||
|
pages.add(
|
||||||
|
Page(pages.size, imageUrl = TextInterceptorHelper.createUrl("", "購入: ${contentItem.shopUrl}")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> Response.parseAs(): T =
|
||||||
|
json.decodeFromString(body.string())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun HttpUrl.Builder.buildImageUrl(
|
||||||
|
readerUrl: HttpUrl,
|
||||||
|
src: String,
|
||||||
|
contentItem: BibContentItem,
|
||||||
|
isSingleQuality: Boolean,
|
||||||
|
highQualityMode: Boolean,
|
||||||
|
) = apply {
|
||||||
|
when (contentItem.serverType) {
|
||||||
|
ServerType.DIRECT -> {
|
||||||
|
val filename = when {
|
||||||
|
isSingleQuality -> "M.jpg"
|
||||||
|
highQualityMode -> "M_H.jpg"
|
||||||
|
else -> "M_L.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
addPathSegments(src)
|
||||||
|
addPathSegment(filename)
|
||||||
|
contentItem.contentDate?.let { setQueryParameter("dmytime", it) }
|
||||||
|
}
|
||||||
|
ServerType.REST -> {
|
||||||
|
addPathSegment("img")
|
||||||
|
addPathSegments(src)
|
||||||
|
if (!isSingleQuality && !highQualityMode) {
|
||||||
|
setQueryParameter("q", "1")
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem.contentDate?.let { setQueryParameter("dmytime", it) }
|
||||||
|
copyKeyParametersFrom(readerUrl)
|
||||||
|
}
|
||||||
|
ServerType.SBC -> {
|
||||||
|
setQueryParameter("src", src)
|
||||||
|
contentItem.requestToken?.let { setQueryParameter("p", it) }
|
||||||
|
|
||||||
|
if (!isSingleQuality) {
|
||||||
|
setQueryParameter("q", if (highQualityMode) "0" else "1")
|
||||||
|
}
|
||||||
|
|
||||||
|
setQueryParameter("vm", contentItem.viewMode.toString())
|
||||||
|
contentItem.contentDate?.let { setQueryParameter("dmytime", it) }
|
||||||
|
copyKeyParametersFrom(readerUrl)
|
||||||
|
}
|
||||||
|
else -> throw UnsupportedOperationException("Unsupported ServerType value ${contentItem.serverType}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun HttpUrl.Builder.copyKeyParametersFrom(url: HttpUrl): HttpUrl.Builder {
|
||||||
|
for (i in 0..9) {
|
||||||
|
url.queryParameter("u$i")?.let {
|
||||||
|
setQueryParameter("u$i", it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
|
@ -0,0 +1,301 @@
|
||||||
|
package eu.kanade.tachiyomi.lib.speedbinb.descrambler
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.lib.speedbinb.PtImgTranslation
|
||||||
|
|
||||||
|
private val PTBINBF_REGEX = Regex("""^=([0-9]+)-([0-9]+)([-+])([0-9]+)-([-_0-9A-Za-z]+)$""")
|
||||||
|
private const val PTBINBF_CHAR_LOOKUP = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||||
|
private const val PTBINBA_CHAR_LOOKUP = "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ"
|
||||||
|
|
||||||
|
abstract class PtBinbDescrambler(
|
||||||
|
val s: String,
|
||||||
|
val u: String,
|
||||||
|
val width: Int,
|
||||||
|
val height: Int,
|
||||||
|
) : SpeedBinbDescrambler()
|
||||||
|
|
||||||
|
class PtBinbDescramblerF(s: String, u: String, width: Int, height: Int) : PtBinbDescrambler(s, u, width, height) {
|
||||||
|
|
||||||
|
private var widthPieces: Int = 0
|
||||||
|
private var heightPieces: Int = 0
|
||||||
|
private var piecePadding: Int = 0
|
||||||
|
private lateinit var hDstPosLookup: List<Int>
|
||||||
|
private lateinit var wDstPosLookup: List<Int>
|
||||||
|
private lateinit var hPosLookup: List<Int>
|
||||||
|
private lateinit var wPosLookup: List<Int>
|
||||||
|
private var pieceDest: List<Int>? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Kotlin init blocks don't allow early returns...
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun init() {
|
||||||
|
val srcData = PTBINBF_REGEX.matchEntire(s)?.groupValues
|
||||||
|
val dstData = PTBINBF_REGEX.matchEntire(u)?.groupValues
|
||||||
|
|
||||||
|
if (
|
||||||
|
dstData == null ||
|
||||||
|
srcData == null ||
|
||||||
|
dstData[1] != srcData[1] ||
|
||||||
|
dstData[2] != srcData[2] ||
|
||||||
|
dstData[4] != srcData[4] ||
|
||||||
|
dstData[3] != "+" ||
|
||||||
|
srcData[3] != "-"
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
widthPieces = dstData[1].toInt()
|
||||||
|
heightPieces = dstData[2].toInt()
|
||||||
|
piecePadding = dstData[4].toInt()
|
||||||
|
|
||||||
|
if (widthPieces < 8 || heightPieces < 8 || widthPieces * heightPieces < 64) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val e = widthPieces + heightPieces + widthPieces * heightPieces
|
||||||
|
|
||||||
|
if (dstData[5].length != e || srcData[5].length != e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val srcTnp = decodePieceData(srcData[5])
|
||||||
|
val dstTnp = decodePieceData(dstData[5])
|
||||||
|
|
||||||
|
hDstPosLookup = dstTnp.hPos
|
||||||
|
wDstPosLookup = dstTnp.wPos
|
||||||
|
hPosLookup = srcTnp.hPos
|
||||||
|
wPosLookup = srcTnp.wPos
|
||||||
|
pieceDest = buildList(widthPieces * heightPieces) {
|
||||||
|
for (i in 0 until widthPieces * heightPieces) {
|
||||||
|
add(dstTnp.pieces[srcTnp.pieces[i]])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isScrambled() =
|
||||||
|
pieceDest != null
|
||||||
|
|
||||||
|
override fun canDescramble(): Boolean {
|
||||||
|
val i = 2 * widthPieces * piecePadding
|
||||||
|
val n = 2 * heightPieces * piecePadding
|
||||||
|
|
||||||
|
return width >= 64 + i && height >= 64 + n && width * height >= (320 + i) * (320 + n)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCanvasDimensions(): Pair<Int, Int> {
|
||||||
|
return if (canDescramble()) {
|
||||||
|
Pair(
|
||||||
|
width - 2 * widthPieces * piecePadding,
|
||||||
|
height - 2 * heightPieces * piecePadding,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Pair(width, height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDescrambleCoords(): List<PtImgTranslation> {
|
||||||
|
val pieceDest = this.pieceDest
|
||||||
|
|
||||||
|
if (!isScrambled() || pieceDest == null) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canDescramble()) {
|
||||||
|
return listOf(
|
||||||
|
PtImgTranslation(0, 0, width, height, 0, 0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val canvasWidth = width - 2 * widthPieces * piecePadding
|
||||||
|
val canvasHeight = height - 2 * heightPieces * piecePadding
|
||||||
|
val pieceWidth = (canvasWidth + widthPieces - 1).div(widthPieces)
|
||||||
|
val remainderWidth = canvasWidth - (widthPieces - 1) * pieceWidth
|
||||||
|
val pieceHeight = (canvasHeight + heightPieces - 1).div(heightPieces)
|
||||||
|
val remainderHeight = canvasHeight - (heightPieces - 1) * pieceHeight
|
||||||
|
|
||||||
|
return buildList(widthPieces * heightPieces) {
|
||||||
|
for (o in 0 until widthPieces * heightPieces) {
|
||||||
|
val hPos = o % widthPieces
|
||||||
|
val wPos = o.div(widthPieces)
|
||||||
|
val hDstPos = pieceDest[o] % widthPieces
|
||||||
|
val wDstPos = pieceDest[o].div(widthPieces)
|
||||||
|
|
||||||
|
add(
|
||||||
|
PtImgTranslation(
|
||||||
|
xsrc = piecePadding + hPos * (pieceWidth + 2 * piecePadding) + if (hPosLookup[wPos] < hPos) remainderWidth - pieceWidth else 0,
|
||||||
|
ysrc = piecePadding + wPos * (pieceHeight + 2 * piecePadding) + if (wPosLookup[hPos] < wPos) remainderHeight - pieceHeight else 0,
|
||||||
|
width = if (hPosLookup[wPos] == hPos) remainderWidth else pieceWidth,
|
||||||
|
height = if (wPosLookup[hPos] == wPos) remainderHeight else pieceHeight,
|
||||||
|
xdest = hDstPos * pieceWidth + if (hDstPosLookup[wDstPos] < hDstPos) remainderWidth - pieceWidth else 0,
|
||||||
|
ydest = wDstPos * pieceHeight + if (wDstPosLookup[hDstPos] < wDstPos) remainderHeight - pieceHeight else 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decodePieceData(key: String): TNP {
|
||||||
|
val wPos = buildList(widthPieces) {
|
||||||
|
for (i in 0 until widthPieces) {
|
||||||
|
add(PTBINBF_CHAR_LOOKUP.indexOf(key[i]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val hPos = buildList(heightPieces) {
|
||||||
|
for (i in 0 until heightPieces) {
|
||||||
|
add(PTBINBF_CHAR_LOOKUP.indexOf(key[widthPieces + i]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val pieces = buildList(widthPieces * heightPieces) {
|
||||||
|
for (i in 0 until widthPieces * heightPieces) {
|
||||||
|
add(PTBINBF_CHAR_LOOKUP.indexOf(key[widthPieces + heightPieces + i]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return TNP(wPos, hPos, pieces)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TNP(val wPos: List<Int>, val hPos: List<Int>, val pieces: List<Int>)
|
||||||
|
}
|
||||||
|
|
||||||
|
class PtBinbDescramblerA(s: String, u: String, width: Int, height: Int) : PtBinbDescrambler(s, u, width, height) {
|
||||||
|
|
||||||
|
private var srcPieces: PieceCollection? = null
|
||||||
|
|
||||||
|
private var dstPieces: PieceCollection? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
val srcPieces = calculatePieces(u)
|
||||||
|
val dstPieces = calculatePieces(s)
|
||||||
|
|
||||||
|
if (
|
||||||
|
srcPieces != null &&
|
||||||
|
dstPieces != null &&
|
||||||
|
srcPieces.ndx == dstPieces.ndx &&
|
||||||
|
srcPieces.ndy == dstPieces.ndy
|
||||||
|
) {
|
||||||
|
this.srcPieces = srcPieces
|
||||||
|
this.dstPieces = dstPieces
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isScrambled() =
|
||||||
|
srcPieces != null && dstPieces != null
|
||||||
|
|
||||||
|
override fun canDescramble(): Boolean =
|
||||||
|
width >= 64 && height >= 64 && width * height >= 102400
|
||||||
|
|
||||||
|
override fun getCanvasDimensions(): Pair<Int, Int> =
|
||||||
|
Pair(width, height)
|
||||||
|
|
||||||
|
override fun getDescrambleCoords(): List<PtImgTranslation> {
|
||||||
|
if (!isScrambled()) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canDescramble()) {
|
||||||
|
return listOf(
|
||||||
|
PtImgTranslation(0, 0, width, height, 0, 0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val srcPieces = this.srcPieces!!
|
||||||
|
val dstPieces = this.dstPieces!!
|
||||||
|
|
||||||
|
return buildList(srcPieces.piece.size + 2) {
|
||||||
|
val n = width - width % 8
|
||||||
|
val pieceWidth = (n - 1).div(7) - (n - 1).div(7) % 8
|
||||||
|
val e = n - 7 * pieceWidth
|
||||||
|
val s = height - height % 8
|
||||||
|
val pieceHeight = (s - 1).div(7) - (s - 1).div(7) % 8
|
||||||
|
val u = s - 7 * pieceHeight
|
||||||
|
|
||||||
|
for (i in srcPieces.piece.indices) {
|
||||||
|
val src = srcPieces.piece[i]
|
||||||
|
val dst = dstPieces.piece[i]
|
||||||
|
|
||||||
|
add(
|
||||||
|
PtImgTranslation(
|
||||||
|
xsrc = src.x.div(2) * pieceWidth + src.x % 2 * e,
|
||||||
|
ysrc = src.y.div(2) * pieceHeight + src.y % 2 * u,
|
||||||
|
width = src.w.div(2) * pieceWidth + src.w % 2 * e,
|
||||||
|
height = src.h.div(2) * pieceHeight + src.h % 2 * u,
|
||||||
|
xdest = dst.x.div(2) * pieceWidth + dst.x % 2 * e,
|
||||||
|
ydest = dst.y.div(2) * pieceHeight + dst.y % 2 * u,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val l = pieceWidth * (srcPieces.ndx - 1) + e
|
||||||
|
val v = pieceHeight * (srcPieces.ndy - 1) + u
|
||||||
|
|
||||||
|
if (l < width) {
|
||||||
|
add(
|
||||||
|
PtImgTranslation(l, 0, width - l, v, l, 0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v < height) {
|
||||||
|
add(
|
||||||
|
PtImgTranslation(0, v, width, height - v, 0, v),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun calculatePieces(key: String): PieceCollection? {
|
||||||
|
if (key.isEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val parts = key.split("-")
|
||||||
|
|
||||||
|
if (parts.size != 3) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val ndx = parts[0].toInt()
|
||||||
|
val ndy = parts[1].toInt()
|
||||||
|
val e = parts[2]
|
||||||
|
|
||||||
|
if (ndx * ndy * 2 != e.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val pieces = buildList(ndx * ndy) {
|
||||||
|
val a = (ndx - 1) * (ndy - 1) - 1
|
||||||
|
val f = ndx - 1 + a
|
||||||
|
val c = ndy - 1 + f
|
||||||
|
val l = 1 + c
|
||||||
|
var w = 0
|
||||||
|
var h = 0
|
||||||
|
|
||||||
|
for (d in 0 until ndx * ndy) {
|
||||||
|
val x = PTBINBA_CHAR_LOOKUP.indexOf(e[2 * d])
|
||||||
|
val y = PTBINBA_CHAR_LOOKUP.indexOf(e[2 * d + 1])
|
||||||
|
|
||||||
|
if (d <= a) {
|
||||||
|
h = 2
|
||||||
|
w = 2
|
||||||
|
} else if (d <= f) {
|
||||||
|
h = 1
|
||||||
|
w = 2
|
||||||
|
} else if (d <= c) {
|
||||||
|
h = 2
|
||||||
|
w = 1
|
||||||
|
} else if (d <= l) {
|
||||||
|
h = 1
|
||||||
|
w = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
add(Piece(x, y, w, h))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PieceCollection(ndx, ndy, pieces)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Piece(val x: Int, val y: Int, val w: Int, val h: Int)
|
||||||
|
|
||||||
|
private class PieceCollection(val ndx: Int, val ndy: Int, val piece: List<Piece>)
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package eu.kanade.tachiyomi.lib.speedbinb.descrambler
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.lib.speedbinb.PtImg
|
||||||
|
|
||||||
|
class PtImgDescrambler(private val metadata: PtImg) : SpeedBinbDescrambler() {
|
||||||
|
override fun isScrambled() = metadata.translations.isNotEmpty()
|
||||||
|
|
||||||
|
override fun canDescramble() = metadata.translations.isNotEmpty()
|
||||||
|
|
||||||
|
override fun getCanvasDimensions() = Pair(metadata.views[0].width, metadata.views[0].height)
|
||||||
|
|
||||||
|
override fun getDescrambleCoords() = metadata.translations
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package eu.kanade.tachiyomi.lib.speedbinb.descrambler
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Rect
|
||||||
|
import eu.kanade.tachiyomi.lib.speedbinb.PtImgTranslation
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
|
abstract class SpeedBinbDescrambler {
|
||||||
|
abstract fun isScrambled(): Boolean
|
||||||
|
abstract fun canDescramble(): Boolean
|
||||||
|
abstract fun getCanvasDimensions(): Pair<Int, Int>
|
||||||
|
abstract fun getDescrambleCoords(): List<PtImgTranslation>
|
||||||
|
|
||||||
|
open fun descrambleImage(image: Bitmap): ByteArray? {
|
||||||
|
if (!isScrambled()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val (width, height) = getCanvasDimensions()
|
||||||
|
val result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = Canvas(result)
|
||||||
|
|
||||||
|
getDescrambleCoords().forEach {
|
||||||
|
val src = Rect(it.xsrc, it.ysrc, it.xsrc + it.width, it.ysrc + it.height)
|
||||||
|
val dst = Rect(it.xdest, it.ydest, it.xdest + it.width, it.ydest + it.height)
|
||||||
|
|
||||||
|
canvas.drawBitmap(image, src, dst, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ByteArrayOutputStream()
|
||||||
|
.also {
|
||||||
|
result.compress(Bitmap.CompressFormat.JPEG, 90, it)
|
||||||
|
}
|
||||||
|
.toByteArray()
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,12 +18,6 @@ import okhttp3.Response
|
||||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
class TextInterceptor : Interceptor {
|
|
||||||
// With help from:
|
|
||||||
// https://github.com/tachiyomiorg/tachiyomi-extensions/pull/13304#issuecomment-1234532897
|
|
||||||
// https://medium.com/over-engineering/drawing-multiline-text-to-canvas-on-android-9b98f0bfa16a
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
// Designer values:
|
// Designer values:
|
||||||
private const val WIDTH: Int = 1000
|
private const val WIDTH: Int = 1000
|
||||||
private const val X_PADDING: Float = 50f
|
private const val X_PADDING: Float = 50f
|
||||||
|
@ -35,15 +29,18 @@ class TextInterceptor : Interceptor {
|
||||||
|
|
||||||
// No need to touch this one:
|
// No need to touch this one:
|
||||||
private const val HOST = TextInterceptorHelper.HOST
|
private const val HOST = TextInterceptorHelper.HOST
|
||||||
}
|
|
||||||
|
|
||||||
|
class TextInterceptor : Interceptor {
|
||||||
|
// With help from:
|
||||||
|
// https://github.com/tachiyomiorg/tachiyomi-extensions/pull/13304#issuecomment-1234532897
|
||||||
|
// https://medium.com/over-engineering/drawing-multiline-text-to-canvas-on-android-9b98f0bfa16a
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val request = chain.request()
|
val request = chain.request()
|
||||||
val url = request.url
|
val url = request.url
|
||||||
if (url.host != HOST) return chain.proceed(request)
|
if (url.host != HOST) return chain.proceed(request)
|
||||||
|
|
||||||
val creator = textFixer("Author's Notes from ${url.pathSegments[0]}")
|
val heading = url.pathSegments[0].takeIf { it.isNotEmpty() }?.let {
|
||||||
val story = textFixer(url.pathSegments[1])
|
val title = textFixer(url.pathSegments[0])
|
||||||
|
|
||||||
// Heading
|
// Heading
|
||||||
val paintHeading = TextPaint().apply {
|
val paintHeading = TextPaint().apply {
|
||||||
|
@ -54,10 +51,14 @@ class TextInterceptor : Interceptor {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
val heading = StaticLayout(
|
StaticLayout(
|
||||||
creator, paintHeading, (WIDTH - 2 * X_PADDING).toInt(),
|
title, paintHeading, (WIDTH - 2 * X_PADDING).toInt(),
|
||||||
Layout.Alignment.ALIGN_NORMAL, SPACING_MULT, SPACING_ADD, true
|
Layout.Alignment.ALIGN_NORMAL, SPACING_MULT, SPACING_ADD, true
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = url.pathSegments[1].takeIf { it.isNotEmpty() }?.let {
|
||||||
|
val story = textFixer(it)
|
||||||
|
|
||||||
// Body
|
// Body
|
||||||
val paintBody = TextPaint().apply {
|
val paintBody = TextPaint().apply {
|
||||||
|
@ -68,19 +69,22 @@ class TextInterceptor : Interceptor {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
val body = StaticLayout(
|
StaticLayout(
|
||||||
story, paintBody, (WIDTH - 2 * X_PADDING).toInt(),
|
story, paintBody, (WIDTH - 2 * X_PADDING).toInt(),
|
||||||
Layout.Alignment.ALIGN_NORMAL, SPACING_MULT, SPACING_ADD, true
|
Layout.Alignment.ALIGN_NORMAL, SPACING_MULT, SPACING_ADD, true
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Image building
|
// Image building
|
||||||
val imgHeight: Int = (heading.height + body.height + 2 * Y_PADDING).toInt()
|
val headingHeight = heading?.height ?: 0
|
||||||
|
val bodyHeight = body?.height ?: 0
|
||||||
|
val imgHeight: Int = (headingHeight + bodyHeight + 2 * Y_PADDING).toInt()
|
||||||
val bitmap: Bitmap = Bitmap.createBitmap(WIDTH, imgHeight, Bitmap.Config.ARGB_8888)
|
val bitmap: Bitmap = Bitmap.createBitmap(WIDTH, imgHeight, Bitmap.Config.ARGB_8888)
|
||||||
|
|
||||||
Canvas(bitmap).apply {
|
Canvas(bitmap).apply {
|
||||||
drawColor(Color.WHITE)
|
drawColor(Color.WHITE)
|
||||||
heading.draw(this, X_PADDING, Y_PADDING)
|
heading?.draw(this, X_PADDING, Y_PADDING)
|
||||||
body.draw(this, X_PADDING, Y_PADDING + heading.height.toFloat())
|
body?.draw(this, X_PADDING, Y_PADDING + headingHeight.toFloat())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Image converting & returning
|
// Image converting & returning
|
||||||
|
@ -119,7 +123,7 @@ object TextInterceptorHelper {
|
||||||
|
|
||||||
const val HOST = "tachiyomi-lib-textinterceptor"
|
const val HOST = "tachiyomi-lib-textinterceptor"
|
||||||
|
|
||||||
fun createUrl(creator: String, text: String): String {
|
fun createUrl(title: String, text: String): String {
|
||||||
return "http://$HOST/" + Uri.encode(creator) + "/" + Uri.encode(text)
|
return "http://$HOST/" + Uri.encode(title) + "/" + Uri.encode(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -129,9 +129,9 @@ class ComicFury(
|
||||||
response.request.url.toString(),
|
response.request.url.toString(),
|
||||||
TextInterceptorHelper.createUrl(
|
TextInterceptorHelper.createUrl(
|
||||||
jsp.selectFirst("a.is--comment-author")?.ownText()
|
jsp.selectFirst("a.is--comment-author")?.ownText()
|
||||||
?: "Error No Author For Comment Found",
|
?.let { "Author's Notes from $it" }
|
||||||
jsp.selectFirst("div.is--comment-content")?.html()
|
.orEmpty(),
|
||||||
?: "Error No Comment Content Found",
|
jsp.selectFirst("div.is--comment-content")?.html().orEmpty(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -65,7 +65,7 @@ open class WebtoonsSrc(
|
||||||
pages = pages + Page(
|
pages = pages + Page(
|
||||||
pages.size,
|
pages.size,
|
||||||
"",
|
"",
|
||||||
TextInterceptorHelper.createUrl(creator, note),
|
TextInterceptorHelper.createUrl("Author's Notes from $creator", note),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,7 +114,7 @@ class GrrlPower(
|
||||||
val text = soup.getElementsByClass("entry").html()
|
val text = soup.getElementsByClass("entry").html()
|
||||||
|
|
||||||
if (text.isNotEmpty() && showAuthorsNotesPref()) {
|
if (text.isNotEmpty() && showAuthorsNotesPref()) {
|
||||||
pages.add(Page(1, "", TextInterceptorHelper.createUrl(comicAuthor, text)))
|
pages.add(Page(1, "", TextInterceptorHelper.createUrl("Author's Notes from $comicAuthor", text)))
|
||||||
}
|
}
|
||||||
return pages
|
return pages
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,7 +91,7 @@ class QuestionableContent : ParsedHttpSource(), ConfigurableSource {
|
||||||
if (showAuthorsNotesPref()) {
|
if (showAuthorsNotesPref()) {
|
||||||
val str = document.selectFirst("#newspost")?.html()
|
val str = document.selectFirst("#newspost")?.html()
|
||||||
if (!str.isNullOrEmpty()) {
|
if (!str.isNullOrEmpty()) {
|
||||||
pages.add(Page(pages.size, "", TextInterceptorHelper.createUrl(AUTHOR, str)))
|
pages.add(Page(pages.size, "", TextInterceptorHelper.createUrl("Author's Notes from $AUTHOR", str)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return pages
|
return pages
|
||||||
|
|
|
@ -363,7 +363,7 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() {
|
||||||
pages = pages + Page(
|
pages = pages + Page(
|
||||||
pages.size,
|
pages.size,
|
||||||
"",
|
"",
|
||||||
TextInterceptorHelper.createUrl(creator, episodeStory),
|
TextInterceptorHelper.createUrl("Author's Notes from $creator", episodeStory),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue