diff --git a/app/build.gradle b/app/build.gradle index fb36175c..844c822f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -38,7 +38,7 @@ android { minSdkVersion 16 targetSdkVersion 31 versionCode 69 - versionName "5.2.14" + versionName "5.2.15" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true } diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json index 7fa35c18..2db26861 100644 --- a/app/release/output-metadata.json +++ b/app/release/output-metadata.json @@ -12,7 +12,7 @@ "filters": [], "attributes": [], "versionCode": 69, - "versionName": "5.2.14", + "versionName": "5.2.15", "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 d7217b34..eb10ab16 100644 --- a/app/src/androidTest/java/xyz/quaver/pupil/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/xyz/quaver/pupil/ExampleInstrumentedTest.kt @@ -46,48 +46,7 @@ class ExampleInstrumentedTest { runBlocking { withContext(Dispatchers.Main) { - WebView.setWebContentsDebuggingEnabled(true) - - webView = WebView(appContext).apply { - with (settings) { - javaScriptEnabled = true - domStorageEnabled = true - } - - userAgent = settings.userAgentString - - webViewClient = object: WebViewClient() { - override fun onPageFinished(view: WebView?, url: String?) { - webViewReady = true - } - - override fun onReceivedError( - view: WebView?, - request: WebResourceRequest?, - error: WebResourceError? - ) { - } - } - - webChromeClient = object: WebChromeClient() { - override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean { - return super.onConsoleMessage(consoleMessage) - } - } - - addJavascriptInterface(object { - @JavascriptInterface - fun onResult(uid: String, result: String) { - _webViewFlow.tryEmit(uid to result) - } - @JavascriptInterface - fun onError(uid: String, message: String) { - _webViewFlow.tryEmit(uid to null) - } - }, "Callback") - } - - reloadWhenFailedOrUpdate() + initWebView(appContext) } } } @@ -141,10 +100,19 @@ class ExampleInstrumentedTest { } } + @Test + fun test_getGallery() { + runBlocking { + val gallery = getGallery(2109479) + + Log.d("PUPILD", gallery.toString()) + } + } + @Test fun test_getGalleryBlock() { runBlocking { - val block = getGalleryBlock(2102731) + val block = getGalleryBlock(2013877) Log.d("PUPILD", block.toString()) } diff --git a/app/src/main/java/xyz/quaver/pupil/Pupil.kt b/app/src/main/java/xyz/quaver/pupil/Pupil.kt index 3fbd978d..81873018 100644 --- a/app/src/main/java/xyz/quaver/pupil/Pupil.kt +++ b/app/src/main/java/xyz/quaver/pupil/Pupil.kt @@ -28,7 +28,6 @@ import android.content.Intent import android.content.pm.ApplicationInfo import android.net.Uri import android.os.Build -import android.util.Log import android.webkit.* import android.widget.Toast import androidx.appcompat.app.AppCompatDelegate @@ -42,18 +41,18 @@ import com.google.android.gms.common.GooglePlayServicesRepairableException import com.google.android.gms.security.ProviderInstaller import com.google.firebase.crashlytics.FirebaseCrashlytics import kotlinx.coroutines.* -import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import okhttp3.* import xyz.quaver.io.FileX import xyz.quaver.pupil.hitomi.evaluationContext +import xyz.quaver.pupil.types.JavascriptConsoleException +import xyz.quaver.pupil.types.JavascriptOnErrorException import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.util.* import java.io.File import java.net.URL import java.util.* -import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import kotlin.reflect.KClass @@ -98,7 +97,7 @@ fun reloadWebView() { runCatching { URL( - if (isDebugBuild) + if (BuildConfig.DEBUG) "https://tom5079.github.io/Pupil/hitomi-dev.html" else "https://tom5079.github.io/Pupil/hitomi.html" @@ -126,7 +125,7 @@ fun reloadWhenFailedOrUpdate() = CoroutineScope(Dispatchers.Default).launch { webViewFailed || runCatching { URL( - if (isDebugBuild) + if (BuildConfig.DEBUG) "https://tom5079.github.io/Pupil/hitomi-dev.html.ver" else "https://tom5079.github.io/Pupil/hitomi.html.ver" @@ -144,7 +143,69 @@ fun reloadWhenFailedOrUpdate() = CoroutineScope(Dispatchers.Default).launch { } } -var isDebugBuild: Boolean = false +@SuppressLint("SetJavaScriptEnabled") +fun initWebView(context: Context) { + if (BuildConfig.DEBUG) WebView.setWebContentsDebuggingEnabled(true) + + webView = WebView(context).apply { + with (settings) { + javaScriptEnabled = true + domStorageEnabled = true + } + + userAgent = settings.userAgentString + + webViewClient = object: WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + webViewReady = true + } + + override fun onReceivedError( + view: WebView?, + request: WebResourceRequest?, + error: WebResourceError? + ) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + FirebaseCrashlytics.getInstance().recordException( + JavascriptOnErrorException("onReceivedError: ${error?.description}") + ) + } + } + } + + webChromeClient = object: WebChromeClient() { + override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean { + FirebaseCrashlytics.getInstance().recordException( + JavascriptConsoleException("onConsoleMessage: ${consoleMessage?.message()} (${consoleMessage?.sourceId()}:${consoleMessage?.lineNumber()})") + ) + + return super.onConsoleMessage(consoleMessage) + } + } + + addJavascriptInterface(object { + @JavascriptInterface + fun onResult(uid: String, result: String) { + CoroutineScope(Dispatchers.Unconfined).launch { + _webViewFlow.emit(uid to result) + } + } + @JavascriptInterface + fun onError(uid: String, message: String) { + CoroutineScope(Dispatchers.Unconfined).launch { + _webViewFlow.emit(uid to null) + } + Toast.makeText(context, message, Toast.LENGTH_LONG).show() + FirebaseCrashlytics.getInstance().recordException( + JavascriptOnErrorException(message) + ) + } + }, "Callback") + } + + reloadWhenFailedOrUpdate() +} + lateinit var userAgent: String class Pupil : Application() { @@ -157,67 +218,8 @@ class Pupil : Application() { @SuppressLint("SetJavaScriptEnabled") override fun onCreate() { instance = this - isDebugBuild = applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0 - if (isDebugBuild) WebView.setWebContentsDebuggingEnabled(true) - - webView = WebView(this).apply { - with (settings) { - javaScriptEnabled = true - domStorageEnabled = true - } - - userAgent = settings.userAgentString - - webViewClient = object: WebViewClient() { - override fun onPageFinished(view: WebView?, url: String?) { - webViewReady = true - } - - override fun onReceivedError( - view: WebView?, - request: WebResourceRequest?, - error: WebResourceError? - ) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - FirebaseCrashlytics.getInstance().log( - "onReceivedError: ${error?.description}" - ) - } - } - } - - webChromeClient = object: WebChromeClient() { - override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean { - FirebaseCrashlytics.getInstance().log( - "onConsoleMessage: ${consoleMessage?.message()} (${consoleMessage?.sourceId()}:${consoleMessage?.lineNumber()})" - ) - - return super.onConsoleMessage(consoleMessage) - } - } - - addJavascriptInterface(object { - @JavascriptInterface - fun onResult(uid: String, result: String) { - CoroutineScope(Dispatchers.Unconfined).launch { - _webViewFlow.emit(uid to result) - } - } - @JavascriptInterface - fun onError(uid: String, message: String) { - CoroutineScope(Dispatchers.Unconfined).launch { - _webViewFlow.emit(uid to null) - } - Toast.makeText(this@Pupil, message, Toast.LENGTH_LONG).show() - FirebaseCrashlytics.getInstance().recordException( - Exception(message) - ) - } - }, "Callback") - } - - reloadWhenFailedOrUpdate() + initWebView(this) AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) 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 58f18023..f4c391e5 100644 --- a/app/src/main/java/xyz/quaver/pupil/hitomi/common.kt +++ b/app/src/main/java/xyz/quaver/pupil/hitomi/common.kt @@ -66,7 +66,7 @@ suspend fun WebView.evaluate(script: String): String = coroutineScope { @OptIn(ExperimentalCoroutinesApi::class) suspend fun WebView.evaluatePromise( script: String, - then: String = ".then(result => Callback.onResult(%uid, JSON.stringify(result))).catch(err => Callback.onError(%uid, JSON.stringify(error)))" + then: String = ".then(result => Callback.onResult(%uid, JSON.stringify(result))).catch(err => { console.err(err); Callback.onError(%uid, JSON.stringify(err)); })" ): String = coroutineScope { var result: String? = null diff --git a/app/src/main/java/xyz/quaver/pupil/hitomi/galleries.kt b/app/src/main/java/xyz/quaver/pupil/hitomi/galleries.kt index 20feaa72..f1602834 100644 --- a/app/src/main/java/xyz/quaver/pupil/hitomi/galleries.kt +++ b/app/src/main/java/xyz/quaver/pupil/hitomi/galleries.kt @@ -17,7 +17,10 @@ package xyz.quaver.pupil.hitomi import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json import org.jsoup.Jsoup +import xyz.quaver.pupil.webView import xyz.quaver.readText import java.net.URL import java.net.URLDecoder @@ -25,7 +28,8 @@ import java.net.URLDecoder @Serializable data class Gallery( val related: List, - val langList: List>, + val langList: Map + , val cover: String, val title: String, val artists: List, @@ -38,43 +42,6 @@ data class Gallery( val thumbnails: List ) suspend fun getGallery(galleryID: Int) : Gallery { - val url = Jsoup.parse(URL("https://hitomi.la/galleries/$galleryID.html").readText()) - .select("link").attr("href") - - val doc = Jsoup.parse(URL(url).readText()) - - val related = Regex("\\d+") - .findAll(doc.select("script").first()!!.html()) - .map { - it.value.toInt() - }.toList() - - val langList = doc.select("#lang-list a").map { - Pair(it.text(), "$protocol//hitomi.la${it.attr("href")}") - } - - val cover = protocol + doc.selectFirst(".cover img")!!.attr("src") - val title = doc.selectFirst(".gallery h1 a")!!.text() - val artists = doc.select(".gallery h2 a").map { it.text() } - val groups = doc.select(".gallery-info a[href~=^/group/]").map { it.text() } - val type = doc.selectFirst(".gallery-info a[href~=^/type/]")!!.text() - - val language = run { - val href = doc.select(".gallery-info a[href~=^/index.+\\.html\$]").attr("href") - Regex("""index-([^-]+)(-.+)?\.html""").find(href)?.groupValues?.getOrNull(1) ?: "" - } - - val series = doc.select(".gallery-info a[href~=^/series/]").map { it.text() } - val characters = doc.select(".gallery-info a[href~=^/character/]").map { it.text() } - - val tags = doc.select(".gallery-info a[href~=^/tag/]").map { - val href = URLDecoder.decode(it.attr("href"), "UTF-8") - href.slice(5 until href.indexOf('-')) - } - - val thumbnails = getGalleryInfo(galleryID).files.map { galleryInfo -> - urlFromUrlFromHash(galleryID, galleryInfo, "smalltn", "jpg", "tn") - } - - return Gallery(related, langList, cover, title, artists, groups, type, language, series, characters, tags, thumbnails) + val result = webView.evaluatePromise("get_gallery($galleryID)") + return Json.decodeFromString(result) } \ No newline at end of file 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 716baf1a..9b598314 100644 --- a/app/src/main/java/xyz/quaver/pupil/hitomi/galleryblock.kt +++ b/app/src/main/java/xyz/quaver/pupil/hitomi/galleryblock.kt @@ -17,6 +17,8 @@ package xyz.quaver.pupil.hitomi import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json import org.jsoup.Jsoup import xyz.quaver.pupil.webView import xyz.quaver.readText @@ -41,39 +43,6 @@ data class GalleryBlock( ) suspend fun getGalleryBlock(galleryID: Int) : GalleryBlock { - val url = "$protocol//$domain/$galleryblockdir/$galleryID$extension" - - 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 thumbnails = doc.select(".dj-img-cont img").map { protocol + it.attr("data-src") } - - val title = doc.selectFirst("h1 > a")!!.text() - val artists = doc.select(".artist-list a").map{ it.text() } - val series = doc.select(".dj-content a[href~=^/series/]").map { it.text() } - val type = doc.selectFirst("a[href~=^/type/]")!!.text() - - val language = run { - val href = doc.select("a[href~=^/index.+\\.html\$]").attr("href") - Regex("""index-([^-]+)(-.+)?\.html""").find(href)?.groupValues?.getOrNull(1) ?: "" - } - - val relatedTags = doc.select(".relatedtags a").map { - val href = URLDecoder.decode(it.attr("href"), "UTF-8") - href.slice(5 until href.indexOf("-all")) - } - - return GalleryBlock(galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags) + val result = webView.evaluatePromise("get_gallery_block($galleryID)") + return Json.decodeFromString(result) } \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt b/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt index 855cb205..74410bcf 100644 --- a/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt +++ b/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt @@ -222,31 +222,33 @@ class DownloadService : Service() { val (galleryID, index, startId) = call.request().tag() as Tag val ext = call.request().url().encodedPath().split('.').last() - kotlin.runCatching { - val image = response.also { if (it.code() != 200) throw IOException("$galleryID $index ${response.request().url()} CODE ${it.code()}") }.body()?.use { it.bytes() } ?: throw Exception("Response null") - val padding = ceil(progress[galleryID]?.size?.let { log10(it.toFloat()) } ?: 0F).toInt() + CoroutineScope(Dispatchers.IO).launch { + runCatching { + response.also { + if (it.code() != 200) throw IOException( + "$galleryID $index ${response.request().url()} CODE ${it.code()}" + ) + }.body()?.use { + val padding = ceil(progress[galleryID]?.size?.let { log10(it.toFloat()) } ?: 0F).toInt() + + Cache.getInstance(this@DownloadService, galleryID) + .putImage(index, "${index.toString().padStart(padding, '0')}.$ext", it.byteStream()) - CoroutineScope(Dispatchers.IO).launch { - kotlin.runCatching { - Cache.getInstance(this@DownloadService, galleryID).putImage(index, "${index.toString().padStart(padding, '0')}.$ext", image) - }.onSuccess { progress[galleryID]?.set(index, Float.POSITIVE_INFINITY) notify(galleryID) if (isCompleted(galleryID)) { if (DownloadManager.getInstance(this@DownloadService) - .getDownloadFolder(galleryID) != null) + .getDownloadFolder(galleryID) != null + ) Cache.getInstance(this@DownloadService, galleryID).moveToDownload() startId?.let { stopSelf(it) } } - }.onFailure { - FirebaseCrashlytics.getInstance().recordException(it) - } + } ?: throw Exception("Response null") + }.onFailure { + FirebaseCrashlytics.getInstance().recordException(it) } - }.onFailure { - it.printStackTrace() - FirebaseCrashlytics.getInstance().recordException(it) } } } diff --git a/app/src/main/java/xyz/quaver/pupil/types/Exceptions.kt b/app/src/main/java/xyz/quaver/pupil/types/Exceptions.kt new file mode 100644 index 00000000..775373eb --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/types/Exceptions.kt @@ -0,0 +1,22 @@ +/* + * Pupil, Hitomi.la viewer for Android + * Copyright (C) 2022 tom5079 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package xyz.quaver.pupil.types + +class JavascriptConsoleException(message: String?): Exception(message) +class JavascriptOnErrorException(message: String?): Exception(message) \ No newline at end of file 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 e4e1e9f0..72ff0ee8 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 @@ -43,6 +43,7 @@ import xyz.quaver.pupil.hitomi.getGalleryInfo import xyz.quaver.pupil.userAgent import java.io.File import java.io.IOException +import java.io.InputStream import java.util.concurrent.ConcurrentHashMap @Serializable @@ -210,12 +211,14 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW metadata.imageList?.getOrNull(index)?.let { findFile(it) } @Suppress("BlockingMethodInNonBlockingContext") - fun putImage(index: Int, fileName: String, data: ByteArray) { + fun putImage(index: Int, fileName: String, data: InputStream) { val file = cacheFolder.getChild(fileName) if (!file.exists()) file.createNewFile() - file.writeBytes(data) + file.outputStream()?.use { + data.copyTo(it) + } setMetadata { metadata -> metadata.imageList!![index] = fileName } }