chore: remove obsolete refactoring plan documents
- Remove ROUTER_REFACTOR_PLAN.md (superseded by EBOOK_REFACTOR_PLAN.md) - Remove SCANNER_RESTORATION_PLAN.md (no longer needed) EBOOK_REFACTOR_PLAN.md remains as the active refactoring plan.
This commit is contained in:
@@ -1,479 +0,0 @@
|
||||
# Router Refactoring Execution Plan
|
||||
|
||||
## Objective
|
||||
Refactor 858-line `cmd/server/main.go` by migrating route definitions to `internal/router/` package while maintaining 100% API compatibility and passing all verification tests.
|
||||
|
||||
## Current State
|
||||
- ✅ `internal/router/` package created with 7 files
|
||||
- ✅ Route stubs implemented for: auth, library, device, frontend, docs
|
||||
- ❌ Router package NOT integrated (never called from main.go)
|
||||
- ❌ All routes still defined in main.go (duplicates)
|
||||
- ⚠️ main.go: 858 lines (target: ~200 lines)
|
||||
|
||||
## Success Criteria
|
||||
1. All 26 verification checks pass (`scripts/verify-guidelines.sh`)
|
||||
2. All Go tests pass (`go test ./...`)
|
||||
3. All Bruno/curl API tests pass
|
||||
4. No API behavior changes (routes, handlers, responses identical)
|
||||
5. main.go reduced to ~200 lines
|
||||
6. Code compiles without errors
|
||||
7. Application runs successfully (containers start, health check returns 200)
|
||||
|
||||
## Migration Strategy: Incremental with Rollback Safety
|
||||
|
||||
### Phase 1: Create Safety Branch ✅
|
||||
- [x] Create branch `continue-router-refactor`
|
||||
- [x] Router package structure exists
|
||||
|
||||
### Phase 2: Integrate Router Package (DO THIS FIRST)
|
||||
|
||||
#### Step 2.1: Add Router Import and Config
|
||||
**File:** `cmd/server/main.go`
|
||||
|
||||
Add to imports:
|
||||
```go
|
||||
"bookhoard/internal/router"
|
||||
```
|
||||
|
||||
Add after line 130 (after rateLimiter initialization):
|
||||
```go
|
||||
// ========================================================================
|
||||
// ROUTER REGISTRATION - Migrate routes to internal/router/ package
|
||||
// ========================================================================
|
||||
routerConfig := &router.Config{
|
||||
Echo: e,
|
||||
Queries: queries,
|
||||
Cfg: cfg,
|
||||
DBPool: dbPool,
|
||||
AuthHandler: authHandler,
|
||||
LibraryHandler: libraryHandler,
|
||||
DeviceHandler: deviceHandler,
|
||||
KOReaderHandler: koreaderHandler,
|
||||
WSHandler: wsHandler,
|
||||
ConflictHandler: conflictHandler,
|
||||
AnalyticsHandler: analyticsHandler,
|
||||
QueueHandler: queueHandler,
|
||||
CollectionHandler: collectionHandler,
|
||||
OPDSHandler: opdsHandler,
|
||||
ConnManager: connManager,
|
||||
QueueProcessor: queueProcessor,
|
||||
DeviceAuthMiddleware: deviceAuthMiddleware,
|
||||
LoginTracker: loginAttemptTracker,
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 2.2: Call Router.RegisterRoutes()
|
||||
Add immediately after routerConfig:
|
||||
```go
|
||||
router.RegisterRoutes(routerConfig)
|
||||
```
|
||||
|
||||
**IMPORTANT:** Do NOT remove any routes from main.go yet!
|
||||
|
||||
#### Step 2.3: Test Compilation
|
||||
```bash
|
||||
go build ./cmd/server
|
||||
```
|
||||
|
||||
**Expected:** Should compile (routes will be duplicated but that's OK temporarily)
|
||||
|
||||
#### Step 2.4: Test Application
|
||||
```bash
|
||||
# Stop containers if running
|
||||
podman-compose down
|
||||
|
||||
# Rebuild and start
|
||||
podman-compose up -d --build
|
||||
|
||||
# Wait for startup
|
||||
sleep 10
|
||||
|
||||
# Test health endpoint
|
||||
curl -s http://localhost:8765/health | jq .
|
||||
|
||||
# Test frontend
|
||||
curl -s http://localhost:8765/ | grep -o "<title>.*</title>"
|
||||
|
||||
# Run verification
|
||||
bash scripts/verify-guidelines.sh
|
||||
```
|
||||
|
||||
**Expected:** All should pass (duplicate routes don't break Echo)
|
||||
|
||||
**ROLLBACK IF:** Compilation fails or health check returns non-200
|
||||
- `git checkout -- cmd/server/main.go`
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Remove Duplicate Routes from main.go
|
||||
|
||||
⚠️ **CRITICAL:** Remove ONE route group at a time, test after each removal!
|
||||
|
||||
#### Step 3.1: Remove Auth Routes (lines 132-191)
|
||||
**Lines to remove:** From `// Auth routes` to `// JWT middleware for protected routes` (before jwtMiddleware creation)
|
||||
|
||||
**Actually:** Keep jwtMiddleware creation (it's used by other routes)
|
||||
Remove: auth POST endpoints and protected auth routes that are now in router/auth.go
|
||||
|
||||
**Test after removal:**
|
||||
```bash
|
||||
go build ./cmd/server
|
||||
podman-compose up -d --build
|
||||
sleep 10
|
||||
# Test auth endpoints
|
||||
curl -X POST http://localhost:8765/api/auth/register -H "Content-Type: application/json" -d '{"email":"test@test.com","username":"test","password":"Test123!"}'
|
||||
```
|
||||
|
||||
#### Step 3.2: Remove Library Routes (lines 192-235)
|
||||
**Lines to remove:** From `// Library management routes` to visibility routes
|
||||
|
||||
**Test after removal:**
|
||||
```bash
|
||||
go build ./cmd/server
|
||||
podman-compose up -d --build
|
||||
# Test library endpoints
|
||||
curl -s http://localhost:8765/api/libraries/types | jq .
|
||||
```
|
||||
|
||||
#### Step 3.3: Remove Device Registration Routes (lines 236-239)
|
||||
**Lines to remove:** Device register and status endpoints
|
||||
|
||||
**Test after removal:**
|
||||
```bash
|
||||
go build ./cmd/server
|
||||
# Device registration test
|
||||
```
|
||||
|
||||
#### Step 3.4: Remove Frontend Routes (lines 627-823)
|
||||
**Lines to remove:** From `// FRONTEND ROUTES` to before `// HEALTH CHECK`
|
||||
|
||||
**Test after removal:**
|
||||
```bash
|
||||
go build ./cmd/server
|
||||
curl -s http://localhost:8765/ | grep -o "<title>.*</title>"
|
||||
```
|
||||
|
||||
#### Step 3.5: Remove Health Check (lines 824-844)
|
||||
**Lines to remove:** From `// HEALTH CHECK` to before `// DOCUMENTATION ROUTES`
|
||||
|
||||
**Test after removal:**
|
||||
```bash
|
||||
go build ./cmd/server
|
||||
curl -s http://localhost:8765/health | jq .
|
||||
```
|
||||
|
||||
#### Step 3.6: Remove Documentation Routes (lines 845-858)
|
||||
**Lines to remove:** From `// DOCUMENTATION ROUTES` to end
|
||||
|
||||
**Test after removal:**
|
||||
```bash
|
||||
go build ./cmd/server
|
||||
curl -s http://localhost:8765/docs | grep -o "<title>.*</title>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Implement Remaining Router Stubs
|
||||
|
||||
#### Step 4.1: Create `router/sync.go`
|
||||
```bash
|
||||
# Create file with sync routes (KOReader, Kobo, websocket)
|
||||
# Copy sync route definitions from main.go
|
||||
```
|
||||
|
||||
**Routes to migrate:**
|
||||
- KOReader sync routes (device authentication required)
|
||||
- Kobo sync routes (device authentication required)
|
||||
- Book matching routes
|
||||
- WebSocket endpoint
|
||||
|
||||
#### Step 4.2: Create `router/media.go`
|
||||
**Routes to migrate:**
|
||||
- Media item routes (download, shelf management)
|
||||
- Bulk book operations
|
||||
|
||||
#### Step 4.3: Create `router/analytics.go`
|
||||
**Routes to migrate:**
|
||||
- Analytics routes (API + SSR)
|
||||
|
||||
#### Step 4.4: Create `router/queue.go`
|
||||
**Routes to migrate:**
|
||||
- Sync queue management routes (API + SSR)
|
||||
|
||||
#### Step 4.5: Create `router/opds.go`
|
||||
**Routes to migrate:**
|
||||
- OPDS routes (public - device authentication optional)
|
||||
|
||||
#### Step 4.6: Update `router/collections.go`
|
||||
**Routes to migrate:**
|
||||
- Collection routes (API + SSR)
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: Complete Migration
|
||||
|
||||
For each new route file created in Phase 4:
|
||||
1. Add `registerXYZRoutes(cfg *Config)` function
|
||||
2. Call it from `router.RegisterRoutes()` in router.go
|
||||
3. Remove corresponding routes from main.go
|
||||
4. Test with: `go build ./cmd/server`
|
||||
5. Test with: `podman-compose up -d --build`
|
||||
6. Test specific endpoints with curl
|
||||
7. Run: `bash scripts/verify-guidelines.sh`
|
||||
|
||||
---
|
||||
|
||||
### Phase 6: Final Verification
|
||||
|
||||
#### Step 6.1: Full Test Suite
|
||||
```bash
|
||||
# Compilation
|
||||
go build ./cmd/server
|
||||
go test ./...
|
||||
|
||||
# Verification
|
||||
bash scripts/verify-guidelines.sh
|
||||
|
||||
# Container test
|
||||
podman-compose down
|
||||
podman-compose up -d --build
|
||||
sleep 15
|
||||
|
||||
# Critical endpoint tests
|
||||
curl -s http://localhost:8765/health | jq .
|
||||
curl -s http://localhost:8765/ | grep -o "<title>.*</title>"
|
||||
curl -s http://localhost:8765/api/libraries/types | jq .
|
||||
curl -s http://localhost:8765/docs | grep -o "<title>.*</title>"
|
||||
|
||||
# Run Bruno tests (if available)
|
||||
# bruno test ...
|
||||
```
|
||||
|
||||
#### Step 6.2: Verify main.go Size
|
||||
```bash
|
||||
wc -l cmd/server/main.go
|
||||
```
|
||||
**Expected:** ~200 lines (down from 858)
|
||||
|
||||
#### Step 6.3: Code Review Checklist
|
||||
- [ ] No routes duplicated (each route defined once)
|
||||
- [ ] All route groups use JWT middleware correctly
|
||||
- [ ] Admin middleware applied where needed
|
||||
- [ ] Rate limiting applied to auth endpoints
|
||||
- [ ] No compilation errors
|
||||
- [ ] All imports used
|
||||
- [ ] Consistent code style with rest of codebase
|
||||
|
||||
---
|
||||
|
||||
### Phase 7: Commit and Push
|
||||
|
||||
#### Step 7.1: Review Changes
|
||||
```bash
|
||||
git diff cmd/server/main.go | head -100
|
||||
git diff internal/router/
|
||||
```
|
||||
|
||||
#### Step 7.2: Run Verification
|
||||
```bash
|
||||
bash scripts/verify-guidelines.sh
|
||||
```
|
||||
|
||||
#### Step 7.3: Commit Changes
|
||||
```bash
|
||||
git add cmd/server/main.go internal/router/
|
||||
git commit -m "refactor: complete router package migration
|
||||
|
||||
- Migrate all routes from main.go to internal/router/ package
|
||||
- Reduce main.go from 858 lines to ~200 lines
|
||||
- Create separate files for route groups:
|
||||
- auth.go: Authentication routes
|
||||
- library.go: Library management
|
||||
- device.go: Device registration & management
|
||||
- sync.go: KOReader/Kobo/WebSocket sync routes
|
||||
- media.go: Media items and bulk operations
|
||||
- analytics.go: Analytics API + SSR
|
||||
- queue.go: Sync queue management
|
||||
- opds.go: OPDS feeds
|
||||
- collections.go: Collection management
|
||||
- frontend.go: SSR pages and health check
|
||||
- docs.go: Documentation routes
|
||||
|
||||
- All 26 verification checks pass
|
||||
- All API endpoints tested and working
|
||||
- Zero API behavior changes (100% compatible)
|
||||
- Follows Go standard project layout
|
||||
|
||||
Breaking Change: None - API compatibility maintained"
|
||||
```
|
||||
|
||||
#### Step 7.4: Push
|
||||
```bash
|
||||
git push origin continue-router-refactor
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rollback Procedures
|
||||
|
||||
### If compilation fails at any point:
|
||||
```bash
|
||||
git checkout -- cmd/server/main.go
|
||||
# Or
|
||||
git reset --hard HEAD
|
||||
```
|
||||
|
||||
### If tests fail:
|
||||
1. Check which endpoint failed
|
||||
2. Verify route is registered in router package
|
||||
3. Check handler method exists
|
||||
4. Check middleware is applied correctly
|
||||
5. Review error logs: `podman logs bookhoard`
|
||||
|
||||
### If verification fails:
|
||||
1. Check which specific check failed
|
||||
2. Fix the issue
|
||||
3. Re-run verification
|
||||
4. Commit the fix separately
|
||||
|
||||
---
|
||||
|
||||
## Testing Commands (Quick Reference)
|
||||
|
||||
```bash
|
||||
# Compile
|
||||
go build ./cmd/server
|
||||
|
||||
# Verification
|
||||
bash scripts/verify-guidelines.sh
|
||||
|
||||
# Unit tests
|
||||
go test ./...
|
||||
|
||||
# Rebuild containers
|
||||
podman-compose down
|
||||
podman-compose up -d --build
|
||||
|
||||
# Wait for startup
|
||||
sleep 10
|
||||
|
||||
# Health check
|
||||
curl -s http://localhost:8765/health | jq .
|
||||
|
||||
# Frontend
|
||||
curl -s http://localhost:8765/ | grep -o "<title>.*</title>"
|
||||
|
||||
# Auth endpoint test
|
||||
curl -X POST http://localhost:8765/api/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"login":"test","password":"wrong"}'
|
||||
|
||||
# Library types
|
||||
curl -s http://localhost:8765/api/libraries/types | jq .
|
||||
|
||||
# Documentation
|
||||
curl -s http://localhost:8765/docs | grep -o "<title>.*</title>"
|
||||
|
||||
# Check container logs
|
||||
podman logs bookhoard | tail -30
|
||||
|
||||
# Check container status
|
||||
podman ps
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
### Created:
|
||||
- `internal/router/router.go` - Main router configuration
|
||||
- `internal/router/auth.go` - Authentication routes
|
||||
- `internal/router/library.go` - Library management routes
|
||||
- `internal/router/device.go` - Device routes
|
||||
- `internal/router/frontend.go` - Frontend SSR routes
|
||||
- `internal/router/docs.go` - Documentation routes
|
||||
- `internal/router/helpers.go` - Template helpers
|
||||
- `internal/router/sync.go` - Sync routes (Phase 4)
|
||||
- `internal/router/media.go` - Media routes (Phase 4)
|
||||
- `internal/router/analytics.go` - Analytics routes (Phase 4)
|
||||
- `internal/router/queue.go` - Queue routes (Phase 4)
|
||||
- `internal/router/opds.go` - OPDS routes (Phase 4)
|
||||
- `internal/router/collections.go` - Collection routes (Phase 4)
|
||||
|
||||
### Modified:
|
||||
- `cmd/server/main.go` - Reduced from 858 to ~200 lines
|
||||
|
||||
---
|
||||
|
||||
## Estimated Time
|
||||
- Phase 2: 15 minutes (integration and initial testing)
|
||||
- Phase 3: 45 minutes (incremental route removal and testing)
|
||||
- Phase 4: 90 minutes (implement remaining route groups)
|
||||
- Phase 5: 30 minutes (complete migration)
|
||||
- Phase 6: 30 minutes (final verification)
|
||||
- Phase 7: 15 minutes (commit and push)
|
||||
|
||||
**Total: ~4 hours**
|
||||
|
||||
---
|
||||
|
||||
## Notes for AI Assistants
|
||||
|
||||
1. **Always test after each change** - don't batch multiple route removals
|
||||
2. **Keep main.go functional** - it should compile at all times
|
||||
3. **Verify API compatibility** - routes must respond identically
|
||||
4. **Use git commits** - commit after each successful phase to enable rollback
|
||||
5. **Check logs** - if something fails, check `podman logs bookhoard`
|
||||
6. **Verification script is authority** - if it fails, fix before continuing
|
||||
7. **Echo allows duplicate routes** - temporarily OK during migration
|
||||
8. **Middleware order matters** - maintain exact middleware application order
|
||||
9. **Import statements** - remove unused imports after route removal
|
||||
10. **Handler methods** - verify handler methods exist before calling them
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Error: "route already registered"
|
||||
- **Cause:** Route defined multiple times
|
||||
- **Fix:** Remove from main.go, keep in router package only
|
||||
|
||||
### Error: "handler method not found"
|
||||
- **Cause:** Typo in method name or handler not initialized in Config
|
||||
- **Fix:** Check method name in handler file, ensure handler is passed in Config
|
||||
|
||||
### Error: "undefined: jwtMiddleware"
|
||||
- **Cause:** JWT middleware not created in that route file
|
||||
- **Fix:** Add JWT middleware creation at top of register function
|
||||
|
||||
### Error: "404 on previously working endpoint"
|
||||
- **Cause:** Route not registered or middleware blocking access
|
||||
- **Fix:** Check route is registered, check middleware conditions
|
||||
|
||||
### Health check returns 503
|
||||
- **Cause:** Database not connected or dbPool not passed to router
|
||||
- **Fix:** Ensure DBPool is set in routerConfig
|
||||
|
||||
### Verification fails with "Build failed"
|
||||
- **Cause:** Compilation error
|
||||
- **Fix:** Run `go build ./cmd/server` to see specific error
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
Before:
|
||||
- `cmd/server/main.go`: 858 lines
|
||||
- All routes defined inline
|
||||
- Mixed concerns (setup + routes + server start)
|
||||
|
||||
After:
|
||||
- `cmd/server/main.go`: ~200 lines
|
||||
- Routes organized by domain in `internal/router/`
|
||||
- Clear separation: setup → router registration → server start
|
||||
- Follows Go standard project layout
|
||||
- Easy to maintain and extend
|
||||
|
||||
---
|
||||
|
||||
## End of Plan
|
||||
@@ -1,699 +0,0 @@
|
||||
# Scanner System Fix & Enhancement Plan
|
||||
|
||||
**Created:** February 6, 2026
|
||||
**Updated:** February 6, 2026 (clarified scope and priorities)
|
||||
**Status:** Ready to implement
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This plan addresses TWO separate issues with the scanner system:
|
||||
|
||||
### 1. **CRITICAL BUG** (Priority: HIGH)
|
||||
Auto-start functionality was removed during router refactor (commit 6784c25):
|
||||
- Scheduler doesn't start on server boot
|
||||
- Watch mode doesn't auto-start for libraries
|
||||
- No graceful shutdown for scanner services
|
||||
- `handlers.SetupRoutes()` returns Handler but return value isn't captured
|
||||
|
||||
### 2. **ENHANCEMENTS** (Priority: MEDIUM)
|
||||
- Library-type-aware scanning (prevents cross-contamination between ebook/comic/manga libraries)
|
||||
- Comic/manga metadata extraction (ComicInfo.xml parsing)
|
||||
- Better code organization (extract scanner routes to separate file)
|
||||
|
||||
**What WASN'T broken:**
|
||||
- ✅ Scanner routes still work (7 endpoints in `internal/handlers/ebook.go:145-154`)
|
||||
- ✅ Can manually start/stop scanner via API
|
||||
- ✅ Scan job tracking works
|
||||
- ✅ Watch mode works when manually triggered
|
||||
|
||||
---
|
||||
|
||||
## Changes Summary
|
||||
|
||||
**Files to Create:**
|
||||
1. `internal/router/scanner.go` (new - optional, for organization)
|
||||
2. `internal/app/app.go` (new - required, for lifecycle management)
|
||||
|
||||
**Files to Modify:**
|
||||
1. `internal/router/router.go` (capture and return EbookHandler)
|
||||
2. `cmd/server/main.go` (restore auto-start calls, use App pattern)
|
||||
3. `internal/services/ebook_scanner.go` (library-type-aware scanning)
|
||||
4. `internal/handlers/ebook.go` (comic/manga metadata extraction)
|
||||
|
||||
**Risk Assessment:** LOW-MEDIUM
|
||||
- Auto-start restoration: LOW (restores existing code that was removed)
|
||||
- Library-type-awareness: MEDIUM (core scanner logic change)
|
||||
- Comic/manga scanning: MEDIUM (new feature)
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Restore Auto-Start Functionality [CRITICAL BUG FIX]
|
||||
|
||||
### Problem
|
||||
Before router refactor (commit 799b640):
|
||||
```go
|
||||
h := handlers.SetupRoutes(protected, queries)
|
||||
|
||||
// Start scheduler for auto-scanning
|
||||
go h.StartScheduler()
|
||||
defer h.StopScheduler()
|
||||
|
||||
// Start watch mode for all libraries (background)
|
||||
go func() {
|
||||
time.Sleep(2 * time.Second)
|
||||
if err := h.StartWatchModeForAllLibraries(context.Background()); err != nil {
|
||||
log.Printf("Warning: failed to start watch mode for libraries: %v", err)
|
||||
}
|
||||
}()
|
||||
```
|
||||
|
||||
After refactor (current):
|
||||
```go
|
||||
handlers.SetupRoutes(protected, cfg.Queries, cfg.ConnManager) // Return value ignored!
|
||||
// No scheduler start
|
||||
// No watch mode start
|
||||
// No graceful shutdown
|
||||
```
|
||||
|
||||
### Solution
|
||||
|
||||
#### Step 1.1: Update `internal/router/router.go`
|
||||
|
||||
**Location:** Line 113-114
|
||||
|
||||
**Current Code:**
|
||||
```go
|
||||
jwtMiddleware := createJWTMiddleware(cfg)
|
||||
protected := e.Group("/api", jwtMiddleware)
|
||||
handlers.SetupRoutes(protected, cfg.Queries, cfg.ConnManager)
|
||||
```
|
||||
|
||||
**New Code:**
|
||||
```go
|
||||
jwtMiddleware := createJWTMiddleware(cfg)
|
||||
protected := e.Group("/api", jwtMiddleware)
|
||||
ebookHandler := handlers.SetupRoutes(protected, cfg.Queries, cfg.ConnManager)
|
||||
return ebookHandler // Add return statement to RegisterRoutes
|
||||
```
|
||||
|
||||
**Update RegisterRoutes signature:**
|
||||
|
||||
**Current (line 86):**
|
||||
```go
|
||||
func RegisterRoutes(cfg *Config) {
|
||||
```
|
||||
|
||||
**New:**
|
||||
```go
|
||||
func RegisterRoutes(cfg *Config) *handlers.Handler {
|
||||
// ... existing code ...
|
||||
return ebookHandler // Return at end of function
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 1.2: Update `cmd/server/main.go`
|
||||
|
||||
**Location:** After line 151 (after `router.RegisterRoutes(routerConfig)`)
|
||||
|
||||
**Add this code:**
|
||||
```go
|
||||
// Register all routes and get ebook handler
|
||||
ebookHandler := router.RegisterRoutes(routerConfig)
|
||||
|
||||
// ========================================================================
|
||||
// BACKGROUND SERVICES - Restore auto-start functionality
|
||||
// ========================================================================
|
||||
|
||||
// Start scheduler for auto-scanning
|
||||
go ebookHandler.StartScheduler()
|
||||
defer ebookHandler.StopScheduler()
|
||||
|
||||
// Start watch mode for all libraries (background)
|
||||
go func() {
|
||||
time.Sleep(2 * time.Second) // Wait for server to be ready
|
||||
if err := ebookHandler.StartWatchModeForAllLibraries(context.Background()); err != nil {
|
||||
log.Printf("Warning: failed to start watch mode for libraries: %v", err)
|
||||
}
|
||||
}()
|
||||
```
|
||||
|
||||
**Verification:**
|
||||
- [ ] Application compiles
|
||||
- [ ] Server starts without errors
|
||||
- [ ] Check logs for "Starting scheduler" message
|
||||
- [ ] Check logs for watch mode starting after 2 seconds
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Implement Library-Type-Aware Scanning [ENHANCEMENT]
|
||||
|
||||
### Purpose
|
||||
Prevent cross-contamination between library types:
|
||||
- Epub libraries should only scan .epub files
|
||||
- Comic libraries should only scan .cbz/.cbr files
|
||||
- Manga libraries should only scan appropriate formats
|
||||
- Each library type has configurable allowed extensions
|
||||
|
||||
### File: `internal/services/ebook_scanner.go`
|
||||
|
||||
#### Change 2.1: Add libraryTypes cache field
|
||||
|
||||
**Location:** Line 59-65 (EbookScanner struct)
|
||||
|
||||
**Current Code:**
|
||||
```go
|
||||
type EbookScanner struct {
|
||||
db *database.Queries
|
||||
watcher *fsnotify.Watcher
|
||||
folders []string
|
||||
adminID pgtype.UUID
|
||||
defaultLibraryID pgtype.UUID
|
||||
}
|
||||
```
|
||||
|
||||
**New Code:**
|
||||
```go
|
||||
type EbookScanner struct {
|
||||
db *database.Queries
|
||||
watcher *fsnotify.Watcher
|
||||
folders []string
|
||||
adminID pgtype.UUID
|
||||
defaultLibraryID pgtype.UUID
|
||||
libraryTypes map[string][]string // folder -> allowed extensions cache
|
||||
}
|
||||
```
|
||||
|
||||
#### Change 2.2: Initialize libraryTypes in NewEbookScanner
|
||||
|
||||
**Location:** Line 73-74
|
||||
|
||||
**New Code:**
|
||||
```go
|
||||
return &EbookScanner{
|
||||
db: db,
|
||||
watcher: watcher,
|
||||
folders: []string{},
|
||||
adminID: pgtype.UUID{},
|
||||
defaultLibraryID: pgtype.UUID{Valid: false},
|
||||
libraryTypes: make(map[string][]string), // ← ADD THIS
|
||||
}
|
||||
```
|
||||
|
||||
#### Change 2.3: Build library types cache in SetFolders
|
||||
|
||||
**Location:** Line 86-109 (SetFolders function)
|
||||
|
||||
**New Code:**
|
||||
```go
|
||||
func (s *EbookScanner) SetFolders(folders []string) error {
|
||||
s.folders = folders
|
||||
|
||||
// Remove old watch if exists
|
||||
if s.watcher != nil {
|
||||
s.watcher.Close()
|
||||
}
|
||||
|
||||
// Create new watcher
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create watcher: %v", err)
|
||||
}
|
||||
s.watcher = watcher
|
||||
|
||||
// Build cache of allowed extensions per folder
|
||||
s.libraryTypes = make(map[string][]string)
|
||||
ctx := context.Background()
|
||||
|
||||
for _, folder := range folders {
|
||||
// Get library for this folder
|
||||
lib, err := s.db.GetLibraryByFolder(ctx, folder)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: failed to get library for folder %s: %v\n", folder, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Get library type with allowed extensions
|
||||
libType, err := s.db.GetLibraryType(ctx, lib.LibraryTypeID)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: failed to get library type for %s: %v\n", folder, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Cache allowed extensions for this folder
|
||||
s.libraryTypes[folder] = libType.AllowedExtensions
|
||||
fmt.Printf("Scanner: Folder %s (type: %s) allows extensions: %v\n",
|
||||
folder, libType.Name, libType.AllowedExtensions)
|
||||
}
|
||||
|
||||
// Add all folders to watch
|
||||
for _, folder := range folders {
|
||||
if err := s.watcher.Add(folder); err != nil {
|
||||
fmt.Printf("Warning: failed to watch folder %s: %v\n", folder, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
#### Change 2.4: Replace isEbookFile with isScannableFile
|
||||
|
||||
**Location:** Line 168-177 (isEbookFile function)
|
||||
|
||||
**New Code:**
|
||||
```go
|
||||
// isScannableFile checks if a file should be scanned based on library type configuration
|
||||
func (s *EbookScanner) isScannableFile(path string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
|
||||
// Find which folder this file belongs to
|
||||
var folder string
|
||||
for _, f := range s.folders {
|
||||
if strings.HasPrefix(path, f) {
|
||||
folder = f
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If no folder match, don't scan
|
||||
if folder == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Get allowed extensions for this folder's library
|
||||
allowed, ok := s.libraryTypes[folder]
|
||||
if !ok {
|
||||
// No library type info, skip file
|
||||
fmt.Printf("Warning: No library type info for folder %s, skipping %s\n", folder, path)
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if file extension is allowed for this library type
|
||||
for _, allowedExt := range allowed {
|
||||
if ext == strings.ToLower(allowedExt) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
#### Change 2.5: Update ScanFolders to use isScannableFile
|
||||
|
||||
**Location:** Line 146 (in ScanFolders function)
|
||||
|
||||
**Current Code:**
|
||||
```go
|
||||
// Check if it's an ebook file
|
||||
if s.isEbookFile(path) {
|
||||
```
|
||||
|
||||
**New Code:**
|
||||
```go
|
||||
// Check if file should be scanned based on library type
|
||||
if s.isScannableFile(path) {
|
||||
```
|
||||
|
||||
**Verification:**
|
||||
- [ ] Code compiles
|
||||
- [ ] Ebook libraries scan only .epub files (check logs)
|
||||
- [ ] Comic libraries scan only .cbz/.cbr files
|
||||
- [ ] No cross-contamination between library types
|
||||
- [ ] Test: Create ebook library, add .cbz file → should be ignored
|
||||
- [ ] Test: Create comic library, add .epub file → should be ignored
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Comic/Manga Metadata Extraction [NEW FEATURE]
|
||||
|
||||
### Purpose
|
||||
Extract metadata from comic/manga archives (.cbz, .cbr, .cb7):
|
||||
- Parse ComicInfo.xml from archives
|
||||
- Extract cover images
|
||||
- Get series, issue number, publisher, etc.
|
||||
- Support comic library management
|
||||
|
||||
### File: `internal/handlers/ebook.go`
|
||||
|
||||
#### Step 3.1: Add ComicInfo.xml parsing
|
||||
|
||||
**Add to imports:**
|
||||
```go
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/xml"
|
||||
"image"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
```
|
||||
|
||||
#### Step 3.2: Define ComicInfo struct
|
||||
|
||||
**Add after existing structs:**
|
||||
```go
|
||||
// ComicInfo represents metadata from ComicInfo.xml
|
||||
type ComicInfo struct {
|
||||
XMLName xml.Name `xml:"ComicInfo"`
|
||||
Title string `xml:"Title"`
|
||||
Series string `xml:"Series"`
|
||||
Number int `xml:"Number"`
|
||||
Volume int `xml:"Volume"`
|
||||
Publisher string `xml:"Publisher"`
|
||||
Year int `xml:"Year"`
|
||||
Month int `xml:"Month"`
|
||||
Day int `xml:"Day"`
|
||||
Writer string `xml:"Writer"`
|
||||
Penciller string `xml:"Penciller"`
|
||||
Inker string `xml:"Inker"`
|
||||
Colorist string `xml:"Colorist"`
|
||||
Letterer string `xml:"Letterer"`
|
||||
CoverArtist string `xml:"CoverArtist"`
|
||||
Genre string `xml:"Genre"`
|
||||
Tags string `xml:"Tags"`
|
||||
Web string `xml:"Web"`
|
||||
Notes string `xml:"Notes"`
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 3.3: Add extraction function
|
||||
|
||||
**Add new function:**
|
||||
```go
|
||||
// extractComicMetadata extracts metadata from comic archive
|
||||
func extractComicMetadata(filePath string) (*ComicInfo, []byte, error) {
|
||||
// Open archive
|
||||
r, err := zip.OpenReader(filePath)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to open comic archive: %w", err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
// Look for ComicInfo.xml
|
||||
var comicInfo *ComicInfo
|
||||
var coverImage []byte
|
||||
|
||||
for _, f := range r.File {
|
||||
if f.Name == "ComicInfo.xml" {
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to open ComicInfo.xml: %w", err)
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(rc)
|
||||
rc.Close()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read ComicInfo.xml: %w", err)
|
||||
}
|
||||
|
||||
comicInfo = &ComicInfo{}
|
||||
if err := xml.Unmarshal(data, comicInfo); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse ComicInfo.xml: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Look for cover image (usually first image in root)
|
||||
if coverImage == nil && isImageFile(f.Name) {
|
||||
// Usually in root directory, not subdirectories
|
||||
if !strings.Contains(filepath.Dir(f.Name), string(filepath.Separator)) ||
|
||||
filepath.Dir(f.Name) == "." {
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
coverImage, err = io.ReadAll(rc)
|
||||
rc.Close()
|
||||
if err == nil {
|
||||
// Validate it's actually an image
|
||||
_, _, err = image.Decode(bytes.NewReader(coverImage))
|
||||
if err != nil {
|
||||
coverImage = nil // Not a valid image
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if comicInfo == nil {
|
||||
// No ComicInfo.xml, create minimal metadata from filename
|
||||
comicInfo = &ComicInfo{}
|
||||
basename := filepath.Base(filePath)
|
||||
comicInfo.Title = strings.TrimSuffix(basename, filepath.Ext(basename))
|
||||
}
|
||||
|
||||
return comicInfo, coverImage, nil
|
||||
}
|
||||
|
||||
// isImageFile checks if a file is an image based on extension
|
||||
func isImageFile(filename string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
return ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".gif"
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 3.4: Integrate into scanner
|
||||
|
||||
**Update ScanFolders to extract comic metadata for .cbz/.cbr files:**
|
||||
|
||||
**Location:** In the file processing loop (around line 160-180)
|
||||
|
||||
**Add before creating media item:**
|
||||
```go
|
||||
var comicInfo *ComicInfo
|
||||
var coverImage []byte
|
||||
|
||||
// Extract comic metadata if applicable
|
||||
if strings.ToLower(filepath.Ext(path)) == ".cbz" {
|
||||
info, cover, err := extractComicMetadata(path)
|
||||
if err != nil {
|
||||
log.Printf("Warning: failed to extract comic metadata from %s: %v", path, err)
|
||||
} else {
|
||||
comicInfo = info
|
||||
coverImage = cover
|
||||
}
|
||||
}
|
||||
|
||||
// When creating media item, use comic metadata
|
||||
title := comicInfo.Title
|
||||
if title == "" {
|
||||
title = filepath.Base(path)
|
||||
}
|
||||
|
||||
// Use cover image if available
|
||||
if len(coverImage) > 0 {
|
||||
// Use extracted cover
|
||||
// ... existing cover processing code ...
|
||||
}
|
||||
```
|
||||
|
||||
**Verification:**
|
||||
- [ ] Code compiles
|
||||
- [ ] Create .cbz file with ComicInfo.xml
|
||||
- [ ] Scan comic library
|
||||
- [ ] Check metadata was extracted (title, series, issue)
|
||||
- [ ] Check cover image was extracted
|
||||
- [ ] Test .cbz without ComicInfo.xml (should use filename)
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Organize Scanner Routes [OPTIONAL - LOW PRIORITY]
|
||||
|
||||
### Purpose
|
||||
Move scanner routes from `internal/handlers/ebook.go` to `internal/router/scanner.go` for better organization.
|
||||
|
||||
**Note:** This is purely cosmetic. Scanner routes already work fine where they are.
|
||||
|
||||
### File: `internal/router/scanner.go` (NEW)
|
||||
|
||||
```go
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// registerScannerRoutes registers all scanner-related endpoints
|
||||
func registerScannerRoutes(cfg *Config) {
|
||||
// Routes are already registered in handlers.SetupRoutes()
|
||||
// This file is for documentation/organization purposes
|
||||
// Actual routes are in:
|
||||
// - internal/handlers/ebook.go:145-154 (scanner endpoints)
|
||||
// - internal/handlers/auth.go (scan settings endpoints)
|
||||
}
|
||||
```
|
||||
|
||||
**Decision:** SKIP this phase. The current organization works fine.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Application Lifecycle Management [OPTIONAL - DEFER]
|
||||
|
||||
### Purpose
|
||||
Create `internal/app/app.go` for better lifecycle management, graceful shutdown, signal handling.
|
||||
|
||||
**Decision:** DEFER to future implementation. The simple approach in Phase 1 is sufficient for now.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order
|
||||
|
||||
### Priority 1: Critical Bug Fix (Phase 1)
|
||||
- Time: 15 minutes
|
||||
- Risk: LOW
|
||||
- Impact: Restores auto-scan and watch mode
|
||||
|
||||
### Priority 2: Library-Type-Awareness (Phase 2)
|
||||
- Time: 1 hour
|
||||
- Risk: MEDIUM
|
||||
- Impact: Prevents cross-contamination
|
||||
|
||||
### Priority 3: Comic/Manga Scanning (Phase 3)
|
||||
- Time: 2-3 hours
|
||||
- Risk: MEDIUM
|
||||
- Impact: New feature for comic libraries
|
||||
|
||||
### Priority 4: Code Organization (Phase 4)
|
||||
- Time: 30 minutes
|
||||
- Risk: LOW
|
||||
- Impact: Cosmetic (SKIP for now)
|
||||
|
||||
### Priority 5: App Lifecycle (Phase 5)
|
||||
- Time: 2 hours
|
||||
- Risk: MEDIUM
|
||||
- Impact: Better structure (DEFER for now)
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### After Phase 1 (Auto-start restoration):
|
||||
- [ ] Application compiles
|
||||
- [ ] Server starts without errors
|
||||
- [ ] Logs show "Starting scheduler"
|
||||
- [ ] Logs show "Starting watch mode for all libraries" after 2 seconds
|
||||
- [ ] Scheduler triggers auto-scans
|
||||
- [ ] SIGTERM triggers graceful shutdown
|
||||
|
||||
### After Phase 2 (Library-type-awareness):
|
||||
- [ ] Create ebook library, add .epub → scans correctly
|
||||
- [ ] Create ebook library, add .cbz → ignored
|
||||
- [ ] Create comic library, add .cbz → scans correctly
|
||||
- [ ] Create comic library, add .epub → ignored
|
||||
- [ ] Check logs for library type messages
|
||||
|
||||
### After Phase 3 (Comic/manga scanning):
|
||||
- [ ] Create test .cbz with ComicInfo.xml
|
||||
- [ ] Add to comic library
|
||||
- [ ] Scan library
|
||||
- [ ] Verify metadata extracted (title, series, issue)
|
||||
- [ ] Verify cover image extracted
|
||||
- [ ] Test .cbz without ComicInfo.xml (uses filename)
|
||||
|
||||
---
|
||||
|
||||
## Verification Commands
|
||||
|
||||
### Test scanner auto-start:
|
||||
```bash
|
||||
# Start server
|
||||
podman-compose up -d --build
|
||||
|
||||
# Check logs
|
||||
podman logs bookhoard | grep -i "scheduler\|watch mode"
|
||||
|
||||
# Should see:
|
||||
# "Starting scheduler for auto-scanning"
|
||||
# "Starting watch mode for all libraries"
|
||||
```
|
||||
|
||||
### Test library-type-awareness:
|
||||
```bash
|
||||
# Create ebook library
|
||||
curl -X POST http://localhost:8765/api/libraries \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"Ebooks","library_type_id":"<ebook-uuid>","folders":["/path/to/ebooks"]}'
|
||||
|
||||
# Create comic library
|
||||
curl -X POST http://localhost:8765/api/libraries \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"Comics","library_type_id":"<comic-uuid>","folders":["/path/to/comics"]}'
|
||||
|
||||
# Add .epub to comic library → should be ignored
|
||||
# Add .cbz to ebook library → should be ignored
|
||||
# Check scan logs for filtering messages
|
||||
```
|
||||
|
||||
### Test comic metadata extraction:
|
||||
```bash
|
||||
# Create test .cbz with ComicInfo.xml
|
||||
zip test.cbz ComicInfo.xml cover.jpg page1.jpg
|
||||
|
||||
# Add to comic library and scan
|
||||
curl -X POST http://localhost:8765/api/scanner/scan \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"folder_paths":["/path/to/comics"]}'
|
||||
|
||||
# Check media item has correct metadata
|
||||
curl http://localhost:8765/api/media-items?library_id=<comic-lib-id> \
|
||||
-H "Authorization: Bearer $TOKEN" | jq .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
### Rollback Phase 1:
|
||||
```bash
|
||||
git checkout cmd/server/main.go
|
||||
git checkout internal/router/router.go
|
||||
```
|
||||
|
||||
### Rollback Phase 2:
|
||||
```bash
|
||||
git checkout internal/services/ebook_scanner.go
|
||||
```
|
||||
|
||||
### Rollback Phase 3:
|
||||
```bash
|
||||
git checkout internal/handlers/ebook.go
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**Critical Issues Fixed:**
|
||||
- ✅ Auto-start scheduler (restored)
|
||||
- ✅ Auto-start watch mode (restored)
|
||||
- ✅ Graceful shutdown (restored)
|
||||
|
||||
**Enhancements Added:**
|
||||
- ✅ Library-type-aware scanning (prevents cross-contamination)
|
||||
- ✅ Comic/manga metadata extraction (ComicInfo.xml parsing)
|
||||
- ✅ Comic cover image extraction
|
||||
|
||||
**Deferred:**
|
||||
- ⏸️ Code organization (scanner routes file)
|
||||
- ⏸️ App lifecycle management
|
||||
|
||||
**Estimated Time:**
|
||||
- Phase 1 (bug fix): 15 minutes
|
||||
- Phase 2 (enhancement): 1 hour
|
||||
- Phase 3 (new feature): 2-3 hours
|
||||
- **Total: 4-5 hours**
|
||||
|
||||
**Risk Level:**
|
||||
- Phase 1: LOW (restoring removed code)
|
||||
- Phase 2: MEDIUM (core scanner logic)
|
||||
- Phase 3: MEDIUM (new feature, isolated)
|
||||
|
||||
**Ready for implementation in priority order.**
|
||||
Reference in New Issue
Block a user