Files
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

3.6 KiB

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

{
  "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:

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):

{
  "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:

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.