Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
82e943c
Fix version rendering in app sidebar
codebude May 28, 2026
f5063e1
Show commit that generated the docs in docs footer
codebude May 28, 2026
5696608
Link ghpr from build package badge in Readme.md
codebude May 28, 2026
faeac78
Made book rating editable from details drawer
codebude May 29, 2026
6dec115
Added ratings statistics
codebude May 29, 2026
d37ba4d
Centered calendar view on page.
codebude May 29, 2026
5344545
Fix last column of calendar stats
codebude May 29, 2026
fbef341
Update logic for gathering top-/worst rated books for statistics
codebude May 29, 2026
496d733
Added docker commands to llc cli
codebude May 29, 2026
3f26286
Only link SHA in buildinfo in docs
codebude May 29, 2026
78dfd49
Added UI dividers to build some structure on statistics page
codebude May 29, 2026
318f4a1
Restructured design of data hygiene page
codebude May 29, 2026
1ad0d6b
Fix broken e2e tests due to ui changes
codebude May 29, 2026
1bbb54f
Harmonize page width over project
codebude May 29, 2026
f25f554
Restructure manage my data section
codebude May 29, 2026
5ee09a9
Added datetime format validation for date_finished
codebude May 29, 2026
e4f1f05
Clear selected mapping when changing files in import editor
codebude May 29, 2026
ae73a84
Fix font in transform editor/input
codebude May 29, 2026
c81ecfd
Improved goodreads default mapping
codebude May 29, 2026
d8f0819
Fixed syntaxhighlighting and rendering of transform input
codebude May 29, 2026
6e94ada
Don't delete import files on erroneous imports
codebude May 29, 2026
77aac2a
Fixed isbn handling in goodreads import and added validator
codebude May 29, 2026
e2db299
Performance optimization for /statistics route
codebude May 29, 2026
8e41b47
Performance optimization for pages per day stats endpoint
codebude May 29, 2026
6041ea2
Strengthen pages per month statistic calculation
codebude May 29, 2026
b4d72fc
Enhanced book import validations and updated goodreads mapping
codebude May 29, 2026
af1b850
Performance optimization for book and tags endpoints
codebude May 29, 2026
4efa736
Add cover image fallback in top authors section
codebude May 29, 2026
c58c930
Show more functionality for best/worst book lists
codebude May 29, 2026
8d3bc3e
Show title when hovering top author book covers
codebude May 29, 2026
2cff18f
Merge pull request #21 from codebude/feature/set-of-small-changes
codebude May 29, 2026
f2b1a64
Missing cover page
codebude May 29, 2026
67935ba
Test cases for missing cover page
codebude May 29, 2026
f6ac824
Project links ok about page
codebude May 29, 2026
2b17e95
Updated docs for missing cover page
codebude May 30, 2026
87e0723
Fixed e2e test cases for missing cover page
codebude May 30, 2026
eed053e
Merge pull request #22 from codebude/feature/missing-book-cover-workflow
codebude May 30, 2026
9210b72
Improved translations in i18n files
codebude May 30, 2026
a2a1b9b
Merge pull request #23 from codebude/feature/translation-quality-init…
codebude May 30, 2026
655c1d1
Fix db restore script
codebude May 31, 2026
939c14b
Updated screenshots in docs
codebude May 31, 2026
b718253
Refined description of backup and restore on docs
codebude May 31, 2026
bdd4478
Fix backup restore tests
codebude May 31, 2026
0c23e77
Merge pull request #24 from codebude/feature/docs-content-update
codebude May 31, 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

<p align="center">
<a href="https://github.com/codebude/librislog/actions/workflows/tests.yml"><img src="https://github.com/codebude/librislog/actions/workflows/tests.yml/badge.svg" alt="Tests"></a>
<a href="https://github.com/codebude/librislog/actions/workflows/docker.yml"><img src="https://github.com/codebude/librislog/actions/workflows/docker.yml/badge.svg" alt="Docker Build"></a>
<a href="https://github.com/codebude?tab=packages&repo_name=librislog"><img src="https://github.com/codebude/librislog/actions/workflows/docker.yml/badge.svg" alt="Docker Build"></a>
<a href="https://codebude.github.io/librislog/"><img src="https://github.com/codebude/librislog/actions/workflows/docs.yml/badge.svg" alt="Docs Build"></a>
<img src="https://img.shields.io/badge/python-3.14-%233776AB?logo=python" alt="Python">
<img src="https://img.shields.io/badge/svelte-5-%23FF3E00?logo=svelte" alt="Svelte">
Expand Down
19 changes: 19 additions & 0 deletions backend/app/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,30 @@

from app.config import settings


def _set_sqlite_pragmas(dbapi_connection, _connection_record) -> None:
"""Apply performance-oriented PRAGMAs to new SQLite connections."""
import sqlite3
if not isinstance(dbapi_connection, sqlite3.Connection):
return
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA journal_mode=WAL")
cursor.execute("PRAGMA synchronous=NORMAL")
cursor.execute("PRAGMA cache_size=-8000")
cursor.execute("PRAGMA temp_store=MEMORY")
cursor.execute("PRAGMA mmap_size=268435456")
cursor.close()


engine = create_engine(
settings.database_url,
connect_args={"check_same_thread": False}, # needed for SQLite
pool_pre_ping=True,
)

from sqlalchemy import event # noqa: E402
event.listen(engine, "connect", _set_sqlite_pragmas)


