Merge branch 'master' into extensions_1.6

This commit is contained in:
Mitchell Syer
2026-06-18 15:02:54 -04:00
committed by GitHub
10 changed files with 384 additions and 85 deletions

View File

@@ -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"

View File

@@ -50,6 +50,7 @@
<!-- OPDS errors -->
<string name="opds_error_manga_not_found">Series with ID %1$d not found.</string>
<string name="opds_error_chapter_not_found">Chapter with index %1$d not found.</string>
<string name="opds_error_chapters_not_found">No chapters found or the source is unreachable on page %1$d.</string>
<!-- OPDS facets (Filters and Sorting) -->
<string name="opds_facetgroup_sort_order">Sort Order</string>

View File

@@ -0,0 +1,125 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="opds_feeds_root">Suwayomi OPDS Κατάλογος</string>
<string name="opds_feeds_manga_chapters">Κεφάλαια %1$s</string>
<string name="opds_feeds_chapter_details">%1$s | %2$s | Λεπτομέρειες</string>
<string name="opds_feeds_explore_title">Εξερεύνηση</string>
<string name="opds_feeds_explore_entry_content">Εξερεύνησε νέες σειρές από τις πηγές σου</string>
<string name="opds_feeds_history_title">Ιστορικό</string>
<string name="opds_feeds_history_entry_content">Πρόσφατα διαβασμένα κεφάλαια</string>
<string name="opds_feeds_all_series_in_library_title">Όλες οι σειρές</string>
<string name="opds_feeds_all_series_in_library_entry_content">Περιήγηση σε όλες τις σειρές της βιβλιοθήκης σου</string>
<string name="opds_feeds_library_sources_title">Πηγές</string>
<string name="opds_feeds_library_sources_entry_content">Περιήγηση σε σειρές της βιβλιοθήκης σου φιλτραρισμένες κατά πηγή</string>
<string name="opds_feeds_categories_title">Κατηγορίες</string>
<string name="opds_feeds_categories_entry_content">Περιήγηση σε σειρές οργανωμένες κατά κατηγορία</string>
<string name="opds_feeds_genres_title">Είδη</string>
<string name="opds_feeds_genres_entry_content">Περιήγηση σε σειρές κατά ετικέτες είδους</string>
<string name="opds_feeds_status_title">Κατάσταση</string>
<string name="opds_feeds_status_entry_content">Περιήγηση σε σειρές κατά κατάσταση δημοσίευσης</string>
<string name="opds_feeds_languages_title">Γλώσσες</string>
<string name="opds_feeds_languages_entry_content">Περιήγηση σε σειρές κατά γλώσσα περιεχομένου</string>
<string name="opds_feeds_library_updates_title">Ιστορικό ενημερώσεων βιβλιοθήκης</string>
<string name="opds_feeds_library_updates_entry_content">Πρόσφατα ενημερωμένα κεφάλαια από τη βιβλιοθήκη σου</string>
<string name="opds_feeds_search_results_title">Αποτελέσματα αναζήτησης</string>
<string name="opds_feeds_sources_title">Όλες οι πηγές</string>
<string name="opds_feeds_category_specific_title">Κατηγορία: %1$s</string>
<string name="opds_feeds_genre_specific_title">Είδος: %1$s</string>
<string name="opds_feeds_status_specific_title">Κατάσταση: %1$s</string>
<string name="opds_feeds_language_specific_title">Γλώσσα: %1$s</string>
<string name="opds_feeds_source_specific_title">Πηγή: %1$s</string>
<string name="opds_feeds_library_source_specific_title">Βιβλιοθήκη - Πηγή: %1$s</string>
<string name="opds_feeds_source_specific_popular_title">Πηγή: %1$s - Δημοφιλές</string>
<string name="opds_feeds_source_specific_latest_title">Πηγή: %1$s - Τελευταίο</string>
<string name="opds_search_shortname">Suwayomi OPDS Αναζήτηση</string>
<string name="opds_search_description">Αναζήτηση σειρών στον κατάλογο.</string>
<string name="opds_error_manga_not_found">Η σειρά με ID %1$d δεν βρέθηκε.</string>
<string name="opds_error_chapter_not_found">Το κεφάλαιο με δείκτη %1$d δεν βρέθηκε.</string>
<string name="opds_facetgroup_sort_order">Σειρά ταξινόμησης</string>
<string name="opds_facetgroup_filter_read_status">Φίλτρο κατά κατάσταση ανάγνωσης</string>
<string name="opds_facetgroup_filter_content">Φίλτρο περιεχομένου</string>
<string name="opds_facetgroup_filter_source">Φίλτρο κατά πηγή</string>
<string name="opds_facetgroup_filter_category">Φίλτρο κατά κατηγορία</string>
<string name="opds_facetgroup_filter_status">Φίλτρο κατά κατάσταση</string>
<string name="opds_facetgroup_filter_language">Φίλτρο κατά γλώσσα</string>
<string name="opds_facetgroup_filter_genre">Φίλτρο κατά είδος</string>
<string name="opds_facet_sort_oldest_first">Παλαιότερα πρώτα</string>
<string name="opds_facet_sort_newest_first">Νεότερα πρώτα</string>
<string name="opds_facet_sort_date_asc">Ημερομηνία αύξουσα</string>
<string name="opds_facet_sort_date_desc">Ημερομηνία φθίνουσα</string>
<string name="opds_facet_sort_popular">Δημοφιλές</string>
<string name="opds_facet_sort_latest">Τελευταίο</string>
<string name="opds_facet_sort_alpha_asc">Αλφαβητικά Α</string>
<string name="opds_facet_sort_alpha_desc">Αλφαβητικά Ω-Α</string>
<string name="opds_facet_sort_last_read_desc">Τελευταία ανάγνωση</string>
<string name="opds_facet_sort_latest_chapter_desc">Τελευταίο κεφάλαιο</string>
<string name="opds_facet_sort_date_added_desc">Ημερομηνία προσθήκης</string>
<string name="opds_facet_sort_unread_desc">Αδιάβαστα κεφάλαια</string>
<string name="opds_facet_filter_all">Όλα</string>
<string name="opds_facet_filter_all_chapters">Όλα τα κεφάλαια</string>
<string name="opds_facet_filter_unread_only">Αδιάβαστα</string>
<string name="opds_facet_filter_read_only">Διαβασμένα</string>
<string name="opds_facet_filter_downloaded">Ληφθέντα</string>
<string name="opds_facet_filter_ongoing">Σε εξέλιξη</string>
<string name="opds_facet_filter_completed">Ολοκληρωμένα</string>
<string name="opds_facet_all_sources">Όλες οι πηγές</string>
<string name="opds_facet_all_categories">Όλες οι κατηγορίες</string>
<string name="opds_facet_all_statuses">Όλες οι καταστάσεις</string>
<string name="opds_facet_all_languages">Όλες οι γλώσσες</string>
<string name="opds_facet_all_genres">Όλα τα είδη</string>
<string name="opds_linktitle_catalog_root">Ρίζα καταλόγου</string>
<string name="opds_linktitle_search_catalog">Αναζήτηση καταλόγου</string>
<string name="opds_linktitle_first_page">Πρώτη σελίδα</string>
<string name="opds_linktitle_previous_page">Προηγούμενη σελίδα</string>
<string name="opds_linktitle_next_page">Επόμενη σελίδα</string>
<string name="opds_linktitle_last_page">Τελευταία σελίδα</string>
<string name="opds_linktitle_self_feed">Τρέχουσα ροή</string>
<string name="opds_linktitle_view_on_web">Προβολή στο Web</string>
<string name="opds_linktitle_stream_pages_start">Διάβασε Online</string>
<string name="opds_linktitle_stream_pages_continue">Συνέχισε να διαβάζεις Online</string>
<string name="opds_linktitle_stream_pages_start_local">Διάβασε Online (Τοπική πρόοδος)</string>
<string name="opds_linktitle_stream_pages_continue_local">Συνέχισε να διαβάζεις Online (Τοπική πρόοδος)</string>
<string name="opds_linktitle_stream_pages_start_remote">Διάβασε Online (Συγχρονισμένο από %1$s)</string>
<string name="opds_linktitle_stream_pages_continue_remote">Συνέχισε να διαβάζεις Online (Συγχρονισμένο από %1$s)</string>
<string name="opds_linktitle_download_cbz">Λήψη CBZ</string>
<string name="opds_linktitle_chapter_cover">Εξώφυλλο κεφαλαίου</string>
<string name="opds_linktitle_view_chapter_details">Λεπτομέρειες κεφαλαίου &amp; Σελίδες</string>
<string name="opds_chapter_status_read"></string>
<string name="opds_chapter_status_in_progress"></string>
<string name="opds_chapter_status_unread"></string>
<string name="opds_chapter_status_downloaded">⬇️</string>
<string name="opds_chapter_status_synced">🌐</string>
<string name="opds_chapter_details_base">Σειρά: %1$s | %2$s</string>
<string name="opds_chapter_details_scanlator">| Από %1$s</string>
<string name="opds_chapter_details_progress">| Πρόοδος: %1$d από %2$d</string>
<string name="opds_manga_summary_status">Κατάσταση: %1$s</string>
<string name="opds_manga_summary_source">Πηγή: %1$s</string>
<string name="opds_manga_summary_language">Γλώσσα: %1$s</string>
<string name="manga_status_ongoing">Σε εξέλιξη</string>
<string name="manga_status_completed">Ολοκληρωμένο</string>
<string name="manga_status_licensed">Υπό άδεια</string>
<string name="manga_status_publishing_finished">Δημοσίευση ολοκληρωμένη</string>
<string name="manga_status_cancelled">Ακυρωμένο</string>
<string name="manga_status_on_hiatus">Σε παύση</string>
<string name="manga_status_unknown">Άγνωστο</string>
<string name="label_error">Σφάλμα</string>
<string name="label_version">Έκδοση <xliff:g id="version" example="v2.0.1833">%1$s</xliff:g></string>
<string name="label_close">Κλείσε</string>
<string name="webview_label_title">Suwayomi WebView</string>
<string name="webview_label_disconnected">Αποσυνδεδεμένο, κάνε ανανέωση</string>
<string name="webview_label_reversescroll">Αντίστροφη κύλιση</string>
<string name="webview_label_bindingshint">Σημείωση: Όταν η εστίαση είναι στο τμήμα WebView, καμία συντόμευση πληκτρολογίου, συμπεριλαμβανομένης της ανανέωσης, δεν θα αντιμετωπίζεται από το πρόγραμμα περιήγησης.</string>
<string name="webview_label_init">Αρχικοποίηση... Παρακαλώ περίμενε</string>
<string name="webview_label_getstarted">Εισήγαγε μια διεύθυνση URL για να ξεκινήσεις</string>
<string name="webview_label_loading">Φόρτωση σελίδας...</string>
<string name="webview_label_copy">Αντιγραφή στο πρόχειρο</string>
<string name="webview_label_copy_description">Η αυτόματη αντιγραφή στο πρόχειρο απέτυχε, χρησιμοποίησε το παρακάτω πεδίο για να αντιγράψεις χειροκίνητα την τιμή.</string>
<string name="webview_label_login_required">Η διαμόρφωσή σου απαιτεί σύνδεση. Εισήγαγε όνομα χρήστη και κωδικό.</string>
<string name="webview_placeholder_url">Εισήγαγε URL...</string>
<string name="login_label_title">Suwayomi Σύνδεση</string>
<string name="login_label_username">Όνομα χρήστη</string>
<string name="login_label_password">Κωδικός</string>
<string name="login_label_login">Σύνδεση</string>
<string name="login_placeholder_username">Πληκτρολόγησε όνομα χρήστη...</string>
<string name="login_placeholder_password">Μυστικό...</string>
</resources>

