dependency update

This commit is contained in:
tom5079
2025-03-08 15:27:57 -08:00
parent 3ee5e683f4
commit 47d96a6ba9
3 changed files with 93 additions and 72 deletions

View File

@@ -17,7 +17,6 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock.System.now import kotlinx.datetime.Clock.System.now
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
@@ -41,7 +40,7 @@ const val nozomiURLIndexDir = "nozomiurlindex"
data class Suggestion( data class Suggestion(
val tag: SearchQuery.Tag, val tag: SearchQuery.Tag,
val count: Int val count: Int,
) )
fun IntBuffer.toSet(): Set<Int> { fun IntBuffer.toSet(): Set<Int> {
@@ -92,7 +91,7 @@ class ImagePathResolver(ggjs: String) {
class ExpirableEntry<T>( class ExpirableEntry<T>(
private val expiryDuration: Duration, private val expiryDuration: Duration,
private val action: suspend () -> T private val action: suspend () -> T,
) { ) {
private var value: T? = null private var value: T? = null
private var expiresAt: Instant = now() private var expiresAt: Instant = now()
@@ -121,7 +120,8 @@ object HitomiHttpClient {
} }
private val tagIndexVersion = ExpirableEntry(1.minutes) { getIndexVersion("tagindex") } private val tagIndexVersion = ExpirableEntry(1.minutes) { getIndexVersion("tagindex") }
private val galleriesIndexVersion = ExpirableEntry(1.minutes) { getIndexVersion("galleriesindex") } private val galleriesIndexVersion =
ExpirableEntry(1.minutes) { getIndexVersion("galleriesindex") }
private suspend fun getIndexVersion(name: String): String = withContext(Dispatchers.IO) { private suspend fun getIndexVersion(name: String): String = withContext(Dispatchers.IO) {
httpClient.get("https://$domain/$name/version?_=${System.currentTimeMillis()}").bodyAsText() httpClient.get("https://$domain/$name/version?_=${System.currentTimeMillis()}").bodyAsText()
@@ -144,18 +144,18 @@ object HitomiHttpClient {
"galleries" -> "https://$domain/$galleriesIndexDir/galleries.${galleriesIndexVersion.getValue()}.index" "galleries" -> "https://$domain/$galleriesIndexDir/galleries.${galleriesIndexVersion.getValue()}.index"
"languages" -> "https://$domain/$galleriesIndexDir/languages.${galleriesIndexVersion.getValue()}.index" "languages" -> "https://$domain/$galleriesIndexDir/languages.${galleriesIndexVersion.getValue()}.index"
"nozomiurl" -> "https://$domain/$galleriesIndexDir/nozomiurl.${galleriesIndexVersion.getValue()}.index" "nozomiurl" -> "https://$domain/$galleriesIndexDir/nozomiurl.${galleriesIndexVersion.getValue()}.index"
else -> "https://$domain/$indexDir/$field.${HitomiHttpClient.tagIndexVersion.getValue()}.index" else -> "https://$domain/$indexDir/$field.${tagIndexVersion.getValue()}.index"
} }
return Node.decodeNode( return Node.decodeNode(
getURLAtRange(url, address ..< address + maxNodeSize) getURLAtRange(url, address..<address + maxNodeSize)
) )
} }
private suspend fun bSearch( private suspend fun bSearch(
field: String, field: String,
key: Node.Key, key: Node.Key,
node: Node node: Node,
): Node.Data? { ): Node.Data? {
if (node.keys.isEmpty()) { if (node.keys.isEmpty()) {
return null return null
@@ -174,12 +174,13 @@ object HitomiHttpClient {
} }
private suspend fun getGalleryIDsFromData(offset: Long, length: Int): IntBuffer { private suspend fun getGalleryIDsFromData(offset: Long, length: Int): IntBuffer {
val url = "https://$domain/$galleriesIndexDir/galleries.${galleriesIndexVersion.getValue()}.data" val url =
"https://$domain/$galleriesIndexDir/galleries.${galleriesIndexVersion.getValue()}.data"
if (length > 100000000 || length <= 0) { if (length > 100000000 || length <= 0) {
error("length $length is too long") error("length $length is too long")
} }
return getURLAtRange(url, offset until (offset+length)).asIntBuffer() return getURLAtRange(url, offset until (offset + length)).asIntBuffer()
} }
private suspend fun getSuggestionsFromData(field: String, data: Node.Data): List<Suggestion> { private suspend fun getSuggestionsFromData(field: String, data: Node.Data): List<Suggestion> {
@@ -188,14 +189,14 @@ object HitomiHttpClient {
check(data.length in 1..10000) { "Invalid length ${data.length}" } check(data.length in 1..10000) { "Invalid length ${data.length}" }
val buffer = getURLAtRange(url, offset..<offset+length).order(ByteOrder.BIG_ENDIAN) val buffer = getURLAtRange(url, offset..<offset + length).order(ByteOrder.BIG_ENDIAN)
val numberOfSuggestions = buffer.int val numberOfSuggestions = buffer.int
check(numberOfSuggestions in 1 .. 100) { "Number of suggestions $numberOfSuggestions is too long" } check(numberOfSuggestions in 1..100) { "Number of suggestions $numberOfSuggestions is too long" }
return buildList { return buildList {
for (i in 0 ..< numberOfSuggestions) { for (i in 0..<numberOfSuggestions) {
val namespaceLen = buffer.int val namespaceLen = buffer.int
val namespace = ByteArray(namespaceLen).apply { val namespace = ByteArray(namespaceLen).apply {
buffer.get(this) buffer.get(this)
@@ -216,7 +217,7 @@ object HitomiHttpClient {
private suspend fun getGalleryIDsFromNozomi( private suspend fun getGalleryIDsFromNozomi(
area: String?, area: String?,
tag: String, tag: String,
language: String language: String,
): IntBuffer { ): IntBuffer {
val nozomiAddress = if (area == null) { val nozomiAddress = if (area == null) {
"https://$domain/$compressedNozomiPrefix/$tag-$language$nozomiExtension" "https://$domain/$compressedNozomiPrefix/$tag-$language$nozomiExtension"
@@ -233,7 +234,10 @@ object HitomiHttpClient {
return ByteBuffer.wrap(result).asIntBuffer() return ByteBuffer.wrap(result).asIntBuffer()
} }
private suspend fun getGalleryIDsForQuery(query: SearchQuery.Tag, language: String = "all"): IntBuffer = when (query.namespace) { private suspend fun getGalleryIDsForQuery(
query: SearchQuery.Tag,
language: String = "all",
): IntBuffer = when (query.namespace) {
"female", "male" -> getGalleryIDsFromNozomi("tag", query.toString(), language) "female", "male" -> getGalleryIDsFromNozomi("tag", query.toString(), language)
"language" -> getGalleryIDsFromNozomi(null, "index", query.tag) "language" -> getGalleryIDsFromNozomi(null, "index", query.tag)
null -> { null -> {
@@ -242,19 +246,24 @@ object HitomiHttpClient {
val node = getNodeAtAddress("galleries", 0) val node = getNodeAtAddress("galleries", 0)
val data = bSearch("galleries", key, node) val data = bSearch("galleries", key, node)
if (data != null) getGalleryIDsFromData(data.offset, data.length) else IntBuffer.allocate(0) if (data != null) getGalleryIDsFromData(
data.offset,
data.length
) else IntBuffer.allocate(0)
} }
else -> getGalleryIDsFromNozomi(query.namespace, query.tag, language) else -> getGalleryIDsFromNozomi(query.namespace, query.tag, language)
} }
suspend fun getSuggestionsForQuery(query: SearchQuery.Tag): Result<List<Suggestion>> = runCatching { suspend fun getSuggestionsForQuery(query: SearchQuery.Tag): Result<List<Suggestion>> =
val field = query.namespace ?: "global" runCatching {
val key = Node.Key(query.tag) val field = query.namespace ?: "global"
val node = getNodeAtAddress(field, 0) val key = Node.Key(query.tag)
val data = bSearch(field, key, node) val node = getNodeAtAddress(field, 0)
val data = bSearch(field, key, node)
data?.let { getSuggestionsFromData(field, data) } ?: emptyList() data?.let { getSuggestionsFromData(field, data) } ?: emptyList()
} }
suspend fun getGalleryInfo(galleryID: Int) = runCatching { suspend fun getGalleryInfo(galleryID: Int) = runCatching {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@@ -277,7 +286,7 @@ object HitomiHttpClient {
val result = LinkedHashSet<Int>() val result = LinkedHashSet<Int>()
with (allGalleries.await()) { with(allGalleries.await()) {
while (this.hasRemaining()) { while (this.hasRemaining()) {
val gallery = this.get() val gallery = this.get()
@@ -289,6 +298,7 @@ object HitomiHttpClient {
result result
} }
is SearchQuery.And -> coroutineScope { is SearchQuery.And -> coroutineScope {
val queries = query.queries.map { query -> val queries = query.queries.map { query ->
async { async {
@@ -306,6 +316,7 @@ object HitomiHttpClient {
result result
} }
is SearchQuery.Or -> coroutineScope { is SearchQuery.Or -> coroutineScope {
val queries = query.queries.map { query -> val queries = query.queries.map { query ->
async { async {
@@ -322,49 +333,52 @@ object HitomiHttpClient {
result result
} }
null -> getGalleryIDsFromNozomi(null, "index", "all").toSet() null -> getGalleryIDsFromNozomi(null, "index", "all").toSet()
} }
} }
suspend fun getImageURL(galleryFile: GalleryFile, thumbnail: Boolean = false): List<String> = buildList { suspend fun getImageURL(galleryFile: GalleryFile, thumbnail: Boolean = false): List<String> =
val imagePathResolver = imagePathResolver.getValue() buildList {
val imagePathResolver = imagePathResolver.getValue()
listOf("webp", "avif", "jxl").forEach { type -> listOf("webp", "avif", "jxl").forEach { type ->
val available = when { val available = when {
thumbnail && type != "jxl" -> true thumbnail && type != "jxl" -> true
type == "webp" -> galleryFile.hasWebP != 0 type == "webp" -> galleryFile.hasWebP != 0
type == "avif" -> galleryFile.hasAVIF != 0 type == "avif" -> galleryFile.hasAVIF != 0
!thumbnail && type == "jxl" -> galleryFile.hasJXL != 0 !thumbnail && type == "jxl" -> galleryFile.hasJXL != 0
else -> false else -> false
}
if (!available) return@forEach
val url = buildString {
append("https://")
append(imagePathResolver.decodeSubdomain(galleryFile.hash, thumbnail))
append(".hitomi.la/")
append(type)
if (thumbnail) append("bigtn")
append('/')
append(imagePathResolver.decodeImagePath(galleryFile.hash, thumbnail))
append('.')
append(type)
}
add(url)
} }
if (!available) return@forEach
val url = buildString {
append("https://")
append(imagePathResolver.decodeSubdomain(galleryFile.hash, thumbnail))
append(".hitomi.la/")
append(type)
if (thumbnail) append("bigtn")
append('/')
append(imagePathResolver.decodeImagePath(galleryFile.hash, thumbnail))
append('.')
append(type)
}
add(url)
} }
}
suspend fun loadImage( suspend fun loadImage(
galleryFile: GalleryFile, galleryFile: GalleryFile,
thumbnail: Boolean = false, thumbnail: Boolean = false,
acceptImage: (String) -> Boolean = { true }, acceptImage: (String) -> Boolean = { true },
onDownload: (bytesSentTotal: Long, contentLength: Long) -> Unit = { _, _ -> } onDownload: (bytesSentTotal: Long, contentLength: Long?) -> Unit = { _, _ -> },
): Result<Pair<ByteReadChannel, String>> { ): Result<Pair<ByteReadChannel, String>> {
return runCatching { return runCatching {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val url = getImageURL(galleryFile, thumbnail).firstOrNull(acceptImage) ?: error("No available image") val url = getImageURL(galleryFile, thumbnail).firstOrNull(acceptImage)
?: error("No available image")
val channel: ByteReadChannel = httpClient.get(url) { onDownload(onDownload) }.body() val channel: ByteReadChannel = httpClient.get(url) { onDownload(onDownload) }.body()
Pair(channel, url) Pair(channel, url)
} }

View File

@@ -17,20 +17,24 @@ import java.io.File
sealed class ImageLoadProgress { sealed class ImageLoadProgress {
data object NotStarted : ImageLoadProgress() data object NotStarted : ImageLoadProgress()
data class Progress(val bytesSent: Long, val contentLength: Long) : ImageLoadProgress() data class Progress(val bytesSent: Long, val contentLength: Long?) : ImageLoadProgress()
data class Finished(val file: File) : ImageLoadProgress() data class Finished(val file: File) : ImageLoadProgress()
data class Error(val exception: Throwable) : ImageLoadProgress() data class Error(val exception: Throwable) : ImageLoadProgress()
} }
interface ImageCache { interface ImageCache {
suspend fun load(galleryFile: GalleryFile, forceDownload: Boolean = false): StateFlow<ImageLoadProgress> suspend fun load(
galleryFile: GalleryFile,
forceDownload: Boolean = false,
): StateFlow<ImageLoadProgress>
suspend fun free(vararg files: GalleryFile) suspend fun free(vararg files: GalleryFile)
suspend fun clear() suspend fun clear()
} }
class FileImageCache( class FileImageCache(
private val cacheDir: File, private val cacheDir: File,
private val cacheLimit: Long = 128 * 1024 * 1024 // 128MB private val cacheLimit: Long = 128 * 1024 * 1024, // 128MB
) : ImageCache { ) : ImageCache {
private val mutex = Mutex() private val mutex = Mutex()
@@ -56,7 +60,7 @@ class FileImageCache(
files.forEach { file -> files.forEach { file ->
val hash = file.hash val hash = file.hash
requests[hash]?.let { (job, _) -> requests[hash]?.let { (job, _) ->
job.cancel() job.cancel()
} }
@@ -74,7 +78,10 @@ class FileImageCache(
} }
} }
override suspend fun load(galleryFile: GalleryFile, forceDownload: Boolean): StateFlow<ImageLoadProgress> { override suspend fun load(
galleryFile: GalleryFile,
forceDownload: Boolean,
): StateFlow<ImageLoadProgress> {
val hash = galleryFile.hash val hash = galleryFile.hash
mutex.withLock { mutex.withLock {

View File

@@ -1,27 +1,27 @@
[versions] [versions]
agp = "8.8.1" agp = "8.8.2"
android-compileSdk = "34" android-compileSdk = "35"
android-minSdk = "21" android-minSdk = "21"
android-targetSdk = "34" android-targetSdk = "35"
kotlin = "2.0.0" kotlin = "2.0.0"
ksp = "2.0.0-1.0.21" ksp = "2.0.0-1.0.21"
androidxComposeBom = "2024.06.00" androidxComposeBom = "2025.02.00"
androidxCore = "1.13.1" androidxCore = "1.15.0"
androidxActivity = "1.9.0" androidxActivity = "1.10.1"
androidxLifecycle = "2.8.3" androidxLifecycle = "2.8.7"
androidxNavigation = "2.7.7" androidxNavigation = "2.8.8"
accompanist = "0.34.0" accompanist = "0.37.2"
material3 = "1.2.1" material3 = "1.3.1"
material-icons = "1.6.8" material-icons = "1.7.8"
firebase = "33.1.1" firebase = "33.10.0"
crashlytics = "3.0.2" crashlytics = "3.0.3"
hilt = "2.51.1" hilt = "2.51.1"
ktor = "2.3.8" ktor = "3.1.0"
coil = "2.6.0" coil = "2.6.0"
room = "2.6.1" room = "2.6.1"
kotlinx-serialization = "1.3.2" kotlinx-serialization = "1.8.0"
kotlinx-coroutines = "1.8.0" kotlinx-coroutines = "1.10.1"
kotlinx-datetime = "0.6.0" kotlinx-datetime = "0.6.0"
[libraries] [libraries]