mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-15 16:54:09 -05:00
Compare commits
3 Commits
a676b5a898
...
renovate/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbfa80f196 | ||
|
|
9d5d88e56c | ||
|
|
d57623fe2e |
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"
|
||||
JAR=$(ls ./server/build/*.jar| head -1)
|
||||
set +e
|
||||
timeout 30s java \
|
||||
timeout 30s java -DcrashOnFailedMigration=true \
|
||||
-Dsuwayomi.tachidesk.config.server.systemTrayEnabled=false \
|
||||
-Dsuwayomi.tachidesk.config.server.initialOpenInBrowserEnabled=false \
|
||||
-Dsuwayomi.tachidesk.config.server.databaseType=POSTGRESQL \
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
exit "$ecode"
|
||||
fi
|
||||
|
||||
timeout 30s java \
|
||||
timeout 30s java -DcrashOnFailedMigration=true \
|
||||
-Dsuwayomi.tachidesk.config.server.systemTrayEnabled=false \
|
||||
-Dsuwayomi.tachidesk.config.server.initialOpenInBrowserEnabled=false \
|
||||
-jar "$JAR"
|
||||
@@ -96,10 +96,6 @@ jobs:
|
||||
fi
|
||||
exit 0
|
||||
|
||||
- name: "Run tests"
|
||||
working-directory: master
|
||||
run: ./gradlew test --stacktrace
|
||||
|
||||
check_docs:
|
||||
name: Validate that all options are documented
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
8
.github/workflows/wiki.yml
vendored
8
.github/workflows/wiki.yml
vendored
@@ -1,7 +1,6 @@
|
||||
name: GitHub Wiki upload
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
@@ -36,13 +35,6 @@ jobs:
|
||||
set -e
|
||||
cd $GITHUB_WORKSPACE/${{github.repository}}.wiki
|
||||
cp -r $GITHUB_WORKSPACE/${{github.repository}}/docs/* .
|
||||
|
||||
stable="$(git -C $GITHUB_WORKSPACE/${{github.repository}} describe --abbrev=0 --tags)"
|
||||
if ! git -C $GITHUB_WORKSPACE/${{github.repository}} log --exit-code --pretty= "$stable.." -- docs/Configuring-Suwayomi‐Server.md; then
|
||||
echo "Changes to config detected, embedding link to stable"
|
||||
sed -i '1s/^/> [!WARNING]\n> This document describes the settings available in the preview version. Please head to ['$stable'](https:\/\/github.com\/Suwayomi\/Suwayomi-Server\/blob\/'$stable'\/docs\/Configuring-Suwayomi%E2%80%90Server.md) for the current stable version\n\n/' Configuring-Suwayomi‐Server.md
|
||||
fi
|
||||
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git add .
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,9 +9,6 @@ package xyz.nulldev.androidcompat.util
|
||||
|
||||
// adopted from: https://github.com/tachiyomiorg/tachiyomi/blob/4cefbce7c34e724b409b6ba127f3c6c5c346ad8d/app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt
|
||||
object SafePath {
|
||||
private const val MAX_FILENAME_CHARS = 240
|
||||
private const val MAX_FILENAME_UTF8_BYTES = 240
|
||||
|
||||
/**
|
||||
* Mutate the given filename to make it valid for a FAT filesystem,
|
||||
* replacing any invalid characters with "_". This method doesn't allow hidden files (starting
|
||||
@@ -30,41 +27,11 @@ object SafePath {
|
||||
sb.append('_')
|
||||
}
|
||||
}
|
||||
|
||||
return truncateFilename(sb.toString())
|
||||
// Even though vfat allows 255 UCS-2 chars, we might eventually write to
|
||||
// ext4 through a FUSE layer, so use that limit minus 15 reserved characters.
|
||||
return sb.toString().take(240)
|
||||
}
|
||||
|
||||
private fun truncateFilename(filename: String): String {
|
||||
// Keep a safety margin under common filesystem limits and satisfy both
|
||||
// character count and UTF-8 byte-length constraints.
|
||||
val output = StringBuilder(minOf(filename.length, MAX_FILENAME_CHARS))
|
||||
var usedBytes = 0
|
||||
var index = 0
|
||||
|
||||
while (index < filename.length && output.length < MAX_FILENAME_CHARS) {
|
||||
val codePoint = Character.codePointAt(filename, index)
|
||||
val codePointBytes = utf8ByteCount(codePoint)
|
||||
|
||||
if (usedBytes + codePointBytes > MAX_FILENAME_UTF8_BYTES) {
|
||||
break
|
||||
}
|
||||
|
||||
output.appendCodePoint(codePoint)
|
||||
usedBytes += codePointBytes
|
||||
index += Character.charCount(codePoint)
|
||||
}
|
||||
|
||||
return output.toString()
|
||||
}
|
||||
|
||||
private fun utf8ByteCount(codePoint: Int): Int =
|
||||
when {
|
||||
codePoint <= 0x7f -> 1
|
||||
codePoint <= 0x7ff -> 2
|
||||
codePoint <= 0xffff -> 3
|
||||
else -> 4
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given character is a valid filename character, false otherwise.
|
||||
*/
|
||||
|
||||
@@ -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,12 @@ import android.webkit.WebViewProvider.ScrollDelegate
|
||||
import android.webkit.WebViewProvider.ViewDelegate
|
||||
import android.webkit.WebViewRenderProcess
|
||||
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.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.cef.CefClient
|
||||
import org.cef.CefSettings
|
||||
import org.cef.browser.CefBrowser
|
||||
import org.cef.browser.CefFrame
|
||||
@@ -86,7 +88,8 @@ import java.io.BufferedWriter
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.Executor
|
||||
import kotlin.math.min
|
||||
import kotlin.collections.Map
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KFunction
|
||||
import kotlin.reflect.full.declaredMemberFunctions
|
||||
import kotlin.reflect.jvm.javaMethod
|
||||
@@ -101,8 +104,8 @@ class KcefWebViewProvider(
|
||||
private val urlHttpMapping: MutableMap<String, String> = mutableMapOf()
|
||||
private var initialRequestData: InitialRequestData? = null
|
||||
|
||||
private var kcefClient: CefClient? = null
|
||||
private var browser: CefBrowser? = null
|
||||
private var kcefClient: KCEFClient? = null
|
||||
private var browser: KCEFBrowser? = null
|
||||
|
||||
private val handler = Handler(view.webViewLooper)
|
||||
|
||||
@@ -114,8 +117,8 @@ class KcefWebViewProvider(
|
||||
private val initHandler: InitBrowserHandler by KoinPlatformTools.defaultContext().get().inject()
|
||||
}
|
||||
|
||||
interface InitBrowserHandler {
|
||||
fun init(provider: KcefWebViewProvider): Unit
|
||||
public interface InitBrowserHandler {
|
||||
public fun init(provider: KcefWebViewProvider): Unit
|
||||
}
|
||||
|
||||
private data class InitialRequestData(
|
||||
@@ -191,7 +194,7 @@ class KcefWebViewProvider(
|
||||
}
|
||||
}
|
||||
|
||||
private class DisplayHandler : CefDisplayHandlerAdapter() {
|
||||
private inner class DisplayHandler : CefDisplayHandlerAdapter() {
|
||||
override fun onConsoleMessage(
|
||||
browser: CefBrowser,
|
||||
level: CefSettings.LogSeverity,
|
||||
@@ -219,7 +222,6 @@ class KcefWebViewProvider(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private inner class LoadHandler : CefLoadHandlerAdapter() {
|
||||
override fun onLoadEnd(
|
||||
browser: CefBrowser,
|
||||
@@ -366,7 +368,7 @@ class KcefWebViewProvider(
|
||||
callback: CefCallback,
|
||||
): Boolean {
|
||||
val data = resolvedData ?: return false
|
||||
val bytesToTransfer = min(bytesToRead, data.size - readOffset)
|
||||
val bytesToTransfer = Math.min(bytesToRead, data.size - readOffset)
|
||||
Log.v(
|
||||
TAG,
|
||||
"readResponse: $readOffset/${data.size}, reading $bytesToRead->$bytesToTransfer",
|
||||
@@ -378,7 +380,7 @@ class KcefWebViewProvider(
|
||||
}
|
||||
}
|
||||
|
||||
private class WebResponseResourceHandler(
|
||||
private inner class WebResponseResourceHandler(
|
||||
val webResponse: WebResourceResponse,
|
||||
) : ArrayResponseResourceHandler() {
|
||||
override fun processRequest(
|
||||
@@ -408,7 +410,7 @@ class KcefWebViewProvider(
|
||||
}
|
||||
}
|
||||
|
||||
private class HtmlResponseResourceHandler(
|
||||
private inner class HtmlResponseResourceHandler(
|
||||
val html: String,
|
||||
) : ArrayResponseResourceHandler() {
|
||||
override fun processRequest(
|
||||
@@ -439,7 +441,7 @@ class KcefWebViewProvider(
|
||||
view,
|
||||
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) }
|
||||
|
||||
@@ -466,7 +468,7 @@ class KcefWebViewProvider(
|
||||
}
|
||||
if (response == null) {
|
||||
// prefer user's response override
|
||||
urlHttpMapping[request.url.trimEnd('/')]?.let {
|
||||
urlHttpMapping.get(request.url.trimEnd('/'))?.let {
|
||||
return HtmlResponseResourceHandler(it)
|
||||
}
|
||||
}
|
||||
@@ -475,7 +477,6 @@ class KcefWebViewProvider(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private inner class RequestHandler : CefRequestHandlerAdapter() {
|
||||
override fun getResourceRequestHandler(
|
||||
browser: CefBrowser,
|
||||
@@ -485,13 +486,11 @@ class KcefWebViewProvider(
|
||||
isDownload: Boolean,
|
||||
requestInitiator: String,
|
||||
disableDefaultHandling: BoolRef,
|
||||
): CefResourceRequestHandler = ResourceRequestHandler()
|
||||
): CefResourceRequestHandler? = ResourceRequestHandler()
|
||||
|
||||
override fun onRenderProcessTerminated(
|
||||
browser: CefBrowser,
|
||||
status: CefRequestHandler.TerminationStatus,
|
||||
errorCode: Int,
|
||||
errorString: String,
|
||||
) {
|
||||
handler.post {
|
||||
viewClient.onRenderProcessGone(
|
||||
@@ -510,13 +509,13 @@ class KcefWebViewProvider(
|
||||
override fun onRequestMediaAccessPermission(
|
||||
browser: CefBrowser,
|
||||
frame: CefFrame,
|
||||
requestingUrl: String,
|
||||
requestedPermissions: Int,
|
||||
requesting_url: String,
|
||||
requested_permissions: Int,
|
||||
callback: CefMediaAccessCallback,
|
||||
): Boolean {
|
||||
handler.post {
|
||||
Log.v(TAG, "Checking permission for $requestingUrl: $requestedPermissions")
|
||||
chromeClient.onPermissionRequest(CefPermissionRequest(requestingUrl, requestedPermissions, callback))
|
||||
Log.v(TAG, "Checking permission for $requesting_url: $requested_permissions")
|
||||
chromeClient.onPermissionRequest(CefPermissionRequest(requesting_url, requested_permissions, callback))
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -529,18 +528,16 @@ class KcefWebViewProvider(
|
||||
Log.v(TAG, "KcefWebViewProvider: initialize")
|
||||
destroy()
|
||||
kcefClient =
|
||||
runBlocking {
|
||||
CefHelper.createClient().apply {
|
||||
addDisplayHandler(DisplayHandler())
|
||||
addLoadHandler(LoadHandler())
|
||||
addRequestHandler(RequestHandler())
|
||||
addPermissionHandler(PermissionHandler())
|
||||
KCEF.newClientBlocking().apply {
|
||||
addDisplayHandler(DisplayHandler())
|
||||
addLoadHandler(LoadHandler())
|
||||
addRequestHandler(RequestHandler())
|
||||
addPermissionHandler(PermissionHandler())
|
||||
|
||||
val config = CefMessageRouter.CefMessageRouterConfig()
|
||||
config.jsQueryFunction = QUERY_FN
|
||||
config.jsCancelFunction = QUERY_CANCEL_FN
|
||||
addMessageRouter(CefMessageRouter.create(config, MessageRouterHandler()))
|
||||
}
|
||||
val config = CefMessageRouter.CefMessageRouterConfig()
|
||||
config.jsQueryFunction = QUERY_FN
|
||||
config.jsCancelFunction = QUERY_CANCEL_FN
|
||||
addMessageRouter(CefMessageRouter.create(config, MessageRouterHandler()))
|
||||
}
|
||||
initHandler.init(this)
|
||||
}
|
||||
@@ -618,7 +615,6 @@ class KcefWebViewProvider(
|
||||
.createBrowser(
|
||||
loadUrl,
|
||||
CefRendering.OFFSCREEN,
|
||||
false,
|
||||
).apply {
|
||||
// NOTE: Without this, we don't seem to be receiving any events
|
||||
createImmediately()
|
||||
@@ -643,7 +639,6 @@ class KcefWebViewProvider(
|
||||
.createBrowser(
|
||||
url,
|
||||
CefRendering.OFFSCREEN,
|
||||
false,
|
||||
).apply {
|
||||
// NOTE: Without this, we don't seem to be receiving any events
|
||||
createImmediately()
|
||||
@@ -669,19 +664,27 @@ class KcefWebViewProvider(
|
||||
browser?.close(true)
|
||||
browser?.dispose()
|
||||
chromeClient.onProgressChanged(view, 0)
|
||||
val url = baseUrl ?: "about:blank"
|
||||
urlHttpMapping[url.trimEnd('/')] = data
|
||||
|
||||
browser =
|
||||
kcefClient!!
|
||||
.createBrowser(
|
||||
url,
|
||||
CefRendering.OFFSCREEN,
|
||||
false,
|
||||
).apply {
|
||||
// NOTE: Without this, we don't seem to be receiving any events
|
||||
createImmediately()
|
||||
(
|
||||
baseUrl?.let { url ->
|
||||
urlHttpMapping.put(url.trimEnd('/'), data)
|
||||
kcefClient!!.createBrowser(
|
||||
url,
|
||||
CefRendering.OFFSCREEN,
|
||||
)
|
||||
}
|
||||
?: 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")
|
||||
}
|
||||
|
||||
@@ -691,11 +694,11 @@ class KcefWebViewProvider(
|
||||
) {
|
||||
browser!!.evaluateJavaScript(
|
||||
script.removePrefix("javascript:"),
|
||||
)
|
||||
{
|
||||
Log.v(TAG, "JS returned: $it")
|
||||
it?.let { handler.post { resultCallback?.onReceiveValue(it) } }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun saveWebArchive(filename: String): Unit = throw RuntimeException("Stub!")
|
||||
@@ -775,23 +778,15 @@ class KcefWebViewProvider(
|
||||
|
||||
override fun getContentWidth(): Int = throw RuntimeException("Stub!")
|
||||
|
||||
override fun pauseTimers() {
|
||||
Log.v(TAG, "pauseTimers: doing nothing")
|
||||
}
|
||||
override fun pauseTimers(): Unit = throw RuntimeException("Stub!")
|
||||
|
||||
override fun resumeTimers() {
|
||||
Log.v(TAG, "resumeTimers: doing nothing")
|
||||
}
|
||||
override fun resumeTimers(): Unit = throw RuntimeException("Stub!")
|
||||
|
||||
override fun onPause() {
|
||||
Log.v(TAG, "onPause: doing nothing")
|
||||
}
|
||||
override fun onPause(): Unit = throw RuntimeException("Stub!")
|
||||
|
||||
override fun onResume() {
|
||||
Log.v(TAG, "onResume: doing nothing")
|
||||
}
|
||||
override fun onResume(): Unit = throw RuntimeException("Stub!")
|
||||
|
||||
override fun isPaused(): Boolean = false
|
||||
override fun isPaused(): Boolean = throw RuntimeException("Stub!")
|
||||
|
||||
override fun freeMemory(): Unit = throw RuntimeException("Stub!")
|
||||
|
||||
@@ -845,7 +840,6 @@ class KcefWebViewProvider(
|
||||
|
||||
override fun getWebChromeClient(): WebChromeClient = chromeClient
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun setPictureListener(listener: PictureListener): Unit = throw RuntimeException("Stub!")
|
||||
|
||||
@Serializable
|
||||
@@ -868,7 +862,7 @@ class KcefWebViewProvider(
|
||||
obj: Any,
|
||||
interfaceName: String,
|
||||
) {
|
||||
val cls = obj::class
|
||||
val cls = obj::class as KClass<Any>
|
||||
mappings.addAll(
|
||||
cls.declaredMemberFunctions.map {
|
||||
// This is ridiculous, but necessary, otherwise "public final" throws
|
||||
@@ -930,8 +924,7 @@ class KcefWebViewProvider(
|
||||
override fun getRendererPriorityWaivedWhenNotVisible(): Boolean = throw RuntimeException("Stub!")
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
override fun setTextClassifier(textClassifier: TextClassifier?) {
|
||||
}
|
||||
override fun setTextClassifier(textClassifier: TextClassifier?) {}
|
||||
|
||||
override fun getTextClassifier(): TextClassifier = TextClassifier.NO_OP
|
||||
|
||||
@@ -957,13 +950,11 @@ class KcefWebViewProvider(
|
||||
override fun onProvideAutofillVirtualStructure(
|
||||
@SuppressWarnings("unused") structure: android.view.ViewStructure,
|
||||
@SuppressWarnings("unused") flags: Int,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
override fun autofill(
|
||||
@SuppressWarnings("unused") values: SparseArray<AutofillValue>,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
override fun isVisibleToUserForAutofill(
|
||||
@SuppressWarnings("unused") virtualId: Int,
|
||||
@@ -974,8 +965,7 @@ class KcefWebViewProvider(
|
||||
override fun onProvideContentCaptureStructure(
|
||||
@SuppressWarnings("unused") structure: android.view.ViewStructure,
|
||||
@SuppressWarnings("unused") flags: Int,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
override fun getAccessibilityNodeProvider(): AccessibilityNodeProvider = throw RuntimeException("Stub!")
|
||||
|
||||
@@ -1045,8 +1035,7 @@ class KcefWebViewProvider(
|
||||
override fun onMovedToDisplay(
|
||||
displayId: Int,
|
||||
config: Configuration,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
override fun onVisibilityChanged(
|
||||
changedView: View,
|
||||
|
||||
19
CHANGELOG.md
19
CHANGELOG.md
@@ -10,25 +10,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
- .
|
||||
|
||||
### 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
|
||||
- (**CloudFlareInterceptor**) Don't send the `cf_clearance` cookie back to Flaresolverr
|
||||
- (**WebUI**) Handle serving non-default webui with "bundled"
|
||||
- (**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**) 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
|
||||
- (**Build**) Fix CURL failing silently in builds
|
||||
- (WebUI) Handle serving non-default webui with "bundled"
|
||||
|
||||
## [v2.2.2100] + [WebUI: v20260508.01] - 2026-05-08
|
||||
|
||||
|
||||
11
README.md
11
README.md
@@ -8,7 +8,7 @@
|
||||
- [Features](#features)
|
||||
- [Suwayomi client projects](#suwayomi-client-projects)
|
||||
- [Integrated clients](#integrated-clients)
|
||||
- [Other clients](#other-clients-potentially-inactive-or-abandoned)
|
||||
- [Other clients](#other-clients-potentially-inactive-or-abondend)
|
||||
- [Downloading and Running the app](#downloading-and-running-the-app)
|
||||
- [Using Operating System Specific Bundles](#using-operating-system-specific-bundles)
|
||||
- [Windows](#windows)
|
||||
@@ -82,7 +82,6 @@ These clients are built-in options, and the server can keep them automatically u
|
||||
- [Tachidesk-JUI](https://github.com/Suwayomi/Tachidesk-JUI): Desktop app (windows, linux, mac); can manage its own suwayomi server instance
|
||||
- [Tachidesk-Sorayomi](https://github.com/Suwayomi/Tachidesk-Sorayomi): Web app; Desktop app (windows, linux, mac); Android app; requires access to a running server
|
||||
- [Tachidesk-qtui](https://github.com/Suwayomi/Tachidesk-qtui): Android app; iOS app Desktop app (linux); requires access to a running server
|
||||
- [Suwayomi Client for KOReader](https://github.com/LK4D4/suwayomi.koplugin): KOReader plugin; works anywhere KOReader can run (Android, Kindle, Kobo, etc.); requires access to a running server
|
||||
|
||||
# Downloading and Running the app
|
||||
## Using Operating System Specific Bundles
|
||||
@@ -107,13 +106,13 @@ Download the latest `linux-x64`(x86_64) release from [the releases section](http
|
||||
|
||||
#### 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.
|
||||
|
||||
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.
|
||||
|
||||
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 so, use `ldd ~/.local/share/Tachidesk/bin/kcef/libjcef.so | grep not` to figure out which libraries are not found on your system.
|
||||
|
||||
@@ -124,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.
|
||||
|
||||
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
|
||||
### 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.
|
||||
|
||||
@@ -25,7 +25,6 @@ allprojects {
|
||||
maven("https://github.com/Suwayomi/Suwayomi-Server/raw/android-jar/")
|
||||
maven("https://jitpack.io")
|
||||
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 webviewJbrRelease = "jbr-release-25.0.3b475.60"
|
||||
|
||||
private val getCommitCount = {
|
||||
runCatching {
|
||||
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.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
|
||||
```
|
||||
server.downloadAsCbz = true
|
||||
|
||||
@@ -1,92 +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).
|
||||
|
||||
|
||||
## Flaresolverr required
|
||||
|
||||
- `java.io.IOException: Cloudflare bypass currently disabled`
|
||||
|
||||
The source you are using has enabled CloudFlare's bot protection.
|
||||
If you open the source's website in your browser, you should see the "Confirm I'm human" page.
|
||||
|
||||
Solution:
|
||||
- Download and set up [Flaresolverr](https://github.com/FlareSolverr/FlareSolverr) or [Byparr](https://github.com/ThePhaseless/Byparr). Make sure to run Flaresolverr/Byparr every time you use this source.
|
||||
|
||||
|
||||
## Flaresolverr not running
|
||||
|
||||
- `java.io.IOException: Failed to connect to localhost/[0:0:0:0:0:0:0:1]:8191`
|
||||
|
||||
You have configured Flaresolverr by enabling the `server.flareSolverrEnabled` setting, but Flaresolverr is not installed and/or running.
|
||||
|
||||
Solutions:
|
||||
- Install Flaresolverr if you haven't already (see previous section). Then ensure it is running (Windows: do not close the console window!).
|
||||
- If it is running, ensure the configured url in `server.flareSolverrUrl` is correct. There is usually no need to change this.
|
||||
- If it is running and the url is correct, check your firewall settings, your system may be blocking access to Flaresolverr.
|
||||
|
||||
|
||||
## General Troubleshooting
|
||||
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 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- Delete the Suwayomi data directory located below and re-run the app.
|
||||
|
||||
Note: Replace `<Account>` with the currently logged in account/username on your pc.
|
||||
|
||||
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.
|
||||
|
||||
@@ -4,20 +4,19 @@ coroutines = "1.11.0"
|
||||
serialization = "1.11.0"
|
||||
jvmTarget = "21"
|
||||
okhttp = "5.3.2" # Major version is locked by Tachiyomi extensions
|
||||
javalin = "7.2.2"
|
||||
javalin = "7.2.0"
|
||||
jte = "3.2.4"
|
||||
jackson = "3.1.4" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
|
||||
exposed = "1.3.0"
|
||||
jackson = "3.1.2" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
|
||||
exposed = "0.61.0"
|
||||
dex2jar = "2.4.36"
|
||||
polyglot = "25.0.3"
|
||||
settings = "1.3.0"
|
||||
twelvemonkeys = "3.13.1"
|
||||
graphqlkotlin = "10.0.0-alpha.4"
|
||||
graphqlkotlin = "8.9.0"
|
||||
xmlserialization = "0.91.3"
|
||||
ktlint = "1.8.0"
|
||||
koin = "4.2.1"
|
||||
moko = "0.26.4"
|
||||
jcef = "144.0.15-g72717cf-chromium-144.0.7559.172-api-1.21-262-b34"
|
||||
|
||||
[libraries]
|
||||
# Kotlin
|
||||
@@ -38,9 +37,9 @@ serialization-xml-core = { module = "io.github.pdvrieze.xmlutil:core", version.r
|
||||
serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization-jvm", version.ref = "xmlserialization" }
|
||||
|
||||
# Logging
|
||||
slf4japi = "org.slf4j:slf4j-api:2.0.18"
|
||||
logback = "ch.qos.logback:logback-classic:1.5.33"
|
||||
kotlinlogging = "io.github.oshai:kotlin-logging-jvm:8.0.4"
|
||||
slf4japi = "org.slf4j:slf4j-api:2.0.17"
|
||||
logback = "ch.qos.logback:logback-classic:1.5.32"
|
||||
kotlinlogging = "io.github.oshai:kotlin-logging-jvm:8.0.02"
|
||||
|
||||
# OkHttp
|
||||
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||
@@ -55,7 +54,7 @@ javalin-openapi = { module = "io.javalin:javalin-openapi", version.ref = "javali
|
||||
javalin-rendering = { module = "io.javalin:javalin-rendering-jte", version.ref = "javalin" }
|
||||
jackson-databind = { module = "tools.jackson.core:jackson-databind", version.ref = "jackson" }
|
||||
jackson-kotlin = { module = "tools.jackson.module:jackson-module-kotlin", version.ref = "jackson" }
|
||||
jackson-annotations = "com.fasterxml.jackson.core:jackson-annotations:2.22"
|
||||
jackson-annotations = "com.fasterxml.jackson.core:jackson-annotations:2.20"
|
||||
jte = { module = "gg.jte:jte", version.ref = "jte" }
|
||||
kte = { module = "gg.jte:jte-kotlin", version.ref = "jte" }
|
||||
|
||||
@@ -69,13 +68,12 @@ exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "e
|
||||
exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" }
|
||||
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
|
||||
exposed-javatime = { module = "org.jetbrains.exposed:exposed-java-time", version.ref = "exposed" }
|
||||
exposed-kotlintime = { module = "org.jetbrains.exposed:exposed-kotlin-datetime", version.ref = "exposed" }
|
||||
postgres = "org.postgresql:postgresql:42.7.11"
|
||||
h2 = "com.h2database:h2:2.4.240"
|
||||
h2 = "com.h2database:h2:1.4.200" # current database driver, can't update to h2 v2 without sql migration
|
||||
hikaricp = "com.zaxxer:HikariCP:7.0.2"
|
||||
|
||||
# Exposed Migrations
|
||||
exposed-migrations = "com.github.Suwayomi:exposed-migrations:3.10.1"
|
||||
exposed-migrations = "com.github.Suwayomi:exposed-migrations:3.8.0"
|
||||
|
||||
# Dependency Injection
|
||||
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
|
||||
@@ -117,7 +115,7 @@ appdirs = "ca.gosyer:kotlin-multiplatform-appdirs:2.0.0"
|
||||
cache4k = "io.github.reactivecircus.cache4k:cache4k:0.14.0"
|
||||
zip4j = "net.lingala.zip4j:zip4j:2.11.6"
|
||||
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
|
||||
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84"
|
||||
@@ -148,7 +146,7 @@ twelvemonkeys-imageio-webp = { module = "com.twelvemonkeys.imageio:imageio-webp"
|
||||
imageio-webp = "com.github.usefulness:webp-imageio:0.10.2"
|
||||
|
||||
# Testing
|
||||
mockk = "io.mockk:mockk:1.14.11"
|
||||
mockk = "io.mockk:mockk:1.14.9"
|
||||
|
||||
# cron scheduler
|
||||
cron4j = "it.sauronsoftware.cron4j:cron4j:2.2.5"
|
||||
@@ -157,9 +155,7 @@ cron4j = "it.sauronsoftware.cron4j:cron4j:2.2.5"
|
||||
cronUtils = "com.cronutils:cron-utils:9.2.1"
|
||||
|
||||
# Webview
|
||||
jcef = { module = "org.jetbrains.intellij.deps.jcef:jcef", version.ref = "jcef" }
|
||||
gluegen = "org.jogamp.gluegen:gluegen-rt:2.5.0"
|
||||
jogl = "org.jogamp.jogl:jogl-all:2.5.0"
|
||||
kcef = "dev.datlag:kcef:2024.04.20.4"
|
||||
|
||||
# User
|
||||
jwt = "com.auth0:java-jwt:4.5.2"
|
||||
@@ -186,7 +182,7 @@ buildconfig = { id = "com.github.gmazzo.buildconfig", version = "6.0.9"}
|
||||
download = { id = "de.undercouch.download", version = "5.7.0"}
|
||||
|
||||
# ShadowJar
|
||||
shadowjar = { id = "com.gradleup.shadow", version = "8.3.11"}
|
||||
shadowjar = { id = "com.gradleup.shadow", version = "8.3.10"}
|
||||
|
||||
# Moko
|
||||
moko = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "moko" }
|
||||
@@ -216,7 +212,7 @@ shared = [
|
||||
"dex2jar-tools",
|
||||
"apk-parser",
|
||||
"jackson-annotations",
|
||||
"jcef",
|
||||
"kcef"
|
||||
]
|
||||
|
||||
sharedTest = [
|
||||
@@ -246,7 +242,6 @@ exposed = [
|
||||
"exposed-dao",
|
||||
"exposed-jdbc",
|
||||
"exposed-javatime",
|
||||
"exposed-kotlintime",
|
||||
]
|
||||
systemtray = [
|
||||
"systemtray-core",
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
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
|
||||
retries=0
|
||||
retryBackOffMs=500
|
||||
|
||||
@@ -149,15 +149,15 @@ move_release_to_output_dir() {
|
||||
}
|
||||
|
||||
download_launcher() {
|
||||
LAUNCHER_URL=$(curl -sf "https://api.github.com/repos/Suwayomi/Suwayomi-Launcher/releases/latest" | grep "browser_download_url" | grep ".jar" | head -n 1 | cut -d '"' -f 4)
|
||||
curl -fL "$LAUNCHER_URL" -o "Suwayomi-Launcher.jar"
|
||||
LAUNCHER_URL=$(curl -s "https://api.github.com/repos/Suwayomi/Suwayomi-Launcher/releases/latest" | grep "browser_download_url" | grep ".jar" | head -n 1 | cut -d '"' -f 4)
|
||||
curl -L "$LAUNCHER_URL" -o "Suwayomi-Launcher.jar"
|
||||
mv "Suwayomi-Launcher.jar" "$RELEASE_NAME/Suwayomi-Launcher.jar"
|
||||
}
|
||||
|
||||
download_jogamp() {
|
||||
local platform="$1"
|
||||
if [ ! -f jogamp-all-platforms.7z ]; then
|
||||
curl -f "https://jogamp.org/deployment/jogamp-current/archive/jogamp-all-platforms.7z" -o jogamp-all-platforms.7z
|
||||
curl "https://jogamp.org/deployment/jogamp-current/archive/jogamp-all-platforms.7z" -o jogamp-all-platforms.7z
|
||||
fi
|
||||
|
||||
7z x jogamp-all-platforms.7z "jogamp-all-platforms/lib/$platform/"
|
||||
@@ -168,7 +168,7 @@ download_jogamp() {
|
||||
|
||||
download_electron() {
|
||||
if [ ! -f "$ELECTRON" ]; then
|
||||
curl -fL "$ELECTRON_URL" -o "$ELECTRON"
|
||||
curl -L "$ELECTRON_URL" -o "$ELECTRON"
|
||||
fi
|
||||
|
||||
unzip "$ELECTRON" -d "$RELEASE_NAME/electron/"
|
||||
@@ -181,7 +181,7 @@ setup_jre() {
|
||||
mv "jre" "$RELEASE_NAME/jre"
|
||||
else
|
||||
if [ ! -f "$JRE" ]; then
|
||||
curl -fL "$JRE_URL" -o "$JRE"
|
||||
curl -L "$JRE_URL" -o "$JRE"
|
||||
fi
|
||||
|
||||
local ext="${JRE##*.}"
|
||||
@@ -273,7 +273,7 @@ make_appimage() {
|
||||
sudo apt update
|
||||
sudo apt install libfuse2
|
||||
fi
|
||||
curl -fL $APPIMAGE_URL -o $APPIMAGE_TOOLNAME
|
||||
curl -L $APPIMAGE_URL -o $APPIMAGE_TOOLNAME
|
||||
chmod +x $APPIMAGE_TOOLNAME
|
||||
ARCH=x86_64 ./$APPIMAGE_TOOLNAME "$RELEASE_NAME" "$RELEASE"
|
||||
}
|
||||
@@ -300,7 +300,7 @@ make_windows_bundle() {
|
||||
#local rcedit_url="https://github.com/electron/rcedit/releases/download/v0.1.1/$rcedit"
|
||||
## change electron's icon
|
||||
#if [ ! -f "$rcedit" ]; then
|
||||
#curl -fL "$rcedit_url" -o "$rcedit"
|
||||
#curl -L "$rcedit_url" -o "$rcedit"
|
||||
#fi
|
||||
|
||||
#local icon="server/src/main/resources/icon/faviconlogo.ico"
|
||||
|
||||
@@ -37,10 +37,6 @@ dependencies {
|
||||
implementation(libs.bundles.shared)
|
||||
testImplementation(libs.bundles.sharedTest)
|
||||
|
||||
// WebView
|
||||
implementation(libs.gluegen)
|
||||
implementation(libs.jogl)
|
||||
|
||||
// OkHttp
|
||||
implementation(libs.bundles.okhttp)
|
||||
implementation(libs.okio)
|
||||
@@ -163,8 +159,6 @@ buildConfig {
|
||||
|
||||
buildConfigField("String", "GITHUB", quoteWrap("https://github.com/Suwayomi/Suwayomi-Server"))
|
||||
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 {
|
||||
@@ -177,8 +171,6 @@ tasks {
|
||||
"Implementation-Vendor" to "The Suwayomi Project",
|
||||
"Specification-Version" to getTachideskVersion(),
|
||||
"Implementation-Version" to getTachideskRevision(),
|
||||
"Multi-Release" to true, // needed for polyglot
|
||||
"X-JBR-Release" to webviewJbrRelease,
|
||||
)
|
||||
}
|
||||
archiveBaseName.set(rootProject.name)
|
||||
@@ -189,11 +181,7 @@ tasks {
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform {
|
||||
if (!project.hasProperty("masstest")) {
|
||||
exclude("**/masstest/*")
|
||||
}
|
||||
}
|
||||
useJUnitPlatform()
|
||||
testLogging {
|
||||
showStandardStreams = true
|
||||
events("passed", "skipped", "failed")
|
||||
|
||||
@@ -2,6 +2,7 @@ package suwayomi.tachidesk.server.settings.generation
|
||||
|
||||
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
||||
import java.io.File
|
||||
import kotlin.text.appendLine
|
||||
|
||||
object SettingsBackupServerSettingsGenerator {
|
||||
fun generate(
|
||||
|
||||
@@ -2,6 +2,7 @@ package suwayomi.tachidesk.server.settings.generation
|
||||
|
||||
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
||||
import java.io.File
|
||||
import kotlin.text.appendLine
|
||||
|
||||
object SettingsBackupSettingsHandlerGenerator {
|
||||
fun generate(
|
||||
|
||||
@@ -2,6 +2,7 @@ package suwayomi.tachidesk.server.settings.generation
|
||||
|
||||
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
||||
import java.io.File
|
||||
import kotlin.text.appendLine
|
||||
|
||||
object SettingsGraphqlTypeGenerator {
|
||||
fun generate(
|
||||
|
||||
@@ -20,13 +20,12 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import suwayomi.tachidesk.graphql.types.AuthMode
|
||||
import suwayomi.tachidesk.graphql.types.CbzMediaType
|
||||
import suwayomi.tachidesk.graphql.types.DatabaseType
|
||||
@@ -57,14 +56,16 @@ import suwayomi.tachidesk.server.settings.PathSetting
|
||||
import suwayomi.tachidesk.server.settings.SettingGroup
|
||||
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
||||
import suwayomi.tachidesk.server.settings.StringSetting
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import xyz.nulldev.ts.config.GlobalConfigManager
|
||||
import xyz.nulldev.ts.config.SystemPropertyOverridableConfigModule
|
||||
import kotlin.collections.associate
|
||||
import kotlin.getValue
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
val mutableConfigValueScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
@@ -74,21 +75,6 @@ val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() }
|
||||
|
||||
private val application: Application by injectLazy()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun <T> subscribeTo(
|
||||
flow: Flow<T>,
|
||||
ignoreInitialValue: Boolean = true,
|
||||
onChange: suspend (value: T) -> Unit,
|
||||
) {
|
||||
val actualFlow =
|
||||
if (ignoreInitialValue) {
|
||||
flow.drop(1)
|
||||
} else {
|
||||
flow
|
||||
}
|
||||
actualFlow.distinctUntilChanged().conflate().onEach { onChange(it) }.launchIn(mutableConfigValueScope)
|
||||
}
|
||||
|
||||
// Settings are ordered by "protoNumber".
|
||||
class ServerConfig(
|
||||
getConfig: () -> Config,
|
||||
@@ -596,7 +582,7 @@ class ServerConfig(
|
||||
privacySafe = true,
|
||||
defaultValue = SortOrder.DESC,
|
||||
enumClass = SortOrder::class,
|
||||
typeInfo = SettingsRegistry.PartialTypeInfo(imports = listOf("org.jetbrains.exposed.v1.core.SortOrder")),
|
||||
typeInfo = SettingsRegistry.PartialTypeInfo(imports = listOf("org.jetbrains.exposed.sql.SortOrder")),
|
||||
)
|
||||
|
||||
val authMode: MutableStateFlow<AuthMode> by EnumSetting(
|
||||
@@ -1030,14 +1016,6 @@ class ServerConfig(
|
||||
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)"
|
||||
)
|
||||
|
||||
|
||||
|
||||
/** ****************************************************************** **/
|
||||
@@ -1083,7 +1061,18 @@ class ServerConfig(
|
||||
flow: Flow<T>,
|
||||
onChange: suspend (value: T) -> Unit,
|
||||
ignoreInitialValue: Boolean = true,
|
||||
) = subscribeTo(flow, ignoreInitialValue, onChange)
|
||||
) {
|
||||
val actualFlow =
|
||||
if (ignoreInitialValue) {
|
||||
flow.drop(1)
|
||||
} else {
|
||||
flow
|
||||
}
|
||||
|
||||
val sharedFlow = MutableSharedFlow<T>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
actualFlow.distinctUntilChanged().mapLatest { sharedFlow.emit(it) }.launchIn(mutableConfigValueScope)
|
||||
sharedFlow.onEach { onChange(it) }.launchIn(mutableConfigValueScope)
|
||||
}
|
||||
|
||||
fun <T> subscribeTo(
|
||||
flow: Flow<T>,
|
||||
|
||||
@@ -17,7 +17,6 @@ enum class SettingGroup(
|
||||
CLOUDFLARE("Cloudflare"),
|
||||
OPDS("OPDS"),
|
||||
KOREADER_SYNC("KOReader sync"),
|
||||
WEB_VIEW("WebView"),
|
||||
;
|
||||
|
||||
override fun toString(): String = value
|
||||
|
||||
@@ -2,6 +2,7 @@ package suwayomi.tachidesk.server.settings
|
||||
|
||||
import com.typesafe.config.ConfigValue
|
||||
import com.typesafe.config.parser.ConfigDocument
|
||||
import kotlin.collections.find
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
@@ -88,6 +89,4 @@ object SettingsRegistry {
|
||||
fun get(name: String): SettingMetadata? = settings[name]
|
||||
|
||||
fun getAll(): Map<String, SettingMetadata> = settings.toMap()
|
||||
|
||||
fun clear() = settings.clear()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.network
|
||||
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.serialization.DeserializationStrategy
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.okio.decodeFromBufferedSource
|
||||
import kotlinx.serialization.serializer
|
||||
|
||||
@@ -103,7 +103,7 @@ class CloudflareInterceptor(
|
||||
companion object {
|
||||
private val ERROR_CODES = listOf(403, 503)
|
||||
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
|
||||
val COOKIE_NAMES = listOf("cf_clearance")
|
||||
private val COOKIE_NAMES = listOf("cf_clearance")
|
||||
private val CHROME_IMAGE_TEMPLATE_REGEX = Regex("""<title>(.*?) \(\d+×\d+\)</title>""")
|
||||
}
|
||||
}
|
||||
@@ -205,12 +205,9 @@ object CFClearance {
|
||||
session = serverConfig.flareSolverrSessionName.value,
|
||||
sessionTtlMinutes = serverConfig.flareSolverrSessionTtl.value,
|
||||
cookies =
|
||||
network.cookieStore
|
||||
.get(originalRequest.url)
|
||||
.filter { it.name !in CloudflareInterceptor.COOKIE_NAMES }
|
||||
.map { cookie ->
|
||||
FlareSolverCookie(cookie.name, cookie.value)
|
||||
},
|
||||
network.cookieStore.get(originalRequest.url).map {
|
||||
FlareSolverCookie(it.name, it.value)
|
||||
},
|
||||
returnOnlyCookies = onlyCookies,
|
||||
maxTimeout = timeout.inWholeMilliseconds.toInt(),
|
||||
postData =
|
||||
|
||||
@@ -36,11 +36,10 @@ import nl.adaptivity.xmlutil.ExperimentalXmlUtilApi
|
||||
import nl.adaptivity.xmlutil.core.KtXmlReader
|
||||
import nl.adaptivity.xmlutil.serialization.XML
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.insert
|
||||
import org.jetbrains.exposed.v1.jdbc.insertAndGetId
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.insertAndGetId
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.registerCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package suwayomi.tachidesk.global.impl
|
||||
|
||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.core.statements.BatchUpdateStatement
|
||||
import org.jetbrains.exposed.v1.jdbc.batchInsert
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
import org.jetbrains.exposed.sql.batchInsert
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.global.model.table.GlobalMetaTable
|
||||
|
||||
/*
|
||||
@@ -34,14 +32,13 @@ object GlobalMeta {
|
||||
val (existingMeta, newMeta) = meta.toList().partition { (key) -> key in dbMetaMap.keys }
|
||||
|
||||
if (existingMeta.isNotEmpty()) {
|
||||
BatchUpdateStatement(GlobalMetaTable)
|
||||
.apply {
|
||||
existingMeta.forEach { (key, value) ->
|
||||
addBatch(EntityID(dbMetaMap[key]!![GlobalMetaTable.id].value, GlobalMetaTable))
|
||||
this[GlobalMetaTable.value] = value
|
||||
}
|
||||
}.toExecutable()
|
||||
.execute(this@transaction)
|
||||
BatchUpdateStatement(GlobalMetaTable).apply {
|
||||
existingMeta.forEach { (key, value) ->
|
||||
addBatch(EntityID(dbMetaMap[key]!![GlobalMetaTable.id].value, GlobalMetaTable))
|
||||
this[GlobalMetaTable.value] = value
|
||||
}
|
||||
execute(this@transaction)
|
||||
}
|
||||
}
|
||||
|
||||
if (newMeta.isNotEmpty()) {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
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 io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.HttpUrl
|
||||
import org.cef.CefClient
|
||||
import org.cef.CefSettings
|
||||
import org.cef.browser.CefBrowser
|
||||
import org.cef.browser.CefFrame
|
||||
@@ -25,9 +26,6 @@ import org.cef.network.CefCookie
|
||||
import org.cef.network.CefCookieManager
|
||||
import org.cef.network.CefRequest
|
||||
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.HeadlessException
|
||||
import java.awt.Rectangle
|
||||
@@ -49,8 +47,8 @@ import javax.swing.JPanel
|
||||
class KcefWebView {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
private val renderHandler = RenderHandler()
|
||||
private var kcefClient: CefClient? = null
|
||||
private var browser: CefBrowser? = null
|
||||
private var kcefClient: KCEFClient? = null
|
||||
private var browser: KCEFBrowser? = null
|
||||
private var width = 1000
|
||||
private var height = 1000
|
||||
|
||||
@@ -78,8 +76,7 @@ class KcefWebView {
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class Event
|
||||
@Serializable sealed class Event
|
||||
|
||||
@Serializable
|
||||
@SerialName("consoleMessage")
|
||||
@@ -250,12 +247,10 @@ class KcefWebView {
|
||||
init {
|
||||
destroy()
|
||||
kcefClient =
|
||||
runBlocking {
|
||||
CefHelper.createClient().apply {
|
||||
addDisplayHandler(DisplayHandler())
|
||||
addLoadHandler(LoadHandler())
|
||||
addRequestHandler(RequestHandler())
|
||||
}
|
||||
KCEF.newClientBlocking().apply {
|
||||
addDisplayHandler(DisplayHandler())
|
||||
addLoadHandler(LoadHandler())
|
||||
addRequestHandler(RequestHandler())
|
||||
}
|
||||
|
||||
logger.debug { "Start loading cookies" }
|
||||
@@ -294,7 +289,6 @@ class KcefWebView {
|
||||
.createBrowser(
|
||||
url,
|
||||
CefRendering.CefRenderingWithHandler(renderHandler, JPanel()),
|
||||
false,
|
||||
// NOTE: with a context, we don't seem to be getting any cookies
|
||||
).apply {
|
||||
// NOTE: Without this, we don't seem to be receiving any events
|
||||
|
||||
@@ -7,12 +7,12 @@ package suwayomi.tachidesk.global.model.table
|
||||
* 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 org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||
|
||||
/**
|
||||
* Metadata storage for clients, server/global level.
|
||||
*/
|
||||
object GlobalMetaTable : IntIdTable() {
|
||||
val key = varchar("meta_key", 256)
|
||||
val key = varchar("key", 256)
|
||||
val value = varchar("value", 4096)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package suwayomi.tachidesk.graphql
|
||||
|
||||
import com.expediagroup.graphql.server.extensions.toGraphQLError
|
||||
import graphql.execution.DataFetcherResult
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
|
||||
val logger = KotlinLogging.logger { }
|
||||
|
||||
inline fun <T> asDataFetcherResult(block: () -> T): DataFetcherResult<T?> {
|
||||
val result =
|
||||
runCatching {
|
||||
block()
|
||||
}
|
||||
|
||||
if (result.isFailure) {
|
||||
logger.error(result.exceptionOrNull()) { "asDataFetcherResult: failed due to" }
|
||||
return DataFetcherResult
|
||||
.newResult<T?>()
|
||||
.error(result.exceptionOrNull()?.toGraphQLError())
|
||||
.build()
|
||||
}
|
||||
|
||||
return DataFetcherResult
|
||||
.newResult<T?>()
|
||||
.data(result.getOrNull())
|
||||
.build()
|
||||
}
|
||||
@@ -3,8 +3,12 @@ package suwayomi.tachidesk.graphql.cache
|
||||
import org.dataloader.CacheMap
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class CustomCacheMap<K : Any, V : Any> : CacheMap<K, V> {
|
||||
private val cache: MutableMap<K, CompletableFuture<V>> = HashMap()
|
||||
class CustomCacheMap<K, V> : CacheMap<K, V> {
|
||||
private val cache: MutableMap<K, CompletableFuture<V>>
|
||||
|
||||
init {
|
||||
cache = HashMap()
|
||||
}
|
||||
|
||||
override fun containsKey(key: K): Boolean = cache.containsKey(key)
|
||||
|
||||
@@ -14,12 +18,12 @@ class CustomCacheMap<K : Any, V : Any> : CacheMap<K, V> {
|
||||
|
||||
override fun getAll(): Collection<CompletableFuture<V>> = cache.values
|
||||
|
||||
override fun putIfAbsentAtomically(
|
||||
override fun set(
|
||||
key: K,
|
||||
value: CompletableFuture<V>,
|
||||
): CompletableFuture<V> {
|
||||
): CacheMap<K, V> {
|
||||
cache[key] = value
|
||||
return value
|
||||
return this
|
||||
}
|
||||
|
||||
override fun delete(key: K): CacheMap<K, V> {
|
||||
@@ -31,6 +35,4 @@ class CustomCacheMap<K : Any, V : Any> : CacheMap<K, V> {
|
||||
cache.clear()
|
||||
return this
|
||||
}
|
||||
|
||||
override fun size(): Int = cache.size
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ import com.expediagroup.graphql.dataloader.KotlinDataLoader
|
||||
import graphql.GraphQLContext
|
||||
import org.dataloader.DataLoader
|
||||
import org.dataloader.DataLoaderFactory
|
||||
import org.jetbrains.exposed.v1.core.Slf4jSqlDebugLogger
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger
|
||||
import org.jetbrains.exposed.sql.addLogger
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.types.CategoryNodeList
|
||||
import suwayomi.tachidesk.graphql.types.CategoryNodeList.Companion.toNodeList
|
||||
import suwayomi.tachidesk.graphql.types.CategoryType
|
||||
|
||||
@@ -11,28 +11,24 @@ import com.expediagroup.graphql.dataloader.KotlinDataLoader
|
||||
import graphql.GraphQLContext
|
||||
import org.dataloader.DataLoader
|
||||
import org.dataloader.DataLoaderFactory
|
||||
import org.jetbrains.exposed.v1.core.Slf4jSqlDebugLogger
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.count
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.core.greater
|
||||
import org.jetbrains.exposed.v1.core.greaterEq
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
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.sql.Slf4jSqlDebugLogger
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.addLogger
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.count
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.types.ChapterNodeList
|
||||
import suwayomi.tachidesk.graphql.types.ChapterNodeList.Companion.toNodeList
|
||||
import suwayomi.tachidesk.graphql.types.ChapterType
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||
|
||||
class ChapterDataLoader : KotlinDataLoader<Int, ChapterType> {
|
||||
class ChapterDataLoader : KotlinDataLoader<Int, ChapterType?> {
|
||||
override val dataLoaderName = "ChapterDataLoader"
|
||||
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType> =
|
||||
DataLoaderFactory.newDataLoader { ids ->
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType?> =
|
||||
DataLoaderFactory.newDataLoader<Int, ChapterType> { ids ->
|
||||
future {
|
||||
transaction {
|
||||
addLogger(Slf4jSqlDebugLogger)
|
||||
@@ -52,7 +48,7 @@ class ChaptersForMangaDataLoader : KotlinDataLoader<Int, ChapterNodeList> {
|
||||
override val dataLoaderName = "ChaptersForMangaDataLoader"
|
||||
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterNodeList> =
|
||||
DataLoaderFactory.newDataLoader { ids ->
|
||||
DataLoaderFactory.newDataLoader<Int, ChapterNodeList> { ids ->
|
||||
future {
|
||||
transaction {
|
||||
addLogger(Slf4jSqlDebugLogger)
|
||||
@@ -72,7 +68,7 @@ class DownloadedChapterCountForMangaDataLoader : KotlinDataLoader<Int, Int> {
|
||||
override val dataLoaderName = "DownloadedChapterCountForMangaDataLoader"
|
||||
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, Int> =
|
||||
DataLoaderFactory.newDataLoader { ids ->
|
||||
DataLoaderFactory.newDataLoader<Int, Int> { ids ->
|
||||
future {
|
||||
transaction {
|
||||
addLogger(Slf4jSqlDebugLogger)
|
||||
@@ -94,7 +90,7 @@ class UnreadChapterCountForMangaDataLoader : KotlinDataLoader<Int, Int> {
|
||||
override val dataLoaderName = "UnreadChapterCountForMangaDataLoader"
|
||||
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, Int> =
|
||||
DataLoaderFactory.newDataLoader { ids ->
|
||||
DataLoaderFactory.newDataLoader<Int, Int> { ids ->
|
||||
future {
|
||||
transaction {
|
||||
addLogger(Slf4jSqlDebugLogger)
|
||||
@@ -116,7 +112,7 @@ class BookmarkedChapterCountForMangaDataLoader : KotlinDataLoader<Int, Int> {
|
||||
override val dataLoaderName = "BookmarkedChapterCountForMangaDataLoader"
|
||||
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, Int> =
|
||||
DataLoaderFactory.newDataLoader { ids ->
|
||||
DataLoaderFactory.newDataLoader<Int, Int> { ids ->
|
||||
future {
|
||||
transaction {
|
||||
addLogger(Slf4jSqlDebugLogger)
|
||||
@@ -161,11 +157,11 @@ class HasDuplicateChaptersForMangaDataLoader : KotlinDataLoader<Int, Boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
class LastReadChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType> {
|
||||
class LastReadChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType?> {
|
||||
override val dataLoaderName = "LastReadChapterForMangaDataLoader"
|
||||
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType> =
|
||||
DataLoaderFactory.newDataLoader { ids ->
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType?> =
|
||||
DataLoaderFactory.newDataLoader<Int, ChapterType?> { ids ->
|
||||
future {
|
||||
transaction {
|
||||
addLogger(Slf4jSqlDebugLogger)
|
||||
@@ -181,11 +177,11 @@ class LastReadChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType> {
|
||||
}
|
||||
}
|
||||
|
||||
class LatestReadChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType> {
|
||||
class LatestReadChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType?> {
|
||||
override val dataLoaderName = "LatestReadChapterForMangaDataLoader"
|
||||
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType> =
|
||||
DataLoaderFactory.newDataLoader { ids ->
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType?> =
|
||||
DataLoaderFactory.newDataLoader<Int, ChapterType?> { ids ->
|
||||
future {
|
||||
transaction {
|
||||
addLogger(Slf4jSqlDebugLogger)
|
||||
@@ -201,11 +197,11 @@ class LatestReadChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType> {
|
||||
}
|
||||
}
|
||||
|
||||
class LatestFetchedChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType> {
|
||||
class LatestFetchedChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType?> {
|
||||
override val dataLoaderName = "LatestFetchedChapterForMangaDataLoader"
|
||||
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType> =
|
||||
DataLoaderFactory.newDataLoader { ids ->
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType?> =
|
||||
DataLoaderFactory.newDataLoader<Int, ChapterType?> { ids ->
|
||||
future {
|
||||
transaction {
|
||||
addLogger(Slf4jSqlDebugLogger)
|
||||
@@ -221,11 +217,11 @@ class LatestFetchedChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType
|
||||
}
|
||||
}
|
||||
|
||||
class LatestUploadedChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType> {
|
||||
class LatestUploadedChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType?> {
|
||||
override val dataLoaderName = "LatestUploadedChapterForMangaDataLoader"
|
||||
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType> =
|
||||
DataLoaderFactory.newDataLoader { ids ->
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType?> =
|
||||
DataLoaderFactory.newDataLoader<Int, ChapterType?> { ids ->
|
||||
future {
|
||||
transaction {
|
||||
addLogger(Slf4jSqlDebugLogger)
|
||||
@@ -241,11 +237,11 @@ class LatestUploadedChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterTyp
|
||||
}
|
||||
}
|
||||
|
||||
class FirstUnreadChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType> {
|
||||
class FirstUnreadChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType?> {
|
||||
override val dataLoaderName = "FirstUnreadChapterForMangaDataLoader"
|
||||
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType> =
|
||||
DataLoaderFactory.newDataLoader { ids ->
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType?> =
|
||||
DataLoaderFactory.newDataLoader<Int, ChapterType?> { ids ->
|
||||
future {
|
||||
transaction {
|
||||
addLogger(Slf4jSqlDebugLogger)
|
||||
@@ -261,11 +257,11 @@ class FirstUnreadChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType>
|
||||
}
|
||||
}
|
||||
|
||||
class HighestNumberedChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType> {
|
||||
class HighestNumberedChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType?> {
|
||||
override val dataLoaderName = "HighestNumberedChapterForMangaDataLoader"
|
||||
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType> =
|
||||
DataLoaderFactory.newDataLoader { ids ->
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType?> =
|
||||
DataLoaderFactory.newDataLoader<Int, ChapterType?> { ids ->
|
||||
future {
|
||||
transaction {
|
||||
addLogger(Slf4jSqlDebugLogger)
|
||||
|
||||
@@ -11,19 +11,19 @@ import com.expediagroup.graphql.dataloader.KotlinDataLoader
|
||||
import graphql.GraphQLContext
|
||||
import org.dataloader.DataLoader
|
||||
import org.dataloader.DataLoaderFactory
|
||||
import org.jetbrains.exposed.v1.core.Slf4jSqlDebugLogger
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger
|
||||
import org.jetbrains.exposed.sql.addLogger
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.types.ExtensionType
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||
|
||||
class ExtensionDataLoader : KotlinDataLoader<String, ExtensionType> {
|
||||
class ExtensionDataLoader : KotlinDataLoader<String, ExtensionType?> {
|
||||
override val dataLoaderName = "ExtensionDataLoader"
|
||||
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, ExtensionType> =
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, ExtensionType?> =
|
||||
DataLoaderFactory.newDataLoader { ids ->
|
||||
future {
|
||||
transaction {
|
||||
@@ -40,10 +40,10 @@ class ExtensionDataLoader : KotlinDataLoader<String, ExtensionType> {
|
||||
}
|
||||
}
|
||||
|
||||
class ExtensionForSourceDataLoader : KotlinDataLoader<Long, ExtensionType> {
|
||||
class ExtensionForSourceDataLoader : KotlinDataLoader<Long, ExtensionType?> {
|
||||
override val dataLoaderName = "ExtensionForSourceDataLoader"
|
||||
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Long, ExtensionType> =
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Long, ExtensionType?> =
|
||||
DataLoaderFactory.newDataLoader { ids ->
|
||||
future {
|
||||
transaction {
|
||||
|
||||
@@ -12,13 +12,11 @@ import graphql.GraphQLContext
|
||||
import org.dataloader.DataLoader
|
||||
import org.dataloader.DataLoaderFactory
|
||||
import org.dataloader.DataLoaderOptions
|
||||
import org.jetbrains.exposed.v1.core.Slf4jSqlDebugLogger
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.core.isNull
|
||||
import org.jetbrains.exposed.v1.jdbc.andWhere
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger
|
||||
import org.jetbrains.exposed.sql.addLogger
|
||||
import org.jetbrains.exposed.sql.andWhere
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.cache.CustomCacheMap
|
||||
import suwayomi.tachidesk.graphql.types.MangaNodeList
|
||||
import suwayomi.tachidesk.graphql.types.MangaNodeList.Companion.toNodeList
|
||||
@@ -27,10 +25,10 @@ import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||
|
||||
class MangaDataLoader : KotlinDataLoader<Int, MangaType> {
|
||||
class MangaDataLoader : KotlinDataLoader<Int, MangaType?> {
|
||||
override val dataLoaderName = "MangaDataLoader"
|
||||
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, MangaType> =
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, MangaType?> =
|
||||
DataLoaderFactory.newDataLoader { ids ->
|
||||
future {
|
||||
transaction {
|
||||
@@ -124,6 +122,6 @@ class MangaForIdsDataLoader : KotlinDataLoader<List<Int>, MangaNodeList> {
|
||||
}
|
||||
}
|
||||
},
|
||||
DataLoaderOptions.newOptions().setCacheMap(CustomCacheMap<List<Int>, MangaNodeList>()).build(),
|
||||
DataLoaderOptions.newOptions().setCacheMap(CustomCacheMap<List<Int>, MangaNodeList>()),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ import com.expediagroup.graphql.dataloader.KotlinDataLoader
|
||||
import graphql.GraphQLContext
|
||||
import org.dataloader.DataLoader
|
||||
import org.dataloader.DataLoaderFactory
|
||||
import org.jetbrains.exposed.v1.core.Slf4jSqlDebugLogger
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger
|
||||
import org.jetbrains.exposed.sql.addLogger
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.global.model.table.GlobalMetaTable
|
||||
import suwayomi.tachidesk.graphql.types.CategoryMetaType
|
||||
import suwayomi.tachidesk.graphql.types.ChapterMetaType
|
||||
@@ -20,11 +20,11 @@ import suwayomi.tachidesk.manga.model.table.MangaMetaTable
|
||||
import suwayomi.tachidesk.manga.model.table.SourceMetaTable
|
||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||
|
||||
class GlobalMetaDataLoader : KotlinDataLoader<String, GlobalMetaType> {
|
||||
class GlobalMetaDataLoader : KotlinDataLoader<String, GlobalMetaType?> {
|
||||
override val dataLoaderName = "GlobalMetaDataLoader"
|
||||
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, GlobalMetaType> =
|
||||
DataLoaderFactory.newDataLoader<String, GlobalMetaType> { ids ->
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, GlobalMetaType?> =
|
||||
DataLoaderFactory.newDataLoader<String, GlobalMetaType?> { ids ->
|
||||
future {
|
||||
transaction {
|
||||
addLogger(Slf4jSqlDebugLogger)
|
||||
|
||||
@@ -11,10 +11,10 @@ import com.expediagroup.graphql.dataloader.KotlinDataLoader
|
||||
import graphql.GraphQLContext
|
||||
import org.dataloader.DataLoader
|
||||
import org.dataloader.DataLoaderFactory
|
||||
import org.jetbrains.exposed.v1.core.Slf4jSqlDebugLogger
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger
|
||||
import org.jetbrains.exposed.sql.addLogger
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.types.SourceNodeList
|
||||
import suwayomi.tachidesk.graphql.types.SourceNodeList.Companion.toNodeList
|
||||
import suwayomi.tachidesk.graphql.types.SourceType
|
||||
@@ -22,10 +22,10 @@ import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||
|
||||
class SourceDataLoader : KotlinDataLoader<Long, SourceType> {
|
||||
class SourceDataLoader : KotlinDataLoader<Long, SourceType?> {
|
||||
override val dataLoaderName = "SourceDataLoader"
|
||||
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Long, SourceType> =
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Long, SourceType?> =
|
||||
DataLoaderFactory.newDataLoader { ids ->
|
||||
future {
|
||||
transaction {
|
||||
|
||||
@@ -11,10 +11,10 @@ import com.expediagroup.graphql.dataloader.KotlinDataLoader
|
||||
import graphql.GraphQLContext
|
||||
import org.dataloader.DataLoader
|
||||
import org.dataloader.DataLoaderFactory
|
||||
import org.jetbrains.exposed.v1.core.Slf4jSqlDebugLogger
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger
|
||||
import org.jetbrains.exposed.sql.addLogger
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.types.TrackRecordNodeList
|
||||
import suwayomi.tachidesk.graphql.types.TrackRecordNodeList.Companion.toNodeList
|
||||
import suwayomi.tachidesk.graphql.types.TrackRecordType
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import org.jetbrains.exposed.v1.core.LikePattern
|
||||
import org.jetbrains.exposed.v1.core.Op
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.core.greaterEq
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.core.lessEq
|
||||
import org.jetbrains.exposed.v1.core.like
|
||||
import org.jetbrains.exposed.v1.core.minus
|
||||
import org.jetbrains.exposed.v1.core.or
|
||||
import org.jetbrains.exposed.v1.core.plus
|
||||
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
||||
import org.jetbrains.exposed.v1.jdbc.insertAndGetId
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import graphql.execution.DataFetcherResult
|
||||
import org.jetbrains.exposed.sql.LikePattern
|
||||
import org.jetbrains.exposed.sql.Op
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.like
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.minus
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.plus
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.insertAndGetId
|
||||
import org.jetbrains.exposed.sql.or
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.types.CategoryMetaType
|
||||
import suwayomi.tachidesk.graphql.types.CategoryType
|
||||
@@ -44,13 +42,14 @@ class CategoryMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun setCategoryMeta(input: SetCategoryMetaInput): SetCategoryMetaPayload? {
|
||||
val (clientMutationId, meta) = input
|
||||
fun setCategoryMeta(input: SetCategoryMetaInput): DataFetcherResult<SetCategoryMetaPayload?> =
|
||||
asDataFetcherResult {
|
||||
val (clientMutationId, meta) = input
|
||||
|
||||
Category.modifyMeta(meta.categoryId, meta.key, meta.value)
|
||||
Category.modifyMeta(meta.categoryId, meta.key, meta.value)
|
||||
|
||||
return SetCategoryMetaPayload(clientMutationId, meta)
|
||||
}
|
||||
SetCategoryMetaPayload(clientMutationId, meta)
|
||||
}
|
||||
|
||||
data class DeleteCategoryMetaInput(
|
||||
val clientMutationId: String? = null,
|
||||
@@ -65,33 +64,34 @@ class CategoryMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun deleteCategoryMeta(input: DeleteCategoryMetaInput): DeleteCategoryMetaPayload? {
|
||||
val (clientMutationId, categoryId, key) = input
|
||||
fun deleteCategoryMeta(input: DeleteCategoryMetaInput): DataFetcherResult<DeleteCategoryMetaPayload?> =
|
||||
asDataFetcherResult {
|
||||
val (clientMutationId, categoryId, key) = input
|
||||
|
||||
val (meta, category) =
|
||||
transaction {
|
||||
val meta =
|
||||
CategoryMetaTable
|
||||
.selectAll()
|
||||
.where { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }
|
||||
.firstOrNull()
|
||||
val (meta, category) =
|
||||
transaction {
|
||||
val meta =
|
||||
CategoryMetaTable
|
||||
.selectAll()
|
||||
.where { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }
|
||||
.firstOrNull()
|
||||
|
||||
CategoryMetaTable.deleteWhere { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }
|
||||
CategoryMetaTable.deleteWhere { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }
|
||||
|
||||
val category =
|
||||
transaction {
|
||||
CategoryType(CategoryTable.selectAll().where { CategoryTable.id eq categoryId }.first())
|
||||
}
|
||||
val category =
|
||||
transaction {
|
||||
CategoryType(CategoryTable.selectAll().where { CategoryTable.id eq categoryId }.first())
|
||||
}
|
||||
|
||||
if (meta != null) {
|
||||
CategoryMetaType(meta)
|
||||
} else {
|
||||
null
|
||||
} to category
|
||||
}
|
||||
if (meta != null) {
|
||||
CategoryMetaType(meta)
|
||||
} else {
|
||||
null
|
||||
} to category
|
||||
}
|
||||
|
||||
return DeleteCategoryMetaPayload(clientMutationId, meta, category)
|
||||
}
|
||||
DeleteCategoryMetaPayload(clientMutationId, meta, category)
|
||||
}
|
||||
|
||||
data class SetCategoryMetasItem(
|
||||
val categoryIds: List<Int>,
|
||||
@@ -110,42 +110,43 @@ class CategoryMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun setCategoryMetas(input: SetCategoryMetasInput): SetCategoryMetasPayload? {
|
||||
val (clientMutationId, items) = input
|
||||
fun setCategoryMetas(input: SetCategoryMetasInput): DataFetcherResult<SetCategoryMetasPayload?> =
|
||||
asDataFetcherResult {
|
||||
val (clientMutationId, items) = input
|
||||
|
||||
val metaByCategoryId =
|
||||
items
|
||||
.flatMap { item ->
|
||||
val metaMap = item.metas.associate { it.key to it.value }
|
||||
item.categoryIds.map { categoryId -> categoryId to metaMap }
|
||||
}.groupBy({ it.first }, { it.second })
|
||||
.mapValues { (_, maps) -> maps.reduce { acc, map -> acc + map } }
|
||||
val metaByCategoryId =
|
||||
items
|
||||
.flatMap { item ->
|
||||
val metaMap = item.metas.associate { it.key to it.value }
|
||||
item.categoryIds.map { categoryId -> categoryId to metaMap }
|
||||
}.groupBy({ it.first }, { it.second })
|
||||
.mapValues { (_, maps) -> maps.reduce { acc, map -> acc + map } }
|
||||
|
||||
Category.modifyCategoriesMetas(metaByCategoryId)
|
||||
Category.modifyCategoriesMetas(metaByCategoryId)
|
||||
|
||||
val allCategoryIds = metaByCategoryId.keys
|
||||
val allMetaKeys = metaByCategoryId.values.flatMap { item -> item.keys }.distinct()
|
||||
val allCategoryIds = metaByCategoryId.keys
|
||||
val allMetaKeys = metaByCategoryId.values.flatMap { item -> item.keys }.distinct()
|
||||
|
||||
val (updatedMetas, categories) =
|
||||
transaction {
|
||||
val updatedMetas =
|
||||
CategoryMetaTable
|
||||
.selectAll()
|
||||
.where { (CategoryMetaTable.ref inList allCategoryIds) and (CategoryMetaTable.key inList allMetaKeys) }
|
||||
.map { CategoryMetaType(it) }
|
||||
val (updatedMetas, categories) =
|
||||
transaction {
|
||||
val updatedMetas =
|
||||
CategoryMetaTable
|
||||
.selectAll()
|
||||
.where { (CategoryMetaTable.ref inList allCategoryIds) and (CategoryMetaTable.key inList allMetaKeys) }
|
||||
.map { CategoryMetaType(it) }
|
||||
|
||||
val categories =
|
||||
CategoryTable
|
||||
.selectAll()
|
||||
.where { CategoryTable.id inList allCategoryIds }
|
||||
.map { CategoryType(it) }
|
||||
.distinctBy { it.id }
|
||||
val categories =
|
||||
CategoryTable
|
||||
.selectAll()
|
||||
.where { CategoryTable.id inList allCategoryIds }
|
||||
.map { CategoryType(it) }
|
||||
.distinctBy { it.id }
|
||||
|
||||
updatedMetas to categories
|
||||
}
|
||||
updatedMetas to categories
|
||||
}
|
||||
|
||||
return SetCategoryMetasPayload(clientMutationId, updatedMetas, categories)
|
||||
}
|
||||
SetCategoryMetasPayload(clientMutationId, updatedMetas, categories)
|
||||
}
|
||||
|
||||
data class DeleteCategoryMetasItem(
|
||||
val categoryIds: List<Int>,
|
||||
@@ -165,63 +166,64 @@ class CategoryMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun deleteCategoryMetas(input: DeleteCategoryMetasInput): DeleteCategoryMetasPayload? {
|
||||
val (clientMutationId, items) = input
|
||||
fun deleteCategoryMetas(input: DeleteCategoryMetasInput): DataFetcherResult<DeleteCategoryMetasPayload?> =
|
||||
asDataFetcherResult {
|
||||
val (clientMutationId, items) = input
|
||||
|
||||
items.forEach { item ->
|
||||
require(!item.keys.isNullOrEmpty() || !item.prefixes.isNullOrEmpty()) {
|
||||
"Either 'keys' or 'prefixes' must be provided for each item"
|
||||
items.forEach { item ->
|
||||
require(!item.keys.isNullOrEmpty() || !item.prefixes.isNullOrEmpty()) {
|
||||
"Either 'keys' or 'prefixes' must be provided for each item"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val (allDeletedMetas, allCategoryIds) =
|
||||
transaction {
|
||||
val deletedMetas = mutableListOf<CategoryMetaType>()
|
||||
val categoryIds = mutableSetOf<Int>()
|
||||
val (allDeletedMetas, allCategoryIds) =
|
||||
transaction {
|
||||
val deletedMetas = mutableListOf<CategoryMetaType>()
|
||||
val categoryIds = mutableSetOf<Int>()
|
||||
|
||||
items.forEach { item ->
|
||||
val keyCondition: Op<Boolean>? =
|
||||
item.keys?.takeIf { it.isNotEmpty() }?.let { CategoryMetaTable.key inList it }
|
||||
items.forEach { item ->
|
||||
val keyCondition: Op<Boolean>? =
|
||||
item.keys?.takeIf { it.isNotEmpty() }?.let { CategoryMetaTable.key inList it }
|
||||
|
||||
val prefixCondition: Op<Boolean>? =
|
||||
item.prefixes
|
||||
?.filter { it.isNotEmpty() }
|
||||
?.map { (CategoryMetaTable.key like LikePattern("$it%")) as Op<Boolean> }
|
||||
?.reduceOrNull { acc, op -> acc or op }
|
||||
val prefixCondition: Op<Boolean>? =
|
||||
item.prefixes
|
||||
?.filter { it.isNotEmpty() }
|
||||
?.map { (CategoryMetaTable.key like LikePattern("$it%")) as Op<Boolean> }
|
||||
?.reduceOrNull { acc, op -> acc or op }
|
||||
|
||||
val metaKeyCondition =
|
||||
if (keyCondition != null && prefixCondition != null) {
|
||||
keyCondition or prefixCondition
|
||||
} else {
|
||||
keyCondition ?: prefixCondition!!
|
||||
}
|
||||
val metaKeyCondition =
|
||||
if (keyCondition != null && prefixCondition != null) {
|
||||
keyCondition or prefixCondition
|
||||
} else {
|
||||
keyCondition ?: prefixCondition!!
|
||||
}
|
||||
|
||||
val condition = (CategoryMetaTable.ref inList item.categoryIds) and metaKeyCondition
|
||||
val condition = (CategoryMetaTable.ref inList item.categoryIds) and metaKeyCondition
|
||||
|
||||
deletedMetas +=
|
||||
CategoryMetaTable
|
||||
.selectAll()
|
||||
.where { condition }
|
||||
.map { CategoryMetaType(it) }
|
||||
deletedMetas +=
|
||||
CategoryMetaTable
|
||||
.selectAll()
|
||||
.where { condition }
|
||||
.map { CategoryMetaType(it) }
|
||||
|
||||
CategoryMetaTable.deleteWhere { condition }
|
||||
categoryIds += item.categoryIds
|
||||
CategoryMetaTable.deleteWhere { condition }
|
||||
categoryIds += item.categoryIds
|
||||
}
|
||||
|
||||
deletedMetas to categoryIds
|
||||
}
|
||||
|
||||
deletedMetas to categoryIds
|
||||
}
|
||||
val categories =
|
||||
transaction {
|
||||
CategoryTable
|
||||
.selectAll()
|
||||
.where { CategoryTable.id inList allCategoryIds }
|
||||
.map { CategoryType(it) }
|
||||
.distinctBy { it.id }
|
||||
}
|
||||
|
||||
val categories =
|
||||
transaction {
|
||||
CategoryTable
|
||||
.selectAll()
|
||||
.where { CategoryTable.id inList allCategoryIds }
|
||||
.map { CategoryType(it) }
|
||||
.distinctBy { it.id }
|
||||
}
|
||||
|
||||
return DeleteCategoryMetasPayload(clientMutationId, allDeletedMetas, categories)
|
||||
}
|
||||
DeleteCategoryMetasPayload(clientMutationId, allDeletedMetas, categories)
|
||||
}
|
||||
|
||||
data class UpdateCategoryPatch(
|
||||
val name: String? = null,
|
||||
@@ -289,38 +291,40 @@ class CategoryMutation {
|
||||
}
|
||||
|
||||
@RequireAuth
|
||||
fun updateCategory(input: UpdateCategoryInput): UpdateCategoryPayload? {
|
||||
val (clientMutationId, id, patch) = input
|
||||
fun updateCategory(input: UpdateCategoryInput): DataFetcherResult<UpdateCategoryPayload?> =
|
||||
asDataFetcherResult {
|
||||
val (clientMutationId, id, patch) = input
|
||||
|
||||
updateCategories(listOf(id), patch)
|
||||
updateCategories(listOf(id), patch)
|
||||
|
||||
val category =
|
||||
transaction {
|
||||
CategoryType(CategoryTable.selectAll().where { CategoryTable.id eq id }.first())
|
||||
}
|
||||
val category =
|
||||
transaction {
|
||||
CategoryType(CategoryTable.selectAll().where { CategoryTable.id eq id }.first())
|
||||
}
|
||||
|
||||
return UpdateCategoryPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
category = category,
|
||||
)
|
||||
}
|
||||
UpdateCategoryPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
category = category,
|
||||
)
|
||||
}
|
||||
|
||||
@RequireAuth
|
||||
fun updateCategories(input: UpdateCategoriesInput): UpdateCategoriesPayload? {
|
||||
val (clientMutationId, ids, patch) = input
|
||||
fun updateCategories(input: UpdateCategoriesInput): DataFetcherResult<UpdateCategoriesPayload?> =
|
||||
asDataFetcherResult {
|
||||
val (clientMutationId, ids, patch) = input
|
||||
|
||||
updateCategories(ids, patch)
|
||||
updateCategories(ids, patch)
|
||||
|
||||
val categories =
|
||||
transaction {
|
||||
CategoryTable.selectAll().where { CategoryTable.id inList ids }.map { CategoryType(it) }
|
||||
}
|
||||
val categories =
|
||||
transaction {
|
||||
CategoryTable.selectAll().where { CategoryTable.id inList ids }.map { CategoryType(it) }
|
||||
}
|
||||
|
||||
return UpdateCategoriesPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
categories = categories,
|
||||
)
|
||||
}
|
||||
UpdateCategoriesPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
categories = categories,
|
||||
)
|
||||
}
|
||||
|
||||
data class UpdateCategoryOrderPayload(
|
||||
val clientMutationId: String?,
|
||||
@@ -334,48 +338,49 @@ class CategoryMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun updateCategoryOrder(input: UpdateCategoryOrderInput): UpdateCategoryOrderPayload? {
|
||||
val (clientMutationId, categoryId, position) = input
|
||||
require(position > 0) {
|
||||
"'order' must not be <= 0"
|
||||
}
|
||||
|
||||
transaction {
|
||||
val currentOrder =
|
||||
CategoryTable
|
||||
.selectAll()
|
||||
.where { CategoryTable.id eq categoryId }
|
||||
.first()[CategoryTable.order]
|
||||
|
||||
if (currentOrder != position) {
|
||||
if (position < currentOrder) {
|
||||
CategoryTable.update({ CategoryTable.order greaterEq position }) {
|
||||
it[CategoryTable.order] = CategoryTable.order + 1
|
||||
}
|
||||
} else {
|
||||
CategoryTable.update({ CategoryTable.order lessEq position }) {
|
||||
it[CategoryTable.order] = CategoryTable.order - 1
|
||||
}
|
||||
}
|
||||
|
||||
CategoryTable.update({ CategoryTable.id eq categoryId }) {
|
||||
it[CategoryTable.order] = position
|
||||
}
|
||||
fun updateCategoryOrder(input: UpdateCategoryOrderInput): DataFetcherResult<UpdateCategoryOrderPayload?> =
|
||||
asDataFetcherResult {
|
||||
val (clientMutationId, categoryId, position) = input
|
||||
require(position > 0) {
|
||||
"'order' must not be <= 0"
|
||||
}
|
||||
}
|
||||
|
||||
Category.normalizeCategories()
|
||||
|
||||
val categories =
|
||||
transaction {
|
||||
CategoryTable.selectAll().orderBy(CategoryTable.order).map { CategoryType(it) }
|
||||
val currentOrder =
|
||||
CategoryTable
|
||||
.selectAll()
|
||||
.where { CategoryTable.id eq categoryId }
|
||||
.first()[CategoryTable.order]
|
||||
|
||||
if (currentOrder != position) {
|
||||
if (position < currentOrder) {
|
||||
CategoryTable.update({ CategoryTable.order greaterEq position }) {
|
||||
it[CategoryTable.order] = CategoryTable.order + 1
|
||||
}
|
||||
} else {
|
||||
CategoryTable.update({ CategoryTable.order lessEq position }) {
|
||||
it[CategoryTable.order] = CategoryTable.order - 1
|
||||
}
|
||||
}
|
||||
|
||||
CategoryTable.update({ CategoryTable.id eq categoryId }) {
|
||||
it[CategoryTable.order] = position
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return UpdateCategoryOrderPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
categories = categories,
|
||||
)
|
||||
}
|
||||
Category.normalizeCategories()
|
||||
|
||||
val categories =
|
||||
transaction {
|
||||
CategoryTable.selectAll().orderBy(CategoryTable.order).map { CategoryType(it) }
|
||||
}
|
||||
|
||||
UpdateCategoryOrderPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
categories = categories,
|
||||
)
|
||||
}
|
||||
|
||||
data class CreateCategoryInput(
|
||||
val clientMutationId: String? = null,
|
||||
@@ -392,52 +397,53 @@ class CategoryMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun createCategory(input: CreateCategoryInput): CreateCategoryPayload? {
|
||||
val (clientMutationId, name, order, default, includeInUpdate, includeInDownload) = input
|
||||
transaction {
|
||||
require(CategoryTable.selectAll().where { CategoryTable.name eq input.name }.isEmpty()) {
|
||||
"'name' must be unique"
|
||||
}
|
||||
}
|
||||
require(!name.equals(Category.DEFAULT_CATEGORY_NAME, ignoreCase = true)) {
|
||||
"'name' must not be ${Category.DEFAULT_CATEGORY_NAME}"
|
||||
}
|
||||
if (order != null) {
|
||||
require(order > 0) {
|
||||
"'order' must not be <= 0"
|
||||
}
|
||||
}
|
||||
|
||||
val category =
|
||||
fun createCategory(input: CreateCategoryInput): DataFetcherResult<CreateCategoryPayload?> =
|
||||
asDataFetcherResult {
|
||||
val (clientMutationId, name, order, default, includeInUpdate, includeInDownload) = input
|
||||
transaction {
|
||||
if (order != null) {
|
||||
CategoryTable.update({ CategoryTable.order greaterEq order }) {
|
||||
it[CategoryTable.order] = CategoryTable.order + 1
|
||||
require(CategoryTable.selectAll().where { CategoryTable.name eq input.name }.isEmpty()) {
|
||||
"'name' must be unique"
|
||||
}
|
||||
}
|
||||
require(!name.equals(Category.DEFAULT_CATEGORY_NAME, ignoreCase = true)) {
|
||||
"'name' must not be ${Category.DEFAULT_CATEGORY_NAME}"
|
||||
}
|
||||
if (order != null) {
|
||||
require(order > 0) {
|
||||
"'order' must not be <= 0"
|
||||
}
|
||||
}
|
||||
|
||||
val category =
|
||||
transaction {
|
||||
if (order != null) {
|
||||
CategoryTable.update({ CategoryTable.order greaterEq order }) {
|
||||
it[CategoryTable.order] = CategoryTable.order + 1
|
||||
}
|
||||
}
|
||||
|
||||
val id =
|
||||
CategoryTable.insertAndGetId {
|
||||
it[CategoryTable.name] = input.name
|
||||
it[CategoryTable.order] = order ?: Int.MAX_VALUE
|
||||
if (default != null) {
|
||||
it[CategoryTable.isDefault] = default
|
||||
}
|
||||
if (includeInUpdate != null) {
|
||||
it[CategoryTable.includeInUpdate] = includeInUpdate.value
|
||||
}
|
||||
if (includeInDownload != null) {
|
||||
it[CategoryTable.includeInDownload] = includeInDownload.value
|
||||
}
|
||||
}
|
||||
|
||||
Category.normalizeCategories()
|
||||
|
||||
CategoryType(CategoryTable.selectAll().where { CategoryTable.id eq id }.first())
|
||||
}
|
||||
|
||||
val id =
|
||||
CategoryTable.insertAndGetId {
|
||||
it[CategoryTable.name] = input.name
|
||||
it[CategoryTable.order] = order ?: Int.MAX_VALUE
|
||||
if (default != null) {
|
||||
it[CategoryTable.isDefault] = default
|
||||
}
|
||||
if (includeInUpdate != null) {
|
||||
it[CategoryTable.includeInUpdate] = includeInUpdate.value
|
||||
}
|
||||
if (includeInDownload != null) {
|
||||
it[CategoryTable.includeInDownload] = includeInDownload.value
|
||||
}
|
||||
}
|
||||
|
||||
Category.normalizeCategories()
|
||||
|
||||
CategoryType(CategoryTable.selectAll().where { CategoryTable.id eq id }.first())
|
||||
}
|
||||
|
||||
return CreateCategoryPayload(clientMutationId, category)
|
||||
}
|
||||
CreateCategoryPayload(clientMutationId, category)
|
||||
}
|
||||
|
||||
data class DeleteCategoryInput(
|
||||
val clientMutationId: String? = null,
|
||||
@@ -451,45 +457,47 @@ class CategoryMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun deleteCategory(input: DeleteCategoryInput): DeleteCategoryPayload? {
|
||||
val (clientMutationId, categoryId) = input
|
||||
if (categoryId == 0) { // Don't delete default category
|
||||
return DeleteCategoryPayload(
|
||||
clientMutationId,
|
||||
null,
|
||||
emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
val (category, mangas) =
|
||||
transaction {
|
||||
val category =
|
||||
CategoryTable
|
||||
.selectAll()
|
||||
.where { CategoryTable.id eq categoryId }
|
||||
.firstOrNull()
|
||||
|
||||
val mangas =
|
||||
transaction {
|
||||
MangaTable
|
||||
.innerJoin(CategoryMangaTable)
|
||||
.selectAll()
|
||||
.where { CategoryMangaTable.category eq categoryId }
|
||||
.map { MangaType(it) }
|
||||
}
|
||||
|
||||
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
|
||||
|
||||
Category.normalizeCategories()
|
||||
|
||||
if (category != null) {
|
||||
CategoryType(category)
|
||||
} else {
|
||||
null
|
||||
} to mangas
|
||||
fun deleteCategory(input: DeleteCategoryInput): DataFetcherResult<DeleteCategoryPayload?> {
|
||||
return asDataFetcherResult {
|
||||
val (clientMutationId, categoryId) = input
|
||||
if (categoryId == 0) { // Don't delete default category
|
||||
return@asDataFetcherResult DeleteCategoryPayload(
|
||||
clientMutationId,
|
||||
null,
|
||||
emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
return DeleteCategoryPayload(clientMutationId, category, mangas)
|
||||
val (category, mangas) =
|
||||
transaction {
|
||||
val category =
|
||||
CategoryTable
|
||||
.selectAll()
|
||||
.where { CategoryTable.id eq categoryId }
|
||||
.firstOrNull()
|
||||
|
||||
val mangas =
|
||||
transaction {
|
||||
MangaTable
|
||||
.innerJoin(CategoryMangaTable)
|
||||
.selectAll()
|
||||
.where { CategoryMangaTable.category eq categoryId }
|
||||
.map { MangaType(it) }
|
||||
}
|
||||
|
||||
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
|
||||
|
||||
Category.normalizeCategories()
|
||||
|
||||
if (category != null) {
|
||||
CategoryType(category)
|
||||
} else {
|
||||
null
|
||||
} to mangas
|
||||
}
|
||||
|
||||
DeleteCategoryPayload(clientMutationId, category, mangas)
|
||||
}
|
||||
}
|
||||
|
||||
data class UpdateMangaCategoriesPatch(
|
||||
@@ -539,36 +547,38 @@ class CategoryMutation {
|
||||
}
|
||||
|
||||
@RequireAuth
|
||||
fun updateMangaCategories(input: UpdateMangaCategoriesInput): UpdateMangaCategoriesPayload? {
|
||||
val (clientMutationId, id, patch) = input
|
||||
fun updateMangaCategories(input: UpdateMangaCategoriesInput): DataFetcherResult<UpdateMangaCategoriesPayload?> =
|
||||
asDataFetcherResult {
|
||||
val (clientMutationId, id, patch) = input
|
||||
|
||||
updateMangas(listOf(id), patch)
|
||||
updateMangas(listOf(id), patch)
|
||||
|
||||
val manga =
|
||||
transaction {
|
||||
MangaType(MangaTable.selectAll().where { MangaTable.id eq id }.first())
|
||||
}
|
||||
val manga =
|
||||
transaction {
|
||||
MangaType(MangaTable.selectAll().where { MangaTable.id eq id }.first())
|
||||
}
|
||||
|
||||
return UpdateMangaCategoriesPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
manga = manga,
|
||||
)
|
||||
}
|
||||
UpdateMangaCategoriesPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
manga = manga,
|
||||
)
|
||||
}
|
||||
|
||||
@RequireAuth
|
||||
fun updateMangasCategories(input: UpdateMangasCategoriesInput): UpdateMangasCategoriesPayload? {
|
||||
val (clientMutationId, ids, patch) = input
|
||||
fun updateMangasCategories(input: UpdateMangasCategoriesInput): DataFetcherResult<UpdateMangasCategoriesPayload?> =
|
||||
asDataFetcherResult {
|
||||
val (clientMutationId, ids, patch) = input
|
||||
|
||||
updateMangas(ids, patch)
|
||||
updateMangas(ids, patch)
|
||||
|
||||
val mangas =
|
||||
transaction {
|
||||
MangaTable.selectAll().where { MangaTable.id inList ids }.map { MangaType(it) }
|
||||
}
|
||||
val mangas =
|
||||
transaction {
|
||||
MangaTable.selectAll().where { MangaTable.id inList ids }.map { MangaType(it) }
|
||||
}
|
||||
|
||||
return UpdateMangasCategoriesPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
mangas = mangas,
|
||||
)
|
||||
}
|
||||
UpdateMangasCategoriesPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
mangas = mangas,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import graphql.execution.DataFetcherResult
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.exposed.v1.core.LikePattern
|
||||
import org.jetbrains.exposed.v1.core.Op
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.core.like
|
||||
import org.jetbrains.exposed.v1.core.or
|
||||
import org.jetbrains.exposed.v1.core.statements.BatchUpdateStatement
|
||||
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
||||
import org.jetbrains.exposed.v1.jdbc.select
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
import org.jetbrains.exposed.sql.LikePattern
|
||||
import org.jetbrains.exposed.sql.Op
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.like
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.or
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.types.ChapterMetaType
|
||||
import suwayomi.tachidesk.graphql.types.ChapterType
|
||||
@@ -92,23 +90,22 @@ class ChapterMutation {
|
||||
if (patch.isRead != null || patch.isBookmarked != null || patch.lastPageRead != null) {
|
||||
val now = Instant.now().epochSecond
|
||||
|
||||
BatchUpdateStatement(ChapterTable)
|
||||
.apply {
|
||||
ids.forEach { chapterId ->
|
||||
addBatch(EntityID(chapterId, ChapterTable))
|
||||
patch.isRead?.also {
|
||||
this[ChapterTable.isRead] = it
|
||||
}
|
||||
patch.isBookmarked?.also {
|
||||
this[ChapterTable.isBookmarked] = it
|
||||
}
|
||||
patch.lastPageRead?.also {
|
||||
this[ChapterTable.lastPageRead] = it.coerceAtMost(chapterIdToPageCount[chapterId] ?: 0).coerceAtLeast(0)
|
||||
this[ChapterTable.lastReadAt] = now
|
||||
}
|
||||
BatchUpdateStatement(ChapterTable).apply {
|
||||
ids.forEach { chapterId ->
|
||||
addBatch(EntityID(chapterId, ChapterTable))
|
||||
patch.isRead?.also {
|
||||
this[ChapterTable.isRead] = it
|
||||
}
|
||||
}.toExecutable()
|
||||
.execute(this@transaction)
|
||||
patch.isBookmarked?.also {
|
||||
this[ChapterTable.isBookmarked] = it
|
||||
}
|
||||
patch.lastPageRead?.also {
|
||||
this[ChapterTable.lastPageRead] = it.coerceAtMost(chapterIdToPageCount[chapterId] ?: 0).coerceAtLeast(0)
|
||||
this[ChapterTable.lastReadAt] = now
|
||||
}
|
||||
}
|
||||
execute(this@transaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,38 +120,40 @@ class ChapterMutation {
|
||||
}
|
||||
|
||||
@RequireAuth
|
||||
fun updateChapter(input: UpdateChapterInput): UpdateChapterPayload? {
|
||||
val (clientMutationId, id, patch) = input
|
||||
fun updateChapter(input: UpdateChapterInput): DataFetcherResult<UpdateChapterPayload?> =
|
||||
asDataFetcherResult {
|
||||
val (clientMutationId, id, patch) = input
|
||||
|
||||
updateChapters(listOf(id), patch)
|
||||
updateChapters(listOf(id), patch)
|
||||
|
||||
val chapter =
|
||||
transaction {
|
||||
ChapterType(ChapterTable.selectAll().where { ChapterTable.id eq id }.first())
|
||||
}
|
||||
val chapter =
|
||||
transaction {
|
||||
ChapterType(ChapterTable.selectAll().where { ChapterTable.id eq id }.first())
|
||||
}
|
||||
|
||||
return UpdateChapterPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
chapter = chapter,
|
||||
)
|
||||
}
|
||||
UpdateChapterPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
chapter = chapter,
|
||||
)
|
||||
}
|
||||
|
||||
@RequireAuth
|
||||
fun updateChapters(input: UpdateChaptersInput): UpdateChaptersPayload? {
|
||||
val (clientMutationId, ids, patch) = input
|
||||
fun updateChapters(input: UpdateChaptersInput): DataFetcherResult<UpdateChaptersPayload?> =
|
||||
asDataFetcherResult {
|
||||
val (clientMutationId, ids, patch) = input
|
||||
|
||||
updateChapters(ids, patch)
|
||||
updateChapters(ids, patch)
|
||||
|
||||
val chapters =
|
||||
transaction {
|
||||
ChapterTable.selectAll().where { ChapterTable.id inList ids }.map { ChapterType(it) }
|
||||
}
|
||||
val chapters =
|
||||
transaction {
|
||||
ChapterTable.selectAll().where { ChapterTable.id inList ids }.map { ChapterType(it) }
|
||||
}
|
||||
|
||||
return UpdateChaptersPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
chapters = chapters,
|
||||
)
|
||||
}
|
||||
UpdateChaptersPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
chapters = chapters,
|
||||
)
|
||||
}
|
||||
|
||||
data class FetchChaptersInput(
|
||||
val clientMutationId: String? = null,
|
||||
@@ -167,25 +166,27 @@ class ChapterMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun fetchChapters(input: FetchChaptersInput): CompletableFuture<FetchChaptersPayload?> {
|
||||
fun fetchChapters(input: FetchChaptersInput): CompletableFuture<DataFetcherResult<FetchChaptersPayload?>> {
|
||||
val (clientMutationId, mangaId) = input
|
||||
|
||||
return future {
|
||||
Chapter.fetchChapterList(mangaId)
|
||||
asDataFetcherResult {
|
||||
Chapter.fetchChapterList(mangaId)
|
||||
|
||||
val chapters =
|
||||
transaction {
|
||||
ChapterTable
|
||||
.selectAll()
|
||||
.where { ChapterTable.manga eq mangaId }
|
||||
.orderBy(ChapterTable.sourceOrder)
|
||||
.map { ChapterType(it) }
|
||||
}
|
||||
val chapters =
|
||||
transaction {
|
||||
ChapterTable
|
||||
.selectAll()
|
||||
.where { ChapterTable.manga eq mangaId }
|
||||
.orderBy(ChapterTable.sourceOrder)
|
||||
.map { ChapterType(it) }
|
||||
}
|
||||
|
||||
FetchChaptersPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
chapters = chapters,
|
||||
)
|
||||
FetchChaptersPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
chapters = chapters,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,13 +201,14 @@ class ChapterMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun setChapterMeta(input: SetChapterMetaInput): SetChapterMetaPayload? {
|
||||
val (clientMutationId, meta) = input
|
||||
fun setChapterMeta(input: SetChapterMetaInput): DataFetcherResult<SetChapterMetaPayload?> =
|
||||
asDataFetcherResult {
|
||||
val (clientMutationId, meta) = input
|
||||
|
||||
Chapter.modifyChapterMeta(meta.chapterId, meta.key, meta.value)
|
||||
Chapter.modifyChapterMeta(meta.chapterId, meta.key, meta.value)
|
||||
|
||||
return SetChapterMetaPayload(clientMutationId, meta)
|
||||
}
|
||||
SetChapterMetaPayload(clientMutationId, meta)
|
||||
}
|
||||
|
||||
data class DeleteChapterMetaInput(
|
||||
val clientMutationId: String? = null,
|
||||
@@ -221,33 +223,34 @@ class ChapterMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun deleteChapterMeta(input: DeleteChapterMetaInput): DeleteChapterMetaPayload? {
|
||||
val (clientMutationId, chapterId, key) = input
|
||||
fun deleteChapterMeta(input: DeleteChapterMetaInput): DataFetcherResult<DeleteChapterMetaPayload?> =
|
||||
asDataFetcherResult {
|
||||
val (clientMutationId, chapterId, key) = input
|
||||
|
||||
val (meta, chapter) =
|
||||
transaction {
|
||||
val meta =
|
||||
ChapterMetaTable
|
||||
.selectAll()
|
||||
.where { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }
|
||||
.firstOrNull()
|
||||
val (meta, chapter) =
|
||||
transaction {
|
||||
val meta =
|
||||
ChapterMetaTable
|
||||
.selectAll()
|
||||
.where { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }
|
||||
.firstOrNull()
|
||||
|
||||
ChapterMetaTable.deleteWhere { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }
|
||||
ChapterMetaTable.deleteWhere { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }
|
||||
|
||||
val chapter =
|
||||
transaction {
|
||||
ChapterType(ChapterTable.selectAll().where { ChapterTable.id eq chapterId }.first())
|
||||
}
|
||||
val chapter =
|
||||
transaction {
|
||||
ChapterType(ChapterTable.selectAll().where { ChapterTable.id eq chapterId }.first())
|
||||
}
|
||||
|
||||
if (meta != null) {
|
||||
ChapterMetaType(meta)
|
||||
} else {
|
||||
null
|
||||
} to chapter
|
||||
}
|
||||
if (meta != null) {
|
||||
ChapterMetaType(meta)
|
||||
} else {
|
||||
null
|
||||
} to chapter
|
||||
}
|
||||
|
||||
return DeleteChapterMetaPayload(clientMutationId, meta, chapter)
|
||||
}
|
||||
DeleteChapterMetaPayload(clientMutationId, meta, chapter)
|
||||
}
|
||||
|
||||
data class SetChapterMetasItem(
|
||||
val chapterIds: List<Int>,
|
||||
@@ -266,42 +269,43 @@ class ChapterMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun setChapterMetas(input: SetChapterMetasInput): SetChapterMetasPayload? {
|
||||
val (clientMutationId, items) = input
|
||||
fun setChapterMetas(input: SetChapterMetasInput): DataFetcherResult<SetChapterMetasPayload?> =
|
||||
asDataFetcherResult {
|
||||
val (clientMutationId, items) = input
|
||||
|
||||
val metaByChapterId =
|
||||
items
|
||||
.flatMap { item ->
|
||||
val metaMap = item.metas.associate { it.key to it.value }
|
||||
item.chapterIds.map { chapterId -> chapterId to metaMap }
|
||||
}.groupBy({ it.first }, { it.second })
|
||||
.mapValues { (_, maps) -> maps.reduce { acc, map -> acc + map } }
|
||||
val metaByChapterId =
|
||||
items
|
||||
.flatMap { item ->
|
||||
val metaMap = item.metas.associate { it.key to it.value }
|
||||
item.chapterIds.map { chapterId -> chapterId to metaMap }
|
||||
}.groupBy({ it.first }, { it.second })
|
||||
.mapValues { (_, maps) -> maps.reduce { acc, map -> acc + map } }
|
||||
|
||||
Chapter.modifyChaptersMetas(metaByChapterId)
|
||||
Chapter.modifyChaptersMetas(metaByChapterId)
|
||||
|
||||
val allChapterIds = metaByChapterId.keys
|
||||
val allMetaKeys = metaByChapterId.values.flatMap { it.keys }.distinct()
|
||||
val allChapterIds = metaByChapterId.keys
|
||||
val allMetaKeys = metaByChapterId.values.flatMap { it.keys }.distinct()
|
||||
|
||||
val (updatedMetas, chapters) =
|
||||
transaction {
|
||||
val updatedMetas =
|
||||
ChapterMetaTable
|
||||
.selectAll()
|
||||
.where { (ChapterMetaTable.ref inList allChapterIds) and (ChapterMetaTable.key inList allMetaKeys) }
|
||||
.map { ChapterMetaType(it) }
|
||||
val (updatedMetas, chapters) =
|
||||
transaction {
|
||||
val updatedMetas =
|
||||
ChapterMetaTable
|
||||
.selectAll()
|
||||
.where { (ChapterMetaTable.ref inList allChapterIds) and (ChapterMetaTable.key inList allMetaKeys) }
|
||||
.map { ChapterMetaType(it) }
|
||||
|
||||
val chapters =
|
||||
ChapterTable
|
||||
.selectAll()
|
||||
.where { ChapterTable.id inList allChapterIds }
|
||||
.map { ChapterType(it) }
|
||||
.distinctBy { it.id }
|
||||
val chapters =
|
||||
ChapterTable
|
||||
.selectAll()
|
||||
.where { ChapterTable.id inList allChapterIds }
|
||||
.map { ChapterType(it) }
|
||||
.distinctBy { it.id }
|
||||
|
||||
updatedMetas to chapters
|
||||
}
|
||||
updatedMetas to chapters
|
||||
}
|
||||
|
||||
return SetChapterMetasPayload(clientMutationId, updatedMetas, chapters)
|
||||
}
|
||||
SetChapterMetasPayload(clientMutationId, updatedMetas, chapters)
|
||||
}
|
||||
|
||||
data class DeleteChapterMetasItem(
|
||||
val chapterIds: List<Int>,
|
||||
@@ -321,63 +325,64 @@ class ChapterMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun deleteChapterMetas(input: DeleteChapterMetasInput): DeleteChapterMetasPayload? {
|
||||
val (clientMutationId, items) = input
|
||||
fun deleteChapterMetas(input: DeleteChapterMetasInput): DataFetcherResult<DeleteChapterMetasPayload?> =
|
||||
asDataFetcherResult {
|
||||
val (clientMutationId, items) = input
|
||||
|
||||
items.forEach { item ->
|
||||
require(!item.keys.isNullOrEmpty() || !item.prefixes.isNullOrEmpty()) {
|
||||
"Either 'keys' or 'prefixes' must be provided for each item"
|
||||
items.forEach { item ->
|
||||
require(!item.keys.isNullOrEmpty() || !item.prefixes.isNullOrEmpty()) {
|
||||
"Either 'keys' or 'prefixes' must be provided for each item"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val (allDeletedMetas, allChapterIds) =
|
||||
transaction {
|
||||
val deletedMetas = mutableListOf<ChapterMetaType>()
|
||||
val chapterIds = mutableSetOf<Int>()
|
||||
val (allDeletedMetas, allChapterIds) =
|
||||
transaction {
|
||||
val deletedMetas = mutableListOf<ChapterMetaType>()
|
||||
val chapterIds = mutableSetOf<Int>()
|
||||
|
||||
items.forEach { item ->
|
||||
val keyCondition: Op<Boolean>? =
|
||||
item.keys?.takeIf { it.isNotEmpty() }?.let { ChapterMetaTable.key inList it }
|
||||
items.forEach { item ->
|
||||
val keyCondition: Op<Boolean>? =
|
||||
item.keys?.takeIf { it.isNotEmpty() }?.let { ChapterMetaTable.key inList it }
|
||||
|
||||
val prefixCondition: Op<Boolean>? =
|
||||
item.prefixes
|
||||
?.filter { it.isNotEmpty() }
|
||||
?.map { (ChapterMetaTable.key like LikePattern("$it%")) as Op<Boolean> }
|
||||
?.reduceOrNull { acc, op -> acc or op }
|
||||
val prefixCondition: Op<Boolean>? =
|
||||
item.prefixes
|
||||
?.filter { it.isNotEmpty() }
|
||||
?.map { (ChapterMetaTable.key like LikePattern("$it%")) as Op<Boolean> }
|
||||
?.reduceOrNull { acc, op -> acc or op }
|
||||
|
||||
val metaKeyCondition =
|
||||
if (keyCondition != null && prefixCondition != null) {
|
||||
keyCondition or prefixCondition
|
||||
} else {
|
||||
keyCondition ?: prefixCondition!!
|
||||
}
|
||||
val metaKeyCondition =
|
||||
if (keyCondition != null && prefixCondition != null) {
|
||||
keyCondition or prefixCondition
|
||||
} else {
|
||||
keyCondition ?: prefixCondition!!
|
||||
}
|
||||
|
||||
val condition = (ChapterMetaTable.ref inList item.chapterIds) and metaKeyCondition
|
||||
val condition = (ChapterMetaTable.ref inList item.chapterIds) and metaKeyCondition
|
||||
|
||||
deletedMetas +=
|
||||
ChapterMetaTable
|
||||
.selectAll()
|
||||
.where { condition }
|
||||
.map { ChapterMetaType(it) }
|
||||
deletedMetas +=
|
||||
ChapterMetaTable
|
||||
.selectAll()
|
||||
.where { condition }
|
||||
.map { ChapterMetaType(it) }
|
||||
|
||||
ChapterMetaTable.deleteWhere { condition }
|
||||
chapterIds += item.chapterIds
|
||||
ChapterMetaTable.deleteWhere { condition }
|
||||
chapterIds += item.chapterIds
|
||||
}
|
||||
|
||||
deletedMetas to chapterIds
|
||||
}
|
||||
|
||||
deletedMetas to chapterIds
|
||||
}
|
||||
val chapters =
|
||||
transaction {
|
||||
ChapterTable
|
||||
.selectAll()
|
||||
.where { ChapterTable.id inList allChapterIds }
|
||||
.map { ChapterType(it) }
|
||||
.distinctBy { it.id }
|
||||
}
|
||||
|
||||
val chapters =
|
||||
transaction {
|
||||
ChapterTable
|
||||
.selectAll()
|
||||
.where { ChapterTable.id inList allChapterIds }
|
||||
.map { ChapterType(it) }
|
||||
.distinctBy { it.id }
|
||||
}
|
||||
|
||||
return DeleteChapterMetasPayload(clientMutationId, allDeletedMetas, chapters)
|
||||
}
|
||||
DeleteChapterMetasPayload(clientMutationId, allDeletedMetas, chapters)
|
||||
}
|
||||
|
||||
data class FetchChapterPagesInput(
|
||||
val clientMutationId: String? = null,
|
||||
@@ -400,65 +405,67 @@ class ChapterMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun fetchChapterPages(input: FetchChapterPagesInput): CompletableFuture<FetchChapterPagesPayload?> {
|
||||
fun fetchChapterPages(input: FetchChapterPagesInput): CompletableFuture<DataFetcherResult<FetchChapterPagesPayload?>> {
|
||||
val (clientMutationId, chapterId) = input
|
||||
val paramsMap = input.toParams()
|
||||
|
||||
return future {
|
||||
var chapter = getChapterDownloadReadyById(chapterId)
|
||||
val syncResult = KoreaderSyncService.checkAndPullProgress(chapter.id)
|
||||
var syncConflictInfo: SyncConflictInfoType? = null
|
||||
asDataFetcherResult {
|
||||
var chapter = getChapterDownloadReadyById(chapterId)
|
||||
val syncResult = KoreaderSyncService.checkAndPullProgress(chapter.id)
|
||||
var syncConflictInfo: SyncConflictInfoType? = null
|
||||
|
||||
if (syncResult != null) {
|
||||
if (syncResult.isConflict) {
|
||||
syncConflictInfo =
|
||||
SyncConflictInfoType(
|
||||
deviceName = syncResult.device,
|
||||
remotePage = syncResult.pageRead,
|
||||
if (syncResult != null) {
|
||||
if (syncResult.isConflict) {
|
||||
syncConflictInfo =
|
||||
SyncConflictInfoType(
|
||||
deviceName = syncResult.device,
|
||||
remotePage = syncResult.pageRead,
|
||||
)
|
||||
}
|
||||
|
||||
if (syncResult.shouldUpdate) {
|
||||
// Update DB for SILENT and RECEIVE
|
||||
transaction {
|
||||
ChapterTable.update({ ChapterTable.id eq chapter.id }) {
|
||||
it[lastPageRead] = syncResult.pageRead
|
||||
it[lastReadAt] = syncResult.timestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
// For PROMPT, SILENT, and RECEIVE, return the remote progress
|
||||
chapter =
|
||||
chapter.copy(
|
||||
lastPageRead = if (syncResult.shouldUpdate) syncResult.pageRead else chapter.lastPageRead,
|
||||
lastReadAt = if (syncResult.shouldUpdate) syncResult.timestamp else chapter.lastReadAt,
|
||||
)
|
||||
}
|
||||
|
||||
if (syncResult.shouldUpdate) {
|
||||
// Update DB for SILENT and RECEIVE
|
||||
transaction {
|
||||
ChapterTable.update({ ChapterTable.id eq chapter.id }) {
|
||||
it[lastPageRead] = syncResult.pageRead
|
||||
it[lastReadAt] = syncResult.timestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
// For PROMPT, SILENT, and RECEIVE, return the remote progress
|
||||
chapter =
|
||||
chapter.copy(
|
||||
lastPageRead = if (syncResult.shouldUpdate) syncResult.pageRead else chapter.lastPageRead,
|
||||
lastReadAt = if (syncResult.shouldUpdate) syncResult.timestamp else chapter.lastReadAt,
|
||||
)
|
||||
}
|
||||
|
||||
val params =
|
||||
buildString {
|
||||
if (paramsMap.isNotEmpty()) {
|
||||
append("?")
|
||||
paramsMap.entries.forEach { entry ->
|
||||
if (length > 1) {
|
||||
append("&")
|
||||
val params =
|
||||
buildString {
|
||||
if (paramsMap.isNotEmpty()) {
|
||||
append("?")
|
||||
paramsMap.entries.forEach { entry ->
|
||||
if (length > 1) {
|
||||
append("&")
|
||||
}
|
||||
append(entry.key)
|
||||
append("=")
|
||||
append(URLEncoder.encode(entry.value, Charsets.UTF_8))
|
||||
}
|
||||
append(entry.key)
|
||||
append("=")
|
||||
append(URLEncoder.encode(entry.value, Charsets.UTF_8))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FetchChapterPagesPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
pages =
|
||||
List(chapter.pageCount) { index ->
|
||||
"/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}/page/${index}$params"
|
||||
},
|
||||
chapter = ChapterType(chapter),
|
||||
syncConflict = syncConflictInfo,
|
||||
)
|
||||
FetchChapterPagesPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
pages =
|
||||
List(chapter.pageCount) { index ->
|
||||
"/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}/page/${index}$params"
|
||||
},
|
||||
chapter = ChapterType(chapter),
|
||||
syncConflict = syncConflictInfo,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import graphql.execution.DataFetcherResult
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.types.ChapterType
|
||||
import suwayomi.tachidesk.graphql.types.DownloadStatus
|
||||
@@ -32,21 +30,23 @@ class DownloadMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun deleteDownloadedChapters(input: DeleteDownloadedChaptersInput): DeleteDownloadedChaptersPayload? {
|
||||
fun deleteDownloadedChapters(input: DeleteDownloadedChaptersInput): DataFetcherResult<DeleteDownloadedChaptersPayload?> {
|
||||
val (clientMutationId, chapters) = input
|
||||
|
||||
Chapter.deleteChapters(chapters)
|
||||
return asDataFetcherResult {
|
||||
Chapter.deleteChapters(chapters)
|
||||
|
||||
return DeleteDownloadedChaptersPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
chapters =
|
||||
transaction {
|
||||
ChapterTable
|
||||
.selectAll()
|
||||
.where { ChapterTable.id inList chapters }
|
||||
.map { ChapterType(it) }
|
||||
},
|
||||
)
|
||||
DeleteDownloadedChaptersPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
chapters =
|
||||
transaction {
|
||||
ChapterTable
|
||||
.selectAll()
|
||||
.where { ChapterTable.id inList chapters }
|
||||
.map { ChapterType(it) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class DeleteDownloadedChapterInput(
|
||||
@@ -60,18 +60,20 @@ class DownloadMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun deleteDownloadedChapter(input: DeleteDownloadedChapterInput): DeleteDownloadedChapterPayload? {
|
||||
fun deleteDownloadedChapter(input: DeleteDownloadedChapterInput): DataFetcherResult<DeleteDownloadedChapterPayload?> {
|
||||
val (clientMutationId, chapter) = input
|
||||
|
||||
Chapter.deleteChapters(listOf(chapter))
|
||||
return asDataFetcherResult {
|
||||
Chapter.deleteChapters(listOf(chapter))
|
||||
|
||||
return DeleteDownloadedChapterPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
chapters =
|
||||
transaction {
|
||||
ChapterType(ChapterTable.selectAll().where { ChapterTable.id eq chapter }.first())
|
||||
},
|
||||
)
|
||||
DeleteDownloadedChapterPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
chapters =
|
||||
transaction {
|
||||
ChapterType(ChapterTable.selectAll().where { ChapterTable.id eq chapter }.first())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class EnqueueChapterDownloadsInput(
|
||||
@@ -85,24 +87,28 @@ class DownloadMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun enqueueChapterDownloads(input: EnqueueChapterDownloadsInput): CompletableFuture<EnqueueChapterDownloadsPayload?> {
|
||||
fun enqueueChapterDownloads(
|
||||
input: EnqueueChapterDownloadsInput,
|
||||
): CompletableFuture<DataFetcherResult<EnqueueChapterDownloadsPayload?>> {
|
||||
val (clientMutationId, chapters) = input
|
||||
|
||||
return future {
|
||||
DownloadManager.enqueue(DownloadManager.EnqueueInput(chapters))
|
||||
asDataFetcherResult {
|
||||
DownloadManager.enqueue(DownloadManager.EnqueueInput(chapters))
|
||||
|
||||
EnqueueChapterDownloadsPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
downloadStatus =
|
||||
withTimeout(30.seconds) {
|
||||
DownloadStatus(
|
||||
DownloadManager.updates
|
||||
.first {
|
||||
DownloadManager.getStatus().queue.any { it.chapterId in chapters }
|
||||
}.let { DownloadManager.getStatus() },
|
||||
)
|
||||
},
|
||||
)
|
||||
EnqueueChapterDownloadsPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
downloadStatus =
|
||||
withTimeout(30.seconds) {
|
||||
DownloadStatus(
|
||||
DownloadManager.updates
|
||||
.first {
|
||||
DownloadManager.getStatus().queue.any { it.chapterId in chapters }
|
||||
}.let { DownloadManager.getStatus() },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,23 +123,25 @@ class DownloadMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun enqueueChapterDownload(input: EnqueueChapterDownloadInput): CompletableFuture<EnqueueChapterDownloadPayload?> {
|
||||
fun enqueueChapterDownload(input: EnqueueChapterDownloadInput): CompletableFuture<DataFetcherResult<EnqueueChapterDownloadPayload?>> {
|
||||
val (clientMutationId, chapter) = input
|
||||
|
||||
return future {
|
||||
DownloadManager.enqueue(DownloadManager.EnqueueInput(listOf(chapter)))
|
||||
asDataFetcherResult {
|
||||
DownloadManager.enqueue(DownloadManager.EnqueueInput(listOf(chapter)))
|
||||
|
||||
EnqueueChapterDownloadPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
downloadStatus =
|
||||
withTimeout(30.seconds) {
|
||||
DownloadStatus(
|
||||
DownloadManager.updates
|
||||
.first { it.updates.any { it.downloadQueueItem.chapterId == chapter } }
|
||||
.let { DownloadManager.getStatus() },
|
||||
)
|
||||
},
|
||||
)
|
||||
EnqueueChapterDownloadPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
downloadStatus =
|
||||
withTimeout(30.seconds) {
|
||||
DownloadStatus(
|
||||
DownloadManager.updates
|
||||
.first { it.updates.any { it.downloadQueueItem.chapterId == chapter } }
|
||||
.let { DownloadManager.getStatus() },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,26 +156,30 @@ class DownloadMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun dequeueChapterDownloads(input: DequeueChapterDownloadsInput): CompletableFuture<DequeueChapterDownloadsPayload?> {
|
||||
fun dequeueChapterDownloads(
|
||||
input: DequeueChapterDownloadsInput,
|
||||
): CompletableFuture<DataFetcherResult<DequeueChapterDownloadsPayload?>> {
|
||||
val (clientMutationId, chapters) = input
|
||||
|
||||
return future {
|
||||
DownloadManager.dequeue(DownloadManager.EnqueueInput(chapters))
|
||||
asDataFetcherResult {
|
||||
DownloadManager.dequeue(DownloadManager.EnqueueInput(chapters))
|
||||
|
||||
DequeueChapterDownloadsPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
downloadStatus =
|
||||
withTimeout(30.seconds) {
|
||||
DownloadStatus(
|
||||
DownloadManager.updates
|
||||
.first {
|
||||
it.updates.any {
|
||||
it.downloadQueueItem.chapterId in chapters && it.type == DEQUEUED
|
||||
}
|
||||
}.let { DownloadManager.getStatus() },
|
||||
)
|
||||
},
|
||||
)
|
||||
DequeueChapterDownloadsPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
downloadStatus =
|
||||
withTimeout(30.seconds) {
|
||||
DownloadStatus(
|
||||
DownloadManager.updates
|
||||
.first {
|
||||
it.updates.any {
|
||||
it.downloadQueueItem.chapterId in chapters && it.type == DEQUEUED
|
||||
}
|
||||
}.let { DownloadManager.getStatus() },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,26 +194,28 @@ class DownloadMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun dequeueChapterDownload(input: DequeueChapterDownloadInput): CompletableFuture<DequeueChapterDownloadPayload?> {
|
||||
fun dequeueChapterDownload(input: DequeueChapterDownloadInput): CompletableFuture<DataFetcherResult<DequeueChapterDownloadPayload?>> {
|
||||
val (clientMutationId, chapter) = input
|
||||
|
||||
return future {
|
||||
DownloadManager.dequeue(DownloadManager.EnqueueInput(listOf(chapter)))
|
||||
asDataFetcherResult {
|
||||
DownloadManager.dequeue(DownloadManager.EnqueueInput(listOf(chapter)))
|
||||
|
||||
DequeueChapterDownloadPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
downloadStatus =
|
||||
withTimeout(30.seconds) {
|
||||
DownloadStatus(
|
||||
DownloadManager.updates
|
||||
.first {
|
||||
it.updates.any {
|
||||
it.downloadQueueItem.chapterId == chapter && it.type == DEQUEUED
|
||||
}
|
||||
}.let { DownloadManager.getStatus() },
|
||||
)
|
||||
},
|
||||
)
|
||||
DequeueChapterDownloadPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
downloadStatus =
|
||||
withTimeout(30.seconds) {
|
||||
DownloadStatus(
|
||||
DownloadManager.updates
|
||||
.first {
|
||||
it.updates.any {
|
||||
it.downloadQueueItem.chapterId == chapter && it.type == DEQUEUED
|
||||
}
|
||||
}.let { DownloadManager.getStatus() },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,21 +229,23 @@ class DownloadMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun startDownloader(input: StartDownloaderInput): CompletableFuture<StartDownloaderPayload?> =
|
||||
fun startDownloader(input: StartDownloaderInput): CompletableFuture<DataFetcherResult<StartDownloaderPayload?>> =
|
||||
future {
|
||||
DownloadManager.start()
|
||||
asDataFetcherResult {
|
||||
DownloadManager.start()
|
||||
|
||||
StartDownloaderPayload(
|
||||
input.clientMutationId,
|
||||
downloadStatus =
|
||||
withTimeout(30.seconds) {
|
||||
DownloadStatus(
|
||||
DownloadManager.updates
|
||||
.first { it.status == Status.Started }
|
||||
.let { DownloadManager.getStatus() },
|
||||
)
|
||||
},
|
||||
)
|
||||
StartDownloaderPayload(
|
||||
input.clientMutationId,
|
||||
downloadStatus =
|
||||
withTimeout(30.seconds) {
|
||||
DownloadStatus(
|
||||
DownloadManager.updates
|
||||
.first { it.status == Status.Started }
|
||||
.let { DownloadManager.getStatus() },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class StopDownloaderInput(
|
||||
@@ -242,21 +258,23 @@ class DownloadMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun stopDownloader(input: StopDownloaderInput): CompletableFuture<StopDownloaderPayload?> =
|
||||
fun stopDownloader(input: StopDownloaderInput): CompletableFuture<DataFetcherResult<StopDownloaderPayload?>> =
|
||||
future {
|
||||
DownloadManager.stop()
|
||||
asDataFetcherResult {
|
||||
DownloadManager.stop()
|
||||
|
||||
StopDownloaderPayload(
|
||||
input.clientMutationId,
|
||||
downloadStatus =
|
||||
withTimeout(30.seconds) {
|
||||
DownloadStatus(
|
||||
DownloadManager.updates
|
||||
.first { it.status == Status.Stopped }
|
||||
.let { DownloadManager.getStatus() },
|
||||
)
|
||||
},
|
||||
)
|
||||
StopDownloaderPayload(
|
||||
input.clientMutationId,
|
||||
downloadStatus =
|
||||
withTimeout(30.seconds) {
|
||||
DownloadStatus(
|
||||
DownloadManager.updates
|
||||
.first { it.status == Status.Stopped }
|
||||
.let { DownloadManager.getStatus() },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class ClearDownloaderInput(
|
||||
@@ -269,21 +287,23 @@ class DownloadMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun clearDownloader(input: ClearDownloaderInput): CompletableFuture<ClearDownloaderPayload?> =
|
||||
fun clearDownloader(input: ClearDownloaderInput): CompletableFuture<DataFetcherResult<ClearDownloaderPayload?>> =
|
||||
future {
|
||||
DownloadManager.clear()
|
||||
asDataFetcherResult {
|
||||
DownloadManager.clear()
|
||||
|
||||
ClearDownloaderPayload(
|
||||
input.clientMutationId,
|
||||
downloadStatus =
|
||||
withTimeout(30.seconds) {
|
||||
DownloadStatus(
|
||||
DownloadManager.updates
|
||||
.first { it.status == Status.Stopped }
|
||||
.let { DownloadManager.getStatus() },
|
||||
)
|
||||
},
|
||||
)
|
||||
ClearDownloaderPayload(
|
||||
input.clientMutationId,
|
||||
downloadStatus =
|
||||
withTimeout(30.seconds) {
|
||||
DownloadStatus(
|
||||
DownloadManager.updates
|
||||
.first { it.status == Status.Stopped }
|
||||
.let { DownloadManager.getStatus() },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class ReorderChapterDownloadInput(
|
||||
@@ -298,23 +318,25 @@ class DownloadMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun reorderChapterDownload(input: ReorderChapterDownloadInput): CompletableFuture<ReorderChapterDownloadPayload?> {
|
||||
fun reorderChapterDownload(input: ReorderChapterDownloadInput): CompletableFuture<DataFetcherResult<ReorderChapterDownloadPayload?>> {
|
||||
val (clientMutationId, chapter, to) = input
|
||||
|
||||
return future {
|
||||
DownloadManager.reorder(chapter, to)
|
||||
asDataFetcherResult {
|
||||
DownloadManager.reorder(chapter, to)
|
||||
|
||||
ReorderChapterDownloadPayload(
|
||||
clientMutationId,
|
||||
downloadStatus =
|
||||
withTimeout(30.seconds) {
|
||||
DownloadStatus(
|
||||
DownloadManager.updates
|
||||
.first { it.updates.indexOfFirst { it.downloadQueueItem.chapterId == chapter } <= to }
|
||||
.let { DownloadManager.getStatus() },
|
||||
)
|
||||
},
|
||||
)
|
||||
ReorderChapterDownloadPayload(
|
||||
clientMutationId,
|
||||
downloadStatus =
|
||||
withTimeout(30.seconds) {
|
||||
DownloadStatus(
|
||||
DownloadManager.updates
|
||||
.first { it.updates.indexOfFirst { it.downloadQueueItem.chapterId == chapter } <= to }
|
||||
.let { DownloadManager.getStatus() },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||
import graphql.execution.DataFetcherResult
|
||||
import io.javalin.http.UploadedFile
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.core.neq
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.types.ExtensionType
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension
|
||||
@@ -78,47 +75,51 @@ class ExtensionMutation {
|
||||
}
|
||||
|
||||
@RequireAuth
|
||||
fun updateExtension(input: UpdateExtensionInput): CompletableFuture<UpdateExtensionPayload?> {
|
||||
fun updateExtension(input: UpdateExtensionInput): CompletableFuture<DataFetcherResult<UpdateExtensionPayload?>> {
|
||||
val (clientMutationId, id, patch) = input
|
||||
|
||||
return future {
|
||||
updateExtensions(listOf(id), patch)
|
||||
asDataFetcherResult {
|
||||
updateExtensions(listOf(id), patch)
|
||||
|
||||
val extension =
|
||||
transaction {
|
||||
ExtensionTable
|
||||
.selectAll()
|
||||
.where { ExtensionTable.pkgName eq id }
|
||||
.firstOrNull()
|
||||
?.let { ExtensionType(it) }
|
||||
}
|
||||
val extension =
|
||||
transaction {
|
||||
ExtensionTable
|
||||
.selectAll()
|
||||
.where { ExtensionTable.pkgName eq id }
|
||||
.firstOrNull()
|
||||
?.let { ExtensionType(it) }
|
||||
}
|
||||
|
||||
UpdateExtensionPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
extension = extension,
|
||||
)
|
||||
UpdateExtensionPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
extension = extension,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequireAuth
|
||||
fun updateExtensions(input: UpdateExtensionsInput): CompletableFuture<UpdateExtensionsPayload?> {
|
||||
fun updateExtensions(input: UpdateExtensionsInput): CompletableFuture<DataFetcherResult<UpdateExtensionsPayload?>> {
|
||||
val (clientMutationId, ids, patch) = input
|
||||
|
||||
return future {
|
||||
updateExtensions(ids, patch)
|
||||
asDataFetcherResult {
|
||||
updateExtensions(ids, patch)
|
||||
|
||||
val extensions =
|
||||
transaction {
|
||||
ExtensionTable
|
||||
.selectAll()
|
||||
.where { ExtensionTable.pkgName inList ids }
|
||||
.map { ExtensionType(it) }
|
||||
}
|
||||
val extensions =
|
||||
transaction {
|
||||
ExtensionTable
|
||||
.selectAll()
|
||||
.where { ExtensionTable.pkgName inList ids }
|
||||
.map { ExtensionType(it) }
|
||||
}
|
||||
|
||||
UpdateExtensionsPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
extensions = extensions,
|
||||
)
|
||||
UpdateExtensionsPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
extensions = extensions,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,24 +133,26 @@ class ExtensionMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun fetchExtensions(input: FetchExtensionsInput): CompletableFuture<FetchExtensionsPayload?> {
|
||||
fun fetchExtensions(input: FetchExtensionsInput): CompletableFuture<DataFetcherResult<FetchExtensionsPayload?>> {
|
||||
val (clientMutationId) = input
|
||||
|
||||
return future {
|
||||
ExtensionsList.fetchExtensions()
|
||||
asDataFetcherResult {
|
||||
ExtensionsList.fetchExtensions()
|
||||
|
||||
val extensions =
|
||||
transaction {
|
||||
ExtensionTable
|
||||
.selectAll()
|
||||
.where { ExtensionTable.name neq LocalSource.EXTENSION_NAME }
|
||||
.map { ExtensionType(it) }
|
||||
}
|
||||
val extensions =
|
||||
transaction {
|
||||
ExtensionTable
|
||||
.selectAll()
|
||||
.where { ExtensionTable.name neq LocalSource.EXTENSION_NAME }
|
||||
.map { ExtensionType(it) }
|
||||
}
|
||||
|
||||
FetchExtensionsPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
extensions = extensions,
|
||||
)
|
||||
FetchExtensionsPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
extensions = extensions,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,19 +167,23 @@ class ExtensionMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun installExternalExtension(input: InstallExternalExtensionInput): CompletableFuture<InstallExternalExtensionPayload?> {
|
||||
fun installExternalExtension(
|
||||
input: InstallExternalExtensionInput,
|
||||
): CompletableFuture<DataFetcherResult<InstallExternalExtensionPayload?>> {
|
||||
val (clientMutationId, extensionFile) = input
|
||||
|
||||
return future {
|
||||
Extension.installExternalExtension(extensionFile.content(), extensionFile.filename())
|
||||
asDataFetcherResult {
|
||||
Extension.installExternalExtension(extensionFile.content(), extensionFile.filename())
|
||||
|
||||
val dbExtension =
|
||||
transaction { ExtensionTable.selectAll().where { ExtensionTable.apkName eq extensionFile.filename() }.first() }
|
||||
val dbExtension =
|
||||
transaction { ExtensionTable.selectAll().where { ExtensionTable.apkName eq extensionFile.filename() }.first() }
|
||||
|
||||
InstallExternalExtensionPayload(
|
||||
clientMutationId,
|
||||
extension = ExtensionType(dbExtension),
|
||||
)
|
||||
InstallExternalExtensionPayload(
|
||||
clientMutationId,
|
||||
extension = ExtensionType(dbExtension),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import graphql.execution.DataFetcherResult
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING
|
||||
import suwayomi.tachidesk.graphql.types.UpdateState.ERROR
|
||||
@@ -26,51 +26,55 @@ class InfoMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun updateWebUI(input: WebUIUpdateInput): CompletableFuture<WebUIUpdatePayload?> {
|
||||
fun updateWebUI(input: WebUIUpdateInput): CompletableFuture<DataFetcherResult<WebUIUpdatePayload?>> {
|
||||
return future {
|
||||
withTimeout(30.seconds) {
|
||||
if (WebInterfaceManager.status.value.state === DOWNLOADING) {
|
||||
return@withTimeout WebUIUpdatePayload(input.clientMutationId, WebInterfaceManager.status.value)
|
||||
}
|
||||
asDataFetcherResult {
|
||||
withTimeout(30.seconds) {
|
||||
if (WebInterfaceManager.status.value.state === DOWNLOADING) {
|
||||
return@withTimeout WebUIUpdatePayload(input.clientMutationId, WebInterfaceManager.status.value)
|
||||
}
|
||||
|
||||
val flavor = WebUIFlavor.current
|
||||
val flavor = WebUIFlavor.current
|
||||
|
||||
val (version, updateAvailable) = WebInterfaceManager.isUpdateAvailable(flavor)
|
||||
val (version, updateAvailable) = WebInterfaceManager.isUpdateAvailable(flavor)
|
||||
|
||||
if (!updateAvailable) {
|
||||
val didUpdateCheckFail = version.isEmpty()
|
||||
if (!updateAvailable) {
|
||||
val didUpdateCheckFail = version.isEmpty()
|
||||
|
||||
return@withTimeout WebUIUpdatePayload(
|
||||
return@withTimeout WebUIUpdatePayload(
|
||||
input.clientMutationId,
|
||||
WebInterfaceManager.getStatus(version, if (didUpdateCheckFail) ERROR else IDLE),
|
||||
)
|
||||
}
|
||||
try {
|
||||
WebInterfaceManager.startDownloadInScope(flavor, version)
|
||||
} catch (e: Exception) {
|
||||
// ignore since we use the status anyway
|
||||
}
|
||||
|
||||
WebUIUpdatePayload(
|
||||
input.clientMutationId,
|
||||
WebInterfaceManager.getStatus(version, if (didUpdateCheckFail) ERROR else IDLE),
|
||||
updateStatus = WebInterfaceManager.status.first { it.state == DOWNLOADING },
|
||||
)
|
||||
}
|
||||
try {
|
||||
WebInterfaceManager.startDownloadInScope(flavor, version)
|
||||
} catch (e: Exception) {
|
||||
// ignore since we use the status anyway
|
||||
}
|
||||
|
||||
WebUIUpdatePayload(
|
||||
input.clientMutationId,
|
||||
updateStatus = WebInterfaceManager.status.first { it.state == DOWNLOADING },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequireAuth
|
||||
fun resetWebUIUpdateStatus(): CompletableFuture<WebUIUpdateStatus?> =
|
||||
fun resetWebUIUpdateStatus(): CompletableFuture<DataFetcherResult<WebUIUpdateStatus?>> =
|
||||
future {
|
||||
withTimeout(30.seconds) {
|
||||
val isUpdateFinished = WebInterfaceManager.status.value.state != DOWNLOADING
|
||||
if (!isUpdateFinished) {
|
||||
throw Exception("Status reset is not allowed during status \"$DOWNLOADING\"")
|
||||
asDataFetcherResult {
|
||||
withTimeout(30.seconds) {
|
||||
val isUpdateFinished = WebInterfaceManager.status.value.state != DOWNLOADING
|
||||
if (!isUpdateFinished) {
|
||||
throw Exception("Status reset is not allowed during status \"$DOWNLOADING\"")
|
||||
}
|
||||
|
||||
WebInterfaceManager.resetStatus()
|
||||
|
||||
WebInterfaceManager.status.first { it.state == IDLE }
|
||||
}
|
||||
|
||||
WebInterfaceManager.resetStatus()
|
||||
|
||||
WebInterfaceManager.status.first { it.state == IDLE }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import graphql.execution.DataFetcherResult
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.types.ChapterType
|
||||
import suwayomi.tachidesk.graphql.types.KoSyncConnectPayload
|
||||
@@ -63,24 +62,26 @@ class KoreaderSyncMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun pushKoSyncProgress(input: PushKoSyncProgressInput): CompletableFuture<PushKoSyncProgressPayload?> =
|
||||
fun pushKoSyncProgress(input: PushKoSyncProgressInput): CompletableFuture<DataFetcherResult<PushKoSyncProgressPayload?>> =
|
||||
future {
|
||||
KoreaderSyncService.pushProgress(input.chapterId)
|
||||
asDataFetcherResult {
|
||||
KoreaderSyncService.pushProgress(input.chapterId)
|
||||
|
||||
val chapter =
|
||||
transaction {
|
||||
ChapterTable
|
||||
.selectAll()
|
||||
.where { ChapterTable.id eq input.chapterId }
|
||||
.firstOrNull()
|
||||
?.let { ChapterType(it) }
|
||||
}
|
||||
val chapter =
|
||||
transaction {
|
||||
ChapterTable
|
||||
.selectAll()
|
||||
.where { ChapterTable.id eq input.chapterId }
|
||||
.firstOrNull()
|
||||
?.let { ChapterType(it) }
|
||||
}
|
||||
|
||||
PushKoSyncProgressPayload(
|
||||
clientMutationId = input.clientMutationId,
|
||||
success = true,
|
||||
chapter = chapter,
|
||||
)
|
||||
PushKoSyncProgressPayload(
|
||||
clientMutationId = input.clientMutationId,
|
||||
success = true,
|
||||
chapter = chapter,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class PullKoSyncProgressInput(
|
||||
@@ -95,43 +96,45 @@ class KoreaderSyncMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun pullKoSyncProgress(input: PullKoSyncProgressInput): CompletableFuture<PullKoSyncProgressPayload?> =
|
||||
fun pullKoSyncProgress(input: PullKoSyncProgressInput): CompletableFuture<DataFetcherResult<PullKoSyncProgressPayload?>> =
|
||||
future {
|
||||
val syncResult = KoreaderSyncService.checkAndPullProgress(input.chapterId)
|
||||
var syncConflictInfo: SyncConflictInfoType? = null
|
||||
asDataFetcherResult {
|
||||
val syncResult = KoreaderSyncService.checkAndPullProgress(input.chapterId)
|
||||
var syncConflictInfo: SyncConflictInfoType? = null
|
||||
|
||||
if (syncResult != null) {
|
||||
if (syncResult.isConflict) {
|
||||
syncConflictInfo =
|
||||
SyncConflictInfoType(
|
||||
deviceName = syncResult.device,
|
||||
remotePage = syncResult.pageRead,
|
||||
)
|
||||
}
|
||||
if (syncResult != null) {
|
||||
if (syncResult.isConflict) {
|
||||
syncConflictInfo =
|
||||
SyncConflictInfoType(
|
||||
deviceName = syncResult.device,
|
||||
remotePage = syncResult.pageRead,
|
||||
)
|
||||
}
|
||||
|
||||
if (syncResult.shouldUpdate) {
|
||||
transaction {
|
||||
ChapterTable.update({ ChapterTable.id eq input.chapterId }) {
|
||||
it[lastPageRead] = syncResult.pageRead
|
||||
it[lastReadAt] = syncResult.timestamp
|
||||
if (syncResult.shouldUpdate) {
|
||||
transaction {
|
||||
ChapterTable.update({ ChapterTable.id eq input.chapterId }) {
|
||||
it[lastPageRead] = syncResult.pageRead
|
||||
it[lastReadAt] = syncResult.timestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val chapter =
|
||||
transaction {
|
||||
ChapterTable
|
||||
.selectAll()
|
||||
.where { ChapterTable.id eq input.chapterId }
|
||||
.firstOrNull()
|
||||
?.let { ChapterType(it) }
|
||||
}
|
||||
|
||||
PullKoSyncProgressPayload(
|
||||
clientMutationId = input.clientMutationId,
|
||||
chapter = chapter,
|
||||
syncConflict = syncConflictInfo,
|
||||
)
|
||||
}
|
||||
|
||||
val chapter =
|
||||
transaction {
|
||||
ChapterTable
|
||||
.selectAll()
|
||||
.where { ChapterTable.id eq input.chapterId }
|
||||
.firstOrNull()
|
||||
?.let { ChapterType(it) }
|
||||
}
|
||||
|
||||
PullKoSyncProgressPayload(
|
||||
clientMutationId = input.clientMutationId,
|
||||
chapter = chapter,
|
||||
syncConflict = syncConflictInfo,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import org.jetbrains.exposed.v1.core.LikePattern
|
||||
import org.jetbrains.exposed.v1.core.Op
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.core.like
|
||||
import org.jetbrains.exposed.v1.core.or
|
||||
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import graphql.execution.DataFetcherResult
|
||||
import org.jetbrains.exposed.sql.LikePattern
|
||||
import org.jetbrains.exposed.sql.Op
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.like
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.or
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.types.MangaMetaType
|
||||
import suwayomi.tachidesk.graphql.types.MangaType
|
||||
@@ -98,40 +98,44 @@ class MangaMutation {
|
||||
}
|
||||
|
||||
@RequireAuth
|
||||
fun updateManga(input: UpdateMangaInput): CompletableFuture<UpdateMangaPayload?> {
|
||||
fun updateManga(input: UpdateMangaInput): CompletableFuture<DataFetcherResult<UpdateMangaPayload?>> {
|
||||
val (clientMutationId, id, patch) = input
|
||||
|
||||
return future {
|
||||
updateMangas(listOf(id), patch)
|
||||
asDataFetcherResult {
|
||||
updateMangas(listOf(id), patch)
|
||||
|
||||
val manga =
|
||||
transaction {
|
||||
MangaType(MangaTable.selectAll().where { MangaTable.id eq id }.first())
|
||||
}
|
||||
val manga =
|
||||
transaction {
|
||||
MangaType(MangaTable.selectAll().where { MangaTable.id eq id }.first())
|
||||
}
|
||||
|
||||
UpdateMangaPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
manga = manga,
|
||||
)
|
||||
UpdateMangaPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
manga = manga,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequireAuth
|
||||
fun updateMangas(input: UpdateMangasInput): CompletableFuture<UpdateMangasPayload?> {
|
||||
fun updateMangas(input: UpdateMangasInput): CompletableFuture<DataFetcherResult<UpdateMangasPayload?>> {
|
||||
val (clientMutationId, ids, patch) = input
|
||||
|
||||
return future {
|
||||
updateMangas(ids, patch)
|
||||
asDataFetcherResult {
|
||||
updateMangas(ids, patch)
|
||||
|
||||
val mangas =
|
||||
transaction {
|
||||
MangaTable.selectAll().where { MangaTable.id inList ids }.map { MangaType(it) }
|
||||
}
|
||||
val mangas =
|
||||
transaction {
|
||||
MangaTable.selectAll().where { MangaTable.id inList ids }.map { MangaType(it) }
|
||||
}
|
||||
|
||||
UpdateMangasPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
mangas = mangas,
|
||||
)
|
||||
UpdateMangasPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
mangas = mangas,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,20 +150,22 @@ class MangaMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun fetchManga(input: FetchMangaInput): CompletableFuture<FetchMangaPayload?> {
|
||||
fun fetchManga(input: FetchMangaInput): CompletableFuture<DataFetcherResult<FetchMangaPayload?>> {
|
||||
val (clientMutationId, id) = input
|
||||
|
||||
return future {
|
||||
Manga.fetchManga(id)
|
||||
asDataFetcherResult {
|
||||
Manga.fetchManga(id)
|
||||
|
||||
val manga =
|
||||
transaction {
|
||||
MangaTable.selectAll().where { MangaTable.id eq id }.first()
|
||||
}
|
||||
FetchMangaPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
manga = MangaType(manga),
|
||||
)
|
||||
val manga =
|
||||
transaction {
|
||||
MangaTable.selectAll().where { MangaTable.id eq id }.first()
|
||||
}
|
||||
FetchMangaPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
manga = MangaType(manga),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,12 +180,14 @@ class MangaMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun setMangaMeta(input: SetMangaMetaInput): SetMangaMetaPayload? {
|
||||
fun setMangaMeta(input: SetMangaMetaInput): DataFetcherResult<SetMangaMetaPayload?> {
|
||||
val (clientMutationId, meta) = input
|
||||
|
||||
Manga.modifyMangaMeta(meta.mangaId, meta.key, meta.value)
|
||||
return asDataFetcherResult {
|
||||
Manga.modifyMangaMeta(meta.mangaId, meta.key, meta.value)
|
||||
|
||||
return SetMangaMetaPayload(clientMutationId, meta)
|
||||
SetMangaMetaPayload(clientMutationId, meta)
|
||||
}
|
||||
}
|
||||
|
||||
data class DeleteMangaMetaInput(
|
||||
@@ -195,32 +203,34 @@ class MangaMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun deleteMangaMeta(input: DeleteMangaMetaInput): DeleteMangaMetaPayload? {
|
||||
fun deleteMangaMeta(input: DeleteMangaMetaInput): DataFetcherResult<DeleteMangaMetaPayload?> {
|
||||
val (clientMutationId, mangaId, key) = input
|
||||
|
||||
val (meta, manga) =
|
||||
transaction {
|
||||
val meta =
|
||||
MangaMetaTable
|
||||
.selectAll()
|
||||
.where { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }
|
||||
.firstOrNull()
|
||||
return asDataFetcherResult {
|
||||
val (meta, manga) =
|
||||
transaction {
|
||||
val meta =
|
||||
MangaMetaTable
|
||||
.selectAll()
|
||||
.where { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }
|
||||
.firstOrNull()
|
||||
|
||||
MangaMetaTable.deleteWhere { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }
|
||||
MangaMetaTable.deleteWhere { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }
|
||||
|
||||
val manga =
|
||||
transaction {
|
||||
MangaType(MangaTable.selectAll().where { MangaTable.id eq mangaId }.first())
|
||||
}
|
||||
val manga =
|
||||
transaction {
|
||||
MangaType(MangaTable.selectAll().where { MangaTable.id eq mangaId }.first())
|
||||
}
|
||||
|
||||
if (meta != null) {
|
||||
MangaMetaType(meta)
|
||||
} else {
|
||||
null
|
||||
} to manga
|
||||
}
|
||||
if (meta != null) {
|
||||
MangaMetaType(meta)
|
||||
} else {
|
||||
null
|
||||
} to manga
|
||||
}
|
||||
|
||||
return DeleteMangaMetaPayload(clientMutationId, meta, manga)
|
||||
DeleteMangaMetaPayload(clientMutationId, meta, manga)
|
||||
}
|
||||
}
|
||||
|
||||
data class SetMangaMetasItem(
|
||||
@@ -240,41 +250,43 @@ class MangaMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun setMangaMetas(input: SetMangaMetasInput): SetMangaMetasPayload? {
|
||||
fun setMangaMetas(input: SetMangaMetasInput): DataFetcherResult<SetMangaMetasPayload?> {
|
||||
val (clientMutationId, items) = input
|
||||
|
||||
val metaByMangaId =
|
||||
items
|
||||
.flatMap { item ->
|
||||
val metaMap = item.metas.associate { it.key to it.value }
|
||||
item.mangaIds.map { mangaId -> mangaId to metaMap }
|
||||
}.groupBy({ it.first }, { it.second })
|
||||
.mapValues { (_, maps) -> maps.reduce { acc, map -> acc + map } }
|
||||
return asDataFetcherResult {
|
||||
val metaByMangaId =
|
||||
items
|
||||
.flatMap { item ->
|
||||
val metaMap = item.metas.associate { it.key to it.value }
|
||||
item.mangaIds.map { mangaId -> mangaId to metaMap }
|
||||
}.groupBy({ it.first }, { it.second })
|
||||
.mapValues { (_, maps) -> maps.reduce { acc, map -> acc + map } }
|
||||
|
||||
Manga.modifyMangasMetas(metaByMangaId)
|
||||
Manga.modifyMangasMetas(metaByMangaId)
|
||||
|
||||
val allMangaIds = metaByMangaId.keys
|
||||
val allMetaKeys = metaByMangaId.values.flatMap { it.keys }.distinct()
|
||||
val allMangaIds = metaByMangaId.keys
|
||||
val allMetaKeys = metaByMangaId.values.flatMap { it.keys }.distinct()
|
||||
|
||||
val (updatedMetas, mangas) =
|
||||
transaction {
|
||||
val updatedMetas =
|
||||
MangaMetaTable
|
||||
.selectAll()
|
||||
.where { (MangaMetaTable.ref inList allMangaIds) and (MangaMetaTable.key inList allMetaKeys) }
|
||||
.map { MangaMetaType(it) }
|
||||
val (updatedMetas, mangas) =
|
||||
transaction {
|
||||
val updatedMetas =
|
||||
MangaMetaTable
|
||||
.selectAll()
|
||||
.where { (MangaMetaTable.ref inList allMangaIds) and (MangaMetaTable.key inList allMetaKeys) }
|
||||
.map { MangaMetaType(it) }
|
||||
|
||||
val mangas =
|
||||
MangaTable
|
||||
.selectAll()
|
||||
.where { MangaTable.id inList allMangaIds }
|
||||
.map { MangaType(it) }
|
||||
.distinctBy { it.id }
|
||||
val mangas =
|
||||
MangaTable
|
||||
.selectAll()
|
||||
.where { MangaTable.id inList allMangaIds }
|
||||
.map { MangaType(it) }
|
||||
.distinctBy { it.id }
|
||||
|
||||
updatedMetas to mangas
|
||||
}
|
||||
updatedMetas to mangas
|
||||
}
|
||||
|
||||
return SetMangaMetasPayload(clientMutationId, updatedMetas, mangas)
|
||||
SetMangaMetasPayload(clientMutationId, updatedMetas, mangas)
|
||||
}
|
||||
}
|
||||
|
||||
data class DeleteMangaMetasItem(
|
||||
@@ -295,61 +307,63 @@ class MangaMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun deleteMangaMetas(input: DeleteMangaMetasInput): DeleteMangaMetasPayload? {
|
||||
fun deleteMangaMetas(input: DeleteMangaMetasInput): DataFetcherResult<DeleteMangaMetasPayload?> {
|
||||
val (clientMutationId, items) = input
|
||||
|
||||
items.forEach { item ->
|
||||
require(!item.keys.isNullOrEmpty() || !item.prefixes.isNullOrEmpty()) {
|
||||
"Either 'keys' or 'prefixes' must be provided for each item"
|
||||
return asDataFetcherResult {
|
||||
items.forEach { item ->
|
||||
require(!item.keys.isNullOrEmpty() || !item.prefixes.isNullOrEmpty()) {
|
||||
"Either 'keys' or 'prefixes' must be provided for each item"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val (allDeletedMetas, allMangaIds) =
|
||||
transaction {
|
||||
val deletedMetas = mutableListOf<MangaMetaType>()
|
||||
val mangaIds = mutableSetOf<Int>()
|
||||
val (allDeletedMetas, allMangaIds) =
|
||||
transaction {
|
||||
val deletedMetas = mutableListOf<MangaMetaType>()
|
||||
val mangaIds = mutableSetOf<Int>()
|
||||
|
||||
items.forEach { item ->
|
||||
val keyCondition: Op<Boolean>? =
|
||||
item.keys?.takeIf { it.isNotEmpty() }?.let { MangaMetaTable.key inList it }
|
||||
items.forEach { item ->
|
||||
val keyCondition: Op<Boolean>? =
|
||||
item.keys?.takeIf { it.isNotEmpty() }?.let { MangaMetaTable.key inList it }
|
||||
|
||||
val prefixCondition: Op<Boolean>? =
|
||||
item.prefixes
|
||||
?.filter { it.isNotEmpty() }
|
||||
?.map { (MangaMetaTable.key like LikePattern("$it%")) as Op<Boolean> }
|
||||
?.reduceOrNull { acc, op -> acc or op }
|
||||
val prefixCondition: Op<Boolean>? =
|
||||
item.prefixes
|
||||
?.filter { it.isNotEmpty() }
|
||||
?.map { (MangaMetaTable.key like LikePattern("$it%")) as Op<Boolean> }
|
||||
?.reduceOrNull { acc, op -> acc or op }
|
||||
|
||||
val metaKeyCondition =
|
||||
if (keyCondition != null && prefixCondition != null) {
|
||||
keyCondition or prefixCondition
|
||||
} else {
|
||||
keyCondition ?: prefixCondition!!
|
||||
}
|
||||
val metaKeyCondition =
|
||||
if (keyCondition != null && prefixCondition != null) {
|
||||
keyCondition or prefixCondition
|
||||
} else {
|
||||
keyCondition ?: prefixCondition!!
|
||||
}
|
||||
|
||||
val condition = (MangaMetaTable.ref inList item.mangaIds) and metaKeyCondition
|
||||
val condition = (MangaMetaTable.ref inList item.mangaIds) and metaKeyCondition
|
||||
|
||||
deletedMetas +=
|
||||
MangaMetaTable
|
||||
.selectAll()
|
||||
.where { condition }
|
||||
.map { MangaMetaType(it) }
|
||||
deletedMetas +=
|
||||
MangaMetaTable
|
||||
.selectAll()
|
||||
.where { condition }
|
||||
.map { MangaMetaType(it) }
|
||||
|
||||
MangaMetaTable.deleteWhere { condition }
|
||||
mangaIds += item.mangaIds
|
||||
MangaMetaTable.deleteWhere { condition }
|
||||
mangaIds += item.mangaIds
|
||||
}
|
||||
|
||||
deletedMetas to mangaIds
|
||||
}
|
||||
|
||||
deletedMetas to mangaIds
|
||||
}
|
||||
val mangas =
|
||||
transaction {
|
||||
MangaTable
|
||||
.selectAll()
|
||||
.where { MangaTable.id inList allMangaIds }
|
||||
.map { MangaType(it) }
|
||||
.distinctBy { it.id }
|
||||
}
|
||||
|
||||
val mangas =
|
||||
transaction {
|
||||
MangaTable
|
||||
.selectAll()
|
||||
.where { MangaTable.id inList allMangaIds }
|
||||
.map { MangaType(it) }
|
||||
.distinctBy { it.id }
|
||||
}
|
||||
|
||||
return DeleteMangaMetasPayload(clientMutationId, allDeletedMetas, mangas)
|
||||
DeleteMangaMetasPayload(clientMutationId, allDeletedMetas, mangas)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import org.jetbrains.exposed.v1.core.LikePattern
|
||||
import org.jetbrains.exposed.v1.core.Op
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.core.like
|
||||
import org.jetbrains.exposed.v1.core.or
|
||||
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import graphql.execution.DataFetcherResult
|
||||
import org.jetbrains.exposed.sql.LikePattern
|
||||
import org.jetbrains.exposed.sql.Op
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.like
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.or
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.global.impl.GlobalMeta
|
||||
import suwayomi.tachidesk.global.model.table.GlobalMetaTable
|
||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.types.GlobalMetaType
|
||||
import suwayomi.tachidesk.graphql.types.MetaInput
|
||||
@@ -29,12 +29,14 @@ class MetaMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun setGlobalMeta(input: SetGlobalMetaInput): SetGlobalMetaPayload? {
|
||||
fun setGlobalMeta(input: SetGlobalMetaInput): DataFetcherResult<SetGlobalMetaPayload?> {
|
||||
val (clientMutationId, meta) = input
|
||||
|
||||
GlobalMeta.modifyMeta(meta.key, meta.value)
|
||||
return asDataFetcherResult {
|
||||
GlobalMeta.modifyMeta(meta.key, meta.value)
|
||||
|
||||
return SetGlobalMetaPayload(clientMutationId, meta)
|
||||
SetGlobalMetaPayload(clientMutationId, meta)
|
||||
}
|
||||
}
|
||||
|
||||
data class DeleteGlobalMetaInput(
|
||||
@@ -48,27 +50,29 @@ class MetaMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun deleteGlobalMeta(input: DeleteGlobalMetaInput): DeleteGlobalMetaPayload? {
|
||||
fun deleteGlobalMeta(input: DeleteGlobalMetaInput): DataFetcherResult<DeleteGlobalMetaPayload?> {
|
||||
val (clientMutationId, key) = input
|
||||
|
||||
val meta =
|
||||
transaction {
|
||||
val meta =
|
||||
GlobalMetaTable
|
||||
.selectAll()
|
||||
.where { GlobalMetaTable.key eq key }
|
||||
.firstOrNull()
|
||||
return asDataFetcherResult {
|
||||
val meta =
|
||||
transaction {
|
||||
val meta =
|
||||
GlobalMetaTable
|
||||
.selectAll()
|
||||
.where { GlobalMetaTable.key eq key }
|
||||
.firstOrNull()
|
||||
|
||||
GlobalMetaTable.deleteWhere { GlobalMetaTable.key eq key }
|
||||
GlobalMetaTable.deleteWhere { GlobalMetaTable.key eq key }
|
||||
|
||||
if (meta != null) {
|
||||
GlobalMetaType(meta)
|
||||
} else {
|
||||
null
|
||||
if (meta != null) {
|
||||
GlobalMetaType(meta)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return DeleteGlobalMetaPayload(clientMutationId, meta)
|
||||
DeleteGlobalMetaPayload(clientMutationId, meta)
|
||||
}
|
||||
}
|
||||
|
||||
data class SetGlobalMetasInput(
|
||||
@@ -82,21 +86,23 @@ class MetaMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun setGlobalMetas(input: SetGlobalMetasInput): SetGlobalMetasPayload? {
|
||||
fun setGlobalMetas(input: SetGlobalMetasInput): DataFetcherResult<SetGlobalMetasPayload?> {
|
||||
val (clientMutationId, metas) = input
|
||||
|
||||
val metaMap = metas.associate { it.key to it.value }
|
||||
GlobalMeta.modifyMetas(metaMap)
|
||||
return asDataFetcherResult {
|
||||
val metaMap = metas.associate { it.key to it.value }
|
||||
GlobalMeta.modifyMetas(metaMap)
|
||||
|
||||
val updatedMetas =
|
||||
transaction {
|
||||
GlobalMetaTable
|
||||
.selectAll()
|
||||
.where { GlobalMetaTable.key inList metaMap.keys }
|
||||
.map { GlobalMetaType(it) }
|
||||
}
|
||||
val updatedMetas =
|
||||
transaction {
|
||||
GlobalMetaTable
|
||||
.selectAll()
|
||||
.where { GlobalMetaTable.key inList metaMap.keys }
|
||||
.map { GlobalMetaType(it) }
|
||||
}
|
||||
|
||||
return SetGlobalMetasPayload(clientMutationId, updatedMetas)
|
||||
SetGlobalMetasPayload(clientMutationId, updatedMetas)
|
||||
}
|
||||
}
|
||||
|
||||
data class DeleteGlobalMetasInput(
|
||||
@@ -111,41 +117,43 @@ class MetaMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun deleteGlobalMetas(input: DeleteGlobalMetasInput): DeleteGlobalMetasPayload? {
|
||||
fun deleteGlobalMetas(input: DeleteGlobalMetasInput): DataFetcherResult<DeleteGlobalMetasPayload?> {
|
||||
val (clientMutationId, keys, prefixes) = input
|
||||
|
||||
require(!keys.isNullOrEmpty() || !prefixes.isNullOrEmpty()) {
|
||||
"Either 'keys' or 'prefixes' must be provided"
|
||||
}
|
||||
|
||||
val metas =
|
||||
transaction {
|
||||
val keyCondition: Op<Boolean>? = keys?.takeIf { it.isNotEmpty() }?.let { GlobalMetaTable.key inList it }
|
||||
|
||||
val prefixCondition: Op<Boolean>? =
|
||||
prefixes
|
||||
?.filter { it.isNotEmpty() }
|
||||
?.map { (GlobalMetaTable.key like LikePattern("$it%")) as Op<Boolean> }
|
||||
?.reduceOrNull { acc, op -> acc or op }
|
||||
|
||||
val finalCondition =
|
||||
if (keyCondition != null && prefixCondition != null) {
|
||||
keyCondition or prefixCondition
|
||||
} else {
|
||||
keyCondition ?: prefixCondition!!
|
||||
}
|
||||
|
||||
val metas =
|
||||
GlobalMetaTable
|
||||
.selectAll()
|
||||
.where { finalCondition }
|
||||
.map { GlobalMetaType(it) }
|
||||
|
||||
GlobalMetaTable.deleteWhere { finalCondition }
|
||||
|
||||
metas
|
||||
return asDataFetcherResult {
|
||||
require(!keys.isNullOrEmpty() || !prefixes.isNullOrEmpty()) {
|
||||
"Either 'keys' or 'prefixes' must be provided"
|
||||
}
|
||||
|
||||
return DeleteGlobalMetasPayload(clientMutationId, metas)
|
||||
val metas =
|
||||
transaction {
|
||||
val keyCondition: Op<Boolean>? = keys?.takeIf { it.isNotEmpty() }?.let { GlobalMetaTable.key inList it }
|
||||
|
||||
val prefixCondition: Op<Boolean>? =
|
||||
prefixes
|
||||
?.filter { it.isNotEmpty() }
|
||||
?.map { (GlobalMetaTable.key like LikePattern("$it%")) as Op<Boolean> }
|
||||
?.reduceOrNull { acc, op -> acc or op }
|
||||
|
||||
val finalCondition =
|
||||
if (keyCondition != null && prefixCondition != null) {
|
||||
keyCondition or prefixCondition
|
||||
} else {
|
||||
keyCondition ?: prefixCondition!!
|
||||
}
|
||||
|
||||
val metas =
|
||||
GlobalMetaTable
|
||||
.selectAll()
|
||||
.where { finalCondition }
|
||||
.map { GlobalMetaType(it) }
|
||||
|
||||
GlobalMetaTable.deleteWhere { finalCondition }
|
||||
|
||||
metas
|
||||
}
|
||||
|
||||
DeleteGlobalMetasPayload(clientMutationId, metas)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLIgnore
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import androidx.preference.CheckBoxPreference
|
||||
@@ -7,16 +5,18 @@ import androidx.preference.EditTextPreference
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import org.jetbrains.exposed.v1.core.LikePattern
|
||||
import org.jetbrains.exposed.v1.core.Op
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.core.like
|
||||
import org.jetbrains.exposed.v1.core.or
|
||||
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import graphql.execution.DataFetcherResult
|
||||
import org.jetbrains.exposed.sql.LikePattern
|
||||
import org.jetbrains.exposed.sql.Op
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.like
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.or
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.types.FilterChange
|
||||
import suwayomi.tachidesk.graphql.types.MangaType
|
||||
@@ -47,12 +47,14 @@ class SourceMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun setSourceMeta(input: SetSourceMetaInput): SetSourceMetaPayload? {
|
||||
fun setSourceMeta(input: SetSourceMetaInput): DataFetcherResult<SetSourceMetaPayload?> {
|
||||
val (clientMutationId, meta) = input
|
||||
|
||||
Source.modifyMeta(meta.sourceId, meta.key, meta.value)
|
||||
return asDataFetcherResult {
|
||||
Source.modifyMeta(meta.sourceId, meta.key, meta.value)
|
||||
|
||||
return SetSourceMetaPayload(clientMutationId, meta)
|
||||
SetSourceMetaPayload(clientMutationId, meta)
|
||||
}
|
||||
}
|
||||
|
||||
data class DeleteSourceMetaInput(
|
||||
@@ -68,36 +70,38 @@ class SourceMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun deleteSourceMeta(input: DeleteSourceMetaInput): DeleteSourceMetaPayload? {
|
||||
fun deleteSourceMeta(input: DeleteSourceMetaInput): DataFetcherResult<DeleteSourceMetaPayload?> {
|
||||
val (clientMutationId, sourceId, key) = input
|
||||
|
||||
val (meta, source) =
|
||||
transaction {
|
||||
val meta =
|
||||
SourceMetaTable
|
||||
.selectAll()
|
||||
.where { (SourceMetaTable.ref eq sourceId) and (SourceMetaTable.key eq key) }
|
||||
.firstOrNull()
|
||||
|
||||
SourceMetaTable.deleteWhere { (SourceMetaTable.ref eq sourceId) and (SourceMetaTable.key eq key) }
|
||||
|
||||
val source =
|
||||
transaction {
|
||||
SourceTable
|
||||
return asDataFetcherResult {
|
||||
val (meta, source) =
|
||||
transaction {
|
||||
val meta =
|
||||
SourceMetaTable
|
||||
.selectAll()
|
||||
.where { SourceTable.id eq sourceId }
|
||||
.where { (SourceMetaTable.ref eq sourceId) and (SourceMetaTable.key eq key) }
|
||||
.firstOrNull()
|
||||
?.let { SourceType(it) }
|
||||
}
|
||||
|
||||
if (meta != null) {
|
||||
SourceMetaType(meta)
|
||||
} else {
|
||||
null
|
||||
} to source
|
||||
}
|
||||
SourceMetaTable.deleteWhere { (SourceMetaTable.ref eq sourceId) and (SourceMetaTable.key eq key) }
|
||||
|
||||
return DeleteSourceMetaPayload(clientMutationId, meta, source)
|
||||
val source =
|
||||
transaction {
|
||||
SourceTable
|
||||
.selectAll()
|
||||
.where { SourceTable.id eq sourceId }
|
||||
.firstOrNull()
|
||||
?.let { SourceType(it) }
|
||||
}
|
||||
|
||||
if (meta != null) {
|
||||
SourceMetaType(meta)
|
||||
} else {
|
||||
null
|
||||
} to source
|
||||
}
|
||||
|
||||
DeleteSourceMetaPayload(clientMutationId, meta, source)
|
||||
}
|
||||
}
|
||||
|
||||
data class SetSourceMetasItem(
|
||||
@@ -117,41 +121,43 @@ class SourceMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun setSourceMetas(input: SetSourceMetasInput): SetSourceMetasPayload? {
|
||||
fun setSourceMetas(input: SetSourceMetasInput): DataFetcherResult<SetSourceMetasPayload?> {
|
||||
val (clientMutationId, items) = input
|
||||
|
||||
val metaBySourceId =
|
||||
items
|
||||
.flatMap { item ->
|
||||
val metaMap = item.metas.associate { it.key to it.value }
|
||||
item.sourceIds.map { sourceId -> sourceId to metaMap }
|
||||
}.groupBy({ it.first }, { it.second })
|
||||
.mapValues { (_, maps) -> maps.reduce { acc, map -> acc + map } }
|
||||
return asDataFetcherResult {
|
||||
val metaBySourceId =
|
||||
items
|
||||
.flatMap { item ->
|
||||
val metaMap = item.metas.associate { it.key to it.value }
|
||||
item.sourceIds.map { sourceId -> sourceId to metaMap }
|
||||
}.groupBy({ it.first }, { it.second })
|
||||
.mapValues { (_, maps) -> maps.reduce { acc, map -> acc + map } }
|
||||
|
||||
Source.modifySourceMetas(metaBySourceId)
|
||||
Source.modifySourceMetas(metaBySourceId)
|
||||
|
||||
val allSourceIds = metaBySourceId.keys
|
||||
val allMetaKeys = metaBySourceId.values.flatMap { it.keys }.distinct()
|
||||
val allSourceIds = metaBySourceId.keys
|
||||
val allMetaKeys = metaBySourceId.values.flatMap { it.keys }.distinct()
|
||||
|
||||
val (updatedMetas, sources) =
|
||||
transaction {
|
||||
val updatedMetas =
|
||||
SourceMetaTable
|
||||
.selectAll()
|
||||
.where { (SourceMetaTable.ref inList allSourceIds) and (SourceMetaTable.key inList allMetaKeys) }
|
||||
.map { SourceMetaType(it) }
|
||||
val (updatedMetas, sources) =
|
||||
transaction {
|
||||
val updatedMetas =
|
||||
SourceMetaTable
|
||||
.selectAll()
|
||||
.where { (SourceMetaTable.ref inList allSourceIds) and (SourceMetaTable.key inList allMetaKeys) }
|
||||
.map { SourceMetaType(it) }
|
||||
|
||||
val sources =
|
||||
SourceTable
|
||||
.selectAll()
|
||||
.where { SourceTable.id inList allSourceIds }
|
||||
.mapNotNull { SourceType(it) }
|
||||
.distinctBy { it.id }
|
||||
val sources =
|
||||
SourceTable
|
||||
.selectAll()
|
||||
.where { SourceTable.id inList allSourceIds }
|
||||
.mapNotNull { SourceType(it) }
|
||||
.distinctBy { it.id }
|
||||
|
||||
updatedMetas to sources
|
||||
}
|
||||
updatedMetas to sources
|
||||
}
|
||||
|
||||
return SetSourceMetasPayload(clientMutationId, updatedMetas, sources)
|
||||
SetSourceMetasPayload(clientMutationId, updatedMetas, sources)
|
||||
}
|
||||
}
|
||||
|
||||
data class DeleteSourceMetasItem(
|
||||
@@ -172,62 +178,64 @@ class SourceMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun deleteSourceMetas(input: DeleteSourceMetasInput): DeleteSourceMetasPayload? {
|
||||
fun deleteSourceMetas(input: DeleteSourceMetasInput): DataFetcherResult<DeleteSourceMetasPayload?> {
|
||||
val (clientMutationId, items) = input
|
||||
|
||||
items.forEach { item ->
|
||||
require(!item.keys.isNullOrEmpty() || !item.prefixes.isNullOrEmpty()) {
|
||||
"Either 'keys' or 'prefixes' must be provided for each item"
|
||||
return asDataFetcherResult {
|
||||
items.forEach { item ->
|
||||
require(!item.keys.isNullOrEmpty() || !item.prefixes.isNullOrEmpty()) {
|
||||
"Either 'keys' or 'prefixes' must be provided for each item"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val (allDeletedMetas, allSourceIds) =
|
||||
transaction {
|
||||
val deletedMetas = mutableListOf<SourceMetaType>()
|
||||
val sourceIds = mutableSetOf<Long>()
|
||||
val (allDeletedMetas, allSourceIds) =
|
||||
transaction {
|
||||
val deletedMetas = mutableListOf<SourceMetaType>()
|
||||
val sourceIds = mutableSetOf<Long>()
|
||||
|
||||
items.forEach { item ->
|
||||
val keyCondition: Op<Boolean>? =
|
||||
item.keys?.takeIf { it.isNotEmpty() }?.let { SourceMetaTable.key inList it }
|
||||
items.forEach { item ->
|
||||
val keyCondition: Op<Boolean>? =
|
||||
item.keys?.takeIf { it.isNotEmpty() }?.let { SourceMetaTable.key inList it }
|
||||
|
||||
val prefixCondition: Op<Boolean>? =
|
||||
item.prefixes
|
||||
?.filter { it.isNotEmpty() }
|
||||
?.map { (SourceMetaTable.key like LikePattern("$it%")) as Op<Boolean> }
|
||||
?.reduceOrNull { acc, op -> acc or op }
|
||||
val prefixCondition: Op<Boolean>? =
|
||||
item.prefixes
|
||||
?.filter { it.isNotEmpty() }
|
||||
?.map { (SourceMetaTable.key like LikePattern("$it%")) as Op<Boolean> }
|
||||
?.reduceOrNull { acc, op -> acc or op }
|
||||
|
||||
val metaKeyCondition =
|
||||
if (keyCondition != null && prefixCondition != null) {
|
||||
keyCondition or prefixCondition
|
||||
} else {
|
||||
keyCondition ?: prefixCondition!!
|
||||
}
|
||||
val metaKeyCondition =
|
||||
if (keyCondition != null && prefixCondition != null) {
|
||||
keyCondition or prefixCondition
|
||||
} else {
|
||||
keyCondition ?: prefixCondition!!
|
||||
}
|
||||
|
||||
val condition = (SourceMetaTable.ref inList item.sourceIds) and metaKeyCondition
|
||||
val condition = (SourceMetaTable.ref inList item.sourceIds) and metaKeyCondition
|
||||
|
||||
deletedMetas +=
|
||||
SourceMetaTable
|
||||
.selectAll()
|
||||
.where { condition }
|
||||
.map { SourceMetaType(it) }
|
||||
deletedMetas +=
|
||||
SourceMetaTable
|
||||
.selectAll()
|
||||
.where { condition }
|
||||
.map { SourceMetaType(it) }
|
||||
|
||||
SourceMetaTable.deleteWhere { condition }
|
||||
sourceIds += item.sourceIds
|
||||
SourceMetaTable.deleteWhere { condition }
|
||||
sourceIds += item.sourceIds
|
||||
}
|
||||
|
||||
deletedMetas to sourceIds
|
||||
}
|
||||
|
||||
deletedMetas to sourceIds
|
||||
}
|
||||
val sources =
|
||||
transaction {
|
||||
SourceTable
|
||||
.selectAll()
|
||||
.where { SourceTable.id inList allSourceIds }
|
||||
.mapNotNull { SourceType(it) }
|
||||
.distinctBy { it.id }
|
||||
}
|
||||
|
||||
val sources =
|
||||
transaction {
|
||||
SourceTable
|
||||
.selectAll()
|
||||
.where { SourceTable.id inList allSourceIds }
|
||||
.mapNotNull { SourceType(it) }
|
||||
.distinctBy { it.id }
|
||||
}
|
||||
|
||||
return DeleteSourceMetasPayload(clientMutationId, allDeletedMetas, sources)
|
||||
DeleteSourceMetasPayload(clientMutationId, allDeletedMetas, sources)
|
||||
}
|
||||
}
|
||||
|
||||
enum class FetchSourceMangaType {
|
||||
@@ -252,48 +260,50 @@ class SourceMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun fetchSourceManga(input: FetchSourceMangaInput): CompletableFuture<FetchSourceMangaPayload?> {
|
||||
fun fetchSourceManga(input: FetchSourceMangaInput): CompletableFuture<DataFetcherResult<FetchSourceMangaPayload?>> {
|
||||
val (clientMutationId, sourceId, type, page, query, filters) = input
|
||||
|
||||
return future {
|
||||
val source = GetCatalogueSource.getCatalogueSourceOrNull(sourceId)!!
|
||||
val mangasPage =
|
||||
when (type) {
|
||||
FetchSourceMangaType.SEARCH -> {
|
||||
source.getSearchManga(
|
||||
page = page,
|
||||
query = query.orEmpty(),
|
||||
filters = updateFilterList(source, filters),
|
||||
)
|
||||
asDataFetcherResult {
|
||||
val source = GetCatalogueSource.getCatalogueSourceOrNull(sourceId)!!
|
||||
val mangasPage =
|
||||
when (type) {
|
||||
FetchSourceMangaType.SEARCH -> {
|
||||
source.getSearchManga(
|
||||
page = page,
|
||||
query = query.orEmpty(),
|
||||
filters = updateFilterList(source, filters),
|
||||
)
|
||||
}
|
||||
|
||||
FetchSourceMangaType.POPULAR -> {
|
||||
source.getPopularManga(page)
|
||||
}
|
||||
|
||||
FetchSourceMangaType.LATEST -> {
|
||||
if (!source.supportsLatest) throw Exception("Source does not support latest")
|
||||
source.getLatestUpdates(page)
|
||||
}
|
||||
}
|
||||
|
||||
FetchSourceMangaType.POPULAR -> {
|
||||
source.getPopularManga(page)
|
||||
val mangaIds = mangasPage.insertOrUpdate(sourceId)
|
||||
|
||||
val mangas =
|
||||
transaction {
|
||||
MangaTable
|
||||
.selectAll()
|
||||
.where { MangaTable.id inList mangaIds }
|
||||
.map { MangaType(it) }
|
||||
}.sortedBy {
|
||||
mangaIds.indexOf(it.id)
|
||||
}
|
||||
|
||||
FetchSourceMangaType.LATEST -> {
|
||||
if (!source.supportsLatest) throw Exception("Source does not support latest")
|
||||
source.getLatestUpdates(page)
|
||||
}
|
||||
}
|
||||
|
||||
val mangaIds = mangasPage.insertOrUpdate(sourceId)
|
||||
|
||||
val mangas =
|
||||
transaction {
|
||||
MangaTable
|
||||
.selectAll()
|
||||
.where { MangaTable.id inList mangaIds }
|
||||
.map { MangaType(it) }
|
||||
}.sortedBy {
|
||||
mangaIds.indexOf(it.id)
|
||||
}
|
||||
|
||||
FetchSourceMangaPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
mangas = mangas,
|
||||
hasNextPage = mangasPage.hasNextPage,
|
||||
)
|
||||
FetchSourceMangaPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
mangas = mangas,
|
||||
hasNextPage = mangasPage.hasNextPage,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,27 +329,29 @@ class SourceMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun updateSourcePreference(input: UpdateSourcePreferenceInput): UpdateSourcePreferencePayload? {
|
||||
fun updateSourcePreference(input: UpdateSourcePreferenceInput): DataFetcherResult<UpdateSourcePreferencePayload?> {
|
||||
val (clientMutationId, sourceId, change) = input
|
||||
|
||||
Source.setSourcePreference(sourceId, change.position, "") { preference ->
|
||||
when (preference) {
|
||||
is SwitchPreferenceCompat -> change.switchState
|
||||
is CheckBoxPreference -> change.checkBoxState
|
||||
is EditTextPreference -> change.editTextState
|
||||
is ListPreference -> change.listState
|
||||
is MultiSelectListPreference -> change.multiSelectState?.toSet()
|
||||
else -> throw RuntimeException("sealed class cannot have more subtypes!")
|
||||
} ?: throw Exception("Expected change to ${preference::class.simpleName}")
|
||||
}
|
||||
return asDataFetcherResult {
|
||||
Source.setSourcePreference(sourceId, change.position, "") { preference ->
|
||||
when (preference) {
|
||||
is SwitchPreferenceCompat -> change.switchState
|
||||
is CheckBoxPreference -> change.checkBoxState
|
||||
is EditTextPreference -> change.editTextState
|
||||
is ListPreference -> change.listState
|
||||
is MultiSelectListPreference -> change.multiSelectState?.toSet()
|
||||
else -> throw RuntimeException("sealed class cannot have more subtypes!")
|
||||
} ?: throw Exception("Expected change to ${preference::class.simpleName}")
|
||||
}
|
||||
|
||||
return UpdateSourcePreferencePayload(
|
||||
clientMutationId = clientMutationId,
|
||||
preferences = Source.getSourcePreferencesRaw(sourceId).map { preferenceOf(it) },
|
||||
source =
|
||||
transaction {
|
||||
SourceType(SourceTable.selectAll().where { SourceTable.id eq sourceId }.first())!!
|
||||
},
|
||||
)
|
||||
UpdateSourcePreferencePayload(
|
||||
clientMutationId = clientMutationId,
|
||||
preferences = Source.getSourcePreferencesRaw(sourceId).map { preferenceOf(it) },
|
||||
source =
|
||||
transaction {
|
||||
SourceType(SourceTable.selectAll().where { SourceTable.id eq sourceId }.first())!!
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLDescription
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import graphql.execution.DataFetcherResult
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.types.TrackRecordType
|
||||
import suwayomi.tachidesk.graphql.types.TrackerType
|
||||
@@ -223,22 +222,24 @@ class TrackMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun trackProgress(input: TrackProgressInput): CompletableFuture<TrackProgressPayload?> {
|
||||
fun trackProgress(input: TrackProgressInput): CompletableFuture<DataFetcherResult<TrackProgressPayload?>> {
|
||||
val (clientMutationId, mangaId) = input
|
||||
|
||||
return future {
|
||||
Track.trackChapter(mangaId)
|
||||
val trackRecords =
|
||||
transaction {
|
||||
TrackRecordTable
|
||||
.selectAll()
|
||||
.where { TrackRecordTable.mangaId eq mangaId }
|
||||
.toList()
|
||||
}
|
||||
TrackProgressPayload(
|
||||
clientMutationId,
|
||||
trackRecords.map { TrackRecordType(it) },
|
||||
)
|
||||
asDataFetcherResult {
|
||||
Track.trackChapter(mangaId)
|
||||
val trackRecords =
|
||||
transaction {
|
||||
TrackRecordTable
|
||||
.selectAll()
|
||||
.where { TrackRecordTable.mangaId eq mangaId }
|
||||
.toList()
|
||||
}
|
||||
TrackProgressPayload(
|
||||
clientMutationId,
|
||||
trackRecords.map { TrackRecordType(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import graphql.execution.DataFetcherResult
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.types.LibraryUpdateStatus
|
||||
import suwayomi.tachidesk.graphql.types.UpdateStatus
|
||||
@@ -28,7 +28,7 @@ class UpdateMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun updateLibrary(input: UpdateLibraryInput): CompletableFuture<UpdateLibraryPayload?> {
|
||||
fun updateLibrary(input: UpdateLibraryInput): CompletableFuture<DataFetcherResult<UpdateLibraryPayload?>> {
|
||||
updater.addCategoriesToUpdateQueue(
|
||||
Category.getCategoryList().filter { input.categories?.contains(it.id) ?: true },
|
||||
clear = true,
|
||||
@@ -36,15 +36,17 @@ class UpdateMutation {
|
||||
)
|
||||
|
||||
return future {
|
||||
UpdateLibraryPayload(
|
||||
input.clientMutationId,
|
||||
updateStatus =
|
||||
withTimeout(30.seconds) {
|
||||
LibraryUpdateStatus(
|
||||
updater.updates.first(),
|
||||
)
|
||||
},
|
||||
)
|
||||
asDataFetcherResult {
|
||||
UpdateLibraryPayload(
|
||||
input.clientMutationId,
|
||||
updateStatus =
|
||||
withTimeout(30.seconds) {
|
||||
LibraryUpdateStatus(
|
||||
updater.updates.first(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +60,7 @@ class UpdateMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun updateLibraryManga(input: UpdateLibraryMangaInput): CompletableFuture<UpdateLibraryMangaPayload?> {
|
||||
fun updateLibraryManga(input: UpdateLibraryMangaInput): CompletableFuture<DataFetcherResult<UpdateLibraryMangaPayload?>> {
|
||||
updateLibrary(
|
||||
UpdateLibraryInput(
|
||||
clientMutationId = input.clientMutationId,
|
||||
@@ -67,13 +69,15 @@ class UpdateMutation {
|
||||
)
|
||||
|
||||
return future {
|
||||
UpdateLibraryMangaPayload(
|
||||
input.clientMutationId,
|
||||
updateStatus =
|
||||
withTimeout(30.seconds) {
|
||||
UpdateStatus(updater.status.first())
|
||||
},
|
||||
)
|
||||
asDataFetcherResult {
|
||||
UpdateLibraryMangaPayload(
|
||||
input.clientMutationId,
|
||||
updateStatus =
|
||||
withTimeout(30.seconds) {
|
||||
UpdateStatus(updater.status.first())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +92,7 @@ class UpdateMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun updateCategoryManga(input: UpdateCategoryMangaInput): CompletableFuture<UpdateCategoryMangaPayload?> {
|
||||
fun updateCategoryManga(input: UpdateCategoryMangaInput): CompletableFuture<DataFetcherResult<UpdateCategoryMangaPayload?>> {
|
||||
updateLibrary(
|
||||
UpdateLibraryInput(
|
||||
clientMutationId = input.clientMutationId,
|
||||
@@ -97,13 +101,15 @@ class UpdateMutation {
|
||||
)
|
||||
|
||||
return future {
|
||||
UpdateCategoryMangaPayload(
|
||||
input.clientMutationId,
|
||||
updateStatus =
|
||||
withTimeout(30.seconds) {
|
||||
UpdateStatus(updater.status.first())
|
||||
},
|
||||
)
|
||||
asDataFetcherResult {
|
||||
UpdateCategoryMangaPayload(
|
||||
input.clientMutationId,
|
||||
updateStatus =
|
||||
withTimeout(30.seconds) {
|
||||
UpdateStatus(updater.status.first())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import suwayomi.tachidesk.global.impl.util.Jwt
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.server.JavalinSetup.Attribute
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
|
||||
@@ -10,13 +10,13 @@ package suwayomi.tachidesk.graphql.queries
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import org.jetbrains.exposed.v1.core.Column
|
||||
import org.jetbrains.exposed.v1.core.Op
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.core.greater
|
||||
import org.jetbrains.exposed.v1.core.less
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.Column
|
||||
import org.jetbrains.exposed.sql.Op
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.less
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.Filter
|
||||
|
||||
@@ -10,14 +10,14 @@ package suwayomi.tachidesk.graphql.queries
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import org.jetbrains.exposed.v1.core.Column
|
||||
import org.jetbrains.exposed.v1.core.Op
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.core.greater
|
||||
import org.jetbrains.exposed.v1.core.less
|
||||
import org.jetbrains.exposed.v1.jdbc.andWhere
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.Column
|
||||
import org.jetbrains.exposed.sql.Op
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.less
|
||||
import org.jetbrains.exposed.sql.andWhere
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.DoubleFilter
|
||||
|
||||
@@ -11,14 +11,14 @@ import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import org.jetbrains.exposed.v1.core.Column
|
||||
import org.jetbrains.exposed.v1.core.Op
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.core.greater
|
||||
import org.jetbrains.exposed.v1.core.less
|
||||
import org.jetbrains.exposed.v1.core.neq
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.Column
|
||||
import org.jetbrains.exposed.sql.Op
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.less
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.neq
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.Filter
|
||||
|
||||
@@ -10,17 +10,12 @@ package suwayomi.tachidesk.graphql.queries
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import org.jetbrains.exposed.v1.core.Column
|
||||
import org.jetbrains.exposed.v1.core.Op
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.core.greater
|
||||
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.like
|
||||
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.sql.Column
|
||||
import org.jetbrains.exposed.sql.Op
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.less
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.ComparableScalarFilter
|
||||
@@ -245,16 +240,13 @@ class MangaQuery {
|
||||
): MangaNodeList {
|
||||
val queryResults =
|
||||
transaction {
|
||||
val mangaIdsQuery =
|
||||
val res =
|
||||
MangaTable
|
||||
.leftJoin(CategoryMangaTable)
|
||||
.select(MangaTable.id)
|
||||
.withDistinct()
|
||||
.select(MangaTable.columns)
|
||||
.withDistinctOn(MangaTable.id)
|
||||
|
||||
mangaIdsQuery.applyOps(condition, filter)
|
||||
|
||||
val res =
|
||||
MangaTable.selectAll().where { MangaTable.id inSubQuery mangaIdsQuery }
|
||||
res.applyOps(condition, filter)
|
||||
|
||||
if (order != null || orderBy != null || (last != null || before != null)) {
|
||||
val baseSort = listOf(MangaOrder(MangaOrderBy.ID, SortOrder.ASC))
|
||||
|
||||
@@ -10,13 +10,13 @@ package suwayomi.tachidesk.graphql.queries
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import org.jetbrains.exposed.v1.core.Column
|
||||
import org.jetbrains.exposed.v1.core.Op
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.core.greater
|
||||
import org.jetbrains.exposed.v1.core.less
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.Column
|
||||
import org.jetbrains.exposed.sql.Op
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.less
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.global.model.table.GlobalMetaTable
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.queries.filter.Filter
|
||||
|
||||
@@ -10,13 +10,13 @@ package suwayomi.tachidesk.graphql.queries
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import org.jetbrains.exposed.v1.core.Column
|
||||
import org.jetbrains.exposed.v1.core.Op
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.core.greater
|
||||
import org.jetbrains.exposed.v1.core.less
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.Column
|
||||
import org.jetbrains.exposed.sql.Op
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.less
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.Filter
|
||||
|
||||
@@ -3,13 +3,13 @@ package suwayomi.tachidesk.graphql.queries
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import org.jetbrains.exposed.v1.core.Column
|
||||
import org.jetbrains.exposed.v1.core.Op
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.core.greater
|
||||
import org.jetbrains.exposed.v1.core.less
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.Column
|
||||
import org.jetbrains.exposed.sql.Op
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.less
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.DoubleFilter
|
||||
|
||||
@@ -1,33 +1,21 @@
|
||||
package suwayomi.tachidesk.graphql.queries.filter
|
||||
|
||||
import org.jetbrains.exposed.v1.core.Column
|
||||
import org.jetbrains.exposed.v1.core.ComparisonOp
|
||||
import org.jetbrains.exposed.v1.core.Expression
|
||||
import org.jetbrains.exposed.v1.core.ExpressionWithColumnType
|
||||
import org.jetbrains.exposed.v1.core.LikePattern
|
||||
import org.jetbrains.exposed.v1.core.Op
|
||||
import org.jetbrains.exposed.v1.core.QueryBuilder
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.core.greater
|
||||
import org.jetbrains.exposed.v1.core.greaterEq
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.core.isNotNull
|
||||
import org.jetbrains.exposed.v1.core.isNull
|
||||
import org.jetbrains.exposed.v1.core.less
|
||||
import org.jetbrains.exposed.v1.core.lessEq
|
||||
import org.jetbrains.exposed.v1.core.like
|
||||
import org.jetbrains.exposed.v1.core.neq
|
||||
import org.jetbrains.exposed.v1.core.not
|
||||
import org.jetbrains.exposed.v1.core.notInList
|
||||
import org.jetbrains.exposed.v1.core.notLike
|
||||
import org.jetbrains.exposed.v1.core.or
|
||||
import org.jetbrains.exposed.v1.core.stringParam
|
||||
import org.jetbrains.exposed.v1.core.upperCase
|
||||
import org.jetbrains.exposed.v1.core.wrap
|
||||
import org.jetbrains.exposed.v1.jdbc.Query
|
||||
import org.jetbrains.exposed.v1.jdbc.andWhere
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
import org.jetbrains.exposed.sql.Column
|
||||
import org.jetbrains.exposed.sql.ComparisonOp
|
||||
import org.jetbrains.exposed.sql.Expression
|
||||
import org.jetbrains.exposed.sql.ExpressionWithColumnType
|
||||
import org.jetbrains.exposed.sql.LikePattern
|
||||
import org.jetbrains.exposed.sql.Op
|
||||
import org.jetbrains.exposed.sql.Query
|
||||
import org.jetbrains.exposed.sql.QueryBuilder
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.andWhere
|
||||
import org.jetbrains.exposed.sql.not
|
||||
import org.jetbrains.exposed.sql.or
|
||||
import org.jetbrains.exposed.sql.stringParam
|
||||
import org.jetbrains.exposed.sql.upperCase
|
||||
|
||||
class ILikeEscapeOp(
|
||||
expr1: Expression<*>,
|
||||
@@ -100,7 +88,9 @@ class DistinctFromOp(
|
||||
): DistinctFromOp =
|
||||
DistinctFromOp(
|
||||
expression,
|
||||
expression.wrap(t),
|
||||
with(SqlExpressionBuilder) {
|
||||
expression.wrap(t)
|
||||
},
|
||||
false,
|
||||
)
|
||||
|
||||
@@ -110,7 +100,9 @@ class DistinctFromOp(
|
||||
): DistinctFromOp =
|
||||
DistinctFromOp(
|
||||
expression,
|
||||
expression.wrap(t),
|
||||
with(SqlExpressionBuilder) {
|
||||
expression.wrap(t)
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
@@ -120,7 +112,9 @@ class DistinctFromOp(
|
||||
): DistinctFromOp =
|
||||
DistinctFromOp(
|
||||
expression,
|
||||
expression.wrap(t),
|
||||
with(SqlExpressionBuilder) {
|
||||
expression.wrap(t)
|
||||
},
|
||||
false,
|
||||
)
|
||||
|
||||
@@ -130,7 +124,9 @@ class DistinctFromOp(
|
||||
): DistinctFromOp =
|
||||
DistinctFromOp(
|
||||
expression,
|
||||
expression.wrap(t),
|
||||
with(SqlExpressionBuilder) {
|
||||
expression.wrap(t)
|
||||
},
|
||||
true,
|
||||
)
|
||||
}
|
||||
@@ -435,7 +431,7 @@ fun <T : String, S : T?> andFilterWithCompareString(
|
||||
|
||||
opAnd.andWhere(filter.isNull) { if (it) column.isNull() else column.isNotNull() }
|
||||
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,
|
||||
@@ -455,36 +451,36 @@ fun <T : String, S : T?> andFilterWithCompareString(
|
||||
opAnd.andWhere(filter.greaterThanOrEqualTo) { column greaterEq 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) {
|
||||
ILikeEscapeOp.iLike(column, "%$it%")
|
||||
}
|
||||
opAnd.andNotWhere(filter.notIncludesInsensitive, filter.notIncludesInsensitiveAll, filter.notIncludesInsensitiveAny) {
|
||||
opAnd.andWhere(filter.notIncludesInsensitive, filter.notIncludesInsensitiveAll, filter.notIncludesInsensitiveAny) {
|
||||
ILikeEscapeOp.iNotLike(column, "%$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) {
|
||||
ILikeEscapeOp.iLike(column, "$it%")
|
||||
}
|
||||
opAnd.andNotWhere(filter.notStartsWithInsensitive, filter.notStartsWithInsensitiveAll, filter.notStartsWithInsensitiveAny) {
|
||||
opAnd.andWhere(filter.notStartsWithInsensitive, filter.notStartsWithInsensitiveAll, filter.notStartsWithInsensitiveAny) {
|
||||
ILikeEscapeOp.iNotLike(column, "$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) {
|
||||
ILikeEscapeOp.iLike(column, "%$it")
|
||||
}
|
||||
opAnd.andNotWhere(filter.notEndsWithInsensitive, filter.notEndsWithInsensitiveAll, filter.notEndsWithInsensitiveAny) {
|
||||
opAnd.andWhere(filter.notEndsWithInsensitive, filter.notEndsWithInsensitiveAll, filter.notEndsWithInsensitiveAny) {
|
||||
ILikeEscapeOp.iNotLike(column, "%$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.andNotWhere(filter.notLikeInsensitive, filter.notLikeInsensitiveAll, filter.notLikeInsensitiveAny) {
|
||||
opAnd.andWhere(filter.notLikeInsensitive, filter.notLikeInsensitiveAll, filter.notLikeInsensitiveAny) {
|
||||
ILikeEscapeOp.iNotLike(column, it)
|
||||
}
|
||||
|
||||
@@ -509,75 +505,48 @@ class OpAnd(
|
||||
) {
|
||||
fun <T> andWhere(
|
||||
value: T?,
|
||||
andPart: (T & Any) -> Op<Boolean>,
|
||||
andPart: SqlExpressionBuilder.(T & Any) -> Op<Boolean>,
|
||||
) {
|
||||
value ?: return
|
||||
val expr = andPart(value)
|
||||
val expr = Op.build { andPart(value) }
|
||||
op = if (op == null) expr else (op!! and expr)
|
||||
}
|
||||
|
||||
fun <T : Any> andWhere(
|
||||
values: List<T>?,
|
||||
andPart: (List<T>) -> Op<Boolean>,
|
||||
andPart: SqlExpressionBuilder.(List<T>) -> Op<Boolean>,
|
||||
) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return andWhere(values as T?, andPart as (Any) -> Op<Boolean>)
|
||||
return andWhere(values as T?, andPart as SqlExpressionBuilder.(Any) -> Op<Boolean>)
|
||||
}
|
||||
|
||||
fun <T : Any> andWhere(
|
||||
valueDefault: T?,
|
||||
valueAll: List<T>?,
|
||||
valueAny: List<T>?,
|
||||
expr: (T) -> Op<Boolean>,
|
||||
expr: SqlExpressionBuilder.(T) -> Op<Boolean>,
|
||||
) {
|
||||
andWhere(valueDefault, expr)
|
||||
andWhereAll(valueAll, 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(
|
||||
values: List<T>?,
|
||||
andPart: (T) -> Op<Boolean>,
|
||||
andPart: SqlExpressionBuilder.(T) -> Op<Boolean>,
|
||||
) {
|
||||
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(
|
||||
values: List<T>?,
|
||||
andPart: (T) -> Op<Boolean>,
|
||||
andPart: SqlExpressionBuilder.(T) -> Op<Boolean>,
|
||||
) {
|
||||
values ?: return
|
||||
val expr = values.map { andPart(it) }.reduceOrNull { acc, op -> acc or op } ?: return
|
||||
val expr = values.map { Op.build { andPart(it) } }.reduce { acc, op -> acc or op }
|
||||
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(
|
||||
value: T?,
|
||||
column: Column<T>,
|
||||
@@ -605,7 +574,7 @@ fun <T : Comparable<T>, S : T?> andFilterWithCompare(
|
||||
opAnd.andWhere(filter.isNull) { if (it) column.isNull() else column.isNotNull() }
|
||||
|
||||
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.notDistinctFrom) { DistinctFromOp.notDistinctFrom(column, it as S) }
|
||||
if (!filter.`in`.isNullOrEmpty()) {
|
||||
@@ -633,7 +602,7 @@ fun <T : Comparable<T>> andFilterWithCompareEntity(
|
||||
opAnd.andWhere(filter.isNull) { if (it) column.isNull() else column.isNotNull() }
|
||||
|
||||
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.notDistinctFrom) { DistinctFromOp.notDistinctFrom(column, it) }
|
||||
if (!filter.`in`.isNullOrEmpty()) {
|
||||
|
||||
@@ -11,20 +11,19 @@ import com.expediagroup.graphql.server.execution.GraphQLRequestParser
|
||||
import com.expediagroup.graphql.server.types.GraphQLBatchRequest
|
||||
import com.expediagroup.graphql.server.types.GraphQLRequest
|
||||
import com.expediagroup.graphql.server.types.GraphQLServerRequest
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.javalin.http.Context
|
||||
import io.javalin.http.UploadedFile
|
||||
import io.javalin.json.JavalinJackson
|
||||
import io.javalin.json.fromJsonStream
|
||||
import io.javalin.json.fromJsonString
|
||||
import java.io.IOException
|
||||
|
||||
class JavalinGraphQLRequestParser : GraphQLRequestParser<Context> {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
val jsonMapper = JavalinJackson()
|
||||
|
||||
@Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
|
||||
override suspend fun parseRequest(context: Context): GraphQLServerRequest? {
|
||||
return try {
|
||||
val jsonMapper = context.jsonMapper()
|
||||
val contentType = context.contentType()
|
||||
val formParam =
|
||||
if (
|
||||
@@ -78,8 +77,7 @@ class JavalinGraphQLRequestParser : GraphQLRequestParser<Context> {
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
logger.error(e) { "Error when parsing request" }
|
||||
} catch (_: IOException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,13 @@ package suwayomi.tachidesk.graphql.server
|
||||
import com.expediagroup.graphql.generator.execution.FlowSubscriptionExecutionStrategy
|
||||
import com.expediagroup.graphql.server.execution.GraphQLRequestHandler
|
||||
import com.expediagroup.graphql.server.execution.GraphQLServer
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import graphql.ExceptionWhileDataFetching
|
||||
import graphql.GraphQL
|
||||
import graphql.execution.AsyncExecutionStrategy
|
||||
import graphql.execution.DataFetcherExceptionHandler
|
||||
import graphql.execution.DataFetcherExceptionHandlerResult
|
||||
import graphql.schema.idl.RuntimeWiring
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.javalin.http.Context
|
||||
import io.javalin.websocket.WsCloseContext
|
||||
@@ -25,7 +27,6 @@ import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import suwayomi.tachidesk.graphql.server.subscriptions.ApolloSubscriptionProtocolHandler
|
||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||
import tools.jackson.module.kotlin.jacksonObjectMapper
|
||||
|
||||
class TachideskGraphQLServer(
|
||||
requestParser: JavalinGraphQLRequestParser,
|
||||
|
||||
@@ -58,7 +58,7 @@ private class GraphqlCursorCoercing : Coercing<Cursor, String> {
|
||||
),
|
||||
)
|
||||
}
|
||||
return Cursor(input.value!!)
|
||||
return Cursor(input.value)
|
||||
}
|
||||
|
||||
private fun valueToLiteralImpl(input: Any): StringValue = StringValue.newStringValue(input.toString()).build()
|
||||
|
||||
@@ -71,7 +71,7 @@ private class GraphqlDurationAsStringCoercing : Coercing<Duration, String> {
|
||||
)
|
||||
}
|
||||
return try {
|
||||
Duration.parse(input.value!!)
|
||||
Duration.parse(input.value)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw CoercingParseLiteralException(
|
||||
"Invalid duration format: ${input.value}. Expected ISO-8601 duration string (e.g., 'PT30M', 'P1D')",
|
||||
|
||||
@@ -53,7 +53,7 @@ private class GraphqlLongAsStringCoercing : Coercing<Long, String> {
|
||||
),
|
||||
)
|
||||
}
|
||||
return input.value!!.toLong()
|
||||
return input.value.toLong()
|
||||
}
|
||||
|
||||
private fun valueToLiteralImpl(input: Any): StringValue = StringValue.newStringValue(input.toString()).build()
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
package suwayomi.tachidesk.graphql.server.primitives
|
||||
|
||||
import org.jetbrains.exposed.v1.core.Column
|
||||
import org.jetbrains.exposed.v1.core.Op
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.core.greater
|
||||
import org.jetbrains.exposed.v1.core.less
|
||||
import org.jetbrains.exposed.v1.core.or
|
||||
import org.jetbrains.exposed.v1.jdbc.Query
|
||||
import org.jetbrains.exposed.v1.jdbc.andWhere
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
import org.jetbrains.exposed.sql.Column
|
||||
import org.jetbrains.exposed.sql.Op
|
||||
import org.jetbrains.exposed.sql.Query
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.less
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.andWhere
|
||||
import org.jetbrains.exposed.sql.or
|
||||
|
||||
interface OrderBy<T> {
|
||||
val column: Column<*>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package suwayomi.tachidesk.graphql.server.primitives
|
||||
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
|
||||
data class QueryResults<T>(
|
||||
val total: Long,
|
||||
|
||||
@@ -9,6 +9,9 @@ package suwayomi.tachidesk.graphql.server.subscriptions
|
||||
|
||||
import com.expediagroup.graphql.server.execution.GraphQLRequestHandler
|
||||
import com.expediagroup.graphql.server.types.GraphQLRequest
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.convertValue
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.javalin.http.Header
|
||||
import io.javalin.websocket.WsContext
|
||||
@@ -38,9 +41,6 @@ import suwayomi.tachidesk.server.JavalinSetup.Attribute
|
||||
import suwayomi.tachidesk.server.JavalinSetup.getAttributeOrSet
|
||||
import suwayomi.tachidesk.server.user.UserType
|
||||
import suwayomi.tachidesk.server.user.getUserFromToken
|
||||
import tools.jackson.databind.ObjectMapper
|
||||
import tools.jackson.module.kotlin.convertValue
|
||||
import tools.jackson.module.kotlin.readValue
|
||||
|
||||
/**
|
||||
* Implementation of the `graphql-transport-ws` protocol defined by Denis Badurina
|
||||
|
||||
@@ -9,7 +9,7 @@ package suwayomi.tachidesk.graphql.types
|
||||
|
||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Edge
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Node
|
||||
|
||||
@@ -9,7 +9,7 @@ package suwayomi.tachidesk.graphql.types
|
||||
|
||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Edge
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Node
|
||||
|
||||
@@ -9,7 +9,7 @@ package suwayomi.tachidesk.graphql.types
|
||||
|
||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Edge
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Node
|
||||
|
||||
@@ -10,7 +10,7 @@ package suwayomi.tachidesk.graphql.types
|
||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
import suwayomi.tachidesk.graphql.cache.CustomCacheMap
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Edge
|
||||
|
||||
@@ -2,7 +2,7 @@ package suwayomi.tachidesk.graphql.types
|
||||
|
||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
import suwayomi.tachidesk.global.model.table.GlobalMetaTable
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Edge
|
||||
|
||||
@@ -13,9 +13,8 @@ import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Edge
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Node
|
||||
|
||||
@@ -2,7 +2,7 @@ package suwayomi.tachidesk.graphql.types
|
||||
|
||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Edge
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Node
|
||||
|
||||
@@ -12,9 +12,8 @@ import io.javalin.http.HttpStatus
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.manga.impl.CategoryManga
|
||||
import suwayomi.tachidesk.manga.impl.Chapter
|
||||
import suwayomi.tachidesk.manga.impl.ChapterDownloadHelper
|
||||
|
||||
@@ -7,27 +7,25 @@ package suwayomi.tachidesk.manga.impl
|
||||
* 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 org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.core.isNull
|
||||
import org.jetbrains.exposed.v1.core.neq
|
||||
import org.jetbrains.exposed.v1.core.statements.BatchUpdateStatement
|
||||
import org.jetbrains.exposed.v1.jdbc.andWhere
|
||||
import org.jetbrains.exposed.v1.jdbc.batchInsert
|
||||
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.andWhere
|
||||
import org.jetbrains.exposed.sql.batchInsert
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
|
||||
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
|
||||
import suwayomi.tachidesk.manga.model.table.CategoryMetaTable
|
||||
import suwayomi.tachidesk.manga.model.table.CategoryTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
import suwayomi.tachidesk.manga.model.table.toDataClass
|
||||
import kotlin.collections.component1
|
||||
import kotlin.collections.orEmpty
|
||||
|
||||
object Category {
|
||||
/**
|
||||
@@ -250,14 +248,13 @@ object Category {
|
||||
}
|
||||
|
||||
if (existingMetaByMetaId.isNotEmpty()) {
|
||||
BatchUpdateStatement(CategoryMetaTable)
|
||||
.apply {
|
||||
existingMetaByMetaId.forEach { (metaId, entry) ->
|
||||
addBatch(EntityID(metaId, CategoryMetaTable))
|
||||
this[CategoryMetaTable.value] = entry.value
|
||||
}
|
||||
}.toExecutable()
|
||||
.execute(this@transaction)
|
||||
BatchUpdateStatement(CategoryMetaTable).apply {
|
||||
existingMetaByMetaId.forEach { (metaId, entry) ->
|
||||
addBatch(EntityID(metaId, CategoryMetaTable))
|
||||
this[CategoryMetaTable.value] = entry.value
|
||||
}
|
||||
execute(this@transaction)
|
||||
}
|
||||
}
|
||||
|
||||
if (newMetaByCategoryId.isNotEmpty()) {
|
||||
|
||||
@@ -7,22 +7,19 @@ package suwayomi.tachidesk.manga.impl
|
||||
* 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 org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.core.alias
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.count
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.core.isNull
|
||||
import org.jetbrains.exposed.v1.core.leftJoin
|
||||
import org.jetbrains.exposed.v1.core.max
|
||||
import org.jetbrains.exposed.v1.core.wrapAsExpression
|
||||
import org.jetbrains.exposed.v1.jdbc.batchInsert
|
||||
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
||||
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.sql.ResultRow
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.alias
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.batchInsert
|
||||
import org.jetbrains.exposed.sql.count
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.leftJoin
|
||||
import org.jetbrains.exposed.sql.max
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.wrapAsExpression
|
||||
import suwayomi.tachidesk.manga.impl.Category.DEFAULT_CATEGORY_ID
|
||||
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
|
||||
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
|
||||
|
||||
@@ -17,21 +17,17 @@ import io.github.reactivecircus.cache4k.Cache
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.core.greater
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.core.less
|
||||
import org.jetbrains.exposed.v1.core.statements.BatchUpdateStatement
|
||||
import org.jetbrains.exposed.v1.jdbc.batchInsert
|
||||
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
||||
import org.jetbrains.exposed.v1.jdbc.select
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
import org.jetbrains.exposed.sql.Op
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.batchInsert
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.manga.impl.Manga.getManga
|
||||
import suwayomi.tachidesk.manga.impl.download.DownloadManager
|
||||
import suwayomi.tachidesk.manga.impl.download.DownloadManager.EnqueueInput
|
||||
@@ -236,7 +232,7 @@ object Chapter {
|
||||
val deletedChapterNumbers = TreeSet<Float>()
|
||||
val deletedReadChapterNumbers = TreeSet<Float>()
|
||||
val deletedBookmarkedChapterNumbers = TreeSet<Float>()
|
||||
val deletedDownloadedChapterNumberToChapter = mutableMapOf<Float, ChapterDataClass>()
|
||||
val deletedDownloadedChapterNumberInfoMap = mutableMapOf<Float, MutableMap<String?, Int>>()
|
||||
val deletedChapterNumberDateFetchMap = mutableMapOf<Float, Long>()
|
||||
|
||||
// clear any orphaned/duplicate chapters that are in the db but not in `chapterList`
|
||||
@@ -247,7 +243,13 @@ object Chapter {
|
||||
if (!chapterUrls.contains(dbChapter.url)) {
|
||||
if (dbChapter.read) deletedReadChapterNumbers.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)
|
||||
deletedChapterNumberDateFetchMap[dbChapter.chapterNumber] = dbChapter.fetchedAt
|
||||
dbChapter.id
|
||||
@@ -256,14 +258,16 @@ object Chapter {
|
||||
}
|
||||
}
|
||||
|
||||
transaction {
|
||||
// we got some clean up due
|
||||
if (chaptersIdsToDelete.isNotEmpty()) {
|
||||
DownloadManager.dequeue(chaptersIdsToDelete)
|
||||
// we got some clean up due
|
||||
if (chaptersIdsToDelete.isNotEmpty()) {
|
||||
DownloadManager.dequeue(chaptersIdsToDelete)
|
||||
transaction {
|
||||
PageTable.deleteWhere { chapter inList chaptersIdsToDelete }
|
||||
ChapterTable.deleteWhere { id inList chaptersIdsToDelete }
|
||||
}
|
||||
}
|
||||
|
||||
transaction {
|
||||
if (chaptersToInsert.isNotEmpty()) {
|
||||
ChapterTable
|
||||
.batchInsert(chaptersToInsert) { chapter ->
|
||||
@@ -279,67 +283,41 @@ object Chapter {
|
||||
this[ChapterTable.isRead] = false
|
||||
this[ChapterTable.isBookmarked] = false
|
||||
this[ChapterTable.isDownloaded] = false
|
||||
this[ChapterTable.pageCount] = -1
|
||||
|
||||
// is recognized chapter number
|
||||
if (chapter.chapterNumber >= 0f && chapter.chapterNumber in deletedChapterNumbers) {
|
||||
this[ChapterTable.isRead] = chapter.chapterNumber in deletedReadChapterNumbers
|
||||
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
|
||||
deletedChapterNumberDateFetchMap[chapter.chapterNumber]?.let {
|
||||
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)) }
|
||||
}
|
||||
|
||||
if (chaptersToUpdate.isNotEmpty()) {
|
||||
BatchUpdateStatement(ChapterTable)
|
||||
.apply {
|
||||
chaptersToUpdate.forEach {
|
||||
addBatch(EntityID(it.id, ChapterTable))
|
||||
|
||||
val currentChapter = chaptersInDb.find { dbChapter -> dbChapter.id == it.id }!!
|
||||
|
||||
this[ChapterTable.name] = it.name
|
||||
this[ChapterTable.date_upload] = it.uploadDate
|
||||
this[ChapterTable.chapter_number] = it.chapterNumber
|
||||
this[ChapterTable.scanlator] = it.scanlator
|
||||
this[ChapterTable.sourceOrder] = it.index
|
||||
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()
|
||||
.execute(this@transaction)
|
||||
BatchUpdateStatement(ChapterTable).apply {
|
||||
chaptersToUpdate.forEach {
|
||||
addBatch(EntityID(it.id, ChapterTable))
|
||||
this[ChapterTable.name] = it.name
|
||||
this[ChapterTable.date_upload] = it.uploadDate
|
||||
this[ChapterTable.chapter_number] = it.chapterNumber
|
||||
this[ChapterTable.scanlator] = it.scanlator
|
||||
this[ChapterTable.sourceOrder] = it.index
|
||||
this[ChapterTable.realUrl] = it.realUrl
|
||||
}
|
||||
execute(this@transaction)
|
||||
}
|
||||
}
|
||||
|
||||
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||
@@ -539,11 +517,11 @@ object Chapter {
|
||||
// mangaId is not null, scope query under manga
|
||||
when {
|
||||
input.chapterIds != null -> {
|
||||
(ChapterTable.manga eq mangaId) and (ChapterTable.id inList input.chapterIds)
|
||||
Op.build { (ChapterTable.manga eq mangaId) and (ChapterTable.id inList input.chapterIds) }
|
||||
}
|
||||
|
||||
input.chapterIndexes != null -> {
|
||||
(ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder inList input.chapterIndexes)
|
||||
Op.build { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder inList input.chapterIndexes) }
|
||||
}
|
||||
|
||||
else -> {
|
||||
@@ -556,7 +534,7 @@ object Chapter {
|
||||
// mangaId is null, only chapterIndexes is valid for this case
|
||||
when {
|
||||
input.chapterIds != null -> {
|
||||
(ChapterTable.id inList input.chapterIds)
|
||||
Op.build { (ChapterTable.id inList input.chapterIds) }
|
||||
}
|
||||
|
||||
else -> {
|
||||
@@ -672,14 +650,13 @@ object Chapter {
|
||||
}
|
||||
|
||||
if (existingMetaByMetaId.isNotEmpty()) {
|
||||
BatchUpdateStatement(ChapterMetaTable)
|
||||
.apply {
|
||||
existingMetaByMetaId.forEach { (metaId, entry) ->
|
||||
addBatch(EntityID(metaId, ChapterMetaTable))
|
||||
this[ChapterMetaTable.value] = entry.value
|
||||
}
|
||||
}.toExecutable()
|
||||
.execute(this@transaction)
|
||||
BatchUpdateStatement(ChapterMetaTable).apply {
|
||||
existingMetaByMetaId.forEach { (metaId, entry) ->
|
||||
addBatch(EntityID(metaId, ChapterMetaTable))
|
||||
this[ChapterMetaTable.value] = entry.value
|
||||
}
|
||||
execute(this@transaction)
|
||||
}
|
||||
}
|
||||
|
||||
if (newMetaByChapterId.isNotEmpty()) {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package suwayomi.tachidesk.manga.impl
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.select
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReady
|
||||
import suwayomi.tachidesk.manga.impl.download.fileProvider.ChaptersFilesProvider
|
||||
import suwayomi.tachidesk.manga.impl.download.fileProvider.impl.ArchiveProvider
|
||||
|
||||
@@ -12,14 +12,11 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.core.neq
|
||||
import org.jetbrains.exposed.v1.jdbc.insert
|
||||
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.update
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.manga.impl.Manga.getManga
|
||||
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
|
||||
import suwayomi.tachidesk.manga.model.table.CategoryTable
|
||||
|
||||
@@ -20,18 +20,15 @@ import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.javalin.http.HttpStatus
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Response
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.core.statements.BatchUpdateStatement
|
||||
import org.jetbrains.exposed.v1.jdbc.batchInsert
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.batchInsert
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl
|
||||
import suwayomi.tachidesk.manga.impl.Source.getSource
|
||||
import suwayomi.tachidesk.manga.impl.download.fileProvider.impl.MissingThumbnailException
|
||||
@@ -133,7 +130,7 @@ object Manga {
|
||||
""
|
||||
}
|
||||
if (remoteTitle.isNotEmpty() && remoteTitle != mangaEntry[MangaTable.title]) {
|
||||
val canUpdateTitle = updateMangaDownloadDir(mangaEntry[MangaTable.title], source.toString(), remoteTitle)
|
||||
val canUpdateTitle = updateMangaDownloadDir(mangaId, remoteTitle)
|
||||
|
||||
if (canUpdateTitle) {
|
||||
it[MangaTable.title] = remoteTitle
|
||||
@@ -298,14 +295,13 @@ object Manga {
|
||||
}
|
||||
|
||||
if (existingMetaByMetaId.isNotEmpty()) {
|
||||
BatchUpdateStatement(MangaMetaTable)
|
||||
.apply {
|
||||
existingMetaByMetaId.forEach { (metaId, entry) ->
|
||||
addBatch(EntityID(metaId, MangaMetaTable))
|
||||
this[MangaMetaTable.value] = entry.value
|
||||
}
|
||||
}.toExecutable()
|
||||
.execute(this@transaction)
|
||||
BatchUpdateStatement(MangaMetaTable).apply {
|
||||
existingMetaByMetaId.forEach { (metaId, entry) ->
|
||||
addBatch(EntityID(metaId, MangaMetaTable))
|
||||
this[MangaMetaTable.value] = entry.value
|
||||
}
|
||||
execute(this@transaction)
|
||||
}
|
||||
}
|
||||
|
||||
if (newMetaByMangaId.isNotEmpty()) {
|
||||
|
||||
@@ -9,15 +9,12 @@ package suwayomi.tachidesk.manga.impl
|
||||
|
||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.core.statements.BatchUpdateStatement
|
||||
import org.jetbrains.exposed.v1.jdbc.batchInsert
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.batchInsert
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
@@ -91,28 +88,27 @@ object MangaList {
|
||||
}
|
||||
|
||||
if (mangaToUpdate.isNotEmpty()) {
|
||||
BatchUpdateStatement(MangaTable)
|
||||
.apply {
|
||||
mangaToUpdate.forEach { (sManga, manga) ->
|
||||
addBatch(EntityID(manga[MangaTable.id].value, MangaTable))
|
||||
this[MangaTable.title] = sManga.title
|
||||
this[MangaTable.artist] = sManga.artist ?: manga[MangaTable.artist]
|
||||
this[MangaTable.author] = sManga.author ?: manga[MangaTable.author]
|
||||
this[MangaTable.description] = sManga.description ?: manga[MangaTable.description]
|
||||
this[MangaTable.genre] = sManga.genre ?: manga[MangaTable.genre]
|
||||
this[MangaTable.status] = sManga.status
|
||||
this[MangaTable.thumbnail_url] = sManga.thumbnail_url ?: manga[MangaTable.thumbnail_url]
|
||||
this[MangaTable.updateStrategy] = sManga.update_strategy.name
|
||||
if (!sManga.thumbnail_url.isNullOrEmpty() && manga[MangaTable.thumbnail_url] != sManga.thumbnail_url) {
|
||||
this[MangaTable.thumbnailUrlLastFetched] = Instant.now().epochSecond
|
||||
Manga.clearThumbnail(manga[MangaTable.id].value)
|
||||
} else {
|
||||
this[MangaTable.thumbnailUrlLastFetched] =
|
||||
manga[MangaTable.thumbnailUrlLastFetched]
|
||||
}
|
||||
BatchUpdateStatement(MangaTable).apply {
|
||||
mangaToUpdate.forEach { (sManga, manga) ->
|
||||
addBatch(EntityID(manga[MangaTable.id].value, MangaTable))
|
||||
this[MangaTable.title] = sManga.title
|
||||
this[MangaTable.artist] = sManga.artist ?: manga[MangaTable.artist]
|
||||
this[MangaTable.author] = sManga.author ?: manga[MangaTable.author]
|
||||
this[MangaTable.description] = sManga.description ?: manga[MangaTable.description]
|
||||
this[MangaTable.genre] = sManga.genre ?: manga[MangaTable.genre]
|
||||
this[MangaTable.status] = sManga.status
|
||||
this[MangaTable.thumbnail_url] = sManga.thumbnail_url ?: manga[MangaTable.thumbnail_url]
|
||||
this[MangaTable.updateStrategy] = sManga.update_strategy.name
|
||||
if (!sManga.thumbnail_url.isNullOrEmpty() && manga[MangaTable.thumbnail_url] != sManga.thumbnail_url) {
|
||||
this[MangaTable.thumbnailUrlLastFetched] = Instant.now().epochSecond
|
||||
Manga.clearThumbnail(manga[MangaTable.id].value)
|
||||
} else {
|
||||
this[MangaTable.thumbnailUrlLastFetched] =
|
||||
manga[MangaTable.thumbnailUrlLastFetched]
|
||||
}
|
||||
}.toExecutable()
|
||||
.execute(this@transaction)
|
||||
}
|
||||
execute(this@transaction)
|
||||
}
|
||||
}
|
||||
|
||||
val mangaUrlsToId =
|
||||
|
||||
@@ -13,12 +13,11 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import libcore.net.MimeUtils
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.graphql.types.DownloadConversion
|
||||
import suwayomi.tachidesk.manga.impl.util.getChapterCachePath
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
|
||||
@@ -15,16 +15,12 @@ import eu.kanade.tachiyomi.source.sourcePreferences
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.javalin.json.JsonMapper
|
||||
import io.javalin.json.fromJsonString
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.core.statements.BatchUpdateStatement
|
||||
import org.jetbrains.exposed.v1.jdbc.batchInsert
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import suwayomi.tachidesk.manga.impl.Source.preferenceScreenMap
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.batchInsert
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension.getExtensionIconUrl
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
@@ -214,14 +210,13 @@ object Source {
|
||||
}
|
||||
|
||||
if (existingMetaByMetaId.isNotEmpty()) {
|
||||
BatchUpdateStatement(SourceMetaTable)
|
||||
.apply {
|
||||
existingMetaByMetaId.forEach { (metaId, entry) ->
|
||||
addBatch(EntityID(metaId, SourceMetaTable))
|
||||
this[SourceMetaTable.value] = entry.value
|
||||
}
|
||||
}.toExecutable()
|
||||
.execute(this@transaction)
|
||||
BatchUpdateStatement(SourceMetaTable).apply {
|
||||
existingMetaByMetaId.forEach { (metaId, entry) ->
|
||||
addBatch(EntityID(metaId, SourceMetaTable))
|
||||
this[SourceMetaTable.value] = entry.value
|
||||
}
|
||||
execute(this@transaction)
|
||||
}
|
||||
}
|
||||
|
||||
if (newMetaBySourceId.isNotEmpty()) {
|
||||
|
||||
@@ -19,7 +19,7 @@ import okio.Buffer
|
||||
import okio.Sink
|
||||
import okio.buffer
|
||||
import okio.gzip
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.manga.impl.backup.BackupFlags
|
||||
import suwayomi.tachidesk.manga.impl.backup.proto.handlers.BackupCategoryHandler
|
||||
import suwayomi.tachidesk.manga.impl.backup.proto.handlers.BackupGlobalMetaHandler
|
||||
|
||||
@@ -11,9 +11,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import okio.buffer
|
||||
import okio.gzip
|
||||
import okio.source
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup
|
||||
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
|
||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||
|
||||
@@ -7,8 +7,8 @@ package suwayomi.tachidesk.manga.impl.backup.proto.handlers
|
||||
* 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 org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import suwayomi.tachidesk.manga.impl.Category
|
||||
import suwayomi.tachidesk.manga.impl.Category.modifyCategoriesMetas
|
||||
import suwayomi.tachidesk.manga.impl.backup.BackupFlags
|
||||
|
||||
@@ -8,18 +8,16 @@ package suwayomi.tachidesk.manga.impl.backup.proto.handlers
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.core.statements.BatchUpdateStatement
|
||||
import org.jetbrains.exposed.v1.jdbc.batchInsert
|
||||
import org.jetbrains.exposed.v1.jdbc.insertAndGetId
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.batchInsert
|
||||
import org.jetbrains.exposed.sql.insertAndGetId
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.manga.impl.CategoryManga
|
||||
import suwayomi.tachidesk.manga.impl.Chapter
|
||||
import suwayomi.tachidesk.manga.impl.Chapter.modifyChaptersMetas
|
||||
@@ -345,25 +343,24 @@ object BackupMangaHandler {
|
||||
}
|
||||
|
||||
if (chaptersToUpdateToDbChapter.isNotEmpty()) {
|
||||
BatchUpdateStatement(ChapterTable)
|
||||
.apply {
|
||||
chaptersToUpdateToDbChapter.forEach { (backupChapter, dbChapter) ->
|
||||
addBatch(EntityID(dbChapter[ChapterTable.id].value, ChapterTable))
|
||||
if (flags.includeChapters) {
|
||||
this[ChapterTable.isRead] = backupChapter.read || dbChapter[ChapterTable.isRead]
|
||||
this[ChapterTable.lastPageRead] =
|
||||
max(backupChapter.lastPageRead, dbChapter[ChapterTable.lastPageRead]).coerceAtLeast(0)
|
||||
this[ChapterTable.isBookmarked] = backupChapter.bookmark || dbChapter[ChapterTable.isBookmarked]
|
||||
}
|
||||
|
||||
if (flags.includeHistory) {
|
||||
this[ChapterTable.lastReadAt] =
|
||||
(historyByChapter[backupChapter.url]?.maxOrNull()?.milliseconds?.inWholeSeconds ?: 0)
|
||||
.coerceAtLeast(dbChapter[ChapterTable.lastReadAt])
|
||||
}
|
||||
BatchUpdateStatement(ChapterTable).apply {
|
||||
chaptersToUpdateToDbChapter.forEach { (backupChapter, dbChapter) ->
|
||||
addBatch(EntityID(dbChapter[ChapterTable.id].value, ChapterTable))
|
||||
if (flags.includeChapters) {
|
||||
this[ChapterTable.isRead] = backupChapter.read || dbChapter[ChapterTable.isRead]
|
||||
this[ChapterTable.lastPageRead] =
|
||||
max(backupChapter.lastPageRead, dbChapter[ChapterTable.lastPageRead]).coerceAtLeast(0)
|
||||
this[ChapterTable.isBookmarked] = backupChapter.bookmark || dbChapter[ChapterTable.isBookmarked]
|
||||
}
|
||||
}.toExecutable()
|
||||
.execute(this@dbTransaction)
|
||||
|
||||
if (flags.includeHistory) {
|
||||
this[ChapterTable.lastReadAt] =
|
||||
(historyByChapter[backupChapter.url]?.maxOrNull()?.milliseconds?.inWholeSeconds ?: 0)
|
||||
.coerceAtLeast(dbChapter[ChapterTable.lastReadAt])
|
||||
}
|
||||
}
|
||||
execute(this@dbTransaction)
|
||||
}
|
||||
}
|
||||
|
||||
if (flags.includeClientData) {
|
||||
|
||||
@@ -7,8 +7,7 @@ package suwayomi.tachidesk.manga.impl.backup.proto.handlers
|
||||
* 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 org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import suwayomi.tachidesk.manga.impl.Source
|
||||
import suwayomi.tachidesk.manga.impl.Source.modifySourceMetas
|
||||
import suwayomi.tachidesk.manga.impl.backup.BackupFlags
|
||||
|
||||
@@ -14,14 +14,14 @@ import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.github.reactivecircus.cache4k.Cache
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.batchInsert
|
||||
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.batchInsert
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.manga.impl.ChapterDownloadHelper
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
|
||||
|
||||
@@ -27,12 +27,9 @@ import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.sample
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
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.sql.and
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.manga.impl.download.model.DownloadChapter
|
||||
import suwayomi.tachidesk.manga.impl.download.model.DownloadQueueItem
|
||||
import suwayomi.tachidesk.manga.impl.download.model.DownloadState.Error
|
||||
@@ -346,7 +343,7 @@ object DownloadManager {
|
||||
transaction {
|
||||
ChapterTable
|
||||
.select(ChapterTable.id)
|
||||
.where { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder eq chapterIndex) }
|
||||
.where { ChapterTable.manga.eq(mangaId) and ChapterTable.sourceOrder.eq(chapterIndex) }
|
||||
.first()
|
||||
}
|
||||
enqueue(EnqueueInput(chapterIds = listOf(chapter[ChapterTable.id].value)))
|
||||
|
||||
@@ -17,9 +17,8 @@ import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.manga.impl.ChapterDownloadHelper
|
||||
import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReadyById
|
||||
import suwayomi.tachidesk.manga.impl.download.model.DownloadQueueItem
|
||||
|
||||
@@ -11,10 +11,10 @@ import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.sample
|
||||
import libcore.net.MimeUtils
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.graphql.types.DownloadConversion
|
||||
import suwayomi.tachidesk.manga.impl.Page
|
||||
import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReady
|
||||
import suwayomi.tachidesk.manga.impl.download.model.DownloadQueueItem
|
||||
@@ -26,8 +26,14 @@ import suwayomi.tachidesk.manga.impl.util.getChapterDownloadPath
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import suwayomi.tachidesk.util.ConversionUtil
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import javax.imageio.IIOImage
|
||||
import javax.imageio.ImageIO
|
||||
import javax.imageio.ImageWriteParam
|
||||
import javax.imageio.ImageWriter
|
||||
|
||||
sealed class FileType {
|
||||
data class RegularFile(
|
||||
|
||||
@@ -6,9 +6,8 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.manga.impl.download.fileProvider.ChaptersFilesProvider
|
||||
import suwayomi.tachidesk.manga.impl.download.fileProvider.FileType
|
||||
import suwayomi.tachidesk.manga.impl.util.getChapterCachePath
|
||||
|
||||
@@ -2,9 +2,8 @@ package suwayomi.tachidesk.manga.impl.download.fileProvider.impl
|
||||
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.manga.impl.download.fileProvider.ChaptersFilesProvider
|
||||
import suwayomi.tachidesk.manga.impl.download.fileProvider.FileType.RegularFile
|
||||
import suwayomi.tachidesk.manga.impl.util.getChapterCachePath
|
||||
|
||||
@@ -20,12 +20,12 @@ import okhttp3.CacheControl
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import okio.source
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
||||
import org.jetbrains.exposed.v1.jdbc.insert
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.extensionTableAsDataClass
|
||||
import suwayomi.tachidesk.manga.impl.extension.github.ExtensionGithubApi
|
||||
import suwayomi.tachidesk.manga.impl.util.PackageTools
|
||||
@@ -55,6 +55,7 @@ import java.util.zip.ZipOutputStream
|
||||
import kotlin.io.path.Path
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.outputStream
|
||||
import kotlin.io.path.relativeTo
|
||||
|
||||
object Extension {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
@@ -358,7 +359,6 @@ object Extension {
|
||||
} else {
|
||||
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
|
||||
it[isInstalled] = false
|
||||
it[hasUpdate] = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,16 +11,15 @@ import eu.kanade.tachiyomi.source.local.LocalSource
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.core.statements.BatchUpdateStatement
|
||||
import org.jetbrains.exposed.v1.jdbc.batchInsert
|
||||
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
|
||||
import org.jetbrains.exposed.sql.batchInsert
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension.getExtensionIconUrl
|
||||
import suwayomi.tachidesk.manga.impl.extension.github.ExtensionGithubApi
|
||||
import suwayomi.tachidesk.manga.impl.extension.github.OnlineExtension
|
||||
@@ -126,59 +125,57 @@ object ExtensionsList {
|
||||
.groupBy { it.second[ExtensionTable.isInstalled] }
|
||||
val installedExtensionsToUpdate = extensionsInstalled[true].orEmpty()
|
||||
if (installedExtensionsToUpdate.isNotEmpty()) {
|
||||
BatchUpdateStatement(ExtensionTable)
|
||||
.apply {
|
||||
installedExtensionsToUpdate.forEach { (foundExtension, extensionRecord) ->
|
||||
addBatch(EntityID(extensionRecord[ExtensionTable.id].value, ExtensionTable))
|
||||
// Always update icon url and repo
|
||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||
this[ExtensionTable.repo] = foundExtension.repo
|
||||
BatchUpdateStatement(ExtensionTable).apply {
|
||||
installedExtensionsToUpdate.forEach { (foundExtension, extensionRecord) ->
|
||||
addBatch(EntityID(extensionRecord[ExtensionTable.id].value, ExtensionTable))
|
||||
// Always update icon url and repo
|
||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||
this[ExtensionTable.repo] = foundExtension.repo
|
||||
|
||||
// add these because batch updates need matching columns
|
||||
this[ExtensionTable.hasUpdate] = extensionRecord[ExtensionTable.hasUpdate]
|
||||
this[ExtensionTable.isObsolete] = extensionRecord[ExtensionTable.isObsolete]
|
||||
// add these because batch updates need matching columns
|
||||
this[ExtensionTable.hasUpdate] = extensionRecord[ExtensionTable.hasUpdate]
|
||||
this[ExtensionTable.isObsolete] = extensionRecord[ExtensionTable.isObsolete]
|
||||
|
||||
// a previously removed extension is now available again
|
||||
if (extensionRecord[ExtensionTable.isObsolete] &&
|
||||
foundExtension.versionCode >= extensionRecord[ExtensionTable.versionCode]
|
||||
) {
|
||||
this[ExtensionTable.isObsolete] = false
|
||||
// a previously removed extension is now available again
|
||||
if (extensionRecord[ExtensionTable.isObsolete] &&
|
||||
foundExtension.versionCode >= extensionRecord[ExtensionTable.versionCode]
|
||||
) {
|
||||
this[ExtensionTable.isObsolete] = false
|
||||
}
|
||||
|
||||
when {
|
||||
foundExtension.versionCode > extensionRecord[ExtensionTable.versionCode] -> {
|
||||
// there is an update
|
||||
this[ExtensionTable.hasUpdate] = true
|
||||
updateMap.putIfAbsent(foundExtension.pkgName, foundExtension)
|
||||
}
|
||||
|
||||
when {
|
||||
foundExtension.versionCode > extensionRecord[ExtensionTable.versionCode] -> {
|
||||
// there is an update
|
||||
this[ExtensionTable.hasUpdate] = true
|
||||
updateMap.putIfAbsent(foundExtension.pkgName, foundExtension)
|
||||
}
|
||||
|
||||
foundExtension.versionCode < extensionRecord[ExtensionTable.versionCode] -> {
|
||||
// somehow the user installed an invalid version
|
||||
this[ExtensionTable.isObsolete] = true
|
||||
}
|
||||
foundExtension.versionCode < extensionRecord[ExtensionTable.versionCode] -> {
|
||||
// somehow the user installed an invalid version
|
||||
this[ExtensionTable.isObsolete] = true
|
||||
}
|
||||
}
|
||||
}.toExecutable()
|
||||
.execute(this@transaction)
|
||||
}
|
||||
execute(this@transaction)
|
||||
}
|
||||
}
|
||||
val extensionsToFullyUpdate = extensionsInstalled[false].orEmpty()
|
||||
if (extensionsToFullyUpdate.isNotEmpty()) {
|
||||
BatchUpdateStatement(ExtensionTable)
|
||||
.apply {
|
||||
extensionsToFullyUpdate.forEach { (foundExtension, extensionRecord) ->
|
||||
addBatch(EntityID(extensionRecord[ExtensionTable.id].value, ExtensionTable))
|
||||
// extension is not installed, so we can overwrite the data without a care
|
||||
this[ExtensionTable.repo] = foundExtension.repo
|
||||
this[ExtensionTable.name] = foundExtension.name
|
||||
this[ExtensionTable.versionName] = foundExtension.versionName
|
||||
this[ExtensionTable.versionCode] = foundExtension.versionCode
|
||||
this[ExtensionTable.lang] = foundExtension.lang
|
||||
this[ExtensionTable.isNsfw] = foundExtension.isNsfw
|
||||
this[ExtensionTable.apkName] = foundExtension.apkName
|
||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||
}
|
||||
}.toExecutable()
|
||||
.execute(this@transaction)
|
||||
BatchUpdateStatement(ExtensionTable).apply {
|
||||
extensionsToFullyUpdate.forEach { (foundExtension, extensionRecord) ->
|
||||
addBatch(EntityID(extensionRecord[ExtensionTable.id].value, ExtensionTable))
|
||||
// extension is not installed, so we can overwrite the data without a care
|
||||
this[ExtensionTable.repo] = foundExtension.repo
|
||||
this[ExtensionTable.name] = foundExtension.name
|
||||
this[ExtensionTable.versionName] = foundExtension.versionName
|
||||
this[ExtensionTable.versionCode] = foundExtension.versionCode
|
||||
this[ExtensionTable.lang] = foundExtension.lang
|
||||
this[ExtensionTable.isNsfw] = foundExtension.isNsfw
|
||||
this[ExtensionTable.apkName] = foundExtension.apkName
|
||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||
}
|
||||
execute(this@transaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (extensionsToInsert.isNotEmpty()) {
|
||||
|
||||
@@ -14,10 +14,8 @@ import kotlinx.serialization.json.put
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.select
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.graphql.types.KoSyncStatusPayload
|
||||
import suwayomi.tachidesk.graphql.types.KoreaderSyncChecksumMethod
|
||||
import suwayomi.tachidesk.graphql.types.KoreaderSyncConflictStrategy
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user