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`
|
**File:** `internal/services/media_scanner.go`
|
||||||
|
|
||||||
**Current code (around line 348-360):**
|
**Current code (around line 348-360):**
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func (s *MediaScanner) processMediaFile(ctx context.Context, path string) (bool, error) {
|
func (s *MediaScanner) processMediaFile(ctx context.Context, path string) (bool, error) {
|
||||||
// Get file info
|
// Get file info
|
||||||
@@ -27,6 +28,7 @@ func (s *MediaScanner) processMediaFile(ctx context.Context, path string) (bool,
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Add AFTER line 357 (after getting file info, before existingItem check):**
|
**Add AFTER line 357 (after getting file info, before existingItem check):**
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Get file modification time for created_at
|
// Get file modification time for created_at
|
||||||
fileModTime := info.ModTime()
|
fileModTime := info.ModTime()
|
||||||
@@ -37,6 +39,7 @@ func (s *MediaScanner) processMediaFile(ctx context.Context, path string) (bool,
|
|||||||
**File:** `internal/database/queries/queries.sql`
|
**File:** `internal/database/queries/queries.sql`
|
||||||
|
|
||||||
**Current code (line 131-133):**
|
**Current code (line 131-133):**
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- name: CreateMediaItem :one
|
-- name: CreateMediaItem :one
|
||||||
INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publisher, contributors, contributors_search, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id)
|
INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publisher, contributors, contributors_search, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id)
|
||||||
@@ -45,6 +48,7 @@ RETURNING *;
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Change TO:**
|
**Change TO:**
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- name: CreateMediaItem :one
|
-- name: CreateMediaItem :one
|
||||||
INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publisher, contributors, contributors_search, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at)
|
INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publisher, contributors, contributors_search, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at)
|
||||||
@@ -55,6 +59,7 @@ RETURNING *;
|
|||||||
### Step 1.3: Regenerate Go code from SQL OR manually update queries.sql.go
|
### Step 1.3: Regenerate Go code from SQL OR manually update queries.sql.go
|
||||||
|
|
||||||
**Option A - Run SQL generation (recommended):**
|
**Option A - Run SQL generation (recommended):**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd internal/database && go generate ./...
|
cd internal/database && go generate ./...
|
||||||
```
|
```
|
||||||
@@ -66,6 +71,7 @@ cd internal/database && go generate ./...
|
|||||||
**Find `CreateMediaItemParams` struct (around line 557):**
|
**Find `CreateMediaItemParams` struct (around line 557):**
|
||||||
|
|
||||||
**Add to struct (after AddedByAdminID):**
|
**Add to struct (after AddedByAdminID):**
|
||||||
|
|
||||||
```go
|
```go
|
||||||
CreatedAt pgtype.Timestamp `db:"created_at" json:"created_at"`
|
CreatedAt pgtype.Timestamp `db:"created_at" json:"created_at"`
|
||||||
```
|
```
|
||||||
@@ -73,6 +79,7 @@ CreatedAt pgtype.Timestamp `db:"created_at" json:"created_at"`
|
|||||||
**Find `CreateMediaItem` function (around line 588):**
|
**Find `CreateMediaItem` function (around line 588):**
|
||||||
|
|
||||||
**Add to the query parameters (after AddedByAdminID in the VALUES):**
|
**Add to the query parameters (after AddedByAdminID in the VALUES):**
|
||||||
|
|
||||||
```go
|
```go
|
||||||
arg.CreatedAt,
|
arg.CreatedAt,
|
||||||
```
|
```
|
||||||
@@ -84,6 +91,7 @@ arg.CreatedAt,
|
|||||||
**Find the CreateMediaItem call** - around line 512.
|
**Find the CreateMediaItem call** - around line 512.
|
||||||
|
|
||||||
**Current code (line 512-532):**
|
**Current code (line 512-532):**
|
||||||
|
|
||||||
```go
|
```go
|
||||||
createdItem, err := s.db.CreateMediaItem(ctx, database.CreateMediaItemParams{
|
createdItem, err := s.db.CreateMediaItem(ctx, database.CreateMediaItemParams{
|
||||||
LibraryID: libraryID,
|
LibraryID: libraryID,
|
||||||
@@ -93,11 +101,13 @@ createdItem, err := s.db.CreateMediaItem(ctx, database.CreateMediaItemParams{
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Add to the params (after AddedByAdminID):**
|
**Add to the params (after AddedByAdminID):**
|
||||||
|
|
||||||
```go
|
```go
|
||||||
CreatedAt: pgtype.Timestamp{Time: fileModTime, Valid: true},
|
CreatedAt: pgtype.Timestamp{Time: fileModTime, Valid: true},
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note:** You'll need to import `"github.com/jackc/pgx/v5/pgtype"` if not already present.
|
**Note:** You'll need to import `"github.com/jackc/pgx/v5/pgtype"` if not already present.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
CreatedAt: pgtype.Timestamp{Time: fileModTime, Valid: true},
|
CreatedAt: pgtype.Timestamp{Time: fileModTime, Valid: true},
|
||||||
```
|
```
|
||||||
@@ -115,6 +125,7 @@ createdItem, err := s.db.CreateMediaItem(ctx, database.CreateMediaItemParams{
|
|||||||
**File:** `internal/services/media_scanner.go`
|
**File:** `internal/services/media_scanner.go`
|
||||||
|
|
||||||
**Current code (around lines 365-372):**
|
**Current code (around lines 365-372):**
|
||||||
|
|
||||||
```go
|
```go
|
||||||
if s.forceRescan {
|
if s.forceRescan {
|
||||||
fmt.Printf("Force rescan enabled, re-processing existing media item: %s\n", path)
|
fmt.Printf("Force rescan enabled, re-processing existing media item: %s\n", path)
|
||||||
@@ -129,6 +140,7 @@ if s.forceRescan {
|
|||||||
### Step 2.2: Replace DELETE+INSERT with UPDATE
|
### Step 2.2: Replace DELETE+INSERT with UPDATE
|
||||||
|
|
||||||
**Replace lines 365-372 with:**
|
**Replace lines 365-372 with:**
|
||||||
|
|
||||||
```go
|
```go
|
||||||
if s.forceRescan {
|
if s.forceRescan {
|
||||||
fmt.Printf("Force rescan enabled, updating existing media item: %s\n", path)
|
fmt.Printf("Force rescan enabled, updating existing media item: %s\n", path)
|
||||||
@@ -155,6 +167,7 @@ if s.forceRescan {
|
|||||||
**Goal:** Prevent cross-library interference - scanning one library shouldn't affect another library's entries.
|
**Goal:** Prevent cross-library interference - scanning one library shouldn't affect another library's entries.
|
||||||
|
|
||||||
**Important Context:** The `libraryID` is already available in `StartWatchModeForLibrary` at `scanner.go:385`:
|
**Important Context:** The `libraryID` is already available in `StartWatchModeForLibrary` at `scanner.go:385`:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
h.StartWatchModeForLibrary(ctx, library.ID, library.CreatedByAdminID)
|
h.StartWatchModeForLibrary(ctx, library.ID, library.CreatedByAdminID)
|
||||||
```
|
```
|
||||||
@@ -168,6 +181,7 @@ The function receives `libraryID` but doesn't store it in the scanner. We need t
|
|||||||
**Find the SetAdminID function (around line 104):**
|
**Find the SetAdminID function (around line 104):**
|
||||||
|
|
||||||
**Add AFTER SetAdminID:**
|
**Add AFTER SetAdminID:**
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func (s *MediaScanner) SetLibraryID(libraryID pgtype.UUID) {
|
func (s *MediaScanner) SetLibraryID(libraryID pgtype.UUID) {
|
||||||
s.defaultLibraryID = libraryID
|
s.defaultLibraryID = libraryID
|
||||||
@@ -179,12 +193,14 @@ func (s *MediaScanner) SetLibraryID(libraryID pgtype.UUID) {
|
|||||||
**File:** `internal/handlers/scanner.go`
|
**File:** `internal/handlers/scanner.go`
|
||||||
|
|
||||||
**Current code (around line 268):**
|
**Current code (around line 268):**
|
||||||
|
|
||||||
```go
|
```go
|
||||||
scanner.SetAdminID(adminID)
|
scanner.SetAdminID(adminID)
|
||||||
scanner.WatchChanges(h.watchModeCtx)
|
scanner.WatchChanges(h.watchModeCtx)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Add AFTER line 268:**
|
**Add AFTER line 268:**
|
||||||
|
|
||||||
```go
|
```go
|
||||||
scanner.SetLibraryID(libraryID)
|
scanner.SetLibraryID(libraryID)
|
||||||
```
|
```
|
||||||
@@ -194,12 +210,14 @@ scanner.SetLibraryID(libraryID)
|
|||||||
**File:** `internal/database/queries/queries.sql`
|
**File:** `internal/database/queries/queries.sql`
|
||||||
|
|
||||||
**Current code (line 302-303):**
|
**Current code (line 302-303):**
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- name: GetMediaItemByFilePath :one
|
-- name: GetMediaItemByFilePath :one
|
||||||
SELECT * FROM media_items WHERE file_path = $1;
|
SELECT * FROM media_items WHERE file_path = $1;
|
||||||
```
|
```
|
||||||
|
|
||||||
**Change TO:**
|
**Change TO:**
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- name: GetMediaItemByFilePath :one
|
-- name: GetMediaItemByFilePath :one
|
||||||
SELECT * FROM media_items WHERE file_path = $1 AND library_id = $2;
|
SELECT * FROM media_items WHERE file_path = $1 AND library_id = $2;
|
||||||
@@ -210,10 +228,12 @@ SELECT * FROM media_items WHERE file_path = $1 AND library_id = $2;
|
|||||||
**File:** `internal/database/queries.sql.go`
|
**File:** `internal/database/queries.sql.go`
|
||||||
|
|
||||||
Find `GetMediaItemByFilePath` function and update:
|
Find `GetMediaItemByFilePath` function and update:
|
||||||
|
|
||||||
1. Add `LibraryID pgtype.UUID` parameter to the function and params struct
|
1. Add `LibraryID pgtype.UUID` parameter to the function and params struct
|
||||||
2. Add the parameter to the query call
|
2. Add the parameter to the query call
|
||||||
|
|
||||||
**OR run SQL generation:**
|
**OR run SQL generation:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd internal/database && go generate ./...
|
cd internal/database && go generate ./...
|
||||||
```
|
```
|
||||||
@@ -223,6 +243,7 @@ cd internal/database && go generate ./...
|
|||||||
**File:** `internal/services/media_scanner.go`
|
**File:** `internal/services/media_scanner.go`
|
||||||
|
|
||||||
**Current code (lines 1395-1397):**
|
**Current code (lines 1395-1397):**
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func (s *MediaScanner) getMediaItemByFilePath(ctx context.Context, filePath string) (database.MediaItems, error) {
|
func (s *MediaScanner) getMediaItemByFilePath(ctx context.Context, filePath string) (database.MediaItems, error) {
|
||||||
return s.db.GetMediaItemByFilePath(ctx, filePath)
|
return s.db.GetMediaItemByFilePath(ctx, filePath)
|
||||||
@@ -230,6 +251,7 @@ func (s *MediaScanner) getMediaItemByFilePath(ctx context.Context, filePath stri
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Change TO:**
|
**Change TO:**
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func (s *MediaScanner) getMediaItemByFilePath(ctx context.Context, filePath string) (database.MediaItems, error) {
|
func (s *MediaScanner) getMediaItemByFilePath(ctx context.Context, filePath string) (database.MediaItems, error) {
|
||||||
return s.db.GetMediaItemByFilePath(ctx, filePath, s.defaultLibraryID)
|
return s.db.GetMediaItemByFilePath(ctx, filePath, s.defaultLibraryID)
|
||||||
@@ -241,6 +263,7 @@ func (s *MediaScanner) getMediaItemByFilePath(ctx context.Context, filePath stri
|
|||||||
**File:** `internal/services/media_scanner.go`
|
**File:** `internal/services/media_scanner.go`
|
||||||
|
|
||||||
Update all places that call `getMediaItemByFilePath` to pass the libraryID:
|
Update all places that call `getMediaItemByFilePath` to pass the libraryID:
|
||||||
|
|
||||||
- Line 361: In `processMediaFile` - already has access to libraryID via folder lookup
|
- Line 361: In `processMediaFile` - already has access to libraryID via folder lookup
|
||||||
|
|
||||||
**Note:** The `processMediaFile` function already determines libraryID from the folder path (lines 487-501). Use that libraryID instead of `s.defaultLibraryID` for better accuracy.
|
**Note:** The `processMediaFile` function already determines libraryID from the folder path (lines 487-501). Use that libraryID instead of `s.defaultLibraryID` for better accuracy.
|
||||||
@@ -381,9 +404,11 @@ func (l *ScannerLogger) Close() {
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Add to MediaScanner struct:**
|
**Add to MediaScanner struct:**
|
||||||
|
|
||||||
- Add `logger *ScannerLogger` field to track logger instance
|
- Add `logger *ScannerLogger` field to track logger instance
|
||||||
|
|
||||||
**Update NewMediaScanner function:**
|
**Update NewMediaScanner function:**
|
||||||
|
|
||||||
- Initialize logger: `logger: NewScannerLogger()`
|
- Initialize logger: `logger: NewScannerLogger()`
|
||||||
|
|
||||||
### Step 4.1: Ensure libraryID is available in scanner
|
### Step 4.1: Ensure libraryID is available in scanner
|
||||||
@@ -397,6 +422,7 @@ func (l *ScannerLogger) Close() {
|
|||||||
**Find:** `WatchChanges` function (around line 1427).
|
**Find:** `WatchChanges` function (around line 1427).
|
||||||
|
|
||||||
**Find the event handling section (around lines 1449-1455):**
|
**Find the event handling section (around lines 1449-1455):**
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Handle file modifications and creations
|
// Handle file modifications and creations
|
||||||
if (event.Has(fsnotify.Create) || event.Has(fsnotify.Write)) && s.isScannableFile(event.Name) {
|
if (event.Has(fsnotify.Create) || event.Has(fsnotify.Write)) && s.isScannableFile(event.Name) {
|
||||||
@@ -408,6 +434,7 @@ if (event.Has(fsnotify.Create) || event.Has(fsnotify.Write)) && s.isScannableFil
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Add AFTER that block (before line 1457):**
|
**Add AFTER that block (before line 1457):**
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Handle file deletions
|
// Handle file deletions
|
||||||
if event.Has(fsnotify.Remove) && s.isScannableFile(event.Name) {
|
if event.Has(fsnotify.Remove) && s.isScannableFile(event.Name) {
|
||||||
@@ -468,6 +495,7 @@ if event.Has(fsnotify.Remove) && s.isScannableFile(event.Name) {
|
|||||||
**Find:** End of `ScanFolders` function (after line 242).
|
**Find:** End of `ScanFolders` function (after line 242).
|
||||||
|
|
||||||
**Current code (around line 242-248):**
|
**Current code (around line 242-248):**
|
||||||
|
|
||||||
```go
|
```go
|
||||||
fmt.Printf("Scan completed: %d total files scanned, %d media files found, %d new items, %d errors\n",
|
fmt.Printf("Scan completed: %d total files scanned, %d media files found, %d new items, %d errors\n",
|
||||||
processedFiles, mediaFiles, s.newItems, s.errors)
|
processedFiles, mediaFiles, s.newItems, s.errors)
|
||||||
@@ -480,6 +508,7 @@ return nil
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Add BEFORE `return nil`:**
|
**Add BEFORE `return nil`:**
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Clean up: Find media items in DB that no longer exist on filesystem
|
// Clean up: Find media items in DB that no longer exist on filesystem
|
||||||
for _, folder := range s.folders {
|
for _, folder := range s.folders {
|
||||||
@@ -533,6 +562,7 @@ for _, folder := range s.folders {
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Log Files Location:** `/app/logs/`
|
**Log Files Location:** `/app/logs/`
|
||||||
|
|
||||||
- `scanner-deletes-YYYY-MM-DD.log` - All deletion events (watch mode + rescan)
|
- `scanner-deletes-YYYY-MM-DD.log` - All deletion events (watch mode + rescan)
|
||||||
- `scanner-errors-YYYY-MM-DD.log` - All error events
|
- `scanner-errors-YYYY-MM-DD.log` - All error events
|
||||||
- Rotation: Daily, keeps 7 days of history
|
- Rotation: Daily, keeps 7 days of history
|
||||||
@@ -543,11 +573,13 @@ for _, folder := range s.folders {
|
|||||||
## Verification Steps After Implementation
|
## Verification Steps After Implementation
|
||||||
|
|
||||||
1. **Compile the code:**
|
1. **Compile the code:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go build ./...
|
go build ./...
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Run tests:**
|
2. **Run tests:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go test ./... -v
|
go test ./... -v
|
||||||
```
|
```
|
||||||
@@ -571,6 +603,7 @@ for _, folder := range s.folders {
|
|||||||
## Safety Checks (IMPORTANT)
|
## Safety Checks (IMPORTANT)
|
||||||
|
|
||||||
### Prevent deleting ALL books:
|
### Prevent deleting ALL books:
|
||||||
|
|
||||||
- The cleanup logic MUST check `scannedPaths[filePath]` - this ensures we only delete items whose paths were NOT found during the filesystem scan
|
- The cleanup logic MUST check `scannedPaths[filePath]` - this ensures we only delete items whose paths were NOT found during the filesystem scan
|
||||||
- The key condition is: `if filePath != "" && !scannedPaths[filePath]` - meaning "if this file was NOT found in our scan, delete it"
|
- The key condition is: `if filePath != "" && !scannedPaths[filePath]` - meaning "if this file was NOT found in our scan, delete it"
|
||||||
- This is correct because:
|
- This is correct because:
|
||||||
@@ -579,6 +612,7 @@ for _, folder := range s.folders {
|
|||||||
3. We compare → only delete if DB path is NOT in filesystem paths
|
3. We compare → only delete if DB path is NOT in filesystem paths
|
||||||
|
|
||||||
### Before running against production:
|
### Before running against production:
|
||||||
|
|
||||||
- Test with a small subset of books first
|
- Test with a small subset of books first
|
||||||
- Verify the delete queries target specific library_id (not all libraries)
|
- Verify the delete queries target specific library_id (not all libraries)
|
||||||
- Check logs show only expected deletions
|
- Check logs show only expected deletions
|
||||||
@@ -587,20 +621,20 @@ for _, folder := range s.folders {
|
|||||||
|
|
||||||
## Files to Modify
|
## Files to Modify
|
||||||
|
|
||||||
| Fix | File | Changes |
|
| Fix | File | Changes |
|
||||||
|-----|------|---------|
|
| --- | --------------------------------------- | -------------------------------------------------------- |
|
||||||
| 1a | `internal/database/queries/queries.sql` | Add created_at to INSERT columns |
|
| 1a | `internal/database/queries/queries.sql` | Add created_at to INSERT columns |
|
||||||
| 1b | `internal/database/queries.sql.go` | Add CreatedAt to CreateMediaItemParams struct + query |
|
| 1b | `internal/database/queries.sql.go` | Add CreatedAt to CreateMediaItemParams struct + query |
|
||||||
| 1c | `internal/services/media_scanner.go` | Get file.ModTime() + pass to CreateMediaItem |
|
| 1c | `internal/services/media_scanner.go` | Get file.ModTime() + pass to CreateMediaItem |
|
||||||
| 2 | `internal/services/media_scanner.go` | Change force rescan from DELETE+INSERT to UPDATE |
|
| 2 | `internal/services/media_scanner.go` | Change force rescan from DELETE+INSERT to UPDATE |
|
||||||
| 3a | `internal/services/media_scanner.go` | Add SetLibraryID() method |
|
| 3a | `internal/services/media_scanner.go` | Add SetLibraryID() method |
|
||||||
| 3b | `internal/handlers/scanner.go` | Call scanner.SetLibraryID(libraryID) |
|
| 3b | `internal/handlers/scanner.go` | Call scanner.SetLibraryID(libraryID) |
|
||||||
| 3c | `internal/database/queries/queries.sql` | Add library_id to WHERE clause |
|
| 3c | `internal/database/queries/queries.sql` | Add library_id to WHERE clause |
|
||||||
| 3d | `internal/database/queries.sql.go` | Update GetMediaItemByFilePath params |
|
| 3d | `internal/database/queries.sql.go` | Update GetMediaItemByFilePath params |
|
||||||
| 3e | `internal/services/media_scanner.go` | Pass libraryID to getMediaItemByFilePath |
|
| 3e | `internal/services/media_scanner.go` | Pass libraryID to getMediaItemByFilePath |
|
||||||
| 4.0 | `internal/services/scanner_logger.go` | **NEW FILE** - File logging infrastructure |
|
| 4.0 | `internal/services/scanner_logger.go` | **NEW FILE** - File logging infrastructure |
|
||||||
| 4a | `internal/services/media_scanner.go` | Add fsnotify.Remove handler in WatchChanges + use logger |
|
| 4a | `internal/services/media_scanner.go` | Add fsnotify.Remove handler in WatchChanges + use logger |
|
||||||
| 4b | `internal/services/media_scanner.go` | Add cleanup loop in ScanFolders + use logger |
|
| 4b | `internal/services/media_scanner.go` | Add cleanup loop in ScanFolders + use logger |
|
||||||
|
|
||||||
### Docker Compose Volume Mount
|
### Docker Compose Volume Mount
|
||||||
|
|
||||||
@@ -618,6 +652,7 @@ volumes:
|
|||||||
After editing `queries.sql`, you MUST regenerate the Go code:
|
After editing `queries.sql`, you MUST regenerate the Go code:
|
||||||
|
|
||||||
**Option A - Run SQL code generation (recommended):**
|
**Option A - Run SQL code generation (recommended):**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd internal/database && go generate ./...
|
cd internal/database && go generate ./...
|
||||||
```
|
```
|
||||||
@@ -625,10 +660,12 @@ cd internal/database && go generate ./...
|
|||||||
**Option B - Manual update (if Option A fails):**
|
**Option B - Manual update (if Option A fails):**
|
||||||
|
|
||||||
If `go generate` fails or is not available, manually update `queries.sql.go`:
|
If `go generate` fails or is not available, manually update `queries.sql.go`:
|
||||||
|
|
||||||
1. Add `LibraryID pgtype.UUID` parameter to `GetMediaItemByFilePathParams` struct
|
1. Add `LibraryID pgtype.UUID` parameter to `GetMediaItemByFilePathParams` struct
|
||||||
2. Add parameter to the query function call
|
2. Add parameter to the query function call
|
||||||
|
|
||||||
For Fix 1, manually add `CreatedAt` to:
|
For Fix 1, manually add `CreatedAt` to:
|
||||||
|
|
||||||
- `CreateMediaItemParams` struct
|
- `CreateMediaItemParams` struct
|
||||||
- The query VALUES
|
- The query VALUES
|
||||||
|
|
||||||
@@ -648,11 +685,13 @@ For Fix 1, manually add `CreatedAt` to:
|
|||||||
### Existing Tests Analysis
|
### Existing Tests Analysis
|
||||||
|
|
||||||
**Current scanner integration tests** (`cmd/server/tests/scanner_integration_test.go`):
|
**Current scanner integration tests** (`cmd/server/tests/scanner_integration_test.go`):
|
||||||
|
|
||||||
- Uses `/app/uploads` as test folder
|
- Uses `/app/uploads` as test folder
|
||||||
- Tests scan, progress tracking, watch mode start/stop
|
- Tests scan, progress tracking, watch mode start/stop
|
||||||
- **After Fix 3:** Tests can safely use `/app/uploads` because GetMediaItemByFilePath now filters by library_id - test library's entries are isolated from user's library
|
- **After Fix 3:** Tests can safely use `/app/uploads` because GetMediaItemByFilePath now filters by library_id - test library's entries are isolated from user's library
|
||||||
|
|
||||||
**Current unit tests** (`internal/services/*_test.go`):
|
**Current unit tests** (`internal/services/*_test.go`):
|
||||||
|
|
||||||
- `media_scanner_epub_cover_test.go` - Tests cover extraction
|
- `media_scanner_epub_cover_test.go` - Tests cover extraction
|
||||||
- `media_scanner_hash_test.go` - Tests hash calculation
|
- `media_scanner_hash_test.go` - Tests hash calculation
|
||||||
- `media_scanner_library_type_test.go` - Tests library type detection
|
- `media_scanner_library_type_test.go` - Tests library type detection
|
||||||
@@ -845,12 +884,14 @@ func TestWatchMode_DeletesRemovedFile(t *testing.T) {
|
|||||||
### Documentation Updates
|
### Documentation Updates
|
||||||
|
|
||||||
If API behavior changes, update:
|
If API behavior changes, update:
|
||||||
|
|
||||||
- `docs/developer/api/scanner.md` - For any endpoint changes
|
- `docs/developer/api/scanner.md` - For any endpoint changes
|
||||||
- `docs/user/` - If user-facing behavior changes
|
- `docs/user/` - If user-facing behavior changes
|
||||||
|
|
||||||
### Running Tests
|
### Running Tests
|
||||||
|
|
||||||
After implementation, run:
|
After implementation, run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Unit tests
|
# Unit tests
|
||||||
go test ./internal/services/... -v -run "TestProcessMediaFile|TestGetMediaItemByFilePath|TestForceRescan"
|
go test ./internal/services/... -v -run "TestProcessMediaFile|TestGetMediaItemByFilePath|TestForceRescan"
|
||||||
|
|||||||
+50
-15
@@ -3,17 +3,19 @@
|
|||||||
## 🚨 CRITICAL PROHIBITIONS (Never violate these)
|
## 🚨 CRITICAL PROHIBITIONS (Never violate these)
|
||||||
|
|
||||||
### Backend & Database
|
### Backend & Database
|
||||||
|
|
||||||
- ❌ **NEVER modify backend code when working on frontend-only tasks**
|
- ❌ **NEVER modify backend code when working on frontend-only tasks**
|
||||||
- ❌ **NEVER modify database schema** unless explicitly instructed for full-stack changes
|
- ❌ **NEVER modify database schema** unless explicitly instructed for full-stack changes
|
||||||
- ❌ **NEVER use Docker** - use Podman only
|
- ❌ **NEVER use Docker** - use Podman only
|
||||||
- ❌ **NEVER build server binaries locally** - all builds through Dockerfile/docker-compose
|
- ❌ **NEVER build server binaries locally** - all builds through Dockerfile/docker-compose
|
||||||
- ❌ **NEVER create new migration files** - merge changes into current one until release
|
- ❌ **NEVER create new migration files** - merge changes into current one until release
|
||||||
- ❌ **NEVER use `git checkout` on schema files** without checking what will be lost
|
- ❌ **NEVER use `git checkout` on schema files** without checking what will be lost
|
||||||
- ❌ **NEVER break existing functionality** unless explicitly instructed
|
- ❌ **NEVER break existing functionality** unless explicitly instructed
|
||||||
- ❌ **NEVER duplicate business logic** - keep logic in services, not handlers
|
- ❌ **NEVER duplicate business logic** - keep logic in services, not handlers
|
||||||
- ❌ **NEVER bypass service layer** - all database operations must go through services
|
- ❌ **NEVER bypass service layer** - all database operations must go through services
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
- ✅ **ALWAYS use `setupTestServer()` helper from `cmd/server/tests/test_helpers.go`**
|
- ✅ **ALWAYS use `setupTestServer()` helper from `cmd/server/tests/test_helpers.go`**
|
||||||
- ✅ **Share one test setup across all subtests** - call `setupTestServer()` once at test function level, not per subtest
|
- ✅ **Share one test setup across all subtests** - call `setupTestServer()` once at test function level, not per subtest
|
||||||
- ✅ **Prefer table-driven tests** - use `t.Run()` with test cases instead of duplicate test functions
|
- ✅ **Prefer table-driven tests** - use `t.Run()` with test cases instead of duplicate test functions
|
||||||
@@ -24,6 +26,7 @@
|
|||||||
- ✅ **Use `t.Cleanup()` properly** - the `TestServerSetup` pattern automatically handles cleanup via `t.Cleanup()`
|
- ✅ **Use `t.Cleanup()` properly** - the `TestServerSetup` pattern automatically handles cleanup via `t.Cleanup()`
|
||||||
|
|
||||||
### Frontend & Styling
|
### Frontend & Styling
|
||||||
|
|
||||||
- ❌ **NEVER modify backend/API for frontend features without user confirmation**
|
- ❌ **NEVER modify backend/API for frontend features without user confirmation**
|
||||||
- ❌ **NEVER use custom CSS** - TailwindCSS classes only
|
- ❌ **NEVER use custom CSS** - TailwindCSS classes only
|
||||||
- **⚠️ EXCEPTION**: `templates/error.templ` may have inline CSS because error pages must work when main app fails (404, server errors, CSS fails to load)
|
- **⚠️ EXCEPTION**: `templates/error.templ` may have inline CSS because error pages must work when main app fails (404, server errors, CSS fails to load)
|
||||||
@@ -37,9 +40,10 @@
|
|||||||
- ❌ **NEVER fetch initial data via AJAX on page load** - use server-side rendering instead
|
- ❌ **NEVER fetch initial data via AJAX on page load** - use server-side rendering instead
|
||||||
- ❌ **NEVER break progressive enhancement** - pages must work without JavaScript
|
- ❌ **NEVER break progressive enhancement** - pages must work without JavaScript
|
||||||
|
|
||||||
**Note:** Go methods in the backend are fine and encouraged. This guideline applies to TypeScript/JavaScript frontend code only.**
|
**Note:** Go methods in the backend are fine and encouraged. This guideline applies to TypeScript/JavaScript frontend code only.\*\*
|
||||||
|
|
||||||
### General
|
### General
|
||||||
|
|
||||||
- ❌ **NEVER skip pre-commit hooks** unless explicitly requested
|
- ❌ **NEVER skip pre-commit hooks** unless explicitly requested
|
||||||
- ❌ **NEVER force push to main/master** branches
|
- ❌ **NEVER force push to main/master** branches
|
||||||
- ❌ **NEVER commit files with secrets** (.env, credentials.json, etc.)
|
- ❌ **NEVER commit files with secrets** (.env, credentials.json, etc.)
|
||||||
@@ -60,6 +64,7 @@
|
|||||||
### Cascading Fix-up Pattern (PROHIBITED)
|
### Cascading Fix-up Pattern (PROHIBITED)
|
||||||
|
|
||||||
**WHAT NOT TO DO** - This caused critical bugs:
|
**WHAT NOT TO DO** - This caused critical bugs:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// ❌ WRONG: Blindly making fixes after compilation error
|
// ❌ WRONG: Blindly making fixes after compilation error
|
||||||
|
|
||||||
@@ -74,6 +79,7 @@ Edit 3: Try to fix again (worse damage)
|
|||||||
```
|
```
|
||||||
|
|
||||||
**CORRECT APPROACH**:
|
**CORRECT APPROACH**:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// ✅ CORRECT: Stop, understand, then fix deliberately
|
// ✅ CORRECT: Stop, understand, then fix deliberately
|
||||||
|
|
||||||
@@ -86,6 +92,7 @@ VERIFY → Compile successfully
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Key Principle**: When compilation errors occur after edits:
|
**Key Principle**: When compilation errors occur after edits:
|
||||||
|
|
||||||
1. STOP - Don't make more edits
|
1. STOP - Don't make more edits
|
||||||
2. ANALYZE - Use `git diff` to understand what was changed
|
2. ANALYZE - Use `git diff` to understand what was changed
|
||||||
3. RECOVER - Restore what was accidentally deleted/broken
|
3. RECOVER - Restore what was accidentally deleted/broken
|
||||||
@@ -96,6 +103,7 @@ VERIFY → Compile successfully
|
|||||||
## 🎯 CONTEXT-SPECIFIC RULES
|
## 🎯 CONTEXT-SPECIFIC RULES
|
||||||
|
|
||||||
### When Working on Frontend-Only Tasks
|
### When Working on Frontend-Only Tasks
|
||||||
|
|
||||||
- **DO NOT touch backend code** - handlers, services, database layer
|
- **DO NOT touch backend code** - handlers, services, database layer
|
||||||
- **DO NOT modify API routes** - use existing endpoints only
|
- **DO NOT modify API routes** - use existing endpoints only
|
||||||
- **DO NOT change database schema** - work with existing structure
|
- **DO NOT change database schema** - work with existing structure
|
||||||
@@ -106,6 +114,7 @@ VERIFY → Compile successfully
|
|||||||
4. **ASK FOR USER CONFIRMATION before proceeding**
|
4. **ASK FOR USER CONFIRMATION before proceeding**
|
||||||
|
|
||||||
### When Working on Full-Stack Tasks
|
### When Working on Full-Stack Tasks
|
||||||
|
|
||||||
- Backend changes are allowed when explicitly part of the task
|
- Backend changes are allowed when explicitly part of the task
|
||||||
- Still follow all database protocols (atomic changes, validation, etc.)
|
- Still follow all database protocols (atomic changes, validation, etc.)
|
||||||
- **If modifying database schema:** Update local database after schema.sql changes (see Database Operations section)
|
- **If modifying database schema:** Update local database after schema.sql changes (see Database Operations section)
|
||||||
@@ -117,6 +126,7 @@ VERIFY → Compile successfully
|
|||||||
## ✅ MANDATORY REQUIREMENTS
|
## ✅ MANDATORY REQUIREMENTS
|
||||||
|
|
||||||
### Database Operations (Full-Stack Tasks Only)
|
### Database Operations (Full-Stack Tasks Only)
|
||||||
|
|
||||||
- ✅ Follow **pgx v5 standards** for all database operations
|
- ✅ Follow **pgx v5 standards** for all database operations
|
||||||
- ✅ Treat schema changes as **ATOMIC** - complete success or complete rejection
|
- ✅ Treat schema changes as **ATOMIC** - complete success or complete rejection
|
||||||
- ✅ **⚠️ CRITICAL: This is a pre-production application (NO production deployments exist)**
|
- ✅ **⚠️ CRITICAL: This is a pre-production application (NO production deployments exist)**
|
||||||
@@ -132,16 +142,19 @@ VERIFY → Compile successfully
|
|||||||
- ✅ **Post-change validation**: ensure schema.sql, models.go, and queries.sql are in sync
|
- ✅ **Post-change validation**: ensure schema.sql, models.go, and queries.sql are in sync
|
||||||
|
|
||||||
### Build & Deployment
|
### Build & Deployment
|
||||||
|
|
||||||
- ✅ Use **Podman** exclusively (not Docker)
|
- ✅ Use **Podman** exclusively (not Docker)
|
||||||
- ✅ All builds through existing **Dockerfile** and **docker-compose.yml**
|
- ✅ All builds through existing **Dockerfile** and **docker-compose.yml**
|
||||||
- ✅ Stop building server binaries - everything goes through containers
|
- ✅ Stop building server binaries - everything goes through containers
|
||||||
|
|
||||||
### API Changes (Full-Stack Tasks Only)
|
### API Changes (Full-Stack Tasks Only)
|
||||||
|
|
||||||
- ✅ Include **Bruno OpenCollection YAML requests** with all API documentation
|
- ✅ Include **Bruno OpenCollection YAML requests** with all API documentation
|
||||||
- ✅ Tests must be **comprehensive and cover three contexts**: no user, user, and admin
|
- ✅ Tests must be **comprehensive and cover three contexts**: no user, user, and admin
|
||||||
- ✅ Maintain backward compatibility for mobile apps and external consumers
|
- ✅ Maintain backward compatibility for mobile apps and external consumers
|
||||||
|
|
||||||
### Frontend & Styling
|
### Frontend & Styling
|
||||||
|
|
||||||
- ✅ Always use **TailwindCSS classes** for all styling
|
- ✅ Always use **TailwindCSS classes** for all styling
|
||||||
- ✅ Convert all JavaScript to **TypeScript**
|
- ✅ Convert all JavaScript to **TypeScript**
|
||||||
- ✅ **Never use Object-Oriented Programming** (no classes, inheritance, or this-capture)
|
- ✅ **Never use Object-Oriented Programming** (no classes, inheritance, or this-capture)
|
||||||
@@ -154,12 +167,14 @@ VERIFY → Compile successfully
|
|||||||
- ✅ **Ensure progressive enhancement** - pages work without JavaScript
|
- ✅ **Ensure progressive enhancement** - pages work without JavaScript
|
||||||
|
|
||||||
### Service Layer Architecture
|
### Service Layer Architecture
|
||||||
|
|
||||||
- ✅ **All business logic in services** - never in handlers
|
- ✅ **All business logic in services** - never in handlers
|
||||||
- ✅ **Services must be reusable** by both SSR handlers and API endpoints
|
- ✅ **Services must be reusable** by both SSR handlers and API endpoints
|
||||||
- ✅ **Database operations through services only** - never direct from handlers
|
- ✅ **Database operations through services only** - never direct from handlers
|
||||||
- ✅ **When adding features**: Add service logic → Create API endpoint → Use SSR for initial render → Use JS for updates
|
- ✅ **When adding features**: Add service logic → Create API endpoint → Use SSR for initial render → Use JS for updates
|
||||||
|
|
||||||
### Code Organization
|
### Code Organization
|
||||||
|
|
||||||
- ✅ Minimize project structure changes
|
- ✅ Minimize project structure changes
|
||||||
- ✅ Place new files in **contextually appropriate directories**
|
- ✅ Place new files in **contextually appropriate directories**
|
||||||
- ✅ Follow **KISS**, **DRY**, and **YAGNI** principles
|
- ✅ Follow **KISS**, **DRY**, and **YAGNI** principles
|
||||||
@@ -172,10 +187,12 @@ VERIFY → Compile successfully
|
|||||||
- ❌ **NEVER create conversion helper functions** to map between handler and template types - use handler types directly
|
- ❌ **NEVER create conversion helper functions** to map between handler and template types - use handler types directly
|
||||||
|
|
||||||
### Configuration & Environment
|
### Configuration & Environment
|
||||||
|
|
||||||
- ✅ If **.env is missing**, auto-generate secure values
|
- ✅ If **.env is missing**, auto-generate secure values
|
||||||
- ✅ Never commit secrets to repository
|
- ✅ Never commit secrets to repository
|
||||||
|
|
||||||
### Code Modification Safety
|
### Code Modification Safety
|
||||||
|
|
||||||
- ✅ **Post-Edit Verification (MANDATORY for ALL file modifications)**:
|
- ✅ **Post-Edit Verification (MANDATORY for ALL file modifications)**:
|
||||||
- Run `go build` for affected packages immediately after each edit
|
- Run `go build` for affected packages immediately after each edit
|
||||||
- Review `git diff filename` to verify only intended changes
|
- Review `git diff filename` to verify only intended changes
|
||||||
@@ -194,6 +211,7 @@ VERIFY → Compile successfully
|
|||||||
### Documentation
|
### Documentation
|
||||||
|
|
||||||
**Documentation Structure** (updated with full docs system):
|
**Documentation Structure** (updated with full docs system):
|
||||||
|
|
||||||
- ✅ **README.md** - Project overview, quick start, and setup instructions only
|
- ✅ **README.md** - Project overview, quick start, and setup instructions only
|
||||||
- ✅ **docs/** - Comprehensive documentation system with search
|
- ✅ **docs/** - Comprehensive documentation system with search
|
||||||
- ✅ **docs/developer/api/** - API reference documentation (split by endpoint/category)
|
- ✅ **docs/developer/api/** - API reference documentation (split by endpoint/category)
|
||||||
@@ -203,19 +221,20 @@ VERIFY → Compile successfully
|
|||||||
|
|
||||||
**Where to document changes**:
|
**Where to document changes**:
|
||||||
|
|
||||||
| Change Type | Location | Examples |
|
| Change Type | Location | Examples |
|
||||||
|-------------|----------|----------|
|
| ----------------------------------- | ------------------------------------------------------------- | --------------------------------------------------------- |
|
||||||
| **User-facing features** | `docs/user/` | New features, UI changes, workflows |
|
| **User-facing features** | `docs/user/` | New features, UI changes, workflows |
|
||||||
| **API endpoints** | `docs/developer/api/<category>/<endpoint>.md` | New endpoints, modified responses, authentication changes |
|
| **API endpoints** | `docs/developer/api/<category>/<endpoint>.md` | New endpoints, modified responses, authentication changes |
|
||||||
| **API behavior** | Update existing `docs/developer/api/` files | Parameter changes, error codes, rate limits |
|
| **API behavior** | Update existing `docs/developer/api/` files | Parameter changes, error codes, rate limits |
|
||||||
| **Device setup** | `docs/user/devices/` | New device support, setup instructions |
|
| **Device setup** | `docs/user/devices/` | New device support, setup instructions |
|
||||||
| **Development** | `docs/contributing/` | Build changes, architecture decisions |
|
| **Development** | `docs/contributing/` | Build changes, architecture decisions |
|
||||||
| **Quick start/setup** | `README.md` | Installation, environment setup, first-run |
|
| **Quick start/setup** | `README.md` | Installation, environment setup, first-run |
|
||||||
| **Breaking changes** | Both `README.md` and relevant `docs/` | Migration guides, deprecation notices |
|
| **Breaking changes** | Both `README.md` and relevant `docs/` | Migration guides, deprecation notices |
|
||||||
| **Bug fixes** | Update relevant `docs/` only if user-visible | Clarifications, troubleshooting additions |
|
| **Bug fixes** | Update relevant `docs/` only if user-visible | Clarifications, troubleshooting additions |
|
||||||
| **Bruno OpenCollection YAML tests** | `.yml` files in bruno folder in appropriate folder/sub-folder | API contract testing, examples |
|
| **Bruno OpenCollection YAML tests** | `.yml` files in bruno folder in appropriate folder/sub-folder | API contract testing, examples |
|
||||||
|
|
||||||
**Documentation Update Workflow**:
|
**Documentation Update Workflow**:
|
||||||
|
|
||||||
1. **Identify the audience** (end users, developers, API consumers)
|
1. **Identify the audience** (end users, developers, API consumers)
|
||||||
2. **Choose appropriate location** based on table above
|
2. **Choose appropriate location** based on table above
|
||||||
3. **Update documentation** before or with code changes
|
3. **Update documentation** before or with code changes
|
||||||
@@ -225,12 +244,14 @@ VERIFY → Compile successfully
|
|||||||
7. **Commit separately** with clear message: `docs: <description>`
|
7. **Commit separately** with clear message: `docs: <description>`
|
||||||
|
|
||||||
**When in doubt**:
|
**When in doubt**:
|
||||||
|
|
||||||
- End-user visible → `docs/user/`
|
- End-user visible → `docs/user/`
|
||||||
- API reference → `docs/developer/api/`
|
- API reference → `docs/developer/api/`
|
||||||
- Setup/onboarding → `README.md`
|
- Setup/onboarding → `README.md`
|
||||||
- Development related → `docs/contributing/`
|
- Development related → `docs/contributing/`
|
||||||
|
|
||||||
### Process & Continuity
|
### Process & Continuity
|
||||||
|
|
||||||
- ✅ If mid-task and receive "no response", **continue the task**
|
- ✅ If mid-task and receive "no response", **continue the task**
|
||||||
- ✅ Verify no regressions before modifying/removing code
|
- ✅ Verify no regressions before modifying/removing code
|
||||||
|
|
||||||
@@ -239,18 +260,21 @@ VERIFY → Compile successfully
|
|||||||
## 🔧 TECHNICAL STANDARDS
|
## 🔧 TECHNICAL STANDARDS
|
||||||
|
|
||||||
### Backend Stack
|
### Backend Stack
|
||||||
|
|
||||||
- **Language**: Go 1.25+
|
- **Language**: Go 1.25+
|
||||||
- **Database**: PostgreSQL 15+ with **pgx v5 driver** only
|
- **Database**: PostgreSQL 15+ with **pgx v5 driver** only
|
||||||
- **Authentication**: JWT tokens with bcrypt password hashing
|
- **Authentication**: JWT tokens with bcrypt password hashing
|
||||||
- **Architecture**: Service layer pattern (handlers → services → database)
|
- **Architecture**: Service layer pattern (handlers → services → database)
|
||||||
|
|
||||||
### Frontend Stack
|
### Frontend Stack
|
||||||
|
|
||||||
- **Styling**: TailwindCSS (no custom CSS)
|
- **Styling**: TailwindCSS (no custom CSS)
|
||||||
- **Language**: TypeScript (no JavaScript)
|
- **Language**: TypeScript (no JavaScript)
|
||||||
- **Templates**: HTMX with server-side rendering
|
- **Templates**: HTMX with server-side rendering
|
||||||
- **Patterns**: Procedural/imperative with functional techniques where helpful (no OOP)
|
- **Patterns**: Procedural/imperative with functional techniques where helpful (no OOP)
|
||||||
|
|
||||||
### Containerization
|
### Containerization
|
||||||
|
|
||||||
- **Runtime**: Podman (not Docker)
|
- **Runtime**: Podman (not Docker)
|
||||||
- **Build**: Existing Dockerfile and docker-compose.yml only
|
- **Build**: Existing Dockerfile and docker-compose.yml only
|
||||||
- **No local builds** allowed
|
- **No local builds** allowed
|
||||||
@@ -262,6 +286,7 @@ VERIFY → Compile successfully
|
|||||||
When code modification mistakes occur (deleted wrong code, broke compilation, etc.):
|
When code modification mistakes occur (deleted wrong code, broke compilation, etc.):
|
||||||
|
|
||||||
### Immediate Actions
|
### Immediate Actions
|
||||||
|
|
||||||
1. **STOP** - Don't make more edits
|
1. **STOP** - Don't make more edits
|
||||||
2. **ASSESS** - What was deleted? Is it critical?
|
2. **ASSESS** - What was deleted? Is it critical?
|
||||||
3. **REVIEW** - Run `git diff` to see exact changes
|
3. **REVIEW** - Run `git diff` to see exact changes
|
||||||
@@ -273,6 +298,7 @@ When code modification mistakes occur (deleted wrong code, broke compilation, et
|
|||||||
6. **DOCUMENT** - Note what went wrong for future reference
|
6. **DOCUMENT** - Note what went wrong for future reference
|
||||||
|
|
||||||
### Recovery Examples
|
### Recovery Examples
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Recover a deleted function from original file
|
# Recover a deleted function from original file
|
||||||
git show HEAD:internal/handlers/auth.go | sed -n '70,275p' > recovery.txt
|
git show HEAD:internal/handlers/auth.go | sed -n '70,275p' > recovery.txt
|
||||||
@@ -286,6 +312,7 @@ git checkout -- internal/handlers/auth.go
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Prevention (Learn From Mistakes)
|
### Prevention (Learn From Mistakes)
|
||||||
|
|
||||||
- Why did the mistake happen?
|
- Why did the mistake happen?
|
||||||
- Was it too-broad matching?
|
- Was it too-broad matching?
|
||||||
- Was it insufficient context reading?
|
- Was it insufficient context reading?
|
||||||
@@ -297,6 +324,7 @@ git checkout -- internal/handlers/auth.go
|
|||||||
## 📋 WORKFLOW CHECKLISTS
|
## 📋 WORKFLOW CHECKLISTS
|
||||||
|
|
||||||
### Before Making Frontend-Only Changes
|
### Before Making Frontend-Only Changes
|
||||||
|
|
||||||
- [ ] Identify if backend modification could make implementation simpler
|
- [ ] Identify if backend modification could make implementation simpler
|
||||||
- [ ] Plan to use existing API endpoints only
|
- [ ] Plan to use existing API endpoints only
|
||||||
- [ ] If backend change seems necessary, prepare confirmation request:
|
- [ ] If backend change seems necessary, prepare confirmation request:
|
||||||
@@ -311,6 +339,7 @@ git checkout -- internal/handlers/auth.go
|
|||||||
- [ ] Setup instructions → `README.md`
|
- [ ] Setup instructions → `README.md`
|
||||||
|
|
||||||
### Before Making Full-Stack Changes
|
### Before Making Full-Stack Changes
|
||||||
|
|
||||||
- [ ] Read current schema completely (if database changes)
|
- [ ] Read current schema completely (if database changes)
|
||||||
- [ ] Identify all columns that must be preserved
|
- [ ] Identify all columns that must be preserved
|
||||||
- [ ] Plan exact changes needed
|
- [ ] Plan exact changes needed
|
||||||
@@ -324,6 +353,7 @@ git checkout -- internal/handlers/auth.go
|
|||||||
- [ ] Bruno OpenCollection YAML `.yml` files → Update/create alongside API changes
|
- [ ] Bruno OpenCollection YAML `.yml` files → Update/create alongside API changes
|
||||||
|
|
||||||
### During Schema Changes (Full-Stack Only)
|
### During Schema Changes (Full-Stack Only)
|
||||||
|
|
||||||
- [ ] Read current schema completely
|
- [ ] Read current schema completely
|
||||||
- [ ] Identify all columns that must be preserved
|
- [ ] Identify all columns that must be preserved
|
||||||
- [ ] Plan exact changes needed
|
- [ ] Plan exact changes needed
|
||||||
@@ -345,6 +375,7 @@ git checkout -- internal/handlers/auth.go
|
|||||||
- [ ] Verify database has new schema (check column types, indexes, etc.)
|
- [ ] Verify database has new schema (check column types, indexes, etc.)
|
||||||
|
|
||||||
### After API Changes
|
### After API Changes
|
||||||
|
|
||||||
- [ ] Create/update Bruno OpenCollection YAML requests
|
- [ ] Create/update Bruno OpenCollection YAML requests
|
||||||
- [ ] Test with no user context
|
- [ ] Test with no user context
|
||||||
- [ ] Test with regular user context
|
- [ ] Test with regular user context
|
||||||
@@ -352,6 +383,7 @@ git checkout -- internal/handlers/auth.go
|
|||||||
- [ ] Verify backward compatibility
|
- [ ] Verify backward compatibility
|
||||||
|
|
||||||
### Before Committing
|
### Before Committing
|
||||||
|
|
||||||
- [ ] **Run verification script**: `bash scripts/verify-guidelines.sh`
|
- [ ] **Run verification script**: `bash scripts/verify-guidelines.sh`
|
||||||
- [ ] **Fix any errors** - verification must pass (0 errors) to commit
|
- [ ] **Fix any errors** - verification must pass (0 errors) to commit
|
||||||
- [ ] **Note warnings** - informational only, do not auto-fix
|
- [ ] **Note warnings** - informational only, do not auto-fix
|
||||||
@@ -368,6 +400,7 @@ git checkout -- internal/handlers/auth.go
|
|||||||
- [ ] **Test docs search** finds new content
|
- [ ] **Test docs search** finds new content
|
||||||
|
|
||||||
### Error Recovery Protocol (If Code Mistakes Occur)
|
### Error Recovery Protocol (If Code Mistakes Occur)
|
||||||
|
|
||||||
- [ ] **Stop immediately** - don't make more edits
|
- [ ] **Stop immediately** - don't make more edits
|
||||||
- [ ] **Assess impact**: What was deleted? Is it critical?
|
- [ ] **Assess impact**: What was deleted? Is it critical?
|
||||||
- [ ] **Review git diff**: See exact changes made
|
- [ ] **Review git diff**: See exact changes made
|
||||||
@@ -378,6 +411,7 @@ git checkout -- internal/handlers/auth.go
|
|||||||
- [ ] **Document mistake**: Note what went wrong for future reference
|
- [ ] **Document mistake**: Note what went wrong for future reference
|
||||||
|
|
||||||
### Phase Completion Verification (Before Declaring "Complete")
|
### Phase Completion Verification (Before Declaring "Complete")
|
||||||
|
|
||||||
- [ ] All target code is removed/intact as intended
|
- [ ] All target code is removed/intact as intended
|
||||||
- [ ] No unintended code was deleted
|
- [ ] No unintended code was deleted
|
||||||
- [ ] All affected files compile successfully
|
- [ ] All affected files compile successfully
|
||||||
@@ -392,6 +426,7 @@ git checkout -- internal/handlers/auth.go
|
|||||||
## 🏗 ARCHITECTURAL PATTERNS
|
## 🏗 ARCHITECTURAL PATTERNS
|
||||||
|
|
||||||
### Current: Hybrid SSR
|
### Current: Hybrid SSR
|
||||||
|
|
||||||
```
|
```
|
||||||
Browser → Go template (with data) → Display instantly
|
Browser → Go template (with data) → Display instantly
|
||||||
↓
|
↓
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ A modern self-hosted media library system built with Go, PostgreSQL, HTMX, and T
|
|||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- **Podman** (recommended) or Docker
|
- **Podman** (recommended) or Docker
|
||||||
- **5 minutes** of your time
|
- **5 minutes** of your time
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ The first user to register automatically becomes an admin.
|
|||||||
## 📖 Key Features
|
## 📖 Key Features
|
||||||
|
|
||||||
### Universal Cross-Platform Sync
|
### Universal Cross-Platform Sync
|
||||||
|
|
||||||
- **Real-Time Progress**: Turn a page on your Kindle, see it on your phone
|
- **Real-Time Progress**: Turn a page on your Kindle, see it on your phone
|
||||||
- **Format-Aware**: EPUB CFI, page numbers, percentages - all handled correctly
|
- **Format-Aware**: EPUB CFI, page numbers, percentages - all handled correctly
|
||||||
- **Offline Queue**: Changes sync when you reconnect, priority-processed
|
- **Offline Queue**: Changes sync when you reconnect, priority-processed
|
||||||
@@ -57,6 +59,7 @@ The first user to register automatically becomes an admin.
|
|||||||
- **Format Conversion**: On-the-fly EPUB→KEPUB for Kobo devices
|
- **Format Conversion**: On-the-fly EPUB→KEPUB for Kobo devices
|
||||||
|
|
||||||
### Media Management
|
### Media Management
|
||||||
|
|
||||||
- **Smart Search**: Partial matching with fuzzy search fallback for typos
|
- **Smart Search**: Partial matching with fuzzy search fallback for typos
|
||||||
- **Advanced Filtering**: Filter by author, series, genre, language, year, cover images
|
- **Advanced Filtering**: Filter by author, series, genre, language, year, cover images
|
||||||
- **Dynamic Sorting**: By title, author, date added, published date, page count, series
|
- **Dynamic Sorting**: By title, author, date added, published date, page count, series
|
||||||
@@ -66,11 +69,13 @@ The first user to register automatically becomes an admin.
|
|||||||
- **Usage Analytics**: Reading statistics, device usage, popular books
|
- **Usage Analytics**: Reading statistics, device usage, popular books
|
||||||
|
|
||||||
### Smart Collections
|
### Smart Collections
|
||||||
|
|
||||||
- **Auto-Assign Rules**: Automatically add books based on genre, author, series, tags, language, publisher, year
|
- **Auto-Assign Rules**: Automatically add books based on genre, author, series, tags, language, publisher, year
|
||||||
- **Device Shelf Mappings**: Sync collections to Kobo shelves and KOReader categories
|
- **Device Shelf Mappings**: Sync collections to Kobo shelves and KOReader categories
|
||||||
- **Test Before Creating**: Preview which books match your rules
|
- **Test Before Creating**: Preview which books match your rules
|
||||||
|
|
||||||
### Library Organization
|
### Library Organization
|
||||||
|
|
||||||
- **Multi-Library Support**: Ebooks, Comics, and Manga with type-specific file formats
|
- **Multi-Library Support**: Ebooks, Comics, and Manga with type-specific file formats
|
||||||
- **Multiple Folders**: Add multiple scanning folders per library
|
- **Multiple Folders**: Add multiple scanning folders per library
|
||||||
- **Visibility Control**: Admins control which libraries each user can see
|
- **Visibility Control**: Admins control which libraries each user can see
|
||||||
@@ -78,6 +83,7 @@ The first user to register automatically becomes an admin.
|
|||||||
- **Watch Mode**: Real-time file system monitoring for instant updates
|
- **Watch Mode**: Real-time file system monitoring for instant updates
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
- **JWT Authentication**: Short-lived access tokens (1 hour) with refresh tokens (7 days)
|
- **JWT Authentication**: Short-lived access tokens (1 hour) with refresh tokens (7 days)
|
||||||
- **Strong Passwords**: Complexity requirements enforced (8+ chars, uppercase, lowercase, number, special)
|
- **Strong Passwords**: Complexity requirements enforced (8+ chars, uppercase, lowercase, number, special)
|
||||||
- **Account Lockout**: 5 failed attempts = 15-minute lockout
|
- **Account Lockout**: 5 failed attempts = 15-minute lockout
|
||||||
@@ -90,6 +96,7 @@ The first user to register automatically becomes an admin.
|
|||||||
## 📚 Documentation
|
## 📚 Documentation
|
||||||
|
|
||||||
### For Users & Self-Hosters
|
### For Users & Self-Hosters
|
||||||
|
|
||||||
- **[docs/user/sync-guide.md](docs/user/sync-guide.md)** - Understanding and using universal sync
|
- **[docs/user/sync-guide.md](docs/user/sync-guide.md)** - Understanding and using universal sync
|
||||||
- **[docs/user/devices/kobo-setup.md](docs/user/devices/kobo-setup.md)** - Kobo e-reader configuration
|
- **[docs/user/devices/kobo-setup.md](docs/user/devices/kobo-setup.md)** - Kobo e-reader configuration
|
||||||
- **[docs/user/devices/koreader-setup.md](docs/user/devices/koreader-setup.md)** - KOReader configuration
|
- **[docs/user/devices/koreader-setup.md](docs/user/devices/koreader-setup.md)** - KOReader configuration
|
||||||
@@ -98,6 +105,7 @@ The first user to register automatically becomes an admin.
|
|||||||
- **[docs/user/settings-guide.md](docs/user/settings-guide.md)** - Settings and preferences
|
- **[docs/user/settings-guide.md](docs/user/settings-guide.md)** - Settings and preferences
|
||||||
|
|
||||||
### For Developers
|
### For Developers
|
||||||
|
|
||||||
- **[docs/developer/api/api-reference.md](docs/developer/api/api-reference.md)** - Complete API documentation
|
- **[docs/developer/api/api-reference.md](docs/developer/api/api-reference.md)** - Complete API documentation
|
||||||
- **[docs/contributing/DEVELOPMENT.md](docs/contributing/DEVELOPMENT.md)** - Development workflow
|
- **[docs/contributing/DEVELOPMENT.md](docs/contributing/DEVELOPMENT.md)** - Development workflow
|
||||||
|
|
||||||
@@ -105,12 +113,12 @@ The first user to register automatically becomes an admin.
|
|||||||
|
|
||||||
## 🎯 Supported Devices
|
## 🎯 Supported Devices
|
||||||
|
|
||||||
| Platform | Sync | OPDS | Status |
|
| Platform | Sync | OPDS | Status |
|
||||||
|----------|------|------|--------|
|
| ---------------- | ---- | ---- | ------------------------ |
|
||||||
| **Web Browser** | ✅ | ✅ | Full support |
|
| **Web Browser** | ✅ | ✅ | Full support |
|
||||||
| **KOReader** | ✅ | ✅ | Kindle, Kobo, PocketBook |
|
| **KOReader** | ✅ | ✅ | Kindle, Kobo, PocketBook |
|
||||||
| **Kobo Devices** | ✅ | ✅ | Clara, Libra, Sage, etc. |
|
| **Kobo Devices** | ✅ | ✅ | Clara, Libra, Sage, etc. |
|
||||||
| **Mobile Apps** | 🚧 | 🚧 | Coming Q2 2026 |
|
| **Mobile Apps** | 🚧 | 🚧 | Coming Q2 2026 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+302
-223
@@ -8,6 +8,7 @@ This document outlines the complete plan for automatically generating screenshot
|
|||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Playwright will be used to:
|
Playwright will be used to:
|
||||||
|
|
||||||
1. Navigate the running Bookhoard server
|
1. Navigate the running Bookhoard server
|
||||||
2. Perform key user/admin workflows
|
2. Perform key user/admin workflows
|
||||||
3. Capture screenshots at each step
|
3. Capture screenshots at each step
|
||||||
@@ -17,6 +18,7 @@ Playwright will be used to:
|
|||||||
## Prerequisites (To Verify When Ready)
|
## Prerequisites (To Verify When Ready)
|
||||||
|
|
||||||
### Frontend Pages Complete
|
### Frontend Pages Complete
|
||||||
|
|
||||||
Verify these pages are fully functional before starting:
|
Verify these pages are fully functional before starting:
|
||||||
|
|
||||||
- [ ] `/` - Login page
|
- [ ] `/` - Login page
|
||||||
@@ -34,6 +36,7 @@ Verify these pages are fully functional before starting:
|
|||||||
- [ ] `/api-explorer` - API testing interface
|
- [ ] `/api-explorer` - API testing interface
|
||||||
|
|
||||||
### Test Environment Ready
|
### Test Environment Ready
|
||||||
|
|
||||||
- [ ] Bookhoard server running on `http://localhost:8765`
|
- [ ] Bookhoard server running on `http://localhost:8765`
|
||||||
- [ ] Test database seeded with sample data (books, collections, devices)
|
- [ ] Test database seeded with sample data (books, collections, devices)
|
||||||
- [ ] Test admin account ready (username, password, role=admin)
|
- [ ] Test admin account ready (username, password, role=admin)
|
||||||
@@ -49,6 +52,7 @@ npx playwright install chromium
|
|||||||
```
|
```
|
||||||
|
|
||||||
Project structure after setup:
|
Project structure after setup:
|
||||||
|
|
||||||
```
|
```
|
||||||
bookhoard/
|
bookhoard/
|
||||||
├── docs/
|
├── docs/
|
||||||
@@ -93,133 +97,151 @@ USER_PASSWORD=SecureUserPass456!
|
|||||||
Before running the screenshot automation, ask the user:
|
Before running the screenshot automation, ask the user:
|
||||||
|
|
||||||
### 1. Server Access
|
### 1. Server Access
|
||||||
|
|
||||||
**Q**: Where is the Bookhoard server running?
|
**Q**: Where is the Bookhoard server running?
|
||||||
|
|
||||||
- [ ] `localhost:8765` (default)
|
- [ ] `localhost:8765` (default)
|
||||||
- [ ] Custom port: `________`
|
- [ ] Custom port: `________`
|
||||||
- [ ] Remote URL: `________`
|
- [ ] Remote URL: `________`
|
||||||
|
|
||||||
### 2. Test Credentials
|
### 2. Test Credentials
|
||||||
|
|
||||||
**Q**: What credentials should Playwright use?
|
**Q**: What credentials should Playwright use?
|
||||||
|
|
||||||
**Admin Account** (for admin guide screenshots):
|
**Admin Account** (for admin guide screenshots):
|
||||||
|
|
||||||
- Username: `________`
|
- Username: `________`
|
||||||
- Password: `________`
|
- Password: `________`
|
||||||
|
|
||||||
**Regular User Account** (for user guide screenshots):
|
**Regular User Account** (for user guide screenshots):
|
||||||
|
|
||||||
- Username: `________`
|
- Username: `________`
|
||||||
- Password: `________`
|
- Password: `________`
|
||||||
|
|
||||||
### 3. Screenshot Format
|
### 3. Screenshot Format
|
||||||
|
|
||||||
**Q**: What format for screenshots?
|
**Q**: What format for screenshots?
|
||||||
|
|
||||||
- [ ] **WebP** (recommended - modern, good compression)
|
- [ ] **WebP** (recommended - modern, good compression)
|
||||||
- [ ] PNG (highest quality, larger files)
|
- [ ] PNG (highest quality, larger files)
|
||||||
- [ ] JPEG (smaller files, compression artifacts)
|
- [ ] JPEG (smaller files, compression artifacts)
|
||||||
|
|
||||||
### 4. Screenshot Dimensions
|
### 4. Screenshot Dimensions
|
||||||
|
|
||||||
**Q**: What viewport sizes for screenshots?
|
**Q**: What viewport sizes for screenshots?
|
||||||
|
|
||||||
- [ ] **Desktop**: 1920x1080 (full-width screenshots)
|
- [ ] **Desktop**: 1920x1080 (full-width screenshots)
|
||||||
- [ ] **Tablet**: 768x1024 (responsive documentation)
|
- [ ] **Tablet**: 768x1024 (responsive documentation)
|
||||||
- [ ] **Mobile**: 375x667 (mobile documentation)
|
- [ ] **Mobile**: 375x667 (mobile documentation)
|
||||||
- [ ] All three sizes (comprehensive coverage)
|
- [ ] All three sizes (comprehensive coverage)
|
||||||
|
|
||||||
### 5. Theme
|
### 5. Theme
|
||||||
|
|
||||||
**Q**: What theme should screenshots use?
|
**Q**: What theme should screenshots use?
|
||||||
|
|
||||||
- [ ] **Default** (Tokyo Night theme as seen in templates)
|
- [ ] **Default** (Tokyo Night theme as seen in templates)
|
||||||
- [ ] Light theme (if implemented)
|
- [ ] Light theme (if implemented)
|
||||||
- [ ] Multiple themes (document theme switching)
|
- [ ] Multiple themes (document theme switching)
|
||||||
|
|
||||||
### 6. Language
|
### 6. Language
|
||||||
|
|
||||||
**Q**: What language/region for the UI?
|
**Q**: What language/region for the UI?
|
||||||
|
|
||||||
- [ ] **English** (default)
|
- [ ] **English** (default)
|
||||||
- [ ] Other: `________`
|
- [ ] Other: `________`
|
||||||
|
|
||||||
## Screenshot Plan by Documentation Section
|
## Screenshot Plan by Documentation Section
|
||||||
|
|
||||||
### 1. User Guide Screenshots
|
### 1. User Guide Screenshots
|
||||||
|
|
||||||
**File**: `docs/user/user-guide.md`
|
**File**: `docs/user/user-guide.md`
|
||||||
|
|
||||||
**Screenshots Needed**:
|
**Screenshots Needed**:
|
||||||
|
|
||||||
| Screenshot Name | Description | Page/Action |
|
| Screenshot Name | Description | Page/Action |
|
||||||
|-----------------|-------------|-------------|
|
| ------------------------- | ---------------------------------- | --------------------------------------- |
|
||||||
| `login-page.webp` | Login form with filled credentials | `/` - Login page |
|
| `login-page.webp` | Login form with filled credentials | `/` - Login page |
|
||||||
| `dashboard-overview.webp` | Main dashboard showing libraries | `/dashboard` |
|
| `dashboard-overview.webp` | Main dashboard showing libraries | `/dashboard` |
|
||||||
| `library-grid.webp` | Media items grid view | `/dashboard` → Click library |
|
| `library-grid.webp` | Media items grid view | `/dashboard` → Click library |
|
||||||
| `book-detail.webp` | Book detail view with metadata | `/dashboard` → Click book |
|
| `book-detail.webp` | Book detail view with metadata | `/dashboard` → Click book |
|
||||||
| `search-results.webp` | Search in action | `/dashboard` → Type in search |
|
| `search-results.webp` | Search in action | `/dashboard` → Type in search |
|
||||||
| `filter-panel.webp` | Filter options expanded | `/dashboard` → Open filters |
|
| `filter-panel.webp` | Filter options expanded | `/dashboard` → Open filters |
|
||||||
| `collections-list.webp` | Collections grid view | `/collections` |
|
| `collections-list.webp` | Collections grid view | `/collections` |
|
||||||
| `create-collection.webp` | New collection modal | `/collections` → Click "New Collection" |
|
| `create-collection.webp` | New collection modal | `/collections` → Click "New Collection" |
|
||||||
| `reading-progress.webp` | Progress tracking view | `/progress` |
|
| `reading-progress.webp` | Progress tracking view | `/progress` |
|
||||||
| `analytics-view.webp` | User analytics dashboard | `/analytics` |
|
| `analytics-view.webp` | User analytics dashboard | `/analytics` |
|
||||||
|
|
||||||
**Estimated Screenshots**: ~10
|
**Estimated Screenshots**: ~10
|
||||||
|
|
||||||
### 2. Admin Guide Screenshots
|
### 2. Admin Guide Screenshots
|
||||||
|
|
||||||
**File**: `docs/user/admin-guide.md`
|
**File**: `docs/user/admin-guide.md`
|
||||||
|
|
||||||
**Screenshots Needed**:
|
**Screenshots Needed**:
|
||||||
|
|
||||||
| Screenshot Name | Description | Page/Action |
|
| Screenshot Name | Description | Page/Action |
|
||||||
|-----------------|-------------|-------------|
|
| ------------------------ | ------------------------- | -------------------------------------- |
|
||||||
| `admin-dashboard.webp` | Admin overview panel | `/admin` |
|
| `admin-dashboard.webp` | Admin overview panel | `/admin` |
|
||||||
| `user-management.webp` | User list with actions | `/admin` → Users section |
|
| `user-management.webp` | User list with actions | `/admin` → Users section |
|
||||||
| `add-user.webp` | Add new user form | `/admin` → Click "Add User" |
|
| `add-user.webp` | Add new user form | `/admin` → Click "Add User" |
|
||||||
| `library-settings.webp` | Library configuration | `/admin/library` |
|
| `library-settings.webp` | Library configuration | `/admin/library` |
|
||||||
| `add-library.webp` | Create new library form | `/admin/library` → Click "Add Library" |
|
| `add-library.webp` | Create new library form | `/admin/library` → Click "Add Library" |
|
||||||
| `analytics-admin.webp` | Admin analytics view | `/analytics` (admin view) |
|
| `analytics-admin.webp` | Admin analytics view | `/analytics` (admin view) |
|
||||||
| `sync-queue.webp` | Sync queue monitoring | `/queue` |
|
| `sync-queue.webp` | Sync queue monitoring | `/queue` |
|
||||||
| `theme-settings.webp` | Theme selection interface | `/admin/profile` → Theme section |
|
| `theme-settings.webp` | Theme selection interface | `/admin/profile` → Theme section |
|
||||||
| `user-profile-edit.webp` | Edit user profile | `/admin/profile` |
|
| `user-profile-edit.webp` | Edit user profile | `/admin/profile` |
|
||||||
|
|
||||||
**Estimated Screenshots**: ~9
|
**Estimated Screenshots**: ~9
|
||||||
|
|
||||||
### 3. Device Setup Screenshots
|
### 3. Device Setup Screenshots
|
||||||
|
|
||||||
**File**: `docs/user/devices/kobo-setup.md` and `koreader-setup.md`
|
**File**: `docs/user/devices/kobo-setup.md` and `koreader-setup.md`
|
||||||
|
|
||||||
**Screenshots Needed**:
|
**Screenshots Needed**:
|
||||||
|
|
||||||
| Screenshot Name | Description | Page/Action |
|
| Screenshot Name | Description | Page/Action |
|
||||||
|-----------------|-------------|-------------|
|
| --------------------------- | --------------------------------- | ----------------------------------- |
|
||||||
| `device-list.webp` | Device management page | `/devices` |
|
| `device-list.webp` | Device management page | `/devices` |
|
||||||
| `add-device-modal.webp` | Add new device modal | `/devices` → Click "Add New Device" |
|
| `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-kobo.webp` | Kobo device registration form | `/devices` → Select Kobo type |
|
||||||
| `device-form-koreader.webp` | KOReader device registration form | `/devices` → Select KOReader 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-qr-code.webp` | QR code for device approval | After device registration |
|
||||||
| `device-approved.webp` | Device approved confirmation | After approving device |
|
| `device-approved.webp` | Device approved confirmation | After approving device |
|
||||||
| `device-sync-settings.webp` | Sync configuration for device | `/devices` → Click device settings |
|
| `device-sync-settings.webp` | Sync configuration for device | `/devices` → Click device settings |
|
||||||
| `shelf-mapping.webp` | Collection to shelf mapping | `/devices` → Click shelf mapping |
|
| `shelf-mapping.webp` | Collection to shelf mapping | `/devices` → Click shelf mapping |
|
||||||
| `sync-queue-item.webp` | Device sync in queue | `/queue` (device specific) |
|
| `sync-queue-item.webp` | Device sync in queue | `/queue` (device specific) |
|
||||||
|
|
||||||
**Estimated Screenshots**: ~9
|
**Estimated Screenshots**: ~9
|
||||||
|
|
||||||
### 4. Sync Guide Screenshots
|
### 4. Sync Guide Screenshots
|
||||||
|
|
||||||
**File**: `docs/user/sync-guide.md`
|
**File**: `docs/user/sync-guide.md`
|
||||||
|
|
||||||
**Screenshots Needed**:
|
**Screenshots Needed**:
|
||||||
|
|
||||||
| Screenshot Name | Description | Page/Action |
|
| Screenshot Name | Description | Page/Action |
|
||||||
|-----------------|-------------|-------------|
|
| -------------------------- | ------------------------- | ------------------------------ |
|
||||||
| `sync-conflicts.webp` | Conflicts list view | `/conflicts` |
|
| `sync-conflicts.webp` | Conflicts list view | `/conflicts` |
|
||||||
| `conflict-resolution.webp` | Resolve conflict dialog | `/conflicts` → Click resolve |
|
| `conflict-resolution.webp` | Resolve conflict dialog | `/conflicts` → Click resolve |
|
||||||
| `unlinked-books.webp` | Unlinked books list | `/unlinked-books` |
|
| `unlinked-books.webp` | Unlinked books list | `/unlinked-books` |
|
||||||
| `book-linking.webp` | Link book to metadata | `/unlinked-books` → Click link |
|
| `book-linking.webp` | Link book to metadata | `/unlinked-books` → Click link |
|
||||||
| `sync-success.webp` | Successful sync indicator | Any page after sync |
|
| `sync-success.webp` | Successful sync indicator | Any page after sync |
|
||||||
|
|
||||||
**Estimated Screenshots**: ~5
|
**Estimated Screenshots**: ~5
|
||||||
|
|
||||||
### 5. Settings Guide Screenshots
|
### 5. Settings Guide Screenshots
|
||||||
|
|
||||||
**File**: `docs/user/settings-guide.md`
|
**File**: `docs/user/settings-guide.md`
|
||||||
|
|
||||||
**Screenshots Needed**:
|
**Screenshots Needed**:
|
||||||
|
|
||||||
| Screenshot Name | Description | Page/Action |
|
| Screenshot Name | Description | Page/Action |
|
||||||
|-----------------|-------------|-------------|
|
| ----------------------- | ------------------------ | --------------------------------- |
|
||||||
| `profile-overview.webp` | Profile settings page | `/admin/profile` |
|
| `profile-overview.webp` | Profile settings page | `/admin/profile` |
|
||||||
| `update-username.webp` | Username change form | `/admin/profile` |
|
| `update-username.webp` | Username change form | `/admin/profile` |
|
||||||
| `update-email.webp` | Email change form | `/admin/profile` |
|
| `update-email.webp` | Email change form | `/admin/profile` |
|
||||||
| `change-password.webp` | Password change form | `/admin/profile` |
|
| `change-password.webp` | Password change form | `/admin/profile` |
|
||||||
| `theme-selector.webp` | Theme selection dropdown | `/admin/profile` (if implemented) |
|
| `theme-selector.webp` | Theme selection dropdown | `/admin/profile` (if implemented) |
|
||||||
|
|
||||||
**Estimated Screenshots**: ~5
|
**Estimated Screenshots**: ~5
|
||||||
|
|
||||||
@@ -228,24 +250,24 @@ Before running the screenshot automation, ask the user:
|
|||||||
### `screenshots/config.ts` - Playwright Configuration
|
### `screenshots/config.ts` - Playwright Configuration
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { defineConfig, devices } from '@playwright/test';
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: './',
|
testDir: "./",
|
||||||
fullyParallel: false,
|
fullyParallel: false,
|
||||||
retries: 1,
|
retries: 1,
|
||||||
reporter: 'list',
|
reporter: "list",
|
||||||
use: {
|
use: {
|
||||||
baseURL: process.env.BASE_URL || 'http://localhost:8765',
|
baseURL: process.env.BASE_URL || "http://localhost:8765",
|
||||||
trace: 'on-first-retry',
|
trace: "on-first-retry",
|
||||||
screenshot: 'only-on-failure',
|
screenshot: "only-on-failure",
|
||||||
},
|
},
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: 'chromium-desktop',
|
name: "chromium-desktop",
|
||||||
use: {
|
use: {
|
||||||
...devices['Desktop Chrome'],
|
...devices["Desktop Chrome"],
|
||||||
viewport: { width: 1920, height: 1080 }
|
viewport: { width: 1920, height: 1080 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -255,70 +277,79 @@ export default defineConfig({
|
|||||||
### `screenshots/auth.spec.ts` - Authentication Screenshots
|
### `screenshots/auth.spec.ts` - Authentication Screenshots
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
test.describe('Authentication Screenshots', () => {
|
test.describe("Authentication Screenshots", () => {
|
||||||
test('Login page', async ({ page }) => {
|
test("Login page", async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto("/");
|
||||||
|
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: 'docs/images/user/login-page.webp',
|
path: "docs/images/user/login-page.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('User login flow', async ({ page }) => {
|
test("User login flow", async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto("/");
|
||||||
|
|
||||||
await page.fill('input[name="login"]', process.env.USER_USERNAME || 'user');
|
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="password"]',
|
||||||
|
process.env.USER_PASSWORD || "password",
|
||||||
|
);
|
||||||
|
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: 'docs/images/user/login-form-filled.webp',
|
path: "docs/images/user/login-form-filled.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.click('button[type="submit"]');
|
await page.click('button[type="submit"]');
|
||||||
await page.waitForURL('/dashboard', { timeout: 5000 });
|
await page.waitForURL("/dashboard", { timeout: 5000 });
|
||||||
|
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: 'docs/images/user/dashboard-after-login.webp',
|
path: "docs/images/user/dashboard-after-login.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Admin login flow', async ({ page }) => {
|
test("Admin login flow", async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto("/");
|
||||||
|
|
||||||
await page.fill('input[name="login"]', process.env.ADMIN_USERNAME || 'admin');
|
await page.fill(
|
||||||
await page.fill('input[name="password"]', process.env.ADMIN_PASSWORD || 'password');
|
'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.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({
|
await page.screenshot({
|
||||||
path: 'docs/images/admin/admin-dashboard.webp',
|
path: "docs/images/admin/admin-dashboard.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Registration page', async ({ page }) => {
|
test("Registration page", async ({ page }) => {
|
||||||
await page.goto('/register');
|
await page.goto("/register");
|
||||||
|
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: 'docs/images/user/registration-page.webp',
|
path: "docs/images/user/registration-page.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.fill('input[name="email"]', 'newuser@example.com');
|
await page.fill('input[name="email"]', "newuser@example.com");
|
||||||
await page.fill('input[name="username"]', 'newuser');
|
await page.fill('input[name="username"]', "newuser");
|
||||||
await page.fill('input[name="password"]', 'SecurePass123!');
|
await page.fill('input[name="password"]', "SecurePass123!");
|
||||||
|
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: 'docs/images/user/registration-form-filled.webp',
|
path: "docs/images/user/registration-form-filled.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -327,112 +358,120 @@ test.describe('Authentication Screenshots', () => {
|
|||||||
### `screenshots/user-workflows.spec.ts` - User Guide Screenshots
|
### `screenshots/user-workflows.spec.ts` - User Guide Screenshots
|
||||||
|
|
||||||
```typescript
|
```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 }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto("/");
|
||||||
await page.fill('input[name="login"]', process.env.USER_USERNAME || 'user');
|
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="password"]',
|
||||||
|
process.env.USER_PASSWORD || "password",
|
||||||
|
);
|
||||||
await page.click('button[type="submit"]');
|
await page.click('button[type="submit"]');
|
||||||
await page.waitForURL('/dashboard', { timeout: 5000 });
|
await page.waitForURL("/dashboard", { timeout: 5000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Dashboard overview', async ({ page }) => {
|
test("Dashboard overview", async ({ page }) => {
|
||||||
await page.goto('/dashboard');
|
await page.goto("/dashboard");
|
||||||
|
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: 'docs/images/user/dashboard-overview.webp',
|
path: "docs/images/user/dashboard-overview.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Library grid view', async ({ page }) => {
|
test("Library grid view", async ({ page }) => {
|
||||||
await page.goto('/dashboard');
|
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()) {
|
if (await firstLibrary.isVisible()) {
|
||||||
await firstLibrary.click();
|
await firstLibrary.click();
|
||||||
await page.waitForURL(/\/dashboard/, { timeout: 5000 });
|
await page.waitForURL(/\/dashboard/, { timeout: 5000 });
|
||||||
|
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: 'docs/images/user/library-grid.webp',
|
path: "docs/images/user/library-grid.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Search functionality', async ({ page }) => {
|
test("Search functionality", async ({ page }) => {
|
||||||
await page.goto('/dashboard');
|
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.waitForTimeout(1000);
|
||||||
|
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: 'docs/images/user/search-results.webp',
|
path: "docs/images/user/search-results.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Filter panel', async ({ page }) => {
|
test("Filter panel", async ({ page }) => {
|
||||||
await page.goto('/dashboard');
|
await page.goto("/dashboard");
|
||||||
|
|
||||||
const filterPanel = page.locator('.filter-panel details');
|
const filterPanel = page.locator(".filter-panel details");
|
||||||
if (await filterPanel.isVisible()) {
|
if (await filterPanel.isVisible()) {
|
||||||
await filterPanel.click();
|
await filterPanel.click();
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: 'docs/images/user/filter-panel-open.webp',
|
path: "docs/images/user/filter-panel-open.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Collections list', async ({ page }) => {
|
test("Collections list", async ({ page }) => {
|
||||||
await page.goto('/collections');
|
await page.goto("/collections");
|
||||||
|
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: 'docs/images/user/collections-list.webp',
|
path: "docs/images/user/collections-list.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Create collection modal', async ({ page }) => {
|
test("Create collection modal", async ({ page }) => {
|
||||||
await page.goto('/collections');
|
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-name", "My Reading List");
|
||||||
await page.fill('#collection-description', 'Books I want to read');
|
await page.fill("#collection-description", "Books I want to read");
|
||||||
|
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: 'docs/images/user/create-collection-modal.webp',
|
path: "docs/images/user/create-collection-modal.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Reading progress view', async ({ page }) => {
|
test("Reading progress view", async ({ page }) => {
|
||||||
await page.goto('/progress');
|
await page.goto("/progress");
|
||||||
|
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: 'docs/images/user/reading-progress.webp',
|
path: "docs/images/user/reading-progress.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Analytics view', async ({ page }) => {
|
test("Analytics view", async ({ page }) => {
|
||||||
await page.goto('/analytics');
|
await page.goto("/analytics");
|
||||||
|
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: 'docs/images/user/analytics-view.webp',
|
path: "docs/images/user/analytics-view.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -441,82 +480,92 @@ test.describe('User Guide Screenshots', () => {
|
|||||||
### `screenshots/admin-workflows.spec.ts` - Admin Guide Screenshots
|
### `screenshots/admin-workflows.spec.ts` - Admin Guide Screenshots
|
||||||
|
|
||||||
```typescript
|
```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 }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto("/");
|
||||||
await page.fill('input[name="login"]', process.env.ADMIN_USERNAME || 'admin');
|
await page.fill(
|
||||||
await page.fill('input[name="password"]', process.env.ADMIN_PASSWORD || 'password');
|
'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.click('button[type="submit"]');
|
||||||
await page.waitForURL('/dashboard', { timeout: 5000 });
|
await page.waitForURL("/dashboard", { timeout: 5000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Admin dashboard', async ({ page }) => {
|
test("Admin dashboard", async ({ page }) => {
|
||||||
await page.goto('/admin');
|
await page.goto("/admin");
|
||||||
|
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: 'docs/images/admin/admin-dashboard.webp',
|
path: "docs/images/admin/admin-dashboard.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('User management', async ({ page }) => {
|
test("User management", async ({ page }) => {
|
||||||
await page.goto('/admin');
|
await page.goto("/admin");
|
||||||
|
|
||||||
const userSection = page.locator('[data-section="users"], text="Users"');
|
const userSection = page.locator('[data-section="users"], text="Users"');
|
||||||
if (await userSection.isVisible()) {
|
if (await userSection.isVisible()) {
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: 'docs/images/admin/user-management.webp',
|
path: "docs/images/admin/user-management.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Library management', async ({ page }) => {
|
test("Library management", async ({ page }) => {
|
||||||
await page.goto('/admin/library');
|
await page.goto("/admin/library");
|
||||||
|
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: 'docs/images/admin/library-management.webp',
|
path: "docs/images/admin/library-management.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Add library form', async ({ page }) => {
|
test("Add library form", async ({ page }) => {
|
||||||
await page.goto('/admin/library');
|
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()) {
|
if (await addLibraryBtn.isVisible()) {
|
||||||
await addLibraryBtn.click();
|
await addLibraryBtn.click();
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: 'docs/images/admin/add-library-form.webp',
|
path: "docs/images/admin/add-library-form.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Profile settings', async ({ page }) => {
|
test("Profile settings", async ({ page }) => {
|
||||||
await page.goto('/admin/profile');
|
await page.goto("/admin/profile");
|
||||||
|
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: 'docs/images/admin/profile-settings.webp',
|
path: "docs/images/admin/profile-settings.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Theme selector', async ({ page }) => {
|
test("Theme selector", async ({ page }) => {
|
||||||
await page.goto('/admin/profile');
|
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()) {
|
if (await themeSelect.isVisible()) {
|
||||||
await themeSelect.click();
|
await themeSelect.click();
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: 'docs/images/admin/theme-selector.webp',
|
path: "docs/images/admin/theme-selector.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -526,83 +575,97 @@ test.describe('Admin Guide Screenshots', () => {
|
|||||||
### `screenshots/device-workflows.spec.ts` - Device Setup Screenshots
|
### `screenshots/device-workflows.spec.ts` - Device Setup Screenshots
|
||||||
|
|
||||||
```typescript
|
```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 }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto("/");
|
||||||
await page.fill('input[name="login"]', process.env.USER_USERNAME || 'user');
|
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="password"]',
|
||||||
|
process.env.USER_PASSWORD || "password",
|
||||||
|
);
|
||||||
await page.click('button[type="submit"]');
|
await page.click('button[type="submit"]');
|
||||||
await page.waitForURL('/dashboard', { timeout: 5000 });
|
await page.waitForURL("/dashboard", { timeout: 5000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Device list page', async ({ page }) => {
|
test("Device list page", async ({ page }) => {
|
||||||
await page.goto('/devices');
|
await page.goto("/devices");
|
||||||
|
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: 'docs/images/devices/device-list.webp',
|
path: "docs/images/devices/device-list.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Add device modal', async ({ page }) => {
|
test("Add device modal", async ({ page }) => {
|
||||||
await page.goto('/devices');
|
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({
|
await page.screenshot({
|
||||||
path: 'docs/images/devices/add-device-modal.webp',
|
path: "docs/images/devices/add-device-modal.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Kobo device form', async ({ page }) => {
|
test("Kobo device form", async ({ page }) => {
|
||||||
await page.goto('/devices');
|
await page.goto("/devices");
|
||||||
|
|
||||||
await page.click('button:has-text("Add New Device")');
|
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"]');
|
const deviceTypeSelect = page.locator('select[name="device_type"]');
|
||||||
if (await deviceTypeSelect.isVisible()) {
|
if (await deviceTypeSelect.isVisible()) {
|
||||||
await deviceTypeSelect.selectOption('kobo');
|
await deviceTypeSelect.selectOption("kobo");
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: 'docs/images/devices/device-form-kobo.webp',
|
path: "docs/images/devices/device-form-kobo.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('KOReader device form', async ({ page }) => {
|
test("KOReader device form", async ({ page }) => {
|
||||||
await page.goto('/devices');
|
await page.goto("/devices");
|
||||||
|
|
||||||
await page.click('button:has-text("Add New Device")');
|
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"]');
|
const deviceTypeSelect = page.locator('select[name="device_type"]');
|
||||||
if (await deviceTypeSelect.isVisible()) {
|
if (await deviceTypeSelect.isVisible()) {
|
||||||
await deviceTypeSelect.selectOption('koreader');
|
await deviceTypeSelect.selectOption("koreader");
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: 'docs/images/devices/device-form-koreader.webp',
|
path: "docs/images/devices/device-form-koreader.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Sync queue', async ({ page }) => {
|
test("Sync queue", async ({ page }) => {
|
||||||
await page.goto('/queue');
|
await page.goto("/queue");
|
||||||
|
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: 'docs/images/devices/sync-queue.webp',
|
path: "docs/images/devices/sync-queue.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -611,32 +674,35 @@ test.describe('Device Setup Screenshots', () => {
|
|||||||
### `screenshots/sync-workflows.spec.ts` - Sync Guide Screenshots
|
### `screenshots/sync-workflows.spec.ts` - Sync Guide Screenshots
|
||||||
|
|
||||||
```typescript
|
```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 }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto("/");
|
||||||
await page.fill('input[name="login"]', process.env.USER_USERNAME || 'user');
|
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="password"]',
|
||||||
|
process.env.USER_PASSWORD || "password",
|
||||||
|
);
|
||||||
await page.click('button[type="submit"]');
|
await page.click('button[type="submit"]');
|
||||||
await page.waitForURL('/dashboard', { timeout: 5000 });
|
await page.waitForURL("/dashboard", { timeout: 5000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Sync conflicts page', async ({ page }) => {
|
test("Sync conflicts page", async ({ page }) => {
|
||||||
await page.goto('/conflicts');
|
await page.goto("/conflicts");
|
||||||
|
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: 'docs/images/sync/sync-conflicts.webp',
|
path: "docs/images/sync/sync-conflicts.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Unlinked books page', async ({ page }) => {
|
test("Unlinked books page", async ({ page }) => {
|
||||||
await page.goto('/unlinked-books');
|
await page.goto("/unlinked-books");
|
||||||
|
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: 'docs/images/sync/unlinked-books.webp',
|
path: "docs/images/sync/unlinked-books.webp",
|
||||||
fullPage: true
|
fullPage: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -645,21 +711,25 @@ test.describe('Sync Guide Screenshots', () => {
|
|||||||
## Running the Tests
|
## Running the Tests
|
||||||
|
|
||||||
### Run all tests
|
### Run all tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx playwright test
|
npx playwright test
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run specific test file
|
### Run specific test file
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx playwright test auth.spec.ts
|
npx playwright test auth.spec.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run in headed mode (see browser)
|
### Run in headed mode (see browser)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx playwright test --headed
|
npx playwright test --headed
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run with debug mode
|
### Run with debug mode
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx playwright test --debug
|
npx playwright test --debug
|
||||||
```
|
```
|
||||||
@@ -667,10 +737,13 @@ npx playwright test --debug
|
|||||||
## Markdown Update Strategy
|
## Markdown Update Strategy
|
||||||
|
|
||||||
### Option 1: Create New Markdown
|
### Option 1: Create New Markdown
|
||||||
|
|
||||||
Generate fresh markdown files with embedded screenshots.
|
Generate fresh markdown files with embedded screenshots.
|
||||||
|
|
||||||
### Option 2: Update Existing Markdown
|
### Option 2: Update Existing Markdown
|
||||||
|
|
||||||
Update existing markdown files by:
|
Update existing markdown files by:
|
||||||
|
|
||||||
1. Finding section headers
|
1. Finding section headers
|
||||||
2. Inserting screenshot references after relevant steps
|
2. Inserting screenshot references after relevant steps
|
||||||
3. Using alt text to describe what's shown
|
3. Using alt text to describe what's shown
|
||||||
@@ -690,25 +763,30 @@ Update existing markdown files by:
|
|||||||
## Workflow When Ready
|
## Workflow When Ready
|
||||||
|
|
||||||
### Step 1: Verify Frontend Complete
|
### Step 1: Verify Frontend Complete
|
||||||
|
|
||||||
- Check all pages listed in "Prerequisites" are working
|
- Check all pages listed in "Prerequisites" are working
|
||||||
- Confirm no TODO placeholders in templates
|
- Confirm no TODO placeholders in templates
|
||||||
|
|
||||||
### Step 2: Seed Test Data
|
### Step 2: Seed Test Data
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Add sample books, collections, devices
|
# Add sample books, collections, devices
|
||||||
# Create test admin and user accounts
|
# Create test admin and user accounts
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 3: Set Up Playwright
|
### Step 3: Set Up Playwright
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -D @playwright/test
|
npm install -D @playwright/test
|
||||||
npx playwright install chromium
|
npx playwright install chromium
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 4: Create Environment File
|
### Step 4: Create Environment File
|
||||||
|
|
||||||
Create `.env.screenshots` with the test credentials
|
Create `.env.screenshots` with the test credentials
|
||||||
|
|
||||||
### Step 5: Run Screenshot Scripts
|
### Step 5: Run Screenshot Scripts
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run all screenshot workflows
|
# Run all screenshot workflows
|
||||||
npx playwright test
|
npx playwright test
|
||||||
@@ -719,6 +797,7 @@ npx playwright test user-workflows.spec.ts
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Step 6: Review and Adjust
|
### Step 6: Review and Adjust
|
||||||
|
|
||||||
- Manually review screenshots
|
- Manually review screenshots
|
||||||
- Retake any that need adjustment
|
- Retake any that need adjustment
|
||||||
- Update markdown files if needed
|
- Update markdown files if needed
|
||||||
@@ -742,15 +821,15 @@ When you're ready to run these, you may need to update:
|
|||||||
|
|
||||||
## Estimated Time Investment
|
## Estimated Time Investment
|
||||||
|
|
||||||
| Task | Time |
|
| Task | Time |
|
||||||
|------|------|
|
| ------------------------------ | ------------- |
|
||||||
| Install & configure Playwright | 15 min |
|
| Install & configure Playwright | 15 min |
|
||||||
| Seed test database | 30 min |
|
| Seed test database | 30 min |
|
||||||
| Write Playwright scripts | 2-3 hours |
|
| Write Playwright scripts | 2-3 hours |
|
||||||
| Run screenshot automation | 10 min |
|
| Run screenshot automation | 10 min |
|
||||||
| Review & retake screenshots | 30-60 min |
|
| Review & retake screenshots | 30-60 min |
|
||||||
| Update markdown files | 30-60 min |
|
| Update markdown files | 30-60 min |
|
||||||
| **Total** | **4-6 hours** |
|
| **Total** | **4-6 hours** |
|
||||||
|
|
||||||
## Future Enhancements
|
## Future Enhancements
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ This document describes the shared test data used across Go integration tests an
|
|||||||
## Test Users
|
## Test Users
|
||||||
|
|
||||||
### Main Admin Test User
|
### Main Admin Test User
|
||||||
|
|
||||||
This is the primary test user used in most integration tests.
|
This is the primary test user used in most integration tests.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -19,15 +20,18 @@ This is the primary test user used in most integration tests.
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Used in:**
|
**Used in:**
|
||||||
|
|
||||||
- Go Tests: `cmd/server/tests/test_helpers.go` (getTestUserID, loginTestUser)
|
- Go Tests: `cmd/server/tests/test_helpers.go` (getTestUserID, loginTestUser)
|
||||||
- Bruno: `user/auth/Login User.yml`, `user/auth/Register User.yml`
|
- Bruno: `user/auth/Login User.yml`, `user/auth/Register User.yml`
|
||||||
|
|
||||||
**Notes:**
|
**Notes:**
|
||||||
|
|
||||||
- Automatically created if doesn't exist
|
- Automatically created if doesn't exist
|
||||||
- Deleted and recreated in tests to ensure fresh state
|
- Deleted and recreated in tests to ensure fresh state
|
||||||
- Used for authentication in most test scenarios
|
- Used for authentication in most test scenarios
|
||||||
|
|
||||||
### Max Devices Test User
|
### Max Devices Test User
|
||||||
|
|
||||||
Used specifically for testing device limit functionality.
|
Used specifically for testing device limit functionality.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -41,9 +45,11 @@ Used specifically for testing device limit functionality.
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Used in:**
|
**Used in:**
|
||||||
|
|
||||||
- Go Tests: `cmd/server/tests/device_cap_test.go` (createTestUserForMaxDevices)
|
- Go Tests: `cmd/server/tests/device_cap_test.go` (createTestUserForMaxDevices)
|
||||||
|
|
||||||
### Secondary Admin Test User
|
### Secondary Admin Test User
|
||||||
|
|
||||||
Used for testing admin creation restrictions and multi-admin scenarios.
|
Used for testing admin creation restrictions and multi-admin scenarios.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -58,11 +64,13 @@ Used for testing admin creation restrictions and multi-admin scenarios.
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Used in:**
|
**Used in:**
|
||||||
|
|
||||||
- Bruno: `user/admin/Register Admin User.yml`
|
- Bruno: `user/admin/Register Admin User.yml`
|
||||||
|
|
||||||
## Test Libraries
|
## Test Libraries
|
||||||
|
|
||||||
### Standard Test Library
|
### Standard Test Library
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "Test Library",
|
"name": "Test Library",
|
||||||
@@ -72,10 +80,12 @@ Used for testing admin creation restrictions and multi-admin scenarios.
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Used in:**
|
**Used in:**
|
||||||
|
|
||||||
- Go Tests: `cmd/server/tests/test_helpers.go` (createTestEbookID)
|
- Go Tests: `cmd/server/tests/test_helpers.go` (createTestEbookID)
|
||||||
- Multiple test files for library management
|
- Multiple test files for library management
|
||||||
|
|
||||||
### Search Test Library
|
### Search Test Library
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "Search Test Library",
|
"name": "Search Test Library",
|
||||||
@@ -85,11 +95,13 @@ Used for testing admin creation restrictions and multi-admin scenarios.
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Used in:**
|
**Used in:**
|
||||||
|
|
||||||
- Go Tests: `cmd/server/tests/search_test.go`
|
- Go Tests: `cmd/server/tests/search_test.go`
|
||||||
|
|
||||||
## Test Books/Media Items
|
## Test Books/Media Items
|
||||||
|
|
||||||
### Standard Test Ebook
|
### Standard Test Ebook
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"title": "Test Ebook",
|
"title": "Test Ebook",
|
||||||
@@ -101,10 +113,13 @@ Used for testing admin creation restrictions and multi-admin scenarios.
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Used in:**
|
**Used in:**
|
||||||
|
|
||||||
- Go Tests: `cmd/server/tests/test_helpers.go` (createTestEbookID)
|
- Go Tests: `cmd/server/tests/test_helpers.go` (createTestEbookID)
|
||||||
|
|
||||||
### Test Book Variants
|
### Test Book Variants
|
||||||
|
|
||||||
Multiple test books with different titles for testing:
|
Multiple test books with different titles for testing:
|
||||||
|
|
||||||
- "Test Book 1"
|
- "Test Book 1"
|
||||||
- "Test Book 2"
|
- "Test Book 2"
|
||||||
- "Test Book Title"
|
- "Test Book Title"
|
||||||
@@ -113,6 +128,7 @@ Multiple test books with different titles for testing:
|
|||||||
## Test Devices
|
## Test Devices
|
||||||
|
|
||||||
Test devices typically follow this pattern:
|
Test devices typically follow this pattern:
|
||||||
|
|
||||||
- Device ID: UUID format
|
- Device ID: UUID format
|
||||||
- Device Name: "Test Device" or descriptive names
|
- Device Name: "Test Device" or descriptive names
|
||||||
- User association: Linked to test users
|
- User association: Linked to test users
|
||||||
@@ -139,23 +155,28 @@ Test devices typically follow this pattern:
|
|||||||
## File Paths
|
## File Paths
|
||||||
|
|
||||||
### Container Paths (inside Docker container)
|
### Container Paths (inside Docker container)
|
||||||
|
|
||||||
- Uploads: `/app/uploads`
|
- Uploads: `/app/uploads`
|
||||||
- Cache: `/app/cache/kepub`
|
- Cache: `/app/cache/kepub`
|
||||||
|
|
||||||
### Host Paths (when running tests from host)
|
### Host Paths (when running tests from host)
|
||||||
|
|
||||||
- Uploads: `./uploads`
|
- Uploads: `./uploads`
|
||||||
- Cache: Docker volume (not on host filesystem)
|
- Cache: Docker volume (not on host filesystem)
|
||||||
|
|
||||||
## How to Use This Data
|
## How to Use This Data
|
||||||
|
|
||||||
### In Bruno Tests
|
### In Bruno Tests
|
||||||
|
|
||||||
1. Start the server: `podman compose up -d`
|
1. Start the server: `podman compose up -d`
|
||||||
2. Run "Register User" to create the test admin user
|
2. Run "Register User" to create the test admin user
|
||||||
3. Run "Login User" to get the JWT token
|
3. Run "Login User" to get the JWT token
|
||||||
4. Use the token for authenticated requests
|
4. Use the token for authenticated requests
|
||||||
|
|
||||||
### In Go Tests
|
### In Go Tests
|
||||||
|
|
||||||
The test helpers automatically create and clean up test data:
|
The test helpers automatically create and clean up test data:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
ts, db, cfg := setupTestServer(t)
|
ts, db, cfg := setupTestServer(t)
|
||||||
token := loginTestUser(t, ts, db)
|
token := loginTestUser(t, ts, db)
|
||||||
@@ -163,7 +184,9 @@ userID := getTestUserID(t, db)
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Cross-Referencing
|
### Cross-Referencing
|
||||||
|
|
||||||
When you find a bug in Bruno tests:
|
When you find a bug in Bruno tests:
|
||||||
|
|
||||||
1. Check the same scenario in Go tests using the same credentials
|
1. Check the same scenario in Go tests using the same credentials
|
||||||
2. Use the same email/password to debug
|
2. Use the same email/password to debug
|
||||||
3. Verify the database state matches expectations
|
3. Verify the database state matches expectations
|
||||||
@@ -171,6 +194,7 @@ When you find a bug in Bruno tests:
|
|||||||
## Resetting Test Data
|
## Resetting Test Data
|
||||||
|
|
||||||
### Reset Database
|
### Reset Database
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Stop containers and remove volumes
|
# Stop containers and remove volumes
|
||||||
podman compose down -v
|
podman compose down -v
|
||||||
@@ -180,7 +204,9 @@ podman compose up -d
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Reset Specific Test User
|
### Reset Specific Test User
|
||||||
|
|
||||||
If you need to recreate just the test user:
|
If you need to recreate just the test user:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Login to database
|
# Login to database
|
||||||
podman exec -it bookhoard_db psql -U postgres -d bookhoard
|
podman exec -it bookhoard_db psql -U postgres -d bookhoard
|
||||||
@@ -202,6 +228,7 @@ DELETE FROM users WHERE email = 'testuser@example.com';
|
|||||||
## Adding New Test Data
|
## Adding New Test Data
|
||||||
|
|
||||||
When adding new test data:
|
When adding new test data:
|
||||||
|
|
||||||
1. Choose descriptive names following the pattern "Test X"
|
1. Choose descriptive names following the pattern "Test X"
|
||||||
2. Use consistent email format: `testpurpose@example.com`
|
2. Use consistent email format: `testpurpose@example.com`
|
||||||
3. Document in this file for cross-reference
|
3. Document in this file for cross-reference
|
||||||
|
|||||||
+85
-46
@@ -3,6 +3,7 @@
|
|||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Fix file and cover image serving to support:
|
Fix file and cover image serving to support:
|
||||||
|
|
||||||
1. Multiple library folders in docker compose (flexible mount points)
|
1. Multiple library folders in docker compose (flexible mount points)
|
||||||
2. Keep files with books (no hardcoded paths)
|
2. Keep files with books (no hardcoded paths)
|
||||||
3. Store relative paths in database (for both files AND covers)
|
3. Store relative paths in database (for both files AND covers)
|
||||||
@@ -12,12 +13,14 @@ Fix file and cover image serving to support:
|
|||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Current Behavior
|
### Current Behavior
|
||||||
|
|
||||||
- File path stored as absolute: `/app/uploads/Jane Austen/Pride and Prejudice/book.epub`
|
- File path stored as absolute: `/app/uploads/Jane Austen/Pride and Prejudice/book.epub`
|
||||||
- Cover path stored as absolute: `/app/uploads/Jane Austen/Pride and Prejudice/cover.jpg`
|
- Cover path stored as absolute: `/app/uploads/Jane Austen/Pride and Prejudice/cover.jpg`
|
||||||
- Frontend uses path directly - doesn't work (browser can't access container paths)
|
- Frontend uses path directly - doesn't work (browser can't access container paths)
|
||||||
- No route serves `/app/uploads/*`
|
- No route serves `/app/uploads/*`
|
||||||
|
|
||||||
### Target Behavior
|
### Target Behavior
|
||||||
|
|
||||||
- File path stored as relative: `Jane Austen/Pride and Prejudice/book.epub`
|
- File path stored as relative: `Jane Austen/Pride and Prejudice/book.epub`
|
||||||
- Cover path stored as relative: `Jane Austen/Pride and Prejudice/cover.jpg`
|
- Cover path stored as relative: `Jane Austen/Pride and Prejudice/cover.jpg`
|
||||||
- Handler resolves relative path using library folder base path
|
- Handler resolves relative path using library folder base path
|
||||||
@@ -26,16 +29,21 @@ Fix file and cover image serving to support:
|
|||||||
- Works with mobile apps, Kobo, KOReader devices via same endpoints
|
- Works with mobile apps, Kobo, KOReader devices via same endpoints
|
||||||
|
|
||||||
### URL Format
|
### URL Format
|
||||||
|
|
||||||
To handle same relative paths in different libraries, use:
|
To handle same relative paths in different libraries, use:
|
||||||
|
|
||||||
```
|
```
|
||||||
/uploads/library-{library_id}/relative/path
|
/uploads/library-{library_id}/relative/path
|
||||||
```
|
```
|
||||||
|
|
||||||
- Requires JWT authentication (like API endpoints)
|
- Requires JWT authentication (like API endpoints)
|
||||||
- Works for both covers and book files
|
- Works for both covers and book files
|
||||||
- Single handler handles all file serving
|
- Single handler handles all file serving
|
||||||
|
|
||||||
### Universal Path Resolution
|
### Universal Path Resolution
|
||||||
|
|
||||||
All handlers use the same `LibraryService.ResolveMediaPath()` function:
|
All handlers use the same `LibraryService.ResolveMediaPath()` function:
|
||||||
|
|
||||||
- MediaHandler (downloads)
|
- MediaHandler (downloads)
|
||||||
- OPDSHandler (device cover images)
|
- OPDSHandler (device cover images)
|
||||||
- Future handlers
|
- Future handlers
|
||||||
@@ -53,16 +61,19 @@ This ensures one source of truth for path resolution.
|
|||||||
**Location**: In `internal/services/media_scanner.go` - wherever `FilePath` is set in the database insert
|
**Location**: In `internal/services/media_scanner.go` - wherever `FilePath` is set in the database insert
|
||||||
|
|
||||||
**Current code** (line 579):
|
**Current code** (line 579):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
FilePath: path, // path is absolute like /app/uploads/Author/Book/file.epub
|
FilePath: path, // path is absolute like /app/uploads/Author/Book/file.epub
|
||||||
```
|
```
|
||||||
|
|
||||||
**New code**:
|
**New code**:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
FilePath: s.getRelativePath(path),
|
FilePath: s.getRelativePath(path),
|
||||||
```
|
```
|
||||||
|
|
||||||
**Also update** line 617 for format file paths:
|
**Also update** line 617 for format file paths:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
FilePath: pgtype.Text{String: s.getRelativePath(format.FilePath), Valid: true},
|
FilePath: pgtype.Text{String: s.getRelativePath(format.FilePath), Valid: true},
|
||||||
```
|
```
|
||||||
@@ -74,6 +85,7 @@ FilePath: pgtype.Text{String: s.getRelativePath(format.FilePath), Valid: tr
|
|||||||
**Location**: In `internal/services/media_scanner.go` - wherever `metadata.CoverPath` is set
|
**Location**: In `internal/services/media_scanner.go` - wherever `metadata.CoverPath` is set
|
||||||
|
|
||||||
**Current code** (example at line 517):
|
**Current code** (example at line 517):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
if len(coverImage) > 0 && metadata.CoverPath == "" {
|
if len(coverImage) > 0 && metadata.CoverPath == "" {
|
||||||
coverPath := path + ".cover.jpg"
|
coverPath := path + ".cover.jpg"
|
||||||
@@ -84,6 +96,7 @@ if len(coverImage) > 0 && metadata.CoverPath == "" {
|
|||||||
```
|
```
|
||||||
|
|
||||||
**New code**:
|
**New code**:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
if len(coverImage) > 0 && metadata.CoverPath == "" {
|
if len(coverImage) > 0 && metadata.CoverPath == "" {
|
||||||
coverPath := path + ".cover.jpg"
|
coverPath := path + ".cover.jpg"
|
||||||
@@ -95,6 +108,7 @@ if len(coverImage) > 0 && metadata.CoverPath == "" {
|
|||||||
```
|
```
|
||||||
|
|
||||||
**All locations where metadata.CoverPath is set**:
|
**All locations where metadata.CoverPath is set**:
|
||||||
|
|
||||||
- Line 517 (main cover)
|
- Line 517 (main cover)
|
||||||
- Line 645 (sidecar cover)
|
- Line 645 (sidecar cover)
|
||||||
- Line 651 (sidecar cover alternative)
|
- Line 651 (sidecar cover alternative)
|
||||||
@@ -209,6 +223,7 @@ Note: The handler already has `libraryService` injected, so this just calls thro
|
|||||||
#### Modify DownloadBook function
|
#### Modify DownloadBook function
|
||||||
|
|
||||||
**Current code** (line 103-144):
|
**Current code** (line 103-144):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func (h *MediaHandler) DownloadBook(c echo.Context) error {
|
func (h *MediaHandler) DownloadBook(c echo.Context) error {
|
||||||
// ...
|
// ...
|
||||||
@@ -227,6 +242,7 @@ func (h *MediaHandler) DownloadBook(c echo.Context) error {
|
|||||||
```
|
```
|
||||||
|
|
||||||
**New code**:
|
**New code**:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func (h *MediaHandler) DownloadBook(c echo.Context) error {
|
func (h *MediaHandler) DownloadBook(c echo.Context) error {
|
||||||
// ...
|
// ...
|
||||||
@@ -332,6 +348,7 @@ e.GET("/uploads/library-:id/*", createJWTMiddleware(cfg), cfg.MediaHandler.Serve
|
|||||||
#### Modify GetCoverImage function
|
#### Modify GetCoverImage function
|
||||||
|
|
||||||
**Current code** (around line 477-549):
|
**Current code** (around line 477-549):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func (h *OPDSHandler) GetCoverImage(c echo.Context) error {
|
func (h *OPDSHandler) GetCoverImage(c echo.Context) error {
|
||||||
// ...
|
// ...
|
||||||
@@ -349,6 +366,7 @@ func (h *OPDSHandler) GetCoverImage(c echo.Context) error {
|
|||||||
```
|
```
|
||||||
|
|
||||||
**New code**:
|
**New code**:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func (h *OPDSHandler) GetCoverImage(c echo.Context) error {
|
func (h *OPDSHandler) GetCoverImage(c echo.Context) error {
|
||||||
// ...
|
// ...
|
||||||
@@ -494,6 +512,7 @@ cfg.CollectionHandler, err = handlers.NewCollectionHandler(cfg.Queries, cfg.Libr
|
|||||||
**File**: `internal/handlers/collections.go`
|
**File**: `internal/handlers/collections.go`
|
||||||
|
|
||||||
**Current code** (lines 193-201 in GetCollection function):
|
**Current code** (lines 193-201 in GetCollection function):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
bookList := make([]BookInfo, 0, len(books))
|
bookList := make([]BookInfo, 0, len(books))
|
||||||
for _, book := range books {
|
for _, book := range books {
|
||||||
@@ -507,6 +526,7 @@ for _, book := range books {
|
|||||||
```
|
```
|
||||||
|
|
||||||
**New code**:
|
**New code**:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
bookList := make([]BookInfo, 0, len(books))
|
bookList := make([]BookInfo, 0, len(books))
|
||||||
for _, book := range books {
|
for _, book := range books {
|
||||||
@@ -553,6 +573,7 @@ func (h *CollectionHandler) resolveCoverURL(libraryID pgtype.UUID, coverPath pgt
|
|||||||
**File**: `internal/handlers/collections.go`
|
**File**: `internal/handlers/collections.go`
|
||||||
|
|
||||||
**Current code** (lines 620-641 in TestRules function):
|
**Current code** (lines 620-641 in TestRules function):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
var matches []BookMatch
|
var matches []BookMatch
|
||||||
for _, item := range mediaItems {
|
for _, item := range mediaItems {
|
||||||
@@ -579,6 +600,7 @@ for _, item := range mediaItems {
|
|||||||
```
|
```
|
||||||
|
|
||||||
**New code**:
|
**New code**:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
var matches []BookMatch
|
var matches []BookMatch
|
||||||
for _, item := range mediaItems {
|
for _, item := range mediaItems {
|
||||||
@@ -609,6 +631,7 @@ for _, item := range mediaItems {
|
|||||||
**Location 1 - PreviewCollection function** (lines 910-919):
|
**Location 1 - PreviewCollection function** (lines 910-919):
|
||||||
|
|
||||||
**Current code**:
|
**Current code**:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
bookCards := make([]BookInfo, len(matchedItems))
|
bookCards := make([]BookInfo, len(matchedItems))
|
||||||
for i, item := range matchedItems {
|
for i, item := range matchedItems {
|
||||||
@@ -623,6 +646,7 @@ for i, item := range matchedItems {
|
|||||||
```
|
```
|
||||||
|
|
||||||
**New code**:
|
**New code**:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
bookCards := make([]BookInfo, len(matchedItems))
|
bookCards := make([]BookInfo, len(matchedItems))
|
||||||
for i, item := range matchedItems {
|
for i, item := range matchedItems {
|
||||||
@@ -639,6 +663,7 @@ for i, item := range matchedItems {
|
|||||||
**Location 2 - mediaItemsToListMediaItemsRow helper** (line 935):
|
**Location 2 - mediaItemsToListMediaItemsRow helper** (line 935):
|
||||||
|
|
||||||
**Current code**:
|
**Current code**:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func mediaItemsToListMediaItemsRow(item database.MediaItems) database.ListMediaItemsRow {
|
func mediaItemsToListMediaItemsRow(item database.MediaItems) database.ListMediaItemsRow {
|
||||||
return database.ListMediaItemsRow{
|
return database.ListMediaItemsRow{
|
||||||
@@ -650,6 +675,7 @@ func mediaItemsToListMediaItemsRow(item database.MediaItems) database.ListMediaI
|
|||||||
```
|
```
|
||||||
|
|
||||||
**New code**:
|
**New code**:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// NOTE: This helper function doesn't have access to libraryID
|
// NOTE: This helper function doesn't have access to libraryID
|
||||||
// Consider refactoring to pass libraryID or handle URL resolution at call site
|
// Consider refactoring to pass libraryID or handle URL resolution at call site
|
||||||
@@ -708,6 +734,7 @@ func (h *Handler) resolveCoverURL(libraryID pgtype.UUID, coverPath pgtype.Text)
|
|||||||
**Location 1 - GetAllProgress function** (lines 286-289):
|
**Location 1 - GetAllProgress function** (lines 286-289):
|
||||||
|
|
||||||
**Current code**:
|
**Current code**:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
coverPath := ""
|
coverPath := ""
|
||||||
if mediaItem.CoverImagePath.Valid {
|
if mediaItem.CoverImagePath.Valid {
|
||||||
@@ -716,6 +743,7 @@ if mediaItem.CoverImagePath.Valid {
|
|||||||
```
|
```
|
||||||
|
|
||||||
**New code** (remove the manual resolution, use helper):
|
**New code** (remove the manual resolution, use helper):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
coverPath := h.resolveCoverURL(mediaItem.LibraryID, mediaItem.CoverImagePath)
|
coverPath := h.resolveCoverURL(mediaItem.LibraryID, mediaItem.CoverImagePath)
|
||||||
```
|
```
|
||||||
@@ -723,6 +751,7 @@ coverPath := h.resolveCoverURL(mediaItem.LibraryID, mediaItem.CoverImagePath)
|
|||||||
**Location 2 - GetAllProgressData function** (lines 357-360):
|
**Location 2 - GetAllProgressData function** (lines 357-360):
|
||||||
|
|
||||||
**Current code**:
|
**Current code**:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
coverPath := ""
|
coverPath := ""
|
||||||
if mediaItem.CoverImagePath.Valid {
|
if mediaItem.CoverImagePath.Valid {
|
||||||
@@ -731,6 +760,7 @@ if mediaItem.CoverImagePath.Valid {
|
|||||||
```
|
```
|
||||||
|
|
||||||
**New code**:
|
**New code**:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
coverPath := h.resolveCoverURL(mediaItem.LibraryID, mediaItem.CoverImagePath)
|
coverPath := h.resolveCoverURL(mediaItem.LibraryID, mediaItem.CoverImagePath)
|
||||||
```
|
```
|
||||||
@@ -742,6 +772,7 @@ coverPath := h.resolveCoverURL(mediaItem.LibraryID, mediaItem.CoverImagePath)
|
|||||||
**File**: `internal/handlers/media.go`
|
**File**: `internal/handlers/media.go`
|
||||||
|
|
||||||
Add to imports:
|
Add to imports:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
"bookhoard/internal/utils"
|
"bookhoard/internal/utils"
|
||||||
```
|
```
|
||||||
@@ -749,11 +780,13 @@ Add to imports:
|
|||||||
**GetMediaItem** - Find where it returns the response (around line 770):
|
**GetMediaItem** - Find where it returns the response (around line 770):
|
||||||
|
|
||||||
**Current code**:
|
**Current code**:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
return c.JSON(http.StatusOK, item)
|
return c.JSON(http.StatusOK, item)
|
||||||
```
|
```
|
||||||
|
|
||||||
**New code**:
|
**New code**:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||||
"id": uuid.UUID(item.ID.Bytes).String(),
|
"id": uuid.UUID(item.ID.Bytes).String(),
|
||||||
@@ -781,12 +814,14 @@ Wrap each item in the response with resolved URLs. The exact implementation depe
|
|||||||
**File**: `web/src/bookshelf.ts`
|
**File**: `web/src/bookshelf.ts`
|
||||||
|
|
||||||
**Current code** (line 49-50):
|
**Current code** (line 49-50):
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
${book.cover_image_path ?
|
${book.cover_image_path ?
|
||||||
`<img src="/covers/${book.cover_image_path}" alt="${book.title}" class="w-full h-48 object-cover rounded mb-2">` :
|
`<img src="/covers/${book.cover_image_path}" alt="${book.title}" class="w-full h-48 object-cover rounded mb-2">` :
|
||||||
```
|
```
|
||||||
|
|
||||||
**New code**:
|
**New code**:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
${book.cover_image_path ?
|
${book.cover_image_path ?
|
||||||
`<img src="${book.cover_image_path}" alt="${book.title}" class="w-full h-48 object-cover rounded mb-2">` :
|
`<img src="${book.cover_image_path}" alt="${book.title}" class="w-full h-48 object-cover rounded mb-2">` :
|
||||||
@@ -798,32 +833,34 @@ The backend now returns full URLs like `/uploads/library-{id}/path/to/cover.jpg`
|
|||||||
|
|
||||||
### Summary of Changes for Phase 7
|
### Summary of Changes for Phase 7
|
||||||
|
|
||||||
| File | Changes |
|
| File | Changes |
|
||||||
|------|---------|
|
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `internal/utils/mediaurl.go` | Create with `ResolveMediaURL()` function for URL resolution (one source of truth) |
|
| `internal/utils/mediaurl.go` | Create with `ResolveMediaURL()` function for URL resolution (one source of truth) |
|
||||||
| `internal/handlers/media.go` | Update GetMediaItem and ListMediaItems to use `utils.ResolveMediaURL()` for resolved URLs in responses |
|
| `internal/handlers/media.go` | Update GetMediaItem and ListMediaItems to use `utils.ResolveMediaURL()` for resolved URLs in responses |
|
||||||
| `internal/handlers/collections.go` | Use `utils.ResolveMediaURL()` in GetCollection, TestRules, PreviewCollection; update lines 193-201, 620-641, 910-919 |
|
| `internal/handlers/collections.go` | Use `utils.ResolveMediaURL()` in GetCollection, TestRules, PreviewCollection; update lines 193-201, 620-641, 910-919 |
|
||||||
| `internal/handlers/progress.go` | Use `utils.ResolveMediaURL()` in GetAllProgress; update lines 286-289, 357-360 |
|
| `internal/handlers/progress.go` | Use `utils.ResolveMediaURL()` in GetAllProgress; update lines 286-289, 357-360 |
|
||||||
| `web/src/bookshelf.ts` | Remove `/covers/` prefix from cover image URL |
|
| `web/src/bookshelf.ts` | Remove `/covers/` prefix from cover image URL |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Additional Plan Updates Needed
|
### Additional Plan Updates Needed
|
||||||
|
|
||||||
| Item | Status |
|
| Item | Status |
|
||||||
|------|--------|
|
| --------------------------------------------------- | ------------------------------------------------------------------- |
|
||||||
| Add `mi.library_id` to GetCollectionItems SQL query | Needs to be done before implementing Step 2 in collections.go |
|
| Add `mi.library_id` to GetCollectionItems SQL query | Needs to be done before implementing Step 2 in collections.go |
|
||||||
| Create `internal/utils/mediaurl.go` | Needs to be created before implementing URL resolution |
|
| Create `internal/utils/mediaurl.go` | Needs to be created before implementing URL resolution |
|
||||||
| Update callers to use utils package | Replace h.resolveCoverURL/resolveFileURL with utils.ResolveMediaURL |
|
| Update callers to use utils package | Replace h.resolveCoverURL/resolveFileURL with utils.ResolveMediaURL |
|
||||||
|
|
||||||
## Phase 8: Backward Compatibility
|
## Phase 8: Backward Compatibility
|
||||||
|
|
||||||
Handle existing absolute paths in database:
|
Handle existing absolute paths in database:
|
||||||
|
|
||||||
### Option A: Migration (One-time)
|
### Option A: Migration (One-time)
|
||||||
|
|
||||||
Create a script to convert existing absolute paths to relative paths using known library folder paths.
|
Create a script to convert existing absolute paths to relative paths using known library folder paths.
|
||||||
|
|
||||||
### Option B: Runtime Resolution (No migration)
|
### Option B: Runtime Resolution (No migration)
|
||||||
|
|
||||||
Add backward compatibility in handlers:
|
Add backward compatibility in handlers:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -949,7 +986,7 @@ info:
|
|||||||
seq: 1
|
seq: 1
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
url: '{{base_url}}/uploads/library-{{library_id}}/path/to/cover.jpg'
|
url: "{{base_url}}/uploads/library-{{library_id}}/path/to/cover.jpg"
|
||||||
auth: none
|
auth: none
|
||||||
|
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -989,7 +1026,7 @@ info:
|
|||||||
seq: 1
|
seq: 1
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
url: '{{base_url}}/uploads/library-{{library_id}}/path/to/book.epub'
|
url: "{{base_url}}/uploads/library-{{library_id}}/path/to/book.epub"
|
||||||
auth: none
|
auth: none
|
||||||
|
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -1030,7 +1067,7 @@ vars:
|
|||||||
|
|
||||||
### File: `docs/developer/api/media-items/get_cover_image.md`
|
### File: `docs/developer/api/media-items/get_cover_image.md`
|
||||||
|
|
||||||
```markdown
|
````markdown
|
||||||
---
|
---
|
||||||
title: Get Cover Image
|
title: Get Cover Image
|
||||||
description: Retrieve the cover image for a media item
|
description: Retrieve the cover image for a media item
|
||||||
@@ -1046,15 +1083,15 @@ Retrieve the cover image for a media item.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
|-----------|------|-------------|
|
| --------- | ------ | ------------------------ |
|
||||||
| id | string | The media item ID (UUID) |
|
| id | string | The media item ID (UUID) |
|
||||||
|
|
||||||
## Headers
|
## Headers
|
||||||
|
|
||||||
| Header | Required | Description |
|
| Header | Required | Description |
|
||||||
|--------|----------|-------------|
|
| ------------- | -------- | ------------ |
|
||||||
| Authorization | Yes | Bearer token |
|
| Authorization | Yes | Bearer token |
|
||||||
|
|
||||||
## Response
|
## Response
|
||||||
|
|
||||||
@@ -1076,6 +1113,7 @@ curl -H "Authorization: Bearer YOUR_TOKEN" \
|
|||||||
http://localhost:8765/api/covers/550e8400-e29b-41d4-a716-446655440000 \
|
http://localhost:8765/api/covers/550e8400-e29b-41d4-a716-446655440000 \
|
||||||
--output cover.jpg
|
--output cover.jpg
|
||||||
```
|
```
|
||||||
|
````
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
@@ -1088,6 +1126,7 @@ curl -H "Authorization: Bearer YOUR_TOKEN" \
|
|||||||
### File: `docs/developer/api/media-items/download_book.md`
|
### File: `docs/developer/api/media-items/download_book.md`
|
||||||
|
|
||||||
Update existing documentation to note:
|
Update existing documentation to note:
|
||||||
|
|
||||||
- File paths are stored relative to library folders
|
- File paths are stored relative to library folders
|
||||||
- Handler resolves path at request time
|
- Handler resolves path at request time
|
||||||
- Backward compatible with existing absolute paths
|
- Backward compatible with existing absolute paths
|
||||||
@@ -1096,29 +1135,29 @@ Update existing documentation to note:
|
|||||||
|
|
||||||
## Summary of Changes
|
## Summary of Changes
|
||||||
|
|
||||||
| Phase | File | Change |
|
| Phase | File | Change |
|
||||||
|-------|------|--------|
|
| -------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||||
| Refactor | `internal/handlers/commonhandlers.go` | Move Handler struct from scanner.go (kitchen sink handler for progress/book matching) |
|
| Refactor | `internal/handlers/commonhandlers.go` | Move Handler struct from scanner.go (kitchen sink handler for progress/book matching) |
|
||||||
| Refactor | `internal/routers/*.go` | Update NewHandler instantiation if needed |
|
| Refactor | `internal/routers/*.go` | Update NewHandler instantiation if needed |
|
||||||
| 1 | `internal/services/media_scanner.go` | Add `getRelativePath()` function; use for both file_path and cover_path |
|
| 1 | `internal/services/media_scanner.go` | Add `getRelativePath()` function; use for both file_path and cover_path |
|
||||||
| 2 | `internal/services/library_service.go` | Add `ResolveMediaPath()` function (one source of truth) |
|
| 2 | `internal/services/library_service.go` | Add `ResolveMediaPath()` function (one source of truth) |
|
||||||
| 3 | `internal/handlers/media.go` | Add `getFullFilePath()` helper that calls service |
|
| 3 | `internal/handlers/media.go` | Add `getFullFilePath()` helper that calls service |
|
||||||
| 4 | `internal/handlers/media.go` | Modify `DownloadBook` to use `getFullFilePath()` |
|
| 4 | `internal/handlers/media.go` | Modify `DownloadBook` to use `getFullFilePath()` |
|
||||||
| 5 | `internal/handlers/media.go` | Add `ServeFile()` handler for authenticated static-style routes |
|
| 5 | `internal/handlers/media.go` | Add `ServeFile()` handler for authenticated static-style routes |
|
||||||
| 5 | `internal/router/media.go` | Add route `GET /uploads/library-:id/*` on Echo (not protected group) |
|
| 5 | `internal/router/media.go` | Add route `GET /uploads/library-:id/*` on Echo (not protected group) |
|
||||||
| 6 | `internal/handlers/opds.go` | Add `libraryService` to struct; update `GetCoverImage` to use service |
|
| 6 | `internal/handlers/opds.go` | Add `libraryService` to struct; update `GetCoverImage` to use service |
|
||||||
| 7 | `internal/handlers/collections.go` | Add `libraryService` to struct/constructor; resolve cover paths to URLs in API responses |
|
| 7 | `internal/handlers/collections.go` | Add `libraryService` to struct/constructor; resolve cover paths to URLs in API responses |
|
||||||
| 7 | `internal/router/*.go` | Update CollectionHandler instantiation to pass LibraryService |
|
| 7 | `internal/router/*.go` | Update CollectionHandler instantiation to pass LibraryService |
|
||||||
| 7 | `internal/handlers/commonhandlers.go` (Handler struct, used by progress.go) | Resolve cover paths to URLs in API responses |
|
| 7 | `internal/handlers/commonhandlers.go` (Handler struct, used by progress.go) | Resolve cover paths to URLs in API responses |
|
||||||
| 7 | `internal/handlers/media.go` | Resolve file paths to URLs in API responses |
|
| 7 | `internal/handlers/media.go` | Resolve file paths to URLs in API responses |
|
||||||
| 7 | `web/src/bookshelf.ts` | Remove `/covers/` prefix (use resolved URL directly) |
|
| 7 | `web/src/bookshelf.ts` | Remove `/covers/` prefix (use resolved URL directly) |
|
||||||
| 8 | Runtime resolution | Handles both absolute (old) and relative (new) paths |
|
| 8 | Runtime resolution | Handles both absolute (old) and relative (new) paths |
|
||||||
| 9 | `internal/handlers/media_test.go` | Add unit tests for path resolution |
|
| 9 | `internal/handlers/media_test.go` | Add unit tests for path resolution |
|
||||||
| 9 | `internal/services/media_scanner_test.go` | Add unit tests for `getRelativePath()` |
|
| 9 | `internal/services/media_scanner_test.go` | Add unit tests for `getRelativePath()` |
|
||||||
| 9 | `cmd/server/tests/cover_file_serving_test.go` | Add integration tests using test_helpers |
|
| 9 | `cmd/server/tests/cover_file_serving_test.go` | Add integration tests using test_helpers |
|
||||||
| 10 | `bruno/media-items/Get Cover Image.yml` | Add Bruno API test |
|
| 10 | `bruno/media-items/Get Cover Image.yml` | Add Bruno API test |
|
||||||
| 10 | `bruno/media-items/EPUB Download.yml` | Update to document relative path handling |
|
| 10 | `bruno/media-items/EPUB Download.yml` | Update to document relative path handling |
|
||||||
| 10 | `docs/developer/api/media-items/` | Update API documentation |
|
| 10 | `docs/developer/api/media-items/` | Update API documentation |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1165,9 +1204,9 @@ Users can configure any mount point in docker-compose:
|
|||||||
services:
|
services:
|
||||||
bookhoard:
|
bookhoard:
|
||||||
volumes:
|
volumes:
|
||||||
- ./epubs:/app/epubs # ebooks
|
- ./epubs:/app/epubs # ebooks
|
||||||
- ./manga:/var/manga # manga
|
- ./manga:/var/manga # manga
|
||||||
- ./comics:/media/comics # comics
|
- ./comics:/media/comics # comics
|
||||||
```
|
```
|
||||||
|
|
||||||
The system stores relative paths, so it works with any configuration.
|
The system stores relative paths, so it works with any configuration.
|
||||||
|
|||||||
@@ -6,14 +6,15 @@ The backend implements dual-field normalization for searchability:
|
|||||||
|
|
||||||
### Architecture
|
### Architecture
|
||||||
|
|
||||||
| Field Type | Purpose | Behavior | Example |
|
| Field Type | Purpose | Behavior | Example |
|
||||||
|-----------|---------|-----------|----------|
|
| ------------------------------------------------------- | -------------- | --------------------------------------------------- | --------------- |
|
||||||
| **Display Field** (`tags`, `contributors`) | Show to users | Preserves exact variant, punctuation, proper casing | `"ACME CORP."` |
|
| **Display Field** (`tags`, `contributors`) | Show to users | Preserves exact variant, punctuation, proper casing | `"ACME CORP."` |
|
||||||
| **Search Field** (`tags_search`, `contributors_search`) | Search against | Lowercase, no punctuation, deduplicated | `["acme corp"]` |
|
| **Search Field** (`tags_search`, `contributors_search`) | Search against | Lowercase, no punctuation, deduplicated | `["acme corp"]` |
|
||||||
|
|
||||||
### Normalization Rules
|
### Normalization Rules
|
||||||
|
|
||||||
#### Tags
|
#### Tags
|
||||||
|
|
||||||
1. Trim whitespace from each tag
|
1. Trim whitespace from each tag
|
||||||
2. Titlecase each tag (preserves hyphenation: "non-fiction" → "Non-Fiction")
|
2. Titlecase each tag (preserves hyphenation: "non-fiction" → "Non-Fiction")
|
||||||
3. Case-insensitive deduplication
|
3. Case-insensitive deduplication
|
||||||
@@ -21,6 +22,7 @@ The backend implements dual-field normalization for searchability:
|
|||||||
5. Store both display and search versions
|
5. Store both display and search versions
|
||||||
|
|
||||||
#### Contributors
|
#### Contributors
|
||||||
|
|
||||||
1. Trim whitespace from each contributor
|
1. Trim whitespace from each contributor
|
||||||
2. Preserve original casing (including CAPSLOCK companies)
|
2. Preserve original casing (including CAPSLOCK companies)
|
||||||
3. Preserve original punctuation for display
|
3. Preserve original punctuation for display
|
||||||
@@ -31,6 +33,7 @@ The backend implements dual-field normalization for searchability:
|
|||||||
### API Request/Response
|
### API Request/Response
|
||||||
|
|
||||||
**Request:**
|
**Request:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"tags": ["science-fiction", "ACME CORP.", " O'Reilly Media"],
|
"tags": ["science-fiction", "ACME CORP.", " O'Reilly Media"],
|
||||||
@@ -39,6 +42,7 @@ The backend implements dual-field normalization for searchability:
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response (after normalization):**
|
**Response (after normalization):**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"tags": ["Science-Fiction", "O'Reilly Media"],
|
"tags": ["Science-Fiction", "O'Reilly Media"],
|
||||||
@@ -51,11 +55,13 @@ The backend implements dual-field normalization for searchability:
|
|||||||
### Frontend Implementation Guidelines
|
### Frontend Implementation Guidelines
|
||||||
|
|
||||||
#### Display
|
#### Display
|
||||||
|
|
||||||
- Use `tags` and `contributors` fields
|
- Use `tags` and `contributors` fields
|
||||||
- These preserve exact user input (casing, punctuation)
|
- These preserve exact user input (casing, punctuation)
|
||||||
- No transformation needed
|
- No transformation needed
|
||||||
|
|
||||||
#### Search
|
#### Search
|
||||||
|
|
||||||
- Use search inputs against `tags_search` and `contributors_search`
|
- Use search inputs against `tags_search` and `contributors_search`
|
||||||
- Normalize user search input:
|
- Normalize user search input:
|
||||||
- Convert to lowercase
|
- Convert to lowercase
|
||||||
@@ -63,6 +69,7 @@ The backend implements dual-field normalization for searchability:
|
|||||||
- Search using `= ANY()` operator
|
- Search using `= ANY()` operator
|
||||||
|
|
||||||
#### User Typing "Science-Fiction"
|
#### User Typing "Science-Fiction"
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// User types exact value
|
// User types exact value
|
||||||
const searchValue = "Science-Fiction";
|
const searchValue = "Science-Fiction";
|
||||||
@@ -73,6 +80,7 @@ const searchValue = "Science-Fiction";
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Search Query Behavior
|
#### Search Query Behavior
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// User searches: "ACME CORP."
|
// User searches: "ACME CORP."
|
||||||
// Backend normalizes search to: "acme corp"
|
// Backend normalizes search to: "acme corp"
|
||||||
@@ -85,6 +93,7 @@ const searchValue = "Science-Fiction";
|
|||||||
When building frontend checkbox filters for contributors/tags:
|
When building frontend checkbox filters for contributors/tags:
|
||||||
|
|
||||||
#### Get Unique Values for Dropdown
|
#### Get Unique Values for Dropdown
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Fetch distinct normalized values for filters
|
// Fetch distinct normalized values for filters
|
||||||
GET /api/contributors?distinct=true
|
GET /api/contributors?distinct=true
|
||||||
@@ -94,6 +103,7 @@ Response: ["acme corp", "oreilly media", "penguin"]
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Filter Query
|
#### Filter Query
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// User selects checkbox
|
// User selects checkbox
|
||||||
const filterValue = "acme corp";
|
const filterValue = "acme corp";
|
||||||
@@ -117,24 +127,28 @@ const filterValue = "acme corp";
|
|||||||
### Common Mistakes to Avoid
|
### Common Mistakes to Avoid
|
||||||
|
|
||||||
❌ **Searching display field directly**
|
❌ **Searching display field directly**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// WRONG - Will miss different casing/punctuation
|
// WRONG - Will miss different casing/punctuation
|
||||||
WHERE 'ACME CORP.' = ANY(contributors)
|
WHERE 'ACME CORP.' = ANY(contributors)
|
||||||
```
|
```
|
||||||
|
|
||||||
✅ **Search search field**
|
✅ **Search search field**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// CORRECT - Case-insensitive, punctuation-free
|
// CORRECT - Case-insensitive, punctuation-free
|
||||||
WHERE 'acme corp' = ANY(contributors_search)
|
WHERE 'acme corp' = ANY(contributors_search)
|
||||||
```
|
```
|
||||||
|
|
||||||
❌ **Don't normalize user search input**
|
❌ **Don't normalize user search input**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// WRONG - If user types "ACME CORP" explicitly to find exact match
|
// WRONG - If user types "ACME CORP" explicitly to find exact match
|
||||||
const search = "acme corp"; // Changes user's intent
|
const search = "acme corp"; // Changes user's intent
|
||||||
```
|
```
|
||||||
|
|
||||||
✅ **Use exact user input for search**
|
✅ **Use exact user input for search**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// CORRECT - Backend handles normalization
|
// CORRECT - Backend handles normalization
|
||||||
const search = "ACME CORP"; // Backend will match "acme corp" in search field
|
const search = "ACME CORP"; // Backend will match "acme corp" in search field
|
||||||
@@ -143,14 +157,17 @@ const search = "ACME CORP"; // Backend will match "acme corp" in search field
|
|||||||
### Schema Reference
|
### Schema Reference
|
||||||
|
|
||||||
**Display Fields:**
|
**Display Fields:**
|
||||||
|
|
||||||
- `tags TEXT[]` - Titlecase, original punctuation
|
- `tags TEXT[]` - Titlecase, original punctuation
|
||||||
- `contributors TEXT[]` - Original casing, original punctuation
|
- `contributors TEXT[]` - Original casing, original punctuation
|
||||||
|
|
||||||
**Search Fields:**
|
**Search Fields:**
|
||||||
|
|
||||||
- `tags_search TEXT[]` - Lowercase, no punctuation, deduplicated
|
- `tags_search TEXT[]` - Lowercase, no punctuation, deduplicated
|
||||||
- `contributors_search TEXT[]` - Lowercase, no punctuation, deduplicated
|
- `contributors_search TEXT[]` - Lowercase, no punctuation, deduplicated
|
||||||
|
|
||||||
**GIN Indexes:**
|
**GIN Indexes:**
|
||||||
|
|
||||||
- `idx_media_items_tags_search` - Fast search on tags_search
|
- `idx_media_items_tags_search` - Fast search on tags_search
|
||||||
- `idx_media_items_contributors_search` - Fast search on contributors_search
|
- `idx_media_items_contributors_search` - Fast search on contributors_search
|
||||||
- `idx_media_items_tags_gin` - Display field (if needed)
|
- `idx_media_items_tags_gin` - Display field (if needed)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ Welcome to the Bookhoard contributing documentation. This section contains guide
|
|||||||
## 🤝 How to Contribute
|
## 🤝 How to Contribute
|
||||||
|
|
||||||
We welcome contributions! Please see our [Development Guide](Development.md) for information on:
|
We welcome contributions! Please see our [Development Guide](Development.md) for information on:
|
||||||
|
|
||||||
- Setting up your development environment
|
- Setting up your development environment
|
||||||
- Understanding the codebase
|
- Understanding the codebase
|
||||||
- Making pull requests
|
- Making pull requests
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ bookhoard/
|
|||||||
### Backend Components
|
### Backend Components
|
||||||
|
|
||||||
**Handlers** (`internal/handlers/`):
|
**Handlers** (`internal/handlers/`):
|
||||||
|
|
||||||
- `auth.go` - Authentication & user management
|
- `auth.go` - Authentication & user management
|
||||||
- `library.go` - Library CRUD operations
|
- `library.go` - Library CRUD operations
|
||||||
- `scanner.go` - Media scanning operations
|
- `scanner.go` - Media scanning operations
|
||||||
@@ -51,6 +52,7 @@ bookhoard/
|
|||||||
- `context.go` - Handler context utilities
|
- `context.go` - Handler context utilities
|
||||||
|
|
||||||
**Middleware** (`internal/middleware/`):
|
**Middleware** (`internal/middleware/`):
|
||||||
|
|
||||||
- `device_auth.go` - Device authentication
|
- `device_auth.go` - Device authentication
|
||||||
- `device_rate_limiter.go` - Device-specific rate limiting
|
- `device_rate_limiter.go` - Device-specific rate limiting
|
||||||
- `error_handler.go` - Global error handling
|
- `error_handler.go` - Global error handling
|
||||||
@@ -62,6 +64,7 @@ bookhoard/
|
|||||||
- `transaction.go` - Database transaction middleware
|
- `transaction.go` - Database transaction middleware
|
||||||
|
|
||||||
**Services** (`internal/services/`):
|
**Services** (`internal/services/`):
|
||||||
|
|
||||||
- `library_service.go` - Library operations
|
- `library_service.go` - Library operations
|
||||||
- `media_scanner.go` - File scanning & metadata extraction
|
- `media_scanner.go` - File scanning & metadata extraction
|
||||||
- `worker.go` - Job queue worker pool
|
- `worker.go` - Job queue worker pool
|
||||||
@@ -71,6 +74,7 @@ bookhoard/
|
|||||||
- `book_matching.go` - Book matching algorithms
|
- `book_matching.go` - Book matching algorithms
|
||||||
|
|
||||||
**Sync Framework** (`internal/sync/`):
|
**Sync Framework** (`internal/sync/`):
|
||||||
|
|
||||||
- `queue.go` - Sync queue processor
|
- `queue.go` - Sync queue processor
|
||||||
- `progress.go` - Universal progress format
|
- `progress.go` - Universal progress format
|
||||||
- `websocket.go` - Real-time sync broadcast
|
- `websocket.go` - Real-time sync broadcast
|
||||||
@@ -80,6 +84,7 @@ bookhoard/
|
|||||||
### Database Schema
|
### Database Schema
|
||||||
|
|
||||||
**Core Tables**:
|
**Core Tables**:
|
||||||
|
|
||||||
- `users` - User accounts with authentication and settings
|
- `users` - User accounts with authentication and settings
|
||||||
- `libraries` - Library definitions
|
- `libraries` - Library definitions
|
||||||
- `library_types` - Media type definitions (ebooks, comics, manga)
|
- `library_types` - Media type definitions (ebooks, comics, manga)
|
||||||
@@ -105,6 +110,7 @@ bookhoard/
|
|||||||
- `refresh_tokens` - JWT refresh token storage
|
- `refresh_tokens` - JWT refresh token storage
|
||||||
|
|
||||||
**Database Functions**:
|
**Database Functions**:
|
||||||
|
|
||||||
- `normalize_isbn()` - ISBN format normalization
|
- `normalize_isbn()` - ISBN format normalization
|
||||||
- `detect_format_group()` - Detect format group (reflowable, fixed_layout, comic_archive)
|
- `detect_format_group()` - Detect format group (reflowable, fixed_layout, comic_archive)
|
||||||
- `convert_progress()` - Convert progress between format groups
|
- `convert_progress()` - Convert progress between format groups
|
||||||
@@ -114,6 +120,7 @@ bookhoard/
|
|||||||
### Technology Stack
|
### Technology Stack
|
||||||
|
|
||||||
**Backend**:
|
**Backend**:
|
||||||
|
|
||||||
- Go 1.25+
|
- Go 1.25+
|
||||||
- Echo v4 - HTTP framework
|
- Echo v4 - HTTP framework
|
||||||
- pgx v5 - PostgreSQL driver
|
- pgx v5 - PostgreSQL driver
|
||||||
@@ -122,18 +129,21 @@ bookhoard/
|
|||||||
- bcrypt - Password hashing
|
- bcrypt - Password hashing
|
||||||
|
|
||||||
**Frontend**:
|
**Frontend**:
|
||||||
|
|
||||||
- Templ - HTML templating with Go
|
- Templ - HTML templating with Go
|
||||||
- HTMX - Dynamic interactions
|
- HTMX - Dynamic interactions
|
||||||
- Tailwind CSS - Styling
|
- Tailwind CSS - Styling
|
||||||
- TypeScript - Frontend logic
|
- TypeScript - Frontend logic
|
||||||
|
|
||||||
**Database**:
|
**Database**:
|
||||||
|
|
||||||
- PostgreSQL 15+
|
- PostgreSQL 15+
|
||||||
- 30+ tables
|
- 30+ tables
|
||||||
- 50+ indexes
|
- 50+ indexes
|
||||||
- JSONB for complex data
|
- JSONB for complex data
|
||||||
|
|
||||||
**Testing**:
|
**Testing**:
|
||||||
|
|
||||||
- Testify - Testing framework
|
- Testify - Testing framework
|
||||||
- Bruno - API testing
|
- Bruno - API testing
|
||||||
- 30+ integration test files
|
- 30+ integration test files
|
||||||
@@ -201,6 +211,7 @@ go run cmd/server/main.go
|
|||||||
### Development Workflow
|
### Development Workflow
|
||||||
|
|
||||||
**Backend Development**:
|
**Backend Development**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Watch mode for Go (requires air or similar)
|
# Watch mode for Go (requires air or similar)
|
||||||
air
|
air
|
||||||
@@ -211,18 +222,21 @@ go build -o bookhoard cmd/server/main.go
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Frontend Development**:
|
**Frontend Development**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd web
|
cd web
|
||||||
npm run dev # Watch mode for TypeScript/CSS
|
npm run dev # Watch mode for TypeScript/CSS
|
||||||
```
|
```
|
||||||
|
|
||||||
**Database Changes**:
|
**Database Changes**:
|
||||||
|
|
||||||
1. Edit `database/schema/schema.sql`
|
1. Edit `database/schema/schema.sql`
|
||||||
2. Edit `internal/database/queries/queries.sql`
|
2. Edit `internal/database/queries/queries.sql`
|
||||||
3. Run: `cd internal/database && sqlc generate`
|
3. Run: `cd internal/database && sqlc generate`
|
||||||
4. Restart server
|
4. Restart server
|
||||||
|
|
||||||
**Template Changes**:
|
**Template Changes**:
|
||||||
|
|
||||||
1. Edit `templates/*.templ`
|
1. Edit `templates/*.templ`
|
||||||
2. Run: `cd templates && templ generate`
|
2. Run: `cd templates && templ generate`
|
||||||
3. Restart server (templates auto-reload in dev mode)
|
3. Restart server (templates auto-reload in dev mode)
|
||||||
@@ -273,6 +287,7 @@ bruno run bruno/sync-kobo/
|
|||||||
### Test Configuration
|
### Test Configuration
|
||||||
|
|
||||||
Environment variables for testing:
|
Environment variables for testing:
|
||||||
|
|
||||||
- `TEST_MODE=true` - Enable test mode (disables rate limiting)
|
- `TEST_MODE=true` - Enable test mode (disables rate limiting)
|
||||||
- `RATE_LIMIT_ENABLED=false` - Disable rate limiting
|
- `RATE_LIMIT_ENABLED=false` - Disable rate limiting
|
||||||
- `REQUESTS_PER_MINUTE=1000` - Increase rate limit
|
- `REQUESTS_PER_MINUTE=1000` - Increase rate limit
|
||||||
@@ -284,6 +299,7 @@ Environment variables for testing:
|
|||||||
Integration tests automatically clean up libraries with "test" in the name (case-insensitive).
|
Integration tests automatically clean up libraries with "test" in the name (case-insensitive).
|
||||||
|
|
||||||
**⚠️ IMPORTANT**: Do not use "test" in library names if you want to keep them!
|
**⚠️ IMPORTANT**: Do not use "test" in library names if you want to keep them!
|
||||||
|
|
||||||
- Libraries containing "test" (e.g., "My Test Library", "Test Library 1") will be deleted by test cleanup
|
- Libraries containing "test" (e.g., "My Test Library", "Test Library 1") will be deleted by test cleanup
|
||||||
- Use names like "Development Library", "Staging Books", or "Personal" for libraries you want to keep
|
- Use names like "Development Library", "Staging Books", or "Personal" for libraries you want to keep
|
||||||
- This ensures your manual test data persists between test runs
|
- This ensures your manual test data persists between test runs
|
||||||
@@ -357,11 +373,13 @@ podman-compose build --no-cache
|
|||||||
### Environment Variables
|
### Environment Variables
|
||||||
|
|
||||||
Required for production:
|
Required for production:
|
||||||
|
|
||||||
- `JWT_SECRET` - 64-byte random string (generate: `openssl rand -hex 32`)
|
- `JWT_SECRET` - 64-byte random string (generate: `openssl rand -hex 32`)
|
||||||
- `DBPASS` - Strong database password (generate: `openssl rand -hex 16`)
|
- `DBPASS` - Strong database password (generate: `openssl rand -hex 16`)
|
||||||
- `BASE_URL` - Public URL (e.g., https://bookhoard.example.com)
|
- `BASE_URL` - Public URL (e.g., https://bookhoard.example.com)
|
||||||
|
|
||||||
Optional:
|
Optional:
|
||||||
|
|
||||||
- `HTTPS_PROXY` - If behind reverse proxy
|
- `HTTPS_PROXY` - If behind reverse proxy
|
||||||
|
|
||||||
**Note**: Conversion service, rate limiting, and other operational settings have defaults in `docker-compose.yml` and can be overridden via `.env` if needed.
|
**Note**: Conversion service, rate limiting, and other operational settings have defaults in `docker-compose.yml` and can be overridden via `.env` if needed.
|
||||||
@@ -369,6 +387,7 @@ Optional:
|
|||||||
### Performance Tuning
|
### Performance Tuning
|
||||||
|
|
||||||
**PostgreSQL Settings**:
|
**PostgreSQL Settings**:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- In postgresql.conf
|
-- In postgresql.conf
|
||||||
shared_buffers = 256MB
|
shared_buffers = 256MB
|
||||||
@@ -385,6 +404,7 @@ max_wal_size = 4GB
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Go Settings**:
|
**Go Settings**:
|
||||||
|
|
||||||
- GOMAXPROCS = number of CPU cores
|
- GOMAXPROCS = number of CPU cores
|
||||||
- Worker pool concurrency: 3 (configurable in services/worker.go)
|
- Worker pool concurrency: 3 (configurable in services/worker.go)
|
||||||
|
|
||||||
@@ -403,19 +423,23 @@ DEBUG=true
|
|||||||
### Common Issues
|
### Common Issues
|
||||||
|
|
||||||
**Database Connection Errors**:
|
**Database Connection Errors**:
|
||||||
|
|
||||||
- Check PostgreSQL is running
|
- Check PostgreSQL is running
|
||||||
- Verify DATABASE_HOST and DATABASE_PORT
|
- Verify DATABASE_HOST and DATABASE_PORT
|
||||||
- Check firewall settings
|
- Check firewall settings
|
||||||
|
|
||||||
**Rate Limiting During Development**:
|
**Rate Limiting During Development**:
|
||||||
|
|
||||||
- Enable test mode: `TEST_MODE=true RATE_LIMIT_ENABLED=false`
|
- Enable test mode: `TEST_MODE=true RATE_LIMIT_ENABLED=false`
|
||||||
- Or increase limit: `REQUESTS_PER_MINUTE=1000`
|
- Or increase limit: `REQUESTS_PER_MINUTE=1000`
|
||||||
|
|
||||||
**Template Not Updating**:
|
**Template Not Updating**:
|
||||||
|
|
||||||
- Run `templ generate` in templates/ directory
|
- Run `templ generate` in templates/ directory
|
||||||
- Restart server
|
- Restart server
|
||||||
|
|
||||||
**Database Queries Not Working**:
|
**Database Queries Not Working**:
|
||||||
|
|
||||||
- Run `sqlc generate` in internal/database/
|
- Run `sqlc generate` in internal/database/
|
||||||
- Check generated code in `queries.sql.go`
|
- Check generated code in `queries.sql.go`
|
||||||
- Verify SQL syntax in `queries.sql`
|
- Verify SQL syntax in `queries.sql`
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
> For updated, split endpoint documentation with interactive API explorer, see [API Documentation Portal](api/api-reference.md).
|
> For updated, split endpoint documentation with interactive API explorer, see [API Documentation Portal](api/api-reference.md).
|
||||||
>
|
>
|
||||||
> **Use the split docs for:**
|
> **Use the split docs for:**
|
||||||
|
>
|
||||||
> - Easier navigation by category
|
> - Easier navigation by category
|
||||||
> - Interactive API explorer
|
> - Interactive API explorer
|
||||||
> - Endpoint-specific examples
|
> - Endpoint-specific examples
|
||||||
@@ -65,6 +66,7 @@ Content-Type: application/json
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (201):
|
**Response** (201):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||||
@@ -93,6 +95,7 @@ Content-Type: application/json
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||||
@@ -118,6 +121,7 @@ Content-Type: application/json
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"token": "new-jwt-token",
|
"token": "new-jwt-token",
|
||||||
@@ -144,6 +148,7 @@ Authorization: Bearer <token>
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"id": "uuid",
|
"id": "uuid",
|
||||||
@@ -219,6 +224,7 @@ Authorization: Bearer <token>
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"libraries": [
|
"libraries": [
|
||||||
@@ -290,11 +296,13 @@ Authorization: Bearer <token>
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Query Parameters**:
|
**Query Parameters**:
|
||||||
|
|
||||||
- `library_id` (required): UUID of library
|
- `library_id` (required): UUID of library
|
||||||
- `limit`: Number of items to return (max 100, default 20)
|
- `limit`: Number of items to return (max 100, default 20)
|
||||||
- `offset`: Number of items to skip
|
- `offset`: Number of items to skip
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"media_items": [
|
"media_items": [
|
||||||
@@ -337,11 +345,13 @@ Authorization: Bearer <token>
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Query Parameters**:
|
**Query Parameters**:
|
||||||
|
|
||||||
- `q` (required): Search query (minimum 2 characters)
|
- `q` (required): Search query (minimum 2 characters)
|
||||||
- `limit`: Number of results (default 20)
|
- `limit`: Number of results (default 20)
|
||||||
- `offset`: Number to skip
|
- `offset`: Number to skip
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"results": [
|
"results": [
|
||||||
@@ -409,6 +419,7 @@ Authorization: Bearer <token>
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"media_item_id": "uuid",
|
"media_item_id": "uuid",
|
||||||
@@ -452,6 +463,7 @@ Content-Type: application/json
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"sync_status": "success",
|
"sync_status": "success",
|
||||||
@@ -478,6 +490,7 @@ Authorization: Bearer <token>
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"notes": [
|
"notes": [
|
||||||
@@ -541,6 +554,7 @@ Authorization: Bearer <token>
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"highlights": [
|
"highlights": [
|
||||||
@@ -611,6 +625,7 @@ Authorization: Bearer <token>
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"rating": 8,
|
"rating": 8,
|
||||||
@@ -668,6 +683,7 @@ Content-Type: application/json
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (201):
|
**Response** (201):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"device_id": "uuid",
|
"device_id": "uuid",
|
||||||
@@ -690,6 +706,7 @@ Content-Type: application/json
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"status": "pending|approved|expired",
|
"status": "pending|approved|expired",
|
||||||
@@ -711,6 +728,7 @@ Authorization: Bearer <token>
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"devices": [
|
"devices": [
|
||||||
@@ -760,10 +778,12 @@ Authorization: Bearer <token>
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Query Parameters**:
|
**Query Parameters**:
|
||||||
|
|
||||||
- `start_date` (optional): Start date (ISO 8601 format)
|
- `start_date` (optional): Start date (ISO 8601 format)
|
||||||
- `end_date` (optional): End date (ISO 8601 format)
|
- `end_date` (optional): End date (ISO 8601 format)
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"pages_read": 1250,
|
"pages_read": 1250,
|
||||||
@@ -782,6 +802,7 @@ Authorization: Bearer <token>
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"devices": [
|
"devices": [
|
||||||
@@ -805,9 +826,11 @@ Authorization: Bearer <token>
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Query Parameters**:
|
**Query Parameters**:
|
||||||
|
|
||||||
- `limit` (optional): Number of results (default: 10)
|
- `limit` (optional): Number of results (default: 10)
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"books": [
|
"books": [
|
||||||
@@ -842,6 +865,7 @@ Content-Type: application/json
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"matches": [
|
"matches": [
|
||||||
@@ -875,6 +899,7 @@ Content-Type: application/json
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"results": [
|
"results": [
|
||||||
@@ -904,6 +929,7 @@ Content-Type: application/json
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"auto_linked": 15,
|
"auto_linked": 15,
|
||||||
@@ -927,6 +953,7 @@ Authorization: Bearer <token>
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"unlinked_book_id": "uuid-1",
|
"unlinked_book_id": "uuid-1",
|
||||||
@@ -950,6 +977,7 @@ Authorization: Bearer <token>
|
|||||||
For complete collection management documentation, see **[COLLECTIONS_API.md](COLLECTIONS_API.md)**.
|
For complete collection management documentation, see **[COLLECTIONS_API.md](COLLECTIONS_API.md)**.
|
||||||
|
|
||||||
**Quick Reference**:
|
**Quick Reference**:
|
||||||
|
|
||||||
- `GET /api/collections` - List all collections
|
- `GET /api/collections` - List all collections
|
||||||
- `POST /api/collections` - Create new collection
|
- `POST /api/collections` - Create new collection
|
||||||
- `GET /api/collections/{id}` - Get collection details
|
- `GET /api/collections/{id}` - Get collection details
|
||||||
@@ -960,6 +988,7 @@ For complete collection management documentation, see **[COLLECTIONS_API.md](COL
|
|||||||
- `GET /api/collections/{id}/books` - Get books in collection
|
- `GET /api/collections/{id}/books` - Get books in collection
|
||||||
|
|
||||||
**Features**:
|
**Features**:
|
||||||
|
|
||||||
- Auto-assign rules based on genre, author, series, tags, language, publisher, year
|
- Auto-assign rules based on genre, author, series, tags, language, publisher, year
|
||||||
- Device shelf mappings (Kobo shelves, KOReader categories)
|
- Device shelf mappings (Kobo shelves, KOReader categories)
|
||||||
- Test rules before applying
|
- Test rules before applying
|
||||||
@@ -974,10 +1003,12 @@ GET /opds/devices/{deviceId}/catalog?page={page}&per_page={per_page}
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Query Parameters**:
|
**Query Parameters**:
|
||||||
|
|
||||||
- `page` (optional): Page number (default: 1)
|
- `page` (optional): Page number (default: 1)
|
||||||
- `per_page` (optional): Items per page (default: 50, max: 200)
|
- `per_page` (optional): Items per page (default: 50, max: 200)
|
||||||
|
|
||||||
**Response** (200 - OPDS 1.2 XML):
|
**Response** (200 - OPDS 1.2 XML):
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<feed xmlns="http://www.w3.org/2005/Atom"
|
<feed xmlns="http://www.w3.org/2005/Atom"
|
||||||
@@ -1018,9 +1049,11 @@ GET /opds/devices/{deviceId}/download/{bookId}?format={format}
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Query Parameters**:
|
**Query Parameters**:
|
||||||
|
|
||||||
- `format` (optional): Book format - `epub` (default), `kepub`
|
- `format` (optional): Book format - `epub` (default), `kepub`
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
- Headers:
|
- Headers:
|
||||||
- `Content-Type`: `application/epub+zip` or `application/vnd.kobo+xml+zip`
|
- `Content-Type`: `application/epub+zip` or `application/vnd.kobo+xml+zip`
|
||||||
- `Content-Disposition`: attachment; filename="The Hobbit.epub"
|
- `Content-Disposition`: attachment; filename="The Hobbit.epub"
|
||||||
@@ -1043,6 +1076,7 @@ GET /opds/devices/{deviceId}/formats/{bookId}
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"media_item_id": "uuid-123",
|
"media_item_id": "uuid-123",
|
||||||
@@ -1109,6 +1143,7 @@ Content-Type: application/json
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (202):
|
**Response** (202):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"sync_status": "accepted",
|
"sync_status": "accepted",
|
||||||
@@ -1133,6 +1168,7 @@ Authorization: Bearer <device_token>
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"uuid": "book-uuid",
|
"uuid": "book-uuid",
|
||||||
@@ -1186,6 +1222,7 @@ Content-Type: application/json
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"Status": "Success",
|
"Status": "Success",
|
||||||
@@ -1202,6 +1239,7 @@ Authorization: Bearer <device_token>
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"library_sync": [
|
"library_sync": [
|
||||||
@@ -1229,6 +1267,7 @@ Authorization: Bearer <token>
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"book_id": "book-uuid",
|
"book_id": "book-uuid",
|
||||||
@@ -1307,10 +1346,12 @@ Authorization: Bearer <token>
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Query Parameters**:
|
**Query Parameters**:
|
||||||
|
|
||||||
- `status`: "unresolved|all" (default: "unresolved")
|
- `status`: "unresolved|all" (default: "unresolved")
|
||||||
- `type`: "progress|note|highlight|all" (default: "all")
|
- `type`: "progress|note|highlight|all" (default: "all")
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"conflicts": [
|
"conflicts": [
|
||||||
@@ -1373,6 +1414,7 @@ Content-Type: application/json
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"conflict_resolved": true,
|
"conflict_resolved": true,
|
||||||
@@ -1408,6 +1450,7 @@ Authorization: Bearer <token>
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"items": [
|
"items": [
|
||||||
@@ -1473,6 +1516,7 @@ Authorization: Bearer <token>
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response** (200):
|
**Response** (200):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"pending": 15,
|
"pending": 15,
|
||||||
@@ -1494,6 +1538,7 @@ WS /ws/sync?token=<token>
|
|||||||
### Message Format
|
### Message Format
|
||||||
|
|
||||||
**Client → Server (Heartbeat)**:
|
**Client → Server (Heartbeat)**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "ping"
|
"type": "ping"
|
||||||
@@ -1501,6 +1546,7 @@ WS /ws/sync?token=<token>
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Server → Client (Progress Update)**:
|
**Server → Client (Progress Update)**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "progress_update",
|
"type": "progress_update",
|
||||||
@@ -1523,6 +1569,7 @@ WS /ws/sync?token=<token>
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Server → Client (Conflict Detected)**:
|
**Server → Client (Conflict Detected)**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "conflict",
|
"type": "conflict",
|
||||||
@@ -1536,6 +1583,7 @@ WS /ws/sync?token=<token>
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Server → Client (Pong)**:
|
**Server → Client (Pong)**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "pong"
|
"type": "pong"
|
||||||
@@ -1571,16 +1619,19 @@ All endpoints return standardized error responses:
|
|||||||
### Rate Limiting
|
### Rate Limiting
|
||||||
|
|
||||||
**Per-Device Limits**:
|
**Per-Device Limits**:
|
||||||
|
|
||||||
- Sync requests: 60/minute
|
- Sync requests: 60/minute
|
||||||
- Progress updates: 120/minute
|
- Progress updates: 120/minute
|
||||||
- Metadata requests: 30/minute
|
- Metadata requests: 30/minute
|
||||||
|
|
||||||
**Per-User Limits**:
|
**Per-User Limits**:
|
||||||
|
|
||||||
- All requests: 300/minute
|
- All requests: 300/minute
|
||||||
- Conflict resolutions: 10/minute
|
- Conflict resolutions: 10/minute
|
||||||
- Device registrations: 5/hour
|
- Device registrations: 5/hour
|
||||||
|
|
||||||
**Rate Limit Headers**:
|
**Rate Limit Headers**:
|
||||||
|
|
||||||
```
|
```
|
||||||
X-RateLimit-Limit: 60
|
X-RateLimit-Limit: 60
|
||||||
X-RateLimit-Remaining: 45
|
X-RateLimit-Remaining: 45
|
||||||
@@ -1612,16 +1663,19 @@ bruno/
|
|||||||
## Testing with Bruno OpenCollection YAML
|
## Testing with Bruno OpenCollection YAML
|
||||||
|
|
||||||
Install Bruno CLI:
|
Install Bruno CLI:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -g @usebruno/cli
|
npm install -g @usebruno/cli
|
||||||
```
|
```
|
||||||
|
|
||||||
Run all tests:
|
Run all tests:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bruno run
|
bruno run
|
||||||
```
|
```
|
||||||
|
|
||||||
Run specific collection:
|
Run specific collection:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bruno run bruno/devices/
|
bruno run bruno/devices/
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -7,17 +7,17 @@ List all users in the system (admin only).
|
|||||||
|
|
||||||
## Query Parameters
|
## Query Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------- | -------- | ----------------------------------------------- |
|
||||||
| limit | integer | No | Maximum number of users to return (default: 50) |
|
| limit | integer | No | Maximum number of users to return (default: 50) |
|
||||||
| offset | integer | No | Number of users to skip (default: 0) |
|
| offset | integer | No | Number of users to skip (default: 0) |
|
||||||
| search | string | No | Search by email or username |
|
| search | string | No | Search by email or username |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ----------------------------------- |
|
||||||
| Authorization | string | Yes | Bearer token (must have admin role) |
|
| Authorization | string | Yes | Bearer token (must have admin role) |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -53,26 +53,26 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|||||||
|
|
||||||
### Response Fields
|
### Response Fields
|
||||||
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
|-------|------|-------------|
|
| -------------- | ------- | ------------------------------------- |
|
||||||
| `id` | string | User ID (UUID) |
|
| `id` | string | User ID (UUID) |
|
||||||
| `email` | string | Email address |
|
| `email` | string | Email address |
|
||||||
| `username` | string | Username |
|
| `username` | string | Username |
|
||||||
| `first_name` | string | First name (optional) |
|
| `first_name` | string | First name (optional) |
|
||||||
| `last_name` | string | Last name (optional) |
|
| `last_name` | string | Last name (optional) |
|
||||||
| `role` | string | User role (`"user"` or `"admin"`) |
|
| `role` | string | User role (`"user"` or `"admin"`) |
|
||||||
| `theme` | string | Theme preference (optional) |
|
| `theme` | string | Theme preference (optional) |
|
||||||
| `max_devices` | integer | Maximum number of devices allowed |
|
| `max_devices` | integer | Maximum number of devices allowed |
|
||||||
| `device_count` | integer | Current number of registered devices |
|
| `device_count` | integer | Current number of registered devices |
|
||||||
| `created_at` | string | Account creation timestamp (ISO 8601) |
|
| `created_at` | string | Account creation timestamp (ISO 8601) |
|
||||||
| `updated_at` | string | Last update timestamp (ISO 8601) |
|
| `updated_at` | string | Last update timestamp (ISO 8601) |
|
||||||
| `total` | integer | Total number of users matching query |
|
| `total` | integer | Total number of users matching query |
|
||||||
| `limit` | integer | Limit applied to this request |
|
| `limit` | integer | Limit applied to this request |
|
||||||
| `offset` | integer | Offset applied to this request |
|
| `offset` | integer | Offset applied to this request |
|
||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ----------------------------------- |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User does not have admin privileges |
|
| 403 | User does not have admin privileges |
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ Update the maximum number of devices a user can register (admin only).
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | ----------- |
|
||||||
| id | string (UUID) | Yes | User UUID |
|
| id | string (UUID) | Yes | User UUID |
|
||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ----------- | ------- | -------- | --------------------------------- |
|
||||||
| max_devices | integer | Yes | Maximum number of devices (1-100) |
|
| max_devices | integer | Yes | Maximum number of devices (1-100) |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -41,9 +41,9 @@ Update the maximum number of devices a user can register (admin only).
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ----------------------------------------- |
|
||||||
| 400 | Invalid max_devices value (must be 1-100) |
|
| 400 | Invalid max_devices value (must be 1-100) |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User does not have admin privileges |
|
| 403 | User does not have admin privileges |
|
||||||
| 404 | User not found |
|
| 404 | User not found |
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ Retrieve reading statistics for a date range.
|
|||||||
|
|
||||||
## Query Parameters
|
## Query Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| ---------- | ------ | -------- | ---------------------------- |
|
||||||
| start_date | string | No | Start date (ISO 8601 format) |
|
| start_date | string | No | Start date (ISO 8601 format) |
|
||||||
| end_date | string | No | End date (ISO 8601 format) |
|
| end_date | string | No | End date (ISO 8601 format) |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ------------ |
|
||||||
| Authorization | string | Yes | Bearer token |
|
| Authorization | string | Yes | Bearer token |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------ |
|
||||||
| 400 | Invalid date format |
|
| 400 | Invalid date format |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ See [WebSocket API](websocket/)
|
|||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- GET /docs - Documentation home
|
- GET /docs - Documentation home
|
||||||
- GET /docs/* - Show documentation pages
|
- GET /docs/\* - Show documentation pages
|
||||||
- GET /docs/api/search - Search API documentation
|
- GET /docs/api/search - Search API documentation
|
||||||
- GET /docs/search-index.json - Search index for documentation search
|
- GET /docs/search-index.json - Search index for documentation search
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ Authenticate with email and password.
|
|||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| -------- | ------ | -------- | -------------------------------- |
|
||||||
| login | string | Yes | User's email address or username |
|
| login | string | Yes | User's email address or username |
|
||||||
| password | string | Yes | User's password |
|
| password | string | Yes | User's password |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -42,6 +42,7 @@ Authenticate with email and password.
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Set-Cookie Header**:
|
**Set-Cookie Header**:
|
||||||
|
|
||||||
```
|
```
|
||||||
Set-Cookie: token=eyJhbG...; Max-Age=604800; Path=/; HttpOnly
|
Set-Cookie: token=eyJhbG...; Max-Age=604800; Path=/; HttpOnly
|
||||||
```
|
```
|
||||||
@@ -50,8 +51,8 @@ Set-Cookie: token=eyJhbG...; Max-Age=604800; Path=/; HttpOnly
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------- |
|
||||||
| 401 | Invalid email or password |
|
| 401 | Invalid email or password |
|
||||||
| 400 | Missing required fields |
|
| 400 | Missing required fields |
|
||||||
| 429 | Too many login attempts |
|
| 429 | Too many login attempts |
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ Invalidate the current JWT token.
|
|||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | --------------------------------------- |
|
||||||
| Authorization | string | Yes | Bearer token (e.g., `Bearer eyJhbG...`) |
|
| Authorization | string | Yes | Bearer token (e.g., `Bearer eyJhbG...`) |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ No response body.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------- |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | Token already invalidated |
|
| 403 | Token already invalidated |
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ When a user registers or logs in:
|
|||||||
4. Server returns JSON response with both tokens and user profile
|
4. Server returns JSON response with both tokens and user profile
|
||||||
|
|
||||||
**Request**:
|
**Request**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
POST /api/auth/login
|
POST /api/auth/login
|
||||||
{
|
{
|
||||||
@@ -51,6 +52,7 @@ POST /api/auth/login
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response**:
|
**Response**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||||
@@ -67,6 +69,7 @@ POST /api/auth/login
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Set-Cookie Header**:
|
**Set-Cookie Header**:
|
||||||
|
|
||||||
```
|
```
|
||||||
Set-Cookie: token=eyJhbG...; Max-Age=604800; Path=/; HttpOnly
|
Set-Cookie: token=eyJhbG...; Max-Age=604800; Path=/; HttpOnly
|
||||||
```
|
```
|
||||||
@@ -94,6 +97,7 @@ POST /api/auth/refresh
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Response**:
|
**Response**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"access_token": "new-jwt-token",
|
"access_token": "new-jwt-token",
|
||||||
@@ -135,6 +139,7 @@ When an API call receives a 401 Unauthorized response:
|
|||||||
```
|
```
|
||||||
|
|
||||||
The frontend toast.js interceptor:
|
The frontend toast.js interceptor:
|
||||||
|
|
||||||
1. Clears invalid tokens from localStorage
|
1. Clears invalid tokens from localStorage
|
||||||
2. Shows an error toast notification
|
2. Shows an error toast notification
|
||||||
3. Allows user to re-authenticate
|
3. Allows user to re-authenticate
|
||||||
@@ -149,10 +154,12 @@ The frontend toast.js interceptor:
|
|||||||
## Token Storage Recommendations
|
## Token Storage Recommendations
|
||||||
|
|
||||||
### Browser Applications
|
### Browser Applications
|
||||||
|
|
||||||
- **Backend**: Automatically manages HTTP-only cookie
|
- **Backend**: Automatically manages HTTP-only cookie
|
||||||
- **Frontend**: Store tokens in localStorage for API calls
|
- **Frontend**: Store tokens in localStorage for API calls
|
||||||
|
|
||||||
### Mobile Applications
|
### Mobile Applications
|
||||||
|
|
||||||
- Store access token in secure storage (Keychain/Keystore)
|
- Store access token in secure storage (Keychain/Keystore)
|
||||||
- Store refresh token in secure storage
|
- Store refresh token in secure storage
|
||||||
- Handle 401 responses by prompting user to re-authenticate
|
- Handle 401 responses by prompting user to re-authenticate
|
||||||
@@ -160,6 +167,7 @@ The frontend toast.js interceptor:
|
|||||||
## Constants Reference
|
## Constants Reference
|
||||||
|
|
||||||
All session durations use constants defined in:
|
All session durations use constants defined in:
|
||||||
|
|
||||||
- `internal/handlers/auth.go` - SessionDuration, SessionDurationSec
|
- `internal/handlers/auth.go` - SessionDuration, SessionDurationSec
|
||||||
- `internal/handlers/refresh_token.go` - SessionDurationSec (mirrored)
|
- `internal/handlers/refresh_token.go` - SessionDurationSec (mirrored)
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ Obtain a new JWT access token using a refresh token.
|
|||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | -------------------------- |
|
||||||
| refresh_token | string | Yes | Valid refresh token (UUID) |
|
| refresh_token | string | Yes | Valid refresh token (UUID) |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ The new access token is valid for 7 days from the time of refresh.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | --------------------------------------- |
|
||||||
| 401 | Invalid or expired refresh token |
|
| 401 | Invalid or expired refresh token |
|
||||||
| 400 | Missing refresh token or invalid format |
|
| 400 | Missing refresh token or invalid format |
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ Create a new user account.
|
|||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ---------- | ------ | -------- | --------------------------------------------------------- |
|
||||||
| email | string | Yes | User's email address |
|
| email | string | Yes | User's email address |
|
||||||
| username | string | Yes | Desired username (3-50 chars) |
|
| username | string | Yes | Desired username (3-50 chars) |
|
||||||
| password | string | Yes | Password (min 8 chars, must meet complexity requirements) |
|
| password | string | Yes | Password (min 8 chars, must meet complexity requirements) |
|
||||||
| first_name | string | No | User's first name |
|
| first_name | string | No | User's first name |
|
||||||
| last_name | string | No | User's last name |
|
| last_name | string | No | User's last name |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -50,6 +50,7 @@ Create a new user account.
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Set-Cookie Header**:
|
**Set-Cookie Header**:
|
||||||
|
|
||||||
```
|
```
|
||||||
Set-Cookie: token=eyJhbG...; Max-Age=604800; Path=/; HttpOnly
|
Set-Cookie: token=eyJhbG...; Max-Age=604800; Path=/; HttpOnly
|
||||||
```
|
```
|
||||||
@@ -60,7 +61,7 @@ Set-Cookie: token=eyJhbG...; Max-Age=604800; Path=/; HttpOnly
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------------------------------------ |
|
||||||
| 400 | Invalid email format, weak password, or missing fields |
|
| 400 | Invalid email format, weak password, or missing fields |
|
||||||
| 409 | Email or username already exists |
|
| 409 | Email or username already exists |
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ Automatically link books to media items based on matching metadata.
|
|||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| --------- | ------------- | -------- | -------------------------------------------------- |
|
||||||
| device_id | string (UUID) | Yes | Device UUID |
|
| device_id | string (UUID) | Yes | Device UUID |
|
||||||
| threshold | float | No | Match confidence threshold (0.0-1.0, default: 0.7) |
|
| threshold | float | No | Match confidence threshold (0.0-1.0, default: 0.7) |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -41,8 +41,8 @@ Automatically link books to media items based on matching metadata.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------ |
|
||||||
| 400 | Invalid request data |
|
| 400 | Invalid request data |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 404 | Device not found |
|
| 404 | Device not found |
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ Link multiple books to media items at once.
|
|||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ----- | ----- | -------- | -------------------------------- |
|
||||||
| links | array | Yes | Array of book-media link objects |
|
| links | array | Yes | Array of book-media link objects |
|
||||||
|
|
||||||
Each link object contains:
|
Each link object contains:
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
@@ -50,8 +50,8 @@ Each link object contains:
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------------------- |
|
||||||
| 400 | Invalid request data |
|
| 400 | Invalid request data |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 404 | Device, book, or media item not found |
|
| 404 | Device, book, or media item not found |
|
||||||
|
|||||||
@@ -8,17 +8,17 @@ Create a new file alias for a device.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | ----------- |
|
||||||
| id | string (UUID) | Yes | Device UUID |
|
| id | string (UUID) | Yes | Device UUID |
|
||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------------- | -------- | ---------------------------------- |
|
||||||
| media_item_id | string (UUID) | Yes | Media item UUID |
|
| media_item_id | string (UUID) | Yes | Media item UUID |
|
||||||
| file_name | string | Yes | Name of the file |
|
| file_name | string | Yes | Name of the file |
|
||||||
| file_hash | string | No | SHA256 hash of the file (optional) |
|
| file_hash | string | No | SHA256 hash of the file (optional) |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -45,9 +45,9 @@ Create a new file alias for a device.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------------ |
|
||||||
| 400 | Invalid request data |
|
| 400 | Invalid request data |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 404 | Device or media item not found |
|
| 404 | Device or media item not found |
|
||||||
| 409 | File alias already exists |
|
| 409 | File alias already exists |
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ Delete a device file alias.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | --------------- |
|
||||||
| id | string (UUID) | Yes | Device UUID |
|
| id | string (UUID) | Yes | Device UUID |
|
||||||
| aliasId | string (UUID) | Yes | File alias UUID |
|
| aliasId | string (UUID) | Yes | File alias UUID |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ------------ |
|
||||||
| Authorization | string | Yes | Bearer token |
|
| Authorization | string | Yes | Bearer token |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ File alias deleted successfully.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------------ |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 404 | Device or file alias not found |
|
| 404 | Device or file alias not found |
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ Get potential book matches for a given query.
|
|||||||
|
|
||||||
## Query Parameters
|
## Query Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------- | -------- | ------------------------------------------------- |
|
||||||
| q | string | Yes | Search query (title, author, etc.) |
|
| q | string | Yes | Search query (title, author, etc.) |
|
||||||
| limit | integer | No | Maximum number of matches to return (default: 10) |
|
| limit | integer | No | Maximum number of matches to return (default: 10) |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ------------ |
|
||||||
| Authorization | string | Yes | Bearer token |
|
| Authorization | string | Yes | Bearer token |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | -------------------------------- |
|
||||||
| 400 | Missing required query parameter |
|
| 400 | Missing required query parameter |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ Get all file aliases for a specific device.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | ----------- |
|
||||||
| id | string (UUID) | Yes | Device UUID |
|
| id | string (UUID) | Yes | Device UUID |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ------------ |
|
||||||
| Authorization | string | Yes | Bearer token |
|
| Authorization | string | Yes | Bearer token |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------ |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 404 | Device not found |
|
| 404 | Device not found |
|
||||||
|
|||||||
@@ -7,22 +7,22 @@ Get suggested matches for unlinked books on a device.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | ----------- |
|
||||||
| id | string (UUID) | Yes | Device UUID |
|
| id | string (UUID) | Yes | Device UUID |
|
||||||
|
|
||||||
## Query Parameters
|
## Query Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------- | -------- | --------------------------------------------------- |
|
||||||
| limit | integer | No | Maximum number of suggestions per book (default: 5) |
|
| limit | integer | No | Maximum number of suggestions per book (default: 5) |
|
||||||
| threshold | float | No | Minimum confidence threshold (default: 0.5) |
|
| threshold | float | No | Minimum confidence threshold (default: 0.5) |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ------------ |
|
||||||
| Authorization | string | Yes | Bearer token |
|
| Authorization | string | Yes | Bearer token |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------ |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 404 | Device not found |
|
| 404 | Device not found |
|
||||||
|
|||||||
@@ -7,22 +7,22 @@ Get all books that haven't been linked to media items yet for a specific device.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | ----------- |
|
||||||
| deviceId | string (UUID) | Yes | Device UUID |
|
| deviceId | string (UUID) | Yes | Device UUID |
|
||||||
|
|
||||||
## Query Parameters
|
## Query Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------- | -------- | ----------------------------------------------- |
|
||||||
| limit | integer | No | Maximum number of items to return (default: 50) |
|
| limit | integer | No | Maximum number of items to return (default: 50) |
|
||||||
| offset | integer | No | Number of items to skip (default: 0) |
|
| offset | integer | No | Number of items to skip (default: 0) |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ------------ |
|
||||||
| Authorization | string | Yes | Bearer token |
|
| Authorization | string | Yes | Bearer token |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------ |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 404 | Device not found |
|
| 404 | Device not found |
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ Link a device book to a Bookhoard media item. Supports bulk linking.
|
|||||||
|
|
||||||
## Manual Link Request Body
|
## Manual Link Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------------------ | ------ | -------- | ------------------------- |
|
||||||
| links | array | Yes | List of book links |
|
| links | array | Yes | List of book links |
|
||||||
| links[].unlinked_book_id | string | Yes | Device book UUID |
|
| links[].unlinked_book_id | string | Yes | Device book UUID |
|
||||||
| links[].media_item_id | string | Yes | Bookhoard media item UUID |
|
| links[].media_item_id | string | Yes | Bookhoard media item UUID |
|
||||||
| links[].confidence_score | float | No | Match confidence (0-1) |
|
| links[].confidence_score | float | No | Match confidence (0-1) |
|
||||||
|
|
||||||
### Example Manual Link Request
|
### Example Manual Link Request
|
||||||
|
|
||||||
@@ -31,10 +31,10 @@ Link a device book to a Bookhoard media item. Supports bulk linking.
|
|||||||
|
|
||||||
## Auto-Link Request Body
|
## Auto-Link Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| -------------------- | ------- | -------- | ----------------------------------------------- |
|
||||||
| confidence_threshold | float | No | Minimum confidence for auto-link (default: 0.8) |
|
| confidence_threshold | float | No | Minimum confidence for auto-link (default: 0.8) |
|
||||||
| limit | integer | No | Maximum books to auto-link (default: 50) |
|
| limit | integer | No | Maximum books to auto-link (default: 50) |
|
||||||
|
|
||||||
### Example Auto-Link Request
|
### Example Auto-Link Request
|
||||||
|
|
||||||
@@ -81,8 +81,8 @@ Link a device book to a Bookhoard media item. Supports bulk linking.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------ |
|
||||||
| 400 | Invalid link data |
|
| 400 | Invalid link data |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 404 | Media item not found |
|
| 404 | Media item not found |
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ Query books to find potential matches for linking.
|
|||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ----------- | ------- | -------- | -------------------------------- |
|
||||||
| identifiers | array | No | List of identifiers (ISBN, UUID) |
|
| identifiers | array | No | List of identifiers (ISBN, UUID) |
|
||||||
| sha256 | string | No | SHA256 hash of book file |
|
| sha256 | string | No | SHA256 hash of book file |
|
||||||
| title | string | No | Book title |
|
| title | string | No | Book title |
|
||||||
| author | string | No | Book author |
|
| author | string | No | Book author |
|
||||||
| file_size | integer | No | File size in bytes |
|
| file_size | integer | No | File size in bytes |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ Query books to find potential matches for linking.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------ |
|
||||||
| 400 | Invalid query parameters |
|
| 400 | Invalid query parameters |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
|
|||||||
@@ -8,17 +8,17 @@ Update an existing device file alias.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | --------------- |
|
||||||
| id | string (UUID) | Yes | Device UUID |
|
| id | string (UUID) | Yes | Device UUID |
|
||||||
| aliasId | string (UUID) | Yes | File alias UUID |
|
| aliasId | string (UUID) | Yes | File alias UUID |
|
||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| --------- | ------ | -------- | ------------- |
|
||||||
| file_name | string | No | New file name |
|
| file_name | string | No | New file name |
|
||||||
| file_hash | string | No | New file hash |
|
| file_hash | string | No | New file hash |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -44,8 +44,8 @@ Update an existing device file alias.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------------ |
|
||||||
| 400 | Invalid request data |
|
| 400 | Invalid request data |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 404 | Device or file alias not found |
|
| 404 | Device or file alias not found |
|
||||||
|
|||||||
@@ -8,44 +8,44 @@ Add an automatic book assignment rule to a collection.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | --------------- |
|
||||||
| id | string (UUID) | Yes | Collection UUID |
|
| id | string (UUID) | Yes | Collection UUID |
|
||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| -------- | ------------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||||
| field | string | Yes | Field to match on (genre, author, series, language, publisher, copyright_year, tags) |
|
| field | string | Yes | Field to match on (genre, author, series, language, publisher, copyright_year, tags) |
|
||||||
| operator | string | Yes | Comparison operator (equals, not_equals, contains, not_contains, starts_with, ends_with, greater_than, less_than) |
|
| operator | string | Yes | Comparison operator (equals, not_equals, contains, not_contains, starts_with, ends_with, greater_than, less_than) |
|
||||||
| value | string/number | Yes | Value to compare against |
|
| value | string/number | Yes | Value to compare against |
|
||||||
| priority | integer | No | Rule priority (1 = highest, default: 1) |
|
| priority | integer | No | Rule priority (1 = highest, default: 1) |
|
||||||
| enabled | boolean | No | Whether rule is active (default: true) |
|
| enabled | boolean | No | Whether rule is active (default: true) |
|
||||||
|
|
||||||
### Supported Fields
|
### Supported Fields
|
||||||
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
|-------|------|-------------|
|
| -------------- | ------ | ------------------------------------- |
|
||||||
| genre | string | Book genre |
|
| genre | string | Book genre |
|
||||||
| author | string | Book author |
|
| author | string | Book author |
|
||||||
| series | string | Book series name |
|
| series | string | Book series name |
|
||||||
| language | string | Book language |
|
| language | string | Book language |
|
||||||
| publisher | string | Publisher name |
|
| publisher | string | Publisher name |
|
||||||
| copyright_year | number | Publication year (numeric comparison) |
|
| copyright_year | number | Publication year (numeric comparison) |
|
||||||
| tags | string | Book tags |
|
| tags | string | Book tags |
|
||||||
|
|
||||||
### Supported Operators
|
### Supported Operators
|
||||||
|
|
||||||
| Operator | Type | Description |
|
| Operator | Type | Description |
|
||||||
|----------|------|-------------|
|
| ------------ | ------ | ------------------------------------- |
|
||||||
| equals | all | Exact match |
|
| equals | all | Exact match |
|
||||||
| not_equals | all | Not equal |
|
| not_equals | all | Not equal |
|
||||||
| contains | string | Contains substring (case-insensitive) |
|
| contains | string | Contains substring (case-insensitive) |
|
||||||
| not_contains | string | Does not contain |
|
| not_contains | string | Does not contain |
|
||||||
| starts_with | string | Starts with (case-insensitive) |
|
| starts_with | string | Starts with (case-insensitive) |
|
||||||
| ends_with | string | Ends with (case-insensitive) |
|
| ends_with | string | Ends with (case-insensitive) |
|
||||||
| greater_than | number | Greater than |
|
| greater_than | number | Greater than |
|
||||||
| less_than | number | Less than |
|
| less_than | number | Less than |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -75,11 +75,11 @@ Add an automatic book assignment rule to a collection.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ----------------------------------- |
|
||||||
| 400 | Invalid request (validation failed) |
|
| 400 | Invalid request (validation failed) |
|
||||||
| 401 | Authentication required |
|
| 401 | Authentication required |
|
||||||
| 404 | Collection not found |
|
| 404 | Collection not found |
|
||||||
| 500 | Internal server error |
|
| 500 | Internal server error |
|
||||||
|
|
||||||
## Try It Out
|
## Try It Out
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ Add multiple books to a collection at once.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | --------------- |
|
||||||
| id | string (UUID) | Yes | Collection UUID |
|
| id | string (UUID) | Yes | Collection UUID |
|
||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| -------- | ------------- | -------- | ------------------------ |
|
||||||
| book_ids | array of UUID | Yes | Array of book IDs to add |
|
| book_ids | array of UUID | Yes | Array of book IDs to add |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -42,11 +42,11 @@ Books added to collection successfully. No response body.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ----------------------------------- |
|
||||||
| 400 | Invalid request (validation failed) |
|
| 400 | Invalid request (validation failed) |
|
||||||
| 401 | Authentication required |
|
| 401 | Authentication required |
|
||||||
| 404 | Collection or book(s) not found |
|
| 404 | Collection or book(s) not found |
|
||||||
| 500 | Internal server error |
|
| 500 | Internal server error |
|
||||||
|
|
||||||
## Try It Out
|
## Try It Out
|
||||||
|
|||||||
@@ -8,24 +8,24 @@ Create a new collection.
|
|||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ----------------- | ------ | -------- | -------------------------------- |
|
||||||
| name | string | Yes | Collection name (max 255 chars) |
|
| name | string | Yes | Collection name (max 255 chars) |
|
||||||
| description | string | No | Collection description |
|
| description | string | No | Collection description |
|
||||||
| color | string | No | Hex color code (e.g., "#FF5733") |
|
| color | string | No | Hex color code (e.g., "#FF5733") |
|
||||||
| icon | string | No | Emoji icon (e.g., "🚀", "📖") |
|
| icon | string | No | Emoji icon (e.g., "🚀", "📖") |
|
||||||
| auto_assign_rules | array | No | Array of rule objects |
|
| auto_assign_rules | array | No | Array of rule objects |
|
||||||
| view_settings | object | No | Per-device display preferences |
|
| view_settings | object | No | Per-device display preferences |
|
||||||
|
|
||||||
### Auto-Assign Rule Object
|
### Auto-Assign Rule Object
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| -------- | ------------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||||
| field | string | Yes | Field to match on (genre, author, series, language, publisher, copyright_year, tags) |
|
| field | string | Yes | Field to match on (genre, author, series, language, publisher, copyright_year, tags) |
|
||||||
| operator | string | Yes | Comparison operator (equals, not_equals, contains, not_contains, starts_with, ends_with, greater_than, less_than) |
|
| operator | string | Yes | Comparison operator (equals, not_equals, contains, not_contains, starts_with, ends_with, greater_than, less_than) |
|
||||||
| value | string/number | Yes | Value to compare against |
|
| value | string/number | Yes | Value to compare against |
|
||||||
| priority | integer | No | Rule priority (1 = highest, default: 1) |
|
| priority | integer | No | Rule priority (1 = highest, default: 1) |
|
||||||
| enabled | boolean | No | Whether rule is active (default: true) |
|
| enabled | boolean | No | Whether rule is active (default: true) |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -87,10 +87,10 @@ Create a new collection.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ----------------------------------- |
|
||||||
| 400 | Invalid request (validation failed) |
|
| 400 | Invalid request (validation failed) |
|
||||||
| 401 | Authentication required |
|
| 401 | Authentication required |
|
||||||
| 500 | Internal server error |
|
| 500 | Internal server error |
|
||||||
|
|
||||||
## Try It Out
|
## Try It Out
|
||||||
|
|||||||
@@ -8,26 +8,26 @@ Map a collection to a device shelf for syncing.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | ----------- |
|
||||||
| deviceId | string (UUID) | Yes | Device UUID |
|
| deviceId | string (UUID) | Yes | Device UUID |
|
||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ----------------- | ------------- | -------- | ----------------------------------------- |
|
||||||
| collection_id | string (UUID) | Yes | Collection UUID to map |
|
| collection_id | string (UUID) | Yes | Collection UUID to map |
|
||||||
| device_shelf_name | string | Yes | Name of the shelf on the device |
|
| device_shelf_name | string | Yes | Name of the shelf on the device |
|
||||||
| sync_direction | string | No | Sync direction (default: "bidirectional") |
|
| sync_direction | string | No | Sync direction (default: "bidirectional") |
|
||||||
|
|
||||||
### Sync Directions
|
### Sync Directions
|
||||||
|
|
||||||
| Direction | Description |
|
| Direction | Description |
|
||||||
|-----------|-------------|
|
| --------------- | ------------------------------------------- |
|
||||||
| bidirectional | Sync both ways between Bookhoard and device |
|
| bidirectional | Sync both ways between Bookhoard and device |
|
||||||
| book_to_hoard | Bookhoard → Device only |
|
| book_to_hoard | Bookhoard → Device only |
|
||||||
| device_to_hoard | Device → Bookhoard only |
|
| device_to_hoard | Device → Bookhoard only |
|
||||||
| none | No sync (mapping only for reference) |
|
| none | No sync (mapping only for reference) |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -58,12 +58,12 @@ Collections can be synced to device-specific shelves (Kobo, KOReader). This allo
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ----------------------------------- |
|
||||||
| 400 | Invalid request (validation failed) |
|
| 400 | Invalid request (validation failed) |
|
||||||
| 401 | Authentication required |
|
| 401 | Authentication required |
|
||||||
| 404 | Device or collection not found |
|
| 404 | Device or collection not found |
|
||||||
| 409 | Mapping already exists |
|
| 409 | Mapping already exists |
|
||||||
| 500 | Internal server error |
|
| 500 | Internal server error |
|
||||||
|
|
||||||
## Try It Out
|
## Try It Out
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ Delete a collection. Books are NOT deleted.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | --------------- |
|
||||||
| id | string (UUID) | Yes | Collection UUID |
|
| id | string (UUID) | Yes | Collection UUID |
|
||||||
|
|
||||||
## Response (204 No Content)
|
## Response (204 No Content)
|
||||||
|
|
||||||
@@ -18,10 +18,10 @@ Collection deleted successfully. No response body.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ----------------------- |
|
||||||
| 401 | Authentication required |
|
| 401 | Authentication required |
|
||||||
| 404 | Collection not found |
|
| 404 | Collection not found |
|
||||||
| 500 | Internal server error |
|
| 500 | Internal server error |
|
||||||
|
|
||||||
## Try It Out
|
## Try It Out
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ Remove a collection-to-shelf mapping for a device.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| ------------ | ------------- | -------- | --------------- |
|
||||||
| deviceId | string (UUID) | Yes | Device UUID |
|
| deviceId | string (UUID) | Yes | Device UUID |
|
||||||
| collectionId | string (UUID) | Yes | Collection UUID |
|
| collectionId | string (UUID) | Yes | Collection UUID |
|
||||||
|
|
||||||
## Response (204 No Content)
|
## Response (204 No Content)
|
||||||
|
|
||||||
@@ -25,10 +25,10 @@ Shelf mapping deleted successfully. No response body.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------------ |
|
||||||
| 401 | Authentication required |
|
| 401 | Authentication required |
|
||||||
| 404 | Device or collection not found |
|
| 404 | Device or collection not found |
|
||||||
| 500 | Internal server error |
|
| 500 | Internal server error |
|
||||||
|
|
||||||
## Try It Out
|
## Try It Out
|
||||||
|
|||||||
@@ -8,17 +8,17 @@ Get single collection with all books.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | --------------- |
|
||||||
| id | string (UUID) | Yes | Collection UUID |
|
| id | string (UUID) | Yes | Collection UUID |
|
||||||
|
|
||||||
## Query Parameters
|
## Query Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| ------------- | ------- | -------- | ----------------------------------------- |
|
||||||
| include_books | boolean | No | Include books in response (default: true) |
|
| include_books | boolean | No | Include books in response (default: true) |
|
||||||
| limit | integer | No | Number of books to return (default: 50) |
|
| limit | integer | No | Number of books to return (default: 50) |
|
||||||
| offset | integer | No | Number of books to skip (default: 0) |
|
| offset | integer | No | Number of books to skip (default: 0) |
|
||||||
|
|
||||||
## Response (200 OK)
|
## Response (200 OK)
|
||||||
|
|
||||||
@@ -45,10 +45,10 @@ Get single collection with all books.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ----------------------- |
|
||||||
| 401 | Authentication required |
|
| 401 | Authentication required |
|
||||||
| 404 | Collection not found |
|
| 404 | Collection not found |
|
||||||
| 500 | Internal server error |
|
| 500 | Internal server error |
|
||||||
|
|
||||||
## Try It Out
|
## Try It Out
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ Get all collections for the authenticated user.
|
|||||||
|
|
||||||
## Query Parameters
|
## Query Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------- | -------- | --------------------------------------------- |
|
||||||
| limit | integer | No | Number of collections to return (default: 50) |
|
| limit | integer | No | Number of collections to return (default: 50) |
|
||||||
| offset | integer | No | Number of collections to skip (default: 0) |
|
| offset | integer | No | Number of collections to skip (default: 0) |
|
||||||
|
|
||||||
## Response (200 OK)
|
## Response (200 OK)
|
||||||
|
|
||||||
@@ -49,9 +49,9 @@ Get all collections for the authenticated user.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ----------------------- |
|
||||||
| 401 | Authentication required |
|
| 401 | Authentication required |
|
||||||
| 500 | Internal server error |
|
| 500 | Internal server error |
|
||||||
|
|
||||||
## Try It Out
|
## Try It Out
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ Remove an automatic book assignment rule from a collection.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| ------------ | ------------- | -------- | --------------- |
|
||||||
| collectionId | string (UUID) | Yes | Collection UUID |
|
| collectionId | string (UUID) | Yes | Collection UUID |
|
||||||
| ruleId | string (UUID) | Yes | Rule UUID |
|
| ruleId | string (UUID) | Yes | Rule UUID |
|
||||||
|
|
||||||
## Response (204 No Content)
|
## Response (204 No Content)
|
||||||
|
|
||||||
@@ -19,10 +19,10 @@ Rule deleted successfully. No response body.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ---------------------------- |
|
||||||
| 401 | Authentication required |
|
| 401 | Authentication required |
|
||||||
| 404 | Collection or rule not found |
|
| 404 | Collection or rule not found |
|
||||||
| 500 | Internal server error |
|
| 500 | Internal server error |
|
||||||
|
|
||||||
## Try It Out
|
## Try It Out
|
||||||
|
|||||||
@@ -8,17 +8,17 @@ Test which books would match given rules without saving.
|
|||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ----- | ----- | -------- | ----------------------------- |
|
||||||
| rules | array | Yes | Array of rule objects to test |
|
| rules | array | Yes | Array of rule objects to test |
|
||||||
|
|
||||||
### Rule Object
|
### Rule Object
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| -------- | ------------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||||
| field | string | Yes | Field to match on (genre, author, series, language, publisher, copyright_year, tags) |
|
| field | string | Yes | Field to match on (genre, author, series, language, publisher, copyright_year, tags) |
|
||||||
| operator | string | Yes | Comparison operator (equals, not_equals, contains, not_contains, starts_with, ends_with, greater_than, less_than) |
|
| operator | string | Yes | Comparison operator (equals, not_equals, contains, not_contains, starts_with, ends_with, greater_than, less_than) |
|
||||||
| value | string/number | Yes | Value to compare against |
|
| value | string/number | Yes | Value to compare against |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -62,10 +62,10 @@ Test rules before creating a collection to verify correct book matching. This en
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ----------------------------------- |
|
||||||
| 400 | Invalid request (validation failed) |
|
| 400 | Invalid request (validation failed) |
|
||||||
| 401 | Authentication required |
|
| 401 | Authentication required |
|
||||||
| 500 | Internal server error |
|
| 500 | Internal server error |
|
||||||
|
|
||||||
## Try It Out
|
## Try It Out
|
||||||
|
|||||||
@@ -8,22 +8,22 @@ Update collection details.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | --------------- |
|
||||||
| id | string (UUID) | Yes | Collection UUID |
|
| id | string (UUID) | Yes | Collection UUID |
|
||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
All fields are optional. Include only fields you want to update.
|
All fields are optional. Include only fields you want to update.
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ----------------- | ------ | -------- | ----------------------------------------------- |
|
||||||
| name | string | No | Collection name (max 255 chars) |
|
| name | string | No | Collection name (max 255 chars) |
|
||||||
| description | string | No | Collection description |
|
| description | string | No | Collection description |
|
||||||
| color | string | No | Hex color code (e.g., "#FF5733") |
|
| color | string | No | Hex color code (e.g., "#FF5733") |
|
||||||
| icon | string | No | Emoji icon (e.g., "🚀", "📖") |
|
| icon | string | No | Emoji icon (e.g., "🚀", "📖") |
|
||||||
| auto_assign_rules | array | No | Array of rule objects (replaces existing rules) |
|
| auto_assign_rules | array | No | Array of rule objects (replaces existing rules) |
|
||||||
| view_settings | object | No | Per-device display preferences |
|
| view_settings | object | No | Per-device display preferences |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -53,11 +53,11 @@ All fields are optional. Include only fields you want to update.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ----------------------------------- |
|
||||||
| 400 | Invalid request (validation failed) |
|
| 400 | Invalid request (validation failed) |
|
||||||
| 401 | Authentication required |
|
| 401 | Authentication required |
|
||||||
| 404 | Collection not found |
|
| 404 | Collection not found |
|
||||||
| 500 | Internal server error |
|
| 500 | Internal server error |
|
||||||
|
|
||||||
## Try It Out
|
## Try It Out
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ Dismiss multiple conflicts at once.
|
|||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------ | ------------- | -------- | ---------------------------------- |
|
||||||
| conflict_ids | array of UUID | Yes | Array of conflict UUIDs to dismiss |
|
| conflict_ids | array of UUID | Yes | Array of conflict UUIDs to dismiss |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ Dismiss multiple conflicts at once.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------ |
|
||||||
| 400 | Invalid request body |
|
| 400 | Invalid request body |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ Resolve multiple conflicts at once using a specified strategy.
|
|||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------ | ------------- | -------- | -------------------------------------------------------------- |
|
||||||
| conflict_ids | array of UUID | Yes | Array of conflict UUIDs to resolve |
|
| conflict_ids | array of UUID | Yes | Array of conflict UUIDs to resolve |
|
||||||
| resolution | string | Yes | Resolution strategy: "device", "server", or "highest_progress" |
|
| resolution | string | Yes | Resolution strategy: "device", "server", or "highest_progress" |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -37,8 +37,8 @@ Resolve multiple conflicts at once using a specified strategy.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | --------------------------- |
|
||||||
| 400 | Invalid request body |
|
| 400 | Invalid request body |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 400 | Invalid resolution strategy |
|
| 400 | Invalid resolution strategy |
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ Delete a specific conflict record.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | ------------- |
|
||||||
| id | string (UUID) | Yes | Conflict UUID |
|
| id | string (UUID) | Yes | Conflict UUID |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ------------ |
|
||||||
| Authorization | string | Yes | Bearer token |
|
| Authorization | string | Yes | Bearer token |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ Conflict deleted successfully.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------ |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 404 | Conflict not found |
|
| 404 | Conflict not found |
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ Dismiss all resolved conflicts.
|
|||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ------------ |
|
||||||
| Authorization | string | Yes | Bearer token |
|
| Authorization | string | Yes | Bearer token |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -29,6 +29,6 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------ |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ Get detailed information about a specific conflict.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | ------------- |
|
||||||
| id | string (UUID) | Yes | Conflict UUID |
|
| id | string (UUID) | Yes | Conflict UUID |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ------------ |
|
||||||
| Authorization | string | Yes | Bearer token |
|
| Authorization | string | Yes | Bearer token |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------ |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 404 | Conflict not found |
|
| 404 | Conflict not found |
|
||||||
|
|||||||
@@ -7,18 +7,18 @@ List all sync conflicts for the current user.
|
|||||||
|
|
||||||
## Query Parameters
|
## Query Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| ------------- | ------------- | -------- | --------------------------------------------------- |
|
||||||
| status | string | No | Filter by status (active, resolved, dismissed) |
|
| status | string | No | Filter by status (active, resolved, dismissed) |
|
||||||
| media_item_id | string (UUID) | No | Filter by media item |
|
| media_item_id | string (UUID) | No | Filter by media item |
|
||||||
| limit | integer | No | Maximum number of conflicts to return (default: 50) |
|
| limit | integer | No | Maximum number of conflicts to return (default: 50) |
|
||||||
| offset | integer | No | Number of conflicts to skip (default: 0) |
|
| offset | integer | No | Number of conflicts to skip (default: 0) |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ------------ |
|
||||||
| Authorization | string | Yes | Bearer token |
|
| Authorization | string | Yes | Bearer token |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -60,6 +60,6 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------ |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ Resolve a specific conflict by choosing which version to keep.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | ------------- |
|
||||||
| id | string (UUID) | Yes | Conflict UUID |
|
| id | string (UUID) | Yes | Conflict UUID |
|
||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ---------- | ------ | -------- | -------------------------------------------------------------- |
|
||||||
| resolution | string | Yes | Resolution strategy: "device", "server", or "highest_progress" |
|
| resolution | string | Yes | Resolution strategy: "device", "server", or "highest_progress" |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -38,9 +38,9 @@ Resolve a specific conflict by choosing which version to keep.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | --------------------------- |
|
||||||
| 400 | Invalid resolution strategy |
|
| 400 | Invalid resolution strategy |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 404 | Conflict not found |
|
| 404 | Conflict not found |
|
||||||
| 400 | Conflict already resolved |
|
| 400 | Conflict already resolved |
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ Evaluates filter rules and returns matching items without saving the collection.
|
|||||||
**Endpoint:** `POST /api/collections/preview`
|
**Endpoint:** `POST /api/collections/preview`
|
||||||
|
|
||||||
**Request Body:**
|
**Request Body:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"library_id": "uuid",
|
"library_id": "uuid",
|
||||||
@@ -28,23 +29,24 @@ Evaluates filter rules and returns matching items without saving the collection.
|
|||||||
|
|
||||||
**Available Filter Fields:**
|
**Available Filter Fields:**
|
||||||
|
|
||||||
| Field | Type | Operators |
|
| Field | Type | Operators |
|
||||||
|-------|------|-----------|
|
| ------------ | ------ | ------------------------------------------------------------------------ |
|
||||||
| `title` | text | contains, equals, starts_with, ends_with, regex |
|
| `title` | text | contains, equals, starts_with, ends_with, regex |
|
||||||
| `author` | text | contains, equals |
|
| `author` | text | contains, equals |
|
||||||
| `genre` | select | equals, not_equals, in, not_in |
|
| `genre` | select | equals, not_equals, in, not_in |
|
||||||
| `series` | text | is_set, is_not_set, equals, contains |
|
| `series` | text | is_set, is_not_set, equals, contains |
|
||||||
| `progress` | number | equals, not_equals, greater_than, less_than, between, is_set, is_not_set |
|
| `progress` | number | equals, not_equals, greater_than, less_than, between, is_set, is_not_set |
|
||||||
| `rating` | number | equals, not_equals, greater_than, less_than, is_set, is_not_set |
|
| `rating` | number | equals, not_equals, greater_than, less_than, is_set, is_not_set |
|
||||||
| `date_added` | date | equals, not_equals, before, after, between, last_x_days |
|
| `date_added` | date | equals, not_equals, before, after, between, last_x_days |
|
||||||
| `last_read` | date | equals, before, after, between, last_x_days, is_set, is_not_set |
|
| `last_read` | date | equals, before, after, between, last_x_days, is_set, is_not_set |
|
||||||
| `publisher` | text | contains, equals |
|
| `publisher` | text | contains, equals |
|
||||||
| `language` | select | equals, not_equals, in |
|
| `language` | select | equals, not_equals, in |
|
||||||
| `format` | select | equals, in |
|
| `format` | select | equals, in |
|
||||||
| `tags` | text | contains, not_contains, equals |
|
| `tags` | text | contains, not_contains, equals |
|
||||||
| `narrators` | text | contains, equals, is_set, is_not_set |
|
| `narrators` | text | contains, equals, is_set, is_not_set |
|
||||||
|
|
||||||
**Response:**
|
**Response:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"items": [
|
"items": [
|
||||||
@@ -65,6 +67,7 @@ Creates a new custom collection with filter rules and/or manual book selection.
|
|||||||
**Endpoint:** `POST /api/collections`
|
**Endpoint:** `POST /api/collections`
|
||||||
|
|
||||||
**Request Body:**
|
**Request Body:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "My Custom Section",
|
"name": "My Custom Section",
|
||||||
@@ -89,6 +92,7 @@ Creates a new custom collection with filter rules and/or manual book selection.
|
|||||||
**TypeScript:** `web/src/custom-section-builder.ts`
|
**TypeScript:** `web/src/custom-section-builder.ts`
|
||||||
|
|
||||||
Key features:
|
Key features:
|
||||||
|
|
||||||
- 14 filter fields with various operators
|
- 14 filter fields with various operators
|
||||||
- Live preview functionality
|
- Live preview functionality
|
||||||
- Search + multi-select for manual book addition
|
- Search + multi-select for manual book addition
|
||||||
@@ -97,6 +101,7 @@ Key features:
|
|||||||
## Example Use Cases
|
## Example Use Cases
|
||||||
|
|
||||||
### Sci-Fi Favorites
|
### Sci-Fi Favorites
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"rules": [
|
"rules": [
|
||||||
@@ -110,6 +115,7 @@ Key features:
|
|||||||
```
|
```
|
||||||
|
|
||||||
### High Rated Books
|
### High Rated Books
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"rules": [
|
"rules": [
|
||||||
@@ -123,6 +129,7 @@ Key features:
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Long Books (Manual Selection)
|
### Long Books (Manual Selection)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"manual_book_ids": ["uuid1", "uuid2", "uuid3"]
|
"manual_book_ids": ["uuid1", "uuid2", "uuid3"]
|
||||||
@@ -130,6 +137,7 @@ Key features:
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Recently Finished Audiobooks
|
### Recently Finished Audiobooks
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"rules": [
|
"rules": [
|
||||||
|
|||||||
@@ -10,26 +10,27 @@ Retrieve all dashboard sections for a specific library, including system collect
|
|||||||
|
|
||||||
### Query Parameters
|
### Query Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|--------|----------|-----------------------------------------------|
|
| ---------- | ------ | -------- | ----------------------------------------- |
|
||||||
| library_id| string | Yes | Library UUID to fetch sections for |
|
| library_id | string | Yes | Library UUID to fetch sections for |
|
||||||
| limit | number | No | Items per section (default: 20, max: 100) |
|
| limit | number | No | Items per section (default: 20, max: 100) |
|
||||||
|
|
||||||
### Response
|
### Response
|
||||||
|
|
||||||
Returns array of sections in user's customized order (respects `collection_order` and `hidden_collections` preferences).
|
Returns array of sections in user's customized order (respects `collection_order` and `hidden_collections` preferences).
|
||||||
|
|
||||||
**Section Types**:
|
**Section Types**:
|
||||||
|
|
||||||
- `is_system: true`: System collections (4 pre-seeded defaults)
|
- `is_system: true`: System collections (4 pre-seeded defaults)
|
||||||
- `is_system: false`: User-created collections with `show_on_dashboard: true`
|
- `is_system: false`: User-created collections with `show_on_dashboard: true`
|
||||||
|
|
||||||
**System Collections**:
|
**System Collections**:
|
||||||
| ID | Title | Icon | Description |
|
| ID | Title | Icon | Description |
|
||||||
|-----------------|------------------|------|--------------------------------------------------|
|
|-----------------|------------------|------|--------------------------------------------------|
|
||||||
| continue-reading| Continue Reading | 📖 | Books with progress > 0% and < 100% |
|
| continue-reading| Continue Reading | 📖 | Books with progress > 0% and < 100% |
|
||||||
| recently-added | Recently Added | 🆕 | Newest items in library |
|
| recently-added | Recently Added | 🆕 | Newest items in library |
|
||||||
| recently-read | Recently Read | ✅ | Books with progress = 100% |
|
| recently-read | Recently Read | ✅ | Books with progress = 100% |
|
||||||
| not-started | Not Started | 📕 | Books with no reading progress |
|
| not-started | Not Started | 📕 | Books with no reading progress |
|
||||||
|
|
||||||
### Example Response
|
### Example Response
|
||||||
|
|
||||||
@@ -115,6 +116,7 @@ Reset a system collection to its default state (removes user customizations).
|
|||||||
```
|
```
|
||||||
|
|
||||||
Valid `collection_name` values:
|
Valid `collection_name` values:
|
||||||
|
|
||||||
- `continue-reading`
|
- `continue-reading`
|
||||||
- `recently-added`
|
- `recently-added`
|
||||||
- `recently-read`
|
- `recently-read`
|
||||||
@@ -130,12 +132,12 @@ Valid `collection_name` values:
|
|||||||
|
|
||||||
### Error Responses
|
### Error Responses
|
||||||
|
|
||||||
| Status | Description |
|
| Status | Description |
|
||||||
|--------|--------------------------------|
|
| ------ | ---------------------------- |
|
||||||
| 400 | Missing library_id |
|
| 400 | Missing library_id |
|
||||||
| 400 | Invalid library_id |
|
| 400 | Invalid library_id |
|
||||||
| 400 | Invalid collection_name |
|
| 400 | Invalid collection_name |
|
||||||
| 401 | Unauthorized |
|
| 401 | Unauthorized |
|
||||||
| 500 | Failed to load sections |
|
| 500 | Failed to load sections |
|
||||||
| 500 | Failed to save preferences |
|
| 500 | Failed to save preferences |
|
||||||
| 500 | Failed to restore collection |
|
| 500 | Failed to restore collection |
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ Add a media item to a device's shelf (Kobo reading shelf).
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | ----------- |
|
||||||
| id | string (UUID) | Yes | Device UUID |
|
| id | string (UUID) | Yes | Device UUID |
|
||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------------- | -------- | ------------------------------- |
|
||||||
| media_item_id | string (UUID) | Yes | Media item UUID to add to shelf |
|
| media_item_id | string (UUID) | Yes | Media item UUID to add to shelf |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -38,9 +38,9 @@ Add a media item to a device's shelf (Kobo reading shelf).
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------------ |
|
||||||
| 400 | Invalid request data |
|
| 400 | Invalid request data |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 404 | Device or media item not found |
|
| 404 | Device or media item not found |
|
||||||
| 409 | Item already on shelf |
|
| 409 | Item already on shelf |
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ Approve a pending device registration request.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------------- | ------------- | -------- | ------------------------- |
|
||||||
| registration_id | string (UUID) | Yes | Registration request UUID |
|
| registration_id | string (UUID) | Yes | Registration request UUID |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ----------------------------------- |
|
||||||
| Authorization | string | Yes | Bearer token (must have admin role) |
|
| Authorization | string | Yes | Bearer token (must have admin role) |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -38,9 +38,9 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ----------------------------------- |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User does not have admin privileges |
|
| 403 | User does not have admin privileges |
|
||||||
| 404 | Registration not found |
|
| 404 | Registration not found |
|
||||||
| 400 | Registration already processed |
|
| 400 | Registration already processed |
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ Remove all items from a device's shelf.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | ----------- |
|
||||||
| id | string (UUID) | Yes | Device UUID |
|
| id | string (UUID) | Yes | Device UUID |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ------------ |
|
||||||
| Authorization | string | Yes | Bearer token |
|
| Authorization | string | Yes | Bearer token |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ Shelf cleared successfully.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------ |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 404 | Device not found |
|
| 404 | Device not found |
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ Delete a device and revoke its access.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | ----------- |
|
||||||
| id | string (UUID) | Yes | Device UUID |
|
| id | string (UUID) | Yes | Device UUID |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ------------ |
|
||||||
| Authorization | string | Yes | Bearer token |
|
| Authorization | string | Yes | Bearer token |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -30,8 +30,8 @@ Device deleted successfully.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------------ |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | Device does not belong to user |
|
| 403 | Device does not belong to user |
|
||||||
| 404 | Device not found |
|
| 404 | Device not found |
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ Check device registration status or get device details.
|
|||||||
|
|
||||||
## Request Body (Status Check)
|
## Request Body (Status Check)
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| --------------- | ------ | -------- | ----------------- |
|
||||||
| registration_id | string | Yes | Registration UUID |
|
| registration_id | string | Yes | Registration UUID |
|
||||||
|
|
||||||
### Example Request (Status Check)
|
### Example Request (Status Check)
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ Check device registration status or get device details.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | --------------------------------------------- |
|
||||||
| 401 | Invalid or expired token (for device details) |
|
| 401 | Invalid or expired token (for device details) |
|
||||||
| 404 | Device or registration not found |
|
| 404 | Device or registration not found |
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ Get all items on a device's shelf.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | ----------- |
|
||||||
| id | string (UUID) | Yes | Device UUID |
|
| id | string (UUID) | Yes | Device UUID |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ------------ |
|
||||||
| Authorization | string | Yes | Bearer token |
|
| Authorization | string | Yes | Bearer token |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------ |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 404 | Device not found |
|
| 404 | Device not found |
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ Retrieve all devices registered to the current user.
|
|||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ------------ |
|
||||||
| Authorization | string | Yes | Bearer token |
|
| Authorization | string | Yes | Bearer token |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -39,6 +39,6 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------ |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ List all pending device registration requests.
|
|||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ----------------------------------- |
|
||||||
| Authorization | string | Yes | Bearer token (must have admin role) |
|
| Authorization | string | Yes | Bearer token (must have admin role) |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ----------------------------------- |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User does not have admin privileges |
|
| 403 | User does not have admin privileges |
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ Register a new device for sync.
|
|||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ----------------- | ------ | -------- | ---------------------------------------- |
|
||||||
| device_name | string | Yes | Device name |
|
| device_name | string | Yes | Device name |
|
||||||
| device_type | string | Yes | Device type: kobo, koreader, web, mobile |
|
| device_type | string | Yes | Device type: kobo, koreader, web, mobile |
|
||||||
| device_identifier | string | Yes | Hardware-specific ID |
|
| device_identifier | string | Yes | Hardware-specific ID |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ Register a new device for sync.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------- |
|
||||||
| 400 | Invalid device data |
|
| 400 | Invalid device data |
|
||||||
| 409 | Device already registered |
|
| 409 | Device already registered |
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ Reject a pending device registration request.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------------- | ------------- | -------- | ------------------------- |
|
||||||
| registration_id | string (UUID) | Yes | Registration request UUID |
|
| registration_id | string (UUID) | Yes | Registration request UUID |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ----------------------------------- |
|
||||||
| Authorization | string | Yes | Bearer token (must have admin role) |
|
| Authorization | string | Yes | Bearer token (must have admin role) |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -34,9 +34,9 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ----------------------------------- |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User does not have admin privileges |
|
| 403 | User does not have admin privileges |
|
||||||
| 404 | Registration not found |
|
| 404 | Registration not found |
|
||||||
| 400 | Registration already processed |
|
| 400 | Registration already processed |
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ Remove a media item from a device's shelf.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | ----------- |
|
||||||
| id | string (UUID) | Yes | Device UUID |
|
| id | string (UUID) | Yes | Device UUID |
|
||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------------- | -------- | ------------------------------------ |
|
||||||
| media_item_id | string (UUID) | Yes | Media item UUID to remove from shelf |
|
| media_item_id | string (UUID) | Yes | Media item UUID to remove from shelf |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -32,8 +32,8 @@ Item removed from shelf successfully.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------------ |
|
||||||
| 400 | Invalid request data |
|
| 400 | Invalid request data |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 404 | Device or shelf item not found |
|
| 404 | Device or shelf item not found |
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ Revoke access to a device.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------ | -------- | ----------- |
|
||||||
| device_id | string | Yes | Device UUID |
|
| device_id | string | Yes | Device UUID |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ------------ |
|
||||||
| Authorization | string | Yes | Bearer token |
|
| Authorization | string | Yes | Bearer token |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -30,8 +30,8 @@ Device revoked successfully.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ----------------------------- |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User does not own this device |
|
| 403 | User does not own this device |
|
||||||
| 404 | Device not found |
|
| 404 | Device not found |
|
||||||
|
|||||||
@@ -8,16 +8,16 @@ Update a device's information.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | ----------- |
|
||||||
| id | string (UUID) | Yes | Device UUID |
|
| id | string (UUID) | Yes | Device UUID |
|
||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ----------- | ------ | -------- | ---------------------------------- |
|
||||||
| name | string | No | Device display name |
|
| name | string | No | Device display name |
|
||||||
| device_type | string | No | Device type (kobo, koreader, etc.) |
|
| device_type | string | No | Device type (kobo, koreader, etc.) |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -44,9 +44,9 @@ Update a device's information.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------------ |
|
||||||
| 400 | Invalid request data |
|
| 400 | Invalid request data |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | Device does not belong to user |
|
| 403 | Device does not belong to user |
|
||||||
| 404 | Device not found |
|
| 404 | Device not found |
|
||||||
|
|||||||
@@ -8,20 +8,20 @@ Create a new highlight for a media item.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------ | -------- | --------------- |
|
||||||
| media_id | string | Yes | Media item UUID |
|
| media_id | string | Yes | Media item UUID |
|
||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ---------------- | ------ | -------- | ----------------------------------------- |
|
||||||
| selection_text | string | Yes | Highlighted text |
|
| selection_text | string | Yes | Highlighted text |
|
||||||
| start_position | string | No | Start position (e.g., epubcfi) |
|
| start_position | string | No | Start position (e.g., epubcfi) |
|
||||||
| end_position | string | No | End position (e.g., epubcfi) |
|
| end_position | string | No | End position (e.g., epubcfi) |
|
||||||
| color | string | No | Highlight color (hex, default: "#ffff00") |
|
| color | string | No | Highlight color (hex, default: "#ffff00") |
|
||||||
| percentage_start | float | No | Start percentage (0-1) |
|
| percentage_start | float | No | Start percentage (0-1) |
|
||||||
| percentage_end | float | No | End percentage (0-1) |
|
| percentage_end | float | No | End percentage (0-1) |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -55,8 +55,8 @@ Create a new highlight for a media item.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------ |
|
||||||
| 400 | Invalid highlight data |
|
| 400 | Invalid highlight data |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 404 | Media item not found |
|
| 404 | Media item not found |
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ Delete a highlight.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| ------------ | ------ | -------- | -------------- |
|
||||||
| highlight_id | string | Yes | Highlight UUID |
|
| highlight_id | string | Yes | Highlight UUID |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ------------ |
|
||||||
| Authorization | string | Yes | Bearer token |
|
| Authorization | string | Yes | Bearer token |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -30,8 +30,8 @@ Highlight deleted successfully.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | -------------------------------- |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User does not own this highlight |
|
| 403 | User does not own this highlight |
|
||||||
| 404 | Highlight not found |
|
| 404 | Highlight not found |
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ Retrieve all highlights for a specific media item.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------ | -------- | --------------- |
|
||||||
| media_id | string | Yes | Media item UUID |
|
| media_id | string | Yes | Media item UUID |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ------------ |
|
||||||
| Authorization | string | Yes | Bearer token |
|
| Authorization | string | Yes | Bearer token |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------ |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 404 | Media item not found |
|
| 404 | Media item not found |
|
||||||
|
|||||||
@@ -8,16 +8,16 @@ Update an existing highlight.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| ------------ | ------ | -------- | -------------- |
|
||||||
| highlight_id | string | Yes | Highlight UUID |
|
| highlight_id | string | Yes | Highlight UUID |
|
||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| -------------- | ------ | -------- | ----------------------------- |
|
||||||
| selection_text | string | No | Updated highlighted text |
|
| selection_text | string | No | Updated highlighted text |
|
||||||
| color | string | No | Updated highlight color (hex) |
|
| color | string | No | Updated highlight color (hex) |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -43,9 +43,9 @@ Update an existing highlight.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | -------------------------------- |
|
||||||
| 400 | Invalid highlight data |
|
| 400 | Invalid highlight data |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User does not own this highlight |
|
| 403 | User does not own this highlight |
|
||||||
| 404 | Highlight not found |
|
| 404 | Highlight not found |
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ This endpoint requires device authentication (not user JWT). This is a Kobo comp
|
|||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| -------- | ------ | -------- | ----------------------------------- |
|
||||||
| (varies) | object | No | Kobo analytics data (format varies) |
|
| (varies) | object | No | Kobo analytics data (format varies) |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -33,9 +33,9 @@ This endpoint requires device authentication (not user JWT). This is a Kobo comp
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ---------------------------- |
|
||||||
| 401 | Device authentication failed |
|
| 401 | Device authentication failed |
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
|||||||
@@ -11,19 +11,19 @@ This endpoint requires device authentication (not user JWT). Kobo devices authen
|
|||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| --------- | ----- | -------- | ------------------------- |
|
||||||
| bookmarks | array | Yes | Array of bookmark objects |
|
| bookmarks | array | Yes | Array of bookmark objects |
|
||||||
|
|
||||||
### Bookmark Object
|
### Bookmark Object
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------- | -------- | ------------------ |
|
||||||
| BookmarkID | string | Yes | Unique bookmark ID |
|
| BookmarkID | string | Yes | Unique bookmark ID |
|
||||||
| ContentID | string | Yes | Book content ID |
|
| ContentID | string | Yes | Book content ID |
|
||||||
| StartPosition | integer | Yes | Bookmark position |
|
| StartPosition | integer | Yes | Bookmark position |
|
||||||
| Text | string | No | Bookmark text |
|
| Text | string | No | Bookmark text |
|
||||||
| DateCreated | string | Yes | ISO 8601 timestamp |
|
| DateCreated | string | Yes | ISO 8601 timestamp |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -52,8 +52,8 @@ This endpoint requires device authentication (not user JWT). Kobo devices authen
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ---------------------------- |
|
||||||
| 401 | Device authentication failed |
|
| 401 | Device authentication failed |
|
||||||
| 400 | Invalid request data |
|
| 400 | Invalid request data |
|
||||||
| 404 | Device or book not found |
|
| 404 | Device or book not found |
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ This endpoint requires device authentication (not user JWT). Kobo devices authen
|
|||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| -------------- | ------ | -------- | ---------------------------- |
|
||||||
| X-Device-ID | string | Yes | Device UUID |
|
| X-Device-ID | string | Yes | Device UUID |
|
||||||
| X-Device-Key | string | Yes | Device authentication key |
|
| X-Device-Key | string | Yes | Device authentication key |
|
||||||
| X-Kobo-UserKey | string | No | Kobo user key (if available) |
|
| X-Kobo-UserKey | string | No | Kobo user key (if available) |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -39,10 +39,10 @@ X-Device-Key: device-auth-key
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ---------------------------- |
|
||||||
| 401 | Device authentication failed |
|
| 401 | Device authentication failed |
|
||||||
| 404 | Device not found |
|
| 404 | Device not found |
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
|||||||
@@ -11,23 +11,23 @@ This endpoint requires device authentication (not user JWT). Kobo devices authen
|
|||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| --------- | ----- | -------- | -------------------------------- |
|
||||||
| bookmarks | array | Yes | Array of bookmark/markup objects |
|
| bookmarks | array | Yes | Array of bookmark/markup objects |
|
||||||
|
|
||||||
### Bookmark Object
|
### Bookmark Object
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------- | -------- | -------------------------- |
|
||||||
| BookmarkID | string | Yes | Unique bookmark ID |
|
| BookmarkID | string | Yes | Unique bookmark ID |
|
||||||
| ContentID | string | Yes | Book content ID |
|
| ContentID | string | Yes | Book content ID |
|
||||||
| StartPosition | integer | Yes | Highlight start position |
|
| StartPosition | integer | Yes | Highlight start position |
|
||||||
| EndPosition | integer | No | Highlight end position |
|
| EndPosition | integer | No | Highlight end position |
|
||||||
| Text | string | No | Highlighted text |
|
| Text | string | No | Highlighted text |
|
||||||
| Annotation | string | No | User annotation |
|
| Annotation | string | No | User annotation |
|
||||||
| DateCreated | string | Yes | ISO 8601 timestamp |
|
| DateCreated | string | Yes | ISO 8601 timestamp |
|
||||||
| Chapter | string | No | Chapter title |
|
| Chapter | string | No | Chapter title |
|
||||||
| Hidden | boolean | No | Whether bookmark is hidden |
|
| Hidden | boolean | No | Whether bookmark is hidden |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -60,8 +60,8 @@ This endpoint requires device authentication (not user JWT). Kobo devices authen
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ---------------------------- |
|
||||||
| 401 | Device authentication failed |
|
| 401 | Device authentication failed |
|
||||||
| 400 | Invalid request data |
|
| 400 | Invalid request data |
|
||||||
| 404 | Device or book not found |
|
| 404 | Device or book not found |
|
||||||
|
|||||||
@@ -11,19 +11,16 @@ This endpoint requires device authentication (not user JWT). Kobo devices authen
|
|||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| --------- | --------------- | -------- | --------------------------------------------- |
|
||||||
| book_ids | array of string | No | Array of ContentIDs to sync |
|
| book_ids | array of string | No | Array of ContentIDs to sync |
|
||||||
| full_sync | boolean | No | Whether to perform full sync (default: false) |
|
| full_sync | boolean | No | Whether to perform full sync (default: false) |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"book_ids": [
|
"book_ids": ["content-id-1", "content-id-2"],
|
||||||
"content-id-1",
|
|
||||||
"content-id-2"
|
|
||||||
],
|
|
||||||
"full_sync": false
|
"full_sync": false
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -53,10 +50,10 @@ This endpoint requires device authentication (not user JWT). Kobo devices authen
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ---------------------------- |
|
||||||
| 401 | Device authentication failed |
|
| 401 | Device authentication failed |
|
||||||
| 404 | Device not found |
|
| 404 | Device not found |
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ This endpoint requires device authentication (not user JWT). Devices authenticat
|
|||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------ | ------ | -------- | ------------------------- |
|
||||||
| X-Device-ID | string | Yes | Device UUID |
|
| X-Device-ID | string | Yes | Device UUID |
|
||||||
| X-Device-Key | string | Yes | Device authentication key |
|
| X-Device-Key | string | Yes | Device authentication key |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ X-Device-Key: device-auth-key
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ---------------------------- |
|
||||||
| 401 | Device authentication failed |
|
| 401 | Device authentication failed |
|
||||||
| 404 | Device not found |
|
| 404 | Device not found |
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ Get metadata for a book from KOReader device.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | ----------- |
|
||||||
| uuid | string (UUID) | Yes | Book UUID |
|
| uuid | string (UUID) | Yes | Book UUID |
|
||||||
|
|
||||||
## Device Authentication
|
## Device Authentication
|
||||||
|
|
||||||
@@ -17,10 +17,10 @@ This endpoint requires device authentication (not user JWT). Devices authenticat
|
|||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------ | ------ | -------- | ------------------------- |
|
||||||
| X-Device-ID | string | Yes | Device UUID |
|
| X-Device-ID | string | Yes | Device UUID |
|
||||||
| X-Device-Key | string | Yes | Device authentication key |
|
| X-Device-Key | string | Yes | Device authentication key |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ X-Device-Key: device-auth-key
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ---------------------------- |
|
||||||
| 401 | Device authentication failed |
|
| 401 | Device authentication failed |
|
||||||
| 404 | Book or device not found |
|
| 404 | Book or device not found |
|
||||||
|
|||||||
@@ -11,23 +11,23 @@ This endpoint requires device authentication (not user JWT). Devices authenticat
|
|||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| --------- | ------------- | -------- | ------------------------- |
|
||||||
| device_id | string (UUID) | Yes | Device UUID |
|
| device_id | string (UUID) | Yes | Device UUID |
|
||||||
| bookmarks | array | Yes | Array of bookmark objects |
|
| bookmarks | array | Yes | Array of bookmark objects |
|
||||||
|
|
||||||
### Bookmark Object
|
### Bookmark Object
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ---------------- | ------- | -------- | -------------------------- |
|
||||||
| book | string | Yes | Book identifier |
|
| book | string | Yes | Book identifier |
|
||||||
| chapter | string | No | Chapter title |
|
| chapter | string | No | Chapter title |
|
||||||
| page | integer | No | Page number |
|
| page | integer | No | Page number |
|
||||||
| position | float | Yes | Position in document (0-1) |
|
| position | float | Yes | Position in document (0-1) |
|
||||||
| notes | string | No | Bookmark notes |
|
| notes | string | No | Bookmark notes |
|
||||||
| highlighted_text | string | No | Highlighted text |
|
| highlighted_text | string | No | Highlighted text |
|
||||||
| time | string | Yes | ISO 8601 timestamp |
|
| time | string | Yes | ISO 8601 timestamp |
|
||||||
| created_at | string | Yes | ISO 8601 timestamp |
|
| created_at | string | Yes | ISO 8601 timestamp |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -60,8 +60,8 @@ This endpoint requires device authentication (not user JWT). Devices authenticat
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ---------------------------- |
|
||||||
| 401 | Device authentication failed |
|
| 401 | Device authentication failed |
|
||||||
| 400 | Invalid request data |
|
| 400 | Invalid request data |
|
||||||
| 404 | Device not found |
|
| 404 | Device not found |
|
||||||
|
|||||||
@@ -11,21 +11,21 @@ This endpoint requires device authentication (not user JWT). Devices authenticat
|
|||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| --------- | ------------- | -------- | ------------------------- |
|
||||||
| device_id | string (UUID) | Yes | Device UUID |
|
| device_id | string (UUID) | Yes | Device UUID |
|
||||||
| progress | array | Yes | Array of progress objects |
|
| progress | array | Yes | Array of progress objects |
|
||||||
|
|
||||||
### Progress Object
|
### Progress Object
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ----------- | ------- | -------- | ---------------------------------- |
|
||||||
| book | string | Yes | Book identifier (filename or UUID) |
|
| book | string | Yes | Book identifier (filename or UUID) |
|
||||||
| percent | float | Yes | Progress percentage (0-100) |
|
| percent | float | Yes | Progress percentage (0-100) |
|
||||||
| page | integer | No | Current page number |
|
| page | integer | No | Current page number |
|
||||||
| total_pages | integer | No | Total pages in document |
|
| total_pages | integer | No | Total pages in document |
|
||||||
| date_read | string | No | ISO 8601 timestamp of last read |
|
| date_read | string | No | ISO 8601 timestamp of last read |
|
||||||
| updated_at | string | Yes | ISO 8601 timestamp |
|
| updated_at | string | Yes | ISO 8601 timestamp |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -56,8 +56,8 @@ This endpoint requires device authentication (not user JWT). Devices authenticat
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ---------------------------- |
|
||||||
| 401 | Device authentication failed |
|
| 401 | Device authentication failed |
|
||||||
| 400 | Invalid request data |
|
| 400 | Invalid request data |
|
||||||
| 404 | Device not found |
|
| 404 | Device not found |
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ Add a folder to an existing library (Admin only).
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| ---------- | ------ | -------- | ------------ |
|
||||||
| library_id | string | Yes | Library UUID |
|
| library_id | string | Yes | Library UUID |
|
||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ----------- | ------ | -------- | ----------------------- |
|
||||||
| folder_path | string | Yes | Absolute path to folder |
|
| folder_path | string | Yes | Absolute path to folder |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -39,9 +39,9 @@ Add a folder to an existing library (Admin only).
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------ |
|
||||||
| 400 | Invalid folder path |
|
| 400 | Invalid folder path |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User is not an admin |
|
| 403 | User is not an admin |
|
||||||
| 404 | Library not found |
|
| 404 | Library not found |
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ Browse server directories for folder selection in library management.
|
|||||||
|
|
||||||
## Query Parameters
|
## Query Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------ | -------- | --------------------------------------- |
|
||||||
| path | string | No | Directory path to browse (default: "/") |
|
| path | string | No | Directory path to browse (default: "/") |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ------------------------- |
|
||||||
| Authorization | string | Yes | Bearer token (admin only) |
|
| Authorization | string | Yes | Bearer token (admin only) |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -36,13 +36,13 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | -------------------------------------- |
|
||||||
| 400 | Path traversal attempt or invalid path |
|
| 400 | Path traversal attempt or invalid path |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User is not an admin |
|
| 403 | User is not an admin |
|
||||||
| 400 | Path does not exist |
|
| 400 | Path does not exist |
|
||||||
| 400 | Path is not a directory |
|
| 400 | Path is not a directory |
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ Create a new library (Admin only).
|
|||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ----------- | ------ | -------- | ----------------------------------------------------- |
|
||||||
| name | string | Yes | Library name |
|
| name | string | Yes | Library name |
|
||||||
| description | string | No | Library description |
|
| description | string | No | Library description |
|
||||||
| type | string | Yes | Library type (e.g., "ebooks", "comics", "audiobooks") |
|
| type | string | Yes | Library type (e.g., "ebooks", "comics", "audiobooks") |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -38,8 +38,8 @@ Create a new library (Admin only).
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------ |
|
||||||
| 400 | Invalid input data |
|
| 400 | Invalid input data |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User is not an admin |
|
| 403 | User is not an admin |
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ Delete a library and all associated data.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | ------------ |
|
||||||
| id | string (UUID) | Yes | Library UUID |
|
| id | string (UUID) | Yes | Library UUID |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ----------------------------------- |
|
||||||
| Authorization | string | Yes | Bearer token (must have admin role) |
|
| Authorization | string | Yes | Bearer token (must have admin role) |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -30,8 +30,8 @@ Library deleted successfully.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ----------------------------------- |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User does not have admin privileges |
|
| 403 | User does not have admin privileges |
|
||||||
| 404 | Library not found |
|
| 404 | Library not found |
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ Delete a folder from a library.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | ------------ |
|
||||||
| id | string (UUID) | Yes | Library UUID |
|
| id | string (UUID) | Yes | Library UUID |
|
||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ----------- | ------ | -------- | ------------------------------------- |
|
||||||
| folder_path | string | Yes | Absolute path to the folder to delete |
|
| folder_path | string | Yes | Absolute path to the folder to delete |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -32,9 +32,9 @@ Library folder deleted successfully.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ----------------------------------- |
|
||||||
| 400 | Invalid request data |
|
| 400 | Invalid request data |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User does not have admin privileges |
|
| 403 | User does not have admin privileges |
|
||||||
| 404 | Library or folder not found |
|
| 404 | Library or folder not found |
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ Retrieve details of a specific library.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| ---------- | ------ | -------- | ------------ |
|
||||||
| library_id | string | Yes | Library UUID |
|
| library_id | string | Yes | Library UUID |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ------------ |
|
||||||
| Authorization | string | Yes | Bearer token |
|
| Authorization | string | Yes | Bearer token |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -44,8 +44,8 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ----------------------------------------- |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User does not have access to this library |
|
| 403 | User does not have access to this library |
|
||||||
| 404 | Library not found |
|
| 404 | Library not found |
|
||||||
|
|||||||
@@ -7,24 +7,24 @@ Get all media items in a specific library.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | ------------ |
|
||||||
| id | string (UUID) | Yes | Library UUID |
|
| id | string (UUID) | Yes | Library UUID |
|
||||||
|
|
||||||
## Query Parameters
|
## Query Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| ---------- | ------- | -------- | ----------------------------------------------- |
|
||||||
| limit | integer | No | Maximum number of items to return (default: 50) |
|
| limit | integer | No | Maximum number of items to return (default: 50) |
|
||||||
| offset | integer | No | Number of items to skip (default: 0) |
|
| offset | integer | No | Number of items to skip (default: 0) |
|
||||||
| sort_by | string | No | Sort field (title, created_at, etc.) |
|
| sort_by | string | No | Sort field (title, created_at, etc.) |
|
||||||
| sort_order | string | No | Sort order (asc, desc) |
|
| sort_order | string | No | Sort order (asc, desc) |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ----------------------------------- |
|
||||||
| Authorization | string | Yes | Bearer token (must have admin role) |
|
| Authorization | string | Yes | Bearer token (must have admin role) |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -54,8 +54,8 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ----------------------------------- |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User does not have admin privileges |
|
| 403 | User does not have admin privileges |
|
||||||
| 404 | Library not found |
|
| 404 | Library not found |
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ Get statistics for a specific library.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | ------------ |
|
||||||
| id | string (UUID) | Yes | Library UUID |
|
| id | string (UUID) | Yes | Library UUID |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ----------------------------------- |
|
||||||
| Authorization | string | Yes | Bearer token (must have admin role) |
|
| Authorization | string | Yes | Bearer token (must have admin role) |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -43,8 +43,8 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ----------------------------------- |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User does not have admin privileges |
|
| 403 | User does not have admin privileges |
|
||||||
| 404 | Library not found |
|
| 404 | Library not found |
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ Retrieve all libraries visible to the current user.
|
|||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ------------ |
|
||||||
| Authorization | string | Yes | Bearer token |
|
| Authorization | string | Yes | Bearer token |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -36,6 +36,6 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------ |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ Set library visibility for a specific user (Admin only).
|
|||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ---------- | ------- | -------- | ---------------------------------- |
|
||||||
| user_id | string | Yes | User UUID |
|
| user_id | string | Yes | User UUID |
|
||||||
| library_id | string | Yes | Library UUID |
|
| library_id | string | Yes | Library UUID |
|
||||||
| is_visible | boolean | Yes | Whether library is visible to user |
|
| is_visible | boolean | Yes | Whether library is visible to user |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -36,9 +36,9 @@ Set library visibility for a specific user (Admin only).
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------- |
|
||||||
| 400 | Invalid input data |
|
| 400 | Invalid input data |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User is not an admin |
|
| 403 | User is not an admin |
|
||||||
| 404 | User or library not found |
|
| 404 | User or library not found |
|
||||||
|
|||||||
@@ -8,16 +8,16 @@ Update a library's information.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------------- | -------- | ------------ |
|
||||||
| id | string (UUID) | Yes | Library UUID |
|
| id | string (UUID) | Yes | Library UUID |
|
||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| --------------- | ------- | -------- | --------------- |
|
||||||
| name | string | No | Library name |
|
| name | string | No | Library name |
|
||||||
| library_type_id | integer | No | Library type ID |
|
| library_type_id | integer | No | Library type ID |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -42,9 +42,9 @@ Update a library's information.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ----------------------------------- |
|
||||||
| 400 | Invalid request data |
|
| 400 | Invalid request data |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User does not have admin privileges |
|
| 403 | User does not have admin privileges |
|
||||||
| 404 | Library not found |
|
| 404 | Library not found |
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ Delete multiple media items at once (supports ebooks, comics, manga).
|
|||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| -------------- | ------------- | -------- | ----------------------------------- |
|
||||||
| media_item_ids | array of UUID | Yes | Array of media item UUIDs to delete |
|
| media_item_ids | array of UUID | Yes | Array of media item UUIDs to delete |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -51,24 +51,24 @@ Delete multiple media items at once (supports ebooks, comics, manga).
|
|||||||
|
|
||||||
## Response Fields
|
## Response Fields
|
||||||
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
|-------|------|-------------|
|
| ----------------------- | ------ | ------------------------------------------------- |
|
||||||
| results | array | Individual result for each media item |
|
| results | array | Individual result for each media item |
|
||||||
| results[].media_item_id | string | UUID of the media item |
|
| results[].media_item_id | string | UUID of the media item |
|
||||||
| results[].status | string | "success" or "error" |
|
| results[].status | string | "success" or "error" |
|
||||||
| results[].error | string | Error message (only present if status is "error") |
|
| results[].error | string | Error message (only present if status is "error") |
|
||||||
| total | number | Total number of media items processed |
|
| total | number | Total number of media items processed |
|
||||||
| deleted | number | Number of media items successfully deleted |
|
| deleted | number | Number of media items successfully deleted |
|
||||||
| failed | number | Number of media items that failed to delete |
|
| failed | number | Number of media items that failed to delete |
|
||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | -------------------------------------------------- |
|
||||||
| 400 | Invalid request data or empty media_item_ids array |
|
| 400 | Invalid request data or empty media_item_ids array |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User does not have permission |
|
| 403 | User does not have permission |
|
||||||
| 500 | Server error during deletion |
|
| 500 | Server error during deletion |
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
|||||||
@@ -8,21 +8,21 @@ Update multiple media items at once (supports ebooks, comics, manga).
|
|||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ---------------------------------- | ---------------- | -------- | -------------------------- |
|
||||||
| media_item_updates | array of objects | Yes | Array of update operations |
|
| media_item_updates | array of objects | Yes | Array of update operations |
|
||||||
| media_item_updates[].media_item_id | string (UUID) | Yes | Media item UUID to update |
|
| media_item_updates[].media_item_id | string (UUID) | Yes | Media item UUID to update |
|
||||||
| media_item_updates[].updates | object | Yes | Fields to update |
|
| media_item_updates[].updates | object | Yes | Fields to update |
|
||||||
|
|
||||||
### Update Fields
|
### Update Fields
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| -------- | ---------------- | -------- | --------------------------------- |
|
||||||
| title | string | No | Updated title |
|
| title | string | No | Updated title |
|
||||||
| author | string | No | Updated author |
|
| author | string | No | Updated author |
|
||||||
| genre | string | No | Updated genre |
|
| genre | string | No | Updated genre |
|
||||||
| language | string | No | Updated language (ISO 639-1 code) |
|
| language | string | No | Updated language (ISO 639-1 code) |
|
||||||
| tags | array of strings | No | Updated tags (auto-normalized) |
|
| tags | array of strings | No | Updated tags (auto-normalized) |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -71,15 +71,15 @@ Update multiple media items at once (supports ebooks, comics, manga).
|
|||||||
|
|
||||||
## Response Fields
|
## Response Fields
|
||||||
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
|-------|------|-------------|
|
| ----------------------- | ------ | ------------------------------------------------- |
|
||||||
| results | array | Individual result for each media item |
|
| results | array | Individual result for each media item |
|
||||||
| results[].media_item_id | string | UUID of the media item |
|
| results[].media_item_id | string | UUID of the media item |
|
||||||
| results[].status | string | "success" or "error" |
|
| results[].status | string | "success" or "error" |
|
||||||
| results[].error | string | Error message (only present if status is "error") |
|
| results[].error | string | Error message (only present if status is "error") |
|
||||||
| total | number | Total number of media items processed |
|
| total | number | Total number of media items processed |
|
||||||
| updated | number | Number of media items successfully updated |
|
| updated | number | Number of media items successfully updated |
|
||||||
| failed | number | Number of media items that failed to update |
|
| failed | number | Number of media items that failed to update |
|
||||||
|
|
||||||
## Tag and Contributor Normalization
|
## Tag and Contributor Normalization
|
||||||
|
|
||||||
@@ -90,13 +90,13 @@ The backend automatically normalizes tags:
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------------------------------------ |
|
||||||
| 400 | Invalid request data or empty media_item_updates array |
|
| 400 | Invalid request data or empty media_item_updates array |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User does not have permission |
|
| 403 | User does not have permission |
|
||||||
| 404 | One or more media items not found |
|
| 404 | One or more media items not found |
|
||||||
| 500 | Server error during update |
|
| 500 | Server error during update |
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
|||||||
@@ -18,24 +18,24 @@ See [Library API documentation](../libraries/) for more details.
|
|||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ---------------- | ---------------- | -------- | ------------------------------------- |
|
||||||
| library_id | string (UUID) | Yes | Library UUID to add the media item to |
|
| library_id | string (UUID) | Yes | Library UUID to add the media item to |
|
||||||
| title | string | Yes | Media item title (1-500 characters) |
|
| title | string | Yes | Media item title (1-500 characters) |
|
||||||
| author | string | No | Author name |
|
| author | string | No | Author name |
|
||||||
| isbn | string | No | ISBN number |
|
| isbn | string | No | ISBN number |
|
||||||
| description | string | No | Description or summary |
|
| description | string | No | Description or summary |
|
||||||
| file_path | string | Yes | Path to the media file |
|
| file_path | string | Yes | Path to the media file |
|
||||||
| file_size | integer | Yes | Size of the file in bytes |
|
| file_size | integer | Yes | Size of the file in bytes |
|
||||||
| mime_type | string | Yes | MIME type of the file |
|
| mime_type | string | Yes | MIME type of the file |
|
||||||
| cover_image_path | string | No | Path to the cover image |
|
| cover_image_path | string | No | Path to the cover image |
|
||||||
| series | string | No | Series name |
|
| series | string | No | Series name |
|
||||||
| series_number | integer | No | Number in the series |
|
| series_number | integer | No | Number in the series |
|
||||||
| tags | array of strings | No | Tags (auto-normalized) |
|
| tags | array of strings | No | Tags (auto-normalized) |
|
||||||
| asin | string | No | Amazon ASIN |
|
| asin | string | No | Amazon ASIN |
|
||||||
| date_published | string | No | Publication date |
|
| date_published | string | No | Publication date |
|
||||||
| publisher | string | No | Publisher name |
|
| publisher | string | No | Publisher name |
|
||||||
| contributors | array of strings | No | Contributors (auto-normalized) |
|
| contributors | array of strings | No | Contributors (auto-normalized) |
|
||||||
|
|
||||||
## Tag/Contributor Normalization
|
## Tag/Contributor Normalization
|
||||||
|
|
||||||
@@ -92,12 +92,12 @@ Tags and contributors are automatically normalized:
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ---------------------------------------------- |
|
||||||
| 400 | Invalid request data OR library has no folders |
|
| 400 | Invalid request data OR library has no folders |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User is not an admin |
|
| 403 | User is not an admin |
|
||||||
| 404 | Library not found |
|
| 404 | Library not found |
|
||||||
|
|
||||||
### 400 - Library Has No Folders
|
### 400 - Library Has No Folders
|
||||||
|
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ Delete a media item from the library (Admin only).
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------ | -------- | --------------- |
|
||||||
| media_id | string | Yes | Media item UUID |
|
| media_id | string | Yes | Media item UUID |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ------------ |
|
||||||
| Authorization | string | Yes | Bearer token |
|
| Authorization | string | Yes | Bearer token |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -30,8 +30,8 @@ Media item deleted successfully.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------ |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User is not an admin |
|
| 403 | User is not an admin |
|
||||||
| 404 | Media item not found |
|
| 404 | Media item not found |
|
||||||
|
|||||||
@@ -8,24 +8,25 @@ Download a media item file (EPUB, PDF, etc.) from the Bookhoard server.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------ | -------- | --------------- |
|
||||||
| uuid | string | Yes | Media item UUID |
|
| uuid | string | Yes | Media item UUID |
|
||||||
|
|
||||||
## Response
|
## Response
|
||||||
|
|
||||||
**Success (200 OK)**: Binary file data
|
**Success (200 OK)**: Binary file data
|
||||||
|
|
||||||
**Response Headers**:
|
**Response Headers**:
|
||||||
|
|
||||||
- `Content-Type`: `application/epub+zip`, `application/pdf`, or appropriate MIME type
|
- `Content-Type`: `application/epub+zip`, `application/pdf`, or appropriate MIME type
|
||||||
- `Content-Disposition`: `attachment; filename="filename.epub"`
|
- `Content-Disposition`: `attachment; filename="filename.epub"`
|
||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | --------------------------------- |
|
||||||
| 404 | Media item not found |
|
| 404 | Media item not found |
|
||||||
| 500 | Server error during file download |
|
| 500 | Server error during file download |
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
|
|||||||
@@ -8,18 +8,18 @@ Filter and sort media items with advanced criteria.
|
|||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------- | -------- | ----------------------------------------------------------- |
|
||||||
| library_id | string | Yes | Library UUID |
|
| library_id | string | Yes | Library UUID |
|
||||||
| author_filter | string | No | Filter by author name |
|
| author_filter | string | No | Filter by author name |
|
||||||
| series_filter | string | No | Filter by series name |
|
| series_filter | string | No | Filter by series name |
|
||||||
| genre_filter | string | No | Filter by genre |
|
| genre_filter | string | No | Filter by genre |
|
||||||
| year_min | integer | No | Minimum copyright year |
|
| year_min | integer | No | Minimum copyright year |
|
||||||
| year_max | integer | No | Maximum copyright year |
|
| year_max | integer | No | Maximum copyright year |
|
||||||
| has_cover | boolean | No | Filter by cover image existence |
|
| has_cover | boolean | No | Filter by cover image existence |
|
||||||
| sort | string | No | Sort field and order (e.g., "title ASC", "created_at DESC") |
|
| sort | string | No | Sort field and order (e.g., "title ASC", "created_at DESC") |
|
||||||
| limit | integer | No | Number of results (default 20) |
|
| limit | integer | No | Number of results (default 20) |
|
||||||
| offset | integer | No | Number to skip |
|
| offset | integer | No | Number to skip |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -56,8 +56,8 @@ Filter and sort media items with advanced criteria.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ----------------------------------------- |
|
||||||
| 400 | Invalid filter parameters |
|
| 400 | Invalid filter parameters |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User does not have access to this library |
|
| 403 | User does not have access to this library |
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ Retrieve details of a specific media item.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------ | -------- | --------------- |
|
||||||
| media_id | string | Yes | Media item UUID |
|
| media_id | string | Yes | Media item UUID |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ------------ |
|
||||||
| Authorization | string | Yes | Bearer token |
|
| Authorization | string | Yes | Bearer token |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -37,13 +37,13 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|||||||
"file_size": 1024000,
|
"file_size": 1024000,
|
||||||
"mime_type": "application/epub+zip",
|
"mime_type": "application/epub+zip",
|
||||||
"cover_image_path": "/path/to/cover.jpg",
|
"cover_image_path": "/path/to/cover.jpg",
|
||||||
"series": "Series Name",
|
"series": "Series Name",
|
||||||
"series_number": 1,
|
"series_number": 1,
|
||||||
"tags": ["sci-fi", "space opera"],
|
"tags": ["sci-fi", "space opera"],
|
||||||
"tags_search": ["sci fi", "space opera"],
|
"tags_search": ["sci fi", "space opera"],
|
||||||
"contributors": ["Author Name", "ACME CORP."],
|
"contributors": ["Author Name", "ACME CORP."],
|
||||||
"contributors_search": ["author name", "acme corp"],
|
"contributors_search": ["author name", "acme corp"],
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"page_count": 350,
|
"page_count": 350,
|
||||||
"genre": "Science Fiction",
|
"genre": "Science Fiction",
|
||||||
"copyright_year": 2023,
|
"copyright_year": 2023,
|
||||||
@@ -53,8 +53,8 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | -------------------------------------------- |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User does not have access to this media item |
|
| 403 | User does not have access to this media item |
|
||||||
| 404 | Media item not found |
|
| 404 | Media item not found |
|
||||||
|
|||||||
@@ -7,17 +7,17 @@ Retrieve a paginated list of media items from a library.
|
|||||||
|
|
||||||
## Query Parameters
|
## Query Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| ---------- | ------- | -------- | ----------------------------------------------- |
|
||||||
| library_id | string | Yes | Library UUID |
|
| library_id | string | Yes | Library UUID |
|
||||||
| limit | integer | No | Number of items to return (max 100, default 20) |
|
| limit | integer | No | Number of items to return (max 100, default 20) |
|
||||||
| offset | integer | No | Number of items to skip |
|
| offset | integer | No | Number of items to skip |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ------------ |
|
||||||
| Authorization | string | Yes | Bearer token |
|
| Authorization | string | Yes | Bearer token |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -41,13 +41,13 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|||||||
"file_size": 1024000,
|
"file_size": 1024000,
|
||||||
"mime_type": "application/epub+zip",
|
"mime_type": "application/epub+zip",
|
||||||
"cover_image_path": "/path/to/cover.jpg",
|
"cover_image_path": "/path/to/cover.jpg",
|
||||||
"series": "Series Name",
|
"series": "Series Name",
|
||||||
"series_number": 1,
|
"series_number": 1,
|
||||||
"tags": ["sci-fi", "space opera"],
|
"tags": ["sci-fi", "space opera"],
|
||||||
"tags_search": ["sci fi", "space opera"],
|
"tags_search": ["sci fi", "space opera"],
|
||||||
"contributors": ["Author Name", "ACME CORP."],
|
"contributors": ["Author Name", "ACME CORP."],
|
||||||
"contributors_search": ["author name", "acme corp"],
|
"contributors_search": ["author name", "acme corp"],
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"page_count": 350,
|
"page_count": 350,
|
||||||
"genre": "Science Fiction",
|
"genre": "Science Fiction",
|
||||||
"copyright_year": 2023,
|
"copyright_year": 2023,
|
||||||
@@ -60,8 +60,8 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ----------------------------------------- |
|
||||||
| 400 | Invalid query parameters |
|
| 400 | Invalid query parameters |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User does not have access to this library |
|
| 403 | User does not have access to this library |
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Search for media items by title, author, series, tags, or contributors.
|
|||||||
**Note:** Search is case-insensitive and punctuation-agnostic. The search query is matched against normalized tags_search and contributors_search fields, allowing users to find matches regardless of casing or punctuation.
|
**Note:** Search is case-insensitive and punctuation-agnostic. The search query is matched against normalized tags_search and contributors_search fields, allowing users to find matches regardless of casing or punctuation.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
- Search "acme corp" finds items with "ACME CORP." or "Acme Corp"
|
- Search "acme corp" finds items with "ACME CORP." or "Acme Corp"
|
||||||
- Search "oreilly" finds items with "O'Reilly Media" or "OReilly Media"
|
- Search "oreilly" finds items with "O'Reilly Media" or "OReilly Media"
|
||||||
- Search "science fiction" finds items with "Science-Fiction" or "science-fiction"
|
- Search "science fiction" finds items with "Science-Fiction" or "science-fiction"
|
||||||
@@ -14,17 +15,17 @@ Examples:
|
|||||||
|
|
||||||
## Query Parameters
|
## Query Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------- | -------- | ----------------------------------- |
|
||||||
| q | string | Yes | Search query (minimum 2 characters) |
|
| q | string | Yes | Search query (minimum 2 characters) |
|
||||||
| limit | integer | No | Number of results (default 20) |
|
| limit | integer | No | Number of results (default 20) |
|
||||||
| offset | integer | No | Number to skip |
|
| offset | integer | No | Number to skip |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ------------ |
|
||||||
| Authorization | string | Yes | Bearer token |
|
| Authorization | string | Yes | Bearer token |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | -------------------------------- |
|
||||||
| 400 | Invalid search query (too short) |
|
| 400 | Invalid search query (too short) |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
|
|||||||
@@ -8,27 +8,28 @@ Update media item metadata (Admin only).
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------ | -------- | --------------- |
|
||||||
| media_id | string | Yes | Media item UUID |
|
| media_id | string | Yes | Media item UUID |
|
||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | --------------- | -------- | -------------------------------------- |
|
||||||
| title | string | No | Updated title |
|
| title | string | No | Updated title |
|
||||||
| author | string | No | Updated author |
|
| author | string | No | Updated author |
|
||||||
| description | string | No | Updated description |
|
| description | string | No | Updated description |
|
||||||
| series | string | No | Series name |
|
| series | string | No | Series name |
|
||||||
| series_number | integer | No | Number in series |
|
| series_number | integer | No | Number in series |
|
||||||
| tags | array of string | No | Updated tags (auto-normalized) |
|
| tags | array of string | No | Updated tags (auto-normalized) |
|
||||||
| contributors | array of string | No | Updated contributors (auto-normalized) |
|
| contributors | array of string | No | Updated contributors (auto-normalized) |
|
||||||
|
|
||||||
**Tag/Contributor Normalization:**
|
**Tag/Contributor Normalization:**
|
||||||
- Tags are titlecased and deduplicated (case-insensitive)
|
|
||||||
- Contributors preserve original casing and punctuation
|
- Tags are titlecased and deduplicated (case-insensitive)
|
||||||
- Punctuation-preferred deduplication (keeps "ACME CORP." over "acme corp")
|
- Contributors preserve original casing and punctuation
|
||||||
- Search fields auto-generated for case-insensitive search
|
- Punctuation-preferred deduplication (keeps "ACME CORP." over "acme corp")
|
||||||
|
- Search fields auto-generated for case-insensitive search
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -58,9 +59,9 @@ Update media item metadata (Admin only).
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------ |
|
||||||
| 400 | Invalid input data |
|
| 400 | Invalid input data |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User is not an admin |
|
| 403 | User is not an admin |
|
||||||
| 404 | Media item not found |
|
| 404 | Media item not found |
|
||||||
|
|||||||
@@ -8,18 +8,18 @@ Create a new note for a media item.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------ | -------- | --------------- |
|
||||||
| media_id | string | Yes | Media item UUID |
|
| media_id | string | Yes | Media item UUID |
|
||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------------- | ------ | -------- | ---------------------------------- |
|
||||||
| content | string | Yes | Note content |
|
| content | string | Yes | Note content |
|
||||||
| position | string | No | Location reference (e.g., epubcfi) |
|
| position | string | No | Location reference (e.g., epubcfi) |
|
||||||
| percentage_location | float | No | Location as percentage (0-1) |
|
| percentage_location | float | No | Location as percentage (0-1) |
|
||||||
| epubcfi_location | string | No | EPUB CFI location |
|
| epubcfi_location | string | No | EPUB CFI location |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -49,8 +49,8 @@ Create a new note for a media item.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------ |
|
||||||
| 400 | Invalid note data |
|
| 400 | Invalid note data |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 404 | Media item not found |
|
| 404 | Media item not found |
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ Delete a note.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------ | -------- | ----------- |
|
||||||
| note_id | string | Yes | Note UUID |
|
| note_id | string | Yes | Note UUID |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ------------ |
|
||||||
| Authorization | string | Yes | Bearer token |
|
| Authorization | string | Yes | Bearer token |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -30,8 +30,8 @@ Note deleted successfully.
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | --------------------------- |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 403 | User does not own this note |
|
| 403 | User does not own this note |
|
||||||
| 404 | Note not found |
|
| 404 | Note not found |
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ Retrieve all notes for a specific media item.
|
|||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|-----------|-------------|
|
| --------- | ------ | -------- | --------------- |
|
||||||
| media_id | string | Yes | Media item UUID |
|
| media_id | string | Yes | Media item UUID |
|
||||||
|
|
||||||
## Request Headers
|
## Request Headers
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
| Header | Type | Required | Description |
|
||||||
|--------|------|-----------|-------------|
|
| ------------- | ------ | -------- | ------------ |
|
||||||
| Authorization | string | Yes | Bearer token |
|
| Authorization | string | Yes | Bearer token |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|-------------|
|
| ---- | ------------------------ |
|
||||||
| 401 | Invalid or expired token |
|
| 401 | Invalid or expired token |
|
||||||
| 404 | Media item not found |
|
| 404 | Media item not found |
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user