mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-13 07:44:34 -05:00
Compare commits
18 Commits
b8ffbebc76
...
extensions
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4440ad502 | ||
|
|
e71c94547d | ||
|
|
84a1789850 | ||
|
|
70a027f6f0 | ||
|
|
650c075723 | ||
|
|
a69e29d1a5 | ||
|
|
eec0951a75 | ||
|
|
4ace76f508 | ||
|
|
cd91b6e6ef | ||
|
|
f748bc7f6d | ||
|
|
11ff4bb280 | ||
|
|
388586f23b | ||
|
|
c8f5d83e9c | ||
|
|
be5e3f022e | ||
|
|
40a21fabca | ||
|
|
b33069f107 | ||
|
|
14ab3aa9f4 | ||
|
|
bfa0038f53 |
@@ -8,7 +8,7 @@ javalin = "7.2.2"
|
|||||||
jte = "3.2.4"
|
jte = "3.2.4"
|
||||||
jackson = "3.2.0" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
|
jackson = "3.2.0" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
|
||||||
exposed = "1.2.0"
|
exposed = "1.2.0"
|
||||||
dex2jar = "2.4.36"
|
dex2jar = "2.4.37"
|
||||||
polyglot = "25.0.3"
|
polyglot = "25.0.3"
|
||||||
settings = "1.3.0"
|
settings = "1.3.0"
|
||||||
twelvemonkeys = "3.13.1"
|
twelvemonkeys = "3.13.1"
|
||||||
@@ -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-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
|
||||||
okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp" }
|
okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp" }
|
||||||
okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", version.ref = "okhttp" }
|
okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", version.ref = "okhttp" }
|
||||||
|
okhttp-zstd = { module = "com.squareup.okhttp3:okhttp-zstd", version.ref = "okhttp" }
|
||||||
okio = "com.squareup.okio:okio:3.17.0"
|
okio = "com.squareup.okio:okio:3.17.0"
|
||||||
|
|
||||||
# Javalin api
|
# Javalin api
|
||||||
@@ -228,6 +229,7 @@ okhttp = [
|
|||||||
"okhttp-logging",
|
"okhttp-logging",
|
||||||
"okhttp-dnsoverhttps",
|
"okhttp-dnsoverhttps",
|
||||||
"okhttp-brotli",
|
"okhttp-brotli",
|
||||||
|
"okhttp-zstd",
|
||||||
]
|
]
|
||||||
javalin = [
|
javalin = [
|
||||||
"javalin-core",
|
"javalin-core",
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
<!-- OPDS errors -->
|
<!-- OPDS errors -->
|
||||||
<string name="opds_error_manga_not_found">Series with ID %1$d not found.</string>
|
<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_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) -->
|
<!-- OPDS facets (Filters and Sorting) -->
|
||||||
<string name="opds_facetgroup_sort_order">Sort Order</string>
|
<string name="opds_facetgroup_sort_order">Sort Order</string>
|
||||||
|
|||||||
125
server/i18n/src/commonMain/moko-resources/values/el/strings.xml
Normal file
125
server/i18n/src/commonMain/moko-resources/values/el/strings.xml
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<?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>
|
||||||
|
</resources>
|
||||||
@@ -284,8 +284,7 @@ class ServerConfig(
|
|||||||
defaultValue = emptyList(),
|
defaultValue = emptyList(),
|
||||||
deprecated =
|
deprecated =
|
||||||
SettingsRegistry.SettingDeprecated(
|
SettingsRegistry.SettingDeprecated(
|
||||||
replaceWith = "extensionStores",
|
message = "Replaced with addExtensionStore and removeExtensionStore mutations",
|
||||||
message = "Replaced with extensionStores",
|
|
||||||
migrateConfigValue = {
|
migrateConfigValue = {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
(it.unwrapped() as? List<String>)
|
(it.unwrapped() as? List<String>)
|
||||||
@@ -304,7 +303,7 @@ class ServerConfig(
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
readMigrated = { extensionStores.value },
|
readMigrated = { extensionStores.value },
|
||||||
setMigrated = { extensionStores.value = (extensionStores.value + it).distinct() },
|
setMigrated = { extensionStores.value = it.distinct() },
|
||||||
typeInfo =
|
typeInfo =
|
||||||
SettingsRegistry.PartialTypeInfo(
|
SettingsRegistry.PartialTypeInfo(
|
||||||
specificType = "List<String>",
|
specificType = "List<String>",
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ package eu.kanade.tachiyomi.network
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
|
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.UncaughtExceptionInterceptor
|
||||||
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
|
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
@@ -22,7 +21,6 @@ import kotlinx.coroutines.flow.launchIn
|
|||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import okhttp3.Cache
|
import okhttp3.Cache
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.brotli.BrotliInterceptor
|
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||||
import java.net.CookieHandler
|
import java.net.CookieHandler
|
||||||
@@ -84,8 +82,6 @@ class NetworkHelper(
|
|||||||
),
|
),
|
||||||
).addInterceptor(UncaughtExceptionInterceptor())
|
).addInterceptor(UncaughtExceptionInterceptor())
|
||||||
.addInterceptor(UserAgentInterceptor(::defaultUserAgentProvider))
|
.addInterceptor(UserAgentInterceptor(::defaultUserAgentProvider))
|
||||||
.addNetworkInterceptor(IgnoreGzipInterceptor())
|
|
||||||
.addNetworkInterceptor(BrotliInterceptor)
|
|
||||||
|
|
||||||
// if (preferences.verboseLogging().get()) {
|
// if (preferences.verboseLogging().get()) {
|
||||||
val httpLoggingInterceptor =
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -483,7 +483,7 @@ class LocalSource(
|
|||||||
it[versionCode] = 0
|
it[versionCode] = 0
|
||||||
it[lang] = LANG
|
it[lang] = LANG
|
||||||
it[extensionLib] = "1.2"
|
it[extensionLib] = "1.2"
|
||||||
it[contentRating] = 0
|
it[contentWarning] = 0
|
||||||
it[isInstalled] = true
|
it[isInstalled] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ import android.app.Application
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.PUT
|
import eu.kanade.tachiyomi.network.PUT
|
||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import io.javalin.http.HttpStatus
|
import io.javalin.http.HttpStatus
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.SerializationException
|
import kotlinx.serialization.SerializationException
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
@@ -36,31 +40,66 @@ object SyncYomiSyncService {
|
|||||||
message: String?,
|
message: String?,
|
||||||
) : Exception(message)
|
) : 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(
|
suspend fun doSync(
|
||||||
syncData: SyncData,
|
syncData: SyncData,
|
||||||
startDate: Instant,
|
startDate: Instant,
|
||||||
setSyncState: (SyncManager.SyncState) -> Unit,
|
setSyncState: (SyncManager.SyncState) -> Unit,
|
||||||
): Backup? {
|
): Backup? {
|
||||||
|
reportSyncEvent(SyncEventStatus.SYNC_STARTED)
|
||||||
setSyncState(SyncManager.SyncState.Downloading(startDate))
|
setSyncState(SyncManager.SyncState.Downloading(startDate))
|
||||||
val (remoteData, etag) = pullSyncData()
|
|
||||||
|
|
||||||
val finalSyncData =
|
return try {
|
||||||
if (remoteData != null) {
|
val (remoteData, etag) = pullSyncData()
|
||||||
require(etag.isNotEmpty()) { "ETag should never be empty if remote data is not null" }
|
|
||||||
logger.debug { "Try update remote data with ETag($etag)" }
|
val finalSyncData =
|
||||||
setSyncState(SyncManager.SyncState.Merging(startDate))
|
if (remoteData != null) {
|
||||||
mergeSyncData(syncData, remoteData)
|
require(etag.isNotEmpty()) { "ETag should never be empty if remote data is not null" }
|
||||||
} else {
|
logger.debug { "Try update remote data with ETag($etag)" }
|
||||||
// init or overwrite remote data
|
setSyncState(SyncManager.SyncState.Merging(startDate))
|
||||||
logger.debug { "Try overwrite remote data with ETag($etag)" }
|
mergeSyncData(syncData, remoteData)
|
||||||
syncData
|
} 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) {
|
val success = pushSyncData(finalSyncData, etag)
|
||||||
setSyncState(SyncManager.SyncState.Uploading(startDate))
|
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> {
|
private suspend fun pullSyncData(): Pair<SyncData?, String> {
|
||||||
@@ -122,8 +161,8 @@ object SyncYomiSyncService {
|
|||||||
private suspend fun pushSyncData(
|
private suspend fun pushSyncData(
|
||||||
syncData: SyncData,
|
syncData: SyncData,
|
||||||
eTag: String,
|
eTag: String,
|
||||||
) {
|
): Boolean {
|
||||||
val backup = syncData.backup ?: return
|
val backup = syncData.backup ?: return true
|
||||||
|
|
||||||
val host = serverConfig.syncYomiHost.value
|
val host = serverConfig.syncYomiHost.value
|
||||||
val apiKey = serverConfig.syncYomiApiKey.value
|
val apiKey = serverConfig.syncYomiApiKey.value
|
||||||
@@ -160,7 +199,7 @@ object SyncYomiSyncService {
|
|||||||
|
|
||||||
val response = client.newCall(uploadRequest).await()
|
val response = client.newCall(uploadRequest).await()
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
return if (response.isSuccessful) {
|
||||||
val newETag =
|
val newETag =
|
||||||
response.headers["ETag"]
|
response.headers["ETag"]
|
||||||
?.takeIf { it.isNotEmpty() } ?: throw SyncYomiException("Missing ETag")
|
?.takeIf { it.isNotEmpty() } ?: throw SyncYomiException("Missing ETag")
|
||||||
@@ -169,12 +208,53 @@ object SyncYomiSyncService {
|
|||||||
.putString("last_sync_etag", newETag)
|
.putString("last_sync_etag", newETag)
|
||||||
.apply()
|
.apply()
|
||||||
logger.debug { "SyncYomi sync completed" }
|
logger.debug { "SyncYomi sync completed" }
|
||||||
|
true
|
||||||
} else if (response.code == HttpStatus.PRECONDITION_FAILED.code) {
|
} else if (response.code == HttpStatus.PRECONDITION_FAILED.code) {
|
||||||
// other clients updated remote data, will try next time
|
// other clients updated remote data, will try next time
|
||||||
logger.debug { "SyncYomi sync failed with 412" }
|
logger.debug { "SyncYomi sync failed with 412" }
|
||||||
|
false
|
||||||
} else {
|
} else {
|
||||||
val responseBody = response.body.string()
|
val responseBody = response.body.string()
|
||||||
logger.error { "SyncError: $responseBody" }
|
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}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,26 +21,6 @@ class ExtensionStoreDataLoader : KotlinDataLoader<String, ExtensionStoreType> {
|
|||||||
|
|
||||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, ExtensionStoreType> =
|
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, ExtensionStoreType> =
|
||||||
DataLoaderFactory.newDataLoader { ids ->
|
DataLoaderFactory.newDataLoader { ids ->
|
||||||
future {
|
|
||||||
transaction {
|
|
||||||
addLogger(Slf4jSqlDebugLogger)
|
|
||||||
val manga =
|
|
||||||
ExtensionStoreTable
|
|
||||||
.selectAll()
|
|
||||||
.where { ExtensionStoreTable.indexUrl inList ids }
|
|
||||||
.map { ExtensionStoreType(it) }
|
|
||||||
.associateBy { it.indexUrl }
|
|
||||||
ids.map { manga[it] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ExtensionStoreForExtension : KotlinDataLoader<String, ExtensionStoreType> {
|
|
||||||
override val dataLoaderName = "ExtensionStoreForExtension"
|
|
||||||
|
|
||||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, ExtensionStoreType> =
|
|
||||||
DataLoaderFactory.newDataLoader<String, ExtensionStoreType> { ids ->
|
|
||||||
future {
|
future {
|
||||||
transaction {
|
transaction {
|
||||||
addLogger(Slf4jSqlDebugLogger)
|
addLogger(Slf4jSqlDebugLogger)
|
||||||
@@ -50,14 +30,14 @@ class ExtensionStoreForExtension : KotlinDataLoader<String, ExtensionStoreType>
|
|||||||
.where { ExtensionStoreTable.indexUrl inList ids }
|
.where { ExtensionStoreTable.indexUrl inList ids }
|
||||||
.map { ExtensionStoreType(it) }
|
.map { ExtensionStoreType(it) }
|
||||||
.associateBy { it.indexUrl }
|
.associateBy { it.indexUrl }
|
||||||
ids.map { (extensionStoreByIndexUrl[it]) }
|
ids.map { extensionStoreByIndexUrl[it] }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExtensionForExtensionStore : KotlinDataLoader<String, ExtensionNodeList> {
|
class ExtensionsForExtensionStore : KotlinDataLoader<String, ExtensionNodeList> {
|
||||||
override val dataLoaderName = "ExtensionForExtensionStore"
|
override val dataLoaderName = "ExtensionsForExtensionStore"
|
||||||
|
|
||||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, ExtensionNodeList> =
|
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, ExtensionNodeList> =
|
||||||
DataLoaderFactory.newDataLoader<String, ExtensionNodeList> { ids ->
|
DataLoaderFactory.newDataLoader<String, ExtensionNodeList> { ids ->
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ class ExtensionStoreMutation {
|
|||||||
contactDiscord = it.contactDiscord,
|
contactDiscord = it.contactDiscord,
|
||||||
indexUrl = it.indexUrl,
|
indexUrl = it.indexUrl,
|
||||||
isLegacy = it.isLegacy,
|
isLegacy = it.isLegacy,
|
||||||
|
extensionListUrl = it.extensionListUrl,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import org.jetbrains.exposed.v1.jdbc.selectAll
|
|||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
|
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.ContentRatingFilter
|
import suwayomi.tachidesk.graphql.queries.filter.ContentWarningFilter
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.Filter
|
import suwayomi.tachidesk.graphql.queries.filter.Filter
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.HasGetOp
|
import suwayomi.tachidesk.graphql.queries.filter.HasGetOp
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.IntFilter
|
import suwayomi.tachidesk.graphql.queries.filter.IntFilter
|
||||||
@@ -43,7 +43,7 @@ import suwayomi.tachidesk.graphql.server.primitives.lessNotUnique
|
|||||||
import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
|
import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
|
||||||
import suwayomi.tachidesk.graphql.types.ExtensionNodeList
|
import suwayomi.tachidesk.graphql.types.ExtensionNodeList
|
||||||
import suwayomi.tachidesk.graphql.types.ExtensionType
|
import suwayomi.tachidesk.graphql.types.ExtensionType
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
@@ -108,9 +108,9 @@ class ExtensionQuery {
|
|||||||
val versionCode: Int? = null,
|
val versionCode: Int? = null,
|
||||||
val versionCodeLong: Long? = null,
|
val versionCodeLong: Long? = null,
|
||||||
val lang: String? = null,
|
val lang: String? = null,
|
||||||
@GraphQLDeprecated("", ReplaceWith("contentRating"))
|
@GraphQLDeprecated("", ReplaceWith("contentWarning"))
|
||||||
val isNsfw: Boolean? = null,
|
val isNsfw: Boolean? = null,
|
||||||
val contentRating: ContentRating? = null,
|
val contentWarning: ContentWarning? = null,
|
||||||
val isInstalled: Boolean? = null,
|
val isInstalled: Boolean? = null,
|
||||||
val hasUpdate: Boolean? = null,
|
val hasUpdate: Boolean? = null,
|
||||||
val isObsolete: Boolean? = null,
|
val isObsolete: Boolean? = null,
|
||||||
@@ -129,10 +129,10 @@ class ExtensionQuery {
|
|||||||
opAnd.eq(versionCodeLong, ExtensionTable.versionCode)
|
opAnd.eq(versionCodeLong, ExtensionTable.versionCode)
|
||||||
opAnd.eq(lang, ExtensionTable.lang)
|
opAnd.eq(lang, ExtensionTable.lang)
|
||||||
opAnd.eq(
|
opAnd.eq(
|
||||||
isNsfw?.let { if (it) ContentRating.PORNOGRAPHIC.ordinal else ContentRating.SAFE.ordinal },
|
isNsfw?.let { if (it) ContentWarning.MIXED.ordinal else ContentWarning.SAFE.ordinal },
|
||||||
ExtensionTable.contentRating,
|
ExtensionTable.contentWarning,
|
||||||
)
|
)
|
||||||
opAnd.eq(contentRating?.ordinal, ExtensionTable.contentRating)
|
opAnd.eq(contentWarning?.ordinal, ExtensionTable.contentWarning)
|
||||||
opAnd.eq(isInstalled, ExtensionTable.isInstalled)
|
opAnd.eq(isInstalled, ExtensionTable.isInstalled)
|
||||||
opAnd.eq(hasUpdate, ExtensionTable.hasUpdate)
|
opAnd.eq(hasUpdate, ExtensionTable.hasUpdate)
|
||||||
opAnd.eq(isObsolete, ExtensionTable.isObsolete)
|
opAnd.eq(isObsolete, ExtensionTable.isObsolete)
|
||||||
@@ -156,9 +156,9 @@ class ExtensionQuery {
|
|||||||
val versionCode: IntFilter? = null,
|
val versionCode: IntFilter? = null,
|
||||||
val versionCodeLong: LongFilter? = null,
|
val versionCodeLong: LongFilter? = null,
|
||||||
val lang: StringFilter? = null,
|
val lang: StringFilter? = null,
|
||||||
@GraphQLDeprecated("", ReplaceWith("storeIndexUrl"))
|
@GraphQLDeprecated("", ReplaceWith("contentWarning"))
|
||||||
val isNsfw: BooleanFilter? = null,
|
val isNsfw: BooleanFilter? = null,
|
||||||
val contentRating: ContentRatingFilter? = null,
|
val contentWarning: ContentWarningFilter? = null,
|
||||||
val isInstalled: BooleanFilter? = null,
|
val isInstalled: BooleanFilter? = null,
|
||||||
val hasUpdate: BooleanFilter? = null,
|
val hasUpdate: BooleanFilter? = null,
|
||||||
val isObsolete: BooleanFilter? = null,
|
val isObsolete: BooleanFilter? = null,
|
||||||
@@ -179,7 +179,7 @@ class ExtensionQuery {
|
|||||||
andFilterWithCompareString(ExtensionTable.versionName, versionName),
|
andFilterWithCompareString(ExtensionTable.versionName, versionName),
|
||||||
andFilterWithCompare(ExtensionTable.versionCode, versionCodeLong),
|
andFilterWithCompare(ExtensionTable.versionCode, versionCodeLong),
|
||||||
andFilterWithCompareString(ExtensionTable.lang, lang),
|
andFilterWithCompareString(ExtensionTable.lang, lang),
|
||||||
andFilterWithCompareEnum(ExtensionTable.contentRating, contentRating),
|
andFilterWithCompareEnum(ExtensionTable.contentWarning, contentWarning),
|
||||||
andFilterWithCompare(ExtensionTable.isInstalled, isInstalled),
|
andFilterWithCompare(ExtensionTable.isInstalled, isInstalled),
|
||||||
andFilterWithCompare(ExtensionTable.hasUpdate, hasUpdate),
|
andFilterWithCompare(ExtensionTable.hasUpdate, hasUpdate),
|
||||||
andFilterWithCompare(ExtensionTable.isObsolete, isObsolete),
|
andFilterWithCompare(ExtensionTable.isObsolete, isObsolete),
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ import org.jetbrains.exposed.v1.core.Op
|
|||||||
import org.jetbrains.exposed.v1.core.SortOrder
|
import org.jetbrains.exposed.v1.core.SortOrder
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
import org.jetbrains.exposed.v1.core.greater
|
import org.jetbrains.exposed.v1.core.greater
|
||||||
|
import org.jetbrains.exposed.v1.core.greaterEq
|
||||||
import org.jetbrains.exposed.v1.core.less
|
import org.jetbrains.exposed.v1.core.less
|
||||||
import org.jetbrains.exposed.v1.core.neq
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
|
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.ContentRatingFilter
|
import suwayomi.tachidesk.graphql.queries.filter.ContentWarningFilter
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.Filter
|
import suwayomi.tachidesk.graphql.queries.filter.Filter
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.HasGetOp
|
import suwayomi.tachidesk.graphql.queries.filter.HasGetOp
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.LongFilter
|
import suwayomi.tachidesk.graphql.queries.filter.LongFilter
|
||||||
@@ -42,7 +42,7 @@ import suwayomi.tachidesk.graphql.server.primitives.lessNotUnique
|
|||||||
import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
|
import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
|
||||||
import suwayomi.tachidesk.graphql.types.SourceNodeList
|
import suwayomi.tachidesk.graphql.types.SourceNodeList
|
||||||
import suwayomi.tachidesk.graphql.types.SourceType
|
import suwayomi.tachidesk.graphql.types.SourceType
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
@@ -95,9 +95,9 @@ class SourceQuery {
|
|||||||
val id: Long? = null,
|
val id: Long? = null,
|
||||||
val name: String? = null,
|
val name: String? = null,
|
||||||
val lang: String? = null,
|
val lang: String? = null,
|
||||||
@GraphQLDeprecated("replace with contentRating == ContentRating.PORNOGRAPHIC", ReplaceWith("contentRating"))
|
@GraphQLDeprecated("replace with contentWarning == ContentRating.MIXED", ReplaceWith("contentWarning"))
|
||||||
val isNsfw: Boolean? = null,
|
val isNsfw: Boolean? = null,
|
||||||
val contentRating: ContentRating? = null,
|
val contentWarning: ContentWarning? = null,
|
||||||
) : HasGetOp {
|
) : HasGetOp {
|
||||||
override fun getOp(): Op<Boolean>? {
|
override fun getOp(): Op<Boolean>? {
|
||||||
val opAnd = OpAnd()
|
val opAnd = OpAnd()
|
||||||
@@ -106,13 +106,12 @@ class SourceQuery {
|
|||||||
opAnd.eq(lang, SourceTable.lang)
|
opAnd.eq(lang, SourceTable.lang)
|
||||||
opAnd.andWhere(isNsfw) {
|
opAnd.andWhere(isNsfw) {
|
||||||
if (it) {
|
if (it) {
|
||||||
SourceTable.contentRating eq ContentRating.PORNOGRAPHIC.ordinal
|
SourceTable.contentWarning greaterEq ContentWarning.MIXED.ordinal
|
||||||
} else {
|
} else {
|
||||||
SourceTable.contentRating neq
|
SourceTable.contentWarning less ContentWarning.MIXED.ordinal
|
||||||
ContentRating.PORNOGRAPHIC.ordinal
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
opAnd.andWhere(contentRating) { SourceTable.contentRating eq it.ordinal }
|
opAnd.andWhere(contentWarning) { SourceTable.contentWarning eq it.ordinal }
|
||||||
|
|
||||||
return opAnd.op
|
return opAnd.op
|
||||||
}
|
}
|
||||||
@@ -122,9 +121,9 @@ class SourceQuery {
|
|||||||
val id: LongFilter? = null,
|
val id: LongFilter? = null,
|
||||||
val name: StringFilter? = null,
|
val name: StringFilter? = null,
|
||||||
val lang: StringFilter? = null,
|
val lang: StringFilter? = null,
|
||||||
@GraphQLDeprecated("replace with contentRating == ContentRating.PORNOGRAPHIC", ReplaceWith("contentRating"))
|
@GraphQLDeprecated("replace with contentWarning", ReplaceWith("contentWarning"))
|
||||||
val isNsfw: BooleanFilter? = null,
|
val isNsfw: BooleanFilter? = null,
|
||||||
val contentRating: ContentRatingFilter? = null,
|
val contentWarning: ContentWarningFilter? = null,
|
||||||
override val and: List<SourceFilter>? = null,
|
override val and: List<SourceFilter>? = null,
|
||||||
override val or: List<SourceFilter>? = null,
|
override val or: List<SourceFilter>? = null,
|
||||||
override val not: SourceFilter? = null,
|
override val not: SourceFilter? = null,
|
||||||
@@ -134,7 +133,7 @@ class SourceQuery {
|
|||||||
andFilterWithCompareEntity(SourceTable.id, id),
|
andFilterWithCompareEntity(SourceTable.id, id),
|
||||||
andFilterWithCompareString(SourceTable.name, name),
|
andFilterWithCompareString(SourceTable.name, name),
|
||||||
andFilterWithCompareString(SourceTable.lang, lang),
|
andFilterWithCompareString(SourceTable.lang, lang),
|
||||||
andFilterWithCompareEnum(SourceTable.contentRating, contentRating),
|
andFilterWithCompareEnum(SourceTable.contentWarning, contentWarning),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import org.jetbrains.exposed.v1.core.upperCase
|
|||||||
import org.jetbrains.exposed.v1.core.wrap
|
import org.jetbrains.exposed.v1.core.wrap
|
||||||
import org.jetbrains.exposed.v1.jdbc.Query
|
import org.jetbrains.exposed.v1.jdbc.Query
|
||||||
import org.jetbrains.exposed.v1.jdbc.andWhere
|
import org.jetbrains.exposed.v1.jdbc.andWhere
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||||
|
|
||||||
class ILikeEscapeOp(
|
class ILikeEscapeOp(
|
||||||
expr1: Expression<*>,
|
expr1: Expression<*>,
|
||||||
@@ -330,23 +330,23 @@ data class DoubleFilter(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ContentRatingFilter(
|
data class ContentWarningFilter(
|
||||||
override val isNull: Boolean? = null,
|
override val isNull: Boolean? = null,
|
||||||
override val equalTo: ContentRating? = null,
|
override val equalTo: ContentWarning? = null,
|
||||||
override val notEqualTo: ContentRating? = null,
|
override val notEqualTo: ContentWarning? = null,
|
||||||
override val notEqualToAll: List<ContentRating>? = null,
|
override val notEqualToAll: List<ContentWarning>? = null,
|
||||||
override val notEqualToAny: List<ContentRating>? = null,
|
override val notEqualToAny: List<ContentWarning>? = null,
|
||||||
override val distinctFrom: ContentRating? = null,
|
override val distinctFrom: ContentWarning? = null,
|
||||||
override val distinctFromAll: List<ContentRating>? = null,
|
override val distinctFromAll: List<ContentWarning>? = null,
|
||||||
override val distinctFromAny: List<ContentRating>? = null,
|
override val distinctFromAny: List<ContentWarning>? = null,
|
||||||
override val notDistinctFrom: ContentRating? = null,
|
override val notDistinctFrom: ContentWarning? = null,
|
||||||
override val `in`: List<ContentRating>? = null,
|
override val `in`: List<ContentWarning>? = null,
|
||||||
override val notIn: List<ContentRating>? = null,
|
override val notIn: List<ContentWarning>? = null,
|
||||||
override val lessThan: ContentRating? = null,
|
override val lessThan: ContentWarning? = null,
|
||||||
override val lessThanOrEqualTo: ContentRating? = null,
|
override val lessThanOrEqualTo: ContentWarning? = null,
|
||||||
override val greaterThan: ContentRating? = null,
|
override val greaterThan: ContentWarning? = null,
|
||||||
override val greaterThanOrEqualTo: ContentRating? = null,
|
override val greaterThanOrEqualTo: ContentWarning? = null,
|
||||||
) : ComparableScalarFilter<ContentRating>
|
) : ComparableScalarFilter<ContentWarning>
|
||||||
|
|
||||||
data class StringFilter(
|
data class StringFilter(
|
||||||
override val isNull: Boolean? = null,
|
override val isNull: Boolean? = null,
|
||||||
|
|||||||
@@ -20,10 +20,9 @@ import suwayomi.tachidesk.graphql.dataLoaders.DisplayScoreForTrackRecordDataLoad
|
|||||||
import suwayomi.tachidesk.graphql.dataLoaders.DisplayScoreForTrackSearchDataLoader
|
import suwayomi.tachidesk.graphql.dataLoaders.DisplayScoreForTrackSearchDataLoader
|
||||||
import suwayomi.tachidesk.graphql.dataLoaders.DownloadedChapterCountForMangaDataLoader
|
import suwayomi.tachidesk.graphql.dataLoaders.DownloadedChapterCountForMangaDataLoader
|
||||||
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionDataLoader
|
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionDataLoader
|
||||||
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionForExtensionStore
|
|
||||||
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionForSourceDataLoader
|
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionForSourceDataLoader
|
||||||
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionStoreDataLoader
|
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionStoreDataLoader
|
||||||
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionStoreForExtension
|
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionsForExtensionStore
|
||||||
import suwayomi.tachidesk.graphql.dataLoaders.FirstUnreadChapterForMangaDataLoader
|
import suwayomi.tachidesk.graphql.dataLoaders.FirstUnreadChapterForMangaDataLoader
|
||||||
import suwayomi.tachidesk.graphql.dataLoaders.GlobalMetaDataLoader
|
import suwayomi.tachidesk.graphql.dataLoaders.GlobalMetaDataLoader
|
||||||
import suwayomi.tachidesk.graphql.dataLoaders.HasDuplicateChaptersForMangaDataLoader
|
import suwayomi.tachidesk.graphql.dataLoaders.HasDuplicateChaptersForMangaDataLoader
|
||||||
@@ -81,9 +80,8 @@ class TachideskDataLoaderRegistryFactory {
|
|||||||
SourceMetaDataLoader(),
|
SourceMetaDataLoader(),
|
||||||
ExtensionDataLoader(),
|
ExtensionDataLoader(),
|
||||||
ExtensionForSourceDataLoader(),
|
ExtensionForSourceDataLoader(),
|
||||||
ExtensionForExtensionStore(),
|
ExtensionsForExtensionStore(),
|
||||||
ExtensionStoreDataLoader(),
|
ExtensionStoreDataLoader(),
|
||||||
ExtensionStoreForExtension(),
|
|
||||||
TrackerDataLoader(),
|
TrackerDataLoader(),
|
||||||
TrackerStatusesDataLoader(),
|
TrackerStatusesDataLoader(),
|
||||||
TrackerScoresDataLoader(),
|
TrackerScoresDataLoader(),
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class ExtensionStoreType(
|
|||||||
val contactDiscord: String?,
|
val contactDiscord: String?,
|
||||||
val indexUrl: String,
|
val indexUrl: String,
|
||||||
val isLegacy: Boolean,
|
val isLegacy: Boolean,
|
||||||
|
val extensionListUrl: String?,
|
||||||
) : Node {
|
) : Node {
|
||||||
constructor(row: ResultRow) : this(
|
constructor(row: ResultRow) : this(
|
||||||
name = row[ExtensionStoreTable.name],
|
name = row[ExtensionStoreTable.name],
|
||||||
@@ -35,10 +36,11 @@ class ExtensionStoreType(
|
|||||||
contactDiscord = row[ExtensionStoreTable.contactDiscord],
|
contactDiscord = row[ExtensionStoreTable.contactDiscord],
|
||||||
indexUrl = row[ExtensionStoreTable.indexUrl],
|
indexUrl = row[ExtensionStoreTable.indexUrl],
|
||||||
isLegacy = row[ExtensionStoreTable.isLegacy],
|
isLegacy = row[ExtensionStoreTable.isLegacy],
|
||||||
|
extensionListUrl = row[ExtensionStoreTable.extensionListUrl],
|
||||||
)
|
)
|
||||||
|
|
||||||
fun extensions(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<ExtensionNodeList> =
|
fun extensions(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<ExtensionNodeList> =
|
||||||
dataFetchingEnvironment.getValueFromDataLoader<String, ExtensionNodeList>("ExtensionForExtensionStore", indexUrl)
|
dataFetchingEnvironment.getValueFromDataLoader<String, ExtensionNodeList>("ExtensionsForExtensionStore", indexUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ExtensionStoreNodeList(
|
data class ExtensionStoreNodeList(
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import suwayomi.tachidesk.graphql.server.primitives.Node
|
|||||||
import suwayomi.tachidesk.graphql.server.primitives.NodeList
|
import suwayomi.tachidesk.graphql.server.primitives.NodeList
|
||||||
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
|
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
|
||||||
import suwayomi.tachidesk.manga.impl.extension.Extension
|
import suwayomi.tachidesk.manga.impl.extension.Extension
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
@@ -41,9 +41,9 @@ class ExtensionType(
|
|||||||
val versionCode: Int,
|
val versionCode: Int,
|
||||||
val versionCodeLong: Long,
|
val versionCodeLong: Long,
|
||||||
val lang: String,
|
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 isNsfw: Boolean,
|
||||||
val contentRating: ContentRating,
|
val contentWarning: ContentWarning,
|
||||||
val isInstalled: Boolean,
|
val isInstalled: Boolean,
|
||||||
val hasUpdate: Boolean,
|
val hasUpdate: Boolean,
|
||||||
val isObsolete: Boolean,
|
val isObsolete: Boolean,
|
||||||
@@ -61,8 +61,8 @@ class ExtensionType(
|
|||||||
versionCode = row[ExtensionTable.versionCode].toInt(),
|
versionCode = row[ExtensionTable.versionCode].toInt(),
|
||||||
versionCodeLong = row[ExtensionTable.versionCode],
|
versionCodeLong = row[ExtensionTable.versionCode],
|
||||||
lang = row[ExtensionTable.lang],
|
lang = row[ExtensionTable.lang],
|
||||||
isNsfw = row[ExtensionTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal,
|
isNsfw = row[ExtensionTable.contentWarning] >= ContentWarning.MIXED.ordinal,
|
||||||
contentRating = ContentRating.valueOf(row[ExtensionTable.contentRating]),
|
contentWarning = ContentWarning.valueOf(row[ExtensionTable.contentWarning]),
|
||||||
isInstalled = row[ExtensionTable.isInstalled],
|
isInstalled = row[ExtensionTable.isInstalled],
|
||||||
hasUpdate = row[ExtensionTable.hasUpdate],
|
hasUpdate = row[ExtensionTable.hasUpdate],
|
||||||
isObsolete = row[ExtensionTable.isObsolete],
|
isObsolete = row[ExtensionTable.isObsolete],
|
||||||
@@ -72,7 +72,7 @@ class ExtensionType(
|
|||||||
dataFetchingEnvironment.getValueFromDataLoader<String, SourceNodeList>("SourcesForExtensionDataLoader", pkgName)
|
dataFetchingEnvironment.getValueFromDataLoader<String, SourceNodeList>("SourcesForExtensionDataLoader", pkgName)
|
||||||
|
|
||||||
fun extensionStore(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<ExtensionStoreType> =
|
fun extensionStore(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<ExtensionStoreType> =
|
||||||
dataFetchingEnvironment.getValueFromDataLoader<String, ExtensionStoreType>("ExtensionStoreForExtension", storeIndexUrl.orEmpty())
|
dataFetchingEnvironment.getValueFromDataLoader<String, ExtensionStoreType>("ExtensionStoreDataLoader", storeIndexUrl.orEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ExtensionNodeList(
|
data class ExtensionNodeList(
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import suwayomi.tachidesk.manga.impl.Source.getSourcePreferencesRaw
|
|||||||
import suwayomi.tachidesk.manga.impl.extension.Extension
|
import suwayomi.tachidesk.manga.impl.extension.Extension
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
@@ -42,12 +42,11 @@ class SourceType(
|
|||||||
val id: Long,
|
val id: Long,
|
||||||
val name: String,
|
val name: String,
|
||||||
val lang: String,
|
val lang: String,
|
||||||
val message: String?,
|
val contentWarning: ContentWarning,
|
||||||
val contentRating: ContentRating,
|
|
||||||
val iconUrl: String,
|
val iconUrl: String,
|
||||||
val supportsLatest: Boolean,
|
val supportsLatest: Boolean,
|
||||||
val isConfigurable: Boolean,
|
val isConfigurable: Boolean,
|
||||||
@GraphQLDeprecated("", ReplaceWith("contentRating"))
|
@GraphQLDeprecated("", ReplaceWith("contentWarning"))
|
||||||
val isNsfw: Boolean,
|
val isNsfw: Boolean,
|
||||||
val displayName: String,
|
val displayName: String,
|
||||||
val homeUrl: String?,
|
val homeUrl: String?,
|
||||||
@@ -58,12 +57,11 @@ class SourceType(
|
|||||||
id = row[SourceTable.id].value,
|
id = row[SourceTable.id].value,
|
||||||
name = row[SourceTable.name],
|
name = row[SourceTable.name],
|
||||||
lang = row[SourceTable.lang],
|
lang = row[SourceTable.lang],
|
||||||
message = row[SourceTable.message],
|
contentWarning = ContentWarning.valueOf(row[SourceTable.contentWarning]),
|
||||||
contentRating = ContentRating.valueOf(row[SourceTable.contentRating]),
|
|
||||||
iconUrl = Extension.proxyExtensionIconUrl(sourceExtension[ExtensionTable.pkgName]),
|
iconUrl = Extension.proxyExtensionIconUrl(sourceExtension[ExtensionTable.pkgName]),
|
||||||
supportsLatest = catalogueSource.supportsLatest,
|
supportsLatest = catalogueSource.supportsLatest,
|
||||||
isConfigurable = catalogueSource is ConfigurableSource,
|
isConfigurable = catalogueSource is ConfigurableSource,
|
||||||
isNsfw = row[SourceTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal,
|
isNsfw = row[SourceTable.contentWarning] >= ContentWarning.MIXED.ordinal,
|
||||||
displayName = catalogueSource.toString(),
|
displayName = catalogueSource.toString(),
|
||||||
homeUrl = runCatching { (catalogueSource as? HttpSource)?.getHomeUrl() }.getOrNull(),
|
homeUrl = runCatching { (catalogueSource as? HttpSource)?.getHomeUrl() }.getOrNull(),
|
||||||
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
|
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import suwayomi.tachidesk.manga.model.table.ChapterTable
|
|||||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||||
import suwayomi.tachidesk.manga.model.table.toDataClass
|
import suwayomi.tachidesk.manga.model.table.toDataClass
|
||||||
import suwayomi.tachidesk.server.serverConfig
|
import suwayomi.tachidesk.server.serverConfig
|
||||||
|
import xyz.nulldev.androidcompat.util.SafePath
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
@@ -76,13 +77,46 @@ object ChapterDownloadHelper {
|
|||||||
.select(ChapterTable.columns + MangaTable.columns)
|
.select(ChapterTable.columns + MangaTable.columns)
|
||||||
.where { ChapterTable.id eq chapterId }
|
.where { ChapterTable.id eq chapterId }
|
||||||
.firstOrNull() ?: throw IllegalArgumentException("ChapterId $chapterId not found")
|
.firstOrNull() ?: throw IllegalArgumentException("ChapterId $chapterId not found")
|
||||||
|
|
||||||
val chapter = ChapterTable.toDataClass(row)
|
val chapter = ChapterTable.toDataClass(row)
|
||||||
val mangaTitle = row[MangaTable.title]
|
val mangaTitle = row[MangaTable.title].trim()
|
||||||
|
|
||||||
val scanlatorPart = chapter.scanlator?.let { "[$it] " } ?: ""
|
val scanlatorName = chapter.scanlator?.trim()?.takeIf { it.isNotEmpty() }
|
||||||
val fileName = "$mangaTitle - $scanlatorPart${chapter.name}.cbz"
|
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(
|
fun getCbzForDownload(
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ 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.getCatalogueSourceOrNull
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
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.GetCatalogueSource.unregisterCatalogueSource
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||||
import suwayomi.tachidesk.manga.model.table.SourceMetaTable
|
import suwayomi.tachidesk.manga.model.table.SourceMetaTable
|
||||||
@@ -53,7 +53,7 @@ object Source {
|
|||||||
iconUrl = proxyExtensionIconUrl(sourceExtension[ExtensionTable.pkgName]),
|
iconUrl = proxyExtensionIconUrl(sourceExtension[ExtensionTable.pkgName]),
|
||||||
supportsLatest = catalogueSource.supportsLatest,
|
supportsLatest = catalogueSource.supportsLatest,
|
||||||
isConfigurable = catalogueSource is ConfigurableSource,
|
isConfigurable = catalogueSource is ConfigurableSource,
|
||||||
isNsfw = it[SourceTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal,
|
isNsfw = it[SourceTable.contentWarning] >= ContentWarning.MIXED.ordinal,
|
||||||
displayName = catalogueSource.toString(),
|
displayName = catalogueSource.toString(),
|
||||||
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
|
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
|
||||||
)
|
)
|
||||||
@@ -74,7 +74,7 @@ object Source {
|
|||||||
iconUrl = proxyExtensionIconUrl(extension[ExtensionTable.pkgName]),
|
iconUrl = proxyExtensionIconUrl(extension[ExtensionTable.pkgName]),
|
||||||
supportsLatest = catalogueSource.supportsLatest,
|
supportsLatest = catalogueSource.supportsLatest,
|
||||||
isConfigurable = catalogueSource is ConfigurableSource,
|
isConfigurable = catalogueSource is ConfigurableSource,
|
||||||
isNsfw = source[SourceTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal,
|
isNsfw = source[SourceTable.contentWarning] >= ContentWarning.MIXED.ordinal,
|
||||||
displayName = catalogueSource.toString(),
|
displayName = catalogueSource.toString(),
|
||||||
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
|
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ import suwayomi.tachidesk.manga.impl.util.PackageTools
|
|||||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.EXTENSION_FEATURE
|
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_MAX
|
||||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MIN
|
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_NSFW
|
||||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.METADATA_SOURCE_CLASS
|
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.dex2jar
|
||||||
@@ -42,7 +45,6 @@ import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
|||||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.clearCachedImage
|
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.getImageResponse
|
||||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.saveImage
|
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.saveImage
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||||
import suwayomi.tachidesk.server.ApplicationDirs
|
import suwayomi.tachidesk.server.ApplicationDirs
|
||||||
@@ -151,7 +153,10 @@ object Extension {
|
|||||||
// throw Exception("This apk is not a signed with the official tachiyomi signature")
|
// 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.getInt(METADATA_NSFW)
|
||||||
|
}
|
||||||
|
|
||||||
val className =
|
val className =
|
||||||
packageInfo.packageName + packageInfo.applicationInfo.metaData.getString(METADATA_SOURCE_CLASS)
|
packageInfo.packageName + packageInfo.applicationInfo.metaData.getString(METADATA_SOURCE_CLASS)
|
||||||
@@ -184,9 +189,16 @@ object Extension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val extensionName =
|
val extensionName =
|
||||||
packageInfo.applicationInfo.nonLocalizedLabel
|
packageInfo.applicationInfo.metaData.getString(METADATA_NAME)
|
||||||
.toString()
|
?: packageInfo.applicationInfo.nonLocalizedLabel
|
||||||
.substringAfter("Tachiyomi: ")
|
.toString()
|
||||||
|
.substringAfter("Tachiyomi: ")
|
||||||
|
|
||||||
|
val extensionLibVersion =
|
||||||
|
packageInfo.applicationInfo.metaData
|
||||||
|
.getString(METADATA_EXTENSION_LIB)
|
||||||
|
.takeUnless { it == "0" }
|
||||||
|
?: packageInfo.versionName.substringBeforeLast('.')
|
||||||
|
|
||||||
// update extension info
|
// update extension info
|
||||||
transaction {
|
transaction {
|
||||||
@@ -197,14 +209,9 @@ object Extension {
|
|||||||
it[this.pkgName] = packageInfo.packageName
|
it[this.pkgName] = packageInfo.packageName
|
||||||
it[versionName] = packageInfo.versionName
|
it[versionName] = packageInfo.versionName
|
||||||
it[versionCode] = packageInfo.versionCode.toLong()
|
it[versionCode] = packageInfo.versionCode.toLong()
|
||||||
|
it[extensionLib] = extensionLibVersion
|
||||||
it[lang] = extensionLang
|
it[lang] = extensionLang
|
||||||
// todo will change
|
it[this.contentWarning] = contentWarning
|
||||||
it[contentRating] =
|
|
||||||
if (isNsfw) {
|
|
||||||
ContentRating.PORNOGRAPHIC.ordinal
|
|
||||||
} else {
|
|
||||||
ContentRating.SAFE.ordinal
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +236,7 @@ object Extension {
|
|||||||
it[name] = httpSource.name
|
it[name] = httpSource.name
|
||||||
it[lang] = httpSource.lang
|
it[lang] = httpSource.lang
|
||||||
it[extension] = extensionId
|
it[extension] = extensionId
|
||||||
it[contentRating] = if (isNsfw) ContentRating.PORNOGRAPHIC.ordinal else ContentRating.SAFE.ordinal
|
it[this.contentWarning] = contentWarning
|
||||||
}
|
}
|
||||||
logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" }
|
logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" }
|
||||||
}
|
}
|
||||||
@@ -397,7 +404,7 @@ object Extension {
|
|||||||
it[versionName] = targetExtension.versionName
|
it[versionName] = targetExtension.versionName
|
||||||
it[versionCode] = targetExtension.versionCode
|
it[versionCode] = targetExtension.versionCode
|
||||||
it[lang] = targetExtension.lang
|
it[lang] = targetExtension.lang
|
||||||
it[contentRating] = targetExtension.contentRating.ordinal
|
it[contentWarning] = targetExtension.contentWarning.ordinal
|
||||||
it[iconUrl] = targetExtension.iconUrl
|
it[iconUrl] = targetExtension.iconUrl
|
||||||
it[hasUpdate] = false
|
it[hasUpdate] = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import kotlinx.serialization.decodeFromByteArray
|
|||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.okio.decodeFromBufferedSource
|
import kotlinx.serialization.json.okio.decodeFromBufferedSource
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
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.eq
|
||||||
import org.jetbrains.exposed.v1.core.inList
|
import org.jetbrains.exposed.v1.core.inList
|
||||||
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
||||||
@@ -41,45 +44,45 @@ object ExtensionStoreService {
|
|||||||
val protoBuf: ProtoBuf by injectLazy()
|
val protoBuf: ProtoBuf by injectLazy()
|
||||||
val json: Json by injectLazy()
|
val json: Json by injectLazy()
|
||||||
|
|
||||||
suspend fun fetch(indexUrl: String): ExtensionStore = fetch(indexUrl, forceV2 = false)
|
suspend fun fetch(indexUrl: String): ExtensionStore {
|
||||||
|
var updatedIndexUrl: String = indexUrl
|
||||||
private suspend fun fetch(
|
|
||||||
indexUrl: String,
|
|
||||||
forceV2: Boolean,
|
|
||||||
): ExtensionStore {
|
|
||||||
var updatedIndexUrl = indexUrl
|
|
||||||
return try {
|
return try {
|
||||||
network.client.newCall(GET(indexUrl)).awaitSuccess().body.source().use { source ->
|
val response = network.client.newCall(GET(updatedIndexUrl)).awaitSuccess()
|
||||||
try {
|
response.body.source().decompressIfGzipped().use { source ->
|
||||||
protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.peek().readByteArray())
|
val networkStore =
|
||||||
} catch (e: IllegalArgumentException) {
|
when (source.peek().readByte()) {
|
||||||
logger.debug { "Failed to decode as protobuf, trying JSON" }
|
// "[..."
|
||||||
try {
|
0x5B.toByte() -> {
|
||||||
json.decodeFromBufferedSource<NetworkExtensionStore>(source.peek())
|
run {
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
if (forceV2) throw e
|
|
||||||
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")) {
|
if (!indexUrl.endsWith("/index.min.json")) {
|
||||||
throw e
|
throw IllegalArgumentException("Provided legacy store url is not valid")
|
||||||
}
|
}
|
||||||
logger.debug { "Checking for new repo.json format" }
|
|
||||||
updatedIndexUrl = indexUrl.replace("/index.min.json", "/repo.json")
|
updatedIndexUrl = indexUrl.replace("/index.min.json", "/repo.json")
|
||||||
network.client.newCall(GET(updatedIndexUrl)).awaitSuccess().body.source().use {
|
network.client.newCall(GET(updatedIndexUrl)).awaitSuccess().body.source().use {
|
||||||
json.decodeFromBufferedSource<NetworkLegacyExtensionRepo>(it)
|
json.decodeFromBufferedSource<NetworkLegacyExtensionRepo>(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (legacyIndex.indexV2 != null) {
|
// "{..."
|
||||||
return fetch(legacyIndex.indexV2, forceV2 = true)
|
0x7B.toByte() -> {
|
||||||
} else {
|
try {
|
||||||
legacyIndex
|
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) {
|
} catch (e: Exception) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
@@ -105,6 +108,7 @@ object ExtensionStoreService {
|
|||||||
it[contactDiscord] = store.contact.discord
|
it[contactDiscord] = store.contact.discord
|
||||||
it[indexUrl] = store.indexUrl
|
it[indexUrl] = store.indexUrl
|
||||||
it[isLegacy] = store.isLegacy
|
it[isLegacy] = store.isLegacy
|
||||||
|
it[extensionListUrl] = store.extensionListUrl
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ExtensionStoreTable.update({ ExtensionStoreTable.indexUrl eq store.indexUrl }) {
|
ExtensionStoreTable.update({ ExtensionStoreTable.indexUrl eq store.indexUrl }) {
|
||||||
@@ -114,6 +118,7 @@ object ExtensionStoreService {
|
|||||||
it[contactWebsite] = store.contact.website
|
it[contactWebsite] = store.contact.website
|
||||||
it[contactDiscord] = store.contact.discord
|
it[contactDiscord] = store.contact.discord
|
||||||
it[isLegacy] = store.isLegacy
|
it[isLegacy] = store.isLegacy
|
||||||
|
it[extensionListUrl] = store.extensionListUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,17 +215,33 @@ object ExtensionStoreService {
|
|||||||
|
|
||||||
suspend fun getExtensions(store: ExtensionStore): List<ExtensionInfo> {
|
suspend fun getExtensions(store: ExtensionStore): List<ExtensionInfo> {
|
||||||
val extensions =
|
val extensions =
|
||||||
if (!store.isLegacy) {
|
if (store.extensionListUrl != null) {
|
||||||
val response = network.client.newCall(GET(store.indexUrl)).awaitSuccess()
|
val response = network.client.newCall(GET(store.extensionListUrl)).awaitSuccess()
|
||||||
response.body
|
response.body.source().decompressIfGzipped().use { source ->
|
||||||
.source()
|
when (source.peek().readByte()) {
|
||||||
.use { source ->
|
// "{..."
|
||||||
try {
|
0x7B.toByte() -> {
|
||||||
protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.peek().readByteArray())
|
json.decodeFromBufferedSource<NetworkExtensionStore.ExtensionList>(source)
|
||||||
} catch (_: IllegalArgumentException) {
|
}
|
||||||
json.decodeFromBufferedSource<NetworkExtensionStore>(source.peek())
|
|
||||||
|
else -> {
|
||||||
|
protoBuf.decodeFromByteArray<NetworkExtensionStore.ExtensionList>(
|
||||||
|
source.readByteArray(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}.toExtensionInfos(store)
|
}.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 {
|
} else {
|
||||||
val storeBaseUrl = store.indexUrl.removeSuffix("/repo.json")
|
val storeBaseUrl = store.indexUrl.removeSuffix("/repo.json")
|
||||||
val response = network.client.newCall(GET("$storeBaseUrl/index.min.json")).awaitSuccess()
|
val response = network.client.newCall(GET("$storeBaseUrl/index.min.json")).awaitSuccess()
|
||||||
@@ -232,4 +253,17 @@ object ExtensionStoreService {
|
|||||||
}
|
}
|
||||||
return extensions
|
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,7 +22,7 @@ import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
|||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
import org.jetbrains.exposed.v1.jdbc.update
|
import org.jetbrains.exposed.v1.jdbc.update
|
||||||
import suwayomi.tachidesk.manga.impl.extension.Extension.proxyExtensionIconUrl
|
import suwayomi.tachidesk.manga.impl.extension.Extension.proxyExtensionIconUrl
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo
|
import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||||
@@ -81,7 +81,7 @@ object ExtensionsList {
|
|||||||
versionName = it[ExtensionTable.versionName],
|
versionName = it[ExtensionTable.versionName],
|
||||||
versionCode = it[ExtensionTable.versionCode].toInt(),
|
versionCode = it[ExtensionTable.versionCode].toInt(),
|
||||||
lang = it[ExtensionTable.lang],
|
lang = it[ExtensionTable.lang],
|
||||||
isNsfw = it[ExtensionTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal,
|
isNsfw = it[ExtensionTable.contentWarning] >= ContentWarning.MIXED.ordinal,
|
||||||
installed = it[ExtensionTable.isInstalled],
|
installed = it[ExtensionTable.isInstalled],
|
||||||
hasUpdate = it[ExtensionTable.hasUpdate],
|
hasUpdate = it[ExtensionTable.hasUpdate],
|
||||||
obsolete = it[ExtensionTable.isObsolete],
|
obsolete = it[ExtensionTable.isObsolete],
|
||||||
@@ -173,7 +173,7 @@ object ExtensionsList {
|
|||||||
this[ExtensionTable.versionName] = foundExtension.versionName
|
this[ExtensionTable.versionName] = foundExtension.versionName
|
||||||
this[ExtensionTable.versionCode] = foundExtension.versionCode
|
this[ExtensionTable.versionCode] = foundExtension.versionCode
|
||||||
this[ExtensionTable.lang] = foundExtension.lang
|
this[ExtensionTable.lang] = foundExtension.lang
|
||||||
this[ExtensionTable.contentRating] = foundExtension.contentRating.ordinal
|
this[ExtensionTable.contentWarning] = foundExtension.contentWarning.ordinal
|
||||||
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
||||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||||
}
|
}
|
||||||
@@ -190,7 +190,7 @@ object ExtensionsList {
|
|||||||
this[ExtensionTable.versionName] = foundExtension.versionName
|
this[ExtensionTable.versionName] = foundExtension.versionName
|
||||||
this[ExtensionTable.versionCode] = foundExtension.versionCode
|
this[ExtensionTable.versionCode] = foundExtension.versionCode
|
||||||
this[ExtensionTable.lang] = foundExtension.lang
|
this[ExtensionTable.lang] = foundExtension.lang
|
||||||
this[ExtensionTable.contentRating] = foundExtension.contentRating.ordinal
|
this[ExtensionTable.contentWarning] = foundExtension.contentWarning.ordinal
|
||||||
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
||||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import kotlinx.serialization.ExperimentalSerializationApi
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.JsonNames
|
import kotlinx.serialization.json.JsonNames
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
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.ExtensionInfo
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionSource
|
import suwayomi.tachidesk.manga.model.dataclass.ExtensionSource
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionStore
|
import suwayomi.tachidesk.manga.model.dataclass.ExtensionStore
|
||||||
@@ -23,7 +23,8 @@ data class NetworkExtensionStore(
|
|||||||
@ProtoNumber(2) val badgeLabel: String,
|
@ProtoNumber(2) val badgeLabel: String,
|
||||||
@ProtoNumber(3) val signingKey: String,
|
@ProtoNumber(3) val signingKey: String,
|
||||||
@ProtoNumber(4) val contact: Contact,
|
@ProtoNumber(4) val contact: Contact,
|
||||||
@ProtoNumber(5) val extensions: List<Extension>,
|
@ProtoNumber(101) val extensionList: ExtensionList?,
|
||||||
|
@ProtoNumber(102) val extensionListUrl: String?,
|
||||||
) : BaseNetworkExtensionStore {
|
) : BaseNetworkExtensionStore {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Contact(
|
data class Contact(
|
||||||
@@ -31,6 +32,11 @@ data class NetworkExtensionStore(
|
|||||||
@ProtoNumber(2) val discord: String?,
|
@ProtoNumber(2) val discord: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ExtensionList(
|
||||||
|
@ProtoNumber(1) val extensions: List<Extension>,
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Extension(
|
data class Extension(
|
||||||
@ProtoNumber(1) val name: String,
|
@ProtoNumber(1) val name: String,
|
||||||
@@ -39,7 +45,8 @@ data class NetworkExtensionStore(
|
|||||||
@ProtoNumber(4) val extensionLib: String,
|
@ProtoNumber(4) val extensionLib: String,
|
||||||
@ProtoNumber(5) val versionCode: Long,
|
@ProtoNumber(5) val versionCode: Long,
|
||||||
@ProtoNumber(6) val versionName: String,
|
@ProtoNumber(6) val versionName: String,
|
||||||
@ProtoNumber(7) val sources: List<Source>,
|
@ProtoNumber(7) val contentWarning: ContentWarning,
|
||||||
|
@ProtoNumber(8) val sources: List<Source>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -55,27 +62,27 @@ data class NetworkExtensionStore(
|
|||||||
@ProtoNumber(3) val language: String,
|
@ProtoNumber(3) val language: String,
|
||||||
@ProtoNumber(4) val homeUrl: String = "",
|
@ProtoNumber(4) val homeUrl: String = "",
|
||||||
@ProtoNumber(5) val mirrorUrls: List<String> = emptyList(),
|
@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,
|
@ProtoNumber(7) val message: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
enum class ContentRating {
|
enum class ContentWarning {
|
||||||
@ProtoNumber(0)
|
@ProtoNumber(0)
|
||||||
@JsonNames("CONTENT_RATING_SAFE")
|
@JsonNames("CONTENT_WARNING_UNSPECIFIED")
|
||||||
SAFE,
|
UNSPECIFIED,
|
||||||
|
|
||||||
@ProtoNumber(1)
|
@ProtoNumber(1)
|
||||||
@JsonNames("CONTENT_RATING_SUGGESTIVE")
|
@JsonNames("CONTENT_WARNING_SAFE")
|
||||||
SUGGESTIVE,
|
SAFE,
|
||||||
|
|
||||||
@ProtoNumber(2)
|
@ProtoNumber(2)
|
||||||
@JsonNames("CONTENT_RATING_EROTICA")
|
@JsonNames("CONTENT_WARNING_MIXED")
|
||||||
EROTICA,
|
MIXED,
|
||||||
|
|
||||||
@ProtoNumber(3)
|
@ProtoNumber(3)
|
||||||
@JsonNames("CONTENT_RATING_PORNOGRAPHIC")
|
@JsonNames("CONTENT_WARNING_NSFW")
|
||||||
PORNOGRAPHIC,
|
NSFW,
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toExtensionStore(indexUrl: String): ExtensionStore =
|
override fun toExtensionStore(indexUrl: String): ExtensionStore =
|
||||||
@@ -90,10 +97,11 @@ data class NetworkExtensionStore(
|
|||||||
discord = contact.discord,
|
discord = contact.discord,
|
||||||
),
|
),
|
||||||
isLegacy = false,
|
isLegacy = false,
|
||||||
|
extensionListUrl = extensionListUrl,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NetworkExtensionStore.toExtensionInfos(store: ExtensionStore): List<ExtensionInfo> =
|
fun NetworkExtensionStore.ExtensionList.toExtensionInfos(store: ExtensionStore): List<ExtensionInfo> =
|
||||||
extensions.map { extension ->
|
extensions.map { extension ->
|
||||||
val lang = extension.sources.map { it.language }.toSet()
|
val lang = extension.sources.map { it.language }.toSet()
|
||||||
ExtensionInfo(
|
ExtensionInfo(
|
||||||
@@ -106,13 +114,15 @@ fun NetworkExtensionStore.toExtensionInfos(store: ExtensionStore): List<Extensio
|
|||||||
versionCode = extension.versionCode,
|
versionCode = extension.versionCode,
|
||||||
versionName = extension.versionName,
|
versionName = extension.versionName,
|
||||||
lang = if (lang.size == 1) lang.first() else "all",
|
lang = if (lang.size == 1) lang.first() else "all",
|
||||||
contentRating =
|
contentWarning =
|
||||||
when (extension.sources.maxOfOrNull { it.contentRating }) {
|
when (extension.contentWarning) {
|
||||||
NetworkExtensionStore.ContentRating.SAFE -> ContentRating.SAFE
|
NetworkExtensionStore.ContentWarning.SAFE,
|
||||||
NetworkExtensionStore.ContentRating.SUGGESTIVE -> ContentRating.SUGGESTIVE
|
NetworkExtensionStore.ContentWarning.UNSPECIFIED,
|
||||||
NetworkExtensionStore.ContentRating.EROTICA -> ContentRating.EROTICA
|
-> ContentWarning.SAFE
|
||||||
NetworkExtensionStore.ContentRating.PORNOGRAPHIC -> ContentRating.PORNOGRAPHIC
|
|
||||||
null -> ContentRating.SAFE
|
NetworkExtensionStore.ContentWarning.MIXED -> ContentWarning.MIXED
|
||||||
|
|
||||||
|
NetworkExtensionStore.ContentWarning.NSFW -> ContentWarning.NSFW
|
||||||
},
|
},
|
||||||
sources =
|
sources =
|
||||||
extension.sources.map { source ->
|
extension.sources.map { source ->
|
||||||
@@ -122,12 +132,15 @@ fun NetworkExtensionStore.toExtensionInfos(store: ExtensionStore): List<Extensio
|
|||||||
lang = source.language,
|
lang = source.language,
|
||||||
homeUrl = source.homeUrl,
|
homeUrl = source.homeUrl,
|
||||||
message = source.message,
|
message = source.message,
|
||||||
contentRating =
|
contentWarning =
|
||||||
when (source.contentRating) {
|
when (extension.contentWarning) { // todo source.contentWarning
|
||||||
NetworkExtensionStore.ContentRating.SAFE -> ContentRating.SAFE
|
NetworkExtensionStore.ContentWarning.SAFE,
|
||||||
NetworkExtensionStore.ContentRating.SUGGESTIVE -> ContentRating.SUGGESTIVE
|
NetworkExtensionStore.ContentWarning.UNSPECIFIED,
|
||||||
NetworkExtensionStore.ContentRating.EROTICA -> ContentRating.EROTICA
|
-> ContentWarning.SAFE
|
||||||
NetworkExtensionStore.ContentRating.PORNOGRAPHIC -> ContentRating.PORNOGRAPHIC
|
|
||||||
|
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.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.Serializable
|
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.ExtensionInfo
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionSource
|
import suwayomi.tachidesk.manga.model.dataclass.ExtensionSource
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionStore
|
import suwayomi.tachidesk.manga.model.dataclass.ExtensionStore
|
||||||
@@ -49,7 +49,7 @@ fun NetworkLegacyExtension.toExtensionInfo(
|
|||||||
versionCode = code,
|
versionCode = code,
|
||||||
versionName = version,
|
versionName = version,
|
||||||
lang = lang,
|
lang = lang,
|
||||||
contentRating = if (nsfw == 1) ContentRating.PORNOGRAPHIC else ContentRating.SAFE,
|
contentWarning = if (nsfw == 1) ContentWarning.MIXED else ContentWarning.SAFE,
|
||||||
sources =
|
sources =
|
||||||
if (sources.isNullOrEmpty()) {
|
if (sources.isNullOrEmpty()) {
|
||||||
listOf(
|
listOf(
|
||||||
@@ -59,7 +59,7 @@ fun NetworkLegacyExtension.toExtensionInfo(
|
|||||||
lang = lang,
|
lang = lang,
|
||||||
homeUrl = "",
|
homeUrl = "",
|
||||||
message = null,
|
message = null,
|
||||||
contentRating = if (nsfw == 1) ContentRating.PORNOGRAPHIC else ContentRating.SAFE,
|
contentWarning = if (nsfw == 1) ContentWarning.MIXED else ContentWarning.SAFE,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -70,7 +70,7 @@ fun NetworkLegacyExtension.toExtensionInfo(
|
|||||||
lang = source.lang,
|
lang = source.lang,
|
||||||
homeUrl = source.baseUrl,
|
homeUrl = source.baseUrl,
|
||||||
message = null,
|
message = null,
|
||||||
contentRating = if (nsfw == 1) ContentRating.PORNOGRAPHIC else ContentRating.SAFE,
|
contentWarning = if (nsfw == 1) ContentWarning.MIXED else ContentWarning.SAFE,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,5 +36,6 @@ data class NetworkLegacyExtensionRepo(
|
|||||||
discord = null,
|
discord = null,
|
||||||
),
|
),
|
||||||
isLegacy = true,
|
isLegacy = true,
|
||||||
|
extensionListUrl = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,29 +127,36 @@ object KoreaderSyncService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val mangaId = chapterRow[ChapterTable.manga].value
|
val mangaId = chapterRow[ChapterTable.manga].value
|
||||||
|
val isDownloaded = chapterRow[ChapterTable.isDownloaded]
|
||||||
val checksumMethod = serverConfig.koreaderSyncChecksumMethod.value
|
val checksumMethod = serverConfig.koreaderSyncChecksumMethod.value
|
||||||
|
|
||||||
val newHash =
|
val newHash =
|
||||||
when (checksumMethod) {
|
when (checksumMethod) {
|
||||||
KoreaderSyncChecksumMethod.BINARY -> {
|
KoreaderSyncChecksumMethod.BINARY -> {
|
||||||
logger.debug { "[KOSYNC HASH] No hash for chapterId=$chapterId. Generating from downloaded content." }
|
// Only generate binary hash if the chapter is downloaded to avoid fetching missing files
|
||||||
try {
|
if (isDownloaded) {
|
||||||
// Always create a CBZ in memory if it doesn't exist
|
logger.debug { "[KOSYNC HASH] No hash for chapterId=$chapterId. Generating from downloaded content." }
|
||||||
val (stream, _) = ChapterDownloadHelper.getArchiveStreamWithSize(mangaId, chapterId)
|
|
||||||
// Write the stream to a temp file for partial hashing
|
|
||||||
val tempFile = File.createTempFile("kosync-hash-", ".cbz")
|
|
||||||
try {
|
try {
|
||||||
tempFile.outputStream().use { fos ->
|
// Always create a CBZ in memory if it doesn't exist
|
||||||
stream.use { it.copyTo(fos) }
|
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
|
} catch (e: Exception) {
|
||||||
KoreaderHelper.hashContents(tempFile)
|
logger.warn(e) { "[KOSYNC HASH] Failed to generate archive stream for chapterId=$chapterId." }
|
||||||
} finally {
|
null
|
||||||
// Always delete the temp file
|
|
||||||
tempFile.delete()
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} else {
|
||||||
logger.warn(e) { "[KOSYNC HASH] Failed to generate archive stream for chapterId=$chapterId." }
|
logger.debug { "[KOSYNC HASH] Skipping binary hash for chapterId=$chapterId because it is not downloaded." }
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,7 +182,7 @@ object KoreaderSyncService {
|
|||||||
}
|
}
|
||||||
logger.info { "[KOSYNC HASH] Generated and saved new hash for chapterId=$chapterId" }
|
logger.info { "[KOSYNC HASH] Generated and saved new hash for chapterId=$chapterId" }
|
||||||
} else {
|
} else {
|
||||||
logger.warn { "[KOSYNC HASH] Hashing failed for chapterId=$chapterId." }
|
logger.warn { "[KOSYNC HASH] Hashing failed or skipped for chapterId=$chapterId." }
|
||||||
}
|
}
|
||||||
newHash
|
newHash
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ object PackageTools {
|
|||||||
const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
||||||
const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
|
const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
|
||||||
const val METADATA_NSFW = "tachiyomi.extension.nsfw"
|
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_MIN = 1.3
|
||||||
const val LIB_VERSION_MAX = 1.6
|
const val LIB_VERSION_MAX = 1.6
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ data class ExtensionInfo(
|
|||||||
val versionCode: Long,
|
val versionCode: Long,
|
||||||
val versionName: String,
|
val versionName: String,
|
||||||
val lang: String,
|
val lang: String,
|
||||||
val contentRating: ContentRating,
|
val contentWarning: ContentWarning,
|
||||||
val sources: List<ExtensionSource>,
|
val sources: List<ExtensionSource>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,17 +27,16 @@ data class ExtensionSource(
|
|||||||
val lang: String,
|
val lang: String,
|
||||||
val homeUrl: String,
|
val homeUrl: String,
|
||||||
val message: String?,
|
val message: String?,
|
||||||
val contentRating: ContentRating,
|
val contentWarning: ContentWarning,
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class ContentRating {
|
enum class ContentWarning {
|
||||||
SAFE,
|
SAFE,
|
||||||
SUGGESTIVE,
|
MIXED,
|
||||||
EROTICA,
|
NSFW,
|
||||||
PORNOGRAPHIC,
|
|
||||||
;
|
;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun valueOf(contentRating: Int) = entries.find { it.ordinal == contentRating } ?: SAFE
|
fun valueOf(contentWarning: Int) = entries.find { it.ordinal == contentWarning } ?: SAFE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ data class ExtensionStore(
|
|||||||
val signingKey: String,
|
val signingKey: String,
|
||||||
val contact: Contact,
|
val contact: Contact,
|
||||||
val isLegacy: Boolean,
|
val isLegacy: Boolean,
|
||||||
|
val extensionListUrl: String?,
|
||||||
) {
|
) {
|
||||||
data class Contact(
|
data class Contact(
|
||||||
val website: String,
|
val website: String,
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ package suwayomi.tachidesk.manga.model.table
|
|||||||
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||||
|
|
||||||
object ExtensionStoreTable : IntIdTable() {
|
object ExtensionStoreTable : IntIdTable() {
|
||||||
val indexUrl = varchar("index_url", 2048)
|
val indexUrl = varchar("index_url", 2048).uniqueIndex()
|
||||||
val name = varchar("name", 256)
|
val name = varchar("name", 256)
|
||||||
val badgeLabel = varchar("badge_label", 32)
|
val badgeLabel = varchar("badge_label", 32)
|
||||||
val signingKey = varchar("signing_key", 512)
|
val signingKey = varchar("signing_key", 512)
|
||||||
val contactWebsite = varchar("contact_website", 2048)
|
val contactWebsite = varchar("contact_website", 2048)
|
||||||
val contactDiscord = varchar("contact_discord", 2048).nullable()
|
val contactDiscord = varchar("contact_discord", 2048).nullable()
|
||||||
val isLegacy = bool("is_legacy").default(false)
|
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() {
|
object ExtensionTable : IntIdTable() {
|
||||||
val apkName = varchar("apk_name", 1024).nullable()
|
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
|
// default is the local source icon from tachiyomi
|
||||||
@Suppress("ktlint:standard:max-line-length")
|
@Suppress("ktlint:standard:max-line-length")
|
||||||
@@ -28,7 +28,7 @@ object ExtensionTable : IntIdTable() {
|
|||||||
val versionName = varchar("version_name", 16)
|
val versionName = varchar("version_name", 16)
|
||||||
val versionCode = long("version_code")
|
val versionCode = long("version_code")
|
||||||
val lang = varchar("lang", 32)
|
val lang = varchar("lang", 32)
|
||||||
val contentRating = integer("content_rating")
|
val contentWarning = integer("content_warning")
|
||||||
|
|
||||||
val isInstalled = bool("is_installed").default(false)
|
val isInstalled = bool("is_installed").default(false)
|
||||||
val hasUpdate = bool("has_update").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/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import org.jetbrains.exposed.v1.core.dao.id.IdTable
|
import org.jetbrains.exposed.v1.core.dao.id.IdTable
|
||||||
import suwayomi.tachidesk.manga.model.table.columns.unlimitedVarchar
|
|
||||||
|
|
||||||
object SourceTable : IdTable<Long>() {
|
object SourceTable : IdTable<Long>() {
|
||||||
override val id = long("id").entityId()
|
override val id = long("id").entityId()
|
||||||
val name = varchar("name", 128)
|
val name = varchar("name", 128)
|
||||||
val lang = varchar("lang", 32)
|
val lang = varchar("lang", 32)
|
||||||
val extension = reference("extension", ExtensionTable)
|
val extension = reference("extension", ExtensionTable)
|
||||||
val message = unlimitedVarchar("message").nullable()
|
val contentWarning = integer("content_warning").default(0)
|
||||||
val contentRating = integer("content_rating").default(0)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -644,6 +644,16 @@ object OpdsFeedBuilder {
|
|||||||
skipMetadata,
|
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 no chapters are found in the database, attempt to fetch them from the source.
|
||||||
if (chapterEntries.isEmpty() && totalChapters == 0L) {
|
if (chapterEntries.isEmpty() && totalChapters == 0L) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ object ChapterRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.awaitAll()
|
}.awaitAll()
|
||||||
|
// Exclude unreachable chapters that are not downloaded and have no page count
|
||||||
|
.filter { it.downloaded || it.pageCount > 0 }
|
||||||
|
|
||||||
return Pair(enrichedChapters, totalCount)
|
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.alias
|
||||||
import org.jetbrains.exposed.v1.core.and
|
import org.jetbrains.exposed.v1.core.and
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
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.inList
|
||||||
|
import org.jetbrains.exposed.v1.core.inSubQuery
|
||||||
import org.jetbrains.exposed.v1.core.intLiteral
|
import org.jetbrains.exposed.v1.core.intLiteral
|
||||||
import org.jetbrains.exposed.v1.core.like
|
import org.jetbrains.exposed.v1.core.like
|
||||||
import org.jetbrains.exposed.v1.core.lowerCase
|
import org.jetbrains.exposed.v1.core.lowerCase
|
||||||
@@ -75,13 +75,28 @@ fun Query.applyOpdsMangaFilter(
|
|||||||
}
|
}
|
||||||
if (excludeField != "filter") {
|
if (excludeField != "filter") {
|
||||||
criteria.filter?.let { filterVal ->
|
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) {
|
when (filterVal) {
|
||||||
"unread" -> having { unreadCountExpr greater 0 }
|
"unread" -> {
|
||||||
"downloaded" -> having { downloadedCountExpr greater 0 }
|
andWhere {
|
||||||
"ongoing" -> andWhere { MangaTable.status eq MangaStatus.ONGOING.value }
|
MangaTable.id inSubQuery
|
||||||
"completed" -> andWhere { MangaTable.status eq MangaStatus.COMPLETED.value }
|
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")
|
val unreadCount = unreadCountExpr.alias("unread_count")
|
||||||
|
|
||||||
// Base query with necessary joins for filtering and sorting
|
// Base query with necessary joins for filtering and sorting
|
||||||
val query =
|
var baseJoin =
|
||||||
MangaTable
|
MangaTable
|
||||||
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
||||||
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
|
.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)
|
.select(MangaTable.columns + SourceTable.lang + SourceTable.name + unreadCount)
|
||||||
.where { MangaTable.inLibrary eq true }
|
.where { MangaTable.inLibrary eq true }
|
||||||
|
|
||||||
@@ -301,7 +322,6 @@ object MangaRepository {
|
|||||||
* Applies sorting and filtering logic to a manga library query.
|
* Applies sorting and filtering logic to a manga library query.
|
||||||
* @param query The Exposed SQL query to modify.
|
* @param query The Exposed SQL query to modify.
|
||||||
* @param sort The sorting parameter.
|
* @param sort The sorting parameter.
|
||||||
* @param filter The filtering parameter.
|
|
||||||
*/
|
*/
|
||||||
private fun applyMangaLibrarySort(
|
private fun applyMangaLibrarySort(
|
||||||
query: Query,
|
query: Query,
|
||||||
@@ -330,36 +350,38 @@ object MangaRepository {
|
|||||||
*/
|
*/
|
||||||
fun getLibraryFilterCounts(activeFilters: OpdsMangaFilter): Map<String, Long> =
|
fun getLibraryFilterCounts(activeFilters: OpdsMangaFilter): Map<String, Long> =
|
||||||
transaction {
|
transaction {
|
||||||
val unreadCountExpr = Case().When(ChapterTable.isRead eq false, intLiteral(1)).Else(intLiteral(0)).sum()
|
var baseJoin =
|
||||||
val downloadedCountExpr = Case().When(ChapterTable.isDownloaded eq true, intLiteral(1)).Else(intLiteral(0)).sum()
|
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 =
|
val baseQuery =
|
||||||
MangaTable
|
baseJoin
|
||||||
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
|
|
||||||
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
|
||||||
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
|
||||||
.select(MangaTable.id)
|
.select(MangaTable.id)
|
||||||
.where { MangaTable.inLibrary eq true }
|
.where { MangaTable.inLibrary eq true }
|
||||||
|
.withDistinct()
|
||||||
|
|
||||||
baseQuery.applyOpdsMangaFilter(activeFilters, excludeField = "filter")
|
baseQuery.applyOpdsMangaFilter(activeFilters, excludeField = "filter")
|
||||||
baseQuery.groupBy(MangaTable.id)
|
|
||||||
|
|
||||||
val unreadCount = baseQuery.copy().having { unreadCountExpr greater 0 }.count()
|
val unreadCount =
|
||||||
val downloadedCount = baseQuery.copy().having { downloadedCountExpr greater 0 }.count()
|
baseQuery
|
||||||
|
.copy()
|
||||||
val statusBaseQuery =
|
.andWhere {
|
||||||
MangaTable
|
MangaTable.id inSubQuery
|
||||||
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
ChapterTable.select(ChapterTable.manga).where { ChapterTable.isRead eq false }
|
||||||
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
}.count()
|
||||||
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
|
val downloadedCount =
|
||||||
.select(MangaTable.id)
|
baseQuery
|
||||||
.where { MangaTable.inLibrary eq true }
|
.copy()
|
||||||
|
.andWhere {
|
||||||
statusBaseQuery.applyOpdsMangaFilter(activeFilters, excludeField = "filter")
|
MangaTable.id inSubQuery
|
||||||
statusBaseQuery.groupBy(MangaTable.id)
|
ChapterTable.select(ChapterTable.manga).where { ChapterTable.isDownloaded eq true }
|
||||||
|
}.count()
|
||||||
val ongoingCount = statusBaseQuery.copy().andWhere { MangaTable.status eq MangaStatus.ONGOING.value }.count()
|
val ongoingCount = baseQuery.copy().andWhere { MangaTable.status eq MangaStatus.ONGOING.value }.count()
|
||||||
val completedCount = statusBaseQuery.copy().andWhere { MangaTable.status eq MangaStatus.COMPLETED.value }.count()
|
val completedCount = baseQuery.copy().andWhere { MangaTable.status eq MangaStatus.COMPLETED.value }.count()
|
||||||
|
|
||||||
mapOf(
|
mapOf(
|
||||||
"unread" to unreadCount,
|
"unread" to unreadCount,
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import suwayomi.tachidesk.i18n.MR
|
|||||||
import suwayomi.tachidesk.manga.impl.extension.Extension
|
import suwayomi.tachidesk.manga.impl.extension.Extension
|
||||||
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
|
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
|
||||||
import suwayomi.tachidesk.manga.model.table.CategoryTable
|
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.ExtensionTable
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaStatus
|
import suwayomi.tachidesk.manga.model.table.MangaStatus
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||||
@@ -166,12 +165,17 @@ object NavigationRepository {
|
|||||||
transaction {
|
transaction {
|
||||||
val mangaCount = MangaTable.id.countDistinct().alias("manga_count")
|
val mangaCount = MangaTable.id.countDistinct().alias("manga_count")
|
||||||
|
|
||||||
val query =
|
var baseJoin =
|
||||||
SourceTable
|
SourceTable
|
||||||
.join(MangaTable, JoinType.INNER, SourceTable.id, MangaTable.sourceReference)
|
.join(MangaTable, JoinType.INNER, SourceTable.id, MangaTable.sourceReference)
|
||||||
.join(ExtensionTable, JoinType.LEFT, onColumn = SourceTable.extension, otherColumn = ExtensionTable.id)
|
.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)
|
.select(SourceTable.id, SourceTable.name, SourceTable.lang, ExtensionTable.pkgName, mangaCount)
|
||||||
.where { MangaTable.inLibrary eq true }
|
.where { MangaTable.inLibrary eq true }
|
||||||
|
|
||||||
@@ -227,7 +231,6 @@ object NavigationRepository {
|
|||||||
.join(CategoryMangaTable, JoinType.INNER, CategoryTable.id, CategoryMangaTable.category)
|
.join(CategoryMangaTable, JoinType.INNER, CategoryTable.id, CategoryMangaTable.category)
|
||||||
.join(MangaTable, JoinType.INNER, CategoryMangaTable.manga, MangaTable.id)
|
.join(MangaTable, JoinType.INNER, CategoryMangaTable.manga, MangaTable.id)
|
||||||
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
||||||
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
|
|
||||||
.select(CategoryTable.id, CategoryTable.name, mangaCount)
|
.select(CategoryTable.id, CategoryTable.name, mangaCount)
|
||||||
.where { MangaTable.inLibrary eq true }
|
.where { MangaTable.inLibrary eq true }
|
||||||
|
|
||||||
@@ -262,11 +265,15 @@ object NavigationRepository {
|
|||||||
activeFilters: OpdsMangaFilter = OpdsMangaFilter(),
|
activeFilters: OpdsMangaFilter = OpdsMangaFilter(),
|
||||||
): Pair<List<OpdsGenreNavEntry>, Long> =
|
): Pair<List<OpdsGenreNavEntry>, Long> =
|
||||||
transaction {
|
transaction {
|
||||||
val query =
|
var baseJoin =
|
||||||
MangaTable
|
MangaTable
|
||||||
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
||||||
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
if (activeFilters.categoryId != null) {
|
||||||
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
|
baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
val query =
|
||||||
|
baseJoin
|
||||||
.select(MangaTable.genre)
|
.select(MangaTable.genre)
|
||||||
.where { MangaTable.inLibrary eq true }
|
.where { MangaTable.inLibrary eq true }
|
||||||
|
|
||||||
@@ -321,11 +328,16 @@ object NavigationRepository {
|
|||||||
val statusCounts =
|
val statusCounts =
|
||||||
transaction {
|
transaction {
|
||||||
val countExpr = MangaTable.id.countDistinct().alias("manga_count")
|
val countExpr = MangaTable.id.countDistinct().alias("manga_count")
|
||||||
val query =
|
|
||||||
|
var baseJoin =
|
||||||
MangaTable
|
MangaTable
|
||||||
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
|
||||||
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
if (activeFilters.categoryId != null) {
|
||||||
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
|
baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
val query =
|
||||||
|
baseJoin
|
||||||
.select(MangaTable.status, countExpr)
|
.select(MangaTable.status, countExpr)
|
||||||
.where { MangaTable.inLibrary eq true }
|
.where { MangaTable.inLibrary eq true }
|
||||||
|
|
||||||
@@ -368,11 +380,16 @@ object NavigationRepository {
|
|||||||
): Pair<List<OpdsLanguageNavEntry>, Long> =
|
): Pair<List<OpdsLanguageNavEntry>, Long> =
|
||||||
transaction {
|
transaction {
|
||||||
val mangaCount = MangaTable.id.countDistinct().alias("manga_count")
|
val mangaCount = MangaTable.id.countDistinct().alias("manga_count")
|
||||||
val query =
|
|
||||||
|
var baseJoin =
|
||||||
SourceTable
|
SourceTable
|
||||||
.join(MangaTable, JoinType.INNER, SourceTable.id, MangaTable.sourceReference)
|
.join(MangaTable, JoinType.INNER, SourceTable.id, MangaTable.sourceReference)
|
||||||
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
if (activeFilters.categoryId != null) {
|
||||||
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
|
baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
val query =
|
||||||
|
baseJoin
|
||||||
.select(SourceTable.lang, mangaCount)
|
.select(SourceTable.lang, mangaCount)
|
||||||
.where { MangaTable.inLibrary eq true }
|
.where { MangaTable.inLibrary eq true }
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ package suwayomi.tachidesk.server.database.migration
|
|||||||
|
|
||||||
import de.neonew.exposed.migrations.helpers.SQLMigration
|
import de.neonew.exposed.migrations.helpers.SQLMigration
|
||||||
import suwayomi.tachidesk.graphql.types.DatabaseType
|
import suwayomi.tachidesk.graphql.types.DatabaseType
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
||||||
import suwayomi.tachidesk.server.database.migration.helpers.MAYBE_TYPE_PREFIX
|
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.UNLIMITED_TEXT
|
||||||
import suwayomi.tachidesk.server.database.migration.helpers.toSqlName
|
import suwayomi.tachidesk.server.database.migration.helpers.toSqlName
|
||||||
@@ -34,6 +34,7 @@ class M0057_AddNewExtensionApiFields : SQLMigration() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ALTER TABLE EXTENSION ALTER COLUMN store_index_url ${MAYBE_TYPE_PREFIX}VARCHAR(2048);
|
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 version_code ${MAYBE_TYPE_PREFIX}BIGINT;
|
||||||
ALTER TABLE EXTENSION ALTER COLUMN apk_name DROP NOT NULL;
|
ALTER TABLE EXTENSION ALTER COLUMN apk_name DROP NOT NULL;
|
||||||
${
|
${
|
||||||
@@ -43,12 +44,11 @@ class M0057_AddNewExtensionApiFields : SQLMigration() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ALTER TABLE EXTENSION ADD COLUMN apk_url VARCHAR(2048);
|
ALTER TABLE EXTENSION ADD COLUMN apk_url VARCHAR(2048);
|
||||||
ALTER TABLE EXTENSION ADD COLUMN content_rating INTEGER DEFAULT 0;
|
ALTER TABLE EXTENSION ADD COLUMN content_warning INTEGER DEFAULT 0;
|
||||||
UPDATE EXTENSION SET content_rating = ${ContentRating.PORNOGRAPHIC.ordinal} WHERE is_nsfw = TRUE;
|
UPDATE EXTENSION SET content_warning = ${ContentWarning.MIXED.ordinal} WHERE is_nsfw = TRUE;
|
||||||
ALTER TABLE EXTENSION DROP COLUMN is_nsfw;
|
ALTER TABLE EXTENSION DROP COLUMN is_nsfw;
|
||||||
ALTER TABLE SOURCE ADD COLUMN message $UNLIMITED_TEXT;
|
ALTER TABLE SOURCE ADD COLUMN content_warning INTEGER DEFAULT 0;
|
||||||
ALTER TABLE SOURCE ADD COLUMN content_rating INTEGER DEFAULT 0;
|
UPDATE SOURCE SET content_warning = ${ContentWarning.MIXED.ordinal} WHERE is_nsfw = TRUE;
|
||||||
UPDATE SOURCE SET content_rating = ${ContentRating.PORNOGRAPHIC.ordinal} WHERE is_nsfw = TRUE;
|
|
||||||
ALTER TABLE SOURCE DROP COLUMN is_nsfw;
|
ALTER TABLE SOURCE DROP COLUMN is_nsfw;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,13 +14,14 @@ import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
|||||||
@Suppress("ClassName", "unused")
|
@Suppress("ClassName", "unused")
|
||||||
class M0058_AddExtensionStore : AddTableMigration() {
|
class M0058_AddExtensionStore : AddTableMigration() {
|
||||||
private class ExtensionStoreTable : IntIdTable() {
|
private class ExtensionStoreTable : IntIdTable() {
|
||||||
val indexUrl = varchar("index_url", 2048)
|
val indexUrl = varchar("index_url", 2048).uniqueIndex()
|
||||||
val name = varchar("name", 256)
|
val name = varchar("name", 256)
|
||||||
val badgeLabel = varchar("badge_label", 32)
|
val badgeLabel = varchar("badge_label", 32)
|
||||||
val signingKey = varchar("signing_key", 512)
|
val signingKey = varchar("signing_key", 512)
|
||||||
val contactWebsite = varchar("contact_website", 2048)
|
val contactWebsite = varchar("contact_website", 2048)
|
||||||
val contactDiscord = varchar("contact_discord", 2048).nullable()
|
val contactDiscord = varchar("contact_discord", 2048).nullable()
|
||||||
val isLegacy = bool("is_legacy").default(false)
|
val isLegacy = bool("is_legacy").default(false)
|
||||||
|
val extensionListUrl = varchar("extension_list_url", 2048).nullable()
|
||||||
}
|
}
|
||||||
|
|
||||||
override val tables: Array<Table>
|
override val tables: Array<Table>
|
||||||
|
|||||||
Reference in New Issue
Block a user