@boldvideo/bold-js 1.15.2 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +64 -1
- package/dist/index.cjs +170 -5
- package/dist/index.d.ts +124 -1
- package/dist/index.js +167 -3
- package/docs/plans/2026-01-22-docs-enhance-llms-txt-for-ai-consumption-plan.md +393 -0
- package/docs/plans/2026-01-23-feat-viewers-api-sdk-implementation-plan.md +380 -0
- package/llms.txt +59 -1
- package/package.json +1 -1
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "feat: Add Viewers & Progress Tracking API to SDK"
|
|
3
|
+
type: feat
|
|
4
|
+
date: 2026-01-23
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Add Viewers & Progress Tracking API to SDK
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
Implement the Viewers API in the bold-js SDK to enable course platforms to sync external users and track their video watch progress. Adds a new `bold.viewers` namespace with CRUD operations and flat progress methods.
|
|
12
|
+
|
|
13
|
+
## Problem Statement / Motivation
|
|
14
|
+
|
|
15
|
+
Course platforms integrating Bold Video need to:
|
|
16
|
+
1. Sync their user database with Bold's viewer system
|
|
17
|
+
2. Track which videos users have watched and their progress
|
|
18
|
+
3. Mark videos as complete for course completion tracking
|
|
19
|
+
4. Query progress filtered by collection (course)
|
|
20
|
+
|
|
21
|
+
The backend API exists (`/api/v1/viewers/*`), but the SDK doesn't expose it yet.
|
|
22
|
+
|
|
23
|
+
## Proposed Solution
|
|
24
|
+
|
|
25
|
+
Add a new `viewers` module following the existing `fetchers.ts` pattern:
|
|
26
|
+
- Curried functions that take axios client
|
|
27
|
+
- Flat method naming (no nested namespaces)
|
|
28
|
+
- Use existing `Response<T>` wrapper type
|
|
29
|
+
- Minimal new types - only what's necessary
|
|
30
|
+
|
|
31
|
+
### API Surface
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
const bold = createClient(apiKey);
|
|
35
|
+
|
|
36
|
+
// Viewer CRUD
|
|
37
|
+
bold.viewers.list()
|
|
38
|
+
bold.viewers.get(id)
|
|
39
|
+
bold.viewers.lookup({ externalId }) // or { email }
|
|
40
|
+
bold.viewers.create({ name, email?, externalId?, traits? })
|
|
41
|
+
bold.viewers.update(id, { name?, email?, externalId?, traits? })
|
|
42
|
+
|
|
43
|
+
// Progress tracking (flat methods on viewers namespace)
|
|
44
|
+
bold.viewers.listProgress(viewerId, options?)
|
|
45
|
+
bold.viewers.getProgress(viewerId, videoId)
|
|
46
|
+
bold.viewers.saveProgress(viewerId, videoId, { currentTime, duration })
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Deferred Features (YAGNI)
|
|
50
|
+
|
|
51
|
+
These can be added when requested:
|
|
52
|
+
- `viewers.delete(id)` - rare admin action
|
|
53
|
+
- `viewers.deleteProgress(viewerId, videoId)` - rare admin action
|
|
54
|
+
- `viewers.completeProgress(viewerId, videoId)` - use `saveProgress` with full duration
|
|
55
|
+
- Progress list pagination
|
|
56
|
+
|
|
57
|
+
### Implemented (originally deferred)
|
|
58
|
+
- `viewers.lookup({ externalId } | { email })` - **Added**: essential for course platforms syncing users
|
|
59
|
+
|
|
60
|
+
## Technical Approach
|
|
61
|
+
|
|
62
|
+
### Architecture
|
|
63
|
+
|
|
64
|
+
Follow the established `fetchers.ts` pattern (not the `ai.ts` factory pattern):
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
/src
|
|
68
|
+
├── lib/
|
|
69
|
+
│ ├── viewers.ts # NEW: viewer fetcher functions
|
|
70
|
+
│ ├── types.ts # ADD: Viewer, ViewerProgress types
|
|
71
|
+
│ └── client.ts # MODIFY: add viewers namespace
|
|
72
|
+
└── index.ts # MODIFY: export new types
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Implementation
|
|
76
|
+
|
|
77
|
+
**Tasks:**
|
|
78
|
+
- [x] Add viewer types to `/src/lib/types.ts`
|
|
79
|
+
- [x] Create `/src/lib/viewers.ts` with fetcher functions
|
|
80
|
+
- [x] Wire up viewers in `/src/lib/client.ts`
|
|
81
|
+
- [x] Export types from `/src/index.ts`
|
|
82
|
+
|
|
83
|
+
**File: `/src/lib/types.ts` (additions ~30 lines)**
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// ============================================
|
|
87
|
+
// Viewers API Types
|
|
88
|
+
// ============================================
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Viewer represents an external user from a course platform
|
|
92
|
+
*/
|
|
93
|
+
export type Viewer = {
|
|
94
|
+
id: string;
|
|
95
|
+
name: string;
|
|
96
|
+
email?: string;
|
|
97
|
+
externalId?: string;
|
|
98
|
+
/** Key-value metadata. Keys must start with letter/underscore, contain only alphanumeric/underscore */
|
|
99
|
+
traits?: Record<string, unknown>;
|
|
100
|
+
insertedAt: string;
|
|
101
|
+
updatedAt: string;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Progress record for a viewer-video pair
|
|
106
|
+
*/
|
|
107
|
+
export type ViewerProgress = {
|
|
108
|
+
id: string;
|
|
109
|
+
viewerId: string;
|
|
110
|
+
videoId: string;
|
|
111
|
+
/** Current playback position in seconds */
|
|
112
|
+
currentTime: number;
|
|
113
|
+
/** Total video duration in seconds */
|
|
114
|
+
duration: number;
|
|
115
|
+
/** Calculated: (currentTime / duration) * 100 */
|
|
116
|
+
percentage: number;
|
|
117
|
+
completed: boolean;
|
|
118
|
+
completedAt?: string;
|
|
119
|
+
insertedAt: string;
|
|
120
|
+
updatedAt: string;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Options for listing viewer progress
|
|
125
|
+
*/
|
|
126
|
+
export type ListProgressOptions = {
|
|
127
|
+
/** Filter by completion status */
|
|
128
|
+
completed?: boolean;
|
|
129
|
+
/** Filter to videos in a specific collection */
|
|
130
|
+
collectionId?: string;
|
|
131
|
+
};
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**File: `/src/lib/viewers.ts` (~90 lines)**
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
import { AxiosInstance } from "axios";
|
|
138
|
+
import { camelizeKeys } from "../util/camelize";
|
|
139
|
+
import type { Viewer, ViewerProgress, ListProgressOptions } from "./types";
|
|
140
|
+
|
|
141
|
+
type Response<T> = { data: T };
|
|
142
|
+
type ApiClient = AxiosInstance;
|
|
143
|
+
|
|
144
|
+
// Re-use the get helper pattern from fetchers.ts
|
|
145
|
+
async function get<T>(client: ApiClient, url: string): Promise<T> {
|
|
146
|
+
try {
|
|
147
|
+
const res = await client.get(url);
|
|
148
|
+
return camelizeKeys(res.data) as T;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error(`Error fetching from ${url}`, error);
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function post<T>(client: ApiClient, url: string, data?: Record<string, unknown>): Promise<T> {
|
|
156
|
+
try {
|
|
157
|
+
const res = await client.post(url, data);
|
|
158
|
+
return camelizeKeys(res.data) as T;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error(`Error posting to ${url}`, error);
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function patch<T>(client: ApiClient, url: string, data: Record<string, unknown>): Promise<T> {
|
|
166
|
+
try {
|
|
167
|
+
const res = await client.patch(url, data);
|
|
168
|
+
return camelizeKeys(res.data) as T;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error(`Error patching ${url}`, error);
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// --- Viewer CRUD ---
|
|
176
|
+
|
|
177
|
+
export function fetchViewers(client: ApiClient) {
|
|
178
|
+
return async () => {
|
|
179
|
+
return get<{ viewers: Viewer[] }>(client, 'viewers');
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function fetchViewer(client: ApiClient) {
|
|
184
|
+
return async (id: string) => {
|
|
185
|
+
if (!id) throw new Error('Viewer ID is required');
|
|
186
|
+
return get<{ viewer: Viewer }>(client, `viewers/${id}`);
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function createViewer(client: ApiClient) {
|
|
191
|
+
return async (data: { name: string; email?: string; externalId?: string; traits?: Record<string, unknown> }) => {
|
|
192
|
+
if (!data.name) throw new Error('Viewer name is required');
|
|
193
|
+
return post<{ viewer: Viewer }>(client, 'viewers', {
|
|
194
|
+
viewer: {
|
|
195
|
+
name: data.name,
|
|
196
|
+
email: data.email,
|
|
197
|
+
external_id: data.externalId,
|
|
198
|
+
traits: data.traits,
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function updateViewer(client: ApiClient) {
|
|
205
|
+
return async (id: string, data: { name?: string; email?: string; externalId?: string; traits?: Record<string, unknown> }) => {
|
|
206
|
+
if (!id) throw new Error('Viewer ID is required');
|
|
207
|
+
const body: Record<string, unknown> = {};
|
|
208
|
+
if (data.name !== undefined) body.name = data.name;
|
|
209
|
+
if (data.email !== undefined) body.email = data.email;
|
|
210
|
+
if (data.externalId !== undefined) body.external_id = data.externalId;
|
|
211
|
+
if (data.traits !== undefined) body.traits = data.traits;
|
|
212
|
+
return patch<{ viewer: Viewer }>(client, `viewers/${id}`, { viewer: body });
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// --- Progress Tracking (flat methods) ---
|
|
217
|
+
|
|
218
|
+
export function fetchViewerProgress(client: ApiClient) {
|
|
219
|
+
return async (viewerId: string, options?: ListProgressOptions) => {
|
|
220
|
+
if (!viewerId) throw new Error('Viewer ID is required');
|
|
221
|
+
const params = new URLSearchParams();
|
|
222
|
+
if (options?.completed !== undefined) params.set('completed', String(options.completed));
|
|
223
|
+
if (options?.collectionId) params.set('collection_id', options.collectionId);
|
|
224
|
+
const query = params.toString();
|
|
225
|
+
const url = query ? `viewers/${viewerId}/progress?${query}` : `viewers/${viewerId}/progress`;
|
|
226
|
+
return get<{ progress: ViewerProgress[]; meta: { total: number; completed: number; inProgress: number } }>(client, url);
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function fetchProgress(client: ApiClient) {
|
|
231
|
+
return async (viewerId: string, videoId: string) => {
|
|
232
|
+
if (!viewerId) throw new Error('Viewer ID is required');
|
|
233
|
+
if (!videoId) throw new Error('Video ID is required');
|
|
234
|
+
return get<{ progress: ViewerProgress }>(client, `viewers/${viewerId}/progress/${videoId}`);
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function saveProgress(client: ApiClient) {
|
|
239
|
+
return async (viewerId: string, videoId: string, data: { currentTime: number; duration: number }) => {
|
|
240
|
+
if (!viewerId) throw new Error('Viewer ID is required');
|
|
241
|
+
if (!videoId) throw new Error('Video ID is required');
|
|
242
|
+
if (data.currentTime === undefined) throw new Error('currentTime is required');
|
|
243
|
+
if (data.duration === undefined) throw new Error('duration is required');
|
|
244
|
+
return post<{ progress: ViewerProgress }>(client, `viewers/${viewerId}/progress/${videoId}`, {
|
|
245
|
+
progress: {
|
|
246
|
+
current_time: data.currentTime,
|
|
247
|
+
duration: data.duration,
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**File: `/src/lib/client.ts` (changes)**
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
import {
|
|
258
|
+
fetchViewers,
|
|
259
|
+
fetchViewer,
|
|
260
|
+
createViewer,
|
|
261
|
+
updateViewer,
|
|
262
|
+
fetchViewerProgress,
|
|
263
|
+
fetchProgress,
|
|
264
|
+
saveProgress,
|
|
265
|
+
} from './viewers';
|
|
266
|
+
|
|
267
|
+
// In createClient(), add to return object:
|
|
268
|
+
return {
|
|
269
|
+
settings: fetchSettings(apiClient),
|
|
270
|
+
videos: { ... },
|
|
271
|
+
playlists: { ... },
|
|
272
|
+
viewers: {
|
|
273
|
+
list: fetchViewers(apiClient),
|
|
274
|
+
get: fetchViewer(apiClient),
|
|
275
|
+
create: createViewer(apiClient),
|
|
276
|
+
update: updateViewer(apiClient),
|
|
277
|
+
listProgress: fetchViewerProgress(apiClient),
|
|
278
|
+
getProgress: fetchProgress(apiClient),
|
|
279
|
+
saveProgress: saveProgress(apiClient),
|
|
280
|
+
},
|
|
281
|
+
ai: createAI(aiConfig),
|
|
282
|
+
trackEvent: ...,
|
|
283
|
+
trackPageView: ...,
|
|
284
|
+
};
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**File: `/src/index.ts` (additions)**
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
export type {
|
|
291
|
+
// ... existing exports ...
|
|
292
|
+
|
|
293
|
+
// Viewers API
|
|
294
|
+
Viewer,
|
|
295
|
+
ViewerProgress,
|
|
296
|
+
ListProgressOptions,
|
|
297
|
+
} from "./lib/types";
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Acceptance Criteria
|
|
301
|
+
|
|
302
|
+
### Functional Requirements
|
|
303
|
+
|
|
304
|
+
- [ ] `bold.viewers.list()` returns all viewers
|
|
305
|
+
- [ ] `bold.viewers.get(id)` returns a single viewer by UUID
|
|
306
|
+
- [ ] `bold.viewers.create(data)` creates a new viewer with name (required) and optional fields
|
|
307
|
+
- [ ] `bold.viewers.update(id, data)` updates viewer fields (traits replaced, not merged)
|
|
308
|
+
- [ ] `bold.viewers.listProgress(viewerId)` returns all progress for a viewer
|
|
309
|
+
- [ ] `bold.viewers.listProgress(viewerId, { completed: true })` filters by completion
|
|
310
|
+
- [ ] `bold.viewers.listProgress(viewerId, { collectionId })` filters by collection
|
|
311
|
+
- [ ] `bold.viewers.getProgress(viewerId, videoId)` returns progress for a video
|
|
312
|
+
- [ ] `bold.viewers.saveProgress(viewerId, videoId, data)` creates or updates progress
|
|
313
|
+
|
|
314
|
+
### Non-Functional Requirements
|
|
315
|
+
|
|
316
|
+
- [ ] All responses are camelCased (consistent with existing SDK)
|
|
317
|
+
- [ ] Request bodies use snake_case for API compatibility (manual transformation)
|
|
318
|
+
- [ ] Trait keys are preserved as-is (not transformed)
|
|
319
|
+
- [ ] TypeScript types are accurate and exported
|
|
320
|
+
- [ ] Errors include descriptive messages with context
|
|
321
|
+
- [ ] No new runtime dependencies added
|
|
322
|
+
- [ ] Follows existing `fetchers.ts` pattern
|
|
323
|
+
|
|
324
|
+
### Quality Gates
|
|
325
|
+
|
|
326
|
+
- [ ] `pnpm run lint` passes
|
|
327
|
+
- [ ] `pnpm run build` succeeds
|
|
328
|
+
- [ ] Types are correctly exported and usable by consumers
|
|
329
|
+
|
|
330
|
+
## Example Usage
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
import { createClient } from '@boldvideo/bold-js';
|
|
334
|
+
|
|
335
|
+
const bold = createClient('your-api-key');
|
|
336
|
+
|
|
337
|
+
// 1. Create viewer when user signs up
|
|
338
|
+
const { viewer } = await bold.viewers.create({
|
|
339
|
+
name: 'John Doe',
|
|
340
|
+
externalId: 'user_123',
|
|
341
|
+
email: 'john@example.com',
|
|
342
|
+
traits: { plan: 'pro', company: 'Acme Inc' }
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// 2. Track progress as video plays (call every 5-10 seconds)
|
|
346
|
+
await bold.viewers.saveProgress(viewer.id, 'video-id', {
|
|
347
|
+
currentTime: 120,
|
|
348
|
+
duration: 600
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// 3. Mark video complete by setting currentTime = duration
|
|
352
|
+
await bold.viewers.saveProgress(viewer.id, 'video-id', {
|
|
353
|
+
currentTime: 600,
|
|
354
|
+
duration: 600
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// 4. Get course progress
|
|
358
|
+
const { progress, meta } = await bold.viewers.listProgress(viewer.id, {
|
|
359
|
+
collectionId: 'course-collection-id'
|
|
360
|
+
});
|
|
361
|
+
console.log(`Completed ${meta.completed} of ${meta.total} videos`);
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## Summary of Changes from Original Plan
|
|
365
|
+
|
|
366
|
+
| Original | Revised | Reason |
|
|
367
|
+
|----------|---------|--------|
|
|
368
|
+
| `viewers.progress.list()` nested | `viewers.listProgress()` flat | Reviewer consensus: flat is better |
|
|
369
|
+
| `ViewerResponse`, `ViewersResponse` types | Inline `{ viewer: Viewer }` | Match existing `Response<T>` pattern |
|
|
370
|
+
| `snakeize.ts` utility file | Manual snake_case in request bodies | Simpler, avoid new abstraction |
|
|
371
|
+
| 11 methods | 7 methods | YAGNI: defer delete, lookup, completeProgress |
|
|
372
|
+
| ~290 lines | ~120 lines | 60% reduction per simplicity review |
|
|
373
|
+
| Factory pattern (`createViewers`) | Curried functions | Match `fetchers.ts` pattern |
|
|
374
|
+
|
|
375
|
+
## References
|
|
376
|
+
|
|
377
|
+
- Client factory: `/src/lib/client.ts`
|
|
378
|
+
- Existing fetchers: `/src/lib/fetchers.ts`
|
|
379
|
+
- Type definitions: `/src/lib/types.ts`
|
|
380
|
+
- API Documentation: `notes/bold/viewers-api.md`
|
package/llms.txt
CHANGED
|
@@ -61,6 +61,33 @@ All content methods return `Promise<{ data: T }>`.
|
|
|
61
61
|
- `bold.playlists.list()` - List playlists
|
|
62
62
|
- `bold.playlists.get(id)` - Get playlist with videos
|
|
63
63
|
|
|
64
|
+
## Viewers API
|
|
65
|
+
|
|
66
|
+
Manage external users and track video progress. Returns `Promise<{ data: T }>` (consistent with other SDK methods).
|
|
67
|
+
|
|
68
|
+
### Viewer CRUD
|
|
69
|
+
|
|
70
|
+
- `bold.viewers.list()` - List all viewers
|
|
71
|
+
- `bold.viewers.get(id)` - Get viewer by UUID
|
|
72
|
+
- `bold.viewers.lookup({ externalId })` - Find viewer by external ID
|
|
73
|
+
- `bold.viewers.lookup({ email })` - Find viewer by email
|
|
74
|
+
- `bold.viewers.create({ name, email?, externalId?, traits? })` - Create viewer
|
|
75
|
+
- `bold.viewers.update(id, { name?, email?, externalId?, traits? })` - Update viewer (traits replaced, not merged)
|
|
76
|
+
|
|
77
|
+
### Progress Tracking
|
|
78
|
+
|
|
79
|
+
- `bold.viewers.listProgress(viewerId, { completed?, collectionId? }?)` - List progress records
|
|
80
|
+
- `bold.viewers.getProgress(viewerId, videoId)` - Get progress for specific video
|
|
81
|
+
- `bold.viewers.saveProgress(viewerId, videoId, { currentTime, duration })` - Upsert progress
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// Example: Course platform integration
|
|
85
|
+
const { data: viewer } = await bold.viewers.lookup({ externalId: 'user_123' });
|
|
86
|
+
await bold.viewers.saveProgress(viewer.id, 'video-id', { currentTime: 120, duration: 600 });
|
|
87
|
+
const { data: progress, meta } = await bold.viewers.listProgress(viewer.id, { collectionId: 'course-id' });
|
|
88
|
+
console.log(`Completed ${meta.completed} of ${meta.total} videos`);
|
|
89
|
+
```
|
|
90
|
+
|
|
64
91
|
## AI Methods
|
|
65
92
|
|
|
66
93
|
All AI methods return `AsyncIterable<AIEvent>` (streaming) or `Promise<AIResponse>` (non-streaming).
|
|
@@ -194,9 +221,40 @@ type AIEvent =
|
|
|
194
221
|
}
|
|
195
222
|
```
|
|
196
223
|
|
|
224
|
+
### Viewer
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
{
|
|
228
|
+
id: string;
|
|
229
|
+
name: string;
|
|
230
|
+
email?: string;
|
|
231
|
+
externalId?: string;
|
|
232
|
+
traits?: Record<string, unknown>; // User-defined metadata (keys preserved as-is)
|
|
233
|
+
insertedAt: string;
|
|
234
|
+
updatedAt: string;
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### ViewerProgress
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
{
|
|
242
|
+
id: string;
|
|
243
|
+
viewerId: string;
|
|
244
|
+
videoId: string;
|
|
245
|
+
currentTime: number; // Playback position in seconds
|
|
246
|
+
duration: number; // Video duration in seconds
|
|
247
|
+
percentage: number; // 0-100
|
|
248
|
+
completed: boolean;
|
|
249
|
+
completedAt?: string;
|
|
250
|
+
insertedAt: string;
|
|
251
|
+
updatedAt: string;
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
197
255
|
### Other types exported
|
|
198
256
|
|
|
199
|
-
`Playlist`, `Settings`, `Portal`, `MenuItem`, `AIResponse`, `AIUsage`, `AIContextMessage`, `Conversation`, `ConversationMessage`, `RecommendationsResponse`
|
|
257
|
+
`Playlist`, `Settings`, `Portal`, `MenuItem`, `AIResponse`, `AIUsage`, `AIContextMessage`, `Conversation`, `ConversationMessage`, `RecommendationsResponse`, `Viewer`, `ViewerProgress`, `ViewerLookupParams`, `ListProgressOptions`
|
|
200
258
|
|
|
201
259
|
## Error Handling
|
|
202
260
|
|