From 14c5eec0ded1b83844bea3a4cd39ef52485aaf61 Mon Sep 17 00:00:00 2001 From: KenjieDec <65448230+KenjieDec@users.noreply.github.com> Date: Mon, 22 Jul 2024 14:20:43 +0700 Subject: [PATCH] Hitomi: Add Image Format Preferences (#4101) * Add image filetype preferences * Update extVersionCode * Apply suggestions - Confirm jxl by using its signatures when passing through interceptor - & others * Update HitomiDto.kt * Fix * Fix - Apply Suggestion * Apply suggestion Co-authored-by: Vetle Ledaal * Fix * Lint Fix? --------- Co-authored-by: Vetle Ledaal --- src/all/hitomi/build.gradle | 2 +- .../tachiyomi/extension/all/hitomi/Hitomi.kt | 80 ++++++++++++++++++- .../extension/all/hitomi/HitomiDto.kt | 3 + 3 files changed, 80 insertions(+), 5 deletions(-) diff --git a/src/all/hitomi/build.gradle b/src/all/hitomi/build.gradle index 212498ec9..9614b9576 100644 --- a/src/all/hitomi/build.gradle +++ b/src/all/hitomi/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'Hitomi' extClass = '.HitomiFactory' - extVersionCode = 31 + extVersionCode = 32 isNsfw = true } diff --git a/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/Hitomi.kt b/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/Hitomi.kt index 834508353..106631706 100644 --- a/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/Hitomi.kt +++ b/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/Hitomi.kt @@ -1,7 +1,12 @@ package eu.kanade.tachiyomi.extension.all.hitomi +import android.app.Application +import android.content.SharedPreferences +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.await +import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.Page @@ -19,9 +24,14 @@ import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import okhttp3.CacheControl import okhttp3.Call +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaType import okhttp3.Request import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody import rx.Observable +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.nio.ByteBuffer import java.nio.ByteOrder @@ -30,13 +40,14 @@ import java.text.ParseException import java.text.SimpleDateFormat import java.util.LinkedList import java.util.Locale +import kotlin.math.max import kotlin.math.min @OptIn(ExperimentalUnsignedTypes::class) class Hitomi( override val lang: String, private val nozomiLang: String, -) : HttpSource() { +) : HttpSource(), ConfigurableSource { override val name = "Hitomi" @@ -50,7 +61,14 @@ class Hitomi( private val json: Json by injectLazy() - override val client = network.cloudflareClient + override val client = network.cloudflareClient.newBuilder() + .addInterceptor(::Intercept) + .build() + + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + private fun imageType() = preferences.getString(PREF_IMAGETYPE, "webp")!! override fun headersBuilder() = super.headersBuilder() .set("referer", "$baseUrl/") @@ -488,7 +506,7 @@ class Hitomi( private suspend fun Gallery.toSManga() = SManga.create().apply { title = this@toSManga.title url = galleryurl - author = groups?.joinToString { it.formatted } + author = groups?.joinToString { it.formatted } ?: artists?.joinToString { it.formatted } artist = artists?.joinToString { it.formatted } genre = tags?.joinToString { it.formatted } thumbnail_url = files.first().let { @@ -567,14 +585,25 @@ class Hitomi( gallery.files.mapIndexed { idx, img -> val hash = img.hash + + val typePref = imageType() + val avif = img.hasavif == 1 && typePref == "avif" + val jxl = img.hasjxl == 1 && typePref == "jxl" + val commonId = commonImageId() val imageId = imageIdFromHash(hash) val subDomain = 'a' + subdomainOffset(imageId) + val imageUrl = when { + jxl -> "https://${subDomain}a.$domain/jxl/$commonId$imageId/$hash.jxl" + avif -> "https://${subDomain}a.$domain/avif/$commonId$imageId/$hash.avif" + else -> "https://${subDomain}a.$domain/webp/$commonId$imageId/$hash.webp" + } + Page( idx, "$baseUrl/reader/$id.html", - "https://${subDomain}a.$domain/webp/$commonId$imageId/$hash.webp", + imageUrl, ) } } @@ -657,6 +686,45 @@ class Hitomi( return hash.replace(Regex("""^.*(..)(.)$"""), "$2/$1") } + override fun setupPreferenceScreen(screen: PreferenceScreen) { + ListPreference(screen.context).apply { + key = PREF_IMAGETYPE + title = "Images Type" + entries = arrayOf("webp", "avif", "jxl") + entryValues = arrayOf("webp", "avif", "jxl") + summary = "Clear chapter cache to apply changes" + setDefaultValue("webp") + }.also(screen::addPreference) + } + + private fun List.toBytesList(): ByteArray = this.map { it.toByte() }.toByteArray() + private val signatureOne = listOf(0xFF, 0x0A).toBytesList() + private val signatureTwo = listOf(0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 0x0D, 0x0A, 0x87, 0x0A).toBytesList() + fun ByteArray.startsWith(byteArray: ByteArray): Boolean { + if (this.size < byteArray.size) return false + return this.sliceArray(byteArray.indices).contentEquals(byteArray) + } + + private fun Intercept(chain: Interceptor.Chain): Response { + val response = chain.proceed(chain.request()) + if (response.headers["Content-Type"] != "application/octet-stream") { + return response + } + + val bytesPeek = max(signatureOne.size, signatureTwo.size).toLong() + val bytesArray = response.peekBody(bytesPeek).bytes() + if (!(bytesArray.startsWith(signatureOne) || bytesArray.startsWith(signatureTwo))) { + return response + } + + val type = "image/jxl" + val body = response.body.bytes().toResponseBody(type.toMediaType()) + return response.newBuilder() + .body(body) + .header("Content-Type", type) + .build() + } + override fun popularMangaParse(response: Response) = throw UnsupportedOperationException() override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException() override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException() @@ -664,4 +732,8 @@ class Hitomi( override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException() override fun searchMangaParse(response: Response) = throw UnsupportedOperationException() override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() + + companion object { + const val PREF_IMAGETYPE = "pref_image_type" + } } diff --git a/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/HitomiDto.kt b/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/HitomiDto.kt index 5b45b254a..32f930380 100644 --- a/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/HitomiDto.kt +++ b/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/HitomiDto.kt @@ -21,6 +21,9 @@ class Gallery( @Serializable class ImageFile( val hash: String, + val haswebp: Int, + val hasavif: Int, + val hasjxl: Int, ) @Serializable