John O'Keefe 7e4c97f639 feat(scanner): fix ABS sidecar series parsing; three-way series mapping; smart genre; language codes; title-derived series
Phase 2a of the metadata overhaul (plan §2.1-2.3, §2.5-2.9).

The headline bug: ABS writes sidecar series as an array of STRINGS
(["The Witcher #3", ...]); the parser only accepted {series, sequence}
OBJECT arrays, so every ABS-matched book silently lost its series (found
live: 9 sidecars, zero series ingested; only folder-structure Malazan
survived). parseSeriesCandidates now normalizes string arrays, object
arrays and Libation plain strings.

Three-way series assignment (plan §2.2):
- primary bibliographic series -> series/series_number columns
- additional real series (Drizzt: Icewind Dale #1 = Legend of Drizzt #4)
  -> alternate_info JSONB, comic-established shape
- ordering constructs ('The Witcher (Reading Order)') -> PlaylistSeries
  candidates; NEVER the series column. Detection is explicit-marker-only
  ((reading order|chronological|publication order|in order|timeline) as
  parenthetical/suffix) — precision over recall; a series merely
  containing 'timeline' mid-name stays a series.

Sequence policy: leading-number extraction ('Book 2' -> 2, '2,
Dramatized Adaptation' -> 2, '1a' -> 1); lossy extractions preserve the
raw text in alternate_info.series_sequence_raw.

Smart genre (deterministic lexicon): compound Audible blobs split
('Epic, Action & Adventure, Military, Sword & Sorcery' -> 4 candidates),
alias-normalized, format noise demoted ('Audiobook' never wins),
most-specific canonical genre takes the genre column, all candidates
stay in tags.

Language: display names normalize to ISO 639-1 on ingest; ROOT-CAUSED
the update-path drop — CreateMediaItem call site never set Language
(explicit NULL over the column default), the update path did; create
now carries it.

Title-derived series fallback (§2.7): 'Baptism of Fire: The Witcher,
Book 3' 2-level folders split conservatively (only when the post-colon
tail matches ', Book N'), subtitle parsed for series+number, blanks-only
precedence, matching-name number fill, zero display mutation. Works
with no sidecar and no network.

Ingest fixes: subtitle/explicit land in their columns (audio tag
subtitle stops joining into title); full authors[] array (was [0]-
only); sidecar descriptions sanitized via bluemonday UGC policy
(vendored).

metadata_overrides: verified scan-update path already applies
ApplyMetadataOverrides; subtitle/explicit/authors covered by the Phase 1
override adaptation.

Tests: new series_parsing_test.go (string-array sidecar e2e, Drizzt
alternate_info, lossy raw preservation, playlist classification,
title-derived fallback incl. no-mutation + blanks-only, genre lexicon,
language map); audio-tag subtitle pin updated to column behavior;
calibre/ABS 2.5-series_index pins widened per float change (changelog'd
in phase 1).

Deferred to 2b: auto-created playlist collections (§2.4) — settings
toggle, collection upsert, ownership.
2026-10-19 14:34:42 -04:00

Bookhoard logo 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.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: 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

For Developers


🎯 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

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
30 MiB
0 Stars 0 Watchers 0 Forks
v2.1.0
Latest
2026-10-21 15:08:01 -04:00
Languages
Go 72.6%
TypeScript 14.4%
templ 10.6%
CSS 1.5%
Python 0.3%
Other 0.4%