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:
parent
b3a4bf4697
commit
14c5eec0de
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Hitomi'
|
extName = 'Hitomi'
|
||||||
extClass = '.HitomiFactory'
|
extClass = '.HitomiFactory'
|
||||||
extVersionCode = 31
|
extVersionCode = 32
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user