mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-13 07:44:34 -05:00
Compare commits
5 Commits
6493eaaa02
...
renovate/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bc75d5b00 | ||
|
|
fe6dd05411 | ||
|
|
57c0a85a35 | ||
|
|
c2c927ae97 | ||
|
|
2c70700bb7 |
8
.github/workflows/build_pull_request.yml
vendored
8
.github/workflows/build_pull_request.yml
vendored
@@ -67,7 +67,7 @@ jobs:
|
|||||||
export LD_PRELOAD="$(pwd)/scripts/resources/catch_abort.so"
|
export LD_PRELOAD="$(pwd)/scripts/resources/catch_abort.so"
|
||||||
JAR=$(ls ./server/build/*.jar| head -1)
|
JAR=$(ls ./server/build/*.jar| head -1)
|
||||||
set +e
|
set +e
|
||||||
timeout 30s java \
|
timeout 30s java -DcrashOnFailedMigration=true \
|
||||||
-Dsuwayomi.tachidesk.config.server.systemTrayEnabled=false \
|
-Dsuwayomi.tachidesk.config.server.systemTrayEnabled=false \
|
||||||
-Dsuwayomi.tachidesk.config.server.initialOpenInBrowserEnabled=false \
|
-Dsuwayomi.tachidesk.config.server.initialOpenInBrowserEnabled=false \
|
||||||
-Dsuwayomi.tachidesk.config.server.databaseType=POSTGRESQL \
|
-Dsuwayomi.tachidesk.config.server.databaseType=POSTGRESQL \
|
||||||
@@ -83,7 +83,7 @@ jobs:
|
|||||||
exit "$ecode"
|
exit "$ecode"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
timeout 30s java \
|
timeout 30s java -DcrashOnFailedMigration=true \
|
||||||
-Dsuwayomi.tachidesk.config.server.systemTrayEnabled=false \
|
-Dsuwayomi.tachidesk.config.server.systemTrayEnabled=false \
|
||||||
-Dsuwayomi.tachidesk.config.server.initialOpenInBrowserEnabled=false \
|
-Dsuwayomi.tachidesk.config.server.initialOpenInBrowserEnabled=false \
|
||||||
-jar "$JAR"
|
-jar "$JAR"
|
||||||
@@ -96,10 +96,6 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
exit 0
|
exit 0
|
||||||
|
|
||||||
- name: "Run tests"
|
|
||||||
working-directory: master
|
|
||||||
run: ./gradlew test --stacktrace
|
|
||||||
|
|
||||||
check_docs:
|
check_docs:
|
||||||
name: Validate that all options are documented
|
name: Validate that all options are documented
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
1
.github/workflows/wiki.yml
vendored
1
.github/workflows/wiki.yml
vendored
@@ -1,7 +1,6 @@
|
|||||||
name: GitHub Wiki upload
|
name: GitHub Wiki upload
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,47 +0,0 @@
|
|||||||
package xyz.nulldev.androidcompat.webkit
|
|
||||||
|
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import org.cef.CefApp
|
|
||||||
import org.cef.CefClient
|
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger {}
|
|
||||||
|
|
||||||
object CefHelper {
|
|
||||||
val cefApp = MutableStateFlow<Result<CefApp?>>(Result.success(null))
|
|
||||||
|
|
||||||
suspend fun createClient(): CefClient {
|
|
||||||
val app = waitForInit().first()
|
|
||||||
val client = app.createClient()
|
|
||||||
JsHandler(client) // This adds itself to a global map
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
|
||||||
fun waitForInit() =
|
|
||||||
callbackFlow<CefApp> {
|
|
||||||
val app = cefApp.first { it.isFailure || it.getOrThrow() != null }.getOrThrow()!!
|
|
||||||
app.onInitialization {
|
|
||||||
logger.debug { "CEF: Initialization state $it" }
|
|
||||||
when (it) {
|
|
||||||
CefApp.CefAppState.INITIALIZED -> {
|
|
||||||
trySend(app)
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
CefApp.CefAppState.SHUTTING_DOWN, CefApp.CefAppState.TERMINATED -> {
|
|
||||||
close(CefException("Shutting down"))
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
awaitClose {}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CefException(
|
|
||||||
msg: String,
|
|
||||||
) : Exception(msg)
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
package xyz.nulldev.androidcompat.webkit
|
|
||||||
|
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import org.cef.CefClient
|
|
||||||
import org.cef.browser.CefBrowser
|
|
||||||
import org.cef.browser.CefFrame
|
|
||||||
import org.cef.browser.CefMessageRouter
|
|
||||||
import org.cef.callback.CefQueryCallback
|
|
||||||
import org.cef.handler.CefMessageRouterHandlerAdapter
|
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger {}
|
|
||||||
private val jsHandler: MutableMap<CefClient, JsHandler> = mutableMapOf()
|
|
||||||
|
|
||||||
fun CefBrowser.evaluateJavaScript(
|
|
||||||
expression: String,
|
|
||||||
cb: (String?) -> Unit,
|
|
||||||
) = jsHandler[this.client]!!.eval(this, expression, cb)
|
|
||||||
|
|
||||||
fun CefBrowser.dispose() {
|
|
||||||
stopLoad()
|
|
||||||
setCloseAllowed()
|
|
||||||
close(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
class JsHandler : CefMessageRouterHandlerAdapter {
|
|
||||||
private val handler: MutableMap<String, (String?) -> Unit> = mutableMapOf()
|
|
||||||
|
|
||||||
constructor(client: CefClient) {
|
|
||||||
val config = CefMessageRouter.CefMessageRouterConfig()
|
|
||||||
config.jsQueryFunction = QUERY_FN
|
|
||||||
config.jsCancelFunction = QUERY_CANCEL_FN
|
|
||||||
client.addMessageRouter(CefMessageRouter.create(config, this))
|
|
||||||
jsHandler[client] = this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun eval(
|
|
||||||
frame: CefFrame,
|
|
||||||
expression: String,
|
|
||||||
cb: (String?) -> Unit,
|
|
||||||
) {
|
|
||||||
val id = Random.nextBytes(48).toHexString()
|
|
||||||
handler[id] = cb
|
|
||||||
frame.executeJavaScript(expression.toCode(id), "about:cef", 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun eval(
|
|
||||||
browser: CefBrowser,
|
|
||||||
expression: String,
|
|
||||||
cb: (String?) -> Unit,
|
|
||||||
) {
|
|
||||||
val id = Random.nextBytes(48).toHexString()
|
|
||||||
handler[id] = cb
|
|
||||||
browser.executeJavaScript(expression.toCode(id), "about:cef", 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQuery(
|
|
||||||
browser: CefBrowser?,
|
|
||||||
frame: CefFrame?,
|
|
||||||
queryId: Long,
|
|
||||||
request: String?,
|
|
||||||
persistent: Boolean,
|
|
||||||
callback: CefQueryCallback?,
|
|
||||||
): Boolean {
|
|
||||||
super.onQuery(browser, frame, queryId, request, persistent, callback)
|
|
||||||
|
|
||||||
if (request != null) {
|
|
||||||
val invoke =
|
|
||||||
try {
|
|
||||||
Json.decodeFromString<FunctionCall>(request)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logger.warn(e) { "Invalid request received" }
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val handler = handler.remove(invoke.id) ?: return false
|
|
||||||
handler(invoke.result)
|
|
||||||
callback?.success("")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
private data class FunctionCall(
|
|
||||||
val id: String,
|
|
||||||
val result: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val QUERY_FN = "__\$_evalQuery"
|
|
||||||
const val QUERY_CANCEL_FN = "__\$_evalQueryCancel"
|
|
||||||
|
|
||||||
private fun Char.isLineBreak(): Boolean = this == '\n' || this == '\r'
|
|
||||||
|
|
||||||
private fun String.containsLineBreak(): Boolean =
|
|
||||||
this.any {
|
|
||||||
it.isLineBreak()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.asFunctionBody(): String =
|
|
||||||
let { expression ->
|
|
||||||
when {
|
|
||||||
expression.containsLineBreak() -> expression
|
|
||||||
expression.trim().startsWith("return", false) -> expression
|
|
||||||
else -> "return $expression"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.toCode(id: String): String =
|
|
||||||
"""
|
|
||||||
function payload() {
|
|
||||||
${this.asFunctionBody()}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
var result = payload();
|
|
||||||
|
|
||||||
window.${QUERY_FN}({
|
|
||||||
request: JSON.stringify({ id: "$id", result }),
|
|
||||||
onSuccess: function (response) {},
|
|
||||||
onFailure: function (error_code, error_message) {}
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to eval $id", e)
|
|
||||||
window.${QUERY_CANCEL_FN}({
|
|
||||||
request: JSON.stringify({ id: "$id", error: ""+e }),
|
|
||||||
onSuccess: function (response) {},
|
|
||||||
onFailure: function (error_code, error_message) {}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -51,10 +51,11 @@ import android.webkit.WebViewProvider.ScrollDelegate
|
|||||||
import android.webkit.WebViewProvider.ViewDelegate
|
import android.webkit.WebViewProvider.ViewDelegate
|
||||||
import android.webkit.WebViewRenderProcess
|
import android.webkit.WebViewRenderProcess
|
||||||
import android.webkit.WebViewRenderProcessClient
|
import android.webkit.WebViewRenderProcessClient
|
||||||
import kotlinx.coroutines.runBlocking
|
import dev.datlag.kcef.KCEF
|
||||||
|
import dev.datlag.kcef.KCEFBrowser
|
||||||
|
import dev.datlag.kcef.KCEFClient
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import org.cef.CefClient
|
|
||||||
import org.cef.CefSettings
|
import org.cef.CefSettings
|
||||||
import org.cef.browser.CefBrowser
|
import org.cef.browser.CefBrowser
|
||||||
import org.cef.browser.CefFrame
|
import org.cef.browser.CefFrame
|
||||||
@@ -86,7 +87,7 @@ import java.io.BufferedWriter
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.Executor
|
import java.util.concurrent.Executor
|
||||||
import kotlin.math.min
|
import kotlin.reflect.KClass
|
||||||
import kotlin.reflect.KFunction
|
import kotlin.reflect.KFunction
|
||||||
import kotlin.reflect.full.declaredMemberFunctions
|
import kotlin.reflect.full.declaredMemberFunctions
|
||||||
import kotlin.reflect.jvm.javaMethod
|
import kotlin.reflect.jvm.javaMethod
|
||||||
@@ -101,8 +102,8 @@ class KcefWebViewProvider(
|
|||||||
private val urlHttpMapping: MutableMap<String, String> = mutableMapOf()
|
private val urlHttpMapping: MutableMap<String, String> = mutableMapOf()
|
||||||
private var initialRequestData: InitialRequestData? = null
|
private var initialRequestData: InitialRequestData? = null
|
||||||
|
|
||||||
private var kcefClient: CefClient? = null
|
private var kcefClient: KCEFClient? = null
|
||||||
private var browser: CefBrowser? = null
|
private var browser: KCEFBrowser? = null
|
||||||
|
|
||||||
private val handler = Handler(view.webViewLooper)
|
private val handler = Handler(view.webViewLooper)
|
||||||
|
|
||||||
@@ -114,8 +115,8 @@ class KcefWebViewProvider(
|
|||||||
private val initHandler: InitBrowserHandler by KoinPlatformTools.defaultContext().get().inject()
|
private val initHandler: InitBrowserHandler by KoinPlatformTools.defaultContext().get().inject()
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InitBrowserHandler {
|
public interface InitBrowserHandler {
|
||||||
fun init(provider: KcefWebViewProvider): Unit
|
public fun init(provider: KcefWebViewProvider): Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class InitialRequestData(
|
private data class InitialRequestData(
|
||||||
@@ -191,7 +192,7 @@ class KcefWebViewProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DisplayHandler : CefDisplayHandlerAdapter() {
|
private inner class DisplayHandler : CefDisplayHandlerAdapter() {
|
||||||
override fun onConsoleMessage(
|
override fun onConsoleMessage(
|
||||||
browser: CefBrowser,
|
browser: CefBrowser,
|
||||||
level: CefSettings.LogSeverity,
|
level: CefSettings.LogSeverity,
|
||||||
@@ -219,7 +220,6 @@ class KcefWebViewProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private inner class LoadHandler : CefLoadHandlerAdapter() {
|
private inner class LoadHandler : CefLoadHandlerAdapter() {
|
||||||
override fun onLoadEnd(
|
override fun onLoadEnd(
|
||||||
browser: CefBrowser,
|
browser: CefBrowser,
|
||||||
@@ -366,7 +366,7 @@ class KcefWebViewProvider(
|
|||||||
callback: CefCallback,
|
callback: CefCallback,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val data = resolvedData ?: return false
|
val data = resolvedData ?: return false
|
||||||
val bytesToTransfer = min(bytesToRead, data.size - readOffset)
|
val bytesToTransfer = Math.min(bytesToRead, data.size - readOffset)
|
||||||
Log.v(
|
Log.v(
|
||||||
TAG,
|
TAG,
|
||||||
"readResponse: $readOffset/${data.size}, reading $bytesToRead->$bytesToTransfer",
|
"readResponse: $readOffset/${data.size}, reading $bytesToRead->$bytesToTransfer",
|
||||||
@@ -378,7 +378,7 @@ class KcefWebViewProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class WebResponseResourceHandler(
|
private inner class WebResponseResourceHandler(
|
||||||
val webResponse: WebResourceResponse,
|
val webResponse: WebResourceResponse,
|
||||||
) : ArrayResponseResourceHandler() {
|
) : ArrayResponseResourceHandler() {
|
||||||
override fun processRequest(
|
override fun processRequest(
|
||||||
@@ -408,7 +408,7 @@ class KcefWebViewProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class HtmlResponseResourceHandler(
|
private inner class HtmlResponseResourceHandler(
|
||||||
val html: String,
|
val html: String,
|
||||||
) : ArrayResponseResourceHandler() {
|
) : ArrayResponseResourceHandler() {
|
||||||
override fun processRequest(
|
override fun processRequest(
|
||||||
@@ -439,7 +439,7 @@ class KcefWebViewProvider(
|
|||||||
view,
|
view,
|
||||||
CefWebResourceRequest(request, frame, false),
|
CefWebResourceRequest(request, frame, false),
|
||||||
)
|
)
|
||||||
Log.v(TAG, "Resource ${request.url}, result is cancel? $cancel")
|
Log.v(TAG, "Resource ${request?.url}, result is cancel? $cancel")
|
||||||
|
|
||||||
handler.post { viewClient.onLoadResource(view, frame?.url) }
|
handler.post { viewClient.onLoadResource(view, frame?.url) }
|
||||||
|
|
||||||
@@ -466,7 +466,7 @@ class KcefWebViewProvider(
|
|||||||
}
|
}
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
// prefer user's response override
|
// prefer user's response override
|
||||||
urlHttpMapping[request.url.trimEnd('/')]?.let {
|
urlHttpMapping.get(request.url.trimEnd('/'))?.let {
|
||||||
return HtmlResponseResourceHandler(it)
|
return HtmlResponseResourceHandler(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -475,7 +475,6 @@ class KcefWebViewProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private inner class RequestHandler : CefRequestHandlerAdapter() {
|
private inner class RequestHandler : CefRequestHandlerAdapter() {
|
||||||
override fun getResourceRequestHandler(
|
override fun getResourceRequestHandler(
|
||||||
browser: CefBrowser,
|
browser: CefBrowser,
|
||||||
@@ -485,13 +484,11 @@ class KcefWebViewProvider(
|
|||||||
isDownload: Boolean,
|
isDownload: Boolean,
|
||||||
requestInitiator: String,
|
requestInitiator: String,
|
||||||
disableDefaultHandling: BoolRef,
|
disableDefaultHandling: BoolRef,
|
||||||
): CefResourceRequestHandler = ResourceRequestHandler()
|
): CefResourceRequestHandler? = ResourceRequestHandler()
|
||||||
|
|
||||||
override fun onRenderProcessTerminated(
|
override fun onRenderProcessTerminated(
|
||||||
browser: CefBrowser,
|
browser: CefBrowser,
|
||||||
status: CefRequestHandler.TerminationStatus,
|
status: CefRequestHandler.TerminationStatus,
|
||||||
errorCode: Int,
|
|
||||||
errorString: String,
|
|
||||||
) {
|
) {
|
||||||
handler.post {
|
handler.post {
|
||||||
viewClient.onRenderProcessGone(
|
viewClient.onRenderProcessGone(
|
||||||
@@ -510,13 +507,13 @@ class KcefWebViewProvider(
|
|||||||
override fun onRequestMediaAccessPermission(
|
override fun onRequestMediaAccessPermission(
|
||||||
browser: CefBrowser,
|
browser: CefBrowser,
|
||||||
frame: CefFrame,
|
frame: CefFrame,
|
||||||
requestingUrl: String,
|
requesting_url: String,
|
||||||
requestedPermissions: Int,
|
requested_permissions: Int,
|
||||||
callback: CefMediaAccessCallback,
|
callback: CefMediaAccessCallback,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
handler.post {
|
handler.post {
|
||||||
Log.v(TAG, "Checking permission for $requestingUrl: $requestedPermissions")
|
Log.v(TAG, "Checking permission for $requesting_url: $requested_permissions")
|
||||||
chromeClient.onPermissionRequest(CefPermissionRequest(requestingUrl, requestedPermissions, callback))
|
chromeClient.onPermissionRequest(CefPermissionRequest(requesting_url, requested_permissions, callback))
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -529,18 +526,16 @@ class KcefWebViewProvider(
|
|||||||
Log.v(TAG, "KcefWebViewProvider: initialize")
|
Log.v(TAG, "KcefWebViewProvider: initialize")
|
||||||
destroy()
|
destroy()
|
||||||
kcefClient =
|
kcefClient =
|
||||||
runBlocking {
|
KCEF.newClientBlocking().apply {
|
||||||
CefHelper.createClient().apply {
|
addDisplayHandler(DisplayHandler())
|
||||||
addDisplayHandler(DisplayHandler())
|
addLoadHandler(LoadHandler())
|
||||||
addLoadHandler(LoadHandler())
|
addRequestHandler(RequestHandler())
|
||||||
addRequestHandler(RequestHandler())
|
addPermissionHandler(PermissionHandler())
|
||||||
addPermissionHandler(PermissionHandler())
|
|
||||||
|
|
||||||
val config = CefMessageRouter.CefMessageRouterConfig()
|
val config = CefMessageRouter.CefMessageRouterConfig()
|
||||||
config.jsQueryFunction = QUERY_FN
|
config.jsQueryFunction = QUERY_FN
|
||||||
config.jsCancelFunction = QUERY_CANCEL_FN
|
config.jsCancelFunction = QUERY_CANCEL_FN
|
||||||
addMessageRouter(CefMessageRouter.create(config, MessageRouterHandler()))
|
addMessageRouter(CefMessageRouter.create(config, MessageRouterHandler()))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
initHandler.init(this)
|
initHandler.init(this)
|
||||||
}
|
}
|
||||||
@@ -618,7 +613,6 @@ class KcefWebViewProvider(
|
|||||||
.createBrowser(
|
.createBrowser(
|
||||||
loadUrl,
|
loadUrl,
|
||||||
CefRendering.OFFSCREEN,
|
CefRendering.OFFSCREEN,
|
||||||
false,
|
|
||||||
).apply {
|
).apply {
|
||||||
// NOTE: Without this, we don't seem to be receiving any events
|
// NOTE: Without this, we don't seem to be receiving any events
|
||||||
createImmediately()
|
createImmediately()
|
||||||
@@ -643,7 +637,6 @@ class KcefWebViewProvider(
|
|||||||
.createBrowser(
|
.createBrowser(
|
||||||
url,
|
url,
|
||||||
CefRendering.OFFSCREEN,
|
CefRendering.OFFSCREEN,
|
||||||
false,
|
|
||||||
).apply {
|
).apply {
|
||||||
// NOTE: Without this, we don't seem to be receiving any events
|
// NOTE: Without this, we don't seem to be receiving any events
|
||||||
createImmediately()
|
createImmediately()
|
||||||
@@ -669,19 +662,27 @@ class KcefWebViewProvider(
|
|||||||
browser?.close(true)
|
browser?.close(true)
|
||||||
browser?.dispose()
|
browser?.dispose()
|
||||||
chromeClient.onProgressChanged(view, 0)
|
chromeClient.onProgressChanged(view, 0)
|
||||||
val url = baseUrl ?: "about:blank"
|
|
||||||
urlHttpMapping[url.trimEnd('/')] = data
|
|
||||||
|
|
||||||
browser =
|
browser =
|
||||||
kcefClient!!
|
(
|
||||||
.createBrowser(
|
baseUrl?.let { url ->
|
||||||
url,
|
urlHttpMapping.put(url.trimEnd('/'), data)
|
||||||
CefRendering.OFFSCREEN,
|
kcefClient!!.createBrowser(
|
||||||
false,
|
url,
|
||||||
).apply {
|
CefRendering.OFFSCREEN,
|
||||||
// NOTE: Without this, we don't seem to be receiving any events
|
)
|
||||||
createImmediately()
|
|
||||||
}
|
}
|
||||||
|
?: run {
|
||||||
|
kcefClient!!.createBrowserWithHtml(
|
||||||
|
data,
|
||||||
|
KCEFBrowser.BLANK_URI,
|
||||||
|
CefRendering.OFFSCREEN,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
).apply {
|
||||||
|
// NOTE: Without this, we don't seem to be receiving any events
|
||||||
|
createImmediately()
|
||||||
|
}
|
||||||
Log.d(TAG, "Page loaded from data at base URL $baseUrl")
|
Log.d(TAG, "Page loaded from data at base URL $baseUrl")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -691,11 +692,11 @@ class KcefWebViewProvider(
|
|||||||
) {
|
) {
|
||||||
browser!!.evaluateJavaScript(
|
browser!!.evaluateJavaScript(
|
||||||
script.removePrefix("javascript:"),
|
script.removePrefix("javascript:"),
|
||||||
)
|
|
||||||
{
|
{
|
||||||
Log.v(TAG, "JS returned: $it")
|
Log.v(TAG, "JS returned: $it")
|
||||||
it?.let { handler.post { resultCallback?.onReceiveValue(it) } }
|
it?.let { handler.post { resultCallback?.onReceiveValue(it) } }
|
||||||
}
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun saveWebArchive(filename: String): Unit = throw RuntimeException("Stub!")
|
override fun saveWebArchive(filename: String): Unit = throw RuntimeException("Stub!")
|
||||||
@@ -837,7 +838,6 @@ class KcefWebViewProvider(
|
|||||||
|
|
||||||
override fun getWebChromeClient(): WebChromeClient = chromeClient
|
override fun getWebChromeClient(): WebChromeClient = chromeClient
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
override fun setPictureListener(listener: PictureListener): Unit = throw RuntimeException("Stub!")
|
override fun setPictureListener(listener: PictureListener): Unit = throw RuntimeException("Stub!")
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -860,7 +860,7 @@ class KcefWebViewProvider(
|
|||||||
obj: Any,
|
obj: Any,
|
||||||
interfaceName: String,
|
interfaceName: String,
|
||||||
) {
|
) {
|
||||||
val cls = obj::class
|
val cls = obj::class as KClass<Any>
|
||||||
mappings.addAll(
|
mappings.addAll(
|
||||||
cls.declaredMemberFunctions.map {
|
cls.declaredMemberFunctions.map {
|
||||||
// This is ridiculous, but necessary, otherwise "public final" throws
|
// This is ridiculous, but necessary, otherwise "public final" throws
|
||||||
@@ -922,8 +922,7 @@ class KcefWebViewProvider(
|
|||||||
override fun getRendererPriorityWaivedWhenNotVisible(): Boolean = throw RuntimeException("Stub!")
|
override fun getRendererPriorityWaivedWhenNotVisible(): Boolean = throw RuntimeException("Stub!")
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
override fun setTextClassifier(textClassifier: TextClassifier?) {
|
override fun setTextClassifier(textClassifier: TextClassifier?) {}
|
||||||
}
|
|
||||||
|
|
||||||
override fun getTextClassifier(): TextClassifier = TextClassifier.NO_OP
|
override fun getTextClassifier(): TextClassifier = TextClassifier.NO_OP
|
||||||
|
|
||||||
@@ -949,13 +948,11 @@ class KcefWebViewProvider(
|
|||||||
override fun onProvideAutofillVirtualStructure(
|
override fun onProvideAutofillVirtualStructure(
|
||||||
@SuppressWarnings("unused") structure: android.view.ViewStructure,
|
@SuppressWarnings("unused") structure: android.view.ViewStructure,
|
||||||
@SuppressWarnings("unused") flags: Int,
|
@SuppressWarnings("unused") flags: Int,
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
override fun autofill(
|
override fun autofill(
|
||||||
@SuppressWarnings("unused") values: SparseArray<AutofillValue>,
|
@SuppressWarnings("unused") values: SparseArray<AutofillValue>,
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
override fun isVisibleToUserForAutofill(
|
override fun isVisibleToUserForAutofill(
|
||||||
@SuppressWarnings("unused") virtualId: Int,
|
@SuppressWarnings("unused") virtualId: Int,
|
||||||
@@ -966,8 +963,7 @@ class KcefWebViewProvider(
|
|||||||
override fun onProvideContentCaptureStructure(
|
override fun onProvideContentCaptureStructure(
|
||||||
@SuppressWarnings("unused") structure: android.view.ViewStructure,
|
@SuppressWarnings("unused") structure: android.view.ViewStructure,
|
||||||
@SuppressWarnings("unused") flags: Int,
|
@SuppressWarnings("unused") flags: Int,
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
override fun getAccessibilityNodeProvider(): AccessibilityNodeProvider = throw RuntimeException("Stub!")
|
override fun getAccessibilityNodeProvider(): AccessibilityNodeProvider = throw RuntimeException("Stub!")
|
||||||
|
|
||||||
@@ -1037,8 +1033,7 @@ class KcefWebViewProvider(
|
|||||||
override fun onMovedToDisplay(
|
override fun onMovedToDisplay(
|
||||||
displayId: Int,
|
displayId: Int,
|
||||||
config: Configuration,
|
config: Configuration,
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onVisibilityChanged(
|
override fun onVisibilityChanged(
|
||||||
changedView: View,
|
changedView: View,
|
||||||
|
|||||||
21
CHANGELOG.md
21
CHANGELOG.md
@@ -10,24 +10,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|||||||
- .
|
- .
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- (**Database/H2**) Use the latest H2 database engine
|
- .
|
||||||
- (**Startup**) Crash on startup if an unrecoverable error happens
|
|
||||||
- (**WebView**) Use JCEF directly and update to newest Chromium
|
|
||||||
- (**Extension/Android**) Switch MessageQueue to LegacyMessageQueue from ConcurrentMessageQueue
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- (**CloudFlareInterceptor**) Don't send the `cf_clearance` cookie back to Flaresolverr
|
- (CloudFlareInterceptor) Don't send the `cf_clearance` cookie back to Flaresolverr
|
||||||
- (**WebUI**) Handle serving non-default webui with "bundled"
|
- (WebUI) Handle serving non-default webui with "bundled"
|
||||||
- (**WebUI**) Wait until WebUI is ready to open in browser
|
- (WebUI) Wait until WebUI is ready to open in browser
|
||||||
- (**Downloads**) Truncate filenames by byte length to prevent "File name too long" IO errors
|
- (Downloads) Truncate filenames by byte length to prevent "File name too long" IO errors
|
||||||
- (**Downloads**) Fix being unable to find downloads after manga was renamed during an update
|
|
||||||
- (**Downloads**) Fix preserving chapter download states during an update
|
|
||||||
- (**Extension**) Do not indicate an update is available when the extension is not installed
|
|
||||||
- (**Chapter**) Fix losing chapter data on failed chapter list update
|
|
||||||
- (**Chapter**) Fix database error when fetching chapter updates
|
|
||||||
- (**Manga/API**) Fix "mangas" graphql query with active sorting and using a postgresql database (QUERY "mangas")
|
|
||||||
- (**API**) Fix GraphQL `Filter` `notAll` and `notAny` being inversed
|
|
||||||
- (**API**) Fix GraphQL `Filter` causing an UnsupportedOperationException when passing an empty list as a `Any` filter value
|
|
||||||
|
|
||||||
## [v2.2.2100] + [WebUI: v20260508.01] - 2026-05-08
|
## [v2.2.2100] + [WebUI: v20260508.01] - 2026-05-08
|
||||||
|
|
||||||
|
|||||||
@@ -106,13 +106,13 @@ Download the latest `linux-x64`(x86_64) release from [the releases section](http
|
|||||||
|
|
||||||
#### WebView support (GNU/Linux)
|
#### WebView support (GNU/Linux)
|
||||||
|
|
||||||
WebView support is implemented via [JCEF](https://github.com/JetBrains/jcef).
|
WebView support is implemented via [KCEF](https://github.com/DATL4G/KCEF).
|
||||||
This is optional, and is only necessary to support some extensions.
|
This is optional, and is only necessary to support some extensions.
|
||||||
|
|
||||||
To have a functional WebView, several dependencies are required; aside from X11 libraries necessary for rendering Chromium, some JNI bindings are necessary: gluegen and jogl (found in Ubuntu as `libgluegen2-jni` and `libjogl2-jni`).
|
To have a functional WebView, several dependencies are required; aside from X11 libraries necessary for rendering Chromium, some JNI bindings are necessary: gluegen and jogl (found in Ubuntu as `libgluegen2-jni` and `libjogl2-jni`).
|
||||||
Note that on some systems (e.g. Ubuntu), the JNI libraries are not automatically found, see below.
|
Note that on some systems (e.g. Ubuntu), the JNI libraries are not automatically found, see below.
|
||||||
|
|
||||||
A CEF server is launched on startup, which loads the X11 libraries.
|
A KCEF server is launched on startup, which loads the X11 libraries.
|
||||||
If those are missing, you should see "Could not load 'jcef' library".
|
If those are missing, you should see "Could not load 'jcef' library".
|
||||||
If so, use `ldd ~/.local/share/Tachidesk/bin/kcef/libjcef.so | grep not` to figure out which libraries are not found on your system.
|
If so, use `ldd ~/.local/share/Tachidesk/bin/kcef/libjcef.so | grep not` to figure out which libraries are not found on your system.
|
||||||
|
|
||||||
@@ -123,10 +123,6 @@ This search path includes the current working directory, if you do not want to m
|
|||||||
|
|
||||||
Refer to the [Dockerfile](https://github.com/Suwayomi/Suwayomi-Server-docker/blob/main/Dockerfile) for more details.
|
Refer to the [Dockerfile](https://github.com/Suwayomi/Suwayomi-Server-docker/blob/main/Dockerfile) for more details.
|
||||||
|
|
||||||
Note that it is required to have an X session active and available to Suwayomi (i.e. `DISPLAY` is set).
|
|
||||||
It is not enough to have `WAYLAND_DISPLAY`, if your environment does not provide xwayland (or if you run Suwayomi as a service), you need to use a tool like [`Xvfb`](https://en.wikipedia.org/wiki/Xvfb).
|
|
||||||
The Dockerfile linked above also does this.
|
|
||||||
|
|
||||||
## Other methods of getting Suwayomi
|
## Other methods of getting Suwayomi
|
||||||
### Docker
|
### Docker
|
||||||
Check our Official Docker release [Suwayomi Container](https://github.com/orgs/Suwayomi/packages/container/package/tachidesk) for running Suwayomi Server in a docker container. Source code for our container is available at [docker-tachidesk](https://github.com/Suwayomi/docker-tachidesk), an example compose file can also be found there. By default, the server will be running on http://localhost:4567 open this url in your browser.
|
Check our Official Docker release [Suwayomi Container](https://github.com/orgs/Suwayomi/packages/container/package/tachidesk) for running Suwayomi Server in a docker container. Source code for our container is available at [docker-tachidesk](https://github.com/Suwayomi/docker-tachidesk), an example compose file can also be found there. By default, the server will be running on http://localhost:4567 open this url in your browser.
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ allprojects {
|
|||||||
maven("https://github.com/Suwayomi/Suwayomi-Server/raw/android-jar/")
|
maven("https://github.com/Suwayomi/Suwayomi-Server/raw/android-jar/")
|
||||||
maven("https://jitpack.io")
|
maven("https://jitpack.io")
|
||||||
maven("https://jogamp.org/deployment/maven")
|
maven("https://jogamp.org/deployment/maven")
|
||||||
maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ val getTachideskVersion = { "v2.2.${getCommitCount()}" }
|
|||||||
|
|
||||||
val webUIRevisionTag = "r3136"
|
val webUIRevisionTag = "r3136"
|
||||||
|
|
||||||
val webviewJbrRelease = "jbr-release-25.0.3b475.60"
|
|
||||||
|
|
||||||
private val getCommitCount = {
|
private val getCommitCount = {
|
||||||
runCatching {
|
runCatching {
|
||||||
ProcessBuilder()
|
ProcessBuilder()
|
||||||
|
|||||||
@@ -63,14 +63,6 @@ server.webUISubpath = ""
|
|||||||
- `server.webUIUpdateCheckInterval` the interval time in hours at which to check for updates. Use `0` to disable update checking.
|
- `server.webUIUpdateCheckInterval` the interval time in hours at which to check for updates. Use `0` to disable update checking.
|
||||||
- `server.webUISubpath` controls on which sub-path the UI is served; by default, it will be accessible on `/` (i.e. directly), with this setting it can also be set to appear at e.g. `/suwayomi`
|
- `server.webUISubpath` controls on which sub-path the UI is served; by default, it will be accessible on `/` (i.e. directly), with this setting it can also be set to appear at e.g. `/suwayomi`
|
||||||
|
|
||||||
|
|
||||||
### webView
|
|
||||||
```
|
|
||||||
server.kcefEnabled = true
|
|
||||||
```
|
|
||||||
- `server.kcefEnabled` controls if KCEF WebView provider is enabled.
|
|
||||||
|
|
||||||
|
|
||||||
### Downloader
|
### Downloader
|
||||||
```
|
```
|
||||||
server.downloadAsCbz = true
|
server.downloadAsCbz = true
|
||||||
|
|||||||
@@ -1,69 +1,19 @@
|
|||||||
# Troubleshooting
|
|
||||||
|
|
||||||
This page is laid out in several sections, where each section describes a specific problem, followed by one or more possible solutions.
|
|
||||||
|
|
||||||
At the end, you will find a General section, which is the nuclear option if nothing else works.
|
|
||||||
|
|
||||||
For further support, visit the [official Suwayomi Discord server](https://discord.gg/DDZdqZWaHA).
|
|
||||||
In such cases, it will be helpful to have logs ready. You can find them in [The Data Directory](./The-Data-Directory) in the logs directory.
|
|
||||||
|
|
||||||
**All steps below assume that you have stopped Suwayomi**.
|
|
||||||
|
|
||||||
|
|
||||||
## Broken database
|
|
||||||
|
|
||||||
- `failed due to
|
|
||||||
org.jetbrains.exposed.exceptions.ExposedSQLException: org.h2.jdbc.JdbcSQLSyntaxErrorException: Column "CATEGORY.SORT_ORDER" not found`
|
|
||||||
- `org.h2.jdbc.JdbcSQLSyntaxErrorException: Column "CHAPTER.KOREADER_HASH" not found`
|
|
||||||
- `java.lang.IllegalStateException: Unable to read the page at position 96170708817765466`
|
|
||||||
- Any other error text that includes "SQL Statement"
|
|
||||||
|
|
||||||
Your database is either corrupted or incompatible.
|
|
||||||
|
|
||||||
One of these is the cause:
|
|
||||||
- You were running a preview version and decided to downgrade to stable.
|
|
||||||
- You did not shut down Suwayomi properly.
|
|
||||||
- Suwayomi crashed in an unexpected way.
|
|
||||||
|
|
||||||
Solutions:
|
|
||||||
- If you downgraded, upgrade to preview again.
|
|
||||||
- Otherwise, you will need to reset and restore from a backup. See [General Troubleshooting](#general-troubleshooting) below.
|
|
||||||
|
|
||||||
|
|
||||||
## `HTTP error 429`
|
|
||||||
|
|
||||||
The source (or, if trackers are enabled, possibly the tracker) has blocked you for sending too many requests.
|
|
||||||
Note that Mass-Migration can result in an unexpectedly high number of requests to both the source and any configured trackers.
|
|
||||||
|
|
||||||
Solution: Use other/more sources, download less, and wait between request-heavy actions.
|
|
||||||
|
|
||||||
|
|
||||||
## Extension times out
|
|
||||||
|
|
||||||
- `Timed out waiting for 20000 ms…`
|
|
||||||
- `Timed out waiting for page list`
|
|
||||||
|
|
||||||
First, check if this is an extension issue or a Suwayomi issue.
|
|
||||||
On the manga page of the problematic entry, click "Open in WebView".
|
|
||||||
|
|
||||||
Solutions:
|
|
||||||
- If the WebView loads: The issue is with the extension. Search [the issues](https://github.com/Suwayomi/Suwayomi-Server/issues) and discord if there are known problems with that extension.
|
|
||||||
- If the WebView errors: Go to [The Data Directory](./The-Data-Directory) and remove the `bin` and `cache` folders.
|
|
||||||
- If the WebView still does not work after a restart, your installation is incomplete. On Linux, refer to [the README](https://github.com/Suwayomi/Suwayomi-Server#webview-support-gnulinux).
|
|
||||||
|
|
||||||
|
|
||||||
## General Troubleshooting
|
## General Troubleshooting
|
||||||
This guide will try to fix Suwayomi by reseting it to a clean installation state.
|
This guide will try to fix Suwayomi by reseting it to a clean installation state.
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> This will remove all your data, including the library.
|
|
||||||
> Make sure you have copied your backups as described above!
|
|
||||||
|
|
||||||
- Make sure you have a recent backup of your library or create one in the app (if possible) because we **are going to wipe all Suwayomi data**.
|
- Make sure you have a recent backup of your library or create one in the app (if possible) because we **are going to wipe all Suwayomi data**.
|
||||||
- Make sure Suwayomi is not running (right click on tray icon and quit or kill it through the way your Operating System provides)
|
- Make sure Suwayomi is not running (right click on tray icon and quit or kill it through the way your Operating System provides)
|
||||||
- Clear all browsing data on your browser if you use Suwayomi from a browser.
|
- Clear all browsing data on your browser if you use Suwayomi from a browser.
|
||||||
- Delete the Suwayomi data directory located below and re-run the app. See the article [The Data Directory](./The-Data-Directory) for information on how to find it.
|
- Delete the Suwayomi data directory located below and re-run the app.
|
||||||
- If you wish to keep your downloads, you may also attempt to surgically remove only parts. You will need to remove `database.mv.db`, `database.trace.db`, `bin`, `cache`, `extensions`, `settings`, `webUI`. Removing only a subset of these files and folders may fail to resolve the problem.
|
|
||||||
- Open Suwayomi and go to Settings > Backup > Restore Backup, and select the latest backup you have.
|
Note: Replace `<Account>` with the currently logged in account/username on your pc.
|
||||||
- Restoring from backup does not restore your downloads. If you chose to keep them in the above step, you will now need to re-download all manga. Suwayomi will pick up on the existing files and not actually download anything that isn't new.
|
|
||||||
|
On Mac OS X : `/Users/<Account>/Library/Application Support/Tachidesk`
|
||||||
|
|
||||||
|
On Windows XP : `C:\Documents and Settings\<Account>\Application Data\Local Settings\Tachidesk`
|
||||||
|
|
||||||
|
On Windows 7 and later : `C:\Users\<Account>\AppData\Local\Tachidesk`
|
||||||
|
|
||||||
|
On Unix/Linux : `/home/<account>/.local/share/Tachidesk`
|
||||||
|
|
||||||
- In the case that you have to periodically perform this fix or the problem persists or the method failed to fix it, open an issue or Join the [Suwayomi discord server](https://discord.gg/DDZdqZWaHA) to hang out with the community and to receive support and help.
|
- In the case that you have to periodically perform this fix or the problem persists or the method failed to fix it, open an issue or Join the [Suwayomi discord server](https://discord.gg/DDZdqZWaHA) to hang out with the community and to receive support and help.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ coroutines = "1.11.0"
|
|||||||
serialization = "1.11.0"
|
serialization = "1.11.0"
|
||||||
jvmTarget = "21"
|
jvmTarget = "21"
|
||||||
okhttp = "5.3.2" # Major version is locked by Tachiyomi extensions
|
okhttp = "5.3.2" # Major version is locked by Tachiyomi extensions
|
||||||
javalin = "7.2.2"
|
javalin = "7.2.0"
|
||||||
jte = "3.2.4"
|
jte = "3.2.4"
|
||||||
jackson = "3.1.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
|
jackson = "3.1.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
|
||||||
exposed = "1.2.0"
|
exposed = "1.2.0"
|
||||||
@@ -12,12 +12,11 @@ dex2jar = "2.4.36"
|
|||||||
polyglot = "25.0.3"
|
polyglot = "25.0.3"
|
||||||
settings = "1.3.0"
|
settings = "1.3.0"
|
||||||
twelvemonkeys = "3.13.1"
|
twelvemonkeys = "3.13.1"
|
||||||
graphqlkotlin = "10.0.0-alpha.4"
|
graphqlkotlin = "10.0.0-alpha.3"
|
||||||
xmlserialization = "0.91.3"
|
xmlserialization = "0.91.3"
|
||||||
ktlint = "1.8.0"
|
ktlint = "1.8.0"
|
||||||
koin = "4.2.1"
|
koin = "4.2.1"
|
||||||
moko = "0.26.4"
|
moko = "0.26.4"
|
||||||
jcef = "144.0.15-g72717cf-chromium-144.0.7559.172-api-1.21-262-b34"
|
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
# Kotlin
|
# Kotlin
|
||||||
@@ -38,7 +37,7 @@ serialization-xml-core = { module = "io.github.pdvrieze.xmlutil:core", version.r
|
|||||||
serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization-jvm", version.ref = "xmlserialization" }
|
serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization-jvm", version.ref = "xmlserialization" }
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
slf4japi = "org.slf4j:slf4j-api:2.0.18"
|
slf4japi = "org.slf4j:slf4j-api:2.0.17"
|
||||||
logback = "ch.qos.logback:logback-classic:1.5.32"
|
logback = "ch.qos.logback:logback-classic:1.5.32"
|
||||||
kotlinlogging = "io.github.oshai:kotlin-logging-jvm:8.0.02"
|
kotlinlogging = "io.github.oshai:kotlin-logging-jvm:8.0.02"
|
||||||
|
|
||||||
@@ -117,7 +116,7 @@ appdirs = "ca.gosyer:kotlin-multiplatform-appdirs:2.0.0"
|
|||||||
cache4k = "io.github.reactivecircus.cache4k:cache4k:0.14.0"
|
cache4k = "io.github.reactivecircus.cache4k:cache4k:0.14.0"
|
||||||
zip4j = "net.lingala.zip4j:zip4j:2.11.6"
|
zip4j = "net.lingala.zip4j:zip4j:2.11.6"
|
||||||
commonscompress = "org.apache.commons:commons-compress:1.28.0"
|
commonscompress = "org.apache.commons:commons-compress:1.28.0"
|
||||||
junrar = "com.github.junrar:junrar:7.6.0"
|
junrar = "com.github.junrar:junrar:7.5.10"
|
||||||
|
|
||||||
# AES/CBC/PKCS7Padding Cypher provider
|
# AES/CBC/PKCS7Padding Cypher provider
|
||||||
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84"
|
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84"
|
||||||
@@ -157,9 +156,7 @@ cron4j = "it.sauronsoftware.cron4j:cron4j:2.2.5"
|
|||||||
cronUtils = "com.cronutils:cron-utils:9.2.1"
|
cronUtils = "com.cronutils:cron-utils:9.2.1"
|
||||||
|
|
||||||
# Webview
|
# Webview
|
||||||
jcef = { module = "org.jetbrains.intellij.deps.jcef:jcef", version.ref = "jcef" }
|
kcef = "dev.datlag:kcef:2024.04.20.4"
|
||||||
gluegen = "org.jogamp.gluegen:gluegen-rt:2.5.0"
|
|
||||||
jogl = "org.jogamp.jogl:jogl-all:2.5.0"
|
|
||||||
|
|
||||||
# User
|
# User
|
||||||
jwt = "com.auth0:java-jwt:4.5.2"
|
jwt = "com.auth0:java-jwt:4.5.2"
|
||||||
@@ -216,7 +213,7 @@ shared = [
|
|||||||
"dex2jar-tools",
|
"dex2jar-tools",
|
||||||
"apk-parser",
|
"apk-parser",
|
||||||
"jackson-annotations",
|
"jackson-annotations",
|
||||||
"jcef",
|
"kcef"
|
||||||
]
|
]
|
||||||
|
|
||||||
sharedTest = [
|
sharedTest = [
|
||||||
|
|||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
retries=0
|
retries=0
|
||||||
retryBackOffMs=500
|
retryBackOffMs=500
|
||||||
|
|||||||
@@ -37,10 +37,6 @@ dependencies {
|
|||||||
implementation(libs.bundles.shared)
|
implementation(libs.bundles.shared)
|
||||||
testImplementation(libs.bundles.sharedTest)
|
testImplementation(libs.bundles.sharedTest)
|
||||||
|
|
||||||
// WebView
|
|
||||||
implementation(libs.gluegen)
|
|
||||||
implementation(libs.jogl)
|
|
||||||
|
|
||||||
// OkHttp
|
// OkHttp
|
||||||
implementation(libs.bundles.okhttp)
|
implementation(libs.bundles.okhttp)
|
||||||
implementation(libs.okio)
|
implementation(libs.okio)
|
||||||
@@ -163,8 +159,6 @@ buildConfig {
|
|||||||
|
|
||||||
buildConfigField("String", "GITHUB", quoteWrap("https://github.com/Suwayomi/Suwayomi-Server"))
|
buildConfigField("String", "GITHUB", quoteWrap("https://github.com/Suwayomi/Suwayomi-Server"))
|
||||||
buildConfigField("String", "DISCORD", quoteWrap("https://discord.gg/DDZdqZWaHA"))
|
buildConfigField("String", "DISCORD", quoteWrap("https://discord.gg/DDZdqZWaHA"))
|
||||||
buildConfigField("String", "JCEF_VERSION", quoteWrap(libs.versions.jcef.get()))
|
|
||||||
buildConfigField("String", "JCEF_JBR_RELEASE", quoteWrap(webviewJbrRelease))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
@@ -178,7 +172,6 @@ tasks {
|
|||||||
"Specification-Version" to getTachideskVersion(),
|
"Specification-Version" to getTachideskVersion(),
|
||||||
"Implementation-Version" to getTachideskRevision(),
|
"Implementation-Version" to getTachideskRevision(),
|
||||||
"Multi-Release" to true, // needed for polyglot
|
"Multi-Release" to true, // needed for polyglot
|
||||||
"X-JBR-Release" to webviewJbrRelease,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
archiveBaseName.set(rootProject.name)
|
archiveBaseName.set(rootProject.name)
|
||||||
@@ -189,11 +182,7 @@ tasks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
useJUnitPlatform {
|
useJUnitPlatform()
|
||||||
if (!project.hasProperty("masstest")) {
|
|
||||||
exclude("**/masstest/*")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
testLogging {
|
testLogging {
|
||||||
showStandardStreams = true
|
showStandardStreams = true
|
||||||
events("passed", "skipped", "failed")
|
events("passed", "skipped", "failed")
|
||||||
|
|||||||
@@ -1014,14 +1014,6 @@ class ServerConfig(
|
|||||||
description = "Use Hikari Connection Pool to connect to the database.",
|
description = "Use Hikari Connection Pool to connect to the database.",
|
||||||
)
|
)
|
||||||
|
|
||||||
val kcefEnabled: MutableStateFlow<Boolean> by BooleanSetting(
|
|
||||||
protoNumber = 86,
|
|
||||||
group = SettingGroup.WEB_VIEW,
|
|
||||||
privacySafe = true,
|
|
||||||
defaultValue = true,
|
|
||||||
description = "Enable the WebView via CEF (Chromium)"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** ****************************************************************** **/
|
/** ****************************************************************** **/
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ enum class SettingGroup(
|
|||||||
CLOUDFLARE("Cloudflare"),
|
CLOUDFLARE("Cloudflare"),
|
||||||
OPDS("OPDS"),
|
OPDS("OPDS"),
|
||||||
KOREADER_SYNC("KOReader sync"),
|
KOREADER_SYNC("KOReader sync"),
|
||||||
WEB_VIEW("WebView"),
|
|
||||||
;
|
;
|
||||||
|
|
||||||
override fun toString(): String = value
|
override fun toString(): String = value
|
||||||
|
|||||||
@@ -88,6 +88,4 @@ object SettingsRegistry {
|
|||||||
fun get(name: String): SettingMetadata? = settings[name]
|
fun get(name: String): SettingMetadata? = settings[name]
|
||||||
|
|
||||||
fun getAll(): Map<String, SettingMetadata> = settings.toMap()
|
fun getAll(): Map<String, SettingMetadata> = settings.toMap()
|
||||||
|
|
||||||
fun clear() = settings.clear()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
package suwayomi.tachidesk.global.impl
|
package suwayomi.tachidesk.global.impl
|
||||||
|
|
||||||
|
import dev.datlag.kcef.KCEF
|
||||||
|
import dev.datlag.kcef.KCEFBrowser
|
||||||
|
import dev.datlag.kcef.KCEFClient
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Cookie
|
import okhttp3.Cookie
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import org.cef.CefClient
|
|
||||||
import org.cef.CefSettings
|
import org.cef.CefSettings
|
||||||
import org.cef.browser.CefBrowser
|
import org.cef.browser.CefBrowser
|
||||||
import org.cef.browser.CefFrame
|
import org.cef.browser.CefFrame
|
||||||
@@ -25,9 +26,6 @@ import org.cef.network.CefCookie
|
|||||||
import org.cef.network.CefCookieManager
|
import org.cef.network.CefCookieManager
|
||||||
import org.cef.network.CefRequest
|
import org.cef.network.CefRequest
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import xyz.nulldev.androidcompat.webkit.CefHelper
|
|
||||||
import xyz.nulldev.androidcompat.webkit.dispose
|
|
||||||
import xyz.nulldev.androidcompat.webkit.evaluateJavaScript
|
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
import java.awt.HeadlessException
|
import java.awt.HeadlessException
|
||||||
import java.awt.Rectangle
|
import java.awt.Rectangle
|
||||||
@@ -49,8 +47,8 @@ import javax.swing.JPanel
|
|||||||
class KcefWebView {
|
class KcefWebView {
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
private val renderHandler = RenderHandler()
|
private val renderHandler = RenderHandler()
|
||||||
private var kcefClient: CefClient? = null
|
private var kcefClient: KCEFClient? = null
|
||||||
private var browser: CefBrowser? = null
|
private var browser: KCEFBrowser? = null
|
||||||
private var width = 1000
|
private var width = 1000
|
||||||
private var height = 1000
|
private var height = 1000
|
||||||
|
|
||||||
@@ -78,8 +76,7 @@ class KcefWebView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable sealed class Event
|
||||||
sealed class Event
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("consoleMessage")
|
@SerialName("consoleMessage")
|
||||||
@@ -250,12 +247,10 @@ class KcefWebView {
|
|||||||
init {
|
init {
|
||||||
destroy()
|
destroy()
|
||||||
kcefClient =
|
kcefClient =
|
||||||
runBlocking {
|
KCEF.newClientBlocking().apply {
|
||||||
CefHelper.createClient().apply {
|
addDisplayHandler(DisplayHandler())
|
||||||
addDisplayHandler(DisplayHandler())
|
addLoadHandler(LoadHandler())
|
||||||
addLoadHandler(LoadHandler())
|
addRequestHandler(RequestHandler())
|
||||||
addRequestHandler(RequestHandler())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug { "Start loading cookies" }
|
logger.debug { "Start loading cookies" }
|
||||||
@@ -294,7 +289,6 @@ class KcefWebView {
|
|||||||
.createBrowser(
|
.createBrowser(
|
||||||
url,
|
url,
|
||||||
CefRendering.CefRenderingWithHandler(renderHandler, JPanel()),
|
CefRendering.CefRenderingWithHandler(renderHandler, JPanel()),
|
||||||
false,
|
|
||||||
// NOTE: with a context, we don't seem to be getting any cookies
|
// NOTE: with a context, we don't seem to be getting any cookies
|
||||||
).apply {
|
).apply {
|
||||||
// NOTE: Without this, we don't seem to be receiving any events
|
// NOTE: Without this, we don't seem to be receiving any events
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
|||||||
* Metadata storage for clients, server/global level.
|
* Metadata storage for clients, server/global level.
|
||||||
*/
|
*/
|
||||||
object GlobalMetaTable : IntIdTable() {
|
object GlobalMetaTable : IntIdTable() {
|
||||||
val key = varchar("meta_key", 256)
|
val key = varchar("key", 256)
|
||||||
val value = varchar("value", 4096)
|
val value = varchar("value", 4096)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,11 +15,9 @@ import org.jetbrains.exposed.v1.core.Op
|
|||||||
import org.jetbrains.exposed.v1.core.SortOrder
|
import org.jetbrains.exposed.v1.core.SortOrder
|
||||||
import org.jetbrains.exposed.v1.core.greater
|
import org.jetbrains.exposed.v1.core.greater
|
||||||
import org.jetbrains.exposed.v1.core.inList
|
import org.jetbrains.exposed.v1.core.inList
|
||||||
import org.jetbrains.exposed.v1.core.inSubQuery
|
|
||||||
import org.jetbrains.exposed.v1.core.less
|
import org.jetbrains.exposed.v1.core.less
|
||||||
import org.jetbrains.exposed.v1.core.like
|
import org.jetbrains.exposed.v1.core.like
|
||||||
import org.jetbrains.exposed.v1.jdbc.select
|
import org.jetbrains.exposed.v1.jdbc.select
|
||||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
|
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
|
||||||
@@ -245,16 +243,13 @@ class MangaQuery {
|
|||||||
): MangaNodeList {
|
): MangaNodeList {
|
||||||
val queryResults =
|
val queryResults =
|
||||||
transaction {
|
transaction {
|
||||||
val mangaIdsQuery =
|
val res =
|
||||||
MangaTable
|
MangaTable
|
||||||
.leftJoin(CategoryMangaTable)
|
.leftJoin(CategoryMangaTable)
|
||||||
.select(MangaTable.id)
|
.select(MangaTable.columns)
|
||||||
.withDistinct()
|
.withDistinctOn(MangaTable.id)
|
||||||
|
|
||||||
mangaIdsQuery.applyOps(condition, filter)
|
res.applyOps(condition, filter)
|
||||||
|
|
||||||
val res =
|
|
||||||
MangaTable.selectAll().where { MangaTable.id inSubQuery mangaIdsQuery }
|
|
||||||
|
|
||||||
if (order != null || orderBy != null || (last != null || before != null)) {
|
if (order != null || orderBy != null || (last != null || before != null)) {
|
||||||
val baseSort = listOf(MangaOrder(MangaOrderBy.ID, SortOrder.ASC))
|
val baseSort = listOf(MangaOrder(MangaOrderBy.ID, SortOrder.ASC))
|
||||||
|
|||||||
@@ -435,7 +435,7 @@ fun <T : String, S : T?> andFilterWithCompareString(
|
|||||||
|
|
||||||
opAnd.andWhere(filter.isNull) { if (it) column.isNull() else column.isNotNull() }
|
opAnd.andWhere(filter.isNull) { if (it) column.isNull() else column.isNotNull() }
|
||||||
opAnd.andWhere(filter.equalTo) { column eq it as S }
|
opAnd.andWhere(filter.equalTo) { column eq it as S }
|
||||||
opAnd.andNotWhere(filter.notEqualTo, filter.notEqualToAll, filter.notEqualToAny) { column neq it as S }
|
opAnd.andWhere(filter.notEqualTo, filter.notEqualToAll, filter.notEqualToAny) { column neq it as S }
|
||||||
opAnd.andWhere(
|
opAnd.andWhere(
|
||||||
filter.distinctFrom,
|
filter.distinctFrom,
|
||||||
filter.distinctFromAll,
|
filter.distinctFromAll,
|
||||||
@@ -455,36 +455,36 @@ fun <T : String, S : T?> andFilterWithCompareString(
|
|||||||
opAnd.andWhere(filter.greaterThanOrEqualTo) { column greaterEq it }
|
opAnd.andWhere(filter.greaterThanOrEqualTo) { column greaterEq it }
|
||||||
|
|
||||||
opAnd.andWhere(filter.includes, filter.includesAll, filter.includesAny) { column like "%$it%" }
|
opAnd.andWhere(filter.includes, filter.includesAll, filter.includesAny) { column like "%$it%" }
|
||||||
opAnd.andNotWhere(filter.notIncludes, filter.notIncludesAll, filter.notIncludesAny) { column notLike "%$it%" }
|
opAnd.andWhere(filter.notIncludes, filter.notIncludesAll, filter.notIncludesAny) { column notLike "%$it%" }
|
||||||
opAnd.andWhere(filter.includesInsensitive, filter.includesInsensitiveAll, filter.includesInsensitiveAny) {
|
opAnd.andWhere(filter.includesInsensitive, filter.includesInsensitiveAll, filter.includesInsensitiveAny) {
|
||||||
ILikeEscapeOp.iLike(column, "%$it%")
|
ILikeEscapeOp.iLike(column, "%$it%")
|
||||||
}
|
}
|
||||||
opAnd.andNotWhere(filter.notIncludesInsensitive, filter.notIncludesInsensitiveAll, filter.notIncludesInsensitiveAny) {
|
opAnd.andWhere(filter.notIncludesInsensitive, filter.notIncludesInsensitiveAll, filter.notIncludesInsensitiveAny) {
|
||||||
ILikeEscapeOp.iNotLike(column, "%$it%")
|
ILikeEscapeOp.iNotLike(column, "%$it%")
|
||||||
}
|
}
|
||||||
|
|
||||||
opAnd.andWhere(filter.startsWith, filter.startsWithAll, filter.startsWithAny) { column like "$it%" }
|
opAnd.andWhere(filter.startsWith, filter.startsWithAll, filter.startsWithAny) { column like "$it%" }
|
||||||
opAnd.andNotWhere(filter.notStartsWith, filter.notStartsWithAll, filter.notStartsWithAny) { column notLike "$it%" }
|
opAnd.andWhere(filter.notStartsWith, filter.notStartsWithAll, filter.notStartsWithAny) { column notLike "$it%" }
|
||||||
opAnd.andWhere(filter.startsWithInsensitive, filter.startsWithInsensitiveAll, filter.startsWithInsensitiveAny) {
|
opAnd.andWhere(filter.startsWithInsensitive, filter.startsWithInsensitiveAll, filter.startsWithInsensitiveAny) {
|
||||||
ILikeEscapeOp.iLike(column, "$it%")
|
ILikeEscapeOp.iLike(column, "$it%")
|
||||||
}
|
}
|
||||||
opAnd.andNotWhere(filter.notStartsWithInsensitive, filter.notStartsWithInsensitiveAll, filter.notStartsWithInsensitiveAny) {
|
opAnd.andWhere(filter.notStartsWithInsensitive, filter.notStartsWithInsensitiveAll, filter.notStartsWithInsensitiveAny) {
|
||||||
ILikeEscapeOp.iNotLike(column, "$it%")
|
ILikeEscapeOp.iNotLike(column, "$it%")
|
||||||
}
|
}
|
||||||
|
|
||||||
opAnd.andWhere(filter.endsWith, filter.endsWithAll, filter.endsWithAny) { column like "%$it" }
|
opAnd.andWhere(filter.endsWith, filter.endsWithAll, filter.endsWithAny) { column like "%$it" }
|
||||||
opAnd.andNotWhere(filter.notEndsWith, filter.notEndsWithAll, filter.notEndsWithAny) { column notLike "%$it" }
|
opAnd.andWhere(filter.notEndsWith, filter.notEndsWithAll, filter.notEndsWithAny) { column notLike "%$it" }
|
||||||
opAnd.andWhere(filter.endsWithInsensitive, filter.endsWithInsensitiveAll, filter.endsWithInsensitiveAny) {
|
opAnd.andWhere(filter.endsWithInsensitive, filter.endsWithInsensitiveAll, filter.endsWithInsensitiveAny) {
|
||||||
ILikeEscapeOp.iLike(column, "%$it")
|
ILikeEscapeOp.iLike(column, "%$it")
|
||||||
}
|
}
|
||||||
opAnd.andNotWhere(filter.notEndsWithInsensitive, filter.notEndsWithInsensitiveAll, filter.notEndsWithInsensitiveAny) {
|
opAnd.andWhere(filter.notEndsWithInsensitive, filter.notEndsWithInsensitiveAll, filter.notEndsWithInsensitiveAny) {
|
||||||
ILikeEscapeOp.iNotLike(column, "%$it")
|
ILikeEscapeOp.iNotLike(column, "%$it")
|
||||||
}
|
}
|
||||||
|
|
||||||
opAnd.andWhere(filter.like, filter.likeAll, filter.likeAny) { column like it }
|
opAnd.andWhere(filter.like, filter.likeAll, filter.likeAny) { column like it }
|
||||||
opAnd.andNotWhere(filter.notLike, filter.notLikeAll, filter.notLikeAny) { column notLike it }
|
opAnd.andWhere(filter.notLike, filter.notLikeAll, filter.notLikeAny) { column notLike it }
|
||||||
opAnd.andWhere(filter.likeInsensitive, filter.likeInsensitiveAll, filter.likeInsensitiveAny) { ILikeEscapeOp.iLike(column, it) }
|
opAnd.andWhere(filter.likeInsensitive, filter.likeInsensitiveAll, filter.likeInsensitiveAny) { ILikeEscapeOp.iLike(column, it) }
|
||||||
opAnd.andNotWhere(filter.notLikeInsensitive, filter.notLikeInsensitiveAll, filter.notLikeInsensitiveAny) {
|
opAnd.andWhere(filter.notLikeInsensitive, filter.notLikeInsensitiveAll, filter.notLikeInsensitiveAny) {
|
||||||
ILikeEscapeOp.iNotLike(column, it)
|
ILikeEscapeOp.iNotLike(column, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -535,17 +535,6 @@ class OpAnd(
|
|||||||
andWhereAny(valueAny, expr)
|
andWhereAny(valueAny, expr)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T : Any> andNotWhere(
|
|
||||||
valueDefault: T?,
|
|
||||||
valueAll: List<T>?,
|
|
||||||
valueAny: List<T>?,
|
|
||||||
expr: (T) -> Op<Boolean>,
|
|
||||||
) {
|
|
||||||
andWhere(valueDefault, expr)
|
|
||||||
andNotWhereAll(valueAll, expr)
|
|
||||||
andNotWhereAny(valueAny, expr)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T : Any> andWhereAll(
|
fun <T : Any> andWhereAll(
|
||||||
values: List<T>?,
|
values: List<T>?,
|
||||||
andPart: (T) -> Op<Boolean>,
|
andPart: (T) -> Op<Boolean>,
|
||||||
@@ -553,31 +542,15 @@ class OpAnd(
|
|||||||
values?.map { andWhere(it, andPart) }
|
values?.map { andWhere(it, andPart) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T : Any> andNotWhereAll(
|
|
||||||
values: List<T>?,
|
|
||||||
andPart: (T) -> Op<Boolean>,
|
|
||||||
) {
|
|
||||||
// Inversed all equals any
|
|
||||||
andWhereAny(values, andPart)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T : Any> andWhereAny(
|
fun <T : Any> andWhereAny(
|
||||||
values: List<T>?,
|
values: List<T>?,
|
||||||
andPart: (T) -> Op<Boolean>,
|
andPart: (T) -> Op<Boolean>,
|
||||||
) {
|
) {
|
||||||
values ?: return
|
values ?: return
|
||||||
val expr = values.map { andPart(it) }.reduceOrNull { acc, op -> acc or op } ?: return
|
val expr = values.map { andPart(it) }.reduce { acc, op -> acc or op }
|
||||||
op = if (op == null) expr else (op!! and expr)
|
op = if (op == null) expr else (op!! and expr)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T : Any> andNotWhereAny(
|
|
||||||
values: List<T>?,
|
|
||||||
andPart: (T) -> Op<Boolean>,
|
|
||||||
) {
|
|
||||||
// Inversed any equals all
|
|
||||||
andWhereAll(values, andPart)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> eq(
|
fun <T> eq(
|
||||||
value: T?,
|
value: T?,
|
||||||
column: Column<T>,
|
column: Column<T>,
|
||||||
@@ -605,7 +578,7 @@ fun <T : Comparable<T>, S : T?> andFilterWithCompare(
|
|||||||
opAnd.andWhere(filter.isNull) { if (it) column.isNull() else column.isNotNull() }
|
opAnd.andWhere(filter.isNull) { if (it) column.isNull() else column.isNotNull() }
|
||||||
|
|
||||||
opAnd.andWhere(filter.equalTo) { column eq it as S }
|
opAnd.andWhere(filter.equalTo) { column eq it as S }
|
||||||
opAnd.andNotWhere(filter.notEqualTo, filter.notEqualToAll, filter.notEqualToAny) { column neq it as S }
|
opAnd.andWhere(filter.notEqualTo, filter.notEqualToAll, filter.notEqualToAny) { column neq it as S }
|
||||||
opAnd.andWhere(filter.distinctFrom, filter.distinctFromAll, filter.distinctFromAny) { DistinctFromOp.distinctFrom(column, it as S) }
|
opAnd.andWhere(filter.distinctFrom, filter.distinctFromAll, filter.distinctFromAny) { DistinctFromOp.distinctFrom(column, it as S) }
|
||||||
opAnd.andWhere(filter.notDistinctFrom) { DistinctFromOp.notDistinctFrom(column, it as S) }
|
opAnd.andWhere(filter.notDistinctFrom) { DistinctFromOp.notDistinctFrom(column, it as S) }
|
||||||
if (!filter.`in`.isNullOrEmpty()) {
|
if (!filter.`in`.isNullOrEmpty()) {
|
||||||
@@ -633,7 +606,7 @@ fun <T : Comparable<T>> andFilterWithCompareEntity(
|
|||||||
opAnd.andWhere(filter.isNull) { if (it) column.isNull() else column.isNotNull() }
|
opAnd.andWhere(filter.isNull) { if (it) column.isNull() else column.isNotNull() }
|
||||||
|
|
||||||
opAnd.andWhere(filter.equalTo) { column eq it }
|
opAnd.andWhere(filter.equalTo) { column eq it }
|
||||||
opAnd.andNotWhere(filter.notEqualTo, filter.notEqualToAll, filter.notEqualToAny) { column neq it }
|
opAnd.andWhere(filter.notEqualTo, filter.notEqualToAll, filter.notEqualToAny) { column neq it }
|
||||||
opAnd.andWhere(filter.distinctFrom, filter.distinctFromAll, filter.distinctFromAny) { DistinctFromOp.distinctFrom(column, it) }
|
opAnd.andWhere(filter.distinctFrom, filter.distinctFromAll, filter.distinctFromAny) { DistinctFromOp.distinctFrom(column, it) }
|
||||||
opAnd.andWhere(filter.notDistinctFrom) { DistinctFromOp.notDistinctFrom(column, it) }
|
opAnd.andWhere(filter.notDistinctFrom) { DistinctFromOp.notDistinctFrom(column, it) }
|
||||||
if (!filter.`in`.isNullOrEmpty()) {
|
if (!filter.`in`.isNullOrEmpty()) {
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ object Chapter {
|
|||||||
val deletedChapterNumbers = TreeSet<Float>()
|
val deletedChapterNumbers = TreeSet<Float>()
|
||||||
val deletedReadChapterNumbers = TreeSet<Float>()
|
val deletedReadChapterNumbers = TreeSet<Float>()
|
||||||
val deletedBookmarkedChapterNumbers = TreeSet<Float>()
|
val deletedBookmarkedChapterNumbers = TreeSet<Float>()
|
||||||
val deletedDownloadedChapterNumberToChapter = mutableMapOf<Float, ChapterDataClass>()
|
val deletedDownloadedChapterNumberInfoMap = mutableMapOf<Float, MutableMap<String?, Int>>()
|
||||||
val deletedChapterNumberDateFetchMap = mutableMapOf<Float, Long>()
|
val deletedChapterNumberDateFetchMap = mutableMapOf<Float, Long>()
|
||||||
|
|
||||||
// clear any orphaned/duplicate chapters that are in the db but not in `chapterList`
|
// clear any orphaned/duplicate chapters that are in the db but not in `chapterList`
|
||||||
@@ -247,7 +247,13 @@ object Chapter {
|
|||||||
if (!chapterUrls.contains(dbChapter.url)) {
|
if (!chapterUrls.contains(dbChapter.url)) {
|
||||||
if (dbChapter.read) deletedReadChapterNumbers.add(dbChapter.chapterNumber)
|
if (dbChapter.read) deletedReadChapterNumbers.add(dbChapter.chapterNumber)
|
||||||
if (dbChapter.bookmarked) deletedBookmarkedChapterNumbers.add(dbChapter.chapterNumber)
|
if (dbChapter.bookmarked) deletedBookmarkedChapterNumbers.add(dbChapter.chapterNumber)
|
||||||
if (dbChapter.downloaded) deletedDownloadedChapterNumberToChapter[dbChapter.chapterNumber] = dbChapter
|
if (dbChapter.downloaded) {
|
||||||
|
val pageCountByScanlator =
|
||||||
|
deletedDownloadedChapterNumberInfoMap.getOrPut(
|
||||||
|
dbChapter.chapterNumber,
|
||||||
|
) { mutableMapOf() }
|
||||||
|
pageCountByScanlator[dbChapter.scanlator] = dbChapter.pageCount
|
||||||
|
}
|
||||||
deletedChapterNumbers.add(dbChapter.chapterNumber)
|
deletedChapterNumbers.add(dbChapter.chapterNumber)
|
||||||
deletedChapterNumberDateFetchMap[dbChapter.chapterNumber] = dbChapter.fetchedAt
|
deletedChapterNumberDateFetchMap[dbChapter.chapterNumber] = dbChapter.fetchedAt
|
||||||
dbChapter.id
|
dbChapter.id
|
||||||
@@ -256,14 +262,16 @@ object Chapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction {
|
// we got some clean up due
|
||||||
// we got some clean up due
|
if (chaptersIdsToDelete.isNotEmpty()) {
|
||||||
if (chaptersIdsToDelete.isNotEmpty()) {
|
DownloadManager.dequeue(chaptersIdsToDelete)
|
||||||
DownloadManager.dequeue(chaptersIdsToDelete)
|
transaction {
|
||||||
PageTable.deleteWhere { chapter inList chaptersIdsToDelete }
|
PageTable.deleteWhere { chapter inList chaptersIdsToDelete }
|
||||||
ChapterTable.deleteWhere { id inList chaptersIdsToDelete }
|
ChapterTable.deleteWhere { id inList chaptersIdsToDelete }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction {
|
||||||
if (chaptersToInsert.isNotEmpty()) {
|
if (chaptersToInsert.isNotEmpty()) {
|
||||||
ChapterTable
|
ChapterTable
|
||||||
.batchInsert(chaptersToInsert) { chapter ->
|
.batchInsert(chaptersToInsert) { chapter ->
|
||||||
@@ -279,31 +287,24 @@ object Chapter {
|
|||||||
this[ChapterTable.isRead] = false
|
this[ChapterTable.isRead] = false
|
||||||
this[ChapterTable.isBookmarked] = false
|
this[ChapterTable.isBookmarked] = false
|
||||||
this[ChapterTable.isDownloaded] = false
|
this[ChapterTable.isDownloaded] = false
|
||||||
this[ChapterTable.pageCount] = -1
|
|
||||||
|
|
||||||
// is recognized chapter number
|
// is recognized chapter number
|
||||||
if (chapter.chapterNumber >= 0f && chapter.chapterNumber in deletedChapterNumbers) {
|
if (chapter.chapterNumber >= 0f && chapter.chapterNumber in deletedChapterNumbers) {
|
||||||
this[ChapterTable.isRead] = chapter.chapterNumber in deletedReadChapterNumbers
|
this[ChapterTable.isRead] = chapter.chapterNumber in deletedReadChapterNumbers
|
||||||
this[ChapterTable.isBookmarked] = chapter.chapterNumber in deletedBookmarkedChapterNumbers
|
this[ChapterTable.isBookmarked] = chapter.chapterNumber in deletedBookmarkedChapterNumbers
|
||||||
|
|
||||||
|
// only preserve download status for chapters of the same scanlator, otherwise,
|
||||||
|
// the downloaded files won't be found anyway
|
||||||
|
val downloadedChapterInfo = deletedDownloadedChapterNumberInfoMap[chapter.chapterNumber]
|
||||||
|
val pageCount = downloadedChapterInfo?.get(chapter.scanlator)
|
||||||
|
if (pageCount != null) {
|
||||||
|
this[ChapterTable.isDownloaded] = true
|
||||||
|
this[ChapterTable.pageCount] = pageCount
|
||||||
|
}
|
||||||
// Try to use the fetch date of the original entry to not pollute 'Updates' tab
|
// Try to use the fetch date of the original entry to not pollute 'Updates' tab
|
||||||
deletedChapterNumberDateFetchMap[chapter.chapterNumber]?.let {
|
deletedChapterNumberDateFetchMap[chapter.chapterNumber]?.let {
|
||||||
this[ChapterTable.fetchedAt] = it
|
this[ChapterTable.fetchedAt] = it
|
||||||
}
|
}
|
||||||
|
|
||||||
deletedDownloadedChapterNumberToChapter[chapter.chapterNumber]?.let {
|
|
||||||
val hasDownloadedPages = it.pageCount > 0
|
|
||||||
val isSameName = it.name == chapter.name
|
|
||||||
val isSameScanlator = it.scanlator == chapter.scanlator
|
|
||||||
|
|
||||||
// Only preserve download status for chapters with the same name and of the same scanlator; otherwise,
|
|
||||||
// the downloaded files won't be found anyway
|
|
||||||
val isDownloadPreservable = hasDownloadedPages && isSameName && isSameScanlator
|
|
||||||
if (isDownloadPreservable) {
|
|
||||||
this[ChapterTable.isDownloaded] = true
|
|
||||||
this[ChapterTable.pageCount] = it.pageCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}.forEach { insertedChapters.add(ChapterTable.toDataClass(it)) }
|
}.forEach { insertedChapters.add(ChapterTable.toDataClass(it)) }
|
||||||
}
|
}
|
||||||
@@ -313,30 +314,12 @@ object Chapter {
|
|||||||
.apply {
|
.apply {
|
||||||
chaptersToUpdate.forEach {
|
chaptersToUpdate.forEach {
|
||||||
addBatch(EntityID(it.id, ChapterTable))
|
addBatch(EntityID(it.id, ChapterTable))
|
||||||
|
|
||||||
val currentChapter = chaptersInDb.find { dbChapter -> dbChapter.id == it.id }!!
|
|
||||||
|
|
||||||
this[ChapterTable.name] = it.name
|
this[ChapterTable.name] = it.name
|
||||||
this[ChapterTable.date_upload] = it.uploadDate
|
this[ChapterTable.date_upload] = it.uploadDate
|
||||||
this[ChapterTable.chapter_number] = it.chapterNumber
|
this[ChapterTable.chapter_number] = it.chapterNumber
|
||||||
this[ChapterTable.scanlator] = it.scanlator
|
this[ChapterTable.scanlator] = it.scanlator
|
||||||
this[ChapterTable.sourceOrder] = it.index
|
this[ChapterTable.sourceOrder] = it.index
|
||||||
this[ChapterTable.realUrl] = it.realUrl
|
this[ChapterTable.realUrl] = it.realUrl
|
||||||
this[ChapterTable.isDownloaded] = currentChapter.downloaded
|
|
||||||
this[ChapterTable.pageCount] = currentChapter.pageCount
|
|
||||||
|
|
||||||
if (!currentChapter.downloaded) {
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
|
|
||||||
val isSameScanlator = currentChapter.scanlator == it.scanlator
|
|
||||||
val isSameName = currentChapter.name == it.name
|
|
||||||
|
|
||||||
val isDownloadPreservable = isSameName && isSameScanlator
|
|
||||||
if (!isDownloadPreservable) {
|
|
||||||
this[ChapterTable.isDownloaded] = false
|
|
||||||
this[ChapterTable.pageCount] = -1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}.toExecutable()
|
}.toExecutable()
|
||||||
.execute(this@transaction)
|
.execute(this@transaction)
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ object Manga {
|
|||||||
""
|
""
|
||||||
}
|
}
|
||||||
if (remoteTitle.isNotEmpty() && remoteTitle != mangaEntry[MangaTable.title]) {
|
if (remoteTitle.isNotEmpty() && remoteTitle != mangaEntry[MangaTable.title]) {
|
||||||
val canUpdateTitle = updateMangaDownloadDir(mangaEntry[MangaTable.title], source.toString(), remoteTitle)
|
val canUpdateTitle = updateMangaDownloadDir(mangaId, remoteTitle)
|
||||||
|
|
||||||
if (canUpdateTitle) {
|
if (canUpdateTitle) {
|
||||||
it[MangaTable.title] = remoteTitle
|
it[MangaTable.title] = remoteTitle
|
||||||
|
|||||||
@@ -358,7 +358,6 @@ object Extension {
|
|||||||
} else {
|
} else {
|
||||||
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
|
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
|
||||||
it[isInstalled] = false
|
it[isInstalled] = false
|
||||||
it[hasUpdate] = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,22 +24,14 @@ private val applicationDirs: ApplicationDirs by injectLazy()
|
|||||||
|
|
||||||
private val logger = KotlinLogging.logger { }
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
private fun getMangaDir(
|
|
||||||
title: String,
|
|
||||||
sourceName: String,
|
|
||||||
): String {
|
|
||||||
val sourceDir = SafePath.buildValidFilename(sourceName)
|
|
||||||
val mangaDir = SafePath.buildValidFilename(title)
|
|
||||||
|
|
||||||
return "$sourceDir/$mangaDir"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMangaDir(mangaId: Int): String =
|
private fun getMangaDir(mangaId: Int): String =
|
||||||
transaction {
|
transaction {
|
||||||
val mangaEntry = MangaTable.selectAll().where { MangaTable.id eq mangaId }.first()
|
val mangaEntry = MangaTable.selectAll().where { MangaTable.id eq mangaId }.first()
|
||||||
val source = GetCatalogueSource.getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
val source = GetCatalogueSource.getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||||
|
|
||||||
getMangaDir(mangaEntry[MangaTable.title], source.toString())
|
val sourceDir = SafePath.buildValidFilename(source.toString())
|
||||||
|
val mangaDir = SafePath.buildValidFilename(mangaEntry[MangaTable.title])
|
||||||
|
"$sourceDir/$mangaDir"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getChapterDir(
|
private fun getChapterDir(
|
||||||
@@ -70,18 +62,8 @@ private fun getChapterDir(
|
|||||||
|
|
||||||
fun getThumbnailDownloadPath(mangaId: Int): String = applicationDirs.thumbnailDownloadsRoot + "/$mangaId"
|
fun getThumbnailDownloadPath(mangaId: Int): String = applicationDirs.thumbnailDownloadsRoot + "/$mangaId"
|
||||||
|
|
||||||
fun getMangaDownloadDir(
|
|
||||||
title: String,
|
|
||||||
sourceName: String,
|
|
||||||
): String = applicationDirs.mangaDownloadsRoot + "/" + getMangaDir(title, sourceName)
|
|
||||||
|
|
||||||
fun getMangaDownloadDir(mangaId: Int): String = applicationDirs.mangaDownloadsRoot + "/" + getMangaDir(mangaId)
|
fun getMangaDownloadDir(mangaId: Int): String = applicationDirs.mangaDownloadsRoot + "/" + getMangaDir(mangaId)
|
||||||
|
|
||||||
fun getMangaCacheDir(
|
|
||||||
title: String,
|
|
||||||
sourceName: String,
|
|
||||||
): String = applicationDirs.tempMangaCacheRoot + "/" + getMangaDir(title, sourceName)
|
|
||||||
|
|
||||||
fun getChapterDownloadPath(
|
fun getChapterDownloadPath(
|
||||||
mangaId: Int,
|
mangaId: Int,
|
||||||
chapterId: Int,
|
chapterId: Int,
|
||||||
@@ -97,21 +79,38 @@ fun getChapterCachePath(
|
|||||||
chapterId: Int,
|
chapterId: Int,
|
||||||
): String = applicationDirs.tempMangaCacheRoot + "/" + getChapterDir(mangaId, chapterId)
|
): String = applicationDirs.tempMangaCacheRoot + "/" + getChapterDir(mangaId, chapterId)
|
||||||
|
|
||||||
private fun updateDownloadDir(
|
/** return value says if rename/move was successful */
|
||||||
currentDir: String,
|
fun updateMangaDownloadDir(
|
||||||
newDir: String,
|
mangaId: Int,
|
||||||
|
newTitle: String,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val currentDirFile = File(currentDir)
|
// Get current manga directory (uses its own transaction)
|
||||||
|
val currentMangaDir = getMangaDir(mangaId)
|
||||||
|
|
||||||
|
// Build new directory path
|
||||||
|
val newMangaDir =
|
||||||
|
transaction {
|
||||||
|
val mangaEntry = MangaTable.selectAll().where { MangaTable.id eq mangaId }.first()
|
||||||
|
val source = GetCatalogueSource.getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||||
|
val sourceDir = SafePath.buildValidFilename(source.toString())
|
||||||
|
val newMangaDirName = SafePath.buildValidFilename(newTitle)
|
||||||
|
"$sourceDir/$newMangaDirName"
|
||||||
|
}
|
||||||
|
|
||||||
|
val oldDir = "${applicationDirs.downloadsRoot}/$currentMangaDir"
|
||||||
|
val newDir = "${applicationDirs.downloadsRoot}/$newMangaDir"
|
||||||
|
|
||||||
|
val oldDirFile = File(oldDir)
|
||||||
val newDirFile = File(newDir)
|
val newDirFile = File(newDir)
|
||||||
|
|
||||||
if (!currentDirFile.exists()) {
|
if (!oldDirFile.exists()) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
Files.move(currentDirFile.toPath(), newDirFile.toPath())
|
Files.move(oldDirFile.toPath(), newDirFile.toPath())
|
||||||
|
|
||||||
if (currentDirFile.exists()) {
|
if (oldDirFile.exists()) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,31 +118,9 @@ private fun updateDownloadDir(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.error(e) { "updateDownloadDir: failed to rename download folder from \"$currentDir\" to \"$newDir\"" }
|
logger.error(e) { "updateMangaDownloadDir: failed to rename manga download folder from \"$oldDir\" to \"$newDir\"" }
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** return value says if rename/move was successful */
|
|
||||||
fun updateMangaDownloadDir(
|
|
||||||
title: String,
|
|
||||||
sourceName: String,
|
|
||||||
newTitle: String,
|
|
||||||
): Boolean {
|
|
||||||
val currentDownloadDir = getMangaDownloadDir(title, sourceName)
|
|
||||||
val newDownloadDir = getMangaDownloadDir(newTitle, sourceName)
|
|
||||||
|
|
||||||
val renamed = updateDownloadDir(currentDownloadDir, newDownloadDir)
|
|
||||||
|
|
||||||
val tryToKeepCachedFilesUsable = renamed
|
|
||||||
if (tryToKeepCachedFilesUsable) {
|
|
||||||
val currentCacheDir = getMangaCacheDir(title, sourceName)
|
|
||||||
val newCacheDir = getMangaCacheDir(newTitle, sourceName)
|
|
||||||
|
|
||||||
updateDownloadDir(currentCacheDir, newCacheDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
return renamed
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,21 +7,12 @@ package suwayomi.tachidesk.manga.model.dataclass
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import java.util.Objects
|
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
open class PaginatedList<T>(
|
open class PaginatedList<T>(
|
||||||
val page: List<T>,
|
val page: List<T>,
|
||||||
val hasNextPage: Boolean,
|
val hasNextPage: Boolean,
|
||||||
) {
|
)
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (other !is PaginatedList<T>) return false
|
|
||||||
return page == other.page && hasNextPage == other.hasNextPage
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int = Objects.hash(page, hasNextPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
const val PAGINATION_FACTOR = 50
|
const val PAGINATION_FACTOR = 50
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import suwayomi.tachidesk.manga.model.table.CategoryMetaTable.ref
|
|||||||
* Metadata storage for clients, about Category with id == [ref].
|
* Metadata storage for clients, about Category with id == [ref].
|
||||||
*/
|
*/
|
||||||
object CategoryMetaTable : IntIdTable() {
|
object CategoryMetaTable : IntIdTable() {
|
||||||
val key = varchar("meta_key", 256)
|
val key = varchar("key", 256)
|
||||||
val value = varchar("value", 4096)
|
val value = varchar("value", 4096)
|
||||||
val ref = reference("category_ref", CategoryTable, ReferenceOption.CASCADE)
|
val ref = reference("category_ref", CategoryTable, ReferenceOption.CASCADE)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import suwayomi.tachidesk.manga.model.table.ChapterMetaTable.ref
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
object ChapterMetaTable : IntIdTable() {
|
object ChapterMetaTable : IntIdTable() {
|
||||||
val key = varchar("meta_key", 256)
|
val key = varchar("key", 256)
|
||||||
val value = varchar("value", 4096)
|
val value = varchar("value", 4096)
|
||||||
val ref = reference("chapter_ref", ChapterTable, ReferenceOption.CASCADE)
|
val ref = reference("chapter_ref", ChapterTable, ReferenceOption.CASCADE)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import suwayomi.tachidesk.manga.model.table.MangaMetaTable.ref
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
object MangaMetaTable : IntIdTable() {
|
object MangaMetaTable : IntIdTable() {
|
||||||
val key = varchar("meta_key", 256)
|
val key = varchar("key", 256)
|
||||||
val value = varchar("value", 4096)
|
val value = varchar("value", 4096)
|
||||||
val ref = reference("manga_ref", MangaTable, ReferenceOption.CASCADE)
|
val ref = reference("manga_ref", MangaTable, ReferenceOption.CASCADE)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import suwayomi.tachidesk.manga.model.table.SourceMetaTable.ref
|
|||||||
* Metadata storage for clients, about Source with id == [ref].
|
* Metadata storage for clients, about Source with id == [ref].
|
||||||
*/
|
*/
|
||||||
object SourceMetaTable : IntIdTable() {
|
object SourceMetaTable : IntIdTable() {
|
||||||
val key = varchar("meta_key", 256)
|
val key = varchar("key", 256)
|
||||||
val value = varchar("value", 4096)
|
val value = varchar("value", 4096)
|
||||||
val ref = long("source_ref")
|
val ref = long("source_ref")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import android.content.Context
|
|||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import suwayomi.tachidesk.manga.impl.update.IUpdater
|
import suwayomi.tachidesk.manga.impl.update.IUpdater
|
||||||
import suwayomi.tachidesk.server.database.H2Migration
|
import suwayomi.tachidesk.server.database.H2Migration
|
||||||
import suwayomi.tachidesk.server.util.ExitCode
|
|
||||||
import suwayomi.tachidesk.server.util.shutdownApp
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -101,35 +99,31 @@ private val MIGRATIONS =
|
|||||||
|
|
||||||
fun runMigrations(applicationDirs: ApplicationDirs) {
|
fun runMigrations(applicationDirs: ApplicationDirs) {
|
||||||
val logger = KotlinLogging.logger("Migration")
|
val logger = KotlinLogging.logger("Migration")
|
||||||
try {
|
|
||||||
val migrationPreferences =
|
|
||||||
Injekt
|
|
||||||
.get<Application>()
|
|
||||||
.getSharedPreferences(
|
|
||||||
"migrations",
|
|
||||||
Context.MODE_PRIVATE,
|
|
||||||
)
|
|
||||||
val version = migrationPreferences.getInt("version", 0)
|
|
||||||
|
|
||||||
logger.info { "Running migrations, previous version $version, target version ${MIGRATIONS.size}" }
|
val migrationPreferences =
|
||||||
|
Injekt
|
||||||
|
.get<Application>()
|
||||||
|
.getSharedPreferences(
|
||||||
|
"migrations",
|
||||||
|
Context.MODE_PRIVATE,
|
||||||
|
)
|
||||||
|
val version = migrationPreferences.getInt("version", 0)
|
||||||
|
|
||||||
MIGRATIONS.forEachIndexed { index, (migrationName, migrationFunction) ->
|
logger.info { "Running migrations, previous version $version, target version ${MIGRATIONS.size}" }
|
||||||
val migrationVersion = index + 1
|
|
||||||
|
|
||||||
val isMigrationRequired = version < migrationVersion
|
MIGRATIONS.forEachIndexed { index, (migrationName, migrationFunction) ->
|
||||||
if (!isMigrationRequired) {
|
val migrationVersion = index + 1
|
||||||
logger.info { "Skipping migration version $migrationVersion: $migrationName" }
|
|
||||||
return@forEachIndexed
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info { "Running migration version $migrationVersion: $migrationName" }
|
val isMigrationRequired = version < migrationVersion
|
||||||
|
if (!isMigrationRequired) {
|
||||||
migrationFunction(applicationDirs)
|
logger.info { "Skipping migration version $migrationVersion: $migrationName" }
|
||||||
|
return@forEachIndexed
|
||||||
migrationPreferences.edit().putInt("version", migrationVersion).apply()
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
logger.error(e) { "Failed to run migrations" }
|
logger.info { "Running migration version $migrationVersion: $migrationName" }
|
||||||
shutdownApp(ExitCode.MigrationsRunFailure)
|
|
||||||
|
migrationFunction(applicationDirs)
|
||||||
|
|
||||||
|
migrationPreferences.edit().putInt("version", migrationVersion).apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +14,15 @@ import com.typesafe.config.ConfigException
|
|||||||
import com.typesafe.config.ConfigRenderOptions
|
import com.typesafe.config.ConfigRenderOptions
|
||||||
import com.typesafe.config.ConfigValue
|
import com.typesafe.config.ConfigValue
|
||||||
import com.typesafe.config.parser.ConfigDocument
|
import com.typesafe.config.parser.ConfigDocument
|
||||||
|
import dev.datlag.kcef.KCEF
|
||||||
|
import dev.datlag.kcef.KCEFBuilder.Settings.LogSeverity
|
||||||
import eu.kanade.tachiyomi.App
|
import eu.kanade.tachiyomi.App
|
||||||
import eu.kanade.tachiyomi.createAppModule
|
import eu.kanade.tachiyomi.createAppModule
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||||
import io.github.config4k.toConfig
|
import io.github.config4k.toConfig
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import io.javalin.json.JavalinJackson3
|
import io.javalin.json.JavalinJackson
|
||||||
import io.javalin.json.JsonMapper
|
import io.javalin.json.JsonMapper
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
@@ -47,7 +49,6 @@ import suwayomi.tachidesk.server.database.databaseUp
|
|||||||
import suwayomi.tachidesk.server.generated.BuildConfig
|
import suwayomi.tachidesk.server.generated.BuildConfig
|
||||||
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
||||||
import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex
|
import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex
|
||||||
import suwayomi.tachidesk.server.util.CEFManager
|
|
||||||
import suwayomi.tachidesk.server.util.ConfigTypeRegistration
|
import suwayomi.tachidesk.server.util.ConfigTypeRegistration
|
||||||
import suwayomi.tachidesk.server.util.ExitCode
|
import suwayomi.tachidesk.server.util.ExitCode
|
||||||
import suwayomi.tachidesk.server.util.SystemTray
|
import suwayomi.tachidesk.server.util.SystemTray
|
||||||
@@ -221,7 +222,7 @@ fun serverModule(applicationDirs: ApplicationDirs): Module =
|
|||||||
module {
|
module {
|
||||||
single { applicationDirs }
|
single { applicationDirs }
|
||||||
single<IUpdater> { Updater() }
|
single<IUpdater> { Updater() }
|
||||||
single<JsonMapper> { JavalinJackson3() }
|
single<JsonMapper> { JavalinJackson() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
@@ -365,7 +366,6 @@ fun applicationSetup() {
|
|||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.error(e) { "Exception while creating initial server.conf" }
|
logger.error(e) { "Exception while creating initial server.conf" }
|
||||||
shutdownApp(ExitCode.SetupConfFileFailed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy local source icon
|
// copy local source icon
|
||||||
@@ -378,7 +378,6 @@ fun applicationSetup() {
|
|||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.error(e) { "Exception while copying Local source's icon" }
|
logger.error(e) { "Exception while copying Local source's icon" }
|
||||||
shutdownApp(ExitCode.LocalSourceIconCopyFailure)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fixes #119 , ref:
|
// fixes #119 , ref:
|
||||||
@@ -396,12 +395,7 @@ fun applicationSetup() {
|
|||||||
|
|
||||||
databaseUp()
|
databaseUp()
|
||||||
|
|
||||||
try {
|
LocalSource.register()
|
||||||
LocalSource.register()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logger.error(e) { "Failed to setup LocalSource" }
|
|
||||||
shutdownApp(ExitCode.LocalSourceSetupFailure)
|
|
||||||
}
|
|
||||||
|
|
||||||
serverConfig.subscribeTo(
|
serverConfig.subscribeTo(
|
||||||
combine<Any, DatabaseSettings>(
|
combine<Any, DatabaseSettings>(
|
||||||
@@ -517,8 +511,56 @@ fun applicationSetup() {
|
|||||||
// start DownloadManager and restore + resume downloads
|
// start DownloadManager and restore + resume downloads
|
||||||
DownloadManager.restoreAndResumeDownloads()
|
DownloadManager.restoreAndResumeDownloads()
|
||||||
|
|
||||||
// asynchronously initialize CEF
|
|
||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
CEFManager.init()
|
val logger = KotlinLogging.logger("KCEF")
|
||||||
|
KCEF.init(
|
||||||
|
builder = {
|
||||||
|
progress {
|
||||||
|
var lastNum = -1
|
||||||
|
onDownloading {
|
||||||
|
val num = it.roundToInt()
|
||||||
|
if (num > lastNum) {
|
||||||
|
lastNum = num
|
||||||
|
logger.info { "KCEF download progress: $num%" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
download { github() }
|
||||||
|
settings {
|
||||||
|
windowlessRenderingEnabled = true
|
||||||
|
cachePath = (Path(applicationDirs.dataRoot) / "cache/kcef").toString()
|
||||||
|
logSeverity = if (serverConfig.debugLogsEnabled.value) LogSeverity.Verbose else LogSeverity.Default
|
||||||
|
}
|
||||||
|
appHandler(
|
||||||
|
KCEF.AppHandler(
|
||||||
|
arrayOf(
|
||||||
|
"--disable-gpu",
|
||||||
|
// #1486 needed to be able to render without a window
|
||||||
|
"--off-screen-rendering-enabled",
|
||||||
|
// #1489 since /dev/shm is restricted in docker (OOM)
|
||||||
|
"--disable-dev-shm-usage",
|
||||||
|
// #1723 support Widevine (incomplete)
|
||||||
|
"--enable-widevine-cdm",
|
||||||
|
// #1736 JCEF does implement stack guards properly
|
||||||
|
"--change-stack-guard-on-fork=disable",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val kcefDir = Path(applicationDirs.dataRoot) / "bin/kcef"
|
||||||
|
kcefDir.createDirectories()
|
||||||
|
installDir(kcefDir.toFile())
|
||||||
|
},
|
||||||
|
onError = { it?.printStackTrace() },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Runtime.getRuntime().addShutdownHook(
|
||||||
|
thread(start = false) {
|
||||||
|
val logger = KotlinLogging.logger("KCEF")
|
||||||
|
logger.debug { "Shutting down KCEF" }
|
||||||
|
KCEF.disposeBlocking()
|
||||||
|
logger.debug { "KCEF shutdown complete" }
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import suwayomi.tachidesk.server.util.ExitCode
|
|||||||
import suwayomi.tachidesk.server.util.shutdownApp
|
import suwayomi.tachidesk.server.util.shutdownApp
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.sql.SQLException
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@@ -144,15 +145,14 @@ object DBManager {
|
|||||||
|
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
fun databaseUp(givenDb: Database? = null) {
|
fun databaseUp() {
|
||||||
val db =
|
val db =
|
||||||
givenDb
|
try {
|
||||||
?: try {
|
DBManager.setupDatabase()
|
||||||
DBManager.setupDatabase()
|
} catch (e: Exception) {
|
||||||
} catch (e: Exception) {
|
logger.error(e) { "Failed to setup Database" }
|
||||||
logger.error(e) { "Failed to setup Database" }
|
return
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
|
||||||
logger.info {
|
logger.info {
|
||||||
"Using ${db.vendor} database version ${db.version}"
|
"Using ${db.vendor} database version ${db.version}"
|
||||||
@@ -184,8 +184,10 @@ fun databaseUp(givenDb: Database? = null) {
|
|||||||
}
|
}
|
||||||
val migrations = loadMigrationsFrom("suwayomi.tachidesk.server.database.migration", ServerConfig::class.java)
|
val migrations = loadMigrationsFrom("suwayomi.tachidesk.server.database.migration", ServerConfig::class.java)
|
||||||
runMigrations(migrations)
|
runMigrations(migrations)
|
||||||
} catch (e: Exception) {
|
} catch (e: SQLException) {
|
||||||
logger.error(e) { "Error up-to-database migration" }
|
logger.error(e) { "Error up-to-database migration" }
|
||||||
shutdownApp(ExitCode.DbMigrationFailure)
|
if (System.getProperty("crashOnFailedMigration").toBoolean()) {
|
||||||
|
shutdownApp(ExitCode.DbMigrationFailure)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,9 @@ import java.net.URLClassLoader
|
|||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import kotlin.io.path.Path
|
import kotlin.io.path.Path
|
||||||
import kotlin.io.path.absolutePathString
|
import kotlin.io.path.absolutePathString
|
||||||
import kotlin.io.path.bufferedReader
|
|
||||||
import kotlin.io.path.bufferedWriter
|
|
||||||
import kotlin.io.path.copyTo
|
import kotlin.io.path.copyTo
|
||||||
import kotlin.io.path.createDirectories
|
import kotlin.io.path.createDirectories
|
||||||
import kotlin.io.path.deleteExisting
|
import kotlin.io.path.deleteExisting
|
||||||
import kotlin.io.path.deleteIfExists
|
|
||||||
import kotlin.io.path.div
|
import kotlin.io.path.div
|
||||||
import kotlin.io.path.exists
|
import kotlin.io.path.exists
|
||||||
import kotlin.io.path.name
|
import kotlin.io.path.name
|
||||||
@@ -49,10 +46,6 @@ object H2Migration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val script = Path("$dbBase.${h2Old.substringAfterLast('.')}.sql")
|
val script = Path("$dbBase.${h2Old.substringAfterLast('.')}.sql")
|
||||||
script.deleteIfExists()
|
|
||||||
|
|
||||||
val modifiedScript = Path("$dbBase.${h2Old.substringAfterLast('.')}.modified.sql")
|
|
||||||
modifiedScript.deleteIfExists()
|
|
||||||
|
|
||||||
// Backup original database.
|
// Backup original database.
|
||||||
val backup = Path("$dbBase.mv.db.${h2Old.substringAfterLast('.')}.backup")
|
val backup = Path("$dbBase.mv.db.${h2Old.substringAfterLast('.')}.backup")
|
||||||
@@ -79,32 +72,19 @@ object H2Migration {
|
|||||||
libsDir.resolve("h2-$h2New.bin"),
|
libsDir.resolve("h2-$h2New.bin"),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Delete attempted migration if failed previously
|
|
||||||
val newDatabase = Path(rootDir, "database.${h2New.substringAfterLast('.')}.mv.db")
|
|
||||||
newDatabase.deleteIfExists()
|
|
||||||
|
|
||||||
val modifiedNewDatabase = Path(rootDir, "database.${h2Old.substringAfterLast('.')}.modified.${h2New.substringAfterLast('.')}.mv.db")
|
|
||||||
modifiedNewDatabase.deleteIfExists()
|
|
||||||
|
|
||||||
runMigrationTool(
|
runMigrationTool(
|
||||||
migrationJar = migrationJar,
|
migrationJar = migrationJar,
|
||||||
libsDir = libsDir,
|
libsDir = libsDir,
|
||||||
mvStore = mvStore,
|
mvStore = mvStore,
|
||||||
script = script,
|
script = script,
|
||||||
modifiedScript = modifiedScript,
|
|
||||||
h2Old = h2Old,
|
h2Old = h2Old,
|
||||||
h2New = h2New,
|
h2New = h2New,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Move database to proper path
|
// Move database to proper path
|
||||||
if (modifiedNewDatabase.exists()) {
|
val newDatabase = Path(rootDir, "database.${h2New.substringAfterLast('.')}.mv.db")
|
||||||
modifiedNewDatabase.copyTo(mvStore, overwrite = true)
|
newDatabase.copyTo(mvStore, overwrite = true)
|
||||||
modifiedNewDatabase.deleteExisting()
|
newDatabase.deleteExisting()
|
||||||
newDatabase.deleteIfExists()
|
|
||||||
} else {
|
|
||||||
newDatabase.copyTo(mvStore, overwrite = true)
|
|
||||||
newDatabase.deleteExisting()
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info { "H2 migration completed successfully." }
|
logger.info { "H2 migration completed successfully." }
|
||||||
}
|
}
|
||||||
@@ -143,7 +123,6 @@ object H2Migration {
|
|||||||
libsDir: Path,
|
libsDir: Path,
|
||||||
mvStore: Path,
|
mvStore: Path,
|
||||||
script: Path,
|
script: Path,
|
||||||
modifiedScript: Path,
|
|
||||||
h2Old: String,
|
h2Old: String,
|
||||||
h2New: String,
|
h2New: String,
|
||||||
) {
|
) {
|
||||||
@@ -157,77 +136,32 @@ object H2Migration {
|
|||||||
val main =
|
val main =
|
||||||
clazz.getMethod("main", Array<String>::class.java)
|
clazz.getMethod("main", Array<String>::class.java)
|
||||||
|
|
||||||
try {
|
main.invoke(
|
||||||
main.invoke(
|
null,
|
||||||
null,
|
arrayOf(
|
||||||
arrayOf(
|
// h2 driver dir
|
||||||
// h2 driver dir
|
"-l",
|
||||||
"-l",
|
libsDir.absolutePathString(),
|
||||||
libsDir.absolutePathString(),
|
// from version
|
||||||
// from version
|
"-f",
|
||||||
"-f",
|
h2Old,
|
||||||
h2Old,
|
// to version
|
||||||
// to version
|
"-t",
|
||||||
"-t",
|
h2New,
|
||||||
h2New,
|
// user
|
||||||
// user
|
"-u",
|
||||||
"-u",
|
"",
|
||||||
"",
|
// password
|
||||||
// password
|
"-p",
|
||||||
"-p",
|
"",
|
||||||
"",
|
// database.mv.db
|
||||||
// database.mv.db
|
"-d",
|
||||||
"-d",
|
mvStore.absolutePathString(),
|
||||||
mvStore.absolutePathString(),
|
// database backup in SQL
|
||||||
// database backup in SQL
|
"-s",
|
||||||
"-s",
|
script.absolutePathString(),
|
||||||
script.absolutePathString(),
|
),
|
||||||
),
|
)
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Modify raw .sql file as needed for compatibility
|
|
||||||
if (e.stackTraceToString().contains("Unknown data type: \"DATETIME\"; SQL statement:") && script.exists()) {
|
|
||||||
script.bufferedReader().use { reader ->
|
|
||||||
modifiedScript.bufferedWriter().use { writer ->
|
|
||||||
reader.forEachLine { line ->
|
|
||||||
writer.write(
|
|
||||||
line.replace(
|
|
||||||
" \"EXECUTED_AT\" DATETIME(9) NOT NULL",
|
|
||||||
" \"EXECUTED_AT\" TIMESTAMP(9) NOT NULL",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
writer.newLine()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main.invoke(
|
|
||||||
null,
|
|
||||||
arrayOf(
|
|
||||||
// h2 driver dir
|
|
||||||
"-l",
|
|
||||||
libsDir.absolutePathString(),
|
|
||||||
// from version
|
|
||||||
"-f",
|
|
||||||
h2Old,
|
|
||||||
// to version
|
|
||||||
"-t",
|
|
||||||
h2New,
|
|
||||||
// user
|
|
||||||
"-u",
|
|
||||||
"",
|
|
||||||
// password
|
|
||||||
"-p",
|
|
||||||
"",
|
|
||||||
// database.mv.db
|
|
||||||
"-d",
|
|
||||||
modifiedScript.absolutePathString(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,17 @@ package suwayomi.tachidesk.server.database.migration
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import de.neonew.exposed.migrations.helpers.SQLMigration
|
import de.neonew.exposed.migrations.helpers.SQLMigration
|
||||||
import suwayomi.tachidesk.server.database.migration.helpers.toSqlName
|
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
|
||||||
|
|
||||||
@Suppress("ClassName", "unused")
|
@Suppress("ClassName", "unused")
|
||||||
class M0023_CategoryMetaRefFix : SQLMigration() {
|
class M0023_CategoryMetaRefFix : SQLMigration() {
|
||||||
|
fun String.toSqlName(): String =
|
||||||
|
TransactionManager.defaultDatabase!!.identifierManager.let {
|
||||||
|
it.quoteIfNecessary(
|
||||||
|
it.inProperCase(this),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private val CategoryMetaTable by lazy { "CategoryMeta".toSqlName() }
|
private val CategoryMetaTable by lazy { "CategoryMeta".toSqlName() }
|
||||||
private val CategoryRefColumn by lazy { "category_ref".toSqlName() }
|
private val CategoryRefColumn by lazy { "category_ref".toSqlName() }
|
||||||
private val CategoryTable by lazy { "Category".toSqlName() }
|
private val CategoryTable by lazy { "Category".toSqlName() }
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ package suwayomi.tachidesk.server.database.migration
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import de.neonew.exposed.migrations.helpers.SQLMigration
|
import de.neonew.exposed.migrations.helpers.SQLMigration
|
||||||
import suwayomi.tachidesk.server.database.migration.helpers.toSqlName
|
|
||||||
|
|
||||||
@Suppress("ClassName", "unused")
|
@Suppress("ClassName", "unused")
|
||||||
class M0049_FixDuplicatedMetas : SQLMigration() {
|
class M0049_FixDuplicatedMetas : SQLMigration() {
|
||||||
@@ -16,7 +15,7 @@ class M0049_FixDuplicatedMetas : SQLMigration() {
|
|||||||
table: String,
|
table: String,
|
||||||
refColumn: String? = null,
|
refColumn: String? = null,
|
||||||
): String {
|
): String {
|
||||||
val groupBy = listOfNotNull(refColumn, "KEY".toSqlName()).joinToString(", ")
|
val groupBy = listOfNotNull(refColumn, "KEY").joinToString(", ")
|
||||||
|
|
||||||
return """
|
return """
|
||||||
DELETE FROM $table
|
DELETE FROM $table
|
||||||
@@ -31,11 +30,10 @@ class M0049_FixDuplicatedMetas : SQLMigration() {
|
|||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
}
|
}
|
||||||
|
|
||||||
override val sql: String by lazy {
|
override val sql: String =
|
||||||
createMigrationForTable("CATEGORYMETA", "CATEGORY_REF") +
|
createMigrationForTable("CATEGORYMETA", "CATEGORY_REF") +
|
||||||
createMigrationForTable("CHAPTERMETA", "CHAPTER_REF") +
|
createMigrationForTable("CHAPTERMETA", "CHAPTER_REF") +
|
||||||
createMigrationForTable("GLOBALMETA") +
|
createMigrationForTable("GLOBALMETA") +
|
||||||
createMigrationForTable("MANGAMETA", "MANGA_REF") +
|
createMigrationForTable("MANGAMETA", "MANGA_REF") +
|
||||||
createMigrationForTable("SOURCEMETA", "SOURCE_REF")
|
createMigrationForTable("SOURCEMETA", "SOURCE_REF")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
package suwayomi.tachidesk.server.database.migration
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
import de.neonew.exposed.migrations.helpers.SQLMigration
|
|
||||||
import suwayomi.tachidesk.graphql.types.DatabaseType
|
|
||||||
import suwayomi.tachidesk.server.database.migration.helpers.toSqlName
|
|
||||||
import suwayomi.tachidesk.server.serverConfig
|
|
||||||
|
|
||||||
@Suppress("ClassName", "unused")
|
|
||||||
class M0055_RenameMetaKeys : SQLMigration() {
|
|
||||||
fun postgresRename(table: String): String =
|
|
||||||
"ALTER TABLE $table " +
|
|
||||||
"RENAME COLUMN " + "KEY".toSqlName() + " TO META_KEY;"
|
|
||||||
|
|
||||||
fun h2Rename(table: String): String =
|
|
||||||
"ALTER TABLE $table " +
|
|
||||||
"ALTER COLUMN " + "KEY".toSqlName() + " RENAME TO META_KEY;"
|
|
||||||
|
|
||||||
fun createRenameMigration(table: String): String =
|
|
||||||
when (serverConfig.databaseType.value) {
|
|
||||||
DatabaseType.H2 -> h2Rename(table.toSqlName())
|
|
||||||
DatabaseType.POSTGRESQL -> postgresRename(table.toSqlName())
|
|
||||||
}
|
|
||||||
|
|
||||||
override val sql: String by lazy {
|
|
||||||
createRenameMigration("CATEGORYMETA") +
|
|
||||||
createRenameMigration("CHAPTERMETA") +
|
|
||||||
createRenameMigration("GLOBALMETA") +
|
|
||||||
createRenameMigration("MANGAMETA") +
|
|
||||||
createRenameMigration("SOURCEMETA")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
package suwayomi.tachidesk.server.database.migration.helpers
|
package suwayomi.tachidesk.server.database.migration.helpers
|
||||||
|
|
||||||
import de.neonew.exposed.migrations.helpers.SQLMigration
|
import de.neonew.exposed.migrations.helpers.SQLMigration
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
|
||||||
import suwayomi.tachidesk.graphql.types.DatabaseType
|
import suwayomi.tachidesk.graphql.types.DatabaseType
|
||||||
import suwayomi.tachidesk.server.serverConfig
|
import suwayomi.tachidesk.server.serverConfig
|
||||||
|
|
||||||
|
fun String.toSqlName(): String =
|
||||||
|
TransactionManager.current().db.identifierManager.let {
|
||||||
|
it.quoteIfNecessary(
|
||||||
|
it.inProperCase(this),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
abstract class RenameFieldMigration(
|
abstract class RenameFieldMigration(
|
||||||
tableName: String,
|
tableName: String,
|
||||||
originalName: String,
|
originalName: String,
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
package suwayomi.tachidesk.server.database.migration.helpers
|
|
||||||
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
|
|
||||||
|
|
||||||
fun String.toSqlName(): String =
|
|
||||||
TransactionManager.current().db.identifierManager.let {
|
|
||||||
it.quoteIfNecessary(
|
|
||||||
it.inProperCase(this),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -21,10 +21,6 @@ enum class ExitCode(
|
|||||||
WebUISetupFailure(3),
|
WebUISetupFailure(3),
|
||||||
ConfigMigrationMisconfiguredFailure(4),
|
ConfigMigrationMisconfiguredFailure(4),
|
||||||
DbMigrationFailure(5),
|
DbMigrationFailure(5),
|
||||||
SetupConfFileFailed(6),
|
|
||||||
LocalSourceIconCopyFailure(7),
|
|
||||||
LocalSourceSetupFailure(8),
|
|
||||||
MigrationsRunFailure(9),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shutdownApp(exitCode: ExitCode) {
|
fun shutdownApp(exitCode: ExitCode) {
|
||||||
|
|||||||
@@ -1,573 +0,0 @@
|
|||||||
package suwayomi.tachidesk.server.util
|
|
||||||
|
|
||||||
import android.text.format.Formatter
|
|
||||||
import com.jetbrains.cef.JCefAppConfig
|
|
||||||
import eu.kanade.tachiyomi.network.ProgressListener
|
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
|
||||||
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
|
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.subscribe
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream
|
|
||||||
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream
|
|
||||||
import org.cef.CefApp
|
|
||||||
import org.cef.CefSettings.LogSeverity
|
|
||||||
import org.cef.SystemBootstrap
|
|
||||||
import suwayomi.tachidesk.server.ApplicationDirs
|
|
||||||
import suwayomi.tachidesk.server.generated.BuildConfig
|
|
||||||
import suwayomi.tachidesk.server.serverConfig
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import xyz.nulldev.androidcompat.webkit.CefHelper
|
|
||||||
import java.io.BufferedOutputStream
|
|
||||||
import java.io.IOException
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.LinkOption
|
|
||||||
import java.nio.file.Path
|
|
||||||
import java.nio.file.StandardOpenOption
|
|
||||||
import java.nio.file.attribute.PosixFilePermission
|
|
||||||
import kotlin.concurrent.thread
|
|
||||||
import kotlin.io.path.ExperimentalPathApi
|
|
||||||
import kotlin.io.path.Path
|
|
||||||
import kotlin.io.path.absolute
|
|
||||||
import kotlin.io.path.absolutePathString
|
|
||||||
import kotlin.io.path.createDirectories
|
|
||||||
import kotlin.io.path.createTempDirectory
|
|
||||||
import kotlin.io.path.deleteExisting
|
|
||||||
import kotlin.io.path.deleteIfExists
|
|
||||||
import kotlin.io.path.deleteRecursively
|
|
||||||
import kotlin.io.path.div
|
|
||||||
import kotlin.io.path.exists
|
|
||||||
import kotlin.io.path.getPosixFilePermissions
|
|
||||||
import kotlin.io.path.inputStream
|
|
||||||
import kotlin.io.path.isRegularFile
|
|
||||||
import kotlin.io.path.isSameFileAs
|
|
||||||
import kotlin.io.path.isSymbolicLink
|
|
||||||
import kotlin.io.path.listDirectoryEntries
|
|
||||||
import kotlin.io.path.moveTo
|
|
||||||
import kotlin.io.path.outputStream
|
|
||||||
import kotlin.io.path.readLines
|
|
||||||
import kotlin.io.path.readSymbolicLink
|
|
||||||
import kotlin.io.path.readText
|
|
||||||
import kotlin.io.path.setPosixFilePermissions
|
|
||||||
import kotlin.io.path.writeText
|
|
||||||
import kotlin.streams.asSequence
|
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger {}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalPathApi::class)
|
|
||||||
object CEFManager {
|
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default + Dispatchers.IO)
|
|
||||||
private val applicationDirs by lazy { Injekt.get<ApplicationDirs>() }
|
|
||||||
private val cefDir by lazy { Path(applicationDirs.dataRoot) / "bin/kcef" }
|
|
||||||
private val releaseFile by lazy { cefDir / "release" }
|
|
||||||
|
|
||||||
fun init() =
|
|
||||||
scope.launch {
|
|
||||||
serverConfig.subscribeTo(serverConfig.kcefEnabled, CEFManager::initAsync, ignoreInitialValue = false)
|
|
||||||
|
|
||||||
Runtime.getRuntime().addShutdownHook(
|
|
||||||
thread(start = false) {
|
|
||||||
CefHelper.cefApp.value.getOrNull()?.let {
|
|
||||||
logger.debug { "Shutting down CEF" }
|
|
||||||
it.dispose()
|
|
||||||
logger.debug { "CEF shutdown complete" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun initAsync(): Unit =
|
|
||||||
try {
|
|
||||||
CefHelper.cefApp.value = Result.success(null)
|
|
||||||
|
|
||||||
if (!serverConfig.kcefEnabled.value) {
|
|
||||||
throw CefException("CEF is disabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
System.loadLibrary("jawt")
|
|
||||||
|
|
||||||
if (serverConfig.debugLogsEnabled.value) System.setProperty("jcef.log.verbose", "true")
|
|
||||||
|
|
||||||
if (!isInstallationValid(releaseFile)) {
|
|
||||||
downloadRelease(cefDir)
|
|
||||||
|
|
||||||
if (!isInstallationValid(releaseFile)) {
|
|
||||||
throw CefException("Failed to provide a valid installation, this is a bug!")
|
|
||||||
}
|
|
||||||
logger.info { "Downloaded CEF successfully!" }
|
|
||||||
}
|
|
||||||
|
|
||||||
val app =
|
|
||||||
if (CefApp.getInstanceIfAny() == null) {
|
|
||||||
val config =
|
|
||||||
JCefAppConfig.getInstance(cefDir.toString(), false).apply {
|
|
||||||
appArgsAsList.addAll(
|
|
||||||
arrayOf(
|
|
||||||
"--disable-gpu",
|
|
||||||
// #1486 needed to be able to render without a window
|
|
||||||
"--off-screen-rendering-enabled",
|
|
||||||
// #1489 since /dev/shm is restricted in docker (OOM)
|
|
||||||
"--disable-dev-shm-usage",
|
|
||||||
// #1723 support Widevine (incomplete)
|
|
||||||
"--enable-widevine-cdm",
|
|
||||||
// #1736 JCEF does implement stack guards properly
|
|
||||||
"--change-stack-guard-on-fork=disable",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
cefSettings.apply {
|
|
||||||
windowless_rendering_enabled = true
|
|
||||||
cache_path = (Path(applicationDirs.dataRoot) / "cache/kcef").absolutePathString()
|
|
||||||
log_severity =
|
|
||||||
if (serverConfig.debugLogsEnabled.value) {
|
|
||||||
LogSeverity.LOGSEVERITY_VERBOSE
|
|
||||||
} else {
|
|
||||||
LogSeverity.LOGSEVERITY_DEFAULT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.debug {
|
|
||||||
"Attempting to initialize CEF: exe=${config.getServerExe()}, settings={${
|
|
||||||
config.cefSettings.getDescription()
|
|
||||||
}}, args=${config.getAppArgs().contentToString()}"
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is essentially https://github.com/JetBrains/jcef/blob/5b93e5b916068316f1c8e7f8a59bf958d5ffd6e1/java/org/cef/CefApp.java#L777
|
|
||||||
// we do this here because JCEF has no mechanism to tell us that initalization failed, they just record in an inaccessible future
|
|
||||||
val os = Platform.current.os
|
|
||||||
when {
|
|
||||||
os.isLinux -> {
|
|
||||||
config.getLoader().loadLibrary("cef")
|
|
||||||
}
|
|
||||||
|
|
||||||
os.isWindows -> {
|
|
||||||
config.getLoader().loadLibrary("chrome_elf")
|
|
||||||
config.getLoader().loadLibrary("libcef")
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
config.getLoader().loadLibrary("jcef")
|
|
||||||
|
|
||||||
CefApp.setIsRemoteEnabled(config.isRemoteEnabled)
|
|
||||||
SystemBootstrap.setLoader(config.getLoader())
|
|
||||||
CefApp.startup(config.getAppArgs())
|
|
||||||
|
|
||||||
CefApp.getInstance(config.getAppArgs(), config.cefSettings, config.getServerExe())
|
|
||||||
} else {
|
|
||||||
logger.debug { "Getting existing app instance" }
|
|
||||||
CefApp.getInstance()
|
|
||||||
}
|
|
||||||
CefHelper.cefApp.value = Result.success(app)
|
|
||||||
logger.debug { "CEF app created" }
|
|
||||||
|
|
||||||
CefHelper.waitForInit().first()
|
|
||||||
return
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
logger.error(e) { "Failed to set up CEF" }
|
|
||||||
CefHelper.cefApp.value = Result.failure(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun isInstallationValid(releaseFile: Path): Boolean {
|
|
||||||
if (!releaseFile.exists() || !releaseFile.isRegularFile()) return false
|
|
||||||
return try {
|
|
||||||
releaseFile
|
|
||||||
.readLines()
|
|
||||||
.firstNotNullOfOrNull {
|
|
||||||
if (it.contains("JCEF_VERSION_DETAILED")) it.split("=").getOrNull(1) else null
|
|
||||||
}?.let {
|
|
||||||
logger.debug { "Comparing internal ${BuildConfig.JCEF_VERSION} against downloaded $it" }
|
|
||||||
BuildConfig.JCEF_VERSION.split("-chromium")[0] == it.split("-chromium")[0]
|
|
||||||
} ?: false
|
|
||||||
} catch (_: Exception) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal suspend fun downloadRelease(installDir: Path) {
|
|
||||||
logger.info { "Downloading CEF from Github (${BuildConfig.JCEF_JBR_RELEASE})" }
|
|
||||||
installDir.deleteRecursively()
|
|
||||||
|
|
||||||
if (!runCatching { installDir.createDirectories() }.isSuccess) {
|
|
||||||
throw CefException("Failed to create installation directory")
|
|
||||||
}
|
|
||||||
|
|
||||||
val client = OkHttpClient.Builder().followRedirects(true).build()
|
|
||||||
val request =
|
|
||||||
Request
|
|
||||||
.Builder()
|
|
||||||
.url("https://api.github.com/repos/JetBrains/JetBrainsRuntime/releases/tags/${BuildConfig.JCEF_JBR_RELEASE}")
|
|
||||||
.addHeader("Content-Type", GithubReleaseTransform.GITHUB_JSON)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val downloadUrl =
|
|
||||||
client.newCall(request).awaitSuccess().use { response ->
|
|
||||||
if (!response.isSuccessful) throw IOException("Unexpected code $response")
|
|
||||||
GithubReleaseTransform.transform(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
val tempDownload = createTempDirectory("cef")
|
|
||||||
try {
|
|
||||||
val downFile = tempDownload / "download.tar.gz"
|
|
||||||
val downloadRequest =
|
|
||||||
Request
|
|
||||||
.Builder()
|
|
||||||
.url(downloadUrl)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
downFile.outputStream().use { output ->
|
|
||||||
client
|
|
||||||
.newCachelessCallWithProgress(
|
|
||||||
downloadRequest,
|
|
||||||
object : ProgressListener {
|
|
||||||
private var lastPercent = 0L
|
|
||||||
|
|
||||||
override fun update(
|
|
||||||
bytesRead: Long,
|
|
||||||
contentLength: Long,
|
|
||||||
done: Boolean,
|
|
||||||
) {
|
|
||||||
val newPercent = (bytesRead * 100).floorDiv(contentLength)
|
|
||||||
if (newPercent != lastPercent) {
|
|
||||||
logger.info { "Downloading $newPercent% of ${Formatter.formatFileSize(null, contentLength)}" }
|
|
||||||
lastPercent = newPercent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
).awaitSuccess()
|
|
||||||
.use { response ->
|
|
||||||
response.body.byteStream().use { input -> input.copyTo(output) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug { "Extracting CEF..." }
|
|
||||||
TarGzExtractor.extract(
|
|
||||||
installDir,
|
|
||||||
downFile,
|
|
||||||
4096,
|
|
||||||
)
|
|
||||||
TarGzExtractor.move(
|
|
||||||
installDir,
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
tempDownload.deleteRecursively()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CefException(
|
|
||||||
msg: String,
|
|
||||||
) : Exception(msg)
|
|
||||||
|
|
||||||
// based on https://github.com/DatL4g/KCEF/blob/master/kcef/src/main/kotlin/dev/datlag/kcef/KCEFBuilder.kt
|
|
||||||
private object GithubReleaseTransform {
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
private val urlRegex = "(https?://|www.)[-a-zA-Z0-9+&@#/%?=~_|!:.;]*[-a-zA-Z0-9+&@#/%=~_|]".toRegex()
|
|
||||||
const val GITHUB_JSON = "application/vnd.github+json"
|
|
||||||
|
|
||||||
fun transform(initialResponse: Response): String {
|
|
||||||
val release = with(json) { initialResponse.parseAs<GitHubRelease>() }
|
|
||||||
|
|
||||||
val packageUrlList =
|
|
||||||
urlRegex
|
|
||||||
.findAll(release.body)
|
|
||||||
.toList()
|
|
||||||
.map { it.value }
|
|
||||||
.filterNot {
|
|
||||||
it.isBlank() || it.endsWith(".checksum", true)
|
|
||||||
}.filter {
|
|
||||||
it.contains("jcef", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
val platform = Platform.current
|
|
||||||
val osPackageList =
|
|
||||||
packageUrlList
|
|
||||||
.filter { url ->
|
|
||||||
platform.os.values.any { os ->
|
|
||||||
url.contains(os, true)
|
|
||||||
}
|
|
||||||
}.ifEmpty {
|
|
||||||
release.assets
|
|
||||||
.filter { asset ->
|
|
||||||
platform.os.values.any { os ->
|
|
||||||
asset.name.contains(os, true) || asset.downloadUrl.contains(os, true)
|
|
||||||
} && asset.downloadUrl.isNotBlank()
|
|
||||||
}.filter { asset ->
|
|
||||||
platform.arch.values.any { arch ->
|
|
||||||
asset.name.contains(arch, ignoreCase = true) ||
|
|
||||||
asset.downloadUrl.contains(
|
|
||||||
arch,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
} && asset.downloadUrl.isNotBlank()
|
|
||||||
}.map { it.downloadUrl }
|
|
||||||
}
|
|
||||||
val platformPackageList =
|
|
||||||
osPackageList.filter { url ->
|
|
||||||
platform.arch.values.any { arch ->
|
|
||||||
url.contains(arch, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (platformPackageList.isEmpty()) {
|
|
||||||
throw CefException("Platform not supported by CEF (${platform.os},${platform.arch})")
|
|
||||||
}
|
|
||||||
|
|
||||||
val sortedPackageList =
|
|
||||||
platformPackageList.sortedWith(
|
|
||||||
compareBy<String> {
|
|
||||||
if (it.contains("sdk", true)) {
|
|
||||||
1
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}.thenBy {
|
|
||||||
if (it.endsWith(".tar.gz", true)) {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return sortedPackageList.first()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
private data class GitHubRelease(
|
|
||||||
val body: String,
|
|
||||||
val assets: List<Asset> = emptyList(),
|
|
||||||
) {
|
|
||||||
@Serializable
|
|
||||||
data class Asset(
|
|
||||||
val name: String = "",
|
|
||||||
@SerialName("browser_download_url") val downloadUrl: String = "",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// based on https://github.com/DatL4g/KCEF/blob/master/kcef/src/main/kotlin/dev/datlag/kcef/step/extract/TarGzExtractor.kt
|
|
||||||
internal data object TarGzExtractor {
|
|
||||||
internal fun Path.validate(parent: Path): Boolean =
|
|
||||||
runCatching {
|
|
||||||
this.normalize().startsWith(parent)
|
|
||||||
}.getOrNull() ?: false
|
|
||||||
|
|
||||||
internal fun Path.isSymlink(): Boolean =
|
|
||||||
runCatching {
|
|
||||||
this.isSymbolicLink()
|
|
||||||
}.getOrNull() ?: runCatching {
|
|
||||||
!this.isRegularFile(LinkOption.NOFOLLOW_LINKS)
|
|
||||||
}.getOrNull() ?: false
|
|
||||||
|
|
||||||
internal fun Path.getRealFile(): Path =
|
|
||||||
if (isSymlink()) {
|
|
||||||
runCatching {
|
|
||||||
this.readSymbolicLink()
|
|
||||||
}.getOrNull() ?: this
|
|
||||||
} else {
|
|
||||||
this
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun Path.isSame(file: Path?): Boolean {
|
|
||||||
var sourceFile = this.getRealFile()
|
|
||||||
if (!sourceFile.exists()) {
|
|
||||||
sourceFile = this
|
|
||||||
}
|
|
||||||
|
|
||||||
var targetFile = file?.getRealFile() ?: file
|
|
||||||
if (targetFile?.exists() == false) {
|
|
||||||
targetFile = file
|
|
||||||
}
|
|
||||||
|
|
||||||
return if (targetFile == null) {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
this == targetFile || runCatching {
|
|
||||||
sourceFile.absolute() == targetFile.absolute() || sourceFile.isSameFileAs(targetFile)
|
|
||||||
}.getOrNull() ?: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun extract(
|
|
||||||
installDir: Path,
|
|
||||||
downloadedFile: Path,
|
|
||||||
bufferSize: Long,
|
|
||||||
) {
|
|
||||||
downloadedFile.inputStream().use { `in` ->
|
|
||||||
GzipCompressorInputStream(`in`).use { gzipIn ->
|
|
||||||
TarArchiveInputStream(gzipIn).use { tarIn ->
|
|
||||||
while (tarIn.nextEntry != null) {
|
|
||||||
val currentEntry = tarIn.currentEntry
|
|
||||||
|
|
||||||
if (currentEntry != null) {
|
|
||||||
val file = installDir / currentEntry.name
|
|
||||||
if (!file.validate(installDir)) {
|
|
||||||
throw CefException("bad archive")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentEntry.isDirectory) {
|
|
||||||
file.createDirectories()
|
|
||||||
} else {
|
|
||||||
BufferedOutputStream(
|
|
||||||
file.outputStream(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE),
|
|
||||||
bufferSize.toInt(),
|
|
||||||
).use { dest ->
|
|
||||||
tarIn.copyTo(dest)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
file.setPosixFilePermissions(
|
|
||||||
file.getPosixFilePermissions() +
|
|
||||||
setOf(
|
|
||||||
PosixFilePermission.OWNER_EXECUTE,
|
|
||||||
PosixFilePermission.GROUP_EXECUTE,
|
|
||||||
PosixFilePermission.OTHERS_EXECUTE,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} catch (_: UnsupportedOperationException) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
downloadedFile.deleteExisting()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun move(installDir: Path) {
|
|
||||||
val releaseFile =
|
|
||||||
Files.walk(installDir).use { s ->
|
|
||||||
s
|
|
||||||
.filter(Files::isRegularFile)
|
|
||||||
.asSequence()
|
|
||||||
.firstOrNull { it.fileName?.toString() == "release" }
|
|
||||||
} ?: (installDir / "release")
|
|
||||||
val releaseFileContents = if (releaseFile.exists()) releaseFile.readText(Charsets.UTF_8) else ""
|
|
||||||
|
|
||||||
val os = Platform.current.os
|
|
||||||
when {
|
|
||||||
os.isLinux -> linuxMove(installDir)
|
|
||||||
os.isMacOSX -> macMove(installDir)
|
|
||||||
os.isWindows -> winMove(installDir)
|
|
||||||
else -> linuxMove(installDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
(installDir / "release").writeText(releaseFileContents)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun linuxMove(installDir: Path) {
|
|
||||||
var foundDir: Path? = null
|
|
||||||
var foundParent: Path? = null
|
|
||||||
|
|
||||||
installDir.listDirectoryEntries().forEach { parent ->
|
|
||||||
if ((parent / "lib").exists()) {
|
|
||||||
foundDir = parent / "lib"
|
|
||||||
foundParent = parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foundDir?.let {
|
|
||||||
val target = it.moveTo(installDir / "lib")
|
|
||||||
foundParent?.let { p ->
|
|
||||||
p.deleteRecursively()
|
|
||||||
p.deleteIfExists()
|
|
||||||
}
|
|
||||||
|
|
||||||
installDir.listDirectoryEntries().forEach { deleteCandidate ->
|
|
||||||
if (!deleteCandidate.isSame(target)) {
|
|
||||||
deleteCandidate.deleteRecursively()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target.listDirectoryEntries().forEach { moveCandidate ->
|
|
||||||
moveCandidate.moveTo(installDir / moveCandidate.fileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
target.deleteExisting()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun macMove(installDir: Path) {
|
|
||||||
var foundDir: Path? = null
|
|
||||||
var foundParent: Path? = null
|
|
||||||
|
|
||||||
installDir.listDirectoryEntries().forEach { parent ->
|
|
||||||
if ((parent / "Contents").exists()) {
|
|
||||||
foundDir = parent / "Contents"
|
|
||||||
foundParent = parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val target = (installDir / "lib").also { it.createDirectories() }
|
|
||||||
foundDir?.let { contents ->
|
|
||||||
(contents / "Home" / "lib").listDirectoryEntries().forEach { moveCandidate ->
|
|
||||||
moveCandidate.moveTo(target / moveCandidate.fileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
(contents / "Frameworks").moveTo(
|
|
||||||
target / "Frameworks",
|
|
||||||
)
|
|
||||||
|
|
||||||
foundParent?.let { p ->
|
|
||||||
p.deleteRecursively()
|
|
||||||
p.deleteIfExists()
|
|
||||||
}
|
|
||||||
|
|
||||||
installDir.listDirectoryEntries().forEach { deleteCandidate ->
|
|
||||||
if (!deleteCandidate.isSame(target)) {
|
|
||||||
deleteCandidate.deleteRecursively()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target.listDirectoryEntries().forEach { moveCandidate ->
|
|
||||||
moveCandidate.moveTo(installDir / moveCandidate.fileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
target.deleteExisting()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun winMove(installDir: Path) {
|
|
||||||
var foundDir: Path? = null
|
|
||||||
|
|
||||||
installDir.listDirectoryEntries().forEach { parent ->
|
|
||||||
if ((parent / "lib").exists()) {
|
|
||||||
foundDir = parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foundDir?.let {
|
|
||||||
val target = (it / "lib").moveTo(installDir / "lib")
|
|
||||||
(it / "bin").listDirectoryEntries().forEach { moveCandidate ->
|
|
||||||
moveCandidate.moveTo(target / moveCandidate.fileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
installDir.listDirectoryEntries().forEach { deleteCandidate ->
|
|
||||||
if (!deleteCandidate.isSame(target)) {
|
|
||||||
deleteCandidate.deleteRecursively()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target.listDirectoryEntries().forEach { moveCandidate ->
|
|
||||||
moveCandidate.moveTo(installDir / moveCandidate.fileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
target.deleteExisting()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
package suwayomi.tachidesk.server.util
|
|
||||||
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
data class OSInfo(
|
|
||||||
val os: OS,
|
|
||||||
val arch: ARCH,
|
|
||||||
)
|
|
||||||
|
|
||||||
object Platform {
|
|
||||||
private val oses: List<OS.OSCreator> = listOf(OS.OSCreator.MACOSX(), OS.OSCreator.LINUX(), OS.OSCreator.WINDOWS())
|
|
||||||
private val archs: List<ARCH.ARCHCreator> =
|
|
||||||
listOf(ARCH.ARCHCreator.AMD64(), ARCH.ARCHCreator.I386(), ARCH.ARCHCreator.ARM64(), ARCH.ARCHCreator.ARM())
|
|
||||||
|
|
||||||
val current: OSInfo by lazy { getCurrentPlatform() }
|
|
||||||
|
|
||||||
private fun getCurrentPlatform(): OSInfo {
|
|
||||||
val osName = System.getProperty("os.name")
|
|
||||||
val archName = System.getProperty("os.arch")
|
|
||||||
val os = oses.firstNotNullOfOrNull { if (it.matches(osName)) it.create(osName) else null }
|
|
||||||
val arch = archs.firstNotNullOfOrNull { if (it.matches(archName)) it.create(archName) else null }
|
|
||||||
if (os == null || arch == null) {
|
|
||||||
throw UnsupportedOperationException("Unsupported platform tuple $osName,$archName")
|
|
||||||
}
|
|
||||||
return OSInfo(os, arch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class OS(
|
|
||||||
val name: String,
|
|
||||||
vararg val values: String,
|
|
||||||
) {
|
|
||||||
internal abstract class OSCreator(
|
|
||||||
vararg val values: String,
|
|
||||||
) {
|
|
||||||
abstract fun create(name: String): OS
|
|
||||||
|
|
||||||
fun matches(name: String): Boolean =
|
|
||||||
values.any { name.startsWith(it, true) } ||
|
|
||||||
values.contains(
|
|
||||||
name.lowercase(
|
|
||||||
Locale.ENGLISH,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
class MACOSX : OSCreator("mac", "darwin", "osx") {
|
|
||||||
override fun create(name: String) = OS.MACOSX(name, *values)
|
|
||||||
}
|
|
||||||
|
|
||||||
class LINUX : OSCreator("linux") {
|
|
||||||
override fun create(name: String) = OS.LINUX(name, *values)
|
|
||||||
}
|
|
||||||
|
|
||||||
class WINDOWS : OSCreator("win", "windows") {
|
|
||||||
override fun create(name: String) = OS.WINDOWS(name, *values)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MACOSX(
|
|
||||||
name: String,
|
|
||||||
vararg values: String,
|
|
||||||
) : OS(name, *values)
|
|
||||||
|
|
||||||
class LINUX(
|
|
||||||
name: String,
|
|
||||||
vararg values: String,
|
|
||||||
) : OS(name, *values)
|
|
||||||
|
|
||||||
class WINDOWS(
|
|
||||||
name: String,
|
|
||||||
vararg values: String,
|
|
||||||
) : OS(name, *values)
|
|
||||||
|
|
||||||
val isLinux: Boolean get() = this is LINUX
|
|
||||||
val isMacOSX: Boolean get() = this is MACOSX
|
|
||||||
val isWindows: Boolean get() = this is WINDOWS
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class ARCH(
|
|
||||||
val name: String,
|
|
||||||
vararg val values: String,
|
|
||||||
) {
|
|
||||||
internal abstract class ARCHCreator(
|
|
||||||
vararg val values: String,
|
|
||||||
) {
|
|
||||||
abstract fun create(name: String): ARCH
|
|
||||||
|
|
||||||
fun matches(name: String): Boolean =
|
|
||||||
values.any { name.startsWith(it, true) } ||
|
|
||||||
values.contains(
|
|
||||||
name.lowercase(
|
|
||||||
Locale.ENGLISH,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
class AMD64 : ARCHCreator("amd64", "x86_64", "x64") {
|
|
||||||
override fun create(name: String) = ARCH.AMD64(name, *values)
|
|
||||||
}
|
|
||||||
|
|
||||||
class I386 : ARCHCreator("x86", "i386", "i486", "i586", "i686", "i786") {
|
|
||||||
override fun create(name: String) = ARCH.I386(name, *values)
|
|
||||||
}
|
|
||||||
|
|
||||||
class ARM64 : ARCHCreator("arm64", "aarch64") {
|
|
||||||
override fun create(name: String) = ARCH.ARM64(name, *values)
|
|
||||||
}
|
|
||||||
|
|
||||||
class ARM : ARCHCreator("arm") {
|
|
||||||
override fun create(name: String) = ARCH.ARM(name, *values)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AMD64(
|
|
||||||
arch: String,
|
|
||||||
vararg values: String,
|
|
||||||
) : ARCH(arch, *values)
|
|
||||||
|
|
||||||
class I386(
|
|
||||||
arch: String,
|
|
||||||
vararg values: String,
|
|
||||||
) : ARCH(arch, *values)
|
|
||||||
|
|
||||||
class ARM64(
|
|
||||||
arch: String,
|
|
||||||
vararg values: String,
|
|
||||||
) : ARCH(arch, *values)
|
|
||||||
|
|
||||||
class ARM(
|
|
||||||
arch: String,
|
|
||||||
vararg values: String,
|
|
||||||
) : ARCH(arch, *values)
|
|
||||||
}
|
|
||||||
@@ -1,22 +1,17 @@
|
|||||||
package masstest
|
package masstest
|
||||||
|
|
||||||
import android.os.Looper
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.jupiter.api.AfterAll
|
|
||||||
import org.junit.jupiter.api.BeforeAll
|
import org.junit.jupiter.api.BeforeAll
|
||||||
import org.junit.jupiter.api.Disabled
|
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.TestInstance
|
import org.junit.jupiter.api.TestInstance
|
||||||
import org.koin.core.context.stopKoin
|
|
||||||
import suwayomi.tachidesk.manga.impl.Source
|
import suwayomi.tachidesk.manga.impl.Source
|
||||||
import suwayomi.tachidesk.manga.impl.extension.Extension
|
import suwayomi.tachidesk.manga.impl.extension.Extension
|
||||||
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList
|
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||||
import suwayomi.tachidesk.server.applicationSetup
|
import suwayomi.tachidesk.server.applicationSetup
|
||||||
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
|
||||||
import suwayomi.tachidesk.test.BASE_PATH
|
import suwayomi.tachidesk.test.BASE_PATH
|
||||||
import suwayomi.tachidesk.test.setLoggingEnabled
|
import suwayomi.tachidesk.test.setLoggingEnabled
|
||||||
import xyz.nulldev.ts.config.CONFIG_PREFIX
|
import xyz.nulldev.ts.config.CONFIG_PREFIX
|
||||||
@@ -30,11 +25,8 @@ class CloudFlareTest {
|
|||||||
fun setup() {
|
fun setup() {
|
||||||
val dataRoot = File(BASE_PATH).absolutePath
|
val dataRoot = File(BASE_PATH).absolutePath
|
||||||
System.setProperty("$CONFIG_PREFIX.server.rootDir", dataRoot)
|
System.setProperty("$CONFIG_PREFIX.server.rootDir", dataRoot)
|
||||||
Looper.clearMainLooperForTest()
|
|
||||||
SettingsRegistry.clear()
|
|
||||||
applicationSetup()
|
applicationSetup()
|
||||||
setLoggingEnabled(false)
|
setLoggingEnabled(false)
|
||||||
return
|
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
val extensions = ExtensionsList.getExtensionList()
|
val extensions = ExtensionsList.getExtensionList()
|
||||||
@@ -56,15 +48,9 @@ class CloudFlareTest {
|
|||||||
setLoggingEnabled(true)
|
setLoggingEnabled(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterAll
|
|
||||||
fun teardown() {
|
|
||||||
stopKoin()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Disabled
|
|
||||||
fun `test nhentai browse`() =
|
fun `test nhentai browse`() =
|
||||||
runTest {
|
runTest {
|
||||||
assert(nhentai.getPopularManga(1).mangas.isNotEmpty()) {
|
assert(nhentai.getPopularManga(1).mangas.isNotEmpty()) {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ package masstest
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import android.os.Looper
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
@@ -18,11 +17,9 @@ import kotlinx.coroutines.awaitAll
|
|||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import org.junit.jupiter.api.AfterAll
|
|
||||||
import org.junit.jupiter.api.BeforeAll
|
import org.junit.jupiter.api.BeforeAll
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.TestInstance
|
import org.junit.jupiter.api.TestInstance
|
||||||
import org.koin.core.context.stopKoin
|
|
||||||
import suwayomi.tachidesk.manga.impl.Source.getSourceList
|
import suwayomi.tachidesk.manga.impl.Source.getSourceList
|
||||||
import suwayomi.tachidesk.manga.impl.extension.Extension.installExtension
|
import suwayomi.tachidesk.manga.impl.extension.Extension.installExtension
|
||||||
import suwayomi.tachidesk.manga.impl.extension.Extension.uninstallExtension
|
import suwayomi.tachidesk.manga.impl.extension.Extension.uninstallExtension
|
||||||
@@ -31,7 +28,6 @@ import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.getExtensionList
|
|||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
|
||||||
import suwayomi.tachidesk.server.applicationSetup
|
import suwayomi.tachidesk.server.applicationSetup
|
||||||
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
|
||||||
import suwayomi.tachidesk.test.BASE_PATH
|
import suwayomi.tachidesk.test.BASE_PATH
|
||||||
import suwayomi.tachidesk.test.setLoggingEnabled
|
import suwayomi.tachidesk.test.setLoggingEnabled
|
||||||
import xyz.nulldev.ts.config.CONFIG_PREFIX
|
import xyz.nulldev.ts.config.CONFIG_PREFIX
|
||||||
@@ -55,8 +51,6 @@ class TestExtensionCompatibility {
|
|||||||
fun setup() {
|
fun setup() {
|
||||||
val dataRoot = File(BASE_PATH).absolutePath
|
val dataRoot = File(BASE_PATH).absolutePath
|
||||||
System.setProperty("$CONFIG_PREFIX.server.rootDir", dataRoot)
|
System.setProperty("$CONFIG_PREFIX.server.rootDir", dataRoot)
|
||||||
Looper.clearMainLooperForTest()
|
|
||||||
SettingsRegistry.clear()
|
|
||||||
applicationSetup()
|
applicationSetup()
|
||||||
setLoggingEnabled(false)
|
setLoggingEnabled(false)
|
||||||
|
|
||||||
@@ -78,22 +72,12 @@ class TestExtensionCompatibility {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sources =
|
sources = getSourceList().map { getCatalogueSourceOrNull(it.id.toLong())!! as HttpSource }
|
||||||
getSourceList()
|
|
||||||
.filter {
|
|
||||||
// filter local source
|
|
||||||
it.id.toLong() != 0L
|
|
||||||
}.map { getCatalogueSourceOrNull(it.id.toLong())!! as HttpSource }
|
|
||||||
}
|
}
|
||||||
setLoggingEnabled(true)
|
setLoggingEnabled(true)
|
||||||
File("$BASE_PATH/sources.txt").writeText(sources.joinToString("\n") { "${it.name} - ${it.lang.uppercase()} - ${it.id}" })
|
File("$BASE_PATH/sources.txt").writeText(sources.joinToString("\n") { "${it.name} - ${it.lang.uppercase()} - ${it.id}" })
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterAll
|
|
||||||
fun teardown() {
|
|
||||||
stopKoin()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun runTest() {
|
fun runTest() {
|
||||||
runBlocking(Dispatchers.Default) {
|
runBlocking(Dispatchers.Default) {
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
package suwayomi.tachidesk
|
|
||||||
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import org.koin.core.context.startKoin
|
|
||||||
import org.koin.core.context.stopKoin
|
|
||||||
import org.koin.dsl.module
|
|
||||||
import suwayomi.tachidesk.server.util.CEFManager
|
|
||||||
import java.nio.file.Files
|
|
||||||
import kotlin.io.path.ExperimentalPathApi
|
|
||||||
import kotlin.io.path.deleteRecursively
|
|
||||||
import kotlin.io.path.div
|
|
||||||
import kotlin.test.Test
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
@OptIn(ExperimentalPathApi::class)
|
|
||||||
class CefTest {
|
|
||||||
@Test
|
|
||||||
fun downloadedJbrIsValidForJcef() =
|
|
||||||
runTest {
|
|
||||||
val tempDownload = Files.createTempDirectory("kcef")
|
|
||||||
val module =
|
|
||||||
module {
|
|
||||||
single {
|
|
||||||
Json {
|
|
||||||
ignoreUnknownKeys = true
|
|
||||||
explicitNulls = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
startKoin {
|
|
||||||
modules(module)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
CEFManager.downloadRelease(tempDownload)
|
|
||||||
assertTrue { CEFManager.isInstallationValid(tempDownload / "release") }
|
|
||||||
} finally {
|
|
||||||
tempDownload.deleteRecursively()
|
|
||||||
stopKoin()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
package suwayomi.tachidesk
|
|
||||||
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.os.Message
|
|
||||||
import java.util.concurrent.CountDownLatch
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import kotlin.test.Test
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertNotEquals
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
import kotlin.text.StringBuilder
|
|
||||||
|
|
||||||
class LooperThread : Thread() {
|
|
||||||
var mHandler: Handler? = null
|
|
||||||
val latch = CountDownLatch(1)
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
Looper.prepare()
|
|
||||||
mHandler = Handler(Looper.myLooper()!!)
|
|
||||||
latch.countDown()
|
|
||||||
Looper.loop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LooperTest {
|
|
||||||
@Test
|
|
||||||
fun multiplePostWork() {
|
|
||||||
val thread = LooperThread()
|
|
||||||
thread.start()
|
|
||||||
val sb = StringBuilder()
|
|
||||||
val latch = CountDownLatch(1)
|
|
||||||
assertTrue(thread.latch.await(5, TimeUnit.SECONDS))
|
|
||||||
|
|
||||||
thread.mHandler!!.post {
|
|
||||||
Thread.sleep(100)
|
|
||||||
sb.append("a_b_c")
|
|
||||||
}
|
|
||||||
thread.mHandler!!.post {
|
|
||||||
Thread.sleep(100)
|
|
||||||
sb.append("_d_e_f")
|
|
||||||
}
|
|
||||||
thread.mHandler!!.post {
|
|
||||||
Thread.sleep(100)
|
|
||||||
sb.append("_g_h_i")
|
|
||||||
latch.countDown()
|
|
||||||
}
|
|
||||||
|
|
||||||
assertNotEquals("a_b_c_d_e_f_g_h_i", sb.toString())
|
|
||||||
assertTrue(latch.await(5, TimeUnit.SECONDS))
|
|
||||||
|
|
||||||
assertEquals("a_b_c_d_e_f_g_h_i", sb.toString())
|
|
||||||
thread.mHandler!!.looper.quit()
|
|
||||||
// thread.join()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun loopTest() {
|
|
||||||
val thread = LooperThread()
|
|
||||||
thread.start()
|
|
||||||
val sb = StringBuilder()
|
|
||||||
val expected = StringBuilder()
|
|
||||||
val latch = CountDownLatch(1)
|
|
||||||
assertTrue(thread.latch.await(5, TimeUnit.SECONDS))
|
|
||||||
val n = 100
|
|
||||||
|
|
||||||
for (i in 0 until n) {
|
|
||||||
thread.mHandler!!.post {
|
|
||||||
Thread.sleep(10)
|
|
||||||
sb.append("$i")
|
|
||||||
}
|
|
||||||
expected.append("$i")
|
|
||||||
}
|
|
||||||
|
|
||||||
thread.mHandler!!.post {
|
|
||||||
latch.countDown()
|
|
||||||
}
|
|
||||||
|
|
||||||
assertNotEquals(expected.toString(), sb.toString())
|
|
||||||
assertTrue(latch.await(5, TimeUnit.SECONDS), "only got to $sb")
|
|
||||||
|
|
||||||
assertEquals(expected.toString(), sb.toString())
|
|
||||||
thread.mHandler!!.looper.quit()
|
|
||||||
// thread.join()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,22 +18,19 @@ import suwayomi.tachidesk.test.clearTables
|
|||||||
class CategoryControllerTest : ApplicationTest() {
|
class CategoryControllerTest : ApplicationTest() {
|
||||||
@Test
|
@Test
|
||||||
fun categoryReorder() {
|
fun categoryReorder() {
|
||||||
clearTables(
|
|
||||||
CategoryTable,
|
|
||||||
)
|
|
||||||
Category.createCategory("foo")
|
Category.createCategory("foo")
|
||||||
Category.createCategory("bar")
|
Category.createCategory("bar")
|
||||||
val cats = Category.getCategoryList()
|
val cats = Category.getCategoryList()
|
||||||
val foo = cats.asSequence().filter { it.name == "foo" }.first()
|
val foo = cats.asSequence().filter { it.name == "foo" }.first()
|
||||||
val bar = cats.asSequence().filter { it.name == "bar" }.first()
|
val bar = cats.asSequence().filter { it.name == "bar" }.first()
|
||||||
assertEquals(0, foo.order)
|
assertEquals(1, foo.order)
|
||||||
assertEquals(1, bar.order)
|
assertEquals(2, bar.order)
|
||||||
Category.reorderCategory(1, 2)
|
Category.reorderCategory(1, 2)
|
||||||
val catsReordered = Category.getCategoryList()
|
val catsReordered = Category.getCategoryList()
|
||||||
val fooReordered = catsReordered.asSequence().filter { it.name == "foo" }.first()
|
val fooReordered = catsReordered.asSequence().filter { it.name == "foo" }.first()
|
||||||
val barReordered = catsReordered.asSequence().filter { it.name == "bar" }.first()
|
val barReordered = catsReordered.asSequence().filter { it.name == "bar" }.first()
|
||||||
assertEquals(1, fooReordered.order)
|
assertEquals(2, fooReordered.order)
|
||||||
assertEquals(0, barReordered.order)
|
assertEquals(1, barReordered.order)
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class CategoryMangaTest : ApplicationTest() {
|
|||||||
CategoryManga.getCategoryMangaList(DEFAULT_CATEGORY_ID)[0].unreadCount,
|
CategoryManga.getCategoryMangaList(DEFAULT_CATEGORY_ID)[0].unreadCount,
|
||||||
"Manga should not have any unread chapters",
|
"Manga should not have any unread chapters",
|
||||||
)
|
)
|
||||||
createChapters(mangaId, 10, false, start = 11)
|
createChapters(mangaId, 10, false)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
10,
|
10,
|
||||||
CategoryManga.getCategoryMangaList(DEFAULT_CATEGORY_ID)[0].unreadCount,
|
CategoryManga.getCategoryMangaList(DEFAULT_CATEGORY_ID)[0].unreadCount,
|
||||||
|
|||||||
@@ -12,12 +12,9 @@ import eu.kanade.tachiyomi.createAppModule
|
|||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import org.jetbrains.exposed.v1.core.DatabaseConfig
|
|
||||||
import org.jetbrains.exposed.v1.core.ExperimentalKeywordApi
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.Database
|
import org.jetbrains.exposed.v1.jdbc.Database
|
||||||
import org.junit.jupiter.api.BeforeAll
|
import org.junit.jupiter.api.BeforeAll
|
||||||
import org.koin.core.context.startKoin
|
import org.koin.core.context.startKoin
|
||||||
import org.koin.core.context.stopKoin
|
|
||||||
import suwayomi.tachidesk.server.ApplicationDirs
|
import suwayomi.tachidesk.server.ApplicationDirs
|
||||||
import suwayomi.tachidesk.server.JavalinSetup
|
import suwayomi.tachidesk.server.JavalinSetup
|
||||||
import suwayomi.tachidesk.server.ServerConfig
|
import suwayomi.tachidesk.server.ServerConfig
|
||||||
@@ -25,9 +22,7 @@ import suwayomi.tachidesk.server.androidCompat
|
|||||||
import suwayomi.tachidesk.server.database.databaseUp
|
import suwayomi.tachidesk.server.database.databaseUp
|
||||||
import suwayomi.tachidesk.server.serverConfig
|
import suwayomi.tachidesk.server.serverConfig
|
||||||
import suwayomi.tachidesk.server.serverModule
|
import suwayomi.tachidesk.server.serverModule
|
||||||
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
|
||||||
import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex
|
import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex
|
||||||
import suwayomi.tachidesk.server.util.ConfigTypeRegistration
|
|
||||||
import suwayomi.tachidesk.server.util.SystemTray
|
import suwayomi.tachidesk.server.util.SystemTray
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@@ -60,13 +55,6 @@ open class ApplicationTest {
|
|||||||
private var initializedTheApp = false
|
private var initializedTheApp = false
|
||||||
|
|
||||||
fun testingSetup() {
|
fun testingSetup() {
|
||||||
// register Tachidesk's config which is dubbed "ServerConfig"
|
|
||||||
SettingsRegistry.clear()
|
|
||||||
ConfigTypeRegistration.registerCustomTypes()
|
|
||||||
GlobalConfigManager.registerModule(
|
|
||||||
ServerConfig.register { GlobalConfigManager.config },
|
|
||||||
)
|
|
||||||
|
|
||||||
// Application dirs
|
// Application dirs
|
||||||
val applicationDirs = ApplicationDirs()
|
val applicationDirs = ApplicationDirs()
|
||||||
|
|
||||||
@@ -84,9 +72,13 @@ open class ApplicationTest {
|
|||||||
File(it).mkdirs()
|
File(it).mkdirs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// register Tachidesk's config which is dubbed "ServerConfig"
|
||||||
|
GlobalConfigManager.registerModule(
|
||||||
|
ServerConfig.register { GlobalConfigManager.config },
|
||||||
|
)
|
||||||
|
|
||||||
// initialize Koin modules
|
// initialize Koin modules
|
||||||
val app = App()
|
val app = App()
|
||||||
stopKoin()
|
|
||||||
startKoin {
|
startKoin {
|
||||||
modules(
|
modules(
|
||||||
createAppModule(app),
|
createAppModule(app),
|
||||||
@@ -136,14 +128,14 @@ open class ApplicationTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create system tray
|
// create system tray
|
||||||
// if (serverConfig.systemTrayEnabled.value) {
|
if (serverConfig.systemTrayEnabled.value) {
|
||||||
// try {
|
try {
|
||||||
// SystemTray.create()
|
SystemTray.create()
|
||||||
// } catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
// // cover both java.lang.Exception and java.lang.Error
|
// cover both java.lang.Exception and java.lang.Error
|
||||||
// e.printStackTrace()
|
e.printStackTrace()
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// Disable jetty's logging
|
// Disable jetty's logging
|
||||||
System.setProperty("org.eclipse.jetty.util.log.announce", "false")
|
System.setProperty("org.eclipse.jetty.util.log.announce", "false")
|
||||||
@@ -162,16 +154,8 @@ open class ApplicationTest {
|
|||||||
// fixes #119 , ref: https://github.com/Suwayomi/Suwayomi-Server/issues/119#issuecomment-894681292 , source Id calculation depends on String.lowercase()
|
// fixes #119 , ref: https://github.com/Suwayomi/Suwayomi-Server/issues/119#issuecomment-894681292 , source Id calculation depends on String.lowercase()
|
||||||
Locale.setDefault(Locale.ENGLISH)
|
Locale.setDefault(Locale.ENGLISH)
|
||||||
|
|
||||||
val dbConfig =
|
|
||||||
DatabaseConfig {
|
|
||||||
useNestedTransactions = true
|
|
||||||
@OptIn(ExperimentalKeywordApi::class)
|
|
||||||
preserveKeywordCasing = false
|
|
||||||
defaultSchema = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// in-memory database, don't discard database between connections/transactions
|
// in-memory database, don't discard database between connections/transactions
|
||||||
val db = Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;", "org.h2.Driver", databaseConfig = dbConfig)
|
val db = Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;", "org.h2.Driver")
|
||||||
|
|
||||||
databaseUp(db)
|
databaseUp(db)
|
||||||
|
|
||||||
|
|||||||
@@ -55,9 +55,8 @@ fun createChapters(
|
|||||||
mangaId: Int,
|
mangaId: Int,
|
||||||
amount: Int,
|
amount: Int,
|
||||||
read: Boolean,
|
read: Boolean,
|
||||||
start: Int = 1,
|
|
||||||
) {
|
) {
|
||||||
val list = listOf((0 until amount)).flatten().map { it + start }
|
val list = listOf((0 until amount)).flatten().map { 1 }
|
||||||
transaction {
|
transaction {
|
||||||
ChapterTable
|
ChapterTable
|
||||||
.batchInsert(list) {
|
.batchInsert(list) {
|
||||||
|
|||||||
Reference in New Issue
Block a user