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