- Add internal/sync package with format detection - FormatGroup types: reflowable, fixed_layout, comic_archive - DetectFormatGroup() function based on mimetype and file extension - MimeType mappings for common ebook formats - Progress conversion engine with: - ConvertProgress() between format groups - Extract percentage from various progress formats - PageToPercentage / PercentageToPage helpers - CharacterToPercentage / PercentageToCharacter helpers - MergeProgress() with 'max progress wins' strategy - FormatProgressForDisplay() for UI rendering - Add sqlc queries for format detection and progress updates - BulkUpdateFormatGroups query for auto-format detection - GetUniversalProgress query with all location references - UpdateUniversalProgress query with device sync metadata - ReadingHistory queries for session tracking
2167 lines
55 KiB
Markdown
2167 lines
55 KiB
Markdown
# Bookmann Universal Cross-Platform Sync Implementation Guide
|
|
|
|
## Executive Summary
|
|
|
|
This document provides a complete technical specification for implementing a universal cross-platform reading progress synchronization system that rivals the Amazon Kindle ecosystem. The system enables seamless reading progress, highlights, and notes synchronization across web interface, KOReader, Kobo devices, and future platforms - all self-hosted and open-source.
|
|
|
|
**Vision**: Users can read on any device, put it down, pick up any other device, and continue exactly where they left off with all annotations synchronized.
|
|
|
|
**Core Philosophy**: Bookmann serves as the "universal translator" between different reading platforms, understanding multiple location reference systems and converting between them seamlessly.
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Current System Status](#current-system-status)
|
|
2. [Problem Analysis](#problem-analysis)
|
|
3. [Technical Architecture](#technical-architecture)
|
|
4. [Database Schema Changes](#database-schema-changes)
|
|
5. [API Endpoints Specification](#api-endpoints-specification)
|
|
6. [Format Grouping System](#format-grouping-system)
|
|
7. [Progress Tracking by Format](#progress-tracking-by-format)
|
|
8. [Wireless Sync Protocols](#wireless-sync-protocols)
|
|
9. [Security Architecture](#security-architecture)
|
|
10. [Conflict Resolution System](#conflict-resolution-system)
|
|
11. [Backup & Recovery Strategy](#backup--recovery-strategy)
|
|
12. [Real-time Synchronization](#real-time-synchronization)
|
|
13. [Device Onboarding Flow](#device-onboarding-flow)
|
|
14. [Implementation Phases](#implementation-phases)
|
|
15. [Testing Requirements](#testing-requirements)
|
|
16. [Performance Considerations](#performance-considerations)
|
|
|
|
---
|
|
|
|
## Current System Status
|
|
|
|
### Working Endpoints (As of Testing)
|
|
All core functionality is operational:
|
|
- Authentication: Register, login, logout, refresh tokens ✅
|
|
- Library management: Create, read, update, delete libraries ✅
|
|
- Scanner: Background scanning, watch mode, status tracking ✅
|
|
- Media items: CRUD operations, search, filter, sort ✅
|
|
- Progress tracking: Basic page-based tracking ✅
|
|
- Notes: Create, read, update, delete ✅
|
|
- Highlights: Create, read, update, delete ✅
|
|
- Ratings: Full rating system ✅
|
|
|
|
### Known Issues
|
|
1. **EPUB Progress Tracking**: Current page-based system is unreliable for reflowable formats
|
|
2. **No Cross-Device Sync**: Progress doesn't sync between devices
|
|
3. **Limited Platform Support**: Only web interface, no device integration
|
|
4. **Format Inconsistency**: Different formats treated identically despite different capabilities
|
|
|
|
---
|
|
|
|
## Problem Analysis
|
|
|
|
### The EPUB Progress Problem
|
|
|
|
**Current Implementation Flaw:**
|
|
- EPUB is reflowable - page count changes based on font size, screen size, zoom level
|
|
- "Page 45 of 200" is meaningless across different devices or reading sessions
|
|
- Notes at "page:45" location become unfindable with different display settings
|
|
- Progress sharing between users or devices is unreliable
|
|
|
|
**Real-World Example:**
|
|
- User reads on tablet (large font) → sees "page 45 of 250"
|
|
- Same user reads on phone (small font) → sees "page 78 of 450"
|
|
- Same book, same progress, but completely different page numbers
|
|
|
|
### Cross-Platform Incompatibility
|
|
|
|
**Kobo Devices:**
|
|
- Use percentage-based progress internally
|
|
- Sync to proprietary Kobo cloud
|
|
- Store annotations in SQLite databases
|
|
- No open API for third-party sync
|
|
|
|
**KOReader:**
|
|
- Open-source, supports Wi-Fi sync
|
|
- Uses calibre-compatible protocols
|
|
- Stores data in `.sdr` sidecar folders
|
|
- Percentage + character offset tracking
|
|
|
|
**Bookmann Current State:**
|
|
- Page-based tracking only
|
|
- No device protocols implemented
|
|
- No sync endpoints for external devices
|
|
|
|
---
|
|
|
|
## Technical Architecture
|
|
|
|
### System Overview
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Bookmann Core System │
|
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
│ │ Web UI │ │ Wireless │ │ Progress │ │
|
|
│ │ Interface │ │ Sync API │ │ Conversion │ │
|
|
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
▲
|
|
│
|
|
┌───────────────────┼───────────────────┐
|
|
│ │ │
|
|
┌───────┴────────┐ ┌──────┴─────────┐ ┌─────┴────────┐
|
|
│ KOReader │ │ Kobo Device │ │ Web Mobile │
|
|
│ .sdr folders │ │ SQLite DB │ │ App/API │
|
|
└────────────────┘ └────────────────┘ └───────────────┘
|
|
```
|
|
|
|
### Data Flow Architecture
|
|
|
|
```
|
|
Device Progress → Device Protocol → Bookmann Wireless API
|
|
↓
|
|
Universal Progress JSON
|
|
↓
|
|
Format Conversion Engine
|
|
↓
|
|
Database Storage (Multi-format)
|
|
↓
|
|
WebSocket Broadcast
|
|
↓
|
|
All Connected Clients (Web, Mobile, Other Devices)
|
|
```
|
|
|
|
### Universal Progress Hub Concept
|
|
|
|
Bookmann doesn't just store progress - it maintains a **multi-dimensional location reference system** that can express the same reading position in multiple ways simultaneously:
|
|
|
|
```json
|
|
{
|
|
"book_id": "uuid-1234",
|
|
"universal_progress": 0.45678,
|
|
"format_group": "reflowable",
|
|
"location_references": {
|
|
"percentage": 0.45678,
|
|
"epubcfi": "epubcfi(/6/4[chap1]!/4/2:15)",
|
|
"character": 15432,
|
|
"chapter": 3,
|
|
"chapter_progress": 0.234,
|
|
"viewport_y": 0.12
|
|
},
|
|
"device_specific": {
|
|
"koreader": { "percent": 0.45678, "timestamp": "..." },
|
|
"kobo": { "progress": 45, "timestamp": "..." },
|
|
"web": { "display_page": 89, "total_pages": 200, "timestamp": "..." }
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Database Schema Changes
|
|
|
|
### Core Schema Additions
|
|
|
|
```sql
|
|
-- ============================================
|
|
-- FORMAT DETECTION AND GROUPING
|
|
-- ============================================
|
|
ALTER TABLE media_items
|
|
ADD COLUMN format_group VARCHAR(20) NOT NULL DEFAULT 'reflowable',
|
|
ADD COLUMN format_mimetype VARCHAR(100),
|
|
ADD COLUMN is_reflowable BOOLEAN DEFAULT TRUE,
|
|
ADD COLUMN has_fixed_layout BOOLEAN DEFAULT FALSE,
|
|
ADD COLUMN total_characters BIGINT,
|
|
ADD COLUMN chapter_count INTEGER;
|
|
|
|
-- ============================================
|
|
-- UNIVERSAL PROGRESS TRACKING
|
|
-- ============================================
|
|
ALTER TABLE reading_progress
|
|
ADD COLUMN percentage FLOAT CHECK (percentage >= 0 AND percentage <= 1),
|
|
ADD COLUMN character_offset BIGINT,
|
|
ADD COLUMN epubcfi TEXT,
|
|
ADD COLUMN chapter INTEGER,
|
|
ADD COLUMN chapter_progress FLOAT CHECK (chapter_progress >= 0 AND chapter_progress <= 1),
|
|
ADD COLUMN viewport_x FLOAT DEFAULT 0,
|
|
ADD COLUMN viewport_y FLOAT DEFAULT 0,
|
|
ADD COLUMN zoom_level FLOAT DEFAULT 1.0,
|
|
ADD COLUMN scroll_position_x FLOAT DEFAULT 0,
|
|
ADD COLUMN scroll_position_y FLOAT DEFAULT 0,
|
|
ADD COLUMN panel_number INTEGER,
|
|
ADD COLUMN reading_mode VARCHAR(20);
|
|
|
|
-- ============================================
|
|
-- DEVICE SYNC METADATA
|
|
-- ============================================
|
|
ALTER TABLE reading_progress
|
|
ADD COLUMN last_sync_device VARCHAR(50),
|
|
ADD COLUMN last_sync_source VARCHAR(20),
|
|
ADD COLUMN last_sync_timestamp TIMESTAMP,
|
|
ADD COLUMN conflict_detected BOOLEAN DEFAULT FALSE,
|
|
ADD COLUMN conflict_resolved BOOLEAN DEFAULT TRUE;
|
|
|
|
-- ============================================
|
|
-- NOTES LOCATION ENHANCEMENTS
|
|
-- ============================================
|
|
ALTER TABLE media_notes
|
|
ADD COLUMN percentage_location FLOAT,
|
|
ADD COLUMN character_start INTEGER,
|
|
ADD COLUMN character_end INTEGER,
|
|
ADD COLUMN epubcfi_location TEXT,
|
|
ADD COLUMN chapter_reference INTEGER,
|
|
ADD COLUMN paragraph_reference INTEGER,
|
|
ADD COLUMN device_sync_data JSONB;
|
|
|
|
-- ============================================
|
|
-- HIGHLIGHTS LOCATION ENHANCEMENTS
|
|
-- ============================================
|
|
ALTER TABLE media_highlights
|
|
ADD COLUMN percentage_start FLOAT,
|
|
ADD COLUMN percentage_end FLOAT,
|
|
ADD COLUMN character_start INTEGER,
|
|
ADD COLUMN character_end INTEGER,
|
|
ADD COLUMN epubcfi_start TEXT,
|
|
ADD COLUMN epubcfi_end TEXT,
|
|
ADD COLUMN chapter_reference INTEGER,
|
|
ADD COLUMN paragraph_start INTEGER,
|
|
ADD COLUMN paragraph_end INTEGER,
|
|
ADD COLUMN panel_number INTEGER,
|
|
ADD COLUMN device_sync_data JSONB;
|
|
|
|
-- ============================================
|
|
-- DEVICE REGISTRY
|
|
-- ============================================
|
|
CREATE TABLE devices (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
device_name VARCHAR(100) NOT NULL,
|
|
device_type VARCHAR(20) NOT NULL, -- 'koreader', 'kobo', 'web', 'mobile'
|
|
device_identifier VARCHAR(255) UNIQUE NOT NULL,
|
|
auth_token VARCHAR(500) UNIQUE NOT NULL,
|
|
last_sync TIMESTAMP,
|
|
last_seen TIMESTAMP,
|
|
sync_enabled BOOLEAN DEFAULT TRUE,
|
|
auto_sync BOOLEAN DEFAULT TRUE,
|
|
sync_frequency_minutes INTEGER DEFAULT 5,
|
|
device_metadata JSONB,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_devices_user_id ON devices(user_id);
|
|
CREATE INDEX idx_devices_device_type ON devices(device_type);
|
|
CREATE INDEX idx_devices_device_identifier ON devices(device_identifier);
|
|
|
|
-- ============================================
|
|
-- SYNC QUEUE FOR OFFLINE SUPPORT
|
|
-- ============================================
|
|
CREATE TABLE sync_queue (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
device_id UUID NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
|
media_item_id UUID REFERENCES media_items(id) ON DELETE CASCADE,
|
|
sync_type VARCHAR(20) NOT NULL, -- 'progress', 'note', 'highlight', 'bookmark'
|
|
sync_data JSONB NOT NULL,
|
|
priority INTEGER DEFAULT 5,
|
|
attempts INTEGER DEFAULT 0,
|
|
max_attempts INTEGER DEFAULT 3,
|
|
status VARCHAR(20) DEFAULT 'pending', -- 'pending', 'processing', 'completed', 'failed'
|
|
error_message TEXT,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
processed_at TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX idx_sync_queue_device_id ON sync_queue(device_id);
|
|
CREATE INDEX idx_sync_queue_status ON sync_queue(status);
|
|
CREATE INDEX idx_sync_queue_priority ON sync_queue(priority);
|
|
|
|
-- ============================================
|
|
-- CONFLICT RESOLUTION
|
|
-- ============================================
|
|
CREATE TABLE sync_conflicts (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
media_item_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
conflict_type VARCHAR(20) NOT NULL, -- 'progress', 'note', 'highlight'
|
|
conflict_data JSONB NOT NULL,
|
|
resolution_status VARCHAR(20) DEFAULT 'unresolved', -- 'unresolved', 'auto_resolved', 'user_resolved'
|
|
resolution_data JSONB,
|
|
resolved_by UUID REFERENCES users(id),
|
|
resolved_at TIMESTAMP,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_sync_conflicts_media_item_id ON sync_conflicts(media_item_id);
|
|
CREATE INDEX idx_sync_conflicts_user_id ON sync_conflicts(user_id);
|
|
CREATE INDEX idx_sync_conflicts_status ON sync_conflicts(resolution_status);
|
|
|
|
-- ============================================
|
|
-- READING HISTORY
|
|
-- ============================================
|
|
CREATE TABLE reading_history (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
media_item_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
|
|
device_id UUID REFERENCES devices(id),
|
|
progress_percentage FLOAT,
|
|
reading_session_start TIMESTAMP,
|
|
reading_session_end TIMESTAMP,
|
|
pages_read INTEGER,
|
|
time_spent_seconds INTEGER,
|
|
device_metadata JSONB,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_reading_history_user_id ON reading_history(user_id);
|
|
CREATE INDEX idx_reading_history_media_item_id ON reading_history(media_item_id);
|
|
CREATE INDEX idx_reading_history_created_at ON reading_history(created_at DESC);
|
|
```
|
|
|
|
### Database Triggers for Automatic Timestamps
|
|
|
|
```sql
|
|
-- Update devices updated_at timestamp
|
|
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = NOW();
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER update_devices_updated_at
|
|
BEFORE UPDATE ON devices
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_updated_at_column();
|
|
```
|
|
|
|
---
|
|
|
|
## API Endpoints Specification
|
|
|
|
### Authentication Endpoints
|
|
|
|
#### Device Registration Flow
|
|
```
|
|
POST /api/devices/register
|
|
Description: Register a new reading device
|
|
|
|
Request Body:
|
|
{
|
|
"device_name": "My Kobo Clara",
|
|
"device_type": "kobo|koreader|web|mobile",
|
|
"device_identifier": "unique-device-hardware-id"
|
|
}
|
|
|
|
Response (201):
|
|
{
|
|
"device_id": "uuid",
|
|
"auth_token": "Bearer token for device",
|
|
"setup_instructions": {
|
|
"kobo": "Sync URL: https://bookmann.example.com/api/sync/kobo",
|
|
"koreader": "Calibre URL: https://bookmann.example.com/api/sync/koreader"
|
|
},
|
|
"qr_code_url": "https://bookmann.example.com/devices/qr/uuid"
|
|
}
|
|
```
|
|
|
|
#### Web Login for Device Authentication
|
|
```
|
|
POST /api/devices/auth/web
|
|
Description: Authenticate device via web login (no API keys on device!)
|
|
|
|
Request Body:
|
|
{
|
|
"device_identifier": "device-unique-id",
|
|
"device_type": "kobo"
|
|
}
|
|
|
|
Response (200):
|
|
{
|
|
"auth_pending": true,
|
|
"auth_url": "https://bookmann.example.com/devices/auth/confirm/abc123",
|
|
"expires_in": 300,
|
|
"poll_interval": 3
|
|
}
|
|
|
|
[User visits auth URL on web, logs in, approves device]
|
|
|
|
Device polls:
|
|
POST /api/devices/auth/status
|
|
|
|
Response (200):
|
|
{
|
|
"auth_complete": true,
|
|
"auth_token": "device-bearer-token",
|
|
"device_id": "uuid",
|
|
"sync_endpoints": {
|
|
"progress": "https://bookmann.example.com/api/sync/progress",
|
|
"metadata": "https://bookmann.example.com/api/sync/metadata",
|
|
"annotations": "https://bookmann.example.com/api/sync/annotations"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Wireless Sync Endpoints
|
|
|
|
#### KOReader Wireless Sync (Calibre-compatible)
|
|
```
|
|
POST /api/sync/koreader/progress
|
|
Description: KOReader sends progress update
|
|
|
|
Request Headers:
|
|
Authorization: Bearer {device_token}
|
|
Content-Type: application/json
|
|
|
|
Request Body:
|
|
{
|
|
"library_id": "optional-uuid",
|
|
"books": [
|
|
{
|
|
"uuid": "book-uuid",
|
|
"title": "Book Title",
|
|
"authors": ["Author Name"],
|
|
"progress": 0.45,
|
|
"percentage": 0.45,
|
|
"last_read": "2026-01-30T20:00:00Z",
|
|
"device_info": {
|
|
"koreader_version": "2024.01",
|
|
"device_model": "kindle-paperwhite-5"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
|
|
Response (202):
|
|
{
|
|
"sync_status": "accepted",
|
|
"books_synced": 1,
|
|
"conflicts": [
|
|
{
|
|
"book_uuid": "book-uuid",
|
|
"conflict_type": "progress_mismatch",
|
|
"device_progress": 0.45,
|
|
"server_progress": 0.42,
|
|
"resolution": "device_wins"
|
|
}
|
|
]
|
|
}
|
|
|
|
GET /api/sync/koreader/metadata/{book_uuid}
|
|
Description: KOReader fetches book metadata and sync status
|
|
|
|
Response (200):
|
|
{
|
|
"uuid": "book-uuid",
|
|
"title": "Book Title",
|
|
"authors": ["Author Name"],
|
|
"progress": {
|
|
"percentage": 0.42,
|
|
"character": 15432,
|
|
"epubcfi": "epubcfi(/6/4/2:15)",
|
|
"chapter": 3,
|
|
"chapter_progress": 0.234
|
|
},
|
|
"annotations": {
|
|
"highlights": [...],
|
|
"notes": [...],
|
|
"bookmarks": [...]
|
|
},
|
|
"last_sync": "2026-01-30T20:00:00Z"
|
|
}
|
|
```
|
|
|
|
#### Kobo Wireless Sync (Kobo API-compatible)
|
|
```
|
|
POST /api/sync/kobo/markup
|
|
Description: Kobo sends reading progress and annotations
|
|
|
|
Request Headers:
|
|
Authorization: Bearer {device_token}
|
|
x-kobo-device: {"DeviceId":"device-id","Model":"Kobo Clara"}
|
|
|
|
Request Body:
|
|
{
|
|
"ReadingSync": [
|
|
{
|
|
"ContentId": "book-uuid",
|
|
"PercentRead": 45.6,
|
|
"EntitlementId": "entitlement-id",
|
|
"RemainingTimeMinutes": 120,
|
|
"LastModified": "2026-01-30T20:00:00Z"
|
|
}
|
|
],
|
|
"BookmarkSync": [
|
|
{
|
|
"ContentId": "book-uuid",
|
|
"BookmarkText": "highlighted text",
|
|
"BookmarkType": "annotation",
|
|
"BookmarkTitle": "Chapter 3"
|
|
}
|
|
]
|
|
}
|
|
|
|
Response (200):
|
|
{
|
|
"Status": "Success",
|
|
"MarkupsSynced": 5,
|
|
"BookmarksSynced": 3
|
|
}
|
|
|
|
GET /api/sync/kobo/library
|
|
Description: Kobo fetches library and sync status
|
|
|
|
Response (200):
|
|
{
|
|
"library_sync": [
|
|
{
|
|
"ContentId": "book-uuid",
|
|
"ContentType": "6",
|
|
"Title": "Book Title",
|
|
"Author": "Author Name",
|
|
"PercentRead": 42.3,
|
|
"PagesRemaining": 115,
|
|
"BookmarkCount": 3,
|
|
"LastModified": "2026-01-30T20:00:00Z"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### Universal Progress Endpoints
|
|
|
|
#### Get Universal Progress
|
|
```
|
|
GET /api/progress/{book_uuid}
|
|
Description: Get progress with all location references
|
|
|
|
Response (200):
|
|
{
|
|
"book_id": "book-uuid",
|
|
"format_group": "reflowable",
|
|
"universal_progress": 0.45678,
|
|
"location_references": {
|
|
"percentage": 0.45678,
|
|
"epubcfi": "epubcfi(/6/4/2:15)",
|
|
"character": 15432,
|
|
"chapter": 3,
|
|
"chapter_progress": 0.234,
|
|
"viewport_y": 0.12
|
|
},
|
|
"device_progress": {
|
|
"koreader": {
|
|
"percentage": 0.45678,
|
|
"last_sync": "2026-01-30T20:00:00Z"
|
|
},
|
|
"kobo": {
|
|
"percentage": 45.6,
|
|
"last_sync": "2026-01-30T19:55:00Z"
|
|
},
|
|
"web": {
|
|
"display_page": 89,
|
|
"total_pages": 200,
|
|
"last_sync": "2026-01-30T20:05:00Z"
|
|
}
|
|
},
|
|
"annotations": {
|
|
"highlights": [...],
|
|
"notes": [...],
|
|
"bookmarks": [...]
|
|
},
|
|
"conflicts": [
|
|
{
|
|
"id": "conflict-uuid",
|
|
"type": "progress",
|
|
"resolved": false,
|
|
"sources": ["koreader", "kobo"]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
#### Update Universal Progress
|
|
```
|
|
POST /api/progress/{book_uuid}
|
|
Description: Update progress with automatic conversion to all formats
|
|
|
|
Request Body:
|
|
{
|
|
"source": "web|koreader|kobo|mobile",
|
|
"location": {
|
|
"percentage": 0.45678,
|
|
"epubcfi": "epubcfi(/6/4/2:15)",
|
|
"character": 15432,
|
|
"chapter": 3,
|
|
"page": 89,
|
|
"total_pages": 200
|
|
},
|
|
"device_metadata": {
|
|
"device_type": "web",
|
|
"user_agent": "..."
|
|
}
|
|
}
|
|
|
|
Response (200):
|
|
{
|
|
"sync_status": "success",
|
|
"progress_updated": true,
|
|
"devices_notified": ["koreader-device-1", "kobo-device-2"],
|
|
"broadcast": true
|
|
}
|
|
```
|
|
|
|
### Conflict Resolution Endpoints
|
|
|
|
#### List Conflicts
|
|
```
|
|
GET /api/conflicts
|
|
Description: List all unresolved sync conflicts
|
|
|
|
Query Parameters:
|
|
status: "unresolved|all"
|
|
type: "progress|note|highlight|all"
|
|
|
|
Response (200):
|
|
{
|
|
"conflicts": [
|
|
{
|
|
"id": "conflict-uuid",
|
|
"media_item_id": "book-uuid",
|
|
"media_item_title": "Book Title",
|
|
"conflict_type": "progress",
|
|
"conflict_data": {
|
|
"koreader": {
|
|
"source": "koreader",
|
|
"timestamp": "2026-01-30T20:10:00Z",
|
|
"data": {
|
|
"percentage": 0.45,
|
|
"epubcfi": "epubcfi(/6/4/2:15)",
|
|
"character": 15432
|
|
}
|
|
},
|
|
"kobo": {
|
|
"source": "kobo",
|
|
"timestamp": "2026-01-30T20:05:00Z",
|
|
"data": {
|
|
"percentage": 0.42,
|
|
"location": "unknown"
|
|
}
|
|
}
|
|
},
|
|
"resolution_status": "unresolved",
|
|
"created_at": "2026-01-30T20:10:05Z"
|
|
}
|
|
],
|
|
"total": 1,
|
|
"unresolved": 1
|
|
}
|
|
```
|
|
|
|
#### Resolve Conflict
|
|
```
|
|
POST /api/conflicts/{conflict_id}/resolve
|
|
Description: Resolve a sync conflict by choosing source
|
|
|
|
Request Body:
|
|
{
|
|
"winner": "koreader|kobo|web|manual",
|
|
"manual_data": {
|
|
"percentage": 0.43,
|
|
"epubcfi": "epubcfi(/6/4/2:20)",
|
|
"character": 15500
|
|
}, // Required if winner is "manual"
|
|
"apply_to_all_future_conflicts": false,
|
|
"reason": "user chose more recent progress"
|
|
}
|
|
|
|
Response (200):
|
|
{
|
|
"conflict_resolved": true,
|
|
"applied_to": {
|
|
"progress": true,
|
|
"annotations": false
|
|
},
|
|
"devices_synced": ["koreader-device-1", "kobo-device-2"]
|
|
}
|
|
```
|
|
|
|
### Device Management Endpoints
|
|
|
|
#### List User Devices
|
|
```
|
|
GET /api/devices
|
|
Response (200):
|
|
{
|
|
"devices": [
|
|
{
|
|
"id": "device-uuid",
|
|
"device_name": "My Kobo Clara",
|
|
"device_type": "kobo",
|
|
"last_sync": "2026-01-30T20:00:00Z",
|
|
"last_seen": "2026-01-30T20:05:00Z",
|
|
"sync_enabled": true,
|
|
"auto_sync": true
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
#### Update Device Settings
|
|
```
|
|
PUT /api/devices/{device_id}
|
|
Request Body:
|
|
{
|
|
"device_name": "Updated Name",
|
|
"sync_enabled": true,
|
|
"auto_sync": true,
|
|
"sync_frequency_minutes": 5
|
|
}
|
|
|
|
Response (200):
|
|
{
|
|
"device_updated": true
|
|
}
|
|
```
|
|
|
|
#### Revoke Device Access
|
|
```
|
|
DELETE /api/devices/{device_id}
|
|
Response (204)
|
|
```
|
|
|
|
---
|
|
|
|
## Format Grouping System
|
|
|
|
### Three Format Groups
|
|
|
|
Based on format capabilities and optimal tracking methods:
|
|
|
|
#### 1. Reflowable Group
|
|
**Formats**: EPUB, MOBI, AZW3, FB2, TXT
|
|
**Characteristics**:
|
|
- Text reflows to fit viewport
|
|
- Page count changes based on display settings
|
|
- Can contain complex formatting and images
|
|
- Support for chapters, TOC
|
|
|
|
**Optimal Tracking**:
|
|
- Percentage (0.0-1.0, 5 decimal precision)
|
|
- EPUB CFI for precise locations
|
|
- Chapter number + chapter progress
|
|
- Character offset from beginning
|
|
- Viewport Y position for visual reference
|
|
|
|
#### 2. Fixed Layout Group
|
|
**Formats**: PDF, DJVU
|
|
**Characteristics**:
|
|
- Fixed page layout (like printed book)
|
|
- Consistent page count across devices
|
|
- Zoom and pan for navigation
|
|
- May contain text layer
|
|
|
|
**Optimal Tracking**:
|
|
- Page number + total pages
|
|
- Page Y position (for scroll position)
|
|
- Zoom level
|
|
- Scroll X/Y coordinates
|
|
- Character offset in extracted text (if available)
|
|
|
|
#### 3. Comic Archive Group
|
|
**Formats**: CBZ, CBR, CBT, CB7, PDF comics
|
|
**Characteristics**:
|
|
- Image-based pages
|
|
- No text layer (usually)
|
|
- Panel-based reading
|
|
- Single/double page modes
|
|
- Panel zoom features
|
|
|
|
**Optimal Tracking**:
|
|
- Page number
|
|
- Panel number (for future panel navigation)
|
|
- Panel coordinates (for future use)
|
|
- Zoom level
|
|
- Pan X/Y coordinates
|
|
- Reading mode (single/double/panel)
|
|
|
|
### Format Detection Logic
|
|
|
|
```sql
|
|
CREATE OR REPLACE FUNCTION detect_format_group(mimetype VARCHAR, file_path VARCHAR)
|
|
RETURNS VARCHAR AS $$
|
|
BEGIN
|
|
CASE
|
|
-- Reflowable formats
|
|
WHEN mimetype = 'application/epub+zip' THEN
|
|
RETURN 'reflowable';
|
|
WHEN mimetype = 'application/x-mobipocket-ebook' THEN
|
|
RETURN 'reflowable';
|
|
WHEN mimetype = 'application/vnd.amazon.mobi8-ebook' THEN
|
|
RETURN 'reflowable';
|
|
WHEN file_path LIKE '%.epub' THEN
|
|
RETURN 'reflowable';
|
|
WHEN file_path LIKE '%.mobi' THEN
|
|
RETURN 'reflowable';
|
|
WHEN file_path LIKE '%.azw3' THEN
|
|
RETURN 'reflowable';
|
|
WHEN file_path LIKE '%.fb2' THEN
|
|
RETURN 'reflowable';
|
|
WHEN file_path LIKE '%.txt' THEN
|
|
RETURN 'reflowable';
|
|
|
|
-- Fixed layout formats
|
|
WHEN mimetype = 'application/pdf' THEN
|
|
RETURN 'fixed_layout';
|
|
WHEN file_path LIKE '%.pdf' THEN
|
|
RETURN 'fixed_layout';
|
|
WHEN file_path LIKE '%.djvu' THEN
|
|
RETURN 'fixed_layout';
|
|
|
|
-- Comic archive formats
|
|
WHEN mimetype = 'application/x-cbr' THEN
|
|
RETURN 'comic_archive';
|
|
WHEN mimetype = 'application/x-cbz' THEN
|
|
RETURN 'comic_archive';
|
|
WHEN file_path LIKE '%.cbz' THEN
|
|
RETURN 'comic_archive';
|
|
WHEN file_path LIKE '%.cbr' THEN
|
|
RETURN 'comic_archive';
|
|
WHEN file_path LIKE '%.cbt' THEN
|
|
RETURN 'comic_archive';
|
|
WHEN file_path LIKE '%.cb7' THEN
|
|
RETURN 'comic_archive';
|
|
|
|
ELSE
|
|
RETURN 'unknown';
|
|
END CASE;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
```
|
|
|
|
---
|
|
|
|
## Progress Tracking by Format
|
|
|
|
### Reflowable Format Progress
|
|
|
|
```json
|
|
{
|
|
"format_group": "reflowable",
|
|
"book_id": "uuid",
|
|
"progress": {
|
|
"percentage": 0.45678,
|
|
"epubcfi": "epubcfi(/6/4[chap01ref]!/4[body01]/10[para05]/2:15)",
|
|
"character": 15432,
|
|
"chapter": 3,
|
|
"chapter_progress": 0.234,
|
|
"viewport_y": 0.12,
|
|
"total_characters": 34567
|
|
},
|
|
"conversion_rules": {
|
|
"to_percentage": "character / total_characters",
|
|
"to_epubcfi": "use stored CFI or calculate from character",
|
|
"to_chapter": "parse from CFI or calculate from character offset",
|
|
"from_koreader": "direct percentage mapping",
|
|
"from_kobo": "percentage / 100"
|
|
},
|
|
"display": {
|
|
"web": "45.7% (Chapter 3)",
|
|
"koreader": "45.7%",
|
|
"kobo": "46%"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Fixed Layout Format Progress
|
|
|
|
```json
|
|
{
|
|
"format_group": "fixed_layout",
|
|
"book_id": "uuid",
|
|
"progress": {
|
|
"page": 45,
|
|
"total_pages": 200,
|
|
"page_y": 234,
|
|
"zoom": 1.25,
|
|
"scroll_x": 0,
|
|
"scroll_y": 0,
|
|
"character": 15432,
|
|
"percentage": 0.225
|
|
},
|
|
"conversion_rules": {
|
|
"to_percentage": "page / total_pages",
|
|
"to_page": "floor(percentage * total_pages)",
|
|
"to_character": "use OCR text extraction",
|
|
"from_koreader": "map percentage to nearest page",
|
|
"from_kobo": "direct page mapping"
|
|
},
|
|
"display": {
|
|
"web": "Page 45 of 200 (22.5%)",
|
|
"koreader": "22.5%",
|
|
"kobo": "Page 45 (23%)"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Comic Archive Format Progress
|
|
|
|
```json
|
|
{
|
|
"format_group": "comic_archive",
|
|
"book_id": "uuid",
|
|
"progress": {
|
|
"page": 12,
|
|
"total_pages": 32,
|
|
"panel": 5,
|
|
"panel_bounds": "120,85,300,250",
|
|
"zoom": 1.5,
|
|
"pan_x": 120,
|
|
"pan_y": 85,
|
|
"reading_mode": "single_page",
|
|
"percentage": 0.375
|
|
},
|
|
"conversion_rules": {
|
|
"to_percentage": "page / total_pages",
|
|
"to_page": "floor(percentage * total_pages)",
|
|
"from_koreader": "percentage to page mapping",
|
|
"from_kobo": "page mapping"
|
|
},
|
|
"display": {
|
|
"web": "Page 12 of 32 (Panel 5)",
|
|
"koreader": "37.5%",
|
|
"kobo": "Page 12 (38%)"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Progress Conversion Engine
|
|
|
|
```sql
|
|
CREATE OR REPLACE FUNCTION convert_progress(
|
|
source_format VARCHAR,
|
|
target_format VARCHAR,
|
|
source_progress JSONB
|
|
) RETURNS JSONB AS $$
|
|
DECLARE
|
|
result JSONB;
|
|
percentage FLOAT;
|
|
BEGIN
|
|
-- Extract percentage from source
|
|
CASE source_format
|
|
WHEN 'reflowable' THEN
|
|
percentage := (source_progress->>'percentage')::FLOAT;
|
|
WHEN 'fixed_layout' THEN
|
|
percentage := ((source_progress->>'page')::FLOAT /
|
|
(source_progress->>'total_pages')::FLOAT);
|
|
WHEN 'comic_archive' THEN
|
|
percentage := ((source_progress->>'page')::FLOAT /
|
|
(source_progress->>'total_pages')::FLOAT);
|
|
ELSE
|
|
percentage := 0.0;
|
|
END CASE;
|
|
|
|
-- Build target format progress
|
|
CASE target_format
|
|
WHEN 'reflowable' THEN
|
|
result := jsonb_build_object(
|
|
'percentage', percentage,
|
|
'character', CAST(percentage * 34567 AS INTEGER),
|
|
'epubcfi', 'epubcfi(/6/4/2:' || CAST(percentage * 100 AS INTEGER) || ')'
|
|
);
|
|
WHEN 'fixed_layout' THEN
|
|
result := jsonb_build_object(
|
|
'page', CAST(percentage * 200 AS INTEGER),
|
|
'total_pages', 200,
|
|
'percentage', percentage
|
|
);
|
|
WHEN 'comic_archive' THEN
|
|
result := jsonb_build_object(
|
|
'page', CAST(percentage * 32 AS INTEGER),
|
|
'total_pages', 32,
|
|
'percentage', percentage
|
|
);
|
|
ELSE
|
|
result := '{}'::jsonb;
|
|
END CASE;
|
|
|
|
RETURN result;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
```
|
|
|
|
---
|
|
|
|
## Wireless Sync Protocols
|
|
|
|
### KOReader Calibre-Compatible Protocol
|
|
|
|
**Endpoint Structure**:
|
|
```
|
|
POST /api/sync/koreader/progress
|
|
POST /api/sync/koreader/bookmarks
|
|
POST /api/sync/koreader/highlights
|
|
GET /api/sync/koreader/metadata
|
|
GET /api/sync/koreader/status
|
|
```
|
|
|
|
**Authentication**: Bearer token from device registration
|
|
|
|
**Data Format** (KOReader → Bookmann):
|
|
```json
|
|
{
|
|
"library_id": "optional-uuid",
|
|
"books": [
|
|
{
|
|
"uuid": "book-uuid",
|
|
"title": "Book Title",
|
|
"authors": ["Author Name"],
|
|
"progress": 0.45,
|
|
"percentage": 0.45,
|
|
"last_read": "2026-01-30T20:00:00Z",
|
|
"bookmarks": [
|
|
{
|
|
"chapter": 3,
|
|
"datetime": "2026-01-30T19:55:00Z",
|
|
"notes": "highlighted text",
|
|
"pos0": "epubcfi(/6/4/2:15)",
|
|
"pos1": "epubcfi(/6/4/2:20)",
|
|
"page": 45,
|
|
"text": "highlighted text excerpt",
|
|
"type": "highlight"
|
|
}
|
|
],
|
|
"highlights": [...],
|
|
"notes": [...]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Immediate Sync on Page Turn**:
|
|
```
|
|
POST /api/sync/koreader/progress
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"sync_mode": "immediate",
|
|
"books": [
|
|
{
|
|
"uuid": "book-uuid",
|
|
"percentage": 0.45678,
|
|
"chapter": 3,
|
|
"timestamp": "2026-01-30T20:00:00Z"
|
|
}
|
|
]
|
|
}
|
|
|
|
Response (202 Accepted):
|
|
{
|
|
"sync_status": "accepted",
|
|
"broadcast_scheduled": true,
|
|
"queue_position": 1
|
|
}
|
|
```
|
|
|
|
**Checkpoint Sync (Fallback)**:
|
|
```
|
|
POST /api/sync/koreader/progress
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"sync_mode": "checkpoint",
|
|
"checkpoint_id": "checkpoint-uuid",
|
|
"since_timestamp": "2026-01-30T19:00:00Z",
|
|
"books": [...]
|
|
}
|
|
|
|
Response (200 OK):
|
|
{
|
|
"sync_status": "completed",
|
|
"books_synced": 1,
|
|
"checkpoint_saved": true
|
|
}
|
|
```
|
|
|
|
### Kobo Sync Protocol (Reverse-Engineered)
|
|
|
|
**Endpoint Structure**:
|
|
```
|
|
POST /api/sync/kobo/markup
|
|
POST /api/sync/kobo/bookmark
|
|
GET /api/sync/kobo/library
|
|
GET /api/sync/kobo/status
|
|
```
|
|
|
|
**Authentication Headers**:
|
|
```
|
|
Authorization: Bearer {device_token}
|
|
x-kobo-userid: {user_id}
|
|
x-kobo-device: {"DeviceId":"device-id","Model":"Kobo Clara","SerialNumber":"..."}
|
|
```
|
|
|
|
**Data Format** (Kobo → Bookmann):
|
|
```json
|
|
{
|
|
"ReadingSync": [
|
|
{
|
|
"ContentId": "book-uuid",
|
|
"PercentRead": 45.6,
|
|
"EntitlementId": "entitlement-id",
|
|
"RemainingTimeMinutes": 120,
|
|
"FirstReadTime": "2026-01-25T10:00:00Z",
|
|
"LastModified": "2026-01-30T20:00:00Z"
|
|
}
|
|
],
|
|
"BookmarkSync": [
|
|
{
|
|
"BookmarkId": "bookmark-uuid",
|
|
"ContentId": "book-uuid",
|
|
"BookmarkText": "highlighted text",
|
|
"BookmarkType": "annotation",
|
|
"BookmarkTitle": "Chapter 3",
|
|
"DateCreated": "2026-01-30T19:55:00Z",
|
|
"Chapter": 3,
|
|
"Hidden": false
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### Single Port Architecture
|
|
|
|
All sync endpoints share the same port (8765) with proper routing:
|
|
|
|
```
|
|
Port 8765 (Bookmann)
|
|
├── /api/auth/* (Authentication)
|
|
├── /api/libraries/* (Library management)
|
|
├── /api/media-items/* (Media items)
|
|
├── /api/sync/koreader/* (KOReader wireless sync)
|
|
├── /api/sync/kobo/* (Kobo wireless sync)
|
|
├── /api/progress/* (Universal progress API)
|
|
├── /api/conflicts/* (Conflict resolution)
|
|
├── /api/devices/* (Device management)
|
|
└── /ws/sync (WebSocket for real-time updates)
|
|
```
|
|
|
|
**Caddy Proxy Configuration**:
|
|
```
|
|
bookmann.example.com {
|
|
reverse_proxy localhost:8765
|
|
encode gzip
|
|
log {
|
|
output file /var/log/caddy/bookmann-access.log
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Security Architecture
|
|
|
|
### Device Authentication Flow
|
|
|
|
**No API Keys on Devices!** All authentication goes through web interface.
|
|
|
|
```
|
|
1. Device generates unique identifier (hardware ID)
|
|
2. Device sends registration request with device type
|
|
3. Bookmann creates pending registration with auth URL
|
|
4. User visits auth URL in web browser
|
|
5. User logs in and approves device
|
|
6. Bookmann generates device token
|
|
7. Device polls for token approval
|
|
8. Device receives token and begins syncing
|
|
```
|
|
|
|
### Authentication Endpoints
|
|
|
|
```
|
|
POST /api/devices/register/initiate
|
|
Request: { "device_identifier": "hw-id-123", "device_type": "kobo" }
|
|
Response: {
|
|
"registration_id": "reg-uuid",
|
|
"auth_url": "https://bookmann.example.com/devices/auth/confirm/reg-uuid",
|
|
"expires_in": 300,
|
|
"qr_code": "data:image/png;base64,..."
|
|
}
|
|
|
|
[User visits auth URL]
|
|
|
|
POST /api/devices/register/status
|
|
Request: { "registration_id": "reg-uuid" }
|
|
Response: {
|
|
"status": "pending" | "approved" | "expired",
|
|
"auth_token": "Bearer token..." (when approved)
|
|
}
|
|
```
|
|
|
|
### Token Management
|
|
|
|
**Device Token Structure**:
|
|
```json
|
|
{
|
|
"device_id": "uuid",
|
|
"user_id": "uuid",
|
|
"device_type": "kobo|koreader|web|mobile",
|
|
"permissions": [
|
|
"sync:progress",
|
|
"sync:annotations",
|
|
"sync:metadata"
|
|
],
|
|
"expires": "never",
|
|
"revocable": true
|
|
}
|
|
```
|
|
|
|
### Rate Limiting
|
|
|
|
```
|
|
Per-device rate limits:
|
|
- Sync requests: 60/minute
|
|
- Progress updates: 120/minute (page turns)
|
|
- Metadata requests: 30/minute
|
|
|
|
Per-user rate limits:
|
|
- All devices combined: 300/minute
|
|
- Conflict resolution: 10/minute
|
|
|
|
Implementation: Redis-backed sliding window
|
|
```
|
|
|
|
### Request Validation
|
|
|
|
```go
|
|
// Example Go validation for sync requests
|
|
func validateSyncRequest(deviceToken string, request SyncRequest) error {
|
|
// 1. Validate device token
|
|
device, err := validateDeviceToken(deviceToken)
|
|
if err != nil {
|
|
return ErrUnauthorized
|
|
}
|
|
|
|
// 2. Check device not revoked
|
|
if device.Revoked {
|
|
return ErrDeviceRevoked
|
|
}
|
|
|
|
// 3. Validate device permissions
|
|
if !hasPermission(device, "sync:progress") {
|
|
return ErrForbidden
|
|
}
|
|
|
|
// 4. Validate request format
|
|
if err := validateRequestFormat(request); err != nil {
|
|
return ErrInvalidRequest
|
|
}
|
|
|
|
// 5. Check rate limits
|
|
if exceededRateLimit(device.ID) {
|
|
return ErrRateLimited
|
|
}
|
|
|
|
// 6. Validate media item ownership
|
|
if !ownsMediaItem(device.UserID, request.BookUUID) {
|
|
return ErrNotFound
|
|
}
|
|
|
|
return nil
|
|
}
|
|
```
|
|
|
|
### CORS Configuration
|
|
|
|
```go
|
|
// Allow device origins
|
|
allowedOrigins := []string{
|
|
"https://bookmann.example.com",
|
|
"kobo://*", // Custom protocol for Kobo app
|
|
"koreader://*", // Custom protocol for KOReader
|
|
}
|
|
|
|
corsConfig := cors.Config{
|
|
AllowOrigins: allowedOrigins,
|
|
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
|
|
AllowHeaders: []string{"Authorization", "Content-Type"},
|
|
ExposeHeaders: []string{"X-Sync-Status", "X-Conflict-Detected"},
|
|
AllowCredentials: true,
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Conflict Resolution System
|
|
|
|
### Conflict Detection
|
|
|
|
**Automatic Conflict Detection**:
|
|
```sql
|
|
CREATE OR REPLACE FUNCTION detect_conflict(
|
|
media_item_id UUID,
|
|
new_progress JSONB,
|
|
device_type VARCHAR
|
|
) RETURNS BOOLEAN AS $$
|
|
DECLARE
|
|
existing_progress JSONB;
|
|
time_diff INTERVAL;
|
|
BEGIN
|
|
-- Get most recent progress from different device
|
|
SELECT pg_jsonb INTO existing_progress
|
|
FROM reading_progress
|
|
WHERE media_item_id = $1
|
|
AND last_sync_source != device_type
|
|
ORDER BY last_sync_timestamp DESC
|
|
LIMIT 1;
|
|
|
|
-- If no existing progress, no conflict
|
|
IF existing_progress IS NULL THEN
|
|
RETURN FALSE;
|
|
END IF;
|
|
|
|
-- Check time difference (within 5 minutes = potential conflict)
|
|
time_diff := NOW() - (existing_progress->>'timestamp')::TIMESTAMP;
|
|
|
|
IF time_diff < INTERVAL '5 minutes' THEN
|
|
-- Check if progress significantly different
|
|
IF ABS((new_progress->>'percentage')::FLOAT -
|
|
(existing_progress->>'percentage')::FLOAT) > 0.01 THEN
|
|
RETURN TRUE;
|
|
END IF;
|
|
END IF;
|
|
|
|
RETURN FALSE;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
```
|
|
|
|
### Side-by-Side Conflict Display
|
|
|
|
**UI Component Specification**:
|
|
```
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ ⚠️ Sync Conflict Detected: Book Title │
|
|
├─────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ Compare progress from different devices: │
|
|
│ │
|
|
│ ┌─────────────────┬─────────────────────────────────┐ │
|
|
│ │ KOReader │ Kobo Device │ │
|
|
│ ├─────────────────┼─────────────────────────────────┤ │
|
|
│ │ Progress: 45.7% │ Progress: 42.3% │ │
|
|
│ │ Location: │ Location: Page 89/200 │ │
|
|
│ │ Chapter 3 │ │ │
|
|
│ │ CFI: ... │ Last Read: 2 hours ago │ │
|
|
│ │ │ │ │
|
|
│ │ Last Read: │ │ │
|
|
│ │ 5 minutes ago │ │ │
|
|
│ │ │ │ │
|
|
│ │ Device: │ Device: Kobo Clara │ │
|
|
│ │ Kindle PW5 │ │ │
|
|
│ └─────────────────┴─────────────────────────────────┘ │
|
|
│ │
|
|
│ [Choose KOReader] [Choose Kobo] [Merge] [Dismiss] │
|
|
│ │
|
|
│ □ Auto-resolve future conflicts from [KOReader] │
|
|
│ │
|
|
└─────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Conflict Resolution Logic
|
|
|
|
**"Last In Wins" with User Override**:
|
|
```
|
|
1. Each sync writes timestamp and source device
|
|
2. If conflict detected:
|
|
- Compare timestamps
|
|
- More recent progress wins by default
|
|
- Create conflict record for user review
|
|
3. User can:
|
|
- Accept auto-resolution
|
|
- Choose different source manually
|
|
- Merge progress (take max of both)
|
|
- Set auto-resolution preference
|
|
```
|
|
|
|
**Merge Logic**:
|
|
```sql
|
|
CREATE OR REPLACE FUNCTION merge_progress(
|
|
progress_a JSONB,
|
|
progress_b JSONB
|
|
) RETURNS JSONB AS $$
|
|
BEGIN
|
|
RETURN jsonb_build_object(
|
|
'percentage', GREATEST(
|
|
(progress_a->>'percentage')::FLOAT,
|
|
(progress_b->>'percentage')::FLOAT
|
|
),
|
|
'merged_from', ARRAY[
|
|
(progress_a->>'source'),
|
|
(progress_b->>'source')
|
|
],
|
|
'merge_timestamp', NOW()
|
|
);
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
```
|
|
|
|
---
|
|
|
|
## Backup & Recovery Strategy
|
|
|
|
### Hybrid Queue + Checkpoint Strategy
|
|
|
|
**Immediate Sync (Page Turn)**:
|
|
```
|
|
Device → POST /api/sync/progress (sync_mode: immediate)
|
|
↓
|
|
Bookmann validates and stores
|
|
↓
|
|
WebSocket broadcast to all clients
|
|
↓
|
|
If network error → Queue locally with retry
|
|
```
|
|
|
|
**Checkpoint Sync (Fallback)**:
|
|
```
|
|
Device → POST /api/sync/progress (sync_mode: checkpoint)
|
|
↓
|
|
Bookmann validates and stores
|
|
↓
|
|
Process queued updates since last checkpoint
|
|
↓
|
|
Clear queue on success
|
|
↓
|
|
On failure → Keep in queue with exponential backoff
|
|
```
|
|
|
|
### Sync Queue Management
|
|
|
|
**Queue Schema**:
|
|
```sql
|
|
CREATE TABLE sync_queue (
|
|
id UUID PRIMARY KEY,
|
|
device_id UUID NOT NULL,
|
|
media_item_id UUID,
|
|
sync_type VARCHAR(20) NOT NULL,
|
|
sync_data JSONB NOT NULL,
|
|
priority INTEGER DEFAULT 5, -- 1=highest, 10=lowest
|
|
attempts INTEGER DEFAULT 0,
|
|
max_attempts INTEGER DEFAULT 3,
|
|
status VARCHAR(20) DEFAULT 'pending',
|
|
error_message TEXT,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
next_retry_at TIMESTAMP,
|
|
processed_at TIMESTAMP
|
|
);
|
|
```
|
|
|
|
**Priority Levels**:
|
|
```
|
|
Priority 1: User-initiated sync (manual refresh)
|
|
Priority 2: Book completion (100% progress)
|
|
Priority 3: Critical annotations (user notes)
|
|
Priority 5: Page turns (immediate sync)
|
|
Priority 7: Checkpoint sync (periodic)
|
|
Priority 10: Background metadata sync
|
|
```
|
|
|
|
**Exponential Backoff**:
|
|
```
|
|
Attempt 1: Immediate
|
|
Attempt 2: 1 minute delay
|
|
Attempt 3: 5 minute delay
|
|
Attempt 4: 15 minute delay
|
|
Attempt 5: 1 hour delay
|
|
After 5 failed attempts: Mark as failed, notify user
|
|
```
|
|
|
|
### Offline Capability
|
|
|
|
**Device-Side Queue** (KOReader example):
|
|
```lua
|
|
-- KOReader plugin code
|
|
local SyncQueue = {
|
|
queue = {},
|
|
max_queue_size = 100,
|
|
|
|
add = function(self, sync_data)
|
|
table.insert(self.queue, {
|
|
data = sync_data,
|
|
timestamp = os.time(),
|
|
retry_count = 0
|
|
})
|
|
self:persist()
|
|
end,
|
|
|
|
sync = function(self)
|
|
for i, item in ipairs(self.queue) do
|
|
local success = self:send_to_server(item.data)
|
|
if success then
|
|
table.remove(self.queue, i)
|
|
else
|
|
item.retry_count = item.retry_count + 1
|
|
if item.retry_count >= 3 then
|
|
self:notify_user(item)
|
|
table.remove(self.queue, i)
|
|
end
|
|
end
|
|
end
|
|
self:persist()
|
|
end,
|
|
|
|
persist = function(self)
|
|
local file = io.open("/mnt/us/sync-queue.json", "w")
|
|
file:write(json.encode(self.queue))
|
|
file:close()
|
|
end
|
|
}
|
|
```
|
|
|
|
### Recovery Scenarios
|
|
|
|
**Scenario 1: Device Offline for Extended Period**
|
|
```
|
|
1. Device queues all changes locally
|
|
2. User marks book as finished (100%)
|
|
3. High-priority sync queued
|
|
4. Device comes back online
|
|
5. Bookmann processes queue in priority order
|
|
6. Finishes book, updates all other devices
|
|
7. User notified of sync completion
|
|
```
|
|
|
|
**Scenario 2: Server Unavailable**
|
|
```
|
|
1. All devices detect server unavailable
|
|
2. Switch to checkpoint mode (batch every 5 min)
|
|
3. Queue changes locally
|
|
4. When server returns:
|
|
- Process all checkpoints
|
|
- Resolve conflicts based on timestamps
|
|
- Notify user of conflicts
|
|
```
|
|
|
|
**Scenario 3: Multiple Devices Offline**
|
|
```
|
|
1. All devices queue changes independently
|
|
2. Server processes queue in order of reconnection
|
|
3. Timestamps determine conflict resolution
|
|
4. User gets conflict summary for manual resolution
|
|
5. Auto-resolution preferences applied to future conflicts
|
|
```
|
|
|
|
---
|
|
|
|
## Real-time Synchronization
|
|
|
|
### WebSocket Architecture
|
|
|
|
**Connection Endpoint**:
|
|
```
|
|
WS /ws/sync?token={device_token}
|
|
```
|
|
|
|
**Message Format**:
|
|
```json
|
|
{
|
|
"type": "progress_update|annotation_update|conflict|sync_complete",
|
|
"timestamp": "2026-01-30T20:00:00Z",
|
|
"data": {
|
|
"book_id": "uuid",
|
|
"progress": {...},
|
|
"annotations": {...}
|
|
},
|
|
"source_device": {
|
|
"id": "device-uuid",
|
|
"name": "My Kobo",
|
|
"type": "kobo"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Broadcast Strategy
|
|
|
|
```
|
|
Device A updates progress
|
|
↓
|
|
Bookmann stores in database
|
|
↓
|
|
WebSocket broadcast to:
|
|
- Web interface (if user logged in)
|
|
- Mobile apps (if connected)
|
|
- Other devices (on next sync)
|
|
↓
|
|
Devices update their display immediately
|
|
```
|
|
|
|
### Connection Management
|
|
|
|
**Device Connection States**:
|
|
```go
|
|
type ConnectionManager struct {
|
|
connections map[string]*DeviceConnection
|
|
broadcast chan BroadcastMessage
|
|
}
|
|
|
|
type DeviceConnection struct {
|
|
DeviceID string
|
|
UserID string
|
|
DeviceType string
|
|
Connected time.Time
|
|
LastPing time.Time
|
|
Send chan Message
|
|
}
|
|
|
|
func (m *ConnectionManager) HandleConnection(conn *websocket.Conn) {
|
|
device := m.authenticate(conn)
|
|
m.connections[device.ID] = device
|
|
|
|
// Send initial state
|
|
device.Send <- m.getInitialState(device.UserID)
|
|
|
|
// Handle messages
|
|
for {
|
|
msg := <-device.Send
|
|
err := conn.WriteJSON(msg)
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Cleanup
|
|
delete(m.connections, device.ID)
|
|
}
|
|
```
|
|
|
|
### Heartbeat & Keep-Alive
|
|
|
|
```
|
|
Client → PING (every 30 seconds)
|
|
Server → PONG
|
|
|
|
If no PING for 90 seconds → Close connection
|
|
```
|
|
|
|
---
|
|
|
|
## Device Onboarding Flow
|
|
|
|
### Step-by-Step Device Setup
|
|
|
|
#### KOReader Setup
|
|
|
|
**1. Generate Registration Request**
|
|
```
|
|
Device sends: POST /api/devices/register
|
|
{
|
|
"device_type": "koreader",
|
|
"device_identifier": "hardware-specific-id",
|
|
"device_name": "My Kindle Paperwhite"
|
|
}
|
|
```
|
|
|
|
**2. Receive Registration Info**
|
|
```
|
|
Server responds: {
|
|
"registration_id": "reg-uuid-123",
|
|
"auth_url": "https://bookmann.example.com/devices/auth/confirm/reg-uuid-123",
|
|
"qr_code": "data:image/png;base64,iVBORw0KG...",
|
|
"expires_in": 300
|
|
}
|
|
```
|
|
|
|
**3. User Approves Device**
|
|
```
|
|
User visits auth URL → Sees:
|
|
┌─────────────────────────────────────┐
|
|
│ Device Registration Request │
|
|
│ │
|
|
│ Device Type: KOReader │
|
|
│ Device Name: My Kindle Paperwhite │
|
|
│ │
|
|
│ Approve this device to sync your │
|
|
│ reading progress and annotations? │
|
|
│ │
|
|
│ [Approve] [Deny] │
|
|
└─────────────────────────────────────┘
|
|
```
|
|
|
|
**4. Configure KOReader**
|
|
```
|
|
In KOReader settings:
|
|
- Calibre sync: ON
|
|
- Server URL: https://bookmann.example.com/api/sync/koreader
|
|
- Wireless sync: ON
|
|
- Sync frequency: Every page turn
|
|
- Auto-sync: ON
|
|
|
|
KOReader stores config in:
|
|
/mnt/us/settings/koreader/calibre.lua
|
|
```
|
|
|
|
**5. Device Polls for Token**
|
|
```
|
|
Device: POST /api/devices/register/status
|
|
{
|
|
"registration_id": "reg-uuid-123"
|
|
}
|
|
|
|
Server responds:
|
|
{
|
|
"status": "approved",
|
|
"auth_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
|
"sync_endpoints": {
|
|
"progress": "https://bookmann.example.com/api/sync/koreader/progress",
|
|
"metadata": "https://bookmann.example.com/api/sync/koreader/metadata",
|
|
"bookmarks": "https://bookmann.example.com/api/sync/koreader/bookmarks"
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Kobo Setup
|
|
|
|
**1-3**: Same as KOReader (registration & approval)
|
|
|
|
**4. Configure Kobo Device**
|
|
```
|
|
In Kobo settings:
|
|
- Sync server: https://bookmann.example.com/api/sync/kobo
|
|
- Auto-sync: ON
|
|
- Sync frequency: Every 5 minutes
|
|
|
|
Kobo stores config in:
|
|
/mnt/onboard/.kobo/Kobo/Kobo eReader.conf
|
|
[Sync]
|
|
ServerURL=https://bookmann.example.com/api/sync/kobo
|
|
AutoSyncEnabled=true
|
|
SyncFrequency=5
|
|
```
|
|
|
|
**5. Device Syncs**
|
|
```
|
|
Kobo sends initial sync:
|
|
POST /api/sync/kobo/library
|
|
Authorization: Bearer {token}
|
|
x-kobo-device: {"DeviceId":"...", "Model":"Kobo Clara"}
|
|
|
|
Server responds with library and current sync state
|
|
```
|
|
|
|
### Device Discovery (Optional)
|
|
|
|
**mDNS/Bonjour for Local Network**:
|
|
```
|
|
Bookmann broadcasts: _bookmann-sync._tcp.local
|
|
Port: 8765
|
|
TXT: "path=/api/sync", "version=1.0"
|
|
|
|
Devices discover automatically
|
|
User confirms connection
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation Phases
|
|
|
|
### Phase 1: Universal Progress System (Weeks 1-4)
|
|
|
|
**Week 1: Database Schema**
|
|
- Add format_group columns to media_items
|
|
- Add universal progress columns to reading_progress
|
|
- Create device registry tables
|
|
- Create sync_queue tables
|
|
- Create sync_conflicts tables
|
|
|
|
**Week 2: Format Detection & Progress Conversion**
|
|
- Implement format detection function
|
|
- Build progress conversion engine
|
|
- Update all existing progress to new format
|
|
- Create migration scripts
|
|
|
|
**Week 3: Core Progress APIs**
|
|
- GET /api/progress/{book_uuid}
|
|
- POST /api/progress/{book_uuid}
|
|
- Progress conversion logic
|
|
- Format-specific display logic
|
|
|
|
**Week 4: Testing & Validation**
|
|
- Unit tests for conversion engine
|
|
- Integration tests for progress APIs
|
|
- Test with existing data
|
|
- Performance testing
|
|
|
|
**Deliverables**:
|
|
- Universal progress tracking working
|
|
- All existing progress converted
|
|
- Format-aware display in web UI
|
|
|
|
### Phase 2: Device Management & Auth (Weeks 5-6)
|
|
|
|
**Week 5: Device Registration**
|
|
- Device registry implementation
|
|
- Registration endpoints
|
|
- Web-based approval flow
|
|
- QR code generation
|
|
- Device management UI
|
|
|
|
**Week 6: Authentication**
|
|
- Device token generation
|
|
- Token validation middleware
|
|
- Permission system
|
|
- Rate limiting per device
|
|
- Revocation system
|
|
|
|
**Deliverables**:
|
|
- Users can register devices
|
|
- Devices authenticate via web flow
|
|
- Device management UI complete
|
|
|
|
### Phase 3: KOReader Integration (Weeks 7-9)
|
|
|
|
**Week 7: KOReader Protocol**
|
|
- Implement calibre-compatible endpoints
|
|
- Progress sync endpoint
|
|
- Metadata sync endpoint
|
|
- Bookmark/highlight sync
|
|
|
|
**Week 8: Bidirectional Sync**
|
|
- Bookmann → KOReader sync
|
|
- KOReader → Bookmann sync
|
|
- Format conversion for KOReader
|
|
- Error handling & retry logic
|
|
|
|
**Week 9: Real-time Updates**
|
|
- WebSocket implementation
|
|
- Progress broadcast system
|
|
- Connection management
|
|
- Testing with actual KOReader devices
|
|
|
|
**Deliverables**:
|
|
- Full KOReader wireless sync
|
|
- Real-time progress updates
|
|
- Tested on actual devices
|
|
|
|
### Phase 4: Kobo Integration (Weeks 10-12)
|
|
|
|
**Week 10: Kobo Protocol**
|
|
- Reverse-engineer Kobo sync API
|
|
- Implement Kobo-compatible endpoints
|
|
- Markup sync endpoint
|
|
- Library sync endpoint
|
|
|
|
**Week 11: Bidirectional Sync**
|
|
- Bookmann → Kobo sync
|
|
- Kobo → Bookmann sync
|
|
- Format conversion for Kobo
|
|
- Annotation sync
|
|
|
|
**Week 12: Testing & Validation**
|
|
- Test on actual Kobo devices
|
|
- Performance testing
|
|
- Error handling
|
|
- Documentation
|
|
|
|
**Deliverables**:
|
|
- Full Kobo wireless sync
|
|
- Tested on Kobo Clara/Naura/etc.
|
|
- Device setup documentation
|
|
|
|
### Phase 5: Conflict Resolution (Weeks 13-14)
|
|
|
|
**Week 13: Conflict Detection**
|
|
- Automatic conflict detection
|
|
- Conflict storage
|
|
- Conflict notifications
|
|
- Conflict listing API
|
|
|
|
**Week 14: Resolution System**
|
|
- Side-by-side conflict UI
|
|
- Resolution endpoints
|
|
- Auto-resolution preferences
|
|
- Merge logic
|
|
|
|
**Deliverables**:
|
|
- Complete conflict resolution system
|
|
- User-friendly conflict UI
|
|
- Auto-resolution preferences
|
|
|
|
### Phase 6: Backup & Recovery (Weeks 15-16)
|
|
|
|
**Week 15: Sync Queue**
|
|
- Queue implementation
|
|
- Priority system
|
|
- Retry logic with exponential backoff
|
|
- Device-side queue (KOReader plugin)
|
|
|
|
**Week 16: Offline Support**
|
|
- Checkpoint sync mode
|
|
- Offline detection
|
|
- Recovery scenarios
|
|
- Progress merge logic
|
|
|
|
**Deliverables**:
|
|
- Robust offline sync
|
|
- Recovery from extended offline
|
|
- Queue management UI
|
|
|
|
### Phase 7: Polish & Documentation (Weeks 17-18)
|
|
|
|
**Week 17: Performance & Security**
|
|
- Performance optimization
|
|
- Security audit
|
|
- Rate limiting tuning
|
|
- Load testing
|
|
|
|
**Week 18: Documentation & Release**
|
|
- User documentation
|
|
- Developer documentation
|
|
- Device setup guides
|
|
- API documentation
|
|
- Release preparation
|
|
|
|
**Deliverables**:
|
|
- Production-ready system
|
|
- Complete documentation
|
|
- Device setup guides
|
|
- API reference
|
|
|
|
---
|
|
|
|
## Testing Requirements
|
|
|
|
### Unit Tests
|
|
|
|
**Format Detection**:
|
|
```go
|
|
TestFormatDetection_Epub()
|
|
TestFormatDetection_Mobi()
|
|
TestFormatDetection_Pdf()
|
|
TestFormatDetection_Cbz()
|
|
TestFormatDetection_Cbr()
|
|
TestFormatDetection_UnknownFormat()
|
|
```
|
|
|
|
**Progress Conversion**:
|
|
```go
|
|
TestProgressConversion_ReflowableToFixed()
|
|
TestProgressConversion_FixedToReflowable()
|
|
TestProgressConversion_ComicToReflowable()
|
|
TestProgressConversion_Precision()
|
|
TestProgressConversion_EdgeCases()
|
|
```
|
|
|
|
**Conflict Detection**:
|
|
```go
|
|
TestConflictDetection_SameTimestamp()
|
|
TestConflictDetection_DifferentDevices()
|
|
TestConflictDetection_SmallDifference()
|
|
TestConflictDetection_LargeDifference()
|
|
```
|
|
|
|
### Integration Tests
|
|
|
|
**Sync Flow**:
|
|
```go
|
|
TestSyncFlow_KOReaderToBookmann()
|
|
TestSyncFlow_BookmannToKOReader()
|
|
TestSyncFlow_KoboToBookmann()
|
|
TestSyncFlow_Bidirectional()
|
|
TestSyncFlow_MultipleDevices()
|
|
```
|
|
|
|
**Conflict Resolution**:
|
|
```go
|
|
TestConflictResolution_AutoResolve()
|
|
TestConflictResolution_ManualResolve()
|
|
TestConflictResolution_Merge()
|
|
TestConflictResolution_UserPreference()
|
|
```
|
|
|
|
**Offline Recovery**:
|
|
```go
|
|
TestOfflineRecovery_QueueProcessing()
|
|
TestOfflineRecovery_PriorityOrder()
|
|
TestOfflineRecovery_ExponentialBackoff()
|
|
TestOfflineRecovery_ExtendedOffline()
|
|
```
|
|
|
|
### End-to-End Tests
|
|
|
|
**User Scenarios**:
|
|
```go
|
|
TestScenario_ReadOnKOReaderContinueOnWeb()
|
|
TestScenario_ReadOnKoboContinueOnMobile()
|
|
TestScenario_MultipleDevicesConflict()
|
|
TestScenario_ExtendedOfflineSync()
|
|
TestScenario_BookCompletionAcrossDevices()
|
|
```
|
|
|
|
### Performance Tests
|
|
|
|
**Load Testing**:
|
|
```
|
|
- 1000 concurrent sync requests
|
|
- 100 devices syncing simultaneously
|
|
- 10,000 progress updates per minute
|
|
- WebSocket connection stability under load
|
|
```
|
|
|
|
**Database Performance**:
|
|
```
|
|
- Query performance with 1M+ progress records
|
|
- Index effectiveness
|
|
- Transaction throughput
|
|
- Conflict detection performance
|
|
```
|
|
|
|
---
|
|
|
|
## Performance Considerations
|
|
|
|
### Database Optimization
|
|
|
|
**Indexes**:
|
|
```sql
|
|
-- Critical indexes for sync performance
|
|
CREATE INDEX idx_reading_progress_media_user
|
|
ON reading_progress(media_item_id, user_id);
|
|
|
|
CREATE INDEX idx_reading_progress_last_sync
|
|
ON reading_progress(last_sync_timestamp DESC)
|
|
WHERE last_sync_timestamp > NOW() - INTERVAL '30 days';
|
|
|
|
CREATE INDEX idx_sync_queue_status_priority
|
|
ON sync_queue(status, priority)
|
|
WHERE status != 'completed';
|
|
|
|
CREATE INDEX idx_devices_active
|
|
ON devices(user_id, sync_enabled)
|
|
WHERE sync_enabled = true;
|
|
```
|
|
|
|
**Partitioning**:
|
|
```sql
|
|
-- Partition reading_history by month
|
|
CREATE TABLE reading_history_2026_01
|
|
PARTITION OF reading_history
|
|
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
|
|
```
|
|
|
|
### Caching Strategy
|
|
|
|
**Redis Cache**:
|
|
```
|
|
- Device authentication tokens (TTL: 24 hours)
|
|
- Active sync sessions (TTL: 1 hour)
|
|
- Progress hot data (TTL: 5 minutes)
|
|
- Conflict data (TTL: 1 hour)
|
|
```
|
|
|
|
**Cache Invalidation**:
|
|
```
|
|
- Progress update → invalidate progress cache
|
|
- New conflict → broadcast to all clients
|
|
- Device auth revoked → invalidate token cache
|
|
```
|
|
|
|
### Connection Pooling
|
|
|
|
**Database Connections**:
|
|
```
|
|
- Max connections: 100
|
|
- Min connections: 10
|
|
- Connection timeout: 30 seconds
|
|
- Query timeout: 10 seconds
|
|
```
|
|
|
|
**WebSocket Connections**:
|
|
```
|
|
- Max concurrent connections: 1000
|
|
- Heartbeat interval: 30 seconds
|
|
- Connection timeout: 90 seconds
|
|
- Message buffer size: 1000 messages
|
|
```
|
|
|
|
### Rate Limiting
|
|
|
|
**Per-Device Limits**:
|
|
```
|
|
- Sync requests: 60/minute
|
|
- Progress updates: 120/minute
|
|
- Metadata requests: 30/minute
|
|
- WebSocket messages: 300/minute
|
|
```
|
|
|
|
**Per-User Limits**:
|
|
```
|
|
- Total requests: 300/minute
|
|
- Conflict resolutions: 10/minute
|
|
- Device registrations: 5/hour
|
|
```
|
|
|
|
---
|
|
|
|
## Conclusion
|
|
|
|
This implementation guide provides a complete blueprint for creating a universal cross-platform reading synchronization system that:
|
|
|
|
1. **Tracks progress optimally** for each format (reflowable, fixed, comics)
|
|
2. **Syncs wirelessly** with KOReader and Kobo devices
|
|
3. **Resolves conflicts** intelligently with user control
|
|
4. **Handles offline scenarios** gracefully with queueing
|
|
5. **Updates in real-time** via WebSocket broadcasts
|
|
6. **Authenticates securely** via web interface (no device API keys)
|
|
7. **Scales efficiently** with proper caching and optimization
|
|
|
|
**Result**: Users get an Amazon Kindle-like experience - pick up any device, continue reading exactly where they left off, with all annotations synchronized - completely self-hosted and open-source.
|
|
|
|
---
|
|
|
|
## Appendix: Example Data Migrations
|
|
|
|
### Migrate Existing Page-Based Progress
|
|
|
|
```sql
|
|
-- Convert existing progress to new format
|
|
UPDATE reading_progress
|
|
SET
|
|
percentage = CASE
|
|
WHEN total_pages > 0 THEN current_page::FLOAT / total_pages::FLOAT
|
|
ELSE 0
|
|
END,
|
|
character_offset = current_page * 500, -- Rough estimate
|
|
chapter = CASE
|
|
WHEN current_page < 100 THEN 1
|
|
WHEN current_page < 200 THEN 2
|
|
ELSE 3
|
|
END,
|
|
chapter_progress = 0.5,
|
|
last_sync_source = 'migration',
|
|
last_sync_timestamp = NOW()
|
|
WHERE percentage IS NULL;
|
|
```
|
|
|
|
### Update Media Items with Format Groups
|
|
|
|
```sql
|
|
-- Set format groups based on file extensions
|
|
UPDATE media_items
|
|
SET
|
|
format_group = CASE
|
|
WHEN file_path LIKE '%.epub' THEN 'reflowable'
|
|
WHEN file_path LIKE '%.mobi' THEN 'reflowable'
|
|
WHEN file_path LIKE '%.pdf' THEN 'fixed_layout'
|
|
WHEN file_path LIKE '%.cbz' THEN 'comic_archive'
|
|
WHEN file_path LIKE '%.cbr' THEN 'comic_archive'
|
|
ELSE 'reflowable' -- Default
|
|
END,
|
|
is_reflowable = CASE
|
|
WHEN file_path LIKE '%.pdf' THEN FALSE
|
|
WHEN file_path LIKE '%.cbz' THEN FALSE
|
|
WHEN file_path LIKE '%.cbr' THEN FALSE
|
|
ELSE TRUE
|
|
END
|
|
WHERE format_group IS NULL OR format_group = '';
|
|
```
|
|
|
|
---
|
|
|
|
**Document Version**: 1.0
|
|
**Last Updated**: 2026-01-30
|
|
**Maintained By**: Bookmann Development Team
|
|
**Status**: Ready for Implementation |