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:
+107
-66
@@ -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
@@ -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
|
||||
↓
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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/
|
||||
```
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user