@atexit.register
def _dispose_engine() -> None:
Expand Down
10 changes: 9 additions & 1 deletion backend/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from datetime import datetime, timezone

import sqlalchemy as sa
from pydantic import model_validator
from sqlalchemy import Column, DateTime, TypeDecorator
from sqlmodel import Field, SQLModel

Expand Down Expand Up @@ -60,6 +61,13 @@ class Book(SQLModel, table=True):
isbn: Optional[str] = Field(default=None, unique=True)
cover_url: Optional[str] = None
publisher: Optional[str] = None

@model_validator(mode="before")
@classmethod
def normalize_empty_cover_url(cls, data: dict) -> dict:
if isinstance(data, dict) and "cover_url" in data and data["cover_url"] == "":
data["cover_url"] = None
return data
published_year: Optional[int] = None
page_count: int = Field(default=0)
language: Optional[str] = Field(default=None, max_length=2)
Expand Down Expand Up @@ -103,7 +111,7 @@ class BookTag(SQLModel, table=True):
__tablename__ = "book_tag"

book_id: int = Field(foreign_key="book.id", primary_key=True)
tag_id: int = Field(foreign_key="tag.id", primary_key=True)
tag_id: int = Field(foreign_key="tag.id", primary_key=True, index=True)


class User(SQLModel, table=True):
Expand Down
34 changes: 29 additions & 5 deletions backend/app/routers/books.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import List, Literal, Optional

from fastapi import APIRouter, Depends, HTTPException, Query, status
import sqlalchemy as sa
from sqlalchemy.exc import IntegrityError
from sqlmodel import Session, func, or_, select

Expand All @@ -31,7 +32,7 @@
)
from app.services.cover_import import import_cover_from_url, is_external_cover_url
from app.services.quote_cache import get_or_fetch_dashboard_quote
from app.services.tags import build_book_read, cleanup_orphan_tags, sync_book_tags
from app.services.tags import build_book_read, cleanup_orphan_tags, load_tags_batch, sync_book_tags
from app.time_utils import utcnow

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -128,10 +129,19 @@ def _raise_integrity_conflict(exc: IntegrityError) -> None:
raise


def _build_book_read_with_tags(book: Book, tags_text: str | None) -> BookRead:
"""Build a BookRead from a Book model with a pre-resolved tags string."""
payload = book.model_dump()
payload.pop("user_id", None)
payload["tags"] = tags_text
return BookRead.model_validate(payload)


@router.get("", response_model=BookListResponse)
def list_books(
status: Optional[ReadingStatus] = Query(default=None),
q: Optional[str] = Query(default=None),
has_cover: Optional[bool] = Query(default=None),
sort: Literal["title", "date_added", "date_started", "date_finished", "rating"] = Query(
default="date_added"
),
Expand Down Expand Up @@ -173,6 +183,14 @@ def list_books(
)
)

if has_cover is not None:
if has_cover:
base_statement = base_statement.where(Book.cover_url.is_not(None), Book.cover_url != "")
else:
base_statement = base_statement.where(
sa.or_(Book.cover_url.is_(None), Book.cover_url == "")
)

total = session.exec(
select(func.count()).select_from(base_statement.subquery())
).one()
Expand Down Expand Up @@ -206,8 +224,13 @@ def list_books(

books = list(session.exec(statement).all())
logger.debug("list_books — returning %d/%d book(s)", len(books), total)
book_ids = [b.id for b in books if b.id is not None]
book_tags_map = load_tags_batch(session, book_ids) if book_ids else {}
return BookListResponse(
books=[build_book_read(session, book) for book in books],
books=[
_build_book_read_with_tags(book, book_tags_map.get(book.id))
for book in books
],
total=total,
)

Expand Down Expand Up @@ -279,12 +302,13 @@ def get_tag_cloud(
session: Session = Depends(get_session),
) -> List[TagCloudEntry]:
"""Return tags sorted by usage count (descending) for the authenticated user."""
count_label = func.count(BookTag.book_id).label("cnt")
rows = session.exec(
select(Tag.name, func.count(BookTag.book_id))
select(Tag.name, count_label)
.join(BookTag, BookTag.tag_id == Tag.id)
.where(Tag.user_id == current_user.id)
.group_by(Tag.id, Tag.name)
.order_by(func.count(BookTag.book_id).desc(), Tag.name.asc())
.group_by(Tag.id)
.order_by(count_label.desc(), Tag.name.asc())
.limit(limit)
).all()
return [TagCloudEntry(tag=name, count=count) for name, count in rows]
Expand Down
4 changes: 3 additions & 1 deletion backend/app/routers/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ async def execute_import_data(
async def event_generator():
with Session(stream_bind) as session:
completed = False
import_failed_rows = 0
final_error: str | None = None
try:
async for event in execute_import(
Expand All @@ -305,6 +306,7 @@ async def event_generator():
):
if event.get("event") == "complete":
completed = True
import_failed_rows = event.get("failed", 0)
if event.get("event") == "error":
final_error = str(event.get("message") or "Import failed")
yield f"data: {json.dumps(event)}\n\n"
Expand All @@ -321,7 +323,7 @@ async def event_generator():
final_error = 'error.importExecutionFailed'
yield f"data: {json.dumps({'event': 'error', 'message': 'error.importExecutionFailed'})}\n\n"
finally:
if completed or final_error is not None:
if completed and import_failed_rows == 0 and final_error is None:
delete_parsed_upload(body.file_id, current_user.id)

return StreamingResponse(
Expand Down
Loading