John O'Keefe 69773ec307 feat(podcasts): smart collections play as a sequence — the Daily Fiction acceptance (item 2.5)
ACCEPTANCE (plan §0 item 2.5): "Daily Fiction" = feed ∈ {A} ∧ unplayed
∧ <7d matches exactly today's episodes, and PLAY = today's episodes
back-to-back. Two pieces were missing after item 2.4:

- The chain builder only read static membership (collection_items) — a
  smart collection has none. buildSmartCollectionSequenceContext now
  evaluates the rules LIVE for the requesting user (mode from the
  collection, family lens via the visible set) and chains the
  downloaded episode matches in NATIVE feed order — the collection's
  computed order is content (R1: never written back; OrderEpisodes is
  the order chokepoint per R3). A match that aged out of the briefing
  (played since, published beyond the window) falls back to the feed
  queue instead of breaking the page.
- The smart-detail branch rendered match cards as books: episode
  matches are now flagged (IsEpisode + download state) so they carry
  the play affordance and the header's Play-as-Sequence button;
  feed titles stay empty on smart cards for now (batch lookup is
  polish, not acceptance).

Pinned by TestPodcastSmartCollections' acceptance subtests: the
briefing starts at index 1/2 and chains into the second with the
queue param on both hops; the order control stays hidden (computed
order); a stale match falls back to the feed queue honestly.
2026-10-30 17:12:07 -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 (no source checkout needed)

Create a directory on the Docker host. You only need the compose file, an .env, and an uploads folder — everything else rides the published image.

mkdir bookhoard && cd bookhoard
mkdir uploads

docker-compose.yml — copy this wholesale:

