Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a1950e9
Add BnF, Inventaire and Library of Congress import sources to backend
codebude Oct 14, 2026
2e0377e
Add new search sources, localize import source names and report each …
codebude Oct 14, 2026
7ed814c
Document new import search sources and their per-source toggles
codebude Oct 14, 2026
b979b1f
Document new import sources and features in README
codebude Oct 14, 2026
ea2509c
Set keyed import sources off in example config and require credential…
codebude Oct 14, 2026
deaf1b3
Merge pull request #109 from codebude/feature/add-more-search-providers
codebude Oct 14, 2026
a2c8003
Update frontend and Python dependency lockfiles with minor safe/upgrades
codebude Oct 14, 2026
5cabc5e
Migrate frontend imports to #lib and upgrade SvelteKit/Vitest
codebude Oct 14, 2026
9eb10e5
Ignore intentional Pydantic deprecation warnings in backend tests
codebude Oct 14, 2026
ecd3758
Document Node.js 22.17 requirement and SvelteKit 3 upgrade
codebude Oct 14, 2026
6755126
Merge pull request #110 from codebude/feature/dependency-upgrades
codebude Oct 15, 2026
4bda28e
Add Deutsche Nationalbibliothek as book import source
codebude Oct 15, 2026
bf07758
Add Deutsche Nationalbibliothek to import search frontend
codebude Oct 15, 2026
bea216e
Document Deutsche Nationalbibliothek import source
codebude Oct 15, 2026
a160767
feat: allow a configurable upstream for the frontend webserver
mj0nez Oct 15, 2026
537edba
docs: describe BACKEND_ADDR
mj0nez Oct 15, 2026
a062277
fix: use default.conf for config
mj0nez Oct 15, 2026
06c4ed7
Merge pull request #111 from codebude/feature/add-german-national-lib
codebude Oct 15, 2026
912186f
chore: addressed feedback
mj0nez Oct 17, 2026
257eaa5
Merge remote-tracking branch 'origin/develop' into feature/allow-conf…
codebude Oct 17, 2026
1f96ead
Warn about invalid BACKEND_ADDR values
codebude Oct 17, 2026
0ead6f0
Merge pull request #112 from mj0nez/feature/allow-configurable-backen…
codebude Oct 17, 2026
91c7f55
Updated release notes for v1.10.0
codebude Oct 18, 2026
6051b0b
Fix release comparison links
codebude Oct 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,40 @@ DASHBOARD_QUOTE_CACHE_TTL=86400 # in seconds; default 24h
# Frontend settings (build-time)
PUBLIC_DEFAULT_LOCALE=en # UI default locale: en | de

# Book import search providers
# At least one source must be able to answer. If every source below is disabled, the
# backend refuses to start and names the variables in the error.
OPEN_LIBRARY_IMPORT_SEARCH_ENABLED=true

# BnF (Bibliotheque nationale de France) - no API key needed, HTTPS
BNF_IMPORT_SEARCH_ENABLED=true

# Inventaire.io - no API key needed, HTTPS, ISBN searches only
INVENTAIRE_IMPORT_SEARCH_ENABLED=true

# Hardcover and Google Books need credentials before they can answer anything, so they
# are set to false here. The built-in default is true so that upgrading never silently
# drops a source; set either to true once its key or token is configured:
# HARDCOVER_APP_API_TOKEN (see https://codebude.github.io/librislog/guide/api-keys)
# GOOGLE_BOOKS_API_KEY
# A source left enabled without its key makes no request and is reported as skipped,
# so leaving the default is harmless, just noisier in the search progress list.
# Setting GOOGLE_BOOKS_IMPORT_SEARCH_ENABLED=false also disables the on-demand
# "Search Google Books too" button.
HARDCOVER_IMPORT_SEARCH_ENABLED=false
GOOGLE_BOOKS_IMPORT_SEARCH_ENABLED=false

# Library of Congress - no API key needed
# WARNING: This source is reached over PLAIN HTTP (unencrypted) on port 210; the LC
# offers no HTTPS for its SRU endpoint. Book queries and responses travel unencrypted
# and can be observed or tampered with on the network path. Only public bibliographic
# data is transferred. Set this to false if unencrypted egress to port 210 is not
# acceptable in your environment. All other sources use HTTPS.
LIBRARY_OF_CONGRESS_IMPORT_SEARCH_ENABLED=false

