Add SpeedBinb reader library (#1316)
* Add SpeedBinb reader library * Make TextInterceptor generic
This commit is contained in:
		
							parent
							
								
									93c5dbc650
								
							
						
					
					
						commit
						a799bf8a5c
					
				
							
								
								
									
										24
									
								
								lib/speedbinb/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								lib/speedbinb/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							| @ -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,32 +18,29 @@ import okhttp3.Response | |||||||
| import okhttp3.ResponseBody.Companion.toResponseBody | import okhttp3.ResponseBody.Companion.toResponseBody | ||||||
| import java.io.ByteArrayOutputStream | import java.io.ByteArrayOutputStream | ||||||
| 
 | 
 | ||||||
|  | // Designer values: | ||||||
|  | private const val WIDTH: Int = 1000 | ||||||
|  | private const val X_PADDING: Float = 50f | ||||||
|  | private const val Y_PADDING: Float = 25f | ||||||
|  | private const val HEADING_FONT_SIZE: Float = 36f | ||||||
|  | private const val BODY_FONT_SIZE: Float = 30f | ||||||
|  | private const val SPACING_MULT: Float = 1.1f | ||||||
|  | private const val SPACING_ADD: Float = 2f | ||||||
|  | 
 | ||||||
|  | // No need to touch this one: | ||||||
|  | private const val HOST = TextInterceptorHelper.HOST | ||||||
|  | 
 | ||||||
| class TextInterceptor : Interceptor { | class TextInterceptor : Interceptor { | ||||||
|     // With help from: |     // With help from: | ||||||
|     // https://github.com/tachiyomiorg/tachiyomi-extensions/pull/13304#issuecomment-1234532897 |     // https://github.com/tachiyomiorg/tachiyomi-extensions/pull/13304#issuecomment-1234532897 | ||||||
|     // https://medium.com/over-engineering/drawing-multiline-text-to-canvas-on-android-9b98f0bfa16a |     // https://medium.com/over-engineering/drawing-multiline-text-to-canvas-on-android-9b98f0bfa16a | ||||||
| 
 |  | ||||||
|     companion object { |  | ||||||
|         // Designer values: |  | ||||||
|         private const val WIDTH: Int = 1000 |  | ||||||
|         private const val X_PADDING: Float = 50f |  | ||||||
|         private const val Y_PADDING: Float = 25f |  | ||||||
|         private const val HEADING_FONT_SIZE: Float = 36f |  | ||||||
|         private const val BODY_FONT_SIZE: Float = 30f |  | ||||||
|         private const val SPACING_MULT: Float = 1.1f |  | ||||||
|         private const val SPACING_ADD: Float = 2f |  | ||||||
| 
 |  | ||||||
|         // No need to touch this one: |  | ||||||
|         private const val HOST = TextInterceptorHelper.HOST |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 beerpsi
						beerpsi