- Add shouldEnablePanelDetection to determine when to enable panel detection - Enable for manga/comics libraries with fixed_layout or comic_archive formats - Fetch library type info using GetLibraryWithType query - Pass panel detection config to reader initialization - Include format_group, manga_type, and reading_direction in reader response
652 lines
21 KiB
Go
652 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, "sepia": true, "dark": true, "night": true, "high-contrast": 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]
|
|
}
|