mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-11 23:04:33 -05:00
Compare commits
19 Commits
3a78453a02
...
renovate/p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcea37ba29 | ||
|
|
612768faeb | ||
|
|
9a1745b626 | ||
|
|
323d58717e | ||
|
|
4d7b7617a9 | ||
|
|
35b48114c6 | ||
|
|
3031aa7ccd | ||
|
|
c79486b8be | ||
|
|
e2fd15158c | ||
|
|
b6de3c3e39 | ||
|
|
656d86c6f6 | ||
|
|
a0fbff5756 | ||
|
|
2d535b44d8 | ||
|
|
c8f5d83e9c | ||
|
|
be5e3f022e | ||
|
|
40a21fabca | ||
|
|
b33069f107 | ||
|
|
14ab3aa9f4 | ||
|
|
bfa0038f53 |
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -42,7 +42,7 @@ body:
|
||||
label: Suwayomi-Server version
|
||||
description: You can find your Suwayomi-Server version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "v2.2.2100"
|
||||
Example: "v2.3.2223"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -143,11 +143,13 @@ body:
|
||||
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 checked the ongoing preview changelog of **[Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI/blob/master/CHANGELOG.md)** and **[Suwayomi-Server](https://github.com/Suwayomi/Suwayomi-Server/blob/master/CHANGELOG.md)** and this bug has **NOT** been listed as fixed
|
||||
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)**.
|
||||
- label: I have updated the (**[Suwayomi-WebUI](https://github.com/suwayomi/suwayomi-webui/releases/latest)** and **[Suwayomi-Server](https://github.com/suwayomi/suwayomi-server/releases/latest)**) to the latest versions
|
||||
required: true
|
||||
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
||||
required: true
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -31,7 +31,7 @@ body:
|
||||
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)**.
|
||||
- label: I have updated the (**[Suwayomi-WebUI](https://github.com/suwayomi/suwayomi-webui/releases/latest)** and **[Suwayomi-Server](https://github.com/suwayomi/suwayomi-server/releases/latest)**) to the latest versions
|
||||
required: true
|
||||
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
||||
required: true
|
||||
27
CHANGELOG.md
27
CHANGELOG.md
@@ -6,10 +6,33 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## [Unreleased] (Preview)
|
||||
|
||||
### Added
|
||||
- .
|
||||
|
||||
### Changed
|
||||
- .
|
||||
|
||||
### Fixed
|
||||
- .
|
||||
|
||||
## [v2.3.2223] + [WebUI: v20260509.01] - 2026-06-30
|
||||
|
||||
### Major Changes
|
||||
|
||||
#### Added [SyncYomi](https://github.com/syncyomi/syncyomi) support
|
||||
This allows you to sync your server manga with other Mihon-based forks! As long as the fork supports SyncYomi it can be sync with!
|
||||
|
||||
#### Support Extension API v1.6
|
||||
This update allows Suwayomi to load and use v1.6 extensions, it is a minor improvement over the existing 1.4 extension API that cleans up much of what we had! It is the basis of future extension APIs that will allow for further development.
|
||||
|
||||
> [!WARNING]
|
||||
> Please back up your extension repos, because of the new extension stores system you may lose them in the update process and may need to re-add them.
|
||||
|
||||
### Added
|
||||
- (**Sync**) Added [SyncYomi](https://github.com/syncyomi/syncyomi) support
|
||||
- (**OPDS**) Add option to skip chapter metadata feed providing direct stream/download links
|
||||
- (**Extension/API**) Support Extensions API v1.6
|
||||
- (**Tracker/API**) Add mutation to bind existing track record
|
||||
|
||||
### Changed
|
||||
- (**Database/H2**) Use the latest H2 database engine
|
||||
@@ -426,6 +449,7 @@ Huge thanks to @martinek who pulled the most of the weight this release!
|
||||
|
||||
<!-- WEBUI LINKS -->
|
||||
|
||||
[WebUI: v20260509.01]: https://github.com/Suwayomi/Suwayomi-WebUI/blob/master/CHANGELOG.md#2026050901-r3147---2026-05-09
|
||||
[WebUI: v20260508.01]: https://github.com/Suwayomi/Suwayomi-WebUI/blob/master/CHANGELOG.md#2026050801-r3136---2026-05-08
|
||||
[WebUI: v20251230.01]: https://github.com/Suwayomi/Suwayomi-WebUI/blob/master/CHANGELOG.md#2025123001-r2937---2025-12-30
|
||||
[WebUI: v20250801.01]: https://github.com/Suwayomi/Suwayomi-WebUI/blob/master/CHANGELOG.md#2025080101-r2717---2025-08-01
|
||||
@@ -452,7 +476,8 @@ Huge thanks to @martinek who pulled the most of the weight this release!
|
||||
|
||||
<!-- SERVER LINKS -->
|
||||
|
||||
[unreleased]: https://github.com/suwayomi/suwayomi-server/compare/v2.2.2100...HEAD
|
||||
[unreleased]: https://github.com/suwayomi/suwayomi-server/compare/v2.3.2223...HEAD
|
||||
[v2.3.2223]: https://github.com/suwayomi/suwayomi-server/compare/v2.1.2100...v2.3.2223
|
||||
[v2.2.2100]: https://github.com/suwayomi/suwayomi-server/compare/v2.1.1867...v2.2.2100
|
||||
[v2.1.1867]: https://github.com/suwayomi/suwayomi-server/compare/v2.0.1727...v2.1.1867
|
||||
[v2.0.1727]: https://github.com/suwayomi/suwayomi-server/compare/v1.1.1...v2.0.1727
|
||||
|
||||
@@ -10,9 +10,9 @@ import java.io.BufferedReader
|
||||
const val MainClass = "suwayomi.tachidesk.MainKt"
|
||||
|
||||
// should be bumped with each stable release
|
||||
val getTachideskVersion = { "v2.2.${getCommitCount()}" }
|
||||
val getTachideskVersion = { "v2.3.${getCommitCount()}" }
|
||||
|
||||
val webUIRevisionTag = "r3136"
|
||||
val webUIRevisionTag = "r3147"
|
||||
|
||||
val webviewJbrRelease = "jbr-release-25.0.3b508.4"
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ server.maxLogFolderSize = "100mb"
|
||||
server.extensionStores = []
|
||||
server.maxSourcesInParallel = 6
|
||||
```
|
||||
- `server.extensionStores` is a list of extension stores(previously called repositories) for custom sources. Uses the same format as Mihon; each entry is expected to be a string URL pointing to a JSON or PROTOBUF file representing the repository.
|
||||
- `server.extensionStores` is a list of extension stores (previously called repositories) for custom sources. Uses the same format as Mihon; each entry is expected to be a string URL pointing to a JSON or PROTOBUF file representing the repository.
|
||||
- `server.maxSourcesInParallel = 6` sets how many sources can do requests (updates, downloads) in parallel. Updates/downloads are grouped by source and all mangas of a source are updated/downloaded synchronously. Range: 1 <= n <= 20.
|
||||
|
||||
### Backup
|
||||
|
||||
@@ -8,8 +8,8 @@ javalin = "7.2.2"
|
||||
jte = "3.2.4"
|
||||
jackson = "3.2.0" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
|
||||
exposed = "1.2.0"
|
||||
dex2jar = "2.4.36"
|
||||
polyglot = "25.0.3"
|
||||
dex2jar = "2.4.37"
|
||||
polyglot = "25.1.3"
|
||||
settings = "1.3.0"
|
||||
twelvemonkeys = "3.13.1"
|
||||
graphqlkotlin = "10.0.0"
|
||||
@@ -47,6 +47,7 @@ okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
|
||||
okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp" }
|
||||
okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", version.ref = "okhttp" }
|
||||
okhttp-zstd = { module = "com.squareup.okhttp3:okhttp-zstd", version.ref = "okhttp" }
|
||||
okio = "com.squareup.okio:okio:3.17.0"
|
||||
|
||||
# Javalin api
|
||||
@@ -228,6 +229,7 @@ okhttp = [
|
||||
"okhttp-logging",
|
||||
"okhttp-dnsoverhttps",
|
||||
"okhttp-brotli",
|
||||
"okhttp-zstd",
|
||||
]
|
||||
javalin = [
|
||||
"javalin-core",
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
<!-- OPDS errors -->
|
||||
<string name="opds_error_manga_not_found">Series with ID %1$d not found.</string>
|
||||
<string name="opds_error_chapter_not_found">Chapter with index %1$d not found.</string>
|
||||
<string name="opds_error_chapters_not_found">No chapters found or the source is unreachable on page %1$d.</string>
|
||||
|
||||
<!-- OPDS facets (Filters and Sorting) -->
|
||||
<string name="opds_facetgroup_sort_order">Sort Order</string>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<string name="opds_feeds_root">Suwayomi OPDS Katalog</string>
|
||||
<string name="opds_feeds_chapter_details">%1$s | %2$s | Details</string>
|
||||
<string name="opds_feeds_sources_title">Alle Quellen</string>
|
||||
<string name="opds_feeds_genres_title">Genres</string>
|
||||
<string name="opds_feeds_genres_title">Genren</string>
|
||||
<string name="opds_feeds_genres_entry_content">Durchsuche Serien nach Genre</string>
|
||||
<string name="opds_feeds_status_entry_content">Durchsuche Serien nach Publikationsstatus</string>
|
||||
<string name="opds_feeds_languages_title">Sprachen</string>
|
||||
@@ -122,4 +122,7 @@
|
||||
<string name="webview_label_login_required">Deine Konfiguration erfordert die Anmeldung. Bitte gib Benutzername und Passwort ein.</string>
|
||||
<string name="opds_linktitle_first_page">Erste Seite</string>
|
||||
<string name="opds_linktitle_last_page">Letzte Seite</string>
|
||||
<string name="opds_error_chapters_not_found">Keine Kapitel gefunden oder die Quelle ist nicht erreichbar auf Seite %1$d.</string>
|
||||
<string name="opds_chapter_title_fallback">Kapitel %1$s</string>
|
||||
<string name="opds_chapter_title_oneshot">Oneshot</string>
|
||||
</resources>
|
||||
|
||||
127
server/i18n/src/commonMain/moko-resources/values/el/strings.xml
Normal file
127
server/i18n/src/commonMain/moko-resources/values/el/strings.xml
Normal file
@@ -0,0 +1,127 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="opds_feeds_root">Suwayomi OPDS Κατάλογος</string>
|
||||
<string name="opds_feeds_manga_chapters">Κεφάλαια %1$s</string>
|
||||
<string name="opds_feeds_chapter_details">%1$s | %2$s | Λεπτομέρειες</string>
|
||||
<string name="opds_feeds_explore_title">Εξερεύνηση</string>
|
||||
<string name="opds_feeds_explore_entry_content">Εξερεύνησε νέες σειρές από τις πηγές σου</string>
|
||||
<string name="opds_feeds_history_title">Ιστορικό</string>
|
||||
<string name="opds_feeds_history_entry_content">Πρόσφατα διαβασμένα κεφάλαια</string>
|
||||
<string name="opds_feeds_all_series_in_library_title">Όλες οι σειρές</string>
|
||||
<string name="opds_feeds_all_series_in_library_entry_content">Περιήγηση σε όλες τις σειρές της βιβλιοθήκης σου</string>
|
||||
<string name="opds_feeds_library_sources_title">Πηγές</string>
|
||||
<string name="opds_feeds_library_sources_entry_content">Περιήγηση σε σειρές της βιβλιοθήκης σου φιλτραρισμένες κατά πηγή</string>
|
||||
<string name="opds_feeds_categories_title">Κατηγορίες</string>
|
||||
<string name="opds_feeds_categories_entry_content">Περιήγηση σε σειρές οργανωμένες κατά κατηγορία</string>
|
||||
<string name="opds_feeds_genres_title">Είδη</string>
|
||||
<string name="opds_feeds_genres_entry_content">Περιήγηση σε σειρές κατά ετικέτες είδους</string>
|
||||
<string name="opds_feeds_status_title">Κατάσταση</string>
|
||||
<string name="opds_feeds_status_entry_content">Περιήγηση σε σειρές κατά κατάσταση δημοσίευσης</string>
|
||||
<string name="opds_feeds_languages_title">Γλώσσες</string>
|
||||
<string name="opds_feeds_languages_entry_content">Περιήγηση σε σειρές κατά γλώσσα περιεχομένου</string>
|
||||
<string name="opds_feeds_library_updates_title">Ιστορικό ενημερώσεων βιβλιοθήκης</string>
|
||||
<string name="opds_feeds_library_updates_entry_content">Πρόσφατα ενημερωμένα κεφάλαια από τη βιβλιοθήκη σου</string>
|
||||
<string name="opds_feeds_search_results_title">Αποτελέσματα αναζήτησης</string>
|
||||
<string name="opds_feeds_sources_title">Όλες οι πηγές</string>
|
||||
<string name="opds_feeds_category_specific_title">Κατηγορία: %1$s</string>
|
||||
<string name="opds_feeds_genre_specific_title">Είδος: %1$s</string>
|
||||
<string name="opds_feeds_status_specific_title">Κατάσταση: %1$s</string>
|
||||
<string name="opds_feeds_language_specific_title">Γλώσσα: %1$s</string>
|
||||
<string name="opds_feeds_source_specific_title">Πηγή: %1$s</string>
|
||||
<string name="opds_feeds_library_source_specific_title">Βιβλιοθήκη - Πηγή: %1$s</string>
|
||||
<string name="opds_feeds_source_specific_popular_title">Πηγή: %1$s - Δημοφιλές</string>
|
||||
<string name="opds_feeds_source_specific_latest_title">Πηγή: %1$s - Τελευταίο</string>
|
||||
<string name="opds_search_shortname">Suwayomi OPDS Αναζήτηση</string>
|
||||
<string name="opds_search_description">Αναζήτηση σειρών στον κατάλογο.</string>
|
||||
<string name="opds_error_manga_not_found">Η σειρά με ID %1$d δεν βρέθηκε.</string>
|
||||
<string name="opds_error_chapter_not_found">Το κεφάλαιο με δείκτη %1$d δεν βρέθηκε.</string>
|
||||
<string name="opds_facetgroup_sort_order">Σειρά ταξινόμησης</string>
|
||||
<string name="opds_facetgroup_filter_read_status">Φίλτρο κατά κατάσταση ανάγνωσης</string>
|
||||
<string name="opds_facetgroup_filter_content">Φίλτρο περιεχομένου</string>
|
||||
<string name="opds_facetgroup_filter_source">Φίλτρο κατά πηγή</string>
|
||||
<string name="opds_facetgroup_filter_category">Φίλτρο κατά κατηγορία</string>
|
||||
<string name="opds_facetgroup_filter_status">Φίλτρο κατά κατάσταση</string>
|
||||
<string name="opds_facetgroup_filter_language">Φίλτρο κατά γλώσσα</string>
|
||||
<string name="opds_facetgroup_filter_genre">Φίλτρο κατά είδος</string>
|
||||
<string name="opds_facet_sort_oldest_first">Παλαιότερα πρώτα</string>
|
||||
<string name="opds_facet_sort_newest_first">Νεότερα πρώτα</string>
|
||||
<string name="opds_facet_sort_date_asc">Ημερομηνία αύξουσα</string>
|
||||
<string name="opds_facet_sort_date_desc">Ημερομηνία φθίνουσα</string>
|
||||
<string name="opds_facet_sort_popular">Δημοφιλές</string>
|
||||
<string name="opds_facet_sort_latest">Τελευταίο</string>
|
||||
<string name="opds_facet_sort_alpha_asc">Αλφαβητικά Α-Ω</string>
|
||||
<string name="opds_facet_sort_alpha_desc">Αλφαβητικά Ω-Α</string>
|
||||
<string name="opds_facet_sort_last_read_desc">Τελευταία ανάγνωση</string>
|
||||
<string name="opds_facet_sort_latest_chapter_desc">Τελευταίο κεφάλαιο</string>
|
||||
<string name="opds_facet_sort_date_added_desc">Ημερομηνία προσθήκης</string>
|
||||
<string name="opds_facet_sort_unread_desc">Αδιάβαστα κεφάλαια</string>
|
||||
<string name="opds_facet_filter_all">Όλα</string>
|
||||
<string name="opds_facet_filter_all_chapters">Όλα τα κεφάλαια</string>
|
||||
<string name="opds_facet_filter_unread_only">Αδιάβαστα</string>
|
||||
<string name="opds_facet_filter_read_only">Διαβασμένα</string>
|
||||
<string name="opds_facet_filter_downloaded">Ληφθέντα</string>
|
||||
<string name="opds_facet_filter_ongoing">Σε εξέλιξη</string>
|
||||
<string name="opds_facet_filter_completed">Ολοκληρωμένα</string>
|
||||
<string name="opds_facet_all_sources">Όλες οι πηγές</string>
|
||||
<string name="opds_facet_all_categories">Όλες οι κατηγορίες</string>
|
||||
<string name="opds_facet_all_statuses">Όλες οι καταστάσεις</string>
|
||||
<string name="opds_facet_all_languages">Όλες οι γλώσσες</string>
|
||||
<string name="opds_facet_all_genres">Όλα τα είδη</string>
|
||||
<string name="opds_linktitle_catalog_root">Ρίζα καταλόγου</string>
|
||||
<string name="opds_linktitle_search_catalog">Αναζήτηση καταλόγου</string>
|
||||
<string name="opds_linktitle_first_page">Πρώτη σελίδα</string>
|
||||
<string name="opds_linktitle_previous_page">Προηγούμενη σελίδα</string>
|
||||
<string name="opds_linktitle_next_page">Επόμενη σελίδα</string>
|
||||
<string name="opds_linktitle_last_page">Τελευταία σελίδα</string>
|
||||
<string name="opds_linktitle_self_feed">Τρέχουσα ροή</string>
|
||||
<string name="opds_linktitle_view_on_web">Προβολή στο Web</string>
|
||||
<string name="opds_linktitle_stream_pages_start">Διάβασε Online</string>
|
||||
<string name="opds_linktitle_stream_pages_continue">Συνέχισε να διαβάζεις Online</string>
|
||||
<string name="opds_linktitle_stream_pages_start_local">Διάβασε Online (Τοπική πρόοδος)</string>
|
||||
<string name="opds_linktitle_stream_pages_continue_local">Συνέχισε να διαβάζεις Online (Τοπική πρόοδος)</string>
|
||||
<string name="opds_linktitle_stream_pages_start_remote">Διάβασε Online (Συγχρονισμένο από %1$s)</string>
|
||||
<string name="opds_linktitle_stream_pages_continue_remote">Συνέχισε να διαβάζεις Online (Συγχρονισμένο από %1$s)</string>
|
||||
<string name="opds_linktitle_download_cbz">Λήψη CBZ</string>
|
||||
<string name="opds_linktitle_chapter_cover">Εξώφυλλο κεφαλαίου</string>
|
||||
<string name="opds_linktitle_view_chapter_details">Λεπτομέρειες κεφαλαίου & Σελίδες</string>
|
||||
<string name="opds_chapter_status_read">✅</string>
|
||||
<string name="opds_chapter_status_in_progress">⌛</string>
|
||||
<string name="opds_chapter_status_unread">⭕</string>
|
||||
<string name="opds_chapter_status_downloaded">⬇️</string>
|
||||
<string name="opds_chapter_status_synced">🌐</string>
|
||||
<string name="opds_chapter_details_base">Σειρά: %1$s | %2$s</string>
|
||||
<string name="opds_chapter_details_scanlator">| Από %1$s</string>
|
||||
<string name="opds_chapter_details_progress">| Πρόοδος: %1$d από %2$d</string>
|
||||
<string name="opds_manga_summary_status">Κατάσταση: %1$s</string>
|
||||
<string name="opds_manga_summary_source">Πηγή: %1$s</string>
|
||||
<string name="opds_manga_summary_language">Γλώσσα: %1$s</string>
|
||||
<string name="manga_status_ongoing">Σε εξέλιξη</string>
|
||||
<string name="manga_status_completed">Ολοκληρωμένο</string>
|
||||
<string name="manga_status_licensed">Υπό άδεια</string>
|
||||
<string name="manga_status_publishing_finished">Δημοσίευση ολοκληρωμένη</string>
|
||||
<string name="manga_status_cancelled">Ακυρωμένο</string>
|
||||
<string name="manga_status_on_hiatus">Σε παύση</string>
|
||||
<string name="manga_status_unknown">Άγνωστο</string>
|
||||
<string name="label_error">Σφάλμα</string>
|
||||
<string name="label_version">Έκδοση <xliff:g id="version" example="v2.0.1833">%1$s</xliff:g></string>
|
||||
<string name="label_close">Κλείσε</string>
|
||||
<string name="webview_label_title">Suwayomi WebView</string>
|
||||
<string name="webview_label_disconnected">Αποσυνδεδεμένο, κάνε ανανέωση</string>
|
||||
<string name="webview_label_reversescroll">Αντίστροφη κύλιση</string>
|
||||
<string name="webview_label_bindingshint">Σημείωση: Όταν η εστίαση είναι στο τμήμα WebView, καμία συντόμευση πληκτρολογίου, συμπεριλαμβανομένης της ανανέωσης, δεν θα αντιμετωπίζεται από το πρόγραμμα περιήγησης.</string>
|
||||
<string name="webview_label_init">Αρχικοποίηση... Παρακαλώ περίμενε</string>
|
||||
<string name="webview_label_getstarted">Εισήγαγε μια διεύθυνση URL για να ξεκινήσεις</string>
|
||||
<string name="webview_label_loading">Φόρτωση σελίδας...</string>
|
||||
<string name="webview_label_copy">Αντιγραφή στο πρόχειρο</string>
|
||||
<string name="webview_label_copy_description">Η αυτόματη αντιγραφή στο πρόχειρο απέτυχε, χρησιμοποίησε το παρακάτω πεδίο για να αντιγράψεις χειροκίνητα την τιμή.</string>
|
||||
<string name="webview_label_login_required">Η διαμόρφωσή σου απαιτεί σύνδεση. Εισήγαγε όνομα χρήστη και κωδικό.</string>
|
||||
<string name="webview_placeholder_url">Εισήγαγε URL...</string>
|
||||
<string name="login_label_title">Suwayomi Σύνδεση</string>
|
||||
<string name="login_label_username">Όνομα χρήστη</string>
|
||||
<string name="login_label_password">Κωδικός</string>
|
||||
<string name="login_label_login">Σύνδεση</string>
|
||||
<string name="login_placeholder_username">Πληκτρολόγησε όνομα χρήστη...</string>
|
||||
<string name="login_placeholder_password">Μυστικό...</string>
|
||||
<string name="opds_error_chapters_not_found">Δεν βρέθηκαν κεφάλαια ή η πηγή είναι μη διαθέσιμη στη σελίδα %1$d.</string>
|
||||
<string name="opds_chapter_title_fallback">Κεφάλαιο %1$s</string>
|
||||
</resources>
|
||||
@@ -122,4 +122,6 @@
|
||||
<string name="webview_label_login_required">Su configuración requiere que inicie sesión. Introduzca su nombre de usuario y contraseña.</string>
|
||||
<string name="opds_linktitle_first_page">Primera página</string>
|
||||
<string name="opds_linktitle_last_page">Última página</string>
|
||||
<string name="opds_error_chapters_not_found">No se encontraron capítulos o la fuente no está disponible en la página %1$d.</string>
|
||||
<string name="opds_chapter_title_fallback">Capítulo %1$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -122,4 +122,7 @@
|
||||
<string name="login_label_login">Se connecter</string>
|
||||
<string name="login_placeholder_username">Tapez le nom d\'utilisateur…</string>
|
||||
<string name="login_placeholder_password">Secret…</string>
|
||||
<string name="opds_error_chapters_not_found">Aucun chapitre trouvé ou la source est inaccessible à la page %1$d.</string>
|
||||
<string name="opds_chapter_title_fallback">Chapitre %1$s</string>
|
||||
<string name="opds_chapter_title_oneshot">One shot</string>
|
||||
</resources>
|
||||
|
||||
@@ -122,4 +122,6 @@
|
||||
<string name="login_label_login">Accedi</string>
|
||||
<string name="login_placeholder_username">Digita il nome utente...</string>
|
||||
<string name="login_placeholder_password">Segreto...</string>
|
||||
<string name="opds_error_chapters_not_found">Nessun capitolo trovato o la fonte non è raggiungibile alla pagina %1$d.</string>
|
||||
<string name="opds_chapter_title_fallback">Capitolo %1$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -60,4 +60,7 @@
|
||||
<string name="opds_feeds_library_sources_title">ソース</string>
|
||||
<string name="opds_feeds_library_sources_entry_content">ソース別にライブラリ内のマンガを閲覧</string>
|
||||
<string name="opds_feeds_search_results_title">検索結果</string>
|
||||
<string name="opds_error_chapters_not_found">ページ %1$d で章が見つからないか、ソースに接続できません。</string>
|
||||
<string name="opds_chapter_title_oneshot">読み切り</string>
|
||||
<string name="opds_chapter_title_fallback">第 %1$s 話</string>
|
||||
</resources>
|
||||
|
||||
@@ -76,4 +76,6 @@
|
||||
<string name="opds_facet_filter_all">Wszystkie</string>
|
||||
<string name="opds_facet_filter_downloaded">Pobrane</string>
|
||||
<string name="opds_facet_filter_ongoing">Trwające</string>
|
||||
<string name="opds_error_chapters_not_found">Nie znaleziono rozdziałów lub źródło jest nieosiągalne na stronie %1$d.</string>
|
||||
<string name="opds_chapter_title_fallback">Rozdział %1$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -122,4 +122,6 @@
|
||||
<string name="login_label_login">Entrar</string>
|
||||
<string name="login_placeholder_username">Digite o nome de usuário...</string>
|
||||
<string name="login_placeholder_password">Segredo...</string>
|
||||
<string name="opds_error_chapters_not_found">Nenhum capítulo encontrado ou a fonte está inacessível na página %1$d.</string>
|
||||
<string name="opds_chapter_title_fallback">Capítulo %1$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -122,4 +122,7 @@
|
||||
<string name="opds_search_description">Ищите тайтлы в каталоге.</string>
|
||||
<string name="opds_error_manga_not_found">Тайтл с ID %1$d не найден.</string>
|
||||
<string name="opds_chapter_details_base">Тайтл: %1$s | %2$s</string>
|
||||
<string name="opds_error_chapters_not_found">Главы не найдены или источник недоступен на странице %1$d.</string>
|
||||
<string name="opds_chapter_title_fallback">Глава %1$s</string>
|
||||
<string name="opds_chapter_title_oneshot">Ваншот</string>
|
||||
</resources>
|
||||
|
||||
@@ -53,4 +53,7 @@
|
||||
<string name="opds_chapter_status_unread">⭕</string>
|
||||
<string name="opds_chapter_details_base">%1$s | %2$s</string>
|
||||
<string name="opds_feeds_genre_specific_title">இசைவகை: %1$s</string>
|
||||
<string name="opds_error_chapters_not_found">பக்கம் %1$d இல் அத்தியாயங்கள் எதுவும் காணப்படவில்லை அல்லது மூலத்தை அணுக முடியவில்லை.</string>
|
||||
<string name="opds_chapter_title_oneshot">ஒன்-ஷாட்</string>
|
||||
<string name="opds_chapter_title_fallback">அத்தியாயம் %1$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -122,4 +122,6 @@
|
||||
<string name="webview_label_login_required">Cấu hình của bạn yêu cầu bạn phải đăng nhập. Vui lòng nhập tên người dùng và mật khẩu.</string>
|
||||
<string name="opds_linktitle_first_page">Trang đầu</string>
|
||||
<string name="opds_linktitle_last_page">Trang cuối</string>
|
||||
<string name="opds_error_chapters_not_found">Không tìm thấy chương nào hoặc nguồn không thể truy cập tại trang %1$d.</string>
|
||||
<string name="opds_chapter_title_fallback">Chương %1$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -122,4 +122,7 @@
|
||||
<string name="login_placeholder_username">输入用户名…</string>
|
||||
<string name="login_placeholder_password">密匙…</string>
|
||||
<string name="label_error">错误</string>
|
||||
<string name="opds_error_chapters_not_found">第 %1$d 页未找到任何章节,或图源无法访问。</string>
|
||||
<string name="opds_chapter_title_fallback">第 %1$s 章</string>
|
||||
<string name="opds_chapter_title_oneshot">单篇</string>
|
||||
</resources>
|
||||
|
||||
@@ -284,12 +284,12 @@ class ServerConfig(
|
||||
defaultValue = emptyList(),
|
||||
deprecated =
|
||||
SettingsRegistry.SettingDeprecated(
|
||||
replaceWith = "extensionStores",
|
||||
message = "Replaced with extensionStores",
|
||||
message = "Replaced with addExtensionStore and removeExtensionStore mutations",
|
||||
migrateConfigValue = {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(it.unwrapped() as? List<String>)
|
||||
?.map {
|
||||
if (it.contains("github")) {
|
||||
if (it.contains("github.com")) {
|
||||
it.replace(repoMatchRegex) {
|
||||
"https://raw.githubusercontent.com/${it.groupValues[2]}/${it.groupValues[3]}/" +
|
||||
(it.groupValues.getOrNull(4)?.ifBlank { null } ?: "repo") +
|
||||
@@ -303,7 +303,7 @@ class ServerConfig(
|
||||
},
|
||||
),
|
||||
readMigrated = { extensionStores.value },
|
||||
setMigrated = { extensionStores.value = it },
|
||||
setMigrated = { extensionStores.value = it.distinct() },
|
||||
typeInfo =
|
||||
SettingsRegistry.PartialTypeInfo(
|
||||
specificType = "List<String>",
|
||||
@@ -1117,8 +1117,9 @@ class ServerConfig(
|
||||
val extensionStores: MutableStateFlow<List<String>> by ListSetting<String>(
|
||||
protoNumber = 97,
|
||||
group = SettingGroup.EXTENSION,
|
||||
privacySafe = false,
|
||||
privacySafe = true,
|
||||
defaultValue = emptyList(),
|
||||
requiresRestart = true,
|
||||
itemValidator = { url ->
|
||||
if (url.isNotEmpty()) {
|
||||
null
|
||||
@@ -1127,11 +1128,7 @@ class ServerConfig(
|
||||
}
|
||||
},
|
||||
itemToValidValue = { url ->
|
||||
if (url.isNotEmpty()) {
|
||||
url
|
||||
} else {
|
||||
null
|
||||
}
|
||||
url.ifEmpty { null }
|
||||
},
|
||||
typeInfo =
|
||||
SettingsRegistry.PartialTypeInfo(
|
||||
|
||||
@@ -9,7 +9,6 @@ package eu.kanade.tachiyomi.network
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
|
||||
import eu.kanade.tachiyomi.network.interceptor.IgnoreGzipInterceptor
|
||||
import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
|
||||
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
@@ -22,9 +21,8 @@ import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.brotli.BrotliInterceptor
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
||||
import java.net.CookieHandler
|
||||
import java.net.CookieManager
|
||||
import java.net.CookiePolicy
|
||||
@@ -64,7 +62,7 @@ class NetworkHelper(
|
||||
userAgent
|
||||
.drop(1)
|
||||
.onEach {
|
||||
GetCatalogueSource.unregisterAllCatalogueSources() // need to reset the headers
|
||||
GetSource.unregisterAllSources() // need to reset the headers
|
||||
}.launchIn(GlobalScope)
|
||||
}
|
||||
|
||||
@@ -84,8 +82,6 @@ class NetworkHelper(
|
||||
),
|
||||
).addInterceptor(UncaughtExceptionInterceptor())
|
||||
.addInterceptor(UserAgentInterceptor(::defaultUserAgentProvider))
|
||||
.addNetworkInterceptor(IgnoreGzipInterceptor())
|
||||
.addNetworkInterceptor(BrotliInterceptor)
|
||||
|
||||
// if (preferences.verboseLogging().get()) {
|
||||
val httpLoggingInterceptor =
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package eu.kanade.tachiyomi.network.interceptor
|
||||
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
/**
|
||||
* To use [okhttp3.brotli.BrotliInterceptor] as a network interceptor,
|
||||
* add [IgnoreGzipInterceptor] right before it.
|
||||
*
|
||||
* This nullifies the transparent gzip of [okhttp3.internal.http.BridgeInterceptor]
|
||||
* so gzip and Brotli are explicitly handled by the [okhttp3.brotli.BrotliInterceptor].
|
||||
*/
|
||||
class IgnoreGzipInterceptor : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
var request = chain.request()
|
||||
if (request.header("Accept-Encoding") == "gzip") {
|
||||
request = request.newBuilder().removeHeader("Accept-Encoding").build()
|
||||
}
|
||||
return chain.proceed(request)
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ import org.jetbrains.exposed.v1.jdbc.insert
|
||||
import org.jetbrains.exposed.v1.jdbc.insertAndGetId
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.registerCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.registerSource
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||
@@ -483,7 +483,7 @@ class LocalSource(
|
||||
it[versionCode] = 0
|
||||
it[lang] = LANG
|
||||
it[extensionLib] = "1.2"
|
||||
it[contentRating] = 0
|
||||
it[contentWarning] = 0
|
||||
it[isInstalled] = true
|
||||
}
|
||||
|
||||
@@ -497,7 +497,7 @@ class LocalSource(
|
||||
}
|
||||
|
||||
val fs = LocalSourceFileSystem(applicationDirs)
|
||||
registerCatalogueSource(ID to LocalSource(fs, LocalCoverManager(fs)))
|
||||
registerSource(ID to LocalSource(fs, LocalCoverManager(fs)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import suwayomi.tachidesk.manga.impl.util.lang.EMPTY
|
||||
|
||||
class SChapterImpl : SChapter {
|
||||
override lateinit var url: String
|
||||
@@ -15,5 +16,5 @@ class SChapterImpl : SChapter {
|
||||
|
||||
override var date_upload: Long = 0
|
||||
|
||||
override var memo: JsonObject = JsonObject(emptyMap())
|
||||
override var memo: JsonObject = JsonObject.EMPTY
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import suwayomi.tachidesk.manga.impl.util.lang.EMPTY
|
||||
|
||||
class SMangaImpl : SManga {
|
||||
override lateinit var url: String
|
||||
@@ -25,5 +26,5 @@ class SMangaImpl : SManga {
|
||||
|
||||
override var initialized: Boolean = false
|
||||
|
||||
override var memo: JsonObject = JsonObject(emptyMap())
|
||||
override var memo: JsonObject = JsonObject.EMPTY
|
||||
}
|
||||
|
||||
@@ -4,11 +4,15 @@ import android.app.Application
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.PUT
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.javalin.http.HttpStatus
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
import okhttp3.Headers
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
@@ -36,31 +40,66 @@ object SyncYomiSyncService {
|
||||
message: String?,
|
||||
) : Exception(message)
|
||||
|
||||
@Serializable
|
||||
private data class SyncEvent(
|
||||
val event: SyncEventStatus,
|
||||
val device_Name: String? = null,
|
||||
val message: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private enum class SyncEventStatus {
|
||||
SYNC_STARTED,
|
||||
SYNC_SUCCESS,
|
||||
SYNC_FAILED,
|
||||
SYNC_ERROR,
|
||||
SYNC_CANCELLED,
|
||||
}
|
||||
|
||||
suspend fun doSync(
|
||||
syncData: SyncData,
|
||||
startDate: Instant,
|
||||
setSyncState: (SyncManager.SyncState) -> Unit,
|
||||
): Backup? {
|
||||
reportSyncEvent(SyncEventStatus.SYNC_STARTED)
|
||||
setSyncState(SyncManager.SyncState.Downloading(startDate))
|
||||
val (remoteData, etag) = pullSyncData()
|
||||
|
||||
val finalSyncData =
|
||||
if (remoteData != null) {
|
||||
require(etag.isNotEmpty()) { "ETag should never be empty if remote data is not null" }
|
||||
logger.debug { "Try update remote data with ETag($etag)" }
|
||||
setSyncState(SyncManager.SyncState.Merging(startDate))
|
||||
mergeSyncData(syncData, remoteData)
|
||||
} else {
|
||||
// init or overwrite remote data
|
||||
logger.debug { "Try overwrite remote data with ETag($etag)" }
|
||||
syncData
|
||||
return try {
|
||||
val (remoteData, etag) = pullSyncData()
|
||||
|
||||
val finalSyncData =
|
||||
if (remoteData != null) {
|
||||
require(etag.isNotEmpty()) { "ETag should never be empty if remote data is not null" }
|
||||
logger.debug { "Try update remote data with ETag($etag)" }
|
||||
setSyncState(SyncManager.SyncState.Merging(startDate))
|
||||
mergeSyncData(syncData, remoteData)
|
||||
} else {
|
||||
// init or overwrite remote data
|
||||
logger.debug { "Try overwrite remote data with ETag($etag)" }
|
||||
syncData
|
||||
}
|
||||
|
||||
if (finalSyncData.backup != null) {
|
||||
setSyncState(SyncManager.SyncState.Uploading(startDate))
|
||||
}
|
||||
|
||||
if (finalSyncData.backup != null) {
|
||||
setSyncState(SyncManager.SyncState.Uploading(startDate))
|
||||
val success = pushSyncData(finalSyncData, etag)
|
||||
if (success) {
|
||||
reportSyncEvent(SyncEventStatus.SYNC_SUCCESS)
|
||||
} else {
|
||||
reportSyncEvent(SyncEventStatus.SYNC_FAILED, "Failed to push sync data")
|
||||
}
|
||||
|
||||
finalSyncData.backup
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) {
|
||||
reportSyncEvent(SyncEventStatus.SYNC_CANCELLED, e.message)
|
||||
throw e
|
||||
}
|
||||
logger.error { "Error syncing: ${e.message}" }
|
||||
reportSyncEvent(SyncEventStatus.SYNC_ERROR, e.message)
|
||||
throw e
|
||||
}
|
||||
pushSyncData(finalSyncData, etag)
|
||||
return finalSyncData.backup
|
||||
}
|
||||
|
||||
private suspend fun pullSyncData(): Pair<SyncData?, String> {
|
||||
@@ -122,8 +161,8 @@ object SyncYomiSyncService {
|
||||
private suspend fun pushSyncData(
|
||||
syncData: SyncData,
|
||||
eTag: String,
|
||||
) {
|
||||
val backup = syncData.backup ?: return
|
||||
): Boolean {
|
||||
val backup = syncData.backup ?: return true
|
||||
|
||||
val host = serverConfig.syncYomiHost.value
|
||||
val apiKey = serverConfig.syncYomiApiKey.value
|
||||
@@ -160,7 +199,7 @@ object SyncYomiSyncService {
|
||||
|
||||
val response = client.newCall(uploadRequest).await()
|
||||
|
||||
if (response.isSuccessful) {
|
||||
return if (response.isSuccessful) {
|
||||
val newETag =
|
||||
response.headers["ETag"]
|
||||
?.takeIf { it.isNotEmpty() } ?: throw SyncYomiException("Missing ETag")
|
||||
@@ -169,12 +208,53 @@ object SyncYomiSyncService {
|
||||
.putString("last_sync_etag", newETag)
|
||||
.apply()
|
||||
logger.debug { "SyncYomi sync completed" }
|
||||
true
|
||||
} else if (response.code == HttpStatus.PRECONDITION_FAILED.code) {
|
||||
// other clients updated remote data, will try next time
|
||||
logger.debug { "SyncYomi sync failed with 412" }
|
||||
false
|
||||
} else {
|
||||
val responseBody = response.body.string()
|
||||
logger.error { "SyncError: $responseBody" }
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun reportSyncEvent(
|
||||
event: SyncEventStatus,
|
||||
message: String? = null,
|
||||
) {
|
||||
try {
|
||||
val host = serverConfig.syncYomiHost.value
|
||||
val apiKey = serverConfig.syncYomiApiKey.value
|
||||
val url = "$host/api/sync/event"
|
||||
|
||||
val headers = Headers.Builder().add("X-API-Token", apiKey).build()
|
||||
|
||||
// Use a fixed server name.
|
||||
val bodyObj =
|
||||
SyncEvent(
|
||||
event = event,
|
||||
device_Name = "Suwayomi Server",
|
||||
message = message,
|
||||
)
|
||||
|
||||
val jsonBody = Json.encodeToString(SyncEvent.serializer(), bodyObj)
|
||||
val requestBody = jsonBody.toRequestBody("application/json; charset=utf-8".toMediaType())
|
||||
|
||||
val request =
|
||||
POST(
|
||||
url = url,
|
||||
headers = headers,
|
||||
body = requestBody,
|
||||
)
|
||||
|
||||
network.client
|
||||
.newCall(request)
|
||||
.await()
|
||||
.close()
|
||||
} catch (e: Exception) {
|
||||
logger.error { "Failed to report sync event: ${e.message}" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,8 +305,7 @@ object SyncYomiSyncService {
|
||||
|
||||
logger.debug { "Starting merge. Local list size: ${localMangaListSafe.size}, Remote list size: ${remoteMangaListSafe.size}" }
|
||||
|
||||
fun mangaCompositeKey(manga: BackupManga): String =
|
||||
"${manga.source}|${manga.url}|${manga.title.lowercase().trim()}|${manga.author?.lowercase()?.trim()}"
|
||||
fun mangaCompositeKey(manga: BackupManga): String = "${manga.source}|${manga.url}"
|
||||
|
||||
// Create maps using composite keys
|
||||
val localMangaMap = localMangaListSafe.associateBy { mangaCompositeKey(it) }
|
||||
@@ -335,7 +414,7 @@ object SyncYomiSyncService {
|
||||
return remoteChapters // If not syncing chapters, keep remote untouched
|
||||
}
|
||||
|
||||
fun chapterCompositeKey(chapter: BackupChapter): String = "${chapter.url}|${chapter.name}|${chapter.chapterNumber}"
|
||||
fun chapterCompositeKey(chapter: BackupChapter): String = chapter.url
|
||||
|
||||
val localChapterMap = localChapters.associateBy { chapterCompositeKey(it) }
|
||||
val remoteChapterMap = remoteChapters.associateBy { chapterCompositeKey(it) }
|
||||
|
||||
@@ -16,11 +16,11 @@ import suwayomi.tachidesk.manga.model.table.ExtensionStoreTable
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||
|
||||
class ExtensionStoreForExtension : KotlinDataLoader<String, ExtensionStoreType> {
|
||||
override val dataLoaderName = "ExtensionStoreForExtension"
|
||||
class ExtensionStoreDataLoader : KotlinDataLoader<String, ExtensionStoreType> {
|
||||
override val dataLoaderName = "ExtensionStoreDataLoader"
|
||||
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, ExtensionStoreType> =
|
||||
DataLoaderFactory.newDataLoader<String, ExtensionStoreType> { ids ->
|
||||
DataLoaderFactory.newDataLoader { ids ->
|
||||
future {
|
||||
transaction {
|
||||
addLogger(Slf4jSqlDebugLogger)
|
||||
@@ -30,14 +30,14 @@ class ExtensionStoreForExtension : KotlinDataLoader<String, ExtensionStoreType>
|
||||
.where { ExtensionStoreTable.indexUrl inList ids }
|
||||
.map { ExtensionStoreType(it) }
|
||||
.associateBy { it.indexUrl }
|
||||
ids.map { (extensionStoreByIndexUrl[it]) }
|
||||
ids.map { extensionStoreByIndexUrl[it] }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ExtensionForExtensionStore : KotlinDataLoader<String, ExtensionNodeList> {
|
||||
override val dataLoaderName = "ExtensionForExtensionStore"
|
||||
class ExtensionsForExtensionStore : KotlinDataLoader<String, ExtensionNodeList> {
|
||||
override val dataLoaderName = "ExtensionsForExtensionStore"
|
||||
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, ExtensionNodeList> =
|
||||
DataLoaderFactory.newDataLoader<String, ExtensionNodeList> { ids ->
|
||||
|
||||
@@ -26,6 +26,7 @@ import suwayomi.tachidesk.graphql.types.ChapterType
|
||||
import suwayomi.tachidesk.graphql.types.MetaInput
|
||||
import suwayomi.tachidesk.graphql.types.SyncConflictInfoType
|
||||
import suwayomi.tachidesk.manga.impl.Chapter
|
||||
import suwayomi.tachidesk.manga.impl.Manga
|
||||
import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReadyById
|
||||
import suwayomi.tachidesk.manga.impl.sync.KoreaderSyncService
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterMetaTable
|
||||
@@ -173,7 +174,7 @@ class ChapterMutation {
|
||||
val (clientMutationId, mangaId) = input
|
||||
|
||||
return future {
|
||||
Chapter.fetchChapterList(mangaId)
|
||||
Manga.updateMangaAndChapters(mangaId, updateManga = false)
|
||||
|
||||
val chapters =
|
||||
transaction {
|
||||
|
||||
@@ -10,9 +10,11 @@ import org.jetbrains.exposed.v1.core.neq
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.types.ExtensionStoreType
|
||||
import suwayomi.tachidesk.graphql.types.ExtensionType
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension
|
||||
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionStoreTable
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||
import java.util.concurrent.CompletableFuture
|
||||
@@ -129,6 +131,7 @@ class ExtensionMutation {
|
||||
data class FetchExtensionsPayload(
|
||||
val clientMutationId: String?,
|
||||
val extensions: List<ExtensionType>,
|
||||
val extensionStores: List<ExtensionStoreType>,
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
@@ -146,9 +149,17 @@ class ExtensionMutation {
|
||||
.map { ExtensionType(it) }
|
||||
}
|
||||
|
||||
val extensionStores =
|
||||
transaction {
|
||||
ExtensionStoreTable
|
||||
.selectAll()
|
||||
.map { ExtensionStoreType(it) }
|
||||
}
|
||||
|
||||
FetchExtensionsPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
extensions = extensions,
|
||||
extensionStores = extensionStores,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import suwayomi.tachidesk.graphql.types.ExtensionStoreType
|
||||
import suwayomi.tachidesk.manga.impl.extension.ExtensionStoreService
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionStoreTable
|
||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class ExtensionStoreMutation {
|
||||
@@ -37,7 +36,7 @@ class ExtensionStoreMutation {
|
||||
val store = ExtensionStoreService.fetch(indexUrl)
|
||||
|
||||
ExtensionStoreService.upsert(store)
|
||||
serverConfig.extensionStores.value = (serverConfig.extensionStores.value + indexUrl).distinct()
|
||||
ExtensionStoreService.syncDbToPrefs()
|
||||
val row =
|
||||
transaction {
|
||||
ExtensionStoreTable
|
||||
@@ -82,7 +81,7 @@ class ExtensionStoreMutation {
|
||||
}
|
||||
}
|
||||
|
||||
serverConfig.extensionStores.value = serverConfig.extensionStores.value.filterNot { it == indexUrl }
|
||||
ExtensionStoreService.syncDbToPrefs()
|
||||
|
||||
RemoveExtensionStorePayload(
|
||||
clientMutationId = clientMutationId,
|
||||
@@ -96,6 +95,7 @@ class ExtensionStoreMutation {
|
||||
contactDiscord = it.contactDiscord,
|
||||
indexUrl = it.indexUrl,
|
||||
isLegacy = it.isLegacy,
|
||||
extensionListUrl = it.extensionListUrl,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -19,11 +19,9 @@ import suwayomi.tachidesk.graphql.types.ChapterType
|
||||
import suwayomi.tachidesk.graphql.types.MangaMetaType
|
||||
import suwayomi.tachidesk.graphql.types.MangaType
|
||||
import suwayomi.tachidesk.graphql.types.MetaInput
|
||||
import suwayomi.tachidesk.manga.impl.Chapter
|
||||
import suwayomi.tachidesk.manga.impl.Library
|
||||
import suwayomi.tachidesk.manga.impl.Manga
|
||||
import suwayomi.tachidesk.manga.impl.update.IUpdater
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaMetaTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
@@ -156,7 +154,7 @@ class MangaMutation {
|
||||
val (clientMutationId, id) = input
|
||||
|
||||
return future {
|
||||
Manga.fetchManga(id)
|
||||
Manga.updateMangaAndChapters(id, updateChapters = false)
|
||||
|
||||
val manga =
|
||||
transaction {
|
||||
@@ -187,20 +185,11 @@ class MangaMutation {
|
||||
val (clientMutationId, id, fetchManga, fetchChapters) = input
|
||||
|
||||
return future {
|
||||
var mangaEntry =
|
||||
transaction { MangaTable.selectAll().where { MangaTable.id eq id }.first() }
|
||||
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
val sMangaUpdate =
|
||||
Manga.fetchMangaAndChapters(
|
||||
mangaEntry = mangaEntry,
|
||||
source = source,
|
||||
fetchDetails = fetchManga,
|
||||
fetchChapters = fetchChapters,
|
||||
)
|
||||
|
||||
Manga.updateMangaDatabase(mangaEntry, source, sMangaUpdate.manga)
|
||||
mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq id }.first() }
|
||||
Chapter.updateChapterListDatabase(mangaEntry, sMangaUpdate.chapters, source)
|
||||
Manga.updateMangaAndChapters(
|
||||
mangaId = id,
|
||||
updateManga = fetchManga,
|
||||
updateChapters = fetchChapters,
|
||||
)
|
||||
|
||||
val (manga, chapters) =
|
||||
transaction {
|
||||
|
||||
@@ -28,7 +28,7 @@ import suwayomi.tachidesk.graphql.types.preferenceOf
|
||||
import suwayomi.tachidesk.graphql.types.updateFilterList
|
||||
import suwayomi.tachidesk.manga.impl.MangaList.insertOrUpdate
|
||||
import suwayomi.tachidesk.manga.impl.Source
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
import suwayomi.tachidesk.manga.model.table.SourceMetaTable
|
||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||
@@ -256,7 +256,7 @@ class SourceMutation {
|
||||
val (clientMutationId, sourceId, type, page, query, filters) = input
|
||||
|
||||
return future {
|
||||
val source = GetCatalogueSource.getCatalogueSourceOrNull(sourceId)!!
|
||||
val source = GetSource.getSourceOrNull(sourceId)!!
|
||||
val mangasPage =
|
||||
when (type) {
|
||||
FetchSourceMangaType.SEARCH -> {
|
||||
|
||||
@@ -148,6 +148,36 @@ class TrackMutation {
|
||||
}
|
||||
}
|
||||
|
||||
data class BindTrackRecordInput(
|
||||
val clientMutationId: String? = null,
|
||||
val mangaId: Int,
|
||||
val trackRecordId: Int,
|
||||
)
|
||||
|
||||
data class BindTrackRecordPayload(
|
||||
val clientMutationId: String?,
|
||||
val trackRecord: TrackRecordType,
|
||||
)
|
||||
|
||||
@RequireAuth
|
||||
fun bindTrackRecord(input: BindTrackRecordInput): CompletableFuture<BindTrackRecordPayload?> {
|
||||
val (clientMutationId, mangaId, trackRecordId) = input
|
||||
|
||||
return future {
|
||||
val boundTrackRecordId = Track.bindTrackRecord(mangaId, trackRecordId)
|
||||
|
||||
val trackRecord =
|
||||
transaction {
|
||||
TrackRecordTable.selectAll().where { TrackRecordTable.id eq boundTrackRecordId }.first()
|
||||
}
|
||||
|
||||
BindTrackRecordPayload(
|
||||
clientMutationId,
|
||||
TrackRecordType(trackRecord),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class FetchTrackInput(
|
||||
val clientMutationId: String? = null,
|
||||
val recordId: Int,
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.ContentWarningFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.Filter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.HasGetOp
|
||||
import suwayomi.tachidesk.graphql.queries.filter.IntFilter
|
||||
@@ -28,6 +29,7 @@ import suwayomi.tachidesk.graphql.queries.filter.LongFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.OpAnd
|
||||
import suwayomi.tachidesk.graphql.queries.filter.StringFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEnum
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
|
||||
import suwayomi.tachidesk.graphql.queries.filter.applyOps
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||
@@ -39,9 +41,9 @@ import suwayomi.tachidesk.graphql.server.primitives.applyBeforeAfter
|
||||
import suwayomi.tachidesk.graphql.server.primitives.greaterNotUnique
|
||||
import suwayomi.tachidesk.graphql.server.primitives.lessNotUnique
|
||||
import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
|
||||
import suwayomi.tachidesk.graphql.types.ContentRating
|
||||
import suwayomi.tachidesk.graphql.types.ExtensionNodeList
|
||||
import suwayomi.tachidesk.graphql.types.ExtensionType
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
@@ -106,9 +108,9 @@ class ExtensionQuery {
|
||||
val versionCode: Int? = null,
|
||||
val versionCodeLong: Long? = null,
|
||||
val lang: String? = null,
|
||||
@GraphQLDeprecated("", ReplaceWith("contentRating"))
|
||||
@GraphQLDeprecated("", ReplaceWith("contentWarning"))
|
||||
val isNsfw: Boolean? = null,
|
||||
val contentRating: ContentRating? = null,
|
||||
val contentWarning: ContentWarning? = null,
|
||||
val isInstalled: Boolean? = null,
|
||||
val hasUpdate: Boolean? = null,
|
||||
val isObsolete: Boolean? = null,
|
||||
@@ -126,8 +128,11 @@ class ExtensionQuery {
|
||||
opAnd.eq(versionCode?.toLong(), ExtensionTable.versionCode)
|
||||
opAnd.eq(versionCodeLong, ExtensionTable.versionCode)
|
||||
opAnd.eq(lang, ExtensionTable.lang)
|
||||
opAnd.eq(isNsfw?.let { if (it) 3 else 0 }, ExtensionTable.contentRating)
|
||||
opAnd.eq(contentRating?.ordinal, ExtensionTable.contentRating)
|
||||
opAnd.eq(
|
||||
isNsfw?.let { if (it) ContentWarning.MIXED.ordinal else ContentWarning.SAFE.ordinal },
|
||||
ExtensionTable.contentWarning,
|
||||
)
|
||||
opAnd.eq(contentWarning?.ordinal, ExtensionTable.contentWarning)
|
||||
opAnd.eq(isInstalled, ExtensionTable.isInstalled)
|
||||
opAnd.eq(hasUpdate, ExtensionTable.hasUpdate)
|
||||
opAnd.eq(isObsolete, ExtensionTable.isObsolete)
|
||||
@@ -151,9 +156,9 @@ class ExtensionQuery {
|
||||
val versionCode: IntFilter? = null,
|
||||
val versionCodeLong: LongFilter? = null,
|
||||
val lang: StringFilter? = null,
|
||||
@GraphQLDeprecated("", ReplaceWith("storeIndexUrl"))
|
||||
@GraphQLDeprecated("", ReplaceWith("contentWarning"))
|
||||
val isNsfw: BooleanFilter? = null,
|
||||
// val contentRating: EnumFilter<ContentRating>? = null,
|
||||
val contentWarning: ContentWarningFilter? = null,
|
||||
val isInstalled: BooleanFilter? = null,
|
||||
val hasUpdate: BooleanFilter? = null,
|
||||
val isObsolete: BooleanFilter? = null,
|
||||
@@ -174,7 +179,7 @@ class ExtensionQuery {
|
||||
andFilterWithCompareString(ExtensionTable.versionName, versionName),
|
||||
andFilterWithCompare(ExtensionTable.versionCode, versionCodeLong),
|
||||
andFilterWithCompareString(ExtensionTable.lang, lang),
|
||||
// andFilterWithCompareEnum(ExtensionTable.contentRating, contentRating),
|
||||
andFilterWithCompareEnum(ExtensionTable.contentWarning, contentWarning),
|
||||
andFilterWithCompare(ExtensionTable.isInstalled, isInstalled),
|
||||
andFilterWithCompare(ExtensionTable.hasUpdate, hasUpdate),
|
||||
andFilterWithCompare(ExtensionTable.isObsolete, isObsolete),
|
||||
|
||||
@@ -7,33 +7,184 @@ package suwayomi.tachidesk.graphql.queries
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import org.jetbrains.exposed.v1.core.Column
|
||||
import org.jetbrains.exposed.v1.core.Op
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.queries.filter.Filter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.HasGetOp
|
||||
import suwayomi.tachidesk.graphql.queries.filter.OpAnd
|
||||
import suwayomi.tachidesk.graphql.queries.filter.StringFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
|
||||
import suwayomi.tachidesk.graphql.queries.filter.applyOps
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Order
|
||||
import suwayomi.tachidesk.graphql.server.primitives.OrderBy
|
||||
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
|
||||
import suwayomi.tachidesk.graphql.server.primitives.QueryResults
|
||||
import suwayomi.tachidesk.graphql.server.primitives.applyBeforeAfter
|
||||
import suwayomi.tachidesk.graphql.server.primitives.greaterNotUnique
|
||||
import suwayomi.tachidesk.graphql.server.primitives.lessNotUnique
|
||||
import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
|
||||
import suwayomi.tachidesk.graphql.types.ExtensionStoreNodeList
|
||||
import suwayomi.tachidesk.graphql.types.ExtensionStoreType
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionStoreTable
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class ExtensionStoreQuery {
|
||||
@RequireAuth
|
||||
fun extensionStore(indexUrl: String): CompletableFuture<ExtensionStoreType?> =
|
||||
CompletableFuture.supplyAsync {
|
||||
transaction {
|
||||
ExtensionStoreTable
|
||||
.selectAll()
|
||||
.where { ExtensionStoreTable.indexUrl eq indexUrl }
|
||||
.firstOrNull()
|
||||
?.let { ExtensionStoreType(it) }
|
||||
fun extensionStore(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
indexUrl: String,
|
||||
): CompletableFuture<ExtensionStoreType> = dataFetchingEnvironment.getValueFromDataLoader("ExtensionStoreDataLoader", indexUrl)
|
||||
|
||||
enum class ExtensionStoreOrderBy(
|
||||
override val column: Column<*>,
|
||||
) : OrderBy<ExtensionStoreType> {
|
||||
NAME(ExtensionStoreTable.name),
|
||||
INDEX_URL(ExtensionStoreTable.indexUrl),
|
||||
;
|
||||
|
||||
override fun greater(cursor: Cursor): Op<Boolean> =
|
||||
when (this) {
|
||||
NAME -> greaterNotUnique(ExtensionStoreTable.name, ExtensionStoreTable.id, cursor, String::toString)
|
||||
INDEX_URL -> greaterNotUnique(ExtensionStoreTable.indexUrl, ExtensionStoreTable.id, cursor, String::toString)
|
||||
}
|
||||
|
||||
override fun less(cursor: Cursor): Op<Boolean> =
|
||||
when (this) {
|
||||
NAME -> lessNotUnique(ExtensionStoreTable.name, ExtensionStoreTable.id, cursor, String::toString)
|
||||
INDEX_URL -> lessNotUnique(ExtensionStoreTable.indexUrl, ExtensionStoreTable.id, cursor, String::toString)
|
||||
}
|
||||
|
||||
override fun asCursor(type: ExtensionStoreType): Cursor {
|
||||
val value =
|
||||
when (this) {
|
||||
INDEX_URL -> type.indexUrl
|
||||
NAME -> type.indexUrl + "-" + type.name
|
||||
}
|
||||
return Cursor(value)
|
||||
}
|
||||
}
|
||||
|
||||
data class ExtensionStoreOrder(
|
||||
override val by: ExtensionStoreOrderBy,
|
||||
override val byType: SortOrder? = null,
|
||||
) : Order<ExtensionStoreOrderBy>
|
||||
|
||||
data class ExtensionStoreCondition(
|
||||
val id: Int? = null,
|
||||
val indexUrl: String? = null,
|
||||
val name: String? = null,
|
||||
) : HasGetOp {
|
||||
override fun getOp(): Op<Boolean>? {
|
||||
val opAnd = OpAnd()
|
||||
opAnd.eq(id, ExtensionStoreTable.id)
|
||||
opAnd.eq(indexUrl, ExtensionStoreTable.indexUrl)
|
||||
opAnd.eq(name, ExtensionStoreTable.name)
|
||||
|
||||
return opAnd.op
|
||||
}
|
||||
}
|
||||
|
||||
data class ExtensionStoreFilter(
|
||||
val indexUrl: StringFilter? = null,
|
||||
val name: StringFilter? = null,
|
||||
override val and: List<ExtensionStoreFilter>? = null,
|
||||
override val or: List<ExtensionStoreFilter>? = null,
|
||||
override val not: ExtensionStoreFilter? = null,
|
||||
) : Filter<ExtensionStoreFilter> {
|
||||
override fun getOpList(): List<Op<Boolean>> =
|
||||
listOfNotNull(
|
||||
andFilterWithCompareString(ExtensionStoreTable.indexUrl, indexUrl),
|
||||
andFilterWithCompareString(ExtensionStoreTable.name, name),
|
||||
)
|
||||
}
|
||||
|
||||
@RequireAuth
|
||||
fun extensionStores(): List<ExtensionStoreType> =
|
||||
transaction {
|
||||
ExtensionStoreTable
|
||||
.selectAll()
|
||||
.toList()
|
||||
.map { ExtensionStoreType(it) }
|
||||
}
|
||||
fun extensionStores(
|
||||
condition: ExtensionStoreCondition? = null,
|
||||
filter: ExtensionStoreFilter? = null,
|
||||
order: List<ExtensionStoreOrder>? = null,
|
||||
before: Cursor? = null,
|
||||
after: Cursor? = null,
|
||||
first: Int? = null,
|
||||
last: Int? = null,
|
||||
offset: Int? = null,
|
||||
): ExtensionStoreNodeList {
|
||||
val queryResults =
|
||||
transaction {
|
||||
val res = ExtensionStoreTable.selectAll()
|
||||
|
||||
res.applyOps(condition, filter)
|
||||
|
||||
if (order != null || (last != null || before != null)) {
|
||||
val baseSort = listOf(ExtensionStoreOrder(ExtensionStoreOrderBy.INDEX_URL, SortOrder.ASC))
|
||||
val actualSort = (order.orEmpty() + baseSort)
|
||||
actualSort.forEach { (orderBy, orderByType) ->
|
||||
val orderByColumn = orderBy.column
|
||||
val orderType = orderByType.maybeSwap(last ?: before)
|
||||
|
||||
res.orderBy(orderByColumn to orderType)
|
||||
}
|
||||
}
|
||||
|
||||
val total = res.count()
|
||||
val firstResult = res.firstOrNull()?.get(ExtensionStoreTable.indexUrl)
|
||||
val lastResult = res.lastOrNull()?.get(ExtensionStoreTable.indexUrl)
|
||||
|
||||
res.applyBeforeAfter(
|
||||
before = before,
|
||||
after = after,
|
||||
orderBy = order?.firstOrNull()?.by ?: ExtensionStoreOrderBy.INDEX_URL,
|
||||
orderByType = order?.firstOrNull()?.byType,
|
||||
)
|
||||
|
||||
if (first != null) {
|
||||
res.limit(first).offset(offset?.toLong() ?: 0)
|
||||
} else if (last != null) {
|
||||
res.limit(last)
|
||||
}
|
||||
|
||||
QueryResults(total, firstResult, lastResult, res.toList())
|
||||
}
|
||||
|
||||
val getAsCursor: (ExtensionStoreType) -> Cursor = (order?.firstOrNull()?.by ?: ExtensionStoreOrderBy.INDEX_URL)::asCursor
|
||||
|
||||
val resultsAsType = queryResults.results.map { ExtensionStoreType(it) }
|
||||
|
||||
return ExtensionStoreNodeList(
|
||||
resultsAsType,
|
||||
if (resultsAsType.isEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
listOfNotNull(
|
||||
resultsAsType.firstOrNull()?.let {
|
||||
ExtensionStoreNodeList.ExtensionStoreEdge(
|
||||
getAsCursor(it),
|
||||
it,
|
||||
)
|
||||
},
|
||||
resultsAsType.lastOrNull()?.let {
|
||||
ExtensionStoreNodeList.ExtensionStoreEdge(
|
||||
getAsCursor(it),
|
||||
it,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
pageInfo =
|
||||
PageInfo(
|
||||
hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.indexUrl,
|
||||
hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.indexUrl,
|
||||
startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(it) },
|
||||
endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(it) },
|
||||
),
|
||||
totalCount = queryResults.total.toInt(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,12 +21,14 @@ import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.ContentWarningFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.Filter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.HasGetOp
|
||||
import suwayomi.tachidesk.graphql.queries.filter.LongFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.OpAnd
|
||||
import suwayomi.tachidesk.graphql.queries.filter.StringFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEnum
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
|
||||
import suwayomi.tachidesk.graphql.queries.filter.applyOps
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||
@@ -38,9 +40,9 @@ import suwayomi.tachidesk.graphql.server.primitives.applyBeforeAfter
|
||||
import suwayomi.tachidesk.graphql.server.primitives.greaterNotUnique
|
||||
import suwayomi.tachidesk.graphql.server.primitives.lessNotUnique
|
||||
import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
|
||||
import suwayomi.tachidesk.graphql.types.ContentRating
|
||||
import suwayomi.tachidesk.graphql.types.SourceNodeList
|
||||
import suwayomi.tachidesk.graphql.types.SourceType
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
@@ -93,17 +95,23 @@ class SourceQuery {
|
||||
val id: Long? = null,
|
||||
val name: String? = null,
|
||||
val lang: String? = null,
|
||||
@GraphQLDeprecated("replace with contentRating == 3", ReplaceWith("contentRating"))
|
||||
@GraphQLDeprecated("replace with contentWarning == ContentRating.MIXED", ReplaceWith("contentWarning"))
|
||||
val isNsfw: Boolean? = null,
|
||||
val contentRating: ContentRating? = null,
|
||||
val contentWarning: ContentWarning? = null,
|
||||
) : HasGetOp {
|
||||
override fun getOp(): Op<Boolean>? {
|
||||
val opAnd = OpAnd()
|
||||
opAnd.eq(id, SourceTable.id)
|
||||
opAnd.eq(name, SourceTable.name)
|
||||
opAnd.eq(lang, SourceTable.lang)
|
||||
opAnd.andWhere(isNsfw) { if (it) SourceTable.contentRating greaterEq 3 else SourceTable.contentRating less 3 }
|
||||
opAnd.andWhere(contentRating) { SourceTable.contentRating eq it.getValue() }
|
||||
opAnd.andWhere(isNsfw) {
|
||||
if (it) {
|
||||
SourceTable.contentWarning greaterEq ContentWarning.MIXED.ordinal
|
||||
} else {
|
||||
SourceTable.contentWarning less ContentWarning.MIXED.ordinal
|
||||
}
|
||||
}
|
||||
opAnd.andWhere(contentWarning) { SourceTable.contentWarning eq it.ordinal }
|
||||
|
||||
return opAnd.op
|
||||
}
|
||||
@@ -113,9 +121,9 @@ class SourceQuery {
|
||||
val id: LongFilter? = null,
|
||||
val name: StringFilter? = null,
|
||||
val lang: StringFilter? = null,
|
||||
@GraphQLDeprecated("replace with contentRating == 3", ReplaceWith("contentRating"))
|
||||
@GraphQLDeprecated("replace with contentWarning", ReplaceWith("contentWarning"))
|
||||
val isNsfw: BooleanFilter? = null,
|
||||
// val contentRating: EnumFilter<ContentRating>? = null,
|
||||
val contentWarning: ContentWarningFilter? = null,
|
||||
override val and: List<SourceFilter>? = null,
|
||||
override val or: List<SourceFilter>? = null,
|
||||
override val not: SourceFilter? = null,
|
||||
@@ -125,7 +133,7 @@ class SourceQuery {
|
||||
andFilterWithCompareEntity(SourceTable.id, id),
|
||||
andFilterWithCompareString(SourceTable.name, name),
|
||||
andFilterWithCompareString(SourceTable.lang, lang),
|
||||
// andFilterWithCompareEnum(SourceTable.contentRating, contentRating)
|
||||
andFilterWithCompareEnum(SourceTable.contentWarning, contentWarning),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.jetbrains.exposed.v1.core.upperCase
|
||||
import org.jetbrains.exposed.v1.core.wrap
|
||||
import org.jetbrains.exposed.v1.jdbc.Query
|
||||
import org.jetbrains.exposed.v1.jdbc.andWhere
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||
|
||||
class ILikeEscapeOp(
|
||||
expr1: Expression<*>,
|
||||
@@ -329,23 +330,23 @@ data class DoubleFilter(
|
||||
)
|
||||
}
|
||||
|
||||
data class EnumFilter<T : Enum<T>>(
|
||||
data class ContentWarningFilter(
|
||||
override val isNull: Boolean? = null,
|
||||
override val equalTo: T? = null,
|
||||
override val notEqualTo: T? = null,
|
||||
override val notEqualToAll: List<T>? = null,
|
||||
override val notEqualToAny: List<T>? = null,
|
||||
override val distinctFrom: T? = null,
|
||||
override val distinctFromAll: List<T>? = null,
|
||||
override val distinctFromAny: List<T>? = null,
|
||||
override val notDistinctFrom: T? = null,
|
||||
override val `in`: List<T>? = null,
|
||||
override val notIn: List<T>? = null,
|
||||
override val lessThan: T? = null,
|
||||
override val lessThanOrEqualTo: T? = null,
|
||||
override val greaterThan: T? = null,
|
||||
override val greaterThanOrEqualTo: T? = null,
|
||||
) : ComparableScalarFilter<T>
|
||||
override val equalTo: ContentWarning? = null,
|
||||
override val notEqualTo: ContentWarning? = null,
|
||||
override val notEqualToAll: List<ContentWarning>? = null,
|
||||
override val notEqualToAny: List<ContentWarning>? = null,
|
||||
override val distinctFrom: ContentWarning? = null,
|
||||
override val distinctFromAll: List<ContentWarning>? = null,
|
||||
override val distinctFromAny: List<ContentWarning>? = null,
|
||||
override val notDistinctFrom: ContentWarning? = null,
|
||||
override val `in`: List<ContentWarning>? = null,
|
||||
override val notIn: List<ContentWarning>? = null,
|
||||
override val lessThan: ContentWarning? = null,
|
||||
override val lessThanOrEqualTo: ContentWarning? = null,
|
||||
override val greaterThan: ContentWarning? = null,
|
||||
override val greaterThanOrEqualTo: ContentWarning? = null,
|
||||
) : ComparableScalarFilter<ContentWarning>
|
||||
|
||||
data class StringFilter(
|
||||
override val isNull: Boolean? = null,
|
||||
|
||||
@@ -20,9 +20,9 @@ import suwayomi.tachidesk.graphql.dataLoaders.DisplayScoreForTrackRecordDataLoad
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.DisplayScoreForTrackSearchDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.DownloadedChapterCountForMangaDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionForExtensionStore
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionForSourceDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionStoreForExtension
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionStoreDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionsForExtensionStore
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.FirstUnreadChapterForMangaDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.GlobalMetaDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.HasDuplicateChaptersForMangaDataLoader
|
||||
@@ -80,8 +80,8 @@ class TachideskDataLoaderRegistryFactory {
|
||||
SourceMetaDataLoader(),
|
||||
ExtensionDataLoader(),
|
||||
ExtensionForSourceDataLoader(),
|
||||
ExtensionForExtensionStore(),
|
||||
ExtensionStoreForExtension(),
|
||||
ExtensionsForExtensionStore(),
|
||||
ExtensionStoreDataLoader(),
|
||||
TrackerDataLoader(),
|
||||
TrackerStatusesDataLoader(),
|
||||
TrackerScoresDataLoader(),
|
||||
|
||||
@@ -10,7 +10,11 @@ package suwayomi.tachidesk.graphql.types
|
||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Edge
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Node
|
||||
import suwayomi.tachidesk.graphql.server.primitives.NodeList
|
||||
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionStoreTable
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
@@ -22,6 +26,7 @@ class ExtensionStoreType(
|
||||
val contactDiscord: String?,
|
||||
val indexUrl: String,
|
||||
val isLegacy: Boolean,
|
||||
val extensionListUrl: String?,
|
||||
) : Node {
|
||||
constructor(row: ResultRow) : this(
|
||||
name = row[ExtensionStoreTable.name],
|
||||
@@ -31,8 +36,51 @@ class ExtensionStoreType(
|
||||
contactDiscord = row[ExtensionStoreTable.contactDiscord],
|
||||
indexUrl = row[ExtensionStoreTable.indexUrl],
|
||||
isLegacy = row[ExtensionStoreTable.isLegacy],
|
||||
extensionListUrl = row[ExtensionStoreTable.extensionListUrl],
|
||||
)
|
||||
|
||||
fun extension(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<ExtensionNodeList> =
|
||||
dataFetchingEnvironment.getValueFromDataLoader<String, ExtensionNodeList>("ExtensionForExtensionStore", indexUrl)
|
||||
fun extensions(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<ExtensionNodeList> =
|
||||
dataFetchingEnvironment.getValueFromDataLoader<String, ExtensionNodeList>("ExtensionsForExtensionStore", indexUrl)
|
||||
}
|
||||
|
||||
data class ExtensionStoreNodeList(
|
||||
override val nodes: List<ExtensionStoreType>,
|
||||
override val edges: List<ExtensionStoreEdge>,
|
||||
override val pageInfo: PageInfo,
|
||||
override val totalCount: Int,
|
||||
) : NodeList() {
|
||||
data class ExtensionStoreEdge(
|
||||
override val cursor: Cursor,
|
||||
override val node: ExtensionStoreType,
|
||||
) : Edge()
|
||||
|
||||
companion object {
|
||||
fun List<ExtensionStoreType>.toNodeList(): ExtensionStoreNodeList =
|
||||
ExtensionStoreNodeList(
|
||||
nodes = this,
|
||||
edges = getEdges(),
|
||||
pageInfo =
|
||||
PageInfo(
|
||||
hasNextPage = false,
|
||||
hasPreviousPage = false,
|
||||
startCursor = Cursor(0.toString()),
|
||||
endCursor = Cursor(lastIndex.toString()),
|
||||
),
|
||||
totalCount = size,
|
||||
)
|
||||
|
||||
private fun List<ExtensionStoreType>.getEdges(): List<ExtensionStoreEdge> {
|
||||
if (isEmpty()) return emptyList()
|
||||
return listOf(
|
||||
ExtensionStoreEdge(
|
||||
cursor = Cursor("0"),
|
||||
node = first(),
|
||||
),
|
||||
ExtensionStoreEdge(
|
||||
cursor = Cursor(lastIndex.toString()),
|
||||
node = last(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import suwayomi.tachidesk.graphql.server.primitives.Node
|
||||
import suwayomi.tachidesk.graphql.server.primitives.NodeList
|
||||
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
@@ -40,9 +41,9 @@ class ExtensionType(
|
||||
val versionCode: Int,
|
||||
val versionCodeLong: Long,
|
||||
val lang: String,
|
||||
@GraphQLDeprecated("Removed in extension api v1.6", ReplaceWith("contentRating"))
|
||||
@GraphQLDeprecated("Removed in extension api v1.6", ReplaceWith("contentWarning"))
|
||||
val isNsfw: Boolean,
|
||||
val contentRating: ContentRating,
|
||||
val contentWarning: ContentWarning,
|
||||
val isInstalled: Boolean,
|
||||
val hasUpdate: Boolean,
|
||||
val isObsolete: Boolean,
|
||||
@@ -60,8 +61,8 @@ class ExtensionType(
|
||||
versionCode = row[ExtensionTable.versionCode].toInt(),
|
||||
versionCodeLong = row[ExtensionTable.versionCode],
|
||||
lang = row[ExtensionTable.lang],
|
||||
isNsfw = row[ExtensionTable.contentRating] == 3,
|
||||
contentRating = ContentRating.valueOf(row[ExtensionTable.contentRating]),
|
||||
isNsfw = row[ExtensionTable.contentWarning] >= ContentWarning.MIXED.ordinal,
|
||||
contentWarning = ContentWarning.valueOf(row[ExtensionTable.contentWarning]),
|
||||
isInstalled = row[ExtensionTable.isInstalled],
|
||||
hasUpdate = row[ExtensionTable.hasUpdate],
|
||||
isObsolete = row[ExtensionTable.isObsolete],
|
||||
@@ -71,7 +72,7 @@ class ExtensionType(
|
||||
dataFetchingEnvironment.getValueFromDataLoader<String, SourceNodeList>("SourcesForExtensionDataLoader", pkgName)
|
||||
|
||||
fun extensionStore(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<ExtensionStoreType> =
|
||||
dataFetchingEnvironment.getValueFromDataLoader<String, ExtensionStoreType>("ExtensionStoreForExtension", storeIndexUrl.orEmpty())
|
||||
dataFetchingEnvironment.getValueFromDataLoader<String, ExtensionStoreType>("ExtensionStoreDataLoader", storeIndexUrl.orEmpty())
|
||||
}
|
||||
|
||||
data class ExtensionNodeList(
|
||||
|
||||
@@ -8,10 +8,9 @@
|
||||
package suwayomi.tachidesk.graphql.types
|
||||
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLIgnore
|
||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
@@ -25,8 +24,9 @@ import suwayomi.tachidesk.graphql.server.primitives.NodeList
|
||||
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
|
||||
import suwayomi.tachidesk.manga.impl.Source.getSourcePreferencesRaw
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||
import java.util.concurrent.CompletableFuture
|
||||
@@ -42,31 +42,29 @@ class SourceType(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val lang: String,
|
||||
val message: String?,
|
||||
val contentRating: ContentRating,
|
||||
val contentWarning: ContentWarning,
|
||||
val iconUrl: String,
|
||||
val supportsLatest: Boolean,
|
||||
val isConfigurable: Boolean,
|
||||
@GraphQLDeprecated("", ReplaceWith("contentRating"))
|
||||
@GraphQLDeprecated("", ReplaceWith("contentWarning"))
|
||||
val isNsfw: Boolean,
|
||||
val displayName: String,
|
||||
val homeUrl: String?,
|
||||
@GraphQLDeprecated("", ReplaceWith("homeUrl"))
|
||||
val baseUrl: String?,
|
||||
) : Node {
|
||||
constructor(row: ResultRow, sourceExtension: ResultRow, catalogueSource: CatalogueSource) : this(
|
||||
constructor(row: ResultRow, sourceExtension: ResultRow, source: Source) : this(
|
||||
id = row[SourceTable.id].value,
|
||||
name = row[SourceTable.name],
|
||||
lang = row[SourceTable.lang],
|
||||
message = row[SourceTable.message],
|
||||
contentRating = ContentRating.valueOf(row[SourceTable.contentRating]),
|
||||
contentWarning = ContentWarning.valueOf(row[SourceTable.contentWarning]),
|
||||
iconUrl = Extension.proxyExtensionIconUrl(sourceExtension[ExtensionTable.pkgName]),
|
||||
supportsLatest = catalogueSource.supportsLatest,
|
||||
isConfigurable = catalogueSource is ConfigurableSource,
|
||||
isNsfw = row[SourceTable.contentRating] >= 3,
|
||||
displayName = catalogueSource.toString(),
|
||||
homeUrl = runCatching { (catalogueSource as? HttpSource)?.getHomeUrl() }.getOrNull(),
|
||||
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
|
||||
supportsLatest = source.supportsLatest,
|
||||
isConfigurable = source is ConfigurableSource,
|
||||
isNsfw = row[SourceTable.contentWarning] >= ContentWarning.MIXED.ordinal,
|
||||
displayName = source.toString(),
|
||||
homeUrl = runCatching { (source as? HttpSource)?.getHomeUrl() }.getOrNull(),
|
||||
baseUrl = runCatching { (source as? HttpSource)?.baseUrl }.getOrNull(),
|
||||
)
|
||||
|
||||
fun manga(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<MangaNodeList> =
|
||||
@@ -77,7 +75,7 @@ class SourceType(
|
||||
|
||||
fun preferences(): List<Preference> = getSourcePreferencesRaw(id).map { preferenceOf(it) }
|
||||
|
||||
fun filters(): List<Filter> = getCatalogueSourceOrStub(id).getFilterList().map { filterOf(it) }
|
||||
fun filters(): List<Filter> = getSourceOrStub(id).getFilterList().map { filterOf(it) }
|
||||
|
||||
fun meta(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<List<SourceMetaType>> =
|
||||
dataFetchingEnvironment.getValueFromDataLoader<Long, List<SourceMetaType>>("SourceMetaDataLoader", id)
|
||||
@@ -86,8 +84,8 @@ class SourceType(
|
||||
@Suppress("ktlint:standard:function-naming")
|
||||
fun SourceType(row: ResultRow): SourceType? {
|
||||
val catalogueSource =
|
||||
GetCatalogueSource
|
||||
.getCatalogueSourceOrNull(row[SourceTable.id].value)
|
||||
GetSource
|
||||
.getSourceOrNull(row[SourceTable.id].value)
|
||||
?: return null
|
||||
val sourceExtension =
|
||||
if (row.hasValue(ExtensionTable.id)) {
|
||||
@@ -298,7 +296,7 @@ data class FilterChange(
|
||||
)
|
||||
|
||||
fun updateFilterList(
|
||||
source: CatalogueSource,
|
||||
source: Source,
|
||||
changes: List<FilterChange>?,
|
||||
): FilterList {
|
||||
val filterList = source.getFilterList()
|
||||
@@ -507,33 +505,3 @@ fun preferenceOf(preference: SourcePreference): Preference =
|
||||
throw RuntimeException("sealed class cannot have more subtypes!")
|
||||
}
|
||||
}
|
||||
|
||||
enum class ContentRating {
|
||||
SAFE,
|
||||
SUGGESTIVE,
|
||||
EROTICA,
|
||||
PORNOGRAPHIC,
|
||||
;
|
||||
|
||||
@GraphQLIgnore
|
||||
fun getValue() =
|
||||
when (this) {
|
||||
SAFE -> 0
|
||||
SUGGESTIVE -> 1
|
||||
EROTICA -> 2
|
||||
PORNOGRAPHIC -> 3
|
||||
}
|
||||
|
||||
@GraphQLIgnore
|
||||
companion object {
|
||||
@GraphQLIgnore
|
||||
fun valueOf(contentRating: Int) =
|
||||
when (contentRating) {
|
||||
0 -> SAFE
|
||||
1 -> SUGGESTIVE
|
||||
2 -> EROTICA
|
||||
3 -> PORNOGRAPHIC
|
||||
else -> SAFE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ object UpdateController {
|
||||
)
|
||||
|
||||
/**
|
||||
* Class made for handling return type in the documentation for [recentChapters],
|
||||
* Class made for handling return type in the documentation for [UpdateController.recentChapters],
|
||||
* since OpenApi cannot handle runtime generics.
|
||||
*/
|
||||
private class PagedMangaChapterListDataClass : PaginatedList<MangaChapterDataClass>(emptyList(), false)
|
||||
|
||||
@@ -7,7 +7,7 @@ package suwayomi.tachidesk.manga.impl
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
@@ -38,7 +38,7 @@ import org.jetbrains.exposed.v1.jdbc.update
|
||||
import suwayomi.tachidesk.manga.impl.download.DownloadManager
|
||||
import suwayomi.tachidesk.manga.impl.download.DownloadManager.EnqueueInput
|
||||
import suwayomi.tachidesk.manga.impl.track.Track
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
|
||||
import suwayomi.tachidesk.manga.model.dataclass.MangaChapterDataClass
|
||||
import suwayomi.tachidesk.manga.model.dataclass.PaginatedList
|
||||
@@ -119,7 +119,7 @@ object Chapter {
|
||||
transaction {
|
||||
MangaTable.selectAll().where { MangaTable.id eq mangaId }.first()
|
||||
}
|
||||
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
val source = getSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
|
||||
val chapters =
|
||||
Manga
|
||||
@@ -139,7 +139,7 @@ object Chapter {
|
||||
fun updateChapterListDatabase(
|
||||
mangaEntry: ResultRow,
|
||||
chapters: List<SChapter>,
|
||||
source: CatalogueSource,
|
||||
source: Source,
|
||||
): List<SChapter> {
|
||||
val currentLatestChapterNumber = Manga.getLatestChapter(mangaEntry[MangaTable.id].value)?.chapterNumber ?: 0f
|
||||
val numberOfCurrentChapters = getCountOfMangaChapters(mangaEntry[MangaTable.id].value)
|
||||
|
||||
@@ -16,6 +16,7 @@ import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
import suwayomi.tachidesk.manga.model.table.toDataClass
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import xyz.nulldev.androidcompat.util.SafePath
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
@@ -76,13 +77,46 @@ object ChapterDownloadHelper {
|
||||
.select(ChapterTable.columns + MangaTable.columns)
|
||||
.where { ChapterTable.id eq chapterId }
|
||||
.firstOrNull() ?: throw IllegalArgumentException("ChapterId $chapterId not found")
|
||||
|
||||
val chapter = ChapterTable.toDataClass(row)
|
||||
val mangaTitle = row[MangaTable.title]
|
||||
val mangaTitle = row[MangaTable.title].trim()
|
||||
|
||||
val scanlatorPart = chapter.scanlator?.let { "[$it] " } ?: ""
|
||||
val fileName = "$mangaTitle - $scanlatorPart${chapter.name}.cbz"
|
||||
val scanlatorName = chapter.scanlator?.trim()?.takeIf { it.isNotEmpty() }
|
||||
val chapterName = chapter.name.trim().takeIf { it.isNotEmpty() }
|
||||
|
||||
Pair(chapter, fileName)
|
||||
val fileName =
|
||||
buildString {
|
||||
append(mangaTitle)
|
||||
append(" - ")
|
||||
|
||||
if (chapterName != null) {
|
||||
append(chapterName)
|
||||
} else if (chapter.chapterNumber >= 0f) {
|
||||
// chapterNumber is stored as Float, drop .0 for whole numbers.
|
||||
val formatNumber =
|
||||
if (chapter.chapterNumber % 1 == 0f) {
|
||||
chapter.chapterNumber.toInt().toString()
|
||||
} else {
|
||||
chapter.chapterNumber.toString()
|
||||
}
|
||||
append("#$formatNumber")
|
||||
} else {
|
||||
// Fallback when neither name nor valid chapter number exists
|
||||
append("#${chapter.index}")
|
||||
}
|
||||
|
||||
if (scanlatorName != null) {
|
||||
append(" [")
|
||||
append(scanlatorName)
|
||||
append("]")
|
||||
}
|
||||
append(".cbz")
|
||||
}
|
||||
|
||||
// Sanitize filename for OS compatibility
|
||||
val safeFileName = SafePath.buildValidFilename(fileName)
|
||||
|
||||
Pair(chapter, safeFileName)
|
||||
}
|
||||
|
||||
fun getCbzForDownload(
|
||||
|
||||
@@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.HttpException
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
@@ -41,8 +41,8 @@ import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import suwayomi.tachidesk.manga.impl.download.fileProvider.impl.MissingThumbnailException
|
||||
import suwayomi.tachidesk.manga.impl.util.network.await
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrNull
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.source.StubSource
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.clearCachedImage
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
|
||||
@@ -81,7 +81,7 @@ object Manga {
|
||||
return if (!onlineFetch && mangaEntry[MangaTable.initialized]) {
|
||||
MangaTable.toDataClass(mangaEntry)
|
||||
} else { // initialize manga
|
||||
fetchManga(mangaId) ?: return MangaTable.toDataClass(mangaEntry)
|
||||
updateMangaAndChapters(mangaId, updateChapters = false)
|
||||
|
||||
mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||
|
||||
@@ -91,7 +91,7 @@ object Manga {
|
||||
|
||||
suspend fun fetchMangaAndChapters(
|
||||
mangaEntry: ResultRow,
|
||||
source: CatalogueSource,
|
||||
source: Source,
|
||||
fetchDetails: Boolean,
|
||||
fetchChapters: Boolean,
|
||||
): SMangaUpdate {
|
||||
@@ -139,7 +139,7 @@ object Manga {
|
||||
return mangaInfoMutex.get(mangaId) { Mutex() }.withLock {
|
||||
val mangaEntry =
|
||||
transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||
val source = getCatalogueSourceOrNull(mangaEntry[MangaTable.sourceReference]) ?: return null
|
||||
val source = getSourceOrNull(mangaEntry[MangaTable.sourceReference]) ?: return null
|
||||
val sManga =
|
||||
fetchMangaAndChapters(
|
||||
mangaEntry,
|
||||
@@ -152,9 +152,41 @@ object Manga {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateMangaAndChapters(
|
||||
mangaId: Int,
|
||||
updateManga: Boolean = true,
|
||||
updateChapters: Boolean = true,
|
||||
) {
|
||||
mangaInfoMutex.get(mangaId) { Mutex() }.withLock {
|
||||
var mangaEntry =
|
||||
transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||
val source =
|
||||
getSourceOrNull(mangaEntry[MangaTable.sourceReference])
|
||||
?: throw NullPointerException("Missing source ${mangaEntry[MangaTable.sourceReference]}")
|
||||
val mangaUpdate =
|
||||
fetchMangaAndChapters(
|
||||
mangaEntry,
|
||||
source,
|
||||
fetchDetails = updateManga,
|
||||
fetchChapters = updateChapters,
|
||||
)
|
||||
|
||||
if (updateManga) {
|
||||
updateMangaDatabase(mangaEntry, source, mangaUpdate.manga)
|
||||
mangaEntry =
|
||||
transaction {
|
||||
MangaTable.selectAll().where { MangaTable.id eq mangaId }.first()
|
||||
}
|
||||
}
|
||||
if (updateChapters) {
|
||||
Chapter.updateChapterListDatabase(mangaEntry, mangaUpdate.chapters, source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMangaDatabase(
|
||||
mangaEntry: ResultRow,
|
||||
source: CatalogueSource,
|
||||
source: Source,
|
||||
sManga: SManga,
|
||||
): SManga {
|
||||
transaction {
|
||||
@@ -206,6 +238,7 @@ object Manga {
|
||||
it[MangaTable.lastFetchedAt] = Instant.now().epochSecond
|
||||
|
||||
it[MangaTable.updateStrategy] = sManga.update_strategy.name
|
||||
it[MangaTable.memo] = Json.encodeToString(sManga.memo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,7 +413,7 @@ object Manga {
|
||||
val mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||
val sourceId = mangaEntry[MangaTable.sourceReference]
|
||||
|
||||
return when (val source = getCatalogueSourceOrStub(sourceId)) {
|
||||
return when (val source = getSourceOrStub(sourceId)) {
|
||||
is HttpSource -> {
|
||||
getImageResponse(cacheSaveDir, fileName) {
|
||||
fetchHttpSourceMangaThumbnail(source, mangaEntry)
|
||||
|
||||
@@ -19,7 +19,7 @@ import org.jetbrains.exposed.v1.jdbc.batchInsert
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
||||
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
import suwayomi.tachidesk.manga.model.table.toDataClass
|
||||
@@ -36,7 +36,7 @@ object MangaList {
|
||||
require(pageNum > 0) {
|
||||
"pageNum = $pageNum is not in valid range"
|
||||
}
|
||||
val source = getCatalogueSourceOrStub(sourceId)
|
||||
val source = getSourceOrStub(sourceId)
|
||||
val mangasPage =
|
||||
if (popular) {
|
||||
source.getPopularManga(pageNum)
|
||||
|
||||
@@ -21,7 +21,7 @@ import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import suwayomi.tachidesk.graphql.types.DownloadConversion
|
||||
import suwayomi.tachidesk.manga.impl.util.getChapterCachePath
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
@@ -118,7 +118,7 @@ object Page {
|
||||
return imageFile.inputStream() to (ImageUtil.findImageType { imageFile.inputStream() }?.mime ?: "image/jpeg")
|
||||
}
|
||||
|
||||
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
val source = getSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
source as HttpSource
|
||||
|
||||
if (pageEntry[PageTable.imageUrl] == null) {
|
||||
|
||||
@@ -7,14 +7,14 @@ package suwayomi.tachidesk.manga.impl
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import io.javalin.json.JsonMapper
|
||||
import io.javalin.json.fromJsonString
|
||||
import kotlinx.serialization.Serializable
|
||||
import suwayomi.tachidesk.manga.impl.MangaList.processEntries
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
||||
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
@@ -24,7 +24,7 @@ object Search {
|
||||
searchTerm: String,
|
||||
pageNum: Int,
|
||||
): PagedMangaListDataClass {
|
||||
val source = getCatalogueSourceOrStub(sourceId)
|
||||
val source = getSourceOrStub(sourceId)
|
||||
val searchManga = source.getSearchManga(pageNum, searchTerm, getFilterListOf(source))
|
||||
return searchManga.processEntries(sourceId)
|
||||
}
|
||||
@@ -34,7 +34,7 @@ object Search {
|
||||
pageNum: Int,
|
||||
filter: FilterData,
|
||||
): PagedMangaListDataClass {
|
||||
val source = getCatalogueSourceOrStub(sourceId)
|
||||
val source = getSourceOrStub(sourceId)
|
||||
val filterList = if (filter.filter != null) buildFilterList(sourceId, filter.filter) else source.getFilterList()
|
||||
val searchManga = source.getSearchManga(pageNum, filter.searchTerm ?: "", filterList)
|
||||
return searchManga.processEntries(sourceId)
|
||||
@@ -43,7 +43,7 @@ object Search {
|
||||
private val filterListCache = mutableMapOf<Long, FilterList>()
|
||||
|
||||
private fun getFilterListOf(
|
||||
source: CatalogueSource,
|
||||
source: Source,
|
||||
reset: Boolean = false,
|
||||
): FilterList {
|
||||
if (reset || !filterListCache.containsKey(source.id)) {
|
||||
@@ -56,7 +56,7 @@ object Search {
|
||||
sourceId: Long,
|
||||
reset: Boolean,
|
||||
): List<FilterObject> {
|
||||
val source = getCatalogueSourceOrStub(sourceId)
|
||||
val source = getSourceOrStub(sourceId)
|
||||
|
||||
return getFilterListOf(source, reset).list.map {
|
||||
FilterObject(
|
||||
@@ -111,7 +111,7 @@ object Search {
|
||||
sourceId: Long,
|
||||
changes: List<FilterChange>,
|
||||
) {
|
||||
val source = getCatalogueSourceOrStub(sourceId)
|
||||
val source = getSourceOrStub(sourceId)
|
||||
val filterList = getFilterListOf(source, false)
|
||||
updateFilterList(filterList, changes)
|
||||
}
|
||||
@@ -169,7 +169,7 @@ object Search {
|
||||
sourceId: Long,
|
||||
changes: List<FilterChange>,
|
||||
): FilterList {
|
||||
val source = getCatalogueSourceOrStub(sourceId)
|
||||
val source = getSourceOrStub(sourceId)
|
||||
val filterList = source.getFilterList()
|
||||
return updateFilterList(filterList, changes)
|
||||
}
|
||||
|
||||
@@ -26,9 +26,10 @@ import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import suwayomi.tachidesk.manga.impl.Source.preferenceScreenMap
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension.proxyExtensionIconUrl
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.unregisterCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrNull
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.unregisterSource
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||
import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
import suwayomi.tachidesk.manga.model.table.SourceMetaTable
|
||||
@@ -42,7 +43,7 @@ object Source {
|
||||
fun getSourceList(): List<SourceDataClass> {
|
||||
return transaction {
|
||||
SourceTable.selectAll().mapNotNull {
|
||||
val catalogueSource = getCatalogueSourceOrNull(it[SourceTable.id].value) ?: return@mapNotNull null
|
||||
val catalogueSource = getSourceOrNull(it[SourceTable.id].value) ?: return@mapNotNull null
|
||||
val sourceExtension = ExtensionTable.selectAll().where { ExtensionTable.id eq it[SourceTable.extension] }.first()
|
||||
|
||||
SourceDataClass(
|
||||
@@ -52,7 +53,7 @@ object Source {
|
||||
iconUrl = proxyExtensionIconUrl(sourceExtension[ExtensionTable.pkgName]),
|
||||
supportsLatest = catalogueSource.supportsLatest,
|
||||
isConfigurable = catalogueSource is ConfigurableSource,
|
||||
isNsfw = it[SourceTable.contentRating] >= 3,
|
||||
isNsfw = it[SourceTable.contentWarning] >= ContentWarning.MIXED.ordinal,
|
||||
displayName = catalogueSource.toString(),
|
||||
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
|
||||
)
|
||||
@@ -63,7 +64,7 @@ object Source {
|
||||
fun getSource(sourceId: Long): SourceDataClass? { // all the data extracted fresh form the source instance
|
||||
return transaction {
|
||||
val source = SourceTable.selectAll().where { SourceTable.id eq sourceId }.firstOrNull() ?: return@transaction null
|
||||
val catalogueSource = getCatalogueSourceOrNull(sourceId) ?: return@transaction null
|
||||
val catalogueSource = getSourceOrNull(sourceId) ?: return@transaction null
|
||||
val extension = ExtensionTable.selectAll().where { ExtensionTable.id eq source[SourceTable.extension] }.first()
|
||||
|
||||
SourceDataClass(
|
||||
@@ -73,7 +74,7 @@ object Source {
|
||||
iconUrl = proxyExtensionIconUrl(extension[ExtensionTable.pkgName]),
|
||||
supportsLatest = catalogueSource.supportsLatest,
|
||||
isConfigurable = catalogueSource is ConfigurableSource,
|
||||
isNsfw = source[SourceTable.contentRating] >= 3,
|
||||
isNsfw = source[SourceTable.contentWarning] >= ContentWarning.MIXED.ordinal,
|
||||
displayName = catalogueSource.toString(),
|
||||
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
|
||||
)
|
||||
@@ -106,7 +107,7 @@ object Source {
|
||||
}
|
||||
|
||||
fun getSourcePreferencesRaw(sourceId: Long): List<Preference> {
|
||||
val source = getCatalogueSourceOrStub(sourceId)
|
||||
val source = getSourceOrStub(sourceId)
|
||||
|
||||
if (source is ConfigurableSource) {
|
||||
val sourceShardPreferences = source.sourcePreferences()
|
||||
@@ -156,7 +157,7 @@ object Source {
|
||||
pref.callChangeListener(newValue)
|
||||
|
||||
// must reload the source because a preference was changed
|
||||
unregisterCatalogueSource(sourceId)
|
||||
unregisterSource(sourceId)
|
||||
}
|
||||
|
||||
fun getSourcesMetaMaps(ids: List<Long>): Map<Long, Map<String, String>> =
|
||||
|
||||
@@ -8,7 +8,6 @@ package suwayomi.tachidesk.manga.impl.backup.proto.handlers
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
@@ -78,7 +77,7 @@ object BackupMangaHandler {
|
||||
lastModifiedAt = mangaRow[MangaTable.lastModifiedAt],
|
||||
version = mangaRow[MangaTable.version],
|
||||
initialized = mangaRow[MangaTable.initialized],
|
||||
memo = Json.encodeToString(mangaRow[MangaTable.memo]).encodeToByteArray(),
|
||||
memo = mangaRow[MangaTable.memo].encodeToByteArray(),
|
||||
)
|
||||
|
||||
val mangaId = mangaRow[MangaTable.id].value
|
||||
@@ -116,6 +115,7 @@ object BackupMangaHandler {
|
||||
sourceOrder = chapters.size - it[ChapterTable.sourceOrder],
|
||||
lastModifiedAt = it[ChapterTable.lastModifiedAt],
|
||||
version = it[ChapterTable.version],
|
||||
memo = it[ChapterTable.memo].encodeToByteArray(),
|
||||
).apply {
|
||||
if (flags.includeClientData) {
|
||||
this.meta = chapterToMeta[it[ChapterTable.id].value] ?: emptyMap()
|
||||
|
||||
@@ -39,7 +39,7 @@ data class BackupManga(
|
||||
@ProtoNumber(106) var lastModifiedAt: Long = 0,
|
||||
@ProtoNumber(109) var version: Long = 0,
|
||||
@ProtoNumber(111) var initialized: Boolean = false,
|
||||
@ProtoNumber(13) var memo: ByteArray = JsonObjectEmptyBytes,
|
||||
@ProtoNumber(112) var memo: ByteArray = JsonObjectEmptyBytes,
|
||||
// suwayomi
|
||||
@ProtoNumber(9000) var meta: Map<String, String> = emptyMap(),
|
||||
)
|
||||
|
||||
@@ -23,7 +23,7 @@ import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import suwayomi.tachidesk.manga.impl.ChapterDownloadHelper
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
@@ -77,7 +77,7 @@ suspend fun refreshChapterPageList(
|
||||
return mutex.withLock {
|
||||
val chapterEntry = existingChapterEntry ?: transaction { ChapterTable.selectAll().where { ChapterTable.id eq chapterId }.first() }
|
||||
val mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
val source = getSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
|
||||
val pageList =
|
||||
source
|
||||
|
||||
@@ -10,7 +10,6 @@ package suwayomi.tachidesk.manga.impl.extension
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||
@@ -32,13 +31,16 @@ import suwayomi.tachidesk.manga.impl.util.PackageTools
|
||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.EXTENSION_FEATURE
|
||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MAX
|
||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MIN
|
||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.METADATA_CONTENT_WARNING
|
||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.METADATA_EXTENSION_LIB
|
||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.METADATA_NAME
|
||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.METADATA_NSFW
|
||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.METADATA_SOURCE_CLASS
|
||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.dex2jar
|
||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.getPackageInfo
|
||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.loadExtensionSources
|
||||
import suwayomi.tachidesk.manga.impl.util.network.await
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.clearCachedImage
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.saveImage
|
||||
@@ -150,7 +152,19 @@ object Extension {
|
||||
// throw Exception("This apk is not a signed with the official tachiyomi signature")
|
||||
// }
|
||||
|
||||
val isNsfw = packageInfo.applicationInfo.metaData.getString(METADATA_NSFW) == "1"
|
||||
var contentWarning = packageInfo.applicationInfo.metaData.getInt(METADATA_CONTENT_WARNING)
|
||||
if (contentWarning == 0) {
|
||||
contentWarning = packageInfo.applicationInfo.metaData
|
||||
.getString(METADATA_CONTENT_WARNING)
|
||||
?.toIntOrNull()
|
||||
?: 0
|
||||
if (contentWarning == 0) {
|
||||
contentWarning = packageInfo.applicationInfo.metaData
|
||||
.getString(METADATA_NSFW)
|
||||
?.toIntOrNull()
|
||||
?: 0
|
||||
}
|
||||
}
|
||||
|
||||
val className =
|
||||
packageInfo.packageName + packageInfo.applicationInfo.metaData.getString(METADATA_SOURCE_CLASS)
|
||||
@@ -159,7 +173,7 @@ object Extension {
|
||||
|
||||
dex2jar(apkFilePath, jarFilePath, fileNameWithoutType)
|
||||
extractAssetsFromApk(apkFilePath, jarFilePath)
|
||||
extractAndCacheApkIcon(apkFilePath, apkName)
|
||||
extractAndCacheApkIcon(apkFilePath, packageInfo.packageName)
|
||||
|
||||
// clean up
|
||||
File(apkFilePath).delete()
|
||||
@@ -167,12 +181,12 @@ object Extension {
|
||||
try {
|
||||
// collect sources from the extension
|
||||
val extensionMainClassInstance = loadExtensionSources(jarFilePath, className)
|
||||
val sources: List<CatalogueSource> =
|
||||
val sources: List<Source> =
|
||||
when (extensionMainClassInstance) {
|
||||
is Source -> listOf(extensionMainClassInstance)
|
||||
is SourceFactory -> extensionMainClassInstance.createSources()
|
||||
else -> throw RuntimeException("Unknown source class type! ${extensionMainClassInstance.javaClass}")
|
||||
}.map { it as CatalogueSource }
|
||||
}
|
||||
|
||||
val langs = sources.map { it.lang }.toSet()
|
||||
val extensionLang =
|
||||
@@ -183,9 +197,16 @@ object Extension {
|
||||
}
|
||||
|
||||
val extensionName =
|
||||
packageInfo.applicationInfo.nonLocalizedLabel
|
||||
.toString()
|
||||
.substringAfter("Tachiyomi: ")
|
||||
packageInfo.applicationInfo.metaData.getString(METADATA_NAME)
|
||||
?: packageInfo.applicationInfo.nonLocalizedLabel
|
||||
.toString()
|
||||
.substringAfter("Tachiyomi: ")
|
||||
|
||||
val extensionLibVersion =
|
||||
packageInfo.applicationInfo.metaData
|
||||
.getString(METADATA_EXTENSION_LIB)
|
||||
.takeUnless { it == "0" }
|
||||
?: packageInfo.versionName.substringBeforeLast('.')
|
||||
|
||||
// update extension info
|
||||
transaction {
|
||||
@@ -196,8 +217,9 @@ object Extension {
|
||||
it[this.pkgName] = packageInfo.packageName
|
||||
it[versionName] = packageInfo.versionName
|
||||
it[versionCode] = packageInfo.versionCode.toLong()
|
||||
it[extensionLib] = extensionLibVersion
|
||||
it[lang] = extensionLang
|
||||
it[contentRating] = if (isNsfw) 3 else 0 // todo will change
|
||||
it[this.contentWarning] = contentWarning
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +244,7 @@ object Extension {
|
||||
it[name] = httpSource.name
|
||||
it[lang] = httpSource.lang
|
||||
it[extension] = extensionId
|
||||
it[contentRating] = if (isNsfw) 3 else 0
|
||||
it[this.contentWarning] = contentWarning
|
||||
}
|
||||
logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" }
|
||||
}
|
||||
@@ -243,7 +265,7 @@ object Extension {
|
||||
|
||||
private fun extractAndCacheApkIcon(
|
||||
apkFilePath: String,
|
||||
apkName: String,
|
||||
pkgName: String,
|
||||
) {
|
||||
val iconCacheDir = "${applicationDirs.extensionsRoot}/icon"
|
||||
try {
|
||||
@@ -256,15 +278,15 @@ object Extension {
|
||||
?.first
|
||||
}
|
||||
if (iconData == null) {
|
||||
logger.warn { "No icon found in APK $apkName" }
|
||||
logger.warn { "No icon found in APK $pkgName" }
|
||||
return
|
||||
}
|
||||
|
||||
File(iconCacheDir).mkdirs()
|
||||
clearCachedImage(iconCacheDir, apkName)
|
||||
saveImage("$iconCacheDir/$apkName", iconData.inputStream(), null)
|
||||
clearCachedImage(iconCacheDir, pkgName)
|
||||
saveImage("$iconCacheDir/$pkgName", iconData.inputStream(), null)
|
||||
} catch (e: Exception) {
|
||||
logger.warn(e) { "Failed to extract icon from APK $apkName" }
|
||||
logger.warn(e) { "Failed to extract icon from APK $pkgName" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,7 +379,7 @@ object Extension {
|
||||
|
||||
SourceTable.deleteWhere { SourceTable.extension eq extensionId }
|
||||
|
||||
if (extensionRecord[ExtensionTable.isObsolete]) {
|
||||
if (extensionRecord[ExtensionTable.isObsolete] || extensionRecord[ExtensionTable.apkUrl] == null) {
|
||||
ExtensionTable.deleteWhere { ExtensionTable.pkgName eq pkgName }
|
||||
} else {
|
||||
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
|
||||
@@ -375,7 +397,7 @@ object Extension {
|
||||
PackageTools.jarLoaderMap.remove(jarPath)?.close()
|
||||
|
||||
// clear all loaded sources
|
||||
sources.forEach { GetCatalogueSource.unregisterCatalogueSource(it) }
|
||||
sources.forEach { GetSource.unregisterSource(it) }
|
||||
|
||||
File(jarPath).delete()
|
||||
}
|
||||
@@ -390,7 +412,7 @@ object Extension {
|
||||
it[versionName] = targetExtension.versionName
|
||||
it[versionCode] = targetExtension.versionCode
|
||||
it[lang] = targetExtension.lang
|
||||
it[contentRating] = targetExtension.contentRating.ordinal
|
||||
it[contentWarning] = targetExtension.contentWarning.ordinal
|
||||
it[iconUrl] = targetExtension.iconUrl
|
||||
it[hasUpdate] = false
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ import kotlinx.serialization.decodeFromByteArray
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.okio.decodeFromBufferedSource
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
import okio.BufferedSource
|
||||
import okio.buffer
|
||||
import okio.gzip
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
||||
@@ -41,52 +44,49 @@ object ExtensionStoreService {
|
||||
val protoBuf: ProtoBuf by injectLazy()
|
||||
val json: Json by injectLazy()
|
||||
|
||||
suspend fun fetch(indexUrl: String): ExtensionStore = fetch(indexUrl, forceV2 = false)
|
||||
|
||||
private suspend fun fetch(
|
||||
indexUrl: String,
|
||||
forceV2: Boolean,
|
||||
): ExtensionStore {
|
||||
var updatedIndexUrl = indexUrl
|
||||
suspend fun fetch(indexUrl: String): ExtensionStore {
|
||||
var updatedIndexUrl: String = indexUrl
|
||||
return try {
|
||||
val response = network.client.newCall(GET(indexUrl)).awaitSuccess()
|
||||
response.body
|
||||
.source()
|
||||
.use { source ->
|
||||
try {
|
||||
protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.peek().readByteArray())
|
||||
} catch (e: IllegalArgumentException) {
|
||||
logger.debug { "Failed to decode as protobuf, trying JSON" }
|
||||
if (forceV2) throw e
|
||||
try {
|
||||
json.decodeFromBufferedSource<NetworkExtensionStore>(source.peek())
|
||||
} catch (_: IllegalArgumentException) {
|
||||
logger.debug { "Failed to decode as NetworkExtensionStore, trying LegacyExtensionRepo" }
|
||||
val legacyIndex =
|
||||
try {
|
||||
json.decodeFromBufferedSource<NetworkLegacyExtensionRepo>(source.peek())
|
||||
} catch (e: IllegalArgumentException) {
|
||||
if (!indexUrl.endsWith("/index.min.json")) {
|
||||
throw e
|
||||
}
|
||||
logger.debug { "Retrying with /index.min.json" }
|
||||
updatedIndexUrl = indexUrl.replace("/index.min.json", "/repo.json")
|
||||
network.client.newCall(GET(updatedIndexUrl)).awaitSuccess().body.source().use {
|
||||
json.decodeFromBufferedSource<NetworkLegacyExtensionRepo>(it)
|
||||
}
|
||||
val response = network.client.newCall(GET(updatedIndexUrl)).awaitSuccess()
|
||||
response.body.source().decompressIfGzipped().use { source ->
|
||||
val networkStore =
|
||||
when (source.peek().readByte()) {
|
||||
// "[..."
|
||||
0x5B.toByte() -> {
|
||||
run {
|
||||
if (!indexUrl.endsWith("/index.min.json")) {
|
||||
throw IllegalArgumentException("Provided legacy store url is not valid")
|
||||
}
|
||||
updatedIndexUrl = indexUrl.replace("/index.min.json", "/repo.json")
|
||||
network.client.newCall(GET(updatedIndexUrl)).awaitSuccess().body.source().use {
|
||||
json.decodeFromBufferedSource<NetworkLegacyExtensionRepo>(it)
|
||||
}
|
||||
|
||||
if (legacyIndex.indexV2 != null) {
|
||||
return fetch(legacyIndex.indexV2, forceV2 = true)
|
||||
} else {
|
||||
legacyIndex
|
||||
}
|
||||
}
|
||||
|
||||
// "{..."
|
||||
0x7B.toByte() -> {
|
||||
try {
|
||||
json.decodeFromBufferedSource<NetworkLegacyExtensionRepo>(source.peek())
|
||||
} catch (_: IllegalArgumentException) {
|
||||
json.decodeFromBufferedSource<NetworkExtensionStore>(source)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.readByteArray())
|
||||
}
|
||||
}
|
||||
}.toExtensionStore(updatedIndexUrl)
|
||||
|
||||
if (networkStore is NetworkLegacyExtensionRepo && networkStore.indexV2 != null) {
|
||||
return fetch(networkStore.indexV2)
|
||||
}
|
||||
|
||||
networkStore.toExtensionStore(updatedIndexUrl)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
logger.debug(e) { "Failed to fetch extension store '$indexUrl'" }
|
||||
logger.error(e) { "Failed to fetch extension store '$indexUrl'" }
|
||||
throw e
|
||||
}
|
||||
}
|
||||
@@ -108,6 +108,7 @@ object ExtensionStoreService {
|
||||
it[contactDiscord] = store.contact.discord
|
||||
it[indexUrl] = store.indexUrl
|
||||
it[isLegacy] = store.isLegacy
|
||||
it[extensionListUrl] = store.extensionListUrl
|
||||
}
|
||||
} else {
|
||||
ExtensionStoreTable.update({ ExtensionStoreTable.indexUrl eq store.indexUrl }) {
|
||||
@@ -117,6 +118,7 @@ object ExtensionStoreService {
|
||||
it[contactWebsite] = store.contact.website
|
||||
it[contactDiscord] = store.contact.discord
|
||||
it[isLegacy] = store.isLegacy
|
||||
it[extensionListUrl] = store.extensionListUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,24 +129,28 @@ object ExtensionStoreService {
|
||||
transaction {
|
||||
ExtensionStoreTable.selectAll().toList()
|
||||
}
|
||||
return stores.mapNotNull { storeRow ->
|
||||
val oldIndexUrl = storeRow[ExtensionStoreTable.indexUrl]
|
||||
val oldName = storeRow[ExtensionStoreTable.name]
|
||||
try {
|
||||
val store = fetch(oldIndexUrl)
|
||||
upsert(store)
|
||||
if (store.indexUrl != oldIndexUrl) {
|
||||
transaction {
|
||||
ExtensionStoreTable.deleteWhere { ExtensionStoreTable.indexUrl eq oldIndexUrl }
|
||||
var needsPrefUpdate = false
|
||||
val updateStores =
|
||||
stores.mapNotNull { storeRow ->
|
||||
val oldIndexUrl = storeRow[ExtensionStoreTable.indexUrl]
|
||||
val oldName = storeRow[ExtensionStoreTable.name]
|
||||
try {
|
||||
val store = fetch(oldIndexUrl)
|
||||
if (store.indexUrl != oldIndexUrl) {
|
||||
transaction {
|
||||
ExtensionStoreTable.deleteWhere { ExtensionStoreTable.indexUrl eq oldIndexUrl }
|
||||
}
|
||||
needsPrefUpdate = true
|
||||
}
|
||||
syncDbToPrefs()
|
||||
upsert(store)
|
||||
store
|
||||
} catch (e: Exception) {
|
||||
logger.warn(e) { "Failed to fetch extension store '$oldName ($oldIndexUrl)'" }
|
||||
null
|
||||
}
|
||||
store
|
||||
} catch (e: Exception) {
|
||||
logger.warn(e) { "Failed to fetch extension store '$oldName ($oldIndexUrl)'" }
|
||||
null
|
||||
}
|
||||
}
|
||||
if (needsPrefUpdate) syncDbToPrefs()
|
||||
return updateStores
|
||||
}
|
||||
|
||||
fun syncDbToPrefs() {
|
||||
@@ -178,37 +184,64 @@ object ExtensionStoreService {
|
||||
}
|
||||
|
||||
val toAdd = prefUrls - dbStores.keys
|
||||
val toRemove = (dbStores.keys - prefUrls).toMutableSet()
|
||||
var needsPrefUpdate = toRemove.isNotEmpty()
|
||||
|
||||
toAdd.forEach { url ->
|
||||
try {
|
||||
val store = fetch(url)
|
||||
if (store.indexUrl != url) {
|
||||
transaction {
|
||||
ExtensionStoreTable.deleteWhere { ExtensionStoreTable.indexUrl eq url }
|
||||
}
|
||||
needsPrefUpdate = true
|
||||
toRemove -= store.indexUrl
|
||||
}
|
||||
upsert(store)
|
||||
} catch (e: Exception) {
|
||||
logger.warn(e) { "Failed to sync preference store '$url' to database" }
|
||||
}
|
||||
}
|
||||
|
||||
val toRemove = dbStores.keys - prefUrls
|
||||
if (toRemove.isNotEmpty()) {
|
||||
transaction {
|
||||
ExtensionStoreTable.deleteWhere { ExtensionStoreTable.indexUrl inList toRemove.toList() }
|
||||
}
|
||||
}
|
||||
if (needsPrefUpdate) {
|
||||
syncDbToPrefs()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getExtensions(store: ExtensionStore): List<ExtensionInfo> {
|
||||
val extensions =
|
||||
if (!store.isLegacy) {
|
||||
val response = network.client.newCall(GET(store.indexUrl)).awaitSuccess()
|
||||
response.body
|
||||
.source()
|
||||
.use { source ->
|
||||
try {
|
||||
protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.peek().readByteArray())
|
||||
} catch (_: IllegalArgumentException) {
|
||||
json.decodeFromBufferedSource<NetworkExtensionStore>(source.peek())
|
||||
if (store.extensionListUrl != null) {
|
||||
val response = network.client.newCall(GET(store.extensionListUrl)).awaitSuccess()
|
||||
response.body.source().decompressIfGzipped().use { source ->
|
||||
when (source.peek().readByte()) {
|
||||
// "{..."
|
||||
0x7B.toByte() -> {
|
||||
json.decodeFromBufferedSource<NetworkExtensionStore.ExtensionList>(source)
|
||||
}
|
||||
|
||||
else -> {
|
||||
protoBuf.decodeFromByteArray<NetworkExtensionStore.ExtensionList>(
|
||||
source.readByteArray(),
|
||||
)
|
||||
}
|
||||
}.toExtensionInfos(store)
|
||||
}
|
||||
} else if (!store.isLegacy) {
|
||||
val response = network.client.newCall(GET(store.indexUrl)).awaitSuccess()
|
||||
response.body.source().decompressIfGzipped().use { source ->
|
||||
when (source.peek().readByte()) {
|
||||
// "{..."
|
||||
0x7B.toByte() -> json.decodeFromBufferedSource<NetworkExtensionStore>(source)
|
||||
|
||||
else -> protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.readByteArray())
|
||||
}.extensionList!!
|
||||
.toExtensionInfos(store)
|
||||
}
|
||||
} else {
|
||||
val storeBaseUrl = store.indexUrl.removeSuffix("/repo.json")
|
||||
val response = network.client.newCall(GET("$storeBaseUrl/index.min.json")).awaitSuccess()
|
||||
@@ -220,4 +253,17 @@ object ExtensionStoreService {
|
||||
}
|
||||
return extensions
|
||||
}
|
||||
|
||||
private fun BufferedSource.decompressIfGzipped(): BufferedSource {
|
||||
val isGzip =
|
||||
peek().use { peeked ->
|
||||
try {
|
||||
peeked.readShort().toInt() == 0x1f8b
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
return if (isGzip) gzip().buffer() else this
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension.proxyExtensionIconUrl
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
@@ -80,7 +81,7 @@ object ExtensionsList {
|
||||
versionName = it[ExtensionTable.versionName],
|
||||
versionCode = it[ExtensionTable.versionCode].toInt(),
|
||||
lang = it[ExtensionTable.lang],
|
||||
isNsfw = it[ExtensionTable.contentRating] == 3,
|
||||
isNsfw = it[ExtensionTable.contentWarning] >= ContentWarning.MIXED.ordinal,
|
||||
installed = it[ExtensionTable.isInstalled],
|
||||
hasUpdate = it[ExtensionTable.hasUpdate],
|
||||
obsolete = it[ExtensionTable.isObsolete],
|
||||
@@ -172,7 +173,7 @@ object ExtensionsList {
|
||||
this[ExtensionTable.versionName] = foundExtension.versionName
|
||||
this[ExtensionTable.versionCode] = foundExtension.versionCode
|
||||
this[ExtensionTable.lang] = foundExtension.lang
|
||||
this[ExtensionTable.contentRating] = foundExtension.contentRating.ordinal
|
||||
this[ExtensionTable.contentWarning] = foundExtension.contentWarning.ordinal
|
||||
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||
}
|
||||
@@ -189,7 +190,7 @@ object ExtensionsList {
|
||||
this[ExtensionTable.versionName] = foundExtension.versionName
|
||||
this[ExtensionTable.versionCode] = foundExtension.versionCode
|
||||
this[ExtensionTable.lang] = foundExtension.lang
|
||||
this[ExtensionTable.contentRating] = foundExtension.contentRating.ordinal
|
||||
this[ExtensionTable.contentWarning] = foundExtension.contentWarning.ordinal
|
||||
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionSource
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionStore
|
||||
@@ -23,7 +23,8 @@ data class NetworkExtensionStore(
|
||||
@ProtoNumber(2) val badgeLabel: String,
|
||||
@ProtoNumber(3) val signingKey: String,
|
||||
@ProtoNumber(4) val contact: Contact,
|
||||
@ProtoNumber(5) val extensions: List<Extension>,
|
||||
@ProtoNumber(101) val extensionList: ExtensionList?,
|
||||
@ProtoNumber(102) val extensionListUrl: String?,
|
||||
) : BaseNetworkExtensionStore {
|
||||
@Serializable
|
||||
data class Contact(
|
||||
@@ -31,6 +32,11 @@ data class NetworkExtensionStore(
|
||||
@ProtoNumber(2) val discord: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ExtensionList(
|
||||
@ProtoNumber(1) val extensions: List<Extension>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Extension(
|
||||
@ProtoNumber(1) val name: String,
|
||||
@@ -39,7 +45,8 @@ data class NetworkExtensionStore(
|
||||
@ProtoNumber(4) val extensionLib: String,
|
||||
@ProtoNumber(5) val versionCode: Long,
|
||||
@ProtoNumber(6) val versionName: String,
|
||||
@ProtoNumber(7) val sources: List<Source>,
|
||||
@ProtoNumber(7) val contentWarning: ContentWarning,
|
||||
@ProtoNumber(8) val sources: List<Source>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -55,27 +62,27 @@ data class NetworkExtensionStore(
|
||||
@ProtoNumber(3) val language: String,
|
||||
@ProtoNumber(4) val homeUrl: String = "",
|
||||
@ProtoNumber(5) val mirrorUrls: List<String> = emptyList(),
|
||||
@ProtoNumber(6) val contentRating: ContentRating = ContentRating.SAFE,
|
||||
// @ProtoNumber(6) val contentWarning: ContentWarning = ContentWarning.SAFE,
|
||||
@ProtoNumber(7) val message: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class ContentRating {
|
||||
enum class ContentWarning {
|
||||
@ProtoNumber(0)
|
||||
@JsonNames("CONTENT_RATING_SAFE")
|
||||
SAFE,
|
||||
@JsonNames("CONTENT_WARNING_UNSPECIFIED")
|
||||
UNSPECIFIED,
|
||||
|
||||
@ProtoNumber(1)
|
||||
@JsonNames("CONTENT_RATING_SUGGESTIVE")
|
||||
SUGGESTIVE,
|
||||
@JsonNames("CONTENT_WARNING_SAFE")
|
||||
SAFE,
|
||||
|
||||
@ProtoNumber(2)
|
||||
@JsonNames("CONTENT_RATING_EROTICA")
|
||||
EROTICA,
|
||||
@JsonNames("CONTENT_WARNING_MIXED")
|
||||
MIXED,
|
||||
|
||||
@ProtoNumber(3)
|
||||
@JsonNames("CONTENT_RATING_PORNOGRAPHIC")
|
||||
PORNOGRAPHIC,
|
||||
@JsonNames("CONTENT_WARNING_NSFW")
|
||||
NSFW,
|
||||
}
|
||||
|
||||
override fun toExtensionStore(indexUrl: String): ExtensionStore =
|
||||
@@ -90,10 +97,11 @@ data class NetworkExtensionStore(
|
||||
discord = contact.discord,
|
||||
),
|
||||
isLegacy = false,
|
||||
extensionListUrl = extensionListUrl,
|
||||
)
|
||||
}
|
||||
|
||||
fun NetworkExtensionStore.toExtensionInfos(store: ExtensionStore): List<ExtensionInfo> =
|
||||
fun NetworkExtensionStore.ExtensionList.toExtensionInfos(store: ExtensionStore): List<ExtensionInfo> =
|
||||
extensions.map { extension ->
|
||||
val lang = extension.sources.map { it.language }.toSet()
|
||||
ExtensionInfo(
|
||||
@@ -106,13 +114,15 @@ fun NetworkExtensionStore.toExtensionInfos(store: ExtensionStore): List<Extensio
|
||||
versionCode = extension.versionCode,
|
||||
versionName = extension.versionName,
|
||||
lang = if (lang.size == 1) lang.first() else "all",
|
||||
contentRating =
|
||||
when (extension.sources.maxOfOrNull { it.contentRating }) {
|
||||
NetworkExtensionStore.ContentRating.SAFE -> ContentRating.SAFE
|
||||
NetworkExtensionStore.ContentRating.SUGGESTIVE -> ContentRating.SUGGESTIVE
|
||||
NetworkExtensionStore.ContentRating.EROTICA -> ContentRating.EROTICA
|
||||
NetworkExtensionStore.ContentRating.PORNOGRAPHIC -> ContentRating.PORNOGRAPHIC
|
||||
null -> ContentRating.SAFE
|
||||
contentWarning =
|
||||
when (extension.contentWarning) {
|
||||
NetworkExtensionStore.ContentWarning.SAFE,
|
||||
NetworkExtensionStore.ContentWarning.UNSPECIFIED,
|
||||
-> ContentWarning.SAFE
|
||||
|
||||
NetworkExtensionStore.ContentWarning.MIXED -> ContentWarning.MIXED
|
||||
|
||||
NetworkExtensionStore.ContentWarning.NSFW -> ContentWarning.NSFW
|
||||
},
|
||||
sources =
|
||||
extension.sources.map { source ->
|
||||
@@ -121,6 +131,17 @@ fun NetworkExtensionStore.toExtensionInfos(store: ExtensionStore): List<Extensio
|
||||
name = source.name,
|
||||
lang = source.language,
|
||||
homeUrl = source.homeUrl,
|
||||
message = source.message,
|
||||
contentWarning =
|
||||
when (extension.contentWarning) { // todo source.contentWarning
|
||||
NetworkExtensionStore.ContentWarning.SAFE,
|
||||
NetworkExtensionStore.ContentWarning.UNSPECIFIED,
|
||||
-> ContentWarning.SAFE
|
||||
|
||||
NetworkExtensionStore.ContentWarning.MIXED -> ContentWarning.MIXED
|
||||
|
||||
NetworkExtensionStore.ContentWarning.NSFW -> ContentWarning.NSFW
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ package suwayomi.tachidesk.manga.impl.extension.github
|
||||
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionSource
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionStore
|
||||
@@ -49,7 +49,7 @@ fun NetworkLegacyExtension.toExtensionInfo(
|
||||
versionCode = code,
|
||||
versionName = version,
|
||||
lang = lang,
|
||||
contentRating = if (nsfw == 1) ContentRating.PORNOGRAPHIC else ContentRating.SAFE,
|
||||
contentWarning = if (nsfw == 1) ContentWarning.MIXED else ContentWarning.SAFE,
|
||||
sources =
|
||||
if (sources.isNullOrEmpty()) {
|
||||
listOf(
|
||||
@@ -58,6 +58,8 @@ fun NetworkLegacyExtension.toExtensionInfo(
|
||||
name = name,
|
||||
lang = lang,
|
||||
homeUrl = "",
|
||||
message = null,
|
||||
contentWarning = if (nsfw == 1) ContentWarning.MIXED else ContentWarning.SAFE,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
@@ -67,6 +69,8 @@ fun NetworkLegacyExtension.toExtensionInfo(
|
||||
name = source.name,
|
||||
lang = source.lang,
|
||||
homeUrl = source.baseUrl,
|
||||
message = null,
|
||||
contentWarning = if (nsfw == 1) ContentWarning.MIXED else ContentWarning.SAFE,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -36,5 +36,6 @@ data class NetworkLegacyExtensionRepo(
|
||||
discord = null,
|
||||
),
|
||||
isLegacy = true,
|
||||
extensionListUrl = null,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -127,29 +127,36 @@ object KoreaderSyncService {
|
||||
}
|
||||
|
||||
val mangaId = chapterRow[ChapterTable.manga].value
|
||||
val isDownloaded = chapterRow[ChapterTable.isDownloaded]
|
||||
val checksumMethod = serverConfig.koreaderSyncChecksumMethod.value
|
||||
|
||||
val newHash =
|
||||
when (checksumMethod) {
|
||||
KoreaderSyncChecksumMethod.BINARY -> {
|
||||
logger.debug { "[KOSYNC HASH] No hash for chapterId=$chapterId. Generating from downloaded content." }
|
||||
try {
|
||||
// Always create a CBZ in memory if it doesn't exist
|
||||
val (stream, _) = ChapterDownloadHelper.getArchiveStreamWithSize(mangaId, chapterId)
|
||||
// Write the stream to a temp file for partial hashing
|
||||
val tempFile = File.createTempFile("kosync-hash-", ".cbz")
|
||||
// Only generate binary hash if the chapter is downloaded to avoid fetching missing files
|
||||
if (isDownloaded) {
|
||||
logger.debug { "[KOSYNC HASH] No hash for chapterId=$chapterId. Generating from downloaded content." }
|
||||
try {
|
||||
tempFile.outputStream().use { fos ->
|
||||
stream.use { it.copyTo(fos) }
|
||||
// Always create a CBZ in memory if it doesn't exist
|
||||
val (stream, _) = ChapterDownloadHelper.getArchiveStreamWithSize(mangaId, chapterId)
|
||||
// Write the stream to a temp file for partial hashing
|
||||
val tempFile = File.createTempFile("kosync-hash-", ".cbz")
|
||||
try {
|
||||
tempFile.outputStream().use { fos ->
|
||||
stream.use { it.copyTo(fos) }
|
||||
}
|
||||
// Use the same hashing method as for downloads
|
||||
KoreaderHelper.hashContents(tempFile)
|
||||
} finally {
|
||||
// Always delete the temp file
|
||||
tempFile.delete()
|
||||
}
|
||||
// Use the same hashing method as for downloads
|
||||
KoreaderHelper.hashContents(tempFile)
|
||||
} finally {
|
||||
// Always delete the temp file
|
||||
tempFile.delete()
|
||||
} catch (e: Exception) {
|
||||
logger.warn(e) { "[KOSYNC HASH] Failed to generate archive stream for chapterId=$chapterId." }
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.warn(e) { "[KOSYNC HASH] Failed to generate archive stream for chapterId=$chapterId." }
|
||||
} else {
|
||||
logger.debug { "[KOSYNC HASH] Skipping binary hash for chapterId=$chapterId because it is not downloaded." }
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -175,7 +182,7 @@ object KoreaderSyncService {
|
||||
}
|
||||
logger.info { "[KOSYNC HASH] Generated and saved new hash for chapterId=$chapterId" }
|
||||
} else {
|
||||
logger.warn { "[KOSYNC HASH] Hashing failed for chapterId=$chapterId." }
|
||||
logger.warn { "[KOSYNC HASH] Hashing failed or skipped for chapterId=$chapterId." }
|
||||
}
|
||||
newHash
|
||||
}
|
||||
|
||||
@@ -222,6 +222,48 @@ object Track {
|
||||
}
|
||||
}
|
||||
|
||||
fun bindTrackRecord(
|
||||
mangaId: Int,
|
||||
trackRecordId: Int,
|
||||
): Int {
|
||||
val (trackRecord, existingTrackRecord) =
|
||||
transaction {
|
||||
val trackRecord =
|
||||
TrackRecordTable
|
||||
.selectAll()
|
||||
.where {
|
||||
(TrackRecordTable.id eq trackRecordId)
|
||||
}.first()
|
||||
.toTrackRecordDataClass()
|
||||
|
||||
val existingTrackRecord =
|
||||
TrackRecordTable
|
||||
.selectAll()
|
||||
.where {
|
||||
(TrackRecordTable.mangaId eq mangaId) and (TrackRecordTable.trackerId eq trackRecord.trackerId)
|
||||
}.firstOrNull()
|
||||
?.toTrackRecordDataClass()
|
||||
|
||||
trackRecord to existingTrackRecord
|
||||
}
|
||||
|
||||
val isAlreadyBoundToManga = trackRecord.mangaId == mangaId
|
||||
if (isAlreadyBoundToManga) {
|
||||
return trackRecordId
|
||||
}
|
||||
|
||||
val hasRecordForTracker = existingTrackRecord != null
|
||||
if (hasRecordForTracker) {
|
||||
val updatedTrack = trackRecord.copy(id = existingTrackRecord.id, mangaId = mangaId).toTrack()
|
||||
|
||||
return updateTrackRecord(updatedTrack)
|
||||
}
|
||||
|
||||
val newTrack = trackRecord.copy(mangaId = mangaId).toTrack()
|
||||
|
||||
return insertTrackRecord(newTrack)
|
||||
}
|
||||
|
||||
suspend fun refresh(recordId: Int) {
|
||||
val recordDb =
|
||||
transaction {
|
||||
@@ -423,9 +465,9 @@ object Track {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTrackRecord(track: Track) = updateTrackRecords(listOf(track))
|
||||
fun updateTrackRecord(track: Track): Int = updateTrackRecords(listOf(track)).first()
|
||||
|
||||
fun updateTrackRecords(tracks: List<Track>) =
|
||||
fun updateTrackRecords(tracks: List<Track>): List<Int> =
|
||||
transaction {
|
||||
if (tracks.isNotEmpty()) {
|
||||
BatchUpdateStatement(TrackRecordTable)
|
||||
@@ -447,6 +489,8 @@ object Track {
|
||||
}.toExecutable()
|
||||
.execute(this@transaction)
|
||||
}
|
||||
|
||||
tracks.map { it.id!! }
|
||||
}
|
||||
|
||||
fun insertTrackRecord(track: Track): Int = insertTrackRecords(listOf(track)).first()
|
||||
|
||||
@@ -31,7 +31,6 @@ import kotlinx.coroutines.sync.withPermit
|
||||
import suwayomi.tachidesk.global.impl.sync.SyncManager
|
||||
import suwayomi.tachidesk.manga.impl.Category
|
||||
import suwayomi.tachidesk.manga.impl.CategoryManga
|
||||
import suwayomi.tachidesk.manga.impl.Chapter
|
||||
import suwayomi.tachidesk.manga.impl.Manga
|
||||
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
|
||||
import suwayomi.tachidesk.manga.model.dataclass.IncludeOrExclude
|
||||
@@ -311,10 +310,10 @@ class Updater : IUpdater {
|
||||
tracker[job.manga.id] =
|
||||
try {
|
||||
logger.info { "Updating ${job.manga}" }
|
||||
if (serverConfig.updateMangas.value || !job.manga.initialized) {
|
||||
Manga.getManga(job.manga.id, true)
|
||||
}
|
||||
Chapter.getChapterList(job.manga.id, true)
|
||||
Manga.updateMangaAndChapters(
|
||||
job.manga.id,
|
||||
updateManga = serverConfig.updateMangas.value || !job.manga.initialized,
|
||||
)
|
||||
job.copy(status = JobStatus.COMPLETE)
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Error while updating ${job.manga}" }
|
||||
|
||||
@@ -11,7 +11,7 @@ import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
import suwayomi.tachidesk.server.ApplicationDirs
|
||||
@@ -37,7 +37,7 @@ private fun getMangaDir(
|
||||
private fun getMangaDir(mangaId: Int): String =
|
||||
transaction {
|
||||
val mangaEntry = MangaTable.selectAll().where { MangaTable.id eq mangaId }.first()
|
||||
val source = GetCatalogueSource.getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
val source = GetSource.getSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
|
||||
getMangaDir(mangaEntry[MangaTable.title], source.toString())
|
||||
}
|
||||
|
||||
@@ -40,6 +40,11 @@ object PackageTools {
|
||||
const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
||||
const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
|
||||
const val METADATA_NSFW = "tachiyomi.extension.nsfw"
|
||||
|
||||
const val METADATA_NAME = "tachiyomix.name"
|
||||
const val METADATA_EXTENSION_LIB = "tachiyomix.extensionLib"
|
||||
const val METADATA_CONTENT_WARNING = "tachiyomix.contentWarning"
|
||||
|
||||
const val LIB_VERSION_MIN = 1.3
|
||||
const val LIB_VERSION_MAX = 1.6
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ package suwayomi.tachidesk.manga.impl.util.source
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
@@ -22,14 +21,14 @@ import suwayomi.tachidesk.server.ApplicationDirs
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
object GetCatalogueSource {
|
||||
object GetSource {
|
||||
private val logger = KotlinLogging.logger { }
|
||||
|
||||
private val sourceCache = ConcurrentHashMap<Long, CatalogueSource>()
|
||||
private val sourceCache = ConcurrentHashMap<Long, Source>()
|
||||
private val applicationDirs: ApplicationDirs by injectLazy()
|
||||
|
||||
private fun getCatalogueSource(sourceId: Long): CatalogueSource? {
|
||||
val cachedResult: CatalogueSource? = sourceCache[sourceId]
|
||||
private fun getSource(sourceId: Long): Source? {
|
||||
val cachedResult: Source? = sourceCache[sourceId]
|
||||
if (cachedResult != null) {
|
||||
return cachedResult
|
||||
}
|
||||
@@ -62,25 +61,25 @@ object GetCatalogueSource {
|
||||
return sourceCache[sourceId]!!
|
||||
}
|
||||
|
||||
fun getCatalogueSourceOrNull(sourceId: Long): CatalogueSource? =
|
||||
fun getSourceOrNull(sourceId: Long): Source? =
|
||||
try {
|
||||
getCatalogueSource(sourceId)
|
||||
getSource(sourceId)
|
||||
} catch (e: Exception) {
|
||||
logger.warn(e) { "getCatalogueSource($sourceId) failed" }
|
||||
null
|
||||
}
|
||||
|
||||
fun getCatalogueSourceOrStub(sourceId: Long): CatalogueSource = getCatalogueSourceOrNull(sourceId) ?: StubSource(sourceId)
|
||||
fun getSourceOrStub(sourceId: Long): Source = getSourceOrNull(sourceId) ?: StubSource(sourceId)
|
||||
|
||||
fun registerCatalogueSource(sourcePair: Pair<Long, CatalogueSource>) {
|
||||
fun registerSource(sourcePair: Pair<Long, Source>) {
|
||||
sourceCache += sourcePair
|
||||
}
|
||||
|
||||
fun unregisterCatalogueSource(sourceId: Long) {
|
||||
fun unregisterSource(sourceId: Long) {
|
||||
sourceCache.remove(sourceId)
|
||||
}
|
||||
|
||||
fun unregisterAllCatalogueSources() {
|
||||
fun unregisterAllSources() {
|
||||
(sourceCache - 0L).forEach { (id, _) ->
|
||||
sourceCache.remove(id)
|
||||
}
|
||||
@@ -46,7 +46,7 @@ object ImageResponse {
|
||||
/**
|
||||
* Get a cached image response
|
||||
*
|
||||
* Note: The caller should also call [clearCachedImage] when appropriate
|
||||
* Note: The caller should also call [ImageResponse.clearCachedImage] when appropriate
|
||||
*
|
||||
* @param cacheSavePath where to save the cached image. Caller should decide to use perma cache or temp cache (OS temp dir)
|
||||
* @param fileName what the saved cache file should be named
|
||||
|
||||
@@ -17,7 +17,7 @@ data class ExtensionInfo(
|
||||
val versionCode: Long,
|
||||
val versionName: String,
|
||||
val lang: String,
|
||||
val contentRating: ContentRating,
|
||||
val contentWarning: ContentWarning,
|
||||
val sources: List<ExtensionSource>,
|
||||
)
|
||||
|
||||
@@ -26,11 +26,17 @@ data class ExtensionSource(
|
||||
val name: String,
|
||||
val lang: String,
|
||||
val homeUrl: String,
|
||||
val message: String?,
|
||||
val contentWarning: ContentWarning,
|
||||
)
|
||||
|
||||
enum class ContentRating {
|
||||
enum class ContentWarning {
|
||||
SAFE,
|
||||
SUGGESTIVE,
|
||||
EROTICA,
|
||||
PORNOGRAPHIC,
|
||||
MIXED,
|
||||
NSFW,
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun valueOf(contentWarning: Int) = entries.find { it.ordinal == contentWarning } ?: SAFE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ data class ExtensionStore(
|
||||
val signingKey: String,
|
||||
val contact: Contact,
|
||||
val isLegacy: Boolean,
|
||||
val extensionListUrl: String?,
|
||||
) {
|
||||
data class Contact(
|
||||
val website: String,
|
||||
|
||||
@@ -10,11 +10,12 @@ package suwayomi.tachidesk.manga.model.table
|
||||
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||
|
||||
object ExtensionStoreTable : IntIdTable() {
|
||||
val indexUrl = varchar("index_url", 2048)
|
||||
val indexUrl = varchar("index_url", 2048).uniqueIndex()
|
||||
val name = varchar("name", 256)
|
||||
val badgeLabel = varchar("badge_label", 32)
|
||||
val signingKey = varchar("signing_key", 512)
|
||||
val contactWebsite = varchar("contact_website", 2048)
|
||||
val contactDiscord = varchar("contact_discord", 2048).nullable()
|
||||
val isLegacy = bool("is_legacy").default(false)
|
||||
val extensionListUrl = varchar("extension_list_url", 2048).nullable()
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||
|
||||
object ExtensionTable : IntIdTable() {
|
||||
val apkName = varchar("apk_name", 1024).nullable()
|
||||
val storeIndexUrl = varchar("store_index_url", 2048).nullable()
|
||||
val storeIndexUrl = varchar("store_index_url", 2048).nullable().index()
|
||||
|
||||
// default is the local source icon from tachiyomi
|
||||
@Suppress("ktlint:standard:max-line-length")
|
||||
@@ -23,12 +23,12 @@ object ExtensionTable : IntIdTable() {
|
||||
|
||||
val name = varchar("name", 128)
|
||||
val pkgName = varchar("pkg_name", 128)
|
||||
val apkUrl = varchar("apk_url", 2048)
|
||||
val apkUrl = varchar("apk_url", 2048).nullable()
|
||||
val extensionLib = varchar("extension_lib", 16).nullable()
|
||||
val versionName = varchar("version_name", 16)
|
||||
val versionCode = long("version_code")
|
||||
val lang = varchar("lang", 32)
|
||||
val contentRating = integer("content_rating")
|
||||
val contentWarning = integer("content_warning")
|
||||
|
||||
val isInstalled = bool("is_installed").default(false)
|
||||
val hasUpdate = bool("has_update").default(false)
|
||||
|
||||
@@ -8,13 +8,11 @@ package suwayomi.tachidesk.manga.model.table
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import org.jetbrains.exposed.v1.core.dao.id.IdTable
|
||||
import suwayomi.tachidesk.manga.model.table.columns.unlimitedVarchar
|
||||
|
||||
object SourceTable : IdTable<Long>() {
|
||||
override val id = long("id").entityId()
|
||||
val name = varchar("name", 128)
|
||||
val lang = varchar("lang", 32)
|
||||
val extension = reference("extension", ExtensionTable)
|
||||
val message = unlimitedVarchar("message").nullable()
|
||||
val contentRating = integer("content_rating").default(0)
|
||||
val contentWarning = integer("content_warning").default(0)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package suwayomi.tachidesk.opds.impl
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import suwayomi.tachidesk.i18n.MR
|
||||
import suwayomi.tachidesk.manga.impl.Manga
|
||||
import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
import suwayomi.tachidesk.opds.constants.OpdsConstants
|
||||
@@ -17,7 +18,6 @@ import suwayomi.tachidesk.opds.repository.ChapterRepository
|
||||
import suwayomi.tachidesk.opds.repository.MangaRepository
|
||||
import suwayomi.tachidesk.opds.repository.NavigationRepository
|
||||
import suwayomi.tachidesk.opds.util.OpdsDateUtil
|
||||
import suwayomi.tachidesk.opds.util.OpdsStringUtil
|
||||
import suwayomi.tachidesk.opds.util.OpdsXmlUtil
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import java.util.Locale
|
||||
@@ -644,11 +644,20 @@ object OpdsFeedBuilder {
|
||||
skipMetadata,
|
||||
)
|
||||
|
||||
// Return a not-found feed if all available chapters are filtered out as unreachable
|
||||
if (skipMetadata && chapterEntries.isEmpty() && totalChapters > 0L) {
|
||||
return buildNotFoundFeed(
|
||||
baseUrl = baseUrl,
|
||||
locale = locale,
|
||||
idPath = "series/$mangaId/chapters",
|
||||
title = MR.strings.opds_error_chapters_not_found.localized(locale, pageNum),
|
||||
)
|
||||
}
|
||||
|
||||
// If no chapters are found in the database, attempt to fetch them from the source.
|
||||
if (chapterEntries.isEmpty() && totalChapters == 0L) {
|
||||
try {
|
||||
suwayomi.tachidesk.manga.impl.Chapter
|
||||
.fetchChapterList(mangaId)
|
||||
Manga.updateMangaAndChapters(mangaId, updateManga = false)
|
||||
|
||||
// Re-query after fetching.
|
||||
val (refetchedChapters, refetchedTotal) =
|
||||
|
||||
@@ -169,6 +169,8 @@ object ChapterRepository {
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
// Exclude unreachable chapters that are not downloaded and have no page count
|
||||
.filter { it.downloaded || it.pageCount > 0 }
|
||||
|
||||
return Pair(enrichedChapters, totalCount)
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.core.alias
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.core.greater
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.core.inSubQuery
|
||||
import org.jetbrains.exposed.v1.core.intLiteral
|
||||
import org.jetbrains.exposed.v1.core.like
|
||||
import org.jetbrains.exposed.v1.core.lowerCase
|
||||
@@ -22,7 +22,7 @@ import org.jetbrains.exposed.v1.jdbc.andWhere
|
||||
import org.jetbrains.exposed.v1.jdbc.select
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import suwayomi.tachidesk.manga.impl.MangaList.insertOrUpdate
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
||||
import suwayomi.tachidesk.manga.model.dataclass.toGenreList
|
||||
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
|
||||
import suwayomi.tachidesk.manga.model.table.CategoryTable
|
||||
@@ -75,13 +75,28 @@ fun Query.applyOpdsMangaFilter(
|
||||
}
|
||||
if (excludeField != "filter") {
|
||||
criteria.filter?.let { filterVal ->
|
||||
val unreadCountExpr = Case().When(ChapterTable.isRead eq false, intLiteral(1)).Else(intLiteral(0)).sum()
|
||||
val downloadedCountExpr = Case().When(ChapterTable.isDownloaded eq true, intLiteral(1)).Else(intLiteral(0)).sum()
|
||||
when (filterVal) {
|
||||
"unread" -> having { unreadCountExpr greater 0 }
|
||||
"downloaded" -> having { downloadedCountExpr greater 0 }
|
||||
"ongoing" -> andWhere { MangaTable.status eq MangaStatus.ONGOING.value }
|
||||
"completed" -> andWhere { MangaTable.status eq MangaStatus.COMPLETED.value }
|
||||
"unread" -> {
|
||||
andWhere {
|
||||
MangaTable.id inSubQuery
|
||||
ChapterTable.select(ChapterTable.manga).where { ChapterTable.isRead eq false }
|
||||
}
|
||||
}
|
||||
|
||||
"downloaded" -> {
|
||||
andWhere {
|
||||
MangaTable.id inSubQuery
|
||||
ChapterTable.select(ChapterTable.manga).where { ChapterTable.isDownloaded eq true }
|
||||
}
|
||||
}
|
||||
|
||||
"ongoing" -> {
|
||||
andWhere { MangaTable.status eq MangaStatus.ONGOING.value }
|
||||
}
|
||||
|
||||
"completed" -> {
|
||||
andWhere { MangaTable.status eq MangaStatus.COMPLETED.value }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,11 +148,17 @@ object MangaRepository {
|
||||
val unreadCount = unreadCountExpr.alias("unread_count")
|
||||
|
||||
// Base query with necessary joins for filtering and sorting
|
||||
val query =
|
||||
var baseJoin =
|
||||
MangaTable
|
||||
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
||||
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
|
||||
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
||||
|
||||
if (criteria.categoryId != null) {
|
||||
baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
||||
}
|
||||
|
||||
val query =
|
||||
baseJoin
|
||||
.select(MangaTable.columns + SourceTable.lang + SourceTable.name + unreadCount)
|
||||
.where { MangaTable.inLibrary eq true }
|
||||
|
||||
@@ -210,7 +231,7 @@ object MangaRepository {
|
||||
pageNum: Int,
|
||||
sort: String,
|
||||
): Pair<List<OpdsMangaAcqEntry>, Boolean> {
|
||||
val source = GetCatalogueSource.getCatalogueSourceOrStub(sourceId)
|
||||
val source = GetSource.getSourceOrStub(sourceId)
|
||||
val mangasPage: MangasPage =
|
||||
if (sort == "latest" && source.supportsLatest) {
|
||||
source.getLatestUpdates(pageNum)
|
||||
@@ -301,7 +322,6 @@ object MangaRepository {
|
||||
* Applies sorting and filtering logic to a manga library query.
|
||||
* @param query The Exposed SQL query to modify.
|
||||
* @param sort The sorting parameter.
|
||||
* @param filter The filtering parameter.
|
||||
*/
|
||||
private fun applyMangaLibrarySort(
|
||||
query: Query,
|
||||
@@ -330,36 +350,38 @@ object MangaRepository {
|
||||
*/
|
||||
fun getLibraryFilterCounts(activeFilters: OpdsMangaFilter): Map<String, Long> =
|
||||
transaction {
|
||||
val unreadCountExpr = Case().When(ChapterTable.isRead eq false, intLiteral(1)).Else(intLiteral(0)).sum()
|
||||
val downloadedCountExpr = Case().When(ChapterTable.isDownloaded eq true, intLiteral(1)).Else(intLiteral(0)).sum()
|
||||
var baseJoin =
|
||||
MangaTable
|
||||
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
||||
|
||||
if (activeFilters.categoryId != null) {
|
||||
baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
||||
}
|
||||
|
||||
val baseQuery =
|
||||
MangaTable
|
||||
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
|
||||
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
||||
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
||||
baseJoin
|
||||
.select(MangaTable.id)
|
||||
.where { MangaTable.inLibrary eq true }
|
||||
.withDistinct()
|
||||
|
||||
baseQuery.applyOpdsMangaFilter(activeFilters, excludeField = "filter")
|
||||
baseQuery.groupBy(MangaTable.id)
|
||||
|
||||
val unreadCount = baseQuery.copy().having { unreadCountExpr greater 0 }.count()
|
||||
val downloadedCount = baseQuery.copy().having { downloadedCountExpr greater 0 }.count()
|
||||
|
||||
val statusBaseQuery =
|
||||
MangaTable
|
||||
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
||||
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
||||
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
|
||||
.select(MangaTable.id)
|
||||
.where { MangaTable.inLibrary eq true }
|
||||
|
||||
statusBaseQuery.applyOpdsMangaFilter(activeFilters, excludeField = "filter")
|
||||
statusBaseQuery.groupBy(MangaTable.id)
|
||||
|
||||
val ongoingCount = statusBaseQuery.copy().andWhere { MangaTable.status eq MangaStatus.ONGOING.value }.count()
|
||||
val completedCount = statusBaseQuery.copy().andWhere { MangaTable.status eq MangaStatus.COMPLETED.value }.count()
|
||||
val unreadCount =
|
||||
baseQuery
|
||||
.copy()
|
||||
.andWhere {
|
||||
MangaTable.id inSubQuery
|
||||
ChapterTable.select(ChapterTable.manga).where { ChapterTable.isRead eq false }
|
||||
}.count()
|
||||
val downloadedCount =
|
||||
baseQuery
|
||||
.copy()
|
||||
.andWhere {
|
||||
MangaTable.id inSubQuery
|
||||
ChapterTable.select(ChapterTable.manga).where { ChapterTable.isDownloaded eq true }
|
||||
}.count()
|
||||
val ongoingCount = baseQuery.copy().andWhere { MangaTable.status eq MangaStatus.ONGOING.value }.count()
|
||||
val completedCount = baseQuery.copy().andWhere { MangaTable.status eq MangaStatus.COMPLETED.value }.count()
|
||||
|
||||
mapOf(
|
||||
"unread" to unreadCount,
|
||||
|
||||
@@ -12,7 +12,6 @@ import suwayomi.tachidesk.i18n.MR
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension
|
||||
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
|
||||
import suwayomi.tachidesk.manga.model.table.CategoryTable
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaStatus
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
@@ -166,12 +165,17 @@ object NavigationRepository {
|
||||
transaction {
|
||||
val mangaCount = MangaTable.id.countDistinct().alias("manga_count")
|
||||
|
||||
val query =
|
||||
var baseJoin =
|
||||
SourceTable
|
||||
.join(MangaTable, JoinType.INNER, SourceTable.id, MangaTable.sourceReference)
|
||||
.join(ExtensionTable, JoinType.LEFT, onColumn = SourceTable.extension, otherColumn = ExtensionTable.id)
|
||||
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
||||
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
|
||||
|
||||
if (activeFilters.categoryId != null) {
|
||||
baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
||||
}
|
||||
|
||||
val query =
|
||||
baseJoin
|
||||
.select(SourceTable.id, SourceTable.name, SourceTable.lang, ExtensionTable.pkgName, mangaCount)
|
||||
.where { MangaTable.inLibrary eq true }
|
||||
|
||||
@@ -227,7 +231,6 @@ object NavigationRepository {
|
||||
.join(CategoryMangaTable, JoinType.INNER, CategoryTable.id, CategoryMangaTable.category)
|
||||
.join(MangaTable, JoinType.INNER, CategoryMangaTable.manga, MangaTable.id)
|
||||
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
||||
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
|
||||
.select(CategoryTable.id, CategoryTable.name, mangaCount)
|
||||
.where { MangaTable.inLibrary eq true }
|
||||
|
||||
@@ -262,11 +265,15 @@ object NavigationRepository {
|
||||
activeFilters: OpdsMangaFilter = OpdsMangaFilter(),
|
||||
): Pair<List<OpdsGenreNavEntry>, Long> =
|
||||
transaction {
|
||||
val query =
|
||||
var baseJoin =
|
||||
MangaTable
|
||||
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
||||
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
||||
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
|
||||
if (activeFilters.categoryId != null) {
|
||||
baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
||||
}
|
||||
|
||||
val query =
|
||||
baseJoin
|
||||
.select(MangaTable.genre)
|
||||
.where { MangaTable.inLibrary eq true }
|
||||
|
||||
@@ -321,11 +328,16 @@ object NavigationRepository {
|
||||
val statusCounts =
|
||||
transaction {
|
||||
val countExpr = MangaTable.id.countDistinct().alias("manga_count")
|
||||
val query =
|
||||
|
||||
var baseJoin =
|
||||
MangaTable
|
||||
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
||||
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
||||
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
|
||||
if (activeFilters.categoryId != null) {
|
||||
baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
||||
}
|
||||
|
||||
val query =
|
||||
baseJoin
|
||||
.select(MangaTable.status, countExpr)
|
||||
.where { MangaTable.inLibrary eq true }
|
||||
|
||||
@@ -368,11 +380,16 @@ object NavigationRepository {
|
||||
): Pair<List<OpdsLanguageNavEntry>, Long> =
|
||||
transaction {
|
||||
val mangaCount = MangaTable.id.countDistinct().alias("manga_count")
|
||||
val query =
|
||||
|
||||
var baseJoin =
|
||||
SourceTable
|
||||
.join(MangaTable, JoinType.INNER, SourceTable.id, MangaTable.sourceReference)
|
||||
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
||||
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
|
||||
if (activeFilters.categoryId != null) {
|
||||
baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
||||
}
|
||||
|
||||
val query =
|
||||
baseJoin
|
||||
.select(SourceTable.lang, mangaCount)
|
||||
.where { MangaTable.inLibrary eq true }
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ package suwayomi.tachidesk.server.database.migration
|
||||
|
||||
import de.neonew.exposed.migrations.helpers.SQLMigration
|
||||
import suwayomi.tachidesk.graphql.types.DatabaseType
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||
import suwayomi.tachidesk.server.database.migration.helpers.MAYBE_TYPE_PREFIX
|
||||
import suwayomi.tachidesk.server.database.migration.helpers.UNLIMITED_TEXT
|
||||
import suwayomi.tachidesk.server.database.migration.helpers.toSqlName
|
||||
@@ -33,6 +34,7 @@ class M0057_AddNewExtensionApiFields : SQLMigration() {
|
||||
}
|
||||
}
|
||||
ALTER TABLE EXTENSION ALTER COLUMN store_index_url ${MAYBE_TYPE_PREFIX}VARCHAR(2048);
|
||||
CREATE INDEX extension_store_index_url ON EXTENSION (store_index_url);
|
||||
ALTER TABLE EXTENSION ALTER COLUMN version_code ${MAYBE_TYPE_PREFIX}BIGINT;
|
||||
ALTER TABLE EXTENSION ALTER COLUMN apk_name DROP NOT NULL;
|
||||
${
|
||||
@@ -42,12 +44,11 @@ class M0057_AddNewExtensionApiFields : SQLMigration() {
|
||||
}
|
||||
}
|
||||
ALTER TABLE EXTENSION ADD COLUMN apk_url VARCHAR(2048);
|
||||
ALTER TABLE EXTENSION ADD COLUMN content_rating INTEGER DEFAULT 0;
|
||||
UPDATE EXTENSION SET content_rating = 3 WHERE is_nsfw = TRUE;
|
||||
ALTER TABLE EXTENSION ADD COLUMN content_warning INTEGER DEFAULT 0;
|
||||
UPDATE EXTENSION SET content_warning = ${ContentWarning.MIXED.ordinal} WHERE is_nsfw = TRUE;
|
||||
ALTER TABLE EXTENSION DROP COLUMN is_nsfw;
|
||||
ALTER TABLE SOURCE ADD COLUMN message $UNLIMITED_TEXT;
|
||||
ALTER TABLE SOURCE ADD COLUMN content_rating INTEGER DEFAULT 0;
|
||||
UPDATE SOURCE SET content_rating = 3 WHERE is_nsfw = TRUE;
|
||||
ALTER TABLE SOURCE ADD COLUMN content_warning INTEGER DEFAULT 0;
|
||||
UPDATE SOURCE SET content_warning = ${ContentWarning.MIXED.ordinal} WHERE is_nsfw = TRUE;
|
||||
ALTER TABLE SOURCE DROP COLUMN is_nsfw;
|
||||
|
||||
|
||||
|
||||
@@ -14,13 +14,14 @@ import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||
@Suppress("ClassName", "unused")
|
||||
class M0058_AddExtensionStore : AddTableMigration() {
|
||||
private class ExtensionStoreTable : IntIdTable() {
|
||||
val indexUrl = varchar("index_url", 2048)
|
||||
val indexUrl = varchar("index_url", 2048).uniqueIndex()
|
||||
val name = varchar("name", 256)
|
||||
val badgeLabel = varchar("badge_label", 32)
|
||||
val signingKey = varchar("signing_key", 512)
|
||||
val contactWebsite = varchar("contact_website", 2048)
|
||||
val contactDiscord = varchar("contact_discord", 2048).nullable()
|
||||
val isLegacy = bool("is_legacy").default(false)
|
||||
val extensionListUrl = varchar("extension_list_url", 2048).nullable()
|
||||
}
|
||||
|
||||
override val tables: Array<Table>
|
||||
|
||||
@@ -14,7 +14,7 @@ import org.koin.core.context.stopKoin
|
||||
import suwayomi.tachidesk.manga.impl.Source
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension
|
||||
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
||||
import suwayomi.tachidesk.server.applicationSetup
|
||||
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
||||
import suwayomi.tachidesk.test.BASE_PATH
|
||||
@@ -51,7 +51,7 @@ class CloudFlareTest {
|
||||
Source
|
||||
.getSourceList()
|
||||
.firstNotNullOf { it.id.toLong().takeIf { it == 3122156392225024195L } }
|
||||
.let(GetCatalogueSource::getCatalogueSourceOrNull) as HttpSource
|
||||
.let(GetSource::getSourceOrNull) as HttpSource
|
||||
}
|
||||
setLoggingEnabled(true)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import suwayomi.tachidesk.manga.impl.extension.Extension.installExtension
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension.uninstallExtension
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension.updateExtension
|
||||
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.getExtensionList
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrNull
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
|
||||
import suwayomi.tachidesk.server.applicationSetup
|
||||
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
||||
@@ -82,7 +82,7 @@ class TestExtensionCompatibility {
|
||||
.filter {
|
||||
// filter local source
|
||||
it.id.toLong() != 0L
|
||||
}.map { getCatalogueSourceOrNull(it.id.toLong())!! as HttpSource }
|
||||
}.map { getSourceOrNull(it.id.toLong())!! as HttpSource }
|
||||
}
|
||||
setLoggingEnabled(true)
|
||||
File("$BASE_PATH/sources.txt").writeText(sources.joinToString("\n") { "${it.name} - ${it.lang.uppercase()} - ${it.id}" })
|
||||
|
||||
@@ -18,15 +18,14 @@ import org.junit.jupiter.api.AfterAll
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import rx.Observable
|
||||
import suwayomi.tachidesk.manga.impl.Search.FilterChange
|
||||
import suwayomi.tachidesk.manga.impl.Search.FilterObject
|
||||
import suwayomi.tachidesk.manga.impl.Search.SerializableGroup
|
||||
import suwayomi.tachidesk.manga.impl.Search.getFilterList
|
||||
import suwayomi.tachidesk.manga.impl.Search.setFilter
|
||||
import suwayomi.tachidesk.manga.impl.Search.sourceSearch
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.registerCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.unregisterCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.registerSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.unregisterSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.StubSource
|
||||
import suwayomi.tachidesk.test.ApplicationTest
|
||||
import suwayomi.tachidesk.test.createSMangas
|
||||
@@ -41,12 +40,11 @@ class SearchTest : ApplicationTest() {
|
||||
) : StubSource(id) {
|
||||
var mangas: List<SManga> = emptyList()
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga"))
|
||||
override fun fetchSearchManga(
|
||||
override suspend fun getSearchManga(
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: FilterList,
|
||||
): Observable<MangasPage> = Observable.just(MangasPage(mangas, false))
|
||||
): MangasPage = MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
private val sourceId = 1L
|
||||
@@ -55,7 +53,7 @@ class SearchTest : ApplicationTest() {
|
||||
|
||||
@BeforeAll
|
||||
fun setup() {
|
||||
registerCatalogueSource(sourceId to source)
|
||||
registerSource(sourceId to source)
|
||||
|
||||
this.source.mangas = createSMangas(mangasCount)
|
||||
}
|
||||
@@ -72,7 +70,7 @@ class SearchTest : ApplicationTest() {
|
||||
|
||||
@AfterAll
|
||||
fun teardown() {
|
||||
unregisterCatalogueSource(this.sourceId)
|
||||
unregisterSource(this.sourceId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,7 +347,7 @@ class FilterListTest : ApplicationTest() {
|
||||
private fun registerSource(sourceClass: KClass<*>): EmptyFilterListSource =
|
||||
synchronized(sourceClass) {
|
||||
val source = sourceClass.primaryConstructor!!.call(sourceCount) as EmptyFilterListSource
|
||||
registerCatalogueSource(sourceCount to source)
|
||||
registerSource(sourceCount to source)
|
||||
sourceCount++
|
||||
source
|
||||
}
|
||||
@@ -357,7 +355,7 @@ class FilterListTest : ApplicationTest() {
|
||||
@AfterAll
|
||||
@JvmStatic
|
||||
fun teardown() {
|
||||
(0 until sourceCount).forEach { unregisterCatalogueSource(it) }
|
||||
(0 until sourceCount).forEach { unregisterSource(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ fun createChapters(
|
||||
this[ChapterTable.sourceOrder] = it
|
||||
this[ChapterTable.isRead] = read
|
||||
this[ChapterTable.manga] = mangaId
|
||||
this[ChapterTable.memo] = "{}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user