Compare commits

..

75 Commits

Author SHA1 Message Date
Aria Moradi
35470b8605 multi-user experiment 2022-12-28 17:26:02 +03:30
Aria Moradi
874aaf4e93 fix when playwright fails on providing a UA 2022-12-28 17:14:12 +03:30
Mitchell Syer
ebf076d9f6 Use extension list fallback if extensions fail to fetch (#469) 2022-12-25 11:15:37 +03:30
Mitchell Syer
073a041d4c Add better manga thumbnail handling (#465) 2022-12-23 00:43:36 +03:30
Mahor
96a9b4dabd Fix debian release (#463)
* Update debhelper-compat to 13

* Re-enable debian release

* Re-enable debian release
2022-12-16 00:21:42 +03:30
Aria Moradi
8e4cdf2386 disable deb release 2022-12-11 20:05:42 +03:30
Mitchell Syer
ab4d925a5a Get Playwright working (#462)
* Get Playwright working with ShadowJar

* Set system driver implementation

* Minor cleanup

* Fix run gradle task and re-add some removed code

* No need to get the FS if it already exists

* use java implementation

Co-authored-by: Aria Moradi <aria.moradi007@gmail.com>
2022-12-09 21:47:26 +03:30
Zero
d9c6f52e21 Basic android.graphics Rect and Canvas implementation (#461)
Some extensions use more Canvas methods, but they don't
really seem to get that far yet, all the others I was
able to test seem to work now.
2022-12-06 07:48:40 +03:30
Zero
0a748cd53b implementation of android.graphics.BitmapFactory (#460)
Only what was needed is implemented, compression method is still untested.
2022-12-05 19:21:16 +03:30
Aria Moradi
07314ef018 get default User Agent from WebView (#457)
* get default User Agent from WebView

* make sure to close browser

Co-authored-by: Mitchell Syer <Mitchellptbo@gmail.com>

Co-authored-by: Mitchell Syer <Mitchellptbo@gmail.com>
2022-12-04 21:21:19 +03:30
Aria Moradi
5eaebf678f fix regex 2022-12-04 13:03:23 +03:30
Aria Moradi
80fbfa60de better description 2022-12-04 12:59:33 +03:30
Aria Moradi
fbbcc9e9b6 update issue mod 2022-12-04 12:50:03 +03:30
Aria Moradi
f47dc6b9de WebView based cloudflare interceptor (#456)
* WebView based cloudflare interceptor

ported https://github.com/vvanglro/cf-clearance to kotlin

* code clean up

* Forgot to commit these

* Get ResolveWithWebView working
1. Make sure to .use all closeable resources
2. Use 10 seconds instead of 1 second for waiting for cloudflare(this was the most probable issue)
3. Use Extension UA when possible
4. Minor cleanup of logging

* rewrite and refactor

Co-authored-by: Syer10 <syer10@users.noreply.github.com>
2022-12-04 12:08:54 +03:30
Aria Moradi
5f8e74f017 fix Changelog typos 2022-11-26 20:45:51 +03:30
Aria Moradi
8c1ca0ac7e add Chagelog TL;DR 2022-11-26 20:37:09 +03:30
Aria Moradi
9018de3c4c v0.6.6 2022-11-26 20:29:51 +03:30
Valter Martinek
e7cb88c757 Download queue missing update fix (#450)
* Add immediate updates to download queue manager for updates that always needs to be delivered

* Update server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/DownloadManager.kt

Co-authored-by: Mitchell Syer <Mitchellptbo@gmail.com>

* Revert change to make sure that data in status sent to client are actual

* Reduce number of immediate updates to clients

Co-authored-by: Mitchell Syer <Mitchellptbo@gmail.com>
2022-11-16 20:03:51 +03:30
Valter Martinek
d6127d6811 Add batch endpoint for removing downloads from download queue (#452) 2022-11-16 20:01:48 +03:30
Aria Moradi
67e09e2e1d make chapters endpoint more unifrom 2022-11-15 15:46:02 +03:30
Valter Martinek
8fbc24c751 Batch editing and deleting any chapter (#449)
* Add new endpoint for batch editing any chapter

* Add option to batch editing chapters to delete chapter (remove downloaded content)

* Rename the endpoint to match single manga batch endpoint

* Do not return early, in case there are other changes

* PR changes
2022-11-15 14:19:20 +03:30
Valter Martinek
c0948209be Fix docs for /server/check-updates (#447) 2022-11-11 16:21:29 +03:30
Valter Martinek
7237161d52 Fix settings/check-update endpoint (#445) 2022-11-10 21:32:27 +03:30
Aria Moradi
94c2e21e2b Future proofing 2022-11-10 04:36:56 +03:30
Aria Moradi
65067e6e01 changes needed for tachiyomi tracker 2022-11-10 02:13:20 +03:30
Valter Martinek
39490ce7ba Add batch chapter update endpoint (#442) 2022-11-09 20:43:29 +03:30
Mitchell Syer
2f3f47c745 Set source preference doc fix (#441) 2022-11-08 10:32:45 +03:30
Mitchell Syer
2195c3df76 Downloader Rewrite (#437)
* Downloader rewrite
- Rewrite downloader to use coroutines instead of a thread
- Remove unused Page functions
- Add page progress
- Add ProgressResponseBody
- Add support for canceling a download in the middle of downloading
- Fix clear download queue

* Minor fix

* Minor improvements
- notifyAllClients now launches in another thread and only sends new data every second
- Better handling of download queue checker in step()
- Minor improvements and fixes

* Reorder downloads

* Download in parallel by source

* Remove TODO
2022-11-08 04:39:26 +03:30
Aria Moradi
119b9db6b4 refactor deprecated api 2022-11-07 22:50:20 +03:30
Aria Moradi
fcbc598732 Revert H2 database to v1 2022-11-07 22:50:20 +03:30
Aria Moradi
e850049e8e add category and global meta (#438) 2022-11-07 21:04:34 +03:30
Aria Moradi
907adea73f Migrate to H2 v2 2022-11-07 14:10:33 +03:30
Valter Martinek
2ac5c1362c add batch download api (#436)
* Add POST /downloads endpoint for creating multiple

* Fix review notes

* Add chapter id to API endpoints

* Rewrite batch chapter download to use chapter id instead of mangaId+chapterIndex combination

* Change EnqueueInput format to be more futureproof

* Change endpoint path

* Change endpoint path
2022-11-07 01:02:18 +03:30
Mitchell Syer
8b20e2b48f Add request body to documentation (#435) 2022-11-06 23:19:11 +03:30
Valter Martinek
c2a9820fc1 POST variant for /{sourceId}/search endpoint (#434)
* Add POST variant for `/{sourceId}/search` endpoint which handles body data as list of FilterChanges

* Revert changes to existing endpoint and create new route and change the interface

* Update doc

* Rename api endpoint
2022-11-05 20:48:20 +03:30
Valter Martinek
a9e5bc0c95 Pre-load meta entries for all chapters for optimization (#432)
Load meta entries for all chapters in one query to prevent N+1 queries
2022-10-30 20:18:27 +03:30
Valter Martinek
0fa2834d25 add MangaTable.lastFetchedAt and ChapterTable.chaptersLastFetchedAt (#431)
* Add lastFetchedAt and chaptersLastFetchedAt columns to manga

* Update lastFetchedAt columns when data are fetched from source

* Add age and chaptersAge fields to MangaDataClass

* Replace two migrations with single migration
2022-10-30 20:16:23 +03:30
Valter Martinek
23f0876c00 Add cache control header to manga page response (#430) 2022-10-29 22:19:19 +03:30
Anurag
6d88d90659 Fix: Error handling for popular/latest api if pageNum was supplied as zero (#424)
* fix: handle and throw proper error if pageNum is zero for popular/latest api, fixes #75

* chore: replace if-else with kotlin require which throws IllegalArgumentException and add comment

* fix: remove comment as exception message is enough
2022-10-28 14:34:22 +03:30
Mitchell Syer
a3c366c360 Lint (#423) 2022-10-22 15:38:14 +03:30
Mitchell Syer
3bef07eeab Update dependencies (#422)
* Update dependencies and lint files

* Revert lint
2022-10-22 03:33:07 +03:30
Aria Moradi
d029e65b8e include list of mangas missing source in restore report (#421) 2022-10-20 00:20:39 +03:30
Aria Moradi
f305ac6905 remove BuildConfig as extensions now use AppInfo 2022-10-19 23:08:08 +03:30
Aria Moradi
4d4a46d2a5 move Tachiyomi's BuildConfig to kotlin dir 2022-10-19 22:44:00 +03:30
Aria Moradi
8218f2f830 ktlint 2022-10-19 16:22:07 +03:30
like
b1bf901eac replace quickjs with Mozilla Rhino (#415)
* replace quickjs with jdk 8 default js engine

* replace quickjs with rhino engine and translate type for read comic online extension

* move quick js to AndroidCompat

* fix commicabc long type cast exception
2022-10-12 14:03:49 +03:30
Mitchell Syer
06eff55210 Updater cleanup and improvements (#416) 2022-10-11 19:57:15 +03:30
Mitchell Syer
71730fddad Documentation cleanup (#417) 2022-10-11 12:54:45 +03:30
Mitchell Syer
f2d1c6e3cb Fix downloader memory leak (#418) 2022-10-11 12:52:10 +03:30
Marco Ebbinghaus
7ae837ca3c Remove support for Sorayomi web interface (#414)
fixes #392
2022-10-07 22:26:26 +03:30
Vedant
b10908df5e Update winget.yml (#410) 2022-10-02 15:07:16 +03:30
Mahor
4dd4d38d5b Revert back to correct way of handling jre_dir (#408) 2022-09-28 22:44:14 +03:30
Mahor
447c286b56 Add libc++-dev (#405)
Use java8-runtime-headless virtual package which is a superset of default-jre-headless
2022-09-25 18:19:37 +03:30
Aria Moradi
c71898ece9 Update Changelog 2022-09-18 09:20:21 +04:30
Aria Moradi
9473e88ea9 bump version 2022-09-18 09:14:44 +04:30
Mahor
d7663ed56e Fix deb package (#397) 2022-08-29 21:59:23 +04:30
voltrare
da7569e2f5 fix jre path(#396)
use `mv -T` @mahor1221
2022-08-27 16:01:05 +04:30
Vedant
d989940a4d Update winget.yml (#393) 2022-08-21 15:29:18 +04:30
Aria Moradi
bd6a86b135 fix more broken stuff 2022-08-19 00:26:03 +04:30
Aria Moradi
b38eb11503 fix more broken stuff 2022-08-19 00:25:37 +04:30
Aria Moradi
7aef32c13d fix more broken stuff 2022-08-19 00:24:40 +04:30
Aria Moradi
fab64b147c fix broken links 2022-08-19 00:21:23 +04:30
Aria Moradi
cc5a63205c v0.6.4 2022-08-19 00:16:55 +04:30
Aria Moradi
814166a884 Fix mistakes from #384 (#385) 2022-08-10 19:14:16 +04:30
Aria Moradi
55240d9e9b Rename every instance of Tachidesk jar to Tachdidesk-Server.jar (#384) 2022-08-10 18:40:16 +04:30
Mahor
9dc598150f Replace linux-all with linux-assets (#381)
* Move linux package specific files to scripts/resources/pkg

* Replace linux-all with linux-assets

* Fix linux-x64 launchers issue

* Remove -e
2022-08-10 18:20:07 +04:30
Mahor
21c087e273 Tidy up bundler script (#380)
* Tidy up bundler script

* Update paths

* Remove -e

* Revert "Remove -e"

This reverts commit 1e29293dd0.
2022-08-10 18:10:45 +04:30
Mitchell Syer
bdf3a7014f Improve DocumentationDsl, bugfix default values and add queryParams (#378)
* Improve DocumentationDsl, bugfix default values and add queryParams

Adding `queryParams<Int>("mangaId[]")` would allow something like this: `http://127.0.0.1:4567/api/v1/download/manga?mangaId[]=1&mangaId[]=2`

* Remove extra comma

* Make QueryParams not nullable and use default value if empty

* Allow nullable again
2022-07-30 17:59:27 +04:30
Mahor
dfea6e9b1b Update gradle action (#372)
* Update gradle action

* Update actions in build_pull_request.yml
2022-07-04 23:23:31 -04:00
Mahor
50eef1190e Run workflow jobs toghether (#371)
* Run scripts in parallel

* Re enable deb package builds
2022-07-04 10:06:30 -04:00
Mahor
ed180121ff Refactor scripts (#370)
* Rename debian to deb

* Merge scripts into one

* Add error handler

* Disable wine installation to change electron icon due to error

* Replace debuild with dpkg-buildpackage

* Update workflows with new script

* Fix path
2022-07-02 15:42:08 -04:00
Vedant
bdb0ad89d4 Publish to Windows Package Managar (WinGet) (#369)
* Update publish.yml

* Create winget.yml

* Update winget.yml

* Update publish.yml

* Update winget.yml
2022-06-30 08:29:23 +04:30
Mahor
7195a30d55 Add linux-all.tar.gz & systemd service (#366)
* Update my email address

* Add systemd configs to debian package

* Add systemd configs

* Tidy up

* Add linux-all.tar.gz

* Rename Tachidesk.jar to tachidesk-server.jar

* Fix typo

* Fix typo
2022-06-16 17:58:31 +04:30
Mitchell Syer
5b0426a94c Docs improvements (#359)
* Use Array since Javalin OpenAPI requires it to read the list generics

* Use custom Pager class for documentation
2022-05-21 14:42:10 +04:30
Mitchell Syer
a6d012abd9 Fix documentation errors (#358) 2022-05-20 15:54:06 +04:30
166 changed files with 3701 additions and 998 deletions

View File

@@ -10,7 +10,7 @@ jobs:
steps: steps:
- name: Clone repo - name: Clone repo
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Validate Gradle Wrapper - name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1 uses: gradle/wrapper-validation-action@v1
@@ -23,7 +23,7 @@ jobs:
steps: steps:
- name: Cancel previous runs - name: Cancel previous runs
uses: styfle/cancel-workflow-action@0.9.0 uses: styfle/cancel-workflow-action@0.10.0
with: with:
access_token: ${{ github.token }} access_token: ${{ github.token }}
@@ -45,13 +45,9 @@ jobs:
mkdir -p ~/.gradle mkdir -p ~/.gradle
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Build Jar - name: Build Jar
uses: eskatos/gradle-command-action@v1 uses: gradle/gradle-build-action@v2
with: with:
build-root-directory: master build-root-directory: master
wrapper-directory: master
arguments: :server:shadowJar --stacktrace arguments: :server:shadowJar --stacktrace
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true

View File

@@ -9,28 +9,26 @@ jobs:
check_wrapper: check_wrapper:
name: Validate Gradle Wrapper name: Validate Gradle Wrapper
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Clone repo - name: Clone repo
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Validate Gradle Wrapper - name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1 uses: gradle/wrapper-validation-action@v1
build: build:
name: Build artifacts and deploy preview name: Build Jar
needs: check_wrapper needs: check_wrapper
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')" if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Cancel previous runs - name: Cancel previous runs
uses: styfle/cancel-workflow-action@0.9.0 uses: styfle/cancel-workflow-action@0.10.0
with: with:
access_token: ${{ github.token }} access_token: ${{ github.token }}
- name: Checkout master branch - name: Checkout master branch
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
ref: master ref: master
path: master path: master
@@ -48,43 +46,136 @@ jobs:
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Build Jar - name: Build Jar
uses: eskatos/gradle-command-action@v1 uses: gradle/gradle-build-action@v2
env: env:
ProductBuildType: "Preview" ProductBuildType: "Preview"
with: with:
build-root-directory: master build-root-directory: master
wrapper-directory: master
arguments: :server:shadowJar --stacktrace arguments: :server:shadowJar --stacktrace
wrapper-cache-enabled: true
dependencies-cache-enabled: true - name: Upload Jar
configuration-cache-enabled: true uses: actions/upload-artifact@v3
with:
name: jar
path: master/server/build/*.jar
if-no-files-found: error
- name: Upload icons
uses: actions/upload-artifact@v3
with:
name: icon
path: master/server/src/main/resources/icon
if-no-files-found: error
- name: Tar scripts dir to maintain file permissions
run: tar -cvzf scripts.tar.gz -C master/ scripts/
- name: Upload scripts.tar.gz
uses: actions/upload-artifact@v3
with:
name: scripts
path: scripts.tar.gz
if-no-files-found: error
bundle:
strategy:
fail-fast: false
matrix:
os:
- debian-all
- linux-assets
- linux-x64
- macOS-x64
- macOS-arm64
- windows-x64
- windows-x86
name: Make ${{ matrix.os }} release
needs: build
runs-on: ubuntu-latest
steps:
- name: Download Jar
uses: actions/download-artifact@v3
with:
name: jar
path: server/build
- name: Download icons
uses: actions/download-artifact@v3
with:
name: icon
path: server/src/main/resources/icon
- name: Download scripts.tar.gz
uses: actions/download-artifact@v3
with:
name: scripts
- name: Make ${{ matrix.os }} release
run: |
mkdir upload
tar -xvpf scripts.tar.gz
scripts/bundler.sh -o upload/ ${{ matrix.os }}
- name: Upload ${{ matrix.os }} release
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.os }}
path: upload/*
if-no-files-found: error
release:
needs: bundle
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v3
with:
name: jar
path: release
- uses: actions/download-artifact@v3
with:
name: debian-all
path: release
- uses: actions/download-artifact@v3
with:
name: linux-assets
path: release
- uses: actions/download-artifact@v3
with:
name: linux-x64
path: release
- uses: actions/download-artifact@v3
with:
name: macOS-x64
path: release
- uses: actions/download-artifact@v3
with:
name: macOS-arm64
path: release
- uses: actions/download-artifact@v3
with:
name: windows-x64
path: release
- uses: actions/download-artifact@v3
with:
name: windows-x86
path: release
- name: Checkout Preview branch
uses: actions/checkout@v3
with:
repository: "Suwayomi/Tachidesk-Server-preview"
ref: main
path: preview
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
- name: Generate Tag Name - name: Generate Tag Name
id: GenTagName id: GenTagName
run: | run: |
cd master/server/build cd release
genTag=$(ls *.jar | sed -e's/Tachidesk-Server-\|.jar//g') genTag=$(ls *.jar | sed -e's/Tachidesk-Server-\|.jar//g')
echo "$genTag" echo "$genTag"
echo "::set-output name=value::$genTag" echo "::set-output name=value::$genTag"
- name: make bundle packages
run: |
cd master/scripts
./windows-bundler.sh win32
./windows-bundler.sh win64
./unix-bundler.sh linux-x64
./debian-packager.sh
./unix-bundler.sh macOS-x64
./unix-bundler.sh macOS-arm64
- name: Checkout preview branch
uses: actions/checkout@v2
with:
repository: 'Suwayomi/Tachidesk-Server-preview'
ref: main
path: preview
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
- name: Create Tag - name: Create Tag
run: | run: |
TAG="${{ steps.GenTagName.outputs.value }}" TAG="${{ steps.GenTagName.outputs.value }}"
@@ -92,7 +183,8 @@ jobs:
cd preview cd preview
echo "{ \"latest\": \"$TAG\" }" > index.json echo "{ \"latest\": \"$TAG\" }" > index.json
git add index.json git add index.json
git config --global user.email "github-actions[bot]@users.noreply.github.com" git config --global user.email \
"github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]" git config --global user.name "github-actions[bot]"
git commit -m "Updated to $TAG" git commit -m "Updated to $TAG"
git push origin main git push origin main
@@ -101,10 +193,10 @@ jobs:
git push origin $TAG git push origin $TAG
- name: Upload Preview Release - name: Upload Preview Release
uses: ncipollo/release-action@v1 uses: softprops/action-gh-release@v1
with: with:
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }} token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
artifacts: "master/server/build/*.jar,master/server/build/*.msi,master/server/build/*.zip,master/server/build/*.tar.gz,master/server/build/*.deb" repository: "Suwayomi/Tachidesk-Server-preview"
owner: "Suwayomi" tag_name: ${{ steps.GenTagName.outputs.value }}
repo: "Tachidesk-Server-preview" files: release/*
tag: ${{ steps.GenTagName.outputs.value }}

View File

@@ -1,24 +1,35 @@
name: Issue closer name: Issue moderator
on: on:
issues: issues:
types: [opened, edited, reopened] types: [opened, edited, reopened]
issue_comment:
types: [created]
jobs: jobs:
autoclose: autoclose:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Autoclose issues - name: Moderate issues
uses: arkon/issue-closer-action@v3.0 uses: tachiyomiorg/issue-moderator-action@v1
with: with:
repo-token: ${{ github.token }} repo-token: ${{ github.token }}
rules: | duplicate-check-enabled: true
duplicate-check-label: Source request
existing-check-enabled: true
existing-check-label: Source request
auto-close-rules: |
[ [
{ {
"type": "title", "type": "title",
"regex": ".*<short description>*", "regex": ".*<short description>.*",
"message": "You did not fill out the description in the title" "message": "You did not fill out the description in the title"
}, },
{
"type": "title",
"regex": ".*(<|>)+.*",
"message": "You did not remove Angle brackets(< and >) from the title"
},
{ {
"type": "body", "type": "body",
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*", "regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
@@ -26,7 +37,7 @@ jobs:
}, },
{ {
"type": "body", "type": "body",
"regex": "(Tachidesk version|Server Operating System|Server Desktop Environment|Server JVM version|Client Operating System|Client Web Browser):.*(\\(Example:|<usually).*", "regex": ".*(Tachidesk version|Server Operating System|Server Desktop Environment|Server JVM version|Client Operating System|Client Web Browser):.*(\\(Example:|<usually).*",
"message": "The requested information was not filled out" "message": "The requested information was not filled out"
}, },
{ {

View File

@@ -1,6 +1,7 @@
name: CI Publish name: CI Publish
on: on:
workflow_dispatch:
push: push:
tags: tags:
- "v*" - "v*"
@@ -9,27 +10,25 @@ jobs:
check_wrapper: check_wrapper:
name: Validate Gradle Wrapper name: Validate Gradle Wrapper
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Clone repo - name: Clone repo
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Validate Gradle Wrapper - name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1 uses: gradle/wrapper-validation-action@v1
build: build:
name: Build artifacts and release name: Build Jar
needs: check_wrapper needs: check_wrapper
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Cancel previous runs - name: Cancel previous runs
uses: styfle/cancel-workflow-action@0.9.0 uses: styfle/cancel-workflow-action@0.10.0
with: with:
access_token: ${{ github.token }} access_token: ${{ github.token }}
- name: Checkout ${{ github.ref }} - name: Checkout ${{ github.ref }}
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
ref: ${{ github.ref }} ref: ${{ github.ref }}
path: master path: master
@@ -44,36 +43,131 @@ jobs:
run: | run: |
cd master cd master
mkdir -p ~/.gradle mkdir -p ~/.gradle
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties cp .github/runner-files/ci-gradle.properties \
~/.gradle/gradle.properties
- name: Build and copy webUI, Build Jar - name: Build and copy webUI, Build Jar
uses: eskatos/gradle-command-action@v1 uses: gradle/gradle-build-action@v2
env: env:
ProductBuildType: "Stable" ProductBuildType: "Stable"
with: with:
build-root-directory: master build-root-directory: master
wrapper-directory: master
arguments: :server:downloadWebUI :server:shadowJar --stacktrace arguments: :server:downloadWebUI :server:shadowJar --stacktrace
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
- name: Make bundle packages - name: Upload Jar
run: | uses: actions/upload-artifact@v3
cd master/scripts
./windows-bundler.sh win32
./windows-bundler.sh win64
./unix-bundler.sh linux-x64
./debian-packager.sh
./unix-bundler.sh macOS-x64
./unix-bundler.sh macOS-arm64
- name: Upload Release
uses: xresloader/upload-to-github-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
file: "master/server/build/*.jar;master/server/build/*.msi;master/server/build/*.zip;master/server/build/*.tar.gz;master/server/build/*.deb" name: jar
tags: true path: master/server/build/*.jar
if-no-files-found: error
- name: Upload icons
uses: actions/upload-artifact@v3
with:
name: icon
path: master/server/src/main/resources/icon
if-no-files-found: error
- name: Tar scripts dir to maintain file permissions
run: tar -cvzf scripts.tar.gz -C master/ scripts/
- name: Upload scripts.tar.gz
uses: actions/upload-artifact@v3
with:
name: scripts
path: scripts.tar.gz
if-no-files-found: error
bundle:
strategy:
fail-fast: false
matrix:
os:
- debian-all
- linux-assets
- linux-x64
- macOS-x64
- macOS-arm64
- windows-x64
- windows-x86
name: Make ${{ matrix.os }} release
needs: build
runs-on: ubuntu-latest
steps:
- name: Download Jar
uses: actions/download-artifact@v3
with:
name: jar
path: server/build
- name: Download icons
uses: actions/download-artifact@v3
with:
name: icon
path: server/src/main/resources/icon
- name: Download scripts.tar.gz
uses: actions/download-artifact@v3
with:
name: scripts
- name: Make ${{ matrix.os }} release
run: |
mkdir upload/
tar -xvpf scripts.tar.gz
scripts/bundler.sh -o upload/ ${{ matrix.os }}
- name: Upload ${{ matrix.os }} files
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.os }}
path: upload/*
if-no-files-found: error
release:
if: startsWith(github.ref, 'refs/tags/v')
needs: bundle
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v3
with:
name: jar
path: release
- uses: actions/download-artifact@v3
with:
name: debian-all
path: release
- uses: actions/download-artifact@v3
with:
name: linux-assets
path: release
- uses: actions/download-artifact@v3
with:
name: linux-x64
path: release
- uses: actions/download-artifact@v3
with:
name: macOS-x64
path: release
- uses: actions/download-artifact@v3
with:
name: macOS-arm64
path: release
- uses: actions/download-artifact@v3
with:
name: windows-x64
path: release
- uses: actions/download-artifact@v3
with:
name: windows-x86
path: release
- name: Generate checksums
run: cd release && sha256sum * > Checksums.sha256
- name: Release
uses: softprops/action-gh-release@v1
with:
token: ${{ secrets.WINGET_PUBLISH_PAT }}
draft: true draft: true
verbose: true files: release/*

13
.github/workflows/winget.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
name: Publish to WinGet
on:
release:
types: [released]
jobs:
publish:
runs-on: windows-latest # action can only be run on windows
steps:
- uses: vedantmgoyal2009/winget-releaser@v1
with:
identifier: Suwayomi.Tachidesk-Server
installers-regex: '.*x64.msi$'
token: ${{ secrets.WINGET_PUBLISH_PAT }}

View File

@@ -9,20 +9,29 @@ dependencies {
implementation(project(":AndroidCompat:Config")) implementation(project(":AndroidCompat:Config"))
// APK sig verifier // APK sig verifier
compileOnly("com.android.tools.build:apksig:7.1.0-beta05") compileOnly("com.android.tools.build:apksig:7.2.1")
// AndroidX annotations // AndroidX annotations
compileOnly("androidx.annotation:annotation:1.3.0") compileOnly("androidx.annotation:annotation:1.5.0")
// substitute for duktape-android // substitute for duktape-android
implementation("org.mozilla:rhino-runtime:1.7.14") // slimmer version of 'org.mozilla:rhino' implementation("org.mozilla:rhino-runtime:1.7.14") // slimmer version of 'org.mozilla:rhino'
implementation("org.mozilla:rhino-engine:1.7.14") // provides the same interface as 'javax.script' a.k.a Nashorn implementation("org.mozilla:rhino-engine:1.7.14") // provides the same interface as 'javax.script' a.k.a Nashorn
// Kotlin wrapper around Java Preferences, makes certain things easier // Kotlin wrapper around Java Preferences, makes certain things easier
val multiplatformSettingsVersion = "0.8.1" val multiplatformSettingsVersion = "1.0.0-RC"
implementation("com.russhwolf:multiplatform-settings-jvm:$multiplatformSettingsVersion") implementation("com.russhwolf:multiplatform-settings-jvm:$multiplatformSettingsVersion")
implementation("com.russhwolf:multiplatform-settings-serialization-jvm:$multiplatformSettingsVersion") implementation("com.russhwolf:multiplatform-settings-serialization-jvm:$multiplatformSettingsVersion")
// Android version of SimpleDateFormat // Android version of SimpleDateFormat
implementation("com.ibm.icu:icu4j:70.1") implementation("com.ibm.icu:icu4j:72.1")
// OpenJDK lacks native JPEG encoder and native WEBP decoder
implementation("com.twelvemonkeys.common:common-lang:3.9.4")
implementation("com.twelvemonkeys.common:common-io:3.9.4")
implementation("com.twelvemonkeys.common:common-image:3.9.4")
implementation("com.twelvemonkeys.imageio:imageio-core:3.9.4")
implementation("com.twelvemonkeys.imageio:imageio-metadata:3.9.4")
implementation("com.twelvemonkeys.imageio:imageio-jpeg:3.4.1")
implementation("com.twelvemonkeys.imageio:imageio-webp:3.9.4")
} }

View File

@@ -0,0 +1,129 @@
package android.graphics;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
public final class Bitmap {
private int width;
private int height;
private BufferedImage image;
public Bitmap(BufferedImage image) {
this.image = image;
this.width = image.getWidth();
this.height = image.getHeight();
}
public BufferedImage getImage() {
return image;
}
public int getHeight() {
return height;
}
public int getWidth() {
return width;
}
public enum CompressFormat {
JPEG (0),
PNG (1),
WEBP (2),
WEBP_LOSSY (3),
WEBP_LOSSLESS (4);
CompressFormat(int nativeInt) {
this.nativeInt = nativeInt;
}
final int nativeInt;
}
public enum Config {
ALPHA_8(1),
RGB_565(3),
ARGB_4444(4),
ARGB_8888(5),
RGBA_F16(6),
HARDWARE(7),
RGBA_1010102(8);
final int nativeInt;
private static Config sConfigs[] = {
null, ALPHA_8, null, RGB_565, ARGB_4444, ARGB_8888, RGBA_F16, HARDWARE, RGBA_1010102
};
Config(int ni) {
this.nativeInt = ni;
}
static Config nativeToConfig(int ni) {
return sConfigs[ni];
}
}
public static Bitmap createBitmap(int width, int height, Config config) {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
return new Bitmap(image);
}
public boolean compress(CompressFormat format, int quality, OutputStream stream) {
if (stream == null) {
throw new NullPointerException();
}
if (quality < 0 || quality > 100) {
throw new IllegalArgumentException("quality must be 0..100");
}
float qualityFloat = ((float) quality) / 100;
String formatString = "";
if (format == Bitmap.CompressFormat.PNG) {
formatString = "png";
} else if (format == Bitmap.CompressFormat.JPEG) {
formatString = "jpg";
} else {
throw new IllegalArgumentException("unsupported compression format!");
}
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName(formatString);
if (!writers.hasNext()) {
throw new IllegalStateException("no image writers found for this format!");
}
ImageWriter writer = (ImageWriter) writers.next();
ImageOutputStream ios;
try {
ios = ImageIO.createImageOutputStream(stream);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
writer.setOutput(ios);
ImageWriteParam param = writer.getDefaultWriteParam();
if (formatString == "jpg") {
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionQuality(qualityFloat);
}
try {
writer.write(null, new IIOImage(image, null, null), param);
ios.close();
writer.dispose();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
return true;
}
}

View File

@@ -0,0 +1,51 @@
package android.graphics;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.util.Iterator;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
public class BitmapFactory {
public static Bitmap decodeStream(InputStream inputStream) {
Bitmap bitmap = null;
try {
ImageInputStream imageInputStream = ImageIO.createImageInputStream(inputStream);
Iterator<ImageReader> imageReaders = ImageIO.getImageReaders(imageInputStream);
if (!imageReaders.hasNext()) {
throw new IllegalArgumentException("no reader for image");
}
ImageReader imageReader = imageReaders.next();
imageReader.setInput(imageInputStream);
BufferedImage image = imageReader.read(0, imageReader.getDefaultReadParam());
bitmap = new Bitmap(image);
imageReader.dispose();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
return bitmap;
}
public static Bitmap decodeByteArray(byte[] data, int offset, int length) {
Bitmap bitmap = null;
ByteArrayInputStream byteArrayStream = new ByteArrayInputStream(data);
try {
BufferedImage image = ImageIO.read(byteArrayStream);
bitmap = new Bitmap(image);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
return bitmap;
}
}

View File

@@ -0,0 +1,21 @@
package android.graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
public final class Canvas {
private BufferedImage canvasImage;
private Graphics2D canvas;
public Canvas(Bitmap bitmap) {
canvasImage = bitmap.getImage();
canvas = canvasImage.createGraphics();
}
public void drawBitmap(Bitmap sourceBitmap, Rect src, Rect dst, Paint paint) {
BufferedImage sourceImage = sourceBitmap.getImage();
BufferedImage sourceImageCropped = sourceImage.getSubimage(src.left, src.top, src.getWidth(), src.getHeight());
canvas.drawImage(sourceImageCropped, null, dst.left, dst.top);
}
}

View File

@@ -0,0 +1,122 @@
package android.graphics;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class Rect {
int left;
int top;
int right;
int bottom;
private static final class UnflattenHelper {
private static final Pattern FLATTENED_PATTERN = Pattern.compile(
"(-?\\d+) (-?\\d+) (-?\\d+) (-?\\d+)");
static Matcher getMatcher(String str) {
return FLATTENED_PATTERN.matcher(str);
}
}
public Rect() {
}
public Rect(int left, int top, int right, int bottom) {
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
public Rect(Rect r) {
if (r == null) {
this.left = 0;
this.top = 0;
this.right = 0;
this.bottom = 0;
} else {
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
}
public final int getWidth() {
return right - left;
}
public final int getHeight() {
return bottom - top;
}
public static Rect unflattenFromString(String str) {
if (str.isEmpty()) {
return null;
}
Matcher matcher = UnflattenHelper.getMatcher(str);
if (!matcher.matches()) {
return null;
}
return new Rect(Integer.parseInt(matcher.group(1)),
Integer.parseInt(matcher.group(2)),
Integer.parseInt(matcher.group(3)),
Integer.parseInt(matcher.group(4)));
}
public String toShortString() {
return toShortString(new StringBuilder(32));
}
public String toShortString(StringBuilder sb) {
sb.setLength(0);
sb.append('['); sb.append(left); sb.append(',');
sb.append(top); sb.append("]["); sb.append(right);
sb.append(','); sb.append(bottom); sb.append(']');
return sb.toString();
}
public String flattenToString() {
StringBuilder sb = new StringBuilder(32);
sb.append(left);
sb.append(' ');
sb.append(top);
sb.append(' ');
sb.append(right);
sb.append(' ');
sb.append(bottom);
return sb.toString();
}
public void writeToParcel(Parcel out, int flags) {
out.writeInt(left);
out.writeInt(top);
out.writeInt(right);
out.writeInt(bottom);
}
public static final Parcelable.Creator<Rect> CREATOR = new Parcelable.Creator<Rect>() {
@Override
public Rect createFromParcel(Parcel in) {
Rect r = new Rect();
r.readFromParcel(in);
return r;
}
@Override
public Rect[] newArray(int size) {
return new Rect[size];
}
};
public void readFromParcel(Parcel in) {
left = in.readInt();
top = in.readInt();
right = in.readInt();
bottom = in.readInt();
}
}

View File

@@ -4,7 +4,7 @@ package android.text;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.safety.Whitelist; import org.jsoup.safety.Safelist;
import org.xml.sax.XMLReader; import org.xml.sax.XMLReader;
/** /**
@@ -18,7 +18,7 @@ import org.xml.sax.XMLReader;
public class Html { public class Html {
public static Spanned fromHtml(String source) { public static Spanned fromHtml(String source) {
return new FakeSpanned(Jsoup.clean(source, Whitelist.none())); return new FakeSpanned(Jsoup.clean(source, Safelist.none()));
} }
public static Spanned fromHtml(String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler) { public static Spanned fromHtml(String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler) {

View File

@@ -0,0 +1,69 @@
package app.cash.quickjs;
import org.mozilla.javascript.ConsString;
import org.mozilla.javascript.NativeArray;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.io.Closeable;
public final class QuickJs implements Closeable {
private ScriptEngine engine;
public static QuickJs create() {
return new QuickJs(new ScriptEngineManager());
}
public QuickJs(ScriptEngineManager manager) {
this.engine = manager.getEngineByName("rhino");
}
public Object evaluate(String script, String fileName) {
return this.evaluate(script);
}
public Object evaluate(String script) {
try {
Object value = engine.eval(script);
return translateType(value);
} catch (Exception exception) {
throw new QuickJsException(exception.getMessage(), exception);
}
}
private Object translateType(Object obj) {
if (obj instanceof NativeArray) {
NativeArray array = (NativeArray) obj;
long length = array.getLength();
Object[] objects = new Object[(int) length];
for (int i = 0; i < (int) length; i++) {
objects[i] = translateType(array.get(i));
}
return objects;
}
if (obj instanceof ConsString) {
ConsString consString = (ConsString) obj;
return consString.toString();
}
if (obj instanceof Long) {
Long value = (Long) obj;
return value.intValue();
}
return obj;
}
public byte[] compile(String sourceCode, String fileName) {
return sourceCode.getBytes();
}
public Object execute(byte[] bytecode) {
return this.evaluate(new String(bytecode));
}
@Override
public void close() {
this.engine = null;
}
}

View File

@@ -0,0 +1,7 @@
package app.cash.quickjs;
public final class QuickJsException extends RuntimeException {
public QuickJsException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -60,9 +60,13 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
private fun internalMove(row: Int) { private fun internalMove(row: Int) {
if (cursor < 0) cursor = 0 if (cursor < 0) {
else if (cursor > resultSetLength + 1) cursor = resultSetLength + 1 cursor = 0
else cursor = row } else if (cursor > resultSetLength + 1) {
cursor = resultSetLength + 1
} else {
cursor = row
}
} }
private fun obj(column: Int): Any? { private fun obj(column: Int): Any? {
@@ -293,10 +297,11 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
override fun <T : Any?> unwrap(iface: Class<T>?): T { override fun <T : Any?> unwrap(iface: Class<T>?): T {
if (thisIsWrapperFor(iface)) if (thisIsWrapperFor(iface)) {
return this as T return this as T
else } else {
return parent.unwrap(iface) return parent.unwrap(iface)
}
} }
override fun next(): Boolean { override fun next(): Boolean {
@@ -531,10 +536,15 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
private fun castToLong(obj: Any?): Long { private fun castToLong(obj: Any?): Long {
if (obj == null) return 0 if (obj == null) {
else if (obj is Long) return obj return 0
else if (obj is Number) return obj.toLong() } else if (obj is Long) {
else throw IllegalStateException("Object is not a long!") return obj
} else if (obj is Number) {
return obj.toLong()
} else {
throw IllegalStateException("Object is not a long!")
}
} }
override fun getLong(columnIndex: Int): Long { override fun getLong(columnIndex: Int): Long {

View File

@@ -10,7 +10,7 @@ package xyz.nulldev.androidcompat.io.sharedprefs
import android.content.SharedPreferences import android.content.SharedPreferences
import com.russhwolf.settings.ExperimentalSettingsApi import com.russhwolf.settings.ExperimentalSettingsApi
import com.russhwolf.settings.ExperimentalSettingsImplementation import com.russhwolf.settings.ExperimentalSettingsImplementation
import com.russhwolf.settings.JvmPreferencesSettings import com.russhwolf.settings.PreferencesSettings
import com.russhwolf.settings.serialization.decodeValue import com.russhwolf.settings.serialization.decodeValue
import com.russhwolf.settings.serialization.decodeValueOrNull import com.russhwolf.settings.serialization.decodeValueOrNull
import com.russhwolf.settings.serialization.encodeValue import com.russhwolf.settings.serialization.encodeValue
@@ -24,7 +24,7 @@ import java.util.prefs.Preferences
@OptIn(ExperimentalSettingsImplementation::class, ExperimentalSerializationApi::class, ExperimentalSettingsApi::class) @OptIn(ExperimentalSettingsImplementation::class, ExperimentalSerializationApi::class, ExperimentalSettingsApi::class)
class JavaSharedPreferences(key: String) : SharedPreferences { class JavaSharedPreferences(key: String) : SharedPreferences {
private val javaPreferences = Preferences.userRoot().node("suwayomi/tachidesk/$key") private val javaPreferences = Preferences.userRoot().node("suwayomi/tachidesk/$key")
private val preferences = JvmPreferencesSettings(javaPreferences) private val preferences = PreferencesSettings(javaPreferences)
private val listeners = mutableMapOf<SharedPreferences.OnSharedPreferenceChangeListener, PreferenceChangeListener>() private val listeners = mutableMapOf<SharedPreferences.OnSharedPreferenceChangeListener, PreferenceChangeListener>()
// TODO: 2021-05-29 Need to find a way to get this working with all pref types // TODO: 2021-05-29 Need to find a way to get this working with all pref types
@@ -76,7 +76,7 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
return Editor(preferences) return Editor(preferences)
} }
class Editor(private val preferences: JvmPreferencesSettings) : SharedPreferences.Editor { class Editor(private val preferences: PreferencesSettings) : SharedPreferences.Editor {
val itemsToAdd = mutableMapOf<String, Any>() val itemsToAdd = mutableMapOf<String, Any>()
override fun putString(key: String, value: String?): SharedPreferences.Editor { override fun putString(key: String, value: String?): SharedPreferences.Editor {

View File

@@ -74,10 +74,11 @@ class PackageController {
fun findPackage(packageName: String): InstalledPackage? { fun findPackage(packageName: String): InstalledPackage? {
val file = File(androidFiles.packagesDir, packageName) val file = File(androidFiles.packagesDir, packageName)
return if (file.exists()) return if (file.exists()) {
InstalledPackage(file) InstalledPackage(file)
else } else {
null null
}
} }
fun findJarFromApk(apkFile: File): File? { fun findJarFromApk(apkFile: File): File? {

View File

@@ -1,6 +1,105 @@
# Server: v0.6.3-next + WebUI: r944 # Server: v0.6.6 + WebUI: r963
## TL;DR ## TL;DR
- N/A - Batch actions for chapters
- Improved the downloader
- WebUI changes:
- Support for chapter actions
- a lot of code cleanup
- some bugfixes
## Tachidesk-Server Changelog
- (r1114) fix broken links (by @AriaMoradi)
- (r1115) fix more broken stuff (by @AriaMoradi)
- (r1116) fix more broken stuff (by @AriaMoradi)
- (r1117) fix more broken stuff (by @AriaMoradi)
- (r1118) Update winget.yml ([#393](https://github.com/Suwayomi/Tachidesk-Server/pull/393) by @vedantmgoyal2009)
- (r1119) fix jre path([#396](https://github.com/Suwayomi/Tachidesk-Server/pull/396) by @vedantmgoyal2009)
- (r1120) Fix deb package ([#397](https://github.com/Suwayomi/Tachidesk-Server/pull/397) by @mahor1221)
- (r1121) bump version (by @AriaMoradi)
- (r1122) Update Changelog (by @AriaMoradi)
- (r1123) Add libc++-dev ([#405](https://github.com/Suwayomi/Tachidesk-Server/pull/405) by @mahor1221)
- (r1124) Revert back to correct way of handling jre_dir ([#408](https://github.com/Suwayomi/Tachidesk-Server/pull/408) by @mahor1221)
- (r1125) Update winget.yml ([#410](https://github.com/Suwayomi/Tachidesk-Server/pull/410) by @vedantmgoyal2009)
- (r1126) Remove support for Sorayomi web interface ([#414](https://github.com/Suwayomi/Tachidesk-Server/pull/414) by @marcoebbinghaus)
- (r1127) Fix downloader memory leak ([#418](https://github.com/Suwayomi/Tachidesk-Server/pull/418) by @Syer10)
- (r1128) Documentation cleanup ([#417](https://github.com/Suwayomi/Tachidesk-Server/pull/417) by @Syer10)
- (r1129) Updater cleanup and improvements ([#416](https://github.com/Suwayomi/Tachidesk-Server/pull/416) by @Syer10)
- (r1130) replace quickjs with Mozilla Rhino ([#415](https://github.com/Suwayomi/Tachidesk-Server/pull/415) by @xhzhe)
- (r1131) ktlint (by @AriaMoradi)
- (r1132) move Tachiyomi's BuildConfig to kotlin dir (by @AriaMoradi)
- (r1133) remove BuildConfig as extensions now use AppInfo (by @AriaMoradi)
- (r1134) include list of mangas missing source in restore report ([#421](https://github.com/Suwayomi/Tachidesk-Server/pull/421) by @AriaMoradi)
- (r1135) Update dependencies ([#422](https://github.com/Suwayomi/Tachidesk-Server/pull/422) by @Syer10)
- (r1136) Lint ([#423](https://github.com/Suwayomi/Tachidesk-Server/pull/423) by @Syer10)
- (r1137) Fix: Error handling for popular/latest api if pageNum was supplied as zero ([#424](https://github.com/Suwayomi/Tachidesk-Server/pull/424) by @meta-boy)
- (r1138) Add cache control header to manga page response ([#430](https://github.com/Suwayomi/Tachidesk-Server/pull/430) by @martinek)
- (r1139) add MangaTable.lastFetchedAt and ChapterTable.chaptersLastFetchedAt ([#431](https://github.com/Suwayomi/Tachidesk-Server/pull/431) by @martinek)
- (r1140) Pre-load meta entries for all chapters for optimization ([#432](https://github.com/Suwayomi/Tachidesk-Server/pull/432) by @martinek)
- (r1141) POST variant for `/{sourceId}/search` endpoint ([#434](https://github.com/Suwayomi/Tachidesk-Server/pull/434) by @martinek)
- (r1142) Add request body to documentation ([#435](https://github.com/Suwayomi/Tachidesk-Server/pull/435) by @Syer10)
- (r1143) add batch download api ([#436](https://github.com/Suwayomi/Tachidesk-Server/pull/436) by @martinek)
- (r1144) Migrate to H2 v2 (by @AriaMoradi)
- (r1145) add category and global meta ([#438](https://github.com/Suwayomi/Tachidesk-Server/pull/438) by @AriaMoradi)
- (r1146) Revert H2 database to v1 (by @AriaMoradi)
- (r1147) refactor deprecated api (by @AriaMoradi)
- (r1148) Downloader Rewrite ([#437](https://github.com/Suwayomi/Tachidesk-Server/pull/437) by @Syer10)
- (r1149) Set source preference doc fix ([#441](https://github.com/Suwayomi/Tachidesk-Server/pull/441) by @Syer10)
- (r1150) Add batch chapter update endpoint ([#442](https://github.com/Suwayomi/Tachidesk-Server/pull/442) by @martinek)
- (r1151) changes needed for tachiyomi tracker (by @AriaMoradi)
- (r1152) Future proofing (by @AriaMoradi)
- (r1153) Fix settings/check-update endpoint ([#445](https://github.com/Suwayomi/Tachidesk-Server/pull/445) by @martinek)
- (r1154) Fix docs for /server/check-updates ([#447](https://github.com/Suwayomi/Tachidesk-Server/pull/447) by @martinek)
- (r1155) Batch editing and deleting any chapter ([#449](https://github.com/Suwayomi/Tachidesk-Server/pull/449) by @martinek)
- (r1156) make chapters endpoint more unifrom (by @AriaMoradi)
- (r1157) Add batch endpoint for removing downloads from download queue ([#452](https://github.com/Suwayomi/Tachidesk-Server/pull/452) by @martinek)
- (r1158) Download queue missing update fix ([#450](https://github.com/Suwayomi/Tachidesk-Server/pull/450) by @martinek)
## Tachidesk-WebUI Changelog
- (r947) Feature/swr for library screens ([#186](https://github.com/Suwayomi/Tachidesk-WebUI/pull/186) by @martinek)
- (r948) Feature/swr for simple queries ([#187](https://github.com/Suwayomi/Tachidesk-WebUI/pull/187) by @martinek)
- (r949) Check download queue for changes and reload chapters if any chapter download changes state. ([#189](https://github.com/Suwayomi/Tachidesk-WebUI/pull/189) by @martinek)
- (r950) Update typescript dependency ([#190](https://github.com/Suwayomi/Tachidesk-WebUI/pull/190) by @martinek)
- (r951) update browserlist (by @AriaMoradi)
- (r952) Feature/batch chapter download ([#191](https://github.com/Suwayomi/Tachidesk-WebUI/pull/191) by @martinek)
- (r953) Memoize empty view face so it does not change on rerender ([#193](https://github.com/Suwayomi/Tachidesk-WebUI/pull/193) by @martinek)
- (r954) Feature/batch chapter actions ([#194](https://github.com/Suwayomi/Tachidesk-WebUI/pull/194) by @martinek)
- (r955) Fix navbar back button behavior ([#195](https://github.com/Suwayomi/Tachidesk-WebUI/pull/195) by @martinek)
- (r956) Options panels refactoring ([#196](https://github.com/Suwayomi/Tachidesk-WebUI/pull/196) by @martinek)
- (r957) Refactor and fix sorting in library ([#197](https://github.com/Suwayomi/Tachidesk-WebUI/pull/197) by @martinek)
- (r958) Scroll window to top when PagedPager changes page ([#198](https://github.com/Suwayomi/Tachidesk-WebUI/pull/198) by @martinek)
- (r959) Verticall scroll navigation and fix ([#200](https://github.com/Suwayomi/Tachidesk-WebUI/pull/200) by @martinek)
- (r960) Hide overflowing text in reader title if text can't be wrapped ([#199](https://github.com/Suwayomi/Tachidesk-WebUI/pull/199) by @martinek)
- (r961) Add safezone to scroll end detection to prevent edge cases when scrolling to the end would not detect end ([#201](https://github.com/Suwayomi/Tachidesk-WebUI/pull/201) by @martinek)
- (r962) Refactor/download queue and cleanup visuals overall ([#202](https://github.com/Suwayomi/Tachidesk-WebUI/pull/202) by @martinek)
- (r963) Fix "back" pagination on double page layout in reader for spread pages ([#203](https://github.com/Suwayomi/Tachidesk-WebUI/pull/203) by @martinek)
# Server: v0.6.5 + WebUI: r946
## TL;DR
- Fixed Windows bundler
## Tachidesk-Server Changelog
- (r1113) v0.6.4 (by @AriaMoradi)
- (r1114) fix broken links (by @AriaMoradi)
- (r1115) fix more broken stuff (by @AriaMoradi)
- (r1116) fix more broken stuff (by @AriaMoradi)
- (r1117) fix more broken stuff (by @AriaMoradi)
- (r1118) Update winget.yml ([#393](https://github.com/Suwayomi/Tachidesk-Server/pull/393) by @vedantmgoyal2009)
- (r1119) fix jre path([#396](https://github.com/Suwayomi/Tachidesk-Server/pull/396) by @voltrare)
- (r1120) Fix deb package ([#397](https://github.com/Suwayomi/Tachidesk-Server/pull/397) by @mahor1221)
- (r1121) bump version (by @AriaMoradi)
## Tachidesk-WebUI Changelog
- None
# Server: v0.6.4 + WebUI: r946
## TL;DR
- No new major features
- Bug fixes and changes for packaging
- Documentation changes
## Tachidesk-Server Changelog ## Tachidesk-Server Changelog
- (r1087) v0.6.3 (by @AriaMoradi) - (r1087) v0.6.3 (by @AriaMoradi)
@@ -15,10 +114,28 @@
- (r1096) document all endpoints ([#350](https://github.com/Suwayomi/Tachidesk-Server/pull/350) by @Syer10) - (r1096) document all endpoints ([#350](https://github.com/Suwayomi/Tachidesk-Server/pull/350) by @Syer10)
- (r1097) fix copymanga ([#354](https://github.com/Suwayomi/Tachidesk-Server/pull/354) by @AriaMoradi) - (r1097) fix copymanga ([#354](https://github.com/Suwayomi/Tachidesk-Server/pull/354) by @AriaMoradi)
- (r1098) fix formatting by kotlinter (by @AriaMoradi) - (r1098) fix formatting by kotlinter (by @AriaMoradi)
- (r1099) bump WebUI (by @AriaMoradi)
- (r1100) fix WebUI release name (by @AriaMoradi)
- (r1101) Fix documentation errors ([#358](https://github.com/Suwayomi/Tachidesk-Server/pull/358) by @Syer10)
- (r1102) Docs improvements ([#359](https://github.com/Suwayomi/Tachidesk-Server/pull/359) by @Syer10)
- (r1103) Add linux-all.tar.gz & systemd service ([#366](https://github.com/Suwayomi/Tachidesk-Server/pull/366) by @mahor1221)
- (r1104) Publish to Windows Package Managar (WinGet) ([#369](https://github.com/Suwayomi/Tachidesk-Server/pull/369) by @vedantmgoyal2009)
- (r1105) Refactor scripts ([#370](https://github.com/Suwayomi/Tachidesk-Server/pull/370) by @mahor1221)
- (r1106) Run workflow jobs toghether ([#371](https://github.com/Suwayomi/Tachidesk-Server/pull/371) by @mahor1221)
- (r1107) Update gradle action ([#372](https://github.com/Suwayomi/Tachidesk-Server/pull/372) by @mahor1221)
- (r1108) Improve DocumentationDsl, bugfix default values and add queryParams ([#378](https://github.com/Suwayomi/Tachidesk-Server/pull/378) by @Syer10)
- (r1109) Tidy up bundler script ([#380](https://github.com/Suwayomi/Tachidesk-Server/pull/380) by @mahor1221)
- (r1110) Replace linux-all with linux-assets ([#381](https://github.com/Suwayomi/Tachidesk-Server/pull/381) by @mahor1221)
- (r1111) Rename every instance of Tachidesk jar to Tachdidesk-Server.jar ([#384](https://github.com/Suwayomi/Tachidesk-Server/pull/384) by @AriaMoradi)
- (r1112) Fix mistakes from #384 ([#385](https://github.com/Suwayomi/Tachidesk-Server/pull/385) by @AriaMoradi)
## Tachidesk-WebUI Changelog ## Tachidesk-WebUI Changelog
- (r943) fix default width ([#171](https://github.com/Suwayomi/Tachidesk-WebUI/pull/171) by @Robonau) - (r943) fix default width ([#171](https://github.com/Suwayomi/Tachidesk-WebUI/pull/171) by @Robonau)
- (r944) added an update checker button for library ([#172](https://github.com/Suwayomi/Tachidesk-WebUI/pull/172) by @infix) - (r944) added an update checker button for library ([#172](https://github.com/Suwayomi/Tachidesk-WebUI/pull/172) by @infix)
- (r945) fix download queue delete button ([#176](https://github.com/Suwayomi/Tachidesk-WebUI/pull/176) by @Kreach37)
- (r946) fix mangadex filters ([#177](https://github.com/Suwayomi/Tachidesk-WebUI/pull/177) by @Robonau)
# Server: v0.6.3 + WebUI: r942 # Server: v0.6.3 + WebUI: r942
## TL;DR ## TL;DR

View File

@@ -48,7 +48,7 @@ Here's a list of known clients/user interfaces for Tachidesk-Server:
##### Actively Developed Cients ##### Actively Developed Cients
- [Tachidesk-WebUI](https://github.com/Suwayomi/Tachidesk-WebUI): The web/ElectronJS front-end that Tachidesk-Server is traditionally shipped with. Usually gets new features faster. - [Tachidesk-WebUI](https://github.com/Suwayomi/Tachidesk-WebUI): The web/ElectronJS front-end that Tachidesk-Server is traditionally shipped with. Usually gets new features faster.
- [Tachidesk-JUI](https://github.com/Suwayomi/Tachidesk-JUI): The native desktop front-end for Tachidesk-Server. Currently the most advanced. - [Tachidesk-JUI](https://github.com/Suwayomi/Tachidesk-JUI): The native desktop front-end for Tachidesk-Server. Currently the most advanced.
- [Tachidesk-qtui](https://github.com/Suwayomi/Tachidesk-qtui): A C++/Qt front-end for mobile devices(Android/linux), in super early stage of development. - [Tachidesk-qtui](https://github.com/Suwayomi/Tachidesk-qtui): A C++/Qt front-end for mobile devices(Android/linux), in super early stage of development.
- [Tachidesk-Sorayomi](https://github.com/Suwayomi/Tachidesk-Sorayomi): A Flutter front-end for Desktop(Linux, windows, etc.), Web and Android. UI and UX similar to Tachiyomi. - [Tachidesk-Sorayomi](https://github.com/Suwayomi/Tachidesk-Sorayomi): A Flutter front-end for Desktop(Linux, windows, etc.), Web and Android. UI and UX similar to Tachiyomi.
##### Inctive/Abandoned Cients ##### Inctive/Abandoned Cients
- [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js, in super early stage of development. - [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js, in super early stage of development.
@@ -85,7 +85,7 @@ Download the latest `win32`(Windows 32-bit) or `win64`(Windows 64-bit) release f
Unzip the downloaded file and double click on one of the launcher scripts. Unzip the downloaded file and double click on one of the launcher scripts.
### macOS ### macOS
Download the latest `macOS-x64`(older macOS systems) or `macOS-arm64`(Apple M1) release from [the releases section](https://github.com/Suwayomi/Tachidesk-Server/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Tachidesk-Server-preview/releases). Download the latest `macOS-x64`(older macOS systems) or `macOS-arm64`(Apple M1 and newer) release from [the releases section](https://github.com/Suwayomi/Tachidesk-Server/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Tachidesk-Server-preview/releases).
Unzip the downloaded file and double click on one of the launcher scripts. Unzip the downloaded file and double click on one of the launcher scripts.

View File

@@ -5,8 +5,9 @@ import org.jmailen.gradle.kotlinter.tasks.LintTask
plugins { plugins {
kotlin("jvm") version kotlinVersion kotlin("jvm") version kotlinVersion
kotlin("plugin.serialization") version kotlinVersion kotlin("plugin.serialization") version kotlinVersion
id("org.jmailen.kotlinter") version "3.8.0" id("org.jmailen.kotlinter") version "3.12.0"
id("com.github.gmazzo.buildconfig") version "3.0.3" apply false id("com.github.gmazzo.buildconfig") version "3.1.0" apply false
id("de.undercouch.download") version "5.3.0"
} }
allprojects { allprojects {
@@ -17,8 +18,8 @@ allprojects {
repositories { repositories {
mavenCentral() mavenCentral()
google() google()
maven("https://jitpack.io")
maven("https://github.com/Suwayomi/Tachidesk-Server/raw/android-jar/") maven("https://github.com/Suwayomi/Tachidesk-Server/raw/android-jar/")
maven("https://jitpack.io")
} }
} }
@@ -43,12 +44,6 @@ configure(projects) {
dependsOn(formatKotlin) dependsOn(formatKotlin)
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString() jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs = listOf(
"-Xopt-in=kotlin.RequiresOptIn",
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
} }
} }
@@ -69,40 +64,42 @@ configure(projects) {
testImplementation(kotlin("test-junit5")) testImplementation(kotlin("test-junit5"))
// coroutines // coroutines
val coroutinesVersion = "1.6.0" val coroutinesVersion = "1.6.4"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
val kotlinSerializationVersion = "1.3.2" val kotlinSerializationVersion = "1.4.1"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
// Dependency Injection // Dependency Injection
implementation("org.kodein.di:kodein-di-conf-jvm:7.10.0") implementation("org.kodein.di:kodein-di-conf-jvm:7.15.0")
// Logging // Logging
// Stuck on old versions since
// 1. Logback 1.3.0+ requires Java 9
// 2. Slf4j 2.0.0+ doesn't register older versions of Logback
// 3. Kotlin-logging 3.0.2+ requires Java 11, but this is probably a bug
implementation("org.slf4j:slf4j-api:1.7.32") implementation("org.slf4j:slf4j-api:1.7.32")
implementation("ch.qos.logback:logback-classic:1.2.6") implementation("ch.qos.logback:logback-classic:1.2.6")
implementation("io.github.microutils:kotlin-logging:2.1.21") implementation("io.github.microutils:kotlin-logging:2.1.21")
// ReactiveX // ReactiveX
implementation("io.reactivex:rxjava:1.3.8") implementation("io.reactivex:rxjava:1.3.8")
implementation("io.reactivex:rxkotlin:1.0.0")
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
// dependency both in AndroidCompat and extensions, version locked by Tachiyomi app/extensions // dependency both in AndroidCompat and extensions, version locked by Tachiyomi app/extensions
implementation("org.jsoup:jsoup:1.14.3") implementation("org.jsoup:jsoup:1.15.3")
// dependency of :AndroidCompat:Config // dependency of :AndroidCompat:Config
implementation("com.typesafe:config:1.4.1") implementation("com.typesafe:config:1.4.2")
implementation("io.github.config4k:config4k:0.4.2") implementation("io.github.config4k:config4k:0.5.0")
// to get application content root // to get application content root
implementation("net.harawata:appdirs:1.2.1") implementation("net.harawata:appdirs:1.2.1")
// dex2jar // dex2jar
val dex2jarVersion = "v35" val dex2jarVersion = "v56"
implementation("com.github.ThexXTURBOXx.dex2jar:dex-translator:$dex2jarVersion") implementation("com.github.ThexXTURBOXx.dex2jar:dex-translator:$dex2jarVersion")
implementation("com.github.ThexXTURBOXx.dex2jar:dex-tools:$dex2jarVersion") implementation("com.github.ThexXTURBOXx.dex2jar:dex-tools:$dex2jarVersion")

View File

@@ -7,15 +7,14 @@ import java.io.BufferedReader
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
const val kotlinVersion = "1.6.10" const val kotlinVersion = "1.7.20"
const val MainClass = "suwayomi.tachidesk.MainKt" const val MainClass = "suwayomi.tachidesk.MainKt"
// should be bumped with each stable release // should be bumped with each stable release
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.6.3" val tachideskVersion = System.getenv("ProductVersion") ?: "v0.6.6"
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r944" val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r963"
val sorayomiRevisionTag = System.getenv("SorayomiRevision") ?: "0.1.5"
// counts commits on the master branch // counts commits on the master branch
val tachideskRevision = runCatching { val tachideskRevision = runCatching {

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

296
scripts/bundler.sh Executable file
View File

@@ -0,0 +1,296 @@
#!/bin/bash
# Copyright (C) Contributors to the Suwayomi project
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
main() {
POSITIONAL_ARGS=()
OUTPUT_DIR=.
while [ "$#" -gt 0 ]; do
case "$1" in
-o|--output-dir)
OUTPUT_DIR="$(readlink -e "$2" || exit 1)"
shift
shift
;;
*)
POSITIONAL_ARGS+=("$1")
shift
;;
esac
done
# restore positional parameters
set -- "${POSITIONAL_ARGS[@]}"
OS="$1"
JAR="$(ls server/build/*.jar | tail -n1)"
RELEASE_NAME="$(echo "${JAR%.*}" | xargs basename)-$OS"
RELEASE_VERSION="$(tmp="${JAR%-*}"; echo "${tmp##*-}" | tr -d v)"
#RELEASE_REVISION_NUMBER="$(tmp="${JAR%.*}" && echo "${tmp##*-}" | tr -d r)"
local electron_version="v14.0.0"
# clean temporary directory on function return
trap "rm -rf $RELEASE_NAME/" RETURN
mkdir "$RELEASE_NAME/"
case "$OS" in
debian-all)
RELEASE="$RELEASE_NAME.deb"
make_deb_package
move_release_to_output_dir
;;
linux-assets)
RELEASE="$RELEASE_NAME.tar.gz"
copy_linux_package_assets_to "$RELEASE_NAME/"
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/"
move_release_to_output_dir
;;
linux-x64)
JRE="OpenJDK8U-jre_x64_linux_hotspot_8u302b08.tar.gz"
JRE_RELEASE="jdk8u302-b08"
JRE_DIR="$JRE_RELEASE-jre"
JRE_URL="https://github.com/adoptium/temurin8-binaries/releases/download/$JRE_RELEASE/$JRE"
ELECTRON="electron-$electron_version-linux-x64.zip"
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
download_jre_and_electron
RELEASE="$RELEASE_NAME.tar.gz"
make_linux_bundle
move_release_to_output_dir
;;
macOS-x64)
JRE="OpenJDK8U-jre_x64_mac_hotspot_8u302b08.tar.gz"
JRE_RELEASE="jdk8u302-b08"
JRE_DIR="$JRE_RELEASE-jre"
JRE_URL="https://github.com/adoptium/temurin8-binaries/releases/download/$JRE_RELEASE/$JRE"
ELECTRON="electron-$electron_version-darwin-x64.zip"
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
download_jre_and_electron
RELEASE="$RELEASE_NAME.zip"
make_macos_bundle
move_release_to_output_dir
;;
macOS-arm64)
JRE="zulu8.56.0.23-ca-jre8.0.302-macosx_aarch64.tar.gz"
JRE_RELEASE="zulu8.56.0.23-ca-jre8.0.302-macosx_aarch64"
JRE_DIR="$JRE_RELEASE/zulu-8.jre"
JRE_URL="https://cdn.azul.com/zulu/bin/$JRE"
ELECTRON="electron-$electron_version-darwin-arm64.zip"
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
download_jre_and_electron
RELEASE="$RELEASE_NAME.zip"
make_macos_bundle
move_release_to_output_dir
;;
windows-x86)
JRE="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
JRE_RELEASE="jdk8u292-b10"
JRE_DIR="$JRE_RELEASE-jre"
JRE_URL="https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/$JRE_RELEASE/$JRE"
ELECTRON="electron-$electron_version-win32-ia32.zip"
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
download_jre_and_electron
RELEASE="$RELEASE_NAME.zip"
make_windows_bundle
move_release_to_output_dir
RELEASE="$RELEASE_NAME.msi"
make_windows_package
move_release_to_output_dir
;;
windows-x64)
JRE="OpenJDK8U-jre_x64_windows_hotspot_8u302b08.zip"
JRE_RELEASE="jdk8u302-b08"
JRE_DIR="$JRE_RELEASE-jre"
JRE_URL="https://github.com/adoptium/temurin8-binaries/releases/download/$JRE_RELEASE/$JRE"
ELECTRON="electron-$electron_version-win32-x64.zip"
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
download_jre_and_electron
RELEASE="$RELEASE_NAME.zip"
make_windows_bundle
move_release_to_output_dir
RELEASE="$RELEASE_NAME.msi"
make_windows_package
move_release_to_output_dir
;;
*)
error $LINENO "Unsupported operating system: $OS" 2
;;
esac
}
move_release_to_output_dir() {
# clean up from possible previous runs
if [ -f "$OUTPUT_DIR/$RELEASE" ]; then
rm "$OUTPUT_DIR/$RELEASE"
fi
mv "$RELEASE" "$OUTPUT_DIR/"
}
download_jre_and_electron() {
if [ ! -f "$JRE" ]; then
curl -L "$JRE_URL" -o "$JRE"
fi
if [ ! -f "$ELECTRON" ]; then
curl -L "$ELECTRON_URL" -o "$ELECTRON"
fi
local ext="${JRE##*.}"
if [ "$ext" = "zip" ]; then
unzip "$JRE"
else
tar xvf "$JRE"
fi
mv "$JRE_DIR" "$RELEASE_NAME/jre"
unzip "$ELECTRON" -d "$RELEASE_NAME/electron/"
tree
}
copy_linux_package_assets_to() {
local output_dir
output_dir="$(readlink -e "$1" || exit 1)"
cp "scripts/resources/pkg/tachidesk-server-browser-launcher.sh" "$output_dir/"
cp "scripts/resources/pkg/tachidesk-server-debug-launcher.sh" "$output_dir/"
cp "scripts/resources/pkg/tachidesk-server-electron-launcher.sh" "$output_dir/"
cp "scripts/resources/pkg/tachidesk-server.desktop" "$output_dir/"
cp "scripts/resources/pkg/systemd"/* "$output_dir/"
cp "server/src/main/resources/icon/faviconlogo.png" \
"$output_dir/tachidesk-server.png"
}
make_linux_bundle() {
cp "$JAR" "$RELEASE_NAME/Tachidesk-Server.jar"
cp "scripts/resources/tachidesk-server-browser-launcher.sh" "$RELEASE_NAME/"
cp "scripts/resources/tachidesk-server-debug-launcher.sh" "$RELEASE_NAME/"
cp "scripts/resources/tachidesk-server-electron-launcher.sh" "$RELEASE_NAME/"
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/"
}
make_macos_bundle() {
cp "$JAR" "$RELEASE_NAME/Tachidesk-Server.jar"
cp "scripts/resources/Tachidesk Browser Launcher.command" "$RELEASE_NAME/"
cp "scripts/resources/Tachidesk Debug Launcher.command" "$RELEASE_NAME/"
cp "scripts/resources/Tachidesk Electron Launcher.command" "$RELEASE_NAME/"
zip -9 -r "$RELEASE" "$RELEASE_NAME/"
}
# https://wiki.debian.org/SimplePackagingTutorial
# https://www.debian.org/doc/manuals/packaging-tutorial/packaging-tutorial.pdf
make_deb_package() {
#behind $RELEASE_VERSION is hyphen "-"
local source_dir="tachidesk-server-$RELEASE_VERSION"
#behind $RELEASE_VERSION is underscore "_"
local upstream_source="tachidesk-server_$RELEASE_VERSION.orig.tar.gz"
mkdir "$RELEASE_NAME/$source_dir/"
cp "$JAR" "$RELEASE_NAME/$source_dir/Tachidesk-Server.jar"
copy_linux_package_assets_to "$RELEASE_NAME/$source_dir/"
tar -I "gzip" -C "$RELEASE_NAME/" -cvf "$upstream_source" "$source_dir"
cp -r "scripts/resources/deb/" "$RELEASE_NAME/$source_dir/debian/"
sed -i "s/\$pkgver/$RELEASE_VERSION/" "$RELEASE_NAME/$source_dir/debian/changelog"
sed -i "s/\$pkgrel/1/" "$RELEASE_NAME/$source_dir/debian/changelog"
sudo apt install devscripts build-essential dh-exec
cd "$RELEASE_NAME/$source_dir/"
dpkg-buildpackage --no-sign --build=all
cd -
local deb="tachidesk-server_$RELEASE_VERSION-1_all.deb"
mv "$RELEASE_NAME/$deb" "$RELEASE"
}
make_windows_bundle() {
## I disabled this section until someone find a solution to this error:
##E: Unable to correct problems, you have held broken packages.
##./bundler.sh: line 250: wine: command not found
## check if running under github actions
#if [ "$CI" = true ]; then
## change electron executable's icon
#sudo dpkg --add-architecture i386
#wget -qO - https://dl.winehq.org/wine-builds/winehq.key \
#| sudo apt-key add -
#sudo add-apt-repository ppa:cybermax-dexter/sdl2-backport
#sudo apt-add-repository "deb https://dl.winehq.org/wine-builds/ubuntu \
#$(lsb_release -cs) main"
#sudo apt install --install-recommends winehq-stable
#fi
## this script assumes that wine is installed here on out
#local rcedit="rcedit-x85.exe"
#local rcedit_url="https://github.com/electron/rcedit/releases/download/v0.1.1/$rcedit"
## change electron's icon
#if [ ! -f "$rcedit" ]; then
#curl -L "$rcedit_url" -o "$rcedit"
#fi
#local icon="server/src/main/resources/icon/faviconlogo.ico"
#WINEARCH=win32 wine "$rcedit" "$RELEASE_NAME/electron/electron.exe" \
# --set-icon "$icon"
cp "$JAR" "$RELEASE_NAME/Tachidesk-Server.jar"
cp "scripts/resources/Tachidesk Browser Launcher.bat" "$RELEASE_NAME"
cp "scripts/resources/Tachidesk Debug Launcher.bat" "$RELEASE_NAME"
cp "scripts/resources/Tachidesk Electron Launcher.bat" "$RELEASE_NAME"
zip -9 -r "$RELEASE" "$RELEASE_NAME"
}
make_windows_package() {
if [ "$CI" = true ]; then
sudo apt install -y wixl
fi
find "$RELEASE_NAME/jre" \
| wixl-heat --var var.SourceDir -p "$RELEASE_NAME/" \
--directory-ref jre --component-group jre >"$RELEASE_NAME/jre.wxs"
find "$RELEASE_NAME/electron" \
| wixl-heat --var var.SourceDir -p "$RELEASE_NAME/" \
--directory-ref electron --component-group electron >"$RELEASE_NAME/electron.wxs"
local icon="server/src/main/resources/icon/faviconlogo.ico"
local arch=${OS##*-}
wixl -D ProductVersion="$RELEASE_VERSION" -D SourceDir="$RELEASE_NAME" \
-D Icon="$icon" --arch "$arch" "scripts/resources/msi/tachidesk-server-$arch.wxs" \
"$RELEASE_NAME/jre.wxs" "$RELEASE_NAME/electron.wxs" -o "$RELEASE"
}
# Error handler
# set -u: Treat unset variables as an error when substituting.
# set -o pipefail: Prevents errors in pipeline from being masked.
# set -e: Immediatly exit if any command has a non-zero exit status.
# set -E: Inherit the trap ERR function before exiting by set.
#
# set -e is not recommended and unpredictable.
# see https://stackoverflow.com/questions/64786/error-handling-in-bash
# and http://mywiki.wooledge.org/BashFAO/105
set -uo pipefail
error() {
local parent_lineno="$1"
local message="$2"
local code="${3:-1}"
if [ -z "$message" ]; then
echo "$0: line $parent_lineno: exiting with status $code"
else
echo "$0: line $parent_lineno: $message: exiting with status $code"
fi
exit "$code"
}
trap 'error $LINENO ""' ERR
main "$@"; exit

View File

@@ -1,51 +0,0 @@
#!/bin/bash
# Copyright (C) Contributors to the Suwayomi project
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
echo "Creating DEB Package"
pkgname="tachidesk-server"
PkgName="Tachidesk-Server"
jar=$(ls ../server/build/*.jar | tail -n1)
pkgver="$(tmp="${jar%-*}" && echo "${tmp##*-}" | tr -d v)"
pkgrel=1
srcdir="$pkgname-$pkgver" # uses hyphen "-"
srctgz="${pkgname}_$pkgver.orig.tar.gz" # uses underscore "_"
deb="${pkgname}_$pkgver-${pkgrel}_all.deb"
Deb="${PkgName}_$pkgver-${pkgrel}_all.deb"
# Prepare
mkdir "$srcdir/"
cp "$jar" "$srcdir/$pkgname.jar"
cp "resources/$pkgname-browser-launcher.sh" "$srcdir/"
cp "resources/$pkgname-debug-launcher.sh" "$srcdir/"
cp "resources/$pkgname-electron-launcher-debian.sh" "$srcdir/$pkgname-electron-launcher.sh"
cp "resources/$pkgname.desktop" "$srcdir/"
cp "../server/src/main/resources/icon/faviconlogo.png" "$srcdir/$pkgname.png"
GZIP=-9 tar -cvzf "$srctgz" "$srcdir/"
cp -r "resources/debian" "$srcdir/"
sed -i "s/\${pkgver}/$pkgver/" "$srcdir/debian/changelog"
sed -i "s/\${pkgrel}/$pkgrel/" "$srcdir/debian/changelog"
# Build
mkdir "debuild/"
mv "$srctgz" "$srcdir/" "debuild/"
sudo apt install devscripts build-essential dh-exec
# --lintian-opts --profile are for building Debian packages on Ubuntu
cd "debuild/$srcdir/debian"
debuild -uc -us --lintian-opts --profile debian
cd -
# clean up from possible previous runs
if [ -f "../server/build/$Deb" ]; then
rm "../server/build/$Deb"
fi
mv "debuild/$deb" "../server/build/$Deb"
rm -rf "debuild/"

View File

@@ -1 +1 @@
start "" jre/bin/javaw -jar Tachidesk.jar start "" jre/bin/javaw -jar Tachidesk-Server.jar

View File

@@ -1,3 +1,3 @@
cd "`dirname "$0"`" cd "`dirname "$0"`"
./jre/Contents/Home/bin/java -jar Tachidesk.jar ./jre/Contents/Home/bin/java -jar Tachidesk-Server.jar

View File

@@ -1,7 +1,7 @@
:: cleaner output :: cleaner output
@echo off @echo off
jre\bin\java -Dsuwayomi.tachidesk.config.server.debugLogsEnabled=true -jar Tachidesk.jar jre\bin\java -Dsuwayomi.tachidesk.config.server.debugLogsEnabled=true -jar Tachidesk-Server.jar
:: Prevent cmd from closing when Tachidesk crashes :: Prevent cmd from closing when Tachidesk crashes
pause pause

View File

@@ -1,3 +1,3 @@
cd "`dirname "$0"`" cd "`dirname "$0"`"
./jre/Contents/Home/bin/java -Dsuwayomi.tachidesk.config.server.debugLogsEnabled=true -jar Tachidesk.jar ./jre/Contents/Home/bin/java -Dsuwayomi.tachidesk.config.server.debugLogsEnabled=true -jar Tachidesk-Server.jar

View File

@@ -1 +1 @@
jre\bin\javaw "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron" "-Dsuwayomi.tachidesk.config.server.electronPath=electron/electron.exe" -jar Tachidesk.jar jre\bin\javaw "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron" "-Dsuwayomi.tachidesk.config.server.electronPath=electron/electron.exe" -jar Tachidesk-Server.jar

View File

@@ -1,3 +1,3 @@
cd "`dirname "$0"`" cd "`dirname "$0"`"
./jre/Contents/Home/bin/java "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron" "-Dsuwayomi.tachidesk.config.server.electronPath=electron/Electron.app/Contents/MacOS/Electron" -jar Tachidesk.jar ./jre/Contents/Home/bin/java "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron" "-Dsuwayomi.tachidesk.config.server.electronPath=electron/Electron.app/Contents/MacOS/Electron" -jar Tachidesk-Server.jar

View File

@@ -0,0 +1,5 @@
tachidesk-server ($pkgver-$pkgrel) unstable; urgency=medium
* See CHANGELOG.md on https://github.com/Suwayomi/Tachidesk-Server
-- Mahor1221 <mahor1221@pm.me> Fri, 14 Jan 2022 00:00:00 +0000

View File

@@ -1,14 +1,14 @@
Source: tachidesk-server Source: tachidesk-server
Section: web Section: web
Priority: optional Priority: optional
Maintainer: Mahor Foruzesh <mahorforuzesh@pm.me> Maintainer: Mahor1221 <mahor1221@pm.me>
Build-Depends: debhelper-compat (= 12), dh-exec Build-Depends: debhelper-compat (= 13), dh-exec
Standards-Version: 4.5.1 Standards-Version: 4.5.1
Homepage: https://github.com/Suwayomi/Tachidesk-Server Homepage: https://github.com/Suwayomi/Tachidesk-Server
Package: tachidesk-server Package: tachidesk-server
Architecture: all Architecture: all
Depends: ${misc:Depends}, default-jre-headless (>= 8) Depends: ${misc:Depends}, java8-runtime-headless, libc++-dev
Description: Manga Reader Description: Manga Reader
A free and open source manga reader server that runs extensions built for Tachiyomi. A free and open source manga reader server that runs extensions built for Tachiyomi.
Tachidesk is an independent Tachiyomi compatible software and is not a Fork of Tachiyomi. Tachidesk is an independent Tachiyomi compatible software and is not a Fork of Tachiyomi.

View File

12
scripts/resources/deb/install Executable file
View File

@@ -0,0 +1,12 @@
#!/usr/bin/dh-exec
Tachidesk-Server.jar usr/share/java/tachidesk-server/
tachidesk-server.png usr/share/pixmaps/
tachidesk-server.desktop usr/share/applications/
tachidesk-server.service usr/lib/systemd/system/
tachidesk-server.sysusers => usr/lib/sysusers.d/tachidesk-server.conf
tachidesk-server.tmpfiles => usr/lib/tmpfiles.d/tachidesk-server.conf
tachidesk-server.conf => etc/tachidesk/server.conf
tachidesk-server-browser-launcher.sh => usr/bin/tachidesk-server-browser
tachidesk-server-debug-launcher.sh => usr/bin/tachidesk-server-debug
tachidesk-server-electron-launcher.sh => usr/bin/tachidesk-server-electron

View File

@@ -1,5 +0,0 @@
tachidesk-server (${pkgver}-${pkgrel}) unstable; urgency=medium
* See CHANGELOG.md on https://github.com/Suwayomi/Tachidesk-Server
-- Mahor Foruzesh <mahorforuzesh@pm.me> Fri, 14 Jan 2022 00:00:00 +0000

View File

@@ -1,8 +0,0 @@
#!/usr/bin/dh-exec
tachidesk-server-browser-launcher.sh => usr/bin/tachidesk-server-browser
tachidesk-server-debug-launcher.sh => usr/bin/tachidesk-server-debug
tachidesk-server-electron-launcher.sh => usr/bin/tachidesk-server-electron
tachidesk-server.jar usr/share/java/tachidesk-server/
tachidesk-server.png usr/share/pixmaps/
tachidesk-server.desktop usr/share/applications/

View File

@@ -31,7 +31,7 @@
<!-- Component --> <!-- Component -->
<DirectoryRef Id="INSTALLDIR"> <DirectoryRef Id="INSTALLDIR">
<Component Id="TachideskJAR" Guid="*" Win64="yes"> <Component Id="TachideskJAR" Guid="*" Win64="yes">
<File Id="Tachidesk.jar" Source="$(var.SourceDir)/Tachidesk.jar" KeyPath="yes" /> <File Id="Tachidesk-Server.jar" Source="$(var.SourceDir)/Tachidesk-Server.jar" KeyPath="yes" />
</Component> </Component>
<Component Id="TachideskBrowserBAT" Guid="*" Win64="yes"> <Component Id="TachideskBrowserBAT" Guid="*" Win64="yes">

View File

@@ -27,7 +27,7 @@
<!-- Component --> <!-- Component -->
<DirectoryRef Id="INSTALLDIR"> <DirectoryRef Id="INSTALLDIR">
<Component Id="TachideskJAR" Guid="*"> <Component Id="TachideskJAR" Guid="*">
<File Id="Tachidesk.jar" Source="$(var.SourceDir)/Tachidesk.jar" KeyPath="yes" /> <File Id="Tachidesk-Server.jar" Source="$(var.SourceDir)/Tachidesk-Server.jar" KeyPath="yes" />
</Component> </Component>
<Component Id="TachideskBrowserBAT" Guid="*"> <Component Id="TachideskBrowserBAT" Guid="*">

View File

@@ -0,0 +1,5 @@
TACHIDESK_ROOT_DIR="/var/lib/tachidesk"
# Extra arguments passed to the java command
# The default value disables the system tray icon, and launching a browser on service start.
JAVA_ARGS=-Dsuwayomi.tachidesk.config.server.initialOpenInBrowserEnabled=false -Dsuwayomi.tachidesk.config.server.systemTrayEnabled=false

View File

@@ -0,0 +1,31 @@
[Unit]
Description=A free and open source manga reader server that runs extensions built for Tachiyomi.
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=tachidesk
Group=tachidesk
SyslogIdentifier=tachidesk
EnvironmentFile=/etc/tachidesk/server.conf
ExecStart=/usr/bin/java $JAVA_ARGS -Dsuwayomi.tachidesk.config.server.rootDir="${TACHIDESK_ROOT_DIR}" -jar /usr/share/java/tachidesk-server/Tachidesk-Server.jar
Restart=on-failure
ProtectSystem=full
ProtectHome=true
PrivateTmp=yes
PrivateDevices=yes
ProtectClock=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
RestrictRealtime=yes
RestrictNamespaces=yes
NoNewPrivileges=yes
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,2 @@
#Type Name ID GECOS Home directory Shell
u tachidesk - "Tachidesk Manga Server" /var/lib/tachidesk

View File

@@ -0,0 +1,2 @@
#Type Path Mode User Group Age Argument
d /var/lib/tachidesk 0755 tachidesk tachidesk

View File

@@ -0,0 +1,3 @@
#!/bin/sh
exec /usr/bin/java -jar /usr/share/java/tachidesk-server/Tachidesk-Server.jar

View File

@@ -0,0 +1,5 @@
#!/bin/sh
exec /usr/bin/java \
-Dsuwayomi.tachidesk.config.server.debugLogsEnabled=true \
-jar /usr/share/java/tachidesk-server/Tachidesk-Server.jar

View File

@@ -0,0 +1,12 @@
#!/bin/sh
if [ ! -f /usr/bin/electron ]; then
echo "Electron executable was not found!
In order to run this launcher, you need Electron installed."
exit 1
fi
exec /usr/bin/java \
-Dsuwayomi.tachidesk.config.server.webUIInterface=electron \
-Dsuwayomi.tachidesk.config.server.electronPath=/usr/bin/electron \
-jar /usr/share/java/tachidesk-server/Tachidesk-Server.jar

View File

@@ -2,7 +2,7 @@
Type=Application Type=Application
Name=Tachidesk-Server Name=Tachidesk-Server
Comment=Manga Reader Comment=Manga Reader
Exec=/usr/bin/java -jar /usr/share/java/tachidesk-server/tachidesk-server.jar "\\$@" Exec=/usr/bin/java -jar /usr/share/java/tachidesk-server/Tachidesk-Server.jar "\\$@"
Icon=tachidesk-server Icon=tachidesk-server
Terminal=false Terminal=false
Categories=Network; Categories=Network;

View File

@@ -1,2 +1,3 @@
#!/bin/sh #!/bin/sh
exec /usr/bin/java -jar /usr/share/java/tachidesk-server/tachidesk-server.jar
exec ./jre/bin/java -jar ./Tachidesk-Server.jar

View File

@@ -1,2 +1,5 @@
#!/bin/sh #!/bin/sh
exec /usr/bin/java -Dsuwayomi.tachidesk.config.server.debugLogsEnabled=true -jar /usr/share/java/tachidesk-server/tachidesk-server.jar
exec ./jre/bin/java \
-Dsuwayomi.tachidesk.config.server.debugLogsEnabled=true \
-jar ./Tachidesk-Server.jar

View File

@@ -1,12 +0,0 @@
#!/bin/sh
if [ ! -f /usr/bin/electron ]; then
echo "Electron executable was not found
In order to run this launcher, you need Electron installed.
You can install it with these commands:
sudo apt install npm
sudo npm install electron -g"
exit 1
fi
exec /usr/bin/java -Dsuwayomi.tachidesk.config.server.webUIInterface=electron -Dsuwayomi.tachidesk.config.server.electronPath=/usr/bin/electron -jar /usr/share/java/tachidesk-server/tachidesk-server.jar

View File

@@ -1,3 +1,6 @@
#!/bin/sh #!/bin/sh
exec /usr/bin/java -Dsuwayomi.tachidesk.config.server.webUIInterface=electron -Dsuwayomi.tachidesk.config.server.electronPath=/usr/bin/electron -jar /usr/share/java/tachidesk-server/tachidesk-server.jar exec ./jre/bin/java \
-Dsuwayomi.tachidesk.config.server.webUIInterface=electron \
-Dsuwayomi.tachidesk.config.server.electronPath=./electron/electron \
-jar ./Tachidesk-Server.jar

View File

@@ -1,87 +0,0 @@
#!/bin/bash
# Copyright (C) Contributors to the Suwayomi project
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
electron_version="v14.0.0"
if [ $1 = "linux-x64" ]; then
jre="OpenJDK8U-jre_x64_linux_hotspot_8u302b08.tar.gz"
jre_release="jdk8u302-b08"
jre_url="https://github.com/adoptium/temurin8-binaries/releases/download/$jre_release/$jre"
jre_dir="$jre_release-jre"
electron="electron-$electron_version-linux-x64.zip"
elif [ $1 = "macOS-x64" ]; then
jre="OpenJDK8U-jre_x64_mac_hotspot_8u302b08.tar.gz"
jre_release="jdk8u302-b08"
jre_url="https://github.com/adoptium/temurin8-binaries/releases/download/$jre_release/$jre"
jre_dir="$jre_release-jre"
electron="electron-$electron_version-darwin-x64.zip"
elif [ $1 = "macOS-arm64" ]; then
jre="zulu8.56.0.23-ca-jre8.0.302-macosx_aarch64.tar.gz"
jre_release="zulu8.56.0.23-ca-jre8.0.302-macosx_aarch64"
jre_url="https://cdn.azul.com/zulu/bin/$jre"
jre_dir="$jre_release/zulu-8.jre"
electron="electron-$electron_version-darwin-arm64.zip"
else
echo "Unsupported arch value: $1"
exit 1
fi
arch="$1"
os=$(echo $arch | cut -d '-' -f1)
echo "creating $arch bundle"
jar=$(ls ../server/build/*.jar | tail -n1)
jar_name=$(echo $jar | cut -d'/' -f4)
release_name=$(echo $jar_name | sed 's/.jar//')-$arch
# make release dir
mkdir $release_name
echo "Dealing with jre..."
if [ ! -f $jre ]; then
curl -L $jre_url -o $jre
fi
tar xvf $jre
mv $jre_dir $release_name/jre
echo "Dealing with electron"
if [ ! -f $electron ]; then
curl -L "https://github.com/electron/electron/releases/download/$electron_version/$electron" -o $electron
fi
unzip $electron -d $release_name/electron
# copy artifacts
cp $jar $release_name/Tachidesk-Server.jar
if [ $os = linux ]; then
cp "resources/tachidesk-server-browser-launcher.sh" $release_name
cp "resources/tachidesk-server-debug-launcher.sh" $release_name
cp "resources/tachidesk-server-electron-launcher.sh" $release_name
elif [ $os = macOS ]; then
cp "resources/Tachidesk Browser Launcher.command" $release_name
cp "resources/Tachidesk Debug Launcher.command" $release_name
cp "resources/Tachidesk Electron Launcher.command" $release_name
fi
archive_name=""
if [ $os = linux ]; then
archive_name=$release_name.tar.gz
GZIP=-9 tar cvzf $archive_name $release_name
elif [ $os = macOS ]; then
archive_name=$release_name.zip
zip -9 -r $archive_name $release_name
fi
rm -rf $release_name
# clean up from possible previous runs
if [ -f ../server/build/$archive_name ]; then
rm ../server/build/$archive_name
fi
mv $archive_name ../server/build/

View File

@@ -1,104 +0,0 @@
#!/bin/bash
# Copyright (C) Contributors to the Suwayomi project
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
electron_version="v14.0.0"
arch=$1
if [ $arch = "win32" ]; then
jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
jre_release="jdk8u292-b10"
jre_url="https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/$jre_release/$jre"
arch="windows-x86"
electron="electron-$electron_version-win32-ia32.zip"
else
jre="OpenJDK8U-jre_x64_windows_hotspot_8u302b08.zip"
jre_release="jdk8u302-b08"
jre_url="https://github.com/adoptium/temurin8-binaries/releases/download/$jre_release/$jre"
arch="windows-x64"
electron="electron-$electron_version-win32-x64.zip"
fi
jre_dir="$jre_release-jre"
echo "creating windows bundle"
jar=$(ls ../server/build/*.jar | tail -n1)
jar_name=$(echo $jar | cut -d'/' -f4)
release_name=$(echo $jar_name | sed 's/.jar//')-$arch
# make release dir
mkdir $release_name
echo "Dealing with jre..."
if [ ! -f $jre ]; then
curl -L $jre_url -o $jre
fi
unzip $jre
mv $jre_dir $release_name/jre
echo "Dealing with electron"
if [ ! -f $electron ]; then
curl -L "https://github.com/electron/electron/releases/download/$electron_version/$electron" -o $electron
fi
unzip $electron -d $release_name/electron
# change electron's icon
rcedit="rcedit-x86.exe"
if [ ! -f $rcedit ]; then
curl -L "https://github.com/electron/rcedit/releases/download/v1.1.1/$rcedit" -o $rcedit
fi
# check if running under github actions
if [ $CI = true ]; then
# change electron executable's icon
sudo dpkg --add-architecture i386
wget -qO - https://dl.winehq.org/wine-builds/winehq.key | sudo apt-key add -
sudo add-apt-repository ppa:cybermax-dexter/sdl2-backport
sudo apt-add-repository "deb https://dl.winehq.org/wine-builds/ubuntu $(lsb_release -cs) main"
sudo apt install --install-recommends winehq-stable
sudo apt install -y wixl
fi
# this script assumes that wine is installed here on out
WINEARCH=win32 wine $rcedit $release_name/electron/electron.exe --set-icon ../server/src/main/resources/icon/faviconlogo.ico
# copy artifacts
cp $jar $release_name/Tachidesk.jar
cp "resources/Tachidesk Browser Launcher.bat" $release_name
cp "resources/Tachidesk Debug Launcher.bat" $release_name
cp "resources/Tachidesk Electron Launcher.bat" $release_name
zip_name=$release_name.zip
zip -9 -r $zip_name $release_name
# create msi package
msi_name=$release_name.msi
release_ver=$(tmp=${jar%-*} && echo ${tmp##*-} | tr -d v)
icon="../server/src/main/resources/icon/faviconlogo.ico"
find $release_name/jre | wixl-heat --var var.SourceDir -p $release_name/ --directory-ref jre --component-group jre >jre.wxs
find $release_name/electron | wixl-heat --var var.SourceDir -p $release_name/ --directory-ref electron --component-group electron >electron.wxs
if [ $arch = "win32" ]; then
wixl -D ProductVersion=$release_ver -D SourceDir=$release_name -D Icon=$icon \
--arch x86 resources/msi/tachidesk-server-x86.wxs jre.wxs electron.wxs -o $msi_name
else
wixl -D ProductVersion=$release_ver -D SourceDir=$release_name -D Icon=$icon \
--arch x64 resources/msi/tachidesk-server-x64.wxs jre.wxs electron.wxs -o $msi_name
fi
rm -rf $release_name
# clean up from possible previous runs
if [ -f ../server/build/$zip_name ]; then
rm ../server/build/$zip_name
fi
if [ -f ../server/build/$msi_name ]; then
rm ../server/build/$msi_name
fi
mv $zip_name ../server/build/
mv $msi_name ../server/build/

View File

@@ -9,31 +9,32 @@ plugins {
dependencies { dependencies {
// okhttp // okhttp
val okhttpVersion = "4.9.3" // Major version is locked by Tachiyomi extensions val okhttpVersion = "4.10.0" // Major version is locked by Tachiyomi extensions
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion") implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion") implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion") implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
implementation("com.squareup.okio:okio:3.0.0") implementation("com.squareup.okio:okio:3.2.0")
// Javalin api // Javalin api
implementation("io.javalin:javalin:4.2.0") // Javalin 5.0.0+ requires Java 11
implementation("io.javalin:javalin-openapi:4.2.0") implementation("io.javalin:javalin:4.6.6")
implementation("io.javalin:javalin-openapi:4.6.6")
// jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency` // jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
val jacksonVersion = "2.12.4" val jacksonVersion = "2.13.3"
implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
// Exposed ORM // Exposed ORM
val exposedVersion = "0.34.1" val exposedVersion = "0.40.1"
implementation("org.jetbrains.exposed:exposed-core:$exposedVersion") implementation("org.jetbrains.exposed:exposed-core:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion") implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion") implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-java-time:$exposedVersion") implementation("org.jetbrains.exposed:exposed-java-time:$exposedVersion")
// current database driver // current database driver, can't update to h2 v2 without sql migration
implementation("com.h2database:h2:1.4.200") implementation("com.h2database:h2:1.4.200")
// Exposed Migrations // Exposed Migrations
implementation("com.github.Suwayomi:exposed-migrations:3.1.4") implementation("com.github.Suwayomi:exposed-migrations:3.2.0")
// tray icon // tray icon
implementation("com.dorkbox:SystemTray:4.1") implementation("com.dorkbox:SystemTray:4.1")
@@ -43,24 +44,23 @@ dependencies {
implementation("com.github.inorichi.injekt:injekt-core:65b0440") implementation("com.github.inorichi.injekt:injekt-core:65b0440")
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion") implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
implementation("io.reactivex:rxjava:1.3.8") implementation("io.reactivex:rxjava:1.3.8")
implementation("org.jsoup:jsoup:1.14.3") implementation("org.jsoup:jsoup:1.15.3")
implementation("app.cash.quickjs:quickjs-jvm:0.9.2")
// Sort // Sort
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1") implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
// asm for ByteCodeEditor(fixing SimpleDateFormat) (must match Dex2Jar version) // asm for ByteCodeEditor(fixing SimpleDateFormat) (must match Dex2Jar version)
implementation("org.ow2.asm:asm:9.2") implementation("org.ow2.asm:asm:9.4")
// Disk & File // Disk & File
implementation("net.lingala.zip4j:zip4j:2.9.1") implementation("net.lingala.zip4j:zip4j:2.11.2")
implementation("com.github.junrar:junrar:7.5.0") implementation("com.github.junrar:junrar:7.5.3")
// CloudflareInterceptor // CloudflareInterceptor
implementation("net.sourceforge.htmlunit:htmlunit:2.56.0") implementation("com.microsoft.playwright:playwright:1.28.0")
// AES/CBC/PKCS7Padding Cypher provider for zh.copymanga // AES/CBC/PKCS7Padding Cypher provider for zh.copymanga
implementation("org.bouncycastle:bcprov-jdk18on:1.71") implementation("org.bouncycastle:bcprov-jdk18on:1.72")
// Source models and interfaces from Tachiyomi 1.x // Source models and interfaces from Tachiyomi 1.x
// using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi // using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi
@@ -74,7 +74,7 @@ dependencies {
// implementation(fileTree("lib/")) // implementation(fileTree("lib/"))
implementation(kotlin("script-runtime")) implementation(kotlin("script-runtime"))
testImplementation("io.mockk:mockk:1.12.2") testImplementation("io.mockk:mockk:1.13.2")
} }
application { application {
@@ -110,9 +110,6 @@ buildConfig {
buildConfigField("String", "WEBUI_REPO", quoteWrap("https://github.com/Suwayomi/Tachidesk-WebUI-preview")) buildConfigField("String", "WEBUI_REPO", quoteWrap("https://github.com/Suwayomi/Tachidesk-WebUI-preview"))
buildConfigField("String", "WEBUI_TAG", quoteWrap(webUIRevisionTag)) buildConfigField("String", "WEBUI_TAG", quoteWrap(webUIRevisionTag))
buildConfigField("String", "SORAYOMI_REPO", quoteWrap("https://github.com/Suwayomi/Tachidesk-Sorayomi"))
buildConfigField("String", "SORAYOMI_TAG", quoteWrap(sorayomiRevisionTag))
buildConfigField("String", "GITHUB", quoteWrap("https://github.com/Suwayomi/Tachidesk-Server")) buildConfigField("String", "GITHUB", quoteWrap("https://github.com/Suwayomi/Tachidesk-Server"))
buildConfigField("String", "DISCORD", quoteWrap("https://discord.gg/DDZdqZWaHA")) buildConfigField("String", "DISCORD", quoteWrap("https://discord.gg/DDZdqZWaHA"))

View File

@@ -1,9 +0,0 @@
package eu.kanade.tachiyomi;
public class BuildConfig {
/** should be something like 74 */
public static final int VERSION_CODE = Integer.parseInt(suwayomi.tachidesk.server.BuildConfig.REVISION.substring(1));
/** should be something like "0.13.1" */
public static final String VERSION_NAME = suwayomi.tachidesk.server.BuildConfig.VERSION.substring(1);
}

View File

@@ -0,0 +1,220 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.kanade.tachiyomi.network.interceptor;
import com.microsoft.playwright.impl.driver.Driver;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.*;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/*
exact copy of https://github.com/microsoft/playwright-java/blob/4d278c391e3c50738ddea6c3e324a4bbbf719d86/driver-bundle/src/main/java/com/microsoft/playwright/impl/driver/jar/DriverJar.java
with diff:
108a109,116
>
> private FileSystem initFileSystem(URI uri) throws IOException {
> try {
> return FileSystems.newFileSystem(uri, Collections.emptyMap());
> } catch (FileSystemAlreadyExistsException e) {
> return null;
> }
> }
116c124
< try (FileSystem fileSystem = "jar".equals(uri.getScheme()) ? FileSystems.newFileSystem(uri, Collections.emptyMap()) : null) {
---
> try (FileSystem fileSystem = "jar".equals(uri.getScheme()) ? initFileSystem(uri) : null) {
*/
public class DriverJar extends Driver {
private static final String PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD";
private static final String SELENIUM_REMOTE_URL = "SELENIUM_REMOTE_URL";
static final String PLAYWRIGHT_NODEJS_PATH = "PLAYWRIGHT_NODEJS_PATH";
private final Path driverTempDir;
private Path preinstalledNodePath;
public DriverJar() throws IOException {
// Allow specifying custom path for the driver installation
// See https://github.com/microsoft/playwright-java/issues/728
String alternativeTmpdir = System.getProperty("playwright.driver.tmpdir");
String prefix = "playwright-java-";
driverTempDir = alternativeTmpdir == null
? Files.createTempDirectory(prefix)
: Files.createTempDirectory(Paths.get(alternativeTmpdir), prefix);
driverTempDir.toFile().deleteOnExit();
String nodePath = System.getProperty("playwright.nodejs.path");
if (nodePath != null) {
preinstalledNodePath = Paths.get(nodePath);
if (!Files.exists(preinstalledNodePath)) {
throw new RuntimeException("Invalid Node.js path specified: " + nodePath);
}
}
logMessage("created DriverJar: " + driverTempDir);
}
@Override
protected void initialize(Boolean installBrowsers) throws Exception {
if (preinstalledNodePath == null && env.containsKey(PLAYWRIGHT_NODEJS_PATH)) {
preinstalledNodePath = Paths.get(env.get(PLAYWRIGHT_NODEJS_PATH));
if (!Files.exists(preinstalledNodePath)) {
throw new RuntimeException("Invalid Node.js path specified: " + preinstalledNodePath);
}
} else if (preinstalledNodePath != null) {
// Pass the env variable to the driver process.
env.put(PLAYWRIGHT_NODEJS_PATH, preinstalledNodePath.toString());
}
extractDriverToTempDir();
logMessage("extracted driver from jar to " + driverPath());
if (installBrowsers)
installBrowsers(env);
}
private void installBrowsers(Map<String, String> env) throws IOException, InterruptedException {
String skip = env.get(PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD);
if (skip == null) {
skip = System.getenv(PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD);
}
if (skip != null && !"0".equals(skip) && !"false".equals(skip)) {
System.out.println("Skipping browsers download because `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD` env variable is set");
return;
}
if (env.get(SELENIUM_REMOTE_URL) != null || System.getenv(SELENIUM_REMOTE_URL) != null) {
logMessage("Skipping browsers download because `SELENIUM_REMOTE_URL` env variable is set");
return;
}
Path driver = driverPath();
if (!Files.exists(driver)) {
throw new RuntimeException("Failed to find driver: " + driver);
}
ProcessBuilder pb = createProcessBuilder();
pb.command().add("install");
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
Process p = pb.start();
boolean result = p.waitFor(10, TimeUnit.MINUTES);
if (!result) {
p.destroy();
throw new RuntimeException("Timed out waiting for browsers to install");
}
if (p.exitValue() != 0) {
throw new RuntimeException("Failed to install browsers, exit code: " + p.exitValue());
}
}
private static boolean isExecutable(Path filePath) {
String name = filePath.getFileName().toString();
return name.endsWith(".sh") || name.endsWith(".exe") || !name.contains(".");
}
private FileSystem initFileSystem(URI uri) throws IOException {
try {
return FileSystems.newFileSystem(uri, Collections.emptyMap());
} catch (FileSystemAlreadyExistsException e) {
return null;
}
}
void extractDriverToTempDir() throws URISyntaxException, IOException {
ClassLoader classloader = Thread.currentThread().getContextClassLoader();
URI originalUri = classloader.getResource(
"driver/" + platformDir()).toURI();
URI uri = maybeExtractNestedJar(originalUri);
// Create zip filesystem if loading from jar.
try (FileSystem fileSystem = "jar".equals(uri.getScheme()) ? initFileSystem(uri) : null) {
Path srcRoot = Paths.get(uri);
// jar file system's .relativize gives wrong results when used with
// spring-boot-maven-plugin, convert to the default filesystem to
// have predictable results.
// See https://github.com/microsoft/playwright-java/issues/306
Path srcRootDefaultFs = Paths.get(srcRoot.toString());
Files.walk(srcRoot).forEach(fromPath -> {
if (preinstalledNodePath != null) {
String fileName = fromPath.getFileName().toString();
if ("node.exe".equals(fileName) || "node".equals(fileName)) {
return;
}
}
Path relative = srcRootDefaultFs.relativize(Paths.get(fromPath.toString()));
Path toPath = driverTempDir.resolve(relative.toString());
try {
if (Files.isDirectory(fromPath)) {
Files.createDirectories(toPath);
} else {
Files.copy(fromPath, toPath);
if (isExecutable(toPath)) {
toPath.toFile().setExecutable(true, true);
}
}
toPath.toFile().deleteOnExit();
} catch (IOException e) {
throw new RuntimeException("Failed to extract driver from " + uri + ", full uri: " + originalUri, e);
}
});
}
}
private URI maybeExtractNestedJar(final URI uri) throws URISyntaxException {
if (!"jar".equals(uri.getScheme())) {
return uri;
}
final String JAR_URL_SEPARATOR = "!/";
String[] parts = uri.toString().split("!/");
if (parts.length != 3) {
return uri;
}
String innerJar = String.join(JAR_URL_SEPARATOR, parts[0], parts[1]);
URI jarUri = new URI(innerJar);
try (FileSystem fs = FileSystems.newFileSystem(jarUri, Collections.emptyMap())) {
Path fromPath = Paths.get(jarUri);
Path toPath = driverTempDir.resolve(fromPath.getFileName().toString());
Files.copy(fromPath, toPath);
toPath.toFile().deleteOnExit();
return new URI("jar:" + toPath.toUri() + JAR_URL_SEPARATOR + parts[2]);
} catch (IOException e) {
throw new RuntimeException("Failed to extract driver's nested .jar from " + jarUri + "; full uri: " + uri, e);
}
}
private static String platformDir() {
String name = System.getProperty("os.name").toLowerCase();
String arch = System.getProperty("os.arch").toLowerCase();
if (name.contains("windows")) {
return "win32_x64";
}
if (name.contains("linux")) {
if (arch.equals("aarch64")) {
return "linux-arm64";
} else {
return "linux";
}
}
if (name.contains("mac os x")) {
return "mac";
}
throw new RuntimeException("Unexpected os.name value: " + name);
}
@Override
protected Path driverDir() {
return driverTempDir;
}
}

View File

@@ -6,6 +6,9 @@ package eu.kanade.tachiyomi
* @since extension-lib 1.3 * @since extension-lib 1.3
*/ */
object AppInfo { object AppInfo {
fun getVersionCode() = BuildConfig.VERSION_CODE /** should be something like 74 */
fun getVersionName() = BuildConfig.VERSION_NAME fun getVersionCode() = suwayomi.tachidesk.server.BuildConfig.REVISION.substring(1).toInt()
/** should be something like "0.13.1" */
fun getVersionName() = suwayomi.tachidesk.server.BuildConfig.VERSION.substring(1)
} }

View File

@@ -29,7 +29,6 @@ import uy.kohesive.injekt.api.get
class AppModule(val app: Application) : InjektModule { class AppModule(val app: Application) : InjektModule {
override fun InjektRegistrar.registerInjectables() { override fun InjektRegistrar.registerInjectables() {
addSingleton(app) addSingleton(app)
// addSingletonFactory { PreferencesHelper(app) } // addSingletonFactory { PreferencesHelper(app) }

View File

@@ -39,6 +39,7 @@ class NetworkHelper(context: Context) {
.cookieJar(cookieManager) .cookieJar(cookieManager)
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS)
.callTimeout(2, TimeUnit.MINUTES)
.addInterceptor(UserAgentInterceptor()) .addInterceptor(UserAgentInterceptor())
if (serverConfig.debugLogsEnabled) { if (serverConfig.debugLogsEnabled) {

View File

@@ -62,7 +62,7 @@ suspend fun Call.await(): Response {
object : Callback { object : Callback {
override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
if (!response.isSuccessful) { if (!response.isSuccessful) {
continuation.resumeWithException(Exception("HTTP error ${response.code}")) continuation.resumeWithException(HttpException(response.code))
return return
} }
@@ -94,7 +94,7 @@ fun Call.asObservableSuccess(): Observable<Response> {
.doOnNext { response -> .doOnNext { response ->
if (!response.isSuccessful) { if (!response.isSuccessful) {
response.close() response.close()
throw Exception("HTTP error ${response.code}") throw HttpException(response.code)
} }
} }
} }
@@ -116,13 +116,13 @@ fun Call.asObservableSuccess(): Observable<Response> {
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call { fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
val progressClient = newBuilder() val progressClient = newBuilder()
// .cache(null) .cache(null)
// .addNetworkInterceptor { chain -> .addNetworkInterceptor { chain ->
// val originalResponse = chain.proceed(chain.request()) val originalResponse = chain.proceed(chain.request())
// originalResponse.newBuilder() originalResponse.newBuilder()
// .body(ProgressResponseBody(originalResponse.body!!, listener)) .body(ProgressResponseBody(originalResponse.body!!, listener))
// .build() .build()
// } }
.build() .build()
return progressClient.newCall(request) return progressClient.newCall(request)
@@ -136,3 +136,5 @@ inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(responseBody) return json.decodeFromString(responseBody)
} }
} }
class HttpException(val code: Int) : IllegalStateException("HTTP error $code")

View File

@@ -0,0 +1,44 @@
package eu.kanade.tachiyomi.network
import okhttp3.MediaType
import okhttp3.ResponseBody
import okio.Buffer
import okio.BufferedSource
import okio.ForwardingSource
import okio.Source
import okio.buffer
import java.io.IOException
class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
private val bufferedSource: BufferedSource by lazy {
source(responseBody.source()).buffer()
}
override fun contentType(): MediaType? {
return responseBody.contentType()
}
override fun contentLength(): Long {
return responseBody.contentLength()
}
override fun source(): BufferedSource {
return bufferedSource
}
private fun source(source: Source): Source {
return object : ForwardingSource(source) {
var totalBytesRead = 0L
@Throws(IOException::class)
override fun read(sink: Buffer, byteCount: Long): Long {
val bytesRead = super.read(sink, byteCount)
// read() returns the number of bytes read, or -1 if this source is exhausted.
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
return bytesRead
}
}
}
}

View File

@@ -1,19 +1,25 @@
package eu.kanade.tachiyomi.network.interceptor package eu.kanade.tachiyomi.network.interceptor
import com.gargoylesoftware.htmlunit.BrowserVersion import com.microsoft.playwright.Browser
import com.gargoylesoftware.htmlunit.WebClient import com.microsoft.playwright.BrowserType.LaunchOptions
import com.gargoylesoftware.htmlunit.html.HtmlPage import com.microsoft.playwright.Page
import com.microsoft.playwright.Playwright
import com.microsoft.playwright.PlaywrightException
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.interceptor.CFClearance.resolveWithWebView
import mu.KotlinLogging import mu.KotlinLogging
import okhttp3.Cookie import okhttp3.Cookie
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import suwayomi.tachidesk.server.ServerConfig
import suwayomi.tachidesk.server.serverConfig
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.io.IOException
import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit
// from TachiWeb-Server
class CloudflareInterceptor : Interceptor { class CloudflareInterceptor : Interceptor {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
@@ -25,20 +31,22 @@ class CloudflareInterceptor : Interceptor {
logger.trace { "CloudflareInterceptor is being used." } logger.trace { "CloudflareInterceptor is being used." }
val response = chain.proceed(originalRequest) val originalResponse = chain.proceed(chain.request())
// Check if Cloudflare anti-bot is on // Check if Cloudflare anti-bot is on
if (response.code != 503 || response.header("Server") !in SERVER_CHECK) { if (!(originalResponse.code in ERROR_CODES && originalResponse.header("Server") in SERVER_CHECK)) {
return response return originalResponse
} }
logger.debug { "Cloudflare anti-bot is on, CloudflareInterceptor is kicking in..." } logger.debug { "Cloudflare anti-bot is on, CloudflareInterceptor is kicking in..." }
return try { return try {
response.close() originalResponse.close()
network.cookies.remove(originalRequest.url.toUri()) network.cookies.remove(originalRequest.url.toUri())
chain.proceed(resolveChallenge(response)) val request = resolveWithWebView(originalRequest)
chain.proceed(request)
} catch (e: Exception) { } catch (e: Exception) {
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
// we don't crash the entire app // we don't crash the entire app
@@ -46,65 +54,174 @@ class CloudflareInterceptor : Interceptor {
} }
} }
private fun resolveChallenge(response: Response): Request { companion object {
val browserVersion = BrowserVersion.BrowserVersionBuilder(BrowserVersion.BEST_SUPPORTED) private val ERROR_CODES = listOf(403, 503)
.setUserAgent(response.request.header("User-Agent") ?: BrowserVersion.BEST_SUPPORTED.userAgent) private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
.build() private val COOKIE_NAMES = listOf("cf_clearance")
val convertedCookies = WebClient(browserVersion).use { webClient -> }
webClient.options.isThrowExceptionOnFailingStatusCode = false }
webClient.options.isThrowExceptionOnScriptError = false
webClient.getPage<HtmlPage>(response.request.url.toString()) /*
webClient.waitForBackgroundJavaScript(10000) * This class is ported from https://github.com/vvanglro/cf-clearance
// Challenge solved, process cookies * The original code is licensed under Apache 2.0
webClient.cookieManager.cookies.filter { */
// Only include Cloudflare cookies object CFClearance {
it.name.startsWith("__cf") || it.name.startsWith("cf_") private val logger = KotlinLogging.logger {}
}.map { private val network: NetworkHelper by injectLazy()
// Convert cookies -> OkHttp format
Cookie.Builder() init {
.domain(it.domain.removePrefix(".")) // Fix the default DriverJar issue by providing our own implementation
.expiresAt(it.expires?.time ?: Long.MAX_VALUE) // ref: https://github.com/microsoft/playwright-java/issues/1138
.name(it.name) System.setProperty("playwright.driver.impl", "eu.kanade.tachiyomi.network.interceptor.DriverJar")
.path(it.path) }
.value(it.value).apply {
if (it.isHttpOnly) httpOnly() fun resolveWithWebView(originalRequest: Request): Request {
if (it.isSecure) secure() val url = originalRequest.url.toString()
}.build()
logger.debug { "resolveWithWebView($url)" }
val cookies = Playwright.create().use { playwright ->
playwright.chromium().launch(
LaunchOptions()
.setHeadless(false)
.apply {
if (serverConfig.socksProxyEnabled) {
setProxy("socks5://${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}")
}
}
).use { browser ->
val userAgent = originalRequest.header("User-Agent")
if (userAgent != null) {
browser.newContext(Browser.NewContextOptions().setUserAgent(userAgent)).use { browserContext ->
browserContext.newPage().use { getCookies(it, url) }
}
} else {
browser.newPage().use { getCookies(it, url) }
}
} }
} }
// Copy cookies to cookie store // Copy cookies to cookie store
convertedCookies.forEach { cookies.groupBy { it.domain }.forEach { (domain, cookies) ->
network.cookies.addAll( network.cookies.addAll(
HttpUrl.Builder() url = HttpUrl.Builder()
.scheme("http") .scheme("http")
.host(it.domain) .host(domain)
.build(), .build(),
listOf(it) cookies = cookies
) )
} }
// Merge new and existing cookies for this request // Merge new and existing cookies for this request
// Find the cookies that we need to merge into this request // Find the cookies that we need to merge into this request
val convertedForThisRequest = convertedCookies.filter { val convertedForThisRequest = cookies.filter {
it.matches(response.request.url) it.matches(originalRequest.url)
} }
// Extract cookies from current request // Extract cookies from current request
val existingCookies = Cookie.parseAll( val existingCookies = Cookie.parseAll(
response.request.url, originalRequest.url,
response.request.headers originalRequest.headers
) )
// Filter out existing values of cookies that we are about to merge in // Filter out existing values of cookies that we are about to merge in
val filteredExisting = existingCookies.filter { existing -> val filteredExisting = existingCookies.filter { existing ->
convertedForThisRequest.none { converted -> converted.name == existing.name } convertedForThisRequest.none { converted -> converted.name == existing.name }
} }
logger.trace { "Existing cookies" }
logger.trace { existingCookies.joinToString("; ") }
val newCookies = filteredExisting + convertedForThisRequest val newCookies = filteredExisting + convertedForThisRequest
return response.request.newBuilder() logger.trace { "New cookies" }
.header("Cookie", newCookies.map { it.toString() }.joinToString("; ")) logger.trace { newCookies.joinToString("; ") }
return originalRequest.newBuilder()
.header("Cookie", newCookies.joinToString("; ") { "${it.name}=${it.value}" })
.build() .build()
} }
companion object { fun getWebViewUserAgent(): String {
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare") return try {
private val COOKIE_NAMES = listOf("cf_clearance") Playwright.create().use { playwright ->
playwright.chromium().launch(
LaunchOptions()
.setHeadless(true)
).use { browser ->
browser.newPage().use { page ->
val userAgent = page.evaluate("() => {return navigator.userAgent}") as String
logger.debug { "WebView User-Agent is $userAgent" }
return userAgent
}
}
}
} catch (e: PlaywrightException) {
// Playwright might fail on headless environments like docker
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"
}
} }
private fun getCookies(page: Page, url: String): List<Cookie> {
applyStealthInitScripts(page)
page.navigate(url)
val challengeResolved = waitForChallengeResolve(page)
return if (challengeResolved) {
val cookies = page.context().cookies()
logger.debug {
val userAgent = page.evaluate("() => {return navigator.userAgent}")
"Playwright User-Agent is $userAgent"
}
// Convert PlayWright cookies to OkHttp cookies
cookies.map {
Cookie.Builder()
.domain(it.domain.removePrefix("."))
.expiresAt(it.expires?.times(1000)?.toLong() ?: Long.MAX_VALUE)
.name(it.name)
.path(it.path)
.value(it.value).apply {
if (it.httpOnly) httpOnly()
if (it.secure) secure()
}.build()
}
} else {
logger.debug { "Cloudflare challenge failed to resolve" }
throw CloudflareBypassException()
}
}
// ref: https://github.com/vvanglro/cf-clearance/blob/44124a8f06d8d0ecf2bf558a027082ff88dab435/cf_clearance/stealth.py#L18
private val stealthInitScripts by lazy {
arrayOf(
ServerConfig::class.java.getResource("/cloudflare-js/canvas.fingerprinting.js")!!.readText(),
ServerConfig::class.java.getResource("/cloudflare-js/chrome.global.js")!!.readText(),
ServerConfig::class.java.getResource("/cloudflare-js/emulate.touch.js")!!.readText(),
ServerConfig::class.java.getResource("/cloudflare-js/navigator.permissions.js")!!.readText(),
ServerConfig::class.java.getResource("/cloudflare-js/navigator.webdriver.js")!!.readText(),
ServerConfig::class.java.getResource("/cloudflare-js/chrome.runtime.js")!!.readText(),
ServerConfig::class.java.getResource("/cloudflare-js/chrome.plugin.js")!!.readText()
)
}
// ref: https://github.com/vvanglro/cf-clearance/blob/44124a8f06d8d0ecf2bf558a027082ff88dab435/cf_clearance/stealth.py#L76
private fun applyStealthInitScripts(page: Page) {
for (script in stealthInitScripts) {
page.addInitScript(script)
}
}
// ref: https://github.com/vvanglro/cf-clearance/blob/44124a8f06d8d0ecf2bf558a027082ff88dab435/cf_clearance/retry.py#L21
private fun waitForChallengeResolve(page: Page): Boolean {
// sometimes the user has to solve the captcha challenge manually, potentially wait a long time
val timeoutSeconds = 120
repeat(timeoutSeconds) {
page.waitForTimeout(1.seconds.toDouble(DurationUnit.MILLISECONDS))
val success = try {
page.querySelector("#challenge-form") == null
} catch (e: Exception) {
logger.debug(e) { "query Error" }
false
}
if (success) return true
}
return false
}
private class CloudflareBypassException : Exception()
} }

View File

@@ -23,13 +23,13 @@ import java.util.concurrent.TimeUnit
fun OkHttpClient.Builder.rateLimit( fun OkHttpClient.Builder.rateLimit(
permits: Int, permits: Int,
period: Long = 1, period: Long = 1,
unit: TimeUnit = TimeUnit.SECONDS, unit: TimeUnit = TimeUnit.SECONDS
) = addInterceptor(RateLimitInterceptor(permits, period, unit)) ) = addInterceptor(RateLimitInterceptor(permits, period, unit))
private class RateLimitInterceptor( private class RateLimitInterceptor(
private val permits: Int, private val permits: Int,
period: Long, period: Long,
unit: TimeUnit, unit: TimeUnit
) : Interceptor { ) : Interceptor {
private val requestQueue = ArrayList<Long>(permits) private val requestQueue = ArrayList<Long>(permits)

View File

@@ -26,14 +26,14 @@ fun OkHttpClient.Builder.rateLimitHost(
httpUrl: HttpUrl, httpUrl: HttpUrl,
permits: Int, permits: Int,
period: Long = 1, period: Long = 1,
unit: TimeUnit = TimeUnit.SECONDS, unit: TimeUnit = TimeUnit.SECONDS
) = addInterceptor(SpecificHostRateLimitInterceptor(httpUrl, permits, period, unit)) ) = addInterceptor(SpecificHostRateLimitInterceptor(httpUrl, permits, period, unit))
class SpecificHostRateLimitInterceptor( class SpecificHostRateLimitInterceptor(
httpUrl: HttpUrl, httpUrl: HttpUrl,
private val permits: Int, private val permits: Int,
period: Long, period: Long,
unit: TimeUnit, unit: TimeUnit
) : Interceptor { ) : Interceptor {
private val requestQueue = ArrayList<Long>(permits) private val requestQueue = ArrayList<Long>(permits)

View File

@@ -327,8 +327,9 @@ class LocalSource : CatalogueSource {
fun getFormat(chapter: SChapter): Format { fun getFormat(chapter: SChapter): Format {
val chapFile = File(applicationDirs.localMangaRoot, chapter.url) val chapFile = File(applicationDirs.localMangaRoot, chapter.url)
if (chapFile.exists()) if (chapFile.exists()) {
return getFormat(chapFile) return getFormat(chapFile)
}
throw Exception("Chapter not found") throw Exception("Chapter not found")
} }

View File

@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.source.local.loader package eu.kanade.tachiyomi.source.local.loader
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.storage.EpubFile import eu.kanade.tachiyomi.util.storage.EpubFile
import java.io.File import java.io.File
@@ -24,7 +23,6 @@ class EpubPageLoader(file: File) : PageLoader {
val streamFn = { epub.getInputStream(epub.getEntry(path)!!) } val streamFn = { epub.getInputStream(epub.getEntry(path)!!) }
ReaderPage(i).apply { ReaderPage(i).apply {
stream = streamFn stream = streamFn
status = Page.READY
} }
} }
} }

View File

@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.source.local.loader
import com.github.junrar.Archive import com.github.junrar.Archive
import com.github.junrar.rarfile.FileHeader import com.github.junrar.rarfile.FileHeader
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
@@ -46,7 +45,6 @@ class RarPageLoader(file: File) : PageLoader {
ReaderPage(i).apply { ReaderPage(i).apply {
stream = streamFn stream = streamFn
status = Page.READY
} }
} }
} }
@@ -58,7 +56,6 @@ class RarPageLoader(file: File) : PageLoader {
ReaderPage(i).apply { ReaderPage(i).apply {
stream = streamFn stream = streamFn
status = Page.READY
} }
} }
} }

View File

@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.source.local.loader package eu.kanade.tachiyomi.source.local.loader
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
import java.io.File import java.io.File
@@ -24,7 +23,6 @@ class ZipPageLoader(file: File) : PageLoader {
val streamFn = { zip.getInputStream(entry) } val streamFn = { zip.getInputStream(entry) }
ReaderPage(i).apply { ReaderPage(i).apply {
stream = streamFn stream = streamFn
status = Page.READY
} }
} }
} }

View File

@@ -2,7 +2,8 @@ package eu.kanade.tachiyomi.source.model
import android.net.Uri import android.net.Uri
import eu.kanade.tachiyomi.network.ProgressListener import eu.kanade.tachiyomi.network.ProgressListener
import rx.subjects.Subject import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
open class Page( open class Page(
val index: Int, val index: Int,
@@ -11,48 +12,17 @@ open class Page(
@Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions @Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions
) : ProgressListener { ) : ProgressListener {
val number: Int private val _progress = MutableStateFlow(0)
get() = index + 1 val progress = _progress.asStateFlow()
@Transient
@Volatile
var status: Int = 0
set(value) {
field = value
statusSubject?.onNext(value)
statusCallback?.invoke(this)
}
@Transient
@Volatile
var progress: Int = 0
set(value) {
field = value
statusCallback?.invoke(this)
}
@Transient
private var statusSubject: Subject<Int, Int>? = null
@Transient
private var statusCallback: ((Page) -> Unit)? = null
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
progress = if (contentLength > 0) { _progress.value = if (contentLength > 0) {
(100 * bytesRead / contentLength).toInt() (100 * bytesRead / contentLength).toInt()
} else { } else {
-1 -1
} }
} }
fun setStatusSubject(subject: Subject<Int, Int>?) {
this.statusSubject = subject
}
fun setStatusCallback(f: ((Page) -> Unit)?) {
statusCallback = f
}
companion object { companion object {
const val QUEUE = 0 const val QUEUE = 0
const val LOAD_PAGE = 1 const val LOAD_PAGE = 1

View File

@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.source.online
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.CFClearance.getWebViewUserAgent
import eu.kanade.tachiyomi.network.newCallWithProgress import eu.kanade.tachiyomi.network.newCallWithProgress
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
@@ -372,6 +373,6 @@ abstract class HttpSource : CatalogueSource {
override fun getFilterList() = FilterList() override fun getFilterList() = FilterList()
companion object { companion object {
const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63" val DEFAULT_USER_AGENT by lazy { getWebViewUserAgent() }
} }
} }

View File

@@ -4,9 +4,7 @@ import eu.kanade.tachiyomi.source.model.Page
import rx.Observable import rx.Observable
fun HttpSource.getImageUrl(page: Page): Observable<Page> { fun HttpSource.getImageUrl(page: Page): Observable<Page> {
page.status = Page.LOAD_PAGE
return fetchImageUrl(page) return fetchImageUrl(page)
.doOnError { page.status = Page.ERROR }
.onErrorReturn { null } .onErrorReturn { null }
.doOnNext { page.imageUrl = it } .doOnNext { page.imageUrl = it }
.map { page } .map { page }

View File

@@ -139,7 +139,7 @@ class EpubFile(file: File) : Closeable {
*/ */
private fun getPagesFromDocument(document: Document): List<String> { private fun getPagesFromDocument(document: Document): List<String> {
val pages = document.select("manifest > item") val pages = document.select("manifest > item")
.filter { "application/xhtml+xml" == it.attr("media-type") } .filter { element -> "application/xhtml+xml" == element.attr("media-type") }
.associateBy { it.attr("id") } .associateBy { it.attr("id") }
val spine = document.select("spine > itemref").map { it.attr("idref") } val spine = document.select("spine > itemref").map { it.attr("idref") }

View File

@@ -8,11 +8,17 @@ package suwayomi.tachidesk.global
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import io.javalin.apibuilder.ApiBuilder.get import io.javalin.apibuilder.ApiBuilder.get
import io.javalin.apibuilder.ApiBuilder.patch
import io.javalin.apibuilder.ApiBuilder.path import io.javalin.apibuilder.ApiBuilder.path
import suwayomi.tachidesk.global.controller.GlobalMetaController
import suwayomi.tachidesk.global.controller.SettingsController import suwayomi.tachidesk.global.controller.SettingsController
object GlobalAPI { object GlobalAPI {
fun defineEndpoints() { fun defineEndpoints() {
path("meta") {
get("", GlobalMetaController.getMeta)
patch("", GlobalMetaController.modifyMeta)
}
path("settings") { path("settings") {
get("about", SettingsController.about) get("about", SettingsController.about)
get("check-update", SettingsController.checkUpdate) get("check-update", SettingsController.checkUpdate)

View File

@@ -0,0 +1,53 @@
package suwayomi.tachidesk.global.controller
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import io.javalin.http.HttpCode
import suwayomi.tachidesk.global.impl.GlobalMeta
import suwayomi.tachidesk.server.util.formParam
import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.withOperation
object GlobalMetaController {
/** used to modify a category's meta parameters */
val getMeta = handler(
documentWith = {
withOperation {
summary("Server level meta mapping")
description("Get a list of globally stored key-value mapping, you can set values for whatever you want inside it.")
}
},
behaviorOf = { ctx ->
ctx.json(GlobalMeta.getMetaMap())
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
}
)
/** used to modify global meta parameters */
val modifyMeta = handler(
formParam<String>("key"),
formParam<String>("value"),
documentWith = {
withOperation {
summary("Add meta data to the global meta mapping")
description("A simple Key-Value stored at server global level, you can set values for whatever you want inside it.")
}
},
behaviorOf = { ctx, key, value ->
GlobalMeta.modifyMeta(key, value)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
}

View File

@@ -43,12 +43,12 @@ object SettingsController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.json( ctx.future(
future { AppUpdate.checkUpdate() } future { AppUpdate.checkUpdate() }
) )
}, },
withResults = { withResults = {
json<UpdateDataClass>(HttpCode.OK) json<Array<UpdateDataClass>>(HttpCode.OK)
} }
) )
} }

View File

@@ -16,7 +16,7 @@ data class AboutDataClass(
val buildType: String, val buildType: String,
val buildTime: Long, val buildTime: Long,
val github: String, val github: String,
val discord: String, val discord: String
) )
object About { object About {
@@ -28,7 +28,7 @@ object About {
BuildConfig.BUILD_TYPE, BuildConfig.BUILD_TYPE,
BuildConfig.BUILD_TIME, BuildConfig.BUILD_TIME,
BuildConfig.GITHUB, BuildConfig.GITHUB,
BuildConfig.DISCORD, BuildConfig.DISCORD
) )
} }
} }

View File

@@ -46,13 +46,13 @@ object AppUpdate {
UpdateDataClass( UpdateDataClass(
"Stable", "Stable",
stableJson["tag_name"]!!.jsonPrimitive.content, stableJson["tag_name"]!!.jsonPrimitive.content,
stableJson["html_url"]!!.jsonPrimitive.content, stableJson["html_url"]!!.jsonPrimitive.content
), ),
UpdateDataClass( UpdateDataClass(
"Preview", "Preview",
previewJson["tag_name"]!!.jsonPrimitive.content, previewJson["tag_name"]!!.jsonPrimitive.content,
previewJson["html_url"]!!.jsonPrimitive.content, previewJson["html_url"]!!.jsonPrimitive.content
), )
) )
} }
} }

View File

@@ -0,0 +1,43 @@
package suwayomi.tachidesk.global.impl
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.global.model.table.GlobalMetaTable
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
object GlobalMeta {
fun modifyMeta(key: String, value: String) {
transaction {
val meta = transaction {
GlobalMetaTable.select { GlobalMetaTable.key eq key }
}.firstOrNull()
if (meta == null) {
GlobalMetaTable.insert {
it[GlobalMetaTable.key] = key
it[GlobalMetaTable.value] = value
}
} else {
GlobalMetaTable.update({ GlobalMetaTable.key eq key }) {
it[GlobalMetaTable.value] = value
}
}
}
}
fun getMetaMap(): Map<String, String> {
return transaction {
GlobalMetaTable.selectAll()
.associate { it[GlobalMetaTable.key] to it[GlobalMetaTable.value] }
}
}
}

View File

@@ -0,0 +1,18 @@
package suwayomi.tachidesk.global.model.table
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import org.jetbrains.exposed.dao.id.IntIdTable
/**
* Metadata storage for clients, server/global level.
*/
object GlobalMetaTable : IntIdTable() {
val key = varchar("key", 256)
val value = varchar("value", 4096)
}

View File

@@ -12,6 +12,7 @@ import io.javalin.apibuilder.ApiBuilder.get
import io.javalin.apibuilder.ApiBuilder.patch import io.javalin.apibuilder.ApiBuilder.patch
import io.javalin.apibuilder.ApiBuilder.path import io.javalin.apibuilder.ApiBuilder.path
import io.javalin.apibuilder.ApiBuilder.post import io.javalin.apibuilder.ApiBuilder.post
import io.javalin.apibuilder.ApiBuilder.put
import io.javalin.apibuilder.ApiBuilder.ws import io.javalin.apibuilder.ApiBuilder.ws
import suwayomi.tachidesk.manga.controller.BackupController import suwayomi.tachidesk.manga.controller.BackupController
import suwayomi.tachidesk.manga.controller.CategoryController import suwayomi.tachidesk.manga.controller.CategoryController
@@ -48,11 +49,13 @@ object MangaAPI {
post("{sourceId}/filters", SourceController.setFilters) post("{sourceId}/filters", SourceController.setFilters)
get("{sourceId}/search", SourceController.searchSingle) get("{sourceId}/search", SourceController.searchSingle)
post("{sourceId}/quick-search", SourceController.quickSearchSingle)
// get("all/search", SourceController.searchGlobal) // TODO // get("all/search", SourceController.searchGlobal) // TODO
} }
path("manga") { path("manga") {
get("{mangaId}", MangaController.retrieve) get("{mangaId}", MangaController.retrieve)
get("{mangaId}/full", MangaController.retrieveFull)
get("{mangaId}/thumbnail", MangaController.thumbnail) get("{mangaId}/thumbnail", MangaController.thumbnail)
get("{mangaId}/category", MangaController.categoryList) get("{mangaId}/category", MangaController.categoryList)
@@ -65,8 +68,10 @@ object MangaAPI {
patch("{mangaId}/meta", MangaController.meta) patch("{mangaId}/meta", MangaController.meta)
get("{mangaId}/chapters", MangaController.chapterList) get("{mangaId}/chapters", MangaController.chapterList)
post("{mangaId}/chapter/batch", MangaController.chapterBatch)
get("{mangaId}/chapter/{chapterIndex}", MangaController.chapterRetrieve) get("{mangaId}/chapter/{chapterIndex}", MangaController.chapterRetrieve)
patch("{mangaId}/chapter/{chapterIndex}", MangaController.chapterModify) patch("{mangaId}/chapter/{chapterIndex}", MangaController.chapterModify)
put("{mangaId}/chapter/{chapterIndex}", MangaController.chapterModify)
delete("{mangaId}/chapter/{chapterIndex}", MangaController.chapterDelete) delete("{mangaId}/chapter/{chapterIndex}", MangaController.chapterDelete)
patch("{mangaId}/chapter/{chapterIndex}/meta", MangaController.chapterMeta) patch("{mangaId}/chapter/{chapterIndex}/meta", MangaController.chapterMeta)
@@ -74,6 +79,10 @@ object MangaAPI {
get("{mangaId}/chapter/{chapterIndex}/page/{index}", MangaController.pageRetrieve) get("{mangaId}/chapter/{chapterIndex}/page/{index}", MangaController.pageRetrieve)
} }
path("chapter") {
post("batch", MangaController.anyChapterBatch)
}
path("category") { path("category") {
get("", CategoryController.categoryList) get("", CategoryController.categoryList)
post("", CategoryController.categoryCreate) post("", CategoryController.categoryCreate)
@@ -85,6 +94,8 @@ object MangaAPI {
get("{categoryId}", CategoryController.categoryMangas) get("{categoryId}", CategoryController.categoryMangas)
patch("{categoryId}", CategoryController.categoryModify) patch("{categoryId}", CategoryController.categoryModify)
delete("{categoryId}", CategoryController.categoryDelete) delete("{categoryId}", CategoryController.categoryDelete)
patch("{categoryId}/meta", CategoryController.meta)
} }
path("backup") { path("backup") {
@@ -103,12 +114,15 @@ object MangaAPI {
get("start", DownloadController.start) get("start", DownloadController.start)
get("stop", DownloadController.stop) get("stop", DownloadController.stop)
get("clear", DownloadController.stop) get("clear", DownloadController.clear)
} }
path("download") { path("download") {
get("{mangaId}/chapter/{chapterIndex}", DownloadController.queueChapter) get("{mangaId}/chapter/{chapterIndex}", DownloadController.queueChapter)
delete("{mangaId}/chapter/{chapterIndex}", DownloadController.unqueueChapter) delete("{mangaId}/chapter/{chapterIndex}", DownloadController.unqueueChapter)
patch("{mangaId}/chapter/{chapterIndex}/reorder/{to}", DownloadController.reorderChapter)
post("batch", DownloadController.queueChapters)
delete("batch", DownloadController.unqueueChapters)
} }
path("update") { path("update") {

View File

@@ -1,7 +1,6 @@
package suwayomi.tachidesk.manga.controller package suwayomi.tachidesk.manga.controller
import io.javalin.http.HttpCode import io.javalin.http.HttpCode
import suwayomi.tachidesk.manga.impl.backup.AbstractBackupValidator
import suwayomi.tachidesk.manga.impl.backup.BackupFlags import suwayomi.tachidesk.manga.impl.backup.BackupFlags
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport
@@ -85,14 +84,14 @@ object BackupController {
includeCategories = true, includeCategories = true,
includeChapters = true, includeChapters = true,
includeTracking = true, includeTracking = true,
includeHistory = true, includeHistory = true
) )
) )
} }
) )
}, },
withResults = { withResults = {
mime(HttpCode.OK, "application/octet-stream") stream(HttpCode.OK)
} }
) )
@@ -117,14 +116,14 @@ object BackupController {
includeCategories = true, includeCategories = true,
includeChapters = true, includeChapters = true,
includeTracking = true, includeTracking = true,
includeHistory = true, includeHistory = true
) )
) )
} }
) )
}, },
withResults = { withResults = {
mime(HttpCode.OK, "application/octet-stream") stream(HttpCode.OK)
} }
) )
@@ -135,8 +134,6 @@ object BackupController {
summary("Validate a backup") summary("Validate a backup")
description("Reports missing sources and trackers, expects a Tachiyomi protobuf backup in the body") description("Reports missing sources and trackers, expects a Tachiyomi protobuf backup in the body")
} }
body<ByteArray>("") {
}
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.future( ctx.future(
@@ -146,7 +143,7 @@ object BackupController {
) )
}, },
withResults = { withResults = {
json<AbstractBackupValidator.ValidationResult>(HttpCode.OK) json<ProtoBackupValidator.ValidationResult>(HttpCode.OK)
} }
) )
@@ -170,7 +167,7 @@ object BackupController {
) )
}, },
withResults = { withResults = {
json<AbstractBackupValidator.ValidationResult>(HttpCode.OK) json<ProtoBackupValidator.ValidationResult>(HttpCode.OK)
} }
) )
} }

View File

@@ -30,7 +30,7 @@ object CategoryController {
ctx.json(Category.getCategoryList()) ctx.json(Category.getCategoryList())
}, },
withResults = { withResults = {
json<List<CategoryDataClass>>(HttpCode.OK) json<Array<CategoryDataClass>>(HttpCode.OK)
} }
) )
@@ -107,7 +107,7 @@ object CategoryController {
ctx.json(CategoryManga.getCategoryMangaList(categoryId)) ctx.json(CategoryManga.getCategoryMangaList(categoryId))
}, },
withResults = { withResults = {
json<List<MangaDataClass>>(HttpCode.OK) json<Array<MangaDataClass>>(HttpCode.OK)
} }
) )
@@ -129,4 +129,25 @@ object CategoryController {
httpCode(HttpCode.OK) httpCode(HttpCode.OK)
} }
) )
/** used to modify a category's meta parameters */
val meta = handler(
pathParam<Int>("categoryId"),
formParam<String>("key"),
formParam<String>("value"),
documentWith = {
withOperation {
summary("Add meta data to category")
description("A simple Key-Value storage in the manga object, you can set values for whatever you want inside it.")
}
},
behaviorOf = { ctx, categoryId, key, value ->
Category.modifyMeta(categoryId, key, value)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
} }

View File

@@ -9,13 +9,21 @@ package suwayomi.tachidesk.manga.controller
import io.javalin.http.HttpCode import io.javalin.http.HttpCode
import io.javalin.websocket.WsConfig import io.javalin.websocket.WsConfig
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import org.kodein.di.DI
import org.kodein.di.conf.global
import org.kodein.di.instance
import suwayomi.tachidesk.manga.impl.download.DownloadManager import suwayomi.tachidesk.manga.impl.download.DownloadManager
import suwayomi.tachidesk.manga.impl.download.DownloadManager.EnqueueInput
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.pathParam import suwayomi.tachidesk.server.util.pathParam
import suwayomi.tachidesk.server.util.withOperation import suwayomi.tachidesk.server.util.withOperation
object DownloadController { object DownloadController {
private val json by DI.global.instance<Json>()
/** Download queue stats */ /** Download queue stats */
fun downloadsWS(ws: WsConfig) { fun downloadsWS(ws: WsConfig) {
ws.onConnect { ctx -> ws.onConnect { ctx ->
@@ -38,10 +46,8 @@ object DownloadController {
description("Start the downloader") description("Start the downloader")
} }
}, },
behaviorOf = { ctx -> behaviorOf = {
DownloadManager.start() DownloadManager.start()
ctx.status(200)
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpCode.OK)
@@ -57,9 +63,9 @@ object DownloadController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
DownloadManager.stop() ctx.future(
future { DownloadManager.stop() }
ctx.status(200) )
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpCode.OK)
@@ -75,29 +81,29 @@ object DownloadController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
DownloadManager.clear() ctx.future(
future { DownloadManager.clear() }
ctx.status(200) )
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpCode.OK)
} }
) )
/** Queue chapter for download */ /** Queue single chapter for download */
val queueChapter = handler( val queueChapter = handler(
pathParam<Int>("chapterIndex"), pathParam<Int>("chapterIndex"),
pathParam<Int>("mangaId"), pathParam<Int>("mangaId"),
documentWith = { documentWith = {
withOperation { withOperation {
summary("Downloader add chapter") summary("Downloader add single chapter")
description("Queue chapter for download") description("Queue single chapter for download")
} }
}, },
behaviorOf = { ctx, chapterIndex, mangaId -> behaviorOf = { ctx, chapterIndex, mangaId ->
ctx.future( ctx.future(
future { future {
DownloadManager.enqueue(chapterIndex, mangaId) DownloadManager.enqueueWithChapterIndex(mangaId, chapterIndex)
} }
) )
}, },
@@ -107,6 +113,49 @@ object DownloadController {
} }
) )
val queueChapters = handler(
documentWith = {
withOperation {
summary("Downloader add multiple chapters")
description("Queue multiple chapters for download")
}
body<EnqueueInput>()
},
behaviorOf = { ctx ->
val inputs = json.decodeFromString<EnqueueInput>(ctx.body())
ctx.future(
future {
DownloadManager.enqueue(inputs)
}
)
},
withResults = {
httpCode(HttpCode.OK)
}
)
/** delete multiple chapters from download queue */
val unqueueChapters = handler(
documentWith = {
withOperation {
summary("Downloader remove multiple downloads")
description("Remove multiple chapters downloads from queue")
}
body<EnqueueInput>()
},
behaviorOf = { ctx ->
val input = json.decodeFromString<EnqueueInput>(ctx.body())
ctx.future(
future {
DownloadManager.unqueue(input)
}
)
},
withResults = {
httpCode(HttpCode.OK)
}
)
/** delete chapter from download queue */ /** delete chapter from download queue */
val unqueueChapter = handler( val unqueueChapter = handler(
pathParam<Int>("chapterIndex"), pathParam<Int>("chapterIndex"),
@@ -126,4 +175,23 @@ object DownloadController {
httpCode(HttpCode.OK) httpCode(HttpCode.OK)
} }
) )
/** clear download queue */
val reorderChapter = handler(
pathParam<Int>("chapterIndex"),
pathParam<Int>("mangaId"),
pathParam<Int>("to"),
documentWith = {
withOperation {
summary("Downloader reorder chapter")
description("Reorder chapter in download queue")
}
},
behaviorOf = { _, chapterIndex, mangaId, to ->
DownloadManager.reorder(chapterIndex, mangaId, to)
},
withResults = {
httpCode(HttpCode.OK)
}
)
} }

View File

@@ -37,7 +37,7 @@ object ExtensionController {
) )
}, },
withResults = { withResults = {
json<List<ExtensionDataClass>>(HttpCode.OK) json<Array<ExtensionDataClass>>(HttpCode.OK)
} }
) )
@@ -158,7 +158,7 @@ object ExtensionController {
) )
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) image(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpCode.NOT_FOUND)
} }
) )

View File

@@ -8,6 +8,11 @@ package suwayomi.tachidesk.manga.controller
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import io.javalin.http.HttpCode import io.javalin.http.HttpCode
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import org.kodein.di.DI
import org.kodein.di.conf.global
import org.kodein.di.instance
import suwayomi.tachidesk.manga.impl.CategoryManga import suwayomi.tachidesk.manga.impl.CategoryManga
import suwayomi.tachidesk.manga.impl.Chapter import suwayomi.tachidesk.manga.impl.Chapter
import suwayomi.tachidesk.manga.impl.Library import suwayomi.tachidesk.manga.impl.Library
@@ -23,15 +28,17 @@ import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.pathParam import suwayomi.tachidesk.server.util.pathParam
import suwayomi.tachidesk.server.util.queryParam import suwayomi.tachidesk.server.util.queryParam
import suwayomi.tachidesk.server.util.withOperation import suwayomi.tachidesk.server.util.withOperation
import kotlin.time.Duration.Companion.days
object MangaController { object MangaController {
/** get manga info */ private val json by DI.global.instance<Json>()
val retrieve = handler( val retrieve = handler(
pathParam<Int>("mangaId"), pathParam<Int>("mangaId"),
queryParam("onlineFetch", false), queryParam("onlineFetch", false),
documentWith = { documentWith = {
withOperation { withOperation {
summary("Get a manga") summary("Get manga info")
description("Get a manga from the database using a specific id.") description("Get a manga from the database using a specific id.")
} }
}, },
@@ -48,6 +55,29 @@ object MangaController {
} }
) )
/** get manga info with all data filled in */
val retrieveFull = handler(
pathParam<Int>("mangaId"),
queryParam("onlineFetch", false),
documentWith = {
withOperation {
summary("Get manga info with all data filled in")
description("Get a manga from the database using a specific id.")
}
},
behaviorOf = { ctx, mangaId, onlineFetch ->
ctx.future(
future {
Manga.getMangaFull(mangaId, onlineFetch)
}
)
},
withResults = {
json<MangaDataClass>(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
/** manga thumbnail */ /** manga thumbnail */
val thumbnail = handler( val thumbnail = handler(
pathParam<Int>("mangaId"), pathParam<Int>("mangaId"),
@@ -63,14 +93,14 @@ object MangaController {
future { Manga.getMangaThumbnail(mangaId, useCache) } future { Manga.getMangaThumbnail(mangaId, useCache) }
.thenApply { .thenApply {
ctx.header("content-type", it.second) ctx.header("content-type", it.second)
val httpCacheSeconds = 60 * 60 * 24 val httpCacheSeconds = 1.days.inWholeSeconds
ctx.header("cache-control", "max-age=$httpCacheSeconds") ctx.header("cache-control", "max-age=$httpCacheSeconds")
it.first it.first
} }
) )
}, },
withResults = { withResults = {
mime(HttpCode.OK, "image/*") image(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpCode.NOT_FOUND)
} }
) )
@@ -128,7 +158,7 @@ object MangaController {
ctx.json(CategoryManga.getMangaCategories(mangaId)) ctx.json(CategoryManga.getMangaCategories(mangaId))
}, },
withResults = { withResults = {
json<List<CategoryDataClass>>(HttpCode.OK) json<Array<CategoryDataClass>>(HttpCode.OK)
} }
) )
@@ -205,11 +235,54 @@ object MangaController {
ctx.future(future { Chapter.getChapterList(mangaId, onlineFetch) }) ctx.future(future { Chapter.getChapterList(mangaId, onlineFetch) })
}, },
withResults = { withResults = {
json<List<ChapterDataClass>>(HttpCode.OK) json<Array<ChapterDataClass>>(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpCode.NOT_FOUND)
} }
) )
/** batch edit chapters of single manga */
val chapterBatch = handler(
pathParam<Int>("mangaId"),
documentWith = {
withOperation {
summary("Chapters update multiple")
description("Update multiple chapters of single manga. For batch marking as read, or bookmarking")
}
body<Chapter.MangaChapterBatchEditInput>()
},
behaviorOf = { ctx, mangaId ->
val input = json.decodeFromString<Chapter.MangaChapterBatchEditInput>(ctx.body())
Chapter.modifyChapters(input, mangaId)
},
withResults = {
httpCode(HttpCode.OK)
}
)
/** batch edit chapters from multiple manga */
val anyChapterBatch = handler(
documentWith = {
withOperation {
summary("Chapters update multiple")
description("Update multiple chapters on any manga. For batch marking as read, or bookmarking")
}
body<Chapter.ChapterBatchEditInput>()
},
behaviorOf = { ctx ->
val input = json.decodeFromString<Chapter.ChapterBatchEditInput>(ctx.body())
Chapter.modifyChapters(
Chapter.MangaChapterBatchEditInput(
input.chapterIds,
null,
input.change
)
)
},
withResults = {
httpCode(HttpCode.OK)
}
)
/** used to display a chapter, get a chapter in order to show its pages */ /** used to display a chapter, get a chapter in order to show its pages */
val chapterRetrieve = handler( val chapterRetrieve = handler(
pathParam<Int>("mangaId"), pathParam<Int>("mangaId"),
@@ -314,12 +387,14 @@ object MangaController {
future { Page.getPageImage(mangaId, chapterIndex, index, useCache) } future { Page.getPageImage(mangaId, chapterIndex, index, useCache) }
.thenApply { .thenApply {
ctx.header("content-type", it.second) ctx.header("content-type", it.second)
val httpCacheSeconds = 1.days.inWholeSeconds
ctx.header("cache-control", "max-age=$httpCacheSeconds")
it.first it.first
} }
) )
}, },
withResults = { withResults = {
mime(HttpCode.OK, "image/*") image(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpCode.NOT_FOUND)
} }
) )

View File

@@ -16,15 +16,16 @@ import org.kodein.di.instance
import suwayomi.tachidesk.manga.impl.MangaList import suwayomi.tachidesk.manga.impl.MangaList
import suwayomi.tachidesk.manga.impl.Search import suwayomi.tachidesk.manga.impl.Search
import suwayomi.tachidesk.manga.impl.Search.FilterChange import suwayomi.tachidesk.manga.impl.Search.FilterChange
import suwayomi.tachidesk.manga.impl.Search.FilterData
import suwayomi.tachidesk.manga.impl.Source import suwayomi.tachidesk.manga.impl.Source
import suwayomi.tachidesk.manga.impl.Source.SourcePreferenceChange import suwayomi.tachidesk.manga.impl.Source.SourcePreferenceChange
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.pathParam import suwayomi.tachidesk.server.util.pathParam
import suwayomi.tachidesk.server.util.queryParam import suwayomi.tachidesk.server.util.queryParam
import suwayomi.tachidesk.server.util.withOperation import suwayomi.tachidesk.server.util.withOperation
import javax.sound.sampled.SourceDataLine
object SourceController { object SourceController {
/** list of sources */ /** list of sources */
@@ -39,7 +40,7 @@ object SourceController {
ctx.json(Source.getSourceList()) ctx.json(Source.getSourceList())
}, },
withResults = { withResults = {
json<List<SourceDataLine>>(HttpCode.OK) json<Array<SourceDataClass>>(HttpCode.OK)
} }
) )
@@ -56,7 +57,7 @@ object SourceController {
ctx.json(Source.getSource(sourceId)!!) ctx.json(Source.getSource(sourceId)!!)
}, },
withResults = { withResults = {
json<SourceDataLine>(HttpCode.OK) json<SourceDataClass>(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpCode.NOT_FOUND)
} }
) )
@@ -118,7 +119,7 @@ object SourceController {
ctx.json(Source.getSourcePreferences(sourceId)) ctx.json(Source.getSourcePreferences(sourceId))
}, },
withResults = { withResults = {
json<List<Source.PreferenceObject>>(HttpCode.OK) json<Array<Source.PreferenceObject>>(HttpCode.OK)
} }
) )
@@ -130,6 +131,7 @@ object SourceController {
summary("Source preference set") summary("Source preference set")
description("Set one preference of source with id `sourceId`") description("Set one preference of source with id `sourceId`")
} }
body<SourcePreferenceChange>()
}, },
behaviorOf = { ctx, sourceId -> behaviorOf = { ctx, sourceId ->
val preferenceChange = ctx.bodyAsClass(SourcePreferenceChange::class.java) val preferenceChange = ctx.bodyAsClass(SourcePreferenceChange::class.java)
@@ -154,7 +156,7 @@ object SourceController {
ctx.json(Search.getFilterList(sourceId, reset)) ctx.json(Search.getFilterList(sourceId, reset))
}, },
withResults = { withResults = {
json<List<Search.FilterObject>>(HttpCode.OK) json<Array<Search.FilterObject>>(HttpCode.OK)
} }
) )
@@ -168,6 +170,8 @@ object SourceController {
summary("Source filters set") summary("Source filters set")
description("Change filters of source with id `sourceId`") description("Change filters of source with id `sourceId`")
} }
body<FilterChange>()
body<Array<FilterChange>>()
}, },
behaviorOf = { ctx, sourceId -> behaviorOf = { ctx, sourceId ->
val filterChange = try { val filterChange = try {
@@ -202,6 +206,26 @@ object SourceController {
} }
) )
/** quick search single source filter */
val quickSearchSingle = handler(
pathParam<Long>("sourceId"),
queryParam("pageNum", 1),
documentWith = {
withOperation {
summary("Source manga quick search")
description("Returns list of manga from source matching posted searchTerm and filter")
}
body<FilterData>()
},
behaviorOf = { ctx, sourceId, pageNum ->
val filter = json.decodeFromString<FilterData>(ctx.body())
ctx.future(future { Search.sourceFilter(sourceId, pageNum, filter) })
},
withResults = {
json<PagedMangaListDataClass>(HttpCode.OK)
}
)
/** all source search */ /** all source search */
val searchAll = handler( val searchAll = handler(
pathParam<String>("searchTerm"), pathParam<String>("searchTerm"),

View File

@@ -2,7 +2,6 @@ package suwayomi.tachidesk.manga.controller
import io.javalin.http.HttpCode import io.javalin.http.HttpCode
import io.javalin.websocket.WsConfig import io.javalin.websocket.WsConfig
import kotlinx.coroutines.runBlocking
import mu.KotlinLogging import mu.KotlinLogging
import org.kodein.di.DI import org.kodein.di.DI
import org.kodein.di.conf.global import org.kodein.di.conf.global
@@ -14,6 +13,7 @@ import suwayomi.tachidesk.manga.impl.update.IUpdater
import suwayomi.tachidesk.manga.impl.update.UpdateStatus import suwayomi.tachidesk.manga.impl.update.UpdateStatus
import suwayomi.tachidesk.manga.impl.update.UpdaterSocket import suwayomi.tachidesk.manga.impl.update.UpdaterSocket
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
import suwayomi.tachidesk.manga.model.dataclass.MangaChapterDataClass
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.manga.model.dataclass.PaginatedList import suwayomi.tachidesk.manga.model.dataclass.PaginatedList
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
@@ -49,10 +49,16 @@ object UpdateController {
) )
}, },
withResults = { withResults = {
json<PaginatedList<MangaDataClass>>(HttpCode.OK) json<PagedMangaChapterListDataClass>(HttpCode.OK)
} }
) )
/**
* Class made for handling return type in the documentation for [recentChapters],
* since OpenApi cannot handle runtime generics.
*/
private class PagedMangaChapterListDataClass : PaginatedList<MangaChapterDataClass>(emptyList(), false)
val categoryUpdate = handler( val categoryUpdate = handler(
formParam<Int?>("categoryId"), formParam<Int?>("categoryId"),
documentWith = { documentWith = {
@@ -62,22 +68,18 @@ object UpdateController {
} }
}, },
behaviorOf = { ctx, categoryId -> behaviorOf = { ctx, categoryId ->
val categoriesForUpdate = ArrayList<CategoryDataClass>()
if (categoryId == null) { if (categoryId == null) {
logger.info { "Adding Library to Update Queue" } logger.info { "Adding Library to Update Queue" }
categoriesForUpdate.addAll(Category.getCategoryList()) addCategoriesToUpdateQueue(Category.getCategoryList(), true)
} else { } else {
val category = Category.getCategoryById(categoryId) val category = Category.getCategoryById(categoryId)
if (category != null) { if (category != null) {
categoriesForUpdate.add(category) addCategoriesToUpdateQueue(listOf(category), true)
} else { } else {
logger.info { "No Category found" } logger.info { "No Category found" }
ctx.status(HttpCode.BAD_REQUEST) ctx.status(HttpCode.BAD_REQUEST)
return@handler
} }
} }
addCategoriesToUpdateQueue(categoriesForUpdate, true)
ctx.status(HttpCode.OK)
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpCode.OK)
@@ -88,14 +90,15 @@ object UpdateController {
private fun addCategoriesToUpdateQueue(categories: List<CategoryDataClass>, clear: Boolean = false) { private fun addCategoriesToUpdateQueue(categories: List<CategoryDataClass>, clear: Boolean = false) {
val updater by DI.global.instance<IUpdater>() val updater by DI.global.instance<IUpdater>()
if (clear) { if (clear) {
runBlocking { updater.reset() } updater.reset()
} }
categories.forEach { category -> categories
val mangas = CategoryManga.getCategoryMangaList(category.id) .flatMap { CategoryManga.getCategoryMangaList(it.id) }
mangas.forEach { manga -> .distinctBy { it.id }
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, MangaDataClass::title))
.forEach { manga ->
updater.addMangaToQueue(manga) updater.addMangaToQueue(manga)
} }
}
} }
fun categoryUpdateWS(ws: WsConfig) { fun categoryUpdateWS(ws: WsConfig) {
@@ -119,7 +122,7 @@ object UpdateController {
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
val updater by DI.global.instance<IUpdater>() val updater by DI.global.instance<IUpdater>()
ctx.json(updater.getStatus().value.getJsonSummary()) ctx.json(updater.status.value)
}, },
withResults = { withResults = {
json<UpdateStatus>(HttpCode.OK) json<UpdateStatus>(HttpCode.OK)

View File

@@ -8,8 +8,10 @@ package suwayomi.tachidesk.manga.impl
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.insertAndGetId import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
@@ -19,6 +21,7 @@ import suwayomi.tachidesk.manga.impl.CategoryManga.removeMangaFromCategory
import suwayomi.tachidesk.manga.impl.util.lang.isNotEmpty import suwayomi.tachidesk.manga.impl.util.lang.isNotEmpty
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
import suwayomi.tachidesk.manga.model.table.CategoryMetaTable
import suwayomi.tachidesk.manga.model.table.CategoryTable import suwayomi.tachidesk.manga.model.table.CategoryTable
import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.manga.model.table.toDataClass import suwayomi.tachidesk.manga.model.table.toDataClass
@@ -41,7 +44,9 @@ object Category {
normalizeCategories() normalizeCategories()
newCategoryId newCategoryId
} else -1 } else {
-1
}
} }
} }
@@ -117,4 +122,31 @@ object Category {
} }
} }
} }
fun getCategoryMetaMap(categoryId: Int): Map<String, String> {
return transaction {
CategoryMetaTable.select { CategoryMetaTable.ref eq categoryId }
.associate { it[CategoryMetaTable.key] to it[CategoryMetaTable.value] }
}
}
fun modifyMeta(categoryId: Int, key: String, value: String) {
transaction {
val meta = transaction {
CategoryMetaTable.select { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }
}.firstOrNull()
if (meta == null) {
CategoryMetaTable.insert {
it[CategoryMetaTable.key] = key
it[CategoryMetaTable.value] = value
it[CategoryMetaTable.ref] = categoryId
}
} else {
CategoryMetaTable.update({ (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }) {
it[CategoryMetaTable.value] = value
}
}
}
}
} }

View File

@@ -9,6 +9,7 @@ package suwayomi.tachidesk.manga.impl
import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.count import org.jetbrains.exposed.sql.count
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.deleteWhere
@@ -80,19 +81,20 @@ object CategoryManga {
val transform: (ResultRow) -> MangaDataClass = { val transform: (ResultRow) -> MangaDataClass = {
val dataClass = MangaTable.toDataClass(it) val dataClass = MangaTable.toDataClass(it)
dataClass.unreadCount = it[unreadExpression]?.toInt() dataClass.unreadCount = it[unreadExpression]
dataClass.downloadCount = it[downloadExpression]?.toInt() dataClass.downloadCount = it[downloadExpression]
dataClass.chapterCount = it[chapterCountExpression]?.toInt() dataClass.chapterCount = it[chapterCountExpression]
dataClass dataClass
} }
if (categoryId == DEFAULT_CATEGORY_ID) if (categoryId == DEFAULT_CATEGORY_ID) {
return transaction { return transaction {
MangaTable MangaTable
.slice(selectedColumns) .slice(selectedColumns)
.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) } .select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }
.map(transform) .map(transform)
} }
}
return transaction { return transaction {
CategoryMangaTable.innerJoin(MangaTable) CategoryMangaTable.innerJoin(MangaTable)

View File

@@ -10,15 +10,13 @@ package suwayomi.tachidesk.manga.impl
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
import kotlinx.serialization.Serializable
import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SortOrder.ASC import org.jetbrains.exposed.sql.SortOrder.ASC
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.manga.impl.Manga.getManga import suwayomi.tachidesk.manga.impl.Manga.getManga
import suwayomi.tachidesk.manga.impl.util.getChapterDir import suwayomi.tachidesk.manga.impl.util.getChapterDir
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
@@ -100,6 +98,10 @@ object Chapter {
} }
} }
} }
MangaTable.update({ MangaTable.id eq mangaId }) {
it[MangaTable.chaptersLastFetchedAt] = Instant.now().epochSecond
}
} }
// clear any orphaned/duplicate chapters that are in the db but not in `chapterList` // clear any orphaned/duplicate chapters that are in the db but not in `chapterList`
@@ -128,11 +130,15 @@ object Chapter {
.associateBy({ it[ChapterTable.url] }, { it }) .associateBy({ it[ChapterTable.url] }, { it })
} }
val chapterIds = chapterList.map { dbChapterMap.getValue(it.url)[ChapterTable.id] }
val chapterMetas = getChaptersMetaMaps(chapterIds)
return chapterList.mapIndexed { index, it -> return chapterList.mapIndexed { index, it ->
val dbChapter = dbChapterMap.getValue(it.url) val dbChapter = dbChapterMap.getValue(it.url)
ChapterDataClass( ChapterDataClass(
dbChapter[ChapterTable.id].value,
it.url, it.url,
it.name, it.name,
it.date_upload, it.date_upload,
@@ -152,7 +158,7 @@ object Chapter {
dbChapter[ChapterTable.pageCount], dbChapter[ChapterTable.pageCount],
chapterList.size, chapterList.size,
meta = getChapterMetaMap(dbChapter[ChapterTable.id]) meta = chapterMetas.getValue(dbChapter[ChapterTable.id])
) )
} }
} }
@@ -189,6 +195,87 @@ object Chapter {
} }
} }
@Serializable
data class ChapterChange(
val isRead: Boolean? = null,
val isBookmarked: Boolean? = null,
val lastPageRead: Int? = null,
val delete: Boolean? = null
)
@Serializable
data class MangaChapterBatchEditInput(
val chapterIds: List<Int>? = null,
val chapterIndexes: List<Int>? = null,
val change: ChapterChange?
)
@Serializable
data class ChapterBatchEditInput(
val chapterIds: List<Int>? = null,
val change: ChapterChange?
)
fun modifyChapters(input: MangaChapterBatchEditInput, mangaId: Int? = null) {
// Make sure change is defined
if (input.change == null) return
val (isRead, isBookmarked, lastPageRead, delete) = input.change
// Handle deleting separately
if (delete == true) {
deleteChapters(input, mangaId)
}
// return early if there are no other changes
if (listOfNotNull(isRead, isBookmarked, lastPageRead).isEmpty()) return
// Make sure some filter is defined
val condition = when {
mangaId != null ->
// mangaId is not null, scope query under manga
when {
input.chapterIds != null ->
Op.build { (ChapterTable.manga eq mangaId) and (ChapterTable.id inList input.chapterIds) }
input.chapterIndexes != null ->
Op.build { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder inList input.chapterIndexes) }
else -> null
}
else -> {
// mangaId is null, only chapterIndexes is valid for this case
when {
input.chapterIds != null ->
Op.build { (ChapterTable.id inList input.chapterIds) }
else -> null
}
}
} ?: return
transaction {
val now = Instant.now().epochSecond
ChapterTable.update({ condition }) { update ->
isRead?.also {
update[ChapterTable.isRead] = it
}
isBookmarked?.also {
update[ChapterTable.isBookmarked] = it
}
lastPageRead?.also {
update[ChapterTable.lastPageRead] = it
update[ChapterTable.lastReadAt] = now
}
}
}
}
fun getChaptersMetaMaps(chapterIds: List<EntityID<Int>>): Map<EntityID<Int>, Map<String, String>> {
return transaction {
ChapterMetaTable.select { ChapterMetaTable.ref inList chapterIds }
.groupBy { it[ChapterMetaTable.ref] }
.mapValues { it.value.associate { it[ChapterMetaTable.key] to it[ChapterMetaTable.value] } }
.withDefault { emptyMap<String, String>() }
}
}
fun getChapterMetaMap(chapter: EntityID<Int>): Map<String, String> { fun getChapterMetaMap(chapter: EntityID<Int>): Map<String, String> {
return transaction { return transaction {
ChapterMetaTable.select { ChapterMetaTable.ref eq chapter } ChapterMetaTable.select { ChapterMetaTable.ref eq chapter }
@@ -201,9 +288,9 @@ object Chapter {
val chapterId = val chapterId =
ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder eq chapterIndex) } ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder eq chapterIndex) }
.first()[ChapterTable.id].value .first()[ChapterTable.id].value
val meta = transaction { val meta =
ChapterMetaTable.select { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) } ChapterMetaTable.select { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }
}.firstOrNull() .firstOrNull()
if (meta == null) { if (meta == null) {
ChapterMetaTable.insert { ChapterMetaTable.insert {
@@ -235,6 +322,42 @@ object Chapter {
} }
} }
private fun deleteChapters(input: MangaChapterBatchEditInput, mangaId: Int? = null) {
if (input.chapterIds != null) {
val chapterIds = input.chapterIds
transaction {
ChapterTable.slice(ChapterTable.manga, ChapterTable.id)
.select { ChapterTable.id inList chapterIds }
.forEach { row ->
val chapterMangaId = row[ChapterTable.manga].value
val chapterId = row[ChapterTable.id].value
val chapterDir = getChapterDir(chapterMangaId, chapterId)
File(chapterDir).deleteRecursively()
}
ChapterTable.update({ ChapterTable.id inList chapterIds }) {
it[isDownloaded] = false
}
}
} else if (input.chapterIndexes != null && mangaId != null) {
transaction {
val chapterIds = ChapterTable.slice(ChapterTable.manga, ChapterTable.id)
.select { (ChapterTable.sourceOrder inList input.chapterIndexes) and (ChapterTable.manga eq mangaId) }
.map { row ->
val chapterId = row[ChapterTable.id].value
val chapterDir = getChapterDir(mangaId, chapterId)
File(chapterDir).deleteRecursively()
chapterId
}
ChapterTable.update({ ChapterTable.id inList chapterIds }) {
it[isDownloaded] = false
}
}
}
}
fun getRecentChapters(pageNum: Int): PaginatedList<MangaChapterDataClass> { fun getRecentChapters(pageNum: Int): PaginatedList<MangaChapterDataClass> {
return paginatedFrom(pageNum) { return paginatedFrom(pageNum) {
transaction { transaction {

View File

@@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.source.local.LocalSource
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
@@ -34,21 +35,25 @@ import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
import suwayomi.tachidesk.manga.impl.util.updateMangaDownloadDir import suwayomi.tachidesk.manga.impl.util.updateMangaDownloadDir
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.manga.model.dataclass.toGenreList import suwayomi.tachidesk.manga.model.dataclass.toGenreList
import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.manga.model.table.MangaMetaTable import suwayomi.tachidesk.manga.model.table.MangaMetaTable
import suwayomi.tachidesk.manga.model.table.MangaStatus import suwayomi.tachidesk.manga.model.table.MangaStatus
import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.manga.model.table.toDataClass
import suwayomi.tachidesk.server.ApplicationDirs import suwayomi.tachidesk.server.ApplicationDirs
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.time.Instant
object Manga { object Manga {
private fun truncate(text: String?, maxLength: Int): String? { private fun truncate(text: String?, maxLength: Int): String? {
return if (text?.length ?: 0 > maxLength) return if (text?.length ?: 0 > maxLength) {
text?.take(maxLength - 3) + "..." text?.take(maxLength - 3) + "..."
else } else {
text text
}
} }
suspend fun getManga(mangaId: Int, onlineFetch: Boolean = false): MangaDataClass { suspend fun getManga(mangaId: Int, onlineFetch: Boolean = false): MangaDataClass {
@@ -68,12 +73,12 @@ object Manga {
transaction { transaction {
MangaTable.update({ MangaTable.id eq mangaId }) { MangaTable.update({ MangaTable.id eq mangaId }) {
if (sManga.title != mangaEntry[MangaTable.title]) { if (sManga.title != mangaEntry[MangaTable.title]) {
val canUpdateTitle = updateMangaDownloadDir(mangaId, sManga.title) val canUpdateTitle = updateMangaDownloadDir(mangaId, sManga.title)
if (canUpdateTitle) if (canUpdateTitle) {
it[MangaTable.title] = sManga.title it[MangaTable.title] = sManga.title
}
} }
it[MangaTable.initialized] = true it[MangaTable.initialized] = true
@@ -82,17 +87,20 @@ object Manga {
it[MangaTable.description] = truncate(sManga.description, 4096) it[MangaTable.description] = truncate(sManga.description, 4096)
it[MangaTable.genre] = sManga.genre it[MangaTable.genre] = sManga.genre
it[MangaTable.status] = sManga.status it[MangaTable.status] = sManga.status
if (sManga.thumbnail_url != null && sManga.thumbnail_url.orEmpty().isNotEmpty()) if (!sManga.thumbnail_url.isNullOrEmpty() && sManga.thumbnail_url != mangaEntry[MangaTable.thumbnail_url]) {
it[MangaTable.thumbnail_url] = sManga.thumbnail_url it[MangaTable.thumbnail_url] = sManga.thumbnail_url
it[MangaTable.thumbnailUrlLastFetched] = Instant.now().epochSecond
clearMangaThumbnail(mangaId)
}
it[MangaTable.realUrl] = runCatching { it[MangaTable.realUrl] = runCatching {
(source as? HttpSource)?.mangaDetailsRequest(sManga)?.url?.toString() (source as? HttpSource)?.mangaDetailsRequest(sManga)?.url?.toString()
}.getOrNull() }.getOrNull()
it[MangaTable.lastFetchedAt] = Instant.now().epochSecond
} }
} }
clearMangaThumbnail(mangaId)
mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() } mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
MangaDataClass( MangaDataClass(
@@ -102,6 +110,7 @@ object Manga {
mangaEntry[MangaTable.url], mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title], mangaEntry[MangaTable.title],
proxyThumbnailUrl(mangaId), proxyThumbnailUrl(mangaId),
mangaEntry[MangaTable.thumbnailUrlLastFetched],
true, true,
@@ -115,11 +124,47 @@ object Manga {
getSource(mangaEntry[MangaTable.sourceReference]), getSource(mangaEntry[MangaTable.sourceReference]),
getMangaMetaMap(mangaId), getMangaMetaMap(mangaId),
mangaEntry[MangaTable.realUrl], mangaEntry[MangaTable.realUrl],
mangaEntry[MangaTable.lastFetchedAt],
mangaEntry[MangaTable.chaptersLastFetchedAt],
true true
) )
} }
} }
suspend fun getMangaFull(mangaId: Int, onlineFetch: Boolean = false): MangaDataClass {
val mangaDaaClass = getManga(mangaId, onlineFetch)
return transaction {
val unreadCount =
ChapterTable
.select { (ChapterTable.manga eq mangaId) and (ChapterTable.isRead eq false) }
.count()
val downloadCount =
ChapterTable
.select { (ChapterTable.manga eq mangaId) and (ChapterTable.isDownloaded eq true) }
.count()
val chapterCount =
ChapterTable
.select { (ChapterTable.manga eq mangaId) }
.count()
val lastChapterRead =
ChapterTable
.select { (ChapterTable.manga eq mangaId) }
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC)
.firstOrNull { it[ChapterTable.isRead] }
mangaDaaClass.unreadCount = unreadCount
mangaDaaClass.downloadCount = downloadCount
mangaDaaClass.chapterCount = chapterCount
mangaDaaClass.lastChapterRead = lastChapterRead?.let { ChapterTable.toDataClass(it) }
mangaDaaClass
}
}
private fun getMangaDataClass(mangaId: Int, mangaEntry: ResultRow) = MangaDataClass( private fun getMangaDataClass(mangaId: Int, mangaEntry: ResultRow) = MangaDataClass(
mangaId, mangaId,
mangaEntry[MangaTable.sourceReference].toString(), mangaEntry[MangaTable.sourceReference].toString(),
@@ -127,6 +172,7 @@ object Manga {
mangaEntry[MangaTable.url], mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title], mangaEntry[MangaTable.title],
proxyThumbnailUrl(mangaId), proxyThumbnailUrl(mangaId),
mangaEntry[MangaTable.thumbnailUrlLastFetched],
true, true,
@@ -140,32 +186,32 @@ object Manga {
getSource(mangaEntry[MangaTable.sourceReference]), getSource(mangaEntry[MangaTable.sourceReference]),
getMangaMetaMap(mangaId), getMangaMetaMap(mangaId),
mangaEntry[MangaTable.realUrl], mangaEntry[MangaTable.realUrl],
mangaEntry[MangaTable.lastFetchedAt],
mangaEntry[MangaTable.chaptersLastFetchedAt],
false false
) )
fun getMangaMetaMap(manga: Int): Map<String, String> { fun getMangaMetaMap(mangaId: Int): Map<String, String> {
return transaction { return transaction {
MangaMetaTable.select { MangaMetaTable.ref eq manga } MangaMetaTable.select { MangaMetaTable.ref eq mangaId }
.associate { it[MangaMetaTable.key] to it[MangaMetaTable.value] } .associate { it[MangaMetaTable.key] to it[MangaMetaTable.value] }
} }
} }
fun modifyMangaMeta(mangaId: Int, key: String, value: String) { fun modifyMangaMeta(mangaId: Int, key: String, value: String) {
transaction { transaction {
val manga = MangaTable.select { MangaTable.id eq mangaId } val meta =
.first()[MangaTable.id] MangaMetaTable.select { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }
val meta = transaction { .firstOrNull()
MangaMetaTable.select { (MangaMetaTable.ref eq manga) and (MangaMetaTable.key eq key) }
}.firstOrNull()
if (meta == null) { if (meta == null) {
MangaMetaTable.insert { MangaMetaTable.insert {
it[MangaMetaTable.key] = key it[MangaMetaTable.key] = key
it[MangaMetaTable.value] = value it[MangaMetaTable.value] = value
it[MangaMetaTable.ref] = manga it[MangaMetaTable.ref] = mangaId
} }
} else { } else {
MangaMetaTable.update({ (MangaMetaTable.ref eq manga) and (MangaMetaTable.key eq key) }) { MangaMetaTable.update({ (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }) {
it[MangaMetaTable.value] = value it[MangaMetaTable.value] = value
} }
} }
@@ -199,6 +245,7 @@ object Manga {
GET(thumbnailUrl, source.headers) GET(thumbnailUrl, source.headers)
).await() ).await()
} }
is LocalSource -> { is LocalSource -> {
val imageFile = mangaEntry[MangaTable.thumbnail_url]?.let { val imageFile = mangaEntry[MangaTable.thumbnail_url]?.let {
val file = File(it) val file = File(it)
@@ -212,6 +259,7 @@ object Manga {
?: "image/jpeg" ?: "image/jpeg"
imageFile.inputStream() to contentType imageFile.inputStream() to contentType
} }
is StubSource -> getImageResponse(saveDir, fileName, useCache) { is StubSource -> getImageResponse(saveDir, fileName, useCache) {
val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url] val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
?: throw NullPointerException("No thumbnail found") ?: throw NullPointerException("No thumbnail found")
@@ -219,6 +267,7 @@ object Manga {
GET(thumbnailUrl) GET(thumbnailUrl)
).await() ).await()
} }
else -> throw IllegalArgumentException("Unknown source") else -> throw IllegalArgumentException("Unknown source")
} }
} }

View File

@@ -27,6 +27,9 @@ object MangaList {
} }
suspend fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass { suspend fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
require(pageNum > 0) {
"pageNum = $pageNum is not in valid range"
}
val source = getCatalogueSourceOrStub(sourceId) val source = getCatalogueSourceOrStub(sourceId)
val mangasPage = if (popular) { val mangasPage = if (popular) {
source.fetchPopularManga(pageNum).awaitSingle() source.fetchPopularManga(pageNum).awaitSingle()
@@ -73,6 +76,7 @@ object MangaList {
manga.url, manga.url,
manga.title, manga.title,
proxyThumbnailUrl(mangaId), proxyThumbnailUrl(mangaId),
mangaEntry[MangaTable.thumbnailUrlLastFetched],
manga.initialized, manga.initialized,
@@ -85,6 +89,8 @@ object MangaList {
0, 0,
meta = getMangaMetaMap(mangaId), meta = getMangaMetaMap(mangaId),
realUrl = mangaEntry[MangaTable.realUrl], realUrl = mangaEntry[MangaTable.realUrl],
lastFetchedAt = mangaEntry[MangaTable.lastFetchedAt],
chaptersLastFetchedAt = mangaEntry[MangaTable.chaptersLastFetchedAt],
freshData = true freshData = true
) )
} else { } else {
@@ -96,6 +102,7 @@ object MangaList {
manga.url, manga.url,
manga.title, manga.title,
proxyThumbnailUrl(mangaId), proxyThumbnailUrl(mangaId),
mangaEntry[MangaTable.thumbnailUrlLastFetched],
true, true,
@@ -108,6 +115,8 @@ object MangaList {
mangaEntry[MangaTable.inLibraryAt], mangaEntry[MangaTable.inLibraryAt],
meta = getMangaMetaMap(mangaId), meta = getMangaMetaMap(mangaId),
realUrl = mangaEntry[MangaTable.realUrl], realUrl = mangaEntry[MangaTable.realUrl],
lastFetchedAt = mangaEntry[MangaTable.lastFetchedAt],
chaptersLastFetchedAt = mangaEntry[MangaTable.chaptersLastFetchedAt],
freshData = false freshData = false
) )
} }

View File

@@ -10,6 +10,7 @@ package suwayomi.tachidesk.manga.impl
import eu.kanade.tachiyomi.source.local.LocalSource import eu.kanade.tachiyomi.source.local.LocalSource
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.flow.StateFlow
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
@@ -37,7 +38,7 @@ object Page {
return page.imageUrl!! return page.imageUrl!!
} }
suspend fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int, useCache: Boolean = true): Pair<InputStream, String> { suspend fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int, useCache: Boolean = true, progressFlow: ((StateFlow<Int>) -> Unit)? = null): Pair<InputStream, String> {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() } val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference]) val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
val chapterEntry = transaction { val chapterEntry = transaction {
@@ -55,6 +56,7 @@ object Page {
pageEntry[PageTable.url], pageEntry[PageTable.url],
pageEntry[PageTable.imageUrl] pageEntry[PageTable.imageUrl]
) )
progressFlow?.invoke(tachiyomiPage.progress)
// we treat Local source differently // we treat Local source differently
if (source.id == LocalSource.ID) { if (source.id == LocalSource.ID) {

View File

@@ -27,6 +27,13 @@ object Search {
return searchManga.processEntries(sourceId) return searchManga.processEntries(sourceId)
} }
suspend fun sourceFilter(sourceId: Long, pageNum: Int, filter: FilterData): PagedMangaListDataClass {
val source = getCatalogueSourceOrStub(sourceId)
val filterList = if (filter.filter != null) buildFilterList(sourceId, filter.filter) else source.getFilterList()
val searchManga = source.fetchSearchManga(pageNum, filter.searchTerm ?: "", filterList).awaitSingle()
return searchManga.processEntries(sourceId)
}
private val filterListCache = mutableMapOf<Long, FilterList>() private val filterListCache = mutableMapOf<Long, FilterList>()
private fun getFilterListOf(source: CatalogueSource, reset: Boolean = false): FilterList { private fun getFilterListOf(source: CatalogueSource, reset: Boolean = false): FilterList {
@@ -78,13 +85,16 @@ object Search {
data class FilterObject( data class FilterObject(
val type: String, val type: String,
val filter: Filter<*>, val filter: Filter<*>
) )
fun setFilter(sourceId: Long, changes: List<FilterChange>) { fun setFilter(sourceId: Long, changes: List<FilterChange>) {
val source = getCatalogueSourceOrStub(sourceId) val source = getCatalogueSourceOrStub(sourceId)
val filterList = getFilterListOf(source, false) val filterList = getFilterListOf(source, false)
updateFilterList(filterList, changes)
}
private fun updateFilterList(filterList: FilterList, changes: List<FilterChange>): FilterList {
changes.forEach { change -> changes.forEach { change ->
when (val filter = filterList[change.position]) { when (val filter = filterList[change.position]) {
is Filter.Header -> { is Filter.Header -> {
@@ -112,6 +122,13 @@ object Search {
} }
} }
} }
return filterList
}
private fun buildFilterList(sourceId: Long, changes: List<FilterChange>): FilterList {
val source = getCatalogueSourceOrStub(sourceId)
val filterList = source.getFilterList()
return updateFilterList(filterList, changes)
} }
private val jsonMapper by DI.global.instance<JsonMapper>() private val jsonMapper by DI.global.instance<JsonMapper>()
@@ -122,6 +139,12 @@ object Search {
val state: String val state: String
) )
@Serializable
data class FilterData(
val searchTerm: String?,
val filter: List<FilterChange>?
)
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
fun sourceGlobalSearch(searchTerm: String) { fun sourceGlobalSearch(searchTerm: String) {
// TODO // TODO

View File

@@ -48,7 +48,7 @@ object Source {
catalogueSource.supportsLatest, catalogueSource.supportsLatest,
catalogueSource is ConfigurableSource, catalogueSource is ConfigurableSource,
it[SourceTable.isNsfw], it[SourceTable.isNsfw],
catalogueSource.toString(), catalogueSource.toString()
) )
} }
} }

View File

@@ -12,5 +12,5 @@ data class BackupFlags(
val includeCategories: Boolean, val includeCategories: Boolean,
val includeChapters: Boolean, val includeChapters: Boolean,
val includeTracking: Boolean, val includeTracking: Boolean,
val includeHistory: Boolean, val includeHistory: Boolean
) )

View File

@@ -69,7 +69,7 @@ object ProtoBackupExport : ProtoBackupBase() {
MangaStatus.valueOf(mangaRow[MangaTable.status]).value, MangaStatus.valueOf(mangaRow[MangaTable.status]).value,
mangaRow[MangaTable.thumbnail_url], mangaRow[MangaTable.thumbnail_url],
TimeUnit.SECONDS.toMillis(mangaRow[MangaTable.inLibraryAt]), TimeUnit.SECONDS.toMillis(mangaRow[MangaTable.inLibraryAt]),
0, // not supported in Tachidesk 0 // not supported in Tachidesk
) )
val mangaId = mangaRow[MangaTable.id].value val mangaId = mangaRow[MangaTable.id].value
@@ -94,7 +94,7 @@ object ProtoBackupExport : ProtoBackupBase() {
TimeUnit.SECONDS.toMillis(it.fetchedAt), TimeUnit.SECONDS.toMillis(it.fetchedAt),
it.uploadDate, it.uploadDate,
it.chapterNumber, it.chapterNumber,
chapters.size - it.index, chapters.size - it.index
) )
} }
} }
@@ -122,7 +122,7 @@ object ProtoBackupExport : ProtoBackupBase() {
BackupCategory( BackupCategory(
it.name, it.name,
it.order, it.order,
0, // not supported in Tachidesk 0 // not supported in Tachidesk
) )
} }
} }

View File

@@ -19,10 +19,10 @@ import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.manga.impl.Category import suwayomi.tachidesk.manga.impl.Category
import suwayomi.tachidesk.manga.impl.CategoryManga import suwayomi.tachidesk.manga.impl.CategoryManga
import suwayomi.tachidesk.manga.impl.backup.AbstractBackupValidator.ValidationResult
import suwayomi.tachidesk.manga.impl.backup.models.Chapter import suwayomi.tachidesk.manga.impl.backup.models.Chapter
import suwayomi.tachidesk.manga.impl.backup.models.Manga import suwayomi.tachidesk.manga.impl.backup.models.Manga
import suwayomi.tachidesk.manga.impl.backup.models.Track import suwayomi.tachidesk.manga.impl.backup.models.Track
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator.ValidationResult
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator.validate import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator.validate
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupCategory import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupCategory
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupHistory import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupHistory
@@ -77,6 +77,8 @@ object ProtoBackupImport : ProtoBackupBase() {
Restore Summary: Restore Summary:
- Missing Sources: - Missing Sources:
${validationResult.missingSources.joinToString("\n ")} ${validationResult.missingSources.joinToString("\n ")}
- Titles missing Sources:
${validationResult.mangasMissingSources.joinToString("\n ")}
- Missing Trackers: - Missing Trackers:
${validationResult.missingTrackers.joinToString("\n ")} ${validationResult.missingTrackers.joinToString("\n ")}
""".trimIndent() """.trimIndent()

View File

@@ -12,13 +12,18 @@ import okio.gzip
import okio.source import okio.source
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.manga.impl.backup.AbstractBackupValidator
import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSerializer import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSerializer
import suwayomi.tachidesk.manga.model.table.SourceTable import suwayomi.tachidesk.manga.model.table.SourceTable
import java.io.InputStream import java.io.InputStream
object ProtoBackupValidator : AbstractBackupValidator() { object ProtoBackupValidator {
data class ValidationResult(
val missingSources: List<String>,
val missingTrackers: List<String>,
val mangasMissingSources: List<String>
)
fun validate(backup: Backup): ValidationResult { fun validate(backup: Backup): ValidationResult {
if (backup.backupManga.isEmpty()) { if (backup.backupManga.isEmpty()) {
throw Exception("Backup does not contain any manga.") throw Exception("Backup does not contain any manga.")
@@ -33,6 +38,12 @@ object ProtoBackupValidator : AbstractBackupValidator() {
.sorted() .sorted()
} }
val brokenSourceIds = backup.brokenBackupSources.map { it.sourceId }
val mangasMissingSources = backup.backupManga
.filter { it.source in brokenSourceIds }
.map { manga -> "${manga.title} (from ${backup.brokenBackupSources.first { it.sourceId == manga.source }.name})" }
// val trackers = backup.backupManga // val trackers = backup.backupManga
// .flatMap { it.tracking } // .flatMap { it.tracking }
// .map { it.syncId } // .map { it.syncId }
@@ -45,7 +56,7 @@ object ProtoBackupValidator : AbstractBackupValidator() {
// .map { context.getString(it.nameRes()) } // .map { context.getString(it.nameRes()) }
// .sorted() // .sorted()
return ValidationResult(missingSources, missingTrackers) return ValidationResult(missingSources, missingTrackers, mangasMissingSources)
} }
suspend fun validate(sourceStream: InputStream): ValidationResult { suspend fun validate(sourceStream: InputStream): ValidationResult {

Some files were not shown because too many files have changed in this diff Show More