Compare commits

...

312 Commits

Author SHA1 Message Date
Aria Moradi 4b6c51b1f8 bump to v0.4.0
CI Publish / Validate Gradle Wrapper (push) Successful in 12s
CI Publish / Build artifacts and release (push) Failing after 17s
2021-05-27 19:32:16 +04:30
Aria Moradi bd02edf0b1 barebones anime player 2021-05-27 18:37:45 +04:30
Aria Moradi 5c7123a997 Manga page Finished 2021-05-27 17:13:22 +04:30
Aria Moradi c17e3bd04f can work with anime extensions successfully 2021-05-27 05:13:01 +04:30
Aria Moradi 994ae97256 no dependenct on tachidesk 2021-05-27 03:33:56 +04:30
Aria Moradi 781428a690 add initial anime stuff 2021-05-27 03:25:55 +04:30
Aria Moradi c23ac5faa8 fix compile issue 2021-05-27 02:23:17 +04:30
Aria Moradi e8d41f83c2 move databse to server package, move tables to a better place 2021-05-27 02:21:53 +04:30
Aria Moradi 921a0a3361 Merge branch 'master' into anime 2021-05-27 02:16:07 +04:30
Aria Moradi dda5a2df93 reconsider package strings 2021-05-27 02:13:17 +04:30
Aria Moradi 155f9f107d more of package moving 2021-05-27 02:07:32 +04:30
Aria Moradi 24f68b8f1a move packages 2021-05-27 01:57:40 +04:30
Aria Moradi 0ffbe194fa move packages 2021-05-27 01:57:15 +04:30
Aria Moradi 0b41e2b72b Adapted Tachiyomi-mi extensions-lib implementation 2021-05-27 00:04:33 +04:30
arbuilder ef07b9b4ce [SKIP CI] Update Docker info (#99)
* Update README.md

update docker info

* [SKIP CI] update docker info

* [SKIP CI] Update Readme

change to small case
2021-05-26 11:00:13 +04:30
Aria Moradi f3999cf2d9 [SKIP CI] update docker info 2021-05-26 02:48:13 +04:30
Syer10 1729847937 Allow building without git access (#98)
Just something SY needs...
2021-05-26 02:39:25 +04:30
Aria Moradi 37bff6c76c About Screen 2021-05-25 21:06:27 +04:30
Aria Moradi 4ef32d8037 build type 2021-05-25 19:23:47 +04:30
Aria Moradi d2f6a33f0a fix listener not being removed 2021-05-25 16:19:08 +04:30
Aria Moradi 31d9903251 got rid of all instances of diabling no-unused-vars 2021-05-25 13:24:56 +04:30
Aria Moradi e97642d92a prevPage handle
* go back to previous chapter on page 0 when prevPage is triggered
2021-05-25 13:14:07 +04:30
Aria Moradi c49fc0ff5f only show supported pagers 2021-05-25 13:12:42 +04:30
Aria Moradi deb2ab1ff4 closes #96 2021-05-25 13:12:17 +04:30
Manchewable 23466cf853 Added some key mappings to navigate pages (#95)
* Added some key mappings to navigate pages

* use keyboard event codes

* unused files removed

* use a reference to current page

* fix some bugs with Virtuoso

* add keymapping for space to navigate to next page

* commit my changes

* fix functions not regenerating

* fix partial scroll back to start of page issue

Co-authored-by: Aria Moradi <aria.moradi007@gmail.com>
2021-05-24 23:46:05 +04:30
Aria Moradi 16b34f874d fix some bugs with Virtuoso 2021-05-24 21:26:55 +04:30
Aria Moradi 0e0d08ae5a bump to v0.3.9
CI Publish / Validate Gradle Wrapper (push) Successful in 14s
CI Publish / Build artifacts and release (push) Failing after 19s
2021-05-24 18:32:01 +04:30
Aria Moradi 986b4c2c27 unused files removed 2021-05-24 18:31:39 +04:30
Aria Moradi 0bf9ccfcbd [SKIP CI] fix more typo 2021-05-24 18:22:48 +04:30
Aria Moradi 5e8c47928d [SKIP CI] fix typo 2021-05-24 18:22:16 +04:30
Aria Moradi ffae7f911f [SKIP CI] update windows instructions 2021-05-24 18:21:23 +04:30
Aria Moradi e37fdf6d79 [SKIP CI] update preview link 2021-05-24 17:31:07 +04:30
Aria Moradi b359116745 clean up tests 2021-05-24 17:09:05 +04:30
Aria Moradi 60073aace3 test new publish 2021-05-24 17:04:47 +04:30
Aria Moradi 874b13fa14 test new publish 2021-05-24 17:02:30 +04:30
Aria Moradi b146d1024b test new publish 2021-05-24 16:56:03 +04:30
Aria Moradi 332e95c021 test new publish 2021-05-24 16:52:30 +04:30
Aria Moradi 1f68141df5 test new publish 2021-05-24 16:39:17 +04:30
Aria Moradi dd731cd306 test new publish 2021-05-24 16:34:12 +04:30
Aria Moradi 38d8d03cae test new publish 2021-05-24 16:30:32 +04:30
Aria Moradi ec7d840f37 test new publish 2021-05-24 16:29:03 +04:30
Aria Moradi 2813dbb897 test new publish 2021-05-24 16:27:46 +04:30
Aria Moradi 77d1402b8a test new publish 2021-05-24 16:25:42 +04:30
Aria Moradi 08e8a9d105 Electron launcher 2021-05-24 15:37:25 +04:30
Aria Moradi 71661f70b6 flexible z names 2021-05-24 01:49:42 +04:30
Aria Moradi ac1e79ba83 electron! 2021-05-24 01:39:12 +04:30
Aria Moradi d082809776 prepare for electron 2021-05-24 00:42:25 +04:30
Aria Moradi a458a696db webview starts! 2021-05-23 23:04:02 +04:30
Aria Moradi 75786a91b0 add webview 2021-05-23 22:10:04 +04:30
Aria Moradi 6ddb5db57b use HEAD for counting commits
CI Publish / Validate Gradle Wrapper (push) Successful in 10s
CI Publish / Build artifacts and release (push) Failing after 18s
2021-05-23 18:22:35 +04:30
Aria Moradi 4f70cc9283 bump to v0.3.8 2021-05-23 17:27:33 +04:30
Aria Moradi 23b643d637 set default category when adding new manga 2021-05-23 15:28:46 +04:30
Aria Moradi fdfc256c4d Meaningful icons! 2021-05-23 13:48:02 +04:30
Aria Moradi fba56c1b75 replace win64 exe with @Syer10's MSVC build 2021-05-23 12:48:47 +04:30
Aria Moradi 4743bfacf7 [SKIP CI] removing Swing force fixed it for @nar1n 2021-05-21 16:47:46 +04:30
Aria Moradi 2356537f7c try swing 2021-05-21 15:55:37 +04:30
Aria Moradi fa071aee84 refactor github api 2021-05-20 20:41:00 +04:30
Aria Moradi c00ca23a8b put the comment where it should be 2021-05-20 20:27:22 +04:30
Aria Moradi 733b017936 fix webUI not being copied 2021-05-20 19:51:52 +04:30
Aria Moradi 4147f2e368 better comment 2021-05-20 19:21:30 +04:30
Aria Moradi 154b9992eb rewrite without retrofit and kotlin-serialization 2021-05-20 19:20:07 +04:30
Aria Moradi 88b881b043 get rid of guava 2021-05-20 17:56:33 +04:30
Aria Moradi 5d1491fb8c fix package directive 2021-05-20 16:23:13 +04:30
Aria Moradi 3a33196cf1 cleanup dependencies 2021-05-20 16:22:54 +04:30
Aria Moradi fa8e0478da lint file 2021-05-20 13:50:10 +04:30
Aria Moradi 7e7e069244 - Set log level eairlier
- Set AndroidCompat's data root properly
2021-05-20 13:48:33 +04:30
Aria Moradi 18e0d34af0 [SKIP CI] "improvments" 2021-05-20 10:52:57 +04:30
Aria Moradi 3fe3f35483 better commit messages 2021-05-20 10:33:33 +04:30
Aria Moradi cf8e274883 better use of kotlin DSL 2021-05-20 10:24:33 +04:30
Aria Moradi 10dee8b345 improve downloader 2021-05-20 02:36:20 +04:30
Aria Moradi ae8d30593f lint 2021-05-19 23:05:25 +04:30
Aria Moradi 9cde46b5da Fix chpater names, closes #81 2021-05-19 23:03:40 +04:30
Aria Moradi 8e61632155 open the right ip 2021-05-19 17:40:26 +04:30
Aria Moradi e2c4b4cb57 handle when the user runs the app instead of clicking on systemtray 2021-05-19 17:38:33 +04:30
Aria Moradi 326da504ea fix gradle complaning about lint tasks depending on webUI:copyBuild 2021-05-19 17:03:12 +04:30
Aria Moradi c5874a3f10 better chapter looks 2021-05-19 16:50:48 +04:30
Aria Moradi 02802fab97 Application mutex 2021-05-19 16:36:17 +04:30
Aria Moradi 29dea10be2 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-05-19 13:42:59 +04:30
Aria Moradi 6bc36193dc open server's location please! 2021-05-19 13:42:18 +04:30
Syer10 81e123388e Fix restore crashing (#90) 2021-05-19 05:29:44 +04:30
Aria Moradi 8ebd7869a5 [SKIP CI] name em 2021-05-19 04:30:21 +04:30
Aria Moradi 7a2f5f13f1 [SKIP CI] name em 2021-05-19 04:28:57 +04:30
Aria Moradi 25d7dad39f build the ref that you have been given! 2021-05-19 04:26:20 +04:30
Aria Moradi c8f8795920 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-05-19 03:57:27 +04:30
Aria Moradi 84206a7074 bump to v0.3.7
CI Publish / Validate Gradle Wrapper (push) Successful in 13s
CI Publish / Build FatJar (push) Failing after 16s
2021-05-19 03:52:55 +04:30
Aria Moradi 6fd8b36dca [SKIP CI] links to the new preview repo 2021-05-19 03:45:24 +04:30
Aria Moradi d1500baae1 try with access token 2021-05-19 03:21:47 +04:30
Aria Moradi 045801dd1a push to Suwayomi/Tachidesk-preview 2021-05-19 02:56:46 +04:30
Aria Moradi 14a2cbc793 [SKIP CI] fix typo 2021-05-19 02:52:28 +04:30
Aria Moradi fd385017df [SKIP CI] fix typo 2021-05-19 02:50:22 +04:30
Aria Moradi 9b05954cf2 [SKIP CI] fix typo 2021-05-19 02:49:10 +04:30
Aria Moradi 6aaf636069 [SKIP CI] update for new scripts 2021-05-19 02:47:48 +04:30
Aria Moradi d30e89e5ec update workflows to include both 32-bit and 64-bit windows bundles 2021-05-19 02:42:14 +04:30
Aria Moradi 7acc745478 Merge the two windows bundlers 2021-05-19 02:31:56 +04:30
Aria Moradi 5a9a2d816e 32-bit variant of bundler 2021-05-19 02:26:29 +04:30
Aria Moradi 105f11ed02 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-05-19 00:28:56 +04:30
Aria Moradi 3021437a05 new preview system! 2021-05-19 00:28:14 +04:30
Aria Moradi 439602fc03 [SKIP CI] no more preview 2021-05-18 23:42:43 +04:30
Aria Moradi 34e13b9589 [SKIP CI] improve wording... 2021-05-18 23:28:59 +04:30
Aria Moradi 2aab4ae918 [SKIP CI] asking for help! 2021-05-18 23:27:14 +04:30
Aria Moradi 7ef67671a4 print tachidesk info on startup 2021-05-18 22:36:41 +04:30
Syer10 e8df84416c Smarter Chapters and cleanup (#87)
* Smarter Chapters and cleanup

* Fix check
2021-05-18 22:22:15 +04:30
Aria Moradi be930bb68b update to the new scheme 2021-05-18 22:03:18 +04:30
Aria Moradi db52948865 update windows instructions 2021-05-18 21:42:30 +04:30
Aria Moradi d2a72526f6 bump to v0.3.6
CI Publish / Validate Gradle Wrapper (push) Successful in 11s
CI Publish / Build FatJar (push) Failing after 16s
2021-05-18 21:40:42 +04:30
Aria Moradi 0a9f57b32b cleanup 2021-05-18 21:38:41 +04:30
Aria Moradi 180f210536 update windows instructions 2021-05-18 21:35:57 +04:30
Aria Moradi c1baa31eed the new and simple way of packaging windows 2021-05-18 21:31:25 +04:30
Aria Moradi cacc97cec7 bump to v0.3.5
CI Publish / Validate Gradle Wrapper (push) Successful in 11s
CI Publish / Build FatJar (push) Failing after 16s
2021-05-18 02:39:00 +04:30
Aria Moradi d5691fd81c show last read page on initial load 2021-05-18 02:26:45 +04:30
Aria Moradi 49dc9fe5f6 fix wrong chapter count, abstract next page 2021-05-18 01:10:28 +04:30
Aria Moradi c0b49c7428 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-05-18 00:45:42 +04:30
Aria Moradi fa345af42d use Dispatchers.IO 2021-05-18 00:43:32 +04:30
Aria Moradi db3cc786a1 rename the job 2021-05-18 00:43:21 +04:30
Aria Moradi fe879ae51d [SKIP CI] update windows instructions 2021-05-18 00:01:04 +04:30
Aria Moradi 2f55460ffb unzip the jre
CI Publish / Validate Gradle Wrapper (push) Successful in 14s
CI Publish / Build FatJar (push) Failing after 17s
2021-05-17 23:33:54 +04:30
Aria Moradi fbc5bd4642 bump to v0.3.4 2021-05-17 23:29:36 +04:30
Aria Moradi 5e0c7d3c9d ditch packr because it can't load extension jars 2021-05-17 23:28:23 +04:30
Aria Moradi 083996a48d wating on: https://github.com/Kotlin/kotlinx.coroutines/issues/261
CI Publish / Validate Gradle Wrapper (push) Successful in 11s
CI Publish / Build FatJar (push) Failing after 17s
2021-05-17 14:30:59 +04:30
Aria Moradi 9d38f478e3 fix slow manga thumbnails issue, next manga reset page issue 2021-05-17 14:22:24 +04:30
Aria Moradi 57274a0a01 remove unused electron script 2021-05-17 12:32:15 +04:30
Aria Moradi b3b56b7fc8 also build windows package for publish 2021-05-17 12:30:15 +04:30
Forgenn 0b690577da Load next chapter when getting to the last page (#84)
* Load next chapter when scrolling to the bottom (Webtoon, Continues Vertical)

* Load next chapter when scrolling to the bottom (Paged reader)

* Added missing types to IReaderProps

* Move load next chapter when at last page to VerticalReader

* Dependency fix

* Use react history for loading next page
2021-05-17 12:27:14 +04:30
Aria Moradi e9683a3a37 bump to v0.3.3 2021-05-17 12:02:01 +04:30
Aria Moradi f8f67b3eba finish up 2021-05-17 11:59:59 +04:30
Aria Moradi 7b16b082d8 needs kt 2021-05-17 11:55:49 +04:30
Aria Moradi 2a783f0d8e btter folder name 2021-05-17 11:54:33 +04:30
Aria Moradi 42ae32de33 give the correct path 2021-05-17 11:40:38 +04:30
Aria Moradi cec7ddc486 update file permissions 2021-05-17 11:28:26 +04:30
Aria Moradi 9c55fc3868 make windows package with packr 2021-05-17 11:20:24 +04:30
Syer10 104c5a8d83 Code cleanup (#85)
* GC Unused or only used once objects

* Move things around a bit

* Revert some changes

* Fix imports

* Revert about change

* Put back logger

* Private logger

* Revert systemtray

* Move import
2021-05-17 02:48:01 +04:30
Aria Moradi 7450b16742 - Reader -> Pager
- add cloneObject
- add missing copyright notices
2021-05-17 01:38:59 +04:30
Aria Moradi 3ecd0931a1 missing from the previous commit 2021-05-17 01:37:25 +04:30
Aria Moradi 2f2a52ae2f Move navbars 2021-05-17 01:36:31 +04:30
Aria Moradi f464087c30 also handle Errors from java 2021-05-16 23:58:32 +04:30
Aria Moradi 2364960388 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-05-16 22:15:35 +04:30
Aria Moradi 76be4d64cd update launch4j's jre and make it 64bit 2021-05-16 12:39:05 +04:30
Aria Moradi 7d98e8ce47 [SKIP CI] correct link 2021-05-16 01:53:35 +04:30
Aria Moradi 40831fc681 [SKIP CI] new links 2021-05-16 01:52:01 +04:30
Aria Moradi e38e7ccf26 [SKIP CI] troubleshooting from the wiki 2021-05-16 01:48:49 +04:30
Aria Moradi 98b9e2f2cf [SKIP CI] no need to delete data anymore... 2021-05-16 01:47:33 +04:30
Aria Moradi 4bf3c12f76 fixed when spinner stops just after first offline chapter fetch 2021-05-16 01:07:48 +04:30
Aria Moradi bab25f9ad9 bump version
CI Publish / Validate Gradle Wrapper (push) Successful in 12s
CI Publish / Build FatJar (push) Failing after 16s
2021-05-15 23:24:25 +04:30
Aria Moradi a62ee8f8c3 handle reader types 2021-05-15 23:22:37 +04:30
Aria Moradi 5f23691e20 add toast lib 2021-05-15 20:37:07 +04:30
Aria Moradi 3de9ccc62f stop spinning if chapter list is empty 2021-05-15 18:51:38 +04:30
Aria Moradi 1896f7f37b remove lazy load 2021-05-15 18:26:40 +04:30
Aria Moradi 490643dc02 proof of concept readers 2021-05-15 18:17:12 +04:30
Aria Moradi 9808976088 restructure the reader 2021-05-15 17:18:57 +04:30
Aria Moradi 5a73068a10 bump to v0.3.1
CI Publish / Validate Gradle Wrapper (push) Successful in 17s
CI Publish / Build FatJar (push) Failing after 16s
2021-05-15 14:53:44 +04:30
Aria Moradi 01d5c2540d chapter updates when pressing UI buttons 2021-05-15 13:43:26 +04:30
Aria Moradi 866b01f865 support bookmarked and isRead in webUI 2021-05-14 19:22:10 +04:30
Aria Moradi da6a953099 exposed error 2021-05-14 17:31:07 +04:30
Aria Moradi bce8d58845 make it look nicer? 2021-05-14 02:01:28 +04:30
Aria Moradi 3cfce2db04 fix chapters not shown on movbile 2021-05-13 21:40:42 +04:30
Aria Moradi 327aae5dd9 Merge pull request #79 from Suwayomi/read-category
Support more chapter parameters
2021-05-13 18:34:27 +04:30
Aria Moradi 1bdfde7032 no longer TODO 2021-05-13 18:33:24 +04:30
Aria Moradi 295a0817b0 fix wrong chapters being removed, fix da css 2021-05-13 18:29:20 +04:30
Aria Moradi a02dc02d52 remove console prints 2021-05-13 17:49:33 +04:30
Aria Moradi dc012edf7d staisfying results? with chapters scrolling 2021-05-13 17:46:40 +04:30
Aria Moradi 1e2eb11c13 update dependencies 2021-05-13 15:01:37 +04:30
Aria Moradi 3a825f4f25 fix manga thumbnail loading bug 2021-05-12 10:39:21 +04:30
Aria Moradi b9ea8c5f74 code cleanup 2021-05-12 10:20:01 +04:30
Aria Moradi 320d7e2536 reformat 2021-05-11 18:55:09 +04:30
Aria Moradi c200785479 handle front, handle orphans 2021-05-11 18:45:53 +04:30
Aria Moradi 8abb132ad6 chapter new parameters get endpoint 2021-05-11 15:50:18 +04:30
Aria Moradi 8bb2269f36 [SKIP CI] fix typo 2021-05-08 20:33:53 +04:30
Aria Moradi 9d17b26283 [SKIP CI] up for debate 2021-05-07 19:41:34 +04:30
Aria Moradi 5909f15db7 [SKIP CI] code of conduct 2021-05-07 19:39:24 +04:30
Aria Moradi 11672ca576 [SKIP CI] 2021-05-07 19:35:11 +04:30
Aria Moradi e09773def3 [SKIP CI] potential contributors 2021-05-07 19:32:32 +04:30
Aria Moradi f6d4432e6f [SKIP CI] typo 2021-05-07 19:13:48 +04:30
Aria Moradi 45a6abc5c2 [SKIP CI] improve structure 2021-05-07 19:13:04 +04:30
Aria Moradi dc5e677a38 [SKIP CI] simplify 2021-05-07 19:05:11 +04:30
Aria Moradi a82549dc17 [SKIP CI] why a web app? 2021-05-07 19:02:56 +04:30
Aria Moradi a002e19d9d [SKIP CI] move info to CONTRIBUTING.md 2021-05-07 18:58:09 +04:30
Aria Moradi cdf1f98d28 [SKIP CI] move troubleshooting to wiki 2021-05-07 18:51:47 +04:30
Aria Moradi 0ff1ebdeb7 [SKIP CI] not alternative 2021-05-07 18:36:32 +04:30
Aria Moradi 17f4a396f8 [SKIP CI] one must know friend from foe! 2021-05-07 18:29:01 +04:30
Aria Moradi 8aa3cf4368 [SKIP CI] move stuff around 2021-05-07 18:27:10 +04:30
Aria Moradi 0136c5e493 [SKIP CI] alternative ui projects 2021-05-07 18:21:32 +04:30
Aria Moradi 8b94b9ee80 [SKIP CI] add contributing.md 2021-05-07 18:17:55 +04:30
Aria Moradi bed63f19f2 [SKIP CI] move technical info to contributing.md 2021-05-07 18:04:51 +04:30
Aria Moradi e2a6545a84 [SKIP CI] 2021-05-07 15:28:30 +04:30
Aria Moradi e3d3ec6895 fix sed output 2021-05-07 14:41:19 +04:30
Aria Moradi 7ba476bd79 include v 2021-05-07 14:39:15 +04:30
Aria Moradi 2dd41ebd27 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-05-07 14:25:11 +04:30
Aria Moradi 038df78171 new preview version format 2021-05-07 14:24:28 +04:30
Aria Moradi 6e5ff2b508 [SKIP CI] it be "chrome OS" 2021-05-07 13:45:31 +04:30
Aria Moradi ec8d1e8680 [SKIP CI] more housekeeping! 2021-05-07 13:42:02 +04:30
Aria Moradi 1f0f0c33b7 [SKIP CI] housekeeping 2021-05-07 13:32:42 +04:30
Aria Moradi 825940fcac [SKIP CI] add running instructions 2021-05-07 13:17:15 +04:30
Aria Moradi 4618834af2 [SKIP CI] Make it simpler 2021-05-07 11:24:40 +04:30
Aria Moradi 55d968df5e [SKIP CI] add some doc comments 2021-05-06 23:06:35 +04:30
Aria Moradi 63a078cf7d Add migrations (#76)
CI Publish / Validate Gradle Wrapper (push) Successful in 11s
CI Publish / Build FatJar (push) Failing after 16s
2021-05-06 18:46:12 +04:30
Aria Moradi 5304917e53 fix multiple version warning 2021-05-06 14:14:04 +04:30
Aria Moradi 831b74d2ec bintray is dead 2021-05-06 13:07:40 +04:30
Aria Moradi 1bad9dcd69 [SKIP CI] 2021-05-05 15:54:05 +04:30
Aria Moradi dd43716851 [SKIP CI] more info 2021-05-05 15:26:52 +04:30
Aria Moradi f2e55e95a2 reaffirmation of what Tachidesk is for some Twitter users... 2021-05-05 15:22:46 +04:30
Aria Moradi 14658a0c4d be fancy about the manifest info 😎 2021-05-04 00:15:31 +04:30
Aria Moradi 4195e7056b fix windowsPackge not building the jar 2021-05-03 23:51:48 +04:30
Aria Moradi 1d29e8b248 fix webUI not working with gradle 7.0 2021-05-03 23:21:37 +04:30
Syer10 b718c718df Download directly to file instead of a dir (#70) 2021-05-03 23:00:18 +04:30
Aria Moradi a3601cf1b5 fix pull request build 2021-05-03 22:57:54 +04:30
Aria Moradi 0236a9639b rename vals, comments 2021-05-03 22:30:43 +04:30
Syer10 5f4c7454ee Update everything (#68)
* Update everything, cleanup build.gradle.kts's

* Make requested changes
2021-05-03 22:19:09 +04:30
Aria Moradi 773120c96a make the app build 2021-05-03 20:50:09 +04:30
Aria Moradi 4b273c6bf9 add a bit of docs 2021-05-03 20:48:29 +04:30
Aria Moradi b626aa66ba Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-05-03 20:41:46 +04:30
Syer10 1dd029559e Stop Javalin properly on shutdown (#69) 2021-05-03 20:40:02 +04:30
Aria Moradi 59cbe5d5bc [SKIP CI] Yes, we do use git 2021-04-30 06:15:56 +04:30
Aria Moradi 40d1173653 put DBManager where it should be 2021-04-30 06:10:41 +04:30
Aria Moradi bf6a0aba5d Rework the version endpoint 2021-04-30 06:07:29 +04:30
Kolby Moroz Liebl 34d8feacdd Add version api endpoint (#66) 2021-04-30 05:47:23 +04:30
Kolby Moroz Liebl 1ea821584c [SKIP CI] Remove IE Requirement in ps1 script (#65) 2021-04-29 01:52:24 +04:30
Aria Moradi 3d2fee19bb refactor 2021-04-28 10:25:01 +04:30
Forgenn 449d12779a Truncate manga description if it's too long (#63)
* Manga description changed from 4096 to 8192

* Check that the description of a manga is not longer than 4096, trim otherwise

* Revert description length changes
2021-04-28 09:50:56 +04:30
Aria Moradi 6fb6a251e7 [SKIP CI] latest pointer link 2021-04-18 20:52:59 +04:30
Aria Moradi 4d6220f894 dir pointer 2021-04-18 20:45:15 +04:30
Aria Moradi fe747bfc52 [SKIP CI] fix link 2021-04-18 20:14:17 +04:30
Aria Moradi 0c2d038870 [SKIP CI] add logo back 2021-04-18 20:05:58 +04:30
Aria Moradi 4e3f73af75 fix string 2021-04-18 19:58:07 +04:30
Aria Moradi 63e5e1b45f add indexer 2021-04-18 19:50:26 +04:30
Aria Moradi 2e1558bd96 [SKIP CI] fix discord links, again 2021-04-18 19:40:25 +04:30
Aria Moradi 0671dee8b2 [SKIP CI] fix discord link 2021-04-18 19:38:43 +04:30
Aria Moradi 8f91b8089a [SKIP CI] fix preview 2021-04-18 19:38:05 +04:30
Aria Moradi 009b45f676 [SKIP CI] discord? 2021-04-18 19:26:28 +04:30
Aria Moradi 8f7d5eb311 [SKIP CI] badges 2021-04-18 19:22:25 +04:30
Aria Moradi f3de835ef3 Update build_push.yml 2021-04-18 18:51:58 +04:30
Aria Moradi fd6662f428 bring back the old licenses 2021-04-18 17:48:42 +04:30
Aria Moradi fde137b3ed restore categories 2021-04-18 12:40:09 +04:30
Aria Moradi a1349aa0e3 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-04-18 11:54:44 +04:30
Aria Moradi c9ef5f9b9d lint meh 2021-04-18 11:50:02 +04:30
Aria Moradi 8fbf564177 also backup chapter and category data 2021-04-18 11:48:14 +04:30
Aria Moradi ae0b1a818c [SKIP CI] update troubleshooting guide 2021-04-15 15:57:42 +04:30
Aria Moradi c04cc780b7 update discord invite
CI Publish / Validate Gradle Wrapper (push) Successful in 12s
CI Publish / Build FatJar (push) Failing after 17s
2021-04-15 14:15:49 +04:30
Aria Moradi 71ad1bb6e3 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-04-13 10:54:50 +04:30
Aria Moradi c1be77ee9b Merge pull request #60 from Arias800/patch-1
Fix white screen if the manga doesn't have genre.
2021-04-13 10:54:08 +04:30
Aria Moradi d1fa857ffb avoid creating the jar befre the front-end is copied 2021-04-13 10:53:23 +04:30
Aria Moradi 93fd81b38b [SKIP CI] fix the glob 2021-04-13 10:36:34 +04:30
Aria Moradi 2f116b40b2 fix chapter reading not working 2021-04-13 01:23:09 +04:30
Arias800 b884d34bdf Fix white screen if the manga doesn't have genre. 2021-04-10 19:42:05 +02:00
Aria Moradi 309803368b [SKIP CI] rename workflow 2021-04-10 10:39:47 +04:30
Aria Moradi 19fc5be8f3 [SKIP CI] rename workflow 2021-04-10 10:35:28 +04:30
Aria Moradi c28fac14c0 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-04-10 10:30:47 +04:30
Aria Moradi 66e38de29f check for deps 2021-04-10 10:28:54 +04:30
Aria Moradi 282cb1d3be [CI SKIP] account for windows 2021-04-10 10:16:24 +04:30
Aria Moradi b741ded595 [SKIP CI] improve wording 2021-04-10 10:13:56 +04:30
Aria Moradi 6b290695fc [SKIP CI] update for preview builds 2021-04-10 10:09:25 +04:30
Aria Moradi 4e43c554c0 fix triggers 2021-04-10 10:01:39 +04:30
Aria Moradi 090a72b35f rename repo to preview 2021-04-10 09:57:02 +04:30
Aria Moradi 3fcc269df3 now we can deploy repo too 2021-04-10 09:54:03 +04:30
Aria Moradi 9958e0eb34 bump version 2021-04-10 09:21:42 +04:30
Aria Moradi c5269002a2 Update README.md 2021-04-10 01:18:15 +04:30
Aria Moradi 455a35f8ae front-end UI done
Publish / Validate Gradle Wrapper (push) Successful in 12s
Publish / Build FatJar (push) Failing after 16s
2021-04-10 00:44:13 +04:30
Aria Moradi 0c79f207c3 fist version of a working backup system 2021-04-09 22:59:13 +04:30
Aria Moradi cd16d32a35 first instance of legacy import 2021-04-09 19:21:04 +04:30
Aria Moradi 1989c1eb48 add copyright notice 2021-04-09 18:18:40 +04:30
Aria Moradi f56856529f Merge pull request #55 from txtsd/master
Fix blatant typo
2021-04-06 22:27:53 +04:30
txtsd 52e27a3e39 Fix typo 2021-04-06 14:52:26 +05:30
Aria Moradi 177c971b52 This is better. 2021-04-04 03:37:00 +04:30
Aria Moradi 7a52e19235 Better way of setting it maybe? 2021-04-04 03:23:50 +04:30
Aria Moradi 5171e509a5 remove TOOD 2021-04-04 03:16:02 +04:30
Aria Moradi 975a3b1828 Merge pull request #53 from Syer10/testing_suit
Add initial testing suit
2021-04-04 03:14:27 +04:30
Syer10 c11887fada Allow rootdir to be used as a argument 2021-04-03 18:35:30 -04:00
Syer10 e043cb5690 Use properties to set rootDir so that ConfigManager can use it 2021-04-03 18:12:01 -04:00
Syer10 b2d5354798 dirs -> applicationDirs 2021-04-03 17:25:53 -04:00
Syer10 a211a4143b Revert source id to long 2021-04-03 16:57:03 -04:00
Syer10 c0df7d314b Add initial testing suit 2021-04-03 16:42:13 -04:00
Aria Moradi c8a8ce07e2 fix windows path 2021-04-04 00:52:58 +04:30
Aria Moradi e0e474dfce fixes from inspector 2021-04-03 22:40:14 +04:30
Aria Moradi 7591748811 fixes from inspector 2021-04-03 20:30:28 +04:30
Aria Moradi 884308690f fixes from inspector 2021-04-03 20:08:50 +04:30
Aria Moradi 15bd5b4b7a the new and improved apk installer 2021-04-03 19:47:31 +04:30
Aria Moradi abc3a16ee3 fix typo 2021-04-03 15:40:23 +04:30
Aria Moradi bb09ccf3c0 lint 2021-04-03 15:26:23 +04:30
Aria Moradi ad2ea8095b fixes from the inspector project 2021-04-03 15:09:48 +04:30
Aria Moradi 760d1116a1 prepare to install apk from any source 2021-04-03 13:20:14 +04:30
Aria Moradi 47fcf7eb97 export extensions 2021-04-02 18:10:02 +04:30
Aria Moradi b0e90c2f63 use the correct endpoint 2021-04-02 18:05:42 +04:30
Aria Moradi f502884fdd a partially working legacy import... 2021-04-02 17:57:29 +04:30
Aria Moradi 5ed79523d2 Move getHttpSource to util as it is a util 2021-04-02 14:17:37 +04:30
Aria Moradi da5dd70969 use future properly 2021-04-02 14:06:41 +04:30
Aria Moradi 68e69085df flatten the code 2021-04-02 13:56:38 +04:30
Aria Moradi 640ce8f5d7 add future shorthand 2021-04-02 04:09:48 +04:30
Aria Moradi c960cc1ee5 update comment 2021-04-02 03:24:12 +04:30
Aria Moradi 2b2601aa4a move coroutines to root 2021-04-02 03:14:40 +04:30
Aria Moradi 99a10ec7db improve logging 2021-04-02 03:14:19 +04:30
Aria Moradi 035105adf0 refactor and more 2021-04-02 02:56:16 +04:30
Aria Moradi f983f0e359 Merge pull request #46 from Syer10/future
Implement coroutines
2021-04-02 02:53:59 +04:30
Syer10 769472b24c Implement coroutines 2021-04-01 16:07:35 -04:00
Aria Moradi 8c80ad7575 fix time between extensions list checks 2021-04-01 23:39:00 +04:30
Aria Moradi 63db2e6695 fix HNI-Scantard not installing correctly 2021-04-01 23:34:52 +04:30
Aria Moradi d6d5e97fbd Merge pull request #45 from Syer10/getRepo
Fix getRepo function
2021-04-01 23:15:45 +04:30
Syer10 1ae0a8326e Fix getRepo function 2021-04-01 14:42:13 -04:00
Aria Moradi 57693fef7b clean up 2021-03-30 21:48:05 +04:30
Aria Moradi 5656016700 let's not polute the namespace together 2021-03-30 21:10:41 +04:30
Aria Moradi 90ae180b3e let's not polute the namespace 2021-03-30 21:04:06 +04:30
Aria Moradi 2a3c78d43e refactor 2021-03-30 20:49:54 +04:30
Aria Moradi 11000af718 get gz insead of big big json 2021-03-30 20:41:20 +04:30
Aria Moradi b808121f1d Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-03-30 20:39:56 +04:30
Aria Moradi addadefeb1 some refactor and comments 2021-03-30 20:39:40 +04:30
Aria Moradi 838cd20e57 new build scripts 2021-03-30 20:21:58 +04:30
Aria Moradi 5b9219522d separate jar task from webUI copy task 2021-03-30 20:14:25 +04:30
Aria Moradi caeb4d273d refactor 2021-03-30 17:34:15 +04:30
Aria Moradi 77cf87c989 refactor 2021-03-30 17:21:41 +04:30
Aria Moradi 50c2dbed5d refactor 2021-03-30 16:52:10 +04:30
Aria Moradi 71a9396952 starts of legacy backup support 2021-03-30 01:18:57 +04:30
Aria Moradi bc3ad75328 finished Update support: webUI side 2021-03-29 02:47:51 +04:30
Aria Moradi 077bbc3c38 refactor & support for extension update: Backend 2021-03-29 00:35:58 +04:30
Aria Moradi b1b1abad1d refactor proxy 2021-03-28 20:41:39 +04:30
234 changed files with 13375 additions and 5585 deletions
+3 -2
View File
@@ -9,7 +9,7 @@
# Gradle wrapper
*.jar binary
# Images
# Binary files types
*.webp binary
*.png binary
*.jpg binary
@@ -24,4 +24,5 @@
*.woff binary
*.pyc binary
*.swp binary
*.pdf binary
*.pdf binary
*.exe binary
-25
View File
@@ -1,25 +0,0 @@
#!/bin/bash
git lfs install
#git lfs track "*.zip"
cp ../master/repo/* .
new_jar_build=$(ls *.jar| tail -1)
echo "last jar build file name: $new_jar_build"
new_win32_build=$(ls *.zip| tail -1)
echo "last win32 build file name: $new_win32_build"
cp -f $new_jar_build Tachidesk-latest.jar
cp -f $new_win32_build Tachidesk-latest-win32.zip
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
git status
if [ -n "$(git status --porcelain)" ]; then
git add .
git commit -m "Update repo"
git push
else
echo "No changes to commit"
fi
-20
View File
@@ -1,20 +0,0 @@
#!/bin/bash
# Get last commit message
#last_commit_log=$(git log -1 --pretty=format:"%s")
#echo "last commit log: $last_commit_log"
#
#filter_count=$(echo "$last_commit_log" | grep -e '\[RELEASE CI\]' -e '\[CI RELEASE\]' | wc -c)
#echo "count is: $filter_count"
mkdir -p repo/
cp server/build/Tachidesk-*.jar repo/
cp server/build/Tachidesk-*.zip repo/
ls repo
pwd
#if [ "$filter_count" -gt 0 ]; then
# cp server/build/Tachidesk-*.jar repo/
# cp server/build/Tachidesk-*.zip repo/
#fi
@@ -1,9 +1,6 @@
name: CI
name: CI Pull Request
on:
push:
branches:
- master
pull_request:
jobs:
@@ -19,7 +16,7 @@ jobs:
uses: gradle/wrapper-validation-action@v1
build:
name: Build FatJar
name: Build pull request
needs: check_wrapper
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
runs-on: ubuntu-latest
@@ -30,10 +27,10 @@ jobs:
with:
access_token: ${{ github.token }}
- name: Checkout master branch
- name: Checkout pull request
uses: actions/checkout@v2
with:
ref: master
ref: ${{ github.event.pull_request.head.sha }}
path: master
fetch-depth: 0
@@ -60,12 +57,12 @@ jobs:
**/react/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }}
- name: Build Jar and launch4j
- name: Build and copy webUI, Build Jar
uses: eskatos/gradle-command-action@v1
with:
build-root-directory: master
wrapper-directory: master
arguments: :server:windowsPackage --stacktrace
arguments: :webUI:copyBuild :server:shadowJar --stacktrace
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
+129
View File
@@ -0,0 +1,129 @@
name: CI build
on:
push:
branches:
- master
jobs:
check_wrapper:
name: Validate Gradle Wrapper
runs-on: ubuntu-latest
steps:
- name: Clone repo
uses: actions/checkout@v2
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
build:
name: Build artifacts and deploy preview
needs: check_wrapper
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
runs-on: ubuntu-latest
steps:
- name: Cancel previous runs
uses: styfle/cancel-workflow-action@0.5.0
with:
access_token: ${{ github.token }}
- name: Checkout master branch
uses: actions/checkout@v2
with:
ref: master
path: master
fetch-depth: 0
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Copy CI gradle.properties
run: |
cd master
mkdir -p ~/.gradle
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Download android.jar
run: |
cd master
curl https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
- name: Cache node_modules
uses: actions/cache@v2
with:
path: |
**/react/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }}
- name: Build and copy webUI, Build Jar
uses: eskatos/gradle-command-action@v1
env:
TachideskBuildType: "Preview"
with:
build-root-directory: master
wrapper-directory: master
arguments: :webUI:copyBuild :server:shadowJar --stacktrace
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
# - name: Mock Build and copy webUI, Build Jar
# run: |
# mkdir -p master/server/build
# cd master/server/build
# echo "test" > Tachidesk-v0.3.8-r583.jar
- name: Generate Tag Name
id: GenTagName
run: |
cd master/server/build
genTag=$(ls *.jar | sed -e's/Tachidesk-\|.jar//g')
echo "$genTag"
echo "::set-output name=value::$genTag"
- name: make windows packages
run: |
cd master/scripts
./windows-bundler.sh win32
./windows-bundler.sh win64
# - name: Mock make windows packages
# run: |
# cd master/server/build
# echo test > Tachidesk-v0.3.8-r580-win32.zip
- name: Checkout preview branch
uses: actions/checkout@v2
with:
repository: 'Suwayomi/Tachidesk-preview'
ref: main
path: preview
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
- name: Create Tag
run: |
TAG="${{ steps.GenTagName.outputs.value }}"
echo "tag: $TAG"
cd preview
echo "{ \"latest\": \"$TAG\" }" > index.json
git add index.json
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
git commit -m "Updated to $TAG"
git push origin main
git tag $TAG
git push origin $TAG
- name: Upload Preview Release
uses: ncipollo/release-action@v1
with:
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
artifacts: "master/server/build/*.jar,master/server/build/*.zip"
owner: "Suwayomi"
repo: "Tachidesk-preview"
tag: ${{ steps.GenTagName.outputs.value }}
+15 -43
View File
@@ -1,4 +1,4 @@
name: Publish
name: CI Publish
on:
push:
@@ -18,7 +18,7 @@ jobs:
uses: gradle/wrapper-validation-action@v1
build:
name: Build FatJar
name: Build artifacts and release
needs: check_wrapper
runs-on: ubuntu-latest
@@ -28,10 +28,10 @@ jobs:
with:
access_token: ${{ github.token }}
- name: Checkout master branch
- name: Checkout ${{ github.ref }}
uses: actions/checkout@v2
with:
ref: master
ref: ${{ github.ref }}
path: master
fetch-depth: 0
@@ -56,60 +56,32 @@ jobs:
with:
path: |
**/react/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }}
- name: Build Jar and launch4j
- name: Build and copy webUI, Build Jar
uses: eskatos/gradle-command-action@v1
env:
TachideskBuildType: "Stable"
with:
build-root-directory: master
wrapper-directory: master
arguments: :server:windowsPackage --stacktrace
arguments: :webUI:copyBuild :server:shadowJar --stacktrace
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
- name: Create repo artifacts
- name: make windows packages
run: |
cd master
./.github/scripts/create-repo.sh
cd master/scripts
./windows-bundler.sh win32
./windows-bundler.sh win64
- name: Upload Release
uses: xresloader/upload-to-github-release@master
uses: xresloader/upload-to-github-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
file: "master/repo/*"
file: "master/server/build/*.jar;master/server/build/*.zip"
tags: true
draft: true
verbose: true
# - name: Create Release
# id: create_release
# uses: actions/create-release@v1
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# tag_name: ${{ github.ref }}
# release_name: Release ${{ github.ref }}
# body: |
# Release body
# draft: false
# prerelease: true
#
# - name: Get the Ref
# id: get-ref
# uses: ankitvgupta/ref-to-tag-action@master
# with:
# ref: ${{ github.ref }}
# head_ref: ${{ github.head_ref }}
#
# - name: Get the tag
# run: echo "The tag was ${{ steps.get-ref.outputs.tag }}"
#
# - name: Upload Release
# uses: AButler/upload-release-assets@v2.0
# with:
# files: 'master/repo/*'
# repo-token: ${{ secrets.GITHUB_TOKEN }}
# release-tag: ${{ steps.get-ref.outputs.tag }}
+8
View File
@@ -1,8 +1,16 @@
# Ignore Gradle project-specific cache directory
.gradle
.idea
gradle.properties
# Ignore Gradle build output directory
build
server/src/main/resources/react
server/tmp/
server/tachiserver-data/
# bundle asset downlaods
OpenJDK*.zip
electron-*.zip
rcedit-*
@@ -0,0 +1,18 @@
package xyz.nulldev.ts.config
import net.harawata.appdirs.AppDirsFactory
/*
* 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/. */
val ApplicationRootDir: String
get(): String {
return System.getProperty(
"suwayomi.server.rootDir",
AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
)
}
@@ -7,19 +7,17 @@ package xyz.nulldev.ts.config
* 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 ch.qos.logback.classic.Level
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigRenderOptions
import mu.KotlinLogging
import net.harawata.appdirs.AppDirsFactory
import java.io.File
/**
* Manages app config.
*/
open class ConfigManager {
private val dataRoot by lazy { AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)!! }
private val generatedModules = mutableMapOf<Class<out ConfigModule>, ConfigModule>()
val config by lazy { loadConfigs() }
@@ -27,8 +25,6 @@ open class ConfigManager {
val loadedModules: Map<Class<out ConfigModule>, ConfigModule>
get() = generatedModules
open val appConfigFile: String = "$dataRoot/server.conf"
val logger = KotlinLogging.logger {}
/**
@@ -46,21 +42,34 @@ open class ConfigManager {
*/
fun loadConfigs(): Config {
//Load reference configs
val compatConfig = ConfigFactory.parseResources("compat-reference.conf")
val compatConfig = ConfigFactory.parseResources("compat-reference.conf")
val serverConfig = ConfigFactory.parseResources("server-reference.conf")
val baseConfig =
ConfigFactory.parseMap(
mapOf(
"ts.server.rootDir" to ApplicationRootDir
)
)
//Load user config
val userConfig =
File(appConfigFile).let{
File(ApplicationRootDir, "server.conf").let {
ConfigFactory.parseFile(it)
}
}
val config = ConfigFactory.empty()
.withFallback(baseConfig)
.withFallback(userConfig)
.withFallback(compatConfig)
.withFallback(serverConfig)
.resolve()
// set log level early
if (debugLogsEnabled(config)) {
setLogLevel(Level.DEBUG)
}
logger.debug {
"Loaded config:\n" + config.root().render(ConfigRenderOptions.concise().setFormatted(true))
}
@@ -69,7 +78,7 @@ open class ConfigManager {
}
fun registerModule(module: ConfigModule) {
generatedModules.put(module.javaClass, module)
generatedModules[module.javaClass] = module
}
fun registerModules(vararg modules: ConfigModule) {
@@ -0,0 +1,20 @@
package xyz.nulldev.ts.config
/*
* 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 ch.qos.logback.classic.Level
import com.typesafe.config.Config
import mu.KotlinLogging
import org.slf4j.Logger
fun setLogLevel(level: Level) {
(KotlinLogging.logger(Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger).level = level
}
fun debugLogsEnabled(config: Config)
= System.getProperty("suwayomi.server.debugLogsEnabled", config.getString("server.debugLogsEnabled")).toBoolean()
-12
View File
@@ -6,7 +6,6 @@ plugins {
repositories {
mavenCentral()
jcenter()
maven {
url = uri("https://jitpack.io")
}
@@ -30,23 +29,12 @@ dependencies {
// Javassist
compileOnly( "org.javassist:javassist:3.27.0-GA")
// Coroutines
val kotlinx_coroutines_version = "1.4.2"
compileOnly( "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_coroutines_version")
compileOnly( "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$kotlinx_coroutines_version")
// XML
compileOnly( group= "xmlpull", name= "xmlpull", version= "1.1.3.1")
// Config API
implementation(project(":AndroidCompat:Config"))
// dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
compileOnly("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
// APK parser
compileOnly("net.dongliu:apk-parser:2.6.10")
// APK sig verifier
compileOnly("com.android.tools.build:apksig:4.2.0-alpha13")
+1 -2
View File
@@ -15,7 +15,7 @@ Write-Output "Getting required Android.jar..."
Remove-Item -Recurse -Force "tmp" -ErrorAction SilentlyContinue | Out-Null
New-Item -ItemType Directory -Force -Path "tmp" | Out-Null
$androidEncoded = (Invoke-WebRequest -Uri "https://android.googlesource.com/platform/prebuilts/sdk/+/3b8a524d25fa6c3d795afb1eece3f24870c60988/27/public/android.jar?format=TEXT").content
$androidEncoded = (Invoke-WebRequest -Uri "https://android.googlesource.com/platform/prebuilts/sdk/+/3b8a524d25fa6c3d795afb1eece3f24870c60988/27/public/android.jar?format=TEXT" -UseBasicParsing).content
$android_jar = (Get-Location).Path + "\tmp\android.jar"
@@ -87,7 +87,6 @@ function Dedupe($path)
}
Dedupe "AndroidCompat/src/main/java"
Dedupe "server/src/main/java"
Dedupe "server/src/main/kotlin"
Write-Output "Copying Android.jar to library folder..."
+17 -9
View File
@@ -8,8 +8,19 @@
# This is a bash script to create android.jar stubs
for dep in "curl" "base64" "zip"
do
which $dep >/dev/null 2>&1 || { echo >&2 "Error: This script needs $dep installed."; abort=yes; }
done
if [ $abort = yes ]; then
echo "Some of the dependencies didn't exist. Aborting."
exit 1
fi
# foolproof against running from AndroidCompat dir instead of running from project root
if [ "$(basename $(pwd))" = "AndroidCompat" ]; then
if [ "$(basename "$(pwd)")" = "AndroidCompat" ]; then
cd ..
fi
@@ -48,7 +59,7 @@ zip --delete android.jar javax/*
echo "Removing java..."
zip --delete android.jar java/*
echo "Removing overriden classes..."
echo "Removing overridden classes..."
zip --delete android.jar android/app/Application.class
zip --delete android.jar android/app/Service.class
zip --delete android.jar android/net/Uri.class
@@ -57,12 +68,12 @@ zip --delete android.jar android/os/Environment.class
zip --delete android.jar android/text/format/Formatter.class
zip --delete android.jar android/text/Html.class
# Dedup overriden Android classes
# Dedup overridden Android classes
ABS_JAR="$(realpath android.jar)"
function dedup() {
pushd "$1"
CLASSES="$(find * -type f)"
echo "$CLASSES" | while read class
CLASSES="$(find ./* -type f)"
echo "$CLASSES" | while read -r class
do
NAME="${class%.*}"
echo "Processing class: $NAME"
@@ -71,13 +82,10 @@ function dedup() {
popd
}
pushd ..
popd
dedup AndroidCompat/src/main/java
dedup server/src/main/java
dedup server/src/main/kotlin
popd
popd
echo "Copying Android.jar to library folder..."
mv tmp/android.jar AndroidCompat/lib
@@ -1,11 +1,20 @@
package com.squareup.duktape;
/*
* Copyright (C) Contributors to the Suwayomi project
* Copyright (C) 2015 Square, Inc.
*
* 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/. */
* 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 com.squareup.duktape;
import kotlin.NotImplementedError;
@@ -64,18 +73,18 @@ public final class Duktape implements Closeable, AutoCloseable {
throw new NotImplementedError("Not implemented!");
}
// /**
// * Attaches to a global JavaScript object called {@code name} that implements {@code type}.
// * {@code type} defines the interface implemented in JavaScript that will be accessible to Java.
// * {@code type} must be an interface that does not extend any other interfaces, and cannot define
// * any overloaded methods.
// * <p>Methods of the interface may return {@code void} or any of the following supported argument
// * types: {@code boolean}, {@link Boolean}, {@code int}, {@link Integer}, {@code double},
// * {@link Double}, {@link String}.
// */
// public synchronized <T> T get(final String name, final Class<T> type) {
// throw new NotImplementedError("Not implemented!");
// }
/**
* Attaches to a global JavaScript object called {@code name} that implements {@code type}.
* {@code type} defines the interface implemented in JavaScript that will be accessible to Java.
* {@code type} must be an interface that does not extend any other interfaces, and cannot define
* any overloaded methods.
* <p>Methods of the interface may return {@code void} or any of the following supported argument
* types: {@code boolean}, {@link Boolean}, {@code int}, {@link Integer}, {@code double},
* {@link Double}, {@link String}.
*/
public synchronized <T> T get(final String name, final Class<T> type) {
throw new NotImplementedError("Not implemented!");
}
/**
* Release the native resources associated with this object. You <strong>must</strong> call this
@@ -1,13 +1,6 @@
package com.squareup.duktape;
/*
* 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/. */
// part of tachiyomi-extensions which was originally licensed under Apache License Version 2.0
/* part of tachiyomi-extensions which is licensed under Apache License Version 2.0 */
import java.io.Closeable;
import java.io.IOException;
@@ -22,7 +22,7 @@ data class InstalledPackage(val root: File) {
val icon = File(root, "icon.png")
val info: PackageInfo
get() = ApkParsers.getMetaInfo(apk).toPackageInfo(root, apk).also {
get() = ApkParsers.getMetaInfo(apk).toPackageInfo(apk).also {
val parsed = ApkFile(apk)
val dbFactory = DocumentBuilderFactory.newInstance()
val dBuilder = dbFactory.newDocumentBuilder()
@@ -82,12 +82,14 @@ data class InstalledPackage(val root: File) {
}
}
private fun NodeList.toList(): List<Node> {
val out = mutableListOf<Node>()
companion object {
fun NodeList.toList(): List<Node> {
val out = mutableListOf<Node>()
for(i in 0 until length)
out += item(i)
for (i in 0 until length)
out += item(i)
return out
return out
}
}
}
@@ -6,7 +6,7 @@ import android.content.pm.PackageInfo
import net.dongliu.apk.parser.bean.ApkMeta
import java.io.File
fun ApkMeta.toPackageInfo(root: File, apk: File): PackageInfo {
fun ApkMeta.toPackageInfo(apk: File): PackageInfo {
return PackageInfo().also {
it.packageName = packageName
it.versionCode = versionCode.toInt()
+5
View File
@@ -0,0 +1,5 @@
# Code Of Conduct
- Don't be a dick.
# expanding the code of conduct!
The contents of this document is up for debate and improvement! Discussions on discord.
+52
View File
@@ -0,0 +1,52 @@
# Contributing
## Where should I start?
Checkout [This Kanban Board](https://github.com/Suwayomi/Tachidesk/projects/1) to see the rough development roadmap.
**Note to potential contributors:** Notify the developers on Suwayomi discord (#programming channel) 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.
## How does Tachidesk work?
This project has two components:
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
2. **webUI:** A react SPA(`create-react-app`) project that works with the server to do the presentation.
## Why a web app?
This structure is chosen to
- Achieve the maximum multi-platform-ness
- Gives the ability to acces Tachidesk from a remote web browser e.g. your phone, tablet or smart TV
- Eaise development of alternative user intefaces for Tachidesk
## User Interfaces for Tachidesk server
Currently, there are three known interfaces for Tachidesk:
1. [webUI](https://github.com/Suwayomi/Tachidesk/tree/master/webUI/react): The react SPA that Tachidesk is traditionally shipped with.
2. [TachideskJUI](https://github.com/Suwayomi/TachideskJUI): A Jetbrains Compose Native app, re-uses components made for the upcoming Tachiyomi 1.x
3. [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js, in super early stages of development.
## Building from source
### Prerequisites
You need these software packages installed in order to build the project
### Server
- Java Development Kit and Java Runtime Environment version 8 or newer(both Oracle JDK and OpenJDK works)
- Android stubs jar
- Manual download: Download [android.jar](https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
- Automated download: Run `AndroidCompat/getAndroid.sh`(MacOS/Linux) or `AndroidCompat/getAndroid.ps1`(Windows) from project's root directory to download and rebuild the jar file from Google's repository.
### webUI
- Nodejs LTS or latest
- Yarn
- Git
### building the full-blown jar
Run `./gradlew :webUI:copyBuild server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
### building without `webUI` bundled(server only)
Delete the `server/src/main/resources/react` directory if exists from previous runs, then run `./gradlew server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
### building the Windows package
First Build the jar, then cd into the `scripts` directory and run `./windows<bits>-bundler.sh` (or `./windows<bits>-bundler.ps1` if you are on windows), the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win64.zip`.
## Running in development mode
First satisfy [the prerequisites](#prerequisites)
### server
run `./gradlew :server:run --stacktrace` to run the server
### webUI
How to do it is described in `webUI/react/README.md` but for short,
first cd into `webUI/react` then run `yarn` to install the node modules(do this only once)
then `yarn start` to start the development server, if a new browser window doesn't get opened automatically,
then open `http://127.0.0.1:3000` in a modern browser. This is a `create-react-app` project
and supports HMR and all the other goodies you'll need.
+37 -53
View File
@@ -1,12 +1,21 @@
![image](https://github.com/Suwayomi/Tachidesk/raw/master/server/src/main/resources/icon/faviconlogo.png)
| Build | Stable | Preview | Support Server |
|-------|----------|---------|---------|
| ![CI](https://github.com/Suwayomi/Tachidesk/actions/workflows/build_push.yml/badge.svg) | [![stable release](https://img.shields.io/github/release/Suwayomi/Tachidesk.svg?maxAge=3600&label=download)](https://github.com/Suwayomi/Tachidesk/releases) | [![preview](https://img.shields.io/badge/dynamic/json?url=https://github.com/Suwayomi/Tachidesk-preview/raw/main/index.json&label=download&query=$.latest&color=blue)](https://github.com/Suwayomi/Tachidesk-preview/releases/latest) | [![Discord](https://img.shields.io/discord/801021177333940224.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/DDZdqZWaHA) |
# Tachidesk
<img src="https://github.com/Suwayomi/Tachidesk/raw/master/server/src/main/resources/icon/faviconlogo.png" alt="drawing" width="200"/>
A free and open source manga reader that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
Tachidesk is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it.
Tachidesk is an independent Tachiyomi compatible software and is **not a Fork of** Tachiyomi.
Tachidesk 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 read and write Tachiyomi compatible backups and syncing is a planned feature.
**Tachidesk needs serious front-end dev help for it's reader and other parts, if you like the app and want to see it become better please don't hesitate to contribute some code!**
## Is this application usable? Should I test it?
Here is a list of current features:
@@ -15,23 +24,26 @@ Here is a list of current features:
- Searching and browsing installed sources.
- A decent chapter reader.
- Ability to download Mangas for offline read(This partially works)
- Backup and restore support powered by Tachiyomi Legacy Backups
**Note:** Keep in mind that Tachidesk is alpha software and can break rarely and/or with each update, so you may have to delete your data to fix it. See [General troubleshooting](#general-troubleshooting) and [Support and help](#support-and-help) if it happens.
Anyways, for more info checkout [finished milestone #1](https://github.com/Suwayomi/Tachidesk/issues/2) and [milestone #2](https://github.com/Suwayomi/Tachidesk/projects/1) to see what's implemented in more detail.
**Note:** Keep in mind that Tachidesk is alpha software and can break rarely and/or with each update. See [Troubleshooting](https://github.com/Suwayomi/Tachidesk/wiki/Troubleshooting) if it happens.
## Downloading and Running the app
### All Operating Systems
You should have The Java Runtime Environment(JRE) 8 or newer and a modern browser installed. Also an internet connection is required as almost everything this app does is downloading stuff.
You should have The Java Runtime Environment(JRE) 8 or newer and a modern browser installed(Google is your friend for seeking assitance). Also an internet connection is required as almost everything this app does is downloading stuff.
Download the latest jar release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases).
Download the latest "Stable" jar release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview jar build from [the preview repository](https://github.com/Suwayomi/Tachidesk-preview/releases).
Double click on the jar file or run `java -jar Tachidesk-vX.Y.Z-rxxx.jar` from a Terminal/Command Prompt window to run the app which will open a new browser window automatically. Also the System Tray Icon is your friend if you need to open the browser window again or close Tachidesk.
Double click on the jar file or run `java -jar Tachidesk-vX.Y.Z-rxxx.jar` (or `java -jar Tachidesk-latest.jar` if you have the latest preview) from a Terminal/Command Prompt window to run the app which will open a new browser window automatically. Also the System Tray Icon is your friend if you need to open the browser window again or close Tachidesk.
### Windows
Download the latest win32 release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases).
Download the latest "Stable" win32 or win64 (depending on your system, usually you want win64) release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Tachidesk-preview/releases).
The Windows specific build has java bundled inside, so you don't have to install java to use it. Unzip `Tachidesk-vX.Y.Z-rxxx-win32.zip` and run `server.exe`. The rest works like the previous section.
The Windows specific build has java bundled inside, so you don't have to install java to use it. Unzip `Tachidesk-vX.Y.Z-rxxx-win64.zip` and run one of the Launcher files depending on what you want(see bellow). The rest works like the previous section.
#### Windows Launchers
- `Tachidesk Electron Launcher.bat`: Launches Tachidesk inside Electron as a desktop applicaton
- `Tachidesk Browser Launcher.bat`: Launches Tachidesk in a browser window
- `Tachidesk Debug Launcher.bat`: Launches Tachidesk with debug logs attached. If Tachidesk doesn't work for you, running this can give you insight into why.
### Arch Linux
You can install Tachidesk from the AUR
@@ -40,53 +52,25 @@ yay -S tachidesk
```
### Docker
Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
Check our Offical Docker release [Tachidesk Container](https://github.com/orgs/Suwayomi/packages/container/package/tachidesk) or use [arbuilder's](https://github.com/arbuilder/Tachidesk-docker) tachidesk docker repo for installation. Source code for our container is available at [docker-tachidesk](https://github.com/Suwayomi/docker-tachidesk). By default the server will be running on http://localhost:4567 open this url in your browser.
## General troubleshooting
If the app breaks try deleting the directory below and re-running the app (**This will delete all your data!**) and if the problem persists open an issue.
Install from the command line:
```
$ docker pull ghcr.io/suwayomi/tachidesk
```
Run Container from the command line:
```
$ docker run -p 4567:4567 ghcr.io/suwayomi/tachidesk
```
On Mac OS X : `/Users/<Account>/Library/Application Support/Tachidesk`
### Using Tachidesk Remotely
You can run Tachidesk on your computer or a server and connect to it remotely through the web interface with a web browser on any device including a mobile or tablet or even your smart TV!, this method of using Tachidesk is only recommended if you are a power user and know what you are doing.
On Windows XP : `C:\Documents and Settings\<Account>\Application Data\Local Settings\Tachidesk`
## Troubleshooting and Support
See [this troubleshooting wiki page](https://github.com/Suwayomi/Tachidesk/wiki/Troubleshooting).
On Windows 7 and later : `C:\Users\<Account>\AppData\Local\Tachidesk`
On Unix/Linux : `/home/<account>/.local/share/Tachidesk`
## Support and help
Join Tachidesk's [discord server](https://discord.gg/wgPyb7hE5d) to hang out with the community and receive support and help.
## How does it work?
This project has two components:
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
2. **webUI:** A react SPA project that works with the server to do the presentation.
## Building from source
### Prerequisite: Get Android stubs jar
#### Manual download
Download [android.jar](https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
#### Automated download(needs `bash`, `curl`, `base64`, `zip` to work)
Run `AndroidCompat/getAndroid.sh`(MacOS/Linux) or `AndroidCompat/getAndroid.ps1`(Windows) from project's root directory to download and rebuild the jar file from Google's repository.
### Prerequisite: Software dependencies
You need this software packages installed in order to build this project:
- Java Development Kit and Java Runtime Environment version 8 or newer(both Oracle JDK and OpenJDK works)
- Nodejs LTS or latest
- Yarn
### building the full-blown jar
Run `./gradlew server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
### building without `webUI` bundled
Delete the `server/src/main/resources/react` directory if exists from previous runs, then run `./gradlew server:shadowJar -x :webUI:copyBuild`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
### building the Windows package
Run `./gradlew windowsPackage`, the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win32.zip`.
## Running for development purposes
### `server` module
Follow [Get Android stubs jar](#prerequisite-get-android-stubs-jar) then run `./gradlew :server:run -x :webUI:copyBuild --stacktrace` to run the server
### `webUI` module
How to do it is described in `webUI/react/README.md` but for short,
first cd into `webUI/react` then run `yarn` to install the node modules(do this only once)
then `yarn start` to start the client if a new browser window doesn't start automatically,
then open `http://127.0.0.1:3000` in a modern browser. This is a `create-react-app` project
and supports HMR and all the other goodies you'll need.
## Contributing and Technical info
See [CONTRIBUTING.md](./CONTRIBUTING.md).
## Credit
This project is a spiritual successor of [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server), Many of the ideas and the groundwork adopted in this project comes from TachiWeb.
+29 -31
View File
@@ -1,33 +1,30 @@
import org.jetbrains.kotlin.config.KotlinCompilerVersion
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.jetbrains.kotlin.jvm") version "1.4.21" apply false // Also in buildSrc Config.kt
id("java")
kotlin("jvm") version "1.4.32"
}
allprojects {
group = "xyz.nulldev.ts"
group = "suwayomi"
version = "1.0"
repositories {
jcenter()
mavenCentral()
maven("https://maven.google.com/")
maven("https://jitpack.io")
maven("https://oss.sonatype.org/content/repositories/snapshots/")
maven("https://dl.bintray.com/inorichi/maven")
maven("https://dl.google.com/dl/android/maven2/")
}
}
val javaProjects = listOf(
val projects = listOf(
project(":AndroidCompat"),
project(":AndroidCompat:Config"),
project(":server")
)
configure(javaProjects) {
apply(plugin = "java")
configure(projects) {
apply(plugin = "org.jetbrains.kotlin.jvm")
java {
@@ -35,51 +32,52 @@ configure(javaProjects) {
targetCompatibility = JavaVersion.VERSION_1_8
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
tasks.withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}
dependencies {
// Kotlin
implementation(kotlin("stdlib", KotlinCompilerVersion.VERSION))
implementation(kotlin("stdlib", KotlinCompilerVersion.VERSION))
testImplementation(kotlin("test", version = "1.4.21"))
}
}
implementation(kotlin("stdlib-jdk8"))
implementation(kotlin("reflect"))
testImplementation(kotlin("test"))
// coroutines
val coroutinesVersion = "1.4.3"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
configure(listOf(
project(":AndroidCompat"),
project(":server"),
project(":AndroidCompat:Config")
)) {
dependencies {
// Dependency Injection
implementation("org.kodein.di:kodein-di-conf-jvm:7.1.0")
implementation("org.kodein.di:kodein-di-conf-jvm:7.5.0")
// Logging
implementation("org.slf4j:slf4j-api:1.7.30")
implementation("ch.qos.logback:logback-classic:1.2.3")
implementation("io.github.microutils:kotlin-logging:2.0.3")
implementation("io.github.microutils:kotlin-logging:2.0.6")
// RxJava
// ReactiveX
implementation("io.reactivex:rxjava:1.3.8")
implementation("io.reactivex:rxkotlin:1.0.0")
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
// JSoup
implementation("org.jsoup:jsoup:1.13.1")
// Kotlin
implementation(kotlin("reflect", version = "1.4.21"))
// dependency of :AndroidCompat:Config
implementation("com.typesafe:config:1.4.0")
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.0")
implementation("net.harawata:appdirs:1.2.1")
// dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
implementation("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
// APK parser
implementation("net.dongliu:apk-parser:2.6.10")
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
@@ -0,0 +1 @@
start "" jre/bin/javaw -jar Tachidesk.jar
@@ -0,0 +1 @@
jre\bin\java -Dsuwayomi.server.debugLogsEnabled=true -jar Tachidesk.jar
@@ -0,0 +1 @@
jre\bin\javaw "-Dsuwayomi.server.webInterface=electron" "-Dsuwayomi.server.electronPath=electron/electron.exe" -jar Tachidesk.jar
+5
View File
@@ -0,0 +1,5 @@
#include <stdlib.h>
int main() {
system("start jre\\bin\\javaw -jar Tachidesk.jar");
}
+3
View File
@@ -0,0 +1,3 @@
# Building `Tachidesk Launcher.exe`
1. compile `Tachidesk Launcher.c` statically with MSVC compiler.
2. Add `server/src/main/resources/icon/faviconlogo.ico` into the exe with `rcedit` from the electron project: `rcedit "Tachidesk Launcher.exe" --set-icon faviconlogo.ico`
+83
View File
@@ -0,0 +1,83 @@
#!/bin/bash
# Copyright (C) Contributors to the Suwayomi project
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
electron_version="v12.0.9"
if [ $1 = "win32" ]; then
jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
arch="win32"
electron="electron-$electron_version-win32-ia32.zip"
else
jre="OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip"
arch="win64"
electron="electron-$electron_version-win32-x64.zip"
fi
jre_dir="jdk8u292-b10-jre"
echo "creating windows bundle"
jar=$(ls ../server/build/Tachidesk-*.jar)
jar_name=$(echo $jar | cut -d'/' -f4)
release_name=$(echo $jar_name | cut -d'.' -f4 --complement)-$arch
# make release dir
mkdir $release_name
echo "Dealing with jre..."
if [ ! -f $jre ]; then
curl -L "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/$jre" -o $jre
fi
unzip $jre
mv $jre_dir $release_name/jre
echo "Dealing with electron"
if [ ! -f $electron ]; then
curl -L "https://github.com/electron/electron/releases/download/$electron_version/$electron" -o $electron
fi
unzip $electron -d $release_name/electron
# change electron's icon
rcedit="rcedit-x86.exe"
if [ ! -f $rcedit ]; then
curl -L "https://github.com/electron/rcedit/releases/download/v1.1.1/$rcedit" -o $rcedit
fi
# check if running under github actions
if [ $CI = true ]; then
# change electron executable's icon
sudo dpkg --add-architecture i386
wget -qO - https://dl.winehq.org/wine-builds/winehq.key | sudo apt-key add -
sudo add-apt-repository ppa:cybermax-dexter/sdl2-backport
sudo apt-add-repository "deb https://dl.winehq.org/wine-builds/ubuntu $(lsb_release -cs) main"
sudo apt install --install-recommends winehq-stable
fi
# this script assumes that wine is installed here on out
WINEARCH=win32 wine $rcedit $release_name/electron/electron.exe --set-icon ../server/src/main/resources/icon/faviconlogo.ico
# copy artifacts
cp $jar $release_name/Tachidesk.jar
#cp "resources/Tachidesk Launcher-$arch.exe" "$release_name/Tachidesk Launcher.exe"
cp "resources/Tachidesk Browser Launcher.bat" $release_name
cp "resources/Tachidesk Debug Launcher.bat" $release_name
cp "resources/Tachidesk Electron Launcher.bat" $release_name
zip_name=$release_name.zip
zip -9 -r $zip_name $release_name
rm -rf $release_name
# clean up from possible previous runs
if [ -f ../server/build/$zip_name ]; then
rm ../server/build/$zip_name
fi
mv $zip_name ../server/build/
+122 -152
View File
@@ -1,100 +1,87 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jmailen.gradle.kotlinter.tasks.FormatTask
import org.jmailen.gradle.kotlinter.tasks.LintTask
import java.io.BufferedReader
import java.time.Instant
plugins {
// id("org.jetbrains.kotlin.jvm") version "1.4.21"
application
id("com.github.johnrengelman.shadow") version "6.1.0"
id("org.jmailen.kotlinter") version "3.3.0"
id("edu.sc.seis.launch4j") version "2.4.9"
id("com.github.johnrengelman.shadow") version "7.0.0"
id("org.jmailen.kotlinter") version "3.4.3"
id("de.fuerstenau.buildconfig") version "1.1.8"
}
val TachideskVersion = "v0.2.7"
repositories {
mavenCentral()
jcenter()
maven {
url = uri("https://repo1.maven.org/maven2/")
}
maven {
url = uri("https://jitpack.io")
}
}
dependencies {
// implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
// okhttp
val okhttpVersion = "4.9.1" // 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:2.10.0")
// Javalin api
implementation("io.javalin:javalin:3.13.6")
// jackson version is tied to javalin, ref: `io.javalin.core.util.OptionalDependency`
implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.10.3")
// Exposed ORM
val exposedVersion = "0.31.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")
// tray icon
implementation("com.dorkbox:SystemTray:4.1")
implementation("com.dorkbox:Utilities:1.9")
// 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:4.9.1")
implementation("io.reactivex:rxjava:1.3.8")
implementation("org.jsoup:jsoup:1.13.1")
implementation("com.google.code.gson:gson:2.8.6")
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
// 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("com.github.inorichi.injekt:injekt-core:65b0440")
val okhttp_version = "4.10.0-RC1"
implementation("com.squareup.okhttp3:okhttp:$okhttp_version")
implementation("com.squareup.okhttp3:logging-interceptor:$okhttp_version")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttp_version")
implementation("com.squareup.okio:okio:2.9.0")
// retrofit
val retrofit_version = "2.9.0"
implementation("com.squareup.retrofit2:retrofit:$retrofit_version")
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0")
implementation("com.squareup.retrofit2:converter-gson:$retrofit_version")
implementation("com.squareup.retrofit2:adapter-rxjava:$retrofit_version")
// reactivex
implementation("io.reactivex:rxjava:1.3.8")
// implementation("io.reactivex:rxandroid:1.2.1")
// implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
// implementation("com.github.pwittchen:reactivenetwork:0.13.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.0")
implementation("com.google.code.gson:gson:2.8.6")
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
implementation("org.jsoup:jsoup:1.13.1")
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
val coroutinesVersion = "1.3.9"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
// dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
implementation("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
// api
implementation("io.javalin:javalin:3.12.0")
implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3")
// Exposed ORM
val exposed_version = "0.28.1"
implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
implementation("org.jetbrains.exposed:exposed-dao:$exposed_version")
implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
implementation("com.h2database:h2:1.4.199")
// tray icon
implementation("com.dorkbox:SystemTray:3.17")
// AndroidCompat
implementation(project(":AndroidCompat"))
implementation(project(":AndroidCompat:Config"))
// uncomment to test extensions directly
// implementation(fileTree("lib/"))
// Testing
testImplementation(kotlin("test-junit5"))
}
val name = "ir.armor.tachidesk.Main"
val MainClass = "suwayomi.MainKt"
application {
mainClass.set(name)
mainClass.set(MainClass)
// Required by ShadowJar.
mainClassName = name
// for testing electron
// applicationDefaultJvmArgs = listOf(
// "-Dsuwayomi.tachidesk.webInterface=electron",
// "-Dsuwayomi.tachidesk.electronPath=/usr/bin/electron"
// )
}
sourceSets {
@@ -105,9 +92,14 @@ sourceSets {
}
}
val TachideskRevision = Runtime
// should be bumped with each stable release
val tachideskVersion = "v0.4.0"
// counts commit count on master
val tachideskRevision = runCatching {
Runtime
.getRuntime()
.exec("git rev-list master --count")
.exec("git rev-list HEAD --count")
.let { process ->
process.waitFor()
val output = process.inputStream.use {
@@ -117,95 +109,73 @@ val TachideskRevision = Runtime
"r" + output.trim()
}
}.getOrDefault("r0")
buildConfig {
clsName = "BuildConfig"
packageName = "suwayomi.server"
buildConfigField("String", "NAME", rootProject.name)
buildConfigField("String", "VERSION", tachideskVersion)
buildConfigField("String", "REVISION", tachideskRevision)
buildConfigField("String", "BUILD_TYPE", if (System.getenv("TachideskBuildType") == "Stable") "Stable" else "Preview")
buildConfigField("long", "BUILD_TIME", Instant.now().epochSecond.toString())
buildConfigField("String", "GITHUB", "https://github.com/Suwayomi/Tachidesk")
buildConfigField("String", "DISCORD", "https://discord.gg/DDZdqZWaHA")
}
tasks {
jar {
shadowJar {
manifest {
attributes(
mapOf(
"Main-Class" to "com.example.MainKt", //will make your jar (produced by jar task) runnable
"ImplementationTitle" to project.name,
"Implementation-Version" to project.version)
"Main-Class" to MainClass,
"Implementation-Title" to rootProject.name,
"Implementation-Vendor" to "The Suwayomi Project",
"Specification-Version" to tachideskVersion,
"Implementation-Version" to tachideskRevision
)
)
}
archiveBaseName.set(rootProject.name)
archiveVersion.set(tachideskVersion)
archiveClassifier.set(tachideskRevision)
}
withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf(
"-Xopt-in=kotlin.RequiresOptIn",
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi"
)
}
}
shadowJar {
manifest.inheritFrom(jar.get().manifest) //will make your shadowJar (produced by jar task) runnable
archiveBaseName.set("Tachidesk")
archiveVersion.set(TachideskVersion)
archiveClassifier.set(TachideskRevision)
test {
useJUnit()
}
withType<ShadowJar> {
destinationDirectory.set(File("$rootDir/server/build"))
dependsOn("formatKotlin", "lintKotlin")
}
named("run") {
dependsOn("formatKotlin", "lintKotlin")
}
named<Copy>("processResources") {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
mustRunAfter(":webUI:copyBuild")
}
withType<LintTask> {
source(files("src/kotlin"))
}
withType<FormatTask> {
source(files("src/kotlin"))
}
}
launch4j { //used for windows
mainClassName = name
bundledJrePath = "jre"
bundledJre64Bit = true
jreMinVersion = "8"
outputDir = "Tachidesk-$TachideskVersion-$TachideskRevision-win32"
icon = "${projectDir}/src/main/resources/icon/faviconlogo.ico"
jar = "${projectDir}/build/Tachidesk-$TachideskVersion-$TachideskRevision.jar"
}
tasks.register<Zip>("windowsPackage") {
from(fileTree("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32"))
destinationDirectory.set(File("$buildDir"))
archiveFileName.set("Tachidesk-$TachideskVersion-$TachideskRevision-win32.zip")
dependsOn("windowsPackageWorkaround2")
}
tasks.register<Delete>("windowsPackageWorkaround2") {
delete(
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/jre",
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/lib",
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/server.exe",
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32"
)
dependsOn("windowsPackageWorkaround")
}
tasks.register<Copy>("windowsPackageWorkaround") {
from("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
into("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
dependsOn("deleteUnwantedJreDir")
}
tasks.register<Delete>("deleteUnwantedJreDir") {
delete(
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/jdk8u282-b08-jre"
)
dependsOn("addJreToDistributable")
}
tasks.register<Copy>("addJreToDistributable") {
from(zipTree("$buildDir/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip"))
into("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
eachFile {
path = path.replace(".*-jre".toRegex(),"jre")
}
dependsOn("downloadJre")
dependsOn("createExe")
}
tasks.register<de.undercouch.gradle.tasks.download.Download>("downloadJre") {
src("https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u282-b08/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip")
dest(buildDir)
overwrite(false)
onlyIfModified(true)
}
tasks.withType<ShadowJar> {
destinationDir = File("$rootDir/server/build")
dependsOn("lintKotlin")
}
tasks.named("processResources") {
dependsOn(":webUI:copyBuild")
}
tasks.named("run") {
dependsOn("formatKotlin", "lintKotlin")
}
@@ -1,90 +0,0 @@
package eu.kanade.tachiyomi.extension.api
/*
* 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 android.content.Context
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import ir.armor.tachidesk.database.dataclass.ExtensionDataClass
// import kotlinx.coroutines.Dispatchers
// import kotlinx.coroutines.withContext
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
// import uy.kohesive.injekt.injectLazy
internal class ExtensionGithubApi {
// private val preferences: PreferencesHelper by injectLazy()
suspend fun findExtensions(): List<Extension.Available> {
val service: ExtensionGithubService = ExtensionGithubService.create()
val response = service.getRepo()
return parseResponse(response)
}
// suspend fun checkForUpdates(): List<Extension.Installed> {
// val extensions = fin dExtensions()
//
// // preferences.lastExtCheck().set(Date().time)
//
// val installedExtensions = ExtensionLoader.loadExtensions(context)
// .filterIsInstance<LoadResult.Success>()
// .map { it.extension }
//
// val extensionsWithUpdate = mutableListOf<Extension.Installed>()
// for (installedExt in installedExtensions) {
// val pkgName = installedExt.pkgName
// val availableExt = extensions.find { it.pkgName == pkgName } ?: continue
//
// val hasUpdate = availableExt.versionCode > installedExt.versionCode
// if (hasUpdate) {
// extensionsWithUpdate.add(installedExt)
// }
// }
//
// return extensionsWithUpdate
// }
private fun parseResponse(json: JsonArray): List<Extension.Available> {
return json
.filter { element ->
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
val libVersion = versionName.substringBeforeLast('.').toDouble()
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
}
.map { element ->
val name = element.jsonObject["name"]!!.jsonPrimitive.content.substringAfter("Tachiyomi: ")
val pkgName = element.jsonObject["pkg"]!!.jsonPrimitive.content
val apkName = element.jsonObject["apk"]!!.jsonPrimitive.content
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int
val lang = element.jsonObject["lang"]!!.jsonPrimitive.content
val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
}
}
fun getApkUrl(extension: Extension.Available): String {
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
}
fun getApkUrl(extension: ExtensionDataClass): String {
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
}
companion object {
const val BASE_URL = "https://raw.githubusercontent.com/"
const val REPO_URL_PREFIX = "${BASE_URL}inorichi/tachiyomi-extensions/repo"
}
}
@@ -1,46 +0,0 @@
package eu.kanade.tachiyomi.extension.api
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import okhttp3.MediaType.Companion.toMediaType
import retrofit2.Retrofit
import retrofit2.http.GET
import uy.kohesive.injekt.injectLazy
// import uy.kohesive.injekt.injectLazy
/**
* Used to get the extension repo listing from GitHub.
*/
interface ExtensionGithubService {
companion object {
private val client by lazy {
val network: NetworkHelper by injectLazy()
network.client.newBuilder()
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.header("Content-Encoding", "gzip")
.header("Content-Type", "application/json")
.build()
}
.build()
}
fun create(): ExtensionGithubService {
val adapter = Retrofit.Builder()
.baseUrl(ExtensionGithubApi.BASE_URL)
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
.client(client)
.build()
return adapter.create(ExtensionGithubService::class.java)
}
}
@GET("${ExtensionGithubApi.REPO_URL_PREFIX}/index.json.gz")
suspend fun getRepo(): JsonArray
}
@@ -1,47 +0,0 @@
package eu.kanade.tachiyomi.extension.model
import eu.kanade.tachiyomi.source.Source
sealed class Extension {
abstract val name: String
abstract val pkgName: String
abstract val versionName: String
abstract val versionCode: Int
abstract val lang: String?
abstract val isNsfw: Boolean
data class Installed(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Int,
override val lang: String,
override val isNsfw: Boolean,
val sources: List<Source>,
val hasUpdate: Boolean = false,
val isObsolete: Boolean = false,
val isUnofficial: Boolean = false
) : Extension()
data class Available(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Int,
override val lang: String,
override val isNsfw: Boolean,
val apkName: String,
val iconUrl: String
) : Extension()
data class Untrusted(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Int,
val signatureHash: String,
override val lang: String? = null,
override val isNsfw: Boolean = false
) : Extension()
}
@@ -1,9 +0,0 @@
package eu.kanade.tachiyomi.extension.model
enum class InstallStep {
Pending, Downloading, Installing, Installed, Error;
fun isCompleted(): Boolean {
return this == Installed || this == Error
}
}
@@ -1,10 +0,0 @@
package eu.kanade.tachiyomi.extension.model
sealed class LoadResult {
class Success(val extension: Extension.Installed) : LoadResult()
class Untrusted(val extension: Extension.Untrusted) : LoadResult()
class Error(val message: String? = null) : LoadResult() {
constructor(exception: Throwable) : this(exception.message)
}
}
@@ -1,234 +0,0 @@
package eu.kanade.tachiyomi.extension.util
/*
* 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 android.annotation.SuppressLint
// import android.content.Context
// import android.content.pm.PackageInfo
// import android.content.pm.PackageManager
// import dalvik.system.PathClassLoader
// import eu.kanade.tachiyomi.data.preference.PreferenceValues
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
// import eu.kanade.tachiyomi.util.lang.Hash
// import kotlinx.coroutines.async
// import kotlinx.coroutines.runBlocking
// import timber.log.Timber
// import uy.kohesive.injekt.injectLazy
/**
* Class that handles the loading of the extensions installed in the system.
*/
// @SuppressLint("PackageManagerGetSignatures")
internal object ExtensionLoader {
// private val preferences: PreferencesHelper by injectLazy()
// private val allowNsfwSource by lazy {
// preferences.allowNsfwSource().get()
// }
private const val EXTENSION_FEATURE = "tachiyomi.extension"
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
const val LIB_VERSION_MIN = 1.2
const val LIB_VERSION_MAX = 1.2
// private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
// inorichi's key
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
/**
* List of the trusted signatures.
*/
// var trustedSignatures = mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignature
/**
* Return a list of all the installed extensions initialized concurrently.
*
* @param context The application context.
*/
// fun loadExtensions(context: Context): List<LoadResult> {
// val pkgManager = context.packageManager
// val installedPkgs = pkgManager.getInstalledPackages(PACKAGE_FLAGS)
// val extPkgs = installedPkgs.filter { isPackageAnExtension(it) }
//
// if (extPkgs.isEmpty()) return emptyList()
//
// // Load each extension concurrently and wait for completion
// return runBlocking {
// val deferred = extPkgs.map {
// async { loadExtension(context, it.packageName, it) }
// }
// deferred.map { it.await() }
// }
// }
/**
* Attempts to load an extension from the given package name. It checks if the extension
* contains the required feature flag before trying to load it.
*/
// fun loadExtensionFromPkgName(context: Context, pkgName: String): LoadResult {
// val pkgInfo = try {
// context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
// } catch (error: PackageManager.NameNotFoundException) {
// // Unlikely, but the package may have been uninstalled at this point
// return LoadResult.Error(error)
// }
// if (!isPackageAnExtension(pkgInfo)) {
// return LoadResult.Error("Tried to load a package that wasn't a extension")
// }
// return loadExtension(context, pkgName, pkgInfo)
// }
/**
* Loads an extension given its package name.
*
* @param context The application context.
* @param pkgName The package name of the extension to load.
* @param pkgInfo The package info of the extension.
*/
// private fun loadExtension(context: Context, pkgName: String, pkgInfo: PackageInfo): LoadResult {
// val pkgManager = context.packageManager
//
// val appInfo = try {
// pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
// } catch (error: PackageManager.NameNotFoundException) {
// // Unlikely, but the package may have been uninstalled at this point
// return LoadResult.Error(error)
// }
//
// val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
// val versionName = pkgInfo.versionName
// val versionCode = pkgInfo.versionCode
//
// if (versionName.isNullOrEmpty()) {
// val exception = Exception("Missing versionName for extension $extName")
// Timber.w(exception)
// return LoadResult.Error(exception)
// }
//
// // Validate lib version
// val libVersion = versionName.substringBeforeLast('.').toDouble()
// if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
// val exception = Exception(
// "Lib version is $libVersion, while only versions " +
// "$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
// )
// Timber.w(exception)
// return LoadResult.Error(exception)
// }
//
// val signatureHash = getSignatureHash(pkgInfo)
//
// if (signatureHash == null) {
// return LoadResult.Error("Package $pkgName isn't signed")
// } else if (signatureHash !in trustedSignatures) {
// val extension = Extension.Untrusted(extName, pkgName, versionName, versionCode, signatureHash)
// Timber.w("Extension $pkgName isn't trusted")
// return LoadResult.Untrusted(extension)
// }
//
// val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
// if (allowNsfwSource == PreferenceValues.NsfwAllowance.BLOCKED && isNsfw) {
// return LoadResult.Error("NSFW extension $pkgName not allowed")
// }
//
// val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
//
// val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
// .split(";")
// .map {
// val sourceClass = it.trim()
// if (sourceClass.startsWith(".")) {
// pkgInfo.packageName + sourceClass
// } else {
// sourceClass
// }
// }
// .flatMap {
// try {
// when (val obj = Class.forName(it, false, classLoader).newInstance()) {
// is Source -> listOf(obj)
// is SourceFactory -> {
// if (isSourceNsfw(obj)) {
// emptyList()
// } else {
// obj.createSources()
// }
// }
// else -> throw Exception("Unknown source class type! ${obj.javaClass}")
// }
// } catch (e: Throwable) {
// Timber.e(e, "Extension load error: $extName.")
// return LoadResult.Error(e)
// }
// }
// .filter { !isSourceNsfw(it) }
//
// val langs = sources.filterIsInstance<CatalogueSource>()
// .map { it.lang }
// .toSet()
// val lang = when (langs.size) {
// 0 -> ""
// 1 -> langs.first()
// else -> "all"
// }
//
// val extension = Extension.Installed(
// extName,
// pkgName,
// versionName,
// versionCode,
// lang,
// isNsfw,
// sources,
// isUnofficial = signatureHash != officialSignature
// )
// return LoadResult.Success(extension)
// }
/**
* Returns true if the given package is an extension.
*
* @param pkgInfo The package info of the application.
*/
// private fun isPackageAnExtension(pkgInfo: PackageInfo): Boolean {
// return pkgInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }
// }
/**
* Returns the signature hash of the package or null if it's not signed.
*
* @param pkgInfo The package info of the application.
*/
// private fun getSignatureHash(pkgInfo: PackageInfo): String? {
// val signatures = pkgInfo.signatures
// return if (signatures != null && signatures.isNotEmpty()) {
// Hash.sha256(signatures.first().toByteArray())
// } else {
// null
// }
// }
/**
* Checks whether a Source or SourceFactory is annotated with @Nsfw.
*/
// private fun isSourceNsfw(clazz: Any): Boolean {
// if (allowNsfwSource == PreferenceValues.NsfwAllowance.ALLOWED) {
// return false
// }
//
// if (clazz !is Source && clazz !is SourceFactory) {
// return false
// }
//
// // Annotations are proxied, hence this janky way of checking for them
// return clazz.javaClass.annotations
// .flatMap { it.javaClass.interfaces.map { it.simpleName } }
// .firstOrNull { it == Nsfw::class.java.simpleName } != null
// }
}
@@ -0,0 +1,46 @@
package eu.kanade.tachiyomi.source
import eu.kanade.tachiyomi.source.model.AnimesPage
import eu.kanade.tachiyomi.source.model.FilterList
import rx.Observable
interface AnimeCatalogueSource : AnimeSource {
/**
* An ISO 639-1 compliant language code (two letters in lower case).
*/
val lang: String
/**
* Whether the source has support for latest updates.
*/
val supportsLatest: Boolean
/**
* Returns an observable containing a page with a list of anime.
*
* @param page the page number to retrieve.
*/
fun fetchPopularAnime(page: Int): Observable<AnimesPage>
/**
* Returns an observable containing a page with a list of anime.
*
* @param page the page number to retrieve.
* @param query the search query.
* @param filters the list of filters to apply.
*/
fun fetchSearchAnime(page: Int, query: String, filters: FilterList): Observable<AnimesPage>
/**
* Returns an observable containing a page with a list of latest anime updates.
*
* @param page the page number to retrieve.
*/
fun fetchLatestUpdates(page: Int): Observable<AnimesPage>
/**
* Returns the list of filters for the source.
*/
fun getFilterList(): FilterList
}
@@ -0,0 +1,77 @@
package eu.kanade.tachiyomi.source
import eu.kanade.tachiyomi.source.model.SAnime
import eu.kanade.tachiyomi.source.model.SEpisode
import rx.Observable
/**
* A basic interface for creating a source. It could be an online source, a local source, etc...
*/
interface AnimeSource {
/**
* Id for the source. Must be unique.
*/
val id: Long
/**
* Name of the source.
*/
val name: String
/**
* Returns an observable with the updated details for a anime.
*
* @param anime the anime to update.
*/
// @Deprecated("Use getAnimeDetails instead")
fun fetchAnimeDetails(anime: SAnime): Observable<SAnime>
/**
* Returns an observable with all the available episodes for an anime.
*
* @param anime the anime to update.
*/
// @Deprecated("Use getEpisodeList instead")
fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>>
/**
* Returns an observable with a link for the episode of an anime.
*
* @param episode the episode to get the link for.
*/
// @Deprecated("Use getEpisodeList instead")
fun fetchEpisodeLink(episode: SEpisode): Observable<String>
// /**
// * [1.x API] Get the updated details for a anime.
// */
// @Suppress("DEPRECATION")
// override suspend fun getAnimeDetails(anime: AnimeInfo): AnimeInfo {
// val sAnime = anime.toSAnime()
// val networkAnime = fetchAnimeDetails(sAnime).awaitSingle()
// sAnime.copyFrom(networkAnime)
// return sAnime.toAnimeInfo()
// }
// /**
// * [1.x API] Get all the available episodes for a anime.
// */
// @Suppress("DEPRECATION")
// override suspend fun getEpisodeList(anime: AnimeInfo): List<EpisodeInfo> {
// return fetchEpisodeList(anime.toSAnime()).awaitSingle()
// .map { it.toEpisodeInfo() }
// }
// /**
// * [1.x API] Get a link for the episode of an anime.
// */
// @Suppress("DEPRECATION")
// override suspend fun getEpisodeLink(episode: EpisodeInfo): String {
// return fetchEpisodeLink(episode.toSEpisode()).awaitSingle()
// }
}
// fun AnimeSource.icon(): Drawable? = Injekt.get<AnimeExtensionManager>().getAppIconForSource(this)
// fun AnimeSource.getPreferenceKey(): String = "source_$id"
@@ -0,0 +1,12 @@
package eu.kanade.tachiyomi.source
/**
* A factory for creating sources at runtime.
*/
interface AnimeSourceFactory {
/**
* Create a new copy of the sources
* @return The created sources
*/
fun createSources(): List<AnimeSource>
}
@@ -0,0 +1,76 @@
package eu.kanade.tachiyomi.source
import android.content.Context
import eu.kanade.tachiyomi.source.model.SAnime
import eu.kanade.tachiyomi.source.model.SEpisode
import eu.kanade.tachiyomi.source.online.AnimeHttpSource
import rx.Observable
open class AnimeSourceManager(private val context: Context) {
private val sourcesMap = mutableMapOf<Long, AnimeSource>()
private val stubSourcesMap = mutableMapOf<Long, StubSource>()
init {
createInternalSources().forEach { registerSource(it) }
}
open fun get(sourceKey: Long): AnimeSource? {
return sourcesMap[sourceKey]
}
fun getOrStub(sourceKey: Long): AnimeSource {
return sourcesMap[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) {
StubSource(sourceKey)
}
}
fun getOnlineSources() = sourcesMap.values.filterIsInstance<AnimeHttpSource>()
fun getCatalogueSources() = sourcesMap.values.filterIsInstance<AnimeCatalogueSource>()
internal fun registerSource(source: AnimeSource) {
if (!sourcesMap.containsKey(source.id)) {
sourcesMap[source.id] = source
}
if (!stubSourcesMap.containsKey(source.id)) {
stubSourcesMap[source.id] = StubSource(source.id)
}
}
internal fun unregisterSource(source: AnimeSource) {
sourcesMap.remove(source.id)
}
private fun createInternalSources(): List<AnimeSource> = listOf(
// LocalAnimeSource(context)
)
inner class StubSource(override val id: Long) : AnimeSource {
override val name: String
get() = id.toString()
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
return Observable.error(getSourceNotInstalledException())
}
override fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>> {
return Observable.error(getSourceNotInstalledException())
}
override fun fetchEpisodeLink(episode: SEpisode): Observable<String> {
return Observable.error(getSourceNotInstalledException())
}
override fun toString(): String {
return name
}
private fun getSourceNotInstalledException(): Exception {
// return Exception(context.getString(R.string.source_not_installed, id.toString()))
return Exception("source not found")
}
}
}
@@ -0,0 +1,8 @@
package eu.kanade.tachiyomi.source
import android.support.v7.preference.PreferenceScreen
interface ConfigurableAnimeSource : AnimeSource {
fun setupPreferenceScreen(screen: PreferenceScreen)
}
@@ -0,0 +1,358 @@
package eu.kanade.tachiyomi.source
import android.content.Context
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import rx.Observable
// import com.github.junrar.Archive
// import com.google.gson.JsonParser
// import eu.kanade.tachiyomi.R
// import eu.kanade.tachiyomi.source.model.Filter
// import eu.kanade.tachiyomi.source.model.FilterList
// import eu.kanade.tachiyomi.source.model.MangasPage
// import eu.kanade.tachiyomi.source.model.Page
// import eu.kanade.tachiyomi.source.model.SChapter
// import eu.kanade.tachiyomi.source.model.SManga
// import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
// import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
// import eu.kanade.tachiyomi.util.storage.DiskUtil
// import eu.kanade.tachiyomi.util.storage.EpubFile
// import eu.kanade.tachiyomi.util.system.ImageUtil
// import rx.Observable
// import timber.log.Timber
// import java.io.File
// import java.io.FileInputStream
// import java.io.InputStream
// import java.util.Locale
// import java.util.concurrent.TimeUnit
// import java.util.zip.ZipFile
class LocalSource(private val context: Context) : CatalogueSource {
companion object {
const val ID = 0L
// const val HELP_URL = "https://tachiyomi.org/help/guides/reading-local-manga/"
//
// private const val COVER_NAME = "cover.jpg"
// private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")
//
// private val POPULAR_FILTERS = FilterList(OrderBy())
// private val LATEST_FILTERS = FilterList(OrderBy().apply { state = Filter.Sort.Selection(1, false) })
// private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
//
// fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
// val dir = getBaseDirectories(context).firstOrNull()
// if (dir == null) {
// input.close()
// return null
// }
// val cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME)
//
// // It might not exist if using the external SD card
// cover.parentFile?.mkdirs()
// input.use {
// cover.outputStream().use {
// input.copyTo(it)
// }
// }
// return cover
// }
//
// private fun getBaseDirectories(context: Context): List<File> {
// val c = context.getString(R.string.app_name) + File.separator + "local"
// return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) }
// }
}
override val id = ID
override val name = "Local source"
override val lang = ""
override val supportsLatest = true
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
TODO("Not yet implemented")
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
TODO("Not yet implemented")
}
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
TODO("Not yet implemented")
}
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
TODO("Not yet implemented")
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
TODO("Not yet implemented")
}
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
TODO("Not yet implemented")
}
override fun getFilterList(): FilterList {
TODO("Not yet implemented")
}
//
// override fun toString() = context.getString(R.string.local_source)
//
// override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
//
// override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
// val baseDirs = getBaseDirectories(context)
//
// val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
// var mangaDirs = baseDirs
// .asSequence()
// .mapNotNull { it.listFiles()?.toList() }
// .flatten()
// .filter { it.isDirectory }
// .filterNot { it.name.startsWith('.') }
// .filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
// .distinctBy { it.name }
//
// val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
// when (state?.index) {
// 0 -> {
// mangaDirs = if (state.ascending) {
// mangaDirs.sortedBy { it.name.toLowerCase(Locale.ENGLISH) }
// } else {
// mangaDirs.sortedByDescending { it.name.toLowerCase(Locale.ENGLISH) }
// }
// }
// 1 -> {
// mangaDirs = if (state.ascending) {
// mangaDirs.sortedBy(File::lastModified)
// } else {
// mangaDirs.sortedByDescending(File::lastModified)
// }
// }
// }
//
// val mangas = mangaDirs.map { mangaDir ->
// SManga.create().apply {
// title = mangaDir.name
// url = mangaDir.name
//
// // Try to find the cover
// for (dir in baseDirs) {
// val cover = File("${dir.absolutePath}/$url", COVER_NAME)
// if (cover.exists()) {
// thumbnail_url = cover.absolutePath
// break
// }
// }
//
// val chapters = fetchChapterList(this).toBlocking().first()
// if (chapters.isNotEmpty()) {
// val chapter = chapters.last()
// val format = getFormat(chapter)
// if (format is Format.Epub) {
// EpubFile(format.file).use { epub ->
// epub.fillMangaMetadata(this)
// }
// }
//
// // Copy the cover from the first chapter found.
// if (thumbnail_url == null) {
// try {
// val dest = updateCover(chapter, this)
// thumbnail_url = dest?.absolutePath
// } catch (e: Exception) {
// Timber.e(e)
// }
// }
// }
// }
// }
//
// return Observable.just(MangasPage(mangas.toList(), false))
// }
//
// override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
//
// override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
// getBaseDirectories(context)
// .asSequence()
// .mapNotNull { File(it, manga.url).listFiles()?.toList() }
// .flatten()
// .firstOrNull { it.extension == "json" }
// ?.apply {
// val reader = this.inputStream().bufferedReader()
// val json = JsonParser.parseReader(reader).asJsonObject
//
// manga.title = json["title"]?.asString ?: manga.title
// manga.author = json["author"]?.asString ?: manga.author
// manga.artist = json["artist"]?.asString ?: manga.artist
// manga.description = json["description"]?.asString ?: manga.description
// manga.genre = json["genre"]?.asJsonArray?.joinToString(", ") { it.asString }
// ?: manga.genre
// manga.status = json["status"]?.asInt ?: manga.status
// }
//
// return Observable.just(manga)
// }
//
// override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
// val chapters = getBaseDirectories(context)
// .asSequence()
// .mapNotNull { File(it, manga.url).listFiles()?.toList() }
// .flatten()
// .filter { it.isDirectory || isSupportedFile(it.extension) }
// .map { chapterFile ->
// SChapter.create().apply {
// url = "${manga.url}/${chapterFile.name}"
// name = if (chapterFile.isDirectory) {
// chapterFile.name
// } else {
// chapterFile.nameWithoutExtension
// }
// date_upload = chapterFile.lastModified()
//
// val format = getFormat(this)
// if (format is Format.Epub) {
// EpubFile(format.file).use { epub ->
// epub.fillChapterMetadata(this)
// }
// }
//
// val chapNameCut = stripMangaTitle(name, manga.title)
// if (chapNameCut.isNotEmpty()) name = chapNameCut
// ChapterRecognition.parseChapterNumber(this, manga)
// }
// }
// .sortedWith(
// Comparator { c1, c2 ->
// val c = c2.chapter_number.compareTo(c1.chapter_number)
// if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
// }
// )
// .toList()
//
// return Observable.just(chapters)
// }
//
// /**
// * Strips the manga title from a chapter name, matching only based on alphanumeric and whitespace
// * characters.
// */
// private fun stripMangaTitle(chapterName: String, mangaTitle: String): String {
// var chapterNameIndex = 0
// var mangaTitleIndex = 0
// while (chapterNameIndex < chapterName.length && mangaTitleIndex < mangaTitle.length) {
// val chapterChar = chapterName[chapterNameIndex]
// val mangaChar = mangaTitle[mangaTitleIndex]
// if (!chapterChar.equals(mangaChar, true)) {
// val invalidChapterChar = !chapterChar.isLetterOrDigit() && !chapterChar.isWhitespace()
// val invalidMangaChar = !mangaChar.isLetterOrDigit() && !mangaChar.isWhitespace()
//
// if (!invalidChapterChar && !invalidMangaChar) {
// return chapterName
// }
//
// if (invalidChapterChar) {
// chapterNameIndex++
// }
//
// if (invalidMangaChar) {
// mangaTitleIndex++
// }
// } else {
// chapterNameIndex++
// mangaTitleIndex++
// }
// }
//
// return chapterName.substring(chapterNameIndex).trimStart(' ', '-', '_', ',', ':')
// }
//
// override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
// return Observable.error(Exception("Unused"))
// }
//
// private fun isSupportedFile(extension: String): Boolean {
// return extension.toLowerCase() in SUPPORTED_ARCHIVE_TYPES
// }
//
// fun getFormat(chapter: SChapter): Format {
// val baseDirs = getBaseDirectories(context)
//
// for (dir in baseDirs) {
// val chapFile = File(dir, chapter.url)
// if (!chapFile.exists()) continue
//
// return getFormat(chapFile)
// }
// throw Exception("Chapter not found")
// }
//
// private fun getFormat(file: File): Format {
// val extension = file.extension
// return if (file.isDirectory) {
// Format.Directory(file)
// } else if (extension.equals("zip", true) || extension.equals("cbz", true)) {
// Format.Zip(file)
// } else if (extension.equals("rar", true) || extension.equals("cbr", true)) {
// Format.Rar(file)
// } else if (extension.equals("epub", true)) {
// Format.Epub(file)
// } else {
// throw Exception("Invalid chapter format")
// }
// }
//
// private fun updateCover(chapter: SChapter, manga: SManga): File? {
// return when (val format = getFormat(chapter)) {
// is Format.Directory -> {
// val entry = format.file.listFiles()
// ?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
// ?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
//
// entry?.let { updateCover(context, manga, it.inputStream()) }
// }
// is Format.Zip -> {
// ZipFile(format.file).use { zip ->
// val entry = zip.entries().toList()
// .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
// .find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
//
// entry?.let { updateCover(context, manga, zip.getInputStream(it)) }
// }
// }
// is Format.Rar -> {
// Archive(format.file).use { archive ->
// val entry = archive.fileHeaders
// .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
// .find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
//
// entry?.let { updateCover(context, manga, archive.getInputStream(it)) }
// }
// }
// is Format.Epub -> {
// EpubFile(format.file).use { epub ->
// val entry = epub.getImagesFromPages()
// .firstOrNull()
// ?.let { epub.getEntry(it) }
//
// entry?.let { updateCover(context, manga, epub.getInputStream(it)) }
// }
// }
// }
// }
//
// private class OrderBy : Filter.Sort("Order by", arrayOf("Title", "Date"), Selection(0, true))
//
// override fun getFilterList() = FilterList(OrderBy())
//
// sealed class Format {
// data class Directory(val file: File) : Format()
// data class Zip(val file: File) : Format()
// data class Rar(val file: File) : Format()
// data class Epub(val file: File) : Format()
// }
}
@@ -1,13 +1,9 @@
package eu.kanade.tachiyomi.source
// import android.graphics.drawable.Drawable
// import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import rx.Observable
// import uy.kohesive.injekt.Injekt
// import uy.kohesive.injekt.api.get
/**
* A basic interface for creating a source. It could be an online source, a local source, etc...
@@ -1,14 +1,13 @@
package eu.kanade.tachiyomi.source
// import android.content.Context
// import eu.kanade.tachiyomi.R
import android.content.Context
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import rx.Observable
open class SourceManager() {
open class SourceManager(private val context: Context) {
private val sourcesMap = mutableMapOf<Long, Source>()
@@ -0,0 +1,3 @@
package eu.kanade.tachiyomi.source.model
data class AnimesPage(val animes: List<SAnime>, val hasNextPage: Boolean)
@@ -0,0 +1,67 @@
package eu.kanade.tachiyomi.source.model
import java.io.Serializable
interface SAnime : Serializable {
var url: String
var title: String
var artist: String?
var author: String?
var description: String?
var genre: String?
var status: Int
var thumbnail_url: String?
var initialized: Boolean
fun copyFrom(other: SAnime) {
if (other.title != null) {
title = other.title
}
if (other.author != null) {
author = other.author
}
if (other.artist != null) {
artist = other.artist
}
if (other.description != null) {
description = other.description
}
if (other.genre != null) {
genre = other.genre
}
if (other.thumbnail_url != null) {
thumbnail_url = other.thumbnail_url
}
status = other.status
if (!initialized) {
initialized = other.initialized
}
}
companion object {
const val UNKNOWN = 0
const val ONGOING = 1
const val COMPLETED = 2
const val LICENSED = 3
fun create(): SAnime {
return SAnimeImpl()
}
}
}
@@ -0,0 +1,22 @@
package eu.kanade.tachiyomi.source.model
class SAnimeImpl : SAnime {
override lateinit var url: String
override lateinit var title: String
override var artist: String? = null
override var author: String? = null
override var description: String? = null
override var genre: String? = null
override var status: Int = 0
override var thumbnail_url: String? = null
override var initialized: Boolean = false
}
@@ -0,0 +1,30 @@
package eu.kanade.tachiyomi.source.model
import java.io.Serializable
interface SEpisode : Serializable {
var url: String
var name: String
var date_upload: Long
var episode_number: Float
var scanlator: String?
fun copyFrom(other: SEpisode) {
name = other.name
url = other.url
date_upload = other.date_upload
episode_number = other.episode_number
scanlator = other.scanlator
}
companion object {
fun create(): SEpisode {
return SEpisodeImpl()
}
}
}
@@ -0,0 +1,14 @@
package eu.kanade.tachiyomi.source.model
class SEpisodeImpl : SEpisode {
override lateinit var url: String
override lateinit var name: String
override var date_upload: Long = 0
override var episode_number: Float = -1f
override var scanlator: String? = null
}
@@ -0,0 +1,388 @@
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.newCallWithProgress
import eu.kanade.tachiyomi.source.AnimeCatalogueSource
import eu.kanade.tachiyomi.source.model.AnimesPage
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SAnime
import eu.kanade.tachiyomi.source.model.SEpisode
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.net.URI
import java.net.URISyntaxException
import java.security.MessageDigest
/**
* A simple implementation for sources from a website.
*/
abstract class AnimeHttpSource : AnimeCatalogueSource {
/**
* Network service.
*/
protected val network: NetworkHelper by injectLazy()
// /**
// * Preferences that a source may need.
// */
// val preferences: SharedPreferences by lazy {
// Injekt.get<Application>().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE)
// }
/**
* Base url of the website without the trailing slash, like: http://mysite.com
*/
abstract val baseUrl: String
/**
* Version id used to generate the source id. If the site completely changes and urls are
* incompatible, you may increase this value and it'll be considered as a new source.
*/
open val versionId = 1
/**
* Id of the source. By default it uses a generated id using the first 16 characters (64 bits)
* of the MD5 of the string: sourcename/language/versionId
* Note the generated id sets the sign bit to 0.
*/
override val id by lazy {
val key = "${name.toLowerCase()}/$lang/$versionId"
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
}
/**
* Headers used for requests.
*/
val headers: Headers by lazy { headersBuilder().build() }
/**
* Default network client for doing requests.
*/
open val client: OkHttpClient
get() = network.client
/**
* Headers builder for requests. Implementations can override this method for custom headers.
*/
protected open fun headersBuilder() = Headers.Builder().apply {
add("User-Agent", DEFAULT_USER_AGENT)
}
/**
* Visible name of the source.
*/
override fun toString() = "$name (${lang.toUpperCase()})"
/**
* Returns an observable containing a page with a list of anime. Normally it's not needed to
* override this method.
*
* @param page the page number to retrieve.
*/
override fun fetchPopularAnime(page: Int): Observable<AnimesPage> {
return client.newCall(popularAnimeRequest(page))
.asObservableSuccess()
.map { response ->
popularAnimeParse(response)
}
}
/**
* Returns the request for the popular anime given the page.
*
* @param page the page number to retrieve.
*/
protected abstract fun popularAnimeRequest(page: Int): Request
/**
* Parses the response from the site and returns a [AnimesPage] object.
*
* @param response the response from the site.
*/
protected abstract fun popularAnimeParse(response: Response): AnimesPage
/**
* Returns an observable containing a page with a list of anime. Normally it's not needed to
* override this method.
*
* @param page the page number to retrieve.
* @param query the search query.
* @param filters the list of filters to apply.
*/
override fun fetchSearchAnime(page: Int, query: String, filters: FilterList): Observable<AnimesPage> {
return client.newCall(searchAnimeRequest(page, query, filters))
.asObservableSuccess()
.map { response ->
searchAnimeParse(response)
}
}
/**
* Returns the request for the search anime given the page.
*
* @param page the page number to retrieve.
* @param query the search query.
* @param filters the list of filters to apply.
*/
protected abstract fun searchAnimeRequest(page: Int, query: String, filters: FilterList): Request
/**
* Parses the response from the site and returns a [AnimesPage] object.
*
* @param response the response from the site.
*/
protected abstract fun searchAnimeParse(response: Response): AnimesPage
/**
* Returns an observable containing a page with a list of latest anime updates.
*
* @param page the page number to retrieve.
*/
override fun fetchLatestUpdates(page: Int): Observable<AnimesPage> {
return client.newCall(latestUpdatesRequest(page))
.asObservableSuccess()
.map { response ->
latestUpdatesParse(response)
}
}
/**
* Returns the request for latest anime given the page.
*
* @param page the page number to retrieve.
*/
protected abstract fun latestUpdatesRequest(page: Int): Request
/**
* Parses the response from the site and returns a [AnimesPage] object.
*
* @param response the response from the site.
*/
protected abstract fun latestUpdatesParse(response: Response): AnimesPage
/**
* Returns an observable with the updated details for a anime. Normally it's not needed to
* override this method.
*
* @param anime the anime to be updated.
*/
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
return client.newCall(animeDetailsRequest(anime))
.asObservableSuccess()
.map { response ->
animeDetailsParse(response).apply { initialized = true }
}
}
/**
* Returns the request for the details of a anime. Override only if it's needed to change the
* url, send different headers or request method like POST.
*
* @param anime the anime to be updated.
*/
open fun animeDetailsRequest(anime: SAnime): Request {
return GET(baseUrl + anime.url, headers)
}
/**
* Parses the response from the site and returns the details of a anime.
*
* @param response the response from the site.
*/
protected abstract fun animeDetailsParse(response: Response): SAnime
/**
* Returns an observable with the updated episode list for a anime. Normally it's not needed to
* override this method. If a anime is licensed an empty episode list observable is returned
*
* @param anime the anime to look for episodes.
*/
override fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>> {
return if (anime.status != SAnime.LICENSED) {
client.newCall(episodeListRequest(anime))
.asObservableSuccess()
.map { response ->
episodeListParse(response)
}
} else {
Observable.error(Exception("Licensed - No episodes to show"))
}
}
override fun fetchEpisodeLink(episode: SEpisode): Observable<String> {
return client.newCall(episodeLinkRequest(episode))
.asObservableSuccess()
.map { response ->
episodeLinkParse(response)
}
}
/**
* Returns the request for updating the episode list. Override only if it's needed to override
* the url, send different headers or request method like POST.
*
* @param anime the anime to look for episodes.
*/
protected open fun episodeListRequest(anime: SAnime): Request {
return GET(baseUrl + anime.url, headers)
}
/**
* Returns the request for getting the episode link. Override only if it's needed to override
* the url, send different headers or request method like POST.
*
* @param episode the episode to look for links.
*/
protected open fun episodeLinkRequest(episode: SEpisode): Request {
return GET(baseUrl + episode.url, headers)
}
/**
* Parses the response from the site and returns a list of episodes.
*
* @param response the response from the site.
*/
protected abstract fun episodeListParse(response: Response): List<SEpisode>
/**
* Parses the response from the site and returns a list of episodes.
*
* @param response the response from the site.
*/
protected abstract fun episodeLinkParse(response: Response): String
/**
* Returns the request for getting the page list. Override only if it's needed to override the
* url, send different headers or request method like POST.
*
* @param episode the episode whose page list has to be fetched.
*/
protected open fun pageListRequest(episode: SEpisode): Request {
return GET(baseUrl + episode.url, headers)
}
/**
* Parses the response from the site and returns a list of pages.
*
* @param response the response from the site.
*/
protected abstract fun pageListParse(response: Response): List<Page>
/**
* Returns an observable with the page containing the source url of the image. If there's any
* error, it will return null instead of throwing an exception.
*
* @param page the page whose source image has to be fetched.
*/
open fun fetchImageUrl(page: Page): Observable<String> {
return client.newCall(imageUrlRequest(page))
.asObservableSuccess()
.map { imageUrlParse(it) }
}
/**
* Returns the request for getting the url to the source image. Override only if it's needed to
* override the url, send different headers or request method like POST.
*
* @param page the episode whose page list has to be fetched
*/
protected open fun imageUrlRequest(page: Page): Request {
return GET(page.url, headers)
}
/**
* Parses the response from the site and returns the absolute url to the source image.
*
* @param response the response from the site.
*/
protected abstract fun imageUrlParse(response: Response): String
/**
* Returns an observable with the response of the source image.
*
* @param page the page whose source image has to be downloaded.
*/
fun fetchImage(page: Page): Observable<Response> {
return client.newCallWithProgress(imageRequest(page), page)
.asObservableSuccess()
}
/**
* Returns the request for getting the source image. Override only if it's needed to override
* the url, send different headers or request method like POST.
*
* @param page the episode whose page list has to be fetched
*/
protected open fun imageRequest(page: Page): Request {
return GET(page.imageUrl!!, headers)
}
/**
* Assigns the url of the episode without the scheme and domain. It saves some redundancy from
* database and the urls could still work after a domain change.
*
* @param url the full url to the episode.
*/
fun SEpisode.setUrlWithoutDomain(url: String) {
this.url = getUrlWithoutDomain(url)
}
/**
* Assigns the url of the anime without the scheme and domain. It saves some redundancy from
* database and the urls could still work after a domain change.
*
* @param url the full url to the anime.
*/
fun SAnime.setUrlWithoutDomain(url: String) {
this.url = getUrlWithoutDomain(url)
}
/**
* Returns the url of the given string without the scheme and domain.
*
* @param orig the full url.
*/
private fun getUrlWithoutDomain(orig: String): String {
return try {
val uri = URI(orig)
var out = uri.path
if (uri.query != null) {
out += "?" + uri.query
}
if (uri.fragment != null) {
out += "#" + uri.fragment
}
out
} catch (e: URISyntaxException) {
orig
}
}
/**
* Called before inserting a new episode into database. Use it if you need to override episode
* fields, like the title or the episode number. Do not change anything to [anime].
*
* @param episode the episode to be added.
* @param anime the anime of the episode.
*/
open fun prepareNewEpisode(episode: SEpisode, anime: SAnime) {
}
/**
* Returns the list of filters for the source.
*/
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"
}
}
@@ -0,0 +1,25 @@
package eu.kanade.tachiyomi.source.online
import eu.kanade.tachiyomi.source.model.Page
import rx.Observable
fun AnimeHttpSource.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 }
}
fun AnimeHttpSource.fetchAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
return Observable.from(pages)
.filter { !it.imageUrl.isNullOrEmpty() }
.mergeWith(fetchRemainingImageUrlsFromPageList(pages))
}
fun AnimeHttpSource.fetchRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
return Observable.from(pages)
.filter { it.imageUrl.isNullOrEmpty() }
.concatMap { getImageUrl(it) }
}
@@ -0,0 +1,222 @@
package eu.kanade.tachiyomi.source.online
import eu.kanade.tachiyomi.source.model.AnimesPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SAnime
import eu.kanade.tachiyomi.source.model.SEpisode
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
/**
* A simple implementation for sources from a website using Jsoup, an HTML parser.
*/
abstract class ParsedAnimeHttpSource : AnimeHttpSource() {
/**
* Parses the response from the site and returns a [AnimesPage] object.
*
* @param response the response from the site.
*/
override fun popularAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val animes = document.select(popularAnimeSelector()).map { element ->
popularAnimeFromElement(element)
}
val hasNextPage = popularAnimeNextPageSelector()?.let { selector ->
document.select(selector).first()
} != null
return AnimesPage(animes, hasNextPage)
}
/**
* Returns the Jsoup selector that returns a list of [Element] corresponding to each anime.
*/
protected abstract fun popularAnimeSelector(): String
/**
* Returns a anime from the given [element]. Most sites only show the title and the url, it's
* totally fine to fill only those two values.
*
* @param element an element obtained from [popularAnimeSelector].
*/
protected abstract fun popularAnimeFromElement(element: Element): SAnime
/**
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
* there's no next page.
*/
protected abstract fun popularAnimeNextPageSelector(): String?
/**
* Parses the response from the site and returns a [AnimesPage] object.
*
* @param response the response from the site.
*/
override fun searchAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val animes = document.select(searchAnimeSelector()).map { element ->
searchAnimeFromElement(element)
}
val hasNextPage = searchAnimeNextPageSelector()?.let { selector ->
document.select(selector).first()
} != null
return AnimesPage(animes, hasNextPage)
}
/**
* Returns the Jsoup selector that returns a list of [Element] corresponding to each anime.
*/
protected abstract fun searchAnimeSelector(): String
/**
* Returns a anime from the given [element]. Most sites only show the title and the url, it's
* totally fine to fill only those two values.
*
* @param element an element obtained from [searchAnimeSelector].
*/
protected abstract fun searchAnimeFromElement(element: Element): SAnime
/**
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
* there's no next page.
*/
protected abstract fun searchAnimeNextPageSelector(): String?
/**
* Parses the response from the site and returns a [AnimesPage] object.
*
* @param response the response from the site.
*/
override fun latestUpdatesParse(response: Response): AnimesPage {
val document = response.asJsoup()
val animes = document.select(latestUpdatesSelector()).map { element ->
latestUpdatesFromElement(element)
}
val hasNextPage = latestUpdatesNextPageSelector()?.let { selector ->
document.select(selector).first()
} != null
return AnimesPage(animes, hasNextPage)
}
/**
* Returns the Jsoup selector that returns a list of [Element] corresponding to each anime.
*/
protected abstract fun latestUpdatesSelector(): String
/**
* Returns a anime from the given [element]. Most sites only show the title and the url, it's
* totally fine to fill only those two values.
*
* @param element an element obtained from [latestUpdatesSelector].
*/
protected abstract fun latestUpdatesFromElement(element: Element): SAnime
/**
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
* there's no next page.
*/
protected abstract fun latestUpdatesNextPageSelector(): String?
/**
* Parses the response from the site and returns the details of a anime.
*
* @param response the response from the site.
*/
override fun animeDetailsParse(response: Response): SAnime {
return animeDetailsParse(response.asJsoup())
}
/**
* Returns the details of the anime from the given [document].
*
* @param document the parsed document.
*/
protected abstract fun animeDetailsParse(document: Document): SAnime
/**
* Parses the response from the site and returns a list of episodes.
*
* @param response the response from the site.
*/
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
return document.select(episodeListSelector()).map { episodeFromElement(it) }
}
/**
* Returns the Jsoup selector that returns a list of [Element] corresponding to each episode.
*/
protected abstract fun episodeListSelector(): String
/**
* Parses the response from the site and returns a list of episodes.
*
* @param response the response from the site.
*/
override fun episodeLinkParse(response: Response): String {
val document = response.asJsoup()
return linkFromElement(document.select(episodeLinkSelector()).first())
}
/**
* Returns the Jsoup selector that returns a list of [Element] corresponding to each episode.
*/
protected abstract fun episodeLinkSelector(): String
/**
* Returns a episode from the given element.
*
* @param element an element obtained from [episodeListSelector].
*/
protected abstract fun episodeFromElement(element: Element): SEpisode
/**
* Returns a episode from the given element.
*
* @param element an element obtained from [episodeListSelector].
*/
protected abstract fun linkFromElement(element: Element): String
/**
* Parses the response from the site and returns the page list.
*
* @param response the response from the site.
*/
override fun pageListParse(response: Response): List<Page> {
return pageListParse(response.asJsoup())
}
/**
* Returns a page list from the given document.
*
* @param document the parsed document.
*/
protected abstract fun pageListParse(document: Document): List<Page>
/**
* Parse the response from the site and returns the absolute url to the source image.
*
* @param response the response from the site.
*/
override fun imageUrlParse(response: Response): String {
return imageUrlParse(response.asJsoup())
}
/**
* Returns the absolute url to the source image from the document.
*
* @param document the parsed document.
*/
protected abstract fun imageUrlParse(document: Document): String
}
@@ -0,0 +1,44 @@
package eu.kanade.tachiyomi.util.lang
import java.security.MessageDigest
object Hash {
private val chars = charArrayOf(
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f'
)
private val MD5 get() = MessageDigest.getInstance("MD5")
private val SHA256 get() = MessageDigest.getInstance("SHA-256")
fun sha256(bytes: ByteArray): String {
return encodeHex(SHA256.digest(bytes))
}
fun sha256(string: String): String {
return sha256(string.toByteArray())
}
fun md5(bytes: ByteArray): String {
return encodeHex(MD5.digest(bytes))
}
fun md5(string: String): String {
return md5(string.toByteArray())
}
private fun encodeHex(data: ByteArray): String {
val l = data.size
val out = CharArray(l shl 1)
var i = 0
var j = 0
while (i < l) {
out[j++] = chars[(240 and data[i].toInt()).ushr(4)]
out[j++] = chars[15 and data[i].toInt()]
i++
}
return String(out)
}
}
@@ -1,22 +0,0 @@
package ir.armor.tachidesk
/*
* 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 ir.armor.tachidesk.server.applicationSetup
import ir.armor.tachidesk.server.javalinSetup
class Main {
companion object {
@JvmStatic
fun main(args: Array<String>) {
applicationSetup()
javalinSetup()
}
}
}
@@ -1,44 +0,0 @@
package ir.armor.tachidesk.database
/*
* 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 ir.armor.tachidesk.database.table.CategoryMangaTable
import ir.armor.tachidesk.database.table.CategoryTable
import ir.armor.tachidesk.database.table.ChapterTable
import ir.armor.tachidesk.database.table.ExtensionTable
import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.PageTable
import ir.armor.tachidesk.database.table.SourceTable
import ir.armor.tachidesk.server.applicationDirs
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
object DBMangaer {
val db by lazy {
Database.connect("jdbc:h2:${applicationDirs.dataRoot}/database", "org.h2.Driver")
}
}
fun makeDataBaseTables() {
// must mention db object so the lazy block executes
val db = DBMangaer.db
db.useNestedTransactions = true
transaction {
SchemaUtils.createMissingTablesAndColumns(
ExtensionTable,
SourceTable,
MangaTable,
ChapterTable,
PageTable,
CategoryTable,
CategoryMangaTable,
)
}
}
@@ -1,21 +0,0 @@
package ir.armor.tachidesk.database.dataclass
/*
* 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/. */
data class ChapterDataClass(
val id: Int,
val url: String,
val name: String,
val date_upload: Long,
val chapter_number: Float,
val scanlator: String?,
val mangaId: Int,
val chapterIndex: Int,
val chapterCount: Int,
val pageCount: Int? = null,
)
@@ -1,28 +0,0 @@
package ir.armor.tachidesk.database.entity
/*
* 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 ir.armor.tachidesk.database.table.ExtensionTable
import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.id.EntityID
class ExtensionEntity(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<ExtensionEntity>(ExtensionTable)
var name by ExtensionTable.name
var pkgName by ExtensionTable.pkgName
var versionName by ExtensionTable.versionName
var versionCode by ExtensionTable.versionCode
var lang by ExtensionTable.lang
var isNsfw by ExtensionTable.isNsfw
var apkName by ExtensionTable.apkName
var iconUrl by ExtensionTable.iconUrl
var installed by ExtensionTable.installed
var classFQName by ExtensionTable.classFQName
}
@@ -1,30 +0,0 @@
package ir.armor.tachidesk.database.entity
/*
* 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 ir.armor.tachidesk.database.table.MangaTable
import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.id.EntityID
class MangaEntity(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<MangaEntity>(MangaTable)
var url by MangaTable.url
var title by MangaTable.title
var initialized by MangaTable.initialized
var artist by MangaTable.artist
var author by MangaTable.author
var description by MangaTable.description
var genre by MangaTable.genre
var status by MangaTable.status
var thumbnail_url by MangaTable.thumbnail_url
var sourceReference by MangaEntity referencedOn MangaTable.sourceReference
}
@@ -1,24 +0,0 @@
package ir.armor.tachidesk.database.entity
/*
* 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 ir.armor.tachidesk.database.table.SourceTable
import org.jetbrains.exposed.dao.EntityClass
import org.jetbrains.exposed.dao.LongEntity
import org.jetbrains.exposed.dao.id.EntityID
class SourceEntity(id: EntityID<Long>) : LongEntity(id) {
companion object : EntityClass<Long, SourceEntity>(SourceTable, null)
var sourceId by SourceTable.id
var name by SourceTable.name
var lang by SourceTable.lang
var extension by ExtensionEntity referencedOn SourceTable.extension
var partOfFactorySource by SourceTable.partOfFactorySource
var positionInFactorySource by SourceTable.positionInFactorySource
}
@@ -1,22 +0,0 @@
package ir.armor.tachidesk.database.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
object ChapterTable : IntIdTable() {
val url = varchar("url", 2048)
val name = varchar("name", 512)
val date_upload = long("date_upload").default(0)
val chapter_number = float("chapter_number").default(-1f)
val scanlator = varchar("scanlator", 128).nullable()
val chapterIndex = integer("number_in_list")
val manga = reference("manga", MangaTable)
}
@@ -1,69 +0,0 @@
package ir.armor.tachidesk.impl
/*
* 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 ir.armor.tachidesk.database.dataclass.CategoryDataClass
import ir.armor.tachidesk.database.table.CategoryMangaTable
import ir.armor.tachidesk.database.table.CategoryTable
import ir.armor.tachidesk.database.table.toDataClass
import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.deleteWhere
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
fun createCategory(name: String) {
transaction {
val count = CategoryTable.selectAll().count()
if (CategoryTable.select { CategoryTable.name eq name }.firstOrNull() == null)
CategoryTable.insert {
it[CategoryTable.name] = name
it[CategoryTable.order] = count.toInt() + 1
}
}
}
fun updateCategory(categoryId: Int, name: String?, isLanding: Boolean?) {
transaction {
CategoryTable.update({ CategoryTable.id eq categoryId }) {
if (name != null) it[CategoryTable.name] = name
if (isLanding != null) it[CategoryTable.isLanding] = isLanding
}
}
}
fun reorderCategory(categoryId: Int, from: Int, to: Int) {
transaction {
val categories = CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).toMutableList()
categories.add(to - 1, categories.removeAt(from - 1))
categories.forEachIndexed { index, cat ->
CategoryTable.update({ CategoryTable.id eq cat[CategoryTable.id].value }) {
it[CategoryTable.order] = index + 1
}
}
}
}
fun removeCategory(categoryId: Int) {
transaction {
CategoryMangaTable.select { CategoryMangaTable.category eq categoryId }.forEach {
removeMangaFromCategory(it[CategoryMangaTable.manga].value, categoryId)
}
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
}
}
fun getCategoryList(): List<CategoryDataClass> {
return transaction {
CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).map {
CategoryTable.toDataClass(it)
}
}
}
@@ -1,64 +0,0 @@
package ir.armor.tachidesk.impl
/*
* 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 ir.armor.tachidesk.database.dataclass.CategoryDataClass
import ir.armor.tachidesk.database.dataclass.MangaDataClass
import ir.armor.tachidesk.database.table.CategoryMangaTable
import ir.armor.tachidesk.database.table.CategoryTable
import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.toDataClass
import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
fun addMangaToCategory(mangaId: Int, categoryId: Int) {
transaction {
if (CategoryMangaTable.select { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }.firstOrNull() == null) {
CategoryMangaTable.insert {
it[CategoryMangaTable.category] = categoryId
it[CategoryMangaTable.manga] = mangaId
}
MangaTable.update({ MangaTable.id eq mangaId }) {
it[MangaTable.defaultCategory] = false
}
}
}
}
fun removeMangaFromCategory(mangaId: Int, categoryId: Int) {
transaction {
CategoryMangaTable.deleteWhere { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }
if (CategoryMangaTable.select { CategoryMangaTable.manga eq mangaId }.count() == 0L) {
MangaTable.update({ MangaTable.id eq mangaId }) {
it[MangaTable.defaultCategory] = true
}
}
}
}
fun getCategoryMangaList(categoryId: Int): List<MangaDataClass> {
return transaction {
CategoryMangaTable.innerJoin(MangaTable).select { CategoryMangaTable.category eq categoryId }.map {
MangaTable.toDataClass(it)
}
}
}
fun getMangaCategories(mangaId: Int): List<CategoryDataClass> {
return transaction {
CategoryMangaTable.innerJoin(CategoryTable).select { CategoryMangaTable.manga eq mangaId }.orderBy(CategoryTable.order to SortOrder.ASC).map {
CategoryTable.toDataClass(it)
}
}
}
@@ -1,141 +0,0 @@
package ir.armor.tachidesk.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import ir.armor.tachidesk.database.dataclass.ChapterDataClass
import ir.armor.tachidesk.database.table.ChapterTable
import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.PageTable
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.insert
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
fun getChapterList(mangaId: Int): List<ChapterDataClass> {
val mangaDetails = getManga(mangaId)
val source = getHttpSource(mangaDetails.sourceId.toLong())
val chapterList = source.fetchChapterList(
SManga.create().apply {
title = mangaDetails.title
url = mangaDetails.url
}
).toBlocking().first()
val chapterCount = chapterList.count()
return transaction {
chapterList.reversed().forEachIndexed { index, fetchedChapter ->
val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull()
if (chapterEntry == null) {
ChapterTable.insertAndGetId {
it[url] = fetchedChapter.url
it[name] = fetchedChapter.name
it[date_upload] = fetchedChapter.date_upload
it[chapter_number] = fetchedChapter.chapter_number
it[scanlator] = fetchedChapter.scanlator
it[chapterIndex] = index + 1
it[manga] = mangaId
}
} else {
ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) {
it[name] = fetchedChapter.name
it[date_upload] = fetchedChapter.date_upload
it[chapter_number] = fetchedChapter.chapter_number
it[scanlator] = fetchedChapter.scanlator
it[chapterIndex] = index + 1
it[manga] = mangaId
}
}
}
// clear any orphaned chapters
val dbChapterCount = transaction { ChapterTable.selectAll().count() }
if (dbChapterCount > chapterCount) { // we got some clean up due
// TODO
}
return@transaction chapterList.mapIndexed { index, it ->
ChapterDataClass(
ChapterTable.select { ChapterTable.url eq it.url }.firstOrNull()!![ChapterTable.id].value,
it.url,
it.name,
it.date_upload,
it.chapter_number,
it.scanlator,
mangaId,
chapterCount - index,
chapterCount
)
}
}
}
fun getChapter(chapterIndex: Int, mangaId: Int): ChapterDataClass {
return transaction {
val chapterEntry = ChapterTable.select {
ChapterTable.chapterIndex eq chapterIndex and (ChapterTable.manga eq mangaId)
}.firstOrNull()!!
val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
val pageList = source.fetchPageList(
SChapter.create().apply {
url = chapterEntry[ChapterTable.url]
name = chapterEntry[ChapterTable.name]
}
).toBlocking().first()
val chapterId = chapterEntry[ChapterTable.id].value
val chapterCount = transaction { ChapterTable.selectAll().count() }
val chapter = ChapterDataClass(
chapterId,
chapterEntry[ChapterTable.url],
chapterEntry[ChapterTable.name],
chapterEntry[ChapterTable.date_upload],
chapterEntry[ChapterTable.chapter_number],
chapterEntry[ChapterTable.scanlator],
mangaId,
chapterEntry[ChapterTable.chapterIndex],
chapterCount.toInt(),
pageList.count()
)
pageList.forEach { page ->
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() }
if (pageEntry == null) {
transaction {
PageTable.insert {
it[index] = page.index
it[url] = page.url
it[imageUrl] = page.imageUrl
it[this.chapter] = chapterId
}
}
} else {
transaction {
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }) {
it[url] = page.url
it[imageUrl] = page.imageUrl
}
}
}
}
return@transaction chapter
}
}
@@ -1,207 +0,0 @@
package ir.armor.tachidesk.impl
/*
* 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 com.googlecode.d2j.dex.Dex2jar
import com.googlecode.d2j.reader.MultiDexFileReader
import com.googlecode.dex2jar.tools.BaksmaliBaseDexExceptionHandler
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.database.table.ExtensionTable
import ir.armor.tachidesk.database.table.SourceTable
import ir.armor.tachidesk.impl.util.APKExtractor
import ir.armor.tachidesk.server.applicationDirs
import kotlinx.coroutines.runBlocking
import mu.KotlinLogging
import okhttp3.Request
import okio.buffer
import okio.sink
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.InputStream
import java.net.URL
import java.net.URLClassLoader
import java.nio.file.Files
import java.nio.file.Path
private val logger = KotlinLogging.logger {}
private fun dex2jar(dexFile: String, jarFile: String, fileNameWithoutType: String) {
// adopted from com.googlecode.dex2jar.tools.Dex2jarCmd.doCommandLine
// source at: https://github.com/DexPatcher/dex2jar/tree/v2.1-20190905-lanchon/dex-tools/src/main/java/com/googlecode/dex2jar/tools/Dex2jarCmd.java
val jarFilePath = File(jarFile).toPath()
val reader = MultiDexFileReader.open(Files.readAllBytes(File(dexFile).toPath()))
val handler = BaksmaliBaseDexExceptionHandler()
Dex2jar
.from(reader)
.withExceptionHandler(handler)
.reUseReg(false)
.topoLogicalSort()
.skipDebug(true)
.optimizeSynchronized(false)
.printIR(false)
.noCode(false)
.skipExceptions(false)
.to(jarFilePath)
if (handler.hasException()) {
val errorFile: Path = File(applicationDirs.extensionsRoot).toPath().resolve("$fileNameWithoutType-error.txt")
logger.error(
"Detail Error Information in File $errorFile\n" +
"Please report this file to one of following link if possible (any one).\n" +
" https://sourceforge.net/p/dex2jar/tickets/\n" +
" https://bitbucket.org/pxb1988/dex2jar/issues\n" +
" https://github.com/pxb1988/dex2jar/issues\n" +
" dex2jar@googlegroups.com"
)
handler.dump(errorFile, emptyArray<String>())
}
}
fun installAPK(apkName: String): Int {
logger.debug("Installing $apkName")
val extensionRecord = getExtensionList(true).first { it.apkName == apkName }
val fileNameWithoutType = apkName.substringBefore(".apk")
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType"
// check if we don't have the dex file already downloaded
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
if (!File(jarPath).exists()) {
runBlocking {
val api = ExtensionGithubApi()
val apkToDownload = api.getApkUrl(extensionRecord)
val apkFilePath = "$dirPathWithoutType.apk"
val jarFilePath = "$dirPathWithoutType.jar"
val dexFilePath = "$dirPathWithoutType.dex"
// download apk file
downloadAPKFile(apkToDownload, apkFilePath)
val className: String = APKExtractor.extract_dex_and_read_className(apkFilePath, dexFilePath)
logger.debug(className)
// dex -> jar
dex2jar(dexFilePath, jarFilePath, fileNameWithoutType)
// clean up
File(apkFilePath).delete()
File(dexFilePath).delete()
// update sources of the extension
val child = URLClassLoader(arrayOf<URL>(URL("file:$jarFilePath")), this::class.java.classLoader)
val classToLoad = Class.forName(className, true, child)
val instance = classToLoad.newInstance()
val extensionId = transaction {
return@transaction ExtensionTable.select { ExtensionTable.name eq extensionRecord.name }.first()[ExtensionTable.id]
}
if (instance is HttpSource) { // single source
val httpSource = instance as HttpSource
transaction {
if (SourceTable.select { SourceTable.id eq httpSource.id }.count() == 0L) {
SourceTable.insert {
it[this.id] = httpSource.id
it[name] = httpSource.name
it[this.lang] = httpSource.lang
it[extension] = extensionId
}
}
logger.debug("Installed source ${httpSource.name} with id ${httpSource.id}")
}
} else { // multi source
val sourceFactory = instance as SourceFactory
transaction {
sourceFactory.createSources().forEachIndexed { index, source ->
val httpSource = source as HttpSource
if (SourceTable.select { SourceTable.id eq httpSource.id }.count() == 0L) {
SourceTable.insert {
it[this.id] = httpSource.id
it[name] = httpSource.name
it[this.lang] = httpSource.lang
it[extension] = extensionId
it[partOfFactorySource] = true
it[positionInFactorySource] = index
}
}
logger.debug("Installed source ${httpSource.name} with id:${httpSource.id}")
}
}
}
// update extension info
transaction {
ExtensionTable.update({ ExtensionTable.name eq extensionRecord.name }) {
it[installed] = true
it[classFQName] = className
}
}
}
return 201 // we downloaded successfully
} else {
return 302
}
}
val networkHelper: NetworkHelper by injectLazy()
private fun downloadAPKFile(url: String, apkPath: String) {
val request = Request.Builder().url(url).build()
val response = networkHelper.client.newCall(request).execute()
val downloadedFile = File(apkPath)
val sink = downloadedFile.sink().buffer()
sink.writeAll(response.body!!.source())
sink.close()
}
fun removeExtension(apkName: String) {
logger.debug("Uninstalling $apkName")
val extensionRecord = getExtensionList(true).first { it.apkName == apkName }
val fileNameWithoutType = apkName.substringBefore(".apk")
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
transaction {
val extensionId = ExtensionTable.select { ExtensionTable.name eq extensionRecord.name }.first()[ExtensionTable.id]
SourceTable.deleteWhere { SourceTable.extension eq extensionId }
ExtensionTable.update({ ExtensionTable.name eq extensionRecord.name }) {
it[ExtensionTable.installed] = false
}
}
if (File(jarPath).exists()) {
File(jarPath).delete()
}
}
val network: NetworkHelper by injectLazy()
fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()!! }[ExtensionTable.iconUrl]
val saveDir = "${applicationDirs.extensionsRoot}/icon"
return getCachedImageResponse(saveDir, apkName) {
network.client.newCall(
GET(iconUrl)
).execute()
}
}
fun getExtensionIconUrl(apkName: String): String {
return "/api/v1/extension/icon/$apkName"
}
@@ -1,94 +0,0 @@
package ir.armor.tachidesk.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.extension.model.Extension
import ir.armor.tachidesk.database.dataclass.ExtensionDataClass
import ir.armor.tachidesk.database.table.ExtensionTable
import kotlinx.coroutines.runBlocking
import mu.KotlinLogging
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
private val logger = KotlinLogging.logger {}
private object Data {
var lastExtensionCheck: Long = 0
}
private fun extensionDatabaseIsEmtpy(): Boolean {
return transaction {
return@transaction ExtensionTable.selectAll().count() == 0L
}
}
fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
// update if 60 seconds has passed or requested offline and database is empty
if (Data.lastExtensionCheck + 60 * 1000 < System.currentTimeMillis() || (offline && extensionDatabaseIsEmtpy())) {
logger.debug("Getting extensions list from the internet")
Data.lastExtensionCheck = System.currentTimeMillis()
var foundExtensions: List<Extension.Available>
runBlocking {
val api = ExtensionGithubApi()
foundExtensions = api.findExtensions()
transaction {
foundExtensions.forEach { foundExtension ->
val extensionRecord = ExtensionTable.select { ExtensionTable.name eq foundExtension.name }.firstOrNull()
if (extensionRecord != null) {
// update the record
ExtensionTable.update({ ExtensionTable.name eq foundExtension.name }) {
it[name] = foundExtension.name
it[pkgName] = foundExtension.pkgName
it[versionName] = foundExtension.versionName
it[versionCode] = foundExtension.versionCode
it[lang] = foundExtension.lang
it[isNsfw] = foundExtension.isNsfw
it[apkName] = foundExtension.apkName
it[iconUrl] = foundExtension.iconUrl
}
} else {
// insert new record
ExtensionTable.insert {
it[name] = foundExtension.name
it[pkgName] = foundExtension.pkgName
it[versionName] = foundExtension.versionName
it[versionCode] = foundExtension.versionCode
it[lang] = foundExtension.lang
it[isNsfw] = foundExtension.isNsfw
it[apkName] = foundExtension.apkName
it[iconUrl] = foundExtension.iconUrl
}
}
}
}
}
} else {
logger.debug("used cached extension list")
}
return transaction {
return@transaction ExtensionTable.selectAll().map {
ExtensionDataClass(
it[ExtensionTable.name],
it[ExtensionTable.pkgName],
it[ExtensionTable.versionName],
it[ExtensionTable.versionCode],
it[ExtensionTable.lang],
it[ExtensionTable.isNsfw],
it[ExtensionTable.apkName],
getExtensionIconUrl(it[ExtensionTable.apkName]),
it[ExtensionTable.installed],
it[ExtensionTable.classFQName]
)
}
}
}
@@ -1,50 +0,0 @@
package ir.armor.tachidesk.impl
/*
* 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 ir.armor.tachidesk.database.dataclass.MangaDataClass
import ir.armor.tachidesk.database.table.CategoryMangaTable
import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.toDataClass
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
fun addMangaToLibrary(mangaId: Int) {
val manga = getManga(mangaId)
if (!manga.inLibrary) {
transaction {
MangaTable.update({ MangaTable.id eq manga.id }) {
it[inLibrary] = true
}
}
}
}
fun removeMangaFromLibrary(mangaId: Int) {
val manga = getManga(mangaId)
if (manga.inLibrary) {
transaction {
MangaTable.update({ MangaTable.id eq manga.id }) {
it[inLibrary] = false
it[defaultCategory] = true
}
CategoryMangaTable.deleteWhere { CategoryMangaTable.manga eq mangaId }
}
}
}
fun getLibraryMangas(): List<MangaDataClass> {
return transaction {
MangaTable.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }.map {
MangaTable.toDataClass(it)
}
}
}
@@ -1,108 +0,0 @@
package ir.armor.tachidesk.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.SManga
import ir.armor.tachidesk.database.dataclass.MangaDataClass
import ir.armor.tachidesk.database.table.MangaStatus
import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.server.applicationDirs
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import java.io.InputStream
fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
return if (mangaEntry[MangaTable.initialized]) {
MangaDataClass(
mangaId,
mangaEntry[MangaTable.sourceReference].toString(),
mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title],
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else mangaEntry[MangaTable.thumbnail_url],
true,
mangaEntry[MangaTable.artist],
mangaEntry[MangaTable.author],
mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
mangaEntry[MangaTable.inLibrary],
getSource(mangaEntry[MangaTable.sourceReference])
)
} else { // initialize manga
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
val fetchedManga = source.fetchMangaDetails(
SManga.create().apply {
url = mangaEntry[MangaTable.url]
title = mangaEntry[MangaTable.title]
}
).toBlocking().first()
transaction {
MangaTable.update({ MangaTable.id eq mangaId }) {
it[MangaTable.initialized] = true
it[MangaTable.artist] = fetchedManga.artist
it[MangaTable.author] = fetchedManga.author
it[MangaTable.description] = fetchedManga.description
it[MangaTable.genre] = fetchedManga.genre
it[MangaTable.status] = fetchedManga.status
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
}
}
mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val newThumbnail = mangaEntry[MangaTable.thumbnail_url]
MangaDataClass(
mangaId,
mangaEntry[MangaTable.sourceReference].toString(),
mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title],
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else newThumbnail,
true,
fetchedManga.artist,
fetchedManga.author,
fetchedManga.description,
fetchedManga.genre,
MangaStatus.valueOf(fetchedManga.status).name,
false,
getSource(mangaEntry[MangaTable.sourceReference])
)
}
}
fun getThumbnail(mangaId: Int): Pair<InputStream, String> {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val saveDir = applicationDirs.thumbnailsRoot
val fileName = mangaId.toString()
return getCachedImageResponse(saveDir, fileName) {
val sourceId = mangaEntry[MangaTable.sourceReference]
val source = getHttpSource(sourceId)
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) {
thumbnailUrl = getManga(mangaId, proxyThumbnail = false).thumbnailUrl!!
}
source.client.newCall(
GET(thumbnailUrl, source.headers)
).execute()
}
}
@@ -1,98 +0,0 @@
package ir.armor.tachidesk.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.online.HttpSource
import okhttp3.FormBody
import okhttp3.OkHttpClient
import java.net.URLEncoder
class MangaDexHelper(private val mangaDexSource: HttpSource) {
private fun clientBuilder(): OkHttpClient = clientBuilder(0)
private fun clientBuilder(
r18Toggle: Int,
okHttpClient: OkHttpClient = mangaDexSource.network.client
): OkHttpClient = okHttpClient.newBuilder()
.addNetworkInterceptor { chain ->
val originalCookies = chain.request().header("Cookie") ?: ""
val newReq = chain
.request()
.newBuilder()
.header("Cookie", "$originalCookies; ${cookiesHeader(r18Toggle)}")
.build()
chain.proceed(newReq)
}.build()
private fun cookiesHeader(r18Toggle: Int): String {
val cookies = mutableMapOf<String, String>()
cookies["mangadex_h_toggle"] = r18Toggle.toString()
return buildCookies(cookies)
}
private fun buildCookies(cookies: Map<String, String>) =
cookies.entries.joinToString(separator = "; ", postfix = ";") {
"${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}"
}
// fun isLogged(): Boolean {
// val httpUrl = mangaDexSource.baseUrl.toHttpUrlOrNull()!!
// return network.cookieManager.get(httpUrl).any { it.name == REMEMBER_ME }
// }
fun login(username: String, password: String, twoFactorCode: String = ""): Boolean {
val formBody = FormBody.Builder()
.add("login_username", username)
.add("login_password", password)
.add("no_js", "1")
.add("remember_me", "1")
twoFactorCode.let {
formBody.add("two_factor", it)
}
val response = clientBuilder().newCall(
POST(
"${mangaDexSource.baseUrl}/ajax/actions.ajax.php?function=login",
mangaDexSource.headers,
formBody.build()
)
).execute()
return response.body!!.string().isEmpty()
}
//
// fun logout(): Boolean {
// return withContext(Dispatchers.IO) {
// // https://mangadex.org/ajax/actions.ajax.php?function=logout
// val httpUrl = baseUrl.toHttpUrlOrNull()!!
// val listOfDexCookies = network.cookieManager.get(httpUrl)
// val cookie = listOfDexCookies.find { it.name == REMEMBER_ME }
// val token = cookie?.value
// if (token.isNullOrEmpty()) {
// return@withContext true
// }
// val result = clientBuilder().newCall(
// POSTWithCookie(
// "$baseUrl/ajax/actions.ajax.php?function=logout",
// REMEMBER_ME,
// token,
// headers
// )
// ).execute()
// val resultStr = result.body!!.string()
// if (resultStr.contains("success", true)) {
// network.cookieManager.remove(httpUrl)
// return@withContext true
// }
//
// false
// }
// }
}
@@ -1,98 +0,0 @@
package ir.armor.tachidesk.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.MangasPage
import ir.armor.tachidesk.database.dataclass.MangaDataClass
import ir.armor.tachidesk.database.dataclass.PagedMangaListDataClass
import ir.armor.tachidesk.database.table.MangaStatus
import ir.armor.tachidesk.database.table.MangaTable
import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
fun proxyThumbnailUrl(mangaId: Int): String {
return "/api/v1/manga/$mangaId/thumbnail"
}
fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
val source = getHttpSource(sourceId.toLong())
val mangasPage = if (popular) {
source.fetchPopularManga(pageNum).toBlocking().first()
} else {
if (source.supportsLatest)
source.fetchLatestUpdates(pageNum).toBlocking().first()
else
throw Exception("Source $source doesn't support latest")
}
return mangasPage.processEntries(sourceId)
}
fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
val mangasPage = this
val mangaList = transaction {
return@transaction mangasPage.mangas.map { manga ->
var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull()
if (mangaEntry == null) { // create manga entry
val mangaId = MangaTable.insertAndGetId {
it[url] = manga.url
it[title] = manga.title
it[artist] = manga.artist
it[author] = manga.author
it[description] = manga.description
it[genre] = manga.genre
it[status] = manga.status
it[thumbnail_url] = manga.thumbnail_url
it[sourceReference] = sourceId
}.value
MangaDataClass(
mangaId,
sourceId.toString(),
manga.url,
manga.title,
proxyThumbnailUrl(mangaId),
manga.initialized,
manga.artist,
manga.author,
manga.description,
manga.genre,
MangaStatus.valueOf(manga.status).name
)
} else {
val mangaId = mangaEntry[MangaTable.id].value
MangaDataClass(
mangaId,
sourceId.toString(),
manga.url,
manga.title,
proxyThumbnailUrl(mangaId),
true,
mangaEntry[MangaTable.artist],
mangaEntry[MangaTable.author],
mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
mangaEntry[MangaTable.inLibrary]
)
}
}
}
return PagedMangaListDataClass(
mangaList,
mangasPage.hasNextPage
)
}
@@ -1,85 +0,0 @@
package ir.armor.tachidesk.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.database.table.ChapterTable
import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.PageTable
import ir.armor.tachidesk.database.table.SourceTable
import ir.armor.tachidesk.server.applicationDirs
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import java.io.File
import java.io.InputStream
fun getTrueImageUrl(page: Page, source: HttpSource): String {
if (page.imageUrl == null) {
page.imageUrl = source.fetchImageUrl(page).toBlocking().first()!!
}
return page.imageUrl!!
}
fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int): Pair<InputStream, String> {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
val chapterEntry = transaction {
ChapterTable.select {
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
}.firstOrNull()!!
}
val chapterId = chapterEntry[ChapterTable.id].value
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq index) }.firstOrNull()!! }
val tachiPage = Page(
pageEntry[PageTable.index],
pageEntry[PageTable.url],
pageEntry[PageTable.imageUrl]
)
if (pageEntry[PageTable.imageUrl] == null) {
transaction {
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq index) }) {
it[imageUrl] = getTrueImageUrl(tachiPage, source)
}
}
}
val saveDir = getChapterDir(mangaId, chapterId)
File(saveDir).mkdirs()
val fileName = index.toString()
return getCachedImageResponse(saveDir, fileName) {
source.fetchImage(tachiPage).toBlocking().first()
}
}
fun getChapterDir(mangaId: Int, chapterId: Int): String {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val sourceId = mangaEntry[MangaTable.sourceReference]
val source = getHttpSource(sourceId)
val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! }
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! }
val chapterDir = when {
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
else -> chapterEntry[ChapterTable.name]
}
val mangaTitle = mangaEntry[MangaTable.title]
val sourceName = source.toString()
val mangaDir = "${applicationDirs.mangaRoot}/$sourceName/$mangaTitle/$chapterDir"
// make sure dirs exist
File(mangaDir).mkdirs()
return mangaDir
}
@@ -1,107 +0,0 @@
package ir.armor.tachidesk.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.database.dataclass.SourceDataClass
import ir.armor.tachidesk.database.entity.ExtensionEntity
import ir.armor.tachidesk.database.entity.SourceEntity
import ir.armor.tachidesk.database.table.ExtensionTable
import ir.armor.tachidesk.database.table.SourceTable
import ir.armor.tachidesk.server.applicationDirs
import mu.KotlinLogging
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import java.lang.NullPointerException
import java.net.URL
import java.net.URLClassLoader
private val logger = KotlinLogging.logger {}
private val sourceCache = mutableListOf<Pair<Long, HttpSource>>()
private val extensionCache = mutableListOf<Pair<String, Any>>()
fun getHttpSource(sourceId: Long): HttpSource {
val sourceRecord = transaction {
SourceEntity.findById(sourceId)
} ?: throw NullPointerException("Source with id $sourceId is not installed")
val cachedResult: Pair<Long, HttpSource>? = sourceCache.firstOrNull { it.first == sourceId }
if (cachedResult != null) {
logger.debug("used cached HttpSource: ${cachedResult.second.name}")
return cachedResult.second
}
val result: HttpSource = transaction {
val extensionId = sourceRecord.extension.id.value
val extensionRecord = ExtensionEntity.findById(extensionId)!!
val apkName = extensionRecord.apkName
val className = extensionRecord.classFQName
val jarName = apkName.substringBefore(".apk") + ".jar"
val jarPath = "${applicationDirs.extensionsRoot}/$jarName"
val cachedExtensionPair = extensionCache.firstOrNull { it.first == jarPath }
var usedCached = false
val instance =
if (cachedExtensionPair != null) {
usedCached = true
logger.debug("Used cached Extension")
cachedExtensionPair.second
} else {
logger.debug("No Extension cache")
val child = URLClassLoader(arrayOf<URL>(URL("file:$jarPath")), this::class.java.classLoader)
val classToLoad = Class.forName(className, true, child)
classToLoad.newInstance()
}
if (sourceRecord.partOfFactorySource) {
return@transaction if (usedCached) {
(instance as List<HttpSource>)[sourceRecord.positionInFactorySource!!]
} else {
val list = (instance as SourceFactory).createSources()
extensionCache.add(Pair(jarPath, list))
list[sourceRecord.positionInFactorySource!!] as HttpSource
}
} else {
if (!usedCached)
extensionCache.add(Pair(jarPath, instance))
return@transaction instance as HttpSource
}
}
sourceCache.add(Pair(sourceId, result))
return result
}
fun getSourceList(): List<SourceDataClass> {
return transaction {
return@transaction SourceTable.selectAll().map {
SourceDataClass(
it[SourceTable.id].value.toString(),
it[SourceTable.name],
it[SourceTable.lang],
getExtensionIconUrl(ExtensionTable.select { ExtensionTable.id eq it[SourceTable.extension] }.first()[ExtensionTable.apkName]),
getHttpSource(it[SourceTable.id].value).supportsLatest
)
}
}
}
fun getSource(sourceId: Long): SourceDataClass {
return transaction {
val source = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()
return@transaction SourceDataClass(
sourceId.toString(),
source?.get(SourceTable.name),
source?.get(SourceTable.lang),
source?.let { ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first()[ExtensionTable.iconUrl] },
source?.let { getHttpSource(sourceId).supportsLatest }
)
}
}
@@ -1,510 +0,0 @@
package ir.armor.tachidesk.impl.util
/*
* 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.w3c.dom.Document
import org.xml.sax.InputSource
import java.io.IOException
import java.io.StringReader
import java.nio.file.Files
import java.nio.file.Paths
import java.util.zip.ZipFile
import javax.xml.parsers.DocumentBuilderFactory
object APKExtractor {
// decompressXML -- Parse the 'compressed' binary form of Android XML docs
// such as for AndroidManifest.xml in .apk files
var endDocTag = 0x00100101
var startTag = 0x00100102
var endTag = 0x00100103
fun prt(str: String?) {
// System.err.print(str);
}
fun decompressXML(xml: ByteArray): String {
val finalXML = StringBuilder()
// Compressed XML file/bytes starts with 24x bytes of data,
// 9 32 bit words in little endian order (LSB first):
// 0th word is 03 00 08 00
// 3rd word SEEMS TO BE: Offset at then of StringTable
// 4th word is: Number of strings in string table
// WARNING: Sometime I indiscriminently display or refer to word in
// little endian storage format, or in integer format (ie MSB first).
val numbStrings = LEW(xml, 4 * 4)
// StringIndexTable starts at offset 24x, an array of 32 bit LE offsets
// of the length/string data in the StringTable.
val sitOff = 0x24 // Offset of start of StringIndexTable
// StringTable, each string is represented with a 16 bit little endian
// character count, followed by that number of 16 bit (LE) (Unicode)
// chars.
val stOff = sitOff + numbStrings * 4 // StringTable follows
// StrIndexTable
// XMLTags, The XML tag tree starts after some unknown content after the
// StringTable. There is some unknown data after the StringTable, scan
// forward from this point to the flag for the start of an XML start
// tag.
var xmlTagOff = LEW(xml, 3 * 4) // Start from the offset in the 3rd
// word.
// Scan forward until we find the bytes: 0x02011000(x00100102 in normal
// int)
var ii = xmlTagOff
while (ii < xml.size - 4) {
if (LEW(xml, ii) == startTag) {
xmlTagOff = ii
break
}
ii += 4
}
// XML tags and attributes:
// Every XML start and end tag consists of 6 32 bit words:
// 0th word: 02011000 for startTag and 03011000 for endTag
// 1st word: a flag?, like 38000000
// 2nd word: Line of where this tag appeared in the original source file
// 3rd word: FFFFFFFF ??
// 4th word: StringIndex of NameSpace name, or FFFFFFFF for default NS
// 5th word: StringIndex of Element Name
// (Note: 01011000 in 0th word means end of XML document, endDocTag)
// Start tags (not end tags) contain 3 more words:
// 6th word: 14001400 meaning??
// 7th word: Number of Attributes that follow this tag(follow word 8th)
// 8th word: 00000000 meaning??
// Attributes consist of 5 words:
// 0th word: StringIndex of Attribute Name's Namespace, or FFFFFFFF
// 1st word: StringIndex of Attribute Name
// 2nd word: StringIndex of Attribute Value, or FFFFFFF if ResourceId
// used
// 3rd word: Flags?
// 4th word: str ind of attr value again, or ResourceId of value
// TMP, dump string table to tr for debugging
// tr.addSelect("strings", null);
// for (int ii=0; ii<numbStrings; ii++) {
// // Length of string starts at StringTable plus offset in StrIndTable
// String str = compXmlString(xml, sitOff, stOff, ii);
// tr.add(String.valueOf(ii), str);
// }
// tr.parent();
// Step through the XML tree element tags and attributes
var off = xmlTagOff
var indent = 0
var startTagLineNo = -2
while (off < xml.size) {
val tag0 = LEW(xml, off)
// int tag1 = LEW(xml, off+1*4);
val lineNo = LEW(xml, off + 2 * 4)
// int tag3 = LEW(xml, off+3*4);
val nameNsSi = LEW(xml, off + 4 * 4)
val nameSi = LEW(xml, off + 5 * 4)
if (tag0 == startTag) { // XML START TAG
val tag6 = LEW(xml, off + 6 * 4) // Expected to be 14001400
val numbAttrs = LEW(xml, off + 7 * 4) // Number of Attributes
// to follow
// int tag8 = LEW(xml, off+8*4); // Expected to be 00000000
off += 9 * 4 // Skip over 6+3 words of startTag data
val name = compXmlString(xml, sitOff, stOff, nameSi)
// tr.addSelect(name, null);
startTagLineNo = lineNo
// Look for the Attributes
val sb = StringBuffer()
for (ii in 0 until numbAttrs) {
val attrNameNsSi = LEW(xml, off) // AttrName Namespace Str
// Ind, or FFFFFFFF
val attrNameSi = LEW(xml, off + 1 * 4) // AttrName String
// Index
val attrValueSi = LEW(xml, off + 2 * 4) // AttrValue Str
// Ind, or
// FFFFFFFF
val attrFlags = LEW(xml, off + 3 * 4)
val attrResId = LEW(xml, off + 4 * 4) // AttrValue
// ResourceId or dup
// AttrValue StrInd
off += 5 * 4 // Skip over the 5 words of an attribute
val attrName = compXmlString(
xml, sitOff, stOff,
attrNameSi
)
val attrValue = if (attrValueSi != -1) compXmlString(xml, sitOff, stOff, attrValueSi)
else "resourceID 0x ${Integer.toHexString(attrResId)}"
sb.append(" $attrName=\"$attrValue\"")
// tr.add(attrName, attrValue);
}
finalXML.append("<$name$sb>")
prtIndent(indent, "<$name$sb>")
indent++
} else if (tag0 == endTag) { // XML END TAG
indent--
off += 6 * 4 // Skip over 6 words of endTag data
val name = compXmlString(xml, sitOff, stOff, nameSi)
finalXML.append("</$name>")
prtIndent(
indent,
"</" + name + "> (line " + startTagLineNo +
"-" + lineNo + ")"
)
// tr.parent(); // Step back up the NobTree
} else if (tag0 == endDocTag) { // END OF XML DOC TAG
break
} else {
prt(
" Unrecognized tag code '" + Integer.toHexString(tag0) +
"' at offset " + off
)
break
}
} // end of while loop scanning tags and attributes of XML tree
// prt(" end at offset " + off);
return finalXML.toString()
} // end of decompressXML
fun compXmlString(xml: ByteArray, sitOff: Int, stOff: Int, strInd: Int): String? {
if (strInd < 0) return null
val strOff = stOff + LEW(xml, sitOff + strInd * 4)
return compXmlStringAt(xml, strOff)
}
var spaces = " "
fun prtIndent(indent: Int, str: String) {
prt(spaces.substring(0, Math.min(indent * 2, spaces.length)) + str)
}
// compXmlStringAt -- Return the string stored in StringTable format at
// offset strOff. This offset points to the 16 bit string length, which
// is followed by that number of 16 bit (Unicode) chars.
fun compXmlStringAt(arr: ByteArray, strOff: Int): String {
val strLen: Int = arr[strOff + 1].toInt() shl 8 and 0xff00 or arr[strOff].toInt() and 0xff
val chars = ByteArray(strLen)
for (ii in 0 until strLen) {
chars[ii] = arr[strOff + 2 + ii * 2]
}
return String(chars) // Hack, just use 8 byte chars
} // end of compXmlStringAt
// LEW -- Return value of a Little Endian 32 bit word from the byte array
// at offset off.
fun LEW(arr: ByteArray, off: Int): Int {
return (arr[off + 3].toInt() shl 24) and -0x1000000 or
(arr[off + 2].toInt() shl 16 and 0xff0000) or
(arr[off + 1].toInt() shl 8 and 0xff00) or
(arr[off].toInt() and 0xFF)
} // end of LEW
@Throws(Exception::class)
fun loadXMLFromString(xml: String?): Document {
val docBuilderFactory = DocumentBuilderFactory.newInstance()
val docBuilder = docBuilderFactory.newDocumentBuilder()
return docBuilder.parse(InputSource(StringReader(xml)))
}
@Throws(IOException::class)
fun extract_dex_and_read_className(filePath: String?, dexPath: String?): String {
var zip: ZipFile? = null
zip = ZipFile(filePath)
val androidManifest = zip.getEntry("AndroidManifest.xml")
val classesDex = zip.getEntry("classes.dex")
// write dex file
val dexStream = zip.getInputStream(classesDex)
Files.newOutputStream(Paths.get(dexPath)).use { os ->
val buffer = ByteArray(1024)
var len: Int
while (dexStream.read(buffer).also { len = it } > 0) {
os.write(buffer, 0, len)
}
}
// read xml file
val `is` = zip.getInputStream(androidManifest)
val buf = ByteArray(1024000) // 100 kb
`is`.read(buf)
`is`.close()
zip.close()
val xml = decompressXML(buf)
try {
val xmlDoc = loadXMLFromString(xml)
val pkg = xmlDoc.documentElement.getAttribute("package")
val nodes = xmlDoc.getElementsByTagName("meta-data")
for (i in 0 until nodes.length) {
val attributes = nodes.item(i).attributes
println(attributes.getNamedItem("name").nodeValue)
if (attributes.getNamedItem("name").nodeValue == "tachiyomi.extension.class") return pkg + attributes.getNamedItem("value").nodeValue
}
} catch (e: Exception) {
e.printStackTrace()
}
return ""
}
}
// original Java code
// package ir.armor.tachidesk.impl.util;
//
// /*
// * 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.w3c.dom.Document;
// import org.w3c.dom.NamedNodeMap;
// import org.w3c.dom.NodeList;
// import org.xml.sax.InputSource;
//
// import javax.xml.parsers.DocumentBuilder;
// import javax.xml.parsers.DocumentBuilderFactory;
// import java.io.*;
// import java.nio.file.Files;
// import java.nio.file.Paths;
// import java.util.zip.ZipEntry;
// import java.util.zip.ZipFile;
//
// public class APKExtractor {
// // decompressXML -- Parse the 'compressed' binary form of Android XML docs
// // such as for AndroidManifest.xml in .apk files
// public static int endDocTag = 0x00100101;
// public static int startTag = 0x00100102;
// public static int endTag = 0x00100103;
//
// static void prt(String str) {
// //System.err.print(str);
// }
//
// public static String decompressXML(byte[] xml) {
//
// StringBuilder finalXML = new StringBuilder();
//
// // Compressed XML file/bytes starts with 24x bytes of data,
// // 9 32 bit words in little endian order (LSB first):
// // 0th word is 03 00 08 00
// // 3rd word SEEMS TO BE: Offset at then of StringTable
// // 4th word is: Number of strings in string table
// // WARNING: Sometime I indiscriminently display or refer to word in
// // little endian storage format, or in integer format (ie MSB first).
// int numbStrings = LEW(xml, 4 * 4);
//
// // StringIndexTable starts at offset 24x, an array of 32 bit LE offsets
// // of the length/string data in the StringTable.
// int sitOff = 0x24; // Offset of start of StringIndexTable
//
// // StringTable, each string is represented with a 16 bit little endian
// // character count, followed by that number of 16 bit (LE) (Unicode)
// // chars.
// int stOff = sitOff + numbStrings * 4; // StringTable follows
// // StrIndexTable
//
// // XMLTags, The XML tag tree starts after some unknown content after the
// // StringTable. There is some unknown data after the StringTable, scan
// // forward from this point to the flag for the start of an XML start
// // tag.
// int xmlTagOff = LEW(xml, 3 * 4); // Start from the offset in the 3rd
// // word.
// // Scan forward until we find the bytes: 0x02011000(x00100102 in normal
// // int)
// for (int ii = xmlTagOff; ii < xml.length - 4; ii += 4) {
// if (LEW(xml, ii) == startTag) {
// xmlTagOff = ii;
// break;
// }
// } // end of hack, scanning for start of first start tag
//
// // XML tags and attributes:
// // Every XML start and end tag consists of 6 32 bit words:
// // 0th word: 02011000 for startTag and 03011000 for endTag
// // 1st word: a flag?, like 38000000
// // 2nd word: Line of where this tag appeared in the original source file
// // 3rd word: FFFFFFFF ??
// // 4th word: StringIndex of NameSpace name, or FFFFFFFF for default NS
// // 5th word: StringIndex of Element Name
// // (Note: 01011000 in 0th word means end of XML document, endDocTag)
//
// // Start tags (not end tags) contain 3 more words:
// // 6th word: 14001400 meaning??
// // 7th word: Number of Attributes that follow this tag(follow word 8th)
// // 8th word: 00000000 meaning??
//
// // Attributes consist of 5 words:
// // 0th word: StringIndex of Attribute Name's Namespace, or FFFFFFFF
// // 1st word: StringIndex of Attribute Name
// // 2nd word: StringIndex of Attribute Value, or FFFFFFF if ResourceId
// // used
// // 3rd word: Flags?
// // 4th word: str ind of attr value again, or ResourceId of value
//
// // TMP, dump string table to tr for debugging
// // tr.addSelect("strings", null);
// // for (int ii=0; ii<numbStrings; ii++) {
// // // Length of string starts at StringTable plus offset in StrIndTable
// // String str = compXmlString(xml, sitOff, stOff, ii);
// // tr.add(String.valueOf(ii), str);
// // }
// // tr.parent();
//
// // Step through the XML tree element tags and attributes
// int off = xmlTagOff;
// int indent = 0;
// int startTagLineNo = -2;
// while (off < xml.length) {
// int tag0 = LEW(xml, off);
// // int tag1 = LEW(xml, off+1*4);
// int lineNo = LEW(xml, off + 2 * 4);
// // int tag3 = LEW(xml, off+3*4);
// int nameNsSi = LEW(xml, off + 4 * 4);
// int nameSi = LEW(xml, off + 5 * 4);
//
// if (tag0 == startTag) { // XML START TAG
// int tag6 = LEW(xml, off + 6 * 4); // Expected to be 14001400
// int numbAttrs = LEW(xml, off + 7 * 4); // Number of Attributes
// // to follow
// // int tag8 = LEW(xml, off+8*4); // Expected to be 00000000
// off += 9 * 4; // Skip over 6+3 words of startTag data
// String name = compXmlString(xml, sitOff, stOff, nameSi);
// // tr.addSelect(name, null);
// startTagLineNo = lineNo;
//
// // Look for the Attributes
// StringBuffer sb = new StringBuffer();
// for (int ii = 0; ii < numbAttrs; ii++) {
// int attrNameNsSi = LEW(xml, off); // AttrName Namespace Str
// // Ind, or FFFFFFFF
// int attrNameSi = LEW(xml, off + 1 * 4); // AttrName String
// // Index
// int attrValueSi = LEW(xml, off + 2 * 4); // AttrValue Str
// // Ind, or
// // FFFFFFFF
// int attrFlags = LEW(xml, off + 3 * 4);
// int attrResId = LEW(xml, off + 4 * 4); // AttrValue
// // ResourceId or dup
// // AttrValue StrInd
// off += 5 * 4; // Skip over the 5 words of an attribute
//
// String attrName = compXmlString(xml, sitOff, stOff,
// attrNameSi);
// String attrValue = attrValueSi != -1 ? compXmlString(xml,
// sitOff, stOff, attrValueSi) : "resourceID 0x"
// + Integer.toHexString(attrResId);
// sb.append(" " + attrName + "=\"" + attrValue + "\"");
// // tr.add(attrName, attrValue);
// }
// finalXML.append("<" + name + sb + ">");
// prtIndent(indent, "<" + name + sb + ">");
// indent++;
//
// } else if (tag0 == endTag) { // XML END TAG
// indent--;
// off += 6 * 4; // Skip over 6 words of endTag data
// String name = compXmlString(xml, sitOff, stOff, nameSi);
// finalXML.append("</" + name + ">");
// prtIndent(indent, "</" + name + "> (line " + startTagLineNo
// + "-" + lineNo + ")");
// // tr.parent(); // Step back up the NobTree
//
// } else if (tag0 == endDocTag) { // END OF XML DOC TAG
// break;
//
// } else {
// prt(" Unrecognized tag code '" + Integer.toHexString(tag0)
// + "' at offset " + off);
// break;
// }
// } // end of while loop scanning tags and attributes of XML tree
// //prt(" end at offset " + off);
// return finalXML.toString();
// } // end of decompressXML
//
// public static String compXmlString(byte[] xml, int sitOff, int stOff, int strInd) {
// if (strInd < 0)
// return null;
// int strOff = stOff + LEW(xml, sitOff + strInd * 4);
// return compXmlStringAt(xml, strOff);
// }
//
// public static String spaces = " ";
//
// public static void prtIndent(int indent, String str) {
// prt(spaces.substring(0, Math.min(indent * 2, spaces.length())) + str);
// }
//
// // compXmlStringAt -- Return the string stored in StringTable format at
// // offset strOff. This offset points to the 16 bit string length, which
// // is followed by that number of 16 bit (Unicode) chars.
// public static String compXmlStringAt(byte[] arr, int strOff) {
// int strLen = arr[strOff + 1] << 8 & 0xff00 | arr[strOff] & 0xff;
// byte[] chars = new byte[strLen];
// for (int ii = 0; ii < strLen; ii++) {
// chars[ii] = arr[strOff + 2 + ii * 2];
// }
// return new String(chars); // Hack, just use 8 byte chars
// } // end of compXmlStringAt
//
// // LEW -- Return value of a Little Endian 32 bit word from the byte array
// // at offset off.
// public static int LEW(byte[] arr, int off) {
// return ((arr[off + 3] << 24) & 0xff000000) |
// ((arr[off + 2] << 16) & 0xff0000) |
// ((arr[off + 1] << 8) & 0xff00) |
// (arr[off] & 0xFF);
// } // end of LEW
//
// public static Document loadXMLFromString(String xml) throws Exception {
// DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
// DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
// return docBuilder.parse(new InputSource(new StringReader(xml)));
// }
//
// public static String extract_dex_and_read_className(String filePath, String dexPath) throws IOException {
// ZipFile zip = null;
//
// zip = new ZipFile(filePath);
// ZipEntry androidManifest = zip.getEntry("AndroidManifest.xml");
// ZipEntry classesDex = zip.getEntry("classes.dex");
//
// // write dex file
// InputStream dexStream = zip.getInputStream(classesDex);
// try (OutputStream os = Files.newOutputStream(Paths.get(dexPath))) {
// byte[] buffer = new byte[1024];
// int len;
// while ((len = dexStream.read(buffer)) > 0) {
// os.write(buffer, 0, len);
// }
// }
//
// // read xml file
// InputStream is = zip.getInputStream(androidManifest);
// byte[] buf = new byte[1024000]; // 100 kb
// is.read(buf);
// is.close();
// zip.close();
//
// String xml = APKExtractor.decompressXML(buf);
// try {
// Document xmlDoc = loadXMLFromString(xml);
// String pkg = xmlDoc.getDocumentElement().getAttribute("package");
// NodeList nodes = xmlDoc.getElementsByTagName("meta-data");
// for (int i = 0; i < nodes.getLength(); i++) {
// NamedNodeMap attributes = nodes.item(i).getAttributes();
// System.out.println(attributes.getNamedItem("name").getNodeValue());
// if (attributes.getNamedItem("name").getNodeValue().equals("tachiyomi.extension.class"))
// return pkg + attributes.getNamedItem("value").getNodeValue();
// }
// } catch (Exception e) {
// e.printStackTrace();
// }
// return "";
// }
// }
@@ -1,85 +0,0 @@
package ir.armor.tachidesk.impl
/*
* 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 okhttp3.Response
import okio.BufferedSource
import okio.buffer
import okio.sink
import java.io.BufferedInputStream
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Files
import java.nio.file.Paths
// fun writeStream(fileStream: InputStream, path: String) {
// Files.newOutputStream(Paths.get(path)).use { os ->
// val buffer = ByteArray(128 * 1024)
// var len: Int
// while (fileStream.read(buffer).also { len = it } > 0) {
// os.write(buffer, 0, len)
// }
// }
// }
fun pathToInputStream(path: String): InputStream {
return BufferedInputStream(FileInputStream(path))
}
fun findFileNameStartingWith(directoryPath: String, fileName: String): String? {
File(directoryPath).listFiles().forEach { file ->
if (file.name.startsWith(fileName))
return "$directoryPath/${file.name}"
}
return null
}
/**
* Saves the given source to an output stream and closes both resources.
*
* @param stream the stream where the source is copied.
*/
private fun BufferedSource.saveTo(stream: OutputStream) {
use { input ->
stream.sink().buffer().use {
it.writeAll(input)
it.flush()
}
}
}
fun getCachedImageResponse(saveDir: String, fileName: String, fetcher: () -> Response): Pair<InputStream, String> {
val cachedFile = findFileNameStartingWith(saveDir, fileName)
val filePath = "$saveDir/$fileName"
if (cachedFile != null) {
val fileType = cachedFile.substringAfter(filePath)
return Pair(
pathToInputStream(cachedFile),
"image/$fileType"
)
}
val response = fetcher()
if (response.code == 200) {
val contentType = response.headers["content-type"]!!
val fullPath = filePath + "." + contentType.substringAfter("image/")
Files.newOutputStream(Paths.get(fullPath)).use { os ->
response.body!!.source().saveTo(os)
}
return Pair(
pathToInputStream(fullPath),
contentType
)
} else {
throw Exception("request error! ${response.code}")
}
}
@@ -1,260 +0,0 @@
package ir.armor.tachidesk.server
import io.javalin.Javalin
import ir.armor.tachidesk.Main
import ir.armor.tachidesk.impl.addMangaToCategory
import ir.armor.tachidesk.impl.addMangaToLibrary
import ir.armor.tachidesk.impl.createCategory
import ir.armor.tachidesk.impl.getCategoryList
import ir.armor.tachidesk.impl.getCategoryMangaList
import ir.armor.tachidesk.impl.getChapter
import ir.armor.tachidesk.impl.getChapterList
import ir.armor.tachidesk.impl.getExtensionIcon
import ir.armor.tachidesk.impl.getExtensionList
import ir.armor.tachidesk.impl.getLibraryMangas
import ir.armor.tachidesk.impl.getManga
import ir.armor.tachidesk.impl.getMangaCategories
import ir.armor.tachidesk.impl.getMangaList
import ir.armor.tachidesk.impl.getPageImage
import ir.armor.tachidesk.impl.getSource
import ir.armor.tachidesk.impl.getSourceList
import ir.armor.tachidesk.impl.getThumbnail
import ir.armor.tachidesk.impl.installAPK
import ir.armor.tachidesk.impl.removeCategory
import ir.armor.tachidesk.impl.removeExtension
import ir.armor.tachidesk.impl.removeMangaFromCategory
import ir.armor.tachidesk.impl.removeMangaFromLibrary
import ir.armor.tachidesk.impl.reorderCategory
import ir.armor.tachidesk.impl.sourceFilters
import ir.armor.tachidesk.impl.sourceGlobalSearch
import ir.armor.tachidesk.impl.sourceSearch
import ir.armor.tachidesk.impl.updateCategory
import ir.armor.tachidesk.server.util.openInBrowser
import mu.KotlinLogging
/*
* 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/. */
private val logger = KotlinLogging.logger {}
fun javalinSetup() {
var hasWebUiBundled = false
val app = Javalin.create { config ->
try {
Main::class.java.getResource("/react/index.html")
hasWebUiBundled = true
config.addStaticFiles("/react")
config.addSinglePageRoot("/", "/react/index.html")
} catch (e: RuntimeException) {
logger.warn("react build files are missing.")
hasWebUiBundled = false
}
config.enableCorsForAllOrigins()
}.start(serverConfig.ip, serverConfig.port)
if (hasWebUiBundled && serverConfig.initialOpenInBrowserEnabled) {
openInBrowser()
}
app.exception(NullPointerException::class.java) { e, ctx ->
logger.error("NullPointerException while handling the request", e)
ctx.status(404)
}
app.get("/api/v1/extension/list") { ctx ->
ctx.json(getExtensionList())
}
app.get("/api/v1/extension/install/:apkName") { ctx ->
val apkName = ctx.pathParam("apkName")
ctx.status(
installAPK(apkName)
)
}
app.get("/api/v1/extension/uninstall/:apkName") { ctx ->
val apkName = ctx.pathParam("apkName")
removeExtension(apkName)
ctx.status(200)
}
// icon for extension named `apkName`
app.get("/api/v1/extension/icon/:apkName") { ctx ->
val apkName = ctx.pathParam("apkName")
val result = getExtensionIcon(apkName)
ctx.result(result.first)
ctx.header("content-type", result.second)
}
// list of sources
app.get("/api/v1/source/list") { ctx ->
ctx.json(getSourceList())
}
// fetch source with id `sourceId`
app.get("/api/v1/source/:sourceId") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
ctx.json(getSource(sourceId))
}
// popular mangas from source with id `sourceId`
app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(getMangaList(sourceId, pageNum, popular = true))
}
// latest mangas from source with id `sourceId`
app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(getMangaList(sourceId, pageNum, popular = false))
}
// get manga info
app.get("/api/v1/manga/:mangaId/") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getManga(mangaId))
}
// manga thumbnail
app.get("api/v1/manga/:mangaId/thumbnail") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
val result = getThumbnail(mangaId)
ctx.result(result.first)
ctx.header("content-type", result.second)
}
// adds the manga to library
app.get("api/v1/manga/:mangaId/library") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
addMangaToLibrary(mangaId)
ctx.status(200)
}
// removes the manga from the library
app.delete("api/v1/manga/:mangaId/library") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
removeMangaFromLibrary(mangaId)
ctx.status(200)
}
// list manga's categories
app.get("api/v1/manga/:mangaId/category/") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getMangaCategories(mangaId))
}
// adds the manga to category
app.get("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
val categoryId = ctx.pathParam("categoryId").toInt()
addMangaToCategory(mangaId, categoryId)
ctx.status(200)
}
// removes the manga from the category
app.delete("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
val categoryId = ctx.pathParam("categoryId").toInt()
removeMangaFromCategory(mangaId, categoryId)
ctx.status(200)
}
app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getChapterList(mangaId))
}
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex") { ctx ->
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getChapter(chapterIndex, mangaId))
}
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex/page/:index") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
val index = ctx.pathParam("index").toInt()
val result = getPageImage(mangaId, chapterIndex, index)
ctx.result(result.first)
ctx.header("content-type", result.second)
}
// global search
app.get("/api/v1/search/:searchTerm") { ctx ->
val searchTerm = ctx.pathParam("searchTerm")
ctx.json(sourceGlobalSearch(searchTerm))
}
// single source search
app.get("/api/v1/source/:sourceId/search/:searchTerm/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
val searchTerm = ctx.pathParam("searchTerm")
val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(sourceSearch(sourceId, searchTerm, pageNum))
}
// source filter list
app.get("/api/v1/source/:sourceId/filters/") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
ctx.json(sourceFilters(sourceId))
}
// lists mangas that have no category assigned
app.get("/api/v1/library/") { ctx ->
ctx.json(getLibraryMangas())
}
// category list
app.get("/api/v1/category/") { ctx ->
ctx.json(getCategoryList())
}
// category create
app.post("/api/v1/category/") { ctx ->
val name = ctx.formParam("name")!!
createCategory(name)
ctx.status(200)
}
// category modification
app.patch("/api/v1/category/:categoryId") { ctx ->
val categoryId = ctx.pathParam("categoryId").toInt()
val name = ctx.formParam("name")
val isLanding = if (ctx.formParam("isLanding") != null) ctx.formParam("isLanding")?.toBoolean() else null
updateCategory(categoryId, name, isLanding)
ctx.status(200)
}
// category re-ordering
app.patch("/api/v1/category/:categoryId/reorder") { ctx ->
val categoryId = ctx.pathParam("categoryId").toInt()
val from = ctx.formParam("from")!!.toInt()
val to = ctx.formParam("to")!!.toInt()
reorderCategory(categoryId, from, to)
ctx.status(200)
}
// category delete
app.delete("/api/v1/category/:categoryId") { ctx ->
val categoryId = ctx.pathParam("categoryId").toInt()
removeCategory(categoryId)
ctx.status(200)
}
// returns the manga list associated with a category
app.get("/api/v1/category/:categoryId") { ctx ->
val categoryId = ctx.pathParam("categoryId").toInt()
ctx.json(getCategoryMangaList(categoryId))
}
}
@@ -1,71 +0,0 @@
package ir.armor.tachidesk.server.util
/*
* 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 dorkbox.systemTray.MenuItem
import dorkbox.systemTray.SystemTray
import dorkbox.systemTray.SystemTray.TrayType
import dorkbox.util.CacheUtil
import dorkbox.util.Desktop
import ir.armor.tachidesk.Main
import ir.armor.tachidesk.server.serverConfig
import java.awt.event.ActionListener
import java.io.IOException
fun openInBrowser() {
try {
Desktop.browseURL("http://127.0.0.1:4567")
} catch (e1: IOException) {
e1.printStackTrace()
}
}
fun systemTray(): SystemTray? {
try {
// ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java
SystemTray.DEBUG = serverConfig.debugLogsEnabled
if (System.getProperty("os.name").startsWith("Windows"))
SystemTray.FORCE_TRAY_TYPE = TrayType.Swing
CacheUtil.clear()
val systemTray = SystemTray.get() ?: return null
val mainMenu = systemTray.menu
mainMenu.add(
MenuItem(
"Open Tachidesk",
ActionListener {
try {
Desktop.browseURL("http://127.0.0.1:4567")
} catch (e: IOException) {
e.printStackTrace()
}
}
)
)
val icon = Main::class.java.getResource("/icon/faviconlogo.png")
// systemTray.setTooltip("Tachidesk")
systemTray.setImage(icon)
// systemTray.status = "No Mail"
systemTray.getMenu().add(
MenuItem("Quit") {
systemTray.shutdown()
System.exit(0)
}
)
return systemTray
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
+16
View File
@@ -0,0 +1,16 @@
package suwayomi
/*
* 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 suwayomi.server.JavalinSetup.javalinSetup
import suwayomi.server.applicationSetup
fun main() {
applicationSetup()
javalinSetup()
}
@@ -0,0 +1,379 @@
package suwayomi.anime
/*
* 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.Javalin
import suwayomi.anime.impl.Anime.getAnime
import suwayomi.anime.impl.Anime.getAnimeThumbnail
import suwayomi.anime.impl.AnimeList.getAnimeList
import suwayomi.anime.impl.Episode.getEpisode
import suwayomi.anime.impl.Episode.getEpisodeList
import suwayomi.anime.impl.Episode.modifyEpisode
import suwayomi.anime.impl.Source.getAnimeSource
import suwayomi.anime.impl.Source.getSourceList
import suwayomi.anime.impl.extension.Extension.getExtensionIcon
import suwayomi.anime.impl.extension.Extension.installExtension
import suwayomi.anime.impl.extension.Extension.uninstallExtension
import suwayomi.anime.impl.extension.Extension.updateExtension
import suwayomi.anime.impl.extension.ExtensionsList.getExtensionList
import suwayomi.server.JavalinSetup.future
object AnimeAPI {
fun defineEndpoints(app: Javalin) {
// list all extensions
app.get("/api/v1/anime/extension/list") { ctx ->
ctx.json(
future {
getExtensionList()
}
)
}
// install extension identified with "pkgName"
app.get("/api/v1/anime/extension/install/:pkgName") { ctx ->
val pkgName = ctx.pathParam("pkgName")
ctx.json(
future {
installExtension(pkgName)
}
)
}
// update extension identified with "pkgName"
app.get("/api/v1/anime/extension/update/:pkgName") { ctx ->
val pkgName = ctx.pathParam("pkgName")
ctx.json(
future {
updateExtension(pkgName)
}
)
}
// uninstall extension identified with "pkgName"
app.get("/api/v1/anime/extension/uninstall/:pkgName") { ctx ->
val pkgName = ctx.pathParam("pkgName")
uninstallExtension(pkgName)
ctx.status(200)
}
// icon for extension named `apkName`
app.get("/api/v1/anime/extension/icon/:apkName") { ctx -> // TODO: move to pkgName
val apkName = ctx.pathParam("apkName")
ctx.result(
future { getExtensionIcon(apkName) }
.thenApply {
ctx.header("content-type", it.second)
it.first
}
)
}
// list of sources
app.get("/api/v1/anime/source/list") { ctx ->
ctx.json(getSourceList())
}
// fetch source with id `sourceId`
app.get("/api/v1/anime/source/:sourceId") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
ctx.json(getAnimeSource(sourceId))
}
// popular animes from source with id `sourceId`
app.get("/api/v1/anime/source/:sourceId/popular/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(
future {
getAnimeList(sourceId, pageNum, popular = true)
}
)
}
// latest animes from source with id `sourceId`
app.get("/api/v1/anime/source/:sourceId/latest/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(
future {
getAnimeList(sourceId, pageNum, popular = false)
}
)
}
// get anime info
app.get("/api/v1/anime/anime/:animeId/") { ctx ->
val animeId = ctx.pathParam("animeId").toInt()
val onlineFetch = ctx.queryParam("onlineFetch", "false").toBoolean()
ctx.json(
future {
getAnime(animeId, onlineFetch)
}
)
}
// anime thumbnail
app.get("api/v1/anime/anime/:animeId/thumbnail") { ctx ->
val animeId = ctx.pathParam("animeId").toInt()
ctx.result(
future { getAnimeThumbnail(animeId) }
.thenApply {
ctx.header("content-type", it.second)
it.first
}
)
}
//
// // list manga's categories
// app.get("api/v1/manga/:mangaId/category/") { ctx ->
// val mangaId = ctx.pathParam("mangaId").toInt()
// ctx.json(getMangaCategories(mangaId))
// }
//
// // adds the manga to category
// app.get("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
// val mangaId = ctx.pathParam("mangaId").toInt()
// val categoryId = ctx.pathParam("categoryId").toInt()
// addMangaToCategory(mangaId, categoryId)
// ctx.status(200)
// }
//
// // removes the manga from the category
// app.delete("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
// val mangaId = ctx.pathParam("mangaId").toInt()
// val categoryId = ctx.pathParam("categoryId").toInt()
// removeMangaFromCategory(mangaId, categoryId)
// ctx.status(200)
// }
//
// get episode list when showing a anime
app.get("/api/v1/anime/anime/:animeId/episodes") { ctx ->
val animeId = ctx.pathParam("animeId").toInt()
val onlineFetch = ctx.queryParam("onlineFetch")?.toBoolean()
ctx.json(future { getEpisodeList(animeId, onlineFetch) })
}
// used to display a episode, get a episode in order to show it's <Quality pending>
app.get("/api/v1/anime/anime/:animeId/episode/:episodeIndex") { ctx ->
val episodeIndex = ctx.pathParam("episodeIndex").toInt()
val animeId = ctx.pathParam("animeId").toInt()
ctx.json(future { getEpisode(episodeIndex, animeId) })
}
// used to modify a episode's parameters
app.patch("/api/v1/anime/anime/:animeId/episode/:episodeIndex") { ctx ->
val episodeIndex = ctx.pathParam("episodeIndex").toInt()
val animeId = ctx.pathParam("animeId").toInt()
val read = ctx.formParam("read")?.toBoolean()
val bookmarked = ctx.formParam("bookmarked")?.toBoolean()
val markPrevRead = ctx.formParam("markPrevRead")?.toBoolean()
val lastPageRead = ctx.formParam("lastPageRead")?.toInt()
modifyEpisode(animeId, episodeIndex, read, bookmarked, markPrevRead, lastPageRead)
ctx.status(200)
}
//
// // get page at index "index"
// app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex/page/:index") { ctx ->
// val mangaId = ctx.pathParam("mangaId").toInt()
// val chapterIndex = ctx.pathParam("chapterIndex").toInt()
// val index = ctx.pathParam("index").toInt()
//
// ctx.result(
// JavalinSetup.future { getPageImage(mangaId, chapterIndex, index) }
// .thenApply {
// ctx.header("content-type", it.second)
// it.first
// }
// )
// }
//
// // submit a chapter for download
// app.put("/api/v1/manga/:mangaId/chapter/:chapterIndex/download") { ctx ->
// // TODO
// }
//
// // cancel a chapter download
// app.delete("/api/v1/manga/:mangaId/chapter/:chapterIndex/download") { ctx ->
// // TODO
// }
//
// // global search, Not implemented yet
// app.get("/api/v1/search/:searchTerm") { ctx ->
// val searchTerm = ctx.pathParam("searchTerm")
// ctx.json(sourceGlobalSearch(searchTerm))
// }
//
// // single source search
// app.get("/api/v1/source/:sourceId/search/:searchTerm/:pageNum") { ctx ->
// val sourceId = ctx.pathParam("sourceId").toLong()
// val searchTerm = ctx.pathParam("searchTerm")
// val pageNum = ctx.pathParam("pageNum").toInt()
// ctx.json(JavalinSetup.future { sourceSearch(sourceId, searchTerm, pageNum) })
// }
//
// // source filter list
// app.get("/api/v1/source/:sourceId/filters/") { ctx ->
// val sourceId = ctx.pathParam("sourceId").toLong()
// ctx.json(sourceFilters(sourceId))
// }
//
// // adds the manga to library
// app.get("api/v1/manga/:mangaId/library") { ctx ->
// val mangaId = ctx.pathParam("mangaId").toInt()
//
// ctx.result(
// JavalinSetup.future { addMangaToLibrary(mangaId) }
// )
// }
//
// // removes the manga from the library
// app.delete("api/v1/manga/:mangaId/library") { ctx ->
// val mangaId = ctx.pathParam("mangaId").toInt()
//
// ctx.result(
// JavalinSetup.future { removeMangaFromLibrary(mangaId) }
// )
// }
//
// // lists mangas that have no category assigned
// app.get("/api/v1/library/") { ctx ->
// ctx.json(getLibraryMangas())
// }
//
// // category list
// app.get("/api/v1/category/") { ctx ->
// ctx.json(Category.getCategoryList())
// }
//
// // category create
// app.post("/api/v1/category/") { ctx ->
// val name = ctx.formParam("name")!!
// Category.createCategory(name)
// ctx.status(200)
// }
//
// // returns some static info of the current app build
// app.get("/api/v1/about/") { ctx ->
// ctx.json(About.getAbout())
// }
//
// // category modification
// app.patch("/api/v1/category/:categoryId") { ctx ->
// val categoryId = ctx.pathParam("categoryId").toInt()
// val name = ctx.formParam("name")
// val isDefault = ctx.formParam("default")?.toBoolean()
// Category.updateCategory(categoryId, name, isDefault)
// ctx.status(200)
// }
//
// // category re-ordering
// app.patch("/api/v1/category/:categoryId/reorder") { ctx ->
// val categoryId = ctx.pathParam("categoryId").toInt()
// val from = ctx.formParam("from")!!.toInt()
// val to = ctx.formParam("to")!!.toInt()
// Category.reorderCategory(categoryId, from, to)
// ctx.status(200)
// }
//
// // category delete
// app.delete("/api/v1/category/:categoryId") { ctx ->
// val categoryId = ctx.pathParam("categoryId").toInt()
// Category.removeCategory(categoryId)
// ctx.status(200)
// }
//
// // returns the manga list associated with a category
// app.get("/api/v1/category/:categoryId") { ctx ->
// val categoryId = ctx.pathParam("categoryId").toInt()
// ctx.json(getCategoryMangaList(categoryId))
// }
//
// // expects a Tachiyomi legacy backup json in the body
// app.post("/api/v1/backup/legacy/import") { ctx ->
// ctx.result(
// future {
// restoreLegacyBackup(ctx.bodyAsInputStream())
// }
// )
// }
//
// // expects a Tachiyomi legacy backup json as a file upload, the file must be named "backup.json"
// app.post("/api/v1/backup/legacy/import/file") { ctx ->
// ctx.result(
// JavalinSetup.future {
// restoreLegacyBackup(ctx.uploadedFile("backup.json")!!.content)
// }
// )
// }
//
// // returns a Tachiyomi legacy backup json created from the current database as a json body
// app.get("/api/v1/backup/legacy/export") { ctx ->
// ctx.contentType("application/json")
// ctx.result(
// JavalinSetup.future {
// createLegacyBackup(
// BackupFlags(
// includeManga = true,
// includeCategories = true,
// includeChapters = true,
// includeTracking = true,
// includeHistory = true,
// )
// )
// }
// )
// }
//
// // returns a Tachiyomi legacy backup json created from the current database as a file
// app.get("/api/v1/backup/legacy/export/file") { ctx ->
// ctx.contentType("application/json")
// val sdf = SimpleDateFormat("yyyy-MM-dd_HH-mm")
// val currentDate = sdf.format(Date())
//
// ctx.header("Content-Disposition", "attachment; filename=\"tachidesk_$currentDate.json\"")
// ctx.result(
// JavalinSetup.future {
// createLegacyBackup(
// BackupFlags(
// includeManga = true,
// includeCategories = true,
// includeChapters = true,
// includeTracking = true,
// includeHistory = true,
// )
// )
// }
// )
// }
//
// // Download queue stats
// app.ws("/api/v1/downloads") { ws ->
// ws.onConnect { ctx ->
// // TODO: send current stat
// // TODO: add to downlad subscribers
// }
// ws.onMessage {
// // TODO: send current stat
// }
// ws.onClose { ctx ->
// // TODO: remove from subscribers
// }
// }
}
}
@@ -0,0 +1,139 @@
package suwayomi.anime.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.SAnime
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import org.kodein.di.DI
import org.kodein.di.conf.global
import org.kodein.di.instance
import suwayomi.anime.impl.AnimeList.proxyThumbnailUrl
import suwayomi.anime.impl.Source.getAnimeSource
import suwayomi.anime.impl.util.GetAnimeHttpSource.getAnimeHttpSource
import suwayomi.anime.model.dataclass.AnimeDataClass
import suwayomi.anime.model.table.AnimeStatus
import suwayomi.anime.model.table.AnimeTable
import suwayomi.server.ApplicationDirs
import suwayomi.tachidesk.impl.util.lang.awaitSingle
import suwayomi.tachidesk.impl.util.network.await
import suwayomi.tachidesk.impl.util.storage.CachedImageResponse.clearCachedImage
import suwayomi.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse
import java.io.InputStream
object Anime {
private fun truncate(text: String?, maxLength: Int): String? {
return if (text?.length ?: 0 > maxLength)
text?.take(maxLength - 3) + "..."
else
text
}
suspend fun getAnime(animeId: Int, onlineFetch: Boolean = false): AnimeDataClass {
var animeEntry = transaction { AnimeTable.select { AnimeTable.id eq animeId }.first() }
return if (animeEntry[AnimeTable.initialized] && !onlineFetch) {
AnimeDataClass(
animeId,
animeEntry[AnimeTable.sourceReference].toString(),
animeEntry[AnimeTable.url],
animeEntry[AnimeTable.title],
proxyThumbnailUrl(animeId),
true,
animeEntry[AnimeTable.artist],
animeEntry[AnimeTable.author],
animeEntry[AnimeTable.description],
animeEntry[AnimeTable.genre],
AnimeStatus.valueOf(animeEntry[AnimeTable.status]).name,
animeEntry[AnimeTable.inLibrary],
getAnimeSource(animeEntry[AnimeTable.sourceReference]),
false
)
} else { // initialize anime
val source = getAnimeHttpSource(animeEntry[AnimeTable.sourceReference])
val fetchedAnime = source.fetchAnimeDetails(
SAnime.create().apply {
url = animeEntry[AnimeTable.url]
title = animeEntry[AnimeTable.title]
}
).awaitSingle()
transaction {
AnimeTable.update({ AnimeTable.id eq animeId }) {
it[AnimeTable.initialized] = true
it[AnimeTable.artist] = fetchedAnime.artist
it[AnimeTable.author] = fetchedAnime.author
it[AnimeTable.description] = truncate(fetchedAnime.description, 4096)
it[AnimeTable.genre] = fetchedAnime.genre
it[AnimeTable.status] = fetchedAnime.status
if (fetchedAnime.thumbnail_url != null && fetchedAnime.thumbnail_url.orEmpty().isNotEmpty())
it[AnimeTable.thumbnail_url] = fetchedAnime.thumbnail_url
}
}
clearAnimeThumbnail(animeId)
animeEntry = transaction { AnimeTable.select { AnimeTable.id eq animeId }.first() }
AnimeDataClass(
animeId,
animeEntry[AnimeTable.sourceReference].toString(),
animeEntry[AnimeTable.url],
animeEntry[AnimeTable.title],
proxyThumbnailUrl(animeId),
true,
fetchedAnime.artist,
fetchedAnime.author,
fetchedAnime.description,
fetchedAnime.genre,
AnimeStatus.valueOf(fetchedAnime.status).name,
false,
getAnimeSource(animeEntry[AnimeTable.sourceReference]),
true
)
}
}
private val applicationDirs by DI.global.instance<ApplicationDirs>()
suspend fun getAnimeThumbnail(animeId: Int): Pair<InputStream, String> {
val saveDir = applicationDirs.animeThumbnailsRoot
val fileName = animeId.toString()
return getCachedImageResponse(saveDir, fileName) {
getAnime(animeId) // make sure is initialized
val animeEntry = transaction { AnimeTable.select { AnimeTable.id eq animeId }.first() }
val sourceId = animeEntry[AnimeTable.sourceReference]
val source = getAnimeHttpSource(sourceId)
val thumbnailUrl = animeEntry[AnimeTable.thumbnail_url]!!
source.client.newCall(
GET(thumbnailUrl, source.headers)
).await()
}
}
private fun clearAnimeThumbnail(animeId: Int) {
val saveDir = applicationDirs.animeThumbnailsRoot
val fileName = animeId.toString()
clearCachedImage(saveDir, fileName)
}
}
@@ -0,0 +1,102 @@
package suwayomi.anime.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.AnimesPage
import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.anime.impl.util.GetAnimeHttpSource.getAnimeHttpSource
import suwayomi.anime.model.dataclass.AnimeDataClass
import suwayomi.anime.model.dataclass.PagedAnimeListDataClass
import suwayomi.anime.model.table.AnimeStatus
import suwayomi.anime.model.table.AnimeTable
import suwayomi.tachidesk.impl.util.lang.awaitSingle
object AnimeList {
fun proxyThumbnailUrl(animeId: Int): String {
return "/api/v1/anime/anime/$animeId/thumbnail"
}
suspend fun getAnimeList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedAnimeListDataClass {
val source = getAnimeHttpSource(sourceId)
val animesPage = if (popular) {
source.fetchPopularAnime(pageNum).awaitSingle()
} else {
if (source.supportsLatest)
source.fetchLatestUpdates(pageNum).awaitSingle()
else
throw Exception("Source $source doesn't support latest")
}
return animesPage.processEntries(sourceId)
}
fun AnimesPage.processEntries(sourceId: Long): PagedAnimeListDataClass {
val animesPage = this
val animeList = transaction {
return@transaction animesPage.animes.map { anime ->
val animeEntry = AnimeTable.select { AnimeTable.url eq anime.url }.firstOrNull()
if (animeEntry == null) { // create anime entry
val animeId = AnimeTable.insertAndGetId {
it[url] = anime.url
it[title] = anime.title
it[artist] = anime.artist
it[author] = anime.author
it[description] = anime.description
it[genre] = anime.genre
it[status] = anime.status
it[thumbnail_url] = anime.thumbnail_url
it[sourceReference] = sourceId
}.value
AnimeDataClass(
animeId,
sourceId.toString(),
anime.url,
anime.title,
proxyThumbnailUrl(animeId),
anime.initialized,
anime.artist,
anime.author,
anime.description,
anime.genre,
AnimeStatus.valueOf(anime.status).name
)
} else {
val animeId = animeEntry[AnimeTable.id].value
AnimeDataClass(
animeId,
sourceId.toString(),
anime.url,
anime.title,
proxyThumbnailUrl(animeId),
true,
animeEntry[AnimeTable.artist],
animeEntry[AnimeTable.author],
animeEntry[AnimeTable.description],
animeEntry[AnimeTable.genre],
AnimeStatus.valueOf(animeEntry[AnimeTable.status]).name,
animeEntry[AnimeTable.inLibrary]
)
}
}
}
return PagedAnimeListDataClass(
animeList,
animesPage.hasNextPage
)
}
}
@@ -0,0 +1,241 @@
package suwayomi.anime.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.SAnime
import eu.kanade.tachiyomi.source.model.SEpisode
import org.jetbrains.exposed.sql.SortOrder.DESC
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import suwayomi.anime.impl.Anime.getAnime
import suwayomi.anime.impl.util.GetAnimeHttpSource.getAnimeHttpSource
import suwayomi.anime.model.dataclass.EpisodeDataClass
import suwayomi.anime.model.table.AnimeTable
import suwayomi.anime.model.table.EpisodeTable
import suwayomi.anime.model.table.toDataClass
import suwayomi.tachidesk.impl.util.lang.awaitSingle
object Episode {
/** get episode list when showing an anime */
suspend fun getEpisodeList(animeId: Int, onlineFetch: Boolean?): List<EpisodeDataClass> {
return if (onlineFetch == true) {
getSourceEpisodes(animeId)
} else {
transaction {
EpisodeTable.select { EpisodeTable.anime eq animeId }.orderBy(EpisodeTable.episodeIndex to DESC)
.map {
EpisodeTable.toDataClass(it)
}
}.ifEmpty {
// If it was explicitly set to offline dont grab episodes
if (onlineFetch == null) {
getSourceEpisodes(animeId)
} else emptyList()
}
}
}
private suspend fun getSourceEpisodes(animeId: Int): List<EpisodeDataClass> {
val animeDetails = getAnime(animeId)
val source = getAnimeHttpSource(animeDetails.sourceId.toLong())
val episodeList = source.fetchEpisodeList(
SAnime.create().apply {
title = animeDetails.title
url = animeDetails.url
}
).awaitSingle()
val episodeCount = episodeList.count()
transaction {
episodeList.reversed().forEachIndexed { index, fetchedEpisode ->
val episodeEntry = EpisodeTable.select { EpisodeTable.url eq fetchedEpisode.url }.firstOrNull()
if (episodeEntry == null) {
EpisodeTable.insert {
it[url] = fetchedEpisode.url
it[name] = fetchedEpisode.name
it[date_upload] = fetchedEpisode.date_upload
it[episode_number] = fetchedEpisode.episode_number
it[scanlator] = fetchedEpisode.scanlator
it[episodeIndex] = index + 1
it[anime] = animeId
}
} else {
EpisodeTable.update({ EpisodeTable.url eq fetchedEpisode.url }) {
it[name] = fetchedEpisode.name
it[date_upload] = fetchedEpisode.date_upload
it[episode_number] = fetchedEpisode.episode_number
it[scanlator] = fetchedEpisode.scanlator
it[episodeIndex] = index + 1
it[anime] = animeId
}
}
}
}
// clear any orphaned episodes that are in the db but not in `episodeList`
val dbEpisodeCount = transaction { EpisodeTable.select { EpisodeTable.anime eq animeId }.count() }
if (dbEpisodeCount > episodeCount) { // we got some clean up due
val dbEpisodeList = transaction { EpisodeTable.select { EpisodeTable.anime eq animeId } }
dbEpisodeList.forEach {
if (it[EpisodeTable.episodeIndex] >= episodeList.size ||
episodeList[it[EpisodeTable.episodeIndex] - 1].url != it[EpisodeTable.url]
) {
transaction {
// PageTable.deleteWhere { PageTable.episode eq it[EpisodeTable.id] }
EpisodeTable.deleteWhere { EpisodeTable.id eq it[EpisodeTable.id] }
}
}
}
}
val dbEpisodeMap = transaction {
EpisodeTable.select { EpisodeTable.anime eq animeId }
.associateBy({ it[EpisodeTable.url] }, { it })
}
return episodeList.mapIndexed { index, it ->
val dbEpisode = dbEpisodeMap.getValue(it.url)
EpisodeDataClass(
it.url,
it.name,
it.date_upload,
it.episode_number,
it.scanlator,
animeId,
dbEpisode[EpisodeTable.isRead],
dbEpisode[EpisodeTable.isBookmarked],
dbEpisode[EpisodeTable.lastPageRead],
episodeCount - index,
episodeList.size
)
}
}
/** used to display a episode, get a episode in order to show it's video */
suspend fun getEpisode(episodeIndex: Int, animeId: Int): EpisodeDataClass {
val episode = getEpisodeList(animeId, false)
.first { it.index == episodeIndex }
val animeEntry = transaction { AnimeTable.select { AnimeTable.id eq animeId }.first() }
val source = getAnimeHttpSource(animeEntry[AnimeTable.sourceReference])
val fetchedLinkUrl = source.fetchEpisodeLink(
SEpisode.create().also {
it.url = episode.url
it.name = episode.name
}
).awaitSingle()
return EpisodeDataClass(
episode.url,
episode.name,
episode.uploadDate,
episode.episodeNumber,
episode.scanlator,
animeId,
episode.read,
episode.bookmarked,
episode.lastPageRead,
episode.index,
episode.episodeCount,
fetchedLinkUrl
)
}
// /** used to display a episode, get a episode in order to show it's pages */
// suspend fun getEpisode(episodeIndex: Int, animeId: Int): EpisodeDataClass {
// val episodeEntry = transaction {
// EpisodeTable.select {
// (EpisodeTable.episodeIndex eq episodeIndex) and (EpisodeTable.anime eq animeId)
// }.first()
// }
// val animeEntry = transaction { MangaTable.select { MangaTable.id eq animeId }.first() }
// val source = getAnimeHttpSource(animeEntry[MangaTable.sourceReference])
//
// val pageList = source.fetchPageList(
// SEpisode.create().apply {
// url = episodeEntry[EpisodeTable.url]
// name = episodeEntry[EpisodeTable.name]
// }
// ).awaitSingle()
//
// val episodeId = episodeEntry[EpisodeTable.id].value
// val episodeCount = transaction { EpisodeTable.select { EpisodeTable.anime eq animeId }.count() }
//
// // update page list for this episode
// transaction {
// pageList.forEach { page ->
// val pageEntry = transaction { PageTable.select { (PageTable.episode eq episodeId) and (PageTable.index eq page.index) }.firstOrNull() }
// if (pageEntry == null) {
// PageTable.insert {
// it[index] = page.index
// it[url] = page.url
// it[imageUrl] = page.imageUrl
// it[episode] = episodeId
// }
// } else {
// PageTable.update({ (PageTable.episode eq episodeId) and (PageTable.index eq page.index) }) {
// it[url] = page.url
// it[imageUrl] = page.imageUrl
// }
// }
// }
// }
//
// return EpisodeDataClass(
// episodeEntry[EpisodeTable.url],
// episodeEntry[EpisodeTable.name],
// episodeEntry[EpisodeTable.date_upload],
// episodeEntry[EpisodeTable.episode_number],
// episodeEntry[EpisodeTable.scanlator],
// animeId,
// episodeEntry[EpisodeTable.isRead],
// episodeEntry[EpisodeTable.isBookmarked],
// episodeEntry[EpisodeTable.lastPageRead],
//
// episodeEntry[EpisodeTable.episodeIndex],
// episodeCount.toInt(),
// pageList.count()
// )
// }
fun modifyEpisode(animeId: Int, episodeIndex: Int, isRead: Boolean?, isBookmarked: Boolean?, markPrevRead: Boolean?, lastPageRead: Int?) {
transaction {
if (listOf(isRead, isBookmarked, lastPageRead).any { it != null }) {
EpisodeTable.update({ (EpisodeTable.anime eq animeId) and (EpisodeTable.episodeIndex eq episodeIndex) }) { update ->
isRead?.also {
update[EpisodeTable.isRead] = it
}
isBookmarked?.also {
update[EpisodeTable.isBookmarked] = it
}
lastPageRead?.also {
update[EpisodeTable.lastPageRead] = it
}
}
}
markPrevRead?.let {
EpisodeTable.update({ (EpisodeTable.anime eq animeId) and (EpisodeTable.episodeIndex less episodeIndex) }) {
it[EpisodeTable.isRead] = markPrevRead
}
}
}
}
}
@@ -0,0 +1,50 @@
package suwayomi.anime.impl
/*
* 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 mu.KotlinLogging
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.anime.impl.extension.Extension.getExtensionIconUrl
import suwayomi.anime.impl.util.GetAnimeHttpSource.getAnimeHttpSource
import suwayomi.anime.model.dataclass.AnimeSourceDataClass
import suwayomi.anime.model.table.AnimeExtensionTable
import suwayomi.anime.model.table.AnimeSourceTable
object Source {
private val logger = KotlinLogging.logger {}
fun getSourceList(): List<AnimeSourceDataClass> {
return transaction {
AnimeSourceTable.selectAll().map {
AnimeSourceDataClass(
it[AnimeSourceTable.id].value.toString(),
it[AnimeSourceTable.name],
it[AnimeSourceTable.lang],
getExtensionIconUrl(AnimeExtensionTable.select { AnimeExtensionTable.id eq it[AnimeSourceTable.extension] }.first()[AnimeExtensionTable.apkName]),
getAnimeHttpSource(it[AnimeSourceTable.id].value).supportsLatest
)
}
}
}
fun getAnimeSource(sourceId: Long): AnimeSourceDataClass {
return transaction {
val source = AnimeSourceTable.select { AnimeSourceTable.id eq sourceId }.firstOrNull()
AnimeSourceDataClass(
sourceId.toString(),
source?.get(AnimeSourceTable.name),
source?.get(AnimeSourceTable.lang),
source?.let { AnimeExtensionTable.select { AnimeExtensionTable.id eq source[AnimeSourceTable.extension] }.first()[AnimeExtensionTable.iconUrl] },
source?.let { getAnimeHttpSource(sourceId).supportsLatest }
)
}
}
}
@@ -0,0 +1,251 @@
package suwayomi.anime.impl.extension
/*
* 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 android.net.Uri
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.AnimeCatalogueSource
import eu.kanade.tachiyomi.source.AnimeSource
import eu.kanade.tachiyomi.source.AnimeSourceFactory
import mu.KotlinLogging
import okhttp3.Request
import okio.buffer
import okio.sink
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import org.kodein.di.DI
import org.kodein.di.conf.global
import org.kodein.di.instance
import suwayomi.anime.impl.extension.ExtensionsList.extensionTableAsDataClass
import suwayomi.anime.impl.extension.github.ExtensionGithubApi
import suwayomi.anime.impl.util.PackageTools.EXTENSION_FEATURE
import suwayomi.anime.impl.util.PackageTools.LIB_VERSION_MAX
import suwayomi.anime.impl.util.PackageTools.LIB_VERSION_MIN
import suwayomi.anime.impl.util.PackageTools.METADATA_NSFW
import suwayomi.anime.impl.util.PackageTools.METADATA_SOURCE_CLASS
import suwayomi.anime.impl.util.PackageTools.dex2jar
import suwayomi.anime.impl.util.PackageTools.getPackageInfo
import suwayomi.anime.impl.util.PackageTools.getSignatureHash
import suwayomi.anime.impl.util.PackageTools.loadExtensionSources
import suwayomi.anime.impl.util.PackageTools.trustedSignatures
import suwayomi.anime.model.table.AnimeExtensionTable
import suwayomi.anime.model.table.AnimeSourceTable
import suwayomi.server.ApplicationDirs
import suwayomi.tachidesk.impl.util.network.await
import suwayomi.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.InputStream
object Extension {
private val logger = KotlinLogging.logger {}
private val applicationDirs by DI.global.instance<ApplicationDirs>()
data class InstallableAPK(
val apkFilePath: String,
val pkgName: String
)
suspend fun installExtension(pkgName: String): Int {
logger.debug("Installing $pkgName")
val extensionRecord = extensionTableAsDataClass().first { it.pkgName == pkgName }
return installAPK {
val apkURL = ExtensionGithubApi.getApkUrl(extensionRecord)
val apkName = Uri.parse(apkURL).lastPathSegment!!
val apkSavePath = "${applicationDirs.extensionsRoot}/$apkName"
// download apk file
downloadAPKFile(apkURL, apkSavePath)
apkSavePath
}
}
suspend fun installAPK(fetcher: suspend () -> String): Int {
val apkFilePath = fetcher()
val apkName = File(apkFilePath).name
// check if we don't have the extension already installed
// if it's installed and we want to update, it first has to be uninstalled
val isInstalled = transaction {
AnimeExtensionTable.select { AnimeExtensionTable.apkName eq apkName }.firstOrNull()
}?.get(AnimeExtensionTable.isInstalled) ?: false
if (!isInstalled) {
val fileNameWithoutType = apkName.substringBefore(".apk")
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType"
val jarFilePath = "$dirPathWithoutType.jar"
val dexFilePath = "$dirPathWithoutType.dex"
val packageInfo = getPackageInfo(apkFilePath)
val pkgName = packageInfo.packageName
if (!packageInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }) {
throw Exception("This apk is not a Tachiyomi extension")
}
// Validate lib version
val libVersion = packageInfo.versionName.substringBeforeLast('.').toDouble()
if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
throw Exception(
"Lib version is $libVersion, while only versions " +
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
)
}
val signatureHash = getSignatureHash(packageInfo)
if (signatureHash == null) {
throw Exception("Package $pkgName isn't signed")
} else if (signatureHash !in trustedSignatures) {
// TODO: allow trusting keys
throw Exception("This apk is not a signed with the official tachiyomi signature")
}
val isNsfw = packageInfo.applicationInfo.metaData.getString(METADATA_NSFW) == "1"
val className = packageInfo.packageName + packageInfo.applicationInfo.metaData.getString(METADATA_SOURCE_CLASS)
logger.debug("Main class for extension is $className")
dex2jar(apkFilePath, jarFilePath, fileNameWithoutType)
// clean up
// File(apkFilePath).delete()
File(dexFilePath).delete()
// collect sources from the extension
val sources: List<AnimeCatalogueSource> = when (val instance = loadExtensionSources(jarFilePath, className)) {
is AnimeSource -> listOf(instance)
is AnimeSourceFactory -> instance.createSources()
else -> throw RuntimeException("Unknown source class type! ${instance.javaClass}")
}.map { it as AnimeCatalogueSource }
val langs = sources.map { it.lang }.toSet()
val extensionLang = when (langs.size) {
0 -> ""
1 -> langs.first()
else -> "all"
}
val extensionName = packageInfo.applicationInfo.nonLocalizedLabel.toString().substringAfter("Tachiyomi: ")
// update extension info
transaction {
if (AnimeExtensionTable.select { AnimeExtensionTable.pkgName eq pkgName }.firstOrNull() == null) {
AnimeExtensionTable.insert {
it[this.apkName] = apkName
it[name] = extensionName
it[this.pkgName] = packageInfo.packageName
it[versionName] = packageInfo.versionName
it[versionCode] = packageInfo.versionCode
it[lang] = extensionLang
it[this.isNsfw] = isNsfw
}
}
AnimeExtensionTable.update({ AnimeExtensionTable.pkgName eq pkgName }) {
it[this.isInstalled] = true
it[this.classFQName] = className
}
val extensionId = AnimeExtensionTable.select { AnimeExtensionTable.pkgName eq pkgName }.first()[AnimeExtensionTable.id].value
sources.forEach { httpSource ->
AnimeSourceTable.insert {
it[id] = httpSource.id
it[name] = httpSource.name
it[lang] = httpSource.lang
it[extension] = extensionId
}
logger.debug("Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}")
}
}
return 201 // we installed successfully
} else {
return 302 // extension was already installed
}
}
private val network: NetworkHelper by injectLazy()
private suspend fun downloadAPKFile(url: String, savePath: String) {
val request = Request.Builder().url(url).build()
val response = network.client.newCall(request).await()
val downloadedFile = File(savePath)
downloadedFile.sink().buffer().use { sink ->
response.body!!.source().use { source ->
sink.writeAll(source)
sink.flush()
}
}
}
fun uninstallExtension(pkgName: String) {
logger.debug("Uninstalling $pkgName")
val extensionRecord = transaction { AnimeExtensionTable.select { AnimeExtensionTable.pkgName eq pkgName }.first() }
val fileNameWithoutType = extensionRecord[AnimeExtensionTable.apkName].substringBefore(".apk")
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
transaction {
val extensionId = extensionRecord[AnimeExtensionTable.id].value
AnimeSourceTable.deleteWhere { AnimeSourceTable.extension eq extensionId }
if (extensionRecord[AnimeExtensionTable.isObsolete])
AnimeExtensionTable.deleteWhere { AnimeExtensionTable.pkgName eq pkgName }
else
AnimeExtensionTable.update({ AnimeExtensionTable.pkgName eq pkgName }) {
it[isInstalled] = false
}
}
if (File(jarPath).exists()) {
File(jarPath).delete()
}
}
suspend fun updateExtension(pkgName: String): Int {
val targetExtension = ExtensionsList.updateMap.remove(pkgName)!!
uninstallExtension(pkgName)
transaction {
AnimeExtensionTable.update({ AnimeExtensionTable.pkgName eq pkgName }) {
it[name] = targetExtension.name
it[versionName] = targetExtension.versionName
it[versionCode] = targetExtension.versionCode
it[lang] = targetExtension.lang
it[isNsfw] = targetExtension.isNsfw
it[apkName] = targetExtension.apkName
it[iconUrl] = targetExtension.iconUrl
it[hasUpdate] = false
}
}
return installExtension(pkgName)
}
suspend fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
val iconUrl = transaction { AnimeExtensionTable.select { AnimeExtensionTable.apkName eq apkName }.first() }[AnimeExtensionTable.iconUrl]
val saveDir = "${applicationDirs.extensionsRoot}/icon"
return getCachedImageResponse(saveDir, apkName) {
network.client.newCall(
GET(iconUrl)
).await()
}
}
fun getExtensionIconUrl(apkName: String): String {
return "/api/v1/anime/extension/icon/$apkName"
}
}
@@ -0,0 +1,132 @@
package suwayomi.anime.impl.extension
/*
* 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 mu.KotlinLogging
import org.jetbrains.exposed.sql.deleteWhere
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.anime.impl.extension.Extension.getExtensionIconUrl
import suwayomi.anime.impl.extension.github.ExtensionGithubApi
import suwayomi.anime.impl.extension.github.OnlineExtension
import suwayomi.anime.model.dataclass.AnimeExtensionDataClass
import suwayomi.anime.model.table.AnimeExtensionTable
import java.util.concurrent.ConcurrentHashMap
object ExtensionsList {
private val logger = KotlinLogging.logger {}
var lastUpdateCheck: Long = 0
var updateMap = ConcurrentHashMap<String, OnlineExtension>()
/** 60,000 milliseconds = 60 seconds */
private const val ExtensionUpdateDelayTime = 60 * 1000
suspend fun getExtensionList(): List<AnimeExtensionDataClass> {
// update if {ExtensionUpdateDelayTime} seconds has passed or requested offline and database is empty
if (lastUpdateCheck + ExtensionUpdateDelayTime < System.currentTimeMillis()) {
logger.debug("Getting extensions list from the internet")
lastUpdateCheck = System.currentTimeMillis()
val foundExtensions = ExtensionGithubApi.findExtensions()
updateExtensionDatabase(foundExtensions)
} else {
logger.debug("used cached extension list")
}
return extensionTableAsDataClass()
}
fun extensionTableAsDataClass() = transaction {
AnimeExtensionTable.selectAll().map {
AnimeExtensionDataClass(
it[AnimeExtensionTable.apkName],
getExtensionIconUrl(it[AnimeExtensionTable.apkName]),
it[AnimeExtensionTable.name],
it[AnimeExtensionTable.pkgName],
it[AnimeExtensionTable.versionName],
it[AnimeExtensionTable.versionCode],
it[AnimeExtensionTable.lang],
it[AnimeExtensionTable.isNsfw],
it[AnimeExtensionTable.isInstalled],
it[AnimeExtensionTable.hasUpdate],
it[AnimeExtensionTable.isObsolete],
)
}
}
private fun updateExtensionDatabase(foundExtensions: List<OnlineExtension>) {
transaction {
foundExtensions.forEach { foundExtension ->
val extensionRecord = AnimeExtensionTable.select { AnimeExtensionTable.pkgName eq foundExtension.pkgName }.firstOrNull()
if (extensionRecord != null) {
if (extensionRecord[AnimeExtensionTable.isInstalled]) {
when {
foundExtension.versionCode > extensionRecord[AnimeExtensionTable.versionCode] -> {
// there is an update
AnimeExtensionTable.update({ AnimeExtensionTable.pkgName eq foundExtension.pkgName }) {
it[hasUpdate] = true
}
updateMap.putIfAbsent(foundExtension.pkgName, foundExtension)
}
foundExtension.versionCode < extensionRecord[AnimeExtensionTable.versionCode] -> {
// some how the user installed an invalid version
AnimeExtensionTable.update({ AnimeExtensionTable.pkgName eq foundExtension.pkgName }) {
it[isObsolete] = true
}
}
}
} else {
// extension is not installed so we can overwrite the data without a care
AnimeExtensionTable.update({ AnimeExtensionTable.pkgName eq foundExtension.pkgName }) {
it[name] = foundExtension.name
it[versionName] = foundExtension.versionName
it[versionCode] = foundExtension.versionCode
it[lang] = foundExtension.lang
it[isNsfw] = foundExtension.isNsfw
it[apkName] = foundExtension.apkName
it[iconUrl] = foundExtension.iconUrl
}
}
} else {
// insert new record
AnimeExtensionTable.insert {
it[name] = foundExtension.name
it[pkgName] = foundExtension.pkgName
it[versionName] = foundExtension.versionName
it[versionCode] = foundExtension.versionCode
it[lang] = foundExtension.lang
it[isNsfw] = foundExtension.isNsfw
it[apkName] = foundExtension.apkName
it[iconUrl] = foundExtension.iconUrl
}
}
}
// deal with obsolete extensions
AnimeExtensionTable.selectAll().forEach { extensionRecord ->
val foundExtension = foundExtensions.find { it.pkgName == extensionRecord[AnimeExtensionTable.pkgName] }
if (foundExtension == null) {
// not in the repo, so this extensions is obsolete
if (extensionRecord[AnimeExtensionTable.isInstalled]) {
// is installed so we should mark it as obsolete
AnimeExtensionTable.update({ AnimeExtensionTable.pkgName eq extensionRecord[AnimeExtensionTable.pkgName] }) {
it[isObsolete] = true
}
} else {
// is not installed so we can remove the record without a care
AnimeExtensionTable.deleteWhere { AnimeExtensionTable.pkgName eq extensionRecord[AnimeExtensionTable.pkgName] }
}
}
}
}
}
}
@@ -0,0 +1,80 @@
package suwayomi.anime.impl.extension.github
/*
* 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 com.github.salomonbrys.kotson.int
import com.github.salomonbrys.kotson.string
import com.google.gson.JsonArray
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.network.NetworkHelper
import okhttp3.Request
import suwayomi.anime.model.dataclass.AnimeExtensionDataClass
import suwayomi.tachidesk.impl.util.network.UnzippingInterceptor
import uy.kohesive.injekt.injectLazy
object ExtensionGithubApi {
const val BASE_URL = "https://raw.githubusercontent.com"
const val REPO_URL_PREFIX = "$BASE_URL/jmir1/tachiyomi-extensions/repo"
private const val LIB_VERSION_MIN = 1.3
private const val LIB_VERSION_MAX = 1.3
private fun parseResponse(json: JsonArray): List<OnlineExtension> {
return json
.map { it.asJsonObject }
.filter { element ->
val versionName = element["version"].string
val libVersion = versionName.substringBeforeLast('.').toDouble()
libVersion in LIB_VERSION_MIN..LIB_VERSION_MAX
}
.map { element ->
val name = element["name"].string.substringAfter("Tachiyomi: ")
val pkgName = element["pkg"].string
val apkName = element["apk"].string
val versionName = element["version"].string
val versionCode = element["code"].int
val lang = element["lang"].string
val nsfw = element["nsfw"].int == 1
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
OnlineExtension(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
}
}
suspend fun findExtensions(): List<OnlineExtension> {
val response = getRepo()
return parseResponse(response)
}
fun getApkUrl(extension: AnimeExtensionDataClass): String {
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
}
private val client by lazy {
val network: NetworkHelper by injectLazy()
network.client.newBuilder()
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.header("Content-Encoding", "gzip")
.header("Content-Type", "application/json")
.build()
}
.addInterceptor(UnzippingInterceptor())
.build()
}
private fun getRepo(): com.google.gson.JsonArray {
val request = Request.Builder()
.url("$REPO_URL_PREFIX/index.json.gz")
.build()
val response = client.newCall(request).execute().use { response -> response.body!!.string() }
return JsonParser.parseString(response).asJsonArray
}
}
@@ -0,0 +1,12 @@
package suwayomi.anime.impl.extension.github
data class OnlineExtension(
val name: String,
val pkgName: String,
val versionName: String,
val versionCode: Int,
val lang: String,
val isNsfw: Boolean,
val apkName: String,
val iconUrl: String
)
@@ -0,0 +1,57 @@
package suwayomi.anime.impl.util
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.AnimeSource
import eu.kanade.tachiyomi.source.AnimeSourceFactory
import eu.kanade.tachiyomi.source.online.AnimeHttpSource
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.kodein.di.DI
import org.kodein.di.conf.global
import org.kodein.di.instance
import suwayomi.anime.impl.util.PackageTools.loadExtensionSources
import suwayomi.anime.model.table.AnimeExtensionTable
import suwayomi.anime.model.table.AnimeSourceTable
import suwayomi.server.ApplicationDirs
import java.util.concurrent.ConcurrentHashMap
object GetAnimeHttpSource {
private val sourceCache = ConcurrentHashMap<Long, AnimeHttpSource>()
private val applicationDirs by DI.global.instance<ApplicationDirs>()
fun getAnimeHttpSource(sourceId: Long): AnimeHttpSource {
val cachedResult: AnimeHttpSource? = sourceCache[sourceId]
if (cachedResult != null) {
return cachedResult
}
val sourceRecord = transaction {
AnimeSourceTable.select { AnimeSourceTable.id eq sourceId }.first()
}
val extensionId = sourceRecord[AnimeSourceTable.extension]
val extensionRecord = transaction {
AnimeExtensionTable.select { AnimeExtensionTable.id eq extensionId }.first()
}
val apkName = extensionRecord[AnimeExtensionTable.apkName]
val className = extensionRecord[AnimeExtensionTable.classFQName]
val jarName = apkName.substringBefore(".apk") + ".jar"
val jarPath = "${applicationDirs.extensionsRoot}/$jarName"
when (val instance = loadExtensionSources(jarPath, className)) {
is AnimeSource -> listOf(instance)
is AnimeSourceFactory -> instance.createSources()
else -> throw Exception("Unknown source class type! ${instance.javaClass}")
}.forEach {
sourceCache[it.id] = it as AnimeHttpSource
}
return sourceCache[sourceId]!!
}
}
@@ -0,0 +1,145 @@
package suwayomi.anime.impl.util
/*
* 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 android.content.pm.PackageInfo
import android.content.pm.Signature
import android.os.Bundle
import com.googlecode.d2j.dex.Dex2jar
import com.googlecode.d2j.reader.MultiDexFileReader
import com.googlecode.dex2jar.tools.BaksmaliBaseDexExceptionHandler
import eu.kanade.tachiyomi.util.lang.Hash
import mu.KotlinLogging
import net.dongliu.apk.parser.ApkFile
import net.dongliu.apk.parser.ApkParsers
import org.kodein.di.DI
import org.kodein.di.conf.global
import org.kodein.di.instance
import org.w3c.dom.Element
import org.w3c.dom.Node
import suwayomi.server.ApplicationDirs
import xyz.nulldev.androidcompat.pm.InstalledPackage.Companion.toList
import xyz.nulldev.androidcompat.pm.toPackageInfo
import java.io.File
import java.net.URL
import java.net.URLClassLoader
import java.nio.file.Files
import java.nio.file.Path
import javax.xml.parsers.DocumentBuilderFactory
object PackageTools {
private val logger = KotlinLogging.logger {}
private val applicationDirs by DI.global.instance<ApplicationDirs>()
const val EXTENSION_FEATURE = "tachiyomi.animeextension"
const val METADATA_SOURCE_CLASS = "tachiyomi.animeextension.class"
const val METADATA_SOURCE_FACTORY = "tachiyomi.animeextension.factory"
const val METADATA_NSFW = "tachiyomi.animeextension.nsfw"
const val LIB_VERSION_MIN = 1.3
const val LIB_VERSION_MAX = 1.3
private const val officialSignature = "50ab1d1e3a20d204d0ad6d334c7691c632e41b98dfa132bf385695fdfa63839c" // jmir1's key
var trustedSignatures = mutableSetOf<String>() + officialSignature
/**
* Convert dex to jar, a wrapper for the dex2jar library
*/
fun dex2jar(dexFile: String, jarFile: String, fileNameWithoutType: String) {
// adopted from com.googlecode.dex2jar.tools.Dex2jarCmd.doCommandLine
// source at: https://github.com/DexPatcher/dex2jar/tree/v2.1-20190905-lanchon/dex-tools/src/main/java/com/googlecode/dex2jar/tools/Dex2jarCmd.java
val jarFilePath = File(jarFile).toPath()
val reader = MultiDexFileReader.open(Files.readAllBytes(File(dexFile).toPath()))
val handler = BaksmaliBaseDexExceptionHandler()
Dex2jar
.from(reader)
.withExceptionHandler(handler)
.reUseReg(false)
.topoLogicalSort()
.skipDebug(true)
.optimizeSynchronized(false)
.printIR(false)
.noCode(false)
.skipExceptions(false)
.to(jarFilePath)
if (handler.hasException()) {
val errorFile: Path = File(applicationDirs.extensionsRoot).toPath().resolve("$fileNameWithoutType-error.txt")
logger.error(
"""
Detail Error Information in File $errorFile
Please report this file to one of following link if possible (any one).
https://sourceforge.net/p/dex2jar/tickets/
https://bitbucket.org/pxb1988/dex2jar/issues
https://github.com/pxb1988/dex2jar/issues
dex2jar@googlegroups.com
""".trimIndent()
)
handler.dump(errorFile, emptyArray<String>())
}
}
/** A modified version of `xyz.nulldev.androidcompat.pm.InstalledPackage.info` */
fun getPackageInfo(apkFilePath: String): PackageInfo {
val apk = File(apkFilePath)
return ApkParsers.getMetaInfo(apk).toPackageInfo(apk).apply {
val parsed = ApkFile(apk)
val dbFactory = DocumentBuilderFactory.newInstance()
val dBuilder = dbFactory.newDocumentBuilder()
val doc = parsed.manifestXml.byteInputStream().use {
dBuilder.parse(it)
}
logger.debug(parsed.manifestXml)
applicationInfo.metaData = Bundle().apply {
val appTag = doc.getElementsByTagName("application").item(0)
appTag?.childNodes?.toList()
.orEmpty()
.asSequence()
.filter {
it.nodeType == Node.ELEMENT_NODE
}.map {
it as Element
}.filter {
it.tagName == "meta-data"
}.forEach {
putString(
it.attributes.getNamedItem("android:name").nodeValue,
it.attributes.getNamedItem("android:value").nodeValue
)
}
}
signatures = (
parsed.apkSingers.flatMap { it.certificateMetas }
/*+ parsed.apkV2Singers.flatMap { it.certificateMetas }*/
) // Blocked by: https://github.com/hsiafan/apk-parser/issues/72
.map { Signature(it.data) }.toTypedArray()
}
}
fun getSignatureHash(pkgInfo: PackageInfo): String? {
val signatures = pkgInfo.signatures
return if (signatures != null && signatures.isNotEmpty()) {
Hash.sha256(signatures.first().toByteArray())
} else {
null
}
}
/**
* loads the extension main class called $className from the jar located at $jarPath
* It may return an instance of HttpSource or SourceFactory depending on the extension.
*/
fun loadExtensionSources(jarPath: String, className: String): Any {
val classLoader = URLClassLoader(arrayOf<URL>(URL("file:$jarPath")))
val classToLoad = Class.forName(className, false, classLoader)
return classToLoad.getDeclaredConstructor().newInstance()
}
}
@@ -0,0 +1,36 @@
package suwayomi.anime.model.dataclass
/*
* 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 suwayomi.anime.model.table.AnimeStatus
data class AnimeDataClass(
val id: Int,
val sourceId: String,
val url: String,
val title: String,
val thumbnailUrl: String? = null,
val initialized: Boolean = false,
val artist: String? = null,
val author: String? = null,
val description: String? = null,
val genre: String? = null,
val status: String = AnimeStatus.UNKNOWN.name,
val inLibrary: Boolean = false,
val source: AnimeSourceDataClass? = null,
val freshData: Boolean = false
)
data class PagedAnimeListDataClass(
val mangaList: List<AnimeDataClass>,
val hasNextPage: Boolean
)
@@ -0,0 +1,24 @@
package suwayomi.anime.model.dataclass
/*
* 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/. */
data class AnimeExtensionDataClass(
val apkName: String,
val iconUrl: String,
val name: String,
val pkgName: String,
val versionName: String,
val versionCode: Int,
val lang: String,
val isNsfw: Boolean,
val installed: Boolean,
val hasUpdate: Boolean,
val obsolete: Boolean,
)
@@ -0,0 +1,16 @@
package suwayomi.anime.model.dataclass
/*
* 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/. */
data class AnimeSourceDataClass(
val id: String,
val name: String?,
val lang: String?,
val iconUrl: String?,
val supportsLatest: Boolean?
)
@@ -0,0 +1,35 @@
package suwayomi.anime.model.dataclass
/*
* 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/. */
data class EpisodeDataClass(
val url: String,
val name: String,
val uploadDate: Long,
val episodeNumber: Float,
val scanlator: String?,
val animeId: Int,
/** chapter is read */
val read: Boolean,
/** chapter is bookmarked */
val bookmarked: Boolean,
/** last read page, zero means not read/no data */
val lastPageRead: Int,
/** this chapter's index, starts with 1 */
val index: Int,
/** total episode count, used to calculate if there's a next and prev episode */
val episodeCount: Int? = null,
/** used to construct pages in the front-end */
val linkUrl: String? = null,
)
@@ -0,0 +1,31 @@
package suwayomi.anime.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
object AnimeExtensionTable : IntIdTable() {
val apkName = varchar("apk_name", 1024)
// default is the local source icon from tachiyomi
val iconUrl = varchar("icon_url", 2048)
.default("https://raw.githubusercontent.com/tachiyomiorg/tachiyomi/64ba127e7d43b1d7e6d58a6f5c9b2bd5fe0543f7/app/src/main/res/mipmap-xxxhdpi/ic_local_source.webp")
val name = varchar("name", 128)
val pkgName = varchar("pkg_name", 128)
val versionName = varchar("version_name", 16)
val versionCode = integer("version_code")
val lang = varchar("lang", 10)
val isNsfw = bool("is_nsfw")
val isInstalled = bool("is_installed").default(false)
val hasUpdate = bool("has_update").default(false)
val isObsolete = bool("is_obsolete").default(false)
val classFQName = varchar("class_name", 1024).default("") // fully qualified name
}
@@ -0,0 +1,18 @@
package suwayomi.anime.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.IdTable
object AnimeSourceTable : IdTable<Long>() {
override val id = long("id").entityId()
val name = varchar("name", 128)
val lang = varchar("lang", 10)
val extension = reference("extension", AnimeExtensionTable)
val partOfFactorySource = bool("part_of_factory_source").default(false)
}
@@ -0,0 +1,65 @@
package suwayomi.anime.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 eu.kanade.tachiyomi.source.model.SAnime
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.ResultRow
import suwayomi.tachidesk.impl.MangaList.proxyThumbnailUrl
import suwayomi.tachidesk.model.dataclass.MangaDataClass
import suwayomi.tachidesk.model.table.MangaStatus.Companion
object AnimeTable : IntIdTable() {
val url = varchar("url", 2048)
val title = varchar("title", 512)
val initialized = bool("initialized").default(false)
val artist = varchar("artist", 64).nullable()
val author = varchar("author", 64).nullable()
val description = varchar("description", 4096).nullable()
val genre = varchar("genre", 1024).nullable()
val status = integer("status").default(SAnime.UNKNOWN)
val thumbnail_url = varchar("thumbnail_url", 2048).nullable()
val inLibrary = bool("in_library").default(false)
val defaultCategory = bool("default_category").default(true)
// source is used by some ancestor of IntIdTable
val sourceReference = long("source")
}
fun AnimeTable.toDataClass(mangaEntry: ResultRow) =
MangaDataClass(
mangaEntry[this.id].value,
mangaEntry[sourceReference].toString(),
mangaEntry[url],
mangaEntry[title],
proxyThumbnailUrl(mangaEntry[this.id].value),
mangaEntry[initialized],
mangaEntry[artist],
mangaEntry[author],
mangaEntry[description],
mangaEntry[genre],
Companion.valueOf(mangaEntry[status]).name,
mangaEntry[inLibrary]
)
enum class AnimeStatus(val status: Int) {
UNKNOWN(0),
ONGOING(1),
COMPLETED(2),
LICENSED(3);
companion object {
fun valueOf(value: Int): AnimeStatus = values().find { it.status == value } ?: UNKNOWN
}
}
@@ -0,0 +1,46 @@
package suwayomi.anime.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
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.anime.model.dataclass.EpisodeDataClass
object EpisodeTable : IntIdTable() {
val url = varchar("url", 2048)
val name = varchar("name", 512)
val date_upload = long("date_upload").default(0)
val episode_number = float("episode_number").default(-1f)
val scanlator = varchar("scanlator", 128).nullable()
val isRead = bool("read").default(false)
val isBookmarked = bool("bookmark").default(false)
val lastPageRead = integer("last_page_read").default(0)
// index is reserved by a function
val episodeIndex = integer("index")
val anime = reference("anime", AnimeTable)
}
fun EpisodeTable.toDataClass(episodeEntry: ResultRow) =
EpisodeDataClass(
episodeEntry[url],
episodeEntry[name],
episodeEntry[date_upload],
episodeEntry[episode_number],
episodeEntry[scanlator],
episodeEntry[anime].value,
episodeEntry[isRead],
episodeEntry[isBookmarked],
episodeEntry[lastPageRead],
episodeEntry[episodeIndex],
transaction { EpisodeTable.select { anime eq episodeEntry[anime] }.count().toInt() }
)
@@ -0,0 +1,81 @@
package suwayomi.server
import io.javalin.Javalin
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.future.future
import mu.KotlinLogging
import suwayomi.anime.AnimeAPI
import suwayomi.server.util.Browser
import suwayomi.tachidesk.TachideskAPI
import java.io.IOException
import java.util.concurrent.CompletableFuture
import kotlin.concurrent.thread
/*
* 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 JavalinSetup {
private val logger = KotlinLogging.logger {}
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
fun <T> future(block: suspend CoroutineScope.() -> T): CompletableFuture<T> {
return scope.future(block = block)
}
fun javalinSetup() {
var hasWebUiBundled = false
val app = Javalin.create { config ->
try {
// if the bellow line throws an exception then webUI is not bundled
this::class.java.getResource("/react/index.html")
// no exception so we can tell javalin to serve webUI
hasWebUiBundled = true
config.addStaticFiles("/react")
config.addSinglePageRoot("/", "/react/index.html")
} catch (e: RuntimeException) {
logger.warn("react build files are missing.")
hasWebUiBundled = false
}
config.enableCorsForAllOrigins()
}.events { event ->
event.serverStarted {
if (hasWebUiBundled && serverConfig.initialOpenInBrowserEnabled) {
Browser.openInBrowser()
}
}
}.start(serverConfig.ip, serverConfig.port)
// when JVM is prompted to shutdown, stop javalin gracefully
Runtime.getRuntime().addShutdownHook(
thread(start = false) {
app.stop()
}
)
app.exception(NullPointerException::class.java) { e, ctx ->
logger.error("NullPointerException while handling the request", e)
ctx.status(404)
}
app.exception(NoSuchElementException::class.java) { e, ctx ->
logger.error("NoSuchElementException while handling the request", e)
ctx.status(404)
}
app.exception(IOException::class.java) { e, ctx ->
logger.error("IOException while handling the request", e)
ctx.status(500)
ctx.result(e.message ?: "Internal Server Error")
}
TachideskAPI.defineEndpoints(app)
AnimeAPI.defineEndpoints(app)
}
}
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.server
package suwayomi.server
/*
* Copyright (C) Contributors to the Suwayomi project
@@ -10,18 +10,20 @@ package ir.armor.tachidesk.server
import com.typesafe.config.Config
import io.github.config4k.getValue
import xyz.nulldev.ts.config.ConfigModule
import xyz.nulldev.ts.config.GlobalConfigManager
import xyz.nulldev.ts.config.debugLogsEnabled
class ServerConfig(config: Config) : ConfigModule(config) {
val ip: String by config
val port: Int by config
// proxy
val socksProxy: Boolean by config
val socksProxyEnabled: Boolean by config
val socksProxyHost: String by config
val socksProxyPort: String by config
// misc
val debugLogsEnabled: Boolean by config
val debugLogsEnabled: Boolean = debugLogsEnabled(GlobalConfigManager.config)
val systemTrayEnabled: Boolean by config
val initialOpenInBrowserEnabled: Boolean by config
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.server
package suwayomi.server
/*
* Copyright (C) Contributors to the Suwayomi project
@@ -7,62 +7,81 @@ package ir.armor.tachidesk.server
* 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 ch.qos.logback.classic.Level
import eu.kanade.tachiyomi.App
import ir.armor.tachidesk.Main
import ir.armor.tachidesk.database.makeDataBaseTables
import ir.armor.tachidesk.server.util.systemTray
import mu.KotlinLogging
import net.harawata.appdirs.AppDirsFactory
import org.kodein.di.DI
import org.kodein.di.bind
import org.kodein.di.conf.global
import org.kodein.di.singleton
import suwayomi.server.database.databaseUp
import suwayomi.server.util.AppMutex.handleAppMutex
import suwayomi.server.util.SystemTray.systemTray
import xyz.nulldev.androidcompat.AndroidCompat
import xyz.nulldev.androidcompat.AndroidCompatInitializer
import xyz.nulldev.ts.config.ApplicationRootDir
import xyz.nulldev.ts.config.ConfigKodeinModule
import xyz.nulldev.ts.config.GlobalConfigManager
import java.io.File
private val logger = KotlinLogging.logger {}
object applicationDirs {
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)!!
class ApplicationDirs(
val dataRoot: String = ApplicationRootDir
) {
val extensionsRoot = "$dataRoot/extensions"
val thumbnailsRoot = "$dataRoot/thumbnails"
val mangaThumbnailsRoot = "$dataRoot/manga-thumbnails"
val animeThumbnailsRoot = "$dataRoot/anime-thumbnails"
val mangaRoot = "$dataRoot/manga"
}
val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() }
val systemTray by lazy { systemTray() }
val systemTrayInstance by lazy { systemTray() }
val androidCompat by lazy { AndroidCompat() }
fun applicationSetup() {
// register server config
GlobalConfigManager.registerModule(
ServerConfig.register(GlobalConfigManager.config)
)
logger.info("Running Tachidesk ${BuildConfig.VERSION} revision ${BuildConfig.REVISION}")
// set application wide logging level
if (serverConfig.debugLogsEnabled) {
(mu.KotlinLogging.logger(org.slf4j.Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger).level = Level.DEBUG
}
// Application dirs
val applicationDirs = ApplicationDirs()
DI.global.addImport(
DI.Module("Server") {
bind<ApplicationDirs>() with singleton { applicationDirs }
}
)
// make dirs we need
listOf(
applicationDirs.dataRoot,
applicationDirs.extensionsRoot,
"${applicationDirs.extensionsRoot}/icon",
applicationDirs.thumbnailsRoot
applicationDirs.extensionsRoot + "/icon",
applicationDirs.mangaThumbnailsRoot,
applicationDirs.animeThumbnailsRoot,
).forEach {
File(it).mkdirs()
}
// register Tachidesk's config which is dubbed "ServerConfig"
GlobalConfigManager.registerModule(
ServerConfig.register(GlobalConfigManager.config)
)
// Make sure only one instance of the app is running
handleAppMutex()
// Load config API
DI.global.addImport(ConfigKodeinModule().create())
// Load Android compatibility dependencies
AndroidCompatInitializer().init()
// start app
androidCompat.startApp(App())
// create conf file if doesn't exist
try {
val dataConfFile = File("${applicationDirs.dataRoot}/server.conf")
if (!dataConfFile.exists()) {
Main::class.java.getResourceAsStream("/server-reference.conf").use { input ->
JavalinSetup::class.java.getResourceAsStream("/server-reference.conf").use { input ->
dataConfFile.outputStream().use { output ->
input.copyTo(output)
}
@@ -72,22 +91,16 @@ fun applicationSetup() {
logger.error("Exception while creating initial server.conf:\n", e)
}
makeDataBaseTables()
databaseUp()
// create system tray
if (serverConfig.systemTrayEnabled)
if (serverConfig.systemTrayEnabled) {
try {
systemTray
} catch (e: Exception) {
systemTrayInstance
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
e.printStackTrace()
}
// Load config API
DI.global.addImport(ConfigKodeinModule().create())
// Load Android compatibility dependencies
AndroidCompatInitializer().init()
// start app
androidCompat.startApp(App())
}
// Disable jetty's logging
System.setProperty("org.eclipse.jetty.util.log.announce", "false")
@@ -95,7 +108,9 @@ fun applicationSetup() {
System.setProperty("org.eclipse.jetty.LEVEL", "OFF")
// socks proxy settings
System.getProperties()["proxySet"] = serverConfig.socksProxy.toString()
System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost
System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort
if (serverConfig.socksProxyEnabled) {
System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost
System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort
logger.info("Socks Proxy is enabled to ${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}")
}
}
@@ -0,0 +1,32 @@
package suwayomi.server.database
/*
* 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.sql.Database
import org.kodein.di.DI
import org.kodein.di.conf.global
import org.kodein.di.instance
import suwayomi.server.ApplicationDirs
import suwayomi.server.database.migration.lib.loadMigrationsFrom
import suwayomi.server.database.migration.lib.runMigrations
object DBManager {
val db by lazy {
val applicationDirs by DI.global.instance<ApplicationDirs>()
Database.connect("jdbc:h2:${applicationDirs.dataRoot}/database", "org.h2.Driver")
}
}
fun databaseUp() {
// must mention db object so the lazy block executes
val db = DBManager.db
db.useNestedTransactions = true
val migrations = loadMigrationsFrom("suwayomi.server.database.migration")
runMigrations(migrations)
}
@@ -0,0 +1,135 @@
package suwayomi.server.database.migration
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.SManga
import org.jetbrains.exposed.dao.id.IdTable
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.server.database.migration.lib.Migration
@Suppress("ClassName", "unused")
class M0001_Initial : Migration() {
private class ExtensionTable : IntIdTable() {
init {
varchar("apk_name", 1024)
// default is the local source icon from tachiyomi
varchar("icon_url", 2048)
.default("https://raw.githubusercontent.com/tachiyomiorg/tachiyomi/64ba127e7d43b1d7e6d58a6f5c9b2bd5fe0543f7/app/src/main/res/mipmap-xxxhdpi/ic_local_source.webp")
varchar("name", 128)
varchar("pkg_name", 128)
varchar("version_name", 16)
integer("version_code")
varchar("lang", 10)
bool("is_nsfw")
bool("is_installed").default(false)
bool("has_update").default(false)
bool("is_obsolete").default(false)
varchar("class_name", 1024).default("") // fully qualified name
}
}
private class SourceTable(extensionTable: ExtensionTable) : IdTable<Long>() {
override val id = long("id").entityId()
init {
varchar("name", 128)
varchar("lang", 10)
reference("extension", extensionTable)
bool("part_of_factory_source").default(false)
}
}
private class MangaTable : IntIdTable() {
init {
varchar("url", 2048)
varchar("title", 512)
bool("initialized").default(false)
varchar("artist", 64).nullable()
varchar("author", 64).nullable()
varchar("description", 4096).nullable()
varchar("genre", 1024).nullable()
// val status = enumeration("status", MangaStatus::class).default(MangaStatus.UNKNOWN)
integer("status").default(SManga.UNKNOWN)
varchar("thumbnail_url", 2048).nullable()
bool("in_library").default(false)
bool("default_category").default(true)
// source is used by some ancestor of IntIdTable
long("source")
}
}
private class ChapterTable(mangaTable: MangaTable) : IntIdTable() {
init {
varchar("url", 2048)
varchar("name", 512)
long("date_upload").default(0)
float("chapter_number").default(-1f)
varchar("scanlator", 128).nullable()
bool("read").default(false)
bool("bookmark").default(false)
integer("last_page_read").default(0)
integer("number_in_list")
reference("manga", mangaTable)
}
}
private class PageTable(chapterTable: ChapterTable) : IntIdTable() {
init {
integer("index")
varchar("url", 2048)
varchar("imageUrl", 2048).nullable()
reference("chapter", chapterTable)
}
}
private class CategoryTable : IntIdTable() {
init {
varchar("name", 64)
bool("is_landing").default(false)
integer("order").default(0)
}
}
private class CategoryMangaTable(categoryTable: CategoryTable, mangaTable: MangaTable) : IntIdTable() {
init {
reference("category", categoryTable)
reference("manga", mangaTable)
}
}
/** initial migration, create all tables */
override fun run() {
transaction {
val extensionTable = ExtensionTable()
val sourceTable = SourceTable(extensionTable)
val mangaTable = MangaTable()
val chapterTable = ChapterTable(mangaTable)
val pageTable = PageTable(chapterTable)
val categoryTable = CategoryTable()
val categoryMangaTable = CategoryMangaTable(categoryTable, mangaTable)
SchemaUtils.create(
extensionTable,
sourceTable,
mangaTable,
chapterTable,
pageTable,
categoryTable,
categoryMangaTable,
)
}
}
}

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