mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-13 15:54:35 -05:00
Compare commits
38 Commits
c79486b8be
...
jar_instal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d6e879cd4 | ||
|
|
f88a281862 | ||
|
|
4f0fbafc1d | ||
|
|
3d73836329 | ||
|
|
2051c6d51d | ||
|
|
c84ab6e426 | ||
|
|
a830782b4b | ||
|
|
c0586e7f36 | ||
|
|
47f0e60659 | ||
|
|
d65a004a17 | ||
|
|
c18155cf06 | ||
|
|
4fdef7d17d | ||
|
|
a66649715e | ||
|
|
393a7349ef | ||
|
|
b0bc8c6fb3 | ||
|
|
9a0871ab93 | ||
|
|
0271a0174f | ||
|
|
a1770cb055 | ||
|
|
cf3589f272 | ||
|
|
d7a839b1fd | ||
|
|
5d46f2a197 | ||
|
|
fbd3a3cc5d | ||
|
|
41d23c0a09 | ||
|
|
f473135d04 | ||
|
|
36ecb3ce6f | ||
|
|
0cac79f354 | ||
|
|
0fd10cc08e | ||
|
|
87aac38253 | ||
|
|
9ca66c18de | ||
|
|
b3c7962503 | ||
|
|
ca93783e72 | ||
|
|
c1d0df8e3a | ||
|
|
612768faeb | ||
|
|
9a1745b626 | ||
|
|
323d58717e | ||
|
|
4d7b7617a9 | ||
|
|
35b48114c6 | ||
|
|
3031aa7ccd |
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -42,7 +42,7 @@ body:
|
||||
label: Suwayomi-Server version
|
||||
description: You can find your Suwayomi-Server version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "v2.2.2100"
|
||||
Example: "v2.3.2238"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
2
.github/workflows/docker_build_stable.yml
vendored
2
.github/workflows/docker_build_stable.yml
vendored
@@ -11,5 +11,5 @@ jobs:
|
||||
steps:
|
||||
- name: run docker build and publish script
|
||||
run: |
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
37
.github/workflows/winget.yml
vendored
37
.github/workflows/winget.yml
vendored
@@ -1,19 +1,52 @@
|
||||
name: Publish to WinGet
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Version
|
||||
required: false
|
||||
type: string
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: windows-latest # action can only be run on windows
|
||||
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
|
||||
with:
|
||||
identifier: Suwayomi.Suwayomi-Server
|
||||
installers-regex: '.*x64.msi$'
|
||||
token: ${{ secrets.WINGET_PUBLISH_PAT }}
|
||||
version: ${{ inputs.version || github.ref_name }}
|
||||
release-tag: ${{ inputs.version || github.ref_name }}
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
release-tag: ${{ steps.version.outputs.version }}
|
||||
54
CHANGELOG.md
54
CHANGELOG.md
@@ -6,10 +6,57 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## [Unreleased] (Preview)
|
||||
|
||||
### Added
|
||||
- (**Extension**) Add ability to install jar versions of extensions
|
||||
|
||||
### Changed
|
||||
- .
|
||||
|
||||
### 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
|
||||
- (**Sync**) Added [SyncYomi](https://github.com/syncyomi/syncyomi) support
|
||||
- (**OPDS**) Add option to skip chapter metadata feed providing direct stream/download links
|
||||
- (**Extension/API**) Support Extensions API v1.6
|
||||
- (**Tracker/API**) Add mutation to bind existing track record
|
||||
|
||||
### Changed
|
||||
- (**Database/H2**) Use the latest H2 database engine
|
||||
@@ -426,6 +473,7 @@ Huge thanks to @martinek who pulled the most of the weight this release!
|
||||
|
||||
<!-- 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: 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
|
||||
@@ -452,7 +500,11 @@ Huge thanks to @martinek who pulled the most of the weight this release!
|
||||
|
||||
<!-- SERVER LINKS -->
|
||||
|
||||
[unreleased]: https://github.com/suwayomi/suwayomi-server/compare/v2.2.2100...HEAD
|
||||
[unreleased]: https://github.com/suwayomi/suwayomi-server/compare/v2.3.2238...HEAD
|
||||
[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.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
|
||||
|
||||
@@ -10,9 +10,9 @@ import java.io.BufferedReader
|
||||
const val MainClass = "suwayomi.tachidesk.MainKt"
|
||||
|
||||
// should be bumped with each stable release
|
||||
val getTachideskVersion = { "v2.2.${getCommitCount()}" }
|
||||
val getTachideskVersion = { "v2.3.${getCommitCount()}" }
|
||||
|
||||
val webUIRevisionTag = "r3136"
|
||||
val webUIRevisionTag = "r3147"
|
||||
|
||||
val webviewJbrRelease = "jbr-release-25.0.3b508.4"
|
||||
|
||||
|
||||
@@ -110,6 +110,7 @@ dex2jar-tools = { module = "de.femtopedia.dex2jar:dex-tools", version.ref = "dex
|
||||
# APK
|
||||
apk-parser = "net.dongliu:apk-parser:2.6.10"
|
||||
apksig = "com.android.tools.build:apksig:9.2.1"
|
||||
axml = "com.github.Aliucord:axml:ed26565eb0"
|
||||
|
||||
# Xml
|
||||
xmlpull = "xmlpull:xmlpull:1.1.3.4a"
|
||||
@@ -215,6 +216,7 @@ shared = [
|
||||
"dex2jar-translator",
|
||||
"dex2jar-tools",
|
||||
"apk-parser",
|
||||
"axml",
|
||||
"jackson-annotations",
|
||||
"jcef",
|
||||
]
|
||||
|
||||
@@ -26,7 +26,7 @@ main() {
|
||||
set -- "${POSITIONAL_ARGS[@]}"
|
||||
|
||||
OS="$1"
|
||||
JAR="$(ls server/build/*.jar | tail -n1)"
|
||||
JAR="$(ls server/build/*.jar | tail -n1)" || error $LINENO "No JAR found in server/build/" 1
|
||||
RELEASE_NAME="$(echo "${JAR%.*}" | xargs basename)-$OS"
|
||||
RELEASE_VERSION=$(echo "$JAR" | grep -oP "v\K[0-9]+\.[0-9]+\.[0-9]+")
|
||||
#RELEASE_REVISION_NUMBER="$(tmp="${JAR%.*}" && echo "${tmp##*-}" | tr -d r)"
|
||||
@@ -34,12 +34,12 @@ main() {
|
||||
|
||||
# clean temporary directory on function return
|
||||
trap "rm -rf $RELEASE_NAME/" RETURN
|
||||
mkdir "$RELEASE_NAME/"
|
||||
mkdir "$RELEASE_NAME/" || error $LINENO "Failed to create release directory" 1
|
||||
|
||||
download_launcher
|
||||
|
||||
if [ ! -f scripts/resources/catch_abort.so ]; then
|
||||
gcc -fPIC -shared scripts/resources/catch_abort.c -lpthread -o scripts/resources/catch_abort.so
|
||||
gcc -fPIC -shared scripts/resources/catch_abort.c -lpthread -o scripts/resources/catch_abort.so || error $LINENO "Failed to compile catch_abort" 1
|
||||
fi
|
||||
|
||||
JRE_ZULU="25.34.17_25.0.3"
|
||||
@@ -49,7 +49,6 @@ main() {
|
||||
case "$OS" in
|
||||
debian-all)
|
||||
RELEASE="$RELEASE_NAME.deb"
|
||||
download_jogamp "linux-*" # it's easier to bundle them ourselves than to handle Debian's path conventions
|
||||
make_deb_package
|
||||
move_release_to_output_dir
|
||||
;;
|
||||
@@ -57,7 +56,6 @@ main() {
|
||||
JRE="$ZULU_RELEASE-ca-$JRE_RELEASE-linux_x64.zip"
|
||||
JRE_DIR="${JRE%.*}"
|
||||
JRE_URL="https://cdn.azul.com/zulu/bin/$JRE"
|
||||
download_jogamp "linux-amd64"
|
||||
setup_jre
|
||||
|
||||
RELEASE="$RELEASE_NAME.AppImage"
|
||||
@@ -67,7 +65,7 @@ main() {
|
||||
linux-assets)
|
||||
RELEASE="$RELEASE_NAME.tar.gz"
|
||||
copy_linux_package_assets_to "$RELEASE_NAME/"
|
||||
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/"
|
||||
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/" || error $LINENO "Failed to create tar.gz" 1
|
||||
move_release_to_output_dir
|
||||
;;
|
||||
linux-x64)
|
||||
@@ -77,7 +75,6 @@ main() {
|
||||
ELECTRON="electron-$electron_version-linux-x64.zip"
|
||||
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
|
||||
download_electron
|
||||
download_jogamp "linux-amd64"
|
||||
setup_jre
|
||||
tree "$RELEASE_NAME"
|
||||
|
||||
@@ -92,7 +89,6 @@ main() {
|
||||
ELECTRON="electron-$electron_version-darwin-x64.zip"
|
||||
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
|
||||
download_electron
|
||||
download_jogamp "macosx-universal"
|
||||
setup_jre
|
||||
tree "$RELEASE_NAME"
|
||||
|
||||
@@ -107,7 +103,6 @@ main() {
|
||||
ELECTRON="electron-$electron_version-darwin-arm64.zip"
|
||||
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
|
||||
download_electron
|
||||
download_jogamp "macosx-universal"
|
||||
setup_jre
|
||||
tree "$RELEASE_NAME"
|
||||
|
||||
@@ -122,7 +117,6 @@ main() {
|
||||
ELECTRON="electron-$electron_version-win32-x64.zip"
|
||||
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
|
||||
download_electron
|
||||
download_jogamp "windows-amd64"
|
||||
setup_jre
|
||||
tree "$RELEASE_NAME"
|
||||
|
||||
@@ -145,52 +139,50 @@ move_release_to_output_dir() {
|
||||
if [ -f "$OUTPUT_DIR/$RELEASE" ]; then
|
||||
rm "$OUTPUT_DIR/$RELEASE"
|
||||
fi
|
||||
mv "$RELEASE" "$OUTPUT_DIR/"
|
||||
mv "$RELEASE" "$OUTPUT_DIR/" || error $LINENO "Failed to move release to output dir" 1
|
||||
}
|
||||
|
||||
download_launcher() {
|
||||
LAUNCHER_URL=$(curl -sf "https://api.github.com/repos/Suwayomi/Suwayomi-Launcher/releases/latest" | grep "browser_download_url" | grep ".jar" | head -n 1 | cut -d '"' -f 4)
|
||||
curl -fL "$LAUNCHER_URL" -o "Suwayomi-Launcher.jar"
|
||||
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
|
||||
if [ -z "$LAUNCHER_URL" ]; then
|
||||
error $LINENO "Failed to determine launcher URL from GitHub API" 1
|
||||
fi
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
download_electron() {
|
||||
if [ ! -f "$ELECTRON" ]; then
|
||||
curl -fL "$ELECTRON_URL" -o "$ELECTRON"
|
||||
curl -fL "$ELECTRON_URL" -o "$ELECTRON" || error $LINENO "Failed to download electron" 1
|
||||
fi
|
||||
|
||||
unzip "$ELECTRON" -d "$RELEASE_NAME/electron/"
|
||||
unzip "$ELECTRON" -d "$RELEASE_NAME/electron/" || error $LINENO "Failed to extract electron" 1
|
||||
}
|
||||
|
||||
setup_jre() {
|
||||
if [ -d "jre" ]; then
|
||||
chmod +x ./jre/bin/java
|
||||
chmod +x ./jre/lib/jspawnhelper
|
||||
mv "jre" "$RELEASE_NAME/jre"
|
||||
if [ ! -f "./jre/bin/java.exe" ]; then
|
||||
chmod +x ./jre/bin/java || error $LINENO "Failed to set java executable permission" 1
|
||||
chmod +x ./jre/lib/jspawnhelper || error $LINENO "Failed to set jspawnhelper executable permission" 1
|
||||
fi
|
||||
mv "jre" "$RELEASE_NAME/jre" || error $LINENO "Failed to move jre" 1
|
||||
else
|
||||
if [ ! -f "$JRE" ]; then
|
||||
curl -fL "$JRE_URL" -o "$JRE"
|
||||
curl -fL "$JRE_URL" -o "$JRE" || error $LINENO "Failed to download JRE" 1
|
||||
fi
|
||||
|
||||
local ext="${JRE##*.}"
|
||||
if [ "$ext" = "zip" ]; then
|
||||
unzip "$JRE"
|
||||
unzip "$JRE" || error $LINENO "Failed to extract JRE zip" 1
|
||||
else
|
||||
tar xvf "$JRE"
|
||||
tar xvf "$JRE" || error $LINENO "Failed to extract JRE tar" 1
|
||||
fi
|
||||
mv "$JRE_DIR" "$RELEASE_NAME/jre" || error $LINENO "Failed to move extracted JRE" 1
|
||||
|
||||
if [ ! -f "$RELEASE_NAME/jre/bin/java.exe" ]; then
|
||||
chmod +x "$RELEASE_NAME/jre/bin/java" || error $LINENO "Failed to set java executable permission" 1
|
||||
chmod +x "$RELEASE_NAME/jre/lib/jspawnhelper" || error $LINENO "Failed to set jspawnhelper executable permission" 1
|
||||
fi
|
||||
mv "$JRE_DIR" "$RELEASE_NAME/jre"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -198,31 +190,31 @@ copy_linux_package_assets_to() {
|
||||
local output_dir
|
||||
output_dir="$(readlink -e "$1" || exit 1)"
|
||||
|
||||
cp "scripts/resources/pkg/suwayomi-server.sh" "$output_dir/"
|
||||
cp "scripts/resources/pkg/suwayomi-server.desktop" "$output_dir/"
|
||||
cp "scripts/resources/pkg/suwayomi-launcher.sh" "$output_dir/"
|
||||
cp "scripts/resources/pkg/suwayomi-launcher.desktop" "$output_dir/"
|
||||
cp "scripts/resources/pkg/systemd"/* "$output_dir/"
|
||||
cp "scripts/resources/pkg/suwayomi-server.sh" "$output_dir/" || error $LINENO "Failed to copy server script" 1
|
||||
cp "scripts/resources/pkg/suwayomi-server.desktop" "$output_dir/" || error $LINENO "Failed to copy server desktop file" 1
|
||||
cp "scripts/resources/pkg/suwayomi-launcher.sh" "$output_dir/" || error $LINENO "Failed to copy launcher script" 1
|
||||
cp "scripts/resources/pkg/suwayomi-launcher.desktop" "$output_dir/" || error $LINENO "Failed to copy launcher desktop file" 1
|
||||
cp "scripts/resources/pkg/systemd"/* "$output_dir/" || error $LINENO "Failed to copy systemd files" 1
|
||||
cp "server/src/main/resources/icon/faviconlogo-128.png" \
|
||||
"$output_dir/suwayomi-server.png"
|
||||
"$output_dir/suwayomi-server.png" || error $LINENO "Failed to copy icon" 1
|
||||
}
|
||||
|
||||
make_linux_bundle() {
|
||||
mkdir "$RELEASE_NAME/bin"
|
||||
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar"
|
||||
cp "scripts/resources/suwayomi-launcher.sh" "$RELEASE_NAME/"
|
||||
cp "scripts/resources/suwayomi-server.sh" "$RELEASE_NAME/"
|
||||
cp "scripts/resources/catch_abort.so" "$RELEASE_NAME/bin/"
|
||||
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar" || error $LINENO "Failed to copy server jar" 1
|
||||
cp "scripts/resources/suwayomi-launcher.sh" "$RELEASE_NAME/" || error $LINENO "Failed to copy launcher script" 1
|
||||
cp "scripts/resources/suwayomi-server.sh" "$RELEASE_NAME/" || error $LINENO "Failed to copy server script" 1
|
||||
cp "scripts/resources/catch_abort.so" "$RELEASE_NAME/bin/" || error $LINENO "Failed to copy catch_abort" 1
|
||||
|
||||
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/"
|
||||
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/" || error $LINENO "Failed to create tar.gz" 1
|
||||
}
|
||||
|
||||
make_macos_bundle() {
|
||||
mkdir "$RELEASE_NAME/bin"
|
||||
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar"
|
||||
cp "scripts/resources/Suwayomi Launcher.command" "$RELEASE_NAME/"
|
||||
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar" || error $LINENO "Failed to copy server jar" 1
|
||||
cp "scripts/resources/Suwayomi Launcher.command" "$RELEASE_NAME/" || error $LINENO "Failed to copy launcher command" 1
|
||||
|
||||
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/"
|
||||
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/" || error $LINENO "Failed to create tar.gz" 1
|
||||
}
|
||||
|
||||
# https://wiki.debian.org/SimplePackagingTutorial
|
||||
@@ -233,28 +225,27 @@ make_deb_package() {
|
||||
#behind $RELEASE_VERSION is underscore "_"
|
||||
local upstream_source="suwayomi-server_$RELEASE_VERSION.orig.tar.gz"
|
||||
|
||||
mkdir "$RELEASE_NAME/$source_dir/"
|
||||
mv "$RELEASE_NAME/natives" "$RELEASE_NAME/$source_dir/natives"
|
||||
mv "$RELEASE_NAME/Suwayomi-Launcher.jar" "$RELEASE_NAME/$source_dir/Suwayomi-Launcher.jar"
|
||||
cp "$JAR" "$RELEASE_NAME/$source_dir/Suwayomi-Server.jar"
|
||||
mkdir "$RELEASE_NAME/$source_dir/" || error $LINENO "Failed to create source directory" 1
|
||||
mv "$RELEASE_NAME/Suwayomi-Launcher.jar" "$RELEASE_NAME/$source_dir/Suwayomi-Launcher.jar" || error $LINENO "Failed to move launcher jar" 1
|
||||
cp "$JAR" "$RELEASE_NAME/$source_dir/Suwayomi-Server.jar" || error $LINENO "Failed to copy server jar" 1
|
||||
copy_linux_package_assets_to "$RELEASE_NAME/$source_dir/"
|
||||
cp "scripts/resources/catch_abort.so" "$RELEASE_NAME/$source_dir/"
|
||||
tar -I "gzip" -C "$RELEASE_NAME/" -cvf "$upstream_source" "$source_dir"
|
||||
cp "scripts/resources/catch_abort.so" "$RELEASE_NAME/$source_dir/" || error $LINENO "Failed to copy catch_abort" 1
|
||||
tar -I "gzip" -C "$RELEASE_NAME/" -cvf "$upstream_source" "$source_dir" || error $LINENO "Failed to create source tar.gz" 1
|
||||
|
||||
cp -r "scripts/resources/deb/" "$RELEASE_NAME/$source_dir/debian/"
|
||||
sed -i "s/\$pkgver/$RELEASE_VERSION/" "$RELEASE_NAME/$source_dir/debian/changelog"
|
||||
sed -i "s/\$pkgrel/1/" "$RELEASE_NAME/$source_dir/debian/changelog"
|
||||
cp -r "scripts/resources/deb/" "$RELEASE_NAME/$source_dir/debian/" || error $LINENO "Failed to copy debian resources" 1
|
||||
sed -i "s/\$pkgver/$RELEASE_VERSION/" "$RELEASE_NAME/$source_dir/debian/changelog" || error $LINENO "Failed to update changelog" 1
|
||||
sed -i "s/\$pkgrel/1/" "$RELEASE_NAME/$source_dir/debian/changelog" || error $LINENO "Failed to update changelog" 1
|
||||
|
||||
if [ "${CI:-}" = true ]; then
|
||||
sudo apt update
|
||||
sudo apt install devscripts build-essential dh-exec
|
||||
sudo apt update || error $LINENO "Failed to update apt" 1
|
||||
sudo apt install devscripts build-essential dh-exec || error $LINENO "Failed to install build deps" 1
|
||||
fi
|
||||
cd "$RELEASE_NAME/$source_dir/"
|
||||
dpkg-buildpackage --no-sign --build=all
|
||||
cd -
|
||||
cd "$RELEASE_NAME/$source_dir/" || error $LINENO "Failed to change directory" 1
|
||||
dpkg-buildpackage --no-sign --build=all || error $LINENO "Debian package build failed" 1
|
||||
cd - || error $LINENO "Failed to return to previous directory" 1
|
||||
|
||||
local deb="suwayomi-server_$RELEASE_VERSION-1_all.deb"
|
||||
mv "$RELEASE_NAME/$deb" "$RELEASE"
|
||||
mv "$RELEASE_NAME/$deb" "$RELEASE" || error $LINENO "Failed to move resulting .deb" 1
|
||||
}
|
||||
|
||||
# https://linuxconfig.org/building-a-hello-world-appimage-on-linux
|
||||
@@ -262,20 +253,20 @@ make_appimage() {
|
||||
local APPIMAGE_URL="https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
|
||||
local APPIMAGE_TOOLNAME="appimagetool-x86_64.AppImage"
|
||||
mkdir "$RELEASE_NAME/bin/"
|
||||
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar"
|
||||
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar" || error $LINENO "Failed to copy server jar" 1
|
||||
|
||||
cp "scripts/resources/pkg/suwayomi-server.desktop" "$RELEASE_NAME/suwayomi-server.desktop"
|
||||
cp "server/src/main/resources/icon/faviconlogo.png" "$RELEASE_NAME/suwayomi-server.png"
|
||||
cp "scripts/resources/appimage/AppRun" "$RELEASE_NAME/AppRun"
|
||||
chmod +x "$RELEASE_NAME/AppRun"
|
||||
cp "scripts/resources/pkg/suwayomi-server.desktop" "$RELEASE_NAME/suwayomi-server.desktop" || error $LINENO "Failed to copy desktop file" 1
|
||||
cp "server/src/main/resources/icon/faviconlogo.png" "$RELEASE_NAME/suwayomi-server.png" || error $LINENO "Failed to copy icon" 1
|
||||
cp "scripts/resources/appimage/AppRun" "$RELEASE_NAME/AppRun" || error $LINENO "Failed to copy AppRun" 1
|
||||
chmod +x "$RELEASE_NAME/AppRun" || error $LINENO "Failed to make AppRun executable" 1
|
||||
|
||||
if [ "${CI:-}" = true ]; then
|
||||
sudo apt update
|
||||
sudo apt install libfuse2
|
||||
sudo apt update || error $LINENO "Failed to update apt" 1
|
||||
sudo apt install libfuse2 || error $LINENO "Failed to install libfuse2" 1
|
||||
fi
|
||||
curl -fL $APPIMAGE_URL -o $APPIMAGE_TOOLNAME
|
||||
chmod +x $APPIMAGE_TOOLNAME
|
||||
ARCH=x86_64 ./$APPIMAGE_TOOLNAME "$RELEASE_NAME" "$RELEASE"
|
||||
curl -fL $APPIMAGE_URL -o $APPIMAGE_TOOLNAME || error $LINENO "Failed to download appimagetool" 1
|
||||
chmod +x $APPIMAGE_TOOLNAME || error $LINENO "Failed to make appimagetool executable" 1
|
||||
ARCH=x86_64 ./$APPIMAGE_TOOLNAME "$RELEASE_NAME" "$RELEASE" || error $LINENO "AppImage creation failed" 1
|
||||
}
|
||||
|
||||
make_windows_bundle() {
|
||||
@@ -300,7 +291,7 @@ make_windows_bundle() {
|
||||
#local rcedit_url="https://github.com/electron/rcedit/releases/download/v0.1.1/$rcedit"
|
||||
## change electron's icon
|
||||
#if [ ! -f "$rcedit" ]; then
|
||||
#curl -fL "$rcedit_url" -o "$rcedit"
|
||||
#curl -fL "$rcedit_url" -o "$rcedit" || error $LINENO "Failed to download rcedit" 1
|
||||
#fi
|
||||
|
||||
#local icon="server/src/main/resources/icon/faviconlogo.ico"
|
||||
@@ -308,38 +299,35 @@ make_windows_bundle() {
|
||||
# --set-icon "$icon"
|
||||
|
||||
mkdir "$RELEASE_NAME/bin"
|
||||
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar"
|
||||
cp "scripts/resources/Suwayomi Launcher.bat" "$RELEASE_NAME"
|
||||
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar" || error $LINENO "Failed to copy server jar" 1
|
||||
cp "scripts/resources/Suwayomi Launcher.bat" "$RELEASE_NAME" || error $LINENO "Failed to copy launcher bat" 1
|
||||
|
||||
zip -9 -r "$RELEASE" "$RELEASE_NAME"
|
||||
zip -9 -r "$RELEASE" "$RELEASE_NAME" || error $LINENO "Failed to create windows bundle zip" 1
|
||||
}
|
||||
|
||||
make_windows_package() {
|
||||
if [ "${CI:-}" = true ]; then
|
||||
sudo apt update
|
||||
sudo apt install -y wixl
|
||||
sudo apt update || error $LINENO "Failed to update apt" 1
|
||||
sudo apt install -y wixl || error $LINENO "Failed to install wixl" 1
|
||||
fi
|
||||
|
||||
find "$RELEASE_NAME/jre" \
|
||||
| wixl-heat --var var.SourceDir -p "$RELEASE_NAME/" \
|
||||
--directory-ref jre --component-group jre >"$RELEASE_NAME/jre.wxs"
|
||||
--directory-ref jre --component-group jre >"$RELEASE_NAME/jre.wxs" || error $LINENO "Failed to heat jre" 1
|
||||
find "$RELEASE_NAME/electron" \
|
||||
| wixl-heat --var var.SourceDir -p "$RELEASE_NAME/" \
|
||||
--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"
|
||||
--directory-ref electron --component-group electron >"$RELEASE_NAME/electron.wxs" || error $LINENO "Failed to heat electron" 1
|
||||
|
||||
find "$RELEASE_NAME/bin" \
|
||||
| wixl-heat --var var.SourceDir -p "$RELEASE_NAME/" \
|
||||
--directory-ref bin --component-group bin >"$RELEASE_NAME/bin.wxs"
|
||||
--directory-ref bin --component-group bin >"$RELEASE_NAME/bin.wxs" || error $LINENO "Failed to heat bin" 1
|
||||
|
||||
local icon="server/src/main/resources/icon/faviconlogo.ico"
|
||||
local arch=${OS##*-}
|
||||
|
||||
wixl -D ProductVersion="$RELEASE_VERSION" -D SourceDir="$RELEASE_NAME" \
|
||||
-D Icon="$icon" --arch "$arch" "scripts/resources/msi/suwayomi-server-$arch.wxs" \
|
||||
"$RELEASE_NAME/jre.wxs" "$RELEASE_NAME/electron.wxs" "$RELEASE_NAME/natives.wxs" "$RELEASE_NAME/bin.wxs" -o "$RELEASE"
|
||||
"$RELEASE_NAME/jre.wxs" "$RELEASE_NAME/electron.wxs" "$RELEASE_NAME/bin.wxs" -o "$RELEASE" || error $LINENO "Windows package build failed" 1
|
||||
}
|
||||
|
||||
# Error handler
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
Suwayomi-Server.jar usr/share/java/suwayomi-server/bin/
|
||||
Suwayomi-Launcher.jar usr/share/java/suwayomi-server/
|
||||
natives/* usr/share/java/suwayomi-server/natives/
|
||||
suwayomi-server.png usr/share/pixmaps/
|
||||
suwayomi-server.desktop usr/share/applications/
|
||||
suwayomi-launcher.desktop usr/share/applications/
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<Directory Id="INSTALLDIR" Name="Suwayomi-Server" >
|
||||
<Directory Id="jre"/>
|
||||
<Directory Id="electron"/>
|
||||
<Directory Id="natives"/>
|
||||
<Directory Id="bin"/>
|
||||
</Directory>
|
||||
</Directory>
|
||||
@@ -63,7 +62,6 @@
|
||||
<ComponentRef Id="SuwayomiLauncherBAT" />
|
||||
<ComponentRef Id="ProgramMenuDir" />
|
||||
<ComponentGroupRef Id="electron" />
|
||||
<ComponentGroupRef Id="natives" />
|
||||
</Feature>
|
||||
|
||||
<Icon Id="Suwayomi.ico" SourceFile="$(var.Icon)" />
|
||||
|
||||
@@ -22,7 +22,7 @@ import kotlinx.coroutines.flow.onEach
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
||||
import java.net.CookieHandler
|
||||
import java.net.CookieManager
|
||||
import java.net.CookiePolicy
|
||||
@@ -62,7 +62,7 @@ class NetworkHelper(
|
||||
userAgent
|
||||
.drop(1)
|
||||
.onEach {
|
||||
GetCatalogueSource.unregisterAllCatalogueSources() // need to reset the headers
|
||||
GetSource.unregisterAllSources() // need to reset the headers
|
||||
}.launchIn(GlobalScope)
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ import org.jetbrains.exposed.v1.jdbc.insert
|
||||
import org.jetbrains.exposed.v1.jdbc.insertAndGetId
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.registerCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.registerSource
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||
@@ -497,7 +497,7 @@ class LocalSource(
|
||||
}
|
||||
|
||||
val fs = LocalSourceFileSystem(applicationDirs)
|
||||
registerCatalogueSource(ID to LocalSource(fs, LocalCoverManager(fs)))
|
||||
registerSource(ID to LocalSource(fs, LocalCoverManager(fs)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,10 +179,10 @@ class ExtensionMutation {
|
||||
val (clientMutationId, extensionFile) = input
|
||||
|
||||
return future {
|
||||
Extension.installExternalExtension(extensionFile.content(), extensionFile.filename())
|
||||
val pkgName = Extension.installExternalExtension(extensionFile.content(), extensionFile.filename())
|
||||
|
||||
val dbExtension =
|
||||
transaction { ExtensionTable.selectAll().where { ExtensionTable.apkName eq extensionFile.filename() }.first() }
|
||||
transaction { ExtensionTable.selectAll().where { ExtensionTable.pkgName eq pkgName }.first() }
|
||||
|
||||
InstallExternalExtensionPayload(
|
||||
clientMutationId,
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
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.Op
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
@@ -181,15 +183,21 @@ class MangaMutation {
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun fetchMangaAndChapters(input: FetchMangaAndChaptersInput): CompletableFuture<FetchMangaAndChaptersPayload?> {
|
||||
fun fetchMangaAndChapters(input: FetchMangaAndChaptersInput): CompletableFuture<DataFetcherResult<FetchMangaAndChaptersPayload?>> {
|
||||
val (clientMutationId, id, fetchManga, fetchChapters) = input
|
||||
|
||||
return future {
|
||||
Manga.updateMangaAndChapters(
|
||||
mangaId = id,
|
||||
updateManga = fetchManga,
|
||||
updateChapters = fetchChapters,
|
||||
)
|
||||
val error =
|
||||
try {
|
||||
Manga.updateMangaAndChapters(
|
||||
mangaId = id,
|
||||
updateManga = fetchManga,
|
||||
updateChapters = fetchChapters,
|
||||
)
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
e
|
||||
}
|
||||
|
||||
val (manga, chapters) =
|
||||
transaction {
|
||||
@@ -202,11 +210,20 @@ class MangaMutation {
|
||||
.map { ChapterType(it) },
|
||||
)
|
||||
}
|
||||
FetchMangaAndChaptersPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
manga = MangaType(manga),
|
||||
chapters = chapters,
|
||||
)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
DataFetcherResult
|
||||
.newResult<FetchMangaAndChaptersPayload>()
|
||||
.data(
|
||||
FetchMangaAndChaptersPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
manga = MangaType(manga),
|
||||
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.manga.impl.MangaList.insertOrUpdate
|
||||
import suwayomi.tachidesk.manga.impl.Source
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
import suwayomi.tachidesk.manga.model.table.SourceMetaTable
|
||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||
@@ -256,7 +256,7 @@ class SourceMutation {
|
||||
val (clientMutationId, sourceId, type, page, query, filters) = input
|
||||
|
||||
return future {
|
||||
val source = GetCatalogueSource.getCatalogueSourceOrNull(sourceId)!!
|
||||
val source = GetSource.getSourceOrNull(sourceId)!!
|
||||
val mangasPage =
|
||||
when (type) {
|
||||
FetchSourceMangaType.SEARCH -> {
|
||||
|
||||
@@ -148,6 +148,36 @@ class TrackMutation {
|
||||
}
|
||||
}
|
||||
|
||||
data class BindTrackRecordInput(
|
||||
val clientMutationId: String? = null,
|
||||
val mangaId: Int,
|
||||
val trackRecordId: Int,
|
||||
)
|
||||
|
||||
data class BindTrackRecordPayload(
|
||||
val clientMutationId: String?,
|
||||
val trackRecord: TrackRecordType,
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun bindTrackRecord(input: BindTrackRecordInput): CompletableFuture<BindTrackRecordPayload?> {
|
||||
val (clientMutationId, mangaId, trackRecordId) = input
|
||||
|
||||
return future {
|
||||
val boundTrackRecordId = Track.bindTrackRecord(mangaId, trackRecordId)
|
||||
|
||||
val trackRecord =
|
||||
transaction {
|
||||
TrackRecordTable.selectAll().where { TrackRecordTable.id eq boundTrackRecordId }.first()
|
||||
}
|
||||
|
||||
BindTrackRecordPayload(
|
||||
clientMutationId,
|
||||
TrackRecordType(trackRecord),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class FetchTrackInput(
|
||||
val clientMutationId: String? = null,
|
||||
val recordId: Int,
|
||||
|
||||
@@ -103,6 +103,7 @@ class ExtensionQuery {
|
||||
val name: String? = null,
|
||||
val pkgName: String? = null,
|
||||
val apkUrl: String? = null,
|
||||
val jarUrl: String? = null,
|
||||
val extensionLib: String? = null,
|
||||
val versionName: String? = null,
|
||||
val versionCode: Int? = null,
|
||||
@@ -122,6 +123,7 @@ class ExtensionQuery {
|
||||
opAnd.eq(apkName, ExtensionTable.apkName)
|
||||
opAnd.eq(iconUrl, ExtensionTable.iconUrl)
|
||||
opAnd.eq(apkUrl, ExtensionTable.apkUrl)
|
||||
opAnd.eq(jarUrl, ExtensionTable.jarUrl)
|
||||
opAnd.eq(name, ExtensionTable.name)
|
||||
opAnd.eq(extensionLib, ExtensionTable.extensionLib)
|
||||
opAnd.eq(versionName, ExtensionTable.versionName)
|
||||
@@ -150,6 +152,7 @@ class ExtensionQuery {
|
||||
val name: StringFilter? = null,
|
||||
val pkgName: StringFilter? = null,
|
||||
val apkUrl: StringFilter? = null,
|
||||
val jarUrl: StringFilter? = null,
|
||||
val versionName: StringFilter? = null,
|
||||
val extensionLib: StringFilter? = null,
|
||||
@GraphQLDeprecated("", ReplaceWith("versionCodeLong"))
|
||||
@@ -175,6 +178,7 @@ class ExtensionQuery {
|
||||
andFilterWithCompareString(ExtensionTable.name, name),
|
||||
andFilterWithCompareString(ExtensionTable.pkgName, pkgName),
|
||||
andFilterWithCompareString(ExtensionTable.apkUrl, apkUrl),
|
||||
andFilterWithCompareString(ExtensionTable.jarUrl, jarUrl),
|
||||
andFilterWithCompareString(ExtensionTable.extensionLib, extensionLib),
|
||||
andFilterWithCompareString(ExtensionTable.versionName, versionName),
|
||||
andFilterWithCompare(ExtensionTable.versionCode, versionCodeLong),
|
||||
|
||||
@@ -32,6 +32,7 @@ class ExtensionType(
|
||||
val name: String,
|
||||
val pkgName: String,
|
||||
val apkUrl: String?,
|
||||
val jarUrl: String?,
|
||||
val extensionLib: String?,
|
||||
val versionName: String,
|
||||
@GraphQLDeprecated(
|
||||
@@ -56,6 +57,7 @@ class ExtensionType(
|
||||
name = row[ExtensionTable.name],
|
||||
pkgName = row[ExtensionTable.pkgName],
|
||||
apkUrl = row[ExtensionTable.apkUrl],
|
||||
jarUrl = row[ExtensionTable.jarUrl],
|
||||
extensionLib = row[ExtensionTable.extensionLib],
|
||||
versionName = row[ExtensionTable.versionName],
|
||||
versionCode = row[ExtensionTable.versionCode].toInt(),
|
||||
@@ -71,8 +73,8 @@ class ExtensionType(
|
||||
fun source(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<SourceNodeList> =
|
||||
dataFetchingEnvironment.getValueFromDataLoader<String, SourceNodeList>("SourcesForExtensionDataLoader", pkgName)
|
||||
|
||||
fun extensionStore(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<ExtensionStoreType> =
|
||||
dataFetchingEnvironment.getValueFromDataLoader<String, ExtensionStoreType>("ExtensionStoreDataLoader", storeIndexUrl.orEmpty())
|
||||
fun extensionStore(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<ExtensionStoreType?> =
|
||||
dataFetchingEnvironment.getValueFromDataLoader<String, ExtensionStoreType?>("ExtensionStoreDataLoader", storeIndexUrl.orEmpty())
|
||||
}
|
||||
|
||||
data class ExtensionNodeList(
|
||||
|
||||
@@ -9,8 +9,8 @@ package suwayomi.tachidesk.graphql.types
|
||||
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
@@ -24,8 +24,8 @@ import suwayomi.tachidesk.graphql.server.primitives.NodeList
|
||||
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
|
||||
import suwayomi.tachidesk.manga.impl.Source.getSourcePreferencesRaw
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||
@@ -53,18 +53,18 @@ class SourceType(
|
||||
@GraphQLDeprecated("", ReplaceWith("homeUrl"))
|
||||
val baseUrl: String?,
|
||||
) : Node {
|
||||
constructor(row: ResultRow, sourceExtension: ResultRow, catalogueSource: CatalogueSource) : this(
|
||||
constructor(row: ResultRow, sourceExtension: ResultRow, source: Source) : this(
|
||||
id = row[SourceTable.id].value,
|
||||
name = row[SourceTable.name],
|
||||
lang = row[SourceTable.lang],
|
||||
contentWarning = ContentWarning.valueOf(row[SourceTable.contentWarning]),
|
||||
iconUrl = Extension.proxyExtensionIconUrl(sourceExtension[ExtensionTable.pkgName]),
|
||||
supportsLatest = catalogueSource.supportsLatest,
|
||||
isConfigurable = catalogueSource is ConfigurableSource,
|
||||
supportsLatest = source.supportsLatest,
|
||||
isConfigurable = source is ConfigurableSource,
|
||||
isNsfw = row[SourceTable.contentWarning] >= ContentWarning.MIXED.ordinal,
|
||||
displayName = catalogueSource.toString(),
|
||||
homeUrl = runCatching { (catalogueSource as? HttpSource)?.getHomeUrl() }.getOrNull(),
|
||||
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
|
||||
displayName = source.toString(),
|
||||
homeUrl = runCatching { (source as? HttpSource)?.getHomeUrl() }.getOrNull(),
|
||||
baseUrl = runCatching { (source as? HttpSource)?.baseUrl }.getOrNull(),
|
||||
)
|
||||
|
||||
fun manga(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<MangaNodeList> =
|
||||
@@ -75,7 +75,7 @@ class SourceType(
|
||||
|
||||
fun preferences(): List<Preference> = getSourcePreferencesRaw(id).map { preferenceOf(it) }
|
||||
|
||||
fun filters(): List<Filter> = getCatalogueSourceOrStub(id).getFilterList().map { filterOf(it) }
|
||||
fun filters(): List<Filter> = getSourceOrStub(id).getFilterList().map { filterOf(it) }
|
||||
|
||||
fun meta(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<List<SourceMetaType>> =
|
||||
dataFetchingEnvironment.getValueFromDataLoader<Long, List<SourceMetaType>>("SourceMetaDataLoader", id)
|
||||
@@ -84,8 +84,8 @@ class SourceType(
|
||||
@Suppress("ktlint:standard:function-naming")
|
||||
fun SourceType(row: ResultRow): SourceType? {
|
||||
val catalogueSource =
|
||||
GetCatalogueSource
|
||||
.getCatalogueSourceOrNull(row[SourceTable.id].value)
|
||||
GetSource
|
||||
.getSourceOrNull(row[SourceTable.id].value)
|
||||
?: return null
|
||||
val sourceExtension =
|
||||
if (row.hasValue(ExtensionTable.id)) {
|
||||
@@ -296,7 +296,7 @@ data class FilterChange(
|
||||
)
|
||||
|
||||
fun updateFilterList(
|
||||
source: CatalogueSource,
|
||||
source: Source,
|
||||
changes: List<FilterChange>?,
|
||||
): FilterList {
|
||||
val filterList = source.getFilterList()
|
||||
|
||||
@@ -63,8 +63,6 @@ object ExtensionController {
|
||||
ctx.future {
|
||||
future {
|
||||
Extension.installExtension(pkgName)
|
||||
}.thenApply {
|
||||
ctx.status(it)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -99,8 +97,6 @@ object ExtensionController {
|
||||
uploadedFile.content(),
|
||||
uploadedFile.filename(),
|
||||
)
|
||||
}.thenApply {
|
||||
ctx.status(it)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -126,8 +122,6 @@ object ExtensionController {
|
||||
ctx.future {
|
||||
future {
|
||||
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
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
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.EnqueueInput
|
||||
import suwayomi.tachidesk.manga.impl.track.Track
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
|
||||
import suwayomi.tachidesk.manga.model.dataclass.MangaChapterDataClass
|
||||
import suwayomi.tachidesk.manga.model.dataclass.PaginatedList
|
||||
@@ -119,7 +119,7 @@ object Chapter {
|
||||
transaction {
|
||||
MangaTable.selectAll().where { MangaTable.id eq mangaId }.first()
|
||||
}
|
||||
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
val source = getSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
|
||||
val chapters =
|
||||
Manga
|
||||
@@ -139,7 +139,7 @@ object Chapter {
|
||||
fun updateChapterListDatabase(
|
||||
mangaEntry: ResultRow,
|
||||
chapters: List<SChapter>,
|
||||
source: CatalogueSource,
|
||||
source: Source,
|
||||
): List<SChapter> {
|
||||
val currentLatestChapterNumber = Manga.getLatestChapter(mangaEntry[MangaTable.id].value)?.chapterNumber ?: 0f
|
||||
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.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
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 suwayomi.tachidesk.manga.impl.download.fileProvider.impl.MissingThumbnailException
|
||||
import suwayomi.tachidesk.manga.impl.util.network.await
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrNull
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
||||
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.getImageResponse
|
||||
@@ -91,7 +91,7 @@ object Manga {
|
||||
|
||||
suspend fun fetchMangaAndChapters(
|
||||
mangaEntry: ResultRow,
|
||||
source: CatalogueSource,
|
||||
source: Source,
|
||||
fetchDetails: Boolean,
|
||||
fetchChapters: Boolean,
|
||||
): SMangaUpdate {
|
||||
@@ -139,7 +139,7 @@ object Manga {
|
||||
return mangaInfoMutex.get(mangaId) { Mutex() }.withLock {
|
||||
val mangaEntry =
|
||||
transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||
val source = getCatalogueSourceOrNull(mangaEntry[MangaTable.sourceReference]) ?: return null
|
||||
val source = getSourceOrNull(mangaEntry[MangaTable.sourceReference]) ?: return null
|
||||
val sManga =
|
||||
fetchMangaAndChapters(
|
||||
mangaEntry,
|
||||
@@ -161,7 +161,7 @@ object Manga {
|
||||
var mangaEntry =
|
||||
transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||
val source =
|
||||
getCatalogueSourceOrNull(mangaEntry[MangaTable.sourceReference])
|
||||
getSourceOrNull(mangaEntry[MangaTable.sourceReference])
|
||||
?: throw NullPointerException("Missing source ${mangaEntry[MangaTable.sourceReference]}")
|
||||
val mangaUpdate =
|
||||
fetchMangaAndChapters(
|
||||
@@ -186,7 +186,7 @@ object Manga {
|
||||
|
||||
fun updateMangaDatabase(
|
||||
mangaEntry: ResultRow,
|
||||
source: CatalogueSource,
|
||||
source: Source,
|
||||
sManga: SManga,
|
||||
): SManga {
|
||||
transaction {
|
||||
@@ -238,6 +238,7 @@ object Manga {
|
||||
it[MangaTable.lastFetchedAt] = Instant.now().epochSecond
|
||||
|
||||
it[MangaTable.updateStrategy] = sManga.update_strategy.name
|
||||
it[MangaTable.memo] = Json.encodeToString(sManga.memo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,7 +413,7 @@ object Manga {
|
||||
val mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||
val sourceId = mangaEntry[MangaTable.sourceReference]
|
||||
|
||||
return when (val source = getCatalogueSourceOrStub(sourceId)) {
|
||||
return when (val source = getSourceOrStub(sourceId)) {
|
||||
is HttpSource -> {
|
||||
getImageResponse(cacheSaveDir, fileName) {
|
||||
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.statements.toExecutable
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
||||
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
import suwayomi.tachidesk.manga.model.table.toDataClass
|
||||
@@ -36,7 +36,7 @@ object MangaList {
|
||||
require(pageNum > 0) {
|
||||
"pageNum = $pageNum is not in valid range"
|
||||
}
|
||||
val source = getCatalogueSourceOrStub(sourceId)
|
||||
val source = getSourceOrStub(sourceId)
|
||||
val mangasPage =
|
||||
if (popular) {
|
||||
source.getPopularManga(pageNum)
|
||||
|
||||
@@ -21,7 +21,7 @@ import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import suwayomi.tachidesk.graphql.types.DownloadConversion
|
||||
import suwayomi.tachidesk.manga.impl.util.getChapterCachePath
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
@@ -118,7 +118,7 @@ object Page {
|
||||
return imageFile.inputStream() to (ImageUtil.findImageType { imageFile.inputStream() }?.mime ?: "image/jpeg")
|
||||
}
|
||||
|
||||
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
val source = getSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
source as HttpSource
|
||||
|
||||
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
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import io.javalin.json.JsonMapper
|
||||
import io.javalin.json.fromJsonString
|
||||
import kotlinx.serialization.Serializable
|
||||
import suwayomi.tachidesk.manga.impl.MangaList.processEntries
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
||||
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
@@ -24,7 +24,7 @@ object Search {
|
||||
searchTerm: String,
|
||||
pageNum: Int,
|
||||
): PagedMangaListDataClass {
|
||||
val source = getCatalogueSourceOrStub(sourceId)
|
||||
val source = getSourceOrStub(sourceId)
|
||||
val searchManga = source.getSearchManga(pageNum, searchTerm, getFilterListOf(source))
|
||||
return searchManga.processEntries(sourceId)
|
||||
}
|
||||
@@ -34,7 +34,7 @@ object Search {
|
||||
pageNum: Int,
|
||||
filter: FilterData,
|
||||
): PagedMangaListDataClass {
|
||||
val source = getCatalogueSourceOrStub(sourceId)
|
||||
val source = getSourceOrStub(sourceId)
|
||||
val filterList = if (filter.filter != null) buildFilterList(sourceId, filter.filter) else source.getFilterList()
|
||||
val searchManga = source.getSearchManga(pageNum, filter.searchTerm ?: "", filterList)
|
||||
return searchManga.processEntries(sourceId)
|
||||
@@ -43,7 +43,7 @@ object Search {
|
||||
private val filterListCache = mutableMapOf<Long, FilterList>()
|
||||
|
||||
private fun getFilterListOf(
|
||||
source: CatalogueSource,
|
||||
source: Source,
|
||||
reset: Boolean = false,
|
||||
): FilterList {
|
||||
if (reset || !filterListCache.containsKey(source.id)) {
|
||||
@@ -56,7 +56,7 @@ object Search {
|
||||
sourceId: Long,
|
||||
reset: Boolean,
|
||||
): List<FilterObject> {
|
||||
val source = getCatalogueSourceOrStub(sourceId)
|
||||
val source = getSourceOrStub(sourceId)
|
||||
|
||||
return getFilterListOf(source, reset).list.map {
|
||||
FilterObject(
|
||||
@@ -111,7 +111,7 @@ object Search {
|
||||
sourceId: Long,
|
||||
changes: List<FilterChange>,
|
||||
) {
|
||||
val source = getCatalogueSourceOrStub(sourceId)
|
||||
val source = getSourceOrStub(sourceId)
|
||||
val filterList = getFilterListOf(source, false)
|
||||
updateFilterList(filterList, changes)
|
||||
}
|
||||
@@ -169,7 +169,7 @@ object Search {
|
||||
sourceId: Long,
|
||||
changes: List<FilterChange>,
|
||||
): FilterList {
|
||||
val source = getCatalogueSourceOrStub(sourceId)
|
||||
val source = getSourceOrStub(sourceId)
|
||||
val filterList = source.getFilterList()
|
||||
return updateFilterList(filterList, changes)
|
||||
}
|
||||
|
||||
@@ -26,9 +26,9 @@ import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import suwayomi.tachidesk.manga.impl.Source.preferenceScreenMap
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension.proxyExtensionIconUrl
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.unregisterCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrNull
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.unregisterSource
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||
import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
@@ -43,7 +43,7 @@ object Source {
|
||||
fun getSourceList(): List<SourceDataClass> {
|
||||
return transaction {
|
||||
SourceTable.selectAll().mapNotNull {
|
||||
val catalogueSource = getCatalogueSourceOrNull(it[SourceTable.id].value) ?: return@mapNotNull null
|
||||
val catalogueSource = getSourceOrNull(it[SourceTable.id].value) ?: return@mapNotNull null
|
||||
val sourceExtension = ExtensionTable.selectAll().where { ExtensionTable.id eq it[SourceTable.extension] }.first()
|
||||
|
||||
SourceDataClass(
|
||||
@@ -64,7 +64,7 @@ object Source {
|
||||
fun getSource(sourceId: Long): SourceDataClass? { // all the data extracted fresh form the source instance
|
||||
return transaction {
|
||||
val source = SourceTable.selectAll().where { SourceTable.id eq sourceId }.firstOrNull() ?: return@transaction null
|
||||
val catalogueSource = getCatalogueSourceOrNull(sourceId) ?: return@transaction null
|
||||
val catalogueSource = getSourceOrNull(sourceId) ?: return@transaction null
|
||||
val extension = ExtensionTable.selectAll().where { ExtensionTable.id eq source[SourceTable.extension] }.first()
|
||||
|
||||
SourceDataClass(
|
||||
@@ -107,7 +107,7 @@ object Source {
|
||||
}
|
||||
|
||||
fun getSourcePreferencesRaw(sourceId: Long): List<Preference> {
|
||||
val source = getCatalogueSourceOrStub(sourceId)
|
||||
val source = getSourceOrStub(sourceId)
|
||||
|
||||
if (source is ConfigurableSource) {
|
||||
val sourceShardPreferences = source.sourcePreferences()
|
||||
@@ -157,7 +157,7 @@ object Source {
|
||||
pref.callChangeListener(newValue)
|
||||
|
||||
// must reload the source because a preference was changed
|
||||
unregisterCatalogueSource(sourceId)
|
||||
unregisterSource(sourceId)
|
||||
}
|
||||
|
||||
fun getSourcesMetaMaps(ids: List<Long>): Map<Long, Map<String, String>> =
|
||||
|
||||
@@ -8,7 +8,6 @@ package suwayomi.tachidesk.manga.impl.backup.proto.handlers
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
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.SortOrder
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
@@ -78,7 +77,7 @@ object BackupMangaHandler {
|
||||
lastModifiedAt = mangaRow[MangaTable.lastModifiedAt],
|
||||
version = mangaRow[MangaTable.version],
|
||||
initialized = mangaRow[MangaTable.initialized],
|
||||
memo = Json.encodeToString(mangaRow[MangaTable.memo]).encodeToByteArray(),
|
||||
memo = mangaRow[MangaTable.memo].encodeToByteArray(),
|
||||
)
|
||||
|
||||
val mangaId = mangaRow[MangaTable.id].value
|
||||
@@ -116,6 +115,7 @@ object BackupMangaHandler {
|
||||
sourceOrder = chapters.size - it[ChapterTable.sourceOrder],
|
||||
lastModifiedAt = it[ChapterTable.lastModifiedAt],
|
||||
version = it[ChapterTable.version],
|
||||
memo = it[ChapterTable.memo].encodeToByteArray(),
|
||||
).apply {
|
||||
if (flags.includeClientData) {
|
||||
this.meta = chapterToMeta[it[ChapterTable.id].value] ?: emptyMap()
|
||||
|
||||
@@ -14,6 +14,7 @@ import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.github.reactivecircus.cache4k.Cache
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
@@ -23,7 +24,7 @@ import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import suwayomi.tachidesk.manga.impl.ChapterDownloadHelper
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
@@ -77,7 +78,7 @@ suspend fun refreshChapterPageList(
|
||||
return mutex.withLock {
|
||||
val chapterEntry = existingChapterEntry ?: transaction { ChapterTable.selectAll().where { ChapterTable.id eq chapterId }.first() }
|
||||
val mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
val source = getSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
|
||||
val pageList =
|
||||
source
|
||||
@@ -88,6 +89,7 @@ suspend fun refreshChapterPageList(
|
||||
scanlator = chapterEntry[ChapterTable.scanlator]
|
||||
chapter_number = chapterEntry[ChapterTable.chapter_number]
|
||||
date_upload = chapterEntry[ChapterTable.date_upload]
|
||||
memo = Json.decodeFromString(chapterEntry[ChapterTable.memo])
|
||||
},
|
||||
).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
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import android.content.pm.PackageInfo
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||
@@ -18,9 +18,7 @@ import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import net.dongliu.apk.parser.ApkFile
|
||||
import net.dongliu.apk.parser.bean.Icon
|
||||
import okhttp3.CacheControl
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import okio.source
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
||||
import org.jetbrains.exposed.v1.jdbc.insert
|
||||
@@ -28,6 +26,7 @@ import org.jetbrains.exposed.v1.jdbc.select
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import suwayomi.tachidesk.manga.impl.util.AndroidManifestParser
|
||||
import suwayomi.tachidesk.manga.impl.util.PackageTools
|
||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.EXTENSION_FEATURE
|
||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MAX
|
||||
@@ -40,8 +39,9 @@ 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.getPackageInfo
|
||||
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.source.GetCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.clearCachedImage
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.saveImage
|
||||
@@ -49,220 +49,421 @@ import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||
import suwayomi.tachidesk.server.ApplicationDirs
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Path
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
import kotlin.io.path.ExperimentalPathApi
|
||||
import kotlin.io.path.Path
|
||||
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.relativeTo
|
||||
import kotlin.io.path.walk
|
||||
|
||||
object Extension {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
private val applicationDirs: ApplicationDirs by injectLazy()
|
||||
|
||||
suspend fun installExtension(pkgName: String): Int {
|
||||
private suspend fun fetchExtensionFile(url: String): Path {
|
||||
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" }
|
||||
val apkUrl =
|
||||
val extension =
|
||||
transaction {
|
||||
ExtensionTable
|
||||
.select(ExtensionTable.apkUrl)
|
||||
.select(ExtensionTable.apkUrl, ExtensionTable.jarUrl)
|
||||
.where { ExtensionTable.pkgName eq pkgName }
|
||||
.firstOrNull()
|
||||
?.get(ExtensionTable.apkUrl)
|
||||
} ?: throw NullPointerException("Could not find extension $pkgName")
|
||||
} ?: throw NullPointerException("Could not find extension for $pkgName")
|
||||
val jarUrl = extension[ExtensionTable.jarUrl]
|
||||
val apkUrl = extension[ExtensionTable.apkUrl]
|
||||
|
||||
return installAPK {
|
||||
val apkName = Uri.parse(apkUrl).lastPathSegment!!
|
||||
val apkSavePath = "${applicationDirs.extensionsRoot}/$apkName"
|
||||
// download apk file
|
||||
downloadAPKFile(apkUrl, apkSavePath)
|
||||
return when {
|
||||
jarUrl != null -> {
|
||||
installExtension {
|
||||
val jar = fetchExtensionFile(jarUrl)
|
||||
val manifest = extractAndParseAndroidManifest(jar)
|
||||
ExtensionPackage.Jar(jar, manifest)
|
||||
}
|
||||
}
|
||||
|
||||
apkSavePath
|
||||
apkUrl != null -> {
|
||||
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(
|
||||
inputStream: InputStream,
|
||||
apkName: String,
|
||||
): Int =
|
||||
installAPK(true) {
|
||||
val rootPath = Path(applicationDirs.extensionsRoot)
|
||||
val downloadedFile = rootPath.resolve(apkName).normalize()
|
||||
check(downloadedFile.startsWith(rootPath) && downloadedFile.parent == rootPath) {
|
||||
"File '$apkName' is not a valid extension file"
|
||||
}
|
||||
logger.debug { "Saving apk at $apkName" }
|
||||
// download apk file
|
||||
downloadedFile.outputStream().sink().buffer().use { sink ->
|
||||
inputStream.source().use { source ->
|
||||
sink.writeAll(source)
|
||||
sink.flush()
|
||||
}
|
||||
}
|
||||
downloadedFile.absolutePathString()
|
||||
}
|
||||
|
||||
suspend fun installAPK(
|
||||
forceReinstall: Boolean = false,
|
||||
fetcher: suspend () -> String,
|
||||
): Int {
|
||||
val apkFilePath = fetcher()
|
||||
val apkName = File(apkFilePath).name
|
||||
|
||||
// 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 =
|
||||
transaction {
|
||||
ExtensionTable.selectAll().where { ExtensionTable.apkName eq apkName }.firstOrNull()
|
||||
}?.get(ExtensionTable.isInstalled) ?: false
|
||||
|
||||
val fileNameWithoutType = apkName.substringBefore(".apk")
|
||||
|
||||
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType"
|
||||
val jarFilePath = "$dirPathWithoutType.jar"
|
||||
|
||||
val packageInfo = getPackageInfo(apkFilePath)
|
||||
val pkgName = packageInfo.packageName
|
||||
if (isInstalled && forceReinstall) {
|
||||
uninstallExtension(pkgName)
|
||||
}
|
||||
|
||||
if (!isInstalled || forceReinstall) {
|
||||
if (!packageInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }) {
|
||||
throw Exception("This apk is not a Tachiyomi extension")
|
||||
}
|
||||
|
||||
// Validate lib version
|
||||
val libVersion = packageInfo.versionName.substringBeforeLast('.').toDouble()
|
||||
if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
|
||||
throw Exception(
|
||||
"Lib version is $libVersion, while only versions " +
|
||||
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed",
|
||||
extensionName: String,
|
||||
): String =
|
||||
when {
|
||||
extensionName.endsWith(".jar") -> {
|
||||
installExtension(
|
||||
true,
|
||||
{
|
||||
val jar = copyToExtensionsRoot(inputStream, extensionName)
|
||||
val manifest = extractAndParseAndroidManifest(jar)
|
||||
ExtensionPackage.Jar(jar, manifest)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: allow trusting keys
|
||||
// val signatureHash = getSignatureHash(packageInfo)
|
||||
|
||||
// if (signatureHash == null) {
|
||||
// throw Exception("Package $pkgName isn't signed")
|
||||
// } else if (signatureHash !in trustedSignatures) {
|
||||
// throw Exception("This apk is not a signed with the official tachiyomi signature")
|
||||
// }
|
||||
|
||||
var contentWarning = packageInfo.applicationInfo.metaData.getInt(METADATA_CONTENT_WARNING)
|
||||
if (contentWarning == 0) {
|
||||
contentWarning = packageInfo.applicationInfo.metaData.getInt(METADATA_NSFW)
|
||||
extensionName.endsWith(".apk") -> {
|
||||
installExtension(
|
||||
true,
|
||||
{
|
||||
val apk = copyToExtensionsRoot(inputStream, extensionName)
|
||||
val packageInfo = getPackageInfo(apk)
|
||||
ExtensionPackage.Apk(apk, packageInfo)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
val className =
|
||||
packageInfo.packageName + packageInfo.applicationInfo.metaData.getString(METADATA_SOURCE_CLASS)
|
||||
else -> {
|
||||
throw NullPointerException("Could not find extension type for $extensionName")
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug { "Main class for extension is $className" }
|
||||
sealed class ExtensionPackage {
|
||||
abstract val file: Path
|
||||
abstract val metadata: PackageMetadata
|
||||
|
||||
dex2jar(apkFilePath, jarFilePath, fileNameWithoutType)
|
||||
extractAssetsFromApk(apkFilePath, jarFilePath)
|
||||
extractAndCacheApkIcon(apkFilePath, packageInfo.packageName)
|
||||
// Abstract hook for type-specific preprocessing
|
||||
abstract suspend fun prepareJarAndIcons(extensionsRoot: Path): Path
|
||||
|
||||
// clean up
|
||||
File(apkFilePath).delete()
|
||||
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(),
|
||||
)
|
||||
|
||||
try {
|
||||
// collect sources from the extension
|
||||
val extensionMainClassInstance = loadExtensionSources(jarFilePath, className)
|
||||
val sources: List<CatalogueSource> =
|
||||
when (extensionMainClassInstance) {
|
||||
is Source -> listOf(extensionMainClassInstance)
|
||||
is SourceFactory -> extensionMainClassInstance.createSources()
|
||||
else -> throw RuntimeException("Unknown source class type! ${extensionMainClassInstance.javaClass}")
|
||||
}.map { it as CatalogueSource }
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
val langs = sources.map { it.lang }.toSet()
|
||||
val extensionLang =
|
||||
when (langs.size) {
|
||||
0 -> ""
|
||||
1 -> langs.first()
|
||||
else -> "all"
|
||||
}
|
||||
class Jar(
|
||||
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,
|
||||
)
|
||||
|
||||
val extensionName =
|
||||
packageInfo.applicationInfo.metaData.getString(METADATA_NAME)
|
||||
?: packageInfo.applicationInfo.nonLocalizedLabel
|
||||
.toString()
|
||||
.substringAfter("Tachiyomi: ")
|
||||
override suspend fun prepareJarAndIcons(extensionsRoot: Path): Path {
|
||||
val jarFile = extensionsRoot / file.name
|
||||
|
||||
val extensionLibVersion =
|
||||
packageInfo.applicationInfo.metaData
|
||||
.getString(METADATA_EXTENSION_LIB)
|
||||
.takeUnless { it == "0" }
|
||||
?: packageInfo.versionName.substringBeforeLast('.')
|
||||
|
||||
// update extension info
|
||||
transaction {
|
||||
if (ExtensionTable.selectAll().where { ExtensionTable.pkgName eq pkgName }.firstOrNull() == null) {
|
||||
ExtensionTable.insert {
|
||||
it[this.apkName] = apkName
|
||||
it[name] = extensionName
|
||||
it[this.pkgName] = packageInfo.packageName
|
||||
it[versionName] = packageInfo.versionName
|
||||
it[versionCode] = packageInfo.versionCode.toLong()
|
||||
it[extensionLib] = extensionLibVersion
|
||||
it[lang] = extensionLang
|
||||
it[this.contentWarning] = contentWarning
|
||||
}
|
||||
}
|
||||
|
||||
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
|
||||
it[this.apkName] = apkName
|
||||
it[this.isInstalled] = true
|
||||
it[this.classFQName] = className
|
||||
it[versionName] = packageInfo.versionName
|
||||
it[versionCode] = packageInfo.versionCode.toLong()
|
||||
}
|
||||
|
||||
val extensionId =
|
||||
ExtensionTable
|
||||
.selectAll()
|
||||
.where { ExtensionTable.pkgName eq pkgName }
|
||||
.first()[ExtensionTable.id]
|
||||
.value
|
||||
|
||||
sources.forEach { httpSource ->
|
||||
SourceTable.insert {
|
||||
it[id] = httpSource.id
|
||||
it[name] = httpSource.name
|
||||
it[lang] = httpSource.lang
|
||||
it[extension] = extensionId
|
||||
it[this.contentWarning] = contentWarning
|
||||
}
|
||||
logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" }
|
||||
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}" }
|
||||
}
|
||||
}
|
||||
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
|
||||
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,
|
||||
fetchPackage: suspend () -> ExtensionPackage,
|
||||
): String {
|
||||
val extPackage = fetchPackage()
|
||||
val metadata = extPackage.metadata
|
||||
val pkgName = metadata.packageName
|
||||
|
||||
val isInstalled =
|
||||
transaction {
|
||||
ExtensionTable
|
||||
.select(ExtensionTable.isInstalled)
|
||||
.where { ExtensionTable.pkgName eq pkgName }
|
||||
.firstOrNull()
|
||||
}?.get(ExtensionTable.isInstalled) ?: false
|
||||
|
||||
if (isInstalled) {
|
||||
if (forceReinstall) {
|
||||
uninstallExtension(pkgName)
|
||||
} else {
|
||||
extPackage.file.deleteExisting()
|
||||
return pkgName
|
||||
}
|
||||
}
|
||||
|
||||
if (!metadata.reqFeatures.contains(EXTENSION_FEATURE)) {
|
||||
extPackage.file.deleteExisting()
|
||||
throw Exception("This file is not a Tachiyomi extension")
|
||||
}
|
||||
|
||||
val libVersion = metadata.versionName.substringBeforeLast('.').toDouble()
|
||||
if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
|
||||
extPackage.file.deleteExisting()
|
||||
throw Exception(
|
||||
"Lib version is $libVersion, while only versions " +
|
||||
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed",
|
||||
)
|
||||
}
|
||||
|
||||
var contentWarning =
|
||||
extPackage.metadata
|
||||
.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 =
|
||||
metadata.metaData
|
||||
.getString(METADATA_SOURCE_CLASS)!!
|
||||
.trim()
|
||||
|
||||
val className =
|
||||
if (sourceClass.startsWith(".")) {
|
||||
pkgName + sourceClass
|
||||
} else {
|
||||
sourceClass
|
||||
}
|
||||
|
||||
logger.debug { "Main class for extension is $className" }
|
||||
|
||||
val extensionsRoot = Path(applicationDirs.extensionsRoot)
|
||||
val jarFile = extPackage.prepareJarAndIcons(extensionsRoot)
|
||||
|
||||
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
|
||||
val extensionMainClassInstance = loadExtensionSources(jarFile, className)
|
||||
val sources: List<Source> =
|
||||
when (extensionMainClassInstance) {
|
||||
is Source -> listOf(extensionMainClassInstance)
|
||||
is SourceFactory -> extensionMainClassInstance.createSources()
|
||||
else -> throw RuntimeException("Unknown source class type! ${extensionMainClassInstance.javaClass}")
|
||||
}
|
||||
|
||||
val langs = sources.map { it.lang }.toSet()
|
||||
val extensionLang =
|
||||
when (langs.size) {
|
||||
0 -> ""
|
||||
1 -> langs.first()
|
||||
else -> "all"
|
||||
}
|
||||
|
||||
// update extension info
|
||||
transaction {
|
||||
if (ExtensionTable.selectAll().where { ExtensionTable.pkgName eq pkgName }.firstOrNull() == null) {
|
||||
ExtensionTable.insert {
|
||||
it[this.apkName] = apkName
|
||||
it[name] = extensionName
|
||||
it[this.pkgName] = pkgName
|
||||
it[this.versionName] = versionName
|
||||
it[this.versionCode] = versionCode.toLong()
|
||||
it[extensionLib] = extensionLibVersion
|
||||
it[lang] = extensionLang
|
||||
it[this.contentWarning] = contentWarning
|
||||
}
|
||||
}
|
||||
|
||||
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
|
||||
it[this.apkName] = apkName
|
||||
it[this.isInstalled] = true
|
||||
it[this.classFQName] = className
|
||||
it[this.versionName] = versionName
|
||||
it[this.versionCode] = versionCode.toLong()
|
||||
}
|
||||
|
||||
val extensionId =
|
||||
ExtensionTable
|
||||
.selectAll()
|
||||
.where { ExtensionTable.pkgName eq pkgName }
|
||||
.first()[ExtensionTable.id]
|
||||
.value
|
||||
|
||||
sources.forEach { httpSource ->
|
||||
SourceTable.insert {
|
||||
it[id] = httpSource.id
|
||||
it[name] = httpSource.name
|
||||
it[lang] = httpSource.lang
|
||||
it[extension] = extensionId
|
||||
it[this.contentWarning] = contentWarning
|
||||
}
|
||||
logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" }
|
||||
}
|
||||
} else {
|
||||
return 302 // extension was already installed
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractAndCacheApkIcon(
|
||||
apkFilePath: String,
|
||||
apkFile: Path,
|
||||
pkgName: String,
|
||||
) {
|
||||
val iconCacheDir = "${applicationDirs.extensionsRoot}/icon"
|
||||
try {
|
||||
val iconData =
|
||||
ApkFile(File(apkFilePath)).use { apk ->
|
||||
ApkFile(apkFile.toFile()).use { apk ->
|
||||
apk.allIcons
|
||||
.filterIsInstance<Icon>()
|
||||
.mapNotNull { it.data?.let { data -> data to it.density } }
|
||||
@@ -273,31 +474,36 @@ object Extension {
|
||||
logger.warn { "No icon found in APK $pkgName" }
|
||||
return
|
||||
}
|
||||
|
||||
File(iconCacheDir).mkdirs()
|
||||
clearCachedImage(iconCacheDir, pkgName)
|
||||
saveImage("$iconCacheDir/$pkgName", iconData.inputStream(), null)
|
||||
cacheIcon(pkgName, iconData.inputStream())
|
||||
} catch (e: Exception) {
|
||||
logger.warn(e) { "Failed to extract icon from APK $pkgName" }
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractAssetsFromApk(
|
||||
apkPath: String,
|
||||
jarPath: String,
|
||||
private fun cacheIcon(
|
||||
pkgName: String,
|
||||
inputStream: InputStream,
|
||||
) {
|
||||
val apkFile = File(apkPath)
|
||||
val jarFile = File(jarPath)
|
||||
val iconCacheDir = Path(applicationDirs.extensionsRoot) / "icon"
|
||||
iconCacheDir.createDirectories()
|
||||
clearCachedImage(iconCacheDir.absolutePathString(), pkgName)
|
||||
saveImage("$iconCacheDir/$pkgName", inputStream, null)
|
||||
}
|
||||
|
||||
val assetsFolder = File("${apkFile.parent}/${apkFile.nameWithoutExtension}_assets")
|
||||
assetsFolder.mkdir()
|
||||
@OptIn(ExperimentalPathApi::class)
|
||||
private fun extractAssetsFromApk(
|
||||
apkFile: Path,
|
||||
jarFile: Path,
|
||||
) {
|
||||
val assetsFolder = apkFile.parent / "${apkFile.nameWithoutExtension}_assets"
|
||||
assetsFolder.createDirectories()
|
||||
ZipInputStream(apkFile.inputStream()).use { zipInputStream ->
|
||||
var zipEntry = zipInputStream.nextEntry
|
||||
while (zipEntry != null) {
|
||||
if (zipEntry.name.startsWith("assets/") && !zipEntry.isDirectory) {
|
||||
val assetFile = File(assetsFolder, zipEntry.name)
|
||||
assetFile.parentFile.mkdirs()
|
||||
FileOutputStream(assetFile).use { outputStream ->
|
||||
val assetFile = assetsFolder / zipEntry.name
|
||||
assetFile.createParentDirectories()
|
||||
assetFile.outputStream().use { outputStream ->
|
||||
zipInputStream.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
@@ -305,9 +511,9 @@ object Extension {
|
||||
}
|
||||
}
|
||||
|
||||
val tempJarFile = File("${jarFile.parent}/${jarFile.nameWithoutExtension}_temp.jar")
|
||||
val tempJarFile = jarFile.parent / "${jarFile.nameWithoutExtension}_temp.jar"
|
||||
ZipInputStream(jarFile.inputStream()).use { jarZipInputStream ->
|
||||
ZipOutputStream(FileOutputStream(tempJarFile)).use { jarZipOutputStream ->
|
||||
ZipOutputStream(tempJarFile.outputStream()).use { jarZipOutputStream ->
|
||||
var zipEntry = jarZipInputStream.nextEntry
|
||||
while (zipEntry != null) {
|
||||
if (!zipEntry.name.startsWith("META-INF/")) {
|
||||
@@ -316,8 +522,8 @@ object Extension {
|
||||
}
|
||||
zipEntry = jarZipInputStream.nextEntry
|
||||
}
|
||||
assetsFolder.walkTopDown().forEach { file ->
|
||||
if (file.isFile) {
|
||||
assetsFolder.walk().forEach { file ->
|
||||
if (file.isRegularFile()) {
|
||||
jarZipOutputStream.putNextEntry(ZipEntry(file.relativeTo(assetsFolder).toString().replace("\\", "/")))
|
||||
file.inputStream().use { inputStream ->
|
||||
inputStream.copyTo(jarZipOutputStream)
|
||||
@@ -328,17 +534,25 @@ object Extension {
|
||||
}
|
||||
}
|
||||
|
||||
jarFile.delete()
|
||||
tempJarFile.renameTo(jarFile)
|
||||
jarFile.deleteIfExists()
|
||||
tempJarFile.copyTo(jarFile)
|
||||
tempJarFile.deleteExisting()
|
||||
|
||||
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 suspend fun downloadAPKFile(
|
||||
private suspend fun downloadExtension(
|
||||
url: String,
|
||||
savePath: String,
|
||||
savePath: Path,
|
||||
) {
|
||||
val response =
|
||||
network.client
|
||||
@@ -346,11 +560,10 @@ object Extension {
|
||||
GET(url, cache = CacheControl.FORCE_NETWORK),
|
||||
).await()
|
||||
|
||||
val downloadedFile = File(savePath)
|
||||
downloadedFile.sink().buffer().use { sink ->
|
||||
response.body.source().use { source ->
|
||||
sink.writeAll(source)
|
||||
sink.flush()
|
||||
savePath.createParentDirectories()
|
||||
response.body.byteStream().use {
|
||||
savePath.outputStream().buffered().use { out ->
|
||||
it.copyTo(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -362,7 +575,7 @@ object Extension {
|
||||
val fileNameWithoutType =
|
||||
extensionRecord[ExtensionTable.apkName]?.substringBefore(".apk")
|
||||
?: throw NullPointerException("Missing $pkgName apkName")
|
||||
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
||||
val jarPath = Path(applicationDirs.extensionsRoot) / "$fileNameWithoutType.jar"
|
||||
val sources =
|
||||
transaction {
|
||||
val extensionId = extensionRecord[ExtensionTable.id].value
|
||||
@@ -384,18 +597,18 @@ object Extension {
|
||||
sources
|
||||
}
|
||||
|
||||
if (File(jarPath).exists()) {
|
||||
if (jarPath.exists()) {
|
||||
// free up the file descriptor if exists
|
||||
PackageTools.jarLoaderMap.remove(jarPath)?.close()
|
||||
PackageTools.jarLoaderMap.remove(jarPath.absolutePathString())?.close()
|
||||
|
||||
// clear all loaded sources
|
||||
sources.forEach { GetCatalogueSource.unregisterCatalogueSource(it) }
|
||||
sources.forEach { GetSource.unregisterSource(it) }
|
||||
|
||||
File(jarPath).delete()
|
||||
jarPath.deleteExisting()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateExtension(pkgName: String): Int {
|
||||
suspend fun updateExtension(pkgName: String): String {
|
||||
val targetExtension = ExtensionsList.updateMap.remove(pkgName)!!
|
||||
uninstallExtension(pkgName)
|
||||
transaction {
|
||||
|
||||
@@ -132,6 +132,8 @@ object ExtensionsList {
|
||||
// Always update icon url and repo
|
||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||
this[ExtensionTable.storeIndexUrl] = foundExtension.storeIndexUrl
|
||||
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
||||
this[ExtensionTable.jarUrl] = foundExtension.jarUrl
|
||||
|
||||
// add these because batch updates need matching columns
|
||||
this[ExtensionTable.hasUpdate] = extensionRecord[ExtensionTable.hasUpdate]
|
||||
@@ -175,6 +177,7 @@ object ExtensionsList {
|
||||
this[ExtensionTable.lang] = foundExtension.lang
|
||||
this[ExtensionTable.contentWarning] = foundExtension.contentWarning.ordinal
|
||||
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
||||
this[ExtensionTable.jarUrl] = foundExtension.jarUrl
|
||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||
}
|
||||
}.toExecutable()
|
||||
@@ -192,6 +195,7 @@ object ExtensionsList {
|
||||
this[ExtensionTable.lang] = foundExtension.lang
|
||||
this[ExtensionTable.contentWarning] = foundExtension.contentWarning.ordinal
|
||||
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
||||
this[ExtensionTable.jarUrl] = foundExtension.jarUrl
|
||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,8 @@ data class NetworkExtensionStore(
|
||||
data class Resources(
|
||||
@ProtoNumber(1) val apkUrl: String,
|
||||
@ProtoNumber(2) val iconUrl: String,
|
||||
// Keiyoushi specific output
|
||||
@ProtoNumber(501) val jarUrl: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -109,6 +111,7 @@ fun NetworkExtensionStore.ExtensionList.toExtensionInfos(store: ExtensionStore):
|
||||
name = extension.name,
|
||||
pkgName = extension.packageName,
|
||||
apkUrl = extension.resources.apkUrl,
|
||||
jarUrl = extension.resources.jarUrl,
|
||||
iconUrl = extension.resources.iconUrl,
|
||||
extensionLib = extension.extensionLib,
|
||||
versionCode = extension.versionCode,
|
||||
|
||||
@@ -44,6 +44,7 @@ fun NetworkLegacyExtension.toExtensionInfo(
|
||||
name = name.substringAfter("Tachiyomi: "),
|
||||
pkgName = pkg,
|
||||
apkUrl = "$storeBaseUrl/apk/$apk",
|
||||
jarUrl = null,
|
||||
iconUrl = "$storeBaseUrl/icon/$pkg.png",
|
||||
extensionLib = version.substringBeforeLast('.'),
|
||||
versionCode = code,
|
||||
|
||||
@@ -222,6 +222,48 @@ object Track {
|
||||
}
|
||||
}
|
||||
|
||||
fun bindTrackRecord(
|
||||
mangaId: Int,
|
||||
trackRecordId: Int,
|
||||
): Int {
|
||||
val (trackRecord, existingTrackRecord) =
|
||||
transaction {
|
||||
val trackRecord =
|
||||
TrackRecordTable
|
||||
.selectAll()
|
||||
.where {
|
||||
(TrackRecordTable.id eq trackRecordId)
|
||||
}.first()
|
||||
.toTrackRecordDataClass()
|
||||
|
||||
val existingTrackRecord =
|
||||
TrackRecordTable
|
||||
.selectAll()
|
||||
.where {
|
||||
(TrackRecordTable.mangaId eq mangaId) and (TrackRecordTable.trackerId eq trackRecord.trackerId)
|
||||
}.firstOrNull()
|
||||
?.toTrackRecordDataClass()
|
||||
|
||||
trackRecord to existingTrackRecord
|
||||
}
|
||||
|
||||
val isAlreadyBoundToManga = trackRecord.mangaId == mangaId
|
||||
if (isAlreadyBoundToManga) {
|
||||
return trackRecordId
|
||||
}
|
||||
|
||||
val hasRecordForTracker = existingTrackRecord != null
|
||||
if (hasRecordForTracker) {
|
||||
val updatedTrack = trackRecord.copy(id = existingTrackRecord.id, mangaId = mangaId).toTrack()
|
||||
|
||||
return updateTrackRecord(updatedTrack)
|
||||
}
|
||||
|
||||
val newTrack = trackRecord.copy(mangaId = mangaId).toTrack()
|
||||
|
||||
return insertTrackRecord(newTrack)
|
||||
}
|
||||
|
||||
suspend fun refresh(recordId: Int) {
|
||||
val recordDb =
|
||||
transaction {
|
||||
@@ -423,9 +465,9 @@ object Track {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTrackRecord(track: Track) = updateTrackRecords(listOf(track))
|
||||
fun updateTrackRecord(track: Track): Int = updateTrackRecords(listOf(track)).first()
|
||||
|
||||
fun updateTrackRecords(tracks: List<Track>) =
|
||||
fun updateTrackRecords(tracks: List<Track>): List<Int> =
|
||||
transaction {
|
||||
if (tracks.isNotEmpty()) {
|
||||
BatchUpdateStatement(TrackRecordTable)
|
||||
@@ -447,6 +489,8 @@ object Track {
|
||||
}.toExecutable()
|
||||
.execute(this@transaction)
|
||||
}
|
||||
|
||||
tracks.map { it.id!! }
|
||||
}
|
||||
|
||||
fun insertTrackRecord(track: Track): Int = insertTrackRecords(listOf(track)).first()
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
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.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
import suwayomi.tachidesk.server.ApplicationDirs
|
||||
@@ -37,7 +37,7 @@ private fun getMangaDir(
|
||||
private fun getMangaDir(mangaId: Int): String =
|
||||
transaction {
|
||||
val mangaEntry = MangaTable.selectAll().where { MangaTable.id eq mangaId }.first()
|
||||
val source = GetCatalogueSource.getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
val source = GetSource.getSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
|
||||
getMangaDir(mangaEntry[MangaTable.title], source.toString())
|
||||
}
|
||||
|
||||
@@ -23,13 +23,14 @@ import suwayomi.tachidesk.server.ApplicationDirs
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import xyz.nulldev.androidcompat.pm.InstalledPackage.Companion.toList
|
||||
import xyz.nulldev.androidcompat.pm.toPackageInfo
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
import java.net.URLClassLoader
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
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
|
||||
|
||||
object PackageTools {
|
||||
@@ -52,15 +53,13 @@ object PackageTools {
|
||||
* Convert dex to jar, a wrapper for the dex2jar library
|
||||
*/
|
||||
fun dex2jar(
|
||||
dexFile: String,
|
||||
jarFile: String,
|
||||
fileNameWithoutType: String,
|
||||
dexFile: Path,
|
||||
jarFile: Path,
|
||||
) {
|
||||
// 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
|
||||
|
||||
val jarFilePath = File(jarFile).toPath()
|
||||
val reader = MultiDexFileReader.open(Files.readAllBytes(File(dexFile).toPath()))
|
||||
val reader = MultiDexFileReader.open(dexFile.readBytes())
|
||||
val handler = BaksmaliBaseDexExceptionHandler()
|
||||
Dex2jar
|
||||
.from(reader)
|
||||
@@ -73,10 +72,10 @@ object PackageTools {
|
||||
.noCode(false)
|
||||
.skipExceptions(false)
|
||||
.dontSanitizeNames(true)
|
||||
.to(jarFilePath)
|
||||
.to(jarFile)
|
||||
if (handler.hasException()) {
|
||||
val rootPath = Path(applicationDirs.extensionsRoot)
|
||||
val errorFile: Path = rootPath.resolve("$fileNameWithoutType-error.txt")
|
||||
val errorFile: Path = rootPath.resolve("${dexFile.nameWithoutExtension}-error.txt")
|
||||
logger.error {
|
||||
"""
|
||||
Detail Error Information in File ${errorFile.relativeTo(rootPath)}
|
||||
@@ -89,56 +88,55 @@ object PackageTools {
|
||||
}
|
||||
handler.dump(errorFile, emptyArray<String>())
|
||||
} else {
|
||||
BytecodeEditor.fixAndroidClasses(jarFilePath)
|
||||
BytecodeEditor.fixAndroidClasses(jarFile)
|
||||
}
|
||||
}
|
||||
|
||||
/** A modified version of `xyz.nulldev.androidcompat.pm.InstalledPackage.info` */
|
||||
fun getPackageInfo(apkFilePath: String): PackageInfo {
|
||||
val apk = File(apkFilePath)
|
||||
return ApkParsers.getMetaInfo(apk).toPackageInfo(apk).apply {
|
||||
val parsed = ApkFile(apk)
|
||||
val dbFactory = DocumentBuilderFactory.newInstance()
|
||||
val dBuilder = dbFactory.newDocumentBuilder()
|
||||
val doc =
|
||||
parsed.manifestXml.byteInputStream().use {
|
||||
dBuilder.parse(it)
|
||||
}
|
||||
fun getPackageInfo(apkFile: Path): PackageInfo =
|
||||
ApkParsers.getMetaInfo(apkFile.toFile()).toPackageInfo(apkFile.toFile()).apply {
|
||||
ApkFile(apkFile.toFile()).use { parsed ->
|
||||
val dbFactory = DocumentBuilderFactory.newInstance()
|
||||
val dBuilder = dbFactory.newDocumentBuilder()
|
||||
val doc =
|
||||
parsed.manifestXml.byteInputStream().use {
|
||||
dBuilder.parse(it)
|
||||
}
|
||||
|
||||
logger.trace { parsed.manifestXml }
|
||||
logger.trace { parsed.manifestXml }
|
||||
|
||||
applicationInfo.metaData =
|
||||
Bundle().apply {
|
||||
val appTag = doc.getElementsByTagName("application").item(0)
|
||||
applicationInfo.metaData =
|
||||
Bundle().apply {
|
||||
val appTag = doc.getElementsByTagName("application").item(0)
|
||||
|
||||
appTag
|
||||
?.childNodes
|
||||
?.toList()
|
||||
.orEmpty()
|
||||
.asSequence()
|
||||
.filter {
|
||||
it.nodeType == Node.ELEMENT_NODE
|
||||
}.map {
|
||||
it as Element
|
||||
}.filter {
|
||||
it.tagName == "meta-data"
|
||||
}.forEach {
|
||||
putString(
|
||||
it.attributes.getNamedItem("android:name").nodeValue,
|
||||
it.attributes.getNamedItem("android:value").nodeValue,
|
||||
)
|
||||
}
|
||||
}
|
||||
appTag
|
||||
?.childNodes
|
||||
?.toList()
|
||||
.orEmpty()
|
||||
.asSequence()
|
||||
.filter {
|
||||
it.nodeType == Node.ELEMENT_NODE
|
||||
}.map {
|
||||
it as Element
|
||||
}.filter {
|
||||
it.tagName == "meta-data"
|
||||
}.forEach {
|
||||
putString(
|
||||
it.attributes.getNamedItem("android:name").nodeValue,
|
||||
it.attributes.getNamedItem("android:value").nodeValue,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
signatures =
|
||||
(
|
||||
parsed.apkSingers.flatMap { it.certificateMetas }
|
||||
// + parsed.apkV2Singers.flatMap { it.certificateMetas }
|
||||
) // Blocked by: https://github.com/hsiafan/apk-parser/issues/72
|
||||
.map { Signature(it.data) }
|
||||
.toTypedArray()
|
||||
signatures =
|
||||
(
|
||||
parsed.apkSingers.flatMap { it.certificateMetas }
|
||||
// + parsed.apkV2Singers.flatMap { it.certificateMetas }
|
||||
) // Blocked by: https://github.com/hsiafan/apk-parser/issues/72
|
||||
.map { Signature(it.data) }
|
||||
.toTypedArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getSignatureHash(pkgInfo: PackageInfo): String? {
|
||||
val signatures = pkgInfo.signatures
|
||||
@@ -156,19 +154,19 @@ object PackageTools {
|
||||
* It may return an instance of HttpSource or SourceFactory depending on the extension.
|
||||
*/
|
||||
fun loadExtensionSources(
|
||||
jarPath: String,
|
||||
jar: Path,
|
||||
className: String,
|
||||
): Any {
|
||||
try {
|
||||
logger.debug { "loading jar with path: $jarPath" }
|
||||
val classLoader = jarLoaderMap[jarPath] ?: ChildFirstURLClassLoader(arrayOf<URL>(Path(jarPath).toUri().toURL()))
|
||||
logger.debug { "loading jar with path: ${jar.absolutePathString()}" }
|
||||
val classLoader = jarLoaderMap[jar.absolutePathString()] ?: ChildFirstURLClassLoader(arrayOf<URL>(jar.toUri().toURL()))
|
||||
val classToLoad = Class.forName(className, false, classLoader)
|
||||
|
||||
jarLoaderMap[jarPath] = classLoader
|
||||
jarLoaderMap[jar.absolutePathString()] = classLoader
|
||||
|
||||
return classToLoad.getDeclaredConstructor().newInstance()
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Failed to load jar with path: $jarPath" }
|
||||
logger.error(e) { "Failed to load jar with path: ${jar.absolutePathString()}" }
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
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,7 +7,6 @@ package suwayomi.tachidesk.manga.impl.util.source
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* 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.SourceFactory
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
@@ -21,15 +20,17 @@ import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||
import suwayomi.tachidesk.server.ApplicationDirs
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.io.path.Path
|
||||
import kotlin.io.path.div
|
||||
|
||||
object GetCatalogueSource {
|
||||
object GetSource {
|
||||
private val logger = KotlinLogging.logger { }
|
||||
|
||||
private val sourceCache = ConcurrentHashMap<Long, CatalogueSource>()
|
||||
private val sourceCache = ConcurrentHashMap<Long, Source>()
|
||||
private val applicationDirs: ApplicationDirs by injectLazy()
|
||||
|
||||
private fun getCatalogueSource(sourceId: Long): CatalogueSource? {
|
||||
val cachedResult: CatalogueSource? = sourceCache[sourceId]
|
||||
private fun getSource(sourceId: Long): Source? {
|
||||
val cachedResult: Source? = sourceCache[sourceId]
|
||||
if (cachedResult != null) {
|
||||
return cachedResult
|
||||
}
|
||||
@@ -50,7 +51,7 @@ object GetCatalogueSource {
|
||||
?: throw NullPointerException("Missing apkName")
|
||||
val className = extensionRecord[ExtensionTable.classFQName]
|
||||
val jarName = apkName.substringBefore(".apk") + ".jar"
|
||||
val jarPath = "${applicationDirs.extensionsRoot}/$jarName"
|
||||
val jarPath = Path(applicationDirs.extensionsRoot) / jarName
|
||||
|
||||
when (val instance = loadExtensionSources(jarPath, className)) {
|
||||
is Source -> listOf(instance)
|
||||
@@ -62,25 +63,25 @@ object GetCatalogueSource {
|
||||
return sourceCache[sourceId]!!
|
||||
}
|
||||
|
||||
fun getCatalogueSourceOrNull(sourceId: Long): CatalogueSource? =
|
||||
fun getSourceOrNull(sourceId: Long): Source? =
|
||||
try {
|
||||
getCatalogueSource(sourceId)
|
||||
getSource(sourceId)
|
||||
} catch (e: Exception) {
|
||||
logger.warn(e) { "getCatalogueSource($sourceId) failed" }
|
||||
null
|
||||
}
|
||||
|
||||
fun getCatalogueSourceOrStub(sourceId: Long): CatalogueSource = getCatalogueSourceOrNull(sourceId) ?: StubSource(sourceId)
|
||||
fun getSourceOrStub(sourceId: Long): Source = getSourceOrNull(sourceId) ?: StubSource(sourceId)
|
||||
|
||||
fun registerCatalogueSource(sourcePair: Pair<Long, CatalogueSource>) {
|
||||
fun registerSource(sourcePair: Pair<Long, Source>) {
|
||||
sourceCache += sourcePair
|
||||
}
|
||||
|
||||
fun unregisterCatalogueSource(sourceId: Long) {
|
||||
fun unregisterSource(sourceId: Long) {
|
||||
sourceCache.remove(sourceId)
|
||||
}
|
||||
|
||||
fun unregisterAllCatalogueSources() {
|
||||
fun unregisterAllSources() {
|
||||
(sourceCache - 0L).forEach { (id, _) ->
|
||||
sourceCache.remove(id)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ data class ExtensionInfo(
|
||||
val name: String,
|
||||
val pkgName: String,
|
||||
val apkUrl: String,
|
||||
val jarUrl: String?,
|
||||
val iconUrl: String,
|
||||
val extensionLib: String,
|
||||
val versionCode: Long,
|
||||
|
||||
@@ -24,6 +24,7 @@ object ExtensionTable : IntIdTable() {
|
||||
val name = varchar("name", 128)
|
||||
val pkgName = varchar("pkg_name", 128)
|
||||
val apkUrl = varchar("apk_url", 2048).nullable()
|
||||
val jarUrl = varchar("jar_url", 2048).nullable()
|
||||
val extensionLib = varchar("extension_lib", 16).nullable()
|
||||
val versionName = varchar("version_name", 16)
|
||||
val versionCode = long("version_code")
|
||||
|
||||
@@ -22,7 +22,7 @@ import org.jetbrains.exposed.v1.jdbc.andWhere
|
||||
import org.jetbrains.exposed.v1.jdbc.select
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import suwayomi.tachidesk.manga.impl.MangaList.insertOrUpdate
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
||||
import suwayomi.tachidesk.manga.model.dataclass.toGenreList
|
||||
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
|
||||
import suwayomi.tachidesk.manga.model.table.CategoryTable
|
||||
@@ -231,7 +231,7 @@ object MangaRepository {
|
||||
pageNum: Int,
|
||||
sort: String,
|
||||
): Pair<List<OpdsMangaAcqEntry>, Boolean> {
|
||||
val source = GetCatalogueSource.getCatalogueSourceOrStub(sourceId)
|
||||
val source = GetSource.getSourceOrStub(sourceId)
|
||||
val mangasPage: MangasPage =
|
||||
if (sort == "latest" && source.supportsLatest) {
|
||||
source.getLatestUpdates(pageNum)
|
||||
|
||||
@@ -130,9 +130,12 @@ object H2Migration {
|
||||
)
|
||||
}
|
||||
|
||||
output.outputStream().use { out ->
|
||||
val tmp = output.resolveSibling(output.name + ".tmp")
|
||||
tmp.outputStream().use { out ->
|
||||
response.body.byteStream().copyTo(out)
|
||||
}
|
||||
tmp.copyTo(output)
|
||||
tmp.deleteExisting()
|
||||
}
|
||||
|
||||
logger.info { "Saved: ${output.absolutePathString()}" }
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
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.extension.Extension
|
||||
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
||||
import suwayomi.tachidesk.server.applicationSetup
|
||||
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
||||
import suwayomi.tachidesk.test.BASE_PATH
|
||||
@@ -51,7 +51,7 @@ class CloudFlareTest {
|
||||
Source
|
||||
.getSourceList()
|
||||
.firstNotNullOf { it.id.toLong().takeIf { it == 3122156392225024195L } }
|
||||
.let(GetCatalogueSource::getCatalogueSourceOrNull) as HttpSource
|
||||
.let(GetSource::getSourceOrNull) as HttpSource
|
||||
}
|
||||
setLoggingEnabled(true)
|
||||
}
|
||||
|
||||
@@ -27,8 +27,9 @@ import suwayomi.tachidesk.manga.impl.Source.getSourceList
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension.installExtension
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension.uninstallExtension
|
||||
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.util.source.GetCatalogueSource.getCatalogueSourceOrNull
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrNull
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
|
||||
import suwayomi.tachidesk.server.applicationSetup
|
||||
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
||||
@@ -50,6 +51,8 @@ class TestExtensionCompatibility {
|
||||
private val chaptersToFetch = mutableListOf<Triple<HttpSource, SManga, SChapter>>()
|
||||
private val chaptersPageListFailedToFetch = mutableListOf<Triple<HttpSource, Pair<SManga, SChapter>, Exception>>()
|
||||
|
||||
val repos = listOf<String>()
|
||||
|
||||
@BeforeAll
|
||||
fun setup() {
|
||||
val dataRoot = File(BASE_PATH).absolutePath
|
||||
@@ -57,6 +60,14 @@ class TestExtensionCompatibility {
|
||||
Looper.clearMainLooperForTest()
|
||||
SettingsRegistry.clear()
|
||||
applicationSetup()
|
||||
repos.forEach {
|
||||
val store =
|
||||
runBlocking {
|
||||
ExtensionStoreService.fetch(it)
|
||||
}
|
||||
ExtensionStoreService.upsert(store)
|
||||
}
|
||||
ExtensionStoreService.syncDbToPrefs()
|
||||
setLoggingEnabled(false)
|
||||
|
||||
runBlocking {
|
||||
@@ -72,7 +83,10 @@ class TestExtensionCompatibility {
|
||||
}
|
||||
|
||||
else -> {
|
||||
uninstallExtension(it.pkgName)
|
||||
try {
|
||||
uninstallExtension(it.pkgName)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
installExtension(it.pkgName)
|
||||
}
|
||||
}
|
||||
@@ -82,7 +96,7 @@ class TestExtensionCompatibility {
|
||||
.filter {
|
||||
// filter local source
|
||||
it.id.toLong() != 0L
|
||||
}.map { getCatalogueSourceOrNull(it.id.toLong())!! as HttpSource }
|
||||
}.map { getSourceOrNull(it.id.toLong())!! as HttpSource }
|
||||
}
|
||||
setLoggingEnabled(true)
|
||||
File("$BASE_PATH/sources.txt").writeText(sources.joinToString("\n") { "${it.name} - ${it.lang.uppercase()} - ${it.id}" })
|
||||
@@ -135,9 +149,9 @@ class TestExtensionCompatibility {
|
||||
repeat { source.getMangaUpdate(manga, emptyList(), true, true) }
|
||||
} catch (e: Exception) {
|
||||
logger.warn {
|
||||
"Failed to fetch manga info and chapters from $source for ${manga.title} (${source.mangaDetailsRequest(
|
||||
"Failed to fetch manga info and chapters from $source for ${manga.title} (${source.getMangaUrl(
|
||||
manga,
|
||||
).url}): ${e.message}"
|
||||
)}): ${e.message}"
|
||||
}
|
||||
mangaFailedToFetch += Triple(source, manga, e)
|
||||
}
|
||||
@@ -147,7 +161,7 @@ class TestExtensionCompatibility {
|
||||
File("$BASE_PATH/MangaFailedToFetch.txt").writeText(
|
||||
mangaFailedToFetch.joinToString("\n") { (source, manga, exception) ->
|
||||
"${source.name} (${source.lang}, ${source.id}):" +
|
||||
" ${manga.title} (${source.mangaDetailsRequest(manga).url}):" +
|
||||
" ${manga.title} (${source.getMangaUrl(manga)}):" +
|
||||
" ${exception.message}"
|
||||
},
|
||||
)
|
||||
@@ -163,9 +177,9 @@ class TestExtensionCompatibility {
|
||||
repeat { source.getPageList(chapter) }
|
||||
} catch (e: Exception) {
|
||||
logger.warn {
|
||||
"Failed to fetch manga info from $source for ${manga.title} (${source.mangaDetailsRequest(
|
||||
"Failed to fetch manga info from $source for ${manga.title} (${source.getMangaUrl(
|
||||
manga,
|
||||
).url}): ${e.message}"
|
||||
)}): ${e.message}"
|
||||
}
|
||||
chaptersPageListFailedToFetch += Triple(source, manga to chapter, e)
|
||||
}
|
||||
@@ -176,7 +190,7 @@ class TestExtensionCompatibility {
|
||||
File("$BASE_PATH/ChapterPageListFailedToFetch.txt").writeText(
|
||||
chaptersPageListFailedToFetch.joinToString("\n") { (source, manga, exception) ->
|
||||
"${source.name} (${source.lang}, ${source.id}):" +
|
||||
" ${manga.first.title} (${source.mangaDetailsRequest(manga.first).url}):" +
|
||||
" ${manga.first.title} (${source.getMangaUrl(manga.first)}):" +
|
||||
" ${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.setFilter
|
||||
import suwayomi.tachidesk.manga.impl.Search.sourceSearch
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.registerCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.unregisterCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.registerSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.unregisterSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.StubSource
|
||||
import suwayomi.tachidesk.test.ApplicationTest
|
||||
import suwayomi.tachidesk.test.createSMangas
|
||||
@@ -53,7 +53,7 @@ class SearchTest : ApplicationTest() {
|
||||
|
||||
@BeforeAll
|
||||
fun setup() {
|
||||
registerCatalogueSource(sourceId to source)
|
||||
registerSource(sourceId to source)
|
||||
|
||||
this.source.mangas = createSMangas(mangasCount)
|
||||
}
|
||||
@@ -70,7 +70,7 @@ class SearchTest : ApplicationTest() {
|
||||
|
||||
@AfterAll
|
||||
fun teardown() {
|
||||
unregisterCatalogueSource(this.sourceId)
|
||||
unregisterSource(this.sourceId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,7 +347,7 @@ class FilterListTest : ApplicationTest() {
|
||||
private fun registerSource(sourceClass: KClass<*>): EmptyFilterListSource =
|
||||
synchronized(sourceClass) {
|
||||
val source = sourceClass.primaryConstructor!!.call(sourceCount) as EmptyFilterListSource
|
||||
registerCatalogueSource(sourceCount to source)
|
||||
registerSource(sourceCount to source)
|
||||
sourceCount++
|
||||
source
|
||||
}
|
||||
@@ -355,7 +355,7 @@ class FilterListTest : ApplicationTest() {
|
||||
@AfterAll
|
||||
@JvmStatic
|
||||
fun teardown() {
|
||||
(0 until sourceCount).forEach { unregisterCatalogueSource(it) }
|
||||
(0 until sourceCount).forEach { unregisterSource(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user