@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @boldvideo/bold-js
|
|
2
2
|
|
|
3
|
+
## 1.16.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 06c8abc: Add Viewers API for managing external users and tracking video progress
|
|
8
|
+
|
|
9
|
+
New methods on the `viewers` namespace:
|
|
10
|
+
|
|
11
|
+
- `viewers.list()` - List all viewers
|
|
12
|
+
- `viewers.get(id)` - Get a viewer by ID
|
|
13
|
+
- `viewers.lookup({ externalId } | { email })` - Find viewer by external ID or email
|
|
14
|
+
- `viewers.create(data)` - Create a new viewer
|
|
15
|
+
- `viewers.update(id, data)` - Update a viewer
|
|
16
|
+
- `viewers.listProgress(viewerId, options?)` - List progress for a viewer
|
|
17
|
+
- `viewers.getProgress(viewerId, videoId)` - Get progress for a video
|
|
18
|
+
- `viewers.saveProgress(viewerId, videoId, data)` - Save/update progress
|
|
19
|
+
|
|
20
|
+
Also fixes `camelizeKeys` to preserve user-defined trait keys (e.g., `company_name` stays as-is instead of becoming `companyName`).
|
|
21
|
+
|
|
3
22
|
## 1.15.2
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -101,6 +101,65 @@ settings.menuItems.forEach(item => {
|
|
|
101
101
|
|
|
102
102
|
---
|
|
103
103
|
|
|
104
|
+
## Viewers API
|
|
105
|
+
|
|
106
|
+
Manage external users and track their video watch progress. Ideal for course platforms integrating with Bold Video.
|
|
107
|
+
|
|
108
|
+
### Viewer Management
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
// Create a viewer (e.g., when user signs up)
|
|
112
|
+
const { data: viewer } = await bold.viewers.create({
|
|
113
|
+
name: 'John Doe',
|
|
114
|
+
externalId: 'user_123', // Your platform's user ID
|
|
115
|
+
email: 'john@example.com',
|
|
116
|
+
traits: { plan: 'pro', company_name: 'Acme Inc' }
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Find viewer by external ID (common for syncing users)
|
|
120
|
+
const { data: viewer } = await bold.viewers.lookup({ externalId: 'user_123' });
|
|
121
|
+
|
|
122
|
+
// Or find by email
|
|
123
|
+
const { data: viewer } = await bold.viewers.lookup({ email: 'john@example.com' });
|
|
124
|
+
|
|
125
|
+
// Update viewer
|
|
126
|
+
await bold.viewers.update(viewer.id, {
|
|
127
|
+
traits: { plan: 'enterprise' } // Note: traits are replaced, not merged
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// List all viewers
|
|
131
|
+
const { data: viewers } = await bold.viewers.list();
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Progress Tracking
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
// Save progress as video plays (call every 5-10 seconds)
|
|
138
|
+
await bold.viewers.saveProgress(viewerId, videoId, {
|
|
139
|
+
currentTime: 120, // seconds
|
|
140
|
+
duration: 600 // total video duration
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Mark video complete by setting currentTime = duration
|
|
144
|
+
await bold.viewers.saveProgress(viewerId, videoId, {
|
|
145
|
+
currentTime: 600,
|
|
146
|
+
duration: 600
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Get progress for a specific video
|
|
150
|
+
const { data: progress } = await bold.viewers.getProgress(viewerId, videoId);
|
|
151
|
+
console.log(`${progress.percentage}% complete`);
|
|
152
|
+
|
|
153
|
+
// List all progress for a viewer (e.g., for a course dashboard)
|
|
154
|
+
const { data: progress, meta } = await bold.viewers.listProgress(viewerId, {
|
|
155
|
+
collectionId: 'course-collection-id', // Filter to a course
|
|
156
|
+
completed: false // Only in-progress videos
|
|
157
|
+
});
|
|
158
|
+
console.log(`Completed ${meta.completed} of ${meta.total} videos`);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
104
163
|
## AI Methods
|
|
105
164
|
|
|
106
165
|
All AI methods support both streaming (default) and non-streaming modes.
|
|
@@ -294,7 +353,11 @@ import type {
|
|
|
294
353
|
Recommendation,
|
|
295
354
|
Conversation,
|
|
296
355
|
ConversationMessage,
|
|
297
|
-
Source
|
|
356
|
+
Source,
|
|
357
|
+
Viewer,
|
|
358
|
+
ViewerProgress,
|
|
359
|
+
ViewerLookupParams,
|
|
360
|
+
ListProgressOptions
|
|
298
361
|
} from '@boldvideo/bold-js';
|
|
299
362
|
```
|
|
300
363
|
|
package/dist/index.cjs
CHANGED
|
@@ -32,19 +32,21 @@ var src_exports = {};
|
|
|
32
32
|
__export(src_exports, {
|
|
33
33
|
DEFAULT_API_BASE_URL: () => DEFAULT_API_BASE_URL,
|
|
34
34
|
DEFAULT_INTERNAL_API_BASE_URL: () => DEFAULT_INTERNAL_API_BASE_URL,
|
|
35
|
+
ViewerAPIError: () => ViewerAPIError,
|
|
35
36
|
createClient: () => createClient
|
|
36
37
|
});
|
|
37
38
|
module.exports = __toCommonJS(src_exports);
|
|
38
39
|
|
|
39
40
|
// src/lib/client.ts
|
|
40
|
-
var
|
|
41
|
+
var import_axios2 = __toESM(require("axios"), 1);
|
|
41
42
|
|
|
42
43
|
// src/util/camelize.ts
|
|
43
44
|
var isPlainObject = (value) => value !== null && typeof value === "object" && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
44
45
|
var snakeToCamel = (key) => key.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
45
|
-
function camelizeKeys(input) {
|
|
46
|
+
function camelizeKeys(input, options = {}) {
|
|
47
|
+
const preserve = new Set(options.preserveKeys ?? []);
|
|
46
48
|
if (Array.isArray(input)) {
|
|
47
|
-
return input.map((item) => camelizeKeys(item));
|
|
49
|
+
return input.map((item) => camelizeKeys(item, options));
|
|
48
50
|
}
|
|
49
51
|
if (!isPlainObject(input)) {
|
|
50
52
|
return input;
|
|
@@ -52,7 +54,11 @@ function camelizeKeys(input) {
|
|
|
52
54
|
const out = {};
|
|
53
55
|
for (const [rawKey, value] of Object.entries(input)) {
|
|
54
56
|
const key = snakeToCamel(rawKey);
|
|
55
|
-
|
|
57
|
+
if (preserve.has(rawKey) || preserve.has(key)) {
|
|
58
|
+
out[key] = value;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
out[key] = camelizeKeys(value, options);
|
|
56
62
|
}
|
|
57
63
|
return out;
|
|
58
64
|
}
|
|
@@ -137,6 +143,154 @@ function fetchPlaylist(client) {
|
|
|
137
143
|
};
|
|
138
144
|
}
|
|
139
145
|
|
|
146
|
+
// src/lib/viewers.ts
|
|
147
|
+
var import_axios = require("axios");
|
|
148
|
+
var VIEWER_CAMELIZE_OPTIONS = { preserveKeys: ["traits"] };
|
|
149
|
+
var ViewerAPIError = class extends Error {
|
|
150
|
+
constructor(method, url, error) {
|
|
151
|
+
var __super = (...args) => {
|
|
152
|
+
super(...args);
|
|
153
|
+
};
|
|
154
|
+
if (error instanceof import_axios.AxiosError) {
|
|
155
|
+
const status = error.response?.status;
|
|
156
|
+
const message = error.response?.data?.error || error.message;
|
|
157
|
+
__super(`${method} ${url} failed (${status}): ${message}`);
|
|
158
|
+
this.status = status;
|
|
159
|
+
this.originalError = error;
|
|
160
|
+
} else if (error instanceof Error) {
|
|
161
|
+
__super(`${method} ${url} failed: ${error.message}`);
|
|
162
|
+
this.originalError = error;
|
|
163
|
+
} else {
|
|
164
|
+
__super(`${method} ${url} failed: ${String(error)}`);
|
|
165
|
+
}
|
|
166
|
+
this.name = "ViewerAPIError";
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
async function get2(client, url, options) {
|
|
170
|
+
try {
|
|
171
|
+
const res = await client.get(url);
|
|
172
|
+
return camelizeKeys(res.data, options);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
throw new ViewerAPIError("GET", url, error);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function post(client, url, data, options) {
|
|
178
|
+
try {
|
|
179
|
+
const res = await client.post(url, data);
|
|
180
|
+
return camelizeKeys(res.data, options);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
throw new ViewerAPIError("POST", url, error);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async function patch(client, url, data, options) {
|
|
186
|
+
try {
|
|
187
|
+
const res = await client.patch(url, data);
|
|
188
|
+
return camelizeKeys(res.data, options);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
throw new ViewerAPIError("PATCH", url, error);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function fetchViewers(client) {
|
|
194
|
+
return async () => {
|
|
195
|
+
return get2(client, "viewers", VIEWER_CAMELIZE_OPTIONS);
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function fetchViewer(client) {
|
|
199
|
+
return async (id) => {
|
|
200
|
+
if (!id)
|
|
201
|
+
throw new Error("Viewer ID is required");
|
|
202
|
+
return get2(client, `viewers/${id}`, VIEWER_CAMELIZE_OPTIONS);
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function lookupViewer(client) {
|
|
206
|
+
return async (params) => {
|
|
207
|
+
const qs = new URLSearchParams();
|
|
208
|
+
if ("externalId" in params && params.externalId) {
|
|
209
|
+
qs.set("external_id", params.externalId);
|
|
210
|
+
}
|
|
211
|
+
if ("email" in params && params.email) {
|
|
212
|
+
qs.set("email", params.email);
|
|
213
|
+
}
|
|
214
|
+
if (!qs.toString()) {
|
|
215
|
+
throw new Error("Either externalId or email is required");
|
|
216
|
+
}
|
|
217
|
+
return get2(client, `viewers/lookup?${qs.toString()}`, VIEWER_CAMELIZE_OPTIONS);
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function createViewer(client) {
|
|
221
|
+
return async (data) => {
|
|
222
|
+
if (!data.name)
|
|
223
|
+
throw new Error("Viewer name is required");
|
|
224
|
+
return post(client, "viewers", {
|
|
225
|
+
viewer: {
|
|
226
|
+
name: data.name,
|
|
227
|
+
email: data.email,
|
|
228
|
+
external_id: data.externalId,
|
|
229
|
+
traits: data.traits
|
|
230
|
+
}
|
|
231
|
+
}, VIEWER_CAMELIZE_OPTIONS);
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function updateViewer(client) {
|
|
235
|
+
return async (id, data) => {
|
|
236
|
+
if (!id)
|
|
237
|
+
throw new Error("Viewer ID is required");
|
|
238
|
+
const body = {};
|
|
239
|
+
if (data.name !== void 0)
|
|
240
|
+
body.name = data.name;
|
|
241
|
+
if (data.email !== void 0)
|
|
242
|
+
body.email = data.email;
|
|
243
|
+
if (data.externalId !== void 0)
|
|
244
|
+
body.external_id = data.externalId;
|
|
245
|
+
if (data.traits !== void 0)
|
|
246
|
+
body.traits = data.traits;
|
|
247
|
+
return patch(client, `viewers/${id}`, { viewer: body }, VIEWER_CAMELIZE_OPTIONS);
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function fetchViewerProgress(client) {
|
|
251
|
+
return async (viewerId, options) => {
|
|
252
|
+
if (!viewerId)
|
|
253
|
+
throw new Error("Viewer ID is required");
|
|
254
|
+
const params = new URLSearchParams();
|
|
255
|
+
if (options?.completed !== void 0)
|
|
256
|
+
params.set("completed", String(options.completed));
|
|
257
|
+
if (options?.collectionId)
|
|
258
|
+
params.set("collection_id", options.collectionId);
|
|
259
|
+
const query = params.toString();
|
|
260
|
+
const url = query ? `viewers/${viewerId}/progress?${query}` : `viewers/${viewerId}/progress`;
|
|
261
|
+
return get2(client, url);
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function fetchProgress(client) {
|
|
265
|
+
return async (viewerId, videoId) => {
|
|
266
|
+
if (!viewerId)
|
|
267
|
+
throw new Error("Viewer ID is required");
|
|
268
|
+
if (!videoId)
|
|
269
|
+
throw new Error("Video ID is required");
|
|
270
|
+
return get2(client, `viewers/${viewerId}/progress/${videoId}`);
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function saveProgress(client) {
|
|
274
|
+
return async (viewerId, videoId, data) => {
|
|
275
|
+
if (!viewerId)
|
|
276
|
+
throw new Error("Viewer ID is required");
|
|
277
|
+
if (!videoId)
|
|
278
|
+
throw new Error("Video ID is required");
|
|
279
|
+
if (!Number.isFinite(data.currentTime) || data.currentTime < 0) {
|
|
280
|
+
throw new Error("currentTime must be a non-negative number");
|
|
281
|
+
}
|
|
282
|
+
if (!Number.isFinite(data.duration) || data.duration <= 0) {
|
|
283
|
+
throw new Error("duration must be a positive number");
|
|
284
|
+
}
|
|
285
|
+
return post(client, `viewers/${viewerId}/progress/${videoId}`, {
|
|
286
|
+
progress: {
|
|
287
|
+
current_time: data.currentTime,
|
|
288
|
+
duration: data.duration
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
140
294
|
// src/util/throttle.ts
|
|
141
295
|
var throttle = (fn, delay) => {
|
|
142
296
|
let wait = false;
|
|
@@ -438,7 +592,7 @@ function createClient(apiKey, options = {}) {
|
|
|
438
592
|
};
|
|
439
593
|
let apiClient;
|
|
440
594
|
try {
|
|
441
|
-
apiClient =
|
|
595
|
+
apiClient = import_axios2.default.create(apiClientOptions);
|
|
442
596
|
} catch (error) {
|
|
443
597
|
console.error("Error creating API client", error);
|
|
444
598
|
throw error;
|
|
@@ -459,6 +613,16 @@ function createClient(apiKey, options = {}) {
|
|
|
459
613
|
list: fetchPlaylists(apiClient),
|
|
460
614
|
get: fetchPlaylist(apiClient)
|
|
461
615
|
},
|
|
616
|
+
viewers: {
|
|
617
|
+
list: fetchViewers(apiClient),
|
|
618
|
+
get: fetchViewer(apiClient),
|
|
619
|
+
lookup: lookupViewer(apiClient),
|
|
620
|
+
create: createViewer(apiClient),
|
|
621
|
+
update: updateViewer(apiClient),
|
|
622
|
+
listProgress: fetchViewerProgress(apiClient),
|
|
623
|
+
getProgress: fetchProgress(apiClient),
|
|
624
|
+
saveProgress: saveProgress(apiClient)
|
|
625
|
+
},
|
|
462
626
|
ai: createAI(aiConfig),
|
|
463
627
|
trackEvent: trackEvent(apiClient, userId, { debug }),
|
|
464
628
|
trackPageView: trackPageView(apiClient, userId, { debug })
|
|
@@ -468,5 +632,6 @@ function createClient(apiKey, options = {}) {
|
|
|
468
632
|
0 && (module.exports = {
|
|
469
633
|
DEFAULT_API_BASE_URL,
|
|
470
634
|
DEFAULT_INTERNAL_API_BASE_URL,
|
|
635
|
+
ViewerAPIError,
|
|
471
636
|
createClient
|
|
472
637
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -390,6 +390,89 @@ interface Conversation {
|
|
|
390
390
|
createdAt: string;
|
|
391
391
|
updatedAt: string;
|
|
392
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* Viewer represents an external user from a course platform
|
|
395
|
+
*/
|
|
396
|
+
type Viewer = {
|
|
397
|
+
id: string;
|
|
398
|
+
name: string;
|
|
399
|
+
email?: string;
|
|
400
|
+
externalId?: string;
|
|
401
|
+
/** Key-value metadata. Keys must start with letter/underscore, contain only alphanumeric/underscore */
|
|
402
|
+
traits?: Record<string, unknown>;
|
|
403
|
+
insertedAt: string;
|
|
404
|
+
updatedAt: string;
|
|
405
|
+
};
|
|
406
|
+
/**
|
|
407
|
+
* Progress record for a viewer-video pair
|
|
408
|
+
*/
|
|
409
|
+
type ViewerProgress = {
|
|
410
|
+
id: string;
|
|
411
|
+
viewerId: string;
|
|
412
|
+
videoId: string;
|
|
413
|
+
/** Current playback position in seconds */
|
|
414
|
+
currentTime: number;
|
|
415
|
+
/** Total video duration in seconds */
|
|
416
|
+
duration: number;
|
|
417
|
+
/** Calculated: (currentTime / duration) * 100 */
|
|
418
|
+
percentage: number;
|
|
419
|
+
completed: boolean;
|
|
420
|
+
completedAt?: string;
|
|
421
|
+
insertedAt: string;
|
|
422
|
+
updatedAt: string;
|
|
423
|
+
};
|
|
424
|
+
/**
|
|
425
|
+
* Options for listing viewer progress
|
|
426
|
+
*/
|
|
427
|
+
type ListProgressOptions = {
|
|
428
|
+
/** Filter by completion status */
|
|
429
|
+
completed?: boolean;
|
|
430
|
+
/** Filter to videos in a specific collection */
|
|
431
|
+
collectionId?: string;
|
|
432
|
+
};
|
|
433
|
+
/**
|
|
434
|
+
* Data for creating a new viewer
|
|
435
|
+
*/
|
|
436
|
+
type CreateViewerData = {
|
|
437
|
+
/** Display name (required) */
|
|
438
|
+
name: string;
|
|
439
|
+
/** Email address for lookup */
|
|
440
|
+
email?: string;
|
|
441
|
+
/** Your platform's user ID for lookup */
|
|
442
|
+
externalId?: string;
|
|
443
|
+
/** Key-value metadata. Keys must start with letter/underscore, contain only alphanumeric/underscore */
|
|
444
|
+
traits?: Record<string, unknown>;
|
|
445
|
+
};
|
|
446
|
+
/**
|
|
447
|
+
* Data for updating an existing viewer
|
|
448
|
+
*/
|
|
449
|
+
type UpdateViewerData = {
|
|
450
|
+
name?: string;
|
|
451
|
+
email?: string;
|
|
452
|
+
externalId?: string;
|
|
453
|
+
/** Note: traits are replaced entirely, not merged */
|
|
454
|
+
traits?: Record<string, unknown>;
|
|
455
|
+
};
|
|
456
|
+
/**
|
|
457
|
+
* Data for saving video progress
|
|
458
|
+
*/
|
|
459
|
+
type SaveProgressData = {
|
|
460
|
+
/** Current playback position in seconds (must be non-negative) */
|
|
461
|
+
currentTime: number;
|
|
462
|
+
/** Total video duration in seconds (must be positive) */
|
|
463
|
+
duration: number;
|
|
464
|
+
};
|
|
465
|
+
/**
|
|
466
|
+
* Metadata returned with progress list
|
|
467
|
+
*/
|
|
468
|
+
type ProgressListMeta = {
|
|
469
|
+
/** Total number of progress records */
|
|
470
|
+
total: number;
|
|
471
|
+
/** Number of completed videos */
|
|
472
|
+
completed: number;
|
|
473
|
+
/** Number of in-progress videos */
|
|
474
|
+
inProgress: number;
|
|
475
|
+
};
|
|
393
476
|
|
|
394
477
|
/**
|
|
395
478
|
* AI client interface for type-safe method overloading
|
|
@@ -503,6 +586,19 @@ interface AIClient {
|
|
|
503
586
|
getConversation(conversationId: string): Promise<Conversation>;
|
|
504
587
|
}
|
|
505
588
|
|
|
589
|
+
declare class ViewerAPIError extends Error {
|
|
590
|
+
readonly status?: number;
|
|
591
|
+
readonly originalError?: Error;
|
|
592
|
+
constructor(method: string, url: string, error: unknown);
|
|
593
|
+
}
|
|
594
|
+
type ViewerLookupParams = {
|
|
595
|
+
externalId: string;
|
|
596
|
+
email?: never;
|
|
597
|
+
} | {
|
|
598
|
+
email: string;
|
|
599
|
+
externalId?: never;
|
|
600
|
+
};
|
|
601
|
+
|
|
506
602
|
type ClientOptions = {
|
|
507
603
|
baseURL?: string;
|
|
508
604
|
debug?: boolean;
|
|
@@ -531,6 +627,33 @@ declare function createClient(apiKey: string, options?: ClientOptions): {
|
|
|
531
627
|
data: Playlist;
|
|
532
628
|
}>;
|
|
533
629
|
};
|
|
630
|
+
viewers: {
|
|
631
|
+
list: () => Promise<{
|
|
632
|
+
data: Viewer[];
|
|
633
|
+
}>;
|
|
634
|
+
get: (id: string) => Promise<{
|
|
635
|
+
data: Viewer;
|
|
636
|
+
}>;
|
|
637
|
+
lookup: (params: ViewerLookupParams) => Promise<{
|
|
638
|
+
data: Viewer;
|
|
639
|
+
}>;
|
|
640
|
+
create: (data: CreateViewerData) => Promise<{
|
|
641
|
+
data: Viewer;
|
|
642
|
+
}>;
|
|
643
|
+
update: (id: string, data: UpdateViewerData) => Promise<{
|
|
644
|
+
data: Viewer;
|
|
645
|
+
}>;
|
|
646
|
+
listProgress: (viewerId: string, options?: ListProgressOptions | undefined) => Promise<{
|
|
647
|
+
data: ViewerProgress[];
|
|
648
|
+
meta: ProgressListMeta;
|
|
649
|
+
}>;
|
|
650
|
+
getProgress: (viewerId: string, videoId: string) => Promise<{
|
|
651
|
+
data: ViewerProgress;
|
|
652
|
+
}>;
|
|
653
|
+
saveProgress: (viewerId: string, videoId: string, data: SaveProgressData) => Promise<{
|
|
654
|
+
data: ViewerProgress;
|
|
655
|
+
}>;
|
|
656
|
+
};
|
|
534
657
|
ai: AIClient;
|
|
535
658
|
trackEvent: (video: any, event: Event) => void;
|
|
536
659
|
trackPageView: (title: string) => void;
|
|
@@ -545,4 +668,4 @@ declare const DEFAULT_API_BASE_URL = "https://app.boldvideo.io/api/v1/";
|
|
|
545
668
|
*/
|
|
546
669
|
declare const DEFAULT_INTERNAL_API_BASE_URL = "https://app.boldvideo.io/i/v1/";
|
|
547
670
|
|
|
548
|
-
export { AIContextMessage, AIEvent, AIResponse, AIUsage, Account, AccountAI, AnalyticsProvider, AskOptions, AssistantConfig, ChatOptions, Citation, ClientOptions, Conversation, ConversationMessage, ConversationMetadata, CustomRedirect, DEFAULT_API_BASE_URL, DEFAULT_INTERNAL_API_BASE_URL, MenuItem, Playlist, Portal, PortalDisplay, PortalHero, PortalLayout, PortalNavigation, PortalTheme, RecommendOptions, RecommendResponse, Recommendation, RecommendationVideo, RecommendationsOptions, RecommendationsResponse, SearchOptions, Segment, Settings, Source, ThemeColors, ThemeConfig, Video, VideoAttachment, VideoDownloadUrls, VideoMetadata, VideoSubtitles, VideoTranscript, createClient };
|
|
671
|
+
export { AIContextMessage, AIEvent, AIResponse, AIUsage, Account, AccountAI, AnalyticsProvider, AskOptions, AssistantConfig, ChatOptions, Citation, ClientOptions, Conversation, ConversationMessage, ConversationMetadata, CreateViewerData, CustomRedirect, DEFAULT_API_BASE_URL, DEFAULT_INTERNAL_API_BASE_URL, ListProgressOptions, MenuItem, Playlist, Portal, PortalDisplay, PortalHero, PortalLayout, PortalNavigation, PortalTheme, ProgressListMeta, RecommendOptions, RecommendResponse, Recommendation, RecommendationVideo, RecommendationsOptions, RecommendationsResponse, SaveProgressData, SearchOptions, Segment, Settings, Source, ThemeColors, ThemeConfig, UpdateViewerData, Video, VideoAttachment, VideoDownloadUrls, VideoMetadata, VideoSubtitles, VideoTranscript, Viewer, ViewerAPIError, ViewerLookupParams, ViewerProgress, createClient };
|
package/dist/index.js
CHANGED
|
@@ -4,9 +4,10 @@ import axios from "axios";
|
|
|
4
4
|
// src/util/camelize.ts
|
|
5
5
|
var isPlainObject = (value) => value !== null && typeof value === "object" && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
6
6
|
var snakeToCamel = (key) => key.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
7
|
-
function camelizeKeys(input) {
|
|
7
|
+
function camelizeKeys(input, options = {}) {
|
|
8
|
+
const preserve = new Set(options.preserveKeys ?? []);
|
|
8
9
|
if (Array.isArray(input)) {
|
|
9
|
-
return input.map((item) => camelizeKeys(item));
|
|
10
|
+
return input.map((item) => camelizeKeys(item, options));
|
|
10
11
|
}
|
|
11
12
|
if (!isPlainObject(input)) {
|
|
12
13
|
return input;
|
|
@@ -14,7 +15,11 @@ function camelizeKeys(input) {
|
|
|
14
15
|
const out = {};
|
|
15
16
|
for (const [rawKey, value] of Object.entries(input)) {
|
|
16
17
|
const key = snakeToCamel(rawKey);
|
|
17
|
-
|
|
18
|
+
if (preserve.has(rawKey) || preserve.has(key)) {
|
|
19
|
+
out[key] = value;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
out[key] = camelizeKeys(value, options);
|
|
18
23
|
}
|
|
19
24
|
return out;
|
|
20
25
|
}
|
|
@@ -99,6 +104,154 @@ function fetchPlaylist(client) {
|
|
|
99
104
|
};
|
|
100
105
|
}
|
|
101
106
|
|
|
107
|
+
// src/lib/viewers.ts
|
|
108
|
+
import { AxiosError } from "axios";
|
|
109
|
+
var VIEWER_CAMELIZE_OPTIONS = { preserveKeys: ["traits"] };
|
|
110
|
+
var ViewerAPIError = class extends Error {
|
|
111
|
+
constructor(method, url, error) {
|
|
112
|
+
var __super = (...args) => {
|
|
113
|
+
super(...args);
|
|
114
|
+
};
|
|
115
|
+
if (error instanceof AxiosError) {
|
|
116
|
+
const status = error.response?.status;
|
|
117
|
+
const message = error.response?.data?.error || error.message;
|
|
118
|
+
__super(`${method} ${url} failed (${status}): ${message}`);
|
|
119
|
+
this.status = status;
|
|
120
|
+
this.originalError = error;
|
|
121
|
+
} else if (error instanceof Error) {
|
|
122
|
+
__super(`${method} ${url} failed: ${error.message}`);
|
|
123
|
+
this.originalError = error;
|
|
124
|
+
} else {
|
|
125
|
+
__super(`${method} ${url} failed: ${String(error)}`);
|
|
126
|
+
}
|
|
127
|
+
this.name = "ViewerAPIError";
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
async function get2(client, url, options) {
|
|
131
|
+
try {
|
|
132
|
+
const res = await client.get(url);
|
|
133
|
+
return camelizeKeys(res.data, options);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
throw new ViewerAPIError("GET", url, error);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function post(client, url, data, options) {
|
|
139
|
+
try {
|
|
140
|
+
const res = await client.post(url, data);
|
|
141
|
+
return camelizeKeys(res.data, options);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
throw new ViewerAPIError("POST", url, error);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function patch(client, url, data, options) {
|
|
147
|
+
try {
|
|
148
|
+
const res = await client.patch(url, data);
|
|
149
|
+
return camelizeKeys(res.data, options);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
throw new ViewerAPIError("PATCH", url, error);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function fetchViewers(client) {
|
|
155
|
+
return async () => {
|
|
156
|
+
return get2(client, "viewers", VIEWER_CAMELIZE_OPTIONS);
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function fetchViewer(client) {
|
|
160
|
+
return async (id) => {
|
|
161
|
+
if (!id)
|
|
162
|
+
throw new Error("Viewer ID is required");
|
|
163
|
+
return get2(client, `viewers/${id}`, VIEWER_CAMELIZE_OPTIONS);
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function lookupViewer(client) {
|
|
167
|
+
return async (params) => {
|
|
168
|
+
const qs = new URLSearchParams();
|
|
169
|
+
if ("externalId" in params && params.externalId) {
|
|
170
|
+
qs.set("external_id", params.externalId);
|
|
171
|
+
}
|
|
172
|
+
if ("email" in params && params.email) {
|
|
173
|
+
qs.set("email", params.email);
|
|
174
|
+
}
|
|
175
|
+
if (!qs.toString()) {
|
|
176
|
+
throw new Error("Either externalId or email is required");
|
|
177
|
+
}
|
|
178
|
+
return get2(client, `viewers/lookup?${qs.toString()}`, VIEWER_CAMELIZE_OPTIONS);
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function createViewer(client) {
|
|
182
|
+
return async (data) => {
|
|
183
|
+
if (!data.name)
|
|
184
|
+
throw new Error("Viewer name is required");
|
|
185
|
+
return post(client, "viewers", {
|
|
186
|
+
viewer: {
|
|
187
|
+
name: data.name,
|
|
188
|
+
email: data.email,
|
|
189
|
+
external_id: data.externalId,
|
|
190
|
+
traits: data.traits
|
|
191
|
+
}
|
|
192
|
+
}, VIEWER_CAMELIZE_OPTIONS);
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function updateViewer(client) {
|
|
196
|
+
return async (id, data) => {
|
|
197
|
+
if (!id)
|
|
198
|
+
throw new Error("Viewer ID is required");
|
|
199
|
+
const body = {};
|
|
200
|
+
if (data.name !== void 0)
|
|
201
|
+
body.name = data.name;
|
|
202
|
+
if (data.email !== void 0)
|
|
203
|
+
body.email = data.email;
|
|
204
|
+
if (data.externalId !== void 0)
|
|
205
|
+
body.external_id = data.externalId;
|
|
206
|
+
if (data.traits !== void 0)
|
|
207
|
+
body.traits = data.traits;
|
|
208
|
+
return patch(client, `viewers/${id}`, { viewer: body }, VIEWER_CAMELIZE_OPTIONS);
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function fetchViewerProgress(client) {
|
|
212
|
+
return async (viewerId, options) => {
|
|
213
|
+
if (!viewerId)
|
|
214
|
+
throw new Error("Viewer ID is required");
|
|
215
|
+
const params = new URLSearchParams();
|
|
216
|
+
if (options?.completed !== void 0)
|
|
217
|
+
params.set("completed", String(options.completed));
|
|
218
|
+
if (options?.collectionId)
|
|
219
|
+
params.set("collection_id", options.collectionId);
|
|
220
|
+
const query = params.toString();
|
|
221
|
+
const url = query ? `viewers/${viewerId}/progress?${query}` : `viewers/${viewerId}/progress`;
|
|
222
|
+
return get2(client, url);
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function fetchProgress(client) {
|
|
226
|
+
return async (viewerId, videoId) => {
|
|
227
|
+
if (!viewerId)
|
|
228
|
+
throw new Error("Viewer ID is required");
|
|
229
|
+
if (!videoId)
|
|
230
|
+
throw new Error("Video ID is required");
|
|
231
|
+
return get2(client, `viewers/${viewerId}/progress/${videoId}`);
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function saveProgress(client) {
|
|
235
|
+
return async (viewerId, videoId, data) => {
|
|
236
|
+
if (!viewerId)
|
|
237
|
+
throw new Error("Viewer ID is required");
|
|
238
|
+
if (!videoId)
|
|
239
|
+
throw new Error("Video ID is required");
|
|
240
|
+
if (!Number.isFinite(data.currentTime) || data.currentTime < 0) {
|
|
241
|
+
throw new Error("currentTime must be a non-negative number");
|
|
242
|
+
}
|
|
243
|
+
if (!Number.isFinite(data.duration) || data.duration <= 0) {
|
|
244
|
+
throw new Error("duration must be a positive number");
|
|
245
|
+
}
|
|
246
|
+
return post(client, `viewers/${viewerId}/progress/${videoId}`, {
|
|
247
|
+
progress: {
|
|
248
|
+
current_time: data.currentTime,
|
|
249
|
+
duration: data.duration
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
102
255
|
// src/util/throttle.ts
|
|
103
256
|
var throttle = (fn, delay) => {
|
|
104
257
|
let wait = false;
|
|
@@ -421,6 +574,16 @@ function createClient(apiKey, options = {}) {
|
|
|
421
574
|
list: fetchPlaylists(apiClient),
|
|
422
575
|
get: fetchPlaylist(apiClient)
|
|
423
576
|
},
|
|
577
|
+
viewers: {
|
|
578
|
+
list: fetchViewers(apiClient),
|
|
579
|
+
get: fetchViewer(apiClient),
|
|
580
|
+
lookup: lookupViewer(apiClient),
|
|
581
|
+
create: createViewer(apiClient),
|
|
582
|
+
update: updateViewer(apiClient),
|
|
583
|
+
listProgress: fetchViewerProgress(apiClient),
|
|
584
|
+
getProgress: fetchProgress(apiClient),
|
|
585
|
+
saveProgress: saveProgress(apiClient)
|
|
586
|
+
},
|
|
424
587
|
ai: createAI(aiConfig),
|
|
425
588
|
trackEvent: trackEvent(apiClient, userId, { debug }),
|
|
426
589
|
trackPageView: trackPageView(apiClient, userId, { debug })
|
|
@@ -429,5 +592,6 @@ function createClient(apiKey, options = {}) {
|
|
|
429
592
|
export {
|
|
430
593
|
DEFAULT_API_BASE_URL,
|
|
431
594
|
DEFAULT_INTERNAL_API_BASE_URL,
|
|
595
|
+
ViewerAPIError,
|
|
432
596
|
createClient
|
|
433
597
|
};
|