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:
+55
-14
@@ -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.
|
||||
@@ -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,6 +434,7 @@ 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) {
|
||||
@@ -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 {
|
||||
@@ -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,6 +603,7 @@ 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:
|
||||
@@ -579,6 +612,7 @@ for _, folder := range s.folders {
|
||||
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
|
||||
@@ -845,12 +884,14 @@ 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"
|
||||
|
||||
+50
-15
@@ -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 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+302
-223
@@ -8,6 +8,7 @@ This document outlines the complete plan for automatically generating screenshot
|
||||
## Overview
|
||||
|
||||
Playwright will be used to:
|
||||
|
||||
1. Navigate the running Bookhoard server
|
||||
2. Perform key user/admin workflows
|
||||
3. Capture screenshots at each step
|
||||
@@ -17,6 +18,7 @@ Playwright will be used to:
|
||||
## Prerequisites (To Verify When Ready)
|
||||
|
||||
### Frontend Pages Complete
|
||||
|
||||
Verify these pages are fully functional before starting:
|
||||
|
||||
- [ ] `/` - Login page
|
||||
@@ -34,6 +36,7 @@ Verify these pages are fully functional before starting:
|
||||
- [ ] `/api-explorer` - API testing interface
|
||||
|
||||
### Test Environment Ready
|
||||
|
||||
- [ ] Bookhoard server running on `http://localhost:8765`
|
||||
- [ ] Test database seeded with sample data (books, collections, devices)
|
||||
- [ ] Test admin account ready (username, password, role=admin)
|
||||
@@ -49,6 +52,7 @@ npx playwright install chromium
|
||||
```
|
||||
|
||||
Project structure after setup:
|
||||
|
||||
```
|
||||
bookhoard/
|
||||
├── docs/
|
||||
@@ -93,133 +97,151 @@ USER_PASSWORD=SecureUserPass456!
|
||||
Before running the screenshot automation, ask the user:
|
||||
|
||||
### 1. Server Access
|
||||
|
||||
**Q**: Where is the Bookhoard server running?
|
||||
|
||||
- [ ] `localhost:8765` (default)
|
||||
- [ ] Custom port: `________`
|
||||
- [ ] Remote URL: `________`
|
||||
|
||||
### 2. Test Credentials
|
||||
|
||||
**Q**: What credentials should Playwright use?
|
||||
|
||||
**Admin Account** (for admin guide screenshots):
|
||||
|
||||
- Username: `________`
|
||||
- Password: `________`
|
||||
|
||||
**Regular User Account** (for user guide screenshots):
|
||||
|
||||
- Username: `________`
|
||||
- Password: `________`
|
||||
|
||||
### 3. Screenshot Format
|
||||
|
||||
**Q**: What format for screenshots?
|
||||
|
||||
- [ ] **WebP** (recommended - modern, good compression)
|
||||
- [ ] PNG (highest quality, larger files)
|
||||
- [ ] JPEG (smaller files, compression artifacts)
|
||||
|
||||
### 4. Screenshot Dimensions
|
||||
|
||||
**Q**: What viewport sizes for screenshots?
|
||||
|
||||
- [ ] **Desktop**: 1920x1080 (full-width screenshots)
|
||||
- [ ] **Tablet**: 768x1024 (responsive documentation)
|
||||
- [ ] **Mobile**: 375x667 (mobile documentation)
|
||||
- [ ] All three sizes (comprehensive coverage)
|
||||
|
||||
### 5. Theme
|
||||
|
||||
**Q**: What theme should screenshots use?
|
||||
|
||||
- [ ] **Default** (Tokyo Night theme as seen in templates)
|
||||
- [ ] Light theme (if implemented)
|
||||
- [ ] Multiple themes (document theme switching)
|
||||
|
||||
### 6. Language
|
||||
|
||||
**Q**: What language/region for the UI?
|
||||
|
||||
- [ ] **English** (default)
|
||||
- [ ] Other: `________`
|
||||
|
||||
## Screenshot Plan by Documentation Section
|
||||
|
||||
### 1. User Guide Screenshots
|
||||
|
||||
**File**: `docs/user/user-guide.md`
|
||||
|
||||
**Screenshots Needed**:
|
||||
|
||||
| Screenshot Name | Description | Page/Action |
|
||||
|-----------------|-------------|-------------|
|
||||
| `login-page.webp` | Login form with filled credentials | `/` - Login page |
|
||||
| `dashboard-overview.webp` | Main dashboard showing libraries | `/dashboard` |
|
||||
| `library-grid.webp` | Media items grid view | `/dashboard` → Click library |
|
||||
| `book-detail.webp` | Book detail view with metadata | `/dashboard` → Click book |
|
||||
| `search-results.webp` | Search in action | `/dashboard` → Type in search |
|
||||
| `filter-panel.webp` | Filter options expanded | `/dashboard` → Open filters |
|
||||
| `collections-list.webp` | Collections grid view | `/collections` |
|
||||
| `create-collection.webp` | New collection modal | `/collections` → Click "New Collection" |
|
||||
| `reading-progress.webp` | Progress tracking view | `/progress` |
|
||||
| `analytics-view.webp` | User analytics dashboard | `/analytics` |
|
||||
| Screenshot Name | Description | Page/Action |
|
||||
| ------------------------- | ---------------------------------- | --------------------------------------- |
|
||||
| `login-page.webp` | Login form with filled credentials | `/` - Login page |
|
||||
| `dashboard-overview.webp` | Main dashboard showing libraries | `/dashboard` |
|
||||
| `library-grid.webp` | Media items grid view | `/dashboard` → Click library |
|
||||
| `book-detail.webp` | Book detail view with metadata | `/dashboard` → Click book |
|
||||
| `search-results.webp` | Search in action | `/dashboard` → Type in search |
|
||||
| `filter-panel.webp` | Filter options expanded | `/dashboard` → Open filters |
|
||||
| `collections-list.webp` | Collections grid view | `/collections` |
|
||||
| `create-collection.webp` | New collection modal | `/collections` → Click "New Collection" |
|
||||
| `reading-progress.webp` | Progress tracking view | `/progress` |
|
||||
| `analytics-view.webp` | User analytics dashboard | `/analytics` |
|
||||
|
||||
**Estimated Screenshots**: ~10
|
||||
|
||||
### 2. Admin Guide Screenshots
|
||||
|
||||
**File**: `docs/user/admin-guide.md`
|
||||
|
||||
**Screenshots Needed**:
|
||||
|
||||
| Screenshot Name | Description | Page/Action |
|
||||
|-----------------|-------------|-------------|
|
||||
| `admin-dashboard.webp` | Admin overview panel | `/admin` |
|
||||
| `user-management.webp` | User list with actions | `/admin` → Users section |
|
||||
| `add-user.webp` | Add new user form | `/admin` → Click "Add User" |
|
||||
| `library-settings.webp` | Library configuration | `/admin/library` |
|
||||
| `add-library.webp` | Create new library form | `/admin/library` → Click "Add Library" |
|
||||
| `analytics-admin.webp` | Admin analytics view | `/analytics` (admin view) |
|
||||
| `sync-queue.webp` | Sync queue monitoring | `/queue` |
|
||||
| `theme-settings.webp` | Theme selection interface | `/admin/profile` → Theme section |
|
||||
| `user-profile-edit.webp` | Edit user profile | `/admin/profile` |
|
||||
| Screenshot Name | Description | Page/Action |
|
||||
| ------------------------ | ------------------------- | -------------------------------------- |
|
||||
| `admin-dashboard.webp` | Admin overview panel | `/admin` |
|
||||
| `user-management.webp` | User list with actions | `/admin` → Users section |
|
||||
| `add-user.webp` | Add new user form | `/admin` → Click "Add User" |
|
||||
| `library-settings.webp` | Library configuration | `/admin/library` |
|
||||
| `add-library.webp` | Create new library form | `/admin/library` → Click "Add Library" |
|
||||
| `analytics-admin.webp` | Admin analytics view | `/analytics` (admin view) |
|
||||
| `sync-queue.webp` | Sync queue monitoring | `/queue` |
|
||||
| `theme-settings.webp` | Theme selection interface | `/admin/profile` → Theme section |
|
||||
| `user-profile-edit.webp` | Edit user profile | `/admin/profile` |
|
||||
|
||||
**Estimated Screenshots**: ~9
|
||||
|
||||
### 3. Device Setup Screenshots
|
||||
|
||||
**File**: `docs/user/devices/kobo-setup.md` and `koreader-setup.md`
|
||||
|
||||
**Screenshots Needed**:
|
||||
|
||||
| Screenshot Name | Description | Page/Action |
|
||||
|-----------------|-------------|-------------|
|
||||
| `device-list.webp` | Device management page | `/devices` |
|
||||
| `add-device-modal.webp` | Add new device modal | `/devices` → Click "Add New Device" |
|
||||
| `device-form-kobo.webp` | Kobo device registration form | `/devices` → Select Kobo type |
|
||||
| `device-form-koreader.webp` | KOReader device registration form | `/devices` → Select KOReader type |
|
||||
| `device-qr-code.webp` | QR code for device approval | After device registration |
|
||||
| `device-approved.webp` | Device approved confirmation | After approving device |
|
||||
| `device-sync-settings.webp` | Sync configuration for device | `/devices` → Click device settings |
|
||||
| `shelf-mapping.webp` | Collection to shelf mapping | `/devices` → Click shelf mapping |
|
||||
| `sync-queue-item.webp` | Device sync in queue | `/queue` (device specific) |
|
||||
| Screenshot Name | Description | Page/Action |
|
||||
| --------------------------- | --------------------------------- | ----------------------------------- |
|
||||
| `device-list.webp` | Device management page | `/devices` |
|
||||
| `add-device-modal.webp` | Add new device modal | `/devices` → Click "Add New Device" |
|
||||
| `device-form-kobo.webp` | Kobo device registration form | `/devices` → Select Kobo type |
|
||||
| `device-form-koreader.webp` | KOReader device registration form | `/devices` → Select KOReader type |
|
||||
| `device-qr-code.webp` | QR code for device approval | After device registration |
|
||||
| `device-approved.webp` | Device approved confirmation | After approving device |
|
||||
| `device-sync-settings.webp` | Sync configuration for device | `/devices` → Click device settings |
|
||||
| `shelf-mapping.webp` | Collection to shelf mapping | `/devices` → Click shelf mapping |
|
||||
| `sync-queue-item.webp` | Device sync in queue | `/queue` (device specific) |
|
||||
|
||||
**Estimated Screenshots**: ~9
|
||||
|
||||
### 4. Sync Guide Screenshots
|
||||
|
||||
**File**: `docs/user/sync-guide.md`
|
||||
|
||||
**Screenshots Needed**:
|
||||
|
||||
| Screenshot Name | Description | Page/Action |
|
||||
|-----------------|-------------|-------------|
|
||||
| `sync-conflicts.webp` | Conflicts list view | `/conflicts` |
|
||||
| `conflict-resolution.webp` | Resolve conflict dialog | `/conflicts` → Click resolve |
|
||||
| `unlinked-books.webp` | Unlinked books list | `/unlinked-books` |
|
||||
| `book-linking.webp` | Link book to metadata | `/unlinked-books` → Click link |
|
||||
| `sync-success.webp` | Successful sync indicator | Any page after sync |
|
||||
| Screenshot Name | Description | Page/Action |
|
||||
| -------------------------- | ------------------------- | ------------------------------ |
|
||||
| `sync-conflicts.webp` | Conflicts list view | `/conflicts` |
|
||||
| `conflict-resolution.webp` | Resolve conflict dialog | `/conflicts` → Click resolve |
|
||||
| `unlinked-books.webp` | Unlinked books list | `/unlinked-books` |
|
||||
| `book-linking.webp` | Link book to metadata | `/unlinked-books` → Click link |
|
||||
| `sync-success.webp` | Successful sync indicator | Any page after sync |
|
||||
|
||||
**Estimated Screenshots**: ~5
|
||||
|
||||
### 5. Settings Guide Screenshots
|
||||
|
||||
**File**: `docs/user/settings-guide.md`
|
||||
|
||||
**Screenshots Needed**:
|
||||
|
||||
| Screenshot Name | Description | Page/Action |
|
||||
|-----------------|-------------|-------------|
|
||||
| `profile-overview.webp` | Profile settings page | `/admin/profile` |
|
||||
| `update-username.webp` | Username change form | `/admin/profile` |
|
||||
| `update-email.webp` | Email change form | `/admin/profile` |
|
||||
| `change-password.webp` | Password change form | `/admin/profile` |
|
||||
| `theme-selector.webp` | Theme selection dropdown | `/admin/profile` (if implemented) |
|
||||
| Screenshot Name | Description | Page/Action |
|
||||
| ----------------------- | ------------------------ | --------------------------------- |
|
||||
| `profile-overview.webp` | Profile settings page | `/admin/profile` |
|
||||
| `update-username.webp` | Username change form | `/admin/profile` |
|
||||
| `update-email.webp` | Email change form | `/admin/profile` |
|
||||
| `change-password.webp` | Password change form | `/admin/profile` |
|
||||
| `theme-selector.webp` | Theme selection dropdown | `/admin/profile` (if implemented) |
|
||||
|
||||
**Estimated Screenshots**: ~5
|
||||
|
||||
@@ -228,24 +250,24 @@ Before running the screenshot automation, ask the user:
|
||||
### `screenshots/config.ts` - Playwright Configuration
|
||||
|
||||
```typescript
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './',
|
||||
testDir: "./",
|
||||
fullyParallel: false,
|
||||
retries: 1,
|
||||
reporter: 'list',
|
||||
reporter: "list",
|
||||
use: {
|
||||
baseURL: process.env.BASE_URL || 'http://localhost:8765',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
baseURL: process.env.BASE_URL || "http://localhost:8765",
|
||||
trace: "on-first-retry",
|
||||
screenshot: "only-on-failure",
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium-desktop',
|
||||
name: "chromium-desktop",
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
viewport: { width: 1920, height: 1080 }
|
||||
...devices["Desktop Chrome"],
|
||||
viewport: { width: 1920, height: 1080 },
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -255,70 +277,79 @@ export default defineConfig({
|
||||
### `screenshots/auth.spec.ts` - Authentication Screenshots
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe('Authentication Screenshots', () => {
|
||||
test('Login page', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
test.describe("Authentication Screenshots", () => {
|
||||
test("Login page", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/user/login-page.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/user/login-page.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('User login flow', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
test("User login flow", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await page.fill('input[name="login"]', process.env.USER_USERNAME || 'user');
|
||||
await page.fill('input[name="password"]', process.env.USER_PASSWORD || 'password');
|
||||
await page.fill('input[name="login"]', process.env.USER_USERNAME || "user");
|
||||
await page.fill(
|
||||
'input[name="password"]',
|
||||
process.env.USER_PASSWORD || "password",
|
||||
);
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/user/login-form-filled.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/user/login-form-filled.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL('/dashboard', { timeout: 5000 });
|
||||
await page.waitForURL("/dashboard", { timeout: 5000 });
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/user/dashboard-after-login.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/user/dashboard-after-login.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('Admin login flow', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
test("Admin login flow", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await page.fill('input[name="login"]', process.env.ADMIN_USERNAME || 'admin');
|
||||
await page.fill('input[name="password"]', process.env.ADMIN_PASSWORD || 'password');
|
||||
await page.fill(
|
||||
'input[name="login"]',
|
||||
process.env.ADMIN_USERNAME || "admin",
|
||||
);
|
||||
await page.fill(
|
||||
'input[name="password"]',
|
||||
process.env.ADMIN_PASSWORD || "password",
|
||||
);
|
||||
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL('/dashboard', { timeout: 5000 });
|
||||
await page.waitForURL("/dashboard", { timeout: 5000 });
|
||||
|
||||
await page.goto('/admin');
|
||||
await page.goto("/admin");
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/admin/admin-dashboard.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/admin/admin-dashboard.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('Registration page', async ({ page }) => {
|
||||
await page.goto('/register');
|
||||
test("Registration page", async ({ page }) => {
|
||||
await page.goto("/register");
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/user/registration-page.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/user/registration-page.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
await page.fill('input[name="email"]', 'newuser@example.com');
|
||||
await page.fill('input[name="username"]', 'newuser');
|
||||
await page.fill('input[name="password"]', 'SecurePass123!');
|
||||
await page.fill('input[name="email"]', "newuser@example.com");
|
||||
await page.fill('input[name="username"]', "newuser");
|
||||
await page.fill('input[name="password"]', "SecurePass123!");
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/user/registration-form-filled.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/user/registration-form-filled.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -327,112 +358,120 @@ test.describe('Authentication Screenshots', () => {
|
||||
### `screenshots/user-workflows.spec.ts` - User Guide Screenshots
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe('User Guide Screenshots', () => {
|
||||
test.describe("User Guide Screenshots", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.fill('input[name="login"]', process.env.USER_USERNAME || 'user');
|
||||
await page.fill('input[name="password"]', process.env.USER_PASSWORD || 'password');
|
||||
await page.goto("/");
|
||||
await page.fill('input[name="login"]', process.env.USER_USERNAME || "user");
|
||||
await page.fill(
|
||||
'input[name="password"]',
|
||||
process.env.USER_PASSWORD || "password",
|
||||
);
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL('/dashboard', { timeout: 5000 });
|
||||
await page.waitForURL("/dashboard", { timeout: 5000 });
|
||||
});
|
||||
|
||||
test('Dashboard overview', async ({ page }) => {
|
||||
await page.goto('/dashboard');
|
||||
test("Dashboard overview", async ({ page }) => {
|
||||
await page.goto("/dashboard");
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/user/dashboard-overview.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/user/dashboard-overview.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('Library grid view', async ({ page }) => {
|
||||
await page.goto('/dashboard');
|
||||
test("Library grid view", async ({ page }) => {
|
||||
await page.goto("/dashboard");
|
||||
|
||||
await page.waitForSelector('#libraries-container', { timeout: 5000 });
|
||||
await page.waitForSelector("#libraries-container", { timeout: 5000 });
|
||||
|
||||
const firstLibrary = page.locator('[data-library]').first();
|
||||
const firstLibrary = page.locator("[data-library]").first();
|
||||
if (await firstLibrary.isVisible()) {
|
||||
await firstLibrary.click();
|
||||
await page.waitForURL(/\/dashboard/, { timeout: 5000 });
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/user/library-grid.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/user/library-grid.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('Search functionality', async ({ page }) => {
|
||||
await page.goto('/dashboard');
|
||||
test("Search functionality", async ({ page }) => {
|
||||
await page.goto("/dashboard");
|
||||
|
||||
await page.waitForSelector('#search-input', { timeout: 5000 });
|
||||
await page.waitForSelector("#search-input", { timeout: 5000 });
|
||||
|
||||
await page.fill('#search-input', 'science');
|
||||
await page.fill("#search-input", "science");
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/user/search-results.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/user/search-results.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('Filter panel', async ({ page }) => {
|
||||
await page.goto('/dashboard');
|
||||
test("Filter panel", async ({ page }) => {
|
||||
await page.goto("/dashboard");
|
||||
|
||||
const filterPanel = page.locator('.filter-panel details');
|
||||
const filterPanel = page.locator(".filter-panel details");
|
||||
if (await filterPanel.isVisible()) {
|
||||
await filterPanel.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/user/filter-panel-open.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/user/filter-panel-open.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('Collections list', async ({ page }) => {
|
||||
await page.goto('/collections');
|
||||
test("Collections list", async ({ page }) => {
|
||||
await page.goto("/collections");
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/user/collections-list.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/user/collections-list.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('Create collection modal', async ({ page }) => {
|
||||
await page.goto('/collections');
|
||||
test("Create collection modal", async ({ page }) => {
|
||||
await page.goto("/collections");
|
||||
|
||||
await page.click('button:has-text("New Collection"), button:has-text("Create Your First Collection")');
|
||||
await page.click(
|
||||
'button:has-text("New Collection"), button:has-text("Create Your First Collection")',
|
||||
);
|
||||
|
||||
await page.waitForSelector('#create-modal', { state: 'visible', timeout: 5000 });
|
||||
await page.waitForSelector("#create-modal", {
|
||||
state: "visible",
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
await page.fill('#collection-name', 'My Reading List');
|
||||
await page.fill('#collection-description', 'Books I want to read');
|
||||
await page.fill("#collection-name", "My Reading List");
|
||||
await page.fill("#collection-description", "Books I want to read");
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/user/create-collection-modal.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/user/create-collection-modal.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('Reading progress view', async ({ page }) => {
|
||||
await page.goto('/progress');
|
||||
test("Reading progress view", async ({ page }) => {
|
||||
await page.goto("/progress");
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/user/reading-progress.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/user/reading-progress.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('Analytics view', async ({ page }) => {
|
||||
await page.goto('/analytics');
|
||||
test("Analytics view", async ({ page }) => {
|
||||
await page.goto("/analytics");
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/user/analytics-view.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/user/analytics-view.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -441,82 +480,92 @@ test.describe('User Guide Screenshots', () => {
|
||||
### `screenshots/admin-workflows.spec.ts` - Admin Guide Screenshots
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe('Admin Guide Screenshots', () => {
|
||||
test.describe("Admin Guide Screenshots", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.fill('input[name="login"]', process.env.ADMIN_USERNAME || 'admin');
|
||||
await page.fill('input[name="password"]', process.env.ADMIN_PASSWORD || 'password');
|
||||
await page.goto("/");
|
||||
await page.fill(
|
||||
'input[name="login"]',
|
||||
process.env.ADMIN_USERNAME || "admin",
|
||||
);
|
||||
await page.fill(
|
||||
'input[name="password"]',
|
||||
process.env.ADMIN_PASSWORD || "password",
|
||||
);
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL('/dashboard', { timeout: 5000 });
|
||||
await page.waitForURL("/dashboard", { timeout: 5000 });
|
||||
});
|
||||
|
||||
test('Admin dashboard', async ({ page }) => {
|
||||
await page.goto('/admin');
|
||||
test("Admin dashboard", async ({ page }) => {
|
||||
await page.goto("/admin");
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/admin/admin-dashboard.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/admin/admin-dashboard.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('User management', async ({ page }) => {
|
||||
await page.goto('/admin');
|
||||
test("User management", async ({ page }) => {
|
||||
await page.goto("/admin");
|
||||
|
||||
const userSection = page.locator('[data-section="users"], text="Users"');
|
||||
if (await userSection.isVisible()) {
|
||||
await page.screenshot({
|
||||
path: 'docs/images/admin/user-management.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/admin/user-management.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('Library management', async ({ page }) => {
|
||||
await page.goto('/admin/library');
|
||||
test("Library management", async ({ page }) => {
|
||||
await page.goto("/admin/library");
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/admin/library-management.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/admin/library-management.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('Add library form', async ({ page }) => {
|
||||
await page.goto('/admin/library');
|
||||
test("Add library form", async ({ page }) => {
|
||||
await page.goto("/admin/library");
|
||||
|
||||
const addLibraryBtn = page.locator('button:has-text("Add Library"), button:has-text("Create Library")');
|
||||
const addLibraryBtn = page.locator(
|
||||
'button:has-text("Add Library"), button:has-text("Create Library")',
|
||||
);
|
||||
if (await addLibraryBtn.isVisible()) {
|
||||
await addLibraryBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/admin/add-library-form.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/admin/add-library-form.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('Profile settings', async ({ page }) => {
|
||||
await page.goto('/admin/profile');
|
||||
test("Profile settings", async ({ page }) => {
|
||||
await page.goto("/admin/profile");
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/admin/profile-settings.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/admin/profile-settings.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('Theme selector', async ({ page }) => {
|
||||
await page.goto('/admin/profile');
|
||||
test("Theme selector", async ({ page }) => {
|
||||
await page.goto("/admin/profile");
|
||||
|
||||
const themeSelect = page.locator('select[name="theme"], [data-theme-selector]');
|
||||
const themeSelect = page.locator(
|
||||
'select[name="theme"], [data-theme-selector]',
|
||||
);
|
||||
if (await themeSelect.isVisible()) {
|
||||
await themeSelect.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/admin/theme-selector.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/admin/theme-selector.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -526,83 +575,97 @@ test.describe('Admin Guide Screenshots', () => {
|
||||
### `screenshots/device-workflows.spec.ts` - Device Setup Screenshots
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe('Device Setup Screenshots', () => {
|
||||
test.describe("Device Setup Screenshots", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.fill('input[name="login"]', process.env.USER_USERNAME || 'user');
|
||||
await page.fill('input[name="password"]', process.env.USER_PASSWORD || 'password');
|
||||
await page.goto("/");
|
||||
await page.fill('input[name="login"]', process.env.USER_USERNAME || "user");
|
||||
await page.fill(
|
||||
'input[name="password"]',
|
||||
process.env.USER_PASSWORD || "password",
|
||||
);
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL('/dashboard', { timeout: 5000 });
|
||||
await page.waitForURL("/dashboard", { timeout: 5000 });
|
||||
});
|
||||
|
||||
test('Device list page', async ({ page }) => {
|
||||
await page.goto('/devices');
|
||||
test("Device list page", async ({ page }) => {
|
||||
await page.goto("/devices");
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/devices/device-list.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/devices/device-list.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('Add device modal', async ({ page }) => {
|
||||
await page.goto('/devices');
|
||||
test("Add device modal", async ({ page }) => {
|
||||
await page.goto("/devices");
|
||||
|
||||
await page.click('button:has-text("Add New Device"), button:has-text("Add Your First Device")');
|
||||
await page.click(
|
||||
'button:has-text("Add New Device"), button:has-text("Add Your First Device")',
|
||||
);
|
||||
|
||||
await page.waitForSelector('[data-modal="add-device"], #add-device-modal', { state: 'visible', timeout: 5000 });
|
||||
await page.waitForSelector('[data-modal="add-device"], #add-device-modal', {
|
||||
state: "visible",
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/devices/add-device-modal.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/devices/add-device-modal.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('Kobo device form', async ({ page }) => {
|
||||
await page.goto('/devices');
|
||||
test("Kobo device form", async ({ page }) => {
|
||||
await page.goto("/devices");
|
||||
|
||||
await page.click('button:has-text("Add New Device")');
|
||||
|
||||
await page.waitForSelector('[data-modal="add-device"]', { state: 'visible', timeout: 5000 });
|
||||
await page.waitForSelector('[data-modal="add-device"]', {
|
||||
state: "visible",
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
const deviceTypeSelect = page.locator('select[name="device_type"]');
|
||||
if (await deviceTypeSelect.isVisible()) {
|
||||
await deviceTypeSelect.selectOption('kobo');
|
||||
await deviceTypeSelect.selectOption("kobo");
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/devices/device-form-kobo.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/devices/device-form-kobo.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('KOReader device form', async ({ page }) => {
|
||||
await page.goto('/devices');
|
||||
test("KOReader device form", async ({ page }) => {
|
||||
await page.goto("/devices");
|
||||
|
||||
await page.click('button:has-text("Add New Device")');
|
||||
|
||||
await page.waitForSelector('[data-modal="add-device"]', { state: 'visible', timeout: 5000 });
|
||||
await page.waitForSelector('[data-modal="add-device"]', {
|
||||
state: "visible",
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
const deviceTypeSelect = page.locator('select[name="device_type"]');
|
||||
if (await deviceTypeSelect.isVisible()) {
|
||||
await deviceTypeSelect.selectOption('koreader');
|
||||
await deviceTypeSelect.selectOption("koreader");
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/devices/device-form-koreader.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/devices/device-form-koreader.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('Sync queue', async ({ page }) => {
|
||||
await page.goto('/queue');
|
||||
test("Sync queue", async ({ page }) => {
|
||||
await page.goto("/queue");
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/devices/sync-queue.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/devices/sync-queue.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -611,32 +674,35 @@ test.describe('Device Setup Screenshots', () => {
|
||||
### `screenshots/sync-workflows.spec.ts` - Sync Guide Screenshots
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe('Sync Guide Screenshots', () => {
|
||||
test.describe("Sync Guide Screenshots", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.fill('input[name="login"]', process.env.USER_USERNAME || 'user');
|
||||
await page.fill('input[name="password"]', process.env.USER_PASSWORD || 'password');
|
||||
await page.goto("/");
|
||||
await page.fill('input[name="login"]', process.env.USER_USERNAME || "user");
|
||||
await page.fill(
|
||||
'input[name="password"]',
|
||||
process.env.USER_PASSWORD || "password",
|
||||
);
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL('/dashboard', { timeout: 5000 });
|
||||
await page.waitForURL("/dashboard", { timeout: 5000 });
|
||||
});
|
||||
|
||||
test('Sync conflicts page', async ({ page }) => {
|
||||
await page.goto('/conflicts');
|
||||
test("Sync conflicts page", async ({ page }) => {
|
||||
await page.goto("/conflicts");
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/sync/sync-conflicts.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/sync/sync-conflicts.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('Unlinked books page', async ({ page }) => {
|
||||
await page.goto('/unlinked-books');
|
||||
test("Unlinked books page", async ({ page }) => {
|
||||
await page.goto("/unlinked-books");
|
||||
|
||||
await page.screenshot({
|
||||
path: 'docs/images/sync/unlinked-books.webp',
|
||||
fullPage: true
|
||||
path: "docs/images/sync/unlinked-books.webp",
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -645,21 +711,25 @@ test.describe('Sync Guide Screenshots', () => {
|
||||
## Running the Tests
|
||||
|
||||
### Run all tests
|
||||
|
||||
```bash
|
||||
npx playwright test
|
||||
```
|
||||
|
||||
### Run specific test file
|
||||
|
||||
```bash
|
||||
npx playwright test auth.spec.ts
|
||||
```
|
||||
|
||||
### Run in headed mode (see browser)
|
||||
|
||||
```bash
|
||||
npx playwright test --headed
|
||||
```
|
||||
|
||||
### Run with debug mode
|
||||
|
||||
```bash
|
||||
npx playwright test --debug
|
||||
```
|
||||
@@ -667,10 +737,13 @@ npx playwright test --debug
|
||||
## Markdown Update Strategy
|
||||
|
||||
### Option 1: Create New Markdown
|
||||
|
||||
Generate fresh markdown files with embedded screenshots.
|
||||
|
||||
### Option 2: Update Existing Markdown
|
||||
|
||||
Update existing markdown files by:
|
||||
|
||||
1. Finding section headers
|
||||
2. Inserting screenshot references after relevant steps
|
||||
3. Using alt text to describe what's shown
|
||||
@@ -690,25 +763,30 @@ Update existing markdown files by:
|
||||
## Workflow When Ready
|
||||
|
||||
### Step 1: Verify Frontend Complete
|
||||
|
||||
- Check all pages listed in "Prerequisites" are working
|
||||
- Confirm no TODO placeholders in templates
|
||||
|
||||
### Step 2: Seed Test Data
|
||||
|
||||
```bash
|
||||
# Add sample books, collections, devices
|
||||
# Create test admin and user accounts
|
||||
```
|
||||
|
||||
### Step 3: Set Up Playwright
|
||||
|
||||
```bash
|
||||
npm install -D @playwright/test
|
||||
npx playwright install chromium
|
||||
```
|
||||
|
||||
### Step 4: Create Environment File
|
||||
|
||||
Create `.env.screenshots` with the test credentials
|
||||
|
||||
### Step 5: Run Screenshot Scripts
|
||||
|
||||
```bash
|
||||
# Run all screenshot workflows
|
||||
npx playwright test
|
||||
@@ -719,6 +797,7 @@ npx playwright test user-workflows.spec.ts
|
||||
```
|
||||
|
||||
### Step 6: Review and Adjust
|
||||
|
||||
- Manually review screenshots
|
||||
- Retake any that need adjustment
|
||||
- Update markdown files if needed
|
||||
@@ -742,15 +821,15 @@ When you're ready to run these, you may need to update:
|
||||
|
||||
## Estimated Time Investment
|
||||
|
||||
| Task | Time |
|
||||
|------|------|
|
||||
| Install & configure Playwright | 15 min |
|
||||
| Seed test database | 30 min |
|
||||
| Write Playwright scripts | 2-3 hours |
|
||||
| Run screenshot automation | 10 min |
|
||||
| Review & retake screenshots | 30-60 min |
|
||||
| Update markdown files | 30-60 min |
|
||||
| **Total** | **4-6 hours** |
|
||||
| Task | Time |
|
||||
| ------------------------------ | ------------- |
|
||||
| Install & configure Playwright | 15 min |
|
||||
| Seed test database | 30 min |
|
||||
| Write Playwright scripts | 2-3 hours |
|
||||
| Run screenshot automation | 10 min |
|
||||
| Review & retake screenshots | 30-60 min |
|
||||
| Update markdown files | 30-60 min |
|
||||
| **Total** | **4-6 hours** |
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+85
-46
@@ -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)
|
||||
@@ -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 {
|
||||
// ...
|
||||
@@ -332,6 +348,7 @@ 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 {
|
||||
// ...
|
||||
@@ -349,6 +366,7 @@ func (h *OPDSHandler) GetCoverImage(c echo.Context) error {
|
||||
```
|
||||
|
||||
**New code**:
|
||||
|
||||
```go
|
||||
func (h *OPDSHandler) GetCoverImage(c echo.Context) error {
|
||||
// ...
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -949,7 +986,7 @@ 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: |-
|
||||
@@ -989,7 +1026,7 @@ 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: |-
|
||||
@@ -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
|
||||
|
||||
@@ -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,10 +1003,12 @@ 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"
|
||||
@@ -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
|
||||
|
||||
@@ -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,24 +18,24 @@ 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
|
||||
|
||||
@@ -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) |
|
||||
| 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
|
||||
**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