docs(ebook): add comprehensive ebook.go refactor plan
- Add EBOOK_REFACTOR_PLAN.md with 856 lines of detailed instructions - Split 1,350-line ebook.go into focused single-responsibility files - Zero API changes, only code organization for maintainability - Phase-by-phase safety checkpoints and rollback procedures Target file organization after refactor: - media.go (~600 lines): Media CRUD + metadata - search.go (~80 lines): Query and search operations - matching.go (~200 lines): Book matching and sync operations - ebook.go (~150 lines): SetupRoutes only Plan ensures AI can implement without breaking any functionality.
This commit is contained in:
@@ -0,0 +1,857 @@
|
||||
# Comprehensive ebook.go Refactor Plan
|
||||
|
||||
**Created:** February 6, 2026
|
||||
**Purpose:** Refactor 1,350-line ebook.go into focused, single-responsibility files
|
||||
**Assumption:** Scanner routes already moved to router/scanner.go
|
||||
**Goal:** Zero API changes, 100% functional compatibility
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
`internal/handlers/ebook.go` violates Go single responsibility principle at 1,350 lines:
|
||||
- **Scanner operations**: 296 lines (12 methods)
|
||||
- **Media CRUD & metadata**: 822 lines (24 methods)
|
||||
- **Book matching/sync**: ~150 lines (8 methods)
|
||||
- **Collection device mappings**: 60+ lines (6 methods)
|
||||
|
||||
**Proposed split:**
|
||||
1. `media.go` - Media items, ratings, progress, notes, highlights (CRUD + metadata)
|
||||
2. `search.go` - Query, search, filtered operations
|
||||
3. `matching.go` - Book linking, file aliases, matching algorithms
|
||||
4. `ebook.go` - Slimmed to only core Handler struct and SetupRoutes
|
||||
|
||||
**Risk Level:** VERY LOW
|
||||
- No API endpoint changes
|
||||
- No database schema changes
|
||||
- Only code organization
|
||||
- All existing tests continue to work
|
||||
|
||||
---
|
||||
|
||||
## File Analysis Before Refactor
|
||||
|
||||
### Current ebook.go Structure (1,350 lines)
|
||||
|
||||
```go
|
||||
// Lines 38-164: Handler setup and SetupRoutes()
|
||||
func NewHandler(...) *Handler { }
|
||||
func SetupRoutes(...) *Handler { }
|
||||
|
||||
// Lines 166-461: Scanner operations (12 methods)
|
||||
func (h *Handler) ScanEbooks() { }
|
||||
func (h *Handler) StartScanner() { }
|
||||
// ... 10 more scanner methods
|
||||
|
||||
// Lines 474-1296: Media operations (24 methods)
|
||||
func (h *Handler) ListMediaItems() { }
|
||||
func (h *Handler) GetMediaItem() { }
|
||||
func (h *Handler) CreateMediaRating() { }
|
||||
// ... 21 more media methods
|
||||
|
||||
// Lines 1297-end: Search & matching (8 methods)
|
||||
func (h *Handler) SearchMediaItems() { }
|
||||
func (h *Handler) QueryBooks() { }
|
||||
// ... 6 more methods
|
||||
```
|
||||
|
||||
### Handler Dependencies
|
||||
|
||||
All methods share this Handler struct:
|
||||
```go
|
||||
type Handler struct {
|
||||
db *database.Queries
|
||||
worker *worker.Worker
|
||||
scanner *services.EbookScanner
|
||||
scheduler *cron.Cron
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
mu sync.RWMutex
|
||||
watchingLibraries map[string]bool
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: SetupRoutes Restructure (FOUNDATION)
|
||||
|
||||
### Purpose
|
||||
Remove all route registration from SetupRoutes, make it a pure factory function
|
||||
|
||||
### File: `internal/handlers/ebook.go`
|
||||
|
||||
#### Step 1.1: Simplify SetupRoutes (Lines 70-157)
|
||||
|
||||
**Current Code Problem:**
|
||||
```go
|
||||
func SetupRoutes(g *echo.Group, db *database.Queries, connManager *wsync.ConnectionManager) *Handler {
|
||||
h := NewHandler(db, connManager)
|
||||
|
||||
// Collections API routes (21 lines)
|
||||
collectionHandler := NewCollectionHandler(db, connManager)
|
||||
collections := g.Group("/collections")
|
||||
collections.GET("", collectionHandler.GetCollections)
|
||||
// ... 7 more collection routes
|
||||
|
||||
// Device shelf mapping routes (6 lines)
|
||||
deviceCollections := g.Group("/devices/:id/collections")
|
||||
// ... 6 device routes
|
||||
|
||||
// Book matching routes (4 lines)
|
||||
g.POST("/sync/books/query", h.QueryBooks)
|
||||
// ... 3 more matching routes
|
||||
|
||||
// Media item routes (4 lines)
|
||||
g.GET("/media-items", h.ListMediaItems)
|
||||
// ... 3 more media routes
|
||||
|
||||
// Admin-only routes (11 lines)
|
||||
admin := g.Group("", AdminMiddleware)
|
||||
admin.POST("/media-items", h.CreateMediaItem)
|
||||
// ... 10 more admin routes (including scanner)
|
||||
|
||||
return h
|
||||
}
|
||||
```
|
||||
|
||||
**New Code:**
|
||||
```go
|
||||
func SetupRoutes(g *echo.Group, db *database.Queries, connManager *wsync.ConnectionManager) *Handler {
|
||||
return NewHandler(db, connManager)
|
||||
}
|
||||
```
|
||||
|
||||
**Verification:**
|
||||
- ✅ SetupRoutes now only creates handler instance
|
||||
- ✅ All routes will be registered via router package
|
||||
- ✅ Handler struct and dependencies unchanged
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Create Media Handler (CORE CRUD)
|
||||
|
||||
### File: `internal/handlers/media.go` (NEW)
|
||||
|
||||
#### Step 2.1: Create MediaHandler struct
|
||||
|
||||
```go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bookhoard/internal/database"
|
||||
"bookhoard/internal/sync"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// MediaHandler handles media item CRUD operations and metadata
|
||||
type MediaHandler struct {
|
||||
db *database.Queries
|
||||
worker *worker.Worker
|
||||
}
|
||||
|
||||
// NewMediaHandler creates a new media handler
|
||||
func NewMediaHandler(db *database.Queries, worker *worker.Worker) *MediaHandler {
|
||||
return &MediaHandler{
|
||||
db: db,
|
||||
worker: worker,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 2.2: Move media CRUD methods (From ebook.go lines 474-959)
|
||||
|
||||
**Methods to move:**
|
||||
- `ListMediaItems()` → `media.go:ListMediaItems()`
|
||||
- `GetMediaItem()` → `media.go:GetMediaItem()`
|
||||
- `ListMediaItemsFiltered()` → `media.go:ListMediaItemsFiltered()`
|
||||
- `CreateMediaItem()` → `media.go:CreateMediaItem()`
|
||||
- `UpdateMediaItem()` → `media.go:UpdateMediaItem()`
|
||||
- `DeleteMediaItem()` → `media.go:DeleteMediaItem()`
|
||||
- `SearchMediaItems()` → `media.go:SearchMediaItems()`
|
||||
|
||||
**Method signature changes:**
|
||||
```go
|
||||
// OLD in ebook.go
|
||||
func (h *Handler) ListMediaItems(c echo.Context) error
|
||||
|
||||
// NEW in media.go
|
||||
func (mh *MediaHandler) ListMediaItems(c echo.Context) error
|
||||
```
|
||||
|
||||
**Implementation:**
|
||||
Copy exact method bodies, change receiver from `h *Handler` to `mh *MediaHandler`
|
||||
|
||||
#### Step 2.3: Move metadata methods (From ebook.go lines 630-839)
|
||||
|
||||
**Methods to move:**
|
||||
- `CreateMediaRating()` → `media.go:CreateMediaRating()`
|
||||
- `GetMediaRating()` → `media.go:GetMediaRating()`
|
||||
- `UpdateMediaRating()` → `media.go:UpdateMediaRating()`
|
||||
- `DeleteMediaRating()` → `media.go:DeleteMediaRating()`
|
||||
|
||||
- `GetMediaReadingProgress()` → `media.go:GetMediaReadingProgress()`
|
||||
- `UpdateMediaReadingProgress()` → `media.go:UpdateMediaReadingProgress()`
|
||||
- `DeleteMediaReadingProgress()` → `media.go:DeleteMediaReadingProgress()`
|
||||
|
||||
- `GetMediaNotes()` → `media.go:GetMediaNotes()`
|
||||
- `CreateMediaNote()` → `media.go:CreateMediaNote()`
|
||||
- `GetMediaNote()` → `media.go:GetMediaNote()`
|
||||
- `UpdateMediaNote()` → `media.go:UpdateMediaNote()`
|
||||
- `DeleteMediaNote()` → `media.go:DeleteMediaNote()`
|
||||
|
||||
- `GetMediaHighlights()` → `media.go:GetMediaHighlights()`
|
||||
- `CreateMediaHighlight()` → `media.go:CreateMediaHighlight()`
|
||||
- `GetMediaHighlight()` → `media.go:GetMediaHighlight()`
|
||||
- `UpdateMediaHighlight()` → `media.go:UpdateMediaHighlight()`
|
||||
- `DeleteMediaHighlight()` → `media.go:DeleteMediaHighlight()`
|
||||
|
||||
**Verification after move:**
|
||||
- [ ] All methods compile with new receiver type
|
||||
- [ ] No changes to method bodies
|
||||
- [ ] All database calls work (same `mh.db` reference)
|
||||
- [ ] All imports are correct
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Create Search Handler (QUERY OPERATIONS)
|
||||
|
||||
### File: `internal/handlers/search.go` (NEW)
|
||||
|
||||
#### Step 3.1: Create SearchHandler struct
|
||||
|
||||
```go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bookhoard/internal/database"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// SearchHandler handles search, query, and filtered operations
|
||||
type SearchHandler struct {
|
||||
db *database.Queries
|
||||
}
|
||||
|
||||
// NewSearchHandler creates a new search handler
|
||||
func NewSearchHandler(db *database.Queries) *SearchHandler {
|
||||
return &SearchHandler{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 3.2: Move search methods (From ebook.go lines 1297-end)
|
||||
|
||||
**Methods to move:**
|
||||
- `SearchMediaItems()` → `search.go:SearchMediaItems()`
|
||||
- `QueryBooks()` → `search.go:QueryBooks()`
|
||||
|
||||
**Note:** These methods are small and focused, perfect for separate handler
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Create Matching Handler (SYNC OPERATIONS)
|
||||
|
||||
### File: `internal/handlers/matching.go` (NEW)
|
||||
|
||||
#### Step 4.1: Create MatchingHandler struct
|
||||
|
||||
```go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bookhoard/internal/database"
|
||||
"bookhoard/internal/sync"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// MatchingHandler handles book matching, linking, and file alias operations
|
||||
type MatchingHandler struct {
|
||||
db *database.Queries
|
||||
connManager *wsync.ConnectionManager
|
||||
}
|
||||
|
||||
// NewMatchingHandler creates a new matching handler
|
||||
func NewMatchingHandler(db *database.Queries, connManager *wsync.ConnectionManager) *MatchingHandler {
|
||||
return &MatchingHandler{
|
||||
db: db,
|
||||
connManager: connManager,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 4.2: Move matching methods (From ebook.go lines 96-104)
|
||||
|
||||
**Methods to move:**
|
||||
- `QueryBooks()` → `matching.go:QueryBooks()`
|
||||
- `LinkBook()` → `matching.go:LinkBook()`
|
||||
- `GetUnlinkedBooks()` → `matching.go:GetUnlinkedBooks()`
|
||||
- `GetDeviceFileAliases()` → `matching.go:GetDeviceFileAliases()`
|
||||
- `CreateDeviceFileAlias()` → `matching.go:CreateDeviceFileAlias()`
|
||||
- `UpdateDeviceFileAlias()` → `matching.go:UpdateDeviceFileAlias()`
|
||||
- `DeleteDeviceFileAlias()` → `matching.go:DeleteDeviceFileAlias()`
|
||||
- `GetBookMatches()` → `matching.go:GetBookMatches()`
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Update Router Package (ROUTE REGISTRATION)
|
||||
|
||||
### Purpose
|
||||
Register all routes with new handler types, maintain exact same endpoints
|
||||
|
||||
#### Step 5.1: Update router.go Config struct
|
||||
|
||||
**File:** `internal/router/router.go`
|
||||
|
||||
**Current Config (lines 33-52):**
|
||||
```go
|
||||
type Config struct {
|
||||
Echo *echo.Echo
|
||||
Queries *database.Queries
|
||||
Cfg *config.Config
|
||||
DBPool interface{}
|
||||
AuthHandler *handlers.AuthHandler
|
||||
LibraryHandler *handlers.LibraryHandler
|
||||
DeviceHandler *handlers.DeviceHandler
|
||||
EbookHandler *handlers.Handler // ← WILL BE REMOVED
|
||||
KOReaderHandler *handlers.KOReaderHandler
|
||||
WSHandler *handlers.WSHandler
|
||||
ConflictHandler *handlers.ConflictHandler
|
||||
AnalyticsHandler *handlers.AnalyticsHandler
|
||||
QueueHandler *handlers.QueueHandler
|
||||
CollectionHandler *handlers.CollectionHandler
|
||||
OPDSHandler *handlers.OPDSHandler
|
||||
ConnManager *sync.ConnectionManager
|
||||
QueueProcessor *sync.SyncQueueProcessor
|
||||
DeviceAuthMiddleware *middleware.DeviceAuthMiddleware
|
||||
LoginTracker *ratelimit.LoginAttemptTracker
|
||||
}
|
||||
```
|
||||
|
||||
**New Config (replace EbookHandler with 3 new handlers):**
|
||||
```go
|
||||
type Config struct {
|
||||
Echo *echo.Echo
|
||||
Queries *database.Queries
|
||||
Cfg *config.Config
|
||||
DBPool interface{}
|
||||
AuthHandler *handlers.AuthHandler
|
||||
LibraryHandler *handlers.LibraryHandler
|
||||
DeviceHandler *handlers.DeviceHandler
|
||||
// EbookHandler REMOVED - replaced below:
|
||||
MediaHandler *handlers.MediaHandler // ← NEW
|
||||
SearchHandler *handlers.SearchHandler // ← NEW
|
||||
MatchingHandler *handlers.MatchingHandler // ← NEW
|
||||
KOReaderHandler *handlers.KOReaderHandler
|
||||
WSHandler *handlers.WSHandler
|
||||
ConflictHandler *handlers.ConflictHandler
|
||||
AnalyticsHandler *handlers.AnalyticsHandler
|
||||
QueueHandler *handlers.QueueHandler
|
||||
CollectionHandler *handlers.CollectionHandler
|
||||
OPDSHandler *handlers.OPDSHandler
|
||||
ConnManager *sync.ConnectionManager
|
||||
QueueProcessor *sync.SyncQueueProcessor
|
||||
DeviceAuthMiddleware *middleware.DeviceAuthMiddleware
|
||||
LoginTracker *ratelimit.LoginAttemptTracker
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 5.2: Update RegisterRoutes function
|
||||
|
||||
**File:** `internal/router/router.go`
|
||||
|
||||
**Current route registration (lines 114-129):**
|
||||
```go
|
||||
jwtMiddleware := createJWTMiddleware(cfg)
|
||||
protected := e.Group("/api", jwtMiddleware)
|
||||
handlers.SetupRoutes(protected, cfg.Queries, cfg.ConnManager) // OLD METHOD
|
||||
|
||||
// Register route groups
|
||||
registerAuthRoutes(cfg, rateLimitMiddleware)
|
||||
registerLibraryRoutes(cfg)
|
||||
registerDeviceRoutes(cfg)
|
||||
registerSyncRoutes(cfg)
|
||||
registerMediaRoutes(cfg)
|
||||
registerConflictRoutes(cfg)
|
||||
registerAnalyticsRoutes(cfg)
|
||||
registerQueueRoutes(cfg)
|
||||
registerOPDSRoutes(cfg)
|
||||
registerWebSocketRoutes(cfg)
|
||||
registerFrontendRoutes(cfg)
|
||||
registerDocumentationRoutes(cfg)
|
||||
```
|
||||
|
||||
**New route registration:**
|
||||
```go
|
||||
jwtMiddleware := createJWTMiddleware(cfg)
|
||||
protected := e.Group("/api", jwtMiddleware)
|
||||
|
||||
// Create handler instances using factory functions
|
||||
mediaHandler := handlers.NewMediaHandler(cfg.Queries, cfg.Worker) // Worker needed
|
||||
searchHandler := handlers.NewSearchHandler(cfg.Queries)
|
||||
matchingHandler := handlers.NewMatchingHandler(cfg.Queries, cfg.ConnManager)
|
||||
|
||||
// Register route groups
|
||||
registerAuthRoutes(cfg, rateLimitMiddleware)
|
||||
registerLibraryRoutes(cfg)
|
||||
registerDeviceRoutes(cfg)
|
||||
registerSyncRoutes(cfg)
|
||||
registerMediaRoutes(cfg) // ← WILL USE NEW MediaHandler
|
||||
registerSearchRoutes(cfg) // ← NEW FILE
|
||||
registerMatchingRoutes(cfg) // ← NEW FILE
|
||||
registerConflictRoutes(cfg)
|
||||
registerAnalyticsRoutes(cfg)
|
||||
registerQueueRoutes(cfg)
|
||||
registerOPDSRoutes(cfg)
|
||||
registerWebSocketRoutes(cfg)
|
||||
registerFrontendRoutes(cfg)
|
||||
registerDocumentationRoutes(cfg)
|
||||
```
|
||||
|
||||
#### Step 5.3: Create registerSearchRoutes function
|
||||
|
||||
**File:** `internal/router/search.go` (NEW)
|
||||
|
||||
```go
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func registerSearchRoutes(cfg *Config) {
|
||||
e := cfg.Echo
|
||||
|
||||
// JWT middleware for protected routes
|
||||
jwtMiddleware := createJWTMiddleware(cfg)
|
||||
protected := e.Group("/api", jwtMiddleware)
|
||||
|
||||
// Search and query endpoints (all authenticated users)
|
||||
protected.GET("/media-items/search", cfg.SearchHandler.SearchMediaItems)
|
||||
protected.POST("/sync/books/query", cfg.MatchingHandler.QueryBooks)
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 5.4: Create registerMatchingRoutes function
|
||||
|
||||
**File:** `internal/router/matching.go` (NEW)
|
||||
|
||||
```go
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func registerMatchingRoutes(cfg *Config) {
|
||||
e := cfg.Echo
|
||||
|
||||
// JWT middleware for protected routes
|
||||
jwtMiddleware := createJWTMiddleware(cfg)
|
||||
protected := e.Group("/api", jwtMiddleware)
|
||||
|
||||
// Book matching and linking routes (all authenticated users)
|
||||
protected.POST("/devices/:deviceId/sync/link-book", cfg.MatchingHandler.LinkBook)
|
||||
protected.GET("/devices/:deviceId/sync/unlinked-books", cfg.MatchingHandler.GetUnlinkedBooks)
|
||||
|
||||
// File alias routes (all authenticated users)
|
||||
protected.GET("/devices/:id/file-aliases", cfg.MatchingHandler.GetDeviceFileAliases)
|
||||
protected.POST("/devices/:id/file-aliases", cfg.MatchingHandler.CreateDeviceFileAlias)
|
||||
protected.PUT("/devices/:id/file-aliases/:aliasId", cfg.MatchingHandler.UpdateDeviceFileAlias)
|
||||
protected.DELETE("/devices/:id/file-aliases/:aliasId", cfg.MatchingHandler.DeleteDeviceFileAlias)
|
||||
protected.GET("/books/match", cfg.MatchingHandler.GetBookMatches)
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 5.5: Update existing registerMediaRoutes function
|
||||
|
||||
**File:** `internal/router/media.go` (MODIFY)
|
||||
|
||||
**Current:** Empty or minimal
|
||||
**New:** Register all media CRUD endpoints
|
||||
|
||||
```go
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"bookhoard/internal/handlers"
|
||||
)
|
||||
|
||||
func registerMediaRoutes(cfg *Config) {
|
||||
e := cfg.Echo
|
||||
|
||||
// JWT middleware for protected routes
|
||||
jwtMiddleware := createJWTMiddleware(cfg)
|
||||
protected := e.Group("/api", jwtMiddleware)
|
||||
admin := protected.Group("", handlers.AdminMiddleware)
|
||||
|
||||
// Media item routes (all authenticated users)
|
||||
protected.GET("/media-items", cfg.MediaHandler.ListMediaItems)
|
||||
protected.GET("/media-items/filtered", cfg.MediaHandler.ListMediaItemsFiltered)
|
||||
protected.GET("/media-items/search", cfg.MediaHandler.SearchMediaItems)
|
||||
protected.GET("/media-items/:id", cfg.MediaHandler.GetMediaItem)
|
||||
|
||||
// Media rating routes (all authenticated users)
|
||||
protected.POST("/media-items/:id/rating", cfg.MediaHandler.CreateMediaRating)
|
||||
protected.GET("/media-items/:id/rating", cfg.MediaHandler.GetMediaRating)
|
||||
protected.PUT("/media-items/:id/rating", cfg.MediaHandler.UpdateMediaRating)
|
||||
protected.DELETE("/media-items/:id/rating", cfg.MediaHandler.DeleteMediaRating)
|
||||
|
||||
// Progress routes (all authenticated users)
|
||||
protected.GET("/progress/:id", cfg.MediaHandler.GetUniversalProgress)
|
||||
protected.POST("/progress/:id", cfg.MediaHandler.UpdateUniversalProgress)
|
||||
protected.GET("/progress/:id/history", cfg.MediaHandler.GetProgressHistory)
|
||||
|
||||
// Notes routes (all authenticated users)
|
||||
protected.GET("/media-items/:id/notes", cfg.MediaHandler.GetMediaNotes)
|
||||
protected.POST("/media-items/:id/notes", cfg.MediaHandler.CreateMediaNote)
|
||||
protected.GET("/media-items/:id/notes/:noteId", cfg.MediaHandler.GetMediaNote)
|
||||
protected.PUT("/media-items/:id/notes/:noteId", cfg.MediaHandler.UpdateMediaNote)
|
||||
protected.DELETE("/media-items/:id/notes/:noteId", cfg.MediaHandler.DeleteMediaNote)
|
||||
|
||||
// Highlights routes (all authenticated users)
|
||||
protected.GET("/media-items/:id/highlights", cfg.MediaHandler.GetMediaHighlights)
|
||||
protected.POST("/media-items/:id/highlights", cfg.MediaHandler.CreateMediaHighlight)
|
||||
protected.GET("/media-items/:id/highlights/:highlightId", cfg.MediaHandler.GetMediaHighlight)
|
||||
protected.PUT("/media-items/:id/highlights/:highlightId", cfg.MediaHandler.UpdateMediaHighlight)
|
||||
protected.DELETE("/media-items/:id/highlights/:highlightId", cfg.MediaHandler.DeleteMediaHighlight)
|
||||
|
||||
// Admin-only media routes
|
||||
admin.POST("/media-items", cfg.MediaHandler.CreateMediaItem)
|
||||
admin.PUT("/media-items/:id", cfg.MediaHandler.UpdateMediaItem)
|
||||
admin.DELETE("/media-items/:id", cfg.MediaHandler.DeleteMediaItem)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Update main.go (INITIALIZATION)
|
||||
|
||||
### File: `cmd/server/main.go`
|
||||
|
||||
#### Step 6.1: Create new handler instances
|
||||
|
||||
**Current handler creation (lines 78-99):**
|
||||
```go
|
||||
authHandler := handlers.NewAuthHandler(queries, cfg.JWTSecret, loginAttemptTracker)
|
||||
libraryHandler := handlers.NewLibraryHandler(queries)
|
||||
deviceHandler := handlers.NewDeviceHandler(queries, cfg.JWTSecret, cfg)
|
||||
// ... other handlers
|
||||
```
|
||||
|
||||
**Add new handler instances:**
|
||||
```go
|
||||
authHandler := handlers.NewAuthHandler(queries, cfg.JWTSecret, loginAttemptTracker)
|
||||
libraryHandler := handlers.NewLibraryHandler(queries)
|
||||
deviceHandler := handlers.NewDeviceHandler(queries, cfg.JWTSecret, cfg)
|
||||
// ... existing handlers ...
|
||||
|
||||
// NEW: Create refactored handlers
|
||||
mediaHandler := handlers.NewMediaHandler(queries, queueProcessor) // Worker needed
|
||||
searchHandler := handlers.NewSearchHandler(queries)
|
||||
matchingHandler := handlers.NewMatchingHandler(queries, connManager)
|
||||
```
|
||||
|
||||
#### Step 6.2: Update routerConfig
|
||||
|
||||
**Current routerConfig (lines 131-150):**
|
||||
```go
|
||||
routerConfig := &router.Config{
|
||||
Echo: e,
|
||||
Queries: queries,
|
||||
Cfg: cfg,
|
||||
DBPool: dbPool,
|
||||
AuthHandler: authHandler,
|
||||
LibraryHandler: libraryHandler,
|
||||
DeviceHandler: deviceHandler,
|
||||
EbookHandler: ebookHandler, // ← REMOVE
|
||||
KOReaderHandler: koreaderHandler,
|
||||
WSHandler: wsHandler,
|
||||
// ... other fields
|
||||
}
|
||||
```
|
||||
|
||||
**New routerConfig:**
|
||||
```go
|
||||
routerConfig := &router.Config{
|
||||
Echo: e,
|
||||
Queries: queries,
|
||||
Cfg: cfg,
|
||||
DBPool: dbPool,
|
||||
AuthHandler: authHandler,
|
||||
LibraryHandler: libraryHandler,
|
||||
DeviceHandler: deviceHandler,
|
||||
// EbookHandler REMOVED
|
||||
MediaHandler: mediaHandler, // ← NEW
|
||||
SearchHandler: searchHandler, // ← NEW
|
||||
MatchingHandler: matchingHandler, // ← NEW
|
||||
KOReaderHandler: koreaderHandler,
|
||||
WSHandler: wsHandler,
|
||||
ConflictHandler: conflictHandler,
|
||||
AnalyticsHandler: analyticsHandler,
|
||||
QueueHandler: queueHandler,
|
||||
CollectionHandler: collectionHandler,
|
||||
OPDSHandler: opdsHandler,
|
||||
ConnManager: connManager,
|
||||
QueueProcessor: queueProcessor,
|
||||
DeviceAuthMiddleware: deviceAuthMiddleware,
|
||||
LoginTracker: loginAttemptTracker,
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Need to determine where `worker` comes from for MediaHandler. Check existing code.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Clean Up ebook.go (FINALIZE)
|
||||
|
||||
### File: `internal/handlers/ebook.go`
|
||||
|
||||
#### Step 7.1: Remove moved methods
|
||||
|
||||
**Remove these method sections:**
|
||||
- Lines 474-959: All media CRUD and metadata methods
|
||||
- Lines 1297-end: All search and matching methods
|
||||
- Lines 73-104: Collection and device mapping routes
|
||||
- Lines 105-143: All route registrations
|
||||
|
||||
**Keep these sections:**
|
||||
- Lines 38-69: Handler struct and NewHandler function
|
||||
- Lines 70-72: Simplified SetupRoutes function
|
||||
- Lines 159-end: Request/response structs
|
||||
|
||||
#### Step 7.2: Update Handler struct dependencies
|
||||
|
||||
**Current Handler struct (lines 19-30):**
|
||||
```go
|
||||
type Handler struct {
|
||||
db *database.Queries
|
||||
worker *worker.Worker // ← MOVE TO MediaHandler
|
||||
scanner *services.EbookScanner // ← MAY MOVE TO ScannerHandler
|
||||
scheduler *cron.Cron // ← MAY MOVE TO ScannerHandler
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
mu sync.RWMutex
|
||||
watchingLibraries map[string]bool
|
||||
}
|
||||
```
|
||||
|
||||
**Potential final Handler struct:**
|
||||
```go
|
||||
// If keeping only SetupRoutes function:
|
||||
type Handler struct {
|
||||
// This might be entirely removed if SetupRoutes is only factory
|
||||
}
|
||||
|
||||
func SetupRoutes(g *echo.Group, db *database.Queries, connManager *wsync.ConnectionManager) *Handler {
|
||||
return NewHandler(db, connManager)
|
||||
}
|
||||
```
|
||||
|
||||
OR
|
||||
|
||||
**If Handler is no longer needed:**
|
||||
- Remove entire file
|
||||
- Move remaining structs to appropriate files
|
||||
- Update all imports
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order (SAFEST FIRST)
|
||||
|
||||
### Phase 1: Create new handler files (ZERO RISK)
|
||||
1. Create `internal/handlers/media.go` with struct and empty methods
|
||||
2. Create `internal/handlers/search.go` with struct and empty methods
|
||||
3. Create `internal/handlers/matching.go` with struct and empty methods
|
||||
4. Verify compilation
|
||||
|
||||
### Phase 2: Move method bodies (LOW RISK)
|
||||
1. Copy media methods from ebook.go to media.go
|
||||
2. Copy search methods from ebook.go to search.go
|
||||
3. Copy matching methods from ebook.go to matching.go
|
||||
4. Test compilation after each file
|
||||
|
||||
### Phase 3: Update router registration (MEDIUM RISK)
|
||||
1. Update router.go Config struct
|
||||
2. Create registerSearchRoutes and registerMatchingRoutes
|
||||
3. Update registerMediaRoutes to use MediaHandler
|
||||
4. Test compilation
|
||||
|
||||
### Phase 4: Update main.go initialization (LOW RISK)
|
||||
1. Add new handler instance creation
|
||||
2. Update routerConfig
|
||||
3. Test compilation and runtime
|
||||
|
||||
### Phase 5: Clean up ebook.go (LOW RISK)
|
||||
1. Remove moved methods
|
||||
2. Simplify/Remove Handler struct if no longer needed
|
||||
3. Verify final state
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
### After Each Phase:
|
||||
|
||||
**Phase 1 (new files created):**
|
||||
- [ ] All 3 new files created
|
||||
- [ ] Code compiles without errors
|
||||
- [ ] No existing functionality broken
|
||||
|
||||
**Phase 2 (methods moved):**
|
||||
- [ ] All media methods in media.go compile
|
||||
- [ ] All search methods in search.go compile
|
||||
- [ ] All matching methods in matching.go compile
|
||||
- [ ] Original ebook.go still compiles
|
||||
|
||||
**Phase 3 (router updated):**
|
||||
- [ ] router.go compiles with new Config struct
|
||||
- [ ] registerSearchRoutes compiles
|
||||
- [ ] registerMatchingRoutes compiles
|
||||
- [ ] registerMediaRoutes updated correctly
|
||||
|
||||
**Phase 4 (main.go updated):**
|
||||
- [ ] Application compiles
|
||||
- [ ] All handler instances created
|
||||
- [ ] routerConfig populated correctly
|
||||
|
||||
**Phase 5 (cleanup):**
|
||||
- [ ] ebook.go reduced from 1,350 to <200 lines
|
||||
- [ ] All route endpoints still exist
|
||||
- [ ] Bruno tests still pass
|
||||
- [ ] Verification script passes (26/26)
|
||||
|
||||
### Final Verification Commands:
|
||||
|
||||
```bash
|
||||
# Compilation test
|
||||
go build ./cmd/server
|
||||
|
||||
# Test all critical endpoints still work
|
||||
curl http://localhost:8765/api/media-items | jq .
|
||||
curl http://localhost:8765/api/media-items/search?q=test | jq .
|
||||
curl http://localhost:8765/api/books/match | jq .
|
||||
|
||||
# Run verification script
|
||||
bash scripts/verify-guidelines.sh
|
||||
|
||||
# Check file sizes
|
||||
wc -l internal/handlers/*.go | sort -n
|
||||
# Expected: media.go ~600, search.go ~50, matching.go ~150, ebook.go <200
|
||||
|
||||
# Test Bruno tests still pass
|
||||
cd bruno && npx bruno test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rollback Strategy
|
||||
|
||||
### If any phase breaks:
|
||||
|
||||
**Immediate rollback:**
|
||||
```bash
|
||||
git checkout -- internal/handlers/media.go
|
||||
git checkout -- internal/handlers/search.go
|
||||
git checkout -- internal/handlers/matching.go
|
||||
git checkout -- internal/router/router.go
|
||||
git checkout -- internal/router/media.go
|
||||
git checkout -- cmd/server/main.go
|
||||
```
|
||||
|
||||
**Phase-by-phase rollback:**
|
||||
```bash
|
||||
# After Phase 2: If methods don't compile
|
||||
git checkout -- internal/handlers/media.go
|
||||
git checkout -- internal/handlers/search.go
|
||||
git checkout -- internal/handlers/matching.go
|
||||
|
||||
# After Phase 3: If router breaks
|
||||
git checkout -- internal/router/router.go
|
||||
git checkout -- internal/router/media.go
|
||||
|
||||
# After Phase 4: If initialization breaks
|
||||
git checkout -- cmd/server/main.go
|
||||
```
|
||||
|
||||
**Full project reset:**
|
||||
```bash
|
||||
git checkout -- internal/handlers/ebook.go
|
||||
git clean -fd internal/handlers/media.go internal/handlers/search.go internal/handlers/matching.go
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
**Before refactor:**
|
||||
- ebook.go: 1,350 lines
|
||||
- 1 monolithic handler
|
||||
- Mixed concerns in single file
|
||||
|
||||
**After refactor:**
|
||||
- ebook.go: ~150 lines (SetupRoutes only)
|
||||
- media.go: ~600 lines (CRUD + metadata)
|
||||
- search.go: ~80 lines (search operations)
|
||||
- matching.go: ~200 lines (sync operations)
|
||||
- Clear single responsibilities per file
|
||||
- All API endpoints identical
|
||||
- All tests continue to work
|
||||
|
||||
**File count increase:** +3 new handler files
|
||||
**Lines of code change:** ~0 (moved, not modified)
|
||||
**API compatibility:** 100% maintained
|
||||
**Risk level:** VERY LOW
|
||||
|
||||
---
|
||||
|
||||
## Final Notes
|
||||
|
||||
### Dependencies to verify:
|
||||
|
||||
1. **Worker access for MediaHandler:**
|
||||
- Check where `worker` is created in current code
|
||||
- Pass to NewMediaHandler properly
|
||||
|
||||
2. **AdminMiddleware import:**
|
||||
- Ensure all router files import handlers package
|
||||
- AdminMiddleware is accessible
|
||||
|
||||
3. **Import statements:**
|
||||
- Verify all new handler files have correct imports
|
||||
- No circular dependencies created
|
||||
|
||||
### Testing strategy:
|
||||
|
||||
1. **Compile after each file creation**
|
||||
2. **Test endpoint accessibility after router changes**
|
||||
3. **Run existing test suite before/after**
|
||||
4. **Verify Bruno tests still pass**
|
||||
5. **Check verification script passes**
|
||||
|
||||
This plan ensures zero API changes while completely reorganizing the codebase into maintainable, single-responsibility files.
|
||||
|
||||
---
|
||||
|
||||
**Ready for safe implementation by any AI following these exact phases.**
|
||||
+479
-896
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user