From 1cb34adb01ef63ac54ecb2992a68b8d62866a915 Mon Sep 17 00:00:00 2001
From: KaiserBh <41852205+KaiserBh@users.noreply.github.com>
Date: Tue, 11 Jul 2023 05:52:57 +1000
Subject: [PATCH] Database changes to support library syncing (#9683)

* feat: added migrations.

* feat: create triggers, account for new installs.

* feat: update mappers to include the new field.

* feat: update backupManga and backupChapter.

Include the new fields to be backed up as well.

* feat: add sql query to fetch all manga with `last_favorited_at` field.

* feat: version bump.

* chore: revert and refactor.

* chore: forgot to lower case the field name.

* chore: added getAllManga query as well renamed `fetchMangaWithLastFavorite` to `getMangasWithFavoriteTimestamp`

* chore: oops that's not meant to be there.

* feat: back fill and set last_modified_at to not null.

* chore: remove redundant triggers.

* fix: build error, accidentally removed insert.

* fix: build error, accidentally removed insert.

* refactor: review pointer, make fields not null.

(cherry picked from commit a577f5534f31086174b1cc851d8b489d69f557e8)

# Conflicts:
#	app/build.gradle.kts
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt
#	data/src/main/java/tachiyomi/data/manga/MangaMapper.kt
#	data/src/main/sqldelight/tachiyomi/data/mangas.sq
#	data/src/main/sqldelight/tachiyomi/migrations/25.sqm
#	domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt
---
 app/build.gradle.kts                          |  2 +-
 .../data/backup/models/BackupChapter.kt       |  5 +-
 .../data/backup/models/BackupManga.kt         |  5 ++
 .../tachiyomi/data/database/models/Chapter.kt |  3 ++
 .../data/database/models/ChapterImpl.kt       |  2 +
 .../main/java/exh/eh/EHentaiUpdateHelper.kt   |  1 +
 .../main/java/tachiyomi/data/LibraryQuery.kt  | 16 +++---
 .../tachiyomi/data/chapter/ChapterMapper.kt   |  5 +-
 .../java/tachiyomi/data/manga/MangaMapper.kt  | 14 ++++--
 .../sqldelight/tachiyomi/data/chapters.sq     | 14 +++++-
 .../main/sqldelight/tachiyomi/data/mangas.sq  | 34 +++++++++++--
 .../tachiyomi/data/mangas_categories.sq       | 14 +++++-
 .../sqldelight/tachiyomi/migrations/28.sqm    | 49 +++++++++++++++++++
 .../tachiyomi/domain/chapter/model/Chapter.kt |  2 +
 .../tachiyomi/domain/manga/model/Manga.kt     |  4 ++
 15 files changed, 148 insertions(+), 22 deletions(-)
 create mode 100644 data/src/main/sqldelight/tachiyomi/migrations/28.sqm

diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 3d03b7935..f86732df0 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -27,7 +27,7 @@ android {
     defaultConfig {
         applicationId = "eu.kanade.tachiyomi.sy"
 
-        versionCode = 54
+        versionCode = 55
         versionName = "1.9.3"
 
         buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt
index a70121a8c..ed42a75ad 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt
@@ -20,6 +20,7 @@ data class BackupChapter(
     // chapterNumber is called number is 1.x
     @ProtoNumber(9) var chapterNumber: Float = 0F,
     @ProtoNumber(10) var sourceOrder: Long = 0,
+    @ProtoNumber(11) var lastModifiedAt: Long = 0,
 ) {
     fun toChapterImpl(): Chapter {
         return Chapter.create().copy(
@@ -33,11 +34,12 @@ data class BackupChapter(
             dateFetch = this@BackupChapter.dateFetch,
             dateUpload = this@BackupChapter.dateUpload,
             sourceOrder = this@BackupChapter.sourceOrder,
+            lastModifiedAt = this@BackupChapter.lastModifiedAt,
         )
     }
 }
 
-val backupChapterMapper = { _: Long, _: Long, url: String, name: String, scanlator: String?, read: Boolean, bookmark: Boolean, lastPageRead: Long, chapterNumber: Float, source_order: Long, dateFetch: Long, dateUpload: Long ->
+val backupChapterMapper = { _: Long, _: Long, url: String, name: String, scanlator: String?, read: Boolean, bookmark: Boolean, lastPageRead: Long, chapterNumber: Float, source_order: Long, dateFetch: Long, dateUpload: Long, lastModifiedAt: Long ->
     BackupChapter(
         url = url,
         name = name,
@@ -49,5 +51,6 @@ val backupChapterMapper = { _: Long, _: Long, url: String, name: String, scanlat
         dateFetch = dateFetch,
         dateUpload = dateUpload,
         sourceOrder = source_order,
+        lastModifiedAt = lastModifiedAt,
     )
 }
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt
index 06ca854c3..d642cdee4 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt
@@ -41,6 +41,8 @@ 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,
+    @ProtoNumber(106) var lastModifiedAt: Long = 0,
+    @ProtoNumber(107) var favoriteModifiedAt: Long? = 0,
 
     // SY specific values
     @ProtoNumber(600) var mergedMangaReferences: List<BackupMergedMangaReference> = emptyList(),
@@ -76,6 +78,7 @@ data class BackupManga(
             viewerFlags = (this@BackupManga.viewer_flags ?: this@BackupManga.viewer).toLong(),
             chapterFlags = this@BackupManga.chapterFlags.toLong(),
             updateStrategy = this@BackupManga.updateStrategy,
+            lastModifiedAt = this@BackupManga.lastModifiedAt,
             filteredScanlators = this@BackupManga.filtered_scanlators?.let(listOfStringsAndAdapter::decode),
         )
     }
@@ -135,6 +138,8 @@ data class BackupManga(
                 viewer_flags = manga.viewerFlags.toInt(),
                 chapterFlags = manga.chapterFlags.toInt(),
                 updateStrategy = manga.updateStrategy,
+                lastModifiedAt = manga.lastModifiedAt,
+                favoriteModifiedAt = manga.favoriteModifiedAt,
                 // SY -->
                 filtered_scanlators = manga.filteredScanlators?.let(listOfStringsAndAdapter::encode),
             ).also { backupManga ->
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt
index 8ca265d6b..be1d72b08 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt
@@ -19,6 +19,8 @@ interface Chapter : SChapter, Serializable {
     var date_fetch: Long
 
     var source_order: Int
+
+    var last_modified: Long
 }
 
 fun Chapter.toDomainChapter(): DomainChapter? {
@@ -36,5 +38,6 @@ fun Chapter.toDomainChapter(): DomainChapter? {
         dateUpload = date_upload,
         chapterNumber = chapter_number,
         scanlator = scanlator,
+        lastModifiedAt = last_modified,
     )
 }
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt
index a1a2d3f55..58ba41dec 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt
@@ -26,6 +26,8 @@ class ChapterImpl : Chapter {
 
     override var source_order: Int = 0
 
+    override var last_modified: Long = 0
+
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (other == null || javaClass != other.javaClass) return false
diff --git a/app/src/main/java/exh/eh/EHentaiUpdateHelper.kt b/app/src/main/java/exh/eh/EHentaiUpdateHelper.kt
index 00c1ff9ea..43437d28e 100644
--- a/app/src/main/java/exh/eh/EHentaiUpdateHelper.kt
+++ b/app/src/main/java/exh/eh/EHentaiUpdateHelper.kt
@@ -246,6 +246,7 @@ class EHentaiUpdateHelper(context: Context) {
                         chapterNumber = -1F,
                         scanlator = null,
                         sourceOrder = -1,
+                        lastModifiedAt = 0,
                     )
                 }
             }
diff --git a/data/src/main/java/tachiyomi/data/LibraryQuery.kt b/data/src/main/java/tachiyomi/data/LibraryQuery.kt
index eeb8a2396..33fc08c11 100644
--- a/data/src/main/java/tachiyomi/data/LibraryQuery.kt
+++ b/data/src/main/java/tachiyomi/data/LibraryQuery.kt
@@ -30,13 +30,15 @@ private val mapper = { cursor: SqlCursor ->
         filtered_scanlators = cursor.getString(18)?.let(listOfStringsAndAdapter::decode),
         update_strategy = updateStrategyAdapter.decode(cursor.getLong(19)!!),
         calculate_interval = cursor.getLong(20)!!,
-        totalCount = cursor.getLong(21)!!,
-        readCount = cursor.getLong(22)!!,
-        latestUpload = cursor.getLong(23)!!,
-        chapterFetchedAt = cursor.getLong(24)!!,
-        lastRead = cursor.getLong(25)!!,
-        bookmarkCount = cursor.getLong(26)!!,
-        category = cursor.getLong(27)!!,
+        last_modified_at = cursor.getLong(21)!!,
+        favorite_modified_at = cursor.getLong(22)!!,
+        totalCount = cursor.getLong(23)!!,
+        readCount = cursor.getLong(24)!!,
+        latestUpload = cursor.getLong(25)!!,
+        chapterFetchedAt = cursor.getLong(26)!!,
+        lastRead = cursor.getLong(27)!!,
+        bookmarkCount = cursor.getLong(28)!!,
+        category = cursor.getLong(29)!!,
     )
 }
 
diff --git a/data/src/main/java/tachiyomi/data/chapter/ChapterMapper.kt b/data/src/main/java/tachiyomi/data/chapter/ChapterMapper.kt
index 7b1888205..b91ccd7b4 100644
--- a/data/src/main/java/tachiyomi/data/chapter/ChapterMapper.kt
+++ b/data/src/main/java/tachiyomi/data/chapter/ChapterMapper.kt
@@ -2,8 +2,8 @@ package tachiyomi.data.chapter
 
 import tachiyomi.domain.chapter.model.Chapter
 
-val chapterMapper: (Long, Long, String, String, String?, Boolean, Boolean, Long, Float, Long, Long, Long) -> Chapter =
-    { id, mangaId, url, name, scanlator, read, bookmark, lastPageRead, chapterNumber, sourceOrder, dateFetch, dateUpload ->
+val chapterMapper: (Long, Long, String, String, String?, Boolean, Boolean, Long, Float, Long, Long, Long, Long) -> Chapter =
+    { id, mangaId, url, name, scanlator, read, bookmark, lastPageRead, chapterNumber, sourceOrder, dateFetch, dateUpload, lastModifiedAt ->
         Chapter(
             id = id,
             mangaId = mangaId,
@@ -17,5 +17,6 @@ val chapterMapper: (Long, Long, String, String, String?, Boolean, Boolean, Long,
             dateUpload = dateUpload,
             chapterNumber = chapterNumber,
             scanlator = scanlator,
+            lastModifiedAt = lastModifiedAt,
         )
     }
diff --git a/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt b/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt
index b40c98422..792604468 100644
--- a/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt
+++ b/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt
@@ -5,8 +5,8 @@ import tachiyomi.domain.library.model.LibraryManga
 import tachiyomi.domain.manga.model.Manga
 import tachiyomi.view.LibraryView
 
-val mangaMapper: (Long, Long, String, String?, String?, String?, List<String>?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long, List<String>?, UpdateStrategy, Long) -> Manga =
-    { id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, nextUpdate, initialized, viewerFlags, chapterFlags, coverLastModified, dateAdded, filteredScanlators, updateStrategy, calculateInterval ->
+val mangaMapper: (Long, Long, String, String?, String?, String?, List<String>?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long, List<String>?, UpdateStrategy, Long, Long, Long?) -> Manga =
+    { id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, nextUpdate, initialized, viewerFlags, chapterFlags, coverLastModified, dateAdded, filteredScanlators, updateStrategy, calculateInterval, lastModifiedAt, favoriteModifiedAt ->
         Manga(
             id = id,
             source = source,
@@ -33,11 +33,13 @@ val mangaMapper: (Long, Long, String, String?, String?, String?, List<String>?,
             // SY -->
             filteredScanlators = filteredScanlators,
             // SY <--
+            lastModifiedAt = lastModifiedAt,
+            favoriteModifiedAt = favoriteModifiedAt,
         )
     }
 
-val libraryManga: (Long, Long, String, String?, String?, String?, List<String>?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long, List<String>?, UpdateStrategy, Long, Long, Long, Long, Long, Long, Long, Long) -> LibraryManga =
-    { id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, nextUpdate, initialized, viewerFlags, chapterFlags, coverLastModified, dateAdded, filteredScanlators, updateStrategy, calculateInterval, totalCount, readCount, latestUpload, chapterFetchedAt, lastRead, bookmarkCount, category ->
+val libraryManga: (Long, Long, String, String?, String?, String?, List<String>?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long, List<String>?, UpdateStrategy, Long, Long, Long?, Long, Long, Long, Long, Long, Long, Long) -> LibraryManga =
+    { id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, nextUpdate, initialized, viewerFlags, chapterFlags, coverLastModified, dateAdded, filteredScanlators, updateStrategy, calculateInterval, lastModifiedAt, favoriteModifiedAt, totalCount, readCount, latestUpload, chapterFetchedAt, lastRead, bookmarkCount, category ->
         LibraryManga(
             manga = mangaMapper(
                 id,
@@ -63,6 +65,8 @@ val libraryManga: (Long, Long, String, String?, String?, String?, List<String>?,
                 // SY <--
                 updateStrategy,
                 calculateInterval,
+                lastModifiedAt,
+                favoriteModifiedAt,
             ),
             category = category,
             totalChapters = totalCount,
@@ -98,6 +102,8 @@ val libraryViewMapper: (LibraryView) -> LibraryManga = {
             initialized = it.initialized,
             filteredScanlators = it.filtered_scanlators,
             calculateInterval = it.calculate_interval.toInt(),
+            lastModifiedAt = it.last_modified_at,
+            favoriteModifiedAt = it.favorite_modified_at,
         ),
         it.category,
         it.totalCount,
diff --git a/data/src/main/sqldelight/tachiyomi/data/chapters.sq b/data/src/main/sqldelight/tachiyomi/data/chapters.sq
index 310939645..ee986c9b5 100644
--- a/data/src/main/sqldelight/tachiyomi/data/chapters.sq
+++ b/data/src/main/sqldelight/tachiyomi/data/chapters.sq
@@ -11,6 +11,7 @@ CREATE TABLE chapters(
     source_order INTEGER NOT NULL,
     date_fetch INTEGER AS Long NOT NULL,
     date_upload INTEGER AS Long NOT NULL,
+    last_modified_at INTEGER AS Long NOT NULL,
     FOREIGN KEY(manga_id) REFERENCES mangas (_id)
     ON DELETE CASCADE
 );
@@ -18,6 +19,15 @@ CREATE TABLE chapters(
 CREATE INDEX chapters_manga_id_index ON chapters(manga_id);
 CREATE INDEX chapters_unread_by_manga_index ON chapters(manga_id, read) WHERE read = 0;
 
+CREATE TRIGGER update_last_modified_at_chapters
+AFTER UPDATE ON chapters
+FOR EACH ROW
+BEGIN
+  UPDATE chapters
+  SET last_modified_at = strftime('%s', 'now')
+  WHERE _id = new._id;
+END;
+
 getChapterById:
 SELECT *
 FROM chapters
@@ -58,8 +68,8 @@ DELETE FROM chapters
 WHERE _id IN :chapterIds;
 
 insert:
-INSERT INTO chapters(manga_id,url,name,scanlator,read,bookmark,last_page_read,chapter_number,source_order,date_fetch,date_upload)
-VALUES (:mangaId,:url,:name,:scanlator,:read,:bookmark,:lastPageRead,:chapterNumber,:sourceOrder,:dateFetch,:dateUpload);
+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, strftime('%s', 'now'));
 
 update:
 UPDATE chapters
diff --git a/data/src/main/sqldelight/tachiyomi/data/mangas.sq b/data/src/main/sqldelight/tachiyomi/data/mangas.sq
index 33fdde0a8..204d69161 100644
--- a/data/src/main/sqldelight/tachiyomi/data/mangas.sq
+++ b/data/src/main/sqldelight/tachiyomi/data/mangas.sq
@@ -23,12 +23,31 @@ CREATE TABLE mangas(
     date_added INTEGER AS Long NOT NULL,
     filtered_scanlators TEXT AS List<String>,
     update_strategy INTEGER AS UpdateStrategy NOT NULL DEFAULT 0,
-    calculate_interval INTEGER DEFAULT 0 NOT NULL
+    calculate_interval INTEGER DEFAULT 0 NOT NULL,
+    last_modified_at INTEGER AS Long NOT NULL,
+    favorite_modified_at INTEGER AS Long
 );
 
 CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1;
 CREATE INDEX mangas_url_index ON mangas(url);
 
+CREATE TRIGGER update_favorite_modified_at_mangas
+AFTER UPDATE OF favorite ON mangas
+BEGIN
+  UPDATE mangas
+  SET favorite_modified_at = strftime('%s', 'now')
+  WHERE _id = new._id;
+END;
+
+CREATE TRIGGER update_last_modified_at_mangas
+AFTER UPDATE ON mangas
+FOR EACH ROW
+BEGIN
+  UPDATE mangas
+  SET last_modified_at = strftime('%s', 'now')
+  WHERE _id = new._id;
+END;
+
 getMangaById:
 SELECT *
 FROM mangas
@@ -53,6 +72,15 @@ WHERE favorite = 0 AND _id IN(
     SELECT chapters.manga_id FROM chapters WHERE read = 1 OR last_page_read != 0
 );
 
+getAllManga:
+SELECT *
+FROM mangas;
+
+getMangasWithFavoriteTimestamp:
+SELECT *
+FROM mangas
+WHERE favorite_modified_at IS NOT NULL;
+
 getSourceIdWithFavoriteCount:
 SELECT
 source,
@@ -99,8 +127,8 @@ WHERE favorite = 0 AND source IN :sourceIdsAND AND _id NOT IN (
 );
 
 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,filtered_scanlators,update_strategy,calculate_interval)
-VALUES (:source,:url,:artist,:author,:description,:genre,:title,:status,:thumbnailUrl,:favorite,:lastUpdate,:nextUpdate,:initialized,:viewerFlags,:chapterFlags,:coverLastModified,:dateAdded,:filteredScanlators,:updateStrategy,:calculateInterval);
+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,filtered_scanlators,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,:filteredScanlators,:updateStrategy,:calculateInterval, strftime('%s', 'now'));
 
 update:
 UPDATE mangas SET
diff --git a/data/src/main/sqldelight/tachiyomi/data/mangas_categories.sq b/data/src/main/sqldelight/tachiyomi/data/mangas_categories.sq
index a97c9d3ca..c10387a6a 100644
--- a/data/src/main/sqldelight/tachiyomi/data/mangas_categories.sq
+++ b/data/src/main/sqldelight/tachiyomi/data/mangas_categories.sq
@@ -2,15 +2,25 @@ CREATE TABLE mangas_categories(
     _id INTEGER NOT NULL PRIMARY KEY,
     manga_id INTEGER NOT NULL,
     category_id INTEGER NOT NULL,
+    last_modified_at INTEGER AS Long NOT NULL,
     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
+BEGIN
+  UPDATE mangas_categories
+  SET last_modified_at = strftime('%s', 'now')
+  WHERE _id = new._id;
+END;
+
 insert:
-INSERT INTO mangas_categories(manga_id, category_id)
-VALUES (:mangaId, :categoryId);
+INSERT INTO mangas_categories(manga_id, category_id, last_modified_at)
+VALUES (:mangaId, :categoryId, strftime('%s', 'now'));
 
 deleteMangaCategoryByMangaId:
 DELETE FROM mangas_categories
diff --git a/data/src/main/sqldelight/tachiyomi/migrations/28.sqm b/data/src/main/sqldelight/tachiyomi/migrations/28.sqm
new file mode 100644
index 000000000..b4d98546a
--- /dev/null
+++ b/data/src/main/sqldelight/tachiyomi/migrations/28.sqm
@@ -0,0 +1,49 @@
+ALTER TABLE mangas ADD COLUMN last_modified_at INTEGER AS Long NOT NULL;
+ALTER TABLE mangas ADD COLUMN favorite_modified_at INTEGER AS Long;
+ALTER TABLE mangas_categories ADD COLUMN last_modified_at INTEGER AS Long NOT NULL;
+ALTER TABLE chapters ADD COLUMN last_modified_at INTEGER AS Long NOT NULL;
+
+UPDATE mangas SET last_modified_at = strftime('%s', 'now');
+UPDATE mangas SET favorite_modified_at = strftime('%s', 'now') WHERE favorite = 1;
+UPDATE mangas_categories SET last_modified_at = strftime('%s', 'now');
+UPDATE chapters SET last_modified_at = strftime('%s', 'now');
+
+-- Create triggers
+DROP TRIGGER IF EXISTS update_last_modified_at_mangas;
+CREATE TRIGGER update_last_modified_at_mangas
+AFTER UPDATE ON mangas
+FOR EACH ROW
+BEGIN
+  UPDATE mangas
+  SET last_modified_at = strftime('%s', 'now')
+  WHERE _id = new._id;
+END;
+
+DROP TRIGGER IF EXISTS update_favorite_modified_at_mangas;
+CREATE TRIGGER update_last_favorited_at_mangas
+AFTER UPDATE OF favorite ON mangas
+BEGIN
+  UPDATE mangas
+  SET favorite_modified_at = strftime('%s', 'now')
+  WHERE _id = new._id;
+END;
+
+DROP TRIGGER IF EXISTS update_last_modified_at_chapters;
+CREATE TRIGGER update_last_modified_at_chapters
+AFTER UPDATE ON chapters
+FOR EACH ROW
+BEGIN
+  UPDATE chapters
+  SET last_modified_at = strftime('%s', 'now')
+  WHERE _id = new._id;
+END;
+
+DROP TRIGGER IF EXISTS update_last_modified_at_mangas_categories;
+CREATE TRIGGER update_last_modified_at_mangas_categories
+AFTER UPDATE ON mangas_categories
+FOR EACH ROW
+BEGIN
+  UPDATE mangas_categories
+  SET last_modified_at = strftime('%s', 'now')
+  WHERE _id = new._id;
+END;
\ No newline at end of file
diff --git a/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt b/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt
index e11ca6564..9adee3f1b 100644
--- a/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt
+++ b/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt
@@ -13,6 +13,7 @@ data class Chapter(
     val dateUpload: Long,
     val chapterNumber: Float,
     val scanlator: String?,
+    val lastModifiedAt: Long,
 ) {
     val isRecognizedNumber: Boolean
         get() = chapterNumber >= 0f
@@ -31,6 +32,7 @@ data class Chapter(
             dateUpload = -1,
             chapterNumber = -1f,
             scanlator = null,
+            lastModifiedAt = 0,
         )
     }
 }
diff --git a/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt b/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt
index a95dbff8b..a4d3699f5 100644
--- a/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt
+++ b/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt
@@ -28,6 +28,8 @@ data class Manga(
     val thumbnailUrl: String?,
     val updateStrategy: UpdateStrategy,
     val initialized: Boolean,
+    val lastModifiedAt: Long,
+    val favoriteModifiedAt: Long?,
     // SY -->
     val filteredScanlators: List<String>?,
     // SY <--
@@ -146,6 +148,8 @@ data class Manga(
             thumbnailUrl = null,
             updateStrategy = UpdateStrategy.ALWAYS_UPDATE,
             initialized = false,
+            lastModifiedAt = 0L,
+            favoriteModifiedAt = 0L,
             // SY -->
             filteredScanlators = null,
             // SY <--