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 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 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 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
|
## [v0.20.1] - 2026-07-09
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -148,13 +148,30 @@ class BackupRestorer(
|
|||||||
mangaRestorer.sortByNew(backupMangas)
|
mangaRestorer.sortByNew(backupMangas)
|
||||||
.chunked(100)
|
.chunked(100)
|
||||||
.forEach { chunk ->
|
.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 {
|
chunk.forEach {
|
||||||
ensureActive()
|
ensureActive()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mangaRestorer.restore(it, backupCategories)
|
mangaRestorer.restore(it, backupCategories)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
ensureActive()
|
||||||
val sourceName = sourceMapping[it.source] ?: it.source.toString()
|
val sourceName = sourceMapping[it.source] ?: it.source.toString()
|
||||||
errors.add(Date() to "${it.title} [$sourceName]: ${e.message}")
|
errors.add(Date() to "${it.title} [$sourceName]: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -162,6 +179,7 @@ class BackupRestorer(
|
|||||||
restoreProgress.incrementAndFetch()
|
restoreProgress.incrementAndFetch()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
notifier.showRestoreProgress(chunk.last().title, restoreProgress.load(), restoreAmount, isSync)
|
notifier.showRestoreProgress(chunk.last().title, restoreProgress.load(), restoreAmount, isSync)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -284,7 +284,7 @@ class MangaRestorer(
|
|||||||
restoreCategories(manga, categories, backupCategories)
|
restoreCategories(manga, categories, backupCategories)
|
||||||
restoreChapters(manga, chapters)
|
restoreChapters(manga, chapters)
|
||||||
restoreTracking(manga, tracks)
|
restoreTracking(manga, tracks)
|
||||||
restoreHistory(history)
|
restoreHistory(manga, history)
|
||||||
restoreExcludedScanlators(manga, excludedScanlators)
|
restoreExcludedScanlators(manga, excludedScanlators)
|
||||||
updateManga.awaitUpdateFetchInterval(manga, now, currentFetchWindow)
|
updateManga.awaitUpdateFetchInterval(manga, now, currentFetchWindow)
|
||||||
return manga
|
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 toUpdate = backupHistory.mapNotNull { history ->
|
||||||
val dbHistory = database.historyQueries
|
val dbHistory = database.historyQueries
|
||||||
.getHistoryByChapterUrl(history.url)
|
.getHistoryByChapterUrlAndMangaId(history.url, manga.id)
|
||||||
.awaitAsOneOrNull()
|
.awaitAsOneOrNull()
|
||||||
val item = history.getHistoryImpl()
|
val item = history.getHistoryImpl()
|
||||||
|
|
||||||
if (dbHistory == null) {
|
if (dbHistory == null) {
|
||||||
val chapter = database.chaptersQueries
|
val chapter = database.chaptersQueries
|
||||||
.getChapterByUrl(history.url)
|
.getChapterByUrlAndMangaId(history.url, manga.id)
|
||||||
.awaitAsOneOrNull()
|
.awaitAsOneOrNull()
|
||||||
return@mapNotNull if (chapter == null) {
|
return@mapNotNull if (chapter == null) {
|
||||||
// Chapter doesn't exist; skip
|
// Chapter doesn't exist; skip
|
||||||
|
|||||||
@@ -88,7 +88,8 @@ getChapterByUrlAndMangaId:
|
|||||||
SELECT *
|
SELECT *
|
||||||
FROM chapters
|
FROM chapters
|
||||||
WHERE url = :chapterUrl
|
WHERE url = :chapterUrl
|
||||||
AND manga_id = :mangaId;
|
AND manga_id = :mangaId
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
removeChaptersWithIds:
|
removeChaptersWithIds:
|
||||||
DELETE FROM chapters
|
DELETE FROM chapters
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ JOIN chapters C
|
|||||||
ON H.chapter_id = C._id
|
ON H.chapter_id = C._id
|
||||||
WHERE C.manga_id = :mangaId AND C._id = H.chapter_id;
|
WHERE C.manga_id = :mangaId AND C._id = H.chapter_id;
|
||||||
|
|
||||||
getHistoryByChapterUrl:
|
getHistoryByChapterUrlAndMangaId:
|
||||||
SELECT
|
SELECT
|
||||||
H._id,
|
H._id,
|
||||||
H.chapter_id,
|
H.chapter_id,
|
||||||
@@ -32,7 +32,8 @@ H.time_read
|
|||||||
FROM history H
|
FROM history H
|
||||||
JOIN chapters C
|
JOIN chapters C
|
||||||
ON H.chapter_id = C._id
|
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:
|
resetHistoryById:
|
||||||
UPDATE history
|
UPDATE history
|
||||||
|
|||||||
Reference in New Issue
Block a user