Fixed image not loading

This commit is contained in:
tom5079
2022-01-03 14:46:22 +09:00
parent e8056072b8
commit 03c5cfa791
12 changed files with 272 additions and 505 deletions

View File

@@ -38,7 +38,7 @@ android {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 30 targetSdkVersion 30
versionCode 69 versionCode 69
versionName "5.1.34" versionName "5.2.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
} }
@@ -133,6 +133,7 @@ dependencies {
implementation "xyz.quaver:floatingsearchview:1.1.7" implementation "xyz.quaver:floatingsearchview:1.1.7"
testImplementation "junit:junit:4.13.1" 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.ext:junit:1.1.2"
androidTestImplementation "androidx.test:rules:1.3.0" androidTestImplementation "androidx.test:rules:1.3.0"
androidTestImplementation "androidx.test:runner:1.3.0" androidTestImplementation "androidx.test:runner:1.3.0"

View File

@@ -11,7 +11,7 @@
"type": "SINGLE", "type": "SINGLE",
"filters": [], "filters": [],
"versionCode": 69, "versionCode": 69,
"versionName": "5.1.34", "versionName": "5.2.1",
"outputFile": "app-release.apk" "outputFile": "app-release.apk"
} }
] ]

View File

@@ -21,15 +21,14 @@
package xyz.quaver.pupil package xyz.quaver.pupil
import android.util.Log import android.util.Log
import android.webkit.WebView import android.webkit.*
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.MainScope import kotlinx.coroutines.*
import kotlinx.coroutines.launch import org.junit.Before
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import xyz.quaver.pupil.hitomi.*
/** /**
* Instrumented test, which will execute on an Android device. * Instrumented test, which will execute on an Android device.
@@ -38,22 +37,95 @@ import org.junit.runner.RunWith
*/ */
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest { class ExampleInstrumentedTest {
@Before
@Test fun init() {
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext val appContext = InstrumentationRegistry.getInstrumentation().targetContext
runBlocking { runBlocking {
MainScope().launch { withContext(Dispatchers.Main) {
val webView = WebView(appContext).apply { webView = WebView(appContext).apply {
settings.javaScriptEnabled = true settings.javaScriptEnabled = true
addJavascriptInterface(object {
@JavascriptInterface
fun onResult(uid: String, result: String) {
_webViewFlow.tryEmit(uid to result)
} }
webView.evaluateJavascript("3") { }, "Callback")
Log.d("PUPILD", it)
} loadDataWithBaseURL(
Log.d("PUPILD", "SYNC?") "https://hitomi.la/",
}.join() """
<script src="https://ltn.hitomi.la/jquery.min.js"></script>
<script src="https://ltn.hitomi.la/common.js"></script>
<script src="https://ltn.hitomi.la/search.js"></script>
<script src="https://ltn.hitomi.la/searchlib.js"></script>
<script src="https://ltn.hitomi.la/results.js></script>
""".trimIndent(),
"text/html",
null,
null
)
}
}
}
}
@Test
fun test_getGalleryIDsFromNozomi() {
runBlocking {
val result = getGalleryIDsFromNozomi(null, "index", "all")
Log.d("PUPILD", "getGalleryIDsFromNozomi: ${result.size}")
}
}
@Test
fun test_getGalleryIDsForQuery() {
runBlocking {
val result = getGalleryIDsForQuery("female:crotch tattoo")
Log.d("PUPILD", "getGalleryIDsForQuery: ${result.size}")
}
}
@Test
fun test_getSuggestionsForQuery() {
runBlocking {
val result = getSuggestionsForQuery("fem")
Log.d("PUPILD", "getSuggestionsForQuery: ${result.size}")
}
}
@Test
fun test_urlFromUrlFromHash() {
runBlocking {
val galleryInfo = getGalleryInfo(2102416)
val result = galleryInfo.files.map {
imageUrlFromImage(2102416, it, false)
}
Log.d("PUPILD", result.toString())
}
}
@Test
fun test_getGalleryInfo() {
runBlocking {
val galleryInfo = getGalleryInfo(2102416)
Log.d("PUPILD", galleryInfo.toString())
}
}
@Test
fun test_getGalleryBlock() {
runBlocking {
val block = getGalleryBlock(2102731)
Log.d("PUPILD", block.toString())
} }
} }
} }

