@xhub-short/adapters 0.1.0-beta.10 → 0.1.0-beta.12
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 +146 -127
- package/dist/index.js +374 -174
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IDataSource, VideoItem, FeedResponse, ILogger, LogLevel, LogEntry, IStorage, ISessionStorage, SessionSnapshot, IInteraction, Comment, ICommentAdapter, MockCommentAdapterConfig, CommentListResponse, ReplyListResponse, PostCommentPayload, CommentItem, PostReplyPayload, ReplyItem, EditCommentPayload, DeleteCommentPayload, ReportCommentPayload, IAnalytics, AnalyticsEvent, INetworkAdapter, NetworkType, NetworkQuality, IVideoLoader, VideoSource, PreloadConfig, PreloadResult, PreloadStatus, IPosterLoader, CommentTransformers } from '@xhub-short/contracts';
|
|
1
|
+
import { IDataSource, VideoItem, FeedResponse, IPlaylistDataSource, PlaylistData, PlaylistCollectionResponse, ILogger, LogLevel, LogEntry, IStorage, ISessionStorage, SessionSnapshot, IInteraction, Comment, ICommentAdapter, MockCommentAdapterConfig, CommentListResponse, ReplyListResponse, PostCommentPayload, CommentItem, PostReplyPayload, ReplyItem, EditCommentPayload, DeleteCommentPayload, ReportCommentPayload, IAnalytics, AnalyticsEvent, INetworkAdapter, NetworkType, NetworkQuality, IVideoLoader, VideoSource, PreloadConfig, PreloadResult, PreloadStatus, IPosterLoader, ReportReason, CommentTransformers } from '@xhub-short/contracts';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* MockDataAdapter - Development/Testing Data Source
|
|
@@ -46,6 +46,19 @@ interface MockDataAdapterOptions {
|
|
|
46
46
|
delay?: number;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* MockPlaylistAdapter - Development/Testing Playlist Data Source
|
|
51
|
+
*/
|
|
52
|
+
declare class MockPlaylistAdapter implements IPlaylistDataSource {
|
|
53
|
+
private readonly delay;
|
|
54
|
+
constructor(options?: {
|
|
55
|
+
delay?: number;
|
|
56
|
+
});
|
|
57
|
+
fetchPlaylist(id: string): Promise<PlaylistData>;
|
|
58
|
+
fetchPlaylistCollection(cursor?: string): Promise<PlaylistCollectionResponse>;
|
|
59
|
+
private simulateDelay;
|
|
60
|
+
}
|
|
61
|
+
|
|
49
62
|
/**
|
|
50
63
|
* Configuration options for MockLoggerAdapter
|
|
51
64
|
*/
|
|
@@ -446,11 +459,11 @@ declare class MockInteractionAdapter implements IInteraction {
|
|
|
446
459
|
/**
|
|
447
460
|
* Report a video
|
|
448
461
|
*
|
|
449
|
-
* @param
|
|
450
|
-
* @param
|
|
451
|
-
* @param
|
|
462
|
+
* @param videoId - ID of the video to report
|
|
463
|
+
* @param reason - Report reason code
|
|
464
|
+
* @param description - Optional additional description
|
|
452
465
|
*/
|
|
453
|
-
report(
|
|
466
|
+
report(videoId: string, reason: string, description?: string): Promise<void>;
|
|
454
467
|
/**
|
|
455
468
|
* Check if video is liked (for testing)
|
|
456
469
|
*
|
|
@@ -972,6 +985,22 @@ interface RESTEndpointMap {
|
|
|
972
985
|
*/
|
|
973
986
|
detail: string;
|
|
974
987
|
};
|
|
988
|
+
/**
|
|
989
|
+
* Playlist-related endpoints
|
|
990
|
+
*/
|
|
991
|
+
playlist?: {
|
|
992
|
+
/**
|
|
993
|
+
* GET endpoint for playlist collection (listing)
|
|
994
|
+
* Example: '/playlists'
|
|
995
|
+
*/
|
|
996
|
+
list: string;
|
|
997
|
+
/**
|
|
998
|
+
* GET endpoint for playlist detail
|
|
999
|
+
* :id will be replaced with playlist ID
|
|
1000
|
+
* Example: '/playlists/:id'
|
|
1001
|
+
*/
|
|
1002
|
+
detail: string;
|
|
1003
|
+
};
|
|
975
1004
|
/**
|
|
976
1005
|
* Interaction endpoints
|
|
977
1006
|
*/
|
|
@@ -990,6 +1019,12 @@ interface RESTEndpointMap {
|
|
|
990
1019
|
deleteComment: string;
|
|
991
1020
|
/** POST /videos/:id/share (optional) */
|
|
992
1021
|
share?: string;
|
|
1022
|
+
/** POST /content/:id/report (optional) */
|
|
1023
|
+
report?: string;
|
|
1024
|
+
/** GET /report-reasons (optional) */
|
|
1025
|
+
reportReasons?: string;
|
|
1026
|
+
/** POST /content/:id/not-interested (optional) */
|
|
1027
|
+
notInterested?: string;
|
|
993
1028
|
};
|
|
994
1029
|
/**
|
|
995
1030
|
* Analytics endpoints (optional) - BATCH mode
|
|
@@ -1100,6 +1135,14 @@ interface RetryConfig {
|
|
|
1100
1135
|
*/
|
|
1101
1136
|
exponentialBackoff?: boolean;
|
|
1102
1137
|
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Report reason from API
|
|
1140
|
+
*/
|
|
1141
|
+
interface ReportReasonItem {
|
|
1142
|
+
id: string;
|
|
1143
|
+
label: string;
|
|
1144
|
+
description?: string;
|
|
1145
|
+
}
|
|
1103
1146
|
/**
|
|
1104
1147
|
* Response transform configuration
|
|
1105
1148
|
*/
|
|
@@ -1118,6 +1161,51 @@ interface TransformConfig {
|
|
|
1118
1161
|
nextCursor: string | null;
|
|
1119
1162
|
hasMore: boolean;
|
|
1120
1163
|
};
|
|
1164
|
+
/**
|
|
1165
|
+
* Transform playlist response from API
|
|
1166
|
+
* If not provided, uses default transform
|
|
1167
|
+
*/
|
|
1168
|
+
playlist?: (apiResponse: unknown) => PlaylistData;
|
|
1169
|
+
/**
|
|
1170
|
+
* Transform playlist collection response from API
|
|
1171
|
+
* If not provided, uses default transform
|
|
1172
|
+
*/
|
|
1173
|
+
playlistCollection?: (apiResponse: unknown) => PlaylistCollectionResponse;
|
|
1174
|
+
/**
|
|
1175
|
+
* Transform report reasons response from API
|
|
1176
|
+
* If not provided, uses default transform
|
|
1177
|
+
*
|
|
1178
|
+
* @example
|
|
1179
|
+
* ```ts
|
|
1180
|
+
* reportReasons: (response) => {
|
|
1181
|
+
* const data = response.data?.reasons || [];
|
|
1182
|
+
* return data.map(item => ({
|
|
1183
|
+
* id: item.id,
|
|
1184
|
+
* label: item.title,
|
|
1185
|
+
* description: item.description,
|
|
1186
|
+
* }));
|
|
1187
|
+
* }
|
|
1188
|
+
* ```
|
|
1189
|
+
*/
|
|
1190
|
+
reportReasons?: (apiResponse: unknown) => ReportReasonItem[];
|
|
1191
|
+
/**
|
|
1192
|
+
* Transform report request body before sending to API
|
|
1193
|
+
* If not provided, uses default format: { reason, description }
|
|
1194
|
+
*
|
|
1195
|
+
* @example
|
|
1196
|
+
* ```ts
|
|
1197
|
+
* reportBody: ({ contentId, reasonId, description }) => ({
|
|
1198
|
+
* video_id: contentId,
|
|
1199
|
+
* reason_id: reasonId,
|
|
1200
|
+
* description: description || '',
|
|
1201
|
+
* })
|
|
1202
|
+
* ```
|
|
1203
|
+
*/
|
|
1204
|
+
reportBody?: (input: {
|
|
1205
|
+
contentId: string;
|
|
1206
|
+
reasonId: string;
|
|
1207
|
+
description?: string;
|
|
1208
|
+
}) => Record<string, unknown>;
|
|
1121
1209
|
/**
|
|
1122
1210
|
* Field mapping for default transforms
|
|
1123
1211
|
* Used when API field names differ from defaults
|
|
@@ -1430,6 +1518,8 @@ interface PresetAdapters {
|
|
|
1430
1518
|
dataSource: IDataSource;
|
|
1431
1519
|
interaction: IInteraction;
|
|
1432
1520
|
analytics: IAnalytics;
|
|
1521
|
+
/** Playlist adapter (optional) */
|
|
1522
|
+
playlist?: IPlaylistDataSource;
|
|
1433
1523
|
/** Comment adapter (only if comment endpoints are configured) */
|
|
1434
1524
|
comment?: ICommentAdapter;
|
|
1435
1525
|
}
|
|
@@ -1897,6 +1987,8 @@ interface FullPresetAdapters {
|
|
|
1897
1987
|
videoLoader: IVideoLoader;
|
|
1898
1988
|
/** Poster preloader (Image) */
|
|
1899
1989
|
posterLoader: IPosterLoader;
|
|
1990
|
+
/** Playlist adapter (REST API, optional) */
|
|
1991
|
+
playlist?: IPlaylistDataSource;
|
|
1900
1992
|
/** Comment adapter (REST API, only if comment endpoints configured) */
|
|
1901
1993
|
comment?: ICommentAdapter;
|
|
1902
1994
|
/** Logger (passed through) */
|
|
@@ -2060,127 +2152,6 @@ declare class HttpClient {
|
|
|
2060
2152
|
private sleep;
|
|
2061
2153
|
}
|
|
2062
2154
|
|
|
2063
|
-
/**
|
|
2064
|
-
* CircuitBreaker - Prevents cascade failures
|
|
2065
|
-
*
|
|
2066
|
-
* State transitions:
|
|
2067
|
-
* CLOSED → (failures >= threshold) → OPEN
|
|
2068
|
-
* OPEN → (cooldown passes) → HALF_OPEN
|
|
2069
|
-
* HALF_OPEN → (success) → CLOSED
|
|
2070
|
-
* HALF_OPEN → (failure) → OPEN
|
|
2071
|
-
*
|
|
2072
|
-
* @packageDocumentation
|
|
2073
|
-
*/
|
|
2074
|
-
/**
|
|
2075
|
-
* Circuit breaker states
|
|
2076
|
-
*/
|
|
2077
|
-
type CircuitState = 'closed' | 'open' | 'half-open';
|
|
2078
|
-
/**
|
|
2079
|
-
* Circuit breaker configuration
|
|
2080
|
-
*/
|
|
2081
|
-
interface CircuitBreakerConfig {
|
|
2082
|
-
/** Number of failures before opening (default: 5) */
|
|
2083
|
-
failureThreshold?: number;
|
|
2084
|
-
/** Time window for failure counting in ms (default: 30000) */
|
|
2085
|
-
failureWindow?: number;
|
|
2086
|
-
/** Cool-down period before half-open in ms (default: 30000) */
|
|
2087
|
-
cooldownPeriod?: number;
|
|
2088
|
-
/** Number of successes in half-open to close (default: 1) */
|
|
2089
|
-
successThreshold?: number;
|
|
2090
|
-
/** Name for logging */
|
|
2091
|
-
name?: string;
|
|
2092
|
-
}
|
|
2093
|
-
/**
|
|
2094
|
-
* Circuit breaker events
|
|
2095
|
-
*/
|
|
2096
|
-
interface CircuitBreakerEvents {
|
|
2097
|
-
stateChange: (state: CircuitState, previousState: CircuitState) => void;
|
|
2098
|
-
failure: (error: Error, failureCount: number) => void;
|
|
2099
|
-
success: () => void;
|
|
2100
|
-
rejected: () => void;
|
|
2101
|
-
}
|
|
2102
|
-
/**
|
|
2103
|
-
* Error thrown when circuit is open
|
|
2104
|
-
*/
|
|
2105
|
-
declare class CircuitOpenError extends Error {
|
|
2106
|
-
constructor(message?: string);
|
|
2107
|
-
}
|
|
2108
|
-
/**
|
|
2109
|
-
* CircuitBreaker - Prevents cascade failures
|
|
2110
|
-
*
|
|
2111
|
-
* @example
|
|
2112
|
-
* ```typescript
|
|
2113
|
-
* const breaker = new CircuitBreaker({ failureThreshold: 5 });
|
|
2114
|
-
*
|
|
2115
|
-
* async function fetchData() {
|
|
2116
|
-
* return breaker.execute(async () => {
|
|
2117
|
-
* const response = await fetch('/api/data');
|
|
2118
|
-
* return response.json();
|
|
2119
|
-
* });
|
|
2120
|
-
* }
|
|
2121
|
-
* ```
|
|
2122
|
-
*/
|
|
2123
|
-
declare class CircuitBreaker {
|
|
2124
|
-
private state;
|
|
2125
|
-
private failures;
|
|
2126
|
-
private lastFailureTime;
|
|
2127
|
-
private halfOpenSuccesses;
|
|
2128
|
-
private config;
|
|
2129
|
-
private listeners;
|
|
2130
|
-
constructor(config?: CircuitBreakerConfig);
|
|
2131
|
-
/**
|
|
2132
|
-
* Get current circuit state
|
|
2133
|
-
*/
|
|
2134
|
-
getState(): CircuitState;
|
|
2135
|
-
/**
|
|
2136
|
-
* Check if circuit allows requests
|
|
2137
|
-
*/
|
|
2138
|
-
isAllowed(): boolean;
|
|
2139
|
-
/**
|
|
2140
|
-
* Execute a function through the circuit breaker
|
|
2141
|
-
*/
|
|
2142
|
-
execute<T>(fn: () => Promise<T>): Promise<T>;
|
|
2143
|
-
/**
|
|
2144
|
-
* Record a successful call
|
|
2145
|
-
*/
|
|
2146
|
-
recordSuccess(): void;
|
|
2147
|
-
/**
|
|
2148
|
-
* Record a failed call
|
|
2149
|
-
*/
|
|
2150
|
-
recordFailure(error: Error): void;
|
|
2151
|
-
/**
|
|
2152
|
-
* Manually reset the circuit breaker
|
|
2153
|
-
*/
|
|
2154
|
-
reset(): void;
|
|
2155
|
-
/**
|
|
2156
|
-
* Subscribe to events
|
|
2157
|
-
*/
|
|
2158
|
-
on<K extends keyof CircuitBreakerEvents>(event: K, listener: CircuitBreakerEvents[K]): () => void;
|
|
2159
|
-
/**
|
|
2160
|
-
* Get failure count
|
|
2161
|
-
*/
|
|
2162
|
-
getFailureCount(): number;
|
|
2163
|
-
/**
|
|
2164
|
-
* Get config
|
|
2165
|
-
*/
|
|
2166
|
-
getConfig(): Required<CircuitBreakerConfig>;
|
|
2167
|
-
private updateState;
|
|
2168
|
-
private transitionTo;
|
|
2169
|
-
private emit;
|
|
2170
|
-
}
|
|
2171
|
-
/**
|
|
2172
|
-
* Get or create a circuit breaker by name
|
|
2173
|
-
*/
|
|
2174
|
-
declare function getCircuitBreaker(name?: string, config?: CircuitBreakerConfig): CircuitBreaker;
|
|
2175
|
-
/**
|
|
2176
|
-
* Reset all circuit breakers
|
|
2177
|
-
*/
|
|
2178
|
-
declare function resetAllCircuitBreakers(): void;
|
|
2179
|
-
/**
|
|
2180
|
-
* Get global circuit breaker (default)
|
|
2181
|
-
*/
|
|
2182
|
-
declare function getGlobalCircuitBreaker(): CircuitBreaker;
|
|
2183
|
-
|
|
2184
2155
|
/**
|
|
2185
2156
|
* Default Transforms - Auto-transform API responses to SDK format
|
|
2186
2157
|
*
|
|
@@ -2215,6 +2186,8 @@ interface ResolvedTransforms {
|
|
|
2215
2186
|
nextCursor: string | null;
|
|
2216
2187
|
hasMore: boolean;
|
|
2217
2188
|
};
|
|
2189
|
+
playlist: (data: unknown) => PlaylistData;
|
|
2190
|
+
playlistCollection: (data: unknown) => PlaylistCollectionResponse;
|
|
2218
2191
|
}
|
|
2219
2192
|
declare function createTransforms(config?: TransformConfig, logger?: ILogger): ResolvedTransforms;
|
|
2220
2193
|
|
|
@@ -2280,6 +2253,14 @@ declare class RESTDataAdapter implements IDataSource {
|
|
|
2280
2253
|
* - share (optional)
|
|
2281
2254
|
*/
|
|
2282
2255
|
|
|
2256
|
+
/**
|
|
2257
|
+
* Report body input from SDK
|
|
2258
|
+
*/
|
|
2259
|
+
interface ReportBodyInput {
|
|
2260
|
+
contentId: string;
|
|
2261
|
+
reasonId: string;
|
|
2262
|
+
description?: string;
|
|
2263
|
+
}
|
|
2283
2264
|
/**
|
|
2284
2265
|
* REST Interaction Adapter configuration
|
|
2285
2266
|
*/
|
|
@@ -2287,6 +2268,10 @@ interface RESTInteractionAdapterConfig {
|
|
|
2287
2268
|
httpClient: HttpClient;
|
|
2288
2269
|
endpoints: RESTEndpointMap['interaction'];
|
|
2289
2270
|
logger?: ILogger;
|
|
2271
|
+
/** Custom transform for report reasons response */
|
|
2272
|
+
transformReportReasons?: (apiResponse: unknown) => ReportReasonItem[];
|
|
2273
|
+
/** Custom transform for report request body */
|
|
2274
|
+
transformReportBody?: (input: ReportBodyInput) => Record<string, unknown>;
|
|
2290
2275
|
}
|
|
2291
2276
|
/**
|
|
2292
2277
|
* REST Interaction Adapter
|
|
@@ -2295,6 +2280,8 @@ declare class RESTInteractionAdapter implements IInteraction {
|
|
|
2295
2280
|
private readonly httpClient;
|
|
2296
2281
|
private readonly endpoints;
|
|
2297
2282
|
private readonly logger?;
|
|
2283
|
+
private readonly customTransformReportReasons?;
|
|
2284
|
+
private readonly customTransformReportBody?;
|
|
2298
2285
|
constructor(config: RESTInteractionAdapterConfig);
|
|
2299
2286
|
/**
|
|
2300
2287
|
* Like a video
|
|
@@ -2332,6 +2319,38 @@ declare class RESTInteractionAdapter implements IInteraction {
|
|
|
2332
2319
|
* Share a video (optional tracking)
|
|
2333
2320
|
*/
|
|
2334
2321
|
share(videoId: string, platform?: string): Promise<void>;
|
|
2322
|
+
/**
|
|
2323
|
+
* Report content (video or image post)
|
|
2324
|
+
*
|
|
2325
|
+
* @param contentId - ID of the content to report
|
|
2326
|
+
* @param reason - Report reason code/ID
|
|
2327
|
+
* @param description - Optional additional description
|
|
2328
|
+
*/
|
|
2329
|
+
report(contentId: string, reason: string, description?: string): Promise<void>;
|
|
2330
|
+
/**
|
|
2331
|
+
* Get available report reasons
|
|
2332
|
+
*
|
|
2333
|
+
* @returns Array of report reasons, or empty array if not configured
|
|
2334
|
+
*/
|
|
2335
|
+
getReportReasons(): Promise<ReportReason[]>;
|
|
2336
|
+
/**
|
|
2337
|
+
* Mark content as "not interested"
|
|
2338
|
+
*
|
|
2339
|
+
* Used for recommendation algorithm feedback.
|
|
2340
|
+
* Content should be hidden from feed after this action.
|
|
2341
|
+
*
|
|
2342
|
+
* @param contentId - ID of the content (video or image post)
|
|
2343
|
+
*/
|
|
2344
|
+
notInterested(contentId: string): Promise<void>;
|
|
2345
|
+
/**
|
|
2346
|
+
* Default transform for API report reasons response to ReportReason[]
|
|
2347
|
+
*
|
|
2348
|
+
* Expected format: [{ id, label, description }]
|
|
2349
|
+
* Or wrapped: { data: [{ id, label, description }] }
|
|
2350
|
+
*
|
|
2351
|
+
* For custom API formats, use `transforms.reportReasons` in preset config.
|
|
2352
|
+
*/
|
|
2353
|
+
private transformReportReasons;
|
|
2335
2354
|
/**
|
|
2336
2355
|
* Transform API comment response to Comment type
|
|
2337
2356
|
*/
|
|
@@ -2701,4 +2720,4 @@ declare class RESTCommentAdapter implements ICommentAdapter {
|
|
|
2701
2720
|
private unwrapResponse;
|
|
2702
2721
|
}
|
|
2703
2722
|
|
|
2704
|
-
export { type AuthConfig, type AuthError, type BatchAnalyticsConfig, type BatchAnalyticsContext, type BatchAnalyticsDeviceType, type BatchAnalyticsEventData, type BatchAnalyticsEventTransformer, type BatchAnalyticsEventType, type BatchAnalyticsNetworkType, type BatchAnalyticsRequestBody, type BatchAnalyticsRequestEvent, type BrowserAdaptersConfig, BrowserPosterLoader, BrowserVideoLoader, type BrowserVideoLoaderConfig,
|
|
2723
|
+
export { type AuthConfig, type AuthError, type BatchAnalyticsConfig, type BatchAnalyticsContext, type BatchAnalyticsDeviceType, type BatchAnalyticsEventData, type BatchAnalyticsEventTransformer, type BatchAnalyticsEventType, type BatchAnalyticsNetworkType, type BatchAnalyticsRequestBody, type BatchAnalyticsRequestEvent, type BrowserAdaptersConfig, BrowserPosterLoader, BrowserVideoLoader, type BrowserVideoLoaderConfig, DEFAULT_REQUEST_CONFIG, DEFAULT_RETRY_CONFIG, type FieldMapConfig, type FullPresetAdapters, HttpClient, HttpError, LocalSessionStorageAdapter, LocalStorageAdapter, type LocalStorageConfig, MockAnalyticsAdapter, type MockAnalyticsAdapterOptions, MockCommentAdapter, MockDataAdapter, type MockDataAdapterOptions, MockInteractionAdapter, type MockInteractionAdapterOptions, MockLoggerAdapter, type MockLoggerAdapterOptions, MockNetworkAdapter, type MockNetworkAdapterOptions, MockPlaylistAdapter, MockPosterLoader, MockSessionStorageAdapter, type MockSessionStorageAdapterOptions, MockStorageAdapter, type MockStorageAdapterOptions, MockVideoLoader, type MockVideoLoaderOptions, type PresetAdapters, RESTAnalyticsAdapter, type RESTAnalyticsAdapterConfig, RESTCommentAdapter, type RESTCommentAdapterConfig, RESTDataAdapter, type RESTEndpointMap, RESTInteractionAdapter, type RESTPresetConfig, type RESTRequestConfig, RESTViewTrackingAdapter, type RESTViewTrackingAdapterConfig, type ResolvedTransforms, type RetryConfig, type TransformConfig, type ViewEventData, type ViewEventRequestBody, type ViewEventTransformer, type ViewTrackingConfig, WebNetworkAdapter, type WebNetworkConfig, createBrowserAdapters, createBrowserPosterLoader, createBrowserVideoLoader, createLocalStorageAdapter, createNoOpAnalyticsAdapter, createRESTAdapters, createSessionStorageAdapter, createTransforms, createWebNetworkAdapter, defaultFeedResponseTransform, defaultVideoItemTransform };
|
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",
|
|
@@ -576,6 +596,86 @@ var MockDataAdapter = class {
|
|
|
576
596
|
}
|
|
577
597
|
};
|
|
578
598
|
|
|
599
|
+
// src/playlist/MockPlaylistAdapter.ts
|
|
600
|
+
var MOCK_PLAYLISTS = [
|
|
601
|
+
{
|
|
602
|
+
id: "p1",
|
|
603
|
+
title: "Workout Jams",
|
|
604
|
+
description: "High energy tracks for your workout",
|
|
605
|
+
cover: "https://images.unsplash.com/photo-1534438327276-14e5300c3a48?w=400&h=600&fit=crop",
|
|
606
|
+
totalItems: 4,
|
|
607
|
+
items: []
|
|
608
|
+
// Populated on fetchPlaylist
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
id: "p2",
|
|
612
|
+
title: "Chill Vibes",
|
|
613
|
+
description: "Relax and unwind with these lo-fi beats",
|
|
614
|
+
cover: "https://images.unsplash.com/photo-1516280440614-37939bbacd81?w=400&h=600&fit=crop",
|
|
615
|
+
totalItems: 4,
|
|
616
|
+
items: []
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
id: "p3",
|
|
620
|
+
title: "Travel Diaries",
|
|
621
|
+
description: "Explore the world through music and video",
|
|
622
|
+
cover: "https://images.unsplash.com/photo-1476514525535-07fb3b4ae5f1?w=400&h=600&fit=crop",
|
|
623
|
+
totalItems: 4,
|
|
624
|
+
items: []
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
id: "p4",
|
|
628
|
+
title: "Cooking with Chef loct",
|
|
629
|
+
description: "Delicious recipes and kitchen tips",
|
|
630
|
+
cover: "https://images.unsplash.com/photo-1556910103-1c02745aae4d?w=400&h=600&fit=crop",
|
|
631
|
+
totalItems: 4,
|
|
632
|
+
items: []
|
|
633
|
+
}
|
|
634
|
+
];
|
|
635
|
+
var MockPlaylistAdapter = class {
|
|
636
|
+
constructor(options = {}) {
|
|
637
|
+
this.delay = options.delay ?? 300;
|
|
638
|
+
}
|
|
639
|
+
async fetchPlaylist(id) {
|
|
640
|
+
await this.simulateDelay();
|
|
641
|
+
const playlist = MOCK_PLAYLISTS.find((p) => p.id === id);
|
|
642
|
+
if (!playlist) throw new Error(`Playlist ${id} not found`);
|
|
643
|
+
let items = [];
|
|
644
|
+
if (id === "p1") items = MOCK_VIDEOS.slice(0, 4);
|
|
645
|
+
else if (id === "p2") items = MOCK_VIDEOS.slice(4, 8);
|
|
646
|
+
else if (id === "p3") items = MOCK_VIDEOS.slice(8, 12);
|
|
647
|
+
else if (id === "p4") items = MOCK_VIDEOS.slice(12, 16);
|
|
648
|
+
return { ...playlist, items };
|
|
649
|
+
}
|
|
650
|
+
async fetchPlaylistCollection(cursor) {
|
|
651
|
+
await this.simulateDelay();
|
|
652
|
+
const offset = cursor ? Number.parseInt(cursor, 10) : 0;
|
|
653
|
+
const limit = 4;
|
|
654
|
+
const items = MOCK_PLAYLISTS.slice(offset, offset + limit);
|
|
655
|
+
const nextOffset = offset + limit;
|
|
656
|
+
const hasMore = nextOffset < MOCK_PLAYLISTS.length;
|
|
657
|
+
const playlists = items.map((p) => ({
|
|
658
|
+
id: p.id,
|
|
659
|
+
title: p.title,
|
|
660
|
+
description: p.description,
|
|
661
|
+
cover: p.cover,
|
|
662
|
+
totalItems: p.totalItems,
|
|
663
|
+
author: { id: "a1", name: "System" },
|
|
664
|
+
updatedAt: "2 days ago"
|
|
665
|
+
}));
|
|
666
|
+
return {
|
|
667
|
+
playlists,
|
|
668
|
+
nextCursor: hasMore ? String(nextOffset) : null,
|
|
669
|
+
hasMore
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
async simulateDelay() {
|
|
673
|
+
if (this.delay > 0) {
|
|
674
|
+
await new Promise((resolve) => setTimeout(resolve, this.delay));
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
|
|
579
679
|
// src/logger/mock.ts
|
|
580
680
|
var LOG_LEVEL_PRIORITY = {
|
|
581
681
|
debug: 0,
|
|
@@ -1107,13 +1207,14 @@ var MockInteractionAdapter = class {
|
|
|
1107
1207
|
/**
|
|
1108
1208
|
* Report a video
|
|
1109
1209
|
*
|
|
1110
|
-
* @param
|
|
1111
|
-
* @param
|
|
1112
|
-
* @param
|
|
1210
|
+
* @param videoId - ID of the video to report
|
|
1211
|
+
* @param reason - Report reason code
|
|
1212
|
+
* @param description - Optional additional description
|
|
1113
1213
|
*/
|
|
1114
|
-
async report(
|
|
1214
|
+
async report(videoId, reason, description) {
|
|
1115
1215
|
await this.simulateDelay();
|
|
1116
1216
|
this.maybeThrowError();
|
|
1217
|
+
console.log("[MockInteractionAdapter] Report called (mock)", { videoId, reason, description });
|
|
1117
1218
|
}
|
|
1118
1219
|
// ═══════════════════════════════════════════════════════════════
|
|
1119
1220
|
// TESTING HELPERS (not part of IInteraction interface)
|
|
@@ -2609,6 +2710,7 @@ var RESTDataAdapter = class {
|
|
|
2609
2710
|
createFallbackVideoItem(data) {
|
|
2610
2711
|
const obj = data ?? {};
|
|
2611
2712
|
return {
|
|
2713
|
+
type: "video",
|
|
2612
2714
|
id: String(obj.id ?? obj.video_id ?? `fallback-${Date.now()}`),
|
|
2613
2715
|
source: {
|
|
2614
2716
|
url: String(obj.video_url ?? obj.url ?? ""),
|
|
@@ -2638,6 +2740,8 @@ var RESTInteractionAdapter = class {
|
|
|
2638
2740
|
this.httpClient = config.httpClient;
|
|
2639
2741
|
this.endpoints = config.endpoints;
|
|
2640
2742
|
this.logger = config.logger;
|
|
2743
|
+
this.customTransformReportReasons = config.transformReportReasons;
|
|
2744
|
+
this.customTransformReportBody = config.transformReportBody;
|
|
2641
2745
|
}
|
|
2642
2746
|
/**
|
|
2643
2747
|
* Like a video
|
|
@@ -2762,6 +2866,107 @@ var RESTInteractionAdapter = class {
|
|
|
2762
2866
|
this.logger?.warn("[RESTInteractionAdapter] share tracking failed", { error });
|
|
2763
2867
|
}
|
|
2764
2868
|
}
|
|
2869
|
+
/**
|
|
2870
|
+
* Report content (video or image post)
|
|
2871
|
+
*
|
|
2872
|
+
* @param contentId - ID of the content to report
|
|
2873
|
+
* @param reason - Report reason code/ID
|
|
2874
|
+
* @param description - Optional additional description
|
|
2875
|
+
*/
|
|
2876
|
+
async report(contentId, reason, description) {
|
|
2877
|
+
if (!this.endpoints.report) {
|
|
2878
|
+
this.logger?.warn("[RESTInteractionAdapter] report endpoint not configured");
|
|
2879
|
+
return;
|
|
2880
|
+
}
|
|
2881
|
+
try {
|
|
2882
|
+
const body = this.customTransformReportBody ? this.customTransformReportBody({ contentId, reasonId: reason, description }) : { reason, description };
|
|
2883
|
+
this.logger?.debug("[RESTInteractionAdapter] Sending report", {
|
|
2884
|
+
path: this.endpoints.report,
|
|
2885
|
+
contentId,
|
|
2886
|
+
body
|
|
2887
|
+
});
|
|
2888
|
+
await this.httpClient.request({
|
|
2889
|
+
method: "POST",
|
|
2890
|
+
path: this.endpoints.report,
|
|
2891
|
+
pathParams: { id: contentId },
|
|
2892
|
+
body
|
|
2893
|
+
});
|
|
2894
|
+
this.logger?.debug("[RESTInteractionAdapter] Report sent successfully");
|
|
2895
|
+
} catch (error) {
|
|
2896
|
+
this.logger?.error("[RESTInteractionAdapter] report failed", error);
|
|
2897
|
+
throw error;
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
/**
|
|
2901
|
+
* Get available report reasons
|
|
2902
|
+
*
|
|
2903
|
+
* @returns Array of report reasons, or empty array if not configured
|
|
2904
|
+
*/
|
|
2905
|
+
async getReportReasons() {
|
|
2906
|
+
if (!this.endpoints.reportReasons) {
|
|
2907
|
+
this.logger?.debug("[RESTInteractionAdapter] reportReasons endpoint not configured");
|
|
2908
|
+
return [];
|
|
2909
|
+
}
|
|
2910
|
+
try {
|
|
2911
|
+
const response = await this.httpClient.request({
|
|
2912
|
+
method: "GET",
|
|
2913
|
+
path: this.endpoints.reportReasons
|
|
2914
|
+
});
|
|
2915
|
+
if (this.customTransformReportReasons) {
|
|
2916
|
+
return this.customTransformReportReasons(response);
|
|
2917
|
+
}
|
|
2918
|
+
return this.transformReportReasons(response);
|
|
2919
|
+
} catch (error) {
|
|
2920
|
+
this.logger?.error("[RESTInteractionAdapter] getReportReasons failed", error);
|
|
2921
|
+
return [];
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
/**
|
|
2925
|
+
* Mark content as "not interested"
|
|
2926
|
+
*
|
|
2927
|
+
* Used for recommendation algorithm feedback.
|
|
2928
|
+
* Content should be hidden from feed after this action.
|
|
2929
|
+
*
|
|
2930
|
+
* @param contentId - ID of the content (video or image post)
|
|
2931
|
+
*/
|
|
2932
|
+
async notInterested(contentId) {
|
|
2933
|
+
if (!this.endpoints.notInterested) {
|
|
2934
|
+
this.logger?.warn("[RESTInteractionAdapter] notInterested endpoint not configured");
|
|
2935
|
+
return;
|
|
2936
|
+
}
|
|
2937
|
+
try {
|
|
2938
|
+
await this.httpClient.request({
|
|
2939
|
+
method: "POST",
|
|
2940
|
+
path: this.endpoints.notInterested,
|
|
2941
|
+
pathParams: { id: contentId }
|
|
2942
|
+
});
|
|
2943
|
+
} catch (error) {
|
|
2944
|
+
this.logger?.error("[RESTInteractionAdapter] notInterested failed", error);
|
|
2945
|
+
throw error;
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
/**
|
|
2949
|
+
* Default transform for API report reasons response to ReportReason[]
|
|
2950
|
+
*
|
|
2951
|
+
* Expected format: [{ id, label, description }]
|
|
2952
|
+
* Or wrapped: { data: [{ id, label, description }] }
|
|
2953
|
+
*
|
|
2954
|
+
* For custom API formats, use `transforms.reportReasons` in preset config.
|
|
2955
|
+
*/
|
|
2956
|
+
transformReportReasons(response) {
|
|
2957
|
+
const data = this.unwrapResponse(response);
|
|
2958
|
+
if (!Array.isArray(data)) {
|
|
2959
|
+
return [];
|
|
2960
|
+
}
|
|
2961
|
+
return data.map((item) => {
|
|
2962
|
+
const obj = item;
|
|
2963
|
+
return {
|
|
2964
|
+
id: String(obj.id ?? obj.reason_id ?? ""),
|
|
2965
|
+
label: String(obj.label ?? obj.name ?? obj.title ?? ""),
|
|
2966
|
+
description: obj.description
|
|
2967
|
+
};
|
|
2968
|
+
});
|
|
2969
|
+
}
|
|
2765
2970
|
/**
|
|
2766
2971
|
* Transform API comment response to Comment type
|
|
2767
2972
|
*/
|
|
@@ -2796,6 +3001,69 @@ var RESTInteractionAdapter = class {
|
|
|
2796
3001
|
}
|
|
2797
3002
|
};
|
|
2798
3003
|
|
|
3004
|
+
// src/preset/adapters/RESTPlaylistAdapter.ts
|
|
3005
|
+
var RESTPlaylistAdapter = class {
|
|
3006
|
+
constructor(config) {
|
|
3007
|
+
this.httpClient = config.httpClient;
|
|
3008
|
+
this.endpoint = config.endpoint;
|
|
3009
|
+
this.collectionEndpoint = config.collectionEndpoint;
|
|
3010
|
+
this.transforms = config.transforms;
|
|
3011
|
+
this.logger = config.logger;
|
|
3012
|
+
}
|
|
3013
|
+
/**
|
|
3014
|
+
* Fetch a complete playlist by ID
|
|
3015
|
+
*
|
|
3016
|
+
* @param id - The playlist ID
|
|
3017
|
+
* @returns Promise resolving to PlaylistData
|
|
3018
|
+
*/
|
|
3019
|
+
async fetchPlaylist(id) {
|
|
3020
|
+
try {
|
|
3021
|
+
this.logger?.info(`[RESTPlaylistAdapter] Fetching playlist: ${id}`);
|
|
3022
|
+
const path = this.endpoint.replace(":id", id);
|
|
3023
|
+
const response = await this.httpClient.request({
|
|
3024
|
+
method: "GET",
|
|
3025
|
+
path
|
|
3026
|
+
});
|
|
3027
|
+
const playlist = this.transforms.playlist(response);
|
|
3028
|
+
if (playlist.items.length === 0) {
|
|
3029
|
+
this.logger?.warn(`[RESTPlaylistAdapter] Playlist ${id} is empty`);
|
|
3030
|
+
}
|
|
3031
|
+
return playlist;
|
|
3032
|
+
} catch (error) {
|
|
3033
|
+
this.logger?.error(`[RESTPlaylistAdapter] Failed to fetch playlist: ${id}`, error);
|
|
3034
|
+
throw error;
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
/**
|
|
3038
|
+
* Fetch a collection of playlists
|
|
3039
|
+
*
|
|
3040
|
+
* @param cursor - Pagination cursor
|
|
3041
|
+
* @returns Promise resolving to PlaylistCollectionResponse
|
|
3042
|
+
*/
|
|
3043
|
+
async fetchPlaylistCollection(cursor) {
|
|
3044
|
+
try {
|
|
3045
|
+
if (!this.collectionEndpoint || !this.transforms.collection) {
|
|
3046
|
+
throw new Error(
|
|
3047
|
+
"[RESTPlaylistAdapter] collectionEndpoint or collection transform not configured"
|
|
3048
|
+
);
|
|
3049
|
+
}
|
|
3050
|
+
this.logger?.info(`[RESTPlaylistAdapter] Fetching playlist collection (cursor: ${cursor})`);
|
|
3051
|
+
const path = cursor ? `${this.collectionEndpoint}?cursor=${cursor}` : this.collectionEndpoint;
|
|
3052
|
+
const response = await this.httpClient.request({
|
|
3053
|
+
method: "GET",
|
|
3054
|
+
path
|
|
3055
|
+
});
|
|
3056
|
+
return this.transforms.collection(response);
|
|
3057
|
+
} catch (error) {
|
|
3058
|
+
this.logger?.error(
|
|
3059
|
+
"[RESTPlaylistAdapter] Failed to fetch playlist collection",
|
|
3060
|
+
error
|
|
3061
|
+
);
|
|
3062
|
+
throw error;
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
};
|
|
3066
|
+
|
|
2799
3067
|
// src/preset/adapters/RESTViewTrackingAdapter.ts
|
|
2800
3068
|
var DEFAULT_VIEW_EVENT_VALUE = "seek";
|
|
2801
3069
|
var DEFAULT_HEARTBEAT_INTERVAL = 1e4;
|
|
@@ -3266,6 +3534,83 @@ var HttpClient = class {
|
|
|
3266
3534
|
}
|
|
3267
3535
|
};
|
|
3268
3536
|
|
|
3537
|
+
// src/preset/transforms/playlist.ts
|
|
3538
|
+
function defaultPlaylistTransform(apiResponse, videoItemTransform, logger) {
|
|
3539
|
+
if (!apiResponse || typeof apiResponse !== "object") {
|
|
3540
|
+
logger?.error("[PlaylistTransform] Invalid API response", void 0, {
|
|
3541
|
+
apiResponse: String(apiResponse)
|
|
3542
|
+
});
|
|
3543
|
+
return createEmptyPlaylist();
|
|
3544
|
+
}
|
|
3545
|
+
const obj = apiResponse;
|
|
3546
|
+
const data = obj.data ?? obj.result ?? obj.playlist ?? obj;
|
|
3547
|
+
const rawItems = data.items ?? data.reels ?? data.videos ?? data.list ?? [];
|
|
3548
|
+
if (!Array.isArray(rawItems)) {
|
|
3549
|
+
logger?.warn("[PlaylistTransform] Items is not an array", { data });
|
|
3550
|
+
return createEmptyPlaylist(data);
|
|
3551
|
+
}
|
|
3552
|
+
const items = rawItems.map((item) => {
|
|
3553
|
+
try {
|
|
3554
|
+
return videoItemTransform(item);
|
|
3555
|
+
} catch (error) {
|
|
3556
|
+
logger?.error("[PlaylistTransform] Failed to transform item", error);
|
|
3557
|
+
return null;
|
|
3558
|
+
}
|
|
3559
|
+
}).filter((item) => item !== null);
|
|
3560
|
+
return {
|
|
3561
|
+
id: String(data.id ?? data.playlist_id ?? ""),
|
|
3562
|
+
title: String(data.title ?? data.name ?? "Untitled Playlist"),
|
|
3563
|
+
description: String(data.description ?? ""),
|
|
3564
|
+
cover: String(data.cover ?? data.cover_url ?? data.thumbnail ?? ""),
|
|
3565
|
+
items,
|
|
3566
|
+
totalItems: Number(data.total_items ?? data.total ?? items.length)
|
|
3567
|
+
};
|
|
3568
|
+
}
|
|
3569
|
+
function defaultPlaylistSummaryTransform(data) {
|
|
3570
|
+
const obj = data;
|
|
3571
|
+
const authorObj = obj.author ?? obj.user ?? obj.creator ?? {};
|
|
3572
|
+
return {
|
|
3573
|
+
id: String(obj.id ?? obj.playlist_id ?? ""),
|
|
3574
|
+
title: String(obj.title ?? obj.name ?? "Untitled Playlist"),
|
|
3575
|
+
description: String(obj.description ?? ""),
|
|
3576
|
+
cover: String(obj.cover ?? obj.cover_url ?? obj.thumbnail ?? ""),
|
|
3577
|
+
totalItems: Number(obj.total_items ?? obj.items_count ?? 0),
|
|
3578
|
+
author: {
|
|
3579
|
+
id: String(authorObj.id ?? authorObj.user_id ?? ""),
|
|
3580
|
+
name: String(authorObj.name ?? authorObj.display_name ?? authorObj.username ?? "Unknown")
|
|
3581
|
+
},
|
|
3582
|
+
updatedAt: String(obj.updated_at ?? obj.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString())
|
|
3583
|
+
};
|
|
3584
|
+
}
|
|
3585
|
+
function defaultPlaylistCollectionTransform(apiResponse, logger) {
|
|
3586
|
+
if (!apiResponse || typeof apiResponse !== "object") {
|
|
3587
|
+
return { playlists: [], nextCursor: null, hasMore: false };
|
|
3588
|
+
}
|
|
3589
|
+
const obj = apiResponse;
|
|
3590
|
+
const data = obj.data ?? obj.result ?? obj;
|
|
3591
|
+
const rawPlaylists = data.playlists ?? data.items ?? data.list ?? [];
|
|
3592
|
+
if (!Array.isArray(rawPlaylists)) {
|
|
3593
|
+
logger?.warn("[PlaylistTransform] Playlists is not an array", { data });
|
|
3594
|
+
return { playlists: [], nextCursor: null, hasMore: false };
|
|
3595
|
+
}
|
|
3596
|
+
const playlists = rawPlaylists.map(defaultPlaylistSummaryTransform);
|
|
3597
|
+
const nextCursor = String(data.next_cursor ?? data.cursor ?? data.nextCursor ?? null);
|
|
3598
|
+
const hasMore = Boolean(data.has_more ?? data.hasMore ?? (nextCursor && nextCursor !== "null"));
|
|
3599
|
+
return {
|
|
3600
|
+
playlists,
|
|
3601
|
+
nextCursor: nextCursor === "null" ? null : nextCursor,
|
|
3602
|
+
hasMore
|
|
3603
|
+
};
|
|
3604
|
+
}
|
|
3605
|
+
function createEmptyPlaylist(data) {
|
|
3606
|
+
return {
|
|
3607
|
+
id: String(data?.id ?? ""),
|
|
3608
|
+
title: String(data?.title ?? "Empty Playlist"),
|
|
3609
|
+
items: [],
|
|
3610
|
+
totalItems: 0
|
|
3611
|
+
};
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3269
3614
|
// src/preset/transforms/defaults.ts
|
|
3270
3615
|
function getNestedValue(obj, path) {
|
|
3271
3616
|
if (!obj || typeof obj !== "object") return void 0;
|
|
@@ -3373,6 +3718,7 @@ function defaultVideoItemTransform(apiResponse, fieldMap, logger) {
|
|
|
3373
3718
|
const author = defaultAuthorTransform(obj);
|
|
3374
3719
|
const stats = defaultStatsTransform(obj);
|
|
3375
3720
|
const videoItem = {
|
|
3721
|
+
type: "video",
|
|
3376
3722
|
id,
|
|
3377
3723
|
source,
|
|
3378
3724
|
poster: toSafeString(
|
|
@@ -3454,7 +3800,13 @@ function defaultFeedResponseTransform(apiResponse, fieldMap, logger) {
|
|
|
3454
3800
|
function createTransforms(config, logger) {
|
|
3455
3801
|
return {
|
|
3456
3802
|
videoItem: config?.videoItem ? config.videoItem : (data) => defaultVideoItemTransform(data, config?.fieldMap?.video, logger),
|
|
3457
|
-
feedResponse: config?.feedResponse ? config.feedResponse : (data) => defaultFeedResponseTransform(data, config?.fieldMap?.feed, logger)
|
|
3803
|
+
feedResponse: config?.feedResponse ? config.feedResponse : (data) => defaultFeedResponseTransform(data, config?.fieldMap?.feed, logger),
|
|
3804
|
+
playlist: config?.playlist ? config.playlist : (data) => defaultPlaylistTransform(
|
|
3805
|
+
data,
|
|
3806
|
+
(item) => config?.videoItem ? config.videoItem(item) : defaultVideoItemTransform(item, config?.fieldMap?.video, logger),
|
|
3807
|
+
logger
|
|
3808
|
+
),
|
|
3809
|
+
playlistCollection: config?.playlistCollection ? config.playlistCollection : (data) => defaultPlaylistCollectionTransform(data, logger)
|
|
3458
3810
|
};
|
|
3459
3811
|
}
|
|
3460
3812
|
|
|
@@ -3506,7 +3858,9 @@ function createRESTAdapters(config) {
|
|
|
3506
3858
|
const interaction = new RESTInteractionAdapter({
|
|
3507
3859
|
httpClient,
|
|
3508
3860
|
endpoints: endpoints.interaction,
|
|
3509
|
-
logger
|
|
3861
|
+
logger,
|
|
3862
|
+
transformReportReasons: transforms?.reportReasons,
|
|
3863
|
+
transformReportBody: transforms?.reportBody
|
|
3510
3864
|
});
|
|
3511
3865
|
let analytics;
|
|
3512
3866
|
if (endpoints.viewTracking) {
|
|
@@ -3531,11 +3885,22 @@ function createRESTAdapters(config) {
|
|
|
3531
3885
|
endpoints: endpoints.comment,
|
|
3532
3886
|
logger
|
|
3533
3887
|
}) : void 0;
|
|
3888
|
+
const playlist = endpoints.playlist ? new RESTPlaylistAdapter({
|
|
3889
|
+
httpClient,
|
|
3890
|
+
endpoint: endpoints.playlist.detail,
|
|
3891
|
+
collectionEndpoint: endpoints.playlist.list,
|
|
3892
|
+
transforms: {
|
|
3893
|
+
playlist: resolvedTransforms.playlist,
|
|
3894
|
+
collection: (data) => resolvedTransforms.playlistCollection(data)
|
|
3895
|
+
},
|
|
3896
|
+
logger
|
|
3897
|
+
}) : void 0;
|
|
3534
3898
|
return {
|
|
3535
3899
|
dataSource,
|
|
3536
3900
|
interaction,
|
|
3537
3901
|
analytics,
|
|
3538
|
-
comment
|
|
3902
|
+
comment,
|
|
3903
|
+
playlist
|
|
3539
3904
|
};
|
|
3540
3905
|
}
|
|
3541
3906
|
|
|
@@ -4170,174 +4535,9 @@ function createBrowserAdapters(config) {
|
|
|
4170
4535
|
videoLoader,
|
|
4171
4536
|
posterLoader,
|
|
4172
4537
|
comment,
|
|
4538
|
+
playlist: restAdapters.playlist,
|
|
4173
4539
|
logger: config.logger
|
|
4174
4540
|
};
|
|
4175
4541
|
}
|
|
4176
4542
|
|
|
4177
|
-
|
|
4178
|
-
var DEFAULT_CONFIG3 = {
|
|
4179
|
-
failureThreshold: 5,
|
|
4180
|
-
failureWindow: 3e4,
|
|
4181
|
-
cooldownPeriod: 3e4,
|
|
4182
|
-
successThreshold: 1,
|
|
4183
|
-
name: "default"
|
|
4184
|
-
};
|
|
4185
|
-
var CircuitOpenError = class extends Error {
|
|
4186
|
-
constructor(message = "Circuit breaker is open") {
|
|
4187
|
-
super(message);
|
|
4188
|
-
this.name = "CircuitOpenError";
|
|
4189
|
-
}
|
|
4190
|
-
};
|
|
4191
|
-
var CircuitBreaker = class {
|
|
4192
|
-
constructor(config = {}) {
|
|
4193
|
-
this.state = "closed";
|
|
4194
|
-
this.failures = [];
|
|
4195
|
-
this.lastFailureTime = 0;
|
|
4196
|
-
this.halfOpenSuccesses = 0;
|
|
4197
|
-
this.listeners = /* @__PURE__ */ new Map();
|
|
4198
|
-
this.config = { ...DEFAULT_CONFIG3, ...config };
|
|
4199
|
-
}
|
|
4200
|
-
/**
|
|
4201
|
-
* Get current circuit state
|
|
4202
|
-
*/
|
|
4203
|
-
getState() {
|
|
4204
|
-
this.updateState();
|
|
4205
|
-
return this.state;
|
|
4206
|
-
}
|
|
4207
|
-
/**
|
|
4208
|
-
* Check if circuit allows requests
|
|
4209
|
-
*/
|
|
4210
|
-
isAllowed() {
|
|
4211
|
-
this.updateState();
|
|
4212
|
-
return this.state !== "open";
|
|
4213
|
-
}
|
|
4214
|
-
/**
|
|
4215
|
-
* Execute a function through the circuit breaker
|
|
4216
|
-
*/
|
|
4217
|
-
async execute(fn) {
|
|
4218
|
-
this.updateState();
|
|
4219
|
-
if (this.state === "open") {
|
|
4220
|
-
this.emit("rejected");
|
|
4221
|
-
throw new CircuitOpenError(`Circuit breaker [${this.config.name}] is open`);
|
|
4222
|
-
}
|
|
4223
|
-
try {
|
|
4224
|
-
const result = await fn();
|
|
4225
|
-
this.recordSuccess();
|
|
4226
|
-
return result;
|
|
4227
|
-
} catch (error) {
|
|
4228
|
-
this.recordFailure(error);
|
|
4229
|
-
throw error;
|
|
4230
|
-
}
|
|
4231
|
-
}
|
|
4232
|
-
/**
|
|
4233
|
-
* Record a successful call
|
|
4234
|
-
*/
|
|
4235
|
-
recordSuccess() {
|
|
4236
|
-
if (this.state === "half-open") {
|
|
4237
|
-
this.halfOpenSuccesses++;
|
|
4238
|
-
if (this.halfOpenSuccesses >= this.config.successThreshold) {
|
|
4239
|
-
this.transitionTo("closed");
|
|
4240
|
-
this.failures = [];
|
|
4241
|
-
this.halfOpenSuccesses = 0;
|
|
4242
|
-
}
|
|
4243
|
-
}
|
|
4244
|
-
this.emit("success");
|
|
4245
|
-
}
|
|
4246
|
-
/**
|
|
4247
|
-
* Record a failed call
|
|
4248
|
-
*/
|
|
4249
|
-
recordFailure(error) {
|
|
4250
|
-
const now = Date.now();
|
|
4251
|
-
this.failures.push(now);
|
|
4252
|
-
this.lastFailureTime = now;
|
|
4253
|
-
const cutoff = now - this.config.failureWindow;
|
|
4254
|
-
this.failures = this.failures.filter((t) => t > cutoff);
|
|
4255
|
-
this.emit("failure", error, this.failures.length);
|
|
4256
|
-
if (this.state === "half-open") {
|
|
4257
|
-
this.transitionTo("open");
|
|
4258
|
-
this.halfOpenSuccesses = 0;
|
|
4259
|
-
} else if (this.state === "closed" && this.failures.length >= this.config.failureThreshold) {
|
|
4260
|
-
this.transitionTo("open");
|
|
4261
|
-
}
|
|
4262
|
-
}
|
|
4263
|
-
/**
|
|
4264
|
-
* Manually reset the circuit breaker
|
|
4265
|
-
*/
|
|
4266
|
-
reset() {
|
|
4267
|
-
this.failures = [];
|
|
4268
|
-
this.halfOpenSuccesses = 0;
|
|
4269
|
-
this.transitionTo("closed");
|
|
4270
|
-
}
|
|
4271
|
-
/**
|
|
4272
|
-
* Subscribe to events
|
|
4273
|
-
*/
|
|
4274
|
-
on(event, listener) {
|
|
4275
|
-
if (!this.listeners.has(event)) {
|
|
4276
|
-
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
4277
|
-
}
|
|
4278
|
-
const listeners = this.listeners.get(event);
|
|
4279
|
-
if (listeners) {
|
|
4280
|
-
listeners.add(listener);
|
|
4281
|
-
}
|
|
4282
|
-
return () => {
|
|
4283
|
-
const listeners2 = this.listeners.get(event);
|
|
4284
|
-
if (listeners2) {
|
|
4285
|
-
listeners2.delete(listener);
|
|
4286
|
-
}
|
|
4287
|
-
};
|
|
4288
|
-
}
|
|
4289
|
-
/**
|
|
4290
|
-
* Get failure count
|
|
4291
|
-
*/
|
|
4292
|
-
getFailureCount() {
|
|
4293
|
-
return this.failures.length;
|
|
4294
|
-
}
|
|
4295
|
-
/**
|
|
4296
|
-
* Get config
|
|
4297
|
-
*/
|
|
4298
|
-
getConfig() {
|
|
4299
|
-
return { ...this.config };
|
|
4300
|
-
}
|
|
4301
|
-
updateState() {
|
|
4302
|
-
if (this.state === "open") {
|
|
4303
|
-
const timeSinceLastFailure = Date.now() - this.lastFailureTime;
|
|
4304
|
-
if (timeSinceLastFailure >= this.config.cooldownPeriod) {
|
|
4305
|
-
this.transitionTo("half-open");
|
|
4306
|
-
this.halfOpenSuccesses = 0;
|
|
4307
|
-
}
|
|
4308
|
-
}
|
|
4309
|
-
}
|
|
4310
|
-
transitionTo(newState) {
|
|
4311
|
-
if (this.state === newState) return;
|
|
4312
|
-
const previousState = this.state;
|
|
4313
|
-
this.state = newState;
|
|
4314
|
-
this.emit("stateChange", newState, previousState);
|
|
4315
|
-
}
|
|
4316
|
-
emit(event, ...args) {
|
|
4317
|
-
const listeners = this.listeners.get(event);
|
|
4318
|
-
if (listeners) {
|
|
4319
|
-
for (const listener of listeners) {
|
|
4320
|
-
listener(...args);
|
|
4321
|
-
}
|
|
4322
|
-
}
|
|
4323
|
-
}
|
|
4324
|
-
};
|
|
4325
|
-
var circuitBreakers = /* @__PURE__ */ new Map();
|
|
4326
|
-
function getCircuitBreaker(name = "default", config) {
|
|
4327
|
-
let breaker = circuitBreakers.get(name);
|
|
4328
|
-
if (!breaker) {
|
|
4329
|
-
breaker = new CircuitBreaker({ ...config, name });
|
|
4330
|
-
circuitBreakers.set(name, breaker);
|
|
4331
|
-
}
|
|
4332
|
-
return breaker;
|
|
4333
|
-
}
|
|
4334
|
-
function resetAllCircuitBreakers() {
|
|
4335
|
-
for (const breaker of circuitBreakers.values()) {
|
|
4336
|
-
breaker.reset();
|
|
4337
|
-
}
|
|
4338
|
-
}
|
|
4339
|
-
function getGlobalCircuitBreaker() {
|
|
4340
|
-
return getCircuitBreaker("global");
|
|
4341
|
-
}
|
|
4342
|
-
|
|
4343
|
-
export { BrowserPosterLoader, BrowserVideoLoader, CircuitBreaker, CircuitOpenError, DEFAULT_REQUEST_CONFIG, DEFAULT_RETRY_CONFIG, HttpClient, HttpError, LocalSessionStorageAdapter, LocalStorageAdapter, MockAnalyticsAdapter, MockCommentAdapter, MockDataAdapter, MockInteractionAdapter, MockLoggerAdapter, MockNetworkAdapter, MockPosterLoader, MockSessionStorageAdapter, MockStorageAdapter, MockVideoLoader, RESTAnalyticsAdapter, RESTCommentAdapter, RESTDataAdapter, RESTInteractionAdapter, RESTViewTrackingAdapter, WebNetworkAdapter, createBrowserAdapters, createBrowserPosterLoader, createBrowserVideoLoader, createLocalStorageAdapter, createNoOpAnalyticsAdapter, createRESTAdapters, createSessionStorageAdapter, createTransforms, createWebNetworkAdapter, defaultFeedResponseTransform, defaultVideoItemTransform, getCircuitBreaker, getGlobalCircuitBreaker, resetAllCircuitBreakers };
|
|
4543
|
+
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 };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xhub-short/adapters",
|
|
3
3
|
"sideEffects": false,
|
|
4
|
-
"version": "0.1.0-beta.
|
|
4
|
+
"version": "0.1.0-beta.12",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"dist"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@xhub-short/contracts": "0.1.0-beta.
|
|
23
|
+
"@xhub-short/contracts": "0.1.0-beta.12"
|
|
24
24
|
},
|
|
25
25
|
"optionalDependencies": {
|
|
26
26
|
"hls.js": "^1.5.0"
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"tsup": "^8.3.0",
|
|
30
30
|
"typescript": "^5.7.0",
|
|
31
31
|
"vitest": "^2.1.0",
|
|
32
|
-
"@xhub-short/
|
|
33
|
-
"@xhub-short/
|
|
32
|
+
"@xhub-short/vitest-config": "0.1.0-beta.11",
|
|
33
|
+
"@xhub-short/tsconfig": "0.0.1-beta.0"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "tsup",
|