Compare commits

...

229 Commits

Author SHA1 Message Date
Syer10 22df7e3074 Release v2.0.1727
CI Publish / Validate Gradle Wrapper (push) Successful in 2m14s
CI Publish / jlink (linux-x64, ubuntu-latest) (push) Failing after 1m6s
CI Publish / Build Jar (push) Failing after 2m8s
CI Publish / jlink (macOS-arm64, macos-14) (push) Has been cancelled
CI Publish / jlink (macOS-x64, macos-13) (push) Has been cancelled
CI Publish / jlink (windows-x64, windows-latest) (push) Has been cancelled
CI Publish / Make debian-all release (push) Has been cancelled
CI Publish / Make linux-assets release (push) Has been cancelled
CI Publish / Make linux-x64 release (push) Has been cancelled
CI Publish / Make macOS-arm64 release (push) Has been cancelled
CI Publish / Make macOS-x64 release (push) Has been cancelled
CI Publish / Make windows-x64 release (push) Has been cancelled
CI Publish / release (push) Has been cancelled
2025-04-21 13:32:42 -04:00
schroda 1d5323a477 [skip ci] Add link to discord in issue templates (#1347) 2025-04-16 18:10:37 -04:00
KAAAsS f8d73819ea [Feature] Support Bangumi Tracker (#1343)
* feat: Support Bangumi Tracker

Credits: Andreas, AntsyLich, Caleb Morris, Gauthier, MCAxiaz, MajorTanya, NarwhalHorns, arkon, fei long, jmir1, mutsumi, stevenyomi

* Use Suwayomi api keys

---------

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
2025-04-12 19:34:04 -04:00
Syer10 cbe26b7291 Chmod in build script 2025-04-08 12:48:09 -04:00
Syer10 93477f60c2 Fix release name 2025-04-08 12:36:42 -04:00
Syer10 9feebbfe17 Use tar for MacOS 2025-04-08 12:34:24 -04:00
Mitchell Syer 6e365491a9 Add permissions to jspawnhelper (#1339) 2025-04-08 12:16:37 -04:00
Mitchell Syer 2e58658129 Fix MacOS builds (#1338) 2025-04-08 11:59:38 -04:00
schroda 256c564b91 Fix release version extraction from jar name (#1335)
Broke due to the changes made in 3167d8aa15
2025-04-06 15:53:04 -04:00
schroda 96b50f52ec Ensure webui "channel" is always of corresponding enum (#1334)
The "channel" was just the string from the config file, which will never equal the enum unless via case-insensitive comparison
2025-04-06 15:10:07 -04:00
schroda 3167d8aa15 Fix/startup jvm error after installation update via msi (#1229)
* Remove existing installations with msi installer

* Remove unused x86 wxs file

* Uninstall old msi versions with different upgrade code

* Progress but error 2721 happens on install

* Remove added uninstall previous version wxs stuff

* Use revision as patch number

MSI only uninstalls previous versions in case the version number changed (it only checks the first three numbers (major, minor, patch)).
Thus, to prevent each preview install to result in it getting registered as a new "app" and for it to uninstall the old versions, we have to change the version on each release.

* Deprecate "BuildConfig.REVISION"

* Remove outdated env vars

---------

Co-authored-by: Syer10 <syer10@users.noreply.github.com>
2025-04-06 15:09:56 -04:00
schroda 78fd09c728 Prevent IndexOutOfBoundsException in "libraryUpdate" subscription (#1320) 2025-03-22 22:50:25 -04:00
schroda 4c5598cedf Feature/graphql log execution exceptions (#1319)
* Log exceptions during graphql execution

Exceptions got swallowed by graphql

* Add stack trace to error in graphql response

Depending on the exceptions error message, the error in the response might be quite useless (e.g. "Stub!" error in android classes)
2025-03-22 19:35:16 -04:00
schroda c3347d94ab Feature/use GitHub issue yml format (#1314)
* Update github issue templates to yml format

* Remove "issue moderator" workflow

* Require more client info for "bug issues"

- client name
- client version
2025-03-22 19:35:08 -04:00
Mitchell Syer 7ca4aa75a8 Fix checkbox preference title nullability (#1313) 2025-03-22 19:35:02 -04:00
schroda 226fad5594 Remove "default" category from backups (#1307)
Restoring a suwayomi backup in mihon created a category named "Default"
2025-03-22 19:34:57 -04:00
schroda d0ee1ba5af Align kitsu icon with icons of other trackers (#1303)
The used icon for kitsu has a transparent background while all other tracker icons have a background color
2025-03-22 19:34:50 -04:00
schroda 439e0c8284 Emit only updater job changes instead of full status (#1302)
The update subscription emitted the full update status, which, depending on how big the status was, took forever because the graphql subscription does not support data loader batching, causing it to run into the n+1 problem
2025-03-22 19:34:43 -04:00
renovate[bot] 7d079a8728 Update kotlin monorepo to v2.1.20 (#1315)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-22 19:34:30 -04:00
renovate[bot] 945a52653e Update graphqlkotlin to v8.4.0 (#1311)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-22 19:34:23 -04:00
renovate[bot] bdafc86990 Update polyglot to v24.2.0 (#1310)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-22 19:34:13 -04:00
renovate[bot] 57d425ab9f Update dependency ch.qos.logback:logback-classic to v1.5.18 (#1309)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-22 19:34:06 -04:00
renovate[bot] 395ac8e944 Update dependency com.ibm.icu:icu4j to v77 (#1305)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-22 19:34:00 -04:00
renovate[bot] 2f801e7571 Update plugin buildconfig to v5.5.4 (#1304)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-22 19:33:53 -04:00
renovate[bot] d7636045fe Update dependency io.javalin:javalin to v6.5.0 (#1301)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-22 19:33:46 -04:00
renovate[bot] b745f10870 Update dependency org.jsoup:jsoup to v1.19.1 (#1292)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-22 19:33:38 -04:00
renovate[bot] d76849942c Update plugin buildconfig to v5.5.2 (#1299)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-08 11:31:38 -05:00
renovate[bot] b7a8a3ffe8 Update dependency io.github.oshai:kotlin-logging-jvm to v7.0.5 (#1298)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-08 11:31:26 -05:00
Shirish 95d9293fe0 Add support for opds-pse for undownloaded chapters (#1278)
* Add OPDS page streaming for undownloaded chapters

* Add [D] in chapter title prefix when isDownloaded

* Removed Chapter.isDownloaded check in query for other opds endpoints

* Add chapter progression tracking for streaming and refactor code

* dd  Unicode for chapters with 0 pages [post pageRefresh]

* Add Library Updates feed and remove redundant metadata fetching for OPDS chapters and manga

* Address PR comments & add chapter markAsRead for cbzDownload

* Address PR comment/s

* Rem. markAsRead for chapter download

* Rem. markAsRead for chapter download

---------

Co-authored-by: ShowY <showypro@gmail.com>
2025-03-08 11:31:07 -05:00
Constantin Piber 3be165a551 Initial import of Kitsu tracker (#1297)
* Initial import of Kitsu tracker

Based on Mihon 6c6ea84509cc1bd859c880bebbc69067a241b358 because its
successor 9f99f03 relies on incompatible changes

* Kitsu: Avoid stupid long/int cast
2025-03-08 11:30:59 -05:00
renovate[bot] cb498e2128 Update dependency org.slf4j:slf4j-api to v2.0.17 (#1284)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-08 11:30:35 -05:00
renovate[bot] 973f4d66e2 Update jackson monorepo to v2.18.3 (#1289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-08 11:30:02 -05:00
renovate[bot] 2c80672f6e Update plugin ktlint to v12.2.0 (#1288)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-08 11:29:55 -05:00
renovate[bot] 2599813ef1 Update dependency io.mockk:mockk to v1.13.17 (#1287)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-08 11:29:48 -05:00
renovate[bot] 86f849a185 Update dependency net.harawata:appdirs to v1.4.0 (#1286)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-08 11:29:39 -05:00
renovate[bot] 875f1f1506 Update dependency com.android.tools.build:apksig to v8.9.0 (#1285)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-08 11:29:22 -05:00
renovate[bot] e418375963 Update dependency ch.qos.logback:logback-classic to v1.5.17 (#1283)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-08 11:29:07 -05:00
renovate[bot] d528fc7f9e Update dependency gradle to v8.13 (#1282)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-08 11:28:59 -05:00
schroda 633ea97848 Feature/optimize backup import (#1270)
* Optimize restoring manga chapters

* Streamline restoring manga data

* Optimize restoring manga trackers

* Simplify passing manga category restore data

* Properly prevent mangas from getting added to default category

76595233fc never actually worked...

* Extract logic to add manga to categories from gql mutation

* Optimize restoring manga categories

* Optimize restoring categories
2025-02-16 13:00:26 -05:00
schroda 36cb899b91 Prevent chapter lastReadPage coerceIn error (#1272)
coerceIn throws an error in case the max value is less than the min value ("Cannot coerce value to an empty range: maximum <max> is less than minimum <min>")

Regression from c8bd39b4bf
2025-02-14 23:05:04 -05:00
schroda c4d849d6a3 Fix invalid chapter download state in database (#1271)
* Fix invalid chapter download state in database

Should have been added with 37f57c0c55

* Improve "fix chapter invalid download state" migrations
2025-02-14 21:02:10 -05:00
schroda c8bd39b4bf Prevent negative lastPageRead values (#1267)
Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
2025-02-14 19:06:54 -05:00
schroda 733ba16af2 Fix/backup with duplicated chapters failure (#1269)
* Extract logic to restore manga chapters into function

* Extract logic to restore manga categories into function

* Extract logic to restore manga trackers into function

* Handle duplicated chapters in backup

In case a backup contained duplicated chapters for a manga, the manga failed to restore since the ChapterTable has a unique constraint to prevent multiple chapters with the same "url" and "mangaId"
2025-02-14 19:02:02 -05:00
schroda 37f57c0c55 Prevent marking chapter as downloaded without pages (#1268)
The Downloader marked chapters without any pages incorrectly as downloaded.
2025-02-14 19:01:46 -05:00
renovate[bot] 013dbd79b4 Update dependency com.android.tools.build:apksig to v8.8.1 (#1264)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-14 19:01:37 -05:00
Zeedif f76d0b3258 Remove redundant code and add next/prev links (#1263) 2025-02-10 20:34:24 -05:00
Zeedif c2f7cdd72e Refactoring OPDS API for a more versatile root, allowing selection of manga listing by: all, source, genre, category, language, status. (#1262)
* Añadiendo algunos cambios iniciales para probar OPDS

* Add suport to OPDS v1.2

* Added support for OPDS-PSE and reorganized controllers

* Rename chapterIndex to chapterId in the API and controller, and update descriptions in OPDS

* Refactor OPDS to use formatted timestamps and proxy thumbnail URLs

* Refactor OPDS to use formatted timestamps and proxy thumbnail URLs

* Update Manga API to download chapters cbz using only chapterId and improve chapter download query

* Optimize OPDS queries

* Update Manga API to download chapters cbz using only chapterId and improve chapter download query

* Optimize OPDS queries

* Use SourceDataClass to map sources and optimize thumbnail URL retrieval

* Kotlin lint errors in ChapterDownloadHelper and Opds

* Kotlin lint errors in ChapterDownloadHelper and Opds

* Refactor OPDS API endpoints and rename OpdsController to OpdsV1Controller

* Translate OpdsV1Controller comments to English and remove unused imports

* Translate comments in OpdsAPI.kt to English

* Add SearchCriteria class and update OpdsV1Controller

* Remove spanish comments

* Refactor search handling in OpdsV1Controller and update search feed endpoint

* Fix search
2025-02-09 16:03:18 -05:00
schroda 01c37cb0ba Ignore authentication for preflight requests (#1261)
Cors preflight requests never include credentials (https://fetch.spec.whatwg.org/#cors-protocol-and-credentials), thus, they always failed due to being unauthorized
2025-02-08 11:53:32 -05:00
renovate[bot] 0dd0af1b84 Update dependency io.github.oshai:kotlin-logging-jvm to v7.0.4 (#1260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-08 11:53:21 -05:00
Zeedif 26aa684300 Add support for OPDS v1.2 to browse stored CBZ files (#1257)
* Añadiendo algunos cambios iniciales para probar OPDS

* Add suport to OPDS v1.2

* Added support for OPDS-PSE and reorganized controllers

* Rename chapterIndex to chapterId in the API and controller, and update descriptions in OPDS

* Refactor OPDS to use formatted timestamps and proxy thumbnail URLs

* Refactor OPDS to use formatted timestamps and proxy thumbnail URLs

* Update Manga API to download chapters cbz using only chapterId and improve chapter download query

* Optimize OPDS queries

* Update Manga API to download chapters cbz using only chapterId and improve chapter download query

* Optimize OPDS queries

* Use SourceDataClass to map sources and optimize thumbnail URL retrieval

* Kotlin lint errors in ChapterDownloadHelper and Opds

* Kotlin lint errors in ChapterDownloadHelper and Opds
2025-02-08 11:53:06 -05:00
renovate[bot] 9669cdfb76 Update kotlin monorepo to v2.1.10 (#1251)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-08 11:52:54 -05:00
renovate[bot] 2c5e5e283e Update dependency com.github.Suwayomi:exposed-migrations to v3.7.0 (#1248)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-08 11:52:43 -05:00
renovate[bot] 303921c6ea Update dependency gradle to v8.12.1 (#1247)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-08 11:52:31 -05:00
renovate[bot] 593291a60f Update exposed to v0.59.0 (#1228)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-08 11:52:18 -05:00
Mitchell Syer 3af8e395bd Check if file exists (#1246) 2025-01-23 09:38:52 -05:00
Mitchell Syer 0b192cfa52 Normalize Paths (#1245)
* Normalize Paths

* Formatting

* Different format
2025-01-23 09:36:10 -05:00
Mitchell Syer fb8f20f31a Fix MacOS .command file with new JRE (#1243) 2025-01-22 15:49:22 -05:00
renovate[bot] e53386cf72 Update dependency adoptium/temurin21-binaries to jdk-21.0.6+7 (#1241)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 13:33:30 -05:00
renovate[bot] b0e9b9b307 Update polyglot to v24.1.2 (#1240)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 13:33:16 -05:00
renovate[bot] 789678a45d Update dependency io.insert-koin:koin-core to v4.0.2 (#1239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 13:33:04 -05:00
renovate[bot] 4ad01d3451 Update graphqlkotlin to v8.3.0 (#1235)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 13:32:49 -05:00
renovate[bot] 6a87daa0b3 Update dependency org.bouncycastle:bcprov-jdk18on to v1.80 (#1231)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 13:32:35 -05:00
renovate[bot] aebef87076 Update dependency io.github.reactivecircus.cache4k:cache4k to v0.14.0 (#1230)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 13:32:13 -05:00
Mitchell Syer 32ff58598f Merge Service Files during build to fix GraalJS (#1242) 2025-01-22 13:32:01 -05:00
renovate[bot] 2d56cbe227 Update serialization to v1.8.0 (#1220)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-12 14:50:49 -05:00
renovate[bot] fbef5f592b Update dependency com.android.tools.build:apksig to v8.8.0 (#1227)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-12 14:47:47 -05:00
renovate[bot] 090af36f5e Update dependency com.squareup.okio:okio to v3.10.2 (#1223)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-12 14:47:37 -05:00
renovate[bot] 885f27fcf1 Update dependency net.harawata:appdirs to v1.3.0 (#1219)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-12 14:47:09 -05:00
renovate[bot] 8dc4eaf77a Update dependency io.insert-koin:koin-core to v4.0.1 (#1213)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-12 14:46:58 -05:00
renovate[bot] a55bef08a8 Update kotlinx-coroutines monorepo to v1.10.1 (#1212)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-12 14:46:32 -05:00
renovate[bot] 0dc3089739 Update dependency gradle to v8.12 (#1211)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-12 14:46:22 -05:00
renovate[bot] 789ef0d783 Update dependency io.mockk:mockk to v1.13.16 (#1210)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-12 14:46:11 -05:00
renovate[bot] 092db1106d Update dependency ch.qos.logback:logback-classic to v1.5.16 (#1209)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-12 14:46:01 -05:00
renovate[bot] 5f4b5bc570 Update dependency io.javalin:javalin to v6.4.0 (#1205)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-12 14:45:36 -05:00
Mitchell Syer 32581fcd5a [WIP] Customize JRE (#1177)
* Customize JRE

* Fix build push

* Run test

* Where is jre

* Try this

* Fix debain-all and linux-assets

* Stop ref-master for test

* Revert "Stop ref-master for test"

This reverts commit 8e34a12247087eff643676ef0ac692df4c2700ff.

* Revert "Run test"

This reverts commit dad629aaff2cf5c270b7fffeb98dfb9e3d1c93e5.
2025-01-12 14:45:22 -05:00
robo b14d28c406 new icons (#1222)
Co-authored-by: Robonau <30987265+Robonau@users.noreply.github>
2025-01-09 09:02:37 -05:00
schroda 1d1535dc55 Send dequeue download mutation response (#1218)
Response was never sent due to incorrect updates filter condition
2025-01-01 21:26:01 -05:00
schroda de942440e3 Handle missing track search results on bind (#1196)
It's possible that a manga is bound to a tracker while there is no search result.
This happens when e.g. restoring a backup which includes track bindings for which there was never a tracker search.

In that case when trying to e.g. copy the binding to another manga, the mutation would fail due to not finding a search result.
These cases can be handled by additionally checking the TrackRecordTable to get the necessary track info.
2024-12-10 19:50:16 -05:00
renovate[bot] b58fc39cf1 Update dependency io.github.oshai:kotlin-logging-jvm to v7.0.3 (#1191)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-07 23:56:51 -05:00
Mitchell Syer 2e3af25dd4 Fix usage of deprecated functions (#1192)
* Fix usage of deprecated functions

* lint

* Lint

* Another
2024-12-07 23:56:42 -05:00
schroda 1d541a30ae Feature/update to exposed v0.57.0 (#1150)
* Update to exposed-migrations v3.5.0

* Update to kotlin-logging v7.0.0

* Update to exposed v0.46.0

* Update to exposed v0.47.0

* Update to exposed v0.55.0

* Update to exposed v0.56.0

* Update to exposed v0.57.0
2024-12-07 23:49:11 -05:00
renovate[bot] f926714544 Update dependency com.pinterest.ktlint:ktlint-cli to v1.5.0 (#1182)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-07 14:28:09 -05:00
renovate[bot] f68849d3a5 Update dependency com.russhwolf:multiplatform-settings-jvm to v1.3.0 (#1176)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-07 14:27:54 -05:00
renovate[bot] 2111232f42 Update kotlin monorepo to v2.1.0 (#1170)
* Update kotlin monorepo to v2.1.0

* Fix build warnings

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Syer10 <syer10@users.noreply.github.com>
2024-12-07 14:27:47 -05:00
Mitchell Syer 088552bf56 Fix Deprecation Warnings (#1187) 2024-12-07 14:13:06 -05:00
Mitchell Syer 3eabbc9770 Manually update GraphQL-Java to fix subscription data loaders (#1186) 2024-12-07 14:12:56 -05:00
renovate[bot] 8e3b8df497 Update dependency com.android.tools.build:apksig to v8.7.3 (#1179)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-07 14:12:25 -05:00
renovate[bot] 06a5aaaa72 Update dependency com.fasterxml.jackson.core:jackson-databind to v2.18.2 (#1175)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-07 14:12:00 -05:00
renovate[bot] 97c4f14094 Update dependency org.jsoup:jsoup to v1.18.3 (#1169)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-07 14:11:43 -05:00
renovate[bot] 1fa7f18235 Update plugin ktlint to v12.1.2 (#1166)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-07 14:11:33 -05:00
renovate[bot] b309d2fd4a Update dependency gradle to v8.11.1 (#1161)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-07 14:11:22 -05:00
are-are-are 372b56bb1b add manga description (#1165) 2024-11-23 18:22:21 -05:00
schroda 3325a36cae Allow cors with credentials (#1163)
"anyHost" is not allowed in combination with "Access-Control-Allow-Credentials" (https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#sect2).
At least the default webUI always includes credentials which causes a cors policy violation
2024-11-22 20:00:25 -05:00
schroda 38673bbff4 Handle missing credentials as being invalid (#1164)
In case the credentials were missing the basic authentication was just bypassed
2024-11-22 20:00:16 -05:00
Mitchell Syer fb51834153 Fix RealUrl (#1162) 2024-11-20 22:57:42 -05:00
renovate[bot] 3a932a1e8a [skip ci] Update plugin buildconfig to v5.5.1 (#1157)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-19 17:09:04 -05:00
schroda 53c61bcb17 Serve webui on all unmatched routes (#1156)
with "/root" only "http:localhost:4567" opened the webui all other endpoints resulted in "Endpoint GET /endpoint not found"
2024-11-17 21:11:40 -05:00
schroda 6951b4b20d Remove "grapqhl log level" setting (#1155)
internal logging was removed with graphql-java v22.0
2024-11-17 21:11:26 -05:00
Mitchell Syer 746f9f1a11 [WIP] Switch to GraalJS Engine (#793)
* Switch to GraalJS Engine

* Update Polygot
2024-11-17 15:24:08 -05:00
renovate[bot] 9a7344ccbe Update dependency ch.qos.logback:logback-classic to v1.5.12 (#1151)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-17 15:07:59 -05:00
renovate[bot] ab2fb8747f Update jackson monorepo to v2.18.1 (#1148)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-17 15:04:28 -05:00
renovate[bot] 9cd8cb3d54 Update dependency io.javalin:javalin to v6 (#1152)
* Update dependency io.javalin:javalin to v6

* Simple compile fixes

* Simple compile fixes pass 2

* Add results to futures

* Setup jetty server and api routes

* Setup Cors

* Setup basic auth

* Documentation stubs

* Replace chapter mutex cache

* Fix compile

* Disable Jetty Logging

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Syer10 <syer10@users.noreply.github.com>
2024-11-17 15:00:53 -05:00
renovate[bot] ba1c2845b6 chore(deps): update plugin buildconfig to v5 (#1135)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-17 12:50:42 -05:00
renovate[bot] 065aa19e9e Update graphqlkotlin to v8 (major) (#1143)
* Update graphqlkotlin to v8

* Go back to JsonMapper

* Add context to data loaders

* Compile fixes

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Syer10 <syer10@users.noreply.github.com>
2024-11-17 12:50:33 -05:00
Mitchell Syer 4c2a05c3a6 Update Java to 21 (#1149)
* Update Java to 21

* Update Readme
2024-11-17 12:17:39 -05:00
Syer10 fd45c0740c [skip ci] Update Renovate config 2024-11-16 13:00:09 -05:00
Syer10 e44bf920fa [skip ci] Update Renovate config 2024-11-16 12:47:53 -05:00
renovate[bot] 8a327b2dff [skip ci] chore(config): migrate config renovate.json (#1144)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-16 12:41:15 -05:00
Syer10 6ece7e2596 Update Renovate config 2024-11-16 12:37:02 -05:00
renovate[bot] 2e2ce98be3 fix(deps): update dependency com.pinterest.ktlint:ktlint-cli to v1.4.1 (#1126)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-15 21:39:15 -05:00
renovate[bot] fe4c2392db chore(deps): update plugin ktlint to v12 (#1136)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-15 21:31:04 -05:00
schroda 320a0971b4 Fix/gql download subscription (#1137)
* Properly set download update type on exceptions

* Always send FINISHED download update to client for deprecated subscription

By the time the status was sent to the client, the finished download item was already removed from the queue, causing the client to never get the latest status, thus, having an outdated cache

Regression introduced with 168b76cb0c
2024-11-15 21:30:09 -05:00
renovate[bot] bfb70b6a05 fix(deps): update dependency com.ibm.icu:icu4j to v76 (#1140)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-15 21:29:28 -05:00
renovate[bot] a68af62748 fix(deps): update twelvemonkeys to v3.12.0 (#1133)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-15 21:29:11 -05:00
renovate[bot] 52bd5ce5cc fix(deps): update kotlinx-coroutines monorepo to v1.9.0 (#1132)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-15 21:28:44 -05:00
renovate[bot] b93d486348 fix(deps): update dependency org.bouncycastle:bcprov-jdk18on to v1.79 (#1127)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-15 21:28:30 -05:00
renovate[bot] d193c58e5f fix(deps): update dependency androidx.annotation:annotation to v1.9.1 (#1121)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-15 21:28:15 -05:00
renovate[bot] 6ac2a61793 Update serialization to v1.7.3 (#1119)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-14 18:09:02 -05:00
renovate[bot] a45c6f2197 Update kotlin monorepo to v2.0.21 (#1117)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-14 18:08:53 -05:00
renovate[bot] 71d639bf19 Update dependency io.mockk:mockk to v1.13.13 (#1116)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-14 18:08:41 -05:00
Mitchell Syer 9a51472726 Update Titles from the Source (#1115)
* Update Titles from the source

* Properly keep null fields
2024-11-14 18:08:31 -05:00
Mitchell Syer 0670f298cd Switch from Kodein to Koin (#1112)
* Switch from Kodein to Koin

* Ktlint
2024-11-14 18:08:19 -05:00
schroda aa1e98544b Fix/invalid server settings gql mutation request (#1092)
* Validate setting values on mutation

* Handle invalid negative setting values

* Ensure at least one source is downloading at all times

* Prevent possible IllegalArgumentException

The "serverConfig.maxSourcesInParallel" value could have changed after the if-condition
2024-11-14 18:08:07 -05:00
renovate[bot] fa4607e232 Update dependency gradle to v8.11 (#1091)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-14 18:07:55 -05:00
renovate[bot] cb46420c09 Update dependency com.android.tools.build:apksig to v8 (#1076)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-14 18:07:42 -05:00
renovate[bot] d88014fa90 Update xmlserialization (#1075)
* Update xmlserialization

* Fix XML update

* Use Jvm Serialization

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Syer10 <syer10@users.noreply.github.com>
2024-11-14 18:07:29 -05:00
schroda 168b76cb0c Feature/graphql download queue subscription send only updates (#1011)
* Emit only download changes instead of full status

The download subscription emitted the full download status, which, depending on how big the queue was, took forever because the graphql subscription does not support data loader batching, causing it to run into the n+1 problem

* Rename "DownloadManager#status" to "DownloadManager#updates"

* Add initial queue to download subscription type

Adds the current queue at the time of sending the initial message.
This field is null for all following messages after the initial one

* Optionally limit and omit download updates

To prevent the n+1 dataloader issue, the max number of updates included in the download subscription can be limited.
This way, the problem will be circumvented and instead, the latest download status should be (re-)fetched via the download status query, which does not run into this problem.

* Formatting
2024-11-14 18:07:14 -05:00
schroda f5680c6d69 Switch to Koin from Injekt (#1109)
replace "com.github.inorichi.injekt" with "com.github.null2264:injekt-koin"
2024-11-09 11:32:51 -05:00
Mitchell Syer 654a3cc7ed Use Backup.serializer() (#1088) 2024-09-16 21:18:01 -04:00
Mitchell Syer 841cdc474f Remove Broken Sources and Broken History (#1084) 2024-09-15 00:19:47 -04:00
schroda 0adbea3a43 Remove manga artist, author length limit (#1080) 2024-09-15 00:10:23 -04:00
schroda e12bada052 Use correct sync id (#1079) 2024-09-15 00:10:08 -04:00
renovate[bot] 68dbefc46f Update dependency gradle to v8.10.1 (#1072)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-14 09:15:15 -04:00
renovate[bot] 9d71e9b177 Update twelvemonkeys to v3.11.0 (#1069)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-14 09:15:06 -04:00
renovate[bot] 5b5801c2cf Update dependency com.squareup.okio:okio to v3.9.1 (#1071)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-14 09:07:53 -04:00
Antoine Aflalo df1cc2b8e9 fix(flaresolverr): fix cookie expiry for flaresolverr (#1070)
Cookie expiry is returned in seconds, the persistent cache expect it in miliseconds. Cookie is always considered as expired
2024-09-14 09:04:49 -04:00
renovate[bot] 18d399b3f7 Update settings to v1.2.0 (#1068)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-14 09:04:22 -04:00
renovate[bot] e9687fd182 Update serialization to v1.7.2 (#1067)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-14 09:04:13 -04:00
renovate[bot] 79137a074c Update plugin download to v5.6.0 (#1066)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-14 09:04:04 -04:00
renovate[bot] dae55ca386 Update graphqlkotlin to v6.8.5 (#1064)
* Update graphqlkotlin to v6.8.5

* Replace Jackson with Kotlinx.Serialization where possible

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Syer10 <syer10@users.noreply.github.com>
2024-09-14 09:03:53 -04:00
renovate[bot] b7f040d89a Update dependency org.jsoup:jsoup to v1.18.1 (#1060)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-14 09:03:44 -04:00
renovate[bot] dc69df9f4f Update dependency com.squareup.okio:okio to v3.9.0 (#1056)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 21:37:36 -04:00
Syer10 6c1fbfa63b [skip ci] Formatting 2024-09-03 21:37:18 -04:00
renovate[bot] e968a2195a [skip ci] Update dependency com.pinterest.ktlint:ktlint-cli to v1.3.1 (#1055)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 21:36:34 -04:00
Syer10 000bcea181 [skip ci] Update SystemTray 2024-09-03 21:27:02 -04:00
renovate[bot] 8edf508453 [skip ci] Update dependency org.apache.commons:commons-compress to v1.27.1 (#1058)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 21:23:45 -04:00
renovate[bot] bc9cc50130 [skip ci] Update dependency org.bouncycastle:bcprov-jdk18on to v1.78.1 (#1059)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 21:23:26 -04:00
renovate[bot] 71091d88fc [skip ci] Update dependency com.android.tools.build:apksig to v7.4.2 (#1049)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 21:17:59 -04:00
renovate[bot] 70c1d7e21f [skip ci] Update dependency io.github.config4k:config4k to v0.7.0 (#1057)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 21:17:35 -04:00
renovate[bot] c07920978e Update tachiyomiorg/issue-moderator-action action to v2 (#1032)
* Update tachiyomiorg/issue-moderator-action action to v2

* Update issue_moderator.yml

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
2024-09-02 21:47:42 -04:00
renovate[bot] 954b2919ac [skip ci] Update plugin ktlint to v11.6.1 (#1043)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-02 21:44:01 -04:00
renovate[bot] c630f731ed [skip ci] Update dependency androidx.annotation:annotation to v1.8.2 (#1046)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-02 21:43:47 -04:00
renovate[bot] aaefa7f74e [skip ci] Update coroutines to v1.8.1 (#1045)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-02 21:43:19 -04:00
renovate[bot] 2ec6b471f1 [skip ci] Update dependency gradle to v8.10 (#1047)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-02 21:43:02 -04:00
Syer10 a5c5ab68d2 [skip ci] Ktlint 2024-09-02 21:28:59 -04:00
Syer10 fb045c501a Update Kotlin to 2.0.20 2024-09-02 21:25:01 -04:00
renovate[bot] 6a6e411492 [skip ci] Update dependency net.harawata:appdirs to v1.2.2 (#1033)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-02 21:15:22 -04:00
renovate[bot] 76aac330fc [skip ci] Update dependency org.slf4j:slf4j-api to v2.0.16 (#1034)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-02 21:14:59 -04:00
renovate[bot] 07bdf31f66 [skip ci] Update okhttp monorepo to v5.0.0-alpha.14 (#1039)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-02 21:14:43 -04:00
renovate[bot] fe14928af6 [skip ci] Update rhino to v1.7.15 (#1044)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-02 21:13:38 -04:00
renovate[bot] ad0c1033a4 [skip ci] Update GitHub Artifact Actions to v4 (#1040)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-01 22:06:41 -04:00
Mitchell Syer 0c2448fb99 Update gradle build action (#1035) 2024-09-01 21:57:02 -04:00
renovate[bot] cedda145a5 Update gradle/gradle-build-action action to v3 (#1029)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-01 20:02:46 -04:00
renovate[bot] ee73187f1a [skip ci] Update gradle/wrapper-validation-action action to v3 (#1030)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-01 20:02:17 -04:00
renovate[bot] 89f91d6800 [skip ci] Update actions/checkout action to v4 (#1028)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-01 20:02:05 -04:00
renovate[bot] 6714827694 [skip ci] Update softprops/action-gh-release action to v2 (#1031)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-01 20:01:54 -04:00
renovate[bot] aad73f7d19 [skip ci] Update dependency io.mockk:mockk to v1.13.12 (#1026)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-01 19:29:54 -04:00
renovate[bot] c5985de1c3 [skip ci] Update dependency com.typesafe:config to v1.4.3 (#1025)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-01 19:29:41 -04:00
renovate[bot] 86a5b0879a Add renovate.json (#1024)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-01 19:24:12 -04:00
schroda 414972d545 Feature/update log file rotation (#1023)
* Keep up to 31 log files

On average one log file per day gets created, thus, increasing to 31 files will store log files for one month

* Decrease total log files size to 100mb

* Make log appender settings configurable
2024-08-31 18:55:26 -04:00
Antoine Aflalo 9a74ae5844 feat(comicinfo): add date fields to comic info (#1021)
* feat(comicinfo): add date fields to comic info

This will be parsed by Komga, Kavita etc ... and any other library management to also have the date of the chapter.

* refactor: improve code readability

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>

---------

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
2024-08-31 18:55:13 -04:00
Antoine Aflalo 301980ab14 fix(flaresolverr): support possible rewrite flaresolverr (#1020)
This PR:
https://github.com/FlareSolverr/FlareSolverr/pull/1300

Solve a lot of issue with not solving challenge, however, the cookie don't have path, httpOnly, secure and sameSite.

By making them optional that should work for both version of flaresolverr.
2024-08-31 18:55:02 -04:00
schroda 5dced82e5a Fix/missed automated task execution failure crashes server on startup (#1019)
* Catch automated backup task errors

* Catch automated udpate task errors

* Catch automated webui update task errors
2024-08-31 18:54:51 -04:00
schroda 9a1e4df408 Fix/server startup blocked by synchronous tasks (#1018)
* Launch missed auto backup task in background

* Launch missed auto global update task in background

* Launch missed auto webui update check task in background
2024-08-31 18:54:41 -04:00
schroda 5b08b81239 Initialize manga on add to library (#1016)
In case a manga gets added to the library which has not been initialized yet, it should be tried to initialize it.
Since it's not an error to have uninitialized manga in the library, this can be done in the background via the updater and the client receives the updated data via the update subscription.
2024-08-31 18:54:30 -04:00
schroda 7fac538ba3 Initialize uninitialized manga during global update (#1015)
They were only initialized in case the setting to refresh manga metadata during an update was enabled.
However, this should always be done for uninitialized manga, regardless of the setting.

06bfc33e72 prevents uninitialized manga from getting filtered out, however, it did not ensure to initialize the manga
2024-08-31 18:54:18 -04:00
schroda ef6be74ec2 Fix/chapter downloaded check (#1012)
* Properly check for first page in cbz files

The download check for cbz files only checked if the archive existed but didn't check for the first page

* Streamline getImageImpl of ChapterDownloadProviders

* Exclude comic info file from page list

In case the download folder did not contain any page files, only the comic info file existed, which caused the download check to incorrectly detect the first page

* Add logging to ChapterForDownload#asDownloadReady
2024-08-31 18:54:06 -04:00
schroda 9f49587245 [skip ci] Update readme (#1008)
* Move JUI and Sorayomi to inactive clients

* Update client descriptions

* Remove outdated mihon sync info

* Update feature list

* Move Docker installation info to top of list

* Move feature list higher in readme

* Rename feature list section to "Features"

* Separate inactive/abonded clients
2024-08-18 17:17:44 -04:00
schroda b7b733f351 [skip ci] Feature/update readme (#1005)
* Replace tachiyomi with mihon

* Update "syncing with mihon" readme section

* Update graphql section in CONTRIBUTING.md
2024-08-17 20:10:47 -04:00
schroda 06bfc33e72 Always update uninitialized manga in global update (#997)
Manga can be added to the library while they have not been initialized yet.
In this case, depending on the manga exclusion setting, they will never be updated automatically unless they get refreshed once manually.
2024-07-28 15:58:23 -04:00
schroda fbcd55d6c5 Add "hasDuplicatedChapters" field to gql MangaType (#995) 2024-07-28 15:58:12 -04:00
schroda 2484b5f14b Fix/downloaded chapters with lost page count (#994)
* Persist page count during chapter list update

In case a downloaded chapter gets deleted during a chapter list update, the download status was tried to be preserved.
However, in case the status could be preserved, the page count was lost and thus, the chapter now was marked as downloaded with a page count of -1.

* Mark downloaded chapters without page count as not downloaded
2024-07-28 15:58:01 -04:00
schroda 6982659658 Fix/inserting duplicated chapters into database (#991)
* Prevent adding duplicated chapters into the db

it's possible that the source returns a list containing chapters with the same url
once such duplicated chapters have been added, they aren't being removed anymore as long as there is
a chapter with the same url in the fetched chapter list, even if the duplicated chapter itself
does not exist anymore on the source

* Drop duplicated chapters from database table

* Add unique constraint to chapter table

This is to completely prevent duplicated chapters from being added to the database.
Since once a duplicated chapter has been added to the database, it does not get removed anymore as long as a chapter with the same url is included in the requested source chapter list
2024-07-28 15:57:50 -04:00
AeonLucid 9e006166a8 Add setting to use the flaresolverr response (#990) 2024-07-28 15:57:40 -04:00
AeonLucid 25a62e33a1 Fix PersistentCookieStore for domains with an underscore (#989)
* Fix PersistentCookieStore for domains with an underscore

* Missed one uri
2024-07-28 15:57:30 -04:00
AeonLucid d05ed0a56c Fix SOCKS5 authentication by setting a default Authenticator (#988)
* Fix SOCKS5 authentication by setting a default Authenticator

* Fix lint
2024-07-28 15:57:17 -04:00
schroda eaffb2755c Cleanup only created auto backup files (#984)
The automated backup cleanup just deleted every file (recursively in subfolders as well) in the set folder in case it was older than the set backup ttl.
This made it impossible to save the automated backups into a folder with different files.
2024-06-29 11:44:03 -04:00
schroda e0fcae2ae3 Handle deprecated gql sort again (#983) 2024-06-28 09:21:32 -04:00
schroda af9ad61174 Feature/gql simpilify filtering for multiple values (#960)
* Remove code duplication

* Remove unnecessary functions

* Simplify filtering for multiple values in queries

Makes it easier to filter for multiple values at ones without having to nest filters with multiple "and".

e.g.

```gql
query MyQuery {
 mangas(
  filter: {genre: {includesInsensitive: "action"}, and: {genre: {includesInsensitive: "adventure"}, and: { ... }}}
 ) {
  nodes {
   id
  }
 }
}
```

can be simplified to

```gql
query MyQuery {
 mangas(
  filter: {genre: {includesInsensitive: ["action", "adventure", ...]}}
 ) {
  nodes {
   id
  }
 }
}
```

* Add filter for matching "any" value in list

Makes it easier to filter for entries that match any value without having to nest filters with multiple "or".

e.g.

```gql
query MyQuery {
 mangas(
  filter: {genre: {includesInsensitiveAny: ["action", "adventure", ...]}}
 ) {
  nodes {
   id
  }
 }
}
```

instead of

```gql
query MyQuery {
 mangas(
  filter: {genre: {includesInsensitive: "action", or: {genre: includesInsensitive: "adventure", or: {...}}}}
 ) {
  nodes {
   id
  }
 }
}
```

* Add util function to apply "andWhere/All/Any"
2024-06-27 20:34:29 -04:00
schroda 7c54ad54fc Sort gql queries by multiple columns (#963) 2024-06-27 20:34:21 -04:00
schroda aee9f1032c Exit early in case of empty chapter id list (#980)
In case the list was empty, the batch update failed with a NoSuchElement exception
2024-06-27 20:34:12 -04:00
robo f7d0605e0a [skip ci] Remove docker commands (#972)
* [skip ci] Remove docker commands

* mention compose file
2024-06-16 15:44:01 -04:00
Syer10 f40dcafb43 Release v1.1.1
CI Publish / Validate Gradle Wrapper (push) Successful in 11s
CI Publish / Build Jar (push) Failing after 8s
CI Publish / Make debian-all release (push) Has been skipped
CI Publish / Make linux-assets release (push) Has been skipped
CI Publish / Make linux-x64 release (push) Has been skipped
CI Publish / Make macOS-arm64 release (push) Has been skipped
CI Publish / Make macOS-x64 release (push) Has been skipped
CI Publish / Make windows-x64 release (push) Has been skipped
CI Publish / Make windows-x86 release (push) Has been skipped
CI Publish / release (push) Has been skipped
2024-06-15 13:15:00 -04:00
schroda d9cb54b285 Compare webUI version with bundled webUI version (#969)
* Compare webUI version with bundled webUI version

The bundled webUI version was incorrectly compared with the minimum server version.
This worked until the latest release, because the bundled webUI version had a lower revision number than the server revision, however, with the latest release, it is now higher, resulting in no compatible webUI version to be found

* Consider bundled webUI version only for default flavor

* Add "isDefault" util function to WebUIFlavor
2024-06-15 13:05:39 -04:00
schroda f738a162d3 Support for "STABLEPREVIEW" webUI version (#970)
Makes it possible to release new stable webUI versions without having to update the mapping file.
2024-06-15 13:05:28 -04:00
Syer10 fda4cd6783 Release v1.1.0
CI Publish / Validate Gradle Wrapper (push) Successful in 13s
CI Publish / Build Jar (push) Failing after 7s
CI Publish / Make debian-all release (push) Has been skipped
CI Publish / Make linux-assets release (push) Has been skipped
CI Publish / Make linux-x64 release (push) Has been skipped
CI Publish / Make macOS-arm64 release (push) Has been skipped
CI Publish / Make macOS-x64 release (push) Has been skipped
CI Publish / Make windows-x64 release (push) Has been skipped
CI Publish / Make windows-x86 release (push) Has been skipped
CI Publish / release (push) Has been skipped
2024-06-14 21:41:17 -04:00
schroda ecd1604e25 Update metadata in source browse only if new data is not null (#962)
Browsing a source loads only a minimal representation of a manga which does not include some metadata.
This metadata is only loaded when the specific manga gets fetched.

Thus, when the extra metadata of a manga was already loaded, it got removed when browsing the source and a page response included this manga
2024-06-09 12:08:21 -04:00
Mitchell Syer 0f061900af Fix browse source (#961) 2024-06-09 11:23:54 -04:00
Rat Cornu c47f5ea85e [skip ci] doc: add NixOS installation (#959) 2024-06-08 10:48:43 -04:00
Mitchell Syer 306eb0e3c7 Update manga info when browsing if not in library (#958)
* Update manga info when browsing if not in library

* Cleanup
2024-06-06 21:17:17 -04:00
schroda e64025ded8 Correctly set name of logger (#956) 2024-06-02 20:33:32 -04:00
schroda c1fe2da636 Fix/failing thumbnail requests with http 410 (#955)
* Refresh thumbnail url on 410 error

* Refresh thumbnail url on 301 error
2024-06-02 20:33:25 -04:00
schroda ff23f58a4f Support partial mutation responses (#954)
In case e.g. a mutation was made which looked like this

myMutation {
  mutationA { ... }
  mutationB { ... }
  mutationC { ... }
}

and mutation A and B succeeded while mutation C failed, the response only included the error of C and the successful mutation data response of A and B was missing
2024-06-02 20:33:17 -04:00
schroda fc2f5ffdf9 Fix/failing track progress update for logged out trackers (#953)
* Refresh track record only when logged in

In case one tracker was logged out, the refresh failed with an unauthenticated error and caused the other trackers to not get updated

* Prevent chapter track update from failing due to failure of other tracker

* Change level of log to "info"
2024-06-01 12:22:25 -04:00
schroda 6dd9ed7fb0 Fix/prevent importing unsupported trackers from backup II (#945)
* Properly prevent importing unsupported trackers from backup

Missed the early return in case no tracker record exists in the database in 2f362abb91be875e943b1364eb86d70a4144dd6f...

* Remove incorrect non null assertion

Prevented unbinding track records of unsupported trackers
2024-05-07 09:30:45 -04:00
schroda 2f362abb91 Prevent importing unsupported tracker from backup (#944)
* Prevent importing unsupported tracker from backup

This will lead to graphql field validation errors (non null declared field is null) once the track records get used, since they will point to trackers that do not exist

* Delete track records of unsupporter trackers

* Always return all track records of manga

Was already partially changed in 7df5f1c4c4 but this occurrence was missed
2024-05-06 09:29:34 -04:00
FumoVite 96807a64cf [skip ci] Update README.md (#941)
fixed "inctive"
2024-05-05 13:24:31 -04:00
schroda 7df5f1c4c4 Feature/backup tracking (#940)
* Include tracking in validation of backup

* Always return track records

Not clear why an empty list should be returned in case no trackers are logged in

* Include tracking in backup creation

* Restore tracking from backup
2024-05-05 13:24:16 -04:00
schroda cf1ede9cf7 Update lastPageRead on chapter update (#939)
Broken with 729385588a3d8e06ec8be38865a12c47e88f6bcb...
2024-04-28 10:35:33 -04:00
schroda 729385588a Prevent greater last page read than page count (#938)
In case multiple chapters are getting updated, the last page read might be higher than the available pages of a chapter
2024-04-28 00:34:40 -04:00
schroda 668d5cf8f0 Prevent IndexOutOfBoundsException when removing duplicated chapters (#935)
In case the "new" chapters consisted only of re-uploads an out of bound exception was thrown
2024-04-27 20:33:30 -04:00
schroda 72b1b5b0f9 Exit track progress update early in case new chapter is same as current local (#937)
Prevents unnecessary requests
2024-04-27 20:33:19 -04:00
schroda fbf726c174 Use "AsyncExecutionStrategy" for mutations (#932)
Batching only works with "AsyncExecutionStrategy" and by default mutations use "SerialExecutionStrategy"
2024-04-15 17:49:33 -04:00
schroda c441eed847 Exclude duplicated chapters from auto download limit (#923)
In case the new chapters include duplicates from different scanlators, they would be included in the limit causing the auto download to potentially only download duplicated chapters while there might be more non duplicated chapters to download.

Instead, the limit should only consider unique chapters and then should include all duplicates of the chapters that should get downloaded
2024-04-06 23:07:55 -04:00
schroda e8e83ed49c Remove duplicated mangas from gql "mangas" query (#924) 2024-04-06 22:53:56 -04:00
schroda cdc21b067c Fix/recognition of already downloaded chapters (#922)
* Remove overrides of "ChapterFilesProvider::downloadImpl"

* Check final download folder for existing page on download

Downloads were changed to get downloaded to the system temp folder instead to directly into the final download folder.

This broke the check for existing pages, because now only the temp folder was checked instead of both the temp and the final download folder.

Regression introduced with 1c9a139006

* Properly check for already existing downloaded pages

The previous check was always false because the file ending of the page file is unknown and thus, missing from the created file path

* Cleanup cache download folder
2024-04-06 22:53:49 -04:00
schroda 48e19f7914 Feature/auto download of new chapters improve handling of unhandable reuploads (#921)
* Update test/server-reference file

* Properly handle re-uploaded chapters in auto download of new chapters

In case of unhandable re-uploaded chapters (different chapter numbers) they potentially would have prevented auto downloads due being considered as unread.

Additionally, they would not have been considered to get downloaded due to not having a higher chapter number than the previous latest existing chapter before the chapter list fetch.

* Add option to ignore re-uploads for auto downloads

* Extract check for manga category download inclusion

* Extract logic to get new chapter ids to download

* Simplify manga category download inclusion check

In case the DEFAULT category does not exist, someone messed with the database and it is basically corrupted
2024-04-06 22:53:36 -04:00
schroda 89dd570b30 Add mutation to fetch the latest track data from the tracker (#920)
* Add mutation to fetch the latest track data from the tracker

* Update Track.kt

---------

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
2024-03-31 13:25:58 -04:00
schroda 16474d4328 Feature/tracking gql add option to delete remote binding on tracker (#919)
* Extract unbinding track into function

* Introduce new unbind mutation

* Add option to delete track binding on track service

---------

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
2024-03-31 13:22:13 -04:00
schroda 9db612bf03 Move trigger for track progress update to client (#918)
Triggering the progress update on server side does not work because the client needs to get the mutation result, otherwise, the clients cache will get outdated
2024-03-31 13:20:37 -04:00
schroda 7d92dbc5c0 Fix/tracking progress update in case local chapter is smaller than remote (#917)
* Update lastReadChapter on bind in case it's greater than remote

* Update lastReadChapter on chapter read in case it's greater than remote

* [Logging] Improve logs
2024-03-31 13:20:27 -04:00
schroda a9efca8687 Add chapter bookmark count field to MangaType (#912) 2024-03-31 13:20:19 -04:00
schroda dbfea5d02b Update inLibraryAt timestamp when adding manga to library (#911) 2024-03-31 13:20:08 -04:00
schroda a6b05c4a27 Feature/refresh outdated thumbnail url on fetch failure (#910)
* Extract thumbnail url fresh into function

* Remove incorrect non-null assertion

According to the typing there is no guarantee that fetching a manga from the source provides a thumbnail url

* Refresh manga thumbnail url on 404 error

* Refresh manga thumbnail url on unreachable origin cloudflare errors
2024-03-31 13:19:58 -04:00
schroda 6d539d3404 Fix/update subscription clear data loader cache (#908)
* Set updater running flag to false only at the end of the update

For clearing the data loader cache properly, the update status subscription requires the update to be running.

For the last completed manga update the flag was immediately set to false which prevented the dataloader cache from getting cleared, returning outdated data for the last updated manga

* Correctly clear the "MangaForIdsDataLoader" cache

The cache keys for this dataloader are lists of manga ids.
Thus, it is not possible to clear only the cached data of the provided manga id and instead each cache entry that includes the manga id has to be cleared

* Ensure that manga dataloader caches gets cleared during global update

The "StateFlow" drops value updates in case the collector is too slow, which was the case for the "UpdateSubscription".

This caused the dataloader cache to not get properly cleared because the running state of the update was already set to false.
2024-03-31 13:19:49 -04:00
Mitchell Syer b2aff1efc9 Fix MAL after restarting the server (#903)
* Fix MAL after restarting the server

* Cleanup MAL interceptor

* Fix

* Cleanup Anilist interceptor

* Use IOException

* Make Anilist private

* Lint
2024-03-16 23:36:45 -04:00
schroda 8a20a1ef50 Add first unread chapter field to MangaType (#900) 2024-03-10 19:01:03 -04:00
schroda 33cbfa9751 Fix/electron launch error not logged (#895)
* Log "Browser::openInBrowser" errors

The error was never written to the log file.
It was only visible in the console

* Remove "printStackTrace" usage with logs
2024-03-10 19:00:54 -04:00
schroda b95a8d44d4 Always fetch thumbnail of manga from local source (#898)
The local manga thumbnail got "downloaded" to thumbnail download folder of in library manga.
Since the "thumbnail url" of a local source manga never changes, the "downloaded" manga thumbnail never got updated

Regression introduced with f2dd67d87f
2024-03-10 19:00:44 -04:00
331 changed files with 11522 additions and 5730 deletions
-44
View File
@@ -1,44 +0,0 @@
---
name: "🐞 Bug report"
title: "[Bug] <short description>"
about: "Report a bug"
labels: "bug"
---
**PLEASE READ THIS**
I acknowledge that:
- I have updated to the latest version of the app.
- I have tried the troubleshooting guide described in `README.md`
- If this is a request for adding/changing an extension it should be brought up to your extension repo.
- If this is an issue with some extension not working properly, It does work inside Tachiyomi as intended.
- I have searched the existing issues and this is a new ticket **NOT** a duplicate or related to another open issue
- I will fill out the title and the information in this template
Note that the issue will be automatically closed if you do not fill out the title or requested information.
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
---
## Device information
- Suwayomi-Server version: (Example: v1.0.0-r1438-win32)
- Server Operating System: (Example: Ubuntu 20.04)
- Server Desktop Environment: N/A or (Example: Gnome 40)
- Server JVM version: bundled with win32 or (Example: Java 8 Update 281 or OpenJDK 8u281)
- Client Operating System: <usually the same as above Server Operating System>
- Client Web Browser: (Example: Google Chrome 89.0.4389.82)
## Steps to reproduce
1. First Step
2. Second Step
### Expected behavior
Describe what should have happened. Remove this line after you are done.
### Actual behavior
Describe what happens instead. Remove this line after you are done.
## Other details
Describe additional details If necessary. Remove this line after you are done.
+144
View File
@@ -0,0 +1,144 @@
name: 🐞 Bug report
description: Report a bug in Suwayomi-Server
labels: [bug]
body:
- type: textarea
id: reproduce-steps
attributes:
label: Steps to reproduce
description: Provide an example of the issue.
placeholder: |
Example:
1. First step
2. Second step
3. Issue here
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: Explain what you should expect to happen.
placeholder: |
Example: "This should happen..."
validations:
required: true
- type: textarea
id: actual-behavior
attributes:
label: Actual behavior
description: Explain what actually happens.
placeholder: |
Example: "This happened instead..."
validations:
required: true
- type: input
id: suwayomi-server-version
attributes:
label: Suwayomi-Server version
description: You can find your Suwayomi-Server version in **More → About**.
placeholder: |
Example: "v2.0.1727"
validations:
required: true
- type: input
id: server-os
attributes:
label: Server operating system
description: The operating system on which Suwayomi-Server is running on
placeholder: |
Example: "Windows 11 Pro 24H2 | Ubuntu 24.04.2 LTS"
validations:
required: true
- type: input
id: server-desktop-environment
attributes:
label: Server Desktop Environment
description:
placeholder: |
Example: "Gnome 40"
validations:
required: false
- type: input
id: server-jvm-version
attributes:
label: Server JVM version
description: The java version used to run Suwayomi-Server
placeholder: |
Example: "openjdk 21.0.5 2024-10-15 LTS"
validations:
required: true
- type: input
id: client-name
attributes:
label: Used client name
description:
placeholder: |
Example: "Suwayomi-WebUI"
validations:
required: true
- type: input
id: client-version
attributes:
label: Client version
description:
placeholder: |
Example: "v1.2.3"
validations:
required: true
- type: input
id: client-browser
attributes:
label: Used web browser
description: The browser which is used to open Suwayomi-WebUI
placeholder: |
Example: "Chrome 134.0.6998.118 (64-Bit) | FireFox 136.0.2 (64-Bit) | Electron v35.0.2"
validations:
required: true
- type: input
id: client-os
attributes:
label: Client operating system
description: The system on which the Suwayomi-WebUI is running on
placeholder: |
Example: "Windows 11 Pro 24H2 | Ubuntu 24.04.2 LTS"
validations:
required: true
- type: textarea
id: other-details
attributes:
label: Other details
description: The more information that gets provided the better, especially via videos and images
placeholder: |
Additional details and attachments.
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
options:
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
required: true
- label: I have written a short but informative title (ideally less than ~100 characters).
required: true
- label: I have tried the troubleshooting guide described in [README.md](https://github.com/Suwayomi/Suwayomi-Server?tab=readme-ov-file#troubleshooting-and-support)
required: true
- label: I have updated to the **[latest version](https://github.com/suwayomi/suwayomi-server/releases/latest)**.
required: true
- label: I have filled out all of the requested information in this form, including specific version numbers.
required: true
- label: I understand that **Suwayomi does not have or fix any extensions**, and I **will not receive help** for any issues related to sources or extensions.
required: true
+4
View File
@@ -1 +1,5 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links:
- name: ☎️ Support
url: https://discord.gg/DDZdqZWaHA
about: Join our discord to get help for anything that is not a bug or a feature request
-29
View File
@@ -1,29 +0,0 @@
---
name: "🌟 Feature request"
title: "[Feature Request] <short description>"
about: "Suggest a feature to improve the project"
labels: "enhancement"
---
**PLEASE READ THIS**
I acknowledge that:
- I have updated to the latest version of the app.
- I have tried the troubleshooting guide described in `README.md`
- If this is a request for adding/changing an extension it should be brought up to your extension repo.
- If this is an issue with some extension not working properly, It does work in Tachiyomi application as intended.
- I have searched the existing issues and this is a new ticket **NOT** a duplicate or related to another open issue
- I will fill out the title and the information in this template
Note that the issue will be automatically closed if you do not fill out the title or requested information.
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
---
## What feature should be added to Suwayomi?
Explain What the feature is and how it should work in detail. Remove this line after you are done.
## Why/Project's Benefit/Existing Problem
Explain why this should be added. Remove this line after you are done.
@@ -0,0 +1,37 @@
name: 🌟 Feature request
description: Suggest a feature to improve Suwayomi-Server
labels: [enhancement]
body:
- type: textarea
id: feature-description
attributes:
label: Describe your suggested feature
description: How can Suwayomi-Server be improved?
placeholder: |
Example:
"It should work like this..."
validations:
required: true
- type: textarea
id: other-details
attributes:
label: Other details
placeholder: |
Additional details and attachments.
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
options:
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
required: true
- label: I have written a short but informative title (ideally less than ~100 characters).
required: true
- label: I have updated to the **[latest version](https://github.com/suwayomi/suwayomi-server/releases/latest)**.
required: true
- label: I have filled out all of the requested information in this form, including specific version numbers.
required: true
+8 -7
View File
@@ -17,7 +17,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Validate Gradle Wrapper - name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1 uses: gradle/wrapper-validation-action@v3
build: build:
name: Build pull request name: Build pull request
@@ -32,12 +32,15 @@ jobs:
path: master path: master
fetch-depth: 0 fetch-depth: 0
- name: Set up JDK 1.8 - name: Set up JDK
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
java-version: 8 java-version: 21
distribution: 'temurin' distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Copy CI gradle.properties - name: Copy CI gradle.properties
run: | run: |
cd master cd master
@@ -45,8 +48,6 @@ jobs:
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Build Jar - name: Build Jar
uses: gradle/gradle-build-action@v2 working-directory: master
with: run: ./gradlew ktlintCheck :server:shadowJar --stacktrace
build-root-directory: master
arguments: ktlintCheck :server:shadowJar --stacktrace
+64 -30
View File
@@ -15,10 +15,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Clone repo - name: Clone repo
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Validate Gradle Wrapper - name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1 uses: gradle/wrapper-validation-action@v3
build: build:
name: Build Jar name: Build Jar
@@ -32,12 +32,15 @@ jobs:
path: master path: master
fetch-depth: 0 fetch-depth: 0
- name: Set up JDK 1.8 - name: Set up JDK
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
java-version: 8 java-version: 21
distribution: 'temurin' distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Copy CI gradle.properties - name: Copy CI gradle.properties
run: | run: |
cd master cd master
@@ -45,22 +48,20 @@ jobs:
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Build Jar - name: Build Jar
uses: gradle/gradle-build-action@v2
env: env:
ProductBuildType: "Preview" ProductBuildType: "Preview"
with: working-directory: master
build-root-directory: master run: ./gradlew :server:shadowJar --stacktrace
arguments: :server:shadowJar --stacktrace
- name: Upload Jar - name: Upload Jar
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: jar name: jar
path: master/server/build/*.jar path: master/server/build/*.jar
if-no-files-found: error if-no-files-found: error
- name: Upload icons - name: Upload icons
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: icon name: icon
path: master/server/src/main/resources/icon path: master/server/src/main/resources/icon
@@ -70,12 +71,43 @@ jobs:
run: tar -cvzf scripts.tar.gz -C master/ scripts/ run: tar -cvzf scripts.tar.gz -C master/ scripts/
- name: Upload scripts.tar.gz - name: Upload scripts.tar.gz
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: scripts name: scripts
path: scripts.tar.gz path: scripts.tar.gz
if-no-files-found: error if-no-files-found: error
jlink:
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: ubuntu-latest
name: linux-x64
- os: windows-latest
name: windows-x64
- os: macos-14
name: macOS-arm64
- os: macos-13
name: macOS-x64
os: [ubuntu-latest, windows-latest, macos-14, macos-13]
steps:
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: 21
distribution: 'temurin'
- name: Package JDK
run: jlink --add-modules java.base,java.compiler,java.datatransfer,java.desktop,java.instrument,java.logging,java.management,java.naming,java.prefs,java.scripting,java.se,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,jdk.attach,jdk.crypto.ec,jdk.jdi,jdk.management,jdk.net,jdk.random,jdk.unsupported,jdk.unsupported.desktop,jdk.zipfs --output suwa --strip-debug --no-man-pages --no-header-files --compress=2
- name: Upload JRE package
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.name }}-jre
path: suwa
bundle: bundle:
strategy: strategy:
fail-fast: false fail-fast: false
@@ -87,26 +119,32 @@ jobs:
- macOS-x64 - macOS-x64
- macOS-arm64 - macOS-arm64
- windows-x64 - windows-x64
- windows-x86
name: Make ${{ matrix.os }} release name: Make ${{ matrix.os }} release
needs: build needs: [build,jlink]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Download Jar - name: Download Jar
uses: actions/download-artifact@v3 uses: actions/download-artifact@v4
with: with:
name: jar name: jar
path: server/build path: server/build
- name: Download JRE
uses: actions/download-artifact@v4
if: matrix.os != 'linux-assets' && matrix.os != 'debian-all'
with:
name: ${{ matrix.os }}-jre
path: jre
- name: Download icons - name: Download icons
uses: actions/download-artifact@v3 uses: actions/download-artifact@v4
with: with:
name: icon name: icon
path: server/src/main/resources/icon path: server/src/main/resources/icon
- name: Download scripts.tar.gz - name: Download scripts.tar.gz
uses: actions/download-artifact@v3 uses: actions/download-artifact@v4
with: with:
name: scripts name: scripts
@@ -117,7 +155,7 @@ jobs:
scripts/bundler.sh -o upload/ ${{ matrix.os }} scripts/bundler.sh -o upload/ ${{ matrix.os }}
- name: Upload ${{ matrix.os }} release - name: Upload ${{ matrix.os }} release
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: ${{ matrix.os }} name: ${{ matrix.os }}
path: upload/* path: upload/*
@@ -127,41 +165,37 @@ jobs:
needs: bundle needs: bundle
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v4
with: with:
name: jar name: jar
path: release path: release
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v4
with: with:
name: debian-all name: debian-all
path: release path: release
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v4
with: with:
name: linux-assets name: linux-assets
path: release path: release
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v4
with: with:
name: linux-x64 name: linux-x64
path: release path: release
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v4
with: with:
name: macOS-x64 name: macOS-x64
path: release path: release
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v4
with: with:
name: macOS-arm64 name: macOS-arm64
path: release path: release
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v4
with: with:
name: windows-x64 name: windows-x64
path: release path: release
- uses: actions/download-artifact@v3
with:
name: windows-x86
path: release
- name: Checkout Preview branch - name: Checkout Preview branch
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
repository: "Suwayomi/Suwayomi-Server-preview" repository: "Suwayomi/Suwayomi-Server-preview"
ref: main ref: main
@@ -193,7 +227,7 @@ jobs:
git push origin $TAG git push origin $TAG
- name: Upload Preview Release - name: Upload Preview Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v2
with: with:
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }} token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
repository: "Suwayomi/Suwayomi-Server-preview" repository: "Suwayomi/Suwayomi-Server-preview"
-48
View File
@@ -1,48 +0,0 @@
name: Issue moderator
on:
issues:
types: [opened, edited, reopened]
issue_comment:
types: [created]
jobs:
autoclose:
runs-on: ubuntu-latest
steps:
- name: Moderate issues
uses: tachiyomiorg/issue-moderator-action@v1
with:
repo-token: ${{ github.token }}
duplicate-check-enabled: true
duplicate-check-label: Source request
existing-check-enabled: true
existing-check-label: Source request
auto-close-rules: |
[
{
"type": "title",
"regex": ".*<short description>.*",
"message": "You did not fill out the description in the title"
},
{
"type": "title",
"regex": ".*(<|>)+.*",
"message": "You did not remove Angle brackets(< and >) from the title"
},
{
"type": "body",
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
"message": "The acknowledgment section was not removed"
},
{
"type": "body",
"regex": ".*(Suwayomi-Server version|Server Operating System|Server Desktop Environment|Server JVM version|Client Operating System|Client Web Browser):.*(\\(Example:|<usually).*",
"message": "The requested information was not filled out"
},
{
"type": "body",
"regex": ".*Remove this line after you are done.*",
"message": "The lines requesting to be removed were not removed."
}
]
+48 -14
View File
@@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Validate Gradle Wrapper - name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1 uses: gradle/wrapper-validation-action@v3
build: build:
name: Build Jar name: Build Jar
@@ -33,12 +33,15 @@ jobs:
path: master path: master
fetch-depth: 0 fetch-depth: 0
- name: Set up JDK 1.8 - name: Set up JDK
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
java-version: 8 java-version: 21
distribution: 'temurin' distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Copy CI gradle.properties - name: Copy CI gradle.properties
run: | run: |
cd master cd master
@@ -47,12 +50,10 @@ jobs:
~/.gradle/gradle.properties ~/.gradle/gradle.properties
- name: Build and copy webUI, Build Jar - name: Build and copy webUI, Build Jar
uses: gradle/gradle-build-action@v2
env: env:
ProductBuildType: "Stable" ProductBuildType: "Stable"
with: working-directory: master
build-root-directory: master run: ./gradlew :server:downloadWebUI :server:shadowJar --stacktrace
arguments: :server:downloadWebUI :server:shadowJar --stacktrace
- name: Upload Jar - name: Upload Jar
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -78,6 +79,37 @@ jobs:
path: scripts.tar.gz path: scripts.tar.gz
if-no-files-found: error if-no-files-found: error
jlink:
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: ubuntu-latest
name: linux-x64
- os: windows-latest
name: windows-x64
- os: macos-14
name: macOS-arm64
- os: macos-13
name: macOS-x64
os: [ubuntu-latest, windows-latest, macos-14, macos-13]
steps:
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: 21
distribution: 'temurin'
- name: Package JDK
run: jlink --add-modules java.base,java.compiler,java.datatransfer,java.desktop,java.instrument,java.logging,java.management,java.naming,java.prefs,java.scripting,java.se,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,jdk.attach,jdk.crypto.ec,jdk.jdi,jdk.management,jdk.net,jdk.random,jdk.unsupported,jdk.unsupported.desktop,jdk.zipfs --output suwa --strip-debug --no-man-pages --no-header-files --compress=2
- name: Upload JDK package
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.name }}-jre
path: suwa
bundle: bundle:
strategy: strategy:
fail-fast: false fail-fast: false
@@ -89,10 +121,9 @@ jobs:
- macOS-x64 - macOS-x64
- macOS-arm64 - macOS-arm64
- windows-x64 - windows-x64
- windows-x86
name: Make ${{ matrix.os }} release name: Make ${{ matrix.os }} release
needs: build needs: [build, jlink]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Download Jar - name: Download Jar
@@ -101,6 +132,13 @@ jobs:
name: jar name: jar
path: server/build path: server/build
- name: Download JRE
uses: actions/download-artifact@v4
if: matrix.os != 'linux-assets' && matrix.os != 'debian-all'
with:
name: ${{ matrix.os }}-jre
path: jre
- name: Download icons - name: Download icons
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
@@ -158,16 +196,12 @@ jobs:
with: with:
name: windows-x64 name: windows-x64
path: release path: release
- uses: actions/download-artifact@v4
with:
name: windows-x86
path: release
- name: Generate checksums - name: Generate checksums
run: cd release && sha256sum * > Checksums.sha256 run: cd release && sha256sum * > Checksums.sha256
- name: Release - name: Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v2
with: with:
token: ${{ secrets.DEPLOY_RELEASE_TOKEN }} token: ${{ secrets.DEPLOY_RELEASE_TOKEN }}
draft: true draft: true
+1
View File
@@ -5,6 +5,7 @@ gradle.properties
.fleet .fleet
# But we need these # But we need these
!.idea/runConfigurations !.idea/runConfigurations
.kotlin
# Ignore Gradle build output directory # Ignore Gradle build output directory
build build
+15 -3
View File
@@ -1,7 +1,19 @@
plugins { plugins {
id(libs.plugins.kotlin.jvm.get().pluginId) id(
id(libs.plugins.kotlin.serialization.get().pluginId) libs.plugins.kotlin.jvm
id(libs.plugins.ktlint.get().pluginId) .get()
.pluginId,
)
id(
libs.plugins.kotlin.serialization
.get()
.pluginId,
)
id(
libs.plugins.ktlint
.get()
.pluginId,
)
} }
dependencies { dependencies {
@@ -1,13 +0,0 @@
package xyz.nulldev.ts.config
import org.kodein.di.DI
import org.kodein.di.bind
import org.kodein.di.singleton
class ConfigKodeinModule {
fun create() =
DI.Module("ConfigManager") {
// Config module
bind<ConfigManager>() with singleton { GlobalConfigManager }
}
}
@@ -14,9 +14,9 @@ import com.typesafe.config.ConfigValue
import com.typesafe.config.ConfigValueFactory import com.typesafe.config.ConfigValueFactory
import com.typesafe.config.parser.ConfigDocument import com.typesafe.config.parser.ConfigDocument
import com.typesafe.config.parser.ConfigDocumentFactory import com.typesafe.config.parser.ConfigDocumentFactory
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import mu.KotlinLogging
import java.io.File import java.io.File
/** /**
@@ -47,11 +47,10 @@ open class ConfigManager {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun <T : ConfigModule> module(type: Class<T>): T = loadedModules[type] as T fun <T : ConfigModule> module(type: Class<T>): T = loadedModules[type] as T
private fun getUserConfig(): Config { private fun getUserConfig(): Config =
return userConfigFile.let { userConfigFile.let {
ConfigFactory.parseFile(it) ConfigFactory.parseFile(it)
} }
}
/** /**
* Load configs * Load configs
@@ -72,7 +71,8 @@ open class ConfigManager {
val userConfig = getUserConfig() val userConfig = getUserConfig()
val config = val config =
ConfigFactory.empty() ConfigFactory
.empty()
.withFallback(baseConfig) .withFallback(baseConfig)
.withFallback(userConfig) .withFallback(userConfig)
.withFallback(compatConfig) .withFallback(compatConfig)
@@ -153,11 +153,13 @@ open class ConfigManager {
} }
var newUserConfigDoc: ConfigDocument = resetUserConfig(false) var newUserConfigDoc: ConfigDocument = resetUserConfig(false)
userConfig.entrySet().filter { userConfig
serverConfig.hasPath( .entrySet()
it.key, .filter {
) serverConfig.hasPath(
}.forEach { newUserConfigDoc = newUserConfigDoc.withValue(it.key, it.value) } it.key,
)
}.forEach { newUserConfigDoc = newUserConfigDoc.withValue(it.key, it.value) }
userConfigFile.writeText(newUserConfigDoc.render()) userConfigFile.writeText(newUserConfigDoc.render())
} }
@@ -0,0 +1,9 @@
package xyz.nulldev.ts.config
import org.koin.core.module.Module
import org.koin.dsl.module
fun configManagerModule(): Module =
module {
single<ConfigManager> { GlobalConfigManager }
}
@@ -17,17 +17,25 @@ import kotlin.reflect.KProperty
* Abstract config module. * Abstract config module.
*/ */
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
abstract class ConfigModule(getConfig: () -> Config) abstract class ConfigModule(
getConfig: () -> Config,
)
/** /**
* Abstract jvm-commandline-argument-overridable config module. * Abstract jvm-commandline-argument-overridable config module.
*/ */
abstract class SystemPropertyOverridableConfigModule(getConfig: () -> Config, moduleName: String) : ConfigModule(getConfig) { abstract class SystemPropertyOverridableConfigModule(
getConfig: () -> Config,
moduleName: String,
) : ConfigModule(getConfig) {
val overridableConfig = SystemPropertyOverrideDelegate(getConfig, moduleName) val overridableConfig = SystemPropertyOverrideDelegate(getConfig, moduleName)
} }
/** Defines a config property that is overridable with jvm `-D` commandline arguments prefixed with [CONFIG_PREFIX] */ /** Defines a config property that is overridable with jvm `-D` commandline arguments prefixed with [CONFIG_PREFIX] */
class SystemPropertyOverrideDelegate(val getConfig: () -> Config, val moduleName: String) { class SystemPropertyOverrideDelegate(
val getConfig: () -> Config,
val moduleName: String,
) {
inline operator fun <R, reified T> getValue( inline operator fun <R, reified T> getValue(
thisRef: R, thisRef: R,
property: KProperty<*>, property: KProperty<*>,
@@ -15,13 +15,29 @@ import ch.qos.logback.core.rolling.RollingFileAppender
import ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy import ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy
import ch.qos.logback.core.util.FileSize import ch.qos.logback.core.util.FileSize
import com.typesafe.config.Config import com.typesafe.config.Config
import mu.KotlinLogging import io.github.oshai.kotlinlogging.DelegatingKLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
private fun fileSizeValueOfOrDefault(
fileSizeStr: String,
default: String,
): FileSize =
try {
FileSize.valueOf(fileSizeStr)
} catch (e: IllegalArgumentException) {
FileSize.valueOf(default)
}
private const val FILE_APPENDER_NAME = "SuwayomiDefaultAppender"
private fun createRollingFileAppender( private fun createRollingFileAppender(
logContext: LoggerContext, logContext: LoggerContext,
logDirPath: String, logDirPath: String,
maxFiles: Int,
maxFileSize: String,
maxTotalSize: String,
): RollingFileAppender<ILoggingEvent> { ): RollingFileAppender<ILoggingEvent> {
val logFilename = "application" val logFilename = "application"
@@ -34,7 +50,7 @@ private fun createRollingFileAppender(
val appender = val appender =
RollingFileAppender<ILoggingEvent>().apply { RollingFileAppender<ILoggingEvent>().apply {
name = "FILE" name = FILE_APPENDER_NAME
context = logContext context = logContext
encoder = logEncoder encoder = logEncoder
file = "$logDirPath/$logFilename.log" file = "$logDirPath/$logFilename.log"
@@ -45,9 +61,9 @@ private fun createRollingFileAppender(
context = logContext context = logContext
setParent(appender) setParent(appender)
fileNamePattern = "$logDirPath/${logFilename}_%d{yyyy-MM-dd}_%i.log.gz" fileNamePattern = "$logDirPath/${logFilename}_%d{yyyy-MM-dd}_%i.log.gz"
setMaxFileSize(FileSize.valueOf("10mb")) maxHistory = maxFiles.coerceAtLeast(0)
maxHistory = 14 setMaxFileSize(fileSizeValueOfOrDefault(maxFileSize, "10mb"))
setTotalSizeCap(FileSize.valueOf("1gb")) setTotalSizeCap(fileSizeValueOfOrDefault(maxTotalSize, "100mb"))
start() start()
} }
@@ -57,25 +73,52 @@ private fun createRollingFileAppender(
return appender return appender
} }
private fun getBaseLogger(): ch.qos.logback.classic.Logger { private fun getBaseLogger(): ch.qos.logback.classic.Logger =
return (KotlinLogging.logger(Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger) ((KotlinLogging.logger(Logger.ROOT_LOGGER_NAME) as DelegatingKLogger<*>).underlyingLogger as ch.qos.logback.classic.Logger)
}
private fun getLogger(name: String): ch.qos.logback.classic.Logger { private fun getLogger(name: String): ch.qos.logback.classic.Logger {
val context = LoggerFactory.getILoggerFactory() as LoggerContext val context = LoggerFactory.getILoggerFactory() as LoggerContext
return context.getLogger(name) return context.getLogger(name)
} }
fun initLoggerConfig(appRootPath: String) { fun initLoggerConfig(
appRootPath: String,
maxFiles: Int,
maxFileSize: String,
maxTotalSize: String,
) {
val context = LoggerFactory.getILoggerFactory() as LoggerContext val context = LoggerFactory.getILoggerFactory() as LoggerContext
val logger = getBaseLogger() val logger = getBaseLogger()
// logback logs to the console by default (at least when adding a console appender logs in the console are duplicated) // logback logs to the console by default (at least when adding a console appender logs in the console are duplicated)
logger.addAppender(createRollingFileAppender(context, "$appRootPath/logs")) logger.addAppender(createRollingFileAppender(context, "$appRootPath/logs", maxFiles, maxFileSize, maxTotalSize))
// set "kotlin exposed" log level // set "kotlin exposed" log level
setLogLevelFor("Exposed", Level.ERROR) setLogLevelFor("Exposed", Level.ERROR)
} }
fun updateFileAppender(
maxFiles: Int,
maxFileSize: String,
maxTotalSize: String,
) {
val logger = getBaseLogger()
val appender = logger.getAppender(FILE_APPENDER_NAME) as RollingFileAppender<*>? ?: return
val rollingPolicy = appender.rollingPolicy as SizeAndTimeBasedRollingPolicy<*>
rollingPolicy.apply {
maxHistory = maxFiles
setMaxFileSize(FileSize.valueOf(maxFileSize))
setTotalSizeCap(FileSize.valueOf(maxTotalSize))
rollingPolicy.stop()
appender.stop()
rollingPolicy.start()
appender.start()
}
}
const val BASE_LOGGER_NAME = "_BaseLogger" const val BASE_LOGGER_NAME = "_BaseLogger"
fun setLogLevelFor( fun setLogLevelFor(
+17 -5
View File
@@ -1,7 +1,19 @@
plugins { plugins {
id(libs.plugins.kotlin.jvm.get().pluginId) id(
id(libs.plugins.kotlin.serialization.get().pluginId) libs.plugins.kotlin.jvm
id(libs.plugins.ktlint.get().pluginId) .get()
.pluginId,
)
id(
libs.plugins.kotlin.serialization
.get()
.pluginId,
)
id(
libs.plugins.ktlint
.get()
.pluginId,
)
} }
dependencies { dependencies {
@@ -24,8 +36,8 @@ dependencies {
// AndroidX annotations // AndroidX annotations
compileOnly(libs.android.annotations) compileOnly(libs.android.annotations)
// substitute for duktape-android // substitute for duktape-android/quickjs
implementation(libs.bundles.rhino) implementation(libs.bundles.polyglot)
// Kotlin wrapper around Java Preferences, makes certain things easier // Kotlin wrapper around Java Preferences, makes certain things easier
implementation(libs.bundles.settings) implementation(libs.bundles.settings)
@@ -25,7 +25,7 @@ import android.os.IBinder;
import android.util.Log; import android.util.Log;
import kotlin.NotImplementedError; import kotlin.NotImplementedError;
import xyz.nulldev.androidcompat.service.ServiceSupport; import xyz.nulldev.androidcompat.service.ServiceSupport;
import xyz.nulldev.androidcompat.util.KodeinGlobalHelper; import xyz.nulldev.androidcompat.util.KoinGlobalHelper;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.io.PrintWriter; import java.io.PrintWriter;
@@ -299,7 +299,7 @@ import java.lang.annotation.RetentionPolicy;
*/ */
public abstract class Service extends ContextWrapper implements ComponentCallbacks2 { public abstract class Service extends ContextWrapper implements ComponentCallbacks2 {
private static final ServiceSupport serviceSupport = KodeinGlobalHelper.instance(ServiceSupport.class); private static final ServiceSupport serviceSupport = KoinGlobalHelper.instance(ServiceSupport.class);
private static final String TAG = "Service"; private static final String TAG = "Service";
/** /**
@@ -328,7 +328,7 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac
public Service() { public Service() {
//==================[THIS LINE MODIFIED FROM ANDROID SOURCE!]================== //==================[THIS LINE MODIFIED FROM ANDROID SOURCE!]==================
//Service must be initialized with a base context! //Service must be initialized with a base context!
super(KodeinGlobalHelper.instance(Context.class)); super(KoinGlobalHelper.instance(Context.class));
} }
/** Return the application that owns this service. */ /** Return the application that owns this service. */
public final Application getApplication() { public final Application getApplication() {
@@ -1,7 +1,7 @@
package android.os; package android.os;
import xyz.nulldev.androidcompat.io.AndroidFiles; import xyz.nulldev.androidcompat.io.AndroidFiles;
import xyz.nulldev.androidcompat.util.KodeinGlobalHelper; import xyz.nulldev.androidcompat.util.KoinGlobalHelper;
import java.io.File; import java.io.File;
@@ -9,7 +9,7 @@ import java.io.File;
* Android compatibility layer for files * Android compatibility layer for files
*/ */
public class Environment { public class Environment {
private static AndroidFiles androidFiles = KodeinGlobalHelper.instance(AndroidFiles.class); private static AndroidFiles androidFiles = KoinGlobalHelper.instance(AndroidFiles.class);
public static String DIRECTORY_ALARMS = getHomeDirectory("Alarms").getAbsolutePath(); public static String DIRECTORY_ALARMS = getHomeDirectory("Alarms").getAbsolutePath();
public static String DIRECTORY_DCIM = getHomeDirectory("DCIM").getAbsolutePath(); public static String DIRECTORY_DCIM = getHomeDirectory("DCIM").getAbsolutePath();
@@ -1,69 +1,99 @@
package app.cash.quickjs; package app.cash.quickjs;
import org.mozilla.javascript.ConsString; import org.graalvm.polyglot.*;
import org.mozilla.javascript.NativeArray;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.io.Closeable; import java.io.Closeable;
import java.math.BigInteger;
import java.util.Arrays;
public final class QuickJs implements Closeable { public final class QuickJs implements Closeable {
private ScriptEngine engine; private Context context;
public static QuickJs create() { public static QuickJs create() {
return new QuickJs(new ScriptEngineManager()); return new QuickJs();
} }
public QuickJs(ScriptEngineManager manager) { public QuickJs() {
this.engine = manager.getEngineByName("rhino"); this.context = Context
.newBuilder("js")
.allowHostAccess(HostAccess.ALL)
.allowPolyglotAccess(PolyglotAccess.NONE)
.allowHostClassLoading(false)
.build();
context.enter();
} }
public Object evaluate(String script, String fileName) { public Object evaluate(String script, String ignoredFileName) {
return this.evaluate(script); return this.evaluate(script);
} }
public Object evaluate(String script) { public Object evaluate(String script) {
try { try {
Object value = engine.eval(script); Value value = context.eval("js", script);
return translateType(value); return translateType(value);
} catch (Exception exception) { } catch (Exception exception) {
throw new QuickJsException(exception.getMessage(), exception); throw new QuickJsException(exception.getMessage(), exception);
} }
} }
private Object translateType(Object obj) { private Object translateType(Value obj) {
if (obj instanceof NativeArray) { if (obj.isBoolean()) {
NativeArray array = (NativeArray) obj; return obj.asBoolean();
long length = array.getLength(); } else if (obj.hasArrayElements()) {
Object[] objects = new Object[(int) length]; if (obj.getArraySize() == 0) {
for (int i = 0; i < (int) length; i++) { return new int[0];
objects[i] = translateType(array.get(i)); } else {
Value element = obj.getArrayElement(0);
if (element.isBoolean()) {
return obj.as(boolean[].class);
} else if (element.isNumber()) {
if (element.fitsInInt()) {
return obj.as(int[].class);
} else if (element.fitsInBigInteger()) {
return Arrays.stream(obj.as(BigInteger[].class)).map(BigInteger::longValue).toArray();
} else {
return obj.as(double[].class);
}
} else if (element.isHostObject()) {
return obj.as(Object[].class);
} else if (element.isString()) {
return obj.as(String[].class);
}
} }
return objects; } else if (obj.isNumber()) {
} if (obj.fitsInInt()) {
if (obj instanceof ConsString) { return obj.asInt();
ConsString consString = (ConsString) obj; } else if (obj.fitsInBigInteger()) {
return consString.toString(); return obj.asBigInteger().longValue();
} } else {
if (obj instanceof Long) { return obj.asDouble();
Long value = (Long) obj; }
return value.intValue(); } else if (obj.isHostObject()) {
return obj.asHostObject();
} else if (obj.isString()) {
return obj.asString();
} }
return obj; return obj;
} }
public byte[] compile(String sourceCode, String fileName) { public byte[] compile(String sourceCode, String ignoredFileName) {
return sourceCode.getBytes(); return sourceCode.getBytes();
} }
public Object execute(byte[] bytecode) { public Object execute(byte[] bytecode) {
return this.evaluate(new String(bytecode)); return this.evaluate(new String(bytecode));
} }
public <T> void set(String name, Class<T> ignoredType, T object) {
context.getBindings("js").putMember(name, object);
}
@Override @Override
public void close() { public void close() {
this.engine = null; if (this.context != null) {
this.context.leave();
this.context.close();
this.context = null;
}
} }
} }
@@ -17,7 +17,7 @@ package dalvik.system;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import xyz.nulldev.androidcompat.pm.PackageController; import xyz.nulldev.androidcompat.pm.PackageController;
import xyz.nulldev.androidcompat.util.KodeinGlobalHelper; import xyz.nulldev.androidcompat.util.KoinGlobalHelper;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -33,7 +33,7 @@ import java.util.Enumeration;
* {@link ClassLoader} implementations. * {@link ClassLoader} implementations.
*/ */
public class BaseDexClassLoader extends ClassLoader { public class BaseDexClassLoader extends ClassLoader {
private PackageController controller = KodeinGlobalHelper.instance(PackageController.class); private PackageController controller = KoinGlobalHelper.instance(PackageController.class);
private final URLClassLoader realClassloader; private final URLClassLoader realClassloader;
@@ -1,13 +1,11 @@
package xyz.nulldev.androidcompat package xyz.nulldev.androidcompat
import android.app.Application import android.app.Application
import org.kodein.di.DI import org.koin.mp.KoinPlatformTools
import org.kodein.di.conf.global
import org.kodein.di.instance
import xyz.nulldev.androidcompat.androidimpl.CustomContext import xyz.nulldev.androidcompat.androidimpl.CustomContext
class AndroidCompat { class AndroidCompat {
val context: CustomContext by DI.global.instance() val context: CustomContext by KoinPlatformTools.defaultContext().get().inject()
fun startApp(application: Application) { fun startApp(application: Application) {
application.attach(context) application.attach(context)
@@ -1,7 +1,5 @@
package xyz.nulldev.androidcompat package xyz.nulldev.androidcompat
import org.kodein.di.DI
import org.kodein.di.conf.global
import xyz.nulldev.androidcompat.config.ApplicationInfoConfigModule import xyz.nulldev.androidcompat.config.ApplicationInfoConfigModule
import xyz.nulldev.androidcompat.config.FilesConfigModule import xyz.nulldev.androidcompat.config.FilesConfigModule
import xyz.nulldev.androidcompat.config.SystemConfigModule import xyz.nulldev.androidcompat.config.SystemConfigModule
@@ -12,8 +10,6 @@ import xyz.nulldev.ts.config.GlobalConfigManager
*/ */
class AndroidCompatInitializer { class AndroidCompatInitializer {
fun init() { fun init() {
DI.global.addImport(AndroidCompatModule().create())
// Register config modules // Register config modules
GlobalConfigManager.registerModules( GlobalConfigManager.registerModules(
FilesConfigModule.register(GlobalConfigManager.config), FilesConfigModule.register(GlobalConfigManager.config),
@@ -1,11 +1,8 @@
package xyz.nulldev.androidcompat package xyz.nulldev.androidcompat
import android.content.Context import android.content.Context
import org.kodein.di.DI import org.koin.core.module.Module
import org.kodein.di.bind import org.koin.dsl.module
import org.kodein.di.conf.global
import org.kodein.di.instance
import org.kodein.di.singleton
import xyz.nulldev.androidcompat.androidimpl.CustomContext import xyz.nulldev.androidcompat.androidimpl.CustomContext
import xyz.nulldev.androidcompat.androidimpl.FakePackageManager import xyz.nulldev.androidcompat.androidimpl.FakePackageManager
import xyz.nulldev.androidcompat.info.ApplicationInfoImpl import xyz.nulldev.androidcompat.info.ApplicationInfoImpl
@@ -17,25 +14,19 @@ import xyz.nulldev.androidcompat.service.ServiceSupport
* AndroidCompatModule * AndroidCompatModule
*/ */
class AndroidCompatModule { fun androidCompatModule(): Module =
fun create() = module {
DI.Module("AndroidCompat") { single { AndroidFiles() }
bind<AndroidFiles>() with singleton { AndroidFiles() }
bind<ApplicationInfoImpl>() with singleton { ApplicationInfoImpl() } single { ApplicationInfoImpl(get()) }
bind<ServiceSupport>() with singleton { ServiceSupport() } single { ServiceSupport() }
bind<FakePackageManager>() with singleton { FakePackageManager() } single { FakePackageManager() }
bind<PackageController>() with singleton { PackageController() } single { PackageController() }
// Context single { CustomContext() }
bind<CustomContext>() with singleton { CustomContext() }
bind<Context>() with single<Context> { get<CustomContext>() }
singleton { }
val context: Context by DI.global.instance<CustomContext>()
context
}
}
}
@@ -32,15 +32,14 @@ import android.os.*;
import android.view.Display; import android.view.Display;
import android.view.DisplayAdjustments; import android.view.DisplayAdjustments;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.koin.core.Koin;
import org.kodein.di.*;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import xyz.nulldev.androidcompat.info.ApplicationInfoImpl; import xyz.nulldev.androidcompat.info.ApplicationInfoImpl;
import xyz.nulldev.androidcompat.io.AndroidFiles; import xyz.nulldev.androidcompat.io.AndroidFiles;
import xyz.nulldev.androidcompat.io.sharedprefs.JavaSharedPreferences; import xyz.nulldev.androidcompat.io.sharedprefs.JavaSharedPreferences;
import xyz.nulldev.androidcompat.service.ServiceSupport; import xyz.nulldev.androidcompat.service.ServiceSupport;
import xyz.nulldev.androidcompat.util.KodeinGlobalHelper; import xyz.nulldev.androidcompat.util.KoinGlobalHelper;
import java.io.*; import java.io.*;
import java.util.HashMap; import java.util.HashMap;
@@ -51,26 +50,25 @@ import java.util.Map;
* Custom context implementation. * Custom context implementation.
* *
*/ */
public class CustomContext extends Context implements DIAware { public class CustomContext extends Context {
private final DI kodein; private final Koin koin;
public CustomContext() { public CustomContext() {
this(KodeinGlobalHelper.kodein()); this(KoinGlobalHelper.koin());
} }
public CustomContext(DI kodein) { public CustomContext(Koin koin) {
this.kodein = kodein; this.koin = koin;
//Init configs //Init configs
androidFiles = KodeinGlobalHelper.instance(AndroidFiles.class, getDi()); androidFiles = KoinGlobalHelper.instance(AndroidFiles.class, getDi());
applicationInfo = KodeinGlobalHelper.instance(ApplicationInfoImpl.class, getDi()); applicationInfo = KoinGlobalHelper.instance(ApplicationInfoImpl.class, getDi());
serviceSupport = KodeinGlobalHelper.instance(ServiceSupport.class, getDi()); serviceSupport = KoinGlobalHelper.instance(ServiceSupport.class, getDi());
fakePackageManager = KodeinGlobalHelper.instance(FakePackageManager.class, getDi()); fakePackageManager = KoinGlobalHelper.instance(FakePackageManager.class, getDi());
} }
@NotNull @NotNull
@Override public Koin getDi() {
public DI getDi() { return koin;
return kodein;
} }
private AndroidFiles androidFiles; private AndroidFiles androidFiles;
@@ -719,17 +717,5 @@ public class CustomContext extends Context implements DIAware {
public boolean isCredentialProtectedStorage() { public boolean isCredentialProtectedStorage() {
return false; return false;
} }
@NotNull
@Override
public DIContext<?> getDiContext() {
return getDi().getDiContext();
}
@Nullable
@Override
public DITrigger getDiTrigger() {
return null;
}
} }
@@ -16,14 +16,14 @@ import android.os.UserHandle;
import kotlin.NotImplementedError; import kotlin.NotImplementedError;
import xyz.nulldev.androidcompat.pm.InstalledPackage; import xyz.nulldev.androidcompat.pm.InstalledPackage;
import xyz.nulldev.androidcompat.pm.PackageController; import xyz.nulldev.androidcompat.pm.PackageController;
import xyz.nulldev.androidcompat.util.KodeinGlobalHelper; import xyz.nulldev.androidcompat.util.KoinGlobalHelper;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class FakePackageManager extends PackageManager { public class FakePackageManager extends PackageManager {
private PackageController controller = KodeinGlobalHelper.instance(PackageController.class); private PackageController controller = KoinGlobalHelper.instance(PackageController.class);
@Override @Override
public PackageInfo getPackageInfo(String packageName, int flags) throws NameNotFoundException { public PackageInfo getPackageInfo(String packageName, int flags) throws NameNotFoundException {
@@ -8,7 +8,9 @@ import xyz.nulldev.ts.config.ConfigModule
* Application info config. * Application info config.
*/ */
class ApplicationInfoConfigModule(getConfig: () -> Config) : ConfigModule(getConfig) { class ApplicationInfoConfigModule(
getConfig: () -> Config,
) : ConfigModule(getConfig) {
val packageName: String by getConfig() val packageName: String by getConfig()
val debug: Boolean by getConfig() val debug: Boolean by getConfig()
@@ -8,7 +8,9 @@ import xyz.nulldev.ts.config.ConfigModule
* Files configuration modules. Specifies where to store the Android files. * Files configuration modules. Specifies where to store the Android files.
*/ */
class FilesConfigModule(getConfig: () -> Config) : ConfigModule(getConfig) { class FilesConfigModule(
getConfig: () -> Config,
) : ConfigModule(getConfig) {
val dataDir: String by getConfig() val dataDir: String by getConfig()
val filesDir: String by getConfig() val filesDir: String by getConfig()
val noBackupFilesDir: String by getConfig() val noBackupFilesDir: String by getConfig()
@@ -4,7 +4,9 @@ import com.typesafe.config.Config
import io.github.config4k.getValue import io.github.config4k.getValue
import xyz.nulldev.ts.config.ConfigModule import xyz.nulldev.ts.config.ConfigModule
class SystemConfigModule(val getConfig: () -> Config) : ConfigModule(getConfig) { class SystemConfigModule(
val getConfig: () -> Config,
) : ConfigModule(getConfig) {
val isDebuggable: Boolean by getConfig() val isDebuggable: Boolean by getConfig()
val propertyPrefix = "properties." val propertyPrefix = "properties."
@@ -19,7 +19,9 @@ import java.sql.Timestamp
import java.util.Calendar import java.util.Calendar
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent { class ScrollableResultSet(
val parent: ResultSet,
) : ResultSet by parent {
private val cachedContent = mutableListOf<ResultSetEntry>() private val cachedContent = mutableListOf<ResultSetEntry>()
private val columnCache = mutableMapOf<String, Int>() private val columnCache = mutableMapOf<String, Int>()
private var lastReturnWasNull = false private var lastReturnWasNull = false
@@ -29,9 +31,10 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
val parentMetadata = parent.metaData val parentMetadata = parent.metaData
val columnCount = parentMetadata.columnCount val columnCount = parentMetadata.columnCount
val columnLabels = val columnLabels =
(1..columnCount).map { (1..columnCount)
parentMetadata.getColumnLabel(it) .map {
}.toTypedArray() parentMetadata.getColumnLabel(it)
}.toTypedArray()
init { init {
val columnCount = columnCount val columnCount = columnCount
@@ -45,20 +48,17 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
while (parent.next()) { while (parent.next()) {
cachedContent += cachedContent +=
ResultSetEntry().apply { ResultSetEntry().apply {
for (i in 1..columnCount) for (i in 1..columnCount) {
data += parent.getObject(i) data += parent.getObject(i)
}
} }
resultSetLength++ resultSetLength++
} }
} }
private fun notImplemented(): Nothing { private fun notImplemented(): Nothing = throw UnsupportedOperationException("This class currently does not support this operation!")
throw UnsupportedOperationException("This class currently does not support this operation!")
}
private fun cursorValid(): Boolean { private fun cursorValid(): Boolean = isAfterLast || isBeforeFirst
return isAfterLast || isBeforeFirst
}
private fun internalMove(row: Int) { private fun internalMove(row: Int) {
if (cursor < 0) { if (cursor < 0) {
@@ -76,22 +76,16 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
return obj return obj
} }
private fun obj(column: String?): Any? { private fun obj(column: String?): Any? = obj(cachedFindColumn(column))
return obj(cachedFindColumn(column))
}
private fun cachedFindColumn(column: String?) = private fun cachedFindColumn(column: String?) =
columnCache.getOrPut(column!!, { columnCache.getOrPut(column!!, {
findColumn(column) findColumn(column)
}) })
override fun getNClob(columnIndex: Int): NClob { override fun getNClob(columnIndex: Int): NClob = obj(columnIndex) as NClob
return obj(columnIndex) as NClob
}
override fun getNClob(columnLabel: String?): NClob { override fun getNClob(columnLabel: String?): NClob = obj(columnLabel) as NClob
return obj(columnLabel) as NClob
}
override fun updateNString( override fun updateNString(
columnIndex: Int, columnIndex: Int,
@@ -260,17 +254,11 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
notImplemented() notImplemented()
} }
override fun getBoolean(columnIndex: Int): Boolean { override fun getBoolean(columnIndex: Int): Boolean = obj(columnIndex) as Boolean
return obj(columnIndex) as Boolean
}
override fun getBoolean(columnLabel: String?): Boolean { override fun getBoolean(columnLabel: String?): Boolean = obj(columnLabel) as Boolean
return obj(columnLabel) as Boolean
}
override fun isFirst(): Boolean { override fun isFirst(): Boolean = cursor - 1 < resultSetLength
return cursor - 1 < resultSetLength
}
override fun getBigDecimal( override fun getBigDecimal(
columnIndex: Int, columnIndex: Int,
@@ -288,13 +276,9 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
notImplemented() notImplemented()
} }
override fun getBigDecimal(columnIndex: Int): BigDecimal { override fun getBigDecimal(columnIndex: Int): BigDecimal = obj(columnIndex) as BigDecimal
return obj(columnIndex) as BigDecimal
}
override fun getBigDecimal(columnLabel: String?): BigDecimal { override fun getBigDecimal(columnLabel: String?): BigDecimal = obj(columnLabel) as BigDecimal
return obj(columnLabel) as BigDecimal
}
override fun updateBytes( override fun updateBytes(
columnIndex: Int, columnIndex: Int,
@@ -310,9 +294,7 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
notImplemented() notImplemented()
} }
override fun isLast(): Boolean { override fun isLast(): Boolean = cursor == resultSetLength
return cursor == resultSetLength
}
override fun insertRow() { override fun insertRow() {
notImplemented() notImplemented()
@@ -351,9 +333,7 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
return cursorValid() return cursorValid()
} }
override fun isAfterLast(): Boolean { override fun isAfterLast(): Boolean = cursor > resultSetLength
return cursor > resultSetLength
}
override fun relative(rows: Int): Boolean { override fun relative(rows: Int): Boolean {
internalMove(cursor + rows) internalMove(cursor + rows)
@@ -365,8 +345,9 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
internalMove(row) internalMove(row)
} else { } else {
last() last()
for (i in 1..row) for (i in 1..row) {
previous() previous()
}
} }
return cursorValid() return cursorValid()
} }
@@ -394,19 +375,13 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
return cursorValid() return cursorValid()
} }
override fun getFloat(columnIndex: Int): Float { override fun getFloat(columnIndex: Int): Float = obj(columnIndex) as Float
return obj(columnIndex) as Float
}
override fun getFloat(columnLabel: String?): Float { override fun getFloat(columnLabel: String?): Float = obj(columnLabel) as Float
return obj(columnLabel) as Float
}
override fun wasNull() = lastReturnWasNull override fun wasNull() = lastReturnWasNull
override fun getRow(): Int { override fun getRow(): Int = cursor
return cursor
}
override fun first(): Boolean { override fun first(): Boolean {
internalMove(1) internalMove(1)
@@ -459,13 +434,9 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
notImplemented() notImplemented()
} }
override fun getURL(columnIndex: Int): URL { override fun getURL(columnIndex: Int): URL = obj(columnIndex) as URL
return obj(columnIndex) as URL
}
override fun getURL(columnLabel: String?): URL { override fun getURL(columnLabel: String?): URL = obj(columnLabel) as URL
return obj(columnLabel) as URL
}
override fun updateShort( override fun updateShort(
columnIndex: Int, columnIndex: Int,
@@ -643,21 +614,13 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
notImplemented() notImplemented()
} }
override fun getByte(columnIndex: Int): Byte { override fun getByte(columnIndex: Int): Byte = obj(columnIndex) as Byte
return obj(columnIndex) as Byte
}
override fun getByte(columnLabel: String?): Byte { override fun getByte(columnLabel: String?): Byte = obj(columnLabel) as Byte
return obj(columnLabel) as Byte
}
override fun getString(columnIndex: Int): String? { override fun getString(columnIndex: Int): String? = obj(columnIndex) as String?
return obj(columnIndex) as String?
}
override fun getString(columnLabel: String?): String? { override fun getString(columnLabel: String?): String? = obj(columnLabel) as String?
return obj(columnLabel) as String?
}
override fun updateSQLXML( override fun updateSQLXML(
columnIndex: Int, columnIndex: Int,
@@ -687,13 +650,9 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
notImplemented() notImplemented()
} }
override fun getObject(columnIndex: Int): Any? { override fun getObject(columnIndex: Int): Any? = obj(columnIndex)
return obj(columnIndex)
}
override fun getObject(columnLabel: String?): Any? { override fun getObject(columnLabel: String?): Any? = obj(columnLabel)
return obj(columnLabel)
}
override fun getObject( override fun getObject(
columnIndex: Int, columnIndex: Int,
@@ -714,16 +673,12 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
override fun <T : Any?> getObject( override fun <T : Any?> getObject(
columnIndex: Int, columnIndex: Int,
type: Class<T>?, type: Class<T>?,
): T { ): T = obj(columnIndex) as T
return obj(columnIndex) as T
}
override fun <T : Any?> getObject( override fun <T : Any?> getObject(
columnLabel: String?, columnLabel: String?,
type: Class<T>?, type: Class<T>?,
): T { ): T = obj(columnLabel) as T
return obj(columnLabel) as T
}
override fun previous(): Boolean { override fun previous(): Boolean {
internalMove(cursor - 1) internalMove(cursor - 1)
@@ -756,13 +711,9 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
} }
override fun getLong(columnIndex: Int): Long { override fun getLong(columnIndex: Int): Long = castToLong(obj(columnIndex))
return castToLong(obj(columnIndex))
}
override fun getLong(columnLabel: String?): Long { override fun getLong(columnLabel: String?): Long = castToLong(obj(columnLabel))
return castToLong(obj(columnLabel))
}
override fun getClob(columnIndex: Int): Clob { override fun getClob(columnIndex: Int): Clob {
// TODO Maybe? // TODO Maybe?
@@ -840,13 +791,9 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
notImplemented() notImplemented()
} }
override fun getNString(columnIndex: Int): String { override fun getNString(columnIndex: Int): String = obj(columnIndex) as String
return obj(columnIndex) as String
}
override fun getNString(columnLabel: String?): String { override fun getNString(columnLabel: String?): String = obj(columnLabel) as String
return obj(columnLabel) as String
}
override fun getArray(columnIndex: Int): Array { override fun getArray(columnIndex: Int): Array {
// TODO Maybe? // TODO Maybe?
@@ -880,17 +827,11 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
notImplemented() notImplemented()
} }
override fun getCharacterStream(columnIndex: Int): Reader { override fun getCharacterStream(columnIndex: Int): Reader = getNCharacterStream(columnIndex)
return getNCharacterStream(columnIndex)
}
override fun getCharacterStream(columnLabel: String?): Reader { override fun getCharacterStream(columnLabel: String?): Reader = getNCharacterStream(columnLabel)
return getNCharacterStream(columnLabel)
}
override fun isBeforeFirst(): Boolean { override fun isBeforeFirst(): Boolean = cursor - 1 < resultSetLength
return cursor - 1 < resultSetLength
}
override fun updateBoolean( override fun updateBoolean(
columnIndex: Int, columnIndex: Int,
@@ -926,21 +867,13 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
notImplemented() notImplemented()
} }
override fun getShort(columnIndex: Int): Short { override fun getShort(columnIndex: Int): Short = obj(columnIndex) as Short
return obj(columnIndex) as Short
}
override fun getShort(columnLabel: String?): Short { override fun getShort(columnLabel: String?): Short = obj(columnLabel) as Short
return obj(columnLabel) as Short
}
override fun getAsciiStream(columnIndex: Int): InputStream { override fun getAsciiStream(columnIndex: Int): InputStream = getBinaryStream(columnIndex)
return getBinaryStream(columnIndex)
}
override fun getAsciiStream(columnLabel: String?): InputStream { override fun getAsciiStream(columnLabel: String?): InputStream = getBinaryStream(columnLabel)
return getBinaryStream(columnLabel)
}
override fun updateTime( override fun updateTime(
columnIndex: Int, columnIndex: Int,
@@ -1008,13 +941,9 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
notImplemented() notImplemented()
} }
override fun getNCharacterStream(columnIndex: Int): Reader { override fun getNCharacterStream(columnIndex: Int): Reader = getBinaryStream(columnIndex).reader()
return getBinaryStream(columnIndex).reader()
}
override fun getNCharacterStream(columnLabel: String?): Reader { override fun getNCharacterStream(columnLabel: String?): Reader = getBinaryStream(columnLabel).reader()
return getBinaryStream(columnLabel).reader()
}
override fun updateArray( override fun updateArray(
columnIndex: Int, columnIndex: Int,
@@ -1030,45 +959,27 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
notImplemented() notImplemented()
} }
override fun getBytes(columnIndex: Int): ByteArray { override fun getBytes(columnIndex: Int): ByteArray = obj(columnIndex) as ByteArray
return obj(columnIndex) as ByteArray
}
override fun getBytes(columnLabel: String?): ByteArray { override fun getBytes(columnLabel: String?): ByteArray = obj(columnLabel) as ByteArray
return obj(columnLabel) as ByteArray
}
override fun getDouble(columnIndex: Int): Double { override fun getDouble(columnIndex: Int): Double = obj(columnIndex) as Double
return obj(columnIndex) as Double
}
override fun getDouble(columnLabel: String?): Double { override fun getDouble(columnLabel: String?): Double = obj(columnLabel) as Double
return obj(columnLabel) as Double
}
override fun getUnicodeStream(columnIndex: Int): InputStream { override fun getUnicodeStream(columnIndex: Int): InputStream = getBinaryStream(columnIndex)
return getBinaryStream(columnIndex)
}
override fun getUnicodeStream(columnLabel: String?): InputStream { override fun getUnicodeStream(columnLabel: String?): InputStream = getBinaryStream(columnLabel)
return getBinaryStream(columnLabel)
}
override fun rowInserted() = false override fun rowInserted() = false
private fun thisIsWrapperFor(iface: Class<*>?) = this.javaClass.isInstance(iface) private fun thisIsWrapperFor(iface: Class<*>?) = this.javaClass.isInstance(iface)
override fun isWrapperFor(iface: Class<*>?): Boolean { override fun isWrapperFor(iface: Class<*>?): Boolean = thisIsWrapperFor(iface) || parent.isWrapperFor(iface)
return thisIsWrapperFor(iface) || parent.isWrapperFor(iface)
}
override fun getInt(columnIndex: Int): Int { override fun getInt(columnIndex: Int): Int = obj(columnIndex) as Int
return obj(columnIndex) as Int
}
override fun getInt(columnLabel: String?): Int { override fun getInt(columnLabel: String?): Int = obj(columnLabel) as Int
return obj(columnLabel) as Int
}
override fun updateNull(columnIndex: Int) { override fun updateNull(columnIndex: Int) {
notImplemented() notImplemented()
@@ -1088,8 +999,8 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
notImplemented() notImplemented()
} }
override fun getMetaData(): ResultSetMetaData { override fun getMetaData(): ResultSetMetaData =
return object : ResultSetMetaData by parentMetadata { object : ResultSetMetaData by parentMetadata {
override fun isReadOnly(column: Int) = true override fun isReadOnly(column: Int) = true
override fun isWritable(column: Int) = false override fun isWritable(column: Int) = false
@@ -1098,19 +1009,12 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
override fun getColumnCount() = this@ScrollableResultSet.columnCount override fun getColumnCount() = this@ScrollableResultSet.columnCount
override fun getColumnLabel(column: Int): String { override fun getColumnLabel(column: Int): String = columnLabels[column - 1]
return columnLabels[column - 1]
}
} }
}
override fun getBinaryStream(columnIndex: Int): InputStream { override fun getBinaryStream(columnIndex: Int): InputStream = (obj(columnIndex) as ByteArray).inputStream()
return (obj(columnIndex) as ByteArray).inputStream()
}
override fun getBinaryStream(columnLabel: String?): InputStream { override fun getBinaryStream(columnLabel: String?): InputStream = (obj(columnLabel) as ByteArray).inputStream()
return (obj(columnLabel) as ByteArray).inputStream()
}
override fun updateCharacterStream( override fun updateCharacterStream(
columnIndex: Int, columnIndex: Int,
@@ -1,16 +1,12 @@
package xyz.nulldev.androidcompat.info package xyz.nulldev.androidcompat.info
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import org.kodein.di.DI
import org.kodein.di.DIAware
import org.kodein.di.conf.global
import org.kodein.di.instance
import xyz.nulldev.androidcompat.config.ApplicationInfoConfigModule import xyz.nulldev.androidcompat.config.ApplicationInfoConfigModule
import xyz.nulldev.ts.config.ConfigManager import xyz.nulldev.ts.config.ConfigManager
class ApplicationInfoImpl(override val di: DI = DI.global) : ApplicationInfo(), DIAware { class ApplicationInfoImpl(
val configManager: ConfigManager by di.instance() private val configManager: ConfigManager,
) : ApplicationInfo() {
val appInfoConfig: ApplicationInfoConfigModule val appInfoConfig: ApplicationInfoConfigModule
get() = configManager.module() get() = configManager.module()
@@ -8,7 +8,9 @@ import java.io.File
/** /**
* Android file constants. * Android file constants.
*/ */
class AndroidFiles(val configManager: ConfigManager = GlobalConfigManager) { class AndroidFiles(
val configManager: ConfigManager = GlobalConfigManager,
) {
val filesConfig: FilesConfigModule val filesConfig: FilesConfigModule
get() = configManager.module() get() = configManager.module()
@@ -30,9 +32,8 @@ class AndroidFiles(val configManager: ConfigManager = GlobalConfigManager) {
val packagesDir: File get() = registerFile(filesConfig.packageDir) val packagesDir: File get() = registerFile(filesConfig.packageDir)
fun registerFile(file: String): File { fun registerFile(file: String): File =
return File(file).apply { File(file).apply {
mkdirs() mkdirs()
} }
}
} }
@@ -14,11 +14,11 @@ import com.russhwolf.settings.Settings
import com.russhwolf.settings.serialization.decodeValue import com.russhwolf.settings.serialization.decodeValue
import com.russhwolf.settings.serialization.decodeValueOrNull import com.russhwolf.settings.serialization.decodeValueOrNull
import com.russhwolf.settings.serialization.encodeValue import com.russhwolf.settings.serialization.encodeValue
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import kotlinx.serialization.builtins.SetSerializer import kotlinx.serialization.builtins.SetSerializer
import kotlinx.serialization.builtins.serializer import kotlinx.serialization.builtins.serializer
import mu.KotlinLogging
import xyz.nulldev.androidcompat.util.SafePath import xyz.nulldev.androidcompat.util.SafePath
import xyz.nulldev.ts.config.ApplicationRootDir import xyz.nulldev.ts.config.ApplicationRootDir
import java.util.Properties import java.util.Properties
@@ -30,7 +30,9 @@ import kotlin.io.path.inputStream
import kotlin.io.path.outputStream import kotlin.io.path.outputStream
@OptIn(ExperimentalSerializationApi::class, ExperimentalSettingsApi::class) @OptIn(ExperimentalSerializationApi::class, ExperimentalSettingsApi::class)
class JavaSharedPreferences(key: String) : SharedPreferences { class JavaSharedPreferences(
key: String,
) : SharedPreferences {
companion object { companion object {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
} }
@@ -72,20 +74,17 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
private val listeners = mutableMapOf<SharedPreferences.OnSharedPreferenceChangeListener, (String) -> Unit>() private val listeners = mutableMapOf<SharedPreferences.OnSharedPreferenceChangeListener, (String) -> Unit>()
// TODO: 2021-05-29 Need to find a way to get this working with all pref types // TODO: 2021-05-29 Need to find a way to get this working with all pref types
override fun getAll(): MutableMap<String, *> { override fun getAll(): MutableMap<String, *> = preferences.keys.associateWith { preferences.getStringOrNull(it) }.toMutableMap()
return preferences.keys.associateWith { preferences.getStringOrNull(it) }.toMutableMap()
}
override fun getString( override fun getString(
key: String, key: String,
defValue: String?, defValue: String?,
): String? { ): String? =
return if (defValue != null) { if (defValue != null) {
preferences.getString(key, defValue) preferences.getString(key, defValue)
} else { } else {
preferences.getStringOrNull(key) preferences.getStringOrNull(key)
} }
}
override fun getStringSet( override fun getStringSet(
key: String, key: String,
@@ -105,50 +104,48 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
override fun getInt( override fun getInt(
key: String, key: String,
defValue: Int, defValue: Int,
): Int { ): Int = preferences.getInt(key, defValue)
return preferences.getInt(key, defValue)
}
override fun getLong( override fun getLong(
key: String, key: String,
defValue: Long, defValue: Long,
): Long { ): Long = preferences.getLong(key, defValue)
return preferences.getLong(key, defValue)
}
override fun getFloat( override fun getFloat(
key: String, key: String,
defValue: Float, defValue: Float,
): Float { ): Float = preferences.getFloat(key, defValue)
return preferences.getFloat(key, defValue)
}
override fun getBoolean( override fun getBoolean(
key: String, key: String,
defValue: Boolean, defValue: Boolean,
): Boolean { ): Boolean = preferences.getBoolean(key, defValue)
return preferences.getBoolean(key, defValue)
}
override fun contains(key: String): Boolean { override fun contains(key: String): Boolean = key in preferences.keys
return key in preferences.keys
}
override fun edit(): SharedPreferences.Editor { override fun edit(): SharedPreferences.Editor =
return Editor(preferences) { key -> Editor(preferences) { key ->
listeners.forEach { (_, listener) -> listeners.forEach { (_, listener) ->
listener(key) listener(key)
} }
} }
}
class Editor(private val preferences: Settings, private val notify: (String) -> Unit) : SharedPreferences.Editor { class Editor(
private val preferences: Settings,
private val notify: (String) -> Unit,
) : SharedPreferences.Editor {
private val actions = mutableListOf<Action>() private val actions = mutableListOf<Action>()
private sealed class Action { private sealed class Action {
data class Add(val key: String, val value: Any) : Action() data class Add(
val key: String,
val value: Any,
) : Action()
data class Remove(
val key: String,
) : Action()
data class Remove(val key: String) : Action()
data object Clear : Action() data object Clear : Action()
} }
@@ -14,7 +14,9 @@ import java.io.File
import javax.imageio.ImageIO import javax.imageio.ImageIO
import javax.xml.parsers.DocumentBuilderFactory import javax.xml.parsers.DocumentBuilderFactory
data class InstalledPackage(val root: File) { data class InstalledPackage(
val root: File,
) {
val apk = File(root, "package.apk") val apk = File(root, "package.apk")
val jar = File(root, "translated.jar") val jar = File(root, "translated.jar")
val icon = File(root, "icon.png") val icon = File(root, "icon.png")
@@ -34,18 +36,21 @@ data class InstalledPackage(val root: File) {
Bundle().apply { Bundle().apply {
val appTag = doc.getElementsByTagName("application").item(0) val appTag = doc.getElementsByTagName("application").item(0)
appTag?.childNodes?.toList()?.filter { appTag
it.nodeType == Node.ELEMENT_NODE ?.childNodes
}?.map { ?.toList()
it as Element ?.filter {
}?.filter { it.nodeType == Node.ELEMENT_NODE
it.tagName == "meta-data" }?.map {
}?.map { it as Element
putString( }?.filter {
it.attributes.getNamedItem("android:name").nodeValue, it.tagName == "meta-data"
it.attributes.getNamedItem("android:value").nodeValue, }?.map {
) putString(
} it.attributes.getNamedItem("android:name").nodeValue,
it.attributes.getNamedItem("android:value").nodeValue,
)
}
} }
it.signatures = it.signatures =
@@ -53,12 +58,14 @@ data class InstalledPackage(val root: File) {
parsed.apkSingers.flatMap { it.certificateMetas } parsed.apkSingers.flatMap { it.certificateMetas }
// + parsed.apkV2Singers.flatMap { it.certificateMetas } // + parsed.apkV2Singers.flatMap { it.certificateMetas }
) // Blocked by: https://github.com/hsiafan/apk-parser/issues/72 ) // Blocked by: https://github.com/hsiafan/apk-parser/issues/72
.map { Signature(it.data) }.toTypedArray() .map { Signature(it.data) }
.toTypedArray()
} }
fun verify(): Boolean { fun verify(): Boolean {
val res = val res =
ApkVerifier.Builder(apk) ApkVerifier
.Builder(apk)
.build() .build()
.verify() .verify()
@@ -70,11 +77,14 @@ data class InstalledPackage(val root: File) {
val icons = ApkFile(apk).allIcons val icons = ApkFile(apk).allIcons
val read = val read =
icons.filter { it.isFile }.map { icons
it.data.inputStream().use { .filter { it.isFile }
ImageIO.read(it) .map {
} it.data.inputStream().use {
}.sortedByDescending { it.width * it.height }.firstOrNull() ?: return ImageIO.read(it)
}
}.sortedByDescending { it.width * it.height }
.firstOrNull() ?: return
ImageIO.write(read, "png", icon) ImageIO.write(read, "png", icon)
} catch (e: Exception) { } catch (e: Exception) {
@@ -94,8 +104,9 @@ data class InstalledPackage(val root: File) {
fun NodeList.toList(): List<Node> { fun NodeList.toList(): List<Node> {
val out = mutableListOf<Node>() val out = mutableListOf<Node>()
for (i in 0 until length) for (i in 0 until length) {
out += item(i) out += item(i)
}
return out return out
} }
@@ -1,14 +1,12 @@
package xyz.nulldev.androidcompat.pm package xyz.nulldev.androidcompat.pm
import net.dongliu.apk.parser.ApkParsers import net.dongliu.apk.parser.ApkParsers
import org.kodein.di.DI import org.koin.mp.KoinPlatformTools
import org.kodein.di.conf.global
import org.kodein.di.instance
import xyz.nulldev.androidcompat.io.AndroidFiles import xyz.nulldev.androidcompat.io.AndroidFiles
import java.io.File import java.io.File
class PackageController { class PackageController {
private val androidFiles by DI.global.instance<AndroidFiles>() private val androidFiles: AndroidFiles by KoinPlatformTools.defaultContext().get().inject()
private val uninstallListeners = mutableListOf<(String) -> Unit>() private val uninstallListeners = mutableListOf<(String) -> Unit>()
fun registerUninstallListener(listener: (String) -> Unit) { fun registerUninstallListener(listener: (String) -> Unit) {
@@ -57,13 +55,15 @@ class PackageController {
} }
} }
fun listInstalled(): List<InstalledPackage> { fun listInstalled(): List<InstalledPackage> =
return androidFiles.packagesDir.listFiles().orEmpty().filter { androidFiles.packagesDir
it.isDirectory .listFiles()
}.map { .orEmpty()
InstalledPackage(it) .filter {
} it.isDirectory
} }.map {
InstalledPackage(it)
}
fun deletePackage(pack: InstalledPackage) { fun deletePackage(pack: InstalledPackage) {
if (!pack.root.exists()) error("Package was never installed!") if (!pack.root.exists()) error("Package was never installed!")
@@ -6,18 +6,19 @@ import android.content.pm.PackageInfo
import net.dongliu.apk.parser.bean.ApkMeta import net.dongliu.apk.parser.bean.ApkMeta
import java.io.File import java.io.File
fun ApkMeta.toPackageInfo(apk: File): PackageInfo { fun ApkMeta.toPackageInfo(apk: File): PackageInfo =
return PackageInfo().also { PackageInfo().also {
it.packageName = packageName it.packageName = packageName
it.versionCode = versionCode.toInt() it.versionCode = versionCode.toInt()
it.versionName = versionName it.versionName = versionName
it.reqFeatures = it.reqFeatures =
usesFeatures.map { usesFeatures
FeatureInfo().apply { .map {
name = it.name FeatureInfo().apply {
} name = it.name
}.toTypedArray() }
}.toTypedArray()
it.applicationInfo = it.applicationInfo =
ApplicationInfo().apply { ApplicationInfo().apply {
@@ -26,4 +27,3 @@ fun ApkMeta.toPackageInfo(apk: File): PackageInfo {
sourceDir = apk.absolutePath sourceDir = apk.absolutePath
} }
} }
}
@@ -1,7 +1,7 @@
package xyz.nulldev.androidcompat.res; package xyz.nulldev.androidcompat.res;
import xyz.nulldev.androidcompat.info.ApplicationInfoImpl; import xyz.nulldev.androidcompat.info.ApplicationInfoImpl;
import xyz.nulldev.androidcompat.util.KodeinGlobalHelper; import xyz.nulldev.androidcompat.util.KoinGlobalHelper;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Calendar; import java.util.Calendar;
@@ -10,7 +10,7 @@ import java.util.Calendar;
* BuildConfig compat class. * BuildConfig compat class.
*/ */
public class BuildConfigCompat { public class BuildConfigCompat {
private static ApplicationInfoImpl applicationInfo = KodeinGlobalHelper.instance(ApplicationInfoImpl.class); private static ApplicationInfoImpl applicationInfo = KoinGlobalHelper.instance(ApplicationInfoImpl.class);
public static final boolean DEBUG = applicationInfo.getDebug(); public static final boolean DEBUG = applicationInfo.getDebug();
@@ -1,6 +1,8 @@
package xyz.nulldev.androidcompat.res package xyz.nulldev.androidcompat.res
class DrawableResource(val location: String) : Resource { class DrawableResource(
val location: String,
) : Resource {
override fun getType() = DrawableResource::class.java override fun getType() = DrawableResource::class.java
override fun getValue() = javaClass.getResourceAsStream(location) override fun getValue() = javaClass.getResourceAsStream(location)
@@ -19,7 +19,9 @@ package xyz.nulldev.androidcompat.res
/** /**
* String resource. * String resource.
*/ */
class StringResource(val string: String) : Resource { class StringResource(
val string: String,
) : Resource {
override fun getValue() = string override fun getValue() = string
override fun getType() = StringResource::class.java override fun getType() = StringResource::class.java
@@ -3,7 +3,7 @@ package xyz.nulldev.androidcompat.service
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import mu.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import kotlin.concurrent.thread import kotlin.concurrent.thread
@@ -1,70 +0,0 @@
package xyz.nulldev.androidcompat.util
import android.content.Context
import org.kodein.di.DI
import org.kodein.di.conf.global
import org.kodein.di.instance
import xyz.nulldev.androidcompat.androidimpl.CustomContext
import xyz.nulldev.androidcompat.androidimpl.FakePackageManager
import xyz.nulldev.androidcompat.info.ApplicationInfoImpl
import xyz.nulldev.androidcompat.io.AndroidFiles
import xyz.nulldev.androidcompat.pm.PackageController
import xyz.nulldev.androidcompat.service.ServiceSupport
/**
* Helper class to allow access to Kodein from Java
*/
object KodeinGlobalHelper {
/**
* Get the Kodein object
*/
@JvmStatic
fun kodein() = DI.global
/**
* Get a dependency
*/
@JvmStatic
@Suppress("UNCHECKED_CAST")
fun <T : Any> instance(
type: Class<T>,
kodein: DI? = null,
): T {
return when (type) {
AndroidFiles::class.java -> {
val instance: AndroidFiles by (kodein ?: kodein()).instance()
instance as T
}
ApplicationInfoImpl::class.java -> {
val instance: ApplicationInfoImpl by (kodein ?: kodein()).instance()
instance as T
}
ServiceSupport::class.java -> {
val instance: ServiceSupport by (kodein ?: kodein()).instance()
instance as T
}
FakePackageManager::class.java -> {
val instance: FakePackageManager by (kodein ?: kodein()).instance()
instance as T
}
PackageController::class.java -> {
val instance: PackageController by (kodein ?: kodein()).instance()
instance as T
}
CustomContext::class.java -> {
val instance: CustomContext by (kodein ?: kodein()).instance()
instance as T
}
Context::class.java -> {
val instance: Context by (kodein ?: kodein()).instance()
instance as T
}
else -> throw IllegalArgumentException("Kodein instance not found")
}
}
@JvmStatic
fun <T : Any> instance(type: Class<T>): T {
return instance(type, null)
}
}
@@ -0,0 +1,27 @@
package xyz.nulldev.androidcompat.util
import org.koin.core.Koin
import org.koin.mp.KoinPlatformTools
/**
* Helper class to allow access to Kodein from Java
*/
object KoinGlobalHelper {
/**
* Get the Kodein object
*/
@JvmStatic
fun koin() = KoinPlatformTools.defaultContext().get()
/**
* Get a dependency
*/
@JvmStatic
fun <T : Any> instance(
type: Class<T>,
koin: Koin? = null,
): T = (koin ?: koin()).get(type.kotlin)
@JvmStatic
fun <T : Any> instance(type: Class<T>): T = instance(type, null)
}
@@ -18,9 +18,7 @@ class CookieManagerImpl : CookieManager() {
acceptCookie = accept acceptCookie = accept
} }
override fun acceptCookie(): Boolean { override fun acceptCookie(): Boolean = acceptCookie
return acceptCookie
}
override fun setAcceptThirdPartyCookies( override fun setAcceptThirdPartyCookies(
webview: WebView?, webview: WebView?,
@@ -29,9 +27,7 @@ class CookieManagerImpl : CookieManager() {
acceptThirdPartyCookies = accept acceptThirdPartyCookies = accept
} }
override fun acceptThirdPartyCookies(webview: WebView?): Boolean { override fun acceptThirdPartyCookies(webview: WebView?): Boolean = acceptThirdPartyCookies
return acceptThirdPartyCookies
}
override fun setCookie( override fun setCookie(
url: String, url: String,
@@ -65,7 +61,8 @@ class CookieManagerImpl : CookieManager() {
} else { } else {
URI("http://$url") URI("http://$url")
} }
return cookieHandler.cookieStore.get(uri) return cookieHandler.cookieStore
.get(uri)
.joinToString("; ") { "${it.name}=${it.value}" } .joinToString("; ") { "${it.name}=${it.value}" }
} }
@@ -87,15 +84,11 @@ class CookieManagerImpl : CookieManager() {
callback?.onReceiveValue(removedCookies) callback?.onReceiveValue(removedCookies)
} }
override fun hasCookies(): Boolean { override fun hasCookies(): Boolean = cookieHandler.cookieStore.cookies.isNotEmpty()
return cookieHandler.cookieStore.cookies.isNotEmpty()
}
override fun flush() {} override fun flush() {}
override fun allowFileSchemeCookiesImpl(): Boolean { override fun allowFileSchemeCookiesImpl(): Boolean = allowFileSchemeCookies
return allowFileSchemeCookies
}
override fun setAcceptFileSchemeCookiesImpl(accept: Boolean) { override fun setAcceptFileSchemeCookiesImpl(accept: Boolean) {
allowFileSchemeCookies = acceptCookie allowFileSchemeCookies = acceptCookie
+259
View File
@@ -1,3 +1,262 @@
# Server: v2.0.1727 + WevUI: v1.5.1
## TL;DR
-
## Suwayomi-Server Changelog
- ([r1726](https://github.com/Suwayomi/Suwayomi-Server/commit/1d5323a477528649f81fa4bd2e1e9e4a28da6402)) [skip ci] Add link to discord in issue templates ([#1347](https://github.com/Suwayomi/Suwayomi-Server/pull/1347) by @schroda)
- ([r1725](https://github.com/Suwayomi/Suwayomi-Server/commit/f8d73819eaae86e4de10c8795e44d2347f1ce03e)) [Feature] Support Bangumi Tracker ([#1343](https://github.com/Suwayomi/Suwayomi-Server/pull/1343) by @kaaass, @Syer10)
- ([r1724](https://github.com/Suwayomi/Suwayomi-Server/commit/cbe26b7291a41c6a9a9207ab97c3b86db8978e4e)) Chmod in build script (by @Syer10)
- ([r1723](https://github.com/Suwayomi/Suwayomi-Server/commit/93477f60c2204fc10c42c8f62832543696d8e112)) Fix release name (by @Syer10)
- ([r1722](https://github.com/Suwayomi/Suwayomi-Server/commit/9feebbfe177a3ec3044f5c30607c73e9bae7c60c)) Use tar for MacOS (by @Syer10)
- ([r1721](https://github.com/Suwayomi/Suwayomi-Server/commit/6e365491a91962061f806e0debc878df89f46881)) Add permissions to jspawnhelper ([#1339](https://github.com/Suwayomi/Suwayomi-Server/pull/1339) by @Syer10)
- ([r1720](https://github.com/Suwayomi/Suwayomi-Server/commit/2e586581297ce4955b3e25f0ba822b681f2683ed)) Fix MacOS builds ([#1338](https://github.com/Suwayomi/Suwayomi-Server/pull/1338) by @Syer10)
- ([r1719](https://github.com/Suwayomi/Suwayomi-Server/commit/256c564b91a9154d3d8883d2a052095aa581edf5)) Fix release version extraction from jar name ([#1335](https://github.com/Suwayomi/Suwayomi-Server/pull/1335) by @schroda)
- ([r1718](https://github.com/Suwayomi/Suwayomi-Server/commit/96b50f52ec917e8a521c5f87ae85704d188ec9f6)) Ensure webui "channel" is always of corresponding enum ([#1334](https://github.com/Suwayomi/Suwayomi-Server/pull/1334) by @schroda)
- ([r1717](https://github.com/Suwayomi/Suwayomi-Server/commit/3167d8aa15cc5c7b7064a61eccb0e28336808241)) Fix/startup jvm error after installation update via msi ([#1229](https://github.com/Suwayomi/Suwayomi-Server/pull/1229) by @schroda, @Syer10)
- ([r1716](https://github.com/Suwayomi/Suwayomi-Server/commit/78fd09c7286cfbd0db9dd308ebbb41e6e7fbe7c5)) Prevent IndexOutOfBoundsException in "libraryUpdate" subscription ([#1320](https://github.com/Suwayomi/Suwayomi-Server/pull/1320) by @schroda)
- ([r1715](https://github.com/Suwayomi/Suwayomi-Server/commit/4c5598cedfcd40c2dffab31e45f38d692b2c3741)) Feature/graphql log execution exceptions ([#1319](https://github.com/Suwayomi/Suwayomi-Server/pull/1319) by @schroda)
- ([r1714](https://github.com/Suwayomi/Suwayomi-Server/commit/c3347d94abe131f4e75ef3d9736184715f471812)) Feature/use GitHub issue yml format ([#1314](https://github.com/Suwayomi/Suwayomi-Server/pull/1314) by @schroda)
- ([r1713](https://github.com/Suwayomi/Suwayomi-Server/commit/7ca4aa75a898212616b3bbfbbbc864b8d64b08a5)) Fix checkbox preference title nullability ([#1313](https://github.com/Suwayomi/Suwayomi-Server/pull/1313) by @Syer10)
- ([r1712](https://github.com/Suwayomi/Suwayomi-Server/commit/226fad5594871d24c0f670d2bac741076e048376)) Remove "default" category from backups ([#1307](https://github.com/Suwayomi/Suwayomi-Server/pull/1307) by @schroda)
- ([r1711](https://github.com/Suwayomi/Suwayomi-Server/commit/d0ee1ba5af8b4a560722ca5e6a29f58b46a84577)) Align kitsu icon with icons of other trackers ([#1303](https://github.com/Suwayomi/Suwayomi-Server/pull/1303) by @schroda)
- ([r1710](https://github.com/Suwayomi/Suwayomi-Server/commit/439e0c8284308a731b5cbd4255cd4b08f75660f0)) Emit only updater job changes instead of full status ([#1302](https://github.com/Suwayomi/Suwayomi-Server/pull/1302) by @schroda)
- ([r1709](https://github.com/Suwayomi/Suwayomi-Server/commit/7d079a8728d82af3082cde3db70114b946972a18)) Update kotlin monorepo to v2.1.20 ([#1315](https://github.com/Suwayomi/Suwayomi-Server/pull/1315) by @renovate[bot])
- ([r1708](https://github.com/Suwayomi/Suwayomi-Server/commit/945a52653e78b239d6288bb983099d4b76505ace)) Update graphqlkotlin to v8.4.0 ([#1311](https://github.com/Suwayomi/Suwayomi-Server/pull/1311) by @renovate[bot])
- ([r1707](https://github.com/Suwayomi/Suwayomi-Server/commit/bdafc86990f47d22325ace0170d5dbe721be374c)) Update polyglot to v24.2.0 ([#1310](https://github.com/Suwayomi/Suwayomi-Server/pull/1310) by @renovate[bot])
- ([r1706](https://github.com/Suwayomi/Suwayomi-Server/commit/57d425ab9f01917d238fd61d2ecb2d3ce966c73f)) Update dependency ch.qos.logback:logback-classic to v1.5.18 ([#1309](https://github.com/Suwayomi/Suwayomi-Server/pull/1309) by @renovate[bot])
- ([r1705](https://github.com/Suwayomi/Suwayomi-Server/commit/395ac8e944785504d524a0a2a1f36f96975eb602)) Update dependency com.ibm.icu:icu4j to v77 ([#1305](https://github.com/Suwayomi/Suwayomi-Server/pull/1305) by @renovate[bot])
- ([r1704](https://github.com/Suwayomi/Suwayomi-Server/commit/2f801e7571300e0504edb6a46ff83382358bda88)) Update plugin buildconfig to v5.5.4 ([#1304](https://github.com/Suwayomi/Suwayomi-Server/pull/1304) by @renovate[bot])
- ([r1703](https://github.com/Suwayomi/Suwayomi-Server/commit/d7636045fee34e3da8d66d81f59704c8922d0eea)) Update dependency io.javalin:javalin to v6.5.0 ([#1301](https://github.com/Suwayomi/Suwayomi-Server/pull/1301) by @renovate[bot])
- ([r1702](https://github.com/Suwayomi/Suwayomi-Server/commit/b745f108704185882f2bdccf1a69e707548eb89e)) Update dependency org.jsoup:jsoup to v1.19.1 ([#1292](https://github.com/Suwayomi/Suwayomi-Server/pull/1292) by @renovate[bot])
- ([r1701](https://github.com/Suwayomi/Suwayomi-Server/commit/d76849942c412d1b8f04d20747cfce34cf50f553)) Update plugin buildconfig to v5.5.2 ([#1299](https://github.com/Suwayomi/Suwayomi-Server/pull/1299) by @renovate[bot])
- ([r1700](https://github.com/Suwayomi/Suwayomi-Server/commit/b7a8a3ffe84527d25982d0a0be866b3cf6a08214)) Update dependency io.github.oshai:kotlin-logging-jvm to v7.0.5 ([#1298](https://github.com/Suwayomi/Suwayomi-Server/pull/1298) by @renovate[bot])
- ([r1699](https://github.com/Suwayomi/Suwayomi-Server/commit/95d9293fe013d5c553924521bca430c7d7768090)) Add support for opds-pse for undownloaded chapters ([#1278](https://github.com/Suwayomi/Suwayomi-Server/pull/1278) by @ShirishSaxena, @showyee)
- ([r1698](https://github.com/Suwayomi/Suwayomi-Server/commit/3be165a5514af306398d542118da40fab16da411)) Initial import of Kitsu tracker ([#1297](https://github.com/Suwayomi/Suwayomi-Server/pull/1297) by @cpiber)
- ([r1697](https://github.com/Suwayomi/Suwayomi-Server/commit/cb498e2128549fc462d815cf70c89689a3ca5090)) Update dependency org.slf4j:slf4j-api to v2.0.17 ([#1284](https://github.com/Suwayomi/Suwayomi-Server/pull/1284) by @renovate[bot])
- ([r1696](https://github.com/Suwayomi/Suwayomi-Server/commit/973f4d66e2b569d712452633d1be7913f4a958fc)) Update jackson monorepo to v2.18.3 ([#1289](https://github.com/Suwayomi/Suwayomi-Server/pull/1289) by @renovate[bot])
- ([r1695](https://github.com/Suwayomi/Suwayomi-Server/commit/2c80672f6e81071febfa30d8b18740b31def31ad)) Update plugin ktlint to v12.2.0 ([#1288](https://github.com/Suwayomi/Suwayomi-Server/pull/1288) by @renovate[bot])
- ([r1694](https://github.com/Suwayomi/Suwayomi-Server/commit/2599813ef1c7e2c06e8dec2acc3ac449e723fa91)) Update dependency io.mockk:mockk to v1.13.17 ([#1287](https://github.com/Suwayomi/Suwayomi-Server/pull/1287) by @renovate[bot])
- ([r1693](https://github.com/Suwayomi/Suwayomi-Server/commit/86f849a185d69f3e3b3c5085355985f38f42ff7a)) Update dependency net.harawata:appdirs to v1.4.0 ([#1286](https://github.com/Suwayomi/Suwayomi-Server/pull/1286) by @renovate[bot])
- ([r1692](https://github.com/Suwayomi/Suwayomi-Server/commit/875f1f1506c76cf97b6a61ecc800f8aa4eb9c029)) Update dependency com.android.tools.build:apksig to v8.9.0 ([#1285](https://github.com/Suwayomi/Suwayomi-Server/pull/1285) by @renovate[bot])
- ([r1691](https://github.com/Suwayomi/Suwayomi-Server/commit/e418375963a254d8eb7a35b982c8e7078e493bea)) Update dependency ch.qos.logback:logback-classic to v1.5.17 ([#1283](https://github.com/Suwayomi/Suwayomi-Server/pull/1283) by @renovate[bot])
- ([r1690](https://github.com/Suwayomi/Suwayomi-Server/commit/d528fc7f9e6aeddece689e45fb0633508c5751b2)) Update dependency gradle to v8.13 ([#1282](https://github.com/Suwayomi/Suwayomi-Server/pull/1282) by @renovate[bot])
- ([r1689](https://github.com/Suwayomi/Suwayomi-Server/commit/633ea97848ad851103c23b5c0196d8786c8e90cd)) Feature/optimize backup import ([#1270](https://github.com/Suwayomi/Suwayomi-Server/pull/1270) by @schroda)
- ([r1688](https://github.com/Suwayomi/Suwayomi-Server/commit/36cb899b91b3bfcfb94a8baf85e9db7a6e752d6c)) Prevent chapter lastReadPage coerceIn error ([#1272](https://github.com/Suwayomi/Suwayomi-Server/pull/1272) by @schroda)
- ([r1687](https://github.com/Suwayomi/Suwayomi-Server/commit/c4d849d6a33a2ada5320c7910683556e204498e9)) Fix invalid chapter download state in database ([#1271](https://github.com/Suwayomi/Suwayomi-Server/pull/1271) by @schroda)
- ([r1686](https://github.com/Suwayomi/Suwayomi-Server/commit/c8bd39b4bfee44005614df5b647e803279e5218e)) Prevent negative lastPageRead values ([#1267](https://github.com/Suwayomi/Suwayomi-Server/pull/1267) by @schroda, @Syer10)
- ([r1685](https://github.com/Suwayomi/Suwayomi-Server/commit/733ba16af2130013eec6b29c90fea181eb9b7c5e)) Fix/backup with duplicated chapters failure ([#1269](https://github.com/Suwayomi/Suwayomi-Server/pull/1269) by @schroda)
- ([r1684](https://github.com/Suwayomi/Suwayomi-Server/commit/37f57c0c55586d89ecb4985d5e3ce38baf16ae62)) Prevent marking chapter as downloaded without pages ([#1268](https://github.com/Suwayomi/Suwayomi-Server/pull/1268) by @schroda)
- ([r1683](https://github.com/Suwayomi/Suwayomi-Server/commit/013dbd79b4ab6c399db43e3d975e7ed9fbcea517)) Update dependency com.android.tools.build:apksig to v8.8.1 ([#1264](https://github.com/Suwayomi/Suwayomi-Server/pull/1264) by @renovate[bot])
- ([r1682](https://github.com/Suwayomi/Suwayomi-Server/commit/f76d0b3258c51fed1d8f657fdd039c76f3fab40b)) Remove redundant code and add next/prev links ([#1263](https://github.com/Suwayomi/Suwayomi-Server/pull/1263) by @zeedif)
- ([r1681](https://github.com/Suwayomi/Suwayomi-Server/commit/c2f7cdd72e3787fe6451371181da20bc61bd0d19)) Refactoring OPDS API for a more versatile root, allowing selection of manga listing by: all, source, genre, category, language, status. ([#1262](https://github.com/Suwayomi/Suwayomi-Server/pull/1262) by @zeedif)
- ([r1680](https://github.com/Suwayomi/Suwayomi-Server/commit/01c37cb0ba08835e173243db265c98d2219e1c25)) Ignore authentication for preflight requests ([#1261](https://github.com/Suwayomi/Suwayomi-Server/pull/1261) by @schroda)
- ([r1679](https://github.com/Suwayomi/Suwayomi-Server/commit/0dd0af1b8462396749661673598d0650ed8129ad)) Update dependency io.github.oshai:kotlin-logging-jvm to v7.0.4 ([#1260](https://github.com/Suwayomi/Suwayomi-Server/pull/1260) by @renovate[bot])
- ([r1678](https://github.com/Suwayomi/Suwayomi-Server/commit/26aa68430099cadce0b8edffe9bf7cd59c0cec4f)) Add support for OPDS v1.2 to browse stored CBZ files ([#1257](https://github.com/Suwayomi/Suwayomi-Server/pull/1257) by @zeedif)
- ([r1677](https://github.com/Suwayomi/Suwayomi-Server/commit/9669cdfb761e31a764ec31d9bbe2cfe04df8a0ef)) Update kotlin monorepo to v2.1.10 ([#1251](https://github.com/Suwayomi/Suwayomi-Server/pull/1251) by @renovate[bot])
- ([r1676](https://github.com/Suwayomi/Suwayomi-Server/commit/2c5e5e283e7d3b3bb3019dd2d448347ae118d6ee)) Update dependency com.github.Suwayomi:exposed-migrations to v3.7.0 ([#1248](https://github.com/Suwayomi/Suwayomi-Server/pull/1248) by @renovate[bot])
- ([r1675](https://github.com/Suwayomi/Suwayomi-Server/commit/303921c6ea5e2c828b91f2bbea35c3a55e1b5c41)) Update dependency gradle to v8.12.1 ([#1247](https://github.com/Suwayomi/Suwayomi-Server/pull/1247) by @renovate[bot])
- ([r1674](https://github.com/Suwayomi/Suwayomi-Server/commit/593291a60f10da3aa97b4180436ebe8af77d84a0)) Update exposed to v0.59.0 ([#1228](https://github.com/Suwayomi/Suwayomi-Server/pull/1228) by @renovate[bot])
- ([r1673](https://github.com/Suwayomi/Suwayomi-Server/commit/3af8e395bd6f48ee7ed39011115dc190fa99a7e0)) Check if file exists ([#1246](https://github.com/Suwayomi/Suwayomi-Server/pull/1246) by @Syer10)
- ([r1672](https://github.com/Suwayomi/Suwayomi-Server/commit/0b192cfa5243584d734b541c2c5451a50edd577e)) Normalize Paths ([#1245](https://github.com/Suwayomi/Suwayomi-Server/pull/1245) by @Syer10)
- ([r1671](https://github.com/Suwayomi/Suwayomi-Server/commit/fb8f20f31a823b018af704e14a35bc55be05c177)) Fix MacOS .command file with new JRE ([#1243](https://github.com/Suwayomi/Suwayomi-Server/pull/1243) by @Syer10)
- ([r1670](https://github.com/Suwayomi/Suwayomi-Server/commit/e53386cf7278055bd3a74e5534a73f9d42b0b373)) Update dependency adoptium/temurin21-binaries to jdk-21.0.6+7 ([#1241](https://github.com/Suwayomi/Suwayomi-Server/pull/1241) by @renovate[bot])
- ([r1669](https://github.com/Suwayomi/Suwayomi-Server/commit/b0e9b9b307fa879b224f3cb4b046aa279ad8658e)) Update polyglot to v24.1.2 ([#1240](https://github.com/Suwayomi/Suwayomi-Server/pull/1240) by @renovate[bot])
- ([r1668](https://github.com/Suwayomi/Suwayomi-Server/commit/789678a45d02bd23fe6cb9983350fbf9fbea3066)) Update dependency io.insert-koin:koin-core to v4.0.2 ([#1239](https://github.com/Suwayomi/Suwayomi-Server/pull/1239) by @renovate[bot])
- ([r1667](https://github.com/Suwayomi/Suwayomi-Server/commit/4ad01d345175904b2e64617c5c5a5f577218f315)) Update graphqlkotlin to v8.3.0 ([#1235](https://github.com/Suwayomi/Suwayomi-Server/pull/1235) by @renovate[bot])
- ([r1666](https://github.com/Suwayomi/Suwayomi-Server/commit/6a87daa0b3a44cd5b7896cacf5a2feed37d907ab)) Update dependency org.bouncycastle:bcprov-jdk18on to v1.80 ([#1231](https://github.com/Suwayomi/Suwayomi-Server/pull/1231) by @renovate[bot])
- ([r1665](https://github.com/Suwayomi/Suwayomi-Server/commit/aebef87076f33eabbee134acc77dadad5f1209c3)) Update dependency io.github.reactivecircus.cache4k:cache4k to v0.14.0 ([#1230](https://github.com/Suwayomi/Suwayomi-Server/pull/1230) by @renovate[bot])
- ([r1664](https://github.com/Suwayomi/Suwayomi-Server/commit/32ff58598f4537662b7e33671025e889d63a3eae)) Merge Service Files during build to fix GraalJS ([#1242](https://github.com/Suwayomi/Suwayomi-Server/pull/1242) by @Syer10)
- ([r1663](https://github.com/Suwayomi/Suwayomi-Server/commit/2d56cbe227e2955f722d1f1e1961f1b106c31045)) Update serialization to v1.8.0 ([#1220](https://github.com/Suwayomi/Suwayomi-Server/pull/1220) by @renovate[bot])
- ([r1662](https://github.com/Suwayomi/Suwayomi-Server/commit/fbef5f592b5994ebc5ed32f0146b18a3a4b9674c)) Update dependency com.android.tools.build:apksig to v8.8.0 ([#1227](https://github.com/Suwayomi/Suwayomi-Server/pull/1227) by @renovate[bot])
- ([r1661](https://github.com/Suwayomi/Suwayomi-Server/commit/090af36f5e6c320bd409af7eb2aab37707ef4898)) Update dependency com.squareup.okio:okio to v3.10.2 ([#1223](https://github.com/Suwayomi/Suwayomi-Server/pull/1223) by @renovate[bot])
- ([r1660](https://github.com/Suwayomi/Suwayomi-Server/commit/885f27fcf12a30b8294947e9ccbe36bf47c45181)) Update dependency net.harawata:appdirs to v1.3.0 ([#1219](https://github.com/Suwayomi/Suwayomi-Server/pull/1219) by @renovate[bot])
- ([r1659](https://github.com/Suwayomi/Suwayomi-Server/commit/8dc4eaf77abf578f5f0bfcb1683ef50dcfe5657f)) Update dependency io.insert-koin:koin-core to v4.0.1 ([#1213](https://github.com/Suwayomi/Suwayomi-Server/pull/1213) by @renovate[bot])
- ([r1658](https://github.com/Suwayomi/Suwayomi-Server/commit/a55bef08a820f92bdaaea373aadff6d5c89fde94)) Update kotlinx-coroutines monorepo to v1.10.1 ([#1212](https://github.com/Suwayomi/Suwayomi-Server/pull/1212) by @renovate[bot])
- ([r1657](https://github.com/Suwayomi/Suwayomi-Server/commit/0dc308973949c06eda5643e3a91694ffb02d7296)) Update dependency gradle to v8.12 ([#1211](https://github.com/Suwayomi/Suwayomi-Server/pull/1211) by @renovate[bot])
- ([r1656](https://github.com/Suwayomi/Suwayomi-Server/commit/789ef0d7838f2d7790ca06caa31659340c1575e8)) Update dependency io.mockk:mockk to v1.13.16 ([#1210](https://github.com/Suwayomi/Suwayomi-Server/pull/1210) by @renovate[bot])
- ([r1655](https://github.com/Suwayomi/Suwayomi-Server/commit/092db1106d1841ebf15ea01812102985aade11bc)) Update dependency ch.qos.logback:logback-classic to v1.5.16 ([#1209](https://github.com/Suwayomi/Suwayomi-Server/pull/1209) by @renovate[bot])
- ([r1654](https://github.com/Suwayomi/Suwayomi-Server/commit/5f4b5bc57078a20e8badc129205a7ef80b57a047)) Update dependency io.javalin:javalin to v6.4.0 ([#1205](https://github.com/Suwayomi/Suwayomi-Server/pull/1205) by @renovate[bot])
- ([r1653](https://github.com/Suwayomi/Suwayomi-Server/commit/32581fcd5a6d89f095de02800ab4f80ba7da1349)) [WIP] Customize JRE ([#1177](https://github.com/Suwayomi/Suwayomi-Server/pull/1177) by @Syer10)
- ([r1652](https://github.com/Suwayomi/Suwayomi-Server/commit/b14d28c4068e7ee99b13c1228cfe74031a901381)) new icons ([#1222](https://github.com/Suwayomi/Suwayomi-Server/pull/1222) by @Robonau)
- ([r1651](https://github.com/Suwayomi/Suwayomi-Server/commit/1d1535dc55f76ebeb4a90412f2f1e2e4cb69f82d)) Send dequeue download mutation response ([#1218](https://github.com/Suwayomi/Suwayomi-Server/pull/1218) by @schroda)
- ([r1650](https://github.com/Suwayomi/Suwayomi-Server/commit/de942440e3dbd81207f048a1d13a5fbdb4a1c73c)) Handle missing track search results on bind ([#1196](https://github.com/Suwayomi/Suwayomi-Server/pull/1196) by @schroda)
- ([r1649](https://github.com/Suwayomi/Suwayomi-Server/commit/b58fc39cf157a922c85de966d16b536c447217e5)) Update dependency io.github.oshai:kotlin-logging-jvm to v7.0.3 ([#1191](https://github.com/Suwayomi/Suwayomi-Server/pull/1191) by @renovate[bot])
- ([r1648](https://github.com/Suwayomi/Suwayomi-Server/commit/2e3af25dd443bd7c880c47403bd2419414cc879b)) Fix usage of deprecated functions ([#1192](https://github.com/Suwayomi/Suwayomi-Server/pull/1192) by @Syer10)
- ([r1647](https://github.com/Suwayomi/Suwayomi-Server/commit/1d541a30ae03fd69ba049d0611eab41c92785146)) Feature/update to exposed v0.57.0 ([#1150](https://github.com/Suwayomi/Suwayomi-Server/pull/1150) by @schroda)
- ([r1646](https://github.com/Suwayomi/Suwayomi-Server/commit/f926714544ebeb21229ceace7f3e79462d38f5f9)) Update dependency com.pinterest.ktlint:ktlint-cli to v1.5.0 ([#1182](https://github.com/Suwayomi/Suwayomi-Server/pull/1182) by @renovate[bot])
- ([r1645](https://github.com/Suwayomi/Suwayomi-Server/commit/f68849d3a5445519cb0d35cff8792d1c466a8c5b)) Update dependency com.russhwolf:multiplatform-settings-jvm to v1.3.0 ([#1176](https://github.com/Suwayomi/Suwayomi-Server/pull/1176) by @renovate[bot])
- ([r1644](https://github.com/Suwayomi/Suwayomi-Server/commit/2111232f42a84935320da2e2771ea171c48fd754)) Update kotlin monorepo to v2.1.0 ([#1170](https://github.com/Suwayomi/Suwayomi-Server/pull/1170) by @renovate[bot], @Syer10)
- ([r1643](https://github.com/Suwayomi/Suwayomi-Server/commit/088552bf56dbf9053442050137af2eae3b554bf3)) Fix Deprecation Warnings ([#1187](https://github.com/Suwayomi/Suwayomi-Server/pull/1187) by @Syer10)
- ([r1642](https://github.com/Suwayomi/Suwayomi-Server/commit/3eabbc977007ec0c64f0fadb974470ec437cc39a)) Manually update GraphQL-Java to fix subscription data loaders ([#1186](https://github.com/Suwayomi/Suwayomi-Server/pull/1186) by @Syer10)
- ([r1641](https://github.com/Suwayomi/Suwayomi-Server/commit/8e3b8df4978c838cfb93ad6061dec97b00112aab)) Update dependency com.android.tools.build:apksig to v8.7.3 ([#1179](https://github.com/Suwayomi/Suwayomi-Server/pull/1179) by @renovate[bot])
- ([r1640](https://github.com/Suwayomi/Suwayomi-Server/commit/06a5aaaa729f29e83045691b57f8f74fb5e849ff)) Update dependency com.fasterxml.jackson.core:jackson-databind to v2.18.2 ([#1175](https://github.com/Suwayomi/Suwayomi-Server/pull/1175) by @renovate[bot])
- ([r1639](https://github.com/Suwayomi/Suwayomi-Server/commit/97c4f14094964dce7446199652dd2ed448ef1d5d)) Update dependency org.jsoup:jsoup to v1.18.3 ([#1169](https://github.com/Suwayomi/Suwayomi-Server/pull/1169) by @renovate[bot])
- ([r1638](https://github.com/Suwayomi/Suwayomi-Server/commit/1fa7f182356b530ddfc46c987b7345c34449327a)) Update plugin ktlint to v12.1.2 ([#1166](https://github.com/Suwayomi/Suwayomi-Server/pull/1166) by @renovate[bot])
- ([r1637](https://github.com/Suwayomi/Suwayomi-Server/commit/b309d2fd4af31e424f103d8c4a6fb372f1420c78)) Update dependency gradle to v8.11.1 ([#1161](https://github.com/Suwayomi/Suwayomi-Server/pull/1161) by @renovate[bot])
- ([r1636](https://github.com/Suwayomi/Suwayomi-Server/commit/372b56bb1bb1ee9e81b7081c1b40e52cb2f0065c)) add manga description ([#1165](https://github.com/Suwayomi/Suwayomi-Server/pull/1165) by @dejavui)
- ([r1635](https://github.com/Suwayomi/Suwayomi-Server/commit/3325a36caefc6423b02cb4fd35685a61556bd946)) Allow cors with credentials ([#1163](https://github.com/Suwayomi/Suwayomi-Server/pull/1163) by @schroda)
- ([r1634](https://github.com/Suwayomi/Suwayomi-Server/commit/38673bbff4fd0f5f3eb17e9d52ef20a9f91ddd28)) Handle missing credentials as being invalid ([#1164](https://github.com/Suwayomi/Suwayomi-Server/pull/1164) by @schroda)
- ([r1633](https://github.com/Suwayomi/Suwayomi-Server/commit/fb51834153b33060779e93d3c460f55878a712cb)) Fix RealUrl ([#1162](https://github.com/Suwayomi/Suwayomi-Server/pull/1162) by @Syer10)
- ([r1632](https://github.com/Suwayomi/Suwayomi-Server/commit/3a932a1e8acbc0242f5ae75a12e6bf404b101dd3)) [skip ci] Update plugin buildconfig to v5.5.1 ([#1157](https://github.com/Suwayomi/Suwayomi-Server/pull/1157) by @renovate[bot])
- ([r1631](https://github.com/Suwayomi/Suwayomi-Server/commit/53c61bcb1784d702191d20e2f9239bab19b9ab33)) Serve webui on all unmatched routes ([#1156](https://github.com/Suwayomi/Suwayomi-Server/pull/1156) by @schroda)
- ([r1630](https://github.com/Suwayomi/Suwayomi-Server/commit/6951b4b20d6993c4fada4cf50d745281b7cc3f03)) Remove "grapqhl log level" setting ([#1155](https://github.com/Suwayomi/Suwayomi-Server/pull/1155) by @schroda)
- ([r1629](https://github.com/Suwayomi/Suwayomi-Server/commit/746f9f1a11e2691e88f1b21bfaa7b63c1aa0d66f)) [WIP] Switch to GraalJS Engine ([#793](https://github.com/Suwayomi/Suwayomi-Server/pull/793) by @Syer10)
- ([r1628](https://github.com/Suwayomi/Suwayomi-Server/commit/9a7344ccbe9307fe056b542a5cdebd9540d75982)) Update dependency ch.qos.logback:logback-classic to v1.5.12 ([#1151](https://github.com/Suwayomi/Suwayomi-Server/pull/1151) by @renovate[bot])
- ([r1627](https://github.com/Suwayomi/Suwayomi-Server/commit/ab2fb8747f1744301a4c098eee8fcb1c3b4f8a6f)) Update jackson monorepo to v2.18.1 ([#1148](https://github.com/Suwayomi/Suwayomi-Server/pull/1148) by @renovate[bot])
- ([r1626](https://github.com/Suwayomi/Suwayomi-Server/commit/9cd8cb3d54635b10dadf4ec252b6e5ec95ac023c)) Update dependency io.javalin:javalin to v6 ([#1152](https://github.com/Suwayomi/Suwayomi-Server/pull/1152) by @renovate[bot], @Syer10)
- ([r1625](https://github.com/Suwayomi/Suwayomi-Server/commit/ba1c2845b6d40dea32a148febdc59fe540e4e032)) chore(deps): update plugin buildconfig to v5 ([#1135](https://github.com/Suwayomi/Suwayomi-Server/pull/1135) by @renovate[bot])
- ([r1624](https://github.com/Suwayomi/Suwayomi-Server/commit/065aa19e9eaed26709b5516bc9da95001101c9bd)) Update graphqlkotlin to v8 (major) ([#1143](https://github.com/Suwayomi/Suwayomi-Server/pull/1143) by @renovate[bot], @Syer10)
- ([r1623](https://github.com/Suwayomi/Suwayomi-Server/commit/4c2a05c3a6943b37834e3ac29372c74d527b34df)) Update Java to 21 ([#1149](https://github.com/Suwayomi/Suwayomi-Server/pull/1149) by @Syer10)
- ([r1622](https://github.com/Suwayomi/Suwayomi-Server/commit/fd45c0740c87c959d4a074f45fab77ac07478ea8)) [skip ci] Update Renovate config (by @Syer10)
- ([r1621](https://github.com/Suwayomi/Suwayomi-Server/commit/e44bf920fad7042ed11499a0a014b94888331e09)) [skip ci] Update Renovate config (by @Syer10)
- ([r1620](https://github.com/Suwayomi/Suwayomi-Server/commit/8a327b2dffc361e7ece397ea690c294c77b5a715)) [skip ci] chore(config): migrate config renovate.json ([#1144](https://github.com/Suwayomi/Suwayomi-Server/pull/1144) by @renovate[bot])
- ([r1619](https://github.com/Suwayomi/Suwayomi-Server/commit/6ece7e2596dda11bfd4a97a391e5e5d60f8ecff9)) Update Renovate config (by @Syer10)
- ([r1618](https://github.com/Suwayomi/Suwayomi-Server/commit/2e2ce98be3715dc1063dc07ed0114cbc87b051e3)) fix(deps): update dependency com.pinterest.ktlint:ktlint-cli to v1.4.1 ([#1126](https://github.com/Suwayomi/Suwayomi-Server/pull/1126) by @renovate[bot])
- ([r1617](https://github.com/Suwayomi/Suwayomi-Server/commit/fe4c2392dbc83ba810d2279359b84cc95ac67848)) chore(deps): update plugin ktlint to v12 ([#1136](https://github.com/Suwayomi/Suwayomi-Server/pull/1136) by @renovate[bot])
- ([r1616](https://github.com/Suwayomi/Suwayomi-Server/commit/320a0971b4aaac057ab33c0afa2942dc004680fc)) Fix/gql download subscription ([#1137](https://github.com/Suwayomi/Suwayomi-Server/pull/1137) by @schroda)
- ([r1615](https://github.com/Suwayomi/Suwayomi-Server/commit/bfb70b6a055b48dbe5e02cda661add860b596db0)) fix(deps): update dependency com.ibm.icu:icu4j to v76 ([#1140](https://github.com/Suwayomi/Suwayomi-Server/pull/1140) by @renovate[bot])
- ([r1614](https://github.com/Suwayomi/Suwayomi-Server/commit/a68af62748e3c3e0d210868d2fe98ae561093b01)) fix(deps): update twelvemonkeys to v3.12.0 ([#1133](https://github.com/Suwayomi/Suwayomi-Server/pull/1133) by @renovate[bot])
- ([r1613](https://github.com/Suwayomi/Suwayomi-Server/commit/52bd5ce5cc8758930d1b39deaa15be2cae12ebb4)) fix(deps): update kotlinx-coroutines monorepo to v1.9.0 ([#1132](https://github.com/Suwayomi/Suwayomi-Server/pull/1132) by @renovate[bot])
- ([r1612](https://github.com/Suwayomi/Suwayomi-Server/commit/b93d4863483d3d57690279b5c1e2343ea585f008)) fix(deps): update dependency org.bouncycastle:bcprov-jdk18on to v1.79 ([#1127](https://github.com/Suwayomi/Suwayomi-Server/pull/1127) by @renovate[bot])
- ([r1611](https://github.com/Suwayomi/Suwayomi-Server/commit/d193c58e5f86f74d3dbf67aca8b16694a68dc119)) fix(deps): update dependency androidx.annotation:annotation to v1.9.1 ([#1121](https://github.com/Suwayomi/Suwayomi-Server/pull/1121) by @renovate[bot])
- ([r1610](https://github.com/Suwayomi/Suwayomi-Server/commit/6ac2a61793eea51ceccd663d2c8a2570709489b9)) Update serialization to v1.7.3 ([#1119](https://github.com/Suwayomi/Suwayomi-Server/pull/1119) by @renovate[bot])
- ([r1609](https://github.com/Suwayomi/Suwayomi-Server/commit/a45c6f2197c204b498d0d5f669b2872edd26d426)) Update kotlin monorepo to v2.0.21 ([#1117](https://github.com/Suwayomi/Suwayomi-Server/pull/1117) by @renovate[bot])
- ([r1608](https://github.com/Suwayomi/Suwayomi-Server/commit/71d639bf19f7d25f885a29b16dcf776a9ea2c6ee)) Update dependency io.mockk:mockk to v1.13.13 ([#1116](https://github.com/Suwayomi/Suwayomi-Server/pull/1116) by @renovate[bot])
- ([r1607](https://github.com/Suwayomi/Suwayomi-Server/commit/9a51472726079b30f73eef8c19716061328c715b)) Update Titles from the Source ([#1115](https://github.com/Suwayomi/Suwayomi-Server/pull/1115) by @Syer10)
- ([r1606](https://github.com/Suwayomi/Suwayomi-Server/commit/0670f298cd70535c8b719af6bd86600a5124369c)) Switch from Kodein to Koin ([#1112](https://github.com/Suwayomi/Suwayomi-Server/pull/1112) by @Syer10)
- ([r1605](https://github.com/Suwayomi/Suwayomi-Server/commit/aa1e98544b8a11812675a27b4d95db6c7e33a283)) Fix/invalid server settings gql mutation request ([#1092](https://github.com/Suwayomi/Suwayomi-Server/pull/1092) by @schroda)
- ([r1604](https://github.com/Suwayomi/Suwayomi-Server/commit/fa4607e232fd94a8b299797cb23d0e630ccb8c04)) Update dependency gradle to v8.11 ([#1091](https://github.com/Suwayomi/Suwayomi-Server/pull/1091) by @renovate[bot])
- ([r1603](https://github.com/Suwayomi/Suwayomi-Server/commit/cb46420c09d08f9c7337830e2ea2e53bb0c8a711)) Update dependency com.android.tools.build:apksig to v8 ([#1076](https://github.com/Suwayomi/Suwayomi-Server/pull/1076) by @renovate[bot])
- ([r1602](https://github.com/Suwayomi/Suwayomi-Server/commit/d88014fa904421c213ceab0b59fe5139fa52d1e8)) Update xmlserialization ([#1075](https://github.com/Suwayomi/Suwayomi-Server/pull/1075) by @renovate[bot], @Syer10)
- ([r1601](https://github.com/Suwayomi/Suwayomi-Server/commit/168b76cb0c2c424f41380379b3da534c2ebcdd01)) Feature/graphql download queue subscription send only updates ([#1011](https://github.com/Suwayomi/Suwayomi-Server/pull/1011) by @schroda)
- ([r1600](https://github.com/Suwayomi/Suwayomi-Server/commit/f5680c6d695ddd58c2cf38538ec54cf44db3b857)) Switch to Koin from Injekt ([#1109](https://github.com/Suwayomi/Suwayomi-Server/pull/1109) by @schroda)
- ([r1599](https://github.com/Suwayomi/Suwayomi-Server/commit/654a3cc7ed6134afc76be529924317ff5c1f0a9b)) Use Backup.serializer() ([#1088](https://github.com/Suwayomi/Suwayomi-Server/pull/1088) by @Syer10)
- ([r1598](https://github.com/Suwayomi/Suwayomi-Server/commit/841cdc474f393433f33c928eb5e20b7bc35a1a01)) Remove Broken Sources and Broken History ([#1084](https://github.com/Suwayomi/Suwayomi-Server/pull/1084) by @Syer10)
- ([r1597](https://github.com/Suwayomi/Suwayomi-Server/commit/0adbea3a43c021c072e276b0bfe867527006a5bd)) Remove manga artist, author length limit ([#1080](https://github.com/Suwayomi/Suwayomi-Server/pull/1080) by @schroda)
- ([r1596](https://github.com/Suwayomi/Suwayomi-Server/commit/e12bada052d1622f68b9061a174afb9b273dbf9c)) Use correct sync id ([#1079](https://github.com/Suwayomi/Suwayomi-Server/pull/1079) by @schroda)
- ([r1595](https://github.com/Suwayomi/Suwayomi-Server/commit/68dbefc46f1313a306404e6780c4e987c8018bf1)) Update dependency gradle to v8.10.1 ([#1072](https://github.com/Suwayomi/Suwayomi-Server/pull/1072) by @renovate[bot])
- ([r1594](https://github.com/Suwayomi/Suwayomi-Server/commit/9d71e9b177cb88bfe1ec954af57593ce9f366651)) Update twelvemonkeys to v3.11.0 ([#1069](https://github.com/Suwayomi/Suwayomi-Server/pull/1069) by @renovate[bot])
- ([r1593](https://github.com/Suwayomi/Suwayomi-Server/commit/5b5801c2cfd096886871305cfbb915565ae9ec4f)) Update dependency com.squareup.okio:okio to v3.9.1 ([#1071](https://github.com/Suwayomi/Suwayomi-Server/pull/1071) by @renovate[bot])
- ([r1592](https://github.com/Suwayomi/Suwayomi-Server/commit/df1cc2b8e993f542719dfab23d0edcaf34228024)) fix(flaresolverr): fix cookie expiry for flaresolverr ([#1070](https://github.com/Suwayomi/Suwayomi-Server/pull/1070) by @Belphemur)
- ([r1591](https://github.com/Suwayomi/Suwayomi-Server/commit/18d399b3f748ca0863177a791befdc0096bf7610)) Update settings to v1.2.0 ([#1068](https://github.com/Suwayomi/Suwayomi-Server/pull/1068) by @renovate[bot])
- ([r1590](https://github.com/Suwayomi/Suwayomi-Server/commit/e9687fd18207941261d89f4b49f0d92b6e353a0f)) Update serialization to v1.7.2 ([#1067](https://github.com/Suwayomi/Suwayomi-Server/pull/1067) by @renovate[bot])
- ([r1589](https://github.com/Suwayomi/Suwayomi-Server/commit/79137a074cba1d07a3db46a7920a340e26118b0e)) Update plugin download to v5.6.0 ([#1066](https://github.com/Suwayomi/Suwayomi-Server/pull/1066) by @renovate[bot])
- ([r1588](https://github.com/Suwayomi/Suwayomi-Server/commit/dae55ca386a1313cb8f7b5aa6ee065de51d25a95)) Update graphqlkotlin to v6.8.5 ([#1064](https://github.com/Suwayomi/Suwayomi-Server/pull/1064) by @renovate[bot], @Syer10)
- ([r1587](https://github.com/Suwayomi/Suwayomi-Server/commit/b7f040d89aada015f381ab0ab69c8fb88d026407)) Update dependency org.jsoup:jsoup to v1.18.1 ([#1060](https://github.com/Suwayomi/Suwayomi-Server/pull/1060) by @renovate[bot])
- ([r1586](https://github.com/Suwayomi/Suwayomi-Server/commit/dc69df9f4fcb7895c80514630d0b1ead38362784)) Update dependency com.squareup.okio:okio to v3.9.0 ([#1056](https://github.com/Suwayomi/Suwayomi-Server/pull/1056) by @renovate[bot])
- ([r1585](https://github.com/Suwayomi/Suwayomi-Server/commit/6c1fbfa63b6b1dfebac59b0e59e153338cce654a)) [skip ci] Formatting (by @Syer10)
- ([r1584](https://github.com/Suwayomi/Suwayomi-Server/commit/e968a2195a325988fd1b05e7343d8bff08be0e37)) [skip ci] Update dependency com.pinterest.ktlint:ktlint-cli to v1.3.1 ([#1055](https://github.com/Suwayomi/Suwayomi-Server/pull/1055) by @renovate[bot])
- ([r1583](https://github.com/Suwayomi/Suwayomi-Server/commit/000bcea181c69a259a51901884596ee336704dbc)) [skip ci] Update SystemTray (by @Syer10)
- ([r1582](https://github.com/Suwayomi/Suwayomi-Server/commit/8edf508453949be7054d3b9c15922b79b7882463)) [skip ci] Update dependency org.apache.commons:commons-compress to v1.27.1 ([#1058](https://github.com/Suwayomi/Suwayomi-Server/pull/1058) by @renovate[bot])
- ([r1581](https://github.com/Suwayomi/Suwayomi-Server/commit/bc9cc50130c9b1a49aa35fff8c1867eb682ed604)) [skip ci] Update dependency org.bouncycastle:bcprov-jdk18on to v1.78.1 ([#1059](https://github.com/Suwayomi/Suwayomi-Server/pull/1059) by @renovate[bot])
- ([r1580](https://github.com/Suwayomi/Suwayomi-Server/commit/71091d88fc6f441d818a235bac7f436bc4ce857f)) [skip ci] Update dependency com.android.tools.build:apksig to v7.4.2 ([#1049](https://github.com/Suwayomi/Suwayomi-Server/pull/1049) by @renovate[bot])
- ([r1579](https://github.com/Suwayomi/Suwayomi-Server/commit/70c1d7e21f52b7beb12a70794b396522f1c44247)) [skip ci] Update dependency io.github.config4k:config4k to v0.7.0 ([#1057](https://github.com/Suwayomi/Suwayomi-Server/pull/1057) by @renovate[bot])
- ([r1578](https://github.com/Suwayomi/Suwayomi-Server/commit/c07920978ed33e5399a6116766a931f0f62a049f)) Update tachiyomiorg/issue-moderator-action action to v2 ([#1032](https://github.com/Suwayomi/Suwayomi-Server/pull/1032) by @renovate[bot], @Syer10)
- ([r1577](https://github.com/Suwayomi/Suwayomi-Server/commit/954b2919ac7b9e1d4dccfca68768c293cfd09095)) [skip ci] Update plugin ktlint to v11.6.1 ([#1043](https://github.com/Suwayomi/Suwayomi-Server/pull/1043) by @renovate[bot])
- ([r1576](https://github.com/Suwayomi/Suwayomi-Server/commit/c630f731ed90ba6581487ac7f295315b405593c2)) [skip ci] Update dependency androidx.annotation:annotation to v1.8.2 ([#1046](https://github.com/Suwayomi/Suwayomi-Server/pull/1046) by @renovate[bot])
- ([r1575](https://github.com/Suwayomi/Suwayomi-Server/commit/aaefa7f74e5daa96315ef10e8beead3d16c58acd)) [skip ci] Update coroutines to v1.8.1 ([#1045](https://github.com/Suwayomi/Suwayomi-Server/pull/1045) by @renovate[bot])
- ([r1574](https://github.com/Suwayomi/Suwayomi-Server/commit/2ec6b471f15aa08d204c81b217e786ad04f5e378)) [skip ci] Update dependency gradle to v8.10 ([#1047](https://github.com/Suwayomi/Suwayomi-Server/pull/1047) by @renovate[bot])
- ([r1573](https://github.com/Suwayomi/Suwayomi-Server/commit/a5c5ab68d278d6527731bdffb43a2830396de4dd)) [skip ci] Ktlint (by @Syer10)
- ([r1572](https://github.com/Suwayomi/Suwayomi-Server/commit/fb045c501af783e18bc05622bd207f37e5c25ae5)) Update Kotlin to 2.0.20 (by @Syer10)
- ([r1571](https://github.com/Suwayomi/Suwayomi-Server/commit/6a6e411492cd9c9c7a31a420124c74da89f74c0f)) [skip ci] Update dependency net.harawata:appdirs to v1.2.2 ([#1033](https://github.com/Suwayomi/Suwayomi-Server/pull/1033) by @renovate[bot])
- ([r1570](https://github.com/Suwayomi/Suwayomi-Server/commit/76aac330fc111c3748185e0605c7ef321aafd5fb)) [skip ci] Update dependency org.slf4j:slf4j-api to v2.0.16 ([#1034](https://github.com/Suwayomi/Suwayomi-Server/pull/1034) by @renovate[bot])
- ([r1569](https://github.com/Suwayomi/Suwayomi-Server/commit/07bdf31f66805dd09c962e4e0a542dce541371de)) [skip ci] Update okhttp monorepo to v5.0.0-alpha.14 ([#1039](https://github.com/Suwayomi/Suwayomi-Server/pull/1039) by @renovate[bot])
- ([r1568](https://github.com/Suwayomi/Suwayomi-Server/commit/fe14928af6b7ff7cbf339806f0880c6b64f56eb6)) [skip ci] Update rhino to v1.7.15 ([#1044](https://github.com/Suwayomi/Suwayomi-Server/pull/1044) by @renovate[bot])
- ([r1567](https://github.com/Suwayomi/Suwayomi-Server/commit/ad0c1033a4b087b6f2c13be3c8f9a5a3ad275002)) [skip ci] Update GitHub Artifact Actions to v4 ([#1040](https://github.com/Suwayomi/Suwayomi-Server/pull/1040) by @renovate[bot])
- ([r1566](https://github.com/Suwayomi/Suwayomi-Server/commit/0c2448fb99ca7cf77a6a02dd0a7a277ee7cfa2f9)) Update gradle build action ([#1035](https://github.com/Suwayomi/Suwayomi-Server/pull/1035) by @Syer10)
- ([r1565](https://github.com/Suwayomi/Suwayomi-Server/commit/cedda145a5caa16b89d767cc393efe52d40d7bee)) Update gradle/gradle-build-action action to v3 ([#1029](https://github.com/Suwayomi/Suwayomi-Server/pull/1029) by @renovate[bot])
- ([r1564](https://github.com/Suwayomi/Suwayomi-Server/commit/ee73187f1a092db69136a2a11daa40d2cf9df581)) [skip ci] Update gradle/wrapper-validation-action action to v3 ([#1030](https://github.com/Suwayomi/Suwayomi-Server/pull/1030) by @renovate[bot])
- ([r1563](https://github.com/Suwayomi/Suwayomi-Server/commit/89f91d680064e6c3330bdc83d2f34b0c6f1f92cd)) [skip ci] Update actions/checkout action to v4 ([#1028](https://github.com/Suwayomi/Suwayomi-Server/pull/1028) by @renovate[bot])
- ([r1562](https://github.com/Suwayomi/Suwayomi-Server/commit/6714827694842ff55085495d016049936d74e99e)) [skip ci] Update softprops/action-gh-release action to v2 ([#1031](https://github.com/Suwayomi/Suwayomi-Server/pull/1031) by @renovate[bot])
- ([r1561](https://github.com/Suwayomi/Suwayomi-Server/commit/aad73f7d199173d4c75dab3759753e8250c25f91)) [skip ci] Update dependency io.mockk:mockk to v1.13.12 ([#1026](https://github.com/Suwayomi/Suwayomi-Server/pull/1026) by @renovate[bot])
- ([r1560](https://github.com/Suwayomi/Suwayomi-Server/commit/c5985de1c34839a561c421b978e90f11825c39e5)) [skip ci] Update dependency com.typesafe:config to v1.4.3 ([#1025](https://github.com/Suwayomi/Suwayomi-Server/pull/1025) by @renovate[bot])
- ([r1559](https://github.com/Suwayomi/Suwayomi-Server/commit/86a5b0879a9d65589dfe0b241c8a2539a30e9a20)) Add renovate.json ([#1024](https://github.com/Suwayomi/Suwayomi-Server/pull/1024) by @renovate[bot])
- ([r1558](https://github.com/Suwayomi/Suwayomi-Server/commit/414972d54588b7d034902292b5b188b248e1fe27)) Feature/update log file rotation ([#1023](https://github.com/Suwayomi/Suwayomi-Server/pull/1023) by @schroda)
- ([r1557](https://github.com/Suwayomi/Suwayomi-Server/commit/9a74ae5844fd059bf4e8e921a36e48348aa635ca)) feat(comicinfo): add date fields to comic info ([#1021](https://github.com/Suwayomi/Suwayomi-Server/pull/1021) by @Belphemur, @Syer10)
- ([r1556](https://github.com/Suwayomi/Suwayomi-Server/commit/301980ab1439605a69c47a04425d34e1c07cfffd)) fix(flaresolverr): support possible rewrite flaresolverr ([#1020](https://github.com/Suwayomi/Suwayomi-Server/pull/1020) by @Belphemur)
- ([r1555](https://github.com/Suwayomi/Suwayomi-Server/commit/5dced82e5a51929b1e6d65ee8a669564c21d02c9)) Fix/missed automated task execution failure crashes server on startup ([#1019](https://github.com/Suwayomi/Suwayomi-Server/pull/1019) by @schroda)
- ([r1554](https://github.com/Suwayomi/Suwayomi-Server/commit/9a1e4df4084987744ffee37347542c5bbc83a1c9)) Fix/server startup blocked by synchronous tasks ([#1018](https://github.com/Suwayomi/Suwayomi-Server/pull/1018) by @schroda)
- ([r1553](https://github.com/Suwayomi/Suwayomi-Server/commit/5b08b8123939e6befe739056569bc7afc50c665b)) Initialize manga on add to library ([#1016](https://github.com/Suwayomi/Suwayomi-Server/pull/1016) by @schroda)
- ([r1552](https://github.com/Suwayomi/Suwayomi-Server/commit/7fac538ba3c873d1ba69de40ab6c1111f77d3714)) Initialize uninitialized manga during global update ([#1015](https://github.com/Suwayomi/Suwayomi-Server/pull/1015) by @schroda)
- ([r1551](https://github.com/Suwayomi/Suwayomi-Server/commit/ef6be74ec25d0b76db9dc55c986605a90916af36)) Fix/chapter downloaded check ([#1012](https://github.com/Suwayomi/Suwayomi-Server/pull/1012) by @schroda)
- ([r1550](https://github.com/Suwayomi/Suwayomi-Server/commit/9f4958724552441bf4587d11e4f5cbca9f29c858)) [skip ci] Update readme ([#1008](https://github.com/Suwayomi/Suwayomi-Server/pull/1008) by @schroda)
- ([r1549](https://github.com/Suwayomi/Suwayomi-Server/commit/b7b733f351ef3ab0ad69d21812c4d3834ca84185)) [skip ci] Feature/update readme ([#1005](https://github.com/Suwayomi/Suwayomi-Server/pull/1005) by @schroda)
- ([r1548](https://github.com/Suwayomi/Suwayomi-Server/commit/06bfc33e72de3879b3b5da75ada6d98aa0ad18af)) Always update uninitialized manga in global update ([#997](https://github.com/Suwayomi/Suwayomi-Server/pull/997) by @schroda)
- ([r1547](https://github.com/Suwayomi/Suwayomi-Server/commit/fbcd55d6c53d1b7be1ec282ed61648d8fa8467b8)) Add "hasDuplicatedChapters" field to gql MangaType ([#995](https://github.com/Suwayomi/Suwayomi-Server/pull/995) by @schroda)
- ([r1546](https://github.com/Suwayomi/Suwayomi-Server/commit/2484b5f14b0ca14e36f89a798810302825aa9bfa)) Fix/downloaded chapters with lost page count ([#994](https://github.com/Suwayomi/Suwayomi-Server/pull/994) by @schroda)
- ([r1545](https://github.com/Suwayomi/Suwayomi-Server/commit/69826596580768e4927c09c9d3f45d4135e9e56c)) Fix/inserting duplicated chapters into database ([#991](https://github.com/Suwayomi/Suwayomi-Server/pull/991) by @schroda)
- ([r1544](https://github.com/Suwayomi/Suwayomi-Server/commit/9e006166a8af73d7c281f0cb3d51cfabc1b39822)) Add setting to use the flaresolverr response ([#990](https://github.com/Suwayomi/Suwayomi-Server/pull/990) by @AeonLucid)
- ([r1543](https://github.com/Suwayomi/Suwayomi-Server/commit/25a62e33a1893b07689ff5741f7e14051ba202b4)) Fix PersistentCookieStore for domains with an underscore ([#989](https://github.com/Suwayomi/Suwayomi-Server/pull/989) by @AeonLucid)
- ([r1542](https://github.com/Suwayomi/Suwayomi-Server/commit/d05ed0a56c60e4c49d4160b15a30ab713653b55a)) Fix SOCKS5 authentication by setting a default Authenticator ([#988](https://github.com/Suwayomi/Suwayomi-Server/pull/988) by @AeonLucid)
- ([r1541](https://github.com/Suwayomi/Suwayomi-Server/commit/eaffb2755cfae436f4c2b1affe9c125046c2cc55)) Cleanup only created auto backup files ([#984](https://github.com/Suwayomi/Suwayomi-Server/pull/984) by @schroda)
- ([r1540](https://github.com/Suwayomi/Suwayomi-Server/commit/e0fcae2ae35c3d6d4702041b6f489f208f6533ef)) Handle deprecated gql sort again ([#983](https://github.com/Suwayomi/Suwayomi-Server/pull/983) by @schroda)
- ([r1539](https://github.com/Suwayomi/Suwayomi-Server/commit/af9ad611744ced0766cdae980888afec5697201f)) Feature/gql simpilify filtering for multiple values ([#960](https://github.com/Suwayomi/Suwayomi-Server/pull/960) by @schroda)
- ([r1538](https://github.com/Suwayomi/Suwayomi-Server/commit/7c54ad54fc96d37c9d8b59d0dcf2347ebffb7a98)) Sort gql queries by multiple columns ([#963](https://github.com/Suwayomi/Suwayomi-Server/pull/963) by @schroda)
- ([r1537](https://github.com/Suwayomi/Suwayomi-Server/commit/aee9f1032c05e3493124d078ac8a4c0acc5778e9)) Exit early in case of empty chapter id list ([#980](https://github.com/Suwayomi/Suwayomi-Server/pull/980) by @schroda)
- ([r1536](https://github.com/Suwayomi/Suwayomi-Server/commit/f7d0605e0a8b3fb296c8ef186bc67a7ee650a567)) [skip ci] Remove docker commands ([#972](https://github.com/Suwayomi/Suwayomi-Server/pull/972) by @Robonau)
Contributors:
@schroda, @kaaass, @Syer10, @renovate[bot], @ShirishSaxena, @showyee, @cpiber, @zeedif, @Robonau, @dejavui, @Belphemur, @AeonLucid
## [Suwayomi-WebUI Changelog](https://github.com/Suwayomi/Suwayomi-WebUI/blob/master/CHANGELOG.md#v151-r2467)
# Server: v1.1.1 + WevUI: v1.1.0
## TL;DR
- WebUI update bugfixes
## Suwayomi-Server Changelog
- ([r1534](https://github.com/Suwayomi/Suwayomi-Server/commit/d9cb54b28593e4df87522090f03a6e5b9c7d9fa2)) Compare webUI version with bundled webUI version ([#969](https://github.com/Suwayomi/Suwayomi-Server/pull/969) by @schroda)
- ([r1533](https://github.com/Suwayomi/Suwayomi-Server/commit/f738a162d3cd4582612d4986b3d3887e1c309bdd)) Support for "STABLEPREVIEW" webUI version ([#970](https://github.com/Suwayomi/Suwayomi-Server/pull/970) by @schroda)
# Server: v1.1.0 + WevUI: v1.1.0
## TL;DR
- Update Manga Info in browse
- Full Tracking support
- Import Tracking from backups
- Improved support for library filters
- Improved thumbnail handling
- Many minor bugfixes
- WebUI changes: https://github.com/Suwayomi/Suwayomi-WebUI/releases/tag/v1.1.0
## Suwayomi-Server Changelog
- ([r1531](https://github.com/Suwayomi/Suwayomi-Server/commit/ecd1604e25a17a6ef68d568b5d81e69f6e9f7702)) Update metadata in source browse only if new data is not null ([#962](https://github.com/Suwayomi/Suwayomi-Server/pull/962) by @schroda)
- ([r1530](https://github.com/Suwayomi/Suwayomi-Server/commit/0f061900afbd4036b4d081bcb3f39e4f60160ac3)) Fix browse source ([#961](https://github.com/Suwayomi/Suwayomi-Server/pull/961) by @Syer10)
- ([r1529](https://github.com/Suwayomi/Suwayomi-Server/commit/c47f5ea85e75c7119d828a19eda392b2fb9faecd)) [skip ci] doc: add NixOS installation ([#959](https://github.com/Suwayomi/Suwayomi-Server/pull/959) by @RatCornu)
- ([r1528](https://github.com/Suwayomi/Suwayomi-Server/commit/306eb0e3c774b5a8d0d2d4ade2d7091904fe6e58)) Update manga info when browsing if not in library ([#958](https://github.com/Suwayomi/Suwayomi-Server/pull/958) by @Syer10)
- ([r1527](https://github.com/Suwayomi/Suwayomi-Server/commit/e64025ded814a15129999586133d84085e0c5779)) Correctly set name of logger ([#956](https://github.com/Suwayomi/Suwayomi-Server/pull/956) by @schroda)
- ([r1526](https://github.com/Suwayomi/Suwayomi-Server/commit/c1fe2da636b675091ec0ac93162764883938ffc6)) Fix/failing thumbnail requests with http 410 ([#955](https://github.com/Suwayomi/Suwayomi-Server/pull/955) by @schroda)
- ([r1525](https://github.com/Suwayomi/Suwayomi-Server/commit/ff23f58a4f4e8e0b4d459957f0e0701265e0c364)) Support partial mutation responses ([#954](https://github.com/Suwayomi/Suwayomi-Server/pull/954) by @schroda)
- ([r1524](https://github.com/Suwayomi/Suwayomi-Server/commit/fc2f5ffdf9c1e8675f9b978031a49fb4ce3af601)) Fix/failing track progress update for logged out trackers ([#953](https://github.com/Suwayomi/Suwayomi-Server/pull/953) by @schroda)
- ([r1523](https://github.com/Suwayomi/Suwayomi-Server/commit/6dd9ed7fb0816b2f163bec43d04c433099e7e529)) Fix/prevent importing unsupported trackers from backup II ([#945](https://github.com/Suwayomi/Suwayomi-Server/pull/945) by @schroda)
- ([r1522](https://github.com/Suwayomi/Suwayomi-Server/commit/2f362abb91be875e943b1364eb86d70a4144dd6f)) Prevent importing unsupported tracker from backup ([#944](https://github.com/Suwayomi/Suwayomi-Server/pull/944) by @schroda)
- ([r1521](https://github.com/Suwayomi/Suwayomi-Server/commit/96807a64cf1b13b6db655d46e90c42717170ce62)) [skip ci] Update README.md ([#941](https://github.com/Suwayomi/Suwayomi-Server/pull/941) by @FumoVite)
- ([r1520](https://github.com/Suwayomi/Suwayomi-Server/commit/7df5f1c4c4408cfbbd56697ba10f018393df2b4a)) Feature/backup tracking ([#940](https://github.com/Suwayomi/Suwayomi-Server/pull/940) by @schroda)
- ([r1519](https://github.com/Suwayomi/Suwayomi-Server/commit/cf1ede9cf70a2d72a7ff84b9ead24a394ceee2ce)) Update lastPageRead on chapter update ([#939](https://github.com/Suwayomi/Suwayomi-Server/pull/939) by @schroda)
- ([r1518](https://github.com/Suwayomi/Suwayomi-Server/commit/729385588a3d8e06ec8be38865a12c47e88f6bcb)) Prevent greater last page read than page count ([#938](https://github.com/Suwayomi/Suwayomi-Server/pull/938) by @schroda)
- ([r1517](https://github.com/Suwayomi/Suwayomi-Server/commit/668d5cf8f02e35cc53d1430a239ae67837c64f51)) Prevent IndexOutOfBoundsException when removing duplicated chapters ([#935](https://github.com/Suwayomi/Suwayomi-Server/pull/935) by @schroda)
- ([r1516](https://github.com/Suwayomi/Suwayomi-Server/commit/72b1b5b0f9b86f82a0e203802d9a4b6339277c01)) Exit track progress update early in case new chapter is same as current local ([#937](https://github.com/Suwayomi/Suwayomi-Server/pull/937) by @schroda)
- ([r1515](https://github.com/Suwayomi/Suwayomi-Server/commit/fbf726c17434212cdf94b39f52a25a0050d77287)) Use "AsyncExecutionStrategy" for mutations ([#932](https://github.com/Suwayomi/Suwayomi-Server/pull/932) by @schroda)
- ([r1514](https://github.com/Suwayomi/Suwayomi-Server/commit/c441eed84773fdc295e6d004e4f4628453b54659)) Exclude duplicated chapters from auto download limit ([#923](https://github.com/Suwayomi/Suwayomi-Server/pull/923) by @schroda)
- ([r1513](https://github.com/Suwayomi/Suwayomi-Server/commit/e8e83ed49caac2d25f29073d1bd3b5b385aa2d98)) Remove duplicated mangas from gql "mangas" query ([#924](https://github.com/Suwayomi/Suwayomi-Server/pull/924) by @schroda)
- ([r1512](https://github.com/Suwayomi/Suwayomi-Server/commit/cdc21b067c1a341d68ea7a9c1ee565dc3959f552)) Fix/recognition of already downloaded chapters ([#922](https://github.com/Suwayomi/Suwayomi-Server/pull/922) by @schroda)
- ([r1511](https://github.com/Suwayomi/Suwayomi-Server/commit/48e19f7914fee1ea1789b217d5df9b05acb49203)) Feature/auto download of new chapters improve handling of unhandable reuploads ([#921](https://github.com/Suwayomi/Suwayomi-Server/pull/921) by @schroda)
- ([r1510](https://github.com/Suwayomi/Suwayomi-Server/commit/89dd570b3057bee34643858b4a42bfac7d88a82b)) Add mutation to fetch the latest track data from the tracker ([#920](https://github.com/Suwayomi/Suwayomi-Server/pull/920) by @schroda, @Syer10)
- ([r1509](https://github.com/Suwayomi/Suwayomi-Server/commit/16474d4328651f1236722556b7f59628a0f9dbda)) Feature/tracking gql add option to delete remote binding on tracker ([#919](https://github.com/Suwayomi/Suwayomi-Server/pull/919) by @schroda, @Syer10)
- ([r1508](https://github.com/Suwayomi/Suwayomi-Server/commit/9db612bf0317950d0291047b9ee64a0787e49bf2)) Move trigger for track progress update to client ([#918](https://github.com/Suwayomi/Suwayomi-Server/pull/918) by @schroda)
- ([r1507](https://github.com/Suwayomi/Suwayomi-Server/commit/7d92dbc5c0a47176099eb310eaf17a4788ba2ce4)) Fix/tracking progress update in case local chapter is smaller than remote ([#917](https://github.com/Suwayomi/Suwayomi-Server/pull/917) by @schroda)
- ([r1506](https://github.com/Suwayomi/Suwayomi-Server/commit/a9efca86870cec6d74f58535e2e007eb6c8831c2)) Add chapter bookmark count field to MangaType ([#912](https://github.com/Suwayomi/Suwayomi-Server/pull/912) by @schroda)
- ([r1505](https://github.com/Suwayomi/Suwayomi-Server/commit/dbfea5d02b898884fdeb2be2959fe8a73a465704)) Update inLibraryAt timestamp when adding manga to library ([#911](https://github.com/Suwayomi/Suwayomi-Server/pull/911) by @schroda)
- ([r1504](https://github.com/Suwayomi/Suwayomi-Server/commit/a6b05c4a2759d0d5f834a54cad6c8417fe49a0d2)) Feature/refresh outdated thumbnail url on fetch failure ([#910](https://github.com/Suwayomi/Suwayomi-Server/pull/910) by @schroda)
- ([r1503](https://github.com/Suwayomi/Suwayomi-Server/commit/6d539d34040c4e95692b57ce4fedfbeaa73083d0)) Fix/update subscription clear data loader cache ([#908](https://github.com/Suwayomi/Suwayomi-Server/pull/908) by @schroda)
- ([r1502](https://github.com/Suwayomi/Suwayomi-Server/commit/b2aff1efc9e6527e70ba519e5171096394e6ccf7)) Fix MAL after restarting the server ([#903](https://github.com/Suwayomi/Suwayomi-Server/pull/903) by @Syer10)
- ([r1501](https://github.com/Suwayomi/Suwayomi-Server/commit/8a20a1ef5094efc05426ed420bbde40358fdf2dd)) Add first unread chapter field to MangaType ([#900](https://github.com/Suwayomi/Suwayomi-Server/pull/900) by @schroda)
- ([r1500](https://github.com/Suwayomi/Suwayomi-Server/commit/33cbfa9751c3ef7a6babfcff9595782cbac5acae)) Fix/electron launch error not logged ([#895](https://github.com/Suwayomi/Suwayomi-Server/pull/895) by @schroda)
- ([r1499](https://github.com/Suwayomi/Suwayomi-Server/commit/b95a8d44d4bb7c94a04e66b3d6cc0fc101f4880b)) Always fetch thumbnail of manga from local source ([#898](https://github.com/Suwayomi/Suwayomi-Server/pull/898) by @schroda)
## [Suwayomi-WebUI Changelog](https://github.com/Suwayomi/Suwayomi-WebUI/blob/master/CHANGELOG.md#v110-r1689)
# Server: v1.0.0 + WevUI: r1409 # Server: v1.0.0 + WevUI: r1409
## TL;DR ## TL;DR
- GraphQL API - GraphQL API
+4 -6
View File
@@ -8,9 +8,9 @@ Checkout [This Kanban Board](https://github.com/Suwayomi/Suwayomi-Server/project
- We hate big pull requests, make them as small as possible, change one meaningful thing. Spam pull requests, we don't mind. - We hate big pull requests, make them as small as possible, change one meaningful thing. Spam pull requests, we don't mind.
### Project goals and vision ### Project goals and vision
- Porting Tachiyomi and covering its features - Porting Mihon (Tachiyomi) and covering its features
- Syncing with Tachiyomi, [main issue](https://github.com/Suwayomi/Suwayomi-Server/issues/159) - Syncing with Mihon (Tachiyomi), [main issue](https://github.com/Suwayomi/Suwayomi-Server/issues/159)
- Generally rejecting features that Tachiyomi(main app) doesn't have, - Generally rejecting features that Mihon (Tachiyomi) (main app) doesn't have,
- Unless it's something that makes sense for desktop sizes or desktop form factor (keyboard + mouse) - Unless it's something that makes sense for desktop sizes or desktop form factor (keyboard + mouse)
- Additional/crazy features can go in forks and alternative clients - Additional/crazy features can go in forks and alternative clients
- [Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI) should - [Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI) should
@@ -19,13 +19,11 @@ Checkout [This Kanban Board](https://github.com/Suwayomi/Suwayomi-Server/project
## How does Suwayomi-Server work? ## How does Suwayomi-Server work?
This project has two components: 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 jar libraries converted from apk extensions. All this concludes to serving a GraphQL API. 1. **Server:** contains the implementation of [Mihon (Tachiyomi)'s source library](https://github.com/mihonapp/mihon/tree/main/source-api) and uses an Android compatibility library to run jar libraries converted from apk extensions. All this concludes to serving a GraphQL 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/Suwayomi-WebUI 2. **WebUI:** A React SPA(`create-react-app`) project that works with the server to do the presentation located at https://github.com/Suwayomi/Suwayomi-WebUI
### API ### API
#### GraphQL #### GraphQL
*Only available in the preview at the moment*
The GraphQL API can be queried with a POST request to `/api/graphql`. There is also the GraphiQL IDE accessible by the browser at `/api/graphql` to perform ad-hoc queries and explore the API. The GraphQL API can be queried with a POST request to `/api/graphql`. There is also the GraphiQL IDE accessible by the browser at `/api/graphql` to perform ad-hoc queries and explore the API.
#### REST #### REST
+52 -37
View File
@@ -5,8 +5,8 @@
## Table of Content ## Table of Content
- [What is Suwayomi?](#what-is-suwayomi) - [What is Suwayomi?](#what-is-suwayomi)
- [Features](#Features)
- [Suwayomi client projects](#Suwayomi-client-projects) - [Suwayomi client projects](#Suwayomi-client-projects)
* [Is this application usable? Should I test it?](#is-this-application-usable-should-i-test-it)
- [Downloading and Running the app](#downloading-and-running-the-app) - [Downloading and Running the app](#downloading-and-running-the-app)
* [Using Operating System Specific Bundles](#using-operating-system-specific-bundles) * [Using Operating System Specific Bundles](#using-operating-system-specific-bundles)
- [Launcher Scripts](#launcher-scripts) - [Launcher Scripts](#launcher-scripts)
@@ -20,7 +20,7 @@
* [Advanced Methods](#advanced-methods) * [Advanced Methods](#advanced-methods)
+ [Running the jar release directly](#running-the-jar-release-directly) + [Running the jar release directly](#running-the-jar-release-directly)
+ [Using Suwayomi Remotely](#using-suwayomi-remotely) + [Using Suwayomi Remotely](#using-suwayomi-remotely)
- [Syncing With Tachiyomi](#syncing-with-tachiyomi) - [Syncing With Mihon (Tachiyomi)](#syncing-with-mihon-tachiyomi)
- [Troubleshooting and Support](#troubleshooting-and-support) - [Troubleshooting and Support](#troubleshooting-and-support)
- [Contributing and Technical info](#contributing-and-technical-info) - [Contributing and Technical info](#contributing-and-technical-info)
- [Credit](#credit) - [Credit](#credit)
@@ -30,41 +30,48 @@
# What is Suwayomi? # What is Suwayomi?
<img src="https://github.com/Suwayomi/Suwayomi-Server/raw/master/server/src/main/resources/icon/faviconlogo.png" alt="drawing" width="200"/> <img src="https://github.com/Suwayomi/Suwayomi-Server/raw/master/server/src/main/resources/icon/faviconlogo.png" alt="drawing" width="200"/>
A free and open source manga reader server that runs extensions built for [Tachiyomi](https://tachiyomi.org/). A free and open source manga reader server that runs extensions built for [Mihon (Tachiyomi)](https://mihon.app/).
Suwayomi is an independent Tachiyomi compatible software and is **not a Fork of** Tachiyomi. Suwayomi is an independent Mihon (Tachiyomi) compatible software and is **not a Fork of** Mihon (Tachiyomi).
Suwayomi-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. Suwayomi-Server is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it. This includes Windows, Linux, macOS, chrome OS, etc. Follow [Downloading and Running the app](#downloading-and-running-the-app) for installation instructions.
Ability to sync with Tachiyomi is a planned feature, for more info look [here](#syncing-with-tachiyomi). You can use Mihon (Tachiyomi) to access your Suwayomi-Server. For more info look [here](#syncing-with-mihon-tachiyomi).
## Features
> [!NOTE]
>
> These are capabilities of Suwayomi-Server, the actual working support is provided by each front-end app, checkout their respective readme for more info.
- Installing and executing Mihon (Tachiyomi)'s Extensions, So you'll get the same sources
- Searching and browsing installed sources
- A library to save your mangas and categories to put them into
- Automated library updates to check for new chapters
- Automated download of new chapters
- Viewing latest updated chapters
- Ability to download Manga for offline read
- Backup and restore support powered by Mihon (Tachiyomi)-compatible Backups
- Automated backup creations
- Tracking via [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [MangaUpdates](https://www.mangaupdates.com/)
- [FlareSolverr](https://github.com/FlareSolverr/FlareSolverr) support to bypass Cloudflare protection
- Automated WebUI updates (supports the default WebUI and VUI)
# Suwayomi client projects # Suwayomi client projects
**You need a client/user interface app as a front-end for Suwayomi-Server, if you [Directly Download Suwayomi-Server](https://github.com/Suwayomi/Suwayomi-Server/releases/latest) you'll get a bundled version of [Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI) with it.** **You need a client/user interface app as a front-end for Suwayomi-Server, if you [Directly Download Suwayomi-Server](https://github.com/Suwayomi/Suwayomi-Server/releases/latest) you'll get a bundled version of [Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI) with it.**
Here's a list of known clients/user interfaces for Suwayomi-Server: Here's a list of known clients/user interfaces for Suwayomi-Server (checkout the respective GitHub repository for their features):
##### Actively Developed Clients ##### Actively Developed Clients
- [Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI): The web/ElectronJS front-end that Suwayomi-Server ships with by default. - [Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI): The web front-end that Suwayomi-Server ships with by default.
- [Tachidesk-JUI](https://github.com/Suwayomi/Tachidesk-JUI): The native desktop front-end for Suwayomi-Server. Currently, the most advanced. - [Suwayomi-VUI](https://github.com/Suwayomi/Suwayomi-VUI): A Suwayomi-Server preview focused web frontend built with svelte
- [Tachidesk-Sorayomi](https://github.com/Suwayomi/Tachidesk-Sorayomi): A Flutter front-end for Desktop(Linux, windows, etc.), Web and Android with a User Interface inspired by Tachiyomi.
- [Tachidesk-VaadinUI](https://github.com/Suwayomi/Tachidesk-VaadinUI): A Web front-end for Suwayomi-Server built with Vaadin. - [Tachidesk-VaadinUI](https://github.com/Suwayomi/Tachidesk-VaadinUI): A Web front-end for Suwayomi-Server built with Vaadin.
- [Suwayomi-VUI](https://github.com/Suwayomi/Suwayomi-VUI): A preview focused web frontend built with svelte with some features the other UIs might not have (migration) ##### Inactive Clients (functional but outdated)
##### Inctive/Abandoned Clients - [Tachidesk-JUI](https://github.com/Suwayomi/Tachidesk-JUI): The native desktop front-end for Suwayomi-Server.
- [Tachidesk-Sorayomi](https://github.com/Suwayomi/Tachidesk-Sorayomi): A Flutter front-end for Desktop(Linux, windows, etc.), Web and Android with a User Interface inspired by Mihon (Tachiyomi).
##### Abandoned Clients (functionality unknown)
- [Tachidesk-qtui](https://github.com/Suwayomi/Tachidesk-qtui): A C++/Qt front-end for mobile devices(Android/linux), feature support is basic. - [Tachidesk-qtui](https://github.com/Suwayomi/Tachidesk-qtui): A C++/Qt front-end for mobile devices(Android/linux), feature support is basic.
- [Tachidesk-GTK](https://github.com/mahor1221/Tachidesk-GTK): A native Rust/GTK desktop client. - [Tachidesk-GTK](https://github.com/mahor1221/Tachidesk-GTK): A native Rust/GTK desktop client.
- [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js. - [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js.
## 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
- Ability to download Manga for offline read
- Backup and restore support powered by Tachiyomi-compatible Backups
- Viewing latest updated chapters.
**Note:** These are capabilities of Suwayomi-Server, the actual working support is provided by each front-end app, checkout their respective readme for more info.
# Downloading and Running the app # Downloading and Running the app
## Using Operating System Specific Bundles ## Using Operating System Specific Bundles
To facilitate the use of Suwayomi we provide bundle releases that include The Java Runtime Environment, ElectronJS and the Suwayomi-Launcher. To facilitate the use of Suwayomi we provide bundle releases that include The Java Runtime Environment, ElectronJS and the Suwayomi-Launcher.
@@ -72,7 +79,7 @@ To facilitate the use of Suwayomi we provide bundle releases that include The Ja
If a bundle for your operating system or cpu architecture is not provided then refer to [Advanced Methods](#advanced-methods) If a bundle for your operating system or cpu architecture is not provided then refer to [Advanced Methods](#advanced-methods)
### Windows ### Windows
Download the latest `win32`(Windows 32-bit) or `win64`(Windows 64-bit) release from [the releases section](https://github.com/Suwayomi/Suwayomi-Server/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Suwayomi-Server-preview/releases). Download the latest `win64`(Windows 64-bit) release from [the releases section](https://github.com/Suwayomi/Suwayomi-Server/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Suwayomi-Server-preview/releases).
Unzip the downloaded file and double-click on one of the launcher scripts. Unzip the downloaded file and double-click on one of the launcher scripts.
@@ -87,6 +94,9 @@ Download the latest `linux-x64`(x86_64) release from [the releases section](http
`tar xvf` the downloaded file and double-click on one of the launcher scripts or run them using the terminal. `tar xvf` the downloaded file and double-click on one of the launcher scripts or run them using the terminal.
## Other methods of getting Suwayomi ## Other methods of getting Suwayomi
### Docker
Check our Official Docker release [Suwayomi Container](https://github.com/orgs/Suwayomi/packages/container/package/tachidesk) for running Suwayomi Server in a docker container. Source code for our container is available at [docker-tachidesk](https://github.com/Suwayomi/docker-tachidesk), an example compose file can also be found there. By default, the server will be running on http://localhost:4567 open this url in your browser.
### Arch Linux ### Arch Linux
You can install Suwayomi from the AUR: You can install Suwayomi from the AUR:
``` ```
@@ -108,23 +118,26 @@ sudo apt update
sudo apt install suwayomi-server sudo apt install suwayomi-server
``` ```
### Docker ### NixOS
Check our Official Docker release [Suwayomi Container](https://github.com/orgs/Suwayomi/packages/container/package/tachidesk) for running Suwayomi 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. You can deploy Suwayomi on NixOS using the module `services.suwayomi-server` in your configuration:
Install from the command line:
``` ```
$ docker pull ghcr.io/suwayomi/tachidesk {
``` services.suwayomi-server = {
Run Container from the command line: enable = true;
``` };
$ docker run -p 4567:4567 ghcr.io/suwayomi/tachidesk }
``` ```
For more information, see [the NixOS manual](https://nixos.org/manual/nixos/stable/#module-services-suwayomi-server).
You can also directly use the package from [nixpkgs](https://search.nixos.org/packages?channel=unstable&type=packages&query=suwayomi-server).
## Advanced Methods ## Advanced Methods
### Running the jar release directly ### Running the jar release directly
In order to run the app you need the following: In order to run the app you need the following:
- The jar release of Suwayomi-Server - The jar release of Suwayomi-Server
- The Java Runtime Environment(JRE) 8 or newer - The Java Runtime Environment(JRE) 21 or newer
- A Browser like Google Chrome, Firefox, Edge, etc. - A Browser like Google Chrome, Firefox, Edge, etc.
- ElectronJS (optional) - ElectronJS (optional)
@@ -139,13 +152,15 @@ Check out [this wiki page](https://github.com/Suwayomi/Suwayomi-Server/wiki/Conf
If you face issues with your setup then we are happy to provide help, just join our discord server(a discord badge is on the top of the page, you are just a click-clack away!). If you face issues with your setup then we are happy to provide help, just join our discord server(a discord badge is on the top of the page, you are just a click-clack away!).
## Syncing With Tachiyomi ## Syncing With Mihon (Tachiyomi)
### The Suwayomi extension and tracker ### The Suwayomi extension and tracker
- You can install the `Suwayomi` extension inside tachiyomi. - You can install the `Suwayomi` extension inside Mihon (Tachiyomi).
- The extension will load your Suwayomi library. - The extension will load your Suwayomi library.
- By manipulating extension search filters you can browse your categories. - By manipulating extension search filters you can browse your categories.
- You can enable the Suwayomi tracker to track reading progress with your Suwayomi server. - You can enable the Suwayomi tracker to track reading progress with your Suwayomi server.
- Note: Tachiyomi [only allows tracking one way](https://github.com/tachiyomiorg/tachiyomi/issues/1626), meaning that by reading chapters on other Suwayomi clients the last read chapter number will update on the tracker but tachiyomi won't automatically mark them as read for you. - Note: to sync from
- Mihon (Tachiyomi) to Suwayomi: Mihon (Tachiyomi) automatically updates the chapters read status when it's updating the tracker (e.g. while reading)
- Suwayomi to Mihon (Tachiyomi): To sync Mihon (Tachiyomi) with Suwayomi, you have to open the manga's track information, then, Mihon (Tachiyomi) will automatically update its chapter list with the state from Suwayomi
### Other methods ### Other methods
Checkout [this issue](https://github.com/Suwayomi/Suwayomi-Server/issues/159) for tracking progress. Checkout [this issue](https://github.com/Suwayomi/Suwayomi-Server/issues/159) for tracking progress.
@@ -161,7 +176,7 @@ This project is a spiritual successor of [TachiWeb-Server](https://github.com/Ta
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`. 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` and `Copyright 2015 Javier Tomás`. Parts of [Mihon (Tachiyomi)](https://github.com/mihonapp/mihon) 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 You can obtain a copy of `Apache License Version 2.0` from http://www.apache.org/licenses/LICENSE-2.0
+6 -8
View File
@@ -1,3 +1,4 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
import org.jlleitschuh.gradle.ktlint.KtlintExtension import org.jlleitschuh.gradle.ktlint.KtlintExtension
import org.jlleitschuh.gradle.ktlint.KtlintPlugin import org.jlleitschuh.gradle.ktlint.KtlintPlugin
@@ -26,8 +27,8 @@ allprojects {
subprojects { subprojects {
plugins.withType<JavaPlugin> { plugins.withType<JavaPlugin> {
extensions.configure<JavaPluginExtension> { extensions.configure<JavaPluginExtension> {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_21
} }
} }
@@ -43,12 +44,9 @@ subprojects {
tasks { tasks {
withType<KotlinJvmCompile> { withType<KotlinJvmCompile> {
dependsOn("ktlintFormat") dependsOn("ktlintFormat")
kotlinOptions { compilerOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString() jvmTarget = JvmTarget.JVM_21
freeCompilerArgs.add("-Xcontext-receivers")
freeCompilerArgs += listOf(
"-Xcontext-receivers",
)
} }
} }
} }
+9 -7
View File
@@ -10,14 +10,13 @@ import java.io.BufferedReader
const val MainClass = "suwayomi.tachidesk.MainKt" const val MainClass = "suwayomi.tachidesk.MainKt"
// should be bumped with each stable release // should be bumped with each stable release
val tachideskVersion = System.getenv("ProductVersion") ?: "v1.0.0" val getTachideskVersion = { "v2.0.${getCommitCount()}" }
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r1409" val webUIRevisionTag = "r2467"
// counts commits on the current checked out branch private val getCommitCount = {
val getTachideskRevision = {
runCatching { runCatching {
System.getenv("ProductRevision") ?: ProcessBuilder() ProcessBuilder()
.command("git", "rev-list", "HEAD", "--count") .command("git", "rev-list", "HEAD", "--count")
.start() .start()
.let { process -> .let { process ->
@@ -26,8 +25,11 @@ val getTachideskRevision = {
it.bufferedReader().use(BufferedReader::readText) it.bufferedReader().use(BufferedReader::readText)
} }
process.destroy() process.destroy()
"r" + output.trim() output.trim()
} }
}.getOrDefault("r0") }.getOrDefault("0")
} }
// counts commits on the current checked out branch
val getTachideskRevision = { "r${getCommitCount()}" }
+51 -45
View File
@@ -1,18 +1,19 @@
[versions] [versions]
kotlin = "1.9.10" kotlin = "2.1.20"
coroutines = "1.7.3" coroutines = "1.10.1"
serialization = "1.6.0" serialization = "1.8.0"
okhttp = "5.0.0-alpha.11" # Major version is locked by Tachiyomi extensions okhttp = "5.0.0-alpha.14" # Major version is locked by Tachiyomi extensions
javalin = "4.6.8" # Javalin 5.0.0+ requires Java 11 javalin = "6.5.0"
jackson = "2.13.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency` jackson = "2.18.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
exposed = "0.40.1" exposed = "0.59.0"
dex2jar = "v64" # Stuck until https://github.com/ThexXTURBOXx/dex2jar/issues/27 is fixed dex2jar = "v64" # Stuck until https://github.com/ThexXTURBOXx/dex2jar/issues/27 is fixed
rhino = "1.7.14" polyglot = "24.2.0"
settings = "1.0.0-RC" settings = "1.3.0"
twelvemonkeys = "3.9.4" twelvemonkeys = "3.12.0"
graphqlkotlin = "6.5.6" graphqlkotlin = "8.4.0"
xmlserialization = "0.86.2" xmlserialization = "0.90.3"
ktlint = "1.0.0" ktlint = "1.5.0"
koin = "4.0.2"
[libraries] [libraries]
# Kotlin # Kotlin
@@ -29,20 +30,20 @@ coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", ve
serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" } serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
serialization-json-okio = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-okio", version.ref = "serialization" } serialization-json-okio = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-okio", version.ref = "serialization" }
serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "serialization" } serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "serialization" }
serialization-xml-core = { module = "io.github.pdvrieze.xmlutil:core-jvm", version.ref = "xmlserialization" } serialization-xml-core = { module = "io.github.pdvrieze.xmlutil:core", version.ref = "xmlserialization" }
serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization-jvm", version.ref = "xmlserialization" } serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization-jvm", version.ref = "xmlserialization" }
# Logging # Logging
slf4japi = "org.slf4j:slf4j-api:2.0.9" slf4japi = "org.slf4j:slf4j-api:2.0.17"
logback = "ch.qos.logback:logback-classic:1.3.11" logback = "ch.qos.logback:logback-classic:1.5.18"
kotlinlogging = "io.github.microutils:kotlin-logging:3.0.5" kotlinlogging = "io.github.oshai:kotlin-logging-jvm:7.0.5"
# OkHttp # OkHttp
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp" } okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp" }
okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", version.ref = "okhttp" } okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", version.ref = "okhttp" }
okio = "com.squareup.okio:okio:3.3.0" okio = "com.squareup.okio:okio:3.10.2"
# Javalin api # Javalin api
javalin-core = { module = "io.javalin:javalin", version.ref = "javalin" } javalin-core = { module = "io.javalin:javalin", version.ref = "javalin" }
@@ -54,7 +55,8 @@ jackson-annotations = { module = "com.fasterxml.jackson.core:jackson-annotations
# GraphQL # GraphQL
graphql-kotlin-server = { module = "com.expediagroup:graphql-kotlin-server", version.ref = "graphqlkotlin" } graphql-kotlin-server = { module = "com.expediagroup:graphql-kotlin-server", version.ref = "graphqlkotlin" }
graphql-kotlin-scheme = { module = "com.expediagroup:graphql-kotlin-schema-generator", version.ref = "graphqlkotlin" } graphql-kotlin-scheme = { module = "com.expediagroup:graphql-kotlin-schema-generator", version.ref = "graphqlkotlin" }
graphql-scalars = "com.graphql-java:graphql-java-extended-scalars:20.2" graphql-java-core = "com.graphql-java:graphql-java:22.3" # Major version locked by graphql-kotlin
graphql-java-scalars = "com.graphql-java:graphql-java-extended-scalars:22.0"
# Exposed ORM # Exposed ORM
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" } exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
@@ -64,24 +66,24 @@ exposed-javatime = { module = "org.jetbrains.exposed:exposed-java-time", version
h2 = "com.h2database:h2:1.4.200" # current database driver, can't update to h2 v2 without sql migration h2 = "com.h2database:h2:1.4.200" # current database driver, can't update to h2 v2 without sql migration
# Exposed Migrations # Exposed Migrations
exposed-migrations = "com.github.Suwayomi:exposed-migrations:3.2.0" exposed-migrations = "com.github.Suwayomi:exposed-migrations:3.7.0"
# Dependency Injection # Dependency Injection
kodein = "org.kodein.di:kodein-di-conf-jvm:7.20.2" koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
# tray icon # tray icon
systemtray-core = "com.dorkbox:SystemTray:4.2.1" systemtray-core = "com.dorkbox:SystemTray:4.4"
systemtray-utils = "com.dorkbox:Utilities:1.39" # version locked by SystemTray systemtray-utils = "com.dorkbox:Utilities:1.46" # version locked by SystemTray
systemtray-desktop = "com.dorkbox:Desktop:1.0" systemtray-desktop = "com.dorkbox:Desktop:1.1" # version locked by SystemTray
# dependencies of Tachiyomi extensions # dependencies of Tachiyomi extensions
injekt = "com.github.inorichi.injekt:injekt-core:65b0440" injekt = "com.github.null2264:injekt-koin:ee267b2e27"
rxjava = "io.reactivex:rxjava:1.3.8" rxjava = "io.reactivex:rxjava:1.3.8"
jsoup = "org.jsoup:jsoup:1.16.1" jsoup = "org.jsoup:jsoup:1.19.1"
# Config # Config
config = "com.typesafe:config:1.4.2" config = "com.typesafe:config:1.4.3"
config4k = "io.github.config4k:config4k:0.5.0" config4k = "io.github.config4k:config4k:0.7.0"
# Sort # Sort
sort = "com.github.gpanther:java-nat-sort:natural-comparator-1.1" sort = "com.github.gpanther:java-nat-sort:natural-comparator-1.1"
@@ -96,33 +98,34 @@ dex2jar-tools = { module = "com.github.ThexXTURBOXx.dex2jar:dex-tools", version.
# APK # APK
apk-parser = "net.dongliu:apk-parser:2.6.10" apk-parser = "net.dongliu:apk-parser:2.6.10"
apksig = "com.android.tools.build:apksig:7.2.1" apksig = "com.android.tools.build:apksig:8.9.0"
# Xml # Xml
xmlpull = "xmlpull:xmlpull:1.1.3.4a" xmlpull = "xmlpull:xmlpull:1.1.3.4a"
# Disk & File # Disk & File
appdirs = "net.harawata:appdirs:1.2.1" appdirs = "net.harawata:appdirs:1.4.0"
cache4k = "io.github.reactivecircus.cache4k:cache4k:0.14.0"
zip4j = "net.lingala.zip4j:zip4j:2.11.5" zip4j = "net.lingala.zip4j:zip4j:2.11.5"
commonscompress = "org.apache.commons:commons-compress:1.24.0" commonscompress = "org.apache.commons:commons-compress:1.27.1"
junrar = "com.github.junrar:junrar:7.5.5" junrar = "com.github.junrar:junrar:7.5.5"
# AES/CBC/PKCS7Padding Cypher provider # AES/CBC/PKCS7Padding Cypher provider
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.76" bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.80"
# AndroidX annotations # AndroidX annotations
android-annotations = "androidx.annotation:annotation:1.7.0" android-annotations = "androidx.annotation:annotation:1.9.1"
# Substitute for duktape-android # Substitute for duktape-android
rhino-runtime = { module = "org.mozilla:rhino-runtime", version.ref = "rhino" } # slimmer version of 'org.mozilla:rhino' polyglot-core = { module = "org.graalvm.polyglot:polyglot", version.ref = "polyglot" }
rhino-engine = { module = "org.mozilla:rhino-engine", version.ref = "rhino" } # provides the same interface as 'javax.script' a.k.a Nashorn polyglot-graaljs = { module = "org.graalvm.polyglot:js-community", version.ref = "polyglot" }
# Settings # Settings
settings-core = { module = "com.russhwolf:multiplatform-settings-jvm", version.ref = "settings" } settings-core = { module = "com.russhwolf:multiplatform-settings-jvm", version.ref = "settings" }
settings-serialization = { module = "com.russhwolf:multiplatform-settings-serialization-jvm", version.ref = "settings" } settings-serialization = { module = "com.russhwolf:multiplatform-settings-serialization-jvm", version.ref = "settings" }
# ICU4J # ICU4J
icu4j = "com.ibm.icu:icu4j:73.2" icu4j = "com.ibm.icu:icu4j:77.1"
# Image Decoding implementation provider # Image Decoding implementation provider
twelvemonkeys-common-lang = { module = "com.twelvemonkeys.common:common-lang", version.ref = "twelvemonkeys" } twelvemonkeys-common-lang = { module = "com.twelvemonkeys.common:common-lang", version.ref = "twelvemonkeys" }
@@ -134,7 +137,7 @@ twelvemonkeys-imageio-jpeg = { module = "com.twelvemonkeys.imageio:imageio-jpeg"
twelvemonkeys-imageio-webp = { module = "com.twelvemonkeys.imageio:imageio-webp", version.ref = "twelvemonkeys" } twelvemonkeys-imageio-webp = { module = "com.twelvemonkeys.imageio:imageio-webp", version.ref = "twelvemonkeys" }
# Testing # Testing
mockk = "io.mockk:mockk:1.13.7" mockk = "io.mockk:mockk:1.13.17"
# cron scheduler # cron scheduler
cron4j = "it.sauronsoftware.cron4j:cron4j:2.2.5" cron4j = "it.sauronsoftware.cron4j:cron4j:2.2.5"
@@ -142,19 +145,22 @@ cron4j = "it.sauronsoftware.cron4j:cron4j:2.2.5"
# cron-utils # cron-utils
cronUtils = "com.cronutils:cron-utils:9.2.1" cronUtils = "com.cronutils:cron-utils:9.2.1"
# lint - used for renovate to update ktlint version
ktlint = { module = "com.pinterest.ktlint:ktlint-cli", version.ref = "ktlint" }
[plugins] [plugins]
# Kotlin # Kotlin
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin"} kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin"}
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"} kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"}
# Linter # Linter
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version = "11.6.0"} ktlint = { id = "org.jlleitschuh.gradle.ktlint", version = "12.2.0"}
# Build config # Build config
buildconfig = { id = "com.github.gmazzo.buildconfig", version = "3.1.0"} buildconfig = { id = "com.github.gmazzo.buildconfig", version = "5.5.4"}
# Download # Download
download = { id = "de.undercouch.download", version = "5.4.0"} download = { id = "de.undercouch.download", version = "5.6.0"}
# ShadowJar # ShadowJar
shadowjar = { id = "com.github.johnrengelman.shadow", version = "8.1.1"} shadowjar = { id = "com.github.johnrengelman.shadow", version = "8.1.1"}
@@ -168,7 +174,7 @@ shared = [
"serialization-json", "serialization-json",
"serialization-json-okio", "serialization-json-okio",
"serialization-protobuf", "serialization-protobuf",
"kodein", "koin-core",
"slf4japi", "slf4japi",
"logback", "logback",
"kotlinlogging", "kotlinlogging",
@@ -196,7 +202,7 @@ okhttp = [
] ]
javalin = [ javalin = [
"javalin-core", "javalin-core",
"javalin-openapi", #"javalin-openapi",
] ]
jackson = [ jackson = [
"jackson-databind", "jackson-databind",
@@ -214,9 +220,9 @@ systemtray = [
"systemtray-utils", "systemtray-utils",
"systemtray-desktop" "systemtray-desktop"
] ]
rhino = [ polyglot = [
"rhino-runtime", "polyglot-core",
"rhino-engine", "polyglot-graaljs",
] ]
settings = [ settings = [
"settings-core", "settings-core",
Binary file not shown.
+3 -1
View File
@@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
Vendored
+24 -13
View File
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# SPDX-License-Identifier: Apache-2.0
#
############################################################################## ##############################################################################
# #
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,13 +82,11 @@ do
esac esac
done done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # This is normally unused
# shellcheck disable=SC2034
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@@ -133,22 +133,29 @@ location of your Java installation."
fi fi
else else
JAVACMD=java JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
case $MAX_FD in #( case $MAX_FD in #(
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac
@@ -193,11 +200,15 @@ if "$cygwin" || "$msys" ; then
done done
fi fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
# shell script including quotes and variable substitutions, so put them in DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded. # Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
Vendored
+13 -10
View File
@@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and @rem See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@@ -26,6 +28,7 @@ if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@@ -42,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
@@ -56,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
+21
View File
@@ -0,0 +1,21 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
],
"semanticCommits": "disabled",
"customManagers": [
{
"customType": "regex",
"fileMatch": [
"scripts/bundler.sh"
],
"matchStrings": [
"JRE_RELEASE=[\"'](?<currentValue>.+?)[\"']\\s+"
],
"datasourceTemplate": "github-releases",
"depNameTemplate": "adoptium/temurin21-binaries",
"versioningTemplate": "regex:^jdk-?(?<major>\\d+).(?<minor>\\d+).+?(?<patch>[\\d+]+)$"
}
]
}
+55 -56
View File
@@ -28,7 +28,7 @@ main() {
OS="$1" OS="$1"
JAR="$(ls server/build/*.jar | tail -n1)" JAR="$(ls server/build/*.jar | tail -n1)"
RELEASE_NAME="$(echo "${JAR%.*}" | xargs basename)-$OS" RELEASE_NAME="$(echo "${JAR%.*}" | xargs basename)-$OS"
RELEASE_VERSION="$(tmp="${JAR%-*}"; echo "${tmp##*-}" | tr -d v)" RELEASE_VERSION=$(echo "$JAR" | grep -oP "v\K[0-9]+\.[0-9]+\.[0-9]+")
#RELEASE_REVISION_NUMBER="$(tmp="${JAR%.*}" && echo "${tmp##*-}" | tr -d r)" #RELEASE_REVISION_NUMBER="$(tmp="${JAR%.*}" && echo "${tmp##*-}" | tr -d r)"
local electron_version="v28.1.3" local electron_version="v28.1.3"
@@ -51,74 +51,64 @@ main() {
move_release_to_output_dir move_release_to_output_dir
;; ;;
linux-x64) linux-x64)
# https://github.com/adoptium/temurin8-binaries/releases/ # https://github.com/adoptium/temurin21-binaries/releases/
JRE_RELEASE="jdk8u392-b08" JRE_RELEASE="jdk-21.0.6+7"
JRE="OpenJDK8U-jre_x64_linux_hotspot_$(echo "$JRE_RELEASE" | sed 's/jdk//;s/-//g').tar.gz" JRE="OpenJDK21U-jre_x64_linux_hotspot_$(echo "$JRE_RELEASE" | sed 's/jdk//;s/-//g;s/+/_/g').tar.gz"
JRE_DIR="$JRE_RELEASE-jre" JRE_DIR="$JRE_RELEASE-jre"
JRE_URL="https://github.com/adoptium/temurin8-binaries/releases/download/$JRE_RELEASE/$JRE" JRE_URL="https://github.com/adoptium/temurin21-binaries/releases/download/$JRE_RELEASE/$JRE"
ELECTRON="electron-$electron_version-linux-x64.zip" ELECTRON="electron-$electron_version-linux-x64.zip"
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON" ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
download_jre_and_electron download_electron
setup_jre
tree "$RELEASE_NAME"
RELEASE="$RELEASE_NAME.tar.gz" RELEASE="$RELEASE_NAME.tar.gz"
make_linux_bundle make_linux_bundle
move_release_to_output_dir move_release_to_output_dir
;; ;;
macOS-x64) macOS-x64)
# https://github.com/adoptium/temurin8-binaries/releases/ # https://github.com/adoptium/temurin21-binaries/releases/
JRE_RELEASE="jdk8u392-b08" JRE_RELEASE="jdk-21.0.6+7"
JRE="OpenJDK8U-jre_x64_mac_hotspot_$(echo "$JRE_RELEASE" | sed 's/jdk//;s/-//g').tar.gz" JRE="OpenJDK21U-jre_x64_mac_hotspot_$(echo "$JRE_RELEASE" | sed 's/jdk//;s/-//g;s/+/_/g').tar.gz"
JRE_DIR="$JRE_RELEASE-jre" JRE_DIR="$JRE_RELEASE-jre"
JRE_URL="https://github.com/adoptium/temurin8-binaries/releases/download/$JRE_RELEASE/$JRE" JRE_URL="https://github.com/adoptium/temurin21-binaries/releases/download/$JRE_RELEASE/$JRE"
ELECTRON="electron-$electron_version-darwin-x64.zip" ELECTRON="electron-$electron_version-darwin-x64.zip"
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON" ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
download_jre_and_electron download_electron
setup_jre
tree "$RELEASE_NAME"
RELEASE="$RELEASE_NAME.zip" RELEASE="$RELEASE_NAME.tar.gz"
make_macos_bundle make_macos_bundle
move_release_to_output_dir move_release_to_output_dir
;; ;;
macOS-arm64) macOS-arm64)
# https://cdn.azul.com/zulu/bin/ # https://github.com/adoptium/temurin21-binaries/releases/
JRE="zulu8.74.0.17-ca-jre8.0.392-macosx_aarch64.tar.gz" JRE_RELEASE="jdk-21.0.6+7"
JRE_RELEASE="zulu8.74.0.17-ca-jre8.0.392-macosx_aarch64" JRE="OpenJDK21U-jre_aarch64_mac_hotspot_$(echo "$JRE_RELEASE" | sed 's/jdk//;s/-//g;s/+/_/g').tar.gz"
JRE_DIR="$JRE_RELEASE/zulu-8.jre" JRE_DIR="$JRE_RELEASE-jre"
JRE_URL="https://cdn.azul.com/zulu/bin/$JRE" JRE_URL="https://github.com/adoptium/temurin21-binaries/releases/download/$JRE_RELEASE/$JRE"
ELECTRON="electron-$electron_version-darwin-arm64.zip" ELECTRON="electron-$electron_version-darwin-arm64.zip"
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON" ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
download_jre_and_electron download_electron
setup_jre
tree "$RELEASE_NAME"
RELEASE="$RELEASE_NAME.zip" RELEASE="$RELEASE_NAME.tar.gz"
make_macos_bundle make_macos_bundle
move_release_to_output_dir move_release_to_output_dir
;; ;;
windows-x86)
# https://github.com/adoptium/temurin8-binaries/releases/
JRE_RELEASE="jdk8u392-b08"
JRE="OpenJDK8U-jre_x86-32_windows_hotspot_$(echo "$JRE_RELEASE" | sed 's/jdk//;s/-//g').zip"
JRE_DIR="$JRE_RELEASE-jre"
JRE_URL="https://github.com/adoptium/temurin8-binaries/releases/download/$JRE_RELEASE/$JRE"
ELECTRON="electron-$electron_version-win32-ia32.zip"
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
download_jre_and_electron
RELEASE="$RELEASE_NAME.zip"
make_windows_bundle
move_release_to_output_dir
RELEASE="$RELEASE_NAME.msi"
make_windows_package
move_release_to_output_dir
;;
windows-x64) windows-x64)
# https://github.com/adoptium/temurin8-binaries/releases/ # https://github.com/adoptium/temurin21-binaries/releases/
JRE_RELEASE="jdk8u392-b08" JRE_RELEASE="jdk-21.0.6+7"
JRE="OpenJDK8U-jre_x64_windows_hotspot_$(echo "$JRE_RELEASE" | sed 's/jdk//;s/-//g').zip" JRE="OpenJDK21U-jre_x64_windows_hotspot_$(echo "$JRE_RELEASE" | sed 's/jdk//;s/-//g;s/+/_/g').zip"
JRE_DIR="$JRE_RELEASE-jre" JRE_DIR="$JRE_RELEASE-jre"
JRE_URL="https://github.com/adoptium/temurin8-binaries/releases/download/$JRE_RELEASE/$JRE" JRE_URL="https://github.com/adoptium/temurin21-binaries/releases/download/$JRE_RELEASE/$JRE"
ELECTRON="electron-$electron_version-win32-x64.zip" ELECTRON="electron-$electron_version-win32-x64.zip"
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON" ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
download_jre_and_electron download_electron
setup_jre
tree "$RELEASE_NAME"
RELEASE="$RELEASE_NAME.zip" RELEASE="$RELEASE_NAME.zip"
make_windows_bundle make_windows_bundle
@@ -148,26 +138,32 @@ download_launcher() {
mv "Suwayomi-Launcher.jar" "$RELEASE_NAME/Suwayomi-Launcher.jar" mv "Suwayomi-Launcher.jar" "$RELEASE_NAME/Suwayomi-Launcher.jar"
} }
download_jre_and_electron() { download_electron() {
if [ ! -f "$JRE" ]; then
curl -L "$JRE_URL" -o "$JRE"
fi
if [ ! -f "$ELECTRON" ]; then if [ ! -f "$ELECTRON" ]; then
curl -L "$ELECTRON_URL" -o "$ELECTRON" curl -L "$ELECTRON_URL" -o "$ELECTRON"
fi fi
local ext="${JRE##*.}"
if [ "$ext" = "zip" ]; then
unzip "$JRE"
else
tar xvf "$JRE"
fi
mv "$JRE_DIR" "$RELEASE_NAME/jre"
unzip "$ELECTRON" -d "$RELEASE_NAME/electron/" unzip "$ELECTRON" -d "$RELEASE_NAME/electron/"
}
mkdir "$RELEASE_NAME/bin" setup_jre() {
if [ -d "jre" ]; then
chmod +x ./jre/bin/java
chmod +x ./jre/lib/jspawnhelper
mv "jre" "$RELEASE_NAME/jre"
else
if [ ! -f "$JRE" ]; then
curl -L "$JRE_URL" -o "$JRE"
fi
tree local ext="${JRE##*.}"
if [ "$ext" = "zip" ]; then
unzip "$JRE"
else
tar xvf "$JRE"
fi
mv "$JRE_DIR" "$RELEASE_NAME/jre"
fi
} }
copy_linux_package_assets_to() { copy_linux_package_assets_to() {
@@ -184,6 +180,7 @@ copy_linux_package_assets_to() {
} }
make_linux_bundle() { make_linux_bundle() {
mkdir "$RELEASE_NAME/bin"
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar" cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar"
cp "scripts/resources/suwayomi-launcher.sh" "$RELEASE_NAME/" cp "scripts/resources/suwayomi-launcher.sh" "$RELEASE_NAME/"
cp "scripts/resources/suwayomi-server.sh" "$RELEASE_NAME/" cp "scripts/resources/suwayomi-server.sh" "$RELEASE_NAME/"
@@ -192,10 +189,11 @@ make_linux_bundle() {
} }
make_macos_bundle() { make_macos_bundle() {
mkdir "$RELEASE_NAME/bin"
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar" cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar"
cp "scripts/resources/Suwayomi Launcher.command" "$RELEASE_NAME/" cp "scripts/resources/Suwayomi Launcher.command" "$RELEASE_NAME/"
zip -9 -r "$RELEASE" "$RELEASE_NAME/" tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/"
} }
# https://wiki.debian.org/SimplePackagingTutorial # https://wiki.debian.org/SimplePackagingTutorial
@@ -255,6 +253,7 @@ make_windows_bundle() {
#WINEARCH=win32 wine "$rcedit" "$RELEASE_NAME/electron/electron.exe" \ #WINEARCH=win32 wine "$rcedit" "$RELEASE_NAME/electron/electron.exe" \
# --set-icon "$icon" # --set-icon "$icon"
mkdir "$RELEASE_NAME/bin"
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar" cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar"
cp "scripts/resources/Suwayomi Launcher.bat" "$RELEASE_NAME" cp "scripts/resources/Suwayomi Launcher.bat" "$RELEASE_NAME"
+1 -1
View File
@@ -1,3 +1,3 @@
cd "`dirname "$0"`" cd "`dirname "$0"`"
./jre/Contents/Home/bin/java -jar Suwayomi-Launcher.jar ./jre/bin/java -jar Suwayomi-Launcher.jar
+1 -1
View File
@@ -8,7 +8,7 @@ Homepage: https://github.com/Suwayomi/Suwayomi-Server
Package: suwayomi-server Package: suwayomi-server
Architecture: all Architecture: all
Depends: ${misc:Depends}, java8-runtime, libc++-dev Depends: ${misc:Depends}, openjdk-21-jre, libc++-dev
Description: Manga Reader Description: Manga Reader
A free and open source manga reader server that runs extensions built for Tachiyomi. A free and open source manga reader server that runs extensions built for Tachiyomi.
Suwayomi is an independent Tachiyomi compatible software and is not a Fork of Tachiyomi. Suwayomi is an independent Tachiyomi compatible software and is not a Fork of Tachiyomi.
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"> <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product Id="*" UpgradeCode="*" <Product Id="*" UpgradeCode="174c8f36-0bec-4585-9ddd-469c3d889dc1"
Version="$(var.ProductVersion)" Language="1033" Name="Suwayomi Server" Manufacturer="Suwayomi"> Version="$(var.ProductVersion)" Language="1033" Name="Suwayomi Server" Manufacturer="Suwayomi">
<Package InstallerVersion="300" Compressed="yes" /> <Package InstallerVersion="300" Compressed="yes" />
<Media Id="1" Cabinet="Suwayomi_Server.cab" EmbedCab="yes" /> <Media Id="1" Cabinet="Suwayomi_Server.cab" EmbedCab="yes" />
@@ -9,6 +9,8 @@
VersionNT64 VersionNT64
</Condition> </Condition>
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
<!-- Directory --> <!-- Directory -->
<Directory Id="TARGETDIR" Name="SourceDir"> <Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFiles64Folder"> <Directory Id="ProgramFiles64Folder">
@@ -48,6 +50,10 @@
</Component> </Component>
</DirectoryRef> </DirectoryRef>
<InstallExecuteSequence>
<RemoveExistingProducts After="InstallValidate" />
</InstallExecuteSequence>
<!-- Feature --> <!-- Feature -->
<Feature Id="Suwayomi_Server" Title="Suwayomi-Server" Level="1"> <Feature Id="Suwayomi_Server" Title="Suwayomi-Server" Level="1">
<ComponentGroupRef Id="jre" /> <ComponentGroupRef Id="jre" />
@@ -1,60 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product Id="*" UpgradeCode="*"
Version="$(var.ProductVersion)" Language="1033" Name="Suwayomi Server" Manufacturer="Suwayomi">
<Package InstallerVersion="300" Compressed="yes" />
<Media Id="1" Cabinet="Suwayomi_Server.cab" EmbedCab="yes" />
<!-- Directory -->
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFilesFolder">
<Directory Id="INSTALLDIR" Name="Suwayomi-Server" >
<Directory Id="jre"/>
<Directory Id="electron"/>
<Directory Id="bin"/>
</Directory>
</Directory>
<Directory Id="ProgramMenuFolder">
<Directory Id="ProgramMenuDir" Name="Suwayomi-Server">
<Component Id="ProgramMenuDir" Guid="*">
<RemoveFolder Id="ProgramMenuDir" On="uninstall"/>
<RegistryValue Root="HKCU" Key="Software\[Manufacturer]\[ProductName]" Type="string" Value="" KeyPath="yes"/>
</Component>
</Directory>
</Directory>
<Directory Id="DesktopFolder" />
</Directory>
<!-- Component -->
<DirectoryRef Id="INSTALLDIR">
<Component Id="SuwayomiJAR" Guid="*">
<File Id="Suwayomi-Launcher.jar" Source="$(var.SourceDir)/Suwayomi-Launcher.jar" KeyPath="yes" />
</Component>
<Component Id="SuwayomiLauncherBAT" Guid="*" Win64="yes">
<File Id="SuwayomiLauncher.bat" Source="$(var.SourceDir)/Suwayomi Launcher.bat" KeyPath="yes" >
<Shortcut Id="SuwayomiLauncher.lnk" Name="Suwayomi Launcher" Directory="INSTALLDIR"
WorkingDirectory="INSTALLDIR" Icon="Suwayomi.ico" IconIndex="0" Advertise="yes" />
<Shortcut Id="DesktopSuwayomiLauncher.lnk" Name="Suwayomi Launcher" Directory="DesktopFolder"
WorkingDirectory="INSTALLDIR" Icon="Suwayomi.ico" IconIndex="0" Advertise="yes" />
<Shortcut Id="ProgramMenuSuwayomiLauncher.lnk" Name="Suwayomi Launcher" Directory="ProgramMenuDir"
WorkingDirectory="INSTALLDIR" Icon="Suwayomi.ico" IconIndex="0" Advertise="yes"
Description="A free and open source manga reader that runs extensions built for Tachiyomi." />
</File>
</Component>
</DirectoryRef>
<!-- Feature -->
<Feature Id="Suwayomi_Server" Title="Suwayomi-Server" Level="1">
<ComponentGroupRef Id="jre" />
<ComponentGroupRef Id="bin" />
<ComponentRef Id="SuwayomiJAR" />
<ComponentRef Id="SuwayomiLauncherBAT" />
<ComponentRef Id="ProgramMenuDir" />
<ComponentGroupRef Id="electron" />
</Feature>
<Icon Id="Suwayomi.ico" SourceFile="$(var.Icon)" />
<Property Id="ARPPRODUCTICON" Value="Suwayomi.ico" /> <!-- Icon in Add/Remove Programs -->
</Product>
</Wix>
+33 -15
View File
@@ -3,12 +3,28 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
import java.time.Instant import java.time.Instant
plugins { plugins {
id(libs.plugins.kotlin.jvm.get().pluginId) id(
id(libs.plugins.kotlin.serialization.get().pluginId) libs.plugins.kotlin.jvm
id(libs.plugins.ktlint.get().pluginId) .get()
.pluginId,
)
id(
libs.plugins.kotlin.serialization
.get()
.pluginId,
)
id(
libs.plugins.ktlint
.get()
.pluginId,
)
application application
alias(libs.plugins.shadowjar) alias(libs.plugins.shadowjar)
id(libs.plugins.buildconfig.get().pluginId) id(
libs.plugins.buildconfig
.get()
.pluginId,
)
} }
dependencies { dependencies {
@@ -27,7 +43,8 @@ dependencies {
// GraphQL // GraphQL
implementation(libs.graphql.kotlin.server) implementation(libs.graphql.kotlin.server)
implementation(libs.graphql.kotlin.scheme) implementation(libs.graphql.kotlin.scheme)
implementation(libs.graphql.scalars) implementation(libs.graphql.java.core)
implementation(libs.graphql.java.scalars)
// Exposed ORM // Exposed ORM
implementation(libs.bundles.exposed) implementation(libs.bundles.exposed)
@@ -39,7 +56,7 @@ dependencies {
// tray icon // tray icon
implementation(libs.bundles.systemtray) implementation(libs.bundles.systemtray)
// dependencies of Tachiyomi extensions, some are duplicate, keeping it here for reference // dependencies of Mihon (Tachiyomi) extensions, some are duplicate, keeping it here for reference
implementation(libs.injekt) implementation(libs.injekt)
implementation(libs.okhttp.core) implementation(libs.okhttp.core)
implementation(libs.rxjava) implementation(libs.rxjava)
@@ -56,6 +73,7 @@ dependencies {
implementation(libs.asm) implementation(libs.asm)
// Disk & File // Disk & File
implementation(libs.cache4k)
implementation(libs.zip4j) implementation(libs.zip4j)
implementation(libs.commonscompress) implementation(libs.commonscompress)
implementation(libs.junrar) implementation(libs.junrar)
@@ -103,7 +121,7 @@ buildConfig {
fun quoteWrap(obj: Any): String = """"$obj"""" fun quoteWrap(obj: Any): String = """"$obj""""
buildConfigField("String", "NAME", quoteWrap(rootProject.name)) buildConfigField("String", "NAME", quoteWrap(rootProject.name))
buildConfigField("String", "VERSION", quoteWrap(tachideskVersion)) buildConfigField("String", "VERSION", quoteWrap(getTachideskVersion()))
buildConfigField("String", "REVISION", quoteWrap(getTachideskRevision())) buildConfigField("String", "REVISION", quoteWrap(getTachideskRevision()))
buildConfigField("String", "BUILD_TYPE", quoteWrap(if (System.getenv("ProductBuildType") == "Stable") "Stable" else "Preview")) buildConfigField("String", "BUILD_TYPE", quoteWrap(if (System.getenv("ProductBuildType") == "Stable") "Stable" else "Preview"))
buildConfigField("long", "BUILD_TIME", Instant.now().epochSecond.toString()) buildConfigField("long", "BUILD_TIME", Instant.now().epochSecond.toString())
@@ -122,14 +140,15 @@ tasks {
"Main-Class" to MainClass, "Main-Class" to MainClass,
"Implementation-Title" to rootProject.name, "Implementation-Title" to rootProject.name,
"Implementation-Vendor" to "The Suwayomi Project", "Implementation-Vendor" to "The Suwayomi Project",
"Specification-Version" to tachideskVersion, "Specification-Version" to getTachideskVersion(),
"Implementation-Version" to getTachideskRevision(), "Implementation-Version" to getTachideskRevision(),
) )
} }
archiveBaseName.set(rootProject.name) archiveBaseName.set(rootProject.name)
archiveVersion.set(tachideskVersion) archiveVersion.set(getTachideskVersion())
archiveClassifier.set(getTachideskRevision()) archiveClassifier.set("")
destinationDirectory.set(File("$rootDir/server/build")) destinationDirectory.set(File("$rootDir/server/build"))
mergeServiceFiles()
} }
test { test {
@@ -141,11 +160,10 @@ tasks {
} }
withType<KotlinJvmCompile> { withType<KotlinJvmCompile> {
kotlinOptions { compilerOptions {
freeCompilerArgs += freeCompilerArgs.add(
listOf( "-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi", )
)
} }
} }
@@ -9,16 +9,10 @@ package eu.kanade.tachiyomi
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.InjektScope
import uy.kohesive.injekt.registry.default.DefaultRegistrar
open class App : Application() { open class App : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Injekt = InjektScope(DefaultRegistrar())
Injekt.importModule(AppModule(this))
// if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) // if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
} }
@@ -15,7 +15,12 @@ object AppInfo {
* *
* @since extension-lib 1.3 * @since extension-lib 1.3
*/ */
fun getVersionCode() = BuildConfig.REVISION.substring(1).toInt() fun getVersionCode() =
BuildConfig.VERSION
.replace("v", "")
.split('.')
.joinToString("")
.toInt()
/** /**
* should be something like "0.13.1" * should be something like "0.13.1"
@@ -20,21 +20,15 @@ import eu.kanade.tachiyomi.network.JavaScriptEngine
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.protobuf.ProtoBuf import kotlinx.serialization.protobuf.ProtoBuf
import nl.adaptivity.xmlutil.XmlDeclMode
import nl.adaptivity.xmlutil.core.XmlVersion
import nl.adaptivity.xmlutil.serialization.XML import nl.adaptivity.xmlutil.serialization.XML
import org.kodein.di.DI import org.koin.core.module.Module
import org.kodein.di.conf.global import org.koin.dsl.module
import org.kodein.di.instance
import rx.Observable
import rx.schedulers.Schedulers
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addSingleton
import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
class AppModule(val app: Application) : InjektModule { fun createAppModule(app: Application): Module {
override fun InjektRegistrar.registerInjectables() { return module {
addSingleton(app) single { app }
// addSingletonFactory { PreferencesHelper(app) } // addSingletonFactory { PreferencesHelper(app) }
// //
@@ -44,9 +38,9 @@ class AppModule(val app: Application) : InjektModule {
// //
// addSingletonFactory { CoverCache(app) } // addSingletonFactory { CoverCache(app) }
addSingletonFactory { NetworkHelper(app) } single { NetworkHelper(app) }
addSingletonFactory { JavaScriptEngine(app) } single { JavaScriptEngine(app) }
// addSingletonFactory { SourceManager(app).also { get<ExtensionManager>().init(it) } } // addSingletonFactory { SourceManager(app).also { get<ExtensionManager>().init(it) } }
// //
@@ -58,36 +52,38 @@ class AppModule(val app: Application) : InjektModule {
// //
// addSingletonFactory { LibrarySyncManager(app) } // addSingletonFactory { LibrarySyncManager(app) }
addSingletonFactory { single {
val json by DI.global.instance<Json>() Json {
json ignoreUnknownKeys = true
explicitNulls = false
}
} }
addSingletonFactory { single {
val xml by DI.global.instance<XML>() XML {
xml defaultPolicy {
ignoreUnknownChildren()
}
autoPolymorphic = true
xmlDeclMode = XmlDeclMode.Charset
indent = 2
xmlVersion = XmlVersion.XML10
}
} }
addSingletonFactory { single {
val protobuf by DI.global.instance<ProtoBuf>() ProtoBuf
protobuf
} }
}
// Asynchronously init expensive components for a faster cold start // Asynchronously init expensive components for a faster cold start
// rxAsync { get<PreferencesHelper>() } // rxAsync { get<PreferencesHelper>() }
rxAsync { get<NetworkHelper>() } // rxAsync {
rxAsync {
// get<SourceManager>() // get<SourceManager>()
// get<DownloadManager>() // get<DownloadManager>()
} // }
// rxAsync { get<DatabaseHelper>() } // rxAsync { get<DatabaseHelper>() }
}
private fun rxAsync(block: () -> Unit) {
Observable.fromCallable { block() }.subscribeOn(Schedulers.computation()).subscribe()
}
} }
@@ -49,7 +49,9 @@ class MemoryCookieJar : CookieJar {
} }
} }
class WrappedCookie private constructor(val cookie: Cookie) { class WrappedCookie private constructor(
val cookie: Cookie,
) {
fun unwrap() = cookie fun unwrap() = cookie
fun isExpired() = cookie.expiresAt < System.currentTimeMillis() fun isExpired() = cookie.expiresAt < System.currentTimeMillis()
@@ -12,13 +12,13 @@ import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
import eu.kanade.tachiyomi.network.interceptor.IgnoreGzipInterceptor import eu.kanade.tachiyomi.network.interceptor.IgnoreGzipInterceptor
import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import mu.KotlinLogging
import okhttp3.Cache import okhttp3.Cache
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.brotli.BrotliInterceptor import okhttp3.brotli.BrotliInterceptor
@@ -30,7 +30,9 @@ import java.net.CookieManager
import java.net.CookiePolicy import java.net.CookiePolicy
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class NetworkHelper(context: Context) { class NetworkHelper(
context: Context,
) {
// private val preferences: PreferencesHelper by injectLazy() // private val preferences: PreferencesHelper by injectLazy()
// private val cacheDir = File(context.cacheDir, "network_cache") // private val cacheDir = File(context.cacheDir, "network_cache")
@@ -53,9 +55,7 @@ class NetworkHelper(context: Context) {
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
) )
fun defaultUserAgentProvider(): String { fun defaultUserAgentProvider(): String = userAgent.value
return userAgent.value
}
init { init {
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
@@ -63,14 +63,14 @@ class NetworkHelper(context: Context) {
.drop(1) .drop(1)
.onEach { .onEach {
GetCatalogueSource.unregisterAllCatalogueSources() // need to reset the headers GetCatalogueSource.unregisterAllCatalogueSources() // need to reset the headers
} }.launchIn(GlobalScope)
.launchIn(GlobalScope)
} }
private val baseClientBuilder: OkHttpClient.Builder private val baseClientBuilder: OkHttpClient.Builder
get() { get() {
val builder = val builder =
OkHttpClient.Builder() OkHttpClient
.Builder()
.cookieJar(PersistentCookieJar(cookieStore)) .cookieJar(PersistentCookieJar(cookieStore))
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS)
@@ -80,8 +80,7 @@ class NetworkHelper(context: Context) {
directory = File.createTempFile("tachidesk_network_cache", null), directory = File.createTempFile("tachidesk_network_cache", null),
maxSize = 5L * 1024 * 1024, // 5 MiB maxSize = 5L * 1024 * 1024, // 5 MiB
), ),
) ).addInterceptor(UncaughtExceptionInterceptor())
.addInterceptor(UncaughtExceptionInterceptor())
.addInterceptor(UserAgentInterceptor(::defaultUserAgentProvider)) .addInterceptor(UserAgentInterceptor(::defaultUserAgentProvider))
.addNetworkInterceptor(IgnoreGzipInterceptor()) .addNetworkInterceptor(IgnoreGzipInterceptor())
.addNetworkInterceptor(BrotliInterceptor) .addNetworkInterceptor(BrotliInterceptor)
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.network package eu.kanade.tachiyomi.network
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
@@ -50,9 +49,7 @@ fun Call.asObservable(): Observable<Response> {
// call.cancel() // call.cancel()
} }
override fun isUnsubscribed(): Boolean { override fun isUnsubscribed(): Boolean = call.isCanceled()
return call.isCanceled()
}
} }
subscriber.add(requestArbiter) subscriber.add(requestArbiter)
@@ -60,18 +57,16 @@ fun Call.asObservable(): Observable<Response> {
} }
} }
fun Call.asObservableSuccess(): Observable<Response> { fun Call.asObservableSuccess(): Observable<Response> =
return asObservable() asObservable()
.doOnNext { response -> .doOnNext { response ->
if (!response.isSuccessful) { if (!response.isSuccessful) {
response.close() response.close()
throw HttpException(response.code) throw HttpException(response.code)
} }
} }
}
// Based on https://github.com/gildor/kotlin-coroutines-okhttp // Based on https://github.com/gildor/kotlin-coroutines-okhttp
@OptIn(ExperimentalCoroutinesApi::class)
private suspend fun Call.await(callStack: Array<StackTraceElement>): Response { private suspend fun Call.await(callStack: Array<StackTraceElement>): Response {
return suspendCancellableCoroutine { continuation -> return suspendCancellableCoroutine { continuation ->
val callback = val callback =
@@ -80,8 +75,9 @@ private suspend fun Call.await(callStack: Array<StackTraceElement>): Response {
call: Call, call: Call,
response: Response, response: Response,
) { ) {
continuation.resume(response) { continuation.resume(response) { _, resourceToClose, _ ->
response.body.close() response.body.close()
resourceToClose.close()
} }
} }
@@ -135,29 +131,28 @@ fun OkHttpClient.newCachelessCallWithProgress(
.cache(null) .cache(null)
.addNetworkInterceptor { chain -> .addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request()) val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder() originalResponse
.newBuilder()
.body(ProgressResponseBody(originalResponse.body, listener)) .body(ProgressResponseBody(originalResponse.body, listener))
.build() .build()
} }.build()
.build()
return progressClient.newCall(request) return progressClient.newCall(request)
} }
context(Json) context(Json)
inline fun <reified T> Response.parseAs(): T { inline fun <reified T> Response.parseAs(): T = decodeFromJsonResponse(serializer(), this)
return decodeFromJsonResponse(serializer(), this)
}
context(Json) context(Json)
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
fun <T> decodeFromJsonResponse( fun <T> decodeFromJsonResponse(
deserializer: DeserializationStrategy<T>, deserializer: DeserializationStrategy<T>,
response: Response, response: Response,
): T { ): T =
return response.body.source().use { response.body.source().use {
decodeFromBufferedSource(deserializer, it) decodeFromBufferedSource(deserializer, it)
} }
}
class HttpException(val code: Int) : IllegalStateException("HTTP error $code") class HttpException(
val code: Int,
) : IllegalStateException("HTTP error $code")
@@ -5,7 +5,9 @@ import okhttp3.CookieJar
import okhttp3.HttpUrl import okhttp3.HttpUrl
// from TachiWeb-Server // from TachiWeb-Server
class PersistentCookieJar(private val store: PersistentCookieStore) : CookieJar { class PersistentCookieJar(
private val store: PersistentCookieStore,
) : CookieJar {
override fun saveFromResponse( override fun saveFromResponse(
url: HttpUrl, url: HttpUrl,
cookies: List<Cookie>, cookies: List<Cookie>,
@@ -13,7 +15,5 @@ class PersistentCookieJar(private val store: PersistentCookieStore) : CookieJar
store.addAll(url, cookies) store.addAll(url, cookies)
} }
override fun loadForRequest(url: HttpUrl): List<Cookie> { override fun loadForRequest(url: HttpUrl): List<Cookie> = store.get(url)
return store.get(url)
}
} }
@@ -8,13 +8,16 @@ import okio.withLock
import java.net.CookieStore import java.net.CookieStore
import java.net.HttpCookie import java.net.HttpCookie
import java.net.URI import java.net.URI
import java.net.URL
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
// from TachiWeb-Server // from TachiWeb-Server
class PersistentCookieStore(context: Context) : CookieStore { class PersistentCookieStore(
context: Context,
) : CookieStore {
private val cookieMap = ConcurrentHashMap<String, List<Cookie>>() private val cookieMap = ConcurrentHashMap<String, List<Cookie>>()
private val prefs = context.getSharedPreferences("cookie_store", Context.MODE_PRIVATE) private val prefs = context.getSharedPreferences("cookie_store", Context.MODE_PRIVATE)
@@ -22,7 +25,8 @@ class PersistentCookieStore(context: Context) : CookieStore {
init { init {
val domains = val domains =
prefs.all.keys.map { it.substringBeforeLast(".") } prefs.all.keys
.map { it.substringBeforeLast(".") }
.toSet() .toSet()
domains.forEach { domain -> domains.forEach { domain ->
val cookies = prefs.getStringSet(domain, emptySet()) val cookies = prefs.getStringSet(domain, emptySet())
@@ -30,7 +34,8 @@ class PersistentCookieStore(context: Context) : CookieStore {
try { try {
val url = "http://$domain".toHttpUrlOrNull() ?: return@forEach val url = "http://$domain".toHttpUrlOrNull() ?: return@forEach
val nonExpiredCookies = val nonExpiredCookies =
cookies.mapNotNull { Cookie.parse(url, it) } cookies
.mapNotNull { Cookie.parse(url, it) }
.filter { !it.hasExpired() } .filter { !it.hasExpired() }
cookieMap[domain] = nonExpiredCookies cookieMap[domain] = nonExpiredCookies
} catch (e: Exception) { } catch (e: Exception) {
@@ -45,10 +50,8 @@ class PersistentCookieStore(context: Context) : CookieStore {
cookies: List<Cookie>, cookies: List<Cookie>,
) { ) {
lock.withLock { lock.withLock {
val uri = url.toUri()
// Append or replace the cookies for this domain. // Append or replace the cookies for this domain.
val cookiesForDomain = cookieMap[uri.host].orEmpty().toMutableList() val cookiesForDomain = cookieMap[url.host].orEmpty().toMutableList()
for (cookie in cookies) { for (cookie in cookies) {
// Find a cookie with the same name. Replace it if found, otherwise add a new one. // Find a cookie with the same name. Replace it if found, otherwise add a new one.
val pos = cookiesForDomain.indexOfFirst { it.name == cookie.name } val pos = cookiesForDomain.indexOfFirst { it.name == cookie.name }
@@ -58,63 +61,62 @@ class PersistentCookieStore(context: Context) : CookieStore {
cookiesForDomain[pos] = cookie cookiesForDomain[pos] = cookie
} }
} }
cookieMap[uri.host] = cookiesForDomain cookieMap[url.host] = cookiesForDomain
saveToDisk(uri) saveToDisk(url.toUrl())
} }
} }
override fun removeAll(): Boolean { override fun removeAll(): Boolean =
return lock.withLock { lock.withLock {
val wasNotEmpty = cookieMap.isEmpty() val wasNotEmpty = cookieMap.isEmpty()
prefs.edit().clear().apply() prefs.edit().clear().apply()
cookieMap.clear() cookieMap.clear()
wasNotEmpty wasNotEmpty
} }
}
fun remove(uri: URI) { fun remove(uri: URI) {
val url = uri.toURL()
lock.withLock { lock.withLock {
prefs.edit().remove(uri.host).apply() prefs.edit().remove(url.host).apply()
cookieMap.remove(uri.host) cookieMap.remove(url.host)
} }
} }
override fun get(uri: URI): List<HttpCookie> = override fun get(uri: URI): List<HttpCookie> {
get(uri.host).map { val url = uri.toURL()
return get(url.host).map {
it.toHttpCookie() it.toHttpCookie()
} }
fun get(url: HttpUrl): List<Cookie> {
return get(url.toUri().host ?: return emptyList())
} }
fun get(url: HttpUrl): List<Cookie> = get(url.host)
override fun add( override fun add(
uri: URI?, uri: URI?,
cookie: HttpCookie, cookie: HttpCookie,
) { ) {
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
val uri = uri ?: URI("http://" + cookie.domain.removePrefix(".")) val uri = uri ?: URI("http://" + cookie.domain.removePrefix("."))
val url = uri.toURL()
lock.withLock { lock.withLock {
val cookies = cookieMap[uri.host] val cookies = cookieMap[url.host]
cookieMap[uri.host] = cookies.orEmpty() + cookie.toCookie(uri) cookieMap[url.host] = cookies.orEmpty() + cookie.toCookie(uri)
saveToDisk(uri) saveToDisk(url)
} }
} }
override fun getCookies(): List<HttpCookie> { override fun getCookies(): List<HttpCookie> =
return cookieMap.values.flatMap { cookieMap.values.flatMap {
it.map { it.map {
it.toHttpCookie() it.toHttpCookie()
} }
} }
}
override fun getURIs(): List<URI> { override fun getURIs(): List<URI> =
return cookieMap.keys().toList().map { cookieMap.keys().toList().map {
URI("http://$it") URI("http://$it")
} }
}
override fun remove( override fun remove(
uri: URI?, uri: URI?,
@@ -122,8 +124,9 @@ class PersistentCookieStore(context: Context) : CookieStore {
): Boolean { ): Boolean {
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
val uri = uri ?: URI("http://" + cookie.domain.removePrefix(".")) val uri = uri ?: URI("http://" + cookie.domain.removePrefix("."))
val url = uri.toURL()
return lock.withLock { return lock.withLock {
val cookies = cookieMap[uri.host].orEmpty() val cookies = cookieMap[url.host].orEmpty()
val index = val index =
cookies.indexOfFirst { cookies.indexOfFirst {
it.name == cookie.name && it.name == cookie.name &&
@@ -132,8 +135,8 @@ class PersistentCookieStore(context: Context) : CookieStore {
if (index >= 0) { if (index >= 0) {
val newList = cookies.toMutableList() val newList = cookies.toMutableList()
newList.removeAt(index) newList.removeAt(index)
cookieMap[uri.host] = newList.toList() cookieMap[url.host] = newList.toList()
saveToDisk(uri) saveToDisk(url)
true true
} else { } else {
false false
@@ -141,30 +144,29 @@ class PersistentCookieStore(context: Context) : CookieStore {
} }
} }
private fun get(url: String): List<Cookie> { private fun get(url: String): List<Cookie> = cookieMap[url].orEmpty().filter { !it.hasExpired() }
return cookieMap[url].orEmpty().filter { !it.hasExpired() }
}
private fun saveToDisk(uri: URI) { private fun saveToDisk(url: URL) {
// Get cookies to be stored in disk // Get cookies to be stored in disk
val newValues = val newValues =
cookieMap[uri.host] cookieMap[url.host]
.orEmpty() .orEmpty()
.asSequence() .asSequence()
.filter { it.persistent && !it.hasExpired() } .filter { it.persistent && !it.hasExpired() }
.map(Cookie::toString) .map(Cookie::toString)
.toSet() .toSet()
prefs.edit().putStringSet(uri.host, newValues).apply() prefs.edit().putStringSet(url.host, newValues).apply()
} }
private fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt private fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt
private fun HttpCookie.toCookie(uri: URI) = private fun HttpCookie.toCookie(uri: URI) =
Cookie.Builder() Cookie
.Builder()
.name(name) .name(name)
.value(value) .value(value)
.domain(uri.host) .domain(uri.toURL().host)
.path(path ?: "/") .path(path ?: "/")
.let { .let {
if (maxAge != -1L) { if (maxAge != -1L) {
@@ -172,22 +174,19 @@ class PersistentCookieStore(context: Context) : CookieStore {
} else { } else {
it.expiresAt(Long.MAX_VALUE) it.expiresAt(Long.MAX_VALUE)
} }
} }.let {
.let {
if (secure) { if (secure) {
it.secure() it.secure()
} else { } else {
it it
} }
} }.let {
.let {
if (isHttpOnly) { if (isHttpOnly) {
it.httpOnly() it.httpOnly()
} else { } else {
it it
} }
} }.build()
.build()
private fun Cookie.toHttpCookie(): HttpCookie { private fun Cookie.toHttpCookie(): HttpCookie {
val it = this val it = this
@@ -9,22 +9,19 @@ import okio.Source
import okio.buffer import okio.buffer
import java.io.IOException import java.io.IOException
class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() { class ProgressResponseBody(
private val responseBody: ResponseBody,
private val progressListener: ProgressListener,
) : ResponseBody() {
private val bufferedSource: BufferedSource by lazy { private val bufferedSource: BufferedSource by lazy {
source(responseBody.source()).buffer() source(responseBody.source()).buffer()
} }
override fun contentType(): MediaType? { override fun contentType(): MediaType? = responseBody.contentType()
return responseBody.contentType()
}
override fun contentLength(): Long { override fun contentLength(): Long = responseBody.contentLength()
return responseBody.contentLength()
}
override fun source(): BufferedSource { override fun source(): BufferedSource = bufferedSource
return bufferedSource
}
private fun source(source: Source): Source { private fun source(source: Source): Source {
return object : ForwardingSource(source) { return object : ForwardingSource(source) {
@@ -18,13 +18,13 @@ fun GET(
url: String, url: String,
headers: Headers = DEFAULT_HEADERS, headers: Headers = DEFAULT_HEADERS,
cache: CacheControl = DEFAULT_CACHE_CONTROL, cache: CacheControl = DEFAULT_CACHE_CONTROL,
): Request { ): Request =
return Request.Builder() Request
.Builder()
.url(url) .url(url)
.headers(headers) .headers(headers)
.cacheControl(cache) .cacheControl(cache)
.build() .build()
}
/** /**
* @since extensions-lib 1.4 * @since extensions-lib 1.4
@@ -33,52 +33,52 @@ fun GET(
url: HttpUrl, url: HttpUrl,
headers: Headers = DEFAULT_HEADERS, headers: Headers = DEFAULT_HEADERS,
cache: CacheControl = DEFAULT_CACHE_CONTROL, cache: CacheControl = DEFAULT_CACHE_CONTROL,
): Request { ): Request =
return Request.Builder() Request
.Builder()
.url(url) .url(url)
.headers(headers) .headers(headers)
.cacheControl(cache) .cacheControl(cache)
.build() .build()
}
fun POST( fun POST(
url: String, url: String,
headers: Headers = DEFAULT_HEADERS, headers: Headers = DEFAULT_HEADERS,
body: RequestBody = DEFAULT_BODY, body: RequestBody = DEFAULT_BODY,
cache: CacheControl = DEFAULT_CACHE_CONTROL, cache: CacheControl = DEFAULT_CACHE_CONTROL,
): Request { ): Request =
return Request.Builder() Request
.Builder()
.url(url) .url(url)
.post(body) .post(body)
.headers(headers) .headers(headers)
.cacheControl(cache) .cacheControl(cache)
.build() .build()
}
fun PUT( fun PUT(
url: String, url: String,
headers: Headers = DEFAULT_HEADERS, headers: Headers = DEFAULT_HEADERS,
body: RequestBody = DEFAULT_BODY, body: RequestBody = DEFAULT_BODY,
cache: CacheControl = DEFAULT_CACHE_CONTROL, cache: CacheControl = DEFAULT_CACHE_CONTROL,
): Request { ): Request =
return Request.Builder() Request
.Builder()
.url(url) .url(url)
.put(body) .put(body)
.headers(headers) .headers(headers)
.cacheControl(cache) .cacheControl(cache)
.build() .build()
}
fun DELETE( fun DELETE(
url: String, url: String,
headers: Headers = DEFAULT_HEADERS, headers: Headers = DEFAULT_HEADERS,
body: RequestBody = DEFAULT_BODY, body: RequestBody = DEFAULT_BODY,
cache: CacheControl = DEFAULT_CACHE_CONTROL, cache: CacheControl = DEFAULT_CACHE_CONTROL,
): Request { ): Request =
return Request.Builder() Request
.Builder()
.url(url) .url(url)
.delete(body) .delete(body)
.headers(headers) .headers(headers)
.cacheControl(cache) .cacheControl(cache)
.build() .build()
}
@@ -4,6 +4,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@@ -15,7 +16,6 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import mu.KotlinLogging
import okhttp3.Cookie import okhttp3.Cookie
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
@@ -23,6 +23,7 @@ import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import suwayomi.tachidesk.server.serverConfig import suwayomi.tachidesk.server.serverConfig
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.io.IOException
@@ -56,11 +57,38 @@ class CloudflareInterceptor(
originalResponse.close() originalResponse.close()
// network.cookieStore.remove(originalRequest.url.toUri()) // network.cookieStore.remove(originalRequest.url.toUri())
val request = val flareResponseFallback = serverConfig.flareSolverrAsResponseFallback.value
val flareResponse =
runBlocking { runBlocking {
CFClearance.resolveWithFlareSolverr(setUserAgent, originalRequest) CFClearance.resolveWithFlareSolver(originalRequest, !flareResponseFallback)
} }
if (flareResponse.message.contains("not detected", ignoreCase = true)) {
logger.debug { "FlareSolverr failed to detect Cloudflare challenge" }
if (flareResponseFallback &&
flareResponse.solution.status in 200..299 &&
flareResponse.solution.response != null
) {
val isImage = flareResponse.solution.response.contains(CHROME_IMAGE_TEMPLATE_REGEX)
if (!isImage) {
logger.debug { "Falling back to FlareSolverr response" }
setUserAgent(flareResponse.solution.userAgent)
return originalResponse
.newBuilder()
.code(flareResponse.solution.status)
.body(flareResponse.solution.response.toResponseBody())
.build()
} else {
logger.debug { "FlareSolverr response is an image html template, not falling back" }
}
}
}
val request = CFClearance.requestWithFlareSolverr(flareResponse, setUserAgent, originalRequest)
chain.proceed(request) chain.proceed(request)
} catch (e: Exception) { } catch (e: Exception) {
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
@@ -73,6 +101,7 @@ class CloudflareInterceptor(
private val ERROR_CODES = listOf(403, 503) private val ERROR_CODES = listOf(403, 503)
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare") private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
private val COOKIE_NAMES = listOf("cf_clearance") private val COOKIE_NAMES = listOf("cf_clearance")
private val CHROME_IMAGE_TEMPLATE_REGEX = Regex("""<title>(.*?) \(\d+×\d+\)</title>""")
} }
} }
@@ -88,12 +117,12 @@ object CFClearance {
serverConfig.flareSolverrTimeout serverConfig.flareSolverrTimeout
.map { timeoutInt -> .map { timeoutInt ->
val timeout = timeoutInt.seconds val timeout = timeoutInt.seconds
network.client.newBuilder() network.client
.newBuilder()
.callTimeout(timeout.plus(10.seconds).toJavaDuration()) .callTimeout(timeout.plus(10.seconds).toJavaDuration())
.readTimeout(timeout.plus(5.seconds).toJavaDuration()) .readTimeout(timeout.plus(5.seconds).toJavaDuration())
.build() .build()
} }.stateIn(GlobalScope, SharingStarted.Eagerly, network.client)
.stateIn(GlobalScope, SharingStarted.Eagerly, network.client)
} }
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val jsonMediaType = "application/json".toMediaType() private val jsonMediaType = "application/json".toMediaType()
@@ -124,13 +153,13 @@ object CFClearance {
val name: String, val name: String,
val value: String, val value: String,
val domain: String, val domain: String,
val path: String, val path: String? = null,
val expires: Double? = null, val expires: Double? = null,
val size: Int? = null, val size: Int? = null,
val httpOnly: Boolean, val httpOnly: Boolean? = null,
val secure: Boolean, val secure: Boolean? = null,
val session: Boolean? = null, val session: Boolean? = null,
val sameSite: String, val sameSite: String? = null,
) )
@Serializable @Serializable
@@ -153,58 +182,68 @@ object CFClearance {
val version: String, val version: String,
) )
suspend fun resolveWithFlareSolverr( suspend fun resolveWithFlareSolver(
setUserAgent: (String) -> Unit,
originalRequest: Request, originalRequest: Request,
): Request { onlyCookies: Boolean,
): FlareSolverResponse {
val timeout = serverConfig.flareSolverrTimeout.value.seconds val timeout = serverConfig.flareSolverrTimeout.value.seconds
val flareSolverResponse =
with(json) { return with(json) {
mutex.withLock { mutex.withLock {
client.value.newCall( client.value
.newCall(
POST( POST(
url = serverConfig.flareSolverrUrl.value.removeSuffix("/") + "/v1", url = serverConfig.flareSolverrUrl.value.removeSuffix("/") + "/v1",
body = body =
Json.encodeToString( Json
FlareSolverRequest( .encodeToString(
"request.get", FlareSolverRequest(
originalRequest.url.toString(), "request.get",
session = serverConfig.flareSolverrSessionName.value, originalRequest.url.toString(),
sessionTtlMinutes = serverConfig.flareSolverrSessionTtl.value, session = serverConfig.flareSolverrSessionName.value,
cookies = sessionTtlMinutes = serverConfig.flareSolverrSessionTtl.value,
network.cookieStore.get(originalRequest.url).map { cookies =
FlareSolverCookie(it.name, it.value) network.cookieStore.get(originalRequest.url).map {
}, FlareSolverCookie(it.name, it.value)
returnOnlyCookies = true, },
maxTimeout = timeout.inWholeMilliseconds.toInt(), returnOnlyCookies = onlyCookies,
), maxTimeout = timeout.inWholeMilliseconds.toInt(),
).toRequestBody(jsonMediaType), ),
).toRequestBody(jsonMediaType),
), ),
).awaitSuccess().parseAs<FlareSolverResponse>() ).awaitSuccess()
} .parseAs<FlareSolverResponse>()
} }
}
}
fun requestWithFlareSolverr(
flareSolverResponse: FlareSolverResponse,
setUserAgent: (String) -> Unit,
originalRequest: Request,
): Request {
if (flareSolverResponse.solution.status in 200..299) { if (flareSolverResponse.solution.status in 200..299) {
setUserAgent(flareSolverResponse.solution.userAgent) setUserAgent(flareSolverResponse.solution.userAgent)
val cookies = val cookies =
flareSolverResponse.solution.cookies flareSolverResponse.solution.cookies
.map { cookie -> .map { cookie ->
Cookie.Builder() Cookie
.Builder()
.name(cookie.name) .name(cookie.name)
.value(cookie.value) .value(cookie.value)
.domain(cookie.domain.removePrefix(".")) .domain(cookie.domain.removePrefix("."))
.path(cookie.path)
.expiresAt(cookie.expires?.takeUnless { it < 0.0 }?.toLong() ?: Long.MAX_VALUE)
.also { .also {
if (cookie.httpOnly) it.httpOnly() if (cookie.httpOnly != null && cookie.httpOnly) it.httpOnly()
if (cookie.secure) it.secure() if (cookie.secure != null && cookie.secure) it.secure()
} if (!cookie.path.isNullOrEmpty()) it.path(cookie.path)
.build() // We need to convert the expires time to milliseconds for the persistent cookie store
} if (cookie.expires != null && cookie.expires > 0) it.expiresAt((cookie.expires * 1000).toLong())
.groupBy { it.domain } }.build()
}.groupBy { it.domain }
.flatMap { (domain, cookies) -> .flatMap { (domain, cookies) ->
network.cookieStore.addAll( network.cookieStore.addAll(
HttpUrl.Builder() HttpUrl
.Builder()
.scheme("http") .scheme("http")
.host(domain.removePrefix(".")) .host(domain.removePrefix("."))
.build(), .build(),
@@ -219,7 +258,8 @@ object CFClearance {
"${it.name}=${it.value}" "${it.name}=${it.value}"
} }
logger.trace { "Final cookies\n$finalCookies" } logger.trace { "Final cookies\n$finalCookies" }
return originalRequest.newBuilder() return originalRequest
.newBuilder()
.header("Cookie", finalCookies) .header("Cookie", finalCookies)
.header("User-Agent", flareSolverResponse.solution.userAgent) .header("User-Agent", flareSolverResponse.solution.userAgent)
.build() .build()
@@ -13,8 +13,8 @@ import java.io.IOException
* See https://square.github.io/okhttp/4.x/okhttp/okhttp3/-interceptor/ * See https://square.github.io/okhttp/4.x/okhttp/okhttp3/-interceptor/
*/ */
class UncaughtExceptionInterceptor : Interceptor { class UncaughtExceptionInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response =
return try { try {
chain.proceed(chain.request()) chain.proceed(chain.request())
} catch (e: Exception) { } catch (e: Exception) {
if (e is IOException) { if (e is IOException) {
@@ -23,5 +23,4 @@ class UncaughtExceptionInterceptor : Interceptor {
throw IOException(e) throw IOException(e)
} }
} }
}
} }
@@ -3,7 +3,9 @@ package eu.kanade.tachiyomi.network.interceptor
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
class UserAgentInterceptor(private val userAgentProvider: () -> String) : Interceptor { class UserAgentInterceptor(
private val userAgentProvider: () -> String,
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request() val originalRequest = chain.request()
@@ -23,9 +23,7 @@ interface CatalogueSource : Source {
* @param page the page number to retrieve. * @param page the page number to retrieve.
*/ */
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
suspend fun getPopularManga(page: Int): MangasPage { suspend fun getPopularManga(page: Int): MangasPage = fetchPopularManga(page).awaitSingle()
return fetchPopularManga(page).awaitSingle()
}
/** /**
* Get a page with a list of manga. * Get a page with a list of manga.
@@ -40,9 +38,7 @@ interface CatalogueSource : Source {
page: Int, page: Int,
query: String, query: String,
filters: FilterList, filters: FilterList,
): MangasPage { ): MangasPage = fetchSearchManga(page, query, filters).awaitSingle()
return fetchSearchManga(page, query, filters).awaitSingle()
}
/** /**
* Get a page with a list of latest manga updates. * Get a page with a list of latest manga updates.
@@ -51,9 +47,7 @@ interface CatalogueSource : Source {
* @param page the page number to retrieve. * @param page the page number to retrieve.
*/ */
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
suspend fun getLatestUpdates(page: Int): MangasPage { suspend fun getLatestUpdates(page: Int): MangasPage = fetchLatestUpdates(page).awaitSingle()
return fetchLatestUpdates(page).awaitSingle()
}
/** /**
* Returns the list of filters for the source. * Returns the list of filters for the source.
@@ -31,9 +31,7 @@ interface Source {
* @return the updated manga. * @return the updated manga.
*/ */
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
suspend fun getMangaDetails(manga: SManga): SManga { suspend fun getMangaDetails(manga: SManga): SManga = fetchMangaDetails(manga).awaitSingle()
return fetchMangaDetails(manga).awaitSingle()
}
/** /**
* Get all the available chapters for a manga. * Get all the available chapters for a manga.
@@ -43,9 +41,7 @@ interface Source {
* @return the chapters for the manga. * @return the chapters for the manga.
*/ */
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
suspend fun getChapterList(manga: SManga): List<SChapter> { suspend fun getChapterList(manga: SManga): List<SChapter> = fetchChapterList(manga).awaitSingle()
return fetchChapterList(manga).awaitSingle()
}
/** /**
* Get the list of pages a chapter has. Pages should be returned * Get the list of pages a chapter has. Pages should be returned
@@ -56,9 +52,7 @@ interface Source {
* @return the pages for the chapter. * @return the pages for the chapter.
*/ */
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
suspend fun getPageList(chapter: SChapter): List<Page> { suspend fun getPageList(chapter: SChapter): List<Page> = fetchPageList(chapter).awaitSingle()
return fetchPageList(chapter).awaitSingle()
}
@Deprecated( @Deprecated(
"Use the non-RxJava API instead", "Use the non-RxJava API instead",
@@ -26,23 +26,20 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.storage.EpubFile import eu.kanade.tachiyomi.util.storage.EpubFile
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.decodeFromStream
import mu.KotlinLogging
import nl.adaptivity.xmlutil.ExperimentalXmlUtilApi import nl.adaptivity.xmlutil.ExperimentalXmlUtilApi
import nl.adaptivity.xmlutil.core.KtXmlReader import nl.adaptivity.xmlutil.core.KtXmlReader
import nl.adaptivity.xmlutil.serialization.XML import nl.adaptivity.xmlutil.serialization.XML
import org.apache.commons.compress.archivers.zip.ZipFile import org.apache.commons.compress.archivers.zip.ZipFile
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.insertAndGetId import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.kodein.di.DI
import org.kodein.di.conf.global
import org.kodein.di.instance
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.registerCatalogueSource import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.registerCatalogueSource
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
import suwayomi.tachidesk.manga.model.table.ExtensionTable import suwayomi.tachidesk.manga.model.table.ExtensionTable
@@ -59,7 +56,8 @@ import com.github.junrar.Archive as JunrarArchive
class LocalSource( class LocalSource(
private val fileSystem: LocalSourceFileSystem, private val fileSystem: LocalSourceFileSystem,
private val coverManager: LocalCoverManager, private val coverManager: LocalCoverManager,
) : CatalogueSource, UnmeteredSource { ) : CatalogueSource,
UnmeteredSource {
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val xml: XML by injectLazy() private val xml: XML by injectLazy()
@@ -93,7 +91,8 @@ class LocalSource(
// Filter out files that are hidden and is not a folder // Filter out files that are hidden and is not a folder
.filter { it.isDirectory && !it.name.startsWith('.') } .filter { it.isDirectory && !it.name.startsWith('.') }
.distinctBy { it.name } .distinctBy { it.name }
.filter { // Filter by query or last modified .filter {
// Filter by query or last modified
if (lastModifiedLimit == 0L) { if (lastModifiedLimit == 0L) {
it.name.contains(query, ignoreCase = true) it.name.contains(query, ignoreCase = true)
} else { } else {
@@ -134,7 +133,8 @@ class LocalSource(
url = mangaDir.name url = mangaDir.name
// Try to find the cover // Try to find the cover
coverManager.find(mangaDir.name) coverManager
.find(mangaDir.name)
?.takeIf(File::exists) ?.takeIf(File::exists)
?.let { thumbnail_url = it.absolutePath } ?.let { thumbnail_url = it.absolutePath }
} }
@@ -238,7 +238,7 @@ class LocalSource(
for (chapter in chapterArchives) { for (chapter in chapterArchives) {
when (Format.valueOf(chapter)) { when (Format.valueOf(chapter)) {
is Format.Zip -> { is Format.Zip -> {
ZipFile(chapter).use { zip: ZipFile -> ZipFile.builder().setFile(chapter).get().use { zip: ZipFile ->
zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile -> zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile ->
zip.getInputStream(comicInfoFile).buffered().use { stream -> zip.getInputStream(comicInfoFile).buffered().use { stream ->
return copyComicInfoFile(stream, folderPath) return copyComicInfoFile(stream, folderPath)
@@ -264,13 +264,12 @@ class LocalSource(
private fun copyComicInfoFile( private fun copyComicInfoFile(
comicInfoFileStream: InputStream, comicInfoFileStream: InputStream,
folderPath: String?, folderPath: String?,
): File { ): File =
return File("$folderPath/$COMIC_INFO_FILE").apply { File("$folderPath/$COMIC_INFO_FILE").apply {
outputStream().use { outputStream -> outputStream().use { outputStream ->
comicInfoFileStream.use { it.copyTo(outputStream) } comicInfoFileStream.use { it.copyTo(outputStream) }
} }
} }
}
@OptIn(ExperimentalXmlUtilApi::class) @OptIn(ExperimentalXmlUtilApi::class)
private fun setMangaDetailsFromComicInfoFile( private fun setMangaDetailsFromComicInfoFile(
@@ -286,8 +285,9 @@ class LocalSource(
} }
// Chapters // Chapters
override suspend fun getChapterList(manga: SManga): List<SChapter> { override suspend fun getChapterList(manga: SManga): List<SChapter> =
return fileSystem.getFilesInMangaDirectory(manga.url) fileSystem
.getFilesInMangaDirectory(manga.url)
// Only keep supported formats // Only keep supported formats
.filter { it.isDirectory || Archive.isSupported(it) } .filter { it.isDirectory || Archive.isSupported(it) }
.map { chapterFile -> .map { chapterFile ->
@@ -312,22 +312,21 @@ class LocalSource(
} }
} }
} }
} }.sortedWith { c1, c2 ->
.sortedWith { c1, c2 ->
val c = c2.chapter_number.compareTo(c1.chapter_number) val c = c2.chapter_number.compareTo(c1.chapter_number)
if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
} }.toList()
.toList()
}
// Filters // Filters
override fun getFilterList() = FilterList(OrderBy.Popular()) override fun getFilterList() = FilterList(OrderBy.Popular())
// TODO Fix Memory Leak // TODO Fix Memory Leak
override suspend fun getPageList(chapter: SChapter): List<Page> { override suspend fun getPageList(chapter: SChapter): List<Page> =
return when (val format = getFormat(chapter)) { when (val format = getFormat(chapter)) {
is Format.Directory -> { is Format.Directory -> {
format.file.listFiles().orEmpty() format.file
.listFiles()
.orEmpty()
.filter { !it.isDirectory && ImageUtil.isImage(it.name, it::inputStream) } .filter { !it.isDirectory && ImageUtil.isImage(it.name, it::inputStream) }
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
.mapIndexed { index, page -> .mapIndexed { index, page ->
@@ -359,11 +358,11 @@ class LocalSource(
pages pages
} }
} }
}
fun getFormat(chapter: SChapter): Format { fun getFormat(chapter: SChapter): Format {
try { try {
return fileSystem.getBaseDirectories() return fileSystem
.getBaseDirectories()
.map { dir -> File(dir, chapter.url) } .map { dir -> File(dir, chapter.url) }
.find { it.exists() } .find { it.exists() }
?.let(Format.Companion::valueOf) ?.let(Format.Companion::valueOf)
@@ -378,21 +377,23 @@ class LocalSource(
private fun updateCover( private fun updateCover(
chapter: SChapter, chapter: SChapter,
manga: SManga, manga: SManga,
): File? { ): File? =
return try { try {
when (val format = getFormat(chapter)) { when (val format = getFormat(chapter)) {
is Format.Directory -> { is Format.Directory -> {
val entry = val entry =
format.file.listFiles() format.file
.listFiles()
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } ?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } } ?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
entry?.let { coverManager.update(manga, it.inputStream()) } entry?.let { coverManager.update(manga, it.inputStream()) }
} }
is Format.Zip -> { is Format.Zip -> {
ZipFile(format.file).use { zip -> ZipFile.builder().setFile(format.file).get().use { zip ->
val entry = val entry =
zip.entries.toList() zip.entries
.toList()
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } } .find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
@@ -412,7 +413,8 @@ class LocalSource(
is Format.Epub -> { is Format.Epub -> {
EpubFile(format.file).use { epub -> EpubFile(format.file).use { epub ->
val entry = val entry =
epub.getImagesFromPages() epub
.getImagesFromPages()
.firstOrNull() .firstOrNull()
?.let { epub.getEntry(it) } ?.let { epub.getEntry(it) }
@@ -424,7 +426,6 @@ class LocalSource(
logger.error(e) { "Error updating cover for ${manga.title}" } logger.error(e) { "Error updating cover for ${manga.title}" }
null null
} }
}
companion object { companion object {
const val ID = 0L const val ID = 0L
@@ -437,13 +438,13 @@ class LocalSource(
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
private val applicationDirs by DI.global.instance<ApplicationDirs>() private val applicationDirs: ApplicationDirs by injectLazy()
val pageCache: MutableMap<String, List<() -> InputStream>> = mutableMapOf() val pageCache: MutableMap<String, List<() -> InputStream>> = mutableMapOf()
fun register() { fun register() {
transaction { transaction {
val sourceRecord = SourceTable.select { SourceTable.id eq ID }.firstOrNull() val sourceRecord = SourceTable.selectAll().where { SourceTable.id eq ID }.firstOrNull()
if (sourceRecord == null) { if (sourceRecord == null) {
// must do this to avoid database integrity errors // must do this to avoid database integrity errors
@@ -2,12 +2,14 @@ package eu.kanade.tachiyomi.source.local.filter
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
sealed class OrderBy(selection: Selection) : Filter.Sort( sealed class OrderBy(
"Order by", selection: Selection,
arrayOf("Title", "Date"), ) : Filter.Sort(
selection, "Order by",
) { arrayOf("Title", "Date"),
class Popular() : OrderBy(Selection(0, true)) selection,
) {
class Popular : OrderBy(Selection(0, true))
class Latest() : OrderBy(Selection(1, false)) class Latest : OrderBy(Selection(1, false))
} }
@@ -11,15 +11,15 @@ private const val DEFAULT_COVER_NAME = "cover.jpg"
class LocalCoverManager( class LocalCoverManager(
private val fileSystem: LocalSourceFileSystem, private val fileSystem: LocalSourceFileSystem,
) { ) {
fun find(mangaUrl: String): File? { fun find(mangaUrl: String): File? =
return fileSystem.getFilesInMangaDirectory(mangaUrl) fileSystem
.getFilesInMangaDirectory(mangaUrl)
// Get all file whose names start with 'cover' // Get all file whose names start with 'cover'
.filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) } .filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) }
// Get the first actual image // Get the first actual image
.firstOrNull { .firstOrNull {
ImageUtil.isImage(it.name) { it.inputStream() } ImageUtil.isImage(it.name) { it.inputStream() }
} }
}
fun update( fun update(
manga: SManga, manga: SManga,
@@ -3,13 +3,21 @@ package eu.kanade.tachiyomi.source.local.io
import java.io.File import java.io.File
sealed interface Format { sealed interface Format {
data class Directory(val file: File) : Format data class Directory(
val file: File,
) : Format
data class Zip(val file: File) : Format data class Zip(
val file: File,
) : Format
data class Rar(val file: File) : Format data class Rar(
val file: File,
) : Format
data class Epub(val file: File) : Format data class Epub(
val file: File,
) : Format
class UnknownFormatException : Exception() class UnknownFormatException : Exception()
@@ -6,27 +6,22 @@ import java.io.File
class LocalSourceFileSystem( class LocalSourceFileSystem(
private val applicationDirs: ApplicationDirs, private val applicationDirs: ApplicationDirs,
) { ) {
fun getBaseDirectories(): Sequence<File> { fun getBaseDirectories(): Sequence<File> = sequenceOf(File(applicationDirs.localMangaRoot))
return sequenceOf(File(applicationDirs.localMangaRoot))
}
fun getFilesInBaseDirectories(): Sequence<File> { fun getFilesInBaseDirectories(): Sequence<File> =
return getBaseDirectories() getBaseDirectories()
// Get all the files inside all baseDir // Get all the files inside all baseDir
.flatMap { it.listFiles().orEmpty().toList() } .flatMap { it.listFiles().orEmpty().toList() }
}
fun getMangaDirectory(name: String): File? { fun getMangaDirectory(name: String): File? =
return getFilesInBaseDirectories() getFilesInBaseDirectories()
// Get the first mangaDir or null // Get the first mangaDir or null
.firstOrNull { it.isDirectory && it.name == name } .firstOrNull { it.isDirectory && it.name == name }
}
fun getFilesInMangaDirectory(name: String): Sequence<File> { fun getFilesInMangaDirectory(name: String): Sequence<File> =
return getFilesInBaseDirectories() getFilesInBaseDirectories()
// Filter out ones that are not related to the manga and is not a directory // Filter out ones that are not related to the manga and is not a directory
.filter { it.isDirectory && it.name == name } .filter { it.isDirectory && it.name == name }
// Get all the files inside the filtered folders // Get all the files inside the filtered folders
.flatMap { it.listFiles().orEmpty().toList() } .flatMap { it.listFiles().orEmpty().toList() }
}
} }
@@ -6,18 +6,20 @@ import java.io.File
/** /**
* Loader used to load a chapter from a .epub file. * Loader used to load a chapter from a .epub file.
*/ */
class EpubPageLoader(file: File) : PageLoader { class EpubPageLoader(
file: File,
) : PageLoader {
private val epub = EpubFile(file) private val epub = EpubFile(file)
override suspend fun getPages(): List<ReaderPage> { override suspend fun getPages(): List<ReaderPage> =
return epub.getImagesFromPages() epub
.getImagesFromPages()
.mapIndexed { i, path -> .mapIndexed { i, path ->
val streamFn = { epub.getInputStream(epub.getEntry(path)!!) } val streamFn = { epub.getInputStream(epub.getEntry(path)!!) }
ReaderPage(i).apply { ReaderPage(i).apply {
stream = streamFn stream = streamFn
} }
} }
}
override fun recycle() { override fun recycle() {
epub.close() epub.close()
@@ -12,20 +12,21 @@ import java.io.PipedOutputStream
/** /**
* Loader used to load a chapter from a .rar or .cbr file. * Loader used to load a chapter from a .rar or .cbr file.
*/ */
class RarPageLoader(file: File) : PageLoader { class RarPageLoader(
file: File,
) : PageLoader {
private val rar = Archive(file) private val rar = Archive(file)
override suspend fun getPages(): List<ReaderPage> { override suspend fun getPages(): List<ReaderPage> =
return rar.fileHeaders.asSequence() rar.fileHeaders
.asSequence()
.filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { rar.getInputStream(it) } } .filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { rar.getInputStream(it) } }
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.mapIndexed { i, header -> .mapIndexed { i, header ->
ReaderPage(i).apply { ReaderPage(i).apply {
stream = { getStream(rar, header) } stream = { getStream(rar, header) }
} }
} }.toList()
.toList()
}
override fun recycle() { override fun recycle() {
rar.close() rar.close()
@@ -8,20 +8,21 @@ import java.io.File
/** /**
* Loader used to load a chapter from a .zip or .cbz file. * Loader used to load a chapter from a .zip or .cbz file.
*/ */
class ZipPageLoader(file: File) : PageLoader { class ZipPageLoader(
private val zip = ZipFile(file) file: File,
) : PageLoader {
private val zip = ZipFile.builder().setFile(file).get()
override suspend fun getPages(): List<ReaderPage> { override suspend fun getPages(): List<ReaderPage> =
return zip.entries.asSequence() zip.entries
.asSequence()
.filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } } .filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
.mapIndexed { i, entry -> .mapIndexed { i, entry ->
ReaderPage(i).apply { ReaderPage(i).apply {
stream = { zip.getInputStream(entry) } stream = { zip.getInputStream(entry) }
} }
} }.toList()
.toList()
}
override fun recycle() { override fun recycle() {
zip.close() zip.close()
@@ -17,8 +17,7 @@ fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
comicInfo.genre?.value, comicInfo.genre?.value,
comicInfo.tags?.value, comicInfo.tags?.value,
comicInfo.categories?.value, comicInfo.categories?.value,
) ).distinct()
.distinct()
.joinToString(", ") { it.trim() } .joinToString(", ") { it.trim() }
.takeIf { it.isNotEmpty() } .takeIf { it.isNotEmpty() }
?.let { genre = it } ?.let { genre = it }
@@ -29,8 +28,7 @@ fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
comicInfo.colorist?.value, comicInfo.colorist?.value,
comicInfo.letterer?.value, comicInfo.letterer?.value,
comicInfo.coverArtist?.value, comicInfo.coverArtist?.value,
) ).flatMap { it.split(", ") }
.flatMap { it.split(", ") }
.distinct() .distinct()
.joinToString(", ") { it.trim() } .joinToString(", ") { it.trim() }
.takeIf { it.isNotEmpty() } .takeIf { it.isNotEmpty() }
@@ -58,6 +56,9 @@ data class ComicInfo(
val web: Web?, val web: Web?,
val publishingStatus: PublishingStatusTachiyomi?, val publishingStatus: PublishingStatusTachiyomi?,
val categories: CategoriesTachiyomi?, val categories: CategoriesTachiyomi?,
val day: Day?,
val month: Month?,
val year: Year?,
) { ) {
@Suppress("UNUSED") @Suppress("UNUSED")
@XmlElement(false) @XmlElement(false)
@@ -87,6 +88,24 @@ data class ComicInfo(
@XmlValue(true) val value: String = "", @XmlValue(true) val value: String = "",
) )
@Serializable
@XmlSerialName("Day", "", "")
data class Day(
@XmlValue(true) val value: Int = 0,
)
@Serializable
@XmlSerialName("Month", "", "")
data class Month(
@XmlValue(true) val value: Int = 0,
)
@Serializable
@XmlSerialName("Year", "", "")
data class Year(
@XmlValue(true) val value: Int = 0,
)
@Serializable @Serializable
@XmlSerialName("Summary", "", "") @XmlSerialName("Summary", "", "")
data class Summary( data class Summary(
@@ -181,14 +200,12 @@ enum class ComicInfoPublishingStatus(
; ;
companion object { companion object {
fun toComicInfoValue(value: Long): String { fun toComicInfoValue(value: Long): String =
return entries.firstOrNull { it.sMangaModelValue == value.toInt() }?.comicInfoValue entries.firstOrNull { it.sMangaModelValue == value.toInt() }?.comicInfoValue
?: UNKNOWN.comicInfoValue ?: UNKNOWN.comicInfoValue
}
fun toSMangaValue(value: String?): Int { fun toSMangaValue(value: String?): Int =
return entries.firstOrNull { it.comicInfoValue == value }?.sMangaModelValue entries.firstOrNull { it.comicInfoValue == value }?.sMangaModelValue
?: UNKNOWN.sMangaModelValue ?: UNKNOWN.sMangaModelValue
}
} }
} }
@@ -2,20 +2,40 @@ package eu.kanade.tachiyomi.source.model
// The class is originally sealed, Tachidesk adds new subclasses for serialization // The class is originally sealed, Tachidesk adds new subclasses for serialization
// sealed class Filter<T>(val name: String, var state: T) { // sealed class Filter<T>(val name: String, var state: T) {
open class Filter<T>(val name: String, var state: T) { open class Filter<T>(
open class Header(name: String) : Filter<Any>(name, 0) val name: String,
var state: T,
) {
open class Header(
name: String,
) : Filter<Any>(name, 0)
open class Separator(name: String = "") : Filter<Any>(name, 0) open class Separator(
name: String = "",
) : Filter<Any>(name, 0)
abstract class Select<V>(name: String, val values: Array<V>, state: Int = 0) : Filter<Int>(name, state) { abstract class Select<V>(
name: String,
val values: Array<V>,
state: Int = 0,
) : Filter<Int>(name, state) {
val displayValues get() = values.map { it.toString() } val displayValues get() = values.map { it.toString() }
} }
abstract class Text(name: String, state: String = "") : Filter<String>(name, state) abstract class Text(
name: String,
state: String = "",
) : Filter<String>(name, state)
abstract class CheckBox(name: String, state: Boolean = false) : Filter<Boolean>(name, state) abstract class CheckBox(
name: String,
state: Boolean = false,
) : Filter<Boolean>(name, state)
abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter<Int>(name, state) { abstract class TriState(
name: String,
state: Int = STATE_IGNORE,
) : Filter<Int>(name, state) {
fun isIgnored() = state == STATE_IGNORE fun isIgnored() = state == STATE_IGNORE
fun isIncluded() = state == STATE_INCLUDE fun isIncluded() = state == STATE_INCLUDE
@@ -29,11 +49,20 @@ open class Filter<T>(val name: String, var state: T) {
} }
} }
abstract class Group<V>(name: String, state: List<V>) : Filter<List<V>>(name, state) abstract class Group<V>(
name: String,
state: List<V>,
) : Filter<List<V>>(name, state)
abstract class Sort(name: String, val values: Array<String>, state: Selection? = null) : abstract class Sort(
Filter<Sort.Selection?>(name, state) { name: String,
data class Selection(val index: Int, val ascending: Boolean) val values: Array<String>,
state: Selection? = null,
) : Filter<Sort.Selection?>(name, state) {
data class Selection(
val index: Int,
val ascending: Boolean,
)
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@@ -1,5 +1,7 @@
package eu.kanade.tachiyomi.source.model package eu.kanade.tachiyomi.source.model
data class FilterList(val list: List<Filter<*>>) : List<Filter<*>> by list { data class FilterList(
val list: List<Filter<*>>,
) : List<Filter<*>> by list {
constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList()) constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList())
} }
@@ -1,3 +1,6 @@
package eu.kanade.tachiyomi.source.model package eu.kanade.tachiyomi.source.model
data class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean) data class MangasPage(
val mangas: List<SManga>,
val hasNextPage: Boolean,
)
@@ -24,8 +24,6 @@ interface SChapter : Serializable {
} }
companion object { companion object {
fun create(): SChapter { fun create(): SChapter = SChapterImpl()
return SChapterImpl()
}
} }
} }
@@ -25,34 +25,6 @@ interface SManga : Serializable {
var initialized: Boolean var initialized: Boolean
fun copyFrom(other: SManga) {
if (other.author != null) {
author = other.author
}
if (other.artist != null) {
artist = other.artist
}
if (other.description != null) {
description = other.description
}
if (other.genre != null) {
genre = other.genre
}
if (other.thumbnail_url != null) {
thumbnail_url = other.thumbnail_url
}
status = other.status
if (!initialized) {
initialized = other.initialized
}
}
companion object { companion object {
const val UNKNOWN = 0 const val UNKNOWN = 0
const val ONGOING = 1 const val ONGOING = 1
@@ -62,9 +34,7 @@ interface SManga : Serializable {
const val CANCELLED = 5 const val CANCELLED = 5
const val ON_HIATUS = 6 const val ON_HIATUS = 6
fun create(): SManga { fun create(): SManga = SMangaImpl()
return SMangaImpl()
}
} }
} }
@@ -66,9 +66,7 @@ abstract class HttpSource : CatalogueSource {
open val client: OkHttpClient open val client: OkHttpClient
get() = network.client get() = network.client
private fun generateId(): Long { private fun generateId(): Long = generateId("${name.lowercase()}/$lang/$versionId")
return generateId("${name.lowercase()}/$lang/$versionId")
}
/** /**
* Generates a unique ID for the source based on the provided [name], [lang] and * Generates a unique ID for the source based on the provided [name], [lang] and
@@ -121,13 +119,13 @@ abstract class HttpSource : CatalogueSource {
* @param page the page number to retrieve. * @param page the page number to retrieve.
*/ */
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga")) @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga"))
override fun fetchPopularManga(page: Int): Observable<MangasPage> { override fun fetchPopularManga(page: Int): Observable<MangasPage> =
return client.newCall(popularMangaRequest(page)) client
.newCall(popularMangaRequest(page))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
popularMangaParse(response) popularMangaParse(response)
} }
}
/** /**
* Returns the request for the popular manga given the page. * Returns the request for the popular manga given the page.
@@ -156,20 +154,19 @@ abstract class HttpSource : CatalogueSource {
page: Int, page: Int,
query: String, query: String,
filters: FilterList, filters: FilterList,
): Observable<MangasPage> { ): Observable<MangasPage> =
return Observable.defer { Observable
try { .defer {
client.newCall(searchMangaRequest(page, query, filters)).asObservableSuccess() try {
} catch (e: NoClassDefFoundError) { client.newCall(searchMangaRequest(page, query, filters)).asObservableSuccess()
// RxJava doesn't handle Errors, which tends to happen during global searches } catch (e: NoClassDefFoundError) {
// if an old extension using non-existent classes is still around // RxJava doesn't handle Errors, which tends to happen during global searches
throw RuntimeException(e) // if an old extension using non-existent classes is still around
} throw RuntimeException(e)
} }
.map { response -> }.map { response ->
searchMangaParse(response) searchMangaParse(response)
} }
}
/** /**
* Returns the request for the search manga given the page. * Returns the request for the search manga given the page.
@@ -197,13 +194,13 @@ abstract class HttpSource : CatalogueSource {
* @param page the page number to retrieve. * @param page the page number to retrieve.
*/ */
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates")) @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates"))
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> { override fun fetchLatestUpdates(page: Int): Observable<MangasPage> =
return client.newCall(latestUpdatesRequest(page)) client
.newCall(latestUpdatesRequest(page))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
latestUpdatesParse(response) latestUpdatesParse(response)
} }
}
/** /**
* Returns the request for latest manga given the page. * Returns the request for latest manga given the page.
@@ -227,18 +224,16 @@ abstract class HttpSource : CatalogueSource {
* @return the updated manga. * @return the updated manga.
*/ */
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
override suspend fun getMangaDetails(manga: SManga): SManga { override suspend fun getMangaDetails(manga: SManga): SManga = fetchMangaDetails(manga).awaitSingle()
return fetchMangaDetails(manga).awaitSingle()
}
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails")) @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails"))
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> =
return client.newCall(mangaDetailsRequest(manga)) client
.newCall(mangaDetailsRequest(manga))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
mangaDetailsParse(response).apply { initialized = true } mangaDetailsParse(response).apply { initialized = true }
} }
}
/** /**
* Returns the request for the details of a manga. Override only if it's needed to change the * Returns the request for the details of a manga. Override only if it's needed to change the
@@ -246,9 +241,7 @@ abstract class HttpSource : CatalogueSource {
* *
* @param manga the manga to be updated. * @param manga the manga to be updated.
*/ */
open fun mangaDetailsRequest(manga: SManga): Request { open fun mangaDetailsRequest(manga: SManga): Request = GET(baseUrl + manga.url, headers)
return GET(baseUrl + manga.url, headers)
}
/** /**
* Parses the response from the site and returns the details of a manga. * Parses the response from the site and returns the details of a manga.
@@ -275,9 +268,10 @@ abstract class HttpSource : CatalogueSource {
} }
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList")) @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList"))
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> =
return if (manga.status != SManga.LICENSED) { if (manga.status != SManga.LICENSED) {
client.newCall(chapterListRequest(manga)) client
.newCall(chapterListRequest(manga))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
chapterListParse(response) chapterListParse(response)
@@ -285,7 +279,6 @@ abstract class HttpSource : CatalogueSource {
} else { } else {
Observable.error(LicensedMangaChaptersException()) Observable.error(LicensedMangaChaptersException())
} }
}
/** /**
* Returns the request for updating the chapter list. Override only if it's needed to override * Returns the request for updating the chapter list. Override only if it's needed to override
@@ -293,9 +286,7 @@ abstract class HttpSource : CatalogueSource {
* *
* @param manga the manga to look for chapters. * @param manga the manga to look for chapters.
*/ */
protected open fun chapterListRequest(manga: SManga): Request { protected open fun chapterListRequest(manga: SManga): Request = GET(baseUrl + manga.url, headers)
return GET(baseUrl + manga.url, headers)
}
/** /**
* Parses the response from the site and returns a list of chapters. * Parses the response from the site and returns a list of chapters.
@@ -312,18 +303,16 @@ abstract class HttpSource : CatalogueSource {
* @return the pages for the chapter. * @return the pages for the chapter.
*/ */
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
override suspend fun getPageList(chapter: SChapter): List<Page> { override suspend fun getPageList(chapter: SChapter): List<Page> = fetchPageList(chapter).awaitSingle()
return fetchPageList(chapter).awaitSingle()
}
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList")) @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList"))
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> { override fun fetchPageList(chapter: SChapter): Observable<List<Page>> =
return client.newCall(pageListRequest(chapter)) client
.newCall(pageListRequest(chapter))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
pageListParse(response) pageListParse(response)
} }
}
/** /**
* Returns the request for getting the page list. Override only if it's needed to override the * Returns the request for getting the page list. Override only if it's needed to override the
@@ -331,9 +320,7 @@ abstract class HttpSource : CatalogueSource {
* *
* @param chapter the chapter whose page list has to be fetched. * @param chapter the chapter whose page list has to be fetched.
*/ */
protected open fun pageListRequest(chapter: SChapter): Request { protected open fun pageListRequest(chapter: SChapter): Request = GET(baseUrl + chapter.url, headers)
return GET(baseUrl + chapter.url, headers)
}
/** /**
* Parses the response from the site and returns a list of pages. * Parses the response from the site and returns a list of pages.
@@ -350,16 +337,14 @@ abstract class HttpSource : CatalogueSource {
* @param page the page whose source image has to be fetched. * @param page the page whose source image has to be fetched.
*/ */
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
open suspend fun getImageUrl(page: Page): String { open suspend fun getImageUrl(page: Page): String = fetchImageUrl(page).awaitSingle()
return fetchImageUrl(page).awaitSingle()
}
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl")) @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl"))
open fun fetchImageUrl(page: Page): Observable<String> { open fun fetchImageUrl(page: Page): Observable<String> =
return client.newCall(imageUrlRequest(page)) client
.newCall(imageUrlRequest(page))
.asObservableSuccess() .asObservableSuccess()
.map { imageUrlParse(it) } .map { imageUrlParse(it) }
}
/** /**
* Returns the request for getting the url to the source image. Override only if it's needed to * Returns the request for getting the url to the source image. Override only if it's needed to
@@ -367,9 +352,7 @@ abstract class HttpSource : CatalogueSource {
* *
* @param page the chapter 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 { protected open fun imageUrlRequest(page: Page): Request = GET(page.url, headers)
return GET(page.url, headers)
}
/** /**
* Parses the response from the site and returns the absolute url to the source image. * Parses the response from the site and returns the absolute url to the source image.
@@ -385,10 +368,10 @@ abstract class HttpSource : CatalogueSource {
* @since extensions-lib 1.5 * @since extensions-lib 1.5
* @param page the page whose source image has to be downloaded. * @param page the page whose source image has to be downloaded.
*/ */
open suspend fun getImage(page: Page): Response { open suspend fun getImage(page: Page): Response =
return client.newCachelessCallWithProgress(imageRequest(page), page) client
.newCachelessCallWithProgress(imageRequest(page), page)
.awaitSuccess() .awaitSuccess()
}
/** /**
* Returns the request for getting the source image. Override only if it's needed to override * Returns the request for getting the source image. Override only if it's needed to override
@@ -396,9 +379,7 @@ abstract class HttpSource : CatalogueSource {
* *
* @param page the chapter whose page list has to be fetched * @param page the chapter whose page list has to be fetched
*/ */
protected open fun imageRequest(page: Page): Request { protected open fun imageRequest(page: Page): Request = GET(page.imageUrl!!, headers)
return GET(page.imageUrl!!, headers)
}
/** /**
* Assigns the url of the chapter without the scheme and domain. It saves some redundancy from * Assigns the url of the chapter without the scheme and domain. It saves some redundancy from
@@ -425,8 +406,8 @@ abstract class HttpSource : CatalogueSource {
* *
* @param orig the full url. * @param orig the full url.
*/ */
private fun getUrlWithoutDomain(orig: String): String { private fun getUrlWithoutDomain(orig: String): String =
return try { try {
val uri = URI(orig.replace(" ", "%20")) val uri = URI(orig.replace(" ", "%20"))
var out = uri.path var out = uri.path
if (uri.query != null) { if (uri.query != null) {
@@ -439,7 +420,6 @@ abstract class HttpSource : CatalogueSource {
} catch (e: URISyntaxException) { } catch (e: URISyntaxException) {
orig orig
} }
}
/** /**
* Returns the url of the provided manga * Returns the url of the provided manga
@@ -448,9 +428,7 @@ abstract class HttpSource : CatalogueSource {
* @param manga the manga * @param manga the manga
* @return url of the manga * @return url of the manga
*/ */
open fun getMangaUrl(manga: SManga): String { open fun getMangaUrl(manga: SManga): String = mangaDetailsRequest(manga).url.toString()
return mangaDetailsRequest(manga).url.toString()
}
/** /**
* Returns the url of the provided chapter * Returns the url of the provided chapter
@@ -459,9 +437,7 @@ abstract class HttpSource : CatalogueSource {
* @param chapter the chapter * @param chapter the chapter
* @return url of the chapter * @return url of the chapter
*/ */
open fun getChapterUrl(chapter: SChapter): String { open fun getChapterUrl(chapter: SChapter): String = pageListRequest(chapter).url.toString()
return pageListRequest(chapter).url.toString()
}
/** /**
* Called before inserting a new chapter into database. Use it if you need to override chapter * Called before inserting a new chapter into database. Use it if you need to override chapter
@@ -138,9 +138,7 @@ abstract class ParsedHttpSource : HttpSource() {
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun mangaDetailsParse(response: Response): SManga { override fun mangaDetailsParse(response: Response): SManga = mangaDetailsParse(response.asJsoup())
return mangaDetailsParse(response.asJsoup())
}
/** /**
* Returns the details of the manga from the given [document]. * Returns the details of the manga from the given [document].
@@ -176,9 +174,7 @@ abstract class ParsedHttpSource : HttpSource() {
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> = pageListParse(response.asJsoup())
return pageListParse(response.asJsoup())
}
/** /**
* Returns a page list from the given document. * Returns a page list from the given document.
@@ -192,9 +188,7 @@ abstract class ParsedHttpSource : HttpSource() {
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun imageUrlParse(response: Response): String { override fun imageUrlParse(response: Response): String = imageUrlParse(response.asJsoup())
return imageUrlParse(response.asJsoup())
}
/** /**
* Returns the absolute url to the source image from the document. * Returns the absolute url to the source image from the document.
@@ -8,25 +8,17 @@ import org.jsoup.nodes.Element
fun Element.selectText( fun Element.selectText(
css: String, css: String,
defaultValue: String? = null, defaultValue: String? = null,
): String? { ): String? = select(css).first()?.text() ?: defaultValue
return select(css).first()?.text() ?: defaultValue
}
fun Element.selectInt( fun Element.selectInt(
css: String, css: String,
defaultValue: Int = 0, defaultValue: Int = 0,
): Int { ): Int = select(css).first()?.text()?.toInt() ?: defaultValue
return select(css).first()?.text()?.toInt() ?: defaultValue
}
fun Element.attrOrText(css: String): String { fun Element.attrOrText(css: String): String = if (css != "text") attr(css) else text()
return if (css != "text") attr(css) else text()
}
/** /**
* Returns a Jsoup document for this response. * Returns a Jsoup document for this response.
* @param html the body of the response. Use only if the body was read before calling this method. * @param html the body of the response. Use only if the body was read before calling this method.
*/ */
fun Response.asJsoup(html: String? = null): Document { fun Response.asJsoup(html: String? = null): Document = Jsoup.parse(html ?: body.string(), request.url.toString())
return Jsoup.parse(html ?: body.string(), request.url.toString())
}
@@ -68,15 +68,14 @@ object ChapterRecognition {
* @param match result of regex * @param match result of regex
* @return chapter number if found else null * @return chapter number if found else null
*/ */
private fun getChapterNumberFromMatch(match: MatchResult): Double { private fun getChapterNumberFromMatch(match: MatchResult): Double =
return match.let { match.let {
val initial = it.groups[1]?.value?.toDouble()!! val initial = it.groups[1]?.value?.toDouble()!!
val subChapterDecimal = it.groups[2]?.value val subChapterDecimal = it.groups[2]?.value
val subChapterAlpha = it.groups[3]?.value val subChapterAlpha = it.groups[3]?.value
val addition = checkForDecimal(subChapterDecimal, subChapterAlpha) val addition = checkForDecimal(subChapterDecimal, subChapterAlpha)
initial.plus(addition) initial.plus(addition)
} }
}
/** /**
* Check for decimal in received strings * Check for decimal in received strings
@@ -1,11 +1,10 @@
package eu.kanade.tachiyomi.util.chapter package eu.kanade.tachiyomi.util.chapter
object ChapterSanitizer { object ChapterSanitizer {
fun String.sanitize(title: String): String { fun String.sanitize(title: String): String =
return trim() trim()
.removePrefix(title) .removePrefix(title)
.trim(*CHAPTER_TRIM_CHARS) .trim(*CHAPTER_TRIM_CHARS)
}
private val CHAPTER_TRIM_CHARS = private val CHAPTER_TRIM_CHARS =
arrayOf( arrayOf(
@@ -5,29 +5,35 @@ import java.security.MessageDigest
object Hash { object Hash {
private val chars = private val chars =
charArrayOf( charArrayOf(
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
'a', 'b', 'c', 'd', 'e', 'f', '1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'a',
'b',
'c',
'd',
'e',
'f',
) )
private val MD5 get() = MessageDigest.getInstance("MD5") private val MD5 get() = MessageDigest.getInstance("MD5")
private val SHA256 get() = MessageDigest.getInstance("SHA-256") private val SHA256 get() = MessageDigest.getInstance("SHA-256")
fun sha256(bytes: ByteArray): String { fun sha256(bytes: ByteArray): String = encodeHex(SHA256.digest(bytes))
return encodeHex(SHA256.digest(bytes))
}
fun sha256(string: String): String { fun sha256(string: String): String = sha256(string.toByteArray())
return sha256(string.toByteArray())
}
fun md5(bytes: ByteArray): String { fun md5(bytes: ByteArray): String = encodeHex(MD5.digest(bytes))
return encodeHex(MD5.digest(bytes))
}
fun md5(string: String): String { fun md5(string: String): String = md5(string.toByteArray())
return md5(string.toByteArray())
}
private fun encodeHex(data: ByteArray): String { private fun encodeHex(data: ByteArray): String {
val l = data.size val l = data.size
@@ -10,13 +10,12 @@ import kotlin.math.floor
fun String.chop( fun String.chop(
count: Int, count: Int,
replacement: String = "", replacement: String = "",
): String { ): String =
return if (length > count) { if (length > count) {
take(count - replacement.length) + replacement take(count - replacement.length) + replacement
} else { } else {
this this
} }
}
/** /**
* Replaces the given string to have at most [count] characters using [replacement] near the center. * Replaces the given string to have at most [count] characters using [replacement] near the center.
@@ -46,9 +45,7 @@ fun String.compareToCaseInsensitiveNaturalOrder(other: String): Int {
/** /**
* Returns the size of the string as the number of bytes. * Returns the size of the string as the number of bytes.
*/ */
fun String.byteSize(): Int { fun String.byteSize(): Int = toByteArray(Charsets.UTF_8).size
return toByteArray(Charsets.UTF_8).size
}
/** /**
* Returns a string containing the first [n] bytes from this string, or the entire string if this * Returns a string containing the first [n] bytes from this string, or the entire string if this
@@ -11,11 +11,13 @@ import java.io.InputStream
/** /**
* Wrapper over ZipFile to load files in epub format. * Wrapper over ZipFile to load files in epub format.
*/ */
class EpubFile(file: File) : Closeable { class EpubFile(
file: File,
) : Closeable {
/** /**
* Zip file of this epub. * Zip file of this epub.
*/ */
private val zip = ZipFile(file) private val zip = ZipFile.builder().setFile(file).get()
/** /**
* Path separator used by this epub. * Path separator used by this epub.
@@ -32,16 +34,12 @@ class EpubFile(file: File) : Closeable {
/** /**
* Returns an input stream for reading the contents of the specified zip file entry. * Returns an input stream for reading the contents of the specified zip file entry.
*/ */
fun getInputStream(entry: ZipArchiveEntry): InputStream { fun getInputStream(entry: ZipArchiveEntry): InputStream = zip.getInputStream(entry)
return zip.getInputStream(entry)
}
/** /**
* Returns the zip file entry for the specified name, or null if not found. * Returns the zip file entry for the specified name, or null if not found.
*/ */
fun getEntry(name: String): ZipArchiveEntry? { fun getEntry(name: String): ZipArchiveEntry? = zip.getEntry(name)
return zip.getEntry(name)
}
/** /**
* Returns the path of all the images found in the epub file. * Returns the path of all the images found in the epub file.
@@ -81,7 +79,8 @@ class EpubFile(file: File) : Closeable {
*/ */
fun getPagesFromDocument(document: Document): List<String> { fun getPagesFromDocument(document: Document): List<String> {
val pages = val pages =
document.select("manifest > item") document
.select("manifest > item")
.filter { node -> "application/xhtml+xml" == node.attr("media-type") } .filter { node -> "application/xhtml+xml" == node.attr("media-type") }
.associateBy { it.attr("id") } .associateBy { it.attr("id") }
@@ -7,7 +7,7 @@ package suwayomi.tachidesk.global.controller
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import io.javalin.http.HttpCode import io.javalin.http.HttpStatus
import suwayomi.tachidesk.global.impl.GlobalMeta import suwayomi.tachidesk.global.impl.GlobalMeta
import suwayomi.tachidesk.server.util.formParam import suwayomi.tachidesk.server.util.formParam
import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.handler
@@ -28,7 +28,7 @@ object GlobalMetaController {
ctx.status(200) ctx.status(200)
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
}, },
) )
@@ -48,8 +48,8 @@ object GlobalMetaController {
ctx.status(200) ctx.status(200)
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpStatus.NOT_FOUND)
}, },
) )
} }
@@ -7,7 +7,7 @@ package suwayomi.tachidesk.global.controller
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import io.javalin.http.HttpCode import io.javalin.http.HttpStatus
import suwayomi.tachidesk.global.impl.About import suwayomi.tachidesk.global.impl.About
import suwayomi.tachidesk.global.impl.AboutDataClass import suwayomi.tachidesk.global.impl.AboutDataClass
import suwayomi.tachidesk.global.impl.AppUpdate import suwayomi.tachidesk.global.impl.AppUpdate
@@ -31,7 +31,7 @@ object SettingsController {
ctx.json(About.getAbout()) ctx.json(About.getAbout())
}, },
withResults = { withResults = {
json<AboutDataClass>(HttpCode.OK) json<AboutDataClass>(HttpStatus.OK)
}, },
) )
@@ -45,12 +45,13 @@ object SettingsController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.future( ctx.future {
future { AppUpdate.checkUpdate() }, future { AppUpdate.checkUpdate() }
) .thenApply { ctx.json(it) }
}
}, },
withResults = { withResults = {
json<Array<UpdateDataClass>>(HttpCode.OK) json<Array<UpdateDataClass>>(HttpStatus.OK)
}, },
) )
} }
@@ -12,6 +12,7 @@ import suwayomi.tachidesk.server.generated.BuildConfig
data class AboutDataClass( data class AboutDataClass(
val name: String, val name: String,
val version: String, val version: String,
@Deprecated("The version includes the revision as the patch number")
val revision: String, val revision: String,
val buildType: String, val buildType: String,
val buildTime: Long, val buildTime: Long,
@@ -20,8 +21,8 @@ data class AboutDataClass(
) )
object About { object About {
fun getAbout(): AboutDataClass { fun getAbout(): AboutDataClass =
return AboutDataClass( AboutDataClass(
BuildConfig.NAME, BuildConfig.NAME,
BuildConfig.VERSION, BuildConfig.VERSION,
BuildConfig.REVISION, BuildConfig.REVISION,
@@ -30,5 +31,4 @@ object About {
BuildConfig.GITHUB, BuildConfig.GITHUB,
BuildConfig.DISCORD, BuildConfig.DISCORD,
) )
}
} }

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