docs: update comprehensive API documentation and project guides

This commit updates all documentation files throughout the project:

- Updated IMPLEMENTATION_PLAN.md with new implementation details
- Updated PROJECT_GUIDELINES.md with coding standards and practices
- Updated README.md with current project information
- Updated SCREENSHOT_AUTOMATION.md with new automation details
- Added TEST_DATA.md with test fixtures data
- Updated cover_image_serving_plan.md with static URL patterns

Documentation API updates:
- Updated API reference documentation for all endpoints including:
  - Authentication (login, logout, register, refresh_token)
  - Book matching (auto_link, bulk_link, link_book, search)
  - Collections (CRUD operations, shelf mappings, auto-assign rules)
  - Conflicts (bulk operations, resolve/dismiss)
  - Devices (registration, approval, shelf management)
  - Highlights (create, update, delete, get)
  - Kobo sync (bookmark, markup, initialization, sync)
  - KOReader sync (library, metadata, bookmarks, progress)
  - Libraries (CRUD, folders, media items, stats)
  - Media items (bulk operations, CRUD)
  - Notes (CRUD operations)
  - OPDS (acquisition, feeds, publication)
  - Progress (reading progress tracking)
  - Queue (device queue management)
  - Ratings (star ratings)
  - Scanner (watch mode, scan operations)
  - Sync protocols (Kobo, KOReader)
  - Users (profile, password, admin operations)
  - WebSocket protocols

