diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 916ed647c..4b842bfd4 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"
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/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 Σύνδεση
+ Όνομα χρήστη
+ Κωδικός
+ Σύνδεση
+ Πληκτρολόγησε όνομα χρήστη...
+ Μυστικό...
+
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}" }
}
}
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(
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 68815dad0..3e5830528 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)
}
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 8d16523ff..3a1ee62ee 100644
--- a/server/src/main/kotlin/suwayomi/tachidesk/opds/repository/NavigationRepository.kt
+++ b/server/src/main/kotlin/suwayomi/tachidesk/opds/repository/NavigationRepository.kt
@@ -12,7 +12,6 @@ import suwayomi.tachidesk.i18n.MR
import suwayomi.tachidesk.manga.impl.extension.Extension
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
import suwayomi.tachidesk.manga.model.table.CategoryTable
-import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.manga.model.table.ExtensionTable
import suwayomi.tachidesk.manga.model.table.MangaStatus
import suwayomi.tachidesk.manga.model.table.MangaTable
@@ -166,12 +165,17 @@ object NavigationRepository {
transaction {
val mangaCount = MangaTable.id.countDistinct().alias("manga_count")
- val query =
+ var baseJoin =
SourceTable
.join(MangaTable, JoinType.INNER, SourceTable.id, MangaTable.sourceReference)
.join(ExtensionTable, JoinType.LEFT, onColumn = SourceTable.extension, otherColumn = ExtensionTable.id)
- .join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
- .join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
+
+ if (activeFilters.categoryId != null) {
+ baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
+ }
+
+ val query =
+ baseJoin
.select(SourceTable.id, SourceTable.name, SourceTable.lang, ExtensionTable.pkgName, mangaCount)
.where { MangaTable.inLibrary eq true }
@@ -227,7 +231,6 @@ object NavigationRepository {
.join(CategoryMangaTable, JoinType.INNER, CategoryTable.id, CategoryMangaTable.category)
.join(MangaTable, JoinType.INNER, CategoryMangaTable.manga, MangaTable.id)
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
- .join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
.select(CategoryTable.id, CategoryTable.name, mangaCount)
.where { MangaTable.inLibrary eq true }
@@ -262,11 +265,15 @@ object NavigationRepository {
activeFilters: OpdsMangaFilter = OpdsMangaFilter(),
): Pair, Long> =
transaction {
- val query =
+ var baseJoin =
MangaTable
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
- .join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
- .join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
+ if (activeFilters.categoryId != null) {
+ baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
+ }
+
+ val query =
+ baseJoin
.select(MangaTable.genre)
.where { MangaTable.inLibrary eq true }
@@ -321,11 +328,16 @@ object NavigationRepository {
val statusCounts =
transaction {
val countExpr = MangaTable.id.countDistinct().alias("manga_count")
- val query =
+
+ var baseJoin =
MangaTable
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
- .join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
- .join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
+ if (activeFilters.categoryId != null) {
+ baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
+ }
+
+ val query =
+ baseJoin
.select(MangaTable.status, countExpr)
.where { MangaTable.inLibrary eq true }
@@ -368,11 +380,16 @@ object NavigationRepository {
): Pair, 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 }