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 <vetle.ledaal@gmail.com>

* Fix

* Lint Fix?

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
This commit is contained in:
KenjieDec 2024-07-22 14:20:43 +07:00 committed by GitHub
parent c6f6162a3b
commit 6da63a37fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 80 additions and 5 deletions

View File

@ -1,7 +1,7 @@
ext {
extName = 'Hitomi'
extClass = '.HitomiFactory'
extVersionCode = 31
extVersionCode = 32
isNsfw = true
}

View File

@ -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<Application>().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<Int>.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"
}
}

View File

@ -21,6 +21,9 @@ class Gallery(
@Serializable
class ImageFile(
val hash: String,
val haswebp: Int,
val hasavif: Int,
val hasjxl: Int,
)
@Serializable