Compare commits

..

1 Commits

Author SHA1 Message Date
renovate[bot]
fcea37ba29 Update polyglot to v25.1.3 2026-06-30 19:19:00 +00:00
27 changed files with 385 additions and 930 deletions

View File

@@ -42,7 +42,7 @@ body:
label: Suwayomi-Server version
description: You can find your Suwayomi-Server version in **More → About**.
placeholder: |
Example: "v2.3.2243"
Example: "v2.3.2223"
validations:
required: true

View File

@@ -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_DOCKER_TOKEN }}" -d '{"ref":"main", "inputs":{"tachidesk_release_type": "stable"}}' https://api.github.com/repos/suwayomi/suwayomi-server-docker/actions/workflows/build_container_images.yml/dispatches
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token ${{ secrets.DEPLOY_PREVIEW_TOKEN }}" -d '{"ref":"main", "inputs":{"tachidesk_release_type": "stable"}}' https://api.github.com/repos/suwayomi/docker-tachidesk/actions/workflows/build_container_images.yml/dispatches

View File

@@ -1,52 +1,19 @@
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: ${{ steps.version.outputs.version }}
release-tag: ${{ steps.version.outputs.version }}
version: ${{ inputs.version || github.ref_name }}
release-tag: ${{ inputs.version || github.ref_name }}

View File

@@ -15,36 +15,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Fixed
- .
## [v2.3.2243] - 2026-07-13
### Added
- (**Extension**) Add ability to install jar versions of extensions
### Fixed
- (**Extension**) Fix missing memo field in getPageList
## [v2.3.2238] - 2026-07-05
### Changed
- (**Extension**) Support fully qualified extension class names
- (**Manga/API**) Return partial responses for Manga And Chapters query
### Fixed
- (**Database/H2**) Use temporary file for H2 migration tools download
## [v2.3.2232] - 2026-07-03
### Fixed
- (**Extension**) Update APK url on installed Extensions
## [v2.3.2230] - 2026-07-01
### Fixed
- (**Extension/API**) Fix GQL handling of extensions without an extension store
- (**Build/Bundler**) Fix build continuing if errors happen
- (**Build/Bundler**) Remove jogamp from build script
- (**Build/Bundler**) Fix chmod attempts on Windows Java
## [v2.3.2223] + [WebUI: v20260509.01] - 2026-06-30
### Major Changes
@@ -55,10 +25,8 @@ This allows you to sync your server manga with other Mihon-based forks! As long
#### 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.
> 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
@@ -508,12 +476,8 @@ Huge thanks to @martinek who pulled the most of the weight this release!
<!-- SERVER LINKS -->
[unreleased]: https://github.com/suwayomi/suwayomi-server/compare/v2.3.2243...HEAD
[v2.3.2243]: https://github.com/suwayomi/suwayomi-server/compare/v2.3.2238...v2.3.2243
[v2.3.2238]: https://github.com/suwayomi/suwayomi-server/compare/v2.3.2232...v2.3.2238
[v2.3.2232]: https://github.com/suwayomi/suwayomi-server/compare/v2.3.2230...v2.3.2232
[v2.3.2230]: https://github.com/suwayomi/suwayomi-server/compare/v2.3.2223...v2.3.2230
[v2.3.2223]: https://github.com/suwayomi/suwayomi-server/compare/v2.2.2100...v2.3.2223
[unreleased]: https://github.com/suwayomi/suwayomi-server/compare/v2.3.2223...HEAD
[v2.3.2223]: https://github.com/suwayomi/suwayomi-server/compare/v2.1.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

View File

@@ -9,7 +9,7 @@ jte = "3.2.4"
jackson = "3.2.0" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
exposed = "1.2.0"
dex2jar = "2.4.37"
polyglot = "25.0.3"
polyglot = "25.1.3"
settings = "1.3.0"
twelvemonkeys = "3.13.1"
graphqlkotlin = "10.0.0"
@@ -110,7 +110,6 @@ 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"
@@ -216,7 +215,6 @@ shared = [
"dex2jar-translator",
"dex2jar-tools",
"apk-parser",
"axml",
"jackson-annotations",
"jcef",
]

View File

