dependency update
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user