View File

@@ -27,7 +27,9 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.webkit.WebView import android.util.Log
import android.webkit.*
import android.widget.Toast
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
@@ -36,16 +38,22 @@ import com.github.piasy.biv.loader.fresco.FrescoImageLoader
import com.google.android.gms.common.GooglePlayServicesNotAvailableException import com.google.android.gms.common.GooglePlayServicesNotAvailableException
import com.google.android.gms.common.GooglePlayServicesRepairableException import com.google.android.gms.common.GooglePlayServicesRepairableException
import com.google.android.gms.security.ProviderInstaller import com.google.android.gms.security.ProviderInstaller
import com.google.android.material.snackbar.Snackbar
import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
import okhttp3.Dispatcher import kotlinx.coroutines.CoroutineScope
import okhttp3.Interceptor import kotlinx.coroutines.Dispatchers
import okhttp3.OkHttpClient import kotlinx.coroutines.channels.BufferOverflow
import okhttp3.Response import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import okhttp3.*
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.util.* import xyz.quaver.pupil.util.*
import java.io.File import java.io.File
import java.net.URL
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.reflect.KClass import kotlin.reflect.KClass
@@ -73,6 +81,14 @@ val client: OkHttpClient
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
lateinit var webView: WebView lateinit var webView: WebView
val _webViewFlow = MutableSharedFlow<Pair<String, String>>(
extraBufferCapacity = 2,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val webViewFlow = _webViewFlow.asSharedFlow()
var webViewReady = false
private lateinit var userAgent: String
class Pupil : Application() { class Pupil : Application() {
@@ -86,9 +102,39 @@ class Pupil : Application() {
instance = this instance = this
webView = WebView(this).apply { webView = WebView(this).apply {
settings.javaScriptEnabled = true with (settings) {
javaScriptEnabled = true
domStorageEnabled = true
}
loadData("""<script src="https://ltn.hitomi.la/gg.js"></script>""", "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) AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
@@ -110,8 +156,7 @@ class Pupil : Application() {
.proxyInfo(proxyInfo) .proxyInfo(proxyInfo)
.addInterceptor { chain -> .addInterceptor { chain ->
val request = chain.request().newBuilder() val request = chain.request().newBuilder()
.header("User-Agent","Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " + .header("User-Agent", userAgent)
"Ubuntu Chromium/70.0.3538.77 Chrome/70.0.3538.77 Safari/537.36")
.build() .build()
val tag = request.tag() ?: return@addInterceptor chain.proceed(request) val tag = request.tag() ?: return@addInterceptor chain.proceed(request)

View File

@@ -54,12 +54,3 @@ fun URL.readText(settings: HeaderSetter? = null): String {
return client.newCall(request).execute().also{ if (it.code() != 200) throw IOException() }.body()?.use { it.string() } ?: throw IOException() 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()
}

View File

@@ -21,127 +21,92 @@ import android.util.Log
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import kotlinx.coroutines.* 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.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import xyz.quaver.json import xyz.quaver.json
import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.webView import xyz.quaver.pupil.webView
import xyz.quaver.pupil.webViewFlow
import xyz.quaver.pupil.webViewReady
import xyz.quaver.readText import xyz.quaver.readText
import java.net.URL import java.net.URL
import java.nio.charset.Charset import java.nio.charset.Charset
import java.util.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
const val protocol = "https:" 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<Pair<String, String>> = webViewFlow.transformWhile { (currentUid, result) ->
if (currentUid == uid) emit(currentUid to result)
currentUid != uid
}
flow.first().second
}
@Suppress("EXPERIMENTAL_API_USAGE") @Suppress("EXPERIMENTAL_API_USAGE")
fun getGalleryInfo(galleryID: Int) = suspend fun getGalleryInfo(galleryID: Int): GalleryInfo {
json.decodeFromString<GalleryInfo>( val result = webView.evaluatePromise(
URL("$protocol//$domain/galleries/$galleryID.js").readText() """
.replace("var galleryinfo = ", "") new Promise((resolve, reject) => {
$.getScript('https://$domain/galleries/$galleryID.js', () => {
resolve(galleryinfo)
});
})
""".trimIndent()
) )
return json.decodeFromString(result)
}
//common.js //common.js
const val domain = "ltn.hitomi.la" const val domain = "ltn.hitomi.la"
const val galleryblockextension = ".html"
const val galleryblockdir = "galleryblock" const val galleryblockdir = "galleryblock"
const val nozomiextension = ".nozomi" const val nozomiextension = ".nozomi"
@SuppressLint("SetJavaScriptEnabled") val String?.js: String
object gg { get() = if (this == null) "null" else "'$this'"
suspend fun m(g: Int): Int = coroutineScope {
var result: Int? = null
launch(Dispatchers.Main) { @OptIn(ExperimentalSerializationApi::class)
while (webView.progress != 100) yield() 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)") { return Json.decodeFromString(result)
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!!
}
}
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 { suspend fun imageUrlFromImage(galleryID: Int, image: GalleryFiles, noWebp: Boolean) : String {
return when { return when {

View File

@@ -18,6 +18,7 @@ package xyz.quaver.pupil.hitomi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.jsoup.Jsoup import org.jsoup.Jsoup
import xyz.quaver.pupil.webView
import xyz.quaver.readText import xyz.quaver.readText
import java.net.URL import java.net.URL
import java.net.URLDecoder import java.net.URLDecoder
@@ -26,42 +27,6 @@ import java.nio.ByteOrder
import java.util.* import java.util.*
import javax.net.ssl.HttpsURLConnection 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<List<Int>, 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<Int>()
val arrayBuffer = ByteBuffer
.wrap(inputStream.readBytes())
.order(ByteOrder.BIG_ENDIAN)
while (arrayBuffer.hasRemaining())
nozomi.add(arrayBuffer.int)
return Pair(nozomi, totalItems)
}
}
@Serializable @Serializable
data class GalleryBlock( data class GalleryBlock(
val id: Int, val id: Int,
@@ -78,7 +43,18 @@ data class GalleryBlock(
suspend fun getGalleryBlock(galleryID: Int) : GalleryBlock { suspend fun getGalleryBlock(galleryID: Int) : GalleryBlock {
val url = "$protocol//$domain/$galleryblockdir/$galleryID$extension" 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") val galleryUrl = doc.selectFirst("h1 > a")!!.attr("href")
@@ -101,5 +77,3 @@ suspend fun getGalleryBlock(galleryID: Int) : GalleryBlock {
return GalleryBlock(galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags) return GalleryBlock(galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
} }
suspend fun getGalleryBlockOrNull(galleryID: Int) = runCatching { getGalleryBlock(galleryID) }.getOrNull()

View File

@@ -44,6 +44,6 @@ data class GalleryFiles(
//Set header `Referer` to reader url to avoid 403 error //Set header `Referer` to reader url to avoid 403 error
@Deprecated("", replaceWith = ReplaceWith("getGalleryInfo")) @Deprecated("", replaceWith = ReplaceWith("getGalleryInfo"))
fun getReader(galleryID: Int) : GalleryInfo { suspend fun getReader(galleryID: Int) : GalleryInfo {
return getGalleryInfo(galleryID) return getGalleryInfo(galleryID)
} }

View File

@@ -16,13 +16,10 @@
package xyz.quaver.pupil.hitomi package xyz.quaver.pupil.hitomi
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import java.util.* import java.util.*
fun doSearch(query: String, sortByPopularity: Boolean = false) : Set<Int> { suspend fun doSearch(query: String, sortByPopularity: Boolean = false) : Set<Int> = coroutineScope {
val terms = query val terms = query
.trim() .trim()
.replace(Regex("""^\?"""), "") .replace(Regex("""^\?"""), "")
@@ -43,16 +40,16 @@ fun doSearch(query: String, sortByPopularity: Boolean = false) : Set<Int> {
} }
val positiveResults = positiveTerms.map { val positiveResults = positiveTerms.map {
CoroutineScope(Dispatchers.IO).async { async {
kotlin.runCatching { runCatching {
getGalleryIDsForQuery(it) getGalleryIDsForQuery(it)
}.getOrElse { emptySet() } }.getOrElse { emptySet() }
} }
} }
val negativeResults = negativeTerms.map { val negativeResults = negativeTerms.map {
CoroutineScope(Dispatchers.IO).async { async {
kotlin.runCatching { runCatching {
getGalleryIDsForQuery(it) getGalleryIDsForQuery(it)
}.getOrElse { emptySet() } }.getOrElse { emptySet() }
} }
@@ -64,15 +61,14 @@ fun doSearch(query: String, sortByPopularity: Boolean = false) : Set<Int> {
else -> emptySet() else -> emptySet()
} }
runBlocking { fun filterPositive(newResults: Set<Int>) {
@Synchronized fun filterPositive(newResults: Set<Int>) {
results = when { results = when {
results.isEmpty() -> newResults results.isEmpty() -> newResults
else -> results intersect newResults else -> results intersect newResults
} }
} }
@Synchronized fun filterNegative(newResults: Set<Int>) { fun filterNegative(newResults: Set<Int>) {
results = results subtract newResults results = results subtract newResults
} }
@@ -85,7 +81,6 @@ fun doSearch(query: String, sortByPopularity: Boolean = false) : Set<Int> {
negativeResults.forEach { negativeResults.forEach {
filterNegative(it.await()) filterNegative(it.await())
} }
}
return results results
} }

View File

@@ -16,315 +16,37 @@
package xyz.quaver.pupil.hitomi package xyz.quaver.pupil.hitomi
import okhttp3.Request import kotlinx.coroutines.Dispatchers
import xyz.quaver.pupil.client import kotlinx.coroutines.withContext
import xyz.quaver.readBytes import kotlinx.serialization.ExperimentalSerializationApi
import xyz.quaver.readText import kotlinx.serialization.Serializable
import java.net.URL import kotlinx.serialization.decodeFromString
import java.nio.ByteBuffer import kotlinx.serialization.json.Json
import java.nio.ByteOrder import xyz.quaver.pupil.webView
import java.security.MessageDigest
import kotlin.math.min
//searchlib.js //searchlib.js
const val separator = "-"
const val extension = ".html" 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") } @OptIn(ExperimentalSerializationApi::class)
val galleries_index_version: String by lazy { getIndexVersion("galleriesindex") } suspend fun getGalleryIDsForQuery(query: String) : Set<Int> {
val result = webView.evaluatePromise("get_galleryids_for_query('$query')")
fun sha256(data: ByteArray) : ByteArray { return Json.decodeFromString(result)
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<Int> {
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<Suggestion> {
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()
}
} }
@Serializable
data class Suggestion(val s: String, val t: Int, val u: String, val n: String) data class Suggestion(val s: String, val t: Int, val u: String, val n: String)
fun getSuggestionsFromData(field: String, data: Pair<Long, Int>) : List<Suggestion> {
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<Suggestion> {
val result = webView.evaluatePromise("get_suggestions_for_query('$query')")
val suggestions = ArrayList<Suggestion>() return Json.decodeFromString<List<List<Suggestion>?>>(result)[0]!!
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)) @OptIn(ExperimentalSerializationApi::class)
} suspend fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : Set<Int> {
val jsArea = if (area == null) "null" else "'$area'"
return suggestions
} return Json.decodeFromString(webView.evaluatePromise("""get_galleryids_from_nozomi($jsArea, '$tag', '$language')"""))
fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : Set<Int> {
val nozomiAddress =
when(area) {
null -> "$protocol//$domain/$compressed_nozomi_prefix/$tag-$language$nozomiextension"
else -> "$protocol//$domain/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension"
}
val bytes = try {
URL(nozomiAddress).readBytes()
} catch (e: Exception) {
return emptySet()
}
val nozomi = mutableSetOf<Int>()
val arrayBuffer = ByteBuffer
.wrap(bytes)
.order(ByteOrder.BIG_ENDIAN)
while (arrayBuffer.hasRemaining())
nozomi.add(arrayBuffer.int)
return nozomi
}
fun getGalleryIDsFromData(data: Pair<Long, Int>) : Set<Int> {
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<Int>()
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<UByteArray>, val datas: List<Pair<Long, Int>>, val subNodeAddresses: List<Long>)
@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<UByteArray>()
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<Pair<Long, Int>>()
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<Long>()
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<Long, Int>? {
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<Boolean, Int> {
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)
} }

View File

@@ -25,6 +25,7 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.InputType import android.text.InputType
import android.text.util.Linkify import android.text.util.Linkify
import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View

View File

@@ -21,6 +21,7 @@ package xyz.quaver.pupil.util.downloader
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.net.Uri import android.net.Uri
import android.util.Log
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch