Archived
feat: pdf reader
This commit is contained in:
@@ -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())
|
||||
|
||||
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
@@ -21,4 +21,5 @@ dependencies = [
|
||||
"httpx>=0.28.0",
|
||||
"ebooklib>=0.18",
|
||||
"beautifulsoup4>=4.12.0",
|
||||
"pypdf>=5.0.0",
|
||||
]
|
||||
|
||||
Generated
+11
@@ -52,6 +52,7 @@ dependencies = [
|
||||
{ name = "psycopg2-binary" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "pypdf" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-cov" },
|
||||
{ name = "pytest-django" },
|
||||
@@ -74,6 +75,7 @@ requires-dist = [
|
||||
{ name = "psycopg2-binary", specifier = "==2.9.10" },
|
||||
{ name = "pydantic", specifier = "==2.10.5" },
|
||||
{ name = "pydantic-settings", specifier = "==2.7.1" },
|
||||
{ name = "pypdf", specifier = ">=5.0.0" },
|
||||
{ name = "pytest", specifier = "==8.3.4" },
|
||||
{ name = "pytest-cov", specifier = "==6.0.0" },
|
||||
{ name = "pytest-django", specifier = "==4.9.0" },
|
||||
@@ -567,6 +569,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pypdf"
|
||||
version = "6.12.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0a/6d/20879428577c1e57ecd41b69dc86beabf43db9287ad2e702207f8b48c751/pypdf-6.12.2.tar.gz", hash = "sha256:111669eb6680c04495ae0c113a1476e3bf93a95761d23c7406b591c80a6490b1", size = 6468184, upload-time = "2026-05-26T13:31:26.911Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/44/fee070a16639d9869bb6a7e0f3a1b3946da1d66f32b9260b4d19cb90d7b2/pypdf-6.12.2-py3-none-any.whl", hash = "sha256:67b2699357a1f3f4c945940ea80826349ee507c9e2577724a14b4941982c104d", size = 343865, upload-time = "2026-05-26T13:31:25.068Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.3.4"
|
||||
|
||||
Reference in New Issue
Block a user