Files
bookhoard/docs/developer/api/admin/hash-conflicts.md
T
john-okeefe ffcdab36a0 docs(api): document hash-conflict resolution endpoints
Cover the admin API added in 03cb4c7 for duplicate-content decisions:

- GET /api/admin/hash-conflicts — pending conflict groups with member
  items and per-item usage counts (progress, highlights, bookmarks,
  notes, collections)
- POST /api/admin/hash-conflicts/:id/resolve — action=keep (merge child
  rows into keep_uuid, delete losers) vs action=keep_all (dismiss);
  JSON and form-encoded bodies, error codes including 409 for already
  resolved
- When conflicts are created (startup backfill, rescans) and the
  guarantee that files on disk are never deleted
2026-08-20 14:40:50 -04:00

112 lines
3.6 KiB
Markdown

# Hash Conflicts API
## Overview
When Bookhoard hashes your library (on import, rescan, or the startup backfill), two media items in the same library with the same `file_sha256` indicate duplicate content. Each duplicate group is recorded as a **hash conflict** and exposed here for an explicit keep/merge decision. Conflicts are also surfaced in the admin UI's Hash Conflicts page.
**Authentication**: Admin JWT token required
**Content-Type**: `application/json` (resolve also accepts form-encoded bodies for htmx)
---
## Endpoints
### List Hash Conflicts
List all pending conflict groups, each with its member items and per-item usage counts (reading progress, highlights, bookmarks, notes, collections) to help decide which copy to keep.
**Endpoint**: `GET /api/admin/hash-conflicts`
**Response**: **200 OK**
```json
{
"conflicts": [
{
"id": "conflict-uuid",
"library_id": "library-uuid",
"library_name": "Ebooks",
"sha256": "abc123...",
"created_at": "2026-08-14T12:00:00Z",
"items": [
{
"id": "media-item-uuid",
"title": "The Hobbit",
"author": "J. R. R. Tolkien",
"file_path": "/books/hobbit.epub",
"file_size": 1048576,
"created_at": "2026-01-01T00:00:00Z",
"progress_count": 2,
"highlight_count": 12,
"bookmark_count": 3,
"note_count": 1,
"collection_count": 2
}
]
}
],
"total": 1
}
```
**Example**:
```bash
curl -X GET https://bookhoard.example.com/api/admin/hash-conflicts \
-H "Authorization: Bearer <admin_token>"
```
---
### Resolve Hash Conflict
Resolve one conflict group.
**Endpoint**: `POST /api/admin/hash-conflicts/{id}/resolve`
**Request Body** (JSON or form-encoded):
```json
{
"action": "keep",
"keep_uuid": "media-item-uuid-to-keep"
}
```
| Field | Type | Required | Description |
| ----------- | ------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `action` | string | Yes | `keep_all` — both copies are intentional; dismiss the conflict. `keep` — keep `keep_uuid` and delete the other copies. |
| `keep_uuid` | string | for `action=keep` | The media item UUID to keep. Must belong to this conflict group. With `keep`, every other copy's child rows (progress, highlights, bookmarks, notes, collections, …) are merged into the kept item before the losers are deleted. |
**Responses**:
- `200 OK` — resolved (body is an HTML confirmation snippet for the admin UI page)
- `400 Bad Request` — invalid conflict ID, missing `keep_uuid`, or `keep_uuid` not in the group
- `404 Not Found` — conflict doesn't exist
- `409 Conflict` — conflict already resolved
**Example**:
```bash
curl -X POST https://bookhoard.example.com/api/admin/hash-conflicts/<id>/resolve \
-H "Authorization: Bearer <admin_token>" \
-H "Content-Type: application/json" \
-d '{"action": "keep", "keep_uuid": "media-item-uuid"}'
```
---
## When Conflicts Are Created
- **Startup backfill**: items imported before hashing existed are hashed automatically ~30s after startup; duplicates discovered land here.
- **Rescan**: hashes are recomputed and content duplicates are flagged.
Files on disk are never deleted — resolution only affects database rows.
---
## Related Endpoints
- [System Settings API](../system/settings.md) — scanning configuration
- [Scanner API](../scanner/) — triggering scans and watch mode