Skip to content

Commit c114028

Browse files
authored
Fix backup restore dropping library entries (#3667)
* Fix backup restore dropping library entries History restore resolved chapters by URL alone, so a backup holding the same entry twice under one source matched multiple chapter rows and threw `ResultSet returned more than 1 row`. Scope the lookups to the entry. Entries are restored in chunks of 100 sharing one transaction. SQLDelight fails the enclosing transaction when a nested one fails, so catching the per-entry exception did not contain it and the whole chunk rolled back, losing up to 100 entries per bad one. Retry entry by entry on failure. Refs #647 * Update changelog --------- Co-authored-by: Naji Astier <na-ji@users.noreply.github.com>
1 parent 4296d05 commit c114028

5 files changed

Lines changed: 29 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
3333
- Fixed app bars remaining visible after changing pages by tapping in the paged reader after using the chapter navigator slider ([@AntsyLich](https://github.com/AntsyLich)) ([#3567](https://github.com/mihonapp/mihon/pull/3567))
3434
- Fixed MangaBaka User Agent string ([@MajorTanya](https://github.com/MajorTanya)) ([#3578](https://github.com/mihonapp/mihon/pull/3578))
3535
- Fixed extension installation with shizuku installer ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#3630](https://github.com/mihonapp/mihon/pull/3630))
36+
- Fixed backup restore dropping library entries when the backup contains duplicate chapters ([@na-ji](https://github.com/na-ji)) ([#3667](https://github.com/mihonapp/mihon/pull/3667))
3637

3738
## [v0.20.1] - 2026-07-09
3839
### Added

app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,20 +148,38 @@ class BackupRestorer(
148148
mangaRestorer.sortByNew(backupMangas)
149149
.chunked(100)
150150
.forEach { chunk ->
151-
database.transaction {
151+
val restoredAsBatch = try {
152+
database.transaction {
153+
chunk.forEach {
154+
ensureActive()
155+
mangaRestorer.restore(it, backupCategories)
156+
}
157+
}
158+
true
159+
} catch (e: Exception) {
160+
ensureActive()
161+
logcat(LogPriority.WARN, e) { "Batch restore failed, retrying entry by entry" }
162+
false
163+
}
164+
165+
if (restoredAsBatch) {
166+
restoreProgress.addAndFetch(chunk.size)
167+
} else {
152168
chunk.forEach {
153169
ensureActive()
154170

155171
try {
156172
mangaRestorer.restore(it, backupCategories)
157173
} catch (e: Exception) {
174+
ensureActive()
158175
val sourceName = sourceMapping[it.source] ?: it.source.toString()
159176
errors.add(Date() to "${it.title} [$sourceName]: ${e.message}")
160177
}
161178

162179
restoreProgress.incrementAndFetch()
163180
}
164181
}
182+
165183
notifier.showRestoreProgress(chunk.last().title, restoreProgress.load(), restoreAmount, isSync)
166184
}
167185
}

app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ class MangaRestorer(
284284
restoreCategories(manga, categories, backupCategories)
285285
restoreChapters(manga, chapters)
286286
restoreTracking(manga, tracks)
287-
restoreHistory(history)
287+
restoreHistory(manga, history)
288288
restoreExcludedScanlators(manga, excludedScanlators)
289289
updateManga.awaitUpdateFetchInterval(manga, now, currentFetchWindow)
290290
return manga
@@ -324,16 +324,16 @@ class MangaRestorer(
324324
}
325325
}
326326

327-
private suspend fun restoreHistory(backupHistory: List<BackupHistory>) {
327+
private suspend fun restoreHistory(manga: Manga, backupHistory: List<BackupHistory>) {
328328
val toUpdate = backupHistory.mapNotNull { history ->
329329
val dbHistory = database.historyQueries
330-
.getHistoryByChapterUrl(history.url)
330+
.getHistoryByChapterUrlAndMangaId(history.url, manga.id)
331331
.awaitAsOneOrNull()
332332
val item = history.getHistoryImpl()
333333

334334
if (dbHistory == null) {
335335
val chapter = database.chaptersQueries
336-
.getChapterByUrl(history.url)
336+
.getChapterByUrlAndMangaId(history.url, manga.id)
337337
.awaitAsOneOrNull()
338338
return@mapNotNull if (chapter == null) {
339339
// Chapter doesn't exist; skip

data/src/main/sqldelight/tachiyomi/data/chapters.sq

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ getChapterByUrlAndMangaId:
8888
SELECT *
8989
FROM chapters
9090
WHERE url = :chapterUrl
91-
AND manga_id = :mangaId;
91+
AND manga_id = :mangaId
92+
LIMIT 1;
9293

9394
removeChaptersWithIds:
9495
DELETE FROM chapters

data/src/main/sqldelight/tachiyomi/data/history.sq

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ JOIN chapters C
2323
ON H.chapter_id = C._id
2424
WHERE C.manga_id = :mangaId AND C._id = H.chapter_id;
2525

26-
getHistoryByChapterUrl:
26+
getHistoryByChapterUrlAndMangaId:
2727
SELECT
2828
H._id,
2929
H.chapter_id,
@@ -32,7 +32,8 @@ H.time_read
3232
FROM history H
3333
JOIN chapters C
3434
ON H.chapter_id = C._id
35-
WHERE C.url = :chapterUrl AND C._id = H.chapter_id;
35+
WHERE C.url = :chapterUrl AND C.manga_id = :mangaId AND C._id = H.chapter_id
36+
LIMIT 1;
3637

3738
resetHistoryById:
3839
UPDATE history

0 commit comments

Comments
 (0)