mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-18 18:23:33 -05:00
Implement SyncYomi (#1813)
* Implement SyncYomi
* Add ability to select what to sync
* Properly fix default category bug
* Add periodic sync
* Add PostgreSQL support
* Deschedule previous task
* Check if SyncYomi is enabled in syncData function
* Don't allow multiple syncs at the same time
* Convert SyncYomiSyncService to object
* Make startSync non-suspend
* Return a result from startSync
* Sync before library update
* Improvements
* Use NetworkHelper client
* Lint
* Use measureTime
* Database improvements
- Move entire sync operation into a single transaction
- Stop loading all manga to memory
* Revert "Database improvements"
This reverts commit bee8d214c3.
* Actual database improvements
* Remove runBlocking
* Remove title check
* Update updateNonFavorites function
* Update timeout code
* Improve PostgreSQL query
* Create lastSyncState variable
* Create lastSyncStatus query
* Convert lastSyncState to StateFlow
* Create lastSyncStatusChange subscription
* Replace backupRestoreStatus with backupRestoreId
* Add startDate and endDate
* Add logs for sync start and end
* Handle all errors in syncData
* Change category restore function to match Mihon's behavior
* Fix comment
* Remove duplicate BackupMangaHandler.backup call
* Remove duplicated log
* Rename subscription to syncStatusChanged
* Use same flags for restoring
* Update syncInterval config to use DurationSetting
* Update sync scheduling logic
* Reorder conditions to reduce database calls
* Prevent deleted ghost chapters from reappearing during sync
jobobby04/TachiyomiSY#1575
* Improve sync merging categories
jobobby04/TachiyomiSY#1559
* Make columns not null
* Improve H2 triggers
* Add documentation
This commit is contained in:
@@ -85,6 +85,12 @@ object CategoryManga {
|
||||
}
|
||||
}
|
||||
|
||||
fun removeMangaFromAllCategories(mangaId: Int) {
|
||||
transaction {
|
||||
CategoryMangaTable.deleteWhere { CategoryMangaTable.manga eq mangaId }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* list of mangas that belong to a category
|
||||
*/
|
||||
|
||||
@@ -129,6 +129,8 @@ object Chapter {
|
||||
downloaded = dbChapter[ChapterTable.isDownloaded],
|
||||
pageCount = dbChapter[ChapterTable.pageCount],
|
||||
chapterCount = chapterList.size,
|
||||
lastModifiedAt = dbChapter[ChapterTable.lastModifiedAt],
|
||||
version = dbChapter[ChapterTable.version],
|
||||
meta = chapterMetas.getValue(dbChapter[ChapterTable.id].value),
|
||||
)
|
||||
}
|
||||
@@ -279,6 +281,8 @@ object Chapter {
|
||||
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
|
||||
@@ -322,6 +326,8 @@ object Chapter {
|
||||
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
|
||||
|
||||
|
||||
@@ -99,6 +99,8 @@ object Manga {
|
||||
updateStrategy = UpdateStrategy.valueOf(mangaEntry[MangaTable.updateStrategy]),
|
||||
freshData = true,
|
||||
trackers = Track.getTrackRecordsByMangaId(mangaId),
|
||||
lastModifiedAt = mangaEntry[MangaTable.lastModifiedAt],
|
||||
version = mangaEntry[MangaTable.version],
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -246,6 +248,8 @@ object Manga {
|
||||
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> =
|
||||
|
||||
@@ -21,6 +21,9 @@ import kotlinx.coroutines.sync.withLock
|
||||
import okio.buffer
|
||||
import okio.gzip
|
||||
import okio.source
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import suwayomi.tachidesk.graphql.types.toStatus
|
||||
import suwayomi.tachidesk.manga.impl.backup.BackupFlags
|
||||
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator.ValidationResult
|
||||
@@ -31,6 +34,8 @@ import suwayomi.tachidesk.manga.impl.backup.proto.handlers.BackupMangaHandler
|
||||
import suwayomi.tachidesk.manga.impl.backup.proto.handlers.BackupSettingsHandler
|
||||
import suwayomi.tachidesk.manga.impl.backup.proto.handlers.BackupSourceHandler
|
||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
import java.io.InputStream
|
||||
import java.util.Date
|
||||
import java.util.Timer
|
||||
@@ -109,6 +114,7 @@ object ProtoBackupImport : ProtoBackupBase() {
|
||||
fun restore(
|
||||
sourceStream: InputStream,
|
||||
flags: BackupFlags,
|
||||
isSync: Boolean = false,
|
||||
): String {
|
||||
val restoreId = System.currentTimeMillis().toString()
|
||||
|
||||
@@ -117,7 +123,7 @@ object ProtoBackupImport : ProtoBackupBase() {
|
||||
updateRestoreState(restoreId, BackupRestoreState.Idle)
|
||||
|
||||
GlobalScope.launch {
|
||||
restoreLegacy(sourceStream, restoreId, flags)
|
||||
restoreLegacy(sourceStream, restoreId, flags, isSync)
|
||||
}
|
||||
|
||||
return restoreId
|
||||
@@ -127,11 +133,12 @@ object ProtoBackupImport : ProtoBackupBase() {
|
||||
sourceStream: InputStream,
|
||||
restoreId: String = "legacy",
|
||||
flags: BackupFlags = BackupFlags.DEFAULT,
|
||||
isSync: Boolean = false,
|
||||
): ValidationResult =
|
||||
backupMutex.withLock {
|
||||
try {
|
||||
logger.info { "restore($restoreId): restoring..." }
|
||||
performRestore(restoreId, sourceStream, flags)
|
||||
performRestore(restoreId, sourceStream, flags, isSync)
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "restore($restoreId): failed due to" }
|
||||
|
||||
@@ -152,12 +159,14 @@ object ProtoBackupImport : ProtoBackupBase() {
|
||||
id: String,
|
||||
sourceStream: InputStream,
|
||||
flags: BackupFlags,
|
||||
isSync: Boolean,
|
||||
): ValidationResult {
|
||||
val backupString =
|
||||
sourceStream
|
||||
.source()
|
||||
.gzip()
|
||||
.buffer()
|
||||
.run {
|
||||
if (!isSync) gzip() else this
|
||||
}.buffer()
|
||||
.use { it.readByteArray() }
|
||||
val backup = parser.decodeFromByteArray(Backup.serializer(), backupString)
|
||||
|
||||
@@ -235,6 +244,17 @@ object ProtoBackupImport : ProtoBackupBase() {
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
if (isSync) {
|
||||
transaction {
|
||||
MangaTable.update({ MangaTable.isSyncing eq true }) {
|
||||
it[isSyncing] = false
|
||||
}
|
||||
ChapterTable.update({ ChapterTable.isSyncing eq true }) {
|
||||
it[isSyncing] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateRestoreState(id, BackupRestoreState.Success)
|
||||
|
||||
return validationResult
|
||||
|
||||
@@ -8,7 +8,11 @@ package suwayomi.tachidesk.manga.impl.backup.proto.handlers
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.insertAndGetId
|
||||
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.Category
|
||||
import suwayomi.tachidesk.manga.impl.Category.modifyCategoriesMetas
|
||||
import suwayomi.tachidesk.manga.impl.backup.BackupFlags
|
||||
@@ -38,6 +42,9 @@ object BackupCategoryHandler {
|
||||
it.name,
|
||||
it.order,
|
||||
0, // not supported in Tachidesk
|
||||
it.version,
|
||||
it.uid,
|
||||
it.lastModifiedAt,
|
||||
).apply {
|
||||
this.meta = categoryToMeta[it.id] ?: emptyMap()
|
||||
}
|
||||
@@ -45,7 +52,56 @@ object BackupCategoryHandler {
|
||||
}
|
||||
|
||||
fun restore(backupCategories: List<BackupCategory>): Map<Int, Int> {
|
||||
val categoryIds = Category.createCategories(backupCategories.map { it.name })
|
||||
val dbCategories = Category.getCategoryList()
|
||||
val dbCategoriesByName = dbCategories.associateBy { it.name }
|
||||
val dbCategoriesByUid = dbCategories.associateBy { it.uid }
|
||||
|
||||
var nextOrder = dbCategories.maxOfOrNull { it.order }?.plus(1) ?: 0
|
||||
|
||||
val categoryIds =
|
||||
transaction {
|
||||
backupCategories
|
||||
.map { backupCategory ->
|
||||
var dbCategory =
|
||||
if (backupCategory.uid != 0L) {
|
||||
dbCategoriesByUid[backupCategory.uid]
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (dbCategory == null) {
|
||||
dbCategory = dbCategoriesByName[backupCategory.name]
|
||||
}
|
||||
|
||||
if (dbCategory != null) {
|
||||
CategoryTable.update({ CategoryTable.id eq dbCategory.id }) {
|
||||
it[name] = backupCategory.name
|
||||
it[order] = backupCategory.order
|
||||
it[version] = backupCategory.version
|
||||
it[uid] = if (backupCategory.uid != 0L) backupCategory.uid else dbCategory.uid
|
||||
it[lastModifiedAt] = backupCategory.lastModifiedAt
|
||||
it[isSyncing] = true
|
||||
}
|
||||
return@map dbCategory.id
|
||||
}
|
||||
|
||||
val currentOrder = nextOrder++
|
||||
CategoryTable
|
||||
.insertAndGetId {
|
||||
it[name] = backupCategory.name
|
||||
it[order] = currentOrder
|
||||
it[version] = backupCategory.version
|
||||
it[uid] = backupCategory.uid
|
||||
it[lastModifiedAt] = backupCategory.lastModifiedAt
|
||||
}.value
|
||||
}
|
||||
}
|
||||
|
||||
transaction {
|
||||
CategoryTable.update({ CategoryTable.isSyncing eq true }) {
|
||||
it[isSyncing] = false
|
||||
}
|
||||
}
|
||||
|
||||
val metaEntryByCategoryId =
|
||||
categoryIds
|
||||
|
||||
@@ -75,6 +75,8 @@ object BackupMangaHandler {
|
||||
dateAdded = mangaRow[MangaTable.inLibraryAt].seconds.inWholeMilliseconds,
|
||||
viewer = 0, // not supported in Tachidesk
|
||||
updateStrategy = UpdateStrategy.valueOf(mangaRow[MangaTable.updateStrategy]),
|
||||
lastModifiedAt = mangaRow[MangaTable.lastModifiedAt],
|
||||
version = mangaRow[MangaTable.version],
|
||||
)
|
||||
|
||||
val mangaId = mangaRow[MangaTable.id].value
|
||||
@@ -110,6 +112,8 @@ object BackupMangaHandler {
|
||||
it.uploadDate,
|
||||
it.chapterNumber,
|
||||
chapters.size - it.index,
|
||||
it.lastModifiedAt,
|
||||
it.version,
|
||||
).apply {
|
||||
if (flags.includeClientData) {
|
||||
this.meta = chapterToMeta[it.id] ?: emptyMap()
|
||||
@@ -232,6 +236,9 @@ object BackupMangaHandler {
|
||||
it[inLibrary] = manga.favorite
|
||||
|
||||
it[inLibraryAt] = manga.dateAdded.milliseconds.inWholeSeconds
|
||||
|
||||
it[lastModifiedAt] = manga.lastModifiedAt
|
||||
it[version] = manga.version
|
||||
}.value
|
||||
} else {
|
||||
val dbMangaId = dbManga[MangaTable.id].value
|
||||
@@ -251,6 +258,9 @@ object BackupMangaHandler {
|
||||
it[inLibrary] = manga.favorite || dbManga[inLibrary]
|
||||
|
||||
it[inLibraryAt] = manga.dateAdded.milliseconds.inWholeSeconds
|
||||
|
||||
it[lastModifiedAt] = manga.lastModifiedAt
|
||||
it[version] = manga.version
|
||||
}
|
||||
|
||||
dbMangaId
|
||||
@@ -268,7 +278,7 @@ object BackupMangaHandler {
|
||||
restoreMangaChapterData(mangaId, restoreMode, chapters, history, flags)
|
||||
}
|
||||
|
||||
// merge categories
|
||||
// update categories
|
||||
if (flags.includeCategories) {
|
||||
restoreMangaCategoryData(mangaId, categoryIds)
|
||||
}
|
||||
@@ -339,6 +349,9 @@ object BackupMangaHandler {
|
||||
this[ChapterTable.lastReadAt] =
|
||||
historyByChapter[chapter.url]?.maxOrNull()?.milliseconds?.inWholeSeconds ?: 0
|
||||
}
|
||||
|
||||
this[ChapterTable.lastModifiedAt] = chapter.lastModifiedAt
|
||||
this[ChapterTable.version] = chapter.version
|
||||
}.map { it[ChapterTable.id].value }
|
||||
} else {
|
||||
emptyList()
|
||||
@@ -387,6 +400,7 @@ object BackupMangaHandler {
|
||||
mangaId: Int,
|
||||
categoryIds: List<Int>,
|
||||
) {
|
||||
CategoryManga.removeMangaFromAllCategories(mangaId)
|
||||
CategoryManga.addMangaToCategories(mangaId, categoryIds)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@ class BackupCategory(
|
||||
// @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x
|
||||
// Bump by 100 to specify this is a 0.x value
|
||||
@ProtoNumber(100) var flags: Int = 0,
|
||||
// syncyomi
|
||||
@ProtoNumber(601) var version: Long = 0,
|
||||
@ProtoNumber(602) var uid: Long = 0,
|
||||
@ProtoNumber(603) var lastModifiedAt: Long = 0,
|
||||
// suwayomi
|
||||
@ProtoNumber(9000) var meta: Map<String, String> = emptyMap(),
|
||||
)
|
||||
|
||||
@@ -19,6 +19,9 @@ data class BackupChapter(
|
||||
// chapterNumber is called number is 1.x
|
||||
@ProtoNumber(9) var chapterNumber: Float = 0F,
|
||||
@ProtoNumber(10) var sourceOrder: Int = 0,
|
||||
// syncyomi
|
||||
@ProtoNumber(11) var lastModifiedAt: Long = 0,
|
||||
@ProtoNumber(12) var version: Long = 0,
|
||||
// suwayomi
|
||||
@ProtoNumber(9000) var meta: Map<String, String> = emptyMap(),
|
||||
)
|
||||
|
||||
@@ -34,6 +34,9 @@ data class BackupManga(
|
||||
@ProtoNumber(103) var viewer_flags: Int? = null,
|
||||
@ProtoNumber(104) var history: List<BackupHistory> = emptyList(),
|
||||
@ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE,
|
||||
// syncyomi
|
||||
@ProtoNumber(106) var lastModifiedAt: Long = 0,
|
||||
@ProtoNumber(109) var version: Long = 0,
|
||||
// suwayomi
|
||||
@ProtoNumber(9000) var meta: Map<String, String> = emptyMap(),
|
||||
)
|
||||
|
||||
@@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import suwayomi.tachidesk.global.impl.sync.SyncManager
|
||||
import suwayomi.tachidesk.manga.impl.Category
|
||||
import suwayomi.tachidesk.manga.impl.CategoryManga
|
||||
import suwayomi.tachidesk.manga.impl.Chapter
|
||||
@@ -337,90 +338,98 @@ class Updater : IUpdater {
|
||||
clear: Boolean?,
|
||||
forceAll: Boolean,
|
||||
) {
|
||||
saveLastUpdateTimestamp()
|
||||
|
||||
if (clear == true) {
|
||||
reset()
|
||||
}
|
||||
|
||||
val includeInUpdateStatusToCategoryMap = categories.groupBy { it.includeInUpdate }
|
||||
val excludedCategories = includeInUpdateStatusToCategoryMap[IncludeOrExclude.EXCLUDE].orEmpty()
|
||||
val includedCategories = includeInUpdateStatusToCategoryMap[IncludeOrExclude.INCLUDE].orEmpty()
|
||||
val unsetCategories = includeInUpdateStatusToCategoryMap[IncludeOrExclude.UNSET].orEmpty()
|
||||
val categoriesToUpdate =
|
||||
if (forceAll) {
|
||||
categories
|
||||
} else {
|
||||
includedCategories.ifEmpty { unsetCategories }
|
||||
}
|
||||
val skippedCategories = categories.subtract(categoriesToUpdate.toSet()).toList()
|
||||
val updateStatusCategories =
|
||||
mapOf(
|
||||
Pair(CategoryUpdateStatus.UPDATING, categoriesToUpdate),
|
||||
Pair(CategoryUpdateStatus.SKIPPED, skippedCategories),
|
||||
)
|
||||
|
||||
logger.debug { "Updating categories: '${categoriesToUpdate.joinToString("', '") { it.name }}'" }
|
||||
|
||||
val categoriesToUpdateMangas =
|
||||
categoriesToUpdate
|
||||
.flatMap { CategoryManga.getCategoryMangaList(it.id) }
|
||||
.distinctBy { it.id }
|
||||
val mangasToCategoriesMap = CategoryManga.getMangasCategories(categoriesToUpdateMangas.map { it.id })
|
||||
val mangasToUpdate =
|
||||
categoriesToUpdateMangas
|
||||
.asSequence()
|
||||
.filter { it.updateStrategy == UpdateStrategy.ALWAYS_UPDATE }
|
||||
.filter {
|
||||
if (serverConfig.excludeUnreadChapters.value) {
|
||||
(it.unreadCount ?: 0L) == 0L
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}.filter {
|
||||
if (it.initialized && serverConfig.excludeNotStarted.value) {
|
||||
it.lastReadAt != null
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}.filter {
|
||||
if (serverConfig.excludeCompleted.value) {
|
||||
it.status != MangaStatus.COMPLETED.name
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}.filter { forceAll || !excludedCategories.any { category -> mangasToCategoriesMap[it.id]?.contains(category) == true } }
|
||||
.toList()
|
||||
val skippedMangas = categoriesToUpdateMangas.subtract(mangasToUpdate.toSet()).toList()
|
||||
|
||||
this.updateStatusCategories = updateStatusCategories
|
||||
this.updateStatusSkippedMangas = skippedMangas
|
||||
|
||||
if (mangasToUpdate.isEmpty()) {
|
||||
// In case no manga gets updated and no update job was running before, the client would never receive an info
|
||||
// about its update request
|
||||
scope.launch {
|
||||
updateStatus(immediate = true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
updateStatus(
|
||||
categoryUpdates =
|
||||
updateStatusCategories[CategoryUpdateStatus.UPDATING]
|
||||
?.map {
|
||||
CategoryUpdateJob(it, CategoryUpdateStatus.UPDATING)
|
||||
}.orEmpty(),
|
||||
mangaUpdates = mangasToUpdate.map { UpdateJob(it) },
|
||||
isRunning = true,
|
||||
SyncManager.ensureSync()
|
||||
|
||||
saveLastUpdateTimestamp()
|
||||
|
||||
if (clear == true) {
|
||||
reset()
|
||||
}
|
||||
|
||||
val includeInUpdateStatusToCategoryMap = categories.groupBy { it.includeInUpdate }
|
||||
val excludedCategories = includeInUpdateStatusToCategoryMap[IncludeOrExclude.EXCLUDE].orEmpty()
|
||||
val includedCategories = includeInUpdateStatusToCategoryMap[IncludeOrExclude.INCLUDE].orEmpty()
|
||||
val unsetCategories = includeInUpdateStatusToCategoryMap[IncludeOrExclude.UNSET].orEmpty()
|
||||
val categoriesToUpdate =
|
||||
if (forceAll) {
|
||||
categories
|
||||
} else {
|
||||
includedCategories.ifEmpty { unsetCategories }
|
||||
}
|
||||
val skippedCategories = categories.subtract(categoriesToUpdate.toSet()).toList()
|
||||
val updateStatusCategories =
|
||||
mapOf(
|
||||
Pair(CategoryUpdateStatus.UPDATING, categoriesToUpdate),
|
||||
Pair(CategoryUpdateStatus.SKIPPED, skippedCategories),
|
||||
)
|
||||
|
||||
logger.debug { "Updating categories: '${categoriesToUpdate.joinToString("', '") { it.name }}'" }
|
||||
|
||||
val categoriesToUpdateMangas =
|
||||
categoriesToUpdate
|
||||
.flatMap { CategoryManga.getCategoryMangaList(it.id) }
|
||||
.distinctBy { it.id }
|
||||
val mangasToCategoriesMap = CategoryManga.getMangasCategories(categoriesToUpdateMangas.map { it.id })
|
||||
val mangasToUpdate =
|
||||
categoriesToUpdateMangas
|
||||
.asSequence()
|
||||
.filter { it.updateStrategy == UpdateStrategy.ALWAYS_UPDATE }
|
||||
.filter {
|
||||
if (serverConfig.excludeUnreadChapters.value) {
|
||||
(it.unreadCount ?: 0L) == 0L
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}.filter {
|
||||
if (it.initialized && serverConfig.excludeNotStarted.value) {
|
||||
it.lastReadAt != null
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}.filter {
|
||||
if (serverConfig.excludeCompleted.value) {
|
||||
it.status != MangaStatus.COMPLETED.name
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}.filter {
|
||||
forceAll ||
|
||||
!excludedCategories.any { category ->
|
||||
mangasToCategoriesMap[it.id]?.contains(category) == true
|
||||
}
|
||||
}.toList()
|
||||
val skippedMangas = categoriesToUpdateMangas.subtract(mangasToUpdate.toSet()).toList()
|
||||
|
||||
this@Updater.updateStatusCategories = updateStatusCategories
|
||||
this@Updater.updateStatusSkippedMangas = skippedMangas
|
||||
|
||||
if (mangasToUpdate.isEmpty()) {
|
||||
// In case no manga gets updated and no update job was running before, the client would never receive an info
|
||||
// about its update request
|
||||
scope.launch {
|
||||
updateStatus(immediate = true)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
updateStatus(
|
||||
categoryUpdates =
|
||||
updateStatusCategories[CategoryUpdateStatus.UPDATING]
|
||||
?.map {
|
||||
CategoryUpdateJob(it, CategoryUpdateStatus.UPDATING)
|
||||
}.orEmpty(),
|
||||
mangaUpdates = mangasToUpdate.map { UpdateJob(it) },
|
||||
isRunning = true,
|
||||
)
|
||||
}
|
||||
|
||||
addMangasToQueue(
|
||||
mangasToUpdate
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, MangaDataClass::title)),
|
||||
)
|
||||
}
|
||||
|
||||
addMangasToQueue(
|
||||
mangasToUpdate
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, MangaDataClass::title)),
|
||||
)
|
||||
}
|
||||
|
||||
override fun addMangasToQueue(mangas: List<MangaDataClass>) {
|
||||
|
||||
@@ -30,5 +30,8 @@ data class CategoryDataClass(
|
||||
val size: Int,
|
||||
val includeInUpdate: IncludeOrExclude,
|
||||
val includeInDownload: IncludeOrExclude,
|
||||
val version: Long,
|
||||
val uid: Long,
|
||||
val lastModifiedAt: Long,
|
||||
val meta: Map<String, String> = emptyMap(),
|
||||
)
|
||||
|
||||
@@ -38,6 +38,8 @@ data class ChapterDataClass(
|
||||
val pageCount: Int = -1,
|
||||
/** total chapter count, used to calculate if there's a next and prev chapter */
|
||||
val chapterCount: Int? = null,
|
||||
val lastModifiedAt: Long = 0,
|
||||
val version: Long = 0,
|
||||
/** used to store client specific values */
|
||||
val meta: Map<String, String> = emptyMap(),
|
||||
) {
|
||||
|
||||
@@ -43,6 +43,8 @@ data class MangaDataClass(
|
||||
val age: Long? = if (lastFetchedAt == null) 0 else Instant.now().epochSecond.minus(lastFetchedAt),
|
||||
val chaptersAge: Long? = if (chaptersLastFetchedAt == null) null else Instant.now().epochSecond.minus(chaptersLastFetchedAt),
|
||||
val trackers: List<MangaTrackerDataClass>? = null,
|
||||
val lastModifiedAt: Long = 0,
|
||||
val version: Long = 0,
|
||||
) {
|
||||
override fun toString(): String = "\"$title\" (id= $id) (sourceId= $sourceId)"
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ object CategoryTable : IntIdTable() {
|
||||
val isDefault = bool("is_default").default(false)
|
||||
val includeInUpdate = integer("include_in_update").default(IncludeOrExclude.UNSET.value)
|
||||
val includeInDownload = integer("include_in_download").default(IncludeOrExclude.UNSET.value)
|
||||
|
||||
val version = long("version").default(0)
|
||||
val uid = long("uid").default(0)
|
||||
val lastModifiedAt = long("last_modified_at").default(0)
|
||||
val isSyncing = bool("is_syncing").default(false)
|
||||
}
|
||||
|
||||
fun CategoryTable.toDataClass(categoryEntry: ResultRow) =
|
||||
@@ -30,5 +35,8 @@ fun CategoryTable.toDataClass(categoryEntry: ResultRow) =
|
||||
Category.getCategorySize(categoryEntry[id].value),
|
||||
IncludeOrExclude.fromValue(categoryEntry[includeInUpdate]),
|
||||
IncludeOrExclude.fromValue(categoryEntry[includeInDownload]),
|
||||
categoryEntry[version],
|
||||
categoryEntry[uid],
|
||||
categoryEntry[lastModifiedAt],
|
||||
Category.getCategoryMetaMap(categoryEntry[id].value),
|
||||
)
|
||||
|
||||
@@ -42,6 +42,10 @@ object ChapterTable : IntIdTable() {
|
||||
val manga = reference("manga", MangaTable, ReferenceOption.CASCADE)
|
||||
|
||||
val koreaderHash = varchar("koreader_hash", 32).nullable()
|
||||
|
||||
val lastModifiedAt = long("last_modified_at").default(0)
|
||||
val version = long("version").default(0)
|
||||
val isSyncing = bool("is_syncing").default(false)
|
||||
}
|
||||
|
||||
fun ChapterTable.toDataClass(
|
||||
@@ -83,4 +87,6 @@ fun ChapterTable.toDataClass(
|
||||
} else {
|
||||
emptyMap()
|
||||
},
|
||||
lastModifiedAt = chapterEntry[lastModifiedAt],
|
||||
version = chapterEntry[version],
|
||||
)
|
||||
|
||||
@@ -46,6 +46,10 @@ object MangaTable : IntIdTable() {
|
||||
val chaptersLastFetchedAt = long("chapters_last_fetched_at").default(0)
|
||||
|
||||
val updateStrategy = varchar("update_strategy", 256).default(UpdateStrategy.ALWAYS_UPDATE.name)
|
||||
|
||||
val lastModifiedAt = long("last_modified_at").default(0)
|
||||
val version = long("version").default(0)
|
||||
val isSyncing = bool("is_syncing").default(false)
|
||||
}
|
||||
|
||||
fun MangaTable.toDataClass(
|
||||
@@ -76,6 +80,8 @@ fun MangaTable.toDataClass(
|
||||
lastFetchedAt = mangaEntry[lastFetchedAt],
|
||||
chaptersLastFetchedAt = mangaEntry[chaptersLastFetchedAt],
|
||||
updateStrategy = UpdateStrategy.valueOf(mangaEntry[updateStrategy]),
|
||||
lastModifiedAt = mangaEntry[lastModifiedAt],
|
||||
version = mangaEntry[version],
|
||||
)
|
||||
|
||||
enum class MangaStatus(
|
||||
|
||||
Reference in New Issue
Block a user