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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user