mirror of
https://github.com/keiyoushi/extensions-source.git
synced 2024-11-22 10:22:47 +01:00
BiliBiliManga(zh-hans): Fix image load. (#6117)
* BiliBiliManga(zh-hans): Fix image load. * Remove mistype unused field * Remove extra data copy
This commit is contained in:
parent
800649ae74
commit
0a6a5da88a
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<application>
|
<application>
|
||||||
<activity
|
<activity
|
||||||
android:name="eu.kanade.tachiyomi.multisrc.bilibili.BilibiliUrlActivity"
|
android:name=".zh.bilibilimanga.BilibiliUrlActivity"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@android:style/Theme.NoDisplay">
|
android:theme="@android:style/Theme.NoDisplay">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'BILIBILI MANGA'
|
extName = 'BILIBILI MANGA'
|
||||||
extClass = '.BilibiliManga'
|
extClass = '.BilibiliManga'
|
||||||
extVersionCode = 11
|
extVersionCode = 12
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.bilibili
|
package eu.kanade.tachiyomi.extension.zh.bilibilimanga
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import android.util.Base64
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
@ -27,12 +28,18 @@ import okhttp3.OkHttpClient
|
|||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
abstract class Bilibili(
|
abstract class Bilibili(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
@ -44,8 +51,10 @@ abstract class Bilibili(
|
|||||||
|
|
||||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||||
.addInterceptor(::expiredImageTokenIntercept)
|
.addInterceptor(::expiredImageTokenIntercept)
|
||||||
|
.addInterceptor(::decryptImageIntercept)
|
||||||
.rateLimitHost(baseUrl.toHttpUrl(), 1)
|
.rateLimitHost(baseUrl.toHttpUrl(), 1)
|
||||||
.rateLimitHost(CDN_URL.toHttpUrl(), 2)
|
.rateLimitHost(CDN_URL.toHttpUrl(), 2)
|
||||||
|
.rateLimitHost(MODIFIED_CDN_URL.toHttpUrl(), 2)
|
||||||
.rateLimitHost(COVER_CDN_URL.toHttpUrl(), 2)
|
.rateLimitHost(COVER_CDN_URL.toHttpUrl(), 2)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@ -239,7 +248,11 @@ abstract class Bilibili(
|
|||||||
return result.data!!.episodeList.map { ep -> chapterFromObject(ep, result.data.id) }
|
return result.data!!.episodeList.map { ep -> chapterFromObject(ep, result.data.id) }
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun chapterFromObject(episode: BilibiliEpisodeDto, comicId: Int, isUnlocked: Boolean = false): SChapter = SChapter.create().apply {
|
protected open fun chapterFromObject(
|
||||||
|
episode: BilibiliEpisodeDto,
|
||||||
|
comicId: Int,
|
||||||
|
isUnlocked: Boolean = false,
|
||||||
|
): SChapter = SChapter.create().apply {
|
||||||
name = buildString {
|
name = buildString {
|
||||||
if (episode.isPaid && !isUnlocked) {
|
if (episode.isPaid && !isUnlocked) {
|
||||||
append("$EMOJI_LOCKED ")
|
append("$EMOJI_LOCKED ")
|
||||||
@ -297,9 +310,9 @@ abstract class Bilibili(
|
|||||||
val imageTokenRequest = imageTokenRequest(imageUrls)
|
val imageTokenRequest = imageTokenRequest(imageUrls)
|
||||||
val imageTokenResponse = client.newCall(imageTokenRequest).execute()
|
val imageTokenResponse = client.newCall(imageTokenRequest).execute()
|
||||||
val imageTokenResult = imageTokenResponse.parseAs<List<BilibiliPageDto>>()
|
val imageTokenResult = imageTokenResponse.parseAs<List<BilibiliPageDto>>()
|
||||||
|
return imageTokenResult.data!!.zip(imageUrls).mapIndexed { i, pair ->
|
||||||
return imageTokenResult.data!!
|
Page(i, pair.second, pair.first.imageUrl)
|
||||||
.mapIndexed { i, page -> Page(i, "", "${page.url}?token=${page.token}") }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun imageTokenRequest(urls: List<String>): Request {
|
protected open fun imageTokenRequest(urls: List<String>): Request {
|
||||||
@ -370,24 +383,62 @@ abstract class Bilibili(
|
|||||||
return FilterList(filters)
|
return FilterList(filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun expiredImageTokenIntercept(chain: Interceptor.Chain): Response {
|
override fun imageRequest(page: Page): Request {
|
||||||
val response = chain.proceed(chain.request())
|
return super.imageRequest(page).newBuilder().tag(TAG_IMAGE_REQUEST)
|
||||||
|
.tag(TagImagePath::class.java, TagImagePath(page.url)).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decryptImageIntercept(chain: Interceptor.Chain): Response {
|
||||||
|
val request = chain.request()
|
||||||
|
val response = chain.proceed(request)
|
||||||
|
if (response.isSuccessful && request.tag() == TAG_IMAGE_REQUEST) {
|
||||||
|
if (response.body.contentType()?.type == "image") {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
val cpx = request.url.queryParameter("cpx")
|
||||||
|
val iv = Base64.decode(cpx, Base64.DEFAULT).copyOfRange(60, 76)
|
||||||
|
val allBytes = response.body.bytes()
|
||||||
|
val size =
|
||||||
|
ByteBuffer.wrap(allBytes.copyOfRange(1, 5)).order(ByteOrder.BIG_ENDIAN).getInt()
|
||||||
|
val data = allBytes.copyOfRange(5, 5 + size)
|
||||||
|
val key = allBytes.copyOfRange(5 + size, allBytes.size)
|
||||||
|
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||||
|
val ivSpec = IvParameterSpec(iv)
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), ivSpec)
|
||||||
|
val encryptedSize = 20 * 1024 + 16
|
||||||
|
val decryptedSegment = cipher.doFinal(data, 0, encryptedSize.coerceAtMost(data.size))
|
||||||
|
val decryptedData = if (encryptedSize < data.size) {
|
||||||
|
// append remaining data
|
||||||
|
decryptedSegment + data.copyOfRange(encryptedSize, data.size)
|
||||||
|
} else {
|
||||||
|
decryptedSegment
|
||||||
|
}
|
||||||
|
val imageExtension = request.url.encodedPath.substringAfterLast(".", "jpg")
|
||||||
|
return response.newBuilder()
|
||||||
|
.body(decryptedData.toResponseBody("image/$imageExtension".toMediaType())).build()
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun expiredImageTokenIntercept(chain: Interceptor.Chain): Response {
|
||||||
|
val request = chain.request()
|
||||||
|
val response = chain.proceed(request)
|
||||||
// Get a new image token if the current one expired.
|
// Get a new image token if the current one expired.
|
||||||
if (response.code == 403 && chain.request().url.toString().contains(CDN_URL)) {
|
if (response.code == 400 && request.tag() == TAG_IMAGE_REQUEST) {
|
||||||
|
val imagePath = request.tag(TagImagePath::class)
|
||||||
|
if (imagePath?.path.isNullOrEmpty()) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
response.close()
|
response.close()
|
||||||
val imagePath = chain.request().url.toString()
|
val imageTokenRequest = imageTokenRequest(listOf(imagePath!!.path))
|
||||||
.substringAfter(CDN_URL)
|
|
||||||
.substringBefore("?token=")
|
|
||||||
val imageTokenRequest = imageTokenRequest(listOf(imagePath))
|
|
||||||
val imageTokenResponse = chain.proceed(imageTokenRequest)
|
val imageTokenResponse = chain.proceed(imageTokenRequest)
|
||||||
val imageTokenResult = imageTokenResponse.parseAs<List<BilibiliPageDto>>()
|
val imageTokenResult = imageTokenResponse.parseAs<List<BilibiliPageDto>>()
|
||||||
imageTokenResponse.close()
|
imageTokenResponse.close()
|
||||||
|
|
||||||
val newPage = imageTokenResult.data!!.first()
|
val newPage = imageTokenResult.data!!.first()
|
||||||
val newPageUrl = "${newPage.url}?token=${newPage.token}"
|
val newPageUrl = newPage.imageUrl
|
||||||
|
|
||||||
val newRequest = imageRequest(Page(0, "", newPageUrl))
|
val newRequest = imageRequest(Page(0, imagePath.path, newPageUrl))
|
||||||
|
|
||||||
return chain.proceed(newRequest)
|
return chain.proceed(newRequest)
|
||||||
}
|
}
|
||||||
@ -396,7 +447,12 @@ abstract class Bilibili(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val SharedPreferences.chapterImageQuality
|
private val SharedPreferences.chapterImageQuality
|
||||||
get() = when (getString("${IMAGE_QUALITY_PREF_KEY}_$lang", IMAGE_QUALITY_PREF_DEFAULT_VALUE)!!) {
|
get() = when (
|
||||||
|
getString(
|
||||||
|
"${IMAGE_QUALITY_PREF_KEY}_$lang",
|
||||||
|
IMAGE_QUALITY_PREF_DEFAULT_VALUE,
|
||||||
|
)!!
|
||||||
|
) {
|
||||||
"hd" -> "1600w"
|
"hd" -> "1600w"
|
||||||
"sd" -> "1000w"
|
"sd" -> "1000w"
|
||||||
"low" -> "800w_50q"
|
"low" -> "800w_50q"
|
||||||
@ -427,13 +483,17 @@ abstract class Bilibili(
|
|||||||
.getOrNull() ?: 0L
|
.getOrNull() ?: 0L
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class TagImagePath(val path: String)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val CDN_URL = "https://manga.hdslb.com"
|
const val CDN_URL = "https://manga.hdslb.com"
|
||||||
|
const val MODIFIED_CDN_URL = "https://mangaup.hdslb.com"
|
||||||
const val COVER_CDN_URL = "https://i0.hdslb.com"
|
const val COVER_CDN_URL = "https://i0.hdslb.com"
|
||||||
|
|
||||||
const val API_COMIC_V1_COMIC_ENDPOINT = "twirp/comic.v1.Comic"
|
const val API_COMIC_V1_COMIC_ENDPOINT = "twirp/comic.v1.Comic"
|
||||||
|
|
||||||
private const val ACCEPT_JSON = "application/json, text/plain, */*"
|
private const val ACCEPT_JSON = "application/json, text/plain, */*"
|
||||||
|
private const val TAG_IMAGE_REQUEST = "tag_image_request"
|
||||||
|
|
||||||
val JSON_MEDIA_TYPE = "application/json;charset=UTF-8".toMediaType()
|
val JSON_MEDIA_TYPE = "application/json;charset=UTF-8".toMediaType()
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.bilibili
|
package eu.kanade.tachiyomi.extension.zh.bilibilimanga
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
@ -74,7 +74,12 @@ data class BilibiliImageDto(
|
|||||||
data class BilibiliPageDto(
|
data class BilibiliPageDto(
|
||||||
val token: String,
|
val token: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
)
|
@SerialName("complete_url")
|
||||||
|
val completeUrl: String,
|
||||||
|
) {
|
||||||
|
val imageUrl: String
|
||||||
|
get() = completeUrl.ifEmpty { "$url?token=$token" }
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BilibiliAccessTokenCookie(
|
data class BilibiliAccessTokenCookie(
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.bilibili
|
package eu.kanade.tachiyomi.extension.zh.bilibilimanga
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.bilibili
|
package eu.kanade.tachiyomi.extension.zh.bilibilimanga
|
||||||
|
|
||||||
import java.text.DateFormatSymbols
|
import java.text.DateFormatSymbols
|
||||||
import java.text.NumberFormat
|
import java.text.NumberFormat
|
@ -1,9 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.extension.zh.bilibilimanga
|
package eu.kanade.tachiyomi.extension.zh.bilibilimanga
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.bilibili.Bilibili
|
|
||||||
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliComicDto
|
|
||||||
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliIntl
|
|
||||||
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliTag
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.bilibili
|
package eu.kanade.tachiyomi.extension.zh.bilibilimanga
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
Loading…
Reference in New Issue
Block a user