@xhub-short/adapters 0.1.0-beta.8 → 1.0.0-beta.21
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/dist/index.d.ts +541 -19
- package/dist/index.js +692 -67
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ var MOCK_VIDEOS = [
|
|
|
4
4
|
// HLS Videos (for testing hls.js integration)
|
|
5
5
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
6
6
|
{
|
|
7
|
+
type: "video",
|
|
7
8
|
id: "video-1",
|
|
8
9
|
source: {
|
|
9
10
|
url: "https://peertube.teknix.services/static/streaming-playlists/hls/dd8de71d-0b75-4677-a1a2-6f60e673bee4/465faffa-6d08-4f34-ae40-691cc904ce7b-master.m3u8",
|
|
@@ -30,6 +31,7 @@ var MOCK_VIDEOS = [
|
|
|
30
31
|
hashtags: ["hls", "streaming", "test"]
|
|
31
32
|
},
|
|
32
33
|
{
|
|
34
|
+
type: "video",
|
|
33
35
|
id: "video-2",
|
|
34
36
|
source: {
|
|
35
37
|
url: "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8",
|
|
@@ -59,6 +61,7 @@ var MOCK_VIDEOS = [
|
|
|
59
61
|
// MP4 Videos
|
|
60
62
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
61
63
|
{
|
|
64
|
+
type: "video",
|
|
62
65
|
id: "video-3",
|
|
63
66
|
source: {
|
|
64
67
|
url: "https://peertube.teknix.services/static/streaming-playlists/hls/ea58b245-b3bf-4958-b2f0-b31f8113d142/83f1bb91-e76a-4dcd-8034-2ec37cb70ead-master.m3u8",
|
|
@@ -85,6 +88,7 @@ var MOCK_VIDEOS = [
|
|
|
85
88
|
hashtags: ["chrome", "blazes"]
|
|
86
89
|
},
|
|
87
90
|
{
|
|
91
|
+
type: "video",
|
|
88
92
|
id: "video-4",
|
|
89
93
|
source: {
|
|
90
94
|
url: "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4",
|
|
@@ -111,6 +115,7 @@ var MOCK_VIDEOS = [
|
|
|
111
115
|
hashtags: ["chrome", "escapes"]
|
|
112
116
|
},
|
|
113
117
|
{
|
|
118
|
+
type: "video",
|
|
114
119
|
id: "video-5",
|
|
115
120
|
source: {
|
|
116
121
|
url: "https://peertube.teknix.services/static/streaming-playlists/hls/003a41a3-25c1-419b-9548-7a1597adc85f/9cc93564-c9c9-4107-ae12-cd22d0db8046-master.m3u8",
|
|
@@ -137,6 +142,7 @@ var MOCK_VIDEOS = [
|
|
|
137
142
|
hashtags: ["chrome", "fun"]
|
|
138
143
|
},
|
|
139
144
|
{
|
|
145
|
+
type: "video",
|
|
140
146
|
id: "video-6",
|
|
141
147
|
source: {
|
|
142
148
|
url: "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerJoyrides.mp4",
|
|
@@ -163,6 +169,7 @@ var MOCK_VIDEOS = [
|
|
|
163
169
|
hashtags: ["adventure", "joyride", "travel"]
|
|
164
170
|
},
|
|
165
171
|
{
|
|
172
|
+
type: "video",
|
|
166
173
|
id: "video-7",
|
|
167
174
|
source: {
|
|
168
175
|
url: "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerMeltdowns.mp4",
|
|
@@ -189,6 +196,7 @@ var MOCK_VIDEOS = [
|
|
|
189
196
|
hashtags: ["satisfying", "icecream", "asmr"]
|
|
190
197
|
},
|
|
191
198
|
{
|
|
199
|
+
type: "video",
|
|
192
200
|
id: "video-8",
|
|
193
201
|
source: {
|
|
194
202
|
url: "https://storage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4",
|
|
@@ -215,6 +223,7 @@ var MOCK_VIDEOS = [
|
|
|
215
223
|
hashtags: ["fantasy", "animation", "sintel", "blender"]
|
|
216
224
|
},
|
|
217
225
|
{
|
|
226
|
+
type: "video",
|
|
218
227
|
id: "video-9",
|
|
219
228
|
source: {
|
|
220
229
|
url: "https://storage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4",
|
|
@@ -241,6 +250,7 @@ var MOCK_VIDEOS = [
|
|
|
241
250
|
hashtags: ["cars", "subaru", "offroad", "review"]
|
|
242
251
|
},
|
|
243
252
|
{
|
|
253
|
+
type: "video",
|
|
244
254
|
id: "video-10",
|
|
245
255
|
source: {
|
|
246
256
|
url: "https://storage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4",
|
|
@@ -267,6 +277,7 @@ var MOCK_VIDEOS = [
|
|
|
267
277
|
hashtags: ["scifi", "drama", "blender", "vfx"]
|
|
268
278
|
},
|
|
269
279
|
{
|
|
280
|
+
type: "video",
|
|
270
281
|
id: "video-11",
|
|
271
282
|
source: {
|
|
272
283
|
url: "https://storage.googleapis.com/gtv-videos-bucket/sample/VolkswagenGTIReview.mp4",
|
|
@@ -293,6 +304,7 @@ var MOCK_VIDEOS = [
|
|
|
293
304
|
hashtags: ["cars", "vw", "gti", "hothatch"]
|
|
294
305
|
},
|
|
295
306
|
{
|
|
307
|
+
type: "video",
|
|
296
308
|
id: "video-12",
|
|
297
309
|
source: {
|
|
298
310
|
url: "https://storage.googleapis.com/gtv-videos-bucket/sample/WeAreGoingOnBullrun.mp4",
|
|
@@ -319,6 +331,7 @@ var MOCK_VIDEOS = [
|
|
|
319
331
|
hashtags: ["rally", "racing", "bullrun", "supercars"]
|
|
320
332
|
},
|
|
321
333
|
{
|
|
334
|
+
type: "video",
|
|
322
335
|
id: "video-13",
|
|
323
336
|
source: {
|
|
324
337
|
url: "https://storage.googleapis.com/gtv-videos-bucket/sample/WhatCarCanYouGetForAGrand.mp4",
|
|
@@ -345,6 +358,7 @@ var MOCK_VIDEOS = [
|
|
|
345
358
|
hashtags: ["budget", "usedcars", "tips", "bargain"]
|
|
346
359
|
},
|
|
347
360
|
{
|
|
361
|
+
type: "video",
|
|
348
362
|
id: "video-14",
|
|
349
363
|
source: {
|
|
350
364
|
url: "https://storage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
|
|
@@ -371,6 +385,7 @@ var MOCK_VIDEOS = [
|
|
|
371
385
|
hashtags: ["bts", "animation", "3d", "making"]
|
|
372
386
|
},
|
|
373
387
|
{
|
|
388
|
+
type: "video",
|
|
374
389
|
id: "video-15",
|
|
375
390
|
source: {
|
|
376
391
|
url: "https://storage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4",
|
|
@@ -397,6 +412,7 @@ var MOCK_VIDEOS = [
|
|
|
397
412
|
hashtags: ["tutorial", "animation", "blender", "makingof"]
|
|
398
413
|
},
|
|
399
414
|
{
|
|
415
|
+
type: "video",
|
|
400
416
|
id: "video-16",
|
|
401
417
|
source: {
|
|
402
418
|
url: "https://storage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4",
|
|
@@ -423,6 +439,7 @@ var MOCK_VIDEOS = [
|
|
|
423
439
|
hashtags: ["characterdesign", "sintel", "tutorial", "art"]
|
|
424
440
|
},
|
|
425
441
|
{
|
|
442
|
+
type: "video",
|
|
426
443
|
id: "video-17",
|
|
427
444
|
source: {
|
|
428
445
|
url: "https://storage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4",
|
|
@@ -449,6 +466,7 @@ var MOCK_VIDEOS = [
|
|
|
449
466
|
hashtags: ["vfx", "breakdown", "compositing", "cgi"]
|
|
450
467
|
},
|
|
451
468
|
{
|
|
469
|
+
type: "video",
|
|
452
470
|
id: "video-18",
|
|
453
471
|
source: {
|
|
454
472
|
url: "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerJoyrides.mp4",
|
|
@@ -475,6 +493,7 @@ var MOCK_VIDEOS = [
|
|
|
475
493
|
hashtags: ["pov", "roadtrip", "travel", "wanderlust"]
|
|
476
494
|
},
|
|
477
495
|
{
|
|
496
|
+
type: "video",
|
|
478
497
|
id: "video-19",
|
|
479
498
|
source: {
|
|
480
499
|
url: "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4",
|
|
@@ -501,6 +520,7 @@ var MOCK_VIDEOS = [
|
|
|
501
520
|
hashtags: ["satisfying", "fire", "asmr", "relaxing"]
|
|
502
521
|
},
|
|
503
522
|
{
|
|
523
|
+
type: "video",
|
|
504
524
|
id: "video-20",
|
|
505
525
|
source: {
|
|
506
526
|
url: "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerMeltdowns.mp4",
|
|
@@ -529,20 +549,20 @@ var MOCK_VIDEOS = [
|
|
|
529
549
|
];
|
|
530
550
|
var MockDataAdapter = class {
|
|
531
551
|
constructor(options = {}) {
|
|
532
|
-
this.
|
|
552
|
+
this.items = options.items ?? options.videos ?? MOCK_VIDEOS;
|
|
533
553
|
this.pageSize = options.pageSize ?? 3;
|
|
534
554
|
this.delay = options.delay ?? 300;
|
|
535
555
|
}
|
|
536
556
|
/**
|
|
537
|
-
* Fetch a page of mock
|
|
557
|
+
* Fetch a page of mock items
|
|
538
558
|
*/
|
|
539
559
|
async fetchFeed(cursor) {
|
|
540
560
|
await this.simulateDelay();
|
|
541
561
|
const offset = cursor ? Number.parseInt(cursor, 10) : 0;
|
|
542
562
|
const start = offset;
|
|
543
563
|
const end = start + this.pageSize;
|
|
544
|
-
const items = this.
|
|
545
|
-
const hasMore = end < this.
|
|
564
|
+
const items = this.items.slice(start, end);
|
|
565
|
+
const hasMore = end < this.items.length;
|
|
546
566
|
const nextCursor = hasMore ? String(end) : null;
|
|
547
567
|
return {
|
|
548
568
|
items,
|
|
@@ -551,15 +571,21 @@ var MockDataAdapter = class {
|
|
|
551
571
|
};
|
|
552
572
|
}
|
|
553
573
|
/**
|
|
554
|
-
* Get a single
|
|
574
|
+
* Get a single content item by ID
|
|
555
575
|
*/
|
|
556
|
-
async
|
|
576
|
+
async getContentDetail(id) {
|
|
557
577
|
await this.simulateDelay();
|
|
558
|
-
const
|
|
559
|
-
if (!
|
|
560
|
-
throw new Error(`
|
|
578
|
+
const item = this.items.find((v) => v.id === id);
|
|
579
|
+
if (!item) {
|
|
580
|
+
throw new Error(`Content not found: ${id}`);
|
|
561
581
|
}
|
|
562
|
-
return
|
|
582
|
+
return item;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* @deprecated Use getContentDetail instead
|
|
586
|
+
*/
|
|
587
|
+
async getVideoDetail(id) {
|
|
588
|
+
return this.getContentDetail(id);
|
|
563
589
|
}
|
|
564
590
|
/**
|
|
565
591
|
* Prefetch videos (no-op for mock)
|
|
@@ -575,6 +601,87 @@ var MockDataAdapter = class {
|
|
|
575
601
|
}
|
|
576
602
|
}
|
|
577
603
|
};
|
|
604
|
+
var MOCK_ITEMS = MOCK_VIDEOS;
|
|
605
|
+
|
|
606
|
+
// src/playlist/MockPlaylistAdapter.ts
|
|
607
|
+
var MOCK_PLAYLISTS = [
|
|
608
|
+
{
|
|
609
|
+
id: "p1",
|
|
610
|
+
title: "Workout Jams",
|
|
611
|
+
description: "High energy tracks for your workout",
|
|
612
|
+
cover: "https://images.unsplash.com/photo-1534438327276-14e5300c3a48?w=400&h=600&fit=crop",
|
|
613
|
+
totalItems: 4,
|
|
614
|
+
items: []
|
|
615
|
+
// Populated on fetchPlaylist
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
id: "p2",
|
|
619
|
+
title: "Chill Vibes",
|
|
620
|
+
description: "Relax and unwind with these lo-fi beats",
|
|
621
|
+
cover: "https://images.unsplash.com/photo-1516280440614-37939bbacd81?w=400&h=600&fit=crop",
|
|
622
|
+
totalItems: 4,
|
|
623
|
+
items: []
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
id: "p3",
|
|
627
|
+
title: "Travel Diaries",
|
|
628
|
+
description: "Explore the world through music and video",
|
|
629
|
+
cover: "https://images.unsplash.com/photo-1476514525535-07fb3b4ae5f1?w=400&h=600&fit=crop",
|
|
630
|
+
totalItems: 4,
|
|
631
|
+
items: []
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
id: "p4",
|
|
635
|
+
title: "Cooking with Chef loct",
|
|
636
|
+
description: "Delicious recipes and kitchen tips",
|
|
637
|
+
cover: "https://images.unsplash.com/photo-1556910103-1c02745aae4d?w=400&h=600&fit=crop",
|
|
638
|
+
totalItems: 4,
|
|
639
|
+
items: []
|
|
640
|
+
}
|
|
641
|
+
];
|
|
642
|
+
var MockPlaylistAdapter = class {
|
|
643
|
+
constructor(options = {}) {
|
|
644
|
+
this.delay = options.delay ?? 300;
|
|
645
|
+
}
|
|
646
|
+
async fetchPlaylist(id) {
|
|
647
|
+
await this.simulateDelay();
|
|
648
|
+
const playlist = MOCK_PLAYLISTS.find((p) => p.id === id);
|
|
649
|
+
if (!playlist) throw new Error(`Playlist ${id} not found`);
|
|
650
|
+
let items = [];
|
|
651
|
+
if (id === "p1") items = MOCK_ITEMS.slice(0, 4);
|
|
652
|
+
else if (id === "p2") items = MOCK_ITEMS.slice(4, 8);
|
|
653
|
+
else if (id === "p3") items = MOCK_ITEMS.slice(8, 12);
|
|
654
|
+
else if (id === "p4") items = MOCK_ITEMS.slice(12, 16);
|
|
655
|
+
return { ...playlist, items };
|
|
656
|
+
}
|
|
657
|
+
async fetchPlaylistCollection(cursor) {
|
|
658
|
+
await this.simulateDelay();
|
|
659
|
+
const offset = cursor ? Number.parseInt(cursor, 10) : 0;
|
|
660
|
+
const limit = 4;
|
|
661
|
+
const items = MOCK_PLAYLISTS.slice(offset, offset + limit);
|
|
662
|
+
const nextOffset = offset + limit;
|
|
663
|
+
const hasMore = nextOffset < MOCK_PLAYLISTS.length;
|
|
664
|
+
const playlists = items.map((p) => ({
|
|
665
|
+
id: p.id,
|
|
666
|
+
title: p.title,
|
|
667
|
+
description: p.description,
|
|
668
|
+
cover: p.cover,
|
|
669
|
+
totalItems: p.totalItems,
|
|
670
|
+
author: { id: "a1", name: "System" },
|
|
671
|
+
updatedAt: "2 days ago"
|
|
672
|
+
}));
|
|
673
|
+
return {
|
|
674
|
+
playlists,
|
|
675
|
+
nextCursor: hasMore ? String(nextOffset) : null,
|
|
676
|
+
hasMore
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
async simulateDelay() {
|
|
680
|
+
if (this.delay > 0) {
|
|
681
|
+
await new Promise((resolve) => setTimeout(resolve, this.delay));
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
};
|
|
578
685
|
|
|
579
686
|
// src/logger/mock.ts
|
|
580
687
|
var LOG_LEVEL_PRIORITY = {
|
|
@@ -1107,9 +1214,9 @@ var MockInteractionAdapter = class {
|
|
|
1107
1214
|
/**
|
|
1108
1215
|
* Report a video
|
|
1109
1216
|
*
|
|
1110
|
-
* @param
|
|
1111
|
-
* @param
|
|
1112
|
-
* @param
|
|
1217
|
+
* @param videoId - ID of the video to report
|
|
1218
|
+
* @param reason - Report reason code
|
|
1219
|
+
* @param description - Optional additional description
|
|
1113
1220
|
*/
|
|
1114
1221
|
async report(_videoId, _reason, _description) {
|
|
1115
1222
|
await this.simulateDelay();
|
|
@@ -1435,27 +1542,23 @@ var MockCommentAdapter = class {
|
|
|
1435
1542
|
await this.simulateNetworkDelay();
|
|
1436
1543
|
if (payload.isReply && payload.parentId) {
|
|
1437
1544
|
const replies = this.replies.get(payload.parentId) ?? [];
|
|
1438
|
-
const filtered = replies.filter((r) => r.id !== payload.
|
|
1545
|
+
const filtered = replies.filter((r) => r.id !== payload.commentId);
|
|
1439
1546
|
this.replies.set(payload.parentId, filtered);
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
break;
|
|
1449
|
-
}
|
|
1547
|
+
const comments = this.comments.get(payload.videoId) ?? [];
|
|
1548
|
+
const idx = comments.findIndex((c) => c.id === payload.parentId);
|
|
1549
|
+
if (idx !== -1 && comments[idx]) {
|
|
1550
|
+
comments[idx] = {
|
|
1551
|
+
...comments[idx],
|
|
1552
|
+
replyCount: Math.max(0, comments[idx].replyCount - 1)
|
|
1553
|
+
};
|
|
1554
|
+
this.comments.set(payload.videoId, comments);
|
|
1450
1555
|
}
|
|
1451
1556
|
} else {
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
break;
|
|
1458
|
-
}
|
|
1557
|
+
const comments = this.comments.get(payload.videoId) ?? [];
|
|
1558
|
+
const filtered = comments.filter((c) => c.id !== payload.commentId);
|
|
1559
|
+
if (filtered.length !== comments.length) {
|
|
1560
|
+
this.comments.set(payload.videoId, filtered);
|
|
1561
|
+
this.replies.delete(payload.commentId);
|
|
1459
1562
|
}
|
|
1460
1563
|
}
|
|
1461
1564
|
}
|
|
@@ -1986,6 +2089,28 @@ var MockPosterLoader = class {
|
|
|
1986
2089
|
// src/preset/adapters/RESTAnalyticsAdapter.ts
|
|
1987
2090
|
var DEFAULT_BATCH_SIZE = 10;
|
|
1988
2091
|
var DEFAULT_FLUSH_INTERVAL = 3e4;
|
|
2092
|
+
var DEFAULT_EVENT_TYPE_MAP = {
|
|
2093
|
+
video_view: "view_start",
|
|
2094
|
+
video_complete: "view_end",
|
|
2095
|
+
scroll: "scroll_pass",
|
|
2096
|
+
like: "like",
|
|
2097
|
+
unlike: "like",
|
|
2098
|
+
// Same API type, different action
|
|
2099
|
+
comment: "comment",
|
|
2100
|
+
share: "share",
|
|
2101
|
+
follow: "follow_creator",
|
|
2102
|
+
unfollow: "follow_creator",
|
|
2103
|
+
// Same API type, different action
|
|
2104
|
+
impression: "view_start"
|
|
2105
|
+
};
|
|
2106
|
+
var DEFAULT_CONTEXT = {
|
|
2107
|
+
getSessionId: () => null,
|
|
2108
|
+
getDeviceType: () => "web",
|
|
2109
|
+
getNetworkType: () => "other",
|
|
2110
|
+
getGeoLocation: () => null,
|
|
2111
|
+
getPositionIndex: () => 0,
|
|
2112
|
+
getScrollSpeed: () => null
|
|
2113
|
+
};
|
|
1989
2114
|
var RESTAnalyticsAdapter = class {
|
|
1990
2115
|
constructor(config) {
|
|
1991
2116
|
// Event queue
|
|
@@ -1995,12 +2120,19 @@ var RESTAnalyticsAdapter = class {
|
|
|
1995
2120
|
// User context
|
|
1996
2121
|
this.userId = null;
|
|
1997
2122
|
this.userProperties = {};
|
|
2123
|
+
// Dedupe: track which videos have been viewed (to avoid duplicate view_start)
|
|
2124
|
+
this.viewedVideos = /* @__PURE__ */ new Set();
|
|
1998
2125
|
this.httpClient = config.httpClient;
|
|
1999
2126
|
this.endpoint = config.endpoints.batch;
|
|
2000
|
-
this.batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
2001
2127
|
this.logger = config.logger;
|
|
2002
|
-
const
|
|
2003
|
-
this.
|
|
2128
|
+
const batchConfig = config.config;
|
|
2129
|
+
this.batchSize = batchConfig?.batchSize ?? config.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
2130
|
+
const flushInterval = batchConfig?.flushInterval ?? config.flushInterval ?? DEFAULT_FLUSH_INTERVAL;
|
|
2131
|
+
this.transform = batchConfig?.transform;
|
|
2132
|
+
this.context = batchConfig?.context ?? DEFAULT_CONTEXT;
|
|
2133
|
+
this.eventTypeMap = batchConfig?.eventTypeMap ?? DEFAULT_EVENT_TYPE_MAP;
|
|
2134
|
+
this.useSendBeacon = batchConfig?.useSendBeacon ?? true;
|
|
2135
|
+
this.flushTimer = setInterval(() => this.flush(), flushInterval);
|
|
2004
2136
|
}
|
|
2005
2137
|
/**
|
|
2006
2138
|
* Track an analytics event
|
|
@@ -2032,19 +2164,14 @@ var RESTAnalyticsAdapter = class {
|
|
|
2032
2164
|
const events = [...this.queue];
|
|
2033
2165
|
this.queue = [];
|
|
2034
2166
|
try {
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
count: events.length
|
|
2038
|
-
});
|
|
2167
|
+
const requestBody = await this.buildRequestBody(events);
|
|
2168
|
+
if (this.trySendBeacon(requestBody)) {
|
|
2039
2169
|
return;
|
|
2040
2170
|
}
|
|
2041
2171
|
await this.httpClient.request({
|
|
2042
2172
|
method: "POST",
|
|
2043
2173
|
path: this.endpoint,
|
|
2044
|
-
body:
|
|
2045
|
-
});
|
|
2046
|
-
this.logger?.debug("[RESTAnalyticsAdapter] Events sent via HTTP", {
|
|
2047
|
-
count: events.length
|
|
2174
|
+
body: requestBody
|
|
2048
2175
|
});
|
|
2049
2176
|
} catch (error) {
|
|
2050
2177
|
if (this.queue.length < this.batchSize * 3) {
|
|
@@ -2054,9 +2181,48 @@ var RESTAnalyticsAdapter = class {
|
|
|
2054
2181
|
}
|
|
2055
2182
|
}
|
|
2056
2183
|
/**
|
|
2057
|
-
*
|
|
2184
|
+
* Build request body from events
|
|
2185
|
+
* Uses custom transform if provided, otherwise SDK default format
|
|
2186
|
+
*/
|
|
2187
|
+
async buildRequestBody(events) {
|
|
2188
|
+
if (!this.transform) {
|
|
2189
|
+
return { events };
|
|
2190
|
+
}
|
|
2191
|
+
const transformedEvents = [];
|
|
2192
|
+
for (const event of events) {
|
|
2193
|
+
try {
|
|
2194
|
+
const eventData = {
|
|
2195
|
+
sdkEventType: event.type,
|
|
2196
|
+
videoId: event.videoId,
|
|
2197
|
+
timestamp: event.timestamp,
|
|
2198
|
+
userId: this.userId,
|
|
2199
|
+
userProperties: this.userProperties,
|
|
2200
|
+
data: event.data
|
|
2201
|
+
};
|
|
2202
|
+
const transformed = await this.transform(eventData, this.context);
|
|
2203
|
+
transformedEvents.push(transformed);
|
|
2204
|
+
} catch (error) {
|
|
2205
|
+
this.logger?.warn("[RESTAnalyticsAdapter] Transform failed for event", {
|
|
2206
|
+
event,
|
|
2207
|
+
error
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
return { events: transformedEvents };
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Track video view duration
|
|
2215
|
+
*
|
|
2216
|
+
* Note: PlayerEngine calls this every second (heartbeat), but for batch analytics
|
|
2217
|
+
* we only need ONE view_start per video. This method deduplicates by videoId.
|
|
2218
|
+
*
|
|
2219
|
+
* If you need heartbeat tracking, use RESTViewTrackingAdapter instead.
|
|
2058
2220
|
*/
|
|
2059
2221
|
trackViewDuration(videoId, duration, totalDuration) {
|
|
2222
|
+
if (this.viewedVideos.has(videoId)) {
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
this.viewedVideos.add(videoId);
|
|
2060
2226
|
this.track({
|
|
2061
2227
|
type: "video_view",
|
|
2062
2228
|
videoId,
|
|
@@ -2068,6 +2234,12 @@ var RESTAnalyticsAdapter = class {
|
|
|
2068
2234
|
}
|
|
2069
2235
|
});
|
|
2070
2236
|
}
|
|
2237
|
+
/**
|
|
2238
|
+
* Clear viewed videos cache (useful for testing or session reset)
|
|
2239
|
+
*/
|
|
2240
|
+
clearViewedVideos() {
|
|
2241
|
+
this.viewedVideos.clear();
|
|
2242
|
+
}
|
|
2071
2243
|
/**
|
|
2072
2244
|
* Track video completion
|
|
2073
2245
|
*/
|
|
@@ -2103,19 +2275,26 @@ var RESTAnalyticsAdapter = class {
|
|
|
2103
2275
|
clearInterval(this.flushTimer);
|
|
2104
2276
|
this.flushTimer = null;
|
|
2105
2277
|
}
|
|
2278
|
+
this.viewedVideos.clear();
|
|
2106
2279
|
this.flush().catch(() => {
|
|
2107
2280
|
});
|
|
2108
2281
|
}
|
|
2109
2282
|
/**
|
|
2110
2283
|
* Try to send via sendBeacon (for reliability on page unload)
|
|
2284
|
+
* Note: sendBeacon shows as "ping" type in Network tab, not "POST"
|
|
2285
|
+
*
|
|
2286
|
+
* Set useSendBeacon: false in config to always use HTTP POST
|
|
2111
2287
|
*/
|
|
2112
|
-
trySendBeacon(
|
|
2288
|
+
trySendBeacon(requestBody) {
|
|
2289
|
+
if (this.useSendBeacon === false) {
|
|
2290
|
+
return false;
|
|
2291
|
+
}
|
|
2113
2292
|
if (typeof navigator === "undefined" || !navigator.sendBeacon) {
|
|
2114
2293
|
return false;
|
|
2115
2294
|
}
|
|
2116
2295
|
try {
|
|
2117
2296
|
const url = this.buildFullUrl();
|
|
2118
|
-
const data = JSON.stringify(
|
|
2297
|
+
const data = JSON.stringify(requestBody);
|
|
2119
2298
|
const blob = new Blob([data], { type: "application/json" });
|
|
2120
2299
|
return navigator.sendBeacon(url, blob);
|
|
2121
2300
|
} catch {
|
|
@@ -2126,7 +2305,23 @@ var RESTAnalyticsAdapter = class {
|
|
|
2126
2305
|
* Build full URL for sendBeacon
|
|
2127
2306
|
*/
|
|
2128
2307
|
buildFullUrl() {
|
|
2129
|
-
|
|
2308
|
+
if (this.endpoint.startsWith("http")) {
|
|
2309
|
+
return this.endpoint;
|
|
2310
|
+
}
|
|
2311
|
+
const baseUrl = this.httpClient.getBaseUrl();
|
|
2312
|
+
return `${baseUrl}${this.endpoint}`;
|
|
2313
|
+
}
|
|
2314
|
+
/**
|
|
2315
|
+
* Get mapped API event type from SDK event type
|
|
2316
|
+
*/
|
|
2317
|
+
getApiEventType(sdkEventType) {
|
|
2318
|
+
return this.eventTypeMap[sdkEventType] ?? sdkEventType;
|
|
2319
|
+
}
|
|
2320
|
+
/**
|
|
2321
|
+
* Get current context (for debugging/testing)
|
|
2322
|
+
*/
|
|
2323
|
+
getContext() {
|
|
2324
|
+
return this.context;
|
|
2130
2325
|
}
|
|
2131
2326
|
};
|
|
2132
2327
|
function createNoOpAnalyticsAdapter() {
|
|
@@ -2260,13 +2455,17 @@ var RESTCommentAdapter = class {
|
|
|
2260
2455
|
}
|
|
2261
2456
|
/**
|
|
2262
2457
|
* Delete a comment or reply
|
|
2458
|
+
* DELETE /reels/:id/comments/:commentId
|
|
2263
2459
|
*/
|
|
2264
2460
|
async deleteComment(payload) {
|
|
2265
2461
|
try {
|
|
2266
2462
|
await this.httpClient.request({
|
|
2267
2463
|
method: "DELETE",
|
|
2268
2464
|
path: this.endpoints.delete,
|
|
2269
|
-
pathParams: {
|
|
2465
|
+
pathParams: {
|
|
2466
|
+
id: payload.videoId,
|
|
2467
|
+
commentId: payload.commentId
|
|
2468
|
+
}
|
|
2270
2469
|
});
|
|
2271
2470
|
} catch (error) {
|
|
2272
2471
|
this.logger?.error("[RESTCommentAdapter] deleteComment failed", error);
|
|
@@ -2457,10 +2656,10 @@ var RESTDataAdapter = class {
|
|
|
2457
2656
|
const feedData = this.transforms.feedResponse(response);
|
|
2458
2657
|
const items = feedData.items.map((item) => {
|
|
2459
2658
|
try {
|
|
2460
|
-
return this.transforms.
|
|
2659
|
+
return this.transforms.contentItem(item);
|
|
2461
2660
|
} catch (error) {
|
|
2462
|
-
this.logger?.error("[RESTDataAdapter] Failed to transform
|
|
2463
|
-
return this.
|
|
2661
|
+
this.logger?.error("[RESTDataAdapter] Failed to transform content item", error);
|
|
2662
|
+
return this.createFallbackItem(item);
|
|
2464
2663
|
}
|
|
2465
2664
|
});
|
|
2466
2665
|
return {
|
|
@@ -2474,24 +2673,30 @@ var RESTDataAdapter = class {
|
|
|
2474
2673
|
}
|
|
2475
2674
|
}
|
|
2476
2675
|
/**
|
|
2477
|
-
* Get
|
|
2676
|
+
* Get content detail by ID
|
|
2478
2677
|
*/
|
|
2479
|
-
async
|
|
2678
|
+
async getContentDetail(id) {
|
|
2480
2679
|
try {
|
|
2481
2680
|
const response = await this.httpClient.request({
|
|
2482
2681
|
method: "GET",
|
|
2483
2682
|
path: this.endpoints.detail,
|
|
2484
2683
|
pathParams: { id }
|
|
2485
2684
|
});
|
|
2486
|
-
const
|
|
2487
|
-
return this.transforms.
|
|
2685
|
+
const itemData = this.unwrapResponse(response);
|
|
2686
|
+
return this.transforms.contentItem(itemData);
|
|
2488
2687
|
} catch (error) {
|
|
2489
|
-
this.logger?.error("[RESTDataAdapter]
|
|
2688
|
+
this.logger?.error("[RESTDataAdapter] getContentDetail failed", error);
|
|
2490
2689
|
throw error;
|
|
2491
2690
|
}
|
|
2492
2691
|
}
|
|
2493
2692
|
/**
|
|
2494
|
-
*
|
|
2693
|
+
* @deprecated Use getContentDetail instead
|
|
2694
|
+
*/
|
|
2695
|
+
async getVideoDetail(id) {
|
|
2696
|
+
return this.getContentDetail(id);
|
|
2697
|
+
}
|
|
2698
|
+
/**
|
|
2699
|
+
* Optional: Prefetch content
|
|
2495
2700
|
* This is a no-op by default, can be overridden if API supports batch fetch
|
|
2496
2701
|
*/
|
|
2497
2702
|
async prefetch(ids) {
|
|
@@ -2512,12 +2717,27 @@ var RESTDataAdapter = class {
|
|
|
2512
2717
|
return response;
|
|
2513
2718
|
}
|
|
2514
2719
|
/**
|
|
2515
|
-
* Create fallback
|
|
2720
|
+
* Create fallback content item when transform fails
|
|
2516
2721
|
*/
|
|
2517
|
-
|
|
2722
|
+
createFallbackItem(data) {
|
|
2518
2723
|
const obj = data ?? {};
|
|
2724
|
+
const id = String(obj.id ?? obj.video_id ?? obj.article_id ?? `fallback-${Date.now()}`);
|
|
2725
|
+
if (obj.type === "article" || obj.images || obj.photos) {
|
|
2726
|
+
return {
|
|
2727
|
+
type: "article",
|
|
2728
|
+
id,
|
|
2729
|
+
images: [],
|
|
2730
|
+
caption: "",
|
|
2731
|
+
author: { id: "unknown", name: "Unknown" },
|
|
2732
|
+
stats: { likes: 0, comments: 0, shares: 0, views: 0 },
|
|
2733
|
+
isLiked: false,
|
|
2734
|
+
isFollowing: false,
|
|
2735
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2736
|
+
};
|
|
2737
|
+
}
|
|
2519
2738
|
return {
|
|
2520
|
-
|
|
2739
|
+
type: "video",
|
|
2740
|
+
id,
|
|
2521
2741
|
source: {
|
|
2522
2742
|
url: String(obj.video_url ?? obj.url ?? ""),
|
|
2523
2743
|
type: "mp4"
|
|
@@ -2531,7 +2751,8 @@ var RESTDataAdapter = class {
|
|
|
2531
2751
|
views: 0,
|
|
2532
2752
|
likes: 0,
|
|
2533
2753
|
comments: 0,
|
|
2534
|
-
shares: 0
|
|
2754
|
+
shares: 0,
|
|
2755
|
+
bookmarks: 0
|
|
2535
2756
|
},
|
|
2536
2757
|
isLiked: false,
|
|
2537
2758
|
isFollowing: false,
|
|
@@ -2546,6 +2767,8 @@ var RESTInteractionAdapter = class {
|
|
|
2546
2767
|
this.httpClient = config.httpClient;
|
|
2547
2768
|
this.endpoints = config.endpoints;
|
|
2548
2769
|
this.logger = config.logger;
|
|
2770
|
+
this.customTransformReportReasons = config.transformReportReasons;
|
|
2771
|
+
this.customTransformReportBody = config.transformReportBody;
|
|
2549
2772
|
}
|
|
2550
2773
|
/**
|
|
2551
2774
|
* Like a video
|
|
@@ -2670,6 +2893,107 @@ var RESTInteractionAdapter = class {
|
|
|
2670
2893
|
this.logger?.warn("[RESTInteractionAdapter] share tracking failed", { error });
|
|
2671
2894
|
}
|
|
2672
2895
|
}
|
|
2896
|
+
/**
|
|
2897
|
+
* Report content (video or image post)
|
|
2898
|
+
*
|
|
2899
|
+
* @param contentId - ID of the content to report
|
|
2900
|
+
* @param reason - Report reason code/ID
|
|
2901
|
+
* @param description - Optional additional description
|
|
2902
|
+
*/
|
|
2903
|
+
async report(contentId, reason, description) {
|
|
2904
|
+
if (!this.endpoints.report) {
|
|
2905
|
+
this.logger?.warn("[RESTInteractionAdapter] report endpoint not configured");
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2908
|
+
try {
|
|
2909
|
+
const body = this.customTransformReportBody ? this.customTransformReportBody({ contentId, reasonId: reason, description }) : { reason, description };
|
|
2910
|
+
this.logger?.debug("[RESTInteractionAdapter] Sending report", {
|
|
2911
|
+
path: this.endpoints.report,
|
|
2912
|
+
contentId,
|
|
2913
|
+
body
|
|
2914
|
+
});
|
|
2915
|
+
await this.httpClient.request({
|
|
2916
|
+
method: "POST",
|
|
2917
|
+
path: this.endpoints.report,
|
|
2918
|
+
pathParams: { id: contentId },
|
|
2919
|
+
body
|
|
2920
|
+
});
|
|
2921
|
+
this.logger?.debug("[RESTInteractionAdapter] Report sent successfully");
|
|
2922
|
+
} catch (error) {
|
|
2923
|
+
this.logger?.error("[RESTInteractionAdapter] report failed", error);
|
|
2924
|
+
throw error;
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
/**
|
|
2928
|
+
* Get available report reasons
|
|
2929
|
+
*
|
|
2930
|
+
* @returns Array of report reasons, or empty array if not configured
|
|
2931
|
+
*/
|
|
2932
|
+
async getReportReasons() {
|
|
2933
|
+
if (!this.endpoints.reportReasons) {
|
|
2934
|
+
this.logger?.debug("[RESTInteractionAdapter] reportReasons endpoint not configured");
|
|
2935
|
+
return [];
|
|
2936
|
+
}
|
|
2937
|
+
try {
|
|
2938
|
+
const response = await this.httpClient.request({
|
|
2939
|
+
method: "GET",
|
|
2940
|
+
path: this.endpoints.reportReasons
|
|
2941
|
+
});
|
|
2942
|
+
if (this.customTransformReportReasons) {
|
|
2943
|
+
return this.customTransformReportReasons(response);
|
|
2944
|
+
}
|
|
2945
|
+
return this.transformReportReasons(response);
|
|
2946
|
+
} catch (error) {
|
|
2947
|
+
this.logger?.error("[RESTInteractionAdapter] getReportReasons failed", error);
|
|
2948
|
+
return [];
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
/**
|
|
2952
|
+
* Mark content as "not interested"
|
|
2953
|
+
*
|
|
2954
|
+
* Used for recommendation algorithm feedback.
|
|
2955
|
+
* Content should be hidden from feed after this action.
|
|
2956
|
+
*
|
|
2957
|
+
* @param contentId - ID of the content (video or image post)
|
|
2958
|
+
*/
|
|
2959
|
+
async notInterested(contentId) {
|
|
2960
|
+
if (!this.endpoints.notInterested) {
|
|
2961
|
+
this.logger?.warn("[RESTInteractionAdapter] notInterested endpoint not configured");
|
|
2962
|
+
return;
|
|
2963
|
+
}
|
|
2964
|
+
try {
|
|
2965
|
+
await this.httpClient.request({
|
|
2966
|
+
method: "POST",
|
|
2967
|
+
path: this.endpoints.notInterested,
|
|
2968
|
+
pathParams: { id: contentId }
|
|
2969
|
+
});
|
|
2970
|
+
} catch (error) {
|
|
2971
|
+
this.logger?.error("[RESTInteractionAdapter] notInterested failed", error);
|
|
2972
|
+
throw error;
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
/**
|
|
2976
|
+
* Default transform for API report reasons response to ReportReason[]
|
|
2977
|
+
*
|
|
2978
|
+
* Expected format: [{ id, label, description }]
|
|
2979
|
+
* Or wrapped: { data: [{ id, label, description }] }
|
|
2980
|
+
*
|
|
2981
|
+
* For custom API formats, use `transforms.reportReasons` in preset config.
|
|
2982
|
+
*/
|
|
2983
|
+
transformReportReasons(response) {
|
|
2984
|
+
const data = this.unwrapResponse(response);
|
|
2985
|
+
if (!Array.isArray(data)) {
|
|
2986
|
+
return [];
|
|
2987
|
+
}
|
|
2988
|
+
return data.map((item) => {
|
|
2989
|
+
const obj = item;
|
|
2990
|
+
return {
|
|
2991
|
+
id: String(obj.id ?? obj.reason_id ?? ""),
|
|
2992
|
+
label: String(obj.label ?? obj.name ?? obj.title ?? ""),
|
|
2993
|
+
description: obj.description
|
|
2994
|
+
};
|
|
2995
|
+
});
|
|
2996
|
+
}
|
|
2673
2997
|
/**
|
|
2674
2998
|
* Transform API comment response to Comment type
|
|
2675
2999
|
*/
|
|
@@ -2704,6 +3028,69 @@ var RESTInteractionAdapter = class {
|
|
|
2704
3028
|
}
|
|
2705
3029
|
};
|
|
2706
3030
|
|
|
3031
|
+
// src/preset/adapters/RESTPlaylistAdapter.ts
|
|
3032
|
+
var RESTPlaylistAdapter = class {
|
|
3033
|
+
constructor(config) {
|
|
3034
|
+
this.httpClient = config.httpClient;
|
|
3035
|
+
this.endpoint = config.endpoint;
|
|
3036
|
+
this.collectionEndpoint = config.collectionEndpoint;
|
|
3037
|
+
this.transforms = config.transforms;
|
|
3038
|
+
this.logger = config.logger;
|
|
3039
|
+
}
|
|
3040
|
+
/**
|
|
3041
|
+
* Fetch a complete playlist by ID
|
|
3042
|
+
*
|
|
3043
|
+
* @param id - The playlist ID
|
|
3044
|
+
* @returns Promise resolving to PlaylistData
|
|
3045
|
+
*/
|
|
3046
|
+
async fetchPlaylist(id) {
|
|
3047
|
+
try {
|
|
3048
|
+
this.logger?.info(`[RESTPlaylistAdapter] Fetching playlist: ${id}`);
|
|
3049
|
+
const path = this.endpoint.replace(":id", id);
|
|
3050
|
+
const response = await this.httpClient.request({
|
|
3051
|
+
method: "GET",
|
|
3052
|
+
path
|
|
3053
|
+
});
|
|
3054
|
+
const playlist = this.transforms.playlist(response);
|
|
3055
|
+
if (playlist.items.length === 0) {
|
|
3056
|
+
this.logger?.warn(`[RESTPlaylistAdapter] Playlist ${id} is empty`);
|
|
3057
|
+
}
|
|
3058
|
+
return playlist;
|
|
3059
|
+
} catch (error) {
|
|
3060
|
+
this.logger?.error(`[RESTPlaylistAdapter] Failed to fetch playlist: ${id}`, error);
|
|
3061
|
+
throw error;
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
/**
|
|
3065
|
+
* Fetch a collection of playlists
|
|
3066
|
+
*
|
|
3067
|
+
* @param cursor - Pagination cursor
|
|
3068
|
+
* @returns Promise resolving to PlaylistCollectionResponse
|
|
3069
|
+
*/
|
|
3070
|
+
async fetchPlaylistCollection(cursor) {
|
|
3071
|
+
try {
|
|
3072
|
+
if (!this.collectionEndpoint || !this.transforms.collection) {
|
|
3073
|
+
throw new Error(
|
|
3074
|
+
"[RESTPlaylistAdapter] collectionEndpoint or collection transform not configured"
|
|
3075
|
+
);
|
|
3076
|
+
}
|
|
3077
|
+
this.logger?.info(`[RESTPlaylistAdapter] Fetching playlist collection (cursor: ${cursor})`);
|
|
3078
|
+
const path = cursor ? `${this.collectionEndpoint}?cursor=${cursor}` : this.collectionEndpoint;
|
|
3079
|
+
const response = await this.httpClient.request({
|
|
3080
|
+
method: "GET",
|
|
3081
|
+
path
|
|
3082
|
+
});
|
|
3083
|
+
return this.transforms.collection(response);
|
|
3084
|
+
} catch (error) {
|
|
3085
|
+
this.logger?.error(
|
|
3086
|
+
"[RESTPlaylistAdapter] Failed to fetch playlist collection",
|
|
3087
|
+
error
|
|
3088
|
+
);
|
|
3089
|
+
throw error;
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
};
|
|
3093
|
+
|
|
2707
3094
|
// src/preset/adapters/RESTViewTrackingAdapter.ts
|
|
2708
3095
|
var DEFAULT_VIEW_EVENT_VALUE = "seek";
|
|
2709
3096
|
var DEFAULT_HEARTBEAT_INTERVAL = 1e4;
|
|
@@ -2958,6 +3345,12 @@ var HttpClient = class {
|
|
|
2958
3345
|
};
|
|
2959
3346
|
this.retryConfig = mergedRetry;
|
|
2960
3347
|
}
|
|
3348
|
+
/**
|
|
3349
|
+
* Get base URL (for sendBeacon which needs full URL)
|
|
3350
|
+
*/
|
|
3351
|
+
getBaseUrl() {
|
|
3352
|
+
return this.config.baseUrl;
|
|
3353
|
+
}
|
|
2961
3354
|
/**
|
|
2962
3355
|
* Make HTTP request with auth and retry
|
|
2963
3356
|
*/
|
|
@@ -3168,6 +3561,83 @@ var HttpClient = class {
|
|
|
3168
3561
|
}
|
|
3169
3562
|
};
|
|
3170
3563
|
|
|
3564
|
+
// src/preset/transforms/playlist.ts
|
|
3565
|
+
function defaultPlaylistTransform(apiResponse, contentItemTransform, logger) {
|
|
3566
|
+
if (!apiResponse || typeof apiResponse !== "object") {
|
|
3567
|
+
logger?.error("[PlaylistTransform] Invalid API response", void 0, {
|
|
3568
|
+
apiResponse: String(apiResponse)
|
|
3569
|
+
});
|
|
3570
|
+
return createEmptyPlaylist();
|
|
3571
|
+
}
|
|
3572
|
+
const obj = apiResponse;
|
|
3573
|
+
const data = obj.data ?? obj.result ?? obj.playlist ?? obj;
|
|
3574
|
+
const rawItems = data.items ?? data.reels ?? data.videos ?? data.list ?? [];
|
|
3575
|
+
if (!Array.isArray(rawItems)) {
|
|
3576
|
+
logger?.warn("[PlaylistTransform] Items is not an array", { data });
|
|
3577
|
+
return createEmptyPlaylist(data);
|
|
3578
|
+
}
|
|
3579
|
+
const items = rawItems.map((item) => {
|
|
3580
|
+
try {
|
|
3581
|
+
return contentItemTransform(item);
|
|
3582
|
+
} catch (error) {
|
|
3583
|
+
logger?.error("[PlaylistTransform] Failed to transform item", error);
|
|
3584
|
+
return null;
|
|
3585
|
+
}
|
|
3586
|
+
}).filter((item) => item !== null);
|
|
3587
|
+
return {
|
|
3588
|
+
id: String(data.id ?? data.playlist_id ?? ""),
|
|
3589
|
+
title: String(data.title ?? data.name ?? "Untitled Playlist"),
|
|
3590
|
+
description: String(data.description ?? ""),
|
|
3591
|
+
cover: String(data.cover ?? data.cover_url ?? data.thumbnail ?? ""),
|
|
3592
|
+
items,
|
|
3593
|
+
totalItems: Number(data.total_items ?? data.total ?? items.length)
|
|
3594
|
+
};
|
|
3595
|
+
}
|
|
3596
|
+
function defaultPlaylistSummaryTransform(data) {
|
|
3597
|
+
const obj = data;
|
|
3598
|
+
const authorObj = obj.author ?? obj.user ?? obj.creator ?? {};
|
|
3599
|
+
return {
|
|
3600
|
+
id: String(obj.id ?? obj.playlist_id ?? ""),
|
|
3601
|
+
title: String(obj.title ?? obj.name ?? "Untitled Playlist"),
|
|
3602
|
+
description: String(obj.description ?? ""),
|
|
3603
|
+
cover: String(obj.cover ?? obj.cover_url ?? obj.thumbnail ?? ""),
|
|
3604
|
+
totalItems: Number(obj.total_items ?? obj.items_count ?? 0),
|
|
3605
|
+
author: {
|
|
3606
|
+
id: String(authorObj.id ?? authorObj.user_id ?? ""),
|
|
3607
|
+
name: String(authorObj.name ?? authorObj.display_name ?? authorObj.username ?? "Unknown")
|
|
3608
|
+
},
|
|
3609
|
+
updatedAt: String(obj.updated_at ?? obj.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString())
|
|
3610
|
+
};
|
|
3611
|
+
}
|
|
3612
|
+
function defaultPlaylistCollectionTransform(apiResponse, logger) {
|
|
3613
|
+
if (!apiResponse || typeof apiResponse !== "object") {
|
|
3614
|
+
return { playlists: [], nextCursor: null, hasMore: false };
|
|
3615
|
+
}
|
|
3616
|
+
const obj = apiResponse;
|
|
3617
|
+
const data = obj.data ?? obj.result ?? obj;
|
|
3618
|
+
const rawPlaylists = data.playlists ?? data.items ?? data.list ?? [];
|
|
3619
|
+
if (!Array.isArray(rawPlaylists)) {
|
|
3620
|
+
logger?.warn("[PlaylistTransform] Playlists is not an array", { data });
|
|
3621
|
+
return { playlists: [], nextCursor: null, hasMore: false };
|
|
3622
|
+
}
|
|
3623
|
+
const playlists = rawPlaylists.map(defaultPlaylistSummaryTransform);
|
|
3624
|
+
const nextCursor = String(data.next_cursor ?? data.cursor ?? data.nextCursor ?? null);
|
|
3625
|
+
const hasMore = Boolean(data.has_more ?? data.hasMore ?? (nextCursor && nextCursor !== "null"));
|
|
3626
|
+
return {
|
|
3627
|
+
playlists,
|
|
3628
|
+
nextCursor: nextCursor === "null" ? null : nextCursor,
|
|
3629
|
+
hasMore
|
|
3630
|
+
};
|
|
3631
|
+
}
|
|
3632
|
+
function createEmptyPlaylist(data) {
|
|
3633
|
+
return {
|
|
3634
|
+
id: String(data?.id ?? ""),
|
|
3635
|
+
title: String(data?.title ?? "Empty Playlist"),
|
|
3636
|
+
items: [],
|
|
3637
|
+
totalItems: 0
|
|
3638
|
+
};
|
|
3639
|
+
}
|
|
3640
|
+
|
|
3171
3641
|
// src/preset/transforms/defaults.ts
|
|
3172
3642
|
function getNestedValue(obj, path) {
|
|
3173
3643
|
if (!obj || typeof obj !== "object") return void 0;
|
|
@@ -3230,9 +3700,11 @@ function defaultAuthorTransform(data) {
|
|
|
3230
3700
|
return {
|
|
3231
3701
|
id: toSafeString(tryFields(author, "id", "user_id", "author_id"), ""),
|
|
3232
3702
|
name: toSafeString(
|
|
3233
|
-
tryFields(author, "display_name", "name", "username", "nickname"),
|
|
3703
|
+
tryFields(author, "display_name", "name", "username", "nickname", "first_name"),
|
|
3234
3704
|
"Unknown"
|
|
3235
3705
|
),
|
|
3706
|
+
// Combine first/last name if present and name is default
|
|
3707
|
+
// Note: This is a simple fallback, specialized logic should be in a custom transform if needed
|
|
3236
3708
|
avatar: toSafeString(
|
|
3237
3709
|
tryFields(author, "avatar", "avatar_url", "profile_picture", "photo"),
|
|
3238
3710
|
void 0
|
|
@@ -3248,7 +3720,8 @@ function defaultStatsTransform(data) {
|
|
|
3248
3720
|
views: toNumber(tryFields(stats, "view_count", "views", "play_count"), 0),
|
|
3249
3721
|
likes: toNumber(tryFields(stats, "like_count", "likes", "digg_count"), 0),
|
|
3250
3722
|
comments: toNumber(tryFields(stats, "comment_count", "comments"), 0),
|
|
3251
|
-
shares: toNumber(tryFields(stats, "share_count", "shares"), 0)
|
|
3723
|
+
shares: toNumber(tryFields(stats, "share_count", "shares"), 0),
|
|
3724
|
+
bookmarks: toNumber(tryFields(stats, "bookmark_count", "bookmarks", "saves"), 0)
|
|
3252
3725
|
};
|
|
3253
3726
|
}
|
|
3254
3727
|
function defaultVideoItemTransform(apiResponse, fieldMap, logger) {
|
|
@@ -3275,6 +3748,7 @@ function defaultVideoItemTransform(apiResponse, fieldMap, logger) {
|
|
|
3275
3748
|
const author = defaultAuthorTransform(obj);
|
|
3276
3749
|
const stats = defaultStatsTransform(obj);
|
|
3277
3750
|
const videoItem = {
|
|
3751
|
+
type: "video",
|
|
3278
3752
|
id,
|
|
3279
3753
|
source,
|
|
3280
3754
|
poster: toSafeString(
|
|
@@ -3301,6 +3775,110 @@ function defaultVideoItemTransform(apiResponse, fieldMap, logger) {
|
|
|
3301
3775
|
};
|
|
3302
3776
|
return videoItem;
|
|
3303
3777
|
}
|
|
3778
|
+
function defaultArticleItemTransform(apiResponse, fieldMap, logger) {
|
|
3779
|
+
const obj = apiResponse;
|
|
3780
|
+
const getMapped = (sdkField, ...fallbacks) => {
|
|
3781
|
+
if (fieldMap?.[sdkField]) {
|
|
3782
|
+
const mapped = getNestedValue(obj, fieldMap[sdkField]);
|
|
3783
|
+
if (mapped !== void 0) return mapped;
|
|
3784
|
+
}
|
|
3785
|
+
return tryFields(obj, ...fallbacks);
|
|
3786
|
+
};
|
|
3787
|
+
const id = toSafeString(getMapped("id", "id", "article_id", "post_id", "_id"), "");
|
|
3788
|
+
if (!id) {
|
|
3789
|
+
logger?.warn("[Transform article] Missing required field: id", { data: obj });
|
|
3790
|
+
}
|
|
3791
|
+
let images = getMapped("images", "images", "media", "photos", "gallery") ?? [];
|
|
3792
|
+
if (!Array.isArray(images) || images.length === 0) {
|
|
3793
|
+
const singleImage = toSafeString(
|
|
3794
|
+
getMapped("image", "image_url", "url", "thumbnail", "cover"),
|
|
3795
|
+
void 0
|
|
3796
|
+
);
|
|
3797
|
+
images = singleImage ? [singleImage] : [];
|
|
3798
|
+
}
|
|
3799
|
+
return {
|
|
3800
|
+
type: "article",
|
|
3801
|
+
id,
|
|
3802
|
+
images,
|
|
3803
|
+
caption: toSafeString(
|
|
3804
|
+
getMapped("caption", "caption", "description", "text", "content", "title", "subject"),
|
|
3805
|
+
""
|
|
3806
|
+
),
|
|
3807
|
+
author: defaultAuthorTransform(obj),
|
|
3808
|
+
stats: defaultStatsTransform(obj),
|
|
3809
|
+
isLiked: toBoolean(getMapped("isLiked", "is_liked", "liked", "user_liked"), false),
|
|
3810
|
+
isFollowing: toBoolean(getMapped("isFollowing", "is_following", "following"), false),
|
|
3811
|
+
createdAt: toSafeString(
|
|
3812
|
+
getMapped("createdAt", "created_at", "create_time", "timestamp"),
|
|
3813
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
3814
|
+
),
|
|
3815
|
+
hashtags: getMapped("hashtags", "hashtags", "tags", "hash_tags") ?? []
|
|
3816
|
+
};
|
|
3817
|
+
}
|
|
3818
|
+
function extractMediaInfo(obj) {
|
|
3819
|
+
const mediaList = tryFields(obj, "media", "medias") ?? [];
|
|
3820
|
+
let videoMedia;
|
|
3821
|
+
let imageMedias = [];
|
|
3822
|
+
if (Array.isArray(mediaList) && mediaList.length > 0) {
|
|
3823
|
+
videoMedia = mediaList.find((m) => m.type === "video");
|
|
3824
|
+
imageMedias = mediaList.filter((m) => m.type === "image");
|
|
3825
|
+
}
|
|
3826
|
+
return { videoMedia, imageMedias };
|
|
3827
|
+
}
|
|
3828
|
+
function extractMusicInfo(obj) {
|
|
3829
|
+
const soundObj = tryFields(obj, "sound", "music", "audio");
|
|
3830
|
+
if (soundObj && (soundObj.id || soundObj.url || soundObj.name || soundObj.title)) {
|
|
3831
|
+
return {
|
|
3832
|
+
id: toSafeString(tryFields(soundObj, "id", "uuid"), "unknown"),
|
|
3833
|
+
title: toSafeString(tryFields(soundObj, "title", "name", "song"), "Unknown Sound"),
|
|
3834
|
+
artist: toSafeString(tryFields(soundObj, "artist", "author", "singer"), "Unknown Artist"),
|
|
3835
|
+
cover: toSafeString(tryFields(soundObj, "cover", "poster", "thumbnail", "image"), void 0),
|
|
3836
|
+
audioUrl: toSafeString(tryFields(soundObj, "url", "audio_url", "download_url"), void 0)
|
|
3837
|
+
};
|
|
3838
|
+
}
|
|
3839
|
+
return void 0;
|
|
3840
|
+
}
|
|
3841
|
+
function defaultContentItemTransform(apiResponse, fieldMap, logger) {
|
|
3842
|
+
const obj = apiResponse;
|
|
3843
|
+
const { videoMedia, imageMedias } = extractMediaInfo(obj);
|
|
3844
|
+
const musicInfo = extractMusicInfo(obj);
|
|
3845
|
+
const typeField = toSafeString(tryFields(obj, "type", "content_type", "item_type")).toLowerCase();
|
|
3846
|
+
let isVideo = false;
|
|
3847
|
+
if (["article", "image", "post", "photo"].includes(typeField)) {
|
|
3848
|
+
isVideo = false;
|
|
3849
|
+
} else if (["video", "reel", "short"].includes(typeField)) {
|
|
3850
|
+
isVideo = true;
|
|
3851
|
+
} else {
|
|
3852
|
+
if (videoMedia || tryFields(obj, "video_url", "playback_url", "source")) {
|
|
3853
|
+
isVideo = true;
|
|
3854
|
+
} else if (imageMedias.length > 0 || tryFields(obj, "images", "photos", "gallery")) {
|
|
3855
|
+
isVideo = false;
|
|
3856
|
+
} else {
|
|
3857
|
+
isVideo = true;
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
if (isVideo) {
|
|
3861
|
+
if (videoMedia) {
|
|
3862
|
+
Object.assign(obj, {
|
|
3863
|
+
video_url: videoMedia.url || videoMedia.download_url,
|
|
3864
|
+
poster: videoMedia.poster || videoMedia.thumbnail,
|
|
3865
|
+
duration: videoMedia.duration
|
|
3866
|
+
});
|
|
3867
|
+
}
|
|
3868
|
+
const item2 = defaultVideoItemTransform(apiResponse, fieldMap?.video, logger);
|
|
3869
|
+
if (musicInfo) item2.music = musicInfo;
|
|
3870
|
+
return item2;
|
|
3871
|
+
}
|
|
3872
|
+
if (imageMedias.length > 0) {
|
|
3873
|
+
const imageUrls = imageMedias.map((m) => toSafeString(m.url || m.download_url)).filter(Boolean);
|
|
3874
|
+
if (imageUrls.length > 0) {
|
|
3875
|
+
obj.images = imageUrls;
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
const item = defaultArticleItemTransform(apiResponse, fieldMap?.article, logger);
|
|
3879
|
+
if (musicInfo) item.music = musicInfo;
|
|
3880
|
+
return item;
|
|
3881
|
+
}
|
|
3304
3882
|
function defaultFeedResponseTransform(apiResponse, fieldMap, logger) {
|
|
3305
3883
|
const obj = apiResponse;
|
|
3306
3884
|
const itemsPath = fieldMap?.items;
|
|
@@ -3354,9 +3932,22 @@ function defaultFeedResponseTransform(apiResponse, fieldMap, logger) {
|
|
|
3354
3932
|
return { items, nextCursor, hasMore };
|
|
3355
3933
|
}
|
|
3356
3934
|
function createTransforms(config, logger) {
|
|
3935
|
+
const contentItem = (data) => {
|
|
3936
|
+
if (config?.contentItem) return config.contentItem(data);
|
|
3937
|
+
if (config?.videoItem) return config.videoItem(data);
|
|
3938
|
+
return defaultContentItemTransform(data, config?.fieldMap, logger);
|
|
3939
|
+
};
|
|
3357
3940
|
return {
|
|
3358
|
-
|
|
3359
|
-
|
|
3941
|
+
contentItem,
|
|
3942
|
+
videoItem: (data) => contentItem(data),
|
|
3943
|
+
feedResponse: config?.feedResponse ? config.feedResponse : (data) => defaultFeedResponseTransform(data, config?.fieldMap?.feed, logger),
|
|
3944
|
+
playlist: config?.playlist ? config.playlist : (data) => defaultPlaylistTransform(
|
|
3945
|
+
data,
|
|
3946
|
+
(item) => contentItem(item),
|
|
3947
|
+
// Pass the generic contentItem transform
|
|
3948
|
+
logger
|
|
3949
|
+
),
|
|
3950
|
+
playlistCollection: config?.playlistCollection ? config.playlistCollection : (data) => defaultPlaylistCollectionTransform(data, logger)
|
|
3360
3951
|
};
|
|
3361
3952
|
}
|
|
3362
3953
|
|
|
@@ -3408,7 +3999,9 @@ function createRESTAdapters(config) {
|
|
|
3408
3999
|
const interaction = new RESTInteractionAdapter({
|
|
3409
4000
|
httpClient,
|
|
3410
4001
|
endpoints: endpoints.interaction,
|
|
3411
|
-
logger
|
|
4002
|
+
logger,
|
|
4003
|
+
transformReportReasons: transforms?.reportReasons,
|
|
4004
|
+
transformReportBody: transforms?.reportBody
|
|
3412
4005
|
});
|
|
3413
4006
|
let analytics;
|
|
3414
4007
|
if (endpoints.viewTracking) {
|
|
@@ -3422,6 +4015,7 @@ function createRESTAdapters(config) {
|
|
|
3422
4015
|
analytics = new RESTAnalyticsAdapter({
|
|
3423
4016
|
httpClient,
|
|
3424
4017
|
endpoints: endpoints.analytics,
|
|
4018
|
+
config: config.batchAnalytics,
|
|
3425
4019
|
logger
|
|
3426
4020
|
});
|
|
3427
4021
|
} else {
|
|
@@ -3432,11 +4026,22 @@ function createRESTAdapters(config) {
|
|
|
3432
4026
|
endpoints: endpoints.comment,
|
|
3433
4027
|
logger
|
|
3434
4028
|
}) : void 0;
|
|
4029
|
+
const playlist = endpoints.playlist ? new RESTPlaylistAdapter({
|
|
4030
|
+
httpClient,
|
|
4031
|
+
endpoint: endpoints.playlist.detail,
|
|
4032
|
+
collectionEndpoint: endpoints.playlist.list,
|
|
4033
|
+
transforms: {
|
|
4034
|
+
playlist: resolvedTransforms.playlist,
|
|
4035
|
+
collection: (data) => resolvedTransforms.playlistCollection(data)
|
|
4036
|
+
},
|
|
4037
|
+
logger
|
|
4038
|
+
}) : void 0;
|
|
3435
4039
|
return {
|
|
3436
4040
|
dataSource,
|
|
3437
4041
|
interaction,
|
|
3438
4042
|
analytics,
|
|
3439
|
-
comment
|
|
4043
|
+
comment,
|
|
4044
|
+
playlist
|
|
3440
4045
|
};
|
|
3441
4046
|
}
|
|
3442
4047
|
|
|
@@ -3807,6 +4412,25 @@ var LocalStorageAdapter = class {
|
|
|
3807
4412
|
count: keysToRemove.length
|
|
3808
4413
|
});
|
|
3809
4414
|
}
|
|
4415
|
+
/**
|
|
4416
|
+
* Get a value from localStorage synchronously
|
|
4417
|
+
*
|
|
4418
|
+
* Used for zero-flash cache hydration during React render phase.
|
|
4419
|
+
* localStorage is inherently synchronous, so this bypasses the async wrapper.
|
|
4420
|
+
*/
|
|
4421
|
+
getSync(key) {
|
|
4422
|
+
try {
|
|
4423
|
+
const fullKey = this.buildKey(key);
|
|
4424
|
+
const value = localStorage.getItem(fullKey);
|
|
4425
|
+
if (value === null) {
|
|
4426
|
+
return null;
|
|
4427
|
+
}
|
|
4428
|
+
return JSON.parse(value);
|
|
4429
|
+
} catch (error) {
|
|
4430
|
+
this.logger?.warn("[LocalStorageAdapter] Failed to getSync/parse value", { key, error });
|
|
4431
|
+
return null;
|
|
4432
|
+
}
|
|
4433
|
+
}
|
|
3810
4434
|
/**
|
|
3811
4435
|
* Get all SDK-namespaced keys
|
|
3812
4436
|
*/
|
|
@@ -4071,8 +4695,9 @@ function createBrowserAdapters(config) {
|
|
|
4071
4695
|
videoLoader,
|
|
4072
4696
|
posterLoader,
|
|
4073
4697
|
comment,
|
|
4698
|
+
playlist: restAdapters.playlist,
|
|
4074
4699
|
logger: config.logger
|
|
4075
4700
|
};
|
|
4076
4701
|
}
|
|
4077
4702
|
|
|
4078
|
-
export { BrowserPosterLoader, BrowserVideoLoader, DEFAULT_REQUEST_CONFIG, DEFAULT_RETRY_CONFIG, HttpClient, HttpError, LocalSessionStorageAdapter, LocalStorageAdapter, MockAnalyticsAdapter, MockCommentAdapter, MockDataAdapter, MockInteractionAdapter, MockLoggerAdapter, MockNetworkAdapter, MockPosterLoader, MockSessionStorageAdapter, MockStorageAdapter, MockVideoLoader, RESTAnalyticsAdapter, RESTCommentAdapter, RESTDataAdapter, RESTInteractionAdapter, WebNetworkAdapter, createBrowserAdapters, createBrowserPosterLoader, createBrowserVideoLoader, createLocalStorageAdapter, createNoOpAnalyticsAdapter, createRESTAdapters, createSessionStorageAdapter, createTransforms, createWebNetworkAdapter, defaultFeedResponseTransform, defaultVideoItemTransform };
|
|
4703
|
+
export { BrowserPosterLoader, BrowserVideoLoader, DEFAULT_REQUEST_CONFIG, DEFAULT_RETRY_CONFIG, HttpClient, HttpError, LocalSessionStorageAdapter, LocalStorageAdapter, MockAnalyticsAdapter, MockCommentAdapter, MockDataAdapter, MockInteractionAdapter, MockLoggerAdapter, MockNetworkAdapter, MockPlaylistAdapter, MockPosterLoader, MockSessionStorageAdapter, MockStorageAdapter, MockVideoLoader, RESTAnalyticsAdapter, RESTCommentAdapter, RESTDataAdapter, RESTInteractionAdapter, RESTViewTrackingAdapter, WebNetworkAdapter, createBrowserAdapters, createBrowserPosterLoader, createBrowserVideoLoader, createLocalStorageAdapter, createNoOpAnalyticsAdapter, createRESTAdapters, createSessionStorageAdapter, createTransforms, createWebNetworkAdapter, defaultFeedResponseTransform, defaultVideoItemTransform };
|