Compare commits

..

36 Commits

Author SHA1 Message Date
renovate[bot]
049c942406 Update dependency ch.qos.logback:logback-classic to v1.5.38 2026-07-09 09:54:49 +00:00
Mitchell Syer
9a0871ab93 Fix missing memo field in getPageList (#2175) 2026-07-08 15:41:22 -04:00
Syer10
0271a0174f Remove leading slash 2026-07-05 15:47:15 -04:00
Syer10
a1770cb055 Release v2.3.2238 2026-07-05 15:22:03 -04:00
Mitchell Syer
cf3589f272 Improve winget action (#2164) 2026-07-05 14:23:43 -04:00
Mitchell Syer
d7a839b1fd Fix automatic docker deploy for stable builds (#2162) 2026-07-05 14:23:30 -04:00
Mitchell Syer
5d46f2a197 Use temporary file for H2 migration tools download (#2169) 2026-07-05 14:23:15 -04:00
Mitchell Syer
fbd3a3cc5d Support Fully Qualified Class Names (#2168)
* Support Fully Qualified Class Names

* Lint
2026-07-05 14:23:01 -04:00
Mitchell Syer
41d23c0a09 Return Partial Manga Data with Errors (#2166)
* Return Partial Manga Data with Errors

* Lint

* Optimize imports

* Update server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/MangaMutation.kt

Co-authored-by: schroda <50052685+schroda@users.noreply.github.com>

* Update server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/MangaMutation.kt

Co-authored-by: schroda <50052685+schroda@users.noreply.github.com>

---------

Co-authored-by: schroda <50052685+schroda@users.noreply.github.com>
2026-07-05 14:22:28 -04:00
Syer10
f473135d04 Release v2.3.2232 2026-07-03 19:09:53 -04:00
Mitchell Syer
36ecb3ce6f Update APK url on installed Extensions (#2159)
* Update APK url on installed Extensions

* Changelog
2026-07-02 13:39:03 -04:00
Syer10
0cac79f354 Release v2.3.2230 2026-07-01 17:36:21 -04:00
Mitchell Syer
0fd10cc08e Fix chmod attempts on Windows Java (#2156) 2026-07-01 17:23:17 -04:00
Mitchell Syer
87aac38253 Remove jogamp (#2155)
* Remove jogamp from bundler

* Changelog
2026-07-01 16:37:03 -04:00
Mitchell Syer
9ca66c18de Add error handling to bundler (#2154)
* Add error handling to bundler

* Changelog
2026-07-01 14:41:59 -04:00
Syer10
b3c7962503 Release v2.3.2226 2026-07-01 11:57:49 -04:00
schroda
ca93783e72 Fix gql "extension" "extension store" typing (#2151)
The local source, externally installed apks and extensions whose extension store got removed do not have a store. This caused gql to fail because the extensions "extensionStore" was expected to be non-nullable.
2026-07-01 11:51:53 -04:00
Syer10
c1d0df8e3a [skip ci] Changelog fixes 2026-06-30 15:37:29 -04:00
Syer10
612768faeb Release v2.3.2223 2026-06-30 15:17:18 -04:00
Syer10
9a1745b626 [skip ci] Add changelog for bind existing track 2026-06-30 14:53:58 -04:00
schroda
323d58717e Add mutation to bind existing track record (#2045)
Makes it possible to copy a bound track record to another manga.
This is necessary during a migration to prevent spamming the actual tracker and causing 429 errors

closes #2033
2026-06-29 14:59:53 -04:00
Mitchell Syer
4d7b7617a9 Fix backup corruption with new extension lib (#2146)
* Fix backup corruption with new extension lib

* Fix missing chapter memo backup
2026-06-29 14:52:37 -04:00
Mitchell Syer
35b48114c6 Use Plain Source over CatalogueSource (#2141) 2026-06-28 14:25:37 -04:00
Mitchell Syer
3031aa7ccd Make contentWarning check more robust (#2140)
* Make contentWarning check more robust

* Lint
2026-06-28 12:01:48 -04:00
Mitchell Syer
c79486b8be Manual Extension Fixes (#2139)
* Fix manual extension icons

* Delete extension where APK Url is null
2026-06-28 02:08:53 -04:00
Mitchell Syer
e2fd15158c Fix Backups (#2138) 2026-06-27 14:09:48 -04:00
Bartu Özen
b6de3c3e39 Use stable manga and chapter composite keys for sync matching (#2124) 2026-06-27 13:41:14 -04:00
Weblate (bot)
656d86c6f6 Weblate translations (#2130)
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/de/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/el/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/es/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/fr/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/it/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/ja/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/pl/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/pt/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/ru/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/ta/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/vi/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/zh_Hans/
Translation: Suwayomi/Suwayomi-Server

Co-authored-by: Constantin Piber <cp.piber@gmail.com>
Co-authored-by: Damien O'Neil <maxiburning@gmail.com>
Co-authored-by: zeedif <carlos_antonio-rl@hotmail.com>
2026-06-27 13:40:44 -04:00
schroda
a0fbff5756 Update issue templates (#2137) 2026-06-27 13:39:43 -04:00
Mitchell Syer
2d535b44d8 Extension API 1.6 (#2120)
* Non-Extension Index changes for 1.6

* Changelog

* Minor fixes

* Implement extension store

* Test build fix

* Docs

* Simplify fetching manga and chapters

* Use EMPTY JsonObject

* Update docs/Configuring-Suwayomi‐Server.md

Co-authored-by: Constantin Piber <59023762+cpiber@users.noreply.github.com>

* Improve Fetch Extension Store

* Fixes

* Simplify deprecated isNsfw in SourceQuery

* Simplify ContentRating in Source.kt

* Simplify isNsfw in SourceType

* No magic numbers for ContentRating, improves safety for future versions of extension api

* Fix SearchTest

* Lint

* Lint

* Optimize imports and fix unchecked cast warning

* Proper extension store queries

* Optimize import fixes

* Add ContentRatingFilter

* Improve extension store sync

* fix: re-sync (#2121)

* Lint

* Add ExtenionStores to the fetchExtensions result since its possible for the stores to change.

* Use a single version of ContentRating

* Exclude ServerConfig.extensionStores from GraphQL

* Use syncDbToPrefs in ExtensionStoreMutation

* Optimize Imports

* Update server/server-config/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt

Co-authored-by: Constantin Piber <59023762+cpiber@users.noreply.github.com>

* Remove replaceWith and add specific description for GQL APIs

* Include OkHttp ZSTD

* Update to latest Mihon extension lib

* Fix latest Mihon Extension Lib

* Lint

* Optimize imports

* Lint

* Review fixes

* Add a index to extesnion table store url

* Lint

---------

Co-authored-by: Constantin Piber <59023762+cpiber@users.noreply.github.com>
2026-06-27 13:39:28 -04:00
herowinb
c8f5d83e9c add reportSyncEvent for SyncYomi service (#2110)
* add reportSyncEvent

* Update SyncYomiSyncService.kt
2026-06-17 22:46:21 -04:00
Zeedif
be5e3f022e feat(download): improve chapter download filenames (#2100)
* feat(download): improve chapter download filenames

* refactor(download): use SafePath helper for filename sanitization
2026-06-17 22:41:31 -04:00
renovate[bot]
40a21fabca Update dex2jar to v2.4.37 (#2122)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-17 22:39:37 -04:00
Zeedif
b33069f107 fix(opds): resolve sql group by syntax error when filtering library (#2118) 2026-06-17 22:39:30 -04:00
Zeedif
14ab3aa9f4 fix(opds): handle dead sources and prevent kosync binary hash crashes (#2116) 2026-06-17 22:39:22 -04:00
Weblate (bot)
bfa0038f53 Weblate translations (#1903)
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/el/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/ja/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/pl/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/ru/
Translation: Suwayomi/Suwayomi-Server

Co-authored-by: Micka149 <dr.mischutckin2017@yandex.ru>
Co-authored-by: Philip Prescott-Decie <presdec@gmail.com>
Co-authored-by: Roland Vezsenyi <miscogd5yf2paqvxvc@farvoid.com>
Co-authored-by: Syer10 <Mitchellptbo@gmail.com>
Co-authored-by: TheRay82 <raycoc1382@gmail.com>
Co-authored-by: UnknownSkyrimPasserby <f7022961@opayq.com>
Co-authored-by: 圭紫 <kaceykoo@gmail.com>
2026-06-17 22:39:15 -04:00
76 changed files with 1042 additions and 515 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.2.2100"
Example: "v2.3.2238"
validations:
required: true
@@ -143,11 +143,13 @@ body:
options:
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
required: true
- label: I have checked the ongoing preview changelog of **[Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI/blob/master/CHANGELOG.md)** and **[Suwayomi-Server](https://github.com/Suwayomi/Suwayomi-Server/blob/master/CHANGELOG.md)** and this bug has **NOT** been listed as fixed
required: true
- label: I have written a short but informative title (ideally less than ~100 characters).
required: true
- label: I have tried the troubleshooting guide described in [README.md](https://github.com/Suwayomi/Suwayomi-Server?tab=readme-ov-file#troubleshooting-and-support)
required: true
- label: I have updated to the **[latest version](https://github.com/suwayomi/suwayomi-server/releases/latest)**.
- label: I have updated the (**[Suwayomi-WebUI](https://github.com/suwayomi/suwayomi-webui/releases/latest)** and **[Suwayomi-Server](https://github.com/suwayomi/suwayomi-server/releases/latest)**) to the latest versions
required: true
- label: I have filled out all of the requested information in this form, including specific version numbers.
required: true

View File

@@ -31,7 +31,7 @@ body:
required: true
- label: I have written a short but informative title (ideally less than ~100 characters).
required: true
- label: I have updated to the **[latest version](https://github.com/suwayomi/suwayomi-server/releases/latest)**.
- label: I have updated the (**[Suwayomi-WebUI](https://github.com/suwayomi/suwayomi-webui/releases/latest)** and **[Suwayomi-Server](https://github.com/suwayomi/suwayomi-server/releases/latest)**) to the latest versions
required: true
- label: I have filled out all of the requested information in this form, including specific version numbers.
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_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

View File

@@ -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 }}

View File

@@ -6,10 +6,57 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased] (Preview)
### Added
- .
### 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

View File

@@ -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"

View File

@@ -8,7 +8,7 @@ javalin = "7.2.2"
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.36"
dex2jar = "2.4.37"
polyglot = "25.0.3"
settings = "1.3.0"
twelvemonkeys = "3.13.1"
@@ -39,7 +39,7 @@ serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization-jvm", v
# Logging
slf4japi = "org.slf4j:slf4j-api:2.0.18"
logback = "ch.qos.logback:logback-classic:1.5.34"
logback = "ch.qos.logback:logback-classic:1.5.38"
kotlinlogging = "io.github.oshai:kotlin-logging-jvm:8.0.4"
# OkHttp
@@ -47,6 +47,7 @@ okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp" }
okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", version.ref = "okhttp" }
okhttp-zstd = { module = "com.squareup.okhttp3:okhttp-zstd", version.ref = "okhttp" }
okio = "com.squareup.okio:okio:3.17.0"
# Javalin api
@@ -228,6 +229,7 @@ okhttp = [
"okhttp-logging",
"okhttp-dnsoverhttps",
"okhttp-brotli",
"okhttp-zstd",
]
javalin = [
"javalin-core",

View File

@@ -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

View File

@@ -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/

View File

@@ -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)" />

View File

@@ -50,6 +50,7 @@
<!-- OPDS errors -->
<string name="opds_error_manga_not_found">Series with ID %1$d not found.</string>
<string name="opds_error_chapter_not_found">Chapter with index %1$d not found.</string>
<string name="opds_error_chapters_not_found">No chapters found or the source is unreachable on page %1$d.</string>
<!-- OPDS facets (Filters and Sorting) -->
<string name="opds_facetgroup_sort_order">Sort Order</string>

View File

@@ -8,7 +8,7 @@
<string name="opds_feeds_root">Suwayomi OPDS Katalog</string>
<string name="opds_feeds_chapter_details">%1$s | %2$s | Details</string>
<string name="opds_feeds_sources_title">Alle Quellen</string>
<string name="opds_feeds_genres_title">Genres</string>
<string name="opds_feeds_genres_title">Genren</string>
<string name="opds_feeds_genres_entry_content">Durchsuche Serien nach Genre</string>
<string name="opds_feeds_status_entry_content">Durchsuche Serien nach Publikationsstatus</string>
<string name="opds_feeds_languages_title">Sprachen</string>
@@ -122,4 +122,7 @@
<string name="webview_label_login_required">Deine Konfiguration erfordert die Anmeldung. Bitte gib Benutzername und Passwort ein.</string>
<string name="opds_linktitle_first_page">Erste Seite</string>
<string name="opds_linktitle_last_page">Letzte Seite</string>
<string name="opds_error_chapters_not_found">Keine Kapitel gefunden oder die Quelle ist nicht erreichbar auf Seite %1$d.</string>
<string name="opds_chapter_title_fallback">Kapitel %1$s</string>
<string name="opds_chapter_title_oneshot">Oneshot</string>
</resources>

View File

@@ -0,0 +1,127 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="opds_feeds_root">Suwayomi OPDS Κατάλογος</string>
<string name="opds_feeds_manga_chapters">Κεφάλαια %1$s</string>
<string name="opds_feeds_chapter_details">%1$s | %2$s | Λεπτομέρειες</string>
<string name="opds_feeds_explore_title">Εξερεύνηση</string>
<string name="opds_feeds_explore_entry_content">Εξερεύνησε νέες σειρές από τις πηγές σου</string>
<string name="opds_feeds_history_title">Ιστορικό</string>
<string name="opds_feeds_history_entry_content">Πρόσφατα διαβασμένα κεφάλαια</string>
<string name="opds_feeds_all_series_in_library_title">Όλες οι σειρές</string>
<string name="opds_feeds_all_series_in_library_entry_content">Περιήγηση σε όλες τις σειρές της βιβλιοθήκης σου</string>
<string name="opds_feeds_library_sources_title">Πηγές</string>
<string name="opds_feeds_library_sources_entry_content">Περιήγηση σε σειρές της βιβλιοθήκης σου φιλτραρισμένες κατά πηγή</string>
<string name="opds_feeds_categories_title">Κατηγορίες</string>
<string name="opds_feeds_categories_entry_content">Περιήγηση σε σειρές οργανωμένες κατά κατηγορία</string>
<string name="opds_feeds_genres_title">Είδη</string>
<string name="opds_feeds_genres_entry_content">Περιήγηση σε σειρές κατά ετικέτες είδους</string>
<string name="opds_feeds_status_title">Κατάσταση</string>
<string name="opds_feeds_status_entry_content">Περιήγηση σε σειρές κατά κατάσταση δημοσίευσης</string>
<string name="opds_feeds_languages_title">Γλώσσες</string>
<string name="opds_feeds_languages_entry_content">Περιήγηση σε σειρές κατά γλώσσα περιεχομένου</string>
<string name="opds_feeds_library_updates_title">Ιστορικό ενημερώσεων βιβλιοθήκης</string>
<string name="opds_feeds_library_updates_entry_content">Πρόσφατα ενημερωμένα κεφάλαια από τη βιβλιοθήκη σου</string>
<string name="opds_feeds_search_results_title">Αποτελέσματα αναζήτησης</string>
<string name="opds_feeds_sources_title">Όλες οι πηγές</string>
<string name="opds_feeds_category_specific_title">Κατηγορία: %1$s</string>
<string name="opds_feeds_genre_specific_title">Είδος: %1$s</string>
<string name="opds_feeds_status_specific_title">Κατάσταση: %1$s</string>
<string name="opds_feeds_language_specific_title">Γλώσσα: %1$s</string>
<string name="opds_feeds_source_specific_title">Πηγή: %1$s</string>
<string name="opds_feeds_library_source_specific_title">Βιβλιοθήκη - Πηγή: %1$s</string>
<string name="opds_feeds_source_specific_popular_title">Πηγή: %1$s - Δημοφιλές</string>
<string name="opds_feeds_source_specific_latest_title">Πηγή: %1$s - Τελευταίο</string>
<string name="opds_search_shortname">Suwayomi OPDS Αναζήτηση</string>
<string name="opds_search_description">Αναζήτηση σειρών στον κατάλογο.</string>
<string name="opds_error_manga_not_found">Η σειρά με ID %1$d δεν βρέθηκε.</string>
<string name="opds_error_chapter_not_found">Το κεφάλαιο με δείκτη %1$d δεν βρέθηκε.</string>
<string name="opds_facetgroup_sort_order">Σειρά ταξινόμησης</string>
<string name="opds_facetgroup_filter_read_status">Φίλτρο κατά κατάσταση ανάγνωσης</string>
<string name="opds_facetgroup_filter_content">Φίλτρο περιεχομένου</string>
<string name="opds_facetgroup_filter_source">Φίλτρο κατά πηγή</string>
<string name="opds_facetgroup_filter_category">Φίλτρο κατά κατηγορία</string>
<string name="opds_facetgroup_filter_status">Φίλτρο κατά κατάσταση</string>
<string name="opds_facetgroup_filter_language">Φίλτρο κατά γλώσσα</string>
<string name="opds_facetgroup_filter_genre">Φίλτρο κατά είδος</string>
<string name="opds_facet_sort_oldest_first">Παλαιότερα πρώτα</string>
<string name="opds_facet_sort_newest_first">Νεότερα πρώτα</string>
<string name="opds_facet_sort_date_asc">Ημερομηνία αύξουσα</string>
<string name="opds_facet_sort_date_desc">Ημερομηνία φθίνουσα</string>
<string name="opds_facet_sort_popular">Δημοφιλές</string>
<string name="opds_facet_sort_latest">Τελευταίο</string>
<string name="opds_facet_sort_alpha_asc">Αλφαβητικά Α</string>
<string name="opds_facet_sort_alpha_desc">Αλφαβητικά Ω-Α</string>
<string name="opds_facet_sort_last_read_desc">Τελευταία ανάγνωση</string>
<string name="opds_facet_sort_latest_chapter_desc">Τελευταίο κεφάλαιο</string>
<string name="opds_facet_sort_date_added_desc">Ημερομηνία προσθήκης</string>
<string name="opds_facet_sort_unread_desc">Αδιάβαστα κεφάλαια</string>
<string name="opds_facet_filter_all">Όλα</string>
<string name="opds_facet_filter_all_chapters">Όλα τα κεφάλαια</string>
<string name="opds_facet_filter_unread_only">Αδιάβαστα</string>
<string name="opds_facet_filter_read_only">Διαβασμένα</string>
<string name="opds_facet_filter_downloaded">Ληφθέντα</string>
<string name="opds_facet_filter_ongoing">Σε εξέλιξη</string>
<string name="opds_facet_filter_completed">Ολοκληρωμένα</string>
<string name="opds_facet_all_sources">Όλες οι πηγές</string>
<string name="opds_facet_all_categories">Όλες οι κατηγορίες</string>
<string name="opds_facet_all_statuses">Όλες οι καταστάσεις</string>
<string name="opds_facet_all_languages">Όλες οι γλώσσες</string>
<string name="opds_facet_all_genres">Όλα τα είδη</string>
<string name="opds_linktitle_catalog_root">Ρίζα καταλόγου</string>
<string name="opds_linktitle_search_catalog">Αναζήτηση καταλόγου</string>
<string name="opds_linktitle_first_page">Πρώτη σελίδα</string>
<string name="opds_linktitle_previous_page">Προηγούμενη σελίδα</string>
<string name="opds_linktitle_next_page">Επόμενη σελίδα</string>
<string name="opds_linktitle_last_page">Τελευταία σελίδα</string>
<string name="opds_linktitle_self_feed">Τρέχουσα ροή</string>
<string name="opds_linktitle_view_on_web">Προβολή στο Web</string>
<string name="opds_linktitle_stream_pages_start">Διάβασε Online</string>
<string name="opds_linktitle_stream_pages_continue">Συνέχισε να διαβάζεις Online</string>
<string name="opds_linktitle_stream_pages_start_local">Διάβασε Online (Τοπική πρόοδος)</string>
<string name="opds_linktitle_stream_pages_continue_local">Συνέχισε να διαβάζεις Online (Τοπική πρόοδος)</string>
<string name="opds_linktitle_stream_pages_start_remote">Διάβασε Online (Συγχρονισμένο από %1$s)</string>
<string name="opds_linktitle_stream_pages_continue_remote">Συνέχισε να διαβάζεις Online (Συγχρονισμένο από %1$s)</string>
<string name="opds_linktitle_download_cbz">Λήψη CBZ</string>
<string name="opds_linktitle_chapter_cover">Εξώφυλλο κεφαλαίου</string>
<string name="opds_linktitle_view_chapter_details">Λεπτομέρειες κεφαλαίου &amp; Σελίδες</string>
<string name="opds_chapter_status_read"></string>
<string name="opds_chapter_status_in_progress"></string>
<string name="opds_chapter_status_unread"></string>
<string name="opds_chapter_status_downloaded">⬇️</string>
<string name="opds_chapter_status_synced">🌐</string>
<string name="opds_chapter_details_base">Σειρά: %1$s | %2$s</string>
<string name="opds_chapter_details_scanlator">| Από %1$s</string>
<string name="opds_chapter_details_progress">| Πρόοδος: %1$d από %2$d</string>
<string name="opds_manga_summary_status">Κατάσταση: %1$s</string>
<string name="opds_manga_summary_source">Πηγή: %1$s</string>
<string name="opds_manga_summary_language">Γλώσσα: %1$s</string>
<string name="manga_status_ongoing">Σε εξέλιξη</string>
<string name="manga_status_completed">Ολοκληρωμένο</string>
<string name="manga_status_licensed">Υπό άδεια</string>
<string name="manga_status_publishing_finished">Δημοσίευση ολοκληρωμένη</string>
<string name="manga_status_cancelled">Ακυρωμένο</string>
<string name="manga_status_on_hiatus">Σε παύση</string>
<string name="manga_status_unknown">Άγνωστο</string>
<string name="label_error">Σφάλμα</string>
<string name="label_version">Έκδοση <xliff:g id="version" example="v2.0.1833">%1$s</xliff:g></string>
<string name="label_close">Κλείσε</string>
<string name="webview_label_title">Suwayomi WebView</string>
<string name="webview_label_disconnected">Αποσυνδεδεμένο, κάνε ανανέωση</string>
<string name="webview_label_reversescroll">Αντίστροφη κύλιση</string>
<string name="webview_label_bindingshint">Σημείωση: Όταν η εστίαση είναι στο τμήμα WebView, καμία συντόμευση πληκτρολογίου, συμπεριλαμβανομένης της ανανέωσης, δεν θα αντιμετωπίζεται από το πρόγραμμα περιήγησης.</string>
<string name="webview_label_init">Αρχικοποίηση... Παρακαλώ περίμενε</string>
<string name="webview_label_getstarted">Εισήγαγε μια διεύθυνση URL για να ξεκινήσεις</string>
<string name="webview_label_loading">Φόρτωση σελίδας...</string>
<string name="webview_label_copy">Αντιγραφή στο πρόχειρο</string>
<string name="webview_label_copy_description">Η αυτόματη αντιγραφή στο πρόχειρο απέτυχε, χρησιμοποίησε το παρακάτω πεδίο για να αντιγράψεις χειροκίνητα την τιμή.</string>
<string name="webview_label_login_required">Η διαμόρφωσή σου απαιτεί σύνδεση. Εισήγαγε όνομα χρήστη και κωδικό.</string>
<string name="webview_placeholder_url">Εισήγαγε URL...</string>
<string name="login_label_title">Suwayomi Σύνδεση</string>
<string name="login_label_username">Όνομα χρήστη</string>
<string name="login_label_password">Κωδικός</string>
<string name="login_label_login">Σύνδεση</string>
<string name="login_placeholder_username">Πληκτρολόγησε όνομα χρήστη...</string>
<string name="login_placeholder_password">Μυστικό...</string>
<string name="opds_error_chapters_not_found">Δεν βρέθηκαν κεφάλαια ή η πηγή είναι μη διαθέσιμη στη σελίδα %1$d.</string>
<string name="opds_chapter_title_fallback">Κεφάλαιο %1$s</string>
</resources>

View File

@@ -122,4 +122,6 @@
<string name="webview_label_login_required">Su configuración requiere que inicie sesión. Introduzca su nombre de usuario y contraseña.</string>
<string name="opds_linktitle_first_page">Primera página</string>
<string name="opds_linktitle_last_page">Última página</string>
<string name="opds_error_chapters_not_found">No se encontraron capítulos o la fuente no está disponible en la página %1$d.</string>
<string name="opds_chapter_title_fallback">Capítulo %1$s</string>
</resources>

View File

@@ -122,4 +122,7 @@
<string name="login_label_login">Se connecter</string>
<string name="login_placeholder_username">Tapez le nom d\'utilisateur…</string>
<string name="login_placeholder_password">Secret…</string>
<string name="opds_error_chapters_not_found">Aucun chapitre trouvé ou la source est inaccessible à la page %1$d.</string>
<string name="opds_chapter_title_fallback">Chapitre %1$s</string>
<string name="opds_chapter_title_oneshot">One shot</string>
</resources>

View File

@@ -122,4 +122,6 @@
<string name="login_label_login">Accedi</string>
<string name="login_placeholder_username">Digita il nome utente...</string>
<string name="login_placeholder_password">Segreto...</string>
<string name="opds_error_chapters_not_found">Nessun capitolo trovato o la fonte non è raggiungibile alla pagina %1$d.</string>
<string name="opds_chapter_title_fallback">Capitolo %1$s</string>
</resources>

View File

@@ -60,4 +60,7 @@
<string name="opds_feeds_library_sources_title">ソース</string>
<string name="opds_feeds_library_sources_entry_content">ソース別にライブラリ内のマンガを閲覧</string>
<string name="opds_feeds_search_results_title">検索結果</string>
<string name="opds_error_chapters_not_found">ページ %1$d で章が見つからないか、ソースに接続できません。</string>
<string name="opds_chapter_title_oneshot">読み切り</string>
<string name="opds_chapter_title_fallback">第 %1$s 話</string>
</resources>

View File

@@ -76,4 +76,6 @@
<string name="opds_facet_filter_all">Wszystkie</string>
<string name="opds_facet_filter_downloaded">Pobrane</string>
<string name="opds_facet_filter_ongoing">Trwające</string>
<string name="opds_error_chapters_not_found">Nie znaleziono rozdziałów lub źródło jest nieosiągalne na stronie %1$d.</string>
<string name="opds_chapter_title_fallback">Rozdział %1$s</string>
</resources>

View File

@@ -122,4 +122,6 @@
<string name="login_label_login">Entrar</string>
<string name="login_placeholder_username">Digite o nome de usuário...</string>
<string name="login_placeholder_password">Segredo...</string>
<string name="opds_error_chapters_not_found">Nenhum capítulo encontrado ou a fonte está inacessível na página %1$d.</string>
<string name="opds_chapter_title_fallback">Capítulo %1$s</string>
</resources>

View File

@@ -122,4 +122,7 @@
<string name="opds_search_description">Ищите тайтлы в каталоге.</string>
<string name="opds_error_manga_not_found">Тайтл с ID %1$d не найден.</string>
<string name="opds_chapter_details_base">Тайтл: %1$s | %2$s</string>
<string name="opds_error_chapters_not_found">Главы не найдены или источник недоступен на странице %1$d.</string>
<string name="opds_chapter_title_fallback">Глава %1$s</string>
<string name="opds_chapter_title_oneshot">Ваншот</string>
</resources>

View File

@@ -53,4 +53,7 @@
<string name="opds_chapter_status_unread"></string>
<string name="opds_chapter_details_base">%1$s | %2$s</string>
<string name="opds_feeds_genre_specific_title">இசைவகை: %1$s</string>
<string name="opds_error_chapters_not_found">பக்கம் %1$d இல் அத்தியாயங்கள் எதுவும் காணப்படவில்லை அல்லது மூலத்தை அணுக முடியவில்லை.</string>
<string name="opds_chapter_title_oneshot">ஒன்-ஷாட்</string>
<string name="opds_chapter_title_fallback">அத்தியாயம் %1$s</string>
</resources>

View File

@@ -122,4 +122,6 @@
<string name="webview_label_login_required">Cấu hình của bạn yêu cầu bạn phải đăng nhập. Vui lòng nhập tên người dùng và mật khẩu.</string>
<string name="opds_linktitle_first_page">Trang đầu</string>
<string name="opds_linktitle_last_page">Trang cuối</string>
<string name="opds_error_chapters_not_found">Không tìm thấy chương nào hoặc nguồn không thể truy cập tại trang %1$d.</string>
<string name="opds_chapter_title_fallback">Chương %1$s</string>
</resources>

View File

@@ -122,4 +122,7 @@
<string name="login_placeholder_username">输入用户名…</string>
<string name="login_placeholder_password">密匙…</string>
<string name="label_error">错误</string>
<string name="opds_error_chapters_not_found">第 %1$d 页未找到任何章节,或图源无法访问。</string>
<string name="opds_chapter_title_fallback">第 %1$s 章</string>
<string name="opds_chapter_title_oneshot">单篇</string>
</resources>

View File

@@ -284,8 +284,7 @@ class ServerConfig(
defaultValue = emptyList(),
deprecated =
SettingsRegistry.SettingDeprecated(
replaceWith = "extensionStores",
message = "Replaced with extensionStores",
message = "Replaced with addExtensionStore and removeExtensionStore mutations",
migrateConfigValue = {
@Suppress("UNCHECKED_CAST")
(it.unwrapped() as? List<String>)
@@ -304,7 +303,7 @@ class ServerConfig(
},
),
readMigrated = { extensionStores.value },
setMigrated = { extensionStores.value = (extensionStores.value + it).distinct() },
setMigrated = { extensionStores.value = it.distinct() },
typeInfo =
SettingsRegistry.PartialTypeInfo(
specificType = "List<String>",

View File

@@ -9,7 +9,6 @@ package eu.kanade.tachiyomi.network
import android.content.Context
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
import eu.kanade.tachiyomi.network.interceptor.IgnoreGzipInterceptor
import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
import io.github.oshai.kotlinlogging.KotlinLogging
@@ -22,9 +21,8 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import okhttp3.Cache
import okhttp3.OkHttpClient
import okhttp3.brotli.BrotliInterceptor
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
@@ -64,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)
}
@@ -84,8 +82,6 @@ class NetworkHelper(
),
).addInterceptor(UncaughtExceptionInterceptor())
.addInterceptor(UserAgentInterceptor(::defaultUserAgentProvider))
.addNetworkInterceptor(IgnoreGzipInterceptor())
.addNetworkInterceptor(BrotliInterceptor)
// if (preferences.verboseLogging().get()) {
val httpLoggingInterceptor =

View File

@@ -1,21 +0,0 @@
package eu.kanade.tachiyomi.network.interceptor
import okhttp3.Interceptor
import okhttp3.Response
/**
* To use [okhttp3.brotli.BrotliInterceptor] as a network interceptor,
* add [IgnoreGzipInterceptor] right before it.
*
* This nullifies the transparent gzip of [okhttp3.internal.http.BridgeInterceptor]
* so gzip and Brotli are explicitly handled by the [okhttp3.brotli.BrotliInterceptor].
*/
class IgnoreGzipInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
if (request.header("Accept-Encoding") == "gzip") {
request = request.newBuilder().removeHeader("Accept-Encoding").build()
}
return chain.proceed(request)
}
}

View File

@@ -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
@@ -483,7 +483,7 @@ class LocalSource(
it[versionCode] = 0
it[lang] = LANG
it[extensionLib] = "1.2"
it[contentRating] = 0
it[contentWarning] = 0
it[isInstalled] = true
}
@@ -497,7 +497,7 @@ class LocalSource(
}
val fs = LocalSourceFileSystem(applicationDirs)
registerCatalogueSource(ID to LocalSource(fs, LocalCoverManager(fs)))
registerSource(ID to LocalSource(fs, LocalCoverManager(fs)))
}
}
}

View File

@@ -4,11 +4,15 @@ import android.app.Application
import android.content.Context
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.PUT
import eu.kanade.tachiyomi.network.await
import io.github.oshai.kotlinlogging.KotlinLogging
import io.javalin.http.HttpStatus
import kotlinx.coroutines.CancellationException
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import kotlinx.serialization.protobuf.ProtoBuf
import okhttp3.Headers
import okhttp3.MediaType.Companion.toMediaType
@@ -36,31 +40,66 @@ object SyncYomiSyncService {
message: String?,
) : Exception(message)
@Serializable
private data class SyncEvent(
val event: SyncEventStatus,
val device_Name: String? = null,
val message: String? = null,
)
@Serializable
private enum class SyncEventStatus {
SYNC_STARTED,
SYNC_SUCCESS,
SYNC_FAILED,
SYNC_ERROR,
SYNC_CANCELLED,
}
suspend fun doSync(
syncData: SyncData,
startDate: Instant,
setSyncState: (SyncManager.SyncState) -> Unit,
): Backup? {
reportSyncEvent(SyncEventStatus.SYNC_STARTED)
setSyncState(SyncManager.SyncState.Downloading(startDate))
val (remoteData, etag) = pullSyncData()
val finalSyncData =
if (remoteData != null) {
require(etag.isNotEmpty()) { "ETag should never be empty if remote data is not null" }
logger.debug { "Try update remote data with ETag($etag)" }
setSyncState(SyncManager.SyncState.Merging(startDate))
mergeSyncData(syncData, remoteData)
} else {
// init or overwrite remote data
logger.debug { "Try overwrite remote data with ETag($etag)" }
syncData
return try {
val (remoteData, etag) = pullSyncData()
val finalSyncData =
if (remoteData != null) {
require(etag.isNotEmpty()) { "ETag should never be empty if remote data is not null" }
logger.debug { "Try update remote data with ETag($etag)" }
setSyncState(SyncManager.SyncState.Merging(startDate))
mergeSyncData(syncData, remoteData)
} else {
// init or overwrite remote data
logger.debug { "Try overwrite remote data with ETag($etag)" }
syncData
}
if (finalSyncData.backup != null) {
setSyncState(SyncManager.SyncState.Uploading(startDate))
}
if (finalSyncData.backup != null) {
setSyncState(SyncManager.SyncState.Uploading(startDate))
val success = pushSyncData(finalSyncData, etag)
if (success) {
reportSyncEvent(SyncEventStatus.SYNC_SUCCESS)
} else {
reportSyncEvent(SyncEventStatus.SYNC_FAILED, "Failed to push sync data")
}
finalSyncData.backup
} catch (e: Exception) {
if (e is CancellationException) {
reportSyncEvent(SyncEventStatus.SYNC_CANCELLED, e.message)
throw e
}
logger.error { "Error syncing: ${e.message}" }
reportSyncEvent(SyncEventStatus.SYNC_ERROR, e.message)
throw e
}
pushSyncData(finalSyncData, etag)
return finalSyncData.backup
}
private suspend fun pullSyncData(): Pair<SyncData?, String> {
@@ -122,8 +161,8 @@ object SyncYomiSyncService {
private suspend fun pushSyncData(
syncData: SyncData,
eTag: String,
) {
val backup = syncData.backup ?: return
): Boolean {
val backup = syncData.backup ?: return true
val host = serverConfig.syncYomiHost.value
val apiKey = serverConfig.syncYomiApiKey.value
@@ -160,7 +199,7 @@ object SyncYomiSyncService {
val response = client.newCall(uploadRequest).await()
if (response.isSuccessful) {
return if (response.isSuccessful) {
val newETag =
response.headers["ETag"]
?.takeIf { it.isNotEmpty() } ?: throw SyncYomiException("Missing ETag")
@@ -169,12 +208,53 @@ object SyncYomiSyncService {
.putString("last_sync_etag", newETag)
.apply()
logger.debug { "SyncYomi sync completed" }
true
} else if (response.code == HttpStatus.PRECONDITION_FAILED.code) {
// other clients updated remote data, will try next time
logger.debug { "SyncYomi sync failed with 412" }
false
} else {
val responseBody = response.body.string()
logger.error { "SyncError: $responseBody" }
false
}
}
private suspend fun reportSyncEvent(
event: SyncEventStatus,
message: String? = null,
) {
try {
val host = serverConfig.syncYomiHost.value
val apiKey = serverConfig.syncYomiApiKey.value
val url = "$host/api/sync/event"
val headers = Headers.Builder().add("X-API-Token", apiKey).build()
// Use a fixed server name.
val bodyObj =
SyncEvent(
event = event,
device_Name = "Suwayomi Server",
message = message,
)
val jsonBody = Json.encodeToString(SyncEvent.serializer(), bodyObj)
val requestBody = jsonBody.toRequestBody("application/json; charset=utf-8".toMediaType())
val request =
POST(
url = url,
headers = headers,
body = requestBody,
)
network.client
.newCall(request)
.await()
.close()
} catch (e: Exception) {
logger.error { "Failed to report sync event: ${e.message}" }
}
}
@@ -225,8 +305,7 @@ object SyncYomiSyncService {
logger.debug { "Starting merge. Local list size: ${localMangaListSafe.size}, Remote list size: ${remoteMangaListSafe.size}" }
fun mangaCompositeKey(manga: BackupManga): String =
"${manga.source}|${manga.url}|${manga.title.lowercase().trim()}|${manga.author?.lowercase()?.trim()}"
fun mangaCompositeKey(manga: BackupManga): String = "${manga.source}|${manga.url}"
// Create maps using composite keys
val localMangaMap = localMangaListSafe.associateBy { mangaCompositeKey(it) }
@@ -335,7 +414,7 @@ object SyncYomiSyncService {
return remoteChapters // If not syncing chapters, keep remote untouched
}
fun chapterCompositeKey(chapter: BackupChapter): String = "${chapter.url}|${chapter.name}|${chapter.chapterNumber}"
fun chapterCompositeKey(chapter: BackupChapter): String = chapter.url
val localChapterMap = localChapters.associateBy { chapterCompositeKey(it) }
val remoteChapterMap = remoteChapters.associateBy { chapterCompositeKey(it) }

View File

@@ -21,26 +21,6 @@ class ExtensionStoreDataLoader : KotlinDataLoader<String, ExtensionStoreType> {
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, ExtensionStoreType> =
DataLoaderFactory.newDataLoader { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val manga =
ExtensionStoreTable
.selectAll()
.where { ExtensionStoreTable.indexUrl inList ids }
.map { ExtensionStoreType(it) }
.associateBy { it.indexUrl }
ids.map { manga[it] }
}
}
}
}
class ExtensionStoreForExtension : KotlinDataLoader<String, ExtensionStoreType> {
override val dataLoaderName = "ExtensionStoreForExtension"
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, ExtensionStoreType> =
DataLoaderFactory.newDataLoader<String, ExtensionStoreType> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
@@ -50,14 +30,14 @@ class ExtensionStoreForExtension : KotlinDataLoader<String, ExtensionStoreType>
.where { ExtensionStoreTable.indexUrl inList ids }
.map { ExtensionStoreType(it) }
.associateBy { it.indexUrl }
ids.map { (extensionStoreByIndexUrl[it]) }
ids.map { extensionStoreByIndexUrl[it] }
}
}
}
}
class ExtensionForExtensionStore : KotlinDataLoader<String, ExtensionNodeList> {
override val dataLoaderName = "ExtensionForExtensionStore"
class ExtensionsForExtensionStore : KotlinDataLoader<String, ExtensionNodeList> {
override val dataLoaderName = "ExtensionsForExtensionStore"
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, ExtensionNodeList> =
DataLoaderFactory.newDataLoader<String, ExtensionNodeList> { ids ->

View File

@@ -95,6 +95,7 @@ class ExtensionStoreMutation {
contactDiscord = it.contactDiscord,
indexUrl = it.indexUrl,
isLegacy = it.isLegacy,
extensionListUrl = it.extensionListUrl,
)
},
)

View File

@@ -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?>
}
}

View File

@@ -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 -> {

View File

@@ -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,

View File

@@ -21,7 +21,7 @@ import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import suwayomi.tachidesk.graphql.directives.RequireAuth
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
import suwayomi.tachidesk.graphql.queries.filter.ContentRatingFilter
import suwayomi.tachidesk.graphql.queries.filter.ContentWarningFilter
import suwayomi.tachidesk.graphql.queries.filter.Filter
import suwayomi.tachidesk.graphql.queries.filter.HasGetOp
import suwayomi.tachidesk.graphql.queries.filter.IntFilter
@@ -43,7 +43,7 @@ import suwayomi.tachidesk.graphql.server.primitives.lessNotUnique
import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
import suwayomi.tachidesk.graphql.types.ExtensionNodeList
import suwayomi.tachidesk.graphql.types.ExtensionType
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
import suwayomi.tachidesk.manga.model.table.ExtensionTable
import java.util.concurrent.CompletableFuture
@@ -108,9 +108,9 @@ class ExtensionQuery {
val versionCode: Int? = null,
val versionCodeLong: Long? = null,
val lang: String? = null,
@GraphQLDeprecated("", ReplaceWith("contentRating"))
@GraphQLDeprecated("", ReplaceWith("contentWarning"))
val isNsfw: Boolean? = null,
val contentRating: ContentRating? = null,
val contentWarning: ContentWarning? = null,
val isInstalled: Boolean? = null,
val hasUpdate: Boolean? = null,
val isObsolete: Boolean? = null,
@@ -129,10 +129,10 @@ class ExtensionQuery {
opAnd.eq(versionCodeLong, ExtensionTable.versionCode)
opAnd.eq(lang, ExtensionTable.lang)
opAnd.eq(
isNsfw?.let { if (it) ContentRating.PORNOGRAPHIC.ordinal else ContentRating.SAFE.ordinal },
ExtensionTable.contentRating,
isNsfw?.let { if (it) ContentWarning.MIXED.ordinal else ContentWarning.SAFE.ordinal },
ExtensionTable.contentWarning,
)
opAnd.eq(contentRating?.ordinal, ExtensionTable.contentRating)
opAnd.eq(contentWarning?.ordinal, ExtensionTable.contentWarning)
opAnd.eq(isInstalled, ExtensionTable.isInstalled)
opAnd.eq(hasUpdate, ExtensionTable.hasUpdate)
opAnd.eq(isObsolete, ExtensionTable.isObsolete)
@@ -156,9 +156,9 @@ class ExtensionQuery {
val versionCode: IntFilter? = null,
val versionCodeLong: LongFilter? = null,
val lang: StringFilter? = null,
@GraphQLDeprecated("", ReplaceWith("storeIndexUrl"))
@GraphQLDeprecated("", ReplaceWith("contentWarning"))
val isNsfw: BooleanFilter? = null,
val contentRating: ContentRatingFilter? = null,
val contentWarning: ContentWarningFilter? = null,
val isInstalled: BooleanFilter? = null,
val hasUpdate: BooleanFilter? = null,
val isObsolete: BooleanFilter? = null,
@@ -179,7 +179,7 @@ class ExtensionQuery {
andFilterWithCompareString(ExtensionTable.versionName, versionName),
andFilterWithCompare(ExtensionTable.versionCode, versionCodeLong),
andFilterWithCompareString(ExtensionTable.lang, lang),
andFilterWithCompareEnum(ExtensionTable.contentRating, contentRating),
andFilterWithCompareEnum(ExtensionTable.contentWarning, contentWarning),
andFilterWithCompare(ExtensionTable.isInstalled, isInstalled),
andFilterWithCompare(ExtensionTable.hasUpdate, hasUpdate),
andFilterWithCompare(ExtensionTable.isObsolete, isObsolete),

View File

@@ -15,13 +15,13 @@ import org.jetbrains.exposed.v1.core.Op
import org.jetbrains.exposed.v1.core.SortOrder
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.greater
import org.jetbrains.exposed.v1.core.greaterEq
import org.jetbrains.exposed.v1.core.less
import org.jetbrains.exposed.v1.core.neq
import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import suwayomi.tachidesk.graphql.directives.RequireAuth
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
import suwayomi.tachidesk.graphql.queries.filter.ContentRatingFilter
import suwayomi.tachidesk.graphql.queries.filter.ContentWarningFilter
import suwayomi.tachidesk.graphql.queries.filter.Filter
import suwayomi.tachidesk.graphql.queries.filter.HasGetOp
import suwayomi.tachidesk.graphql.queries.filter.LongFilter
@@ -42,7 +42,7 @@ import suwayomi.tachidesk.graphql.server.primitives.lessNotUnique
import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
import suwayomi.tachidesk.graphql.types.SourceNodeList
import suwayomi.tachidesk.graphql.types.SourceType
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
import suwayomi.tachidesk.manga.model.table.SourceTable
import java.util.concurrent.CompletableFuture
@@ -95,9 +95,9 @@ class SourceQuery {
val id: Long? = null,
val name: String? = null,
val lang: String? = null,
@GraphQLDeprecated("replace with contentRating == ContentRating.PORNOGRAPHIC", ReplaceWith("contentRating"))
@GraphQLDeprecated("replace with contentWarning == ContentRating.MIXED", ReplaceWith("contentWarning"))
val isNsfw: Boolean? = null,
val contentRating: ContentRating? = null,
val contentWarning: ContentWarning? = null,
) : HasGetOp {
override fun getOp(): Op<Boolean>? {
val opAnd = OpAnd()
@@ -106,13 +106,12 @@ class SourceQuery {
opAnd.eq(lang, SourceTable.lang)
opAnd.andWhere(isNsfw) {
if (it) {
SourceTable.contentRating eq ContentRating.PORNOGRAPHIC.ordinal
SourceTable.contentWarning greaterEq ContentWarning.MIXED.ordinal
} else {
SourceTable.contentRating neq
ContentRating.PORNOGRAPHIC.ordinal
SourceTable.contentWarning less ContentWarning.MIXED.ordinal
}
}
opAnd.andWhere(contentRating) { SourceTable.contentRating eq it.ordinal }
opAnd.andWhere(contentWarning) { SourceTable.contentWarning eq it.ordinal }
return opAnd.op
}
@@ -122,9 +121,9 @@ class SourceQuery {
val id: LongFilter? = null,
val name: StringFilter? = null,
val lang: StringFilter? = null,
@GraphQLDeprecated("replace with contentRating == ContentRating.PORNOGRAPHIC", ReplaceWith("contentRating"))
@GraphQLDeprecated("replace with contentWarning", ReplaceWith("contentWarning"))
val isNsfw: BooleanFilter? = null,
val contentRating: ContentRatingFilter? = null,
val contentWarning: ContentWarningFilter? = null,
override val and: List<SourceFilter>? = null,
override val or: List<SourceFilter>? = null,
override val not: SourceFilter? = null,
@@ -134,7 +133,7 @@ class SourceQuery {
andFilterWithCompareEntity(SourceTable.id, id),
andFilterWithCompareString(SourceTable.name, name),
andFilterWithCompareString(SourceTable.lang, lang),
andFilterWithCompareEnum(SourceTable.contentRating, contentRating),
andFilterWithCompareEnum(SourceTable.contentWarning, contentWarning),
)
}

View File

@@ -28,7 +28,7 @@ import org.jetbrains.exposed.v1.core.upperCase
import org.jetbrains.exposed.v1.core.wrap
import org.jetbrains.exposed.v1.jdbc.Query
import org.jetbrains.exposed.v1.jdbc.andWhere
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
class ILikeEscapeOp(
expr1: Expression<*>,
@@ -330,23 +330,23 @@ data class DoubleFilter(
)
}
data class ContentRatingFilter(
data class ContentWarningFilter(
override val isNull: Boolean? = null,
override val equalTo: ContentRating? = null,
override val notEqualTo: ContentRating? = null,
override val notEqualToAll: List<ContentRating>? = null,
override val notEqualToAny: List<ContentRating>? = null,
override val distinctFrom: ContentRating? = null,
override val distinctFromAll: List<ContentRating>? = null,
override val distinctFromAny: List<ContentRating>? = null,
override val notDistinctFrom: ContentRating? = null,
override val `in`: List<ContentRating>? = null,
override val notIn: List<ContentRating>? = null,
override val lessThan: ContentRating? = null,
override val lessThanOrEqualTo: ContentRating? = null,
override val greaterThan: ContentRating? = null,
override val greaterThanOrEqualTo: ContentRating? = null,
) : ComparableScalarFilter<ContentRating>
override val equalTo: ContentWarning? = null,
override val notEqualTo: ContentWarning? = null,
override val notEqualToAll: List<ContentWarning>? = null,
override val notEqualToAny: List<ContentWarning>? = null,
override val distinctFrom: ContentWarning? = null,
override val distinctFromAll: List<ContentWarning>? = null,
override val distinctFromAny: List<ContentWarning>? = null,
override val notDistinctFrom: ContentWarning? = null,
override val `in`: List<ContentWarning>? = null,
override val notIn: List<ContentWarning>? = null,
override val lessThan: ContentWarning? = null,
override val lessThanOrEqualTo: ContentWarning? = null,
override val greaterThan: ContentWarning? = null,
override val greaterThanOrEqualTo: ContentWarning? = null,
) : ComparableScalarFilter<ContentWarning>
data class StringFilter(
override val isNull: Boolean? = null,

View File

@@ -20,10 +20,9 @@ import suwayomi.tachidesk.graphql.dataLoaders.DisplayScoreForTrackRecordDataLoad
import suwayomi.tachidesk.graphql.dataLoaders.DisplayScoreForTrackSearchDataLoader
import suwayomi.tachidesk.graphql.dataLoaders.DownloadedChapterCountForMangaDataLoader
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionDataLoader
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionForExtensionStore
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionForSourceDataLoader
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionStoreDataLoader
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionStoreForExtension
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionsForExtensionStore
import suwayomi.tachidesk.graphql.dataLoaders.FirstUnreadChapterForMangaDataLoader
import suwayomi.tachidesk.graphql.dataLoaders.GlobalMetaDataLoader
import suwayomi.tachidesk.graphql.dataLoaders.HasDuplicateChaptersForMangaDataLoader
@@ -81,9 +80,8 @@ class TachideskDataLoaderRegistryFactory {
SourceMetaDataLoader(),
ExtensionDataLoader(),
ExtensionForSourceDataLoader(),
ExtensionForExtensionStore(),
ExtensionsForExtensionStore(),
ExtensionStoreDataLoader(),
ExtensionStoreForExtension(),
TrackerDataLoader(),
TrackerStatusesDataLoader(),
TrackerScoresDataLoader(),

View File

@@ -26,6 +26,7 @@ class ExtensionStoreType(
val contactDiscord: String?,
val indexUrl: String,
val isLegacy: Boolean,
val extensionListUrl: String?,
) : Node {
constructor(row: ResultRow) : this(
name = row[ExtensionStoreTable.name],
@@ -35,10 +36,11 @@ class ExtensionStoreType(
contactDiscord = row[ExtensionStoreTable.contactDiscord],
indexUrl = row[ExtensionStoreTable.indexUrl],
isLegacy = row[ExtensionStoreTable.isLegacy],
extensionListUrl = row[ExtensionStoreTable.extensionListUrl],
)
fun extensions(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<ExtensionNodeList> =
dataFetchingEnvironment.getValueFromDataLoader<String, ExtensionNodeList>("ExtensionForExtensionStore", indexUrl)
dataFetchingEnvironment.getValueFromDataLoader<String, ExtensionNodeList>("ExtensionsForExtensionStore", indexUrl)
}
data class ExtensionStoreNodeList(

View File

@@ -18,7 +18,7 @@ import suwayomi.tachidesk.graphql.server.primitives.Node
import suwayomi.tachidesk.graphql.server.primitives.NodeList
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
import suwayomi.tachidesk.manga.impl.extension.Extension
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
import suwayomi.tachidesk.manga.model.table.ExtensionTable
import java.util.concurrent.CompletableFuture
@@ -41,9 +41,9 @@ class ExtensionType(
val versionCode: Int,
val versionCodeLong: Long,
val lang: String,
@GraphQLDeprecated("Removed in extension api v1.6", ReplaceWith("contentRating"))
@GraphQLDeprecated("Removed in extension api v1.6", ReplaceWith("contentWarning"))
val isNsfw: Boolean,
val contentRating: ContentRating,
val contentWarning: ContentWarning,
val isInstalled: Boolean,
val hasUpdate: Boolean,
val isObsolete: Boolean,
@@ -61,8 +61,8 @@ class ExtensionType(
versionCode = row[ExtensionTable.versionCode].toInt(),
versionCodeLong = row[ExtensionTable.versionCode],
lang = row[ExtensionTable.lang],
isNsfw = row[ExtensionTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal,
contentRating = ContentRating.valueOf(row[ExtensionTable.contentRating]),
isNsfw = row[ExtensionTable.contentWarning] >= ContentWarning.MIXED.ordinal,
contentWarning = ContentWarning.valueOf(row[ExtensionTable.contentWarning]),
isInstalled = row[ExtensionTable.isInstalled],
hasUpdate = row[ExtensionTable.hasUpdate],
isObsolete = row[ExtensionTable.isObsolete],
@@ -71,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>("ExtensionStoreForExtension", storeIndexUrl.orEmpty())
fun extensionStore(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<ExtensionStoreType?> =
dataFetchingEnvironment.getValueFromDataLoader<String, ExtensionStoreType?>("ExtensionStoreDataLoader", storeIndexUrl.orEmpty())
}
data class ExtensionNodeList(

View File

@@ -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,9 +24,9 @@ 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.model.dataclass.ContentRating
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
import java.util.concurrent.CompletableFuture
@@ -42,31 +42,29 @@ class SourceType(
val id: Long,
val name: String,
val lang: String,
val message: String?,
val contentRating: ContentRating,
val contentWarning: ContentWarning,
val iconUrl: String,
val supportsLatest: Boolean,
val isConfigurable: Boolean,
@GraphQLDeprecated("", ReplaceWith("contentRating"))
@GraphQLDeprecated("", ReplaceWith("contentWarning"))
val isNsfw: Boolean,
val displayName: String,
val homeUrl: String?,
@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],
message = row[SourceTable.message],
contentRating = ContentRating.valueOf(row[SourceTable.contentRating]),
contentWarning = ContentWarning.valueOf(row[SourceTable.contentWarning]),
iconUrl = Extension.proxyExtensionIconUrl(sourceExtension[ExtensionTable.pkgName]),
supportsLatest = catalogueSource.supportsLatest,
isConfigurable = catalogueSource is ConfigurableSource,
isNsfw = row[SourceTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal,
displayName = catalogueSource.toString(),
homeUrl = runCatching { (catalogueSource as? HttpSource)?.getHomeUrl() }.getOrNull(),
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
supportsLatest = source.supportsLatest,
isConfigurable = source is ConfigurableSource,
isNsfw = row[SourceTable.contentWarning] >= ContentWarning.MIXED.ordinal,
displayName = source.toString(),
homeUrl = runCatching { (source as? HttpSource)?.getHomeUrl() }.getOrNull(),
baseUrl = runCatching { (source as? HttpSource)?.baseUrl }.getOrNull(),
)
fun manga(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<MangaNodeList> =
@@ -77,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)
@@ -86,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)) {
@@ -298,7 +296,7 @@ data class FilterChange(
)
fun updateFilterList(
source: CatalogueSource,
source: Source,
changes: List<FilterChange>?,
): FilterList {
val filterList = source.getFilterList()

View File

@@ -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)

View File

@@ -16,6 +16,7 @@ import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.manga.model.table.toDataClass
import suwayomi.tachidesk.server.serverConfig
import xyz.nulldev.androidcompat.util.SafePath
import java.io.File
import java.io.InputStream
@@ -76,13 +77,46 @@ object ChapterDownloadHelper {
.select(ChapterTable.columns + MangaTable.columns)
.where { ChapterTable.id eq chapterId }
.firstOrNull() ?: throw IllegalArgumentException("ChapterId $chapterId not found")
val chapter = ChapterTable.toDataClass(row)
val mangaTitle = row[MangaTable.title]
val mangaTitle = row[MangaTable.title].trim()
val scanlatorPart = chapter.scanlator?.let { "[$it] " } ?: ""
val fileName = "$mangaTitle - $scanlatorPart${chapter.name}.cbz"
val scanlatorName = chapter.scanlator?.trim()?.takeIf { it.isNotEmpty() }
val chapterName = chapter.name.trim().takeIf { it.isNotEmpty() }
Pair(chapter, fileName)
val fileName =
buildString {
append(mangaTitle)
append(" - ")
if (chapterName != null) {
append(chapterName)
} else if (chapter.chapterNumber >= 0f) {
// chapterNumber is stored as Float, drop .0 for whole numbers.
val formatNumber =
if (chapter.chapterNumber % 1 == 0f) {
chapter.chapterNumber.toInt().toString()
} else {
chapter.chapterNumber.toString()
}
append("#$formatNumber")
} else {
// Fallback when neither name nor valid chapter number exists
append("#${chapter.index}")
}
if (scanlatorName != null) {
append(" [")
append(scanlatorName)
append("]")
}
append(".cbz")
}
// Sanitize filename for OS compatibility
val safeFileName = SafePath.buildValidFilename(fileName)
Pair(chapter, safeFileName)
}
fun getCbzForDownload(

View File

@@ -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)

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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)
}

View File

@@ -26,10 +26,10 @@ 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.model.dataclass.ContentRating
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
import suwayomi.tachidesk.manga.model.table.SourceMetaTable
@@ -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(
@@ -53,7 +53,7 @@ object Source {
iconUrl = proxyExtensionIconUrl(sourceExtension[ExtensionTable.pkgName]),
supportsLatest = catalogueSource.supportsLatest,
isConfigurable = catalogueSource is ConfigurableSource,
isNsfw = it[SourceTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal,
isNsfw = it[SourceTable.contentWarning] >= ContentWarning.MIXED.ordinal,
displayName = catalogueSource.toString(),
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
)
@@ -64,7 +64,7 @@ object Source {
fun getSource(sourceId: Long): SourceDataClass? { // all the data extracted fresh form the source instance
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(
@@ -74,7 +74,7 @@ object Source {
iconUrl = proxyExtensionIconUrl(extension[ExtensionTable.pkgName]),
supportsLatest = catalogueSource.supportsLatest,
isConfigurable = catalogueSource is ConfigurableSource,
isNsfw = source[SourceTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal,
isNsfw = source[SourceTable.contentWarning] >= ContentWarning.MIXED.ordinal,
displayName = catalogueSource.toString(),
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
)
@@ -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>> =

View File

@@ -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()

View File

@@ -39,7 +39,7 @@ data class BackupManga(
@ProtoNumber(106) var lastModifiedAt: Long = 0,
@ProtoNumber(109) var version: Long = 0,
@ProtoNumber(111) var initialized: Boolean = false,
@ProtoNumber(13) var memo: ByteArray = JsonObjectEmptyBytes,
@ProtoNumber(112) var memo: ByteArray = JsonObjectEmptyBytes,
// suwayomi
@ProtoNumber(9000) var meta: Map<String, String> = emptyMap(),
)

View File

@@ -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) }

View File

@@ -10,7 +10,6 @@ package suwayomi.tachidesk.manga.impl.extension
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
@@ -32,17 +31,19 @@ import suwayomi.tachidesk.manga.impl.util.PackageTools
import suwayomi.tachidesk.manga.impl.util.PackageTools.EXTENSION_FEATURE
import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MAX
import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MIN
import suwayomi.tachidesk.manga.impl.util.PackageTools.METADATA_CONTENT_WARNING
import suwayomi.tachidesk.manga.impl.util.PackageTools.METADATA_EXTENSION_LIB
import suwayomi.tachidesk.manga.impl.util.PackageTools.METADATA_NAME
import suwayomi.tachidesk.manga.impl.util.PackageTools.METADATA_NSFW
import suwayomi.tachidesk.manga.impl.util.PackageTools.METADATA_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.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
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
import suwayomi.tachidesk.manga.model.table.ExtensionTable
import suwayomi.tachidesk.manga.model.table.SourceTable
import suwayomi.tachidesk.server.ApplicationDirs
@@ -151,16 +152,37 @@ object Extension {
// throw Exception("This apk is not a signed with the official tachiyomi signature")
// }
val isNsfw = packageInfo.applicationInfo.metaData.getString(METADATA_NSFW) == "1"
var contentWarning = packageInfo.applicationInfo.metaData.getInt(METADATA_CONTENT_WARNING)
if (contentWarning == 0) {
contentWarning = packageInfo.applicationInfo.metaData
.getString(METADATA_CONTENT_WARNING)
?.toIntOrNull()
?: 0
if (contentWarning == 0) {
contentWarning = packageInfo.applicationInfo.metaData
.getString(METADATA_NSFW)
?.toIntOrNull()
?: 0
}
}
val sourceClass =
packageInfo.applicationInfo.metaData
.getString(METADATA_SOURCE_CLASS)!!
.trim()
val className =
packageInfo.packageName + packageInfo.applicationInfo.metaData.getString(METADATA_SOURCE_CLASS)
if (sourceClass.startsWith(".")) {
packageInfo.packageName + sourceClass
} else {
sourceClass
}
logger.debug { "Main class for extension is $className" }
dex2jar(apkFilePath, jarFilePath, fileNameWithoutType)
extractAssetsFromApk(apkFilePath, jarFilePath)
extractAndCacheApkIcon(apkFilePath, apkName)
extractAndCacheApkIcon(apkFilePath, packageInfo.packageName)
// clean up
File(apkFilePath).delete()
@@ -168,12 +190,12 @@ object Extension {
try {
// collect sources from the extension
val extensionMainClassInstance = loadExtensionSources(jarFilePath, className)
val sources: List<CatalogueSource> =
val sources: List<Source> =
when (extensionMainClassInstance) {
is Source -> listOf(extensionMainClassInstance)
is SourceFactory -> extensionMainClassInstance.createSources()
else -> throw RuntimeException("Unknown source class type! ${extensionMainClassInstance.javaClass}")
}.map { it as CatalogueSource }
}
val langs = sources.map { it.lang }.toSet()
val extensionLang =
@@ -184,9 +206,16 @@ object Extension {
}
val extensionName =
packageInfo.applicationInfo.nonLocalizedLabel
.toString()
.substringAfter("Tachiyomi: ")
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 {
@@ -197,14 +226,9 @@ object Extension {
it[this.pkgName] = packageInfo.packageName
it[versionName] = packageInfo.versionName
it[versionCode] = packageInfo.versionCode.toLong()
it[extensionLib] = extensionLibVersion
it[lang] = extensionLang
// todo will change
it[contentRating] =
if (isNsfw) {
ContentRating.PORNOGRAPHIC.ordinal
} else {
ContentRating.SAFE.ordinal
}
it[this.contentWarning] = contentWarning
}
}
@@ -229,7 +253,7 @@ object Extension {
it[name] = httpSource.name
it[lang] = httpSource.lang
it[extension] = extensionId
it[contentRating] = if (isNsfw) ContentRating.PORNOGRAPHIC.ordinal else ContentRating.SAFE.ordinal
it[this.contentWarning] = contentWarning
}
logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" }
}
@@ -250,7 +274,7 @@ object Extension {
private fun extractAndCacheApkIcon(
apkFilePath: String,
apkName: String,
pkgName: String,
) {
val iconCacheDir = "${applicationDirs.extensionsRoot}/icon"
try {
@@ -263,15 +287,15 @@ object Extension {
?.first
}
if (iconData == null) {
logger.warn { "No icon found in APK $apkName" }
logger.warn { "No icon found in APK $pkgName" }
return
}
File(iconCacheDir).mkdirs()
clearCachedImage(iconCacheDir, apkName)
saveImage("$iconCacheDir/$apkName", iconData.inputStream(), null)
clearCachedImage(iconCacheDir, pkgName)
saveImage("$iconCacheDir/$pkgName", iconData.inputStream(), null)
} catch (e: Exception) {
logger.warn(e) { "Failed to extract icon from APK $apkName" }
logger.warn(e) { "Failed to extract icon from APK $pkgName" }
}
}
@@ -364,7 +388,7 @@ object Extension {
SourceTable.deleteWhere { SourceTable.extension eq extensionId }
if (extensionRecord[ExtensionTable.isObsolete]) {
if (extensionRecord[ExtensionTable.isObsolete] || extensionRecord[ExtensionTable.apkUrl] == null) {
ExtensionTable.deleteWhere { ExtensionTable.pkgName eq pkgName }
} else {
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
@@ -382,7 +406,7 @@ object Extension {
PackageTools.jarLoaderMap.remove(jarPath)?.close()
// clear all loaded sources
sources.forEach { GetCatalogueSource.unregisterCatalogueSource(it) }
sources.forEach { GetSource.unregisterSource(it) }
File(jarPath).delete()
}
@@ -397,7 +421,7 @@ object Extension {
it[versionName] = targetExtension.versionName
it[versionCode] = targetExtension.versionCode
it[lang] = targetExtension.lang
it[contentRating] = targetExtension.contentRating.ordinal
it[contentWarning] = targetExtension.contentWarning.ordinal
it[iconUrl] = targetExtension.iconUrl
it[hasUpdate] = false
}

View File

@@ -15,6 +15,9 @@ import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.okio.decodeFromBufferedSource
import kotlinx.serialization.protobuf.ProtoBuf
import okio.BufferedSource
import okio.buffer
import okio.gzip
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.inList
import org.jetbrains.exposed.v1.jdbc.deleteWhere
@@ -41,45 +44,45 @@ object ExtensionStoreService {
val protoBuf: ProtoBuf by injectLazy()
val json: Json by injectLazy()
suspend fun fetch(indexUrl: String): ExtensionStore = fetch(indexUrl, forceV2 = false)
private suspend fun fetch(
indexUrl: String,
forceV2: Boolean,
): ExtensionStore {
var updatedIndexUrl = indexUrl
suspend fun fetch(indexUrl: String): ExtensionStore {
var updatedIndexUrl: String = indexUrl
return try {
network.client.newCall(GET(indexUrl)).awaitSuccess().body.source().use { source ->
try {
protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.peek().readByteArray())
} catch (e: IllegalArgumentException) {
logger.debug { "Failed to decode as protobuf, trying JSON" }
try {
json.decodeFromBufferedSource<NetworkExtensionStore>(source.peek())
} catch (e: IllegalArgumentException) {
if (forceV2) throw e
logger.debug { "Failed to decode as NetworkExtensionStore, trying LegacyExtensionRepo" }
val legacyIndex =
try {
json.decodeFromBufferedSource<NetworkLegacyExtensionRepo>(source.peek())
} catch (e: IllegalArgumentException) {
val response = network.client.newCall(GET(updatedIndexUrl)).awaitSuccess()
response.body.source().decompressIfGzipped().use { source ->
val networkStore =
when (source.peek().readByte()) {
// "[..."
0x5B.toByte() -> {
run {
if (!indexUrl.endsWith("/index.min.json")) {
throw e
throw IllegalArgumentException("Provided legacy store url is not valid")
}
logger.debug { "Checking for new repo.json format" }
updatedIndexUrl = indexUrl.replace("/index.min.json", "/repo.json")
network.client.newCall(GET(updatedIndexUrl)).awaitSuccess().body.source().use {
json.decodeFromBufferedSource<NetworkLegacyExtensionRepo>(it)
}
}
}
if (legacyIndex.indexV2 != null) {
return fetch(legacyIndex.indexV2, forceV2 = true)
} else {
legacyIndex
// "{..."
0x7B.toByte() -> {
try {
json.decodeFromBufferedSource<NetworkLegacyExtensionRepo>(source.peek())
} catch (_: IllegalArgumentException) {
json.decodeFromBufferedSource<NetworkExtensionStore>(source)
}
}
else -> {
protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.readByteArray())
}
}
}.toExtensionStore(updatedIndexUrl)
if (networkStore is NetworkLegacyExtensionRepo && networkStore.indexV2 != null) {
return fetch(networkStore.indexV2)
}
networkStore.toExtensionStore(updatedIndexUrl)
}
} catch (e: Exception) {
if (e is CancellationException) throw e
@@ -105,6 +108,7 @@ object ExtensionStoreService {
it[contactDiscord] = store.contact.discord
it[indexUrl] = store.indexUrl
it[isLegacy] = store.isLegacy
it[extensionListUrl] = store.extensionListUrl
}
} else {
ExtensionStoreTable.update({ ExtensionStoreTable.indexUrl eq store.indexUrl }) {
@@ -114,6 +118,7 @@ object ExtensionStoreService {
it[contactWebsite] = store.contact.website
it[contactDiscord] = store.contact.discord
it[isLegacy] = store.isLegacy
it[extensionListUrl] = store.extensionListUrl
}
}
}
@@ -210,17 +215,33 @@ object ExtensionStoreService {
suspend fun getExtensions(store: ExtensionStore): List<ExtensionInfo> {
val extensions =
if (!store.isLegacy) {
val response = network.client.newCall(GET(store.indexUrl)).awaitSuccess()
response.body
.source()
.use { source ->
try {
protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.peek().readByteArray())
} catch (_: IllegalArgumentException) {
json.decodeFromBufferedSource<NetworkExtensionStore>(source.peek())
if (store.extensionListUrl != null) {
val response = network.client.newCall(GET(store.extensionListUrl)).awaitSuccess()
response.body.source().decompressIfGzipped().use { source ->
when (source.peek().readByte()) {
// "{..."
0x7B.toByte() -> {
json.decodeFromBufferedSource<NetworkExtensionStore.ExtensionList>(source)
}
else -> {
protoBuf.decodeFromByteArray<NetworkExtensionStore.ExtensionList>(
source.readByteArray(),
)
}
}.toExtensionInfos(store)
}
} else if (!store.isLegacy) {
val response = network.client.newCall(GET(store.indexUrl)).awaitSuccess()
response.body.source().decompressIfGzipped().use { source ->
when (source.peek().readByte()) {
// "{..."
0x7B.toByte() -> json.decodeFromBufferedSource<NetworkExtensionStore>(source)
else -> protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.readByteArray())
}.extensionList!!
.toExtensionInfos(store)
}
} else {
val storeBaseUrl = store.indexUrl.removeSuffix("/repo.json")
val response = network.client.newCall(GET("$storeBaseUrl/index.min.json")).awaitSuccess()
@@ -232,4 +253,17 @@ object ExtensionStoreService {
}
return extensions
}
private fun BufferedSource.decompressIfGzipped(): BufferedSource {
val isGzip =
peek().use { peeked ->
try {
peeked.readShort().toInt() == 0x1f8b
} catch (_: Exception) {
false
}
}
return if (isGzip) gzip().buffer() else this
}
}

View File

@@ -22,7 +22,7 @@ import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.jetbrains.exposed.v1.jdbc.update
import suwayomi.tachidesk.manga.impl.extension.Extension.proxyExtensionIconUrl
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo
import suwayomi.tachidesk.manga.model.table.ExtensionTable
@@ -81,7 +81,7 @@ object ExtensionsList {
versionName = it[ExtensionTable.versionName],
versionCode = it[ExtensionTable.versionCode].toInt(),
lang = it[ExtensionTable.lang],
isNsfw = it[ExtensionTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal,
isNsfw = it[ExtensionTable.contentWarning] >= ContentWarning.MIXED.ordinal,
installed = it[ExtensionTable.isInstalled],
hasUpdate = it[ExtensionTable.hasUpdate],
obsolete = it[ExtensionTable.isObsolete],
@@ -132,6 +132,7 @@ object ExtensionsList {
// Always update icon url and repo
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
this[ExtensionTable.storeIndexUrl] = foundExtension.storeIndexUrl
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
// add these because batch updates need matching columns
this[ExtensionTable.hasUpdate] = extensionRecord[ExtensionTable.hasUpdate]
@@ -173,7 +174,7 @@ object ExtensionsList {
this[ExtensionTable.versionName] = foundExtension.versionName
this[ExtensionTable.versionCode] = foundExtension.versionCode
this[ExtensionTable.lang] = foundExtension.lang
this[ExtensionTable.contentRating] = foundExtension.contentRating.ordinal
this[ExtensionTable.contentWarning] = foundExtension.contentWarning.ordinal
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
}
@@ -190,7 +191,7 @@ object ExtensionsList {
this[ExtensionTable.versionName] = foundExtension.versionName
this[ExtensionTable.versionCode] = foundExtension.versionCode
this[ExtensionTable.lang] = foundExtension.lang
this[ExtensionTable.contentRating] = foundExtension.contentRating.ordinal
this[ExtensionTable.contentWarning] = foundExtension.contentWarning.ordinal
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
}

View File

@@ -11,7 +11,7 @@ import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
import kotlinx.serialization.protobuf.ProtoNumber
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo
import suwayomi.tachidesk.manga.model.dataclass.ExtensionSource
import suwayomi.tachidesk.manga.model.dataclass.ExtensionStore
@@ -23,7 +23,8 @@ data class NetworkExtensionStore(
@ProtoNumber(2) val badgeLabel: String,
@ProtoNumber(3) val signingKey: String,
@ProtoNumber(4) val contact: Contact,
@ProtoNumber(5) val extensions: List<Extension>,
@ProtoNumber(101) val extensionList: ExtensionList?,
@ProtoNumber(102) val extensionListUrl: String?,
) : BaseNetworkExtensionStore {
@Serializable
data class Contact(
@@ -31,6 +32,11 @@ data class NetworkExtensionStore(
@ProtoNumber(2) val discord: String?,
)
@Serializable
data class ExtensionList(
@ProtoNumber(1) val extensions: List<Extension>,
)
@Serializable
data class Extension(
@ProtoNumber(1) val name: String,
@@ -39,7 +45,8 @@ data class NetworkExtensionStore(
@ProtoNumber(4) val extensionLib: String,
@ProtoNumber(5) val versionCode: Long,
@ProtoNumber(6) val versionName: String,
@ProtoNumber(7) val sources: List<Source>,
@ProtoNumber(7) val contentWarning: ContentWarning,
@ProtoNumber(8) val sources: List<Source>,
)
@Serializable
@@ -55,27 +62,27 @@ data class NetworkExtensionStore(
@ProtoNumber(3) val language: String,
@ProtoNumber(4) val homeUrl: String = "",
@ProtoNumber(5) val mirrorUrls: List<String> = emptyList(),
@ProtoNumber(6) val contentRating: ContentRating = ContentRating.SAFE,
// @ProtoNumber(6) val contentWarning: ContentWarning = ContentWarning.SAFE,
@ProtoNumber(7) val message: String? = null,
)
@Serializable
enum class ContentRating {
enum class ContentWarning {
@ProtoNumber(0)
@JsonNames("CONTENT_RATING_SAFE")
SAFE,
@JsonNames("CONTENT_WARNING_UNSPECIFIED")
UNSPECIFIED,
@ProtoNumber(1)
@JsonNames("CONTENT_RATING_SUGGESTIVE")
SUGGESTIVE,
@JsonNames("CONTENT_WARNING_SAFE")
SAFE,
@ProtoNumber(2)
@JsonNames("CONTENT_RATING_EROTICA")
EROTICA,
@JsonNames("CONTENT_WARNING_MIXED")
MIXED,
@ProtoNumber(3)
@JsonNames("CONTENT_RATING_PORNOGRAPHIC")
PORNOGRAPHIC,
@JsonNames("CONTENT_WARNING_NSFW")
NSFW,
}
override fun toExtensionStore(indexUrl: String): ExtensionStore =
@@ -90,10 +97,11 @@ data class NetworkExtensionStore(
discord = contact.discord,
),
isLegacy = false,
extensionListUrl = extensionListUrl,
)
}
fun NetworkExtensionStore.toExtensionInfos(store: ExtensionStore): List<ExtensionInfo> =
fun NetworkExtensionStore.ExtensionList.toExtensionInfos(store: ExtensionStore): List<ExtensionInfo> =
extensions.map { extension ->
val lang = extension.sources.map { it.language }.toSet()
ExtensionInfo(
@@ -106,13 +114,15 @@ fun NetworkExtensionStore.toExtensionInfos(store: ExtensionStore): List<Extensio
versionCode = extension.versionCode,
versionName = extension.versionName,
lang = if (lang.size == 1) lang.first() else "all",
contentRating =
when (extension.sources.maxOfOrNull { it.contentRating }) {
NetworkExtensionStore.ContentRating.SAFE -> ContentRating.SAFE
NetworkExtensionStore.ContentRating.SUGGESTIVE -> ContentRating.SUGGESTIVE
NetworkExtensionStore.ContentRating.EROTICA -> ContentRating.EROTICA
NetworkExtensionStore.ContentRating.PORNOGRAPHIC -> ContentRating.PORNOGRAPHIC
null -> ContentRating.SAFE
contentWarning =
when (extension.contentWarning) {
NetworkExtensionStore.ContentWarning.SAFE,
NetworkExtensionStore.ContentWarning.UNSPECIFIED,
-> ContentWarning.SAFE
NetworkExtensionStore.ContentWarning.MIXED -> ContentWarning.MIXED
NetworkExtensionStore.ContentWarning.NSFW -> ContentWarning.NSFW
},
sources =
extension.sources.map { source ->
@@ -122,12 +132,15 @@ fun NetworkExtensionStore.toExtensionInfos(store: ExtensionStore): List<Extensio
lang = source.language,
homeUrl = source.homeUrl,
message = source.message,
contentRating =
when (source.contentRating) {
NetworkExtensionStore.ContentRating.SAFE -> ContentRating.SAFE
NetworkExtensionStore.ContentRating.SUGGESTIVE -> ContentRating.SUGGESTIVE
NetworkExtensionStore.ContentRating.EROTICA -> ContentRating.EROTICA
NetworkExtensionStore.ContentRating.PORNOGRAPHIC -> ContentRating.PORNOGRAPHIC
contentWarning =
when (extension.contentWarning) { // todo source.contentWarning
NetworkExtensionStore.ContentWarning.SAFE,
NetworkExtensionStore.ContentWarning.UNSPECIFIED,
-> ContentWarning.SAFE
NetworkExtensionStore.ContentWarning.MIXED -> ContentWarning.MIXED
NetworkExtensionStore.ContentWarning.NSFW -> ContentWarning.NSFW
},
)
},

View File

@@ -9,7 +9,7 @@ package suwayomi.tachidesk.manga.impl.extension.github
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo
import suwayomi.tachidesk.manga.model.dataclass.ExtensionSource
import suwayomi.tachidesk.manga.model.dataclass.ExtensionStore
@@ -49,7 +49,7 @@ fun NetworkLegacyExtension.toExtensionInfo(
versionCode = code,
versionName = version,
lang = lang,
contentRating = if (nsfw == 1) ContentRating.PORNOGRAPHIC else ContentRating.SAFE,
contentWarning = if (nsfw == 1) ContentWarning.MIXED else ContentWarning.SAFE,
sources =
if (sources.isNullOrEmpty()) {
listOf(
@@ -59,7 +59,7 @@ fun NetworkLegacyExtension.toExtensionInfo(
lang = lang,
homeUrl = "",
message = null,
contentRating = if (nsfw == 1) ContentRating.PORNOGRAPHIC else ContentRating.SAFE,
contentWarning = if (nsfw == 1) ContentWarning.MIXED else ContentWarning.SAFE,
),
)
} else {
@@ -70,7 +70,7 @@ fun NetworkLegacyExtension.toExtensionInfo(
lang = source.lang,
homeUrl = source.baseUrl,
message = null,
contentRating = if (nsfw == 1) ContentRating.PORNOGRAPHIC else ContentRating.SAFE,
contentWarning = if (nsfw == 1) ContentWarning.MIXED else ContentWarning.SAFE,
)
}
},

View File

@@ -36,5 +36,6 @@ data class NetworkLegacyExtensionRepo(
discord = null,
),
isLegacy = true,
extensionListUrl = null,
)
}

View File

@@ -127,29 +127,36 @@ object KoreaderSyncService {
}
val mangaId = chapterRow[ChapterTable.manga].value
val isDownloaded = chapterRow[ChapterTable.isDownloaded]
val checksumMethod = serverConfig.koreaderSyncChecksumMethod.value
val newHash =
when (checksumMethod) {
KoreaderSyncChecksumMethod.BINARY -> {
logger.debug { "[KOSYNC HASH] No hash for chapterId=$chapterId. Generating from downloaded content." }
try {
// Always create a CBZ in memory if it doesn't exist
val (stream, _) = ChapterDownloadHelper.getArchiveStreamWithSize(mangaId, chapterId)
// Write the stream to a temp file for partial hashing
val tempFile = File.createTempFile("kosync-hash-", ".cbz")
// Only generate binary hash if the chapter is downloaded to avoid fetching missing files
if (isDownloaded) {
logger.debug { "[KOSYNC HASH] No hash for chapterId=$chapterId. Generating from downloaded content." }
try {
tempFile.outputStream().use { fos ->
stream.use { it.copyTo(fos) }
// Always create a CBZ in memory if it doesn't exist
val (stream, _) = ChapterDownloadHelper.getArchiveStreamWithSize(mangaId, chapterId)
// Write the stream to a temp file for partial hashing
val tempFile = File.createTempFile("kosync-hash-", ".cbz")
try {
tempFile.outputStream().use { fos ->
stream.use { it.copyTo(fos) }
}
// Use the same hashing method as for downloads
KoreaderHelper.hashContents(tempFile)
} finally {
// Always delete the temp file
tempFile.delete()
}
// Use the same hashing method as for downloads
KoreaderHelper.hashContents(tempFile)
} finally {
// Always delete the temp file
tempFile.delete()
} catch (e: Exception) {
logger.warn(e) { "[KOSYNC HASH] Failed to generate archive stream for chapterId=$chapterId." }
null
}
} catch (e: Exception) {
logger.warn(e) { "[KOSYNC HASH] Failed to generate archive stream for chapterId=$chapterId." }
} else {
logger.debug { "[KOSYNC HASH] Skipping binary hash for chapterId=$chapterId because it is not downloaded." }
null
}
}
@@ -175,7 +182,7 @@ object KoreaderSyncService {
}
logger.info { "[KOSYNC HASH] Generated and saved new hash for chapterId=$chapterId" }
} else {
logger.warn { "[KOSYNC HASH] Hashing failed for chapterId=$chapterId." }
logger.warn { "[KOSYNC HASH] Hashing failed or skipped for chapterId=$chapterId." }
}
newHash
}

View File

@@ -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()

View File

@@ -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())
}

View File

@@ -40,6 +40,11 @@ object PackageTools {
const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
const val METADATA_NSFW = "tachiyomi.extension.nsfw"
const val METADATA_NAME = "tachiyomix.name"
const val METADATA_EXTENSION_LIB = "tachiyomix.extensionLib"
const val METADATA_CONTENT_WARNING = "tachiyomix.contentWarning"
const val LIB_VERSION_MIN = 1.3
const val LIB_VERSION_MAX = 1.6

View File

@@ -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
@@ -22,14 +21,14 @@ import suwayomi.tachidesk.server.ApplicationDirs
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.ConcurrentHashMap
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
}
@@ -62,25 +61,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)
}

View File

@@ -17,7 +17,7 @@ data class ExtensionInfo(
val versionCode: Long,
val versionName: String,
val lang: String,
val contentRating: ContentRating,
val contentWarning: ContentWarning,
val sources: List<ExtensionSource>,
)
@@ -27,17 +27,16 @@ data class ExtensionSource(
val lang: String,
val homeUrl: String,
val message: String?,
val contentRating: ContentRating,
val contentWarning: ContentWarning,
)
enum class ContentRating {
enum class ContentWarning {
SAFE,
SUGGESTIVE,
EROTICA,
PORNOGRAPHIC,
MIXED,
NSFW,
;
companion object {
fun valueOf(contentRating: Int) = entries.find { it.ordinal == contentRating } ?: SAFE
fun valueOf(contentWarning: Int) = entries.find { it.ordinal == contentWarning } ?: SAFE
}
}

View File

@@ -14,6 +14,7 @@ data class ExtensionStore(
val signingKey: String,
val contact: Contact,
val isLegacy: Boolean,
val extensionListUrl: String?,
) {
data class Contact(
val website: String,

View File

@@ -10,11 +10,12 @@ package suwayomi.tachidesk.manga.model.table
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
object ExtensionStoreTable : IntIdTable() {
val indexUrl = varchar("index_url", 2048)
val indexUrl = varchar("index_url", 2048).uniqueIndex()
val name = varchar("name", 256)
val badgeLabel = varchar("badge_label", 32)
val signingKey = varchar("signing_key", 512)
val contactWebsite = varchar("contact_website", 2048)
val contactDiscord = varchar("contact_discord", 2048).nullable()
val isLegacy = bool("is_legacy").default(false)
val extensionListUrl = varchar("extension_list_url", 2048).nullable()
}

View File

@@ -11,7 +11,7 @@ import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
object ExtensionTable : IntIdTable() {
val apkName = varchar("apk_name", 1024).nullable()
val storeIndexUrl = varchar("store_index_url", 2048).nullable()
val storeIndexUrl = varchar("store_index_url", 2048).nullable().index()
// default is the local source icon from tachiyomi
@Suppress("ktlint:standard:max-line-length")
@@ -23,12 +23,12 @@ object ExtensionTable : IntIdTable() {
val name = varchar("name", 128)
val pkgName = varchar("pkg_name", 128)
val apkUrl = varchar("apk_url", 2048)
val apkUrl = varchar("apk_url", 2048).nullable()
val extensionLib = varchar("extension_lib", 16).nullable()
val versionName = varchar("version_name", 16)
val versionCode = long("version_code")
val lang = varchar("lang", 32)
val contentRating = integer("content_rating")
val contentWarning = integer("content_warning")
val isInstalled = bool("is_installed").default(false)
val hasUpdate = bool("has_update").default(false)

View File

@@ -8,13 +8,11 @@ package suwayomi.tachidesk.manga.model.table
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import org.jetbrains.exposed.v1.core.dao.id.IdTable
import suwayomi.tachidesk.manga.model.table.columns.unlimitedVarchar
object SourceTable : IdTable<Long>() {
override val id = long("id").entityId()
val name = varchar("name", 128)
val lang = varchar("lang", 32)
val extension = reference("extension", ExtensionTable)
val message = unlimitedVarchar("message").nullable()
val contentRating = integer("content_rating").default(0)
val contentWarning = integer("content_warning").default(0)
}

View File

@@ -644,6 +644,16 @@ object OpdsFeedBuilder {
skipMetadata,
)
// Return a not-found feed if all available chapters are filtered out as unreachable
if (skipMetadata && chapterEntries.isEmpty() && totalChapters > 0L) {
return buildNotFoundFeed(
baseUrl = baseUrl,
locale = locale,
idPath = "series/$mangaId/chapters",
title = MR.strings.opds_error_chapters_not_found.localized(locale, pageNum),
)
}
// If no chapters are found in the database, attempt to fetch them from the source.
if (chapterEntries.isEmpty() && totalChapters == 0L) {
try {

View File

@@ -169,6 +169,8 @@ object ChapterRepository {
}
}
}.awaitAll()
// Exclude unreachable chapters that are not downloaded and have no page count
.filter { it.downloaded || it.pageCount > 0 }
return Pair(enrichedChapters, totalCount)
}

View File

@@ -9,8 +9,8 @@ import org.jetbrains.exposed.v1.core.SortOrder
import org.jetbrains.exposed.v1.core.alias
import org.jetbrains.exposed.v1.core.and
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.greater
import org.jetbrains.exposed.v1.core.inList
import org.jetbrains.exposed.v1.core.inSubQuery
import org.jetbrains.exposed.v1.core.intLiteral
import org.jetbrains.exposed.v1.core.like
import org.jetbrains.exposed.v1.core.lowerCase
@@ -22,7 +22,7 @@ import org.jetbrains.exposed.v1.jdbc.andWhere
import org.jetbrains.exposed.v1.jdbc.select
import org.jetbrains.exposed.v1.jdbc.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
@@ -75,13 +75,28 @@ fun Query.applyOpdsMangaFilter(
}
if (excludeField != "filter") {
criteria.filter?.let { filterVal ->
val unreadCountExpr = Case().When(ChapterTable.isRead eq false, intLiteral(1)).Else(intLiteral(0)).sum()
val downloadedCountExpr = Case().When(ChapterTable.isDownloaded eq true, intLiteral(1)).Else(intLiteral(0)).sum()
when (filterVal) {
"unread" -> having { unreadCountExpr greater 0 }
"downloaded" -> having { downloadedCountExpr greater 0 }
"ongoing" -> andWhere { MangaTable.status eq MangaStatus.ONGOING.value }
"completed" -> andWhere { MangaTable.status eq MangaStatus.COMPLETED.value }
"unread" -> {
andWhere {
MangaTable.id inSubQuery
ChapterTable.select(ChapterTable.manga).where { ChapterTable.isRead eq false }
}
}
"downloaded" -> {
andWhere {
MangaTable.id inSubQuery
ChapterTable.select(ChapterTable.manga).where { ChapterTable.isDownloaded eq true }
}
}
"ongoing" -> {
andWhere { MangaTable.status eq MangaStatus.ONGOING.value }
}
"completed" -> {
andWhere { MangaTable.status eq MangaStatus.COMPLETED.value }
}
}
}
}
@@ -133,11 +148,17 @@ object MangaRepository {
val unreadCount = unreadCountExpr.alias("unread_count")
// Base query with necessary joins for filtering and sorting
val query =
var baseJoin =
MangaTable
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
if (criteria.categoryId != null) {
baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
}
val query =
baseJoin
.select(MangaTable.columns + SourceTable.lang + SourceTable.name + unreadCount)
.where { MangaTable.inLibrary eq true }
@@ -210,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)
@@ -301,7 +322,6 @@ object MangaRepository {
* Applies sorting and filtering logic to a manga library query.
* @param query The Exposed SQL query to modify.
* @param sort The sorting parameter.
* @param filter The filtering parameter.
*/
private fun applyMangaLibrarySort(
query: Query,
@@ -330,36 +350,38 @@ object MangaRepository {
*/
fun getLibraryFilterCounts(activeFilters: OpdsMangaFilter): Map<String, Long> =
transaction {
val unreadCountExpr = Case().When(ChapterTable.isRead eq false, intLiteral(1)).Else(intLiteral(0)).sum()
val downloadedCountExpr = Case().When(ChapterTable.isDownloaded eq true, intLiteral(1)).Else(intLiteral(0)).sum()
var baseJoin =
MangaTable
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
if (activeFilters.categoryId != null) {
baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
}
val baseQuery =
MangaTable
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
baseJoin
.select(MangaTable.id)
.where { MangaTable.inLibrary eq true }
.withDistinct()
baseQuery.applyOpdsMangaFilter(activeFilters, excludeField = "filter")
baseQuery.groupBy(MangaTable.id)
val unreadCount = baseQuery.copy().having { unreadCountExpr greater 0 }.count()
val downloadedCount = baseQuery.copy().having { downloadedCountExpr greater 0 }.count()
val statusBaseQuery =
MangaTable
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
.select(MangaTable.id)
.where { MangaTable.inLibrary eq true }
statusBaseQuery.applyOpdsMangaFilter(activeFilters, excludeField = "filter")
statusBaseQuery.groupBy(MangaTable.id)
val ongoingCount = statusBaseQuery.copy().andWhere { MangaTable.status eq MangaStatus.ONGOING.value }.count()
val completedCount = statusBaseQuery.copy().andWhere { MangaTable.status eq MangaStatus.COMPLETED.value }.count()
val unreadCount =
baseQuery
.copy()
.andWhere {
MangaTable.id inSubQuery
ChapterTable.select(ChapterTable.manga).where { ChapterTable.isRead eq false }
}.count()
val downloadedCount =
baseQuery
.copy()
.andWhere {
MangaTable.id inSubQuery
ChapterTable.select(ChapterTable.manga).where { ChapterTable.isDownloaded eq true }
}.count()
val ongoingCount = baseQuery.copy().andWhere { MangaTable.status eq MangaStatus.ONGOING.value }.count()
val completedCount = baseQuery.copy().andWhere { MangaTable.status eq MangaStatus.COMPLETED.value }.count()
mapOf(
"unread" to unreadCount,

View File

@@ -12,7 +12,6 @@ import suwayomi.tachidesk.i18n.MR
import suwayomi.tachidesk.manga.impl.extension.Extension
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
import suwayomi.tachidesk.manga.model.table.CategoryTable
import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.manga.model.table.ExtensionTable
import suwayomi.tachidesk.manga.model.table.MangaStatus
import suwayomi.tachidesk.manga.model.table.MangaTable
@@ -166,12 +165,17 @@ object NavigationRepository {
transaction {
val mangaCount = MangaTable.id.countDistinct().alias("manga_count")
val query =
var baseJoin =
SourceTable
.join(MangaTable, JoinType.INNER, SourceTable.id, MangaTable.sourceReference)
.join(ExtensionTable, JoinType.LEFT, onColumn = SourceTable.extension, otherColumn = ExtensionTable.id)
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
if (activeFilters.categoryId != null) {
baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
}
val query =
baseJoin
.select(SourceTable.id, SourceTable.name, SourceTable.lang, ExtensionTable.pkgName, mangaCount)
.where { MangaTable.inLibrary eq true }
@@ -227,7 +231,6 @@ object NavigationRepository {
.join(CategoryMangaTable, JoinType.INNER, CategoryTable.id, CategoryMangaTable.category)
.join(MangaTable, JoinType.INNER, CategoryMangaTable.manga, MangaTable.id)
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
.select(CategoryTable.id, CategoryTable.name, mangaCount)
.where { MangaTable.inLibrary eq true }
@@ -262,11 +265,15 @@ object NavigationRepository {
activeFilters: OpdsMangaFilter = OpdsMangaFilter(),
): Pair<List<OpdsGenreNavEntry>, Long> =
transaction {
val query =
var baseJoin =
MangaTable
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
if (activeFilters.categoryId != null) {
baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
}
val query =
baseJoin
.select(MangaTable.genre)
.where { MangaTable.inLibrary eq true }
@@ -321,11 +328,16 @@ object NavigationRepository {
val statusCounts =
transaction {
val countExpr = MangaTable.id.countDistinct().alias("manga_count")
val query =
var baseJoin =
MangaTable
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
if (activeFilters.categoryId != null) {
baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
}
val query =
baseJoin
.select(MangaTable.status, countExpr)
.where { MangaTable.inLibrary eq true }
@@ -368,11 +380,16 @@ object NavigationRepository {
): Pair<List<OpdsLanguageNavEntry>, Long> =
transaction {
val mangaCount = MangaTable.id.countDistinct().alias("manga_count")
val query =
var baseJoin =
SourceTable
.join(MangaTable, JoinType.INNER, SourceTable.id, MangaTable.sourceReference)
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
if (activeFilters.categoryId != null) {
baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
}
val query =
baseJoin
.select(SourceTable.lang, mangaCount)
.where { MangaTable.inLibrary eq true }

View File

@@ -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()}" }

View File

@@ -9,7 +9,7 @@ package suwayomi.tachidesk.server.database.migration
import de.neonew.exposed.migrations.helpers.SQLMigration
import suwayomi.tachidesk.graphql.types.DatabaseType
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
import suwayomi.tachidesk.server.database.migration.helpers.MAYBE_TYPE_PREFIX
import suwayomi.tachidesk.server.database.migration.helpers.UNLIMITED_TEXT
import suwayomi.tachidesk.server.database.migration.helpers.toSqlName
@@ -34,6 +34,7 @@ class M0057_AddNewExtensionApiFields : SQLMigration() {
}
}
ALTER TABLE EXTENSION ALTER COLUMN store_index_url ${MAYBE_TYPE_PREFIX}VARCHAR(2048);
CREATE INDEX extension_store_index_url ON EXTENSION (store_index_url);
ALTER TABLE EXTENSION ALTER COLUMN version_code ${MAYBE_TYPE_PREFIX}BIGINT;
ALTER TABLE EXTENSION ALTER COLUMN apk_name DROP NOT NULL;
${
@@ -43,12 +44,11 @@ class M0057_AddNewExtensionApiFields : SQLMigration() {
}
}
ALTER TABLE EXTENSION ADD COLUMN apk_url VARCHAR(2048);
ALTER TABLE EXTENSION ADD COLUMN content_rating INTEGER DEFAULT 0;
UPDATE EXTENSION SET content_rating = ${ContentRating.PORNOGRAPHIC.ordinal} WHERE is_nsfw = TRUE;
ALTER TABLE EXTENSION ADD COLUMN content_warning INTEGER DEFAULT 0;
UPDATE EXTENSION SET content_warning = ${ContentWarning.MIXED.ordinal} WHERE is_nsfw = TRUE;
ALTER TABLE EXTENSION DROP COLUMN is_nsfw;
ALTER TABLE SOURCE ADD COLUMN message $UNLIMITED_TEXT;
ALTER TABLE SOURCE ADD COLUMN content_rating INTEGER DEFAULT 0;
UPDATE SOURCE SET content_rating = ${ContentRating.PORNOGRAPHIC.ordinal} WHERE is_nsfw = TRUE;
ALTER TABLE SOURCE ADD COLUMN content_warning INTEGER DEFAULT 0;
UPDATE SOURCE SET content_warning = ${ContentWarning.MIXED.ordinal} WHERE is_nsfw = TRUE;
ALTER TABLE SOURCE DROP COLUMN is_nsfw;

View File

@@ -14,13 +14,14 @@ import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
@Suppress("ClassName", "unused")
class M0058_AddExtensionStore : AddTableMigration() {
private class ExtensionStoreTable : IntIdTable() {
val indexUrl = varchar("index_url", 2048)
val indexUrl = varchar("index_url", 2048).uniqueIndex()
val name = varchar("name", 256)
val badgeLabel = varchar("badge_label", 32)
val signingKey = varchar("signing_key", 512)
val contactWebsite = varchar("contact_website", 2048)
val contactDiscord = varchar("contact_discord", 2048).nullable()
val isLegacy = bool("is_legacy").default(false)
val extensionListUrl = varchar("extension_list_url", 2048).nullable()
}
override val tables: Array<Table>

View File

@@ -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)
}

View File

@@ -28,7 +28,7 @@ import suwayomi.tachidesk.manga.impl.extension.Extension.installExtension
import suwayomi.tachidesk.manga.impl.extension.Extension.uninstallExtension
import suwayomi.tachidesk.manga.impl.extension.Extension.updateExtension
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
@@ -82,7 +82,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}" })

View File

@@ -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) }
}
}
}