feat: pdf reader

This commit is contained in:
2026-06-03 22:46:38 -05:00
parent d9c66e68a6
commit f091386974
29 changed files with 29469 additions and 2593 deletions
+3 -1
View File
@@ -91,13 +91,15 @@ class EBookListSerializer(serializers.ModelSerializer):
return None
def get_started(self, obj):
"""True when the user has opened the reader and has a saved EPUB location."""
"""True when the user has opened the reader with saved progress."""
try:
rp = obj.reading_progress
except ReadingProgress.DoesNotExist:
return False
if rp.current_position >= 99:
return False
if obj.format == "pdf":
return rp.last_page > 0 or rp.current_position > 0
return bool((rp.epub_location or "").strip())
+63 -1
View File
@@ -89,7 +89,69 @@ def _process_epub(file_path: str) -> dict[str, Any]:
def _process_pdf(file_path: str) -> dict[str, Any]:
return {"format": "pdf", "page_count": 0, "metadata": {}, "toc": []}
from pypdf import PdfReader
reader = PdfReader(file_path)
page_count = len(reader.pages)
metadata = _extract_pdf_metadata(reader)
toc = _extract_pdf_outline(reader)
if not toc and page_count > 0:
toc = [
{"title": f"Page {i}", "href": f"pdf:page:{i}", "children": []}
for i in range(1, page_count + 1)
]
return {
"format": "pdf",
"page_count": page_count,
"metadata": metadata,
"toc": toc,
}
def _extract_pdf_metadata(reader) -> dict[str, Any]:
metadata: dict[str, Any] = {}
info = reader.metadata
if not info:
return metadata
title = getattr(info, "title", None)
author = getattr(info, "author", None)
if title:
metadata["title"] = str(title)
if author:
metadata["author"] = str(author)
return metadata
def _extract_pdf_outline(reader) -> list[dict[str, Any]]:
outline = getattr(reader, "outline", None)
if not outline:
return []
return _walk_pdf_outline(reader, outline)
def _walk_pdf_outline(reader, outline: list[Any]) -> list[dict[str, Any]]:
entries: list[dict[str, Any]] = []
i = 0
while i < len(outline):
item = outline[i]
if isinstance(item, list):
if entries:
entries[-1]["children"] = _walk_pdf_outline(reader, item)
i += 1
continue
title = getattr(item, "title", None) or "Section"
page_num = 1
try:
page_num = reader.get_destination_page_number(item) + 1
except Exception:
logger.debug("Could not resolve PDF outline destination", exc_info=True)
entries.append({
"title": str(title),
"href": f"pdf:page:{page_num}",
"children": [],
})
i += 1
return entries
def _extract_epub_metadata(book) -> dict[str, Any]:
+9 -4
View File
@@ -168,18 +168,23 @@ class EBookViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=["get"])
def file(self, request: Request, pk: int | None = None) -> FileResponse | Response:
"""Stream the EPUB file for authenticated in-browser reading."""
"""Stream the ebook file for authenticated in-browser reading."""
ebook = self.get_object()
if ebook.format != "epub":
content_types = {
"epub": "application/epub+zip",
"pdf": "application/pdf",
}
content_type = content_types.get(ebook.format)
if not content_type:
return Response(
{"error": "Reader supports EPUB only."},
{"error": "Unsupported format for in-browser reading."},
status=status.HTTP_400_BAD_REQUEST,
)
if not ebook.file:
return Response({"error": "No file found for this e-book."}, status=status.HTTP_400_BAD_REQUEST)
return FileResponse(
ebook.file.open("rb"),
content_type="application/epub+zip",
content_type=content_type,
filename=ebook.filename(),
)