Compare commits

...

212 Commits

Author SHA1 Message Date
Aria Moradi bc2072e81f bump version
CI Publish / Validate Gradle Wrapper (push) Successful in 13s
CI Publish / Build artifacts and release (push) Failing after 15s
2021-09-19 17:36:46 +04:30
Aria Moradi f36bc3f643 update WebUI 2021-09-19 17:34:18 +04:30
Aria Moradi f7901ad843 fix windows paths 2021-09-19 16:43:16 +04:30
Aria Moradi 3771030ed6 closes #202 2021-09-19 14:24:13 +04:30
Aria Moradi 57197e58b5 fix Task path 2021-09-19 14:14:42 +04:30
Aria Moradi ac601399ac update WebUI 2021-09-19 14:14:21 +04:30
Aria Moradi 6a0e221153 fix compile 2021-09-19 01:01:20 +04:30
Aria Moradi 6a949fc851 Minor cleanup 2021-09-19 00:59:04 +04:30
Aria Moradi f1a077dc2f update CHANGELOG 2021-09-18 22:09:34 +04:30
Mitchell Syer f20962b02b Gradle Updates (#199)
* Cleanup and update gradle, update dependencies

* Duplicate Jsoup
2021-09-18 22:07:19 +04:30
Mitchell Syer 77e057f244 Update BytecodeEditor to use Java NIO Paths (#200) 2021-09-18 21:57:15 +04:30
Aria Moradi 2c5114c770 bump version
CI Publish / Validate Gradle Wrapper (push) Successful in 13s
CI Publish / Build artifacts and release (push) Failing after 16s
2021-09-18 21:07:38 +04:30
Aria Moradi a30895a199 update WebUI 2021-09-18 21:03:01 +04:30
Aria Moradi 6d46d4b3da closes #131 2021-09-18 20:30:56 +04:30
Aria Moradi 006efbbb77 add ChapterRecognition from tachiyomi, closes #10 2021-09-18 19:40:44 +04:30
Aria Moradi 52334087ad add support for Archive chapters to Local source 2021-09-18 19:14:06 +04:30
Aria Moradi ea8fb2c70a add comment 2021-09-18 16:11:25 +04:30
Aria Moradi 531d148718 don't save Local source chapters into disk again! 2021-09-18 14:53:59 +04:30
Aria Moradi d83ddea323 better URLs 2021-09-18 14:49:38 +04:30
Aria Moradi b5dea34090 update WebUI 2021-09-18 02:39:15 +04:30
Aria Moradi 7d8e3202b5 lint 2021-09-18 02:28:52 +04:30
Aria Moradi d956e0af4b add Local Source (#196) 2021-09-18 02:20:30 +04:30
Aria Moradi 9ad70990b5 update WebUI 2021-09-18 02:19:43 +04:30
Aria Moradi 82d711f077 fix typo 2021-09-18 02:19:38 +04:30
Aria Moradi 5fc28ef711 handle when title is changing properly 2021-09-18 02:10:35 +04:30
Aria Moradi 1f3dc682e2 migrate to kotlinx.json 2021-09-18 01:43:10 +04:30
Aria Moradi cce7768246 clean up imports 2021-09-18 01:03:06 +04:30
Aria Moradi 01172b0664 better exception messages 2021-09-18 01:00:42 +04:30
Aria Moradi 1ca11fdd34 add Local Source 2021-09-18 00:47:50 +04:30
Aria Moradi 52a064ae45 lint 2021-09-17 16:32:14 +04:30
Aria Moradi 6c62ddf927 depricate zero based chapters 2021-09-17 16:31:47 +04:30
Aria Moradi 47e04c08d0 fix lint complain 2021-09-17 15:17:18 +04:30
Aria Moradi d1601874b4 [SKIP CI] update CHANGELOG.md 2021-09-17 01:31:13 +04:30
Aria Moradi 6bc19af041 add ability to delete downloaded chapters 2021-09-17 01:29:42 +04:30
Aria Moradi 4e72a3886f actaully make sure the chapter exists 2021-09-17 00:38:15 +04:30
Aria Moradi 8e6b219eea update CHANGELOG.md 2021-09-14 04:18:19 +04:30
Aria Moradi bd638251e4 add BasicAuth support 2021-09-14 04:17:01 +04:30
Aria Moradi 0173d5e4b3 migrate to Javalin 4 2021-09-14 03:23:00 +04:30
Aria Moradi 4f364e134b remove expand char limit on MangaTable columns 2021-09-13 20:57:41 +04:30
Aria Moradi 94d2519717 add CHANGELOG for the next release 2021-09-13 20:14:02 +04:30
Aria Moradi 6a11d2e357 bump version
CI Publish / Validate Gradle Wrapper (push) Successful in 11s
CI Publish / Build artifacts and release (push) Failing after 16s
2021-09-13 19:32:05 +04:30
Aria Moradi 0a9e0bc9e4 update WebUI 2021-09-13 19:31:07 +04:30
Aria Moradi 5914e367d1 better CHANGELOG 2021-09-13 19:29:54 +04:30
Mitchell Syer aeaed888d4 Update Proto models to match Tachi preview (#192) 2021-09-12 04:04:48 +04:30
Aria Moradi a0f054b005 CHANGELOG.md 2021-09-11 19:47:06 +04:30
Aria Moradi 4498e9d444 beter handling of uninstalling Extensions 2021-09-11 16:26:04 +04:30
Aria Moradi 43e0763fef fix a bug where if two sources return the same URL, a false duplicate might be detected 2021-09-11 14:47:03 +04:30
Aria Moradi a519c8a482 better PR links 2021-09-11 03:18:36 +04:30
Aria Moradi 19bc595a2a update CHANGELOG.md 2021-09-11 03:13:26 +04:30
Aria Moradi db07825c58 remove println instance 2021-09-11 03:09:26 +04:30
Aria Moradi b199e3bf0e fix installing apk with weird name 2021-09-11 01:21:22 +04:30
Aria Moradi 1f9ea0891e add some logging 2021-09-11 00:48:17 +04:30
Aria Moradi 0a7aa48f1e fix apk log and apk name 2021-09-11 00:14:52 +04:30
Aria Moradi 4b65b7da6c rm dummy 2021-09-10 21:42:52 +04:30
Mosei 183a7dac4b Create docker_build_stable (#184)
* Create docker_build_stable

Add docker build workflow

* Rename docker_build_stable to docker_build_stable.yml

* Update docker_build_stable.yml

* Update docker_build_stable.yml
2021-09-10 21:42:31 +04:30
Aria Moradi 6726f008c1 add dummy file to trigger workflow 2021-09-10 21:22:28 +04:30
Mosei 89cf0c140f Update publish.yml (#190) 2021-09-10 20:49:57 +04:30
Mosei 504025ce80 Update build_push.yml (#191) 2021-09-10 20:49:33 +04:30
Aria Moradi fee9e914f1 fix exposed crash 2021-09-10 00:45:00 +04:30
Aria Moradi 76efa71c68 update WebUI 2021-09-09 23:05:58 +04:30
Aria Moradi 26e61959ae update WebUI 2021-09-09 16:46:19 +04:30
Aria Moradi 9a8956ef9d update dependencies 2021-09-09 16:41:41 +04:30
Aria Moradi 10d3ffc2f6 better UX 2021-09-09 06:05:09 +04:30
Aria Moradi 090399f61d add support for installing external APK 2021-09-09 05:22:45 +04:30
Aria Moradi ae7d975a92 run won't get stuck now 2021-09-09 05:15:51 +04:30
Aria Moradi 55ec6bcafe add TODO for changing later 2021-09-09 05:15:08 +04:30
Aria Moradi f0566d15af fix compiler warning 2021-09-08 20:27:29 +04:30
Aria Moradi a730b692bc update WebUI 2021-09-08 20:24:22 +04:30
Aria Moradi 826d767423 [SKIP CI] update CHANGELOG.md 2021-09-08 20:23:45 +04:30
Aria Moradi 6d227c7fcd Fix typo 2021-09-06 22:23:26 +04:30
Aria Moradi 7d9d97840e Update README.md 2021-09-06 14:41:00 +04:30
Mosei 110ded45a0 Update README.md (#189)
Remove depreciated arbuilder’s docker link
2021-09-05 21:16:25 +04:30
Aria Moradi 7872b593c7 Credit to @Syer10 2021-09-05 17:50:45 +04:30
Aria Moradi 90be30bddb add support for the new gnere type in WebUI 2021-09-05 01:40:34 +04:30
Mitchell Syer a298c61dab Change type of Manga.genre to a List<String> (#188) 2021-09-05 01:36:52 +04:30
Aria Moradi eb416c45bd remove not-code changes from CHANGELOG 2021-09-05 00:54:01 +04:30
Aria Moradi b05b817aeb Update README.md 2021-09-03 18:52:06 +04:30
Aria Moradi 666602283a update
CI Publish / Validate Gradle Wrapper (push) Successful in 12s
CI Publish / Build artifacts and release (push) Failing after 15s
2021-09-03 07:59:41 +04:30
Aria Moradi ac040a4bae fix oopsies n'shit 2021-09-03 07:57:53 +04:30
Aria Moradi b4982c8f22 update 2021-09-03 07:55:15 +04:30
Aria Moradi ce3ad92095 remove node module cache, won't need it anymore 2021-09-03 07:54:07 +04:30
Aria Moradi 8e1ac8698c bump version 2021-09-03 07:48:31 +04:30
Aria Moradi b60a39c7cb update 2021-09-03 07:46:11 +04:30
Aria Moradi 3b3e8ba4c8 add new unix bundles to release 2021-09-03 07:45:11 +04:30
Aria Moradi e387f6d3be update 2021-09-03 07:44:00 +04:30
Aria Moradi 799d469cb6 bump electron version 2021-09-03 07:43:13 +04:30
Aria Moradi a54a596fa7 fix macOS bundler 2021-09-03 07:39:28 +04:30
Aria Moradi 92d73d0285 add linux and macOS bundler script 2021-09-03 06:50:42 +04:30
Aria Moradi acb752e4e8 update jre 2021-09-03 05:43:33 +04:30
Aria Moradi 9e377abba6 update CHANGELOG 2021-09-03 04:55:01 +04:30
Aria Moradi 04552c0923 update WebUI 2021-09-03 04:52:34 +04:30
Aria Moradi 2e1fb85b73 [skip ci] update changelog 2021-09-03 04:49:38 +04:30
Aria Moradi 50db32d9b4 this is dumb 2021-09-03 04:48:32 +04:30
Aria Moradi 28743d953e [skip ci] r857 2021-09-03 04:41:00 +04:30
Aria Moradi b109d26aa7 fixed typo 2021-09-03 04:39:45 +04:30
Aria Moradi 7fd21bd06d [skip ci] r855 2021-09-02 17:45:52 +04:30
Aria Moradi 12e0ffb466 text cleanup 2021-09-02 17:43:43 +04:30
Aria Moradi 9259341df8 missed this changelog 2021-09-02 17:31:30 +04:30
Aria Moradi 8e8aca7e7b Update CONTRIBUTING.md 2021-09-02 17:29:26 +04:30
Aria Moradi 7f0bcd987b Update CONTRIBUTING.md 2021-09-02 17:24:32 +04:30
Aria Moradi ef21de95cb add continues changelog file 2021-09-02 17:20:53 +04:30
Aria Moradi ca3246de02 bump version 2021-09-02 15:21:27 +04:30
Aria Moradi f0940b7926 bump version
CI Publish / Validate Gradle Wrapper (push) Successful in 15s
CI Publish / Build artifacts and release (push) Failing after 17s
2021-08-31 17:43:23 +04:30
Aria Moradi 0066e0b901 suppress warnings 2021-08-31 17:33:29 +04:30
Aria Moradi 9771f566b0 better comments 2021-08-30 02:48:10 +04:30
Aria Moradi 38ad4c6dec refactor 2021-08-30 02:44:35 +04:30
Aria Moradi 37cf80a188 code cleanup 2021-08-30 02:38:15 +04:30
Aria Moradi c86ee53f66 resolve compiler warnings 2021-08-29 22:25:43 +04:30
Aria Moradi c2cea7e797 can serialize Search Filters 2021-08-29 22:19:44 +04:30
Aria Moradi a8ef6cdd4f change category re-order url 2021-08-29 21:52:23 +04:30
Aria Moradi 53d157fee8 update 2021-08-29 21:47:09 +04:30
Aria Moradi c2e07b13f6 fix categories not being normalized 2021-08-29 20:09:17 +04:30
Aria Moradi 2e8cc48311 update WebUI 2021-08-29 02:11:11 +04:30
Aria Moradi f6f811eb77 update WebUI 2021-08-29 01:59:36 +04:30
Aria Moradi ac5528fb15 add when the statement was true 2021-08-27 04:49:53 +04:30
Aria Moradi 940d2b7862 bump version 2021-08-26 22:31:06 +04:30
Aria Moradi 835fe3dad3 sorround with try, catch as it might throw an exception 2021-08-26 22:24:54 +04:30
Aria Moradi dfaecc08c5 add realUrl to Manga, reperesents open in WebView URL 2021-08-26 22:11:51 +04:30
Aria Moradi 87f5e9b847 fix migration number 2021-08-26 22:10:51 +04:30
Aria Moradi 3d3939e808 better logs 2021-08-26 22:10:27 +04:30
Aria Moradi 90822e3858 merge manga data while restoring backup 2021-08-26 16:28:45 +04:30
Aria Moradi 14eec47e9c correct value for inLibrary 2021-08-26 01:34:56 +04:30
Aria Moradi 15ed3fcc69 actual fix for source order 2021-08-26 01:31:59 +04:30
Aria Moradi fd8fa9f3ef fix chapter restore order 2021-08-26 01:28:42 +04:30
Aria Moradi b81075f4a7 fix docker builds faling? 2021-08-24 22:23:39 +04:30
Aria Moradi f11a52e8e1 we don't need that feild anymore 2021-08-24 22:23:00 +04:30
Aria Moradi 9c007483d4 better method of detemining if a source is Nsfw 2021-08-24 02:44:13 +04:30
Aria Moradi ff4e818e4c add some comments 2021-08-23 21:48:27 +04:30
Aria Moradi 45a50ca0c1 add isNsfw to SourceDataClass 2021-08-23 21:46:28 +04:30
Aria Moradi 65d9021c37 close response 2021-08-23 06:10:31 +04:30
Aria Moradi 66481a0391 NPE fix suggested by @syer10 2021-08-23 06:05:04 +04:30
Aria Moradi a14a82bc9a fix oppsie, sync dependencies with tachiyomi 2021-08-23 05:27:39 +04:30
Aria Moradi 756c57a16e also intercept on 403 2021-08-23 04:56:27 +04:30
Aria Moradi 8b19e34dc5 Update README.md 2021-08-23 04:38:32 +04:30
Aria Moradi 50083019ee add copyright notices 2021-08-23 04:37:30 +04:30
Aria Moradi 155272e638 add new keys 2021-08-23 04:28:07 +04:30
Aria Moradi 08443ceb3d remove comment 2021-08-23 04:20:04 +04:30
Aria Moradi c215696f04 have a lighter log level 2021-08-23 04:17:40 +04:30
Aria Moradi 5ca42bf9b6 make it compile 2021-08-23 04:02:55 +04:30
Aria Moradi 3272b9dec5 add CloudflareInterceptor from TachiWeb-Server 2021-08-23 03:45:10 +04:30
Aria Moradi 2ebd5da4aa bump kotlinter version 2021-08-22 19:00:33 +04:30
Aria Moradi 34f024ace2 migrate dex2jar dependency to @ThexXTURBOXx version 2021-08-21 16:36:34 +04:30
Aria Moradi b31f2d50f6 No more legacy backup 2021-08-21 06:39:12 +04:30
Aria Moradi da44d3b2b4 bump to v0.4.7
CI Publish / Validate Gradle Wrapper (push) Successful in 12s
CI Publish / Build artifacts and release (push) Failing after 17s
2021-08-21 06:30:45 +04:30
Aria Moradi 99ec2aca6a update WebUI 2021-08-21 06:27:14 +04:30
Aria Moradi 6c278604ec got rid of legacy backups 2021-08-21 06:23:58 +04:30
Aria Moradi 1e094a467a not TODO 2021-08-21 06:13:55 +04:30
Aria Moradi 978ccfeeba the true commit 2021-08-21 06:12:22 +04:30
Aria Moradi e93d66d8a1 add backup validation endpoints 2021-08-21 06:08:17 +04:30
Aria Moradi c29a749833 proto export support 2021-08-21 05:48:05 +04:30
Aria Moradi b08d5d1261 all forms of Default are illegal 2021-08-21 05:10:09 +04:30
Aria Moradi 9b129789e9 all forms of Default are illegal 2021-08-21 05:05:01 +04:30
Aria Moradi a76a6d2798 creating a categort named Default is illegal 2021-08-21 03:58:46 +04:30
Aria Moradi 086a760378 update WebUI 2021-08-21 03:55:22 +04:30
Aria Moradi f78c8d4fd8 Default is now a category, no more library 2021-08-21 03:54:16 +04:30
Aria Moradi 7b91489997 better print 2021-08-21 01:20:18 +04:30
Aria Moradi 36a8980c95 TODO block no longer relevant 2021-08-21 01:11:47 +04:30
Aria Moradi 7c65640cb7 include extra chapter data in restore 2021-08-21 00:37:50 +04:30
Aria Moradi d70e68495a restoring with clean db and not installed extensions work 2021-08-21 00:18:03 +04:30
Aria Moradi 2586202772 better comments 2021-08-19 21:11:53 +04:30
Aria Moradi b5f771368a put back dex2jar where it should be 2021-08-19 03:15:35 +04:30
Aria Moradi 0c28320ce3 better debug launcher 2021-08-19 02:54:36 +04:30
Aria Moradi c8b4fbc36b new observation 2021-08-19 02:06:48 +04:30
Aria Moradi e9b07849fe move dex2jar to server, lint 2021-08-19 01:47:26 +04:30
Aria Moradi 409260af6f Merge pull request #176 from Suwayomi/protobuf
protobuf backup support
2021-08-19 00:47:32 +04:30
Aria Moradi d3d53d1a4e initial support for portobuf backup 2021-08-19 00:46:45 +04:30
Aria Moradi e2db191f70 consolidate the external backup api 2021-08-18 23:34:39 +04:30
Aria Moradi d61816734d add all proto backup classes we need 2021-08-18 22:58:56 +04:30
Aria Moradi f4dad8058f Merge branch 'master' into protobuf 2021-08-18 21:51:12 +04:30
Aria Moradi 70bdb375c3 Update README.md 2021-08-18 19:10:46 +04:30
Aria Moradi e724ab0a29 Update README.md 2021-08-18 19:10:16 +04:30
Aria Moradi 7d0ee2ac11 Update README.md 2021-08-18 06:17:04 +04:30
Aria Moradi 59b7e852e2 Update README.md 2021-08-18 06:15:44 +04:30
Aria Moradi b2eb1a391d Update README.md 2021-08-18 05:31:34 +04:30
Aria Moradi 9b3aee98d3 Update CONTRIBUTING.md 2021-08-18 05:30:19 +04:30
Aria Moradi 0476f4144c Update CONTRIBUTING.md 2021-08-18 05:29:24 +04:30
Aria Moradi ed77f45fae Update CONTRIBUTING.md 2021-08-18 05:28:26 +04:30
Aria Moradi 0cd529e746 Update CONTRIBUTING.md 2021-08-18 05:26:55 +04:30
Aria Moradi 5969048318 Update README.md 2021-08-18 05:19:42 +04:30
Aria Moradi d1a7f8baa0 Update README.md 2021-08-18 05:04:22 +04:30
Aria Moradi 18dc936002 Update README.md (#177)
* Update README.md

* Update README.md
2021-08-18 04:50:10 +04:30
Aria Moradi b4b7b5d572 bump to v0.4.6
CI Publish / Validate Gradle Wrapper (push) Successful in 12s
CI Publish / Build artifacts and release (push) Failing after 17s
2021-08-18 04:29:14 +04:30
Aria Moradi 291c2e692d clean up build.gradle files, move constants to buildSrc 2021-08-18 04:24:58 +04:30
Aria Moradi 8a9a4f21b1 remove some stuff we don't use 2021-08-18 04:06:13 +04:30
Aria Moradi cd31332b39 better download progress 2021-08-18 03:55:52 +04:30
Aria Moradi cc8d2162a0 fix compile issue 2021-08-18 02:59:07 +04:30
Aria Moradi e6313cdc67 yeet improvments from jui 2021-08-18 01:21:17 +04:30
Aria Moradi 5af64892e7 Merge branch 'master' into protobuf 2021-08-18 00:28:42 +04:30
Aria Moradi a5578a7ac7 fix compile warnings 2021-08-17 23:54:02 +04:30
Aria Moradi fcdda6406e update dependencies 2021-08-17 23:53:41 +04:30
Aria Moradi 9bdd9f8aa6 better endpoint urls based on suggestion from @mgn-norm 2021-08-17 21:45:19 +04:30
Aria Moradi f3856f051b protobuf backup endpoints 2021-08-17 20:09:31 +04:30
Aria Moradi d3a6662c60 make it compile 2021-08-15 03:16:13 +04:30
Aria Moradi 5474eddf84 fix some inconsitencies 2021-08-15 02:41:23 +04:30
Aria Moradi b666cd47d4 fix shouldOverwrite 2021-08-15 00:25:08 +04:30
Aria Moradi 8a986383fe fixes #175, better webUI download task 2021-08-14 17:10:41 +04:30
Aria Moradi 9fa17f617e add anime seach functionality 2021-08-11 08:47:07 +04:30
Aria Moradi e46e165704 bump to v0.4.5
CI Publish / Validate Gradle Wrapper (push) Successful in 12s
CI Publish / Build artifacts and release (push) Failing after 17s
2021-08-11 00:39:08 +04:30
Aria Moradi a6af423fb4 Merge branch 'master' of github.com:Suwayomi/Tachidesk-Server 2021-08-10 09:42:41 +04:30
Aria Moradi 3397e694c0 sync anime lib implementation with 12 (#133)
* sync anime lib implementation with 11

* fix wrong api

* delete unused classes

* adapt to lib 12

* add LICENSE for eu.kanade.tachiyomi

* changes for lib 12

* update to lib 12

* update webUI
2021-08-10 09:42:14 +04:30
Aria Moradi 77ff82505e fix wrong api 2021-08-10 04:01:08 +04:30
Aria Moradi c3f2838270 fix name tag generator 2021-08-10 03:40:49 +04:30
Aria Moradi aed7f205b6 update webUI 2021-08-10 03:39:36 +04:30
Aria Moradi 95b3587f7a update webUI 2021-08-10 02:04:35 +04:30
Aria Moradi a4baeb995a refactor endpoints to the new styles 2021-08-09 23:18:41 +04:30
Aria Moradi 032ab54206 fix tag generator 2021-08-09 23:18:06 +04:30
Aria Moradi 1f9c1eb1c0 only open browser when appropriate 2021-08-09 07:15:41 +04:30
Aria Moradi a213e568ba update property strings 2021-08-09 06:59:16 +04:30
Aria Moradi 7aeaeb4b86 move copyright notice to it's place 2021-08-09 06:49:12 +04:30
Aria Moradi 81aef4b8fa remove un-used files 2021-08-09 06:48:02 +04:30
Aria Moradi 31f0b6a16c ability to override server.conf with java -D arguments 2021-08-09 06:45:49 +04:30
Aria Moradi 44e3a682fd update Project name 2021-08-09 01:27:09 +04:30
175 changed files with 4715 additions and 7745 deletions
+6 -7
View File
@@ -74,15 +74,18 @@ jobs:
id: GenTagName
run: |
cd master/server/build
genTag=$(ls *.jar | sed -e's/Tachidesk-\|.jar//g')
genTag=$(ls *.jar | sed -e's/Tachidesk-Server-\|.jar//g')
echo "$genTag"
echo "::set-output name=value::$genTag"
- name: make windows packages
- name: make bundle packages
run: |
cd master/scripts
./windows-bundler.sh win32
./windows-bundler.sh win64
./unix-bundler.sh linux-x64
./unix-bundler.sh macOS-x64
./unix-bundler.sh macOS-arm64
# - name: Mock make windows packages
# run: |
@@ -116,11 +119,7 @@ jobs:
uses: ncipollo/release-action@v1
with:
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
artifacts: "master/server/build/*.jar,master/server/build/*.zip"
artifacts: "master/server/build/*.jar,master/server/build/*.zip,master/server/build/*.tar.gz"
owner: "Suwayomi"
repo: "Tachidesk-Server-preview"
tag: ${{ steps.GenTagName.outputs.value }}
- name: Run Docker build workflow
run: |
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token ${{ secrets.DEPLOY_PREVIEW_TOKEN }}" -d '{"ref":"main", "inputs":{"tachidesk_release_type": "preview"}}' https://api.github.com/repos/suwayomi/docker-tachidesk/actions/workflows/build_container_images.yml/dispatches
+15
View File
@@ -0,0 +1,15 @@
name: Docker Build Stable
on:
release:
types: [published]
jobs:
build_publish_docker_container:
runs-on: ubuntu-latest
steps:
- name: run docker build and publish script
run: |
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token ${{ secrets.DEPLOY_PREVIEW_TOKEN }}" -d '{"ref":"main", "inputs":{"tachidesk_release_type": "stable"}}' https://api.github.com/repos/suwayomi/docker-tachidesk/actions/workflows/build_container_images.yml/dispatches
+5 -14
View File
@@ -51,13 +51,6 @@ jobs:
cd master
curl https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
- name: Cache node_modules
uses: actions/cache@v2
with:
path: |
**/webUI/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/webUI/yarn.lock') }}
- name: Build and copy webUI, Build Jar
uses: eskatos/gradle-command-action@v1
env:
@@ -70,23 +63,21 @@ jobs:
dependencies-cache-enabled: true
configuration-cache-enabled: true
- name: make windows packages
- name: make bundle packages
run: |
cd master/scripts
./windows-bundler.sh win32
./windows-bundler.sh win64
./unix-bundler.sh linux-x64
./unix-bundler.sh macOS-x64
./unix-bundler.sh macOS-arm64
- name: Upload Release
uses: xresloader/upload-to-github-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
file: "master/server/build/*.jar;master/server/build/*.zip"
file: "master/server/build/*.jar;master/server/build/*.zip;master/server/build/*.tar.gz"
tags: true
draft: true
verbose: true
- name: Run Docker build workflow
run: |
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token ${{ secrets.DEPLOY_PREVIEW_TOKEN }}" -d '{"ref":"main", "inputs":{"tachidesk_release_type": "stable"}}' https://api.github.com/repos/suwayomi/docker-tachidesk/actions/workflows/build_container_images.yml/dispatches
+3 -2
View File
@@ -11,6 +11,7 @@ server/tmp/
server/tachiserver-data/
# bundle asset downlaods
OpenJDK*.zip
electron-*.zip
OpenJDK*.*
zulu*jre*
electron-*.*
rcedit-*
-4
View File
@@ -1,4 +0,0 @@
dependencies {
// Config API, moved to the global build.gradle
// implementation("com.typesafe:config:1.4.0")
}
@@ -1,7 +1,5 @@
package xyz.nulldev.ts.config
import net.harawata.appdirs.AppDirsFactory
/*
* Copyright (C) Contributors to the Suwayomi project
*
@@ -9,10 +7,14 @@ import net.harawata.appdirs.AppDirsFactory
* 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 net.harawata.appdirs.AppDirsFactory
const val CONFIG_PREFIX = "suwayomi.tachidesk.config"
val ApplicationRootDir: String
get(): String {
return System.getProperty(
"suwayomi.tachidesk.server.rootDir",
"$CONFIG_PREFIX.server.rootDir",
AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
)
}
@@ -35,6 +35,7 @@ open class ConfigManager {
/**
* Get a config module (Java API)
*/
@Suppress("UNCHECKED_CAST")
fun <T : ConfigModule> module(type: Class<T>): T = loadedModules[type] as T
/**
@@ -47,7 +48,7 @@ open class ConfigManager {
val baseConfig =
ConfigFactory.parseMap(
mapOf(
"ts.server.rootDir" to ApplicationRootDir
"androidcompat.rootDir" to "$ApplicationRootDir/android-compat" // override AndroidCompat's rootDir
)
)
@@ -1,8 +1,44 @@
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 com.typesafe.config.Config
import io.github.config4k.getValue
import kotlin.reflect.KProperty
/**
* Abstract config module.
*/
@Suppress("UNUSED_PARAMETER")
abstract class ConfigModule(config: Config)
/**
* Abstract jvm-commandline-argument-overridable config module.
*/
abstract class SystemPropertyOverridableConfigModule(config: Config, moduleName: String): ConfigModule(config) {
val overridableConfig = SystemPropertyOverrideDelegate(config, moduleName)
}
/** Defines a config property that is overridable with jvm `-D` commandline arguments prefixed with [CONFIG_PREFIX] */
class SystemPropertyOverrideDelegate(val config: Config, val moduleName: String) {
inline operator fun <R, reified T> getValue(thisRef: R, property: KProperty<*>): T {
val configValue: T = config.getValue(thisRef, property)
val combined = System.getProperty(
"$CONFIG_PREFIX.$moduleName.${property.name}",
configValue.toString()
)
return when(T::class.simpleName) {
"Int" -> combined.toInt()
"Boolean" -> combined.toBoolean()
// add more types as needed
else -> combined // covers String
} as T
}
}
@@ -17,4 +17,4 @@ fun setLogLevel(level: Level) {
}
fun debugLogsEnabled(config: Config)
= System.getProperty("suwayomi.tachidesk.server.debugLogsEnabled", config.getString("server.debugLogsEnabled")).toBoolean()
= System.getProperty("suwayomi.tachidesk.config.server.debugLogsEnabled", config.getString("server.debugLogsEnabled")).toBoolean()
@@ -1 +0,0 @@
xyz.nulldev.ts.api.v2.java.impl.ServerAPIImpl
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

File diff suppressed because one or more lines are too long
@@ -1,22 +0,0 @@
[
{
"label": "Sync",
"icon": "import_export",
"type": "nested",
"prefs": []
},
{
"label": "Server",
"icon": "dns",
"type": "nested",
"prefs": [
{
"label": "Password authentication",
"type": "text-password",
"default": "",
"key": "pref_ts_server_password",
"hint": "Enter a password"
}
]
}
]
+6 -55
View File
@@ -1,77 +1,28 @@
plugins {
application
kotlin("plugin.serialization")
}
repositories {
mavenCentral()
maven {
url = uri("https://jitpack.io")
}
maven {
url = uri("https://maven.google.com")
}
}
dependencies {
// Android stub library
implementation(fileTree("lib/"))
// Android JAR libs
// compileOnly( fileTree(dir: new File(rootProject.rootDir, "libs/other"), include: "*.jar")
// JSON
compileOnly("com.google.code.gson:gson:2.8.6")
// Javassist
compileOnly("org.javassist:javassist:3.27.0-GA")
// XML
compileOnly(group= "xmlpull", name= "xmlpull", version= "1.1.3.1")
compileOnly("xmlpull:xmlpull:1.1.3.4a")
// Config API
implementation(project(":AndroidCompat:Config"))
// APK sig verifier
compileOnly("com.android.tools.build:apksig:4.2.0-alpha13")
compileOnly("com.android.tools.build:apksig:7.1.0-alpha12")
// AndroidX annotations
compileOnly("androidx.annotation:annotation:1.2.0-alpha01")
compileOnly("androidx.annotation:annotation:1.2.0")
// substitute for duktape-android
// 'org.mozilla:rhino' includes some code that we don't need so use 'org.mozilla:rhino-runtime' instead
implementation("org.mozilla:rhino-runtime:1.7.13")
// 'org.mozilla:rhino-engine' provides the same interface as 'javax.script' a.k.a Nashorn
implementation("org.mozilla:rhino-engine:1.7.13")
implementation("org.mozilla:rhino-runtime:1.7.13") // slimmer version of 'org.mozilla:rhino'
implementation("org.mozilla:rhino-engine:1.7.13") // provides the same interface as 'javax.script' a.k.a Nashorn
// Kotlin wrapper around Java Preferences, makes certain things easier
val multiplatformSettingsVersion = "0.7.7"
val multiplatformSettingsVersion = "0.8"
implementation("com.russhwolf:multiplatform-settings-jvm:$multiplatformSettingsVersion")
implementation("com.russhwolf:multiplatform-settings-serialization-jvm:$multiplatformSettingsVersion")
// Android version of SimpleDateFormat
implementation("com.ibm.icu:icu4j:69.1")
}
tasks {
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn")
}
}
//def fatJarTask = tasks.getByPath(':AndroidCompat:JVMPatch:fatJar')
//
//// Copy JVM core patches
//task copyJVMPatches(type: Copy) {
// from fatJarTask.outputs.files
// into 'src/main/resources/patches'
//}
//
//compileOnly(Java.dependsOn gradle.includedBuild('dex2jar').task(':dex-translator:assemble')
//compileOnly(Java.dependsOn copyJVMPatches
//copyJVMPatches.dependsOn fatJarTask
//
@@ -13,7 +13,10 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
public class TwoStatePreference extends Preference {
// Note: remove @JsonIgnore and implement methods if any extension ever uses these methods or the variables behind them
public TwoStatePreference(Context context) { super(context); }
public TwoStatePreference(Context context) {
super(context);
setDefaultValue(false);
}
@JsonIgnore
public boolean isChecked() { throw new RuntimeException("Stub!"); }
@@ -1,2 +0,0 @@
package com.f2prateek;
//TODO Consider if we can change this package into an Android dependency
@@ -1,34 +0,0 @@
/*
Copyright 2014 Prateek Srivastava
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.
This file may have been modified after being copied from it's original source.
*/
package com.f2prateek.rx.preferences;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
final class BooleanAdapter implements Preference.Adapter<Boolean> {
static final BooleanAdapter INSTANCE = new BooleanAdapter();
@Override public Boolean get(@NonNull String key, @NonNull SharedPreferences preferences) {
return preferences.getBoolean(key, false);
}
@Override public void set(@NonNull String key, @NonNull Boolean value,
@NonNull SharedPreferences.Editor editor) {
editor.putBoolean(key, value);
}
}
@@ -1,40 +0,0 @@
/*
Copyright 2014 Prateek Srivastava
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.
This file may have been modified after being copied from it's original source.
*/
package com.f2prateek.rx.preferences;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
final class EnumAdapter<T extends Enum<T>> implements Preference.Adapter<T> {
private final Class<T> enumClass;
EnumAdapter(Class<T> enumClass) {
this.enumClass = enumClass;
}
@Override public T get(@NonNull String key, @NonNull SharedPreferences preferences) {
String value = preferences.getString(key, null);
assert value != null; // Not called unless key is present.
return Enum.valueOf(enumClass, value);
}
@Override
public void set(@NonNull String key, @NonNull T value, @NonNull SharedPreferences.Editor editor) {
editor.putString(key, value.name());
}
}
@@ -1,34 +0,0 @@
/*
Copyright 2014 Prateek Srivastava
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.
This file may have been modified after being copied from it's original source.
*/
package com.f2prateek.rx.preferences;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
final class FloatAdapter implements Preference.Adapter<Float> {
static final FloatAdapter INSTANCE = new FloatAdapter();
@Override public Float get(@NonNull String key, @NonNull SharedPreferences preferences) {
return preferences.getFloat(key, 0f);
}
@Override public void set(@NonNull String key, @NonNull Float value,
@NonNull SharedPreferences.Editor editor) {
editor.putFloat(key, value);
}
}
@@ -1,34 +0,0 @@
/*
Copyright 2014 Prateek Srivastava
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.
This file may have been modified after being copied from it's original source.
*/
package com.f2prateek.rx.preferences;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
final class IntegerAdapter implements Preference.Adapter<Integer> {
static final IntegerAdapter INSTANCE = new IntegerAdapter();
@Override public Integer get(@NonNull String key, @NonNull SharedPreferences preferences) {
return preferences.getInt(key, 0);
}
@Override public void set(@NonNull String key, @NonNull Integer value,
@NonNull SharedPreferences.Editor editor) {
editor.putInt(key, value);
}
}
@@ -1,34 +0,0 @@
/*
Copyright 2014 Prateek Srivastava
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.
This file may have been modified after being copied from it's original source.
*/
package com.f2prateek.rx.preferences;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
final class LongAdapter implements Preference.Adapter<Long> {
static final LongAdapter INSTANCE = new LongAdapter();
@Override public Long get(@NonNull String key, @NonNull SharedPreferences preferences) {
return preferences.getLong(key, 0L);
}
@Override public void set(@NonNull String key, @NonNull Long value,
@NonNull SharedPreferences.Editor editor) {
editor.putLong(key, value);
}
}
@@ -1,127 +0,0 @@
/*
Copyright 2014 Prateek Srivastava
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.
This file has been modified after being copied from it's original source.
*/
package com.f2prateek.rx.preferences;
import android.content.SharedPreferences;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import rx.Observable;
import rx.functions.Action1;
/** A preference of type {@link T}. Instances can be created from {@link RxSharedPreferences}. */
public final class Preference<T> {
/** Stores and retrieves instances of {@code T} in {@link SharedPreferences}. */
public interface Adapter<T> {
/** Retrieve the value for {@code key} from {@code preferences}. */
T get(@NonNull String key, @NonNull SharedPreferences preferences);
/**
* Store non-null {@code value} for {@code key} in {@code editor}.
* <p>
* Note: Implementations <b>must not</b> call {@code commit()} or {@code apply()} on
* {@code editor}.
*/
void set(@NonNull String key, @NonNull T value, @NonNull SharedPreferences.Editor editor);
}
private final SharedPreferences preferences;
private final String key;
private final T defaultValue;
private final Adapter<T> adapter;
private final Observable<T> values;
Preference(SharedPreferences preferences, final String key, T defaultValue, Adapter<T> adapter,
Observable<String> keyChanges) {
this.preferences = preferences;
this.key = key;
this.defaultValue = defaultValue;
this.adapter = adapter;
this.values = keyChanges
.filter(key::equals)
.startWith("<init>") // Dummy value to trigger initial load.
.onBackpressureLatest()
.map(ignored -> get());
}
/** The key for which this preference will store and retrieve values. */
@NonNull
public String key() {
return key;
}
/** The value used if none is stored. May be {@code null}. */
@Nullable
public T defaultValue() {
return defaultValue;
}
/**
* Retrieve the current value for this preference. Returns {@link #defaultValue()} if no value is
* set.
*/
@Nullable
public T get() {
if (!preferences.contains(key)) {
return defaultValue;
}
return adapter.get(key, preferences);
}
/**
* Change this preference's stored value to {@code value}. A value of {@code null} will delete the
* preference.
*/
public void set(@Nullable T value) {
SharedPreferences.Editor editor = preferences.edit();
if (value == null) {
editor.remove(key);
} else {
adapter.set(key, value, editor);
}
editor.apply();
}
/** Returns true if this preference has a stored value. */
public boolean isSet() {
return preferences.contains(key);
}
/** Delete the stored value for this preference, if any. */
public void delete() {
set(null);
}
/**
* Observe changes to this preference. The current value or {@link #defaultValue()} will be
* emitted on first subscribe.
*/
@CheckResult @NonNull
public Observable<T> asObservable() {
return values;
}
/**
* An action which stores a new value for this preference. Passing {@code null} will delete the
* preference.
*/
@CheckResult @NonNull
public Action1<? super T> asAction() {
return (Action1<T>) this::set;
}
}
@@ -1,178 +0,0 @@
/*
Copyright 2014 Prateek Srivastava
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.
This file has been modified after being copied from it's original source.
*/
package com.f2prateek.rx.preferences;
import android.annotation.TargetApi;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.Collections;
import java.util.Set;
import rx.Observable;
import rx.subscriptions.Subscriptions;
import static android.os.Build.VERSION_CODES.HONEYCOMB;
import static com.f2prateek.rx.preferences.Preconditions.checkNotNull;
/** A factory for reactive {@link Preference} objects. */
public final class RxSharedPreferences {
private static final Float DEFAULT_FLOAT = 0f;
private static final Integer DEFAULT_INTEGER = 0;
private static final Boolean DEFAULT_BOOLEAN = Boolean.FALSE;
private static final Long DEFAULT_LONG = 0L;
/** Create an instance of {@link RxSharedPreferences} for {@code preferences}. */
@CheckResult @NonNull
public static RxSharedPreferences create(@NonNull SharedPreferences preferences) {
checkNotNull(preferences, "preferences == null");
return new RxSharedPreferences(preferences);
}
private final SharedPreferences preferences;
private final Observable<String> keyChanges;
private RxSharedPreferences(final SharedPreferences preferences) {
this.preferences = preferences;
this.keyChanges = Observable.create((Observable.OnSubscribe<String>) subscriber -> {
final OnSharedPreferenceChangeListener listener = (preferences1, key) -> subscriber.onNext(key);
preferences.registerOnSharedPreferenceChangeListener(listener);
subscriber.add(Subscriptions.create(() -> preferences.unregisterOnSharedPreferenceChangeListener(listener)));
}).share();
}
/** Create a boolean preference for {@code key}. Default is {@code false}. */
@CheckResult @NonNull
public Preference<Boolean> getBoolean(@NonNull String key) {
return getBoolean(key, DEFAULT_BOOLEAN);
}
/** Create a boolean preference for {@code key} with a default of {@code defaultValue}. */
@CheckResult @NonNull
public Preference<Boolean> getBoolean(@NonNull String key, @Nullable Boolean defaultValue) {
checkNotNull(key, "key == null");
return new Preference<>(preferences, key, defaultValue, BooleanAdapter.INSTANCE, keyChanges);
}
/** Create an enum preference for {@code key}. Default is {@code null}. */
@CheckResult @NonNull
public <T extends Enum<T>> Preference<T> getEnum(@NonNull String key,
@NonNull Class<T> enumClass) {
return getEnum(key, null, enumClass);
}
/** Create an enum preference for {@code key} with a default of {@code defaultValue}. */
@CheckResult @NonNull
public <T extends Enum<T>> Preference<T> getEnum(@NonNull String key, @Nullable T defaultValue,
@NonNull Class<T> enumClass) {
checkNotNull(key, "key == null");
checkNotNull(enumClass, "enumClass == null");
Preference.Adapter<T> adapter = new EnumAdapter<>(enumClass);
return new Preference<>(preferences, key, defaultValue, adapter, keyChanges);
}
/** Create a float preference for {@code key}. Default is {@code 0}. */
@CheckResult @NonNull
public Preference<Float> getFloat(@NonNull String key) {
return getFloat(key, DEFAULT_FLOAT);
}
/** Create a float preference for {@code key} with a default of {@code defaultValue}. */
@CheckResult @NonNull
public Preference<Float> getFloat(@NonNull String key, @Nullable Float defaultValue) {
checkNotNull(key, "key == null");
return new Preference<>(preferences, key, defaultValue, FloatAdapter.INSTANCE, keyChanges);
}
/** Create an integer preference for {@code key}. Default is {@code 0}. */
@CheckResult @NonNull
public Preference<Integer> getInteger(@NonNull String key) {
//noinspection UnnecessaryBoxing
return getInteger(key, DEFAULT_INTEGER);
}
/** Create an integer preference for {@code key} with a default of {@code defaultValue}. */
@CheckResult @NonNull
public Preference<Integer> getInteger(@NonNull String key, @Nullable Integer defaultValue) {
checkNotNull(key, "key == null");
return new Preference<>(preferences, key, defaultValue, IntegerAdapter.INSTANCE, keyChanges);
}
/** Create a long preference for {@code key}. Default is {@code 0}. */
@CheckResult @NonNull
public Preference<Long> getLong(@NonNull String key) {
//noinspection UnnecessaryBoxing
return getLong(key, DEFAULT_LONG);
}
/** Create a long preference for {@code key} with a default of {@code defaultValue}. */
@CheckResult @NonNull
public Preference<Long> getLong(@NonNull String key, @Nullable Long defaultValue) {
checkNotNull(key, "key == null");
return new Preference<>(preferences, key, defaultValue, LongAdapter.INSTANCE, keyChanges);
}
/** Create a preference of type {@code T} for {@code key}. Default is {@code null}. */
@CheckResult @NonNull
public <T> Preference<T> getObject(@NonNull String key, @NonNull Preference.Adapter<T> adapter) {
return getObject(key, null, adapter);
}
/**
* Create a preference for type {@code T} for {@code key} with a default of {@code defaultValue}.
*/
@CheckResult @NonNull
public <T> Preference<T> getObject(@NonNull String key, @Nullable T defaultValue,
@NonNull Preference.Adapter<T> adapter) {
checkNotNull(key, "key == null");
checkNotNull(adapter, "adapter == null");
return new Preference<>(preferences, key, defaultValue, adapter, keyChanges);
}
/** Create a string preference for {@code key}. Default is {@code null}. */
@CheckResult @NonNull
public Preference<String> getString(@NonNull String key) {
return getString(key, null);
}
/** Create a string preference for {@code key} with a default of {@code defaultValue}. */
@CheckResult @NonNull
public Preference<String> getString(@NonNull String key, @Nullable String defaultValue) {
checkNotNull(key, "key == null");
return new Preference<>(preferences, key, defaultValue, StringAdapter.INSTANCE, keyChanges);
}
/** Create a string set preference for {@code key}. Default is an empty set. */
@TargetApi(HONEYCOMB)
@CheckResult @NonNull
public Preference<Set<String>> getStringSet(@NonNull String key) {
return getStringSet(key, Collections.emptySet());
}
/** Create a string set preference for {@code key} with a default of {@code defaultValue}. */
@TargetApi(HONEYCOMB)
@CheckResult @NonNull
public Preference<Set<String>> getStringSet(@NonNull String key,
@NonNull Set<String> defaultValue) {
checkNotNull(key, "key == null");
return new Preference<>(preferences, key, defaultValue, StringSetAdapter.INSTANCE, keyChanges);
}
}
@@ -1,17 +0,0 @@
package com.f2prateek.rx.preferences;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
final class StringAdapter implements Preference.Adapter<String> {
static final StringAdapter INSTANCE = new StringAdapter();
@Override public String get(@NonNull String key, @NonNull SharedPreferences preferences) {
return preferences.getString(key, null);
}
@Override public void set(@NonNull String key, @NonNull String value,
@NonNull SharedPreferences.Editor editor) {
editor.putString(key, value);
}
}
@@ -1,22 +0,0 @@
package com.f2prateek.rx.preferences;
import android.annotation.TargetApi;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
import java.util.Set;
import static android.os.Build.VERSION_CODES.HONEYCOMB;
@TargetApi(HONEYCOMB)
final class StringSetAdapter implements Preference.Adapter<Set<String>> {
static final StringSetAdapter INSTANCE = new StringSetAdapter();
@Override public Set<String> get(@NonNull String key, @NonNull SharedPreferences preferences) {
return preferences.getStringSet(key, null);
}
@Override public void set(@NonNull String key, @NonNull Set<String> value,
@NonNull SharedPreferences.Editor editor) {
editor.putStringSet(key, value);
}
}
@@ -1,7 +0,0 @@
package com.github.pwittchen.reactivenetwork.library
import android.net.NetworkInfo
class Connectivity {
val state = NetworkInfo.State.CONNECTED
}
@@ -1,14 +0,0 @@
package com.github.pwittchen.reactivenetwork.library
import android.content.Context
import rx.Observable
/**
* Created by nulldev on 12/29/16.
*/
class ReactiveNetwork {
companion object {
fun observeNetworkConnectivity(context: Context) = Observable.just(Connectivity())!!
}
}
@@ -1,5 +0,0 @@
package kotlinx.coroutines.experimental.android
import kotlinx.coroutines.GlobalScope
val UI = GlobalScope.coroutineContext
@@ -2,7 +2,6 @@ package xyz.nulldev.androidcompat
import org.kodein.di.DI
import org.kodein.di.conf.global
import xyz.nulldev.androidcompat.bytecode.ModApplier
import xyz.nulldev.androidcompat.config.ApplicationInfoConfigModule
import xyz.nulldev.androidcompat.config.FilesConfigModule
import xyz.nulldev.androidcompat.config.SystemConfigModule
@@ -12,12 +11,7 @@ import xyz.nulldev.ts.config.GlobalConfigManager
* Initializes the Android compatibility module
*/
class AndroidCompatInitializer {
val modApplier by lazy { ModApplier() }
fun init() {
modApplier.apply()
DI.global.addImport(AndroidCompatModule().create())
//Register config modules
@@ -1,22 +0,0 @@
package xyz.nulldev.androidcompat.bytecode
import javassist.CtClass
import mu.KotlinLogging
/**
* Applies Javassist modifications
*/
class ModApplier {
val logger = KotlinLogging.logger {}
fun apply() {
logger.info { "Applying Javassist mods..." }
val modifiedClasses = mutableListOf<CtClass>()
modifiedClasses.forEach {
it.toClass()
}
}
}
@@ -1,6 +1,7 @@
package xyz.nulldev.androidcompat.config
import com.typesafe.config.Config
import io.github.config4k.getValue
import xyz.nulldev.ts.config.ConfigModule
/**
@@ -8,8 +9,8 @@ import xyz.nulldev.ts.config.ConfigModule
*/
class ApplicationInfoConfigModule(config: Config) : ConfigModule(config) {
val packageName = config.getString("packageName")!!
val debug = config.getBoolean("debug")
val packageName: String by config
val debug: Boolean by config
companion object {
fun register(config: Config)
@@ -1,6 +1,7 @@
package xyz.nulldev.androidcompat.config
import com.typesafe.config.Config
import io.github.config4k.getValue
import xyz.nulldev.ts.config.ConfigModule
/**
@@ -8,23 +9,23 @@ import xyz.nulldev.ts.config.ConfigModule
*/
class FilesConfigModule(config: Config) : ConfigModule(config) {
val dataDir = config.getString("dataDir")!!
val filesDir = config.getString("filesDir")!!
val noBackupFilesDir = config.getString("noBackupFilesDir")!!
val externalFilesDirs: MutableList<String> = config.getStringList("externalFilesDirs")!!
val obbDirs: MutableList<String> = config.getStringList("obbDirs")!!
val cacheDir = config.getString("cacheDir")!!
val codeCacheDir = config.getString("codeCacheDir")!!
val externalCacheDirs: MutableList<String> = config.getStringList("externalCacheDirs")!!
val externalMediaDirs: MutableList<String> = config.getStringList("externalMediaDirs")!!
val rootDir = config.getString("rootDir")!!
val externalStorageDir = config.getString("externalStorageDir")!!
val downloadCacheDir = config.getString("downloadCacheDir")!!
val databasesDir = config.getString("databasesDir")!!
val dataDir:String by config
val filesDir:String by config
val noBackupFilesDir:String by config
val externalFilesDirs: MutableList<String> by config
val obbDirs: MutableList<String> by config
val cacheDir:String by config
val codeCacheDir:String by config
val externalCacheDirs: MutableList<String> by config
val externalMediaDirs: MutableList<String> by config
val rootDir:String by config
val externalStorageDir:String by config
val downloadCacheDir:String by config
val databasesDir:String by config
val prefsDir = config.getString("prefsDir")!!
val prefsDir:String by config
val packageDir = config.getString("packageDir")!!
val packageDir:String by config
companion object {
fun register(config: Config)
@@ -2,9 +2,10 @@ package xyz.nulldev.androidcompat.config
import com.typesafe.config.Config
import xyz.nulldev.ts.config.ConfigModule
import io.github.config4k.getValue
class SystemConfigModule(val config: Config) : ConfigModule(config) {
val isDebuggable = config.getBoolean("isDebuggable")
val isDebuggable: Boolean by config
val propertyPrefix = "properties."
@@ -4,11 +4,21 @@ import java.io.InputStream
import java.io.Reader
import java.math.BigDecimal
import java.net.URL
import java.sql.*
import java.sql.Array
import java.sql.Blob
import java.sql.Clob
import java.sql.Date
import java.util.*
import java.sql.NClob
import java.sql.Ref
import java.sql.ResultSet
import java.sql.ResultSetMetaData
import java.sql.RowId
import java.sql.SQLXML
import java.sql.Time
import java.sql.Timestamp
import java.util.Calendar
@Suppress("UNCHECKED_CAST")
class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
private val cachedContent = mutableListOf<ResultSetEntry>()
@@ -18,7 +18,7 @@ class ServiceSupport {
private val logger = KotlinLogging.logger {}
fun startService(context: Context, intent: Intent) {
fun startService(@Suppress("UNUSED_PARAMETER") context: Context, intent: Intent) {
val name = intentToClassName(intent)
logger.debug { "Starting service: $name" }
@@ -35,7 +35,7 @@ class ServiceSupport {
}
}
fun stopService(context: Context, intent: Intent) {
fun stopService(@Suppress("UNUSED_PARAMETER") context: Context, intent: Intent) {
val name = intentToClassName(intent)
stopService(name)
}
@@ -25,6 +25,7 @@ object KodeinGlobalHelper {
* Get a dependency
*/
@JvmStatic
@Suppress("UNCHECKED_CAST")
fun <T : Any> instance(type: Class<T>, kodein: DI? = null): T {
return when(type) {
AndroidFiles::class.java -> {
@@ -1,36 +1,12 @@
# AndroidComapt Root dir
androidcompat.rootDir = androidcompat-root
# Allow/disallow preference changes (useful for demos)
ts.server.allowConfigChanges = true
# Enable the WebUI? Note: The API and multi-user sync server ui will remain available even if the WebUI is disabled
ts.server.enableWebUi = true
# 'true' to use the old, buggy/memory-leaking WebUI
ts.server.useOldWebUi = false
# 'true' to pretty print all JSON API responses
ts.server.prettyPrintApi = false
# List of blacklisted/whitelisted API endpoints/operation IDs
ts.server.disabledApiEndpoints = []
ts.server.enabledApiEndpoints = []
# Message to print in the console when the API has finished booting
ts.server.httpInitializedPrintMessage = ""
# Use external folder for static files
ts.server.useExternalStaticFiles = false
ts.server.externalStaticFilesFolder = ""
# Root storage dir
ts.server.rootDir = tachiserver-data
# Dir to store JVM patches
ts.server.patchesDir = ${ts.server.rootDir}/patches
####################### `android.files` (FilesConfigModule) #######################
# Storage dir for the emulated Android app
android.files.rootDir = ${ts.server.rootDir}/android-compat/appdata
android.files.rootDir = ${androidcompat.rootDir}/appdata
# External storage dir for the emulated Android app's
android.files.externalStorageDir = ${ts.server.rootDir}/android-compat/extappdata
android.files.externalStorageDir = ${androidcompat.rootDir}/extappdata
# Internal Android directories
android.files.dataDir = ${android.files.rootDir}/data
@@ -48,37 +24,16 @@ android.files.externalCacheDirs = [${android.files.externalStorageDir}/cache]
android.files.externalMediaDirs = [${android.files.externalStorageDir}/media]
android.files.downloadCacheDir = ${android.files.externalStorageDir}/downloadCache
android.files.packageDir = ${ts.server.rootDir}/android-compat/packages
android.files.packageDir = ${androidcompat.rootDir}/android-compat/packages
####################### `android.app` (ApplicationInfoConfigModule) #######################
# Emulated Android app package name
android.app.packageName = eu.kanade.tachiyomi
# Debug mode for the emulated Android app
android.app.debug = true
####################### `android.system` (SystemConfigModule) #######################
# Whether or not the emulated Android system is debuggable
android.system.isDebuggable = true
# Is the multi-user sync server enabled? Does not affect the single-user sync server included in the API.
ts.syncd.enable = false
# The URL of this server (displayed in the sync server web ui)
ts.syncd.baseUrl = "http://example.com"
# 'true' to disable the API and only enable the multi-user sync server
ts.syncd.syncOnlyMode = false
# The root directory to store synchronized data
ts.syncd.rootDir = ${ts.server.rootDir}/sync/accounts
# Location to store config files for the sandbox
ts.syncd.sandboxedConfig = ${ts.server.rootDir}/sync/sandboxed_config.config
# Recaptcha stuff for signup/login
ts.syncd.recaptcha.siteKey = ""
ts.syncd.recaptcha.secret = ""
# Sync server display name
ts.syncd.name = "Tachiyomi sync server"
# Header used to forward the IP to the multi-user sync server if the server is behind a reverse proxy
ts.syncd.ipHeader = ""
+28
View File
@@ -0,0 +1,28 @@
# Server: v0.X.Y-rXXX + WebUI: rXXX
## TL;DR
<!-- TODO: fill before release -->
## Tachidesk-Server
### Public API
#### Non-breaking changes
- N/A
#### Breaking changes
- N/A
#### Bug fixes
- N/A
### Private API
- N/A
## Tachidesk-WebUI
#### Visible changes
- N/A
#### Bug fixes
- N/A
#### Internal changes
- N/A
+212
View File
@@ -0,0 +1,212 @@
# Server: v0.5.2 + WebUI: r807
## TL;DR
- Fixed Local source not working on Windows
- Fixed Chapter numbers being shown incorrectly
## Tachidesk-Server
### Public API
#### Non-breaking changes
- N/A
#### Breaking changes
- N/A
#### Bug fixes
- N/A
### Private API
- (r942) Gradle Updates ([#199](https://github.com/Suwayomi/Tachidesk-WebUI/pull/199) by @Syer10)
- (r941) Update BytecodeEditor to use Java NIO Paths ([#200](https://github.com/Suwayomi/Tachidesk-WebUI/pull/200) by @Syer10)
## Tachidesk-WebUI
#### Visible changes
- (r804) update text positioning on Reader and Player ([#35](https://github.com/Suwayomi/Tachidesk-WebUI/pull/35) by @voltrare)
- (r806) Source card for Local source is different
- (r807) add Local source guide
#### Bug fixes
- (r805) fix chapter name
#### Internal changes
- N/A
# Server: v0.5.1 + WebUI: r803
## TL;DR
- Loading sources' manga list is at least twice as fast
- Added support for Tachiyomi's Local source
- Added BasicAuth support, now you can protect your Tachidesk instance if you are running it on a public server
- Added ability to turn off cache for image requests
## Tachidesk-Server
### Public API
#### Non-breaking changes
- (r915) add BasicAuth support
- (r918) add ability to delete downloaded chapters
- (r923-r930) add Local Source
- (r938) add ability to turn off cache for image requests
#### Breaking changes
- N/A
#### Bug fixes
- (r917) detect if a downloaded chapter is missing
### Private API
- (r913) remove expand char limit on MangaTable columns
- (r914) migrate to Javalin 4
- (r921) depricate zero based chapters
- (r937) add ChapterRecognition from tachiyomi, closes #10
## Tachidesk-WebUI
#### Visible changes
- (r790) nice looking progress percentage
- (r791) show a Delete button for downloaded chapters
- (r792) Update hover effect using more of Material-UI color pallete ([#29](https://github.com/Suwayomi/Tachidesk-WebUI/pull/29) by @voltrare)
- (r793) Optimize images ([#32](https://github.com/Suwayomi/Tachidesk-WebUI/pull/32) by @phanirithvij)
- (r794) try fix #30 ([#31](https://github.com/Suwayomi/Tachidesk-WebUI/pull/31) by @phanirithvij)
- (r795) fix viewing page number when the string is long
- (r796) show proper display name for source
- (r797) fail gracefully when a thumbnail has errors
- (r798) fix when a source fails to load mangas
- (r800) add Local source ([#31](https://github.com/Suwayomi/Tachidesk-WebUI/pull/31))
- (r803) add support for useCache
#### Bug fixes
- N/A
#### Internal changes
- N/A
# Server: v0.5.0 + WebUI: r789
## TL;DR
- You can now install APK extensions from the extensions page
- WebUI now comes with an updated Material Design looks and is faster a little bit.
- WebUI now shows Nsfw content by default, disable it in settings if you prefer to not see Nsfw stuff
- Added support for configuration of sources, this enables MangaDex, Komga, Cubari and many other sources
- Chapters in the Manga page and Sources in the source page now look nicer and will glow with mouse hover
## Tachidesk-Server
### Public API
#### Non-breaking changes
- (r888) add installing APK from external sources endpoint
#### Breaking changes
- (r877 [#188](https://github.com/Suwayomi/Tachidesk-Server/pull/188) by @Syer10) `MangaDataClass.genre` changed type to `List<String>`
#### Bug fixes
- (r899-r901) fix when an external apk is installed and it doesn't have the default tachiyomi-extensions name
- (r905) fix a bug where if two sources return the same URL, a false duplicate might be detected
### Private API
- (r887) the `run` task won't call `downloadWebUI` now
- (r902) cleanup print/ln instances
- (r906) better handling of uninstalling Extensions
## Tachidesk-WebUI
#### Visible changes
- (r770) add support for the new genre type
- (r771) set the default value of `showNsfw` to `true` so we won't have visual artifacts with a clean install
- (r774 [#21](https://github.com/Suwayomi/Tachidesk-WebUI/pull/21) by @voltrare) `ReaderNavbar.jsx`: Swap close and retract Navbar buttons
- (r775 [#23](https://github.com/Suwayomi/Tachidesk-WebUI/pull/23) by @voltrare) `yarn.lock`: Fixes version inconsistency after commit 9b866811b
- (r776 [#23](https://github.com/Suwayomi/Tachidesk-WebUI/pull/23) by @voltrare) add margin between Source and Extension cards, make the Search button look nicer
- (r777) add support for installing external APK files
- (r778) fix the makeToaster?
- (r779) Action button for installing external extension
- (r780 Suwayomi/Tachidesk-WebUI#25) add on hover, active effect to Chapter/Episode card
- (r782-r785) updating material-ui to v5 changed the theme
- (r785-r788) better `SourceCard` looks on mobile, move `SourceDataClass.isConfigurable` gear button to `SourceMangas`
- (r789) implement source configuration
#### Bug fixes
- N/A
#### Internal changes
- (r782-r785) update dependencies, migrate material-ui from v4 to v5
# Server: v0.4.9 + WebUI: r769
## Tachidesk-Server
### Public API
#### Non-breaking changes
- N/A
#### Breaking changes
- (r857) renamed: `SourceDataClass.isNSFW` -> `SourceDataClass.isNsfw`
#### Bug fixes
- N/A
### Private API
- (r850) Bump WebUI version to r767
- (r861) Bump WebUI version to r769
#### Non-code changes
- (r851) Add this changelog file and `CHANGELOG-TEMPLATE.md`
- (r852-r853) `CONTRIBUTING.md`: Add a note about this maintaining this file changelog
- (r855) `CONTRIBUTING.md`: text cleanup
- (r859) `CONTRIBUTING.md`: remove dumb rule
- (r862) `windows-bundler.sh`: update jre
- (r864) add linux and macOS bundler script and launcher scripts
- (r865) fix macOS bundler script and launcher scripts
- (r866) bump electron version to v14.0.0
- (r868) add linux and macOS bundlers to the publish workflow
- (r871) `publish.yml`: remove node module cache, won't need it anymore
- (r873) `publish.yml` and `build_push.yml`: fix oopsies
## Tachidesk-WebUI
#### Visible changes
- (r767-r769) Support for hiding NSFW content in settings screen, extensions screen, sources screen
#### Bug fixes
- N/A
#### Internal changes
- (r767) Remove some duplicate dependency declaration from `package.json`
#### Non-code changes
- (r42-r45) Change `README.md`: some links and stuff
- (r45-r765) Add all of the commit history from when WebUI was separated from Server, jumping from r45 to r765 (r45 is exactly the same as r765)
- (r766) Steal `.gitattributes` from Tachidesk-Server
- (r767) Dependency cleanup in `package.json`
# Server: v0.4.8 + WebUI: r41
## Tachidesk-Server
### Public API
#### Non-breaking changes
- Added support for serializing Search Filters
- `SourceDataClass` now has a `isNsfw` key
#### Breaking changes
- N/A
#### Bug fixes
- Fixed a bug where backup restore reversed chapter order
- Open Site feature now works properly (https://github.com/Suwayomi/Tachidesk-WebUI/issues/19)
### Private API
- Added `CloudflareInterceptor` from TachiWeb-Server
- Restoring backup for mangas in library(merging manga data) is now supported
## Tachidesk-WebUI
#### Visible changes
- Better looking manga card titles
- Better reader title, next, prev buttons
#### Bug fixes
- Open Site feature now works properly (https://github.com/Suwayomi/Tachidesk-WebUI/issues/19)
- Re-ordering categories now works
#### Internal changes
- N/A
+22 -35
View File
@@ -2,51 +2,38 @@
## 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.
**Note 1:** Notify the developers on [Suwayomi discord](https://discord.gg/DDZdqZWaHA) (#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?
**Note 2:** Store all changes with each direct commit/PR in [CHANGELOG.md](./CHANGELOG.md).
## How does Tachidesk-Server 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.
1. **Server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run jar libraries converted from apk extensions. All this concludes to serving a REST API.
2. **WebUI:** A React SPA(`create-react-app`) project that works with the server to do the presentation located at https://github.com/Suwayomi/Tachidesk-WebUI
## 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.
- Gives the ability to access Tachidesk-Server from a remote client e.g., your phone, tablet or smart TV
- Ease development of user interfaces for Tachidesk
## 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.
- **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.
### building the full-blown jar (Tachidesk-Server + Tachidesk-WebUI bundle)
Run `./gradlew server:downloadWebUI server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-Server-vX.Y.Z-rxxx.jar`.
### building without `webUI` bundled (server only)
Delete `server/src/main/resources/WebUI.zip` if exists from previous runs, then run `./gradlew server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-Server-vX.Y.Z-rxxx.jar`.
### building the Windows package
First Build the jar, then cd into the `scripts` directory and run `./windows-bundler.sh win32` or `./windows-bundler.sh win64` depending on the target architecture, the resulting built zip package file will be `server/build/Tachidesk-Server-vX.Y.Z-rxxx-winXX.zip`.
## Running in development mode
run `./gradlew :server:run --stacktrace` to run the server
+61 -26
View File
@@ -3,48 +3,83 @@
|-------|----------|---------|---------|
| ![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
# Tachidesk-Server is a server app! You may not want to Download Tachidesk-Server directly.
Yes, you need a client/user interface app as a front-end for Tachidesk-Server, if you Directly Download Tachidesk-Server you'll get a bundled version of [Tachidesk-WebUI](https://github.com/Suwayomi/Tachidesk-WebUI) with it.
Here's a list of known clients/user interfaces for Tachidesk-Server:
- [Tachidesk-JUI](https://github.com/Suwayomi/Tachidesk-JUI): The "official" front-end for Tachidesk-Server, A native desktop Application.
- [Tachidesk-WebUI](https://github.com/Suwayomi/Tachidesk-WebUI): The web/electrion front-end that Tachidesk-Server is traditionally shipped with.
- [Tachidesk-qtui](https://github.com/Suwayomi/Tachidesk-qtui): A C++/Qt front-end for mobile devices(Android/linux), in super early stage of development.
- [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js, in super early stage of development.
# What is Tachidesk then?
<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/).
A free and open source manga reader server that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
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.
Tachidesk-Server is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it. This includes Windows, Linux, macOS, chrome OS, etc. Follow [Downloading and Running the app](#downloading-and-running-the-app) for installation instructions.
Ability to 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:
- Installing and executing Tachiyomi's Extensions, So you'll get the same sources.
- A library to save your mangas and categories to put them into.
- Searching and browsing installed sources.
- A decent chapter reader.
- Ability to download Manga for offline read
- Backup and restore support powered by Tachiyomi Legacy Backups
- From Tachiyomi
- Installing and executing Tachiyomi's Extensions, So you'll get the same sources
- A library to save your mangas and categories to put them into
- Searching and browsing installed sources
- Ability to download Manga for offline read
- Backup and restore support powered by Tachiyomi Backups
- From Aniyomi
- Installing and executing Aniyomi's Extensions
- Searching and browsing installed sources.
- Viewing an anime and it's episodes
**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.
**Note:** These are capabilities of Tachidesk-Server, the actual working support is provided by each front-end app, checkout their respective readme for more info.
## Downloading and Running the app
### All Operating Systems
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.
**Note:** Tachidesk-Server is alpha software and can break rarely and/or with each update. See [Troubleshooting](https://github.com/Suwayomi/Tachidesk-Server/wiki/Troubleshooting) if it happens.
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).
# Downloading and Running the app
## General Requirements
In order to use the app effectively you need the following:
- The jar release of Tachideesk-Server
- The Java Runtime Environment(JRE) 8 or newer (included in bundle releases)
- A Modern Browser like Google Chrome, Firefox, etc.
- ElectronJS (optional) (included in bundle releases)
- An internet connection (when you want to use online features)
## Using the jar release directly
Download the latest `.jar` release from [the releases section](https://github.com/Suwayomi/Tachidesk-Server/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` (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.
Make sure you have The Java Runtime Environment installed on your system, 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.
## Using Operating System Specific Bundles
To facilitate the use of Tachidesk we provide bundle releases that include The Java Runtime Environment, ElectronJS and 3 Tachidesk Launcher Scripts.
#### Launcher Scripts
- `Tachidesk Electron Launcher`: Launches Tachidesk inside Electron as a desktop applicaton
- `Tachidesk Browser Launcher`: Launches Tachidesk in a browser window
- `Tachidesk Debug Launcher`: Launches Tachidesk with debug logs attached. If Tachidesk doesn't work for you, running this can give you insight into why.
**Node:** Linux launcher scripts are named a bit differently but work the same.
### Windows
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).
Download the latest `win32`(Windows 32-bit) or `win64`(Windows 64-bit) release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Tachidesk-preview/releases).
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.
Unzip the downloaded file and double click on one of the launcher scripts.
### macOS
Download the latest `macOS-x64`(older macOS systems) or `macOS-arm64`(Apple M1) release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Tachidesk-preview/releases).
Unzip the downloaded file and double click on one of the launcher scripts.
### GNU/Linux
Download the latest `linux-x64`(x86_64) release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Tachidesk-preview/releases).
`tar xvf` the downloaded file and double click on one of the launcher scripts or run them using the terminal.
## Other methods of getting Tachidesk
### Arch Linux
You can install Tachidesk from the AUR
```
@@ -52,7 +87,7 @@ yay -S tachidesk
```
### Docker
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.
Check our Official Docker release [Tachidesk Container](https://github.com/orgs/Suwayomi/packages/container/package/tachidesk) for running Tachidesk Server in a docker container. 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.
Install from the command line:
```
@@ -75,9 +110,9 @@ 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.
The `AndroidCompat` module was originally developed by [@null-dev](https://github.com/null-dev) for [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server) and is licensed under `Apache License Version 2.0`.
The `AndroidCompat` module was originally developed by [@null-dev](https://github.com/null-dev) for [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server) and is licensed under `Apache License Version 2.0` and `Copyright 2019 Andy Bao and contributors`.
Parts of [tachiyomi](https://github.com/tachiyomiorg/tachiyomi) is adopted into this codebase, also licensed under `Apache License Version 2.0`.
Parts of [tachiyomi](https://github.com/tachiyomiorg/tachiyomi) is adopted into this codebase, also licensed under `Apache License Version 2.0` and `Copyright 2015 Javier Tomás`.
You can obtain a copy of `Apache License Version 2.0` from http://www.apache.org/licenses/LICENSE-2.0
+39 -19
View File
@@ -1,8 +1,11 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jmailen.gradle.kotlinter.tasks.FormatTask
import org.jmailen.gradle.kotlinter.tasks.LintTask
plugins {
kotlin("jvm") version "1.4.32"
kotlin("plugin.serialization") version "1.4.32" apply false
kotlin("jvm") version kotlinVersion
kotlin("plugin.serialization") version kotlinVersion
id("org.jmailen.kotlinter") version "3.6.0"
}
allprojects {
@@ -12,10 +15,8 @@ allprojects {
repositories {
mavenCentral()
maven("https://maven.google.com/")
google()
maven("https://jitpack.io")
maven("https://oss.sonatype.org/content/repositories/snapshots/")
maven("https://dl.google.com/dl/android/maven2/")
}
}
@@ -27,18 +28,36 @@ val projects = listOf(
configure(projects) {
apply(plugin = "org.jetbrains.kotlin.jvm")
apply(plugin = "org.jetbrains.kotlin.plugin.serialization")
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
tasks.withType<KotlinCompile> {
tasks {
withType<KotlinCompile> {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs = listOf(
"-Xopt-in=kotlin.RequiresOptIn",
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}
}
withType<LintTask> {
source(files("src/kotlin"))
}
withType<FormatTask> {
source(files("src/kotlin"))
}
}
dependencies {
// Kotlin
implementation(kotlin("stdlib-jdk8"))
@@ -46,31 +65,30 @@ configure(projects) {
testImplementation(kotlin("test-junit5"))
// coroutines
val coroutinesVersion = "1.4.3"
val coroutinesVersion = "1.5.2"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
val kotlinSerializationVersion = "1.1.0"
val kotlinSerializationVersion = "1.3.0-RC"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
// Dependency Injection
implementation("org.kodein.di:kodein-di-conf-jvm:7.5.0")
implementation("org.kodein.di:kodein-di-conf-jvm:7.8.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.6")
implementation("org.slf4j:slf4j-api:1.7.32")
implementation("ch.qos.logback:logback-classic:1.2.6")
implementation("io.github.microutils:kotlin-logging:2.0.11")
// 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")
// dependency both in AndroidCompat and extensions, version locked by Tachiyomi app/extensions
implementation("org.jsoup:jsoup:1.14.2")
// dependency of :AndroidCompat:Config
implementation("com.typesafe:config:1.4.1")
@@ -79,13 +97,15 @@ configure(projects) {
// to get application content root
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")
// dex2jar
val dex2jarVersion = "v26"
implementation("com.github.ThexXTURBOXx.dex2jar:dex-translator:$dex2jarVersion")
implementation("com.github.ThexXTURBOXx.dex2jar:dex-tools:$dex2jarVersion")
// APK parser
implementation("net.dongliu:apk-parser:2.6.10")
// Jackson
implementation("com.fasterxml.jackson.core:jackson-annotations:2.10.3")
// dependency both in AndroidCompat and server, version locked by javalin
implementation("com.fasterxml.jackson.core:jackson-annotations:2.12.4")
}
}
+11
View File
@@ -0,0 +1,11 @@
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
}
dependencies {
implementation("net.lingala.zip4j:zip4j:2.9.0")
}
+33
View File
@@ -0,0 +1,33 @@
import java.io.BufferedReader
/*
* 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/. */
const val kotlinVersion = "1.5.30"
const val MainClass = "suwayomi.tachidesk.MainKt"
// should be bumped with each stable release
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.5.2"
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r807"
// counts commits on the master branch
val tachideskRevision = runCatching {
System.getenv("ProductRevision") ?: Runtime
.getRuntime()
.exec("git rev-list HEAD --count")
.let { process ->
process.waitFor()
val output = process.inputStream.use {
it.bufferedReader().use(BufferedReader::readText)
}
process.destroy()
"r" + output.trim()
}
}.getOrDefault("r0")
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+3
View File
@@ -0,0 +1,3 @@
cd "`dirname "$0"`"
./jre/Contents/Home/bin/java -jar Tachidesk.jar
@@ -1 +1,7 @@
jre\bin\java -Dsuwayomi.tachidesk.server.debugLogsEnabled=true -jar Tachidesk.jar
:: cleaner output
@echo off
jre\bin\java -Dsuwayomi.tachidesk.config.server.debugLogsEnabled=true -jar Tachidesk.jar
:: Prevent cmd from closing when Tachidesk crashes
pause
+3
View File
@@ -0,0 +1,3 @@
cd "`dirname "$0"`"
./jre/Contents/Home/bin/java -Dsuwayomi.tachidesk.config.server.debugLogsEnabled=true -jar Tachidesk.jar
@@ -1 +1 @@
jre\bin\javaw "-Dsuwayomi.tachidesk.server.webInterface=electron" "-Dsuwayomi.tachidesk.server.electronPath=electron/electron.exe" -jar Tachidesk.jar
jre\bin\javaw "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron" "-Dsuwayomi.tachidesk.config.server.electronPath=electron/electron.exe" -jar Tachidesk.jar
+3
View File
@@ -0,0 +1,3 @@
cd "`dirname "$0"`"
./jre/Contents/Home/bin/java "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron" "-Dsuwayomi.tachidesk.config.server.electronPath=electron/Electron.app/Contents/MacOS/Electron" -jar Tachidesk.jar
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
./jre/bin/java -jar Tachidesk.jar
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
./jre/bin/java -Dsuwayomi.tachidesk.config.server.debugLogsEnabled=true -jar Tachidesk.jar
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
./jre/bin/java "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron" "-Dsuwayomi.tachidesk.config.server.electronPath=electron/electron" -jar Tachidesk.jar
+89
View File
@@ -0,0 +1,89 @@
#!/bin/bash
# Copyright (C) Contributors to the Suwayomi project
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
electron_version="v14.0.0"
if [ $1 = "linux-x64" ]; then
jre="OpenJDK8U-jre_x64_linux_hotspot_8u302b08.tar.gz"
jre_release="jdk8u302-b08"
jre_url="https://github.com/adoptium/temurin8-binaries/releases/download/$jre_release/$jre"
jre_dir="$jre_release-jre"
electron="electron-$electron_version-linux-x64.zip"
elif [ $1 = "macOS-x64" ]; then
jre="OpenJDK8U-jre_x64_mac_hotspot_8u302b08.tar.gz"
jre_release="jdk8u302-b08"
jre_url="https://github.com/adoptium/temurin8-binaries/releases/download/$jre_release/$jre"
jre_dir="$jre_release-jre"
electron="electron-$electron_version-darwin-x64.zip"
elif [ $1 = "macOS-arm64" ]; then
jre="zulu8.56.0.23-ca-jre8.0.302-macosx_aarch64.tar.gz"
jre_release="zulu8.56.0.23-ca-jre8.0.302-macosx_aarch64"
jre_url="https://cdn.azul.com/zulu/bin/$jre"
jre_dir="$jre_release"
electron="electron-$electron_version-darwin-arm64.zip"
else
echo "Unsupported arch value: $1"
exit 1
fi
arch="$1"
os=$(echo $arch | cut -d '-' -f1)
echo "creating $arch bundle"
jar=$(ls ../server/build/*.jar | tail -n1)
jar_name=$(echo $jar | cut -d'/' -f4)
release_name=$(echo $jar_name | sed 's/.jar//')-$arch
# make release dir
mkdir $release_name
echo "Dealing with jre..."
if [ ! -f $jre ]; then
curl -L $jre_url -o $jre
fi
tar xvf $jre
mv $jre_dir $release_name/jre
echo "Dealing with electron"
if [ ! -f $electron ]; then
curl -L "https://github.com/electron/electron/releases/download/$electron_version/$electron" -o $electron
fi
unzip $electron -d $release_name/electron
# copy artifacts
cp $jar $release_name/Tachidesk.jar
if [ $os = linux ]; then
cp "resources/tachidesk-browser-launcher.sh" $release_name
cp "resources/tachidesk-debug-launcher.sh" $release_name
cp "resources/tachidesk-electron-launcher.sh" $release_name
elif [ $os = macOS ]; then
cp "resources/Tachidesk Browser Launcher.command" $release_name
cp "resources/Tachidesk Debug Launcher.command" $release_name
cp "resources/Tachidesk Electron Launcher.command" $release_name
fi
archive_name=""
if [ $os = linux ]; then
archive_name=$release_name.tar.gz
GZIP=-9 tar cvzf $archive_name $release_name
elif [ $os = macOS ]; then
archive_name=$release_name.zip
zip -9 -r $archive_name $release_name
fi
rm -rf $release_name
# clean up from possible previous runs
if [ -f ../server/build/$archive_name ]; then
rm ../server/build/$archive_name
fi
mv $archive_name ../server/build/
+8 -4
View File
@@ -6,19 +6,23 @@
# 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"
electron_version="v14.0.0"
if [ $1 = "win32" ]; then
jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
jre_release="jdk8u292-b10"
jre_url="https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/$jre_release/$jre"
arch="win32"
electron="electron-$electron_version-win32-ia32.zip"
else
jre="OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip"
jre="OpenJDK8U-jre_x64_windows_hotspot_8u302b08.zip"
jre_release="jdk8u302-b08"
jre_url="https://github.com/adoptium/temurin8-binaries/releases/download/$jre_release/$jre"
arch="win64"
electron="electron-$electron_version-win32-x64.zip"
fi
jre_dir="jdk8u292-b10-jre"
jre_dir="$jre_release-jre"
echo "creating windows bundle"
@@ -33,7 +37,7 @@ 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
curl -L $jre_url -o $jre
fi
unzip $jre
mv $jre_dir $release_name/jre
+56 -81
View File
@@ -1,24 +1,11 @@
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 de.undercouch.gradle.tasks.download.Download
import java.time.Instant
plugins {
application
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"
}
repositories {
maven {
url = uri("https://repo1.maven.org/maven2/")
}
maven {
url = uri("https://jitpack.io")
}
id("com.github.gmazzo.buildconfig") version "3.0.3"
}
dependencies {
@@ -30,13 +17,14 @@ dependencies {
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")
implementation("io.javalin:javalin:4.0.0")
// jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
val jacksonVersion = "2.12.4"
implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
// Exposed ORM
val exposedVersion = "0.31.1"
val exposedVersion = "0.34.1"
implementation("org.jetbrains.exposed:exposed-core:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
@@ -45,29 +33,32 @@ dependencies {
implementation("com.h2database:h2:1.4.200")
// Exposed Migrations
val exposedMigrationsVersion = "3.1.0"
implementation("com.github.Suwayomi:exposed-migrations:$exposedMigrationsVersion")
implementation("com.github.Suwayomi:exposed-migrations:3.1.2")
// tray icon
implementation("com.dorkbox:SystemTray:4.1")
implementation("com.dorkbox:Utilities:1.9")
implementation("com.dorkbox:Utilities:1.9") // version locked by SystemTray
// dependencies of Tachiyomi extensions, some are duplicate, keeping it here for reference
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
implementation("com.squareup.okhttp3:okhttp: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("org.jsoup:jsoup:1.14.2")
implementation("com.google.code.gson:gson:2.8.8")
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
// Sort
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
// asm for fixing SimpleDateFormat (must match Dex2Jar version)
implementation("org.ow2.asm:asm-debug-all:5.0.3")
// asm for ByteCodeEditor(fixing SimpleDateFormat) (must match Dex2Jar version)
implementation("org.ow2.asm:asm:9.2")
// extracting zip files
// Disk & File
implementation("net.lingala.zip4j:zip4j:2.9.0")
implementation("com.github.junrar:junrar:7.4.0")
// CloudflareInterceptor
implementation("net.sourceforge.htmlunit:htmlunit:2.52.0")
// Source models and interfaces from Tachiyomi 1.x
// using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi
@@ -81,14 +72,13 @@ dependencies {
// implementation(fileTree("lib/"))
}
val MainClass = "suwayomi.tachidesk.MainKt"
application {
mainClass.set(MainClass)
// for testing electron
// uncomment for testing electron
// applicationDefaultJvmArgs = listOf(
// "-Dsuwayomi.tachidesk.webInterface=electron",
// "-Dsuwayomi.tachidesk.electronPath=/usr/bin/electron"
// "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron",
// "-Dsuwayomi.tachidesk.config.server.electronPath=/usr/bin/electron"
// )
}
@@ -100,71 +90,44 @@ sourceSets {
}
}
// should be bumped with each stable release
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.4.4"
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r20"
// counts commit count on master
val tachideskRevision = runCatching {
System.getenv("ProductRevision") ?: Runtime
.getRuntime()
.exec("git rev-list HEAD --count")
.let { process ->
process.waitFor()
val output = process.inputStream.use {
it.bufferedReader().use(BufferedReader::readText)
}
process.destroy()
"r" + output.trim()
}
}.getOrDefault("r0")
buildConfig {
clsName = "BuildConfig"
packageName = "suwayomi.tachidesk.server"
className("BuildConfig")
packageName("suwayomi.tachidesk.server")
useKotlinOutput()
buildConfigField("String", "NAME", rootProject.name)
buildConfigField("String", "VERSION", tachideskVersion)
buildConfigField("String", "REVISION", tachideskRevision)
buildConfigField("String", "BUILD_TYPE", if (System.getenv("ProductBuildType") == "Stable") "Stable" else "Preview")
fun quoteWrap(obj: Any): String = """"$obj""""
buildConfigField("String", "NAME", quoteWrap(rootProject.name))
buildConfigField("String", "VERSION", quoteWrap(tachideskVersion))
buildConfigField("String", "REVISION", quoteWrap(tachideskRevision))
buildConfigField("String", "BUILD_TYPE", quoteWrap(if (System.getenv("ProductBuildType") == "Stable") "Stable" else "Preview"))
buildConfigField("long", "BUILD_TIME", Instant.now().epochSecond.toString())
buildConfigField("String", "WEBUI_REPO", "https://github.com/Suwayomi/Tachidesk-WebUI-preview")
buildConfigField("String", "WEBUI_TAG", webUIRevisionTag)
buildConfigField("String", "WEBUI_REPO", quoteWrap("https://github.com/Suwayomi/Tachidesk-WebUI-preview"))
buildConfigField("String", "WEBUI_TAG", quoteWrap(webUIRevisionTag))
buildConfigField("String", "GITHUB", "https://github.com/Suwayomi/Tachidesk")
buildConfigField("String", "DISCORD", "https://discord.gg/DDZdqZWaHA")
buildConfigField("String", "GITHUB", quoteWrap("https://github.com/Suwayomi/Tachidesk"))
buildConfigField("String", "DISCORD", quoteWrap("https://discord.gg/DDZdqZWaHA"))
}
tasks {
shadowJar {
manifest {
attributes(
mapOf(
"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"
)
}
}
test {
useJUnit()
@@ -172,11 +135,10 @@ tasks {
withType<ShadowJar> {
destinationDirectory.set(File("$rootDir/server/build"))
dependsOn("formatKotlin", "lintKotlin")
}
named("run") {
dependsOn("formatKotlin", "lintKotlin")
dependsOn(":formatKotlin", ":lintKotlin")
}
named<Copy>("processResources") {
@@ -184,16 +146,29 @@ tasks {
mustRunAfter("downloadWebUI")
}
register<de.undercouch.gradle.tasks.download.Download>("downloadWebUI") {
register<Download>("downloadWebUI") {
src("https://github.com/Suwayomi/Tachidesk-WebUI-preview/releases/download/$webUIRevisionTag/Tachidesk-WebUI-$webUIRevisionTag.zip")
dest("src/main/resources/WebUI.zip")
fun shouldOverwrite(): Boolean {
val zipPath = project.projectDir.absolutePath + "/src/main/resources/WebUI.zip"
val zipFile = net.lingala.zip4j.ZipFile(zipPath)
var shouldOverwrite = true
if (zipFile.isValidZipFile) {
val zipRevision = zipFile.getInputStream(zipFile.getFileHeader("revision")).bufferedReader().use {
it.readText().trim()
}
withType<LintTask> {
source(files("src/kotlin"))
if (zipRevision == webUIRevisionTag) {
shouldOverwrite = false
}
}
withType<FormatTask> {
source(files("src/kotlin"))
return shouldOverwrite
}
overwrite(shouldOverwrite())
}
}
@@ -1,30 +1,13 @@
/*
Copyright 2014 Prateek Srivastava
Copyright 2015 Javier Tomás
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
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.
This file may have been modified after being copied from it's original source.
*/
package com.f2prateek.rx.preferences;
final class Preconditions {
static void checkNotNull(Object o, String message) {
if (o == null) {
throw new NullPointerException(message);
}
}
private Preconditions() {
throw new AssertionError("No instances");
}
}
@@ -9,7 +9,7 @@ interface AnimeCatalogueSource : AnimeSource {
/**
* An ISO 639-1 compliant language code (two letters in lower case).
*/
val lang: String
override val lang: String
/**
* Whether the source has support for latest updates.
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.animesource
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import rx.Observable
/**
@@ -19,6 +20,9 @@ interface AnimeSource {
*/
val name: String
val lang: String
get() = ""
/**
* Returns an observable with the updated details for a anime.
*
@@ -36,12 +40,12 @@ interface AnimeSource {
fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>>
/**
* Returns an observable with a link for the episode of an anime.
* Returns an observable with a list of video 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>
fun fetchVideoList(episode: SEpisode): Observable<List<Video>>
// /**
// * [1.x API] Get the updated details for a anime.
@@ -74,4 +78,4 @@ interface AnimeSource {
// fun AnimeSource.icon(): Drawable? = Injekt.get<AnimeExtensionManager>().getAppIconForSource(this)
// fun AnimeSource.getPreferenceKey(): String = "source_$id"
fun AnimeSource.getPreferenceKey(): String = "source_$id"
@@ -1,76 +0,0 @@
package eu.kanade.tachiyomi.animesource
import android.content.Context
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.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")
}
}
}
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.animesource.model
// import tachiyomi.animesource.model.AnimeInfo
import java.io.Serializable
interface SAnime : Serializable {
@@ -23,9 +24,7 @@ interface SAnime : Serializable {
var initialized: Boolean
fun copyFrom(other: SAnime) {
if (other.title != null) {
title = other.title
}
if (other.author != null) {
author = other.author
@@ -65,3 +64,30 @@ interface SAnime : Serializable {
}
}
}
// fun SAnime.toAnimeInfo(): AnimeInfo {
// return AnimeInfo(
// key = this.url,
// title = this.title,
// artist = this.artist ?: "",
// author = this.author ?: "",
// description = this.description ?: "",
// genres = this.genre?.split(", ") ?: emptyList(),
// status = this.status,
// cover = this.thumbnail_url ?: ""
// )
// }
// fun AnimeInfo.toSAnime(): SAnime {
// val animeInfo = this
// return SAnime.create().apply {
// url = animeInfo.key
// title = animeInfo.title
// artist = animeInfo.artist
// author = animeInfo.author
// description = animeInfo.description
// genre = animeInfo.genres.joinToString(", ")
// status = animeInfo.status
// thumbnail_url = animeInfo.cover
// }
// }
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.animesource.model
// import tachiyomi.animesource.model.EpisodeInfo
import java.io.Serializable
interface SEpisode : Serializable {
@@ -28,3 +29,24 @@ interface SEpisode : Serializable {
}
}
}
// fun SEpisode.toEpisodeInfo(): EpisodeInfo {
// return EpisodeInfo(
// dateUpload = this.date_upload,
// key = this.url,
// name = this.name,
// number = this.episode_number,
// scanlator = this.scanlator ?: ""
// )
// }
//
// fun EpisodeInfo.toSEpisode(): SEpisode {
// val episode = this
// return SEpisode.create().apply {
// url = episode.key
// name = episode.name
// date_upload = episode.dateUpload
// episode_number = episode.number
// scanlator = episode.scanlator
// }
// }
@@ -0,0 +1,73 @@
package eu.kanade.tachiyomi.animesource.model
import android.net.Uri
import eu.kanade.tachiyomi.network.ProgressListener
import rx.subjects.Subject
// import tachiyomi.animesource.model.VideoUrl
open class Video(
val url: String = "",
val quality: String = "",
var videoUrl: String? = null,
@Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions
) : ProgressListener {
@Transient
@Volatile
var status: Int = 0
set(value) {
field = value
statusSubject?.onNext(value)
statusCallback?.invoke(this)
}
@Transient
@Volatile
var progress: Int = 0
set(value) {
field = value
statusCallback?.invoke(this)
}
@Transient
private var statusSubject: Subject<Int, Int>? = null
@Transient
private var statusCallback: ((Video) -> Unit)? = null
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
progress = if (contentLength > 0) {
(100 * bytesRead / contentLength).toInt()
} else {
-1
}
}
fun setStatusSubject(subject: Subject<Int, Int>?) {
this.statusSubject = subject
}
fun setStatusCallback(f: ((Video) -> Unit)?) {
statusCallback = f
}
companion object {
const val QUEUE = 0
const val LOAD_VIDEO = 1
const val DOWNLOAD_IMAGE = 2
const val READY = 3
const val ERROR = 4
}
}
// fun Video.toVideoUrl(): VideoUrl {
// return VideoUrl(
// url = this.videoUrl ?: this.url
// )
// }
//
// fun VideoUrl.toVideo(index: Int): Video {
// return Video(
// videoUrl = this.url
// )
// }
@@ -5,11 +5,11 @@ import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
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.model.Page
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
@@ -54,7 +54,7 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
* Note the generated id sets the sign bit to 0.
*/
override val id by lazy {
val key = "${name.toLowerCase()}/$lang/$versionId"
val key = "${name.lowercase()}/$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
}
@@ -80,7 +80,7 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
/**
* Visible name of the source.
*/
override fun toString() = "$name (${lang.toUpperCase()})"
override fun toString() = "$name (${lang.uppercase()})"
/**
* Returns an observable containing a page with a list of anime. Normally it's not needed to
@@ -218,14 +218,6 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
}
}
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.
@@ -236,16 +228,6 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
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.
*
@@ -254,19 +236,25 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
protected abstract fun episodeListParse(response: Response): List<SEpisode>
/**
* Parses the response from the site and returns a list of episodes.
* Returns an observable with the page list for a chapter.
*
* @param response the response from the site.
* @param chapter the chapter whose page list has to be fetched.
*/
protected abstract fun episodeLinkParse(response: Response): String
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> {
return client.newCall(videoListRequest(episode))
.asObservableSuccess()
.map { response ->
videoListParse(response)
}
}
/**
* 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.
* 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 whose page list has to be fetched.
* @param episode the episode to look for links.
*/
protected open fun pageListRequest(episode: SEpisode): Request {
protected open fun videoListRequest(episode: SEpisode): Request {
return GET(baseUrl + episode.url, headers)
}
@@ -275,7 +263,7 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
*
* @param response the response from the site.
*/
protected abstract fun pageListParse(response: Response): List<Page>
protected abstract fun videoListParse(response: Response): List<Video>
/**
* Returns an observable with the page containing the source url of the image. If there's any
@@ -283,20 +271,20 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
*
* @param page the page whose source image has to be fetched.
*/
open fun fetchImageUrl(page: Page): Observable<String> {
return client.newCall(imageUrlRequest(page))
open fun fetchVideoUrl(video: Video): Observable<String> {
return client.newCall(videoUrlRequest(video))
.asObservableSuccess()
.map { imageUrlParse(it) }
.map { videoUrlParse(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
* @param page the chapter whose page list has to be fetched
*/
protected open fun imageUrlRequest(page: Page): Request {
return GET(page.url, headers)
protected open fun videoUrlRequest(video: Video): Request {
return GET(video.url, headers)
}
/**
@@ -304,15 +292,15 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
*
* @param response the response from the site.
*/
protected abstract fun imageUrlParse(response: Response): String
protected abstract fun videoUrlParse(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)
fun fetchVideo(video: Video): Observable<Response> {
return client.newCallWithProgress(videoRequest(video), video)
.asObservableSuccess()
}
@@ -320,10 +308,10 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
* 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
* @param video the video whose link has to be fetched
*/
protected open fun imageRequest(page: Page): Request {
return GET(page.imageUrl!!, headers)
protected open fun videoRequest(video: Video): Request {
return GET(video.videoUrl!!, headers)
}
/**
@@ -1,26 +1,25 @@
package eu.kanade.tachiyomi.source.online
package eu.kanade.tachiyomi.animesource.online
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.animesource.model.Video
import rx.Observable
fun AnimeHttpSource.getImageUrl(page: Page): Observable<Page> {
page.status = Page.LOAD_PAGE
return fetchImageUrl(page)
.doOnError { page.status = Page.ERROR }
fun AnimeHttpSource.getVideoUrl(video: Video): Observable<Video> {
video.status = Video.LOAD_VIDEO
return fetchVideoUrl(video)
.doOnError { video.status = Video.ERROR }
.onErrorReturn { null }
.doOnNext { page.imageUrl = it }
.map { page }
.doOnNext { video.videoUrl = it }
.map { video }
}
fun AnimeHttpSource.fetchAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
return Observable.from(pages)
.filter { !it.imageUrl.isNullOrEmpty() }
.mergeWith(fetchRemainingImageUrlsFromPageList(pages))
fun AnimeHttpSource.fetchUrlFromVideo(video: Video): Observable<Video> {
return Observable.just(video)
.filter { !it.videoUrl.isNullOrEmpty() }
.mergeWith(fetchRemainingVideoUrlsFromVideoList(video))
}
fun AnimeHttpSource.fetchRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
return Observable.from(pages)
.filter { it.imageUrl.isNullOrEmpty() }
.concatMap { getImageUrl(it) }
fun AnimeHttpSource.fetchRemainingVideoUrlsFromVideoList(video: Video): Observable<Video> {
return Observable.just(video)
.filter { it.videoUrl.isNullOrEmpty() }
.concatMap { getVideoUrl(it) }
}
@@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.animesource.online
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Response
import org.jsoup.nodes.Document
@@ -159,21 +159,6 @@ abstract class ParsedAnimeHttpSource : AnimeHttpSource() {
*/
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.
*
@@ -181,36 +166,35 @@ abstract class ParsedAnimeHttpSource : AnimeHttpSource() {
*/
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())
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
return document.select(videoListSelector()).map { videoFromElement(it) }
}
/**
* Returns a page list from the given document.
*
* @param document the parsed document.
* Returns the Jsoup selector that returns a list of [Element] corresponding to each video.
*/
protected abstract fun pageListParse(document: Document): List<Page>
protected abstract fun videoListSelector(): String
/**
* Parse the response from the site and returns the absolute url to the source image.
* Returns a video from the given element.
*
* @param element an element obtained from [videoListSelector].
*/
protected abstract fun videoFromElement(element: Element): Video
/**
* Parse the response from the site and returns the absolute url to the source video.
*
* @param response the response from the site.
*/
override fun imageUrlParse(response: Response): String {
return imageUrlParse(response.asJsoup())
override fun videoUrlParse(response: Response): String {
return videoUrlParse(response.asJsoup())
}
/**
@@ -218,5 +202,5 @@ abstract class ParsedAnimeHttpSource : AnimeHttpSource() {
*
* @param document the parsed document.
*/
protected abstract fun imageUrlParse(document: Document): String
protected abstract fun videoUrlParse(document: Document): String
}
@@ -1,177 +0,0 @@
package eu.kanade.tachiyomi.network
/*
* 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.os.Build
// import android.os.Handler
// import android.os.Looper
// import android.webkit.WebSettings
// import android.webkit.WebView
// import android.widget.Toast
// import eu.kanade.tachiyomi.R
// import eu.kanade.tachiyomi.util.lang.launchUI
// import eu.kanade.tachiyomi.util.system.WebViewClientCompat
// import eu.kanade.tachiyomi.util.system.WebViewUtil
// import eu.kanade.tachiyomi.util.system.isOutdated
// import eu.kanade.tachiyomi.util.system.setDefaultSettings
// import eu.kanade.tachiyomi.util.system.toast
import okhttp3.Interceptor
import okhttp3.Response
// import uy.kohesive.injekt.injectLazy
class CloudflareInterceptor() : Interceptor {
// private val handler = Handler(Looper.getMainLooper())
// private val networkHelper = NetworkHelper()
/**
* When this is called, it initializes the WebView if it wasn't already. We use this to avoid
* blocking the main thread too much. If used too often we could consider moving it to the
* Application class.
*/
// private val initWebView by lazy {
// WebSettings.getDefaultUserAgent(context)
// }
@Synchronized
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
return chain.proceed(originalRequest)
// if (!WebViewUtil.supportsWebView(context)) {
// launchUI {
// context.toast(R.string.information_webview_required, Toast.LENGTH_LONG)
// }
// return chain.proceed(originalRequest)
// }
//
// initWebView
//
// val response = chain.proceed(originalRequest)
//
// // Check if Cloudflare anti-bot is on
// if (response.code != 503 || response.header("Server") !in SERVER_CHECK) {
// return response
// }
//
// try {
// response.close()
// networkHelper.cookieManager.remove(originalRequest.url, COOKIE_NAMES, 0)
// val oldCookie = networkHelper.cookieManager.get(originalRequest.url)
// .firstOrNull { it.name == "cf_clearance" }
// resolveWithWebView(originalRequest, oldCookie)
//
// return chain.proceed(originalRequest)
// } catch (e: Exception) {
// // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
// // we don't crash the entire app
// throw IOException(e)
// }
}
//
// // @SuppressLint("SetJavaScriptEnabled")
// private fun resolveWithWebView(request: Request, oldCookie: Cookie?) {
// // We need to lock this thread until the WebView finds the challenge solution url, because
// // OkHttp doesn't support asynchronous interceptors.
// val latch = CountDownLatch(1)
//
// var webView: WebView? = null
//
// var challengeFound = false
// var cloudflareBypassed = false
// var isWebViewOutdated = false
//
// val origRequestUrl = request.url.toString()
// val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
// headers["X-Requested-With"] = WebViewUtil.REQUESTED_WITH
//
// handler.post {
// val webview = WebView(context)
// webView = webview
// webview.setDefaultSettings()
//
// // Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
// webview.settings.userAgentString = request.header("User-Agent")
// ?: HttpSource.DEFAULT_USERAGENT
//
// webview.webViewClient = object : WebViewClientCompat() {
// override fun onPageFinished(view: WebView, url: String) {
// fun isCloudFlareBypassed(): Boolean {
// return networkHelper.cookieManager.get(origRequestUrl.toHttpUrl())
// .firstOrNull { it.name == "cf_clearance" }
// .let { it != null && it != oldCookie }
// }
//
// if (isCloudFlareBypassed()) {
// cloudflareBypassed = true
// latch.countDown()
// }
//
// // HTTP error codes are only received since M
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
// url == origRequestUrl && !challengeFound
// ) {
// // The first request didn't return the challenge, abort.
// latch.countDown()
// }
// }
//
// override fun onReceivedErrorCompat(
// view: WebView,
// errorCode: Int,
// description: String?,
// failingUrl: String,
// isMainFrame: Boolean
// ) {
// if (isMainFrame) {
// if (errorCode == 503) {
// // Found the Cloudflare challenge page.
// challengeFound = true
// } else {
// // Unlock thread, the challenge wasn't found.
// latch.countDown()
// }
// }
// }
// }
//
// webView?.loadUrl(origRequestUrl, headers)
// }
//
// // Wait a reasonable amount of time to retrieve the solution. The minimum should be
// // around 4 seconds but it can take more due to slow networks or server issues.
// latch.await(12, TimeUnit.SECONDS)
//
// handler.post {
// if (!cloudflareBypassed) {
// isWebViewOutdated = webView?.isOutdated() == true
// }
//
// webView?.stopLoading()
// webView?.destroy()
// }
//
// // Throw exception if we failed to bypass Cloudflare
// if (!cloudflareBypassed) {
// // Prompt user to update WebView if it seems too outdated
// if (isWebViewOutdated) {
// context.toast(R.string.information_webview_outdated, Toast.LENGTH_LONG)
// }
//
// throw Exception(context.getString(R.string.information_cloudflare_bypass_failure))
// }
// }
//
// companion object {
// private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
// private val COOKIE_NAMES = listOf("__cfduid", "cf_clearance")
// }
}
@@ -10,69 +10,63 @@ package eu.kanade.tachiyomi.network
// import android.content.Context
// import eu.kanade.tachiyomi.BuildConfig
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import android.content.Context
// import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
// import okhttp3.dnsoverhttps.DnsOverHttps
// import okhttp3.logging.HttpLoggingInterceptor
// import uy.kohesive.injekt.injectLazy
import android.content.Context
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import suwayomi.tachidesk.server.serverConfig
import java.util.concurrent.TimeUnit
@Suppress("UNUSED_PARAMETER")
class NetworkHelper(context: Context) {
// private val preferences: PreferencesHelper by injectLazy()
// private val cacheDir = File(context.cacheDir, "network_cache")
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
// private val cacheSize = 5L * 1024 * 1024 // 5 MiB
val cookieManager = MemoryCookieJar()
val cookieManager = PersistentCookieJar(context)
val client by lazy {
private val baseClientBuilder: OkHttpClient.Builder
get() {
val builder = OkHttpClient.Builder()
.cookieJar(cookieManager)
// .cache(Cache(cacheDir, cacheSize))
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.MINUTES)
.writeTimeout(5, TimeUnit.MINUTES)
// .dispatcher(Dispatcher(Executors.newFixedThreadPool(1)))
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(UserAgentInterceptor())
// .addInterceptor(UserAgentInterceptor())
// if (BuildConfig.DEBUG) {
// val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
// level = HttpLoggingInterceptor.Level.HEADERS
// }
// builder.addInterceptor(httpLoggingInterceptor)
// }
// if (preferences.enableDoh()) {
// builder.dns(
// DnsOverHttps.Builder().client(builder.build())
// .url("https://cloudflare-dns.com/dns-query".toHttpUrl())
// .bootstrapDnsHosts(
// listOf(
// InetAddress.getByName("162.159.36.1"),
// InetAddress.getByName("162.159.46.1"),
// InetAddress.getByName("1.1.1.1"),
// InetAddress.getByName("1.0.0.1"),
// InetAddress.getByName("162.159.132.53"),
// InetAddress.getByName("2606:4700:4700::1111"),
// InetAddress.getByName("2606:4700:4700::1001"),
// InetAddress.getByName("2606:4700:4700::0064"),
// InetAddress.getByName("2606:4700:4700::6400")
// )
// )
// .build()
// )
// }
builder.build()
if (serverConfig.debugLogsEnabled) {
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BASIC
}
builder.addInterceptor(httpLoggingInterceptor)
}
// when (preferences.dohProvider()) {
// PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
// PREF_DOH_GOOGLE -> builder.dohGoogle()
// }
return builder
}
// val client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() }
val client by lazy { baseClientBuilder.build() }
val cloudflareClient by lazy {
client.newBuilder()
.addInterceptor(CloudflareInterceptor())
.build()
}
// Tachidesk -->
val cookies: PersistentCookieStore
get() = cookieManager.store
// Tachidesk <--
}
@@ -1,14 +1,19 @@
package eu.kanade.tachiyomi.network
// import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.internal.closeQuietly
import rx.Observable
import rx.Producer
import rx.Subscription
import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.resumeWithException
fun Call.asObservable(): Observable<Response> {
return Observable.unsafeCreate { subscriber ->
@@ -48,36 +53,38 @@ fun Call.asObservable(): Observable<Response> {
}
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
// suspend fun Call.await(assertSuccess: Boolean = false): Response {
// return suspendCancellableCoroutine { continuation ->
// enqueue(
// object : Callback {
// override fun onResponse(call: Call, response: Response) {
// if (assertSuccess && !response.isSuccessful) {
// continuation.resumeWithException(Exception("HTTP error ${response.code}"))
// return
// }
//
// continuation.resume(response)
// }
//
// override fun onFailure(call: Call, e: IOException) {
// // Don't bother with resuming the continuation if it is already cancelled.
// if (continuation.isCancelled) return
// continuation.resumeWithException(e)
// }
// }
// )
//
// continuation.invokeOnCancellation {
// try {
// cancel()
// } catch (ex: Throwable) {
// // Ignore cancel exception
// }
// }
// }
// }
suspend fun Call.await(): Response {
return suspendCancellableCoroutine { continuation ->
enqueue(
object : Callback {
override fun onResponse(call: Call, response: Response) {
if (!response.isSuccessful) {
continuation.resumeWithException(Exception("HTTP error ${response.code}"))
return
}
continuation.resume(response) {
response.body?.closeQuietly()
}
}
override fun onFailure(call: Call, e: IOException) {
// Don't bother with resuming the continuation if it is already cancelled.
if (continuation.isCancelled) return
continuation.resumeWithException(e)
}
}
)
continuation.invokeOnCancellation {
try {
cancel()
} catch (ex: Throwable) {
// Ignore cancel exception
}
}
}
}
fun Call.asObservableSuccess(): Observable<Response> {
return asObservable()
@@ -103,6 +110,7 @@ fun Call.asObservableSuccess(): Observable<Response> {
// return progressClient.newCall(request)
// }
@Suppress("UNUSED_PARAMETER")
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
val progressClient = newBuilder()
// .cache(null)
@@ -0,0 +1,20 @@
package eu.kanade.tachiyomi.network
import android.content.Context
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
// from TachiWeb-Server
class PersistentCookieJar(context: Context) : CookieJar {
val store = PersistentCookieStore(context)
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
store.addAll(url, cookies)
}
override fun loadForRequest(url: HttpUrl): List<Cookie> {
return store.get(url)
}
}
@@ -0,0 +1,79 @@
package eu.kanade.tachiyomi.network
import android.content.Context
import okhttp3.Cookie
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import java.net.URI
import java.util.concurrent.ConcurrentHashMap
// from TachiWeb-Server
class PersistentCookieStore(context: Context) {
private val cookieMap = ConcurrentHashMap<String, List<Cookie>>()
private val prefs = context.getSharedPreferences("cookie_store", Context.MODE_PRIVATE)
init {
for ((key, value) in prefs.all) {
@Suppress("UNCHECKED_CAST")
val cookies = value as? Set<String>
if (cookies != null) {
try {
val url = "http://$key".toHttpUrlOrNull() ?: continue
val nonExpiredCookies = cookies.mapNotNull { Cookie.parse(url, it) }
.filter { !it.hasExpired() }
cookieMap.put(key, nonExpiredCookies)
} catch (e: Exception) {
// Ignore
}
}
}
}
@Synchronized
fun addAll(url: HttpUrl, cookies: List<Cookie>) {
val key = url.toUri().host
// Append or replace the cookies for this domain.
val cookiesForDomain = cookieMap[key].orEmpty().toMutableList()
for (cookie in cookies) {
// Find a cookie with the same name. Replace it if found, otherwise add a new one.
val pos = cookiesForDomain.indexOfFirst { it.name == cookie.name }
if (pos == -1) {
cookiesForDomain.add(cookie)
} else {
cookiesForDomain[pos] = cookie
}
}
cookieMap.put(key, cookiesForDomain)
// Get cookies to be stored in disk
val newValues = cookiesForDomain.asSequence()
.filter { it.persistent && !it.hasExpired() }
.map(Cookie::toString)
.toSet()
prefs.edit().putStringSet(key, newValues).apply()
}
@Synchronized
fun removeAll() {
prefs.edit().clear().apply()
cookieMap.clear()
}
fun remove(uri: URI) {
prefs.edit().remove(uri.host).apply()
cookieMap.remove(uri.host)
}
fun get(url: HttpUrl) = get(url.toUri().host)
fun get(uri: URI) = get(uri.host)
private fun get(url: String): List<Cookie> {
return cookieMap[url].orEmpty().filter { !it.hasExpired() }
}
private fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt
}
@@ -0,0 +1,110 @@
package eu.kanade.tachiyomi.network.interceptor
import com.gargoylesoftware.htmlunit.BrowserVersion
import com.gargoylesoftware.htmlunit.WebClient
import com.gargoylesoftware.htmlunit.html.HtmlPage
import eu.kanade.tachiyomi.network.NetworkHelper
import mu.KotlinLogging
import okhttp3.Cookie
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
import java.io.IOException
// from TachiWeb-Server
class CloudflareInterceptor : Interceptor {
private val logger = KotlinLogging.logger {}
private val network: NetworkHelper by injectLazy()
@Synchronized
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
logger.trace { "CloudflareInterceptor is being used." }
val response = chain.proceed(originalRequest)
// Check if Cloudflare anti-bot is on
if (response.code != 503 || response.header("Server") !in SERVER_CHECK) {
return response
}
logger.debug { "Cloudflare anti-bot is on, CloudflareInterceptor is kicking in..." }
return try {
response.close()
network.cookies.remove(originalRequest.url.toUri())
chain.proceed(resolveChallenge(response))
} catch (e: Exception) {
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
// we don't crash the entire app
throw IOException(e)
}
}
private fun resolveChallenge(response: Response): Request {
val browserVersion = BrowserVersion.BrowserVersionBuilder(BrowserVersion.BEST_SUPPORTED)
.setUserAgent(response.request.header("User-Agent") ?: BrowserVersion.BEST_SUPPORTED.userAgent)
.build()
val convertedCookies = WebClient(browserVersion).use { webClient ->
webClient.options.isThrowExceptionOnFailingStatusCode = false
webClient.options.isThrowExceptionOnScriptError = false
webClient.getPage<HtmlPage>(response.request.url.toString())
webClient.waitForBackgroundJavaScript(10000)
// Challenge solved, process cookies
webClient.cookieManager.cookies.filter {
// Only include Cloudflare cookies
it.name.startsWith("__cf") || it.name.startsWith("cf_")
}.map {
// Convert cookies -> OkHttp format
Cookie.Builder()
.domain(it.domain.removePrefix("."))
.expiresAt(it.expires?.time ?: Long.MAX_VALUE)
.name(it.name)
.path(it.path)
.value(it.value).apply {
if (it.isHttpOnly) httpOnly()
if (it.isSecure) secure()
}.build()
}
}
// Copy cookies to cookie store
convertedCookies.forEach {
network.cookies.addAll(
HttpUrl.Builder()
.scheme("http")
.host(it.domain)
.build(),
listOf(it)
)
}
// Merge new and existing cookies for this request
// Find the cookies that we need to merge into this request
val convertedForThisRequest = convertedCookies.filter {
it.matches(response.request.url)
}
// Extract cookies from current request
val existingCookies = Cookie.parseAll(
response.request.url,
response.request.headers
)
// Filter out existing values of cookies that we are about to merge in
val filteredExisting = existingCookies.filter { existing ->
convertedForThisRequest.none { converted -> converted.name == existing.name }
}
val newCookies = filteredExisting + convertedForThisRequest
return response.request.newBuilder()
.header("Cookie", newCookies.map { it.toString() }.joinToString("; "))
.build()
}
companion object {
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
private val COOKIE_NAMES = listOf("cf_clearance")
}
}
@@ -0,0 +1,22 @@
package eu.kanade.tachiyomi.network.interceptor
import eu.kanade.tachiyomi.source.online.HttpSource
import okhttp3.Interceptor
import okhttp3.Response
class UserAgentInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
return if (originalRequest.header("User-Agent").isNullOrEmpty()) {
val newRequest = originalRequest
.newBuilder()
.removeHeader("User-Agent")
.addHeader("User-Agent", HttpSource.DEFAULT_USER_AGENT)
.build()
chain.proceed(newRequest)
} else {
chain.proceed(originalRequest)
}
}
}
@@ -1,358 +0,0 @@
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,77 +0,0 @@
package eu.kanade.tachiyomi.source
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(private val context: Context) {
private val sourcesMap = mutableMapOf<Long, Source>()
private val stubSourcesMap = mutableMapOf<Long, StubSource>()
init {
createInternalSources().forEach { registerSource(it) }
}
open fun get(sourceKey: Long): Source? {
return sourcesMap[sourceKey]
}
fun getOrStub(sourceKey: Long): Source {
return sourcesMap[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) {
StubSource(sourceKey)
}
}
fun getOnlineSources() = sourcesMap.values.filterIsInstance<HttpSource>()
fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>()
internal fun registerSource(source: Source, overwrite: Boolean = false) {
if (overwrite || !sourcesMap.containsKey(source.id)) {
sourcesMap[source.id] = source
}
if (overwrite || !stubSourcesMap.containsKey(source.id)) {
stubSourcesMap[source.id] = StubSource(source.id)
}
}
internal fun unregisterSource(source: Source) {
sourcesMap.remove(source.id)
}
private fun createInternalSources(): List<Source> = listOf(
// LocalSource(context)
)
private inner class StubSource(override val id: Long) : Source {
override val name: String
get() = id.toString()
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return Observable.error(getSourceNotInstalledException())
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return Observable.error(getSourceNotInstalledException())
}
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
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,478 @@
package eu.kanade.tachiyomi.source.local
import com.github.junrar.Archive
import eu.kanade.tachiyomi.source.local.FileSystemInterceptor.fakeUrlFrom
import eu.kanade.tachiyomi.source.local.LocalSource.Format.Directory
import eu.kanade.tachiyomi.source.local.LocalSource.Format.Epub
import eu.kanade.tachiyomi.source.local.LocalSource.Format.Rar
import eu.kanade.tachiyomi.source.local.LocalSource.Format.Zip
import eu.kanade.tachiyomi.source.local.loader.EpubPageLoader
import eu.kanade.tachiyomi.source.local.loader.RarPageLoader
import eu.kanade.tachiyomi.source.local.loader.ZipPageLoader
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.source.online.HttpSource
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.storage.EpubFile
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
import mu.KotlinLogging
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Protocol
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody.Companion.asResponseBody
import okhttp3.ResponseBody.Companion.toResponseBody
import okio.buffer
import okio.source
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.insertAndGetId
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 rx.Observable
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
import suwayomi.tachidesk.manga.model.table.ExtensionTable
import suwayomi.tachidesk.manga.model.table.SourceTable
import suwayomi.tachidesk.server.ApplicationDirs
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.InputStream
import java.net.URLDecoder
import java.util.Locale
import java.util.concurrent.TimeUnit
import java.util.zip.ZipFile
class LocalSource : HttpSource() {
companion object {
const val ID = 0L
const val LANG = "localsourcelang"
const val NAME = "Local source"
const val EXTENSION_NAME = "Local Source fake extension"
const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
private val logger = KotlinLogging.logger {}
private val applicationDirs by DI.global.instance<ApplicationDirs>()
val pageCache: MutableMap<String, List<() -> InputStream>> = mutableMapOf()
fun updateCover(manga: SManga, input: InputStream): File? {
val cover = getCoverFile(File("${applicationDirs.localMangaRoot}/${manga.url}"))
?: File("${applicationDirs.localMangaRoot}/${manga.url}/cover.jpg")
cover.parentFile?.mkdirs()
input.use {
cover.outputStream().use {
input.copyTo(it)
}
}
return cover
}
/**
* Returns valid cover file inside [parent] directory.
*/
private fun getCoverFile(parent: File): File? {
return parent.listFiles()?.find { it.nameWithoutExtension == "cover" }?.takeIf {
it.isFile && ImageUtil.isImage(it.name) { it.inputStream() }
}
}
fun addDbRecords() {
transaction {
val sourceRecord = SourceTable.select { SourceTable.id eq ID }.firstOrNull()
if (sourceRecord == null) {
// must do this to avoid database integrity errors
val extensionId = ExtensionTable.insertAndGetId {
it[apkName] = "localSource"
it[name] = EXTENSION_NAME
it[pkgName] = LocalSource::class.java.`package`.name
it[versionName] = "1.2"
it[versionCode] = 0
it[lang] = LANG
it[isNsfw] = false
it[isInstalled] = true
}
SourceTable.insert {
it[id] = ID
it[name] = NAME
it[lang] = LANG
it[extension] = extensionId
it[isNsfw] = false
}
}
}
}
}
override val id = ID
override val name = NAME
override val lang = LANG
override val baseUrl: String = ""
override val supportsLatest = true
override val client: OkHttpClient = super.client.newBuilder()
.addInterceptor(FileSystemInterceptor)
.build()
private val json: Json by injectLazy()
override fun toString() = name
override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
var mangaDirs = File(applicationDirs.localMangaRoot).listFiles().orEmpty().toList()
.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.lowercase(Locale.ENGLISH) }
} else {
mangaDirs.sortedByDescending { it.name.lowercase(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
val cover = getCoverFile(File("${applicationDirs.localMangaRoot}/$url"))
if (cover != null && cover.exists()) {
thumbnail_url = fakeUrlFrom(cover.absolutePath)
}
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?.let { fakeUrlFrom(it) }
} catch (e: Exception) {
logger.error { e }
}
}
}
}
}
return Observable.just(MangasPage(mangas.toList(), false))
}
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
File(applicationDirs.localMangaRoot, manga.url).listFiles().orEmpty().toList()
.firstOrNull { it.extension == "json" }
?.apply {
val obj = json.decodeFromStream<JsonObject>(inputStream())
manga.title = obj["title"]?.jsonPrimitive?.contentOrNull ?: manga.title
manga.author = obj["author"]?.jsonPrimitive?.contentOrNull ?: manga.author
manga.artist = obj["artist"]?.jsonPrimitive?.contentOrNull ?: manga.artist
manga.description = obj["description"]?.jsonPrimitive?.contentOrNull ?: manga.description
manga.genre = obj["genre"]?.jsonArray?.joinToString(", ") { it.jsonPrimitive.content }
?: manga.genre
manga.status = obj["status"]?.jsonPrimitive?.intOrNull ?: manga.status
}
return Observable.just(manga)
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val chapters = File(applicationDirs.localMangaRoot, manga.url).listFiles().orEmpty().toList()
.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 { 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(' ', '-', '_', ',', ':')
}
private fun isSupportedFile(extension: String): Boolean {
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
}
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
val chapterFile = File(applicationDirs.localMangaRoot + "/" + chapter.url)
return when (getFormat(chapterFile)) {
is Directory -> {
Observable.just(
chapterFile.listFiles().orEmpty().sortedBy { it.name }.mapIndexed { index, page ->
Page(
index,
imageUrl = fakeUrlFrom(applicationDirs.localMangaRoot + "/" + chapter.url + "/" + page.name)
)
}
)
}
is Zip -> {
val pages = ZipPageLoader(chapterFile).getPages()
pageCache[chapter.url] = pages.map { it.stream!! }
Observable.just(pages)
}
is Rar -> {
val pages = RarPageLoader(chapterFile).getPages()
pageCache[chapter.url] = pages.map { it.stream!! }
Observable.just(pages)
}
is Epub -> {
val pages = EpubPageLoader(chapterFile).getPages()
pageCache[chapter.url] = pages.map { it.stream!! }
Observable.just(pages)
}
}
}
fun getFormat(chapter: SChapter): Format {
val chapFile = File(applicationDirs.localMangaRoot, chapter.url)
if (chapFile.exists())
return getFormat(chapFile)
throw Exception("Chapter not found")
}
private fun getFormat(file: File): Format = with(file) {
when {
isDirectory -> Format.Directory(this)
extension.equals("zip", true) || extension.equals("cbz", true) -> Format.Zip(this)
extension.equals("rar", true) || extension.equals("cbr", true) -> Format.Rar(this)
extension.equals("epub", true) -> Format.Epub(this)
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(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(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(manga, archive.getInputStream(it)) }
}
}
is Format.Epub -> {
EpubFile(format.file).use { epub ->
val entry = epub.getImagesFromPages()
.firstOrNull()
?.let { epub.getEntry(it) }
entry?.let { updateCover(manga, epub.getInputStream(it)) }
}
}
}
}
override fun getFilterList() = POPULAR_FILTERS
private val POPULAR_FILTERS = FilterList(OrderBy())
private val LATEST_FILTERS = FilterList(OrderBy().apply { state = Filter.Sort.Selection(1, false) })
private class OrderBy : Filter.Sort(
"Order by",
arrayOf("Title", "Date"),
Selection(0, true)
)
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()
}
// ///////////////////// Not used ///////////////////// //
override fun mangaDetailsParse(response: Response): SManga = throw Exception("Not used")
override fun chapterListParse(response: Response): List<SChapter> = throw Exception("Not used")
override fun pageListParse(response: Response): List<Page> = throw Exception("Not used")
override fun imageUrlParse(response: Response): String = throw Exception("Not used")
override fun popularMangaRequest(page: Int): Request = throw Exception("Not used")
override fun popularMangaParse(response: Response): MangasPage = throw Exception("Not used")
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request =
throw Exception("Not used")
override fun searchMangaParse(response: Response): MangasPage = throw Exception("Not used")
override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used")
override fun latestUpdatesParse(response: Response): MangasPage = throw Exception("Not used")
}
private object FileSystemInterceptor : Interceptor {
fun fakeUrlFrom(path: String): String = "http://$path"
private fun restoreFilePath(url: String): String {
val path = URLDecoder.decode(url.replaceFirst("http://", ""), "UTF-8")
// Windows
if (System.getProperty("os.name").lowercase().startsWith("win")) {
// convert paths like "c/Users/..." to "c:/Users/..."
return StringBuilder(path).insert(1, ":").toString()
}
return "/$path"
}
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val url = request.url
val filePath = restoreFilePath(url.toString())
return try {
Response.Builder()
.body(File(filePath).source().buffer().asResponseBody())
.code(200)
.message("Some file")
.protocol(Protocol.HTTP_1_0)
.request(request)
.build()
} catch (e: FileNotFoundException) {
Response.Builder()
.body("".toResponseBody())
.code(404)
.message(e.message ?: "File not found ($filePath)")
.protocol(Protocol.HTTP_1_0)
.request(request)
.build()
}
}
}
@@ -0,0 +1,31 @@
package eu.kanade.tachiyomi.source.local.loader
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.storage.EpubFile
import java.io.File
/**
* Loader used to load a chapter from a .epub file.
*/
class EpubPageLoader(file: File) : PageLoader {
/**
* The epub file.
*/
private val epub = EpubFile(file)
/**
* Returns an observable containing the pages found on this zip archive ordered with a natural
* comparator.
*/
override fun getPages(): List<ReaderPage> {
return epub.getImagesFromPages()
.mapIndexed { i, path ->
val streamFn = { epub.getInputStream(epub.getEntry(path)!!) }
ReaderPage(i).apply {
stream = streamFn
status = Page.READY
}
}
}
}
@@ -0,0 +1,10 @@
package eu.kanade.tachiyomi.source.local.loader
// adapted from eu.kanade.tachiyomi.ui.reader.loader.PageLoader
interface PageLoader {
/**
* Returns an observable containing the list of pages of a chapter. Only the first emission
* will be used.
*/
fun getPages(): List<ReaderPage>
}
@@ -0,0 +1,63 @@
package eu.kanade.tachiyomi.source.local.loader
import com.github.junrar.Archive
import com.github.junrar.rarfile.FileHeader
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
import java.io.File
import java.io.InputStream
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.util.concurrent.Executors
/**
* Loader used to load a chapter from a .rar or .cbr file.
*/
class RarPageLoader(file: File) : PageLoader {
/**
* The rar archive to load pages from.
*/
private val archive = Archive(file)
/**
* Pool for copying compressed files to an input stream.
*/
private val pool = Executors.newFixedThreadPool(1)
/**
* Returns an observable containing the pages found on this rar archive ordered with a natural
* comparator.
*/
override fun getPages(): List<ReaderPage> {
return archive.fileHeaders
.filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.mapIndexed { i, header ->
val streamFn = { getStream(header) }
ReaderPage(i).apply {
stream = streamFn
status = Page.READY
}
}
}
/**
* Returns an input stream for the given [header].
*/
private fun getStream(header: FileHeader): InputStream {
val pipeIn = PipedInputStream()
val pipeOut = PipedOutputStream(pipeIn)
pool.execute {
try {
pipeOut.use {
archive.extractFile(header, it)
}
} catch (e: Exception) {
}
}
return pipeIn
}
}
@@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.source.local.loader
import eu.kanade.tachiyomi.source.model.Page
import java.io.InputStream
class ReaderPage(
index: Int,
url: String = "",
imageUrl: String? = null,
var stream: (() -> InputStream)? = null
) : Page(index, url, imageUrl, null)
@@ -0,0 +1,31 @@
package eu.kanade.tachiyomi.source.local.loader
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
import java.io.File
import java.util.zip.ZipFile
class ZipPageLoader(file: File) : PageLoader {
/**
* The zip file to load pages from.
*/
private val zip = ZipFile(file)
/**
* Returns an observable containing the pages found on this zip archive ordered with a natural
* comparator.
*/
override fun getPages(): List<ReaderPage> {
return zip.entries().toList()
.filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
.mapIndexed { i, entry ->
val streamFn = { zip.getInputStream(entry) }
ReaderPage(i).apply {
stream = streamFn
status = Page.READY
}
}
}
}
@@ -61,3 +61,30 @@ interface SManga : Serializable {
}
}
}
// fun SManga.toMangaInfo(): MangaInfo {
// return MangaInfo(
// key = this.url,
// title = this.title,
// artist = this.artist ?: "",
// author = this.author ?: "",
// description = this.description ?: "",
// genres = this.genre?.split(", ") ?: emptyList(),
// status = this.status,
// cover = this.thumbnail_url ?: ""
// )
// }
//
// fun MangaInfo.toSManga(): SManga {
// val mangaInfo = this
// return SManga.create().apply {
// url = mangaInfo.key
// title = mangaInfo.title
// artist = mangaInfo.artist
// author = mangaInfo.author
// description = mangaInfo.description
// genre = mangaInfo.genres.joinToString(", ")
// status = mangaInfo.status
// thumbnail_url = mangaInfo.cover
// }
// }
@@ -55,7 +55,7 @@ abstract class HttpSource : CatalogueSource {
* Note the generated id sets the sign bit to 0.
*/
override val id by lazy {
val key = "${name.toLowerCase()}/$lang/$versionId"
val key = "${name.lowercase()}/$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
}
@@ -75,13 +75,13 @@ abstract class HttpSource : CatalogueSource {
* Headers builder for requests. Implementations can override this method for custom headers.
*/
protected open fun headersBuilder() = Headers.Builder().apply {
add("User-Agent", DEFAULT_USERAGENT)
add("User-Agent", DEFAULT_USER_AGENT)
}
/**
* Visible name of the source.
*/
override fun toString() = "$name (${lang.toUpperCase()})"
override fun toString() = "$name (${lang.uppercase()})"
/**
* Returns an observable containing a page with a list of manga. Normally it's not needed to
@@ -372,6 +372,6 @@ abstract class HttpSource : CatalogueSource {
override fun getFilterList() = FilterList()
companion object {
const val DEFAULT_USERAGENT = "Mozilla/5.0 (Windows NT 6.3; WOW64)"
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,152 @@
package eu.kanade.tachiyomi.util.chapter
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
/**
* -R> = regex conversion.
*/
object ChapterRecognition {
/**
* All cases with Ch.xx
* Mokushiroku Alice Vol.1 Ch. 4: Misrepresentation -R> 4
*/
private val basic = Regex("""(?<=ch\.) *([0-9]+)(\.[0-9]+)?(\.?[a-z]+)?""")
/**
* Regex used when only one number occurrence
* Example: Bleach 567: Down With Snowwhite -R> 567
*/
private val occurrence = Regex("""([0-9]+)(\.[0-9]+)?(\.?[a-z]+)?""")
/**
* Regex used when manga title removed
* Example: Solanin 028 Vol. 2 -> 028 Vol.2 -> 028Vol.2 -R> 028
*/
private val withoutManga = Regex("""^([0-9]+)(\.[0-9]+)?(\.?[a-z]+)?""")
/**
* Regex used to remove unwanted tags
* Example Prison School 12 v.1 vol004 version1243 volume64 -R> Prison School 12
*/
private val unwanted = Regex("""(?<![a-z])(v|ver|vol|version|volume|season|s).?[0-9]+""")
/**
* Regex used to remove unwanted whitespace
* Example One Piece 12 special -R> One Piece 12special
*/
private val unwantedWhiteSpace = Regex("""(\s)(extra|special|omake)""")
fun parseChapterNumber(chapter: SChapter, manga: SManga) {
// If chapter number is known return.
if (chapter.chapter_number == -2f || chapter.chapter_number > -1f) {
return
}
// Get chapter title with lower case
var name = chapter.name.lowercase()
// Remove comma's from chapter.
name = name.replace(',', '.')
// Remove unwanted white spaces.
unwantedWhiteSpace.findAll(name).let {
it.forEach { occurrence -> name = name.replace(occurrence.value, occurrence.value.trim()) }
}
// Remove unwanted tags.
unwanted.findAll(name).let {
it.forEach { occurrence -> name = name.replace(occurrence.value, "") }
}
// Check base case ch.xx
if (updateChapter(basic.find(name), chapter)) {
return
}
// Check one number occurrence.
val occurrences: MutableList<MatchResult> = arrayListOf()
occurrence.findAll(name).let {
it.forEach { occurrence -> occurrences.add(occurrence) }
}
if (occurrences.size == 1) {
if (updateChapter(occurrences[0], chapter)) {
return
}
}
// Remove manga title from chapter title.
val nameWithoutManga = name.replace(manga.title.lowercase(), "").trim()
// Check if first value is number after title remove.
if (updateChapter(withoutManga.find(nameWithoutManga), chapter)) {
return
}
// Take the first number encountered.
if (updateChapter(occurrence.find(nameWithoutManga), chapter)) {
return
}
}
/**
* Check if volume is found and update chapter
* @param match result of regex
* @param chapter chapter object
* @return true if volume is found
*/
private fun updateChapter(match: MatchResult?, chapter: SChapter): Boolean {
match?.let {
val initial = it.groups[1]?.value?.toFloat()!!
val subChapterDecimal = it.groups[2]?.value
val subChapterAlpha = it.groups[3]?.value
val addition = checkForDecimal(subChapterDecimal, subChapterAlpha)
chapter.chapter_number = initial.plus(addition)
return true
}
return false
}
/**
* Check for decimal in received strings
* @param decimal decimal value of regex
* @param alpha alpha value of regex
* @return decimal/alpha float value
*/
private fun checkForDecimal(decimal: String?, alpha: String?): Float {
if (!decimal.isNullOrEmpty()) {
return decimal.toFloat()
}
if (!alpha.isNullOrEmpty()) {
if (alpha.contains("extra")) {
return .99f
}
if (alpha.contains("omake")) {
return .98f
}
if (alpha.contains("special")) {
return .97f
}
return if (alpha[0] == '.') {
// Take value after (.)
parseAlphaPostFix(alpha[1])
} else {
parseAlphaPostFix(alpha[0])
}
}
return .0f
}
/**
* x.a -> x.1, x.b -> x.2, etc
*/
private fun parseAlphaPostFix(alpha: Char): Float {
return ("0." + (alpha.code - 96).toString()).toFloat()
}
}
@@ -0,0 +1,58 @@
package eu.kanade.tachiyomi.util.lang
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
import kotlin.math.floor
/**
* Replaces the given string to have at most [count] characters using [replacement] at its end.
* If [replacement] is longer than [count] an exception will be thrown when `length > count`.
*/
fun String.chop(count: Int, replacement: String = ""): String {
return if (length > count) {
take(count - replacement.length) + replacement
} else {
this
}
}
/**
* Replaces the given string to have at most [count] characters using [replacement] near the center.
* If [replacement] is longer than [count] an exception will be thrown when `length > count`.
*/
fun String.truncateCenter(count: Int, replacement: String = "..."): String {
if (length <= count) {
return this
}
val pieceLength: Int = floor((count - replacement.length).div(2.0)).toInt()
return "${take(pieceLength)}$replacement${takeLast(pieceLength)}"
}
/**
* Case-insensitive natural comparator for strings.
*/
fun String.compareToCaseInsensitiveNaturalOrder(other: String): Int {
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
return comparator.compare(this, other)
}
/**
* Returns the size of the string as the number of bytes.
*/
fun String.byteSize(): Int {
return toByteArray(Charsets.UTF_8).size
}
/**
* Returns a string containing the first [n] bytes from this string, or the entire string if this
* string is shorter.
*/
fun String.takeBytes(n: Int): String {
val bytes = toByteArray(Charsets.UTF_8)
return if (bytes.size <= n) {
this
} else {
bytes.decodeToString(endIndex = n).replace("\uFFFD", "")
}
}
@@ -0,0 +1,215 @@
package eu.kanade.tachiyomi.util.storage
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import java.io.Closeable
import java.io.File
import java.io.InputStream
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
/**
* Wrapper over ZipFile to load files in epub format.
*/
class EpubFile(file: File) : Closeable {
/**
* Zip file of this epub.
*/
private val zip = ZipFile(file)
/**
* Path separator used by this epub.
*/
private val pathSeparator = getPathSeparator()
/**
* Closes the underlying zip file.
*/
override fun close() {
zip.close()
}
/**
* Returns an input stream for reading the contents of the specified zip file entry.
*/
fun getInputStream(entry: ZipEntry): InputStream {
return zip.getInputStream(entry)
}
/**
* Returns the zip file entry for the specified name, or null if not found.
*/
fun getEntry(name: String): ZipEntry? {
return zip.getEntry(name)
}
/**
* Fills manga metadata using this epub file's metadata.
*/
fun fillMangaMetadata(manga: SManga) {
val ref = getPackageHref()
val doc = getPackageDocument(ref)
val creator = doc.getElementsByTag("dc:creator").first()
val description = doc.getElementsByTag("dc:description").first()
manga.author = creator?.text()
manga.description = description?.text()
}
/**
* Fills chapter metadata using this epub file's metadata.
*/
fun fillChapterMetadata(chapter: SChapter) {
val ref = getPackageHref()
val doc = getPackageDocument(ref)
val title = doc.getElementsByTag("dc:title").first()
val publisher = doc.getElementsByTag("dc:publisher").first()
val creator = doc.getElementsByTag("dc:creator").first()
var date = doc.getElementsByTag("dc:date").first()
if (date == null) {
date = doc.select("meta[property=dcterms:modified]").first()
}
if (title != null) {
chapter.name = title.text()
}
if (publisher != null) {
chapter.scanlator = publisher.text()
} else if (creator != null) {
chapter.scanlator = creator.text()
}
if (date != null) {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault())
try {
val parsedDate = dateFormat.parse(date.text())
if (parsedDate != null) {
chapter.date_upload = parsedDate.time
}
} catch (e: ParseException) {
// Empty
}
}
}
/**
* Returns the path of all the images found in the epub file.
*/
fun getImagesFromPages(): List<String> {
val ref = getPackageHref()
val doc = getPackageDocument(ref)
val pages = getPagesFromDocument(doc)
return getImagesFromPages(pages, ref)
}
/**
* Returns the path to the package document.
*/
private fun getPackageHref(): String {
val meta = zip.getEntry(resolveZipPath("META-INF", "container.xml"))
if (meta != null) {
val metaDoc = zip.getInputStream(meta).use { Jsoup.parse(it, null, "") }
val path = metaDoc.getElementsByTag("rootfile").first()?.attr("full-path")
if (path != null) {
return path
}
}
return resolveZipPath("OEBPS", "content.opf")
}
/**
* Returns the package document where all the files are listed.
*/
private fun getPackageDocument(ref: String): Document {
val entry = zip.getEntry(ref)
return zip.getInputStream(entry).use { Jsoup.parse(it, null, "") }
}
/**
* Returns all the pages from the epub.
*/
private fun getPagesFromDocument(document: Document): List<String> {
val pages = document.select("manifest > item")
.filter { "application/xhtml+xml" == it.attr("media-type") }
.associateBy { it.attr("id") }
val spine = document.select("spine > itemref").map { it.attr("idref") }
return spine.mapNotNull { pages[it] }.map { it.attr("href") }
}
/**
* Returns all the images contained in every page from the epub.
*/
private fun getImagesFromPages(pages: List<String>, packageHref: String): List<String> {
val result = mutableListOf<String>()
val basePath = getParentDirectory(packageHref)
pages.forEach { page ->
val entryPath = resolveZipPath(basePath, page)
val entry = zip.getEntry(entryPath)
val document = zip.getInputStream(entry).use { Jsoup.parse(it, null, "") }
val imageBasePath = getParentDirectory(entryPath)
document.allElements.forEach {
if (it.tagName() == "img") {
result.add(resolveZipPath(imageBasePath, it.attr("src")))
} else if (it.tagName() == "image") {
result.add(resolveZipPath(imageBasePath, it.attr("xlink:href")))
}
}
}
return result
}
/**
* Returns the path separator used by the epub file.
*/
private fun getPathSeparator(): String {
val meta = zip.getEntry("META-INF\\container.xml")
return if (meta != null) {
"\\"
} else {
"/"
}
}
/**
* Resolves a zip path from base and relative components and a path separator.
*/
private fun resolveZipPath(basePath: String, relativePath: String): String {
if (relativePath.startsWith(pathSeparator)) {
// Path is absolute, so return as-is.
return relativePath
}
var fixedBasePath = basePath.replace(pathSeparator, File.separator)
if (!fixedBasePath.startsWith(File.separator)) {
fixedBasePath = "${File.separator}$fixedBasePath"
}
val fixedRelativePath = relativePath.replace(pathSeparator, File.separator)
val resolvedPath = File(fixedBasePath, fixedRelativePath).canonicalPath
return resolvedPath.replace(File.separator, pathSeparator).substring(1)
}
/**
* Gets the parent directory of a path.
*/
private fun getParentDirectory(path: String): String {
val separatorIndex = path.lastIndexOf(pathSeparator)
return if (separatorIndex >= 0) {
path.substring(0, separatorIndex)
} else {
""
}
}
}
@@ -14,6 +14,7 @@ import suwayomi.tachidesk.anime.impl.AnimeList.getAnimeList
import suwayomi.tachidesk.anime.impl.Episode.getEpisode
import suwayomi.tachidesk.anime.impl.Episode.getEpisodeList
import suwayomi.tachidesk.anime.impl.Episode.modifyEpisode
import suwayomi.tachidesk.anime.impl.Search.sourceSearch
import suwayomi.tachidesk.anime.impl.Source.getAnimeSource
import suwayomi.tachidesk.anime.impl.Source.getSourceList
import suwayomi.tachidesk.anime.impl.extension.Extension.getExtensionIcon
@@ -27,7 +28,7 @@ object AnimeAPI {
fun defineEndpoints(app: Javalin) {
// list all extensions
app.get("/api/v1/anime/extension/list") { ctx ->
ctx.json(
ctx.future(
future {
getExtensionList()
}
@@ -35,10 +36,10 @@ object AnimeAPI {
}
// install extension identified with "pkgName"
app.get("/api/v1/anime/extension/install/:pkgName") { ctx ->
app.get("/api/v1/anime/extension/install/{pkgName}") { ctx ->
val pkgName = ctx.pathParam("pkgName")
ctx.json(
ctx.future(
future {
installExtension(pkgName)
}
@@ -46,10 +47,10 @@ object AnimeAPI {
}
// update extension identified with "pkgName"
app.get("/api/v1/anime/extension/update/:pkgName") { ctx ->
app.get("/api/v1/anime/extension/update/{pkgName}") { ctx ->
val pkgName = ctx.pathParam("pkgName")
ctx.json(
ctx.future(
future {
updateExtension(pkgName)
}
@@ -57,7 +58,7 @@ object AnimeAPI {
}
// uninstall extension identified with "pkgName"
app.get("/api/v1/anime/extension/uninstall/:pkgName") { ctx ->
app.get("/api/v1/anime/extension/uninstall/{pkgName}") { ctx ->
val pkgName = ctx.pathParam("pkgName")
uninstallExtension(pkgName)
@@ -65,10 +66,10 @@ object AnimeAPI {
}
// icon for extension named `apkName`
app.get("/api/v1/anime/extension/icon/:apkName") { ctx -> // TODO: move to pkgName
app.get("/api/v1/anime/extension/icon/{apkName}") { ctx -> // TODO: move to pkgName
val apkName = ctx.pathParam("apkName")
ctx.result(
ctx.future(
future { getExtensionIcon(apkName) }
.thenApply {
ctx.header("content-type", it.second)
@@ -83,16 +84,16 @@ object AnimeAPI {
}
// fetch source with id `sourceId`
app.get("/api/v1/anime/source/:sourceId") { ctx ->
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 ->
app.get("/api/v1/anime/source/{sourceId}/popular/{pageNum}") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(
ctx.future(
future {
getAnimeList(sourceId, pageNum, popular = true)
}
@@ -100,10 +101,10 @@ object AnimeAPI {
}
// latest animes from source with id `sourceId`
app.get("/api/v1/anime/source/:sourceId/latest/:pageNum") { ctx ->
app.get("/api/v1/anime/source/{sourceId}/latest/{pageNum}") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(
ctx.future(
future {
getAnimeList(sourceId, pageNum, popular = false)
}
@@ -111,11 +112,11 @@ object AnimeAPI {
}
// get anime info
app.get("/api/v1/anime/anime/:animeId/") { ctx ->
app.get("/api/v1/anime/anime/{animeId}/") { ctx ->
val animeId = ctx.pathParam("animeId").toInt()
val onlineFetch = ctx.queryParam("onlineFetch", "false").toBoolean()
val onlineFetch = ctx.queryParam("onlineFetch")?.toBoolean() ?: false
ctx.json(
ctx.future(
future {
getAnime(animeId, onlineFetch)
}
@@ -123,10 +124,10 @@ object AnimeAPI {
}
// anime thumbnail
app.get("api/v1/anime/anime/:animeId/thumbnail") { ctx ->
app.get("api/v1/anime/anime/{animeId}/thumbnail") { ctx ->
val animeId = ctx.pathParam("animeId").toInt()
ctx.result(
ctx.future(
future { getAnimeThumbnail(animeId) }
.thenApply {
ctx.header("content-type", it.second)
@@ -136,13 +137,13 @@ object AnimeAPI {
}
//
// // list manga's categories
// app.get("api/v1/manga/:mangaId/category/") { ctx ->
// 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 ->
// app.get("api/v1/manga/{mangaId}/category/{categoryId}") { ctx ->
// val mangaId = ctx.pathParam("mangaId").toInt()
// val categoryId = ctx.pathParam("categoryId").toInt()
// addMangaToCategory(mangaId, categoryId)
@@ -150,7 +151,7 @@ object AnimeAPI {
// }
//
// // removes the manga from the category
// app.delete("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
// app.delete("api/v1/manga/{mangaId}/category/{categoryId}") { ctx ->
// val mangaId = ctx.pathParam("mangaId").toInt()
// val categoryId = ctx.pathParam("categoryId").toInt()
// removeMangaFromCategory(mangaId, categoryId)
@@ -158,23 +159,23 @@ object AnimeAPI {
// }
//
// get episode list when showing a anime
app.get("/api/v1/anime/anime/:animeId/episodes") { ctx ->
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) })
ctx.future(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 ->
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) })
ctx.future(future { getEpisode(episodeIndex, animeId) })
}
// used to modify a episode's parameters
app.patch("/api/v1/anime/anime/:animeId/episode/:episodeIndex") { ctx ->
app.patch("/api/v1/anime/anime/{animeId}/episode/{episodeIndex}") { ctx ->
val episodeIndex = ctx.pathParam("episodeIndex").toInt()
val animeId = ctx.pathParam("animeId").toInt()
@@ -189,7 +190,7 @@ object AnimeAPI {
}
//
// // get page at index "index"
// app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex/page/:index") { ctx ->
// 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()
@@ -204,49 +205,49 @@ object AnimeAPI {
// }
//
// // submit a chapter for download
// app.put("/api/v1/manga/:mangaId/chapter/:chapterIndex/download") { ctx ->
// 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 ->
// app.delete("/api/v1/manga/{mangaId}/chapter/{chapterIndex}/download") { ctx ->
// // TODO
// }
//
// // global search, Not implemented yet
// app.get("/api/v1/search/:searchTerm") { ctx ->
// 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) })
// }
// single source search
app.get("/api/v1/anime/source/{sourceId}/search/{searchTerm}/{pageNum}") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
val searchTerm = ctx.pathParam("searchTerm")
val pageNum = ctx.pathParam("pageNum").toInt()
ctx.future(future { sourceSearch(sourceId, searchTerm, pageNum) })
}
//
// // source filter list
// app.get("/api/v1/source/:sourceId/filters/") { ctx ->
// 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 ->
// app.get("api/v1/manga/{mangaId}/library") { ctx ->
// val mangaId = ctx.pathParam("mangaId").toInt()
//
// ctx.result(
// ctx.future(
// JavalinSetup.future { addMangaToLibrary(mangaId) }
// )
// }
//
// // removes the manga from the library
// app.delete("api/v1/manga/:mangaId/library") { ctx ->
// app.delete("api/v1/manga/{mangaId}/library") { ctx ->
// val mangaId = ctx.pathParam("mangaId").toInt()
//
// ctx.result(
// ctx.future(
// JavalinSetup.future { removeMangaFromLibrary(mangaId) }
// )
// }
@@ -274,7 +275,7 @@ object AnimeAPI {
// }
//
// // category modification
// app.patch("/api/v1/category/:categoryId") { ctx ->
// app.patch("/api/v1/category/{categoryId}") { ctx ->
// val categoryId = ctx.pathParam("categoryId").toInt()
// val name = ctx.formParam("name")
// val isDefault = ctx.formParam("default")?.toBoolean()
@@ -283,7 +284,7 @@ object AnimeAPI {
// }
//
// // category re-ordering
// app.patch("/api/v1/category/:categoryId/reorder") { ctx ->
// 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()
@@ -292,21 +293,21 @@ object AnimeAPI {
// }
//
// // category delete
// app.delete("/api/v1/category/:categoryId") { ctx ->
// 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 ->
// 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(
// ctx.future(
// future {
// restoreLegacyBackup(ctx.bodyAsInputStream())
// }
@@ -315,7 +316,7 @@ object AnimeAPI {
//
// // 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(
// ctx.future(
// JavalinSetup.future {
// restoreLegacyBackup(ctx.uploadedFile("backup.json")!!.content)
// }
@@ -325,7 +326,7 @@ object AnimeAPI {
// // 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(
// ctx.future(
// JavalinSetup.future {
// createLegacyBackup(
// BackupFlags(
@@ -347,7 +348,7 @@ object AnimeAPI {
// val currentDate = sdf.format(Date())
//
// ctx.header("Content-Disposition", "attachment; filename=\"tachidesk_$currentDate.json\"")
// ctx.result(
// ctx.future(
// JavalinSetup.future {
// createLegacyBackup(
// BackupFlags(
@@ -23,8 +23,8 @@ import suwayomi.tachidesk.anime.model.table.AnimeStatus
import suwayomi.tachidesk.anime.model.table.AnimeTable
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.network.await
import suwayomi.tachidesk.manga.impl.util.storage.CachedImageResponse.clearCachedImage
import suwayomi.tachidesk.manga.impl.util.storage.CachedImageResponse.getCachedImageResponse
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.clearCachedImage
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getCachedImageResponse
import suwayomi.tachidesk.server.ApplicationDirs
import java.io.InputStream
@@ -19,6 +19,7 @@ import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.anime.impl.Anime.getAnime
import suwayomi.tachidesk.anime.impl.util.GetAnimeHttpSource.getAnimeHttpSource
import suwayomi.tachidesk.anime.model.dataclass.EpisodeDataClass
import suwayomi.tachidesk.anime.model.dataclass.VideoDataClass
import suwayomi.tachidesk.anime.model.table.AnimeTable
import suwayomi.tachidesk.anime.model.table.EpisodeTable
import suwayomi.tachidesk.anime.model.table.toDataClass
@@ -135,7 +136,7 @@ object Episode {
val animeEntry = transaction { AnimeTable.select { AnimeTable.id eq animeId }.first() }
val source = getAnimeHttpSource(animeEntry[AnimeTable.sourceReference])
val fetchedLinkUrl = source.fetchEpisodeLink(
val fetchedVideos = source.fetchVideoList(
SEpisode.create().also {
it.url = episode.url
it.name = episode.name
@@ -154,7 +155,13 @@ object Episode {
episode.lastPageRead,
episode.index,
episode.episodeCount,
fetchedLinkUrl
fetchedVideos.map {
VideoDataClass(
it.url,
it.quality,
it.videoUrl,
)
}
)
}
@@ -0,0 +1,21 @@
package suwayomi.tachidesk.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 suwayomi.tachidesk.anime.impl.AnimeList.processEntries
import suwayomi.tachidesk.anime.impl.util.GetAnimeHttpSource.getAnimeHttpSource
import suwayomi.tachidesk.anime.model.dataclass.PagedAnimeListDataClass
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
object Search {
suspend fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedAnimeListDataClass {
val source = getAnimeHttpSource(sourceId)
val searchManga = source.fetchSearchAnime(pageNum, searchTerm, source.getFilterList()).awaitSingle()
return searchManga.processEntries(sourceId)
}
}
@@ -40,7 +40,7 @@ import suwayomi.tachidesk.anime.impl.util.PackageTools.trustedSignatures
import suwayomi.tachidesk.anime.model.table.AnimeExtensionTable
import suwayomi.tachidesk.anime.model.table.AnimeSourceTable
import suwayomi.tachidesk.manga.impl.util.network.await
import suwayomi.tachidesk.manga.impl.util.storage.CachedImageResponse.getCachedImageResponse
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getCachedImageResponse
import suwayomi.tachidesk.server.ApplicationDirs
import uy.kohesive.injekt.injectLazy
import java.io.File
@@ -138,7 +138,7 @@ object Extension {
else -> "all"
}
val extensionName = packageInfo.applicationInfo.nonLocalizedLabel.toString().substringAfter("Tachiyomi: ")
val extensionName = packageInfo.applicationInfo.nonLocalizedLabel.toString().substringAfter("Aniyomi: ")
// update extension info
transaction {
@@ -32,7 +32,7 @@ object ExtensionGithubApi {
libVersion in LIB_VERSION_MIN..LIB_VERSION_MAX
}
.map { element ->
val name = element["name"].string.substringAfter("Tachiyomi: ")
val name = element["name"].string.substringAfter("Aniyomi: ")
val pkgName = element["pkg"].string
val apkName = element["apk"].string
val versionName = element["version"].string
@@ -41,8 +41,8 @@ object PackageTools {
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 = 10
const val LIB_VERSION_MAX = 10
const val LIB_VERSION_MIN = 12
const val LIB_VERSION_MAX = 12
private const val officialSignature = "50ab1d1e3a20d204d0ad6d334c7691c632e41b98dfa132bf385695fdfa63839c" // jmir1's key
var trustedSignatures = mutableSetOf<String>() + officialSignature
@@ -82,7 +82,7 @@ object PackageTools {
)
handler.dump(errorFile, emptyArray<String>())
} else {
BytecodeEditor.fixAndroidClasses(jarFilePath.toFile())
BytecodeEditor.fixAndroidClasses(jarFilePath)
}
}
@@ -31,5 +31,5 @@ data class EpisodeDataClass(
val episodeCount: Int? = null,
/** used to construct pages in the front-end */
val linkUrl: String? = null,
val videos: List<VideoDataClass>? = null,
)
@@ -0,0 +1,14 @@
package suwayomi.tachidesk.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 VideoDataClass(
val url: String,
val quality: String,
var videoUrl: String?,
)
@@ -12,6 +12,7 @@ import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.ResultRow
import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.manga.model.dataclass.toGenreList
import suwayomi.tachidesk.manga.model.table.MangaStatus.Companion
object AnimeTable : IntIdTable() {
@@ -48,7 +49,7 @@ fun AnimeTable.toDataClass(mangaEntry: ResultRow) =
mangaEntry[artist],
mangaEntry[author],
mangaEntry[description],
mangaEntry[genre],
mangaEntry[genre].toGenreList(),
Companion.valueOf(mangaEntry[status]).name,
mangaEntry[inLibrary]
)
@@ -7,18 +7,15 @@ package suwayomi.tachidesk.global
* 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 io.javalin.apibuilder.ApiBuilder.get
import io.javalin.apibuilder.ApiBuilder.path
import suwayomi.tachidesk.global.controller.SettingsController
object GlobalAPI {
fun defineEndpoints(app: Javalin) {
app.routes {
path("api/v1/settings") {
fun defineEndpoints() {
path("settings") {
get("about", SettingsController::about)
get("check-update", SettingsController::checkUpdate)
}
}
}
}
@@ -10,19 +10,19 @@ package suwayomi.tachidesk.global.controller
import io.javalin.http.Context
import suwayomi.tachidesk.global.impl.About
import suwayomi.tachidesk.global.impl.AppUpdate
import suwayomi.tachidesk.server.JavalinSetup
import suwayomi.tachidesk.server.JavalinSetup.future
/** Settings Page/Screen */
object SettingsController {
/** returns some static info about the current app build */
fun about(ctx: Context): Context {
return ctx.json(About.getAbout())
fun about(ctx: Context) {
ctx.json(About.getAbout())
}
/** check for app updates */
fun checkUpdate(ctx: Context): Context {
return ctx.json(
JavalinSetup.future { AppUpdate.checkUpdate() }
fun checkUpdate(ctx: Context) {
ctx.json(
future { AppUpdate.checkUpdate() }
)
}
}

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