feat: merge books into one record

Files move on disk before any row moves, then everything repoints in one
transaction and the folded records are deleted. Progress keeps whichever is
furthest, shelves and tags union, and identifiers move only under a name the
survivor lacks. Metadata is left alone unless the caller resolves it, since
choosing between two titles is a judgement this endpoint cannot make. Nothing
is removed from disk.
This commit is contained in:
2026-08-16 20:21:10 -04:00
parent a48be517e4
commit 22539644a9
6 changed files with 632 additions and 1 deletions
+49
View File
@@ -706,6 +706,55 @@ class TestDuplicateBooks:
response = await authenticated_client.get("/books/duplicate-books?library_id=1")
assert len(response.json()) == 1
async def test_two_books_merge_into_one(
self, authenticated_client: AsyncClient
) -> None:
"""The survivor keeps its id and gains the other's file; the other is gone."""
first = await authenticated_client.post(
"/books/fromFiles?library_id=1", files=self.upload("first.epub")
)
second = await authenticated_client.post(
"/books/fromFiles?library_id=1", files=self.upload("second.epub", pad=True)
)
keep = first.json()["created"][0]
fold = second.json()["created"][0]
response = await authenticated_client.post(
"/books/merge?library_id=1",
json={
"survivor_id": keep["id"],
"merged_ids": [fold["id"]],
"metadata": {"title": "Metamorphosis", "edition": 2},
},
)
assert response.status_code == 201
merged = response.json()
assert merged["id"] == keep["id"]
assert merged["edition"] == 2
assert len(merged["files"]) == 2
# The folded record is gone, and the group it formed with it.
assert (await authenticated_client.get(f"/books/{fold['id']}")).status_code == 404
groups = await authenticated_client.get("/books/duplicate-books?library_id=1")
assert groups.json() == []
async def test_merging_an_unknown_book_is_refused(
self, authenticated_client: AsyncClient
) -> None:
created = await authenticated_client.post(
"/books/fromFiles?library_id=1", files=self.upload("first.epub")
)
keep = created.json()["created"][0]["id"]
response = await authenticated_client.post(
"/books/merge?library_id=1",
json={"survivor_id": keep, "merged_ids": [9999]},
)
assert response.status_code == 400
async def test_dismissing_an_unknown_book_is_refused(
self, authenticated_client: AsyncClient
) -> None: