mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-02 02:14:36 -05:00
Compare commits
4 Commits
renovate/m
...
renovate/k
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a3096e0a0 | ||
|
|
64d7939b74 | ||
|
|
3f610da0c5 | ||
|
|
47e0b835d5 |
36
.github/workflows/build_push.yml
vendored
36
.github/workflows/build_push.yml
vendored
@@ -54,14 +54,14 @@ jobs:
|
||||
run: ./gradlew :server:shadowJar --stacktrace
|
||||
|
||||
- name: Upload Jar
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: jar
|
||||
path: master/server/build/*.jar
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload icons
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: icon
|
||||
path: master/server/src/main/resources/icon
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
run: tar -cvzf scripts.tar.gz -C master/ scripts/
|
||||
|
||||
- name: Upload scripts.tar.gz
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: scripts
|
||||
path: scripts.tar.gz
|
||||
@@ -103,7 +103,7 @@ jobs:
|
||||
run: jlink --add-modules java.base,java.compiler,java.datatransfer,java.desktop,java.instrument,java.logging,java.management,java.naming,java.prefs,java.scripting,java.se,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,jdk.attach,jdk.crypto.ec,jdk.jdi,jdk.management,jdk.net,jdk.unsupported,jdk.unsupported.desktop,jdk.zipfs,jdk.accessibility --output suwa --strip-debug --no-man-pages --no-header-files --compress=2
|
||||
|
||||
- name: Upload JRE package
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ matrix.name }}-jre
|
||||
path: suwa
|
||||
@@ -134,26 +134,26 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download Jar
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: jar
|
||||
path: server/build
|
||||
|
||||
- name: Download JRE
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@v7
|
||||
if: matrix.name != 'linux-assets' && matrix.name != 'debian-all'
|
||||
with:
|
||||
name: ${{ matrix.jre }}-jre
|
||||
path: jre
|
||||
|
||||
- name: Download icons
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: icon
|
||||
path: server/src/main/resources/icon
|
||||
|
||||
- name: Download scripts.tar.gz
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: scripts
|
||||
|
||||
@@ -164,7 +164,7 @@ jobs:
|
||||
scripts/bundler.sh -o upload/ ${{ matrix.name }}
|
||||
|
||||
- name: Upload ${{ matrix.name }} release
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ matrix.name }}
|
||||
path: upload/*
|
||||
@@ -174,35 +174,35 @@ jobs:
|
||||
needs: bundle
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/download-artifact@v8
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: jar
|
||||
path: release
|
||||
- uses: actions/download-artifact@v8
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: debian-all
|
||||
path: release
|
||||
- uses: actions/download-artifact@v8
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: appimage
|
||||
path: release
|
||||
- uses: actions/download-artifact@v8
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: linux-assets
|
||||
path: release
|
||||
- uses: actions/download-artifact@v8
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: linux-x64
|
||||
path: release
|
||||
- uses: actions/download-artifact@v8
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: macOS-x64
|
||||
path: release
|
||||
- uses: actions/download-artifact@v8
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: macOS-arm64
|
||||
path: release
|
||||
- uses: actions/download-artifact@v8
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: windows-x64
|
||||
path: release
|
||||
@@ -240,7 +240,7 @@ jobs:
|
||||
git push origin $TAG
|
||||
|
||||
- name: Upload Preview Release
|
||||
uses: softprops/action-gh-release@v3
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
|
||||
repository: "Suwayomi/Suwayomi-Server-preview"
|
||||
|
||||
36
.github/workflows/publish.yml
vendored
36
.github/workflows/publish.yml
vendored
@@ -56,14 +56,14 @@ jobs:
|
||||
run: ./gradlew :server:downloadWebUI :server:shadowJar --stacktrace
|
||||
|
||||
- name: Upload Jar
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: jar
|
||||
path: master/server/build/*.jar
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload icons
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: icon
|
||||
path: master/server/src/main/resources/icon
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
run: tar -cvzf scripts.tar.gz -C master/ scripts/
|
||||
|
||||
- name: Upload scripts.tar.gz
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: scripts
|
||||
path: scripts.tar.gz
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
run: jlink --add-modules java.base,java.compiler,java.datatransfer,java.desktop,java.instrument,java.logging,java.management,java.naming,java.prefs,java.scripting,java.se,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,jdk.attach,jdk.crypto.ec,jdk.jdi,jdk.management,jdk.net,jdk.unsupported,jdk.unsupported.desktop,jdk.zipfs,jdk.accessibility --output suwa --strip-debug --no-man-pages --no-header-files --compress=2
|
||||
|
||||
- name: Upload JDK package
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ matrix.name }}-jre
|
||||
path: suwa
|
||||
@@ -136,26 +136,26 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download Jar
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: jar
|
||||
path: server/build
|
||||
|
||||
- name: Download JRE
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@v7
|
||||
if: matrix.name != 'linux-assets' && matrix.name != 'debian-all'
|
||||
with:
|
||||
name: ${{ matrix.jre }}-jre
|
||||
path: jre
|
||||
|
||||
- name: Download icons
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: icon
|
||||
path: server/src/main/resources/icon
|
||||
|
||||
- name: Download scripts.tar.gz
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: scripts
|
||||
|
||||
@@ -166,7 +166,7 @@ jobs:
|
||||
scripts/bundler.sh -o upload/ ${{ matrix.name }}
|
||||
|
||||
- name: Upload ${{ matrix.name }} files
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ matrix.name }}
|
||||
path: upload/*
|
||||
@@ -177,35 +177,35 @@ jobs:
|
||||
needs: bundle
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/download-artifact@v8
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: jar
|
||||
path: release
|
||||
- uses: actions/download-artifact@v8
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: debian-all
|
||||
path: release
|
||||
- uses: actions/download-artifact@v8
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: appimage
|
||||
path: release
|
||||
- uses: actions/download-artifact@v8
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: linux-assets
|
||||
path: release
|
||||
- uses: actions/download-artifact@v8
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: linux-x64
|
||||
path: release
|
||||
- uses: actions/download-artifact@v8
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: macOS-x64
|
||||
path: release
|
||||
- uses: actions/download-artifact@v8
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: macOS-arm64
|
||||
path: release
|
||||
- uses: actions/download-artifact@v8
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: windows-x64
|
||||
path: release
|
||||
@@ -214,7 +214,7 @@ jobs:
|
||||
run: cd release && sha256sum * > Checksums.sha256
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v3
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
token: ${{ secrets.DEPLOY_RELEASE_TOKEN }}
|
||||
draft: true
|
||||
|
||||
@@ -76,9 +76,9 @@ These clients are built-in options, and the server can keep them automatically u
|
||||
- [Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI): Web app, PWA
|
||||
- [Suwayomi-VUI](https://github.com/Suwayomi/Suwayomi-VUI): Web app, PWA
|
||||
|
||||
##### Other clients (potentially inactive or abandoned)
|
||||
##### Other clients (potentially inactive or abondend)
|
||||
- [Tachidesk-VaadinUI](https://github.com/Suwayomi/Tachidesk-VaadinUI): Desktop app (windows, linux, mac); UI in the browser, manages its own suwayomi server instance
|
||||
- [Moku](https://github.com/Youwes09/Moku): Desktop app (windows, linux, mac), can manage its own suwayomi server instance
|
||||
- [Moku](https://github.com/Youwes09/Moku): Desktop app (windows, linux, mac), requires access to a running server
|
||||
- [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
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
[versions]
|
||||
kotlin = "2.3.21"
|
||||
coroutines = "1.11.0"
|
||||
serialization = "1.11.0"
|
||||
coroutines = "1.10.2"
|
||||
serialization = "1.10.0"
|
||||
jvmTarget = "21"
|
||||
okhttp = "5.3.2" # Major version is locked by Tachiyomi extensions
|
||||
javalin = "7.2.0"
|
||||
jte = "3.2.4"
|
||||
jackson = "3.1.2" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
|
||||
javalin = "6.7.0"
|
||||
jte = "3.2.3"
|
||||
jackson = "2.18.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
|
||||
exposed = "0.61.0"
|
||||
dex2jar = "2.4.36"
|
||||
polyglot = "25.0.3"
|
||||
dex2jar = "2.4.34"
|
||||
polyglot = "24.2.2"
|
||||
settings = "1.3.0"
|
||||
twelvemonkeys = "3.13.1"
|
||||
graphqlkotlin = "10.0.0-alpha.3"
|
||||
graphqlkotlin = "8.8.1"
|
||||
xmlserialization = "0.91.3"
|
||||
ktlint = "1.8.0"
|
||||
koin = "4.2.1"
|
||||
moko = "0.26.4"
|
||||
koin = "4.1.1"
|
||||
moko = "0.26.0"
|
||||
|
||||
[libraries]
|
||||
# Kotlin
|
||||
@@ -39,22 +39,22 @@ serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization-jvm", v
|
||||
# Logging
|
||||
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"
|
||||
kotlinlogging = "io.github.oshai:kotlin-logging-jvm:8.0.01"
|
||||
|
||||
# OkHttp
|
||||
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
|
||||
okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp" }
|
||||
okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", version.ref = "okhttp" }
|
||||
okio = "com.squareup.okio:okio:3.17.0"
|
||||
okio = "com.squareup.okio:okio:3.16.4"
|
||||
|
||||
# Javalin api
|
||||
javalin-core = { module = "io.javalin:javalin", version.ref = "javalin" }
|
||||
javalin-openapi = { module = "io.javalin:javalin-openapi", version.ref = "javalin" }
|
||||
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.20"
|
||||
javalin-rendering = { module = "io.javalin:javalin-rendering", version.ref = "javalin" }
|
||||
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }
|
||||
jackson-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" }
|
||||
jackson-annotations = { module = "com.fasterxml.jackson.core:jackson-annotations", version.ref = "jackson" }
|
||||
jte = { module = "gg.jte:jte", version.ref = "jte" }
|
||||
kte = { module = "gg.jte:jte-kotlin", version.ref = "jte" }
|
||||
|
||||
@@ -68,7 +68,7 @@ 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" }
|
||||
postgres = "org.postgresql:postgresql:42.7.11"
|
||||
postgres = "org.postgresql:postgresql:42.7.10"
|
||||
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"
|
||||
|
||||
@@ -86,10 +86,10 @@ systemtray-desktop = "com.dorkbox:Desktop:1.1" # version locked by SystemTray
|
||||
# dependencies of Tachiyomi extensions
|
||||
injekt = "com.github.null2264:injekt-koin:ee267b2e27"
|
||||
rxjava = "io.reactivex:rxjava:1.3.8"
|
||||
jsoup = "org.jsoup:jsoup:1.22.2"
|
||||
jsoup = "org.jsoup:jsoup:1.22.1"
|
||||
|
||||
# Config
|
||||
config = "com.typesafe:config:1.4.8"
|
||||
config = "com.typesafe:config:1.4.5"
|
||||
config4k = "io.github.config4k:config4k:0.7.0"
|
||||
|
||||
# Sort
|
||||
@@ -105,7 +105,7 @@ dex2jar-tools = { module = "de.femtopedia.dex2jar:dex-tools", version.ref = "dex
|
||||
|
||||
# APK
|
||||
apk-parser = "net.dongliu:apk-parser:2.6.10"
|
||||
apksig = "com.android.tools.build:apksig:9.2.1"
|
||||
apksig = "com.android.tools.build:apksig:9.0.1"
|
||||
|
||||
# Xml
|
||||
xmlpull = "xmlpull:xmlpull:1.1.3.4a"
|
||||
@@ -118,10 +118,10 @@ commonscompress = "org.apache.commons:commons-compress:1.28.0"
|
||||
junrar = "com.github.junrar:junrar:7.5.10"
|
||||
|
||||
# AES/CBC/PKCS7Padding Cypher provider
|
||||
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84"
|
||||
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.83"
|
||||
|
||||
# AndroidX annotations
|
||||
android-annotations = "androidx.annotation:annotation:1.10.0"
|
||||
android-annotations = "androidx.annotation:annotation:1.9.1"
|
||||
|
||||
# Substitute for duktape-android
|
||||
polyglot-core = { module = "org.graalvm.polyglot:polyglot", version.ref = "polyglot" }
|
||||
@@ -132,7 +132,7 @@ settings-core = { module = "com.russhwolf:multiplatform-settings-jvm", version.r
|
||||
settings-serialization = { module = "com.russhwolf:multiplatform-settings-serialization-jvm", version.ref = "settings" }
|
||||
|
||||
# ICU4J
|
||||
icu4j = "com.ibm.icu:icu4j:78.3"
|
||||
icu4j = "com.ibm.icu:icu4j:78.2"
|
||||
|
||||
# Image Decoding implementation provider
|
||||
twelvemonkeys-common-lang = { module = "com.twelvemonkeys.common:common-lang", version.ref = "twelvemonkeys" }
|
||||
@@ -158,7 +158,7 @@ cronUtils = "com.cronutils:cron-utils:9.2.1"
|
||||
kcef = "dev.datlag:kcef:2024.04.20.4"
|
||||
|
||||
# User
|
||||
jwt = "com.auth0:java-jwt:4.5.2"
|
||||
jwt = "com.auth0:java-jwt:4.5.1"
|
||||
|
||||
# lint - used for renovate to update ktlint version
|
||||
ktlint = { module = "com.pinterest.ktlint:ktlint-cli", version.ref = "ktlint" }
|
||||
@@ -173,16 +173,16 @@ kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi
|
||||
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin"}
|
||||
|
||||
# Linter
|
||||
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version = "14.2.0"}
|
||||
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version = "14.0.1"}
|
||||
|
||||
# Build config
|
||||
buildconfig = { id = "com.github.gmazzo.buildconfig", version = "6.0.9"}
|
||||
buildconfig = { id = "com.github.gmazzo.buildconfig", version = "6.0.7"}
|
||||
|
||||
# Download
|
||||
download = { id = "de.undercouch.download", version = "5.7.0"}
|
||||
|
||||
# ShadowJar
|
||||
shadowjar = { id = "com.gradleup.shadow", version = "8.3.10"}
|
||||
shadowjar = { id = "com.gradleup.shadow", version = "8.3.9"}
|
||||
|
||||
# Moko
|
||||
moko = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "moko" }
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,9 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
||||
networkTimeout=10000
|
||||
retries=0
|
||||
retryBackOffMs=500
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
2
gradlew
vendored
2
gradlew
vendored
@@ -57,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
|
||||
31
gradlew.bat
vendored
31
gradlew.bat
vendored
@@ -23,8 +23,8 @@
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||
setlocal EnableExtensions
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@@ -51,7 +51,7 @@ echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
"%COMSPEC%" /c exit 1
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
@@ -65,7 +65,7 @@ echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
"%COMSPEC%" /c exit 1
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
@@ -73,10 +73,21 @@ echo location of your Java installation. 1>&2
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||
@rem which allows us to clear the local environment before executing the java command
|
||||
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:exitWithErrorLevel
|
||||
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
|
||||
@@ -6,7 +6,7 @@ import io.javalin.websocket.WsMessageContext
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.eclipse.jetty.websocket.core.CloseStatus
|
||||
import org.eclipse.jetty.websocket.api.CloseStatus
|
||||
import suwayomi.tachidesk.manga.impl.update.Websocket
|
||||
|
||||
object WebView : Websocket<String>() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -24,11 +24,11 @@ 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)
|
||||
@@ -48,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)
|
||||
@@ -68,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)
|
||||
@@ -90,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)
|
||||
@@ -112,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)
|
||||
@@ -157,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)
|
||||
@@ -177,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)
|
||||
@@ -197,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)
|
||||
@@ -217,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)
|
||||
@@ -237,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)
|
||||
@@ -257,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)
|
||||
|
||||
@@ -20,10 +20,10 @@ 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 {
|
||||
|
||||
@@ -25,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 {
|
||||
@@ -122,6 +122,6 @@ class MangaForIdsDataLoader : KotlinDataLoader<List<Int>, MangaNodeList> {
|
||||
}
|
||||
}
|
||||
},
|
||||
DataLoaderOptions.newOptions().setCacheMap(CustomCacheMap<List<Int>, MangaNodeList>()).build(),
|
||||
DataLoaderOptions.newOptions().setCacheMap(CustomCacheMap<List<Int>, MangaNodeList>()),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import graphql.execution.DataFetcherResult
|
||||
@@ -17,6 +15,7 @@ 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
|
||||
@@ -43,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,
|
||||
@@ -64,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>,
|
||||
@@ -109,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>,
|
||||
@@ -164,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,
|
||||
@@ -288,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?,
|
||||
@@ -333,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,
|
||||
@@ -391,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,
|
||||
@@ -450,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(
|
||||
@@ -538,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,7 +1,6 @@
|
||||
@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.dao.id.EntityID
|
||||
@@ -17,6 +16,7 @@ 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
|
||||
@@ -120,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,
|
||||
@@ -164,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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,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,
|
||||
@@ -218,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>,
|
||||
@@ -263,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>,
|
||||
@@ -318,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,
|
||||
@@ -397,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,5 +1,3 @@
|
||||
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import graphql.execution.DataFetcherResult
|
||||
@@ -7,6 +5,7 @@ import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withTimeout
|
||||
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
|
||||
@@ -31,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(
|
||||
@@ -59,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(
|
||||
@@ -84,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() },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,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() },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,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() },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,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() },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,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(
|
||||
@@ -241,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(
|
||||
@@ -268,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(
|
||||
@@ -297,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,5 +1,3 @@
|
||||
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||
@@ -7,6 +5,7 @@ import graphql.execution.DataFetcherResult
|
||||
import io.javalin.http.UploadedFile
|
||||
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
|
||||
@@ -76,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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,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,10 +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
|
||||
@@ -27,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 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,7 +1,6 @@
|
||||
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import graphql.execution.DataFetcherResult
|
||||
import org.jetbrains.exposed.sql.LikePattern
|
||||
import org.jetbrains.exposed.sql.Op
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
@@ -13,6 +12,7 @@ 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,5 +1,3 @@
|
||||
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import graphql.execution.DataFetcherResult
|
||||
@@ -14,6 +12,7 @@ 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
|
||||
@@ -30,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(
|
||||
@@ -49,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(
|
||||
@@ -83,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(
|
||||
@@ -112,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,6 +5,7 @@ import androidx.preference.EditTextPreference
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import graphql.execution.DataFetcherResult
|
||||
import org.jetbrains.exposed.sql.LikePattern
|
||||
import org.jetbrains.exposed.sql.Op
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
@@ -17,6 +16,7 @@ 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,5 +1,3 @@
|
||||
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||
@@ -8,6 +6,7 @@ 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,10 +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
|
||||
@@ -29,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,
|
||||
@@ -37,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(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +60,7 @@ class UpdateMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun updateLibraryManga(input: UpdateLibraryMangaInput): CompletableFuture<UpdateLibraryMangaPayload?> {
|
||||
fun updateLibraryManga(input: UpdateLibraryMangaInput): CompletableFuture<DataFetcherResult<UpdateLibraryMangaPayload?>> {
|
||||
updateLibrary(
|
||||
UpdateLibraryInput(
|
||||
clientMutationId = input.clientMutationId,
|
||||
@@ -68,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())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +92,7 @@ class UpdateMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun updateCategoryManga(input: UpdateCategoryMangaInput): CompletableFuture<UpdateCategoryMangaPayload?> {
|
||||
fun updateCategoryManga(input: UpdateCategoryMangaInput): CompletableFuture<DataFetcherResult<UpdateCategoryMangaPayload?>> {
|
||||
updateLibrary(
|
||||
UpdateLibraryInput(
|
||||
clientMutationId = input.clientMutationId,
|
||||
@@ -98,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,5 +1,3 @@
|
||||
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
|
||||
@@ -11,21 +11,15 @@ 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 {}
|
||||
|
||||
@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 (
|
||||
@@ -35,17 +29,17 @@ class JavalinGraphQLRequestParser : GraphQLRequestParser<Context> {
|
||||
context.formParam("operations")
|
||||
?: throw IllegalArgumentException("Cannot find 'operations' body")
|
||||
} else {
|
||||
return context.bodyInputStream().use { jsonMapper.fromJsonStream<GraphQLServerRequest>(it) }
|
||||
return context.bodyAsClass(GraphQLServerRequest::class.java)
|
||||
}
|
||||
|
||||
val request =
|
||||
jsonMapper.fromJsonString<GraphQLServerRequest>(formParam)
|
||||
context.jsonMapper().fromJsonString<GraphQLServerRequest>(formParam)
|
||||
|
||||
val map =
|
||||
context
|
||||
.formParam("map")
|
||||
?.let {
|
||||
jsonMapper.fromJsonString<Map<String, List<String>>>(it)
|
||||
context.jsonMapper().fromJsonString<Map<String, List<String>>>(it)
|
||||
}.orEmpty()
|
||||
|
||||
val mapItems =
|
||||
@@ -79,8 +73,7 @@ class JavalinGraphQLRequestParser : GraphQLRequestParser<Context> {
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
logger.error(e) { "Error when parsing request" }
|
||||
} catch (_: IOException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ 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
|
||||
@@ -26,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()
|
||||
|
||||
@@ -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
|
||||
@@ -23,7 +26,7 @@ import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.eclipse.jetty.websocket.core.CloseStatus
|
||||
import org.eclipse.jetty.websocket.api.CloseStatus
|
||||
import suwayomi.tachidesk.graphql.server.TachideskGraphQLContextFactory
|
||||
import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ClientMessages.GQL_CONNECTION_INIT
|
||||
import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ClientMessages.GQL_SUBSCRIBE
|
||||
@@ -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
|
||||
|
||||
@@ -13,7 +13,7 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import org.eclipse.jetty.websocket.core.CloseStatus
|
||||
import org.eclipse.jetty.websocket.api.CloseStatus
|
||||
import suwayomi.tachidesk.graphql.server.toGraphQLContext
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
@@ -13,14 +13,12 @@ import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.javalin.Javalin
|
||||
import io.javalin.apibuilder.ApiBuilder.after
|
||||
import io.javalin.apibuilder.ApiBuilder.path
|
||||
import io.javalin.config.RoutesConfig
|
||||
import io.javalin.http.Context
|
||||
import io.javalin.http.HandlerType
|
||||
import io.javalin.http.HttpStatus
|
||||
import io.javalin.http.NotFoundResponse
|
||||
import io.javalin.http.RedirectResponse
|
||||
import io.javalin.http.UnauthorizedResponse
|
||||
import io.javalin.json.JavalinJackson3
|
||||
import io.javalin.rendering.template.JavalinJte
|
||||
import io.javalin.websocket.WsContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -49,7 +47,6 @@ import java.net.URLEncoder
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.text.get
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
object JavalinSetup {
|
||||
@@ -61,12 +58,10 @@ object JavalinSetup {
|
||||
|
||||
fun javalinSetup() {
|
||||
val app =
|
||||
Javalin.start { config ->
|
||||
Javalin.create { config ->
|
||||
val templateEngine = TemplateEngine.createPrecompiled(ContentType.Html)
|
||||
config.fileRenderer(JavalinJte(templateEngine))
|
||||
|
||||
config.jsonMapper(JavalinJackson3())
|
||||
|
||||
WebInterfaceManager.setup(config)
|
||||
|
||||
// config.registerPlugin(OpenApiPlugin(getOpenApiOptions()))
|
||||
@@ -109,8 +104,7 @@ object JavalinSetup {
|
||||
}
|
||||
}
|
||||
|
||||
config.routes.defineCore()
|
||||
config.routes.apiBuilder {
|
||||
config.router.apiBuilder {
|
||||
path(ServerSubpath.maybeAddAsPrefix("api/")) {
|
||||
path("v1/") {
|
||||
GlobalAPI.defineEndpoints()
|
||||
@@ -123,32 +117,17 @@ object JavalinSetup {
|
||||
after { ctx ->
|
||||
// If not matched, the request was for an invalid endpoint
|
||||
// Return a 404 instead of redirecting to the UI for usability
|
||||
if (ctx.endpoints().lastHttpEndpoint()?.path == "*") {
|
||||
if (ctx.endpointHandlerPath() == "*") {
|
||||
throw NotFoundResponse()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config.events.serverStarted {
|
||||
if (serverConfig.initialOpenInBrowserEnabled.value) {
|
||||
Browser.openInBrowser()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// when JVM is prompted to shutdown, stop javalin gracefully
|
||||
Runtime.getRuntime().addShutdownHook(
|
||||
thread(start = false) {
|
||||
app.stop()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun RoutesConfig.defineCore() {
|
||||
val loginPath = ServerSubpath.maybeAddAsPrefix("/login.html")
|
||||
|
||||
get(loginPath) { ctx ->
|
||||
app.get(loginPath) { ctx ->
|
||||
val locale: Locale = LocalizationHelper.ctxToLocale(ctx)
|
||||
ctx.header("content-type", "text/html")
|
||||
val httpCacheSeconds = 1.days.inWholeSeconds
|
||||
@@ -162,7 +141,7 @@ object JavalinSetup {
|
||||
)
|
||||
}
|
||||
|
||||
post(loginPath) { ctx ->
|
||||
app.post(loginPath) { ctx ->
|
||||
val username = ctx.formParam("user")
|
||||
val password = ctx.formParam("pass")
|
||||
val isValid =
|
||||
@@ -195,7 +174,7 @@ object JavalinSetup {
|
||||
)
|
||||
}
|
||||
|
||||
beforeMatched { ctx ->
|
||||
app.beforeMatched { ctx ->
|
||||
val isWebManifest =
|
||||
listOf("site.webmanifest", "manifest.json", "login.html").any { ctx.path().endsWith(it) }
|
||||
val isPageIcon =
|
||||
@@ -240,43 +219,60 @@ object JavalinSetup {
|
||||
ctx.setAttribute(Attribute.TachideskBasic, credentialsValid())
|
||||
}
|
||||
|
||||
wsBefore {
|
||||
app.events { event ->
|
||||
event.serverStarted {
|
||||
if (serverConfig.initialOpenInBrowserEnabled.value) {
|
||||
Browser.openInBrowser()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.wsBefore {
|
||||
it.onConnect { ctx ->
|
||||
ctx.setAttribute(Attribute.TachideskUser, getUserFromWsContext(ctx))
|
||||
}
|
||||
}
|
||||
|
||||
exception(NullPointerException::class.java) { e, ctx ->
|
||||
// when JVM is prompted to shutdown, stop javalin gracefully
|
||||
Runtime.getRuntime().addShutdownHook(
|
||||
thread(start = false) {
|
||||
app.stop()
|
||||
},
|
||||
)
|
||||
|
||||
app.exception(NullPointerException::class.java) { e, ctx ->
|
||||
logger.error(e) { "NullPointerException while handling the request" }
|
||||
ctx.status(404)
|
||||
}
|
||||
exception(NoSuchElementException::class.java) { e, ctx ->
|
||||
app.exception(NoSuchElementException::class.java) { e, ctx ->
|
||||
logger.error(e) { "NoSuchElementException while handling the request" }
|
||||
ctx.status(404)
|
||||
}
|
||||
exception(IOException::class.java) { e, ctx ->
|
||||
app.exception(IOException::class.java) { e, ctx ->
|
||||
logger.error(e) { "IOException while handling the request" }
|
||||
ctx.status(500)
|
||||
ctx.result(e.message ?: "Internal Server Error")
|
||||
}
|
||||
|
||||
exception(IllegalArgumentException::class.java) { e, ctx ->
|
||||
app.exception(IllegalArgumentException::class.java) { e, ctx ->
|
||||
logger.error(e) { "IllegalArgumentException while handling the request" }
|
||||
ctx.status(400)
|
||||
ctx.result(e.message ?: "Bad Request")
|
||||
}
|
||||
|
||||
exception(UnauthorizedException::class.java) { e, ctx ->
|
||||
app.exception(UnauthorizedException::class.java) { e, ctx ->
|
||||
logger.error(e) { "UnauthorizedException while handling the request" }
|
||||
ctx.status(HttpStatus.UNAUTHORIZED)
|
||||
ctx.result(e.message ?: "Unauthorized")
|
||||
}
|
||||
|
||||
exception(ForbiddenException::class.java) { e, ctx ->
|
||||
app.exception(ForbiddenException::class.java) { e, ctx ->
|
||||
logger.error(e) { "ForbiddenException while handling the request" }
|
||||
ctx.status(HttpStatus.FORBIDDEN)
|
||||
ctx.result(e.message ?: "Forbidden")
|
||||
}
|
||||
|
||||
app.start()
|
||||
}
|
||||
|
||||
// private fun getOpenApiOptions(): OpenApiOptions {
|
||||
|
||||
@@ -71,14 +71,15 @@ fun <T> getParam(
|
||||
is Param.FormParam -> ctx.formParamAsClass(param.key, clazz)
|
||||
is Param.PathParam -> ctx.pathParamAsClass(param.key, clazz)
|
||||
is Param.QueryParam -> ctx.queryParamAsClass(param.key, clazz)
|
||||
else -> throw IllegalStateException("Invalid param")
|
||||
}.let {
|
||||
if (param.nullable) {
|
||||
it.getOrNull() ?: param.defaultValue
|
||||
it.allowNullable().get() ?: param.defaultValue
|
||||
} else {
|
||||
if (param.defaultValue != null) {
|
||||
it.getOrDefault(param.defaultValue!!)
|
||||
} else {
|
||||
it.required().get()
|
||||
it.get()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import io.github.oshai.kotlinlogging.KLogger
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.github.reactivecircus.cache4k.Cache
|
||||
import io.javalin.config.JavalinConfig
|
||||
import io.javalin.http.staticfiles.AliasCheck
|
||||
import io.javalin.http.staticfiles.Location
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
@@ -40,6 +39,7 @@ import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import net.lingala.zip4j.ZipFile
|
||||
import org.eclipse.jetty.server.handler.ContextHandler
|
||||
import suwayomi.tachidesk.graphql.types.AboutWebUI
|
||||
import suwayomi.tachidesk.graphql.types.UpdateState
|
||||
import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING
|
||||
@@ -180,7 +180,7 @@ object WebInterfaceManager {
|
||||
// Use canonical path to avoid Jetty alias issues
|
||||
staticFiles.directory = File(applicationDirs.webUIServe).canonicalPath
|
||||
staticFiles.location = Location.EXTERNAL
|
||||
staticFiles.aliasCheck = AliasCheck { _, _ -> true }
|
||||
staticFiles.aliasCheck = ContextHandler.ApproveAliases()
|
||||
}
|
||||
|
||||
serveWebUI = {
|
||||
|
||||
Reference in New Issue
Block a user