Reverses the Phase 4 scope cut: for EXISTING folder/zip audiobooks a
sidecar edit was invisible to everything, including force rescan —
updateAudioBookItem writes only scan-derived fields, and these formats
often have no useful embedded tags, so the sidecar is the only source.
- Change fingerprint gains the manifest compare in both processors:
changed := size differs || track payload differs || manifest drifted.
The ladder runs against the bulk-loaded map entry (stat pairs only,
zero extra queries); NULL stored manifest backfills without
re-extraction (deploy-day rule, same as single-file).
- Manifest-changed applies the once-extracted book metadata to the
editorial columns (updateMediaItemWithMetadata, overrides protected)
and re-runs the playlist hook. The apply runs BEFORE the focused
payload write: the generic update's format-group pass derives mime
from the sentinel (none) and chapters from metadata.ChapterCount
(unset) — the payload write must win (rescanMultiFileAudioBook's
ordering). Track-only changes keep the existing editorial-keep
update; force rescan keeps it too.
- Manifests seeded at multifile create (folder names derive from the
sentinel — the exact representative the extractor reads — via
detectSidecarChangeWithRepresentative, so manifest and extractor can
never disagree; a directory-basename cover is ignored by the
extractor and therefore not tracked).
- Zip parity fixes: the fast path now lifts archive state like the
folder path always did (a returned zip book stayed archived
forever), and the change branch handles the unchanged-archived case.
- Single-file sidecar change path: extract-failure guard — the old
non-nil empty &MediaMetadata{} fallback bypassed the update path's
filename-title guard (preExtracted == nil only) and would blank the
title on a failed extraction; the row is now left untouched and the
manifest refreshed. Empty-success extraction (sidecar removed from a
tag-less book) now passes nil so the create-equivalent chain (tags →
filename) re-derives instead of blanking.
- Multifile changes now count in the scan summary (changedItems);
they were previously uncounted.
Tests: sentinel-name derivation unit pin; folder sidecar edit
(title + collection sync + fresh manifest + steady-state skip),
track-only change keeps editorial, zip sidecar edit + archive
restore, sidecar removal falls back to filename (the wipe-guard's
testable sibling). Full container suite: green except the documented
pre-existing TestAdminCreateUser red.
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 position, bookmarks, highlights, and notes sync automatically between KOReader and the web - with native Kobo sync and mobile apps coming later.
📱 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 e-reader, see it in your browser
- 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 conversion (for upcoming native Kobo support)
Media Management
- Calibre Integration: Automatic metadata import from Calibre
metadata.opfsidecar 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: Map collections to device shelves (used by native Kobo sync, coming soon)
- 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
- docs/user/calibre-integration.md - Calibre library integration
- docs/user/sync-guide.md - Understanding and using universal sync
- docs/user/devices/koreader-setup.md - KOReader configuration
- docs/user/devices/kobo-setup.md - Kobo e-reader configuration (coming soon)
- docs/user/user-guide.md - General user guide
- docs/user/admin-guide.md - Admin features and configuration
- docs/user/settings-guide.md - Settings and preferences
For Developers
- docs/developer/api/api-reference.md - Complete API documentation
- docs/developer/android-app.md - Android app design & roadmap
- docs/contributing/development.md - Development workflow
🎯 Supported Devices
| Platform | Sync | OPDS | Status |
|---|---|---|---|
| Web Browser | ✅ | ✅ | Full support |
| KOReader | ✅ | ✅ | Runs on Kindle, Kobo, PocketBook hardware |
| Kobo Devices | 🚧 | 🚧 | Native Kobo sync coming soon (use KOReader on Kobo today) |
| Mobile Apps | 🚧 | 🚧 | Native Android app in design (docs); iOS later |
💾 Network Storage (NAS / NFS / SMB)
Libraries on network mounts are supported for everyday use — serving, reading, progress, and annotation sync all work over the mount — with a few behaviors worth knowing:
- Outage hysteresis. When the mount drops, files miss one scan (marked missing, hidden) then a second (archived, still recoverable). When the mount returns — by path or by identical content elsewhere — everything is restored automatically: annotations, progress, and covers included.
- Retention purge. Archived items older than
ARCHIVE_RETENTION_DAYS(default 90,0= never auto-purge) are deleted for good by library scans, annotations included. A NAS offline longer than the retention window loses those books' metadata on return. - Deleting while the storage is down is refused. Deleting a book cascades its annotations, so deletes are rejected with a clear error while the item's library storage is unreachable. Archive the item instead, or retry when the mount is back.
- Watch mode does not fire on network mounts (inotify doesn't work over NFS/SMB). The periodic scan is the change detector.
- Latency — folder reachability checks are cached ~30s; large libraries on slow mounts will still feel slower than local disk.
🛠 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: AGPL-3.0
Status: Production-ready ✅
🤝 Contributing
We welcome contributions! Please see docs/developer/development.md for guidelines.
📄 License
AGPL-3.0 - See LICENSE file for details.
Built with ❤️ using Go, PostgreSQL, HTMX, and Tailwind CSS