@@ -26,7 +26,7 @@ main() {
set -- "${POSITIONAL_ARGS[@]}"
OS="$1"
JAR="$(ls server/build/*.jar | tail -n1)" || error $LINENO "No JAR found in server/build/" 1
JAR="$(ls server/build/*.jar | tail -n1)"
RELEASE_NAME="$(echo "${JAR%.*}" | xargs basename)-$OS"
RELEASE_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/" || error $LINENO "Failed to create release directory" 1
mkdir "$RELEASE_NAME/"
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 || error $LINENO "Failed to compile catch_abort" 1
gcc -fPIC -shared scripts/resources/catch_abort.c -lpthread -o scripts/resources/catch_abort.so
fi
JRE_ZULU="25.34.17_25.0.3"
@@ -49,6 +49,7 @@ 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
;;
@@ -56,6 +57,7 @@ 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"
@@ -65,7 +67,7 @@ main() {
linux-assets)
RELEASE="$RELEASE_NAME.tar.gz"
copy_linux_package_assets_to "$RELEASE_NAME/"
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/" || error $LINENO "Failed to create tar.gz" 1
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/"
move_release_to_output_dir
;;
linux-x64)
@@ -75,6 +77,7 @@ 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"
@@ -89,6 +92,7 @@ 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"
@@ -103,6 +107,7 @@ 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"
@@ -117,6 +122,7 @@ 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"
@@ -139,50 +145,52 @@ move_release_to_output_dir() {
if [ -f "$OUTPUT_DIR/$RELEASE" ]; then
rm "$OUTPUT_DIR/$RELEASE"
fi
mv "$RELEASE" "$OUTPUT_DIR/" || error $LINENO "Failed to move release to output dir" 1
mv "$RELEASE" "$OUTPUT_DIR/"
}
download_launcher() {
LAUNCHER_URL=$(curl -sf "https://api.github.com/repos/Suwayomi/Suwayomi-Launcher/releases/latest" | grep "browser_download_url" | grep ".jar" | head -n 1 | cut -d '"' -f 4)
if [ -z "$LAUNCHER_URL" ]; then
error $LINENO "Failed to determine launcher URL from GitHub API" 1
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
fi
curl -fL "$LAUNCHER_URL" -o "Suwayomi-Launcher.jar" || error $LINENO "Failed to download launcher JAR" 1
mv "Suwayomi-Launcher.jar" "$RELEASE_NAME/Suwayomi-Launcher.jar" || error $LINENO "Failed to move launcher JAR" 1
7z x jogamp-all-platforms.7z "jogamp-all-platforms/lib/$platform/"
mkdir -p "$RELEASE_NAME/natives/"
mv jogamp-all-platforms/lib/* "$RELEASE_NAME/natives/"
rm -rf jogamp-all-platforms
}
download_electron() {
if [ ! -f "$ELECTRON" ]; then
curl -fL "$ELECTRON_URL" -o "$ELECTRON" || error $LINENO "Failed to download electron" 1
curl -fL "$ELECTRON_URL" -o "$ELECTRON"
fi
unzip "$ELECTRON" -d "$RELEASE_NAME/electron/" || error $LINENO "Failed to extract electron" 1
unzip "$ELECTRON" -d "$RELEASE_NAME/electron/"
}
setup_jre() {
if [ -d "jre" ]; then
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
chmod +x ./jre/bin/java
chmod +x ./jre/lib/jspawnhelper
mv "jre" "$RELEASE_NAME/jre"
else
if [ ! -f "$JRE" ]; then
curl -fL "$JRE_URL" -o "$JRE" || error $LINENO "Failed to download JRE" 1
curl -fL "$JRE_URL" -o "$JRE"
fi
local ext="${JRE##*.}"
if [ "$ext" = "zip" ]; then
unzip "$JRE" || error $LINENO "Failed to extract JRE zip" 1
unzip "$JRE"
else
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
tar xvf "$JRE"
fi
mv "$JRE_DIR" "$RELEASE_NAME/jre"
fi
}
@@ -190,31 +198,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/" || 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 "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 "server/src/main/resources/icon/faviconlogo-128.png" \
"$output_dir/suwayomi-server.png" || error $LINENO "Failed to copy icon" 1
"$output_dir/suwayomi-server.png"
}
make_linux_bundle() {
mkdir "$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
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/"
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/" || error $LINENO "Failed to create tar.gz" 1
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/"
}
make_macos_bundle() {
mkdir "$RELEASE_NAME/bin"
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
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar"
cp "scripts/resources/Suwayomi Launcher.command" "$RELEASE_NAME/"
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/" || error $LINENO "Failed to create tar.gz" 1
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/"
}
# https://wiki.debian.org/SimplePackagingTutorial
@@ -225,27 +233,28 @@ make_deb_package() {
#behind $RELEASE_VERSION is underscore "_"
local upstream_source="suwayomi-server_$RELEASE_VERSION.orig.tar.gz"
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
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"
copy_linux_package_assets_to "$RELEASE_NAME/$source_dir/"
cp "scripts/resources/catch_abort.so" "$RELEASE_NAME/$source_dir/" || error $LINENO "Failed to copy catch_abort" 1
tar -I "gzip" -C "$RELEASE_NAME/" -cvf "$upstream_source" "$source_dir" || error $LINENO "Failed to create source tar.gz" 1
cp "scripts/resources/catch_abort.so" "$RELEASE_NAME/$source_dir/"
tar -I "gzip" -C "$RELEASE_NAME/" -cvf "$upstream_source" "$source_dir"
cp -r "scripts/resources/deb/" "$RELEASE_NAME/$source_dir/debian/" || error $LINENO "Failed to copy debian resources" 1
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
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"
if [ "${CI:-}" = true ]; then
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
sudo apt update
sudo apt install devscripts build-essential dh-exec
fi
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
cd "$RELEASE_NAME/$source_dir/"
dpkg-buildpackage --no-sign --build=all
cd -
local deb="suwayomi-server_$RELEASE_VERSION-1_all.deb"
mv "$RELEASE_NAME/$deb" "$RELEASE" || error $LINENO "Failed to move resulting .deb" 1
mv "$RELEASE_NAME/$deb" "$RELEASE"
}
# https://linuxconfig.org/building-a-hello-world-appimage-on-linux
@@ -253,20 +262,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" || error $LINENO "Failed to copy server jar" 1
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar"
cp "scripts/resources/pkg/suwayomi-server.desktop" "$RELEASE_NAME/suwayomi-server.desktop" || error $LINENO "Failed to copy desktop file" 1
cp "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
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"
if [ "${CI:-}" = true ]; then
sudo apt update || error $LINENO "Failed to update apt" 1
sudo apt install libfuse2 || error $LINENO "Failed to install libfuse2" 1
sudo apt update
sudo apt install libfuse2
fi
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
curl -fL $APPIMAGE_URL -o $APPIMAGE_TOOLNAME
chmod +x $APPIMAGE_TOOLNAME
ARCH=x86_64 ./$APPIMAGE_TOOLNAME "$RELEASE_NAME" "$RELEASE"
}
make_windows_bundle() {
@@ -291,7 +300,7 @@ make_windows_bundle() {
#local rcedit_url="https://github.com/electron/rcedit/releases/download/v0.1.1/$rcedit"
## change electron's icon
#if [ ! -f "$rcedit" ]; then
#curl -fL "$rcedit_url" -o "$rcedit" || error $LINENO "Failed to download rcedit" 1
#curl -fL "$rcedit_url" -o "$rcedit"
#fi
#local icon="server/src/main/resources/icon/faviconlogo.ico"
@@ -299,35 +308,38 @@ make_windows_bundle() {
# --set-icon "$icon"
mkdir "$RELEASE_NAME/bin"
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
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar"
cp "scripts/resources/Suwayomi Launcher.bat" "$RELEASE_NAME"
zip -9 -r "$RELEASE" "$RELEASE_NAME" || error $LINENO "Failed to create windows bundle zip" 1
zip -9 -r "$RELEASE" "$RELEASE_NAME"
}
make_windows_package() {
if [ "${CI:-}" = true ]; then
sudo apt update || error $LINENO "Failed to update apt" 1
sudo apt install -y wixl || error $LINENO "Failed to install wixl" 1
sudo apt update
sudo apt install -y wixl
fi
find "$RELEASE_NAME/jre" \
| wixl-heat --var var.SourceDir -p "$RELEASE_NAME/" \
--directory-ref jre --component-group jre >"$RELEASE_NAME/jre.wxs" || error $LINENO "Failed to heat jre" 1
--directory-ref jre --component-group jre >"$RELEASE_NAME/jre.wxs"
find "$RELEASE_NAME/electron" \
| wixl-heat --var var.SourceDir -p "$RELEASE_NAME/" \
--directory-ref electron --component-group electron >"$RELEASE_NAME/electron.wxs" || error $LINENO "Failed to heat electron" 1
--directory-ref electron --component-group electron >"$RELEASE_NAME/electron.wxs"
find "$RELEASE_NAME/natives" \
| wixl-heat --var var.SourceDir -p "$RELEASE_NAME/" \
--directory-ref natives --component-group natives >"$RELEASE_NAME/natives.wxs"
find "$RELEASE_NAME/bin" \
| wixl-heat --var var.SourceDir -p "$RELEASE_NAME/" \
--directory-ref bin --component-group bin >"$RELEASE_NAME/bin.wxs" || error $LINENO "Failed to heat bin" 1
--directory-ref bin --component-group bin >"$RELEASE_NAME/bin.wxs"
local icon="server/src/main/resources/icon/faviconlogo.ico"
local 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/bin.wxs" -o "$RELEASE" || error $LINENO "Windows package build failed" 1
"$RELEASE_NAME/jre.wxs" "$RELEASE_NAME/electron.wxs" "$RELEASE_NAME/natives.wxs" "$RELEASE_NAME/bin.wxs" -o "$RELEASE"
}
# Error handler

View File

@@ -2,6 +2,7 @@
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/

View File

@@ -17,6 +17,7 @@
<Directory Id="INSTALLDIR" Name="Suwayomi-Server" >
<Directory Id="jre"/>
<Directory Id="electron"/>
<Directory Id="natives"/>
<Directory Id="bin"/>
</Directory>
</Directory>
@@ -62,6 +63,7 @@
<ComponentRef Id="SuwayomiLauncherBAT" />
<ComponentRef Id="ProgramMenuDir" />
<ComponentGroupRef Id="electron" />
<ComponentGroupRef Id="natives" />
</Feature>
<Icon Id="Suwayomi.ico" SourceFile="$(var.Icon)" />

View File

@@ -179,10 +179,10 @@ class ExtensionMutation {
val (clientMutationId, extensionFile) = input
return future {
val pkgName = Extension.installExternalExtension(extensionFile.content(), extensionFile.filename())
Extension.installExternalExtension(extensionFile.content(), extensionFile.filename())
val dbExtension =
transaction { ExtensionTable.selectAll().where { ExtensionTable.pkgName eq pkgName }.first() }
transaction { ExtensionTable.selectAll().where { ExtensionTable.apkName eq extensionFile.filename() }.first() }
InstallExternalExtensionPayload(
clientMutationId,

View File

@@ -3,8 +3,6 @@
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
@@ -183,21 +181,15 @@ class MangaMutation {
)
@RequireAuth
fun fetchMangaAndChapters(input: FetchMangaAndChaptersInput): CompletableFuture<DataFetcherResult<FetchMangaAndChaptersPayload?>> {
fun fetchMangaAndChapters(input: FetchMangaAndChaptersInput): CompletableFuture<FetchMangaAndChaptersPayload?> {
val (clientMutationId, id, fetchManga, fetchChapters) = input
return future {
val error =
try {
Manga.updateMangaAndChapters(
mangaId = id,
updateManga = fetchManga,
updateChapters = fetchChapters,
)
null
} catch (e: Exception) {
e
}
val (manga, chapters) =
transaction {
@@ -210,20 +202,11 @@ class MangaMutation {
.map { ChapterType(it) },
)
}
@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?>
)
}
}

View File

@@ -103,7 +103,6 @@ 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,
@@ -123,7 +122,6 @@ 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)
@@ -152,7 +150,6 @@ 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"))
@@ -178,7 +175,6 @@ 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),

View File

@@ -32,7 +32,6 @@ class ExtensionType(
val name: String,
val pkgName: String,
val apkUrl: String?,
val jarUrl: String?,
val extensionLib: String?,
val versionName: String,
@GraphQLDeprecated(
@@ -57,7 +56,6 @@ 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(),
@@ -73,8 +71,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(

View File

@@ -63,6 +63,8 @@ object ExtensionController {
ctx.future {
future {
Extension.installExtension(pkgName)
}.thenApply {
ctx.status(it)
}
}
},
@@ -97,6 +99,8 @@ object ExtensionController {
uploadedFile.content(),
uploadedFile.filename(),
)
}.thenApply {
ctx.status(it)
}
}
},
@@ -122,6 +126,8 @@ object ExtensionController {
ctx.future {
future {
Extension.updateExtension(pkgName)
}.thenApply {
ctx.status(it)
}
}
},

View File

@@ -14,7 +14,6 @@ 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
@@ -89,7 +88,6 @@ 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) }

View File

@@ -7,7 +7,6 @@ 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
@@ -18,7 +17,9 @@ import io.github.oshai.kotlinlogging.KotlinLogging
import net.dongliu.apk.parser.ApkFile
import net.dongliu.apk.parser.bean.Icon
import okhttp3.CacheControl
import org.apache.commons.compress.archivers.zip.ZipFile
import okio.buffer
import okio.sink
import okio.source
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.jdbc.deleteWhere
import org.jetbrains.exposed.v1.jdbc.insert
@@ -26,7 +27,6 @@ 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
@@ -39,7 +39,6 @@ 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.GetSource
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.clearCachedImage
@@ -49,356 +48,139 @@ 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()
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 {
suspend fun installExtension(pkgName: String): Int {
logger.debug { "Installing $pkgName" }
val extension =
val apkUrl =
transaction {
ExtensionTable
.select(ExtensionTable.apkUrl, ExtensionTable.jarUrl)
.select(ExtensionTable.apkUrl)
.where { ExtensionTable.pkgName eq pkgName }
.firstOrNull()
} ?: throw NullPointerException("Could not find extension for $pkgName")
val jarUrl = extension[ExtensionTable.jarUrl]
val apkUrl = extension[ExtensionTable.apkUrl]
?.get(ExtensionTable.apkUrl)
} ?: throw NullPointerException("Could not find extension $pkgName")
return when {
jarUrl != null -> {
installExtension {
val jar = fetchExtensionFile(jarUrl)
val manifest = extractAndParseAndroidManifest(jar)
ExtensionPackage.Jar(jar, manifest)
}
}
return installAPK {
val apkName = Uri.parse(apkUrl).lastPathSegment!!
val apkSavePath = "${applicationDirs.extensionsRoot}/$apkName"
// download apk file
downloadAPKFile(apkUrl, apkSavePath)
apkUrl != null -> {
installExtension {
val apk = fetchExtensionFile(apkUrl)
val packageInfo = getPackageInfo(apk)
ExtensionPackage.Apk(apk, packageInfo)
apkSavePath
}
}
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,
extensionName: String,
): String =
when {
extensionName.endsWith(".jar") -> {
installExtension(
true,
{
val jar = copyToExtensionsRoot(inputStream, extensionName)
val manifest = extractAndParseAndroidManifest(jar)
ExtensionPackage.Jar(jar, manifest)
},
)
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"
}
extensionName.endsWith(".apk") -> {
installExtension(
true,
{
val apk = copyToExtensionsRoot(inputStream, extensionName)
val packageInfo = getPackageInfo(apk)
ExtensionPackage.Apk(apk, packageInfo)
},
)
}
else -> {
throw NullPointerException("Could not find extension type for $extensionName")
logger.debug { "Saving apk at $apkName" }
// download apk file
downloadedFile.outputStream().sink().buffer().use { sink ->
inputStream.source().use { source ->
sink.writeAll(source)
sink.flush()
}
}
sealed class ExtensionPackage {
abstract val file: Path
abstract val metadata: PackageMetadata
// Abstract hook for type-specific preprocessing
abstract suspend fun prepareJarAndIcons(extensionsRoot: Path): Path
class Apk(
override val file: Path,
val packageInfo: PackageInfo,
) : ExtensionPackage() {
override val metadata =
PackageMetadata(
packageName = packageInfo.packageName,
versionName = packageInfo.versionName,
versionCode = packageInfo.versionCode,
reqFeatures = packageInfo.reqFeatures.orEmpty().map { it.name },
metaData = MetadataProvider.FromPackageInfo(packageInfo.applicationInfo.metaData),
label = packageInfo.applicationInfo.nonLocalizedLabel?.toString(),
)
override suspend fun prepareJarAndIcons(extensionsRoot: Path): Path {
val jarFile = extensionsRoot / (file.nameWithoutExtension + ".jar")
dex2jar(file, jarFile)
extractAssetsFromApk(file, jarFile)
extractAndCacheApkIcon(file, metadata.packageName)
file.deleteExisting()
return jarFile
}
downloadedFile.absolutePathString()
}
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,
)
override suspend fun prepareJarAndIcons(extensionsRoot: Path): Path {
val jarFile = extensionsRoot / file.name
ZipFile.builder().setPath(file).get().use { jarZip ->
try {
cacheIcon(
metadata.packageName,
ResourceArscIconParser.extractIcon(jarZip),
)
} catch (e: Exception) {
logger.warn(e) { "Failed to extract icon from JAR ${metadata.packageName}" }
}
}
file.copyTo(jarFile)
file.deleteExisting()
return jarFile
}
}
}
data class PackageMetadata(
val packageName: String,
val versionName: String,
val versionCode: Int,
val reqFeatures: List<String>,
val metaData: MetadataProvider,
val label: String?,
)
sealed interface MetadataProvider {
fun getString(key: String): String?
fun getInt(key: String): Int
class FromPackageInfo(
private val bundle: android.os.Bundle,
) : MetadataProvider {
override fun getString(key: String): String? = bundle.getString(key)
override fun getInt(key: String): Int = bundle.getInt(key) ?: 0
}
class FromManifest(
private val list: List<AndroidManifestParser.MetaData>,
) : MetadataProvider {
override fun getString(key: String): String? = list.find { it.name == key }?.value
override fun getInt(key: String): Int = getString(key)?.toIntOrNull() ?: 0
}
}
suspend fun installExtension(
suspend fun installAPK(
forceReinstall: Boolean = false,
fetchPackage: suspend () -> ExtensionPackage,
): String {
val extPackage = fetchPackage()
val metadata = extPackage.metadata
val pkgName = metadata.packageName
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
.select(ExtensionTable.isInstalled)
.where { ExtensionTable.pkgName eq pkgName }
.firstOrNull()
ExtensionTable.selectAll().where { ExtensionTable.apkName eq apkName }.firstOrNull()
}?.get(ExtensionTable.isInstalled) ?: false
if (isInstalled) {
if (forceReinstall) {
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)
} else {
extPackage.file.deleteExisting()
return pkgName
}
}
if (!metadata.reqFeatures.contains(EXTENSION_FEATURE)) {
extPackage.file.deleteExisting()
throw Exception("This file is not a Tachiyomi extension")
if (!isInstalled || forceReinstall) {
if (!packageInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }) {
throw Exception("This apk is not a Tachiyomi extension")
}
val libVersion = metadata.versionName.substringBeforeLast('.').toDouble()
// Validate lib version
val libVersion = packageInfo.versionName.substringBeforeLast('.').toDouble()
if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
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
}
}
// TODO: allow trusting keys
// val signatureHash = getSignatureHash(packageInfo)
val sourceClass =
metadata.metaData
.getString(METADATA_SOURCE_CLASS)!!
.trim()
// 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
.getString(METADATA_CONTENT_WARNING)
?.toIntOrNull()
?: 0
if (contentWarning == 0) {
contentWarning = packageInfo.applicationInfo.metaData
.getString(METADATA_NSFW)
?.toIntOrNull()
?: 0
}
}
val className =
if (sourceClass.startsWith(".")) {
pkgName + sourceClass
} else {
sourceClass
}
packageInfo.packageName + packageInfo.applicationInfo.metaData.getString(METADATA_SOURCE_CLASS)
logger.debug { "Main class for extension is $className" }
val extensionsRoot = Path(applicationDirs.extensionsRoot)
val jarFile = extPackage.prepareJarAndIcons(extensionsRoot)
dex2jar(apkFilePath, jarFilePath, fileNameWithoutType)
extractAssetsFromApk(apkFilePath, jarFilePath)
extractAndCacheApkIcon(apkFilePath, packageInfo.packageName)
// clean up
File(apkFilePath).delete()
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 extensionMainClassInstance = loadExtensionSources(jarFilePath, className)
val sources: List<Source> =
when (extensionMainClassInstance) {
is Source -> listOf(extensionMainClassInstance)
@@ -414,15 +196,27 @@ object Extension {
else -> "all"
}
val extensionName =
packageInfo.applicationInfo.metaData.getString(METADATA_NAME)
?: packageInfo.applicationInfo.nonLocalizedLabel
.toString()
.substringAfter("Tachiyomi: ")
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] = pkgName
it[this.versionName] = versionName
it[this.versionCode] = versionCode.toLong()
it[this.pkgName] = packageInfo.packageName
it[versionName] = packageInfo.versionName
it[versionCode] = packageInfo.versionCode.toLong()
it[extensionLib] = extensionLibVersion
it[lang] = extensionLang
it[this.contentWarning] = contentWarning
@@ -433,8 +227,8 @@ object Extension {
it[this.apkName] = apkName
it[this.isInstalled] = true
it[this.classFQName] = className
it[this.versionName] = versionName
it[this.versionCode] = versionCode.toLong()
it[versionName] = packageInfo.versionName
it[versionCode] = packageInfo.versionCode.toLong()
}
val extensionId =
@@ -455,15 +249,28 @@ object Extension {
logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" }
}
}
return 201 // we installed successfully
} catch (e: Throwable) {
// free up the file descriptor if exists
PackageTools.jarLoaderMap.remove(jarFilePath)?.close()
File(jarFilePath).delete()
uninstallExtension(pkgName)
throw e
}
} else {
return 302 // extension was already installed
}
}
private fun extractAndCacheApkIcon(
apkFile: Path,
apkFilePath: String,
pkgName: String,
) {
val iconCacheDir = "${applicationDirs.extensionsRoot}/icon"
try {
val iconData =
ApkFile(apkFile.toFile()).use { apk ->
ApkFile(File(apkFilePath)).use { apk ->
apk.allIcons
.filterIsInstance<Icon>()
.mapNotNull { it.data?.let { data -> data to it.density } }
@@ -474,36 +281,31 @@ object Extension {
logger.warn { "No icon found in APK $pkgName" }
return
}
cacheIcon(pkgName, iconData.inputStream())
File(iconCacheDir).mkdirs()
clearCachedImage(iconCacheDir, pkgName)
saveImage("$iconCacheDir/$pkgName", iconData.inputStream(), null)
} catch (e: Exception) {
logger.warn(e) { "Failed to extract icon from APK $pkgName" }
}
}
private fun cacheIcon(
pkgName: String,
inputStream: InputStream,
) {
val iconCacheDir = Path(applicationDirs.extensionsRoot) / "icon"
iconCacheDir.createDirectories()
clearCachedImage(iconCacheDir.absolutePathString(), pkgName)
saveImage("$iconCacheDir/$pkgName", inputStream, null)
}
@OptIn(ExperimentalPathApi::class)
private fun extractAssetsFromApk(
apkFile: Path,
jarFile: Path,
apkPath: String,
jarPath: String,
) {
val assetsFolder = apkFile.parent / "${apkFile.nameWithoutExtension}_assets"
assetsFolder.createDirectories()
val apkFile = File(apkPath)
val jarFile = File(jarPath)
val assetsFolder = File("${apkFile.parent}/${apkFile.nameWithoutExtension}_assets")
assetsFolder.mkdir()
ZipInputStream(apkFile.inputStream()).use { zipInputStream ->
var zipEntry = zipInputStream.nextEntry
while (zipEntry != null) {
if (zipEntry.name.startsWith("assets/") && !zipEntry.isDirectory) {
val assetFile = assetsFolder / zipEntry.name
assetFile.createParentDirectories()
assetFile.outputStream().use { outputStream ->
val assetFile = File(assetsFolder, zipEntry.name)
assetFile.parentFile.mkdirs()
FileOutputStream(assetFile).use { outputStream ->
zipInputStream.copyTo(outputStream)
}
}
@@ -511,9 +313,9 @@ object Extension {
}
}
val tempJarFile = jarFile.parent / "${jarFile.nameWithoutExtension}_temp.jar"
val tempJarFile = File("${jarFile.parent}/${jarFile.nameWithoutExtension}_temp.jar")
ZipInputStream(jarFile.inputStream()).use { jarZipInputStream ->
ZipOutputStream(tempJarFile.outputStream()).use { jarZipOutputStream ->
ZipOutputStream(FileOutputStream(tempJarFile)).use { jarZipOutputStream ->
var zipEntry = jarZipInputStream.nextEntry
while (zipEntry != null) {
if (!zipEntry.name.startsWith("META-INF/")) {
@@ -522,8 +324,8 @@ object Extension {
}
zipEntry = jarZipInputStream.nextEntry
}
assetsFolder.walk().forEach { file ->
if (file.isRegularFile()) {
assetsFolder.walkTopDown().forEach { file ->
if (file.isFile) {
jarZipOutputStream.putNextEntry(ZipEntry(file.relativeTo(assetsFolder).toString().replace("\\", "/")))
file.inputStream().use { inputStream ->
inputStream.copyTo(jarZipOutputStream)
@@ -534,25 +336,17 @@ object Extension {
}
}
jarFile.deleteIfExists()
tempJarFile.copyTo(jarFile)
tempJarFile.deleteExisting()
jarFile.delete()
tempJarFile.renameTo(jarFile)
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 downloadExtension(
private suspend fun downloadAPKFile(
url: String,
savePath: Path,
savePath: String,
) {
val response =
network.client
@@ -560,10 +354,11 @@ object Extension {
GET(url, cache = CacheControl.FORCE_NETWORK),
).await()
savePath.createParentDirectories()
response.body.byteStream().use {
savePath.outputStream().buffered().use { out ->
it.copyTo(out)
val downloadedFile = File(savePath)
downloadedFile.sink().buffer().use { sink ->
response.body.source().use { source ->
sink.writeAll(source)
sink.flush()
}
}
}
@@ -575,7 +370,7 @@ object Extension {
val fileNameWithoutType =
extensionRecord[ExtensionTable.apkName]?.substringBefore(".apk")
?: throw NullPointerException("Missing $pkgName apkName")
val jarPath = Path(applicationDirs.extensionsRoot) / "$fileNameWithoutType.jar"
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
val sources =
transaction {
val extensionId = extensionRecord[ExtensionTable.id].value
@@ -597,18 +392,18 @@ object Extension {
sources
}
if (jarPath.exists()) {
if (File(jarPath).exists()) {
// free up the file descriptor if exists
PackageTools.jarLoaderMap.remove(jarPath.absolutePathString())?.close()
PackageTools.jarLoaderMap.remove(jarPath)?.close()
// clear all loaded sources
sources.forEach { GetSource.unregisterSource(it) }
jarPath.deleteExisting()
File(jarPath).delete()
}
}
suspend fun updateExtension(pkgName: String): String {
suspend fun updateExtension(pkgName: String): Int {
val targetExtension = ExtensionsList.updateMap.remove(pkgName)!!
uninstallExtension(pkgName)
transaction {

View File

@@ -132,8 +132,6 @@ 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]
@@ -177,7 +175,6 @@ 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()
@@ -195,7 +192,6 @@ 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
}
}

View File

@@ -53,8 +53,6 @@ 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
@@ -111,7 +109,6 @@ 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,

View File

@@ -44,7 +44,6 @@ 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,

View File

@@ -1,148 +0,0 @@
package suwayomi.tachidesk.manga.impl.util
import kotlinx.serialization.Serializable
import nl.adaptivity.xmlutil.ExperimentalXmlUtilApi
import nl.adaptivity.xmlutil.XmlDeclMode
import nl.adaptivity.xmlutil.core.KtXmlReader
import nl.adaptivity.xmlutil.serialization.UnknownChildHandler
import nl.adaptivity.xmlutil.serialization.XML
import nl.adaptivity.xmlutil.serialization.XmlElement
import nl.adaptivity.xmlutil.serialization.XmlSerialName
import java.io.InputStream
object AndroidManifestParser {
private const val ANDROID_NS = "http://schemas.android.com/apk/res/android"
@Serializable
@XmlSerialName("manifest", "", "")
data class AndroidManifest(
@XmlSerialName("package", "", "")
val packageName: String,
@XmlSerialName("versionCode", ANDROID_NS, "android")
val versionCode: Int? = null,
@XmlSerialName("versionName", ANDROID_NS, "android")
val versionName: String? = null,
@XmlElement(true)
@XmlSerialName("uses-sdk", "", "")
val usesSdk: UsesSdk? = null,
@XmlElement(true)
@XmlSerialName("uses-feature", "", "")
val usesFeatures: List<UsesFeature> = emptyList(),
@XmlElement(true)
@XmlSerialName("application", "", "")
val application: Application? = null,
)
@Serializable
@XmlSerialName("uses-sdk", "", "")
data class UsesSdk(
@XmlSerialName("minSdkVersion", ANDROID_NS, "android")
val minSdkVersion: Int? = null,
@XmlSerialName("targetSdkVersion", ANDROID_NS, "android")
val targetSdkVersion: Int? = null,
)
@Serializable
@XmlSerialName("uses-feature", "", "")
data class UsesFeature(
@XmlSerialName("name", ANDROID_NS, "android")
val name: String? = null,
)
@Serializable
@XmlSerialName("application", "", "")
data class Application(
@XmlSerialName("label", ANDROID_NS, "android")
val label: String? = null,
@XmlSerialName("icon", ANDROID_NS, "android")
val icon: String? = null,
@XmlSerialName("allowBackup", ANDROID_NS, "android")
val allowBackup: Boolean? = null,
@XmlSerialName("extractNativeLibs", ANDROID_NS, "android")
val extractNativeLibs: Boolean? = null,
@XmlElement(true)
@XmlSerialName("meta-data", "", "")
val metaData: List<MetaData> = emptyList(),
@XmlElement(true)
@XmlSerialName("activity", "", "")
val activities: List<Activity> = emptyList(),
)
@Serializable
@XmlSerialName("meta-data", "", "")
data class MetaData(
@XmlSerialName("name", ANDROID_NS, "android")
val name: String,
@XmlSerialName("value", ANDROID_NS, "android")
val value: String? = null,
@XmlSerialName("resource", ANDROID_NS, "android")
val resource: String? = null,
)
@Serializable
@XmlSerialName("activity", "", "")
data class Activity(
@XmlSerialName("name", ANDROID_NS, "android")
val name: String,
@XmlSerialName("exported", ANDROID_NS, "android")
val exported: Boolean? = null,
@XmlSerialName("theme", ANDROID_NS, "android")
val theme: String? = null,
@XmlElement(true)
@XmlSerialName("intent-filter", "", "")
val intentFilters: List<IntentFilter> = emptyList(),
)
@Serializable
@XmlSerialName("intent-filter", "", "")
data class IntentFilter(
@XmlElement(true)
@XmlSerialName("action", "", "")
val actions: List<Action> = emptyList(),
@XmlElement(true)
@XmlSerialName("category", "", "")
val categories: List<Category> = emptyList(),
@XmlElement(true)
@XmlSerialName("data", "", "")
val data: List<Data> = emptyList(),
)
@Serializable
@XmlSerialName("action", "", "")
data class Action(
@XmlSerialName("name", ANDROID_NS, "android")
val name: String,
)
@Serializable
@XmlSerialName("category", "", "")
data class Category(
@XmlSerialName("name", ANDROID_NS, "android")
val name: String,
)
@Serializable
@XmlSerialName("data", "", "")
data class Data(
@XmlSerialName("scheme", ANDROID_NS, "android")
val scheme: String? = null,
@XmlSerialName("host", ANDROID_NS, "android")
val host: String? = null,
@XmlSerialName("pathPattern", ANDROID_NS, "android")
val pathPattern: String? = null,
)
@OptIn(ExperimentalXmlUtilApi::class)
private val xml =
XML {
autoPolymorphic = false
repairNamespaces = true
xmlDeclMode = XmlDeclMode.Minimal
defaultPolicy {
unknownChildHandler = UnknownChildHandler { _, _, _, _, _ -> emptyList() }
}
}
@OptIn(ExperimentalXmlUtilApi::class)
fun parse(input: InputStream): AndroidManifest = xml.decodeFromReader(AndroidManifest.serializer(), KtXmlReader(input))
}

View File

@@ -23,14 +23,13 @@ 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 {
@@ -53,13 +52,15 @@ object PackageTools {
* Convert dex to jar, a wrapper for the dex2jar library
*/
fun dex2jar(
dexFile: Path,
jarFile: Path,
dexFile: String,
jarFile: String,
fileNameWithoutType: String,
) {
// 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 reader = MultiDexFileReader.open(dexFile.readBytes())
val jarFilePath = File(jarFile).toPath()
val reader = MultiDexFileReader.open(Files.readAllBytes(File(dexFile).toPath()))
val handler = BaksmaliBaseDexExceptionHandler()
Dex2jar
.from(reader)
@@ -72,10 +73,10 @@ object PackageTools {
.noCode(false)
.skipExceptions(false)
.dontSanitizeNames(true)
.to(jarFile)
.to(jarFilePath)
if (handler.hasException()) {
val rootPath = Path(applicationDirs.extensionsRoot)
val errorFile: Path = rootPath.resolve("${dexFile.nameWithoutExtension}-error.txt")
val errorFile: Path = rootPath.resolve("$fileNameWithoutType-error.txt")
logger.error {
"""
Detail Error Information in File ${errorFile.relativeTo(rootPath)}
@@ -88,14 +89,15 @@ object PackageTools {
}
handler.dump(errorFile, emptyArray<String>())
} else {
BytecodeEditor.fixAndroidClasses(jarFile)
BytecodeEditor.fixAndroidClasses(jarFilePath)
}
}
/** A modified version of `xyz.nulldev.androidcompat.pm.InstalledPackage.info` */
fun getPackageInfo(apkFile: Path): PackageInfo =
ApkParsers.getMetaInfo(apkFile.toFile()).toPackageInfo(apkFile.toFile()).apply {
ApkFile(apkFile.toFile()).use { parsed ->
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 =
@@ -154,19 +156,19 @@ object PackageTools {
* It may return an instance of HttpSource or SourceFactory depending on the extension.
*/
fun loadExtensionSources(
jar: Path,
jarPath: String,
className: String,
): Any {
try {
logger.debug { "loading jar with path: ${jar.absolutePathString()}" }
val classLoader = jarLoaderMap[jar.absolutePathString()] ?: ChildFirstURLClassLoader(arrayOf<URL>(jar.toUri().toURL()))
logger.debug { "loading jar with path: $jarPath" }
val classLoader = jarLoaderMap[jarPath] ?: ChildFirstURLClassLoader(arrayOf<URL>(Path(jarPath).toUri().toURL()))
val classToLoad = Class.forName(className, false, classLoader)
jarLoaderMap[jar.absolutePathString()] = classLoader
jarLoaderMap[jarPath] = classLoader
return classToLoad.getDeclaredConstructor().newInstance()
} catch (e: Exception) {
logger.error(e) { "Failed to load jar with path: ${jar.absolutePathString()}" }
logger.error(e) { "Failed to load jar with path: $jarPath" }
throw e
}
}

View File

@@ -1,71 +0,0 @@
package suwayomi.tachidesk.manga.impl.util
import nl.adaptivity.xmlutil.core.impl.multiplatform.InputStream
import org.apache.commons.compress.archivers.zip.ZipFile
import pxb.android.arsc.ArscParser
import pxb.android.arsc.Config
import pxb.android.arsc.Pkg
import java.nio.ByteBuffer
import java.nio.ByteOrder
object ResourceArscIconParser {
private data class IconCandidate(
val density: Int,
val path: String,
)
fun extractIcon(zip: ZipFile): InputStream {
val packages =
zip
.getInputStream(zip.getEntry("resources.arsc"))
.use { ArscParser(it.readBytes()).parse() }
val icon =
packages
.flatMap { it.iconCandidates() }
.maxByOrNull { it.density }
?: throw NullPointerException("No valid icons")
val entry =
zip.getEntry(icon.path)
?: throw NullPointerException("Icon ${icon.path} missing")
return zip.getInputStream(entry)
}
private fun Pkg.iconCandidates(): List<IconCandidate> =
types.values
.filter { it.name == "mipmap" || it.name == "drawable" }
.flatMap {
it.configs.flatMap {
val density = it.density()
it.resources.values
.asSequence()
.filter { it.spec.name == "ic_launcher" }
.map { it.value.toString() }
.filter(::isRasterImage)
.map { IconCandidate(density, it) }
}
}
private fun Config.density(): Int =
ByteBuffer
.wrap(id)
.order(ByteOrder.LITTLE_ENDIAN)
.getShort(14)
.toInt() and 0xffff
private val rasterExtensions =
setOf(
"png",
"webp",
"jpg",
"jpeg",
)
private fun isRasterImage(path: String): Boolean =
path
.substringAfterLast('.', "")
.lowercase() in rasterExtensions
}

View File

@@ -20,8 +20,6 @@ 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 GetSource {
private val logger = KotlinLogging.logger { }
@@ -51,7 +49,7 @@ object GetSource {
?: throw NullPointerException("Missing apkName")
val className = extensionRecord[ExtensionTable.classFQName]
val jarName = apkName.substringBefore(".apk") + ".jar"
val jarPath = Path(applicationDirs.extensionsRoot) / jarName
val jarPath = "${applicationDirs.extensionsRoot}/$jarName"
when (val instance = loadExtensionSources(jarPath, className)) {
is Source -> listOf(instance)

View File

@@ -12,7 +12,6 @@ data class ExtensionInfo(
val name: String,
val pkgName: String,
val apkUrl: String,
val jarUrl: String?,
val iconUrl: String,
val extensionLib: String,
val versionCode: Long,

View File

@@ -24,7 +24,6 @@ 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")

View File

@@ -130,12 +130,9 @@ object H2Migration {
)
}
val tmp = output.resolveSibling(output.name + ".tmp")
tmp.outputStream().use { out ->
output.outputStream().use { out ->
response.body.byteStream().copyTo(out)
}
tmp.copyTo(output)
tmp.deleteExisting()
}
logger.info { "Saved: ${output.absolutePathString()}" }

View File

@@ -1,19 +0,0 @@
package suwayomi.tachidesk.server.database.migration
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import de.neonew.exposed.migrations.helpers.AddColumnMigration
@Suppress("ClassName", "unused")
class M0059_AddJarUrlToExtensionTable :
AddColumnMigration(
"extension",
"jar_url",
"VARCHAR(2048)",
"NULL",
)

View File

@@ -27,7 +27,6 @@ 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.GetSource.getSourceOrNull
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
@@ -51,8 +50,6 @@ 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
@@ -60,14 +57,6 @@ class TestExtensionCompatibility {
Looper.clearMainLooperForTest()
SettingsRegistry.clear()
applicationSetup()
repos.forEach {
val store =
runBlocking {
ExtensionStoreService.fetch(it)
}
ExtensionStoreService.upsert(store)
}
ExtensionStoreService.syncDbToPrefs()
setLoggingEnabled(false)
runBlocking {
@@ -83,10 +72,7 @@ class TestExtensionCompatibility {
}
else -> {
try {
uninstallExtension(it.pkgName)
} catch (_: Exception) {
}
installExtension(it.pkgName)
}
}
@@ -149,9 +135,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.getMangaUrl(
"Failed to fetch manga info and chapters from $source for ${manga.title} (${source.mangaDetailsRequest(
manga,
)}): ${e.message}"
).url}): ${e.message}"
}
mangaFailedToFetch += Triple(source, manga, e)
}
@@ -161,7 +147,7 @@ class TestExtensionCompatibility {
File("$BASE_PATH/MangaFailedToFetch.txt").writeText(
mangaFailedToFetch.joinToString("\n") { (source, manga, exception) ->
"${source.name} (${source.lang}, ${source.id}):" +
" ${manga.title} (${source.getMangaUrl(manga)}):" +
" ${manga.title} (${source.mangaDetailsRequest(manga).url}):" +
" ${exception.message}"
},
)
@@ -177,9 +163,9 @@ class TestExtensionCompatibility {
repeat { source.getPageList(chapter) }
} catch (e: Exception) {
logger.warn {
"Failed to fetch manga info from $source for ${manga.title} (${source.getMangaUrl(
"Failed to fetch manga info from $source for ${manga.title} (${source.mangaDetailsRequest(
manga,
)}): ${e.message}"
).url}): ${e.message}"
}
chaptersPageListFailedToFetch += Triple(source, manga to chapter, e)
}
@@ -190,7 +176,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.getMangaUrl(manga.first)}):" +
" ${manga.first.title} (${source.mangaDetailsRequest(manga.first).url}):" +
" ${manga.second.name} (${manga.second.url}): ${exception.message}"
},
)