- Updated user guides (admin, dashboard, settings, sync)
- Updated device setup guides (Kobo, KOReader)
- Updated developer guides (testing, contributing, operations)
- Updated scripts/README.md
This commit is contained in:
2026-02-27 17:06:22 -05:00
parent 6562b20ee5
commit 4d321528b2
154 changed files with 2817 additions and 2152 deletions
+107 -66
View File
@@ -13,6 +13,7 @@ This document provides precise, line-by-line steps to implement the scanner fixe
**File:** `internal/services/media_scanner.go` **File:** `internal/services/media_scanner.go`
**Current code (around line 348-360):** **Current code (around line 348-360):**
```go ```go
func (s *MediaScanner) processMediaFile(ctx context.Context, path string) (bool, error) { func (s *MediaScanner) processMediaFile(ctx context.Context, path string) (bool, error) {
// Get file info // Get file info
@@ -27,6 +28,7 @@ func (s *MediaScanner) processMediaFile(ctx context.Context, path string) (bool,
``` ```
**Add AFTER line 357 (after getting file info, before existingItem check):** **Add AFTER line 357 (after getting file info, before existingItem check):**
```go ```go
// Get file modification time for created_at // Get file modification time for created_at
fileModTime := info.ModTime() fileModTime := info.ModTime()
@@ -37,6 +39,7 @@ func (s *MediaScanner) processMediaFile(ctx context.Context, path string) (bool,
**File:** `internal/database/queries/queries.sql` **File:** `internal/database/queries/queries.sql`
**Current code (line 131-133):** **Current code (line 131-133):**
```sql ```sql
-- name: CreateMediaItem :one -- name: CreateMediaItem :one
INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publisher, contributors, contributors_search, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id) INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publisher, contributors, contributors_search, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id)
@@ -45,6 +48,7 @@ RETURNING *;
``` ```
**Change TO:** **Change TO:**
```sql ```sql
-- name: CreateMediaItem :one -- name: CreateMediaItem :one
INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publisher, contributors, contributors_search, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at) INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publisher, contributors, contributors_search, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at)
@@ -55,6 +59,7 @@ RETURNING *;
### Step 1.3: Regenerate Go code from SQL OR manually update queries.sql.go ### Step 1.3: Regenerate Go code from SQL OR manually update queries.sql.go
**Option A - Run SQL generation (recommended):** **Option A - Run SQL generation (recommended):**
```bash ```bash
cd internal/database && go generate ./... cd internal/database && go generate ./...
``` ```
@@ -66,6 +71,7 @@ cd internal/database && go generate ./...
**Find `CreateMediaItemParams` struct (around line 557):** **Find `CreateMediaItemParams` struct (around line 557):**
**Add to struct (after AddedByAdminID):** **Add to struct (after AddedByAdminID):**
```go ```go
CreatedAt pgtype.Timestamp `db:"created_at" json:"created_at"` CreatedAt pgtype.Timestamp `db:"created_at" json:"created_at"`
``` ```
@@ -73,6 +79,7 @@ CreatedAt pgtype.Timestamp `db:"created_at" json:"created_at"`
**Find `CreateMediaItem` function (around line 588):** **Find `CreateMediaItem` function (around line 588):**
**Add to the query parameters (after AddedByAdminID in the VALUES):** **Add to the query parameters (after AddedByAdminID in the VALUES):**
```go ```go
arg.CreatedAt, arg.CreatedAt,
``` ```
@@ -84,6 +91,7 @@ arg.CreatedAt,
**Find the CreateMediaItem call** - around line 512. **Find the CreateMediaItem call** - around line 512.
**Current code (line 512-532):** **Current code (line 512-532):**
```go ```go
createdItem, err := s.db.CreateMediaItem(ctx, database.CreateMediaItemParams{ createdItem, err := s.db.CreateMediaItem(ctx, database.CreateMediaItemParams{
LibraryID: libraryID, LibraryID: libraryID,
@@ -93,11 +101,13 @@ createdItem, err := s.db.CreateMediaItem(ctx, database.CreateMediaItemParams{
``` ```
**Add to the params (after AddedByAdminID):** **Add to the params (after AddedByAdminID):**
```go ```go
CreatedAt: pgtype.Timestamp{Time: fileModTime, Valid: true}, CreatedAt: pgtype.Timestamp{Time: fileModTime, Valid: true},
``` ```
**Note:** You'll need to import `"github.com/jackc/pgx/v5/pgtype"` if not already present. **Note:** You'll need to import `"github.com/jackc/pgx/v5/pgtype"` if not already present.
```go ```go
CreatedAt: pgtype.Timestamp{Time: fileModTime, Valid: true}, CreatedAt: pgtype.Timestamp{Time: fileModTime, Valid: true},
``` ```
@@ -115,6 +125,7 @@ createdItem, err := s.db.CreateMediaItem(ctx, database.CreateMediaItemParams{
**File:** `internal/services/media_scanner.go` **File:** `internal/services/media_scanner.go`
**Current code (around lines 365-372):** **Current code (around lines 365-372):**
```go ```go
if s.forceRescan { if s.forceRescan {
fmt.Printf("Force rescan enabled, re-processing existing media item: %s\n", path) fmt.Printf("Force rescan enabled, re-processing existing media item: %s\n", path)
@@ -129,6 +140,7 @@ if s.forceRescan {
### Step 2.2: Replace DELETE+INSERT with UPDATE ### Step 2.2: Replace DELETE+INSERT with UPDATE
**Replace lines 365-372 with:** **Replace lines 365-372 with:**
```go ```go
if s.forceRescan { if s.forceRescan {
fmt.Printf("Force rescan enabled, updating existing media item: %s\n", path) fmt.Printf("Force rescan enabled, updating existing media item: %s\n", path)
@@ -155,6 +167,7 @@ if s.forceRescan {
**Goal:** Prevent cross-library interference - scanning one library shouldn't affect another library's entries. **Goal:** Prevent cross-library interference - scanning one library shouldn't affect another library's entries.
**Important Context:** The `libraryID` is already available in `StartWatchModeForLibrary` at `scanner.go:385`: **Important Context:** The `libraryID` is already available in `StartWatchModeForLibrary` at `scanner.go:385`:
```go ```go
h.StartWatchModeForLibrary(ctx, library.ID, library.CreatedByAdminID) h.StartWatchModeForLibrary(ctx, library.ID, library.CreatedByAdminID)
``` ```
@@ -168,6 +181,7 @@ The function receives `libraryID` but doesn't store it in the scanner. We need t
**Find the SetAdminID function (around line 104):** **Find the SetAdminID function (around line 104):**
**Add AFTER SetAdminID:** **Add AFTER SetAdminID:**
```go ```go
func (s *MediaScanner) SetLibraryID(libraryID pgtype.UUID) { func (s *MediaScanner) SetLibraryID(libraryID pgtype.UUID) {
s.defaultLibraryID = libraryID s.defaultLibraryID = libraryID
@@ -179,12 +193,14 @@ func (s *MediaScanner) SetLibraryID(libraryID pgtype.UUID) {
**File:** `internal/handlers/scanner.go` **File:** `internal/handlers/scanner.go`
**Current code (around line 268):** **Current code (around line 268):**
```go ```go
scanner.SetAdminID(adminID) scanner.SetAdminID(adminID)
scanner.WatchChanges(h.watchModeCtx) scanner.WatchChanges(h.watchModeCtx)
``` ```
**Add AFTER line 268:** **Add AFTER line 268:**
```go ```go
scanner.SetLibraryID(libraryID) scanner.SetLibraryID(libraryID)
``` ```
@@ -194,12 +210,14 @@ scanner.SetLibraryID(libraryID)
**File:** `internal/database/queries/queries.sql` **File:** `internal/database/queries/queries.sql`
**Current code (line 302-303):** **Current code (line 302-303):**
```sql ```sql
-- name: GetMediaItemByFilePath :one -- name: GetMediaItemByFilePath :one
SELECT * FROM media_items WHERE file_path = $1; SELECT * FROM media_items WHERE file_path = $1;
``` ```
**Change TO:** **Change TO:**
```sql ```sql
-- name: GetMediaItemByFilePath :one -- name: GetMediaItemByFilePath :one
SELECT * FROM media_items WHERE file_path = $1 AND library_id = $2; SELECT * FROM media_items WHERE file_path = $1 AND library_id = $2;
@@ -210,10 +228,12 @@ SELECT * FROM media_items WHERE file_path = $1 AND library_id = $2;
**File:** `internal/database/queries.sql.go` **File:** `internal/database/queries.sql.go`
Find `GetMediaItemByFilePath` function and update: Find `GetMediaItemByFilePath` function and update:
1. Add `LibraryID pgtype.UUID` parameter to the function and params struct 1. Add `LibraryID pgtype.UUID` parameter to the function and params struct
2. Add the parameter to the query call 2. Add the parameter to the query call
**OR run SQL generation:** **OR run SQL generation:**
```bash ```bash
cd internal/database && go generate ./... cd internal/database && go generate ./...
``` ```
@@ -223,6 +243,7 @@ cd internal/database && go generate ./...
**File:** `internal/services/media_scanner.go` **File:** `internal/services/media_scanner.go`
**Current code (lines 1395-1397):** **Current code (lines 1395-1397):**
```go ```go
func (s *MediaScanner) getMediaItemByFilePath(ctx context.Context, filePath string) (database.MediaItems, error) { func (s *MediaScanner) getMediaItemByFilePath(ctx context.Context, filePath string) (database.MediaItems, error) {
return s.db.GetMediaItemByFilePath(ctx, filePath) return s.db.GetMediaItemByFilePath(ctx, filePath)
@@ -230,6 +251,7 @@ func (s *MediaScanner) getMediaItemByFilePath(ctx context.Context, filePath stri
``` ```
**Change TO:** **Change TO:**
```go ```go
func (s *MediaScanner) getMediaItemByFilePath(ctx context.Context, filePath string) (database.MediaItems, error) { func (s *MediaScanner) getMediaItemByFilePath(ctx context.Context, filePath string) (database.MediaItems, error) {
return s.db.GetMediaItemByFilePath(ctx, filePath, s.defaultLibraryID) return s.db.GetMediaItemByFilePath(ctx, filePath, s.defaultLibraryID)
@@ -241,6 +263,7 @@ func (s *MediaScanner) getMediaItemByFilePath(ctx context.Context, filePath stri
**File:** `internal/services/media_scanner.go` **File:** `internal/services/media_scanner.go`
Update all places that call `getMediaItemByFilePath` to pass the libraryID: Update all places that call `getMediaItemByFilePath` to pass the libraryID:
- Line 361: In `processMediaFile` - already has access to libraryID via folder lookup - Line 361: In `processMediaFile` - already has access to libraryID via folder lookup
**Note:** The `processMediaFile` function already determines libraryID from the folder path (lines 487-501). Use that libraryID instead of `s.defaultLibraryID` for better accuracy. **Note:** The `processMediaFile` function already determines libraryID from the folder path (lines 487-501). Use that libraryID instead of `s.defaultLibraryID` for better accuracy.
@@ -288,12 +311,12 @@ func NewScannerLogger() *ScannerLogger {
// ensureLogFiles creates/opens log files for today // ensureLogFiles creates/opens log files for today
func (l *ScannerLogger) ensureLogFiles() error { func (l *ScannerLogger) ensureLogFiles() error {
today := time.Now().Format("2006-01-02") today := time.Now().Format("2006-01-02")
// Check if we need to rotate (new day) // Check if we need to rotate (new day)
if l.currentDate == today && l.deletesFile != nil { if l.currentDate == today && l.deletesFile != nil {
return nil // Already have today's files open return nil // Already have today's files open
} }
// Close existing files // Close existing files
if l.deletesFile != nil { if l.deletesFile != nil {
l.deletesFile.Close() l.deletesFile.Close()
@@ -301,41 +324,41 @@ func (l *ScannerLogger) ensureLogFiles() error {
if l.errorsFile != nil { if l.errorsFile != nil {
l.errorsFile.Close() l.errorsFile.Close()
} }
// Create log directory if it doesn't exist // Create log directory if it doesn't exist
if err := os.MkdirAll(logDir, 0755); err != nil { if err := os.MkdirAll(logDir, 0755); err != nil {
return fmt.Errorf("failed to create log directory: %v", err) return fmt.Errorf("failed to create log directory: %v", err)
} }
// Open new files for today // Open new files for today
deletesPath := filepath.Join(logDir, fmt.Sprintf("scanner-deletes-%s.log", today)) deletesPath := filepath.Join(logDir, fmt.Sprintf("scanner-deletes-%s.log", today))
errorsPath := filepath.Join(logDir, fmt.Sprintf("scanner-errors-%s.log", today)) errorsPath := filepath.Join(logDir, fmt.Sprintf("scanner-errors-%s.log", today))
deletesFile, err := os.OpenFile(deletesPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) deletesFile, err := os.OpenFile(deletesPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil { if err != nil {
return fmt.Errorf("failed to open deletes log file: %v", err) return fmt.Errorf("failed to open deletes log file: %v", err)
} }
errorsFile, err := os.OpenFile(errorsPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) errorsFile, err := os.OpenFile(errorsPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil { if err != nil {
deletesFile.Close() deletesFile.Close()
return fmt.Errorf("failed to open errors log file: %v", err) return fmt.Errorf("failed to open errors log file: %v", err)
} }
l.deletesFile = deletesFile l.deletesFile = deletesFile
l.errorsFile = errorsFile l.errorsFile = errorsFile
l.currentDate = today l.currentDate = today
// Clean up old log files // Clean up old log files
l.cleanupOldLogs() l.cleanupOldLogs()
return nil return nil
} }
// cleanupOldLogs removes log files older than maxLogAgeDays // cleanupOldLogs removes log files older than maxLogAgeDays
func (l *ScannerLogger) cleanupOldLogs() { func (l *ScannerLogger) cleanupOldLogs() {
cutoff := time.Now().AddDate(0, 0, -maxLogAgeDays) cutoff := time.Now().AddDate(0, 0, -maxLogAgeDays)
filepath.Walk(logDir, func(path string, info os.FileInfo) error { filepath.Walk(logDir, func(path string, info os.FileInfo) error {
if err != nil { if err != nil {
return err return err
@@ -381,9 +404,11 @@ func (l *ScannerLogger) Close() {
``` ```
**Add to MediaScanner struct:** **Add to MediaScanner struct:**
- Add `logger *ScannerLogger` field to track logger instance - Add `logger *ScannerLogger` field to track logger instance
**Update NewMediaScanner function:** **Update NewMediaScanner function:**
- Initialize logger: `logger: NewScannerLogger()` - Initialize logger: `logger: NewScannerLogger()`
### Step 4.1: Ensure libraryID is available in scanner ### Step 4.1: Ensure libraryID is available in scanner
@@ -397,6 +422,7 @@ func (l *ScannerLogger) Close() {
**Find:** `WatchChanges` function (around line 1427). **Find:** `WatchChanges` function (around line 1427).
**Find the event handling section (around lines 1449-1455):** **Find the event handling section (around lines 1449-1455):**
```go ```go
// Handle file modifications and creations // Handle file modifications and creations
if (event.Has(fsnotify.Create) || event.Has(fsnotify.Write)) && s.isScannableFile(event.Name) { if (event.Has(fsnotify.Create) || event.Has(fsnotify.Write)) && s.isScannableFile(event.Name) {
@@ -408,12 +434,13 @@ if (event.Has(fsnotify.Create) || event.Has(fsnotify.Write)) && s.isScannableFil
``` ```
**Add AFTER that block (before line 1457):** **Add AFTER that block (before line 1457):**
```go ```go
// Handle file deletions // Handle file deletions
if event.Has(fsnotify.Remove) && s.isScannableFile(event.Name) { if event.Has(fsnotify.Remove) && s.isScannableFile(event.Name) {
// Use file logger for persistence // Use file logger for persistence
s.logger.LogDelete(fmt.Sprintf("[WATCH-DELETE] File removed from filesystem: %s", event.Name)) s.logger.LogDelete(fmt.Sprintf("[WATCH-DELETE] File removed from filesystem: %s", event.Name))
// Determine libraryID for this file // Determine libraryID for this file
var libraryID pgtype.UUID var libraryID pgtype.UUID
for _, folder := range s.folders { for _, folder := range s.folders {
@@ -431,20 +458,20 @@ if event.Has(fsnotify.Remove) && s.isScannableFile(event.Name) {
s.logger.LogError(msg) s.logger.LogError(msg)
return return
} }
// Look up media item BEFORE deleting - log for safety // Look up media item BEFORE deleting - log for safety
existingItem, err := s.db.GetMediaItemByFilePath(ctx, event.Name, libraryID) existingItem, err := s.db.GetMediaItemByFilePath(ctx, event.Name, libraryID)
if err == nil { if err == nil {
msg := fmt.Sprintf("[WATCH-DELETE] Found media item to delete: ID=%s, Title=%s, Path=%s", msg := fmt.Sprintf("[WATCH-DELETE] Found media item to delete: ID=%s, Title=%s, Path=%s",
existingItem.ID, existingItem.Title.String, existingItem.FilePath.String) existingItem.ID, existingItem.Title.String, existingItem.FilePath.String)
s.logger.LogDelete(msg) s.logger.LogDelete(msg)
if err := s.db.DeleteMediaItem(ctx, existingItem.ID); err != nil { if err := s.db.DeleteMediaItem(ctx, existingItem.ID); err != nil {
errMsg := fmt.Sprintf("[WATCH-DELETE] ERROR: failed to delete media item %s: %v", existingItem.ID, err) errMsg := fmt.Sprintf("[WATCH-DELETE] ERROR: failed to delete media item %s: %v", existingItem.ID, err)
s.logger.LogDelete(errMsg) s.logger.LogDelete(errMsg)
s.logger.LogError(errMsg) s.logger.LogError(errMsg)
} else { } else {
s.logger.LogDelete(fmt.Sprintf("[WATCH-DELETE] SUCCESS: deleted media item '%s' (was at %s)", s.logger.LogDelete(fmt.Sprintf("[WATCH-DELETE] SUCCESS: deleted media item '%s' (was at %s)",
existingItem.Title.String, existingItem.FilePath.String)) existingItem.Title.String, existingItem.FilePath.String))
} }
} else if err != pgx.ErrNoRows { } else if err != pgx.ErrNoRows {
@@ -468,6 +495,7 @@ if event.Has(fsnotify.Remove) && s.isScannableFile(event.Name) {
**Find:** End of `ScanFolders` function (after line 242). **Find:** End of `ScanFolders` function (after line 242).
**Current code (around line 242-248):** **Current code (around line 242-248):**
```go ```go
fmt.Printf("Scan completed: %d total files scanned, %d media files found, %d new items, %d errors\n", fmt.Printf("Scan completed: %d total files scanned, %d media files found, %d new items, %d errors\n",
processedFiles, mediaFiles, s.newItems, s.errors) processedFiles, mediaFiles, s.newItems, s.errors)
@@ -480,6 +508,7 @@ return nil
``` ```
**Add BEFORE `return nil`:** **Add BEFORE `return nil`:**
```go ```go
// Clean up: Find media items in DB that no longer exist on filesystem // Clean up: Find media items in DB that no longer exist on filesystem
for _, folder := range s.folders { for _, folder := range s.folders {
@@ -512,14 +541,14 @@ for _, folder := range s.folders {
for _, item := range dbItems { for _, item := range dbItems {
filePath := item.FilePath.String filePath := item.FilePath.String
if filePath != "" && !scannedPaths[filePath] { if filePath != "" && !scannedPaths[filePath] {
msg := fmt.Sprintf("[RESCAN-CLEANUP] Orphaned media item found: ID=%s, Title=%s, Path=%s", msg := fmt.Sprintf("[RESCAN-CLEANUP] Orphaned media item found: ID=%s, Title=%s, Path=%s",
item.ID, item.Title.String, filePath) item.ID, item.Title.String, filePath)
s.logger.LogDelete(msg) s.logger.LogDelete(msg)
delMsg := fmt.Sprintf("[RESCAN-CLEANUP] Deleting orphaned item '%s' (file no longer exists at %s)", delMsg := fmt.Sprintf("[RESCAN-CLEANUP] Deleting orphaned item '%s' (file no longer exists at %s)",
item.Title.String, filePath) item.Title.String, filePath)
s.logger.LogDelete(delMsg) s.logger.LogDelete(delMsg)
if err := s.db.DeleteMediaItem(ctx, item.ID); err != nil { if err := s.db.DeleteMediaItem(ctx, item.ID); err != nil {
errMsg := fmt.Sprintf("[RESCAN-CLEANUP] ERROR: failed to delete orphaned item %s: %v", item.Title.String, err) errMsg := fmt.Sprintf("[RESCAN-CLEANUP] ERROR: failed to delete orphaned item %s: %v", item.Title.String, err)
s.logger.LogDelete(errMsg) s.logger.LogDelete(errMsg)
@@ -533,6 +562,7 @@ for _, folder := range s.folders {
``` ```
**Log Files Location:** `/app/logs/` **Log Files Location:** `/app/logs/`
- `scanner-deletes-YYYY-MM-DD.log` - All deletion events (watch mode + rescan) - `scanner-deletes-YYYY-MM-DD.log` - All deletion events (watch mode + rescan)
- `scanner-errors-YYYY-MM-DD.log` - All error events - `scanner-errors-YYYY-MM-DD.log` - All error events
- Rotation: Daily, keeps 7 days of history - Rotation: Daily, keeps 7 days of history
@@ -543,11 +573,13 @@ for _, folder := range s.folders {
## Verification Steps After Implementation ## Verification Steps After Implementation
1. **Compile the code:** 1. **Compile the code:**
```bash ```bash
go build ./... go build ./...
``` ```
2. **Run tests:** 2. **Run tests:**
```bash ```bash
go test ./... -v go test ./... -v
``` ```
@@ -571,14 +603,16 @@ for _, folder := range s.folders {
## Safety Checks (IMPORTANT) ## Safety Checks (IMPORTANT)
### Prevent deleting ALL books: ### Prevent deleting ALL books:
- The cleanup logic MUST check `scannedPaths[filePath]` - this ensures we only delete items whose paths were NOT found during the filesystem scan - The cleanup logic MUST check `scannedPaths[filePath]` - this ensures we only delete items whose paths were NOT found during the filesystem scan
- The key condition is: `if filePath != "" && !scannedPaths[filePath]` - meaning "if this file was NOT found in our scan, delete it" - The key condition is: `if filePath != "" && !scannedPaths[filePath]` - meaning "if this file was NOT found in our scan, delete it"
- This is correct because: - This is correct because:
1. We scan the filesystem → get all current file paths 1. We scan the filesystem → get all current file paths
2. We query DB → get all stored file paths 2. We query DB → get all stored file paths
3. We compare → only delete if DB path is NOT in filesystem paths 3. We compare → only delete if DB path is NOT in filesystem paths
### Before running against production: ### Before running against production:
- Test with a small subset of books first - Test with a small subset of books first
- Verify the delete queries target specific library_id (not all libraries) - Verify the delete queries target specific library_id (not all libraries)
- Check logs show only expected deletions - Check logs show only expected deletions
@@ -587,20 +621,20 @@ for _, folder := range s.folders {
## Files to Modify ## Files to Modify
| Fix | File | Changes | | Fix | File | Changes |
|-----|------|---------| | --- | --------------------------------------- | -------------------------------------------------------- |
| 1a | `internal/database/queries/queries.sql` | Add created_at to INSERT columns | | 1a | `internal/database/queries/queries.sql` | Add created_at to INSERT columns |
| 1b | `internal/database/queries.sql.go` | Add CreatedAt to CreateMediaItemParams struct + query | | 1b | `internal/database/queries.sql.go` | Add CreatedAt to CreateMediaItemParams struct + query |
| 1c | `internal/services/media_scanner.go` | Get file.ModTime() + pass to CreateMediaItem | | 1c | `internal/services/media_scanner.go` | Get file.ModTime() + pass to CreateMediaItem |
| 2 | `internal/services/media_scanner.go` | Change force rescan from DELETE+INSERT to UPDATE | | 2 | `internal/services/media_scanner.go` | Change force rescan from DELETE+INSERT to UPDATE |
| 3a | `internal/services/media_scanner.go` | Add SetLibraryID() method | | 3a | `internal/services/media_scanner.go` | Add SetLibraryID() method |
| 3b | `internal/handlers/scanner.go` | Call scanner.SetLibraryID(libraryID) | | 3b | `internal/handlers/scanner.go` | Call scanner.SetLibraryID(libraryID) |
| 3c | `internal/database/queries/queries.sql` | Add library_id to WHERE clause | | 3c | `internal/database/queries/queries.sql` | Add library_id to WHERE clause |
| 3d | `internal/database/queries.sql.go` | Update GetMediaItemByFilePath params | | 3d | `internal/database/queries.sql.go` | Update GetMediaItemByFilePath params |
| 3e | `internal/services/media_scanner.go` | Pass libraryID to getMediaItemByFilePath | | 3e | `internal/services/media_scanner.go` | Pass libraryID to getMediaItemByFilePath |
| 4.0 | `internal/services/scanner_logger.go` | **NEW FILE** - File logging infrastructure | | 4.0 | `internal/services/scanner_logger.go` | **NEW FILE** - File logging infrastructure |
| 4a | `internal/services/media_scanner.go` | Add fsnotify.Remove handler in WatchChanges + use logger | | 4a | `internal/services/media_scanner.go` | Add fsnotify.Remove handler in WatchChanges + use logger |
| 4b | `internal/services/media_scanner.go` | Add cleanup loop in ScanFolders + use logger | | 4b | `internal/services/media_scanner.go` | Add cleanup loop in ScanFolders + use logger |
### Docker Compose Volume Mount ### Docker Compose Volume Mount
@@ -618,6 +652,7 @@ volumes:
After editing `queries.sql`, you MUST regenerate the Go code: After editing `queries.sql`, you MUST regenerate the Go code:
**Option A - Run SQL code generation (recommended):** **Option A - Run SQL code generation (recommended):**
```bash ```bash
cd internal/database && go generate ./... cd internal/database && go generate ./...
``` ```
@@ -625,10 +660,12 @@ cd internal/database && go generate ./...
**Option B - Manual update (if Option A fails):** **Option B - Manual update (if Option A fails):**
If `go generate` fails or is not available, manually update `queries.sql.go`: If `go generate` fails or is not available, manually update `queries.sql.go`:
1. Add `LibraryID pgtype.UUID` parameter to `GetMediaItemByFilePathParams` struct 1. Add `LibraryID pgtype.UUID` parameter to `GetMediaItemByFilePathParams` struct
2. Add parameter to the query function call 2. Add parameter to the query function call
For Fix 1, manually add `CreatedAt` to: For Fix 1, manually add `CreatedAt` to:
- `CreateMediaItemParams` struct - `CreateMediaItemParams` struct
- The query VALUES - The query VALUES
@@ -648,11 +685,13 @@ For Fix 1, manually add `CreatedAt` to:
### Existing Tests Analysis ### Existing Tests Analysis
**Current scanner integration tests** (`cmd/server/tests/scanner_integration_test.go`): **Current scanner integration tests** (`cmd/server/tests/scanner_integration_test.go`):
- Uses `/app/uploads` as test folder - Uses `/app/uploads` as test folder
- Tests scan, progress tracking, watch mode start/stop - Tests scan, progress tracking, watch mode start/stop
- **After Fix 3:** Tests can safely use `/app/uploads` because GetMediaItemByFilePath now filters by library_id - test library's entries are isolated from user's library - **After Fix 3:** Tests can safely use `/app/uploads` because GetMediaItemByFilePath now filters by library_id - test library's entries are isolated from user's library
**Current unit tests** (`internal/services/*_test.go`): **Current unit tests** (`internal/services/*_test.go`):
- `media_scanner_epub_cover_test.go` - Tests cover extraction - `media_scanner_epub_cover_test.go` - Tests cover extraction
- `media_scanner_hash_test.go` - Tests hash calculation - `media_scanner_hash_test.go` - Tests hash calculation
- `media_scanner_library_type_test.go` - Tests library type detection - `media_scanner_library_type_test.go` - Tests library type detection
@@ -672,20 +711,20 @@ func TestProcessMediaFile_UsesFileMtime(t *testing.T) {
// Create test file with specific modification time // Create test file with specific modification time
testFile := createTestEpub(t, "test-book.epub") testFile := createTestEpub(t, "test-book.epub")
defer os.Remove(testFile) defer os.Remove(testFile)
// Set specific mtime // Set specific mtime
pastTime := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) pastTime := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
os.Chtimes(testFile, pastTime, pastTime) os.Chtimes(testFile, pastTime, pastTime)
// Process file // Process file
scanner := NewMediaScanner(db) scanner := NewMediaScanner(db)
scanner.SetFolders([]string{filepath.Dir(testFile)}) scanner.SetFolders([]string{filepath.Dir(testFile)})
scanner.SetLibraryID(testLibraryID) scanner.SetLibraryID(testLibraryID)
wasNew, err := scanner.ProcessMediaFile(ctx, testFile) wasNew, err := scanner.ProcessMediaFile(ctx, testFile)
require.NoError(t, err) require.NoError(t, err)
require.True(t, wasNew) require.True(t, wasNew)
// Verify created_at matches file mtime, not scan time // Verify created_at matches file mtime, not scan time
item, err := db.GetMediaItemByFilePath(ctx, testFile, testLibraryID) item, err := db.GetMediaItemByFilePath(ctx, testFile, testLibraryID)
require.NoError(t, err) require.NoError(t, err)
@@ -697,24 +736,24 @@ func TestForceRescan_PreservesCreatedAt(t *testing.T) {
// Create and process file // Create and process file
testFile := createTestEpub(t, "test-book.epub") testFile := createTestEpub(t, "test-book.epub")
defer os.Remove(testFile) defer os.Remove(testFile)
scanner := NewMediaScanner(db) scanner := NewMediaScanner(db)
scanner.SetFolders([]string{filepath.Dir(testFile)}) scanner.SetFolders([]string{filepath.Dir(testFile)})
scanner.SetLibraryID(testLibraryID) scanner.SetLibraryID(testLibraryID)
_, _ = scanner.ProcessMediaFile(ctx, testFile) _, _ = scanner.ProcessMediaFile(ctx, testFile)
// Get original created_at // Get original created_at
item, _ := db.GetMediaItemByFilePath(ctx, testFile, testLibraryID) item, _ := db.GetMediaItemByFilePath(ctx, testFile, testLibraryID)
originalCreatedAt := item.CreatedAt.Time originalCreatedAt := item.CreatedAt.Time
// Wait a moment to ensure time difference // Wait a moment to ensure time difference
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
// Force rescan // Force rescan
scanner.SetForce(true) scanner.SetForce(true)
_, _ = scanner.ProcessMediaFile(ctx, testFile) _, _ = scanner.ProcessMediaFile(ctx, testFile)
// Verify created_at is preserved // Verify created_at is preserved
item, _ = db.GetMediaItemByFilePath(ctx, testFile, testLibraryID) item, _ = db.GetMediaItemByFilePath(ctx, testFile, testLibraryID)
assert.Equal(t, originalCreatedAt, item.CreatedAt.Time) assert.Equal(t, originalCreatedAt, item.CreatedAt.Time)
@@ -731,21 +770,21 @@ func TestGetMediaItemByFilePath_FiltersByLibrary(t *testing.T) {
// Same file path in two different libraries // Same file path in two different libraries
testFile := createTestEpub(t, "shared-book.epub") testFile := createTestEpub(t, "shared-book.epub")
defer os.Remove(testFile) defer os.Remove(testFile)
// Add to library A // Add to library A
scannerA := NewMediaScanner(db) scannerA := NewMediaScanner(db)
scannerA.SetLibraryID(libraryAID) scannerA.SetLibraryID(libraryAID)
_, _ = scannerA.ProcessMediaFile(ctx, testFile) _, _ = scannerA.ProcessMediaFile(ctx, testFile)
// Add same path to library B (simulating shared folder scenario) // Add same path to library B (simulating shared folder scenario)
scannerB := NewMediaScanner(db) scannerB := NewMediaScanner(db)
scannerB.SetLibraryID(libraryBID) scannerB.SetLibraryID(libraryBID)
_, _ = scannerB.ProcessMediaFile(ctx, testFile) _, _ = scannerB.ProcessMediaFile(ctx, testFile)
// Verify each library has its own entry // Verify each library has its own entry
itemA, errA := db.GetMediaItemByFilePath(ctx, testFile, libraryAID) itemA, errA := db.GetMediaItemByFilePath(ctx, testFile, libraryAID)
itemB, errB := db.GetMediaItemByFilePath(ctx, testFile, libraryBID) itemB, errB := db.GetMediaItemByFilePath(ctx, testFile, libraryBID)
require.NoError(t, errA) require.NoError(t, errA)
require.NoError(t, errB) require.NoError(t, errB)
assert.Equal(t, libraryAID, itemA.LibraryID) assert.Equal(t, libraryAID, itemA.LibraryID)
@@ -765,30 +804,30 @@ func TestGetMediaItemByFilePath_FiltersByLibrary(t *testing.T) {
func TestScan_DeletesOrphanedBooks(t *testing.T) { func TestScan_DeletesOrphanedBooks(t *testing.T) {
// Create test library pointing to /app/uploads (same as existing tests) // Create test library pointing to /app/uploads (same as existing tests)
testFolder := "/app/uploads" testFolder := "/app/uploads"
// Create test library with test folder // Create test library with test folder
libraryID := createTestLibrary(t, s.setup.Token, "Orphan Test Library", testFolder) libraryID := createTestLibrary(t, s.setup.Token, "Orphan Test Library", testFolder)
// Add a test file // Add a test file
testFile := createTestEpubFile(t, testFolder, "test-orphan-book.epub") testFile := createTestEpubFile(t, testFolder, "test-orphan-book.epub")
defer os.Remove(testFile) // Cleanup after test defer os.Remove(testFile) // Cleanup after test
// Add a test file // Add a test file
testFile := createTestEpubFile(t, testFolder, "test-book.epub") testFile := createTestEpubFile(t, testFolder, "test-book.epub")
// Initial scan // Initial scan
scanLibrary(t, s.setup.Server.URL, libraryID, s.setup.Token) scanLibrary(t, s.setup.Server.URL, libraryID, s.setup.Token)
// Verify book exists // Verify book exists
items, _ := s.setup.DB.ListMediaItemsByLibrary(ctx, libraryID) items, _ := s.setup.DB.ListMediaItemsByLibrary(ctx, libraryID)
require.Len(t, items, 1) require.Len(t, items, 1)
// Delete file from filesystem (simulating user deletion) // Delete file from filesystem (simulating user deletion)
os.Remove(testFile) os.Remove(testFile)
// Rescan // Rescan
scanLibrary(t, s.setup.Server.URL, libraryID, s.setup.Token) scanLibrary(t, s.setup.Server.URL, libraryID, s.setup.Token)
// Verify book was deleted from DB // Verify book was deleted from DB
items, _ = s.setup.DB.ListMediaItemsByLibrary(ctx, libraryID) items, _ = s.setup.DB.ListMediaItemsByLibrary(ctx, libraryID)
assert.Len(t, items, 0, "Orphaned book should be removed from database") assert.Len(t, items, 0, "Orphaned book should be removed from database")
@@ -804,33 +843,33 @@ func TestScan_DeletesOrphanedBooks(t *testing.T) {
func TestWatchMode_DeletesRemovedFile(t *testing.T) { func TestWatchMode_DeletesRemovedFile(t *testing.T) {
// Use /app/uploads - Fix 3 handles isolation // Use /app/uploads - Fix 3 handles isolation
testFolder := "/app/uploads" testFolder := "/app/uploads"
// Create test library // Create test library
libraryID := createTestLibrary(t, s.setup.Token, "Watch Delete Test", testFolder) libraryID := createTestLibrary(t, s.setup.Token, "Watch Delete Test", testFolder)
// Add test file // Add test file
testFile := createTestEpubFile(t, testFolder, "watch-test.epub") testFile := createTestEpubFile(t, testFolder, "watch-test.epub")
// Start watch mode // Start watch mode
startWatchMode(t, s.setup.Server.URL, libraryID, s.setup.Token) startWatchMode(t, s.setup.Server.URL, libraryID, s.setup.Token)
// Wait for initial scan // Wait for initial scan
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
// Verify book exists // Verify book exists
items, _ := s.setup.DB.ListMediaItemsByLibrary(ctx, libraryID) items, _ := s.setup.DB.ListMediaItemsByLibrary(ctx, libraryID)
require.Len(t, items, 1) require.Len(t, items, 1)
// Delete file // Delete file
os.Remove(testFile) os.Remove(testFile)
// Wait for watch mode to detect // Wait for watch mode to detect
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
// Verify book was deleted // Verify book was deleted
items, _ = s.setup.DB.ListMediaItemsByLibrary(ctx, libraryID) items, _ = s.setup.DB.ListMediaItemsByLibrary(ctx, libraryID)
assert.Len(t, items, 0, "Book should be deleted when file removed from filesystem") assert.Len(t, items, 0, "Book should be deleted when file removed from filesystem")
// Stop watch mode // Stop watch mode
stopWatchMode(t, s.setup.Server.URL, libraryID, s.setup.Token) stopWatchMode(t, s.setup.Server.URL, libraryID, s.setup.Token)
} }
@@ -845,17 +884,19 @@ func TestWatchMode_DeletesRemovedFile(t *testing.T) {
### Documentation Updates ### Documentation Updates
If API behavior changes, update: If API behavior changes, update:
- `docs/developer/api/scanner.md` - For any endpoint changes - `docs/developer/api/scanner.md` - For any endpoint changes
- `docs/user/` - If user-facing behavior changes - `docs/user/` - If user-facing behavior changes
### Running Tests ### Running Tests
After implementation, run: After implementation, run:
```bash ```bash
# Unit tests # Unit tests
go test ./internal/services/... -v -run "TestProcessMediaFile|TestGetMediaItemByFilePath|TestForceRescan" go test ./internal/services/... -v -run "TestProcessMediaFile|TestGetMediaItemByFilePath|TestForceRescan"
# Integration tests # Integration tests
go test ./cmd/server/tests/... -v -run "Scanner" go test ./cmd/server/tests/... -v -run "Scanner"
# All tests # All tests
+51 -16
View File
@@ -3,17 +3,19 @@
## 🚨 CRITICAL PROHIBITIONS (Never violate these) ## 🚨 CRITICAL PROHIBITIONS (Never violate these)
### Backend & Database ### Backend & Database
-**NEVER modify backend code when working on frontend-only tasks** -**NEVER modify backend code when working on frontend-only tasks**
-**NEVER modify database schema** unless explicitly instructed for full-stack changes -**NEVER modify database schema** unless explicitly instructed for full-stack changes
-**NEVER use Docker** - use Podman only -**NEVER use Docker** - use Podman only
-**NEVER build server binaries locally** - all builds through Dockerfile/docker-compose -**NEVER build server binaries locally** - all builds through Dockerfile/docker-compose
-**NEVER create new migration files** - merge changes into current one until release -**NEVER create new migration files** - merge changes into current one until release
-**NEVER use `git checkout` on schema files** without checking what will be lost -**NEVER use `git checkout` on schema files** without checking what will be lost
-**NEVER break existing functionality** unless explicitly instructed -**NEVER break existing functionality** unless explicitly instructed
-**NEVER duplicate business logic** - keep logic in services, not handlers -**NEVER duplicate business logic** - keep logic in services, not handlers
-**NEVER bypass service layer** - all database operations must go through services -**NEVER bypass service layer** - all database operations must go through services
### Testing ### Testing
-**ALWAYS use `setupTestServer()` helper from `cmd/server/tests/test_helpers.go`** -**ALWAYS use `setupTestServer()` helper from `cmd/server/tests/test_helpers.go`**
-**Share one test setup across all subtests** - call `setupTestServer()` once at test function level, not per subtest -**Share one test setup across all subtests** - call `setupTestServer()` once at test function level, not per subtest
-**Prefer table-driven tests** - use `t.Run()` with test cases instead of duplicate test functions -**Prefer table-driven tests** - use `t.Run()` with test cases instead of duplicate test functions
@@ -24,6 +26,7 @@
-**Use `t.Cleanup()` properly** - the `TestServerSetup` pattern automatically handles cleanup via `t.Cleanup()` -**Use `t.Cleanup()` properly** - the `TestServerSetup` pattern automatically handles cleanup via `t.Cleanup()`
### Frontend & Styling ### Frontend & Styling
-**NEVER modify backend/API for frontend features without user confirmation** -**NEVER modify backend/API for frontend features without user confirmation**
-**NEVER use custom CSS** - TailwindCSS classes only -**NEVER use custom CSS** - TailwindCSS classes only
- **⚠️ EXCEPTION**: `templates/error.templ` may have inline CSS because error pages must work when main app fails (404, server errors, CSS fails to load) - **⚠️ EXCEPTION**: `templates/error.templ` may have inline CSS because error pages must work when main app fails (404, server errors, CSS fails to load)
@@ -37,9 +40,10 @@
-**NEVER fetch initial data via AJAX on page load** - use server-side rendering instead -**NEVER fetch initial data via AJAX on page load** - use server-side rendering instead
-**NEVER break progressive enhancement** - pages must work without JavaScript -**NEVER break progressive enhancement** - pages must work without JavaScript
**Note:** Go methods in the backend are fine and encouraged. This guideline applies to TypeScript/JavaScript frontend code only.** **Note:** Go methods in the backend are fine and encouraged. This guideline applies to TypeScript/JavaScript frontend code only.\*\*
### General ### General
-**NEVER skip pre-commit hooks** unless explicitly requested -**NEVER skip pre-commit hooks** unless explicitly requested
-**NEVER force push to main/master** branches -**NEVER force push to main/master** branches
-**NEVER commit files with secrets** (.env, credentials.json, etc.) -**NEVER commit files with secrets** (.env, credentials.json, etc.)
@@ -60,6 +64,7 @@
### Cascading Fix-up Pattern (PROHIBITED) ### Cascading Fix-up Pattern (PROHIBITED)
**WHAT NOT TO DO** - This caused critical bugs: **WHAT NOT TO DO** - This caused critical bugs:
```go ```go
// ❌ WRONG: Blindly making fixes after compilation error // ❌ WRONG: Blindly making fixes after compilation error
@@ -74,6 +79,7 @@ Edit 3: Try to fix again (worse damage)
``` ```
**CORRECT APPROACH**: **CORRECT APPROACH**:
```go ```go
// ✅ CORRECT: Stop, understand, then fix deliberately // ✅ CORRECT: Stop, understand, then fix deliberately
@@ -86,6 +92,7 @@ VERIFY → Compile successfully
``` ```
**Key Principle**: When compilation errors occur after edits: **Key Principle**: When compilation errors occur after edits:
1. STOP - Don't make more edits 1. STOP - Don't make more edits
2. ANALYZE - Use `git diff` to understand what was changed 2. ANALYZE - Use `git diff` to understand what was changed
3. RECOVER - Restore what was accidentally deleted/broken 3. RECOVER - Restore what was accidentally deleted/broken
@@ -96,6 +103,7 @@ VERIFY → Compile successfully
## 🎯 CONTEXT-SPECIFIC RULES ## 🎯 CONTEXT-SPECIFIC RULES
### When Working on Frontend-Only Tasks ### When Working on Frontend-Only Tasks
- **DO NOT touch backend code** - handlers, services, database layer - **DO NOT touch backend code** - handlers, services, database layer
- **DO NOT modify API routes** - use existing endpoints only - **DO NOT modify API routes** - use existing endpoints only
- **DO NOT change database schema** - work with existing structure - **DO NOT change database schema** - work with existing structure
@@ -106,6 +114,7 @@ VERIFY → Compile successfully
4. **ASK FOR USER CONFIRMATION before proceeding** 4. **ASK FOR USER CONFIRMATION before proceeding**
### When Working on Full-Stack Tasks ### When Working on Full-Stack Tasks
- Backend changes are allowed when explicitly part of the task - Backend changes are allowed when explicitly part of the task
- Still follow all database protocols (atomic changes, validation, etc.) - Still follow all database protocols (atomic changes, validation, etc.)
- **If modifying database schema:** Update local database after schema.sql changes (see Database Operations section) - **If modifying database schema:** Update local database after schema.sql changes (see Database Operations section)
@@ -117,6 +126,7 @@ VERIFY → Compile successfully
## ✅ MANDATORY REQUIREMENTS ## ✅ MANDATORY REQUIREMENTS
### Database Operations (Full-Stack Tasks Only) ### Database Operations (Full-Stack Tasks Only)
- ✅ Follow **pgx v5 standards** for all database operations - ✅ Follow **pgx v5 standards** for all database operations
- ✅ Treat schema changes as **ATOMIC** - complete success or complete rejection - ✅ Treat schema changes as **ATOMIC** - complete success or complete rejection
-**⚠️ CRITICAL: This is a pre-production application (NO production deployments exist)** -**⚠️ CRITICAL: This is a pre-production application (NO production deployments exist)**
@@ -132,16 +142,19 @@ VERIFY → Compile successfully
- ✅ **Post-change validation**: ensure schema.sql, models.go, and queries.sql are in sync - ✅ **Post-change validation**: ensure schema.sql, models.go, and queries.sql are in sync
### Build & Deployment ### Build & Deployment
- ✅ Use **Podman** exclusively (not Docker) - ✅ Use **Podman** exclusively (not Docker)
- ✅ All builds through existing **Dockerfile** and **docker-compose.yml** - ✅ All builds through existing **Dockerfile** and **docker-compose.yml**
- ✅ Stop building server binaries - everything goes through containers - ✅ Stop building server binaries - everything goes through containers
### API Changes (Full-Stack Tasks Only) ### API Changes (Full-Stack Tasks Only)
- ✅ Include **Bruno OpenCollection YAML requests** with all API documentation - ✅ Include **Bruno OpenCollection YAML requests** with all API documentation
- ✅ Tests must be **comprehensive and cover three contexts**: no user, user, and admin - ✅ Tests must be **comprehensive and cover three contexts**: no user, user, and admin
- ✅ Maintain backward compatibility for mobile apps and external consumers - ✅ Maintain backward compatibility for mobile apps and external consumers
### Frontend & Styling ### Frontend & Styling
- ✅ Always use **TailwindCSS classes** for all styling - ✅ Always use **TailwindCSS classes** for all styling
- ✅ Convert all JavaScript to **TypeScript** - ✅ Convert all JavaScript to **TypeScript**
- ✅ **Never use Object-Oriented Programming** (no classes, inheritance, or this-capture) - ✅ **Never use Object-Oriented Programming** (no classes, inheritance, or this-capture)
@@ -154,12 +167,14 @@ VERIFY → Compile successfully
- ✅ **Ensure progressive enhancement** - pages work without JavaScript - ✅ **Ensure progressive enhancement** - pages work without JavaScript
### Service Layer Architecture ### Service Layer Architecture
- ✅ **All business logic in services** - never in handlers - ✅ **All business logic in services** - never in handlers
- ✅ **Services must be reusable** by both SSR handlers and API endpoints - ✅ **Services must be reusable** by both SSR handlers and API endpoints
- ✅ **Database operations through services only** - never direct from handlers - ✅ **Database operations through services only** - never direct from handlers
- ✅ **When adding features**: Add service logic → Create API endpoint → Use SSR for initial render → Use JS for updates - ✅ **When adding features**: Add service logic → Create API endpoint → Use SSR for initial render → Use JS for updates
### Code Organization ### Code Organization
- ✅ Minimize project structure changes - ✅ Minimize project structure changes
- ✅ Place new files in **contextually appropriate directories** - ✅ Place new files in **contextually appropriate directories**
- ✅ Follow **KISS**, **DRY**, and **YAGNI** principles - ✅ Follow **KISS**, **DRY**, and **YAGNI** principles
@@ -172,10 +187,12 @@ VERIFY → Compile successfully
- ❌ **NEVER create conversion helper functions** to map between handler and template types - use handler types directly - ❌ **NEVER create conversion helper functions** to map between handler and template types - use handler types directly
### Configuration & Environment ### Configuration & Environment
- ✅ If **.env is missing**, auto-generate secure values - ✅ If **.env is missing**, auto-generate secure values
- ✅ Never commit secrets to repository - ✅ Never commit secrets to repository
### Code Modification Safety ### Code Modification Safety
- ✅ **Post-Edit Verification (MANDATORY for ALL file modifications)**: - ✅ **Post-Edit Verification (MANDATORY for ALL file modifications)**:
- Run `go build` for affected packages immediately after each edit - Run `go build` for affected packages immediately after each edit
- Review `git diff filename` to verify only intended changes - Review `git diff filename` to verify only intended changes
@@ -194,6 +211,7 @@ VERIFY → Compile successfully
### Documentation ### Documentation
**Documentation Structure** (updated with full docs system): **Documentation Structure** (updated with full docs system):
- ✅ **README.md** - Project overview, quick start, and setup instructions only - ✅ **README.md** - Project overview, quick start, and setup instructions only
- ✅ **docs/** - Comprehensive documentation system with search - ✅ **docs/** - Comprehensive documentation system with search
- ✅ **docs/developer/api/** - API reference documentation (split by endpoint/category) - ✅ **docs/developer/api/** - API reference documentation (split by endpoint/category)
@@ -203,19 +221,20 @@ VERIFY → Compile successfully
**Where to document changes**: **Where to document changes**:
| Change Type | Location | Examples | | Change Type | Location | Examples |
|-------------|----------|----------| | ----------------------------------- | ------------------------------------------------------------- | --------------------------------------------------------- |
| **User-facing features** | `docs/user/` | New features, UI changes, workflows | | **User-facing features** | `docs/user/` | New features, UI changes, workflows |
| **API endpoints** | `docs/developer/api/<category>/<endpoint>.md` | New endpoints, modified responses, authentication changes | | **API endpoints** | `docs/developer/api/<category>/<endpoint>.md` | New endpoints, modified responses, authentication changes |
| **API behavior** | Update existing `docs/developer/api/` files | Parameter changes, error codes, rate limits | | **API behavior** | Update existing `docs/developer/api/` files | Parameter changes, error codes, rate limits |
| **Device setup** | `docs/user/devices/` | New device support, setup instructions | | **Device setup** | `docs/user/devices/` | New device support, setup instructions |
| **Development** | `docs/contributing/` | Build changes, architecture decisions | | **Development** | `docs/contributing/` | Build changes, architecture decisions |
| **Quick start/setup** | `README.md` | Installation, environment setup, first-run | | **Quick start/setup** | `README.md` | Installation, environment setup, first-run |
| **Breaking changes** | Both `README.md` and relevant `docs/` | Migration guides, deprecation notices | | **Breaking changes** | Both `README.md` and relevant `docs/` | Migration guides, deprecation notices |
| **Bug fixes** | Update relevant `docs/` only if user-visible | Clarifications, troubleshooting additions | | **Bug fixes** | Update relevant `docs/` only if user-visible | Clarifications, troubleshooting additions |
| **Bruno OpenCollection YAML tests** | `.yml` files in bruno folder in appropriate folder/sub-folder | API contract testing, examples | | **Bruno OpenCollection YAML tests** | `.yml` files in bruno folder in appropriate folder/sub-folder | API contract testing, examples |
**Documentation Update Workflow**: **Documentation Update Workflow**:
1. **Identify the audience** (end users, developers, API consumers) 1. **Identify the audience** (end users, developers, API consumers)
2. **Choose appropriate location** based on table above 2. **Choose appropriate location** based on table above
3. **Update documentation** before or with code changes 3. **Update documentation** before or with code changes
@@ -225,12 +244,14 @@ VERIFY → Compile successfully
7. **Commit separately** with clear message: `docs: <description>` 7. **Commit separately** with clear message: `docs: <description>`
**When in doubt**: **When in doubt**:
- End-user visible → `docs/user/` - End-user visible → `docs/user/`
- API reference → `docs/developer/api/` - API reference → `docs/developer/api/`
- Setup/onboarding → `README.md` - Setup/onboarding → `README.md`
- Development related → `docs/contributing/` - Development related → `docs/contributing/`
### Process & Continuity ### Process & Continuity
- ✅ If mid-task and receive "no response", **continue the task** - ✅ If mid-task and receive "no response", **continue the task**
- ✅ Verify no regressions before modifying/removing code - ✅ Verify no regressions before modifying/removing code
@@ -239,18 +260,21 @@ VERIFY → Compile successfully
## 🔧 TECHNICAL STANDARDS ## 🔧 TECHNICAL STANDARDS
### Backend Stack ### Backend Stack
- **Language**: Go 1.25+ - **Language**: Go 1.25+
- **Database**: PostgreSQL 15+ with **pgx v5 driver** only - **Database**: PostgreSQL 15+ with **pgx v5 driver** only
- **Authentication**: JWT tokens with bcrypt password hashing - **Authentication**: JWT tokens with bcrypt password hashing
- **Architecture**: Service layer pattern (handlers → services → database) - **Architecture**: Service layer pattern (handlers → services → database)
### Frontend Stack ### Frontend Stack
- **Styling**: TailwindCSS (no custom CSS) - **Styling**: TailwindCSS (no custom CSS)
- **Language**: TypeScript (no JavaScript) - **Language**: TypeScript (no JavaScript)
- **Templates**: HTMX with server-side rendering - **Templates**: HTMX with server-side rendering
- **Patterns**: Procedural/imperative with functional techniques where helpful (no OOP) - **Patterns**: Procedural/imperative with functional techniques where helpful (no OOP)
### Containerization ### Containerization
- **Runtime**: Podman (not Docker) - **Runtime**: Podman (not Docker)
- **Build**: Existing Dockerfile and docker-compose.yml only - **Build**: Existing Dockerfile and docker-compose.yml only
- **No local builds** allowed - **No local builds** allowed
@@ -262,6 +286,7 @@ VERIFY → Compile successfully
When code modification mistakes occur (deleted wrong code, broke compilation, etc.): When code modification mistakes occur (deleted wrong code, broke compilation, etc.):
### Immediate Actions ### Immediate Actions
1. **STOP** - Don't make more edits 1. **STOP** - Don't make more edits
2. **ASSESS** - What was deleted? Is it critical? 2. **ASSESS** - What was deleted? Is it critical?
3. **REVIEW** - Run `git diff` to see exact changes 3. **REVIEW** - Run `git diff` to see exact changes
@@ -273,6 +298,7 @@ When code modification mistakes occur (deleted wrong code, broke compilation, et
6. **DOCUMENT** - Note what went wrong for future reference 6. **DOCUMENT** - Note what went wrong for future reference
### Recovery Examples ### Recovery Examples
```bash ```bash
# Recover a deleted function from original file # Recover a deleted function from original file
git show HEAD:internal/handlers/auth.go | sed -n '70,275p' > recovery.txt git show HEAD:internal/handlers/auth.go | sed -n '70,275p' > recovery.txt
@@ -286,6 +312,7 @@ git checkout -- internal/handlers/auth.go
``` ```
### Prevention (Learn From Mistakes) ### Prevention (Learn From Mistakes)
- Why did the mistake happen? - Why did the mistake happen?
- Was it too-broad matching? - Was it too-broad matching?
- Was it insufficient context reading? - Was it insufficient context reading?
@@ -297,6 +324,7 @@ git checkout -- internal/handlers/auth.go
## 📋 WORKFLOW CHECKLISTS ## 📋 WORKFLOW CHECKLISTS
### Before Making Frontend-Only Changes ### Before Making Frontend-Only Changes
- [ ] Identify if backend modification could make implementation simpler - [ ] Identify if backend modification could make implementation simpler
- [ ] Plan to use existing API endpoints only - [ ] Plan to use existing API endpoints only
- [ ] If backend change seems necessary, prepare confirmation request: - [ ] If backend change seems necessary, prepare confirmation request:
@@ -311,6 +339,7 @@ git checkout -- internal/handlers/auth.go
- [ ] Setup instructions → `README.md` - [ ] Setup instructions → `README.md`
### Before Making Full-Stack Changes ### Before Making Full-Stack Changes
- [ ] Read current schema completely (if database changes) - [ ] Read current schema completely (if database changes)
- [ ] Identify all columns that must be preserved - [ ] Identify all columns that must be preserved
- [ ] Plan exact changes needed - [ ] Plan exact changes needed
@@ -324,6 +353,7 @@ git checkout -- internal/handlers/auth.go
- [ ] Bruno OpenCollection YAML `.yml` files → Update/create alongside API changes - [ ] Bruno OpenCollection YAML `.yml` files → Update/create alongside API changes
### During Schema Changes (Full-Stack Only) ### During Schema Changes (Full-Stack Only)
- [ ] Read current schema completely - [ ] Read current schema completely
- [ ] Identify all columns that must be preserved - [ ] Identify all columns that must be preserved
- [ ] Plan exact changes needed - [ ] Plan exact changes needed
@@ -345,6 +375,7 @@ git checkout -- internal/handlers/auth.go
- [ ] Verify database has new schema (check column types, indexes, etc.) - [ ] Verify database has new schema (check column types, indexes, etc.)
### After API Changes ### After API Changes
- [ ] Create/update Bruno OpenCollection YAML requests - [ ] Create/update Bruno OpenCollection YAML requests
- [ ] Test with no user context - [ ] Test with no user context
- [ ] Test with regular user context - [ ] Test with regular user context
@@ -352,6 +383,7 @@ git checkout -- internal/handlers/auth.go
- [ ] Verify backward compatibility - [ ] Verify backward compatibility
### Before Committing ### Before Committing
- [ ] **Run verification script**: `bash scripts/verify-guidelines.sh` - [ ] **Run verification script**: `bash scripts/verify-guidelines.sh`
- [ ] **Fix any errors** - verification must pass (0 errors) to commit - [ ] **Fix any errors** - verification must pass (0 errors) to commit
- [ ] **Note warnings** - informational only, do not auto-fix - [ ] **Note warnings** - informational only, do not auto-fix
@@ -368,6 +400,7 @@ git checkout -- internal/handlers/auth.go
- [ ] **Test docs search** finds new content - [ ] **Test docs search** finds new content
### Error Recovery Protocol (If Code Mistakes Occur) ### Error Recovery Protocol (If Code Mistakes Occur)
- [ ] **Stop immediately** - don't make more edits - [ ] **Stop immediately** - don't make more edits
- [ ] **Assess impact**: What was deleted? Is it critical? - [ ] **Assess impact**: What was deleted? Is it critical?
- [ ] **Review git diff**: See exact changes made - [ ] **Review git diff**: See exact changes made
@@ -378,6 +411,7 @@ git checkout -- internal/handlers/auth.go
- [ ] **Document mistake**: Note what went wrong for future reference - [ ] **Document mistake**: Note what went wrong for future reference
### Phase Completion Verification (Before Declaring "Complete") ### Phase Completion Verification (Before Declaring "Complete")
- [ ] All target code is removed/intact as intended - [ ] All target code is removed/intact as intended
- [ ] No unintended code was deleted - [ ] No unintended code was deleted
- [ ] All affected files compile successfully - [ ] All affected files compile successfully
@@ -392,6 +426,7 @@ git checkout -- internal/handlers/auth.go
## 🏗 ARCHITECTURAL PATTERNS ## 🏗 ARCHITECTURAL PATTERNS
### Current: Hybrid SSR ### Current: Hybrid SSR
``` ```
Browser → Go template (with data) → Display instantly Browser → Go template (with data) → Display instantly
+14 -6
View File
@@ -17,6 +17,7 @@ A modern self-hosted media library system built with Go, PostgreSQL, HTMX, and T
## 🚀 Quick Start ## 🚀 Quick Start
### Prerequisites ### Prerequisites
- **Podman** (recommended) or Docker - **Podman** (recommended) or Docker
- **5 minutes** of your time - **5 minutes** of your time
@@ -48,6 +49,7 @@ The first user to register automatically becomes an admin.
## 📖 Key Features ## 📖 Key Features
### Universal Cross-Platform Sync ### Universal Cross-Platform Sync
- **Real-Time Progress**: Turn a page on your Kindle, see it on your phone - **Real-Time Progress**: Turn a page on your Kindle, see it on your phone
- **Format-Aware**: EPUB CFI, page numbers, percentages - all handled correctly - **Format-Aware**: EPUB CFI, page numbers, percentages - all handled correctly
- **Offline Queue**: Changes sync when you reconnect, priority-processed - **Offline Queue**: Changes sync when you reconnect, priority-processed
@@ -57,6 +59,7 @@ The first user to register automatically becomes an admin.
- **Format Conversion**: On-the-fly EPUB→KEPUB for Kobo devices - **Format Conversion**: On-the-fly EPUB→KEPUB for Kobo devices
### Media Management ### Media Management
- **Smart Search**: Partial matching with fuzzy search fallback for typos - **Smart Search**: Partial matching with fuzzy search fallback for typos
- **Advanced Filtering**: Filter by author, series, genre, language, year, cover images - **Advanced Filtering**: Filter by author, series, genre, language, year, cover images
- **Dynamic Sorting**: By title, author, date added, published date, page count, series - **Dynamic Sorting**: By title, author, date added, published date, page count, series
@@ -66,11 +69,13 @@ The first user to register automatically becomes an admin.
- **Usage Analytics**: Reading statistics, device usage, popular books - **Usage Analytics**: Reading statistics, device usage, popular books
### Smart Collections ### Smart Collections
- **Auto-Assign Rules**: Automatically add books based on genre, author, series, tags, language, publisher, year - **Auto-Assign Rules**: Automatically add books based on genre, author, series, tags, language, publisher, year
- **Device Shelf Mappings**: Sync collections to Kobo shelves and KOReader categories - **Device Shelf Mappings**: Sync collections to Kobo shelves and KOReader categories
- **Test Before Creating**: Preview which books match your rules - **Test Before Creating**: Preview which books match your rules
### Library Organization ### Library Organization
- **Multi-Library Support**: Ebooks, Comics, and Manga with type-specific file formats - **Multi-Library Support**: Ebooks, Comics, and Manga with type-specific file formats
- **Multiple Folders**: Add multiple scanning folders per library - **Multiple Folders**: Add multiple scanning folders per library
- **Visibility Control**: Admins control which libraries each user can see - **Visibility Control**: Admins control which libraries each user can see
@@ -78,6 +83,7 @@ The first user to register automatically becomes an admin.
- **Watch Mode**: Real-time file system monitoring for instant updates - **Watch Mode**: Real-time file system monitoring for instant updates
### Security ### Security
- **JWT Authentication**: Short-lived access tokens (1 hour) with refresh tokens (7 days) - **JWT Authentication**: Short-lived access tokens (1 hour) with refresh tokens (7 days)
- **Strong Passwords**: Complexity requirements enforced (8+ chars, uppercase, lowercase, number, special) - **Strong Passwords**: Complexity requirements enforced (8+ chars, uppercase, lowercase, number, special)
- **Account Lockout**: 5 failed attempts = 15-minute lockout - **Account Lockout**: 5 failed attempts = 15-minute lockout
@@ -90,6 +96,7 @@ The first user to register automatically becomes an admin.
## 📚 Documentation ## 📚 Documentation
### For Users & Self-Hosters ### For Users & Self-Hosters
- **[docs/user/sync-guide.md](docs/user/sync-guide.md)** - Understanding and using universal sync - **[docs/user/sync-guide.md](docs/user/sync-guide.md)** - Understanding and using universal sync
- **[docs/user/devices/kobo-setup.md](docs/user/devices/kobo-setup.md)** - Kobo e-reader configuration - **[docs/user/devices/kobo-setup.md](docs/user/devices/kobo-setup.md)** - Kobo e-reader configuration
- **[docs/user/devices/koreader-setup.md](docs/user/devices/koreader-setup.md)** - KOReader configuration - **[docs/user/devices/koreader-setup.md](docs/user/devices/koreader-setup.md)** - KOReader configuration
@@ -98,6 +105,7 @@ The first user to register automatically becomes an admin.
- **[docs/user/settings-guide.md](docs/user/settings-guide.md)** - Settings and preferences - **[docs/user/settings-guide.md](docs/user/settings-guide.md)** - Settings and preferences
### For Developers ### For Developers
- **[docs/developer/api/api-reference.md](docs/developer/api/api-reference.md)** - Complete API documentation - **[docs/developer/api/api-reference.md](docs/developer/api/api-reference.md)** - Complete API documentation
- **[docs/contributing/DEVELOPMENT.md](docs/contributing/DEVELOPMENT.md)** - Development workflow - **[docs/contributing/DEVELOPMENT.md](docs/contributing/DEVELOPMENT.md)** - Development workflow
@@ -105,12 +113,12 @@ The first user to register automatically becomes an admin.
## 🎯 Supported Devices ## 🎯 Supported Devices
| Platform | Sync | OPDS | Status | | Platform | Sync | OPDS | Status |
|----------|------|------|--------| | ---------------- | ---- | ---- | ------------------------ |
| **Web Browser** | ✅ | ✅ | Full support | | **Web Browser** | ✅ | ✅ | Full support |
| **KOReader** | ✅ | ✅ | Kindle, Kobo, PocketBook | | **KOReader** | ✅ | ✅ | Kindle, Kobo, PocketBook |
| **Kobo Devices** | ✅ | ✅ | Clara, Libra, Sage, etc. | | **Kobo Devices** | ✅ | ✅ | Clara, Libra, Sage, etc. |
| **Mobile Apps** | 🚧 | 🚧 | Coming Q2 2026 | | **Mobile Apps** | 🚧 | 🚧 | Coming Q2 2026 |
--- ---
+381 -302
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -5,6 +5,7 @@ This document describes the shared test data used across Go integration tests an
## Test Users ## Test Users
### Main Admin Test User ### Main Admin Test User
This is the primary test user used in most integration tests. This is the primary test user used in most integration tests.
```json ```json
@@ -19,15 +20,18 @@ This is the primary test user used in most integration tests.
``` ```
**Used in:** **Used in:**
- Go Tests: `cmd/server/tests/test_helpers.go` (getTestUserID, loginTestUser) - Go Tests: `cmd/server/tests/test_helpers.go` (getTestUserID, loginTestUser)
- Bruno: `user/auth/Login User.yml`, `user/auth/Register User.yml` - Bruno: `user/auth/Login User.yml`, `user/auth/Register User.yml`
**Notes:** **Notes:**
- Automatically created if doesn't exist - Automatically created if doesn't exist
- Deleted and recreated in tests to ensure fresh state - Deleted and recreated in tests to ensure fresh state
- Used for authentication in most test scenarios - Used for authentication in most test scenarios
### Max Devices Test User ### Max Devices Test User
Used specifically for testing device limit functionality. Used specifically for testing device limit functionality.
```json ```json
@@ -41,9 +45,11 @@ Used specifically for testing device limit functionality.
``` ```
**Used in:** **Used in:**
- Go Tests: `cmd/server/tests/device_cap_test.go` (createTestUserForMaxDevices) - Go Tests: `cmd/server/tests/device_cap_test.go` (createTestUserForMaxDevices)
### Secondary Admin Test User ### Secondary Admin Test User
Used for testing admin creation restrictions and multi-admin scenarios. Used for testing admin creation restrictions and multi-admin scenarios.
```json ```json
@@ -58,11 +64,13 @@ Used for testing admin creation restrictions and multi-admin scenarios.
``` ```
**Used in:** **Used in:**
- Bruno: `user/admin/Register Admin User.yml` - Bruno: `user/admin/Register Admin User.yml`
## Test Libraries ## Test Libraries
### Standard Test Library ### Standard Test Library
```json ```json
{ {
"name": "Test Library", "name": "Test Library",
@@ -72,10 +80,12 @@ Used for testing admin creation restrictions and multi-admin scenarios.
``` ```
**Used in:** **Used in:**
- Go Tests: `cmd/server/tests/test_helpers.go` (createTestEbookID) - Go Tests: `cmd/server/tests/test_helpers.go` (createTestEbookID)
- Multiple test files for library management - Multiple test files for library management
### Search Test Library ### Search Test Library
```json ```json
{ {
"name": "Search Test Library", "name": "Search Test Library",
@@ -85,11 +95,13 @@ Used for testing admin creation restrictions and multi-admin scenarios.
``` ```
**Used in:** **Used in:**
- Go Tests: `cmd/server/tests/search_test.go` - Go Tests: `cmd/server/tests/search_test.go`
## Test Books/Media Items ## Test Books/Media Items
### Standard Test Ebook ### Standard Test Ebook
```json ```json
{ {
"title": "Test Ebook", "title": "Test Ebook",
@@ -101,10 +113,13 @@ Used for testing admin creation restrictions and multi-admin scenarios.
``` ```
**Used in:** **Used in:**
- Go Tests: `cmd/server/tests/test_helpers.go` (createTestEbookID) - Go Tests: `cmd/server/tests/test_helpers.go` (createTestEbookID)
### Test Book Variants ### Test Book Variants
Multiple test books with different titles for testing: Multiple test books with different titles for testing:
- "Test Book 1" - "Test Book 1"
- "Test Book 2" - "Test Book 2"
- "Test Book Title" - "Test Book Title"
@@ -113,6 +128,7 @@ Multiple test books with different titles for testing:
## Test Devices ## Test Devices
Test devices typically follow this pattern: Test devices typically follow this pattern:
- Device ID: UUID format - Device ID: UUID format
- Device Name: "Test Device" or descriptive names - Device Name: "Test Device" or descriptive names
- User association: Linked to test users - User association: Linked to test users
@@ -139,23 +155,28 @@ Test devices typically follow this pattern:
## File Paths ## File Paths
### Container Paths (inside Docker container) ### Container Paths (inside Docker container)
- Uploads: `/app/uploads` - Uploads: `/app/uploads`
- Cache: `/app/cache/kepub` - Cache: `/app/cache/kepub`
### Host Paths (when running tests from host) ### Host Paths (when running tests from host)
- Uploads: `./uploads` - Uploads: `./uploads`
- Cache: Docker volume (not on host filesystem) - Cache: Docker volume (not on host filesystem)
## How to Use This Data ## How to Use This Data
### In Bruno Tests ### In Bruno Tests
1. Start the server: `podman compose up -d` 1. Start the server: `podman compose up -d`
2. Run "Register User" to create the test admin user 2. Run "Register User" to create the test admin user
3. Run "Login User" to get the JWT token 3. Run "Login User" to get the JWT token
4. Use the token for authenticated requests 4. Use the token for authenticated requests
### In Go Tests ### In Go Tests
The test helpers automatically create and clean up test data: The test helpers automatically create and clean up test data:
```go ```go
ts, db, cfg := setupTestServer(t) ts, db, cfg := setupTestServer(t)
token := loginTestUser(t, ts, db) token := loginTestUser(t, ts, db)
@@ -163,7 +184,9 @@ userID := getTestUserID(t, db)
``` ```
### Cross-Referencing ### Cross-Referencing
When you find a bug in Bruno tests: When you find a bug in Bruno tests:
1. Check the same scenario in Go tests using the same credentials 1. Check the same scenario in Go tests using the same credentials
2. Use the same email/password to debug 2. Use the same email/password to debug
3. Verify the database state matches expectations 3. Verify the database state matches expectations
@@ -171,6 +194,7 @@ When you find a bug in Bruno tests:
## Resetting Test Data ## Resetting Test Data
### Reset Database ### Reset Database
```bash ```bash
# Stop containers and remove volumes # Stop containers and remove volumes
podman compose down -v podman compose down -v
@@ -180,7 +204,9 @@ podman compose up -d
``` ```
### Reset Specific Test User ### Reset Specific Test User
If you need to recreate just the test user: If you need to recreate just the test user:
```bash ```bash
# Login to database # Login to database
podman exec -it bookhoard_db psql -U postgres -d bookhoard podman exec -it bookhoard_db psql -U postgres -d bookhoard
@@ -202,6 +228,7 @@ DELETE FROM users WHERE email = 'testuser@example.com';
## Adding New Test Data ## Adding New Test Data
When adding new test data: When adding new test data:
1. Choose descriptive names following the pattern "Test X" 1. Choose descriptive names following the pattern "Test X"
2. Use consistent email format: `testpurpose@example.com` 2. Use consistent email format: `testpurpose@example.com`
3. Document in this file for cross-reference 3. Document in this file for cross-reference
+132 -93
View File
@@ -3,6 +3,7 @@
## Overview ## Overview
Fix file and cover image serving to support: Fix file and cover image serving to support:
1. Multiple library folders in docker compose (flexible mount points) 1. Multiple library folders in docker compose (flexible mount points)
2. Keep files with books (no hardcoded paths) 2. Keep files with books (no hardcoded paths)
3. Store relative paths in database (for both files AND covers) 3. Store relative paths in database (for both files AND covers)
@@ -12,12 +13,14 @@ Fix file and cover image serving to support:
## Architecture ## Architecture
### Current Behavior ### Current Behavior
- File path stored as absolute: `/app/uploads/Jane Austen/Pride and Prejudice/book.epub` - File path stored as absolute: `/app/uploads/Jane Austen/Pride and Prejudice/book.epub`
- Cover path stored as absolute: `/app/uploads/Jane Austen/Pride and Prejudice/cover.jpg` - Cover path stored as absolute: `/app/uploads/Jane Austen/Pride and Prejudice/cover.jpg`
- Frontend uses path directly - doesn't work (browser can't access container paths) - Frontend uses path directly - doesn't work (browser can't access container paths)
- No route serves `/app/uploads/*` - No route serves `/app/uploads/*`
### Target Behavior ### Target Behavior
- File path stored as relative: `Jane Austen/Pride and Prejudice/book.epub` - File path stored as relative: `Jane Austen/Pride and Prejudice/book.epub`
- Cover path stored as relative: `Jane Austen/Pride and Prejudice/cover.jpg` - Cover path stored as relative: `Jane Austen/Pride and Prejudice/cover.jpg`
- Handler resolves relative path using library folder base path - Handler resolves relative path using library folder base path
@@ -26,16 +29,21 @@ Fix file and cover image serving to support:
- Works with mobile apps, Kobo, KOReader devices via same endpoints - Works with mobile apps, Kobo, KOReader devices via same endpoints
### URL Format ### URL Format
To handle same relative paths in different libraries, use: To handle same relative paths in different libraries, use:
``` ```
/uploads/library-{library_id}/relative/path /uploads/library-{library_id}/relative/path
``` ```
- Requires JWT authentication (like API endpoints) - Requires JWT authentication (like API endpoints)
- Works for both covers and book files - Works for both covers and book files
- Single handler handles all file serving - Single handler handles all file serving
### Universal Path Resolution ### Universal Path Resolution
All handlers use the same `LibraryService.ResolveMediaPath()` function: All handlers use the same `LibraryService.ResolveMediaPath()` function:
- MediaHandler (downloads) - MediaHandler (downloads)
- OPDSHandler (device cover images) - OPDSHandler (device cover images)
- Future handlers - Future handlers
@@ -53,16 +61,19 @@ This ensures one source of truth for path resolution.
**Location**: In `internal/services/media_scanner.go` - wherever `FilePath` is set in the database insert **Location**: In `internal/services/media_scanner.go` - wherever `FilePath` is set in the database insert
**Current code** (line 579): **Current code** (line 579):
```go ```go
FilePath: path, // path is absolute like /app/uploads/Author/Book/file.epub FilePath: path, // path is absolute like /app/uploads/Author/Book/file.epub
``` ```
**New code**: **New code**:
```go ```go
FilePath: s.getRelativePath(path), FilePath: s.getRelativePath(path),
``` ```
**Also update** line 617 for format file paths: **Also update** line 617 for format file paths:
```go ```go
FilePath: pgtype.Text{String: s.getRelativePath(format.FilePath), Valid: true}, FilePath: pgtype.Text{String: s.getRelativePath(format.FilePath), Valid: true},
``` ```
@@ -74,6 +85,7 @@ FilePath: pgtype.Text{String: s.getRelativePath(format.FilePath), Valid: tr
**Location**: In `internal/services/media_scanner.go` - wherever `metadata.CoverPath` is set **Location**: In `internal/services/media_scanner.go` - wherever `metadata.CoverPath` is set
**Current code** (example at line 517): **Current code** (example at line 517):
```go ```go
if len(coverImage) > 0 && metadata.CoverPath == "" { if len(coverImage) > 0 && metadata.CoverPath == "" {
coverPath := path + ".cover.jpg" coverPath := path + ".cover.jpg"
@@ -84,6 +96,7 @@ if len(coverImage) > 0 && metadata.CoverPath == "" {
``` ```
**New code**: **New code**:
```go ```go
if len(coverImage) > 0 && metadata.CoverPath == "" { if len(coverImage) > 0 && metadata.CoverPath == "" {
coverPath := path + ".cover.jpg" coverPath := path + ".cover.jpg"
@@ -95,6 +108,7 @@ if len(coverImage) > 0 && metadata.CoverPath == "" {
``` ```
**All locations where metadata.CoverPath is set**: **All locations where metadata.CoverPath is set**:
- Line 517 (main cover) - Line 517 (main cover)
- Line 645 (sidecar cover) - Line 645 (sidecar cover)
- Line 651 (sidecar cover alternative) - Line 651 (sidecar cover alternative)
@@ -187,12 +201,12 @@ func (mh *MediaHandler) getFullFilePath(ctx context.Context, libraryID pgtype.UU
if relativePath == "" { if relativePath == "" {
return "", fmt.Errorf("no file path") return "", fmt.Errorf("no file path")
} }
// Check if already absolute (backward compatibility) // Check if already absolute (backward compatibility)
if filepath.IsAbs(relativePath) { if filepath.IsAbs(relativePath) {
return relativePath, nil return relativePath, nil
} }
// Use service for resolution (one source of truth) // Use service for resolution (one source of truth)
return mh.libraryService.ResolveMediaPath(ctx, libraryID, relativePath) return mh.libraryService.ResolveMediaPath(ctx, libraryID, relativePath)
} }
@@ -209,6 +223,7 @@ Note: The handler already has `libraryService` injected, so this just calls thro
#### Modify DownloadBook function #### Modify DownloadBook function
**Current code** (line 103-144): **Current code** (line 103-144):
```go ```go
func (h *MediaHandler) DownloadBook(c echo.Context) error { func (h *MediaHandler) DownloadBook(c echo.Context) error {
// ... // ...
@@ -227,6 +242,7 @@ func (h *MediaHandler) DownloadBook(c echo.Context) error {
``` ```
**New code**: **New code**:
```go ```go
func (h *MediaHandler) DownloadBook(c echo.Context) error { func (h *MediaHandler) DownloadBook(c echo.Context) error {
// ... // ...
@@ -264,32 +280,32 @@ Create a single handler that serves both covers and book files:
func (mh *MediaHandler) ServeFile(c echo.Context) error { func (mh *MediaHandler) ServeFile(c echo.Context) error {
// URL format: /uploads/library-{libraryID}/{relativePath} // URL format: /uploads/library-{libraryID}/{relativePath}
path := c.Param("*") // Gets everything after /uploads/library-{id}/ path := c.Param("*") // Gets everything after /uploads/library-{id}/
// Extract library ID from path // Extract library ID from path
parts := strings.SplitN(path, "/", 2) parts := strings.SplitN(path, "/", 2)
if len(parts) < 2 { if len(parts) < 2 {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid path"}) return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid path"})
} }
libraryIDStr := strings.TrimPrefix(parts[0], "library-") libraryIDStr := strings.TrimPrefix(parts[0], "library-")
libraryUUID, err := uuid.Parse(libraryIDStr) libraryUUID, err := uuid.Parse(libraryIDStr)
if err != nil { if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library ID"}) return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library ID"})
} }
relativePath := parts[1] relativePath := parts[1]
// Resolve using service // Resolve using service
fullPath, err := mh.getFullFilePath(c.Request().Context(), pgtype.UUID{Bytes: libraryUUID, Valid: true}, relativePath) fullPath, err := mh.getFullFilePath(c.Request().Context(), pgtype.UUID{Bytes: libraryUUID, Valid: true}, relativePath)
if err != nil { if err != nil {
return c.JSON(http.StatusNotFound, map[string]string{"error": "file not found"}) return c.JSON(http.StatusNotFound, map[string]string{"error": "file not found"})
} }
// Check if file exists // Check if file exists
if _, err := os.Stat(fullPath); os.IsNotExist(err) { if _, err := os.Stat(fullPath); os.IsNotExist(err) {
return c.JSON(http.StatusNotFound, map[string]string{"error": "file not found"}) return c.JSON(http.StatusNotFound, map[string]string{"error": "file not found"})
} }
// Determine content type // Determine content type
ext := strings.ToLower(filepath.Ext(fullPath)) ext := strings.ToLower(filepath.Ext(fullPath))
contentType := "application/octet-stream" contentType := "application/octet-stream"
@@ -304,7 +320,7 @@ func (mh *MediaHandler) ServeFile(c echo.Context) error {
} else if ext == ".pdf" { } else if ext == ".pdf" {
contentType = "application/pdf" contentType = "application/pdf"
} }
c.Response().Header().Set("Content-Type", contentType) c.Response().Header().Set("Content-Type", contentType)
c.Response().Header().Set("Cache-Control", "public, max-age=86400") c.Response().Header().Set("Cache-Control", "public, max-age=86400")
return c.File(fullPath) return c.File(fullPath)
@@ -332,16 +348,17 @@ e.GET("/uploads/library-:id/*", createJWTMiddleware(cfg), cfg.MediaHandler.Serve
#### Modify GetCoverImage function #### Modify GetCoverImage function
**Current code** (around line 477-549): **Current code** (around line 477-549):
```go ```go
func (h *OPDSHandler) GetCoverImage(c echo.Context) error { func (h *OPDSHandler) GetCoverImage(c echo.Context) error {
// ... // ...
coverPath := mediaItem.CoverImagePath.String coverPath := mediaItem.CoverImagePath.String
// Check if file exists // Check if file exists
if _, err := os.Stat(coverPath); os.IsNotExist(err) { if _, err := os.Stat(coverPath); os.IsNotExist(err) {
return c.NoContent(http.StatusNoContent) return c.NoContent(http.StatusNoContent)
} }
// Open file // Open file
file, err := os.Open(coverPath) file, err := os.Open(coverPath)
// ... // ...
@@ -349,22 +366,23 @@ func (h *OPDSHandler) GetCoverImage(c echo.Context) error {
``` ```
**New code**: **New code**:
```go ```go
func (h *OPDSHandler) GetCoverImage(c echo.Context) error { func (h *OPDSHandler) GetCoverImage(c echo.Context) error {
// ... // ...
coverPath := mediaItem.CoverImagePath.String coverPath := mediaItem.CoverImagePath.String
// Resolve relative path using library service // Resolve relative path using library service
fullPath, err := h.libraryService.ResolveMediaPath(c.Request().Context(), mediaItem.LibraryID, coverPath) fullPath, err := h.libraryService.ResolveMediaPath(c.Request().Context(), mediaItem.LibraryID, coverPath)
if err != nil { if err != nil {
return c.NoContent(http.StatusNoContent) return c.NoContent(http.StatusNoContent)
} }
// Check if file exists // Check if file exists
if _, err := os.Stat(fullPath); os.IsNotExist(err) { if _, err := os.Stat(fullPath); os.IsNotExist(err) {
return c.NoContent(http.StatusNoContent) return c.NoContent(http.StatusNoContent)
} }
// Open file // Open file
file, err := os.Open(fullPath) file, err := os.Open(fullPath)
// ... // ...
@@ -417,7 +435,7 @@ func (mh *MediaHandler) ResolveCoverURL(libraryID pgtype.UUID, coverPath pgtype.
if !coverPath.Valid || coverPath.String == "" { if !coverPath.Valid || coverPath.String == "" {
return "" return ""
} }
return mh.resolveMediaURL(libraryID, coverPath.String) return mh.resolveMediaURL(libraryID, coverPath.String)
} }
@@ -426,7 +444,7 @@ func (mh *MediaHandler) ResolveFileURL(libraryID pgtype.UUID, filePath pgtype.Te
if !filePath.Valid || filePath.String == "" { if !filePath.Valid || filePath.String == "" {
return "" return ""
} }
return mh.resolveMediaURL(libraryID, filePath.String) return mh.resolveMediaURL(libraryID, filePath.String)
} }
@@ -436,13 +454,13 @@ func (mh *MediaHandler) resolveMediaURL(libraryID pgtype.UUID, relativePath stri
if strings.HasPrefix(relativePath, "/uploads/") { if strings.HasPrefix(relativePath, "/uploads/") {
return relativePath return relativePath
} }
// Already absolute path? Convert to URL format (backward compatibility) // Already absolute path? Convert to URL format (backward compatibility)
// Note: This loses library ID info, but existing data won't have it // Note: This loses library ID info, but existing data won't have it
if filepath.IsAbs(relativePath) { if filepath.IsAbs(relativePath) {
return relativePath return relativePath
} }
// Resolve relative path to URL format // Resolve relative path to URL format
libraryIDStr := libraryID.Bytes.String() libraryIDStr := libraryID.Bytes.String()
return fmt.Sprintf("/uploads/library-%s/%s", libraryIDStr, relativePath) return fmt.Sprintf("/uploads/library-%s/%s", libraryIDStr, relativePath)
@@ -494,6 +512,7 @@ cfg.CollectionHandler, err = handlers.NewCollectionHandler(cfg.Queries, cfg.Libr
**File**: `internal/handlers/collections.go` **File**: `internal/handlers/collections.go`
**Current code** (lines 193-201 in GetCollection function): **Current code** (lines 193-201 in GetCollection function):
```go ```go
bookList := make([]BookInfo, 0, len(books)) bookList := make([]BookInfo, 0, len(books))
for _, book := range books { for _, book := range books {
@@ -507,6 +526,7 @@ for _, book := range books {
``` ```
**New code**: **New code**:
```go ```go
bookList := make([]BookInfo, 0, len(books)) bookList := make([]BookInfo, 0, len(books))
for _, book := range books { for _, book := range books {
@@ -527,17 +547,17 @@ func (h *CollectionHandler) resolveCoverURL(libraryID pgtype.UUID, coverPath pgt
if !coverPath.Valid || coverPath.String == "" { if !coverPath.Valid || coverPath.String == "" {
return "" return ""
} }
// Already a full URL? Return as-is // Already a full URL? Return as-is
if strings.HasPrefix(coverPath.String, "/uploads/") { if strings.HasPrefix(coverPath.String, "/uploads/") {
return coverPath.String return coverPath.String
} }
// Already absolute path? Return as-is (backward compatibility) // Already absolute path? Return as-is (backward compatibility)
if filepath.IsAbs(coverPath.String) { if filepath.IsAbs(coverPath.String) {
return coverPath.String return coverPath.String
} }
// Resolve relative path to URL format // Resolve relative path to URL format
libraryIDStr := libraryID.Bytes.String() libraryIDStr := libraryID.Bytes.String()
return fmt.Sprintf("/uploads/library-%s/%s", libraryIDStr, coverPath.String) return fmt.Sprintf("/uploads/library-%s/%s", libraryIDStr, coverPath.String)
@@ -553,6 +573,7 @@ func (h *CollectionHandler) resolveCoverURL(libraryID pgtype.UUID, coverPath pgt
**File**: `internal/handlers/collections.go` **File**: `internal/handlers/collections.go`
**Current code** (lines 620-641 in TestRules function): **Current code** (lines 620-641 in TestRules function):
```go ```go
var matches []BookMatch var matches []BookMatch
for _, item := range mediaItems { for _, item := range mediaItems {
@@ -579,6 +600,7 @@ for _, item := range mediaItems {
``` ```
**New code**: **New code**:
```go ```go
var matches []BookMatch var matches []BookMatch
for _, item := range mediaItems { for _, item := range mediaItems {
@@ -609,6 +631,7 @@ for _, item := range mediaItems {
**Location 1 - PreviewCollection function** (lines 910-919): **Location 1 - PreviewCollection function** (lines 910-919):
**Current code**: **Current code**:
```go ```go
bookCards := make([]BookInfo, len(matchedItems)) bookCards := make([]BookInfo, len(matchedItems))
for i, item := range matchedItems { for i, item := range matchedItems {
@@ -623,6 +646,7 @@ for i, item := range matchedItems {
``` ```
**New code**: **New code**:
```go ```go
bookCards := make([]BookInfo, len(matchedItems)) bookCards := make([]BookInfo, len(matchedItems))
for i, item := range matchedItems { for i, item := range matchedItems {
@@ -639,6 +663,7 @@ for i, item := range matchedItems {
**Location 2 - mediaItemsToListMediaItemsRow helper** (line 935): **Location 2 - mediaItemsToListMediaItemsRow helper** (line 935):
**Current code**: **Current code**:
```go ```go
func mediaItemsToListMediaItemsRow(item database.MediaItems) database.ListMediaItemsRow { func mediaItemsToListMediaItemsRow(item database.MediaItems) database.ListMediaItemsRow {
return database.ListMediaItemsRow{ return database.ListMediaItemsRow{
@@ -650,6 +675,7 @@ func mediaItemsToListMediaItemsRow(item database.MediaItems) database.ListMediaI
``` ```
**New code**: **New code**:
```go ```go
// NOTE: This helper function doesn't have access to libraryID // NOTE: This helper function doesn't have access to libraryID
// Consider refactoring to pass libraryID or handle URL resolution at call site // Consider refactoring to pass libraryID or handle URL resolution at call site
@@ -663,15 +689,15 @@ func (h *CollectionHandler) resolveFileURL(libraryID pgtype.UUID, filePath pgtyp
if !filePath.Valid || filePath.String == "" { if !filePath.Valid || filePath.String == "" {
return "" return ""
} }
if strings.HasPrefix(filePath.String, "/uploads/") { if strings.HasPrefix(filePath.String, "/uploads/") {
return filePath.String return filePath.String
} }
if filepath.IsAbs(filePath.String) { if filepath.IsAbs(filePath.String) {
return filePath.String return filePath.String
} }
libraryIDStr := libraryID.Bytes.String() libraryIDStr := libraryID.Bytes.String()
return fmt.Sprintf("/uploads/library-%s/%s", libraryIDStr, filePath.String) return fmt.Sprintf("/uploads/library-%s/%s", libraryIDStr, filePath.String)
} }
@@ -691,15 +717,15 @@ func (h *Handler) resolveCoverURL(libraryID pgtype.UUID, coverPath pgtype.Text)
if !coverPath.Valid || coverPath.String == "" { if !coverPath.Valid || coverPath.String == "" {
return "" return ""
} }
if strings.HasPrefix(coverPath.String, "/uploads/") { if strings.HasPrefix(coverPath.String, "/uploads/") {
return coverPath.String return coverPath.String
} }
if filepath.IsAbs(coverPath.String) { if filepath.IsAbs(coverPath.String) {
return coverPath.String return coverPath.String
} }
libraryIDStr := libraryID.Bytes.String() libraryIDStr := libraryID.Bytes.String()
return fmt.Sprintf("/uploads/library-%s/%s", libraryIDStr, coverPath.String) return fmt.Sprintf("/uploads/library-%s/%s", libraryIDStr, coverPath.String)
} }
@@ -708,6 +734,7 @@ func (h *Handler) resolveCoverURL(libraryID pgtype.UUID, coverPath pgtype.Text)
**Location 1 - GetAllProgress function** (lines 286-289): **Location 1 - GetAllProgress function** (lines 286-289):
**Current code**: **Current code**:
```go ```go
coverPath := "" coverPath := ""
if mediaItem.CoverImagePath.Valid { if mediaItem.CoverImagePath.Valid {
@@ -716,6 +743,7 @@ if mediaItem.CoverImagePath.Valid {
``` ```
**New code** (remove the manual resolution, use helper): **New code** (remove the manual resolution, use helper):
```go ```go
coverPath := h.resolveCoverURL(mediaItem.LibraryID, mediaItem.CoverImagePath) coverPath := h.resolveCoverURL(mediaItem.LibraryID, mediaItem.CoverImagePath)
``` ```
@@ -723,6 +751,7 @@ coverPath := h.resolveCoverURL(mediaItem.LibraryID, mediaItem.CoverImagePath)
**Location 2 - GetAllProgressData function** (lines 357-360): **Location 2 - GetAllProgressData function** (lines 357-360):
**Current code**: **Current code**:
```go ```go
coverPath := "" coverPath := ""
if mediaItem.CoverImagePath.Valid { if mediaItem.CoverImagePath.Valid {
@@ -731,6 +760,7 @@ if mediaItem.CoverImagePath.Valid {
``` ```
**New code**: **New code**:
```go ```go
coverPath := h.resolveCoverURL(mediaItem.LibraryID, mediaItem.CoverImagePath) coverPath := h.resolveCoverURL(mediaItem.LibraryID, mediaItem.CoverImagePath)
``` ```
@@ -742,6 +772,7 @@ coverPath := h.resolveCoverURL(mediaItem.LibraryID, mediaItem.CoverImagePath)
**File**: `internal/handlers/media.go` **File**: `internal/handlers/media.go`
Add to imports: Add to imports:
```go ```go
"bookhoard/internal/utils" "bookhoard/internal/utils"
``` ```
@@ -749,11 +780,13 @@ Add to imports:
**GetMediaItem** - Find where it returns the response (around line 770): **GetMediaItem** - Find where it returns the response (around line 770):
**Current code**: **Current code**:
```go ```go
return c.JSON(http.StatusOK, item) return c.JSON(http.StatusOK, item)
``` ```
**New code**: **New code**:
```go ```go
return c.JSON(http.StatusOK, map[string]interface{}{ return c.JSON(http.StatusOK, map[string]interface{}{
"id": uuid.UUID(item.ID.Bytes).String(), "id": uuid.UUID(item.ID.Bytes).String(),
@@ -781,12 +814,14 @@ Wrap each item in the response with resolved URLs. The exact implementation depe
**File**: `web/src/bookshelf.ts` **File**: `web/src/bookshelf.ts`
**Current code** (line 49-50): **Current code** (line 49-50):
```typescript ```typescript
${book.cover_image_path ? ${book.cover_image_path ?
`<img src="/covers/${book.cover_image_path}" alt="${book.title}" class="w-full h-48 object-cover rounded mb-2">` : `<img src="/covers/${book.cover_image_path}" alt="${book.title}" class="w-full h-48 object-cover rounded mb-2">` :
``` ```
**New code**: **New code**:
```typescript ```typescript
${book.cover_image_path ? ${book.cover_image_path ?
`<img src="${book.cover_image_path}" alt="${book.title}" class="w-full h-48 object-cover rounded mb-2">` : `<img src="${book.cover_image_path}" alt="${book.title}" class="w-full h-48 object-cover rounded mb-2">` :
@@ -798,32 +833,34 @@ The backend now returns full URLs like `/uploads/library-{id}/path/to/cover.jpg`
### Summary of Changes for Phase 7 ### Summary of Changes for Phase 7
| File | Changes | | File | Changes |
|------|---------| | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `internal/utils/mediaurl.go` | Create with `ResolveMediaURL()` function for URL resolution (one source of truth) | | `internal/utils/mediaurl.go` | Create with `ResolveMediaURL()` function for URL resolution (one source of truth) |
| `internal/handlers/media.go` | Update GetMediaItem and ListMediaItems to use `utils.ResolveMediaURL()` for resolved URLs in responses | | `internal/handlers/media.go` | Update GetMediaItem and ListMediaItems to use `utils.ResolveMediaURL()` for resolved URLs in responses |
| `internal/handlers/collections.go` | Use `utils.ResolveMediaURL()` in GetCollection, TestRules, PreviewCollection; update lines 193-201, 620-641, 910-919 | | `internal/handlers/collections.go` | Use `utils.ResolveMediaURL()` in GetCollection, TestRules, PreviewCollection; update lines 193-201, 620-641, 910-919 |
| `internal/handlers/progress.go` | Use `utils.ResolveMediaURL()` in GetAllProgress; update lines 286-289, 357-360 | | `internal/handlers/progress.go` | Use `utils.ResolveMediaURL()` in GetAllProgress; update lines 286-289, 357-360 |
| `web/src/bookshelf.ts` | Remove `/covers/` prefix from cover image URL | | `web/src/bookshelf.ts` | Remove `/covers/` prefix from cover image URL |
--- ---
### Additional Plan Updates Needed ### Additional Plan Updates Needed
| Item | Status | | Item | Status |
|------|--------| | --------------------------------------------------- | ------------------------------------------------------------------- |
| Add `mi.library_id` to GetCollectionItems SQL query | Needs to be done before implementing Step 2 in collections.go | | Add `mi.library_id` to GetCollectionItems SQL query | Needs to be done before implementing Step 2 in collections.go |
| Create `internal/utils/mediaurl.go` | Needs to be created before implementing URL resolution | | Create `internal/utils/mediaurl.go` | Needs to be created before implementing URL resolution |
| Update callers to use utils package | Replace h.resolveCoverURL/resolveFileURL with utils.ResolveMediaURL | | Update callers to use utils package | Replace h.resolveCoverURL/resolveFileURL with utils.ResolveMediaURL |
## Phase 8: Backward Compatibility ## Phase 8: Backward Compatibility
Handle existing absolute paths in database: Handle existing absolute paths in database:
### Option A: Migration (One-time) ### Option A: Migration (One-time)
Create a script to convert existing absolute paths to relative paths using known library folder paths. Create a script to convert existing absolute paths to relative paths using known library folder paths.
### Option B: Runtime Resolution (No migration) ### Option B: Runtime Resolution (No migration)
Add backward compatibility in handlers: Add backward compatibility in handlers:
```go ```go
@@ -832,7 +869,7 @@ func (mh *MediaHandler) getFullFilePath(ctx context.Context, libraryID pgtype.UU
if filepath.IsAbs(relativePath) { if filepath.IsAbs(relativePath) {
return relativePath, nil return relativePath, nil
} }
// Otherwise resolve as relative path // Otherwise resolve as relative path
return mh.libraryService.ResolveMediaPath(ctx, libraryID, relativePath) return mh.libraryService.ResolveMediaPath(ctx, libraryID, relativePath)
} }
@@ -894,7 +931,7 @@ func TestGetRelativePath(t *testing.T) {
scanner := &MediaScanner{ scanner := &MediaScanner{
folders: []string{"/app/uploads", "/var/books"}, folders: []string{"/app/uploads", "/var/books"},
} }
tests := []struct { tests := []struct {
absolute string absolute string
expected string expected string
@@ -903,7 +940,7 @@ func TestGetRelativePath(t *testing.T) {
{"/var/books/manga/Naruto/vol1", "manga/Naruto/vol1"}, {"/var/books/manga/Naruto/vol1", "manga/Naruto/vol1"},
{"/other/path/file.pdf", "/other/path/file.pdf"}, // fallback {"/other/path/file.pdf", "/other/path/file.pdf"}, // fallback
} }
for _, tt := range tests { for _, tt := range tests {
result := scanner.getRelativePath(tt.absolute) result := scanner.getRelativePath(tt.absolute)
assert.Equal(t, tt.expected, result) assert.Equal(t, tt.expected, result)
@@ -949,28 +986,28 @@ info:
seq: 1 seq: 1
http: http:
method: GET method: GET
url: '{{base_url}}/uploads/library-{{library_id}}/path/to/cover.jpg' url: "{{base_url}}/uploads/library-{{library_id}}/path/to/cover.jpg"
auth: none auth: none
docs: |- docs: |-
## Get Cover Image ## Get Cover Image
Retrieve the cover image for a media item via authenticated static-style URL. Retrieve the cover image for a media item via authenticated static-style URL.
Uses JWT authentication (same as API endpoints). Uses JWT authentication (same as API endpoints).
**Method:** GET **Method:** GET
**Endpoint:** /uploads/library-{id}/{path} **Endpoint:** /uploads/library-{id}/{path}
**Authentication:** Bearer token required **Authentication:** Bearer token required
**Response:** Binary image data (JPEG, PNG, etc.) **Response:** Binary image data (JPEG, PNG, etc.)
**Status Codes:** **Status Codes:**
- 200: Success - returns image - 200: Success - returns image
- 401: Unauthorized (missing/invalid JWT) - 401: Unauthorized (missing/invalid JWT)
- 404: File not found - 404: File not found
**Note:** The actual path would come from the API response which provides **Note:** The actual path would come from the API response which provides
the resolved URL. This test is a template showing the URL format. the resolved URL. This test is a template showing the URL format.
@@ -989,34 +1026,34 @@ info:
seq: 1 seq: 1
http: http:
method: GET method: GET
url: '{{base_url}}/uploads/library-{{library_id}}/path/to/book.epub' url: "{{base_url}}/uploads/library-{{library_id}}/path/to/book.epub"
auth: none auth: none
docs: |- docs: |-
## Download Media Item ## Download Media Item
Download a media item file (EPUB, PDF, CBZ, etc.) via authenticated static-style URL. Download a media item file (EPUB, PDF, CBZ, etc.) via authenticated static-style URL.
Uses JWT authentication (same as API endpoints). Uses JWT authentication (same as API endpoints).
**Method:** GET **Method:** GET
**Endpoint:** /uploads/library-{id}/{path} **Endpoint:** /uploads/library-{id}/{path}
**Authentication:** Bearer token required **Authentication:** Bearer token required
**Path Resolution:** The handler resolves the relative file path stored in the **Path Resolution:** The handler resolves the relative file path stored in the
database against the library's configured folder(s) to locate the actual file. database against the library's configured folder(s) to locate the actual file.
**Backward Compatibility:** Supports both relative paths (new) and absolute **Backward Compatibility:** Supports both relative paths (new) and absolute
paths (legacy data). paths (legacy data).
**Response:** Binary file data with appropriate Content-Type header **Response:** Binary file data with appropriate Content-Type header
**Status Codes:** **Status Codes:**
- 200: Success - returns file - 200: Success - returns file
- 401: Unauthorized (missing/invalid JWT) - 401: Unauthorized (missing/invalid JWT)
- 404: File not found - 404: File not found
**Note:** The actual path would come from the API response which provides **Note:** The actual path would come from the API response which provides
the resolved URL. This test shows the URL format. the resolved URL. This test shows the URL format.
@@ -1030,7 +1067,7 @@ vars:
### File: `docs/developer/api/media-items/get_cover_image.md` ### File: `docs/developer/api/media-items/get_cover_image.md`
```markdown ````markdown
--- ---
title: Get Cover Image title: Get Cover Image
description: Retrieve the cover image for a media item description: Retrieve the cover image for a media item
@@ -1046,15 +1083,15 @@ Retrieve the cover image for a media item.
## Path Parameters ## Path Parameters
| Parameter | Type | Description | | Parameter | Type | Description |
|-----------|------|-------------| | --------- | ------ | ------------------------ |
| id | string | The media item ID (UUID) | | id | string | The media item ID (UUID) |
## Headers ## Headers
| Header | Required | Description | | Header | Required | Description |
|--------|----------|-------------| | ------------- | -------- | ------------ |
| Authorization | Yes | Bearer token | | Authorization | Yes | Bearer token |
## Response ## Response
@@ -1064,7 +1101,7 @@ Retrieve the cover image for a media item.
- **400 Bad Request**: Invalid media item ID - **400 Bad Request**: Invalid media item ID
- **404 Not Found**: - **404 Not Found**:
- Media item not found - Media item not found
- No cover image configured - No cover image configured
- Cover image file not found on disk - Cover image file not found on disk
@@ -1076,6 +1113,7 @@ curl -H "Authorization: Bearer YOUR_TOKEN" \
http://localhost:8765/api/covers/550e8400-e29b-41d4-a716-446655440000 \ http://localhost:8765/api/covers/550e8400-e29b-41d4-a716-446655440000 \
--output cover.jpg --output cover.jpg
``` ```
````
## Notes ## Notes
@@ -1088,6 +1126,7 @@ curl -H "Authorization: Bearer YOUR_TOKEN" \
### File: `docs/developer/api/media-items/download_book.md` ### File: `docs/developer/api/media-items/download_book.md`
Update existing documentation to note: Update existing documentation to note:
- File paths are stored relative to library folders - File paths are stored relative to library folders
- Handler resolves path at request time - Handler resolves path at request time
- Backward compatible with existing absolute paths - Backward compatible with existing absolute paths
@@ -1096,29 +1135,29 @@ Update existing documentation to note:
## Summary of Changes ## Summary of Changes
| Phase | File | Change | | Phase | File | Change |
|-------|------|--------| | -------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| Refactor | `internal/handlers/commonhandlers.go` | Move Handler struct from scanner.go (kitchen sink handler for progress/book matching) | | Refactor | `internal/handlers/commonhandlers.go` | Move Handler struct from scanner.go (kitchen sink handler for progress/book matching) |
| Refactor | `internal/routers/*.go` | Update NewHandler instantiation if needed | | Refactor | `internal/routers/*.go` | Update NewHandler instantiation if needed |
| 1 | `internal/services/media_scanner.go` | Add `getRelativePath()` function; use for both file_path and cover_path | | 1 | `internal/services/media_scanner.go` | Add `getRelativePath()` function; use for both file_path and cover_path |
| 2 | `internal/services/library_service.go` | Add `ResolveMediaPath()` function (one source of truth) | | 2 | `internal/services/library_service.go` | Add `ResolveMediaPath()` function (one source of truth) |
| 3 | `internal/handlers/media.go` | Add `getFullFilePath()` helper that calls service | | 3 | `internal/handlers/media.go` | Add `getFullFilePath()` helper that calls service |
| 4 | `internal/handlers/media.go` | Modify `DownloadBook` to use `getFullFilePath()` | | 4 | `internal/handlers/media.go` | Modify `DownloadBook` to use `getFullFilePath()` |
| 5 | `internal/handlers/media.go` | Add `ServeFile()` handler for authenticated static-style routes | | 5 | `internal/handlers/media.go` | Add `ServeFile()` handler for authenticated static-style routes |
| 5 | `internal/router/media.go` | Add route `GET /uploads/library-:id/*` on Echo (not protected group) | | 5 | `internal/router/media.go` | Add route `GET /uploads/library-:id/*` on Echo (not protected group) |
| 6 | `internal/handlers/opds.go` | Add `libraryService` to struct; update `GetCoverImage` to use service | | 6 | `internal/handlers/opds.go` | Add `libraryService` to struct; update `GetCoverImage` to use service |
| 7 | `internal/handlers/collections.go` | Add `libraryService` to struct/constructor; resolve cover paths to URLs in API responses | | 7 | `internal/handlers/collections.go` | Add `libraryService` to struct/constructor; resolve cover paths to URLs in API responses |
| 7 | `internal/router/*.go` | Update CollectionHandler instantiation to pass LibraryService | | 7 | `internal/router/*.go` | Update CollectionHandler instantiation to pass LibraryService |
| 7 | `internal/handlers/commonhandlers.go` (Handler struct, used by progress.go) | Resolve cover paths to URLs in API responses | | 7 | `internal/handlers/commonhandlers.go` (Handler struct, used by progress.go) | Resolve cover paths to URLs in API responses |
| 7 | `internal/handlers/media.go` | Resolve file paths to URLs in API responses | | 7 | `internal/handlers/media.go` | Resolve file paths to URLs in API responses |
| 7 | `web/src/bookshelf.ts` | Remove `/covers/` prefix (use resolved URL directly) | | 7 | `web/src/bookshelf.ts` | Remove `/covers/` prefix (use resolved URL directly) |
| 8 | Runtime resolution | Handles both absolute (old) and relative (new) paths | | 8 | Runtime resolution | Handles both absolute (old) and relative (new) paths |
| 9 | `internal/handlers/media_test.go` | Add unit tests for path resolution | | 9 | `internal/handlers/media_test.go` | Add unit tests for path resolution |
| 9 | `internal/services/media_scanner_test.go` | Add unit tests for `getRelativePath()` | | 9 | `internal/services/media_scanner_test.go` | Add unit tests for `getRelativePath()` |
| 9 | `cmd/server/tests/cover_file_serving_test.go` | Add integration tests using test_helpers | | 9 | `cmd/server/tests/cover_file_serving_test.go` | Add integration tests using test_helpers |
| 10 | `bruno/media-items/Get Cover Image.yml` | Add Bruno API test | | 10 | `bruno/media-items/Get Cover Image.yml` | Add Bruno API test |
| 10 | `bruno/media-items/EPUB Download.yml` | Update to document relative path handling | | 10 | `bruno/media-items/EPUB Download.yml` | Update to document relative path handling |
| 10 | `docs/developer/api/media-items/` | Update API documentation | | 10 | `docs/developer/api/media-items/` | Update API documentation |
--- ---
@@ -1165,9 +1204,9 @@ Users can configure any mount point in docker-compose:
services: services:
bookhoard: bookhoard:
volumes: volumes:
- ./epubs:/app/epubs # ebooks - ./epubs:/app/epubs # ebooks
- ./manga:/var/manga # manga - ./manga:/var/manga # manga
- ./comics:/media/comics # comics - ./comics:/media/comics # comics
``` ```
The system stores relative paths, so it works with any configuration. The system stores relative paths, so it works with any configuration.
+21 -4
View File
@@ -6,14 +6,15 @@ The backend implements dual-field normalization for searchability:
### Architecture ### Architecture
| Field Type | Purpose | Behavior | Example | | Field Type | Purpose | Behavior | Example |
|-----------|---------|-----------|----------| | ------------------------------------------------------- | -------------- | --------------------------------------------------- | --------------- |
| **Display Field** (`tags`, `contributors`) | Show to users | Preserves exact variant, punctuation, proper casing | `"ACME CORP."` | | **Display Field** (`tags`, `contributors`) | Show to users | Preserves exact variant, punctuation, proper casing | `"ACME CORP."` |
| **Search Field** (`tags_search`, `contributors_search`) | Search against | Lowercase, no punctuation, deduplicated | `["acme corp"]` | | **Search Field** (`tags_search`, `contributors_search`) | Search against | Lowercase, no punctuation, deduplicated | `["acme corp"]` |
### Normalization Rules ### Normalization Rules
#### Tags #### Tags
1. Trim whitespace from each tag 1. Trim whitespace from each tag
2. Titlecase each tag (preserves hyphenation: "non-fiction" → "Non-Fiction") 2. Titlecase each tag (preserves hyphenation: "non-fiction" → "Non-Fiction")
3. Case-insensitive deduplication 3. Case-insensitive deduplication
@@ -21,6 +22,7 @@ The backend implements dual-field normalization for searchability:
5. Store both display and search versions 5. Store both display and search versions
#### Contributors #### Contributors
1. Trim whitespace from each contributor 1. Trim whitespace from each contributor
2. Preserve original casing (including CAPSLOCK companies) 2. Preserve original casing (including CAPSLOCK companies)
3. Preserve original punctuation for display 3. Preserve original punctuation for display
@@ -31,6 +33,7 @@ The backend implements dual-field normalization for searchability:
### API Request/Response ### API Request/Response
**Request:** **Request:**
```json ```json
{ {
"tags": ["science-fiction", "ACME CORP.", " O'Reilly Media"], "tags": ["science-fiction", "ACME CORP.", " O'Reilly Media"],
@@ -39,6 +42,7 @@ The backend implements dual-field normalization for searchability:
``` ```
**Response (after normalization):** **Response (after normalization):**
```json ```json
{ {
"tags": ["Science-Fiction", "O'Reilly Media"], "tags": ["Science-Fiction", "O'Reilly Media"],
@@ -51,11 +55,13 @@ The backend implements dual-field normalization for searchability:
### Frontend Implementation Guidelines ### Frontend Implementation Guidelines
#### Display #### Display
- Use `tags` and `contributors` fields - Use `tags` and `contributors` fields
- These preserve exact user input (casing, punctuation) - These preserve exact user input (casing, punctuation)
- No transformation needed - No transformation needed
#### Search #### Search
- Use search inputs against `tags_search` and `contributors_search` - Use search inputs against `tags_search` and `contributors_search`
- Normalize user search input: - Normalize user search input:
- Convert to lowercase - Convert to lowercase
@@ -63,6 +69,7 @@ The backend implements dual-field normalization for searchability:
- Search using `= ANY()` operator - Search using `= ANY()` operator
#### User Typing "Science-Fiction" #### User Typing "Science-Fiction"
```typescript ```typescript
// User types exact value // User types exact value
const searchValue = "Science-Fiction"; const searchValue = "Science-Fiction";
@@ -73,6 +80,7 @@ const searchValue = "Science-Fiction";
``` ```
#### Search Query Behavior #### Search Query Behavior
```typescript ```typescript
// User searches: "ACME CORP." // User searches: "ACME CORP."
// Backend normalizes search to: "acme corp" // Backend normalizes search to: "acme corp"
@@ -85,6 +93,7 @@ const searchValue = "Science-Fiction";
When building frontend checkbox filters for contributors/tags: When building frontend checkbox filters for contributors/tags:
#### Get Unique Values for Dropdown #### Get Unique Values for Dropdown
```typescript ```typescript
// Fetch distinct normalized values for filters // Fetch distinct normalized values for filters
GET /api/contributors?distinct=true GET /api/contributors?distinct=true
@@ -94,6 +103,7 @@ Response: ["acme corp", "oreilly media", "penguin"]
``` ```
#### Filter Query #### Filter Query
```typescript ```typescript
// User selects checkbox // User selects checkbox
const filterValue = "acme corp"; const filterValue = "acme corp";
@@ -117,24 +127,28 @@ const filterValue = "acme corp";
### Common Mistakes to Avoid ### Common Mistakes to Avoid
**Searching display field directly** **Searching display field directly**
```typescript ```typescript
// WRONG - Will miss different casing/punctuation // WRONG - Will miss different casing/punctuation
WHERE 'ACME CORP.' = ANY(contributors) WHERE 'ACME CORP.' = ANY(contributors)
``` ```
**Search search field** **Search search field**
```typescript ```typescript
// CORRECT - Case-insensitive, punctuation-free // CORRECT - Case-insensitive, punctuation-free
WHERE 'acme corp' = ANY(contributors_search) WHERE 'acme corp' = ANY(contributors_search)
``` ```
**Don't normalize user search input** **Don't normalize user search input**
```typescript ```typescript
// WRONG - If user types "ACME CORP" explicitly to find exact match // WRONG - If user types "ACME CORP" explicitly to find exact match
const search = "acme corp"; // Changes user's intent const search = "acme corp"; // Changes user's intent
``` ```
**Use exact user input for search** **Use exact user input for search**
```typescript ```typescript
// CORRECT - Backend handles normalization // CORRECT - Backend handles normalization
const search = "ACME CORP"; // Backend will match "acme corp" in search field const search = "ACME CORP"; // Backend will match "acme corp" in search field
@@ -143,14 +157,17 @@ const search = "ACME CORP"; // Backend will match "acme corp" in search field
### Schema Reference ### Schema Reference
**Display Fields:** **Display Fields:**
- `tags TEXT[]` - Titlecase, original punctuation - `tags TEXT[]` - Titlecase, original punctuation
- `contributors TEXT[]` - Original casing, original punctuation - `contributors TEXT[]` - Original casing, original punctuation
**Search Fields:** **Search Fields:**
- `tags_search TEXT[]` - Lowercase, no punctuation, deduplicated - `tags_search TEXT[]` - Lowercase, no punctuation, deduplicated
- `contributors_search TEXT[]` - Lowercase, no punctuation, deduplicated - `contributors_search TEXT[]` - Lowercase, no punctuation, deduplicated
**GIN Indexes:** **GIN Indexes:**
- `idx_media_items_tags_search` - Fast search on tags_search - `idx_media_items_tags_search` - Fast search on tags_search
- `idx_media_items_contributors_search` - Fast search on contributors_search - `idx_media_items_contributors_search` - Fast search on contributors_search
- `idx_media_items_tags_gin` - Display field (if needed) - `idx_media_items_tags_gin` - Display field (if needed)
+1
View File
@@ -23,6 +23,7 @@ Welcome to the Bookhoard contributing documentation. This section contains guide
## 🤝 How to Contribute ## 🤝 How to Contribute
We welcome contributions! Please see our [Development Guide](Development.md) for information on: We welcome contributions! Please see our [Development Guide](Development.md) for information on:
- Setting up your development environment - Setting up your development environment
- Understanding the codebase - Understanding the codebase
- Making pull requests - Making pull requests
+24
View File
@@ -30,6 +30,7 @@ bookhoard/
### Backend Components ### Backend Components
**Handlers** (`internal/handlers/`): **Handlers** (`internal/handlers/`):
- `auth.go` - Authentication & user management - `auth.go` - Authentication & user management
- `library.go` - Library CRUD operations - `library.go` - Library CRUD operations
- `scanner.go` - Media scanning operations - `scanner.go` - Media scanning operations
@@ -51,6 +52,7 @@ bookhoard/
- `context.go` - Handler context utilities - `context.go` - Handler context utilities
**Middleware** (`internal/middleware/`): **Middleware** (`internal/middleware/`):
- `device_auth.go` - Device authentication - `device_auth.go` - Device authentication
- `device_rate_limiter.go` - Device-specific rate limiting - `device_rate_limiter.go` - Device-specific rate limiting
- `error_handler.go` - Global error handling - `error_handler.go` - Global error handling
@@ -62,6 +64,7 @@ bookhoard/
- `transaction.go` - Database transaction middleware - `transaction.go` - Database transaction middleware
**Services** (`internal/services/`): **Services** (`internal/services/`):
- `library_service.go` - Library operations - `library_service.go` - Library operations
- `media_scanner.go` - File scanning & metadata extraction - `media_scanner.go` - File scanning & metadata extraction
- `worker.go` - Job queue worker pool - `worker.go` - Job queue worker pool
@@ -71,6 +74,7 @@ bookhoard/
- `book_matching.go` - Book matching algorithms - `book_matching.go` - Book matching algorithms
**Sync Framework** (`internal/sync/`): **Sync Framework** (`internal/sync/`):
- `queue.go` - Sync queue processor - `queue.go` - Sync queue processor
- `progress.go` - Universal progress format - `progress.go` - Universal progress format
- `websocket.go` - Real-time sync broadcast - `websocket.go` - Real-time sync broadcast
@@ -80,6 +84,7 @@ bookhoard/
### Database Schema ### Database Schema
**Core Tables**: **Core Tables**:
- `users` - User accounts with authentication and settings - `users` - User accounts with authentication and settings
- `libraries` - Library definitions - `libraries` - Library definitions
- `library_types` - Media type definitions (ebooks, comics, manga) - `library_types` - Media type definitions (ebooks, comics, manga)
@@ -105,6 +110,7 @@ bookhoard/
- `refresh_tokens` - JWT refresh token storage - `refresh_tokens` - JWT refresh token storage
**Database Functions**: **Database Functions**:
- `normalize_isbn()` - ISBN format normalization - `normalize_isbn()` - ISBN format normalization
- `detect_format_group()` - Detect format group (reflowable, fixed_layout, comic_archive) - `detect_format_group()` - Detect format group (reflowable, fixed_layout, comic_archive)
- `convert_progress()` - Convert progress between format groups - `convert_progress()` - Convert progress between format groups
@@ -114,6 +120,7 @@ bookhoard/
### Technology Stack ### Technology Stack
**Backend**: **Backend**:
- Go 1.25+ - Go 1.25+
- Echo v4 - HTTP framework - Echo v4 - HTTP framework
- pgx v5 - PostgreSQL driver - pgx v5 - PostgreSQL driver
@@ -122,18 +129,21 @@ bookhoard/
- bcrypt - Password hashing - bcrypt - Password hashing
**Frontend**: **Frontend**:
- Templ - HTML templating with Go - Templ - HTML templating with Go
- HTMX - Dynamic interactions - HTMX - Dynamic interactions
- Tailwind CSS - Styling - Tailwind CSS - Styling
- TypeScript - Frontend logic - TypeScript - Frontend logic
**Database**: **Database**:
- PostgreSQL 15+ - PostgreSQL 15+
- 30+ tables - 30+ tables
- 50+ indexes - 50+ indexes
- JSONB for complex data - JSONB for complex data
**Testing**: **Testing**:
- Testify - Testing framework - Testify - Testing framework
- Bruno - API testing - Bruno - API testing
- 30+ integration test files - 30+ integration test files
@@ -201,6 +211,7 @@ go run cmd/server/main.go
### Development Workflow ### Development Workflow
**Backend Development**: **Backend Development**:
```bash ```bash
# Watch mode for Go (requires air or similar) # Watch mode for Go (requires air or similar)
air air
@@ -211,18 +222,21 @@ go build -o bookhoard cmd/server/main.go
``` ```
**Frontend Development**: **Frontend Development**:
```bash ```bash
cd web cd web
npm run dev # Watch mode for TypeScript/CSS npm run dev # Watch mode for TypeScript/CSS
``` ```
**Database Changes**: **Database Changes**:
1. Edit `database/schema/schema.sql` 1. Edit `database/schema/schema.sql`
2. Edit `internal/database/queries/queries.sql` 2. Edit `internal/database/queries/queries.sql`
3. Run: `cd internal/database && sqlc generate` 3. Run: `cd internal/database && sqlc generate`
4. Restart server 4. Restart server
**Template Changes**: **Template Changes**:
1. Edit `templates/*.templ` 1. Edit `templates/*.templ`
2. Run: `cd templates && templ generate` 2. Run: `cd templates && templ generate`
3. Restart server (templates auto-reload in dev mode) 3. Restart server (templates auto-reload in dev mode)
@@ -273,6 +287,7 @@ bruno run bruno/sync-kobo/
### Test Configuration ### Test Configuration
Environment variables for testing: Environment variables for testing:
- `TEST_MODE=true` - Enable test mode (disables rate limiting) - `TEST_MODE=true` - Enable test mode (disables rate limiting)
- `RATE_LIMIT_ENABLED=false` - Disable rate limiting - `RATE_LIMIT_ENABLED=false` - Disable rate limiting
- `REQUESTS_PER_MINUTE=1000` - Increase rate limit - `REQUESTS_PER_MINUTE=1000` - Increase rate limit
@@ -284,6 +299,7 @@ Environment variables for testing:
Integration tests automatically clean up libraries with "test" in the name (case-insensitive). Integration tests automatically clean up libraries with "test" in the name (case-insensitive).
**⚠️ IMPORTANT**: Do not use "test" in library names if you want to keep them! **⚠️ IMPORTANT**: Do not use "test" in library names if you want to keep them!
- Libraries containing "test" (e.g., "My Test Library", "Test Library 1") will be deleted by test cleanup - Libraries containing "test" (e.g., "My Test Library", "Test Library 1") will be deleted by test cleanup
- Use names like "Development Library", "Staging Books", or "Personal" for libraries you want to keep - Use names like "Development Library", "Staging Books", or "Personal" for libraries you want to keep
- This ensures your manual test data persists between test runs - This ensures your manual test data persists between test runs
@@ -357,11 +373,13 @@ podman-compose build --no-cache
### Environment Variables ### Environment Variables
Required for production: Required for production:
- `JWT_SECRET` - 64-byte random string (generate: `openssl rand -hex 32`) - `JWT_SECRET` - 64-byte random string (generate: `openssl rand -hex 32`)
- `DBPASS` - Strong database password (generate: `openssl rand -hex 16`) - `DBPASS` - Strong database password (generate: `openssl rand -hex 16`)
- `BASE_URL` - Public URL (e.g., https://bookhoard.example.com) - `BASE_URL` - Public URL (e.g., https://bookhoard.example.com)
Optional: Optional:
- `HTTPS_PROXY` - If behind reverse proxy - `HTTPS_PROXY` - If behind reverse proxy
**Note**: Conversion service, rate limiting, and other operational settings have defaults in `docker-compose.yml` and can be overridden via `.env` if needed. **Note**: Conversion service, rate limiting, and other operational settings have defaults in `docker-compose.yml` and can be overridden via `.env` if needed.
@@ -369,6 +387,7 @@ Optional:
### Performance Tuning ### Performance Tuning
**PostgreSQL Settings**: **PostgreSQL Settings**:
```sql ```sql
-- In postgresql.conf -- In postgresql.conf
shared_buffers = 256MB shared_buffers = 256MB
@@ -385,6 +404,7 @@ max_wal_size = 4GB
``` ```
**Go Settings**: **Go Settings**:
- GOMAXPROCS = number of CPU cores - GOMAXPROCS = number of CPU cores
- Worker pool concurrency: 3 (configurable in services/worker.go) - Worker pool concurrency: 3 (configurable in services/worker.go)
@@ -403,19 +423,23 @@ DEBUG=true
### Common Issues ### Common Issues
**Database Connection Errors**: **Database Connection Errors**:
- Check PostgreSQL is running - Check PostgreSQL is running
- Verify DATABASE_HOST and DATABASE_PORT - Verify DATABASE_HOST and DATABASE_PORT
- Check firewall settings - Check firewall settings
**Rate Limiting During Development**: **Rate Limiting During Development**:
- Enable test mode: `TEST_MODE=true RATE_LIMIT_ENABLED=false` - Enable test mode: `TEST_MODE=true RATE_LIMIT_ENABLED=false`
- Or increase limit: `REQUESTS_PER_MINUTE=1000` - Or increase limit: `REQUESTS_PER_MINUTE=1000`
**Template Not Updating**: **Template Not Updating**:
- Run `templ generate` in templates/ directory - Run `templ generate` in templates/ directory
- Restart server - Restart server
**Database Queries Not Working**: **Database Queries Not Working**:
- Run `sqlc generate` in internal/database/ - Run `sqlc generate` in internal/database/
- Check generated code in `queries.sql.go` - Check generated code in `queries.sql.go`
- Verify SQL syntax in `queries.sql` - Verify SQL syntax in `queries.sql`
+64 -10
View File
@@ -4,6 +4,7 @@
> For updated, split endpoint documentation with interactive API explorer, see [API Documentation Portal](api/api-reference.md). > For updated, split endpoint documentation with interactive API explorer, see [API Documentation Portal](api/api-reference.md).
> >
> **Use the split docs for:** > **Use the split docs for:**
>
> - Easier navigation by category > - Easier navigation by category
> - Interactive API explorer > - Interactive API explorer
> - Endpoint-specific examples > - Endpoint-specific examples
@@ -65,6 +66,7 @@ Content-Type: application/json
``` ```
**Response** (201): **Response** (201):
```json ```json
{ {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
@@ -93,6 +95,7 @@ Content-Type: application/json
``` ```
**Response** (200): **Response** (200):
```json ```json
{ {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
@@ -118,6 +121,7 @@ Content-Type: application/json
``` ```
**Response** (200): **Response** (200):
```json ```json
{ {
"token": "new-jwt-token", "token": "new-jwt-token",
@@ -144,6 +148,7 @@ Authorization: Bearer <token>
``` ```
**Response** (200): **Response** (200):
```json ```json
{ {
"id": "uuid", "id": "uuid",
@@ -219,6 +224,7 @@ Authorization: Bearer <token>
``` ```
**Response** (200): **Response** (200):
```json ```json
{ {
"libraries": [ "libraries": [
@@ -290,11 +296,13 @@ Authorization: Bearer <token>
``` ```
**Query Parameters**: **Query Parameters**:
- `library_id` (required): UUID of library - `library_id` (required): UUID of library
- `limit`: Number of items to return (max 100, default 20) - `limit`: Number of items to return (max 100, default 20)
- `offset`: Number of items to skip - `offset`: Number of items to skip
**Response** (200): **Response** (200):
```json ```json
{ {
"media_items": [ "media_items": [
@@ -337,11 +345,13 @@ Authorization: Bearer <token>
``` ```
**Query Parameters**: **Query Parameters**:
- `q` (required): Search query (minimum 2 characters) - `q` (required): Search query (minimum 2 characters)
- `limit`: Number of results (default 20) - `limit`: Number of results (default 20)
- `offset`: Number to skip - `offset`: Number to skip
**Response** (200): **Response** (200):
```json ```json
{ {
"results": [ "results": [
@@ -409,6 +419,7 @@ Authorization: Bearer <token>
``` ```
**Response** (200): **Response** (200):
```json ```json
{ {
"media_item_id": "uuid", "media_item_id": "uuid",
@@ -452,6 +463,7 @@ Content-Type: application/json
``` ```
**Response** (200): **Response** (200):
```json ```json
{ {
"sync_status": "success", "sync_status": "success",
@@ -478,6 +490,7 @@ Authorization: Bearer <token>
``` ```
**Response** (200): **Response** (200):
```json ```json
{ {
"notes": [ "notes": [
@@ -541,6 +554,7 @@ Authorization: Bearer <token>
``` ```
**Response** (200): **Response** (200):
```json ```json
{ {
"highlights": [ "highlights": [
@@ -611,6 +625,7 @@ Authorization: Bearer <token>
``` ```
**Response** (200): **Response** (200):
```json ```json
{ {
"rating": 8, "rating": 8,
@@ -668,6 +683,7 @@ Content-Type: application/json
``` ```
**Response** (201): **Response** (201):
```json ```json
{ {
"device_id": "uuid", "device_id": "uuid",
@@ -690,6 +706,7 @@ Content-Type: application/json
``` ```
**Response** (200): **Response** (200):
```json ```json
{ {
"status": "pending|approved|expired", "status": "pending|approved|expired",
@@ -711,6 +728,7 @@ Authorization: Bearer <token>
``` ```
**Response** (200): **Response** (200):
```json ```json
{ {
"devices": [ "devices": [
@@ -760,10 +778,12 @@ Authorization: Bearer <token>
``` ```
**Query Parameters**: **Query Parameters**:
- `start_date` (optional): Start date (ISO 8601 format) - `start_date` (optional): Start date (ISO 8601 format)
- `end_date` (optional): End date (ISO 8601 format) - `end_date` (optional): End date (ISO 8601 format)
**Response** (200): **Response** (200):
```json ```json
{ {
"pages_read": 1250, "pages_read": 1250,
@@ -782,6 +802,7 @@ Authorization: Bearer <token>
``` ```
**Response** (200): **Response** (200):
```json ```json
{ {
"devices": [ "devices": [
@@ -805,9 +826,11 @@ Authorization: Bearer <token>
``` ```
**Query Parameters**: **Query Parameters**:
- `limit` (optional): Number of results (default: 10) - `limit` (optional): Number of results (default: 10)
**Response** (200): **Response** (200):
```json ```json
{ {
"books": [ "books": [
@@ -842,6 +865,7 @@ Content-Type: application/json
``` ```
**Response** (200): **Response** (200):
```json ```json
{ {
"matches": [ "matches": [
@@ -875,6 +899,7 @@ Content-Type: application/json
``` ```
**Response** (200): **Response** (200):
```json ```json
{ {
"results": [ "results": [
@@ -904,6 +929,7 @@ Content-Type: application/json
``` ```
**Response** (200): **Response** (200):
```json ```json
{ {
"auto_linked": 15, "auto_linked": 15,
@@ -927,6 +953,7 @@ Authorization: Bearer <token>
``` ```
**Response** (200): **Response** (200):
```json ```json
{ {
"unlinked_book_id": "uuid-1", "unlinked_book_id": "uuid-1",
@@ -950,6 +977,7 @@ Authorization: Bearer <token>
For complete collection management documentation, see **[COLLECTIONS_API.md](COLLECTIONS_API.md)**. For complete collection management documentation, see **[COLLECTIONS_API.md](COLLECTIONS_API.md)**.
**Quick Reference**: **Quick Reference**:
- `GET /api/collections` - List all collections - `GET /api/collections` - List all collections
- `POST /api/collections` - Create new collection - `POST /api/collections` - Create new collection
- `GET /api/collections/{id}` - Get collection details - `GET /api/collections/{id}` - Get collection details
@@ -960,6 +988,7 @@ For complete collection management documentation, see **[COLLECTIONS_API.md](COL
- `GET /api/collections/{id}/books` - Get books in collection - `GET /api/collections/{id}/books` - Get books in collection
**Features**: **Features**:
- Auto-assign rules based on genre, author, series, tags, language, publisher, year - Auto-assign rules based on genre, author, series, tags, language, publisher, year
- Device shelf mappings (Kobo shelves, KOReader categories) - Device shelf mappings (Kobo shelves, KOReader categories)
- Test rules before applying - Test rules before applying
@@ -974,37 +1003,39 @@ GET /opds/devices/{deviceId}/catalog?page={page}&per_page={per_page}
``` ```
**Query Parameters**: **Query Parameters**:
- `page` (optional): Page number (default: 1) - `page` (optional): Page number (default: 1)
- `per_page` (optional): Items per page (default: 50, max: 200) - `per_page` (optional): Items per page (default: 50, max: 200)
**Response** (200 - OPDS 1.2 XML): **Response** (200 - OPDS 1.2 XML):
```xml ```xml
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" <feed xmlns="http://www.w3.org/2005/Atom"
xmlns:opds="http://opds-spec.org/2010/" xmlns:opds="http://opds-spec.org/2010/"
xmlns:dc="http://purl.org/dc/elements/1.1/"> xmlns:dc="http://purl.org/dc/elements/1.1/">
<id>urn:uuid:device-id</id> <id>urn:uuid:device-id</id>
<title>Bookhoard Library</title> <title>Bookhoard Library</title>
<updated>2026-02-01T12:00:00Z</updated> <updated>2026-02-01T12:00:00Z</updated>
<link rel="self" href="http://localhost:8765/opds/devices/kobo-id/catalog"/> <link rel="self" href="http://localhost:8765/opds/devices/kobo-id/catalog"/>
<link rel="search" href="http://localhost:8765/opds/devices/kobo-id/search"/> <link rel="search" href="http://localhost:8765/opds/devices/kobo-id/search"/>
<link rel="start" href="http://localhost:8765/opds/devices/kobo-id/nav"/> <link rel="start" href="http://localhost:8765/opds/devices/kobo-id/nav"/>
<entry> <entry>
<id>urn:uuid:bookhoard-uuid-123</id> <id>urn:uuid:bookhoard-uuid-123</id>
<dc:title>The Hobbit</dc:title> <dc:title>The Hobbit</dc:title>
<dc:creator>J.R.R. Tolkien</dc:creator> <dc:creator>J.R.R. Tolkien</dc:creator>
<updated>2026-02-01T10:00:00Z</updated> <updated>2026-02-01T10:00:00Z</updated>
<link href="http://localhost:8765/opds/devices/kobo-id/download/uuid-123" <link href="http://localhost:8765/opds/devices/kobo-id/download/uuid-123"
type="application/epub+zip" type="application/epub+zip"
rel="http://opds-spec.org/acquisition/open-access"/> rel="http://opds-spec.org/acquisition/open-access"/>
<link href="http://localhost:8765/opds/devices/kobo-id/download/uuid-123?format=kepub" <link href="http://localhost:8765/opds/devices/kobo-id/download/uuid-123?format=kepub"
type="application/vnd.kobo+xml+zip" type="application/vnd.kobo+xml+zip"
rel="alternate"/> rel="alternate"/>
<dc:identifier id="bookhoard">uuid-123</dc:identifier> <dc:identifier id="bookhoard">uuid-123</dc:identifier>
<meta property="bookhoard:sha256">abc123...</meta> <meta property="bookhoard:sha256">abc123...</meta>
</entry> </entry>
@@ -1018,9 +1049,11 @@ GET /opds/devices/{deviceId}/download/{bookId}?format={format}
``` ```
**Query Parameters**: **Query Parameters**:
- `format` (optional): Book format - `epub` (default), `kepub` - `format` (optional): Book format - `epub` (default), `kepub`
**Response** (200): **Response** (200):
- Headers: - Headers:
- `Content-Type`: `application/epub+zip` or `application/vnd.kobo+xml+zip` - `Content-Type`: `application/epub+zip` or `application/vnd.kobo+xml+zip`
- `Content-Disposition`: attachment; filename="The Hobbit.epub" - `Content-Disposition`: attachment; filename="The Hobbit.epub"
@@ -1043,6 +1076,7 @@ GET /opds/devices/{deviceId}/formats/{bookId}
``` ```
**Response** (200): **Response** (200):
```json ```json
{ {
"media_item_id": "uuid-123", "media_item_id": "uuid-123",
@@ -1109,6 +1143,7 @@ Content-Type: application/json
``` ```
**Response** (202): **Response** (202):
```json ```json
{ {
"sync_status": "accepted", "sync_status": "accepted",
@@ -1133,6 +1168,7 @@ Authorization: Bearer <device_token>
``` ```
**Response** (200): **Response** (200):
```json ```json
{ {
"uuid": "book-uuid", "uuid": "book-uuid",
@@ -1186,6 +1222,7 @@ Content-Type: application/json
``` ```
**Response** (200): **Response** (200):
```json ```json
{ {
"Status": "Success", "Status": "Success",
@@ -1202,6 +1239,7 @@ Authorization: Bearer <device_token>
``` ```
**Response** (200): **Response** (200):
```json ```json
{ {
"library_sync": [ "library_sync": [
@@ -1229,6 +1267,7 @@ Authorization: Bearer <token>
``` ```
**Response** (200): **Response** (200):
```json ```json
{ {
"book_id": "book-uuid", "book_id": "book-uuid",
@@ -1307,10 +1346,12 @@ Authorization: Bearer <token>
``` ```
**Query Parameters**: **Query Parameters**:
- `status`: "unresolved|all" (default: "unresolved") - `status`: "unresolved|all" (default: "unresolved")
- `type`: "progress|note|highlight|all" (default: "all") - `type`: "progress|note|highlight|all" (default: "all")
**Response** (200): **Response** (200):
```json ```json
{ {
"conflicts": [ "conflicts": [
@@ -1373,6 +1414,7 @@ Content-Type: application/json
``` ```
**Response** (200): **Response** (200):
```json ```json
{ {
"conflict_resolved": true, "conflict_resolved": true,
@@ -1408,6 +1450,7 @@ Authorization: Bearer <token>
``` ```
**Response** (200): **Response** (200):
```json ```json
{ {
"items": [ "items": [
@@ -1473,6 +1516,7 @@ Authorization: Bearer <token>
``` ```
**Response** (200): **Response** (200):
```json ```json
{ {
"pending": 15, "pending": 15,
@@ -1494,6 +1538,7 @@ WS /ws/sync?token=<token>
### Message Format ### Message Format
**Client → Server (Heartbeat)**: **Client → Server (Heartbeat)**:
```json ```json
{ {
"type": "ping" "type": "ping"
@@ -1501,6 +1546,7 @@ WS /ws/sync?token=<token>
``` ```
**Server → Client (Progress Update)**: **Server → Client (Progress Update)**:
```json ```json
{ {
"type": "progress_update", "type": "progress_update",
@@ -1523,6 +1569,7 @@ WS /ws/sync?token=<token>
``` ```
**Server → Client (Conflict Detected)**: **Server → Client (Conflict Detected)**:
```json ```json
{ {
"type": "conflict", "type": "conflict",
@@ -1536,6 +1583,7 @@ WS /ws/sync?token=<token>
``` ```
**Server → Client (Pong)**: **Server → Client (Pong)**:
```json ```json
{ {
"type": "pong" "type": "pong"
@@ -1571,16 +1619,19 @@ All endpoints return standardized error responses:
### Rate Limiting ### Rate Limiting
**Per-Device Limits**: **Per-Device Limits**:
- Sync requests: 60/minute - Sync requests: 60/minute
- Progress updates: 120/minute - Progress updates: 120/minute
- Metadata requests: 30/minute - Metadata requests: 30/minute
**Per-User Limits**: **Per-User Limits**:
- All requests: 300/minute - All requests: 300/minute
- Conflict resolutions: 10/minute - Conflict resolutions: 10/minute
- Device registrations: 5/hour - Device registrations: 5/hour
**Rate Limit Headers**: **Rate Limit Headers**:
``` ```
X-RateLimit-Limit: 60 X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45 X-RateLimit-Remaining: 45
@@ -1612,16 +1663,19 @@ bruno/
## Testing with Bruno OpenCollection YAML ## Testing with Bruno OpenCollection YAML
Install Bruno CLI: Install Bruno CLI:
```bash ```bash
npm install -g @usebruno/cli npm install -g @usebruno/cli
``` ```
Run all tests: Run all tests:
```bash ```bash
bruno run bruno run
``` ```
Run specific collection: Run specific collection:
```bash ```bash
bruno run bruno/devices/ bruno run bruno/devices/
``` ```
+28 -28
View File
@@ -7,17 +7,17 @@ List all users in the system (admin only).
## Query Parameters ## Query Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------- | -------- | ----------------------------------------------- |
| limit | integer | No | Maximum number of users to return (default: 50) | | limit | integer | No | Maximum number of users to return (default: 50) |
| offset | integer | No | Number of users to skip (default: 0) | | offset | integer | No | Number of users to skip (default: 0) |
| search | string | No | Search by email or username | | search | string | No | Search by email or username |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ----------------------------------- |
| Authorization | string | Yes | Bearer token (must have admin role) | | Authorization | string | Yes | Bearer token (must have admin role) |
### Example Request ### Example Request
@@ -53,26 +53,26 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
### Response Fields ### Response Fields
| Field | Type | Description | | Field | Type | Description |
|-------|------|-------------| | -------------- | ------- | ------------------------------------- |
| `id` | string | User ID (UUID) | | `id` | string | User ID (UUID) |
| `email` | string | Email address | | `email` | string | Email address |
| `username` | string | Username | | `username` | string | Username |
| `first_name` | string | First name (optional) | | `first_name` | string | First name (optional) |
| `last_name` | string | Last name (optional) | | `last_name` | string | Last name (optional) |
| `role` | string | User role (`"user"` or `"admin"`) | | `role` | string | User role (`"user"` or `"admin"`) |
| `theme` | string | Theme preference (optional) | | `theme` | string | Theme preference (optional) |
| `max_devices` | integer | Maximum number of devices allowed | | `max_devices` | integer | Maximum number of devices allowed |
| `device_count` | integer | Current number of registered devices | | `device_count` | integer | Current number of registered devices |
| `created_at` | string | Account creation timestamp (ISO 8601) | | `created_at` | string | Account creation timestamp (ISO 8601) |
| `updated_at` | string | Last update timestamp (ISO 8601) | | `updated_at` | string | Last update timestamp (ISO 8601) |
| `total` | integer | Total number of users matching query | | `total` | integer | Total number of users matching query |
| `limit` | integer | Limit applied to this request | | `limit` | integer | Limit applied to this request |
| `offset` | integer | Offset applied to this request | | `offset` | integer | Offset applied to this request |
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ----------------------------------- |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User does not have admin privileges | | 403 | User does not have admin privileges |
@@ -8,15 +8,15 @@ Update the maximum number of devices a user can register (admin only).
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | ----------- |
| id | string (UUID) | Yes | User UUID | | id | string (UUID) | Yes | User UUID |
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ----------- | ------- | -------- | --------------------------------- |
| max_devices | integer | Yes | Maximum number of devices (1-100) | | max_devices | integer | Yes | Maximum number of devices (1-100) |
### Example Request ### Example Request
@@ -41,9 +41,9 @@ Update the maximum number of devices a user can register (admin only).
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ----------------------------------------- |
| 400 | Invalid max_devices value (must be 1-100) | | 400 | Invalid max_devices value (must be 1-100) |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User does not have admin privileges | | 403 | User does not have admin privileges |
| 404 | User not found | | 404 | User not found |
+11 -11
View File
@@ -7,16 +7,16 @@ Retrieve reading statistics for a date range.
## Query Parameters ## Query Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | ---------- | ------ | -------- | ---------------------------- |
| start_date | string | No | Start date (ISO 8601 format) | | start_date | string | No | Start date (ISO 8601 format) |
| end_date | string | No | End date (ISO 8601 format) | | end_date | string | No | End date (ISO 8601 format) |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token | | Authorization | string | Yes | Bearer token |
### Example Request ### Example Request
@@ -39,7 +39,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------ |
| 400 | Invalid date format | | 400 | Invalid date format |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
+1 -1
View File
@@ -255,7 +255,7 @@ See [WebSocket API](websocket/)
## Documentation ## Documentation
- GET /docs - Documentation home - GET /docs - Documentation home
- GET /docs/* - Show documentation pages - GET /docs/\* - Show documentation pages
- GET /docs/api/search - Search API documentation - GET /docs/api/search - Search API documentation
- GET /docs/search-index.json - Search index for documentation search - GET /docs/search-index.json - Search index for documentation search
+10 -9
View File
@@ -8,10 +8,10 @@ Authenticate with email and password.
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | -------- | ------ | -------- | -------------------------------- |
| login | string | Yes | User's email address or username | | login | string | Yes | User's email address or username |
| password | string | Yes | User's password | | password | string | Yes | User's password |
### Example Request ### Example Request
@@ -42,6 +42,7 @@ Authenticate with email and password.
``` ```
**Set-Cookie Header**: **Set-Cookie Header**:
``` ```
Set-Cookie: token=eyJhbG...; Max-Age=604800; Path=/; HttpOnly Set-Cookie: token=eyJhbG...; Max-Age=604800; Path=/; HttpOnly
``` ```
@@ -50,8 +51,8 @@ Set-Cookie: token=eyJhbG...; Max-Age=604800; Path=/; HttpOnly
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------- |
| 401 | Invalid email or password | | 401 | Invalid email or password |
| 400 | Missing required fields | | 400 | Missing required fields |
| 429 | Too many login attempts | | 429 | Too many login attempts |
+7 -7
View File
@@ -8,9 +8,9 @@ Invalidate the current JWT token.
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | --------------------------------------- |
| Authorization | string | Yes | Bearer token (e.g., `Bearer eyJhbG...`) | | Authorization | string | Yes | Bearer token (e.g., `Bearer eyJhbG...`) |
### Example Request ### Example Request
@@ -25,7 +25,7 @@ No response body.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------- |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | Token already invalidated | | 403 | Token already invalidated |
@@ -42,6 +42,7 @@ When a user registers or logs in:
4. Server returns JSON response with both tokens and user profile 4. Server returns JSON response with both tokens and user profile
**Request**: **Request**:
```json ```json
POST /api/auth/login POST /api/auth/login
{ {
@@ -51,6 +52,7 @@ POST /api/auth/login
``` ```
**Response**: **Response**:
```json ```json
{ {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
@@ -67,6 +69,7 @@ POST /api/auth/login
``` ```
**Set-Cookie Header**: **Set-Cookie Header**:
``` ```
Set-Cookie: token=eyJhbG...; Max-Age=604800; Path=/; HttpOnly Set-Cookie: token=eyJhbG...; Max-Age=604800; Path=/; HttpOnly
``` ```
@@ -94,6 +97,7 @@ POST /api/auth/refresh
``` ```
**Response**: **Response**:
```json ```json
{ {
"access_token": "new-jwt-token", "access_token": "new-jwt-token",
@@ -135,6 +139,7 @@ When an API call receives a 401 Unauthorized response:
``` ```
The frontend toast.js interceptor: The frontend toast.js interceptor:
1. Clears invalid tokens from localStorage 1. Clears invalid tokens from localStorage
2. Shows an error toast notification 2. Shows an error toast notification
3. Allows user to re-authenticate 3. Allows user to re-authenticate
@@ -149,10 +154,12 @@ The frontend toast.js interceptor:
## Token Storage Recommendations ## Token Storage Recommendations
### Browser Applications ### Browser Applications
- **Backend**: Automatically manages HTTP-only cookie - **Backend**: Automatically manages HTTP-only cookie
- **Frontend**: Store tokens in localStorage for API calls - **Frontend**: Store tokens in localStorage for API calls
### Mobile Applications ### Mobile Applications
- Store access token in secure storage (Keychain/Keystore) - Store access token in secure storage (Keychain/Keystore)
- Store refresh token in secure storage - Store refresh token in secure storage
- Handle 401 responses by prompting user to re-authenticate - Handle 401 responses by prompting user to re-authenticate
@@ -160,6 +167,7 @@ The frontend toast.js interceptor:
## Constants Reference ## Constants Reference
All session durations use constants defined in: All session durations use constants defined in:
- `internal/handlers/auth.go` - SessionDuration, SessionDurationSec - `internal/handlers/auth.go` - SessionDuration, SessionDurationSec
- `internal/handlers/refresh_token.go` - SessionDurationSec (mirrored) - `internal/handlers/refresh_token.go` - SessionDurationSec (mirrored)
@@ -8,9 +8,9 @@ Obtain a new JWT access token using a refresh token.
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | -------------------------- |
| refresh_token | string | Yes | Valid refresh token (UUID) | | refresh_token | string | Yes | Valid refresh token (UUID) |
### Example Request ### Example Request
@@ -36,7 +36,7 @@ The new access token is valid for 7 days from the time of refresh.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | --------------------------------------- |
| 401 | Invalid or expired refresh token | | 401 | Invalid or expired refresh token |
| 400 | Missing refresh token or invalid format | | 400 | Missing refresh token or invalid format |
+12 -11
View File
@@ -8,13 +8,13 @@ Create a new user account.
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ---------- | ------ | -------- | --------------------------------------------------------- |
| email | string | Yes | User's email address | | email | string | Yes | User's email address |
| username | string | Yes | Desired username (3-50 chars) | | username | string | Yes | Desired username (3-50 chars) |
| password | string | Yes | Password (min 8 chars, must meet complexity requirements) | | password | string | Yes | Password (min 8 chars, must meet complexity requirements) |
| first_name | string | No | User's first name | | first_name | string | No | User's first name |
| last_name | string | No | User's last name | | last_name | string | No | User's last name |
### Example Request ### Example Request
@@ -50,6 +50,7 @@ Create a new user account.
``` ```
**Set-Cookie Header**: **Set-Cookie Header**:
``` ```
Set-Cookie: token=eyJhbG...; Max-Age=604800; Path=/; HttpOnly Set-Cookie: token=eyJhbG...; Max-Age=604800; Path=/; HttpOnly
``` ```
@@ -60,7 +61,7 @@ Set-Cookie: token=eyJhbG...; Max-Age=604800; Path=/; HttpOnly
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------------------------------------ |
| 400 | Invalid email format, weak password, or missing fields | | 400 | Invalid email format, weak password, or missing fields |
| 409 | Email or username already exists | | 409 | Email or username already exists |
@@ -8,10 +8,10 @@ Automatically link books to media items based on matching metadata.
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | --------- | ------------- | -------- | -------------------------------------------------- |
| device_id | string (UUID) | Yes | Device UUID | | device_id | string (UUID) | Yes | Device UUID |
| threshold | float | No | Match confidence threshold (0.0-1.0, default: 0.7) | | threshold | float | No | Match confidence threshold (0.0-1.0, default: 0.7) |
### Example Request ### Example Request
@@ -41,8 +41,8 @@ Automatically link books to media items based on matching metadata.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------ |
| 400 | Invalid request data | | 400 | Invalid request data |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 404 | Device not found | | 404 | Device not found |
@@ -8,9 +8,9 @@ Link multiple books to media items at once.
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ----- | ----- | -------- | -------------------------------- |
| links | array | Yes | Array of book-media link objects | | links | array | Yes | Array of book-media link objects |
Each link object contains: Each link object contains:
| Field | Type | Required | Description | | Field | Type | Required | Description |
@@ -50,8 +50,8 @@ Each link object contains:
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------------------- |
| 400 | Invalid request data | | 400 | Invalid request data |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 404 | Device, book, or media item not found | | 404 | Device, book, or media item not found |
@@ -8,17 +8,17 @@ Create a new file alias for a device.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | ----------- |
| id | string (UUID) | Yes | Device UUID | | id | string (UUID) | Yes | Device UUID |
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------------- | -------- | ---------------------------------- |
| media_item_id | string (UUID) | Yes | Media item UUID | | media_item_id | string (UUID) | Yes | Media item UUID |
| file_name | string | Yes | Name of the file | | file_name | string | Yes | Name of the file |
| file_hash | string | No | SHA256 hash of the file (optional) | | file_hash | string | No | SHA256 hash of the file (optional) |
### Example Request ### Example Request
@@ -45,9 +45,9 @@ Create a new file alias for a device.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------------ |
| 400 | Invalid request data | | 400 | Invalid request data |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 404 | Device or media item not found | | 404 | Device or media item not found |
| 409 | File alias already exists | | 409 | File alias already exists |
@@ -7,16 +7,16 @@ Delete a device file alias.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | --------------- |
| id | string (UUID) | Yes | Device UUID | | id | string (UUID) | Yes | Device UUID |
| aliasId | string (UUID) | Yes | File alias UUID | | aliasId | string (UUID) | Yes | File alias UUID |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token | | Authorization | string | Yes | Bearer token |
### Example Request ### Example Request
@@ -31,7 +31,7 @@ File alias deleted successfully.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------------ |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 404 | Device or file alias not found | | 404 | Device or file alias not found |
@@ -7,16 +7,16 @@ Get potential book matches for a given query.
## Query Parameters ## Query Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------- | -------- | ------------------------------------------------- |
| q | string | Yes | Search query (title, author, etc.) | | q | string | Yes | Search query (title, author, etc.) |
| limit | integer | No | Maximum number of matches to return (default: 10) | | limit | integer | No | Maximum number of matches to return (default: 10) |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token | | Authorization | string | Yes | Bearer token |
### Example Request ### Example Request
@@ -46,7 +46,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | -------------------------------- |
| 400 | Missing required query parameter | | 400 | Missing required query parameter |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
@@ -7,15 +7,15 @@ Get all file aliases for a specific device.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | ----------- |
| id | string (UUID) | Yes | Device UUID | | id | string (UUID) | Yes | Device UUID |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token | | Authorization | string | Yes | Bearer token |
### Example Request ### Example Request
@@ -44,7 +44,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------ |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 404 | Device not found | | 404 | Device not found |
@@ -7,22 +7,22 @@ Get suggested matches for unlinked books on a device.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | ----------- |
| id | string (UUID) | Yes | Device UUID | | id | string (UUID) | Yes | Device UUID |
## Query Parameters ## Query Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------- | -------- | --------------------------------------------------- |
| limit | integer | No | Maximum number of suggestions per book (default: 5) | | limit | integer | No | Maximum number of suggestions per book (default: 5) |
| threshold | float | No | Minimum confidence threshold (default: 0.5) | | threshold | float | No | Minimum confidence threshold (default: 0.5) |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token | | Authorization | string | Yes | Bearer token |
### Example Request ### Example Request
@@ -61,7 +61,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------ |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 404 | Device not found | | 404 | Device not found |
@@ -7,22 +7,22 @@ Get all books that haven't been linked to media items yet for a specific device.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | ----------- |
| deviceId | string (UUID) | Yes | Device UUID | | deviceId | string (UUID) | Yes | Device UUID |
## Query Parameters ## Query Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------- | -------- | ----------------------------------------------- |
| limit | integer | No | Maximum number of items to return (default: 50) | | limit | integer | No | Maximum number of items to return (default: 50) |
| offset | integer | No | Number of items to skip (default: 0) | | offset | integer | No | Number of items to skip (default: 0) |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token | | Authorization | string | Yes | Bearer token |
### Example Request ### Example Request
@@ -52,7 +52,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------ |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 404 | Device not found | | 404 | Device not found |
+15 -15
View File
@@ -8,12 +8,12 @@ Link a device book to a Bookhoard media item. Supports bulk linking.
## Manual Link Request Body ## Manual Link Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ------------------------ | ------ | -------- | ------------------------- |
| links | array | Yes | List of book links | | links | array | Yes | List of book links |
| links[].unlinked_book_id | string | Yes | Device book UUID | | links[].unlinked_book_id | string | Yes | Device book UUID |
| links[].media_item_id | string | Yes | Bookhoard media item UUID | | links[].media_item_id | string | Yes | Bookhoard media item UUID |
| links[].confidence_score | float | No | Match confidence (0-1) | | links[].confidence_score | float | No | Match confidence (0-1) |
### Example Manual Link Request ### Example Manual Link Request
@@ -31,10 +31,10 @@ Link a device book to a Bookhoard media item. Supports bulk linking.
## Auto-Link Request Body ## Auto-Link Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | -------------------- | ------- | -------- | ----------------------------------------------- |
| confidence_threshold | float | No | Minimum confidence for auto-link (default: 0.8) | | confidence_threshold | float | No | Minimum confidence for auto-link (default: 0.8) |
| limit | integer | No | Maximum books to auto-link (default: 50) | | limit | integer | No | Maximum books to auto-link (default: 50) |
### Example Auto-Link Request ### Example Auto-Link Request
@@ -81,8 +81,8 @@ Link a device book to a Bookhoard media item. Supports bulk linking.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------ |
| 400 | Invalid link data | | 400 | Invalid link data |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 404 | Media item not found | | 404 | Media item not found |
@@ -8,13 +8,13 @@ Query books to find potential matches for linking.
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ----------- | ------- | -------- | -------------------------------- |
| identifiers | array | No | List of identifiers (ISBN, UUID) | | identifiers | array | No | List of identifiers (ISBN, UUID) |
| sha256 | string | No | SHA256 hash of book file | | sha256 | string | No | SHA256 hash of book file |
| title | string | No | Book title | | title | string | No | Book title |
| author | string | No | Book author | | author | string | No | Book author |
| file_size | integer | No | File size in bytes | | file_size | integer | No | File size in bytes |
### Example Request ### Example Request
@@ -46,7 +46,7 @@ Query books to find potential matches for linking.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------ |
| 400 | Invalid query parameters | | 400 | Invalid query parameters |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
@@ -8,17 +8,17 @@ Update an existing device file alias.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | --------------- |
| id | string (UUID) | Yes | Device UUID | | id | string (UUID) | Yes | Device UUID |
| aliasId | string (UUID) | Yes | File alias UUID | | aliasId | string (UUID) | Yes | File alias UUID |
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | --------- | ------ | -------- | ------------- |
| file_name | string | No | New file name | | file_name | string | No | New file name |
| file_hash | string | No | New file hash | | file_hash | string | No | New file hash |
### Example Request ### Example Request
@@ -44,8 +44,8 @@ Update an existing device file alias.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------------ |
| 400 | Invalid request data | | 400 | Invalid request data |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 404 | Device or file alias not found | | 404 | Device or file alias not found |
@@ -8,44 +8,44 @@ Add an automatic book assignment rule to a collection.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | --------------- |
| id | string (UUID) | Yes | Collection UUID | | id | string (UUID) | Yes | Collection UUID |
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | -------- | ------------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
| field | string | Yes | Field to match on (genre, author, series, language, publisher, copyright_year, tags) | | field | string | Yes | Field to match on (genre, author, series, language, publisher, copyright_year, tags) |
| operator | string | Yes | Comparison operator (equals, not_equals, contains, not_contains, starts_with, ends_with, greater_than, less_than) | | operator | string | Yes | Comparison operator (equals, not_equals, contains, not_contains, starts_with, ends_with, greater_than, less_than) |
| value | string/number | Yes | Value to compare against | | value | string/number | Yes | Value to compare against |
| priority | integer | No | Rule priority (1 = highest, default: 1) | | priority | integer | No | Rule priority (1 = highest, default: 1) |
| enabled | boolean | No | Whether rule is active (default: true) | | enabled | boolean | No | Whether rule is active (default: true) |
### Supported Fields ### Supported Fields
| Field | Type | Description | | Field | Type | Description |
|-------|------|-------------| | -------------- | ------ | ------------------------------------- |
| genre | string | Book genre | | genre | string | Book genre |
| author | string | Book author | | author | string | Book author |
| series | string | Book series name | | series | string | Book series name |
| language | string | Book language | | language | string | Book language |
| publisher | string | Publisher name | | publisher | string | Publisher name |
| copyright_year | number | Publication year (numeric comparison) | | copyright_year | number | Publication year (numeric comparison) |
| tags | string | Book tags | | tags | string | Book tags |
### Supported Operators ### Supported Operators
| Operator | Type | Description | | Operator | Type | Description |
|----------|------|-------------| | ------------ | ------ | ------------------------------------- |
| equals | all | Exact match | | equals | all | Exact match |
| not_equals | all | Not equal | | not_equals | all | Not equal |
| contains | string | Contains substring (case-insensitive) | | contains | string | Contains substring (case-insensitive) |
| not_contains | string | Does not contain | | not_contains | string | Does not contain |
| starts_with | string | Starts with (case-insensitive) | | starts_with | string | Starts with (case-insensitive) |
| ends_with | string | Ends with (case-insensitive) | | ends_with | string | Ends with (case-insensitive) |
| greater_than | number | Greater than | | greater_than | number | Greater than |
| less_than | number | Less than | | less_than | number | Less than |
### Example Request ### Example Request
@@ -75,11 +75,11 @@ Add an automatic book assignment rule to a collection.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ----------------------------------- |
| 400 | Invalid request (validation failed) | | 400 | Invalid request (validation failed) |
| 401 | Authentication required | | 401 | Authentication required |
| 404 | Collection not found | | 404 | Collection not found |
| 500 | Internal server error | | 500 | Internal server error |
## Try It Out ## Try It Out
+12 -12
View File
@@ -8,15 +8,15 @@ Add multiple books to a collection at once.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | --------------- |
| id | string (UUID) | Yes | Collection UUID | | id | string (UUID) | Yes | Collection UUID |
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | -------- | ------------- | -------- | ------------------------ |
| book_ids | array of UUID | Yes | Array of book IDs to add | | book_ids | array of UUID | Yes | Array of book IDs to add |
### Example Request ### Example Request
@@ -42,11 +42,11 @@ Books added to collection successfully. No response body.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ----------------------------------- |
| 400 | Invalid request (validation failed) | | 400 | Invalid request (validation failed) |
| 401 | Authentication required | | 401 | Authentication required |
| 404 | Collection or book(s) not found | | 404 | Collection or book(s) not found |
| 500 | Internal server error | | 500 | Internal server error |
## Try It Out ## Try It Out
@@ -8,24 +8,24 @@ Create a new collection.
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ----------------- | ------ | -------- | -------------------------------- |
| name | string | Yes | Collection name (max 255 chars) | | name | string | Yes | Collection name (max 255 chars) |
| description | string | No | Collection description | | description | string | No | Collection description |
| color | string | No | Hex color code (e.g., "#FF5733") | | color | string | No | Hex color code (e.g., "#FF5733") |
| icon | string | No | Emoji icon (e.g., "🚀", "📖") | | icon | string | No | Emoji icon (e.g., "🚀", "📖") |
| auto_assign_rules | array | No | Array of rule objects | | auto_assign_rules | array | No | Array of rule objects |
| view_settings | object | No | Per-device display preferences | | view_settings | object | No | Per-device display preferences |
### Auto-Assign Rule Object ### Auto-Assign Rule Object
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | -------- | ------------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
| field | string | Yes | Field to match on (genre, author, series, language, publisher, copyright_year, tags) | | field | string | Yes | Field to match on (genre, author, series, language, publisher, copyright_year, tags) |
| operator | string | Yes | Comparison operator (equals, not_equals, contains, not_contains, starts_with, ends_with, greater_than, less_than) | | operator | string | Yes | Comparison operator (equals, not_equals, contains, not_contains, starts_with, ends_with, greater_than, less_than) |
| value | string/number | Yes | Value to compare against | | value | string/number | Yes | Value to compare against |
| priority | integer | No | Rule priority (1 = highest, default: 1) | | priority | integer | No | Rule priority (1 = highest, default: 1) |
| enabled | boolean | No | Whether rule is active (default: true) | | enabled | boolean | No | Whether rule is active (default: true) |
### Example Request ### Example Request
@@ -87,10 +87,10 @@ Create a new collection.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ----------------------------------- |
| 400 | Invalid request (validation failed) | | 400 | Invalid request (validation failed) |
| 401 | Authentication required | | 401 | Authentication required |
| 500 | Internal server error | | 500 | Internal server error |
## Try It Out ## Try It Out
@@ -8,26 +8,26 @@ Map a collection to a device shelf for syncing.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | ----------- |
| deviceId | string (UUID) | Yes | Device UUID | | deviceId | string (UUID) | Yes | Device UUID |
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ----------------- | ------------- | -------- | ----------------------------------------- |
| collection_id | string (UUID) | Yes | Collection UUID to map | | collection_id | string (UUID) | Yes | Collection UUID to map |
| device_shelf_name | string | Yes | Name of the shelf on the device | | device_shelf_name | string | Yes | Name of the shelf on the device |
| sync_direction | string | No | Sync direction (default: "bidirectional") | | sync_direction | string | No | Sync direction (default: "bidirectional") |
### Sync Directions ### Sync Directions
| Direction | Description | | Direction | Description |
|-----------|-------------| | --------------- | ------------------------------------------- |
| bidirectional | Sync both ways between Bookhoard and device | | bidirectional | Sync both ways between Bookhoard and device |
| book_to_hoard | Bookhoard → Device only | | book_to_hoard | Bookhoard → Device only |
| device_to_hoard | Device → Bookhoard only | | device_to_hoard | Device → Bookhoard only |
| none | No sync (mapping only for reference) | | none | No sync (mapping only for reference) |
### Example Request ### Example Request
@@ -58,12 +58,12 @@ Collections can be synced to device-specific shelves (Kobo, KOReader). This allo
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ----------------------------------- |
| 400 | Invalid request (validation failed) | | 400 | Invalid request (validation failed) |
| 401 | Authentication required | | 401 | Authentication required |
| 404 | Device or collection not found | | 404 | Device or collection not found |
| 409 | Mapping already exists | | 409 | Mapping already exists |
| 500 | Internal server error | | 500 | Internal server error |
## Try It Out ## Try It Out
@@ -8,9 +8,9 @@ Delete a collection. Books are NOT deleted.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | --------------- |
| id | string (UUID) | Yes | Collection UUID | | id | string (UUID) | Yes | Collection UUID |
## Response (204 No Content) ## Response (204 No Content)
@@ -18,10 +18,10 @@ Collection deleted successfully. No response body.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ----------------------- |
| 401 | Authentication required | | 401 | Authentication required |
| 404 | Collection not found | | 404 | Collection not found |
| 500 | Internal server error | | 500 | Internal server error |
## Try It Out ## Try It Out
@@ -8,10 +8,10 @@ Remove a collection-to-shelf mapping for a device.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | ------------ | ------------- | -------- | --------------- |
| deviceId | string (UUID) | Yes | Device UUID | | deviceId | string (UUID) | Yes | Device UUID |
| collectionId | string (UUID) | Yes | Collection UUID | | collectionId | string (UUID) | Yes | Collection UUID |
## Response (204 No Content) ## Response (204 No Content)
@@ -25,10 +25,10 @@ Shelf mapping deleted successfully. No response body.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------------ |
| 401 | Authentication required | | 401 | Authentication required |
| 404 | Device or collection not found | | 404 | Device or collection not found |
| 500 | Internal server error | | 500 | Internal server error |
## Try It Out ## Try It Out
@@ -8,17 +8,17 @@ Get single collection with all books.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | --------------- |
| id | string (UUID) | Yes | Collection UUID | | id | string (UUID) | Yes | Collection UUID |
## Query Parameters ## Query Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | ------------- | ------- | -------- | ----------------------------------------- |
| include_books | boolean | No | Include books in response (default: true) | | include_books | boolean | No | Include books in response (default: true) |
| limit | integer | No | Number of books to return (default: 50) | | limit | integer | No | Number of books to return (default: 50) |
| offset | integer | No | Number of books to skip (default: 0) | | offset | integer | No | Number of books to skip (default: 0) |
## Response (200 OK) ## Response (200 OK)
@@ -45,10 +45,10 @@ Get single collection with all books.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ----------------------- |
| 401 | Authentication required | | 401 | Authentication required |
| 404 | Collection not found | | 404 | Collection not found |
| 500 | Internal server error | | 500 | Internal server error |
## Try It Out ## Try It Out
@@ -8,10 +8,10 @@ Get all collections for the authenticated user.
## Query Parameters ## Query Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------- | -------- | --------------------------------------------- |
| limit | integer | No | Number of collections to return (default: 50) | | limit | integer | No | Number of collections to return (default: 50) |
| offset | integer | No | Number of collections to skip (default: 0) | | offset | integer | No | Number of collections to skip (default: 0) |
## Response (200 OK) ## Response (200 OK)
@@ -49,9 +49,9 @@ Get all collections for the authenticated user.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ----------------------- |
| 401 | Authentication required | | 401 | Authentication required |
| 500 | Internal server error | | 500 | Internal server error |
## Try It Out ## Try It Out
@@ -8,10 +8,10 @@ Remove an automatic book assignment rule from a collection.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | ------------ | ------------- | -------- | --------------- |
| collectionId | string (UUID) | Yes | Collection UUID | | collectionId | string (UUID) | Yes | Collection UUID |
| ruleId | string (UUID) | Yes | Rule UUID | | ruleId | string (UUID) | Yes | Rule UUID |
## Response (204 No Content) ## Response (204 No Content)
@@ -19,10 +19,10 @@ Rule deleted successfully. No response body.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ---------------------------- |
| 401 | Authentication required | | 401 | Authentication required |
| 404 | Collection or rule not found | | 404 | Collection or rule not found |
| 500 | Internal server error | | 500 | Internal server error |
## Try It Out ## Try It Out
+13 -13
View File
@@ -8,17 +8,17 @@ Test which books would match given rules without saving.
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ----- | ----- | -------- | ----------------------------- |
| rules | array | Yes | Array of rule objects to test | | rules | array | Yes | Array of rule objects to test |
### Rule Object ### Rule Object
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | -------- | ------------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
| field | string | Yes | Field to match on (genre, author, series, language, publisher, copyright_year, tags) | | field | string | Yes | Field to match on (genre, author, series, language, publisher, copyright_year, tags) |
| operator | string | Yes | Comparison operator (equals, not_equals, contains, not_contains, starts_with, ends_with, greater_than, less_than) | | operator | string | Yes | Comparison operator (equals, not_equals, contains, not_contains, starts_with, ends_with, greater_than, less_than) |
| value | string/number | Yes | Value to compare against | | value | string/number | Yes | Value to compare against |
### Example Request ### Example Request
@@ -62,10 +62,10 @@ Test rules before creating a collection to verify correct book matching. This en
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ----------------------------------- |
| 400 | Invalid request (validation failed) | | 400 | Invalid request (validation failed) |
| 401 | Authentication required | | 401 | Authentication required |
| 500 | Internal server error | | 500 | Internal server error |
## Try It Out ## Try It Out
@@ -8,22 +8,22 @@ Update collection details.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | --------------- |
| id | string (UUID) | Yes | Collection UUID | | id | string (UUID) | Yes | Collection UUID |
## Request Body ## Request Body
All fields are optional. Include only fields you want to update. All fields are optional. Include only fields you want to update.
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ----------------- | ------ | -------- | ----------------------------------------------- |
| name | string | No | Collection name (max 255 chars) | | name | string | No | Collection name (max 255 chars) |
| description | string | No | Collection description | | description | string | No | Collection description |
| color | string | No | Hex color code (e.g., "#FF5733") | | color | string | No | Hex color code (e.g., "#FF5733") |
| icon | string | No | Emoji icon (e.g., "🚀", "📖") | | icon | string | No | Emoji icon (e.g., "🚀", "📖") |
| auto_assign_rules | array | No | Array of rule objects (replaces existing rules) | | auto_assign_rules | array | No | Array of rule objects (replaces existing rules) |
| view_settings | object | No | Per-device display preferences | | view_settings | object | No | Per-device display preferences |
### Example Request ### Example Request
@@ -53,11 +53,11 @@ All fields are optional. Include only fields you want to update.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ----------------------------------- |
| 400 | Invalid request (validation failed) | | 400 | Invalid request (validation failed) |
| 401 | Authentication required | | 401 | Authentication required |
| 404 | Collection not found | | 404 | Collection not found |
| 500 | Internal server error | | 500 | Internal server error |
## Try It Out ## Try It Out
@@ -8,9 +8,9 @@ Dismiss multiple conflicts at once.
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ------------ | ------------- | -------- | ---------------------------------- |
| conflict_ids | array of UUID | Yes | Array of conflict UUIDs to dismiss | | conflict_ids | array of UUID | Yes | Array of conflict UUIDs to dismiss |
### Example Request ### Example Request
@@ -34,7 +34,7 @@ Dismiss multiple conflicts at once.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------ |
| 400 | Invalid request body | | 400 | Invalid request body |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
@@ -8,10 +8,10 @@ Resolve multiple conflicts at once using a specified strategy.
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ------------ | ------------- | -------- | -------------------------------------------------------------- |
| conflict_ids | array of UUID | Yes | Array of conflict UUIDs to resolve | | conflict_ids | array of UUID | Yes | Array of conflict UUIDs to resolve |
| resolution | string | Yes | Resolution strategy: "device", "server", or "highest_progress" | | resolution | string | Yes | Resolution strategy: "device", "server", or "highest_progress" |
### Example Request ### Example Request
@@ -37,8 +37,8 @@ Resolve multiple conflicts at once using a specified strategy.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | --------------------------- |
| 400 | Invalid request body | | 400 | Invalid request body |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 400 | Invalid resolution strategy | | 400 | Invalid resolution strategy |
+10 -10
View File
@@ -7,15 +7,15 @@ Delete a specific conflict record.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | ------------- |
| id | string (UUID) | Yes | Conflict UUID | | id | string (UUID) | Yes | Conflict UUID |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token | | Authorization | string | Yes | Bearer token |
### Example Request ### Example Request
@@ -30,7 +30,7 @@ Conflict deleted successfully.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------ |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 404 | Conflict not found | | 404 | Conflict not found |
@@ -7,9 +7,9 @@ Dismiss all resolved conflicts.
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token | | Authorization | string | Yes | Bearer token |
### Example Request ### Example Request
@@ -29,6 +29,6 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------ |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
+10 -10
View File
@@ -7,15 +7,15 @@ Get detailed information about a specific conflict.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | ------------- |
| id | string (UUID) | Yes | Conflict UUID | | id | string (UUID) | Yes | Conflict UUID |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token | | Authorization | string | Yes | Bearer token |
### Example Request ### Example Request
@@ -69,7 +69,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------ |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 404 | Conflict not found | | 404 | Conflict not found |
+12 -12
View File
@@ -7,18 +7,18 @@ List all sync conflicts for the current user.
## Query Parameters ## Query Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | ------------- | ------------- | -------- | --------------------------------------------------- |
| status | string | No | Filter by status (active, resolved, dismissed) | | status | string | No | Filter by status (active, resolved, dismissed) |
| media_item_id | string (UUID) | No | Filter by media item | | media_item_id | string (UUID) | No | Filter by media item |
| limit | integer | No | Maximum number of conflicts to return (default: 50) | | limit | integer | No | Maximum number of conflicts to return (default: 50) |
| offset | integer | No | Number of conflicts to skip (default: 0) | | offset | integer | No | Number of conflicts to skip (default: 0) |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token | | Authorization | string | Yes | Bearer token |
### Example Request ### Example Request
@@ -60,6 +60,6 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------ |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
@@ -8,15 +8,15 @@ Resolve a specific conflict by choosing which version to keep.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | ------------- |
| id | string (UUID) | Yes | Conflict UUID | | id | string (UUID) | Yes | Conflict UUID |
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ---------- | ------ | -------- | -------------------------------------------------------------- |
| resolution | string | Yes | Resolution strategy: "device", "server", or "highest_progress" | | resolution | string | Yes | Resolution strategy: "device", "server", or "highest_progress" |
### Example Request ### Example Request
@@ -38,9 +38,9 @@ Resolve a specific conflict by choosing which version to keep.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | --------------------------- |
| 400 | Invalid resolution strategy | | 400 | Invalid resolution strategy |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 404 | Conflict not found | | 404 | Conflict not found |
| 400 | Conflict already resolved | | 400 | Conflict already resolved |
+23 -15
View File
@@ -9,6 +9,7 @@ Evaluates filter rules and returns matching items without saving the collection.
**Endpoint:** `POST /api/collections/preview` **Endpoint:** `POST /api/collections/preview`
**Request Body:** **Request Body:**
```json ```json
{ {
"library_id": "uuid", "library_id": "uuid",
@@ -28,23 +29,24 @@ Evaluates filter rules and returns matching items without saving the collection.
**Available Filter Fields:** **Available Filter Fields:**
| Field | Type | Operators | | Field | Type | Operators |
|-------|------|-----------| | ------------ | ------ | ------------------------------------------------------------------------ |
| `title` | text | contains, equals, starts_with, ends_with, regex | | `title` | text | contains, equals, starts_with, ends_with, regex |
| `author` | text | contains, equals | | `author` | text | contains, equals |
| `genre` | select | equals, not_equals, in, not_in | | `genre` | select | equals, not_equals, in, not_in |
| `series` | text | is_set, is_not_set, equals, contains | | `series` | text | is_set, is_not_set, equals, contains |
| `progress` | number | equals, not_equals, greater_than, less_than, between, is_set, is_not_set | | `progress` | number | equals, not_equals, greater_than, less_than, between, is_set, is_not_set |
| `rating` | number | equals, not_equals, greater_than, less_than, is_set, is_not_set | | `rating` | number | equals, not_equals, greater_than, less_than, is_set, is_not_set |
| `date_added` | date | equals, not_equals, before, after, between, last_x_days | | `date_added` | date | equals, not_equals, before, after, between, last_x_days |
| `last_read` | date | equals, before, after, between, last_x_days, is_set, is_not_set | | `last_read` | date | equals, before, after, between, last_x_days, is_set, is_not_set |
| `publisher` | text | contains, equals | | `publisher` | text | contains, equals |
| `language` | select | equals, not_equals, in | | `language` | select | equals, not_equals, in |
| `format` | select | equals, in | | `format` | select | equals, in |
| `tags` | text | contains, not_contains, equals | | `tags` | text | contains, not_contains, equals |
| `narrators` | text | contains, equals, is_set, is_not_set | | `narrators` | text | contains, equals, is_set, is_not_set |
**Response:** **Response:**
```json ```json
{ {
"items": [ "items": [
@@ -65,6 +67,7 @@ Creates a new custom collection with filter rules and/or manual book selection.
**Endpoint:** `POST /api/collections` **Endpoint:** `POST /api/collections`
**Request Body:** **Request Body:**
```json ```json
{ {
"name": "My Custom Section", "name": "My Custom Section",
@@ -89,6 +92,7 @@ Creates a new custom collection with filter rules and/or manual book selection.
**TypeScript:** `web/src/custom-section-builder.ts` **TypeScript:** `web/src/custom-section-builder.ts`
Key features: Key features:
- 14 filter fields with various operators - 14 filter fields with various operators
- Live preview functionality - Live preview functionality
- Search + multi-select for manual book addition - Search + multi-select for manual book addition
@@ -97,6 +101,7 @@ Key features:
## Example Use Cases ## Example Use Cases
### Sci-Fi Favorites ### Sci-Fi Favorites
```json ```json
{ {
"rules": [ "rules": [
@@ -110,6 +115,7 @@ Key features:
``` ```
### High Rated Books ### High Rated Books
```json ```json
{ {
"rules": [ "rules": [
@@ -123,6 +129,7 @@ Key features:
``` ```
### Long Books (Manual Selection) ### Long Books (Manual Selection)
```json ```json
{ {
"manual_book_ids": ["uuid1", "uuid2", "uuid3"] "manual_book_ids": ["uuid1", "uuid2", "uuid3"]
@@ -130,6 +137,7 @@ Key features:
``` ```
### Recently Finished Audiobooks ### Recently Finished Audiobooks
```json ```json
{ {
"rules": [ "rules": [
+20 -18
View File
@@ -10,26 +10,27 @@ Retrieve all dashboard sections for a specific library, including system collect
### Query Parameters ### Query Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|--------|----------|-----------------------------------------------| | ---------- | ------ | -------- | ----------------------------------------- |
| library_id| string | Yes | Library UUID to fetch sections for | | library_id | string | Yes | Library UUID to fetch sections for |
| limit | number | No | Items per section (default: 20, max: 100) | | limit | number | No | Items per section (default: 20, max: 100) |
### Response ### Response
Returns array of sections in user's customized order (respects `collection_order` and `hidden_collections` preferences). Returns array of sections in user's customized order (respects `collection_order` and `hidden_collections` preferences).
**Section Types**: **Section Types**:
- `is_system: true`: System collections (4 pre-seeded defaults) - `is_system: true`: System collections (4 pre-seeded defaults)
- `is_system: false`: User-created collections with `show_on_dashboard: true` - `is_system: false`: User-created collections with `show_on_dashboard: true`
**System Collections**: **System Collections**:
| ID | Title | Icon | Description | | ID | Title | Icon | Description |
|-----------------|------------------|------|--------------------------------------------------| |-----------------|------------------|------|--------------------------------------------------|
| continue-reading| Continue Reading | 📖 | Books with progress > 0% and < 100% | | continue-reading| Continue Reading | 📖 | Books with progress > 0% and < 100% |
| recently-added | Recently Added | 🆕 | Newest items in library | | recently-added | Recently Added | 🆕 | Newest items in library |
| recently-read | Recently Read | ✅ | Books with progress = 100% | | recently-read | Recently Read | ✅ | Books with progress = 100% |
| not-started | Not Started | 📕 | Books with no reading progress | | not-started | Not Started | 📕 | Books with no reading progress |
### Example Response ### Example Response
@@ -115,6 +116,7 @@ Reset a system collection to its default state (removes user customizations).
``` ```
Valid `collection_name` values: Valid `collection_name` values:
- `continue-reading` - `continue-reading`
- `recently-added` - `recently-added`
- `recently-read` - `recently-read`
@@ -130,12 +132,12 @@ Valid `collection_name` values:
### Error Responses ### Error Responses
| Status | Description | | Status | Description |
|--------|--------------------------------| | ------ | ---------------------------- |
| 400 | Missing library_id | | 400 | Missing library_id |
| 400 | Invalid library_id | | 400 | Invalid library_id |
| 400 | Invalid collection_name | | 400 | Invalid collection_name |
| 401 | Unauthorized | | 401 | Unauthorized |
| 500 | Failed to load sections | | 500 | Failed to load sections |
| 500 | Failed to save preferences | | 500 | Failed to save preferences |
| 500 | Failed to restore collection | | 500 | Failed to restore collection |
+12 -12
View File
@@ -8,15 +8,15 @@ Add a media item to a device's shelf (Kobo reading shelf).
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | ----------- |
| id | string (UUID) | Yes | Device UUID | | id | string (UUID) | Yes | Device UUID |
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------------- | -------- | ------------------------------- |
| media_item_id | string (UUID) | Yes | Media item UUID to add to shelf | | media_item_id | string (UUID) | Yes | Media item UUID to add to shelf |
### Example Request ### Example Request
@@ -38,9 +38,9 @@ Add a media item to a device's shelf (Kobo reading shelf).
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------------ |
| 400 | Invalid request data | | 400 | Invalid request data |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 404 | Device or media item not found | | 404 | Device or media item not found |
| 409 | Item already on shelf | | 409 | Item already on shelf |
@@ -7,15 +7,15 @@ Approve a pending device registration request.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------------- | ------------- | -------- | ------------------------- |
| registration_id | string (UUID) | Yes | Registration request UUID | | registration_id | string (UUID) | Yes | Registration request UUID |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ----------------------------------- |
| Authorization | string | Yes | Bearer token (must have admin role) | | Authorization | string | Yes | Bearer token (must have admin role) |
### Example Request ### Example Request
@@ -30,7 +30,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
{ {
"message": "device approved successfully", "message": "device approved successfully",
"device_name": "string", "device_name": "string",
"device_type": "string", "device_type": "string",
"registration_id": "uuid", "registration_id": "uuid",
"approved": true "approved": true
} }
@@ -38,9 +38,9 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ----------------------------------- |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User does not have admin privileges | | 403 | User does not have admin privileges |
| 404 | Registration not found | | 404 | Registration not found |
| 400 | Registration already processed | | 400 | Registration already processed |
+10 -10
View File
@@ -7,15 +7,15 @@ Remove all items from a device's shelf.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | ----------- |
| id | string (UUID) | Yes | Device UUID | | id | string (UUID) | Yes | Device UUID |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token | | Authorization | string | Yes | Bearer token |
### Example Request ### Example Request
@@ -30,7 +30,7 @@ Shelf cleared successfully.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------ |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 404 | Device not found | | 404 | Device not found |
+11 -11
View File
@@ -7,15 +7,15 @@ Delete a device and revoke its access.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | ----------- |
| id | string (UUID) | Yes | Device UUID | | id | string (UUID) | Yes | Device UUID |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token | | Authorization | string | Yes | Bearer token |
### Example Request ### Example Request
@@ -30,8 +30,8 @@ Device deleted successfully.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------------ |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | Device does not belong to user | | 403 | Device does not belong to user |
| 404 | Device not found | | 404 | Device not found |
+7 -7
View File
@@ -8,9 +8,9 @@ Check device registration status or get device details.
## Request Body (Status Check) ## Request Body (Status Check)
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | --------------- | ------ | -------- | ----------------- |
| registration_id | string | Yes | Registration UUID | | registration_id | string | Yes | Registration UUID |
### Example Request (Status Check) ### Example Request (Status Check)
@@ -52,7 +52,7 @@ Check device registration status or get device details.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | --------------------------------------------- |
| 401 | Invalid or expired token (for device details) | | 401 | Invalid or expired token (for device details) |
| 404 | Device or registration not found | | 404 | Device or registration not found |
+10 -10
View File
@@ -7,15 +7,15 @@ Get all items on a device's shelf.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | ----------- |
| id | string (UUID) | Yes | Device UUID | | id | string (UUID) | Yes | Device UUID |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token | | Authorization | string | Yes | Bearer token |
### Example Request ### Example Request
@@ -44,7 +44,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------ |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 404 | Device not found | | 404 | Device not found |
+6 -6
View File
@@ -7,9 +7,9 @@ Retrieve all devices registered to the current user.
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token | | Authorization | string | Yes | Bearer token |
### Example Request ### Example Request
@@ -39,6 +39,6 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------ |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
@@ -7,9 +7,9 @@ List all pending device registration requests.
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ----------------------------------- |
| Authorization | string | Yes | Bearer token (must have admin role) | | Authorization | string | Yes | Bearer token (must have admin role) |
### Example Request ### Example Request
@@ -37,7 +37,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ----------------------------------- |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User does not have admin privileges | | 403 | User does not have admin privileges |
@@ -8,11 +8,11 @@ Register a new device for sync.
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ----------------- | ------ | -------- | ---------------------------------------- |
| device_name | string | Yes | Device name | | device_name | string | Yes | Device name |
| device_type | string | Yes | Device type: kobo, koreader, web, mobile | | device_type | string | Yes | Device type: kobo, koreader, web, mobile |
| device_identifier | string | Yes | Hardware-specific ID | | device_identifier | string | Yes | Hardware-specific ID |
### Example Request ### Example Request
@@ -38,7 +38,7 @@ Register a new device for sync.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------- |
| 400 | Invalid device data | | 400 | Invalid device data |
| 409 | Device already registered | | 409 | Device already registered |
@@ -7,15 +7,15 @@ Reject a pending device registration request.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------------- | ------------- | -------- | ------------------------- |
| registration_id | string (UUID) | Yes | Registration request UUID | | registration_id | string (UUID) | Yes | Registration request UUID |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ----------------------------------- |
| Authorization | string | Yes | Bearer token (must have admin role) | | Authorization | string | Yes | Bearer token (must have admin role) |
### Example Request ### Example Request
@@ -34,9 +34,9 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ----------------------------------- |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User does not have admin privileges | | 403 | User does not have admin privileges |
| 404 | Registration not found | | 404 | Registration not found |
| 400 | Registration already processed | | 400 | Registration already processed |
+11 -11
View File
@@ -8,15 +8,15 @@ Remove a media item from a device's shelf.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | ----------- |
| id | string (UUID) | Yes | Device UUID | | id | string (UUID) | Yes | Device UUID |
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------------- | -------- | ------------------------------------ |
| media_item_id | string (UUID) | Yes | Media item UUID to remove from shelf | | media_item_id | string (UUID) | Yes | Media item UUID to remove from shelf |
### Example Request ### Example Request
@@ -32,8 +32,8 @@ Item removed from shelf successfully.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------------ |
| 400 | Invalid request data | | 400 | Invalid request data |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 404 | Device or shelf item not found | | 404 | Device or shelf item not found |
+11 -11
View File
@@ -7,15 +7,15 @@ Revoke access to a device.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------ | -------- | ----------- |
| device_id | string | Yes | Device UUID | | device_id | string | Yes | Device UUID |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token | | Authorization | string | Yes | Bearer token |
### Example Request ### Example Request
@@ -30,8 +30,8 @@ Device revoked successfully.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ----------------------------- |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User does not own this device | | 403 | User does not own this device |
| 404 | Device not found | | 404 | Device not found |
+13 -13
View File
@@ -8,16 +8,16 @@ Update a device's information.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | ----------- |
| id | string (UUID) | Yes | Device UUID | | id | string (UUID) | Yes | Device UUID |
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ----------- | ------ | -------- | ---------------------------------- |
| name | string | No | Device display name | | name | string | No | Device display name |
| device_type | string | No | Device type (kobo, koreader, etc.) | | device_type | string | No | Device type (kobo, koreader, etc.) |
### Example Request ### Example Request
@@ -44,9 +44,9 @@ Update a device's information.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------------ |
| 400 | Invalid request data | | 400 | Invalid request data |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | Device does not belong to user | | 403 | Device does not belong to user |
| 404 | Device not found | | 404 | Device not found |
@@ -8,20 +8,20 @@ Create a new highlight for a media item.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------ | -------- | --------------- |
| media_id | string | Yes | Media item UUID | | media_id | string | Yes | Media item UUID |
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ---------------- | ------ | -------- | ----------------------------------------- |
| selection_text | string | Yes | Highlighted text | | selection_text | string | Yes | Highlighted text |
| start_position | string | No | Start position (e.g., epubcfi) | | start_position | string | No | Start position (e.g., epubcfi) |
| end_position | string | No | End position (e.g., epubcfi) | | end_position | string | No | End position (e.g., epubcfi) |
| color | string | No | Highlight color (hex, default: "#ffff00") | | color | string | No | Highlight color (hex, default: "#ffff00") |
| percentage_start | float | No | Start percentage (0-1) | | percentage_start | float | No | Start percentage (0-1) |
| percentage_end | float | No | End percentage (0-1) | | percentage_end | float | No | End percentage (0-1) |
### Example Request ### Example Request
@@ -55,8 +55,8 @@ Create a new highlight for a media item.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------ |
| 400 | Invalid highlight data | | 400 | Invalid highlight data |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 404 | Media item not found | | 404 | Media item not found |
@@ -7,15 +7,15 @@ Delete a highlight.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | ------------ | ------ | -------- | -------------- |
| highlight_id | string | Yes | Highlight UUID | | highlight_id | string | Yes | Highlight UUID |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token | | Authorization | string | Yes | Bearer token |
### Example Request ### Example Request
@@ -30,8 +30,8 @@ Highlight deleted successfully.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | -------------------------------- |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User does not own this highlight | | 403 | User does not own this highlight |
| 404 | Highlight not found | | 404 | Highlight not found |
+10 -10
View File
@@ -7,15 +7,15 @@ Retrieve all highlights for a specific media item.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------ | -------- | --------------- |
| media_id | string | Yes | Media item UUID | | media_id | string | Yes | Media item UUID |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token | | Authorization | string | Yes | Bearer token |
### Example Request ### Example Request
@@ -51,7 +51,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------ |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 404 | Media item not found | | 404 | Media item not found |
@@ -8,16 +8,16 @@ Update an existing highlight.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | ------------ | ------ | -------- | -------------- |
| highlight_id | string | Yes | Highlight UUID | | highlight_id | string | Yes | Highlight UUID |
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | -------------- | ------ | -------- | ----------------------------- |
| selection_text | string | No | Updated highlighted text | | selection_text | string | No | Updated highlighted text |
| color | string | No | Updated highlight color (hex) | | color | string | No | Updated highlight color (hex) |
### Example Request ### Example Request
@@ -43,9 +43,9 @@ Update an existing highlight.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | -------------------------------- |
| 400 | Invalid highlight data | | 400 | Invalid highlight data |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User does not own this highlight | | 403 | User does not own this highlight |
| 404 | Highlight not found | | 404 | Highlight not found |
@@ -11,9 +11,9 @@ This endpoint requires device authentication (not user JWT). This is a Kobo comp
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | -------- | ------ | -------- | ----------------------------------- |
| (varies) | object | No | Kobo analytics data (format varies) | | (varies) | object | No | Kobo analytics data (format varies) |
### Example Request ### Example Request
@@ -33,9 +33,9 @@ This endpoint requires device authentication (not user JWT). This is a Kobo comp
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ---------------------------- |
| 401 | Device authentication failed | | 401 | Device authentication failed |
## Notes ## Notes
+15 -15
View File
@@ -11,19 +11,19 @@ This endpoint requires device authentication (not user JWT). Kobo devices authen
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | --------- | ----- | -------- | ------------------------- |
| bookmarks | array | Yes | Array of bookmark objects | | bookmarks | array | Yes | Array of bookmark objects |
### Bookmark Object ### Bookmark Object
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------- | -------- | ------------------ |
| BookmarkID | string | Yes | Unique bookmark ID | | BookmarkID | string | Yes | Unique bookmark ID |
| ContentID | string | Yes | Book content ID | | ContentID | string | Yes | Book content ID |
| StartPosition | integer | Yes | Bookmark position | | StartPosition | integer | Yes | Bookmark position |
| Text | string | No | Bookmark text | | Text | string | No | Bookmark text |
| DateCreated | string | Yes | ISO 8601 timestamp | | DateCreated | string | Yes | ISO 8601 timestamp |
### Example Request ### Example Request
@@ -52,8 +52,8 @@ This endpoint requires device authentication (not user JWT). Kobo devices authen
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ---------------------------- |
| 401 | Device authentication failed | | 401 | Device authentication failed |
| 400 | Invalid request data | | 400 | Invalid request data |
| 404 | Device or book not found | | 404 | Device or book not found |
+9 -9
View File
@@ -11,11 +11,11 @@ This endpoint requires device authentication (not user JWT). Kobo devices authen
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | -------------- | ------ | -------- | ---------------------------- |
| X-Device-ID | string | Yes | Device UUID | | X-Device-ID | string | Yes | Device UUID |
| X-Device-Key | string | Yes | Device authentication key | | X-Device-Key | string | Yes | Device authentication key |
| X-Kobo-UserKey | string | No | Kobo user key (if available) | | X-Kobo-UserKey | string | No | Kobo user key (if available) |
### Example Request ### Example Request
@@ -39,10 +39,10 @@ X-Device-Key: device-auth-key
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ---------------------------- |
| 401 | Device authentication failed | | 401 | Device authentication failed |
| 404 | Device not found | | 404 | Device not found |
## Notes ## Notes
+19 -19
View File
@@ -11,23 +11,23 @@ This endpoint requires device authentication (not user JWT). Kobo devices authen
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | --------- | ----- | -------- | -------------------------------- |
| bookmarks | array | Yes | Array of bookmark/markup objects | | bookmarks | array | Yes | Array of bookmark/markup objects |
### Bookmark Object ### Bookmark Object
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------- | -------- | -------------------------- |
| BookmarkID | string | Yes | Unique bookmark ID | | BookmarkID | string | Yes | Unique bookmark ID |
| ContentID | string | Yes | Book content ID | | ContentID | string | Yes | Book content ID |
| StartPosition | integer | Yes | Highlight start position | | StartPosition | integer | Yes | Highlight start position |
| EndPosition | integer | No | Highlight end position | | EndPosition | integer | No | Highlight end position |
| Text | string | No | Highlighted text | | Text | string | No | Highlighted text |
| Annotation | string | No | User annotation | | Annotation | string | No | User annotation |
| DateCreated | string | Yes | ISO 8601 timestamp | | DateCreated | string | Yes | ISO 8601 timestamp |
| Chapter | string | No | Chapter title | | Chapter | string | No | Chapter title |
| Hidden | boolean | No | Whether bookmark is hidden | | Hidden | boolean | No | Whether bookmark is hidden |
### Example Request ### Example Request
@@ -60,8 +60,8 @@ This endpoint requires device authentication (not user JWT). Kobo devices authen
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ---------------------------- |
| 401 | Device authentication failed | | 401 | Device authentication failed |
| 400 | Invalid request data | | 400 | Invalid request data |
| 404 | Device or book not found | | 404 | Device or book not found |
+9 -12
View File
@@ -11,19 +11,16 @@ This endpoint requires device authentication (not user JWT). Kobo devices authen
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | --------- | --------------- | -------- | --------------------------------------------- |
| book_ids | array of string | No | Array of ContentIDs to sync | | book_ids | array of string | No | Array of ContentIDs to sync |
| full_sync | boolean | No | Whether to perform full sync (default: false) | | full_sync | boolean | No | Whether to perform full sync (default: false) |
### Example Request ### Example Request
```json ```json
{ {
"book_ids": [ "book_ids": ["content-id-1", "content-id-2"],
"content-id-1",
"content-id-2"
],
"full_sync": false "full_sync": false
} }
``` ```
@@ -53,10 +50,10 @@ This endpoint requires device authentication (not user JWT). Kobo devices authen
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ---------------------------- |
| 401 | Device authentication failed | | 401 | Device authentication failed |
| 404 | Device not found | | 404 | Device not found |
## Notes ## Notes
+8 -8
View File
@@ -11,10 +11,10 @@ This endpoint requires device authentication (not user JWT). Devices authenticat
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------ | ------ | -------- | ------------------------- |
| X-Device-ID | string | Yes | Device UUID | | X-Device-ID | string | Yes | Device UUID |
| X-Device-Key | string | Yes | Device authentication key | | X-Device-Key | string | Yes | Device authentication key |
### Example Request ### Example Request
@@ -43,7 +43,7 @@ X-Device-Key: device-auth-key
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ---------------------------- |
| 401 | Device authentication failed | | 401 | Device authentication failed |
| 404 | Device not found | | 404 | Device not found |
+11 -11
View File
@@ -7,9 +7,9 @@ Get metadata for a book from KOReader device.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | ----------- |
| uuid | string (UUID) | Yes | Book UUID | | uuid | string (UUID) | Yes | Book UUID |
## Device Authentication ## Device Authentication
@@ -17,10 +17,10 @@ This endpoint requires device authentication (not user JWT). Devices authenticat
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------ | ------ | -------- | ------------------------- |
| X-Device-ID | string | Yes | Device UUID | | X-Device-ID | string | Yes | Device UUID |
| X-Device-Key | string | Yes | Device authentication key | | X-Device-Key | string | Yes | Device authentication key |
### Example Request ### Example Request
@@ -45,7 +45,7 @@ X-Device-Key: device-auth-key
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ---------------------------- |
| 401 | Device authentication failed | | 401 | Device authentication failed |
| 404 | Book or device not found | | 404 | Book or device not found |
+19 -19
View File
@@ -11,23 +11,23 @@ This endpoint requires device authentication (not user JWT). Devices authenticat
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | --------- | ------------- | -------- | ------------------------- |
| device_id | string (UUID) | Yes | Device UUID | | device_id | string (UUID) | Yes | Device UUID |
| bookmarks | array | Yes | Array of bookmark objects | | bookmarks | array | Yes | Array of bookmark objects |
### Bookmark Object ### Bookmark Object
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ---------------- | ------- | -------- | -------------------------- |
| book | string | Yes | Book identifier | | book | string | Yes | Book identifier |
| chapter | string | No | Chapter title | | chapter | string | No | Chapter title |
| page | integer | No | Page number | | page | integer | No | Page number |
| position | float | Yes | Position in document (0-1) | | position | float | Yes | Position in document (0-1) |
| notes | string | No | Bookmark notes | | notes | string | No | Bookmark notes |
| highlighted_text | string | No | Highlighted text | | highlighted_text | string | No | Highlighted text |
| time | string | Yes | ISO 8601 timestamp | | time | string | Yes | ISO 8601 timestamp |
| created_at | string | Yes | ISO 8601 timestamp | | created_at | string | Yes | ISO 8601 timestamp |
### Example Request ### Example Request
@@ -60,8 +60,8 @@ This endpoint requires device authentication (not user JWT). Devices authenticat
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ---------------------------- |
| 401 | Device authentication failed | | 401 | Device authentication failed |
| 400 | Invalid request data | | 400 | Invalid request data |
| 404 | Device not found | | 404 | Device not found |
+17 -17
View File
@@ -11,21 +11,21 @@ This endpoint requires device authentication (not user JWT). Devices authenticat
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | --------- | ------------- | -------- | ------------------------- |
| device_id | string (UUID) | Yes | Device UUID | | device_id | string (UUID) | Yes | Device UUID |
| progress | array | Yes | Array of progress objects | | progress | array | Yes | Array of progress objects |
### Progress Object ### Progress Object
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ----------- | ------- | -------- | ---------------------------------- |
| book | string | Yes | Book identifier (filename or UUID) | | book | string | Yes | Book identifier (filename or UUID) |
| percent | float | Yes | Progress percentage (0-100) | | percent | float | Yes | Progress percentage (0-100) |
| page | integer | No | Current page number | | page | integer | No | Current page number |
| total_pages | integer | No | Total pages in document | | total_pages | integer | No | Total pages in document |
| date_read | string | No | ISO 8601 timestamp of last read | | date_read | string | No | ISO 8601 timestamp of last read |
| updated_at | string | Yes | ISO 8601 timestamp | | updated_at | string | Yes | ISO 8601 timestamp |
### Example Request ### Example Request
@@ -56,8 +56,8 @@ This endpoint requires device authentication (not user JWT). Devices authenticat
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ---------------------------- |
| 401 | Device authentication failed | | 401 | Device authentication failed |
| 400 | Invalid request data | | 400 | Invalid request data |
| 404 | Device not found | | 404 | Device not found |
@@ -8,15 +8,15 @@ Add a folder to an existing library (Admin only).
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | ---------- | ------ | -------- | ------------ |
| library_id | string | Yes | Library UUID | | library_id | string | Yes | Library UUID |
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ----------- | ------ | -------- | ----------------------- |
| folder_path | string | Yes | Absolute path to folder | | folder_path | string | Yes | Absolute path to folder |
### Example Request ### Example Request
@@ -39,9 +39,9 @@ Add a folder to an existing library (Admin only).
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------ |
| 400 | Invalid folder path | | 400 | Invalid folder path |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User is not an admin | | 403 | User is not an admin |
| 404 | Library not found | | 404 | Library not found |
+13 -13
View File
@@ -7,15 +7,15 @@ Browse server directories for folder selection in library management.
## Query Parameters ## Query Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------ | -------- | --------------------------------------- |
| path | string | No | Directory path to browse (default: "/") | | path | string | No | Directory path to browse (default: "/") |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ------------------------- |
| Authorization | string | Yes | Bearer token (admin only) | | Authorization | string | Yes | Bearer token (admin only) |
### Example Request ### Example Request
@@ -36,13 +36,13 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | -------------------------------------- |
| 400 | Path traversal attempt or invalid path | | 400 | Path traversal attempt or invalid path |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User is not an admin | | 403 | User is not an admin |
| 400 | Path does not exist | | 400 | Path does not exist |
| 400 | Path is not a directory | | 400 | Path is not a directory |
## Security ## Security
+10 -10
View File
@@ -8,11 +8,11 @@ Create a new library (Admin only).
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ----------- | ------ | -------- | ----------------------------------------------------- |
| name | string | Yes | Library name | | name | string | Yes | Library name |
| description | string | No | Library description | | description | string | No | Library description |
| type | string | Yes | Library type (e.g., "ebooks", "comics", "audiobooks") | | type | string | Yes | Library type (e.g., "ebooks", "comics", "audiobooks") |
### Example Request ### Example Request
@@ -38,8 +38,8 @@ Create a new library (Admin only).
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------ |
| 400 | Invalid input data | | 400 | Invalid input data |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User is not an admin | | 403 | User is not an admin |
+11 -11
View File
@@ -7,15 +7,15 @@ Delete a library and all associated data.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | ------------ |
| id | string (UUID) | Yes | Library UUID | | id | string (UUID) | Yes | Library UUID |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ----------------------------------- |
| Authorization | string | Yes | Bearer token (must have admin role) | | Authorization | string | Yes | Bearer token (must have admin role) |
### Example Request ### Example Request
@@ -30,8 +30,8 @@ Library deleted successfully.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ----------------------------------- |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User does not have admin privileges | | 403 | User does not have admin privileges |
| 404 | Library not found | | 404 | Library not found |
@@ -8,15 +8,15 @@ Delete a folder from a library.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | ------------ |
| id | string (UUID) | Yes | Library UUID | | id | string (UUID) | Yes | Library UUID |
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ----------- | ------ | -------- | ------------------------------------- |
| folder_path | string | Yes | Absolute path to the folder to delete | | folder_path | string | Yes | Absolute path to the folder to delete |
### Example Request ### Example Request
@@ -32,9 +32,9 @@ Library folder deleted successfully.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ----------------------------------- |
| 400 | Invalid request data | | 400 | Invalid request data |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User does not have admin privileges | | 403 | User does not have admin privileges |
| 404 | Library or folder not found | | 404 | Library or folder not found |
+11 -11
View File
@@ -7,15 +7,15 @@ Retrieve details of a specific library.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | ---------- | ------ | -------- | ------------ |
| library_id | string | Yes | Library UUID | | library_id | string | Yes | Library UUID |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token | | Authorization | string | Yes | Bearer token |
### Example Request ### Example Request
@@ -44,8 +44,8 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ----------------------------------------- |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User does not have access to this library | | 403 | User does not have access to this library |
| 404 | Library not found | | 404 | Library not found |
@@ -7,24 +7,24 @@ Get all media items in a specific library.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | ------------ |
| id | string (UUID) | Yes | Library UUID | | id | string (UUID) | Yes | Library UUID |
## Query Parameters ## Query Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | ---------- | ------- | -------- | ----------------------------------------------- |
| limit | integer | No | Maximum number of items to return (default: 50) | | limit | integer | No | Maximum number of items to return (default: 50) |
| offset | integer | No | Number of items to skip (default: 0) | | offset | integer | No | Number of items to skip (default: 0) |
| sort_by | string | No | Sort field (title, created_at, etc.) | | sort_by | string | No | Sort field (title, created_at, etc.) |
| sort_order | string | No | Sort order (asc, desc) | | sort_order | string | No | Sort order (asc, desc) |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ----------------------------------- |
| Authorization | string | Yes | Bearer token (must have admin role) | | Authorization | string | Yes | Bearer token (must have admin role) |
### Example Request ### Example Request
@@ -54,8 +54,8 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ----------------------------------- |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User does not have admin privileges | | 403 | User does not have admin privileges |
| 404 | Library not found | | 404 | Library not found |
@@ -7,15 +7,15 @@ Get statistics for a specific library.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | ------------ |
| id | string (UUID) | Yes | Library UUID | | id | string (UUID) | Yes | Library UUID |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ----------------------------------- |
| Authorization | string | Yes | Bearer token (must have admin role) | | Authorization | string | Yes | Bearer token (must have admin role) |
### Example Request ### Example Request
@@ -43,8 +43,8 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ----------------------------------- |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User does not have admin privileges | | 403 | User does not have admin privileges |
| 404 | Library not found | | 404 | Library not found |
@@ -7,9 +7,9 @@ Retrieve all libraries visible to the current user.
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token | | Authorization | string | Yes | Bearer token |
### Example Request ### Example Request
@@ -36,6 +36,6 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------ |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
@@ -8,11 +8,11 @@ Set library visibility for a specific user (Admin only).
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ---------- | ------- | -------- | ---------------------------------- |
| user_id | string | Yes | User UUID | | user_id | string | Yes | User UUID |
| library_id | string | Yes | Library UUID | | library_id | string | Yes | Library UUID |
| is_visible | boolean | Yes | Whether library is visible to user | | is_visible | boolean | Yes | Whether library is visible to user |
### Example Request ### Example Request
@@ -36,9 +36,9 @@ Set library visibility for a specific user (Admin only).
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------- |
| 400 | Invalid input data | | 400 | Invalid input data |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User is not an admin | | 403 | User is not an admin |
| 404 | User or library not found | | 404 | User or library not found |
+13 -13
View File
@@ -8,16 +8,16 @@ Update a library's information.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------------- | -------- | ------------ |
| id | string (UUID) | Yes | Library UUID | | id | string (UUID) | Yes | Library UUID |
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | --------------- | ------- | -------- | --------------- |
| name | string | No | Library name | | name | string | No | Library name |
| library_type_id | integer | No | Library type ID | | library_type_id | integer | No | Library type ID |
### Example Request ### Example Request
@@ -42,9 +42,9 @@ Update a library's information.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ----------------------------------- |
| 400 | Invalid request data | | 400 | Invalid request data |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User does not have admin privileges | | 403 | User does not have admin privileges |
| 404 | Library not found | | 404 | Library not found |
@@ -8,9 +8,9 @@ Delete multiple media items at once (supports ebooks, comics, manga).
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | -------------- | ------------- | -------- | ----------------------------------- |
| media_item_ids | array of UUID | Yes | Array of media item UUIDs to delete | | media_item_ids | array of UUID | Yes | Array of media item UUIDs to delete |
### Example Request ### Example Request
@@ -51,24 +51,24 @@ Delete multiple media items at once (supports ebooks, comics, manga).
## Response Fields ## Response Fields
| Field | Type | Description | | Field | Type | Description |
|-------|------|-------------| | ----------------------- | ------ | ------------------------------------------------- |
| results | array | Individual result for each media item | | results | array | Individual result for each media item |
| results[].media_item_id | string | UUID of the media item | | results[].media_item_id | string | UUID of the media item |
| results[].status | string | "success" or "error" | | results[].status | string | "success" or "error" |
| results[].error | string | Error message (only present if status is "error") | | results[].error | string | Error message (only present if status is "error") |
| total | number | Total number of media items processed | | total | number | Total number of media items processed |
| deleted | number | Number of media items successfully deleted | | deleted | number | Number of media items successfully deleted |
| failed | number | Number of media items that failed to delete | | failed | number | Number of media items that failed to delete |
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | -------------------------------------------------- |
| 400 | Invalid request data or empty media_item_ids array | | 400 | Invalid request data or empty media_item_ids array |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User does not have permission | | 403 | User does not have permission |
| 500 | Server error during deletion | | 500 | Server error during deletion |
## Notes ## Notes
@@ -8,21 +8,21 @@ Update multiple media items at once (supports ebooks, comics, manga).
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ---------------------------------- | ---------------- | -------- | -------------------------- |
| media_item_updates | array of objects | Yes | Array of update operations | | media_item_updates | array of objects | Yes | Array of update operations |
| media_item_updates[].media_item_id | string (UUID) | Yes | Media item UUID to update | | media_item_updates[].media_item_id | string (UUID) | Yes | Media item UUID to update |
| media_item_updates[].updates | object | Yes | Fields to update | | media_item_updates[].updates | object | Yes | Fields to update |
### Update Fields ### Update Fields
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | -------- | ---------------- | -------- | --------------------------------- |
| title | string | No | Updated title | | title | string | No | Updated title |
| author | string | No | Updated author | | author | string | No | Updated author |
| genre | string | No | Updated genre | | genre | string | No | Updated genre |
| language | string | No | Updated language (ISO 639-1 code) | | language | string | No | Updated language (ISO 639-1 code) |
| tags | array of strings | No | Updated tags (auto-normalized) | | tags | array of strings | No | Updated tags (auto-normalized) |
### Example Request ### Example Request
@@ -71,15 +71,15 @@ Update multiple media items at once (supports ebooks, comics, manga).
## Response Fields ## Response Fields
| Field | Type | Description | | Field | Type | Description |
|-------|------|-------------| | ----------------------- | ------ | ------------------------------------------------- |
| results | array | Individual result for each media item | | results | array | Individual result for each media item |
| results[].media_item_id | string | UUID of the media item | | results[].media_item_id | string | UUID of the media item |
| results[].status | string | "success" or "error" | | results[].status | string | "success" or "error" |
| results[].error | string | Error message (only present if status is "error") | | results[].error | string | Error message (only present if status is "error") |
| total | number | Total number of media items processed | | total | number | Total number of media items processed |
| updated | number | Number of media items successfully updated | | updated | number | Number of media items successfully updated |
| failed | number | Number of media items that failed to update | | failed | number | Number of media items that failed to update |
## Tag and Contributor Normalization ## Tag and Contributor Normalization
@@ -90,13 +90,13 @@ The backend automatically normalizes tags:
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------------------------------------ |
| 400 | Invalid request data or empty media_item_updates array | | 400 | Invalid request data or empty media_item_updates array |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User does not have permission | | 403 | User does not have permission |
| 404 | One or more media items not found | | 404 | One or more media items not found |
| 500 | Server error during update | | 500 | Server error during update |
## Notes ## Notes
@@ -18,31 +18,31 @@ See [Library API documentation](../libraries/) for more details.
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ---------------- | ---------------- | -------- | ------------------------------------- |
| library_id | string (UUID) | Yes | Library UUID to add the media item to | | library_id | string (UUID) | Yes | Library UUID to add the media item to |
| title | string | Yes | Media item title (1-500 characters) | | title | string | Yes | Media item title (1-500 characters) |
| author | string | No | Author name | | author | string | No | Author name |
| isbn | string | No | ISBN number | | isbn | string | No | ISBN number |
| description | string | No | Description or summary | | description | string | No | Description or summary |
| file_path | string | Yes | Path to the media file | | file_path | string | Yes | Path to the media file |
| file_size | integer | Yes | Size of the file in bytes | | file_size | integer | Yes | Size of the file in bytes |
| mime_type | string | Yes | MIME type of the file | | mime_type | string | Yes | MIME type of the file |
| cover_image_path | string | No | Path to the cover image | | cover_image_path | string | No | Path to the cover image |
| series | string | No | Series name | | series | string | No | Series name |
| series_number | integer | No | Number in the series | | series_number | integer | No | Number in the series |
| tags | array of strings | No | Tags (auto-normalized) | | tags | array of strings | No | Tags (auto-normalized) |
| asin | string | No | Amazon ASIN | | asin | string | No | Amazon ASIN |
| date_published | string | No | Publication date | | date_published | string | No | Publication date |
| publisher | string | No | Publisher name | | publisher | string | No | Publisher name |
| contributors | array of strings | No | Contributors (auto-normalized) | | contributors | array of strings | No | Contributors (auto-normalized) |
## Tag/Contributor Normalization ## Tag/Contributor Normalization
Tags and contributors are automatically normalized: Tags and contributors are automatically normalized:
- **Tags**: Titlecased, punctuation preserved, case-insensitive deduplication - **Tags**: Titlecased, punctuation preserved, case-insensitive deduplication
- **Contributors**: Original casing and punctuation preserved, case-insensitive deduplication - **Contributors**: Original casing and punctuation preserved, case-insensitive deduplication
- **Search fields**: Auto-generated for case-insensitive search - **Search fields**: Auto-generated for case-insensitive search
### Example Request ### Example Request
@@ -92,12 +92,12 @@ Tags and contributors are automatically normalized:
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ---------------------------------------------- |
| 400 | Invalid request data OR library has no folders | | 400 | Invalid request data OR library has no folders |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User is not an admin | | 403 | User is not an admin |
| 404 | Library not found | | 404 | Library not found |
### 400 - Library Has No Folders ### 400 - Library Has No Folders
@@ -7,15 +7,15 @@ Delete a media item from the library (Admin only).
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------ | -------- | --------------- |
| media_id | string | Yes | Media item UUID | | media_id | string | Yes | Media item UUID |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token | | Authorization | string | Yes | Bearer token |
### Example Request ### Example Request
@@ -30,8 +30,8 @@ Media item deleted successfully.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------ |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User is not an admin | | 403 | User is not an admin |
| 404 | Media item not found | | 404 | Media item not found |
@@ -8,24 +8,25 @@ Download a media item file (EPUB, PDF, etc.) from the Bookhoard server.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------ | -------- | --------------- |
| uuid | string | Yes | Media item UUID | | uuid | string | Yes | Media item UUID |
## Response ## Response
**Success (200 OK)**: Binary file data **Success (200 OK)**: Binary file data
**Response Headers**: **Response Headers**:
- `Content-Type`: `application/epub+zip`, `application/pdf`, or appropriate MIME type - `Content-Type`: `application/epub+zip`, `application/pdf`, or appropriate MIME type
- `Content-Disposition`: `attachment; filename="filename.epub"` - `Content-Disposition`: `attachment; filename="filename.epub"`
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | --------------------------------- |
| 404 | Media item not found | | 404 | Media item not found |
| 500 | Server error during file download | | 500 | Server error during file download |
## Example ## Example
@@ -8,18 +8,18 @@ Filter and sort media items with advanced criteria.
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------- | -------- | ----------------------------------------------------------- |
| library_id | string | Yes | Library UUID | | library_id | string | Yes | Library UUID |
| author_filter | string | No | Filter by author name | | author_filter | string | No | Filter by author name |
| series_filter | string | No | Filter by series name | | series_filter | string | No | Filter by series name |
| genre_filter | string | No | Filter by genre | | genre_filter | string | No | Filter by genre |
| year_min | integer | No | Minimum copyright year | | year_min | integer | No | Minimum copyright year |
| year_max | integer | No | Maximum copyright year | | year_max | integer | No | Maximum copyright year |
| has_cover | boolean | No | Filter by cover image existence | | has_cover | boolean | No | Filter by cover image existence |
| sort | string | No | Sort field and order (e.g., "title ASC", "created_at DESC") | | sort | string | No | Sort field and order (e.g., "title ASC", "created_at DESC") |
| limit | integer | No | Number of results (default 20) | | limit | integer | No | Number of results (default 20) |
| offset | integer | No | Number to skip | | offset | integer | No | Number to skip |
### Example Request ### Example Request
@@ -56,8 +56,8 @@ Filter and sort media items with advanced criteria.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ----------------------------------------- |
| 400 | Invalid filter parameters | | 400 | Invalid filter parameters |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User does not have access to this library | | 403 | User does not have access to this library |
@@ -7,15 +7,15 @@ Retrieve details of a specific media item.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------ | -------- | --------------- |
| media_id | string | Yes | Media item UUID | | media_id | string | Yes | Media item UUID |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token | | Authorization | string | Yes | Bearer token |
### Example Request ### Example Request
@@ -37,13 +37,13 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
"file_size": 1024000, "file_size": 1024000,
"mime_type": "application/epub+zip", "mime_type": "application/epub+zip",
"cover_image_path": "/path/to/cover.jpg", "cover_image_path": "/path/to/cover.jpg",
"series": "Series Name", "series": "Series Name",
"series_number": 1, "series_number": 1,
"tags": ["sci-fi", "space opera"], "tags": ["sci-fi", "space opera"],
"tags_search": ["sci fi", "space opera"], "tags_search": ["sci fi", "space opera"],
"contributors": ["Author Name", "ACME CORP."], "contributors": ["Author Name", "ACME CORP."],
"contributors_search": ["author name", "acme corp"], "contributors_search": ["author name", "acme corp"],
"language": "en", "language": "en",
"page_count": 350, "page_count": 350,
"genre": "Science Fiction", "genre": "Science Fiction",
"copyright_year": 2023, "copyright_year": 2023,
@@ -53,8 +53,8 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | -------------------------------------------- |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User does not have access to this media item | | 403 | User does not have access to this media item |
| 404 | Media item not found | | 404 | Media item not found |
@@ -7,17 +7,17 @@ Retrieve a paginated list of media items from a library.
## Query Parameters ## Query Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | ---------- | ------- | -------- | ----------------------------------------------- |
| library_id | string | Yes | Library UUID | | library_id | string | Yes | Library UUID |
| limit | integer | No | Number of items to return (max 100, default 20) | | limit | integer | No | Number of items to return (max 100, default 20) |
| offset | integer | No | Number of items to skip | | offset | integer | No | Number of items to skip |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token | | Authorization | string | Yes | Bearer token |
### Example Request ### Example Request
@@ -41,13 +41,13 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
"file_size": 1024000, "file_size": 1024000,
"mime_type": "application/epub+zip", "mime_type": "application/epub+zip",
"cover_image_path": "/path/to/cover.jpg", "cover_image_path": "/path/to/cover.jpg",
"series": "Series Name", "series": "Series Name",
"series_number": 1, "series_number": 1,
"tags": ["sci-fi", "space opera"], "tags": ["sci-fi", "space opera"],
"tags_search": ["sci fi", "space opera"], "tags_search": ["sci fi", "space opera"],
"contributors": ["Author Name", "ACME CORP."], "contributors": ["Author Name", "ACME CORP."],
"contributors_search": ["author name", "acme corp"], "contributors_search": ["author name", "acme corp"],
"language": "en", "language": "en",
"page_count": 350, "page_count": 350,
"genre": "Science Fiction", "genre": "Science Fiction",
"copyright_year": 2023, "copyright_year": 2023,
@@ -60,8 +60,8 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ----------------------------------------- |
| 400 | Invalid query parameters | | 400 | Invalid query parameters |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User does not have access to this library | | 403 | User does not have access to this library |
@@ -5,6 +5,7 @@ Search for media items by title, author, series, tags, or contributors.
**Note:** Search is case-insensitive and punctuation-agnostic. The search query is matched against normalized tags_search and contributors_search fields, allowing users to find matches regardless of casing or punctuation. **Note:** Search is case-insensitive and punctuation-agnostic. The search query is matched against normalized tags_search and contributors_search fields, allowing users to find matches regardless of casing or punctuation.
Examples: Examples:
- Search "acme corp" finds items with "ACME CORP." or "Acme Corp" - Search "acme corp" finds items with "ACME CORP." or "Acme Corp"
- Search "oreilly" finds items with "O'Reilly Media" or "OReilly Media" - Search "oreilly" finds items with "O'Reilly Media" or "OReilly Media"
- Search "science fiction" finds items with "Science-Fiction" or "science-fiction" - Search "science fiction" finds items with "Science-Fiction" or "science-fiction"
@@ -14,17 +15,17 @@ Examples:
## Query Parameters ## Query Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------- | -------- | ----------------------------------- |
| q | string | Yes | Search query (minimum 2 characters) | | q | string | Yes | Search query (minimum 2 characters) |
| limit | integer | No | Number of results (default 20) | | limit | integer | No | Number of results (default 20) |
| offset | integer | No | Number to skip | | offset | integer | No | Number to skip |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token | | Authorization | string | Yes | Bearer token |
### Example Request ### Example Request
@@ -51,7 +52,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | -------------------------------- |
| 400 | Invalid search query (too short) | | 400 | Invalid search query (too short) |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
@@ -8,27 +8,28 @@ Update media item metadata (Admin only).
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------ | -------- | --------------- |
| media_id | string | Yes | Media item UUID | | media_id | string | Yes | Media item UUID |
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | --------------- | -------- | -------------------------------------- |
| title | string | No | Updated title | | title | string | No | Updated title |
| author | string | No | Updated author | | author | string | No | Updated author |
| description | string | No | Updated description | | description | string | No | Updated description |
| series | string | No | Series name | | series | string | No | Series name |
| series_number | integer | No | Number in series | | series_number | integer | No | Number in series |
| tags | array of string | No | Updated tags (auto-normalized) | | tags | array of string | No | Updated tags (auto-normalized) |
| contributors | array of string | No | Updated contributors (auto-normalized) | | contributors | array of string | No | Updated contributors (auto-normalized) |
**Tag/Contributor Normalization:** **Tag/Contributor Normalization:**
- Tags are titlecased and deduplicated (case-insensitive)
- Contributors preserve original casing and punctuation - Tags are titlecased and deduplicated (case-insensitive)
- Punctuation-preferred deduplication (keeps "ACME CORP." over "acme corp") - Contributors preserve original casing and punctuation
- Search fields auto-generated for case-insensitive search - Punctuation-preferred deduplication (keeps "ACME CORP." over "acme corp")
- Search fields auto-generated for case-insensitive search
### Example Request ### Example Request
@@ -58,9 +59,9 @@ Update media item metadata (Admin only).
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------ |
| 400 | Invalid input data | | 400 | Invalid input data |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User is not an admin | | 403 | User is not an admin |
| 404 | Media item not found | | 404 | Media item not found |
+14 -14
View File
@@ -8,18 +8,18 @@ Create a new note for a media item.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------ | -------- | --------------- |
| media_id | string | Yes | Media item UUID | | media_id | string | Yes | Media item UUID |
## Request Body ## Request Body
| Field | Type | Required | Description | | Field | Type | Required | Description |
|--------|------|-----------|-------------| | ------------------- | ------ | -------- | ---------------------------------- |
| content | string | Yes | Note content | | content | string | Yes | Note content |
| position | string | No | Location reference (e.g., epubcfi) | | position | string | No | Location reference (e.g., epubcfi) |
| percentage_location | float | No | Location as percentage (0-1) | | percentage_location | float | No | Location as percentage (0-1) |
| epubcfi_location | string | No | EPUB CFI location | | epubcfi_location | string | No | EPUB CFI location |
### Example Request ### Example Request
@@ -49,8 +49,8 @@ Create a new note for a media item.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------ |
| 400 | Invalid note data | | 400 | Invalid note data |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 404 | Media item not found | | 404 | Media item not found |
+11 -11
View File
@@ -7,15 +7,15 @@ Delete a note.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------ | -------- | ----------- |
| note_id | string | Yes | Note UUID | | note_id | string | Yes | Note UUID |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token | | Authorization | string | Yes | Bearer token |
### Example Request ### Example Request
@@ -30,8 +30,8 @@ Note deleted successfully.
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | --------------------------- |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 403 | User does not own this note | | 403 | User does not own this note |
| 404 | Note not found | | 404 | Note not found |
+10 -10
View File
@@ -7,15 +7,15 @@ Retrieve all notes for a specific media item.
## Path Parameters ## Path Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|-----------|-------------| | --------- | ------ | -------- | --------------- |
| media_id | string | Yes | Media item UUID | | media_id | string | Yes | Media item UUID |
## Request Headers ## Request Headers
| Header | Type | Required | Description | | Header | Type | Required | Description |
|--------|------|-----------|-------------| | ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token | | Authorization | string | Yes | Bearer token |
### Example Request ### Example Request
@@ -48,7 +48,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Error Responses ## Error Responses
| Code | Description | | Code | Description |
|------|-------------| | ---- | ------------------------ |
| 401 | Invalid or expired token | | 401 | Invalid or expired token |
| 404 | Media item not found | | 404 | Media item not found |

Some files were not shown because too many files have changed in this diff Show More