from rest_framework import serializers import logging from apps.books.models import Book, BookChapter, EBook, ReadingProgress, ReadingStatus, DownloadRecord from apps.books.services.ebook_metadata import subjects_from_ebook from apps.books.services.metadata import enrich_ebook_metadata from apps.books.services.process_ebook import apply_processing_to_ebook from apps.reader.models import ReadingSettings logger = logging.getLogger(__name__) class BookReadingSettingsSerializer(serializers.ModelSerializer): font_style = serializers.CharField(source="font_family") class Meta: model = ReadingSettings fields = ["font_size", "font_style", "background_color"] def validate_font_size(self, value: int) -> int: if value < 12 or value > 36: raise serializers.ValidationError("Font size must be between 12 and 36.") return value class BookChapterSerializer(serializers.ModelSerializer): class Meta: model = BookChapter fields = ["id", "title", "index", "href", "children"] class EBookContentSerializer(serializers.Serializer): page = serializers.IntegerField() total_pages = serializers.IntegerField() content = serializers.CharField() chapter_title = serializers.CharField() format = serializers.CharField() class EBookTocSerializer(serializers.Serializer): chapters = serializers.ListField(child=BookChapterSerializer()) format = serializers.CharField() page_count = serializers.IntegerField() class BookListSerializer(serializers.ModelSerializer): reading_status_display = serializers.CharField(source="get_reading_status_display", read_only=True) class Meta: model = Book fields = ["id", "title", "author", "genre", "reading_status", "reading_status_display", "cover_image"] class BookDetailSerializer(serializers.ModelSerializer): reading_status_display = serializers.CharField(source="get_reading_status_display", read_only=True) class Meta: model = Book fields = ["id", "title", "author", "genre", "description", "reading_status", "reading_status_display", "cover_image", "total_pages", "created_at", "updated_at"] read_only_fields = ["id", "created_at", "updated_at"] class BookSerializer(serializers.ModelSerializer): class Meta: model = Book fields = ["id", "title", "author", "genre", "description", "reading_status", "cover_image", "total_pages", "created_at", "updated_at"] read_only_fields = ["id", "created_at", "updated_at"] class EBookListSerializer(serializers.ModelSerializer): filename = serializers.CharField(read_only=True) format = serializers.CharField(read_only=True) progress = serializers.SerializerMethodField() started = serializers.SerializerMethodField() subjects = serializers.SerializerMethodField() class Meta: model = EBook fields = [ "id", "title", "author", "filename", "format", "page_count", "file_size", "cover_image", "created_at", "progress", "started", "subjects", ] def get_subjects(self, obj: EBook) -> list[str]: return subjects_from_ebook(obj) def get_progress(self, obj): try: return obj.reading_progress.current_position except ReadingProgress.DoesNotExist: return None def get_started(self, obj): """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()) class EBookDetailSerializer(serializers.ModelSerializer): filename = serializers.CharField(read_only=True) format = serializers.CharField(read_only=True) file_url = serializers.SerializerMethodField() progress = serializers.SerializerMethodField() metadata = serializers.JSONField(source="metadata_json", read_only=True) class Meta: model = EBook fields = [ "id", "title", "author", "filename", "format", "page_count", "file_size", "file_url", "cover_image", "metadata", "created_at", "updated_at", "progress", ] def get_file_url(self, obj): request = self.context.get("request") if request and obj.file: return request.build_absolute_uri(obj.file.url) return "" def get_progress(self, obj): try: rp = obj.reading_progress return { "current_position": rp.current_position, "last_page": rp.last_page, "epub_location": rp.epub_location, } except ReadingProgress.DoesNotExist: return None class EBookUploadSerializer(serializers.ModelSerializer): class Meta: model = EBook fields = ["title", "author", "file", "cover_image"] extra_kwargs = {"title": {"required": True}, "file": {"required": True}} def validate_file(self, value): import os if value is None: return value ext = os.path.splitext(str(getattr(value, "name", "")))[1].lower() if ext not in (".epub", ".pdf"): raise serializers.ValidationError("Only EPUB and PDF files are supported.") return value def create(self, validated_data): import os validated_data["user"] = self.context["request"].user name = str(getattr(validated_data.get("file"), "name", "")) ext = os.path.splitext(name)[1].lower().lstrip(".") if ext: validated_data["format"] = ext ebook = super().create(validated_data) try: apply_processing_to_ebook(ebook) except Exception: logger.exception("E-book processing failed for ebook %s", ebook.pk) try: enrich_ebook_metadata(ebook) except Exception: logger.exception("Metadata enrichment failed for ebook %s", ebook.pk) ebook.refresh_from_db() return ebook class ReadingProgressSerializer(serializers.ModelSerializer): class Meta: model = ReadingProgress fields = [ "current_position", "last_page", "epub_location", "device_id", "device_name", "version", "updated_at", ] read_only_fields = ["version", "updated_at"] extra_kwargs = {"current_position": {"required": False, "min_value": 0.0, "max_value": 100.0}} def validate_current_position(self, value): if value < 0.0 or value > 100.0: raise serializers.ValidationError("Position must be between 0.0 and 100.0.") return value class DownloadRecordSerializer(serializers.ModelSerializer): ebook_id = serializers.IntegerField(source="ebook.id", read_only=True) ebook_title = serializers.CharField(source="ebook.title", read_only=True) author = serializers.CharField(source="ebook.author", read_only=True) filename = serializers.SerializerMethodField() cover_image = serializers.ImageField(source="ebook.cover_image", read_only=True) format = serializers.CharField(source="ebook.format", read_only=True) progress = serializers.SerializerMethodField() file_url = serializers.SerializerMethodField() class Meta: model = DownloadRecord fields = [ "id", "ebook_id", "ebook_title", "author", "filename", "file_url", "file_size", "cover_image", "format", "downloaded_at", "progress", ] def get_filename(self, obj): return obj.ebook.filename() def get_file_url(self, obj): request = self.context.get("request") if request and obj.ebook.file: return request.build_absolute_uri(obj.ebook.file.url) return "" def get_progress(self, obj): try: rp = obj.ebook.reading_progress return { "current_position": rp.current_position, "last_page": rp.last_page, "epub_location": rp.epub_location, } except ReadingProgress.DoesNotExist: return None class StorageSummarySerializer(serializers.Serializer): total_downloads = serializers.IntegerField() total_size_bytes = serializers.IntegerField() ebooks = serializers.ListField(child=serializers.DictField())