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

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