From bfa0038f5329bf4bca711c82a21c73b5da458cc3 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Thu, 18 Jun 2026 04:39:15 +0200 Subject: [PATCH 1/6] Weblate translations (#1903) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/el/ Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/ja/ Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/pl/ Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/ru/ Translation: Suwayomi/Suwayomi-Server Co-authored-by: Micka149 Co-authored-by: Philip Prescott-Decie Co-authored-by: Roland Vezsenyi Co-authored-by: Syer10 Co-authored-by: TheRay82 Co-authored-by: UnknownSkyrimPasserby Co-authored-by: 圭紫 --- .../moko-resources/values/el/strings.xml | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 server/i18n/src/commonMain/moko-resources/values/el/strings.xml diff --git a/server/i18n/src/commonMain/moko-resources/values/el/strings.xml b/server/i18n/src/commonMain/moko-resources/values/el/strings.xml new file mode 100644 index 000000000..70d0286e5 --- /dev/null +++ b/server/i18n/src/commonMain/moko-resources/values/el/strings.xml @@ -0,0 +1,125 @@ + + + Suwayomi OPDS Κατάλογος + Κεφάλαια %1$s + %1$s | %2$s | Λεπτομέρειες + Εξερεύνηση + Εξερεύνησε νέες σειρές από τις πηγές σου + Ιστορικό + Πρόσφατα διαβασμένα κεφάλαια + Όλες οι σειρές + Περιήγηση σε όλες τις σειρές της βιβλιοθήκης σου + Πηγές + Περιήγηση σε σειρές της βιβλιοθήκης σου φιλτραρισμένες κατά πηγή + Κατηγορίες + Περιήγηση σε σειρές οργανωμένες κατά κατηγορία + Είδη + Περιήγηση σε σειρές κατά ετικέτες είδους + Κατάσταση + Περιήγηση σε σειρές κατά κατάσταση δημοσίευσης + Γλώσσες + Περιήγηση σε σειρές κατά γλώσσα περιεχομένου + Ιστορικό ενημερώσεων βιβλιοθήκης + Πρόσφατα ενημερωμένα κεφάλαια από τη βιβλιοθήκη σου + Αποτελέσματα αναζήτησης + Όλες οι πηγές + Κατηγορία: %1$s + Είδος: %1$s + Κατάσταση: %1$s + Γλώσσα: %1$s + Πηγή: %1$s + Βιβλιοθήκη - Πηγή: %1$s + Πηγή: %1$s - Δημοφιλές + Πηγή: %1$s - Τελευταίο + Suwayomi OPDS Αναζήτηση + Αναζήτηση σειρών στον κατάλογο. + Η σειρά με ID %1$d δεν βρέθηκε. + Το κεφάλαιο με δείκτη %1$d δεν βρέθηκε. + Σειρά ταξινόμησης + Φίλτρο κατά κατάσταση ανάγνωσης + Φίλτρο περιεχομένου + Φίλτρο κατά πηγή + Φίλτρο κατά κατηγορία + Φίλτρο κατά κατάσταση + Φίλτρο κατά γλώσσα + Φίλτρο κατά είδος + Παλαιότερα πρώτα + Νεότερα πρώτα + Ημερομηνία αύξουσα + Ημερομηνία φθίνουσα + Δημοφιλές + Τελευταίο + Αλφαβητικά Α-Ω + Αλφαβητικά Ω-Α + Τελευταία ανάγνωση + Τελευταίο κεφάλαιο + Ημερομηνία προσθήκης + Αδιάβαστα κεφάλαια + Όλα + Όλα τα κεφάλαια + Αδιάβαστα + Διαβασμένα + Ληφθέντα + Σε εξέλιξη + Ολοκληρωμένα + Όλες οι πηγές + Όλες οι κατηγορίες + Όλες οι καταστάσεις + Όλες οι γλώσσες + Όλα τα είδη + Ρίζα καταλόγου + Αναζήτηση καταλόγου + Πρώτη σελίδα + Προηγούμενη σελίδα + Επόμενη σελίδα + Τελευταία σελίδα + Τρέχουσα ροή + Προβολή στο Web + Διάβασε Online + Συνέχισε να διαβάζεις Online + Διάβασε Online (Τοπική πρόοδος) + Συνέχισε να διαβάζεις Online (Τοπική πρόοδος) + Διάβασε Online (Συγχρονισμένο από %1$s) + Συνέχισε να διαβάζεις Online (Συγχρονισμένο από %1$s) + Λήψη CBZ + Εξώφυλλο κεφαλαίου + Λεπτομέρειες κεφαλαίου & Σελίδες + + + + ⬇️ + 🌐 + Σειρά: %1$s | %2$s + | Από %1$s + | Πρόοδος: %1$d από %2$d + Κατάσταση: %1$s + Πηγή: %1$s + Γλώσσα: %1$s + Σε εξέλιξη + Ολοκληρωμένο + Υπό άδεια + Δημοσίευση ολοκληρωμένη + Ακυρωμένο + Σε παύση + Άγνωστο + Σφάλμα + Έκδοση %1$s + Κλείσε + Suwayomi WebView + Αποσυνδεδεμένο, κάνε ανανέωση + Αντίστροφη κύλιση + Σημείωση: Όταν η εστίαση είναι στο τμήμα WebView, καμία συντόμευση πληκτρολογίου, συμπεριλαμβανομένης της ανανέωσης, δεν θα αντιμετωπίζεται από το πρόγραμμα περιήγησης. + Αρχικοποίηση... Παρακαλώ περίμενε + Εισήγαγε μια διεύθυνση URL για να ξεκινήσεις + Φόρτωση σελίδας... + Αντιγραφή στο πρόχειρο + Η αυτόματη αντιγραφή στο πρόχειρο απέτυχε, χρησιμοποίησε το παρακάτω πεδίο για να αντιγράψεις χειροκίνητα την τιμή. + Η διαμόρφωσή σου απαιτεί σύνδεση. Εισήγαγε όνομα χρήστη και κωδικό. + Εισήγαγε URL... + Suwayomi Σύνδεση + Όνομα χρήστη + Κωδικός + Σύνδεση + Πληκτρολόγησε όνομα χρήστη... + Μυστικό... + From 14ab3aa9f452ee62a6693f4b6f5dc95361ebff34 Mon Sep 17 00:00:00 2001 From: Zeedif Date: Wed, 17 Jun 2026 20:39:22 -0600 Subject: [PATCH 2/6] fix(opds): handle dead sources and prevent kosync binary hash crashes (#2116) --- .../moko-resources/values/base/strings.xml | 1 + .../manga/impl/sync/KoreaderSyncService.kt | 39 +++++++++++-------- .../tachidesk/opds/impl/OpdsFeedBuilder.kt | 10 +++++ .../opds/repository/ChapterRepository.kt | 2 + 4 files changed, 36 insertions(+), 16 deletions(-) diff --git a/server/i18n/src/commonMain/moko-resources/values/base/strings.xml b/server/i18n/src/commonMain/moko-resources/values/base/strings.xml index 0e599ff49..1671f1c01 100644 --- a/server/i18n/src/commonMain/moko-resources/values/base/strings.xml +++ b/server/i18n/src/commonMain/moko-resources/values/base/strings.xml @@ -50,6 +50,7 @@ Series with ID %1$d not found. Chapter with index %1$d not found. + No chapters found or the source is unreachable on page %1$d. Sort Order diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/sync/KoreaderSyncService.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/sync/KoreaderSyncService.kt index a0ca9db8a..cbb47f81b 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/sync/KoreaderSyncService.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/sync/KoreaderSyncService.kt @@ -127,29 +127,36 @@ object KoreaderSyncService { } val mangaId = chapterRow[ChapterTable.manga].value + val isDownloaded = chapterRow[ChapterTable.isDownloaded] val checksumMethod = serverConfig.koreaderSyncChecksumMethod.value val newHash = when (checksumMethod) { KoreaderSyncChecksumMethod.BINARY -> { - logger.debug { "[KOSYNC HASH] No hash for chapterId=$chapterId. Generating from downloaded content." } - try { - // Always create a CBZ in memory if it doesn't exist - val (stream, _) = ChapterDownloadHelper.getArchiveStreamWithSize(mangaId, chapterId) - // Write the stream to a temp file for partial hashing - val tempFile = File.createTempFile("kosync-hash-", ".cbz") + // Only generate binary hash if the chapter is downloaded to avoid fetching missing files + if (isDownloaded) { + logger.debug { "[KOSYNC HASH] No hash for chapterId=$chapterId. Generating from downloaded content." } try { - tempFile.outputStream().use { fos -> - stream.use { it.copyTo(fos) } + // Always create a CBZ in memory if it doesn't exist + val (stream, _) = ChapterDownloadHelper.getArchiveStreamWithSize(mangaId, chapterId) + // Write the stream to a temp file for partial hashing + val tempFile = File.createTempFile("kosync-hash-", ".cbz") + try { + tempFile.outputStream().use { fos -> + stream.use { it.copyTo(fos) } + } + // Use the same hashing method as for downloads + KoreaderHelper.hashContents(tempFile) + } finally { + // Always delete the temp file + tempFile.delete() } - // Use the same hashing method as for downloads - KoreaderHelper.hashContents(tempFile) - } finally { - // Always delete the temp file - tempFile.delete() + } catch (e: Exception) { + logger.warn(e) { "[KOSYNC HASH] Failed to generate archive stream for chapterId=$chapterId." } + null } - } catch (e: Exception) { - logger.warn(e) { "[KOSYNC HASH] Failed to generate archive stream for chapterId=$chapterId." } + } else { + logger.debug { "[KOSYNC HASH] Skipping binary hash for chapterId=$chapterId because it is not downloaded." } null } } @@ -175,7 +182,7 @@ object KoreaderSyncService { } logger.info { "[KOSYNC HASH] Generated and saved new hash for chapterId=$chapterId" } } else { - logger.warn { "[KOSYNC HASH] Hashing failed for chapterId=$chapterId." } + logger.warn { "[KOSYNC HASH] Hashing failed or skipped for chapterId=$chapterId." } } newHash } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/opds/impl/OpdsFeedBuilder.kt b/server/src/main/kotlin/suwayomi/tachidesk/opds/impl/OpdsFeedBuilder.kt index 6451a23cb..41c505190 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/opds/impl/OpdsFeedBuilder.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/opds/impl/OpdsFeedBuilder.kt @@ -644,6 +644,16 @@ object OpdsFeedBuilder { skipMetadata, ) + // Return a not-found feed if all available chapters are filtered out as unreachable + if (skipMetadata && chapterEntries.isEmpty() && totalChapters > 0L) { + return buildNotFoundFeed( + baseUrl = baseUrl, + locale = locale, + idPath = "series/$mangaId/chapters", + title = MR.strings.opds_error_chapters_not_found.localized(locale, pageNum), + ) + } + // If no chapters are found in the database, attempt to fetch them from the source. if (chapterEntries.isEmpty() && totalChapters == 0L) { try { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/opds/repository/ChapterRepository.kt b/server/src/main/kotlin/suwayomi/tachidesk/opds/repository/ChapterRepository.kt index df7ca43ea..7b9599ffd 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/opds/repository/ChapterRepository.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/opds/repository/ChapterRepository.kt @@ -169,6 +169,8 @@ object ChapterRepository { } } }.awaitAll() + // Exclude unreachable chapters that are not downloaded and have no page count + .filter { it.downloaded || it.pageCount > 0 } return Pair(enrichedChapters, totalCount) } From b33069f107dcdacfc952e7d1b8515cb3606dea28 Mon Sep 17 00:00:00 2001 From: Zeedif Date: Wed, 17 Jun 2026 20:39:30 -0600 Subject: [PATCH 3/6] fix(opds): resolve sql group by syntax error when filtering library (#2118) --- .../opds/repository/MangaRepository.kt | 87 ++++++++++++------- .../opds/repository/NavigationRepository.kt | 45 +++++++--- 2 files changed, 86 insertions(+), 46 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/opds/repository/MangaRepository.kt b/server/src/main/kotlin/suwayomi/tachidesk/opds/repository/MangaRepository.kt index 2e8245fcb..a5ae49560 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/opds/repository/MangaRepository.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/opds/repository/MangaRepository.kt @@ -11,6 +11,7 @@ import org.jetbrains.exposed.v1.core.and import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.core.greater import org.jetbrains.exposed.v1.core.inList +import org.jetbrains.exposed.v1.core.inSubQuery import org.jetbrains.exposed.v1.core.intLiteral import org.jetbrains.exposed.v1.core.like import org.jetbrains.exposed.v1.core.lowerCase @@ -75,13 +76,28 @@ fun Query.applyOpdsMangaFilter( } if (excludeField != "filter") { criteria.filter?.let { filterVal -> - val unreadCountExpr = Case().When(ChapterTable.isRead eq false, intLiteral(1)).Else(intLiteral(0)).sum() - val downloadedCountExpr = Case().When(ChapterTable.isDownloaded eq true, intLiteral(1)).Else(intLiteral(0)).sum() when (filterVal) { - "unread" -> having { unreadCountExpr greater 0 } - "downloaded" -> having { downloadedCountExpr greater 0 } - "ongoing" -> andWhere { MangaTable.status eq MangaStatus.ONGOING.value } - "completed" -> andWhere { MangaTable.status eq MangaStatus.COMPLETED.value } + "unread" -> { + andWhere { + MangaTable.id inSubQuery + ChapterTable.select(ChapterTable.manga).where { ChapterTable.isRead eq false } + } + } + + "downloaded" -> { + andWhere { + MangaTable.id inSubQuery + ChapterTable.select(ChapterTable.manga).where { ChapterTable.isDownloaded eq true } + } + } + + "ongoing" -> { + andWhere { MangaTable.status eq MangaStatus.ONGOING.value } + } + + "completed" -> { + andWhere { MangaTable.status eq MangaStatus.COMPLETED.value } + } } } } @@ -133,11 +149,17 @@ object MangaRepository { val unreadCount = unreadCountExpr.alias("unread_count") // Base query with necessary joins for filtering and sorting - val query = + var baseJoin = MangaTable .join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id) .join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga) - .join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) + + if (criteria.categoryId != null) { + baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) + } + + val query = + baseJoin .select(MangaTable.columns + SourceTable.lang + SourceTable.name + unreadCount) .where { MangaTable.inLibrary eq true } @@ -301,7 +323,6 @@ object MangaRepository { * Applies sorting and filtering logic to a manga library query. * @param query The Exposed SQL query to modify. * @param sort The sorting parameter. - * @param filter The filtering parameter. */ private fun applyMangaLibrarySort( query: Query, @@ -330,36 +351,38 @@ object MangaRepository { */ fun getLibraryFilterCounts(activeFilters: OpdsMangaFilter): Map = transaction { - val unreadCountExpr = Case().When(ChapterTable.isRead eq false, intLiteral(1)).Else(intLiteral(0)).sum() - val downloadedCountExpr = Case().When(ChapterTable.isDownloaded eq true, intLiteral(1)).Else(intLiteral(0)).sum() + var baseJoin = + MangaTable + .join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id) + + if (activeFilters.categoryId != null) { + baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) + } val baseQuery = - MangaTable - .join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga) - .join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id) - .join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) + baseJoin .select(MangaTable.id) .where { MangaTable.inLibrary eq true } + .withDistinct() baseQuery.applyOpdsMangaFilter(activeFilters, excludeField = "filter") - baseQuery.groupBy(MangaTable.id) - val unreadCount = baseQuery.copy().having { unreadCountExpr greater 0 }.count() - val downloadedCount = baseQuery.copy().having { downloadedCountExpr greater 0 }.count() - - val statusBaseQuery = - MangaTable - .join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id) - .join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) - .join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga) - .select(MangaTable.id) - .where { MangaTable.inLibrary eq true } - - statusBaseQuery.applyOpdsMangaFilter(activeFilters, excludeField = "filter") - statusBaseQuery.groupBy(MangaTable.id) - - val ongoingCount = statusBaseQuery.copy().andWhere { MangaTable.status eq MangaStatus.ONGOING.value }.count() - val completedCount = statusBaseQuery.copy().andWhere { MangaTable.status eq MangaStatus.COMPLETED.value }.count() + val unreadCount = + baseQuery + .copy() + .andWhere { + MangaTable.id inSubQuery + ChapterTable.select(ChapterTable.manga).where { ChapterTable.isRead eq false } + }.count() + val downloadedCount = + baseQuery + .copy() + .andWhere { + MangaTable.id inSubQuery + ChapterTable.select(ChapterTable.manga).where { ChapterTable.isDownloaded eq true } + }.count() + val ongoingCount = baseQuery.copy().andWhere { MangaTable.status eq MangaStatus.ONGOING.value }.count() + val completedCount = baseQuery.copy().andWhere { MangaTable.status eq MangaStatus.COMPLETED.value }.count() mapOf( "unread" to unreadCount, diff --git a/server/src/main/kotlin/suwayomi/tachidesk/opds/repository/NavigationRepository.kt b/server/src/main/kotlin/suwayomi/tachidesk/opds/repository/NavigationRepository.kt index 4bed7a9a9..97eb08f73 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/opds/repository/NavigationRepository.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/opds/repository/NavigationRepository.kt @@ -13,7 +13,6 @@ import suwayomi.tachidesk.i18n.MR import suwayomi.tachidesk.manga.impl.extension.Extension import suwayomi.tachidesk.manga.model.table.CategoryMangaTable import suwayomi.tachidesk.manga.model.table.CategoryTable -import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.ExtensionTable import suwayomi.tachidesk.manga.model.table.MangaStatus import suwayomi.tachidesk.manga.model.table.MangaTable @@ -167,12 +166,17 @@ object NavigationRepository { transaction { val mangaCount = MangaTable.id.countDistinct().alias("manga_count") - val query = + var baseJoin = SourceTable .join(MangaTable, JoinType.INNER, SourceTable.id, MangaTable.sourceReference) .join(ExtensionTable, JoinType.LEFT, onColumn = SourceTable.extension, otherColumn = ExtensionTable.id) - .join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) - .join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga) + + if (activeFilters.categoryId != null) { + baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) + } + + val query = + baseJoin .select(SourceTable.id, SourceTable.name, SourceTable.lang, ExtensionTable.apkName, mangaCount) .where { MangaTable.inLibrary eq true } @@ -228,7 +232,6 @@ object NavigationRepository { .join(CategoryMangaTable, JoinType.INNER, CategoryTable.id, CategoryMangaTable.category) .join(MangaTable, JoinType.INNER, CategoryMangaTable.manga, MangaTable.id) .join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id) - .join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga) .select(CategoryTable.id, CategoryTable.name, mangaCount) .where { MangaTable.inLibrary eq true } @@ -263,11 +266,15 @@ object NavigationRepository { activeFilters: OpdsMangaFilter = OpdsMangaFilter(), ): Pair, Long> = transaction { - val query = + var baseJoin = MangaTable .join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id) - .join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) - .join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga) + if (activeFilters.categoryId != null) { + baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) + } + + val query = + baseJoin .select(MangaTable.genre) .where { MangaTable.inLibrary eq true } @@ -322,11 +329,16 @@ object NavigationRepository { val statusCounts = transaction { val countExpr = MangaTable.id.countDistinct().alias("manga_count") - val query = + + var baseJoin = MangaTable .join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id) - .join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) - .join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga) + if (activeFilters.categoryId != null) { + baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) + } + + val query = + baseJoin .select(MangaTable.status, countExpr) .where { MangaTable.inLibrary eq true } @@ -369,11 +381,16 @@ object NavigationRepository { ): Pair, Long> = transaction { val mangaCount = MangaTable.id.countDistinct().alias("manga_count") - val query = + + var baseJoin = SourceTable .join(MangaTable, JoinType.INNER, SourceTable.id, MangaTable.sourceReference) - .join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) - .join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga) + if (activeFilters.categoryId != null) { + baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) + } + + val query = + baseJoin .select(SourceTable.lang, mangaCount) .where { MangaTable.inLibrary eq true } From 40a21fabca6fdf640986e91fbdc43b3e3dd4316b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:39:37 -0400 Subject: [PATCH 4/6] Update dex2jar to v2.4.37 (#2122) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e62dd34da..afef2f1d4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ javalin = "7.2.2" jte = "3.2.4" jackson = "3.2.0" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency` exposed = "1.2.0" -dex2jar = "2.4.36" +dex2jar = "2.4.37" polyglot = "25.0.3" settings = "1.3.0" twelvemonkeys = "3.13.1" From be5e3f022e6950d53d5614f9ce4fc90f39c6007f Mon Sep 17 00:00:00 2001 From: Zeedif Date: Wed, 17 Jun 2026 20:41:31 -0600 Subject: [PATCH 5/6] feat(download): improve chapter download filenames (#2100) * feat(download): improve chapter download filenames * refactor(download): use SafePath helper for filename sanitization --- .../manga/impl/ChapterDownloadHelper.kt | 42 +++++++++++++++++-- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/ChapterDownloadHelper.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/ChapterDownloadHelper.kt index 9d203557f..ba1255ee4 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/ChapterDownloadHelper.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/ChapterDownloadHelper.kt @@ -16,6 +16,7 @@ import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.toDataClass import suwayomi.tachidesk.server.serverConfig +import xyz.nulldev.androidcompat.util.SafePath import java.io.File import java.io.InputStream @@ -76,13 +77,46 @@ object ChapterDownloadHelper { .select(ChapterTable.columns + MangaTable.columns) .where { ChapterTable.id eq chapterId } .firstOrNull() ?: throw IllegalArgumentException("ChapterId $chapterId not found") + val chapter = ChapterTable.toDataClass(row) - val mangaTitle = row[MangaTable.title] + val mangaTitle = row[MangaTable.title].trim() - val scanlatorPart = chapter.scanlator?.let { "[$it] " } ?: "" - val fileName = "$mangaTitle - $scanlatorPart${chapter.name}.cbz" + val scanlatorName = chapter.scanlator?.trim()?.takeIf { it.isNotEmpty() } + val chapterName = chapter.name.trim().takeIf { it.isNotEmpty() } - Pair(chapter, fileName) + val fileName = + buildString { + append(mangaTitle) + append(" - ") + + if (chapterName != null) { + append(chapterName) + } else if (chapter.chapterNumber >= 0f) { + // chapterNumber is stored as Float, drop .0 for whole numbers. + val formatNumber = + if (chapter.chapterNumber % 1 == 0f) { + chapter.chapterNumber.toInt().toString() + } else { + chapter.chapterNumber.toString() + } + append("#$formatNumber") + } else { + // Fallback when neither name nor valid chapter number exists + append("#${chapter.index}") + } + + if (scanlatorName != null) { + append(" [") + append(scanlatorName) + append("]") + } + append(".cbz") + } + + // Sanitize filename for OS compatibility + val safeFileName = SafePath.buildValidFilename(fileName) + + Pair(chapter, safeFileName) } fun getCbzForDownload( From c8f5d83e9cca295a5a00f792de87354131e40052 Mon Sep 17 00:00:00 2001 From: herowinb Date: Thu, 18 Jun 2026 09:46:21 +0700 Subject: [PATCH 6/6] add reportSyncEvent for SyncYomi service (#2110) * add reportSyncEvent * Update SyncYomiSyncService.kt --- .../global/impl/sync/SyncYomiSyncService.kt | 116 +++++++++++++++--- 1 file changed, 98 insertions(+), 18 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/global/impl/sync/SyncYomiSyncService.kt b/server/src/main/kotlin/suwayomi/tachidesk/global/impl/sync/SyncYomiSyncService.kt index 6a33efeea..901e0c8af 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/global/impl/sync/SyncYomiSyncService.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/global/impl/sync/SyncYomiSyncService.kt @@ -4,11 +4,15 @@ import android.app.Application import android.content.Context import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.NetworkHelper +import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.PUT import eu.kanade.tachiyomi.network.await import io.github.oshai.kotlinlogging.KotlinLogging import io.javalin.http.HttpStatus +import kotlinx.coroutines.CancellationException +import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json import kotlinx.serialization.protobuf.ProtoBuf import okhttp3.Headers import okhttp3.MediaType.Companion.toMediaType @@ -36,31 +40,66 @@ object SyncYomiSyncService { message: String?, ) : Exception(message) + @Serializable + private data class SyncEvent( + val event: SyncEventStatus, + val device_Name: String? = null, + val message: String? = null, + ) + + @Serializable + private enum class SyncEventStatus { + SYNC_STARTED, + SYNC_SUCCESS, + SYNC_FAILED, + SYNC_ERROR, + SYNC_CANCELLED, + } + suspend fun doSync( syncData: SyncData, startDate: Instant, setSyncState: (SyncManager.SyncState) -> Unit, ): Backup? { + reportSyncEvent(SyncEventStatus.SYNC_STARTED) setSyncState(SyncManager.SyncState.Downloading(startDate)) - val (remoteData, etag) = pullSyncData() - val finalSyncData = - if (remoteData != null) { - require(etag.isNotEmpty()) { "ETag should never be empty if remote data is not null" } - logger.debug { "Try update remote data with ETag($etag)" } - setSyncState(SyncManager.SyncState.Merging(startDate)) - mergeSyncData(syncData, remoteData) - } else { - // init or overwrite remote data - logger.debug { "Try overwrite remote data with ETag($etag)" } - syncData + return try { + val (remoteData, etag) = pullSyncData() + + val finalSyncData = + if (remoteData != null) { + require(etag.isNotEmpty()) { "ETag should never be empty if remote data is not null" } + logger.debug { "Try update remote data with ETag($etag)" } + setSyncState(SyncManager.SyncState.Merging(startDate)) + mergeSyncData(syncData, remoteData) + } else { + // init or overwrite remote data + logger.debug { "Try overwrite remote data with ETag($etag)" } + syncData + } + + if (finalSyncData.backup != null) { + setSyncState(SyncManager.SyncState.Uploading(startDate)) } - if (finalSyncData.backup != null) { - setSyncState(SyncManager.SyncState.Uploading(startDate)) + val success = pushSyncData(finalSyncData, etag) + if (success) { + reportSyncEvent(SyncEventStatus.SYNC_SUCCESS) + } else { + reportSyncEvent(SyncEventStatus.SYNC_FAILED, "Failed to push sync data") + } + + finalSyncData.backup + } catch (e: Exception) { + if (e is CancellationException) { + reportSyncEvent(SyncEventStatus.SYNC_CANCELLED, e.message) + throw e + } + logger.error { "Error syncing: ${e.message}" } + reportSyncEvent(SyncEventStatus.SYNC_ERROR, e.message) + throw e } - pushSyncData(finalSyncData, etag) - return finalSyncData.backup } private suspend fun pullSyncData(): Pair { @@ -122,8 +161,8 @@ object SyncYomiSyncService { private suspend fun pushSyncData( syncData: SyncData, eTag: String, - ) { - val backup = syncData.backup ?: return + ): Boolean { + val backup = syncData.backup ?: return true val host = serverConfig.syncYomiHost.value val apiKey = serverConfig.syncYomiApiKey.value @@ -160,7 +199,7 @@ object SyncYomiSyncService { val response = client.newCall(uploadRequest).await() - if (response.isSuccessful) { + return if (response.isSuccessful) { val newETag = response.headers["ETag"] ?.takeIf { it.isNotEmpty() } ?: throw SyncYomiException("Missing ETag") @@ -169,12 +208,53 @@ object SyncYomiSyncService { .putString("last_sync_etag", newETag) .apply() logger.debug { "SyncYomi sync completed" } + true } else if (response.code == HttpStatus.PRECONDITION_FAILED.code) { // other clients updated remote data, will try next time logger.debug { "SyncYomi sync failed with 412" } + false } else { val responseBody = response.body.string() logger.error { "SyncError: $responseBody" } + false + } + } + + private suspend fun reportSyncEvent( + event: SyncEventStatus, + message: String? = null, + ) { + try { + val host = serverConfig.syncYomiHost.value + val apiKey = serverConfig.syncYomiApiKey.value + val url = "$host/api/sync/event" + + val headers = Headers.Builder().add("X-API-Token", apiKey).build() + + // Use a fixed server name. + val bodyObj = + SyncEvent( + event = event, + device_Name = "Suwayomi Server", + message = message, + ) + + val jsonBody = Json.encodeToString(SyncEvent.serializer(), bodyObj) + val requestBody = jsonBody.toRequestBody("application/json; charset=utf-8".toMediaType()) + + val request = + POST( + url = url, + headers = headers, + body = requestBody, + ) + + network.client + .newCall(request) + .await() + .close() + } catch (e: Exception) { + logger.error { "Failed to report sync event: ${e.message}" } } }