Compare commits

...

41 Commits

Author SHA1 Message Date
Aria Moradi cc5a63205c v0.6.4
CI Publish / Validate Gradle Wrapper (push) Successful in 18s
CI Publish / Build Jar (push) Failing after 47s
CI Publish / Make debian-all release (push) Has been skipped
CI Publish / Make linux-assets release (push) Has been skipped
CI Publish / Make linux-x64 release (push) Has been skipped
CI Publish / Make macOS-arm64 release (push) Has been skipped
CI Publish / Make macOS-x64 release (push) Has been skipped
CI Publish / Make windows-x64 release (push) Has been skipped
CI Publish / Make windows-x86 release (push) Has been skipped
CI Publish / release (push) Has been skipped
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 1e29293dd0148c8aa692004f36b29d7abd9ca0f0.
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
Aria Moradi 86f0b3f29f fix WebUI release name
CI Publish / Validate Gradle Wrapper (push) Successful in 13s
CI Publish / Build artifacts and release (push) Failing after 15s
2022-05-06 20:36:42 +04:30
Aria Moradi 85e3aa34ac bump WebUI 2022-05-06 20:19:11 +04:30
Aria Moradi 5bbc1dedef fix formatting by kotlinter 2022-05-06 17:52:16 +04:30
Aria Moradi 39b468ef06 fix copymanga (#354) 2022-05-06 17:45:05 +04:30
Mitchell Syer fe17176b31 document all endpoints (#350)
* Document all endpoints

* Forgot about global endpoints
2022-04-27 16:01:39 +04:30
abhijeetChawla 84f701c4ab add ChapterCount to manga object in categoryMangas endpoint (#349)
* adds ChapterCount to the Manga returned when accessing the array of Manga is a category

* removed a conflicting expresssion
2022-04-24 13:13:35 +04:30
Mitchell Syer 047f8c176f document manga endpoints (#348) 2022-04-24 13:08:33 +04:30
Mitchell Syer d82e79b680 Add displayValues json field for select filter (#347) 2022-04-24 13:06:19 +04:30
Aria Moradi 320d1ae9d8 add support for alternative web interfaces (#342)
* add support for alternative web interfaces

* fix naming

* won't bundle sorayomi zip

* clean diff
2022-04-16 21:09:36 +04:30
Aria Moradi a8892143a2 fix Applications dir dependency (#344) 2022-04-16 20:58:12 +04:30
Aria Moradi 50f4532406 add support for changing downloads dir (#343) 2022-04-16 20:20:57 +04:30
Fidel Selva 844454053d handle solid RAR archives (#339)
* Upgrade junrar version to 7.5.0 and set unrar.extractor.thread-keep-alive-seconds to 30 (default is 5)

* #338 Read whole archive in case RAR file is solid (it is, it can't be decompressed at an arbitrary location).
2022-04-16 18:24:03 +04:30
Mitchell Syer db5c5ed534 Save categories when manga is unfavorited (#335)
Fixes non-library manga with categories in backups
2022-04-08 06:10:39 +04:30
Aria Moradi a26b8ecca0 v0.6.3 2022-04-07 15:54:42 +04:30
Aria Moradi 5a32ccfa7a fix auth not actually blocking requests (#333) 2022-04-06 21:30:38 +04:30
Mitchell Syer f51818b157 Add QuickJS, replaces Duktape for Extensions Lib 1.3 (#331) 2022-04-02 19:43:45 +04:30
Mitchell Syer 31a624db51 Add last bit of code needed for Extensions Lib 1.3 (#330) 2022-04-02 05:02:26 +04:30
DattatreyaReddy Panta f045b18762 update description for Tachidesk-Sorayomi (#326)
* added Tachidesk-Flutter to readme

* Updated Description for Tachidesk-Sorayomi
2022-03-27 16:41:35 +04:30
Mitchell Syer f5006cac7d Add thumbnail support for stub sources (#320) 2022-03-22 15:51:58 +04:30
Mitchell Syer 152b193ad5 Improve source handling, fix errors with uninitialized mangas in broken sources (#319) 2022-03-22 15:51:07 +04:30
Mitchell Syer a27af0b642 Fix sources list of one source throws an exception (#308) 2022-03-20 19:24:09 +03:30
Aria Moradi 44ffed3f7c add support for tachiyomi extensions Lib 1.3 (#316)
* closes #315

* provide real values

* add support for tachiyomi extensions lib 1.3
2022-03-19 02:36:42 +03:30
Aria Moradi fa035ad9be fix meta update changing all keys (#314) 2022-03-18 00:14:22 +03:30
Mahor 186ace4343 Update README.md (#305)
* Update README.md

* Update README.md again
2022-03-05 09:38:20 +03:30
Aria Moradi 8fb1a0bb1f fix filterlist bugs (#306) 2022-03-05 01:13:48 +03:30
Aria Moradi 05513bf8b9 support array filter changes (#304)
* support array filter changes

* typo

* better formating
2022-03-05 00:06:55 +03:30
Aria Moradi 858784857e v0.6.2
CI Publish / Validate Gradle Wrapper (push) Successful in 12s
CI Publish / Build artifacts and release (push) Failing after 15s
2022-03-04 19:03:23 +03:30
Mahor 291a23949a Refactor debian-packager.sh, rename launcher scripts (#303)
* Improve windows-bundler.sh

* Overhaul debian-packager.sh. Rename base package name to tachidesk-server

* Add -electron-launcher-debian.sh
2022-03-03 13:26:56 +03:30
86 changed files with 2378 additions and 981 deletions
+4 -8
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
+128 -36
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 }}
+123 -29
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/*
+12
View File
@@ -0,0 +1,12 @@
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@latest
with:
identifier: Suwayomi.Tachidesk-Server
token: ${{ secrets.WINGET_PUBLISH_PAT }}
+117
View File
@@ -1,3 +1,120 @@
# Server: v0.6.4 + WebUI: r946
## TL;DR
- No new major features
- Bug fixes and changes for packaging
- Documentation changes
## Tachidesk-Server Changelog
- (r1087) v0.6.3 (by @AriaMoradi)
- (r1088) Save categories when manga is unfavorited ([#335](https://github.com/Suwayomi/Tachidesk-Server/pull/335) by @Syer10)
- (r1089) handle solid RAR archives ([#339](https://github.com/Suwayomi/Tachidesk-Server/pull/339)) cfso100@gmail.com
- (r1090) add support for changing downloads dir ([#343](https://github.com/Suwayomi/Tachidesk-Server/pull/343) by @AriaMoradi)
- (r1091) fix Applications dir dependency ([#344](https://github.com/Suwayomi/Tachidesk-Server/pull/344) by @AriaMoradi)
- (r1092) add support for alternative web interfaces ([#342](https://github.com/Suwayomi/Tachidesk-Server/pull/342) by @AriaMoradi)
- (r1093) Add displayValues json field for select filter ([#347](https://github.com/Suwayomi/Tachidesk-Server/pull/347) by @Syer10)
- (r1094) document manga endpoints ([#348](https://github.com/Suwayomi/Tachidesk-Server/pull/348) by @Syer10)
- (r1095) add ChapterCount to manga object in categoryMangas endpoint ([#349](https://github.com/Suwayomi/Tachidesk-Server/pull/349) by @abhijeetChawla)
- (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)
- (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/scripts/pull/358) by @Syer10)
- (r1102) Docs improvements ([#359](https://github.com/Suwayomi/scripts/pull/359) by @Syer10)
- (r1103) Add linux-all.tar.gz & systemd service ([#366](https://github.com/Suwayomi/scripts/pull/366)) mahor1221@pm.me
- (r1104) Publish to Windows Package Managar (WinGet [#369](https://github.com/Suwayomi/scripts/pull/369)) 83997633+vedantmgoyal2009@users.noreply.github.com
- (r1105) Refactor scripts ([#370](https://github.com/Suwayomi/scripts/pull/370)) mahor1221@pm.me
- (r1106) Run workflow jobs toghether ([#371](https://github.com/Suwayomi/scripts/pull/371)) mahor1221@pm.me
- (r1107) Update gradle action ([#372](https://github.com/Suwayomi/scripts/pull/372)) mahor1221@pm.me
- (r1108) Improve DocumentationDsl, bugfix default values and add queryParams ([#378](https://github.com/Suwayomi/scripts/pull/378) by @Syer10)
- (r1109) Tidy up bundler script ([#380](https://github.com/Suwayomi/scripts/pull/380)) mahor1221@pm.me
- (r1110) Replace linux-all with linux-assets ([#381](https://github.com/Suwayomi/scripts/pull/381)) mahor1221@pm.me
- (r1111) Rename every instance of Tachidesk jar to Tachdidesk-Server.jar ([#384](https://github.com/Suwayomi/scripts/pull/384) by @AriaMoradi)
- (r1112) Fix mistakes from #384 ([#385](https://github.com/Suwayomi/scripts/pull/385) by @AriaMoradi)
## Tachidesk-WebUI Changelog
- (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)
- (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
## TL;DR
- Changes in Server
- Support for array search filter changes list
- Support for Tachiyomi extensions lib 1.3
- Changes in WebUI
- Better search filter support
- Fluid manga grid
- Library comfortable grid
- Sources view layouts
- Various other changes...
## Tachidesk-Server Changelog
- (r1074) v0.6.2 (by @AriaMoradi)
- (r1075) support array filter changes ([#304](https://github.com/Suwayomi/Tachidesk-Server/pull/304) by @AriaMoradi)
- (r1076) fix filterlist bugs ([#306](https://github.com/Suwayomi/Tachidesk-Server/pull/306) by @AriaMoradi)
- (r1077) Update README.md ([#305](https://github.com/Suwayomi/Tachidesk-Server/pull/305) by @mahor1221)
- (r1078) fix meta update changing all keys ([#314](https://github.com/Suwayomi/Tachidesk-Server/pull/314) by @AriaMoradi)
- (r1079) add support for tachiyomi extensions Lib 1.3 ([#316](https://github.com/Suwayomi/Tachidesk-Server/pull/316) by @AriaMoradi)
- (r1080) Fix sources list of one source throws an exception ([#308](https://github.com/Suwayomi/Tachidesk-Server/pull/308) by @Syer10)
- (r1081) Improve source handling, fix errors with uninitialized mangas in broken sources ([#319](https://github.com/Suwayomi/Tachidesk-Server/pull/319) by @Syer10)
- (r1082) Add thumbnail support for stub sources ([#320](https://github.com/Suwayomi/Tachidesk-Server/pull/320) by @Syer10)
- (r1083) update description for Tachidesk-Sorayomi ([#326](https://github.com/Suwayomi/Tachidesk-Server/pull/326) by @DattatreyaReddy)
- (r1084) Add last bit of code needed for Extensions Lib 1.3 ([#330](https://github.com/Suwayomi/Tachidesk-Server/pull/330) by @Syer10)
- (r1085) Add QuickJS, replaces Duktape for Extensions Lib 1.3 ([#331](https://github.com/Suwayomi/Tachidesk-Server/pull/331) by @Syer10)
- (r1086) fix auth not actually blocking requests ([#333](https://github.com/Suwayomi/Tachidesk-Server/pull/333) by @AriaMoradi)
## Tachidesk-WebUI Changelog
- (r930) Source filter scroll fix (array of filters on submit [#149](https://github.com/Suwayomi/Tachidesk-WebUI/pull/149) by @Robonau)
- (r931) fix manga badges setting menu that turns the update/download badges on and off ([#150](https://github.com/Suwayomi/Tachidesk-WebUI/pull/150) by @Robonau)
- (r932) move sorts to copy tachiyomi ([#151](https://github.com/Suwayomi/Tachidesk-WebUI/pull/151) by @Robonau)
- (r933) add comfortable grid option ([#152](https://github.com/Suwayomi/Tachidesk-WebUI/pull/152) by @Robonau)
- (r934) source layouts ([#153](https://github.com/Suwayomi/Tachidesk-WebUI/pull/153) by @Robonau)
- (r935) List layout ([#154](https://github.com/Suwayomi/Tachidesk-WebUI/pull/154) by @Robonau)
- (r936) in library badge to manga in sources ([#156](https://github.com/Suwayomi/Tachidesk-WebUI/pull/156) by @Robonau)
- (r937) mass search ([#157](https://github.com/Suwayomi/Tachidesk-WebUI/pull/157) by @Robonau)
- (r938) 18+ tag on source/extension cards ([#160](https://github.com/Suwayomi/Tachidesk-WebUI/pull/160) by @Robonau)
- (r939) fix search source click ([#164](https://github.com/Suwayomi/Tachidesk-WebUI/pull/164) by @Robonau)
- (r940) items per row setting ([#165](https://github.com/Suwayomi/Tachidesk-WebUI/pull/165) by @Robonau)
- (r941) fix the grid width thing ([#169](https://github.com/Suwayomi/Tachidesk-WebUI/pull/169) by @Robonau)
- (r942) unified library options ([#168](https://github.com/Suwayomi/Tachidesk-WebUI/pull/168) by @infix)
# Server: v0.6.2 + WebUI: r929
## TL;DR
- Changes in WebUI
- Moved search to Browse
- Support for Source Filters
- Better visuals for Download Queue
- A live version of WebUI is now available [at this link](https://tachidesk-webui-preview.github.io/).
## Tachidesk-Server Changelog
- (r1073) Refactor debian-packager.sh, rename launcher scripts ([#303](https://github.com/Suwayomi/Tachidesk-Server/pull/303) by @mahor1221)
## Tachidesk-WebUI Changelog
- (r912) show locale date, less confusing ([#131](https://github.com/Suwayomi/Tachidesk-WebUI/pull/131) by @AriaMoradi)
- (r913) fix links to work on a bare host ([#132](https://github.com/Suwayomi/Tachidesk-WebUI/pull/132) by @AriaMoradi)
- (r914) fix direct links ([#133](https://github.com/Suwayomi/Tachidesk-WebUI/pull/133) by @AriaMoradi)
- (r915) deploy to github pages (by @AriaMoradi)
- (r916) fix typo (by @AriaMoradi)
- (r917) better naming (by @AriaMoradi)
- (r918) update notice about github pages (by @AriaMoradi)
- (r919) move text (by @AriaMoradi)
- (r920) make all links work by catching 404 (by @AriaMoradi)
- (r921) fix scrolling 8px ([#135](https://github.com/Suwayomi/Tachidesk-WebUI/pull/135) by @Robonau)
- (r922) sorting ([#136](https://github.com/Suwayomi/Tachidesk-WebUI/pull/136) by @Robonau)
- (r923) Close button fix ([#141](https://github.com/Suwayomi/Tachidesk-WebUI/pull/141)) z14942744@gmail.com
- (r924) add NavBarContextProvider ([#128](https://github.com/Suwayomi/Tachidesk-WebUI/pull/128) by @abhijeetChawla)
- (r925) Resolved Merged Conflicts ([#127](https://github.com/Suwayomi/Tachidesk-WebUI/pull/127) by @abhijeetChawla)
- (r926) more Download Queue info ([#138](https://github.com/Suwayomi/Tachidesk-WebUI/pull/138) by @Robonau)
- (r927) Source filters, move search to SourceMangas ([#142](https://github.com/Suwayomi/Tachidesk-WebUI/pull/142) by @Robonau)
- (r928) Source genre sorts design ([#147](https://github.com/Suwayomi/Tachidesk-WebUI/pull/147) by @Robonau)
- (r929) Update LibraryOptions.tsx ([#146](https://github.com/Suwayomi/Tachidesk-WebUI/pull/146) by @Robonau)
# Server: v0.6.1 + WebUI: r911 # Server: v0.6.1 + WebUI: r911
## TL;DR ## TL;DR
- msi and deb packages thanks to @mahor1221 - msi and deb packages thanks to @mahor1221
+16 -9
View File
@@ -49,7 +49,7 @@ Here's a list of known clients/user interfaces for Tachidesk-Server:
- [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-Flutter](https://github.com/Suwayomi/Tachidesk-Flutter): A Flutter front-end for Desktop(Linux, windows, etc.), in 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.
##### 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.
- [Tachidesk-GTK](https://github.com/mahor1221/Tachidesk-GTK): A native Rust/GTK desktop client, in super early stage of development. - [Tachidesk-GTK](https://github.com/mahor1221/Tachidesk-GTK): A native Rust/GTK desktop client, in super early stage of development.
@@ -80,33 +80,40 @@ If a bundle for your operating system or cpu architecture is not provided then r
**Node:** Linux launcher scripts are named a bit differently but work the same. **Node:** Linux launcher scripts are named a bit differently but work the same.
### Windows ### Windows
Download the latest `win32`(Windows 32-bit) or `win64`(Windows 64-bit) release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Tachidesk-preview/releases). Download the latest `win32`(Windows 32-bit) or `win64`(Windows 64-bit) 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.
### 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/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Tachidesk-preview/releases). 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).
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.
### GNU/Linux ### GNU/Linux
Download the latest `linux-x64`(x86_64) release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Tachidesk-preview/releases). Download the latest `linux-x64`(x86_64) 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).
`tar xvf` the downloaded file and double click on one of the launcher scripts or run them using the terminal. `tar xvf` the downloaded file and double click on one of the launcher scripts or run them using the terminal.
## Other methods of getting Tachidesk ## Other methods of getting Tachidesk
### Arch Linux ### Arch Linux
You can install Tachidesk from the AUR You can install Tachidesk from the AUR:
``` ```
yay -S tachidesk yay -S tachidesk
``` ```
### Ubuntu-based distributions ### Debian/Ubuntu
More information can be found on the [PPA's page](https://launchpad.net/~suwayomi/+archive/ubuntu/tachidesk). Download the latest deb package from the release section or Install from the MPR
``` ```
sudo add-apt-repository ppa:suwayomi/tachidesk git clone https://mpr.makedeb.org/tachidesk-server.git
cd tachidesk-server
makedeb -si
```
### Ubuntu
```
sudo add-apt-repository ppa:suwayomi/tachidesk-server
sudo apt update sudo apt update
sudo apt install tachidesk sudo apt install tachidesk-server
``` ```
### Docker ### Docker
+3 -2
View File
@@ -12,9 +12,10 @@ const val kotlinVersion = "1.6.10"
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.1" val tachideskVersion = System.getenv("ProductVersion") ?: "v0.6.4"
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r911" val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r946"
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 {
+292
View File
@@ -0,0 +1,292 @@
#!/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_URL="https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u302-b08/$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_URL="https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u302-b08/$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_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_URL="https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/$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_URL="https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u302-b08/$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
mkdir -p "$RELEASE_NAME/jre/"
local ext="${JRE##*.}"
local jre_dir
if [ "$ext" = "zip" ]; then
jre_dir="$(unzip "$JRE" | sed -n '2p' | cut -d: -f2 | xargs basename)"
mv "$jre_dir" "$RELEASE_NAME/jre"
else
# --strip-components=1: untar an archive without the root folder
tar xvf "$JRE" --strip-components=1 -C "$RELEASE_NAME/jre/"
fi
unzip "$ELECTRON" -d "$RELEASE_NAME/electron/"
}
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() {
local temp_dir
temp_dir="$(mktemp -d)"
trap "rm -rf $temp_dir" RETURN
cp "$JAR" "$RELEASE_NAME/Tachidesk-Server.jar"
tar -I "gzip" -cvf "$RELEASE_NAME.tar.gz" "$RELEASE_NAME/"
#behind $RELEASE_VERSION is underscore "_"
local upstream_source="tachidesk-server_$RELEASE_VERSION.orig.tar.gz"
mv "$RELEASE_NAME.tar.gz" "$temp_dir/$upstream_source"
cp -r "scripts/resources/deb/" "$RELEASE_NAME/debian/"
copy_linux_package_assets_to "$RELEASE_NAME/debian/"
sed -i "s/\$pkgver/$RELEASE_VERSION/" "$RELEASE_NAME/debian/changelog"
sed -i "s/\$pkgrel/1/" "$RELEASE_NAME/debian/changelog"
#behind $RELEASE_VERSION is hyphen "-"
local source_dir="tachidesk-server-$RELEASE_VERSION"
mv "$RELEASE_NAME/" "$temp_dir/$source_dir/"
sudo apt install devscripts build-essential dh-exec
cd "$temp_dir/$source_dir/"
dpkg-buildpackage --no-sign --build=all
cd -
local deb="tachidesk-server_$RELEASE_VERSION-1_all.deb"
mv "$temp_dir/$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
-48
View File
@@ -1,48 +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 debian package"
jar=$(ls ../server/build/*.jar | tail -n1)
release_ver=$(tmp="${jar%-*}" && echo "${tmp##*-}" | tr -d v)
orig_dir="tachidesk-$release_ver" # dir uses hyphen "-"
orig_tar_gz="tachidesk_$release_ver.orig.tar.gz" # orig file uses underscore "_"
package_name="tachidesk_$release_ver-1_all.deb"
# copy artifacts
mkdir "$orig_dir"
cp "$jar" "$orig_dir/Tachidesk.jar"
cp -r "resources/debian" "$orig_dir"
cp "resources/tachidesk-browser-launcher-standalone.sh" "$orig_dir/debian"
cp "resources/tachidesk-debug-launcher-standalone.sh" "$orig_dir/debian"
cp "resources/tachidesk-electron-launcher-standalone.sh" "$orig_dir/debian"
cp "resources/tachidesk.desktop" "$orig_dir/debian"
cp "../server/src/main/resources/icon/faviconlogo.png" "$orig_dir/debian/tachidesk.png"
# prepare
tar cvzf "$orig_tar_gz" "$orig_dir/Tachidesk.jar"
sed -i "s/\${version}/$release_ver/" "$orig_dir/debian/changelog"
# build
mkdir "build"
mv $orig_dir $orig_tar_gz "build/"
cd "build/$orig_dir/debian"
sudo apt install devscripts build-essential dh-exec
# --lintian-opts --profile debian: build Debian packages on Ubuntu
debuild -uc -us --lintian-opts --profile debian
cd -
# clean build directory
mv "build/$package_name" "./"
rm -rf "build"
# clean up from possible previous runs
if [ -f "../server/build/$package_name" ]; then
rm "../server/build/$package_name"
fi
mv "$package_name" "../server/build/"
@@ -1 +1 @@
start "" jre/bin/javaw -jar Tachidesk.jar start "" jre/bin/javaw -jar Tachidesk-Server.jar
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+5
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
+3 -3
View File
@@ -1,12 +1,12 @@
Source: tachidesk 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 (= 12), 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 Package: tachidesk-server
Architecture: all Architecture: all
Depends: ${misc:Depends}, default-jre-headless (>= 8) Depends: ${misc:Depends}, default-jre-headless (>= 8)
Description: Manga Reader Description: Manga Reader
+1 -1
View File
@@ -1,5 +1,5 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: tachidesk Upstream-Name: tachidesk-server
Upstream-Contact: https://discord.gg/DDZdqZWaHA Upstream-Contact: https://discord.gg/DDZdqZWaHA
Source: https://github.com/Suwayomi/Tachidesk-Server Source: https://github.com/Suwayomi/Tachidesk-Server
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/dh-exec
Tachidesk-Server.jar usr/share/java/tachidesk-server/
debian/tachidesk-server.png usr/share/pixmaps/
debian/tachidesk-server.desktop usr/share/applications/
debian/tachidesk-server.service usr/lib/systemd/system/
debian/tachidesk-server.sysusers => usr/lib/sysusers.d/tachidesk-server.conf
debian/tachidesk-server.tmpfiles => usr/lib/tmpfiles.d/tachidesk-server.conf
debian/tachidesk-server.conf => etc/tachidesk/server.conf
debian/tachidesk-server-browser-launcher.sh => usr/bin/tachidesk-server-browser
debian/tachidesk-server-debug-launcher.sh => usr/bin/tachidesk-server-debug
debian/tachidesk-server-electron-launcher.sh => usr/bin/tachidesk-server-electron
@@ -0,0 +1 @@
tachidesk-server.png
-5
View File
@@ -1,5 +0,0 @@
tachidesk (${version}-1) 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
-9
View File
@@ -1,9 +0,0 @@
#!/usr/bin/dh-exec
debian/tachidesk-browser-launcher-standalone.sh => usr/bin/tachidesk
debian/tachidesk-browser-launcher-standalone.sh => usr/bin/tachidesk-browser
debian/tachidesk-debug-launcher-standalone.sh => usr/bin/tachidesk-debug
debian/tachidesk-electron-launcher-standalone.sh => usr/bin/tachidesk-electron
Tachidesk.jar => usr/share/java/tachidesk/tachidesk.jar
debian/tachidesk.png usr/share/pixmaps/
debian/tachidesk.desktop usr/share/applications/
@@ -1 +0,0 @@
debian/tachidesk.png
@@ -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">
@@ -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="*">
@@ -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
@@ -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
@@ -0,0 +1,2 @@
#Type Name ID GECOS Home directory Shell
u tachidesk - "Tachidesk Manga Server" /var/lib/tachidesk
@@ -0,0 +1,2 @@
#Type Path Mode User Group Age Argument
d /var/lib/tachidesk 0755 tachidesk tachidesk
@@ -0,0 +1,3 @@
#!/bin/sh
exec /usr/bin/java -jar /usr/share/java/tachidesk-server/Tachidesk-Server.jar
@@ -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
@@ -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
@@ -0,0 +1,8 @@
[Desktop Entry]
Type=Application
Name=Tachidesk-Server
Comment=Manga Reader
Exec=/usr/bin/java -jar /usr/share/java/tachidesk-server/Tachidesk-Server.jar "\\$@"
Icon=tachidesk-server
Terminal=false
Categories=Network;
@@ -1,2 +0,0 @@
#!/bin/sh
exec /usr/bin/java -jar /usr/share/java/tachidesk/tachidesk.jar
@@ -1,3 +0,0 @@
#!/bin/sh
./jre/bin/java -jar Tachidesk.jar
@@ -1,2 +0,0 @@
#!/bin/sh
exec /usr/bin/java -Dsuwayomi.tachidesk.config.server.debugLogsEnabled=true -jar /usr/share/java/tachidesk/tachidesk.jar
@@ -1,3 +0,0 @@
#!/bin/sh
./jre/bin/java -Dsuwayomi.tachidesk.config.server.debugLogsEnabled=true -jar Tachidesk.jar
@@ -1,14 +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/tachidesk.jar
@@ -1,3 +0,0 @@
#!/bin/sh
./jre/bin/java "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron" "-Dsuwayomi.tachidesk.config.server.electronPath=electron/electron" -jar Tachidesk.jar
@@ -0,0 +1,3 @@
#!/bin/sh
exec ./jre/bin/java -jar ./Tachidesk-Server.jar
@@ -0,0 +1,5 @@
#!/bin/sh
exec ./jre/bin/java \
-Dsuwayomi.tachidesk.config.server.debugLogsEnabled=true \
-jar ./Tachidesk-Server.jar
@@ -0,0 +1,6 @@
#!/bin/sh
exec ./jre/bin/java \
-Dsuwayomi.tachidesk.config.server.webUIInterface=electron \
-Dsuwayomi.tachidesk.config.server.electronPath=./electron/electron \
-jar ./Tachidesk-Server.jar
-8
View File
@@ -1,8 +0,0 @@
[Desktop Entry]
Type=Application
Name=Tachidesk
Comment=Manga Reader
Exec=/usr/bin/java -jar /usr/share/java/tachidesk/tachidesk.jar "\\$@"
Icon=tachidesk
Terminal=false
Categories=Network;
-87
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.jar
if [ $os = linux ]; then
cp "resources/tachidesk-browser-launcher.sh" $release_name
cp "resources/tachidesk-debug-launcher.sh" $release_name
cp "resources/tachidesk-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/
-104
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/
+11 -1
View File
@@ -44,6 +44,7 @@ dependencies {
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.14.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")
@@ -53,11 +54,14 @@ dependencies {
// Disk & File // Disk & File
implementation("net.lingala.zip4j:zip4j:2.9.1") implementation("net.lingala.zip4j:zip4j:2.9.1")
implementation("com.github.junrar:junrar:7.4.0") implementation("com.github.junrar:junrar:7.5.0")
// CloudflareInterceptor // CloudflareInterceptor
implementation("net.sourceforge.htmlunit:htmlunit:2.56.0") implementation("net.sourceforge.htmlunit:htmlunit:2.56.0")
// AES/CBC/PKCS7Padding Cypher provider for zh.copymanga
implementation("org.bouncycastle:bcprov-jdk18on:1.71")
// 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
// implementation("tachiyomi.sourceapi:source-api:1.1") // implementation("tachiyomi.sourceapi:source-api:1.1")
@@ -74,6 +78,9 @@ dependencies {
} }
application { application {
applicationDefaultJvmArgs = listOf(
"-Djunrar.extractor.thread-keep-alive-seconds=30"
)
mainClass.set(MainClass) mainClass.set(MainClass)
} }
@@ -103,6 +110,9 @@ 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"))
@@ -0,0 +1,9 @@
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);
}
@@ -0,0 +1,11 @@
package eu.kanade.tachiyomi
/**
* Used by extensions.
*
* @since extension-lib 1.3
*/
object AppInfo {
fun getVersionCode() = BuildConfig.VERSION_CODE
fun getVersionName() = BuildConfig.VERSION_NAME
}
@@ -1,6 +0,0 @@
package eu.kanade.tachiyomi;
public class BuildConfig {
public static final int VERSION_CODE = -1;
public static final String VERSION_NAME = "stub";
}
@@ -0,0 +1,67 @@
package eu.kanade.tachiyomi.network.interceptor
import android.os.SystemClock
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import java.util.concurrent.TimeUnit
/**
* An OkHttp interceptor that handles rate limiting.
*
* Examples:
*
* permits = 5, period = 1, unit = seconds => 5 requests per second
* permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes
*
* @since extension-lib 1.3
*
* @param permits {Int} Number of requests allowed within a period of units.
* @param period {Long} The limiting duration. Defaults to 1.
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
*/
fun OkHttpClient.Builder.rateLimit(
permits: Int,
period: Long = 1,
unit: TimeUnit = TimeUnit.SECONDS,
) = addInterceptor(RateLimitInterceptor(permits, period, unit))
private class RateLimitInterceptor(
private val permits: Int,
period: Long,
unit: TimeUnit,
) : Interceptor {
private val requestQueue = ArrayList<Long>(permits)
private val rateLimitMillis = unit.toMillis(period)
override fun intercept(chain: Interceptor.Chain): Response {
synchronized(requestQueue) {
val now = SystemClock.elapsedRealtime()
val waitTime = if (requestQueue.size < permits) {
0
} else {
val oldestReq = requestQueue[0]
val newestReq = requestQueue[permits - 1]
if (newestReq - oldestReq > rateLimitMillis) {
0
} else {
oldestReq + rateLimitMillis - now // Remaining time
}
}
if (requestQueue.size == permits) {
requestQueue.removeAt(0)
}
if (waitTime > 0) {
requestQueue.add(now + waitTime)
Thread.sleep(waitTime) // Sleep inside synchronized to pause queued requests
} else {
requestQueue.add(now)
}
}
return chain.proceed(chain.request())
}
}
@@ -0,0 +1,75 @@
package eu.kanade.tachiyomi.network.interceptor
import android.os.SystemClock
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import java.util.concurrent.TimeUnit
/**
* An OkHttp interceptor that handles given url host's rate limiting.
*
* Examples:
*
* httpUrl = "api.manga.com".toHttpUrlOrNull(), permits = 5, period = 1, unit = seconds => 5 requests per second to api.manga.com
* httpUrl = "imagecdn.manga.com".toHttpUrlOrNull(), permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes to imagecdn.manga.com
*
* @since extension-lib 1.3
*
* @param httpUrl {HttpUrl} The url host that this interceptor should handle. Will get url's host by using HttpUrl.host()
* @param permits {Int} Number of requests allowed within a period of units.
* @param period {Long} The limiting duration. Defaults to 1.
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
*/
fun OkHttpClient.Builder.rateLimitHost(
httpUrl: HttpUrl,
permits: Int,
period: Long = 1,
unit: TimeUnit = TimeUnit.SECONDS,
) = addInterceptor(SpecificHostRateLimitInterceptor(httpUrl, permits, period, unit))
class SpecificHostRateLimitInterceptor(
httpUrl: HttpUrl,
private val permits: Int,
period: Long,
unit: TimeUnit,
) : Interceptor {
private val requestQueue = ArrayList<Long>(permits)
private val rateLimitMillis = unit.toMillis(period)
private val host = httpUrl.host
override fun intercept(chain: Interceptor.Chain): Response {
if (chain.request().url.host != host) {
return chain.proceed(chain.request())
}
synchronized(requestQueue) {
val now = SystemClock.elapsedRealtime()
val waitTime = if (requestQueue.size < permits) {
0
} else {
val oldestReq = requestQueue[0]
val newestReq = requestQueue[permits - 1]
if (newestReq - oldestReq > rateLimitMillis) {
0
} else {
oldestReq + rateLimitMillis - now // Remaining time
}
}
if (requestQueue.size == permits) {
requestQueue.removeAt(0)
}
if (waitTime > 0) {
requestQueue.add(now + waitTime)
Thread.sleep(waitTime) // Sleep inside synchronized to pause queued requests
} else {
requestQueue.add(now)
}
}
return chain.proceed(chain.request())
}
}
@@ -0,0 +1,8 @@
package eu.kanade.tachiyomi.source
/**
* A source that explicitly doesn't require traffic considerations.
*
* This typically applies for self-hosted sources.
*/
interface UnmeteredSource
@@ -5,11 +5,10 @@ import com.github.junrar.rarfile.FileHeader
import eu.kanade.tachiyomi.source.model.Page 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.ByteArrayOutputStream
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.util.concurrent.Executors
/** /**
* Loader used to load a chapter from a .rar or .cbr file. * Loader used to load a chapter from a .rar or .cbr file.
@@ -22,20 +21,40 @@ class RarPageLoader(file: File) : PageLoader {
private val archive = Archive(file) private val archive = Archive(file)
/** /**
* Pool for copying compressed files to an input stream. * The fully uncompressed files, to be used in case archive is solid.
*/ */
private val pool = Executors.newFixedThreadPool(1) private var archiveMap = mutableMapOf<FileHeader, InputStream>()
/** /**
* Returns an observable containing the pages found on this rar archive ordered with a natural * Returns an observable containing the pages found on this rar archive ordered with a natural
* comparator. * comparator.
*/ */
override fun getPages(): List<ReaderPage> { override fun getPages(): List<ReaderPage> {
if (archive.mainHeader.isSolid) {
// Solid means that we need to read all the file sequentially
for (header in archive.fileHeaders) {
val baos = ByteArrayOutputStream()
archive.extractFile(header, baos)
archiveMap[header] = ByteArrayInputStream(baos.toByteArray())
}
// After reading the full archive, proceed to filter and transform
return archive.fileHeaders
.filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { archiveMap.getValue(it) } }
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.mapIndexed { i, header ->
val streamFn = { archiveMap.getValue(header) }
ReaderPage(i).apply {
stream = streamFn
status = Page.READY
}
}
}
return archive.fileHeaders return archive.fileHeaders
.filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } } .filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.mapIndexed { i, header -> .mapIndexed { i, header ->
val streamFn = { getStream(header) } val streamFn = { archive.getInputStream(header) }
ReaderPage(i).apply { ReaderPage(i).apply {
stream = streamFn stream = streamFn
@@ -43,21 +62,4 @@ class RarPageLoader(file: File) : PageLoader {
} }
} }
} }
/**
* Returns an input stream for the given [header].
*/
private fun getStream(header: FileHeader): InputStream {
val pipeIn = PipedInputStream()
val pipeOut = PipedOutputStream(pipeIn)
pool.execute {
try {
pipeOut.use {
archive.extractFile(header, it)
}
} catch (e: Exception) {
}
}
return pipeIn
}
} }
@@ -5,7 +5,9 @@ package eu.kanade.tachiyomi.source.model
open class Filter<T>(val name: String, var state: T) { open class Filter<T>(val name: String, var state: T) {
open class Header(name: String) : Filter<Any>(name, 0) open class Header(name: String) : Filter<Any>(name, 0)
open class Separator(name: String = "") : Filter<Any>(name, 0) open class Separator(name: String = "") : Filter<Any>(name, 0)
abstract class Select<V>(name: String, val values: Array<V>, state: Int = 0) : Filter<Int>(name, state) abstract class Select<V>(name: String, val values: Array<V>, state: Int = 0) : Filter<Int>(name, state) {
val displayValues get() = values.map { it.toString() }
}
abstract class Text(name: String, state: String = "") : Filter<String>(name, state) abstract class Text(name: String, state: String = "") : Filter<String>(name, state)
abstract class CheckBox(name: String, state: Boolean = false) : Filter<Boolean>(name, state) abstract class CheckBox(name: String, state: Boolean = false) : Filter<Boolean>(name, state)
abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter<Int>(name, state) { abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter<Int>(name, state) {
@@ -55,6 +55,9 @@ interface SManga : Serializable {
const val ONGOING = 1 const val ONGOING = 1
const val COMPLETED = 2 const val COMPLETED = 2
const val LICENSED = 3 const val LICENSED = 3
const val PUBLISHING_FINISHED = 4
const val CANCELLED = 5
const val ON_HIATUS = 6
fun create(): SManga { fun create(): SManga {
return SMangaImpl() return SMangaImpl()
@@ -14,8 +14,8 @@ import suwayomi.tachidesk.global.controller.SettingsController
object GlobalAPI { object GlobalAPI {
fun defineEndpoints() { fun defineEndpoints() {
path("settings") { path("settings") {
get("about", SettingsController::about) get("about", SettingsController.about)
get("check-update", SettingsController::checkUpdate) get("check-update", SettingsController.checkUpdate)
} }
} }
} }
@@ -7,22 +7,48 @@ package suwayomi.tachidesk.global.controller
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import io.javalin.http.Context import io.javalin.http.HttpCode
import suwayomi.tachidesk.global.impl.About import suwayomi.tachidesk.global.impl.About
import suwayomi.tachidesk.global.impl.AboutDataClass
import suwayomi.tachidesk.global.impl.AppUpdate import suwayomi.tachidesk.global.impl.AppUpdate
import suwayomi.tachidesk.global.impl.UpdateDataClass
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.withOperation
/** Settings Page/Screen */ /** Settings Page/Screen */
object SettingsController { object SettingsController {
/** returns some static info about the current app build */ /** returns some static info about the current app build */
fun about(ctx: Context) { val about = handler(
ctx.json(About.getAbout()) documentWith = {
} withOperation {
summary("About Tachidesk")
description("Returns some static info about the current app build")
}
},
behaviorOf = { ctx ->
ctx.json(About.getAbout())
},
withResults = {
json<AboutDataClass>(HttpCode.OK)
}
)
/** check for app updates */ /** check for app updates */
fun checkUpdate(ctx: Context) { val checkUpdate = handler(
ctx.json( documentWith = {
future { AppUpdate.checkUpdate() } withOperation {
) summary("Tachidesk update check")
} description("Check for app updates")
}
},
behaviorOf = { ctx ->
ctx.json(
future { AppUpdate.checkUpdate() }
)
},
withResults = {
json<UpdateDataClass>(HttpCode.OK)
}
)
} }
@@ -24,98 +24,98 @@ import suwayomi.tachidesk.manga.controller.UpdateController
object MangaAPI { object MangaAPI {
fun defineEndpoints() { fun defineEndpoints() {
path("extension") { path("extension") {
get("list", ExtensionController::list) get("list", ExtensionController.list)
get("install/{pkgName}", ExtensionController::install) get("install/{pkgName}", ExtensionController.install)
post("install", ExtensionController::installFile) post("install", ExtensionController.installFile)
get("update/{pkgName}", ExtensionController::update) get("update/{pkgName}", ExtensionController.update)
get("uninstall/{pkgName}", ExtensionController::uninstall) get("uninstall/{pkgName}", ExtensionController.uninstall)
get("icon/{apkName}", ExtensionController::icon) get("icon/{apkName}", ExtensionController.icon)
} }
path("source") { path("source") {
get("list", SourceController::list) get("list", SourceController.list)
get("{sourceId}", SourceController::retrieve) get("{sourceId}", SourceController.retrieve)
get("{sourceId}/popular/{pageNum}", SourceController::popular) get("{sourceId}/popular/{pageNum}", SourceController.popular)
get("{sourceId}/latest/{pageNum}", SourceController::latest) get("{sourceId}/latest/{pageNum}", SourceController.latest)
get("{sourceId}/preferences", SourceController::getPreferences) get("{sourceId}/preferences", SourceController.getPreferences)
post("{sourceId}/preferences", SourceController::setPreference) post("{sourceId}/preferences", SourceController.setPreference)
get("{sourceId}/filters", SourceController::getFilters) get("{sourceId}/filters", SourceController.getFilters)
post("{sourceId}/filters", SourceController::setFilter) post("{sourceId}/filters", SourceController.setFilters)
get("{sourceId}/search", SourceController::searchSingle) get("{sourceId}/search", SourceController.searchSingle)
// 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}/thumbnail", MangaController::thumbnail) get("{mangaId}/thumbnail", MangaController.thumbnail)
get("{mangaId}/category", MangaController::categoryList) get("{mangaId}/category", MangaController.categoryList)
get("{mangaId}/category/{categoryId}", MangaController::addToCategory) get("{mangaId}/category/{categoryId}", MangaController.addToCategory)
delete("{mangaId}/category/{categoryId}", MangaController::removeFromCategory) delete("{mangaId}/category/{categoryId}", MangaController.removeFromCategory)
get("{mangaId}/library", MangaController::addToLibrary) get("{mangaId}/library", MangaController.addToLibrary)
delete("{mangaId}/library", MangaController::removeFromLibrary) delete("{mangaId}/library", MangaController.removeFromLibrary)
patch("{mangaId}/meta", MangaController::meta) patch("{mangaId}/meta", MangaController.meta)
get("{mangaId}/chapters", MangaController::chapterList) get("{mangaId}/chapters", MangaController.chapterList)
get("{mangaId}/chapter/{chapterIndex}", MangaController::chapterRetrieve) get("{mangaId}/chapter/{chapterIndex}", MangaController.chapterRetrieve)
patch("{mangaId}/chapter/{chapterIndex}", MangaController::chapterModify) patch("{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)
get("{mangaId}/chapter/{chapterIndex}/page/{index}", MangaController::pageRetrieve) get("{mangaId}/chapter/{chapterIndex}/page/{index}", MangaController.pageRetrieve)
} }
path("category") { path("category") {
get("", CategoryController::categoryList) get("", CategoryController.categoryList)
post("", CategoryController::categoryCreate) post("", CategoryController.categoryCreate)
// The order here is important {categoryId} needs to be applied last // The order here is important {categoryId} needs to be applied last
// or throws a NumberFormatException // or throws a NumberFormatException
patch("reorder", CategoryController::categoryReorder) patch("reorder", CategoryController.categoryReorder)
get("{categoryId}", CategoryController::categoryMangas) get("{categoryId}", CategoryController.categoryMangas)
patch("{categoryId}", CategoryController::categoryModify) patch("{categoryId}", CategoryController.categoryModify)
delete("{categoryId}", CategoryController::categoryDelete) delete("{categoryId}", CategoryController.categoryDelete)
} }
path("backup") { path("backup") {
post("import", BackupController::protobufImport) post("import", BackupController.protobufImport)
post("import/file", BackupController::protobufImportFile) post("import/file", BackupController.protobufImportFile)
post("validate", BackupController::protobufValidate) post("validate", BackupController.protobufValidate)
post("validate/file", BackupController::protobufValidateFile) post("validate/file", BackupController.protobufValidateFile)
get("export", BackupController::protobufExport) get("export", BackupController.protobufExport)
get("export/file", BackupController::protobufExportFile) get("export/file", BackupController.protobufExportFile)
} }
path("downloads") { path("downloads") {
ws("", DownloadController::downloadsWS) ws("", DownloadController::downloadsWS)
get("start", DownloadController::start) get("start", DownloadController.start)
get("stop", DownloadController::stop) get("stop", DownloadController.stop)
get("clear", DownloadController::stop) get("clear", DownloadController.stop)
} }
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)
} }
path("update") { path("update") {
get("recentChapters/{pageNum}", UpdateController::recentChapters) get("recentChapters/{pageNum}", UpdateController.recentChapters)
post("fetch", UpdateController::categoryUpdate) post("fetch", UpdateController.categoryUpdate)
post("reset", UpdateController.reset) post("reset", UpdateController.reset)
get("summary", UpdateController::updateSummary) get("summary", UpdateController.updateSummary)
ws("", UpdateController::categoryUpdateWS) ws("", UpdateController::categoryUpdateWS)
} }
} }
@@ -1,11 +1,14 @@
package suwayomi.tachidesk.manga.controller package suwayomi.tachidesk.manga.controller
import io.javalin.http.Context 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
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.withOperation
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
@@ -19,78 +22,153 @@ import java.util.Date
object BackupController { object BackupController {
/** expects a Tachiyomi protobuf backup in the body */ /** expects a Tachiyomi protobuf backup in the body */
fun protobufImport(ctx: Context) { val protobufImport = handler(
ctx.future( documentWith = {
future { withOperation {
ProtoBackupImport.performRestore(ctx.bodyAsInputStream()) summary("Restore a backup")
description("Expects a Tachiyomi protobuf backup in the body")
} }
) },
} behaviorOf = { ctx ->
ctx.future(
future {
ProtoBackupImport.performRestore(ctx.bodyAsInputStream())
}
)
},
withResults = {
httpCode(HttpCode.OK)
}
)
/** expects a Tachiyomi protobuf backup as a file upload, the file must be named "backup.proto.gz" */ /** expects a Tachiyomi protobuf backup as a file upload, the file must be named "backup.proto.gz" */
fun protobufImportFile(ctx: Context) { val protobufImportFile = handler(
// TODO: rewrite this with ctx.uploadedFiles(), don't call the multipart field "backup.proto.gz" documentWith = {
ctx.future( withOperation {
future { summary("Restore a backup file")
ProtoBackupImport.performRestore(ctx.uploadedFile("backup.proto.gz")!!.content) description("Expects a Tachiyomi protobuf backup as a file upload, the file must be named \"backup.proto.gz\"")
} }
) uploadedFile("backup.proto.gz") {
} it.description("Protobuf backup")
it.required(true)
}
},
behaviorOf = { ctx ->
// TODO: rewrite this with ctx.uploadedFiles(), don't call the multipart field "backup.proto.gz"
ctx.future(
future {
ProtoBackupImport.performRestore(ctx.uploadedFile("backup.proto.gz")!!.content)
}
)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
/** returns a Tachiyomi protobuf backup created from the current database as a body */ /** returns a Tachiyomi protobuf backup created from the current database as a body */
fun protobufExport(ctx: Context) { val protobufExport = handler(
ctx.contentType("application/octet-stream") documentWith = {
ctx.future( withOperation {
future { summary("Create a backup")
ProtoBackupExport.createBackup( description("Returns a Tachiyomi protobuf backup created from the current database as a body")
BackupFlags(
includeManga = true,
includeCategories = true,
includeChapters = true,
includeTracking = true,
includeHistory = true,
)
)
} }
) },
} behaviorOf = { ctx ->
ctx.contentType("application/octet-stream")
ctx.future(
future {
ProtoBackupExport.createBackup(
BackupFlags(
includeManga = true,
includeCategories = true,
includeChapters = true,
includeTracking = true,
includeHistory = true,
)
)
}
)
},
withResults = {
mime(HttpCode.OK, "application/octet-stream")
}
)
/** returns a Tachiyomi protobuf backup created from the current database as a file */ /** returns a Tachiyomi protobuf backup created from the current database as a file */
fun protobufExportFile(ctx: Context) { val protobufExportFile = handler(
ctx.contentType("application/octet-stream") documentWith = {
val currentDate = SimpleDateFormat("yyyy-MM-dd_HH-mm").format(Date()) withOperation {
summary("Create a backup file")
ctx.header("Content-Disposition", """attachment; filename="tachidesk_$currentDate.proto.gz"""") description("Returns a Tachiyomi protobuf backup created from the current database as a file")
ctx.future(
future {
ProtoBackupExport.createBackup(
BackupFlags(
includeManga = true,
includeCategories = true,
includeChapters = true,
includeTracking = true,
includeHistory = true,
)
)
} }
) },
} behaviorOf = { ctx ->
ctx.contentType("application/octet-stream")
val currentDate = SimpleDateFormat("yyyy-MM-dd_HH-mm").format(Date())
ctx.header("Content-Disposition", """attachment; filename="tachidesk_$currentDate.proto.gz"""")
ctx.future(
future {
ProtoBackupExport.createBackup(
BackupFlags(
includeManga = true,
includeCategories = true,
includeChapters = true,
includeTracking = true,
includeHistory = true,
)
)
}
)
},
withResults = {
mime(HttpCode.OK, "application/octet-stream")
}
)
/** Reports missing sources and trackers, expects a Tachiyomi protobuf backup in the body */ /** Reports missing sources and trackers, expects a Tachiyomi protobuf backup in the body */
fun protobufValidate(ctx: Context) { val protobufValidate = handler(
ctx.future( documentWith = {
future { withOperation {
ProtoBackupValidator.validate(ctx.bodyAsInputStream()) summary("Validate a backup")
description("Reports missing sources and trackers, expects a Tachiyomi protobuf backup in the body")
} }
) },
} behaviorOf = { ctx ->
ctx.future(
future {
ProtoBackupValidator.validate(ctx.bodyAsInputStream())
}
)
},
withResults = {
json<AbstractBackupValidator.ValidationResult>(HttpCode.OK)
}
)
/** Reports missing sources and trackers, expects a Tachiyomi protobuf backup as a file upload, the file must be named "backup.proto.gz" */ /** Reports missing sources and trackers, expects a Tachiyomi protobuf backup as a file upload, the file must be named "backup.proto.gz" */
fun protobufValidateFile(ctx: Context) { val protobufValidateFile = handler(
ctx.future( documentWith = {
future { withOperation {
ProtoBackupValidator.validate(ctx.uploadedFile("backup.proto.gz")!!.content) summary("Validate a backup file")
description("Reports missing sources and trackers, expects a Tachiyomi protobuf backup as a file upload, the file must be named \"backup.proto.gz\"")
} }
) uploadedFile("backup.proto.gz") {
} it.description("Protobuf backup")
it.required(true)
}
},
behaviorOf = { ctx ->
ctx.future(
future {
ProtoBackupValidator.validate(ctx.uploadedFile("backup.proto.gz")!!.content)
}
)
},
withResults = {
json<AbstractBackupValidator.ValidationResult>(HttpCode.OK)
}
)
} }
@@ -7,50 +7,126 @@ package suwayomi.tachidesk.manga.controller
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import io.javalin.http.Context import io.javalin.http.HttpCode
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.model.dataclass.CategoryDataClass
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.server.util.formParam
import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.pathParam
import suwayomi.tachidesk.server.util.withOperation
object CategoryController { object CategoryController {
/** category list */ /** category list */
fun categoryList(ctx: Context) { val categoryList = handler(
ctx.json(Category.getCategoryList()) documentWith = {
} withOperation {
summary("Category list")
description("get a list of categories")
}
},
behaviorOf = { ctx ->
ctx.json(Category.getCategoryList())
},
withResults = {
json<Array<CategoryDataClass>>(HttpCode.OK)
}
)
/** category create */ /** category create */
fun categoryCreate(ctx: Context) { val categoryCreate = handler(
val name = ctx.formParam("name")!! formParam<String>("name"),
Category.createCategory(name) documentWith = {
ctx.status(200) withOperation {
} summary("Category create")
description("Create a category")
}
},
behaviorOf = { ctx, name ->
if (Category.createCategory(name) != -1) {
ctx.status(200)
} else {
ctx.status(HttpCode.BAD_REQUEST)
}
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.BAD_REQUEST)
}
)
/** category modification */ /** category modification */
fun categoryModify(ctx: Context) { val categoryModify = handler(
val categoryId = ctx.pathParam("categoryId").toInt() pathParam<Int>("categoryId"),
val name = ctx.formParam("name") formParam<String?>("name"),
val isDefault = ctx.formParam("default")?.toBoolean() formParam<Boolean?>("default"),
Category.updateCategory(categoryId, name, isDefault) documentWith = {
ctx.status(200) withOperation {
} summary("Category modify")
description("Modify a category")
}
},
behaviorOf = { ctx, categoryId, name, isDefault ->
Category.updateCategory(categoryId, name, isDefault)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
}
)
/** category delete */ /** category delete */
fun categoryDelete(ctx: Context) { val categoryDelete = handler(
val categoryId = ctx.pathParam("categoryId").toInt() pathParam<Int>("categoryId"),
Category.removeCategory(categoryId) documentWith = {
ctx.status(200) withOperation {
} summary("Category delete")
description("Delete a category")
}
},
behaviorOf = { ctx, categoryId ->
Category.removeCategory(categoryId)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
}
)
/** returns the manga list associated with a category */ /** returns the manga list associated with a category */
fun categoryMangas(ctx: Context) { val categoryMangas = handler(
val categoryId = ctx.pathParam("categoryId").toInt() pathParam<Int>("categoryId"),
ctx.json(CategoryManga.getCategoryMangaList(categoryId)) documentWith = {
} withOperation {
summary("Category manga")
description("Returns the manga list associated with a category")
}
},
behaviorOf = { ctx, categoryId ->
ctx.json(CategoryManga.getCategoryMangaList(categoryId))
},
withResults = {
json<Array<MangaDataClass>>(HttpCode.OK)
}
)
/** category re-ordering */ /** category re-ordering */
fun categoryReorder(ctx: Context) { val categoryReorder = handler(
val from = ctx.formParam("from")!!.toInt() formParam<Int>("from"),
val to = ctx.formParam("to")!!.toInt() formParam<Int>("to"),
Category.reorderCategory(from, to) documentWith = {
ctx.status(200) withOperation {
} summary("Category re-ordering")
description("Re-order a category")
}
},
behaviorOf = { ctx, from, to ->
Category.reorderCategory(from, to)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
}
)
} }
@@ -7,10 +7,13 @@ package suwayomi.tachidesk.manga.controller
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import io.javalin.http.Context import io.javalin.http.HttpCode
import io.javalin.websocket.WsConfig import io.javalin.websocket.WsConfig
import suwayomi.tachidesk.manga.impl.download.DownloadManager import suwayomi.tachidesk.manga.impl.download.DownloadManager
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.pathParam
import suwayomi.tachidesk.server.util.withOperation
object DownloadController { object DownloadController {
/** Download queue stats */ /** Download queue stats */
@@ -28,45 +31,99 @@ object DownloadController {
} }
/** Start the downloader */ /** Start the downloader */
fun start(ctx: Context) { val start = handler(
DownloadManager.start() documentWith = {
withOperation {
summary("Downloader start")
description("Start the downloader")
}
},
behaviorOf = { ctx ->
DownloadManager.start()
ctx.status(200) ctx.status(200)
} },
withResults = {
httpCode(HttpCode.OK)
}
)
/** Stop the downloader */ /** Stop the downloader */
fun stop(ctx: Context) { val stop = handler(
DownloadManager.stop() documentWith = {
withOperation {
summary("Downloader stop")
description("Stop the downloader")
}
},
behaviorOf = { ctx ->
DownloadManager.stop()
ctx.status(200) ctx.status(200)
} },
withResults = {
httpCode(HttpCode.OK)
}
)
/** clear download queue */ /** clear download queue */
fun clear(ctx: Context) { val clear = handler(
DownloadManager.clear() documentWith = {
withOperation {
summary("Downloader clear")
description("Clear download queue")
}
},
behaviorOf = { ctx ->
DownloadManager.clear()
ctx.status(200) ctx.status(200)
} },
withResults = {
httpCode(HttpCode.OK)
}
)
/** Queue chapter for download */ /** Queue chapter for download */
fun queueChapter(ctx: Context) { val queueChapter = handler(
val chapterIndex = ctx.pathParam("chapterIndex").toInt() pathParam<Int>("chapterIndex"),
val mangaId = ctx.pathParam("mangaId").toInt() pathParam<Int>("mangaId"),
documentWith = {
ctx.future( withOperation {
future { summary("Downloader add chapter")
DownloadManager.enqueue(chapterIndex, mangaId) description("Queue chapter for download")
} }
) },
} behaviorOf = { ctx, chapterIndex, mangaId ->
ctx.future(
future {
DownloadManager.enqueue(chapterIndex, mangaId)
}
)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
/** delete chapter from download queue */ /** delete chapter from download queue */
fun unqueueChapter(ctx: Context) { val unqueueChapter = handler(
val chapterIndex = ctx.pathParam("chapterIndex").toInt() pathParam<Int>("chapterIndex"),
val mangaId = ctx.pathParam("mangaId").toInt() pathParam<Int>("mangaId"),
documentWith = {
withOperation {
summary("Downloader remove chapter")
description("Delete chapter from download queue")
}
},
behaviorOf = { ctx, chapterIndex, mangaId ->
DownloadManager.unqueue(chapterIndex, mangaId)
DownloadManager.unqueue(chapterIndex, mangaId) ctx.status(200)
},
ctx.status(200) withResults = {
} httpCode(HttpCode.OK)
}
)
} }
@@ -7,78 +7,159 @@ package suwayomi.tachidesk.manga.controller
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import io.javalin.http.Context import io.javalin.http.HttpCode
import mu.KotlinLogging import mu.KotlinLogging
import suwayomi.tachidesk.manga.impl.extension.Extension import suwayomi.tachidesk.manga.impl.extension.Extension
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList import suwayomi.tachidesk.manga.impl.extension.ExtensionsList
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.pathParam
import suwayomi.tachidesk.server.util.queryParam
import suwayomi.tachidesk.server.util.withOperation
object ExtensionController { object ExtensionController {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
/** list all extensions */ /** list all extensions */
fun list(ctx: Context) { val list = handler(
ctx.future( documentWith = {
future { withOperation {
ExtensionsList.getExtensionList() summary("Extension list")
description("List all extensions")
} }
) },
} behaviorOf = { ctx ->
ctx.future(
future {
ExtensionsList.getExtensionList()
}
)
},
withResults = {
json<Array<ExtensionDataClass>>(HttpCode.OK)
}
)
/** install extension identified with "pkgName" */ /** install extension identified with "pkgName" */
fun install(ctx: Context) { val install = handler(
val pkgName = ctx.pathParam("pkgName") pathParam<String>("pkgName"),
documentWith = {
ctx.future( withOperation {
future { summary("Extension install")
Extension.installExtension(pkgName) description("install extension identified with \"pkgName\"")
} }
) },
} behaviorOf = { ctx, pkgName ->
ctx.future(
future {
Extension.installExtension(pkgName)
}
)
},
withResults = {
httpCode(HttpCode.CREATED)
httpCode(HttpCode.FOUND)
httpCode(HttpCode.INTERNAL_SERVER_ERROR)
}
)
/** install the uploaded apk file */ /** install the uploaded apk file */
fun installFile(ctx: Context) { val installFile = handler(
documentWith = {
val uploadedFile = ctx.uploadedFile("file")!! withOperation {
logger.debug { "Uploaded extension file name: " + uploadedFile.filename } summary("Extension install apk")
description("Install the uploaded apk file")
ctx.future(
future {
Extension.installExternalExtension(uploadedFile.content, uploadedFile.filename)
} }
) uploadedFile("file") {
} it.description("Extension apk")
it.required(true)
}
},
behaviorOf = { ctx ->
val uploadedFile = ctx.uploadedFile("file")!!
logger.debug { "Uploaded extension file name: " + uploadedFile.filename }
ctx.future(
future {
Extension.installExternalExtension(uploadedFile.content, uploadedFile.filename)
}
)
},
withResults = {
httpCode(HttpCode.CREATED)
httpCode(HttpCode.FOUND)
httpCode(HttpCode.INTERNAL_SERVER_ERROR)
}
)
/** update extension identified with "pkgName" */ /** update extension identified with "pkgName" */
fun update(ctx: Context) { val update = handler(
val pkgName = ctx.pathParam("pkgName") pathParam<String>("pkgName"),
documentWith = {
ctx.future( withOperation {
future { summary("Extension update")
Extension.updateExtension(pkgName) description("Update extension identified with \"pkgName\"")
} }
) },
} behaviorOf = { ctx, pkgName ->
ctx.future(
future {
Extension.updateExtension(pkgName)
}
)
},
withResults = {
httpCode(HttpCode.CREATED)
httpCode(HttpCode.FOUND)
httpCode(HttpCode.NOT_FOUND)
httpCode(HttpCode.INTERNAL_SERVER_ERROR)
}
)
/** uninstall extension identified with "pkgName" */ /** uninstall extension identified with "pkgName" */
fun uninstall(ctx: Context) { val uninstall = handler(
val pkgName = ctx.pathParam("pkgName") pathParam<String>("pkgName"),
documentWith = {
Extension.uninstallExtension(pkgName) withOperation {
ctx.status(200) summary("Extension uninstall")
} description("Uninstall extension identified with \"pkgName\"")
}
},
behaviorOf = { ctx, pkgName ->
Extension.uninstallExtension(pkgName)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.CREATED)
httpCode(HttpCode.FOUND)
httpCode(HttpCode.NOT_FOUND)
httpCode(HttpCode.INTERNAL_SERVER_ERROR)
}
)
/** icon for extension named `apkName` */ /** icon for extension named `apkName` */
fun icon(ctx: Context) { val icon = handler(
val apkName = ctx.pathParam("apkName") pathParam<String>("apkName"),
val useCache = ctx.queryParam("useCache")?.toBoolean() ?: true queryParam("useCache", true),
documentWith = {
ctx.future( withOperation {
future { Extension.getExtensionIcon(apkName, useCache) } summary("Extension icon")
.thenApply { description("Icon for extension named `apkName`")
ctx.header("content-type", it.second) }
it.first },
} behaviorOf = { ctx, apkName, useCache ->
) ctx.future(
} future { Extension.getExtensionIcon(apkName, useCache) }
.thenApply {
ctx.header("content-type", it.second)
it.first
}
)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
} }
@@ -7,7 +7,6 @@ package suwayomi.tachidesk.manga.controller
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import io.javalin.http.Context
import io.javalin.http.HttpCode import io.javalin.http.HttpCode
import suwayomi.tachidesk.manga.impl.CategoryManga import suwayomi.tachidesk.manga.impl.CategoryManga
import suwayomi.tachidesk.manga.impl.Chapter import suwayomi.tachidesk.manga.impl.Chapter
@@ -15,8 +14,11 @@ import suwayomi.tachidesk.manga.impl.Library
import suwayomi.tachidesk.manga.impl.Manga import suwayomi.tachidesk.manga.impl.Manga
import suwayomi.tachidesk.manga.impl.Page import suwayomi.tachidesk.manga.impl.Page
import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReady import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReady
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.util.formParam
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
@@ -30,7 +32,7 @@ object MangaController {
documentWith = { documentWith = {
withOperation { withOperation {
summary("Get a manga") summary("Get a manga")
description("Get a manga from the database using a specific id") description("Get a manga from the database using a specific id.")
} }
}, },
behaviorOf = { ctx, mangaId, onlineFetch -> behaviorOf = { ctx, mangaId, onlineFetch ->
@@ -47,140 +49,278 @@ object MangaController {
) )
/** manga thumbnail */ /** manga thumbnail */
fun thumbnail(ctx: Context) { val thumbnail = handler(
val mangaId = ctx.pathParam("mangaId").toInt() pathParam<Int>("mangaId"),
val useCache = ctx.queryParam("useCache")?.toBoolean() ?: true queryParam("useCache", true),
documentWith = {
ctx.future( withOperation {
future { Manga.getMangaThumbnail(mangaId, useCache) } summary("Get a manga thumbnail")
.thenApply { description("Get a manga thumbnail from the source or the cache.")
ctx.header("content-type", it.second) }
val httpCacheSeconds = 60 * 60 * 24 },
ctx.header("cache-control", "max-age=$httpCacheSeconds") behaviorOf = { ctx, mangaId, useCache ->
it.first ctx.future(
} future { Manga.getMangaThumbnail(mangaId, useCache) }
) .thenApply {
} ctx.header("content-type", it.second)
val httpCacheSeconds = 60 * 60 * 24
ctx.header("cache-control", "max-age=$httpCacheSeconds")
it.first
}
)
},
withResults = {
mime(HttpCode.OK, "image/*")
httpCode(HttpCode.NOT_FOUND)
}
)
/** adds the manga to library */ /** adds the manga to library */
fun addToLibrary(ctx: Context) { val addToLibrary = handler(
val mangaId = ctx.pathParam("mangaId").toInt() pathParam<Int>("mangaId"),
documentWith = {
ctx.future( withOperation {
future { Library.addMangaToLibrary(mangaId) } summary("Add manga to library")
) description("Use a manga id to add the manga to your library.\nWill do nothing if manga is already in your library.")
} }
},
behaviorOf = { ctx, mangaId ->
ctx.future(
future { Library.addMangaToLibrary(mangaId) }
)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
/** removes the manga from the library */ /** removes the manga from the library */
fun removeFromLibrary(ctx: Context) { val removeFromLibrary = handler(
val mangaId = ctx.pathParam("mangaId").toInt() pathParam<Int>("mangaId"),
documentWith = {
ctx.future( withOperation {
future { Library.removeMangaFromLibrary(mangaId) } summary("Remove manga to library")
) description("Use a manga id to remove the manga to your library.\nWill do nothing if manga not in your library.")
} }
},
behaviorOf = { ctx, mangaId ->
ctx.future(
future { Library.removeMangaFromLibrary(mangaId) }
)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
/** list manga's categories */ /** list manga's categories */
fun categoryList(ctx: Context) { val categoryList = handler(
val mangaId = ctx.pathParam("mangaId").toInt() pathParam<Int>("mangaId"),
ctx.json(CategoryManga.getMangaCategories(mangaId)) documentWith = {
} withOperation {
summary("Get a manga's categories")
description("Get the list of categories for this manga")
}
},
behaviorOf = { ctx, mangaId ->
ctx.json(CategoryManga.getMangaCategories(mangaId))
},
withResults = {
json<Array<CategoryDataClass>>(HttpCode.OK)
}
)
/** adds the manga to category */ /** adds the manga to category */
fun addToCategory(ctx: Context) { val addToCategory = handler(
val mangaId = ctx.pathParam("mangaId").toInt() pathParam<Int>("mangaId"),
val categoryId = ctx.pathParam("categoryId").toInt() pathParam<Int>("categoryId"),
CategoryManga.addMangaToCategory(mangaId, categoryId) documentWith = {
ctx.status(200) withOperation {
} summary("Add manga to category")
description("Add a manga to a category using their ids.")
}
},
behaviorOf = { ctx, mangaId, categoryId ->
CategoryManga.addMangaToCategory(mangaId, categoryId)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
}
)
/** removes the manga from the category */ /** removes the manga from the category */
fun removeFromCategory(ctx: Context) { val removeFromCategory = handler(
val mangaId = ctx.pathParam("mangaId").toInt() pathParam<Int>("mangaId"),
val categoryId = ctx.pathParam("categoryId").toInt() pathParam<Int>("categoryId"),
CategoryManga.removeMangaFromCategory(mangaId, categoryId) documentWith = {
ctx.status(200) withOperation {
} summary("Remove manga from category")
description("Remove a manga from a category using their ids.")
}
},
behaviorOf = { ctx, mangaId, categoryId ->
CategoryManga.removeMangaFromCategory(mangaId, categoryId)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
}
)
/** used to modify a manga's meta parameters */ /** used to modify a manga's meta parameters */
fun meta(ctx: Context) { val meta = handler(
val mangaId = ctx.pathParam("mangaId").toInt() pathParam<Int>("mangaId"),
formParam<String>("key"),
val key = ctx.formParam("key")!! formParam<String>("value"),
val value = ctx.formParam("value")!! documentWith = {
withOperation {
Manga.modifyMangaMeta(mangaId, key, value) summary("Add data to manga")
description("A simple Key-Value storage in the manga object, you can set values for whatever you want inside it.")
ctx.status(200) }
} },
behaviorOf = { ctx, mangaId, key, value ->
Manga.modifyMangaMeta(mangaId, key, value)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
/** get chapter list when showing a manga */ /** get chapter list when showing a manga */
fun chapterList(ctx: Context) { val chapterList = handler(
val mangaId = ctx.pathParam("mangaId").toInt() pathParam<Int>("mangaId"),
queryParam("onlineFetch", false),
val onlineFetch = ctx.queryParam("onlineFetch")?.toBoolean() ?: false documentWith = {
withOperation {
ctx.future(future { Chapter.getChapterList(mangaId, onlineFetch) }) summary("Get manga chapter list")
} description("Get the manga chapter list from the database or online. If there is no chapters in the database it fetches the chapters online. Use onlineFetch to update chapter list.")
}
},
behaviorOf = { ctx, mangaId, onlineFetch ->
ctx.future(future { Chapter.getChapterList(mangaId, onlineFetch) })
},
withResults = {
json<Array<ChapterDataClass>>(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
/** 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 */
fun chapterRetrieve(ctx: Context) { val chapterRetrieve = handler(
val chapterIndex = ctx.pathParam("chapterIndex").toInt() pathParam<Int>("mangaId"),
val mangaId = ctx.pathParam("mangaId").toInt() pathParam<Int>("chapterIndex"),
ctx.future(future { getChapterDownloadReady(chapterIndex, mangaId) }) documentWith = {
} withOperation {
summary("Get a chapter")
description("Get the chapter from the manga id and chapter index. It will also retrieve the pages for this chapter.")
}
},
behaviorOf = { ctx, mangaId, chapterIndex ->
ctx.future(future { getChapterDownloadReady(chapterIndex, mangaId) })
},
withResults = {
json<ChapterDataClass>(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
/** used to modify a chapter's parameters */ /** used to modify a chapter's parameters */
fun chapterModify(ctx: Context) { val chapterModify = handler(
val chapterIndex = ctx.pathParam("chapterIndex").toInt() pathParam<Int>("mangaId"),
val mangaId = ctx.pathParam("mangaId").toInt() pathParam<Int>("chapterIndex"),
formParam<Boolean?>("read"),
formParam<Boolean?>("bookmarked"),
formParam<Boolean?>("markPrevRead"),
formParam<Int?>("lastPageRead"),
documentWith = {
withOperation {
summary("Modify a chapter")
description("Update user info for a given chapter, such as read status, bookmarked, and more.")
}
},
behaviorOf = { ctx, mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead ->
Chapter.modifyChapter(mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead)
val read = ctx.formParam("read")?.toBoolean() ctx.status(200)
val bookmarked = ctx.formParam("bookmarked")?.toBoolean() },
val markPrevRead = ctx.formParam("markPrevRead")?.toBoolean() withResults = {
val lastPageRead = ctx.formParam("lastPageRead")?.toInt() httpCode(HttpCode.OK)
}
Chapter.modifyChapter(mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead) )
ctx.status(200)
}
/** delete a downloaded chapter */ /** delete a downloaded chapter */
fun chapterDelete(ctx: Context) { val chapterDelete = handler(
val chapterIndex = ctx.pathParam("chapterIndex").toInt() pathParam<Int>("mangaId"),
val mangaId = ctx.pathParam("mangaId").toInt() pathParam<Int>("chapterIndex"),
documentWith = {
withOperation {
summary("Delete a chapter download")
description("Delete the downloaded chapter and its files.")
}
},
behaviorOf = { ctx, mangaId, chapterIndex ->
Chapter.deleteChapter(mangaId, chapterIndex)
Chapter.deleteChapter(mangaId, chapterIndex) ctx.status(200)
},
ctx.status(200) withResults = {
} httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
/** used to modify a chapter's meta parameters */ /** used to modify a chapter's meta parameters */
fun chapterMeta(ctx: Context) { val chapterMeta = handler(
val chapterIndex = ctx.pathParam("chapterIndex").toInt() pathParam<Int>("mangaId"),
val mangaId = ctx.pathParam("mangaId").toInt() pathParam<Int>("chapterIndex"),
formParam<String>("key"),
formParam<String>("value"),
documentWith = {
withOperation {
summary("Add data to chapter")
description("A simple Key-Value storage in the chapter object, you can set values for whatever you want inside it.")
}
},
behaviorOf = { ctx, mangaId, chapterIndex, key, value ->
Chapter.modifyChapterMeta(mangaId, chapterIndex, key, value)
val key = ctx.formParam("key")!! ctx.status(200)
val value = ctx.formParam("value")!! },
withResults = {
Chapter.modifyChapterMeta(mangaId, chapterIndex, key, value) httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
ctx.status(200) }
} )
/** get page at index "index" */ /** get page at index "index" */
fun pageRetrieve(ctx: Context) { val pageRetrieve = handler(
val mangaId = ctx.pathParam("mangaId").toInt() pathParam<Int>("mangaId"),
val chapterIndex = ctx.pathParam("chapterIndex").toInt() pathParam<Int>("chapterIndex"),
val index = ctx.pathParam("index").toInt() pathParam<Int>("index"),
val useCache = ctx.queryParam("useCache")?.toBoolean() ?: true queryParam("useCache", true),
documentWith = {
ctx.future( withOperation {
future { Page.getPageImage(mangaId, chapterIndex, index, useCache) } summary("Get a chapter page")
.thenApply { description("Get a chapter page for a given index. Cache use can be disabled so it only retrieves it directly from the source.")
ctx.header("content-type", it.second) }
it.first },
} behaviorOf = { ctx, mangaId, chapterIndex, index, useCache ->
) ctx.future(
} future { Page.getPageImage(mangaId, chapterIndex, index, useCache) }
.thenApply {
ctx.header("content-type", it.second)
it.first
}
)
},
withResults = {
mime(HttpCode.OK, "image/*")
httpCode(HttpCode.NOT_FOUND)
}
)
} }
@@ -7,87 +7,215 @@ package suwayomi.tachidesk.manga.controller
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import io.javalin.http.Context 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.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.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.SourceDataClass
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.pathParam
import suwayomi.tachidesk.server.util.queryParam
import suwayomi.tachidesk.server.util.withOperation
object SourceController { object SourceController {
/** list of sources */ /** list of sources */
fun list(ctx: Context) { val list = handler(
ctx.json(Source.getSourceList()) documentWith = {
} withOperation {
summary("Sources list")
description("List of sources")
}
},
behaviorOf = { ctx ->
ctx.json(Source.getSourceList())
},
withResults = {
json<Array<SourceDataClass>>(HttpCode.OK)
}
)
/** fetch source with id `sourceId` */ /** fetch source with id `sourceId` */
fun retrieve(ctx: Context) { val retrieve = handler(
val sourceId = ctx.pathParam("sourceId").toLong() pathParam<Long>("sourceId"),
ctx.json(Source.getSource(sourceId)) documentWith = {
} withOperation {
summary("Source fetch")
description("Fetch source with id `sourceId`")
}
},
behaviorOf = { ctx, sourceId ->
ctx.json(Source.getSource(sourceId)!!)
},
withResults = {
json<SourceDataClass>(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
/** popular mangas from source with id `sourceId` */ /** popular mangas from source with id `sourceId` */
fun popular(ctx: Context) { val popular = handler(
val sourceId = ctx.pathParam("sourceId").toLong() pathParam<Long>("sourceId"),
val pageNum = ctx.pathParam("pageNum").toInt() pathParam<Int>("pageNum"),
ctx.future( documentWith = {
future { withOperation {
MangaList.getMangaList(sourceId, pageNum, popular = true) summary("Source popular manga")
description("Popular mangas from source with id `sourceId`")
} }
) },
} behaviorOf = { ctx, sourceId, pageNum ->
ctx.future(
future {
MangaList.getMangaList(sourceId, pageNum, popular = true)
}
)
},
withResults = {
json<PagedMangaListDataClass>(HttpCode.OK)
}
)
/** latest mangas from source with id `sourceId` */ /** latest mangas from source with id `sourceId` */
fun latest(ctx: Context) { val latest = handler(
val sourceId = ctx.pathParam("sourceId").toLong() pathParam<Long>("sourceId"),
val pageNum = ctx.pathParam("pageNum").toInt() pathParam<Int>("pageNum"),
ctx.future( documentWith = {
future { withOperation {
MangaList.getMangaList(sourceId, pageNum, popular = false) summary("Source latest manga")
description("Latest mangas from source with id `sourceId`")
} }
) },
} behaviorOf = { ctx, sourceId, pageNum ->
ctx.future(
future {
MangaList.getMangaList(sourceId, pageNum, popular = false)
}
)
},
withResults = {
json<PagedMangaListDataClass>(HttpCode.OK)
}
)
/** fetch preferences of source with id `sourceId` */ /** fetch preferences of source with id `sourceId` */
fun getPreferences(ctx: Context) { val getPreferences = handler(
val sourceId = ctx.pathParam("sourceId").toLong() pathParam<Long>("sourceId"),
ctx.json(Source.getSourcePreferences(sourceId)) documentWith = {
} withOperation {
summary("Source preferences")
description("Fetch preferences of source with id `sourceId`")
}
},
behaviorOf = { ctx, sourceId ->
ctx.json(Source.getSourcePreferences(sourceId))
},
withResults = {
json<Array<Source.PreferenceObject>>(HttpCode.OK)
}
)
/** set one preference of source with id `sourceId` */ /** set one preference of source with id `sourceId` */
fun setPreference(ctx: Context) { val setPreference = handler(
val sourceId = ctx.pathParam("sourceId").toLong() pathParam<Long>("sourceId"),
val preferenceChange = ctx.bodyAsClass(SourcePreferenceChange::class.java) documentWith = {
ctx.json(Source.setSourcePreference(sourceId, preferenceChange)) withOperation {
} summary("Source preference set")
description("Set one preference of source with id `sourceId`")
}
},
behaviorOf = { ctx, sourceId ->
val preferenceChange = ctx.bodyAsClass(SourcePreferenceChange::class.java)
ctx.json(Source.setSourcePreference(sourceId, preferenceChange))
},
withResults = {
httpCode(HttpCode.OK)
}
)
/** fetch filters of source with id `sourceId` */ /** fetch filters of source with id `sourceId` */
fun getFilters(ctx: Context) { val getFilters = handler(
val sourceId = ctx.pathParam("sourceId").toLong() pathParam<Long>("sourceId"),
val reset = ctx.queryParam("reset")?.toBoolean() ?: false queryParam("reset", false),
ctx.json(Search.getFilterList(sourceId, reset)) documentWith = {
} withOperation {
summary("Source filters")
description("Fetch filters of source with id `sourceId`")
}
},
behaviorOf = { ctx, sourceId, reset ->
ctx.json(Search.getFilterList(sourceId, reset))
},
withResults = {
json<Array<Search.FilterObject>>(HttpCode.OK)
}
)
/** set one filter of source with id `sourceId` */ private val json by DI.global.instance<Json>()
fun setFilter(ctx: Context) {
val sourceId = ctx.pathParam("sourceId").toLong()
val filterChange = ctx.bodyAsClass(FilterChange::class.java)
ctx.json(Search.setFilter(sourceId, filterChange)) /** change filters of source with id `sourceId` */
} val setFilters = handler(
pathParam<Long>("sourceId"),
documentWith = {
withOperation {
summary("Source filters set")
description("Change filters of source with id `sourceId`")
}
},
behaviorOf = { ctx, sourceId ->
val filterChange = try {
json.decodeFromString<List<FilterChange>>(ctx.body())
} catch (e: Exception) {
listOf(json.decodeFromString<FilterChange>(ctx.body()))
}
ctx.json(Search.setFilter(sourceId, filterChange))
},
withResults = {
httpCode(HttpCode.OK)
}
)
/** single source search */ /** single source search */
fun searchSingle(ctx: Context) { val searchSingle = handler(
val sourceId = ctx.pathParam("sourceId").toLong() pathParam<Long>("sourceId"),
val searchTerm = ctx.queryParam("searchTerm") ?: "" queryParam("searchTerm", ""),
val pageNum = ctx.queryParam("pageNum")?.toInt() ?: 1 queryParam("pageNum", 1),
ctx.future(future { Search.sourceSearch(sourceId, searchTerm, pageNum) }) documentWith = {
} withOperation {
summary("Source search")
description("Single source search")
}
},
behaviorOf = { ctx, sourceId, searchTerm, pageNum ->
ctx.future(future { Search.sourceSearch(sourceId, searchTerm, pageNum) })
},
withResults = {
json<PagedMangaListDataClass>(HttpCode.OK)
}
)
/** all source search */ /** all source search */
fun searchAll(ctx: Context) { // TODO val searchAll = handler(
val searchTerm = ctx.pathParam("searchTerm") pathParam<String>("searchTerm"),
ctx.json(Search.sourceGlobalSearch(searchTerm)) documentWith = {
} withOperation {
summary("Source global search")
description("All source search")
}
},
behaviorOf = { ctx, searchTerm -> // TODO
ctx.json(Search.sourceGlobalSearch(searchTerm))
},
withResults = {
httpCode(HttpCode.OK)
}
)
} }
@@ -1,6 +1,5 @@
package suwayomi.tachidesk.manga.controller package suwayomi.tachidesk.manga.controller
import io.javalin.http.Context
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 kotlinx.coroutines.runBlocking
@@ -12,10 +11,15 @@ import suwayomi.tachidesk.manga.impl.Category
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.update.IUpdater import suwayomi.tachidesk.manga.impl.update.IUpdater
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.PaginatedList
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.util.formParam
import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.pathParam
import suwayomi.tachidesk.server.util.withOperation import suwayomi.tachidesk.server.util.withOperation
/* /*
@@ -29,35 +33,63 @@ object UpdateController {
private val logger = KotlinLogging.logger { } private val logger = KotlinLogging.logger { }
/** get recently updated manga chapters */ /** get recently updated manga chapters */
fun recentChapters(ctx: Context) { val recentChapters = handler(
val pageNum = ctx.pathParam("pageNum").toInt() pathParam<Int>("pageNum"),
documentWith = {
ctx.future( withOperation {
future { summary("Updates fetch")
Chapter.getRecentChapters(pageNum) description("Get recently updated manga chapters")
}
)
}
fun categoryUpdate(ctx: Context) {
val categoryId = ctx.formParam("category")?.toIntOrNull()
val categoriesForUpdate = ArrayList<CategoryDataClass>()
if (categoryId == null) {
logger.info { "Adding Library to Update Queue" }
categoriesForUpdate.addAll(Category.getCategoryList())
} else {
val category = Category.getCategoryById(categoryId)
if (category != null) {
categoriesForUpdate.add(category)
} else {
logger.info { "No Category found" }
ctx.status(HttpCode.BAD_REQUEST)
return
} }
},
behaviorOf = { ctx, pageNum ->
ctx.future(
future {
Chapter.getRecentChapters(pageNum)
}
)
},
withResults = {
json<PagedMangaChapterListDataClass>(HttpCode.OK)
} }
addCategoriesToUpdateQueue(categoriesForUpdate, true) )
ctx.status(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(
formParam<Int?>("categoryId"),
documentWith = {
withOperation {
summary("Updater start")
description("Starts the updater")
}
},
behaviorOf = { ctx, categoryId ->
val categoriesForUpdate = ArrayList<CategoryDataClass>()
if (categoryId == null) {
logger.info { "Adding Library to Update Queue" }
categoriesForUpdate.addAll(Category.getCategoryList())
} else {
val category = Category.getCategoryById(categoryId)
if (category != null) {
categoriesForUpdate.add(category)
} else {
logger.info { "No Category found" }
ctx.status(HttpCode.BAD_REQUEST)
return@handler
}
}
addCategoriesToUpdateQueue(categoriesForUpdate, true)
ctx.status(HttpCode.OK)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.BAD_REQUEST)
}
)
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>()
@@ -84,15 +116,27 @@ object UpdateController {
} }
} }
fun updateSummary(ctx: Context) { val updateSummary = handler(
val updater by DI.global.instance<IUpdater>() documentWith = {
ctx.json(updater.getStatus().value.getJsonSummary()) withOperation {
} summary("Updater summary")
description("Gets the latest updater summary")
}
},
behaviorOf = { ctx ->
val updater by DI.global.instance<IUpdater>()
ctx.json(updater.getStatus().value.getJsonSummary())
},
withResults = {
json<UpdateStatus>(HttpCode.OK)
}
)
val reset = handler( val reset = handler(
documentWith = { documentWith = {
withOperation { withOperation {
summary("Stops and resets the Updater") summary("Updater reset")
description("Stops and resets the Updater")
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
@@ -70,12 +70,19 @@ object CategoryManga {
.slice(ChapterTable.id.count()) .slice(ChapterTable.id.count())
.select { (MangaTable.id eq ChapterTable.manga) and (ChapterTable.isDownloaded eq true) } .select { (MangaTable.id eq ChapterTable.manga) and (ChapterTable.isDownloaded eq true) }
) )
val chapterCountExpression = wrapAsExpression<Long>(
ChapterTable
.slice(ChapterTable.id.count())
.select { (MangaTable.id eq ChapterTable.manga) }
)
val selectedColumns = MangaTable.columns + unreadExpression + downloadExpression + chapterCountExpression
val selectedColumns = MangaTable.columns + unreadExpression + downloadExpression
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]?.toInt()
dataClass.downloadCount = it[downloadExpression]?.toInt() dataClass.downloadCount = it[downloadExpression]?.toInt()
dataClass.chapterCount = it[chapterCountExpression]?.toInt()
dataClass dataClass
} }
@@ -90,7 +97,7 @@ object CategoryManga {
return transaction { return transaction {
CategoryMangaTable.innerJoin(MangaTable) CategoryMangaTable.innerJoin(MangaTable)
.slice(selectedColumns) .slice(selectedColumns)
.select { CategoryMangaTable.category eq categoryId } .select { (MangaTable.inLibrary eq true) and (CategoryMangaTable.category eq categoryId) }
.map(transform) .map(transform)
} }
} }
@@ -201,8 +201,10 @@ 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 = val meta = transaction {
transaction { ChapterMetaTable.select { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) } }.firstOrNull() ChapterMetaTable.select { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }
}.firstOrNull()
if (meta == null) { if (meta == null) {
ChapterMetaTable.insert { ChapterMetaTable.insert {
it[ChapterMetaTable.key] = key it[ChapterMetaTable.key] = key
@@ -210,7 +212,7 @@ object Chapter {
it[ChapterMetaTable.ref] = chapterId it[ChapterMetaTable.ref] = chapterId
} }
} else { } else {
ChapterMetaTable.update { ChapterMetaTable.update({ (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }) {
it[ChapterMetaTable.value] = value it[ChapterMetaTable.value] = value
} }
} }
@@ -7,7 +7,6 @@ package suwayomi.tachidesk.manga.impl
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
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
@@ -24,17 +23,20 @@ object Library {
if (!manga.inLibrary) { if (!manga.inLibrary) {
transaction { transaction {
val defaultCategories = CategoryTable.select { CategoryTable.isDefault eq true }.toList() val defaultCategories = CategoryTable.select { CategoryTable.isDefault eq true }.toList()
val existingCategories = CategoryMangaTable.select { CategoryMangaTable.manga eq mangaId }.toList()
MangaTable.update({ MangaTable.id eq manga.id }) { MangaTable.update({ MangaTable.id eq manga.id }) {
it[inLibrary] = true it[inLibrary] = true
it[inLibraryAt] = Instant.now().epochSecond it[inLibraryAt] = Instant.now().epochSecond
it[defaultCategory] = defaultCategories.isEmpty() it[defaultCategory] = defaultCategories.isEmpty() && existingCategories.isEmpty()
} }
defaultCategories.forEach { category -> if (existingCategories.isEmpty()) {
CategoryMangaTable.insert { defaultCategories.forEach { category ->
it[CategoryMangaTable.category] = category[CategoryTable.id].value CategoryMangaTable.insert {
it[CategoryMangaTable.manga] = mangaId it[CategoryMangaTable.category] = category[CategoryTable.id].value
it[CategoryMangaTable.manga] = mangaId
}
} }
} }
} }
@@ -47,9 +49,7 @@ object Library {
transaction { transaction {
MangaTable.update({ MangaTable.id eq manga.id }) { MangaTable.update({ MangaTable.id eq manga.id }) {
it[inLibrary] = false it[inLibrary] = false
it[defaultCategory] = true
} }
CategoryMangaTable.deleteWhere { CategoryMangaTable.manga eq mangaId }
} }
} }
} }
@@ -8,9 +8,11 @@ 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 eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.local.LocalSource 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.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
@@ -23,7 +25,9 @@ import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl
import suwayomi.tachidesk.manga.impl.Source.getSource import suwayomi.tachidesk.manga.impl.Source.getSource
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.network.await import suwayomi.tachidesk.manga.impl.util.network.await
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.impl.util.source.StubSource
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.clearCachedImage import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.clearCachedImage
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
@@ -34,6 +38,7 @@ 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.server.ApplicationDirs import suwayomi.tachidesk.server.ApplicationDirs
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
@@ -50,30 +55,10 @@ object Manga {
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() } var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
return if (mangaEntry[MangaTable.initialized] && !onlineFetch) { return if (mangaEntry[MangaTable.initialized] && !onlineFetch) {
MangaDataClass( getMangaDataClass(mangaId, mangaEntry)
mangaId,
mangaEntry[MangaTable.sourceReference].toString(),
mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title],
proxyThumbnailUrl(mangaId),
true,
mangaEntry[MangaTable.artist],
mangaEntry[MangaTable.author],
mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre].toGenreList(),
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
mangaEntry[MangaTable.inLibrary],
mangaEntry[MangaTable.inLibraryAt],
getSource(mangaEntry[MangaTable.sourceReference]),
getMangaMetaMap(mangaId),
mangaEntry[MangaTable.realUrl],
false
)
} else { // initialize manga } else { // initialize manga
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference]) val source = getCatalogueSourceOrNull(mangaEntry[MangaTable.sourceReference])
?: return getMangaDataClass(mangaId, mangaEntry)
val sManga = SManga.create().apply { val sManga = SManga.create().apply {
url = mangaEntry[MangaTable.url] url = mangaEntry[MangaTable.url]
title = mangaEntry[MangaTable.title] title = mangaEntry[MangaTable.title]
@@ -135,6 +120,29 @@ object Manga {
} }
} }
private fun getMangaDataClass(mangaId: Int, mangaEntry: ResultRow) = MangaDataClass(
mangaId,
mangaEntry[MangaTable.sourceReference].toString(),
mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title],
proxyThumbnailUrl(mangaId),
true,
mangaEntry[MangaTable.artist],
mangaEntry[MangaTable.author],
mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre].toGenreList(),
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
mangaEntry[MangaTable.inLibrary],
mangaEntry[MangaTable.inLibraryAt],
getSource(mangaEntry[MangaTable.sourceReference]),
getMangaMetaMap(mangaId),
mangaEntry[MangaTable.realUrl],
false
)
fun getMangaMetaMap(manga: Int): Map<String, String> { fun getMangaMetaMap(manga: Int): Map<String, String> {
return transaction { return transaction {
MangaMetaTable.select { MangaMetaTable.ref eq manga } MangaMetaTable.select { MangaMetaTable.ref eq manga }
@@ -146,8 +154,10 @@ object Manga {
transaction { transaction {
val manga = MangaTable.select { MangaTable.id eq mangaId } val manga = MangaTable.select { MangaTable.id eq mangaId }
.first()[MangaTable.id] .first()[MangaTable.id]
val meta = val meta = transaction {
transaction { MangaMetaTable.select { (MangaMetaTable.ref eq manga) and (MangaMetaTable.key eq key) } }.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
@@ -155,7 +165,7 @@ object Manga {
it[MangaMetaTable.ref] = manga it[MangaMetaTable.ref] = manga
} }
} else { } else {
MangaMetaTable.update { MangaMetaTable.update({ (MangaMetaTable.ref eq manga) and (MangaMetaTable.key eq key) }) {
it[MangaMetaTable.value] = value it[MangaMetaTable.value] = value
} }
} }
@@ -163,6 +173,7 @@ object Manga {
} }
private val applicationDirs by DI.global.instance<ApplicationDirs>() private val applicationDirs by DI.global.instance<ApplicationDirs>()
private val network: NetworkHelper by injectLazy()
suspend fun getMangaThumbnail(mangaId: Int, useCache: Boolean): Pair<InputStream, String> { suspend fun getMangaThumbnail(mangaId: Int, useCache: Boolean): Pair<InputStream, String> {
val saveDir = applicationDirs.thumbnailsRoot val saveDir = applicationDirs.thumbnailsRoot
val fileName = mangaId.toString() val fileName = mangaId.toString()
@@ -176,10 +187,12 @@ object Manga {
?: if (!mangaEntry[MangaTable.initialized]) { ?: if (!mangaEntry[MangaTable.initialized]) {
// initialize then try again // initialize then try again
getManga(mangaId) getManga(mangaId)
transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }[MangaTable.thumbnail_url]!! transaction {
MangaTable.select { MangaTable.id eq mangaId }.first()
}[MangaTable.thumbnail_url]!!
} else { } else {
// source provides no thumbnail url for this manga // source provides no thumbnail url for this manga
throw NullPointerException() throw NullPointerException("No thumbnail found")
} }
source.client.newCall( source.client.newCall(
@@ -199,6 +212,13 @@ object Manga {
?: "image/jpeg" ?: "image/jpeg"
imageFile.inputStream() to contentType imageFile.inputStream() to contentType
} }
is StubSource -> getImageResponse(saveDir, fileName, useCache) {
val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
?: throw NullPointerException("No thumbnail found")
network.client.newCall(
GET(thumbnailUrl)
).await()
}
else -> throw IllegalArgumentException("Unknown source") else -> throw IllegalArgumentException("Unknown source")
} }
} }
@@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import io.javalin.plugin.json.JsonMapper import io.javalin.plugin.json.JsonMapper
import kotlinx.serialization.Serializable
import org.kodein.di.DI import org.kodein.di.DI
import org.kodein.di.conf.global import org.kodein.di.conf.global
import org.kodein.di.instance import org.kodein.di.instance
@@ -80,37 +81,42 @@ object Search {
val filter: Filter<*>, val filter: Filter<*>,
) )
fun setFilter(sourceId: Long, change: 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)
when (val filter = filterList[change.position]) { changes.forEach { change ->
is Filter.Header -> { when (val filter = filterList[change.position]) {
// NOOP is Filter.Header -> {
} // NOOP
is Filter.Separator -> { }
// NOOP is Filter.Separator -> {
} // NOOP
is Filter.Select<*> -> filter.state = change.state.toInt() }
is Filter.Text -> filter.state = change.state is Filter.Select<*> -> filter.state = change.state.toInt()
is Filter.CheckBox -> filter.state = change.state.toBooleanStrict() is Filter.Text -> filter.state = change.state
is Filter.TriState -> filter.state = change.state.toInt() is Filter.CheckBox -> filter.state = change.state.toBooleanStrict()
is Filter.Group<*> -> { is Filter.TriState -> filter.state = change.state.toInt()
val groupChange = jsonMapper.fromJsonString(change.state, FilterChange::class.java) is Filter.Group<*> -> {
val groupChange = jsonMapper.fromJsonString(change.state, FilterChange::class.java)
when (val groupFilter = filter.state[groupChange.position]) { when (val groupFilter = filter.state[groupChange.position]) {
is Filter.CheckBox -> groupFilter.state = groupChange.state.toBooleanStrict() is Filter.CheckBox -> groupFilter.state = groupChange.state.toBooleanStrict()
is Filter.TriState -> groupFilter.state = groupChange.state.toInt() is Filter.TriState -> groupFilter.state = groupChange.state.toInt()
is Filter.Text -> groupFilter.state = groupChange.state is Filter.Text -> groupFilter.state = groupChange.state
is Filter.Select<*> -> groupFilter.state = groupChange.state.toInt() is Filter.Select<*> -> groupFilter.state = groupChange.state.toInt()
}
}
is Filter.Sort -> {
filter.state = jsonMapper.fromJsonString(change.state, Filter.Sort.Selection::class.java)
} }
} }
is Filter.Sort -> filter.state = jsonMapper.fromJsonString(change.state, Filter.Sort.Selection::class.java)
} }
} }
private val jsonMapper by DI.global.instance<JsonMapper>() private val jsonMapper by DI.global.instance<JsonMapper>()
@Serializable
data class FilterChange( data class FilterChange(
val position: Int, val position: Int,
val state: String val state: String
@@ -21,7 +21,7 @@ import org.kodein.di.DI
import org.kodein.di.conf.global import org.kodein.di.conf.global
import org.kodein.di.instance import org.kodein.di.instance
import suwayomi.tachidesk.manga.impl.extension.Extension.getExtensionIconUrl import suwayomi.tachidesk.manga.impl.extension.Extension.getExtensionIconUrl
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSource import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.unregisterCatalogueSource import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.unregisterCatalogueSource
import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass
@@ -36,8 +36,8 @@ object Source {
fun getSourceList(): List<SourceDataClass> { fun getSourceList(): List<SourceDataClass> {
return transaction { return transaction {
SourceTable.selectAll().map { SourceTable.selectAll().mapNotNull {
val catalogueSource = getCatalogueSourceOrStub(it[SourceTable.id].value) val catalogueSource = getCatalogueSourceOrNull(it[SourceTable.id].value) ?: return@mapNotNull null
val sourceExtension = ExtensionTable.select { ExtensionTable.id eq it[SourceTable.extension] }.first() val sourceExtension = ExtensionTable.select { ExtensionTable.id eq it[SourceTable.extension] }.first()
SourceDataClass( SourceDataClass(
@@ -54,27 +54,23 @@ object Source {
} }
} }
fun getSource(sourceId: Long): SourceDataClass { // all the data extracted fresh form the source instance fun getSource(sourceId: Long): SourceDataClass? { // all the data extracted fresh form the source instance
return transaction { return transaction {
val source = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull() val source = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull() ?: return@transaction null
val catalogueSource = source?.let { getCatalogueSource(sourceId) } val catalogueSource = getCatalogueSourceOrNull(sourceId) ?: return@transaction null
val extension = source?.let { val extension = ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first()
ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first()
}
SourceDataClass( SourceDataClass(
sourceId.toString(), sourceId.toString(),
source?.get(SourceTable.name), source[SourceTable.name],
source?.get(SourceTable.lang), source[SourceTable.lang],
source?.let { getExtensionIconUrl(
getExtensionIconUrl( extension[ExtensionTable.apkName]
extension!![ExtensionTable.apkName] ),
) catalogueSource.supportsLatest,
}, catalogueSource is ConfigurableSource,
catalogueSource?.supportsLatest, source[SourceTable.isNsfw],
catalogueSource?.let { it is ConfigurableSource }, catalogueSource.toString()
source?.get(SourceTable.isNsfw),
catalogueSource?.toString()
) )
} }
} }
@@ -41,7 +41,7 @@ object PackageTools {
const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory" const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
const val METADATA_NSFW = "tachiyomi.extension.nsfw" const val METADATA_NSFW = "tachiyomi.extension.nsfw"
const val LIB_VERSION_MIN = 1.2 const val LIB_VERSION_MIN = 1.2
const val LIB_VERSION_MAX = 1.2 const val LIB_VERSION_MAX = 1.3
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23" // inorichi's key private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23" // inorichi's key
private const val unofficialSignature = "64feb21075ba97ebc9cc981243645b331595c111cef1b0d084236a0403b00581" // ArMor's key private const val unofficialSignature = "64feb21075ba97ebc9cc981243645b331595c111cef1b0d084236a0403b00581" // ArMor's key
@@ -26,7 +26,7 @@ object GetCatalogueSource {
private val sourceCache = ConcurrentHashMap<Long, CatalogueSource>() private val sourceCache = ConcurrentHashMap<Long, CatalogueSource>()
private val applicationDirs by DI.global.instance<ApplicationDirs>() private val applicationDirs by DI.global.instance<ApplicationDirs>()
fun getCatalogueSource(sourceId: Long): CatalogueSource? { private fun getCatalogueSource(sourceId: Long): CatalogueSource? {
val cachedResult: CatalogueSource? = sourceCache[sourceId] val cachedResult: CatalogueSource? = sourceCache[sourceId]
if (cachedResult != null) { if (cachedResult != null) {
return cachedResult return cachedResult
@@ -56,8 +56,12 @@ object GetCatalogueSource {
return sourceCache[sourceId]!! return sourceCache[sourceId]!!
} }
fun getCatalogueSourceOrNull(sourceId: Long): CatalogueSource? {
return runCatching { getCatalogueSource(sourceId) }.getOrNull()
}
fun getCatalogueSourceOrStub(sourceId: Long): CatalogueSource { fun getCatalogueSourceOrStub(sourceId: Long): CatalogueSource {
return getCatalogueSource(sourceId) ?: StubSource(sourceId) return getCatalogueSourceOrNull(sourceId) ?: StubSource(sourceId)
} }
fun registerCatalogueSource(sourcePair: Pair<Long, CatalogueSource>) { fun registerCatalogueSource(sourcePair: Pair<Long, CatalogueSource>) {
@@ -36,7 +36,8 @@ data class MangaDataClass(
val freshData: Boolean = false, val freshData: Boolean = false,
var unreadCount: Int? = null, var unreadCount: Int? = null,
var downloadCount: Int? = null var downloadCount: Int? = null,
var chapterCount: Int? = null
) )
data class PagedMangaListDataClass( data class PagedMangaListDataClass(
@@ -9,7 +9,7 @@ package suwayomi.tachidesk.manga.model.dataclass
import kotlin.math.min import kotlin.math.min
data class PaginatedList<T>( open class PaginatedList<T>(
val page: List<T>, val page: List<T>,
val hasNextPage: Boolean, val hasNextPage: Boolean,
) )
@@ -11,19 +11,19 @@ import eu.kanade.tachiyomi.source.ConfigurableSource
data class SourceDataClass( data class SourceDataClass(
val id: String, val id: String,
val name: String?, val name: String,
val lang: String?, val lang: String,
val iconUrl: String?, val iconUrl: String,
/** The Source provides a latest listing */ /** The Source provides a latest listing */
val supportsLatest: Boolean?, val supportsLatest: Boolean,
/** The Source implements [ConfigurableSource] */ /** The Source implements [ConfigurableSource] */
val isConfigurable: Boolean?, val isConfigurable: Boolean,
/** The Source class has a @Nsfw annotation */ /** The Source class has a @Nsfw annotation */
val isNsfw: Boolean?, val isNsfw: Boolean,
/** A nicer version of [name] */ /** A nicer version of [name] */
val displayName: String?, val displayName: String,
) )
@@ -66,7 +66,10 @@ enum class MangaStatus(val value: Int) {
UNKNOWN(0), UNKNOWN(0),
ONGOING(1), ONGOING(1),
COMPLETED(2), COMPLETED(2),
LICENSED(3); LICENSED(3),
PUBLISHING_FINISHED(4),
CANCELLED(5),
ON_HIATUS(6);
companion object { companion object {
fun valueOf(value: Int): MangaStatus = values().find { it.value == value } ?: UNKNOWN fun valueOf(value: Int): MangaStatus = values().find { it.value == value } ?: UNKNOWN
@@ -26,7 +26,7 @@ import org.kodein.di.instance
import suwayomi.tachidesk.global.GlobalAPI import suwayomi.tachidesk.global.GlobalAPI
import suwayomi.tachidesk.manga.MangaAPI import suwayomi.tachidesk.manga.MangaAPI
import suwayomi.tachidesk.server.util.Browser import suwayomi.tachidesk.server.util.Browser
import suwayomi.tachidesk.server.util.setupWebUI import suwayomi.tachidesk.server.util.setupWebInterface
import java.io.IOException import java.io.IOException
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import kotlin.concurrent.thread import kotlin.concurrent.thread
@@ -45,15 +45,29 @@ object JavalinSetup {
fun javalinSetup() { fun javalinSetup() {
val app = Javalin.create { config -> val app = Javalin.create { config ->
if (serverConfig.webUIEnabled) { if (serverConfig.webUIEnabled) {
setupWebUI() setupWebInterface()
logger.info { "Serving webUI static files" } logger.info { "Serving web static files for ${serverConfig.webUIFlavor}" }
config.addStaticFiles(applicationDirs.webUIRoot, Location.EXTERNAL) config.addStaticFiles(applicationDirs.webUIRoot, Location.EXTERNAL)
config.addSinglePageRoot("/", applicationDirs.webUIRoot + "/index.html", Location.EXTERNAL) config.addSinglePageRoot("/", applicationDirs.webUIRoot + "/index.html", Location.EXTERNAL)
config.registerPlugin(OpenApiPlugin(getOpenApiOptions())) config.registerPlugin(OpenApiPlugin(getOpenApiOptions()))
} }
config.enableCorsForAllOrigins() config.enableCorsForAllOrigins()
config.accessManager { handler, ctx, _ ->
fun credentialsValid(): Boolean {
val (username, password) = ctx.basicAuthCredentials()
return username == serverConfig.basicAuthUsername && password == serverConfig.basicAuthPassword
}
if (serverConfig.basicAuthEnabled && !(ctx.basicAuthCredentialsExist() && credentialsValid())) {
ctx.header("WWW-Authenticate", "Basic")
ctx.status(401).json("Unauthorized")
} else {
handler.handle(ctx)
}
}
}.events { event -> }.events { event ->
event.serverStarted { event.serverStarted {
if (serverConfig.initialOpenInBrowserEnabled) { if (serverConfig.initialOpenInBrowserEnabled) {
@@ -83,18 +97,6 @@ object JavalinSetup {
ctx.result(e.message ?: "Internal Server Error") ctx.result(e.message ?: "Internal Server Error")
} }
app.before { ctx ->
fun credentialsValid(): Boolean {
val (username, password) = ctx.basicAuthCredentials()
return username == serverConfig.basicAuthUsername && password == serverConfig.basicAuthPassword
}
if (serverConfig.basicAuthEnabled && !(ctx.basicAuthCredentialsExist() && credentialsValid())) {
ctx.header("WWW-Authenticate", "Basic")
ctx.status(401).json("Unauthorized")
}
}
app.routes { app.routes {
path("api/v1/") { path("api/v1/") {
GlobalAPI.defineEndpoints() GlobalAPI.defineEndpoints()
@@ -26,9 +26,11 @@ class ServerConfig(config: Config, moduleName: String = MODULE_NAME) : SystemPro
// misc // misc
val debugLogsEnabled: Boolean = debugLogsEnabled(GlobalConfigManager.config) val debugLogsEnabled: Boolean = debugLogsEnabled(GlobalConfigManager.config)
val systemTrayEnabled: Boolean by overridableConfig val systemTrayEnabled: Boolean by overridableConfig
val downloadsPath: String by overridableConfig
// webUI // webUI
val webUIEnabled: Boolean by overridableConfig val webUIEnabled: Boolean by overridableConfig
val webUIFlavor: String by overridableConfig
val initialOpenInBrowserEnabled: Boolean by overridableConfig val initialOpenInBrowserEnabled: Boolean by overridableConfig
val webUIInterface: String by overridableConfig val webUIInterface: String by overridableConfig
val electronPath: String by overridableConfig val electronPath: String by overridableConfig
@@ -11,7 +11,9 @@ import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.source.local.LocalSource import eu.kanade.tachiyomi.source.local.LocalSource
import io.javalin.plugin.json.JavalinJackson import io.javalin.plugin.json.JavalinJackson
import io.javalin.plugin.json.JsonMapper import io.javalin.plugin.json.JsonMapper
import kotlinx.serialization.json.Json
import mu.KotlinLogging import mu.KotlinLogging
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.kodein.di.DI import org.kodein.di.DI
import org.kodein.di.bind import org.kodein.di.bind
import org.kodein.di.conf.global import org.kodein.di.conf.global
@@ -28,6 +30,7 @@ import xyz.nulldev.ts.config.ApplicationRootDir
import xyz.nulldev.ts.config.ConfigKodeinModule import xyz.nulldev.ts.config.ConfigKodeinModule
import xyz.nulldev.ts.config.GlobalConfigManager import xyz.nulldev.ts.config.GlobalConfigManager
import java.io.File import java.io.File
import java.security.Security
import java.util.Locale import java.util.Locale
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
@@ -37,7 +40,7 @@ class ApplicationDirs(
) { ) {
val extensionsRoot = "$dataRoot/extensions" val extensionsRoot = "$dataRoot/extensions"
val thumbnailsRoot = "$dataRoot/thumbnails" val thumbnailsRoot = "$dataRoot/thumbnails"
val mangaDownloadsRoot = "$dataRoot/downloads" val mangaDownloadsRoot = serverConfig.downloadsPath.ifBlank { "$dataRoot/downloads" }
val localMangaRoot = "$dataRoot/local" val localMangaRoot = "$dataRoot/local"
val webUIRoot = "$dataRoot/webUI" val webUIRoot = "$dataRoot/webUI"
} }
@@ -51,6 +54,11 @@ val androidCompat by lazy { AndroidCompat() }
fun applicationSetup() { fun applicationSetup() {
logger.info("Running Tachidesk ${BuildConfig.VERSION} revision ${BuildConfig.REVISION}") logger.info("Running Tachidesk ${BuildConfig.VERSION} revision ${BuildConfig.REVISION}")
// register Tachidesk's config which is dubbed "ServerConfig"
GlobalConfigManager.registerModule(
ServerConfig.register(GlobalConfigManager.config)
)
// Application dirs // Application dirs
val applicationDirs = ApplicationDirs() val applicationDirs = ApplicationDirs()
@@ -59,6 +67,7 @@ fun applicationSetup() {
bind<ApplicationDirs>() with singleton { applicationDirs } bind<ApplicationDirs>() with singleton { applicationDirs }
bind<IUpdater>() with singleton { Updater() } bind<IUpdater>() with singleton { Updater() }
bind<JsonMapper>() with singleton { JavalinJackson() } bind<JsonMapper>() with singleton { JavalinJackson() }
bind<Json>() with singleton { Json { ignoreUnknownKeys = true } }
} }
) )
@@ -67,7 +76,6 @@ fun applicationSetup() {
// Migrate Directories from old versions // Migrate Directories from old versions
File("$ApplicationRootDir/manga-thumbnails").renameTo(applicationDirs.thumbnailsRoot) File("$ApplicationRootDir/manga-thumbnails").renameTo(applicationDirs.thumbnailsRoot)
File("$ApplicationRootDir/manga-local").renameTo(applicationDirs.localMangaRoot) File("$ApplicationRootDir/manga-local").renameTo(applicationDirs.localMangaRoot)
File("$ApplicationRootDir/manga").renameTo(applicationDirs.mangaDownloadsRoot)
File("$ApplicationRootDir/anime-thumbnails").delete() File("$ApplicationRootDir/anime-thumbnails").delete()
// make dirs we need // make dirs we need
@@ -82,11 +90,6 @@ fun applicationSetup() {
File(it).mkdirs() File(it).mkdirs()
} }
// register Tachidesk's config which is dubbed "ServerConfig"
GlobalConfigManager.registerModule(
ServerConfig.register(GlobalConfigManager.config)
)
// Make sure only one instance of the app is running // Make sure only one instance of the app is running
handleAppMutex() handleAppMutex()
@@ -152,4 +155,7 @@ fun applicationSetup() {
System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort
logger.info("Socks Proxy is enabled to ${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}") logger.info("Socks Proxy is enabled to ${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}")
} }
// AES/CBC/PKCS7Padding Cypher provider for zh.copymanga
Security.addProvider(BouncyCastleProvider())
} }
@@ -12,23 +12,44 @@ fun <T> getSimpleParamItem(ctx: Context, param: Param<T>): String? {
is Param.FormParam -> ctx.formParam(param.key) is Param.FormParam -> ctx.formParam(param.key)
is Param.PathParam -> ctx.pathParam(param.key) is Param.PathParam -> ctx.pathParam(param.key)
is Param.QueryParam -> ctx.queryParam(param.key) is Param.QueryParam -> ctx.queryParam(param.key)
else -> throw IllegalStateException("Invalid param")
} }
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun <T> getParam(ctx: Context, param: Param<T>): T { fun <T> getParam(ctx: Context, param: Param<T>): T {
val typedItem: Any? = when (param.clazz) { if (param is Param.QueryParams<*, *>) {
String::class.java -> getSimpleParamItem(ctx, param) val item = ctx.queryParams(param.key).filter(String::isNotBlank)
Int::class.java -> getSimpleParamItem(ctx, param)?.toIntOrNull() val typedItem: List<Any?> = when (param.clazz) {
Long::class.java -> getSimpleParamItem(ctx, param)?.toLongOrNull() String::class.java, java.lang.String::class.java -> item
Boolean::class.java -> getSimpleParamItem(ctx, param)?.toBoolean() Int::class.java, java.lang.Integer::class.java -> item.map { it.toIntOrNull() }
Float::class.java -> getSimpleParamItem(ctx, param)?.toFloatOrNull() Long::class.java, java.lang.Long::class.java -> item.map { it.toLongOrNull() }
Double::class.java -> getSimpleParamItem(ctx, param)?.toDoubleOrNull() Boolean::class.java, java.lang.Boolean::class.java -> item.map { it.toBoolean() }
Float::class.java, java.lang.Float::class.java -> item.map { it.toFloatOrNull() }
Double::class.java, java.lang.Double::class.java -> item.map { it.toDoubleOrNull() }
else -> throw IllegalStateException("Unknown class ${param.clazz.simpleName}")
}.let {
if (param.nullable) {
it
} else {
it.filterNotNull()
}
}.ifEmpty { param.defaultValue }
return typedItem as T
}
val typedItem: Any? = when (val clazz = param.clazz as Class<T>) {
String::class.java, java.lang.String::class.java -> getSimpleParamItem(ctx, param) ?: param.defaultValue
Int::class.java, java.lang.Integer::class.java -> getSimpleParamItem(ctx, param)?.toIntOrNull() ?: param.defaultValue
Long::class.java, java.lang.Long::class.java -> getSimpleParamItem(ctx, param)?.toLongOrNull() ?: param.defaultValue
Boolean::class.java, java.lang.Boolean::class.java -> getSimpleParamItem(ctx, param)?.toBoolean() ?: param.defaultValue
Float::class.java, java.lang.Float::class.java -> getSimpleParamItem(ctx, param)?.toFloatOrNull() ?: param.defaultValue
Double::class.java, java.lang.Double::class.java -> getSimpleParamItem(ctx, param)?.toDoubleOrNull() ?: param.defaultValue
else -> { else -> {
when (param) { when (param) {
is Param.FormParam -> ctx.formParamAsClass(param.key, param.clazz) is Param.FormParam -> ctx.formParamAsClass(param.key, clazz)
is Param.PathParam -> ctx.pathParamAsClass(param.key, param.clazz) is Param.PathParam -> ctx.pathParamAsClass(param.key, clazz)
is Param.QueryParam -> ctx.queryParamAsClass(param.key, param.clazz) is Param.QueryParam -> ctx.queryParamAsClass(param.key, clazz)
else -> throw IllegalStateException("Invalid param")
}.let { }.let {
if (param.nullable) { if (param.nullable) {
it.allowNullable().get() ?: param.defaultValue it.allowNullable().get() ?: param.defaultValue
@@ -61,7 +82,8 @@ inline fun getDocumentation(
when (it) { when (it) {
is Param.FormParam -> formParam(it.key, it.clazz, !it.nullable && it.defaultValue == null) is Param.FormParam -> formParam(it.key, it.clazz, !it.nullable && it.defaultValue == null)
is Param.PathParam -> pathParam(it.key, it.clazz) is Param.PathParam -> pathParam(it.key, it.clazz)
is Param.QueryParam -> queryParam(it.key, it.clazz,) is Param.QueryParam -> queryParam(it.key, it.clazz)
is Param.QueryParams<*, *> -> queryParam(it.key, it.clazz, isRepeatable = true)
} }
} }
} }
@@ -83,37 +105,46 @@ inline fun <reified T> formParam(key: String, defaultValue: T? = null): Param.Fo
inline fun <reified T> queryParam(key: String, defaultValue: T? = null): Param.QueryParam<T> { inline fun <reified T> queryParam(key: String, defaultValue: T? = null): Param.QueryParam<T> {
return Param.QueryParam(key, T::class.java, defaultValue, null is T) return Param.QueryParam(key, T::class.java, defaultValue, null is T)
} }
inline fun <reified T> queryParams(key: String, defaultValue: List<T> = emptyList()): Param.QueryParams<T, List<T>> {
return Param.QueryParams(key, T::class.java, defaultValue, null is T)
}
inline fun <reified T> pathParam(key: String): Param.PathParam<T> { inline fun <reified T> pathParam(key: String): Param.PathParam<T> {
return Param.PathParam(key, T::class.java, null, false) return Param.PathParam(key, T::class.java, null, false)
} }
sealed class Param<T> { sealed class Param<T> {
abstract val key: String abstract val key: String
abstract val clazz: Class<T> abstract val clazz: Class<*>
abstract val defaultValue: T? abstract val defaultValue: T?
abstract val nullable: Boolean abstract val nullable: Boolean
data class FormParam<T>( data class FormParam<T>(
override val key: String, override val key: String,
override val clazz: Class<T>, override val clazz: Class<*>,
override val defaultValue: T?, override val defaultValue: T?,
override val nullable: Boolean override val nullable: Boolean
) : Param<T>() ) : Param<T>()
data class QueryParam<T>( data class QueryParam<T>(
override val key: String, override val key: String,
override val clazz: Class<T>, override val clazz: Class<*>,
override val defaultValue: T?, override val defaultValue: T?,
override val nullable: Boolean override val nullable: Boolean
) : Param<T>() ) : Param<T>()
data class QueryParams<R, T : List<R>>(
override val key: String,
override val clazz: Class<R>,
override val defaultValue: T,
override val nullable: Boolean
) : Param<T>()
data class PathParam<T>( data class PathParam<T>(
override val key: String, override val key: String,
override val clazz: Class<T>, override val clazz: Class<*>,
override val defaultValue: T?, override val defaultValue: T?,
override val nullable: Boolean override val nullable: Boolean
) : Param<T>() ) : Param<T>()
} }
class ResultsBuilder { class ResultsBuilder {
val results = mutableListOf<ResultType<*>>() val results = mutableListOf<ResultType>()
inline fun <reified T> json(code: HttpCode) { inline fun <reified T> json(code: HttpCode) {
results += ResultType.MimeType(code, "application/json", T::class.java) results += ResultType.MimeType(code, "application/json", T::class.java)
@@ -121,19 +152,22 @@ class ResultsBuilder {
fun plainText(code: HttpCode) { fun plainText(code: HttpCode) {
results += ResultType.MimeType(code, "text/plain", String::class.java) results += ResultType.MimeType(code, "text/plain", String::class.java)
} }
fun mime(code: HttpCode, mime: String) {
results += ResultType.MimeType(code, mime, null)
}
fun httpCode(code: HttpCode) { fun httpCode(code: HttpCode) {
results += ResultType.StatusCode(code) results += ResultType.StatusCode(code)
} }
} }
sealed class ResultType <T> { sealed class ResultType {
abstract fun applyTo(documentation: OpenApiDocumentation) abstract fun applyTo(documentation: OpenApiDocumentation)
data class MimeType<T>(val code: HttpCode, val mime: String, private val clazz: Class<T>) : ResultType<T>() { data class MimeType(val code: HttpCode, val mime: String, private val clazz: Class<*>?) : ResultType() {
override fun applyTo(documentation: OpenApiDocumentation) { override fun applyTo(documentation: OpenApiDocumentation) {
documentation.result(code.status.toString(), clazz) documentation.result(code.status.toString(), clazz)
} }
} }
data class StatusCode(val code: HttpCode) : ResultType<Unit>() { data class StatusCode(val code: HttpCode) : ResultType() {
override fun applyTo(documentation: OpenApiDocumentation) { override fun applyTo(documentation: OpenApiDocumentation) {
documentation.result<Unit>(code.status.toString()) documentation.result<Unit>(code.status.toString())
} }
@@ -7,6 +7,9 @@ package suwayomi.tachidesk.server.util
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import mu.KotlinLogging import mu.KotlinLogging
import net.lingala.zip4j.ZipFile import net.lingala.zip4j.ZipFile
import org.kodein.di.DI import org.kodein.di.DI
@@ -14,6 +17,8 @@ import org.kodein.di.conf.global
import org.kodein.di.instance import org.kodein.di.instance
import suwayomi.tachidesk.server.ApplicationDirs import suwayomi.tachidesk.server.ApplicationDirs
import suwayomi.tachidesk.server.BuildConfig import suwayomi.tachidesk.server.BuildConfig
import suwayomi.tachidesk.server.serverConfig
import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.net.HttpURLConnection import java.net.HttpURLConnection
@@ -23,6 +28,7 @@ import java.security.MessageDigest
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
private val applicationDirs by DI.global.instance<ApplicationDirs>() private val applicationDirs by DI.global.instance<ApplicationDirs>()
private val json: Json by injectLazy()
private val tmpDir = System.getProperty("java.io.tmpdir") private val tmpDir = System.getProperty("java.io.tmpdir")
private fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) } private fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }
@@ -44,6 +50,19 @@ private fun directoryMD5(fileDir: String): String {
return digest.toHex() return digest.toHex()
} }
/** Make sure a valid web interface installation is available */
fun setupWebInterface() {
when (serverConfig.webUIFlavor) {
"WebUI" -> setupWebUI()
"Sorayomi" -> setupSorayomi()
"Custom" -> {
/* do nothing */
}
else -> setupWebUI()
}
}
/** Make sure a valid copy of WebUI is available */
fun setupWebUI() { fun setupWebUI() {
// check if we have webUI installed and is correct version // check if we have webUI installed and is correct version
val webUIRevisionFile = File(applicationDirs.webUIRoot + "/revision") val webUIRevisionFile = File(applicationDirs.webUIRoot + "/revision")
@@ -117,3 +136,63 @@ fun setupWebUI() {
logger.info { "Extracting WebUI zip Done." } logger.info { "Extracting WebUI zip Done." }
} }
} }
/** Make sure a valid copy of Sorayomi is available */
fun setupSorayomi() {
// check if we have Sorayomi installed and is correct version
val sorayomiVersionFile = File(applicationDirs.webUIRoot + "/version.json")
if (sorayomiVersionFile.exists() && json.parseToJsonElement(
sorayomiVersionFile.readText()
).jsonObject["version"]!!.jsonPrimitive.content == BuildConfig.SORAYOMI_TAG
) {
logger.info { "Sorayomi Static files exists and is the correct revision" }
logger.info { "Verifying Sorayomi Static files..." }
logger.info { "md5: " + directoryMD5(applicationDirs.webUIRoot) }
} else {
File(applicationDirs.webUIRoot).deleteRecursively()
val sorayomiZip = "tachidesk-sorayomi-${BuildConfig.SORAYOMI_TAG}-web.zip"
val sorayomiZipPath = "$tmpDir/$sorayomiZip"
val sorayomiZipFile = File(sorayomiZipPath)
// download sorayomi zip
val sorayomiZipURL = "${BuildConfig.SORAYOMI_REPO}/releases/download/${BuildConfig.SORAYOMI_TAG}/$sorayomiZip"
sorayomiZipFile.delete()
logger.info { "Downloading Sorayomi zip from the Internet..." }
val data = ByteArray(1024)
sorayomiZipFile.outputStream().use { sorayomiZipFileOut ->
val connection = URL(sorayomiZipURL).openConnection() as HttpURLConnection
connection.connect()
val contentLength = connection.contentLength
connection.inputStream.buffered().use { inp ->
var totalCount = 0
print("Download progress: % 00")
while (true) {
val count = inp.read(data, 0, 1024)
if (count == -1)
break
totalCount += count
val percentage = (totalCount.toFloat() / contentLength * 100).toInt().toString().padStart(2, '0')
print("\b\b$percentage")
sorayomiZipFileOut.write(data, 0, count)
}
println()
logger.info { "Downloading Sorayomi Done." }
}
}
// extract Sorayomi zip
logger.info { "Extracting Sorayomi zip..." }
File(applicationDirs.webUIRoot).mkdirs()
ZipFile(sorayomiZipPath).extractAll(applicationDirs.webUIRoot)
logger.info { "Extracting Sorayomi zip Done." }
}
}
@@ -9,6 +9,7 @@ server.socksProxyPort = ""
# webUI # webUI
server.webUIEnabled = true server.webUIEnabled = true
server.webUIFlavor = "WebUI" # "WebUI" or "Sorayomi" or "Custom"
server.initialOpenInBrowserEnabled = true server.initialOpenInBrowserEnabled = true
server.webUIInterface = "browser" # "browser" or "electron" server.webUIInterface = "browser" # "browser" or "electron"
server.electronPath = "" server.electronPath = ""
@@ -21,3 +22,4 @@ server.basicAuthPassword = ""
# misc # misc
server.debugLogsEnabled = false server.debugLogsEnabled = false
server.systemTrayEnabled = true server.systemTrayEnabled = true
server.downloadsPath = ""
@@ -27,7 +27,7 @@ import suwayomi.tachidesk.manga.impl.extension.Extension.uninstallExtension
import suwayomi.tachidesk.manga.impl.extension.Extension.updateExtension import suwayomi.tachidesk.manga.impl.extension.Extension.updateExtension
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.getExtensionList import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.getExtensionList
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSource import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
import suwayomi.tachidesk.server.applicationSetup import suwayomi.tachidesk.server.applicationSetup
import suwayomi.tachidesk.test.BASE_PATH import suwayomi.tachidesk.test.BASE_PATH
@@ -72,7 +72,7 @@ class TestExtensionCompatibility {
} }
} }
} }
sources = getSourceList().map { getCatalogueSource(it.id.toLong())!! as HttpSource } sources = getSourceList().map { getCatalogueSourceOrNull(it.id.toLong())!! as HttpSource }
} }
setLoggingEnabled(true) setLoggingEnabled(true)
File("$BASE_PATH/sources.txt").writeText(sources.joinToString("\n") { "${it.name} - ${it.lang.uppercase()} - ${it.id}" }) File("$BASE_PATH/sources.txt").writeText(sources.joinToString("\n") { "${it.name} - ${it.lang.uppercase()} - ${it.id}" })