services:
  # PostgreSQL Database
  db:
    image: postgres:15-alpine
    container_name: bookhoard_db
    environment:
      POSTGRES_DB: bookhoard
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: ${DBPASS}
      # PGPORT makes Postgres listen on DB_PORT (kept in sync with the host mapping + app's DATABASE_PORT)
      PGPORT: ${DB_PORT:-5432}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "127.0.0.1:${DB_PORT:-5432}:${DB_PORT:-5432}"
      - "[::1]:${DB_PORT:-5432}:${DB_PORT:-5432}"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s
    env_file:
      - .env

  # One-shot bootstrap: creates the bookhoard_srv runtime role and
  # transfers object ownership to it (RLS prerequisite). Idempotent —
  # safe on every startup; the app waits for its success. Passwords
  # accept inline values or .env, like the rest of this file.
  # NOTE: plpgsql $$ tags are written $$$$ — compose eats one dollar
  # per pair before the container sees the SQL.
  db-init:
    image: postgres:15-alpine
    container_name: bookhoard_db_init
    environment:
      PGHOST: db
      PGPORT: ${DB_PORT:-5432}
      PGUSER: postgres
      PGDATABASE: bookhoard
      PGPASSWORD: ${DBPASS}
      DBSRVPASS: ${DBSRVPASS}
    entrypoint: ["bash", "-c"]
    command:
      - |
        set -e
        : "${DBSRVPASS:?DBSRVPASS required in .env}"
        psql -v ON_ERROR_STOP=1 -v role_pass="$DBSRVPASS" <<'EOSQL'
        DO $$$$
        BEGIN
          IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'bookhoard_srv') THEN
            CREATE ROLE bookhoard_srv LOGIN;
          END IF;
        END $$$$;
        ALTER ROLE bookhoard_srv LOGIN PASSWORD :'role_pass';
        DO $$$$
        DECLARE r RECORD;
        BEGIN
          FOR r IN SELECT tablename FROM pg_tables WHERE schemaname = 'public' LOOP
            EXECUTE format('ALTER TABLE public.%I OWNER TO bookhoard_srv', r.tablename);
          END LOOP;
          FOR r IN SELECT sequencename FROM pg_sequences WHERE schemaname = 'public' LOOP
            EXECUTE format('ALTER SEQUENCE public.%I OWNER TO bookhoard_srv', r.sequencename);
          END LOOP;
          FOR r IN SELECT p.oid::regprocedure AS fn FROM pg_proc p
                   JOIN pg_namespace n ON p.pronamespace = n.oid
                   WHERE n.nspname = 'public' AND p.prokind = 'f' LOOP
            EXECUTE format('ALTER FUNCTION %s OWNER TO bookhoard_srv', r.fn);
          END LOOP;
        END $$$$;
        ALTER SCHEMA public OWNER TO bookhoard_srv;
        ALTER DATABASE bookhoard OWNER TO bookhoard_srv;
        EOSQL
    depends_on:
      db:
        condition: service_healthy
    restart: "no"

  # Bookhoard Application
  # In production this image is pulled from the Gitea container registry.
  # Override IMAGE_TAG in .env to pin or rollback a specific version (defaults to "latest").
  app:
    image: git.linuxhg.com/bookhoard/bookhoard:${IMAGE_TAG:-latest}
    container_name: bookhoard
    restart: unless-stopped
    environment:
      # Database Configuration
      # Runtime role = bookhoard_srv (non-superuser OWNER, subject to RLS
      # via FORCE). POSTGRES_USER above is the bootstrap account ONLY —
      # the app must NEVER connect as it (superusers bypass RLS).
      DATABASE_HOST: db
      DATABASE_PORT: ${DB_PORT:-5432}
      DATABASE_USER: bookhoard_srv
      DATABASE_PASSWORD: ${DBSRVPASS}
      DATABASE_NAME: bookhoard

      # Application Configuration
      JWT_SECRET: ${JWT_SECRET}
      SERVER_PORT: ${SERVER_PORT:-8765}
      # IMPORTANT: Device sync requires full URL with protocol
      # Local: http://localhost:8765
      # Local network: http://192.168.1.X:8765
      # Domain: https://bookhoard.example.com
      BASE_URL: ${BASE_URL:-http://localhost:8765}
      # Mark session cookies Secure; set true behind a TLS-terminating reverse proxy (Caddy/nginx/traefik)
      COOKIE_SECURE: ${COOKIE_SECURE:-false}

      # Rate Limiting Configuration
      TEST_MODE: ${TEST_MODE:-false}
      RATE_LIMIT_ENABLED: ${RATE_LIMIT_ENABLED:-true}
      REQUESTS_PER_MINUTE: ${REQUESTS_PER_MINUTE:-10}

      # Conversion Service Configuration
      BOOKHOARD_CONVERSION_CACHE_DIR: ${BOOKHOARD_CONVERSION_CACHE_DIR:-/app/cache/kepub}
      BOOKHOARD_CONVERSION_TOOL: ${BOOKHOARD_CONVERSION_TOOL:-/usr/bin/kepubify}
      BOOKHOARD_CONVERSION_CACHE_TTL: ${BOOKHOARD_CONVERSION_CACHE_TTL:-24h}

      # Library Maintenance
      # Days an archived item (file missing from disk for 2+ scans) is kept,
      # with its reading history, before library scans purge it for good.
      # Set 0 to keep archived items until purged manually on the library admin page.
      ARCHIVE_RETENTION_DAYS: ${ARCHIVE_RETENTION_DAYS:-90}

      # System timezone (fallback for server-side time operations)
      TZ: ${TZ:-UTC}
    ports:
      - "${SERVER_PORT:-8765}:${SERVER_PORT:-8765}"
    depends_on:
      db:
        condition: service_healthy
      db-init:
        condition: service_completed_successfully
    volumes:
      - ./uploads:/app/uploads
      - bookhoard_conversion_cache:/app/cache/kepub
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:${SERVER_PORT:-8765}/health || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

# Named Volumes
volumes:
  postgres_data:
  bookhoard_conversion_cache:

📊 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
33 MiB
0 Stars 0 Watchers 0 Forks
v2.4.3
Latest
2026-11-01 21:38:12 -05:00
Languages
Go 71.7%
TypeScript 15.7%
templ 10.5%
CSS 1.3%
Python 0.2%
Other 0.4%