Compare commits

...

3 Commits

Author SHA1 Message Date
KaiserBh
ed2b7df11d chore: detekt auto fix. 2024-02-23 18:52:10 +11:00
KaiserBh
7c85f50705 refactor: Use the new powerful versioning system. 2024-02-23 18:09:31 +11:00
KaiserBh
157a996f40 feat: db changes to accommodate new syncing logic.
Using timestamp to sync is a bit skewed due to system clock etc and therefore there was a lot of issues with it such as removing a manga that shouldn't have been removed. Marking chapters as unread even though it was marked as a read. Hopefully by using versioning system it should eliminate those issues.
2024-02-23 17:56:29 +11:00
20 changed files with 237 additions and 195 deletions

View File

@@ -22,7 +22,7 @@ android {
defaultConfig {
applicationId = "app.mihon"
versionCode = 4
versionCode = 5
versionName = "0.16.3"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")

View File

@@ -98,4 +98,6 @@ private fun Manga.toBackupManga() =
updateStrategy = this.updateStrategy,
lastModifiedAt = this.lastModifiedAt,
favoriteModifiedAt = this.favoriteModifiedAt,
version = this.version,
isSyncing = this.isSyncing,
)

View File

@@ -21,6 +21,8 @@ data class BackupChapter(
@ProtoNumber(9) var chapterNumber: Float = 0F,
@ProtoNumber(10) var sourceOrder: Long = 0,
@ProtoNumber(11) var lastModifiedAt: Long = 0,
@ProtoNumber(12) var version: Long = 0,
@ProtoNumber(13) var isSyncing: Long = 0,
) {
fun toChapterImpl(): Chapter {
return Chapter.create().copy(
@@ -35,6 +37,8 @@ data class BackupChapter(
dateUpload = this@BackupChapter.dateUpload,
sourceOrder = this@BackupChapter.sourceOrder,
lastModifiedAt = this@BackupChapter.lastModifiedAt,
version = this@BackupChapter.version,
isSyncing = this@BackupChapter.isSyncing
)
}
}
@@ -53,6 +57,8 @@ val backupChapterMapper = {
dateFetch: Long,
dateUpload: Long,
lastModifiedAt: Long,
version: Long,
isSyncing: Long,
->
BackupChapter(
url = url,
@@ -66,5 +72,7 @@ val backupChapterMapper = {
dateUpload = dateUpload,
sourceOrder = sourceOrder,
lastModifiedAt = lastModifiedAt,
version = version,
isSyncing = isSyncing
)
}

View File

@@ -39,6 +39,8 @@ data class BackupManga(
@ProtoNumber(106) var lastModifiedAt: Long = 0,
@ProtoNumber(107) var favoriteModifiedAt: Long? = null,
@ProtoNumber(108) var excludedScanlators: List<String> = emptyList(),
@ProtoNumber(109) var version: Long = 0,
@ProtoNumber(110) var isSyncing: Long = 0,
) {
fun getMangaImpl(): Manga {
return Manga.create().copy(
@@ -58,6 +60,8 @@ data class BackupManga(
updateStrategy = this@BackupManga.updateStrategy,
lastModifiedAt = this@BackupManga.lastModifiedAt,
favoriteModifiedAt = this@BackupManga.favoriteModifiedAt,
version = this@BackupManga.version,
isSyncing = this@BackupManga.isSyncing
)
}
}

View File

@@ -76,6 +76,11 @@ class MangaRestorer(
tracks = backupManga.tracking,
excludedScanlators = backupManga.excludedScanlators,
)
if (isSync) {
mangasQueries.resetIsSyncing()
chaptersQueries.resetIsSyncing()
}
}
}
@@ -84,7 +89,7 @@ class MangaRestorer(
}
private suspend fun restoreExistingManga(manga: Manga, dbManga: Manga): Manga {
return if (manga.lastModifiedAt > dbManga.lastModifiedAt) {
return if (manga.version > dbManga.version) {
updateManga(dbManga.copyFrom(manga).copy(id = dbManga.id))
} else {
updateManga(manga.copyFrom(dbManga).copy(id = dbManga.id))
@@ -92,6 +97,21 @@ class MangaRestorer(
}
private fun Manga.copyFrom(newer: Manga): Manga {
if (isSync) {
return this.copy(
favorite = this.favorite || newer.favorite,
author = newer.author,
artist = newer.artist,
description = newer.description,
genre = newer.genre,
thumbnailUrl = newer.thumbnailUrl,
status = newer.status,
initialized = this.initialized || newer.initialized,
version = newer.version,
isSyncing = 1,
)
}
return this.copy(
favorite = this.favorite || newer.favorite,
author = newer.author,
@@ -101,6 +121,7 @@ class MangaRestorer(
thumbnailUrl = newer.thumbnailUrl,
status = newer.status,
initialized = this.initialized || newer.initialized,
version = newer.version,
)
}
@@ -127,6 +148,8 @@ class MangaRestorer(
dateAdded = manga.dateAdded,
mangaId = manga.id,
updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
version = manga.version,
isSyncing = manga.isSyncing
)
}
return manga
@@ -138,6 +161,7 @@ class MangaRestorer(
return manga.copy(
initialized = manga.description != null,
id = insertManga(manga),
version = manga.version
)
}
@@ -169,6 +193,7 @@ class MangaRestorer(
bookmark = chapter.bookmark || dbChapter.bookmark,
read = chapter.read,
lastPageRead = chapter.lastPageRead,
isSyncing = 1,
)
} else {
chapter.copyFrom(dbChapter).let {
@@ -184,7 +209,7 @@ class MangaRestorer(
}
private fun Chapter.forComparison() =
this.copy(id = 0L, mangaId = 0L, dateFetch = 0L, dateUpload = 0L, lastModifiedAt = 0L)
this.copy(id = 0L, mangaId = 0L, dateFetch = 0L, dateUpload = 0L, lastModifiedAt = 0L, version = 0L)
private suspend fun insertNewChapters(chapters: List<Chapter>) {
handler.await(true) {
@@ -201,6 +226,7 @@ class MangaRestorer(
chapter.sourceOrder,
chapter.dateFetch,
chapter.dateUpload,
chapter.version,
)
}
}
@@ -222,6 +248,8 @@ class MangaRestorer(
dateFetch = null,
dateUpload = null,
chapterId = chapter.id,
version = chapter.version,
isSyncing = chapter.isSyncing,
)
}
}
@@ -254,6 +282,7 @@ class MangaRestorer(
coverLastModified = manga.coverLastModified,
dateAdded = manga.dateAdded,
updateStrategy = manga.updateStrategy,
version = manga.version,
)
mangasQueries.selectLastInsertedRowId()
}

View File

@@ -21,6 +21,10 @@ interface Chapter : SChapter, Serializable {
var source_order: Int
var last_modified: Long
var version: Long
var isSyncing: Long
}
fun Chapter.toDomainChapter(): DomainChapter? {
@@ -39,5 +43,7 @@ fun Chapter.toDomainChapter(): DomainChapter? {
chapterNumber = chapter_number.toDouble(),
scanlator = scanlator,
lastModifiedAt = last_modified,
version = version,
isSyncing = isSyncing,
)
}

View File

@@ -28,6 +28,10 @@ class ChapterImpl : Chapter {
override var last_modified: Long = 0
override var version: Long = 0
override var isSyncing: Long = 0
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false

View File

@@ -23,7 +23,6 @@ import tachiyomi.data.Chapters
import tachiyomi.data.DatabaseHandler
import tachiyomi.data.manga.MangaMapper.mapManga
import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.manga.interactor.GetFavorites
import tachiyomi.domain.manga.model.Manga
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -47,7 +46,6 @@ class SyncManager(
encodeDefaults = true
ignoreUnknownKeys = true
},
private val getFavorites: GetFavorites = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
) {
private val backupCreator: BackupCreator = BackupCreator(context, false)
@@ -72,8 +70,15 @@ class SyncManager(
* from the database using the BackupManager, then synchronizes the data with a sync service.
*/
suspend fun syncData() {
// Reset isSyncing in case it was left over or failed syncing during restore.
handler.await(inTransaction = true) {
mangasQueries.resetIsSyncing()
chaptersQueries.resetIsSyncing()
}
val syncOptions = syncPreferences.getSyncSettings()
val databaseManga = getAllMangaFromDB()
val databaseManga = getAllMangaThatNeedsSync()
val backupOptions = BackupOptions(
libraryEntries = syncOptions.libraryEntries,
categories = syncOptions.categories,
@@ -198,132 +203,47 @@ class SyncManager(
return handler.awaitList { mangasQueries.getAllManga(::mapManga) }
}
private suspend fun getAllMangaThatNeedsSync(): List<Manga> {
return handler.awaitList { mangasQueries.getMangasWithFavoriteTimestamp(::mapManga) }
}
private suspend fun isMangaDifferent(localManga: Manga, remoteManga: BackupManga): Boolean {
val localChapters = handler.await { chaptersQueries.getChaptersByMangaId(localManga.id, 0).executeAsList() }
val localCategories = getCategories.await(localManga.id).map { it.order }
val logTag = "isMangaDifferent"
// Logging the start of comparison
logcat(LogPriority.DEBUG, logTag) {
"Comparing local manga (Title: ${localManga.title}) with remote manga (Title: ${remoteManga.title})"
}
var isDifferent = false
// Compare each field and log if they are different
if (localManga.source != remoteManga.source) {
logDifference(logTag, "Source", localManga.source.toString(), remoteManga.source.toString())
isDifferent = true
}
if (localManga.url != remoteManga.url) {
logDifference(logTag, "URL", localManga.url, remoteManga.url)
isDifferent = true
}
if (localManga.favorite != remoteManga.favorite) {
logDifference(logTag, "Favorite", localManga.favorite.toString(), remoteManga.favorite.toString())
isDifferent = true
}
if (areChaptersDifferent(localChapters, remoteManga.chapters)) {
logcat(LogPriority.DEBUG, logTag) {
"Chapters are different for manga: ${localManga.title}"
}
isDifferent = true
}
if (localCategories.toSet() != remoteManga.categories.toSet()) {
logcat(LogPriority.DEBUG, logTag) {
"Categories differ for manga: ${localManga.title}. " +
"Local categories: ${localCategories.joinToString()}, " +
"Remote categories: ${remoteManga.categories.joinToString()}"
}
isDifferent = true
}
// Log final result
logcat(LogPriority.DEBUG, logTag) {
"Manga difference check result for local manga (Title: ${localManga.title}): $isDifferent"
}
return isDifferent
}
private fun logDifference(tag: String, field: String, localValue: String, remoteValue: String) {
logcat(LogPriority.DEBUG, tag) {
"Difference found in $field. Local: $localValue, Remote: $remoteValue"
}
}
private fun areChaptersDifferent(localChapters: List<Chapters>, remoteChapters: List<BackupChapter>): Boolean {
val logTag = "areChaptersDifferent"
// Early return if the sizes are different
if (localChapters.size != remoteChapters.size) {
logcat(LogPriority.DEBUG, logTag) {
"Chapter lists differ in size. Local: ${localChapters.size}, Remote: ${remoteChapters.size}"
}
return true
}
// Convert all remote chapters to Chapter instances
val convertedRemoteChapters = remoteChapters.map { it.toChapterImpl() }
if (localManga.version != remoteManga.version) {
return true
}
// Create a map for the local chapters for efficient comparison
if (localCategories.toSet() != remoteManga.categories.toSet()) {
return true
}
return false
}
private fun areChaptersDifferent(localChapters: List<Chapters>, remoteChapters: List<BackupChapter>): Boolean {
val localChapterMap = localChapters.associateBy { it.url }
val remoteChapterMap = remoteChapters.associateBy { it.url }
var isDifferent = false
// Check for any differences
for (remoteChapter in convertedRemoteChapters) {
val localChapter = localChapterMap[remoteChapter.url]
if (localChapter == null) {
logcat(LogPriority.DEBUG, logTag) {
"Local chapter not found for URL: ${remoteChapter.url}"
}
isDifferent = true
continue
}
if (localChapter.url != remoteChapter.url) {
logDifference(logTag, "URL", localChapter.url, remoteChapter.url)
isDifferent = true
}
if (localChapter.read != remoteChapter.read) {
logDifference(logTag, "Read Status", localChapter.read.toString(), remoteChapter.read.toString())
isDifferent = true
}
if (localChapter.bookmark != remoteChapter.bookmark) {
logDifference(
logTag,
"Bookmark Status",
localChapter.bookmark.toString(),
remoteChapter.bookmark.toString(),
)
isDifferent = true
}
if (localChapter.last_page_read != remoteChapter.lastPageRead) {
logDifference(
logTag,
"Last Page Read",
localChapter.last_page_read.toString(),
remoteChapter.lastPageRead.toString(),
)
isDifferent = true
}
// Break the loop if a difference is found
if (isDifferent) break
if (localChapterMap.size != remoteChapterMap.size) {
return true
}
if (!isDifferent) {
logcat(LogPriority.DEBUG, logTag) {
"No differences found in chapters."
for ((url, localChapter) in localChapterMap) {
val remoteChapter = remoteChapterMap[url]
// If a matching remote chapter doesn't exist, or the version numbers are different, consider them different
if (remoteChapter == null || localChapter.version != remoteChapter.version) {
return true
}
}
return isDifferent
return false
}
/**
@@ -339,8 +259,8 @@ class SyncManager(
val logTag = "filterFavoritesAndNonFavorites"
val elapsedTimeMillis = measureTimeMillis {
val databaseMangaFavorites = getFavorites.await()
val localMangaMap = databaseMangaFavorites.associateBy {
val databaseManga = getAllMangaFromDB()
val localMangaMap = databaseManga.associateBy {
Triple(it.source, it.url, it.title)
}
@@ -384,10 +304,12 @@ class SyncManager(
*/
private suspend fun updateNonFavorites(nonFavorites: List<BackupManga>) {
val localMangaList = getAllMangaFromDB()
val localMangaMap = localMangaList.associateBy { it.url }
val localMangaMap = localMangaList.associateBy { Triple(it.source, it.url, it.title) }
nonFavorites.forEach { nonFavorite ->
localMangaMap[nonFavorite.url]?.let { localManga ->
val key = Triple(nonFavorite.source, nonFavorite.url, nonFavorite.title)
localMangaMap[key]?.let { localManga ->
if (localManga.favorite != nonFavorite.favorite) {
val updatedManga = localManga.copy(favorite = nonFavorite.favorite)
mangaRestorer.updateManga(updatedManga)

View File

@@ -13,7 +13,6 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import logcat.LogPriority
import logcat.logcat
import java.time.Instant
@Serializable
data class SyncData(
@@ -100,7 +99,6 @@ abstract class SyncService(
): List<BackupManga> {
val logTag = "MergeMangaLists"
// Convert null lists to empty to simplify logic
val localMangaListSafe = localMangaList.orEmpty()
val remoteMangaListSafe = remoteMangaList.orEmpty()
@@ -108,7 +106,6 @@ abstract class SyncService(
"Starting merge. Local list size: ${localMangaListSafe.size}, Remote list size: ${remoteMangaListSafe.size}"
}
// Define a function to create a composite key from manga
fun mangaCompositeKey(manga: BackupManga): String {
return "${manga.source}|${manga.url}|${manga.title.lowercase().trim()}|${manga.author?.lowercase()?.trim()}"
}
@@ -121,65 +118,31 @@ abstract class SyncService(
"Starting merge. Local list size: ${localMangaListSafe.size}, Remote list size: ${remoteMangaListSafe.size}"
}
// Prepare to merge both sets of manga
val mergedList = (localMangaMap.keys + remoteMangaMap.keys).distinct().mapNotNull { compositeKey ->
val local = localMangaMap[compositeKey]
val remote = remoteMangaMap[compositeKey]
logcat(LogPriority.DEBUG, logTag) {
"Processing key: $compositeKey. Local favorite: ${local?.favorite}, " +
"Remote favorite: ${remote?.favorite}"
}
// New version comparison logic
when {
local != null && remote == null -> {
logcat(LogPriority.DEBUG, logTag) {
"Taking local manga: ${local.title} as it is not present remotely. " +
"Favorite status: ${local.favorite}"
}
local
}
local == null && remote != null -> {
logcat(LogPriority.DEBUG, logTag) {
"Taking remote manga: ${remote.title} as it is not present locally. " +
"Favorite status: ${remote.favorite}"
}
remote
}
local != null && remote == null -> local
local == null && remote != null -> remote
local != null && remote != null -> {
logcat(LogPriority.DEBUG, logTag) {
"Inspecting timestamps for ${local.title}. " +
"Local lastModifiedAt: ${local.lastModifiedAt * 1000L}, " +
"Remote lastModifiedAt: ${remote.lastModifiedAt * 1000L}"
}
// Convert seconds to milliseconds for accurate time comparison
val localTime = Instant.ofEpochMilli(local.lastModifiedAt * 1000L)
val remoteTime = Instant.ofEpochMilli(remote.lastModifiedAt * 1000L)
val mergedChapters = mergeChapters(local.chapters, remote.chapters)
logcat(LogPriority.DEBUG, logTag) {
"Merging manga: ${local.title}. Local time: $localTime, Remote time: $remoteTime, " +
"Local favorite: ${local.favorite}, Remote favorite: ${remote.favorite}"
}
if (localTime >= remoteTime) {
// Compare versions to decide which manga to keep
if (local.version >= remote.version) {
logcat(
LogPriority.DEBUG,
logTag,
logTag
) { "Keeping local version of ${local.title} with merged chapters." }
local.copy(chapters = mergedChapters)
local.copy(chapters = mergeChapters(local.chapters, remote.chapters))
} else {
logcat(
LogPriority.DEBUG,
logTag,
logTag
) { "Keeping remote version of ${remote.title} with merged chapters." }
remote.copy(chapters = mergedChapters)
remote.copy(chapters = mergeChapters(local.chapters, remote.chapters))
}
}
else -> {
logcat(LogPriority.DEBUG, logTag) { "No manga found for key: $compositeKey. Skipping." }
null
}
else -> null // No manga found for key
}
}
@@ -200,12 +163,10 @@ abstract class SyncService(
): List<BackupChapter> {
val logTag = "MergeChapters"
// Define a function to create a composite key from a chapter
fun chapterCompositeKey(chapter: BackupChapter): String {
return "${chapter.url}|${chapter.name}|${chapter.chapterNumber}"
}
// Create maps using composite keys
val localChapterMap = localChapters.associateBy { chapterCompositeKey(it) }
val remoteChapterMap = remoteChapters.associateBy { chapterCompositeKey(it) }
@@ -213,7 +174,7 @@ abstract class SyncService(
"Starting chapter merge. Local chapters: ${localChapters.size}, Remote chapters: ${remoteChapters.size}"
}
// Merge both chapter maps
// Merge both chapter maps based on version numbers
val mergedChapters = (localChapterMap.keys + remoteChapterMap.keys).distinct().mapNotNull { compositeKey ->
val localChapter = localChapterMap[compositeKey]
val remoteChapter = remoteChapterMap[compositeKey]
@@ -233,16 +194,12 @@ abstract class SyncService(
remoteChapter
}
localChapter != null && remoteChapter != null -> {
val localInstant = Instant.ofEpochMilli(localChapter.lastModifiedAt * 1000L)
val remoteInstant = Instant.ofEpochMilli(remoteChapter.lastModifiedAt * 1000L)
val chosenChapter = if (localInstant >= remoteInstant) localChapter else remoteChapter
// Use version number to decide which chapter to keep
val chosenChapter = if (localChapter.version >= remoteChapter.version) localChapter else remoteChapter
logcat(LogPriority.DEBUG, logTag) {
"Merging chapter: ${chosenChapter.name}. Chosen from: ${if (localInstant >= remoteInstant) {
"Local"
} else {
"Remote"
}}."
"Merging chapter: ${chosenChapter.name}. Chosen version from: ${
if (localChapter.version >= remoteChapter.version) "Local" else "Remote"
}, Local version: ${localChapter.version}, Remote version: ${remoteChapter.version}."
}
chosenChapter
}

View File

@@ -29,6 +29,7 @@ class ChapterRepositoryImpl(
chapter.sourceOrder,
chapter.dateFetch,
chapter.dateUpload,
chapter.version
)
val lastInsertId = chaptersQueries.selectLastInsertedRowId().executeAsOne()
chapter.copy(id = lastInsertId)
@@ -64,6 +65,8 @@ class ChapterRepositoryImpl(
dateFetch = chapterUpdate.dateFetch,
dateUpload = chapterUpdate.dateUpload,
chapterId = chapterUpdate.id,
version = chapterUpdate.version,
isSyncing = chapterUpdate.isSyncing,
)
}
}
@@ -138,6 +141,8 @@ class ChapterRepositoryImpl(
dateFetch: Long,
dateUpload: Long,
lastModifiedAt: Long,
version: Long,
isSyncing: Long,
): Chapter = Chapter(
id = id,
mangaId = mangaId,
@@ -152,5 +157,7 @@ class ChapterRepositoryImpl(
chapterNumber = chapterNumber,
scanlator = scanlator,
lastModifiedAt = lastModifiedAt,
version = version,
isSyncing = isSyncing,
)
}

View File

@@ -28,6 +28,8 @@ object MangaMapper {
calculateInterval: Long,
lastModifiedAt: Long,
favoriteModifiedAt: Long?,
version: Long,
isSyncing: Long,
): Manga = Manga(
id = id,
source = source,
@@ -51,6 +53,8 @@ object MangaMapper {
initialized = initialized,
lastModifiedAt = lastModifiedAt,
favoriteModifiedAt = favoriteModifiedAt,
version = version,
isSyncing = isSyncing,
)
fun mapLibraryManga(
@@ -76,6 +80,8 @@ object MangaMapper {
calculateInterval: Long,
lastModifiedAt: Long,
favoriteModifiedAt: Long?,
version: Long,
isSyncing: Long,
totalCount: Long,
readCount: Double,
latestUpload: Long,
@@ -107,6 +113,8 @@ object MangaMapper {
calculateInterval,
lastModifiedAt,
favoriteModifiedAt,
version,
isSyncing,
),
category = category,
totalChapters = totalCount,

View File

@@ -106,6 +106,7 @@ class MangaRepositoryImpl(
coverLastModified = manga.coverLastModified,
dateAdded = manga.dateAdded,
updateStrategy = manga.updateStrategy,
version = manga.version,
)
mangasQueries.selectLastInsertedRowId()
}
@@ -155,6 +156,8 @@ class MangaRepositoryImpl(
dateAdded = value.dateAdded,
mangaId = value.id,
updateStrategy = value.updateStrategy?.let(UpdateStrategyColumnAdapter::encode),
version = value.version,
isSyncing = value.isSyncing,
)
}
}

View File

@@ -14,6 +14,8 @@ CREATE TABLE chapters(
date_fetch INTEGER NOT NULL,
date_upload INTEGER NOT NULL,
last_modified_at INTEGER NOT NULL DEFAULT 0,
version INTEGER NOT NULL DEFAULT 0,
is_syncing INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
ON DELETE CASCADE
);
@@ -30,6 +32,16 @@ BEGIN
WHERE _id = new._id;
END;
CREATE TRIGGER update_chapter_version AFTER UPDATE ON chapters
BEGIN
UPDATE chapters SET version = version + 1
WHERE _id = new._id AND new.is_syncing = 0 AND (
new.read != old.read OR
new.bookmark != old.bookmark OR
new.last_page_read != old.last_page_read
);
END;
getChapterById:
SELECT *
FROM chapters
@@ -73,9 +85,14 @@ removeChaptersWithIds:
DELETE FROM chapters
WHERE _id IN :chapterIds;
resetIsSyncing:
UPDATE chapters
SET is_syncing = 0
WHERE is_syncing = 1;
insert:
INSERT INTO chapters(manga_id, url, name, scanlator, read, bookmark, last_page_read, chapter_number, source_order, date_fetch, date_upload, last_modified_at)
VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, 0);
INSERT INTO chapters(manga_id, url, name, scanlator, read, bookmark, last_page_read, chapter_number, source_order, date_fetch, date_upload, last_modified_at, version, is_syncing)
VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, 0, :version, 0);
update:
UPDATE chapters
@@ -89,7 +106,9 @@ SET manga_id = coalesce(:mangaId, manga_id),
chapter_number = coalesce(:chapterNumber, chapter_number),
source_order = coalesce(:sourceOrder, source_order),
date_fetch = coalesce(:dateFetch, date_fetch),
date_upload = coalesce(:dateUpload, date_upload)
date_upload = coalesce(:dateUpload, date_upload),
version = coalesce(:version, version),
is_syncing = coalesce(:isSyncing, is_syncing)
WHERE _id = :chapterId;
selectLastInsertedRowId:

View File

@@ -25,7 +25,9 @@ CREATE TABLE mangas(
update_strategy INTEGER AS UpdateStrategy NOT NULL DEFAULT 0,
calculate_interval INTEGER DEFAULT 0 NOT NULL,
last_modified_at INTEGER NOT NULL DEFAULT 0,
favorite_modified_at INTEGER
favorite_modified_at INTEGER,
version INTEGER NOT NULL DEFAULT 0,
is_syncing INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1;
@@ -48,6 +50,16 @@ BEGIN
WHERE _id = new._id;
END;
CREATE TRIGGER update_manga_version AFTER UPDATE ON mangas
BEGIN
UPDATE mangas SET version = version + 1
WHERE _id = new._id AND new.is_syncing = 0 AND (
new.url != old.url OR
new.description != old.description OR
new.favorite != old.favorite
);
END;
getMangaById:
SELECT *
FROM mangas
@@ -104,6 +116,11 @@ resetViewerFlags:
UPDATE mangas
SET viewer = 0;
resetIsSyncing:
UPDATE mangas
SET is_syncing = 0
WHERE is_syncing = 1;
getSourceIdsWithNonLibraryManga:
SELECT source, COUNT(*) AS manga_count
FROM mangas
@@ -116,8 +133,8 @@ WHERE favorite = 0
AND source IN :sourceIds;
insert:
INSERT INTO mangas(source, url, artist, author, description, genre, title, status, thumbnail_url, favorite, last_update, next_update, initialized, viewer, chapter_flags, cover_last_modified, date_added, update_strategy, calculate_interval, last_modified_at)
VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favorite, :lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :dateAdded, :updateStrategy, :calculateInterval, 0);
INSERT INTO mangas(source, url, artist, author, description, genre, title, status, thumbnail_url, favorite, last_update, next_update, initialized, viewer, chapter_flags, cover_last_modified, date_added, update_strategy, calculate_interval, last_modified_at, version)
VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favorite, :lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :dateAdded, :updateStrategy, :calculateInterval, 0, :version);
update:
UPDATE mangas SET
@@ -139,7 +156,9 @@ UPDATE mangas SET
cover_last_modified = coalesce(:coverLastModified, cover_last_modified),
date_added = coalesce(:dateAdded, date_added),
update_strategy = coalesce(:updateStrategy, update_strategy),
calculate_interval = coalesce(:calculateInterval, calculate_interval)
calculate_interval = coalesce(:calculateInterval, calculate_interval),
version = coalesce(:version, version),
is_syncing = coalesce(:isSyncing, is_syncing)
WHERE _id = :mangaId;
selectLastInsertedRowId:

View File

@@ -2,25 +2,22 @@ CREATE TABLE mangas_categories(
_id INTEGER NOT NULL PRIMARY KEY,
manga_id INTEGER NOT NULL,
category_id INTEGER NOT NULL,
last_modified_at INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(category_id) REFERENCES categories (_id)
ON DELETE CASCADE,
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
ON DELETE CASCADE
);
CREATE TRIGGER update_last_modified_at_mangas_categories
AFTER UPDATE ON mangas_categories
FOR EACH ROW
CREATE TRIGGER insert_manga_category_update_version AFTER INSERT ON mangas_categories
BEGIN
UPDATE mangas_categories
SET last_modified_at = strftime('%s', 'now')
WHERE _id = new._id;
UPDATE mangas
SET version = version + 1
WHERE _id = new.manga_id AND (SELECT is_syncing FROM mangas WHERE _id = new.manga_id) = 0;
END;
insert:
INSERT INTO mangas_categories(manga_id, category_id, last_modified_at)
VALUES (:mangaId, :categoryId, 0);
INSERT INTO mangas_categories(manga_id, category_id)
VALUES (:mangaId, :categoryId);
deleteMangaCategoryByMangaId:
DELETE FROM mangas_categories

View File

@@ -0,0 +1,40 @@
-- Mangas table
ALTER TABLE mangas ADD COLUMN version INTEGER NOT NULL DEFAULT 0;
ALTER TABLE mangas ADD COLUMN is_syncing INTEGER NOT NULL DEFAULT 0;
-- Chapters table
ALTER TABLE chapters ADD COLUMN version INTEGER NOT NULL DEFAULT 0;
ALTER TABLE chapters ADD COLUMN is_syncing INTEGER NOT NULL DEFAULT 0;
-- Mangas triggers
DROP TRIGGER IF EXISTS update_manga_version;
CREATE TRIGGER update_manga_version AFTER UPDATE ON mangas
BEGIN
UPDATE mangas SET version = version + 1
WHERE _id = new._id AND new.is_syncing = 0 AND (
new.url != old.url OR
new.description != old.description OR
new.favorite != old.favorite
);
END;
-- Chapters triggers
DROP TRIGGER IF EXISTS update_chapter_version;
CREATE TRIGGER update_chapter_version AFTER UPDATE ON chapters
BEGIN
UPDATE chapters SET version = version + 1
WHERE _id = new._id AND new.is_syncing = 0 AND (
new.read != old.read OR
new.bookmark != old.bookmark OR
new.last_page_read != old.last_page_read
);
END;
-- manga_categories table
DROP TRIGGER IF EXISTS insert_manga_category_update_version;
CREATE TRIGGER insert_manga_category_update_version AFTER INSERT ON mangas_categories
BEGIN
UPDATE mangas
SET version = version + 1
WHERE _id = new.manga_id AND (SELECT is_syncing FROM mangas WHERE _id = new.manga_id) = 0;
END;

View File

@@ -14,6 +14,8 @@ data class Chapter(
val chapterNumber: Double,
val scanlator: String?,
val lastModifiedAt: Long,
val version: Long,
val isSyncing: Long,
) {
val isRecognizedNumber: Boolean
get() = chapterNumber >= 0f
@@ -43,6 +45,8 @@ data class Chapter(
chapterNumber = -1.0,
scanlator = null,
lastModifiedAt = 0,
version = 1,
isSyncing = 0,
)
}
}

View File

@@ -13,6 +13,8 @@ data class ChapterUpdate(
val dateUpload: Long? = null,
val chapterNumber: Double? = null,
val scanlator: String? = null,
val version: Long? = null,
val isSyncing: Long? = null,
)
fun Chapter.toChapterUpdate(): ChapterUpdate {
@@ -29,5 +31,7 @@ fun Chapter.toChapterUpdate(): ChapterUpdate {
dateUpload,
chapterNumber,
scanlator,
version,
isSyncing
)
}

View File

@@ -29,6 +29,9 @@ data class Manga(
val initialized: Boolean,
val lastModifiedAt: Long,
val favoriteModifiedAt: Long?,
val version: Long,
val isSyncing: Long,
) : Serializable {
val expectedNextUpdate: Instant?
@@ -122,6 +125,8 @@ data class Manga(
initialized = false,
lastModifiedAt = 0L,
favoriteModifiedAt = null,
version = 0L,
isSyncing = 0L,
)
}
}

View File

@@ -23,6 +23,8 @@ data class MangaUpdate(
val thumbnailUrl: String? = null,
val updateStrategy: UpdateStrategy? = null,
val initialized: Boolean? = null,
val version: Long? = null,
val isSyncing: Long? = null,
)
fun Manga.toMangaUpdate(): MangaUpdate {
@@ -47,5 +49,7 @@ fun Manga.toMangaUpdate(): MangaUpdate {
thumbnailUrl = thumbnailUrl,
updateStrategy = updateStrategy,
initialized = initialized,
version = version,
isSyncing = isSyncing
)
}