feat(collections): merge archived duplicates into their active copy
Covers the copy-then-delete-later workflow: a user copies books to a new
library, deletes the originals, and ends up with an active copy in the new
library plus an archived twin holding the real reading history. Until now
those twins could only be purged (destroying the history) or restored
(showing a permanently broken entry).
- POST /api/media-items/:id/merge (admin only, body {target_id}):
validates the source is archived/missing, the target is active, and
both share the same file_sha256; then re-parents every child row onto
the target via the existing reparent_media_item_children function (the
same machinery as hash-conflict resolution) and deletes the source row.
Per-user collisions keep the active copy's data, mirroring that flow.
Affected data: reading progress, speed, ratings, highlights, notes,
bookmarks (including tombstoned deleted-annotation history), formats,
collections, kobo shelves/entitlements, sync rows, panel data,
processing issues, and device aliases.
- GET /admin/archived: ListHiddenMediaItems' match_* columns surface each
row's best active twin; rows with a twin get a confirm-guarded "Merge"
button (data-merge-source/-target) next to Restore/Delete, wired in
web/src/admin.ts like the existing unarchive/delete handlers.
- templates.ArchivedItem gains MatchID/MatchTitle/MatchLibraryName;
frontend.go populates them from the listing row.
After a merge the archived row is gone, so the retention purge can never
destroy the merged data. Deleted-annotation tombstones carry over and
remain restorable from the target book's "recently deleted" history.
This commit is contained in:
+16
-10
@@ -52,17 +52,23 @@ type FolderData struct {
|
||||
}
|
||||
|
||||
// ArchivedItem feeds the admin archived-items page: media hidden from
|
||||
// libraries because their files vanished, with their pending fate.
|
||||
// libraries because their files vanished, with their pending fate. When an
|
||||
// active item with identical content (same SHA-256) exists, MatchID and
|
||||
// friends are set so the UI can offer merging the archived row's reading
|
||||
// data into its active twin.
|
||||
type ArchivedItem struct {
|
||||
ID string
|
||||
Title string
|
||||
Author string
|
||||
LibraryName string
|
||||
FilePath string
|
||||
MissingScans int32
|
||||
ArchivedAt *time.Time
|
||||
PurgeAt *time.Time
|
||||
Status string
|
||||
ID string
|
||||
Title string
|
||||
Author string
|
||||
LibraryName string
|
||||
FilePath string
|
||||
MissingScans int32
|
||||
ArchivedAt *time.Time
|
||||
PurgeAt *time.Time
|
||||
Status string
|
||||
MatchID string
|
||||
MatchTitle string
|
||||
MatchLibraryName string
|
||||
}
|
||||
|
||||
type DirEntry struct {
|
||||
|
||||
Reference in New Issue
Block a user