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>
This commit is contained in:
Naji Astier
2026-08-01 17:26:24 +02:00
committed by GitHub
parent 4296d056b5
commit c114028a33
5 changed files with 29 additions and 8 deletions
+1
View File
@@ -33,6 +33,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
- 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))
- Fixed MangaBaka User Agent string ([@MajorTanya](https://github.com/MajorTanya)) ([#3578](https://github.com/mihonapp/mihon/pull/3578))
- Fixed extension installation with shizuku installer ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#3630](https://github.com/mihonapp/mihon/pull/3630))
- 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))
## [v0.20.1] - 2026-07-09
### Added
@@ -148,13 +148,30 @@ class BackupRestorer(
mangaRestorer.sortByNew(backupMangas)
.chunked(100)
.forEach { chunk ->
database.transaction {
val restoredAsBatch = try {
database.transaction {
chunk.forEach {
ensureActive()
mangaRestorer.restore(it, backupCategories)
}
}
true
} catch (e: Exception) {
ensureActive()
logcat(LogPriority.WARN, e) { "Batch restore failed, retrying entry by entry" }
false
}
if (restoredAsBatch) {
restoreProgress.addAndFetch(chunk.size)
} else {
chunk.forEach {
ensureActive()
try {
mangaRestorer.restore(it, backupCategories)
} catch (e: Exception) {
ensureActive()
val sourceName = sourceMapping[it.source] ?: it.source.toString()
errors.add(Date() to "${it.title} [$sourceName]: ${e.message}")
}
@@ -162,6 +179,7 @@ class BackupRestorer(
restoreProgress.incrementAndFetch()
}
}
notifier.showRestoreProgress(chunk.last().title, restoreProgress.load(), restoreAmount, isSync)
}
}
@@ -284,7 +284,7 @@ class MangaRestorer(
restoreCategories(manga, categories, backupCategories)
restoreChapters(manga, chapters)
restoreTracking(manga, tracks)
restoreHistory(history)
restoreHistory(manga, history)
restoreExcludedScanlators(manga, excludedScanlators)
updateManga.awaitUpdateFetchInterval(manga, now, currentFetchWindow)
return manga
@@ -324,16 +324,16 @@ class MangaRestorer(
}
}
private suspend fun restoreHistory(backupHistory: List<BackupHistory>) {
private suspend fun restoreHistory(manga: Manga, backupHistory: List<BackupHistory>) {
val toUpdate = backupHistory.mapNotNull { history ->
val dbHistory = database.historyQueries
.getHistoryByChapterUrl(history.url)
.getHistoryByChapterUrlAndMangaId(history.url, manga.id)
.awaitAsOneOrNull()
val item = history.getHistoryImpl()
if (dbHistory == null) {
val chapter = database.chaptersQueries
.getChapterByUrl(history.url)
.getChapterByUrlAndMangaId(history.url, manga.id)
.awaitAsOneOrNull()
return@mapNotNull if (chapter == null) {
// Chapter doesn't exist; skip
@@ -88,7 +88,8 @@ getChapterByUrlAndMangaId:
SELECT *
FROM chapters
WHERE url = :chapterUrl
AND manga_id = :mangaId;
AND manga_id = :mangaId
LIMIT 1;
removeChaptersWithIds:
DELETE FROM chapters
@@ -23,7 +23,7 @@ JOIN chapters C
ON H.chapter_id = C._id
WHERE C.manga_id = :mangaId AND C._id = H.chapter_id;
getHistoryByChapterUrl:
getHistoryByChapterUrlAndMangaId:
SELECT
H._id,
H.chapter_id,
@@ -32,7 +32,8 @@ H.time_read
FROM history H
JOIN chapters C
ON H.chapter_id = C._id
WHERE C.url = :chapterUrl AND C._id = H.chapter_id;
WHERE C.url = :chapterUrl AND C.manga_id = :mangaId AND C._id = H.chapter_id
LIMIT 1;
resetHistoryById:
UPDATE history