From 03c5cfa79108aa947d9bb312e18dd7875e87e65e Mon Sep 17 00:00:00 2001 From: tom5079 Date: Mon, 3 Jan 2022 14:46:22 +0900 Subject: [PATCH] Fixed image not loading --- app/build.gradle | 3 +- app/release/output-metadata.json | 2 +- .../quaver/pupil/ExampleInstrumentedTest.kt | 104 +++++- app/src/main/java/xyz/quaver/pupil/Pupil.kt | 63 +++- .../java/xyz/quaver/pupil/hitomi/Utils.kt | 9 - .../java/xyz/quaver/pupil/hitomi/common.kt | 165 ++++----- .../xyz/quaver/pupil/hitomi/galleryblock.kt | 54 +-- .../java/xyz/quaver/pupil/hitomi/reader.kt | 2 +- .../java/xyz/quaver/pupil/hitomi/results.kt | 55 ++- .../java/xyz/quaver/pupil/hitomi/search.kt | 318 ++---------------- .../java/xyz/quaver/pupil/ui/MainActivity.kt | 1 + .../xyz/quaver/pupil/util/downloader/Cache.kt | 1 + 12 files changed, 272 insertions(+), 505 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index fc992f6a..b57e58fc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -38,7 +38,7 @@ android { minSdkVersion 16 targetSdkVersion 30 versionCode 69 - versionName "5.1.34" + versionName "5.2.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true } @@ -133,6 +133,7 @@ dependencies { implementation "xyz.quaver:floatingsearchview:1.1.7" testImplementation "junit:junit:4.13.1" + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0" androidTestImplementation "androidx.test.ext:junit:1.1.2" androidTestImplementation "androidx.test:rules:1.3.0" androidTestImplementation "androidx.test:runner:1.3.0" diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json index fe37a5be..78766588 100644 --- a/app/release/output-metadata.json +++ b/app/release/output-metadata.json @@ -11,7 +11,7 @@ "type": "SINGLE", "filters": [], "versionCode": 69, - "versionName": "5.1.34", + "versionName": "5.2.1", "outputFile": "app-release.apk" } ] diff --git a/app/src/androidTest/java/xyz/quaver/pupil/ExampleInstrumentedTest.kt b/app/src/androidTest/java/xyz/quaver/pupil/ExampleInstrumentedTest.kt index 7ac7c415..15f8ffda 100644 --- a/app/src/androidTest/java/xyz/quaver/pupil/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/xyz/quaver/pupil/ExampleInstrumentedTest.kt @@ -21,15 +21,14 @@ package xyz.quaver.pupil import android.util.Log -import android.webkit.WebView +import android.webkit.* import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import xyz.quaver.pupil.hitomi.* /** * Instrumented test, which will execute on an Android device. @@ -38,22 +37,95 @@ import org.junit.runner.RunWith */ @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { - - @Test - fun useAppContext() { - // Context of the app under test. + @Before + fun init() { val appContext = InstrumentationRegistry.getInstrumentation().targetContext runBlocking { - MainScope().launch { - val webView = WebView(appContext).apply { + withContext(Dispatchers.Main) { + webView = WebView(appContext).apply { settings.javaScriptEnabled = true + + addJavascriptInterface(object { + @JavascriptInterface + fun onResult(uid: String, result: String) { + _webViewFlow.tryEmit(uid to result) + } + }, "Callback") + + loadDataWithBaseURL( + "https://hitomi.la/", + """ + + + + + """, "text/html", null) + userAgent = settings.userAgentString + + webViewClient = object: WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + webViewReady = true + } + } + + addJavascriptInterface(object { + @JavascriptInterface + fun onResult(uid: String, result: String) { + _webViewFlow.tryEmit(uid to result) + } + }, "Callback") + + CoroutineScope(Dispatchers.IO).launch { + val html = URL("https://tom5079.github.io/Pupil/hitomi.html").readText() + + launch(Dispatchers.Main) { + loadDataWithBaseURL( + "https://hitomi.la/", + html, + "text/html", + null, + null + ) + } + } } AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) @@ -110,8 +156,7 @@ class Pupil : Application() { .proxyInfo(proxyInfo) .addInterceptor { chain -> val request = chain.request().newBuilder() - .header("User-Agent","Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " + - "Ubuntu Chromium/70.0.3538.77 Chrome/70.0.3538.77 Safari/537.36") + .header("User-Agent", userAgent) .build() val tag = request.tag() ?: return@addInterceptor chain.proceed(request) diff --git a/app/src/main/java/xyz/quaver/pupil/hitomi/Utils.kt b/app/src/main/java/xyz/quaver/pupil/hitomi/Utils.kt index 0ebcc8f5..27e160c0 100644 --- a/app/src/main/java/xyz/quaver/pupil/hitomi/Utils.kt +++ b/app/src/main/java/xyz/quaver/pupil/hitomi/Utils.kt @@ -53,13 +53,4 @@ fun URL.readText(settings: HeaderSetter? = null): String { }.build() return client.newCall(request).execute().also{ if (it.code() != 200) throw IOException() }.body()?.use { it.string() } ?: throw IOException() -} - -fun URL.readBytes(settings: HeaderSetter? = null): ByteArray { - val request = Request.Builder() - .url(this).let { - settings?.invoke(it) ?: it - }.build() - - return client.newCall(request).execute().also { if (it.code() != 200) throw IOException() }.body()?.use { it.bytes() } ?: throw IOException() } \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/hitomi/common.kt b/app/src/main/java/xyz/quaver/pupil/hitomi/common.kt index 69e3adf5..168d34d8 100644 --- a/app/src/main/java/xyz/quaver/pupil/hitomi/common.kt +++ b/app/src/main/java/xyz/quaver/pupil/hitomi/common.kt @@ -21,128 +21,93 @@ import android.util.Log import android.webkit.WebView import android.webkit.WebViewClient import kotlinx.coroutines.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.transformWhile +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import xyz.quaver.json import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.webView +import xyz.quaver.pupil.webViewFlow +import xyz.quaver.pupil.webViewReady import xyz.quaver.readText import java.net.URL import java.nio.charset.Charset +import java.util.* +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine const val protocol = "https:" +suspend inline fun WebView.evaluate(script: String): String = withContext(Dispatchers.Main) { + while (!webViewReady) yield() + + val result: String = suspendCoroutine { continuation -> + evaluateJavascript(script) { + continuation.resume(it) + } + } + + result +} + +@OptIn(ExperimentalCoroutinesApi::class) +suspend inline fun WebView.evaluatePromise(script: String, then: String = ".then(result => Callback.onResult(%uid, JSON.stringify(result)))"): String = withContext(Dispatchers.Main) { + while (!webViewReady) yield() + + val uid = UUID.randomUUID().toString() + + evaluateJavascript((script+then).replace("%uid", "'$uid'"), null) + + val flow: Flow> = webViewFlow.transformWhile { (currentUid, result) -> + if (currentUid == uid) emit(currentUid to result) + currentUid != uid + } + + flow.first().second +} + @Suppress("EXPERIMENTAL_API_USAGE") -fun getGalleryInfo(galleryID: Int) = - json.decodeFromString( - URL("$protocol//$domain/galleries/$galleryID.js").readText() - .replace("var galleryinfo = ", "") +suspend fun getGalleryInfo(galleryID: Int): GalleryInfo { + val result = webView.evaluatePromise( + """ + new Promise((resolve, reject) => { + $.getScript('https://$domain/galleries/$galleryID.js', () => { + resolve(galleryinfo) + }); + }) + """.trimIndent() ) + return json.decodeFromString(result) +} + //common.js const val domain = "ltn.hitomi.la" -const val galleryblockextension = ".html" const val galleryblockdir = "galleryblock" const val nozomiextension = ".nozomi" -@SuppressLint("SetJavaScriptEnabled") -object gg { - suspend fun m(g: Int): Int = coroutineScope { - var result: Int? = null +val String?.js: String + get() = if (this == null) "null" else "'$this'" - launch(Dispatchers.Main) { - while (webView.progress != 100) yield() +@OptIn(ExperimentalSerializationApi::class) +suspend fun urlFromUrlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null, base: String? = null): String { + val result = webView.evaluate( + """ + url_from_url_from_hash( + ${galleryID.toString().js}, + ${Json.encodeToString(image)}, + ${dir.js}, ${ext.js}, ${base.js} + ) + """.trimIndent() + ) - webView.evaluateJavascript("gg.m($g)") { - result = it.toInt() - } - } - - while (result == null) yield() - - result!! - } - - suspend fun b(): String = coroutineScope { - var result: String? = null - - launch(Dispatchers.Main) { - while (webView.progress != 100) yield() - - webView.evaluateJavascript("gg.b") { - result = it.replace("\"", "") - } - } - - while (result == null) yield() - - result!! - } - - suspend fun s(h: String): String = coroutineScope { - var result: String? = null - - launch(Dispatchers.Main) { - while (webView.progress != 100) yield() - - webView.evaluateJavascript("gg.s('$h')") { - result = it.replace("\"", "") - } - } - - while (result == null) yield() - - result!! - } + return Json.decodeFromString(result) } -suspend fun subdomainFromURL(url: String, base: String? = null) : String { - var retval = "b" - - if (!base.isNullOrBlank()) - retval = base - - val b = 16 - - val r = Regex("""/[0-9a-f]{61}([0-9a-f]{2})([0-9a-f])""") - val m = r.find(url) ?: return "a" - - val g = m.groupValues.let { it[2]+it[1] }.toIntOrNull(b) - - if (g != null) { - retval = (97+ gg.m(g)).toChar().toString() + retval - } - - return retval -} - -suspend fun urlFromUrl(url: String, base: String? = null) : String { - return url.replace(Regex("""//..?\.hitomi\.la/"""), "//${subdomainFromURL(url, base)}.hitomi.la/") -} - - -suspend fun fullPathFromHash(hash: String) : String = - "${gg.b()}${gg.s(hash)}/$hash" - -fun realFullPathFromHash(hash: String): String = - hash.replace(Regex("""^.*(..)(.)$"""), "$2/$1/$hash") - -suspend fun urlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null) : String { - val ext = ext ?: dir ?: image.name.takeLastWhile { it != '.' } - val dir = dir ?: "images" - return "https://a.hitomi.la/$dir/${fullPathFromHash(image.hash)}.$ext" -} - -suspend fun urlFromUrlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null, base: String? = null) = - if (base == "tn") - urlFromUrl("https://a.hitomi.la/$dir/${realFullPathFromHash(image.hash)}.$ext", base) - else - urlFromUrl(urlFromHash(galleryID, image, dir, ext), base) - -suspend fun rewriteTnPaths(html: String) = - Regex("""//tn\.hitomi\.la/[^/]+/[0-9a-f]/[0-9a-f]{2}/[0-9a-f]{64}""").find(html)?.let { m -> - html.replaceRange(m.range, urlFromUrl(m.value, "tn")) - } ?: html - suspend fun imageUrlFromImage(galleryID: Int, image: GalleryFiles, noWebp: Boolean) : String { return when { noWebp -> diff --git a/app/src/main/java/xyz/quaver/pupil/hitomi/galleryblock.kt b/app/src/main/java/xyz/quaver/pupil/hitomi/galleryblock.kt index f9596947..3232af9c 100644 --- a/app/src/main/java/xyz/quaver/pupil/hitomi/galleryblock.kt +++ b/app/src/main/java/xyz/quaver/pupil/hitomi/galleryblock.kt @@ -18,6 +18,7 @@ package xyz.quaver.pupil.hitomi import kotlinx.serialization.Serializable import org.jsoup.Jsoup +import xyz.quaver.pupil.webView import xyz.quaver.readText import java.net.URL import java.net.URLDecoder @@ -26,42 +27,6 @@ import java.nio.ByteOrder import java.util.* import javax.net.ssl.HttpsURLConnection -//galleryblock.js -fun fetchNozomi(area: String? = null, tag: String = "index", language: String = "all", start: Int = -1, count: Int = -1) : Pair, Int> { - val url = - when(area) { - null -> "$protocol//$domain/$tag-$language$nozomiextension" - else -> "$protocol//$domain/$area/$tag-$language$nozomiextension" - } - - with(URL(url).openConnection() as HttpsURLConnection) { - requestMethod = "GET" - - if (start != -1 && count != -1) { - val startByte = start*4 - val endByte = (start+count)*4-1 - - setRequestProperty("Range", "bytes=$startByte-$endByte") - } - - connect() - - val totalItems = getHeaderField("Content-Range") - .replace(Regex("^[Bb]ytes \\d+-\\d+/"), "").toInt() / 4 - - val nozomi = ArrayList() - - val arrayBuffer = ByteBuffer - .wrap(inputStream.readBytes()) - .order(ByteOrder.BIG_ENDIAN) - - while (arrayBuffer.hasRemaining()) - nozomi.add(arrayBuffer.int) - - return Pair(nozomi, totalItems) - } -} - @Serializable data class GalleryBlock( val id: Int, @@ -78,7 +43,18 @@ data class GalleryBlock( suspend fun getGalleryBlock(galleryID: Int) : GalleryBlock { val url = "$protocol//$domain/$galleryblockdir/$galleryID$extension" - val doc = Jsoup.parse(rewriteTnPaths(URL(url).readText())) + val html: String = webView.evaluatePromise( + """ + $.get('$url').always(function(data, status) { + if (status === 'success') { + Callback.onResult(%uid, data); + } + }); + """.trimIndent(), + then = "" + ) + + val doc = Jsoup.parse(html) val galleryUrl = doc.selectFirst("h1 > a")!!.attr("href") @@ -100,6 +76,4 @@ suspend fun getGalleryBlock(galleryID: Int) : GalleryBlock { } return GalleryBlock(galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags) -} - -suspend fun getGalleryBlockOrNull(galleryID: Int) = runCatching { getGalleryBlock(galleryID) }.getOrNull() \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/hitomi/reader.kt b/app/src/main/java/xyz/quaver/pupil/hitomi/reader.kt index 7d33642d..be7a86eb 100644 --- a/app/src/main/java/xyz/quaver/pupil/hitomi/reader.kt +++ b/app/src/main/java/xyz/quaver/pupil/hitomi/reader.kt @@ -44,6 +44,6 @@ data class GalleryFiles( //Set header `Referer` to reader url to avoid 403 error @Deprecated("", replaceWith = ReplaceWith("getGalleryInfo")) -fun getReader(galleryID: Int) : GalleryInfo { +suspend fun getReader(galleryID: Int) : GalleryInfo { return getGalleryInfo(galleryID) } \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/hitomi/results.kt b/app/src/main/java/xyz/quaver/pupil/hitomi/results.kt index ad7cb1c7..749dd1d1 100644 --- a/app/src/main/java/xyz/quaver/pupil/hitomi/results.kt +++ b/app/src/main/java/xyz/quaver/pupil/hitomi/results.kt @@ -16,13 +16,10 @@ package xyz.quaver.pupil.hitomi -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.* import java.util.* -fun doSearch(query: String, sortByPopularity: Boolean = false) : Set { +suspend fun doSearch(query: String, sortByPopularity: Boolean = false) : Set = coroutineScope { val terms = query .trim() .replace(Regex("""^\?"""), "") @@ -43,16 +40,16 @@ fun doSearch(query: String, sortByPopularity: Boolean = false) : Set { } val positiveResults = positiveTerms.map { - CoroutineScope(Dispatchers.IO).async { - kotlin.runCatching { + async { + runCatching { getGalleryIDsForQuery(it) }.getOrElse { emptySet() } } } val negativeResults = negativeTerms.map { - CoroutineScope(Dispatchers.IO).async { - kotlin.runCatching { + async { + runCatching { getGalleryIDsForQuery(it) }.getOrElse { emptySet() } } @@ -64,28 +61,26 @@ fun doSearch(query: String, sortByPopularity: Boolean = false) : Set { else -> emptySet() } - runBlocking { - @Synchronized fun filterPositive(newResults: Set) { - results = when { - results.isEmpty() -> newResults - else -> results intersect newResults - } - } - - @Synchronized fun filterNegative(newResults: Set) { - results = results subtract newResults - } - - //positive results - positiveResults.forEach { - filterPositive(it.await()) - } - - //negative results - negativeResults.forEach { - filterNegative(it.await()) + fun filterPositive(newResults: Set) { + results = when { + results.isEmpty() -> newResults + else -> results intersect newResults } } - return results + fun filterNegative(newResults: Set) { + results = results subtract newResults + } + + //positive results + positiveResults.forEach { + filterPositive(it.await()) + } + + //negative results + negativeResults.forEach { + filterNegative(it.await()) + } + + results } \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/hitomi/search.kt b/app/src/main/java/xyz/quaver/pupil/hitomi/search.kt index 74d2fece..858df44e 100644 --- a/app/src/main/java/xyz/quaver/pupil/hitomi/search.kt +++ b/app/src/main/java/xyz/quaver/pupil/hitomi/search.kt @@ -16,315 +16,37 @@ package xyz.quaver.pupil.hitomi -import okhttp3.Request -import xyz.quaver.pupil.client -import xyz.quaver.readBytes -import xyz.quaver.readText -import java.net.URL -import java.nio.ByteBuffer -import java.nio.ByteOrder -import java.security.MessageDigest -import kotlin.math.min +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import xyz.quaver.pupil.webView //searchlib.js -const val separator = "-" const val extension = ".html" -const val index_dir = "tagindex" -const val galleries_index_dir = "galleriesindex" -const val max_node_size = 464 -const val B = 16 -const val compressed_nozomi_prefix = "n" -val tag_index_version: String by lazy { getIndexVersion("tagindex") } -val galleries_index_version: String by lazy { getIndexVersion("galleriesindex") } +@OptIn(ExperimentalSerializationApi::class) +suspend fun getGalleryIDsForQuery(query: String) : Set { + val result = webView.evaluatePromise("get_galleryids_for_query('$query')") -fun sha256(data: ByteArray) : ByteArray { - return MessageDigest.getInstance("SHA-256").digest(data) -} - -@OptIn(ExperimentalUnsignedTypes::class) -fun hashTerm(term: String) : UByteArray { - return sha256(term.toByteArray()).toUByteArray().sliceArray(0 until 4) -} - -fun sanitize(input: String) : String { - return input.replace(Regex("[/#]"), "") -} - -fun getIndexVersion(name: String) = - URL("$protocol//$domain/$name/version?_=${System.currentTimeMillis()}").readText() - -//search.js -fun getGalleryIDsForQuery(query: String) : Set { - query.replace("_", " ").let { - if (it.indexOf(':') > -1) { - val sides = it.split(":") - val ns = sides[0] - var tag = sides[1] - - var area : String? = ns - var language = "all" - when (ns) { - "female", "male" -> { - area = "tag" - tag = it - } - "language" -> { - area = null - language = tag - tag = "index" - } - } - - return getGalleryIDsFromNozomi(area, tag, language) - } - - val key = hashTerm(it) - val field = "galleries" - - val node = getNodeAtAddress(field, 0) ?: return emptySet() - - val data = bSearch(field, key, node) - - if (data != null) - return getGalleryIDsFromData(data) - - return emptySet() - } -} - -fun getSuggestionsForQuery(query: String) : List { - query.replace('_', ' ').let { - var field = "global" - var term = it - - if (term.indexOf(':') > -1) { - val sides = it.split(':') - field = sides[0] - term = sides[1] - } - - val key = hashTerm(term) - val node = getNodeAtAddress(field, 0) ?: return emptyList() - val data = bSearch(field, key, node) - - if (data != null) - return getSuggestionsFromData(field, data) - - return emptyList() - } + return Json.decodeFromString(result) } +@Serializable data class Suggestion(val s: String, val t: Int, val u: String, val n: String) -fun getSuggestionsFromData(field: String, data: Pair) : List { - val url = "$protocol//$domain/$index_dir/$field.$tag_index_version.data" - val (offset, length) = data - if (length > 10000 || length <= 0) - throw Exception("length $length is too long") - val inbuf = getURLAtRange(url, offset.until(offset+length)) +@OptIn(ExperimentalSerializationApi::class) +suspend fun getSuggestionsForQuery(query: String) : List { + val result = webView.evaluatePromise("get_suggestions_for_query('$query')") - val suggestions = ArrayList() - - val buffer = ByteBuffer - .wrap(inbuf) - .order(ByteOrder.BIG_ENDIAN) - val numberOfSuggestions = buffer.int - - if (numberOfSuggestions > 100 || numberOfSuggestions <= 0) - throw Exception("number of suggestions $numberOfSuggestions is too long") - - for (i in 0.until(numberOfSuggestions)) { - var top = buffer.int - - val ns = inbuf.sliceArray(buffer.position().until(buffer.position()+top)).toString(charset("UTF-8")) - buffer.position(buffer.position()+top) - - top = buffer.int - - val tag = inbuf.sliceArray(buffer.position().until(buffer.position()+top)).toString(charset("UTF-8")) - buffer.position(buffer.position()+top) - - val count = buffer.int - - val tagname = sanitize(tag) - val u = - when(ns) { - "female", "male" -> "/tag/$ns:$tagname${separator}1$extension" - "language" -> "/index-$tagname${separator}1$extension" - else -> "/$ns/$tagname${separator}all${separator}1$extension" - } - - suggestions.add(Suggestion(tag, count, u, ns)) - } - - return suggestions + return Json.decodeFromString?>>(result)[0]!! } -fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : Set { - val nozomiAddress = - when(area) { - null -> "$protocol//$domain/$compressed_nozomi_prefix/$tag-$language$nozomiextension" - else -> "$protocol//$domain/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension" - } +@OptIn(ExperimentalSerializationApi::class) +suspend fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : Set { + val jsArea = if (area == null) "null" else "'$area'" - val bytes = try { - URL(nozomiAddress).readBytes() - } catch (e: Exception) { - return emptySet() - } - - val nozomi = mutableSetOf() - - val arrayBuffer = ByteBuffer - .wrap(bytes) - .order(ByteOrder.BIG_ENDIAN) - - while (arrayBuffer.hasRemaining()) - nozomi.add(arrayBuffer.int) - - return nozomi -} - -fun getGalleryIDsFromData(data: Pair) : Set { - val url = "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.data" - val (offset, length) = data - if (length > 100000000 || length <= 0) - throw Exception("length $length is too long") - - val inbuf = getURLAtRange(url, offset.until(offset+length)) - - val galleryIDs = mutableSetOf() - - val buffer = ByteBuffer - .wrap(inbuf) - .order(ByteOrder.BIG_ENDIAN) - - val numberOfGalleryIDs = buffer.int - - val expectedLength = numberOfGalleryIDs*4+4 - - if (numberOfGalleryIDs > 10000000 || numberOfGalleryIDs <= 0) - throw Exception("number_of_galleryids $numberOfGalleryIDs is too long") - else if (inbuf.size != expectedLength) - throw Exception("inbuf.byteLength ${inbuf.size} != expected_length $expectedLength") - - for (i in 0.until(numberOfGalleryIDs)) - galleryIDs.add(buffer.int) - - return galleryIDs -} - -fun getNodeAtAddress(field: String, address: Long) : Node? { - val url = - when(field) { - "galleries" -> "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.index" - "languages" -> "$protocol//$domain/$galleries_index_dir/languages.$galleries_index_version.index" - "nozomiurl" -> "$protocol//$domain/$galleries_index_dir/nozomiurl.$galleries_index_version.index" - else -> "$protocol//$domain/$index_dir/$field.$tag_index_version.index" - } - - val nodedata = getURLAtRange(url, address.until(address+max_node_size)) - - return decodeNode(nodedata) -} - -fun getURLAtRange(url: String, range: LongRange) : ByteArray { - val request = Request.Builder() - .url(url) - .header("Range", "bytes=${range.first}-${range.last}") - .build() - - return client.newCall(request).execute().body()?.use { it.bytes() } ?: byteArrayOf() -} - -@OptIn(ExperimentalUnsignedTypes::class) -data class Node(val keys: List, val datas: List>, val subNodeAddresses: List) -@OptIn(ExperimentalUnsignedTypes::class) -fun decodeNode(data: ByteArray) : Node { - val buffer = ByteBuffer - .wrap(data) - .order(ByteOrder.BIG_ENDIAN) - - val uData = data.toUByteArray() - - val numberOfKeys = buffer.int - val keys = ArrayList() - - for (i in 0.until(numberOfKeys)) { - val keySize = buffer.int - - if (keySize == 0 || keySize > 32) - throw Exception("fatal: !keySize || keySize > 32") - - keys.add(uData.sliceArray(buffer.position().until(buffer.position()+keySize))) - buffer.position(buffer.position()+keySize) - } - - val numberOfDatas = buffer.int - val datas = ArrayList>() - - for (i in 0.until(numberOfDatas)) { - val offset = buffer.long - val length = buffer.int - - datas.add(Pair(offset, length)) - } - - val numberOfSubNodeAddresses = B+1 - val subNodeAddresses = ArrayList() - - for (i in 0.until(numberOfSubNodeAddresses)) { - val subNodeAddress = buffer.long - subNodeAddresses.add(subNodeAddress) - } - - return Node(keys, datas, subNodeAddresses) -} - -@OptIn(ExperimentalUnsignedTypes::class) -fun bSearch(field: String, key: UByteArray, node: Node) : Pair? { - fun compareArrayBuffers(dv1: UByteArray, dv2: UByteArray) : Int { - val top = min(dv1.size, dv2.size) - - for (i in 0.until(top)) { - if (dv1[i] < dv2[i]) - return -1 - else if (dv1[i] > dv2[i]) - return 1 - } - - return 0 - } - - fun locateKey(key: UByteArray, node: Node) : Pair { - for (i in node.keys.indices) { - val cmpResult = compareArrayBuffers(key, node.keys[i]) - - if (cmpResult <= 0) - return Pair(cmpResult==0, i) - } - - return Pair(false, node.keys.size) - } - - fun isLeaf(node: Node) : Boolean { - for (subnode in node.subNodeAddresses) - if (subnode != 0L) - return false - - return true - } - - if (node.keys.isEmpty()) - return null - - val (there, where) = locateKey(key, node) - if (there) - return node.datas[where] - else if (isLeaf(node)) - return null - - val nextNode = getNodeAtAddress(field, node.subNodeAddresses[where]) ?: return null - return bSearch(field, key, nextNode) + return Json.decodeFromString(webView.evaluatePromise("""get_galleryids_from_nozomi($jsArea, '$tag', '$language')""")) } \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt index 56148b87..c2c011c9 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt @@ -25,6 +25,7 @@ import android.os.Build import android.os.Bundle import android.text.InputType import android.text.util.Linkify +import android.util.Log import android.view.KeyEvent import android.view.MenuItem import android.view.View diff --git a/app/src/main/java/xyz/quaver/pupil/util/downloader/Cache.kt b/app/src/main/java/xyz/quaver/pupil/util/downloader/Cache.kt index f9a005df..7e335220 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/downloader/Cache.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/downloader/Cache.kt @@ -21,6 +21,7 @@ package xyz.quaver.pupil.util.downloader import android.content.Context import android.content.ContextWrapper import android.net.Uri +import android.util.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch