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 Draff
parent b3a4bf4697
commit 14c5eec0de
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
3 changed files with 80 additions and 5 deletions

View File

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

View File

@ -1,7 +1,12 @@
package eu.kanade.tachiyomi.extension.all.hitomi 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.GET
import eu.kanade.tachiyomi.network.await 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.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@ -19,9 +24,14 @@ import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.Call import okhttp3.Call
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
@ -30,13 +40,14 @@ import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.LinkedList import java.util.LinkedList
import java.util.Locale import java.util.Locale
import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@OptIn(ExperimentalUnsignedTypes::class) @OptIn(ExperimentalUnsignedTypes::class)
class Hitomi( class Hitomi(
override val lang: String, override val lang: String,
private val nozomiLang: String, private val nozomiLang: String,
) : HttpSource() { ) : HttpSource(), ConfigurableSource {
override val name = "Hitomi" override val name = "Hitomi"
@ -50,7 +61,14 @@ class Hitomi(
private val json: Json by injectLazy() 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() override fun headersBuilder() = super.headersBuilder()
.set("referer", "$baseUrl/") .set("referer", "$baseUrl/")
@ -488,7 +506,7 @@ class Hitomi(
private suspend fun Gallery.toSManga() = SManga.create().apply { private suspend fun Gallery.toSManga() = SManga.create().apply {
title = this@toSManga.title title = this@toSManga.title
url = galleryurl url = galleryurl
author = groups?.joinToString { it.formatted } author = groups?.joinToString { it.formatted } ?: artists?.joinToString { it.formatted }
artist = artists?.joinToString { it.formatted } artist = artists?.joinToString { it.formatted }
genre = tags?.joinToString { it.formatted } genre = tags?.joinToString { it.formatted }
thumbnail_url = files.first().let { thumbnail_url = files.first().let {
@ -567,14 +585,25 @@ class Hitomi(
gallery.files.mapIndexed { idx, img -> gallery.files.mapIndexed { idx, img ->
val hash = img.hash 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 commonId = commonImageId()
val imageId = imageIdFromHash(hash) val imageId = imageIdFromHash(hash)
val subDomain = 'a' + subdomainOffset(imageId) 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( Page(
idx, idx,
"$baseUrl/reader/$id.html", "$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") 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 popularMangaParse(response: Response) = throw UnsupportedOperationException()
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException() override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
override fun latestUpdatesRequest(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 searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException() override fun searchMangaParse(response: Response) = throw UnsupportedOperationException()
override fun imageUrlParse(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 @Serializable
class ImageFile( class ImageFile(
val hash: String, val hash: String,
val haswebp: Int,
val hasavif: Int,
val hasjxl: Int,
) )
@Serializable @Serializable