View File

@@ -4,11 +4,15 @@ import android.app.Application
import android.content.Context
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.PUT
import eu.kanade.tachiyomi.network.await
import io.github.oshai.kotlinlogging.KotlinLogging
import io.javalin.http.HttpStatus
import kotlinx.coroutines.CancellationException
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import kotlinx.serialization.protobuf.ProtoBuf
import okhttp3.Headers
import okhttp3.MediaType.Companion.toMediaType
@@ -36,31 +40,66 @@ object SyncYomiSyncService {
message: String?,
) : Exception(message)
@Serializable
private data class SyncEvent(
val event: SyncEventStatus,
val device_Name: String? = null,
val message: String? = null,
)
@Serializable
private enum class SyncEventStatus {
SYNC_STARTED,
SYNC_SUCCESS,
SYNC_FAILED,
SYNC_ERROR,
SYNC_CANCELLED,
}
suspend fun doSync(
syncData: SyncData,
startDate: Instant,
setSyncState: (SyncManager.SyncState) -> Unit,
): Backup? {
reportSyncEvent(SyncEventStatus.SYNC_STARTED)
setSyncState(SyncManager.SyncState.Downloading(startDate))
val (remoteData, etag) = pullSyncData()
val finalSyncData =
if (remoteData != null) {
require(etag.isNotEmpty()) { "ETag should never be empty if remote data is not null" }
logger.debug { "Try update remote data with ETag($etag)" }
setSyncState(SyncManager.SyncState.Merging(startDate))
mergeSyncData(syncData, remoteData)
} else {
// init or overwrite remote data
logger.debug { "Try overwrite remote data with ETag($etag)" }
syncData
return try {
val (remoteData, etag) = pullSyncData()
val finalSyncData =
if (remoteData != null) {
require(etag.isNotEmpty()) { "ETag should never be empty if remote data is not null" }
logger.debug { "Try update remote data with ETag($etag)" }
setSyncState(SyncManager.SyncState.Merging(startDate))
mergeSyncData(syncData, remoteData)
} else {
// init or overwrite remote data
logger.debug { "Try overwrite remote data with ETag($etag)" }
syncData
}
if (finalSyncData.backup != null) {
setSyncState(SyncManager.SyncState.Uploading(startDate))
}
if (finalSyncData.backup != null) {
setSyncState(SyncManager.SyncState.Uploading(startDate))
val success = pushSyncData(finalSyncData, etag)
if (success) {
reportSyncEvent(SyncEventStatus.SYNC_SUCCESS)
} else {
reportSyncEvent(SyncEventStatus.SYNC_FAILED, "Failed to push sync data")
}
finalSyncData.backup
} catch (e: Exception) {
if (e is CancellationException) {
reportSyncEvent(SyncEventStatus.SYNC_CANCELLED, e.message)
throw e
}
logger.error { "Error syncing: ${e.message}" }
reportSyncEvent(SyncEventStatus.SYNC_ERROR, e.message)
throw e
}
pushSyncData(finalSyncData, etag)
return finalSyncData.backup
}
private suspend fun pullSyncData(): Pair<SyncData?, String> {
@@ -122,8 +161,8 @@ object SyncYomiSyncService {
private suspend fun pushSyncData(
syncData: SyncData,
eTag: String,
) {
val backup = syncData.backup ?: return
): Boolean {
val backup = syncData.backup ?: return true
val host = serverConfig.syncYomiHost.value
val apiKey = serverConfig.syncYomiApiKey.value
@@ -160,7 +199,7 @@ object SyncYomiSyncService {
val response = client.newCall(uploadRequest).await()
if (response.isSuccessful) {
return if (response.isSuccessful) {
val newETag =
response.headers["ETag"]
?.takeIf { it.isNotEmpty() } ?: throw SyncYomiException("Missing ETag")
@@ -169,12 +208,53 @@ object SyncYomiSyncService {
.putString("last_sync_etag", newETag)
.apply()
logger.debug { "SyncYomi sync completed" }
true
} else if (response.code == HttpStatus.PRECONDITION_FAILED.code) {
// other clients updated remote data, will try next time
logger.debug { "SyncYomi sync failed with 412" }
false
} else {
val responseBody = response.body.string()
logger.error { "SyncError: $responseBody" }
false
}
}
private suspend fun reportSyncEvent(
event: SyncEventStatus,
message: String? = null,
) {
try {
val host = serverConfig.syncYomiHost.value
val apiKey = serverConfig.syncYomiApiKey.value
val url = "$host/api/sync/event"
val headers = Headers.Builder().add("X-API-Token", apiKey).build()
// Use a fixed server name.
val bodyObj =
SyncEvent(
event = event,
device_Name = "Suwayomi Server",
message = message,
)
val jsonBody = Json.encodeToString(SyncEvent.serializer(), bodyObj)
val requestBody = jsonBody.toRequestBody("application/json; charset=utf-8".toMediaType())
val request =
POST(
url = url,
headers = headers,
body = requestBody,
)
network.client
.newCall(request)
.await()
.close()
} catch (e: Exception) {
logger.error { "Failed to report sync event: ${e.message}" }
}
}

View File

@@ -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(

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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<String, Long> =
transaction {
val unreadCountExpr = Case().When(ChapterTable.isRead eq false, intLiteral(1)).Else(intLiteral(0)).sum()
val downloadedCountExpr = Case().When(ChapterTable.isDownloaded eq true, intLiteral(1)).Else(intLiteral(0)).sum()
var baseJoin =
MangaTable
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
if (activeFilters.categoryId != null) {
baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
}
val baseQuery =
MangaTable
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
baseJoin
.select(MangaTable.id)
.where { MangaTable.inLibrary eq true }
.withDistinct()
baseQuery.applyOpdsMangaFilter(activeFilters, excludeField = "filter")
baseQuery.groupBy(MangaTable.id)
val unreadCount = baseQuery.copy().having { unreadCountExpr greater 0 }.count()
val downloadedCount = baseQuery.copy().having { downloadedCountExpr greater 0 }.count()
val statusBaseQuery =
MangaTable
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
.select(MangaTable.id)
.where { MangaTable.inLibrary eq true }
statusBaseQuery.applyOpdsMangaFilter(activeFilters, excludeField = "filter")
statusBaseQuery.groupBy(MangaTable.id)
val ongoingCount = statusBaseQuery.copy().andWhere { MangaTable.status eq MangaStatus.ONGOING.value }.count()
val completedCount = statusBaseQuery.copy().andWhere { MangaTable.status eq MangaStatus.COMPLETED.value }.count()
val unreadCount =
baseQuery
.copy()
.andWhere {
MangaTable.id inSubQuery
ChapterTable.select(ChapterTable.manga).where { ChapterTable.isRead eq false }
}.count()
val downloadedCount =
baseQuery
.copy()
.andWhere {
MangaTable.id inSubQuery
ChapterTable.select(ChapterTable.manga).where { ChapterTable.isDownloaded eq true }
}.count()
val ongoingCount = baseQuery.copy().andWhere { MangaTable.status eq MangaStatus.ONGOING.value }.count()
val completedCount = baseQuery.copy().andWhere { MangaTable.status eq MangaStatus.COMPLETED.value }.count()
mapOf(
"unread" to unreadCount,

View File

@@ -12,7 +12,6 @@ import suwayomi.tachidesk.i18n.MR
import suwayomi.tachidesk.manga.impl.extension.Extension
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
import suwayomi.tachidesk.manga.model.table.CategoryTable
import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.manga.model.table.ExtensionTable
import suwayomi.tachidesk.manga.model.table.MangaStatus
import suwayomi.tachidesk.manga.model.table.MangaTable
@@ -166,12 +165,17 @@ object NavigationRepository {
transaction {
val mangaCount = MangaTable.id.countDistinct().alias("manga_count")
val query =
var baseJoin =
SourceTable
.join(MangaTable, JoinType.INNER, SourceTable.id, MangaTable.sourceReference)
.join(ExtensionTable, JoinType.LEFT, onColumn = SourceTable.extension, otherColumn = ExtensionTable.id)
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
if (activeFilters.categoryId != null) {
baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
}
val query =
baseJoin
.select(SourceTable.id, SourceTable.name, SourceTable.lang, ExtensionTable.pkgName, mangaCount)
.where { MangaTable.inLibrary eq true }
@@ -227,7 +231,6 @@ object NavigationRepository {
.join(CategoryMangaTable, JoinType.INNER, CategoryTable.id, CategoryMangaTable.category)
.join(MangaTable, JoinType.INNER, CategoryMangaTable.manga, MangaTable.id)
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
.select(CategoryTable.id, CategoryTable.name, mangaCount)
.where { MangaTable.inLibrary eq true }
@@ -262,11 +265,15 @@ object NavigationRepository {
activeFilters: OpdsMangaFilter = OpdsMangaFilter(),
): Pair<List<OpdsGenreNavEntry>, Long> =
transaction {
val query =
var baseJoin =
MangaTable
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
if (activeFilters.categoryId != null) {
baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
}
val query =
baseJoin
.select(MangaTable.genre)
.where { MangaTable.inLibrary eq true }
@@ -321,11 +328,16 @@ object NavigationRepository {
val statusCounts =
transaction {
val countExpr = MangaTable.id.countDistinct().alias("manga_count")
val query =
var baseJoin =
MangaTable
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
if (activeFilters.categoryId != null) {
baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
}
val query =
baseJoin
.select(MangaTable.status, countExpr)
.where { MangaTable.inLibrary eq true }
@@ -368,11 +380,16 @@ object NavigationRepository {
): Pair<List<OpdsLanguageNavEntry>, Long> =
transaction {
val mangaCount = MangaTable.id.countDistinct().alias("manga_count")
val query =
var baseJoin =
SourceTable
.join(MangaTable, JoinType.INNER, SourceTable.id, MangaTable.sourceReference)
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
if (activeFilters.categoryId != null) {
baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
}
val query =
baseJoin
.select(SourceTable.lang, mangaCount)
.where { MangaTable.inLibrary eq true }