Compare commits

..

178 Commits

Author SHA1 Message Date
Aria Moradi
464b9659fe queries 2023-06-10 04:00:49 +03:30
Aria Moradi
7a59d0d4dd it compiles 2023-06-09 22:56:14 +03:30
Mitchell Syer
7c3eff2ba7 Complete source mutations (#567) 2023-06-05 16:49:03 +03:30
Mitchell Syer
300c0a8f35 Category Mutations (#566)
* Complete Category mutations

* Remove TODO
2023-06-05 16:48:57 +03:30
schroda
51bfdc0947 Feature/make config settings changeable during runtime (#545)
* Add logic to update config during runtime

* Update ConfigModule to always use the latest config

* Make ServerConfig settings re-assignable
2023-06-05 16:48:18 +03:30
Aria Moradi
a64566c0f3 fill in the cover according to spec (#571) 2023-06-05 16:18:03 +03:30
Aria Moradi
dbb9a80ea6 use commons-compress everywhere (#570) 2023-06-05 16:16:27 +03:30
Aria Moradi
e930c54246 improve zip parsing (#569) 2023-06-05 14:31:14 +03:30
Mitchell Syer
dfff047cbf Fix cascade migration (#565) 2023-05-29 17:29:54 -04:00
Mitchell Syer
44fb2b02bc Fix global meta delete (#564) 2023-05-29 23:41:39 +03:30
Mitchell Syer
6a7efafd9f Improve database column references and default category handling (#563)
* Improve default category handling and add cascade to references where possible

* Minor fix for default category

* Make the default category always first in the normalization
2023-05-28 04:11:27 +03:30
Mitchell Syer
241abc3956 Add items that are related to the deleted meta (#562) 2023-05-27 21:39:47 +03:30
Mitchell Syer
1e82c879bf Add default category to the database (#561)
* Add default category to the database

* Fix getCategorySize
2023-05-27 03:20:21 +03:30
Mitchell Syer
a81d01d2e3 Don't use data fetchers in mutations (#559) 2023-05-27 03:09:31 +03:30
Mitchell Syer
2230796504 Extension mutations (#560) 2023-05-27 03:09:17 +03:30
Mitchell Syer
458ca7c7cf Fix update chapters (#557) 2023-05-26 13:45:25 +03:30
Mitchell Syer
3f91663ecf Rewrite meta and add meta mutations (#556) 2023-05-26 13:45:16 +03:30
Mitchell Syer
04a671382a Improve GQL Playground (#558) 2023-05-26 13:44:18 +03:30
Mitchell Syer
945ec818e5 Remove category filter (#551) 2023-05-24 14:01:21 +03:30
Mitchell Syer
ff7ac8a785 Fetch Manga and Chapters in GQL (#555) 2023-05-24 14:01:07 +03:30
Mitchell Syer
603105e2ea Fix StringFilter (#554) 2023-05-24 14:00:54 +03:30
Mitchell Syer
5475567b48 Cleanup download type (#553) 2023-05-24 14:00:29 +03:30
Mitchell Syer
2aec0adb08 Category mangas (#552) 2023-05-24 14:00:06 +03:30
Mitchell Syer
54fc3761bf Put graphql under api (#549) 2023-05-17 03:58:00 +03:30
Mitchell Syer
99e1912bfe Fix manga/source and manga/chapters for graphql (#548) 2023-05-17 03:57:20 +03:30
Aria Moradi
ecc1cabafd Merge pull request #547 from Suwayomi/graphql
add graphql
2023-05-13 23:00:13 +03:30
Aria Moradi
1a5b847b23 Update README.md 2023-05-01 19:16:32 +03:30
Aria Moradi
d3409e7133 Update README.md 2023-05-01 19:09:59 +03:30
Aria Moradi
4e553e3eb3 better description about the Tachiyomi extension 2023-05-01 18:44:28 +03:30
Syer10
4577bbc572 More mutations 2023-04-28 21:56:25 -04:00
Syer10
da8ca23496 Start working on mutations 2023-04-28 21:29:06 -04:00
Syer10
988853be63 Seems like this should return null if it errors 2023-04-28 21:28:18 -04:00
schroda
cde5dc5bfa Update "dex2jar" to v60 (#538) 2023-04-10 11:44:22 +03:30
Syer10
b617250eff Delete updates query since the chapters query can now mimic it 2023-04-08 22:55:56 -04:00
Syer10
313da99536 Add in library filter for chapters 2023-04-08 22:54:56 -04:00
Syer10
442e245216 Update TODO 2023-04-08 22:37:51 -04:00
Syer10
050ab17019 Complete SourceQuery 2023-04-08 21:05:05 -04:00
Syer10
c80f488a13 Complete ChapterQuery 2023-04-08 20:40:18 -04:00
Syer10
cf73804c71 Complete ExtensionQuery 2023-04-08 20:39:38 -04:00
Syer10
a90e5d13ea Complete MetaQuery 2023-04-08 15:47:10 -04:00
Syer10
891fb0b479 Simplify keyset pagination 2023-04-08 15:27:18 -04:00
Syer10
58a623d44d Fix keyset pagination for non-unique order by modes 2023-04-08 13:37:09 -04:00
Syer10
0e84b8a154 Lint 2023-04-08 13:36:28 -04:00
Syer10
a4dfcf80e4 Implement manga status filter 2023-04-07 21:30:20 -04:00
Syer10
d8567eadb2 Simplify queries 2023-04-07 21:10:38 -04:00
Syer10
0b88207ad5 Fix empty results errors 2023-04-07 00:02:00 -04:00
Syer10
671466a737 Complete CategoryQuery 2023-04-06 21:53:30 -04:00
Syer10
84881a0d52 Complete MangaQuery 2023-04-04 21:02:29 -04:00
Syer10
a589049cc7 Move things around and introduce Cursor type 2023-04-04 21:02:07 -04:00
Syer10
17877e0f17 Fix case insensitive 2023-04-03 22:12:38 -04:00
Syer10
1ed9bef2a1 Fix the playground explorer and add a updated default query 2023-04-03 22:07:10 -04:00
Syer10
a6dddf311c Basically finish MangaQuery, only paging left 2023-04-03 22:04:46 -04:00
Syer10
e8c2bad187 Handle missing objects in graphql 2023-04-02 20:39:56 -04:00
Syer10
52bda2c080 Start working on graphql paging 2023-04-02 20:15:09 -04:00
Syer10
607919f40f Implement more query parameters 2023-04-02 17:33:19 -04:00
Syer10
d830638ee6 Use actual MangaStatus enum 2023-04-02 16:47:00 -04:00
Syer10
106bda2097 Proper conversion Scalar for Long to String and back 2023-04-02 16:30:03 -04:00
Syer10
7debb27374 Might not need a updates query 2023-03-31 23:11:18 -04:00
Syer10
05b5a7f598 Add updates 2023-03-31 23:03:26 -04:00
Syer10
3bbda7ba54 More todos 2023-03-31 22:36:01 -04:00
Syer10
9312f5fd14 Add global meta 2023-03-31 22:30:14 -04:00
Syer10
399eb07e35 Fix imports 2023-03-31 22:21:09 -04:00
Syer10
eb197ebcee Switch database logger to SLF4J 2023-03-31 22:19:13 -04:00
Syer10
4c30d8ab05 Some TODOs with ideas 2023-03-31 22:00:01 -04:00
Syer10
3a67ddf0f6 Add Extensions to Graphql 2023-03-31 20:58:18 -04:00
Syer10
6541c7b5b7 Serialize Long as String in graphql 2023-03-31 20:30:24 -04:00
Syer10
37f41ade43 Directly use the database for sources in graphql 2023-03-31 20:29:55 -04:00
Syer10
007d20d417 Add Sources to Graphql 2023-03-31 19:44:21 -04:00
Syer10
00370a81fa Minor cleanup 2023-03-31 19:10:04 -04:00
Syer10
d4599c3331 Use Graphiql with the Explorer plugin for the query builder 2023-03-31 19:09:32 -04:00
Syer10
bce76bbcf3 Use Kotlin Coroutines Flow instead of Project reactor 2023-03-30 18:28:56 -04:00
Valter Martinek
847a5fe71b Subscriptions! 2023-03-30 17:05:41 -04:00
Valter Martinek
e2fa003239 Rewrite graphql controller execute as function without docs 2023-03-30 17:04:01 -04:00
Valter Martinek
0c555e88d3 Update graphql-playground endpoint 2023-03-30 17:04:01 -04:00
Valter Martinek
bf7f1a04b3 Add categories to graphql 2023-03-30 17:04:01 -04:00
Valter Martinek
623172af6d Add mutation for updating chapters 2023-03-30 17:04:01 -04:00
Valter Martinek
4fb689d9e4 Add chapter and manga meta field 2023-03-30 17:04:01 -04:00
Valter Martinek
6054c489c6 Add graphql playground 2023-03-30 17:04:01 -04:00
Valter Martinek
21719f4408 Add basic graphql implementation with manga and chapters loading with data loaders 2023-03-30 17:03:56 -04:00
Aria Moradi
f2a650ba02 fix typo 2023-03-27 21:27:10 +03:30
Aria Moradi
871c28b1ea cleanup notes 2023-03-27 21:26:37 +03:30
schroda
d3aa32147a Add logic to only update specific categories (#520)
Makes it possible to only update specific categories.

In case a manga is in an excluded category it will be excluded even if it is also in an included category.
2023-03-27 20:15:46 +03:30
schroda
9a50f2e408 Notify clients even if no manga gets updated (#531)
In case no manga gets updated and no update job was running before, the client would never receive an info about its update request
2023-03-27 17:11:19 +03:30
schroda
dcde4947e8 Emit update to clients after adding all mangas to the queue (#521)
Emitting updates before all the mangas were added to the queue could lead to e.g. wrong progress calculation.
2023-03-25 22:08:42 +03:30
schroda
5b61bdc3a8 add size field to Category data class (#519)
Makes it possible to display the size of a category to the user
2023-03-25 22:07:50 +03:30
schroda
ec1d65f4c3 update library grouped by source (#511)
* Update mangas grouped by source

* Limit parallel update requests
2023-03-10 11:03:09 +03:30
akabhirav
a0081dec07 fix manga unread and download count (#509) 2023-02-23 22:04:03 +03:30
akabhirav
783787e514 Send last read chapter in Mangas in Category API (#507)
* Send last read chapter with manga

* optimize query

* introduce new field for better performance
2023-02-21 02:47:45 +03:30
akabhirav
ac99dd55a2 Fix random page sent when manga is downloaded (#508) 2023-02-21 02:40:38 +03:30
Mitchell Syer
c56f984952 Fix SharedPreferences.Editor.clear and SharedPreferences.Editor.remove (#505)
* Fix SharedPreferences.Editor.clear and SharedPreferences.Editor.remove

* UNCHECKED_CAST

* Support removing Set<String>

* Typo

* Remove unneeded OptIn
2023-02-19 07:54:24 +03:30
Aria Moradi
9269ca726e It's not us, I swear ;;; 2023-02-16 10:57:26 +03:30
DattatreyaReddy Panta
eca3205dcf Update winget.yml (#500) 2023-02-14 15:02:41 +03:30
akabhirav
13f5486d0b Fix CBZ download bug for newly added mangas in Library (#499) 2023-02-13 19:17:14 +03:30
Aria Moradi
d4e71274f9 update changelog 2023-02-12 23:33:06 +03:30
Aria Moradi
4cc96de806 v0.7.0 2023-02-12 23:08:05 +03:30
Aria Moradi
d27ef12039 stop using depricated API 2023-02-12 23:03:45 +03:30
Aria Moradi
f3c2ee4c40 re-order config options 2023-02-12 22:50:06 +03:30
akabhirav
555f73b478 Download as CBZ (#490)
* Download as CBZ

* Better error handling for zips (code review changes)
2023-02-12 22:45:58 +03:30
akabhirav
544bf2ea21 fix Page index issues for some providers (#491) 2023-02-12 18:34:30 +03:30
Aria Moradi
54bbb5e384 rethink image cache (#498) 2023-02-12 18:33:36 +03:30
akabhirav
b10062c73d Decouple Cache and Download behaviour (#493)
* Separate cache dir from download dir

* Move downloader logic outside of caching/image download logic

* remove unnecessary method duplication

* moved download logic inside download provider

* optimize and handle partial downloads

* made code review changes
2023-02-12 18:26:26 +03:30
Aria Moradi
a027d6df1b disable playwright for v0.6.7 2023-02-12 14:35:11 +03:30
Mitchell Syer
926a53a4b0 add support for Extensions Lib 1.4 (#496)
* Support extensions lib 1.4

* Fix build

* Support UpdateStrategy

* Update extension lib min/max to match Tachiyomi

* Use HttpSource.getMangaUrl and add Chapter.realUrl
2023-02-12 05:49:32 +03:30
Mitchell Syer
406cb46170 Fix logging and update system try (#488)
- Dorkbox SystemTray now automatically adds its shutdown hook, and removed the manual function
2023-02-05 22:21:35 +03:30
akabhirav
acc58dc892 Fixe Dex2Jar and dorkbox dependency issues (#487)
Co-authored-by: akxer <>
2023-02-05 18:43:00 +03:30
Aria Moradi
55894c22a4 upgrade dorkbox stuff 2023-01-15 12:46:50 +03:30
Aria Moradi
476b10b862 update gradle version 2023-01-13 13:05:50 +03:30
Aria Moradi
3cbbe446ab fix ambiguous reference issue on JDK 13+ 2023-01-11 15:46:02 +03:30
Mitchell Syer
4cf7512ee0 Improve Playwright handling (#479)
* Improve playwright

* Move DriverJar.java and Chromium.kt
2023-01-08 01:17:53 +03:30
Mitchell Syer
ee8ec460a1 Improve Gradle Configuration (#478)
* Improve gradle configuration

* Formatting fix

Co-authored-by: Aria Moradi <aria.moradi007@gmail.com>

* Improve asm version lock description

Co-authored-by: Aria Moradi <aria.moradi007@gmail.com>

* Improve image decoder description

Co-authored-by: Aria Moradi <aria.moradi007@gmail.com>

Co-authored-by: Aria Moradi <aria.moradi007@gmail.com>
2023-01-07 20:07:53 +03:30
Aria Moradi
deecab3cca fix typo 2023-01-03 13:39:15 +03:30
Aria Moradi
d2f5c1a195 link to Tachiyomi section 2023-01-03 13:38:20 +03:30
Aria Moradi
dba77e26a3 Clarify and Update 2023-01-03 13:30:58 +03:30
Aria Moradi
fa48bafbc6 Clarify and Update 2023-01-03 13:28:54 +03:30
Aria Moradi
73c48694c7 remove possibly misleading sentence 2023-01-03 13:24:42 +03:30
Aria Moradi
0ff89d039b fix CategoryMetaTable reference to CategoryTable (#473) 2023-01-03 13:19:44 +03:30
Aria Moradi
7a7081ee13 Update CategoryMetaTable.kt 2023-01-02 18:23:01 +03:30
Aria Moradi
874aaf4e93 fix when playwright fails on providing a UA 2022-12-28 17:14:12 +03:30
Mitchell Syer
ebf076d9f6 Use extension list fallback if extensions fail to fetch (#469) 2022-12-25 11:15:37 +03:30
Mitchell Syer
073a041d4c Add better manga thumbnail handling (#465) 2022-12-23 00:43:36 +03:30
Mahor
96a9b4dabd Fix debian release (#463)
* Update debhelper-compat to 13

* Re-enable debian release

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

* Set system driver implementation

* Minor cleanup

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

* No need to get the FS if it already exists

* use java implementation

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

* make sure to close browser

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

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

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

* code clean up

* Forgot to commit these

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

* rewrite and refactor

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

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

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

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

* Reduce number of immediate updates to clients

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

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

* Rename the endpoint to match single manga batch endpoint

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

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

* Minor fix

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

* Reorder downloads

* Download in parallel by source

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

* Fix review notes

* Add chapter id to API endpoints

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

* Change EnqueueInput format to be more futureproof

* Change endpoint path

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

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

* Update doc

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

* Update lastFetchedAt columns when data are fetched from source

* Add age and chaptersAge fields to MangaDataClass

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

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

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

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

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

* move quick js to AndroidCompat

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

View File

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

View File

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

2
.gitignore vendored
View File

@@ -2,7 +2,7 @@
.gradle
.idea
gradle.properties
.fleet
# But we need these
!.idea/runConfigurations

View File

@@ -0,0 +1,12 @@
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id(libs.plugins.kotlin.jvm.get().pluginId)
id(libs.plugins.kotlin.serialization.get().pluginId)
id(libs.plugins.kotlinter.get().pluginId)
}
dependencies {
// Shared
implementation(libs.bundles.shared)
testImplementation(libs.bundles.sharedTest)
}

View File

@@ -11,6 +11,9 @@ import ch.qos.logback.classic.Level
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigRenderOptions
import com.typesafe.config.ConfigValue
import com.typesafe.config.ConfigValueFactory
import com.typesafe.config.parser.ConfigDocumentFactory
import mu.KotlinLogging
import java.io.File
@@ -18,15 +21,17 @@ import java.io.File
* Manages app config.
*/
open class ConfigManager {
val logger = KotlinLogging.logger {}
private val generatedModules = mutableMapOf<Class<out ConfigModule>, ConfigModule>()
val config by lazy { loadConfigs() }
private val userConfigFile = File(ApplicationRootDir, "server.conf")
private var internalConfig = loadConfigs()
val config: Config
get() = internalConfig
// Public read-only view of modules
val loadedModules: Map<Class<out ConfigModule>, ConfigModule>
get() = generatedModules
val logger = KotlinLogging.logger {}
/**
* Get a config module
*/
@@ -54,7 +59,7 @@ open class ConfigManager {
// Load user config
val userConfig =
File(ApplicationRootDir, "server.conf").let {
userConfigFile.let {
ConfigFactory.parseFile(it)
}
@@ -86,6 +91,20 @@ open class ConfigManager {
registerModule(it)
}
}
private fun updateUserConfigFile(path: String, value: ConfigValue) {
val userConfigDoc = ConfigDocumentFactory.parseFile(userConfigFile)
val updatedConfigDoc = userConfigDoc.withValue(path, value)
val newFileContent = updatedConfigDoc.render()
userConfigFile.writeText(newFileContent)
}
fun updateValue(path: String, value: Any) {
val configValue = ConfigValueFactory.fromAnyRef(value)
updateUserConfigFile(path, configValue)
internalConfig = internalConfig.withValue(path, configValue)
}
}
object GlobalConfigManager : ConfigManager()

View File

@@ -15,19 +15,23 @@ import kotlin.reflect.KProperty
* Abstract config module.
*/
@Suppress("UNUSED_PARAMETER")
abstract class ConfigModule(config: Config)
abstract class ConfigModule(getConfig: () -> Config)
/**
* Abstract jvm-commandline-argument-overridable config module.
*/
abstract class SystemPropertyOverridableConfigModule(config: Config, moduleName: String) : ConfigModule(config) {
val overridableConfig = SystemPropertyOverrideDelegate(config, moduleName)
abstract class SystemPropertyOverridableConfigModule(getConfig: () -> Config, moduleName: String) : ConfigModule(getConfig) {
val overridableConfig = SystemPropertyOverrideDelegate(getConfig, moduleName)
}
/** Defines a config property that is overridable with jvm `-D` commandline arguments prefixed with [CONFIG_PREFIX] */
class SystemPropertyOverrideDelegate(val config: Config, val moduleName: String) {
class SystemPropertyOverrideDelegate(val getConfig: () -> Config, val moduleName: String) {
operator fun <R> setValue(thisRef: R, property: KProperty<*>, value: Any) {
GlobalConfigManager.updateValue("$moduleName.${property.name}", value)
}
inline operator fun <R, reified T> getValue(thisRef: R, property: KProperty<*>): T {
val configValue: T = config.getValue(thisRef, property)
val configValue: T = getConfig().getValue(thisRef, property)
val combined = System.getProperty(
"$CONFIG_PREFIX.$moduleName.${property.name}",

View File

@@ -1,28 +1,39 @@
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id(libs.plugins.kotlin.jvm.get().pluginId)
id(libs.plugins.kotlin.serialization.get().pluginId)
id(libs.plugins.kotlinter.get().pluginId)
}
dependencies {
// Shared
implementation(libs.bundles.shared)
testImplementation(libs.bundles.sharedTest)
// Android stub library
implementation("com.github.Suwayomi:android-jar:1.0.0")
implementation(libs.android.stubs)
// XML
compileOnly("xmlpull:xmlpull:1.1.3.4a")
compileOnly(libs.xmlpull)
// Config API
implementation(project(":AndroidCompat:Config"))
implementation(projects.androidCompat.config)
// APK sig verifier
compileOnly("com.android.tools.build:apksig:7.1.0-beta05")
compileOnly(libs.apksig)
// AndroidX annotations
compileOnly("androidx.annotation:annotation:1.3.0")
compileOnly(libs.android.annotations)
// substitute for duktape-android
implementation("org.mozilla:rhino-runtime:1.7.14") // slimmer version of 'org.mozilla:rhino'
implementation("org.mozilla:rhino-engine:1.7.14") // provides the same interface as 'javax.script' a.k.a Nashorn
implementation(libs.bundles.rhino)
// Kotlin wrapper around Java Preferences, makes certain things easier
val multiplatformSettingsVersion = "0.8.1"
implementation("com.russhwolf:multiplatform-settings-jvm:$multiplatformSettingsVersion")
implementation("com.russhwolf:multiplatform-settings-serialization-jvm:$multiplatformSettingsVersion")
implementation(libs.bundles.settings)
// Android version of SimpleDateFormat
implementation("com.ibm.icu:icu4j:70.1")
implementation(libs.icu4j)
// OpenJDK lacks native JPEG encoder and native WEBP decoder
implementation(libs.bundles.twelvemonkeys)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,12 +8,12 @@ import xyz.nulldev.ts.config.ConfigModule
* Application info config.
*/
class ApplicationInfoConfigModule(config: Config) : ConfigModule(config) {
val packageName: String by config
val debug: Boolean by config
class ApplicationInfoConfigModule(getConfig: () -> Config) : ConfigModule(getConfig) {
val packageName: String by getConfig()
val debug: Boolean by getConfig()
companion object {
fun register(config: Config) =
ApplicationInfoConfigModule(config.getConfig("android.app"))
ApplicationInfoConfigModule { config.getConfig("android.app") }
}
}

View File

@@ -8,27 +8,27 @@ import xyz.nulldev.ts.config.ConfigModule
* Files configuration modules. Specifies where to store the Android files.
*/
class FilesConfigModule(config: Config) : ConfigModule(config) {
val dataDir: String by config
val filesDir: String by config
val noBackupFilesDir: String by config
val externalFilesDirs: MutableList<String> by config
val obbDirs: MutableList<String> by config
val cacheDir: String by config
val codeCacheDir: String by config
val externalCacheDirs: MutableList<String> by config
val externalMediaDirs: MutableList<String> by config
val rootDir: String by config
val externalStorageDir: String by config
val downloadCacheDir: String by config
val databasesDir: String by config
class FilesConfigModule(getConfig: () -> Config) : ConfigModule(getConfig) {
val dataDir: String by getConfig()
val filesDir: String by getConfig()
val noBackupFilesDir: String by getConfig()
val externalFilesDirs: MutableList<String> by getConfig()
val obbDirs: MutableList<String> by getConfig()
val cacheDir: String by getConfig()
val codeCacheDir: String by getConfig()
val externalCacheDirs: MutableList<String> by getConfig()
val externalMediaDirs: MutableList<String> by getConfig()
val rootDir: String by getConfig()
val externalStorageDir: String by getConfig()
val downloadCacheDir: String by getConfig()
val databasesDir: String by getConfig()
val prefsDir: String by config
val prefsDir: String by getConfig()
val packageDir: String by config
val packageDir: String by getConfig()
companion object {
fun register(config: Config) =
FilesConfigModule(config.getConfig("android.files"))
FilesConfigModule { config.getConfig("android.files") }
}
}

View File

@@ -4,19 +4,19 @@ import com.typesafe.config.Config
import io.github.config4k.getValue
import xyz.nulldev.ts.config.ConfigModule
class SystemConfigModule(val config: Config) : ConfigModule(config) {
val isDebuggable: Boolean by config
class SystemConfigModule(val getConfig: () -> Config) : ConfigModule(getConfig) {
val isDebuggable: Boolean by getConfig()
val propertyPrefix = "properties."
fun getStringProperty(property: String) = config.getString("$propertyPrefix$property")!!
fun getIntProperty(property: String) = config.getInt("$propertyPrefix$property")
fun getLongProperty(property: String) = config.getLong("$propertyPrefix$property")
fun getBooleanProperty(property: String) = config.getBoolean("$propertyPrefix$property")
fun hasProperty(property: String) = config.hasPath("$propertyPrefix$property")
fun getStringProperty(property: String) = getConfig().getString("$propertyPrefix$property")!!
fun getIntProperty(property: String) = getConfig().getInt("$propertyPrefix$property")
fun getLongProperty(property: String) = getConfig().getLong("$propertyPrefix$property")
fun getBooleanProperty(property: String) = getConfig().getBoolean("$propertyPrefix$property")
fun hasProperty(property: String) = getConfig().hasPath("$propertyPrefix$property")
companion object {
fun register(config: Config) =
SystemConfigModule(config.getConfig("android.system"))
SystemConfigModule { config.getConfig("android.system") }
}
}

View File

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

View File

@@ -9,8 +9,7 @@ package xyz.nulldev.androidcompat.io.sharedprefs
import android.content.SharedPreferences
import com.russhwolf.settings.ExperimentalSettingsApi
import com.russhwolf.settings.ExperimentalSettingsImplementation
import com.russhwolf.settings.JvmPreferencesSettings
import com.russhwolf.settings.PreferencesSettings
import com.russhwolf.settings.serialization.decodeValue
import com.russhwolf.settings.serialization.decodeValueOrNull
import com.russhwolf.settings.serialization.encodeValue
@@ -21,10 +20,10 @@ import kotlinx.serialization.builtins.serializer
import java.util.prefs.PreferenceChangeListener
import java.util.prefs.Preferences
@OptIn(ExperimentalSettingsImplementation::class, ExperimentalSerializationApi::class, ExperimentalSettingsApi::class)
@OptIn(ExperimentalSerializationApi::class, ExperimentalSettingsApi::class)
class JavaSharedPreferences(key: String) : SharedPreferences {
private val javaPreferences = Preferences.userRoot().node("suwayomi/tachidesk/$key")
private val preferences = JvmPreferencesSettings(javaPreferences)
private val preferences = PreferencesSettings(javaPreferences)
private val listeners = mutableMapOf<SharedPreferences.OnSharedPreferenceChangeListener, PreferenceChangeListener>()
// TODO: 2021-05-29 Need to find a way to get this working with all pref types
@@ -76,14 +75,20 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
return Editor(preferences)
}
class Editor(private val preferences: JvmPreferencesSettings) : SharedPreferences.Editor {
val itemsToAdd = mutableMapOf<String, Any>()
class Editor(private val preferences: PreferencesSettings) : SharedPreferences.Editor {
private val actions = mutableListOf<Action>()
private sealed class Action {
data class Add(val key: String, val value: Any) : Action()
data class Remove(val key: String) : Action()
object Clear : Action()
}
override fun putString(key: String, value: String?): SharedPreferences.Editor {
if (value != null) {
itemsToAdd[key] = value
actions += Action.Add(key, value)
} else {
remove(key)
actions += Action.Remove(key)
}
return this
}
@@ -93,40 +98,40 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
values: MutableSet<String>?
): SharedPreferences.Editor {
if (values != null) {
itemsToAdd[key] = values
actions += Action.Add(key, values)
} else {
remove(key)
actions += Action.Remove(key)
}
return this
}
override fun putInt(key: String, value: Int): SharedPreferences.Editor {
itemsToAdd[key] = value
actions += Action.Add(key, value)
return this
}
override fun putLong(key: String, value: Long): SharedPreferences.Editor {
itemsToAdd[key] = value
actions += Action.Add(key, value)
return this
}
override fun putFloat(key: String, value: Float): SharedPreferences.Editor {
itemsToAdd[key] = value
actions += Action.Add(key, value)
return this
}
override fun putBoolean(key: String, value: Boolean): SharedPreferences.Editor {
itemsToAdd[key] = value
actions += Action.Add(key, value)
return this
}
override fun remove(key: String): SharedPreferences.Editor {
itemsToAdd.remove(key)
actions += Action.Remove(key)
return this
}
override fun clear(): SharedPreferences.Editor {
itemsToAdd.clear()
actions.add(Action.Clear)
return this
}
@@ -140,16 +145,33 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
}
private fun addToPreferences() {
itemsToAdd.forEach { (key, value) ->
actions.forEach {
@Suppress("UNCHECKED_CAST")
when (value) {
is Set<*> -> preferences.encodeValue(SetSerializer(String.serializer()), key, value as Set<String>)
is String -> preferences.putString(key, value)
is Int -> preferences.putInt(key, value)
is Long -> preferences.putLong(key, value)
is Float -> preferences.putFloat(key, value)
is Double -> preferences.putDouble(key, value)
is Boolean -> preferences.putBoolean(key, value)
when (it) {
is Action.Add -> when (val value = it.value) {
is Set<*> -> preferences.encodeValue(SetSerializer(String.serializer()), it.key, value as Set<String>)
is String -> preferences.putString(it.key, value)
is Int -> preferences.putInt(it.key, value)
is Long -> preferences.putLong(it.key, value)
is Float -> preferences.putFloat(it.key, value)
is Double -> preferences.putDouble(it.key, value)
is Boolean -> preferences.putBoolean(it.key, value)
}
is Action.Remove -> {
preferences.remove(it.key)
/**
* Set<String> are stored like
* key.0 = value1
* key.1 = value2
* key.size = 2
*/
preferences.keys.forEach { key ->
if (key.startsWith(it.key + ".")) {
preferences.remove(key)
}
}
}
Action.Clear -> preferences.clear()
}
}
}

View File

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

View File

@@ -1,3 +1,173 @@
# Server: v0.7.0 + WebUI: r983
## TL;DR
- CBZ downloads support
- Webview implementation based on Microsoft playwright, disabled for this release
- Fixed compatibility with some chinese extensions
- Support for Tachiyomi extensions lib 1.4
- WebUI changes:
- Uhh, idk, find out yourself...
## Tachidesk-Server Changelog
- (r1159) v0.6.6 (by @AriaMoradi)
- (r1160) add Chagelog TL;DR (by @AriaMoradi)
- (r1161) fix Changelog typos (by @AriaMoradi)
- (r1162) WebView based cloudflare interceptor ([#456](https://github.com/Suwayomi/Tachidesk-Server/pull/456) by @AriaMoradi)
- (r1163) update issue mod (by @AriaMoradi)
- (r1164) better description (by @AriaMoradi)
- (r1165) fix regex (by @AriaMoradi)
- (r1166) get default User Agent from WebView ([#457](https://github.com/Suwayomi/Tachidesk-Server/pull/457) by @AriaMoradi)
- (r1167) implementation of android.graphics.BitmapFactory ([#460](https://github.com/Suwayomi/Tachidesk-Server/pull/460) by @animeavi)
- (r1168) Basic android.graphics Rect and Canvas implementation ([#461](https://github.com/Suwayomi/Tachidesk-Server/pull/461) by @animeavi)
- (r1169) Get Playwright working ([#462](https://github.com/Suwayomi/Tachidesk-Server/pull/462) by @Syer10)
- (r1170) disable deb release (by @AriaMoradi)
- (r1171) Fix debian release ([#463](https://github.com/Suwayomi/Tachidesk-Server/pull/463) by @mahor1221)
- (r1172) Add better manga thumbnail handling ([#465](https://github.com/Suwayomi/Tachidesk-Server/pull/465) by @Syer10)
- (r1173) Use extension list fallback if extensions fail to fetch ([#469](https://github.com/Suwayomi/Tachidesk-Server/pull/469) by @Syer10)
- (r1174) fix when playwright fails on providing a UA (by @AriaMoradi)
- (r1175) Update CategoryMetaTable.kt (by @AriaMoradi)
- (r1176) fix CategoryMetaTable reference to CategoryTable ([#473](https://github.com/Suwayomi/Tachidesk-Server/pull/473) by @AriaMoradi)
- (r1177) remove possibly misleading sentence (by @AriaMoradi)
- (r1178) Clarify and Update (by @AriaMoradi)
- (r1179) Clarify and Update (by @AriaMoradi)
- (r1180) link to Tachiyomi section (by @AriaMoradi)
- (r1181) fix typo (by @AriaMoradi)
- (r1182) Improve Gradle Configuration ([#478](https://github.com/Suwayomi/Tachidesk-Server/pull/478) by @Syer10)
- (r1183) Improve Playwright handling ([#479](https://github.com/Suwayomi/Tachidesk-Server/pull/479) by @Syer10)
- (r1184) fix ambiguous reference issue on JDK 13+ (by @AriaMoradi)
- (r1185) update gradle version (by @AriaMoradi)
- (r1186) upgrade dorkbox stuff (by @AriaMoradi)
- (r1187) Fixe Dex2Jar and dorkbox dependency issues ([#487](https://github.com/Suwayomi/Tachidesk-Server/pull/487) by @akabhirav)
- (r1188) Fix logging and update system try ([#488](https://github.com/Suwayomi/Tachidesk-Server/pull/488) by @Syer10)
- (r1189) add support for Extensions Lib 1.4 ([#496](https://github.com/Suwayomi/Tachidesk-Server/pull/496) by @Syer10)
- (r1190) disable playwright for v0.6.7 (by @AriaMoradi)
- (r1191) Decouple Cache and Download behaviour ([#493](https://github.com/Suwayomi/Tachidesk-Server/pull/493) by @akabhirav)
- (r1192) rethink image cache ([#498](https://github.com/Suwayomi/Tachidesk-Server/pull/498) by @AriaMoradi)
- (r1193) fix Page index issues for some providers ([#491](https://github.com/Suwayomi/Tachidesk-Server/pull/491) by @akabhirav)
- (r1194) Download as CBZ ([#490](https://github.com/Suwayomi/Tachidesk-Server/pull/490) by @akabhirav)
- (r1195) re-order config options (by @AriaMoradi)
- (r1196) stop using depricated API (by @AriaMoradi)
## Tachidesk-WebUI Changelog
- (r964) Created a GridLayout enum and updated all locations to use it. ([#208](https://github.com/Suwayomi/Tachidesk-WebUI/pull/208) by @infix)
- (r965) fix library update progress rendering ([#210](https://github.com/Suwayomi/Tachidesk-WebUI/pull/210) by @schroda)
- (r966) Save reader settings per manga in Meta ([#216](https://github.com/Suwayomi/Tachidesk-WebUI/pull/216) by @schroda)
- (r967) make default reader settings changeable ([#217](https://github.com/Suwayomi/Tachidesk-WebUI/pull/217) by @schroda)
- (r968) [#211] Refresh Library after a update ([#212](https://github.com/Suwayomi/Tachidesk-WebUI/pull/212) by @schroda)
- (r969) add logic for metadata migration ([#218](https://github.com/Suwayomi/Tachidesk-WebUI/pull/218) by @schroda)
- (r970) set browser tab title ([#220](https://github.com/Suwayomi/Tachidesk-WebUI/pull/220) by @schroda)
- (r971) Add tooltip containing full manga title to title of manga ([#221](https://github.com/Suwayomi/Tachidesk-WebUI/pull/221) by @schroda)
- (r972) show more detailed upload dates for today and yesterday ([#222](https://github.com/Suwayomi/Tachidesk-WebUI/pull/222) by @schroda)
- (r973) add GitHub action on pushing to run lint ([#224](https://github.com/Suwayomi/Tachidesk-WebUI/pull/224) by @schroda)
- (r974) Ignore filters while searching ([#226](https://github.com/Suwayomi/Tachidesk-WebUI/pull/226) by @schroda)
- (r975) force absolute import path ([#223](https://github.com/Suwayomi/Tachidesk-WebUI/pull/223) by @schroda)
- (r976) add prettier for auto formatting ([#231](https://github.com/Suwayomi/Tachidesk-WebUI/pull/231) by @schroda)
- (r977) Fix import path ([#228](https://github.com/Suwayomi/Tachidesk-WebUI/pull/228) by @schroda)
- (r978) increase prettier line length to 120 ([#233](https://github.com/Suwayomi/Tachidesk-WebUI/pull/233) by @schroda)
- (r979) Add chapter page dropdown ([#230](https://github.com/Suwayomi/Tachidesk-WebUI/pull/230) by @schroda)
- (r980) add chapter dropdown to reader nav bar ([#229](https://github.com/Suwayomi/Tachidesk-WebUI/pull/229) by @schroda)
- (r981) Fix lint error ([#235](https://github.com/Suwayomi/Tachidesk-WebUI/pull/235) by @schroda)
- (r982) Fix reader nav bar scroll to page ([#236](https://github.com/Suwayomi/Tachidesk-WebUI/pull/236) by @schroda)
- (r964) Created a GridLayout enum and updated all locations to use it. ([#208](https://github.com/Suwayomi/Tachidesk-WebUI/pull/208) by @infix)
# Server: v0.6.6 + WebUI: r963
## TL;DR
- Batch actions for chapters
- Improved the downloader
- WebUI changes:
- Support for chapter actions
- a lot of code cleanup
- some bugfixes
## Tachidesk-Server Changelog
- (r1114) fix broken links (by @AriaMoradi)
- (r1115) fix more broken stuff (by @AriaMoradi)
- (r1116) fix more broken stuff (by @AriaMoradi)
- (r1117) fix more broken stuff (by @AriaMoradi)
- (r1118) Update winget.yml ([#393](https://github.com/Suwayomi/Tachidesk-Server/pull/393) by @vedantmgoyal2009)
- (r1119) fix jre path([#396](https://github.com/Suwayomi/Tachidesk-Server/pull/396) by @vedantmgoyal2009)
- (r1120) Fix deb package ([#397](https://github.com/Suwayomi/Tachidesk-Server/pull/397) by @mahor1221)
- (r1121) bump version (by @AriaMoradi)
- (r1122) Update Changelog (by @AriaMoradi)
- (r1123) Add libc++-dev ([#405](https://github.com/Suwayomi/Tachidesk-Server/pull/405) by @mahor1221)
- (r1124) Revert back to correct way of handling jre_dir ([#408](https://github.com/Suwayomi/Tachidesk-Server/pull/408) by @mahor1221)
- (r1125) Update winget.yml ([#410](https://github.com/Suwayomi/Tachidesk-Server/pull/410) by @vedantmgoyal2009)
- (r1126) Remove support for Sorayomi web interface ([#414](https://github.com/Suwayomi/Tachidesk-Server/pull/414) by @marcoebbinghaus)
- (r1127) Fix downloader memory leak ([#418](https://github.com/Suwayomi/Tachidesk-Server/pull/418) by @Syer10)
- (r1128) Documentation cleanup ([#417](https://github.com/Suwayomi/Tachidesk-Server/pull/417) by @Syer10)
- (r1129) Updater cleanup and improvements ([#416](https://github.com/Suwayomi/Tachidesk-Server/pull/416) by @Syer10)
- (r1130) replace quickjs with Mozilla Rhino ([#415](https://github.com/Suwayomi/Tachidesk-Server/pull/415) by @xhzhe)
- (r1131) ktlint (by @AriaMoradi)
- (r1132) move Tachiyomi's BuildConfig to kotlin dir (by @AriaMoradi)
- (r1133) remove BuildConfig as extensions now use AppInfo (by @AriaMoradi)
- (r1134) include list of mangas missing source in restore report ([#421](https://github.com/Suwayomi/Tachidesk-Server/pull/421) by @AriaMoradi)
- (r1135) Update dependencies ([#422](https://github.com/Suwayomi/Tachidesk-Server/pull/422) by @Syer10)
- (r1136) Lint ([#423](https://github.com/Suwayomi/Tachidesk-Server/pull/423) by @Syer10)
- (r1137) Fix: Error handling for popular/latest api if pageNum was supplied as zero ([#424](https://github.com/Suwayomi/Tachidesk-Server/pull/424) by @meta-boy)
- (r1138) Add cache control header to manga page response ([#430](https://github.com/Suwayomi/Tachidesk-Server/pull/430) by @martinek)
- (r1139) add MangaTable.lastFetchedAt and ChapterTable.chaptersLastFetchedAt ([#431](https://github.com/Suwayomi/Tachidesk-Server/pull/431) by @martinek)
- (r1140) Pre-load meta entries for all chapters for optimization ([#432](https://github.com/Suwayomi/Tachidesk-Server/pull/432) by @martinek)
- (r1141) POST variant for `/{sourceId}/search` endpoint ([#434](https://github.com/Suwayomi/Tachidesk-Server/pull/434) by @martinek)
- (r1142) Add request body to documentation ([#435](https://github.com/Suwayomi/Tachidesk-Server/pull/435) by @Syer10)
- (r1143) add batch download api ([#436](https://github.com/Suwayomi/Tachidesk-Server/pull/436) by @martinek)
- (r1144) Migrate to H2 v2 (by @AriaMoradi)
- (r1145) add category and global meta ([#438](https://github.com/Suwayomi/Tachidesk-Server/pull/438) by @AriaMoradi)
- (r1146) Revert H2 database to v1 (by @AriaMoradi)
- (r1147) refactor deprecated api (by @AriaMoradi)
- (r1148) Downloader Rewrite ([#437](https://github.com/Suwayomi/Tachidesk-Server/pull/437) by @Syer10)
- (r1149) Set source preference doc fix ([#441](https://github.com/Suwayomi/Tachidesk-Server/pull/441) by @Syer10)
- (r1150) Add batch chapter update endpoint ([#442](https://github.com/Suwayomi/Tachidesk-Server/pull/442) by @martinek)
- (r1151) changes needed for tachiyomi tracker (by @AriaMoradi)
- (r1152) Future proofing (by @AriaMoradi)
- (r1153) Fix settings/check-update endpoint ([#445](https://github.com/Suwayomi/Tachidesk-Server/pull/445) by @martinek)
- (r1154) Fix docs for /server/check-updates ([#447](https://github.com/Suwayomi/Tachidesk-Server/pull/447) by @martinek)
- (r1155) Batch editing and deleting any chapter ([#449](https://github.com/Suwayomi/Tachidesk-Server/pull/449) by @martinek)
- (r1156) make chapters endpoint more unifrom (by @AriaMoradi)
- (r1157) Add batch endpoint for removing downloads from download queue ([#452](https://github.com/Suwayomi/Tachidesk-Server/pull/452) by @martinek)
- (r1158) Download queue missing update fix ([#450](https://github.com/Suwayomi/Tachidesk-Server/pull/450) by @martinek)
## Tachidesk-WebUI Changelog
- (r947) Feature/swr for library screens ([#186](https://github.com/Suwayomi/Tachidesk-WebUI/pull/186) by @martinek)
- (r948) Feature/swr for simple queries ([#187](https://github.com/Suwayomi/Tachidesk-WebUI/pull/187) by @martinek)
- (r949) Check download queue for changes and reload chapters if any chapter download changes state. ([#189](https://github.com/Suwayomi/Tachidesk-WebUI/pull/189) by @martinek)
- (r950) Update typescript dependency ([#190](https://github.com/Suwayomi/Tachidesk-WebUI/pull/190) by @martinek)
- (r951) update browserlist (by @AriaMoradi)
- (r952) Feature/batch chapter download ([#191](https://github.com/Suwayomi/Tachidesk-WebUI/pull/191) by @martinek)
- (r953) Memoize empty view face so it does not change on rerender ([#193](https://github.com/Suwayomi/Tachidesk-WebUI/pull/193) by @martinek)
- (r954) Feature/batch chapter actions ([#194](https://github.com/Suwayomi/Tachidesk-WebUI/pull/194) by @martinek)
- (r955) Fix navbar back button behavior ([#195](https://github.com/Suwayomi/Tachidesk-WebUI/pull/195) by @martinek)
- (r956) Options panels refactoring ([#196](https://github.com/Suwayomi/Tachidesk-WebUI/pull/196) by @martinek)
- (r957) Refactor and fix sorting in library ([#197](https://github.com/Suwayomi/Tachidesk-WebUI/pull/197) by @martinek)
- (r958) Scroll window to top when PagedPager changes page ([#198](https://github.com/Suwayomi/Tachidesk-WebUI/pull/198) by @martinek)
- (r959) Verticall scroll navigation and fix ([#200](https://github.com/Suwayomi/Tachidesk-WebUI/pull/200) by @martinek)
- (r960) Hide overflowing text in reader title if text can't be wrapped ([#199](https://github.com/Suwayomi/Tachidesk-WebUI/pull/199) by @martinek)
- (r961) Add safezone to scroll end detection to prevent edge cases when scrolling to the end would not detect end ([#201](https://github.com/Suwayomi/Tachidesk-WebUI/pull/201) by @martinek)
- (r962) Refactor/download queue and cleanup visuals overall ([#202](https://github.com/Suwayomi/Tachidesk-WebUI/pull/202) by @martinek)
- (r963) Fix "back" pagination on double page layout in reader for spread pages ([#203](https://github.com/Suwayomi/Tachidesk-WebUI/pull/203) by @martinek)
# Server: v0.6.5 + WebUI: r946
## TL;DR
- Fixed Windows bundler
## Tachidesk-Server Changelog
- (r1113) v0.6.4 (by @AriaMoradi)
- (r1114) fix broken links (by @AriaMoradi)
- (r1115) fix more broken stuff (by @AriaMoradi)
- (r1116) fix more broken stuff (by @AriaMoradi)
- (r1117) fix more broken stuff (by @AriaMoradi)
- (r1118) Update winget.yml ([#393](https://github.com/Suwayomi/Tachidesk-Server/pull/393) by @vedantmgoyal2009)
- (r1119) fix jre path([#396](https://github.com/Suwayomi/Tachidesk-Server/pull/396) by @voltrare)
- (r1120) Fix deb package ([#397](https://github.com/Suwayomi/Tachidesk-Server/pull/397) by @mahor1221)
- (r1121) bump version (by @AriaMoradi)
## Tachidesk-WebUI Changelog
- None
# Server: v0.6.4 + WebUI: r946
## TL;DR
- No new major features
@@ -19,18 +189,18 @@
- (r1098) fix formatting by kotlinter (by @AriaMoradi)
- (r1099) bump WebUI (by @AriaMoradi)
- (r1100) fix WebUI release name (by @AriaMoradi)
- (r1101) Fix documentation errors ([#358](https://github.com/Suwayomi/scripts/pull/358) by @Syer10)
- (r1102) Docs improvements ([#359](https://github.com/Suwayomi/scripts/pull/359) by @Syer10)
- (r1103) Add linux-all.tar.gz & systemd service ([#366](https://github.com/Suwayomi/scripts/pull/366)) mahor1221@pm.me
- (r1104) Publish to Windows Package Managar (WinGet [#369](https://github.com/Suwayomi/scripts/pull/369)) 83997633+vedantmgoyal2009@users.noreply.github.com
- (r1105) Refactor scripts ([#370](https://github.com/Suwayomi/scripts/pull/370)) mahor1221@pm.me
- (r1106) Run workflow jobs toghether ([#371](https://github.com/Suwayomi/scripts/pull/371)) mahor1221@pm.me
- (r1107) Update gradle action ([#372](https://github.com/Suwayomi/scripts/pull/372)) mahor1221@pm.me
- (r1108) Improve DocumentationDsl, bugfix default values and add queryParams ([#378](https://github.com/Suwayomi/scripts/pull/378) by @Syer10)
- (r1109) Tidy up bundler script ([#380](https://github.com/Suwayomi/scripts/pull/380)) mahor1221@pm.me
- (r1110) Replace linux-all with linux-assets ([#381](https://github.com/Suwayomi/scripts/pull/381)) mahor1221@pm.me
- (r1111) Rename every instance of Tachidesk jar to Tachdidesk-Server.jar ([#384](https://github.com/Suwayomi/scripts/pull/384) by @AriaMoradi)
- (r1112) Fix mistakes from #384 ([#385](https://github.com/Suwayomi/scripts/pull/385) by @AriaMoradi)
- (r1101) Fix documentation errors ([#358](https://github.com/Suwayomi/Tachidesk-Server/pull/358) by @Syer10)
- (r1102) Docs improvements ([#359](https://github.com/Suwayomi/Tachidesk-Server/pull/359) by @Syer10)
- (r1103) Add linux-all.tar.gz & systemd service ([#366](https://github.com/Suwayomi/Tachidesk-Server/pull/366) by @mahor1221)
- (r1104) Publish to Windows Package Managar (WinGet) ([#369](https://github.com/Suwayomi/Tachidesk-Server/pull/369) by @vedantmgoyal2009)
- (r1105) Refactor scripts ([#370](https://github.com/Suwayomi/Tachidesk-Server/pull/370) by @mahor1221)
- (r1106) Run workflow jobs toghether ([#371](https://github.com/Suwayomi/Tachidesk-Server/pull/371) by @mahor1221)
- (r1107) Update gradle action ([#372](https://github.com/Suwayomi/Tachidesk-Server/pull/372) by @mahor1221)
- (r1108) Improve DocumentationDsl, bugfix default values and add queryParams ([#378](https://github.com/Suwayomi/Tachidesk-Server/pull/378) by @Syer10)
- (r1109) Tidy up bundler script ([#380](https://github.com/Suwayomi/Tachidesk-Server/pull/380) by @mahor1221)
- (r1110) Replace linux-all with linux-assets ([#381](https://github.com/Suwayomi/Tachidesk-Server/pull/381) by @mahor1221)
- (r1111) Rename every instance of Tachidesk jar to Tachdidesk-Server.jar ([#384](https://github.com/Suwayomi/Tachidesk-Server/pull/384) by @AriaMoradi)
- (r1112) Fix mistakes from #384 ([#385](https://github.com/Suwayomi/Tachidesk-Server/pull/385) by @AriaMoradi)
## Tachidesk-WebUI Changelog
- (r943) fix default width ([#171](https://github.com/Suwayomi/Tachidesk-WebUI/pull/171) by @Robonau)

View File

@@ -2,9 +2,10 @@
## Where should I start?
Checkout [This Kanban Board](https://github.com/Suwayomi/Tachidesk/projects/1) to see the rough development roadmap.
**Note 1:** Notify the developers on [Suwayomi discord](https://discord.gg/DDZdqZWaHA) (#tachidesk-server and #tachidesk-webui channels) or open a WIP pull request before starting if you decide to take on working on anything from/not from the roadmap in order to avoid parallel efforts on the same issue/feature.
**Note 2:** Your pull request will be squashed into a single commit.
### Important notes
- Notify the developers on [Suwayomi discord](https://discord.gg/DDZdqZWaHA) (#tachidesk-server and #tachidesk-webui channels) or open a WIP pull request before starting if you decide to take on working on anything from/not from the roadmap in order to avoid parallel efforts on the same issue/feature.
- Your pull request will be squashed into a single commit.
- We hate big pull requests, make them as small as possible, change one meaningful thing. Spam pull requests, we don't mind.
### Project goals and vision
- Porting Tachiyomi and covering it's features

View File

@@ -34,25 +34,22 @@ A free and open source manga reader server that runs extensions built for [Tachi
Tachidesk is an independent Tachiyomi compatible software and is **not a Fork of** Tachiyomi.
`Tachidesk` is a general term used to describe the combination of Tachidesk-Server(this project) and one of our clients.
Think of it roughly like the concept of "distribution" in GNU/Linux distributions, in which Linux(Tachidesk-Server) is the kernel and the difference is which desktop environment(Tachidesk client) you get with it.
Tachidesk-Server is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it. This includes Windows, Linux, macOS, chrome OS, etc. Follow [Downloading and Running the app](#downloading-and-running-the-app) for installation instructions.
Ability to sync with Tachiyomi is a planned feature.
Ability to sync with Tachiyomi is a planned feature, for more info look [here](#syncing-with-tachiyomi).
# Tachidesk client projects
**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](https://github.com/Suwayomi/Tachidesk-Server/releases/latest) 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:
##### 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 ships with by default.
- [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-Sorayomi](https://github.com/Suwayomi/Tachidesk-Sorayomi): A Flutter front-end for Desktop(Linux, windows, etc.), Web and Android. UI and UX similar to Tachiyomi.
- [Tachidesk-qtui](https://github.com/Suwayomi/Tachidesk-qtui): A C++/Qt front-end for mobile devices(Android/linux), feature support is basic.
- [Tachidesk-Sorayomi](https://github.com/Suwayomi/Tachidesk-Sorayomi): A Flutter front-end for Desktop(Linux, windows, etc.), Web and Android with a User Inerface inspired by 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.
- [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js.
- [Tachidesk-GTK](https://github.com/mahor1221/Tachidesk-GTK): A native Rust/GTK desktop client.
## Is this application usable? Should I test it?
Here is a list of current features:
@@ -85,7 +82,7 @@ Download the latest `win32`(Windows 32-bit) or `win64`(Windows 64-bit) release f
Unzip the downloaded file and double click on one of the launcher scripts.
### macOS
Download the latest `macOS-x64`(older macOS systems) or `macOS-arm64`(Apple M1) release from [the releases section](https://github.com/Suwayomi/Tachidesk-Server/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Tachidesk-Server-preview/releases).
Download the latest `macOS-x64`(older macOS systems) or `macOS-arm64`(Apple M1 and newer) release from [the releases section](https://github.com/Suwayomi/Tachidesk-Server/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Tachidesk-Server-preview/releases).
Unzip the downloaded file and double click on one of the launcher scripts.
@@ -148,10 +145,12 @@ Check out [this wiki page](https://github.com/Suwayomi/Tachidesk-Server/wiki/Con
If you face issues with your setup then we are happy to provide help, just join our discord server(a discord badge is on the top of the page, you are just a click clack away!).
## Syncing With Tachiyomi
### The Tachidesk extension
- You can install the `Tachidesk` extension inside tachiyomi.
- The extension will load Tachidesk library.
- By manipulating filters you can browse your categories.
### The Suwayomi extension and tracker
- You can install the `Suwayomi` extension inside tachiyomi.
- The extension will load your Tachidesk library.
- By manipulating extension search filters you can browse your categories.
- You can enable the Suwayomi tracker to track reading progress with your Tachidesk server.
- Note: Tachiyomi [only allowes tracking one way](https://github.com/tachiyomiorg/tachiyomi/issues/1626), meaning that by reading chapters on other Tachidesk clients the last read chapter number will updated on the tracker but tachiyomi won't automatically mark them as read for you.
### Other methods
Checkout [this issue](https://github.com/Suwayomi/Tachidesk-Server/issues/159) for tracking progress.
@@ -180,3 +179,7 @@ Changes to both codebases is licensed under `MPL v. 2.0` as the rest of this pro
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 http://mozilla.org/MPL/2.0/.
## Disclaimer
The developer of this application does not have any affiliation with the content providers available.

View File

@@ -1,12 +1,14 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
import org.jmailen.gradle.kotlinter.tasks.FormatTask
import org.jmailen.gradle.kotlinter.tasks.LintTask
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
kotlin("jvm") version kotlinVersion
kotlin("plugin.serialization") version kotlinVersion
id("org.jmailen.kotlinter") version "3.8.0"
id("com.github.gmazzo.buildconfig") version "3.0.3" apply false
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlinter)
alias(libs.plugins.buildconfig) apply false
alias(libs.plugins.download)
}
allprojects {
@@ -17,38 +19,24 @@ allprojects {
repositories {
mavenCentral()
google()
maven("https://jitpack.io")
maven("https://github.com/Suwayomi/Tachidesk-Server/raw/android-jar/")
maven("https://jitpack.io")
}
}
val projects = listOf(
project(":AndroidCompat"),
project(":AndroidCompat:Config"),
project(":server")
)
configure(projects) {
apply(plugin = "org.jetbrains.kotlin.jvm")
apply(plugin = "org.jetbrains.kotlin.plugin.serialization")
apply(plugin = "org.jmailen.kotlinter")
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
subprojects {
plugins.withType<JavaPlugin> {
extensions.configure<JavaPluginExtension> {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
tasks {
withType<KotlinCompile> {
dependsOn(formatKotlin)
withType<KotlinJvmCompile> {
dependsOn("formatKotlin")
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs = listOf(
"-Xopt-in=kotlin.RequiresOptIn",
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}
}
@@ -60,56 +48,4 @@ configure(projects) {
source(files("src/kotlin"))
}
}
dependencies {
// Kotlin
implementation(kotlin("stdlib-jdk8"))
implementation(kotlin("reflect"))
testImplementation(kotlin("test-junit5"))
// coroutines
val coroutinesVersion = "1.6.0"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
val kotlinSerializationVersion = "1.3.2"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
// Dependency Injection
implementation("org.kodein.di:kodein-di-conf-jvm:7.10.0")
// Logging
implementation("org.slf4j:slf4j-api:1.7.32")
implementation("ch.qos.logback:logback-classic:1.2.6")
implementation("io.github.microutils:kotlin-logging:2.1.21")
// ReactiveX
implementation("io.reactivex:rxjava:1.3.8")
implementation("io.reactivex:rxkotlin:1.0.0")
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
// dependency both in AndroidCompat and extensions, version locked by Tachiyomi app/extensions
implementation("org.jsoup:jsoup:1.14.3")
// dependency of :AndroidCompat:Config
implementation("com.typesafe:config:1.4.1")
implementation("io.github.config4k:config4k:0.4.2")
// to get application content root
implementation("net.harawata:appdirs:1.2.1")
// dex2jar
val dex2jarVersion = "v35"
implementation("com.github.ThexXTURBOXx.dex2jar:dex-translator:$dex2jarVersion")
implementation("com.github.ThexXTURBOXx.dex2jar:dex-tools:$dex2jarVersion")
// APK parser
implementation("net.dongliu:apk-parser:2.6.10")
// dependency both in AndroidCompat and server, version locked by javalin
implementation("com.fasterxml.jackson.core:jackson-annotations:2.12.4")
}
}

View File

@@ -7,21 +7,18 @@ import java.io.BufferedReader
* 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/. */
const val kotlinVersion = "1.6.10"
const val MainClass = "suwayomi.tachidesk.MainKt"
// should be bumped with each stable release
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.6.4"
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.7.0"
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r946"
val sorayomiRevisionTag = System.getenv("SorayomiRevision") ?: "0.1.5"
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r983"
// counts commits on the master branch
val tachideskRevision = runCatching {
System.getenv("ProductRevision") ?: Runtime
.getRuntime()
.exec("git rev-list HEAD --count")
System.getenv("ProductRevision") ?: ProcessBuilder()
.command("git", "rev-list", "HEAD", "--count")
.start()
.let { process ->
process.waitFor()
val output = process.inputStream.use {

217
gradle/libs.versions.toml Normal file
View File

@@ -0,0 +1,217 @@
[versions]
kotlin = "1.8.0"
coroutines = "1.6.4"
serialization = "1.4.1"
okhttp = "5.0.0-alpha.11" # Major version is locked by Tachiyomi extensions
javalin = "4.6.6" # Javalin 5.0.0+ requires Java 11
jackson = "2.13.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
exposed = "0.40.1"
dex2jar = "v60"
rhino = "1.7.14"
settings = "1.0.0-RC"
twelvemonkeys = "3.9.4"
playwright = "1.28.0"
[libraries]
# Kotlin
kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
kotlin-test-junit5 = { module = "org.jetbrains.kotlin:kotlin-test-junit5", version.ref = "kotlin" }
# Coroutines
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
coroutines-jdk8 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8", version.ref = "coroutines" }
coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
# Serialization
serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "serialization" }
# Logging
slf4japi = "org.slf4j:slf4j-api:2.0.6"
logback = "ch.qos.logback:logback-classic:1.3.5"
kotlinlogging = "io.github.microutils:kotlin-logging:3.0.5"
# OkHttp
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp" }
okio = "com.squareup.okio:okio:3.3.0"
# Javalin api
javalin-core = { module = "io.javalin:javalin", version.ref = "javalin" }
javalin-openapi = { module = "io.javalin:javalin-openapi", version.ref = "javalin" }
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }
jackson-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" }
jackson-annotations = { module = "com.fasterxml.jackson.core:jackson-annotations", version.ref = "jackson" }
# Exposed ORM
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" }
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
exposed-javatime = { module = "org.jetbrains.exposed:exposed-java-time", version.ref = "exposed" }
h2 = "com.h2database:h2:1.4.200" # current database driver, can't update to h2 v2 without sql migration
# Exposed Migrations
exposed-migrations = "com.github.Suwayomi:exposed-migrations:3.2.0"
# Dependency Injection
kodein = "org.kodein.di:kodein-di-conf-jvm:7.15.0"
# tray icon
systemtray-core = "com.dorkbox:SystemTray:4.2.1"
systemtray-utils = "com.dorkbox:Utilities:1.39" # version locked by SystemTray
systemtray-desktop = "com.dorkbox:Desktop:1.0"
# dependencies of Tachiyomi extensions
injekt = "com.github.inorichi.injekt:injekt-core:65b0440"
rxjava = "io.reactivex:rxjava:1.3.8"
jsoup = "org.jsoup:jsoup:1.15.3"
# Config
config = "com.typesafe:config:1.4.2"
config4k = "io.github.config4k:config4k:0.5.0"
# Sort
sort = "com.github.gpanther:java-nat-sort:natural-comparator-1.1"
# Android stub library
android-stubs = "com.github.Suwayomi:android-jar:1.0.0"
# Asm modificiation
asm = "org.ow2.asm:asm:9.4" # version locked by Dex2Jar
dex2jar-translator = { module = "com.github.ThexXTURBOXx.dex2jar:dex-translator", version.ref = "dex2jar" }
dex2jar-tools = { module = "com.github.ThexXTURBOXx.dex2jar:dex-tools", version.ref = "dex2jar" }
# APK
apk-parser = "net.dongliu:apk-parser:2.6.10"
apksig = "com.android.tools.build:apksig:7.2.1"
# Xml
xmlpull = "xmlpull:xmlpull:1.1.3.4a"
# Disk & File
appdirs = "net.harawata:appdirs:1.2.1"
zip4j = "net.lingala.zip4j:zip4j:2.11.2"
commonscompress = "org.apache.commons:commons-compress:1.23.0"
junrar = "com.github.junrar:junrar:7.5.3"
# CloudflareInterceptor
playwright = { module = "com.microsoft.playwright:playwright", version.ref = "playwright" }
# AES/CBC/PKCS7Padding Cypher provider
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.72"
# AndroidX annotations
android-annotations = "androidx.annotation:annotation:1.5.0"
# Substitute for duktape-android
rhino-runtime = { module = "org.mozilla:rhino-runtime", version.ref = "rhino" } # slimmer version of 'org.mozilla:rhino'
rhino-engine = { module = "org.mozilla:rhino-engine", version.ref = "rhino" } # provides the same interface as 'javax.script' a.k.a Nashorn
# Settings
settings-core = { module = "com.russhwolf:multiplatform-settings-jvm", version.ref = "settings" }
settings-serialization = { module = "com.russhwolf:multiplatform-settings-serialization-jvm", version.ref = "settings" }
# ICU4J
icu4j = "com.ibm.icu:icu4j:72.1"
# Image Decoding implementation provider
twelvemonkeys-common-lang = { module = "com.twelvemonkeys.common:common-lang", version.ref = "twelvemonkeys" }
twelvemonkeys-common-io = { module = "com.twelvemonkeys.common:common-io", version.ref = "twelvemonkeys" }
twelvemonkeys-common-image = { module = "com.twelvemonkeys.common:common-image", version.ref = "twelvemonkeys" }
twelvemonkeys-imageio-core = { module = "com.twelvemonkeys.imageio:imageio-core", version.ref = "twelvemonkeys" }
twelvemonkeys-imageio-metadata = { module = "com.twelvemonkeys.imageio:imageio-metadata", version.ref = "twelvemonkeys" }
twelvemonkeys-imageio-jpeg = { module = "com.twelvemonkeys.imageio:imageio-jpeg", version.ref = "twelvemonkeys" }
twelvemonkeys-imageio-webp = { module = "com.twelvemonkeys.imageio:imageio-webp", version.ref = "twelvemonkeys" }
# Testing
mockk = "io.mockk:mockk:1.13.2"
[plugins]
# Kotlin
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin"}
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"}
# Linter
kotlinter = { id = "org.jmailen.kotlinter", version = "3.12.0"}
# Build config
buildconfig = { id = "com.github.gmazzo.buildconfig", version = "3.1.0"}
# Download
download = { id = "de.undercouch.download", version = "5.3.0"}
# ShadowJar
shadowjar = { id = "com.github.johnrengelman.shadow", version = "7.1.2"}
[bundles]
shared = [
"kotlin-stdlib-jdk8",
"kotlin-reflect",
"coroutines-core",
"coroutines-jdk8",
"serialization-json",
"serialization-protobuf",
"kodein",
"slf4japi",
"logback",
"kotlinlogging",
"appdirs",
"rxjava",
"jsoup",
"config",
"config4k",
"dex2jar-translator",
"dex2jar-tools",
"apk-parser",
"jackson-annotations"
]
sharedTest = [
"kotlin-test-junit5",
"coroutines-test",
]
okhttp = [
"okhttp-core",
"okhttp-logging",
"okhttp-dnsoverhttps",
]
javalin = [
"javalin-core",
"javalin-openapi",
]
jackson = [
"jackson-databind",
"jackson-kotlin",
"jackson-annotations",
]
exposed = [
"exposed-core",
"exposed-dao",
"exposed-jdbc",
"exposed-javatime",
]
systemtray = [
"systemtray-core",
"systemtray-utils",
"systemtray-desktop"
]
rhino = [
"rhino-runtime",
"rhino-engine",
]
settings = [
"settings-core",
"settings-serialization",
]
twelvemonkeys = [
"twelvemonkeys-common-lang",
"twelvemonkeys-common-io",
"twelvemonkeys-common-image",
"twelvemonkeys-imageio-core",
"twelvemonkeys-imageio-metadata",
"twelvemonkeys-imageio-jpeg",
"twelvemonkeys-imageio-webp",
]

Binary file not shown.

View File

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

16
gradlew vendored
View File

@@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -32,10 +32,10 @@
# 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».
# * 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:
#
@@ -205,6 +205,12 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.

14
gradlew.bat vendored
View File

@@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,7 +25,7 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal

View File

@@ -26,6 +26,8 @@ main() {
set -- "${POSITIONAL_ARGS[@]}"
OS="$1"
PLAYWRIGHT_VERSION="$(cat gradle/libs.versions.toml | grep -oP "playwright = \"\K([0-9\.]*)(?=\")")"
PLAYWRIGHT_REVISION="$(curl --silent "https://raw.githubusercontent.com/microsoft/playwright/v$PLAYWRIGHT_VERSION/packages/playwright-core/browsers.json" 2>&1 | grep -ozP "\"name\": \"chromium\",\n *\"revision\": \"\K[0-9]*")"
JAR="$(ls server/build/*.jar | tail -n1)"
RELEASE_NAME="$(echo "${JAR%.*}" | xargs basename)-$OS"
RELEASE_VERSION="$(tmp="${JAR%-*}"; echo "${tmp##*-}" | tr -d v)"
@@ -50,44 +52,64 @@ main() {
;;
linux-x64)
JRE="OpenJDK8U-jre_x64_linux_hotspot_8u302b08.tar.gz"
JRE_URL="https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u302-b08/$JRE"
JRE_RELEASE="jdk8u302-b08"
JRE_DIR="$JRE_RELEASE-jre"
JRE_URL="https://github.com/adoptium/temurin8-binaries/releases/download/$JRE_RELEASE/$JRE"
ELECTRON="electron-$electron_version-linux-x64.zip"
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
download_jre_and_electron
PLAYWRIGHT_PLATFORM="linux"
setup_playwright
RELEASE="$RELEASE_NAME.tar.gz"
make_linux_bundle
move_release_to_output_dir
;;
macOS-x64)
JRE="OpenJDK8U-jre_x64_mac_hotspot_8u302b08.tar.gz"
JRE_URL="https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u302-b08/$JRE"
JRE_RELEASE="jdk8u302-b08"
JRE_DIR="$JRE_RELEASE-jre"
JRE_URL="https://github.com/adoptium/temurin8-binaries/releases/download/$JRE_RELEASE/$JRE"
ELECTRON="electron-$electron_version-darwin-x64.zip"
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
download_jre_and_electron
PLAYWRIGHT_PLATFORM="mac"
setup_playwright
RELEASE="$RELEASE_NAME.zip"
make_macos_bundle
move_release_to_output_dir
;;
macOS-arm64)
JRE="zulu8.56.0.23-ca-jre8.0.302-macosx_aarch64.tar.gz"
JRE_RELEASE="zulu8.56.0.23-ca-jre8.0.302-macosx_aarch64"
JRE_DIR="$JRE_RELEASE/zulu-8.jre"
JRE_URL="https://cdn.azul.com/zulu/bin/$JRE"
ELECTRON="electron-$electron_version-darwin-arm64.zip"
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
download_jre_and_electron
PLAYWRIGHT_PLATFORM="mac-arm64"
setup_playwright
RELEASE="$RELEASE_NAME.zip"
make_macos_bundle
move_release_to_output_dir
;;
windows-x86)
JRE="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
JRE_URL="https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/$JRE"
JRE_RELEASE="jdk8u292-b10"
JRE_DIR="$JRE_RELEASE-jre"
JRE_URL="https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/$JRE_RELEASE/$JRE"
ELECTRON="electron-$electron_version-win32-ia32.zip"
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
download_jre_and_electron
PLAYWRIGHT_PLATFORM="win64"
setup_playwright
RELEASE="$RELEASE_NAME.zip"
make_windows_bundle
move_release_to_output_dir
@@ -98,11 +120,16 @@ main() {
;;
windows-x64)
JRE="OpenJDK8U-jre_x64_windows_hotspot_8u302b08.zip"
JRE_URL="https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u302-b08/$JRE"
JRE_RELEASE="jdk8u302-b08"
JRE_DIR="$JRE_RELEASE-jre"
JRE_URL="https://github.com/adoptium/temurin8-binaries/releases/download/$JRE_RELEASE/$JRE"
ELECTRON="electron-$electron_version-win32-x64.zip"
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
download_jre_and_electron
PLAYWRIGHT_PLATFORM="win64"
setup_playwright
RELEASE="$RELEASE_NAME.zip"
make_windows_bundle
move_release_to_output_dir
@@ -133,17 +160,15 @@ download_jre_and_electron() {
curl -L "$ELECTRON_URL" -o "$ELECTRON"
fi
mkdir -p "$RELEASE_NAME/jre/"
local ext="${JRE##*.}"
local jre_dir
if [ "$ext" = "zip" ]; then
jre_dir="$(unzip "$JRE" | sed -n '2p' | cut -d: -f2 | xargs basename)"
mv "$jre_dir" "$RELEASE_NAME/jre"
unzip "$JRE"
else
# --strip-components=1: untar an archive without the root folder
tar xvf "$JRE" --strip-components=1 -C "$RELEASE_NAME/jre/"
tar xvf "$JRE"
fi
mv "$JRE_DIR" "$RELEASE_NAME/jre"
unzip "$ELECTRON" -d "$RELEASE_NAME/electron/"
tree
}
copy_linux_package_assets_to() {
@@ -180,31 +205,27 @@ make_macos_bundle() {
# https://wiki.debian.org/SimplePackagingTutorial
# https://www.debian.org/doc/manuals/packaging-tutorial/packaging-tutorial.pdf
make_deb_package() {
local temp_dir
temp_dir="$(mktemp -d)"
trap "rm -rf $temp_dir" RETURN
cp "$JAR" "$RELEASE_NAME/Tachidesk-Server.jar"
tar -I "gzip" -cvf "$RELEASE_NAME.tar.gz" "$RELEASE_NAME/"
#behind $RELEASE_VERSION is underscore "_"
local upstream_source="tachidesk-server_$RELEASE_VERSION.orig.tar.gz"
mv "$RELEASE_NAME.tar.gz" "$temp_dir/$upstream_source"
cp -r "scripts/resources/deb/" "$RELEASE_NAME/debian/"
copy_linux_package_assets_to "$RELEASE_NAME/debian/"
sed -i "s/\$pkgver/$RELEASE_VERSION/" "$RELEASE_NAME/debian/changelog"
sed -i "s/\$pkgrel/1/" "$RELEASE_NAME/debian/changelog"
#behind $RELEASE_VERSION is hyphen "-"
local source_dir="tachidesk-server-$RELEASE_VERSION"
mv "$RELEASE_NAME/" "$temp_dir/$source_dir/"
#behind $RELEASE_VERSION is underscore "_"
local upstream_source="tachidesk-server_$RELEASE_VERSION.orig.tar.gz"
mkdir "$RELEASE_NAME/$source_dir/"
cp "$JAR" "$RELEASE_NAME/$source_dir/Tachidesk-Server.jar"
copy_linux_package_assets_to "$RELEASE_NAME/$source_dir/"
tar -I "gzip" -C "$RELEASE_NAME/" -cvf "$upstream_source" "$source_dir"
cp -r "scripts/resources/deb/" "$RELEASE_NAME/$source_dir/debian/"
sed -i "s/\$pkgver/$RELEASE_VERSION/" "$RELEASE_NAME/$source_dir/debian/changelog"
sed -i "s/\$pkgrel/1/" "$RELEASE_NAME/$source_dir/debian/changelog"
sudo apt install devscripts build-essential dh-exec
cd "$temp_dir/$source_dir/"
cd "$RELEASE_NAME/$source_dir/"
dpkg-buildpackage --no-sign --build=all
cd -
local deb="tachidesk-server_$RELEASE_VERSION-1_all.deb"
mv "$temp_dir/$deb" "$RELEASE"
mv "$RELEASE_NAME/$deb" "$RELEASE"
}
make_windows_bundle() {
@@ -264,6 +285,11 @@ make_windows_package() {
"$RELEASE_NAME/jre.wxs" "$RELEASE_NAME/electron.wxs" -o "$RELEASE"
}
setup_playwright() {
mkdir "$RELEASE_NAME/bin"
curl -L "https://playwright.azureedge.net/builds/chromium/$PLAYWRIGHT_REVISION/chromium-$PLAYWRIGHT_PLATFORM.zip" -o "$RELEASE_NAME/bin/chromium.zip"
}
# Error handler
# set -u: Treat unset variables as an error when substituting.
# set -o pipefail: Prevents errors in pipeline from being masked.

View File

@@ -2,13 +2,13 @@ Source: tachidesk-server
Section: web
Priority: optional
Maintainer: Mahor1221 <mahor1221@pm.me>
Build-Depends: debhelper-compat (= 12), dh-exec
Build-Depends: debhelper-compat (= 13), 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)
Depends: ${misc:Depends}, java8-runtime-headless, libc++-dev
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.

View File

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

View File

@@ -1,80 +1,75 @@
import de.undercouch.gradle.tasks.download.Download
import java.time.Instant
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id(libs.plugins.kotlin.jvm.get().pluginId)
id(libs.plugins.kotlin.serialization.get().pluginId)
id(libs.plugins.kotlinter.get().pluginId)
application
id("com.github.johnrengelman.shadow") version "7.1.2"
id("com.github.gmazzo.buildconfig")
alias(libs.plugins.shadowjar)
id(libs.plugins.buildconfig.get().pluginId)
}
dependencies {
// okhttp
val okhttpVersion = "4.9.3" // Major version is locked by Tachiyomi extensions
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
implementation("com.squareup.okio:okio:3.0.0")
// Shared
implementation(libs.bundles.shared)
testImplementation(libs.bundles.sharedTest)
// OkHttp
implementation(libs.bundles.okhttp)
implementation(libs.okio)
// Javalin api
implementation("io.javalin:javalin:4.2.0")
implementation("io.javalin:javalin-openapi:4.2.0")
// jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
val jacksonVersion = "2.12.4"
implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
implementation(libs.bundles.javalin)
implementation(libs.bundles.jackson)
// Exposed ORM
val exposedVersion = "0.34.1"
implementation("org.jetbrains.exposed:exposed-core:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-java-time:$exposedVersion")
// current database driver
implementation("com.h2database:h2:1.4.200")
implementation(libs.bundles.exposed)
implementation(libs.h2)
// Exposed Migrations
implementation("com.github.Suwayomi:exposed-migrations:3.1.4")
implementation(libs.exposed.migrations)
// tray icon
implementation("com.dorkbox:SystemTray:4.1")
implementation("com.dorkbox:Utilities:1.9") // version locked by SystemTray
implementation(libs.bundles.systemtray)
// dependencies of Tachiyomi extensions, some are duplicate, keeping it here for reference
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
implementation("io.reactivex:rxjava:1.3.8")
implementation("org.jsoup:jsoup:1.14.3")
implementation("app.cash.quickjs:quickjs-jvm:0.9.2")
implementation(libs.injekt)
implementation(libs.okhttp.core)
implementation(libs.rxjava)
implementation(libs.jsoup)
// Sort
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
implementation(libs.sort)
// asm for ByteCodeEditor(fixing SimpleDateFormat) (must match Dex2Jar version)
implementation("org.ow2.asm:asm:9.2")
implementation(libs.asm)
// Disk & File
implementation("net.lingala.zip4j:zip4j:2.9.1")
implementation("com.github.junrar:junrar:7.5.0")
implementation(libs.zip4j)
implementation(libs.commonscompress)
implementation(libs.junrar)
// CloudflareInterceptor
implementation("net.sourceforge.htmlunit:htmlunit:2.56.0")
implementation(libs.playwright)
// AES/CBC/PKCS7Padding Cypher provider for zh.copymanga
implementation("org.bouncycastle:bcprov-jdk18on:1.71")
// Source models and interfaces from Tachiyomi 1.x
// using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi
// implementation("tachiyomi.sourceapi:source-api:1.1")
implementation(libs.bouncycastle)
// AndroidCompat
implementation(project(":AndroidCompat"))
implementation(project(":AndroidCompat:Config"))
implementation(projects.androidCompat)
implementation(projects.androidCompat.config)
// uncomment to test extensions directly
// implementation(fileTree("lib/"))
implementation(kotlin("script-runtime"))
testImplementation("io.mockk:mockk:1.12.2")
implementation("com.expediagroup:graphql-kotlin-server:6.4.0")
implementation("com.expediagroup:graphql-kotlin-schema-generator:6.4.0")
implementation("com.graphql-java:graphql-java-extended-scalars:20.0")
testImplementation(libs.mockk)
}
application {
@@ -110,9 +105,6 @@ buildConfig {
buildConfigField("String", "WEBUI_REPO", quoteWrap("https://github.com/Suwayomi/Tachidesk-WebUI-preview"))
buildConfigField("String", "WEBUI_TAG", quoteWrap(webUIRevisionTag))
buildConfigField("String", "SORAYOMI_REPO", quoteWrap("https://github.com/Suwayomi/Tachidesk-Sorayomi"))
buildConfigField("String", "SORAYOMI_TAG", quoteWrap(sorayomiRevisionTag))
buildConfigField("String", "GITHUB", quoteWrap("https://github.com/Suwayomi/Tachidesk-Server"))
buildConfigField("String", "DISCORD", quoteWrap("https://discord.gg/DDZdqZWaHA"))

View File

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

View File

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

View File

@@ -0,0 +1,33 @@
package eu.kanade.domain.track.service
import eu.kanade.tachiyomi.data.track.TrackService
// import eu.kanade.tachiyomi.data.track.anilist.Anilist
import tachiyomi.core.preference.PreferenceStore
class TrackPreferences(
private val preferenceStore: PreferenceStore
) {
fun trackUsername(sync: TrackService) = preferenceStore.getString(trackUsername(sync.id), "")
fun trackPassword(sync: TrackService) = preferenceStore.getString(trackPassword(sync.id), "")
fun setTrackCredentials(sync: TrackService, username: String, password: String) {
trackUsername(sync).set(username)
trackPassword(sync).set(password)
}
fun trackToken(sync: TrackService) = preferenceStore.getString(trackToken(sync.id), "")
// fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
companion object {
fun trackUsername(syncId: Long) = "pref_mangasync_username_$syncId"
private fun trackPassword(syncId: Long) = "pref_mangasync_password_$syncId"
private fun trackToken(syncId: Long) = "track_token_$syncId"
}
}

View File

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

View File

@@ -16,6 +16,7 @@ package eu.kanade.tachiyomi
// import eu.kanade.tachiyomi.data.track.TrackManager
// import eu.kanade.tachiyomi.extension.ExtensionManager
import android.app.Application
import eu.kanade.tachiyomi.network.JavaScriptEngine
import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.serialization.json.Json
import rx.Observable
@@ -29,7 +30,6 @@ import uy.kohesive.injekt.api.get
class AppModule(val app: Application) : InjektModule {
override fun InjektRegistrar.registerInjectables() {
addSingleton(app)
// addSingletonFactory { PreferencesHelper(app) }
@@ -42,6 +42,8 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { NetworkHelper(app) }
addSingletonFactory { JavaScriptEngine(app) }
// addSingletonFactory { SourceManager(app).also { get<ExtensionManager>().init(it) } }
//
// addSingletonFactory { ExtensionManager(app) }

View File

@@ -0,0 +1,46 @@
package eu.kanade.tachiyomi.data.database.models
import java.io.Serializable
interface Track : Serializable {
var id: Long?
var manga_id: Long
var sync_id: Int
var media_id: Long
var library_id: Long?
var title: String
var last_chapter_read: Float
var total_chapters: Int
var score: Float
var status: Int
var started_reading_date: Long
var finished_reading_date: Long
var tracking_url: String
fun copyPersonalFrom(other: Track) {
last_chapter_read = other.last_chapter_read
score = other.score
status = other.status
started_reading_date = other.started_reading_date
finished_reading_date = other.finished_reading_date
}
companion object {
fun create(serviceId: Long): Track = TrackImpl().apply {
sync_id = serviceId.toInt()
}
}
}

View File

@@ -0,0 +1,30 @@
package eu.kanade.tachiyomi.data.database.models
class TrackImpl : Track {
override var id: Long? = null
override var manga_id: Long = 0
override var sync_id: Int = 0
override var media_id: Long = 0
override var library_id: Long? = null
override lateinit var title: String
override var last_chapter_read: Float = 0F
override var total_chapters: Int = 0
override var score: Float = 0f
override var status: Int = 0
override var started_reading_date: Long = 0
override var finished_reading_date: Long = 0
override var tracking_url: String = ""
}

View File

@@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.data.track
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.source.Source
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.track.model.Track
/**
* An Enhanced Track Service will never prompt the user to match a manga with the remote.
* It is expected that such Track Service can only work with specific sources and unique IDs.
*/
interface EnhancedTrackService {
/**
* This TrackService will only work with the sources that are accepted by this filter function.
*/
fun accept(source: Source): Boolean {
return source::class.qualifiedName in getAcceptedSources()
}
/**
* Fully qualified source classes that this track service is compatible with.
*/
fun getAcceptedSources(): List<String>
fun loginNoop()
/**
* match is similar to TrackService.search, but only return zero or one match.
*/
suspend fun match(manga: Manga): TrackSearch?
/**
* Checks whether the provided source/track/manga triplet is from this TrackService
*/
fun isTrackFrom(track: Track, manga: Manga, source: Source?): Boolean
/**
* Migrates the given track for the manga to the newSource, if possible
*/
fun migrateTrack(track: Track, manga: Manga, newSource: Source): Track?
}

View File

@@ -0,0 +1,43 @@
package eu.kanade.tachiyomi.data.track
// import eu.kanade.tachiyomi.data.track.anilist.Anilist
// import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
// import eu.kanade.tachiyomi.data.track.kavita.Kavita
// import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
// import eu.kanade.tachiyomi.data.track.komga.Komga
import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates
// import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
// import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
// import eu.kanade.tachiyomi.data.track.suwayomi.Suwayomi
class TrackManager {
companion object {
const val MYANIMELIST = 1L
const val ANILIST = 2L
const val KITSU = 3L
const val SHIKIMORI = 4L
const val BANGUMI = 5L
const val KOMGA = 6L
const val MANGA_UPDATES = 7L
const val KAVITA = 8L
const val SUWAYOMI = 9L
}
// val myAnimeList = MyAnimeList(MYANIMELIST)
// val aniList = Anilist(ANILIST)
// val kitsu = Kitsu(KITSU)
// val shikimori = Shikimori(SHIKIMORI)
// val bangumi = Bangumi(BANGUMI)
// val komga = Komga(KOMGA)
val mangaUpdates = MangaUpdates(MANGA_UPDATES)
// val kavita = Kavita(KAVITA)
// val suwayomi = Suwayomi(SUWAYOMI)
// val services = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi, komga, mangaUpdates, kavita, suwayomi)
val services = listOf(mangaUpdates)
fun getService(id: Long) = services.find { it.id == id }
fun hasLoggedServices() = services.any { it.isLogged }
}

View File

@@ -0,0 +1,188 @@
package eu.kanade.tachiyomi.data.track
// import androidx.annotation.CallSuper
// import androidx.annotation.ColorInt
// import androidx.annotation.DrawableRes
// import androidx.annotation.StringRes
// import eu.kanade.domain.base.BasePreferences
// import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
// import eu.kanade.domain.track.model.toDbTrack
// import eu.kanade.domain.track.model.toDomainTrack
import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.NetworkHelper
// import eu.kanade.tachiyomi.util.system.toast
// import logcat.LogPriority
import okhttp3.OkHttpClient
// import tachiyomi.core.util.lang.withIOContext
// import tachiyomi.core.util.lang.withUIContext
// import tachiyomi.core.util.system.logcat
// import tachiyomi.domain.chapter.interactor.GetChapterByMangaId
// import tachiyomi.domain.track.interactor.InsertTrack
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import tachiyomi.domain.track.model.Track as DomainTrack
abstract class TrackService(val id: Long) {
// val preferences: BasePreferences by injectLazy()
val trackPreferences: TrackPreferences by injectLazy()
val networkService: NetworkHelper by injectLazy()
open val client: OkHttpClient
get() = networkService.client
// Name of the manga sync service to display
abstract val name: String
// Application and remote support for reading dates
open val supportsReadingDates: Boolean = false
// @DrawableRes
// abstract fun getLogo(): Int
// @ColorInt
// abstract fun getLogoColor(): Int
abstract fun getStatusList(): List<Int>
// @StringRes
abstract fun getStatus(status: Int): String?
abstract fun getReadingStatus(): Int
abstract fun getRereadingStatus(): Int
abstract fun getCompletionStatus(): Int
abstract fun getScoreList(): List<String>
// TODO: Store all scores as 10 point in the future maybe?
open fun get10PointScore(track: DomainTrack): Float {
return track.score
}
open fun indexToScore(index: Int): Float {
return index.toFloat()
}
abstract fun displayScore(track: Track): String
abstract suspend fun update(track: Track, didReadChapter: Boolean = false): Track
abstract suspend fun bind(track: Track, hasReadChapters: Boolean = false): Track
abstract suspend fun search(query: String): List<TrackSearch>
abstract suspend fun refresh(track: Track): Track
abstract suspend fun login(username: String, password: String)
// @CallSuper
open fun logout() {
trackPreferences.setTrackCredentials(this, "", "")
}
open val isLogged: Boolean
get() = getUsername().isNotEmpty() &&
getPassword().isNotEmpty()
fun getUsername() = trackPreferences.trackUsername(this).get()
fun getPassword() = trackPreferences.trackPassword(this).get()
fun saveCredentials(username: String, password: String) {
trackPreferences.setTrackCredentials(this, username, password)
}
fun withIOContext(body: () -> Unit) { body() }
fun withUIContext(body: () -> Unit) { body() }
fun registerTracking(item: Track, mangaId: Long) {
// item.manga_id = mangaId
// try {
// withIOContext {
// val allChapters = Injekt.get<GetChapterByMangaId>().await(mangaId)
// val hasReadChapters = allChapters.any { it.read }
// bind(item, hasReadChapters)
//
// val track = item.toDomainTrack(idRequired = false) ?: return@withIOContext
//
// Injekt.get<InsertTrack>().await(track)
//
// // Update chapter progress if newer chapters marked read locally
// if (hasReadChapters) {
// val latestLocalReadChapterNumber = allChapters
// .sortedBy { it.chapterNumber }
// .takeWhile { it.read }
// .lastOrNull()
// ?.chapterNumber?.toDouble() ?: -1.0
//
// if (latestLocalReadChapterNumber > track.lastChapterRead) {
// val updatedTrack = track.copy(
// lastChapterRead = latestLocalReadChapterNumber
// )
// setRemoteLastChapterRead(updatedTrack.toDbTrack(), latestLocalReadChapterNumber.toInt())
// }
// }
//
// if (this is EnhancedTrackService) {
// // Injekt.get<SyncChaptersWithTrackServiceTwoWay>().await(allChapters, track, this@TrackService)
// }
// }
// } catch (e: Throwable) {
// withUIContext {
// // Injekt.get<Application>().toast(e.message)
// }
// }
}
fun setRemoteStatus(track: Track, status: Int) {
track.status = status
if (track.status == getCompletionStatus() && track.total_chapters != 0) {
track.last_chapter_read = track.total_chapters.toFloat()
}
withIOContext { updateRemote(track) }
}
fun setRemoteLastChapterRead(track: Track, chapterNumber: Int) {
if (track.last_chapter_read == 0F && track.last_chapter_read < chapterNumber && track.status != getRereadingStatus()) {
track.status = getReadingStatus()
}
track.last_chapter_read = chapterNumber.toFloat()
if (track.total_chapters != 0 && track.last_chapter_read.toInt() == track.total_chapters) {
track.status = getCompletionStatus()
}
withIOContext { updateRemote(track) }
}
fun setRemoteScore(track: Track, scoreString: String) {
track.score = indexToScore(getScoreList().indexOf(scoreString))
withIOContext { updateRemote(track) }
}
fun setRemoteStartDate(track: Track, epochMillis: Long) {
track.started_reading_date = epochMillis
withIOContext { updateRemote(track) }
}
fun setRemoteFinishDate(track: Track, epochMillis: Long) {
track.finished_reading_date = epochMillis
withIOContext { updateRemote(track) }
}
private fun updateRemote(track: Track) {
// withIOContext {
// try {
// update(track)
// track.toDomainTrack(idRequired = false)?.let {
// Injekt.get<InsertTrack>().await(it)
// }
// } catch (e: Exception) {
// logcat(LogPriority.ERROR, e) { "Failed to update remote track data id=$id" }
// withUIContext { Injekt.get<Application>().toast(e.message) }
// }
// }
}
}

View File

@@ -0,0 +1,101 @@
package eu.kanade.tachiyomi.data.track.mangaupdates
// import android.graphics.Color
// import androidx.annotation.StringRes
// import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.copyTo
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.toTrackSearch
import eu.kanade.tachiyomi.data.track.model.TrackSearch
class MangaUpdates(id: Long) : TrackService(id) {
companion object {
const val READING_LIST = 0
const val WISH_LIST = 1
const val COMPLETE_LIST = 2
const val UNFINISHED_LIST = 3
const val ON_HOLD_LIST = 4
}
private val interceptor by lazy { MangaUpdatesInterceptor(this) }
private val api by lazy { MangaUpdatesApi(interceptor, client) }
override val name: String = "MangaUpdates"
// override fun getLogo(): Int = R.drawable.ic_manga_updates
// override fun getLogoColor(): Int = Color.rgb(146, 160, 173)
override fun getStatusList(): List<Int> {
return listOf(READING_LIST, COMPLETE_LIST, ON_HOLD_LIST, UNFINISHED_LIST, WISH_LIST)
}
// @StringRes
override fun getStatus(status: Int): String? = when (status) {
READING_LIST -> "Reading List"
WISH_LIST -> "Wish List"
COMPLETE_LIST -> "Complete List"
ON_HOLD_LIST -> ">On Hold List"
UNFINISHED_LIST -> "Unfinished List"
else -> null
}
override fun getReadingStatus(): Int = READING_LIST
override fun getRereadingStatus(): Int = -1
override fun getCompletionStatus(): Int = COMPLETE_LIST
private val _scoreList = (0..9).flatMap { i -> (0..9).map { j -> "$i.$j" } } + listOf("10.0")
override fun getScoreList(): List<String> = _scoreList
override fun indexToScore(index: Int): Float = _scoreList[index].toFloat()
override fun displayScore(track: Track): String = track.score.toString()
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
if (track.status != COMPLETE_LIST && didReadChapter) {
track.status = READING_LIST
}
api.updateSeriesListItem(track)
return track
}
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
return try {
val (series, rating) = api.getSeriesListItem(track)
series.copyTo(track)
rating?.copyTo(track) ?: track
} catch (e: Exception) {
api.addSeriesToList(track, hasReadChapters)
track
}
}
override suspend fun search(query: String): List<TrackSearch> {
return api.search(query)
.map {
it.toTrackSearch(id)
}
}
override suspend fun refresh(track: Track): Track {
val (series, rating) = api.getSeriesListItem(track)
series.copyTo(track)
return rating?.copyTo(track) ?: track
}
override suspend fun login(username: String, password: String) {
val authenticated = api.authenticate(username, password) ?: throw Throwable("Unable to login")
saveCredentials(authenticated.uid.toString(), authenticated.sessionToken)
interceptor.newAuth(authenticated.sessionToken)
}
fun restoreSession(): String? {
return trackPreferences.trackPassword(this).get()
}
}

View File

@@ -0,0 +1,196 @@
package eu.kanade.tachiyomi.data.track.mangaupdates
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates.Companion.READING_LIST
import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates.Companion.WISH_LIST
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.Context
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.ListItem
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.Rating
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.Record
import eu.kanade.tachiyomi.network.DELETE
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.PUT
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.add
import kotlinx.serialization.json.addJsonObject
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject
// import logcat.LogPriority
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
// import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.injectLazy
class MangaUpdatesApi(
interceptor: MangaUpdatesInterceptor,
private val client: OkHttpClient
) {
private val json: Json by injectLazy()
private val baseUrl = "https://api.mangaupdates.com"
private val contentType = "application/vnd.api+json".toMediaType()
private val authClient by lazy {
client.newBuilder()
.addInterceptor(interceptor)
.build()
}
suspend fun getSeriesListItem(track: Track): Pair<ListItem, Rating?> {
val listItem = with(json) {
authClient.newCall(GET("$baseUrl/v1/lists/series/${track.media_id}"))
.awaitSuccess()
.parseAs<ListItem>()
}
val rating = getSeriesRating(track)
return listItem to rating
}
suspend fun addSeriesToList(track: Track, hasReadChapters: Boolean) {
val status = if (hasReadChapters) READING_LIST else WISH_LIST
val body = buildJsonArray {
addJsonObject {
putJsonObject("series") {
put("id", track.media_id)
}
put("list_id", status)
}
}
authClient.newCall(
POST(
url = "$baseUrl/v1/lists/series",
body = body.toString().toRequestBody(contentType)
)
)
.awaitSuccess()
.let {
if (it.code == 200) {
track.status = status
track.last_chapter_read = 1f
}
}
}
suspend fun updateSeriesListItem(track: Track) {
val body = buildJsonArray {
addJsonObject {
putJsonObject("series") {
put("id", track.media_id)
}
put("list_id", track.status)
putJsonObject("status") {
put("chapter", track.last_chapter_read.toInt())
}
}
}
authClient.newCall(
POST(
url = "$baseUrl/v1/lists/series/update",
body = body.toString().toRequestBody(contentType)
)
)
.awaitSuccess()
updateSeriesRating(track)
}
private suspend fun getSeriesRating(track: Track): Rating? {
return try {
with(json) {
authClient.newCall(GET("$baseUrl/v1/series/${track.media_id}/rating"))
.awaitSuccess()
.parseAs<Rating>()
}
} catch (e: Exception) {
null
}
}
private suspend fun updateSeriesRating(track: Track) {
if (track.score != 0f) {
val body = buildJsonObject {
put("rating", track.score)
}
authClient.newCall(
PUT(
url = "$baseUrl/v1/series/${track.media_id}/rating",
body = body.toString().toRequestBody(contentType)
)
)
.awaitSuccess()
} else {
authClient.newCall(
DELETE(
url = "$baseUrl/v1/series/${track.media_id}/rating"
)
)
.awaitSuccess()
}
}
suspend fun search(query: String): List<Record> {
val body = buildJsonObject {
put("search", query)
put(
"filter_types",
buildJsonArray {
add("drama cd")
add("novel")
}
)
}
return with(json) {
client.newCall(
POST(
url = "$baseUrl/v1/series/search",
body = body.toString().toRequestBody(contentType)
)
)
.awaitSuccess()
.parseAs<JsonObject>()
.let { obj ->
obj["results"]?.jsonArray?.map { element ->
json.decodeFromJsonElement<Record>(element.jsonObject["record"]!!)
}
}
.orEmpty()
}
}
suspend fun authenticate(username: String, password: String): Context? {
val body = buildJsonObject {
put("username", username)
put("password", password)
}
return with(json) {
client.newCall(
PUT(
url = "$baseUrl/v1/account/login",
body = body.toString().toRequestBody(contentType)
)
)
.awaitSuccess()
.parseAs<JsonObject>()
.let { obj ->
try {
json.decodeFromJsonElement<Context>(obj["context"]!!)
} catch (e: Exception) {
// logcat(LogPriority.ERROR, e)
null
}
}
}
}
}

View File

@@ -0,0 +1,29 @@
package eu.kanade.tachiyomi.data.track.mangaupdates
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
class MangaUpdatesInterceptor(
mangaUpdates: MangaUpdates
) : Interceptor {
private var token: String? = mangaUpdates.restoreSession()
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val token = token ?: throw IOException("Not authenticated with MangaUpdates")
// Add the authorization header to the original request.
val authRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer $token")
.build()
return chain.proceed(authRequest)
}
fun newAuth(token: String?) {
this.token = token
}
}

View File

@@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.data.track.mangaupdates.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Context(
@SerialName("session_token")
val sessionToken: String,
val uid: Long
)

View File

@@ -0,0 +1,10 @@
package eu.kanade.tachiyomi.data.track.mangaupdates.dto
import kotlinx.serialization.Serializable
@Serializable
data class Image(
val url: Url? = null,
val height: Int? = null,
val width: Int? = null
)

View File

@@ -0,0 +1,22 @@
package eu.kanade.tachiyomi.data.track.mangaupdates.dto
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates.Companion.READING_LIST
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ListItem(
val series: Series? = null,
@SerialName("list_id")
val listId: Int? = null,
val status: Status? = null,
val priority: Int? = null
)
fun ListItem.copyTo(track: Track): Track {
return track.apply {
this.status = listId ?: READING_LIST
this.last_chapter_read = this@copyTo.status?.chapter?.toFloat() ?: 0f
}
}

View File

@@ -0,0 +1,15 @@
package eu.kanade.tachiyomi.data.track.mangaupdates.dto
import eu.kanade.tachiyomi.data.database.models.Track
import kotlinx.serialization.Serializable
@Serializable
data class Rating(
val rating: Float? = null
)
fun Rating.copyTo(track: Track): Track {
return track.apply {
this.score = rating ?: 0f
}
}

View File

@@ -0,0 +1,38 @@
package eu.kanade.tachiyomi.data.track.mangaupdates.dto
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.util.lang.htmlDecode
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Record(
@SerialName("series_id")
val seriesId: Long? = null,
val title: String? = null,
val url: String? = null,
val description: String? = null,
val image: Image? = null,
val type: String? = null,
val year: String? = null,
@SerialName("bayesian_rating")
val bayesianRating: Double? = null,
@SerialName("rating_votes")
val ratingVotes: Int? = null,
@SerialName("latest_chapter")
val latestChapter: Int? = null
)
fun Record.toTrackSearch(id: Long): TrackSearch {
return TrackSearch.create(id).apply {
media_id = this@toTrackSearch.seriesId ?: 0L
title = this@toTrackSearch.title?.htmlDecode() ?: ""
total_chapters = 0
cover_url = this@toTrackSearch.image?.url?.original ?: ""
summary = this@toTrackSearch.description?.htmlDecode() ?: ""
tracking_url = this@toTrackSearch.url ?: ""
publishing_status = ""
publishing_type = this@toTrackSearch.type.toString()
start_date = this@toTrackSearch.year.toString()
}
}

View File

@@ -0,0 +1,9 @@
package eu.kanade.tachiyomi.data.track.mangaupdates.dto
import kotlinx.serialization.Serializable
@Serializable
data class Series(
val id: Long? = null,
val title: String? = null
)

View File

@@ -0,0 +1,9 @@
package eu.kanade.tachiyomi.data.track.mangaupdates.dto
import kotlinx.serialization.Serializable
@Serializable
data class Status(
val volume: Int? = null,
val chapter: Int? = null
)

View File

@@ -0,0 +1,9 @@
package eu.kanade.tachiyomi.data.track.mangaupdates.dto
import kotlinx.serialization.Serializable
@Serializable
data class Url(
val original: String? = null,
val thumb: String? = null
)

View File

@@ -0,0 +1,68 @@
package eu.kanade.tachiyomi.data.track.model
import eu.kanade.tachiyomi.data.database.models.Track
class TrackSearch : Track {
override var id: Long? = null
override var manga_id: Long = 0
override var sync_id: Int = 0
override var media_id: Long = 0
override var library_id: Long? = null
override lateinit var title: String
override var last_chapter_read: Float = 0F
override var total_chapters: Int = 0
override var score: Float = 0f
override var status: Int = 0
override var started_reading_date: Long = 0
override var finished_reading_date: Long = 0
override lateinit var tracking_url: String
var cover_url: String = ""
var summary: String = ""
var publishing_status: String = ""
var publishing_type: String = ""
var start_date: String = ""
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as TrackSearch
if (manga_id != other.manga_id) return false
if (sync_id != other.sync_id) return false
if (media_id != other.media_id) return false
return true
}
override fun hashCode(): Int {
var result = manga_id.hashCode()
result = 31 * result + sync_id
result = 31 * result + media_id.hashCode()
return result
}
companion object {
fun create(serviceId: Long): TrackSearch = TrackSearch().apply {
sync_id = serviceId.toInt()
}
}
}

View File

@@ -0,0 +1,27 @@
package eu.kanade.tachiyomi.network
import android.content.Context
import app.cash.quickjs.QuickJs
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* Util for evaluating JavaScript in sources.
*/
class JavaScriptEngine(context: Context) {
/**
* Evaluate arbitrary JavaScript code and get the result as a primitive type
* (e.g., String, Int).
*
* @since extensions-lib 1.4
* @param script JavaScript to execute.
* @return Result of JavaScript code as a primitive type.
*/
@Suppress("UNUSED", "UNCHECKED_CAST")
suspend fun <T> evaluate(script: String): T = withContext(Dispatchers.IO) {
QuickJs.create().use {
it.evaluate(script) as T
}
}
}

View File

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

View File

@@ -62,7 +62,7 @@ suspend fun Call.await(): Response {
object : Callback {
override fun onResponse(call: Call, response: Response) {
if (!response.isSuccessful) {
continuation.resumeWithException(Exception("HTTP error ${response.code}"))
continuation.resumeWithException(HttpException(response.code))
return
}
@@ -89,12 +89,17 @@ suspend fun Call.await(): Response {
}
}
suspend fun Call.awaitSuccess(): Response {
// awaitSuccess is a renamed version of our await, they added a new await that allows non-success error codes
return await()
}
fun Call.asObservableSuccess(): Observable<Response> {
return asObservable()
.doOnNext { response ->
if (!response.isSuccessful) {
response.close()
throw Exception("HTTP error ${response.code}")
throw HttpException(response.code)
}
}
}
@@ -116,13 +121,13 @@ fun Call.asObservableSuccess(): Observable<Response> {
@Suppress("UNUSED_PARAMETER")
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
val progressClient = newBuilder()
// .cache(null)
// .addNetworkInterceptor { chain ->
// val originalResponse = chain.proceed(chain.request())
// originalResponse.newBuilder()
// .body(ProgressResponseBody(originalResponse.body!!, listener))
// .build()
// }
.cache(null)
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.body(ProgressResponseBody(originalResponse.body!!, listener))
.build()
}
.build()
return progressClient.newCall(request)
@@ -136,3 +141,5 @@ inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(responseBody)
}
}
class HttpException(val code: Int) : IllegalStateException("HTTP error $code")

View File

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

View File

@@ -3,6 +3,8 @@ package eu.kanade.tachiyomi.network
import okhttp3.CacheControl
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.RequestBody
import java.util.concurrent.TimeUnit.MINUTES
@@ -15,6 +17,17 @@ fun GET(
url: String,
headers: Headers = DEFAULT_HEADERS,
cache: CacheControl = DEFAULT_CACHE_CONTROL
): Request {
return GET(url.toHttpUrl(), headers, cache)
}
/**
* @since extensions-lib 1.4
*/
fun GET(
url: HttpUrl,
headers: Headers = DEFAULT_HEADERS,
cache: CacheControl = DEFAULT_CACHE_CONTROL
): Request {
return Request.Builder()
.url(url)
@@ -36,3 +49,31 @@ fun POST(
.cacheControl(cache)
.build()
}
fun PUT(
url: String,
headers: Headers = DEFAULT_HEADERS,
body: RequestBody = DEFAULT_BODY,
cache: CacheControl = DEFAULT_CACHE_CONTROL
): Request {
return Request.Builder()
.url(url)
.put(body)
.headers(headers)
.cacheControl(cache)
.build()
}
fun DELETE(
url: String,
headers: Headers = DEFAULT_HEADERS,
body: RequestBody = DEFAULT_BODY,
cache: CacheControl = DEFAULT_CACHE_CONTROL
): Request {
return Request.Builder()
.url(url)
.delete(body)
.headers(headers)
.cacheControl(cache)
.build()
}

View File

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

View File

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

View File

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

View File

@@ -26,6 +26,7 @@ import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
import mu.KotlinLogging
import org.apache.commons.compress.archivers.zip.ZipFile
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.select
@@ -45,7 +46,6 @@ import java.io.FileInputStream
import java.io.InputStream
import java.util.Locale
import java.util.concurrent.TimeUnit
import java.util.zip.ZipFile
class LocalSource : CatalogueSource {
companion object {
@@ -212,6 +212,12 @@ class LocalSource : CatalogueSource {
manga.status = obj["status"]?.jsonPrimitive?.intOrNull ?: manga.status
}
// update the cover
val cover = getCoverFile(File("${applicationDirs.localMangaRoot}/${manga.url}"))
if (cover != null && cover.exists()) {
manga.thumbnail_url = cover.absolutePath
}
return Observable.just(manga)
}
@@ -327,8 +333,9 @@ class LocalSource : CatalogueSource {
fun getFormat(chapter: SChapter): Format {
val chapFile = File(applicationDirs.localMangaRoot, chapter.url)
if (chapFile.exists())
if (chapFile.exists()) {
return getFormat(chapFile)
}
throw Exception("Chapter not found")
}
@@ -355,7 +362,7 @@ class LocalSource : CatalogueSource {
}
is Format.Zip -> {
ZipFile(format.file).use { zip ->
val entry = zip.entries().toList()
val entry = zip.entries.toList()
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }

View File

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

View File

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

View File

@@ -1,10 +1,9 @@
package eu.kanade.tachiyomi.source.local.loader
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import org.apache.commons.compress.archivers.zip.ZipFile
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
import java.io.File
import java.util.zip.ZipFile
class ZipPageLoader(file: File) : PageLoader {
/**
@@ -17,14 +16,13 @@ class ZipPageLoader(file: File) : PageLoader {
* comparator.
*/
override fun getPages(): List<ReaderPage> {
return zip.entries().toList()
return zip.entries.toList()
.filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
.mapIndexed { i, entry ->
val streamFn = { zip.getInputStream(entry) }
ReaderPage(i).apply {
stream = streamFn
status = Page.READY
}
}
}

View File

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

View File

@@ -20,6 +20,8 @@ interface SManga : Serializable {
var thumbnail_url: String?
var update_strategy: UpdateStrategy
var initialized: Boolean
fun copyFrom(other: SManga) {

View File

@@ -18,5 +18,7 @@ class SMangaImpl : SManga {
override var thumbnail_url: String? = null
override var update_strategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE
override var initialized: Boolean = false
}

View File

@@ -0,0 +1,6 @@
package eu.kanade.tachiyomi.source.model
enum class UpdateStrategy {
ALWAYS_UPDATE,
ONLY_FETCH_ONCE
}

View File

@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.source.online
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.CFClearance.getWebViewUserAgent
import eu.kanade.tachiyomi.network.newCallWithProgress
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList
@@ -356,6 +357,28 @@ abstract class HttpSource : CatalogueSource {
}
}
/**
* Returns the url of the provided manga
*
* @since extensions-lib 1.4
* @param manga the manga
* @return url of the manga
*/
open fun getMangaUrl(manga: SManga): String {
return mangaDetailsRequest(manga).url.toString()
}
/**
* Returns the url of the provided chapter
*
* @since extensions-lib 1.4
* @param chapter the chapter
* @return url of the chapter
*/
open fun getChapterUrl(chapter: SChapter): String {
return pageListRequest(chapter).url.toString()
}
/**
* Called before inserting a new chapter into database. Use it if you need to override chapter
* fields, like the title or the chapter number. Do not change anything to [manga].
@@ -363,8 +386,7 @@ abstract class HttpSource : CatalogueSource {
* @param chapter the chapter to be added.
* @param manga the manga of the chapter.
*/
open fun prepareNewChapter(chapter: SChapter, manga: SManga) {
}
open fun prepareNewChapter(chapter: SChapter, manga: SManga) {}
/**
* Returns the list of filters for the source.
@@ -372,6 +394,6 @@ abstract class HttpSource : CatalogueSource {
override fun getFilterList() = FilterList()
companion object {
const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63"
val DEFAULT_USER_AGENT by lazy { getWebViewUserAgent() }
}
}

View File

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

View File

@@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.util.lang
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
import org.jsoup.Jsoup
import org.jsoup.safety.Safelist
import kotlin.math.floor
/**
@@ -56,3 +58,10 @@ fun String.takeBytes(n: Int): String {
bytes.decodeToString(endIndex = n).replace("\uFFFD", "")
}
}
/**
* HTML-decode the string
*/
fun String.htmlDecode(): String {
return Jsoup.clean(this, Safelist.none()).toString()
}

View File

@@ -2,6 +2,8 @@ package eu.kanade.tachiyomi.util.storage
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
import org.apache.commons.compress.archivers.zip.ZipFile
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import java.io.Closeable
@@ -10,8 +12,6 @@ import java.io.InputStream
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
/**
* Wrapper over ZipFile to load files in epub format.
@@ -38,14 +38,14 @@ class EpubFile(file: File) : Closeable {
/**
* Returns an input stream for reading the contents of the specified zip file entry.
*/
fun getInputStream(entry: ZipEntry): InputStream {
fun getInputStream(entry: ZipArchiveEntry): InputStream {
return zip.getInputStream(entry)
}
/**
* Returns the zip file entry for the specified name, or null if not found.
*/
fun getEntry(name: String): ZipEntry? {
fun getEntry(name: String): ZipArchiveEntry? {
return zip.getEntry(name)
}
@@ -139,7 +139,7 @@ class EpubFile(file: File) : Closeable {
*/
private fun getPagesFromDocument(document: Document): List<String> {
val pages = document.select("manifest > item")
.filter { "application/xhtml+xml" == it.attr("media-type") }
.filter { element -> "application/xhtml+xml" == element.attr("media-type") }
.associateBy { it.attr("id") }
val spine = document.select("spine > itemref").map { it.attr("idref") }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,23 @@
/*
* 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/. */
package suwayomi.tachidesk.graphql
import io.javalin.apibuilder.ApiBuilder.get
import io.javalin.apibuilder.ApiBuilder.post
import io.javalin.apibuilder.ApiBuilder.ws
import suwayomi.tachidesk.graphql.controller.GraphQLController
object GraphQL {
fun defineEndpoints() {
post("graphql", GraphQLController::execute)
ws("graphql", GraphQLController::webSocket)
// graphql playground
get("graphql", GraphQLController::playground)
}
}

View File

@@ -0,0 +1,41 @@
/*
* 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/. */
package suwayomi.tachidesk.graphql.controller
import io.javalin.http.ContentType
import io.javalin.http.Context
import io.javalin.websocket.WsConfig
import suwayomi.tachidesk.graphql.server.TachideskGraphQLServer
import suwayomi.tachidesk.server.JavalinSetup.future
object GraphQLController {
private val server = TachideskGraphQLServer.create()
/** execute graphql query */
fun execute(ctx: Context) {
ctx.future(
future {
server.execute(ctx)
}
)
}
fun playground(ctx: Context) {
ctx.contentType(ContentType.TEXT_HTML)
ctx.result(javaClass.getResourceAsStream("/graphql-playground.html")!!)
}
fun webSocket(ws: WsConfig) {
ws.onMessage { ctx ->
server.handleSubscriptionMessage(ctx)
}
ws.onClose { ctx ->
server.handleSubscriptionDisconnect(ctx)
}
}
}

View File

@@ -0,0 +1,54 @@
/*
* 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/. */
package suwayomi.tachidesk.graphql.dataLoaders
import com.expediagroup.graphql.dataloader.KotlinDataLoader
import org.dataloader.DataLoader
import org.dataloader.DataLoaderFactory
import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger
import org.jetbrains.exposed.sql.addLogger
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.types.CategoryNodeList
import suwayomi.tachidesk.graphql.types.CategoryNodeList.Companion.toNodeList
import suwayomi.tachidesk.graphql.types.CategoryType
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
import suwayomi.tachidesk.manga.model.table.CategoryTable
import suwayomi.tachidesk.server.JavalinSetup.future
class CategoryDataLoader : KotlinDataLoader<Int, CategoryType> {
override val dataLoaderName = "CategoryDataLoader"
override fun getDataLoader(): DataLoader<Int, CategoryType> = DataLoaderFactory.newDataLoader { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val categories = CategoryTable.select { CategoryTable.id inList ids }
.map { CategoryType(it) }
.associateBy { it.id }
ids.map { categories[it] }
}
}
}
}
class CategoriesForMangaDataLoader : KotlinDataLoader<Int, CategoryNodeList> {
override val dataLoaderName = "CategoriesForMangaDataLoader"
override fun getDataLoader(): DataLoader<Int, CategoryNodeList> = DataLoaderFactory.newDataLoader<Int, CategoryNodeList> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val itemsByRef = CategoryMangaTable.innerJoin(CategoryTable)
.select { CategoryMangaTable.manga inList ids }
.map { Pair(it[CategoryMangaTable.manga].value, CategoryType(it)) }
.groupBy { it.first }
.mapValues { it.value.map { pair -> pair.second } }
ids.map { (itemsByRef[it] ?: emptyList()).toNodeList() }
}
}
}
}

View File

@@ -0,0 +1,51 @@
/*
* 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/. */
package suwayomi.tachidesk.graphql.dataLoaders
import com.expediagroup.graphql.dataloader.KotlinDataLoader
import org.dataloader.DataLoader
import org.dataloader.DataLoaderFactory
import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger
import org.jetbrains.exposed.sql.addLogger
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.types.ChapterNodeList
import suwayomi.tachidesk.graphql.types.ChapterNodeList.Companion.toNodeList
import suwayomi.tachidesk.graphql.types.ChapterType
import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.server.JavalinSetup.future
class ChapterDataLoader : KotlinDataLoader<Int, ChapterType?> {
override val dataLoaderName = "ChapterDataLoader"
override fun getDataLoader(): DataLoader<Int, ChapterType?> = DataLoaderFactory.newDataLoader<Int, ChapterType> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val chapters = ChapterTable.select { ChapterTable.id inList ids }
.map { ChapterType(it) }
.associateBy { it.id }
ids.map { chapters[it] }
}
}
}
}
class ChaptersForMangaDataLoader : KotlinDataLoader<Int, ChapterNodeList> {
override val dataLoaderName = "ChaptersForMangaDataLoader"
override fun getDataLoader(): DataLoader<Int, ChapterNodeList> = DataLoaderFactory.newDataLoader<Int, ChapterNodeList> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val chaptersByMangaId = ChapterTable.select { ChapterTable.manga inList ids }
.map { ChapterType(it) }
.groupBy { it.mangaId }
ids.map { (chaptersByMangaId[it] ?: emptyList()).toNodeList() }
}
}
}
}

View File

@@ -0,0 +1,63 @@
/*
* 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/. */
package suwayomi.tachidesk.graphql.dataLoaders
import com.expediagroup.graphql.dataloader.KotlinDataLoader
import org.dataloader.DataLoader
import org.dataloader.DataLoaderFactory
import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger
import org.jetbrains.exposed.sql.addLogger
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.types.ExtensionType
import suwayomi.tachidesk.manga.model.table.ExtensionTable
import suwayomi.tachidesk.manga.model.table.SourceTable
import suwayomi.tachidesk.server.JavalinSetup.future
class ExtensionDataLoader : KotlinDataLoader<String, ExtensionType?> {
override val dataLoaderName = "ExtensionDataLoader"
override fun getDataLoader(): DataLoader<String, ExtensionType?> = DataLoaderFactory.newDataLoader { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val extensions = ExtensionTable.select { ExtensionTable.pkgName inList ids }
.map { ExtensionType(it) }
.associateBy { it.pkgName }
ids.map { extensions[it] }
}
}
}
}
class ExtensionForSourceDataLoader : KotlinDataLoader<Long, ExtensionType?> {
override val dataLoaderName = "ExtensionForSourceDataLoader"
override fun getDataLoader(): DataLoader<Long, ExtensionType?> = DataLoaderFactory.newDataLoader { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val extensions = ExtensionTable.innerJoin(SourceTable)
.select { SourceTable.id inList ids }
.toList()
.map { Triple(it[SourceTable.id].value, it[ExtensionTable.pkgName], it) }
.let { triples ->
val sources = buildMap {
triples.forEach {
if (!containsKey(it.second)) {
put(it.second, ExtensionType(it.third))
}
}
}
triples.associate {
it.first to sources[it.second]
}
}
ids.map { extensions[it] }
}
}
}
}

View File

@@ -0,0 +1,67 @@
/*
* 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/. */
package suwayomi.tachidesk.graphql.dataLoaders
import com.expediagroup.graphql.dataloader.KotlinDataLoader
import org.dataloader.DataLoader
import org.dataloader.DataLoaderFactory
import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger
import org.jetbrains.exposed.sql.addLogger
import org.jetbrains.exposed.sql.andWhere
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.types.MangaNodeList
import suwayomi.tachidesk.graphql.types.MangaNodeList.Companion.toNodeList
import suwayomi.tachidesk.graphql.types.MangaType
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.server.JavalinSetup.future
class MangaDataLoader : KotlinDataLoader<Int, MangaType?> {
override val dataLoaderName = "MangaDataLoader"
override fun getDataLoader(): DataLoader<Int, MangaType?> = DataLoaderFactory.newDataLoader { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val manga = MangaTable.select { MangaTable.id inList ids }
.map { MangaType(it) }
.associateBy { it.id }
ids.map { manga[it] }
}
}
}
}
class MangaForCategoryDataLoader : KotlinDataLoader<Int, MangaNodeList> {
override val dataLoaderName = "MangaForCategoryDataLoader"
override fun getDataLoader(): DataLoader<Int, MangaNodeList> = DataLoaderFactory.newDataLoader<Int, MangaNodeList> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val itemsByRef = if (ids.contains(0)) {
MangaTable
.leftJoin(CategoryMangaTable)
.select { MangaTable.inLibrary eq true }
.andWhere { CategoryMangaTable.manga.isNull() }
.map { MangaType(it) }
.let {
mapOf(0 to it)
}
} else {
emptyMap()
} + CategoryMangaTable.innerJoin(MangaTable)
.select { CategoryMangaTable.category inList ids }
.map { Pair(it[CategoryMangaTable.category].value, MangaType(it)) }
.groupBy { it.first }
.mapValues { it.value.map { pair -> pair.second } }
ids.map { (itemsByRef[it] ?: emptyList()).toNodeList() }
}
}
}
}

View File

@@ -0,0 +1,78 @@
package suwayomi.tachidesk.graphql.dataLoaders
import com.expediagroup.graphql.dataloader.KotlinDataLoader
import org.dataloader.DataLoader
import org.dataloader.DataLoaderFactory
import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger
import org.jetbrains.exposed.sql.addLogger
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.global.model.table.GlobalMetaTable
import suwayomi.tachidesk.graphql.types.CategoryMetaType
import suwayomi.tachidesk.graphql.types.ChapterMetaType
import suwayomi.tachidesk.graphql.types.GlobalMetaType
import suwayomi.tachidesk.graphql.types.MangaMetaType
import suwayomi.tachidesk.manga.model.table.CategoryMetaTable
import suwayomi.tachidesk.manga.model.table.ChapterMetaTable
import suwayomi.tachidesk.manga.model.table.MangaMetaTable
import suwayomi.tachidesk.server.JavalinSetup.future
class GlobalMetaDataLoader : KotlinDataLoader<String, GlobalMetaType?> {
override val dataLoaderName = "GlobalMetaDataLoader"
override fun getDataLoader(): DataLoader<String, GlobalMetaType?> = DataLoaderFactory.newDataLoader<String, GlobalMetaType?> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val metasByRefId = GlobalMetaTable.select { GlobalMetaTable.key inList ids }
.map { GlobalMetaType(it) }
.associateBy { it.key }
ids.map { metasByRefId[it] }
}
}
}
}
class ChapterMetaDataLoader : KotlinDataLoader<Int, List<ChapterMetaType>> {
override val dataLoaderName = "ChapterMetaDataLoader"
override fun getDataLoader(): DataLoader<Int, List<ChapterMetaType>> = DataLoaderFactory.newDataLoader<Int, List<ChapterMetaType>> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val metasByRefId = ChapterMetaTable.select { ChapterMetaTable.ref inList ids }
.map { ChapterMetaType(it) }
.groupBy { it.chapterId }
ids.map { metasByRefId[it].orEmpty() }
}
}
}
}
class MangaMetaDataLoader : KotlinDataLoader<Int, List<MangaMetaType>> {
override val dataLoaderName = "MangaMetaDataLoader"
override fun getDataLoader(): DataLoader<Int, List<MangaMetaType>> = DataLoaderFactory.newDataLoader<Int, List<MangaMetaType>> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val metasByRefId = MangaMetaTable.select { MangaMetaTable.ref inList ids }
.map { MangaMetaType(it) }
.groupBy { it.mangaId }
ids.map { metasByRefId[it].orEmpty() }
}
}
}
}
class CategoryMetaDataLoader : KotlinDataLoader<Int, List<CategoryMetaType>> {
override val dataLoaderName = "CategoryMetaDataLoader"
override fun getDataLoader(): DataLoader<Int, List<CategoryMetaType>> = DataLoaderFactory.newDataLoader<Int, List<CategoryMetaType>> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val metasByRefId = CategoryMetaTable.select { CategoryMetaTable.ref inList ids }
.map { CategoryMetaType(it) }
.groupBy { it.categoryId }
ids.map { metasByRefId[it].orEmpty() }
}
}
}
}

View File

@@ -0,0 +1,56 @@
/*
* 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/. */
package suwayomi.tachidesk.graphql.dataLoaders
import com.expediagroup.graphql.dataloader.KotlinDataLoader
import org.dataloader.DataLoader
import org.dataloader.DataLoaderFactory
import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger
import org.jetbrains.exposed.sql.addLogger
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.types.SourceNodeList
import suwayomi.tachidesk.graphql.types.SourceNodeList.Companion.toNodeList
import suwayomi.tachidesk.graphql.types.SourceType
import suwayomi.tachidesk.manga.model.table.ExtensionTable
import suwayomi.tachidesk.manga.model.table.SourceTable
import suwayomi.tachidesk.server.JavalinSetup.future
class SourceDataLoader : KotlinDataLoader<Long, SourceType?> {
override val dataLoaderName = "SourceDataLoader"
override fun getDataLoader(): DataLoader<Long, SourceType?> = DataLoaderFactory.newDataLoader { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val source = SourceTable.select { SourceTable.id inList ids }
.mapNotNull { SourceType(it) }
.associateBy { it.id }
ids.map { source[it] }
}
}
}
}
class SourcesForExtensionDataLoader : KotlinDataLoader<String, SourceNodeList> {
override val dataLoaderName = "SourcesForExtensionDataLoader"
override fun getDataLoader(): DataLoader<String, SourceNodeList> = DataLoaderFactory.newDataLoader { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val sourcesByExtensionPkg = SourceTable.innerJoin(ExtensionTable)
.select { ExtensionTable.pkgName inList ids }
.map { Pair(it[ExtensionTable.pkgName], SourceType(it)) }
.groupBy { it.first }
.mapValues { it.value.mapNotNull { pair -> pair.second } }
ids.map { (sourcesByExtensionPkg[it] ?: emptyList()).toNodeList() }
}
}
}
}

View File

@@ -0,0 +1,400 @@
package suwayomi.tachidesk.graphql.mutations
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
import org.jetbrains.exposed.sql.SqlExpressionBuilder.minus
import org.jetbrains.exposed.sql.SqlExpressionBuilder.plus
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.batchInsert
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.graphql.types.CategoryMetaType
import suwayomi.tachidesk.graphql.types.CategoryType
import suwayomi.tachidesk.graphql.types.MangaType
import suwayomi.tachidesk.manga.impl.Category
import suwayomi.tachidesk.manga.impl.util.lang.isEmpty
import suwayomi.tachidesk.manga.impl.util.lang.isNotEmpty
import suwayomi.tachidesk.manga.model.dataclass.IncludeInUpdate
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
import suwayomi.tachidesk.manga.model.table.CategoryMetaTable
import suwayomi.tachidesk.manga.model.table.CategoryTable
import suwayomi.tachidesk.manga.model.table.MangaTable
class CategoryMutation {
data class SetCategoryMetaInput(
val clientMutationId: String? = null,
val meta: CategoryMetaType
)
data class SetCategoryMetaPayload(
val clientMutationId: String?,
val meta: CategoryMetaType
)
fun setCategoryMeta(
input: SetCategoryMetaInput
): SetCategoryMetaPayload {
val (clientMutationId, meta) = input
Category.modifyMeta(meta.categoryId, meta.key, meta.value)
return SetCategoryMetaPayload(clientMutationId, meta)
}
data class DeleteCategoryMetaInput(
val clientMutationId: String? = null,
val categoryId: Int,
val key: String
)
data class DeleteCategoryMetaPayload(
val clientMutationId: String?,
val meta: CategoryMetaType?,
val category: CategoryType
)
fun deleteCategoryMeta(
input: DeleteCategoryMetaInput
): DeleteCategoryMetaPayload {
val (clientMutationId, categoryId, key) = input
val (meta, category) = transaction {
val meta = CategoryMetaTable.select { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }
.firstOrNull()
CategoryMetaTable.deleteWhere { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }
val category = transaction {
CategoryType(CategoryTable.select { CategoryTable.id eq categoryId }.first())
}
if (meta != null) {
CategoryMetaType(meta)
} else {
null
} to category
}
return DeleteCategoryMetaPayload(clientMutationId, meta, category)
}
data class UpdateCategoryPatch(
val name: String? = null,
val default: Boolean? = null,
val includeInUpdate: IncludeInUpdate? = null
)
data class UpdateCategoryPayload(
val clientMutationId: String?,
val category: CategoryType
)
data class UpdateCategoryInput(
val clientMutationId: String? = null,
val id: Int,
val patch: UpdateCategoryPatch
)
data class UpdateCategoriesPayload(
val clientMutationId: String?,
val categories: List<CategoryType>
)
data class UpdateCategoriesInput(
val clientMutationId: String? = null,
val ids: List<Int>,
val patch: UpdateCategoryPatch
)
private fun updateCategories(ids: List<Int>, patch: UpdateCategoryPatch) {
transaction {
if (patch.name != null) {
CategoryTable.update({ CategoryTable.id inList ids }) { update ->
patch.name.also {
update[name] = it
}
}
}
if (patch.default != null) {
CategoryTable.update({ CategoryTable.id inList ids }) { update ->
patch.default.also {
update[isDefault] = it
}
}
}
if (patch.includeInUpdate != null) {
CategoryTable.update({ CategoryTable.id inList ids }) { update ->
patch.includeInUpdate.also {
update[includeInUpdate] = it.value
}
}
}
}
}
fun updateCategory(input: UpdateCategoryInput): UpdateCategoryPayload {
val (clientMutationId, id, patch) = input
updateCategories(listOf(id), patch)
val category = transaction {
CategoryType(CategoryTable.select { CategoryTable.id eq id }.first())
}
return UpdateCategoryPayload(
clientMutationId = clientMutationId,
category = category
)
}
fun updateCategories(input: UpdateCategoriesInput): UpdateCategoriesPayload {
val (clientMutationId, ids, patch) = input
updateCategories(ids, patch)
val categories = transaction {
CategoryTable.select { CategoryTable.id inList ids }.map { CategoryType(it) }
}
return UpdateCategoriesPayload(
clientMutationId = clientMutationId,
categories = categories
)
}
data class UpdateCategoryOrderPayload(
val clientMutationId: String?,
val categories: List<CategoryType>
)
data class UpdateCategoryOrderInput(
val clientMutationId: String? = null,
val id: Int,
val position: Int
)
fun updateCategoryOrder(input: UpdateCategoryOrderInput): UpdateCategoryOrderPayload {
val (clientMutationId, categoryId, position) = input
require(position > 0) {
"'order' must not be <= 0"
}
transaction {
val currentOrder = CategoryTable
.select { CategoryTable.id eq categoryId }
.first()[CategoryTable.order]
if (currentOrder != position) {
if (position < currentOrder) {
CategoryTable.update({ CategoryTable.order greaterEq position }) {
it[CategoryTable.order] = CategoryTable.order + 1
}
} else {
CategoryTable.update({ CategoryTable.order lessEq position }) {
it[CategoryTable.order] = CategoryTable.order - 1
}
}
CategoryTable.update({ CategoryTable.id eq categoryId }) {
it[CategoryTable.order] = position
}
}
}
Category.normalizeCategories()
val categories = transaction {
CategoryTable.selectAll().orderBy(CategoryTable.order).map { CategoryType(it) }
}
return UpdateCategoryOrderPayload(
clientMutationId = clientMutationId,
categories = categories
)
}
data class CreateCategoryInput(
val clientMutationId: String? = null,
val name: String,
val order: Int? = null,
val default: Boolean? = null,
val includeInUpdate: IncludeInUpdate? = null
)
data class CreateCategoryPayload(
val clientMutationId: String?,
val category: CategoryType
)
fun createCategory(
input: CreateCategoryInput
): CreateCategoryPayload {
val (clientMutationId, name, order, default, includeInUpdate) = input
transaction {
require(CategoryTable.select { CategoryTable.name eq input.name }.isEmpty()) {
"'name' must be unique"
}
}
require(!name.equals(Category.DEFAULT_CATEGORY_NAME, ignoreCase = true)) {
"'name' must not be ${Category.DEFAULT_CATEGORY_NAME}"
}
if (order != null) {
require(order > 0) {
"'order' must not be <= 0"
}
}
val category = transaction {
if (order != null) {
CategoryTable.update({ CategoryTable.order greaterEq order }) {
it[CategoryTable.order] = CategoryTable.order + 1
}
}
val id = CategoryTable.insertAndGetId {
it[CategoryTable.name] = input.name
it[CategoryTable.order] = order ?: Int.MAX_VALUE
if (default != null) {
it[CategoryTable.isDefault] = default
}
if (includeInUpdate != null) {
it[CategoryTable.includeInUpdate] = includeInUpdate.value
}
}
Category.normalizeCategories()
CategoryType(CategoryTable.select { CategoryTable.id eq id }.first())
}
return CreateCategoryPayload(clientMutationId, category)
}
data class DeleteCategoryInput(
val clientMutationId: String? = null,
val categoryId: Int
)
data class DeleteCategoryPayload(
val clientMutationId: String?,
val category: CategoryType?,
val mangas: List<MangaType>
)
fun deleteCategory(
input: DeleteCategoryInput
): DeleteCategoryPayload {
val (clientMutationId, categoryId) = input
if (categoryId == 0) { // Don't delete default category
return DeleteCategoryPayload(
clientMutationId,
null,
emptyList()
)
}
val (category, mangas) = transaction {
val category = CategoryTable.select { CategoryTable.id eq categoryId }
.firstOrNull()
val mangas = transaction {
MangaTable.innerJoin(CategoryMangaTable)
.select { CategoryMangaTable.category eq categoryId }
.map { MangaType(it) }
}
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
Category.normalizeCategories()
if (category != null) {
CategoryType(category)
} else {
null
} to mangas
}
return DeleteCategoryPayload(clientMutationId, category, mangas)
}
data class UpdateMangaCategoriesPatch(
val clearCategories: Boolean? = null,
val addToCategories: List<Int>? = null,
val removeFromCategories: List<Int>? = null
)
data class UpdateMangaCategoriesPayload(
val clientMutationId: String?,
val manga: MangaType
)
data class UpdateMangaCategoriesInput(
val clientMutationId: String? = null,
val id: Int,
val patch: UpdateMangaCategoriesPatch
)
data class UpdateMangasCategoriesPayload(
val clientMutationId: String?,
val mangas: List<MangaType>
)
data class UpdateMangasCategoriesInput(
val clientMutationId: String? = null,
val ids: List<Int>,
val patch: UpdateMangaCategoriesPatch
)
private fun updateMangas(ids: List<Int>, patch: UpdateMangaCategoriesPatch) {
transaction {
if (patch.clearCategories == true) {
CategoryMangaTable.deleteWhere { CategoryMangaTable.manga inList ids }
} else if (!patch.removeFromCategories.isNullOrEmpty()) {
CategoryMangaTable.deleteWhere {
(CategoryMangaTable.manga inList ids) and (CategoryMangaTable.category inList patch.removeFromCategories)
}
}
if (!patch.addToCategories.isNullOrEmpty()) {
val newCategories = buildList {
ids.forEach { mangaId ->
patch.addToCategories.forEach { categoryId ->
val existingMapping = CategoryMangaTable.select {
(CategoryMangaTable.manga eq mangaId) and (CategoryMangaTable.category eq categoryId)
}.isNotEmpty()
if (!existingMapping) {
add(mangaId to categoryId)
}
}
}
}
CategoryMangaTable.batchInsert(newCategories) { (manga, category) ->
this[CategoryMangaTable.manga] = manga
this[CategoryMangaTable.category] = category
}
}
}
}
fun updateMangaCategories(input: UpdateMangaCategoriesInput): UpdateMangaCategoriesPayload {
val (clientMutationId, id, patch) = input
updateMangas(listOf(id), patch)
val manga = transaction {
MangaType(MangaTable.select { MangaTable.id eq id }.first())
}
return UpdateMangaCategoriesPayload(
clientMutationId = clientMutationId,
manga = manga
)
}
fun updateMangasCategories(input: UpdateMangasCategoriesInput): UpdateMangasCategoriesPayload {
val (clientMutationId, ids, patch) = input
updateMangas(ids, patch)
val mangas = transaction {
MangaTable.select { MangaTable.id inList ids }.map { MangaType(it) }
}
return UpdateMangasCategoriesPayload(
clientMutationId = clientMutationId,
mangas = mangas
)
}
}

View File

@@ -0,0 +1,186 @@
package suwayomi.tachidesk.graphql.mutations
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.graphql.types.ChapterMetaType
import suwayomi.tachidesk.graphql.types.ChapterType
import suwayomi.tachidesk.manga.impl.Chapter
import suwayomi.tachidesk.manga.model.table.ChapterMetaTable
import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.server.JavalinSetup.future
import java.time.Instant
import java.util.concurrent.CompletableFuture
/**
* TODO Mutations
* - Download
* - Delete download
*/
class ChapterMutation {
data class UpdateChapterPatch(
val isBookmarked: Boolean? = null,
val isRead: Boolean? = null,
val lastPageRead: Int? = null
)
data class UpdateChapterPayload(
val clientMutationId: String?,
val chapter: ChapterType
)
data class UpdateChapterInput(
val clientMutationId: String? = null,
val id: Int,
val patch: UpdateChapterPatch
)
data class UpdateChaptersPayload(
val clientMutationId: String?,
val chapters: List<ChapterType>
)
data class UpdateChaptersInput(
val clientMutationId: String? = null,
val ids: List<Int>,
val patch: UpdateChapterPatch
)
private fun updateChapters(ids: List<Int>, patch: UpdateChapterPatch) {
transaction {
if (patch.isRead != null || patch.isBookmarked != null || patch.lastPageRead != null) {
val now = Instant.now().epochSecond
ChapterTable.update({ ChapterTable.id inList ids }) { update ->
patch.isRead?.also {
update[isRead] = it
}
patch.isBookmarked?.also {
update[isBookmarked] = it
}
patch.lastPageRead?.also {
update[lastPageRead] = it
update[lastReadAt] = now
}
}
}
}
}
fun updateChapter(
input: UpdateChapterInput
): UpdateChapterPayload {
val (clientMutationId, id, patch) = input
updateChapters(listOf(id), patch)
val chapter = transaction {
ChapterType(ChapterTable.select { ChapterTable.id eq id }.first())
}
return UpdateChapterPayload(
clientMutationId = clientMutationId,
chapter = chapter
)
}
fun updateChapters(
input: UpdateChaptersInput
): UpdateChaptersPayload {
val (clientMutationId, ids, patch) = input
updateChapters(ids, patch)
val chapters = transaction {
ChapterTable.select { ChapterTable.id inList ids }.map { ChapterType(it) }
}
return UpdateChaptersPayload(
clientMutationId = clientMutationId,
chapters = chapters
)
}
data class FetchChaptersInput(
val clientMutationId: String? = null,
val mangaId: Int
)
data class FetchChaptersPayload(
val clientMutationId: String?,
val chapters: List<ChapterType>
)
fun fetchChapters(
input: FetchChaptersInput
): CompletableFuture<FetchChaptersPayload> {
val (clientMutationId, mangaId) = input
return future {
Chapter.fetchChapterList(mangaId)
}.thenApply {
val chapters = transaction {
ChapterTable.select { ChapterTable.manga eq mangaId }
.orderBy(ChapterTable.sourceOrder)
.map { ChapterType(it) }
}
FetchChaptersPayload(
clientMutationId = clientMutationId,
chapters = chapters
)
}
}
data class SetChapterMetaInput(
val clientMutationId: String? = null,
val meta: ChapterMetaType
)
data class SetChapterMetaPayload(
val clientMutationId: String?,
val meta: ChapterMetaType
)
fun setChapterMeta(
input: SetChapterMetaInput
): SetChapterMetaPayload {
val (clientMutationId, meta) = input
Chapter.modifyChapterMeta(meta.chapterId, meta.key, meta.value)
return SetChapterMetaPayload(clientMutationId, meta)
}
data class DeleteChapterMetaInput(
val clientMutationId: String? = null,
val chapterId: Int,
val key: String
)
data class DeleteChapterMetaPayload(
val clientMutationId: String?,
val meta: ChapterMetaType?,
val chapter: ChapterType
)
fun deleteChapterMeta(
input: DeleteChapterMetaInput
): DeleteChapterMetaPayload {
val (clientMutationId, chapterId, key) = input
val (meta, chapter) = transaction {
val meta = ChapterMetaTable.select { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }
.firstOrNull()
ChapterMetaTable.deleteWhere { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }
val chapter = transaction {
ChapterType(ChapterTable.select { ChapterTable.id eq chapterId }.first())
}
if (meta != null) {
ChapterMetaType(meta)
} else {
null
} to chapter
}
return DeleteChapterMetaPayload(clientMutationId, meta, chapter)
}
}

View File

@@ -0,0 +1,127 @@
package suwayomi.tachidesk.graphql.mutations
import eu.kanade.tachiyomi.source.local.LocalSource
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.types.ExtensionType
import suwayomi.tachidesk.manga.impl.extension.Extension
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList
import suwayomi.tachidesk.manga.model.table.ExtensionTable
import suwayomi.tachidesk.server.JavalinSetup.future
import java.util.concurrent.CompletableFuture
class ExtensionMutation {
data class UpdateExtensionPatch(
val install: Boolean? = null,
val update: Boolean? = null,
val uninstall: Boolean? = null
)
data class UpdateExtensionPayload(
val clientMutationId: String?,
val extension: ExtensionType
)
data class UpdateExtensionInput(
val clientMutationId: String? = null,
val id: String,
val patch: UpdateExtensionPatch
)
data class UpdateExtensionsPayload(
val clientMutationId: String?,
val extensions: List<ExtensionType>
)
data class UpdateExtensionsInput(
val clientMutationId: String? = null,
val ids: List<String>,
val patch: UpdateExtensionPatch
)
private suspend fun updateExtensions(ids: List<String>, patch: UpdateExtensionPatch) {
val extensions = transaction {
ExtensionTable.select { ExtensionTable.pkgName inList ids }
.map { ExtensionType(it) }
}
if (patch.update == true) {
extensions.filter { it.hasUpdate }.forEach {
Extension.updateExtension(it.pkgName)
}
}
if (patch.install == true) {
extensions.filterNot { it.isInstalled }.forEach {
Extension.installExtension(it.pkgName)
}
}
if (patch.uninstall == true) {
extensions.filter { it.isInstalled }.forEach {
Extension.uninstallExtension(it.pkgName)
}
}
}
fun updateExtension(input: UpdateExtensionInput): CompletableFuture<UpdateExtensionPayload> {
val (clientMutationId, id, patch) = input
return future {
updateExtensions(listOf(id), patch)
}.thenApply {
val extension = transaction {
ExtensionType(ExtensionTable.select { ExtensionTable.pkgName eq id }.first())
}
UpdateExtensionPayload(
clientMutationId = clientMutationId,
extension = extension
)
}
}
fun updateExtensions(input: UpdateExtensionsInput): CompletableFuture<UpdateExtensionsPayload> {
val (clientMutationId, ids, patch) = input
return future {
updateExtensions(ids, patch)
}.thenApply {
val extensions = transaction {
ExtensionTable.select { ExtensionTable.pkgName inList ids }
.map { ExtensionType(it) }
}
UpdateExtensionsPayload(
clientMutationId = clientMutationId,
extensions = extensions
)
}
}
data class FetchExtensionsInput(
val clientMutationId: String? = null
)
data class FetchExtensionsPayload(
val clientMutationId: String?,
val extensions: List<ExtensionType>
)
fun fetchExtensions(
input: FetchExtensionsInput
): CompletableFuture<FetchExtensionsPayload> {
val (clientMutationId) = input
return future {
ExtensionsList.fetchExtensions()
}.thenApply {
val extensions = transaction {
ExtensionTable.select { ExtensionTable.name neq LocalSource.EXTENSION_NAME }
.map { ExtensionType(it) }
}
FetchExtensionsPayload(
clientMutationId = clientMutationId,
extensions = extensions
)
}
}
}

View File

@@ -0,0 +1,168 @@
package suwayomi.tachidesk.graphql.mutations
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.graphql.types.MangaMetaType
import suwayomi.tachidesk.graphql.types.MangaType
import suwayomi.tachidesk.manga.impl.Manga
import suwayomi.tachidesk.manga.model.table.MangaMetaTable
import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.server.JavalinSetup.future
import java.util.concurrent.CompletableFuture
/**
* TODO Mutations
* - Download x(all = -1) chapters
* - Delete read/all downloaded chapters
*/
class MangaMutation {
data class UpdateMangaPatch(
val inLibrary: Boolean? = null
)
data class UpdateMangaPayload(
val clientMutationId: String?,
val manga: MangaType
)
data class UpdateMangaInput(
val clientMutationId: String? = null,
val id: Int,
val patch: UpdateMangaPatch
)
data class UpdateMangasPayload(
val clientMutationId: String?,
val mangas: List<MangaType>
)
data class UpdateMangasInput(
val clientMutationId: String? = null,
val ids: List<Int>,
val patch: UpdateMangaPatch
)
private fun updateMangas(ids: List<Int>, patch: UpdateMangaPatch) {
transaction {
if (patch.inLibrary != null) {
MangaTable.update({ MangaTable.id inList ids }) { update ->
patch.inLibrary.also {
update[inLibrary] = it
}
}
}
}
}
fun updateManga(input: UpdateMangaInput): UpdateMangaPayload {
val (clientMutationId, id, patch) = input
updateMangas(listOf(id), patch)
val manga = transaction {
MangaType(MangaTable.select { MangaTable.id eq id }.first())
}
return UpdateMangaPayload(
clientMutationId = clientMutationId,
manga = manga
)
}
fun updateMangas(input: UpdateMangasInput): UpdateMangasPayload {
val (clientMutationId, ids, patch) = input
updateMangas(ids, patch)
val mangas = transaction {
MangaTable.select { MangaTable.id inList ids }.map { MangaType(it) }
}
return UpdateMangasPayload(
clientMutationId = clientMutationId,
mangas = mangas
)
}
data class FetchMangaInput(
val clientMutationId: String? = null,
val id: Int
)
data class FetchMangaPayload(
val clientMutationId: String?,
val manga: MangaType
)
fun fetchManga(
input: FetchMangaInput
): CompletableFuture<FetchMangaPayload> {
val (clientMutationId, id) = input
return future {
Manga.fetchManga(id)
}.thenApply {
val manga = transaction {
MangaTable.select { MangaTable.id eq id }.first()
}
FetchMangaPayload(
clientMutationId = clientMutationId,
manga = MangaType(manga)
)
}
}
data class SetMangaMetaInput(
val clientMutationId: String? = null,
val meta: MangaMetaType
)
data class SetMangaMetaPayload(
val clientMutationId: String?,
val meta: MangaMetaType
)
fun setMangaMeta(
input: SetMangaMetaInput
): SetMangaMetaPayload {
val (clientMutationId, meta) = input
Manga.modifyMangaMeta(meta.mangaId, meta.key, meta.value)
return SetMangaMetaPayload(clientMutationId, meta)
}
data class DeleteMangaMetaInput(
val clientMutationId: String? = null,
val mangaId: Int,
val key: String
)
data class DeleteMangaMetaPayload(
val clientMutationId: String?,
val meta: MangaMetaType?,
val manga: MangaType
)
fun deleteMangaMeta(
input: DeleteMangaMetaInput
): DeleteMangaMetaPayload {
val (clientMutationId, mangaId, key) = input
val (meta, manga) = transaction {
val meta = MangaMetaTable.select { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }
.firstOrNull()
MangaMetaTable.deleteWhere { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }
val manga = transaction {
MangaType(MangaTable.select { MangaTable.id eq mangaId }.first())
}
if (meta != null) {
MangaMetaType(meta)
} else {
null
} to manga
}
return DeleteMangaMetaPayload(clientMutationId, meta, manga)
}
}

View File

@@ -0,0 +1,59 @@
package suwayomi.tachidesk.graphql.mutations
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.global.impl.GlobalMeta
import suwayomi.tachidesk.global.model.table.GlobalMetaTable
import suwayomi.tachidesk.graphql.types.GlobalMetaType
class MetaMutation {
data class SetGlobalMetaInput(
val clientMutationId: String? = null,
val meta: GlobalMetaType
)
data class SetGlobalMetaPayload(
val clientMutationId: String?,
val meta: GlobalMetaType
)
fun setGlobalMeta(
input: SetGlobalMetaInput
): SetGlobalMetaPayload {
val (clientMutationId, meta) = input
GlobalMeta.modifyMeta(meta.key, meta.value)
return SetGlobalMetaPayload(clientMutationId, meta)
}
data class DeleteGlobalMetaInput(
val clientMutationId: String? = null,
val key: String
)
data class DeleteGlobalMetaPayload(
val clientMutationId: String?,
val meta: GlobalMetaType?
)
fun deleteGlobalMeta(
input: DeleteGlobalMetaInput
): DeleteGlobalMetaPayload {
val (clientMutationId, key) = input
val meta = transaction {
val meta = GlobalMetaTable.select { GlobalMetaTable.key eq key }
.firstOrNull()
GlobalMetaTable.deleteWhere { GlobalMetaTable.key eq key }
if (meta != null) {
GlobalMetaType(meta)
} else {
null
}
}
return DeleteGlobalMetaPayload(clientMutationId, meta)
}
}

View File

@@ -0,0 +1,112 @@
package suwayomi.tachidesk.graphql.mutations
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.types.MangaType
import suwayomi.tachidesk.graphql.types.PreferenceObject
import suwayomi.tachidesk.manga.impl.MangaList.insertOrGet
import suwayomi.tachidesk.manga.impl.Search
import suwayomi.tachidesk.manga.impl.Source
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.server.JavalinSetup.future
import java.util.concurrent.CompletableFuture
class SourceMutation {
enum class FetchSourceMangaType {
SEARCH,
POPULAR,
LATEST
}
data class FilterChange(
val position: Int,
val state: String
)
data class FetchSourceMangaInput(
val clientMutationId: String? = null,
val source: Long,
val type: FetchSourceMangaType,
val page: Int,
val query: String? = null,
val filters: List<FilterChange>? = null
)
data class FetchSourceMangaPayload(
val clientMutationId: String?,
val mangas: List<MangaType>,
val hasNextPage: Boolean
)
fun fetchSourceManga(
input: FetchSourceMangaInput
): CompletableFuture<FetchSourceMangaPayload> {
val (clientMutationId, sourceId, type, page, query, filters) = input
return future {
val source = GetCatalogueSource.getCatalogueSourceOrNull(sourceId)!!
val mangasPage = when (type) {
FetchSourceMangaType.SEARCH -> {
source.fetchSearchManga(
page = page,
query = query.orEmpty(),
filters = Search.buildFilterList(
sourceId = sourceId,
changes = filters?.map { Search.FilterChange(it.position, it.state) }
.orEmpty()
)
).awaitSingle()
}
FetchSourceMangaType.POPULAR -> {
source.fetchPopularManga(page).awaitSingle()
}
FetchSourceMangaType.LATEST -> {
if (!source.supportsLatest) throw Exception("Source does not support latest")
source.fetchLatestUpdates(page).awaitSingle()
}
}
val mangaIds = mangasPage.insertOrGet(sourceId)
val mangas = transaction {
MangaTable.select { MangaTable.id inList mangaIds }
.map { MangaType(it) }
}.sortedBy {
mangaIds.indexOf(it.id)
}
FetchSourceMangaPayload(
clientMutationId = clientMutationId,
mangas = mangas,
hasNextPage = mangasPage.hasNextPage
)
}
}
data class SourcePreferenceChange(
val position: Int,
val state: String
)
data class UpdateSourcePreferenceInput(
val clientMutationId: String? = null,
val source: Long,
val change: SourcePreferenceChange
)
data class UpdateSourcePreferencePayload(
val clientMutationId: String?,
val preferences: List<PreferenceObject>
)
fun updateSourcePreference(
input: UpdateSourcePreferenceInput
): UpdateSourcePreferencePayload {
val (clientMutationId, sourceId, change) = input
Source.setSourcePreference(sourceId, Source.SourcePreferenceChange(change.position, change.state))
return UpdateSourcePreferencePayload(
clientMutationId = clientMutationId,
preferences = Source.getSourcePreferences(sourceId).map { PreferenceObject(it.type, it.props) }
)
}
}

View File

@@ -0,0 +1,200 @@
/*
* 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/. */
package suwayomi.tachidesk.graphql.queries
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
import graphql.schema.DataFetchingEnvironment
import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.Op
import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater
import org.jetbrains.exposed.sql.SqlExpressionBuilder.less
import org.jetbrains.exposed.sql.andWhere
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
import suwayomi.tachidesk.graphql.queries.filter.Filter
import suwayomi.tachidesk.graphql.queries.filter.HasGetOp
import suwayomi.tachidesk.graphql.queries.filter.IntFilter
import suwayomi.tachidesk.graphql.queries.filter.OpAnd
import suwayomi.tachidesk.graphql.queries.filter.StringFilter
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
import suwayomi.tachidesk.graphql.queries.filter.applyOps
import suwayomi.tachidesk.graphql.server.primitives.Cursor
import suwayomi.tachidesk.graphql.server.primitives.OrderBy
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
import suwayomi.tachidesk.graphql.server.primitives.QueryResults
import suwayomi.tachidesk.graphql.server.primitives.greaterNotUnique
import suwayomi.tachidesk.graphql.server.primitives.lessNotUnique
import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
import suwayomi.tachidesk.graphql.types.CategoryNodeList
import suwayomi.tachidesk.graphql.types.CategoryType
import suwayomi.tachidesk.manga.model.table.CategoryTable
import java.util.concurrent.CompletableFuture
class CategoryQuery {
fun category(dataFetchingEnvironment: DataFetchingEnvironment, id: Int): CompletableFuture<CategoryType?> {
return dataFetchingEnvironment.getValueFromDataLoader("CategoryDataLoader", id)
}
enum class CategoryOrderBy(override val column: Column<out Comparable<*>>) : OrderBy<CategoryType> {
ID(CategoryTable.id),
NAME(CategoryTable.name),
ORDER(CategoryTable.order);
override fun greater(cursor: Cursor): Op<Boolean> {
return when (this) {
ID -> CategoryTable.id greater cursor.value.toInt()
NAME -> greaterNotUnique(CategoryTable.name, CategoryTable.id, cursor, String::toString)
ORDER -> greaterNotUnique(CategoryTable.order, CategoryTable.id, cursor, String::toInt)
}
}
override fun less(cursor: Cursor): Op<Boolean> {
return when (this) {
ID -> CategoryTable.id less cursor.value.toInt()
NAME -> lessNotUnique(CategoryTable.name, CategoryTable.id, cursor, String::toString)
ORDER -> lessNotUnique(CategoryTable.order, CategoryTable.id, cursor, String::toInt)
}
}
override fun asCursor(type: CategoryType): Cursor {
val value = when (this) {
ID -> type.id.toString()
NAME -> type.id.toString() + "-" + type.name
ORDER -> type.id.toString() + "-" + type.order
}
return Cursor(value)
}
}
data class CategoryCondition(
val id: Int? = null,
val order: Int? = null,
val name: String? = null,
val default: Boolean? = null
) : HasGetOp {
override fun getOp(): Op<Boolean>? {
val opAnd = OpAnd()
opAnd.eq(id, CategoryTable.id)
opAnd.eq(order, CategoryTable.order)
opAnd.eq(name, CategoryTable.name)
opAnd.eq(default, CategoryTable.isDefault)
return opAnd.op
}
}
data class CategoryFilter(
val id: IntFilter? = null,
val order: IntFilter? = null,
val name: StringFilter? = null,
val default: BooleanFilter? = null,
override val and: List<CategoryFilter>? = null,
override val or: List<CategoryFilter>? = null,
override val not: CategoryFilter? = null
) : Filter<CategoryFilter> {
override fun getOpList(): List<Op<Boolean>> {
return listOfNotNull(
andFilterWithCompareEntity(CategoryTable.id, id),
andFilterWithCompare(CategoryTable.order, order),
andFilterWithCompareString(CategoryTable.name, name),
andFilterWithCompare(CategoryTable.isDefault, default)
)
}
}
fun categories(
condition: CategoryCondition? = null,
filter: CategoryFilter? = null,
orderBy: CategoryOrderBy? = null,
orderByType: SortOrder? = null,
before: Cursor? = null,
after: Cursor? = null,
first: Int? = null,
last: Int? = null,
offset: Int? = null
): CategoryNodeList {
val queryResults = transaction {
val res = CategoryTable.selectAll()
res.applyOps(condition, filter)
if (orderBy != null || (last != null || before != null)) {
val orderByColumn = orderBy?.column ?: CategoryTable.id
val orderType = orderByType.maybeSwap(last ?: before)
if (orderBy == CategoryOrderBy.ID || orderBy == null) {
res.orderBy(orderByColumn to orderType)
} else {
res.orderBy(
orderByColumn to orderType,
CategoryTable.id to SortOrder.ASC
)
}
}
val total = res.count()
val firstResult = res.firstOrNull()?.get(CategoryTable.id)?.value
val lastResult = res.lastOrNull()?.get(CategoryTable.id)?.value
if (after != null) {
res.andWhere {
(orderBy ?: CategoryOrderBy.ID).greater(after)
}
} else if (before != null) {
res.andWhere {
(orderBy ?: CategoryOrderBy.ID).less(before)
}
}
if (first != null) {
res.limit(first, offset?.toLong() ?: 0)
} else if (last != null) {
res.limit(last)
}
QueryResults(total, firstResult, lastResult, res.toList())
}
val getAsCursor: (CategoryType) -> Cursor = (orderBy ?: CategoryOrderBy.ID)::asCursor
val resultsAsType = queryResults.results.map { CategoryType(it) }
return CategoryNodeList(
resultsAsType,
if (resultsAsType.isEmpty()) {
emptyList()
} else {
listOfNotNull(
resultsAsType.firstOrNull()?.let {
CategoryNodeList.CategoryEdge(
getAsCursor(it),
it
)
},
resultsAsType.lastOrNull()?.let {
CategoryNodeList.CategoryEdge(
getAsCursor(it),
it
)
}
)
},
pageInfo = PageInfo(
hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.id,
hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.id,
startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(it) },
endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(it) }
),
totalCount = queryResults.total.toInt()
)
}
}

View File

@@ -0,0 +1,283 @@
/*
* 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/. */
package suwayomi.tachidesk.graphql.queries
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
import graphql.schema.DataFetchingEnvironment
import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.Op
import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater
import org.jetbrains.exposed.sql.SqlExpressionBuilder.less
import org.jetbrains.exposed.sql.andWhere
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
import suwayomi.tachidesk.graphql.queries.filter.Filter
import suwayomi.tachidesk.graphql.queries.filter.FloatFilter
import suwayomi.tachidesk.graphql.queries.filter.HasGetOp
import suwayomi.tachidesk.graphql.queries.filter.IntFilter
import suwayomi.tachidesk.graphql.queries.filter.LongFilter
import suwayomi.tachidesk.graphql.queries.filter.OpAnd
import suwayomi.tachidesk.graphql.queries.filter.StringFilter
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
import suwayomi.tachidesk.graphql.queries.filter.applyOps
import suwayomi.tachidesk.graphql.server.primitives.Cursor
import suwayomi.tachidesk.graphql.server.primitives.OrderBy
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
import suwayomi.tachidesk.graphql.server.primitives.QueryResults
import suwayomi.tachidesk.graphql.server.primitives.greaterNotUnique
import suwayomi.tachidesk.graphql.server.primitives.lessNotUnique
import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
import suwayomi.tachidesk.graphql.types.ChapterNodeList
import suwayomi.tachidesk.graphql.types.ChapterType
import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.manga.model.table.MangaTable
import java.util.concurrent.CompletableFuture
/**
* TODO Queries
* - Filter in library
* - Get page list?
*/
class ChapterQuery {
fun chapter(dataFetchingEnvironment: DataFetchingEnvironment, id: Int): CompletableFuture<ChapterType?> {
return dataFetchingEnvironment.getValueFromDataLoader("ChapterDataLoader", id)
}
enum class ChapterOrderBy(override val column: Column<out Comparable<*>>) : OrderBy<ChapterType> {
ID(ChapterTable.id),
SOURCE_ORDER(ChapterTable.sourceOrder),
NAME(ChapterTable.name),
UPLOAD_DATE(ChapterTable.date_upload),
CHAPTER_NUMBER(ChapterTable.chapter_number),
LAST_READ_AT(ChapterTable.lastReadAt),
FETCHED_AT(ChapterTable.fetchedAt);
override fun greater(cursor: Cursor): Op<Boolean> {
return when (this) {
ID -> ChapterTable.id greater cursor.value.toInt()
SOURCE_ORDER -> greaterNotUnique(ChapterTable.sourceOrder, ChapterTable.id, cursor, String::toInt)
NAME -> greaterNotUnique(ChapterTable.name, ChapterTable.id, cursor, String::toString)
UPLOAD_DATE -> greaterNotUnique(ChapterTable.date_upload, ChapterTable.id, cursor, String::toLong)
CHAPTER_NUMBER -> greaterNotUnique(ChapterTable.chapter_number, ChapterTable.id, cursor, String::toFloat)
LAST_READ_AT -> greaterNotUnique(ChapterTable.lastReadAt, ChapterTable.id, cursor, String::toLong)
FETCHED_AT -> greaterNotUnique(ChapterTable.fetchedAt, ChapterTable.id, cursor, String::toLong)
}
}
override fun less(cursor: Cursor): Op<Boolean> {
return when (this) {
ID -> ChapterTable.id less cursor.value.toInt()
SOURCE_ORDER -> lessNotUnique(ChapterTable.sourceOrder, ChapterTable.id, cursor, String::toInt)
NAME -> lessNotUnique(ChapterTable.name, ChapterTable.id, cursor, String::toString)
UPLOAD_DATE -> lessNotUnique(ChapterTable.date_upload, ChapterTable.id, cursor, String::toLong)
CHAPTER_NUMBER -> lessNotUnique(ChapterTable.chapter_number, ChapterTable.id, cursor, String::toFloat)
LAST_READ_AT -> lessNotUnique(ChapterTable.lastReadAt, ChapterTable.id, cursor, String::toLong)
FETCHED_AT -> lessNotUnique(ChapterTable.fetchedAt, ChapterTable.id, cursor, String::toLong)
}
}
override fun asCursor(type: ChapterType): Cursor {
val value = when (this) {
ID -> type.id.toString()
SOURCE_ORDER -> type.id.toString() + "-" + type.sourceOrder
NAME -> type.id.toString() + "-" + type.name
UPLOAD_DATE -> type.id.toString() + "-" + type.uploadDate
CHAPTER_NUMBER -> type.id.toString() + "-" + type.chapterNumber
LAST_READ_AT -> type.id.toString() + "-" + type.lastReadAt
FETCHED_AT -> type.id.toString() + "-" + type.fetchedAt
}
return Cursor(value)
}
}
data class ChapterCondition(
val id: Int? = null,
val url: String? = null,
val name: String? = null,
val uploadDate: Long? = null,
val chapterNumber: Float? = null,
val scanlator: String? = null,
val mangaId: Int? = null,
val isRead: Boolean? = null,
val isBookmarked: Boolean? = null,
val lastPageRead: Int? = null,
val lastReadAt: Long? = null,
val sourceOrder: Int? = null,
val realUrl: String? = null,
val fetchedAt: Long? = null,
val isDownloaded: Boolean? = null,
val pageCount: Int? = null
) : HasGetOp {
override fun getOp(): Op<Boolean>? {
val opAnd = OpAnd()
opAnd.eq(id, ChapterTable.id)
opAnd.eq(url, ChapterTable.url)
opAnd.eq(name, ChapterTable.name)
opAnd.eq(uploadDate, ChapterTable.date_upload)
opAnd.eq(chapterNumber, ChapterTable.chapter_number)
opAnd.eq(scanlator, ChapterTable.scanlator)
opAnd.eq(mangaId, ChapterTable.manga)
opAnd.eq(isRead, ChapterTable.isRead)
opAnd.eq(isBookmarked, ChapterTable.isBookmarked)
opAnd.eq(lastPageRead, ChapterTable.lastPageRead)
opAnd.eq(lastReadAt, ChapterTable.lastReadAt)
opAnd.eq(sourceOrder, ChapterTable.sourceOrder)
opAnd.eq(realUrl, ChapterTable.realUrl)
opAnd.eq(fetchedAt, ChapterTable.fetchedAt)
opAnd.eq(isDownloaded, ChapterTable.isDownloaded)
opAnd.eq(pageCount, ChapterTable.pageCount)
return opAnd.op
}
}
data class ChapterFilter(
val id: IntFilter? = null,
val url: StringFilter? = null,
val name: StringFilter? = null,
val uploadDate: LongFilter? = null,
val chapterNumber: FloatFilter? = null,
val scanlator: StringFilter? = null,
val mangaId: IntFilter? = null,
val isRead: BooleanFilter? = null,
val isBookmarked: BooleanFilter? = null,
val lastPageRead: IntFilter? = null,
val lastReadAt: LongFilter? = null,
val sourceOrder: IntFilter? = null,
val realUrl: StringFilter? = null,
val fetchedAt: LongFilter? = null,
val isDownloaded: BooleanFilter? = null,
val pageCount: IntFilter? = null,
val inLibrary: BooleanFilter? = null,
override val and: List<ChapterFilter>? = null,
override val or: List<ChapterFilter>? = null,
override val not: ChapterFilter? = null
) : Filter<ChapterFilter> {
override fun getOpList(): List<Op<Boolean>> {
return listOfNotNull(
andFilterWithCompareEntity(ChapterTable.id, id),
andFilterWithCompareString(ChapterTable.url, url),
andFilterWithCompareString(ChapterTable.name, name),
andFilterWithCompare(ChapterTable.date_upload, uploadDate),
andFilterWithCompare(ChapterTable.chapter_number, chapterNumber),
andFilterWithCompareString(ChapterTable.scanlator, scanlator),
andFilterWithCompareEntity(ChapterTable.manga, mangaId),
andFilterWithCompare(ChapterTable.isRead, isRead),
andFilterWithCompare(ChapterTable.isBookmarked, isBookmarked),
andFilterWithCompare(ChapterTable.lastPageRead, lastPageRead),
andFilterWithCompare(ChapterTable.lastReadAt, lastReadAt),
andFilterWithCompare(ChapterTable.sourceOrder, sourceOrder),
andFilterWithCompareString(ChapterTable.realUrl, realUrl),
andFilterWithCompare(ChapterTable.fetchedAt, fetchedAt),
andFilterWithCompare(ChapterTable.isDownloaded, isDownloaded),
andFilterWithCompare(ChapterTable.pageCount, pageCount)
)
}
fun getLibraryOp() = andFilterWithCompare(MangaTable.inLibrary, inLibrary)
}
fun chapters(
condition: ChapterCondition? = null,
filter: ChapterFilter? = null,
orderBy: ChapterOrderBy? = null,
orderByType: SortOrder? = null,
before: Cursor? = null,
after: Cursor? = null,
first: Int? = null,
last: Int? = null,
offset: Int? = null
): ChapterNodeList {
val queryResults = transaction {
val res = ChapterTable.selectAll()
val libraryOp = filter?.getLibraryOp()
if (libraryOp != null) {
res.adjustColumnSet {
innerJoin(MangaTable)
}
res.andWhere { libraryOp }
}
res.applyOps(condition, filter)
if (orderBy != null || (last != null || before != null)) {
val orderByColumn = orderBy?.column ?: ChapterTable.id
val orderType = orderByType.maybeSwap(last ?: before)
if (orderBy == ChapterOrderBy.ID || orderBy == null) {
res.orderBy(orderByColumn to orderType)
} else {
res.orderBy(
orderByColumn to orderType,
ChapterTable.id to SortOrder.ASC
)
}
}
val total = res.count()
val firstResult = res.firstOrNull()?.get(ChapterTable.id)?.value
val lastResult = res.lastOrNull()?.get(ChapterTable.id)?.value
if (after != null) {
res.andWhere {
(orderBy ?: ChapterOrderBy.ID).greater(after)
}
} else if (before != null) {
res.andWhere {
(orderBy ?: ChapterOrderBy.ID).less(before)
}
}
if (first != null) {
res.limit(first, offset?.toLong() ?: 0)
} else if (last != null) {
res.limit(last)
}
QueryResults(total, firstResult, lastResult, res.toList())
}
val getAsCursor: (ChapterType) -> Cursor = (orderBy ?: ChapterOrderBy.ID)::asCursor
val resultsAsType = queryResults.results.map { ChapterType(it) }
return ChapterNodeList(
resultsAsType,
if (resultsAsType.isEmpty()) {
emptyList()
} else {
listOfNotNull(
resultsAsType.firstOrNull()?.let {
ChapterNodeList.ChapterEdge(
getAsCursor(it),
it
)
},
resultsAsType.lastOrNull()?.let {
ChapterNodeList.ChapterEdge(
getAsCursor(it),
it
)
}
)
},
pageInfo = PageInfo(
hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.id,
hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.id,
startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(it) },
endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(it) }
),
totalCount = queryResults.total.toInt()
)
}
}

View File

@@ -0,0 +1,230 @@
/*
* 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/. */
package suwayomi.tachidesk.graphql.queries
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
import eu.kanade.tachiyomi.source.local.LocalSource
import graphql.schema.DataFetchingEnvironment
import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.Op
import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater
import org.jetbrains.exposed.sql.SqlExpressionBuilder.less
import org.jetbrains.exposed.sql.SqlExpressionBuilder.neq
import org.jetbrains.exposed.sql.andWhere
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
import suwayomi.tachidesk.graphql.queries.filter.Filter
import suwayomi.tachidesk.graphql.queries.filter.HasGetOp
import suwayomi.tachidesk.graphql.queries.filter.IntFilter
import suwayomi.tachidesk.graphql.queries.filter.OpAnd
import suwayomi.tachidesk.graphql.queries.filter.StringFilter
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
import suwayomi.tachidesk.graphql.queries.filter.applyOps
import suwayomi.tachidesk.graphql.server.primitives.Cursor
import suwayomi.tachidesk.graphql.server.primitives.OrderBy
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
import suwayomi.tachidesk.graphql.server.primitives.QueryResults
import suwayomi.tachidesk.graphql.server.primitives.greaterNotUnique
import suwayomi.tachidesk.graphql.server.primitives.lessNotUnique
import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
import suwayomi.tachidesk.graphql.types.ExtensionNodeList
import suwayomi.tachidesk.graphql.types.ExtensionType
import suwayomi.tachidesk.manga.model.table.ExtensionTable
import java.util.concurrent.CompletableFuture
class ExtensionQuery {
fun extension(dataFetchingEnvironment: DataFetchingEnvironment, pkgName: String): CompletableFuture<ExtensionType?> {
return dataFetchingEnvironment.getValueFromDataLoader("ExtensionDataLoader", pkgName)
}
enum class ExtensionOrderBy(override val column: Column<out Comparable<*>>) : OrderBy<ExtensionType> {
PKG_NAME(ExtensionTable.pkgName),
NAME(ExtensionTable.name),
APK_NAME(ExtensionTable.apkName);
override fun greater(cursor: Cursor): Op<Boolean> {
return when (this) {
PKG_NAME -> ExtensionTable.pkgName greater cursor.value
NAME -> greaterNotUnique(ExtensionTable.name, ExtensionTable.pkgName, cursor, String::toString)
APK_NAME -> greaterNotUnique(ExtensionTable.apkName, ExtensionTable.pkgName, cursor, String::toString)
}
}
override fun less(cursor: Cursor): Op<Boolean> {
return when (this) {
PKG_NAME -> ExtensionTable.pkgName less cursor.value
NAME -> lessNotUnique(ExtensionTable.name, ExtensionTable.pkgName, cursor, String::toString)
APK_NAME -> lessNotUnique(ExtensionTable.apkName, ExtensionTable.pkgName, cursor, String::toString)
}
}
override fun asCursor(type: ExtensionType): Cursor {
val value = when (this) {
PKG_NAME -> type.pkgName
NAME -> type.pkgName + "\\-" + type.name
APK_NAME -> type.pkgName + "\\-" + type.apkName
}
return Cursor(value)
}
}
data class ExtensionCondition(
val apkName: String? = null,
val iconUrl: String? = null,
val name: String? = null,
val pkgName: String? = null,
val versionName: String? = null,
val versionCode: Int? = null,
val lang: String? = null,
val isNsfw: Boolean? = null,
val isInstalled: Boolean? = null,
val hasUpdate: Boolean? = null,
val isObsolete: Boolean? = null
) : HasGetOp {
override fun getOp(): Op<Boolean>? {
val opAnd = OpAnd()
opAnd.eq(apkName, ExtensionTable.apkName)
opAnd.eq(iconUrl, ExtensionTable.iconUrl)
opAnd.eq(name, ExtensionTable.name)
opAnd.eq(versionName, ExtensionTable.versionName)
opAnd.eq(versionCode, ExtensionTable.versionCode)
opAnd.eq(lang, ExtensionTable.lang)
opAnd.eq(isNsfw, ExtensionTable.isNsfw)
opAnd.eq(isInstalled, ExtensionTable.isInstalled)
opAnd.eq(hasUpdate, ExtensionTable.hasUpdate)
opAnd.eq(isObsolete, ExtensionTable.isObsolete)
return opAnd.op
}
}
data class ExtensionFilter(
val apkName: StringFilter? = null,
val iconUrl: StringFilter? = null,
val name: StringFilter? = null,
val pkgName: StringFilter? = null,
val versionName: StringFilter? = null,
val versionCode: IntFilter? = null,
val lang: StringFilter? = null,
val isNsfw: BooleanFilter? = null,
val isInstalled: BooleanFilter? = null,
val hasUpdate: BooleanFilter? = null,
val isObsolete: BooleanFilter? = null,
override val and: List<ExtensionFilter>? = null,
override val or: List<ExtensionFilter>? = null,
override val not: ExtensionFilter? = null
) : Filter<ExtensionFilter> {
override fun getOpList(): List<Op<Boolean>> {
return listOfNotNull(
andFilterWithCompareString(ExtensionTable.apkName, apkName),
andFilterWithCompareString(ExtensionTable.iconUrl, iconUrl),
andFilterWithCompareString(ExtensionTable.name, name),
andFilterWithCompareString(ExtensionTable.pkgName, pkgName),
andFilterWithCompareString(ExtensionTable.versionName, versionName),
andFilterWithCompare(ExtensionTable.versionCode, versionCode),
andFilterWithCompareString(ExtensionTable.lang, lang),
andFilterWithCompare(ExtensionTable.isNsfw, isNsfw),
andFilterWithCompare(ExtensionTable.isInstalled, isInstalled),
andFilterWithCompare(ExtensionTable.hasUpdate, hasUpdate),
andFilterWithCompare(ExtensionTable.isObsolete, isObsolete)
)
}
}
fun extensions(
condition: ExtensionCondition? = null,
filter: ExtensionFilter? = null,
orderBy: ExtensionOrderBy? = null,
orderByType: SortOrder? = null,
before: Cursor? = null,
after: Cursor? = null,
first: Int? = null,
last: Int? = null,
offset: Int? = null
): ExtensionNodeList {
val queryResults = transaction {
val res = ExtensionTable.selectAll()
res.adjustWhere { ExtensionTable.name neq LocalSource.EXTENSION_NAME }
res.applyOps(condition, filter)
if (orderBy != null || (last != null || before != null)) {
val orderByColumn = orderBy?.column ?: ExtensionTable.pkgName
val orderType = orderByType.maybeSwap(last ?: before)
if (orderBy == ExtensionOrderBy.PKG_NAME || orderBy == null) {
res.orderBy(orderByColumn to orderType)
} else {
res.orderBy(
orderByColumn to orderType,
ExtensionTable.pkgName to SortOrder.ASC
)
}
}
val total = res.count()
val firstResult = res.firstOrNull()?.get(ExtensionTable.pkgName)
val lastResult = res.lastOrNull()?.get(ExtensionTable.pkgName)
if (after != null) {
res.andWhere {
(orderBy ?: ExtensionOrderBy.PKG_NAME).greater(after)
}
} else if (before != null) {
res.andWhere {
(orderBy ?: ExtensionOrderBy.PKG_NAME).less(before)
}
}
if (first != null) {
res.limit(first, offset?.toLong() ?: 0)
} else if (last != null) {
res.limit(last)
}
QueryResults(total, firstResult, lastResult, res.toList())
}
val getAsCursor: (ExtensionType) -> Cursor = (orderBy ?: ExtensionOrderBy.PKG_NAME)::asCursor
val resultsAsType = queryResults.results.map { ExtensionType(it) }
return ExtensionNodeList(
resultsAsType,
if (resultsAsType.isEmpty()) {
emptyList()
} else {
listOfNotNull(
resultsAsType.firstOrNull()?.let {
ExtensionNodeList.ExtensionEdge(
getAsCursor(it),
it
)
},
resultsAsType.lastOrNull()?.let {
ExtensionNodeList.ExtensionEdge(
getAsCursor(it),
it
)
}
)
},
pageInfo = PageInfo(
hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.pkgName,
hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.pkgName,
startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(it) },
endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(it) }
),
totalCount = queryResults.total.toInt()
)
}
}

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