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