mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-13 15:54:35 -05:00
Compare commits
33 Commits
master
...
f748bc7f6d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f748bc7f6d | ||
|
|
11ff4bb280 | ||
|
|
388586f23b | ||
|
|
b8ffbebc76 | ||
|
|
3048df307d | ||
|
|
3b3770cb3d | ||
|
|
9667aeba18 | ||
|
|
6c8a024b0f | ||
|
|
5b2613dad3 | ||
|
|
396cfa734a | ||
|
|
a1fdf6d77a | ||
|
|
07ae17105b | ||
|
|
b8772f60bf | ||
|
|
733b9c9919 | ||
|
|
0b0c056bcb | ||
|
|
aff95bfc37 | ||
|
|
00bc3e39b6 | ||
|
|
e9c2cc49a6 | ||
|
|
ea310ba54b | ||
|
|
72347f45cc | ||
|
|
1e73e526c6 | ||
|
|
8fd0fdba08 | ||
|
|
4b61d375ed | ||
|
|
3cf4cf6cf8 | ||
|
|
74ade8a3a3 | ||
|
|
3bb2e4329e | ||
|
|
33ec15c136 | ||
|
|
3a78453a02 | ||
|
|
b7c259a4cb | ||
|
|
41ef220a0b | ||
|
|
85fe9802e2 | ||
|
|
676aed14c0 | ||
|
|
ceac5f74c4 |
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.3.2243"
|
Example: "v2.2.2100"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -143,13 +143,11 @@ 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 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
|
- label: I have updated to the **[latest version](https://github.com/suwayomi/suwayomi-server/releases/latest)**.
|
||||||
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 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
|
- label: I have updated to the **[latest version](https://github.com/suwayomi/suwayomi-server/releases/latest)**.
|
||||||
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/workflows/docker_build_stable.yml
vendored
2
.github/workflows/docker_build_stable.yml
vendored
@@ -11,5 +11,5 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: run docker build and publish script
|
- name: run docker build and publish script
|
||||||
run: |
|
run: |
|
||||||
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token ${{ secrets.DEPLOY_DOCKER_TOKEN }}" -d '{"ref":"main", "inputs":{"tachidesk_release_type": "stable"}}' https://api.github.com/repos/suwayomi/suwayomi-server-docker/actions/workflows/build_container_images.yml/dispatches
|
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token ${{ secrets.DEPLOY_PREVIEW_TOKEN }}" -d '{"ref":"main", "inputs":{"tachidesk_release_type": "stable"}}' https://api.github.com/repos/suwayomi/docker-tachidesk/actions/workflows/build_container_images.yml/dispatches
|
||||||
|
|
||||||
|
|||||||
37
.github/workflows/winget.yml
vendored
37
.github/workflows/winget.yml
vendored
@@ -1,52 +1,19 @@
|
|||||||
name: Publish to WinGet
|
name: Publish to WinGet
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
description: Version
|
description: Version
|
||||||
required: false
|
required: false
|
||||||
type: string
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
publish:
|
||||||
runs-on: windows-latest # action can only be run on windows
|
runs-on: windows-latest # action can only be run on windows
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Determine version
|
|
||||||
id: version
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
|
||||||
if [ -n "${{ inputs.version }}" ]; then
|
|
||||||
VERSION="${{ inputs.version }}"
|
|
||||||
elif [ "${{ github.event_name }}" = "release" ]; then
|
|
||||||
VERSION="${{ github.event.release.tag_name }}"
|
|
||||||
else
|
|
||||||
VERSION="$(gh release view --json tagName -q .tagName)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Sync winget-pkgs fork with upstream
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.WINGET_PUBLISH_PAT }}
|
|
||||||
run: |
|
|
||||||
gh api \
|
|
||||||
--method POST \
|
|
||||||
-H "Accept: application/vnd.github+json" \
|
|
||||||
"repos/Suwayomi/winget-pkgs/merge-upstream" \
|
|
||||||
-f branch=master
|
|
||||||
|
|
||||||
- uses: vedantmgoyal2009/winget-releaser@v2
|
- uses: vedantmgoyal2009/winget-releaser@v2
|
||||||
with:
|
with:
|
||||||
identifier: Suwayomi.Suwayomi-Server
|
identifier: Suwayomi.Suwayomi-Server
|
||||||
installers-regex: '.*x64.msi$'
|
installers-regex: '.*x64.msi$'
|
||||||
token: ${{ secrets.WINGET_PUBLISH_PAT }}
|
token: ${{ secrets.WINGET_PUBLISH_PAT }}
|
||||||
version: ${{ steps.version.outputs.version }}
|
version: ${{ inputs.version || github.ref_name }}
|
||||||
release-tag: ${{ steps.version.outputs.version }}
|
release-tag: ${{ inputs.version || github.ref_name }}
|
||||||
|
|||||||
63
CHANGELOG.md
63
CHANGELOG.md
@@ -6,65 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|||||||
|
|
||||||
## [Unreleased] (Preview)
|
## [Unreleased] (Preview)
|
||||||
|
|
||||||
### Added
|
|
||||||
- .
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- .
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- .
|
|
||||||
|
|
||||||
## [v2.3.2243] - 2026-07-13
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- (**Extension**) Add ability to install jar versions of extensions
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- (**Extension**) Fix missing memo field in getPageList
|
|
||||||
|
|
||||||
## [v2.3.2238] - 2026-07-05
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- (**Extension**) Support fully qualified extension class names
|
|
||||||
- (**Manga/API**) Return partial responses for Manga And Chapters query
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- (**Database/H2**) Use temporary file for H2 migration tools download
|
|
||||||
|
|
||||||
## [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
|
||||||
@@ -481,7 +426,6 @@ 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
|
||||||
@@ -508,12 +452,7 @@ 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.3.2243...HEAD
|
[unreleased]: https://github.com/suwayomi/suwayomi-server/compare/v2.2.2100...HEAD
|
||||||
[v2.3.2243]: https://github.com/suwayomi/suwayomi-server/compare/v2.3.2238...v2.3.2243
|
|
||||||
[v2.3.2238]: https://github.com/suwayomi/suwayomi-server/compare/v2.3.2232...v2.3.2238
|
|
||||||
[v2.3.2232]: https://github.com/suwayomi/suwayomi-server/compare/v2.3.2230...v2.3.2232
|
|
||||||
[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.3.${getCommitCount()}" }
|
val getTachideskVersion = { "v2.2.${getCommitCount()}" }
|
||||||
|
|
||||||
val webUIRevisionTag = "r3147"
|
val webUIRevisionTag = "r3136"
|
||||||
|
|
||||||
val webviewJbrRelease = "jbr-release-25.0.3b508.4"
|
val webviewJbrRelease = "jbr-release-25.0.3b508.4"
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ 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
|
||||||
@@ -110,7 +109,6 @@ dex2jar-tools = { module = "de.femtopedia.dex2jar:dex-tools", version.ref = "dex
|
|||||||
# APK
|
# APK
|
||||||
apk-parser = "net.dongliu:apk-parser:2.6.10"
|
apk-parser = "net.dongliu:apk-parser:2.6.10"
|
||||||
apksig = "com.android.tools.build:apksig:9.2.1"
|
apksig = "com.android.tools.build:apksig:9.2.1"
|
||||||
axml = "com.github.Aliucord:axml:ed26565eb0"
|
|
||||||
|
|
||||||
# Xml
|
# Xml
|
||||||
xmlpull = "xmlpull:xmlpull:1.1.3.4a"
|
xmlpull = "xmlpull:xmlpull:1.1.3.4a"
|
||||||
@@ -216,7 +214,6 @@ shared = [
|
|||||||
"dex2jar-translator",
|
"dex2jar-translator",
|
||||||
"dex2jar-tools",
|
"dex2jar-tools",
|
||||||
"apk-parser",
|
"apk-parser",
|
||||||
"axml",
|
|
||||||
"jackson-annotations",
|
"jackson-annotations",
|
||||||
"jcef",
|
"jcef",
|
||||||
]
|
]
|
||||||
@@ -231,7 +228,6 @@ 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)" || error $LINENO "No JAR found in server/build/" 1
|
JAR="$(ls server/build/*.jar | tail -n1)"
|
||||||
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/" || error $LINENO "Failed to create release directory" 1
|
mkdir "$RELEASE_NAME/"
|
||||||
|
|
||||||
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 || error $LINENO "Failed to compile catch_abort" 1
|
gcc -fPIC -shared scripts/resources/catch_abort.c -lpthread -o scripts/resources/catch_abort.so
|
||||||
fi
|
fi
|
||||||
|
|
||||||
JRE_ZULU="25.34.17_25.0.3"
|
JRE_ZULU="25.34.17_25.0.3"
|
||||||
@@ -49,6 +49,7 @@ 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
|
||||||
;;
|
;;
|
||||||
@@ -56,6 +57,7 @@ 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"
|
||||||
@@ -65,7 +67,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/" || error $LINENO "Failed to create tar.gz" 1
|
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/"
|
||||||
move_release_to_output_dir
|
move_release_to_output_dir
|
||||||
;;
|
;;
|
||||||
linux-x64)
|
linux-x64)
|
||||||
@@ -75,6 +77,7 @@ 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"
|
||||||
|
|
||||||
@@ -89,6 +92,7 @@ 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"
|
||||||
|
|
||||||
@@ -103,6 +107,7 @@ 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"
|
||||||
|
|
||||||
@@ -117,6 +122,7 @@ 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"
|
||||||
|
|
||||||
@@ -139,50 +145,52 @@ 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/" || error $LINENO "Failed to move release to output dir" 1
|
mv "$RELEASE" "$OUTPUT_DIR/"
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
if [ -z "$LAUNCHER_URL" ]; then
|
curl -fL "$LAUNCHER_URL" -o "Suwayomi-Launcher.jar"
|
||||||
error $LINENO "Failed to determine launcher URL from GitHub API" 1
|
mv "Suwayomi-Launcher.jar" "$RELEASE_NAME/Suwayomi-Launcher.jar"
|
||||||
|
}
|
||||||
|
|
||||||
|
download_jogamp() {
|
||||||
|
local platform="$1"
|
||||||
|
if [ ! -f jogamp-all-platforms.7z ]; then
|
||||||
|
curl -f "https://jogamp.org/deployment/jogamp-current/archive/jogamp-all-platforms.7z" -o jogamp-all-platforms.7z
|
||||||
fi
|
fi
|
||||||
curl -fL "$LAUNCHER_URL" -o "Suwayomi-Launcher.jar" || error $LINENO "Failed to download launcher JAR" 1
|
|
||||||
mv "Suwayomi-Launcher.jar" "$RELEASE_NAME/Suwayomi-Launcher.jar" || error $LINENO "Failed to move launcher JAR" 1
|
7z x jogamp-all-platforms.7z "jogamp-all-platforms/lib/$platform/"
|
||||||
|
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" || error $LINENO "Failed to download electron" 1
|
curl -fL "$ELECTRON_URL" -o "$ELECTRON"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
unzip "$ELECTRON" -d "$RELEASE_NAME/electron/" || error $LINENO "Failed to extract electron" 1
|
unzip "$ELECTRON" -d "$RELEASE_NAME/electron/"
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_jre() {
|
setup_jre() {
|
||||||
if [ -d "jre" ]; then
|
if [ -d "jre" ]; then
|
||||||
if [ ! -f "./jre/bin/java.exe" ]; then
|
chmod +x ./jre/bin/java
|
||||||
chmod +x ./jre/bin/java || error $LINENO "Failed to set java executable permission" 1
|
chmod +x ./jre/lib/jspawnhelper
|
||||||
chmod +x ./jre/lib/jspawnhelper || error $LINENO "Failed to set jspawnhelper executable permission" 1
|
mv "jre" "$RELEASE_NAME/jre"
|
||||||
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" || error $LINENO "Failed to download JRE" 1
|
curl -fL "$JRE_URL" -o "$JRE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local ext="${JRE##*.}"
|
local ext="${JRE##*.}"
|
||||||
if [ "$ext" = "zip" ]; then
|
if [ "$ext" = "zip" ]; then
|
||||||
unzip "$JRE" || error $LINENO "Failed to extract JRE zip" 1
|
unzip "$JRE"
|
||||||
else
|
else
|
||||||
tar xvf "$JRE" || error $LINENO "Failed to extract JRE tar" 1
|
tar xvf "$JRE"
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,31 +198,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/" || error $LINENO "Failed to copy server script" 1
|
cp "scripts/resources/pkg/suwayomi-server.sh" "$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-server.desktop" "$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.sh" "$output_dir/"
|
||||||
cp "scripts/resources/pkg/suwayomi-launcher.desktop" "$output_dir/" || error $LINENO "Failed to copy launcher desktop file" 1
|
cp "scripts/resources/pkg/suwayomi-launcher.desktop" "$output_dir/"
|
||||||
cp "scripts/resources/pkg/systemd"/* "$output_dir/" || error $LINENO "Failed to copy systemd files" 1
|
cp "scripts/resources/pkg/systemd"/* "$output_dir/"
|
||||||
cp "server/src/main/resources/icon/faviconlogo-128.png" \
|
cp "server/src/main/resources/icon/faviconlogo-128.png" \
|
||||||
"$output_dir/suwayomi-server.png" || error $LINENO "Failed to copy icon" 1
|
"$output_dir/suwayomi-server.png"
|
||||||
}
|
}
|
||||||
|
|
||||||
make_linux_bundle() {
|
make_linux_bundle() {
|
||||||
mkdir "$RELEASE_NAME/bin"
|
mkdir "$RELEASE_NAME/bin"
|
||||||
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar" || error $LINENO "Failed to copy server jar" 1
|
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar"
|
||||||
cp "scripts/resources/suwayomi-launcher.sh" "$RELEASE_NAME/" || error $LINENO "Failed to copy launcher script" 1
|
cp "scripts/resources/suwayomi-launcher.sh" "$RELEASE_NAME/"
|
||||||
cp "scripts/resources/suwayomi-server.sh" "$RELEASE_NAME/" || error $LINENO "Failed to copy server script" 1
|
cp "scripts/resources/suwayomi-server.sh" "$RELEASE_NAME/"
|
||||||
cp "scripts/resources/catch_abort.so" "$RELEASE_NAME/bin/" || error $LINENO "Failed to copy catch_abort" 1
|
cp "scripts/resources/catch_abort.so" "$RELEASE_NAME/bin/"
|
||||||
|
|
||||||
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/" || error $LINENO "Failed to create tar.gz" 1
|
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/"
|
||||||
}
|
}
|
||||||
|
|
||||||
make_macos_bundle() {
|
make_macos_bundle() {
|
||||||
mkdir "$RELEASE_NAME/bin"
|
mkdir "$RELEASE_NAME/bin"
|
||||||
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar" || error $LINENO "Failed to copy server jar" 1
|
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar"
|
||||||
cp "scripts/resources/Suwayomi Launcher.command" "$RELEASE_NAME/" || error $LINENO "Failed to copy launcher command" 1
|
cp "scripts/resources/Suwayomi Launcher.command" "$RELEASE_NAME/"
|
||||||
|
|
||||||
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/" || error $LINENO "Failed to create tar.gz" 1
|
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/"
|
||||||
}
|
}
|
||||||
|
|
||||||
# https://wiki.debian.org/SimplePackagingTutorial
|
# https://wiki.debian.org/SimplePackagingTutorial
|
||||||
@@ -225,27 +233,28 @@ 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/" || error $LINENO "Failed to create source directory" 1
|
mkdir "$RELEASE_NAME/$source_dir/"
|
||||||
mv "$RELEASE_NAME/Suwayomi-Launcher.jar" "$RELEASE_NAME/$source_dir/Suwayomi-Launcher.jar" || error $LINENO "Failed to move launcher jar" 1
|
mv "$RELEASE_NAME/natives" "$RELEASE_NAME/$source_dir/natives"
|
||||||
cp "$JAR" "$RELEASE_NAME/$source_dir/Suwayomi-Server.jar" || error $LINENO "Failed to copy server jar" 1
|
mv "$RELEASE_NAME/Suwayomi-Launcher.jar" "$RELEASE_NAME/$source_dir/Suwayomi-Launcher.jar"
|
||||||
|
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/" || error $LINENO "Failed to copy catch_abort" 1
|
cp "scripts/resources/catch_abort.so" "$RELEASE_NAME/$source_dir/"
|
||||||
tar -I "gzip" -C "$RELEASE_NAME/" -cvf "$upstream_source" "$source_dir" || error $LINENO "Failed to create source tar.gz" 1
|
tar -I "gzip" -C "$RELEASE_NAME/" -cvf "$upstream_source" "$source_dir"
|
||||||
|
|
||||||
cp -r "scripts/resources/deb/" "$RELEASE_NAME/$source_dir/debian/" || error $LINENO "Failed to copy debian resources" 1
|
cp -r "scripts/resources/deb/" "$RELEASE_NAME/$source_dir/debian/"
|
||||||
sed -i "s/\$pkgver/$RELEASE_VERSION/" "$RELEASE_NAME/$source_dir/debian/changelog" || error $LINENO "Failed to update changelog" 1
|
sed -i "s/\$pkgver/$RELEASE_VERSION/" "$RELEASE_NAME/$source_dir/debian/changelog"
|
||||||
sed -i "s/\$pkgrel/1/" "$RELEASE_NAME/$source_dir/debian/changelog" || error $LINENO "Failed to update changelog" 1
|
sed -i "s/\$pkgrel/1/" "$RELEASE_NAME/$source_dir/debian/changelog"
|
||||||
|
|
||||||
if [ "${CI:-}" = true ]; then
|
if [ "${CI:-}" = true ]; then
|
||||||
sudo apt update || error $LINENO "Failed to update apt" 1
|
sudo apt update
|
||||||
sudo apt install devscripts build-essential dh-exec || error $LINENO "Failed to install build deps" 1
|
sudo apt install devscripts build-essential dh-exec
|
||||||
fi
|
fi
|
||||||
cd "$RELEASE_NAME/$source_dir/" || error $LINENO "Failed to change directory" 1
|
cd "$RELEASE_NAME/$source_dir/"
|
||||||
dpkg-buildpackage --no-sign --build=all || error $LINENO "Debian package build failed" 1
|
dpkg-buildpackage --no-sign --build=all
|
||||||
cd - || error $LINENO "Failed to return to previous directory" 1
|
cd -
|
||||||
|
|
||||||
local deb="suwayomi-server_$RELEASE_VERSION-1_all.deb"
|
local deb="suwayomi-server_$RELEASE_VERSION-1_all.deb"
|
||||||
mv "$RELEASE_NAME/$deb" "$RELEASE" || error $LINENO "Failed to move resulting .deb" 1
|
mv "$RELEASE_NAME/$deb" "$RELEASE"
|
||||||
}
|
}
|
||||||
|
|
||||||
# https://linuxconfig.org/building-a-hello-world-appimage-on-linux
|
# https://linuxconfig.org/building-a-hello-world-appimage-on-linux
|
||||||
@@ -253,20 +262,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" || error $LINENO "Failed to copy server jar" 1
|
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar"
|
||||||
|
|
||||||
cp "scripts/resources/pkg/suwayomi-server.desktop" "$RELEASE_NAME/suwayomi-server.desktop" || error $LINENO "Failed to copy desktop file" 1
|
cp "scripts/resources/pkg/suwayomi-server.desktop" "$RELEASE_NAME/suwayomi-server.desktop"
|
||||||
cp "server/src/main/resources/icon/faviconlogo.png" "$RELEASE_NAME/suwayomi-server.png" || error $LINENO "Failed to copy icon" 1
|
cp "server/src/main/resources/icon/faviconlogo.png" "$RELEASE_NAME/suwayomi-server.png"
|
||||||
cp "scripts/resources/appimage/AppRun" "$RELEASE_NAME/AppRun" || error $LINENO "Failed to copy AppRun" 1
|
cp "scripts/resources/appimage/AppRun" "$RELEASE_NAME/AppRun"
|
||||||
chmod +x "$RELEASE_NAME/AppRun" || error $LINENO "Failed to make AppRun executable" 1
|
chmod +x "$RELEASE_NAME/AppRun"
|
||||||
|
|
||||||
if [ "${CI:-}" = true ]; then
|
if [ "${CI:-}" = true ]; then
|
||||||
sudo apt update || error $LINENO "Failed to update apt" 1
|
sudo apt update
|
||||||
sudo apt install libfuse2 || error $LINENO "Failed to install libfuse2" 1
|
sudo apt install libfuse2
|
||||||
fi
|
fi
|
||||||
curl -fL $APPIMAGE_URL -o $APPIMAGE_TOOLNAME || error $LINENO "Failed to download appimagetool" 1
|
curl -fL $APPIMAGE_URL -o $APPIMAGE_TOOLNAME
|
||||||
chmod +x $APPIMAGE_TOOLNAME || error $LINENO "Failed to make appimagetool executable" 1
|
chmod +x $APPIMAGE_TOOLNAME
|
||||||
ARCH=x86_64 ./$APPIMAGE_TOOLNAME "$RELEASE_NAME" "$RELEASE" || error $LINENO "AppImage creation failed" 1
|
ARCH=x86_64 ./$APPIMAGE_TOOLNAME "$RELEASE_NAME" "$RELEASE"
|
||||||
}
|
}
|
||||||
|
|
||||||
make_windows_bundle() {
|
make_windows_bundle() {
|
||||||
@@ -291,7 +300,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" || error $LINENO "Failed to download rcedit" 1
|
#curl -fL "$rcedit_url" -o "$rcedit"
|
||||||
#fi
|
#fi
|
||||||
|
|
||||||
#local icon="server/src/main/resources/icon/faviconlogo.ico"
|
#local icon="server/src/main/resources/icon/faviconlogo.ico"
|
||||||
@@ -299,35 +308,38 @@ 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" || error $LINENO "Failed to copy server jar" 1
|
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar"
|
||||||
cp "scripts/resources/Suwayomi Launcher.bat" "$RELEASE_NAME" || error $LINENO "Failed to copy launcher bat" 1
|
cp "scripts/resources/Suwayomi Launcher.bat" "$RELEASE_NAME"
|
||||||
|
|
||||||
zip -9 -r "$RELEASE" "$RELEASE_NAME" || error $LINENO "Failed to create windows bundle zip" 1
|
zip -9 -r "$RELEASE" "$RELEASE_NAME"
|
||||||
}
|
}
|
||||||
|
|
||||||
make_windows_package() {
|
make_windows_package() {
|
||||||
if [ "${CI:-}" = true ]; then
|
if [ "${CI:-}" = true ]; then
|
||||||
sudo apt update || error $LINENO "Failed to update apt" 1
|
sudo apt update
|
||||||
sudo apt install -y wixl || error $LINENO "Failed to install wixl" 1
|
sudo apt install -y wixl
|
||||||
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" || error $LINENO "Failed to heat jre" 1
|
--directory-ref jre --component-group jre >"$RELEASE_NAME/jre.wxs"
|
||||||
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" || error $LINENO "Failed to heat electron" 1
|
--directory-ref electron --component-group electron >"$RELEASE_NAME/electron.wxs"
|
||||||
|
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" || error $LINENO "Failed to heat bin" 1
|
--directory-ref bin --component-group bin >"$RELEASE_NAME/bin.wxs"
|
||||||
|
|
||||||
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/bin.wxs" -o "$RELEASE" || error $LINENO "Windows package build failed" 1
|
"$RELEASE_NAME/jre.wxs" "$RELEASE_NAME/electron.wxs" "$RELEASE_NAME/natives.wxs" "$RELEASE_NAME/bin.wxs" -o "$RELEASE"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Error handler
|
# Error handler
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
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,6 +17,7 @@
|
|||||||
<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>
|
||||||
@@ -62,6 +63,7 @@
|
|||||||
<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)" />
|
||||||
|
|||||||
@@ -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">Genren</string>
|
<string name="opds_feeds_genres_title">Genres</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,7 +122,4 @@
|
|||||||
<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>
|
||||||
|
|||||||
@@ -122,6 +122,4 @@
|
|||||||
<string name="login_label_login">Σύνδεση</string>
|
<string name="login_label_login">Σύνδεση</string>
|
||||||
<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="opds_error_chapters_not_found">Δεν βρέθηκαν κεφάλαια ή η πηγή είναι μη διαθέσιμη στη σελίδα %1$d.</string>
|
|
||||||
<string name="opds_chapter_title_fallback">Κεφάλαιο %1$s</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -122,6 +122,4 @@
|
|||||||
<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,7 +122,4 @@
|
|||||||
<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,6 +122,4 @@
|
|||||||
<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,7 +60,4 @@
|
|||||||
<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,6 +76,4 @@
|
|||||||
<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,6 +122,4 @@
|
|||||||
<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,7 +122,4 @@
|
|||||||
<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,7 +53,4 @@
|
|||||||
<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,6 +122,4 @@
|
|||||||
<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,7 +122,4 @@
|
|||||||
<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>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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
|
||||||
@@ -21,8 +22,9 @@ 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.GetSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||||
import java.net.CookieHandler
|
import java.net.CookieHandler
|
||||||
import java.net.CookieManager
|
import java.net.CookieManager
|
||||||
import java.net.CookiePolicy
|
import java.net.CookiePolicy
|
||||||
@@ -62,7 +64,7 @@ class NetworkHelper(
|
|||||||
userAgent
|
userAgent
|
||||||
.drop(1)
|
.drop(1)
|
||||||
.onEach {
|
.onEach {
|
||||||
GetSource.unregisterAllSources() // need to reset the headers
|
GetCatalogueSource.unregisterAllCatalogueSources() // need to reset the headers
|
||||||
}.launchIn(GlobalScope)
|
}.launchIn(GlobalScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +84,8 @@ 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 =
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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.GetSource.registerSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.registerCatalogueSource
|
||||||
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[contentWarning] = 0
|
it[contentRating] = 0
|
||||||
it[isInstalled] = true
|
it[isInstalled] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,7 +497,7 @@ class LocalSource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val fs = LocalSourceFileSystem(applicationDirs)
|
val fs = LocalSourceFileSystem(applicationDirs)
|
||||||
registerSource(ID to LocalSource(fs, LocalCoverManager(fs)))
|
registerCatalogueSource(ID to LocalSource(fs, LocalCoverManager(fs)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -305,7 +305,8 @@ 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 = "${manga.source}|${manga.url}"
|
fun mangaCompositeKey(manga: BackupManga): String =
|
||||||
|
"${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) }
|
||||||
@@ -414,7 +415,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
|
fun chapterCompositeKey(chapter: BackupChapter): String = "${chapter.url}|${chapter.name}|${chapter.chapterNumber}"
|
||||||
|
|
||||||
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,6 +21,26 @@ 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)
|
||||||
@@ -30,14 +50,14 @@ class ExtensionStoreDataLoader : 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 ExtensionsForExtensionStore : KotlinDataLoader<String, ExtensionNodeList> {
|
class ExtensionForExtensionStore : KotlinDataLoader<String, ExtensionNodeList> {
|
||||||
override val dataLoaderName = "ExtensionsForExtensionStore"
|
override val dataLoaderName = "ExtensionForExtensionStore"
|
||||||
|
|
||||||
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 ->
|
||||||
|
|||||||
@@ -179,10 +179,10 @@ class ExtensionMutation {
|
|||||||
val (clientMutationId, extensionFile) = input
|
val (clientMutationId, extensionFile) = input
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
val pkgName = Extension.installExternalExtension(extensionFile.content(), extensionFile.filename())
|
Extension.installExternalExtension(extensionFile.content(), extensionFile.filename())
|
||||||
|
|
||||||
val dbExtension =
|
val dbExtension =
|
||||||
transaction { ExtensionTable.selectAll().where { ExtensionTable.pkgName eq pkgName }.first() }
|
transaction { ExtensionTable.selectAll().where { ExtensionTable.apkName eq extensionFile.filename() }.first() }
|
||||||
|
|
||||||
InstallExternalExtensionPayload(
|
InstallExternalExtensionPayload(
|
||||||
clientMutationId,
|
clientMutationId,
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ class ExtensionStoreMutation {
|
|||||||
contactDiscord = it.contactDiscord,
|
contactDiscord = it.contactDiscord,
|
||||||
indexUrl = it.indexUrl,
|
indexUrl = it.indexUrl,
|
||||||
isLegacy = it.isLegacy,
|
isLegacy = it.isLegacy,
|
||||||
extensionListUrl = it.extensionListUrl,
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
package suwayomi.tachidesk.graphql.mutations
|
package suwayomi.tachidesk.graphql.mutations
|
||||||
|
|
||||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||||
import com.expediagroup.graphql.server.extensions.toGraphQLError
|
|
||||||
import graphql.execution.DataFetcherResult
|
|
||||||
import org.jetbrains.exposed.v1.core.LikePattern
|
import org.jetbrains.exposed.v1.core.LikePattern
|
||||||
import org.jetbrains.exposed.v1.core.Op
|
import org.jetbrains.exposed.v1.core.Op
|
||||||
import org.jetbrains.exposed.v1.core.and
|
import org.jetbrains.exposed.v1.core.and
|
||||||
@@ -183,21 +181,15 @@ class MangaMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun fetchMangaAndChapters(input: FetchMangaAndChaptersInput): CompletableFuture<DataFetcherResult<FetchMangaAndChaptersPayload?>> {
|
fun fetchMangaAndChapters(input: FetchMangaAndChaptersInput): CompletableFuture<FetchMangaAndChaptersPayload?> {
|
||||||
val (clientMutationId, id, fetchManga, fetchChapters) = input
|
val (clientMutationId, id, fetchManga, fetchChapters) = input
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
val error =
|
|
||||||
try {
|
|
||||||
Manga.updateMangaAndChapters(
|
Manga.updateMangaAndChapters(
|
||||||
mangaId = id,
|
mangaId = id,
|
||||||
updateManga = fetchManga,
|
updateManga = fetchManga,
|
||||||
updateChapters = fetchChapters,
|
updateChapters = fetchChapters,
|
||||||
)
|
)
|
||||||
null
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e
|
|
||||||
}
|
|
||||||
|
|
||||||
val (manga, chapters) =
|
val (manga, chapters) =
|
||||||
transaction {
|
transaction {
|
||||||
@@ -210,20 +202,11 @@ class MangaMutation {
|
|||||||
.map { ChapterType(it) },
|
.map { ChapterType(it) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
DataFetcherResult
|
|
||||||
.newResult<FetchMangaAndChaptersPayload>()
|
|
||||||
.data(
|
|
||||||
FetchMangaAndChaptersPayload(
|
FetchMangaAndChaptersPayload(
|
||||||
clientMutationId = clientMutationId,
|
clientMutationId = clientMutationId,
|
||||||
manga = MangaType(manga),
|
manga = MangaType(manga),
|
||||||
chapters = chapters,
|
chapters = chapters,
|
||||||
),
|
)
|
||||||
).also {
|
|
||||||
if (error != null) {
|
|
||||||
it.error(error.toGraphQLError())
|
|
||||||
}
|
|
||||||
}.build() as DataFetcherResult<FetchMangaAndChaptersPayload?>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.GetSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||||
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 = GetSource.getSourceOrNull(sourceId)!!
|
val source = GetCatalogueSource.getCatalogueSourceOrNull(sourceId)!!
|
||||||
val mangasPage =
|
val mangasPage =
|
||||||
when (type) {
|
when (type) {
|
||||||
FetchSourceMangaType.SEARCH -> {
|
FetchSourceMangaType.SEARCH -> {
|
||||||
|
|||||||
@@ -148,36 +148,6 @@ 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.ContentWarningFilter
|
import suwayomi.tachidesk.graphql.queries.filter.ContentRatingFilter
|
||||||
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.ContentWarning
|
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
@@ -103,15 +103,14 @@ class ExtensionQuery {
|
|||||||
val name: String? = null,
|
val name: String? = null,
|
||||||
val pkgName: String? = null,
|
val pkgName: String? = null,
|
||||||
val apkUrl: String? = null,
|
val apkUrl: String? = null,
|
||||||
val jarUrl: String? = null,
|
|
||||||
val extensionLib: String? = null,
|
val extensionLib: String? = null,
|
||||||
val versionName: String? = null,
|
val versionName: String? = null,
|
||||||
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("contentWarning"))
|
@GraphQLDeprecated("", ReplaceWith("contentRating"))
|
||||||
val isNsfw: Boolean? = null,
|
val isNsfw: Boolean? = null,
|
||||||
val contentWarning: ContentWarning? = null,
|
val contentRating: ContentRating? = 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,
|
||||||
@@ -123,7 +122,6 @@ class ExtensionQuery {
|
|||||||
opAnd.eq(apkName, ExtensionTable.apkName)
|
opAnd.eq(apkName, ExtensionTable.apkName)
|
||||||
opAnd.eq(iconUrl, ExtensionTable.iconUrl)
|
opAnd.eq(iconUrl, ExtensionTable.iconUrl)
|
||||||
opAnd.eq(apkUrl, ExtensionTable.apkUrl)
|
opAnd.eq(apkUrl, ExtensionTable.apkUrl)
|
||||||
opAnd.eq(jarUrl, ExtensionTable.jarUrl)
|
|
||||||
opAnd.eq(name, ExtensionTable.name)
|
opAnd.eq(name, ExtensionTable.name)
|
||||||
opAnd.eq(extensionLib, ExtensionTable.extensionLib)
|
opAnd.eq(extensionLib, ExtensionTable.extensionLib)
|
||||||
opAnd.eq(versionName, ExtensionTable.versionName)
|
opAnd.eq(versionName, ExtensionTable.versionName)
|
||||||
@@ -131,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) ContentWarning.MIXED.ordinal else ContentWarning.SAFE.ordinal },
|
isNsfw?.let { if (it) ContentRating.PORNOGRAPHIC.ordinal else ContentRating.SAFE.ordinal },
|
||||||
ExtensionTable.contentWarning,
|
ExtensionTable.contentRating,
|
||||||
)
|
)
|
||||||
opAnd.eq(contentWarning?.ordinal, ExtensionTable.contentWarning)
|
opAnd.eq(contentRating?.ordinal, ExtensionTable.contentRating)
|
||||||
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)
|
||||||
@@ -152,16 +150,15 @@ class ExtensionQuery {
|
|||||||
val name: StringFilter? = null,
|
val name: StringFilter? = null,
|
||||||
val pkgName: StringFilter? = null,
|
val pkgName: StringFilter? = null,
|
||||||
val apkUrl: StringFilter? = null,
|
val apkUrl: StringFilter? = null,
|
||||||
val jarUrl: StringFilter? = null,
|
|
||||||
val versionName: StringFilter? = null,
|
val versionName: StringFilter? = null,
|
||||||
val extensionLib: StringFilter? = null,
|
val extensionLib: StringFilter? = null,
|
||||||
@GraphQLDeprecated("", ReplaceWith("versionCodeLong"))
|
@GraphQLDeprecated("", ReplaceWith("versionCodeLong"))
|
||||||
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("contentWarning"))
|
@GraphQLDeprecated("", ReplaceWith("storeIndexUrl"))
|
||||||
val isNsfw: BooleanFilter? = null,
|
val isNsfw: BooleanFilter? = null,
|
||||||
val contentWarning: ContentWarningFilter? = null,
|
val contentRating: ContentRatingFilter? = 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,
|
||||||
@@ -178,12 +175,11 @@ class ExtensionQuery {
|
|||||||
andFilterWithCompareString(ExtensionTable.name, name),
|
andFilterWithCompareString(ExtensionTable.name, name),
|
||||||
andFilterWithCompareString(ExtensionTable.pkgName, pkgName),
|
andFilterWithCompareString(ExtensionTable.pkgName, pkgName),
|
||||||
andFilterWithCompareString(ExtensionTable.apkUrl, apkUrl),
|
andFilterWithCompareString(ExtensionTable.apkUrl, apkUrl),
|
||||||
andFilterWithCompareString(ExtensionTable.jarUrl, jarUrl),
|
|
||||||
andFilterWithCompareString(ExtensionTable.extensionLib, extensionLib),
|
andFilterWithCompareString(ExtensionTable.extensionLib, extensionLib),
|
||||||
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.contentWarning, contentWarning),
|
andFilterWithCompareEnum(ExtensionTable.contentRating, contentRating),
|
||||||
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.ContentWarningFilter
|
import suwayomi.tachidesk.graphql.queries.filter.ContentRatingFilter
|
||||||
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.ContentWarning
|
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
||||||
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 contentWarning == ContentRating.MIXED", ReplaceWith("contentWarning"))
|
@GraphQLDeprecated("replace with contentRating == ContentRating.PORNOGRAPHIC", ReplaceWith("contentRating"))
|
||||||
val isNsfw: Boolean? = null,
|
val isNsfw: Boolean? = null,
|
||||||
val contentWarning: ContentWarning? = null,
|
val contentRating: ContentRating? = null,
|
||||||
) : HasGetOp {
|
) : HasGetOp {
|
||||||
override fun getOp(): Op<Boolean>? {
|
override fun getOp(): Op<Boolean>? {
|
||||||
val opAnd = OpAnd()
|
val opAnd = OpAnd()
|
||||||
@@ -106,12 +106,13 @@ class SourceQuery {
|
|||||||
opAnd.eq(lang, SourceTable.lang)
|
opAnd.eq(lang, SourceTable.lang)
|
||||||
opAnd.andWhere(isNsfw) {
|
opAnd.andWhere(isNsfw) {
|
||||||
if (it) {
|
if (it) {
|
||||||
SourceTable.contentWarning greaterEq ContentWarning.MIXED.ordinal
|
SourceTable.contentRating eq ContentRating.PORNOGRAPHIC.ordinal
|
||||||
} else {
|
} else {
|
||||||
SourceTable.contentWarning less ContentWarning.MIXED.ordinal
|
SourceTable.contentRating neq
|
||||||
|
ContentRating.PORNOGRAPHIC.ordinal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
opAnd.andWhere(contentWarning) { SourceTable.contentWarning eq it.ordinal }
|
opAnd.andWhere(contentRating) { SourceTable.contentRating eq it.ordinal }
|
||||||
|
|
||||||
return opAnd.op
|
return opAnd.op
|
||||||
}
|
}
|
||||||
@@ -121,9 +122,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 contentWarning", ReplaceWith("contentWarning"))
|
@GraphQLDeprecated("replace with contentRating == ContentRating.PORNOGRAPHIC", ReplaceWith("contentRating"))
|
||||||
val isNsfw: BooleanFilter? = null,
|
val isNsfw: BooleanFilter? = null,
|
||||||
val contentWarning: ContentWarningFilter? = null,
|
val contentRating: ContentRatingFilter? = 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,
|
||||||
@@ -133,7 +134,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.contentWarning, contentWarning),
|
andFilterWithCompareEnum(SourceTable.contentRating, contentRating),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.ContentWarning
|
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
||||||
|
|
||||||
class ILikeEscapeOp(
|
class ILikeEscapeOp(
|
||||||
expr1: Expression<*>,
|
expr1: Expression<*>,
|
||||||
@@ -330,23 +330,23 @@ data class DoubleFilter(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ContentWarningFilter(
|
data class ContentRatingFilter(
|
||||||
override val isNull: Boolean? = null,
|
override val isNull: Boolean? = null,
|
||||||
override val equalTo: ContentWarning? = null,
|
override val equalTo: ContentRating? = null,
|
||||||
override val notEqualTo: ContentWarning? = null,
|
override val notEqualTo: ContentRating? = null,
|
||||||
override val notEqualToAll: List<ContentWarning>? = null,
|
override val notEqualToAll: List<ContentRating>? = null,
|
||||||
override val notEqualToAny: List<ContentWarning>? = null,
|
override val notEqualToAny: List<ContentRating>? = null,
|
||||||
override val distinctFrom: ContentWarning? = null,
|
override val distinctFrom: ContentRating? = null,
|
||||||
override val distinctFromAll: List<ContentWarning>? = null,
|
override val distinctFromAll: List<ContentRating>? = null,
|
||||||
override val distinctFromAny: List<ContentWarning>? = null,
|
override val distinctFromAny: List<ContentRating>? = null,
|
||||||
override val notDistinctFrom: ContentWarning? = null,
|
override val notDistinctFrom: ContentRating? = null,
|
||||||
override val `in`: List<ContentWarning>? = null,
|
override val `in`: List<ContentRating>? = null,
|
||||||
override val notIn: List<ContentWarning>? = null,
|
override val notIn: List<ContentRating>? = null,
|
||||||
override val lessThan: ContentWarning? = null,
|
override val lessThan: ContentRating? = null,
|
||||||
override val lessThanOrEqualTo: ContentWarning? = null,
|
override val lessThanOrEqualTo: ContentRating? = null,
|
||||||
override val greaterThan: ContentWarning? = null,
|
override val greaterThan: ContentRating? = null,
|
||||||
override val greaterThanOrEqualTo: ContentWarning? = null,
|
override val greaterThanOrEqualTo: ContentRating? = null,
|
||||||
) : ComparableScalarFilter<ContentWarning>
|
) : ComparableScalarFilter<ContentRating>
|
||||||
|
|
||||||
data class StringFilter(
|
data class StringFilter(
|
||||||
override val isNull: Boolean? = null,
|
override val isNull: Boolean? = null,
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ 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.ExtensionsForExtensionStore
|
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionStoreForExtension
|
||||||
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
|
||||||
@@ -80,8 +81,9 @@ class TachideskDataLoaderRegistryFactory {
|
|||||||
SourceMetaDataLoader(),
|
SourceMetaDataLoader(),
|
||||||
ExtensionDataLoader(),
|
ExtensionDataLoader(),
|
||||||
ExtensionForSourceDataLoader(),
|
ExtensionForSourceDataLoader(),
|
||||||
ExtensionsForExtensionStore(),
|
ExtensionForExtensionStore(),
|
||||||
ExtensionStoreDataLoader(),
|
ExtensionStoreDataLoader(),
|
||||||
|
ExtensionStoreForExtension(),
|
||||||
TrackerDataLoader(),
|
TrackerDataLoader(),
|
||||||
TrackerStatusesDataLoader(),
|
TrackerStatusesDataLoader(),
|
||||||
TrackerScoresDataLoader(),
|
TrackerScoresDataLoader(),
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ 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],
|
||||||
@@ -36,11 +35,10 @@ 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>("ExtensionsForExtensionStore", indexUrl)
|
dataFetchingEnvironment.getValueFromDataLoader<String, ExtensionNodeList>("ExtensionForExtensionStore", 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.ContentWarning
|
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
@@ -32,7 +32,6 @@ class ExtensionType(
|
|||||||
val name: String,
|
val name: String,
|
||||||
val pkgName: String,
|
val pkgName: String,
|
||||||
val apkUrl: String?,
|
val apkUrl: String?,
|
||||||
val jarUrl: String?,
|
|
||||||
val extensionLib: String?,
|
val extensionLib: String?,
|
||||||
val versionName: String,
|
val versionName: String,
|
||||||
@GraphQLDeprecated(
|
@GraphQLDeprecated(
|
||||||
@@ -42,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("contentWarning"))
|
@GraphQLDeprecated("Removed in extension api v1.6", ReplaceWith("contentRating"))
|
||||||
val isNsfw: Boolean,
|
val isNsfw: Boolean,
|
||||||
val contentWarning: ContentWarning,
|
val contentRating: ContentRating,
|
||||||
val isInstalled: Boolean,
|
val isInstalled: Boolean,
|
||||||
val hasUpdate: Boolean,
|
val hasUpdate: Boolean,
|
||||||
val isObsolete: Boolean,
|
val isObsolete: Boolean,
|
||||||
@@ -57,14 +56,13 @@ class ExtensionType(
|
|||||||
name = row[ExtensionTable.name],
|
name = row[ExtensionTable.name],
|
||||||
pkgName = row[ExtensionTable.pkgName],
|
pkgName = row[ExtensionTable.pkgName],
|
||||||
apkUrl = row[ExtensionTable.apkUrl],
|
apkUrl = row[ExtensionTable.apkUrl],
|
||||||
jarUrl = row[ExtensionTable.jarUrl],
|
|
||||||
extensionLib = row[ExtensionTable.extensionLib],
|
extensionLib = row[ExtensionTable.extensionLib],
|
||||||
versionName = row[ExtensionTable.versionName],
|
versionName = row[ExtensionTable.versionName],
|
||||||
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.contentWarning] >= ContentWarning.MIXED.ordinal,
|
isNsfw = row[ExtensionTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal,
|
||||||
contentWarning = ContentWarning.valueOf(row[ExtensionTable.contentWarning]),
|
contentRating = ContentRating.valueOf(row[ExtensionTable.contentRating]),
|
||||||
isInstalled = row[ExtensionTable.isInstalled],
|
isInstalled = row[ExtensionTable.isInstalled],
|
||||||
hasUpdate = row[ExtensionTable.hasUpdate],
|
hasUpdate = row[ExtensionTable.hasUpdate],
|
||||||
isObsolete = row[ExtensionTable.isObsolete],
|
isObsolete = row[ExtensionTable.isObsolete],
|
||||||
@@ -73,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?>("ExtensionStoreDataLoader", storeIndexUrl.orEmpty())
|
dataFetchingEnvironment.getValueFromDataLoader<String, ExtensionStoreType>("ExtensionStoreForExtension", 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.GetSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
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 java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
@@ -42,29 +42,31 @@ class SourceType(
|
|||||||
val id: Long,
|
val id: Long,
|
||||||
val name: String,
|
val name: String,
|
||||||
val lang: String,
|
val lang: String,
|
||||||
val contentWarning: ContentWarning,
|
val message: String?,
|
||||||
|
val contentRating: ContentRating,
|
||||||
val iconUrl: String,
|
val iconUrl: String,
|
||||||
val supportsLatest: Boolean,
|
val supportsLatest: Boolean,
|
||||||
val isConfigurable: Boolean,
|
val isConfigurable: Boolean,
|
||||||
@GraphQLDeprecated("", ReplaceWith("contentWarning"))
|
@GraphQLDeprecated("", ReplaceWith("contentRating"))
|
||||||
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, source: Source) : this(
|
constructor(row: ResultRow, sourceExtension: ResultRow, catalogueSource: CatalogueSource) : 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],
|
||||||
contentWarning = ContentWarning.valueOf(row[SourceTable.contentWarning]),
|
message = row[SourceTable.message],
|
||||||
|
contentRating = ContentRating.valueOf(row[SourceTable.contentRating]),
|
||||||
iconUrl = Extension.proxyExtensionIconUrl(sourceExtension[ExtensionTable.pkgName]),
|
iconUrl = Extension.proxyExtensionIconUrl(sourceExtension[ExtensionTable.pkgName]),
|
||||||
supportsLatest = source.supportsLatest,
|
supportsLatest = catalogueSource.supportsLatest,
|
||||||
isConfigurable = source is ConfigurableSource,
|
isConfigurable = catalogueSource is ConfigurableSource,
|
||||||
isNsfw = row[SourceTable.contentWarning] >= ContentWarning.MIXED.ordinal,
|
isNsfw = row[SourceTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal,
|
||||||
displayName = source.toString(),
|
displayName = catalogueSource.toString(),
|
||||||
homeUrl = runCatching { (source as? HttpSource)?.getHomeUrl() }.getOrNull(),
|
homeUrl = runCatching { (catalogueSource as? HttpSource)?.getHomeUrl() }.getOrNull(),
|
||||||
baseUrl = runCatching { (source as? HttpSource)?.baseUrl }.getOrNull(),
|
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun manga(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<MangaNodeList> =
|
fun manga(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<MangaNodeList> =
|
||||||
@@ -75,7 +77,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> = getSourceOrStub(id).getFilterList().map { filterOf(it) }
|
fun filters(): List<Filter> = getCatalogueSourceOrStub(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)
|
||||||
@@ -84,8 +86,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 =
|
||||||
GetSource
|
GetCatalogueSource
|
||||||
.getSourceOrNull(row[SourceTable.id].value)
|
.getCatalogueSourceOrNull(row[SourceTable.id].value)
|
||||||
?: return null
|
?: return null
|
||||||
val sourceExtension =
|
val sourceExtension =
|
||||||
if (row.hasValue(ExtensionTable.id)) {
|
if (row.hasValue(ExtensionTable.id)) {
|
||||||
@@ -296,7 +298,7 @@ data class FilterChange(
|
|||||||
)
|
)
|
||||||
|
|
||||||
fun updateFilterList(
|
fun updateFilterList(
|
||||||
source: Source,
|
source: CatalogueSource,
|
||||||
changes: List<FilterChange>?,
|
changes: List<FilterChange>?,
|
||||||
): FilterList {
|
): FilterList {
|
||||||
val filterList = source.getFilterList()
|
val filterList = source.getFilterList()
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ object ExtensionController {
|
|||||||
ctx.future {
|
ctx.future {
|
||||||
future {
|
future {
|
||||||
Extension.installExtension(pkgName)
|
Extension.installExtension(pkgName)
|
||||||
|
}.thenApply {
|
||||||
|
ctx.status(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -97,6 +99,8 @@ object ExtensionController {
|
|||||||
uploadedFile.content(),
|
uploadedFile.content(),
|
||||||
uploadedFile.filename(),
|
uploadedFile.filename(),
|
||||||
)
|
)
|
||||||
|
}.thenApply {
|
||||||
|
ctx.status(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -122,6 +126,8 @@ object ExtensionController {
|
|||||||
ctx.future {
|
ctx.future {
|
||||||
future {
|
future {
|
||||||
Extension.updateExtension(pkgName)
|
Extension.updateExtension(pkgName)
|
||||||
|
}.thenApply {
|
||||||
|
ctx.status(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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.Source
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
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.GetSource.getSourceOrStub
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||||
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 = getSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
val source = getCatalogueSourceOrStub(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: Source,
|
source: CatalogueSource,
|
||||||
): 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)
|
||||||
|
|||||||
@@ -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.Source
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
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.GetSource.getSourceOrNull
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||||
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: Source,
|
source: CatalogueSource,
|
||||||
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 = getSourceOrNull(mangaEntry[MangaTable.sourceReference]) ?: return null
|
val source = getCatalogueSourceOrNull(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 =
|
||||||
getSourceOrNull(mangaEntry[MangaTable.sourceReference])
|
getCatalogueSourceOrNull(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: Source,
|
source: CatalogueSource,
|
||||||
sManga: SManga,
|
sManga: SManga,
|
||||||
): SManga {
|
): SManga {
|
||||||
transaction {
|
transaction {
|
||||||
@@ -238,7 +238,6 @@ 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,7 +412,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 = getSourceOrStub(sourceId)) {
|
return when (val source = getCatalogueSourceOrStub(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.GetSource.getSourceOrStub
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||||
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 = getSourceOrStub(sourceId)
|
val source = getCatalogueSourceOrStub(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.GetSource.getSourceOrStub
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||||
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 = getSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
val source = getCatalogueSourceOrStub(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.Source
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
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.GetSource.getSourceOrStub
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||||
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 = getSourceOrStub(sourceId)
|
val source = getCatalogueSourceOrStub(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 = getSourceOrStub(sourceId)
|
val source = getCatalogueSourceOrStub(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: Source,
|
source: CatalogueSource,
|
||||||
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 = getSourceOrStub(sourceId)
|
val source = getCatalogueSourceOrStub(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 = getSourceOrStub(sourceId)
|
val source = getCatalogueSourceOrStub(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 = getSourceOrStub(sourceId)
|
val source = getCatalogueSourceOrStub(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.GetSource.getSourceOrNull
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.unregisterSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.unregisterCatalogueSource
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
||||||
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 = getSourceOrNull(it[SourceTable.id].value) ?: return@mapNotNull null
|
val catalogueSource = getCatalogueSourceOrNull(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.contentWarning] >= ContentWarning.MIXED.ordinal,
|
isNsfw = it[SourceTable.contentRating] == ContentRating.PORNOGRAPHIC.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 = getSourceOrNull(sourceId) ?: return@transaction null
|
val catalogueSource = getCatalogueSourceOrNull(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.contentWarning] >= ContentWarning.MIXED.ordinal,
|
isNsfw = source[SourceTable.contentRating] == ContentRating.PORNOGRAPHIC.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 = getSourceOrStub(sourceId)
|
val source = getCatalogueSourceOrStub(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
|
||||||
unregisterSource(sourceId)
|
unregisterCatalogueSource(sourceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSourcesMetaMaps(ids: List<Long>): Map<Long, Map<String, String>> =
|
fun getSourcesMetaMaps(ids: List<Long>): Map<Long, Map<String, String>> =
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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
|
||||||
@@ -77,7 +78,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 = mangaRow[MangaTable.memo].encodeToByteArray(),
|
memo = Json.encodeToString(mangaRow[MangaTable.memo]).encodeToByteArray(),
|
||||||
)
|
)
|
||||||
|
|
||||||
val mangaId = mangaRow[MangaTable.id].value
|
val mangaId = mangaRow[MangaTable.id].value
|
||||||
@@ -115,7 +116,6 @@ 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(112) var memo: ByteArray = JsonObjectEmptyBytes,
|
@ProtoNumber(13) var memo: ByteArray = JsonObjectEmptyBytes,
|
||||||
// suwayomi
|
// suwayomi
|
||||||
@ProtoNumber(9000) var meta: Map<String, String> = emptyMap(),
|
@ProtoNumber(9000) var meta: Map<String, String> = emptyMap(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import io.github.oshai.kotlinlogging.KotlinLogging
|
|||||||
import io.github.reactivecircus.cache4k.Cache
|
import io.github.reactivecircus.cache4k.Cache
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
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.and
|
import org.jetbrains.exposed.v1.core.and
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
@@ -24,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.GetSource.getSourceOrStub
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||||
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
|
||||||
@@ -78,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 = getSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||||
|
|
||||||
val pageList =
|
val pageList =
|
||||||
source
|
source
|
||||||
@@ -89,7 +88,6 @@ suspend fun refreshChapterPageList(
|
|||||||
scanlator = chapterEntry[ChapterTable.scanlator]
|
scanlator = chapterEntry[ChapterTable.scanlator]
|
||||||
chapter_number = chapterEntry[ChapterTable.chapter_number]
|
chapter_number = chapterEntry[ChapterTable.chapter_number]
|
||||||
date_upload = chapterEntry[ChapterTable.date_upload]
|
date_upload = chapterEntry[ChapterTable.date_upload]
|
||||||
memo = Json.decodeFromString(chapterEntry[ChapterTable.memo])
|
|
||||||
},
|
},
|
||||||
).mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) }
|
).mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) }
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ package suwayomi.tachidesk.manga.impl.extension
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import android.content.pm.PackageInfo
|
|
||||||
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
|
||||||
@@ -18,7 +18,9 @@ import io.github.oshai.kotlinlogging.KotlinLogging
|
|||||||
import net.dongliu.apk.parser.ApkFile
|
import net.dongliu.apk.parser.ApkFile
|
||||||
import net.dongliu.apk.parser.bean.Icon
|
import net.dongliu.apk.parser.bean.Icon
|
||||||
import okhttp3.CacheControl
|
import okhttp3.CacheControl
|
||||||
import org.apache.commons.compress.archivers.zip.ZipFile
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
|
import okio.source
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
||||||
import org.jetbrains.exposed.v1.jdbc.insert
|
import org.jetbrains.exposed.v1.jdbc.insert
|
||||||
@@ -26,385 +28,152 @@ import org.jetbrains.exposed.v1.jdbc.select
|
|||||||
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 org.jetbrains.exposed.v1.jdbc.update
|
import org.jetbrains.exposed.v1.jdbc.update
|
||||||
import suwayomi.tachidesk.manga.impl.util.AndroidManifestParser
|
|
||||||
import suwayomi.tachidesk.manga.impl.util.PackageTools
|
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.ResourceArscIconParser
|
|
||||||
import suwayomi.tachidesk.manga.impl.util.network.await
|
import suwayomi.tachidesk.manga.impl.util.network.await
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||||
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
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.nio.file.Path
|
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
import kotlin.io.path.ExperimentalPathApi
|
|
||||||
import kotlin.io.path.Path
|
import kotlin.io.path.Path
|
||||||
import kotlin.io.path.absolutePathString
|
import kotlin.io.path.absolutePathString
|
||||||
import kotlin.io.path.copyTo
|
|
||||||
import kotlin.io.path.createDirectories
|
|
||||||
import kotlin.io.path.createParentDirectories
|
|
||||||
import kotlin.io.path.deleteExisting
|
|
||||||
import kotlin.io.path.deleteIfExists
|
|
||||||
import kotlin.io.path.deleteRecursively
|
|
||||||
import kotlin.io.path.div
|
|
||||||
import kotlin.io.path.exists
|
|
||||||
import kotlin.io.path.inputStream
|
|
||||||
import kotlin.io.path.isRegularFile
|
|
||||||
import kotlin.io.path.name
|
|
||||||
import kotlin.io.path.nameWithoutExtension
|
|
||||||
import kotlin.io.path.outputStream
|
import kotlin.io.path.outputStream
|
||||||
import kotlin.io.path.relativeTo
|
|
||||||
import kotlin.io.path.walk
|
|
||||||
|
|
||||||
object Extension {
|
object Extension {
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
private val applicationDirs: ApplicationDirs by injectLazy()
|
private val applicationDirs: ApplicationDirs by injectLazy()
|
||||||
|
|
||||||
private suspend fun fetchExtensionFile(url: String): Path {
|
suspend fun installExtension(pkgName: String): Int {
|
||||||
val name = Uri.parse(url).lastPathSegment!!
|
|
||||||
val savePath = Path(applicationDirs.tempRoot) / "extensions" / name
|
|
||||||
// download jar file
|
|
||||||
downloadExtension(url, savePath)
|
|
||||||
|
|
||||||
return savePath
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun installExtension(pkgName: String): String {
|
|
||||||
logger.debug { "Installing $pkgName" }
|
logger.debug { "Installing $pkgName" }
|
||||||
val extension =
|
val apkUrl =
|
||||||
transaction {
|
transaction {
|
||||||
ExtensionTable
|
ExtensionTable
|
||||||
.select(ExtensionTable.apkUrl, ExtensionTable.jarUrl)
|
.select(ExtensionTable.apkUrl)
|
||||||
.where { ExtensionTable.pkgName eq pkgName }
|
.where { ExtensionTable.pkgName eq pkgName }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
} ?: throw NullPointerException("Could not find extension for $pkgName")
|
?.get(ExtensionTable.apkUrl)
|
||||||
val jarUrl = extension[ExtensionTable.jarUrl]
|
} ?: throw NullPointerException("Could not find extension $pkgName")
|
||||||
val apkUrl = extension[ExtensionTable.apkUrl]
|
|
||||||
|
|
||||||
return when {
|
return installAPK {
|
||||||
jarUrl != null -> {
|
val apkName = Uri.parse(apkUrl).lastPathSegment!!
|
||||||
installExtension {
|
val apkSavePath = "${applicationDirs.extensionsRoot}/$apkName"
|
||||||
val jar = fetchExtensionFile(jarUrl)
|
// download apk file
|
||||||
val manifest = extractAndParseAndroidManifest(jar)
|
downloadAPKFile(apkUrl, apkSavePath)
|
||||||
ExtensionPackage.Jar(jar, manifest)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apkUrl != null -> {
|
apkSavePath
|
||||||
installExtension {
|
|
||||||
val apk = fetchExtensionFile(apkUrl)
|
|
||||||
val packageInfo = getPackageInfo(apk)
|
|
||||||
ExtensionPackage.Apk(apk, packageInfo)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
|
||||||
throw NullPointerException("Could not find extension url for $pkgName")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyToExtensionsRoot(
|
|
||||||
inputStream: InputStream,
|
|
||||||
extensionName: String,
|
|
||||||
): Path {
|
|
||||||
val rootPath = Path(applicationDirs.tempRoot) / "extensions"
|
|
||||||
val downloadedFile = rootPath.resolve(extensionName).normalize()
|
|
||||||
check(downloadedFile.startsWith(rootPath) && downloadedFile.parent == rootPath) {
|
|
||||||
"File '$extensionName' is not a valid extension file"
|
|
||||||
}
|
|
||||||
logger.debug { "Saving jar at $extensionName" }
|
|
||||||
// download jar file
|
|
||||||
downloadedFile.createParentDirectories()
|
|
||||||
downloadedFile.outputStream().buffered().use { out ->
|
|
||||||
inputStream.use {
|
|
||||||
it.copyTo(out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return downloadedFile
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun installExternalExtension(
|
suspend fun installExternalExtension(
|
||||||
inputStream: InputStream,
|
inputStream: InputStream,
|
||||||
extensionName: String,
|
apkName: String,
|
||||||
): String =
|
): Int =
|
||||||
when {
|
installAPK(true) {
|
||||||
extensionName.endsWith(".jar") -> {
|
val rootPath = Path(applicationDirs.extensionsRoot)
|
||||||
installExtension(
|
val downloadedFile = rootPath.resolve(apkName).normalize()
|
||||||
true,
|
check(downloadedFile.startsWith(rootPath) && downloadedFile.parent == rootPath) {
|
||||||
{
|
"File '$apkName' is not a valid extension file"
|
||||||
val jar = copyToExtensionsRoot(inputStream, extensionName)
|
|
||||||
val manifest = extractAndParseAndroidManifest(jar)
|
|
||||||
ExtensionPackage.Jar(jar, manifest)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
logger.debug { "Saving apk at $apkName" }
|
||||||
extensionName.endsWith(".apk") -> {
|
// download apk file
|
||||||
installExtension(
|
downloadedFile.outputStream().sink().buffer().use { sink ->
|
||||||
true,
|
inputStream.source().use { source ->
|
||||||
{
|
sink.writeAll(source)
|
||||||
val apk = copyToExtensionsRoot(inputStream, extensionName)
|
sink.flush()
|
||||||
val packageInfo = getPackageInfo(apk)
|
|
||||||
ExtensionPackage.Apk(apk, packageInfo)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
throw NullPointerException("Could not find extension type for $extensionName")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
downloadedFile.absolutePathString()
|
||||||
sealed class ExtensionPackage {
|
|
||||||
abstract val file: Path
|
|
||||||
abstract val metadata: PackageMetadata
|
|
||||||
|
|
||||||
// Abstract hook for type-specific preprocessing
|
|
||||||
abstract suspend fun prepareJarAndIcons(extensionsRoot: Path): Path
|
|
||||||
|
|
||||||
class Apk(
|
|
||||||
override val file: Path,
|
|
||||||
val packageInfo: PackageInfo,
|
|
||||||
) : ExtensionPackage() {
|
|
||||||
override val metadata =
|
|
||||||
PackageMetadata(
|
|
||||||
packageName = packageInfo.packageName,
|
|
||||||
versionName = packageInfo.versionName,
|
|
||||||
versionCode = packageInfo.versionCode,
|
|
||||||
reqFeatures = packageInfo.reqFeatures.orEmpty().map { it.name },
|
|
||||||
metaData = MetadataProvider.FromPackageInfo(packageInfo.applicationInfo.metaData),
|
|
||||||
label = packageInfo.applicationInfo.nonLocalizedLabel?.toString(),
|
|
||||||
)
|
|
||||||
|
|
||||||
override suspend fun prepareJarAndIcons(extensionsRoot: Path): Path {
|
|
||||||
val jarFile = extensionsRoot / (file.nameWithoutExtension + ".jar")
|
|
||||||
dex2jar(file, jarFile)
|
|
||||||
extractAssetsFromApk(file, jarFile)
|
|
||||||
extractAndCacheApkIcon(file, metadata.packageName)
|
|
||||||
file.deleteExisting()
|
|
||||||
return jarFile
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Jar(
|
suspend fun installAPK(
|
||||||
override val file: Path,
|
|
||||||
val manifest: AndroidManifestParser.AndroidManifest,
|
|
||||||
) : ExtensionPackage() {
|
|
||||||
override val metadata =
|
|
||||||
PackageMetadata(
|
|
||||||
packageName = manifest.packageName,
|
|
||||||
versionName = manifest.versionName!!,
|
|
||||||
versionCode = manifest.versionCode!!,
|
|
||||||
reqFeatures = manifest.usesFeatures.mapNotNull { it.name },
|
|
||||||
metaData = MetadataProvider.FromManifest(manifest.application!!.metaData),
|
|
||||||
label = manifest.application.label,
|
|
||||||
)
|
|
||||||
|
|
||||||
override suspend fun prepareJarAndIcons(extensionsRoot: Path): Path {
|
|
||||||
val jarFile = extensionsRoot / file.name
|
|
||||||
|
|
||||||
ZipFile.builder().setPath(file).get().use { jarZip ->
|
|
||||||
try {
|
|
||||||
cacheIcon(
|
|
||||||
metadata.packageName,
|
|
||||||
ResourceArscIconParser.extractIcon(jarZip),
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logger.warn(e) { "Failed to extract icon from JAR ${metadata.packageName}" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
file.copyTo(jarFile)
|
|
||||||
file.deleteExisting()
|
|
||||||
return jarFile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class PackageMetadata(
|
|
||||||
val packageName: String,
|
|
||||||
val versionName: String,
|
|
||||||
val versionCode: Int,
|
|
||||||
val reqFeatures: List<String>,
|
|
||||||
val metaData: MetadataProvider,
|
|
||||||
val label: String?,
|
|
||||||
)
|
|
||||||
|
|
||||||
sealed interface MetadataProvider {
|
|
||||||
fun getString(key: String): String?
|
|
||||||
|
|
||||||
fun getInt(key: String): Int
|
|
||||||
|
|
||||||
class FromPackageInfo(
|
|
||||||
private val bundle: android.os.Bundle,
|
|
||||||
) : MetadataProvider {
|
|
||||||
override fun getString(key: String): String? = bundle.getString(key)
|
|
||||||
|
|
||||||
override fun getInt(key: String): Int = bundle.getInt(key) ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
class FromManifest(
|
|
||||||
private val list: List<AndroidManifestParser.MetaData>,
|
|
||||||
) : MetadataProvider {
|
|
||||||
override fun getString(key: String): String? = list.find { it.name == key }?.value
|
|
||||||
|
|
||||||
override fun getInt(key: String): Int = getString(key)?.toIntOrNull() ?: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun installExtension(
|
|
||||||
forceReinstall: Boolean = false,
|
forceReinstall: Boolean = false,
|
||||||
fetchPackage: suspend () -> ExtensionPackage,
|
fetcher: suspend () -> String,
|
||||||
): String {
|
): Int {
|
||||||
val extPackage = fetchPackage()
|
val apkFilePath = fetcher()
|
||||||
val metadata = extPackage.metadata
|
val apkName = File(apkFilePath).name
|
||||||
val pkgName = metadata.packageName
|
|
||||||
|
|
||||||
|
// check if we don't have the extension already installed
|
||||||
|
// if it's installed and we want to update, it first has to be uninstalled
|
||||||
val isInstalled =
|
val isInstalled =
|
||||||
transaction {
|
transaction {
|
||||||
ExtensionTable
|
ExtensionTable.selectAll().where { ExtensionTable.apkName eq apkName }.firstOrNull()
|
||||||
.select(ExtensionTable.isInstalled)
|
|
||||||
.where { ExtensionTable.pkgName eq pkgName }
|
|
||||||
.firstOrNull()
|
|
||||||
}?.get(ExtensionTable.isInstalled) ?: false
|
}?.get(ExtensionTable.isInstalled) ?: false
|
||||||
|
|
||||||
if (isInstalled) {
|
val fileNameWithoutType = apkName.substringBefore(".apk")
|
||||||
if (forceReinstall) {
|
|
||||||
|
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType"
|
||||||
|
val jarFilePath = "$dirPathWithoutType.jar"
|
||||||
|
|
||||||
|
val packageInfo = getPackageInfo(apkFilePath)
|
||||||
|
val pkgName = packageInfo.packageName
|
||||||
|
if (isInstalled && forceReinstall) {
|
||||||
uninstallExtension(pkgName)
|
uninstallExtension(pkgName)
|
||||||
} else {
|
|
||||||
extPackage.file.deleteExisting()
|
|
||||||
return pkgName
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!metadata.reqFeatures.contains(EXTENSION_FEATURE)) {
|
if (!isInstalled || forceReinstall) {
|
||||||
extPackage.file.deleteExisting()
|
if (!packageInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }) {
|
||||||
throw Exception("This file is not a Tachiyomi extension")
|
throw Exception("This apk is not a Tachiyomi extension")
|
||||||
}
|
}
|
||||||
|
|
||||||
val libVersion = metadata.versionName.substringBeforeLast('.').toDouble()
|
// Validate lib version
|
||||||
|
val libVersion = packageInfo.versionName.substringBeforeLast('.').toDouble()
|
||||||
if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
|
if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
|
||||||
extPackage.file.deleteExisting()
|
|
||||||
throw Exception(
|
throw Exception(
|
||||||
"Lib version is $libVersion, while only versions " +
|
"Lib version is $libVersion, while only versions " +
|
||||||
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed",
|
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentWarning =
|
// TODO: allow trusting keys
|
||||||
extPackage.metadata
|
// val signatureHash = getSignatureHash(packageInfo)
|
||||||
.metaData
|
|
||||||
.getInt(METADATA_CONTENT_WARNING)
|
|
||||||
if (contentWarning == 0) {
|
|
||||||
contentWarning = extPackage.metadata
|
|
||||||
.metaData
|
|
||||||
.getString(METADATA_CONTENT_WARNING)
|
|
||||||
?.toIntOrNull() ?: 0
|
|
||||||
if (contentWarning == 0) {
|
|
||||||
contentWarning = extPackage.metadata
|
|
||||||
.metaData
|
|
||||||
.getString(METADATA_NSFW)
|
|
||||||
?.toIntOrNull() ?: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val sourceClass =
|
// if (signatureHash == null) {
|
||||||
metadata.metaData
|
// throw Exception("Package $pkgName isn't signed")
|
||||||
.getString(METADATA_SOURCE_CLASS)!!
|
// } else if (signatureHash !in trustedSignatures) {
|
||||||
.trim()
|
// throw Exception("This apk is not a signed with the official tachiyomi signature")
|
||||||
|
// }
|
||||||
|
|
||||||
|
val isNsfw = packageInfo.applicationInfo.metaData.getString(METADATA_NSFW) == "1"
|
||||||
|
|
||||||
val className =
|
val className =
|
||||||
if (sourceClass.startsWith(".")) {
|
packageInfo.packageName + packageInfo.applicationInfo.metaData.getString(METADATA_SOURCE_CLASS)
|
||||||
pkgName + sourceClass
|
|
||||||
} else {
|
|
||||||
sourceClass
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug { "Main class for extension is $className" }
|
logger.debug { "Main class for extension is $className" }
|
||||||
|
|
||||||
val extensionsRoot = Path(applicationDirs.extensionsRoot)
|
dex2jar(apkFilePath, jarFilePath, fileNameWithoutType)
|
||||||
val jarFile = extPackage.prepareJarAndIcons(extensionsRoot)
|
extractAssetsFromApk(apkFilePath, jarFilePath)
|
||||||
|
extractAndCacheApkIcon(apkFilePath, apkName)
|
||||||
|
|
||||||
|
// clean up
|
||||||
|
File(apkFilePath).delete()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val extensionName =
|
|
||||||
metadata.metaData.getString(METADATA_NAME)
|
|
||||||
?: metadata.label?.substringAfter("Tachiyomi: ")
|
|
||||||
?: throw Exception("Could not resolve extension name")
|
|
||||||
|
|
||||||
val extensionLibVersion =
|
|
||||||
metadata.metaData
|
|
||||||
.getString(METADATA_EXTENSION_LIB)
|
|
||||||
.takeUnless { it == "0" }
|
|
||||||
?: metadata.versionName.substringBeforeLast('.')
|
|
||||||
|
|
||||||
val apkName =
|
|
||||||
when (extPackage) {
|
|
||||||
is ExtensionPackage.Apk -> extPackage.file.name
|
|
||||||
is ExtensionPackage.Jar -> jarFile.name.removeSuffix(".jar") + ".apk"
|
|
||||||
}
|
|
||||||
|
|
||||||
setupJar(
|
|
||||||
jarFile = jarFile,
|
|
||||||
className = className,
|
|
||||||
extensionName = extensionName,
|
|
||||||
extensionLibVersion = extensionLibVersion,
|
|
||||||
apkName = apkName,
|
|
||||||
pkgName = pkgName,
|
|
||||||
versionName = metadata.versionName,
|
|
||||||
versionCode = metadata.versionCode,
|
|
||||||
contentWarning = contentWarning,
|
|
||||||
)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
// free up the file descriptor if exists
|
|
||||||
PackageTools.jarLoaderMap.remove(jarFile.absolutePathString())?.close()
|
|
||||||
jarFile.deleteIfExists()
|
|
||||||
|
|
||||||
try {
|
|
||||||
uninstallExtension(pkgName)
|
|
||||||
} catch (_: Throwable) {
|
|
||||||
}
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
return pkgName
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupJar(
|
|
||||||
jarFile: Path,
|
|
||||||
className: String,
|
|
||||||
extensionName: String,
|
|
||||||
extensionLibVersion: String,
|
|
||||||
apkName: String,
|
|
||||||
pkgName: String,
|
|
||||||
versionName: String,
|
|
||||||
versionCode: Int,
|
|
||||||
contentWarning: Int,
|
|
||||||
) {
|
|
||||||
// collect sources from the extension
|
// collect sources from the extension
|
||||||
val extensionMainClassInstance = loadExtensionSources(jarFile, className)
|
val extensionMainClassInstance = loadExtensionSources(jarFilePath, className)
|
||||||
val sources: List<Source> =
|
val sources: List<CatalogueSource> =
|
||||||
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 =
|
||||||
@@ -414,18 +183,28 @@ object Extension {
|
|||||||
else -> "all"
|
else -> "all"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val extensionName =
|
||||||
|
packageInfo.applicationInfo.nonLocalizedLabel
|
||||||
|
.toString()
|
||||||
|
.substringAfter("Tachiyomi: ")
|
||||||
|
|
||||||
// update extension info
|
// update extension info
|
||||||
transaction {
|
transaction {
|
||||||
if (ExtensionTable.selectAll().where { ExtensionTable.pkgName eq pkgName }.firstOrNull() == null) {
|
if (ExtensionTable.selectAll().where { ExtensionTable.pkgName eq pkgName }.firstOrNull() == null) {
|
||||||
ExtensionTable.insert {
|
ExtensionTable.insert {
|
||||||
it[this.apkName] = apkName
|
it[this.apkName] = apkName
|
||||||
it[name] = extensionName
|
it[name] = extensionName
|
||||||
it[this.pkgName] = pkgName
|
it[this.pkgName] = packageInfo.packageName
|
||||||
it[this.versionName] = versionName
|
it[versionName] = packageInfo.versionName
|
||||||
it[this.versionCode] = versionCode.toLong()
|
it[versionCode] = packageInfo.versionCode.toLong()
|
||||||
it[extensionLib] = extensionLibVersion
|
|
||||||
it[lang] = extensionLang
|
it[lang] = extensionLang
|
||||||
it[this.contentWarning] = contentWarning
|
// todo will change
|
||||||
|
it[contentRating] =
|
||||||
|
if (isNsfw) {
|
||||||
|
ContentRating.PORNOGRAPHIC.ordinal
|
||||||
|
} else {
|
||||||
|
ContentRating.SAFE.ordinal
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,8 +212,8 @@ object Extension {
|
|||||||
it[this.apkName] = apkName
|
it[this.apkName] = apkName
|
||||||
it[this.isInstalled] = true
|
it[this.isInstalled] = true
|
||||||
it[this.classFQName] = className
|
it[this.classFQName] = className
|
||||||
it[this.versionName] = versionName
|
it[versionName] = packageInfo.versionName
|
||||||
it[this.versionCode] = versionCode.toLong()
|
it[versionCode] = packageInfo.versionCode.toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
val extensionId =
|
val extensionId =
|
||||||
@@ -450,20 +229,33 @@ 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[this.contentWarning] = contentWarning
|
it[contentRating] = if (isNsfw) ContentRating.PORNOGRAPHIC.ordinal else ContentRating.SAFE.ordinal
|
||||||
}
|
}
|
||||||
logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" }
|
logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return 201 // we installed successfully
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// free up the file descriptor if exists
|
||||||
|
PackageTools.jarLoaderMap.remove(jarFilePath)?.close()
|
||||||
|
File(jarFilePath).delete()
|
||||||
|
|
||||||
|
uninstallExtension(pkgName)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 302 // extension was already installed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractAndCacheApkIcon(
|
private fun extractAndCacheApkIcon(
|
||||||
apkFile: Path,
|
apkFilePath: String,
|
||||||
pkgName: String,
|
apkName: String,
|
||||||
) {
|
) {
|
||||||
|
val iconCacheDir = "${applicationDirs.extensionsRoot}/icon"
|
||||||
try {
|
try {
|
||||||
val iconData =
|
val iconData =
|
||||||
ApkFile(apkFile.toFile()).use { apk ->
|
ApkFile(File(apkFilePath)).use { apk ->
|
||||||
apk.allIcons
|
apk.allIcons
|
||||||
.filterIsInstance<Icon>()
|
.filterIsInstance<Icon>()
|
||||||
.mapNotNull { it.data?.let { data -> data to it.density } }
|
.mapNotNull { it.data?.let { data -> data to it.density } }
|
||||||
@@ -471,39 +263,34 @@ object Extension {
|
|||||||
?.first
|
?.first
|
||||||
}
|
}
|
||||||
if (iconData == null) {
|
if (iconData == null) {
|
||||||
logger.warn { "No icon found in APK $pkgName" }
|
logger.warn { "No icon found in APK $apkName" }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cacheIcon(pkgName, iconData.inputStream())
|
|
||||||
|
File(iconCacheDir).mkdirs()
|
||||||
|
clearCachedImage(iconCacheDir, apkName)
|
||||||
|
saveImage("$iconCacheDir/$apkName", iconData.inputStream(), null)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.warn(e) { "Failed to extract icon from APK $pkgName" }
|
logger.warn(e) { "Failed to extract icon from APK $apkName" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cacheIcon(
|
|
||||||
pkgName: String,
|
|
||||||
inputStream: InputStream,
|
|
||||||
) {
|
|
||||||
val iconCacheDir = Path(applicationDirs.extensionsRoot) / "icon"
|
|
||||||
iconCacheDir.createDirectories()
|
|
||||||
clearCachedImage(iconCacheDir.absolutePathString(), pkgName)
|
|
||||||
saveImage("$iconCacheDir/$pkgName", inputStream, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalPathApi::class)
|
|
||||||
private fun extractAssetsFromApk(
|
private fun extractAssetsFromApk(
|
||||||
apkFile: Path,
|
apkPath: String,
|
||||||
jarFile: Path,
|
jarPath: String,
|
||||||
) {
|
) {
|
||||||
val assetsFolder = apkFile.parent / "${apkFile.nameWithoutExtension}_assets"
|
val apkFile = File(apkPath)
|
||||||
assetsFolder.createDirectories()
|
val jarFile = File(jarPath)
|
||||||
|
|
||||||
|
val assetsFolder = File("${apkFile.parent}/${apkFile.nameWithoutExtension}_assets")
|
||||||
|
assetsFolder.mkdir()
|
||||||
ZipInputStream(apkFile.inputStream()).use { zipInputStream ->
|
ZipInputStream(apkFile.inputStream()).use { zipInputStream ->
|
||||||
var zipEntry = zipInputStream.nextEntry
|
var zipEntry = zipInputStream.nextEntry
|
||||||
while (zipEntry != null) {
|
while (zipEntry != null) {
|
||||||
if (zipEntry.name.startsWith("assets/") && !zipEntry.isDirectory) {
|
if (zipEntry.name.startsWith("assets/") && !zipEntry.isDirectory) {
|
||||||
val assetFile = assetsFolder / zipEntry.name
|
val assetFile = File(assetsFolder, zipEntry.name)
|
||||||
assetFile.createParentDirectories()
|
assetFile.parentFile.mkdirs()
|
||||||
assetFile.outputStream().use { outputStream ->
|
FileOutputStream(assetFile).use { outputStream ->
|
||||||
zipInputStream.copyTo(outputStream)
|
zipInputStream.copyTo(outputStream)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -511,9 +298,9 @@ object Extension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val tempJarFile = jarFile.parent / "${jarFile.nameWithoutExtension}_temp.jar"
|
val tempJarFile = File("${jarFile.parent}/${jarFile.nameWithoutExtension}_temp.jar")
|
||||||
ZipInputStream(jarFile.inputStream()).use { jarZipInputStream ->
|
ZipInputStream(jarFile.inputStream()).use { jarZipInputStream ->
|
||||||
ZipOutputStream(tempJarFile.outputStream()).use { jarZipOutputStream ->
|
ZipOutputStream(FileOutputStream(tempJarFile)).use { jarZipOutputStream ->
|
||||||
var zipEntry = jarZipInputStream.nextEntry
|
var zipEntry = jarZipInputStream.nextEntry
|
||||||
while (zipEntry != null) {
|
while (zipEntry != null) {
|
||||||
if (!zipEntry.name.startsWith("META-INF/")) {
|
if (!zipEntry.name.startsWith("META-INF/")) {
|
||||||
@@ -522,8 +309,8 @@ object Extension {
|
|||||||
}
|
}
|
||||||
zipEntry = jarZipInputStream.nextEntry
|
zipEntry = jarZipInputStream.nextEntry
|
||||||
}
|
}
|
||||||
assetsFolder.walk().forEach { file ->
|
assetsFolder.walkTopDown().forEach { file ->
|
||||||
if (file.isRegularFile()) {
|
if (file.isFile) {
|
||||||
jarZipOutputStream.putNextEntry(ZipEntry(file.relativeTo(assetsFolder).toString().replace("\\", "/")))
|
jarZipOutputStream.putNextEntry(ZipEntry(file.relativeTo(assetsFolder).toString().replace("\\", "/")))
|
||||||
file.inputStream().use { inputStream ->
|
file.inputStream().use { inputStream ->
|
||||||
inputStream.copyTo(jarZipOutputStream)
|
inputStream.copyTo(jarZipOutputStream)
|
||||||
@@ -534,25 +321,17 @@ object Extension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
jarFile.deleteIfExists()
|
jarFile.delete()
|
||||||
tempJarFile.copyTo(jarFile)
|
tempJarFile.renameTo(jarFile)
|
||||||
tempJarFile.deleteExisting()
|
|
||||||
|
|
||||||
assetsFolder.deleteRecursively()
|
assetsFolder.deleteRecursively()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractAndParseAndroidManifest(jar: Path): AndroidManifestParser.AndroidManifest =
|
|
||||||
ZipFile.builder().setPath(jar).get().use { jarZip ->
|
|
||||||
jarZip.getInputStream(jarZip.getEntry("AndroidManifest.xml")).use {
|
|
||||||
AndroidManifestParser.parse(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val network: NetworkHelper by injectLazy()
|
private val network: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
private suspend fun downloadExtension(
|
private suspend fun downloadAPKFile(
|
||||||
url: String,
|
url: String,
|
||||||
savePath: Path,
|
savePath: String,
|
||||||
) {
|
) {
|
||||||
val response =
|
val response =
|
||||||
network.client
|
network.client
|
||||||
@@ -560,10 +339,11 @@ object Extension {
|
|||||||
GET(url, cache = CacheControl.FORCE_NETWORK),
|
GET(url, cache = CacheControl.FORCE_NETWORK),
|
||||||
).await()
|
).await()
|
||||||
|
|
||||||
savePath.createParentDirectories()
|
val downloadedFile = File(savePath)
|
||||||
response.body.byteStream().use {
|
downloadedFile.sink().buffer().use { sink ->
|
||||||
savePath.outputStream().buffered().use { out ->
|
response.body.source().use { source ->
|
||||||
it.copyTo(out)
|
sink.writeAll(source)
|
||||||
|
sink.flush()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -575,7 +355,7 @@ object Extension {
|
|||||||
val fileNameWithoutType =
|
val fileNameWithoutType =
|
||||||
extensionRecord[ExtensionTable.apkName]?.substringBefore(".apk")
|
extensionRecord[ExtensionTable.apkName]?.substringBefore(".apk")
|
||||||
?: throw NullPointerException("Missing $pkgName apkName")
|
?: throw NullPointerException("Missing $pkgName apkName")
|
||||||
val jarPath = Path(applicationDirs.extensionsRoot) / "$fileNameWithoutType.jar"
|
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
||||||
val sources =
|
val sources =
|
||||||
transaction {
|
transaction {
|
||||||
val extensionId = extensionRecord[ExtensionTable.id].value
|
val extensionId = extensionRecord[ExtensionTable.id].value
|
||||||
@@ -584,7 +364,7 @@ object Extension {
|
|||||||
|
|
||||||
SourceTable.deleteWhere { SourceTable.extension eq extensionId }
|
SourceTable.deleteWhere { SourceTable.extension eq extensionId }
|
||||||
|
|
||||||
if (extensionRecord[ExtensionTable.isObsolete] || extensionRecord[ExtensionTable.apkUrl] == null) {
|
if (extensionRecord[ExtensionTable.isObsolete]) {
|
||||||
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 }) {
|
||||||
@@ -597,18 +377,18 @@ object Extension {
|
|||||||
sources
|
sources
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jarPath.exists()) {
|
if (File(jarPath).exists()) {
|
||||||
// free up the file descriptor if exists
|
// free up the file descriptor if exists
|
||||||
PackageTools.jarLoaderMap.remove(jarPath.absolutePathString())?.close()
|
PackageTools.jarLoaderMap.remove(jarPath)?.close()
|
||||||
|
|
||||||
// clear all loaded sources
|
// clear all loaded sources
|
||||||
sources.forEach { GetSource.unregisterSource(it) }
|
sources.forEach { GetCatalogueSource.unregisterCatalogueSource(it) }
|
||||||
|
|
||||||
jarPath.deleteExisting()
|
File(jarPath).delete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateExtension(pkgName: String): String {
|
suspend fun updateExtension(pkgName: String): Int {
|
||||||
val targetExtension = ExtensionsList.updateMap.remove(pkgName)!!
|
val targetExtension = ExtensionsList.updateMap.remove(pkgName)!!
|
||||||
uninstallExtension(pkgName)
|
uninstallExtension(pkgName)
|
||||||
transaction {
|
transaction {
|
||||||
@@ -617,7 +397,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[contentWarning] = targetExtension.contentWarning.ordinal
|
it[contentRating] = targetExtension.contentRating.ordinal
|
||||||
it[iconUrl] = targetExtension.iconUrl
|
it[iconUrl] = targetExtension.iconUrl
|
||||||
it[hasUpdate] = false
|
it[hasUpdate] = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ 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
|
||||||
@@ -44,45 +41,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 {
|
suspend fun fetch(indexUrl: String): ExtensionStore = fetch(indexUrl, forceV2 = false)
|
||||||
var updatedIndexUrl: String = indexUrl
|
|
||||||
|
private suspend fun fetch(
|
||||||
|
indexUrl: String,
|
||||||
|
forceV2: Boolean,
|
||||||
|
): ExtensionStore {
|
||||||
|
var updatedIndexUrl = indexUrl
|
||||||
return try {
|
return try {
|
||||||
val response = network.client.newCall(GET(updatedIndexUrl)).awaitSuccess()
|
network.client.newCall(GET(indexUrl)).awaitSuccess().body.source().use { source ->
|
||||||
response.body.source().decompressIfGzipped().use { source ->
|
try {
|
||||||
val networkStore =
|
protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.peek().readByteArray())
|
||||||
when (source.peek().readByte()) {
|
} catch (e: IllegalArgumentException) {
|
||||||
// "[..."
|
logger.debug { "Failed to decode as protobuf, trying JSON" }
|
||||||
0x5B.toByte() -> {
|
try {
|
||||||
run {
|
json.decodeFromBufferedSource<NetworkExtensionStore>(source.peek())
|
||||||
|
} 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 IllegalArgumentException("Provided legacy store url is not valid")
|
throw e
|
||||||
}
|
}
|
||||||
|
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) {
|
||||||
0x7B.toByte() -> {
|
return fetch(legacyIndex.indexV2, forceV2 = true)
|
||||||
try {
|
} else {
|
||||||
json.decodeFromBufferedSource<NetworkLegacyExtensionRepo>(source.peek())
|
legacyIndex
|
||||||
} catch (_: IllegalArgumentException) {
|
|
||||||
json.decodeFromBufferedSource<NetworkExtensionStore>(source)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}.toExtensionStore(updatedIndexUrl)
|
||||||
else -> {
|
|
||||||
protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.readByteArray())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
@@ -108,7 +105,6 @@ 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 }) {
|
||||||
@@ -118,7 +114,6 @@ 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -215,33 +210,17 @@ object ExtensionStoreService {
|
|||||||
|
|
||||||
suspend fun getExtensions(store: ExtensionStore): List<ExtensionInfo> {
|
suspend fun getExtensions(store: ExtensionStore): List<ExtensionInfo> {
|
||||||
val extensions =
|
val extensions =
|
||||||
if (store.extensionListUrl != null) {
|
if (!store.isLegacy) {
|
||||||
val response = network.client.newCall(GET(store.extensionListUrl)).awaitSuccess()
|
val response = network.client.newCall(GET(store.indexUrl)).awaitSuccess()
|
||||||
response.body.source().decompressIfGzipped().use { source ->
|
response.body
|
||||||
when (source.peek().readByte()) {
|
.source()
|
||||||
// "{..."
|
.use { source ->
|
||||||
0x7B.toByte() -> {
|
try {
|
||||||
json.decodeFromBufferedSource<NetworkExtensionStore.ExtensionList>(source)
|
protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.peek().readByteArray())
|
||||||
}
|
} 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()
|
||||||
@@ -253,17 +232,4 @@ 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.ContentWarning
|
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
||||||
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.contentWarning] >= ContentWarning.MIXED.ordinal,
|
isNsfw = it[ExtensionTable.contentRating] == ContentRating.PORNOGRAPHIC.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,8 +132,6 @@ 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
|
|
||||||
this[ExtensionTable.jarUrl] = foundExtension.jarUrl
|
|
||||||
|
|
||||||
// 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]
|
||||||
@@ -175,9 +173,8 @@ 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.contentWarning] = foundExtension.contentWarning.ordinal
|
this[ExtensionTable.contentRating] = foundExtension.contentRating.ordinal
|
||||||
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
||||||
this[ExtensionTable.jarUrl] = foundExtension.jarUrl
|
|
||||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||||
}
|
}
|
||||||
}.toExecutable()
|
}.toExecutable()
|
||||||
@@ -193,9 +190,8 @@ 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.contentWarning] = foundExtension.contentWarning.ordinal
|
this[ExtensionTable.contentRating] = foundExtension.contentRating.ordinal
|
||||||
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
||||||
this[ExtensionTable.jarUrl] = foundExtension.jarUrl
|
|
||||||
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.ContentWarning
|
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
||||||
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,8 +23,7 @@ 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(101) val extensionList: ExtensionList?,
|
@ProtoNumber(5) val extensions: List<Extension>,
|
||||||
@ProtoNumber(102) val extensionListUrl: String?,
|
|
||||||
) : BaseNetworkExtensionStore {
|
) : BaseNetworkExtensionStore {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Contact(
|
data class Contact(
|
||||||
@@ -32,11 +31,6 @@ 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,
|
||||||
@@ -45,16 +39,13 @@ 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 contentWarning: ContentWarning,
|
@ProtoNumber(7) val sources: List<Source>,
|
||||||
@ProtoNumber(8) val sources: List<Source>,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Resources(
|
data class Resources(
|
||||||
@ProtoNumber(1) val apkUrl: String,
|
@ProtoNumber(1) val apkUrl: String,
|
||||||
@ProtoNumber(2) val iconUrl: String,
|
@ProtoNumber(2) val iconUrl: String,
|
||||||
// Keiyoushi specific output
|
|
||||||
@ProtoNumber(501) val jarUrl: String? = null,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -64,27 +55,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 contentWarning: ContentWarning = ContentWarning.SAFE,
|
@ProtoNumber(6) val contentRating: ContentRating = ContentRating.SAFE,
|
||||||
@ProtoNumber(7) val message: String? = null,
|
@ProtoNumber(7) val message: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
enum class ContentWarning {
|
enum class ContentRating {
|
||||||
@ProtoNumber(0)
|
@ProtoNumber(0)
|
||||||
@JsonNames("CONTENT_WARNING_UNSPECIFIED")
|
@JsonNames("CONTENT_RATING_SAFE")
|
||||||
UNSPECIFIED,
|
|
||||||
|
|
||||||
@ProtoNumber(1)
|
|
||||||
@JsonNames("CONTENT_WARNING_SAFE")
|
|
||||||
SAFE,
|
SAFE,
|
||||||
|
|
||||||
|
@ProtoNumber(1)
|
||||||
|
@JsonNames("CONTENT_RATING_SUGGESTIVE")
|
||||||
|
SUGGESTIVE,
|
||||||
|
|
||||||
@ProtoNumber(2)
|
@ProtoNumber(2)
|
||||||
@JsonNames("CONTENT_WARNING_MIXED")
|
@JsonNames("CONTENT_RATING_EROTICA")
|
||||||
MIXED,
|
EROTICA,
|
||||||
|
|
||||||
@ProtoNumber(3)
|
@ProtoNumber(3)
|
||||||
@JsonNames("CONTENT_WARNING_NSFW")
|
@JsonNames("CONTENT_RATING_PORNOGRAPHIC")
|
||||||
NSFW,
|
PORNOGRAPHIC,
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toExtensionStore(indexUrl: String): ExtensionStore =
|
override fun toExtensionStore(indexUrl: String): ExtensionStore =
|
||||||
@@ -99,11 +90,10 @@ data class NetworkExtensionStore(
|
|||||||
discord = contact.discord,
|
discord = contact.discord,
|
||||||
),
|
),
|
||||||
isLegacy = false,
|
isLegacy = false,
|
||||||
extensionListUrl = extensionListUrl,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NetworkExtensionStore.ExtensionList.toExtensionInfos(store: ExtensionStore): List<ExtensionInfo> =
|
fun NetworkExtensionStore.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(
|
||||||
@@ -111,21 +101,18 @@ fun NetworkExtensionStore.ExtensionList.toExtensionInfos(store: ExtensionStore):
|
|||||||
name = extension.name,
|
name = extension.name,
|
||||||
pkgName = extension.packageName,
|
pkgName = extension.packageName,
|
||||||
apkUrl = extension.resources.apkUrl,
|
apkUrl = extension.resources.apkUrl,
|
||||||
jarUrl = extension.resources.jarUrl,
|
|
||||||
iconUrl = extension.resources.iconUrl,
|
iconUrl = extension.resources.iconUrl,
|
||||||
extensionLib = extension.extensionLib,
|
extensionLib = extension.extensionLib,
|
||||||
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",
|
||||||
contentWarning =
|
contentRating =
|
||||||
when (extension.contentWarning) {
|
when (extension.sources.maxOfOrNull { it.contentRating }) {
|
||||||
NetworkExtensionStore.ContentWarning.SAFE,
|
NetworkExtensionStore.ContentRating.SAFE -> ContentRating.SAFE
|
||||||
NetworkExtensionStore.ContentWarning.UNSPECIFIED,
|
NetworkExtensionStore.ContentRating.SUGGESTIVE -> ContentRating.SUGGESTIVE
|
||||||
-> ContentWarning.SAFE
|
NetworkExtensionStore.ContentRating.EROTICA -> ContentRating.EROTICA
|
||||||
|
NetworkExtensionStore.ContentRating.PORNOGRAPHIC -> ContentRating.PORNOGRAPHIC
|
||||||
NetworkExtensionStore.ContentWarning.MIXED -> ContentWarning.MIXED
|
null -> ContentRating.SAFE
|
||||||
|
|
||||||
NetworkExtensionStore.ContentWarning.NSFW -> ContentWarning.NSFW
|
|
||||||
},
|
},
|
||||||
sources =
|
sources =
|
||||||
extension.sources.map { source ->
|
extension.sources.map { source ->
|
||||||
@@ -135,15 +122,12 @@ fun NetworkExtensionStore.ExtensionList.toExtensionInfos(store: ExtensionStore):
|
|||||||
lang = source.language,
|
lang = source.language,
|
||||||
homeUrl = source.homeUrl,
|
homeUrl = source.homeUrl,
|
||||||
message = source.message,
|
message = source.message,
|
||||||
contentWarning =
|
contentRating =
|
||||||
when (extension.contentWarning) { // todo source.contentWarning
|
when (source.contentRating) {
|
||||||
NetworkExtensionStore.ContentWarning.SAFE,
|
NetworkExtensionStore.ContentRating.SAFE -> ContentRating.SAFE
|
||||||
NetworkExtensionStore.ContentWarning.UNSPECIFIED,
|
NetworkExtensionStore.ContentRating.SUGGESTIVE -> ContentRating.SUGGESTIVE
|
||||||
-> ContentWarning.SAFE
|
NetworkExtensionStore.ContentRating.EROTICA -> ContentRating.EROTICA
|
||||||
|
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.ContentWarning
|
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
||||||
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
|
||||||
@@ -44,13 +44,12 @@ fun NetworkLegacyExtension.toExtensionInfo(
|
|||||||
name = name.substringAfter("Tachiyomi: "),
|
name = name.substringAfter("Tachiyomi: "),
|
||||||
pkgName = pkg,
|
pkgName = pkg,
|
||||||
apkUrl = "$storeBaseUrl/apk/$apk",
|
apkUrl = "$storeBaseUrl/apk/$apk",
|
||||||
jarUrl = null,
|
|
||||||
iconUrl = "$storeBaseUrl/icon/$pkg.png",
|
iconUrl = "$storeBaseUrl/icon/$pkg.png",
|
||||||
extensionLib = version.substringBeforeLast('.'),
|
extensionLib = version.substringBeforeLast('.'),
|
||||||
versionCode = code,
|
versionCode = code,
|
||||||
versionName = version,
|
versionName = version,
|
||||||
lang = lang,
|
lang = lang,
|
||||||
contentWarning = if (nsfw == 1) ContentWarning.MIXED else ContentWarning.SAFE,
|
contentRating = if (nsfw == 1) ContentRating.PORNOGRAPHIC else ContentRating.SAFE,
|
||||||
sources =
|
sources =
|
||||||
if (sources.isNullOrEmpty()) {
|
if (sources.isNullOrEmpty()) {
|
||||||
listOf(
|
listOf(
|
||||||
@@ -60,7 +59,7 @@ fun NetworkLegacyExtension.toExtensionInfo(
|
|||||||
lang = lang,
|
lang = lang,
|
||||||
homeUrl = "",
|
homeUrl = "",
|
||||||
message = null,
|
message = null,
|
||||||
contentWarning = if (nsfw == 1) ContentWarning.MIXED else ContentWarning.SAFE,
|
contentRating = if (nsfw == 1) ContentRating.PORNOGRAPHIC else ContentRating.SAFE,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -71,7 +70,7 @@ fun NetworkLegacyExtension.toExtensionInfo(
|
|||||||
lang = source.lang,
|
lang = source.lang,
|
||||||
homeUrl = source.baseUrl,
|
homeUrl = source.baseUrl,
|
||||||
message = null,
|
message = null,
|
||||||
contentWarning = if (nsfw == 1) ContentWarning.MIXED else ContentWarning.SAFE,
|
contentRating = if (nsfw == 1) ContentRating.PORNOGRAPHIC else ContentRating.SAFE,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,6 +36,5 @@ data class NetworkLegacyExtensionRepo(
|
|||||||
discord = null,
|
discord = null,
|
||||||
),
|
),
|
||||||
isLegacy = true,
|
isLegacy = true,
|
||||||
extensionListUrl = null,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,48 +222,6 @@ 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 {
|
||||||
@@ -465,9 +423,9 @@ object Track {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateTrackRecord(track: Track): Int = updateTrackRecords(listOf(track)).first()
|
fun updateTrackRecord(track: Track) = updateTrackRecords(listOf(track))
|
||||||
|
|
||||||
fun updateTrackRecords(tracks: List<Track>): List<Int> =
|
fun updateTrackRecords(tracks: List<Track>) =
|
||||||
transaction {
|
transaction {
|
||||||
if (tracks.isNotEmpty()) {
|
if (tracks.isNotEmpty()) {
|
||||||
BatchUpdateStatement(TrackRecordTable)
|
BatchUpdateStatement(TrackRecordTable)
|
||||||
@@ -489,8 +447,6 @@ 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()
|
||||||
|
|||||||
@@ -1,148 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.util
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import nl.adaptivity.xmlutil.ExperimentalXmlUtilApi
|
|
||||||
import nl.adaptivity.xmlutil.XmlDeclMode
|
|
||||||
import nl.adaptivity.xmlutil.core.KtXmlReader
|
|
||||||
import nl.adaptivity.xmlutil.serialization.UnknownChildHandler
|
|
||||||
import nl.adaptivity.xmlutil.serialization.XML
|
|
||||||
import nl.adaptivity.xmlutil.serialization.XmlElement
|
|
||||||
import nl.adaptivity.xmlutil.serialization.XmlSerialName
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
object AndroidManifestParser {
|
|
||||||
private const val ANDROID_NS = "http://schemas.android.com/apk/res/android"
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@XmlSerialName("manifest", "", "")
|
|
||||||
data class AndroidManifest(
|
|
||||||
@XmlSerialName("package", "", "")
|
|
||||||
val packageName: String,
|
|
||||||
@XmlSerialName("versionCode", ANDROID_NS, "android")
|
|
||||||
val versionCode: Int? = null,
|
|
||||||
@XmlSerialName("versionName", ANDROID_NS, "android")
|
|
||||||
val versionName: String? = null,
|
|
||||||
@XmlElement(true)
|
|
||||||
@XmlSerialName("uses-sdk", "", "")
|
|
||||||
val usesSdk: UsesSdk? = null,
|
|
||||||
@XmlElement(true)
|
|
||||||
@XmlSerialName("uses-feature", "", "")
|
|
||||||
val usesFeatures: List<UsesFeature> = emptyList(),
|
|
||||||
@XmlElement(true)
|
|
||||||
@XmlSerialName("application", "", "")
|
|
||||||
val application: Application? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@XmlSerialName("uses-sdk", "", "")
|
|
||||||
data class UsesSdk(
|
|
||||||
@XmlSerialName("minSdkVersion", ANDROID_NS, "android")
|
|
||||||
val minSdkVersion: Int? = null,
|
|
||||||
@XmlSerialName("targetSdkVersion", ANDROID_NS, "android")
|
|
||||||
val targetSdkVersion: Int? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@XmlSerialName("uses-feature", "", "")
|
|
||||||
data class UsesFeature(
|
|
||||||
@XmlSerialName("name", ANDROID_NS, "android")
|
|
||||||
val name: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@XmlSerialName("application", "", "")
|
|
||||||
data class Application(
|
|
||||||
@XmlSerialName("label", ANDROID_NS, "android")
|
|
||||||
val label: String? = null,
|
|
||||||
@XmlSerialName("icon", ANDROID_NS, "android")
|
|
||||||
val icon: String? = null,
|
|
||||||
@XmlSerialName("allowBackup", ANDROID_NS, "android")
|
|
||||||
val allowBackup: Boolean? = null,
|
|
||||||
@XmlSerialName("extractNativeLibs", ANDROID_NS, "android")
|
|
||||||
val extractNativeLibs: Boolean? = null,
|
|
||||||
@XmlElement(true)
|
|
||||||
@XmlSerialName("meta-data", "", "")
|
|
||||||
val metaData: List<MetaData> = emptyList(),
|
|
||||||
@XmlElement(true)
|
|
||||||
@XmlSerialName("activity", "", "")
|
|
||||||
val activities: List<Activity> = emptyList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@XmlSerialName("meta-data", "", "")
|
|
||||||
data class MetaData(
|
|
||||||
@XmlSerialName("name", ANDROID_NS, "android")
|
|
||||||
val name: String,
|
|
||||||
@XmlSerialName("value", ANDROID_NS, "android")
|
|
||||||
val value: String? = null,
|
|
||||||
@XmlSerialName("resource", ANDROID_NS, "android")
|
|
||||||
val resource: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@XmlSerialName("activity", "", "")
|
|
||||||
data class Activity(
|
|
||||||
@XmlSerialName("name", ANDROID_NS, "android")
|
|
||||||
val name: String,
|
|
||||||
@XmlSerialName("exported", ANDROID_NS, "android")
|
|
||||||
val exported: Boolean? = null,
|
|
||||||
@XmlSerialName("theme", ANDROID_NS, "android")
|
|
||||||
val theme: String? = null,
|
|
||||||
@XmlElement(true)
|
|
||||||
@XmlSerialName("intent-filter", "", "")
|
|
||||||
val intentFilters: List<IntentFilter> = emptyList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@XmlSerialName("intent-filter", "", "")
|
|
||||||
data class IntentFilter(
|
|
||||||
@XmlElement(true)
|
|
||||||
@XmlSerialName("action", "", "")
|
|
||||||
val actions: List<Action> = emptyList(),
|
|
||||||
@XmlElement(true)
|
|
||||||
@XmlSerialName("category", "", "")
|
|
||||||
val categories: List<Category> = emptyList(),
|
|
||||||
@XmlElement(true)
|
|
||||||
@XmlSerialName("data", "", "")
|
|
||||||
val data: List<Data> = emptyList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@XmlSerialName("action", "", "")
|
|
||||||
data class Action(
|
|
||||||
@XmlSerialName("name", ANDROID_NS, "android")
|
|
||||||
val name: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@XmlSerialName("category", "", "")
|
|
||||||
data class Category(
|
|
||||||
@XmlSerialName("name", ANDROID_NS, "android")
|
|
||||||
val name: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@XmlSerialName("data", "", "")
|
|
||||||
data class Data(
|
|
||||||
@XmlSerialName("scheme", ANDROID_NS, "android")
|
|
||||||
val scheme: String? = null,
|
|
||||||
@XmlSerialName("host", ANDROID_NS, "android")
|
|
||||||
val host: String? = null,
|
|
||||||
@XmlSerialName("pathPattern", ANDROID_NS, "android")
|
|
||||||
val pathPattern: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@OptIn(ExperimentalXmlUtilApi::class)
|
|
||||||
private val xml =
|
|
||||||
XML {
|
|
||||||
autoPolymorphic = false
|
|
||||||
repairNamespaces = true
|
|
||||||
xmlDeclMode = XmlDeclMode.Minimal
|
|
||||||
defaultPolicy {
|
|
||||||
unknownChildHandler = UnknownChildHandler { _, _, _, _, _ -> emptyList() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalXmlUtilApi::class)
|
|
||||||
fun parse(input: InputStream): AndroidManifest = xml.decodeFromReader(AndroidManifest.serializer(), KtXmlReader(input))
|
|
||||||
}
|
|
||||||
@@ -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.GetSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||||
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 = GetSource.getSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
val source = GetCatalogueSource.getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||||
|
|
||||||
getMangaDir(mangaEntry[MangaTable.title], source.toString())
|
getMangaDir(mangaEntry[MangaTable.title], source.toString())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,14 +23,13 @@ import suwayomi.tachidesk.server.ApplicationDirs
|
|||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import xyz.nulldev.androidcompat.pm.InstalledPackage.Companion.toList
|
import xyz.nulldev.androidcompat.pm.InstalledPackage.Companion.toList
|
||||||
import xyz.nulldev.androidcompat.pm.toPackageInfo
|
import xyz.nulldev.androidcompat.pm.toPackageInfo
|
||||||
|
import java.io.File
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.URLClassLoader
|
import java.net.URLClassLoader
|
||||||
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import javax.xml.parsers.DocumentBuilderFactory
|
import javax.xml.parsers.DocumentBuilderFactory
|
||||||
import kotlin.io.path.Path
|
import kotlin.io.path.Path
|
||||||
import kotlin.io.path.absolutePathString
|
|
||||||
import kotlin.io.path.nameWithoutExtension
|
|
||||||
import kotlin.io.path.readBytes
|
|
||||||
import kotlin.io.path.relativeTo
|
import kotlin.io.path.relativeTo
|
||||||
|
|
||||||
object PackageTools {
|
object PackageTools {
|
||||||
@@ -41,11 +40,6 @@ 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
|
||||||
|
|
||||||
@@ -53,13 +47,15 @@ object PackageTools {
|
|||||||
* Convert dex to jar, a wrapper for the dex2jar library
|
* Convert dex to jar, a wrapper for the dex2jar library
|
||||||
*/
|
*/
|
||||||
fun dex2jar(
|
fun dex2jar(
|
||||||
dexFile: Path,
|
dexFile: String,
|
||||||
jarFile: Path,
|
jarFile: String,
|
||||||
|
fileNameWithoutType: String,
|
||||||
) {
|
) {
|
||||||
// adopted from com.googlecode.dex2jar.tools.Dex2jarCmd.doCommandLine
|
// adopted from com.googlecode.dex2jar.tools.Dex2jarCmd.doCommandLine
|
||||||
// source at: https://github.com/DexPatcher/dex2jar/tree/v2.1-20190905-lanchon/dex-tools/src/main/java/com/googlecode/dex2jar/tools/Dex2jarCmd.java
|
// source at: https://github.com/DexPatcher/dex2jar/tree/v2.1-20190905-lanchon/dex-tools/src/main/java/com/googlecode/dex2jar/tools/Dex2jarCmd.java
|
||||||
|
|
||||||
val reader = MultiDexFileReader.open(dexFile.readBytes())
|
val jarFilePath = File(jarFile).toPath()
|
||||||
|
val reader = MultiDexFileReader.open(Files.readAllBytes(File(dexFile).toPath()))
|
||||||
val handler = BaksmaliBaseDexExceptionHandler()
|
val handler = BaksmaliBaseDexExceptionHandler()
|
||||||
Dex2jar
|
Dex2jar
|
||||||
.from(reader)
|
.from(reader)
|
||||||
@@ -72,10 +68,10 @@ object PackageTools {
|
|||||||
.noCode(false)
|
.noCode(false)
|
||||||
.skipExceptions(false)
|
.skipExceptions(false)
|
||||||
.dontSanitizeNames(true)
|
.dontSanitizeNames(true)
|
||||||
.to(jarFile)
|
.to(jarFilePath)
|
||||||
if (handler.hasException()) {
|
if (handler.hasException()) {
|
||||||
val rootPath = Path(applicationDirs.extensionsRoot)
|
val rootPath = Path(applicationDirs.extensionsRoot)
|
||||||
val errorFile: Path = rootPath.resolve("${dexFile.nameWithoutExtension}-error.txt")
|
val errorFile: Path = rootPath.resolve("$fileNameWithoutType-error.txt")
|
||||||
logger.error {
|
logger.error {
|
||||||
"""
|
"""
|
||||||
Detail Error Information in File ${errorFile.relativeTo(rootPath)}
|
Detail Error Information in File ${errorFile.relativeTo(rootPath)}
|
||||||
@@ -88,14 +84,15 @@ object PackageTools {
|
|||||||
}
|
}
|
||||||
handler.dump(errorFile, emptyArray<String>())
|
handler.dump(errorFile, emptyArray<String>())
|
||||||
} else {
|
} else {
|
||||||
BytecodeEditor.fixAndroidClasses(jarFile)
|
BytecodeEditor.fixAndroidClasses(jarFilePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A modified version of `xyz.nulldev.androidcompat.pm.InstalledPackage.info` */
|
/** A modified version of `xyz.nulldev.androidcompat.pm.InstalledPackage.info` */
|
||||||
fun getPackageInfo(apkFile: Path): PackageInfo =
|
fun getPackageInfo(apkFilePath: String): PackageInfo {
|
||||||
ApkParsers.getMetaInfo(apkFile.toFile()).toPackageInfo(apkFile.toFile()).apply {
|
val apk = File(apkFilePath)
|
||||||
ApkFile(apkFile.toFile()).use { parsed ->
|
return ApkParsers.getMetaInfo(apk).toPackageInfo(apk).apply {
|
||||||
|
val parsed = ApkFile(apk)
|
||||||
val dbFactory = DocumentBuilderFactory.newInstance()
|
val dbFactory = DocumentBuilderFactory.newInstance()
|
||||||
val dBuilder = dbFactory.newDocumentBuilder()
|
val dBuilder = dbFactory.newDocumentBuilder()
|
||||||
val doc =
|
val doc =
|
||||||
@@ -154,19 +151,19 @@ object PackageTools {
|
|||||||
* It may return an instance of HttpSource or SourceFactory depending on the extension.
|
* It may return an instance of HttpSource or SourceFactory depending on the extension.
|
||||||
*/
|
*/
|
||||||
fun loadExtensionSources(
|
fun loadExtensionSources(
|
||||||
jar: Path,
|
jarPath: String,
|
||||||
className: String,
|
className: String,
|
||||||
): Any {
|
): Any {
|
||||||
try {
|
try {
|
||||||
logger.debug { "loading jar with path: ${jar.absolutePathString()}" }
|
logger.debug { "loading jar with path: $jarPath" }
|
||||||
val classLoader = jarLoaderMap[jar.absolutePathString()] ?: ChildFirstURLClassLoader(arrayOf<URL>(jar.toUri().toURL()))
|
val classLoader = jarLoaderMap[jarPath] ?: ChildFirstURLClassLoader(arrayOf<URL>(Path(jarPath).toUri().toURL()))
|
||||||
val classToLoad = Class.forName(className, false, classLoader)
|
val classToLoad = Class.forName(className, false, classLoader)
|
||||||
|
|
||||||
jarLoaderMap[jar.absolutePathString()] = classLoader
|
jarLoaderMap[jarPath] = classLoader
|
||||||
|
|
||||||
return classToLoad.getDeclaredConstructor().newInstance()
|
return classToLoad.getDeclaredConstructor().newInstance()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.error(e) { "Failed to load jar with path: ${jar.absolutePathString()}" }
|
logger.error(e) { "Failed to load jar with path: $jarPath" }
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.util
|
|
||||||
|
|
||||||
import nl.adaptivity.xmlutil.core.impl.multiplatform.InputStream
|
|
||||||
import org.apache.commons.compress.archivers.zip.ZipFile
|
|
||||||
import pxb.android.arsc.ArscParser
|
|
||||||
import pxb.android.arsc.Config
|
|
||||||
import pxb.android.arsc.Pkg
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.nio.ByteOrder
|
|
||||||
|
|
||||||
object ResourceArscIconParser {
|
|
||||||
private data class IconCandidate(
|
|
||||||
val density: Int,
|
|
||||||
val path: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun extractIcon(zip: ZipFile): InputStream {
|
|
||||||
val packages =
|
|
||||||
zip
|
|
||||||
.getInputStream(zip.getEntry("resources.arsc"))
|
|
||||||
.use { ArscParser(it.readBytes()).parse() }
|
|
||||||
|
|
||||||
val icon =
|
|
||||||
packages
|
|
||||||
.flatMap { it.iconCandidates() }
|
|
||||||
.maxByOrNull { it.density }
|
|
||||||
?: throw NullPointerException("No valid icons")
|
|
||||||
|
|
||||||
val entry =
|
|
||||||
zip.getEntry(icon.path)
|
|
||||||
?: throw NullPointerException("Icon ${icon.path} missing")
|
|
||||||
|
|
||||||
return zip.getInputStream(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Pkg.iconCandidates(): List<IconCandidate> =
|
|
||||||
types.values
|
|
||||||
.filter { it.name == "mipmap" || it.name == "drawable" }
|
|
||||||
.flatMap {
|
|
||||||
it.configs.flatMap {
|
|
||||||
val density = it.density()
|
|
||||||
|
|
||||||
it.resources.values
|
|
||||||
.asSequence()
|
|
||||||
.filter { it.spec.name == "ic_launcher" }
|
|
||||||
.map { it.value.toString() }
|
|
||||||
.filter(::isRasterImage)
|
|
||||||
.map { IconCandidate(density, it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Config.density(): Int =
|
|
||||||
ByteBuffer
|
|
||||||
.wrap(id)
|
|
||||||
.order(ByteOrder.LITTLE_ENDIAN)
|
|
||||||
.getShort(14)
|
|
||||||
.toInt() and 0xffff
|
|
||||||
|
|
||||||
private val rasterExtensions =
|
|
||||||
setOf(
|
|
||||||
"png",
|
|
||||||
"webp",
|
|
||||||
"jpg",
|
|
||||||
"jpeg",
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun isRasterImage(path: String): Boolean =
|
|
||||||
path
|
|
||||||
.substringAfterLast('.', "")
|
|
||||||
.lowercase() in rasterExtensions
|
|
||||||
}
|
|
||||||
@@ -7,6 +7,7 @@ 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
|
||||||
@@ -20,17 +21,15 @@ import suwayomi.tachidesk.manga.model.table.SourceTable
|
|||||||
import suwayomi.tachidesk.server.ApplicationDirs
|
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
|
||||||
import kotlin.io.path.Path
|
|
||||||
import kotlin.io.path.div
|
|
||||||
|
|
||||||
object GetSource {
|
object GetCatalogueSource {
|
||||||
private val logger = KotlinLogging.logger { }
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
private val sourceCache = ConcurrentHashMap<Long, Source>()
|
private val sourceCache = ConcurrentHashMap<Long, CatalogueSource>()
|
||||||
private val applicationDirs: ApplicationDirs by injectLazy()
|
private val applicationDirs: ApplicationDirs by injectLazy()
|
||||||
|
|
||||||
private fun getSource(sourceId: Long): Source? {
|
private fun getCatalogueSource(sourceId: Long): CatalogueSource? {
|
||||||
val cachedResult: Source? = sourceCache[sourceId]
|
val cachedResult: CatalogueSource? = sourceCache[sourceId]
|
||||||
if (cachedResult != null) {
|
if (cachedResult != null) {
|
||||||
return cachedResult
|
return cachedResult
|
||||||
}
|
}
|
||||||
@@ -51,7 +50,7 @@ object GetSource {
|
|||||||
?: throw NullPointerException("Missing apkName")
|
?: throw NullPointerException("Missing apkName")
|
||||||
val className = extensionRecord[ExtensionTable.classFQName]
|
val className = extensionRecord[ExtensionTable.classFQName]
|
||||||
val jarName = apkName.substringBefore(".apk") + ".jar"
|
val jarName = apkName.substringBefore(".apk") + ".jar"
|
||||||
val jarPath = Path(applicationDirs.extensionsRoot) / jarName
|
val jarPath = "${applicationDirs.extensionsRoot}/$jarName"
|
||||||
|
|
||||||
when (val instance = loadExtensionSources(jarPath, className)) {
|
when (val instance = loadExtensionSources(jarPath, className)) {
|
||||||
is Source -> listOf(instance)
|
is Source -> listOf(instance)
|
||||||
@@ -63,25 +62,25 @@ object GetSource {
|
|||||||
return sourceCache[sourceId]!!
|
return sourceCache[sourceId]!!
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSourceOrNull(sourceId: Long): Source? =
|
fun getCatalogueSourceOrNull(sourceId: Long): CatalogueSource? =
|
||||||
try {
|
try {
|
||||||
getSource(sourceId)
|
getCatalogueSource(sourceId)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.warn(e) { "getCatalogueSource($sourceId) failed" }
|
logger.warn(e) { "getCatalogueSource($sourceId) failed" }
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSourceOrStub(sourceId: Long): Source = getSourceOrNull(sourceId) ?: StubSource(sourceId)
|
fun getCatalogueSourceOrStub(sourceId: Long): CatalogueSource = getCatalogueSourceOrNull(sourceId) ?: StubSource(sourceId)
|
||||||
|
|
||||||
fun registerSource(sourcePair: Pair<Long, Source>) {
|
fun registerCatalogueSource(sourcePair: Pair<Long, CatalogueSource>) {
|
||||||
sourceCache += sourcePair
|
sourceCache += sourcePair
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unregisterSource(sourceId: Long) {
|
fun unregisterCatalogueSource(sourceId: Long) {
|
||||||
sourceCache.remove(sourceId)
|
sourceCache.remove(sourceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unregisterAllSources() {
|
fun unregisterAllCatalogueSources() {
|
||||||
(sourceCache - 0L).forEach { (id, _) ->
|
(sourceCache - 0L).forEach { (id, _) ->
|
||||||
sourceCache.remove(id)
|
sourceCache.remove(id)
|
||||||
}
|
}
|
||||||
@@ -12,13 +12,12 @@ data class ExtensionInfo(
|
|||||||
val name: String,
|
val name: String,
|
||||||
val pkgName: String,
|
val pkgName: String,
|
||||||
val apkUrl: String,
|
val apkUrl: String,
|
||||||
val jarUrl: String?,
|
|
||||||
val iconUrl: String,
|
val iconUrl: String,
|
||||||
val extensionLib: String,
|
val extensionLib: String,
|
||||||
val versionCode: Long,
|
val versionCode: Long,
|
||||||
val versionName: String,
|
val versionName: String,
|
||||||
val lang: String,
|
val lang: String,
|
||||||
val contentWarning: ContentWarning,
|
val contentRating: ContentRating,
|
||||||
val sources: List<ExtensionSource>,
|
val sources: List<ExtensionSource>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,16 +27,17 @@ data class ExtensionSource(
|
|||||||
val lang: String,
|
val lang: String,
|
||||||
val homeUrl: String,
|
val homeUrl: String,
|
||||||
val message: String?,
|
val message: String?,
|
||||||
val contentWarning: ContentWarning,
|
val contentRating: ContentRating,
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class ContentWarning {
|
enum class ContentRating {
|
||||||
SAFE,
|
SAFE,
|
||||||
MIXED,
|
SUGGESTIVE,
|
||||||
NSFW,
|
EROTICA,
|
||||||
|
PORNOGRAPHIC,
|
||||||
;
|
;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun valueOf(contentWarning: Int) = entries.find { it.ordinal == contentWarning } ?: SAFE
|
fun valueOf(contentRating: Int) = entries.find { it.ordinal == contentRating } ?: SAFE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ 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,12 +10,11 @@ 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).uniqueIndex()
|
val indexUrl = varchar("index_url", 2048)
|
||||||
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().index()
|
val storeIndexUrl = varchar("store_index_url", 2048).nullable()
|
||||||
|
|
||||||
// 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,13 +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).nullable()
|
val apkUrl = varchar("apk_url", 2048)
|
||||||
val jarUrl = varchar("jar_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 contentWarning = integer("content_warning")
|
val contentRating = integer("content_rating")
|
||||||
|
|
||||||
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,11 +8,13 @@ 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 contentWarning = integer("content_warning").default(0)
|
val message = unlimitedVarchar("message").nullable()
|
||||||
|
val contentRating = integer("content_rating").default(0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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.inSubQuery
|
||||||
import org.jetbrains.exposed.v1.core.intLiteral
|
import org.jetbrains.exposed.v1.core.intLiteral
|
||||||
@@ -22,7 +23,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.GetSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||||
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
|
||||||
@@ -231,7 +232,7 @@ object MangaRepository {
|
|||||||
pageNum: Int,
|
pageNum: Int,
|
||||||
sort: String,
|
sort: String,
|
||||||
): Pair<List<OpdsMangaAcqEntry>, Boolean> {
|
): Pair<List<OpdsMangaAcqEntry>, Boolean> {
|
||||||
val source = GetSource.getSourceOrStub(sourceId)
|
val source = GetCatalogueSource.getCatalogueSourceOrStub(sourceId)
|
||||||
val mangasPage: MangasPage =
|
val mangasPage: MangasPage =
|
||||||
if (sort == "latest" && source.supportsLatest) {
|
if (sort == "latest" && source.supportsLatest) {
|
||||||
source.getLatestUpdates(pageNum)
|
source.getLatestUpdates(pageNum)
|
||||||
|
|||||||
@@ -130,12 +130,9 @@ object H2Migration {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val tmp = output.resolveSibling(output.name + ".tmp")
|
output.outputStream().use { out ->
|
||||||
tmp.outputStream().use { out ->
|
|
||||||
response.body.byteStream().copyTo(out)
|
response.body.byteStream().copyTo(out)
|
||||||
}
|
}
|
||||||
tmp.copyTo(output)
|
|
||||||
tmp.deleteExisting()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info { "Saved: ${output.absolutePathString()}" }
|
logger.info { "Saved: ${output.absolutePathString()}" }
|
||||||
|
|||||||
@@ -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.ContentWarning
|
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
||||||
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,7 +34,6 @@ 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;
|
||||||
${
|
${
|
||||||
@@ -44,11 +43,12 @@ 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_warning INTEGER DEFAULT 0;
|
ALTER TABLE EXTENSION ADD COLUMN content_rating INTEGER DEFAULT 0;
|
||||||
UPDATE EXTENSION SET content_warning = ${ContentWarning.MIXED.ordinal} WHERE is_nsfw = TRUE;
|
UPDATE EXTENSION SET content_rating = ${ContentRating.PORNOGRAPHIC.ordinal} WHERE is_nsfw = TRUE;
|
||||||
ALTER TABLE EXTENSION DROP COLUMN is_nsfw;
|
ALTER TABLE EXTENSION DROP COLUMN is_nsfw;
|
||||||
ALTER TABLE SOURCE ADD COLUMN content_warning INTEGER DEFAULT 0;
|
ALTER TABLE SOURCE ADD COLUMN message $UNLIMITED_TEXT;
|
||||||
UPDATE SOURCE SET content_warning = ${ContentWarning.MIXED.ordinal} WHERE is_nsfw = TRUE;
|
ALTER TABLE SOURCE ADD COLUMN content_rating INTEGER DEFAULT 0;
|
||||||
|
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,14 +14,13 @@ 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).uniqueIndex()
|
val indexUrl = varchar("index_url", 2048)
|
||||||
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>
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
package suwayomi.tachidesk.server.database.migration
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
import de.neonew.exposed.migrations.helpers.AddColumnMigration
|
|
||||||
|
|
||||||
@Suppress("ClassName", "unused")
|
|
||||||
class M0059_AddJarUrlToExtensionTable :
|
|
||||||
AddColumnMigration(
|
|
||||||
"extension",
|
|
||||||
"jar_url",
|
|
||||||
"VARCHAR(2048)",
|
|
||||||
"NULL",
|
|
||||||
)
|
|
||||||
@@ -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.GetSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||||
import suwayomi.tachidesk.server.applicationSetup
|
import suwayomi.tachidesk.server.applicationSetup
|
||||||
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
import suwayomi.tachidesk.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(GetSource::getSourceOrNull) as HttpSource
|
.let(GetCatalogueSource::getCatalogueSourceOrNull) as HttpSource
|
||||||
}
|
}
|
||||||
setLoggingEnabled(true)
|
setLoggingEnabled(true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,8 @@ import suwayomi.tachidesk.manga.impl.Source.getSourceList
|
|||||||
import suwayomi.tachidesk.manga.impl.extension.Extension.installExtension
|
import suwayomi.tachidesk.manga.impl.extension.Extension.installExtension
|
||||||
import suwayomi.tachidesk.manga.impl.extension.Extension.uninstallExtension
|
import suwayomi.tachidesk.manga.impl.extension.Extension.uninstallExtension
|
||||||
import suwayomi.tachidesk.manga.impl.extension.Extension.updateExtension
|
import suwayomi.tachidesk.manga.impl.extension.Extension.updateExtension
|
||||||
import suwayomi.tachidesk.manga.impl.extension.ExtensionStoreService
|
|
||||||
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.getExtensionList
|
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.getExtensionList
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrNull
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
|
||||||
import suwayomi.tachidesk.server.applicationSetup
|
import suwayomi.tachidesk.server.applicationSetup
|
||||||
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
||||||
@@ -51,8 +50,6 @@ class TestExtensionCompatibility {
|
|||||||
private val chaptersToFetch = mutableListOf<Triple<HttpSource, SManga, SChapter>>()
|
private val chaptersToFetch = mutableListOf<Triple<HttpSource, SManga, SChapter>>()
|
||||||
private val chaptersPageListFailedToFetch = mutableListOf<Triple<HttpSource, Pair<SManga, SChapter>, Exception>>()
|
private val chaptersPageListFailedToFetch = mutableListOf<Triple<HttpSource, Pair<SManga, SChapter>, Exception>>()
|
||||||
|
|
||||||
val repos = listOf<String>()
|
|
||||||
|
|
||||||
@BeforeAll
|
@BeforeAll
|
||||||
fun setup() {
|
fun setup() {
|
||||||
val dataRoot = File(BASE_PATH).absolutePath
|
val dataRoot = File(BASE_PATH).absolutePath
|
||||||
@@ -60,14 +57,6 @@ class TestExtensionCompatibility {
|
|||||||
Looper.clearMainLooperForTest()
|
Looper.clearMainLooperForTest()
|
||||||
SettingsRegistry.clear()
|
SettingsRegistry.clear()
|
||||||
applicationSetup()
|
applicationSetup()
|
||||||
repos.forEach {
|
|
||||||
val store =
|
|
||||||
runBlocking {
|
|
||||||
ExtensionStoreService.fetch(it)
|
|
||||||
}
|
|
||||||
ExtensionStoreService.upsert(store)
|
|
||||||
}
|
|
||||||
ExtensionStoreService.syncDbToPrefs()
|
|
||||||
setLoggingEnabled(false)
|
setLoggingEnabled(false)
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
@@ -83,10 +72,7 @@ class TestExtensionCompatibility {
|
|||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
try {
|
|
||||||
uninstallExtension(it.pkgName)
|
uninstallExtension(it.pkgName)
|
||||||
} catch (_: Exception) {
|
|
||||||
}
|
|
||||||
installExtension(it.pkgName)
|
installExtension(it.pkgName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,7 +82,7 @@ class TestExtensionCompatibility {
|
|||||||
.filter {
|
.filter {
|
||||||
// filter local source
|
// filter local source
|
||||||
it.id.toLong() != 0L
|
it.id.toLong() != 0L
|
||||||
}.map { getSourceOrNull(it.id.toLong())!! as HttpSource }
|
}.map { getCatalogueSourceOrNull(it.id.toLong())!! as HttpSource }
|
||||||
}
|
}
|
||||||
setLoggingEnabled(true)
|
setLoggingEnabled(true)
|
||||||
File("$BASE_PATH/sources.txt").writeText(sources.joinToString("\n") { "${it.name} - ${it.lang.uppercase()} - ${it.id}" })
|
File("$BASE_PATH/sources.txt").writeText(sources.joinToString("\n") { "${it.name} - ${it.lang.uppercase()} - ${it.id}" })
|
||||||
@@ -149,9 +135,9 @@ class TestExtensionCompatibility {
|
|||||||
repeat { source.getMangaUpdate(manga, emptyList(), true, true) }
|
repeat { source.getMangaUpdate(manga, emptyList(), true, true) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.warn {
|
logger.warn {
|
||||||
"Failed to fetch manga info and chapters from $source for ${manga.title} (${source.getMangaUrl(
|
"Failed to fetch manga info and chapters from $source for ${manga.title} (${source.mangaDetailsRequest(
|
||||||
manga,
|
manga,
|
||||||
)}): ${e.message}"
|
).url}): ${e.message}"
|
||||||
}
|
}
|
||||||
mangaFailedToFetch += Triple(source, manga, e)
|
mangaFailedToFetch += Triple(source, manga, e)
|
||||||
}
|
}
|
||||||
@@ -161,7 +147,7 @@ class TestExtensionCompatibility {
|
|||||||
File("$BASE_PATH/MangaFailedToFetch.txt").writeText(
|
File("$BASE_PATH/MangaFailedToFetch.txt").writeText(
|
||||||
mangaFailedToFetch.joinToString("\n") { (source, manga, exception) ->
|
mangaFailedToFetch.joinToString("\n") { (source, manga, exception) ->
|
||||||
"${source.name} (${source.lang}, ${source.id}):" +
|
"${source.name} (${source.lang}, ${source.id}):" +
|
||||||
" ${manga.title} (${source.getMangaUrl(manga)}):" +
|
" ${manga.title} (${source.mangaDetailsRequest(manga).url}):" +
|
||||||
" ${exception.message}"
|
" ${exception.message}"
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -177,9 +163,9 @@ class TestExtensionCompatibility {
|
|||||||
repeat { source.getPageList(chapter) }
|
repeat { source.getPageList(chapter) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.warn {
|
logger.warn {
|
||||||
"Failed to fetch manga info from $source for ${manga.title} (${source.getMangaUrl(
|
"Failed to fetch manga info from $source for ${manga.title} (${source.mangaDetailsRequest(
|
||||||
manga,
|
manga,
|
||||||
)}): ${e.message}"
|
).url}): ${e.message}"
|
||||||
}
|
}
|
||||||
chaptersPageListFailedToFetch += Triple(source, manga to chapter, e)
|
chaptersPageListFailedToFetch += Triple(source, manga to chapter, e)
|
||||||
}
|
}
|
||||||
@@ -190,7 +176,7 @@ class TestExtensionCompatibility {
|
|||||||
File("$BASE_PATH/ChapterPageListFailedToFetch.txt").writeText(
|
File("$BASE_PATH/ChapterPageListFailedToFetch.txt").writeText(
|
||||||
chaptersPageListFailedToFetch.joinToString("\n") { (source, manga, exception) ->
|
chaptersPageListFailedToFetch.joinToString("\n") { (source, manga, exception) ->
|
||||||
"${source.name} (${source.lang}, ${source.id}):" +
|
"${source.name} (${source.lang}, ${source.id}):" +
|
||||||
" ${manga.first.title} (${source.getMangaUrl(manga.first)}):" +
|
" ${manga.first.title} (${source.mangaDetailsRequest(manga.first).url}):" +
|
||||||
" ${manga.second.name} (${manga.second.url}): ${exception.message}"
|
" ${manga.second.name} (${manga.second.url}): ${exception.message}"
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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.GetSource.registerSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.registerCatalogueSource
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.unregisterSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.unregisterCatalogueSource
|
||||||
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() {
|
||||||
registerSource(sourceId to source)
|
registerCatalogueSource(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() {
|
||||||
unregisterSource(this.sourceId)
|
unregisterCatalogueSource(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
|
||||||
registerSource(sourceCount to source)
|
registerCatalogueSource(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 { unregisterSource(it) }
|
(0 until sourceCount).forEach { unregisterCatalogueSource(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user