mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-13 07:44:34 -05:00
Compare commits
27 Commits
b8ffbebc76
...
v2.3.2232
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f473135d04 | ||
|
|
36ecb3ce6f | ||
|
|
0cac79f354 | ||
|
|
0fd10cc08e | ||
|
|
87aac38253 | ||
|
|
9ca66c18de | ||
|
|
b3c7962503 | ||
|
|
ca93783e72 | ||
|
|
c1d0df8e3a | ||
|
|
612768faeb | ||
|
|
9a1745b626 | ||
|
|
323d58717e | ||
|
|
4d7b7617a9 | ||
|
|
35b48114c6 | ||
|
|
3031aa7ccd | ||
|
|
c79486b8be | ||
|
|
e2fd15158c | ||
|
|
b6de3c3e39 | ||
|
|
656d86c6f6 | ||
|
|
a0fbff5756 | ||
|
|
2d535b44d8 | ||
|
|
c8f5d83e9c | ||
|
|
be5e3f022e | ||
|
|
40a21fabca | ||
|
|
b33069f107 | ||
|
|
14ab3aa9f4 | ||
|
|
bfa0038f53 |
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -42,7 +42,7 @@ body:
|
|||||||
label: Suwayomi-Server version
|
label: Suwayomi-Server version
|
||||||
description: You can find your Suwayomi-Server version in **More → About**.
|
description: You can find your Suwayomi-Server version in **More → About**.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example: "v2.2.2100"
|
Example: "v2.3.2232"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -143,11 +143,13 @@ body:
|
|||||||
options:
|
options:
|
||||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
|
||||||
required: true
|
required: true
|
||||||
|
- label: I have checked the ongoing preview changelog of **[Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI/blob/master/CHANGELOG.md)** and **[Suwayomi-Server](https://github.com/Suwayomi/Suwayomi-Server/blob/master/CHANGELOG.md)** and this bug has **NOT** been listed as fixed
|
||||||
|
required: true
|
||||||
- label: I have written a short but informative title (ideally less than ~100 characters).
|
- label: I have written a short but informative title (ideally less than ~100 characters).
|
||||||
required: true
|
required: true
|
||||||
- label: I have tried the troubleshooting guide described in [README.md](https://github.com/Suwayomi/Suwayomi-Server?tab=readme-ov-file#troubleshooting-and-support)
|
- label: I have tried the troubleshooting guide described in [README.md](https://github.com/Suwayomi/Suwayomi-Server?tab=readme-ov-file#troubleshooting-and-support)
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated to the **[latest version](https://github.com/suwayomi/suwayomi-server/releases/latest)**.
|
- label: I have updated the (**[Suwayomi-WebUI](https://github.com/suwayomi/suwayomi-webui/releases/latest)** and **[Suwayomi-Server](https://github.com/suwayomi/suwayomi-server/releases/latest)**) to the latest versions
|
||||||
required: true
|
required: true
|
||||||
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -31,7 +31,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I have written a short but informative title (ideally less than ~100 characters).
|
- label: I have written a short but informative title (ideally less than ~100 characters).
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated to the **[latest version](https://github.com/suwayomi/suwayomi-server/releases/latest)**.
|
- label: I have updated the (**[Suwayomi-WebUI](https://github.com/suwayomi/suwayomi-webui/releases/latest)** and **[Suwayomi-Server](https://github.com/suwayomi/suwayomi-server/releases/latest)**) to the latest versions
|
||||||
required: true
|
required: true
|
||||||
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
||||||
required: true
|
required: true
|
||||||
43
CHANGELOG.md
43
CHANGELOG.md
@@ -6,10 +6,48 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|||||||
|
|
||||||
## [Unreleased] (Preview)
|
## [Unreleased] (Preview)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- .
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- .
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- .
|
||||||
|
|
||||||
|
## [v2.3.2232] - 2026-07-03
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- (**Extension**) Update APK url on installed Extensions
|
||||||
|
|
||||||
|
## [v2.3.2230] - 2026-07-01
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- (**Extension/API**) Fix GQL handling of extensions without an extension store
|
||||||
|
- (**Build/Bundler**) Fix build continuing if errors happen
|
||||||
|
- (**Build/Bundler**) Remove jogamp from build script
|
||||||
|
- (**Build/Bundler**) Fix chmod attempts on Windows Java
|
||||||
|
|
||||||
|
## [v2.3.2223] + [WebUI: v20260509.01] - 2026-06-30
|
||||||
|
|
||||||
|
### Major Changes
|
||||||
|
|
||||||
|
#### Added [SyncYomi](https://github.com/syncyomi/syncyomi) support
|
||||||
|
This allows you to sync your server manga with other Mihon-based forks! As long as the fork supports SyncYomi it can be sync with!
|
||||||
|
|
||||||
|
#### Support Extension API v1.6
|
||||||
|
This update allows Suwayomi to load and use v1.6 extensions, it is a minor improvement over the existing 1.4 extension API that cleans up much of what we had! It is the basis of future extension APIs that will allow for further development.
|
||||||
|
|
||||||
|
This also allows us to move to Mihon's Extension Store system and replace our Extension Repo system. Old Extension Repos are still compatible and will be automatically migrated if they move to the Extension Store system.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Please back up your Extension Repos, because of the new Extension Stores system you may lose them in the update process and may need to re-add them.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- (**Sync**) Added [SyncYomi](https://github.com/syncyomi/syncyomi) support
|
- (**Sync**) Added [SyncYomi](https://github.com/syncyomi/syncyomi) support
|
||||||
- (**OPDS**) Add option to skip chapter metadata feed providing direct stream/download links
|
- (**OPDS**) Add option to skip chapter metadata feed providing direct stream/download links
|
||||||
- (**Extension/API**) Support Extensions API v1.6
|
- (**Extension/API**) Support Extensions API v1.6
|
||||||
|
- (**Tracker/API**) Add mutation to bind existing track record
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- (**Database/H2**) Use the latest H2 database engine
|
- (**Database/H2**) Use the latest H2 database engine
|
||||||
@@ -426,6 +464,7 @@ Huge thanks to @martinek who pulled the most of the weight this release!
|
|||||||
|
|
||||||
<!-- WEBUI LINKS -->
|
<!-- WEBUI LINKS -->
|
||||||
|
|
||||||
|
[WebUI: v20260509.01]: https://github.com/Suwayomi/Suwayomi-WebUI/blob/master/CHANGELOG.md#2026050901-r3147---2026-05-09
|
||||||
[WebUI: v20260508.01]: https://github.com/Suwayomi/Suwayomi-WebUI/blob/master/CHANGELOG.md#2026050801-r3136---2026-05-08
|
[WebUI: v20260508.01]: https://github.com/Suwayomi/Suwayomi-WebUI/blob/master/CHANGELOG.md#2026050801-r3136---2026-05-08
|
||||||
[WebUI: v20251230.01]: https://github.com/Suwayomi/Suwayomi-WebUI/blob/master/CHANGELOG.md#2025123001-r2937---2025-12-30
|
[WebUI: v20251230.01]: https://github.com/Suwayomi/Suwayomi-WebUI/blob/master/CHANGELOG.md#2025123001-r2937---2025-12-30
|
||||||
[WebUI: v20250801.01]: https://github.com/Suwayomi/Suwayomi-WebUI/blob/master/CHANGELOG.md#2025080101-r2717---2025-08-01
|
[WebUI: v20250801.01]: https://github.com/Suwayomi/Suwayomi-WebUI/blob/master/CHANGELOG.md#2025080101-r2717---2025-08-01
|
||||||
@@ -452,7 +491,9 @@ Huge thanks to @martinek who pulled the most of the weight this release!
|
|||||||
|
|
||||||
<!-- SERVER LINKS -->
|
<!-- SERVER LINKS -->
|
||||||
|
|
||||||
[unreleased]: https://github.com/suwayomi/suwayomi-server/compare/v2.2.2100...HEAD
|
[unreleased]: https://github.com/suwayomi/suwayomi-server/compare/v2.3.2230...HEAD
|
||||||
|
[v2.3.2230]: https://github.com/suwayomi/suwayomi-server/compare/v2.3.2223...v2.3.2230
|
||||||
|
[v2.3.2223]: https://github.com/suwayomi/suwayomi-server/compare/v2.2.2100...v2.3.2223
|
||||||
[v2.2.2100]: https://github.com/suwayomi/suwayomi-server/compare/v2.1.1867...v2.2.2100
|
[v2.2.2100]: https://github.com/suwayomi/suwayomi-server/compare/v2.1.1867...v2.2.2100
|
||||||
[v2.1.1867]: https://github.com/suwayomi/suwayomi-server/compare/v2.0.1727...v2.1.1867
|
[v2.1.1867]: https://github.com/suwayomi/suwayomi-server/compare/v2.0.1727...v2.1.1867
|
||||||
[v2.0.1727]: https://github.com/suwayomi/suwayomi-server/compare/v1.1.1...v2.0.1727
|
[v2.0.1727]: https://github.com/suwayomi/suwayomi-server/compare/v1.1.1...v2.0.1727
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import java.io.BufferedReader
|
|||||||
const val MainClass = "suwayomi.tachidesk.MainKt"
|
const val MainClass = "suwayomi.tachidesk.MainKt"
|
||||||
|
|
||||||
// should be bumped with each stable release
|
// should be bumped with each stable release
|
||||||
val getTachideskVersion = { "v2.2.${getCommitCount()}" }
|
val getTachideskVersion = { "v2.3.${getCommitCount()}" }
|
||||||
|
|
||||||
val webUIRevisionTag = "r3136"
|
val webUIRevisionTag = "r3147"
|
||||||
|
|
||||||
val webviewJbrRelease = "jbr-release-25.0.3b508.4"
|
val webviewJbrRelease = "jbr-release-25.0.3b508.4"
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ javalin = "7.2.2"
|
|||||||
jte = "3.2.4"
|
jte = "3.2.4"
|
||||||
jackson = "3.2.0" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
|
jackson = "3.2.0" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
|
||||||
exposed = "1.2.0"
|
exposed = "1.2.0"
|
||||||
dex2jar = "2.4.36"
|
dex2jar = "2.4.37"
|
||||||
polyglot = "25.0.3"
|
polyglot = "25.0.3"
|
||||||
settings = "1.3.0"
|
settings = "1.3.0"
|
||||||
twelvemonkeys = "3.13.1"
|
twelvemonkeys = "3.13.1"
|
||||||
@@ -47,6 +47,7 @@ okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
|||||||
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", 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-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp" }
|
||||||
okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", version.ref = "okhttp" }
|
okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", version.ref = "okhttp" }
|
||||||
|
okhttp-zstd = { module = "com.squareup.okhttp3:okhttp-zstd", version.ref = "okhttp" }
|
||||||
okio = "com.squareup.okio:okio:3.17.0"
|
okio = "com.squareup.okio:okio:3.17.0"
|
||||||
|
|
||||||
# Javalin api
|
# Javalin api
|
||||||
@@ -228,6 +229,7 @@ okhttp = [
|
|||||||
"okhttp-logging",
|
"okhttp-logging",
|
||||||
"okhttp-dnsoverhttps",
|
"okhttp-dnsoverhttps",
|
||||||
"okhttp-brotli",
|
"okhttp-brotli",
|
||||||
|
"okhttp-zstd",
|
||||||
]
|
]
|
||||||
javalin = [
|
javalin = [
|
||||||
"javalin-core",
|
"javalin-core",
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ main() {
|
|||||||
set -- "${POSITIONAL_ARGS[@]}"
|
set -- "${POSITIONAL_ARGS[@]}"
|
||||||
|
|
||||||
OS="$1"
|
OS="$1"
|
||||||
JAR="$(ls server/build/*.jar | tail -n1)"
|
JAR="$(ls server/build/*.jar | tail -n1)" || error $LINENO "No JAR found in server/build/" 1
|
||||||
RELEASE_NAME="$(echo "${JAR%.*}" | xargs basename)-$OS"
|
RELEASE_NAME="$(echo "${JAR%.*}" | xargs basename)-$OS"
|
||||||
RELEASE_VERSION=$(echo "$JAR" | grep -oP "v\K[0-9]+\.[0-9]+\.[0-9]+")
|
RELEASE_VERSION=$(echo "$JAR" | grep -oP "v\K[0-9]+\.[0-9]+\.[0-9]+")
|
||||||
#RELEASE_REVISION_NUMBER="$(tmp="${JAR%.*}" && echo "${tmp##*-}" | tr -d r)"
|
#RELEASE_REVISION_NUMBER="$(tmp="${JAR%.*}" && echo "${tmp##*-}" | tr -d r)"
|
||||||
@@ -34,12 +34,12 @@ main() {
|
|||||||
|
|
||||||
# clean temporary directory on function return
|
# clean temporary directory on function return
|
||||||
trap "rm -rf $RELEASE_NAME/" RETURN
|
trap "rm -rf $RELEASE_NAME/" RETURN
|
||||||
mkdir "$RELEASE_NAME/"
|
mkdir "$RELEASE_NAME/" || error $LINENO "Failed to create release directory" 1
|
||||||
|
|
||||||
download_launcher
|
download_launcher
|
||||||
|
|
||||||
if [ ! -f scripts/resources/catch_abort.so ]; then
|
if [ ! -f scripts/resources/catch_abort.so ]; then
|
||||||
gcc -fPIC -shared scripts/resources/catch_abort.c -lpthread -o scripts/resources/catch_abort.so
|
gcc -fPIC -shared scripts/resources/catch_abort.c -lpthread -o scripts/resources/catch_abort.so || error $LINENO "Failed to compile catch_abort" 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
JRE_ZULU="25.34.17_25.0.3"
|
JRE_ZULU="25.34.17_25.0.3"
|
||||||
@@ -49,7 +49,6 @@ main() {
|
|||||||
case "$OS" in
|
case "$OS" in
|
||||||
debian-all)
|
debian-all)
|
||||||
RELEASE="$RELEASE_NAME.deb"
|
RELEASE="$RELEASE_NAME.deb"
|
||||||
download_jogamp "linux-*" # it's easier to bundle them ourselves than to handle Debian's path conventions
|
|
||||||
make_deb_package
|
make_deb_package
|
||||||
move_release_to_output_dir
|
move_release_to_output_dir
|
||||||
;;
|
;;
|
||||||
@@ -57,7 +56,6 @@ main() {
|
|||||||
JRE="$ZULU_RELEASE-ca-$JRE_RELEASE-linux_x64.zip"
|
JRE="$ZULU_RELEASE-ca-$JRE_RELEASE-linux_x64.zip"
|
||||||
JRE_DIR="${JRE%.*}"
|
JRE_DIR="${JRE%.*}"
|
||||||
JRE_URL="https://cdn.azul.com/zulu/bin/$JRE"
|
JRE_URL="https://cdn.azul.com/zulu/bin/$JRE"
|
||||||
download_jogamp "linux-amd64"
|
|
||||||
setup_jre
|
setup_jre
|
||||||
|
|
||||||
RELEASE="$RELEASE_NAME.AppImage"
|
RELEASE="$RELEASE_NAME.AppImage"
|
||||||
@@ -67,7 +65,7 @@ main() {
|
|||||||
linux-assets)
|
linux-assets)
|
||||||
RELEASE="$RELEASE_NAME.tar.gz"
|
RELEASE="$RELEASE_NAME.tar.gz"
|
||||||
copy_linux_package_assets_to "$RELEASE_NAME/"
|
copy_linux_package_assets_to "$RELEASE_NAME/"
|
||||||
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/"
|
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/" || error $LINENO "Failed to create tar.gz" 1
|
||||||
move_release_to_output_dir
|
move_release_to_output_dir
|
||||||
;;
|
;;
|
||||||
linux-x64)
|
linux-x64)
|
||||||
@@ -77,7 +75,6 @@ main() {
|
|||||||
ELECTRON="electron-$electron_version-linux-x64.zip"
|
ELECTRON="electron-$electron_version-linux-x64.zip"
|
||||||
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
|
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
|
||||||
download_electron
|
download_electron
|
||||||
download_jogamp "linux-amd64"
|
|
||||||
setup_jre
|
setup_jre
|
||||||
tree "$RELEASE_NAME"
|
tree "$RELEASE_NAME"
|
||||||
|
|
||||||
@@ -92,7 +89,6 @@ main() {
|
|||||||
ELECTRON="electron-$electron_version-darwin-x64.zip"
|
ELECTRON="electron-$electron_version-darwin-x64.zip"
|
||||||
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
|
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
|
||||||
download_electron
|
download_electron
|
||||||
download_jogamp "macosx-universal"
|
|
||||||
setup_jre
|
setup_jre
|
||||||
tree "$RELEASE_NAME"
|
tree "$RELEASE_NAME"
|
||||||
|
|
||||||
@@ -107,7 +103,6 @@ main() {
|
|||||||
ELECTRON="electron-$electron_version-darwin-arm64.zip"
|
ELECTRON="electron-$electron_version-darwin-arm64.zip"
|
||||||
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
|
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
|
||||||
download_electron
|
download_electron
|
||||||
download_jogamp "macosx-universal"
|
|
||||||
setup_jre
|
setup_jre
|
||||||
tree "$RELEASE_NAME"
|
tree "$RELEASE_NAME"
|
||||||
|
|
||||||
@@ -122,7 +117,6 @@ main() {
|
|||||||
ELECTRON="electron-$electron_version-win32-x64.zip"
|
ELECTRON="electron-$electron_version-win32-x64.zip"
|
||||||
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
|
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
|
||||||
download_electron
|
download_electron
|
||||||
download_jogamp "windows-amd64"
|
|
||||||
setup_jre
|
setup_jre
|
||||||
tree "$RELEASE_NAME"
|
tree "$RELEASE_NAME"
|
||||||
|
|
||||||
@@ -145,52 +139,50 @@ move_release_to_output_dir() {
|
|||||||
if [ -f "$OUTPUT_DIR/$RELEASE" ]; then
|
if [ -f "$OUTPUT_DIR/$RELEASE" ]; then
|
||||||
rm "$OUTPUT_DIR/$RELEASE"
|
rm "$OUTPUT_DIR/$RELEASE"
|
||||||
fi
|
fi
|
||||||
mv "$RELEASE" "$OUTPUT_DIR/"
|
mv "$RELEASE" "$OUTPUT_DIR/" || error $LINENO "Failed to move release to output dir" 1
|
||||||
}
|
}
|
||||||
|
|
||||||
download_launcher() {
|
download_launcher() {
|
||||||
LAUNCHER_URL=$(curl -sf "https://api.github.com/repos/Suwayomi/Suwayomi-Launcher/releases/latest" | grep "browser_download_url" | grep ".jar" | head -n 1 | cut -d '"' -f 4)
|
LAUNCHER_URL=$(curl -sf "https://api.github.com/repos/Suwayomi/Suwayomi-Launcher/releases/latest" | grep "browser_download_url" | grep ".jar" | head -n 1 | cut -d '"' -f 4)
|
||||||
curl -fL "$LAUNCHER_URL" -o "Suwayomi-Launcher.jar"
|
if [ -z "$LAUNCHER_URL" ]; then
|
||||||
mv "Suwayomi-Launcher.jar" "$RELEASE_NAME/Suwayomi-Launcher.jar"
|
error $LINENO "Failed to determine launcher URL from GitHub API" 1
|
||||||
}
|
|
||||||
|
|
||||||
download_jogamp() {
|
|
||||||
local platform="$1"
|
|
||||||
if [ ! -f jogamp-all-platforms.7z ]; then
|
|
||||||
curl -f "https://jogamp.org/deployment/jogamp-current/archive/jogamp-all-platforms.7z" -o jogamp-all-platforms.7z
|
|
||||||
fi
|
fi
|
||||||
|
curl -fL "$LAUNCHER_URL" -o "Suwayomi-Launcher.jar" || error $LINENO "Failed to download launcher JAR" 1
|
||||||
7z x jogamp-all-platforms.7z "jogamp-all-platforms/lib/$platform/"
|
mv "Suwayomi-Launcher.jar" "$RELEASE_NAME/Suwayomi-Launcher.jar" || error $LINENO "Failed to move launcher JAR" 1
|
||||||
mkdir -p "$RELEASE_NAME/natives/"
|
|
||||||
mv jogamp-all-platforms/lib/* "$RELEASE_NAME/natives/"
|
|
||||||
rm -rf jogamp-all-platforms
|
|
||||||
}
|
}
|
||||||
|
|
||||||
download_electron() {
|
download_electron() {
|
||||||
if [ ! -f "$ELECTRON" ]; then
|
if [ ! -f "$ELECTRON" ]; then
|
||||||
curl -fL "$ELECTRON_URL" -o "$ELECTRON"
|
curl -fL "$ELECTRON_URL" -o "$ELECTRON" || error $LINENO "Failed to download electron" 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
unzip "$ELECTRON" -d "$RELEASE_NAME/electron/"
|
unzip "$ELECTRON" -d "$RELEASE_NAME/electron/" || error $LINENO "Failed to extract electron" 1
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_jre() {
|
setup_jre() {
|
||||||
if [ -d "jre" ]; then
|
if [ -d "jre" ]; then
|
||||||
chmod +x ./jre/bin/java
|
if [ ! -f "./jre/bin/java.exe" ]; then
|
||||||
chmod +x ./jre/lib/jspawnhelper
|
chmod +x ./jre/bin/java || error $LINENO "Failed to set java executable permission" 1
|
||||||
mv "jre" "$RELEASE_NAME/jre"
|
chmod +x ./jre/lib/jspawnhelper || error $LINENO "Failed to set jspawnhelper executable permission" 1
|
||||||
|
fi
|
||||||
|
mv "jre" "$RELEASE_NAME/jre" || error $LINENO "Failed to move jre" 1
|
||||||
else
|
else
|
||||||
if [ ! -f "$JRE" ]; then
|
if [ ! -f "$JRE" ]; then
|
||||||
curl -fL "$JRE_URL" -o "$JRE"
|
curl -fL "$JRE_URL" -o "$JRE" || error $LINENO "Failed to download JRE" 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local ext="${JRE##*.}"
|
local ext="${JRE##*.}"
|
||||||
if [ "$ext" = "zip" ]; then
|
if [ "$ext" = "zip" ]; then
|
||||||
unzip "$JRE"
|
unzip "$JRE" || error $LINENO "Failed to extract JRE zip" 1
|
||||||
else
|
else
|
||||||
tar xvf "$JRE"
|
tar xvf "$JRE" || error $LINENO "Failed to extract JRE tar" 1
|
||||||
|
fi
|
||||||
|
mv "$JRE_DIR" "$RELEASE_NAME/jre" || error $LINENO "Failed to move extracted JRE" 1
|
||||||
|
|
||||||
|
if [ ! -f "$RELEASE_NAME/jre/bin/java.exe" ]; then
|
||||||
|
chmod +x "$RELEASE_NAME/jre/bin/java" || error $LINENO "Failed to set java executable permission" 1
|
||||||
|
chmod +x "$RELEASE_NAME/jre/lib/jspawnhelper" || error $LINENO "Failed to set jspawnhelper executable permission" 1
|
||||||
fi
|
fi
|
||||||
mv "$JRE_DIR" "$RELEASE_NAME/jre"
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,31 +190,31 @@ copy_linux_package_assets_to() {
|
|||||||
local output_dir
|
local output_dir
|
||||||
output_dir="$(readlink -e "$1" || exit 1)"
|
output_dir="$(readlink -e "$1" || exit 1)"
|
||||||
|
|
||||||
cp "scripts/resources/pkg/suwayomi-server.sh" "$output_dir/"
|
cp "scripts/resources/pkg/suwayomi-server.sh" "$output_dir/" || error $LINENO "Failed to copy server script" 1
|
||||||
cp "scripts/resources/pkg/suwayomi-server.desktop" "$output_dir/"
|
cp "scripts/resources/pkg/suwayomi-server.desktop" "$output_dir/" || error $LINENO "Failed to copy server desktop file" 1
|
||||||
cp "scripts/resources/pkg/suwayomi-launcher.sh" "$output_dir/"
|
cp "scripts/resources/pkg/suwayomi-launcher.sh" "$output_dir/" || error $LINENO "Failed to copy launcher script" 1
|
||||||
cp "scripts/resources/pkg/suwayomi-launcher.desktop" "$output_dir/"
|
cp "scripts/resources/pkg/suwayomi-launcher.desktop" "$output_dir/" || error $LINENO "Failed to copy launcher desktop file" 1
|
||||||
cp "scripts/resources/pkg/systemd"/* "$output_dir/"
|
cp "scripts/resources/pkg/systemd"/* "$output_dir/" || error $LINENO "Failed to copy systemd files" 1
|
||||||
cp "server/src/main/resources/icon/faviconlogo-128.png" \
|
cp "server/src/main/resources/icon/faviconlogo-128.png" \
|
||||||
"$output_dir/suwayomi-server.png"
|
"$output_dir/suwayomi-server.png" || error $LINENO "Failed to copy icon" 1
|
||||||
}
|
}
|
||||||
|
|
||||||
make_linux_bundle() {
|
make_linux_bundle() {
|
||||||
mkdir "$RELEASE_NAME/bin"
|
mkdir "$RELEASE_NAME/bin"
|
||||||
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar"
|
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar" || error $LINENO "Failed to copy server jar" 1
|
||||||
cp "scripts/resources/suwayomi-launcher.sh" "$RELEASE_NAME/"
|
cp "scripts/resources/suwayomi-launcher.sh" "$RELEASE_NAME/" || error $LINENO "Failed to copy launcher script" 1
|
||||||
cp "scripts/resources/suwayomi-server.sh" "$RELEASE_NAME/"
|
cp "scripts/resources/suwayomi-server.sh" "$RELEASE_NAME/" || error $LINENO "Failed to copy server script" 1
|
||||||
cp "scripts/resources/catch_abort.so" "$RELEASE_NAME/bin/"
|
cp "scripts/resources/catch_abort.so" "$RELEASE_NAME/bin/" || error $LINENO "Failed to copy catch_abort" 1
|
||||||
|
|
||||||
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/"
|
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/" || error $LINENO "Failed to create tar.gz" 1
|
||||||
}
|
}
|
||||||
|
|
||||||
make_macos_bundle() {
|
make_macos_bundle() {
|
||||||
mkdir "$RELEASE_NAME/bin"
|
mkdir "$RELEASE_NAME/bin"
|
||||||
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar"
|
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar" || error $LINENO "Failed to copy server jar" 1
|
||||||
cp "scripts/resources/Suwayomi Launcher.command" "$RELEASE_NAME/"
|
cp "scripts/resources/Suwayomi Launcher.command" "$RELEASE_NAME/" || error $LINENO "Failed to copy launcher command" 1
|
||||||
|
|
||||||
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/"
|
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/" || error $LINENO "Failed to create tar.gz" 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# https://wiki.debian.org/SimplePackagingTutorial
|
# https://wiki.debian.org/SimplePackagingTutorial
|
||||||
@@ -233,28 +225,27 @@ make_deb_package() {
|
|||||||
#behind $RELEASE_VERSION is underscore "_"
|
#behind $RELEASE_VERSION is underscore "_"
|
||||||
local upstream_source="suwayomi-server_$RELEASE_VERSION.orig.tar.gz"
|
local upstream_source="suwayomi-server_$RELEASE_VERSION.orig.tar.gz"
|
||||||
|
|
||||||
mkdir "$RELEASE_NAME/$source_dir/"
|
mkdir "$RELEASE_NAME/$source_dir/" || error $LINENO "Failed to create source directory" 1
|
||||||
mv "$RELEASE_NAME/natives" "$RELEASE_NAME/$source_dir/natives"
|
mv "$RELEASE_NAME/Suwayomi-Launcher.jar" "$RELEASE_NAME/$source_dir/Suwayomi-Launcher.jar" || error $LINENO "Failed to move launcher jar" 1
|
||||||
mv "$RELEASE_NAME/Suwayomi-Launcher.jar" "$RELEASE_NAME/$source_dir/Suwayomi-Launcher.jar"
|
cp "$JAR" "$RELEASE_NAME/$source_dir/Suwayomi-Server.jar" || error $LINENO "Failed to copy server jar" 1
|
||||||
cp "$JAR" "$RELEASE_NAME/$source_dir/Suwayomi-Server.jar"
|
|
||||||
copy_linux_package_assets_to "$RELEASE_NAME/$source_dir/"
|
copy_linux_package_assets_to "$RELEASE_NAME/$source_dir/"
|
||||||
cp "scripts/resources/catch_abort.so" "$RELEASE_NAME/$source_dir/"
|
cp "scripts/resources/catch_abort.so" "$RELEASE_NAME/$source_dir/" || error $LINENO "Failed to copy catch_abort" 1
|
||||||
tar -I "gzip" -C "$RELEASE_NAME/" -cvf "$upstream_source" "$source_dir"
|
tar -I "gzip" -C "$RELEASE_NAME/" -cvf "$upstream_source" "$source_dir" || error $LINENO "Failed to create source tar.gz" 1
|
||||||
|
|
||||||
cp -r "scripts/resources/deb/" "$RELEASE_NAME/$source_dir/debian/"
|
cp -r "scripts/resources/deb/" "$RELEASE_NAME/$source_dir/debian/" || error $LINENO "Failed to copy debian resources" 1
|
||||||
sed -i "s/\$pkgver/$RELEASE_VERSION/" "$RELEASE_NAME/$source_dir/debian/changelog"
|
sed -i "s/\$pkgver/$RELEASE_VERSION/" "$RELEASE_NAME/$source_dir/debian/changelog" || error $LINENO "Failed to update changelog" 1
|
||||||
sed -i "s/\$pkgrel/1/" "$RELEASE_NAME/$source_dir/debian/changelog"
|
sed -i "s/\$pkgrel/1/" "$RELEASE_NAME/$source_dir/debian/changelog" || error $LINENO "Failed to update changelog" 1
|
||||||
|
|
||||||
if [ "${CI:-}" = true ]; then
|
if [ "${CI:-}" = true ]; then
|
||||||
sudo apt update
|
sudo apt update || error $LINENO "Failed to update apt" 1
|
||||||
sudo apt install devscripts build-essential dh-exec
|
sudo apt install devscripts build-essential dh-exec || error $LINENO "Failed to install build deps" 1
|
||||||
fi
|
fi
|
||||||
cd "$RELEASE_NAME/$source_dir/"
|
cd "$RELEASE_NAME/$source_dir/" || error $LINENO "Failed to change directory" 1
|
||||||
dpkg-buildpackage --no-sign --build=all
|
dpkg-buildpackage --no-sign --build=all || error $LINENO "Debian package build failed" 1
|
||||||
cd -
|
cd - || error $LINENO "Failed to return to previous directory" 1
|
||||||
|
|
||||||
local deb="suwayomi-server_$RELEASE_VERSION-1_all.deb"
|
local deb="suwayomi-server_$RELEASE_VERSION-1_all.deb"
|
||||||
mv "$RELEASE_NAME/$deb" "$RELEASE"
|
mv "$RELEASE_NAME/$deb" "$RELEASE" || error $LINENO "Failed to move resulting .deb" 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# https://linuxconfig.org/building-a-hello-world-appimage-on-linux
|
# https://linuxconfig.org/building-a-hello-world-appimage-on-linux
|
||||||
@@ -262,20 +253,20 @@ make_appimage() {
|
|||||||
local APPIMAGE_URL="https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
|
local APPIMAGE_URL="https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
|
||||||
local APPIMAGE_TOOLNAME="appimagetool-x86_64.AppImage"
|
local APPIMAGE_TOOLNAME="appimagetool-x86_64.AppImage"
|
||||||
mkdir "$RELEASE_NAME/bin/"
|
mkdir "$RELEASE_NAME/bin/"
|
||||||
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar"
|
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar" || error $LINENO "Failed to copy server jar" 1
|
||||||
|
|
||||||
cp "scripts/resources/pkg/suwayomi-server.desktop" "$RELEASE_NAME/suwayomi-server.desktop"
|
cp "scripts/resources/pkg/suwayomi-server.desktop" "$RELEASE_NAME/suwayomi-server.desktop" || error $LINENO "Failed to copy desktop file" 1
|
||||||
cp "server/src/main/resources/icon/faviconlogo.png" "$RELEASE_NAME/suwayomi-server.png"
|
cp "server/src/main/resources/icon/faviconlogo.png" "$RELEASE_NAME/suwayomi-server.png" || error $LINENO "Failed to copy icon" 1
|
||||||
cp "scripts/resources/appimage/AppRun" "$RELEASE_NAME/AppRun"
|
cp "scripts/resources/appimage/AppRun" "$RELEASE_NAME/AppRun" || error $LINENO "Failed to copy AppRun" 1
|
||||||
chmod +x "$RELEASE_NAME/AppRun"
|
chmod +x "$RELEASE_NAME/AppRun" || error $LINENO "Failed to make AppRun executable" 1
|
||||||
|
|
||||||
if [ "${CI:-}" = true ]; then
|
if [ "${CI:-}" = true ]; then
|
||||||
sudo apt update
|
sudo apt update || error $LINENO "Failed to update apt" 1
|
||||||
sudo apt install libfuse2
|
sudo apt install libfuse2 || error $LINENO "Failed to install libfuse2" 1
|
||||||
fi
|
fi
|
||||||
curl -fL $APPIMAGE_URL -o $APPIMAGE_TOOLNAME
|
curl -fL $APPIMAGE_URL -o $APPIMAGE_TOOLNAME || error $LINENO "Failed to download appimagetool" 1
|
||||||
chmod +x $APPIMAGE_TOOLNAME
|
chmod +x $APPIMAGE_TOOLNAME || error $LINENO "Failed to make appimagetool executable" 1
|
||||||
ARCH=x86_64 ./$APPIMAGE_TOOLNAME "$RELEASE_NAME" "$RELEASE"
|
ARCH=x86_64 ./$APPIMAGE_TOOLNAME "$RELEASE_NAME" "$RELEASE" || error $LINENO "AppImage creation failed" 1
|
||||||
}
|
}
|
||||||
|
|
||||||
make_windows_bundle() {
|
make_windows_bundle() {
|
||||||
@@ -300,7 +291,7 @@ make_windows_bundle() {
|
|||||||
#local rcedit_url="https://github.com/electron/rcedit/releases/download/v0.1.1/$rcedit"
|
#local rcedit_url="https://github.com/electron/rcedit/releases/download/v0.1.1/$rcedit"
|
||||||
## change electron's icon
|
## change electron's icon
|
||||||
#if [ ! -f "$rcedit" ]; then
|
#if [ ! -f "$rcedit" ]; then
|
||||||
#curl -fL "$rcedit_url" -o "$rcedit"
|
#curl -fL "$rcedit_url" -o "$rcedit" || error $LINENO "Failed to download rcedit" 1
|
||||||
#fi
|
#fi
|
||||||
|
|
||||||
#local icon="server/src/main/resources/icon/faviconlogo.ico"
|
#local icon="server/src/main/resources/icon/faviconlogo.ico"
|
||||||
@@ -308,38 +299,35 @@ make_windows_bundle() {
|
|||||||
# --set-icon "$icon"
|
# --set-icon "$icon"
|
||||||
|
|
||||||
mkdir "$RELEASE_NAME/bin"
|
mkdir "$RELEASE_NAME/bin"
|
||||||
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar"
|
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar" || error $LINENO "Failed to copy server jar" 1
|
||||||
cp "scripts/resources/Suwayomi Launcher.bat" "$RELEASE_NAME"
|
cp "scripts/resources/Suwayomi Launcher.bat" "$RELEASE_NAME" || error $LINENO "Failed to copy launcher bat" 1
|
||||||
|
|
||||||
zip -9 -r "$RELEASE" "$RELEASE_NAME"
|
zip -9 -r "$RELEASE" "$RELEASE_NAME" || error $LINENO "Failed to create windows bundle zip" 1
|
||||||
}
|
}
|
||||||
|
|
||||||
make_windows_package() {
|
make_windows_package() {
|
||||||
if [ "${CI:-}" = true ]; then
|
if [ "${CI:-}" = true ]; then
|
||||||
sudo apt update
|
sudo apt update || error $LINENO "Failed to update apt" 1
|
||||||
sudo apt install -y wixl
|
sudo apt install -y wixl || error $LINENO "Failed to install wixl" 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
find "$RELEASE_NAME/jre" \
|
find "$RELEASE_NAME/jre" \
|
||||||
| wixl-heat --var var.SourceDir -p "$RELEASE_NAME/" \
|
| wixl-heat --var var.SourceDir -p "$RELEASE_NAME/" \
|
||||||
--directory-ref jre --component-group jre >"$RELEASE_NAME/jre.wxs"
|
--directory-ref jre --component-group jre >"$RELEASE_NAME/jre.wxs" || error $LINENO "Failed to heat jre" 1
|
||||||
find "$RELEASE_NAME/electron" \
|
find "$RELEASE_NAME/electron" \
|
||||||
| wixl-heat --var var.SourceDir -p "$RELEASE_NAME/" \
|
| wixl-heat --var var.SourceDir -p "$RELEASE_NAME/" \
|
||||||
--directory-ref electron --component-group electron >"$RELEASE_NAME/electron.wxs"
|
--directory-ref electron --component-group electron >"$RELEASE_NAME/electron.wxs" || error $LINENO "Failed to heat electron" 1
|
||||||
find "$RELEASE_NAME/natives" \
|
|
||||||
| wixl-heat --var var.SourceDir -p "$RELEASE_NAME/" \
|
|
||||||
--directory-ref natives --component-group natives >"$RELEASE_NAME/natives.wxs"
|
|
||||||
|
|
||||||
find "$RELEASE_NAME/bin" \
|
find "$RELEASE_NAME/bin" \
|
||||||
| wixl-heat --var var.SourceDir -p "$RELEASE_NAME/" \
|
| wixl-heat --var var.SourceDir -p "$RELEASE_NAME/" \
|
||||||
--directory-ref bin --component-group bin >"$RELEASE_NAME/bin.wxs"
|
--directory-ref bin --component-group bin >"$RELEASE_NAME/bin.wxs" || error $LINENO "Failed to heat bin" 1
|
||||||
|
|
||||||
local icon="server/src/main/resources/icon/faviconlogo.ico"
|
local icon="server/src/main/resources/icon/faviconlogo.ico"
|
||||||
local arch=${OS##*-}
|
local arch=${OS##*-}
|
||||||
|
|
||||||
wixl -D ProductVersion="$RELEASE_VERSION" -D SourceDir="$RELEASE_NAME" \
|
wixl -D ProductVersion="$RELEASE_VERSION" -D SourceDir="$RELEASE_NAME" \
|
||||||
-D Icon="$icon" --arch "$arch" "scripts/resources/msi/suwayomi-server-$arch.wxs" \
|
-D Icon="$icon" --arch "$arch" "scripts/resources/msi/suwayomi-server-$arch.wxs" \
|
||||||
"$RELEASE_NAME/jre.wxs" "$RELEASE_NAME/electron.wxs" "$RELEASE_NAME/natives.wxs" "$RELEASE_NAME/bin.wxs" -o "$RELEASE"
|
"$RELEASE_NAME/jre.wxs" "$RELEASE_NAME/electron.wxs" "$RELEASE_NAME/bin.wxs" -o "$RELEASE" || error $LINENO "Windows package build failed" 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Error handler
|
# Error handler
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
Suwayomi-Server.jar usr/share/java/suwayomi-server/bin/
|
Suwayomi-Server.jar usr/share/java/suwayomi-server/bin/
|
||||||
Suwayomi-Launcher.jar usr/share/java/suwayomi-server/
|
Suwayomi-Launcher.jar usr/share/java/suwayomi-server/
|
||||||
natives/* usr/share/java/suwayomi-server/natives/
|
|
||||||
suwayomi-server.png usr/share/pixmaps/
|
suwayomi-server.png usr/share/pixmaps/
|
||||||
suwayomi-server.desktop usr/share/applications/
|
suwayomi-server.desktop usr/share/applications/
|
||||||
suwayomi-launcher.desktop usr/share/applications/
|
suwayomi-launcher.desktop usr/share/applications/
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
<Directory Id="INSTALLDIR" Name="Suwayomi-Server" >
|
<Directory Id="INSTALLDIR" Name="Suwayomi-Server" >
|
||||||
<Directory Id="jre"/>
|
<Directory Id="jre"/>
|
||||||
<Directory Id="electron"/>
|
<Directory Id="electron"/>
|
||||||
<Directory Id="natives"/>
|
|
||||||
<Directory Id="bin"/>
|
<Directory Id="bin"/>
|
||||||
</Directory>
|
</Directory>
|
||||||
</Directory>
|
</Directory>
|
||||||
@@ -63,7 +62,6 @@
|
|||||||
<ComponentRef Id="SuwayomiLauncherBAT" />
|
<ComponentRef Id="SuwayomiLauncherBAT" />
|
||||||
<ComponentRef Id="ProgramMenuDir" />
|
<ComponentRef Id="ProgramMenuDir" />
|
||||||
<ComponentGroupRef Id="electron" />
|
<ComponentGroupRef Id="electron" />
|
||||||
<ComponentGroupRef Id="natives" />
|
|
||||||
</Feature>
|
</Feature>
|
||||||
|
|
||||||
<Icon Id="Suwayomi.ico" SourceFile="$(var.Icon)" />
|
<Icon Id="Suwayomi.ico" SourceFile="$(var.Icon)" />
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
<!-- OPDS errors -->
|
<!-- OPDS errors -->
|
||||||
<string name="opds_error_manga_not_found">Series with ID %1$d not found.</string>
|
<string name="opds_error_manga_not_found">Series with ID %1$d not found.</string>
|
||||||
<string name="opds_error_chapter_not_found">Chapter with index %1$d not found.</string>
|
<string name="opds_error_chapter_not_found">Chapter with index %1$d not found.</string>
|
||||||
|
<string name="opds_error_chapters_not_found">No chapters found or the source is unreachable on page %1$d.</string>
|
||||||
|
|
||||||
<!-- OPDS facets (Filters and Sorting) -->
|
<!-- OPDS facets (Filters and Sorting) -->
|
||||||
<string name="opds_facetgroup_sort_order">Sort Order</string>
|
<string name="opds_facetgroup_sort_order">Sort Order</string>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<string name="opds_feeds_root">Suwayomi OPDS Katalog</string>
|
<string name="opds_feeds_root">Suwayomi OPDS Katalog</string>
|
||||||
<string name="opds_feeds_chapter_details">%1$s | %2$s | Details</string>
|
<string name="opds_feeds_chapter_details">%1$s | %2$s | Details</string>
|
||||||
<string name="opds_feeds_sources_title">Alle Quellen</string>
|
<string name="opds_feeds_sources_title">Alle Quellen</string>
|
||||||
<string name="opds_feeds_genres_title">Genres</string>
|
<string name="opds_feeds_genres_title">Genren</string>
|
||||||
<string name="opds_feeds_genres_entry_content">Durchsuche Serien nach Genre</string>
|
<string name="opds_feeds_genres_entry_content">Durchsuche Serien nach Genre</string>
|
||||||
<string name="opds_feeds_status_entry_content">Durchsuche Serien nach Publikationsstatus</string>
|
<string name="opds_feeds_status_entry_content">Durchsuche Serien nach Publikationsstatus</string>
|
||||||
<string name="opds_feeds_languages_title">Sprachen</string>
|
<string name="opds_feeds_languages_title">Sprachen</string>
|
||||||
@@ -122,4 +122,7 @@
|
|||||||
<string name="webview_label_login_required">Deine Konfiguration erfordert die Anmeldung. Bitte gib Benutzername und Passwort ein.</string>
|
<string name="webview_label_login_required">Deine Konfiguration erfordert die Anmeldung. Bitte gib Benutzername und Passwort ein.</string>
|
||||||
<string name="opds_linktitle_first_page">Erste Seite</string>
|
<string name="opds_linktitle_first_page">Erste Seite</string>
|
||||||
<string name="opds_linktitle_last_page">Letzte Seite</string>
|
<string name="opds_linktitle_last_page">Letzte Seite</string>
|
||||||
|
<string name="opds_error_chapters_not_found">Keine Kapitel gefunden oder die Quelle ist nicht erreichbar auf Seite %1$d.</string>
|
||||||
|
<string name="opds_chapter_title_fallback">Kapitel %1$s</string>
|
||||||
|
<string name="opds_chapter_title_oneshot">Oneshot</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
127
server/i18n/src/commonMain/moko-resources/values/el/strings.xml
Normal file
127
server/i18n/src/commonMain/moko-resources/values/el/strings.xml
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="opds_feeds_root">Suwayomi OPDS Κατάλογος</string>
|
||||||
|
<string name="opds_feeds_manga_chapters">Κεφάλαια %1$s</string>
|
||||||
|
<string name="opds_feeds_chapter_details">%1$s | %2$s | Λεπτομέρειες</string>
|
||||||
|
<string name="opds_feeds_explore_title">Εξερεύνηση</string>
|
||||||
|
<string name="opds_feeds_explore_entry_content">Εξερεύνησε νέες σειρές από τις πηγές σου</string>
|
||||||
|
<string name="opds_feeds_history_title">Ιστορικό</string>
|
||||||
|
<string name="opds_feeds_history_entry_content">Πρόσφατα διαβασμένα κεφάλαια</string>
|
||||||
|
<string name="opds_feeds_all_series_in_library_title">Όλες οι σειρές</string>
|
||||||
|
<string name="opds_feeds_all_series_in_library_entry_content">Περιήγηση σε όλες τις σειρές της βιβλιοθήκης σου</string>
|
||||||
|
<string name="opds_feeds_library_sources_title">Πηγές</string>
|
||||||
|
<string name="opds_feeds_library_sources_entry_content">Περιήγηση σε σειρές της βιβλιοθήκης σου φιλτραρισμένες κατά πηγή</string>
|
||||||
|
<string name="opds_feeds_categories_title">Κατηγορίες</string>
|
||||||
|
<string name="opds_feeds_categories_entry_content">Περιήγηση σε σειρές οργανωμένες κατά κατηγορία</string>
|
||||||
|
<string name="opds_feeds_genres_title">Είδη</string>
|
||||||
|
<string name="opds_feeds_genres_entry_content">Περιήγηση σε σειρές κατά ετικέτες είδους</string>
|
||||||
|
<string name="opds_feeds_status_title">Κατάσταση</string>
|
||||||
|
<string name="opds_feeds_status_entry_content">Περιήγηση σε σειρές κατά κατάσταση δημοσίευσης</string>
|
||||||
|
<string name="opds_feeds_languages_title">Γλώσσες</string>
|
||||||
|
<string name="opds_feeds_languages_entry_content">Περιήγηση σε σειρές κατά γλώσσα περιεχομένου</string>
|
||||||
|
<string name="opds_feeds_library_updates_title">Ιστορικό ενημερώσεων βιβλιοθήκης</string>
|
||||||
|
<string name="opds_feeds_library_updates_entry_content">Πρόσφατα ενημερωμένα κεφάλαια από τη βιβλιοθήκη σου</string>
|
||||||
|
<string name="opds_feeds_search_results_title">Αποτελέσματα αναζήτησης</string>
|
||||||
|
<string name="opds_feeds_sources_title">Όλες οι πηγές</string>
|
||||||
|
<string name="opds_feeds_category_specific_title">Κατηγορία: %1$s</string>
|
||||||
|
<string name="opds_feeds_genre_specific_title">Είδος: %1$s</string>
|
||||||
|
<string name="opds_feeds_status_specific_title">Κατάσταση: %1$s</string>
|
||||||
|
<string name="opds_feeds_language_specific_title">Γλώσσα: %1$s</string>
|
||||||
|
<string name="opds_feeds_source_specific_title">Πηγή: %1$s</string>
|
||||||
|
<string name="opds_feeds_library_source_specific_title">Βιβλιοθήκη - Πηγή: %1$s</string>
|
||||||
|
<string name="opds_feeds_source_specific_popular_title">Πηγή: %1$s - Δημοφιλές</string>
|
||||||
|
<string name="opds_feeds_source_specific_latest_title">Πηγή: %1$s - Τελευταίο</string>
|
||||||
|
<string name="opds_search_shortname">Suwayomi OPDS Αναζήτηση</string>
|
||||||
|
<string name="opds_search_description">Αναζήτηση σειρών στον κατάλογο.</string>
|
||||||
|
<string name="opds_error_manga_not_found">Η σειρά με ID %1$d δεν βρέθηκε.</string>
|
||||||
|
<string name="opds_error_chapter_not_found">Το κεφάλαιο με δείκτη %1$d δεν βρέθηκε.</string>
|
||||||
|
<string name="opds_facetgroup_sort_order">Σειρά ταξινόμησης</string>
|
||||||
|
<string name="opds_facetgroup_filter_read_status">Φίλτρο κατά κατάσταση ανάγνωσης</string>
|
||||||
|
<string name="opds_facetgroup_filter_content">Φίλτρο περιεχομένου</string>
|
||||||
|
<string name="opds_facetgroup_filter_source">Φίλτρο κατά πηγή</string>
|
||||||
|
<string name="opds_facetgroup_filter_category">Φίλτρο κατά κατηγορία</string>
|
||||||
|
<string name="opds_facetgroup_filter_status">Φίλτρο κατά κατάσταση</string>
|
||||||
|
<string name="opds_facetgroup_filter_language">Φίλτρο κατά γλώσσα</string>
|
||||||
|
<string name="opds_facetgroup_filter_genre">Φίλτρο κατά είδος</string>
|
||||||
|
<string name="opds_facet_sort_oldest_first">Παλαιότερα πρώτα</string>
|
||||||
|
<string name="opds_facet_sort_newest_first">Νεότερα πρώτα</string>
|
||||||
|
<string name="opds_facet_sort_date_asc">Ημερομηνία αύξουσα</string>
|
||||||
|
<string name="opds_facet_sort_date_desc">Ημερομηνία φθίνουσα</string>
|
||||||
|
<string name="opds_facet_sort_popular">Δημοφιλές</string>
|
||||||
|
<string name="opds_facet_sort_latest">Τελευταίο</string>
|
||||||
|
<string name="opds_facet_sort_alpha_asc">Αλφαβητικά Α-Ω</string>
|
||||||
|
<string name="opds_facet_sort_alpha_desc">Αλφαβητικά Ω-Α</string>
|
||||||
|
<string name="opds_facet_sort_last_read_desc">Τελευταία ανάγνωση</string>
|
||||||
|
<string name="opds_facet_sort_latest_chapter_desc">Τελευταίο κεφάλαιο</string>
|
||||||
|
<string name="opds_facet_sort_date_added_desc">Ημερομηνία προσθήκης</string>
|
||||||
|
<string name="opds_facet_sort_unread_desc">Αδιάβαστα κεφάλαια</string>
|
||||||
|
<string name="opds_facet_filter_all">Όλα</string>
|
||||||
|
<string name="opds_facet_filter_all_chapters">Όλα τα κεφάλαια</string>
|
||||||
|
<string name="opds_facet_filter_unread_only">Αδιάβαστα</string>
|
||||||
|
<string name="opds_facet_filter_read_only">Διαβασμένα</string>
|
||||||
|
<string name="opds_facet_filter_downloaded">Ληφθέντα</string>
|
||||||
|
<string name="opds_facet_filter_ongoing">Σε εξέλιξη</string>
|
||||||
|
<string name="opds_facet_filter_completed">Ολοκληρωμένα</string>
|
||||||
|
<string name="opds_facet_all_sources">Όλες οι πηγές</string>
|
||||||
|
<string name="opds_facet_all_categories">Όλες οι κατηγορίες</string>
|
||||||
|
<string name="opds_facet_all_statuses">Όλες οι καταστάσεις</string>
|
||||||
|
<string name="opds_facet_all_languages">Όλες οι γλώσσες</string>
|
||||||
|
<string name="opds_facet_all_genres">Όλα τα είδη</string>
|
||||||
|
<string name="opds_linktitle_catalog_root">Ρίζα καταλόγου</string>
|
||||||
|
<string name="opds_linktitle_search_catalog">Αναζήτηση καταλόγου</string>
|
||||||
|
<string name="opds_linktitle_first_page">Πρώτη σελίδα</string>
|
||||||
|
<string name="opds_linktitle_previous_page">Προηγούμενη σελίδα</string>
|
||||||
|
<string name="opds_linktitle_next_page">Επόμενη σελίδα</string>
|
||||||
|
<string name="opds_linktitle_last_page">Τελευταία σελίδα</string>
|
||||||
|
<string name="opds_linktitle_self_feed">Τρέχουσα ροή</string>
|
||||||
|
<string name="opds_linktitle_view_on_web">Προβολή στο Web</string>
|
||||||
|
<string name="opds_linktitle_stream_pages_start">Διάβασε Online</string>
|
||||||
|
<string name="opds_linktitle_stream_pages_continue">Συνέχισε να διαβάζεις Online</string>
|
||||||
|
<string name="opds_linktitle_stream_pages_start_local">Διάβασε Online (Τοπική πρόοδος)</string>
|
||||||
|
<string name="opds_linktitle_stream_pages_continue_local">Συνέχισε να διαβάζεις Online (Τοπική πρόοδος)</string>
|
||||||
|
<string name="opds_linktitle_stream_pages_start_remote">Διάβασε Online (Συγχρονισμένο από %1$s)</string>
|
||||||
|
<string name="opds_linktitle_stream_pages_continue_remote">Συνέχισε να διαβάζεις Online (Συγχρονισμένο από %1$s)</string>
|
||||||
|
<string name="opds_linktitle_download_cbz">Λήψη CBZ</string>
|
||||||
|
<string name="opds_linktitle_chapter_cover">Εξώφυλλο κεφαλαίου</string>
|
||||||
|
<string name="opds_linktitle_view_chapter_details">Λεπτομέρειες κεφαλαίου & Σελίδες</string>
|
||||||
|
<string name="opds_chapter_status_read">✅</string>
|
||||||
|
<string name="opds_chapter_status_in_progress">⌛</string>
|
||||||
|
<string name="opds_chapter_status_unread">⭕</string>
|
||||||
|
<string name="opds_chapter_status_downloaded">⬇️</string>
|
||||||
|
<string name="opds_chapter_status_synced">🌐</string>
|
||||||
|
<string name="opds_chapter_details_base">Σειρά: %1$s | %2$s</string>
|
||||||
|
<string name="opds_chapter_details_scanlator">| Από %1$s</string>
|
||||||
|
<string name="opds_chapter_details_progress">| Πρόοδος: %1$d από %2$d</string>
|
||||||
|
<string name="opds_manga_summary_status">Κατάσταση: %1$s</string>
|
||||||
|
<string name="opds_manga_summary_source">Πηγή: %1$s</string>
|
||||||
|
<string name="opds_manga_summary_language">Γλώσσα: %1$s</string>
|
||||||
|
<string name="manga_status_ongoing">Σε εξέλιξη</string>
|
||||||
|
<string name="manga_status_completed">Ολοκληρωμένο</string>
|
||||||
|
<string name="manga_status_licensed">Υπό άδεια</string>
|
||||||
|
<string name="manga_status_publishing_finished">Δημοσίευση ολοκληρωμένη</string>
|
||||||
|
<string name="manga_status_cancelled">Ακυρωμένο</string>
|
||||||
|
<string name="manga_status_on_hiatus">Σε παύση</string>
|
||||||
|
<string name="manga_status_unknown">Άγνωστο</string>
|
||||||
|
<string name="label_error">Σφάλμα</string>
|
||||||
|
<string name="label_version">Έκδοση <xliff:g id="version" example="v2.0.1833">%1$s</xliff:g></string>
|
||||||
|
<string name="label_close">Κλείσε</string>
|
||||||
|
<string name="webview_label_title">Suwayomi WebView</string>
|
||||||
|
<string name="webview_label_disconnected">Αποσυνδεδεμένο, κάνε ανανέωση</string>
|
||||||
|
<string name="webview_label_reversescroll">Αντίστροφη κύλιση</string>
|
||||||
|
<string name="webview_label_bindingshint">Σημείωση: Όταν η εστίαση είναι στο τμήμα WebView, καμία συντόμευση πληκτρολογίου, συμπεριλαμβανομένης της ανανέωσης, δεν θα αντιμετωπίζεται από το πρόγραμμα περιήγησης.</string>
|
||||||
|
<string name="webview_label_init">Αρχικοποίηση... Παρακαλώ περίμενε</string>
|
||||||
|
<string name="webview_label_getstarted">Εισήγαγε μια διεύθυνση URL για να ξεκινήσεις</string>
|
||||||
|
<string name="webview_label_loading">Φόρτωση σελίδας...</string>
|
||||||
|
<string name="webview_label_copy">Αντιγραφή στο πρόχειρο</string>
|
||||||
|
<string name="webview_label_copy_description">Η αυτόματη αντιγραφή στο πρόχειρο απέτυχε, χρησιμοποίησε το παρακάτω πεδίο για να αντιγράψεις χειροκίνητα την τιμή.</string>
|
||||||
|
<string name="webview_label_login_required">Η διαμόρφωσή σου απαιτεί σύνδεση. Εισήγαγε όνομα χρήστη και κωδικό.</string>
|
||||||
|
<string name="webview_placeholder_url">Εισήγαγε URL...</string>
|
||||||
|
<string name="login_label_title">Suwayomi Σύνδεση</string>
|
||||||
|
<string name="login_label_username">Όνομα χρήστη</string>
|
||||||
|
<string name="login_label_password">Κωδικός</string>
|
||||||
|
<string name="login_label_login">Σύνδεση</string>
|
||||||
|
<string name="login_placeholder_username">Πληκτρολόγησε όνομα χρήστη...</string>
|
||||||
|
<string name="login_placeholder_password">Μυστικό...</string>
|
||||||
|
<string name="opds_error_chapters_not_found">Δεν βρέθηκαν κεφάλαια ή η πηγή είναι μη διαθέσιμη στη σελίδα %1$d.</string>
|
||||||
|
<string name="opds_chapter_title_fallback">Κεφάλαιο %1$s</string>
|
||||||
|
</resources>
|
||||||
@@ -122,4 +122,6 @@
|
|||||||
<string name="webview_label_login_required">Su configuración requiere que inicie sesión. Introduzca su nombre de usuario y contraseña.</string>
|
<string name="webview_label_login_required">Su configuración requiere que inicie sesión. Introduzca su nombre de usuario y contraseña.</string>
|
||||||
<string name="opds_linktitle_first_page">Primera página</string>
|
<string name="opds_linktitle_first_page">Primera página</string>
|
||||||
<string name="opds_linktitle_last_page">Última página</string>
|
<string name="opds_linktitle_last_page">Última página</string>
|
||||||
|
<string name="opds_error_chapters_not_found">No se encontraron capítulos o la fuente no está disponible en la página %1$d.</string>
|
||||||
|
<string name="opds_chapter_title_fallback">Capítulo %1$s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -122,4 +122,7 @@
|
|||||||
<string name="login_label_login">Se connecter</string>
|
<string name="login_label_login">Se connecter</string>
|
||||||
<string name="login_placeholder_username">Tapez le nom d\'utilisateur…</string>
|
<string name="login_placeholder_username">Tapez le nom d\'utilisateur…</string>
|
||||||
<string name="login_placeholder_password">Secret…</string>
|
<string name="login_placeholder_password">Secret…</string>
|
||||||
|
<string name="opds_error_chapters_not_found">Aucun chapitre trouvé ou la source est inaccessible à la page %1$d.</string>
|
||||||
|
<string name="opds_chapter_title_fallback">Chapitre %1$s</string>
|
||||||
|
<string name="opds_chapter_title_oneshot">One shot</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -122,4 +122,6 @@
|
|||||||
<string name="login_label_login">Accedi</string>
|
<string name="login_label_login">Accedi</string>
|
||||||
<string name="login_placeholder_username">Digita il nome utente...</string>
|
<string name="login_placeholder_username">Digita il nome utente...</string>
|
||||||
<string name="login_placeholder_password">Segreto...</string>
|
<string name="login_placeholder_password">Segreto...</string>
|
||||||
|
<string name="opds_error_chapters_not_found">Nessun capitolo trovato o la fonte non è raggiungibile alla pagina %1$d.</string>
|
||||||
|
<string name="opds_chapter_title_fallback">Capitolo %1$s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -60,4 +60,7 @@
|
|||||||
<string name="opds_feeds_library_sources_title">ソース</string>
|
<string name="opds_feeds_library_sources_title">ソース</string>
|
||||||
<string name="opds_feeds_library_sources_entry_content">ソース別にライブラリ内のマンガを閲覧</string>
|
<string name="opds_feeds_library_sources_entry_content">ソース別にライブラリ内のマンガを閲覧</string>
|
||||||
<string name="opds_feeds_search_results_title">検索結果</string>
|
<string name="opds_feeds_search_results_title">検索結果</string>
|
||||||
|
<string name="opds_error_chapters_not_found">ページ %1$d で章が見つからないか、ソースに接続できません。</string>
|
||||||
|
<string name="opds_chapter_title_oneshot">読み切り</string>
|
||||||
|
<string name="opds_chapter_title_fallback">第 %1$s 話</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -76,4 +76,6 @@
|
|||||||
<string name="opds_facet_filter_all">Wszystkie</string>
|
<string name="opds_facet_filter_all">Wszystkie</string>
|
||||||
<string name="opds_facet_filter_downloaded">Pobrane</string>
|
<string name="opds_facet_filter_downloaded">Pobrane</string>
|
||||||
<string name="opds_facet_filter_ongoing">Trwające</string>
|
<string name="opds_facet_filter_ongoing">Trwające</string>
|
||||||
|
<string name="opds_error_chapters_not_found">Nie znaleziono rozdziałów lub źródło jest nieosiągalne na stronie %1$d.</string>
|
||||||
|
<string name="opds_chapter_title_fallback">Rozdział %1$s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -122,4 +122,6 @@
|
|||||||
<string name="login_label_login">Entrar</string>
|
<string name="login_label_login">Entrar</string>
|
||||||
<string name="login_placeholder_username">Digite o nome de usuário...</string>
|
<string name="login_placeholder_username">Digite o nome de usuário...</string>
|
||||||
<string name="login_placeholder_password">Segredo...</string>
|
<string name="login_placeholder_password">Segredo...</string>
|
||||||
|
<string name="opds_error_chapters_not_found">Nenhum capítulo encontrado ou a fonte está inacessível na página %1$d.</string>
|
||||||
|
<string name="opds_chapter_title_fallback">Capítulo %1$s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -122,4 +122,7 @@
|
|||||||
<string name="opds_search_description">Ищите тайтлы в каталоге.</string>
|
<string name="opds_search_description">Ищите тайтлы в каталоге.</string>
|
||||||
<string name="opds_error_manga_not_found">Тайтл с ID %1$d не найден.</string>
|
<string name="opds_error_manga_not_found">Тайтл с ID %1$d не найден.</string>
|
||||||
<string name="opds_chapter_details_base">Тайтл: %1$s | %2$s</string>
|
<string name="opds_chapter_details_base">Тайтл: %1$s | %2$s</string>
|
||||||
|
<string name="opds_error_chapters_not_found">Главы не найдены или источник недоступен на странице %1$d.</string>
|
||||||
|
<string name="opds_chapter_title_fallback">Глава %1$s</string>
|
||||||
|
<string name="opds_chapter_title_oneshot">Ваншот</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -53,4 +53,7 @@
|
|||||||
<string name="opds_chapter_status_unread">⭕</string>
|
<string name="opds_chapter_status_unread">⭕</string>
|
||||||
<string name="opds_chapter_details_base">%1$s | %2$s</string>
|
<string name="opds_chapter_details_base">%1$s | %2$s</string>
|
||||||
<string name="opds_feeds_genre_specific_title">இசைவகை: %1$s</string>
|
<string name="opds_feeds_genre_specific_title">இசைவகை: %1$s</string>
|
||||||
|
<string name="opds_error_chapters_not_found">பக்கம் %1$d இல் அத்தியாயங்கள் எதுவும் காணப்படவில்லை அல்லது மூலத்தை அணுக முடியவில்லை.</string>
|
||||||
|
<string name="opds_chapter_title_oneshot">ஒன்-ஷாட்</string>
|
||||||
|
<string name="opds_chapter_title_fallback">அத்தியாயம் %1$s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -122,4 +122,6 @@
|
|||||||
<string name="webview_label_login_required">Cấu hình của bạn yêu cầu bạn phải đăng nhập. Vui lòng nhập tên người dùng và mật khẩu.</string>
|
<string name="webview_label_login_required">Cấu hình của bạn yêu cầu bạn phải đăng nhập. Vui lòng nhập tên người dùng và mật khẩu.</string>
|
||||||
<string name="opds_linktitle_first_page">Trang đầu</string>
|
<string name="opds_linktitle_first_page">Trang đầu</string>
|
||||||
<string name="opds_linktitle_last_page">Trang cuối</string>
|
<string name="opds_linktitle_last_page">Trang cuối</string>
|
||||||
|
<string name="opds_error_chapters_not_found">Không tìm thấy chương nào hoặc nguồn không thể truy cập tại trang %1$d.</string>
|
||||||
|
<string name="opds_chapter_title_fallback">Chương %1$s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -122,4 +122,7 @@
|
|||||||
<string name="login_placeholder_username">输入用户名…</string>
|
<string name="login_placeholder_username">输入用户名…</string>
|
||||||
<string name="login_placeholder_password">密匙…</string>
|
<string name="login_placeholder_password">密匙…</string>
|
||||||
<string name="label_error">错误</string>
|
<string name="label_error">错误</string>
|
||||||
|
<string name="opds_error_chapters_not_found">第 %1$d 页未找到任何章节,或图源无法访问。</string>
|
||||||
|
<string name="opds_chapter_title_fallback">第 %1$s 章</string>
|
||||||
|
<string name="opds_chapter_title_oneshot">单篇</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -284,8 +284,7 @@ class ServerConfig(
|
|||||||
defaultValue = emptyList(),
|
defaultValue = emptyList(),
|
||||||
deprecated =
|
deprecated =
|
||||||
SettingsRegistry.SettingDeprecated(
|
SettingsRegistry.SettingDeprecated(
|
||||||
replaceWith = "extensionStores",
|
message = "Replaced with addExtensionStore and removeExtensionStore mutations",
|
||||||
message = "Replaced with extensionStores",
|
|
||||||
migrateConfigValue = {
|
migrateConfigValue = {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
(it.unwrapped() as? List<String>)
|
(it.unwrapped() as? List<String>)
|
||||||
@@ -304,7 +303,7 @@ class ServerConfig(
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
readMigrated = { extensionStores.value },
|
readMigrated = { extensionStores.value },
|
||||||
setMigrated = { extensionStores.value = (extensionStores.value + it).distinct() },
|
setMigrated = { extensionStores.value = it.distinct() },
|
||||||
typeInfo =
|
typeInfo =
|
||||||
SettingsRegistry.PartialTypeInfo(
|
SettingsRegistry.PartialTypeInfo(
|
||||||
specificType = "List<String>",
|
specificType = "List<String>",
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ package eu.kanade.tachiyomi.network
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
|
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
|
||||||
import eu.kanade.tachiyomi.network.interceptor.IgnoreGzipInterceptor
|
|
||||||
import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
|
import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
|
||||||
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
|
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
@@ -22,9 +21,8 @@ import kotlinx.coroutines.flow.launchIn
|
|||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import okhttp3.Cache
|
import okhttp3.Cache
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.brotli.BrotliInterceptor
|
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
||||||
import java.net.CookieHandler
|
import java.net.CookieHandler
|
||||||
import java.net.CookieManager
|
import java.net.CookieManager
|
||||||
import java.net.CookiePolicy
|
import java.net.CookiePolicy
|
||||||
@@ -64,7 +62,7 @@ class NetworkHelper(
|
|||||||
userAgent
|
userAgent
|
||||||
.drop(1)
|
.drop(1)
|
||||||
.onEach {
|
.onEach {
|
||||||
GetCatalogueSource.unregisterAllCatalogueSources() // need to reset the headers
|
GetSource.unregisterAllSources() // need to reset the headers
|
||||||
}.launchIn(GlobalScope)
|
}.launchIn(GlobalScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,8 +82,6 @@ class NetworkHelper(
|
|||||||
),
|
),
|
||||||
).addInterceptor(UncaughtExceptionInterceptor())
|
).addInterceptor(UncaughtExceptionInterceptor())
|
||||||
.addInterceptor(UserAgentInterceptor(::defaultUserAgentProvider))
|
.addInterceptor(UserAgentInterceptor(::defaultUserAgentProvider))
|
||||||
.addNetworkInterceptor(IgnoreGzipInterceptor())
|
|
||||||
.addNetworkInterceptor(BrotliInterceptor)
|
|
||||||
|
|
||||||
// if (preferences.verboseLogging().get()) {
|
// if (preferences.verboseLogging().get()) {
|
||||||
val httpLoggingInterceptor =
|
val httpLoggingInterceptor =
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.network.interceptor
|
|
||||||
|
|
||||||
import okhttp3.Interceptor
|
|
||||||
import okhttp3.Response
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To use [okhttp3.brotli.BrotliInterceptor] as a network interceptor,
|
|
||||||
* add [IgnoreGzipInterceptor] right before it.
|
|
||||||
*
|
|
||||||
* This nullifies the transparent gzip of [okhttp3.internal.http.BridgeInterceptor]
|
|
||||||
* so gzip and Brotli are explicitly handled by the [okhttp3.brotli.BrotliInterceptor].
|
|
||||||
*/
|
|
||||||
class IgnoreGzipInterceptor : Interceptor {
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
|
||||||
var request = chain.request()
|
|
||||||
if (request.header("Accept-Encoding") == "gzip") {
|
|
||||||
request = request.newBuilder().removeHeader("Accept-Encoding").build()
|
|
||||||
}
|
|
||||||
return chain.proceed(request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -44,7 +44,7 @@ import org.jetbrains.exposed.v1.jdbc.insert
|
|||||||
import org.jetbrains.exposed.v1.jdbc.insertAndGetId
|
import org.jetbrains.exposed.v1.jdbc.insertAndGetId
|
||||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.registerCatalogueSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetSource.registerSource
|
||||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
|
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||||
@@ -483,7 +483,7 @@ class LocalSource(
|
|||||||
it[versionCode] = 0
|
it[versionCode] = 0
|
||||||
it[lang] = LANG
|
it[lang] = LANG
|
||||||
it[extensionLib] = "1.2"
|
it[extensionLib] = "1.2"
|
||||||
it[contentRating] = 0
|
it[contentWarning] = 0
|
||||||
it[isInstalled] = true
|
it[isInstalled] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,7 +497,7 @@ class LocalSource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val fs = LocalSourceFileSystem(applicationDirs)
|
val fs = LocalSourceFileSystem(applicationDirs)
|
||||||
registerCatalogueSource(ID to LocalSource(fs, LocalCoverManager(fs)))
|
registerSource(ID to LocalSource(fs, LocalCoverManager(fs)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ import android.app.Application
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.PUT
|
import eu.kanade.tachiyomi.network.PUT
|
||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import io.javalin.http.HttpStatus
|
import io.javalin.http.HttpStatus
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.SerializationException
|
import kotlinx.serialization.SerializationException
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
@@ -36,31 +40,66 @@ object SyncYomiSyncService {
|
|||||||
message: String?,
|
message: String?,
|
||||||
) : Exception(message)
|
) : Exception(message)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class SyncEvent(
|
||||||
|
val event: SyncEventStatus,
|
||||||
|
val device_Name: String? = null,
|
||||||
|
val message: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private enum class SyncEventStatus {
|
||||||
|
SYNC_STARTED,
|
||||||
|
SYNC_SUCCESS,
|
||||||
|
SYNC_FAILED,
|
||||||
|
SYNC_ERROR,
|
||||||
|
SYNC_CANCELLED,
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun doSync(
|
suspend fun doSync(
|
||||||
syncData: SyncData,
|
syncData: SyncData,
|
||||||
startDate: Instant,
|
startDate: Instant,
|
||||||
setSyncState: (SyncManager.SyncState) -> Unit,
|
setSyncState: (SyncManager.SyncState) -> Unit,
|
||||||
): Backup? {
|
): Backup? {
|
||||||
|
reportSyncEvent(SyncEventStatus.SYNC_STARTED)
|
||||||
setSyncState(SyncManager.SyncState.Downloading(startDate))
|
setSyncState(SyncManager.SyncState.Downloading(startDate))
|
||||||
val (remoteData, etag) = pullSyncData()
|
|
||||||
|
|
||||||
val finalSyncData =
|
return try {
|
||||||
if (remoteData != null) {
|
val (remoteData, etag) = pullSyncData()
|
||||||
require(etag.isNotEmpty()) { "ETag should never be empty if remote data is not null" }
|
|
||||||
logger.debug { "Try update remote data with ETag($etag)" }
|
val finalSyncData =
|
||||||
setSyncState(SyncManager.SyncState.Merging(startDate))
|
if (remoteData != null) {
|
||||||
mergeSyncData(syncData, remoteData)
|
require(etag.isNotEmpty()) { "ETag should never be empty if remote data is not null" }
|
||||||
} else {
|
logger.debug { "Try update remote data with ETag($etag)" }
|
||||||
// init or overwrite remote data
|
setSyncState(SyncManager.SyncState.Merging(startDate))
|
||||||
logger.debug { "Try overwrite remote data with ETag($etag)" }
|
mergeSyncData(syncData, remoteData)
|
||||||
syncData
|
} else {
|
||||||
|
// init or overwrite remote data
|
||||||
|
logger.debug { "Try overwrite remote data with ETag($etag)" }
|
||||||
|
syncData
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalSyncData.backup != null) {
|
||||||
|
setSyncState(SyncManager.SyncState.Uploading(startDate))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (finalSyncData.backup != null) {
|
val success = pushSyncData(finalSyncData, etag)
|
||||||
setSyncState(SyncManager.SyncState.Uploading(startDate))
|
if (success) {
|
||||||
|
reportSyncEvent(SyncEventStatus.SYNC_SUCCESS)
|
||||||
|
} else {
|
||||||
|
reportSyncEvent(SyncEventStatus.SYNC_FAILED, "Failed to push sync data")
|
||||||
|
}
|
||||||
|
|
||||||
|
finalSyncData.backup
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) {
|
||||||
|
reportSyncEvent(SyncEventStatus.SYNC_CANCELLED, e.message)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
logger.error { "Error syncing: ${e.message}" }
|
||||||
|
reportSyncEvent(SyncEventStatus.SYNC_ERROR, e.message)
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
pushSyncData(finalSyncData, etag)
|
|
||||||
return finalSyncData.backup
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun pullSyncData(): Pair<SyncData?, String> {
|
private suspend fun pullSyncData(): Pair<SyncData?, String> {
|
||||||
@@ -122,8 +161,8 @@ object SyncYomiSyncService {
|
|||||||
private suspend fun pushSyncData(
|
private suspend fun pushSyncData(
|
||||||
syncData: SyncData,
|
syncData: SyncData,
|
||||||
eTag: String,
|
eTag: String,
|
||||||
) {
|
): Boolean {
|
||||||
val backup = syncData.backup ?: return
|
val backup = syncData.backup ?: return true
|
||||||
|
|
||||||
val host = serverConfig.syncYomiHost.value
|
val host = serverConfig.syncYomiHost.value
|
||||||
val apiKey = serverConfig.syncYomiApiKey.value
|
val apiKey = serverConfig.syncYomiApiKey.value
|
||||||
@@ -160,7 +199,7 @@ object SyncYomiSyncService {
|
|||||||
|
|
||||||
val response = client.newCall(uploadRequest).await()
|
val response = client.newCall(uploadRequest).await()
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
return if (response.isSuccessful) {
|
||||||
val newETag =
|
val newETag =
|
||||||
response.headers["ETag"]
|
response.headers["ETag"]
|
||||||
?.takeIf { it.isNotEmpty() } ?: throw SyncYomiException("Missing ETag")
|
?.takeIf { it.isNotEmpty() } ?: throw SyncYomiException("Missing ETag")
|
||||||
@@ -169,12 +208,53 @@ object SyncYomiSyncService {
|
|||||||
.putString("last_sync_etag", newETag)
|
.putString("last_sync_etag", newETag)
|
||||||
.apply()
|
.apply()
|
||||||
logger.debug { "SyncYomi sync completed" }
|
logger.debug { "SyncYomi sync completed" }
|
||||||
|
true
|
||||||
} else if (response.code == HttpStatus.PRECONDITION_FAILED.code) {
|
} else if (response.code == HttpStatus.PRECONDITION_FAILED.code) {
|
||||||
// other clients updated remote data, will try next time
|
// other clients updated remote data, will try next time
|
||||||
logger.debug { "SyncYomi sync failed with 412" }
|
logger.debug { "SyncYomi sync failed with 412" }
|
||||||
|
false
|
||||||
} else {
|
} else {
|
||||||
val responseBody = response.body.string()
|
val responseBody = response.body.string()
|
||||||
logger.error { "SyncError: $responseBody" }
|
logger.error { "SyncError: $responseBody" }
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun reportSyncEvent(
|
||||||
|
event: SyncEventStatus,
|
||||||
|
message: String? = null,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
val host = serverConfig.syncYomiHost.value
|
||||||
|
val apiKey = serverConfig.syncYomiApiKey.value
|
||||||
|
val url = "$host/api/sync/event"
|
||||||
|
|
||||||
|
val headers = Headers.Builder().add("X-API-Token", apiKey).build()
|
||||||
|
|
||||||
|
// Use a fixed server name.
|
||||||
|
val bodyObj =
|
||||||
|
SyncEvent(
|
||||||
|
event = event,
|
||||||
|
device_Name = "Suwayomi Server",
|
||||||
|
message = message,
|
||||||
|
)
|
||||||
|
|
||||||
|
val jsonBody = Json.encodeToString(SyncEvent.serializer(), bodyObj)
|
||||||
|
val requestBody = jsonBody.toRequestBody("application/json; charset=utf-8".toMediaType())
|
||||||
|
|
||||||
|
val request =
|
||||||
|
POST(
|
||||||
|
url = url,
|
||||||
|
headers = headers,
|
||||||
|
body = requestBody,
|
||||||
|
)
|
||||||
|
|
||||||
|
network.client
|
||||||
|
.newCall(request)
|
||||||
|
.await()
|
||||||
|
.close()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error { "Failed to report sync event: ${e.message}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,8 +305,7 @@ object SyncYomiSyncService {
|
|||||||
|
|
||||||
logger.debug { "Starting merge. Local list size: ${localMangaListSafe.size}, Remote list size: ${remoteMangaListSafe.size}" }
|
logger.debug { "Starting merge. Local list size: ${localMangaListSafe.size}, Remote list size: ${remoteMangaListSafe.size}" }
|
||||||
|
|
||||||
fun mangaCompositeKey(manga: BackupManga): String =
|
fun mangaCompositeKey(manga: BackupManga): String = "${manga.source}|${manga.url}"
|
||||||
"${manga.source}|${manga.url}|${manga.title.lowercase().trim()}|${manga.author?.lowercase()?.trim()}"
|
|
||||||
|
|
||||||
// Create maps using composite keys
|
// Create maps using composite keys
|
||||||
val localMangaMap = localMangaListSafe.associateBy { mangaCompositeKey(it) }
|
val localMangaMap = localMangaListSafe.associateBy { mangaCompositeKey(it) }
|
||||||
@@ -335,7 +414,7 @@ object SyncYomiSyncService {
|
|||||||
return remoteChapters // If not syncing chapters, keep remote untouched
|
return remoteChapters // If not syncing chapters, keep remote untouched
|
||||||
}
|
}
|
||||||
|
|
||||||
fun chapterCompositeKey(chapter: BackupChapter): String = "${chapter.url}|${chapter.name}|${chapter.chapterNumber}"
|
fun chapterCompositeKey(chapter: BackupChapter): String = chapter.url
|
||||||
|
|
||||||
val localChapterMap = localChapters.associateBy { chapterCompositeKey(it) }
|
val localChapterMap = localChapters.associateBy { chapterCompositeKey(it) }
|
||||||
val remoteChapterMap = remoteChapters.associateBy { chapterCompositeKey(it) }
|
val remoteChapterMap = remoteChapters.associateBy { chapterCompositeKey(it) }
|
||||||
|
|||||||
@@ -21,26 +21,6 @@ class ExtensionStoreDataLoader : KotlinDataLoader<String, ExtensionStoreType> {
|
|||||||
|
|
||||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, ExtensionStoreType> =
|
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, ExtensionStoreType> =
|
||||||
DataLoaderFactory.newDataLoader { ids ->
|
DataLoaderFactory.newDataLoader { ids ->
|
||||||
future {
|
|
||||||
transaction {
|
|
||||||
addLogger(Slf4jSqlDebugLogger)
|
|
||||||
val manga =
|
|
||||||
ExtensionStoreTable
|
|
||||||
.selectAll()
|
|
||||||
.where { ExtensionStoreTable.indexUrl inList ids }
|
|
||||||
.map { ExtensionStoreType(it) }
|
|
||||||
.associateBy { it.indexUrl }
|
|
||||||
ids.map { manga[it] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ExtensionStoreForExtension : KotlinDataLoader<String, ExtensionStoreType> {
|
|
||||||
override val dataLoaderName = "ExtensionStoreForExtension"
|
|
||||||
|
|
||||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, ExtensionStoreType> =
|
|
||||||
DataLoaderFactory.newDataLoader<String, ExtensionStoreType> { ids ->
|
|
||||||
future {
|
future {
|
||||||
transaction {
|
transaction {
|
||||||
addLogger(Slf4jSqlDebugLogger)
|
addLogger(Slf4jSqlDebugLogger)
|
||||||
@@ -50,14 +30,14 @@ class ExtensionStoreForExtension : KotlinDataLoader<String, ExtensionStoreType>
|
|||||||
.where { ExtensionStoreTable.indexUrl inList ids }
|
.where { ExtensionStoreTable.indexUrl inList ids }
|
||||||
.map { ExtensionStoreType(it) }
|
.map { ExtensionStoreType(it) }
|
||||||
.associateBy { it.indexUrl }
|
.associateBy { it.indexUrl }
|
||||||
ids.map { (extensionStoreByIndexUrl[it]) }
|
ids.map { extensionStoreByIndexUrl[it] }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExtensionForExtensionStore : KotlinDataLoader<String, ExtensionNodeList> {
|
class ExtensionsForExtensionStore : KotlinDataLoader<String, ExtensionNodeList> {
|
||||||
override val dataLoaderName = "ExtensionForExtensionStore"
|
override val dataLoaderName = "ExtensionsForExtensionStore"
|
||||||
|
|
||||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, ExtensionNodeList> =
|
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, ExtensionNodeList> =
|
||||||
DataLoaderFactory.newDataLoader<String, ExtensionNodeList> { ids ->
|
DataLoaderFactory.newDataLoader<String, ExtensionNodeList> { ids ->
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ class ExtensionStoreMutation {
|
|||||||
contactDiscord = it.contactDiscord,
|
contactDiscord = it.contactDiscord,
|
||||||
indexUrl = it.indexUrl,
|
indexUrl = it.indexUrl,
|
||||||
isLegacy = it.isLegacy,
|
isLegacy = it.isLegacy,
|
||||||
|
extensionListUrl = it.extensionListUrl,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import suwayomi.tachidesk.graphql.types.preferenceOf
|
|||||||
import suwayomi.tachidesk.graphql.types.updateFilterList
|
import suwayomi.tachidesk.graphql.types.updateFilterList
|
||||||
import suwayomi.tachidesk.manga.impl.MangaList.insertOrUpdate
|
import suwayomi.tachidesk.manga.impl.MangaList.insertOrUpdate
|
||||||
import suwayomi.tachidesk.manga.impl.Source
|
import suwayomi.tachidesk.manga.impl.Source
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||||
import suwayomi.tachidesk.manga.model.table.SourceMetaTable
|
import suwayomi.tachidesk.manga.model.table.SourceMetaTable
|
||||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||||
@@ -256,7 +256,7 @@ class SourceMutation {
|
|||||||
val (clientMutationId, sourceId, type, page, query, filters) = input
|
val (clientMutationId, sourceId, type, page, query, filters) = input
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
val source = GetCatalogueSource.getCatalogueSourceOrNull(sourceId)!!
|
val source = GetSource.getSourceOrNull(sourceId)!!
|
||||||
val mangasPage =
|
val mangasPage =
|
||||||
when (type) {
|
when (type) {
|
||||||
FetchSourceMangaType.SEARCH -> {
|
FetchSourceMangaType.SEARCH -> {
|
||||||
|
|||||||
@@ -148,6 +148,36 @@ class TrackMutation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class BindTrackRecordInput(
|
||||||
|
val clientMutationId: String? = null,
|
||||||
|
val mangaId: Int,
|
||||||
|
val trackRecordId: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BindTrackRecordPayload(
|
||||||
|
val clientMutationId: String?,
|
||||||
|
val trackRecord: TrackRecordType,
|
||||||
|
)
|
||||||
|
|
||||||
|
@RequireAuth
|
||||||
|
fun bindTrackRecord(input: BindTrackRecordInput): CompletableFuture<BindTrackRecordPayload?> {
|
||||||
|
val (clientMutationId, mangaId, trackRecordId) = input
|
||||||
|
|
||||||
|
return future {
|
||||||
|
val boundTrackRecordId = Track.bindTrackRecord(mangaId, trackRecordId)
|
||||||
|
|
||||||
|
val trackRecord =
|
||||||
|
transaction {
|
||||||
|
TrackRecordTable.selectAll().where { TrackRecordTable.id eq boundTrackRecordId }.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
BindTrackRecordPayload(
|
||||||
|
clientMutationId,
|
||||||
|
TrackRecordType(trackRecord),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class FetchTrackInput(
|
data class FetchTrackInput(
|
||||||
val clientMutationId: String? = null,
|
val clientMutationId: String? = null,
|
||||||
val recordId: Int,
|
val recordId: Int,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import org.jetbrains.exposed.v1.jdbc.selectAll
|
|||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
|
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.ContentRatingFilter
|
import suwayomi.tachidesk.graphql.queries.filter.ContentWarningFilter
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.Filter
|
import suwayomi.tachidesk.graphql.queries.filter.Filter
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.HasGetOp
|
import suwayomi.tachidesk.graphql.queries.filter.HasGetOp
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.IntFilter
|
import suwayomi.tachidesk.graphql.queries.filter.IntFilter
|
||||||
@@ -43,7 +43,7 @@ import suwayomi.tachidesk.graphql.server.primitives.lessNotUnique
|
|||||||
import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
|
import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
|
||||||
import suwayomi.tachidesk.graphql.types.ExtensionNodeList
|
import suwayomi.tachidesk.graphql.types.ExtensionNodeList
|
||||||
import suwayomi.tachidesk.graphql.types.ExtensionType
|
import suwayomi.tachidesk.graphql.types.ExtensionType
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
@@ -108,9 +108,9 @@ class ExtensionQuery {
|
|||||||
val versionCode: Int? = null,
|
val versionCode: Int? = null,
|
||||||
val versionCodeLong: Long? = null,
|
val versionCodeLong: Long? = null,
|
||||||
val lang: String? = null,
|
val lang: String? = null,
|
||||||
@GraphQLDeprecated("", ReplaceWith("contentRating"))
|
@GraphQLDeprecated("", ReplaceWith("contentWarning"))
|
||||||
val isNsfw: Boolean? = null,
|
val isNsfw: Boolean? = null,
|
||||||
val contentRating: ContentRating? = null,
|
val contentWarning: ContentWarning? = null,
|
||||||
val isInstalled: Boolean? = null,
|
val isInstalled: Boolean? = null,
|
||||||
val hasUpdate: Boolean? = null,
|
val hasUpdate: Boolean? = null,
|
||||||
val isObsolete: Boolean? = null,
|
val isObsolete: Boolean? = null,
|
||||||
@@ -129,10 +129,10 @@ class ExtensionQuery {
|
|||||||
opAnd.eq(versionCodeLong, ExtensionTable.versionCode)
|
opAnd.eq(versionCodeLong, ExtensionTable.versionCode)
|
||||||
opAnd.eq(lang, ExtensionTable.lang)
|
opAnd.eq(lang, ExtensionTable.lang)
|
||||||
opAnd.eq(
|
opAnd.eq(
|
||||||
isNsfw?.let { if (it) ContentRating.PORNOGRAPHIC.ordinal else ContentRating.SAFE.ordinal },
|
isNsfw?.let { if (it) ContentWarning.MIXED.ordinal else ContentWarning.SAFE.ordinal },
|
||||||
ExtensionTable.contentRating,
|
ExtensionTable.contentWarning,
|
||||||
)
|
)
|
||||||
opAnd.eq(contentRating?.ordinal, ExtensionTable.contentRating)
|
opAnd.eq(contentWarning?.ordinal, ExtensionTable.contentWarning)
|
||||||
opAnd.eq(isInstalled, ExtensionTable.isInstalled)
|
opAnd.eq(isInstalled, ExtensionTable.isInstalled)
|
||||||
opAnd.eq(hasUpdate, ExtensionTable.hasUpdate)
|
opAnd.eq(hasUpdate, ExtensionTable.hasUpdate)
|
||||||
opAnd.eq(isObsolete, ExtensionTable.isObsolete)
|
opAnd.eq(isObsolete, ExtensionTable.isObsolete)
|
||||||
@@ -156,9 +156,9 @@ class ExtensionQuery {
|
|||||||
val versionCode: IntFilter? = null,
|
val versionCode: IntFilter? = null,
|
||||||
val versionCodeLong: LongFilter? = null,
|
val versionCodeLong: LongFilter? = null,
|
||||||
val lang: StringFilter? = null,
|
val lang: StringFilter? = null,
|
||||||
@GraphQLDeprecated("", ReplaceWith("storeIndexUrl"))
|
@GraphQLDeprecated("", ReplaceWith("contentWarning"))
|
||||||
val isNsfw: BooleanFilter? = null,
|
val isNsfw: BooleanFilter? = null,
|
||||||
val contentRating: ContentRatingFilter? = null,
|
val contentWarning: ContentWarningFilter? = null,
|
||||||
val isInstalled: BooleanFilter? = null,
|
val isInstalled: BooleanFilter? = null,
|
||||||
val hasUpdate: BooleanFilter? = null,
|
val hasUpdate: BooleanFilter? = null,
|
||||||
val isObsolete: BooleanFilter? = null,
|
val isObsolete: BooleanFilter? = null,
|
||||||
@@ -179,7 +179,7 @@ class ExtensionQuery {
|
|||||||
andFilterWithCompareString(ExtensionTable.versionName, versionName),
|
andFilterWithCompareString(ExtensionTable.versionName, versionName),
|
||||||
andFilterWithCompare(ExtensionTable.versionCode, versionCodeLong),
|
andFilterWithCompare(ExtensionTable.versionCode, versionCodeLong),
|
||||||
andFilterWithCompareString(ExtensionTable.lang, lang),
|
andFilterWithCompareString(ExtensionTable.lang, lang),
|
||||||
andFilterWithCompareEnum(ExtensionTable.contentRating, contentRating),
|
andFilterWithCompareEnum(ExtensionTable.contentWarning, contentWarning),
|
||||||
andFilterWithCompare(ExtensionTable.isInstalled, isInstalled),
|
andFilterWithCompare(ExtensionTable.isInstalled, isInstalled),
|
||||||
andFilterWithCompare(ExtensionTable.hasUpdate, hasUpdate),
|
andFilterWithCompare(ExtensionTable.hasUpdate, hasUpdate),
|
||||||
andFilterWithCompare(ExtensionTable.isObsolete, isObsolete),
|
andFilterWithCompare(ExtensionTable.isObsolete, isObsolete),
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ import org.jetbrains.exposed.v1.core.Op
|
|||||||
import org.jetbrains.exposed.v1.core.SortOrder
|
import org.jetbrains.exposed.v1.core.SortOrder
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
import org.jetbrains.exposed.v1.core.greater
|
import org.jetbrains.exposed.v1.core.greater
|
||||||
|
import org.jetbrains.exposed.v1.core.greaterEq
|
||||||
import org.jetbrains.exposed.v1.core.less
|
import org.jetbrains.exposed.v1.core.less
|
||||||
import org.jetbrains.exposed.v1.core.neq
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
|
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.ContentRatingFilter
|
import suwayomi.tachidesk.graphql.queries.filter.ContentWarningFilter
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.Filter
|
import suwayomi.tachidesk.graphql.queries.filter.Filter
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.HasGetOp
|
import suwayomi.tachidesk.graphql.queries.filter.HasGetOp
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.LongFilter
|
import suwayomi.tachidesk.graphql.queries.filter.LongFilter
|
||||||
@@ -42,7 +42,7 @@ import suwayomi.tachidesk.graphql.server.primitives.lessNotUnique
|
|||||||
import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
|
import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
|
||||||
import suwayomi.tachidesk.graphql.types.SourceNodeList
|
import suwayomi.tachidesk.graphql.types.SourceNodeList
|
||||||
import suwayomi.tachidesk.graphql.types.SourceType
|
import suwayomi.tachidesk.graphql.types.SourceType
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
@@ -95,9 +95,9 @@ class SourceQuery {
|
|||||||
val id: Long? = null,
|
val id: Long? = null,
|
||||||
val name: String? = null,
|
val name: String? = null,
|
||||||
val lang: String? = null,
|
val lang: String? = null,
|
||||||
@GraphQLDeprecated("replace with contentRating == ContentRating.PORNOGRAPHIC", ReplaceWith("contentRating"))
|
@GraphQLDeprecated("replace with contentWarning == ContentRating.MIXED", ReplaceWith("contentWarning"))
|
||||||
val isNsfw: Boolean? = null,
|
val isNsfw: Boolean? = null,
|
||||||
val contentRating: ContentRating? = null,
|
val contentWarning: ContentWarning? = null,
|
||||||
) : HasGetOp {
|
) : HasGetOp {
|
||||||
override fun getOp(): Op<Boolean>? {
|
override fun getOp(): Op<Boolean>? {
|
||||||
val opAnd = OpAnd()
|
val opAnd = OpAnd()
|
||||||
@@ -106,13 +106,12 @@ class SourceQuery {
|
|||||||
opAnd.eq(lang, SourceTable.lang)
|
opAnd.eq(lang, SourceTable.lang)
|
||||||
opAnd.andWhere(isNsfw) {
|
opAnd.andWhere(isNsfw) {
|
||||||
if (it) {
|
if (it) {
|
||||||
SourceTable.contentRating eq ContentRating.PORNOGRAPHIC.ordinal
|
SourceTable.contentWarning greaterEq ContentWarning.MIXED.ordinal
|
||||||
} else {
|
} else {
|
||||||
SourceTable.contentRating neq
|
SourceTable.contentWarning less ContentWarning.MIXED.ordinal
|
||||||
ContentRating.PORNOGRAPHIC.ordinal
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
opAnd.andWhere(contentRating) { SourceTable.contentRating eq it.ordinal }
|
opAnd.andWhere(contentWarning) { SourceTable.contentWarning eq it.ordinal }
|
||||||
|
|
||||||
return opAnd.op
|
return opAnd.op
|
||||||
}
|
}
|
||||||
@@ -122,9 +121,9 @@ class SourceQuery {
|
|||||||
val id: LongFilter? = null,
|
val id: LongFilter? = null,
|
||||||
val name: StringFilter? = null,
|
val name: StringFilter? = null,
|
||||||
val lang: StringFilter? = null,
|
val lang: StringFilter? = null,
|
||||||
@GraphQLDeprecated("replace with contentRating == ContentRating.PORNOGRAPHIC", ReplaceWith("contentRating"))
|
@GraphQLDeprecated("replace with contentWarning", ReplaceWith("contentWarning"))
|
||||||
val isNsfw: BooleanFilter? = null,
|
val isNsfw: BooleanFilter? = null,
|
||||||
val contentRating: ContentRatingFilter? = null,
|
val contentWarning: ContentWarningFilter? = null,
|
||||||
override val and: List<SourceFilter>? = null,
|
override val and: List<SourceFilter>? = null,
|
||||||
override val or: List<SourceFilter>? = null,
|
override val or: List<SourceFilter>? = null,
|
||||||
override val not: SourceFilter? = null,
|
override val not: SourceFilter? = null,
|
||||||
@@ -134,7 +133,7 @@ class SourceQuery {
|
|||||||
andFilterWithCompareEntity(SourceTable.id, id),
|
andFilterWithCompareEntity(SourceTable.id, id),
|
||||||
andFilterWithCompareString(SourceTable.name, name),
|
andFilterWithCompareString(SourceTable.name, name),
|
||||||
andFilterWithCompareString(SourceTable.lang, lang),
|
andFilterWithCompareString(SourceTable.lang, lang),
|
||||||
andFilterWithCompareEnum(SourceTable.contentRating, contentRating),
|
andFilterWithCompareEnum(SourceTable.contentWarning, contentWarning),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import org.jetbrains.exposed.v1.core.upperCase
|
|||||||
import org.jetbrains.exposed.v1.core.wrap
|
import org.jetbrains.exposed.v1.core.wrap
|
||||||
import org.jetbrains.exposed.v1.jdbc.Query
|
import org.jetbrains.exposed.v1.jdbc.Query
|
||||||
import org.jetbrains.exposed.v1.jdbc.andWhere
|
import org.jetbrains.exposed.v1.jdbc.andWhere
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||||
|
|
||||||
class ILikeEscapeOp(
|
class ILikeEscapeOp(
|
||||||
expr1: Expression<*>,
|
expr1: Expression<*>,
|
||||||
@@ -330,23 +330,23 @@ data class DoubleFilter(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ContentRatingFilter(
|
data class ContentWarningFilter(
|
||||||
override val isNull: Boolean? = null,
|
override val isNull: Boolean? = null,
|
||||||
override val equalTo: ContentRating? = null,
|
override val equalTo: ContentWarning? = null,
|
||||||
override val notEqualTo: ContentRating? = null,
|
override val notEqualTo: ContentWarning? = null,
|
||||||
override val notEqualToAll: List<ContentRating>? = null,
|
override val notEqualToAll: List<ContentWarning>? = null,
|
||||||
override val notEqualToAny: List<ContentRating>? = null,
|
override val notEqualToAny: List<ContentWarning>? = null,
|
||||||
override val distinctFrom: ContentRating? = null,
|
override val distinctFrom: ContentWarning? = null,
|
||||||
override val distinctFromAll: List<ContentRating>? = null,
|
override val distinctFromAll: List<ContentWarning>? = null,
|
||||||
override val distinctFromAny: List<ContentRating>? = null,
|
override val distinctFromAny: List<ContentWarning>? = null,
|
||||||
override val notDistinctFrom: ContentRating? = null,
|
override val notDistinctFrom: ContentWarning? = null,
|
||||||
override val `in`: List<ContentRating>? = null,
|
override val `in`: List<ContentWarning>? = null,
|
||||||
override val notIn: List<ContentRating>? = null,
|
override val notIn: List<ContentWarning>? = null,
|
||||||
override val lessThan: ContentRating? = null,
|
override val lessThan: ContentWarning? = null,
|
||||||
override val lessThanOrEqualTo: ContentRating? = null,
|
override val lessThanOrEqualTo: ContentWarning? = null,
|
||||||
override val greaterThan: ContentRating? = null,
|
override val greaterThan: ContentWarning? = null,
|
||||||
override val greaterThanOrEqualTo: ContentRating? = null,
|
override val greaterThanOrEqualTo: ContentWarning? = null,
|
||||||
) : ComparableScalarFilter<ContentRating>
|
) : ComparableScalarFilter<ContentWarning>
|
||||||
|
|
||||||
data class StringFilter(
|
data class StringFilter(
|
||||||
override val isNull: Boolean? = null,
|
override val isNull: Boolean? = null,
|
||||||
|
|||||||
@@ -20,10 +20,9 @@ import suwayomi.tachidesk.graphql.dataLoaders.DisplayScoreForTrackRecordDataLoad
|
|||||||
import suwayomi.tachidesk.graphql.dataLoaders.DisplayScoreForTrackSearchDataLoader
|
import suwayomi.tachidesk.graphql.dataLoaders.DisplayScoreForTrackSearchDataLoader
|
||||||
import suwayomi.tachidesk.graphql.dataLoaders.DownloadedChapterCountForMangaDataLoader
|
import suwayomi.tachidesk.graphql.dataLoaders.DownloadedChapterCountForMangaDataLoader
|
||||||
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionDataLoader
|
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionDataLoader
|
||||||
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionForExtensionStore
|
|
||||||
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionForSourceDataLoader
|
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionForSourceDataLoader
|
||||||
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionStoreDataLoader
|
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionStoreDataLoader
|
||||||
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionStoreForExtension
|
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionsForExtensionStore
|
||||||
import suwayomi.tachidesk.graphql.dataLoaders.FirstUnreadChapterForMangaDataLoader
|
import suwayomi.tachidesk.graphql.dataLoaders.FirstUnreadChapterForMangaDataLoader
|
||||||
import suwayomi.tachidesk.graphql.dataLoaders.GlobalMetaDataLoader
|
import suwayomi.tachidesk.graphql.dataLoaders.GlobalMetaDataLoader
|
||||||
import suwayomi.tachidesk.graphql.dataLoaders.HasDuplicateChaptersForMangaDataLoader
|
import suwayomi.tachidesk.graphql.dataLoaders.HasDuplicateChaptersForMangaDataLoader
|
||||||
@@ -81,9 +80,8 @@ class TachideskDataLoaderRegistryFactory {
|
|||||||
SourceMetaDataLoader(),
|
SourceMetaDataLoader(),
|
||||||
ExtensionDataLoader(),
|
ExtensionDataLoader(),
|
||||||
ExtensionForSourceDataLoader(),
|
ExtensionForSourceDataLoader(),
|
||||||
ExtensionForExtensionStore(),
|
ExtensionsForExtensionStore(),
|
||||||
ExtensionStoreDataLoader(),
|
ExtensionStoreDataLoader(),
|
||||||
ExtensionStoreForExtension(),
|
|
||||||
TrackerDataLoader(),
|
TrackerDataLoader(),
|
||||||
TrackerStatusesDataLoader(),
|
TrackerStatusesDataLoader(),
|
||||||
TrackerScoresDataLoader(),
|
TrackerScoresDataLoader(),
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class ExtensionStoreType(
|
|||||||
val contactDiscord: String?,
|
val contactDiscord: String?,
|
||||||
val indexUrl: String,
|
val indexUrl: String,
|
||||||
val isLegacy: Boolean,
|
val isLegacy: Boolean,
|
||||||
|
val extensionListUrl: String?,
|
||||||
) : Node {
|
) : Node {
|
||||||
constructor(row: ResultRow) : this(
|
constructor(row: ResultRow) : this(
|
||||||
name = row[ExtensionStoreTable.name],
|
name = row[ExtensionStoreTable.name],
|
||||||
@@ -35,10 +36,11 @@ class ExtensionStoreType(
|
|||||||
contactDiscord = row[ExtensionStoreTable.contactDiscord],
|
contactDiscord = row[ExtensionStoreTable.contactDiscord],
|
||||||
indexUrl = row[ExtensionStoreTable.indexUrl],
|
indexUrl = row[ExtensionStoreTable.indexUrl],
|
||||||
isLegacy = row[ExtensionStoreTable.isLegacy],
|
isLegacy = row[ExtensionStoreTable.isLegacy],
|
||||||
|
extensionListUrl = row[ExtensionStoreTable.extensionListUrl],
|
||||||
)
|
)
|
||||||
|
|
||||||
fun extensions(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<ExtensionNodeList> =
|
fun extensions(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<ExtensionNodeList> =
|
||||||
dataFetchingEnvironment.getValueFromDataLoader<String, ExtensionNodeList>("ExtensionForExtensionStore", indexUrl)
|
dataFetchingEnvironment.getValueFromDataLoader<String, ExtensionNodeList>("ExtensionsForExtensionStore", indexUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ExtensionStoreNodeList(
|
data class ExtensionStoreNodeList(
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import suwayomi.tachidesk.graphql.server.primitives.Node
|
|||||||
import suwayomi.tachidesk.graphql.server.primitives.NodeList
|
import suwayomi.tachidesk.graphql.server.primitives.NodeList
|
||||||
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
|
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
|
||||||
import suwayomi.tachidesk.manga.impl.extension.Extension
|
import suwayomi.tachidesk.manga.impl.extension.Extension
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
@@ -41,9 +41,9 @@ class ExtensionType(
|
|||||||
val versionCode: Int,
|
val versionCode: Int,
|
||||||
val versionCodeLong: Long,
|
val versionCodeLong: Long,
|
||||||
val lang: String,
|
val lang: String,
|
||||||
@GraphQLDeprecated("Removed in extension api v1.6", ReplaceWith("contentRating"))
|
@GraphQLDeprecated("Removed in extension api v1.6", ReplaceWith("contentWarning"))
|
||||||
val isNsfw: Boolean,
|
val isNsfw: Boolean,
|
||||||
val contentRating: ContentRating,
|
val contentWarning: ContentWarning,
|
||||||
val isInstalled: Boolean,
|
val isInstalled: Boolean,
|
||||||
val hasUpdate: Boolean,
|
val hasUpdate: Boolean,
|
||||||
val isObsolete: Boolean,
|
val isObsolete: Boolean,
|
||||||
@@ -61,8 +61,8 @@ class ExtensionType(
|
|||||||
versionCode = row[ExtensionTable.versionCode].toInt(),
|
versionCode = row[ExtensionTable.versionCode].toInt(),
|
||||||
versionCodeLong = row[ExtensionTable.versionCode],
|
versionCodeLong = row[ExtensionTable.versionCode],
|
||||||
lang = row[ExtensionTable.lang],
|
lang = row[ExtensionTable.lang],
|
||||||
isNsfw = row[ExtensionTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal,
|
isNsfw = row[ExtensionTable.contentWarning] >= ContentWarning.MIXED.ordinal,
|
||||||
contentRating = ContentRating.valueOf(row[ExtensionTable.contentRating]),
|
contentWarning = ContentWarning.valueOf(row[ExtensionTable.contentWarning]),
|
||||||
isInstalled = row[ExtensionTable.isInstalled],
|
isInstalled = row[ExtensionTable.isInstalled],
|
||||||
hasUpdate = row[ExtensionTable.hasUpdate],
|
hasUpdate = row[ExtensionTable.hasUpdate],
|
||||||
isObsolete = row[ExtensionTable.isObsolete],
|
isObsolete = row[ExtensionTable.isObsolete],
|
||||||
@@ -71,8 +71,8 @@ class ExtensionType(
|
|||||||
fun source(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<SourceNodeList> =
|
fun source(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<SourceNodeList> =
|
||||||
dataFetchingEnvironment.getValueFromDataLoader<String, SourceNodeList>("SourcesForExtensionDataLoader", pkgName)
|
dataFetchingEnvironment.getValueFromDataLoader<String, SourceNodeList>("SourcesForExtensionDataLoader", pkgName)
|
||||||
|
|
||||||
fun extensionStore(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<ExtensionStoreType> =
|
fun extensionStore(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<ExtensionStoreType?> =
|
||||||
dataFetchingEnvironment.getValueFromDataLoader<String, ExtensionStoreType>("ExtensionStoreForExtension", storeIndexUrl.orEmpty())
|
dataFetchingEnvironment.getValueFromDataLoader<String, ExtensionStoreType?>("ExtensionStoreDataLoader", storeIndexUrl.orEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ExtensionNodeList(
|
data class ExtensionNodeList(
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ package suwayomi.tachidesk.graphql.types
|
|||||||
|
|
||||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import graphql.schema.DataFetchingEnvironment
|
import graphql.schema.DataFetchingEnvironment
|
||||||
@@ -24,9 +24,9 @@ import suwayomi.tachidesk.graphql.server.primitives.NodeList
|
|||||||
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
|
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
|
||||||
import suwayomi.tachidesk.manga.impl.Source.getSourcePreferencesRaw
|
import suwayomi.tachidesk.manga.impl.Source.getSourcePreferencesRaw
|
||||||
import suwayomi.tachidesk.manga.impl.extension.Extension
|
import suwayomi.tachidesk.manga.impl.extension.Extension
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
@@ -42,31 +42,29 @@ class SourceType(
|
|||||||
val id: Long,
|
val id: Long,
|
||||||
val name: String,
|
val name: String,
|
||||||
val lang: String,
|
val lang: String,
|
||||||
val message: String?,
|
val contentWarning: ContentWarning,
|
||||||
val contentRating: ContentRating,
|
|
||||||
val iconUrl: String,
|
val iconUrl: String,
|
||||||
val supportsLatest: Boolean,
|
val supportsLatest: Boolean,
|
||||||
val isConfigurable: Boolean,
|
val isConfigurable: Boolean,
|
||||||
@GraphQLDeprecated("", ReplaceWith("contentRating"))
|
@GraphQLDeprecated("", ReplaceWith("contentWarning"))
|
||||||
val isNsfw: Boolean,
|
val isNsfw: Boolean,
|
||||||
val displayName: String,
|
val displayName: String,
|
||||||
val homeUrl: String?,
|
val homeUrl: String?,
|
||||||
@GraphQLDeprecated("", ReplaceWith("homeUrl"))
|
@GraphQLDeprecated("", ReplaceWith("homeUrl"))
|
||||||
val baseUrl: String?,
|
val baseUrl: String?,
|
||||||
) : Node {
|
) : Node {
|
||||||
constructor(row: ResultRow, sourceExtension: ResultRow, catalogueSource: CatalogueSource) : this(
|
constructor(row: ResultRow, sourceExtension: ResultRow, source: Source) : this(
|
||||||
id = row[SourceTable.id].value,
|
id = row[SourceTable.id].value,
|
||||||
name = row[SourceTable.name],
|
name = row[SourceTable.name],
|
||||||
lang = row[SourceTable.lang],
|
lang = row[SourceTable.lang],
|
||||||
message = row[SourceTable.message],
|
contentWarning = ContentWarning.valueOf(row[SourceTable.contentWarning]),
|
||||||
contentRating = ContentRating.valueOf(row[SourceTable.contentRating]),
|
|
||||||
iconUrl = Extension.proxyExtensionIconUrl(sourceExtension[ExtensionTable.pkgName]),
|
iconUrl = Extension.proxyExtensionIconUrl(sourceExtension[ExtensionTable.pkgName]),
|
||||||
supportsLatest = catalogueSource.supportsLatest,
|
supportsLatest = source.supportsLatest,
|
||||||
isConfigurable = catalogueSource is ConfigurableSource,
|
isConfigurable = source is ConfigurableSource,
|
||||||
isNsfw = row[SourceTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal,
|
isNsfw = row[SourceTable.contentWarning] >= ContentWarning.MIXED.ordinal,
|
||||||
displayName = catalogueSource.toString(),
|
displayName = source.toString(),
|
||||||
homeUrl = runCatching { (catalogueSource as? HttpSource)?.getHomeUrl() }.getOrNull(),
|
homeUrl = runCatching { (source as? HttpSource)?.getHomeUrl() }.getOrNull(),
|
||||||
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
|
baseUrl = runCatching { (source as? HttpSource)?.baseUrl }.getOrNull(),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun manga(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<MangaNodeList> =
|
fun manga(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<MangaNodeList> =
|
||||||
@@ -77,7 +75,7 @@ class SourceType(
|
|||||||
|
|
||||||
fun preferences(): List<Preference> = getSourcePreferencesRaw(id).map { preferenceOf(it) }
|
fun preferences(): List<Preference> = getSourcePreferencesRaw(id).map { preferenceOf(it) }
|
||||||
|
|
||||||
fun filters(): List<Filter> = getCatalogueSourceOrStub(id).getFilterList().map { filterOf(it) }
|
fun filters(): List<Filter> = getSourceOrStub(id).getFilterList().map { filterOf(it) }
|
||||||
|
|
||||||
fun meta(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<List<SourceMetaType>> =
|
fun meta(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<List<SourceMetaType>> =
|
||||||
dataFetchingEnvironment.getValueFromDataLoader<Long, List<SourceMetaType>>("SourceMetaDataLoader", id)
|
dataFetchingEnvironment.getValueFromDataLoader<Long, List<SourceMetaType>>("SourceMetaDataLoader", id)
|
||||||
@@ -86,8 +84,8 @@ class SourceType(
|
|||||||
@Suppress("ktlint:standard:function-naming")
|
@Suppress("ktlint:standard:function-naming")
|
||||||
fun SourceType(row: ResultRow): SourceType? {
|
fun SourceType(row: ResultRow): SourceType? {
|
||||||
val catalogueSource =
|
val catalogueSource =
|
||||||
GetCatalogueSource
|
GetSource
|
||||||
.getCatalogueSourceOrNull(row[SourceTable.id].value)
|
.getSourceOrNull(row[SourceTable.id].value)
|
||||||
?: return null
|
?: return null
|
||||||
val sourceExtension =
|
val sourceExtension =
|
||||||
if (row.hasValue(ExtensionTable.id)) {
|
if (row.hasValue(ExtensionTable.id)) {
|
||||||
@@ -298,7 +296,7 @@ data class FilterChange(
|
|||||||
)
|
)
|
||||||
|
|
||||||
fun updateFilterList(
|
fun updateFilterList(
|
||||||
source: CatalogueSource,
|
source: Source,
|
||||||
changes: List<FilterChange>?,
|
changes: List<FilterChange>?,
|
||||||
): FilterList {
|
): FilterList {
|
||||||
val filterList = source.getFilterList()
|
val filterList = source.getFilterList()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ package suwayomi.tachidesk.manga.impl
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||||
@@ -38,7 +38,7 @@ import org.jetbrains.exposed.v1.jdbc.update
|
|||||||
import suwayomi.tachidesk.manga.impl.download.DownloadManager
|
import suwayomi.tachidesk.manga.impl.download.DownloadManager
|
||||||
import suwayomi.tachidesk.manga.impl.download.DownloadManager.EnqueueInput
|
import suwayomi.tachidesk.manga.impl.download.DownloadManager.EnqueueInput
|
||||||
import suwayomi.tachidesk.manga.impl.track.Track
|
import suwayomi.tachidesk.manga.impl.track.Track
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.MangaChapterDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.MangaChapterDataClass
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.PaginatedList
|
import suwayomi.tachidesk.manga.model.dataclass.PaginatedList
|
||||||
@@ -119,7 +119,7 @@ object Chapter {
|
|||||||
transaction {
|
transaction {
|
||||||
MangaTable.selectAll().where { MangaTable.id eq mangaId }.first()
|
MangaTable.selectAll().where { MangaTable.id eq mangaId }.first()
|
||||||
}
|
}
|
||||||
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
val source = getSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||||
|
|
||||||
val chapters =
|
val chapters =
|
||||||
Manga
|
Manga
|
||||||
@@ -139,7 +139,7 @@ object Chapter {
|
|||||||
fun updateChapterListDatabase(
|
fun updateChapterListDatabase(
|
||||||
mangaEntry: ResultRow,
|
mangaEntry: ResultRow,
|
||||||
chapters: List<SChapter>,
|
chapters: List<SChapter>,
|
||||||
source: CatalogueSource,
|
source: Source,
|
||||||
): List<SChapter> {
|
): List<SChapter> {
|
||||||
val currentLatestChapterNumber = Manga.getLatestChapter(mangaEntry[MangaTable.id].value)?.chapterNumber ?: 0f
|
val currentLatestChapterNumber = Manga.getLatestChapter(mangaEntry[MangaTable.id].value)?.chapterNumber ?: 0f
|
||||||
val numberOfCurrentChapters = getCountOfMangaChapters(mangaEntry[MangaTable.id].value)
|
val numberOfCurrentChapters = getCountOfMangaChapters(mangaEntry[MangaTable.id].value)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import suwayomi.tachidesk.manga.model.table.ChapterTable
|
|||||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||||
import suwayomi.tachidesk.manga.model.table.toDataClass
|
import suwayomi.tachidesk.manga.model.table.toDataClass
|
||||||
import suwayomi.tachidesk.server.serverConfig
|
import suwayomi.tachidesk.server.serverConfig
|
||||||
|
import xyz.nulldev.androidcompat.util.SafePath
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
@@ -76,13 +77,46 @@ object ChapterDownloadHelper {
|
|||||||
.select(ChapterTable.columns + MangaTable.columns)
|
.select(ChapterTable.columns + MangaTable.columns)
|
||||||
.where { ChapterTable.id eq chapterId }
|
.where { ChapterTable.id eq chapterId }
|
||||||
.firstOrNull() ?: throw IllegalArgumentException("ChapterId $chapterId not found")
|
.firstOrNull() ?: throw IllegalArgumentException("ChapterId $chapterId not found")
|
||||||
|
|
||||||
val chapter = ChapterTable.toDataClass(row)
|
val chapter = ChapterTable.toDataClass(row)
|
||||||
val mangaTitle = row[MangaTable.title]
|
val mangaTitle = row[MangaTable.title].trim()
|
||||||
|
|
||||||
val scanlatorPart = chapter.scanlator?.let { "[$it] " } ?: ""
|
val scanlatorName = chapter.scanlator?.trim()?.takeIf { it.isNotEmpty() }
|
||||||
val fileName = "$mangaTitle - $scanlatorPart${chapter.name}.cbz"
|
val chapterName = chapter.name.trim().takeIf { it.isNotEmpty() }
|
||||||
|
|
||||||
Pair(chapter, fileName)
|
val fileName =
|
||||||
|
buildString {
|
||||||
|
append(mangaTitle)
|
||||||
|
append(" - ")
|
||||||
|
|
||||||
|
if (chapterName != null) {
|
||||||
|
append(chapterName)
|
||||||
|
} else if (chapter.chapterNumber >= 0f) {
|
||||||
|
// chapterNumber is stored as Float, drop .0 for whole numbers.
|
||||||
|
val formatNumber =
|
||||||
|
if (chapter.chapterNumber % 1 == 0f) {
|
||||||
|
chapter.chapterNumber.toInt().toString()
|
||||||
|
} else {
|
||||||
|
chapter.chapterNumber.toString()
|
||||||
|
}
|
||||||
|
append("#$formatNumber")
|
||||||
|
} else {
|
||||||
|
// Fallback when neither name nor valid chapter number exists
|
||||||
|
append("#${chapter.index}")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scanlatorName != null) {
|
||||||
|
append(" [")
|
||||||
|
append(scanlatorName)
|
||||||
|
append("]")
|
||||||
|
}
|
||||||
|
append(".cbz")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize filename for OS compatibility
|
||||||
|
val safeFileName = SafePath.buildValidFilename(fileName)
|
||||||
|
|
||||||
|
Pair(chapter, safeFileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCbzForDownload(
|
fun getCbzForDownload(
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.network.GET
|
|||||||
import eu.kanade.tachiyomi.network.HttpException
|
import eu.kanade.tachiyomi.network.HttpException
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
@@ -41,8 +41,8 @@ import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
|||||||
import org.jetbrains.exposed.v1.jdbc.update
|
import org.jetbrains.exposed.v1.jdbc.update
|
||||||
import suwayomi.tachidesk.manga.impl.download.fileProvider.impl.MissingThumbnailException
|
import suwayomi.tachidesk.manga.impl.download.fileProvider.impl.MissingThumbnailException
|
||||||
import suwayomi.tachidesk.manga.impl.util.network.await
|
import suwayomi.tachidesk.manga.impl.util.network.await
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
|
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrNull
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.StubSource
|
import suwayomi.tachidesk.manga.impl.util.source.StubSource
|
||||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.clearCachedImage
|
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.clearCachedImage
|
||||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
|
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
|
||||||
@@ -91,7 +91,7 @@ object Manga {
|
|||||||
|
|
||||||
suspend fun fetchMangaAndChapters(
|
suspend fun fetchMangaAndChapters(
|
||||||
mangaEntry: ResultRow,
|
mangaEntry: ResultRow,
|
||||||
source: CatalogueSource,
|
source: Source,
|
||||||
fetchDetails: Boolean,
|
fetchDetails: Boolean,
|
||||||
fetchChapters: Boolean,
|
fetchChapters: Boolean,
|
||||||
): SMangaUpdate {
|
): SMangaUpdate {
|
||||||
@@ -139,7 +139,7 @@ object Manga {
|
|||||||
return mangaInfoMutex.get(mangaId) { Mutex() }.withLock {
|
return mangaInfoMutex.get(mangaId) { Mutex() }.withLock {
|
||||||
val mangaEntry =
|
val mangaEntry =
|
||||||
transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||||
val source = getCatalogueSourceOrNull(mangaEntry[MangaTable.sourceReference]) ?: return null
|
val source = getSourceOrNull(mangaEntry[MangaTable.sourceReference]) ?: return null
|
||||||
val sManga =
|
val sManga =
|
||||||
fetchMangaAndChapters(
|
fetchMangaAndChapters(
|
||||||
mangaEntry,
|
mangaEntry,
|
||||||
@@ -161,7 +161,7 @@ object Manga {
|
|||||||
var mangaEntry =
|
var mangaEntry =
|
||||||
transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||||
val source =
|
val source =
|
||||||
getCatalogueSourceOrNull(mangaEntry[MangaTable.sourceReference])
|
getSourceOrNull(mangaEntry[MangaTable.sourceReference])
|
||||||
?: throw NullPointerException("Missing source ${mangaEntry[MangaTable.sourceReference]}")
|
?: throw NullPointerException("Missing source ${mangaEntry[MangaTable.sourceReference]}")
|
||||||
val mangaUpdate =
|
val mangaUpdate =
|
||||||
fetchMangaAndChapters(
|
fetchMangaAndChapters(
|
||||||
@@ -186,7 +186,7 @@ object Manga {
|
|||||||
|
|
||||||
fun updateMangaDatabase(
|
fun updateMangaDatabase(
|
||||||
mangaEntry: ResultRow,
|
mangaEntry: ResultRow,
|
||||||
source: CatalogueSource,
|
source: Source,
|
||||||
sManga: SManga,
|
sManga: SManga,
|
||||||
): SManga {
|
): SManga {
|
||||||
transaction {
|
transaction {
|
||||||
@@ -238,6 +238,7 @@ object Manga {
|
|||||||
it[MangaTable.lastFetchedAt] = Instant.now().epochSecond
|
it[MangaTable.lastFetchedAt] = Instant.now().epochSecond
|
||||||
|
|
||||||
it[MangaTable.updateStrategy] = sManga.update_strategy.name
|
it[MangaTable.updateStrategy] = sManga.update_strategy.name
|
||||||
|
it[MangaTable.memo] = Json.encodeToString(sManga.memo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,7 +413,7 @@ object Manga {
|
|||||||
val mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
val mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||||
val sourceId = mangaEntry[MangaTable.sourceReference]
|
val sourceId = mangaEntry[MangaTable.sourceReference]
|
||||||
|
|
||||||
return when (val source = getCatalogueSourceOrStub(sourceId)) {
|
return when (val source = getSourceOrStub(sourceId)) {
|
||||||
is HttpSource -> {
|
is HttpSource -> {
|
||||||
getImageResponse(cacheSaveDir, fileName) {
|
getImageResponse(cacheSaveDir, fileName) {
|
||||||
fetchHttpSourceMangaThumbnail(source, mangaEntry)
|
fetchHttpSourceMangaThumbnail(source, mangaEntry)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import org.jetbrains.exposed.v1.jdbc.batchInsert
|
|||||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||||
import suwayomi.tachidesk.manga.model.table.toDataClass
|
import suwayomi.tachidesk.manga.model.table.toDataClass
|
||||||
@@ -36,7 +36,7 @@ object MangaList {
|
|||||||
require(pageNum > 0) {
|
require(pageNum > 0) {
|
||||||
"pageNum = $pageNum is not in valid range"
|
"pageNum = $pageNum is not in valid range"
|
||||||
}
|
}
|
||||||
val source = getCatalogueSourceOrStub(sourceId)
|
val source = getSourceOrStub(sourceId)
|
||||||
val mangasPage =
|
val mangasPage =
|
||||||
if (popular) {
|
if (popular) {
|
||||||
source.getPopularManga(pageNum)
|
source.getPopularManga(pageNum)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
|||||||
import org.jetbrains.exposed.v1.jdbc.update
|
import org.jetbrains.exposed.v1.jdbc.update
|
||||||
import suwayomi.tachidesk.graphql.types.DownloadConversion
|
import suwayomi.tachidesk.graphql.types.DownloadConversion
|
||||||
import suwayomi.tachidesk.manga.impl.util.getChapterCachePath
|
import suwayomi.tachidesk.manga.impl.util.getChapterCachePath
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
||||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
|
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
|
||||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
|
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
|
||||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||||
@@ -118,7 +118,7 @@ object Page {
|
|||||||
return imageFile.inputStream() to (ImageUtil.findImageType { imageFile.inputStream() }?.mime ?: "image/jpeg")
|
return imageFile.inputStream() to (ImageUtil.findImageType { imageFile.inputStream() }?.mime ?: "image/jpeg")
|
||||||
}
|
}
|
||||||
|
|
||||||
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
val source = getSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||||
source as HttpSource
|
source as HttpSource
|
||||||
|
|
||||||
if (pageEntry[PageTable.imageUrl] == null) {
|
if (pageEntry[PageTable.imageUrl] == null) {
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ package suwayomi.tachidesk.manga.impl
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import io.javalin.json.JsonMapper
|
import io.javalin.json.JsonMapper
|
||||||
import io.javalin.json.fromJsonString
|
import io.javalin.json.fromJsonString
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import suwayomi.tachidesk.manga.impl.MangaList.processEntries
|
import suwayomi.tachidesk.manga.impl.MangaList.processEntries
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ object Search {
|
|||||||
searchTerm: String,
|
searchTerm: String,
|
||||||
pageNum: Int,
|
pageNum: Int,
|
||||||
): PagedMangaListDataClass {
|
): PagedMangaListDataClass {
|
||||||
val source = getCatalogueSourceOrStub(sourceId)
|
val source = getSourceOrStub(sourceId)
|
||||||
val searchManga = source.getSearchManga(pageNum, searchTerm, getFilterListOf(source))
|
val searchManga = source.getSearchManga(pageNum, searchTerm, getFilterListOf(source))
|
||||||
return searchManga.processEntries(sourceId)
|
return searchManga.processEntries(sourceId)
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ object Search {
|
|||||||
pageNum: Int,
|
pageNum: Int,
|
||||||
filter: FilterData,
|
filter: FilterData,
|
||||||
): PagedMangaListDataClass {
|
): PagedMangaListDataClass {
|
||||||
val source = getCatalogueSourceOrStub(sourceId)
|
val source = getSourceOrStub(sourceId)
|
||||||
val filterList = if (filter.filter != null) buildFilterList(sourceId, filter.filter) else source.getFilterList()
|
val filterList = if (filter.filter != null) buildFilterList(sourceId, filter.filter) else source.getFilterList()
|
||||||
val searchManga = source.getSearchManga(pageNum, filter.searchTerm ?: "", filterList)
|
val searchManga = source.getSearchManga(pageNum, filter.searchTerm ?: "", filterList)
|
||||||
return searchManga.processEntries(sourceId)
|
return searchManga.processEntries(sourceId)
|
||||||
@@ -43,7 +43,7 @@ object Search {
|
|||||||
private val filterListCache = mutableMapOf<Long, FilterList>()
|
private val filterListCache = mutableMapOf<Long, FilterList>()
|
||||||
|
|
||||||
private fun getFilterListOf(
|
private fun getFilterListOf(
|
||||||
source: CatalogueSource,
|
source: Source,
|
||||||
reset: Boolean = false,
|
reset: Boolean = false,
|
||||||
): FilterList {
|
): FilterList {
|
||||||
if (reset || !filterListCache.containsKey(source.id)) {
|
if (reset || !filterListCache.containsKey(source.id)) {
|
||||||
@@ -56,7 +56,7 @@ object Search {
|
|||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
reset: Boolean,
|
reset: Boolean,
|
||||||
): List<FilterObject> {
|
): List<FilterObject> {
|
||||||
val source = getCatalogueSourceOrStub(sourceId)
|
val source = getSourceOrStub(sourceId)
|
||||||
|
|
||||||
return getFilterListOf(source, reset).list.map {
|
return getFilterListOf(source, reset).list.map {
|
||||||
FilterObject(
|
FilterObject(
|
||||||
@@ -111,7 +111,7 @@ object Search {
|
|||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
changes: List<FilterChange>,
|
changes: List<FilterChange>,
|
||||||
) {
|
) {
|
||||||
val source = getCatalogueSourceOrStub(sourceId)
|
val source = getSourceOrStub(sourceId)
|
||||||
val filterList = getFilterListOf(source, false)
|
val filterList = getFilterListOf(source, false)
|
||||||
updateFilterList(filterList, changes)
|
updateFilterList(filterList, changes)
|
||||||
}
|
}
|
||||||
@@ -169,7 +169,7 @@ object Search {
|
|||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
changes: List<FilterChange>,
|
changes: List<FilterChange>,
|
||||||
): FilterList {
|
): FilterList {
|
||||||
val source = getCatalogueSourceOrStub(sourceId)
|
val source = getSourceOrStub(sourceId)
|
||||||
val filterList = source.getFilterList()
|
val filterList = source.getFilterList()
|
||||||
return updateFilterList(filterList, changes)
|
return updateFilterList(filterList, changes)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
|||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
import suwayomi.tachidesk.manga.impl.Source.preferenceScreenMap
|
import suwayomi.tachidesk.manga.impl.Source.preferenceScreenMap
|
||||||
import suwayomi.tachidesk.manga.impl.extension.Extension.proxyExtensionIconUrl
|
import suwayomi.tachidesk.manga.impl.extension.Extension.proxyExtensionIconUrl
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
|
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrNull
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.unregisterCatalogueSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetSource.unregisterSource
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||||
import suwayomi.tachidesk.manga.model.table.SourceMetaTable
|
import suwayomi.tachidesk.manga.model.table.SourceMetaTable
|
||||||
@@ -43,7 +43,7 @@ object Source {
|
|||||||
fun getSourceList(): List<SourceDataClass> {
|
fun getSourceList(): List<SourceDataClass> {
|
||||||
return transaction {
|
return transaction {
|
||||||
SourceTable.selectAll().mapNotNull {
|
SourceTable.selectAll().mapNotNull {
|
||||||
val catalogueSource = getCatalogueSourceOrNull(it[SourceTable.id].value) ?: return@mapNotNull null
|
val catalogueSource = getSourceOrNull(it[SourceTable.id].value) ?: return@mapNotNull null
|
||||||
val sourceExtension = ExtensionTable.selectAll().where { ExtensionTable.id eq it[SourceTable.extension] }.first()
|
val sourceExtension = ExtensionTable.selectAll().where { ExtensionTable.id eq it[SourceTable.extension] }.first()
|
||||||
|
|
||||||
SourceDataClass(
|
SourceDataClass(
|
||||||
@@ -53,7 +53,7 @@ object Source {
|
|||||||
iconUrl = proxyExtensionIconUrl(sourceExtension[ExtensionTable.pkgName]),
|
iconUrl = proxyExtensionIconUrl(sourceExtension[ExtensionTable.pkgName]),
|
||||||
supportsLatest = catalogueSource.supportsLatest,
|
supportsLatest = catalogueSource.supportsLatest,
|
||||||
isConfigurable = catalogueSource is ConfigurableSource,
|
isConfigurable = catalogueSource is ConfigurableSource,
|
||||||
isNsfw = it[SourceTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal,
|
isNsfw = it[SourceTable.contentWarning] >= ContentWarning.MIXED.ordinal,
|
||||||
displayName = catalogueSource.toString(),
|
displayName = catalogueSource.toString(),
|
||||||
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
|
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
|
||||||
)
|
)
|
||||||
@@ -64,7 +64,7 @@ object Source {
|
|||||||
fun getSource(sourceId: Long): SourceDataClass? { // all the data extracted fresh form the source instance
|
fun getSource(sourceId: Long): SourceDataClass? { // all the data extracted fresh form the source instance
|
||||||
return transaction {
|
return transaction {
|
||||||
val source = SourceTable.selectAll().where { SourceTable.id eq sourceId }.firstOrNull() ?: return@transaction null
|
val source = SourceTable.selectAll().where { SourceTable.id eq sourceId }.firstOrNull() ?: return@transaction null
|
||||||
val catalogueSource = getCatalogueSourceOrNull(sourceId) ?: return@transaction null
|
val catalogueSource = getSourceOrNull(sourceId) ?: return@transaction null
|
||||||
val extension = ExtensionTable.selectAll().where { ExtensionTable.id eq source[SourceTable.extension] }.first()
|
val extension = ExtensionTable.selectAll().where { ExtensionTable.id eq source[SourceTable.extension] }.first()
|
||||||
|
|
||||||
SourceDataClass(
|
SourceDataClass(
|
||||||
@@ -74,7 +74,7 @@ object Source {
|
|||||||
iconUrl = proxyExtensionIconUrl(extension[ExtensionTable.pkgName]),
|
iconUrl = proxyExtensionIconUrl(extension[ExtensionTable.pkgName]),
|
||||||
supportsLatest = catalogueSource.supportsLatest,
|
supportsLatest = catalogueSource.supportsLatest,
|
||||||
isConfigurable = catalogueSource is ConfigurableSource,
|
isConfigurable = catalogueSource is ConfigurableSource,
|
||||||
isNsfw = source[SourceTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal,
|
isNsfw = source[SourceTable.contentWarning] >= ContentWarning.MIXED.ordinal,
|
||||||
displayName = catalogueSource.toString(),
|
displayName = catalogueSource.toString(),
|
||||||
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
|
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
|
||||||
)
|
)
|
||||||
@@ -107,7 +107,7 @@ object Source {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getSourcePreferencesRaw(sourceId: Long): List<Preference> {
|
fun getSourcePreferencesRaw(sourceId: Long): List<Preference> {
|
||||||
val source = getCatalogueSourceOrStub(sourceId)
|
val source = getSourceOrStub(sourceId)
|
||||||
|
|
||||||
if (source is ConfigurableSource) {
|
if (source is ConfigurableSource) {
|
||||||
val sourceShardPreferences = source.sourcePreferences()
|
val sourceShardPreferences = source.sourcePreferences()
|
||||||
@@ -157,7 +157,7 @@ object Source {
|
|||||||
pref.callChangeListener(newValue)
|
pref.callChangeListener(newValue)
|
||||||
|
|
||||||
// must reload the source because a preference was changed
|
// must reload the source because a preference was changed
|
||||||
unregisterCatalogueSource(sourceId)
|
unregisterSource(sourceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSourcesMetaMaps(ids: List<Long>): Map<Long, Map<String, String>> =
|
fun getSourcesMetaMaps(ids: List<Long>): Map<Long, Map<String, String>> =
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ package suwayomi.tachidesk.manga.impl.backup.proto.handlers
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import org.jetbrains.exposed.v1.core.ResultRow
|
import org.jetbrains.exposed.v1.core.ResultRow
|
||||||
import org.jetbrains.exposed.v1.core.SortOrder
|
import org.jetbrains.exposed.v1.core.SortOrder
|
||||||
import org.jetbrains.exposed.v1.core.and
|
import org.jetbrains.exposed.v1.core.and
|
||||||
@@ -78,7 +77,7 @@ object BackupMangaHandler {
|
|||||||
lastModifiedAt = mangaRow[MangaTable.lastModifiedAt],
|
lastModifiedAt = mangaRow[MangaTable.lastModifiedAt],
|
||||||
version = mangaRow[MangaTable.version],
|
version = mangaRow[MangaTable.version],
|
||||||
initialized = mangaRow[MangaTable.initialized],
|
initialized = mangaRow[MangaTable.initialized],
|
||||||
memo = Json.encodeToString(mangaRow[MangaTable.memo]).encodeToByteArray(),
|
memo = mangaRow[MangaTable.memo].encodeToByteArray(),
|
||||||
)
|
)
|
||||||
|
|
||||||
val mangaId = mangaRow[MangaTable.id].value
|
val mangaId = mangaRow[MangaTable.id].value
|
||||||
@@ -116,6 +115,7 @@ object BackupMangaHandler {
|
|||||||
sourceOrder = chapters.size - it[ChapterTable.sourceOrder],
|
sourceOrder = chapters.size - it[ChapterTable.sourceOrder],
|
||||||
lastModifiedAt = it[ChapterTable.lastModifiedAt],
|
lastModifiedAt = it[ChapterTable.lastModifiedAt],
|
||||||
version = it[ChapterTable.version],
|
version = it[ChapterTable.version],
|
||||||
|
memo = it[ChapterTable.memo].encodeToByteArray(),
|
||||||
).apply {
|
).apply {
|
||||||
if (flags.includeClientData) {
|
if (flags.includeClientData) {
|
||||||
this.meta = chapterToMeta[it[ChapterTable.id].value] ?: emptyMap()
|
this.meta = chapterToMeta[it[ChapterTable.id].value] ?: emptyMap()
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ data class BackupManga(
|
|||||||
@ProtoNumber(106) var lastModifiedAt: Long = 0,
|
@ProtoNumber(106) var lastModifiedAt: Long = 0,
|
||||||
@ProtoNumber(109) var version: Long = 0,
|
@ProtoNumber(109) var version: Long = 0,
|
||||||
@ProtoNumber(111) var initialized: Boolean = false,
|
@ProtoNumber(111) var initialized: Boolean = false,
|
||||||
@ProtoNumber(13) var memo: ByteArray = JsonObjectEmptyBytes,
|
@ProtoNumber(112) var memo: ByteArray = JsonObjectEmptyBytes,
|
||||||
// suwayomi
|
// suwayomi
|
||||||
@ProtoNumber(9000) var meta: Map<String, String> = emptyMap(),
|
@ProtoNumber(9000) var meta: Map<String, String> = emptyMap(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import org.jetbrains.exposed.v1.jdbc.selectAll
|
|||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
import org.jetbrains.exposed.v1.jdbc.update
|
import org.jetbrains.exposed.v1.jdbc.update
|
||||||
import suwayomi.tachidesk.manga.impl.ChapterDownloadHelper
|
import suwayomi.tachidesk.manga.impl.ChapterDownloadHelper
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
|
||||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||||
@@ -77,7 +77,7 @@ suspend fun refreshChapterPageList(
|
|||||||
return mutex.withLock {
|
return mutex.withLock {
|
||||||
val chapterEntry = existingChapterEntry ?: transaction { ChapterTable.selectAll().where { ChapterTable.id eq chapterId }.first() }
|
val chapterEntry = existingChapterEntry ?: transaction { ChapterTable.selectAll().where { ChapterTable.id eq chapterId }.first() }
|
||||||
val mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
val mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||||
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
val source = getSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||||
|
|
||||||
val pageList =
|
val pageList =
|
||||||
source
|
source
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ package suwayomi.tachidesk.manga.impl.extension
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||||
@@ -32,17 +31,19 @@ import suwayomi.tachidesk.manga.impl.util.PackageTools
|
|||||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.EXTENSION_FEATURE
|
import suwayomi.tachidesk.manga.impl.util.PackageTools.EXTENSION_FEATURE
|
||||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MAX
|
import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MAX
|
||||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MIN
|
import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MIN
|
||||||
|
import suwayomi.tachidesk.manga.impl.util.PackageTools.METADATA_CONTENT_WARNING
|
||||||
|
import suwayomi.tachidesk.manga.impl.util.PackageTools.METADATA_EXTENSION_LIB
|
||||||
|
import suwayomi.tachidesk.manga.impl.util.PackageTools.METADATA_NAME
|
||||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.METADATA_NSFW
|
import suwayomi.tachidesk.manga.impl.util.PackageTools.METADATA_NSFW
|
||||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.METADATA_SOURCE_CLASS
|
import suwayomi.tachidesk.manga.impl.util.PackageTools.METADATA_SOURCE_CLASS
|
||||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.dex2jar
|
import suwayomi.tachidesk.manga.impl.util.PackageTools.dex2jar
|
||||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.getPackageInfo
|
import suwayomi.tachidesk.manga.impl.util.PackageTools.getPackageInfo
|
||||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.loadExtensionSources
|
import suwayomi.tachidesk.manga.impl.util.PackageTools.loadExtensionSources
|
||||||
import suwayomi.tachidesk.manga.impl.util.network.await
|
import suwayomi.tachidesk.manga.impl.util.network.await
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
||||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.clearCachedImage
|
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.clearCachedImage
|
||||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
|
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
|
||||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.saveImage
|
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.saveImage
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||||
import suwayomi.tachidesk.server.ApplicationDirs
|
import suwayomi.tachidesk.server.ApplicationDirs
|
||||||
@@ -151,7 +152,19 @@ object Extension {
|
|||||||
// throw Exception("This apk is not a signed with the official tachiyomi signature")
|
// throw Exception("This apk is not a signed with the official tachiyomi signature")
|
||||||
// }
|
// }
|
||||||
|
|
||||||
val isNsfw = packageInfo.applicationInfo.metaData.getString(METADATA_NSFW) == "1"
|
var contentWarning = packageInfo.applicationInfo.metaData.getInt(METADATA_CONTENT_WARNING)
|
||||||
|
if (contentWarning == 0) {
|
||||||
|
contentWarning = packageInfo.applicationInfo.metaData
|
||||||
|
.getString(METADATA_CONTENT_WARNING)
|
||||||
|
?.toIntOrNull()
|
||||||
|
?: 0
|
||||||
|
if (contentWarning == 0) {
|
||||||
|
contentWarning = packageInfo.applicationInfo.metaData
|
||||||
|
.getString(METADATA_NSFW)
|
||||||
|
?.toIntOrNull()
|
||||||
|
?: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val className =
|
val className =
|
||||||
packageInfo.packageName + packageInfo.applicationInfo.metaData.getString(METADATA_SOURCE_CLASS)
|
packageInfo.packageName + packageInfo.applicationInfo.metaData.getString(METADATA_SOURCE_CLASS)
|
||||||
@@ -160,7 +173,7 @@ object Extension {
|
|||||||
|
|
||||||
dex2jar(apkFilePath, jarFilePath, fileNameWithoutType)
|
dex2jar(apkFilePath, jarFilePath, fileNameWithoutType)
|
||||||
extractAssetsFromApk(apkFilePath, jarFilePath)
|
extractAssetsFromApk(apkFilePath, jarFilePath)
|
||||||
extractAndCacheApkIcon(apkFilePath, apkName)
|
extractAndCacheApkIcon(apkFilePath, packageInfo.packageName)
|
||||||
|
|
||||||
// clean up
|
// clean up
|
||||||
File(apkFilePath).delete()
|
File(apkFilePath).delete()
|
||||||
@@ -168,12 +181,12 @@ object Extension {
|
|||||||
try {
|
try {
|
||||||
// collect sources from the extension
|
// collect sources from the extension
|
||||||
val extensionMainClassInstance = loadExtensionSources(jarFilePath, className)
|
val extensionMainClassInstance = loadExtensionSources(jarFilePath, className)
|
||||||
val sources: List<CatalogueSource> =
|
val sources: List<Source> =
|
||||||
when (extensionMainClassInstance) {
|
when (extensionMainClassInstance) {
|
||||||
is Source -> listOf(extensionMainClassInstance)
|
is Source -> listOf(extensionMainClassInstance)
|
||||||
is SourceFactory -> extensionMainClassInstance.createSources()
|
is SourceFactory -> extensionMainClassInstance.createSources()
|
||||||
else -> throw RuntimeException("Unknown source class type! ${extensionMainClassInstance.javaClass}")
|
else -> throw RuntimeException("Unknown source class type! ${extensionMainClassInstance.javaClass}")
|
||||||
}.map { it as CatalogueSource }
|
}
|
||||||
|
|
||||||
val langs = sources.map { it.lang }.toSet()
|
val langs = sources.map { it.lang }.toSet()
|
||||||
val extensionLang =
|
val extensionLang =
|
||||||
@@ -184,9 +197,16 @@ object Extension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val extensionName =
|
val extensionName =
|
||||||
packageInfo.applicationInfo.nonLocalizedLabel
|
packageInfo.applicationInfo.metaData.getString(METADATA_NAME)
|
||||||
.toString()
|
?: packageInfo.applicationInfo.nonLocalizedLabel
|
||||||
.substringAfter("Tachiyomi: ")
|
.toString()
|
||||||
|
.substringAfter("Tachiyomi: ")
|
||||||
|
|
||||||
|
val extensionLibVersion =
|
||||||
|
packageInfo.applicationInfo.metaData
|
||||||
|
.getString(METADATA_EXTENSION_LIB)
|
||||||
|
.takeUnless { it == "0" }
|
||||||
|
?: packageInfo.versionName.substringBeforeLast('.')
|
||||||
|
|
||||||
// update extension info
|
// update extension info
|
||||||
transaction {
|
transaction {
|
||||||
@@ -197,14 +217,9 @@ object Extension {
|
|||||||
it[this.pkgName] = packageInfo.packageName
|
it[this.pkgName] = packageInfo.packageName
|
||||||
it[versionName] = packageInfo.versionName
|
it[versionName] = packageInfo.versionName
|
||||||
it[versionCode] = packageInfo.versionCode.toLong()
|
it[versionCode] = packageInfo.versionCode.toLong()
|
||||||
|
it[extensionLib] = extensionLibVersion
|
||||||
it[lang] = extensionLang
|
it[lang] = extensionLang
|
||||||
// todo will change
|
it[this.contentWarning] = contentWarning
|
||||||
it[contentRating] =
|
|
||||||
if (isNsfw) {
|
|
||||||
ContentRating.PORNOGRAPHIC.ordinal
|
|
||||||
} else {
|
|
||||||
ContentRating.SAFE.ordinal
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +244,7 @@ object Extension {
|
|||||||
it[name] = httpSource.name
|
it[name] = httpSource.name
|
||||||
it[lang] = httpSource.lang
|
it[lang] = httpSource.lang
|
||||||
it[extension] = extensionId
|
it[extension] = extensionId
|
||||||
it[contentRating] = if (isNsfw) ContentRating.PORNOGRAPHIC.ordinal else ContentRating.SAFE.ordinal
|
it[this.contentWarning] = contentWarning
|
||||||
}
|
}
|
||||||
logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" }
|
logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" }
|
||||||
}
|
}
|
||||||
@@ -250,7 +265,7 @@ object Extension {
|
|||||||
|
|
||||||
private fun extractAndCacheApkIcon(
|
private fun extractAndCacheApkIcon(
|
||||||
apkFilePath: String,
|
apkFilePath: String,
|
||||||
apkName: String,
|
pkgName: String,
|
||||||
) {
|
) {
|
||||||
val iconCacheDir = "${applicationDirs.extensionsRoot}/icon"
|
val iconCacheDir = "${applicationDirs.extensionsRoot}/icon"
|
||||||
try {
|
try {
|
||||||
@@ -263,15 +278,15 @@ object Extension {
|
|||||||
?.first
|
?.first
|
||||||
}
|
}
|
||||||
if (iconData == null) {
|
if (iconData == null) {
|
||||||
logger.warn { "No icon found in APK $apkName" }
|
logger.warn { "No icon found in APK $pkgName" }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
File(iconCacheDir).mkdirs()
|
File(iconCacheDir).mkdirs()
|
||||||
clearCachedImage(iconCacheDir, apkName)
|
clearCachedImage(iconCacheDir, pkgName)
|
||||||
saveImage("$iconCacheDir/$apkName", iconData.inputStream(), null)
|
saveImage("$iconCacheDir/$pkgName", iconData.inputStream(), null)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.warn(e) { "Failed to extract icon from APK $apkName" }
|
logger.warn(e) { "Failed to extract icon from APK $pkgName" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,7 +379,7 @@ object Extension {
|
|||||||
|
|
||||||
SourceTable.deleteWhere { SourceTable.extension eq extensionId }
|
SourceTable.deleteWhere { SourceTable.extension eq extensionId }
|
||||||
|
|
||||||
if (extensionRecord[ExtensionTable.isObsolete]) {
|
if (extensionRecord[ExtensionTable.isObsolete] || extensionRecord[ExtensionTable.apkUrl] == null) {
|
||||||
ExtensionTable.deleteWhere { ExtensionTable.pkgName eq pkgName }
|
ExtensionTable.deleteWhere { ExtensionTable.pkgName eq pkgName }
|
||||||
} else {
|
} else {
|
||||||
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
|
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
|
||||||
@@ -382,7 +397,7 @@ object Extension {
|
|||||||
PackageTools.jarLoaderMap.remove(jarPath)?.close()
|
PackageTools.jarLoaderMap.remove(jarPath)?.close()
|
||||||
|
|
||||||
// clear all loaded sources
|
// clear all loaded sources
|
||||||
sources.forEach { GetCatalogueSource.unregisterCatalogueSource(it) }
|
sources.forEach { GetSource.unregisterSource(it) }
|
||||||
|
|
||||||
File(jarPath).delete()
|
File(jarPath).delete()
|
||||||
}
|
}
|
||||||
@@ -397,7 +412,7 @@ object Extension {
|
|||||||
it[versionName] = targetExtension.versionName
|
it[versionName] = targetExtension.versionName
|
||||||
it[versionCode] = targetExtension.versionCode
|
it[versionCode] = targetExtension.versionCode
|
||||||
it[lang] = targetExtension.lang
|
it[lang] = targetExtension.lang
|
||||||
it[contentRating] = targetExtension.contentRating.ordinal
|
it[contentWarning] = targetExtension.contentWarning.ordinal
|
||||||
it[iconUrl] = targetExtension.iconUrl
|
it[iconUrl] = targetExtension.iconUrl
|
||||||
it[hasUpdate] = false
|
it[hasUpdate] = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import kotlinx.serialization.decodeFromByteArray
|
|||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.okio.decodeFromBufferedSource
|
import kotlinx.serialization.json.okio.decodeFromBufferedSource
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
|
import okio.BufferedSource
|
||||||
|
import okio.buffer
|
||||||
|
import okio.gzip
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
import org.jetbrains.exposed.v1.core.inList
|
import org.jetbrains.exposed.v1.core.inList
|
||||||
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
||||||
@@ -41,45 +44,45 @@ object ExtensionStoreService {
|
|||||||
val protoBuf: ProtoBuf by injectLazy()
|
val protoBuf: ProtoBuf by injectLazy()
|
||||||
val json: Json by injectLazy()
|
val json: Json by injectLazy()
|
||||||
|
|
||||||
suspend fun fetch(indexUrl: String): ExtensionStore = fetch(indexUrl, forceV2 = false)
|
suspend fun fetch(indexUrl: String): ExtensionStore {
|
||||||
|
var updatedIndexUrl: String = indexUrl
|
||||||
private suspend fun fetch(
|
|
||||||
indexUrl: String,
|
|
||||||
forceV2: Boolean,
|
|
||||||
): ExtensionStore {
|
|
||||||
var updatedIndexUrl = indexUrl
|
|
||||||
return try {
|
return try {
|
||||||
network.client.newCall(GET(indexUrl)).awaitSuccess().body.source().use { source ->
|
val response = network.client.newCall(GET(updatedIndexUrl)).awaitSuccess()
|
||||||
try {
|
response.body.source().decompressIfGzipped().use { source ->
|
||||||
protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.peek().readByteArray())
|
val networkStore =
|
||||||
} catch (e: IllegalArgumentException) {
|
when (source.peek().readByte()) {
|
||||||
logger.debug { "Failed to decode as protobuf, trying JSON" }
|
// "[..."
|
||||||
try {
|
0x5B.toByte() -> {
|
||||||
json.decodeFromBufferedSource<NetworkExtensionStore>(source.peek())
|
run {
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
if (forceV2) throw e
|
|
||||||
logger.debug { "Failed to decode as NetworkExtensionStore, trying LegacyExtensionRepo" }
|
|
||||||
val legacyIndex =
|
|
||||||
try {
|
|
||||||
json.decodeFromBufferedSource<NetworkLegacyExtensionRepo>(source.peek())
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
if (!indexUrl.endsWith("/index.min.json")) {
|
if (!indexUrl.endsWith("/index.min.json")) {
|
||||||
throw e
|
throw IllegalArgumentException("Provided legacy store url is not valid")
|
||||||
}
|
}
|
||||||
logger.debug { "Checking for new repo.json format" }
|
|
||||||
updatedIndexUrl = indexUrl.replace("/index.min.json", "/repo.json")
|
updatedIndexUrl = indexUrl.replace("/index.min.json", "/repo.json")
|
||||||
network.client.newCall(GET(updatedIndexUrl)).awaitSuccess().body.source().use {
|
network.client.newCall(GET(updatedIndexUrl)).awaitSuccess().body.source().use {
|
||||||
json.decodeFromBufferedSource<NetworkLegacyExtensionRepo>(it)
|
json.decodeFromBufferedSource<NetworkLegacyExtensionRepo>(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (legacyIndex.indexV2 != null) {
|
// "{..."
|
||||||
return fetch(legacyIndex.indexV2, forceV2 = true)
|
0x7B.toByte() -> {
|
||||||
} else {
|
try {
|
||||||
legacyIndex
|
json.decodeFromBufferedSource<NetworkLegacyExtensionRepo>(source.peek())
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
json.decodeFromBufferedSource<NetworkExtensionStore>(source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.readByteArray())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.toExtensionStore(updatedIndexUrl)
|
|
||||||
|
if (networkStore is NetworkLegacyExtensionRepo && networkStore.indexV2 != null) {
|
||||||
|
return fetch(networkStore.indexV2)
|
||||||
|
}
|
||||||
|
|
||||||
|
networkStore.toExtensionStore(updatedIndexUrl)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
@@ -105,6 +108,7 @@ object ExtensionStoreService {
|
|||||||
it[contactDiscord] = store.contact.discord
|
it[contactDiscord] = store.contact.discord
|
||||||
it[indexUrl] = store.indexUrl
|
it[indexUrl] = store.indexUrl
|
||||||
it[isLegacy] = store.isLegacy
|
it[isLegacy] = store.isLegacy
|
||||||
|
it[extensionListUrl] = store.extensionListUrl
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ExtensionStoreTable.update({ ExtensionStoreTable.indexUrl eq store.indexUrl }) {
|
ExtensionStoreTable.update({ ExtensionStoreTable.indexUrl eq store.indexUrl }) {
|
||||||
@@ -114,6 +118,7 @@ object ExtensionStoreService {
|
|||||||
it[contactWebsite] = store.contact.website
|
it[contactWebsite] = store.contact.website
|
||||||
it[contactDiscord] = store.contact.discord
|
it[contactDiscord] = store.contact.discord
|
||||||
it[isLegacy] = store.isLegacy
|
it[isLegacy] = store.isLegacy
|
||||||
|
it[extensionListUrl] = store.extensionListUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,17 +215,33 @@ object ExtensionStoreService {
|
|||||||
|
|
||||||
suspend fun getExtensions(store: ExtensionStore): List<ExtensionInfo> {
|
suspend fun getExtensions(store: ExtensionStore): List<ExtensionInfo> {
|
||||||
val extensions =
|
val extensions =
|
||||||
if (!store.isLegacy) {
|
if (store.extensionListUrl != null) {
|
||||||
val response = network.client.newCall(GET(store.indexUrl)).awaitSuccess()
|
val response = network.client.newCall(GET(store.extensionListUrl)).awaitSuccess()
|
||||||
response.body
|
response.body.source().decompressIfGzipped().use { source ->
|
||||||
.source()
|
when (source.peek().readByte()) {
|
||||||
.use { source ->
|
// "{..."
|
||||||
try {
|
0x7B.toByte() -> {
|
||||||
protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.peek().readByteArray())
|
json.decodeFromBufferedSource<NetworkExtensionStore.ExtensionList>(source)
|
||||||
} catch (_: IllegalArgumentException) {
|
}
|
||||||
json.decodeFromBufferedSource<NetworkExtensionStore>(source.peek())
|
|
||||||
|
else -> {
|
||||||
|
protoBuf.decodeFromByteArray<NetworkExtensionStore.ExtensionList>(
|
||||||
|
source.readByteArray(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}.toExtensionInfos(store)
|
}.toExtensionInfos(store)
|
||||||
|
}
|
||||||
|
} else if (!store.isLegacy) {
|
||||||
|
val response = network.client.newCall(GET(store.indexUrl)).awaitSuccess()
|
||||||
|
response.body.source().decompressIfGzipped().use { source ->
|
||||||
|
when (source.peek().readByte()) {
|
||||||
|
// "{..."
|
||||||
|
0x7B.toByte() -> json.decodeFromBufferedSource<NetworkExtensionStore>(source)
|
||||||
|
|
||||||
|
else -> protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.readByteArray())
|
||||||
|
}.extensionList!!
|
||||||
|
.toExtensionInfos(store)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
val storeBaseUrl = store.indexUrl.removeSuffix("/repo.json")
|
val storeBaseUrl = store.indexUrl.removeSuffix("/repo.json")
|
||||||
val response = network.client.newCall(GET("$storeBaseUrl/index.min.json")).awaitSuccess()
|
val response = network.client.newCall(GET("$storeBaseUrl/index.min.json")).awaitSuccess()
|
||||||
@@ -232,4 +253,17 @@ object ExtensionStoreService {
|
|||||||
}
|
}
|
||||||
return extensions
|
return extensions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun BufferedSource.decompressIfGzipped(): BufferedSource {
|
||||||
|
val isGzip =
|
||||||
|
peek().use { peeked ->
|
||||||
|
try {
|
||||||
|
peeked.readShort().toInt() == 0x1f8b
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (isGzip) gzip().buffer() else this
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
|||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
import org.jetbrains.exposed.v1.jdbc.update
|
import org.jetbrains.exposed.v1.jdbc.update
|
||||||
import suwayomi.tachidesk.manga.impl.extension.Extension.proxyExtensionIconUrl
|
import suwayomi.tachidesk.manga.impl.extension.Extension.proxyExtensionIconUrl
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo
|
import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||||
@@ -81,7 +81,7 @@ object ExtensionsList {
|
|||||||
versionName = it[ExtensionTable.versionName],
|
versionName = it[ExtensionTable.versionName],
|
||||||
versionCode = it[ExtensionTable.versionCode].toInt(),
|
versionCode = it[ExtensionTable.versionCode].toInt(),
|
||||||
lang = it[ExtensionTable.lang],
|
lang = it[ExtensionTable.lang],
|
||||||
isNsfw = it[ExtensionTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal,
|
isNsfw = it[ExtensionTable.contentWarning] >= ContentWarning.MIXED.ordinal,
|
||||||
installed = it[ExtensionTable.isInstalled],
|
installed = it[ExtensionTable.isInstalled],
|
||||||
hasUpdate = it[ExtensionTable.hasUpdate],
|
hasUpdate = it[ExtensionTable.hasUpdate],
|
||||||
obsolete = it[ExtensionTable.isObsolete],
|
obsolete = it[ExtensionTable.isObsolete],
|
||||||
@@ -132,6 +132,7 @@ object ExtensionsList {
|
|||||||
// Always update icon url and repo
|
// Always update icon url and repo
|
||||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||||
this[ExtensionTable.storeIndexUrl] = foundExtension.storeIndexUrl
|
this[ExtensionTable.storeIndexUrl] = foundExtension.storeIndexUrl
|
||||||
|
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
||||||
|
|
||||||
// add these because batch updates need matching columns
|
// add these because batch updates need matching columns
|
||||||
this[ExtensionTable.hasUpdate] = extensionRecord[ExtensionTable.hasUpdate]
|
this[ExtensionTable.hasUpdate] = extensionRecord[ExtensionTable.hasUpdate]
|
||||||
@@ -173,7 +174,7 @@ object ExtensionsList {
|
|||||||
this[ExtensionTable.versionName] = foundExtension.versionName
|
this[ExtensionTable.versionName] = foundExtension.versionName
|
||||||
this[ExtensionTable.versionCode] = foundExtension.versionCode
|
this[ExtensionTable.versionCode] = foundExtension.versionCode
|
||||||
this[ExtensionTable.lang] = foundExtension.lang
|
this[ExtensionTable.lang] = foundExtension.lang
|
||||||
this[ExtensionTable.contentRating] = foundExtension.contentRating.ordinal
|
this[ExtensionTable.contentWarning] = foundExtension.contentWarning.ordinal
|
||||||
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
||||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||||
}
|
}
|
||||||
@@ -190,7 +191,7 @@ object ExtensionsList {
|
|||||||
this[ExtensionTable.versionName] = foundExtension.versionName
|
this[ExtensionTable.versionName] = foundExtension.versionName
|
||||||
this[ExtensionTable.versionCode] = foundExtension.versionCode
|
this[ExtensionTable.versionCode] = foundExtension.versionCode
|
||||||
this[ExtensionTable.lang] = foundExtension.lang
|
this[ExtensionTable.lang] = foundExtension.lang
|
||||||
this[ExtensionTable.contentRating] = foundExtension.contentRating.ordinal
|
this[ExtensionTable.contentWarning] = foundExtension.contentWarning.ordinal
|
||||||
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
||||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import kotlinx.serialization.ExperimentalSerializationApi
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.JsonNames
|
import kotlinx.serialization.json.JsonNames
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo
|
import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionSource
|
import suwayomi.tachidesk.manga.model.dataclass.ExtensionSource
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionStore
|
import suwayomi.tachidesk.manga.model.dataclass.ExtensionStore
|
||||||
@@ -23,7 +23,8 @@ data class NetworkExtensionStore(
|
|||||||
@ProtoNumber(2) val badgeLabel: String,
|
@ProtoNumber(2) val badgeLabel: String,
|
||||||
@ProtoNumber(3) val signingKey: String,
|
@ProtoNumber(3) val signingKey: String,
|
||||||
@ProtoNumber(4) val contact: Contact,
|
@ProtoNumber(4) val contact: Contact,
|
||||||
@ProtoNumber(5) val extensions: List<Extension>,
|
@ProtoNumber(101) val extensionList: ExtensionList?,
|
||||||
|
@ProtoNumber(102) val extensionListUrl: String?,
|
||||||
) : BaseNetworkExtensionStore {
|
) : BaseNetworkExtensionStore {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Contact(
|
data class Contact(
|
||||||
@@ -31,6 +32,11 @@ data class NetworkExtensionStore(
|
|||||||
@ProtoNumber(2) val discord: String?,
|
@ProtoNumber(2) val discord: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ExtensionList(
|
||||||
|
@ProtoNumber(1) val extensions: List<Extension>,
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Extension(
|
data class Extension(
|
||||||
@ProtoNumber(1) val name: String,
|
@ProtoNumber(1) val name: String,
|
||||||
@@ -39,7 +45,8 @@ data class NetworkExtensionStore(
|
|||||||
@ProtoNumber(4) val extensionLib: String,
|
@ProtoNumber(4) val extensionLib: String,
|
||||||
@ProtoNumber(5) val versionCode: Long,
|
@ProtoNumber(5) val versionCode: Long,
|
||||||
@ProtoNumber(6) val versionName: String,
|
@ProtoNumber(6) val versionName: String,
|
||||||
@ProtoNumber(7) val sources: List<Source>,
|
@ProtoNumber(7) val contentWarning: ContentWarning,
|
||||||
|
@ProtoNumber(8) val sources: List<Source>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -55,27 +62,27 @@ data class NetworkExtensionStore(
|
|||||||
@ProtoNumber(3) val language: String,
|
@ProtoNumber(3) val language: String,
|
||||||
@ProtoNumber(4) val homeUrl: String = "",
|
@ProtoNumber(4) val homeUrl: String = "",
|
||||||
@ProtoNumber(5) val mirrorUrls: List<String> = emptyList(),
|
@ProtoNumber(5) val mirrorUrls: List<String> = emptyList(),
|
||||||
@ProtoNumber(6) val contentRating: ContentRating = ContentRating.SAFE,
|
// @ProtoNumber(6) val contentWarning: ContentWarning = ContentWarning.SAFE,
|
||||||
@ProtoNumber(7) val message: String? = null,
|
@ProtoNumber(7) val message: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
enum class ContentRating {
|
enum class ContentWarning {
|
||||||
@ProtoNumber(0)
|
@ProtoNumber(0)
|
||||||
@JsonNames("CONTENT_RATING_SAFE")
|
@JsonNames("CONTENT_WARNING_UNSPECIFIED")
|
||||||
SAFE,
|
UNSPECIFIED,
|
||||||
|
|
||||||
@ProtoNumber(1)
|
@ProtoNumber(1)
|
||||||
@JsonNames("CONTENT_RATING_SUGGESTIVE")
|
@JsonNames("CONTENT_WARNING_SAFE")
|
||||||
SUGGESTIVE,
|
SAFE,
|
||||||
|
|
||||||
@ProtoNumber(2)
|
@ProtoNumber(2)
|
||||||
@JsonNames("CONTENT_RATING_EROTICA")
|
@JsonNames("CONTENT_WARNING_MIXED")
|
||||||
EROTICA,
|
MIXED,
|
||||||
|
|
||||||
@ProtoNumber(3)
|
@ProtoNumber(3)
|
||||||
@JsonNames("CONTENT_RATING_PORNOGRAPHIC")
|
@JsonNames("CONTENT_WARNING_NSFW")
|
||||||
PORNOGRAPHIC,
|
NSFW,
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toExtensionStore(indexUrl: String): ExtensionStore =
|
override fun toExtensionStore(indexUrl: String): ExtensionStore =
|
||||||
@@ -90,10 +97,11 @@ data class NetworkExtensionStore(
|
|||||||
discord = contact.discord,
|
discord = contact.discord,
|
||||||
),
|
),
|
||||||
isLegacy = false,
|
isLegacy = false,
|
||||||
|
extensionListUrl = extensionListUrl,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NetworkExtensionStore.toExtensionInfos(store: ExtensionStore): List<ExtensionInfo> =
|
fun NetworkExtensionStore.ExtensionList.toExtensionInfos(store: ExtensionStore): List<ExtensionInfo> =
|
||||||
extensions.map { extension ->
|
extensions.map { extension ->
|
||||||
val lang = extension.sources.map { it.language }.toSet()
|
val lang = extension.sources.map { it.language }.toSet()
|
||||||
ExtensionInfo(
|
ExtensionInfo(
|
||||||
@@ -106,13 +114,15 @@ fun NetworkExtensionStore.toExtensionInfos(store: ExtensionStore): List<Extensio
|
|||||||
versionCode = extension.versionCode,
|
versionCode = extension.versionCode,
|
||||||
versionName = extension.versionName,
|
versionName = extension.versionName,
|
||||||
lang = if (lang.size == 1) lang.first() else "all",
|
lang = if (lang.size == 1) lang.first() else "all",
|
||||||
contentRating =
|
contentWarning =
|
||||||
when (extension.sources.maxOfOrNull { it.contentRating }) {
|
when (extension.contentWarning) {
|
||||||
NetworkExtensionStore.ContentRating.SAFE -> ContentRating.SAFE
|
NetworkExtensionStore.ContentWarning.SAFE,
|
||||||
NetworkExtensionStore.ContentRating.SUGGESTIVE -> ContentRating.SUGGESTIVE
|
NetworkExtensionStore.ContentWarning.UNSPECIFIED,
|
||||||
NetworkExtensionStore.ContentRating.EROTICA -> ContentRating.EROTICA
|
-> ContentWarning.SAFE
|
||||||
NetworkExtensionStore.ContentRating.PORNOGRAPHIC -> ContentRating.PORNOGRAPHIC
|
|
||||||
null -> ContentRating.SAFE
|
NetworkExtensionStore.ContentWarning.MIXED -> ContentWarning.MIXED
|
||||||
|
|
||||||
|
NetworkExtensionStore.ContentWarning.NSFW -> ContentWarning.NSFW
|
||||||
},
|
},
|
||||||
sources =
|
sources =
|
||||||
extension.sources.map { source ->
|
extension.sources.map { source ->
|
||||||
@@ -122,12 +132,15 @@ fun NetworkExtensionStore.toExtensionInfos(store: ExtensionStore): List<Extensio
|
|||||||
lang = source.language,
|
lang = source.language,
|
||||||
homeUrl = source.homeUrl,
|
homeUrl = source.homeUrl,
|
||||||
message = source.message,
|
message = source.message,
|
||||||
contentRating =
|
contentWarning =
|
||||||
when (source.contentRating) {
|
when (extension.contentWarning) { // todo source.contentWarning
|
||||||
NetworkExtensionStore.ContentRating.SAFE -> ContentRating.SAFE
|
NetworkExtensionStore.ContentWarning.SAFE,
|
||||||
NetworkExtensionStore.ContentRating.SUGGESTIVE -> ContentRating.SUGGESTIVE
|
NetworkExtensionStore.ContentWarning.UNSPECIFIED,
|
||||||
NetworkExtensionStore.ContentRating.EROTICA -> ContentRating.EROTICA
|
-> ContentWarning.SAFE
|
||||||
NetworkExtensionStore.ContentRating.PORNOGRAPHIC -> ContentRating.PORNOGRAPHIC
|
|
||||||
|
NetworkExtensionStore.ContentWarning.MIXED -> ContentWarning.MIXED
|
||||||
|
|
||||||
|
NetworkExtensionStore.ContentWarning.NSFW -> ContentWarning.NSFW
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ package suwayomi.tachidesk.manga.impl.extension.github
|
|||||||
|
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo
|
import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionSource
|
import suwayomi.tachidesk.manga.model.dataclass.ExtensionSource
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionStore
|
import suwayomi.tachidesk.manga.model.dataclass.ExtensionStore
|
||||||
@@ -49,7 +49,7 @@ fun NetworkLegacyExtension.toExtensionInfo(
|
|||||||
versionCode = code,
|
versionCode = code,
|
||||||
versionName = version,
|
versionName = version,
|
||||||
lang = lang,
|
lang = lang,
|
||||||
contentRating = if (nsfw == 1) ContentRating.PORNOGRAPHIC else ContentRating.SAFE,
|
contentWarning = if (nsfw == 1) ContentWarning.MIXED else ContentWarning.SAFE,
|
||||||
sources =
|
sources =
|
||||||
if (sources.isNullOrEmpty()) {
|
if (sources.isNullOrEmpty()) {
|
||||||
listOf(
|
listOf(
|
||||||
@@ -59,7 +59,7 @@ fun NetworkLegacyExtension.toExtensionInfo(
|
|||||||
lang = lang,
|
lang = lang,
|
||||||
homeUrl = "",
|
homeUrl = "",
|
||||||
message = null,
|
message = null,
|
||||||
contentRating = if (nsfw == 1) ContentRating.PORNOGRAPHIC else ContentRating.SAFE,
|
contentWarning = if (nsfw == 1) ContentWarning.MIXED else ContentWarning.SAFE,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -70,7 +70,7 @@ fun NetworkLegacyExtension.toExtensionInfo(
|
|||||||
lang = source.lang,
|
lang = source.lang,
|
||||||
homeUrl = source.baseUrl,
|
homeUrl = source.baseUrl,
|
||||||
message = null,
|
message = null,
|
||||||
contentRating = if (nsfw == 1) ContentRating.PORNOGRAPHIC else ContentRating.SAFE,
|
contentWarning = if (nsfw == 1) ContentWarning.MIXED else ContentWarning.SAFE,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,5 +36,6 @@ data class NetworkLegacyExtensionRepo(
|
|||||||
discord = null,
|
discord = null,
|
||||||
),
|
),
|
||||||
isLegacy = true,
|
isLegacy = true,
|
||||||
|
extensionListUrl = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,29 +127,36 @@ object KoreaderSyncService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val mangaId = chapterRow[ChapterTable.manga].value
|
val mangaId = chapterRow[ChapterTable.manga].value
|
||||||
|
val isDownloaded = chapterRow[ChapterTable.isDownloaded]
|
||||||
val checksumMethod = serverConfig.koreaderSyncChecksumMethod.value
|
val checksumMethod = serverConfig.koreaderSyncChecksumMethod.value
|
||||||
|
|
||||||
val newHash =
|
val newHash =
|
||||||
when (checksumMethod) {
|
when (checksumMethod) {
|
||||||
KoreaderSyncChecksumMethod.BINARY -> {
|
KoreaderSyncChecksumMethod.BINARY -> {
|
||||||
logger.debug { "[KOSYNC HASH] No hash for chapterId=$chapterId. Generating from downloaded content." }
|
// Only generate binary hash if the chapter is downloaded to avoid fetching missing files
|
||||||
try {
|
if (isDownloaded) {
|
||||||
// Always create a CBZ in memory if it doesn't exist
|
logger.debug { "[KOSYNC HASH] No hash for chapterId=$chapterId. Generating from downloaded content." }
|
||||||
val (stream, _) = ChapterDownloadHelper.getArchiveStreamWithSize(mangaId, chapterId)
|
|
||||||
// Write the stream to a temp file for partial hashing
|
|
||||||
val tempFile = File.createTempFile("kosync-hash-", ".cbz")
|
|
||||||
try {
|
try {
|
||||||
tempFile.outputStream().use { fos ->
|
// Always create a CBZ in memory if it doesn't exist
|
||||||
stream.use { it.copyTo(fos) }
|
val (stream, _) = ChapterDownloadHelper.getArchiveStreamWithSize(mangaId, chapterId)
|
||||||
|
// Write the stream to a temp file for partial hashing
|
||||||
|
val tempFile = File.createTempFile("kosync-hash-", ".cbz")
|
||||||
|
try {
|
||||||
|
tempFile.outputStream().use { fos ->
|
||||||
|
stream.use { it.copyTo(fos) }
|
||||||
|
}
|
||||||
|
// Use the same hashing method as for downloads
|
||||||
|
KoreaderHelper.hashContents(tempFile)
|
||||||
|
} finally {
|
||||||
|
// Always delete the temp file
|
||||||
|
tempFile.delete()
|
||||||
}
|
}
|
||||||
// Use the same hashing method as for downloads
|
} catch (e: Exception) {
|
||||||
KoreaderHelper.hashContents(tempFile)
|
logger.warn(e) { "[KOSYNC HASH] Failed to generate archive stream for chapterId=$chapterId." }
|
||||||
} finally {
|
null
|
||||||
// Always delete the temp file
|
|
||||||
tempFile.delete()
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} else {
|
||||||
logger.warn(e) { "[KOSYNC HASH] Failed to generate archive stream for chapterId=$chapterId." }
|
logger.debug { "[KOSYNC HASH] Skipping binary hash for chapterId=$chapterId because it is not downloaded." }
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,7 +182,7 @@ object KoreaderSyncService {
|
|||||||
}
|
}
|
||||||
logger.info { "[KOSYNC HASH] Generated and saved new hash for chapterId=$chapterId" }
|
logger.info { "[KOSYNC HASH] Generated and saved new hash for chapterId=$chapterId" }
|
||||||
} else {
|
} else {
|
||||||
logger.warn { "[KOSYNC HASH] Hashing failed for chapterId=$chapterId." }
|
logger.warn { "[KOSYNC HASH] Hashing failed or skipped for chapterId=$chapterId." }
|
||||||
}
|
}
|
||||||
newHash
|
newHash
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,6 +222,48 @@ object Track {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun bindTrackRecord(
|
||||||
|
mangaId: Int,
|
||||||
|
trackRecordId: Int,
|
||||||
|
): Int {
|
||||||
|
val (trackRecord, existingTrackRecord) =
|
||||||
|
transaction {
|
||||||
|
val trackRecord =
|
||||||
|
TrackRecordTable
|
||||||
|
.selectAll()
|
||||||
|
.where {
|
||||||
|
(TrackRecordTable.id eq trackRecordId)
|
||||||
|
}.first()
|
||||||
|
.toTrackRecordDataClass()
|
||||||
|
|
||||||
|
val existingTrackRecord =
|
||||||
|
TrackRecordTable
|
||||||
|
.selectAll()
|
||||||
|
.where {
|
||||||
|
(TrackRecordTable.mangaId eq mangaId) and (TrackRecordTable.trackerId eq trackRecord.trackerId)
|
||||||
|
}.firstOrNull()
|
||||||
|
?.toTrackRecordDataClass()
|
||||||
|
|
||||||
|
trackRecord to existingTrackRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
val isAlreadyBoundToManga = trackRecord.mangaId == mangaId
|
||||||
|
if (isAlreadyBoundToManga) {
|
||||||
|
return trackRecordId
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasRecordForTracker = existingTrackRecord != null
|
||||||
|
if (hasRecordForTracker) {
|
||||||
|
val updatedTrack = trackRecord.copy(id = existingTrackRecord.id, mangaId = mangaId).toTrack()
|
||||||
|
|
||||||
|
return updateTrackRecord(updatedTrack)
|
||||||
|
}
|
||||||
|
|
||||||
|
val newTrack = trackRecord.copy(mangaId = mangaId).toTrack()
|
||||||
|
|
||||||
|
return insertTrackRecord(newTrack)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun refresh(recordId: Int) {
|
suspend fun refresh(recordId: Int) {
|
||||||
val recordDb =
|
val recordDb =
|
||||||
transaction {
|
transaction {
|
||||||
@@ -423,9 +465,9 @@ object Track {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateTrackRecord(track: Track) = updateTrackRecords(listOf(track))
|
fun updateTrackRecord(track: Track): Int = updateTrackRecords(listOf(track)).first()
|
||||||
|
|
||||||
fun updateTrackRecords(tracks: List<Track>) =
|
fun updateTrackRecords(tracks: List<Track>): List<Int> =
|
||||||
transaction {
|
transaction {
|
||||||
if (tracks.isNotEmpty()) {
|
if (tracks.isNotEmpty()) {
|
||||||
BatchUpdateStatement(TrackRecordTable)
|
BatchUpdateStatement(TrackRecordTable)
|
||||||
@@ -447,6 +489,8 @@ object Track {
|
|||||||
}.toExecutable()
|
}.toExecutable()
|
||||||
.execute(this@transaction)
|
.execute(this@transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tracks.map { it.id!! }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun insertTrackRecord(track: Track): Int = insertTrackRecords(listOf(track)).first()
|
fun insertTrackRecord(track: Track): Int = insertTrackRecords(listOf(track)).first()
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import io.github.oshai.kotlinlogging.KotlinLogging
|
|||||||
import org.jetbrains.exposed.v1.core.eq
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
||||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||||
import suwayomi.tachidesk.server.ApplicationDirs
|
import suwayomi.tachidesk.server.ApplicationDirs
|
||||||
@@ -37,7 +37,7 @@ private fun getMangaDir(
|
|||||||
private fun getMangaDir(mangaId: Int): String =
|
private fun getMangaDir(mangaId: Int): String =
|
||||||
transaction {
|
transaction {
|
||||||
val mangaEntry = MangaTable.selectAll().where { MangaTable.id eq mangaId }.first()
|
val mangaEntry = MangaTable.selectAll().where { MangaTable.id eq mangaId }.first()
|
||||||
val source = GetCatalogueSource.getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
val source = GetSource.getSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||||
|
|
||||||
getMangaDir(mangaEntry[MangaTable.title], source.toString())
|
getMangaDir(mangaEntry[MangaTable.title], source.toString())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ object PackageTools {
|
|||||||
const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
||||||
const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
|
const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
|
||||||
const val METADATA_NSFW = "tachiyomi.extension.nsfw"
|
const val METADATA_NSFW = "tachiyomi.extension.nsfw"
|
||||||
|
|
||||||
|
const val METADATA_NAME = "tachiyomix.name"
|
||||||
|
const val METADATA_EXTENSION_LIB = "tachiyomix.extensionLib"
|
||||||
|
const val METADATA_CONTENT_WARNING = "tachiyomix.contentWarning"
|
||||||
|
|
||||||
const val LIB_VERSION_MIN = 1.3
|
const val LIB_VERSION_MIN = 1.3
|
||||||
const val LIB_VERSION_MAX = 1.6
|
const val LIB_VERSION_MAX = 1.6
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ package suwayomi.tachidesk.manga.impl.util.source
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
@@ -22,14 +21,14 @@ import suwayomi.tachidesk.server.ApplicationDirs
|
|||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
object GetCatalogueSource {
|
object GetSource {
|
||||||
private val logger = KotlinLogging.logger { }
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
private val sourceCache = ConcurrentHashMap<Long, CatalogueSource>()
|
private val sourceCache = ConcurrentHashMap<Long, Source>()
|
||||||
private val applicationDirs: ApplicationDirs by injectLazy()
|
private val applicationDirs: ApplicationDirs by injectLazy()
|
||||||
|
|
||||||
private fun getCatalogueSource(sourceId: Long): CatalogueSource? {
|
private fun getSource(sourceId: Long): Source? {
|
||||||
val cachedResult: CatalogueSource? = sourceCache[sourceId]
|
val cachedResult: Source? = sourceCache[sourceId]
|
||||||
if (cachedResult != null) {
|
if (cachedResult != null) {
|
||||||
return cachedResult
|
return cachedResult
|
||||||
}
|
}
|
||||||
@@ -62,25 +61,25 @@ object GetCatalogueSource {
|
|||||||
return sourceCache[sourceId]!!
|
return sourceCache[sourceId]!!
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCatalogueSourceOrNull(sourceId: Long): CatalogueSource? =
|
fun getSourceOrNull(sourceId: Long): Source? =
|
||||||
try {
|
try {
|
||||||
getCatalogueSource(sourceId)
|
getSource(sourceId)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.warn(e) { "getCatalogueSource($sourceId) failed" }
|
logger.warn(e) { "getCatalogueSource($sourceId) failed" }
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCatalogueSourceOrStub(sourceId: Long): CatalogueSource = getCatalogueSourceOrNull(sourceId) ?: StubSource(sourceId)
|
fun getSourceOrStub(sourceId: Long): Source = getSourceOrNull(sourceId) ?: StubSource(sourceId)
|
||||||
|
|
||||||
fun registerCatalogueSource(sourcePair: Pair<Long, CatalogueSource>) {
|
fun registerSource(sourcePair: Pair<Long, Source>) {
|
||||||
sourceCache += sourcePair
|
sourceCache += sourcePair
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unregisterCatalogueSource(sourceId: Long) {
|
fun unregisterSource(sourceId: Long) {
|
||||||
sourceCache.remove(sourceId)
|
sourceCache.remove(sourceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unregisterAllCatalogueSources() {
|
fun unregisterAllSources() {
|
||||||
(sourceCache - 0L).forEach { (id, _) ->
|
(sourceCache - 0L).forEach { (id, _) ->
|
||||||
sourceCache.remove(id)
|
sourceCache.remove(id)
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ data class ExtensionInfo(
|
|||||||
val versionCode: Long,
|
val versionCode: Long,
|
||||||
val versionName: String,
|
val versionName: String,
|
||||||
val lang: String,
|
val lang: String,
|
||||||
val contentRating: ContentRating,
|
val contentWarning: ContentWarning,
|
||||||
val sources: List<ExtensionSource>,
|
val sources: List<ExtensionSource>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,17 +27,16 @@ data class ExtensionSource(
|
|||||||
val lang: String,
|
val lang: String,
|
||||||
val homeUrl: String,
|
val homeUrl: String,
|
||||||
val message: String?,
|
val message: String?,
|
||||||
val contentRating: ContentRating,
|
val contentWarning: ContentWarning,
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class ContentRating {
|
enum class ContentWarning {
|
||||||
SAFE,
|
SAFE,
|
||||||
SUGGESTIVE,
|
MIXED,
|
||||||
EROTICA,
|
NSFW,
|
||||||
PORNOGRAPHIC,
|
|
||||||
;
|
;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun valueOf(contentRating: Int) = entries.find { it.ordinal == contentRating } ?: SAFE
|
fun valueOf(contentWarning: Int) = entries.find { it.ordinal == contentWarning } ?: SAFE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ data class ExtensionStore(
|
|||||||
val signingKey: String,
|
val signingKey: String,
|
||||||
val contact: Contact,
|
val contact: Contact,
|
||||||
val isLegacy: Boolean,
|
val isLegacy: Boolean,
|
||||||
|
val extensionListUrl: String?,
|
||||||
) {
|
) {
|
||||||
data class Contact(
|
data class Contact(
|
||||||
val website: String,
|
val website: String,
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ package suwayomi.tachidesk.manga.model.table
|
|||||||
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||||
|
|
||||||
object ExtensionStoreTable : IntIdTable() {
|
object ExtensionStoreTable : IntIdTable() {
|
||||||
val indexUrl = varchar("index_url", 2048)
|
val indexUrl = varchar("index_url", 2048).uniqueIndex()
|
||||||
val name = varchar("name", 256)
|
val name = varchar("name", 256)
|
||||||
val badgeLabel = varchar("badge_label", 32)
|
val badgeLabel = varchar("badge_label", 32)
|
||||||
val signingKey = varchar("signing_key", 512)
|
val signingKey = varchar("signing_key", 512)
|
||||||
val contactWebsite = varchar("contact_website", 2048)
|
val contactWebsite = varchar("contact_website", 2048)
|
||||||
val contactDiscord = varchar("contact_discord", 2048).nullable()
|
val contactDiscord = varchar("contact_discord", 2048).nullable()
|
||||||
val isLegacy = bool("is_legacy").default(false)
|
val isLegacy = bool("is_legacy").default(false)
|
||||||
|
val extensionListUrl = varchar("extension_list_url", 2048).nullable()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
|||||||
|
|
||||||
object ExtensionTable : IntIdTable() {
|
object ExtensionTable : IntIdTable() {
|
||||||
val apkName = varchar("apk_name", 1024).nullable()
|
val apkName = varchar("apk_name", 1024).nullable()
|
||||||
val storeIndexUrl = varchar("store_index_url", 2048).nullable()
|
val storeIndexUrl = varchar("store_index_url", 2048).nullable().index()
|
||||||
|
|
||||||
// default is the local source icon from tachiyomi
|
// default is the local source icon from tachiyomi
|
||||||
@Suppress("ktlint:standard:max-line-length")
|
@Suppress("ktlint:standard:max-line-length")
|
||||||
@@ -23,12 +23,12 @@ object ExtensionTable : IntIdTable() {
|
|||||||
|
|
||||||
val name = varchar("name", 128)
|
val name = varchar("name", 128)
|
||||||
val pkgName = varchar("pkg_name", 128)
|
val pkgName = varchar("pkg_name", 128)
|
||||||
val apkUrl = varchar("apk_url", 2048)
|
val apkUrl = varchar("apk_url", 2048).nullable()
|
||||||
val extensionLib = varchar("extension_lib", 16).nullable()
|
val extensionLib = varchar("extension_lib", 16).nullable()
|
||||||
val versionName = varchar("version_name", 16)
|
val versionName = varchar("version_name", 16)
|
||||||
val versionCode = long("version_code")
|
val versionCode = long("version_code")
|
||||||
val lang = varchar("lang", 32)
|
val lang = varchar("lang", 32)
|
||||||
val contentRating = integer("content_rating")
|
val contentWarning = integer("content_warning")
|
||||||
|
|
||||||
val isInstalled = bool("is_installed").default(false)
|
val isInstalled = bool("is_installed").default(false)
|
||||||
val hasUpdate = bool("has_update").default(false)
|
val hasUpdate = bool("has_update").default(false)
|
||||||
|
|||||||
@@ -8,13 +8,11 @@ package suwayomi.tachidesk.manga.model.table
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import org.jetbrains.exposed.v1.core.dao.id.IdTable
|
import org.jetbrains.exposed.v1.core.dao.id.IdTable
|
||||||
import suwayomi.tachidesk.manga.model.table.columns.unlimitedVarchar
|
|
||||||
|
|
||||||
object SourceTable : IdTable<Long>() {
|
object SourceTable : IdTable<Long>() {
|
||||||
override val id = long("id").entityId()
|
override val id = long("id").entityId()
|
||||||
val name = varchar("name", 128)
|
val name = varchar("name", 128)
|
||||||
val lang = varchar("lang", 32)
|
val lang = varchar("lang", 32)
|
||||||
val extension = reference("extension", ExtensionTable)
|
val extension = reference("extension", ExtensionTable)
|
||||||
val message = unlimitedVarchar("message").nullable()
|
val contentWarning = integer("content_warning").default(0)
|
||||||
val contentRating = integer("content_rating").default(0)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -644,6 +644,16 @@ object OpdsFeedBuilder {
|
|||||||
skipMetadata,
|
skipMetadata,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Return a not-found feed if all available chapters are filtered out as unreachable
|
||||||
|
if (skipMetadata && chapterEntries.isEmpty() && totalChapters > 0L) {
|
||||||
|
return buildNotFoundFeed(
|
||||||
|
baseUrl = baseUrl,
|
||||||
|
locale = locale,
|
||||||
|
idPath = "series/$mangaId/chapters",
|
||||||
|
title = MR.strings.opds_error_chapters_not_found.localized(locale, pageNum),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// If no chapters are found in the database, attempt to fetch them from the source.
|
// If no chapters are found in the database, attempt to fetch them from the source.
|
||||||
if (chapterEntries.isEmpty() && totalChapters == 0L) {
|
if (chapterEntries.isEmpty() && totalChapters == 0L) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ object ChapterRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.awaitAll()
|
}.awaitAll()
|
||||||
|
// Exclude unreachable chapters that are not downloaded and have no page count
|
||||||
|
.filter { it.downloaded || it.pageCount > 0 }
|
||||||
|
|
||||||
return Pair(enrichedChapters, totalCount)
|
return Pair(enrichedChapters, totalCount)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import org.jetbrains.exposed.v1.core.SortOrder
|
|||||||
import org.jetbrains.exposed.v1.core.alias
|
import org.jetbrains.exposed.v1.core.alias
|
||||||
import org.jetbrains.exposed.v1.core.and
|
import org.jetbrains.exposed.v1.core.and
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
import org.jetbrains.exposed.v1.core.greater
|
|
||||||
import org.jetbrains.exposed.v1.core.inList
|
import org.jetbrains.exposed.v1.core.inList
|
||||||
|
import org.jetbrains.exposed.v1.core.inSubQuery
|
||||||
import org.jetbrains.exposed.v1.core.intLiteral
|
import org.jetbrains.exposed.v1.core.intLiteral
|
||||||
import org.jetbrains.exposed.v1.core.like
|
import org.jetbrains.exposed.v1.core.like
|
||||||
import org.jetbrains.exposed.v1.core.lowerCase
|
import org.jetbrains.exposed.v1.core.lowerCase
|
||||||
@@ -22,7 +22,7 @@ import org.jetbrains.exposed.v1.jdbc.andWhere
|
|||||||
import org.jetbrains.exposed.v1.jdbc.select
|
import org.jetbrains.exposed.v1.jdbc.select
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
import suwayomi.tachidesk.manga.impl.MangaList.insertOrUpdate
|
import suwayomi.tachidesk.manga.impl.MangaList.insertOrUpdate
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.toGenreList
|
import suwayomi.tachidesk.manga.model.dataclass.toGenreList
|
||||||
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
|
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
|
||||||
import suwayomi.tachidesk.manga.model.table.CategoryTable
|
import suwayomi.tachidesk.manga.model.table.CategoryTable
|
||||||
@@ -75,13 +75,28 @@ fun Query.applyOpdsMangaFilter(
|
|||||||
}
|
}
|
||||||
if (excludeField != "filter") {
|
if (excludeField != "filter") {
|
||||||
criteria.filter?.let { filterVal ->
|
criteria.filter?.let { filterVal ->
|
||||||
val unreadCountExpr = Case().When(ChapterTable.isRead eq false, intLiteral(1)).Else(intLiteral(0)).sum()
|
|
||||||
val downloadedCountExpr = Case().When(ChapterTable.isDownloaded eq true, intLiteral(1)).Else(intLiteral(0)).sum()
|
|
||||||
when (filterVal) {
|
when (filterVal) {
|
||||||
"unread" -> having { unreadCountExpr greater 0 }
|
"unread" -> {
|
||||||
"downloaded" -> having { downloadedCountExpr greater 0 }
|
andWhere {
|
||||||
"ongoing" -> andWhere { MangaTable.status eq MangaStatus.ONGOING.value }
|
MangaTable.id inSubQuery
|
||||||
"completed" -> andWhere { MangaTable.status eq MangaStatus.COMPLETED.value }
|
ChapterTable.select(ChapterTable.manga).where { ChapterTable.isRead eq false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"downloaded" -> {
|
||||||
|
andWhere {
|
||||||
|
MangaTable.id inSubQuery
|
||||||
|
ChapterTable.select(ChapterTable.manga).where { ChapterTable.isDownloaded eq true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"ongoing" -> {
|
||||||
|
andWhere { MangaTable.status eq MangaStatus.ONGOING.value }
|
||||||
|
}
|
||||||
|
|
||||||
|
"completed" -> {
|
||||||
|
andWhere { MangaTable.status eq MangaStatus.COMPLETED.value }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,11 +148,17 @@ object MangaRepository {
|
|||||||
val unreadCount = unreadCountExpr.alias("unread_count")
|
val unreadCount = unreadCountExpr.alias("unread_count")
|
||||||
|
|
||||||
// Base query with necessary joins for filtering and sorting
|
// Base query with necessary joins for filtering and sorting
|
||||||
val query =
|
var baseJoin =
|
||||||
MangaTable
|
MangaTable
|
||||||
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
||||||
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
|
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
|
||||||
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
|
||||||
|
if (criteria.categoryId != null) {
|
||||||
|
baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
val query =
|
||||||
|
baseJoin
|
||||||
.select(MangaTable.columns + SourceTable.lang + SourceTable.name + unreadCount)
|
.select(MangaTable.columns + SourceTable.lang + SourceTable.name + unreadCount)
|
||||||
.where { MangaTable.inLibrary eq true }
|
.where { MangaTable.inLibrary eq true }
|
||||||
|
|
||||||
@@ -210,7 +231,7 @@ object MangaRepository {
|
|||||||
pageNum: Int,
|
pageNum: Int,
|
||||||
sort: String,
|
sort: String,
|
||||||
): Pair<List<OpdsMangaAcqEntry>, Boolean> {
|
): Pair<List<OpdsMangaAcqEntry>, Boolean> {
|
||||||
val source = GetCatalogueSource.getCatalogueSourceOrStub(sourceId)
|
val source = GetSource.getSourceOrStub(sourceId)
|
||||||
val mangasPage: MangasPage =
|
val mangasPage: MangasPage =
|
||||||
if (sort == "latest" && source.supportsLatest) {
|
if (sort == "latest" && source.supportsLatest) {
|
||||||
source.getLatestUpdates(pageNum)
|
source.getLatestUpdates(pageNum)
|
||||||
@@ -301,7 +322,6 @@ object MangaRepository {
|
|||||||
* Applies sorting and filtering logic to a manga library query.
|
* Applies sorting and filtering logic to a manga library query.
|
||||||
* @param query The Exposed SQL query to modify.
|
* @param query The Exposed SQL query to modify.
|
||||||
* @param sort The sorting parameter.
|
* @param sort The sorting parameter.
|
||||||
* @param filter The filtering parameter.
|
|
||||||
*/
|
*/
|
||||||
private fun applyMangaLibrarySort(
|
private fun applyMangaLibrarySort(
|
||||||
query: Query,
|
query: Query,
|
||||||
@@ -330,36 +350,38 @@ object MangaRepository {
|
|||||||
*/
|
*/
|
||||||
fun getLibraryFilterCounts(activeFilters: OpdsMangaFilter): Map<String, Long> =
|
fun getLibraryFilterCounts(activeFilters: OpdsMangaFilter): Map<String, Long> =
|
||||||
transaction {
|
transaction {
|
||||||
val unreadCountExpr = Case().When(ChapterTable.isRead eq false, intLiteral(1)).Else(intLiteral(0)).sum()
|
var baseJoin =
|
||||||
val downloadedCountExpr = Case().When(ChapterTable.isDownloaded eq true, intLiteral(1)).Else(intLiteral(0)).sum()
|
MangaTable
|
||||||
|
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
||||||
|
|
||||||
|
if (activeFilters.categoryId != null) {
|
||||||
|
baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
||||||
|
}
|
||||||
|
|
||||||
val baseQuery =
|
val baseQuery =
|
||||||
MangaTable
|
baseJoin
|
||||||
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
|
|
||||||
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
|
||||||
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
|
||||||
.select(MangaTable.id)
|
.select(MangaTable.id)
|
||||||
.where { MangaTable.inLibrary eq true }
|
.where { MangaTable.inLibrary eq true }
|
||||||
|
.withDistinct()
|
||||||
|
|
||||||
baseQuery.applyOpdsMangaFilter(activeFilters, excludeField = "filter")
|
baseQuery.applyOpdsMangaFilter(activeFilters, excludeField = "filter")
|
||||||
baseQuery.groupBy(MangaTable.id)
|
|
||||||
|
|
||||||
val unreadCount = baseQuery.copy().having { unreadCountExpr greater 0 }.count()
|
val unreadCount =
|
||||||
val downloadedCount = baseQuery.copy().having { downloadedCountExpr greater 0 }.count()
|
baseQuery
|
||||||
|
.copy()
|
||||||
val statusBaseQuery =
|
.andWhere {
|
||||||
MangaTable
|
MangaTable.id inSubQuery
|
||||||
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
ChapterTable.select(ChapterTable.manga).where { ChapterTable.isRead eq false }
|
||||||
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
}.count()
|
||||||
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
|
val downloadedCount =
|
||||||
.select(MangaTable.id)
|
baseQuery
|
||||||
.where { MangaTable.inLibrary eq true }
|
.copy()
|
||||||
|
.andWhere {
|
||||||
statusBaseQuery.applyOpdsMangaFilter(activeFilters, excludeField = "filter")
|
MangaTable.id inSubQuery
|
||||||
statusBaseQuery.groupBy(MangaTable.id)
|
ChapterTable.select(ChapterTable.manga).where { ChapterTable.isDownloaded eq true }
|
||||||
|
}.count()
|
||||||
val ongoingCount = statusBaseQuery.copy().andWhere { MangaTable.status eq MangaStatus.ONGOING.value }.count()
|
val ongoingCount = baseQuery.copy().andWhere { MangaTable.status eq MangaStatus.ONGOING.value }.count()
|
||||||
val completedCount = statusBaseQuery.copy().andWhere { MangaTable.status eq MangaStatus.COMPLETED.value }.count()
|
val completedCount = baseQuery.copy().andWhere { MangaTable.status eq MangaStatus.COMPLETED.value }.count()
|
||||||
|
|
||||||
mapOf(
|
mapOf(
|
||||||
"unread" to unreadCount,
|
"unread" to unreadCount,
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import suwayomi.tachidesk.i18n.MR
|
|||||||
import suwayomi.tachidesk.manga.impl.extension.Extension
|
import suwayomi.tachidesk.manga.impl.extension.Extension
|
||||||
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
|
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
|
||||||
import suwayomi.tachidesk.manga.model.table.CategoryTable
|
import suwayomi.tachidesk.manga.model.table.CategoryTable
|
||||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaStatus
|
import suwayomi.tachidesk.manga.model.table.MangaStatus
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||||
@@ -166,12 +165,17 @@ object NavigationRepository {
|
|||||||
transaction {
|
transaction {
|
||||||
val mangaCount = MangaTable.id.countDistinct().alias("manga_count")
|
val mangaCount = MangaTable.id.countDistinct().alias("manga_count")
|
||||||
|
|
||||||
val query =
|
var baseJoin =
|
||||||
SourceTable
|
SourceTable
|
||||||
.join(MangaTable, JoinType.INNER, SourceTable.id, MangaTable.sourceReference)
|
.join(MangaTable, JoinType.INNER, SourceTable.id, MangaTable.sourceReference)
|
||||||
.join(ExtensionTable, JoinType.LEFT, onColumn = SourceTable.extension, otherColumn = ExtensionTable.id)
|
.join(ExtensionTable, JoinType.LEFT, onColumn = SourceTable.extension, otherColumn = ExtensionTable.id)
|
||||||
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
|
||||||
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
|
if (activeFilters.categoryId != null) {
|
||||||
|
baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
val query =
|
||||||
|
baseJoin
|
||||||
.select(SourceTable.id, SourceTable.name, SourceTable.lang, ExtensionTable.pkgName, mangaCount)
|
.select(SourceTable.id, SourceTable.name, SourceTable.lang, ExtensionTable.pkgName, mangaCount)
|
||||||
.where { MangaTable.inLibrary eq true }
|
.where { MangaTable.inLibrary eq true }
|
||||||
|
|
||||||
@@ -227,7 +231,6 @@ object NavigationRepository {
|
|||||||
.join(CategoryMangaTable, JoinType.INNER, CategoryTable.id, CategoryMangaTable.category)
|
.join(CategoryMangaTable, JoinType.INNER, CategoryTable.id, CategoryMangaTable.category)
|
||||||
.join(MangaTable, JoinType.INNER, CategoryMangaTable.manga, MangaTable.id)
|
.join(MangaTable, JoinType.INNER, CategoryMangaTable.manga, MangaTable.id)
|
||||||
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
||||||
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
|
|
||||||
.select(CategoryTable.id, CategoryTable.name, mangaCount)
|
.select(CategoryTable.id, CategoryTable.name, mangaCount)
|
||||||
.where { MangaTable.inLibrary eq true }
|
.where { MangaTable.inLibrary eq true }
|
||||||
|
|
||||||
@@ -262,11 +265,15 @@ object NavigationRepository {
|
|||||||
activeFilters: OpdsMangaFilter = OpdsMangaFilter(),
|
activeFilters: OpdsMangaFilter = OpdsMangaFilter(),
|
||||||
): Pair<List<OpdsGenreNavEntry>, Long> =
|
): Pair<List<OpdsGenreNavEntry>, Long> =
|
||||||
transaction {
|
transaction {
|
||||||
val query =
|
var baseJoin =
|
||||||
MangaTable
|
MangaTable
|
||||||
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
||||||
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
if (activeFilters.categoryId != null) {
|
||||||
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
|
baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
val query =
|
||||||
|
baseJoin
|
||||||
.select(MangaTable.genre)
|
.select(MangaTable.genre)
|
||||||
.where { MangaTable.inLibrary eq true }
|
.where { MangaTable.inLibrary eq true }
|
||||||
|
|
||||||
@@ -321,11 +328,16 @@ object NavigationRepository {
|
|||||||
val statusCounts =
|
val statusCounts =
|
||||||
transaction {
|
transaction {
|
||||||
val countExpr = MangaTable.id.countDistinct().alias("manga_count")
|
val countExpr = MangaTable.id.countDistinct().alias("manga_count")
|
||||||
val query =
|
|
||||||
|
var baseJoin =
|
||||||
MangaTable
|
MangaTable
|
||||||
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
||||||
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
if (activeFilters.categoryId != null) {
|
||||||
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
|
baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
val query =
|
||||||
|
baseJoin
|
||||||
.select(MangaTable.status, countExpr)
|
.select(MangaTable.status, countExpr)
|
||||||
.where { MangaTable.inLibrary eq true }
|
.where { MangaTable.inLibrary eq true }
|
||||||
|
|
||||||
@@ -368,11 +380,16 @@ object NavigationRepository {
|
|||||||
): Pair<List<OpdsLanguageNavEntry>, Long> =
|
): Pair<List<OpdsLanguageNavEntry>, Long> =
|
||||||
transaction {
|
transaction {
|
||||||
val mangaCount = MangaTable.id.countDistinct().alias("manga_count")
|
val mangaCount = MangaTable.id.countDistinct().alias("manga_count")
|
||||||
val query =
|
|
||||||
|
var baseJoin =
|
||||||
SourceTable
|
SourceTable
|
||||||
.join(MangaTable, JoinType.INNER, SourceTable.id, MangaTable.sourceReference)
|
.join(MangaTable, JoinType.INNER, SourceTable.id, MangaTable.sourceReference)
|
||||||
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
if (activeFilters.categoryId != null) {
|
||||||
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
|
baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
val query =
|
||||||
|
baseJoin
|
||||||
.select(SourceTable.lang, mangaCount)
|
.select(SourceTable.lang, mangaCount)
|
||||||
.where { MangaTable.inLibrary eq true }
|
.where { MangaTable.inLibrary eq true }
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ package suwayomi.tachidesk.server.database.migration
|
|||||||
|
|
||||||
import de.neonew.exposed.migrations.helpers.SQLMigration
|
import de.neonew.exposed.migrations.helpers.SQLMigration
|
||||||
import suwayomi.tachidesk.graphql.types.DatabaseType
|
import suwayomi.tachidesk.graphql.types.DatabaseType
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||||
import suwayomi.tachidesk.server.database.migration.helpers.MAYBE_TYPE_PREFIX
|
import suwayomi.tachidesk.server.database.migration.helpers.MAYBE_TYPE_PREFIX
|
||||||
import suwayomi.tachidesk.server.database.migration.helpers.UNLIMITED_TEXT
|
import suwayomi.tachidesk.server.database.migration.helpers.UNLIMITED_TEXT
|
||||||
import suwayomi.tachidesk.server.database.migration.helpers.toSqlName
|
import suwayomi.tachidesk.server.database.migration.helpers.toSqlName
|
||||||
@@ -34,6 +34,7 @@ class M0057_AddNewExtensionApiFields : SQLMigration() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ALTER TABLE EXTENSION ALTER COLUMN store_index_url ${MAYBE_TYPE_PREFIX}VARCHAR(2048);
|
ALTER TABLE EXTENSION ALTER COLUMN store_index_url ${MAYBE_TYPE_PREFIX}VARCHAR(2048);
|
||||||
|
CREATE INDEX extension_store_index_url ON EXTENSION (store_index_url);
|
||||||
ALTER TABLE EXTENSION ALTER COLUMN version_code ${MAYBE_TYPE_PREFIX}BIGINT;
|
ALTER TABLE EXTENSION ALTER COLUMN version_code ${MAYBE_TYPE_PREFIX}BIGINT;
|
||||||
ALTER TABLE EXTENSION ALTER COLUMN apk_name DROP NOT NULL;
|
ALTER TABLE EXTENSION ALTER COLUMN apk_name DROP NOT NULL;
|
||||||
${
|
${
|
||||||
@@ -43,12 +44,11 @@ class M0057_AddNewExtensionApiFields : SQLMigration() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ALTER TABLE EXTENSION ADD COLUMN apk_url VARCHAR(2048);
|
ALTER TABLE EXTENSION ADD COLUMN apk_url VARCHAR(2048);
|
||||||
ALTER TABLE EXTENSION ADD COLUMN content_rating INTEGER DEFAULT 0;
|
ALTER TABLE EXTENSION ADD COLUMN content_warning INTEGER DEFAULT 0;
|
||||||
UPDATE EXTENSION SET content_rating = ${ContentRating.PORNOGRAPHIC.ordinal} WHERE is_nsfw = TRUE;
|
UPDATE EXTENSION SET content_warning = ${ContentWarning.MIXED.ordinal} WHERE is_nsfw = TRUE;
|
||||||
ALTER TABLE EXTENSION DROP COLUMN is_nsfw;
|
ALTER TABLE EXTENSION DROP COLUMN is_nsfw;
|
||||||
ALTER TABLE SOURCE ADD COLUMN message $UNLIMITED_TEXT;
|
ALTER TABLE SOURCE ADD COLUMN content_warning INTEGER DEFAULT 0;
|
||||||
ALTER TABLE SOURCE ADD COLUMN content_rating INTEGER DEFAULT 0;
|
UPDATE SOURCE SET content_warning = ${ContentWarning.MIXED.ordinal} WHERE is_nsfw = TRUE;
|
||||||
UPDATE SOURCE SET content_rating = ${ContentRating.PORNOGRAPHIC.ordinal} WHERE is_nsfw = TRUE;
|
|
||||||
ALTER TABLE SOURCE DROP COLUMN is_nsfw;
|
ALTER TABLE SOURCE DROP COLUMN is_nsfw;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,13 +14,14 @@ import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
|||||||
@Suppress("ClassName", "unused")
|
@Suppress("ClassName", "unused")
|
||||||
class M0058_AddExtensionStore : AddTableMigration() {
|
class M0058_AddExtensionStore : AddTableMigration() {
|
||||||
private class ExtensionStoreTable : IntIdTable() {
|
private class ExtensionStoreTable : IntIdTable() {
|
||||||
val indexUrl = varchar("index_url", 2048)
|
val indexUrl = varchar("index_url", 2048).uniqueIndex()
|
||||||
val name = varchar("name", 256)
|
val name = varchar("name", 256)
|
||||||
val badgeLabel = varchar("badge_label", 32)
|
val badgeLabel = varchar("badge_label", 32)
|
||||||
val signingKey = varchar("signing_key", 512)
|
val signingKey = varchar("signing_key", 512)
|
||||||
val contactWebsite = varchar("contact_website", 2048)
|
val contactWebsite = varchar("contact_website", 2048)
|
||||||
val contactDiscord = varchar("contact_discord", 2048).nullable()
|
val contactDiscord = varchar("contact_discord", 2048).nullable()
|
||||||
val isLegacy = bool("is_legacy").default(false)
|
val isLegacy = bool("is_legacy").default(false)
|
||||||
|
val extensionListUrl = varchar("extension_list_url", 2048).nullable()
|
||||||
}
|
}
|
||||||
|
|
||||||
override val tables: Array<Table>
|
override val tables: Array<Table>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import org.koin.core.context.stopKoin
|
|||||||
import suwayomi.tachidesk.manga.impl.Source
|
import suwayomi.tachidesk.manga.impl.Source
|
||||||
import suwayomi.tachidesk.manga.impl.extension.Extension
|
import suwayomi.tachidesk.manga.impl.extension.Extension
|
||||||
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList
|
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
||||||
import suwayomi.tachidesk.server.applicationSetup
|
import suwayomi.tachidesk.server.applicationSetup
|
||||||
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
||||||
import suwayomi.tachidesk.test.BASE_PATH
|
import suwayomi.tachidesk.test.BASE_PATH
|
||||||
@@ -51,7 +51,7 @@ class CloudFlareTest {
|
|||||||
Source
|
Source
|
||||||
.getSourceList()
|
.getSourceList()
|
||||||
.firstNotNullOf { it.id.toLong().takeIf { it == 3122156392225024195L } }
|
.firstNotNullOf { it.id.toLong().takeIf { it == 3122156392225024195L } }
|
||||||
.let(GetCatalogueSource::getCatalogueSourceOrNull) as HttpSource
|
.let(GetSource::getSourceOrNull) as HttpSource
|
||||||
}
|
}
|
||||||
setLoggingEnabled(true)
|
setLoggingEnabled(true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import suwayomi.tachidesk.manga.impl.extension.Extension.installExtension
|
|||||||
import suwayomi.tachidesk.manga.impl.extension.Extension.uninstallExtension
|
import suwayomi.tachidesk.manga.impl.extension.Extension.uninstallExtension
|
||||||
import suwayomi.tachidesk.manga.impl.extension.Extension.updateExtension
|
import suwayomi.tachidesk.manga.impl.extension.Extension.updateExtension
|
||||||
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.getExtensionList
|
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.getExtensionList
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
|
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrNull
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
|
||||||
import suwayomi.tachidesk.server.applicationSetup
|
import suwayomi.tachidesk.server.applicationSetup
|
||||||
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
||||||
@@ -82,7 +82,7 @@ class TestExtensionCompatibility {
|
|||||||
.filter {
|
.filter {
|
||||||
// filter local source
|
// filter local source
|
||||||
it.id.toLong() != 0L
|
it.id.toLong() != 0L
|
||||||
}.map { getCatalogueSourceOrNull(it.id.toLong())!! as HttpSource }
|
}.map { getSourceOrNull(it.id.toLong())!! as HttpSource }
|
||||||
}
|
}
|
||||||
setLoggingEnabled(true)
|
setLoggingEnabled(true)
|
||||||
File("$BASE_PATH/sources.txt").writeText(sources.joinToString("\n") { "${it.name} - ${it.lang.uppercase()} - ${it.id}" })
|
File("$BASE_PATH/sources.txt").writeText(sources.joinToString("\n") { "${it.name} - ${it.lang.uppercase()} - ${it.id}" })
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ import suwayomi.tachidesk.manga.impl.Search.SerializableGroup
|
|||||||
import suwayomi.tachidesk.manga.impl.Search.getFilterList
|
import suwayomi.tachidesk.manga.impl.Search.getFilterList
|
||||||
import suwayomi.tachidesk.manga.impl.Search.setFilter
|
import suwayomi.tachidesk.manga.impl.Search.setFilter
|
||||||
import suwayomi.tachidesk.manga.impl.Search.sourceSearch
|
import suwayomi.tachidesk.manga.impl.Search.sourceSearch
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.registerCatalogueSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetSource.registerSource
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.unregisterCatalogueSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetSource.unregisterSource
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.StubSource
|
import suwayomi.tachidesk.manga.impl.util.source.StubSource
|
||||||
import suwayomi.tachidesk.test.ApplicationTest
|
import suwayomi.tachidesk.test.ApplicationTest
|
||||||
import suwayomi.tachidesk.test.createSMangas
|
import suwayomi.tachidesk.test.createSMangas
|
||||||
@@ -53,7 +53,7 @@ class SearchTest : ApplicationTest() {
|
|||||||
|
|
||||||
@BeforeAll
|
@BeforeAll
|
||||||
fun setup() {
|
fun setup() {
|
||||||
registerCatalogueSource(sourceId to source)
|
registerSource(sourceId to source)
|
||||||
|
|
||||||
this.source.mangas = createSMangas(mangasCount)
|
this.source.mangas = createSMangas(mangasCount)
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,7 @@ class SearchTest : ApplicationTest() {
|
|||||||
|
|
||||||
@AfterAll
|
@AfterAll
|
||||||
fun teardown() {
|
fun teardown() {
|
||||||
unregisterCatalogueSource(this.sourceId)
|
unregisterSource(this.sourceId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,7 +347,7 @@ class FilterListTest : ApplicationTest() {
|
|||||||
private fun registerSource(sourceClass: KClass<*>): EmptyFilterListSource =
|
private fun registerSource(sourceClass: KClass<*>): EmptyFilterListSource =
|
||||||
synchronized(sourceClass) {
|
synchronized(sourceClass) {
|
||||||
val source = sourceClass.primaryConstructor!!.call(sourceCount) as EmptyFilterListSource
|
val source = sourceClass.primaryConstructor!!.call(sourceCount) as EmptyFilterListSource
|
||||||
registerCatalogueSource(sourceCount to source)
|
registerSource(sourceCount to source)
|
||||||
sourceCount++
|
sourceCount++
|
||||||
source
|
source
|
||||||
}
|
}
|
||||||
@@ -355,7 +355,7 @@ class FilterListTest : ApplicationTest() {
|
|||||||
@AfterAll
|
@AfterAll
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun teardown() {
|
fun teardown() {
|
||||||
(0 until sourceCount).forEach { unregisterCatalogueSource(it) }
|
(0 until sourceCount).forEach { unregisterSource(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user