This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Before writing code, check for BRANCH_STATUS.md in the root directory.
- If it exists: Read it first. Use it as primary context. Focus on the
CRITICAL FILE MAPandCURRENT OBJECTIVE. - If it's missing: Perform a deep dive, then create
BRANCH_STATUS.mdusing the template in.cursorrules. Populate architecture, schema, workflows, and objective before touching any code.
After completing a task: update BRANCH_STATUS.md (check off objectives, add changelog entry, update file map).
# Run all tests
pytest tests/
# Run a single test file
pytest tests/test_abs_socket_listener.py -v
# Run a single test by name
pytest tests/test_abs_socket_listener.py::TestKosyncPutInstantSync::test_put_records_debounce_event_for_active_linked_book -v
# Apply database migrations
alembic upgrade head
# Create a new migration
alembic revision --autogenerate -m "description"
# Start the server (production/Docker entry point)
./start.sh
# Start the server (development, direct)
python -m src.web_serverThere is no configured linter or formatter beyond PEP 8 — the project does not use black or flake8 in CI.
BookBridge (this project) is a 5-way reading-progress synchronization engine. It bridges:
- Audiobookshelf (ABS) — audiobook server (source of truth for audio timestamps)
- KOReader / KoSync — e-reader device progress (by percentage)
- Storyteller — audiobook companion app (by ebook EPUB position)
- Grimmory — ebook library/shelf manager (by percentage)
- Hardcover — book tracking service (write-only metadata)
1. Instant Sync (event-driven, INSTANT_SYNC_ENABLED)
├── ABS Socket.IO listener → debounce → sync_cycle(target_abs_id)
└── KoSync PUT endpoint → debounce → sync_cycle(target_abs_id)
2. Per-client polling (ClientPoller, {CLIENT}_POLL_MODE=custom)
└── Lightweight: fetch position → compare to cache → sync_cycle if changed
3. Global poll cycle (schedule, SYNC_PERIOD_MINS)
└── Full sync pipeline for all active books
All services are wired in src/utils/di_container.py using python-dependency-injector. Providers use Factory (re-reads os.environ on each call) or Singleton (created once). The container is built after settings are loaded from the database into os.environ.
web_server.py startup order:
DatabaseServiceinitializedConfigLoader.bootstrap_config()— seeds settings table from env/defaults if emptyConfigLoader.load_settings()— loads DB settings intoos.environcreate_container()— builds DI container with updated env vars- Blueprints registered (KoSync, Hardcover)
- Daemon threads started: sync scheduler, ABS socket listener, client poller
Every sync client (src/sync_clients/) implements SyncClient from sync_client_interface.py:
| Method | Purpose |
|---|---|
is_configured() |
Returns False if credentials are missing — client is silently skipped |
get_service_state(book, prev_state, bulk_context) |
Fetch current progress → returns ServiceState or None |
update_progress(book, request) |
Push a new position → returns SyncResult |
get_text_from_current_state(book, state) |
Extract text snippet at current position (for alignment) |
fetch_bulk_state() |
Optional: pre-fetch all positions in one API call |
ServiceState.current is a dict — keys vary by client:
- Audiobook clients:
{'ts': float, 'pct': float, ...} - Ebook clients:
{'pct': float, 'href': str, 'frag': str, ...}
Settings are stored in the SQLite settings table and mirrored into os.environ at startup. ALL_SETTINGS in src/utils/config_loader.py is the canonical list. Add new settings there (both to ALL_SETTINGS and DEFAULT_CONFIG) — they are bootstrapped into the DB automatically for existing users.
The settings UI at /settings reads from os.environ via get_val()/get_bool() context processors and saves via POST /settings.
src/services/write_tracker.py tracks recent writes per (client_name, abs_id). After pushing progress to a client, call record_write(client_name, abs_id). Before reacting to a progress change from that client, call is_own_write(client_name, abs_id) to suppress feedback loops.
Client name keys: 'ABS', 'Storyteller', 'BookLore' (internal key, displayed as Grimmory), 'KoSync'.
abs_socket_listener.py exposes backward-compat wrappers record_abs_write(abs_id) / is_own_write(abs_id) that delegate to the shared tracker.
SQLAlchemy ORM. Key models in src/db/models.py:
| Model | Purpose |
|---|---|
Book |
Core mapping: ABS item ↔ ebook file, with UUIDs for each client |
State |
Per-client last-known progress (one row per book+client) |
Setting |
Runtime configuration key/value store |
KosyncDocument |
KOReader document hash ↔ ABS book link |
Job |
Transcription/alignment job tracking |
PendingSuggestion |
Auto-discovered book match candidates |
BookloreBook |
Cached Grimmory book metadata |
DatabaseService (src/db/database_service.py) is the only access layer — use it, never query the ORM directly from services.
get_books_by_status('active') is the standard way to get books eligible for sync.
Scope discipline: Only modify what is explicitly requested. Don't refactor surrounding code, reorganize imports, or "improve" working logic unless asked.
Renaming: Search the entire codebase before renaming any symbol. Update all occurrences atomically.
No print statements: Use the logging module. logger = logging.getLogger(__name__) at the top of each module.
No commented-out code: Delete old code entirely.
Type hints: Required on all new public functions and methods.
Test after every change: pytest tests/ must pass before marking a task complete. The test suite runs in ~7s.
Commits: Format git commit -m "[Type] Description". Do not push without explicit user approval.
New settings: Always add to both ALL_SETTINGS and DEFAULT_CONFIG in src/utils/config_loader.py. Boolean settings default to 'true'/'false' strings.
Template/JS: templates/settings.html is a large Jinja2 + vanilla JS file. Match existing patterns for toggles (toggleSection), conditional visibility, and CSS class names.
Tests use unittest.TestCase + pytest as runner. Integration tests create real SQLite databases in temp directories.
MockContainer pattern (used in test_webserver.py, test_settings_comprehensive.py): a plain Python class with callable methods matching the DI container interface, passed to create_app(test_container=...).
Accessing module-level state in kosync_server tests: The KoSync blueprint uses module globals (_manager, _database_service, _kosync_debounce). Tests save/restore them in try/finally. Reset _debounce_thread_started = False and clear _kosync_debounce in setUp for clean isolation.
Socket listener tests: ABSSocketListener is instantiated with patch("src.services.abs_socket_listener.socketio.Client") to prevent real connections.