Compare commits

...

52 Commits

Author SHA1 Message Date
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
Aria Moradi 454de23844 fix wrong release name
CI Publish / Validate Gradle Wrapper (push) Successful in 12s
CI Publish / Build artifacts and release (push) Failing after 15s
2022-02-19 05:38:04 +03:30
Aria Moradi 1176092fc6 v0.6.1 2022-02-19 05:29:46 +03:30
Aria Moradi 0fc2c57395 fix mentions 2022-02-19 05:27:34 +03:30
Aria Moradi 0b292f8d74 update WebUI and CHANGELOG 2022-02-19 05:17:56 +03:30
Aria Moradi 4bbe51331f remove gson (#295)
* remove gson

* also remove kotson

* fix build
2022-02-19 05:12:20 +03:30
Aria Moradi c289786dfa auto-remove duplicate chapters (#294)
* auto-remove duplicate chapters

* Apply suggestions from code review

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

* Update Chapter.kt

Co-authored-by: Mitchell Syer <Mitchellptbo@gmail.com>
2022-02-19 04:44:17 +03:30
Aria Moradi 9a9752a6a1 no online fetch on backup (#293)
* no online fetch on backup

* Revert "no online fetch on backup"

This reverts commit 09af582b1a3ef77ca62167bc01f7dcc9643329c9.

* alternative solution
2022-02-19 02:57:03 +03:30
DattatreyaReddy Panta c43b688f8e add Tachidesk-Flutter to readme (#292) 2022-02-01 21:52:58 +03:30
Mahor a6272d5221 Improve windows-bundler.sh (#290) 2022-01-22 14:10:10 +03:30
Mahor b440a58d95 Fix build_push.yml Hopefully (#289) 2022-01-18 11:01:39 +03:30
Mahor ec1589aa2d fix Debian package errors (#288)
* Fix lintian errors

Fix lintian errors
Short description in debian/changelog to hide lintian changelog warnings
Use launcher scripts that run java and electron from /usr/bin

* Update changelog version

* Rename laucnher scripts. Put electron installaion guide in electron-launcher-standalone.sh

* Seperate debian packager codes from unix-bundler.sh

* chmod +x debian-packager.sh

* Fix mistakes

* Fix mistakes

* Add missing '!' to shebang

* Change faviconlogo.png to tachidesk.png

* Change faviconlogo.png to tachidesk.png
2022-01-17 18:41:58 +03:30
Mahor 8d48e56fa0 Automated debian package building (#287)
* Automate debian package building

Move wxs files to scripts/resources/msi/
Define icon's path inside script instead of hardcoding it in wxs

* Revert back Windows script improvments

* Remove copyright year. Use uppercase for first letter of foruzesh

* Seperate deb from tar code with defining new debian-x64 arch

* Add ./unix-bundler.sh debian-x64

* Fix mistake

* Remove unneeded change of license
2022-01-16 08:27:59 +03:30
Mahor df7938037e Automated MSI package building (#277)
* Create msi package instead of zipping stuff

* Keep the zip release, Install wixl inside windows-bundler.sh

* Fix mistakes

* Seprate mv command

* Automate versioning

* Fix syntax mistake

* Fix mistake
2022-01-12 23:48:43 +03:30
Mitchell Syer c908ee2d49 Allow app compilation on Java 18+ (#286) 2022-01-09 22:30:10 +03:30
Mitchell Syer b714abddae Handlers must return a result (#282) 2022-01-07 17:03:35 +03:30
Mitchell Syer 63ca189907 Update Gradle and Dependencies (#281) 2022-01-07 17:03:01 +03:30
Aria Moradi 6130fb9ba2 update dex2jar 2021-12-13 17:16:19 +03:30
Aria Moradi f0edc676fc fix compile erorr 2021-12-13 17:16:03 +03:30
Aria Moradi b68fdb0772 ignore non image files (#269)
* ignore non image files

* Update server/src/main/kotlin/eu/kanade/tachiyomi/source/local/LocalSource.kt

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

Co-authored-by: Mitchell Syer <Mitchellptbo@gmail.com>
2021-12-03 20:53:18 +03:30
Sascha Hahne 76c7bdd604 Add Route to stop and reset the updater (#260) 2021-12-01 23:18:32 +03:30
Mitchell Syer 205101568e Improve documentation with Http codes (#261)
* Improve documentation dsl with Http codes

* Fix plaintext requiring T
2021-12-01 23:17:41 +03:30
Aria Moradi efff68c49b refactor getChapter (#268)
* refactor

* better naming

* fix copytight notice
2021-12-01 22:39:19 +03:30
Aria Moradi f74f60bb1d Update README.md 2021-11-30 11:18:44 +03:30
Aria Moradi 63ea28a620 Update README.md 2021-11-29 20:22:43 +03:30
101 changed files with 2929 additions and 961 deletions
+2 -1
View File
@@ -73,6 +73,7 @@ jobs:
./windows-bundler.sh win32 ./windows-bundler.sh win32
./windows-bundler.sh win64 ./windows-bundler.sh win64
./unix-bundler.sh linux-x64 ./unix-bundler.sh linux-x64
./debian-packager.sh
./unix-bundler.sh macOS-x64 ./unix-bundler.sh macOS-x64
./unix-bundler.sh macOS-arm64 ./unix-bundler.sh macOS-arm64
@@ -103,7 +104,7 @@ jobs:
uses: ncipollo/release-action@v1 uses: ncipollo/release-action@v1
with: with:
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }} token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
artifacts: "master/server/build/*.jar,master/server/build/*.zip,master/server/build/*.tar.gz" artifacts: "master/server/build/*.jar,master/server/build/*.msi,master/server/build/*.zip,master/server/build/*.tar.gz,master/server/build/*.deb"
owner: "Suwayomi" owner: "Suwayomi"
repo: "Tachidesk-Server-preview" repo: "Tachidesk-Server-preview"
tag: ${{ steps.GenTagName.outputs.value }} tag: ${{ steps.GenTagName.outputs.value }}
+4 -3
View File
@@ -3,7 +3,7 @@ name: CI Publish
on: on:
push: push:
tags: tags:
- 'v*' - "v*"
jobs: jobs:
check_wrapper: check_wrapper:
@@ -58,12 +58,13 @@ jobs:
dependencies-cache-enabled: true dependencies-cache-enabled: true
configuration-cache-enabled: true configuration-cache-enabled: true
- name: make bundle packages - name: Make bundle packages
run: | run: |
cd master/scripts cd master/scripts
./windows-bundler.sh win32 ./windows-bundler.sh win32
./windows-bundler.sh win64 ./windows-bundler.sh win64
./unix-bundler.sh linux-x64 ./unix-bundler.sh linux-x64
./debian-packager.sh
./unix-bundler.sh macOS-x64 ./unix-bundler.sh macOS-x64
./unix-bundler.sh macOS-arm64 ./unix-bundler.sh macOS-arm64
@@ -72,7 +73,7 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
file: "master/server/build/*.jar;master/server/build/*.zip;master/server/build/*.tar.gz" file: "master/server/build/*.jar;master/server/build/*.msi;master/server/build/*.zip;master/server/build/*.tar.gz;master/server/build/*.deb"
tags: true tags: true
draft: true draft: true
verbose: true verbose: true
+6 -6
View File
@@ -9,20 +9,20 @@ dependencies {
implementation(project(":AndroidCompat:Config")) implementation(project(":AndroidCompat:Config"))
// APK sig verifier // APK sig verifier
compileOnly("com.android.tools.build:apksig:7.1.0-alpha12") compileOnly("com.android.tools.build:apksig:7.1.0-beta05")
// AndroidX annotations // AndroidX annotations
compileOnly("androidx.annotation:annotation:1.2.0") compileOnly("androidx.annotation:annotation:1.3.0")
// substitute for duktape-android // substitute for duktape-android
implementation("org.mozilla:rhino-runtime:1.7.13") // slimmer version of 'org.mozilla:rhino' implementation("org.mozilla:rhino-runtime:1.7.14") // slimmer version of 'org.mozilla:rhino'
implementation("org.mozilla:rhino-engine:1.7.13") // provides the same interface as 'javax.script' a.k.a Nashorn implementation("org.mozilla:rhino-engine:1.7.14") // provides the same interface as 'javax.script' a.k.a Nashorn
// Kotlin wrapper around Java Preferences, makes certain things easier // Kotlin wrapper around Java Preferences, makes certain things easier
val multiplatformSettingsVersion = "0.8" val multiplatformSettingsVersion = "0.8.1"
implementation("com.russhwolf:multiplatform-settings-jvm:$multiplatformSettingsVersion") implementation("com.russhwolf:multiplatform-settings-jvm:$multiplatformSettingsVersion")
implementation("com.russhwolf:multiplatform-settings-serialization-jvm:$multiplatformSettingsVersion") implementation("com.russhwolf:multiplatform-settings-serialization-jvm:$multiplatformSettingsVersion")
// Android version of SimpleDateFormat // Android version of SimpleDateFormat
implementation("com.ibm.icu:icu4j:69.1") implementation("com.ibm.icu:icu4j:70.1")
} }
@@ -14,9 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
package com.android.internal.util; package com.android.internal.util;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Bitmap.CompressFormat; import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.ArrayMap; import android.util.ArrayMap;
@@ -25,19 +26,14 @@ import android.util.Xml;
import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer; import org.xmlpull.v1.XmlSerializer;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.ProtocolException; import java.net.ProtocolException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** {@hide} */ /** {@hide} */
public class XmlUtils { public class XmlUtils {
private static final String STRING_ARRAY_SEPARATOR = ":"; private static final String STRING_ARRAY_SEPARATOR = ":";
@@ -1396,9 +1392,9 @@ public class XmlUtils {
} else if (tagName.equals("long")) { } else if (tagName.equals("long")) {
return Long.valueOf(parser.getAttributeValue(null, "value")); return Long.valueOf(parser.getAttributeValue(null, "value"));
} else if (tagName.equals("float")) { } else if (tagName.equals("float")) {
return new Float(parser.getAttributeValue(null, "value")); return Float.valueOf(parser.getAttributeValue(null, "value"));
} else if (tagName.equals("double")) { } else if (tagName.equals("double")) {
return new Double(parser.getAttributeValue(null, "value")); return Double.valueOf(parser.getAttributeValue(null, "value"));
} else if (tagName.equals("boolean")) { } else if (tagName.equals("boolean")) {
return Boolean.valueOf(parser.getAttributeValue(null, "value")); return Boolean.valueOf(parser.getAttributeValue(null, "value"));
} else { } else {
+150
View File
@@ -1,3 +1,151 @@
# Server: v0.6.3-next + WebUI: r944
## TL;DR
- N/A
## 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)
## 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)
# 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
## TL;DR
- msi and deb packages thanks to @mahor1221
- [Tachidesk-Flutter](https://github.com/Suwayomi/Tachidesk-Flutter) exists now!
## Tachidesk-Server Changelog
- (r1047) update (by @AriaMoradi)
- (r1048) bump version (by @AriaMoradi)
- (r1049) Update README.md (by @AriaMoradi)
- (r1050) Update README.md (by @AriaMoradi)
- (r1051) refactor getChapter ([#268](https://github.com/Suwayomi/Tachidesk-Server/pull/268) by @AriaMoradi)
- (r1052) Improve documentation with Http codes ([#261](https://github.com/Suwayomi/Tachidesk-Server/pull/261) by @Syer10)
- (r1053) Add Route to stop and reset the updater ([#260](https://github.com/Suwayomi/Tachidesk-Server/pull/260) by @ntbm)
- (r1054) ignore non image files ([#269](https://github.com/Suwayomi/Tachidesk-Server/pull/269) by @AriaMoradi)
- (r1055) fix compile erorr (by @AriaMoradi)
- (r1056) update dex2jar (by @AriaMoradi)
- (r1057) Update Gradle and Dependencies ([#281](https://github.com/Suwayomi/Tachidesk-Server/pull/281) by @Syer10)
- (r1058) Handlers must return a result ([#282](https://github.com/Suwayomi/Tachidesk-Server/pull/282) by @Syer10)
- (r1059) Allow app compilation on Java 18+ ([#286](https://github.com/Suwayomi/Tachidesk-Server/pull/286) by @Syer10)
- (r1060) Automated MSI package building ([#277](https://github.com/Suwayomi/Tachidesk-Server/pull/277) by @mahor1221)
- (r1061) Automated debian package building ([#287](https://github.com/Suwayomi/Tachidesk-Server/pull/287) by @mahor1221)
- (r1062) fix Debian package errors ([#288](https://github.com/Suwayomi/Tachidesk-Server/pull/288) by @mahor1221)
- (r1063) Fix build_push.yml Hopefully ([#289](https://github.com/Suwayomi/Tachidesk-Server/pull/289) by @mahor1221)
- (r1064) Improve windows-bundler.sh ([#290](https://github.com/Suwayomi/Tachidesk-Server/pull/290) by @mahor1221)
- (r1065) add Tachidesk-Flutter to readme ([#292](https://github.com/Suwayomi/Tachidesk-Server/pull/292)) @DattatreyaReddy)
- (r1066) no online fetch on backup ([#293](https://github.com/Suwayomi/Tachidesk-Server/pull/293) by @AriaMoradi)
- (r1067) auto-remove duplicate chapters ([#294](https://github.com/Suwayomi/Tachidesk-Server/pull/294) by @AriaMoradi)
- (r1068) remove gson ([#295](https://github.com/Suwayomi/Tachidesk-Server/pull/295) by @AriaMoradi)
## Tachidesk-WebUI Changelog
- (r894) migrate ReaderNavbar to Mui 5 ([#84](https://github.com/Suwayomi/Tachidesk-WebUI/pull/84) by @AriaMoradi)
- (r895) migrate SpinnerImage to Mui 5 ([#97](https://github.com/Suwayomi/Tachidesk-WebUI/pull/97) by @AriaMoradi)
- (r896) migrate VerticalPager to Mui 5 ([#94](https://github.com/Suwayomi/Tachidesk-WebUI/pull/94) by @AriaMoradi)
- (r897) migrate PagedPager to Mui 5 ([#93](https://github.com/Suwayomi/Tachidesk-WebUI/pull/93) by @AriaMoradi)
- (r898) MangaCard imges don't stretch now ([#110](https://github.com/Suwayomi/Tachidesk-WebUI/pull/110) by @abhijeetChawla)
- (r899) show correct title ([#111](https://github.com/Suwayomi/Tachidesk-WebUI/pull/111) by @AriaMoradi)
- (r900) migrate DoublePage to Mui 5 ([#88](https://github.com/Suwayomi/Tachidesk-WebUI/pull/88) by @AriaMoradi)
- (r901) migrate DoublePagedPager to Mui 5 ([#91](https://github.com/Suwayomi/Tachidesk-WebUI/pull/91) by @AriaMoradi)
- (r902) migrate Reader to Mui 5 ([#100](https://github.com/Suwayomi/Tachidesk-WebUI/pull/100) by @AriaMoradi)
- (r903) migrate HorizantalPager to Mui 5 ([#92](https://github.com/Suwayomi/Tachidesk-WebUI/pull/92) by @AriaMoradi)
- (r904) migrate PageNumber to Mui 5 ([#90](https://github.com/Suwayomi/Tachidesk-WebUI/pull/90) by @AriaMoradi)
- (r905) Chapter filter is woking ([#114](https://github.com/Suwayomi/Tachidesk-WebUI/pull/114) by @abhijeetChawla)
- (r906) added extension search ([#115](https://github.com/Suwayomi/Tachidesk-WebUI/pull/115) by @abhijeetChawla)
- (r907) cleanup ([#117](https://github.com/Suwayomi/Tachidesk-WebUI/pull/117) by @AriaMoradi)
- (r908) handle search shortcuts ([#116](https://github.com/Suwayomi/Tachidesk-WebUI/pull/116) by @AriaMoradi)
- (r909) Refactor for Removing unnecesary UseEffect ([#118](https://github.com/Suwayomi/Tachidesk-WebUI/pull/118) by @abhijeetChawla)
- (r910) refactor ChapterList ([#125](https://github.com/Suwayomi/Tachidesk-WebUI/pull/125) by @abhijeetChawla)
- (r911) refactor ChapterOptions ([#126](https://github.com/Suwayomi/Tachidesk-WebUI/pull/126) by @abhijeetChawla)
# Server: v0.6.0 + WebUI: r893 # Server: v0.6.0 + WebUI: r893
## TL;DR ## TL;DR
- WebUI design went through a whole lot of changes, including - WebUI design went through a whole lot of changes, including
@@ -125,6 +273,8 @@
- (r892) add support for emptySearch ([#109](https://github.com/Suwayomi/Tachidesk-WebUI/pull/109) by @AriaMoradi) - (r892) add support for emptySearch ([#109](https://github.com/Suwayomi/Tachidesk-WebUI/pull/109) by @AriaMoradi)
- (r893) add support for MultiSelectListPreference ([#108](https://github.com/Suwayomi/Tachidesk-WebUI/pull/108) by @AriaMoradi) - (r893) add support for MultiSelectListPreference ([#108](https://github.com/Suwayomi/Tachidesk-WebUI/pull/108) by @AriaMoradi)
# Server: v0.5.4 + WebUI: r820 # Server: v0.5.4 + WebUI: r820
## TL;DR ## TL;DR
- Fixed ReadComicOnline, Toonily and possibly other sources not working - Fixed ReadComicOnline, Toonily and possibly other sources not working
+21 -11
View File
@@ -45,11 +45,14 @@ Ability to sync with Tachiyomi is a planned feature.
**You need a client/user interface app as a front-end for Tachidesk-Server, if you Directly Download Tachidesk-Server you'll get a bundled version of [Tachidesk-WebUI](https://github.com/Suwayomi/Tachidesk-WebUI) with it.** **You need a client/user interface app as a front-end for Tachidesk-Server, if you Directly Download Tachidesk-Server you'll get a bundled version of [Tachidesk-WebUI](https://github.com/Suwayomi/Tachidesk-WebUI) with it.**
Here's a list of known clients/user interfaces for Tachidesk-Server: Here's a list of known clients/user interfaces for Tachidesk-Server:
##### Actively Developed Cients
- [Tachidesk-WebUI](https://github.com/Suwayomi/Tachidesk-WebUI): The web/ElectronJS front-end that Tachidesk-Server is traditionally shipped with. Usually gets new features faster. - [Tachidesk-WebUI](https://github.com/Suwayomi/Tachidesk-WebUI): The web/ElectronJS front-end that Tachidesk-Server is traditionally shipped with. Usually gets new features faster.
- [Tachidesk-JUI](https://github.com/Suwayomi/Tachidesk-JUI): The native desktop front-end for Tachidesk-Server. Currently the most advanced. Downlading Tachidesk-JUI is not recommened for now, the current release is getting obsolete, a new version is to be released soon(TM). - [Tachidesk-JUI](https://github.com/Suwayomi/Tachidesk-JUI): The native desktop front-end for Tachidesk-Server. Currently the most advanced.
- [Tachidesk-qtui](https://github.com/Suwayomi/Tachidesk-qtui): A C++/Qt front-end for mobile devices(Android/linux), in super early stage of development. - [Tachidesk-qtui](https://github.com/Suwayomi/Tachidesk-qtui): A C++/Qt front-end for mobile devices(Android/linux), in super early stage of development.
- [Tachidesk-Sorayomi](https://github.com/Suwayomi/Tachidesk-Sorayomi): A Flutter front-end for Desktop(Linux, windows, etc.), Web and Android. UI and UX similar to Tachiyomi.
##### Inctive/Abandoned Cients
- [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.
- [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js, in super early stage of development. Seemingly abandoned.
## Is this application usable? Should I test it? ## Is this application usable? Should I test it?
Here is a list of current features: Here is a list of current features:
@@ -77,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
+7 -7
View File
@@ -5,7 +5,7 @@ import org.jmailen.gradle.kotlinter.tasks.LintTask
plugins { plugins {
kotlin("jvm") version kotlinVersion kotlin("jvm") version kotlinVersion
kotlin("plugin.serialization") version kotlinVersion kotlin("plugin.serialization") version kotlinVersion
id("org.jmailen.kotlinter") version "3.6.0" id("org.jmailen.kotlinter") version "3.8.0"
id("com.github.gmazzo.buildconfig") version "3.0.3" apply false id("com.github.gmazzo.buildconfig") version "3.0.3" apply false
} }
@@ -69,22 +69,22 @@ configure(projects) {
testImplementation(kotlin("test-junit5")) testImplementation(kotlin("test-junit5"))
// coroutines // coroutines
val coroutinesVersion = "1.5.2" val coroutinesVersion = "1.6.0"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
val kotlinSerializationVersion = "1.3.0-RC" val kotlinSerializationVersion = "1.3.2"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
// Dependency Injection // Dependency Injection
implementation("org.kodein.di:kodein-di-conf-jvm:7.8.0") implementation("org.kodein.di:kodein-di-conf-jvm:7.10.0")
// Logging // Logging
implementation("org.slf4j:slf4j-api:1.7.32") implementation("org.slf4j:slf4j-api:1.7.32")
implementation("ch.qos.logback:logback-classic:1.2.6") implementation("ch.qos.logback:logback-classic:1.2.6")
implementation("io.github.microutils:kotlin-logging:2.0.11") implementation("io.github.microutils:kotlin-logging:2.1.21")
// ReactiveX // ReactiveX
implementation("io.reactivex:rxjava:1.3.8") implementation("io.reactivex:rxjava:1.3.8")
@@ -92,7 +92,7 @@ configure(projects) {
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0") implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
// dependency both in AndroidCompat and extensions, version locked by Tachiyomi app/extensions // dependency both in AndroidCompat and extensions, version locked by Tachiyomi app/extensions
implementation("org.jsoup:jsoup:1.14.2") implementation("org.jsoup:jsoup:1.14.3")
// dependency of :AndroidCompat:Config // dependency of :AndroidCompat:Config
implementation("com.typesafe:config:1.4.1") implementation("com.typesafe:config:1.4.1")
@@ -102,7 +102,7 @@ configure(projects) {
implementation("net.harawata:appdirs:1.2.1") implementation("net.harawata:appdirs:1.2.1")
// dex2jar // dex2jar
val dex2jarVersion = "v26" val dex2jarVersion = "v35"
implementation("com.github.ThexXTURBOXx.dex2jar:dex-translator:$dex2jarVersion") implementation("com.github.ThexXTURBOXx.dex2jar:dex-translator:$dex2jarVersion")
implementation("com.github.ThexXTURBOXx.dex2jar:dex-tools:$dex2jarVersion") implementation("com.github.ThexXTURBOXx.dex2jar:dex-tools:$dex2jarVersion")
+4 -3
View File
@@ -7,14 +7,15 @@ import java.io.BufferedReader
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
const val kotlinVersion = "1.5.30" 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.0" val tachideskVersion = System.getenv("ProductVersion") ?: "v0.6.3"
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r893" val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r944"
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 {
Binary file not shown.
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
Vendored
+159 -110
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env sh #!/bin/sh
# #
# Copyright 2015 the original author or authors. # Copyright © 2015-2021 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -17,67 +17,101 @@
# #
############################################################################## ##############################################################################
## #
## Gradle start up script for UN*X # Gradle start up script for POSIX generated by Gradle.
## #
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
############################################################################## ##############################################################################
# Attempt to set APP_HOME # Attempt to set APP_HOME
# Resolve links: $0 may be a link # Resolve links: $0 may be a link
PRG="$0" app_path=$0
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do # Need this for daisy-chained symlinks.
ls=`ls -ld "$PRG"` while
link=`expr "$ls" : '.*-> \(.*\)$'` APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
if expr "$link" : '/.*' > /dev/null; then [ -h "$app_path" ]
PRG="$link" do
else ls=$( ls -ld "$app_path" )
PRG=`dirname "$PRG"`"/$link" link=${ls#*' -> '}
fi case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle" APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"` APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum" MAX_FD=maximum
warn () { warn () {
echo "$*" echo "$*"
} } >&2
die () { die () {
echo echo
echo "$*" echo "$*"
echo echo
exit 1 exit 1
} } >&2
# OS specific support (must be 'true' or 'false'). # OS specific support (must be 'true' or 'false').
cygwin=false cygwin=false
msys=false msys=false
darwin=false darwin=false
nonstop=false nonstop=false
case "`uname`" in case "$( uname )" in #(
CYGWIN* ) CYGWIN* ) cygwin=true ;; #(
cygwin=true Darwin* ) darwin=true ;; #(
;; MSYS* | MINGW* ) msys=true ;; #(
Darwin* ) NONSTOP* ) nonstop=true ;;
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables # IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java" JAVACMD=$JAVA_HOME/jre/sh/java
else else
JAVACMD="$JAVA_HOME/bin/java" JAVACMD=$JAVA_HOME/bin/java
fi fi
if [ ! -x "$JAVACMD" ] ; then if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi fi
else else
JAVACMD="java" JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the Please set the JAVA_HOME variable in your environment to match the
@@ -106,80 +140,95 @@ location of your Java installation."
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
MAX_FD_LIMIT=`ulimit -H -n` case $MAX_FD in #(
if [ $? -eq 0 ] ; then max*)
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD=$( ulimit -H -n ) ||
MAX_FD="$MAX_FD_LIMIT" warn "Could not query maximum file descriptor limit"
fi esac
ulimit -n $MAX_FD case $MAX_FD in #(
if [ $? -ne 0 ] ; then '' | soft) :;; #(
warn "Could not set maximum file descriptor limit: $MAX_FD" *)
fi ulimit -n "$MAX_FD" ||
else warn "Could not set maximum file descriptor limit to $MAX_FD"
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac esac
fi fi
# Escape application args # Collect all arguments for the java command, stacking in reverse order:
save () { # * args from the command line
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done # * the main class name
echo " " # * -classpath
} # * -D...appname settings
APP_ARGS=`save "$@"` # * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules # For Cygwin or MSYS, switch paths to Windows format before running java
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@" exec "$JAVACMD" "$@"
+51
View File
@@ -0,0 +1,51 @@
#!/bin/bash
# Copyright (C) Contributors to the Suwayomi project
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
echo "Creating DEB Package"
pkgname="tachidesk-server"
PkgName="Tachidesk-Server"
jar=$(ls ../server/build/*.jar | tail -n1)
pkgver="$(tmp="${jar%-*}" && echo "${tmp##*-}" | tr -d v)"
pkgrel=1
srcdir="$pkgname-$pkgver" # uses hyphen "-"
srctgz="${pkgname}_$pkgver.orig.tar.gz" # uses underscore "_"
deb="${pkgname}_$pkgver-${pkgrel}_all.deb"
Deb="${PkgName}_$pkgver-${pkgrel}_all.deb"
# Prepare
mkdir "$srcdir/"
cp "$jar" "$srcdir/$pkgname.jar"
cp "resources/$pkgname-browser-launcher.sh" "$srcdir/"
cp "resources/$pkgname-debug-launcher.sh" "$srcdir/"
cp "resources/$pkgname-electron-launcher-debian.sh" "$srcdir/$pkgname-electron-launcher.sh"
cp "resources/$pkgname.desktop" "$srcdir/"
cp "../server/src/main/resources/icon/faviconlogo.png" "$srcdir/$pkgname.png"
GZIP=-9 tar -cvzf "$srctgz" "$srcdir/"
cp -r "resources/debian" "$srcdir/"
sed -i "s/\${pkgver}/$pkgver/" "$srcdir/debian/changelog"
sed -i "s/\${pkgrel}/$pkgrel/" "$srcdir/debian/changelog"
# Build
mkdir "debuild/"
mv "$srctgz" "$srcdir/" "debuild/"
sudo apt install devscripts build-essential dh-exec
# --lintian-opts --profile are for building Debian packages on Ubuntu
cd "debuild/$srcdir/debian"
debuild -uc -us --lintian-opts --profile debian
cd -
# clean up from possible previous runs
if [ -f "../server/build/$Deb" ]; then
rm "../server/build/$Deb"
fi
mv "debuild/$deb" "../server/build/$Deb"
rm -rf "debuild/"
+5
View File
@@ -0,0 +1,5 @@
tachidesk-server (${pkgver}-${pkgrel}) unstable; urgency=medium
* See CHANGELOG.md on https://github.com/Suwayomi/Tachidesk-Server
-- Mahor Foruzesh <mahorforuzesh@pm.me> Fri, 14 Jan 2022 00:00:00 +0000
+14
View File
@@ -0,0 +1,14 @@
Source: tachidesk-server
Section: web
Priority: optional
Maintainer: Mahor Foruzesh <mahorforuzesh@pm.me>
Build-Depends: debhelper-compat (= 12), dh-exec
Standards-Version: 4.5.1
Homepage: https://github.com/Suwayomi/Tachidesk-Server
Package: tachidesk-server
Architecture: all
Depends: ${misc:Depends}, default-jre-headless (>= 8)
Description: Manga Reader
A free and open source manga reader server that runs extensions built for Tachiyomi.
Tachidesk is an independent Tachiyomi compatible software and is not a Fork of Tachiyomi.
+381
View File
@@ -0,0 +1,381 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: tachidesk-server
Upstream-Contact: https://discord.gg/DDZdqZWaHA
Source: https://github.com/Suwayomi/Tachidesk-Server
Files: *
Copyright: Contributors to the Suwayomi project
License: MPL-2.0
Mozilla Public License Version 2.0
==================================
.
1. Definitions
--------------
.
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
.
1.3. "Contribution"
means Covered Software of a particular Contributor.
.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
.
1.5. "Incompatible With Secondary Licenses"
means
.
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
.
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
.
1.8. "License"
means this document.
.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
.
1.10. "Modifications"
means any of the following:
.
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
.
(b) any new file in Source Code Form that contains any Covered
Software.
.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
.
2. License Grants and Conditions
--------------------------------
.
2.1. Grants
.
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
.
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
.
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
.
2.2. Effective Date
.
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
.
2.3. Limitations on Grant Scope
.
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
.
(a) for any code that a Contributor has removed from Covered Software;
or
.
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
.
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
.
2.4. Subsequent Licenses
.
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
.
2.5. Representation
.
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
.
2.6. Fair Use
.
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
.
2.7. Conditions
.
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
.
3. Responsibilities
-------------------
.
3.1. Distribution of Source Form
.
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
.
3.2. Distribution of Executable Form
.
If You distribute Covered Software in Executable Form then:
.
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
.
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
.
3.3. Distribution of a Larger Work
.
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
.
3.4. Notices
.
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
.
3.5. Application of Additional Terms
.
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
.
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
.
5. Termination
--------------
.
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
.
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
.
8. Litigation
-------------
.
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
.
9. Miscellaneous
----------------
.
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
.
10. Versions of the License
---------------------------
.
10.1. New Versions
.
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
.
10.2. Effect of New Versions
.
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
.
10.3. Modified Versions
.
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
.
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
.
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
.
Exhibit A - Source Code Form License Notice
-------------------------------------------
.
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/.
.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
.
You may add additional accurate notices of copyright ownership.
.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
.
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/dh-exec
tachidesk-server-browser-launcher.sh => usr/bin/tachidesk-server-browser
tachidesk-server-debug-launcher.sh => usr/bin/tachidesk-server-debug
tachidesk-server-electron-launcher.sh => usr/bin/tachidesk-server-electron
tachidesk-server.jar usr/share/java/tachidesk-server/
tachidesk-server.png usr/share/pixmaps/
tachidesk-server.desktop usr/share/applications/
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/make -f
# See debhelper(7) (uncomment to enable)
# output every command that modifies files on the build system.
#export DH_VERBOSE = 1
%:
dh $@
override_dh_strip_nondeterminism:
true
+1
View File
@@ -0,0 +1 @@
3.0 (quilt)
+1
View File
@@ -0,0 +1 @@
tachidesk-server.png
+86
View File
@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product Id="*" UpgradeCode="*"
Version="$(var.ProductVersion)" Language="1033" Name="Tachidesk Server" Manufacturer="Suwayomi">
<Package InstallerVersion="300" Compressed="yes" />
<Media Id="1" Cabinet="Tachidesk_Server.cab" EmbedCab="yes" />
<Condition Message="This version of Windows does not support deploying 64-bit packages.">
VersionNT64
</Condition>
<!-- Directory -->
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFiles64Folder">
<Directory Id="INSTALLDIR" Name="Tachidesk-Server" >
<Directory Id="jre"/>
<Directory Id="electron"/>
</Directory>
</Directory>
<Directory Id="ProgramMenuFolder">
<Directory Id="ProgramMenuDir" Name="Tachidesk-Server">
<Component Id="ProgramMenuDir" Guid="*">
<RemoveFolder Id="ProgramMenuDir" On="uninstall"/>
<RegistryValue Root="HKCU" Key="Software\[Manufacturer]\[ProductName]" Type="string" Value="" KeyPath="yes"/>
</Component>
</Directory>
</Directory>
<Directory Id="DesktopFolder" />
</Directory>
<!-- Component -->
<DirectoryRef Id="INSTALLDIR">
<Component Id="TachideskJAR" Guid="*" Win64="yes">
<File Id="Tachidesk.jar" Source="$(var.SourceDir)/Tachidesk.jar" KeyPath="yes" />
</Component>
<Component Id="TachideskBrowserBAT" Guid="*" Win64="yes">
<File Id="TachideskBrowser.bat" Source="$(var.SourceDir)/Tachidesk Browser Launcher.bat" KeyPath="yes" >
<Shortcut Id="TachideskBrowser.lnk" Name="Tachidesk Browser" Directory="INSTALLDIR"
WorkingDirectory="INSTALLDIR" Icon="Tachidesk.ico" IconIndex="0" Advertise="yes" />
<Shortcut Id="DesktopTachideskBrowser.lnk" Name="Tachidesk Browser" Directory="DesktopFolder"
WorkingDirectory="INSTALLDIR" Icon="Tachidesk.ico" IconIndex="0" Advertise="yes" />
<Shortcut Id="ProgramMenuTachideskBrowser.lnk" Name="Tachidesk Browser" Directory="ProgramMenuDir"
WorkingDirectory="INSTALLDIR" Icon="Tachidesk.ico" IconIndex="0" Advertise="yes"
Description="A free and open source manga reader that runs extensions built for Tachiyomi." />
</File>
</Component>
<Component Id="TachideskDebugBAT" Guid="*" Win64="yes">
<File Id="TachideskDebug.bat" Source="$(var.SourceDir)/Tachidesk Debug Launcher.bat" KeyPath="yes">
<Shortcut Id="TachideskDebug.lnk" Name="Tachidesk Debug" Directory="INSTALLDIR"
WorkingDirectory="INSTALLDIR" Icon="Tachidesk.ico" IconIndex="0" Advertise="yes" />
<Shortcut Id="ProgramMenuTachideskDebug.lnk" Name="Tachidesk Debug" Directory="ProgramMenuDir"
WorkingDirectory="INSTALLDIR" Icon="Tachidesk.ico" IconIndex="0" Advertise="yes"
Description="Launches Tachidesk with debug logs attached. If Tachidesk doesn't work for you, running this can give you insight into why." />
</File>
</Component>
<Component Id="TachideskElectronBAT" Guid="*" Win64="yes">
<File Id="TachideskElectron.bat" Source="$(var.SourceDir)/Tachidesk Electron Launcher.bat" KeyPath="yes">
<Shortcut Id="TachideskElectron.lnk" Name="Tachidesk Electron" Directory="INSTALLDIR"
WorkingDirectory="INSTALLDIR" Icon="Tachidesk.ico" IconIndex="0" Advertise="yes" />
<Shortcut Id="DesktopTachideskElectron.lnk" Name="Tachidesk Electron" Directory="DesktopFolder"
WorkingDirectory="INSTALLDIR" Icon="Tachidesk.ico" IconIndex="0" Advertise="yes" />
<Shortcut Id="ProgramMenuTachideskElectron.lnk" Name="Tachidesk Electron" Directory="ProgramMenuDir"
WorkingDirectory="INSTALLDIR" Icon="Tachidesk.ico" IconIndex="0" Advertise="yes"
Description="Launches Tachidesk inside Electron as a desktop applicaton." />
</File>
</Component>
</DirectoryRef>
<!-- Feature -->
<Feature Id="Tachidesk_Server" Title="Tachidesk-Server" Level="1">
<ComponentGroupRef Id="jre" />
<ComponentRef Id="TachideskJAR" />
<ComponentRef Id="TachideskBrowserBAT" />
<ComponentRef Id="TachideskDebugBAT" />
<ComponentRef Id="ProgramMenuDir" />
<ComponentGroupRef Id="electron" />
<ComponentRef Id="TachideskElectronBAT" />
</Feature>
<Icon Id="Tachidesk.ico" SourceFile="$(var.Icon)" />
<Property Id="ARPPRODUCTICON" Value="Tachidesk.ico" /> <!-- Icon in Add/Remove Programs -->
</Product>
</Wix>
+82
View File
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product Id="*" UpgradeCode="*"
Version="$(var.ProductVersion)" Language="1033" Name="Tachidesk Server" Manufacturer="Suwayomi">
<Package InstallerVersion="300" Compressed="yes" />
<Media Id="1" Cabinet="Tachidesk_Server.cab" EmbedCab="yes" />
<!-- Directory -->
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFilesFolder">
<Directory Id="INSTALLDIR" Name="Tachidesk-Server" >
<Directory Id="jre"/>
<Directory Id="electron"/>
</Directory>
</Directory>
<Directory Id="ProgramMenuFolder">
<Directory Id="ProgramMenuDir" Name="Tachidesk-Server">
<Component Id="ProgramMenuDir" Guid="*">
<RemoveFolder Id="ProgramMenuDir" On="uninstall"/>
<RegistryValue Root="HKCU" Key="Software\[Manufacturer]\[ProductName]" Type="string" Value="" KeyPath="yes"/>
</Component>
</Directory>
</Directory>
<Directory Id="DesktopFolder" />
</Directory>
<!-- Component -->
<DirectoryRef Id="INSTALLDIR">
<Component Id="TachideskJAR" Guid="*">
<File Id="Tachidesk.jar" Source="$(var.SourceDir)/Tachidesk.jar" KeyPath="yes" />
</Component>
<Component Id="TachideskBrowserBAT" Guid="*">
<File Id="TachideskBrowser.bat" Source="$(var.SourceDir)/Tachidesk Browser Launcher.bat" KeyPath="yes" >
<Shortcut Id="TachideskBrowser.lnk" Name="Tachidesk Browser" Directory="INSTALLDIR"
WorkingDirectory="INSTALLDIR" Icon="Tachidesk.ico" IconIndex="0" Advertise="yes" />
<Shortcut Id="DesktopTachideskBrowser.lnk" Name="Tachidesk Browser" Directory="DesktopFolder"
WorkingDirectory="INSTALLDIR" Icon="Tachidesk.ico" IconIndex="0" Advertise="yes" />
<Shortcut Id="ProgramMenuTachideskBrowser.lnk" Name="Tachidesk Browser" Directory="ProgramMenuDir"
WorkingDirectory="INSTALLDIR" Icon="Tachidesk.ico" IconIndex="0" Advertise="yes"
Description="A free and open source manga reader that runs extensions built for Tachiyomi." />
</File>
</Component>
<Component Id="TachideskDebugBAT" Guid="*">
<File Id="TachideskDebug.bat" Source="$(var.SourceDir)/Tachidesk Debug Launcher.bat" KeyPath="yes">
<Shortcut Id="TachideskDebug.lnk" Name="Tachidesk Debug" Directory="INSTALLDIR"
WorkingDirectory="INSTALLDIR" Icon="Tachidesk.ico" IconIndex="0" Advertise="yes" />
<Shortcut Id="ProgramMenuTachideskDebug.lnk" Name="Tachidesk Debug" Directory="ProgramMenuDir"
WorkingDirectory="INSTALLDIR" Icon="Tachidesk.ico" IconIndex="0" Advertise="yes"
Description="Launches Tachidesk with debug logs attached. If Tachidesk doesn't work for you, running this can give you insight into why." />
</File>
</Component>
<Component Id="TachideskElectronBAT" Guid="*">
<File Id="TachideskElectron.bat" Source="$(var.SourceDir)/Tachidesk Electron Launcher.bat" KeyPath="yes">
<Shortcut Id="TachideskElectron.lnk" Name="Tachidesk Electron" Directory="INSTALLDIR"
WorkingDirectory="INSTALLDIR" Icon="Tachidesk.ico" IconIndex="0" Advertise="yes" />
<Shortcut Id="DesktopTachideskElectron.lnk" Name="Tachidesk Electron" Directory="DesktopFolder"
WorkingDirectory="INSTALLDIR" Icon="Tachidesk.ico" IconIndex="0" Advertise="yes" />
<Shortcut Id="ProgramMenuTachideskElectron.lnk" Name="Tachidesk Electron" Directory="ProgramMenuDir"
WorkingDirectory="INSTALLDIR" Icon="Tachidesk.ico" IconIndex="0" Advertise="yes"
Description="Launches Tachidesk inside Electron as a desktop applicaton." />
</File>
</Component>
</DirectoryRef>
<!-- Feature -->
<Feature Id="Tachidesk_Server" Title="Tachidesk-Server" Level="1">
<ComponentGroupRef Id="jre" />
<ComponentRef Id="TachideskJAR" />
<ComponentRef Id="TachideskBrowserBAT" />
<ComponentRef Id="TachideskDebugBAT" />
<ComponentRef Id="ProgramMenuDir" />
<ComponentGroupRef Id="electron" />
<ComponentRef Id="TachideskElectronBAT" />
</Feature>
<Icon Id="Tachidesk.ico" SourceFile="$(var.Icon)" />
<Property Id="ARPPRODUCTICON" Value="Tachidesk.ico" /> <!-- Icon in Add/Remove Programs -->
</Product>
</Wix>
@@ -1,3 +0,0 @@
#!/bin/sh
./jre/bin/java -jar Tachidesk.jar
@@ -1,3 +0,0 @@
#!/bin/sh
./jre/bin/java -Dsuwayomi.tachidesk.config.server.debugLogsEnabled=true -jar 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,2 @@
#!/bin/sh
exec /usr/bin/java -jar /usr/share/java/tachidesk-server/tachidesk-server.jar
@@ -0,0 +1,2 @@
#!/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.
You can install it with these commands:
sudo apt install npm
sudo npm install electron -g"
exit 1
fi
exec /usr/bin/java -Dsuwayomi.tachidesk.config.server.webUIInterface=electron -Dsuwayomi.tachidesk.config.server.electronPath=/usr/bin/electron -jar /usr/share/java/tachidesk-server/tachidesk-server.jar
@@ -0,0 +1,3 @@
#!/bin/sh
exec /usr/bin/java -Dsuwayomi.tachidesk.config.server.webUIInterface=electron -Dsuwayomi.tachidesk.config.server.electronPath=/usr/bin/electron -jar /usr/share/java/tachidesk-server/tachidesk-server.jar
@@ -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;
+5 -7
View File
@@ -1,4 +1,4 @@
#/bin/bash #!/bin/bash
# Copyright (C) Contributors to the Suwayomi project # Copyright (C) Contributors to the Suwayomi project
# #
@@ -40,11 +40,9 @@ jar=$(ls ../server/build/*.jar | tail -n1)
jar_name=$(echo $jar | cut -d'/' -f4) jar_name=$(echo $jar | cut -d'/' -f4)
release_name=$(echo $jar_name | sed 's/.jar//')-$arch release_name=$(echo $jar_name | sed 's/.jar//')-$arch
# make release dir # make release dir
mkdir $release_name mkdir $release_name
echo "Dealing with jre..." echo "Dealing with jre..."
if [ ! -f $jre ]; then if [ ! -f $jre ]; then
curl -L $jre_url -o $jre curl -L $jre_url -o $jre
@@ -59,11 +57,11 @@ fi
unzip $electron -d $release_name/electron unzip $electron -d $release_name/electron
# copy artifacts # copy artifacts
cp $jar $release_name/Tachidesk.jar cp $jar $release_name/Tachidesk-Server.jar
if [ $os = linux ]; then if [ $os = linux ]; then
cp "resources/tachidesk-browser-launcher.sh" $release_name cp "resources/tachidesk-server-browser-launcher.sh" $release_name
cp "resources/tachidesk-debug-launcher.sh" $release_name cp "resources/tachidesk-server-debug-launcher.sh" $release_name
cp "resources/tachidesk-electron-launcher.sh" $release_name cp "resources/tachidesk-server-electron-launcher.sh" $release_name
elif [ $os = macOS ]; then elif [ $os = macOS ]; then
cp "resources/Tachidesk Browser Launcher.command" $release_name cp "resources/Tachidesk Browser Launcher.command" $release_name
cp "resources/Tachidesk Debug Launcher.command" $release_name cp "resources/Tachidesk Debug Launcher.command" $release_name
+22 -4
View File
@@ -7,8 +7,8 @@
# file, You can obtain one at https://mozilla.org/MPL/2.0/. # file, You can obtain one at https://mozilla.org/MPL/2.0/.
electron_version="v14.0.0" electron_version="v14.0.0"
arch=$1
if [ $1 = "win32" ]; then if [ $arch = "win32" ]; then
jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip" jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
jre_release="jdk8u292-b10" jre_release="jdk8u292-b10"
jre_url="https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/$jre_release/$jre" jre_url="https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/$jre_release/$jre"
@@ -30,11 +30,9 @@ jar=$(ls ../server/build/*.jar | tail -n1)
jar_name=$(echo $jar | cut -d'/' -f4) jar_name=$(echo $jar | cut -d'/' -f4)
release_name=$(echo $jar_name | sed 's/.jar//')-$arch release_name=$(echo $jar_name | sed 's/.jar//')-$arch
# make release dir # make release dir
mkdir $release_name mkdir $release_name
echo "Dealing with jre..." echo "Dealing with jre..."
if [ ! -f $jre ]; then if [ ! -f $jre ]; then
curl -L $jre_url -o $jre curl -L $jre_url -o $jre
@@ -62,6 +60,7 @@ if [ $CI = true ]; then
sudo add-apt-repository ppa:cybermax-dexter/sdl2-backport 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-add-repository "deb https://dl.winehq.org/wine-builds/ubuntu $(lsb_release -cs) main"
sudo apt install --install-recommends winehq-stable sudo apt install --install-recommends winehq-stable
sudo apt install -y wixl
fi fi
# this script assumes that wine is installed here on out # this script assumes that wine is installed here on out
@@ -76,11 +75,30 @@ cp "resources/Tachidesk Electron Launcher.bat" $release_name
zip_name=$release_name.zip zip_name=$release_name.zip
zip -9 -r $zip_name $release_name 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 rm -rf $release_name
# clean up from possible previous runs # clean up from possible previous runs
if [ -f ../server/build/$zip_name ]; then if [ -f ../server/build/$zip_name ]; then
rm ../server/build/$zip_name rm ../server/build/$zip_name
fi fi
if [ -f ../server/build/$msi_name ]; then
rm ../server/build/$msi_name
fi
mv $zip_name ../server/build/ mv $zip_name ../server/build/
mv $msi_name ../server/build/
+22 -14
View File
@@ -3,21 +3,21 @@ import java.time.Instant
plugins { plugins {
application application
id("com.github.johnrengelman.shadow") version "7.0.0" id("com.github.johnrengelman.shadow") version "7.1.2"
id("com.github.gmazzo.buildconfig") id("com.github.gmazzo.buildconfig")
} }
dependencies { dependencies {
// okhttp // okhttp
val okhttpVersion = "4.9.1" // version is locked by Tachiyomi extensions val okhttpVersion = "4.9.3" // Major version is locked by Tachiyomi extensions
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion") implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion") implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion") implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
implementation("com.squareup.okio:okio:2.10.0") implementation("com.squareup.okio:okio:3.0.0")
// Javalin api // Javalin api
implementation("io.javalin:javalin:4.1.1") implementation("io.javalin:javalin:4.2.0")
implementation("io.javalin:javalin-openapi:4.1.1") implementation("io.javalin:javalin-openapi:4.2.0")
// jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency` // jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
val jacksonVersion = "2.12.4" val jacksonVersion = "2.12.4"
implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
@@ -41,11 +41,10 @@ dependencies {
// dependencies of Tachiyomi extensions, some are duplicate, keeping it here for reference // dependencies of Tachiyomi extensions, some are duplicate, keeping it here for reference
implementation("com.github.inorichi.injekt:injekt-core:65b0440") implementation("com.github.inorichi.injekt:injekt-core:65b0440")
implementation("com.squareup.okhttp3:okhttp:4.9.1") 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.2") implementation("org.jsoup:jsoup:1.14.3")
implementation("com.google.code.gson:gson:2.8.8") implementation("app.cash.quickjs:quickjs-jvm:0.9.2")
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
// Sort // Sort
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1") implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
@@ -54,11 +53,14 @@ dependencies {
implementation("org.ow2.asm:asm:9.2") implementation("org.ow2.asm:asm:9.2")
// Disk & File // Disk & File
implementation("net.lingala.zip4j:zip4j:2.9.0") 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.52.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
@@ -72,10 +74,13 @@ dependencies {
// implementation(fileTree("lib/")) // implementation(fileTree("lib/"))
implementation(kotlin("script-runtime")) implementation(kotlin("script-runtime"))
testImplementation("io.mockk:mockk:1.9.3") testImplementation("io.mockk:mockk:1.12.2")
} }
application { application {
applicationDefaultJvmArgs = listOf(
"-Djunrar.extractor.thread-keep-alive-seconds=30"
)
mainClass.set(MainClass) mainClass.set(MainClass)
} }
@@ -105,8 +110,11 @@ 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"))
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
}
@@ -7,8 +7,6 @@ package eu.kanade.tachiyomi
* 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 android.app.Application
import com.google.gson.Gson
// import eu.kanade.tachiyomi.data.cache.ChapterCache // import eu.kanade.tachiyomi.data.cache.ChapterCache
// import eu.kanade.tachiyomi.data.cache.CoverCache // import eu.kanade.tachiyomi.data.cache.CoverCache
// import eu.kanade.tachiyomi.data.database.DatabaseHelper // import eu.kanade.tachiyomi.data.database.DatabaseHelper
@@ -17,6 +15,7 @@ import com.google.gson.Gson
// import eu.kanade.tachiyomi.data.sync.LibrarySyncManager // import eu.kanade.tachiyomi.data.sync.LibrarySyncManager
// import eu.kanade.tachiyomi.data.track.TrackManager // import eu.kanade.tachiyomi.data.track.TrackManager
// import eu.kanade.tachiyomi.extension.ExtensionManager // import eu.kanade.tachiyomi.extension.ExtensionManager
import android.app.Application
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import rx.Observable import rx.Observable
@@ -53,8 +52,6 @@ class AppModule(val app: Application) : InjektModule {
// //
// addSingletonFactory { LibrarySyncManager(app) } // addSingletonFactory { LibrarySyncManager(app) }
addSingletonFactory { Gson() }
addSingletonFactory { Json { ignoreUnknownKeys = true } } addSingletonFactory { Json { ignoreUnknownKeys = true } }
// Asynchronously init expensive components for a faster cold start // Asynchronously init expensive components for a faster cold start
@@ -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";
}
@@ -1,7 +1,8 @@
package eu.kanade.tachiyomi.network package eu.kanade.tachiyomi.network
// import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Call import okhttp3.Call
import okhttp3.Callback import okhttp3.Callback
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@@ -11,6 +12,8 @@ import okhttp3.internal.closeQuietly
import rx.Observable import rx.Observable
import rx.Producer import rx.Producer
import rx.Subscription import rx.Subscription
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.fullType
import java.io.IOException import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
@@ -124,3 +127,12 @@ fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListene
return progressClient.newCall(request) return progressClient.newCall(request)
} }
inline fun <reified T> Response.parseAs(): T {
// Avoiding Injekt.get<Json>() due to compiler issues
val json = Injekt.getInstance<Json>(fullType<Json>().type)
this.use {
val responseBody = it.body?.string().orEmpty()
return json.decodeFromString(responseBody)
}
}
@@ -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
@@ -293,12 +293,15 @@ class LocalSource : CatalogueSource {
return when (getFormat(chapterFile)) { return when (getFormat(chapterFile)) {
is Directory -> { is Directory -> {
Observable.just( Observable.just(
chapterFile.listFiles().orEmpty().sortedBy { it.name }.mapIndexed { index, page -> chapterFile.listFiles().orEmpty()
Page( .sortedBy { it.name }
index, .filter { !it.isDirectory && ImageUtil.isImage(it.name, it::inputStream) }
imageUrl = applicationDirs.localMangaRoot + "/" + chapter.url + "/" + page.name .mapIndexed { index, page ->
) Page(
} index,
imageUrl = applicationDirs.localMangaRoot + "/" + chapter.url + "/" + page.name
)
}
) )
} }
is Zip -> { is Zip -> {
@@ -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()
@@ -2,7 +2,7 @@ package suwayomi.tachidesk
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -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)
}
)
} }
@@ -10,7 +10,7 @@ import uy.kohesive.injekt.injectLazy
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -24,97 +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)
get("summary", UpdateController::updateSummary) post("reset", UpdateController.reset)
get("summary", UpdateController.updateSummary)
ws("", UpdateController::categoryUpdateWS) ws("", UpdateController::categoryUpdateWS)
} }
} }
@@ -1,17 +1,20 @@
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
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -19,78 +22,155 @@ 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")
} }
) body<ByteArray>("") {
} }
},
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<List<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<List<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<List<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,14 +7,18 @@ 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.CategoryManga import suwayomi.tachidesk.manga.impl.CategoryManga
import suwayomi.tachidesk.manga.impl.Chapter import suwayomi.tachidesk.manga.impl.Chapter
import suwayomi.tachidesk.manga.impl.Library import suwayomi.tachidesk.manga.impl.Library
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.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
@@ -28,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 ->
@@ -39,145 +43,284 @@ object MangaController {
) )
}, },
withResults = { withResults = {
json<MangaDataClass>("OK") json<MangaDataClass>(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
} }
) )
/** 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<List<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<List<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 { Chapter.getChapter(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.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
import javax.sound.sampled.SourceDataLine
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<List<SourceDataLine>>(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<SourceDataLine>(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<List<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<List<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,13 +11,20 @@ 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.MangaDataClass
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.pathParam
import suwayomi.tachidesk.server.util.withOperation
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -27,35 +33,57 @@ 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<PaginatedList<MangaDataClass>>(HttpCode.OK)
} }
addCategoriesToUpdateQueue(categoriesForUpdate, true) )
ctx.status(HttpCode.OK)
} 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>()
@@ -82,8 +110,42 @@ 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(
documentWith = {
withOperation {
summary("Updater reset")
description("Stops and resets the Updater")
}
},
behaviorOf = { ctx ->
val updater by DI.global.instance<IUpdater>()
logger.info { "Resetting Updater" }
ctx.future(
future {
updater.reset()
}.thenApply {
ctx.status(HttpCode.OK)
}
)
},
withResults = {
httpCode(HttpCode.OK)
}
)
} }
@@ -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)
} }
} }
@@ -2,17 +2,17 @@ package suwayomi.tachidesk.manga.impl
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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 eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.SortOrder.ASC
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
@@ -20,11 +20,9 @@ import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.manga.impl.Manga.getManga import suwayomi.tachidesk.manga.impl.Manga.getManga
import suwayomi.tachidesk.manga.impl.Page.getPageName
import suwayomi.tachidesk.manga.impl.util.getChapterDir import suwayomi.tachidesk.manga.impl.util.getChapterDir
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
import suwayomi.tachidesk.manga.model.dataclass.MangaChapterDataClass import suwayomi.tachidesk.manga.model.dataclass.MangaChapterDataClass
import suwayomi.tachidesk.manga.model.dataclass.PaginatedList import suwayomi.tachidesk.manga.model.dataclass.PaginatedList
@@ -104,14 +102,19 @@ object Chapter {
} }
} }
// clear any orphaned chapters that are in the db but not in `chapterList` // clear any orphaned/duplicate chapters that are in the db but not in `chapterList`
val dbChapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() } val dbChapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() }
if (dbChapterCount > chapterCount) { // we got some clean up due if (dbChapterCount > chapterCount) { // we got some clean up due
val dbChapterList = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.toList() } val dbChapterList = transaction {
ChapterTable.select { ChapterTable.manga eq mangaId }.orderBy(ChapterTable.url to ASC).toList()
}
val chapterUrls = chapterList.map { it.url }.toSet() val chapterUrls = chapterList.map { it.url }.toSet()
dbChapterList.forEach { dbChapter -> dbChapterList.forEachIndexed { index, dbChapter ->
if (!chapterUrls.contains(dbChapter[ChapterTable.url])) { if (
!chapterUrls.contains(dbChapter[ChapterTable.url]) || // is orphaned
(index < dbChapterList.lastIndex && dbChapter[ChapterTable.url] == dbChapterList[index + 1][ChapterTable.url]) // is duplicate
) {
transaction { transaction {
PageTable.deleteWhere { PageTable.chapter eq dbChapter[ChapterTable.id] } PageTable.deleteWhere { PageTable.chapter eq dbChapter[ChapterTable.id] }
ChapterTable.deleteWhere { ChapterTable.id eq dbChapter[ChapterTable.id] } ChapterTable.deleteWhere { ChapterTable.id eq dbChapter[ChapterTable.id] }
@@ -154,102 +157,6 @@ object Chapter {
} }
} }
/** used to display a chapter, get a chapter in order to show it's pages */
suspend fun getChapter(chapterIndex: Int, mangaId: Int): ChapterDataClass {
val chapterEntry = transaction {
ChapterTable.select {
(ChapterTable.sourceOrder eq chapterIndex) and (ChapterTable.manga eq mangaId)
}.first()
}
val isPartiallyDownloaded =
!(chapterEntry[ChapterTable.isDownloaded] && firstPageExists(mangaId, chapterEntry[ChapterTable.id].value))
return if (isPartiallyDownloaded) {
// chapter files may have been deleted
transaction {
ChapterTable.update({ (ChapterTable.sourceOrder eq chapterIndex) and (ChapterTable.manga eq mangaId) }) {
it[isDownloaded] = false
}
}
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
val pageList = source.fetchPageList(
SChapter.create().apply {
url = chapterEntry[ChapterTable.url]
name = chapterEntry[ChapterTable.name]
}
).awaitSingle()
val chapterId = chapterEntry[ChapterTable.id].value
val chapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() }
// update page list for this chapter
transaction {
pageList.forEach { page ->
val pageEntry = transaction {
PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }
.firstOrNull()
}
if (pageEntry == null) {
PageTable.insert {
it[index] = page.index
it[url] = page.url
it[imageUrl] = page.imageUrl
it[chapter] = chapterId
}
} else {
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }) {
it[url] = page.url
it[imageUrl] = page.imageUrl
}
}
}
}
val pageCount = pageList.count()
transaction {
ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder eq chapterIndex) }) {
it[ChapterTable.pageCount] = pageCount
}
}
return ChapterDataClass(
chapterEntry[ChapterTable.url],
chapterEntry[ChapterTable.name],
chapterEntry[ChapterTable.date_upload],
chapterEntry[ChapterTable.chapter_number],
chapterEntry[ChapterTable.scanlator],
mangaId,
chapterEntry[ChapterTable.isRead],
chapterEntry[ChapterTable.isBookmarked],
chapterEntry[ChapterTable.lastPageRead],
chapterEntry[ChapterTable.lastReadAt],
chapterEntry[ChapterTable.sourceOrder],
chapterEntry[ChapterTable.fetchedAt],
chapterEntry[ChapterTable.isDownloaded],
pageCount,
chapterCount.toInt(),
getChapterMetaMap(chapterEntry[ChapterTable.id])
)
} else {
ChapterTable.toDataClass(chapterEntry)
}
}
private fun firstPageExists(mangaId: Int, chapterId: Int): Boolean {
val chapterDir = getChapterDir(mangaId, chapterId)
return ImageResponse.findFileNameStartingWith(
chapterDir,
getPageName(1)
) != null
}
fun modifyChapter( fun modifyChapter(
mangaId: Int, mangaId: Int,
chapterIndex: Int, chapterIndex: Int,
@@ -294,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
@@ -303,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 }
} }
} }
} }
@@ -2,15 +2,17 @@ package suwayomi.tachidesk.manga.impl
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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 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")
} }
} }
@@ -2,7 +2,7 @@ package suwayomi.tachidesk.manga.impl
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -2,7 +2,7 @@ package suwayomi.tachidesk.manga.impl
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -2,7 +2,7 @@ package suwayomi.tachidesk.manga.impl
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -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
@@ -2,7 +2,7 @@ package suwayomi.tachidesk.manga.impl
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -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()
) )
} }
} }
@@ -2,7 +2,7 @@ package suwayomi.tachidesk.manga.impl.backup
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -2,12 +2,11 @@ package suwayomi.tachidesk.manga.impl.backup.proto
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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.coroutines.runBlocking
import okio.buffer import okio.buffer
import okio.gzip import okio.gzip
import okio.sink import okio.sink
@@ -17,7 +16,6 @@ import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.manga.impl.CategoryManga import suwayomi.tachidesk.manga.impl.CategoryManga
import suwayomi.tachidesk.manga.impl.Chapter
import suwayomi.tachidesk.manga.impl.backup.BackupFlags import suwayomi.tachidesk.manga.impl.backup.BackupFlags
import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupCategory import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupCategory
@@ -26,6 +24,7 @@ import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupManga
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSerializer import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSerializer
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSource import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSource
import suwayomi.tachidesk.manga.model.table.CategoryTable import suwayomi.tachidesk.manga.model.table.CategoryTable
import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.manga.model.table.MangaStatus import suwayomi.tachidesk.manga.model.table.MangaStatus
import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.manga.model.table.SourceTable import suwayomi.tachidesk.manga.model.table.SourceTable
@@ -76,7 +75,14 @@ object ProtoBackupExport : ProtoBackupBase() {
val mangaId = mangaRow[MangaTable.id].value val mangaId = mangaRow[MangaTable.id].value
if (flags.includeChapters) { if (flags.includeChapters) {
val chapters = runBlocking { Chapter.getChapterList(mangaId) } val chapters = transaction {
ChapterTable.select { ChapterTable.manga eq mangaId }
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC)
.map {
ChapterTable.toDataClass(it)
}
}
backupManga.chapters = chapters.map { backupManga.chapters = chapters.map {
BackupChapter( BackupChapter(
it.url, it.url,
@@ -0,0 +1,147 @@
package suwayomi.tachidesk.manga.impl.chapter
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.manga.impl.Page.getPageName
import suwayomi.tachidesk.manga.impl.util.getChapterDir
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.manga.model.table.PageTable
import suwayomi.tachidesk.manga.model.table.toDataClass
suspend fun getChapterDownloadReady(chapterIndex: Int, mangaId: Int): ChapterDataClass {
val chapter = ChapterForDownload(chapterIndex, mangaId)
return chapter.asDownloadReady()
}
private class ChapterForDownload(
private val chapterIndex: Int,
private val mangaId: Int
) {
suspend fun asDownloadReady(): ChapterDataClass {
if (isNotCompletelyDownloaded()) {
markAsNotDownloaded()
val pageList = fetchPageList()
updateDatabasePages(pageList)
}
return asDataClass()
}
private fun asDataClass() = ChapterTable.toDataClass(chapterEntry)
var chapterEntry: ResultRow = freshChapterEntry()
private fun freshChapterEntry() = transaction {
ChapterTable.select {
(ChapterTable.sourceOrder eq chapterIndex) and (ChapterTable.manga eq mangaId)
}.first()
}
private suspend fun fetchPageList(): List<Page> {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
return source.fetchPageList(
SChapter.create().apply {
url = chapterEntry[ChapterTable.url]
name = chapterEntry[ChapterTable.name]
}
).awaitSingle()
}
private fun markAsNotDownloaded() {
// chapter may be downloaded but if we are here, then images might be deleted and database data be false
transaction {
ChapterTable.update({ (ChapterTable.sourceOrder eq chapterIndex) and (ChapterTable.manga eq mangaId) }) {
it[isDownloaded] = false
}
}
}
private fun updateDatabasePages(pageList: List<Page>) {
val chapterId = chapterEntry[ChapterTable.id].value
val chapterIndex = chapterEntry[ChapterTable.sourceOrder]
val mangaId = chapterEntry[ChapterTable.manga].value
transaction {
pageList.forEach { page ->
val pageEntry = transaction {
PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }
.firstOrNull()
}
if (pageEntry == null) {
PageTable.insert {
it[index] = page.index
it[url] = page.url
it[imageUrl] = page.imageUrl
it[chapter] = chapterId
}
} else {
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }) {
it[url] = page.url
it[imageUrl] = page.imageUrl
}
}
}
}
updatePageCount(pageList, mangaId, chapterIndex)
// chapter was updated
chapterEntry = freshChapterEntry()
}
private fun updatePageCount(
pageList: List<Page>,
mangaId: Int,
chapterIndex: Int
) {
val pageCount = pageList.count()
transaction {
ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder eq chapterIndex) }) {
it[ChapterTable.pageCount] = pageCount
}
}
}
private fun isNotCompletelyDownloaded(): Boolean {
return !(chapterEntry[ChapterTable.isDownloaded] && firstPageExists())
}
private fun firstPageExists(): Boolean {
val chapterId = chapterEntry[ChapterTable.id].value
val chapterDir = getChapterDir(mangaId, chapterId)
println(chapterDir)
println(getPageName(0))
return ImageResponse.findFileNameStartingWith(
chapterDir,
getPageName(0)
) != null
}
}
@@ -12,8 +12,8 @@ import mu.KotlinLogging
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.manga.impl.Chapter.getChapter
import suwayomi.tachidesk.manga.impl.Page.getPageImage import suwayomi.tachidesk.manga.impl.Page.getPageImage
import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReady
import suwayomi.tachidesk.manga.impl.download.model.DownloadChapter import suwayomi.tachidesk.manga.impl.download.model.DownloadChapter
import suwayomi.tachidesk.manga.impl.download.model.DownloadState.Downloading import suwayomi.tachidesk.manga.impl.download.model.DownloadState.Downloading
import suwayomi.tachidesk.manga.impl.download.model.DownloadState.Error import suwayomi.tachidesk.manga.impl.download.model.DownloadState.Error
@@ -47,7 +47,7 @@ class Downloader(private val downloadQueue: CopyOnWriteArrayList<DownloadChapter
download.state = Downloading download.state = Downloading
step() step()
download.chapter = runBlocking { getChapter(download.chapterIndex, download.mangaId) } download.chapter = runBlocking { getChapterDownloadReady(download.chapterIndex, download.mangaId) }
step() step()
val pageCount = download.chapter.pageCount val pageCount = download.chapter.pageCount
@@ -2,7 +2,7 @@ package suwayomi.tachidesk.manga.impl.download.model
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -2,7 +2,7 @@ package suwayomi.tachidesk.manga.impl.download.model
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -2,7 +2,7 @@ package suwayomi.tachidesk.manga.impl.extension
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -2,7 +2,7 @@ package suwayomi.tachidesk.manga.impl.extension
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -7,11 +7,10 @@ package suwayomi.tachidesk.manga.impl.extension.github
* 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 com.github.salomonbrys.kotson.int
import com.github.salomonbrys.kotson.string
import com.google.gson.JsonArray
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.Serializable
import okhttp3.Request import okhttp3.Request
import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MAX import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MAX
import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MIN import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MIN
@@ -19,34 +18,39 @@ import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
object ExtensionGithubApi { object ExtensionGithubApi {
private const val BASE_URL = "https://raw.githubusercontent.com" private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/"
private const val REPO_URL_PREFIX = "$BASE_URL/tachiyomiorg/tachiyomi-extensions/repo"
private fun parseResponse(json: JsonArray): List<OnlineExtension> { @Serializable
return json private data class ExtensionJsonObject(
.map { it.asJsonObject } val name: String,
.filter { element -> val pkg: String,
val versionName = element["version"].string val apk: String,
val libVersion = versionName.substringBeforeLast('.').toDouble() val lang: String,
libVersion in LIB_VERSION_MIN..LIB_VERSION_MAX val code: Int,
} val version: String,
.map { element -> val nsfw: Int,
val name = element["name"].string.substringAfter("Tachiyomi: ") val hasReadme: Int = 0,
val pkgName = element["pkg"].string val hasChangelog: Int = 0,
val apkName = element["apk"].string val sources: List<ExtensionSourceJsonObject>?,
val versionName = element["version"].string )
val versionCode = element["code"].int
val lang = element["lang"].string
val nsfw = element["nsfw"].int == 1
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
OnlineExtension(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon) @Serializable
} private data class ExtensionSourceJsonObject(
} val name: String,
val lang: String,
val id: Long,
val baseUrl: String
)
suspend fun findExtensions(): List<OnlineExtension> { suspend fun findExtensions(): List<OnlineExtension> {
val response = getRepo() val request = Request.Builder()
return parseResponse(response) .url("$REPO_URL_PREFIX/index.min.json")
.build()
return client.newCall(request)
.await()
.parseAs<List<ExtensionJsonObject>>()
.toExtensions()
} }
fun getApkUrl(extension: ExtensionDataClass): String { fun getApkUrl(extension: ExtensionDataClass): String {
@@ -65,12 +69,37 @@ object ExtensionGithubApi {
.build() .build()
} }
private fun getRepo(): JsonArray { private fun List<ExtensionJsonObject>.toExtensions(): List<OnlineExtension> {
val request = Request.Builder() return this
.url("$REPO_URL_PREFIX/index.min.json") .filter {
.build() val libVersion = it.version.substringBeforeLast('.').toDouble()
libVersion in LIB_VERSION_MIN..LIB_VERSION_MAX
}
.map {
OnlineExtension(
name = it.name.substringAfter("Tachiyomi: "),
pkgName = it.pkg,
versionName = it.version,
versionCode = it.code,
lang = it.lang,
isNsfw = it.nsfw == 1,
hasReadme = it.hasReadme == 1,
hasChangelog = it.hasChangelog == 1,
sources = it.sources?.toExtensionSources() ?: emptyList(),
apkName = it.apk,
iconUrl = "${REPO_URL_PREFIX}icon/${it.apk.replace(".apk", ".png")}"
)
}
}
val response = client.newCall(request).execute().use { response -> response.body!!.string() } private fun List<ExtensionSourceJsonObject>.toExtensionSources(): List<OnlineExtensionSource> {
return JsonParser.parseString(response).asJsonArray return this.map {
OnlineExtensionSource(
name = it.name,
lang = it.lang,
id = it.id,
baseUrl = it.baseUrl
)
}
} }
} }
@@ -7,13 +7,23 @@ package suwayomi.tachidesk.manga.impl.extension.github
* 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/. */
data class OnlineExtensionSource(
val name: String,
val lang: String,
val id: Long,
val baseUrl: String
)
data class OnlineExtension( data class OnlineExtension(
val name: String, val name: String,
val pkgName: String, val pkgName: String,
val versionName: String,
val versionCode: Int,
val lang: String,
val isNsfw: Boolean,
val apkName: String, val apkName: String,
val lang: String,
val versionCode: Int,
val versionName: String,
val isNsfw: Boolean,
val hasReadme: Boolean,
val hasChangelog: Boolean,
val sources: List<OnlineExtensionSource>,
val iconUrl: String val iconUrl: String
) )
@@ -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
@@ -4,7 +4,7 @@ import java.io.File
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -11,7 +11,7 @@ import java.io.IOException
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -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>) {
@@ -2,7 +2,7 @@ package suwayomi.tachidesk.manga.impl.util.storage
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -2,7 +2,7 @@ package suwayomi.tachidesk.manga.model.dataclass
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -2,7 +2,7 @@ package suwayomi.tachidesk.manga.model.dataclass
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -2,7 +2,7 @@ package suwayomi.tachidesk.manga.model.dataclass
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -2,7 +2,7 @@ package suwayomi.tachidesk.manga.model.dataclass
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -2,7 +2,7 @@ package suwayomi.tachidesk.manga.model.dataclass
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -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(
@@ -2,7 +2,7 @@ package suwayomi.tachidesk.manga.model.dataclass
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -4,26 +4,26 @@ import eu.kanade.tachiyomi.source.ConfigurableSource
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
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,
) )
@@ -2,7 +2,7 @@ package suwayomi.tachidesk.manga.model.table
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -2,7 +2,7 @@ package suwayomi.tachidesk.manga.model.table
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -2,7 +2,7 @@ package suwayomi.tachidesk.manga.model.table
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -2,7 +2,7 @@ package suwayomi.tachidesk.manga.model.table
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -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
@@ -2,7 +2,7 @@ package suwayomi.tachidesk.manga.model.table
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -2,7 +2,7 @@ package suwayomi.tachidesk.manga.model.table
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -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())
} }
@@ -2,7 +2,7 @@ package suwayomi.tachidesk.server.database
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -1,6 +1,7 @@
package suwayomi.tachidesk.server.util package suwayomi.tachidesk.server.util
import io.javalin.http.Context import io.javalin.http.Context
import io.javalin.http.HttpCode
import io.javalin.plugin.openapi.dsl.DocumentedHandler import io.javalin.plugin.openapi.dsl.DocumentedHandler
import io.javalin.plugin.openapi.dsl.OpenApiDocumentation import io.javalin.plugin.openapi.dsl.OpenApiDocumentation
import io.javalin.plugin.openapi.dsl.documented import io.javalin.plugin.openapi.dsl.documented
@@ -112,21 +113,32 @@ sealed class Param<T> {
} }
class ResultsBuilder { class ResultsBuilder {
val results = mutableListOf<ResultType<*>>() val results = mutableListOf<ResultType>()
inline fun <reified T> json(status: String) { inline fun <reified T> json(code: HttpCode) {
results += ResultType.MimeType(status, "application/json", T::class.java) results += ResultType.MimeType(code, "application/json", T::class.java)
} }
inline fun <reified T> plainText(status: String) { fun plainText(code: HttpCode) {
results += ResultType.MimeType(status, "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) {
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 status: String, 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(status, clazz) documentation.result(code.status.toString(), clazz)
}
}
data class StatusCode(val code: HttpCode) : ResultType() {
override fun applyTo(documentation: OpenApiDocumentation) {
documentation.result<Unit>(code.status.toString())
} }
} }
} }
@@ -2,7 +2,7 @@ package suwayomi.tachidesk.server.util
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. */
@@ -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 = ""

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