# Deutsche Nationalbibliothek (German National Library) - no API key needed, HTTPS
DNB_IMPORT_SEARCH_ENABLED=true

# Cover scraper settings
THALIA_COVER_SEARCH_ENABLED=false # For research only. Do not use in production without understanding the legal implications of scraping.

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ coverage.xml
.hypothesis/
.pytest_cache/
cover/
# Vitest 5 writes generated reports and artifacts here
.vitest/

# Translations
*.mo
Expand Down Expand Up @@ -232,6 +234,7 @@ node_modules/
*-snapshot.md

# AI tools
/AGENTS.md
/.memsearch
/.playwright-mcp
/.sverklo
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<img src="https://img.shields.io/badge/license-MIT-green" alt="License">
</p>

**Multi-user book tracking webapp** — maintain four reading lists, import books from Open Library & Google Books, scrape cover art, and get rich reading analytics — all on your own hardware.
**Multi-user book tracking webapp** — maintain four reading lists, import books from six metadata sources, scrape cover art, and get rich reading analytics — all on your own hardware.

> `docker compose up -d` → full data ownership, no vendor lock-in, no API keys required.

Expand Down Expand Up @@ -62,7 +62,7 @@ Open **http://localhost:8001** and create your account.
## Why LibrisLog?

- **Your data, your rules.** Fully self-hosted under MIT license — no ads, no user tracking, no vendor lock-in. A single SQLite file you can back up anytime.
- **No API keys required.** Works with Open Library out of the box. Add Google Books or Hardcover.app tokens optionally for richer search results.
- **No API keys required.** Five sources work out of the box: Open Library, BnF, Inventaire.io, the Library of Congress, and the German National Library. BnF, Inventaire and the German National Library also find books that Open Library misses, which matters for non-English editions. Add Google Books or Hardcover.app tokens optionally (both are off in `.env.example`; set their key and their `*_IMPORT_SEARCH_ENABLED=true` to use them), and switch any source on or off individually. The Library of Congress endpoint is only reachable over plain HTTP, so it sends queries unencrypted unless you disable it — see [Configuration](https://docs.librislog.app/guide/configuration).
- **Rich insights from day one.** Calendar heatmap, language/status/page distribution charts, books finished per month/year, top authors — all on your hardware.
- **Multi-user from the start.** User roles (admin/user), optional OIDC SSO, per-user libraries. One instance works for your whole household or small group.
- **Import any format you have.** Goodreads or Bookstats exports with automatic field mapping, generic CSV or Excel (XLSX) with per-field Python transforms, JSON, ZIP with covers.
Expand All @@ -80,11 +80,13 @@ Open **http://localhost:8001** and create your account.
- **Library** — Grid/list view, search and sort, four reading statuses (Want to Read, Currently Reading, Read, Did Not Finish)
- **Reading progress** — Page-level slider, full progress timeline per book with edit/history
- **Statistics dashboard** — Calendar heatmap, distribution charts, books finished per period, top authors
- **Book import** — Search Open Library, Google Books, Hardcover.app. Scan ISBN barcodes on mobile. Manual entry for anything not found
- **Reading streaks & goals** — Current and all-time longest reading streak, plus optional daily, monthly, and yearly goals
- **Book import** — Search Open Library, BnF, Inventaire.io, the Library of Congress, the German National Library, Hardcover.app, and Google Books in parallel, each source individually switchable. Results describing the same book are grouped so you can compare editions side by side before importing. Scan ISBN barcodes on mobile, collect candidates in a basket, or enter manually
- **Data portability** — Export as JSON, CSV, or ZIP with covers. Import from the Goodreads or Bookstats presets, or any CSV/Excel file with custom field mapping
- **Cover management** — Automatic multi-source cover search with manual override, URL paste, or file upload
- **Data hygiene** — Find and fix missing metadata (covers, page counts, authors) in bulk
- **Multi-user** — Admin/user roles, per-user libraries, optional OIDC login
- **Public profiles** — Shareable, read-only profile links with configurable sections, audience, language, and optional expiry
- **Themes** — Light, dark, and custom DaisyUI themes with persistent preferences
- **Administration** — Full backup/restore of the SQLite database, user management, API key management

Expand Down
11 changes: 11 additions & 0 deletions backend/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ class Settings(BaseSettings):
cover_import_timeout_seconds: int = 15
hardcover_app_api_token: str = ""
thalia_cover_search_enabled: bool = False
open_library_import_search_enabled: bool = True
# Hardcover and Google Books need credentials to answer at all. The built-in default
# stays true so upgrading never silently drops a source someone had configured
# before these switches existed; without a token/key the source is skipped anyway.
# .env.example and the docs recommend setting these to false when unused.
hardcover_import_search_enabled: bool = True
google_books_import_search_enabled: bool = True
bnf_import_search_enabled: bool = True
inventaire_import_search_enabled: bool = True
library_of_congress_import_search_enabled: bool = True
dnb_import_search_enabled: bool = True
embed_enabled: bool = True
mail_server: str = ""
mail_port: int = 587
Expand Down
37 changes: 37 additions & 0 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,46 @@ async def _telemetry_heartbeat() -> None:
await asyncio.sleep(_TELEMETRY_INTERVAL_SECONDS)


def _validate_import_sources() -> None:
"""Ensure at least one import search provider can actually answer a query.

Counting enabled flags alone is not enough: Hardcover and Google Books require a
token/API key, so being switched on without credentials still means no source can
answer. This checks for a provider that is both enabled and usable.
"""
from app.services import book_import

active: list[str] = []
for spec in book_import._build_sources(""):
if not spec.enabled():
continue
if spec.stage == "hardcover" and not settings.hardcover_app_api_token.strip():
continue
active.append(spec.stage)

if book_import._google_fallback_enabled() and settings.google_books_api_key.strip():
active.append("google_books")

if not active:
keyless = ", ".join(
sorted(
f"{spec.stage.upper()}_IMPORT_SEARCH_ENABLED"
for spec in book_import._build_sources("")
if spec.stage not in {"hardcover", "google_books"}
)
)
raise RuntimeError(
"No usable book import source is configured. "
f"Set at least one of {keyless} to true, or provide "
"HARDCOVER_APP_API_TOKEN with HARDCOVER_IMPORT_SEARCH_ENABLED=true, "
"or GOOGLE_BOOKS_API_KEY with GOOGLE_BOOKS_IMPORT_SEARCH_ENABLED=true."
)


@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application lifespan: create required directories and start background tasks."""
_validate_import_sources()
Path(settings.covers_dir).mkdir(parents=True, exist_ok=True)
Path(settings.import_temp_dir).mkdir(parents=True, exist_ok=True)
cleanup_temp_files()
Expand Down
5 changes: 5 additions & 0 deletions backend/app/routers/import_.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from app.schemas import BookImportCandidate, BookImportRequest, BookRead
from app.services import book_import
from app.services.authors import resolve_authors_payload, sync_book_authors
from app.services.cover_import import is_safe_cover_import_url
from app.services.cover_storage import download_cover
from app.services.tags import build_book_read, sync_book_tags

Expand Down Expand Up @@ -134,6 +135,10 @@ async def import_book(

# Attempt to download and cache the cover locally; fall back to external URL.
cover_url = c.cover_url
if cover_url and not is_safe_cover_import_url(cover_url):
# The candidate is user-submitted, so its cover URL is untrusted input.
logger.warning("Rejecting cover URL for unsafe address: %s", cover_url)
cover_url = None
if cover_url:
async with httpx.AsyncClient(timeout=15.0, follow_redirects=True) as client:
filename = await download_cover(cover_url, settings.covers_dir, client, current_user.id)
Expand Down
2 changes: 1 addition & 1 deletion backend/app/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class BookImportCandidate(SQLModel):
language: Optional[str] = None
tags: Optional[str] = None
blurb: Optional[str] = None
source: str # "open_library" | "google_books" | "hardcover"
source: str # "open_library" | "google_books" | "hardcover" | "bnf" | "inventaire" | "library_of_congress" | "dnb"


class CoverCandidate(SQLModel):
Expand Down
Loading
Loading