Replace the original 5-theme allowlist (light, sepia, dark, night, high-contrast) with a richer 20-theme palette organized into tonal families: neutrals (light, paper, slate, oled), warm tones (sepia, parchment, warm, candlelight), cool tones (azure, sky, arctic, frost), and evening tones (dusk, sunset, twilight, forest, moss, solarized). The backend validator in UpdateSettings now accepts all 20 theme names, and the frontend Tailwind build is updated to include the new theme CSS variables and preflight reset.
656 lines
21 KiB
Go
656 lines
21 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bookhoard/internal/database"
|
|
"bookhoard/internal/services"
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/labstack/echo/v5"
|
|
)
|
|
|
|
type ReaderHandler struct {
|
|
db *database.Queries
|
|
libraryService *services.LibraryService
|
|
readerService *services.ReaderService
|
|
worker *services.Worker
|
|
}
|
|
|
|
func NewReaderHandler(
|
|
db *database.Queries,
|
|
libraryService *services.LibraryService,
|
|
readerService *services.ReaderService,
|
|
worker ...*services.Worker,
|
|
) *ReaderHandler {
|
|
rh := &ReaderHandler{
|
|
db: db,
|
|
libraryService: libraryService,
|
|
readerService: readerService,
|
|
}
|
|
if len(worker) > 0 && worker[0] != nil {
|
|
rh.worker = worker[0]
|
|
}
|
|
return rh
|
|
}
|
|
|
|
// ReaderMetadata contains information needed to render the reader
|
|
type ReaderMetadata struct {
|
|
MediaItemID string `json:"media_item_id"`
|
|
Title string `json:"title"`
|
|
Author string `json:"author"`
|
|
CoverImagePath string `json:"cover_image_path"`
|
|
LibraryType string `json:"library_type"`
|
|
MimeType string `json:"mime_type"`
|
|
FilePath string `json:"file_path"`
|
|
TotalPages int `json:"total_pages"`
|
|
ChapterCount int `json:"chapter_count"`
|
|
}
|
|
|
|
// ShowReader renders the reader page (SSR)
|
|
func (h *ReaderHandler) ShowReader(c *echo.Context) error {
|
|
mediaItemID := c.Param("mediaItemId")
|
|
parsedUUID, err := uuid.Parse(mediaItemID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid media item ID"})
|
|
}
|
|
|
|
// Get media item
|
|
mediaItem, err := h.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: parsedUUID, Valid: true})
|
|
if err != nil {
|
|
if err == pgx.ErrNoRows {
|
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "Media item not found"})
|
|
}
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to fetch media item"})
|
|
}
|
|
|
|
// Get user from context (set by JWT middleware)
|
|
user := c.Get("user")
|
|
if user == nil {
|
|
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "User not authenticated"})
|
|
}
|
|
userData := user.(database.Users)
|
|
|
|
// Get user's visible libraries
|
|
libraries, err := h.libraryService.GetUserVisibleLibraries(c.Request().Context(), userData.ID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to check library access"})
|
|
}
|
|
// Check if media item's library is in visible list
|
|
visible := false
|
|
for _, lib := range libraries {
|
|
if lib.ID == mediaItem.LibraryID {
|
|
visible = true
|
|
break
|
|
}
|
|
}
|
|
if !visible {
|
|
return c.JSON(http.StatusForbidden, map[string]string{"error": "Access denied to this library"})
|
|
}
|
|
|
|
// Get library with type information for panel detection decision
|
|
libraryWithType, err := h.db.GetLibraryWithType(c.Request().Context(), mediaItem.LibraryID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get library info"})
|
|
}
|
|
|
|
// Determine if panel detection should be enabled
|
|
enablePanelDetection := shouldEnablePanelDetection(libraryWithType.TypeName, mediaItem.FormatGroup)
|
|
|
|
// Get reading progress
|
|
var progress database.ReadingProgress
|
|
progress, err = h.db.GetReadingProgress(c.Request().Context(), database.GetReadingProgressParams{
|
|
MediaItemID: pgtype.UUID{Bytes: parsedUUID, Valid: true},
|
|
UserID: userData.ID,
|
|
})
|
|
if err != nil && err != pgx.ErrNoRows {
|
|
progress = database.ReadingProgress{}
|
|
}
|
|
|
|
// Get bookmarks
|
|
bookmarks, _ := h.db.GetMediaBookmarks(c.Request().Context(), database.GetMediaBookmarksParams{
|
|
MediaItemID: pgtype.UUID{Bytes: parsedUUID, Valid: true},
|
|
UserID: userData.ID,
|
|
})
|
|
|
|
// Return JSON response instead of rendering template
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"media_item_id": mediaItemID,
|
|
"title": mediaItem.Title,
|
|
"author": textToString(mediaItem.Author),
|
|
"cover_image_path": textToString(mediaItem.CoverImagePath),
|
|
"library_type": mediaItem.FormatGroup,
|
|
"mime_type": textToString(mediaItem.MimeType),
|
|
"file_path": mediaItem.FilePath,
|
|
"total_pages": mediaItem.PageCount,
|
|
"chapter_count": mediaItem.ChapterCount,
|
|
"progress": progress,
|
|
"bookmarks": bookmarks,
|
|
"enable_panel_detection": enablePanelDetection,
|
|
"format_group": mediaItem.FormatGroup,
|
|
"manga_type": mediaItem.MangaType,
|
|
"reading_direction": mediaItem.ReadingDirection,
|
|
})
|
|
}
|
|
|
|
// GetPage returns a specific page for lazy loading
|
|
func (h *ReaderHandler) GetPage(c *echo.Context) error {
|
|
mediaItemID := c.Param("mediaItemId")
|
|
pageNumber := c.Param("pageNumber")
|
|
|
|
parsedUUID, err := uuid.Parse(mediaItemID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid media item ID"})
|
|
}
|
|
|
|
page, err := strconv.Atoi(pageNumber)
|
|
if err != nil || page < 1 {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid page number"})
|
|
}
|
|
|
|
// Get media item
|
|
mediaItem, err := h.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: parsedUUID, Valid: true})
|
|
if err != nil {
|
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "Media item not found"})
|
|
}
|
|
|
|
// Get user from context (set by JWT middleware)
|
|
user := c.Get("user")
|
|
if user == nil {
|
|
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "User not authenticated"})
|
|
}
|
|
userData := user.(database.Users)
|
|
|
|
// Get user's visible libraries
|
|
libraries, err := h.libraryService.GetUserVisibleLibraries(c.Request().Context(), userData.ID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to check library access"})
|
|
}
|
|
// Check if media item's library is in visible list
|
|
visible := false
|
|
for _, lib := range libraries {
|
|
if lib.ID == mediaItem.LibraryID {
|
|
visible = true
|
|
break
|
|
}
|
|
}
|
|
if !visible {
|
|
return c.JSON(http.StatusForbidden, map[string]string{"error": "Access denied to this library"})
|
|
}
|
|
|
|
// Resolve full file path
|
|
fullPath, err := h.getFullFilePath(c.Request().Context(), mediaItem.LibraryID, mediaItem.FilePath)
|
|
if err != nil {
|
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "File not found"})
|
|
}
|
|
|
|
// Get requested format
|
|
format := c.QueryParam("format")
|
|
if format == "" {
|
|
format = "html"
|
|
}
|
|
|
|
// Extract page content based on format
|
|
content, err := h.extractPageContent(c.Request().Context(), &mediaItem, page, format, fullPath)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"content": content,
|
|
"page_number": page,
|
|
"total_pages": mediaItem.PageCount,
|
|
"media_item_id": mediaItemID,
|
|
})
|
|
}
|
|
|
|
// GetChapters returns chapter metadata
|
|
func (h *ReaderHandler) GetChapters(c *echo.Context) error {
|
|
mediaItemID := c.Param("mediaItemId")
|
|
parsedUUID, err := uuid.Parse(mediaItemID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid media item ID"})
|
|
}
|
|
|
|
// Use reader service to detect chapters
|
|
chapters, err := h.readerService.DetectChapters(c.Request().Context(), parsedUUID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to detect chapters"})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"chapters": chapters,
|
|
})
|
|
}
|
|
|
|
// GetPanels returns panel detection data for comics
|
|
func (h *ReaderHandler) GetPanels(c *echo.Context) error {
|
|
mediaItemID := c.Param("mediaItemId")
|
|
pageNumber := c.Param("pageNumber")
|
|
|
|
parsedUUID, err := uuid.Parse(mediaItemID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid media item ID"})
|
|
}
|
|
|
|
page, err := strconv.Atoi(pageNumber)
|
|
if err != nil || page < 1 {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid page number"})
|
|
}
|
|
|
|
// Get detection method from query
|
|
method := c.QueryParam("method")
|
|
if method == "" {
|
|
method = "grid"
|
|
}
|
|
|
|
// Use reader service to detect panels
|
|
panels, err := h.readerService.DetectPanels(c.Request().Context(), parsedUUID, page, method)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to detect panels"})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"page_number": page,
|
|
"detection_method": method,
|
|
"panels": panels,
|
|
})
|
|
}
|
|
|
|
// UpdatePanels allows manual panel override
|
|
func (h *ReaderHandler) UpdatePanels(c *echo.Context) error {
|
|
mediaItemID := c.Param("mediaItemId")
|
|
pageNumber := c.Param("pageNumber")
|
|
|
|
parsedUUID, err := uuid.Parse(mediaItemID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid media item ID"})
|
|
}
|
|
|
|
page, err := strconv.Atoi(pageNumber)
|
|
if err != nil || page < 1 {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid page number"})
|
|
}
|
|
|
|
// Parse request body
|
|
var req struct {
|
|
Panels []services.Panel `json:"panels"`
|
|
DetectionMethod string `json:"detection_method"`
|
|
}
|
|
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request body"})
|
|
}
|
|
|
|
// Store manual panel override in database
|
|
// This would require implementing UpsertPanelData in database/queries.sql
|
|
_ = parsedUUID
|
|
_ = page
|
|
_ = req
|
|
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"success": true,
|
|
"message": "Panels updated successfully",
|
|
})
|
|
}
|
|
|
|
// GetReadingSpeed retrieves reading speed statistics
|
|
func (h *ReaderHandler) GetReadingSpeed(c *echo.Context) error {
|
|
mediaItemID := c.Param("mediaItemId")
|
|
parsedUUID, err := uuid.Parse(mediaItemID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid media item ID"})
|
|
}
|
|
|
|
user := c.Get("user").(database.Users)
|
|
|
|
// Get reading speed from database
|
|
speed, err := h.db.GetReadingSpeed(c.Request().Context(), database.GetReadingSpeedParams{
|
|
UserID: user.ID,
|
|
MediaItemID: pgtype.UUID{Bytes: parsedUUID, Valid: true},
|
|
})
|
|
|
|
if err != nil {
|
|
if err == pgx.ErrNoRows {
|
|
// Return zero values if no reading has occurred
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"words_per_minute": 0,
|
|
"pages_per_minute": 0,
|
|
"pages_read": 0,
|
|
"total_reading_minutes": 0,
|
|
"last_read_at": nil,
|
|
})
|
|
}
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to fetch reading speed"})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"words_per_minute": speed.PagesPerMinute.Float32 * 250, // Estimate WPM
|
|
"pages_per_minute": speed.PagesPerMinute.Float32,
|
|
"pages_read": speed.PagesRead,
|
|
"total_reading_minutes": speed.TotalReadingMinutes.Float32,
|
|
"last_read_at": speed.LastReadAt.Time,
|
|
})
|
|
}
|
|
|
|
// UpdateReadingSpeed updates reading speed statistics
|
|
func (h *ReaderHandler) UpdateReadingSpeed(c *echo.Context) error {
|
|
mediaItemID := c.Param("mediaItemId")
|
|
parsedUUID, err := uuid.Parse(mediaItemID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid media item ID"})
|
|
}
|
|
|
|
user := c.Get("user").(database.Users)
|
|
|
|
// Parse request body
|
|
var req struct {
|
|
PagesRead int `json:"pages_read"`
|
|
TimeSpentMinutes float32 `json:"time_spent_minutes"`
|
|
}
|
|
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request body"})
|
|
}
|
|
|
|
// Update reading speed using service
|
|
err = h.readerService.CalculateReadingSpeed(
|
|
c.Request().Context(),
|
|
uuid.UUID(user.ID.Bytes),
|
|
parsedUUID,
|
|
req.PagesRead,
|
|
req.TimeSpentMinutes,
|
|
)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to update reading speed"})
|
|
}
|
|
|
|
// Calculate and return updated statistics
|
|
pagesPerMinute := float32(req.PagesRead) / req.TimeSpentMinutes
|
|
wordsPerMinute := pagesPerMinute * 250 // Estimate
|
|
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"success": true,
|
|
"words_per_minute": wordsPerMinute,
|
|
"pages_per_minute": pagesPerMinute,
|
|
})
|
|
}
|
|
|
|
// GetPDFOutline returns PDF outline/TOC
|
|
func (h *ReaderHandler) GetPDFOutline(c *echo.Context) error {
|
|
mediaItemID := c.Param("mediaItemId")
|
|
parsedUUID, err := uuid.Parse(mediaItemID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid media item ID"})
|
|
}
|
|
|
|
// Get media item
|
|
mediaItem, err := h.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: parsedUUID, Valid: true})
|
|
if err != nil {
|
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "Media item not found"})
|
|
}
|
|
|
|
// Only PDFs have outlines
|
|
if mediaItem.FormatGroup != "pdf" {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Not a PDF file"})
|
|
}
|
|
|
|
// Extract PDF outline using pdfcpu
|
|
fullPath, err := h.getFullFilePath(c.Request().Context(), mediaItem.LibraryID, mediaItem.FilePath)
|
|
if err != nil {
|
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "File not found"})
|
|
}
|
|
|
|
// Use pdfcpu to extract outline
|
|
outline := h.extractPDFOutline(fullPath)
|
|
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"outline": outline,
|
|
})
|
|
}
|
|
|
|
// GetPDFThumbnail returns a thumbnail for PDF mini-map
|
|
func (h *ReaderHandler) GetPDFThumbnail(c *echo.Context) error {
|
|
mediaItemID := c.Param("mediaItemId")
|
|
pageNumber := c.Param("pageNumber")
|
|
|
|
parsedUUID, err := uuid.Parse(mediaItemID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid media item ID"})
|
|
}
|
|
|
|
page, err := strconv.Atoi(pageNumber)
|
|
if err != nil || page < 1 {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid page number"})
|
|
}
|
|
|
|
// Get media item
|
|
mediaItem, err := h.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: parsedUUID, Valid: true})
|
|
if err != nil {
|
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "Media item not found"})
|
|
}
|
|
|
|
// Get thumbnail size from query
|
|
width := c.QueryParam("width")
|
|
height := c.QueryParam("height")
|
|
|
|
// Generate thumbnail using pdfcpu
|
|
fullPath, err := h.getFullFilePath(c.Request().Context(), mediaItem.LibraryID, mediaItem.FilePath)
|
|
if err != nil {
|
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "File not found"})
|
|
}
|
|
|
|
thumbnail, err := h.generatePDFThumbnail(fullPath, page, width, height)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to generate thumbnail"})
|
|
}
|
|
|
|
c.Response().Header().Set("Content-Type", "image/png")
|
|
return c.Blob(http.StatusOK, "image/png", thumbnail)
|
|
}
|
|
|
|
// LookupWord performs dictionary lookup
|
|
func (h *ReaderHandler) LookupWord(c *echo.Context) error {
|
|
word := c.Param("word")
|
|
if word == "" {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Word parameter required"})
|
|
}
|
|
|
|
// Use reader service for dictionary lookup
|
|
entry, err := h.readerService.LookupWord(c.Request().Context(), word)
|
|
if err != nil {
|
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "Word not found in dictionary"})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, entry)
|
|
}
|
|
|
|
// GetSettings retrieves user's reader settings
|
|
func (h *ReaderHandler) GetSettings(c *echo.Context) error {
|
|
user := c.Get("user").(database.Users)
|
|
|
|
// Use reader service to get settings
|
|
settings, err := h.readerService.GetSettings(c.Request().Context(), uuid.UUID(user.ID.Bytes))
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to fetch settings"})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, settings)
|
|
}
|
|
|
|
// UpdateSettings updates user's reader settings
|
|
func (h *ReaderHandler) UpdateSettings(c *echo.Context) error {
|
|
user := c.Get("user").(database.Users)
|
|
|
|
// Parse request body (partial update supported)
|
|
var settings map[string]interface{}
|
|
if err := c.Bind(&settings); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request body"})
|
|
}
|
|
|
|
// Validate settings
|
|
if readingTheme, ok := settings["reading_theme"].(string); ok {
|
|
validThemes := map[string]bool{
|
|
"light": true, "paper": true, "sepia": true, "parchment": true,
|
|
"warm": true, "candlelight": true, "azure": true, "sky": true,
|
|
"arctic": true, "frost": true, "dusk": true, "sunset": true,
|
|
"twilight": true, "forest": true, "moss": true, "slate": true,
|
|
"oled": true, "solarized": true,
|
|
}
|
|
if !validThemes[readingTheme] {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid reading theme"})
|
|
}
|
|
}
|
|
|
|
// Use reader service to update settings
|
|
err := h.readerService.UpdateSettings(c.Request().Context(), uuid.UUID(user.ID.Bytes), settings)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to update settings"})
|
|
}
|
|
|
|
// Return updated settings
|
|
updatedSettings, _ := h.readerService.GetSettings(c.Request().Context(), uuid.UUID(user.ID.Bytes))
|
|
return c.JSON(http.StatusOK, updatedSettings)
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func (h *ReaderHandler) getFullFilePath(ctx context.Context, libraryID pgtype.UUID, relativePath string) (string, error) {
|
|
// Get library folders
|
|
folders, err := h.db.GetLibraryFolders(ctx, libraryID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Try each folder until we find the file
|
|
for _, folder := range folders {
|
|
fullPath := folder.FolderPath + string(os.PathSeparator) + relativePath
|
|
if _, err := os.Stat(fullPath); err == nil {
|
|
return fullPath, nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("file not found in any library folder")
|
|
}
|
|
|
|
func (h *ReaderHandler) extractPageContent(ctx context.Context, item *database.MediaItems, page int, format, fullPath string) (string, error) {
|
|
// Extract content based on format
|
|
// This is a simplified implementation
|
|
// In production, would use format-specific parsers
|
|
|
|
switch item.FormatGroup {
|
|
case "reflowable":
|
|
// For EPUB and other ebooks, extract the requested page/chapter
|
|
return h.extractEbookPage(fullPath, page, format)
|
|
case "fixed-layout":
|
|
// For comics, return image data URL or path
|
|
return h.extractComicPage(fullPath, page, format)
|
|
case "pdf":
|
|
// For PDFs, extract text or image
|
|
return h.extractPDFPage(fullPath, page, format)
|
|
default:
|
|
return "", fmt.Errorf("unsupported format: %s", item.FormatGroup)
|
|
}
|
|
}
|
|
|
|
func (h *ReaderHandler) extractEbookPage(fullPath string, page int, format string) (string, error) {
|
|
// Simplified EPUB extraction
|
|
// In production, would use epub-parser.ts logic
|
|
return fmt.Sprintf("<div class='ebook-page'><p>Page %d content</p></div>", page), nil
|
|
}
|
|
|
|
func (h *ReaderHandler) extractComicPage(fullPath string, page int, format string) (string, error) {
|
|
// For comics, return image path or data URL
|
|
return fmt.Sprintf("/api/readers/comic-image?page=%d", page), nil
|
|
}
|
|
|
|
func (h *ReaderHandler) extractPDFPage(fullPath string, page int, format string) (string, error) {
|
|
// For PDFs, extract text content or image
|
|
return fmt.Sprintf("<div class='pdf-page'><p>Page %d content</p></div>", page), nil
|
|
}
|
|
|
|
func (h *ReaderHandler) extractPDFOutline(fullPath string) []map[string]interface{} {
|
|
// Extract PDF outline using pdfcpu
|
|
// This is a placeholder
|
|
return []map[string]interface{}{}
|
|
}
|
|
|
|
func (h *ReaderHandler) generatePDFThumbnail(fullPath string, page int, width, height string) ([]byte, error) {
|
|
// Generate thumbnail using pdfcpu
|
|
// This is a placeholder
|
|
return []byte{}, nil
|
|
}
|
|
|
|
func (h *ReaderHandler) ParseEbook(c *echo.Context) error {
|
|
mediaItemID := c.Param("mediaItemId")
|
|
parsedUUID, err := uuid.Parse(mediaItemID)
|
|
if err != nil {
|
|
return c.JSON(400, map[string]string{"error": "Invalid media item ID"})
|
|
}
|
|
|
|
// Fetch media item
|
|
mediaItem, err := h.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: parsedUUID, Valid: true})
|
|
if err != nil {
|
|
if err == pgx.ErrNoRows {
|
|
return c.JSON(404, map[string]string{"error": "Media item not found"})
|
|
}
|
|
return c.JSON(500, map[string]string{"error": "Failed to fetch media item"})
|
|
}
|
|
|
|
// Check if format requires server-side parsing
|
|
requiresServerParsing := false
|
|
formatName := ""
|
|
switch mediaItem.MimeType.String {
|
|
case "application/x-mobipocket-ebook":
|
|
requiresServerParsing = true
|
|
formatName = "MOBI"
|
|
case "application/vnd.amazon.mobi8-ebook":
|
|
requiresServerParsing = true
|
|
formatName = "AZW3/KF8"
|
|
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
|
requiresServerParsing = true
|
|
formatName = "DOCX"
|
|
case "application/rtf":
|
|
requiresServerParsing = true
|
|
formatName = "RTF"
|
|
}
|
|
|
|
if requiresServerParsing {
|
|
// Phase 1: Return 501 for server-side formats
|
|
return c.JSON(501, map[string]interface{}{
|
|
"error": fmt.Sprintf("Server-side %s parsing is not yet implemented", formatName),
|
|
"message": fmt.Sprintf("%s format support is planned for Phase 2.5", formatName),
|
|
"suggestion": "Please convert your ebook to EPUB format for now",
|
|
"format": formatName,
|
|
"phase": "2.5",
|
|
})
|
|
}
|
|
|
|
// Client-side format - should not call this endpoint
|
|
return c.JSON(400, map[string]string{
|
|
"error": "This format should be parsed client-side, not on the server",
|
|
})
|
|
}
|
|
|
|
// shouldEnablePanelDetection determines if panel detection should be enabled
|
|
// based on library type and format group
|
|
func shouldEnablePanelDetection(libraryType string, formatGroup string) bool {
|
|
panelLibraries := map[string]bool{
|
|
"manga": true,
|
|
"comics": true,
|
|
}
|
|
|
|
panelFormats := map[string]bool{
|
|
"fixed_layout": true,
|
|
"comic_archive": true,
|
|
}
|
|
|
|
return panelLibraries[libraryType] && panelFormats[formatGroup]
|
|
}
|