Archived
feat: uv config other feats
- add uv configuration for the backend - update frontend to make auth work - add new auth endpoints - add bookmars feat - add reader feat
This commit is contained in:
+59
-41
@@ -4,9 +4,10 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from django.db.models import QuerySet, Q
|
||||
from django.http import FileResponse
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import parsers, permissions, status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.decorators import action, api_view, permission_classes
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.request import Request
|
||||
@@ -14,11 +15,13 @@ from rest_framework.response import Response
|
||||
|
||||
from apps.books.models import Book, BookChapter, DownloadRecord, EBook, ReadingProgress
|
||||
from apps.books.serializers import (
|
||||
BookChapterSerializer, BookDetailSerializer, BookListSerializer, BookSerializer,
|
||||
DownloadRecordSerializer, EBookContentSerializer, EBookDetailSerializer,
|
||||
BookChapterSerializer, BookDetailSerializer, BookListSerializer, BookReadingSettingsSerializer,
|
||||
BookSerializer, DownloadRecordSerializer, EBookContentSerializer, EBookDetailSerializer,
|
||||
EBookListSerializer, EBookTocSerializer, EBookUploadSerializer,
|
||||
ReadingProgressSerializer, StorageSummarySerializer,
|
||||
)
|
||||
from apps.reader.models import ReadingSettings
|
||||
from apps.books.services.metadata import enrich_ebook_metadata
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -107,6 +110,22 @@ class EBookViewSet(viewsets.ModelViewSet):
|
||||
serializer.save()
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=["post"], url_path="enrich-metadata")
|
||||
def enrich_metadata(self, request: Request, pk: int | None = None) -> Response:
|
||||
"""Re-fetch Open Library metadata and cover for this ebook."""
|
||||
ebook = self.get_object()
|
||||
try:
|
||||
enrich_ebook_metadata(ebook)
|
||||
except Exception:
|
||||
logger.exception("Manual metadata enrichment failed for ebook %s", ebook.id)
|
||||
return Response(
|
||||
{"error": "Metadata enrichment failed."},
|
||||
status=status.HTTP_502_BAD_GATEWAY,
|
||||
)
|
||||
ebook.refresh_from_db()
|
||||
serializer = EBookDetailSerializer(ebook, context={"request": request})
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=["post"])
|
||||
def process(self, request: Request, pk: int | None = None) -> Response:
|
||||
"""Trigger e-book processing: metadata extraction, TOC building, page counting."""
|
||||
@@ -115,33 +134,31 @@ class EBookViewSet(viewsets.ModelViewSet):
|
||||
return Response({"error": "No file found for this e-book."}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
from apps.books.services import process_ebook
|
||||
from apps.books.services.process_ebook import apply_processing_to_ebook
|
||||
|
||||
file_path = ebook.file.path
|
||||
result = process_ebook(file_path, original_filename=ebook.filename())
|
||||
|
||||
# Update ebook with extracted data
|
||||
ebook.format = result.get("format", ebook.format)
|
||||
ebook.page_count = result.get("page_count", 0)
|
||||
ebook.metadata_json = result.get("metadata", {})
|
||||
ebook.save(update_fields=["format", "page_count", "metadata_json", "updated_at"])
|
||||
|
||||
# Store chapters in DB
|
||||
raw_toc: list[dict[str, Any]] = result.get("toc", [])
|
||||
BookChapter.objects.filter(ebook=ebook).delete()
|
||||
_store_chapters(ebook, raw_toc)
|
||||
|
||||
return Response({
|
||||
"format": ebook.format,
|
||||
"page_count": ebook.page_count,
|
||||
"metadata": ebook.metadata_json,
|
||||
"toc_count": len(raw_toc),
|
||||
"status": "processed",
|
||||
})
|
||||
result = apply_processing_to_ebook(ebook)
|
||||
return Response(result)
|
||||
except Exception as exc:
|
||||
logger.exception("Failed to process ebook %s", ebook.id)
|
||||
return Response({"error": f"Processing failed: {exc}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
@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."""
|
||||
ebook = self.get_object()
|
||||
if ebook.format != "epub":
|
||||
return Response(
|
||||
{"error": "Reader supports EPUB only."},
|
||||
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",
|
||||
filename=ebook.filename(),
|
||||
)
|
||||
|
||||
@action(detail=True, methods=["get"])
|
||||
def toc(self, request: Request, pk: int | None = None) -> Response:
|
||||
"""Return hierarchical table of contents."""
|
||||
@@ -229,21 +246,6 @@ class EBookViewSet(viewsets.ModelViewSet):
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
def _store_chapters(ebook: EBook, toc: list[dict[str, Any]], parent_index: int = 0) -> None:
|
||||
"""Recursively store TOC entries as BookChapter records."""
|
||||
for idx, entry in enumerate(toc):
|
||||
BookChapter.objects.create(
|
||||
ebook=ebook,
|
||||
title=entry.get("title", "Untitled"),
|
||||
index=parent_index + idx,
|
||||
href=entry.get("href", ""),
|
||||
children=entry.get("children", []),
|
||||
)
|
||||
children = entry.get("children", [])
|
||||
if children:
|
||||
_store_chapters(ebook, children, parent_index + idx + 1)
|
||||
|
||||
|
||||
def _fetch_chapter_content(ebook: EBook, chapter: BookChapter) -> str:
|
||||
"""Fetch HTML content for a chapter from the e-book file."""
|
||||
if ebook.format == "epub":
|
||||
@@ -276,4 +278,20 @@ def _fetch_epub_chapter_content(file_path: str, chapter: BookChapter) -> str:
|
||||
return ""
|
||||
except Exception:
|
||||
logger.exception("Failed to fetch EPUB chapter content for %s", chapter.href)
|
||||
return ""
|
||||
return ""
|
||||
|
||||
|
||||
@api_view(["GET", "PATCH"])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def book_reading_settings_view(request: Request) -> Response:
|
||||
"""Get or update reading settings via the books API contract."""
|
||||
settings, _created = ReadingSettings.objects.get_or_create(user=request.user)
|
||||
|
||||
if request.method == "GET":
|
||||
serializer = BookReadingSettingsSerializer(settings)
|
||||
return Response(serializer.data)
|
||||
|
||||
serializer = BookReadingSettingsSerializer(settings, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response(serializer.data)
|
||||
Reference in New Issue
Block a user