mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-06 12:24:33 -05:00
Compare commits
1 Commits
renovate/o
...
97a9bc51b2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97a9bc51b2 |
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -143,13 +143,11 @@ body:
|
|||||||
options:
|
options:
|
||||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
|
||||||
required: true
|
required: true
|
||||||
- label: I have checked the ongoing preview changelog of **[Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI/blob/master/CHANGELOG.md)** and **[Suwayomi-Server](https://github.com/Suwayomi/Suwayomi-Server/blob/master/CHANGELOG.md)** and this bug has **NOT** been listed as fixed
|
|
||||||
required: true
|
|
||||||
- label: I have written a short but informative title (ideally less than ~100 characters).
|
- label: I have written a short but informative title (ideally less than ~100 characters).
|
||||||
required: true
|
required: true
|
||||||
- label: I have tried the troubleshooting guide described in [README.md](https://github.com/Suwayomi/Suwayomi-Server?tab=readme-ov-file#troubleshooting-and-support)
|
- label: I have tried the troubleshooting guide described in [README.md](https://github.com/Suwayomi/Suwayomi-Server?tab=readme-ov-file#troubleshooting-and-support)
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the (**[Suwayomi-WebUI](https://github.com/suwayomi/suwayomi-webui/releases/latest)** and **[Suwayomi-Server](https://github.com/suwayomi/suwayomi-server/releases/latest)**) to the latest versions
|
- label: I have updated to the **[latest version](https://github.com/suwayomi/suwayomi-server/releases/latest)**.
|
||||||
required: true
|
required: true
|
||||||
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -31,7 +31,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I have written a short but informative title (ideally less than ~100 characters).
|
- label: I have written a short but informative title (ideally less than ~100 characters).
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the (**[Suwayomi-WebUI](https://github.com/suwayomi/suwayomi-webui/releases/latest)** and **[Suwayomi-Server](https://github.com/suwayomi/suwayomi-server/releases/latest)**) to the latest versions
|
- label: I have updated to the **[latest version](https://github.com/suwayomi/suwayomi-server/releases/latest)**.
|
||||||
required: true
|
required: true
|
||||||
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
||||||
required: true
|
required: true
|
||||||
@@ -9,7 +9,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|||||||
### Added
|
### Added
|
||||||
- (**Sync**) Added [SyncYomi](https://github.com/syncyomi/syncyomi) support
|
- (**Sync**) Added [SyncYomi](https://github.com/syncyomi/syncyomi) support
|
||||||
- (**OPDS**) Add option to skip chapter metadata feed providing direct stream/download links
|
- (**OPDS**) Add option to skip chapter metadata feed providing direct stream/download links
|
||||||
- (**Extension/API**) Support Extensions API v1.6
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- (**Database/H2**) Use the latest H2 database engine
|
- (**Database/H2**) Use the latest H2 database engine
|
||||||
|
|||||||
@@ -159,20 +159,15 @@ server.systemTrayEnabled = true
|
|||||||
server.maxLogFiles = 31
|
server.maxLogFiles = 31
|
||||||
server.maxLogFileSize = "10mb"
|
server.maxLogFileSize = "10mb"
|
||||||
server.maxLogFolderSize = "100mb"
|
server.maxLogFolderSize = "100mb"
|
||||||
|
server.extensionRepos = []
|
||||||
|
server.maxSourcesInParallel = 6
|
||||||
```
|
```
|
||||||
- `server.debugLogsEnabled` controls whether if Suwayomi-Server should print more information while being run inside a Terminal/CMD/Powershell window.
|
- `server.debugLogsEnabled` controls whether if Suwayomi-Server should print more information while being run inside a Terminal/CMD/Powershell window.
|
||||||
- `server.systemTrayEnabled = true` whether if Suwayomi-Server should show a System Tray Icon, disabling this on headless servers is recommended.
|
- `server.systemTrayEnabled = true` whether if Suwayomi-Server should show a System Tray Icon, disabling this on headless servers is recommended.
|
||||||
- `server.maxLogFiles = 31` sets the maximum number of days to keep files before they get deleted.
|
- `server.maxLogFiles = 31` sets the maximum number of days to keep files before they get deleted.
|
||||||
- `server.maxLogFileSize = "10mb"` sets the maximum size of a log file - values are formatted like: 1 (bytes), 1KB (kilobytes), 1MB (megabytes), 1GB (gigabytes)
|
- `server.maxLogFileSize = "10mb"` sets the maximum size of a log file - values are formatted like: 1 (bytes), 1KB (kilobytes), 1MB (megabytes), 1GB (gigabytes)
|
||||||
- `server.maxLogFolderSize = "100mb"` sets the maximum size of all saved log files - values are formatted like: 1 (bytes), 1KB (kilobytes), 1MB (megabytes), 1GB (gigabytes)
|
- `server.maxLogFolderSize = "100mb"` sets the maximum size of all saved log files - values are formatted like: 1 (bytes), 1KB (kilobytes), 1MB (megabytes), 1GB (gigabytes)
|
||||||
|
- `server.extensionRepos` is a list of extension repositories for custom sources. Uses the same format as Mihon; each entry is expected to be a string URL pointing to a JSON file representing the repository.
|
||||||
### Extension/Source
|
|
||||||
```
|
|
||||||
server.extensionStores = []
|
|
||||||
server.maxSourcesInParallel = 6
|
|
||||||
```
|
|
||||||
- `server.extensionStores` is a list of extension stores (previously called repositories) for custom sources. Uses the same format as Mihon; each entry is expected to be a string URL pointing to a JSON or PROTOBUF file representing the repository.
|
|
||||||
- `server.maxSourcesInParallel = 6` sets how many sources can do requests (updates, downloads) in parallel. Updates/downloads are grouped by source and all mangas of a source are updated/downloaded synchronously. Range: 1 <= n <= 20.
|
- `server.maxSourcesInParallel = 6` sets how many sources can do requests (updates, downloads) in parallel. Updates/downloads are grouped by source and all mangas of a source are updated/downloaded synchronously. Range: 1 <= n <= 20.
|
||||||
|
|
||||||
### Backup
|
### Backup
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ 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
|
||||||
@@ -71,8 +70,7 @@ exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exp
|
|||||||
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
|
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
|
||||||
exposed-javatime = { module = "org.jetbrains.exposed:exposed-java-time", version.ref = "exposed" }
|
exposed-javatime = { module = "org.jetbrains.exposed:exposed-java-time", version.ref = "exposed" }
|
||||||
exposed-kotlintime = { module = "org.jetbrains.exposed:exposed-kotlin-datetime", version.ref = "exposed" }
|
exposed-kotlintime = { module = "org.jetbrains.exposed:exposed-kotlin-datetime", version.ref = "exposed" }
|
||||||
exposed-json = { module = "org.jetbrains.exposed:exposed-json ", version.ref = "exposed" }
|
postgres = "org.postgresql:postgresql:42.7.11"
|
||||||
postgres = "org.postgresql:postgresql:42.7.12"
|
|
||||||
h2 = "com.h2database:h2:2.4.240"
|
h2 = "com.h2database:h2:2.4.240"
|
||||||
hikaricp = "com.zaxxer:HikariCP:7.1.0"
|
hikaricp = "com.zaxxer:HikariCP:7.1.0"
|
||||||
|
|
||||||
@@ -186,7 +184,7 @@ buildconfig = { id = "com.github.gmazzo.buildconfig", version = "6.0.10"}
|
|||||||
download = { id = "de.undercouch.download", version = "5.7.0"}
|
download = { id = "de.undercouch.download", version = "5.7.0"}
|
||||||
|
|
||||||
# ShadowJar
|
# ShadowJar
|
||||||
shadowjar = { id = "com.gradleup.shadow", version = "8.3.11"}
|
shadowjar = { id = "com.gradleup.shadow", version = "9.4.3"}
|
||||||
|
|
||||||
# Moko
|
# Moko
|
||||||
moko = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "moko" }
|
moko = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "moko" }
|
||||||
@@ -229,7 +227,6 @@ okhttp = [
|
|||||||
"okhttp-logging",
|
"okhttp-logging",
|
||||||
"okhttp-dnsoverhttps",
|
"okhttp-dnsoverhttps",
|
||||||
"okhttp-brotli",
|
"okhttp-brotli",
|
||||||
"okhttp-zstd",
|
|
||||||
]
|
]
|
||||||
javalin = [
|
javalin = [
|
||||||
"javalin-core",
|
"javalin-core",
|
||||||
@@ -248,7 +245,6 @@ exposed = [
|
|||||||
"exposed-jdbc",
|
"exposed-jdbc",
|
||||||
"exposed-javatime",
|
"exposed-javatime",
|
||||||
"exposed-kotlintime",
|
"exposed-kotlintime",
|
||||||
"exposed-json",
|
|
||||||
]
|
]
|
||||||
systemtray = [
|
systemtray = [
|
||||||
"systemtray-core",
|
"systemtray-core",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<string name="opds_feeds_root">Suwayomi OPDS Katalog</string>
|
<string name="opds_feeds_root">Suwayomi OPDS Katalog</string>
|
||||||
<string name="opds_feeds_chapter_details">%1$s | %2$s | Details</string>
|
<string name="opds_feeds_chapter_details">%1$s | %2$s | Details</string>
|
||||||
<string name="opds_feeds_sources_title">Alle Quellen</string>
|
<string name="opds_feeds_sources_title">Alle Quellen</string>
|
||||||
<string name="opds_feeds_genres_title">Genren</string>
|
<string name="opds_feeds_genres_title">Genres</string>
|
||||||
<string name="opds_feeds_genres_entry_content">Durchsuche Serien nach Genre</string>
|
<string name="opds_feeds_genres_entry_content">Durchsuche Serien nach Genre</string>
|
||||||
<string name="opds_feeds_status_entry_content">Durchsuche Serien nach Publikationsstatus</string>
|
<string name="opds_feeds_status_entry_content">Durchsuche Serien nach Publikationsstatus</string>
|
||||||
<string name="opds_feeds_languages_title">Sprachen</string>
|
<string name="opds_feeds_languages_title">Sprachen</string>
|
||||||
@@ -122,7 +122,4 @@
|
|||||||
<string name="webview_label_login_required">Deine Konfiguration erfordert die Anmeldung. Bitte gib Benutzername und Passwort ein.</string>
|
<string name="webview_label_login_required">Deine Konfiguration erfordert die Anmeldung. Bitte gib Benutzername und Passwort ein.</string>
|
||||||
<string name="opds_linktitle_first_page">Erste Seite</string>
|
<string name="opds_linktitle_first_page">Erste Seite</string>
|
||||||
<string name="opds_linktitle_last_page">Letzte Seite</string>
|
<string name="opds_linktitle_last_page">Letzte Seite</string>
|
||||||
<string name="opds_error_chapters_not_found">Keine Kapitel gefunden oder die Quelle ist nicht erreichbar auf Seite %1$d.</string>
|
|
||||||
<string name="opds_chapter_title_fallback">Kapitel %1$s</string>
|
|
||||||
<string name="opds_chapter_title_oneshot">Oneshot</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -122,6 +122,4 @@
|
|||||||
<string name="login_label_login">Σύνδεση</string>
|
<string name="login_label_login">Σύνδεση</string>
|
||||||
<string name="login_placeholder_username">Πληκτρολόγησε όνομα χρήστη...</string>
|
<string name="login_placeholder_username">Πληκτρολόγησε όνομα χρήστη...</string>
|
||||||
<string name="login_placeholder_password">Μυστικό...</string>
|
<string name="login_placeholder_password">Μυστικό...</string>
|
||||||
<string name="opds_error_chapters_not_found">Δεν βρέθηκαν κεφάλαια ή η πηγή είναι μη διαθέσιμη στη σελίδα %1$d.</string>
|
|
||||||
<string name="opds_chapter_title_fallback">Κεφάλαιο %1$s</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -122,6 +122,4 @@
|
|||||||
<string name="webview_label_login_required">Su configuración requiere que inicie sesión. Introduzca su nombre de usuario y contraseña.</string>
|
<string name="webview_label_login_required">Su configuración requiere que inicie sesión. Introduzca su nombre de usuario y contraseña.</string>
|
||||||
<string name="opds_linktitle_first_page">Primera página</string>
|
<string name="opds_linktitle_first_page">Primera página</string>
|
||||||
<string name="opds_linktitle_last_page">Última página</string>
|
<string name="opds_linktitle_last_page">Última página</string>
|
||||||
<string name="opds_error_chapters_not_found">No se encontraron capítulos o la fuente no está disponible en la página %1$d.</string>
|
|
||||||
<string name="opds_chapter_title_fallback">Capítulo %1$s</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -122,7 +122,4 @@
|
|||||||
<string name="login_label_login">Se connecter</string>
|
<string name="login_label_login">Se connecter</string>
|
||||||
<string name="login_placeholder_username">Tapez le nom d\'utilisateur…</string>
|
<string name="login_placeholder_username">Tapez le nom d\'utilisateur…</string>
|
||||||
<string name="login_placeholder_password">Secret…</string>
|
<string name="login_placeholder_password">Secret…</string>
|
||||||
<string name="opds_error_chapters_not_found">Aucun chapitre trouvé ou la source est inaccessible à la page %1$d.</string>
|
|
||||||
<string name="opds_chapter_title_fallback">Chapitre %1$s</string>
|
|
||||||
<string name="opds_chapter_title_oneshot">One shot</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -122,6 +122,4 @@
|
|||||||
<string name="login_label_login">Accedi</string>
|
<string name="login_label_login">Accedi</string>
|
||||||
<string name="login_placeholder_username">Digita il nome utente...</string>
|
<string name="login_placeholder_username">Digita il nome utente...</string>
|
||||||
<string name="login_placeholder_password">Segreto...</string>
|
<string name="login_placeholder_password">Segreto...</string>
|
||||||
<string name="opds_error_chapters_not_found">Nessun capitolo trovato o la fonte non è raggiungibile alla pagina %1$d.</string>
|
|
||||||
<string name="opds_chapter_title_fallback">Capitolo %1$s</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -60,7 +60,4 @@
|
|||||||
<string name="opds_feeds_library_sources_title">ソース</string>
|
<string name="opds_feeds_library_sources_title">ソース</string>
|
||||||
<string name="opds_feeds_library_sources_entry_content">ソース別にライブラリ内のマンガを閲覧</string>
|
<string name="opds_feeds_library_sources_entry_content">ソース別にライブラリ内のマンガを閲覧</string>
|
||||||
<string name="opds_feeds_search_results_title">検索結果</string>
|
<string name="opds_feeds_search_results_title">検索結果</string>
|
||||||
<string name="opds_error_chapters_not_found">ページ %1$d で章が見つからないか、ソースに接続できません。</string>
|
|
||||||
<string name="opds_chapter_title_oneshot">読み切り</string>
|
|
||||||
<string name="opds_chapter_title_fallback">第 %1$s 話</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -76,6 +76,4 @@
|
|||||||
<string name="opds_facet_filter_all">Wszystkie</string>
|
<string name="opds_facet_filter_all">Wszystkie</string>
|
||||||
<string name="opds_facet_filter_downloaded">Pobrane</string>
|
<string name="opds_facet_filter_downloaded">Pobrane</string>
|
||||||
<string name="opds_facet_filter_ongoing">Trwające</string>
|
<string name="opds_facet_filter_ongoing">Trwające</string>
|
||||||
<string name="opds_error_chapters_not_found">Nie znaleziono rozdziałów lub źródło jest nieosiągalne na stronie %1$d.</string>
|
|
||||||
<string name="opds_chapter_title_fallback">Rozdział %1$s</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -122,6 +122,4 @@
|
|||||||
<string name="login_label_login">Entrar</string>
|
<string name="login_label_login">Entrar</string>
|
||||||
<string name="login_placeholder_username">Digite o nome de usuário...</string>
|
<string name="login_placeholder_username">Digite o nome de usuário...</string>
|
||||||
<string name="login_placeholder_password">Segredo...</string>
|
<string name="login_placeholder_password">Segredo...</string>
|
||||||
<string name="opds_error_chapters_not_found">Nenhum capítulo encontrado ou a fonte está inacessível na página %1$d.</string>
|
|
||||||
<string name="opds_chapter_title_fallback">Capítulo %1$s</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -122,7 +122,4 @@
|
|||||||
<string name="opds_search_description">Ищите тайтлы в каталоге.</string>
|
<string name="opds_search_description">Ищите тайтлы в каталоге.</string>
|
||||||
<string name="opds_error_manga_not_found">Тайтл с ID %1$d не найден.</string>
|
<string name="opds_error_manga_not_found">Тайтл с ID %1$d не найден.</string>
|
||||||
<string name="opds_chapter_details_base">Тайтл: %1$s | %2$s</string>
|
<string name="opds_chapter_details_base">Тайтл: %1$s | %2$s</string>
|
||||||
<string name="opds_error_chapters_not_found">Главы не найдены или источник недоступен на странице %1$d.</string>
|
|
||||||
<string name="opds_chapter_title_fallback">Глава %1$s</string>
|
|
||||||
<string name="opds_chapter_title_oneshot">Ваншот</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -53,7 +53,4 @@
|
|||||||
<string name="opds_chapter_status_unread">⭕</string>
|
<string name="opds_chapter_status_unread">⭕</string>
|
||||||
<string name="opds_chapter_details_base">%1$s | %2$s</string>
|
<string name="opds_chapter_details_base">%1$s | %2$s</string>
|
||||||
<string name="opds_feeds_genre_specific_title">இசைவகை: %1$s</string>
|
<string name="opds_feeds_genre_specific_title">இசைவகை: %1$s</string>
|
||||||
<string name="opds_error_chapters_not_found">பக்கம் %1$d இல் அத்தியாயங்கள் எதுவும் காணப்படவில்லை அல்லது மூலத்தை அணுக முடியவில்லை.</string>
|
|
||||||
<string name="opds_chapter_title_oneshot">ஒன்-ஷாட்</string>
|
|
||||||
<string name="opds_chapter_title_fallback">அத்தியாயம் %1$s</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -122,6 +122,4 @@
|
|||||||
<string name="webview_label_login_required">Cấu hình của bạn yêu cầu bạn phải đăng nhập. Vui lòng nhập tên người dùng và mật khẩu.</string>
|
<string name="webview_label_login_required">Cấu hình của bạn yêu cầu bạn phải đăng nhập. Vui lòng nhập tên người dùng và mật khẩu.</string>
|
||||||
<string name="opds_linktitle_first_page">Trang đầu</string>
|
<string name="opds_linktitle_first_page">Trang đầu</string>
|
||||||
<string name="opds_linktitle_last_page">Trang cuối</string>
|
<string name="opds_linktitle_last_page">Trang cuối</string>
|
||||||
<string name="opds_error_chapters_not_found">Không tìm thấy chương nào hoặc nguồn không thể truy cập tại trang %1$d.</string>
|
|
||||||
<string name="opds_chapter_title_fallback">Chương %1$s</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -122,7 +122,4 @@
|
|||||||
<string name="login_placeholder_username">输入用户名…</string>
|
<string name="login_placeholder_username">输入用户名…</string>
|
||||||
<string name="login_placeholder_password">密匙…</string>
|
<string name="login_placeholder_password">密匙…</string>
|
||||||
<string name="label_error">错误</string>
|
<string name="label_error">错误</string>
|
||||||
<string name="opds_error_chapters_not_found">第 %1$d 页未找到任何章节,或图源无法访问。</string>
|
|
||||||
<string name="opds_chapter_title_fallback">第 %1$s 章</string>
|
|
||||||
<string name="opds_chapter_title_oneshot">单篇</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -276,38 +276,30 @@ class ServerConfig(
|
|||||||
description = "Ignore re-uploaded chapters from auto-download",
|
description = "Ignore re-uploaded chapters from auto-download",
|
||||||
)
|
)
|
||||||
|
|
||||||
@Deprecated("Will get removed", replaceWith = ReplaceWith("extensionStores"))
|
val extensionRepos: MutableStateFlow<List<String>> by ListSetting<String>(
|
||||||
val extensionRepos: MutableStateFlow<List<String>> by MigratedConfigValue(
|
|
||||||
protoNumber = 22,
|
protoNumber = 22,
|
||||||
group = SettingGroup.EXTENSION,
|
group = SettingGroup.EXTENSION,
|
||||||
privacySafe = false,
|
privacySafe = false,
|
||||||
defaultValue = emptyList(),
|
defaultValue = emptyList(),
|
||||||
deprecated =
|
itemValidator = { url ->
|
||||||
SettingsRegistry.SettingDeprecated(
|
if (url.matches(repoMatchRegex)) {
|
||||||
message = "Replaced with addExtensionStore and removeExtensionStore mutations",
|
null
|
||||||
migrateConfigValue = {
|
} else {
|
||||||
@Suppress("UNCHECKED_CAST")
|
"Invalid repository URL format"
|
||||||
(it.unwrapped() as? List<String>)
|
}
|
||||||
?.map {
|
},
|
||||||
if (it.contains("github.com")) {
|
itemToValidValue = { url ->
|
||||||
it.replace(repoMatchRegex) {
|
if (url.matches(repoMatchRegex)) {
|
||||||
"https://raw.githubusercontent.com/${it.groupValues[2]}/${it.groupValues[3]}/" +
|
url
|
||||||
(it.groupValues.getOrNull(4)?.ifBlank { null } ?: "repo") +
|
} else {
|
||||||
"/" +
|
null
|
||||||
(it.groupValues.getOrNull(5)?.ifBlank { null } ?: "index.min.json")
|
}
|
||||||
}
|
},
|
||||||
} else {
|
|
||||||
it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
readMigrated = { extensionStores.value },
|
|
||||||
setMigrated = { extensionStores.value = it.distinct() },
|
|
||||||
typeInfo =
|
typeInfo =
|
||||||
SettingsRegistry.PartialTypeInfo(
|
SettingsRegistry.PartialTypeInfo(
|
||||||
specificType = "List<String>",
|
specificType = "List<String>",
|
||||||
),
|
),
|
||||||
|
description = "example: [\"https://github.com/MY_ACCOUNT/MY_REPO/tree/repo\"]",
|
||||||
)
|
)
|
||||||
|
|
||||||
val maxSourcesInParallel: MutableStateFlow<Int> by IntSetting(
|
val maxSourcesInParallel: MutableStateFlow<Int> by IntSetting(
|
||||||
@@ -1112,29 +1104,7 @@ class ServerConfig(
|
|||||||
privacySafe = true,
|
privacySafe = true,
|
||||||
defaultValue = false,
|
defaultValue = false,
|
||||||
description = "Skips the metadata feed and provides download/stream links directly in the chapter list. Improves compatibility with KOReader auto-downloader. KoSync strategies are applied, but PROMPT conflicts are ignored (treating local progress as priority)."
|
description = "Skips the metadata feed and provides download/stream links directly in the chapter list. Improves compatibility with KOReader auto-downloader. KoSync strategies are applied, but PROMPT conflicts are ignored (treating local progress as priority)."
|
||||||
)
|
|
||||||
|
|
||||||
val extensionStores: MutableStateFlow<List<String>> by ListSetting<String>(
|
|
||||||
protoNumber = 97,
|
|
||||||
group = SettingGroup.EXTENSION,
|
|
||||||
privacySafe = true,
|
|
||||||
defaultValue = emptyList(),
|
|
||||||
requiresRestart = true,
|
|
||||||
itemValidator = { url ->
|
|
||||||
if (url.isNotEmpty()) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
"Invalid store URL format"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
itemToValidValue = { url ->
|
|
||||||
url.ifEmpty { null }
|
|
||||||
},
|
|
||||||
typeInfo =
|
|
||||||
SettingsRegistry.PartialTypeInfo(
|
|
||||||
specificType = "List<String>",
|
|
||||||
),
|
|
||||||
description = "List of extension store index URLs",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/** ****************************************************************** **/
|
/** ****************************************************************** **/
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.network
|
|
||||||
|
|
||||||
import okhttp3.Response
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exception that handles HTTP codes considered not successful by OkHttp.
|
|
||||||
* Use it to have a standardized error message in the app across the extensions.
|
|
||||||
*
|
|
||||||
* @see Response.isSuccessful
|
|
||||||
* @since tachiyomix 1.6
|
|
||||||
* @param code [Int] the HTTP status code
|
|
||||||
*/
|
|
||||||
class HttpException(
|
|
||||||
val code: Int,
|
|
||||||
) : IllegalStateException("HTTP error $code")
|
|
||||||
@@ -2,25 +2,26 @@ package eu.kanade.tachiyomi.network
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import app.cash.quickjs.QuickJs
|
import app.cash.quickjs.QuickJs
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Util for evaluating JavaScript in sources.
|
* Util for evaluating JavaScript in sources.
|
||||||
*/
|
*/
|
||||||
@Suppress("UNUSED", "UNCHECKED_CAST")
|
|
||||||
class JavaScriptEngine(
|
class JavaScriptEngine(
|
||||||
context: Context,
|
@Suppress("UNUSED_PARAMETER") context: Context,
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Evaluate arbitrary JavaScript code and get the result as a primitive type
|
* Evaluate arbitrary JavaScript code and get the result as a primitive type
|
||||||
* (e.g., String, Int).
|
* (e.g., String, Int).
|
||||||
*
|
*
|
||||||
* @since tachiyomix 1.4
|
* @since extensions-lib 1.4
|
||||||
* @param script JavaScript to execute.
|
* @param script JavaScript to execute.
|
||||||
* @return Result of JavaScript code as a primitive type.
|
* @return Result of JavaScript code as a primitive type.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("UNUSED", "UNCHECKED_CAST")
|
||||||
suspend fun <T> evaluate(script: String): T =
|
suspend fun <T> evaluate(script: String): T =
|
||||||
withIOContext {
|
withContext(Dispatchers.IO) {
|
||||||
QuickJs.create().use {
|
QuickJs.create().use {
|
||||||
it.evaluate(script) as T
|
it.evaluate(script) as T
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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
|
||||||
@@ -21,8 +22,9 @@ 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.GetSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||||
import java.net.CookieHandler
|
import java.net.CookieHandler
|
||||||
import java.net.CookieManager
|
import java.net.CookieManager
|
||||||
import java.net.CookiePolicy
|
import java.net.CookiePolicy
|
||||||
@@ -62,7 +64,7 @@ class NetworkHelper(
|
|||||||
userAgent
|
userAgent
|
||||||
.drop(1)
|
.drop(1)
|
||||||
.onEach {
|
.onEach {
|
||||||
GetSource.unregisterAllSources() // need to reset the headers
|
GetCatalogueSource.unregisterAllCatalogueSources() // need to reset the headers
|
||||||
}.launchIn(GlobalScope)
|
}.launchIn(GlobalScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +84,8 @@ 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 =
|
||||||
@@ -124,7 +128,5 @@ class NetworkHelper(
|
|||||||
// val client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() }
|
// val client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() }
|
||||||
val client by lazy { baseClientBuilder.build() }
|
val client by lazy { baseClientBuilder.build() }
|
||||||
|
|
||||||
@Deprecated("The regular client handles Cloudflare by default")
|
|
||||||
@Suppress("UNUSED")
|
|
||||||
val cloudflareClient by lazy { client }
|
val cloudflareClient by lazy { client }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,14 +15,11 @@ import rx.Observable
|
|||||||
import rx.Producer
|
import rx.Producer
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import kotlin.concurrent.atomics.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
val jsonMime = "application/json; charset=utf-8".toMediaType()
|
val jsonMime = "application/json; charset=utf-8".toMediaType()
|
||||||
|
|
||||||
@OptIn(ExperimentalAtomicApi::class)
|
|
||||||
@Deprecated("Use suspend APIs instead")
|
|
||||||
fun Call.asObservable(): Observable<Response> {
|
fun Call.asObservable(): Observable<Response> {
|
||||||
return Observable.unsafeCreate { subscriber ->
|
return Observable.unsafeCreate { subscriber ->
|
||||||
// Since Call is a one-shot type, clone it for each new subscriber.
|
// Since Call is a one-shot type, clone it for each new subscriber.
|
||||||
@@ -30,11 +27,9 @@ fun Call.asObservable(): Observable<Response> {
|
|||||||
|
|
||||||
// Wrap the call in a helper which handles both unsubscription and backpressure.
|
// Wrap the call in a helper which handles both unsubscription and backpressure.
|
||||||
val requestArbiter =
|
val requestArbiter =
|
||||||
object : Producer, Subscription {
|
object : AtomicBoolean(), Producer, Subscription {
|
||||||
val boolean = AtomicBoolean(false)
|
|
||||||
|
|
||||||
override fun request(n: Long) {
|
override fun request(n: Long) {
|
||||||
if (n == 0L || !boolean.compareAndSet(expectedValue = false, newValue = true)) return
|
if (n == 0L || !compareAndSet(false, true)) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val response = call.execute()
|
val response = call.execute()
|
||||||
@@ -42,15 +37,15 @@ fun Call.asObservable(): Observable<Response> {
|
|||||||
subscriber.onNext(response)
|
subscriber.onNext(response)
|
||||||
subscriber.onCompleted()
|
subscriber.onCompleted()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (error: Exception) {
|
||||||
if (!subscriber.isUnsubscribed) {
|
if (!subscriber.isUnsubscribed) {
|
||||||
subscriber.onError(e)
|
subscriber.onError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun unsubscribe() {
|
override fun unsubscribe() {
|
||||||
call.cancel()
|
// call.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isUnsubscribed(): Boolean = call.isCanceled()
|
override fun isUnsubscribed(): Boolean = call.isCanceled()
|
||||||
@@ -61,50 +56,50 @@ fun Call.asObservable(): Observable<Response> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("Use suspend APIs instead")
|
fun Call.asObservableSuccess(): Observable<Response> =
|
||||||
fun Call.asObservableSuccess(): Observable<Response> {
|
asObservable()
|
||||||
@Suppress("DEPRECATION")
|
.doOnNext { response ->
|
||||||
return asObservable().doOnNext { response ->
|
if (!response.isSuccessful) {
|
||||||
if (!response.isSuccessful) {
|
response.close()
|
||||||
response.close()
|
throw HttpException(response.code)
|
||||||
throw HttpException(response.code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Based on https://github.com/square/okhttp/blob/master/okhttp-coroutines/src/main/kotlin/okhttp3/coroutines/ExecuteAsync.kt
|
|
||||||
// and https://github.com/gildor/kotlin-coroutines-okhttp
|
|
||||||
private suspend fun Call.await(callStack: Array<StackTraceElement>): Response {
|
|
||||||
return suspendCancellableCoroutine { continuation ->
|
|
||||||
continuation.invokeOnCancellation {
|
|
||||||
try {
|
|
||||||
this.cancel()
|
|
||||||
} catch (_: Throwable) {
|
|
||||||
// ignore
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.enqueue(
|
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
|
||||||
|
private suspend fun Call.await(callStack: Array<StackTraceElement>): Response {
|
||||||
|
return suspendCancellableCoroutine { continuation ->
|
||||||
|
val callback =
|
||||||
object : Callback {
|
object : Callback {
|
||||||
override fun onFailure(
|
|
||||||
call: Call,
|
|
||||||
e: IOException,
|
|
||||||
) {
|
|
||||||
if (continuation.isCancelled) return
|
|
||||||
val exception = IOException(e.message, e).apply { stackTrace = callStack }
|
|
||||||
continuation.resumeWithException(exception)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResponse(
|
override fun onResponse(
|
||||||
call: Call,
|
call: Call,
|
||||||
response: Response,
|
response: Response,
|
||||||
) {
|
) {
|
||||||
continuation.resume(response) { _, value, _ ->
|
continuation.resume(response) { _, resourceToClose, _ ->
|
||||||
value.close()
|
response.body.close()
|
||||||
|
resourceToClose.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
)
|
override fun onFailure(
|
||||||
|
call: Call,
|
||||||
|
e: IOException,
|
||||||
|
) {
|
||||||
|
// Don't bother with resuming the continuation if it is already cancelled.
|
||||||
|
if (continuation.isCancelled) return
|
||||||
|
val exception = IOException(e.message, e).apply { stackTrace = callStack }
|
||||||
|
continuation.resumeWithException(exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enqueue(callback)
|
||||||
|
|
||||||
|
continuation.invokeOnCancellation {
|
||||||
|
try {
|
||||||
|
cancel()
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
// Ignore cancel exception
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +109,7 @@ suspend fun Call.await(): Response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Similar to [await] but throws [HttpException] if [Response.isSuccessful] returns false
|
* @since extensions-lib 1.5
|
||||||
*/
|
*/
|
||||||
suspend fun Call.awaitSuccess(): Response {
|
suspend fun Call.awaitSuccess(): Response {
|
||||||
val callStack = Exception().stackTrace.run { copyOfRange(1, size) }
|
val callStack = Exception().stackTrace.run { copyOfRange(1, size) }
|
||||||
@@ -155,3 +150,7 @@ fun <T> decodeFromJsonResponse(
|
|||||||
response.body.source().use {
|
response.body.source().use {
|
||||||
json.decodeFromBufferedSource(deserializer, it)
|
json.decodeFromBufferedSource(deserializer, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class HttpException(
|
||||||
|
val code: Int,
|
||||||
|
) : IllegalStateException("HTTP error $code")
|
||||||
|
|||||||
@@ -35,11 +35,7 @@ class ProgressResponseBody(
|
|||||||
val bytesRead = super.read(sink, byteCount)
|
val bytesRead = super.read(sink, byteCount)
|
||||||
// read() returns the number of bytes read, or -1 if this source is exhausted.
|
// read() returns the number of bytes read, or -1 if this source is exhausted.
|
||||||
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
|
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
|
||||||
progressListener.update(
|
progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
|
||||||
totalBytesRead,
|
|
||||||
responseBody.contentLength(),
|
|
||||||
bytesRead == -1L,
|
|
||||||
)
|
|
||||||
return bytesRead
|
return bytesRead
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import okhttp3.CacheControl
|
|||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import java.util.concurrent.TimeUnit.MINUTES
|
import java.util.concurrent.TimeUnit.MINUTES
|
||||||
@@ -19,7 +18,13 @@ fun GET(
|
|||||||
url: String,
|
url: String,
|
||||||
headers: Headers = DEFAULT_HEADERS,
|
headers: Headers = DEFAULT_HEADERS,
|
||||||
cache: CacheControl = DEFAULT_CACHE_CONTROL,
|
cache: CacheControl = DEFAULT_CACHE_CONTROL,
|
||||||
): Request = GET(url.toHttpUrl(), headers, cache)
|
): Request =
|
||||||
|
Request
|
||||||
|
.Builder()
|
||||||
|
.url(url)
|
||||||
|
.headers(headers)
|
||||||
|
.cacheControl(cache)
|
||||||
|
.build()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since extensions-lib 1.4
|
* @since extensions-lib 1.4
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,6 @@ package eu.kanade.tachiyomi.source
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.model.SMangaUpdate
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.supervisorScope
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
|
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
|
||||||
|
|
||||||
@@ -17,62 +11,68 @@ interface CatalogueSource : Source {
|
|||||||
*/
|
*/
|
||||||
override val lang: String
|
override val lang: String
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
/**
|
||||||
override suspend fun getPopularManga(page: Int): MangasPage = fetchPopularManga(page).awaitSingle()
|
* Whether the source has support for latest updates.
|
||||||
|
*/
|
||||||
|
val supportsLatest: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a page with a list of manga.
|
||||||
|
*
|
||||||
|
* @since extensions-lib 1.5
|
||||||
|
* @param page the page number to retrieve.
|
||||||
|
*/
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
override suspend fun getLatestUpdates(page: Int): MangasPage = fetchLatestUpdates(page).awaitSingle()
|
suspend fun getPopularManga(page: Int): MangasPage = fetchPopularManga(page).awaitSingle()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a page with a list of manga.
|
||||||
|
*
|
||||||
|
* @since extensions-lib 1.5
|
||||||
|
* @param page the page number to retrieve.
|
||||||
|
* @param query the search query.
|
||||||
|
* @param filters the list of filters to apply.
|
||||||
|
*/
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
override suspend fun getSearchManga(
|
suspend fun getSearchManga(
|
||||||
page: Int,
|
page: Int,
|
||||||
query: String,
|
query: String,
|
||||||
filters: FilterList,
|
filters: FilterList,
|
||||||
): MangasPage = fetchSearchManga(page, query, filters).awaitSingle()
|
): MangasPage = fetchSearchManga(page, query, filters).awaitSingle()
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
override suspend fun getMangaUpdate(
|
|
||||||
manga: SManga,
|
|
||||||
chapters: List<SChapter>,
|
|
||||||
fetchDetails: Boolean,
|
|
||||||
fetchChapters: Boolean,
|
|
||||||
): SMangaUpdate =
|
|
||||||
supervisorScope {
|
|
||||||
val asyncManga = if (fetchDetails) async { fetchMangaDetails(manga).awaitSingle() } else null
|
|
||||||
val asyncChapters = if (fetchChapters) async { fetchChapterList(manga).awaitSingle() } else null
|
|
||||||
SMangaUpdate(asyncManga?.await() ?: manga, asyncChapters?.await() ?: chapters)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
override suspend fun getPageList(chapter: SChapter): List<Page> = fetchPageList(chapter).awaitSingle()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable containing a page with a list of manga.
|
* Get a page with a list of latest manga updates.
|
||||||
*
|
*
|
||||||
|
* @since extensions-lib 1.5
|
||||||
* @param page the page number to retrieve.
|
* @param page the page number to retrieve.
|
||||||
*/
|
*/
|
||||||
@Deprecated("Use the suspend API instead", ReplaceWith("getPopularManga"))
|
@Suppress("DEPRECATION")
|
||||||
fun fetchPopularManga(page: Int): Observable<MangasPage> = throw UnsupportedOperationException()
|
suspend fun getLatestUpdates(page: Int): MangasPage = fetchLatestUpdates(page).awaitSingle()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable containing a page with a list of manga.
|
* Returns the list of filters for the source.
|
||||||
*
|
|
||||||
* @param page the page number to retrieve.
|
|
||||||
* @param query the search query.
|
|
||||||
* @param filters the list of filters to apply.
|
|
||||||
*/
|
*/
|
||||||
@Deprecated("Use the suspend API instead", ReplaceWith("getSearchManga"))
|
fun getFilterList(): FilterList
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
"Use the non-RxJava API instead",
|
||||||
|
ReplaceWith("getPopularManga"),
|
||||||
|
)
|
||||||
|
fun fetchPopularManga(page: Int): Observable<MangasPage> = throw IllegalStateException("Not used")
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
"Use the non-RxJava API instead",
|
||||||
|
ReplaceWith("getSearchManga"),
|
||||||
|
)
|
||||||
fun fetchSearchManga(
|
fun fetchSearchManga(
|
||||||
page: Int,
|
page: Int,
|
||||||
query: String,
|
query: String,
|
||||||
filters: FilterList,
|
filters: FilterList,
|
||||||
): Observable<MangasPage> = throw UnsupportedOperationException()
|
): Observable<MangasPage> = throw IllegalStateException("Not used")
|
||||||
|
|
||||||
/**
|
@Deprecated(
|
||||||
* Returns an observable containing a page with a list of latest manga updates.
|
"Use the non-RxJava API instead",
|
||||||
*
|
ReplaceWith("getLatestUpdates"),
|
||||||
* @param page the page number to retrieve.
|
)
|
||||||
*/
|
fun fetchLatestUpdates(page: Int): Observable<MangasPage> = throw IllegalStateException("Not used")
|
||||||
@Deprecated("Use the suspend API instead", ReplaceWith("getLatestUpdates"))
|
|
||||||
fun fetchLatestUpdates(page: Int): Observable<MangasPage> = throw UnsupportedOperationException()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.source
|
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
typealias PreferenceScreen = androidx.preference.PreferenceScreen
|
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
package eu.kanade.tachiyomi.source
|
package eu.kanade.tachiyomi.source
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.model.SMangaUpdate
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A basic interface for creating a source. It could be an online source, a local source, etc.
|
* A basic interface for creating a source. It could be an online source, a local source, etc.
|
||||||
@@ -26,86 +24,53 @@ interface Source {
|
|||||||
get() = ""
|
get() = ""
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the source has support for latest updates.
|
* Get the updated details for a manga.
|
||||||
|
*
|
||||||
|
* @since extensions-lib 1.5
|
||||||
|
* @param manga the manga to update.
|
||||||
|
* @return the updated manga.
|
||||||
*/
|
*/
|
||||||
val supportsLatest: Boolean
|
@Suppress("DEPRECATION")
|
||||||
|
suspend fun getMangaDetails(manga: SManga): SManga = fetchMangaDetails(manga).awaitSingle()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the list of filters for the source.
|
* Get all the available chapters for a manga.
|
||||||
|
*
|
||||||
|
* @since extensions-lib 1.5
|
||||||
|
* @param manga the manga to update.
|
||||||
|
* @return the chapters for the manga.
|
||||||
*/
|
*/
|
||||||
fun getFilterList(): FilterList = FilterList()
|
@Suppress("DEPRECATION")
|
||||||
|
suspend fun getChapterList(manga: SManga): List<SChapter> = fetchChapterList(manga).awaitSingle()
|
||||||
/**
|
|
||||||
* Get a page with a list of manga.
|
|
||||||
*
|
|
||||||
* @since tachiyomix 1.6
|
|
||||||
* @param page the page number to retrieve.
|
|
||||||
*/
|
|
||||||
suspend fun getPopularManga(page: Int): MangasPage
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a page with a list of latest manga updates.
|
|
||||||
*
|
|
||||||
* @since tachiyomix 1.6
|
|
||||||
* @param page the page number to retrieve.
|
|
||||||
*/
|
|
||||||
suspend fun getLatestUpdates(page: Int): MangasPage
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a page with a list of manga.
|
|
||||||
*
|
|
||||||
* @since tachiyomix 1.6
|
|
||||||
* @param page the page number to retrieve.
|
|
||||||
* @param query the search query.
|
|
||||||
* @param filters the list of filters to apply.
|
|
||||||
*/
|
|
||||||
suspend fun getSearchManga(
|
|
||||||
page: Int,
|
|
||||||
query: String,
|
|
||||||
filters: FilterList,
|
|
||||||
): MangasPage
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches updated information for a manga.
|
|
||||||
*
|
|
||||||
* Depending on the provided flags or source availability, this may include
|
|
||||||
* updated manga metadata, available chapters, or both.
|
|
||||||
*
|
|
||||||
* If a value is not requested, the existing provided value can be returned as-is.
|
|
||||||
* The host app may apply any returned updates regardless of the flags,
|
|
||||||
* so care should be taken to only return accurate and intentional changes.
|
|
||||||
*
|
|
||||||
* @since tachiyomix 1.6
|
|
||||||
* @param manga The manga to fetch updates for.
|
|
||||||
* @param chapters Existing chapters of the manga
|
|
||||||
* @param fetchDetails Whether to fetch updated manga details.
|
|
||||||
* @param fetchChapters Whether to fetch available chapters.
|
|
||||||
*/
|
|
||||||
suspend fun getMangaUpdate(
|
|
||||||
manga: SManga,
|
|
||||||
chapters: List<SChapter>,
|
|
||||||
fetchDetails: Boolean,
|
|
||||||
fetchChapters: Boolean,
|
|
||||||
): SMangaUpdate
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the list of pages a chapter has. Pages should be returned
|
* Get the list of pages a chapter has. Pages should be returned
|
||||||
* in the expected order; the index is ignored.
|
* in the expected order; the index is ignored.
|
||||||
*
|
*
|
||||||
* @since tachiyomix 1.6
|
* @since extensions-lib 1.5
|
||||||
* @param chapter the chapter.
|
* @param chapter the chapter.
|
||||||
* @return the pages for the chapter.
|
* @return the pages for the chapter.
|
||||||
*/
|
*/
|
||||||
suspend fun getPageList(chapter: SChapter): List<Page>
|
@Suppress("DEPRECATION")
|
||||||
|
suspend fun getPageList(chapter: SChapter): List<Page> = fetchPageList(chapter).awaitSingle()
|
||||||
|
|
||||||
@Deprecated("Use the combined suspend API instead", ReplaceWith("getMangaUpdate"))
|
@Deprecated(
|
||||||
fun fetchMangaDetails(manga: SManga): Observable<SManga> = throw UnsupportedOperationException()
|
"Use the non-RxJava API instead",
|
||||||
|
ReplaceWith("getMangaDetails"),
|
||||||
|
)
|
||||||
|
fun fetchMangaDetails(manga: SManga): Observable<SManga> = throw IllegalStateException("Not used")
|
||||||
|
|
||||||
@Deprecated("Use the combined suspend API instead", ReplaceWith("getMangaUpdate"))
|
@Deprecated(
|
||||||
fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = throw UnsupportedOperationException()
|
"Use the non-RxJava API instead",
|
||||||
|
ReplaceWith("getChapterList"),
|
||||||
|
)
|
||||||
|
fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = throw IllegalStateException("Not used")
|
||||||
|
|
||||||
@Deprecated("Use the suspend API instead", ReplaceWith("getPageList"))
|
@Deprecated(
|
||||||
fun fetchPageList(chapter: SChapter): Observable<List<Page>> = throw UnsupportedOperationException()
|
"Use the non-RxJava API instead",
|
||||||
|
ReplaceWith("getPageList"),
|
||||||
|
)
|
||||||
|
fun fetchPageList(chapter: SChapter): Observable<List<Page>> = throw IllegalStateException("Not used")
|
||||||
}
|
}
|
||||||
|
|
||||||
// fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)
|
// fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)
|
||||||
|
|||||||
@@ -23,15 +23,12 @@ import eu.kanade.tachiyomi.source.model.MangasPage
|
|||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.model.SMangaUpdate
|
|
||||||
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
||||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||||
import eu.kanade.tachiyomi.util.storage.EpubFile
|
import eu.kanade.tachiyomi.util.storage.EpubFile
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.supervisorScope
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
@@ -44,7 +41,7 @@ import org.jetbrains.exposed.v1.jdbc.insert
|
|||||||
import org.jetbrains.exposed.v1.jdbc.insertAndGetId
|
import org.jetbrains.exposed.v1.jdbc.insertAndGetId
|
||||||
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.manga.impl.util.source.GetSource.registerSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.registerCatalogueSource
|
||||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
|
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||||
@@ -170,20 +167,8 @@ class LocalSource(
|
|||||||
return MangasPage(mangas.toList(), false)
|
return MangasPage(mangas.toList(), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getMangaUpdate(
|
|
||||||
manga: SManga,
|
|
||||||
chapters: List<SChapter>,
|
|
||||||
fetchDetails: Boolean,
|
|
||||||
fetchChapters: Boolean,
|
|
||||||
): SMangaUpdate =
|
|
||||||
supervisorScope {
|
|
||||||
val asyncManga = if (fetchDetails) async { getMangaDetails(manga) } else null
|
|
||||||
val asyncChapters = if (fetchChapters) async { getChapterList(manga) } else null
|
|
||||||
SMangaUpdate(asyncManga?.await() ?: manga, asyncChapters?.await() ?: chapters)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Manga details related
|
// Manga details related
|
||||||
private suspend fun getMangaDetails(manga: SManga): SManga =
|
override suspend fun getMangaDetails(manga: SManga): SManga =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
coverManager.find(manga.url)?.let {
|
coverManager.find(manga.url)?.let {
|
||||||
manga.thumbnail_url = it.absolutePath
|
manga.thumbnail_url = it.absolutePath
|
||||||
@@ -304,7 +289,7 @@ class LocalSource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Chapters
|
// Chapters
|
||||||
private suspend fun getChapterList(manga: SManga): List<SChapter> =
|
override suspend fun getChapterList(manga: SManga): List<SChapter> =
|
||||||
fileSystem
|
fileSystem
|
||||||
.getFilesInMangaDirectory(manga.url)
|
.getFilesInMangaDirectory(manga.url)
|
||||||
// Only keep supported formats
|
// Only keep supported formats
|
||||||
@@ -482,8 +467,7 @@ class LocalSource(
|
|||||||
it[versionName] = "1.2"
|
it[versionName] = "1.2"
|
||||||
it[versionCode] = 0
|
it[versionCode] = 0
|
||||||
it[lang] = LANG
|
it[lang] = LANG
|
||||||
it[extensionLib] = "1.2"
|
it[isNsfw] = false
|
||||||
it[contentWarning] = 0
|
|
||||||
it[isInstalled] = true
|
it[isInstalled] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,12 +476,13 @@ class LocalSource(
|
|||||||
it[name] = NAME
|
it[name] = NAME
|
||||||
it[lang] = LANG
|
it[lang] = LANG
|
||||||
it[extension] = extensionId
|
it[extension] = extensionId
|
||||||
|
it[isNsfw] = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val fs = LocalSourceFileSystem(applicationDirs)
|
val fs = LocalSourceFileSystem(applicationDirs)
|
||||||
registerSource(ID to LocalSource(fs, LocalCoverManager(fs)))
|
registerCatalogueSource(ID to LocalSource(fs, LocalCoverManager(fs)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.source.model
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
class MangasPage(
|
data class MangasPage(
|
||||||
val mangas: List<SManga>,
|
val mangas: List<SManga>,
|
||||||
val hasNextPage: Boolean,
|
val hasNextPage: Boolean,
|
||||||
) {
|
)
|
||||||
@Deprecated("MangasPage is now a regular class")
|
|
||||||
operator fun component1(): List<SManga> = mangas
|
|
||||||
|
|
||||||
@Deprecated("MangasPage is now a regular class")
|
|
||||||
operator fun component2(): Boolean = hasNextPage
|
|
||||||
|
|
||||||
@Deprecated("MangasPage is now a regular class")
|
|
||||||
fun copy(
|
|
||||||
mangas: List<SManga> = this.mangas,
|
|
||||||
hasNextPage: Boolean = this.hasNextPage,
|
|
||||||
): MangasPage =
|
|
||||||
MangasPage(
|
|
||||||
mangas = mangas,
|
|
||||||
hasNextPage = hasNextPage,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -27,4 +27,12 @@ open class Page(
|
|||||||
-1
|
-1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val QUEUE = 0
|
||||||
|
const val LOAD_PAGE = 1
|
||||||
|
const val DOWNLOAD_IMAGE = 2
|
||||||
|
const val READY = 3
|
||||||
|
const val ERROR = 4
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
package eu.kanade.tachiyomi.source.model
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
interface SChapter : Serializable {
|
interface SChapter : Serializable {
|
||||||
@@ -10,25 +9,12 @@ interface SChapter : Serializable {
|
|||||||
|
|
||||||
var name: String
|
var name: String
|
||||||
|
|
||||||
|
var date_upload: Long
|
||||||
|
|
||||||
var chapter_number: Float
|
var chapter_number: Float
|
||||||
|
|
||||||
var scanlator: String?
|
var scanlator: String?
|
||||||
|
|
||||||
var date_upload: Long
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extra metadata associated with the chapter.
|
|
||||||
*
|
|
||||||
* The JSON object is not visible to users and intended for internal or source-specific
|
|
||||||
* purposes. Apps may define their own namespaced keys (e.g., `"mihon.*"`) for sources to populate.
|
|
||||||
*
|
|
||||||
* This allows apps to attach and ask for custom information without affecting the visible
|
|
||||||
* chapter data.
|
|
||||||
*
|
|
||||||
* @since tachiyomix 1.6
|
|
||||||
*/
|
|
||||||
var memo: JsonObject
|
|
||||||
|
|
||||||
fun copyFrom(other: SChapter) {
|
fun copyFrom(other: SChapter) {
|
||||||
name = other.name
|
name = other.name
|
||||||
url = other.url
|
url = other.url
|
||||||
|
|||||||
@@ -2,19 +2,14 @@
|
|||||||
|
|
||||||
package eu.kanade.tachiyomi.source.model
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import suwayomi.tachidesk.manga.impl.util.lang.EMPTY
|
|
||||||
|
|
||||||
class SChapterImpl : SChapter {
|
class SChapterImpl : SChapter {
|
||||||
override lateinit var url: String
|
override lateinit var url: String
|
||||||
|
|
||||||
override lateinit var name: String
|
override lateinit var name: String
|
||||||
|
|
||||||
|
override var date_upload: Long = 0
|
||||||
|
|
||||||
override var chapter_number: Float = -1f
|
override var chapter_number: Float = -1f
|
||||||
|
|
||||||
override var scanlator: String? = null
|
override var scanlator: String? = null
|
||||||
|
|
||||||
override var date_upload: Long = 0
|
|
||||||
|
|
||||||
override var memo: JsonObject = JsonObject.EMPTY
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
package eu.kanade.tachiyomi.source.model
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
interface SManga : Serializable {
|
interface SManga : Serializable {
|
||||||
@@ -10,58 +9,22 @@ interface SManga : Serializable {
|
|||||||
|
|
||||||
var title: String
|
var title: String
|
||||||
|
|
||||||
var thumbnail_url: String?
|
|
||||||
|
|
||||||
var artist: String?
|
var artist: String?
|
||||||
|
|
||||||
var author: String?
|
var author: String?
|
||||||
|
|
||||||
var status: Int
|
|
||||||
|
|
||||||
var description: String?
|
var description: String?
|
||||||
|
|
||||||
var genre: String?
|
var genre: String?
|
||||||
|
|
||||||
|
var status: Int
|
||||||
|
|
||||||
|
var thumbnail_url: String?
|
||||||
|
|
||||||
var update_strategy: UpdateStrategy
|
var update_strategy: UpdateStrategy
|
||||||
|
|
||||||
var initialized: Boolean
|
var initialized: Boolean
|
||||||
|
|
||||||
/**
|
|
||||||
* Extra metadata associated with the manga.
|
|
||||||
*
|
|
||||||
* The JSON object is not visible to users and intended for internal or source-specific
|
|
||||||
* purposes. Apps may define their own namespaced keys (e.g., `"mihon.*"`) for sources to populate.
|
|
||||||
*
|
|
||||||
* This allows apps to attach and ask for custom information without affecting the visible
|
|
||||||
* manga data.
|
|
||||||
*
|
|
||||||
* @since tachiyomix 1.6
|
|
||||||
*/
|
|
||||||
var memo: JsonObject
|
|
||||||
|
|
||||||
fun getGenres(): List<String>? {
|
|
||||||
if (genre.isNullOrBlank()) return null
|
|
||||||
return genre
|
|
||||||
?.split(", ")
|
|
||||||
?.map { it.trim() }
|
|
||||||
?.filterNot { it.isBlank() }
|
|
||||||
?.distinct()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun copy() =
|
|
||||||
create().also {
|
|
||||||
it.url = url
|
|
||||||
it.title = title
|
|
||||||
it.artist = artist
|
|
||||||
it.author = author
|
|
||||||
it.description = description
|
|
||||||
it.genre = genre
|
|
||||||
it.status = status
|
|
||||||
it.thumbnail_url = thumbnail_url
|
|
||||||
it.update_strategy = update_strategy
|
|
||||||
it.initialized = initialized
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val UNKNOWN = 0
|
const val UNKNOWN = 0
|
||||||
const val ONGOING = 1
|
const val ONGOING = 1
|
||||||
@@ -74,3 +37,30 @@ interface SManga : Serializable {
|
|||||||
fun create(): SManga = SMangaImpl()
|
fun create(): SManga = SMangaImpl()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fun SManga.toMangaInfo(): MangaInfo {
|
||||||
|
// return MangaInfo(
|
||||||
|
// key = this.url,
|
||||||
|
// title = this.title,
|
||||||
|
// artist = this.artist ?: "",
|
||||||
|
// author = this.author ?: "",
|
||||||
|
// description = this.description ?: "",
|
||||||
|
// genres = this.genre?.split(", ") ?: emptyList(),
|
||||||
|
// status = this.status,
|
||||||
|
// cover = this.thumbnail_url ?: ""
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// fun MangaInfo.toSManga(): SManga {
|
||||||
|
// val mangaInfo = this
|
||||||
|
// return SManga.create().apply {
|
||||||
|
// url = mangaInfo.key
|
||||||
|
// title = mangaInfo.title
|
||||||
|
// artist = mangaInfo.artist
|
||||||
|
// author = mangaInfo.author
|
||||||
|
// description = mangaInfo.description
|
||||||
|
// genre = mangaInfo.genres.joinToString(", ")
|
||||||
|
// status = mangaInfo.status
|
||||||
|
// thumbnail_url = mangaInfo.cover
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|||||||
@@ -2,29 +2,24 @@
|
|||||||
|
|
||||||
package eu.kanade.tachiyomi.source.model
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import suwayomi.tachidesk.manga.impl.util.lang.EMPTY
|
|
||||||
|
|
||||||
class SMangaImpl : SManga {
|
class SMangaImpl : SManga {
|
||||||
override lateinit var url: String
|
override lateinit var url: String
|
||||||
|
|
||||||
override lateinit var title: String
|
override lateinit var title: String
|
||||||
|
|
||||||
override var thumbnail_url: String? = null
|
|
||||||
|
|
||||||
override var artist: String? = null
|
override var artist: String? = null
|
||||||
|
|
||||||
override var author: String? = null
|
override var author: String? = null
|
||||||
|
|
||||||
override var status: Int = 0
|
|
||||||
|
|
||||||
override var description: String? = null
|
override var description: String? = null
|
||||||
|
|
||||||
override var genre: String? = null
|
override var genre: String? = null
|
||||||
|
|
||||||
|
override var status: Int = 0
|
||||||
|
|
||||||
|
override var thumbnail_url: String? = null
|
||||||
|
|
||||||
override var update_strategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE
|
override var update_strategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE
|
||||||
|
|
||||||
override var initialized: Boolean = false
|
override var initialized: Boolean = false
|
||||||
|
|
||||||
override var memo: JsonObject = JsonObject.EMPTY
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.source.model
|
|
||||||
|
|
||||||
@Suppress("UNUSED")
|
|
||||||
class SMangaUpdate(
|
|
||||||
val manga: SManga,
|
|
||||||
val chapters: List<SChapter>,
|
|
||||||
)
|
|
||||||
@@ -1,22 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.source.model
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
/**
|
|
||||||
* Define the update strategy for a single [SManga].
|
|
||||||
* The strategy used will only take effect on the library update.
|
|
||||||
*
|
|
||||||
* @since extensions-lib 1.4
|
|
||||||
*/
|
|
||||||
enum class UpdateStrategy {
|
enum class UpdateStrategy {
|
||||||
/**
|
|
||||||
* Series marked as always update will be included in the library
|
|
||||||
* update if they aren't excluded by additional restrictions.
|
|
||||||
*/
|
|
||||||
ALWAYS_UPDATE,
|
ALWAYS_UPDATE,
|
||||||
|
|
||||||
/**
|
|
||||||
* Series marked as only fetch once will be automatically skipped
|
|
||||||
* during library updates. Useful for cases where the series is previously
|
|
||||||
* known to be finished and have only a single chapter, for example.
|
|
||||||
*/
|
|
||||||
ONLY_FETCH_ONCE,
|
ONLY_FETCH_ONCE,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import java.security.MessageDigest
|
|||||||
/**
|
/**
|
||||||
* A simple implementation for sources from a website.
|
* A simple implementation for sources from a website.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
abstract class HttpSource : CatalogueSource {
|
abstract class HttpSource : CatalogueSource {
|
||||||
/**
|
/**
|
||||||
* Network service.
|
* Network service.
|
||||||
@@ -36,24 +37,11 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*/
|
*/
|
||||||
abstract val baseUrl: String
|
abstract val baseUrl: String
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the base (home) URL of the website as a string.
|
|
||||||
*
|
|
||||||
* This is typically the root address that serves as the main entry point
|
|
||||||
* to the site's content, such as "https://mihon.tech".
|
|
||||||
*
|
|
||||||
* This method is used in the browse screen to determine the URL
|
|
||||||
* opened when tapping "Open in WebView".
|
|
||||||
*
|
|
||||||
* @return The website’s home page URL. Defaults to [baseUrl].
|
|
||||||
*/
|
|
||||||
open fun getHomeUrl(): String = baseUrl
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Version id used to generate the source id. If the site completely changes and urls are
|
* Version id used to generate the source id. If the site completely changes and urls are
|
||||||
* incompatible, you may increase this value and it'll be considered as a new source.
|
* incompatible, you may increase this value and it'll be considered as a new source.
|
||||||
*/
|
*/
|
||||||
open val versionId: Int = 1
|
open val versionId = 1
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ID of the source. By default it uses a generated id using the first 16 characters (64 bits)
|
* ID of the source. By default it uses a generated id using the first 16 characters (64 bits)
|
||||||
@@ -65,7 +53,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* Note: the generated ID sets the sign bit to `0`.
|
* Note: the generated ID sets the sign bit to `0`.
|
||||||
*/
|
*/
|
||||||
override val id: Long by lazy { generateId(name, lang, versionId) }
|
override val id by lazy { generateId() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Headers used for requests.
|
* Headers used for requests.
|
||||||
@@ -75,7 +63,10 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
/**
|
/**
|
||||||
* Default network client for doing requests.
|
* Default network client for doing requests.
|
||||||
*/
|
*/
|
||||||
open val client: OkHttpClient get() = network.client
|
open val client: OkHttpClient
|
||||||
|
get() = network.client
|
||||||
|
|
||||||
|
private fun generateId(): Long = generateId("${name.lowercase()}/$lang/$versionId")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a unique ID for the source based on the provided [name], [lang] and
|
* Generates a unique ID for the source based on the provided [name], [lang] and
|
||||||
@@ -100,6 +91,10 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
versionId: Int,
|
versionId: Int,
|
||||||
): Long {
|
): Long {
|
||||||
val key = "${name.lowercase()}/$lang/$versionId"
|
val key = "${name.lowercase()}/$lang/$versionId"
|
||||||
|
return generateId(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateId(key: String): Long {
|
||||||
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
||||||
return (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
return (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
||||||
}
|
}
|
||||||
@@ -107,7 +102,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
/**
|
/**
|
||||||
* Headers builder for requests. Implementations can override this method for custom headers.
|
* Headers builder for requests. Implementations can override this method for custom headers.
|
||||||
*/
|
*/
|
||||||
protected open fun headersBuilder(): Headers.Builder =
|
protected open fun headersBuilder() =
|
||||||
Headers.Builder().apply {
|
Headers.Builder().apply {
|
||||||
add("User-Agent", network.defaultUserAgentProvider())
|
add("User-Agent", network.defaultUserAgentProvider())
|
||||||
}
|
}
|
||||||
@@ -115,7 +110,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
/**
|
/**
|
||||||
* Visible name of the source.
|
* Visible name of the source.
|
||||||
*/
|
*/
|
||||||
override fun toString(): String = "$name (${lang.uppercase()})"
|
override fun toString() = "$name (${lang.uppercase()})"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||||
@@ -123,8 +118,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* @param page the page number to retrieve.
|
* @param page the page number to retrieve.
|
||||||
*/
|
*/
|
||||||
@Suppress("DEPRECATION")
|
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga"))
|
||||||
@Deprecated("Use the suspend API instead", ReplaceWith("getPopularManga"))
|
|
||||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> =
|
override fun fetchPopularManga(page: Int): Observable<MangasPage> =
|
||||||
client
|
client
|
||||||
.newCall(popularMangaRequest(page))
|
.newCall(popularMangaRequest(page))
|
||||||
@@ -138,24 +132,14 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* @param page the page number to retrieve.
|
* @param page the page number to retrieve.
|
||||||
*/
|
*/
|
||||||
@Deprecated(
|
protected abstract fun popularMangaRequest(page: Int): Request
|
||||||
message =
|
|
||||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
|
||||||
"Source developers should make their own implementation according to their needs.",
|
|
||||||
)
|
|
||||||
protected open fun popularMangaRequest(page: Int): Request = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the response from the site and returns a [MangasPage] object.
|
* Parses the response from the site and returns a [MangasPage] object.
|
||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
@Deprecated(
|
protected abstract fun popularMangaParse(response: Response): MangasPage
|
||||||
message =
|
|
||||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
|
||||||
"Source developers should make their own implementation according to their needs.",
|
|
||||||
)
|
|
||||||
protected open fun popularMangaParse(response: Response): MangasPage = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||||
@@ -165,17 +149,22 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
* @param query the search query.
|
* @param query the search query.
|
||||||
* @param filters the list of filters to apply.
|
* @param filters the list of filters to apply.
|
||||||
*/
|
*/
|
||||||
@Suppress("DEPRECATION")
|
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga"))
|
||||||
@Deprecated("Use the suspend API instead", ReplaceWith("getSearchManga"))
|
|
||||||
override fun fetchSearchManga(
|
override fun fetchSearchManga(
|
||||||
page: Int,
|
page: Int,
|
||||||
query: String,
|
query: String,
|
||||||
filters: FilterList,
|
filters: FilterList,
|
||||||
): Observable<MangasPage> =
|
): Observable<MangasPage> =
|
||||||
client
|
Observable
|
||||||
.newCall(searchMangaRequest(page, query, filters))
|
.defer {
|
||||||
.asObservableSuccess()
|
try {
|
||||||
.map { response ->
|
client.newCall(searchMangaRequest(page, query, filters)).asObservableSuccess()
|
||||||
|
} catch (e: NoClassDefFoundError) {
|
||||||
|
// RxJava doesn't handle Errors, which tends to happen during global searches
|
||||||
|
// if an old extension using non-existent classes is still around
|
||||||
|
throw RuntimeException(e)
|
||||||
|
}
|
||||||
|
}.map { response ->
|
||||||
searchMangaParse(response)
|
searchMangaParse(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,36 +175,25 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
* @param query the search query.
|
* @param query the search query.
|
||||||
* @param filters the list of filters to apply.
|
* @param filters the list of filters to apply.
|
||||||
*/
|
*/
|
||||||
@Deprecated(
|
protected abstract fun searchMangaRequest(
|
||||||
message =
|
|
||||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
|
||||||
"Source developers should make their own implementation according to their needs.",
|
|
||||||
)
|
|
||||||
protected open fun searchMangaRequest(
|
|
||||||
page: Int,
|
page: Int,
|
||||||
query: String,
|
query: String,
|
||||||
filters: FilterList,
|
filters: FilterList,
|
||||||
): Request = throw UnsupportedOperationException()
|
): Request
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the response from the site and returns a [MangasPage] object.
|
* Parses the response from the site and returns a [MangasPage] object.
|
||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
@Deprecated(
|
protected abstract fun searchMangaParse(response: Response): MangasPage
|
||||||
message =
|
|
||||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
|
||||||
"Source developers should make their own implementation according to their needs.",
|
|
||||||
)
|
|
||||||
protected open fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable containing a page with a list of latest manga updates.
|
* Returns an observable containing a page with a list of latest manga updates.
|
||||||
*
|
*
|
||||||
* @param page the page number to retrieve.
|
* @param page the page number to retrieve.
|
||||||
*/
|
*/
|
||||||
@Suppress("DEPRECATION")
|
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates"))
|
||||||
@Deprecated("Use the suspend API instead", ReplaceWith("getLatestUpdates"))
|
|
||||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> =
|
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> =
|
||||||
client
|
client
|
||||||
.newCall(latestUpdatesRequest(page))
|
.newCall(latestUpdatesRequest(page))
|
||||||
@@ -229,33 +207,26 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* @param page the page number to retrieve.
|
* @param page the page number to retrieve.
|
||||||
*/
|
*/
|
||||||
@Deprecated(
|
protected abstract fun latestUpdatesRequest(page: Int): Request
|
||||||
message =
|
|
||||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
|
||||||
"Source developers should make their own implementation according to their needs.",
|
|
||||||
)
|
|
||||||
protected open fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the response from the site and returns a [MangasPage] object.
|
* Parses the response from the site and returns a [MangasPage] object.
|
||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
@Deprecated(
|
protected abstract fun latestUpdatesParse(response: Response): MangasPage
|
||||||
message =
|
|
||||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
|
||||||
"Source developers should make their own implementation according to their needs.",
|
|
||||||
)
|
|
||||||
protected open fun latestUpdatesParse(response: Response): MangasPage = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
* Get the updated details for a manga.
|
||||||
* override this method.
|
* Normally it's not needed to override this method.
|
||||||
*
|
*
|
||||||
* @param manga the manga to be updated.
|
* @param manga the manga to update.
|
||||||
|
* @return the updated manga.
|
||||||
*/
|
*/
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
@Deprecated("Use the combined suspend API instead", replaceWith = ReplaceWith("getMangaUpdate"))
|
override suspend fun getMangaDetails(manga: SManga): SManga = fetchMangaDetails(manga).awaitSingle()
|
||||||
|
|
||||||
|
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails"))
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> =
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> =
|
||||||
client
|
client
|
||||||
.newCall(mangaDetailsRequest(manga))
|
.newCall(mangaDetailsRequest(manga))
|
||||||
@@ -270,11 +241,6 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* @param manga the manga to be updated.
|
* @param manga the manga to be updated.
|
||||||
*/
|
*/
|
||||||
@Deprecated(
|
|
||||||
message =
|
|
||||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
|
||||||
"Source developers should make their own implementation according to their needs.",
|
|
||||||
)
|
|
||||||
open fun mangaDetailsRequest(manga: SManga): Request = GET(baseUrl + manga.url, headers)
|
open fun mangaDetailsRequest(manga: SManga): Request = GET(baseUrl + manga.url, headers)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -282,28 +248,37 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
@Deprecated(
|
protected abstract fun mangaDetailsParse(response: Response): SManga
|
||||||
message =
|
|
||||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
|
||||||
"Source developers should make their own implementation according to their needs.",
|
|
||||||
)
|
|
||||||
protected open fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable with the updated chapter list for a manga. Normally it's not needed to
|
* Get all the available chapters for a manga.
|
||||||
* override this method.
|
* Normally it's not needed to override this method.
|
||||||
*
|
*
|
||||||
* @param manga the manga to look for chapters.
|
* @param manga the manga to update.
|
||||||
|
* @return the chapters for the manga.
|
||||||
|
* @throws LicensedMangaChaptersException if a manga is licensed and therefore no chapters are available.
|
||||||
*/
|
*/
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
@Deprecated("Use the combined suspend API instead", replaceWith = ReplaceWith("getMangaUpdate"))
|
override suspend fun getChapterList(manga: SManga): List<SChapter> {
|
||||||
|
if (manga.status == SManga.LICENSED) {
|
||||||
|
throw LicensedMangaChaptersException()
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchChapterList(manga).awaitSingle()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList"))
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> =
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> =
|
||||||
client
|
if (manga.status != SManga.LICENSED) {
|
||||||
.newCall(chapterListRequest(manga))
|
client
|
||||||
.asObservableSuccess()
|
.newCall(chapterListRequest(manga))
|
||||||
.map { response ->
|
.asObservableSuccess()
|
||||||
chapterListParse(response)
|
.map { response ->
|
||||||
}
|
chapterListParse(response)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Observable.error(LicensedMangaChaptersException())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the request for updating the chapter list. Override only if it's needed to override
|
* Returns the request for updating the chapter list. Override only if it's needed to override
|
||||||
@@ -311,11 +286,6 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* @param manga the manga to look for chapters.
|
* @param manga the manga to look for chapters.
|
||||||
*/
|
*/
|
||||||
@Deprecated(
|
|
||||||
message =
|
|
||||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
|
||||||
"Source developers should make their own implementation according to their needs.",
|
|
||||||
)
|
|
||||||
protected open fun chapterListRequest(manga: SManga): Request = GET(baseUrl + manga.url, headers)
|
protected open fun chapterListRequest(manga: SManga): Request = GET(baseUrl + manga.url, headers)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -323,20 +293,19 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
@Deprecated(
|
protected abstract fun chapterListParse(response: Response): List<SChapter>
|
||||||
message =
|
|
||||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
|
||||||
"Source developers should make their own implementation according to their needs.",
|
|
||||||
)
|
|
||||||
protected open fun chapterListParse(response: Response): List<SChapter> = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable with the page list for a chapter.
|
* Get the list of pages a chapter has. Pages should be returned
|
||||||
|
* in the expected order; the index is ignored.
|
||||||
*
|
*
|
||||||
* @param chapter the chapter whose page list has to be fetched.
|
* @param chapter the chapter.
|
||||||
|
* @return the pages for the chapter.
|
||||||
*/
|
*/
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
@Deprecated("Use the suspend API instead", ReplaceWith("getPageList"))
|
override suspend fun getPageList(chapter: SChapter): List<Page> = fetchPageList(chapter).awaitSingle()
|
||||||
|
|
||||||
|
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList"))
|
||||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> =
|
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> =
|
||||||
client
|
client
|
||||||
.newCall(pageListRequest(chapter))
|
.newCall(pageListRequest(chapter))
|
||||||
@@ -351,11 +320,6 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* @param chapter the chapter whose page list has to be fetched.
|
* @param chapter the chapter whose page list has to be fetched.
|
||||||
*/
|
*/
|
||||||
@Deprecated(
|
|
||||||
message =
|
|
||||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
|
||||||
"Source developers should make their own implementation according to their needs.",
|
|
||||||
)
|
|
||||||
protected open fun pageListRequest(chapter: SChapter): Request = GET(baseUrl + chapter.url, headers)
|
protected open fun pageListRequest(chapter: SChapter): Request = GET(baseUrl + chapter.url, headers)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -363,47 +327,31 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
@Deprecated(
|
protected abstract fun pageListParse(response: Response): List<Page>
|
||||||
message =
|
|
||||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
|
||||||
"Source developers should make their own implementation according to their needs.",
|
|
||||||
)
|
|
||||||
protected open fun pageListParse(response: Response): List<Page> = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable with the page containing the source url of the image. If there's any
|
* Returns an observable with the page containing the source url of the image. If there's any
|
||||||
* error, it will return null instead of throwing an exception.
|
* error, it will return null instead of throwing an exception.
|
||||||
*
|
*
|
||||||
|
* @since extensions-lib 1.5
|
||||||
* @param page the page whose source image has to be fetched.
|
* @param page the page whose source image has to be fetched.
|
||||||
*/
|
*/
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
@Deprecated("Use the suspend API instead", ReplaceWith("getImageUrl"))
|
open suspend fun getImageUrl(page: Page): String = fetchImageUrl(page).awaitSingle()
|
||||||
|
|
||||||
|
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl"))
|
||||||
open fun fetchImageUrl(page: Page): Observable<String> =
|
open fun fetchImageUrl(page: Page): Observable<String> =
|
||||||
client
|
client
|
||||||
.newCall(imageUrlRequest(page))
|
.newCall(imageUrlRequest(page))
|
||||||
.asObservableSuccess()
|
.asObservableSuccess()
|
||||||
.map { imageUrlParse(it) }
|
.map { imageUrlParse(it) }
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the image url for the provided [page]. The function is only called if [Page.imageUrl] is null.
|
|
||||||
*
|
|
||||||
* @since tachiyomix 1.6
|
|
||||||
* @param page the page whose source image has to be fetched.
|
|
||||||
*/
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
open suspend fun getImageUrl(page: Page): String = fetchImageUrl(page).awaitSingle()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the request for getting the url to the source image. Override only if it's needed to
|
* Returns the request for getting the url to the source image. Override only if it's needed to
|
||||||
* override the url, send different headers or request method like POST.
|
* override the url, send different headers or request method like POST.
|
||||||
*
|
*
|
||||||
* @param page the chapter whose page list has to be fetched
|
* @param page the chapter whose page list has to be fetched
|
||||||
*/
|
*/
|
||||||
@Deprecated(
|
|
||||||
message =
|
|
||||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
|
||||||
"Source developers should make their own implementation according to their needs.",
|
|
||||||
)
|
|
||||||
protected open fun imageUrlRequest(page: Page): Request = GET(page.url, headers)
|
protected open fun imageUrlRequest(page: Page): Request = GET(page.url, headers)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -411,14 +359,16 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
@Deprecated(
|
protected abstract fun imageUrlParse(response: Response): String
|
||||||
message =
|
|
||||||
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
|
||||||
"Source developers should make their own implementation according to their needs.",
|
|
||||||
)
|
|
||||||
protected open fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
suspend fun getImage(page: Page): Response =
|
/**
|
||||||
|
* Returns the response of the source image.
|
||||||
|
* Typically does not need to be overridden.
|
||||||
|
*
|
||||||
|
* @since extensions-lib 1.5
|
||||||
|
* @param page the page whose source image has to be downloaded.
|
||||||
|
*/
|
||||||
|
open suspend fun getImage(page: Page): Response =
|
||||||
client
|
client
|
||||||
.newCachelessCallWithProgress(imageRequest(page), page)
|
.newCachelessCallWithProgress(imageRequest(page), page)
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
@@ -437,7 +387,6 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* @param url the full url to the chapter.
|
* @param url the full url to the chapter.
|
||||||
*/
|
*/
|
||||||
@Suppress("Unused")
|
|
||||||
fun SChapter.setUrlWithoutDomain(url: String) {
|
fun SChapter.setUrlWithoutDomain(url: String) {
|
||||||
this.url = getUrlWithoutDomain(url)
|
this.url = getUrlWithoutDomain(url)
|
||||||
}
|
}
|
||||||
@@ -448,7 +397,6 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* @param url the full url to the manga.
|
* @param url the full url to the manga.
|
||||||
*/
|
*/
|
||||||
@Suppress("Unused")
|
|
||||||
fun SManga.setUrlWithoutDomain(url: String) {
|
fun SManga.setUrlWithoutDomain(url: String) {
|
||||||
this.url = getUrlWithoutDomain(url)
|
this.url = getUrlWithoutDomain(url)
|
||||||
}
|
}
|
||||||
@@ -469,7 +417,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
out += "#" + uri.fragment
|
out += "#" + uri.fragment
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
} catch (_: URISyntaxException) {
|
} catch (e: URISyntaxException) {
|
||||||
orig
|
orig
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,7 +428,6 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
* @param manga the manga
|
* @param manga the manga
|
||||||
* @return url of the manga
|
* @return url of the manga
|
||||||
*/
|
*/
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
open fun getMangaUrl(manga: SManga): String = mangaDetailsRequest(manga).url.toString()
|
open fun getMangaUrl(manga: SManga): String = mangaDetailsRequest(manga).url.toString()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -490,7 +437,6 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
* @param chapter the chapter
|
* @param chapter the chapter
|
||||||
* @return url of the chapter
|
* @return url of the chapter
|
||||||
*/
|
*/
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
open fun getChapterUrl(chapter: SChapter): String = pageListRequest(chapter).url.toString()
|
open fun getChapterUrl(chapter: SChapter): String = pageListRequest(chapter).url.toString()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -500,9 +446,15 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
* @param chapter the chapter to be added.
|
* @param chapter the chapter to be added.
|
||||||
* @param manga the manga of the chapter.
|
* @param manga the manga of the chapter.
|
||||||
*/
|
*/
|
||||||
@Deprecated("All modifications should be done when constructing the chapter")
|
|
||||||
open fun prepareNewChapter(
|
open fun prepareNewChapter(
|
||||||
chapter: SChapter,
|
chapter: SChapter,
|
||||||
manga: SManga,
|
manga: SManga,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of filters for the source.
|
||||||
|
*/
|
||||||
|
override fun getFilterList() = FilterList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LicensedMangaChaptersException : Exception("Licensed - No chapters to show")
|
||||||
|
|||||||
@@ -12,20 +12,12 @@ import org.jsoup.nodes.Element
|
|||||||
/**
|
/**
|
||||||
* A simple implementation for sources from a website using Jsoup, an HTML parser.
|
* A simple implementation for sources from a website using Jsoup, an HTML parser.
|
||||||
*/
|
*/
|
||||||
@Deprecated(
|
|
||||||
message =
|
|
||||||
"In most cases sources only require a subset of the methods from this class. " +
|
|
||||||
"Source developers should make their own implementation according to their needs.",
|
|
||||||
)
|
|
||||||
abstract class ParsedHttpSource : HttpSource() {
|
abstract class ParsedHttpSource : HttpSource() {
|
||||||
/**
|
/**
|
||||||
* Parses the response from the site and returns a [MangasPage] object.
|
* Parses the response from the site and returns a [MangasPage] object.
|
||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
@Deprecated(
|
|
||||||
"The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.",
|
|
||||||
)
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
|
|
||||||
@@ -66,9 +58,6 @@ abstract class ParsedHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
@Deprecated(
|
|
||||||
"The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.",
|
|
||||||
)
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
|
|
||||||
@@ -109,9 +98,6 @@ abstract class ParsedHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
@Deprecated(
|
|
||||||
"The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.",
|
|
||||||
)
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
|
|
||||||
@@ -152,9 +138,6 @@ abstract class ParsedHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
@Deprecated(
|
|
||||||
"The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.",
|
|
||||||
)
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga = mangaDetailsParse(response.asJsoup())
|
override fun mangaDetailsParse(response: Response): SManga = mangaDetailsParse(response.asJsoup())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -169,9 +152,6 @@ abstract class ParsedHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
@Deprecated(
|
|
||||||
"The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.",
|
|
||||||
)
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
return document.select(chapterListSelector()).map { chapterFromElement(it) }
|
return document.select(chapterListSelector()).map { chapterFromElement(it) }
|
||||||
@@ -194,9 +174,6 @@ abstract class ParsedHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
@Deprecated(
|
|
||||||
"The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.",
|
|
||||||
)
|
|
||||||
override fun pageListParse(response: Response): List<Page> = pageListParse(response.asJsoup())
|
override fun pageListParse(response: Response): List<Page> = pageListParse(response.asJsoup())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -211,9 +188,6 @@ abstract class ParsedHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
@Deprecated(
|
|
||||||
"The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.",
|
|
||||||
)
|
|
||||||
override fun imageUrlParse(response: Response): String = imageUrlParse(response.asJsoup())
|
override fun imageUrlParse(response: Response): String = imageUrlParse(response.asJsoup())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,44 +1,26 @@
|
|||||||
package eu.kanade.tachiyomi.source.online
|
package eu.kanade.tachiyomi.source.online
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A source that may handle opening an SManga or SChapter for a given URI.
|
* A source that may handle opening an SManga for a given URI.
|
||||||
*
|
*
|
||||||
* @since extensions-lib 1.5
|
* @since extensions-lib 1.5
|
||||||
*/
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
interface ResolvableSource : Source {
|
interface ResolvableSource : Source {
|
||||||
/**
|
/**
|
||||||
* Returns what the given URI may open.
|
* Whether this source may potentially handle the given URI.
|
||||||
* Returns [UriType.Unknown] if the source is not able to resolve the URI.
|
|
||||||
*
|
*
|
||||||
* @since extensions-lib 1.5
|
* @since extensions-lib 1.5
|
||||||
*/
|
*/
|
||||||
fun getUriType(uri: String): UriType
|
fun canResolveUri(uri: String): Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called if [getUriType] is [UriType.Manga].
|
* Called if canHandleUri is true. Returns the corresponding SManga, if possible.
|
||||||
* Returns the corresponding SManga, if possible.
|
|
||||||
*
|
*
|
||||||
* @since extensions-lib 1.5
|
* @since extensions-lib 1.5
|
||||||
*/
|
*/
|
||||||
suspend fun getManga(uri: String): SManga?
|
suspend fun getManga(uri: String): SManga?
|
||||||
|
|
||||||
/**
|
|
||||||
* Called if [getUriType] is [UriType.Chapter].
|
|
||||||
* Returns the corresponding SChapter, if possible.
|
|
||||||
*
|
|
||||||
* @since extensions-lib 1.5
|
|
||||||
*/
|
|
||||||
suspend fun getChapter(uri: String): SChapter?
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed interface UriType {
|
|
||||||
data object Manga : UriType
|
|
||||||
|
|
||||||
data object Chapter : UriType
|
|
||||||
|
|
||||||
data object Unknown : UriType
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -305,7 +305,8 @@ object SyncYomiSyncService {
|
|||||||
|
|
||||||
logger.debug { "Starting merge. Local list size: ${localMangaListSafe.size}, Remote list size: ${remoteMangaListSafe.size}" }
|
logger.debug { "Starting merge. Local list size: ${localMangaListSafe.size}, Remote list size: ${remoteMangaListSafe.size}" }
|
||||||
|
|
||||||
fun mangaCompositeKey(manga: BackupManga): String = "${manga.source}|${manga.url}"
|
fun mangaCompositeKey(manga: BackupManga): String =
|
||||||
|
"${manga.source}|${manga.url}|${manga.title.lowercase().trim()}|${manga.author?.lowercase()?.trim()}"
|
||||||
|
|
||||||
// Create maps using composite keys
|
// Create maps using composite keys
|
||||||
val localMangaMap = localMangaListSafe.associateBy { mangaCompositeKey(it) }
|
val localMangaMap = localMangaListSafe.associateBy { mangaCompositeKey(it) }
|
||||||
@@ -414,7 +415,7 @@ object SyncYomiSyncService {
|
|||||||
return remoteChapters // If not syncing chapters, keep remote untouched
|
return remoteChapters // If not syncing chapters, keep remote untouched
|
||||||
}
|
}
|
||||||
|
|
||||||
fun chapterCompositeKey(chapter: BackupChapter): String = chapter.url
|
fun chapterCompositeKey(chapter: BackupChapter): String = "${chapter.url}|${chapter.name}|${chapter.chapterNumber}"
|
||||||
|
|
||||||
val localChapterMap = localChapters.associateBy { chapterCompositeKey(it) }
|
val localChapterMap = localChapters.associateBy { chapterCompositeKey(it) }
|
||||||
val remoteChapterMap = remoteChapters.associateBy { chapterCompositeKey(it) }
|
val remoteChapterMap = remoteChapters.associateBy { chapterCompositeKey(it) }
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
package suwayomi.tachidesk.graphql.dataLoaders
|
|
||||||
|
|
||||||
import com.expediagroup.graphql.dataloader.KotlinDataLoader
|
|
||||||
import graphql.GraphQLContext
|
|
||||||
import org.dataloader.DataLoader
|
|
||||||
import org.dataloader.DataLoaderFactory
|
|
||||||
import org.jetbrains.exposed.v1.core.Slf4jSqlDebugLogger
|
|
||||||
import org.jetbrains.exposed.v1.core.inList
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
|
||||||
import suwayomi.tachidesk.graphql.types.ExtensionNodeList
|
|
||||||
import suwayomi.tachidesk.graphql.types.ExtensionNodeList.Companion.toNodeList
|
|
||||||
import suwayomi.tachidesk.graphql.types.ExtensionStoreType
|
|
||||||
import suwayomi.tachidesk.graphql.types.ExtensionType
|
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionStoreTable
|
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
|
||||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
|
||||||
|
|
||||||
class ExtensionStoreDataLoader : KotlinDataLoader<String, ExtensionStoreType> {
|
|
||||||
override val dataLoaderName = "ExtensionStoreDataLoader"
|
|
||||||
|
|
||||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, ExtensionStoreType> =
|
|
||||||
DataLoaderFactory.newDataLoader { ids ->
|
|
||||||
future {
|
|
||||||
transaction {
|
|
||||||
addLogger(Slf4jSqlDebugLogger)
|
|
||||||
val extensionStoreByIndexUrl =
|
|
||||||
ExtensionStoreTable
|
|
||||||
.selectAll()
|
|
||||||
.where { ExtensionStoreTable.indexUrl inList ids }
|
|
||||||
.map { ExtensionStoreType(it) }
|
|
||||||
.associateBy { it.indexUrl }
|
|
||||||
ids.map { extensionStoreByIndexUrl[it] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ExtensionsForExtensionStore : KotlinDataLoader<String, ExtensionNodeList> {
|
|
||||||
override val dataLoaderName = "ExtensionsForExtensionStore"
|
|
||||||
|
|
||||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, ExtensionNodeList> =
|
|
||||||
DataLoaderFactory.newDataLoader<String, ExtensionNodeList> { ids ->
|
|
||||||
future {
|
|
||||||
transaction {
|
|
||||||
addLogger(Slf4jSqlDebugLogger)
|
|
||||||
val extensionByIndexUrl =
|
|
||||||
ExtensionTable
|
|
||||||
.selectAll()
|
|
||||||
.where { ExtensionTable.storeIndexUrl inList ids }
|
|
||||||
.map { ExtensionType(it) }
|
|
||||||
.groupBy { it.storeIndexUrl }
|
|
||||||
ids.map { (extensionByIndexUrl[it] ?: emptyList()).toNodeList() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
package suwayomi.tachidesk.graphql.mutations
|
package suwayomi.tachidesk.graphql.mutations
|
||||||
|
|
||||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jetbrains.exposed.v1.core.LikePattern
|
import org.jetbrains.exposed.v1.core.LikePattern
|
||||||
@@ -26,7 +25,6 @@ import suwayomi.tachidesk.graphql.types.ChapterType
|
|||||||
import suwayomi.tachidesk.graphql.types.MetaInput
|
import suwayomi.tachidesk.graphql.types.MetaInput
|
||||||
import suwayomi.tachidesk.graphql.types.SyncConflictInfoType
|
import suwayomi.tachidesk.graphql.types.SyncConflictInfoType
|
||||||
import suwayomi.tachidesk.manga.impl.Chapter
|
import suwayomi.tachidesk.manga.impl.Chapter
|
||||||
import suwayomi.tachidesk.manga.impl.Manga
|
|
||||||
import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReadyById
|
import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReadyById
|
||||||
import suwayomi.tachidesk.manga.impl.sync.KoreaderSyncService
|
import suwayomi.tachidesk.manga.impl.sync.KoreaderSyncService
|
||||||
import suwayomi.tachidesk.manga.model.table.ChapterMetaTable
|
import suwayomi.tachidesk.manga.model.table.ChapterMetaTable
|
||||||
@@ -169,12 +167,11 @@ class ChapterMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
@GraphQLDeprecated("Deprecated in Tachiyomix 1.6", ReplaceWith("fetchMangaAndChapters"))
|
|
||||||
fun fetchChapters(input: FetchChaptersInput): CompletableFuture<FetchChaptersPayload?> {
|
fun fetchChapters(input: FetchChaptersInput): CompletableFuture<FetchChaptersPayload?> {
|
||||||
val (clientMutationId, mangaId) = input
|
val (clientMutationId, mangaId) = input
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
Manga.updateMangaAndChapters(mangaId, updateManga = false)
|
Chapter.fetchChapterList(mangaId)
|
||||||
|
|
||||||
val chapters =
|
val chapters =
|
||||||
transaction {
|
transaction {
|
||||||
|
|||||||
@@ -10,11 +10,9 @@ 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.types.ExtensionStoreType
|
|
||||||
import suwayomi.tachidesk.graphql.types.ExtensionType
|
import suwayomi.tachidesk.graphql.types.ExtensionType
|
||||||
import suwayomi.tachidesk.manga.impl.extension.Extension
|
import suwayomi.tachidesk.manga.impl.extension.Extension
|
||||||
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList
|
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionStoreTable
|
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
@@ -131,7 +129,6 @@ class ExtensionMutation {
|
|||||||
data class FetchExtensionsPayload(
|
data class FetchExtensionsPayload(
|
||||||
val clientMutationId: String?,
|
val clientMutationId: String?,
|
||||||
val extensions: List<ExtensionType>,
|
val extensions: List<ExtensionType>,
|
||||||
val extensionStores: List<ExtensionStoreType>,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
@@ -149,17 +146,9 @@ class ExtensionMutation {
|
|||||||
.map { ExtensionType(it) }
|
.map { ExtensionType(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
val extensionStores =
|
|
||||||
transaction {
|
|
||||||
ExtensionStoreTable
|
|
||||||
.selectAll()
|
|
||||||
.map { ExtensionStoreType(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
FetchExtensionsPayload(
|
FetchExtensionsPayload(
|
||||||
clientMutationId = clientMutationId,
|
clientMutationId = clientMutationId,
|
||||||
extensions = extensions,
|
extensions = extensions,
|
||||||
extensionStores = extensionStores,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
package suwayomi.tachidesk.graphql.mutations
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
|
||||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
|
||||||
import suwayomi.tachidesk.graphql.types.ExtensionStoreType
|
|
||||||
import suwayomi.tachidesk.manga.impl.extension.ExtensionStoreService
|
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionStoreTable
|
|
||||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
|
||||||
import java.util.concurrent.CompletableFuture
|
|
||||||
|
|
||||||
class ExtensionStoreMutation {
|
|
||||||
data class AddExtensionStoreInput(
|
|
||||||
val clientMutationId: String? = null,
|
|
||||||
val indexUrl: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class AddExtensionStorePayload(
|
|
||||||
val clientMutationId: String?,
|
|
||||||
val extensionStore: ExtensionStoreType,
|
|
||||||
)
|
|
||||||
|
|
||||||
@RequireAuth
|
|
||||||
fun addExtensionStore(input: AddExtensionStoreInput): CompletableFuture<AddExtensionStorePayload?> {
|
|
||||||
val (clientMutationId, indexUrl) = input
|
|
||||||
return future {
|
|
||||||
val store = ExtensionStoreService.fetch(indexUrl)
|
|
||||||
|
|
||||||
ExtensionStoreService.upsert(store)
|
|
||||||
ExtensionStoreService.syncDbToPrefs()
|
|
||||||
val row =
|
|
||||||
transaction {
|
|
||||||
ExtensionStoreTable
|
|
||||||
.selectAll()
|
|
||||||
.where { ExtensionStoreTable.indexUrl eq store.indexUrl }
|
|
||||||
.first()
|
|
||||||
}
|
|
||||||
|
|
||||||
AddExtensionStorePayload(
|
|
||||||
clientMutationId = clientMutationId,
|
|
||||||
extensionStore = ExtensionStoreType(row),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class RemoveExtensionStoreInput(
|
|
||||||
val clientMutationId: String? = null,
|
|
||||||
val indexUrl: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class RemoveExtensionStorePayload(
|
|
||||||
val clientMutationId: String?,
|
|
||||||
val extensionStore: ExtensionStoreType?,
|
|
||||||
)
|
|
||||||
|
|
||||||
@RequireAuth
|
|
||||||
fun removeExtensionStore(input: RemoveExtensionStoreInput): CompletableFuture<RemoveExtensionStorePayload?> {
|
|
||||||
val (clientMutationId, indexUrl) = input
|
|
||||||
return future {
|
|
||||||
val store =
|
|
||||||
transaction {
|
|
||||||
ExtensionStoreTable
|
|
||||||
.selectAll()
|
|
||||||
.where { ExtensionStoreTable.indexUrl eq indexUrl }
|
|
||||||
.firstOrNull()
|
|
||||||
?.let { ExtensionStoreType(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
store?.let {
|
|
||||||
transaction {
|
|
||||||
ExtensionStoreTable.deleteWhere { ExtensionStoreTable.indexUrl eq indexUrl }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ExtensionStoreService.syncDbToPrefs()
|
|
||||||
|
|
||||||
RemoveExtensionStorePayload(
|
|
||||||
clientMutationId = clientMutationId,
|
|
||||||
extensionStore =
|
|
||||||
store?.let {
|
|
||||||
ExtensionStoreType(
|
|
||||||
name = it.name,
|
|
||||||
badgeLabel = it.badgeLabel,
|
|
||||||
signingKey = it.signingKey,
|
|
||||||
contactWebsite = it.contactWebsite,
|
|
||||||
contactDiscord = it.contactDiscord,
|
|
||||||
indexUrl = it.indexUrl,
|
|
||||||
isLegacy = it.isLegacy,
|
|
||||||
extensionListUrl = it.extensionListUrl,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
package suwayomi.tachidesk.graphql.mutations
|
package suwayomi.tachidesk.graphql.mutations
|
||||||
|
|
||||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
|
||||||
import org.jetbrains.exposed.v1.core.LikePattern
|
import org.jetbrains.exposed.v1.core.LikePattern
|
||||||
import org.jetbrains.exposed.v1.core.Op
|
import org.jetbrains.exposed.v1.core.Op
|
||||||
import org.jetbrains.exposed.v1.core.and
|
import org.jetbrains.exposed.v1.core.and
|
||||||
@@ -15,14 +14,12 @@ import org.jetbrains.exposed.v1.jdbc.selectAll
|
|||||||
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.graphql.directives.RequireAuth
|
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||||
import suwayomi.tachidesk.graphql.types.ChapterType
|
|
||||||
import suwayomi.tachidesk.graphql.types.MangaMetaType
|
import suwayomi.tachidesk.graphql.types.MangaMetaType
|
||||||
import suwayomi.tachidesk.graphql.types.MangaType
|
import suwayomi.tachidesk.graphql.types.MangaType
|
||||||
import suwayomi.tachidesk.graphql.types.MetaInput
|
import suwayomi.tachidesk.graphql.types.MetaInput
|
||||||
import suwayomi.tachidesk.manga.impl.Library
|
import suwayomi.tachidesk.manga.impl.Library
|
||||||
import suwayomi.tachidesk.manga.impl.Manga
|
import suwayomi.tachidesk.manga.impl.Manga
|
||||||
import suwayomi.tachidesk.manga.impl.update.IUpdater
|
import suwayomi.tachidesk.manga.impl.update.IUpdater
|
||||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaMetaTable
|
import suwayomi.tachidesk.manga.model.table.MangaMetaTable
|
||||||
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
|
||||||
@@ -149,12 +146,11 @@ class MangaMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
@GraphQLDeprecated("Deprecated in Tachiyomix 1.6", ReplaceWith("fetchMangaAndChapters"))
|
|
||||||
fun fetchManga(input: FetchMangaInput): CompletableFuture<FetchMangaPayload?> {
|
fun fetchManga(input: FetchMangaInput): CompletableFuture<FetchMangaPayload?> {
|
||||||
val (clientMutationId, id) = input
|
val (clientMutationId, id) = input
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
Manga.updateMangaAndChapters(id, updateChapters = false)
|
Manga.fetchManga(id)
|
||||||
|
|
||||||
val manga =
|
val manga =
|
||||||
transaction {
|
transaction {
|
||||||
@@ -167,49 +163,6 @@ class MangaMutation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class FetchMangaAndChaptersInput(
|
|
||||||
val clientMutationId: String? = null,
|
|
||||||
val id: Int,
|
|
||||||
val fetchManga: Boolean,
|
|
||||||
val fetchChapters: Boolean,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class FetchMangaAndChaptersPayload(
|
|
||||||
val clientMutationId: String?,
|
|
||||||
val manga: MangaType,
|
|
||||||
val chapters: List<ChapterType>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@RequireAuth
|
|
||||||
fun fetchMangaAndChapters(input: FetchMangaAndChaptersInput): CompletableFuture<FetchMangaAndChaptersPayload?> {
|
|
||||||
val (clientMutationId, id, fetchManga, fetchChapters) = input
|
|
||||||
|
|
||||||
return future {
|
|
||||||
Manga.updateMangaAndChapters(
|
|
||||||
mangaId = id,
|
|
||||||
updateManga = fetchManga,
|
|
||||||
updateChapters = fetchChapters,
|
|
||||||
)
|
|
||||||
|
|
||||||
val (manga, chapters) =
|
|
||||||
transaction {
|
|
||||||
Pair(
|
|
||||||
MangaTable.selectAll().where { MangaTable.id eq id }.first(),
|
|
||||||
ChapterTable
|
|
||||||
.selectAll()
|
|
||||||
.where { ChapterTable.manga eq id }
|
|
||||||
.orderBy(ChapterTable.sourceOrder)
|
|
||||||
.map { ChapterType(it) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
FetchMangaAndChaptersPayload(
|
|
||||||
clientMutationId = clientMutationId,
|
|
||||||
manga = MangaType(manga),
|
|
||||||
chapters = chapters,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class SetMangaMetaInput(
|
data class SetMangaMetaInput(
|
||||||
val clientMutationId: String? = null,
|
val clientMutationId: String? = null,
|
||||||
val meta: MangaMetaType,
|
val meta: MangaMetaType,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import suwayomi.tachidesk.graphql.types.preferenceOf
|
|||||||
import suwayomi.tachidesk.graphql.types.updateFilterList
|
import suwayomi.tachidesk.graphql.types.updateFilterList
|
||||||
import suwayomi.tachidesk.manga.impl.MangaList.insertOrUpdate
|
import suwayomi.tachidesk.manga.impl.MangaList.insertOrUpdate
|
||||||
import suwayomi.tachidesk.manga.impl.Source
|
import suwayomi.tachidesk.manga.impl.Source
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||||
import suwayomi.tachidesk.manga.model.table.SourceMetaTable
|
import suwayomi.tachidesk.manga.model.table.SourceMetaTable
|
||||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||||
@@ -256,7 +256,7 @@ class SourceMutation {
|
|||||||
val (clientMutationId, sourceId, type, page, query, filters) = input
|
val (clientMutationId, sourceId, type, page, query, filters) = input
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
val source = GetSource.getSourceOrNull(sourceId)!!
|
val source = GetCatalogueSource.getCatalogueSourceOrNull(sourceId)!!
|
||||||
val mangasPage =
|
val mangasPage =
|
||||||
when (type) {
|
when (type) {
|
||||||
FetchSourceMangaType.SEARCH -> {
|
FetchSourceMangaType.SEARCH -> {
|
||||||
|
|||||||
@@ -148,36 +148,6 @@ class TrackMutation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class BindTrackRecordInput(
|
|
||||||
val clientMutationId: String? = null,
|
|
||||||
val mangaId: Int,
|
|
||||||
val trackRecordId: Int,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class BindTrackRecordPayload(
|
|
||||||
val clientMutationId: String?,
|
|
||||||
val trackRecord: TrackRecordType,
|
|
||||||
)
|
|
||||||
|
|
||||||
@RequireAuth
|
|
||||||
fun bindTrackRecord(input: BindTrackRecordInput): CompletableFuture<BindTrackRecordPayload?> {
|
|
||||||
val (clientMutationId, mangaId, trackRecordId) = input
|
|
||||||
|
|
||||||
return future {
|
|
||||||
val boundTrackRecordId = Track.bindTrackRecord(mangaId, trackRecordId)
|
|
||||||
|
|
||||||
val trackRecord =
|
|
||||||
transaction {
|
|
||||||
TrackRecordTable.selectAll().where { TrackRecordTable.id eq boundTrackRecordId }.first()
|
|
||||||
}
|
|
||||||
|
|
||||||
BindTrackRecordPayload(
|
|
||||||
clientMutationId,
|
|
||||||
TrackRecordType(trackRecord),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class FetchTrackInput(
|
data class FetchTrackInput(
|
||||||
val clientMutationId: String? = null,
|
val clientMutationId: String? = null,
|
||||||
val recordId: Int,
|
val recordId: Int,
|
||||||
|
|||||||
@@ -21,15 +21,12 @@ 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.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
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.LongFilter
|
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.OpAnd
|
import suwayomi.tachidesk.graphql.queries.filter.OpAnd
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.StringFilter
|
import suwayomi.tachidesk.graphql.queries.filter.StringFilter
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare
|
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEnum
|
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
|
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.applyOps
|
import suwayomi.tachidesk.graphql.queries.filter.applyOps
|
||||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||||
@@ -43,7 +40,6 @@ 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.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
|
||||||
|
|
||||||
@@ -59,23 +55,21 @@ class ExtensionQuery {
|
|||||||
) : OrderBy<ExtensionType> {
|
) : OrderBy<ExtensionType> {
|
||||||
PKG_NAME(ExtensionTable.pkgName),
|
PKG_NAME(ExtensionTable.pkgName),
|
||||||
NAME(ExtensionTable.name),
|
NAME(ExtensionTable.name),
|
||||||
|
APK_NAME(ExtensionTable.apkName),
|
||||||
@GraphQLDeprecated("")
|
|
||||||
APK_NAME(ExtensionTable.pkgName),
|
|
||||||
;
|
;
|
||||||
|
|
||||||
override fun greater(cursor: Cursor): Op<Boolean> =
|
override fun greater(cursor: Cursor): Op<Boolean> =
|
||||||
when (this) {
|
when (this) {
|
||||||
PKG_NAME -> ExtensionTable.pkgName greater cursor.value
|
PKG_NAME -> ExtensionTable.pkgName greater cursor.value
|
||||||
NAME -> greaterNotUnique(ExtensionTable.name, ExtensionTable.pkgName, cursor, String::toString)
|
NAME -> greaterNotUnique(ExtensionTable.name, ExtensionTable.pkgName, cursor, String::toString)
|
||||||
APK_NAME -> ExtensionTable.pkgName greater cursor.value
|
APK_NAME -> greaterNotUnique(ExtensionTable.apkName, ExtensionTable.pkgName, cursor, String::toString)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun less(cursor: Cursor): Op<Boolean> =
|
override fun less(cursor: Cursor): Op<Boolean> =
|
||||||
when (this) {
|
when (this) {
|
||||||
PKG_NAME -> ExtensionTable.pkgName less cursor.value
|
PKG_NAME -> ExtensionTable.pkgName less cursor.value
|
||||||
NAME -> lessNotUnique(ExtensionTable.name, ExtensionTable.pkgName, cursor, String::toString)
|
NAME -> lessNotUnique(ExtensionTable.name, ExtensionTable.pkgName, cursor, String::toString)
|
||||||
APK_NAME -> ExtensionTable.pkgName less cursor.value
|
APK_NAME -> lessNotUnique(ExtensionTable.apkName, ExtensionTable.pkgName, cursor, String::toString)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun asCursor(type: ExtensionType): Cursor {
|
override fun asCursor(type: ExtensionType): Cursor {
|
||||||
@@ -95,44 +89,29 @@ class ExtensionQuery {
|
|||||||
) : Order<ExtensionOrderBy>
|
) : Order<ExtensionOrderBy>
|
||||||
|
|
||||||
data class ExtensionCondition(
|
data class ExtensionCondition(
|
||||||
val storeIndexUrl: String? = null,
|
|
||||||
@GraphQLDeprecated("", ReplaceWith("storeIndexUrl"))
|
|
||||||
val repo: String? = null,
|
val repo: String? = null,
|
||||||
val apkName: String? = null,
|
val apkName: String? = null,
|
||||||
val iconUrl: String? = null,
|
val iconUrl: String? = null,
|
||||||
val name: String? = null,
|
val name: String? = null,
|
||||||
val pkgName: String? = null,
|
val pkgName: String? = null,
|
||||||
val apkUrl: String? = null,
|
|
||||||
val extensionLib: String? = null,
|
|
||||||
val versionName: String? = null,
|
val versionName: String? = null,
|
||||||
val versionCode: Int? = null,
|
val versionCode: Int? = null,
|
||||||
val versionCodeLong: Long? = null,
|
|
||||||
val lang: String? = null,
|
val lang: String? = null,
|
||||||
@GraphQLDeprecated("", ReplaceWith("contentWarning"))
|
|
||||||
val isNsfw: Boolean? = null,
|
val isNsfw: Boolean? = 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,
|
||||||
) : HasGetOp {
|
) : HasGetOp {
|
||||||
override fun getOp(): Op<Boolean>? {
|
override fun getOp(): Op<Boolean>? {
|
||||||
val opAnd = OpAnd()
|
val opAnd = OpAnd()
|
||||||
opAnd.eq(storeIndexUrl, ExtensionTable.storeIndexUrl)
|
opAnd.eq(repo, ExtensionTable.repo)
|
||||||
opAnd.eq(repo, ExtensionTable.storeIndexUrl)
|
|
||||||
opAnd.eq(apkName, ExtensionTable.apkName)
|
opAnd.eq(apkName, ExtensionTable.apkName)
|
||||||
opAnd.eq(iconUrl, ExtensionTable.iconUrl)
|
opAnd.eq(iconUrl, ExtensionTable.iconUrl)
|
||||||
opAnd.eq(apkUrl, ExtensionTable.apkUrl)
|
|
||||||
opAnd.eq(name, ExtensionTable.name)
|
opAnd.eq(name, ExtensionTable.name)
|
||||||
opAnd.eq(extensionLib, ExtensionTable.extensionLib)
|
|
||||||
opAnd.eq(versionName, ExtensionTable.versionName)
|
opAnd.eq(versionName, ExtensionTable.versionName)
|
||||||
opAnd.eq(versionCode?.toLong(), ExtensionTable.versionCode)
|
opAnd.eq(versionCode, ExtensionTable.versionCode)
|
||||||
opAnd.eq(versionCodeLong, ExtensionTable.versionCode)
|
|
||||||
opAnd.eq(lang, ExtensionTable.lang)
|
opAnd.eq(lang, ExtensionTable.lang)
|
||||||
opAnd.eq(
|
opAnd.eq(isNsfw, ExtensionTable.isNsfw)
|
||||||
isNsfw?.let { if (it) ContentWarning.MIXED.ordinal else ContentWarning.SAFE.ordinal },
|
|
||||||
ExtensionTable.contentWarning,
|
|
||||||
)
|
|
||||||
opAnd.eq(contentWarning?.ordinal, ExtensionTable.contentWarning)
|
|
||||||
opAnd.eq(isInstalled, ExtensionTable.isInstalled)
|
opAnd.eq(isInstalled, ExtensionTable.isInstalled)
|
||||||
opAnd.eq(hasUpdate, ExtensionTable.hasUpdate)
|
opAnd.eq(hasUpdate, ExtensionTable.hasUpdate)
|
||||||
opAnd.eq(isObsolete, ExtensionTable.isObsolete)
|
opAnd.eq(isObsolete, ExtensionTable.isObsolete)
|
||||||
@@ -142,23 +121,15 @@ class ExtensionQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class ExtensionFilter(
|
data class ExtensionFilter(
|
||||||
val storeIndexUrl: StringFilter? = null,
|
|
||||||
@GraphQLDeprecated("", ReplaceWith("storeIndexUrl"))
|
|
||||||
val repo: StringFilter? = null,
|
val repo: StringFilter? = null,
|
||||||
val apkName: StringFilter? = null,
|
val apkName: StringFilter? = null,
|
||||||
val iconUrl: StringFilter? = null,
|
val iconUrl: StringFilter? = null,
|
||||||
val name: StringFilter? = null,
|
val name: StringFilter? = null,
|
||||||
val pkgName: StringFilter? = null,
|
val pkgName: StringFilter? = null,
|
||||||
val apkUrl: StringFilter? = null,
|
|
||||||
val versionName: StringFilter? = null,
|
val versionName: StringFilter? = null,
|
||||||
val extensionLib: StringFilter? = null,
|
|
||||||
@GraphQLDeprecated("", ReplaceWith("versionCodeLong"))
|
|
||||||
val versionCode: IntFilter? = null,
|
val versionCode: IntFilter? = null,
|
||||||
val versionCodeLong: LongFilter? = null,
|
|
||||||
val lang: StringFilter? = null,
|
val lang: StringFilter? = null,
|
||||||
@GraphQLDeprecated("", ReplaceWith("contentWarning"))
|
|
||||||
val isNsfw: BooleanFilter? = null,
|
val isNsfw: BooleanFilter? = 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,
|
||||||
@@ -168,18 +139,15 @@ class ExtensionQuery {
|
|||||||
) : Filter<ExtensionFilter> {
|
) : Filter<ExtensionFilter> {
|
||||||
override fun getOpList(): List<Op<Boolean>> =
|
override fun getOpList(): List<Op<Boolean>> =
|
||||||
listOfNotNull(
|
listOfNotNull(
|
||||||
andFilterWithCompareString(ExtensionTable.storeIndexUrl, storeIndexUrl),
|
andFilterWithCompareString(ExtensionTable.repo, repo),
|
||||||
andFilterWithCompareString(ExtensionTable.storeIndexUrl, repo),
|
|
||||||
andFilterWithCompareString(ExtensionTable.apkName, apkName),
|
andFilterWithCompareString(ExtensionTable.apkName, apkName),
|
||||||
andFilterWithCompareString(ExtensionTable.iconUrl, iconUrl),
|
andFilterWithCompareString(ExtensionTable.iconUrl, iconUrl),
|
||||||
andFilterWithCompareString(ExtensionTable.name, name),
|
andFilterWithCompareString(ExtensionTable.name, name),
|
||||||
andFilterWithCompareString(ExtensionTable.pkgName, pkgName),
|
andFilterWithCompareString(ExtensionTable.pkgName, pkgName),
|
||||||
andFilterWithCompareString(ExtensionTable.apkUrl, apkUrl),
|
|
||||||
andFilterWithCompareString(ExtensionTable.extensionLib, extensionLib),
|
|
||||||
andFilterWithCompareString(ExtensionTable.versionName, versionName),
|
andFilterWithCompareString(ExtensionTable.versionName, versionName),
|
||||||
andFilterWithCompare(ExtensionTable.versionCode, versionCodeLong),
|
andFilterWithCompare(ExtensionTable.versionCode, versionCode),
|
||||||
andFilterWithCompareString(ExtensionTable.lang, lang),
|
andFilterWithCompareString(ExtensionTable.lang, lang),
|
||||||
andFilterWithCompareEnum(ExtensionTable.contentWarning, contentWarning),
|
andFilterWithCompare(ExtensionTable.isNsfw, isNsfw),
|
||||||
andFilterWithCompare(ExtensionTable.isInstalled, isInstalled),
|
andFilterWithCompare(ExtensionTable.isInstalled, isInstalled),
|
||||||
andFilterWithCompare(ExtensionTable.hasUpdate, hasUpdate),
|
andFilterWithCompare(ExtensionTable.hasUpdate, hasUpdate),
|
||||||
andFilterWithCompare(ExtensionTable.isObsolete, isObsolete),
|
andFilterWithCompare(ExtensionTable.isObsolete, isObsolete),
|
||||||
|
|||||||
@@ -1,190 +0,0 @@
|
|||||||
package suwayomi.tachidesk.graphql.queries
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
|
||||||
import graphql.schema.DataFetchingEnvironment
|
|
||||||
import org.jetbrains.exposed.v1.core.Column
|
|
||||||
import org.jetbrains.exposed.v1.core.Op
|
|
||||||
import org.jetbrains.exposed.v1.core.SortOrder
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
|
||||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.Filter
|
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.HasGetOp
|
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.OpAnd
|
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.StringFilter
|
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
|
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.applyOps
|
|
||||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
|
||||||
import suwayomi.tachidesk.graphql.server.primitives.Order
|
|
||||||
import suwayomi.tachidesk.graphql.server.primitives.OrderBy
|
|
||||||
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
|
|
||||||
import suwayomi.tachidesk.graphql.server.primitives.QueryResults
|
|
||||||
import suwayomi.tachidesk.graphql.server.primitives.applyBeforeAfter
|
|
||||||
import suwayomi.tachidesk.graphql.server.primitives.greaterNotUnique
|
|
||||||
import suwayomi.tachidesk.graphql.server.primitives.lessNotUnique
|
|
||||||
import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
|
|
||||||
import suwayomi.tachidesk.graphql.types.ExtensionStoreNodeList
|
|
||||||
import suwayomi.tachidesk.graphql.types.ExtensionStoreType
|
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionStoreTable
|
|
||||||
import java.util.concurrent.CompletableFuture
|
|
||||||
|
|
||||||
class ExtensionStoreQuery {
|
|
||||||
@RequireAuth
|
|
||||||
fun extensionStore(
|
|
||||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
|
||||||
indexUrl: String,
|
|
||||||
): CompletableFuture<ExtensionStoreType> = dataFetchingEnvironment.getValueFromDataLoader("ExtensionStoreDataLoader", indexUrl)
|
|
||||||
|
|
||||||
enum class ExtensionStoreOrderBy(
|
|
||||||
override val column: Column<*>,
|
|
||||||
) : OrderBy<ExtensionStoreType> {
|
|
||||||
NAME(ExtensionStoreTable.name),
|
|
||||||
INDEX_URL(ExtensionStoreTable.indexUrl),
|
|
||||||
;
|
|
||||||
|
|
||||||
override fun greater(cursor: Cursor): Op<Boolean> =
|
|
||||||
when (this) {
|
|
||||||
NAME -> greaterNotUnique(ExtensionStoreTable.name, ExtensionStoreTable.id, cursor, String::toString)
|
|
||||||
INDEX_URL -> greaterNotUnique(ExtensionStoreTable.indexUrl, ExtensionStoreTable.id, cursor, String::toString)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun less(cursor: Cursor): Op<Boolean> =
|
|
||||||
when (this) {
|
|
||||||
NAME -> lessNotUnique(ExtensionStoreTable.name, ExtensionStoreTable.id, cursor, String::toString)
|
|
||||||
INDEX_URL -> lessNotUnique(ExtensionStoreTable.indexUrl, ExtensionStoreTable.id, cursor, String::toString)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun asCursor(type: ExtensionStoreType): Cursor {
|
|
||||||
val value =
|
|
||||||
when (this) {
|
|
||||||
INDEX_URL -> type.indexUrl
|
|
||||||
NAME -> type.indexUrl + "-" + type.name
|
|
||||||
}
|
|
||||||
return Cursor(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ExtensionStoreOrder(
|
|
||||||
override val by: ExtensionStoreOrderBy,
|
|
||||||
override val byType: SortOrder? = null,
|
|
||||||
) : Order<ExtensionStoreOrderBy>
|
|
||||||
|
|
||||||
data class ExtensionStoreCondition(
|
|
||||||
val id: Int? = null,
|
|
||||||
val indexUrl: String? = null,
|
|
||||||
val name: String? = null,
|
|
||||||
) : HasGetOp {
|
|
||||||
override fun getOp(): Op<Boolean>? {
|
|
||||||
val opAnd = OpAnd()
|
|
||||||
opAnd.eq(id, ExtensionStoreTable.id)
|
|
||||||
opAnd.eq(indexUrl, ExtensionStoreTable.indexUrl)
|
|
||||||
opAnd.eq(name, ExtensionStoreTable.name)
|
|
||||||
|
|
||||||
return opAnd.op
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ExtensionStoreFilter(
|
|
||||||
val indexUrl: StringFilter? = null,
|
|
||||||
val name: StringFilter? = null,
|
|
||||||
override val and: List<ExtensionStoreFilter>? = null,
|
|
||||||
override val or: List<ExtensionStoreFilter>? = null,
|
|
||||||
override val not: ExtensionStoreFilter? = null,
|
|
||||||
) : Filter<ExtensionStoreFilter> {
|
|
||||||
override fun getOpList(): List<Op<Boolean>> =
|
|
||||||
listOfNotNull(
|
|
||||||
andFilterWithCompareString(ExtensionStoreTable.indexUrl, indexUrl),
|
|
||||||
andFilterWithCompareString(ExtensionStoreTable.name, name),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequireAuth
|
|
||||||
fun extensionStores(
|
|
||||||
condition: ExtensionStoreCondition? = null,
|
|
||||||
filter: ExtensionStoreFilter? = null,
|
|
||||||
order: List<ExtensionStoreOrder>? = null,
|
|
||||||
before: Cursor? = null,
|
|
||||||
after: Cursor? = null,
|
|
||||||
first: Int? = null,
|
|
||||||
last: Int? = null,
|
|
||||||
offset: Int? = null,
|
|
||||||
): ExtensionStoreNodeList {
|
|
||||||
val queryResults =
|
|
||||||
transaction {
|
|
||||||
val res = ExtensionStoreTable.selectAll()
|
|
||||||
|
|
||||||
res.applyOps(condition, filter)
|
|
||||||
|
|
||||||
if (order != null || (last != null || before != null)) {
|
|
||||||
val baseSort = listOf(ExtensionStoreOrder(ExtensionStoreOrderBy.INDEX_URL, SortOrder.ASC))
|
|
||||||
val actualSort = (order.orEmpty() + baseSort)
|
|
||||||
actualSort.forEach { (orderBy, orderByType) ->
|
|
||||||
val orderByColumn = orderBy.column
|
|
||||||
val orderType = orderByType.maybeSwap(last ?: before)
|
|
||||||
|
|
||||||
res.orderBy(orderByColumn to orderType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val total = res.count()
|
|
||||||
val firstResult = res.firstOrNull()?.get(ExtensionStoreTable.indexUrl)
|
|
||||||
val lastResult = res.lastOrNull()?.get(ExtensionStoreTable.indexUrl)
|
|
||||||
|
|
||||||
res.applyBeforeAfter(
|
|
||||||
before = before,
|
|
||||||
after = after,
|
|
||||||
orderBy = order?.firstOrNull()?.by ?: ExtensionStoreOrderBy.INDEX_URL,
|
|
||||||
orderByType = order?.firstOrNull()?.byType,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (first != null) {
|
|
||||||
res.limit(first).offset(offset?.toLong() ?: 0)
|
|
||||||
} else if (last != null) {
|
|
||||||
res.limit(last)
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryResults(total, firstResult, lastResult, res.toList())
|
|
||||||
}
|
|
||||||
|
|
||||||
val getAsCursor: (ExtensionStoreType) -> Cursor = (order?.firstOrNull()?.by ?: ExtensionStoreOrderBy.INDEX_URL)::asCursor
|
|
||||||
|
|
||||||
val resultsAsType = queryResults.results.map { ExtensionStoreType(it) }
|
|
||||||
|
|
||||||
return ExtensionStoreNodeList(
|
|
||||||
resultsAsType,
|
|
||||||
if (resultsAsType.isEmpty()) {
|
|
||||||
emptyList()
|
|
||||||
} else {
|
|
||||||
listOfNotNull(
|
|
||||||
resultsAsType.firstOrNull()?.let {
|
|
||||||
ExtensionStoreNodeList.ExtensionStoreEdge(
|
|
||||||
getAsCursor(it),
|
|
||||||
it,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
resultsAsType.lastOrNull()?.let {
|
|
||||||
ExtensionStoreNodeList.ExtensionStoreEdge(
|
|
||||||
getAsCursor(it),
|
|
||||||
it,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
pageInfo =
|
|
||||||
PageInfo(
|
|
||||||
hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.indexUrl,
|
|
||||||
hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.indexUrl,
|
|
||||||
startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(it) },
|
|
||||||
endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(it) },
|
|
||||||
),
|
|
||||||
totalCount = queryResults.total.toInt(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,22 +13,19 @@ import graphql.schema.DataFetchingEnvironment
|
|||||||
import org.jetbrains.exposed.v1.core.Column
|
import org.jetbrains.exposed.v1.core.Column
|
||||||
import org.jetbrains.exposed.v1.core.Op
|
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.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.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.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
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.OpAnd
|
import suwayomi.tachidesk.graphql.queries.filter.OpAnd
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.StringFilter
|
import suwayomi.tachidesk.graphql.queries.filter.StringFilter
|
||||||
|
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity
|
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEnum
|
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
|
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.applyOps
|
import suwayomi.tachidesk.graphql.queries.filter.applyOps
|
||||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||||
@@ -42,7 +39,6 @@ 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.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,23 +91,14 @@ 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 contentWarning == ContentRating.MIXED", ReplaceWith("contentWarning"))
|
|
||||||
val isNsfw: Boolean? = null,
|
val isNsfw: Boolean? = null,
|
||||||
val contentWarning: ContentWarning? = null,
|
|
||||||
) : HasGetOp {
|
) : HasGetOp {
|
||||||
override fun getOp(): Op<Boolean>? {
|
override fun getOp(): Op<Boolean>? {
|
||||||
val opAnd = OpAnd()
|
val opAnd = OpAnd()
|
||||||
opAnd.eq(id, SourceTable.id)
|
opAnd.eq(id, SourceTable.id)
|
||||||
opAnd.eq(name, SourceTable.name)
|
opAnd.eq(name, SourceTable.name)
|
||||||
opAnd.eq(lang, SourceTable.lang)
|
opAnd.eq(lang, SourceTable.lang)
|
||||||
opAnd.andWhere(isNsfw) {
|
opAnd.eq(isNsfw, SourceTable.isNsfw)
|
||||||
if (it) {
|
|
||||||
SourceTable.contentWarning greaterEq ContentWarning.MIXED.ordinal
|
|
||||||
} else {
|
|
||||||
SourceTable.contentWarning less ContentWarning.MIXED.ordinal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
opAnd.andWhere(contentWarning) { SourceTable.contentWarning eq it.ordinal }
|
|
||||||
|
|
||||||
return opAnd.op
|
return opAnd.op
|
||||||
}
|
}
|
||||||
@@ -121,9 +108,7 @@ 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 contentWarning", ReplaceWith("contentWarning"))
|
|
||||||
val isNsfw: BooleanFilter? = null,
|
val isNsfw: BooleanFilter? = 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,
|
||||||
@@ -133,7 +118,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.contentWarning, contentWarning),
|
andFilterWithCompare(SourceTable.isNsfw, isNsfw),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ 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.ContentWarning
|
|
||||||
|
|
||||||
class ILikeEscapeOp(
|
class ILikeEscapeOp(
|
||||||
expr1: Expression<*>,
|
expr1: Expression<*>,
|
||||||
@@ -330,24 +329,6 @@ data class DoubleFilter(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ContentWarningFilter(
|
|
||||||
override val isNull: Boolean? = null,
|
|
||||||
override val equalTo: ContentWarning? = null,
|
|
||||||
override val notEqualTo: ContentWarning? = null,
|
|
||||||
override val notEqualToAll: List<ContentWarning>? = null,
|
|
||||||
override val notEqualToAny: List<ContentWarning>? = null,
|
|
||||||
override val distinctFrom: ContentWarning? = null,
|
|
||||||
override val distinctFromAll: List<ContentWarning>? = null,
|
|
||||||
override val distinctFromAny: List<ContentWarning>? = null,
|
|
||||||
override val notDistinctFrom: ContentWarning? = null,
|
|
||||||
override val `in`: List<ContentWarning>? = null,
|
|
||||||
override val notIn: List<ContentWarning>? = null,
|
|
||||||
override val lessThan: ContentWarning? = null,
|
|
||||||
override val lessThanOrEqualTo: ContentWarning? = null,
|
|
||||||
override val greaterThan: ContentWarning? = null,
|
|
||||||
override val greaterThanOrEqualTo: ContentWarning? = null,
|
|
||||||
) : ComparableScalarFilter<ContentWarning>
|
|
||||||
|
|
||||||
data class StringFilter(
|
data class StringFilter(
|
||||||
override val isNull: Boolean? = null,
|
override val isNull: Boolean? = null,
|
||||||
override val equalTo: String? = null,
|
override val equalTo: String? = null,
|
||||||
@@ -637,35 +618,6 @@ fun <T : Comparable<T>, S : T?> andFilterWithCompare(
|
|||||||
return opAnd.op
|
return opAnd.op
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
fun <T : Enum<T>> andFilterWithCompareEnum(
|
|
||||||
column: Column<Int>,
|
|
||||||
filter: ComparableScalarFilter<T>?,
|
|
||||||
): Op<Boolean>? {
|
|
||||||
filter ?: return null
|
|
||||||
val opAnd = OpAnd()
|
|
||||||
|
|
||||||
opAnd.andWhere(filter.lessThan) { column less it.ordinal }
|
|
||||||
opAnd.andWhere(filter.lessThanOrEqualTo) { column lessEq it.ordinal }
|
|
||||||
opAnd.andWhere(filter.greaterThan) { column greater it.ordinal }
|
|
||||||
opAnd.andWhere(filter.greaterThanOrEqualTo) { column greaterEq it.ordinal }
|
|
||||||
|
|
||||||
opAnd.andWhere(filter.isNull) { if (it) column.isNull() else column.isNotNull() }
|
|
||||||
|
|
||||||
opAnd.andWhere(filter.equalTo) { column eq it.ordinal }
|
|
||||||
opAnd.andNotWhere(filter.notEqualTo, filter.notEqualToAll, filter.notEqualToAny) { column neq it.ordinal }
|
|
||||||
opAnd.andWhere(filter.distinctFrom, filter.distinctFromAll, filter.distinctFromAny) { DistinctFromOp.distinctFrom(column, it.ordinal) }
|
|
||||||
opAnd.andWhere(filter.notDistinctFrom) { DistinctFromOp.notDistinctFrom(column, it.ordinal) }
|
|
||||||
if (!filter.`in`.isNullOrEmpty()) {
|
|
||||||
opAnd.andWhere(filter.`in`) { column inList it.map { it.ordinal } }
|
|
||||||
}
|
|
||||||
if (!filter.notIn.isNullOrEmpty()) {
|
|
||||||
opAnd.andWhere(filter.notIn) { column notInList it.map { it.ordinal } }
|
|
||||||
}
|
|
||||||
|
|
||||||
return opAnd.op
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T : Comparable<T>> andFilterWithCompareEntity(
|
fun <T : Comparable<T>> andFilterWithCompareEntity(
|
||||||
column: Column<EntityID<T>>,
|
column: Column<EntityID<T>>,
|
||||||
filter: ComparableScalarFilter<T>?,
|
filter: ComparableScalarFilter<T>?,
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ import suwayomi.tachidesk.graphql.dataLoaders.DisplayScoreForTrackSearchDataLoad
|
|||||||
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.ExtensionForSourceDataLoader
|
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionForSourceDataLoader
|
||||||
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionStoreDataLoader
|
|
||||||
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
|
||||||
@@ -80,8 +78,6 @@ class TachideskDataLoaderRegistryFactory {
|
|||||||
SourceMetaDataLoader(),
|
SourceMetaDataLoader(),
|
||||||
ExtensionDataLoader(),
|
ExtensionDataLoader(),
|
||||||
ExtensionForSourceDataLoader(),
|
ExtensionForSourceDataLoader(),
|
||||||
ExtensionsForExtensionStore(),
|
|
||||||
ExtensionStoreDataLoader(),
|
|
||||||
TrackerDataLoader(),
|
TrackerDataLoader(),
|
||||||
TrackerStatusesDataLoader(),
|
TrackerStatusesDataLoader(),
|
||||||
TrackerScoresDataLoader(),
|
TrackerScoresDataLoader(),
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import suwayomi.tachidesk.graphql.mutations.CategoryMutation
|
|||||||
import suwayomi.tachidesk.graphql.mutations.ChapterMutation
|
import suwayomi.tachidesk.graphql.mutations.ChapterMutation
|
||||||
import suwayomi.tachidesk.graphql.mutations.DownloadMutation
|
import suwayomi.tachidesk.graphql.mutations.DownloadMutation
|
||||||
import suwayomi.tachidesk.graphql.mutations.ExtensionMutation
|
import suwayomi.tachidesk.graphql.mutations.ExtensionMutation
|
||||||
import suwayomi.tachidesk.graphql.mutations.ExtensionStoreMutation
|
|
||||||
import suwayomi.tachidesk.graphql.mutations.ImageMutation
|
import suwayomi.tachidesk.graphql.mutations.ImageMutation
|
||||||
import suwayomi.tachidesk.graphql.mutations.InfoMutation
|
import suwayomi.tachidesk.graphql.mutations.InfoMutation
|
||||||
import suwayomi.tachidesk.graphql.mutations.KoreaderSyncMutation
|
import suwayomi.tachidesk.graphql.mutations.KoreaderSyncMutation
|
||||||
@@ -37,7 +36,6 @@ import suwayomi.tachidesk.graphql.queries.CategoryQuery
|
|||||||
import suwayomi.tachidesk.graphql.queries.ChapterQuery
|
import suwayomi.tachidesk.graphql.queries.ChapterQuery
|
||||||
import suwayomi.tachidesk.graphql.queries.DownloadQuery
|
import suwayomi.tachidesk.graphql.queries.DownloadQuery
|
||||||
import suwayomi.tachidesk.graphql.queries.ExtensionQuery
|
import suwayomi.tachidesk.graphql.queries.ExtensionQuery
|
||||||
import suwayomi.tachidesk.graphql.queries.ExtensionStoreQuery
|
|
||||||
import suwayomi.tachidesk.graphql.queries.InfoQuery
|
import suwayomi.tachidesk.graphql.queries.InfoQuery
|
||||||
import suwayomi.tachidesk.graphql.queries.KoreaderSyncQuery
|
import suwayomi.tachidesk.graphql.queries.KoreaderSyncQuery
|
||||||
import suwayomi.tachidesk.graphql.queries.MangaQuery
|
import suwayomi.tachidesk.graphql.queries.MangaQuery
|
||||||
@@ -97,7 +95,6 @@ val schema =
|
|||||||
TopLevelObject(ChapterQuery()),
|
TopLevelObject(ChapterQuery()),
|
||||||
TopLevelObject(DownloadQuery()),
|
TopLevelObject(DownloadQuery()),
|
||||||
TopLevelObject(ExtensionQuery()),
|
TopLevelObject(ExtensionQuery()),
|
||||||
TopLevelObject(ExtensionStoreQuery()),
|
|
||||||
TopLevelObject(InfoQuery()),
|
TopLevelObject(InfoQuery()),
|
||||||
TopLevelObject(KoreaderSyncQuery()),
|
TopLevelObject(KoreaderSyncQuery()),
|
||||||
TopLevelObject(MangaQuery()),
|
TopLevelObject(MangaQuery()),
|
||||||
@@ -115,7 +112,6 @@ val schema =
|
|||||||
TopLevelObject(ChapterMutation()),
|
TopLevelObject(ChapterMutation()),
|
||||||
TopLevelObject(DownloadMutation()),
|
TopLevelObject(DownloadMutation()),
|
||||||
TopLevelObject(ExtensionMutation()),
|
TopLevelObject(ExtensionMutation()),
|
||||||
TopLevelObject(ExtensionStoreMutation()),
|
|
||||||
TopLevelObject(ImageMutation()),
|
TopLevelObject(ImageMutation()),
|
||||||
TopLevelObject(InfoMutation()),
|
TopLevelObject(InfoMutation()),
|
||||||
TopLevelObject(KoreaderSyncMutation()),
|
TopLevelObject(KoreaderSyncMutation()),
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
package suwayomi.tachidesk.graphql.types
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
|
||||||
import graphql.schema.DataFetchingEnvironment
|
|
||||||
import org.jetbrains.exposed.v1.core.ResultRow
|
|
||||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
|
||||||
import suwayomi.tachidesk.graphql.server.primitives.Edge
|
|
||||||
import suwayomi.tachidesk.graphql.server.primitives.Node
|
|
||||||
import suwayomi.tachidesk.graphql.server.primitives.NodeList
|
|
||||||
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
|
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionStoreTable
|
|
||||||
import java.util.concurrent.CompletableFuture
|
|
||||||
|
|
||||||
class ExtensionStoreType(
|
|
||||||
val name: String,
|
|
||||||
val badgeLabel: String,
|
|
||||||
val signingKey: String,
|
|
||||||
val contactWebsite: String,
|
|
||||||
val contactDiscord: String?,
|
|
||||||
val indexUrl: String,
|
|
||||||
val isLegacy: Boolean,
|
|
||||||
val extensionListUrl: String?,
|
|
||||||
) : Node {
|
|
||||||
constructor(row: ResultRow) : this(
|
|
||||||
name = row[ExtensionStoreTable.name],
|
|
||||||
badgeLabel = row[ExtensionStoreTable.badgeLabel],
|
|
||||||
signingKey = row[ExtensionStoreTable.signingKey],
|
|
||||||
contactWebsite = row[ExtensionStoreTable.contactWebsite],
|
|
||||||
contactDiscord = row[ExtensionStoreTable.contactDiscord],
|
|
||||||
indexUrl = row[ExtensionStoreTable.indexUrl],
|
|
||||||
isLegacy = row[ExtensionStoreTable.isLegacy],
|
|
||||||
extensionListUrl = row[ExtensionStoreTable.extensionListUrl],
|
|
||||||
)
|
|
||||||
|
|
||||||
fun extensions(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<ExtensionNodeList> =
|
|
||||||
dataFetchingEnvironment.getValueFromDataLoader<String, ExtensionNodeList>("ExtensionsForExtensionStore", indexUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ExtensionStoreNodeList(
|
|
||||||
override val nodes: List<ExtensionStoreType>,
|
|
||||||
override val edges: List<ExtensionStoreEdge>,
|
|
||||||
override val pageInfo: PageInfo,
|
|
||||||
override val totalCount: Int,
|
|
||||||
) : NodeList() {
|
|
||||||
data class ExtensionStoreEdge(
|
|
||||||
override val cursor: Cursor,
|
|
||||||
override val node: ExtensionStoreType,
|
|
||||||
) : Edge()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun List<ExtensionStoreType>.toNodeList(): ExtensionStoreNodeList =
|
|
||||||
ExtensionStoreNodeList(
|
|
||||||
nodes = this,
|
|
||||||
edges = getEdges(),
|
|
||||||
pageInfo =
|
|
||||||
PageInfo(
|
|
||||||
hasNextPage = false,
|
|
||||||
hasPreviousPage = false,
|
|
||||||
startCursor = Cursor(0.toString()),
|
|
||||||
endCursor = Cursor(lastIndex.toString()),
|
|
||||||
),
|
|
||||||
totalCount = size,
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun List<ExtensionStoreType>.getEdges(): List<ExtensionStoreEdge> {
|
|
||||||
if (isEmpty()) return emptyList()
|
|
||||||
return listOf(
|
|
||||||
ExtensionStoreEdge(
|
|
||||||
cursor = Cursor("0"),
|
|
||||||
node = first(),
|
|
||||||
),
|
|
||||||
ExtensionStoreEdge(
|
|
||||||
cursor = Cursor(lastIndex.toString()),
|
|
||||||
node = last(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,8 +7,6 @@
|
|||||||
|
|
||||||
package suwayomi.tachidesk.graphql.types
|
package suwayomi.tachidesk.graphql.types
|
||||||
|
|
||||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
|
||||||
import com.expediagroup.graphql.generator.annotations.GraphQLDescription
|
|
||||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
||||||
import graphql.schema.DataFetchingEnvironment
|
import graphql.schema.DataFetchingEnvironment
|
||||||
import org.jetbrains.exposed.v1.core.ResultRow
|
import org.jetbrains.exposed.v1.core.ResultRow
|
||||||
@@ -18,51 +16,33 @@ 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.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
|
||||||
|
|
||||||
class ExtensionType(
|
class ExtensionType(
|
||||||
val storeIndexUrl: String?,
|
|
||||||
@GraphQLDeprecated("Removed in extension api v1.6", ReplaceWith("storeIndexUrl"))
|
|
||||||
val repo: String?,
|
val repo: String?,
|
||||||
@GraphQLDescription("This will be nullable in the future")
|
val apkName: String,
|
||||||
val apkName: String?,
|
|
||||||
val iconUrl: String,
|
val iconUrl: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val pkgName: String,
|
val pkgName: String,
|
||||||
val apkUrl: String?,
|
|
||||||
val extensionLib: String?,
|
|
||||||
val versionName: String,
|
val versionName: String,
|
||||||
@GraphQLDeprecated(
|
|
||||||
"Type was changed to Long, will be switched back to this variable name in the future.",
|
|
||||||
ReplaceWith("versionCodeLong"),
|
|
||||||
)
|
|
||||||
val versionCode: Int,
|
val versionCode: Int,
|
||||||
val versionCodeLong: Long,
|
|
||||||
val lang: String,
|
val lang: String,
|
||||||
@GraphQLDeprecated("Removed in extension api v1.6", ReplaceWith("contentWarning"))
|
|
||||||
val isNsfw: Boolean,
|
val isNsfw: Boolean,
|
||||||
val contentWarning: ContentWarning,
|
|
||||||
val isInstalled: Boolean,
|
val isInstalled: Boolean,
|
||||||
val hasUpdate: Boolean,
|
val hasUpdate: Boolean,
|
||||||
val isObsolete: Boolean,
|
val isObsolete: Boolean,
|
||||||
) : Node {
|
) : Node {
|
||||||
constructor(row: ResultRow) : this(
|
constructor(row: ResultRow) : this(
|
||||||
storeIndexUrl = row[ExtensionTable.storeIndexUrl],
|
repo = row[ExtensionTable.repo],
|
||||||
repo = row[ExtensionTable.storeIndexUrl],
|
apkName = row[ExtensionTable.apkName],
|
||||||
apkName = row[ExtensionTable.apkName].orEmpty(),
|
iconUrl = Extension.getExtensionIconUrl(row[ExtensionTable.apkName]),
|
||||||
iconUrl = Extension.proxyExtensionIconUrl(row[ExtensionTable.pkgName]),
|
|
||||||
name = row[ExtensionTable.name],
|
name = row[ExtensionTable.name],
|
||||||
pkgName = row[ExtensionTable.pkgName],
|
pkgName = row[ExtensionTable.pkgName],
|
||||||
apkUrl = row[ExtensionTable.apkUrl],
|
|
||||||
extensionLib = row[ExtensionTable.extensionLib],
|
|
||||||
versionName = row[ExtensionTable.versionName],
|
versionName = row[ExtensionTable.versionName],
|
||||||
versionCode = row[ExtensionTable.versionCode].toInt(),
|
versionCode = row[ExtensionTable.versionCode],
|
||||||
versionCodeLong = row[ExtensionTable.versionCode],
|
|
||||||
lang = row[ExtensionTable.lang],
|
lang = row[ExtensionTable.lang],
|
||||||
isNsfw = row[ExtensionTable.contentWarning] >= ContentWarning.MIXED.ordinal,
|
isNsfw = row[ExtensionTable.isNsfw],
|
||||||
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],
|
||||||
@@ -70,9 +50,6 @@ class ExtensionType(
|
|||||||
|
|
||||||
fun source(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<SourceNodeList> =
|
fun source(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<SourceNodeList> =
|
||||||
dataFetchingEnvironment.getValueFromDataLoader<String, SourceNodeList>("SourcesForExtensionDataLoader", pkgName)
|
dataFetchingEnvironment.getValueFromDataLoader<String, SourceNodeList>("SourcesForExtensionDataLoader", pkgName)
|
||||||
|
|
||||||
fun extensionStore(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<ExtensionStoreType> =
|
|
||||||
dataFetchingEnvironment.getValueFromDataLoader<String, ExtensionStoreType>("ExtensionStoreDataLoader", storeIndexUrl.orEmpty())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ExtensionNodeList(
|
data class ExtensionNodeList(
|
||||||
|
|||||||
@@ -7,10 +7,9 @@
|
|||||||
|
|
||||||
package suwayomi.tachidesk.graphql.types
|
package suwayomi.tachidesk.graphql.types
|
||||||
|
|
||||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
|
||||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
||||||
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import graphql.schema.DataFetchingEnvironment
|
import graphql.schema.DataFetchingEnvironment
|
||||||
@@ -24,9 +23,9 @@ 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.Source.getSourcePreferencesRaw
|
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.GetSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
import suwayomi.tachidesk.manga.model.table.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,29 +41,35 @@ class SourceType(
|
|||||||
val id: Long,
|
val id: Long,
|
||||||
val name: String,
|
val name: String,
|
||||||
val lang: String,
|
val lang: String,
|
||||||
val contentWarning: ContentWarning,
|
|
||||||
val iconUrl: String,
|
val iconUrl: String,
|
||||||
val supportsLatest: Boolean,
|
val supportsLatest: Boolean,
|
||||||
val isConfigurable: Boolean,
|
val isConfigurable: Boolean,
|
||||||
@GraphQLDeprecated("", ReplaceWith("contentWarning"))
|
|
||||||
val isNsfw: Boolean,
|
val isNsfw: Boolean,
|
||||||
val displayName: String,
|
val displayName: String,
|
||||||
val homeUrl: String?,
|
|
||||||
@GraphQLDeprecated("", ReplaceWith("homeUrl"))
|
|
||||||
val baseUrl: String?,
|
val baseUrl: String?,
|
||||||
) : Node {
|
) : Node {
|
||||||
constructor(row: ResultRow, sourceExtension: ResultRow, source: Source) : this(
|
constructor(source: SourceDataClass) : this(
|
||||||
|
id = source.id.toLong(),
|
||||||
|
name = source.name,
|
||||||
|
lang = source.lang,
|
||||||
|
iconUrl = source.iconUrl,
|
||||||
|
supportsLatest = source.supportsLatest,
|
||||||
|
isConfigurable = source.isConfigurable,
|
||||||
|
isNsfw = source.isNsfw,
|
||||||
|
displayName = source.displayName,
|
||||||
|
baseUrl = source.baseUrl,
|
||||||
|
)
|
||||||
|
|
||||||
|
constructor(row: ResultRow, sourceExtension: ResultRow, catalogueSource: CatalogueSource) : this(
|
||||||
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],
|
||||||
contentWarning = ContentWarning.valueOf(row[SourceTable.contentWarning]),
|
iconUrl = Extension.getExtensionIconUrl(sourceExtension[ExtensionTable.apkName]),
|
||||||
iconUrl = Extension.proxyExtensionIconUrl(sourceExtension[ExtensionTable.pkgName]),
|
supportsLatest = catalogueSource.supportsLatest,
|
||||||
supportsLatest = source.supportsLatest,
|
isConfigurable = catalogueSource is ConfigurableSource,
|
||||||
isConfigurable = source is ConfigurableSource,
|
isNsfw = row[SourceTable.isNsfw],
|
||||||
isNsfw = row[SourceTable.contentWarning] >= ContentWarning.MIXED.ordinal,
|
displayName = catalogueSource.toString(),
|
||||||
displayName = source.toString(),
|
baseUrl = catalogueSource.runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
|
||||||
homeUrl = runCatching { (source as? HttpSource)?.getHomeUrl() }.getOrNull(),
|
|
||||||
baseUrl = runCatching { (source as? HttpSource)?.baseUrl }.getOrNull(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
fun manga(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<MangaNodeList> =
|
fun manga(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<MangaNodeList> =
|
||||||
@@ -75,7 +80,7 @@ class SourceType(
|
|||||||
|
|
||||||
fun preferences(): List<Preference> = getSourcePreferencesRaw(id).map { preferenceOf(it) }
|
fun preferences(): List<Preference> = getSourcePreferencesRaw(id).map { preferenceOf(it) }
|
||||||
|
|
||||||
fun filters(): List<Filter> = getSourceOrStub(id).getFilterList().map { filterOf(it) }
|
fun filters(): List<Filter> = getCatalogueSourceOrStub(id).getFilterList().map { filterOf(it) }
|
||||||
|
|
||||||
fun meta(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<List<SourceMetaType>> =
|
fun meta(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<List<SourceMetaType>> =
|
||||||
dataFetchingEnvironment.getValueFromDataLoader<Long, List<SourceMetaType>>("SourceMetaDataLoader", id)
|
dataFetchingEnvironment.getValueFromDataLoader<Long, List<SourceMetaType>>("SourceMetaDataLoader", id)
|
||||||
@@ -84,8 +89,8 @@ class SourceType(
|
|||||||
@Suppress("ktlint:standard:function-naming")
|
@Suppress("ktlint:standard:function-naming")
|
||||||
fun SourceType(row: ResultRow): SourceType? {
|
fun SourceType(row: ResultRow): SourceType? {
|
||||||
val catalogueSource =
|
val catalogueSource =
|
||||||
GetSource
|
GetCatalogueSource
|
||||||
.getSourceOrNull(row[SourceTable.id].value)
|
.getCatalogueSourceOrNull(row[SourceTable.id].value)
|
||||||
?: return null
|
?: return null
|
||||||
val sourceExtension =
|
val sourceExtension =
|
||||||
if (row.hasValue(ExtensionTable.id)) {
|
if (row.hasValue(ExtensionTable.id)) {
|
||||||
@@ -296,7 +301,7 @@ data class FilterChange(
|
|||||||
)
|
)
|
||||||
|
|
||||||
fun updateFilterList(
|
fun updateFilterList(
|
||||||
source: Source,
|
source: CatalogueSource,
|
||||||
changes: List<FilterChange>?,
|
changes: List<FilterChange>?,
|
||||||
): FilterList {
|
): FilterList {
|
||||||
val filterList = source.getFilterList()
|
val filterList = source.getFilterList()
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ object MangaAPI {
|
|||||||
get("update/{pkgName}", ExtensionController.update)
|
get("update/{pkgName}", ExtensionController.update)
|
||||||
get("uninstall/{pkgName}", ExtensionController.uninstall)
|
get("uninstall/{pkgName}", ExtensionController.uninstall)
|
||||||
|
|
||||||
get("icon/{pkgName}", ExtensionController.icon)
|
get("icon/{apkName}", ExtensionController.icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
path("source") {
|
path("source") {
|
||||||
|
|||||||
@@ -165,17 +165,17 @@ object ExtensionController {
|
|||||||
/** icon for extension named `apkName` */
|
/** icon for extension named `apkName` */
|
||||||
val icon =
|
val icon =
|
||||||
handler(
|
handler(
|
||||||
pathParam<String>("pkgName"),
|
pathParam<String>("apkName"),
|
||||||
documentWith = {
|
documentWith = {
|
||||||
withOperation {
|
withOperation {
|
||||||
summary("Extension icon")
|
summary("Extension icon")
|
||||||
description("Icon for extension named `apkName`")
|
description("Icon for extension named `apkName`")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
behaviorOf = { ctx, pkgName ->
|
behaviorOf = { ctx, apkName ->
|
||||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||||
ctx.future {
|
ctx.future {
|
||||||
future { Extension.getExtensionIcon(pkgName) }
|
future { Extension.getExtensionIcon(apkName) }
|
||||||
.thenApply {
|
.thenApply {
|
||||||
ctx.header("content-type", it.second)
|
ctx.header("content-type", it.second)
|
||||||
val httpCacheSeconds = 365.days.inWholeSeconds
|
val httpCacheSeconds = 365.days.inWholeSeconds
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ object UpdateController {
|
|||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class made for handling return type in the documentation for [UpdateController.recentChapters],
|
* Class made for handling return type in the documentation for [recentChapters],
|
||||||
* since OpenApi cannot handle runtime generics.
|
* since OpenApi cannot handle runtime generics.
|
||||||
*/
|
*/
|
||||||
private class PagedMangaChapterListDataClass : PaginatedList<MangaChapterDataClass>(emptyList(), false)
|
private class PagedMangaChapterListDataClass : PaginatedList<MangaChapterDataClass>(emptyList(), false)
|
||||||
|
|||||||
@@ -7,19 +7,16 @@ package suwayomi.tachidesk.manga.impl
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
||||||
import eu.kanade.tachiyomi.util.chapter.ChapterSanitizer.sanitize
|
import eu.kanade.tachiyomi.util.chapter.ChapterSanitizer.sanitize
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import io.github.reactivecircus.cache4k.Cache
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import org.jetbrains.exposed.v1.core.ResultRow
|
|
||||||
import org.jetbrains.exposed.v1.core.SortOrder
|
import org.jetbrains.exposed.v1.core.SortOrder
|
||||||
import org.jetbrains.exposed.v1.core.and
|
import org.jetbrains.exposed.v1.core.and
|
||||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||||
@@ -35,10 +32,11 @@ import org.jetbrains.exposed.v1.jdbc.selectAll
|
|||||||
import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
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.Manga.getManga
|
||||||
import suwayomi.tachidesk.manga.impl.download.DownloadManager
|
import suwayomi.tachidesk.manga.impl.download.DownloadManager
|
||||||
import suwayomi.tachidesk.manga.impl.download.DownloadManager.EnqueueInput
|
import suwayomi.tachidesk.manga.impl.download.DownloadManager.EnqueueInput
|
||||||
import suwayomi.tachidesk.manga.impl.track.Track
|
import suwayomi.tachidesk.manga.impl.track.Track
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.MangaChapterDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.MangaChapterDataClass
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.PaginatedList
|
import suwayomi.tachidesk.manga.model.dataclass.PaginatedList
|
||||||
@@ -52,6 +50,7 @@ import suwayomi.tachidesk.server.serverConfig
|
|||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.TreeSet
|
import java.util.TreeSet
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
private fun List<ChapterDataClass>.removeDuplicates(currentChapter: ChapterDataClass): List<ChapterDataClass> =
|
private fun List<ChapterDataClass>.removeDuplicates(currentChapter: ChapterDataClass): List<ChapterDataClass> =
|
||||||
groupBy { it.chapterNumber }
|
groupBy { it.chapterNumber }
|
||||||
@@ -105,277 +104,267 @@ object Chapter {
|
|||||||
.associateBy({ it[ChapterTable.url] }, { it })
|
.associateBy({ it[ChapterTable.url] }, { it })
|
||||||
}
|
}
|
||||||
|
|
||||||
return chapterList.map {
|
return chapterList.mapIndexed { index, it ->
|
||||||
|
|
||||||
val dbChapter = dbChapterMap.getValue(it.url)
|
val dbChapter = dbChapterMap.getValue(it.url)
|
||||||
ChapterTable.toDataClass(dbChapter)
|
|
||||||
|
ChapterDataClass(
|
||||||
|
id = dbChapter[ChapterTable.id].value,
|
||||||
|
url = it.url,
|
||||||
|
name = it.name,
|
||||||
|
uploadDate = it.date_upload,
|
||||||
|
chapterNumber = it.chapter_number,
|
||||||
|
scanlator = it.scanlator,
|
||||||
|
mangaId = mangaId,
|
||||||
|
read = dbChapter[ChapterTable.isRead],
|
||||||
|
bookmarked = dbChapter[ChapterTable.isBookmarked],
|
||||||
|
lastPageRead = dbChapter[ChapterTable.lastPageRead],
|
||||||
|
lastReadAt = dbChapter[ChapterTable.lastReadAt],
|
||||||
|
index = chapterList.size - index,
|
||||||
|
fetchedAt = dbChapter[ChapterTable.fetchedAt],
|
||||||
|
realUrl = dbChapter[ChapterTable.realUrl],
|
||||||
|
downloaded = dbChapter[ChapterTable.isDownloaded],
|
||||||
|
pageCount = dbChapter[ChapterTable.pageCount],
|
||||||
|
lastModifiedAt = dbChapter[ChapterTable.lastModifiedAt],
|
||||||
|
version = dbChapter[ChapterTable.version],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val map: Cache<Int, Mutex> =
|
||||||
|
Cache
|
||||||
|
.Builder<Int, Mutex>()
|
||||||
|
.expireAfterAccess(10.minutes)
|
||||||
|
.build()
|
||||||
|
|
||||||
suspend fun fetchChapterList(mangaId: Int): List<SChapter> {
|
suspend fun fetchChapterList(mangaId: Int): List<SChapter> {
|
||||||
val mutex = Manga.mangaInfoMutex.get(mangaId) { Mutex() }
|
val mutex = map.get(mangaId) { Mutex() }
|
||||||
val chapterList =
|
val chapterList =
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
val mangaEntry =
|
val manga = getManga(mangaId)
|
||||||
transaction {
|
val source = getCatalogueSourceOrStub(manga.sourceId.toLong())
|
||||||
MangaTable.selectAll().where { MangaTable.id eq mangaId }.first()
|
|
||||||
|
val sManga =
|
||||||
|
SManga.create().apply {
|
||||||
|
title = manga.title
|
||||||
|
url = manga.url
|
||||||
|
description = manga.description
|
||||||
}
|
}
|
||||||
val source = getSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
|
||||||
|
|
||||||
val chapters =
|
val currentLatestChapterNumber = Manga.getLatestChapter(mangaId)?.chapterNumber ?: 0f
|
||||||
Manga
|
val numberOfCurrentChapters = getCountOfMangaChapters(mangaId)
|
||||||
.fetchMangaAndChapters(
|
|
||||||
mangaEntry = mangaEntry,
|
|
||||||
source = source,
|
|
||||||
fetchDetails = false,
|
|
||||||
fetchChapters = true,
|
|
||||||
).chapters
|
|
||||||
|
|
||||||
updateChapterListDatabase(mangaEntry, chapters, source)
|
val chapters = source.getChapterList(sManga)
|
||||||
|
// it's possible that the source returns a list containing chapters with the same url
|
||||||
|
// once such duplicated chapters have been added, they aren't being removed anymore as long as there is
|
||||||
|
// a chapter with the same url in the fetched chapter list, even if the duplicated chapter itself
|
||||||
|
// does not exist anymore on the source
|
||||||
|
val uniqueChapters = chapters.distinctBy { it.url }
|
||||||
|
|
||||||
|
if (uniqueChapters.isEmpty()) {
|
||||||
|
throw Exception("No chapters found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recognize number for new chapters.
|
||||||
|
uniqueChapters.forEach { chapter ->
|
||||||
|
(source as? HttpSource)?.prepareNewChapter(chapter, sManga)
|
||||||
|
val chapterNumber = ChapterRecognition.parseChapterNumber(manga.title, chapter.name, chapter.chapter_number.toDouble())
|
||||||
|
chapter.chapter_number = chapterNumber.toFloat()
|
||||||
|
chapter.name = chapter.name.sanitize(manga.title)
|
||||||
|
chapter.scanlator = chapter.scanlator?.ifBlank { null }?.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
val now = Instant.now().epochSecond
|
||||||
|
// Used to not set upload date of older chapters
|
||||||
|
// to a higher value than newer chapters
|
||||||
|
var maxSeenUploadDate = 0L
|
||||||
|
|
||||||
|
val chaptersInDb =
|
||||||
|
transaction {
|
||||||
|
ChapterTable
|
||||||
|
.selectAll()
|
||||||
|
.where { ChapterTable.manga eq mangaId }
|
||||||
|
.map { ChapterTable.toDataClass(it) }
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// new chapters after they have been added to the database for auto downloads
|
||||||
|
val insertedChapterIds = mutableListOf<Int>()
|
||||||
|
|
||||||
|
val chaptersToInsert = mutableListOf<ChapterDataClass>() // do not yet have an ID from the database
|
||||||
|
val chaptersToUpdate = mutableListOf<ChapterDataClass>()
|
||||||
|
|
||||||
|
uniqueChapters.reversed().forEachIndexed { index, fetchedChapter ->
|
||||||
|
val chapterEntry = chaptersInDb.find { it.url == fetchedChapter.url }
|
||||||
|
|
||||||
|
val chapterData =
|
||||||
|
ChapterDataClass.fromSChapter(
|
||||||
|
fetchedChapter,
|
||||||
|
chapterEntry?.id ?: 0,
|
||||||
|
index + 1,
|
||||||
|
now,
|
||||||
|
mangaId,
|
||||||
|
runCatching {
|
||||||
|
(source as? HttpSource)?.getChapterUrl(fetchedChapter)
|
||||||
|
}.getOrNull(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (chapterEntry == null) {
|
||||||
|
val newChapterData =
|
||||||
|
if (chapterData.uploadDate == 0L) {
|
||||||
|
val altDateUpload = if (maxSeenUploadDate == 0L) now else maxSeenUploadDate
|
||||||
|
chapterData.copy(uploadDate = altDateUpload)
|
||||||
|
} else {
|
||||||
|
maxSeenUploadDate = max(maxSeenUploadDate, chapterData.uploadDate)
|
||||||
|
chapterData
|
||||||
|
}
|
||||||
|
chaptersToInsert.add(newChapterData)
|
||||||
|
} else {
|
||||||
|
val newChapterData =
|
||||||
|
if (chapterData.uploadDate == 0L) {
|
||||||
|
chapterData.copy(uploadDate = chapterEntry.uploadDate)
|
||||||
|
} else {
|
||||||
|
chapterData
|
||||||
|
}
|
||||||
|
chaptersToUpdate.add(newChapterData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val deletedChapterNumbers = TreeSet<Float>()
|
||||||
|
val deletedReadChapterNumbers = TreeSet<Float>()
|
||||||
|
val deletedBookmarkedChapterNumbers = TreeSet<Float>()
|
||||||
|
val deletedDownloadedChapterNumberToChapter = mutableMapOf<Float, ChapterDataClass>()
|
||||||
|
val deletedChapterNumberDateFetchMap = mutableMapOf<Float, Long>()
|
||||||
|
|
||||||
|
// clear any orphaned/duplicate chapters that are in the db but not in `chapterList`
|
||||||
|
val chapterUrls = uniqueChapters.map { it.url }.toSet()
|
||||||
|
|
||||||
|
val chaptersIdsToDelete =
|
||||||
|
chaptersInDb.mapNotNull { dbChapter ->
|
||||||
|
if (!chapterUrls.contains(dbChapter.url)) {
|
||||||
|
if (dbChapter.read) deletedReadChapterNumbers.add(dbChapter.chapterNumber)
|
||||||
|
if (dbChapter.bookmarked) deletedBookmarkedChapterNumbers.add(dbChapter.chapterNumber)
|
||||||
|
if (dbChapter.downloaded) deletedDownloadedChapterNumberToChapter[dbChapter.chapterNumber] = dbChapter
|
||||||
|
deletedChapterNumbers.add(dbChapter.chapterNumber)
|
||||||
|
deletedChapterNumberDateFetchMap[dbChapter.chapterNumber] = dbChapter.fetchedAt
|
||||||
|
dbChapter.id
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction {
|
||||||
|
// we got some clean up due
|
||||||
|
if (chaptersIdsToDelete.isNotEmpty()) {
|
||||||
|
DownloadManager.dequeue(chaptersIdsToDelete)
|
||||||
|
PageTable.deleteWhere { chapter inList chaptersIdsToDelete }
|
||||||
|
ChapterTable.deleteWhere { id inList chaptersIdsToDelete }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chaptersToInsert.isNotEmpty()) {
|
||||||
|
ChapterTable
|
||||||
|
.batchInsert(chaptersToInsert) { chapter ->
|
||||||
|
this[ChapterTable.url] = chapter.url
|
||||||
|
this[ChapterTable.name] = chapter.name
|
||||||
|
this[ChapterTable.date_upload] = chapter.uploadDate
|
||||||
|
this[ChapterTable.chapter_number] = chapter.chapterNumber
|
||||||
|
this[ChapterTable.scanlator] = chapter.scanlator
|
||||||
|
this[ChapterTable.sourceOrder] = chapter.index
|
||||||
|
this[ChapterTable.fetchedAt] = chapter.fetchedAt
|
||||||
|
this[ChapterTable.manga] = chapter.mangaId
|
||||||
|
this[ChapterTable.realUrl] = chapter.realUrl
|
||||||
|
this[ChapterTable.isRead] = false
|
||||||
|
this[ChapterTable.isBookmarked] = false
|
||||||
|
this[ChapterTable.isDownloaded] = false
|
||||||
|
this[ChapterTable.lastModifiedAt] = chapter.lastModifiedAt
|
||||||
|
this[ChapterTable.version] = chapter.version
|
||||||
|
this[ChapterTable.pageCount] = -1
|
||||||
|
|
||||||
|
// is recognized chapter number
|
||||||
|
if (chapter.chapterNumber >= 0f && chapter.chapterNumber in deletedChapterNumbers) {
|
||||||
|
this[ChapterTable.isRead] = chapter.chapterNumber in deletedReadChapterNumbers
|
||||||
|
this[ChapterTable.isBookmarked] = chapter.chapterNumber in deletedBookmarkedChapterNumbers
|
||||||
|
|
||||||
|
// Try to use the fetch date of the original entry to not pollute 'Updates' tab
|
||||||
|
deletedChapterNumberDateFetchMap[chapter.chapterNumber]?.let {
|
||||||
|
this[ChapterTable.fetchedAt] = it
|
||||||
|
}
|
||||||
|
|
||||||
|
deletedDownloadedChapterNumberToChapter[chapter.chapterNumber]?.let {
|
||||||
|
val hasDownloadedPages = it.pageCount > 0
|
||||||
|
val isSameName = it.name == chapter.name
|
||||||
|
val isSameScanlator = it.scanlator == chapter.scanlator
|
||||||
|
|
||||||
|
// Only preserve download status for chapters with the same name and of the same scanlator; otherwise,
|
||||||
|
// the downloaded files won't be found anyway
|
||||||
|
val isDownloadPreservable = hasDownloadedPages && isSameName && isSameScanlator
|
||||||
|
if (isDownloadPreservable) {
|
||||||
|
this[ChapterTable.isDownloaded] = true
|
||||||
|
this[ChapterTable.pageCount] = it.pageCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.forEach { insertedChapterIds.add(it[ChapterTable.id].value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chaptersToUpdate.isNotEmpty()) {
|
||||||
|
BatchUpdateStatement(ChapterTable)
|
||||||
|
.apply {
|
||||||
|
chaptersToUpdate.forEach {
|
||||||
|
addBatch(EntityID(it.id, ChapterTable))
|
||||||
|
|
||||||
|
val currentChapter = chaptersInDb.find { dbChapter -> dbChapter.id == it.id }!!
|
||||||
|
|
||||||
|
this[ChapterTable.name] = it.name
|
||||||
|
this[ChapterTable.date_upload] = it.uploadDate
|
||||||
|
this[ChapterTable.chapter_number] = it.chapterNumber
|
||||||
|
this[ChapterTable.scanlator] = it.scanlator
|
||||||
|
this[ChapterTable.sourceOrder] = it.index
|
||||||
|
this[ChapterTable.realUrl] = it.realUrl
|
||||||
|
this[ChapterTable.lastModifiedAt] = it.lastModifiedAt
|
||||||
|
this[ChapterTable.version] = it.version
|
||||||
|
this[ChapterTable.isDownloaded] = currentChapter.downloaded
|
||||||
|
this[ChapterTable.pageCount] = currentChapter.pageCount
|
||||||
|
|
||||||
|
if (!currentChapter.downloaded) {
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
val isSameScanlator = currentChapter.scanlator == it.scanlator
|
||||||
|
val isSameName = currentChapter.name == it.name
|
||||||
|
|
||||||
|
val isDownloadPreservable = isSameName && isSameScanlator
|
||||||
|
if (!isDownloadPreservable) {
|
||||||
|
this[ChapterTable.isDownloaded] = false
|
||||||
|
this[ChapterTable.pageCount] = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.toExecutable()
|
||||||
|
.execute(this@transaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||||
|
it[chaptersLastFetchedAt] = Instant.now().epochSecond
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manga.inLibrary) {
|
||||||
|
// We have to query the inserted chapters to get the up-to-date data. I.e. "last_modified_at" is not returned by the insert statement, due to being set by a DB trigger
|
||||||
|
val insertedChapters =
|
||||||
|
transaction {
|
||||||
|
ChapterTable.selectAll().where { ChapterTable.id inList insertedChapterIds }.map(
|
||||||
|
ChapterTable::toDataClass,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
downloadNewChapters(mangaId, currentLatestChapterNumber, numberOfCurrentChapters, insertedChapters)
|
||||||
|
}
|
||||||
|
|
||||||
|
uniqueChapters
|
||||||
}
|
}
|
||||||
|
|
||||||
return chapterList
|
return chapterList
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateChapterListDatabase(
|
|
||||||
mangaEntry: ResultRow,
|
|
||||||
chapters: List<SChapter>,
|
|
||||||
source: Source,
|
|
||||||
): List<SChapter> {
|
|
||||||
val currentLatestChapterNumber = Manga.getLatestChapter(mangaEntry[MangaTable.id].value)?.chapterNumber ?: 0f
|
|
||||||
val numberOfCurrentChapters = getCountOfMangaChapters(mangaEntry[MangaTable.id].value)
|
|
||||||
// it's possible that the source returns a list containing chapters with the same url
|
|
||||||
// once such duplicated chapters have been added, they aren't being removed anymore as long as there is
|
|
||||||
// a chapter with the same url in the fetched chapter list, even if the duplicated chapter itself
|
|
||||||
// does not exist anymore on the source
|
|
||||||
val uniqueChapters = chapters.distinctBy { it.url }
|
|
||||||
|
|
||||||
if (uniqueChapters.isEmpty()) {
|
|
||||||
throw Exception("No chapters found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recognize number for new chapters.
|
|
||||||
val sManga =
|
|
||||||
SManga.create().apply {
|
|
||||||
url = mangaEntry[MangaTable.url]
|
|
||||||
title = mangaEntry[MangaTable.title]
|
|
||||||
thumbnail_url = mangaEntry[MangaTable.thumbnail_url]
|
|
||||||
artist = mangaEntry[MangaTable.artist]
|
|
||||||
author = mangaEntry[MangaTable.author]
|
|
||||||
description = mangaEntry[MangaTable.description]
|
|
||||||
genre = mangaEntry[MangaTable.genre]
|
|
||||||
status = mangaEntry[MangaTable.status]
|
|
||||||
update_strategy = UpdateStrategy.valueOf(mangaEntry[MangaTable.updateStrategy])
|
|
||||||
memo = Json.decodeFromString(mangaEntry[MangaTable.memo])
|
|
||||||
initialized = mangaEntry[MangaTable.initialized]
|
|
||||||
}
|
|
||||||
uniqueChapters.forEach { chapter ->
|
|
||||||
(source as? HttpSource)?.prepareNewChapter(chapter, sManga)
|
|
||||||
val chapterNumber =
|
|
||||||
ChapterRecognition.parseChapterNumber(
|
|
||||||
mangaEntry[MangaTable.title],
|
|
||||||
chapter.name,
|
|
||||||
chapter.chapter_number.toDouble(),
|
|
||||||
)
|
|
||||||
chapter.chapter_number = chapterNumber.toFloat()
|
|
||||||
chapter.name = chapter.name.sanitize(mangaEntry[MangaTable.title])
|
|
||||||
chapter.scanlator = chapter.scanlator?.ifBlank { null }?.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
val now = Instant.now().epochSecond
|
|
||||||
// Used to not set upload date of older chapters
|
|
||||||
// to a higher value than newer chapters
|
|
||||||
var maxSeenUploadDate = 0L
|
|
||||||
|
|
||||||
val chaptersInDb =
|
|
||||||
transaction {
|
|
||||||
ChapterTable
|
|
||||||
.selectAll()
|
|
||||||
.where { ChapterTable.manga eq mangaEntry[MangaTable.id].value }
|
|
||||||
.map { ChapterTable.toDataClass(it) }
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
// new chapters after they have been added to the database for auto downloads
|
|
||||||
val insertedChapterIds = mutableListOf<Int>()
|
|
||||||
|
|
||||||
val chaptersToInsert = mutableListOf<ChapterDataClass>() // do not yet have an ID from the database
|
|
||||||
val chaptersToUpdate = mutableListOf<ChapterDataClass>()
|
|
||||||
|
|
||||||
uniqueChapters.reversed().forEachIndexed { index, fetchedChapter ->
|
|
||||||
val chapterEntry = chaptersInDb.find { it.url == fetchedChapter.url }
|
|
||||||
|
|
||||||
val chapterData =
|
|
||||||
ChapterDataClass.fromSChapter(
|
|
||||||
fetchedChapter,
|
|
||||||
chapterEntry?.id ?: 0,
|
|
||||||
index + 1,
|
|
||||||
now,
|
|
||||||
mangaEntry[MangaTable.id].value,
|
|
||||||
runCatching {
|
|
||||||
(source as? HttpSource)?.getChapterUrl(fetchedChapter)
|
|
||||||
}.getOrNull(),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (chapterEntry == null) {
|
|
||||||
val newChapterData =
|
|
||||||
if (chapterData.uploadDate == 0L) {
|
|
||||||
val altDateUpload = if (maxSeenUploadDate == 0L) now else maxSeenUploadDate
|
|
||||||
chapterData.copy(uploadDate = altDateUpload)
|
|
||||||
} else {
|
|
||||||
maxSeenUploadDate = max(maxSeenUploadDate, chapterData.uploadDate)
|
|
||||||
chapterData
|
|
||||||
}
|
|
||||||
chaptersToInsert.add(newChapterData)
|
|
||||||
} else {
|
|
||||||
val newChapterData =
|
|
||||||
if (chapterData.uploadDate == 0L) {
|
|
||||||
chapterData.copy(uploadDate = chapterEntry.uploadDate)
|
|
||||||
} else {
|
|
||||||
chapterData
|
|
||||||
}
|
|
||||||
chaptersToUpdate.add(newChapterData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val deletedChapterNumbers = TreeSet<Float>()
|
|
||||||
val deletedReadChapterNumbers = TreeSet<Float>()
|
|
||||||
val deletedBookmarkedChapterNumbers = TreeSet<Float>()
|
|
||||||
val deletedDownloadedChapterNumberToChapter = mutableMapOf<Float, ChapterDataClass>()
|
|
||||||
val deletedChapterNumberDateFetchMap = mutableMapOf<Float, Long>()
|
|
||||||
|
|
||||||
// clear any orphaned/duplicate chapters that are in the db but not in `chapterList`
|
|
||||||
val chapterUrls = uniqueChapters.map { it.url }.toSet()
|
|
||||||
|
|
||||||
val chaptersIdsToDelete =
|
|
||||||
chaptersInDb.mapNotNull { dbChapter ->
|
|
||||||
if (!chapterUrls.contains(dbChapter.url)) {
|
|
||||||
if (dbChapter.read) deletedReadChapterNumbers.add(dbChapter.chapterNumber)
|
|
||||||
if (dbChapter.bookmarked) deletedBookmarkedChapterNumbers.add(dbChapter.chapterNumber)
|
|
||||||
if (dbChapter.downloaded) deletedDownloadedChapterNumberToChapter[dbChapter.chapterNumber] = dbChapter
|
|
||||||
deletedChapterNumbers.add(dbChapter.chapterNumber)
|
|
||||||
deletedChapterNumberDateFetchMap[dbChapter.chapterNumber] = dbChapter.fetchedAt
|
|
||||||
dbChapter.id
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
transaction {
|
|
||||||
// we got some clean up due
|
|
||||||
if (chaptersIdsToDelete.isNotEmpty()) {
|
|
||||||
DownloadManager.dequeue(chaptersIdsToDelete)
|
|
||||||
PageTable.deleteWhere { chapter inList chaptersIdsToDelete }
|
|
||||||
ChapterTable.deleteWhere { id inList chaptersIdsToDelete }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chaptersToInsert.isNotEmpty()) {
|
|
||||||
ChapterTable
|
|
||||||
.batchInsert(chaptersToInsert) { chapter ->
|
|
||||||
this[ChapterTable.url] = chapter.url
|
|
||||||
this[ChapterTable.name] = chapter.name
|
|
||||||
this[ChapterTable.date_upload] = chapter.uploadDate
|
|
||||||
this[ChapterTable.chapter_number] = chapter.chapterNumber
|
|
||||||
this[ChapterTable.scanlator] = chapter.scanlator
|
|
||||||
this[ChapterTable.sourceOrder] = chapter.index
|
|
||||||
this[ChapterTable.fetchedAt] = chapter.fetchedAt
|
|
||||||
this[ChapterTable.manga] = chapter.mangaId
|
|
||||||
this[ChapterTable.realUrl] = chapter.realUrl
|
|
||||||
this[ChapterTable.memo] = Json.encodeToString(chapter.memo)
|
|
||||||
this[ChapterTable.isRead] = false
|
|
||||||
this[ChapterTable.isBookmarked] = false
|
|
||||||
this[ChapterTable.isDownloaded] = false
|
|
||||||
this[ChapterTable.lastModifiedAt] = chapter.lastModifiedAt
|
|
||||||
this[ChapterTable.version] = chapter.version
|
|
||||||
this[ChapterTable.pageCount] = -1
|
|
||||||
|
|
||||||
// is recognized chapter number
|
|
||||||
if (chapter.chapterNumber >= 0f && chapter.chapterNumber in deletedChapterNumbers) {
|
|
||||||
this[ChapterTable.isRead] = chapter.chapterNumber in deletedReadChapterNumbers
|
|
||||||
this[ChapterTable.isBookmarked] = chapter.chapterNumber in deletedBookmarkedChapterNumbers
|
|
||||||
|
|
||||||
// Try to use the fetch date of the original entry to not pollute 'Updates' tab
|
|
||||||
deletedChapterNumberDateFetchMap[chapter.chapterNumber]?.let {
|
|
||||||
this[ChapterTable.fetchedAt] = it
|
|
||||||
}
|
|
||||||
|
|
||||||
deletedDownloadedChapterNumberToChapter[chapter.chapterNumber]?.let {
|
|
||||||
val hasDownloadedPages = it.pageCount > 0
|
|
||||||
val isSameName = it.name == chapter.name
|
|
||||||
val isSameScanlator = it.scanlator == chapter.scanlator
|
|
||||||
|
|
||||||
// Only preserve download status for chapters with the same name and of the same scanlator; otherwise,
|
|
||||||
// the downloaded files won't be found anyway
|
|
||||||
val isDownloadPreservable = hasDownloadedPages && isSameName && isSameScanlator
|
|
||||||
if (isDownloadPreservable) {
|
|
||||||
this[ChapterTable.isDownloaded] = true
|
|
||||||
this[ChapterTable.pageCount] = it.pageCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.forEach { insertedChapterIds.add(it[ChapterTable.id].value) }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chaptersToUpdate.isNotEmpty()) {
|
|
||||||
BatchUpdateStatement(ChapterTable)
|
|
||||||
.apply {
|
|
||||||
chaptersToUpdate.forEach {
|
|
||||||
addBatch(EntityID(it.id, ChapterTable))
|
|
||||||
|
|
||||||
val currentChapter = chaptersInDb.find { dbChapter -> dbChapter.id == it.id }!!
|
|
||||||
|
|
||||||
this[ChapterTable.name] = it.name
|
|
||||||
this[ChapterTable.date_upload] = it.uploadDate
|
|
||||||
this[ChapterTable.chapter_number] = it.chapterNumber
|
|
||||||
this[ChapterTable.scanlator] = it.scanlator
|
|
||||||
this[ChapterTable.sourceOrder] = it.index
|
|
||||||
this[ChapterTable.realUrl] = it.realUrl
|
|
||||||
this[ChapterTable.lastModifiedAt] = it.lastModifiedAt
|
|
||||||
this[ChapterTable.version] = it.version
|
|
||||||
this[ChapterTable.memo] = Json.encodeToString(it.memo)
|
|
||||||
this[ChapterTable.isDownloaded] = currentChapter.downloaded
|
|
||||||
this[ChapterTable.pageCount] = currentChapter.pageCount
|
|
||||||
|
|
||||||
if (!currentChapter.downloaded) {
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
|
|
||||||
val isSameScanlator = currentChapter.scanlator == it.scanlator
|
|
||||||
val isSameName = currentChapter.name == it.name
|
|
||||||
|
|
||||||
val isDownloadPreservable = isSameName && isSameScanlator
|
|
||||||
if (!isDownloadPreservable) {
|
|
||||||
this[ChapterTable.isDownloaded] = false
|
|
||||||
this[ChapterTable.pageCount] = -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.toExecutable()
|
|
||||||
.execute(this@transaction)
|
|
||||||
}
|
|
||||||
|
|
||||||
MangaTable.update({ MangaTable.id eq mangaEntry[MangaTable.id].value }) {
|
|
||||||
it[chaptersLastFetchedAt] = Instant.now().epochSecond
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mangaEntry[MangaTable.inLibrary]) {
|
|
||||||
// We have to query the inserted chapters to get the up-to-date data. I.e. "last_modified_at" is not returned by the insert statement, due to being set by a DB trigger
|
|
||||||
val insertedChapters =
|
|
||||||
transaction {
|
|
||||||
ChapterTable.selectAll().where { ChapterTable.id inList insertedChapterIds }.map(
|
|
||||||
ChapterTable::toDataClass,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
downloadNewChapters(
|
|
||||||
mangaEntry[MangaTable.id].value,
|
|
||||||
currentLatestChapterNumber,
|
|
||||||
numberOfCurrentChapters,
|
|
||||||
insertedChapters,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return uniqueChapters
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun downloadNewChapters(
|
private fun downloadNewChapters(
|
||||||
mangaId: Int,
|
mangaId: Int,
|
||||||
prevLatestChapterNumber: Float,
|
prevLatestChapterNumber: Float,
|
||||||
|
|||||||
@@ -11,20 +11,13 @@ import eu.kanade.tachiyomi.network.GET
|
|||||||
import eu.kanade.tachiyomi.network.HttpException
|
import eu.kanade.tachiyomi.network.HttpException
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.model.SMangaUpdate
|
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import io.github.oshai.kotlinlogging.KLogger
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import io.github.reactivecircus.cache4k.Cache
|
|
||||||
import io.javalin.http.HttpStatus
|
import io.javalin.http.HttpStatus
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.CacheControl
|
import okhttp3.CacheControl
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jetbrains.exposed.v1.core.ResultRow
|
import org.jetbrains.exposed.v1.core.ResultRow
|
||||||
@@ -39,10 +32,13 @@ import org.jetbrains.exposed.v1.jdbc.selectAll
|
|||||||
import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
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.MangaList.proxyThumbnailUrl
|
||||||
|
import suwayomi.tachidesk.manga.impl.Source.getSource
|
||||||
import suwayomi.tachidesk.manga.impl.download.fileProvider.impl.MissingThumbnailException
|
import suwayomi.tachidesk.manga.impl.download.fileProvider.impl.MissingThumbnailException
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.Track
|
||||||
import suwayomi.tachidesk.manga.impl.util.network.await
|
import suwayomi.tachidesk.manga.impl.util.network.await
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrNull
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.StubSource
|
import suwayomi.tachidesk.manga.impl.util.source.StubSource
|
||||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.clearCachedImage
|
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.clearCachedImage
|
||||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
|
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
|
||||||
@@ -51,8 +47,10 @@ import suwayomi.tachidesk.manga.impl.util.updateMangaDownloadDir
|
|||||||
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.IncludeOrExclude
|
import suwayomi.tachidesk.manga.model.dataclass.IncludeOrExclude
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
|
||||||
|
import suwayomi.tachidesk.manga.model.dataclass.toGenreList
|
||||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaMetaTable
|
import suwayomi.tachidesk.manga.model.table.MangaMetaTable
|
||||||
|
import suwayomi.tachidesk.manga.model.table.MangaStatus
|
||||||
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.ApplicationDirs
|
import suwayomi.tachidesk.server.ApplicationDirs
|
||||||
@@ -61,17 +59,10 @@ import java.io.File
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import kotlin.time.Duration.Companion.minutes
|
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger { }
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
object Manga {
|
object Manga {
|
||||||
val mangaInfoMutex: Cache<Int, Mutex> =
|
|
||||||
Cache
|
|
||||||
.Builder<Int, Mutex>()
|
|
||||||
.expireAfterAccess(10.minutes)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
suspend fun getManga(
|
suspend fun getManga(
|
||||||
mangaId: Int,
|
mangaId: Int,
|
||||||
onlineFetch: Boolean = false,
|
onlineFetch: Boolean = false,
|
||||||
@@ -79,118 +70,63 @@ object Manga {
|
|||||||
var mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
var mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||||
|
|
||||||
return if (!onlineFetch && mangaEntry[MangaTable.initialized]) {
|
return if (!onlineFetch && mangaEntry[MangaTable.initialized]) {
|
||||||
MangaTable.toDataClass(mangaEntry)
|
getMangaDataClass(mangaId, mangaEntry)
|
||||||
} else { // initialize manga
|
} else { // initialize manga
|
||||||
updateMangaAndChapters(mangaId, updateChapters = false)
|
val sManga = fetchManga(mangaId) ?: return getMangaDataClass(mangaId, mangaEntry)
|
||||||
|
|
||||||
mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||||
|
|
||||||
MangaTable.toDataClass(mangaEntry).copy(freshData = true)
|
MangaDataClass(
|
||||||
|
id = mangaId,
|
||||||
|
sourceId = mangaEntry[MangaTable.sourceReference].toString(),
|
||||||
|
url = mangaEntry[MangaTable.url],
|
||||||
|
title = mangaEntry[MangaTable.title],
|
||||||
|
thumbnailUrl = proxyThumbnailUrl(mangaId),
|
||||||
|
thumbnailUrlLastFetched = mangaEntry[MangaTable.thumbnailUrlLastFetched],
|
||||||
|
initialized = true,
|
||||||
|
artist = sManga.artist,
|
||||||
|
author = sManga.author,
|
||||||
|
description = sManga.description,
|
||||||
|
genre = sManga.genre.toGenreList(),
|
||||||
|
status = MangaStatus.valueOf(sManga.status).name,
|
||||||
|
inLibrary = mangaEntry[MangaTable.inLibrary],
|
||||||
|
inLibraryAt = mangaEntry[MangaTable.inLibraryAt],
|
||||||
|
source = getSource(mangaEntry[MangaTable.sourceReference]),
|
||||||
|
realUrl = mangaEntry[MangaTable.realUrl],
|
||||||
|
lastFetchedAt = mangaEntry[MangaTable.lastFetchedAt],
|
||||||
|
chaptersLastFetchedAt = mangaEntry[MangaTable.chaptersLastFetchedAt],
|
||||||
|
updateStrategy = UpdateStrategy.valueOf(mangaEntry[MangaTable.updateStrategy]),
|
||||||
|
freshData = true,
|
||||||
|
trackers = Track.getTrackRecordsByMangaId(mangaId),
|
||||||
|
lastModifiedAt = mangaEntry[MangaTable.lastModifiedAt],
|
||||||
|
version = mangaEntry[MangaTable.version],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun fetchMangaAndChapters(
|
|
||||||
mangaEntry: ResultRow,
|
|
||||||
source: Source,
|
|
||||||
fetchDetails: Boolean,
|
|
||||||
fetchChapters: Boolean,
|
|
||||||
): SMangaUpdate {
|
|
||||||
val sManga =
|
|
||||||
SManga.create().apply {
|
|
||||||
url = mangaEntry[MangaTable.url]
|
|
||||||
title = mangaEntry[MangaTable.title]
|
|
||||||
thumbnail_url = mangaEntry[MangaTable.thumbnail_url]
|
|
||||||
artist = mangaEntry[MangaTable.artist]
|
|
||||||
author = mangaEntry[MangaTable.author]
|
|
||||||
description = mangaEntry[MangaTable.description]
|
|
||||||
genre = mangaEntry[MangaTable.genre]
|
|
||||||
status = mangaEntry[MangaTable.status]
|
|
||||||
update_strategy = UpdateStrategy.valueOf(mangaEntry[MangaTable.updateStrategy])
|
|
||||||
memo = Json.decodeFromString(mangaEntry[MangaTable.memo])
|
|
||||||
initialized = mangaEntry[MangaTable.initialized]
|
|
||||||
}
|
|
||||||
val sChapters =
|
|
||||||
transaction {
|
|
||||||
ChapterTable
|
|
||||||
.selectAll()
|
|
||||||
.where { ChapterTable.manga eq mangaEntry[MangaTable.id] }
|
|
||||||
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC)
|
|
||||||
.map {
|
|
||||||
SChapter.create().apply {
|
|
||||||
url = it[ChapterTable.url]
|
|
||||||
name = it[ChapterTable.name]
|
|
||||||
chapter_number = it[ChapterTable.chapter_number]
|
|
||||||
scanlator = it[ChapterTable.scanlator]
|
|
||||||
date_upload = it[ChapterTable.date_upload]
|
|
||||||
memo = Json.decodeFromString(it[ChapterTable.memo])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return source.getMangaUpdate(
|
|
||||||
sManga,
|
|
||||||
sChapters,
|
|
||||||
fetchDetails = fetchDetails,
|
|
||||||
fetchChapters = fetchChapters,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun fetchManga(mangaId: Int): SManga? {
|
suspend fun fetchManga(mangaId: Int): SManga? {
|
||||||
return mangaInfoMutex.get(mangaId) { Mutex() }.withLock {
|
val mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||||
val mangaEntry =
|
|
||||||
transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
|
||||||
val source = getSourceOrNull(mangaEntry[MangaTable.sourceReference]) ?: return null
|
|
||||||
val sManga =
|
|
||||||
fetchMangaAndChapters(
|
|
||||||
mangaEntry,
|
|
||||||
source,
|
|
||||||
fetchDetails = true,
|
|
||||||
fetchChapters = false,
|
|
||||||
).manga
|
|
||||||
|
|
||||||
updateMangaDatabase(mangaEntry, source, sManga)
|
val source =
|
||||||
}
|
getCatalogueSourceOrNull(mangaEntry[MangaTable.sourceReference])
|
||||||
}
|
?: return null
|
||||||
|
val sManga =
|
||||||
|
source.getMangaDetails(
|
||||||
|
SManga.create().apply {
|
||||||
|
url = mangaEntry[MangaTable.url]
|
||||||
|
title = mangaEntry[MangaTable.title]
|
||||||
|
thumbnail_url = mangaEntry[MangaTable.thumbnail_url]
|
||||||
|
artist = mangaEntry[MangaTable.artist]
|
||||||
|
author = mangaEntry[MangaTable.author]
|
||||||
|
description = mangaEntry[MangaTable.description]
|
||||||
|
genre = mangaEntry[MangaTable.genre]
|
||||||
|
status = mangaEntry[MangaTable.status]
|
||||||
|
update_strategy = UpdateStrategy.valueOf(mangaEntry[MangaTable.updateStrategy])
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
suspend fun updateMangaAndChapters(
|
|
||||||
mangaId: Int,
|
|
||||||
updateManga: Boolean = true,
|
|
||||||
updateChapters: Boolean = true,
|
|
||||||
) {
|
|
||||||
mangaInfoMutex.get(mangaId) { Mutex() }.withLock {
|
|
||||||
var mangaEntry =
|
|
||||||
transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
|
||||||
val source =
|
|
||||||
getSourceOrNull(mangaEntry[MangaTable.sourceReference])
|
|
||||||
?: throw NullPointerException("Missing source ${mangaEntry[MangaTable.sourceReference]}")
|
|
||||||
val mangaUpdate =
|
|
||||||
fetchMangaAndChapters(
|
|
||||||
mangaEntry,
|
|
||||||
source,
|
|
||||||
fetchDetails = updateManga,
|
|
||||||
fetchChapters = updateChapters,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (updateManga) {
|
|
||||||
updateMangaDatabase(mangaEntry, source, mangaUpdate.manga)
|
|
||||||
mangaEntry =
|
|
||||||
transaction {
|
|
||||||
MangaTable.selectAll().where { MangaTable.id eq mangaId }.first()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (updateChapters) {
|
|
||||||
Chapter.updateChapterListDatabase(mangaEntry, mangaUpdate.chapters, source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateMangaDatabase(
|
|
||||||
mangaEntry: ResultRow,
|
|
||||||
source: Source,
|
|
||||||
sManga: SManga,
|
|
||||||
): SManga {
|
|
||||||
transaction {
|
transaction {
|
||||||
MangaTable.update({ MangaTable.id eq mangaEntry[MangaTable.id] }) {
|
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||||
val remoteTitle =
|
val remoteTitle =
|
||||||
try {
|
try {
|
||||||
sManga.title
|
sManga.title
|
||||||
@@ -215,7 +151,7 @@ object Manga {
|
|||||||
if (!sManga.thumbnail_url.isNullOrEmpty()) {
|
if (!sManga.thumbnail_url.isNullOrEmpty()) {
|
||||||
it[MangaTable.thumbnail_url] = sManga.thumbnail_url
|
it[MangaTable.thumbnail_url] = sManga.thumbnail_url
|
||||||
it[MangaTable.thumbnailUrlLastFetched] = Instant.now().epochSecond
|
it[MangaTable.thumbnailUrlLastFetched] = Instant.now().epochSecond
|
||||||
clearThumbnail(mangaEntry[MangaTable.id].value)
|
clearThumbnail(mangaId)
|
||||||
}
|
}
|
||||||
|
|
||||||
it[MangaTable.realUrl] =
|
it[MangaTable.realUrl] =
|
||||||
@@ -238,7 +174,6 @@ object Manga {
|
|||||||
it[MangaTable.lastFetchedAt] = Instant.now().epochSecond
|
it[MangaTable.lastFetchedAt] = Instant.now().epochSecond
|
||||||
|
|
||||||
it[MangaTable.updateStrategy] = sManga.update_strategy.name
|
it[MangaTable.updateStrategy] = sManga.update_strategy.name
|
||||||
it[MangaTable.memo] = Json.encodeToString(sManga.memo)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,6 +221,35 @@ object Manga {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getMangaDataClass(
|
||||||
|
mangaId: Int,
|
||||||
|
mangaEntry: ResultRow,
|
||||||
|
) = MangaDataClass(
|
||||||
|
id = mangaId,
|
||||||
|
sourceId = mangaEntry[MangaTable.sourceReference].toString(),
|
||||||
|
url = mangaEntry[MangaTable.url],
|
||||||
|
title = mangaEntry[MangaTable.title],
|
||||||
|
thumbnailUrl = proxyThumbnailUrl(mangaId),
|
||||||
|
thumbnailUrlLastFetched = mangaEntry[MangaTable.thumbnailUrlLastFetched],
|
||||||
|
initialized = true,
|
||||||
|
artist = mangaEntry[MangaTable.artist],
|
||||||
|
author = mangaEntry[MangaTable.author],
|
||||||
|
description = mangaEntry[MangaTable.description],
|
||||||
|
genre = mangaEntry[MangaTable.genre].toGenreList(),
|
||||||
|
status = MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
||||||
|
inLibrary = mangaEntry[MangaTable.inLibrary],
|
||||||
|
inLibraryAt = mangaEntry[MangaTable.inLibraryAt],
|
||||||
|
source = getSource(mangaEntry[MangaTable.sourceReference]),
|
||||||
|
realUrl = mangaEntry[MangaTable.realUrl],
|
||||||
|
lastFetchedAt = mangaEntry[MangaTable.lastFetchedAt],
|
||||||
|
chaptersLastFetchedAt = mangaEntry[MangaTable.chaptersLastFetchedAt],
|
||||||
|
updateStrategy = UpdateStrategy.valueOf(mangaEntry[MangaTable.updateStrategy]),
|
||||||
|
freshData = false,
|
||||||
|
trackers = Track.getTrackRecordsByMangaId(mangaId),
|
||||||
|
lastModifiedAt = mangaEntry[MangaTable.lastModifiedAt],
|
||||||
|
version = mangaEntry[MangaTable.version],
|
||||||
|
)
|
||||||
|
|
||||||
fun getMangaMetaMap(mangaId: Int): Map<String, String> =
|
fun getMangaMetaMap(mangaId: Int): Map<String, String> =
|
||||||
transaction {
|
transaction {
|
||||||
MangaMetaTable
|
MangaMetaTable
|
||||||
@@ -413,7 +377,7 @@ object Manga {
|
|||||||
val mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
val mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||||
val sourceId = mangaEntry[MangaTable.sourceReference]
|
val sourceId = mangaEntry[MangaTable.sourceReference]
|
||||||
|
|
||||||
return when (val source = getSourceOrStub(sourceId)) {
|
return when (val source = getCatalogueSourceOrStub(sourceId)) {
|
||||||
is HttpSource -> {
|
is HttpSource -> {
|
||||||
getImageResponse(cacheSaveDir, fileName) {
|
getImageResponse(cacheSaveDir, fileName) {
|
||||||
fetchHttpSourceMangaThumbnail(source, mangaEntry)
|
fetchHttpSourceMangaThumbnail(source, mangaEntry)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ package suwayomi.tachidesk.manga.impl
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import org.jetbrains.exposed.v1.core.and
|
import org.jetbrains.exposed.v1.core.and
|
||||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
@@ -19,7 +18,7 @@ import org.jetbrains.exposed.v1.jdbc.batchInsert
|
|||||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
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 suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
|
||||||
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
|
||||||
@@ -36,7 +35,7 @@ object MangaList {
|
|||||||
require(pageNum > 0) {
|
require(pageNum > 0) {
|
||||||
"pageNum = $pageNum is not in valid range"
|
"pageNum = $pageNum is not in valid range"
|
||||||
}
|
}
|
||||||
val source = getSourceOrStub(sourceId)
|
val source = getCatalogueSourceOrStub(sourceId)
|
||||||
val mangasPage =
|
val mangasPage =
|
||||||
if (popular) {
|
if (popular) {
|
||||||
source.getPopularManga(pageNum)
|
source.getPopularManga(pageNum)
|
||||||
@@ -76,7 +75,6 @@ object MangaList {
|
|||||||
this[MangaTable.status] = it.status
|
this[MangaTable.status] = it.status
|
||||||
this[MangaTable.thumbnail_url] = it.thumbnail_url
|
this[MangaTable.thumbnail_url] = it.thumbnail_url
|
||||||
this[MangaTable.updateStrategy] = it.update_strategy.name
|
this[MangaTable.updateStrategy] = it.update_strategy.name
|
||||||
this[MangaTable.memo] = Json.encodeToString(it.memo)
|
|
||||||
|
|
||||||
this[MangaTable.sourceReference] = sourceId
|
this[MangaTable.sourceReference] = sourceId
|
||||||
}.associate { Pair(it[MangaTable.url], it[MangaTable.id].value) }
|
}.associate { Pair(it[MangaTable.url], it[MangaTable.id].value) }
|
||||||
@@ -105,7 +103,6 @@ object MangaList {
|
|||||||
this[MangaTable.status] = sManga.status
|
this[MangaTable.status] = sManga.status
|
||||||
this[MangaTable.thumbnail_url] = sManga.thumbnail_url ?: manga[MangaTable.thumbnail_url]
|
this[MangaTable.thumbnail_url] = sManga.thumbnail_url ?: manga[MangaTable.thumbnail_url]
|
||||||
this[MangaTable.updateStrategy] = sManga.update_strategy.name
|
this[MangaTable.updateStrategy] = sManga.update_strategy.name
|
||||||
this[MangaTable.memo] = Json.encodeToString(sManga.memo)
|
|
||||||
if (!sManga.thumbnail_url.isNullOrEmpty() && manga[MangaTable.thumbnail_url] != sManga.thumbnail_url) {
|
if (!sManga.thumbnail_url.isNullOrEmpty() && manga[MangaTable.thumbnail_url] != sManga.thumbnail_url) {
|
||||||
this[MangaTable.thumbnailUrlLastFetched] = Instant.now().epochSecond
|
this[MangaTable.thumbnailUrlLastFetched] = Instant.now().epochSecond
|
||||||
Manga.clearThumbnail(manga[MangaTable.id].value)
|
Manga.clearThumbnail(manga[MangaTable.id].value)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ 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.graphql.types.DownloadConversion
|
import suwayomi.tachidesk.graphql.types.DownloadConversion
|
||||||
import suwayomi.tachidesk.manga.impl.util.getChapterCachePath
|
import suwayomi.tachidesk.manga.impl.util.getChapterCachePath
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||||
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.ImageUtil
|
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
|
||||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||||
@@ -118,7 +118,7 @@ object Page {
|
|||||||
return imageFile.inputStream() to (ImageUtil.findImageType { imageFile.inputStream() }?.mime ?: "image/jpeg")
|
return imageFile.inputStream() to (ImageUtil.findImageType { imageFile.inputStream() }?.mime ?: "image/jpeg")
|
||||||
}
|
}
|
||||||
|
|
||||||
val source = getSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||||
source as HttpSource
|
source as HttpSource
|
||||||
|
|
||||||
if (pageEntry[PageTable.imageUrl] == null) {
|
if (pageEntry[PageTable.imageUrl] == null) {
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ package suwayomi.tachidesk.manga.impl
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import io.javalin.json.JsonMapper
|
import io.javalin.json.JsonMapper
|
||||||
import io.javalin.json.fromJsonString
|
import io.javalin.json.fromJsonString
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import suwayomi.tachidesk.manga.impl.MangaList.processEntries
|
import suwayomi.tachidesk.manga.impl.MangaList.processEntries
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ object Search {
|
|||||||
searchTerm: String,
|
searchTerm: String,
|
||||||
pageNum: Int,
|
pageNum: Int,
|
||||||
): PagedMangaListDataClass {
|
): PagedMangaListDataClass {
|
||||||
val source = getSourceOrStub(sourceId)
|
val source = getCatalogueSourceOrStub(sourceId)
|
||||||
val searchManga = source.getSearchManga(pageNum, searchTerm, getFilterListOf(source))
|
val searchManga = source.getSearchManga(pageNum, searchTerm, getFilterListOf(source))
|
||||||
return searchManga.processEntries(sourceId)
|
return searchManga.processEntries(sourceId)
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ object Search {
|
|||||||
pageNum: Int,
|
pageNum: Int,
|
||||||
filter: FilterData,
|
filter: FilterData,
|
||||||
): PagedMangaListDataClass {
|
): PagedMangaListDataClass {
|
||||||
val source = getSourceOrStub(sourceId)
|
val source = getCatalogueSourceOrStub(sourceId)
|
||||||
val filterList = if (filter.filter != null) buildFilterList(sourceId, filter.filter) else source.getFilterList()
|
val filterList = if (filter.filter != null) buildFilterList(sourceId, filter.filter) else source.getFilterList()
|
||||||
val searchManga = source.getSearchManga(pageNum, filter.searchTerm ?: "", filterList)
|
val searchManga = source.getSearchManga(pageNum, filter.searchTerm ?: "", filterList)
|
||||||
return searchManga.processEntries(sourceId)
|
return searchManga.processEntries(sourceId)
|
||||||
@@ -43,7 +43,7 @@ object Search {
|
|||||||
private val filterListCache = mutableMapOf<Long, FilterList>()
|
private val filterListCache = mutableMapOf<Long, FilterList>()
|
||||||
|
|
||||||
private fun getFilterListOf(
|
private fun getFilterListOf(
|
||||||
source: Source,
|
source: CatalogueSource,
|
||||||
reset: Boolean = false,
|
reset: Boolean = false,
|
||||||
): FilterList {
|
): FilterList {
|
||||||
if (reset || !filterListCache.containsKey(source.id)) {
|
if (reset || !filterListCache.containsKey(source.id)) {
|
||||||
@@ -56,7 +56,7 @@ object Search {
|
|||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
reset: Boolean,
|
reset: Boolean,
|
||||||
): List<FilterObject> {
|
): List<FilterObject> {
|
||||||
val source = getSourceOrStub(sourceId)
|
val source = getCatalogueSourceOrStub(sourceId)
|
||||||
|
|
||||||
return getFilterListOf(source, reset).list.map {
|
return getFilterListOf(source, reset).list.map {
|
||||||
FilterObject(
|
FilterObject(
|
||||||
@@ -111,7 +111,7 @@ object Search {
|
|||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
changes: List<FilterChange>,
|
changes: List<FilterChange>,
|
||||||
) {
|
) {
|
||||||
val source = getSourceOrStub(sourceId)
|
val source = getCatalogueSourceOrStub(sourceId)
|
||||||
val filterList = getFilterListOf(source, false)
|
val filterList = getFilterListOf(source, false)
|
||||||
updateFilterList(filterList, changes)
|
updateFilterList(filterList, changes)
|
||||||
}
|
}
|
||||||
@@ -169,7 +169,7 @@ object Search {
|
|||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
changes: List<FilterChange>,
|
changes: List<FilterChange>,
|
||||||
): FilterList {
|
): FilterList {
|
||||||
val source = getSourceOrStub(sourceId)
|
val source = getCatalogueSourceOrStub(sourceId)
|
||||||
val filterList = source.getFilterList()
|
val filterList = source.getFilterList()
|
||||||
return updateFilterList(filterList, changes)
|
return updateFilterList(filterList, changes)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,11 +25,10 @@ import org.jetbrains.exposed.v1.jdbc.selectAll
|
|||||||
import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
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 suwayomi.tachidesk.manga.impl.Source.preferenceScreenMap
|
import suwayomi.tachidesk.manga.impl.Source.preferenceScreenMap
|
||||||
import suwayomi.tachidesk.manga.impl.extension.Extension.proxyExtensionIconUrl
|
import suwayomi.tachidesk.manga.impl.extension.Extension.getExtensionIconUrl
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrNull
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.unregisterSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.unregisterCatalogueSource
|
||||||
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
|
||||||
@@ -43,17 +42,17 @@ object Source {
|
|||||||
fun getSourceList(): List<SourceDataClass> {
|
fun getSourceList(): List<SourceDataClass> {
|
||||||
return transaction {
|
return transaction {
|
||||||
SourceTable.selectAll().mapNotNull {
|
SourceTable.selectAll().mapNotNull {
|
||||||
val catalogueSource = getSourceOrNull(it[SourceTable.id].value) ?: return@mapNotNull null
|
val catalogueSource = getCatalogueSourceOrNull(it[SourceTable.id].value) ?: return@mapNotNull null
|
||||||
val sourceExtension = ExtensionTable.selectAll().where { ExtensionTable.id eq it[SourceTable.extension] }.first()
|
val sourceExtension = ExtensionTable.selectAll().where { ExtensionTable.id eq it[SourceTable.extension] }.first()
|
||||||
|
|
||||||
SourceDataClass(
|
SourceDataClass(
|
||||||
id = it[SourceTable.id].value.toString(),
|
id = it[SourceTable.id].value.toString(),
|
||||||
name = it[SourceTable.name],
|
name = it[SourceTable.name],
|
||||||
lang = it[SourceTable.lang],
|
lang = it[SourceTable.lang],
|
||||||
iconUrl = proxyExtensionIconUrl(sourceExtension[ExtensionTable.pkgName]),
|
iconUrl = getExtensionIconUrl(sourceExtension[ExtensionTable.apkName]),
|
||||||
supportsLatest = catalogueSource.supportsLatest,
|
supportsLatest = catalogueSource.supportsLatest,
|
||||||
isConfigurable = catalogueSource is ConfigurableSource,
|
isConfigurable = catalogueSource is ConfigurableSource,
|
||||||
isNsfw = it[SourceTable.contentWarning] >= ContentWarning.MIXED.ordinal,
|
isNsfw = it[SourceTable.isNsfw],
|
||||||
displayName = catalogueSource.toString(),
|
displayName = catalogueSource.toString(),
|
||||||
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
|
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
|
||||||
)
|
)
|
||||||
@@ -64,17 +63,20 @@ object Source {
|
|||||||
fun getSource(sourceId: Long): SourceDataClass? { // all the data extracted fresh form the source instance
|
fun getSource(sourceId: Long): SourceDataClass? { // all the data extracted fresh form the source instance
|
||||||
return transaction {
|
return transaction {
|
||||||
val source = SourceTable.selectAll().where { SourceTable.id eq sourceId }.firstOrNull() ?: return@transaction null
|
val source = SourceTable.selectAll().where { SourceTable.id eq sourceId }.firstOrNull() ?: return@transaction null
|
||||||
val catalogueSource = getSourceOrNull(sourceId) ?: return@transaction null
|
val catalogueSource = getCatalogueSourceOrNull(sourceId) ?: return@transaction null
|
||||||
val extension = ExtensionTable.selectAll().where { ExtensionTable.id eq source[SourceTable.extension] }.first()
|
val extension = ExtensionTable.selectAll().where { ExtensionTable.id eq source[SourceTable.extension] }.first()
|
||||||
|
|
||||||
SourceDataClass(
|
SourceDataClass(
|
||||||
id = sourceId.toString(),
|
id = sourceId.toString(),
|
||||||
name = source[SourceTable.name],
|
name = source[SourceTable.name],
|
||||||
lang = source[SourceTable.lang],
|
lang = source[SourceTable.lang],
|
||||||
iconUrl = proxyExtensionIconUrl(extension[ExtensionTable.pkgName]),
|
iconUrl =
|
||||||
|
getExtensionIconUrl(
|
||||||
|
extension[ExtensionTable.apkName],
|
||||||
|
),
|
||||||
supportsLatest = catalogueSource.supportsLatest,
|
supportsLatest = catalogueSource.supportsLatest,
|
||||||
isConfigurable = catalogueSource is ConfigurableSource,
|
isConfigurable = catalogueSource is ConfigurableSource,
|
||||||
isNsfw = source[SourceTable.contentWarning] >= ContentWarning.MIXED.ordinal,
|
isNsfw = source[SourceTable.isNsfw],
|
||||||
displayName = catalogueSource.toString(),
|
displayName = catalogueSource.toString(),
|
||||||
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
|
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
|
||||||
)
|
)
|
||||||
@@ -107,7 +109,7 @@ object Source {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getSourcePreferencesRaw(sourceId: Long): List<Preference> {
|
fun getSourcePreferencesRaw(sourceId: Long): List<Preference> {
|
||||||
val source = getSourceOrStub(sourceId)
|
val source = getCatalogueSourceOrStub(sourceId)
|
||||||
|
|
||||||
if (source is ConfigurableSource) {
|
if (source is ConfigurableSource) {
|
||||||
val sourceShardPreferences = source.sourcePreferences()
|
val sourceShardPreferences = source.sourcePreferences()
|
||||||
@@ -157,7 +159,7 @@ object Source {
|
|||||||
pref.callChangeListener(newValue)
|
pref.callChangeListener(newValue)
|
||||||
|
|
||||||
// must reload the source because a preference was changed
|
// must reload the source because a preference was changed
|
||||||
unregisterSource(sourceId)
|
unregisterCatalogueSource(sourceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSourcesMetaMaps(ids: List<Long>): Map<Long, Map<String, String>> =
|
fun getSourcesMetaMaps(ids: List<Long>): Map<Long, Map<String, String>> =
|
||||||
|
|||||||
@@ -76,8 +76,6 @@ object BackupMangaHandler {
|
|||||||
updateStrategy = UpdateStrategy.valueOf(mangaRow[MangaTable.updateStrategy]),
|
updateStrategy = UpdateStrategy.valueOf(mangaRow[MangaTable.updateStrategy]),
|
||||||
lastModifiedAt = mangaRow[MangaTable.lastModifiedAt],
|
lastModifiedAt = mangaRow[MangaTable.lastModifiedAt],
|
||||||
version = mangaRow[MangaTable.version],
|
version = mangaRow[MangaTable.version],
|
||||||
initialized = mangaRow[MangaTable.initialized],
|
|
||||||
memo = mangaRow[MangaTable.memo].encodeToByteArray(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val mangaId = mangaRow[MangaTable.id].value
|
val mangaId = mangaRow[MangaTable.id].value
|
||||||
@@ -115,7 +113,6 @@ object BackupMangaHandler {
|
|||||||
sourceOrder = chapters.size - it[ChapterTable.sourceOrder],
|
sourceOrder = chapters.size - it[ChapterTable.sourceOrder],
|
||||||
lastModifiedAt = it[ChapterTable.lastModifiedAt],
|
lastModifiedAt = it[ChapterTable.lastModifiedAt],
|
||||||
version = it[ChapterTable.version],
|
version = it[ChapterTable.version],
|
||||||
memo = it[ChapterTable.memo].encodeToByteArray(),
|
|
||||||
).apply {
|
).apply {
|
||||||
if (flags.includeClientData) {
|
if (flags.includeClientData) {
|
||||||
this.meta = chapterToMeta[it[ChapterTable.id].value] ?: emptyMap()
|
this.meta = chapterToMeta[it[ChapterTable.id].value] ?: emptyMap()
|
||||||
@@ -241,7 +238,6 @@ object BackupMangaHandler {
|
|||||||
|
|
||||||
it[lastModifiedAt] = manga.lastModifiedAt
|
it[lastModifiedAt] = manga.lastModifiedAt
|
||||||
it[version] = manga.version
|
it[version] = manga.version
|
||||||
it[memo] = manga.memo.decodeToString()
|
|
||||||
}.value
|
}.value
|
||||||
} else {
|
} else {
|
||||||
val dbMangaId = dbManga[MangaTable.id].value
|
val dbMangaId = dbManga[MangaTable.id].value
|
||||||
@@ -264,7 +260,6 @@ object BackupMangaHandler {
|
|||||||
|
|
||||||
it[lastModifiedAt] = manga.lastModifiedAt
|
it[lastModifiedAt] = manga.lastModifiedAt
|
||||||
it[version] = manga.version
|
it[version] = manga.version
|
||||||
it[memo] = manga.memo.decodeToString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dbMangaId
|
dbMangaId
|
||||||
@@ -356,7 +351,6 @@ object BackupMangaHandler {
|
|||||||
|
|
||||||
this[ChapterTable.lastModifiedAt] = chapter.lastModifiedAt
|
this[ChapterTable.lastModifiedAt] = chapter.lastModifiedAt
|
||||||
this[ChapterTable.version] = chapter.version
|
this[ChapterTable.version] = chapter.version
|
||||||
this[ChapterTable.memo] = chapter.memo.decodeToString()
|
|
||||||
}.map { it[ChapterTable.id].value }
|
}.map { it[ChapterTable.id].value }
|
||||||
} else {
|
} else {
|
||||||
emptyList()
|
emptyList()
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package suwayomi.tachidesk.manga.impl.backup.proto.models
|
|||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
import suwayomi.tachidesk.manga.impl.util.lang.JsonObjectEmptyBytes
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BackupChapter(
|
data class BackupChapter(
|
||||||
@@ -23,7 +22,6 @@ data class BackupChapter(
|
|||||||
// syncyomi
|
// syncyomi
|
||||||
@ProtoNumber(11) var lastModifiedAt: Long = 0,
|
@ProtoNumber(11) var lastModifiedAt: Long = 0,
|
||||||
@ProtoNumber(12) var version: Long = 0,
|
@ProtoNumber(12) var version: Long = 0,
|
||||||
@ProtoNumber(13) var memo: ByteArray = JsonObjectEmptyBytes,
|
|
||||||
// suwayomi
|
// suwayomi
|
||||||
@ProtoNumber(9000) var meta: Map<String, String> = emptyMap(),
|
@ProtoNumber(9000) var meta: Map<String, String> = emptyMap(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package suwayomi.tachidesk.manga.impl.backup.proto.models
|
|||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
import suwayomi.tachidesk.manga.impl.util.lang.JsonObjectEmptyBytes
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BackupManga(
|
data class BackupManga(
|
||||||
@@ -38,8 +37,6 @@ data class BackupManga(
|
|||||||
// syncyomi
|
// syncyomi
|
||||||
@ProtoNumber(106) var lastModifiedAt: Long = 0,
|
@ProtoNumber(106) var lastModifiedAt: Long = 0,
|
||||||
@ProtoNumber(109) var version: Long = 0,
|
@ProtoNumber(109) var version: Long = 0,
|
||||||
@ProtoNumber(111) var initialized: Boolean = false,
|
|
||||||
@ProtoNumber(112) var memo: ByteArray = JsonObjectEmptyBytes,
|
|
||||||
// suwayomi
|
// suwayomi
|
||||||
@ProtoNumber(9000) var meta: Map<String, String> = emptyMap(),
|
@ProtoNumber(9000) var meta: Map<String, String> = emptyMap(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import org.jetbrains.exposed.v1.jdbc.selectAll
|
|||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
import org.jetbrains.exposed.v1.jdbc.update
|
import org.jetbrains.exposed.v1.jdbc.update
|
||||||
import suwayomi.tachidesk.manga.impl.ChapterDownloadHelper
|
import suwayomi.tachidesk.manga.impl.ChapterDownloadHelper
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource.getSourceOrStub
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
|
||||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||||
@@ -77,7 +77,7 @@ suspend fun refreshChapterPageList(
|
|||||||
return mutex.withLock {
|
return mutex.withLock {
|
||||||
val chapterEntry = existingChapterEntry ?: transaction { ChapterTable.selectAll().where { ChapterTable.id eq chapterId }.first() }
|
val chapterEntry = existingChapterEntry ?: transaction { ChapterTable.selectAll().where { ChapterTable.id eq chapterId }.first() }
|
||||||
val mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
val mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||||
val source = getSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||||
|
|
||||||
val pageList =
|
val pageList =
|
||||||
source
|
source
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ package suwayomi.tachidesk.manga.impl.extension
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
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.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import net.dongliu.apk.parser.ApkFile
|
import net.dongliu.apk.parser.ApkFile
|
||||||
import net.dongliu.apk.parser.bean.Icon
|
import net.dongliu.apk.parser.bean.Icon
|
||||||
@@ -23,24 +23,22 @@ import okio.source
|
|||||||
import org.jetbrains.exposed.v1.core.eq
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
||||||
import org.jetbrains.exposed.v1.jdbc.insert
|
import org.jetbrains.exposed.v1.jdbc.insert
|
||||||
import org.jetbrains.exposed.v1.jdbc.select
|
|
||||||
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 org.jetbrains.exposed.v1.jdbc.update
|
import org.jetbrains.exposed.v1.jdbc.update
|
||||||
|
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.extensionTableAsDataClass
|
||||||
|
import suwayomi.tachidesk.manga.impl.extension.github.ExtensionGithubApi
|
||||||
import suwayomi.tachidesk.manga.impl.util.PackageTools
|
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
|
||||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.getPackageInfo
|
import suwayomi.tachidesk.manga.impl.util.PackageTools.getPackageInfo
|
||||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.loadExtensionSources
|
import suwayomi.tachidesk.manga.impl.util.PackageTools.loadExtensionSources
|
||||||
import suwayomi.tachidesk.manga.impl.util.network.await
|
import suwayomi.tachidesk.manga.impl.util.network.await
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
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
|
||||||
@@ -64,20 +62,18 @@ object Extension {
|
|||||||
|
|
||||||
suspend fun installExtension(pkgName: String): Int {
|
suspend fun installExtension(pkgName: String): Int {
|
||||||
logger.debug { "Installing $pkgName" }
|
logger.debug { "Installing $pkgName" }
|
||||||
val apkUrl =
|
val extensionRecord = extensionTableAsDataClass().first { it.pkgName == pkgName }
|
||||||
transaction {
|
|
||||||
ExtensionTable
|
|
||||||
.select(ExtensionTable.apkUrl)
|
|
||||||
.where { ExtensionTable.pkgName eq pkgName }
|
|
||||||
.firstOrNull()
|
|
||||||
?.get(ExtensionTable.apkUrl)
|
|
||||||
} ?: throw NullPointerException("Could not find extension $pkgName")
|
|
||||||
|
|
||||||
return installAPK {
|
return installAPK {
|
||||||
val apkName = Uri.parse(apkUrl).lastPathSegment!!
|
val apkURL =
|
||||||
|
ExtensionGithubApi.getApkUrl(
|
||||||
|
extensionRecord.repo ?: throw NullPointerException("Could not find extension repo"),
|
||||||
|
extensionRecord.apkName,
|
||||||
|
)
|
||||||
|
val apkName = Uri.parse(apkURL).lastPathSegment!!
|
||||||
val apkSavePath = "${applicationDirs.extensionsRoot}/$apkName"
|
val apkSavePath = "${applicationDirs.extensionsRoot}/$apkName"
|
||||||
// download apk file
|
// download apk file
|
||||||
downloadAPKFile(apkUrl, apkSavePath)
|
downloadAPKFile(apkURL, apkSavePath)
|
||||||
|
|
||||||
apkSavePath
|
apkSavePath
|
||||||
}
|
}
|
||||||
@@ -152,19 +148,7 @@ 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")
|
||||||
// }
|
// }
|
||||||
|
|
||||||
var contentWarning = packageInfo.applicationInfo.metaData.getInt(METADATA_CONTENT_WARNING)
|
val isNsfw = packageInfo.applicationInfo.metaData.getString(METADATA_NSFW) == "1"
|
||||||
if (contentWarning == 0) {
|
|
||||||
contentWarning = packageInfo.applicationInfo.metaData
|
|
||||||
.getString(METADATA_CONTENT_WARNING)
|
|
||||||
?.toIntOrNull()
|
|
||||||
?: 0
|
|
||||||
if (contentWarning == 0) {
|
|
||||||
contentWarning = packageInfo.applicationInfo.metaData
|
|
||||||
.getString(METADATA_NSFW)
|
|
||||||
?.toIntOrNull()
|
|
||||||
?: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val className =
|
val className =
|
||||||
packageInfo.packageName + packageInfo.applicationInfo.metaData.getString(METADATA_SOURCE_CLASS)
|
packageInfo.packageName + packageInfo.applicationInfo.metaData.getString(METADATA_SOURCE_CLASS)
|
||||||
@@ -173,7 +157,7 @@ object Extension {
|
|||||||
|
|
||||||
dex2jar(apkFilePath, jarFilePath, fileNameWithoutType)
|
dex2jar(apkFilePath, jarFilePath, fileNameWithoutType)
|
||||||
extractAssetsFromApk(apkFilePath, jarFilePath)
|
extractAssetsFromApk(apkFilePath, jarFilePath)
|
||||||
extractAndCacheApkIcon(apkFilePath, packageInfo.packageName)
|
extractAndCacheApkIcon(apkFilePath, apkName)
|
||||||
|
|
||||||
// clean up
|
// clean up
|
||||||
File(apkFilePath).delete()
|
File(apkFilePath).delete()
|
||||||
@@ -181,12 +165,12 @@ object Extension {
|
|||||||
try {
|
try {
|
||||||
// collect sources from the extension
|
// collect sources from the extension
|
||||||
val extensionMainClassInstance = loadExtensionSources(jarFilePath, className)
|
val extensionMainClassInstance = loadExtensionSources(jarFilePath, className)
|
||||||
val sources: List<Source> =
|
val sources: List<CatalogueSource> =
|
||||||
when (extensionMainClassInstance) {
|
when (extensionMainClassInstance) {
|
||||||
is Source -> listOf(extensionMainClassInstance)
|
is Source -> listOf(extensionMainClassInstance)
|
||||||
is SourceFactory -> extensionMainClassInstance.createSources()
|
is SourceFactory -> extensionMainClassInstance.createSources()
|
||||||
else -> throw RuntimeException("Unknown source class type! ${extensionMainClassInstance.javaClass}")
|
else -> throw RuntimeException("Unknown source class type! ${extensionMainClassInstance.javaClass}")
|
||||||
}
|
}.map { it as CatalogueSource }
|
||||||
|
|
||||||
val langs = sources.map { it.lang }.toSet()
|
val langs = sources.map { it.lang }.toSet()
|
||||||
val extensionLang =
|
val extensionLang =
|
||||||
@@ -197,16 +181,9 @@ object Extension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val extensionName =
|
val extensionName =
|
||||||
packageInfo.applicationInfo.metaData.getString(METADATA_NAME)
|
packageInfo.applicationInfo.nonLocalizedLabel
|
||||||
?: packageInfo.applicationInfo.nonLocalizedLabel
|
.toString()
|
||||||
.toString()
|
.substringAfter("Tachiyomi: ")
|
||||||
.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 {
|
||||||
@@ -216,10 +193,9 @@ object Extension {
|
|||||||
it[name] = extensionName
|
it[name] = extensionName
|
||||||
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
|
||||||
it[extensionLib] = extensionLibVersion
|
|
||||||
it[lang] = extensionLang
|
it[lang] = extensionLang
|
||||||
it[this.contentWarning] = contentWarning
|
it[this.isNsfw] = isNsfw
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +204,7 @@ object Extension {
|
|||||||
it[this.isInstalled] = true
|
it[this.isInstalled] = true
|
||||||
it[this.classFQName] = className
|
it[this.classFQName] = className
|
||||||
it[versionName] = packageInfo.versionName
|
it[versionName] = packageInfo.versionName
|
||||||
it[versionCode] = packageInfo.versionCode.toLong()
|
it[versionCode] = packageInfo.versionCode
|
||||||
}
|
}
|
||||||
|
|
||||||
val extensionId =
|
val extensionId =
|
||||||
@@ -244,7 +220,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[this.contentWarning] = contentWarning
|
it[SourceTable.isNsfw] = isNsfw
|
||||||
}
|
}
|
||||||
logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" }
|
logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" }
|
||||||
}
|
}
|
||||||
@@ -265,7 +241,7 @@ object Extension {
|
|||||||
|
|
||||||
private fun extractAndCacheApkIcon(
|
private fun extractAndCacheApkIcon(
|
||||||
apkFilePath: String,
|
apkFilePath: String,
|
||||||
pkgName: String,
|
apkName: String,
|
||||||
) {
|
) {
|
||||||
val iconCacheDir = "${applicationDirs.extensionsRoot}/icon"
|
val iconCacheDir = "${applicationDirs.extensionsRoot}/icon"
|
||||||
try {
|
try {
|
||||||
@@ -278,15 +254,15 @@ object Extension {
|
|||||||
?.first
|
?.first
|
||||||
}
|
}
|
||||||
if (iconData == null) {
|
if (iconData == null) {
|
||||||
logger.warn { "No icon found in APK $pkgName" }
|
logger.warn { "No icon found in APK $apkName" }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
File(iconCacheDir).mkdirs()
|
File(iconCacheDir).mkdirs()
|
||||||
clearCachedImage(iconCacheDir, pkgName)
|
clearCachedImage(iconCacheDir, apkName)
|
||||||
saveImage("$iconCacheDir/$pkgName", iconData.inputStream(), null)
|
saveImage("$iconCacheDir/$apkName", iconData.inputStream(), null)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.warn(e) { "Failed to extract icon from APK $pkgName" }
|
logger.warn(e) { "Failed to extract icon from APK $apkName" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,9 +343,7 @@ object Extension {
|
|||||||
logger.debug { "Uninstalling $pkgName" }
|
logger.debug { "Uninstalling $pkgName" }
|
||||||
|
|
||||||
val extensionRecord = transaction { ExtensionTable.selectAll().where { ExtensionTable.pkgName eq pkgName }.first() }
|
val extensionRecord = transaction { ExtensionTable.selectAll().where { ExtensionTable.pkgName eq pkgName }.first() }
|
||||||
val fileNameWithoutType =
|
val fileNameWithoutType = extensionRecord[ExtensionTable.apkName].substringBefore(".apk")
|
||||||
extensionRecord[ExtensionTable.apkName]?.substringBefore(".apk")
|
|
||||||
?: throw NullPointerException("Missing $pkgName apkName")
|
|
||||||
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
||||||
val sources =
|
val sources =
|
||||||
transaction {
|
transaction {
|
||||||
@@ -379,13 +353,12 @@ object Extension {
|
|||||||
|
|
||||||
SourceTable.deleteWhere { SourceTable.extension eq extensionId }
|
SourceTable.deleteWhere { SourceTable.extension eq extensionId }
|
||||||
|
|
||||||
if (extensionRecord[ExtensionTable.isObsolete] || extensionRecord[ExtensionTable.apkUrl] == null) {
|
if (extensionRecord[ExtensionTable.isObsolete]) {
|
||||||
ExtensionTable.deleteWhere { ExtensionTable.pkgName eq pkgName }
|
ExtensionTable.deleteWhere { ExtensionTable.pkgName eq pkgName }
|
||||||
} else {
|
} else {
|
||||||
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
|
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
|
||||||
it[isInstalled] = false
|
it[isInstalled] = false
|
||||||
it[hasUpdate] = false
|
it[hasUpdate] = false
|
||||||
it[apkName] = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,7 +370,7 @@ object Extension {
|
|||||||
PackageTools.jarLoaderMap.remove(jarPath)?.close()
|
PackageTools.jarLoaderMap.remove(jarPath)?.close()
|
||||||
|
|
||||||
// clear all loaded sources
|
// clear all loaded sources
|
||||||
sources.forEach { GetSource.unregisterSource(it) }
|
sources.forEach { GetCatalogueSource.unregisterCatalogueSource(it) }
|
||||||
|
|
||||||
File(jarPath).delete()
|
File(jarPath).delete()
|
||||||
}
|
}
|
||||||
@@ -412,7 +385,8 @@ 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[contentWarning] = targetExtension.contentWarning.ordinal
|
it[isNsfw] = targetExtension.isNsfw
|
||||||
|
it[apkName] = targetExtension.apkName
|
||||||
it[iconUrl] = targetExtension.iconUrl
|
it[iconUrl] = targetExtension.iconUrl
|
||||||
it[hasUpdate] = false
|
it[hasUpdate] = false
|
||||||
}
|
}
|
||||||
@@ -420,21 +394,17 @@ object Extension {
|
|||||||
return installExtension(pkgName)
|
return installExtension(pkgName)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getExtensionIcon(pkgName: String): Pair<InputStream, String> {
|
suspend fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
|
||||||
|
val iconUrl =
|
||||||
|
if (apkName == "localSource") {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
transaction { ExtensionTable.selectAll().where { ExtensionTable.apkName eq apkName }.first() }[ExtensionTable.iconUrl]
|
||||||
|
}
|
||||||
|
|
||||||
val cacheSaveDir = "${applicationDirs.extensionsRoot}/icon"
|
val cacheSaveDir = "${applicationDirs.extensionsRoot}/icon"
|
||||||
|
|
||||||
if (pkgName == LocalSource::class.java.`package`.name) {
|
return getImageResponse(cacheSaveDir, apkName) {
|
||||||
return getImageResponse(cacheSaveDir, "localSource") {
|
|
||||||
network.client
|
|
||||||
.newCall(GET("", cache = CacheControl.FORCE_NETWORK))
|
|
||||||
.await()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val iconUrl =
|
|
||||||
transaction { ExtensionTable.selectAll().where { ExtensionTable.pkgName eq pkgName }.first() }[ExtensionTable.iconUrl]
|
|
||||||
|
|
||||||
return getImageResponse(cacheSaveDir, pkgName) {
|
|
||||||
network.client
|
network.client
|
||||||
.newCall(
|
.newCall(
|
||||||
GET(iconUrl, cache = CacheControl.FORCE_NETWORK),
|
GET(iconUrl, cache = CacheControl.FORCE_NETWORK),
|
||||||
@@ -442,5 +412,5 @@ object Extension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun proxyExtensionIconUrl(pkgName: String): String = "/api/v1/extension/icon/$pkgName"
|
fun getExtensionIconUrl(apkName: String): String = "/api/v1/extension/icon/$apkName"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,269 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.extension
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
|
||||||
import kotlinx.serialization.decodeFromByteArray
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.okio.decodeFromBufferedSource
|
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
|
||||||
import okio.BufferedSource
|
|
||||||
import okio.buffer
|
|
||||||
import okio.gzip
|
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
|
||||||
import org.jetbrains.exposed.v1.core.inList
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.insert
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.update
|
|
||||||
import suwayomi.tachidesk.manga.impl.extension.github.NetworkExtensionStore
|
|
||||||
import suwayomi.tachidesk.manga.impl.extension.github.NetworkLegacyExtension
|
|
||||||
import suwayomi.tachidesk.manga.impl.extension.github.NetworkLegacyExtensionRepo
|
|
||||||
import suwayomi.tachidesk.manga.impl.extension.github.toExtensionInfo
|
|
||||||
import suwayomi.tachidesk.manga.impl.extension.github.toExtensionInfos
|
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo
|
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionStore
|
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionStoreTable
|
|
||||||
import suwayomi.tachidesk.server.serverConfig
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
|
||||||
|
|
||||||
object ExtensionStoreService {
|
|
||||||
private val logger = KotlinLogging.logger {}
|
|
||||||
|
|
||||||
val network: NetworkHelper by injectLazy()
|
|
||||||
val protoBuf: ProtoBuf by injectLazy()
|
|
||||||
val json: Json by injectLazy()
|
|
||||||
|
|
||||||
suspend fun fetch(indexUrl: String): ExtensionStore {
|
|
||||||
var updatedIndexUrl: String = indexUrl
|
|
||||||
return try {
|
|
||||||
val response = network.client.newCall(GET(updatedIndexUrl)).awaitSuccess()
|
|
||||||
response.body.source().decompressIfGzipped().use { source ->
|
|
||||||
val networkStore =
|
|
||||||
when (source.peek().readByte()) {
|
|
||||||
// "[..."
|
|
||||||
0x5B.toByte() -> {
|
|
||||||
run {
|
|
||||||
if (!indexUrl.endsWith("/index.min.json")) {
|
|
||||||
throw IllegalArgumentException("Provided legacy store url is not valid")
|
|
||||||
}
|
|
||||||
updatedIndexUrl = indexUrl.replace("/index.min.json", "/repo.json")
|
|
||||||
network.client.newCall(GET(updatedIndexUrl)).awaitSuccess().body.source().use {
|
|
||||||
json.decodeFromBufferedSource<NetworkLegacyExtensionRepo>(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// "{..."
|
|
||||||
0x7B.toByte() -> {
|
|
||||||
try {
|
|
||||||
json.decodeFromBufferedSource<NetworkLegacyExtensionRepo>(source.peek())
|
|
||||||
} catch (_: IllegalArgumentException) {
|
|
||||||
json.decodeFromBufferedSource<NetworkExtensionStore>(source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.readByteArray())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (networkStore is NetworkLegacyExtensionRepo && networkStore.indexV2 != null) {
|
|
||||||
return fetch(networkStore.indexV2)
|
|
||||||
}
|
|
||||||
|
|
||||||
networkStore.toExtensionStore(updatedIndexUrl)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (e is CancellationException) throw e
|
|
||||||
logger.error(e) { "Failed to fetch extension store '$indexUrl'" }
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun upsert(store: ExtensionStore) {
|
|
||||||
transaction {
|
|
||||||
val existing =
|
|
||||||
ExtensionStoreTable
|
|
||||||
.selectAll()
|
|
||||||
.where { ExtensionStoreTable.indexUrl eq store.indexUrl }
|
|
||||||
.firstOrNull()
|
|
||||||
|
|
||||||
if (existing == null) {
|
|
||||||
ExtensionStoreTable.insert {
|
|
||||||
it[name] = store.name
|
|
||||||
it[badgeLabel] = store.badgeLabel
|
|
||||||
it[signingKey] = store.signingKey
|
|
||||||
it[contactWebsite] = store.contact.website
|
|
||||||
it[contactDiscord] = store.contact.discord
|
|
||||||
it[indexUrl] = store.indexUrl
|
|
||||||
it[isLegacy] = store.isLegacy
|
|
||||||
it[extensionListUrl] = store.extensionListUrl
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ExtensionStoreTable.update({ ExtensionStoreTable.indexUrl eq store.indexUrl }) {
|
|
||||||
it[name] = store.name
|
|
||||||
it[badgeLabel] = store.badgeLabel
|
|
||||||
it[signingKey] = store.signingKey
|
|
||||||
it[contactWebsite] = store.contact.website
|
|
||||||
it[contactDiscord] = store.contact.discord
|
|
||||||
it[isLegacy] = store.isLegacy
|
|
||||||
it[extensionListUrl] = store.extensionListUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getAndRefresh(): List<ExtensionStore> {
|
|
||||||
val stores =
|
|
||||||
transaction {
|
|
||||||
ExtensionStoreTable.selectAll().toList()
|
|
||||||
}
|
|
||||||
var needsPrefUpdate = false
|
|
||||||
val updateStores =
|
|
||||||
stores.mapNotNull { storeRow ->
|
|
||||||
val oldIndexUrl = storeRow[ExtensionStoreTable.indexUrl]
|
|
||||||
val oldName = storeRow[ExtensionStoreTable.name]
|
|
||||||
try {
|
|
||||||
val store = fetch(oldIndexUrl)
|
|
||||||
if (store.indexUrl != oldIndexUrl) {
|
|
||||||
transaction {
|
|
||||||
ExtensionStoreTable.deleteWhere { ExtensionStoreTable.indexUrl eq oldIndexUrl }
|
|
||||||
}
|
|
||||||
needsPrefUpdate = true
|
|
||||||
}
|
|
||||||
upsert(store)
|
|
||||||
store
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logger.warn(e) { "Failed to fetch extension store '$oldName ($oldIndexUrl)'" }
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (needsPrefUpdate) syncDbToPrefs()
|
|
||||||
return updateStores
|
|
||||||
}
|
|
||||||
|
|
||||||
fun syncDbToPrefs() {
|
|
||||||
val dbStores =
|
|
||||||
transaction {
|
|
||||||
ExtensionStoreTable
|
|
||||||
.selectAll()
|
|
||||||
.map { it[ExtensionStoreTable.indexUrl] }
|
|
||||||
.toSet()
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentPrefs = serverConfig.extensionStores.value.toSet()
|
|
||||||
val toAdd = dbStores - currentPrefs
|
|
||||||
val toRemove = currentPrefs - dbStores
|
|
||||||
|
|
||||||
if (toAdd.isNotEmpty()) {
|
|
||||||
serverConfig.extensionStores.value = (serverConfig.extensionStores.value + toAdd).distinct()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toRemove.isNotEmpty()) {
|
|
||||||
serverConfig.extensionStores.value = serverConfig.extensionStores.value.filterNot { it in toRemove }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun syncPrefsToDb() {
|
|
||||||
val prefUrls = serverConfig.extensionStores.value.toSet()
|
|
||||||
|
|
||||||
val dbStores =
|
|
||||||
transaction {
|
|
||||||
ExtensionStoreTable.selectAll().associateBy { it[ExtensionStoreTable.indexUrl] }
|
|
||||||
}
|
|
||||||
|
|
||||||
val toAdd = prefUrls - dbStores.keys
|
|
||||||
val toRemove = (dbStores.keys - prefUrls).toMutableSet()
|
|
||||||
var needsPrefUpdate = toRemove.isNotEmpty()
|
|
||||||
|
|
||||||
toAdd.forEach { url ->
|
|
||||||
try {
|
|
||||||
val store = fetch(url)
|
|
||||||
if (store.indexUrl != url) {
|
|
||||||
transaction {
|
|
||||||
ExtensionStoreTable.deleteWhere { ExtensionStoreTable.indexUrl eq url }
|
|
||||||
}
|
|
||||||
needsPrefUpdate = true
|
|
||||||
toRemove -= store.indexUrl
|
|
||||||
}
|
|
||||||
upsert(store)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logger.warn(e) { "Failed to sync preference store '$url' to database" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toRemove.isNotEmpty()) {
|
|
||||||
transaction {
|
|
||||||
ExtensionStoreTable.deleteWhere { ExtensionStoreTable.indexUrl inList toRemove.toList() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (needsPrefUpdate) {
|
|
||||||
syncDbToPrefs()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getExtensions(store: ExtensionStore): List<ExtensionInfo> {
|
|
||||||
val extensions =
|
|
||||||
if (store.extensionListUrl != null) {
|
|
||||||
val response = network.client.newCall(GET(store.extensionListUrl)).awaitSuccess()
|
|
||||||
response.body.source().decompressIfGzipped().use { source ->
|
|
||||||
when (source.peek().readByte()) {
|
|
||||||
// "{..."
|
|
||||||
0x7B.toByte() -> {
|
|
||||||
json.decodeFromBufferedSource<NetworkExtensionStore.ExtensionList>(source)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
protoBuf.decodeFromByteArray<NetworkExtensionStore.ExtensionList>(
|
|
||||||
source.readByteArray(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.toExtensionInfos(store)
|
|
||||||
}
|
|
||||||
} else if (!store.isLegacy) {
|
|
||||||
val response = network.client.newCall(GET(store.indexUrl)).awaitSuccess()
|
|
||||||
response.body.source().decompressIfGzipped().use { source ->
|
|
||||||
when (source.peek().readByte()) {
|
|
||||||
// "{..."
|
|
||||||
0x7B.toByte() -> json.decodeFromBufferedSource<NetworkExtensionStore>(source)
|
|
||||||
|
|
||||||
else -> protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.readByteArray())
|
|
||||||
}.extensionList!!
|
|
||||||
.toExtensionInfos(store)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val storeBaseUrl = store.indexUrl.removeSuffix("/repo.json")
|
|
||||||
val response = network.client.newCall(GET("$storeBaseUrl/index.min.json")).awaitSuccess()
|
|
||||||
response.body.source().use { source ->
|
|
||||||
json
|
|
||||||
.decodeFromBufferedSource<List<NetworkLegacyExtension>>(source)
|
|
||||||
.map { it.toExtensionInfo(store, storeBaseUrl) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,11 +21,12 @@ import org.jetbrains.exposed.v1.jdbc.selectAll
|
|||||||
import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
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.getExtensionIconUrl
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
import suwayomi.tachidesk.manga.impl.extension.github.ExtensionGithubApi
|
||||||
|
import suwayomi.tachidesk.manga.impl.extension.github.OnlineExtension
|
||||||
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.table.ExtensionTable
|
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||||
|
import suwayomi.tachidesk.server.serverConfig
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@@ -33,23 +34,23 @@ object ExtensionsList {
|
|||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
var lastUpdateCheck: Long = 0
|
var lastUpdateCheck: Long = 0
|
||||||
var updateMap = ConcurrentHashMap<String, ExtensionInfo>()
|
var updateMap = ConcurrentHashMap<String, OnlineExtension>()
|
||||||
|
|
||||||
suspend fun fetchExtensions() {
|
suspend fun fetchExtensions() {
|
||||||
val allExtensions = mutableListOf<ExtensionInfo>()
|
// update if 60 seconds has passed or requested offline and database is empty
|
||||||
|
val extensions =
|
||||||
ExtensionStoreService.getAndRefresh().forEach { store ->
|
serverConfig.extensionRepos.value.map { repo ->
|
||||||
try {
|
kotlin
|
||||||
val extensions = ExtensionStoreService.getExtensions(store)
|
.runCatching {
|
||||||
allExtensions.addAll(extensions)
|
ExtensionGithubApi.findExtensions(repo.repoUrlReplace())
|
||||||
} catch (e: Exception) {
|
}.onFailure {
|
||||||
logger.warn(e) {
|
logger.warn(it) {
|
||||||
"Failed to fetch extensions for store: ${store.indexUrl}"
|
"Failed to fetch extensions for repo: $repo"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
val foundExtensions = extensions.mapNotNull { it.getOrNull() }.flatten()
|
||||||
|
updateExtensionDatabase(foundExtensions)
|
||||||
updateExtensionDatabase(allExtensions)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun fetchExtensionsCached() {
|
suspend fun fetchExtensionsCached() {
|
||||||
@@ -73,25 +74,25 @@ object ExtensionsList {
|
|||||||
transaction {
|
transaction {
|
||||||
ExtensionTable.selectAll().filter { it[ExtensionTable.name] != LocalSource.EXTENSION_NAME }.map {
|
ExtensionTable.selectAll().filter { it[ExtensionTable.name] != LocalSource.EXTENSION_NAME }.map {
|
||||||
ExtensionDataClass(
|
ExtensionDataClass(
|
||||||
repo = it[ExtensionTable.storeIndexUrl],
|
it[ExtensionTable.repo],
|
||||||
apkName = it[ExtensionTable.apkName].orEmpty(),
|
it[ExtensionTable.apkName],
|
||||||
iconUrl = proxyExtensionIconUrl(it[ExtensionTable.pkgName]),
|
getExtensionIconUrl(it[ExtensionTable.apkName]),
|
||||||
name = it[ExtensionTable.name],
|
it[ExtensionTable.name],
|
||||||
pkgName = it[ExtensionTable.pkgName],
|
it[ExtensionTable.pkgName],
|
||||||
versionName = it[ExtensionTable.versionName],
|
it[ExtensionTable.versionName],
|
||||||
versionCode = it[ExtensionTable.versionCode].toInt(),
|
it[ExtensionTable.versionCode],
|
||||||
lang = it[ExtensionTable.lang],
|
it[ExtensionTable.lang],
|
||||||
isNsfw = it[ExtensionTable.contentWarning] >= ContentWarning.MIXED.ordinal,
|
it[ExtensionTable.isNsfw],
|
||||||
installed = it[ExtensionTable.isInstalled],
|
it[ExtensionTable.isInstalled],
|
||||||
hasUpdate = it[ExtensionTable.hasUpdate],
|
it[ExtensionTable.hasUpdate],
|
||||||
obsolete = it[ExtensionTable.isObsolete],
|
it[ExtensionTable.isObsolete],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val updateExtensionDatabaseMutex = Mutex()
|
private val updateExtensionDatabaseMutex = Mutex()
|
||||||
|
|
||||||
private suspend fun updateExtensionDatabase(foundExtensions: List<ExtensionInfo>) {
|
private suspend fun updateExtensionDatabase(foundExtensions: List<OnlineExtension>) {
|
||||||
updateExtensionDatabaseMutex.withLock {
|
updateExtensionDatabaseMutex.withLock {
|
||||||
transaction {
|
transaction {
|
||||||
val uniqueExtensions =
|
val uniqueExtensions =
|
||||||
@@ -105,10 +106,10 @@ object ExtensionsList {
|
|||||||
.selectAll()
|
.selectAll()
|
||||||
.toList()
|
.toList()
|
||||||
.associateBy { it[ExtensionTable.pkgName] }
|
.associateBy { it[ExtensionTable.pkgName] }
|
||||||
val extensionsToUpdate = mutableListOf<Pair<ExtensionInfo, ResultRow>>()
|
val extensionsToUpdate = mutableListOf<Pair<OnlineExtension, ResultRow>>()
|
||||||
val extensionsToInsert = mutableListOf<ExtensionInfo>()
|
val extensionsToInsert = mutableListOf<OnlineExtension>()
|
||||||
val extensionsToDelete =
|
val extensionsToDelete =
|
||||||
installedExtensions.filter { it.value[ExtensionTable.storeIndexUrl] != null }.mapNotNull { (pkgName, extension) ->
|
installedExtensions.filter { it.value[ExtensionTable.repo] != null }.mapNotNull { (pkgName, extension) ->
|
||||||
extension.takeUnless { uniqueExtensions.any { it.pkgName == pkgName } }
|
extension.takeUnless { uniqueExtensions.any { it.pkgName == pkgName } }
|
||||||
}
|
}
|
||||||
uniqueExtensions.forEach {
|
uniqueExtensions.forEach {
|
||||||
@@ -131,7 +132,7 @@ object ExtensionsList {
|
|||||||
addBatch(EntityID(extensionRecord[ExtensionTable.id].value, ExtensionTable))
|
addBatch(EntityID(extensionRecord[ExtensionTable.id].value, ExtensionTable))
|
||||||
// Always update icon url and repo
|
// Always update icon url and repo
|
||||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||||
this[ExtensionTable.storeIndexUrl] = foundExtension.storeIndexUrl
|
this[ExtensionTable.repo] = foundExtension.repo
|
||||||
|
|
||||||
// add these because batch updates need matching columns
|
// add these because batch updates need matching columns
|
||||||
this[ExtensionTable.hasUpdate] = extensionRecord[ExtensionTable.hasUpdate]
|
this[ExtensionTable.hasUpdate] = extensionRecord[ExtensionTable.hasUpdate]
|
||||||
@@ -167,14 +168,13 @@ object ExtensionsList {
|
|||||||
extensionsToFullyUpdate.forEach { (foundExtension, extensionRecord) ->
|
extensionsToFullyUpdate.forEach { (foundExtension, extensionRecord) ->
|
||||||
addBatch(EntityID(extensionRecord[ExtensionTable.id].value, ExtensionTable))
|
addBatch(EntityID(extensionRecord[ExtensionTable.id].value, ExtensionTable))
|
||||||
// extension is not installed, so we can overwrite the data without a care
|
// extension is not installed, so we can overwrite the data without a care
|
||||||
this[ExtensionTable.storeIndexUrl] = foundExtension.storeIndexUrl
|
this[ExtensionTable.repo] = foundExtension.repo
|
||||||
this[ExtensionTable.name] = foundExtension.name
|
this[ExtensionTable.name] = foundExtension.name
|
||||||
this[ExtensionTable.extensionLib] = foundExtension.extensionLib
|
|
||||||
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.contentWarning] = foundExtension.contentWarning.ordinal
|
this[ExtensionTable.isNsfw] = foundExtension.isNsfw
|
||||||
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
this[ExtensionTable.apkName] = foundExtension.apkName
|
||||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||||
}
|
}
|
||||||
}.toExecutable()
|
}.toExecutable()
|
||||||
@@ -183,15 +183,14 @@ object ExtensionsList {
|
|||||||
}
|
}
|
||||||
if (extensionsToInsert.isNotEmpty()) {
|
if (extensionsToInsert.isNotEmpty()) {
|
||||||
ExtensionTable.batchInsert(extensionsToInsert) { foundExtension ->
|
ExtensionTable.batchInsert(extensionsToInsert) { foundExtension ->
|
||||||
this[ExtensionTable.storeIndexUrl] = foundExtension.storeIndexUrl
|
this[ExtensionTable.repo] = foundExtension.repo
|
||||||
this[ExtensionTable.name] = foundExtension.name
|
this[ExtensionTable.name] = foundExtension.name
|
||||||
this[ExtensionTable.pkgName] = foundExtension.pkgName
|
this[ExtensionTable.pkgName] = foundExtension.pkgName
|
||||||
this[ExtensionTable.extensionLib] = foundExtension.extensionLib
|
|
||||||
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.contentWarning] = foundExtension.contentWarning.ordinal
|
this[ExtensionTable.isNsfw] = foundExtension.isNsfw
|
||||||
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
this[ExtensionTable.apkName] = foundExtension.apkName
|
||||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,4 +215,16 @@ object ExtensionsList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun String.repoUrlReplace(): String =
|
||||||
|
if (contains("github")) {
|
||||||
|
replace(repoMatchRegex) {
|
||||||
|
"https://raw.githubusercontent.com/${it.groupValues[2]}/${it.groupValues[3]}/" +
|
||||||
|
(it.groupValues.getOrNull(4)?.ifBlank { null } ?: "repo") +
|
||||||
|
"/" +
|
||||||
|
(it.groupValues.getOrNull(5)?.ifBlank { null } ?: "index.min.json")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.extension.github
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionStore
|
|
||||||
|
|
||||||
interface BaseNetworkExtensionStore {
|
|
||||||
fun toExtensionStore(indexUrl: String): ExtensionStore
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.extension.github
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MAX
|
||||||
|
import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MIN
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
object ExtensionGithubApi {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class ExtensionJsonObject(
|
||||||
|
val name: String,
|
||||||
|
val pkg: String,
|
||||||
|
val apk: String,
|
||||||
|
val lang: String,
|
||||||
|
val code: Int,
|
||||||
|
val version: String,
|
||||||
|
val nsfw: Int,
|
||||||
|
val hasReadme: Int = 0,
|
||||||
|
val hasChangelog: Int = 0,
|
||||||
|
val sources: List<ExtensionSourceJsonObject>?,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class ExtensionSourceJsonObject(
|
||||||
|
val name: String,
|
||||||
|
val lang: String,
|
||||||
|
val id: Long,
|
||||||
|
val baseUrl: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun findExtensions(repo: String): List<OnlineExtension> {
|
||||||
|
val response =
|
||||||
|
client.newCall(GET(repo)).awaitSuccess()
|
||||||
|
|
||||||
|
return with(json) {
|
||||||
|
response
|
||||||
|
.parseAs<List<ExtensionJsonObject>>()
|
||||||
|
.toExtensions(repo.substringBeforeLast('/') + '/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getApkUrl(
|
||||||
|
repo: String,
|
||||||
|
apkName: String,
|
||||||
|
): String = "${repo}apk/$apkName"
|
||||||
|
|
||||||
|
private val client by lazy {
|
||||||
|
val network: NetworkHelper by injectLazy()
|
||||||
|
network.client
|
||||||
|
.newBuilder()
|
||||||
|
.addNetworkInterceptor { chain ->
|
||||||
|
val originalResponse = chain.proceed(chain.request())
|
||||||
|
originalResponse
|
||||||
|
.newBuilder()
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.build()
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<ExtensionJsonObject>.toExtensions(repo: String): List<OnlineExtension> =
|
||||||
|
this
|
||||||
|
.filter {
|
||||||
|
val libVersion = it.version.substringBeforeLast('.').toDouble()
|
||||||
|
libVersion in LIB_VERSION_MIN..LIB_VERSION_MAX
|
||||||
|
}.map {
|
||||||
|
OnlineExtension(
|
||||||
|
repo = repo,
|
||||||
|
name = it.name.substringAfter("Tachiyomi: "),
|
||||||
|
pkgName = it.pkg,
|
||||||
|
versionName = it.version,
|
||||||
|
versionCode = it.code,
|
||||||
|
lang = it.lang,
|
||||||
|
isNsfw = it.nsfw == 1,
|
||||||
|
hasReadme = it.hasReadme == 1,
|
||||||
|
hasChangelog = it.hasChangelog == 1,
|
||||||
|
sources = it.sources?.toExtensionSources() ?: emptyList(),
|
||||||
|
apkName = it.apk,
|
||||||
|
iconUrl = "${repo}icon/${it.pkg}.png",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<ExtensionSourceJsonObject>.toExtensionSources(): List<OnlineExtensionSource> =
|
||||||
|
this.map {
|
||||||
|
OnlineExtensionSource(
|
||||||
|
name = it.name,
|
||||||
|
lang = it.lang,
|
||||||
|
id = it.id,
|
||||||
|
baseUrl = it.baseUrl,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.extension.github
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.json.JsonNames
|
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo
|
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionSource
|
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionStore
|
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
|
||||||
@Serializable
|
|
||||||
data class NetworkExtensionStore(
|
|
||||||
@ProtoNumber(1) val name: String,
|
|
||||||
@ProtoNumber(2) val badgeLabel: String,
|
|
||||||
@ProtoNumber(3) val signingKey: String,
|
|
||||||
@ProtoNumber(4) val contact: Contact,
|
|
||||||
@ProtoNumber(101) val extensionList: ExtensionList?,
|
|
||||||
@ProtoNumber(102) val extensionListUrl: String?,
|
|
||||||
) : BaseNetworkExtensionStore {
|
|
||||||
@Serializable
|
|
||||||
data class Contact(
|
|
||||||
@ProtoNumber(1) val website: String,
|
|
||||||
@ProtoNumber(2) val discord: String?,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ExtensionList(
|
|
||||||
@ProtoNumber(1) val extensions: List<Extension>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Extension(
|
|
||||||
@ProtoNumber(1) val name: String,
|
|
||||||
@ProtoNumber(2) val packageName: String,
|
|
||||||
@ProtoNumber(3) val resources: Resources,
|
|
||||||
@ProtoNumber(4) val extensionLib: String,
|
|
||||||
@ProtoNumber(5) val versionCode: Long,
|
|
||||||
@ProtoNumber(6) val versionName: String,
|
|
||||||
@ProtoNumber(7) val contentWarning: ContentWarning,
|
|
||||||
@ProtoNumber(8) val sources: List<Source>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Resources(
|
|
||||||
@ProtoNumber(1) val apkUrl: String,
|
|
||||||
@ProtoNumber(2) val iconUrl: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Source(
|
|
||||||
@ProtoNumber(1) val id: Long,
|
|
||||||
@ProtoNumber(2) val name: String,
|
|
||||||
@ProtoNumber(3) val language: String,
|
|
||||||
@ProtoNumber(4) val homeUrl: String = "",
|
|
||||||
@ProtoNumber(5) val mirrorUrls: List<String> = emptyList(),
|
|
||||||
// @ProtoNumber(6) val contentWarning: ContentWarning = ContentWarning.SAFE,
|
|
||||||
@ProtoNumber(7) val message: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
enum class ContentWarning {
|
|
||||||
@ProtoNumber(0)
|
|
||||||
@JsonNames("CONTENT_WARNING_UNSPECIFIED")
|
|
||||||
UNSPECIFIED,
|
|
||||||
|
|
||||||
@ProtoNumber(1)
|
|
||||||
@JsonNames("CONTENT_WARNING_SAFE")
|
|
||||||
SAFE,
|
|
||||||
|
|
||||||
@ProtoNumber(2)
|
|
||||||
@JsonNames("CONTENT_WARNING_MIXED")
|
|
||||||
MIXED,
|
|
||||||
|
|
||||||
@ProtoNumber(3)
|
|
||||||
@JsonNames("CONTENT_WARNING_NSFW")
|
|
||||||
NSFW,
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toExtensionStore(indexUrl: String): ExtensionStore =
|
|
||||||
ExtensionStore(
|
|
||||||
indexUrl = indexUrl,
|
|
||||||
name = name,
|
|
||||||
badgeLabel = badgeLabel,
|
|
||||||
signingKey = signingKey,
|
|
||||||
contact =
|
|
||||||
ExtensionStore.Contact(
|
|
||||||
website = contact.website,
|
|
||||||
discord = contact.discord,
|
|
||||||
),
|
|
||||||
isLegacy = false,
|
|
||||||
extensionListUrl = extensionListUrl,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun NetworkExtensionStore.ExtensionList.toExtensionInfos(store: ExtensionStore): List<ExtensionInfo> =
|
|
||||||
extensions.map { extension ->
|
|
||||||
val lang = extension.sources.map { it.language }.toSet()
|
|
||||||
ExtensionInfo(
|
|
||||||
storeIndexUrl = store.indexUrl,
|
|
||||||
name = extension.name,
|
|
||||||
pkgName = extension.packageName,
|
|
||||||
apkUrl = extension.resources.apkUrl,
|
|
||||||
iconUrl = extension.resources.iconUrl,
|
|
||||||
extensionLib = extension.extensionLib,
|
|
||||||
versionCode = extension.versionCode,
|
|
||||||
versionName = extension.versionName,
|
|
||||||
lang = if (lang.size == 1) lang.first() else "all",
|
|
||||||
contentWarning =
|
|
||||||
when (extension.contentWarning) {
|
|
||||||
NetworkExtensionStore.ContentWarning.SAFE,
|
|
||||||
NetworkExtensionStore.ContentWarning.UNSPECIFIED,
|
|
||||||
-> ContentWarning.SAFE
|
|
||||||
|
|
||||||
NetworkExtensionStore.ContentWarning.MIXED -> ContentWarning.MIXED
|
|
||||||
|
|
||||||
NetworkExtensionStore.ContentWarning.NSFW -> ContentWarning.NSFW
|
|
||||||
},
|
|
||||||
sources =
|
|
||||||
extension.sources.map { source ->
|
|
||||||
ExtensionSource(
|
|
||||||
id = source.id,
|
|
||||||
name = source.name,
|
|
||||||
lang = source.language,
|
|
||||||
homeUrl = source.homeUrl,
|
|
||||||
message = source.message,
|
|
||||||
contentWarning =
|
|
||||||
when (extension.contentWarning) { // todo source.contentWarning
|
|
||||||
NetworkExtensionStore.ContentWarning.SAFE,
|
|
||||||
NetworkExtensionStore.ContentWarning.UNSPECIFIED,
|
|
||||||
-> ContentWarning.SAFE
|
|
||||||
|
|
||||||
NetworkExtensionStore.ContentWarning.MIXED -> ContentWarning.MIXED
|
|
||||||
|
|
||||||
NetworkExtensionStore.ContentWarning.NSFW -> ContentWarning.NSFW
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.extension.github
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
|
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo
|
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionSource
|
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionStore
|
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
|
||||||
@Serializable
|
|
||||||
data class NetworkLegacyExtension(
|
|
||||||
val name: String,
|
|
||||||
val pkg: String,
|
|
||||||
val apk: String,
|
|
||||||
val lang: String,
|
|
||||||
val version: String,
|
|
||||||
val code: Long,
|
|
||||||
val nsfw: Int,
|
|
||||||
val sources: List<Source>? = null,
|
|
||||||
) {
|
|
||||||
@Serializable
|
|
||||||
data class Source(
|
|
||||||
val id: Long,
|
|
||||||
val lang: String,
|
|
||||||
val name: String,
|
|
||||||
val baseUrl: String,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun NetworkLegacyExtension.toExtensionInfo(
|
|
||||||
store: ExtensionStore,
|
|
||||||
storeBaseUrl: String,
|
|
||||||
): ExtensionInfo =
|
|
||||||
ExtensionInfo(
|
|
||||||
storeIndexUrl = store.indexUrl,
|
|
||||||
name = name.substringAfter("Tachiyomi: "),
|
|
||||||
pkgName = pkg,
|
|
||||||
apkUrl = "$storeBaseUrl/apk/$apk",
|
|
||||||
iconUrl = "$storeBaseUrl/icon/$pkg.png",
|
|
||||||
extensionLib = version.substringBeforeLast('.'),
|
|
||||||
versionCode = code,
|
|
||||||
versionName = version,
|
|
||||||
lang = lang,
|
|
||||||
contentWarning = if (nsfw == 1) ContentWarning.MIXED else ContentWarning.SAFE,
|
|
||||||
sources =
|
|
||||||
if (sources.isNullOrEmpty()) {
|
|
||||||
listOf(
|
|
||||||
ExtensionSource(
|
|
||||||
id = 0,
|
|
||||||
name = name,
|
|
||||||
lang = lang,
|
|
||||||
homeUrl = "",
|
|
||||||
message = null,
|
|
||||||
contentWarning = if (nsfw == 1) ContentWarning.MIXED else ContentWarning.SAFE,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
sources.map { source ->
|
|
||||||
ExtensionSource(
|
|
||||||
id = source.id,
|
|
||||||
name = source.name,
|
|
||||||
lang = source.lang,
|
|
||||||
homeUrl = source.baseUrl,
|
|
||||||
message = null,
|
|
||||||
contentWarning = if (nsfw == 1) ContentWarning.MIXED else ContentWarning.SAFE,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.extension.github
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionStore
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class NetworkLegacyExtensionRepo(
|
|
||||||
@SerialName("index_v2") val indexV2: String?,
|
|
||||||
val meta: Meta,
|
|
||||||
) : BaseNetworkExtensionStore {
|
|
||||||
@Serializable
|
|
||||||
data class Meta(
|
|
||||||
val name: String,
|
|
||||||
val shortName: String?,
|
|
||||||
val website: String,
|
|
||||||
val signingKeyFingerprint: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun toExtensionStore(indexUrl: String): ExtensionStore =
|
|
||||||
ExtensionStore(
|
|
||||||
indexUrl = indexUrl,
|
|
||||||
name = meta.name,
|
|
||||||
badgeLabel = meta.shortName ?: meta.name,
|
|
||||||
signingKey = meta.signingKeyFingerprint,
|
|
||||||
contact =
|
|
||||||
ExtensionStore.Contact(
|
|
||||||
website = meta.website,
|
|
||||||
discord = null,
|
|
||||||
),
|
|
||||||
isLegacy = true,
|
|
||||||
extensionListUrl = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.extension.github
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
data class OnlineExtensionSource(
|
||||||
|
val name: String,
|
||||||
|
val lang: String,
|
||||||
|
val id: Long,
|
||||||
|
val baseUrl: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class OnlineExtension(
|
||||||
|
val repo: String,
|
||||||
|
val name: String,
|
||||||
|
val pkgName: String,
|
||||||
|
val apkName: String,
|
||||||
|
val lang: String,
|
||||||
|
val versionCode: Int,
|
||||||
|
val versionName: String,
|
||||||
|
val isNsfw: Boolean,
|
||||||
|
val hasReadme: Boolean,
|
||||||
|
val hasChangelog: Boolean,
|
||||||
|
val sources: List<OnlineExtensionSource>,
|
||||||
|
val iconUrl: String,
|
||||||
|
)
|
||||||
@@ -222,48 +222,6 @@ object Track {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bindTrackRecord(
|
|
||||||
mangaId: Int,
|
|
||||||
trackRecordId: Int,
|
|
||||||
): Int {
|
|
||||||
val (trackRecord, existingTrackRecord) =
|
|
||||||
transaction {
|
|
||||||
val trackRecord =
|
|
||||||
TrackRecordTable
|
|
||||||
.selectAll()
|
|
||||||
.where {
|
|
||||||
(TrackRecordTable.id eq trackRecordId)
|
|
||||||
}.first()
|
|
||||||
.toTrackRecordDataClass()
|
|
||||||
|
|
||||||
val existingTrackRecord =
|
|
||||||
TrackRecordTable
|
|
||||||
.selectAll()
|
|
||||||
.where {
|
|
||||||
(TrackRecordTable.mangaId eq mangaId) and (TrackRecordTable.trackerId eq trackRecord.trackerId)
|
|
||||||
}.firstOrNull()
|
|
||||||
?.toTrackRecordDataClass()
|
|
||||||
|
|
||||||
trackRecord to existingTrackRecord
|
|
||||||
}
|
|
||||||
|
|
||||||
val isAlreadyBoundToManga = trackRecord.mangaId == mangaId
|
|
||||||
if (isAlreadyBoundToManga) {
|
|
||||||
return trackRecordId
|
|
||||||
}
|
|
||||||
|
|
||||||
val hasRecordForTracker = existingTrackRecord != null
|
|
||||||
if (hasRecordForTracker) {
|
|
||||||
val updatedTrack = trackRecord.copy(id = existingTrackRecord.id, mangaId = mangaId).toTrack()
|
|
||||||
|
|
||||||
return updateTrackRecord(updatedTrack)
|
|
||||||
}
|
|
||||||
|
|
||||||
val newTrack = trackRecord.copy(mangaId = mangaId).toTrack()
|
|
||||||
|
|
||||||
return insertTrackRecord(newTrack)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun refresh(recordId: Int) {
|
suspend fun refresh(recordId: Int) {
|
||||||
val recordDb =
|
val recordDb =
|
||||||
transaction {
|
transaction {
|
||||||
@@ -465,9 +423,9 @@ object Track {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateTrackRecord(track: Track): Int = updateTrackRecords(listOf(track)).first()
|
fun updateTrackRecord(track: Track) = updateTrackRecords(listOf(track))
|
||||||
|
|
||||||
fun updateTrackRecords(tracks: List<Track>): List<Int> =
|
fun updateTrackRecords(tracks: List<Track>) =
|
||||||
transaction {
|
transaction {
|
||||||
if (tracks.isNotEmpty()) {
|
if (tracks.isNotEmpty()) {
|
||||||
BatchUpdateStatement(TrackRecordTable)
|
BatchUpdateStatement(TrackRecordTable)
|
||||||
@@ -489,8 +447,6 @@ object Track {
|
|||||||
}.toExecutable()
|
}.toExecutable()
|
||||||
.execute(this@transaction)
|
.execute(this@transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
tracks.map { it.id!! }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun insertTrackRecord(track: Track): Int = insertTrackRecords(listOf(track)).first()
|
fun insertTrackRecord(track: Track): Int = insertTrackRecords(listOf(track)).first()
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import kotlinx.coroutines.sync.withPermit
|
|||||||
import suwayomi.tachidesk.global.impl.sync.SyncManager
|
import suwayomi.tachidesk.global.impl.sync.SyncManager
|
||||||
import suwayomi.tachidesk.manga.impl.Category
|
import suwayomi.tachidesk.manga.impl.Category
|
||||||
import suwayomi.tachidesk.manga.impl.CategoryManga
|
import suwayomi.tachidesk.manga.impl.CategoryManga
|
||||||
|
import suwayomi.tachidesk.manga.impl.Chapter
|
||||||
import suwayomi.tachidesk.manga.impl.Manga
|
import suwayomi.tachidesk.manga.impl.Manga
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.IncludeOrExclude
|
import suwayomi.tachidesk.manga.model.dataclass.IncludeOrExclude
|
||||||
@@ -310,10 +311,10 @@ class Updater : IUpdater {
|
|||||||
tracker[job.manga.id] =
|
tracker[job.manga.id] =
|
||||||
try {
|
try {
|
||||||
logger.info { "Updating ${job.manga}" }
|
logger.info { "Updating ${job.manga}" }
|
||||||
Manga.updateMangaAndChapters(
|
if (serverConfig.updateMangas.value || !job.manga.initialized) {
|
||||||
job.manga.id,
|
Manga.getManga(job.manga.id, true)
|
||||||
updateManga = serverConfig.updateMangas.value || !job.manga.initialized,
|
}
|
||||||
)
|
Chapter.getChapterList(job.manga.id, true)
|
||||||
job.copy(status = JobStatus.COMPLETE)
|
job.copy(status = JobStatus.COMPLETE)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.error(e) { "Error while updating ${job.manga}" }
|
logger.error(e) { "Error while updating ${job.manga}" }
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import io.github.oshai.kotlinlogging.KotlinLogging
|
|||||||
import org.jetbrains.exposed.v1.core.eq
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
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.manga.impl.util.source.GetSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||||
import suwayomi.tachidesk.server.ApplicationDirs
|
import suwayomi.tachidesk.server.ApplicationDirs
|
||||||
@@ -37,7 +37,7 @@ private fun getMangaDir(
|
|||||||
private fun getMangaDir(mangaId: Int): String =
|
private fun getMangaDir(mangaId: Int): String =
|
||||||
transaction {
|
transaction {
|
||||||
val mangaEntry = MangaTable.selectAll().where { MangaTable.id eq mangaId }.first()
|
val mangaEntry = MangaTable.selectAll().where { MangaTable.id eq mangaId }.first()
|
||||||
val source = GetSource.getSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
val source = GetCatalogueSource.getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||||
|
|
||||||
getMangaDir(mangaEntry[MangaTable.title], source.toString())
|
getMangaDir(mangaEntry[MangaTable.title], source.toString())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,13 +40,8 @@ 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.5
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert dex to jar, a wrapper for the dex2jar library
|
* Convert dex to jar, a wrapper for the dex2jar library
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.util.lang
|
|
||||||
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
|
|
||||||
val JsonObjectEmpty = JsonObject(emptyMap())
|
|
||||||
|
|
||||||
val JsonObjectEmptyBytes = byteArrayOf(0x7B, 0x7D)
|
|
||||||
|
|
||||||
val JsonObject.Companion.EMPTY: JsonObject
|
|
||||||
inline get() = JsonObjectEmpty
|
|
||||||
@@ -7,6 +7,7 @@ package suwayomi.tachidesk.manga.impl.util.source
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
@@ -21,14 +22,14 @@ import suwayomi.tachidesk.server.ApplicationDirs
|
|||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
object GetSource {
|
object GetCatalogueSource {
|
||||||
private val logger = KotlinLogging.logger { }
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
private val sourceCache = ConcurrentHashMap<Long, Source>()
|
private val sourceCache = ConcurrentHashMap<Long, CatalogueSource>()
|
||||||
private val applicationDirs: ApplicationDirs by injectLazy()
|
private val applicationDirs: ApplicationDirs by injectLazy()
|
||||||
|
|
||||||
private fun getSource(sourceId: Long): Source? {
|
private fun getCatalogueSource(sourceId: Long): CatalogueSource? {
|
||||||
val cachedResult: Source? = sourceCache[sourceId]
|
val cachedResult: CatalogueSource? = sourceCache[sourceId]
|
||||||
if (cachedResult != null) {
|
if (cachedResult != null) {
|
||||||
return cachedResult
|
return cachedResult
|
||||||
}
|
}
|
||||||
@@ -44,9 +45,7 @@ object GetSource {
|
|||||||
ExtensionTable.selectAll().where { ExtensionTable.id eq extensionId }.first()
|
ExtensionTable.selectAll().where { ExtensionTable.id eq extensionId }.first()
|
||||||
}
|
}
|
||||||
|
|
||||||
val apkName =
|
val apkName = extensionRecord[ExtensionTable.apkName]
|
||||||
extensionRecord[ExtensionTable.apkName]
|
|
||||||
?: throw NullPointerException("Missing apkName")
|
|
||||||
val className = extensionRecord[ExtensionTable.classFQName]
|
val className = extensionRecord[ExtensionTable.classFQName]
|
||||||
val jarName = apkName.substringBefore(".apk") + ".jar"
|
val jarName = apkName.substringBefore(".apk") + ".jar"
|
||||||
val jarPath = "${applicationDirs.extensionsRoot}/$jarName"
|
val jarPath = "${applicationDirs.extensionsRoot}/$jarName"
|
||||||
@@ -61,25 +60,25 @@ object GetSource {
|
|||||||
return sourceCache[sourceId]!!
|
return sourceCache[sourceId]!!
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSourceOrNull(sourceId: Long): Source? =
|
fun getCatalogueSourceOrNull(sourceId: Long): CatalogueSource? =
|
||||||
try {
|
try {
|
||||||
getSource(sourceId)
|
getCatalogueSource(sourceId)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.warn(e) { "getCatalogueSource($sourceId) failed" }
|
logger.warn(e) { "getCatalogueSource($sourceId) failed" }
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSourceOrStub(sourceId: Long): Source = getSourceOrNull(sourceId) ?: StubSource(sourceId)
|
fun getCatalogueSourceOrStub(sourceId: Long): CatalogueSource = getCatalogueSourceOrNull(sourceId) ?: StubSource(sourceId)
|
||||||
|
|
||||||
fun registerSource(sourcePair: Pair<Long, Source>) {
|
fun registerCatalogueSource(sourcePair: Pair<Long, CatalogueSource>) {
|
||||||
sourceCache += sourcePair
|
sourceCache += sourcePair
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unregisterSource(sourceId: Long) {
|
fun unregisterCatalogueSource(sourceId: Long) {
|
||||||
sourceCache.remove(sourceId)
|
sourceCache.remove(sourceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unregisterAllSources() {
|
fun unregisterAllCatalogueSources() {
|
||||||
(sourceCache - 0L).forEach { (id, _) ->
|
(sourceCache - 0L).forEach { (id, _) ->
|
||||||
sourceCache.remove(id)
|
sourceCache.remove(id)
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.source.model.MangasPage
|
|||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.model.SMangaUpdate
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
open class StubSource(
|
open class StubSource(
|
||||||
@@ -24,17 +23,9 @@ open class StubSource(
|
|||||||
override val name: String
|
override val name: String
|
||||||
get() = id.toString()
|
get() = id.toString()
|
||||||
|
|
||||||
override suspend fun getPopularManga(page: Int): MangasPage = throw getSourceNotInstalledException()
|
|
||||||
|
|
||||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga"))
|
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga"))
|
||||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> = Observable.error(getSourceNotInstalledException())
|
override fun fetchPopularManga(page: Int): Observable<MangasPage> = Observable.error(getSourceNotInstalledException())
|
||||||
|
|
||||||
override suspend fun getSearchManga(
|
|
||||||
page: Int,
|
|
||||||
query: String,
|
|
||||||
filters: FilterList,
|
|
||||||
): MangasPage = throw getSourceNotInstalledException()
|
|
||||||
|
|
||||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga"))
|
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga"))
|
||||||
override fun fetchSearchManga(
|
override fun fetchSearchManga(
|
||||||
page: Int,
|
page: Int,
|
||||||
@@ -42,28 +33,17 @@ open class StubSource(
|
|||||||
filters: FilterList,
|
filters: FilterList,
|
||||||
): Observable<MangasPage> = Observable.error(getSourceNotInstalledException())
|
): Observable<MangasPage> = Observable.error(getSourceNotInstalledException())
|
||||||
|
|
||||||
override suspend fun getLatestUpdates(page: Int): MangasPage = throw getSourceNotInstalledException()
|
|
||||||
|
|
||||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates"))
|
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates"))
|
||||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> = Observable.error(getSourceNotInstalledException())
|
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> = Observable.error(getSourceNotInstalledException())
|
||||||
|
|
||||||
override fun getFilterList(): FilterList = FilterList()
|
override fun getFilterList(): FilterList = FilterList()
|
||||||
|
|
||||||
override suspend fun getMangaUpdate(
|
|
||||||
manga: SManga,
|
|
||||||
chapters: List<SChapter>,
|
|
||||||
fetchDetails: Boolean,
|
|
||||||
fetchChapters: Boolean,
|
|
||||||
): SMangaUpdate = throw getSourceNotInstalledException()
|
|
||||||
|
|
||||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails"))
|
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails"))
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> = Observable.error(getSourceNotInstalledException())
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> = Observable.error(getSourceNotInstalledException())
|
||||||
|
|
||||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList"))
|
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList"))
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.error(getSourceNotInstalledException())
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.error(getSourceNotInstalledException())
|
||||||
|
|
||||||
override suspend fun getPageList(chapter: SChapter): List<Page> = throw getSourceNotInstalledException()
|
|
||||||
|
|
||||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList"))
|
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList"))
|
||||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.error(getSourceNotInstalledException())
|
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.error(getSourceNotInstalledException())
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ object ImageResponse {
|
|||||||
/**
|
/**
|
||||||
* Get a cached image response
|
* Get a cached image response
|
||||||
*
|
*
|
||||||
* Note: The caller should also call [ImageResponse.clearCachedImage] when appropriate
|
* Note: The caller should also call [clearCachedImage] when appropriate
|
||||||
*
|
*
|
||||||
* @param cacheSavePath where to save the cached image. Caller should decide to use perma cache or temp cache (OS temp dir)
|
* @param cacheSavePath where to save the cached image. Caller should decide to use perma cache or temp cache (OS temp dir)
|
||||||
* @param fileName what the saved cache file should be named
|
* @param fileName what the saved cache file should be named
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
package suwayomi.tachidesk.manga.model.dataclass
|
package suwayomi.tachidesk.manga.model.dataclass
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
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.manga.impl.Chapter.getChapterMetaMap
|
import suwayomi.tachidesk.manga.impl.Chapter.getChapterMetaMap
|
||||||
import suwayomi.tachidesk.manga.impl.util.lang.EMPTY
|
|
||||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -46,8 +43,6 @@ data class ChapterDataClass(
|
|||||||
val pageCount: Int = -1,
|
val pageCount: Int = -1,
|
||||||
val lastModifiedAt: Long = 0,
|
val lastModifiedAt: Long = 0,
|
||||||
val version: Long = 0,
|
val version: Long = 0,
|
||||||
@JsonIgnore
|
|
||||||
val memo: JsonObject = JsonObject.EMPTY,
|
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun fromSChapter(
|
fun fromSChapter(
|
||||||
@@ -65,7 +60,6 @@ data class ChapterDataClass(
|
|||||||
uploadDate = sChapter.date_upload,
|
uploadDate = sChapter.date_upload,
|
||||||
chapterNumber = sChapter.chapter_number,
|
chapterNumber = sChapter.chapter_number,
|
||||||
scanlator = sChapter.scanlator,
|
scanlator = sChapter.scanlator,
|
||||||
memo = sChapter.memo,
|
|
||||||
index = index,
|
index = index,
|
||||||
fetchedAt = fetchedAt,
|
fetchedAt = fetchedAt,
|
||||||
realUrl = realUrl,
|
realUrl = realUrl,
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.model.dataclass
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
data class ExtensionInfo(
|
|
||||||
val storeIndexUrl: String,
|
|
||||||
val name: String,
|
|
||||||
val pkgName: String,
|
|
||||||
val apkUrl: String,
|
|
||||||
val iconUrl: String,
|
|
||||||
val extensionLib: String,
|
|
||||||
val versionCode: Long,
|
|
||||||
val versionName: String,
|
|
||||||
val lang: String,
|
|
||||||
val contentWarning: ContentWarning,
|
|
||||||
val sources: List<ExtensionSource>,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class ExtensionSource(
|
|
||||||
val id: Long,
|
|
||||||
val name: String,
|
|
||||||
val lang: String,
|
|
||||||
val homeUrl: String,
|
|
||||||
val message: String?,
|
|
||||||
val contentWarning: ContentWarning,
|
|
||||||
)
|
|
||||||
|
|
||||||
enum class ContentWarning {
|
|
||||||
SAFE,
|
|
||||||
MIXED,
|
|
||||||
NSFW,
|
|
||||||
;
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun valueOf(contentWarning: Int) = entries.find { it.ordinal == contentWarning } ?: SAFE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.model.dataclass
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
data class ExtensionStore(
|
|
||||||
val indexUrl: String,
|
|
||||||
val name: String,
|
|
||||||
val badgeLabel: String,
|
|
||||||
val signingKey: String,
|
|
||||||
val contact: Contact,
|
|
||||||
val isLegacy: Boolean,
|
|
||||||
val extensionListUrl: String?,
|
|
||||||
) {
|
|
||||||
data class Contact(
|
|
||||||
val website: String,
|
|
||||||
val discord: String?,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -7,11 +7,8 @@ package suwayomi.tachidesk.manga.model.dataclass
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import suwayomi.tachidesk.manga.impl.Manga.getMangaMetaMap
|
import suwayomi.tachidesk.manga.impl.Manga.getMangaMetaMap
|
||||||
import suwayomi.tachidesk.manga.impl.util.lang.EMPTY
|
|
||||||
import suwayomi.tachidesk.manga.impl.util.lang.trimAll
|
import suwayomi.tachidesk.manga.impl.util.lang.trimAll
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaStatus
|
import suwayomi.tachidesk.manga.model.table.MangaStatus
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -47,8 +44,6 @@ data class MangaDataClass(
|
|||||||
val trackers: List<MangaTrackerDataClass>? = null,
|
val trackers: List<MangaTrackerDataClass>? = null,
|
||||||
val lastModifiedAt: Long = 0,
|
val lastModifiedAt: Long = 0,
|
||||||
val version: Long = 0,
|
val version: Long = 0,
|
||||||
@JsonIgnore
|
|
||||||
val memo: JsonObject = JsonObject.EMPTY,
|
|
||||||
) {
|
) {
|
||||||
override fun toString(): String = "\"$title\" (id= $id) (sourceId= $sourceId)"
|
override fun toString(): String = "\"$title\" (id= $id) (sourceId= $sourceId)"
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,11 @@ package suwayomi.tachidesk.manga.model.table
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import org.jetbrains.exposed.v1.core.ReferenceOption
|
import org.jetbrains.exposed.v1.core.ReferenceOption
|
||||||
import org.jetbrains.exposed.v1.core.ResultRow
|
import org.jetbrains.exposed.v1.core.ResultRow
|
||||||
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
|
||||||
import suwayomi.tachidesk.manga.model.table.columns.truncatingVarchar
|
import suwayomi.tachidesk.manga.model.table.columns.truncatingVarchar
|
||||||
import suwayomi.tachidesk.manga.model.table.columns.unlimitedVarchar
|
|
||||||
|
|
||||||
object ChapterTable : IntIdTable() {
|
object ChapterTable : IntIdTable() {
|
||||||
val url = varchar("url", 2048)
|
val url = varchar("url", 2048)
|
||||||
@@ -44,8 +42,6 @@ object ChapterTable : IntIdTable() {
|
|||||||
val lastModifiedAt = long("last_modified_at").default(0)
|
val lastModifiedAt = long("last_modified_at").default(0)
|
||||||
val version = long("version").default(0)
|
val version = long("version").default(0)
|
||||||
val isSyncing = bool("is_syncing").default(false)
|
val isSyncing = bool("is_syncing").default(false)
|
||||||
|
|
||||||
val memo = unlimitedVarchar("memo")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ChapterTable.toDataClass(chapterEntry: ResultRow) =
|
fun ChapterTable.toDataClass(chapterEntry: ResultRow) =
|
||||||
@@ -68,5 +64,4 @@ fun ChapterTable.toDataClass(chapterEntry: ResultRow) =
|
|||||||
pageCount = chapterEntry[pageCount],
|
pageCount = chapterEntry[pageCount],
|
||||||
lastModifiedAt = chapterEntry[lastModifiedAt],
|
lastModifiedAt = chapterEntry[lastModifiedAt],
|
||||||
version = chapterEntry[version],
|
version = chapterEntry[version],
|
||||||
memo = Json.decodeFromString(chapterEntry[memo]),
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.model.table
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
|
||||||
|
|
||||||
object ExtensionStoreTable : IntIdTable() {
|
|
||||||
val indexUrl = varchar("index_url", 2048).uniqueIndex()
|
|
||||||
val name = varchar("name", 256)
|
|
||||||
val badgeLabel = varchar("badge_label", 32)
|
|
||||||
val signingKey = varchar("signing_key", 512)
|
|
||||||
val contactWebsite = varchar("contact_website", 2048)
|
|
||||||
val contactDiscord = varchar("contact_discord", 2048).nullable()
|
|
||||||
val isLegacy = bool("is_legacy").default(false)
|
|
||||||
val extensionListUrl = varchar("extension_list_url", 2048).nullable()
|
|
||||||
}
|
|
||||||
@@ -10,8 +10,8 @@ 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 ExtensionTable : IntIdTable() {
|
object ExtensionTable : IntIdTable() {
|
||||||
val apkName = varchar("apk_name", 1024).nullable()
|
val apkName = varchar("apk_name", 1024)
|
||||||
val storeIndexUrl = varchar("store_index_url", 2048).nullable().index()
|
val repo = varchar("repo", 1024).nullable()
|
||||||
|
|
||||||
// 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")
|
||||||
@@ -23,12 +23,10 @@ object ExtensionTable : IntIdTable() {
|
|||||||
|
|
||||||
val name = varchar("name", 128)
|
val name = varchar("name", 128)
|
||||||
val pkgName = varchar("pkg_name", 128)
|
val pkgName = varchar("pkg_name", 128)
|
||||||
val apkUrl = varchar("apk_url", 2048).nullable()
|
|
||||||
val extensionLib = varchar("extension_lib", 16).nullable()
|
|
||||||
val versionName = varchar("version_name", 16)
|
val versionName = varchar("version_name", 16)
|
||||||
val versionCode = long("version_code")
|
val versionCode = integer("version_code")
|
||||||
val lang = varchar("lang", 32)
|
val lang = varchar("lang", 32)
|
||||||
val contentWarning = integer("content_warning")
|
val isNsfw = bool("is_nsfw")
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ package suwayomi.tachidesk.manga.model.table
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import org.jetbrains.exposed.v1.core.ResultRow
|
import org.jetbrains.exposed.v1.core.ResultRow
|
||||||
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||||
import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl
|
import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl
|
||||||
@@ -49,7 +48,6 @@ object MangaTable : IntIdTable() {
|
|||||||
val lastModifiedAt = long("last_modified_at").default(0)
|
val lastModifiedAt = long("last_modified_at").default(0)
|
||||||
val version = long("version").default(0)
|
val version = long("version").default(0)
|
||||||
val isSyncing = bool("is_syncing").default(false)
|
val isSyncing = bool("is_syncing").default(false)
|
||||||
val memo = unlimitedVarchar("memo")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MangaTable.toDataClass(mangaEntry: ResultRow) =
|
fun MangaTable.toDataClass(mangaEntry: ResultRow) =
|
||||||
@@ -74,7 +72,6 @@ fun MangaTable.toDataClass(mangaEntry: ResultRow) =
|
|||||||
updateStrategy = UpdateStrategy.valueOf(mangaEntry[updateStrategy]),
|
updateStrategy = UpdateStrategy.valueOf(mangaEntry[updateStrategy]),
|
||||||
lastModifiedAt = mangaEntry[lastModifiedAt],
|
lastModifiedAt = mangaEntry[lastModifiedAt],
|
||||||
version = mangaEntry[version],
|
version = mangaEntry[version],
|
||||||
memo = Json.decodeFromString(mangaEntry[memo]),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class MangaStatus(
|
enum class MangaStatus(
|
||||||
|
|||||||
@@ -14,5 +14,5 @@ object SourceTable : IdTable<Long>() {
|
|||||||
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 contentWarning = integer("content_warning").default(0)
|
val isNsfw = bool("is_nsfw").default(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package suwayomi.tachidesk.opds.impl
|
|||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import org.jetbrains.exposed.v1.core.SortOrder
|
import org.jetbrains.exposed.v1.core.SortOrder
|
||||||
import suwayomi.tachidesk.i18n.MR
|
import suwayomi.tachidesk.i18n.MR
|
||||||
import suwayomi.tachidesk.manga.impl.Manga
|
|
||||||
import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl
|
import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl
|
||||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||||
import suwayomi.tachidesk.opds.constants.OpdsConstants
|
import suwayomi.tachidesk.opds.constants.OpdsConstants
|
||||||
@@ -18,6 +17,7 @@ import suwayomi.tachidesk.opds.repository.ChapterRepository
|
|||||||
import suwayomi.tachidesk.opds.repository.MangaRepository
|
import suwayomi.tachidesk.opds.repository.MangaRepository
|
||||||
import suwayomi.tachidesk.opds.repository.NavigationRepository
|
import suwayomi.tachidesk.opds.repository.NavigationRepository
|
||||||
import suwayomi.tachidesk.opds.util.OpdsDateUtil
|
import suwayomi.tachidesk.opds.util.OpdsDateUtil
|
||||||
|
import suwayomi.tachidesk.opds.util.OpdsStringUtil
|
||||||
import suwayomi.tachidesk.opds.util.OpdsXmlUtil
|
import suwayomi.tachidesk.opds.util.OpdsXmlUtil
|
||||||
import suwayomi.tachidesk.server.serverConfig
|
import suwayomi.tachidesk.server.serverConfig
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -657,7 +657,8 @@ object OpdsFeedBuilder {
|
|||||||
// 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 {
|
||||||
Manga.updateMangaAndChapters(mangaId, updateManga = false)
|
suwayomi.tachidesk.manga.impl.Chapter
|
||||||
|
.fetchChapterList(mangaId)
|
||||||
|
|
||||||
// Re-query after fetching.
|
// Re-query after fetching.
|
||||||
val (refetchedChapters, refetchedTotal) =
|
val (refetchedChapters, refetchedTotal) =
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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.inSubQuery
|
||||||
import org.jetbrains.exposed.v1.core.intLiteral
|
import org.jetbrains.exposed.v1.core.intLiteral
|
||||||
@@ -22,7 +23,7 @@ import org.jetbrains.exposed.v1.jdbc.andWhere
|
|||||||
import org.jetbrains.exposed.v1.jdbc.select
|
import org.jetbrains.exposed.v1.jdbc.select
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
import suwayomi.tachidesk.manga.impl.MangaList.insertOrUpdate
|
import suwayomi.tachidesk.manga.impl.MangaList.insertOrUpdate
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.toGenreList
|
import suwayomi.tachidesk.manga.model.dataclass.toGenreList
|
||||||
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
|
||||||
@@ -231,7 +232,7 @@ object MangaRepository {
|
|||||||
pageNum: Int,
|
pageNum: Int,
|
||||||
sort: String,
|
sort: String,
|
||||||
): Pair<List<OpdsMangaAcqEntry>, Boolean> {
|
): Pair<List<OpdsMangaAcqEntry>, Boolean> {
|
||||||
val source = GetSource.getSourceOrStub(sourceId)
|
val source = GetCatalogueSource.getCatalogueSourceOrStub(sourceId)
|
||||||
val mangasPage: MangasPage =
|
val mangasPage: MangasPage =
|
||||||
if (sort == "latest" && source.supportsLatest) {
|
if (sort == "latest" && source.supportsLatest) {
|
||||||
source.getLatestUpdates(pageNum)
|
source.getLatestUpdates(pageNum)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import dev.icerock.moko.resources.StringResource
|
|||||||
import org.jetbrains.exposed.v1.core.JoinType
|
import org.jetbrains.exposed.v1.core.JoinType
|
||||||
import org.jetbrains.exposed.v1.core.SortOrder
|
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.count
|
||||||
import org.jetbrains.exposed.v1.core.countDistinct
|
import org.jetbrains.exposed.v1.core.countDistinct
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
import org.jetbrains.exposed.v1.jdbc.select
|
import org.jetbrains.exposed.v1.jdbc.select
|
||||||
@@ -137,9 +138,9 @@ object NavigationRepository {
|
|||||||
val query =
|
val query =
|
||||||
SourceTable
|
SourceTable
|
||||||
.join(ExtensionTable, JoinType.LEFT, onColumn = SourceTable.extension, otherColumn = ExtensionTable.id)
|
.join(ExtensionTable, JoinType.LEFT, onColumn = SourceTable.extension, otherColumn = ExtensionTable.id)
|
||||||
.select(SourceTable.id, SourceTable.name, SourceTable.lang, ExtensionTable.pkgName)
|
.select(SourceTable.id, SourceTable.name, SourceTable.lang, ExtensionTable.apkName)
|
||||||
.where { ExtensionTable.isInstalled eq true }
|
.where { ExtensionTable.isInstalled eq true }
|
||||||
.groupBy(SourceTable.id, SourceTable.name, SourceTable.lang, ExtensionTable.pkgName)
|
.groupBy(SourceTable.id, SourceTable.name, SourceTable.lang, ExtensionTable.apkName)
|
||||||
.orderBy(SourceTable.name to SortOrder.ASC)
|
.orderBy(SourceTable.name to SortOrder.ASC)
|
||||||
|
|
||||||
val totalCount = query.count()
|
val totalCount = query.count()
|
||||||
@@ -151,7 +152,7 @@ object NavigationRepository {
|
|||||||
OpdsSourceNavEntry(
|
OpdsSourceNavEntry(
|
||||||
id = it[SourceTable.id].value,
|
id = it[SourceTable.id].value,
|
||||||
name = formatSourceName(it[SourceTable.name], it[SourceTable.lang]),
|
name = formatSourceName(it[SourceTable.name], it[SourceTable.lang]),
|
||||||
iconUrl = it[ExtensionTable.pkgName].let { pkgName -> Extension.proxyExtensionIconUrl(pkgName) },
|
iconUrl = it[ExtensionTable.apkName].let { apkName -> Extension.getExtensionIconUrl(apkName) },
|
||||||
mangaCount = null,
|
mangaCount = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -176,13 +177,13 @@ object NavigationRepository {
|
|||||||
|
|
||||||
val query =
|
val query =
|
||||||
baseJoin
|
baseJoin
|
||||||
.select(SourceTable.id, SourceTable.name, SourceTable.lang, ExtensionTable.pkgName, mangaCount)
|
.select(SourceTable.id, SourceTable.name, SourceTable.lang, ExtensionTable.apkName, mangaCount)
|
||||||
.where { MangaTable.inLibrary eq true }
|
.where { MangaTable.inLibrary eq true }
|
||||||
|
|
||||||
query.applyOpdsMangaFilter(activeFilters, excludeField = "source_id")
|
query.applyOpdsMangaFilter(activeFilters, excludeField = "source_id")
|
||||||
|
|
||||||
query
|
query
|
||||||
.groupBy(SourceTable.id, SourceTable.name, SourceTable.lang, ExtensionTable.pkgName)
|
.groupBy(SourceTable.id, SourceTable.name, SourceTable.lang, ExtensionTable.apkName)
|
||||||
.orderBy(SourceTable.name to SortOrder.ASC)
|
.orderBy(SourceTable.name to SortOrder.ASC)
|
||||||
|
|
||||||
val totalCount = query.count()
|
val totalCount = query.count()
|
||||||
@@ -198,7 +199,7 @@ object NavigationRepository {
|
|||||||
OpdsSourceNavEntry(
|
OpdsSourceNavEntry(
|
||||||
id = it[SourceTable.id].value,
|
id = it[SourceTable.id].value,
|
||||||
name = formatSourceName(it[SourceTable.name], it[SourceTable.lang]),
|
name = formatSourceName(it[SourceTable.name], it[SourceTable.lang]),
|
||||||
iconUrl = it[ExtensionTable.pkgName].let { pkgName -> Extension.proxyExtensionIconUrl(pkgName) },
|
iconUrl = it[ExtensionTable.apkName].let { apkName -> Extension.getExtensionIconUrl(apkName) },
|
||||||
mangaCount = it[mangaCount],
|
mangaCount = it[mangaCount],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -209,12 +210,12 @@ object NavigationRepository {
|
|||||||
transaction {
|
transaction {
|
||||||
SourceTable
|
SourceTable
|
||||||
.join(ExtensionTable, JoinType.LEFT, onColumn = SourceTable.extension, otherColumn = ExtensionTable.id)
|
.join(ExtensionTable, JoinType.LEFT, onColumn = SourceTable.extension, otherColumn = ExtensionTable.id)
|
||||||
.select(SourceTable.name, SourceTable.lang, ExtensionTable.pkgName)
|
.select(SourceTable.name, SourceTable.lang, ExtensionTable.apkName)
|
||||||
.where { SourceTable.id eq sourceId }
|
.where { SourceTable.id eq sourceId }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
?.let {
|
?.let {
|
||||||
val name = formatSourceName(it[SourceTable.name], it[SourceTable.lang])
|
val name = formatSourceName(it[SourceTable.name], it[SourceTable.lang])
|
||||||
val icon = Extension.proxyExtensionIconUrl(it[ExtensionTable.pkgName])
|
val icon = Extension.getExtensionIconUrl(it[ExtensionTable.apkName])
|
||||||
Pair(name, icon)
|
Pair(name, icon)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ import suwayomi.tachidesk.graphql.types.DatabaseType
|
|||||||
import suwayomi.tachidesk.i18n.LocalizationHelper
|
import suwayomi.tachidesk.i18n.LocalizationHelper
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport
|
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport
|
||||||
import suwayomi.tachidesk.manga.impl.download.DownloadManager
|
import suwayomi.tachidesk.manga.impl.download.DownloadManager
|
||||||
import suwayomi.tachidesk.manga.impl.extension.ExtensionStoreService
|
|
||||||
import suwayomi.tachidesk.manga.impl.update.IUpdater
|
import suwayomi.tachidesk.manga.impl.update.IUpdater
|
||||||
import suwayomi.tachidesk.manga.impl.update.Updater
|
import suwayomi.tachidesk.manga.impl.update.Updater
|
||||||
import suwayomi.tachidesk.manga.impl.util.lang.renameTo
|
import suwayomi.tachidesk.manga.impl.util.lang.renameTo
|
||||||
@@ -520,12 +519,4 @@ fun applicationSetup() {
|
|||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
CEFManager.init()
|
CEFManager.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
serverConfig.subscribeTo(
|
|
||||||
serverConfig.extensionStores,
|
|
||||||
{ _ ->
|
|
||||||
ExtensionStoreService.syncPrefsToDb()
|
|
||||||
},
|
|
||||||
ignoreInitialValue = false,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user