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