john-okeefe 9b171a0060 fix(scanner): prevent duplicate media item imports
A read-then-write race in processMediaFile allowed the same file to be
imported twice: two concurrent scan jobs (startup scan, fsnotify dirty-
directory scan, periodic backup poll, or a manual scan each run on
separate worker goroutines with separate MediaScanner instances) could
both SELECT 'not found' and both INSERT. There was no transaction, no
row lock, no unique constraint on (library_id, file_path), and no
ON CONFLICT clause, so nothing stopped the double insert. Observed in
production as two identical 'Head First SQL' rows created in the same
second (same sha256, size, path, library).

Database enforcement:
- schema.sql: add UNIQUE(library_id, file_path) constraint, guarded so
  re-runs don't error
- schema.sql: add self-healing migration that runs on every startup -
  dedup_media_items_by_path() collapses existing path-duplicates and
  reparent_media_item_children() moves all child rows (progress,
  highlights, bookmarks, notes, collections, formats, aliases, kobo
  entitlements, etc.) onto a survivor before deleting losers, so the
  constraint applies cleanly on already-duplicated servers without
  losing reading history. Survivor picks the row with the most user
  data, ties broken by lowest id
- CreateMediaItem: upsert via ON CONFLICT (library_id, file_path) DO
  UPDATE so concurrent inserts collapse to one row and return it
- CreateMediaItemFormat: upsert via ON CONFLICT (media_item_id,
  format_type), closing the same race on format rows

Application-level guards:
- media_scanner processMediaFile: after computing the file hash, check
  GetMediaItemBySHA256AndLibrary (new query) and treat the file as
  existing when identical content is already in the library under a
  different path (content dedup, library-scoped so multi-library
  setups still work)

Ops tooling:
- scripts/dedup_media_items.sql: standalone idempotent maintenance
  script with a dry-run report (path + content duplicate groups, child
  row counts) and transactional cleanup, for servers that prefer to
  dedup manually before upgrading

Verified against the live database: the duplicate pair was collapsed
(reading_progress preserved on the survivor), schema.sql re-runs are a
no-op, and the constraint is in place with 62 unique books remaining.
2026-08-14 08:18:36 -04:00

📚 Bookhoard

A modern self-hosted media library system built with Go, PostgreSQL, HTMX, and Tailwind CSS featuring universal cross-device sync, beautiful dark themes, and comprehensive media management.

Why Bookhoard?

🔄 Universal Sync: Your reading progress, highlights, and notes sync automatically across all your devices - KOReader, Kobo, web, and mobile.

📱 Multi-Library: Organize your ebooks, comics, and manga with per-library folders and smart collections.

🎨 Beautiful UI: 11 gorgeous dark themes with responsive design that works on any device.

🔒 Secure: JWT authentication, bcrypt password hashing, rate limiting, and no passwords on devices.


🚀 Quick Start

Prerequisites

  • Podman (recommended) or Docker
  • 5 minutes of your time

Installation

# 1. Clone the repository
git clone https://git.linuxhg.com/Bookhoard/bookhoard.git
cd bookhoard

# 2. Set up environment
cp .env.example .env
# Generate secure passwords (no special characters):
#   JWT_SECRET: openssl rand -hex 32
#   DBPASS: openssl rand -hex 16
# Edit .env with your generated values

# 3. Pull images and start the server
docker compose pull
docker compose up -d
# Optionally pin a specific version: set IMAGE_TAG in .env (defaults to "latest")

# 4. Open your browser
open http://localhost:8765

The first user to register automatically becomes an admin.


📖 Key Features

Universal Cross-Platform Sync

  • Real-Time Progress: Turn a page on your Kindle, see it on your phone
  • Format-Aware: EPUB CFI, page numbers, percentages - all handled correctly
  • Offline Queue: Changes sync when you reconnect, priority-processed
  • Conflict Resolution: Smart handling when same book read on multiple devices
  • Book Matching: Automatic matching using SHA-256, ISBN, UUID
  • OPDS Catalog: Wireless book delivery to e-readers over Wi-Fi
  • Format Conversion: On-the-fly EPUB→KEPUB for Kobo devices

Media Management

  • Calibre Integration: Automatic metadata import from Calibre metadata.opf sidecar files
  • Smart Search: Partial matching with fuzzy search fallback for typos
  • Advanced Filtering: Filter by author, series, genre, language, year, cover images
  • Dynamic Sorting: By title, author, date added, published date, page count, series
  • Rich Metadata: Title, author, series, publisher, ISBN, language, edition, tags
  • 5-Star Ratings: Half-star precision (1-10 scale)
  • Notes & Highlights: Color-coded annotations with linked notes
  • Usage Analytics: Reading statistics, device usage, popular books

Smart Collections

  • Auto-Assign Rules: Automatically add books based on genre, author, series, tags, language, publisher, year
  • Device Shelf Mappings: Sync collections to Kobo shelves and KOReader categories
  • Test Before Creating: Preview which books match your rules

Library Organization

  • Multi-Library Support: Ebooks, Comics, and Manga with type-specific file formats
  • Multiple Folders: Add multiple scanning folders per library
  • Visibility Control: Admins control which libraries each user can see
  • Background Scanning: Auto-scan with per-user frequency settings
  • Watch Mode: Real-time file system monitoring for instant updates

Security

  • JWT Authentication: Short-lived access tokens (1 hour) with refresh tokens (7 days)
  • Strong Passwords: Complexity requirements enforced (8+ chars, uppercase, lowercase, number, special)
  • Account Lockout: 5 failed attempts = 15-minute lockout
  • Rate Limiting: 10 requests/minute on auth endpoints
  • Input Validation: Comprehensive validation on all inputs
  • No Passwords on Devices: Web-based device approval with QR codes

📚 Documentation

For Users & Self-Hosters

For Developers


🎯 Supported Devices

Platform Sync OPDS Status
Web Browser Full support
KOReader Kindle, Kobo, PocketBook
Kobo Devices Clara, Libra, Sage, etc.
Mobile Apps 🚧 🚧 Coming Q2 2026

🛠 Tech Stack

  • Backend: Go 1.25+ with Echo framework
  • Database: PostgreSQL 15+ with pgx v5
  • Frontend: HTMX + Tailwind CSS + Templ
  • Auth: JWT tokens with bcrypt password hashing
  • Container: Podman (Docker compatible)

🧪 Testing

# Run all tests
make test-all

# Run integration tests (with test mode)
make test-integration

# Run Bruno OpenCollection YAML API tests
npm install -g @usebruno/cli
bruno run

📊 Project Status

Version: 1.0
License: GPL-3.0
Status: Production-ready


🤝 Contributing

We welcome contributions! Please see docs/DEVELOPMENT.md for guidelines.


📄 License

GPL-3.0 - See LICENSE file for details.


Built with ❤️ using Go, PostgreSQL, HTMX, and Tailwind CSS

S
Description
A modern self-hosted media library system built with Go, PostgreSQL, HTMX, and Tailwind CSS featuring universal cross-device sync, beautiful dark themes, and comprehensive media management.
Readme AGPL-3.0
27 MiB
v1.0.1
Latest
2026-08-22 13:59:19 -04:00
Languages
Go 69.4%
TypeScript 12.7%
templ 12%
PLpgSQL 2.7%
CSS 1.5%
Other 1.6%