package router import ( "bookhoard/internal/database" "bookhoard/internal/handlers" "bookhoard/internal/services" "bookhoard/templates" "bytes" "errors" "fmt" "net/http" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" "github.com/labstack/echo/v5" ) func registerReaderRoutes(cfg *Config) { e := cfg.Echo // Create reader service and handler readerService := services.NewReaderService(cfg.Queries, cfg.Worker) cfg.ReaderHandler = handlers.NewReaderHandler( cfg.Queries, services.NewLibraryService(cfg.Queries), readerService, cfg.Worker, ) jwtMiddleware := createJWTMiddleware(cfg) // Reader page routes (SSR + API) reader := e.Group("/readers", jwtMiddleware) // Reader page (SSR) reader.GET("/:mediaItemId", func(c *echo.Context) error { // Get user user, err := getTemplateUserWithTheme(c, cfg) if err != nil { return renderErrorPage(c, "Error loading user", "user_load_error") } // Parse mediaItemID mediaItemID := c.Param("mediaItemId") parsedUUID, err := uuid.Parse(mediaItemID) if err != nil { return renderErrorPage(c, "Invalid media item ID", "invalid_id") } // Fetch media item (directly, no handler) mediaItem, err := cfg.Queries.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: parsedUUID, Valid: true}) if err != nil { return renderErrorPage(c, "Media item not found", "not_found") } // Check library access (using pattern from opds.go) userUUID, _ := uuid.Parse(user.ID) libraries, err := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID)) if err != nil { return renderErrorPage(c, "Failed to check access", "access_error") } visible := false for _, lib := range libraries { if lib.ID == mediaItem.LibraryID { visible = true break } } if !visible { return renderErrorPage(c, "Access denied", "access_denied") } // Get reading progress var progress database.ReadingProgress progress, err = cfg.Queries.GetReadingProgress(c.Request().Context(), database.GetReadingProgressParams{ MediaItemID: pgtype.UUID{Bytes: parsedUUID, Valid: true}, UserID: uuidToPGType(userUUID), }) if err != nil && !errors.Is(err, pgx.ErrNoRows) { progress = database.ReadingProgress{} } // Get bookmarks bookmarks, _ := cfg.Queries.GetMediaBookmarks(c.Request().Context(), database.GetMediaBookmarksParams{ MediaItemID: pgtype.UUID{Bytes: parsedUUID, Valid: true}, UserID: uuidToPGType(userUUID), }) // Convert to template types mediaUUID, _ := uuid.FromBytes(mediaItem.ID.Bytes[0:16]) libUUID, _ := uuid.FromBytes(mediaItem.LibraryID.Bytes[0:16]) metadata := templates.ReaderMetadata{ MediaItemID: mediaUUID.String(), Title: mediaItem.Title, Author: textToString(mediaItem.Author), CoverImagePath: textToString(mediaItem.CoverImagePath), LibraryType: mediaItem.FormatGroup, MimeType: textToString(mediaItem.MimeType), FilePath: mediaItem.FilePath, TotalPages: int(mediaItem.PageCount.Int32), ChapterCount: int(mediaItem.ChapterCount.Int32), FormatGroup: mediaItem.FormatGroup, MangaType: textToString(mediaItem.MangaType), ReadingDirection: textToString(mediaItem.ReadingDirection), LibraryID: libUUID.String(), FileURL: fmt.Sprintf("/uploads/library-%s/%s", libUUID.String(), mediaItem.FilePath), } // Progress conversion (inline) progressUUID, _ := uuid.FromBytes(progress.ID.Bytes[0:16]) progressMediaUUID, _ := uuid.FromBytes(progress.MediaItemID.Bytes[0:16]) progressUserUUID, _ := uuid.FromBytes(progress.UserID.Bytes[0:16]) templateProgress := templates.ReadingProgress{ ID: progressUUID.String(), MediaItemID: progressMediaUUID.String(), UserID: progressUserUUID.String(), CurrentPage: int(progress.CurrentPage.Int32), TotalPages: int(progress.TotalPages.Int32), Percentage: progress.Percentage.Float64, EpubCfi: textToString(progress.Epubcfi), LastReadAt: progress.LastReadAt.Time, } // Bookmarks conversion (inline, with loop) templateBookmarks := make([]templates.Bookmark, len(bookmarks)) for i, b := range bookmarks { bookmarkUUID, _ := uuid.FromBytes(b.ID.Bytes[0:16]) bookmarkMediaUUID, _ := uuid.FromBytes(b.MediaItemID.Bytes[0:16]) bookmarkUserUUID, _ := uuid.FromBytes(b.UserID.Bytes[0:16]) var pageNumber *int if b.PageNumber.Valid { pageNumber = new(int(b.PageNumber.Int32)) } var chapterNumber *int if b.ChapterNumber.Valid { chapterNumber = new(int(b.ChapterNumber.Int32)) } templateBookmarks[i] = templates.Bookmark{ ID: bookmarkUUID.String(), MediaItemID: bookmarkMediaUUID.String(), UserID: bookmarkUserUUID.String(), PageNumber: pageNumber, ChapterNumber: chapterNumber, CfiPosition: textToString(b.CfiPosition), Title: b.Title, Position: textToString(b.Position), Notes: textToString(b.Notes), CreatedAt: b.CreatedAt.Time, } } // 8. Render template var buf bytes.Buffer err = templates.Reader(user, metadata, templateProgress, templateBookmarks).Render(c.Request().Context(), &buf) if err != nil { return renderErrorPage(c, "Error rendering reader", "render_error") } return c.HTML(http.StatusOK, buf.String()) }) // Content serving (lazy-loaded pages) reader.GET("/:mediaItemId/pages/:pageNumber", cfg.ReaderHandler.GetPage) // Chapter metadata reader.GET("/:mediaItemId/chapters", cfg.ReaderHandler.GetChapters) // Panel data (comics/manga) reader.GET("/:mediaItemId/panels/:pageNumber", cfg.ReaderHandler.GetPanels) reader.PUT("/:mediaItemId/panels/:pageNumber", cfg.ReaderHandler.UpdatePanels) // PDF outline/TOC reader.GET("/:mediaItemId/outline", cfg.ReaderHandler.GetPDFOutline) // PDF thumbnails (for mini-map) reader.GET("/:mediaItemId/thumbnails/:pageNumber", cfg.ReaderHandler.GetPDFThumbnail) // Reading speed reader.GET("/:mediaItemId/reading-speed", cfg.ReaderHandler.GetReadingSpeed) reader.POST("/:mediaItemId/reading-speed", cfg.ReaderHandler.UpdateReadingSpeed) // Dictionary lookup reader.GET("/dictionary/:word", cfg.ReaderHandler.LookupWord) // Reader settings reader.GET("/settings", cfg.ReaderHandler.GetSettings) reader.PUT("/settings", cfg.ReaderHandler.UpdateSettings) }