docs: remove temporary planning documents
- Remove EBOOK_REFACTOR_PLAN.md (superseded by comprehensive plan) - Remove TAGS_CONTRIBUTORS_IMPLEMENTATION_PLAN.md (completed/combined into other work)
This commit is contained in:
@@ -1,857 +0,0 @@
|
|||||||
# 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.**
|
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user