@redseat/api 0.3.6 → 0.3.7
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/CLAUDE.md +1 -1
- package/README.md +137 -132
- package/{AGENTS.md → agents.md} +275 -275
- package/client.md +670 -670
- package/dist/client.d.ts +3 -1
- package/dist/client.js +2 -0
- package/dist/interfaces.d.ts +151 -0
- package/dist/sse-types.d.ts +15 -1
- package/encryption.md +533 -533
- package/firebase.md +602 -602
- package/libraries.md +55 -20
- package/package.json +49 -49
- package/server.md +538 -398
- package/test.md +291 -291
package/client.md
CHANGED
|
@@ -1,670 +1,670 @@
|
|
|
1
|
-
# RedseatClient
|
|
2
|
-
|
|
3
|
-
The `RedseatClient` class is the low-level HTTP client that handles all communication with Redseat servers. It provides automatic token management, local server detection, and request/response interceptors.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
`RedseatClient` wraps Axios and adds:
|
|
8
|
-
- Automatic token refresh before expiration
|
|
9
|
-
- Local server detection (for development)
|
|
10
|
-
- 401 error handling with automatic retry
|
|
11
|
-
- Request/response interceptors for authentication
|
|
12
|
-
|
|
13
|
-
## Constructor
|
|
14
|
-
|
|
15
|
-
```typescript
|
|
16
|
-
new RedseatClient(options: ClientOptions)
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
### ClientOptions Interface
|
|
20
|
-
|
|
21
|
-
```typescript
|
|
22
|
-
interface ClientOptions {
|
|
23
|
-
server: IServer;
|
|
24
|
-
getIdToken: () => Promise<string>;
|
|
25
|
-
refreshThreshold?: number; // milliseconds before expiration to refresh (default: 5 minutes)
|
|
26
|
-
}
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
**Parameters:**
|
|
30
|
-
- `server`: Server configuration object with `id`, `url`, and optional `port`
|
|
31
|
-
- `getIdToken`: Async function that returns the current ID token from your auth provider
|
|
32
|
-
- `refreshThreshold`: Optional. Milliseconds before token expiration to trigger refresh (default: 300000 = 5 minutes)
|
|
33
|
-
|
|
34
|
-
**Example:**
|
|
35
|
-
```typescript
|
|
36
|
-
const client = new RedseatClient({
|
|
37
|
-
server: {
|
|
38
|
-
id: 'server-123',
|
|
39
|
-
url: 'example.com',
|
|
40
|
-
port: 443
|
|
41
|
-
},
|
|
42
|
-
getIdToken: async () => {
|
|
43
|
-
// Get ID token from your auth provider (Firebase, Auth0, etc.)
|
|
44
|
-
return await getCurrentUserToken();
|
|
45
|
-
},
|
|
46
|
-
refreshThreshold: 5 * 60 * 1000 // 5 minutes
|
|
47
|
-
});
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
## Features
|
|
51
|
-
|
|
52
|
-
### Automatic Token Refresh
|
|
53
|
-
|
|
54
|
-
The client automatically refreshes tokens before they expire. The refresh happens:
|
|
55
|
-
- Before each request if the token is expired or expiring soon
|
|
56
|
-
- When a 401 error is received (with automatic retry)
|
|
57
|
-
|
|
58
|
-
### Local Server Detection
|
|
59
|
-
|
|
60
|
-
The client automatically detects if a local development server is available by checking `local.{server.url}`. If detected, it uses the local URL instead of the production URL.
|
|
61
|
-
|
|
62
|
-
### Request Interceptor
|
|
63
|
-
|
|
64
|
-
All requests are intercepted to:
|
|
65
|
-
1. Check if token needs refresh
|
|
66
|
-
2. Add `Authorization: Bearer {token}` header
|
|
67
|
-
|
|
68
|
-
### Response Interceptor
|
|
69
|
-
|
|
70
|
-
All responses are intercepted to:
|
|
71
|
-
1. Handle 401 errors by refreshing token and retrying the request
|
|
72
|
-
2. Prevent infinite retry loops with `_retry` flag
|
|
73
|
-
|
|
74
|
-
## Methods
|
|
75
|
-
|
|
76
|
-
### `get<T>(url: string, config?: AxiosRequestConfig)`
|
|
77
|
-
|
|
78
|
-
Performs a GET request.
|
|
79
|
-
|
|
80
|
-
**Parameters:**
|
|
81
|
-
- `url`: The endpoint URL (relative to base URL)
|
|
82
|
-
- `config`: Optional Axios request configuration
|
|
83
|
-
|
|
84
|
-
**Returns:** `Promise<AxiosResponse<T>>`
|
|
85
|
-
|
|
86
|
-
**Example:**
|
|
87
|
-
```typescript
|
|
88
|
-
const response = await client.get<IFile[]>('/libraries/123/medias');
|
|
89
|
-
const medias = response.data;
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
### `post<T>(url: string, data?: unknown, config?: AxiosRequestConfig)`
|
|
93
|
-
|
|
94
|
-
Performs a POST request.
|
|
95
|
-
|
|
96
|
-
**Parameters:**
|
|
97
|
-
- `url`: The endpoint URL
|
|
98
|
-
- `data`: Request body data
|
|
99
|
-
- `config`: Optional Axios request configuration
|
|
100
|
-
|
|
101
|
-
**Returns:** `Promise<AxiosResponse<T>>`
|
|
102
|
-
|
|
103
|
-
**Example:**
|
|
104
|
-
```typescript
|
|
105
|
-
const response = await client.post<ITag>('/libraries/123/tags', {
|
|
106
|
-
name: 'Vacation'
|
|
107
|
-
});
|
|
108
|
-
const tag = response.data;
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### `postForm<T>(url: string, data?: unknown, config?: AxiosRequestConfig)`
|
|
112
|
-
|
|
113
|
-
Performs a POST request with FormData. This method is specifically designed for multipart/form-data uploads and provides better compatibility with React Native environments compared to using `post` with FormData.
|
|
114
|
-
|
|
115
|
-
**Parameters:**
|
|
116
|
-
- `url`: The endpoint URL
|
|
117
|
-
- `data`: FormData object or data to send as form data
|
|
118
|
-
- `config`: Optional Axios request configuration (supports `onUploadProgress` for progress tracking)
|
|
119
|
-
|
|
120
|
-
**Returns:** `Promise<AxiosResponse<T>>`
|
|
121
|
-
|
|
122
|
-
**Example:**
|
|
123
|
-
```typescript
|
|
124
|
-
const formData = new FormData();
|
|
125
|
-
formData.append('info', JSON.stringify({ name: 'photo.jpg', size: 1024 }));
|
|
126
|
-
formData.append('file', fileBlob, 'photo.jpg');
|
|
127
|
-
|
|
128
|
-
const response = await client.postForm<IFile>('/libraries/123/medias', formData, {
|
|
129
|
-
onUploadProgress: (progressEvent) => {
|
|
130
|
-
if (progressEvent.total) {
|
|
131
|
-
const percent = (progressEvent.loaded / progressEvent.total) * 100;
|
|
132
|
-
console.log(`Upload progress: ${percent}%`);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
const uploadedFile = response.data;
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
**Note:** Use `postForm` instead of `post` when sending FormData, especially in React Native applications, as it properly handles multipart/form-data headers and encoding.
|
|
140
|
-
|
|
141
|
-
### `put<T>(url: string, data?: unknown, config?: AxiosRequestConfig)`
|
|
142
|
-
|
|
143
|
-
Performs a PUT request.
|
|
144
|
-
|
|
145
|
-
**Parameters:**
|
|
146
|
-
- `url`: The endpoint URL
|
|
147
|
-
- `data`: Request body data
|
|
148
|
-
- `config`: Optional Axios request configuration
|
|
149
|
-
|
|
150
|
-
**Returns:** `Promise<AxiosResponse<T>>`
|
|
151
|
-
|
|
152
|
-
**Example:**
|
|
153
|
-
```typescript
|
|
154
|
-
const formData = new FormData();
|
|
155
|
-
formData.append('file', fileBlob);
|
|
156
|
-
const response = await client.put('/libraries/123/medias/transfert', formData);
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
### `patch<T>(url: string, data?: unknown, config?: AxiosRequestConfig)`
|
|
160
|
-
|
|
161
|
-
Performs a PATCH request.
|
|
162
|
-
|
|
163
|
-
**Parameters:**
|
|
164
|
-
- `url`: The endpoint URL
|
|
165
|
-
- `data`: Request body data
|
|
166
|
-
- `config`: Optional Axios request configuration
|
|
167
|
-
|
|
168
|
-
**Returns:** `Promise<AxiosResponse<T>>`
|
|
169
|
-
|
|
170
|
-
**Example:**
|
|
171
|
-
```typescript
|
|
172
|
-
const response = await client.patch<ITag>('/libraries/123/tags/tag-id', {
|
|
173
|
-
rename: 'New Tag Name'
|
|
174
|
-
});
|
|
175
|
-
const updatedTag = response.data;
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
### `delete<T>(url: string, config?: AxiosRequestConfig)`
|
|
179
|
-
|
|
180
|
-
Performs a DELETE request.
|
|
181
|
-
|
|
182
|
-
**Parameters:**
|
|
183
|
-
- `url`: The endpoint URL
|
|
184
|
-
- `config`: Optional Axios request configuration (can include `data` for request body)
|
|
185
|
-
|
|
186
|
-
**Returns:** `Promise<AxiosResponse<T>>`
|
|
187
|
-
|
|
188
|
-
**Example:**
|
|
189
|
-
```typescript
|
|
190
|
-
// Simple delete
|
|
191
|
-
await client.delete('/libraries/123/tags/tag-id');
|
|
192
|
-
|
|
193
|
-
// Delete with body
|
|
194
|
-
await client.delete('/libraries/123/medias', {
|
|
195
|
-
data: { ids: ['media-1', 'media-2'] }
|
|
196
|
-
});
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
### `request<T>(method: Method, url: string, data?: unknown, config?: AxiosRequestConfig)`
|
|
200
|
-
|
|
201
|
-
Performs a custom HTTP request.
|
|
202
|
-
|
|
203
|
-
**Parameters:**
|
|
204
|
-
- `method`: HTTP method ('GET', 'POST', 'PUT', 'PATCH', 'DELETE', etc.)
|
|
205
|
-
- `url`: The endpoint URL
|
|
206
|
-
- `data`: Request body data
|
|
207
|
-
- `config`: Optional Axios request configuration
|
|
208
|
-
|
|
209
|
-
**Returns:** `Promise<AxiosResponse<T>>`
|
|
210
|
-
|
|
211
|
-
**Example:**
|
|
212
|
-
```typescript
|
|
213
|
-
const response = await client.request<IFile>(
|
|
214
|
-
'GET',
|
|
215
|
-
'/libraries/123/medias/media-id',
|
|
216
|
-
undefined,
|
|
217
|
-
{ responseType: 'blob' }
|
|
218
|
-
);
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
### `setToken(token: string | IToken)`
|
|
222
|
-
|
|
223
|
-
Manually set the authentication token. Useful when you already have a valid token.
|
|
224
|
-
|
|
225
|
-
**Parameters:**
|
|
226
|
-
- `token`: Either a token string or an `IToken` object with `token` and `expires` properties
|
|
227
|
-
|
|
228
|
-
**Example:**
|
|
229
|
-
```typescript
|
|
230
|
-
// Set as string (will be treated as expiring soon)
|
|
231
|
-
client.setToken('your-token-string');
|
|
232
|
-
|
|
233
|
-
// Set as IToken object with expiration
|
|
234
|
-
client.setToken({
|
|
235
|
-
token: 'your-token-string',
|
|
236
|
-
expires: Date.now() + 3600000 // 1 hour from now
|
|
237
|
-
});
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
## Error Handling
|
|
241
|
-
|
|
242
|
-
The client automatically handles:
|
|
243
|
-
- **401 Unauthorized**: Refreshes token and retries the request once
|
|
244
|
-
- **Token expiration**: Refreshes token before making requests
|
|
245
|
-
- **Network errors**: Passes through to caller
|
|
246
|
-
|
|
247
|
-
**Example error handling:**
|
|
248
|
-
```typescript
|
|
249
|
-
try {
|
|
250
|
-
const response = await client.get('/libraries/123/medias');
|
|
251
|
-
} catch (error) {
|
|
252
|
-
if (error.response?.status === 401) {
|
|
253
|
-
// Token refresh failed or invalid credentials
|
|
254
|
-
console.error('Authentication failed');
|
|
255
|
-
} else if (error.response?.status === 404) {
|
|
256
|
-
// Resource not found
|
|
257
|
-
console.error('Resource not found');
|
|
258
|
-
} else {
|
|
259
|
-
// Other error
|
|
260
|
-
console.error('Request failed:', error.message);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
## Usage with Response Types
|
|
266
|
-
|
|
267
|
-
The client supports specifying response types for binary data:
|
|
268
|
-
|
|
269
|
-
```typescript
|
|
270
|
-
// Get as stream
|
|
271
|
-
const stream = await client.get('/libraries/123/medias/media-id', {
|
|
272
|
-
responseType: 'stream'
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
// Get as ArrayBuffer
|
|
276
|
-
const buffer = await client.get('/libraries/123/medias/media-id', {
|
|
277
|
-
responseType: 'arraybuffer'
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
// Get as Blob
|
|
281
|
-
const blob = await client.get('/libraries/123/medias/media-id', {
|
|
282
|
-
responseType: 'blob'
|
|
283
|
-
});
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
## Progress Tracking
|
|
287
|
-
|
|
288
|
-
For file uploads, you can track progress. Use `postForm` for FormData uploads (recommended for React Native compatibility):
|
|
289
|
-
|
|
290
|
-
```typescript
|
|
291
|
-
const formData = new FormData();
|
|
292
|
-
formData.append('file', fileBlob);
|
|
293
|
-
|
|
294
|
-
await client.postForm('/libraries/123/medias', formData, {
|
|
295
|
-
onUploadProgress: (progressEvent) => {
|
|
296
|
-
if (progressEvent.total) {
|
|
297
|
-
const percent = (progressEvent.loaded / progressEvent.total) * 100;
|
|
298
|
-
console.log(`Upload progress: ${percent}%`);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
## Internal Methods (Private)
|
|
305
|
-
|
|
306
|
-
The following methods are used internally and should not be called directly:
|
|
307
|
-
- `refreshToken()` - Refreshes the authentication token
|
|
308
|
-
- `ensureValidToken()` - Ensures token is valid before requests
|
|
309
|
-
- `isTokenExpiredOrExpiringSoon()` - Checks if token needs refresh
|
|
310
|
-
- `detectLocalUrl()` - Detects local development server
|
|
311
|
-
- `getRegularServerUrl()` - Gets production server URL
|
|
312
|
-
|
|
313
|
-
---
|
|
314
|
-
|
|
315
|
-
## Server-Sent Events (SSE)
|
|
316
|
-
|
|
317
|
-
The `RedseatClient` provides real-time event streaming using Server-Sent Events (SSE). Events are delivered as RxJS Observables for easy integration with reactive programming patterns.
|
|
318
|
-
|
|
319
|
-
### Connection Management
|
|
320
|
-
|
|
321
|
-
#### `connectSSE(options?: SSEConnectionOptions): Promise<void>`
|
|
322
|
-
|
|
323
|
-
Connects to the server's SSE endpoint for real-time updates.
|
|
324
|
-
|
|
325
|
-
**Parameters:**
|
|
326
|
-
- `options`: Optional connection configuration:
|
|
327
|
-
- `libraries?`: Array of library IDs to filter events to specific libraries
|
|
328
|
-
- `autoReconnect?`: Enable auto-reconnect on disconnect (default: `true`)
|
|
329
|
-
- `maxReconnectAttempts?`: Maximum reconnect attempts (default: unlimited)
|
|
330
|
-
- `initialReconnectDelay?`: Initial reconnect delay in ms (default: `1000`)
|
|
331
|
-
- `maxReconnectDelay?`: Maximum reconnect delay in ms (default: `30000`)
|
|
332
|
-
|
|
333
|
-
**Example:**
|
|
334
|
-
```typescript
|
|
335
|
-
// Connect with default options
|
|
336
|
-
await client.connectSSE();
|
|
337
|
-
|
|
338
|
-
// Connect with library filter
|
|
339
|
-
await client.connectSSE({
|
|
340
|
-
libraries: ['library-123', 'library-456']
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
// Connect with custom reconnection settings
|
|
344
|
-
await client.connectSSE({
|
|
345
|
-
autoReconnect: true,
|
|
346
|
-
maxReconnectAttempts: 10,
|
|
347
|
-
initialReconnectDelay: 2000,
|
|
348
|
-
maxReconnectDelay: 60000
|
|
349
|
-
});
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
#### `disconnectSSE(): void`
|
|
353
|
-
|
|
354
|
-
Disconnects from the SSE endpoint and cleans up resources.
|
|
355
|
-
|
|
356
|
-
**Example:**
|
|
357
|
-
```typescript
|
|
358
|
-
client.disconnectSSE();
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
#### `dispose(): void`
|
|
362
|
-
|
|
363
|
-
Disposes of all SSE resources and completes all observables. Call this when the client is no longer needed.
|
|
364
|
-
|
|
365
|
-
**Example:**
|
|
366
|
-
```typescript
|
|
367
|
-
client.dispose();
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
#### `sseConnectionState: SSEConnectionState`
|
|
371
|
-
|
|
372
|
-
Gets the current SSE connection state. Possible values:
|
|
373
|
-
- `'disconnected'` - Not connected
|
|
374
|
-
- `'connecting'` - Connection in progress
|
|
375
|
-
- `'connected'` - Successfully connected
|
|
376
|
-
- `'reconnecting'` - Attempting to reconnect after disconnect
|
|
377
|
-
- `'error'` - Connection error occurred
|
|
378
|
-
|
|
379
|
-
**Example:**
|
|
380
|
-
```typescript
|
|
381
|
-
if (client.sseConnectionState === 'connected') {
|
|
382
|
-
console.log('SSE is connected');
|
|
383
|
-
}
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
### Connection State Observables
|
|
387
|
-
|
|
388
|
-
#### `sseConnectionState$: Observable<SSEConnectionState>`
|
|
389
|
-
|
|
390
|
-
Observable that emits connection state changes.
|
|
391
|
-
|
|
392
|
-
**Example:**
|
|
393
|
-
```typescript
|
|
394
|
-
client.sseConnectionState$.subscribe(state => {
|
|
395
|
-
console.log('SSE connection state:', state);
|
|
396
|
-
});
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
#### `sseConnected$: Observable<boolean>`
|
|
400
|
-
|
|
401
|
-
Observable that emits `true` when connected, `false` otherwise.
|
|
402
|
-
|
|
403
|
-
**Example:**
|
|
404
|
-
```typescript
|
|
405
|
-
client.sseConnected$.subscribe(connected => {
|
|
406
|
-
if (connected) {
|
|
407
|
-
console.log('SSE connected');
|
|
408
|
-
} else {
|
|
409
|
-
console.log('SSE disconnected');
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
#### `sseError$: Observable<SSEConnectionError>`
|
|
415
|
-
|
|
416
|
-
Observable that emits connection errors.
|
|
417
|
-
|
|
418
|
-
**Example:**
|
|
419
|
-
```typescript
|
|
420
|
-
client.sseError$.subscribe(error => {
|
|
421
|
-
console.error(`SSE error (${error.type}): ${error.message}`);
|
|
422
|
-
});
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
### Event Streams
|
|
426
|
-
|
|
427
|
-
All event streams are typed RxJS Observables. Subscribe to receive real-time updates from the server.
|
|
428
|
-
|
|
429
|
-
#### `library$: Observable<SSELibraryEvent>`
|
|
430
|
-
|
|
431
|
-
Emits when libraries are added, updated, or deleted.
|
|
432
|
-
|
|
433
|
-
```typescript
|
|
434
|
-
client.library$.subscribe(event => {
|
|
435
|
-
console.log(`Library ${event.action}: ${event.library.name}`);
|
|
436
|
-
});
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
#### `libraryStatus$: Observable<SSELibraryStatusEvent>`
|
|
440
|
-
|
|
441
|
-
Emits library status updates (e.g., scanning progress).
|
|
442
|
-
|
|
443
|
-
```typescript
|
|
444
|
-
client.libraryStatus$.subscribe(event => {
|
|
445
|
-
console.log(`Library ${event.library}: ${event.message} (${event.progress}%)`);
|
|
446
|
-
});
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
#### `medias$: Observable<SSEMediasEvent>`
|
|
450
|
-
|
|
451
|
-
Emits when media files are added, updated, or deleted.
|
|
452
|
-
|
|
453
|
-
```typescript
|
|
454
|
-
client.medias$.subscribe(event => {
|
|
455
|
-
for (const { action, media } of event.medias) {
|
|
456
|
-
console.log(`Media ${action}: ${media.name}`);
|
|
457
|
-
}
|
|
458
|
-
});
|
|
459
|
-
```
|
|
460
|
-
|
|
461
|
-
#### `uploadProgress$: Observable<SSEUploadProgressEvent>`
|
|
462
|
-
|
|
463
|
-
Emits upload progress updates including download, transfer, and analysis stages.
|
|
464
|
-
|
|
465
|
-
```typescript
|
|
466
|
-
client.uploadProgress$.subscribe(event => {
|
|
467
|
-
const { progress } = event;
|
|
468
|
-
const percent = progress.total ? Math.round((progress.current ?? 0) / progress.total * 100) : 0;
|
|
469
|
-
console.log(`Upload ${progress.id} (${progress.type}): ${percent}% - ${progress.filename}`);
|
|
470
|
-
});
|
|
471
|
-
```
|
|
472
|
-
|
|
473
|
-
#### `convertProgress$: Observable<SSEConvertProgressEvent>`
|
|
474
|
-
|
|
475
|
-
Emits video conversion progress updates.
|
|
476
|
-
|
|
477
|
-
```typescript
|
|
478
|
-
client.convertProgress$.subscribe(event => {
|
|
479
|
-
console.log(`Converting ${event.mediaId}: ${event.progress}% - ${event.status}`);
|
|
480
|
-
});
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
#### `episodes$: Observable<SSEEpisodesEvent>`
|
|
484
|
-
|
|
485
|
-
Emits when episodes are added, updated, or deleted.
|
|
486
|
-
|
|
487
|
-
```typescript
|
|
488
|
-
client.episodes$.subscribe(event => {
|
|
489
|
-
for (const { action, episode } of event.episodes) {
|
|
490
|
-
console.log(`Episode ${action}: ${episode.name}`);
|
|
491
|
-
}
|
|
492
|
-
});
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
#### `series$: Observable<SSESeriesEvent>`
|
|
496
|
-
|
|
497
|
-
Emits when series are added, updated, or deleted.
|
|
498
|
-
|
|
499
|
-
```typescript
|
|
500
|
-
client.series$.subscribe(event => {
|
|
501
|
-
for (const { action, serie } of event.series) {
|
|
502
|
-
console.log(`Series ${action}: ${serie.name}`);
|
|
503
|
-
}
|
|
504
|
-
});
|
|
505
|
-
```
|
|
506
|
-
|
|
507
|
-
#### `movies$: Observable<SSEMoviesEvent>`
|
|
508
|
-
|
|
509
|
-
Emits when movies are added, updated, or deleted.
|
|
510
|
-
|
|
511
|
-
```typescript
|
|
512
|
-
client.movies$.subscribe(event => {
|
|
513
|
-
for (const { action, movie } of event.movies) {
|
|
514
|
-
console.log(`Movie ${action}: ${movie.name}`);
|
|
515
|
-
}
|
|
516
|
-
});
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
#### `people$: Observable<SSEPeopleEvent>`
|
|
520
|
-
|
|
521
|
-
Emits when people are added, updated, or deleted.
|
|
522
|
-
|
|
523
|
-
```typescript
|
|
524
|
-
client.people$.subscribe(event => {
|
|
525
|
-
for (const { action, person } of event.people) {
|
|
526
|
-
console.log(`Person ${action}: ${person.name}`);
|
|
527
|
-
}
|
|
528
|
-
});
|
|
529
|
-
```
|
|
530
|
-
|
|
531
|
-
#### `tags$: Observable<SSETagsEvent>`
|
|
532
|
-
|
|
533
|
-
Emits when tags are added, updated, or deleted.
|
|
534
|
-
|
|
535
|
-
```typescript
|
|
536
|
-
client.tags$.subscribe(event => {
|
|
537
|
-
for (const { action, tag } of event.tags) {
|
|
538
|
-
console.log(`Tag ${action}: ${tag.name}`);
|
|
539
|
-
}
|
|
540
|
-
});
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
#### `backups$: Observable<SSEBackupsEvent>`
|
|
544
|
-
|
|
545
|
-
Emits backup status updates.
|
|
546
|
-
|
|
547
|
-
```typescript
|
|
548
|
-
client.backups$.subscribe(event => {
|
|
549
|
-
console.log(`Backup ${event.backup.name}: ${event.backup.status}`);
|
|
550
|
-
});
|
|
551
|
-
```
|
|
552
|
-
|
|
553
|
-
#### `backupFiles$: Observable<SSEBackupFilesEvent>`
|
|
554
|
-
|
|
555
|
-
Emits backup file progress updates.
|
|
556
|
-
|
|
557
|
-
```typescript
|
|
558
|
-
client.backupFiles$.subscribe(event => {
|
|
559
|
-
console.log(`Backing up ${event.file}: ${event.progress}%`);
|
|
560
|
-
});
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
#### `mediaRating$: Observable<SSEMediaRatingEvent>`
|
|
564
|
-
|
|
565
|
-
Emits when a user rates a media item.
|
|
566
|
-
|
|
567
|
-
```typescript
|
|
568
|
-
client.mediaRating$.subscribe(event => {
|
|
569
|
-
console.log(`User ${event.rating.userRef} rated media ${event.rating.mediaRef}: ${event.rating.rating}`);
|
|
570
|
-
});
|
|
571
|
-
```
|
|
572
|
-
|
|
573
|
-
**Event structure:**
|
|
574
|
-
- `library`: Library ID where the media is located
|
|
575
|
-
- `rating.userRef`: User who rated
|
|
576
|
-
- `rating.mediaRef`: Media that was rated
|
|
577
|
-
- `rating.rating`: Rating value (0-5)
|
|
578
|
-
- `rating.modified`: Timestamp of the rating
|
|
579
|
-
|
|
580
|
-
#### `mediaProgress$: Observable<SSEMediaProgressEvent>`
|
|
581
|
-
|
|
582
|
-
Emits when a user's playback progress is updated.
|
|
583
|
-
|
|
584
|
-
```typescript
|
|
585
|
-
client.mediaProgress$.subscribe(event => {
|
|
586
|
-
console.log(`User ${event.progress.userRef} watched ${event.progress.mediaRef} to ${event.progress.progress}ms`);
|
|
587
|
-
});
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
**Event structure:**
|
|
591
|
-
- `library`: Library ID where the media is located
|
|
592
|
-
- `progress.userRef`: User whose progress updated
|
|
593
|
-
- `progress.mediaRef`: Media being tracked
|
|
594
|
-
- `progress.progress`: Current playback position in milliseconds
|
|
595
|
-
- `progress.modified`: Timestamp of the update
|
|
596
|
-
|
|
597
|
-
#### `playersList$: Observable<SSEPlayersListEvent>`
|
|
598
|
-
|
|
599
|
-
Emits the full list of available media players for the user when it changes.
|
|
600
|
-
|
|
601
|
-
```typescript
|
|
602
|
-
client.playersList$.subscribe(event => {
|
|
603
|
-
console.log(`Available players for ${event.userRef}:`);
|
|
604
|
-
for (const player of event.players) {
|
|
605
|
-
console.log(` - ${player.name} (${player.player})`);
|
|
606
|
-
}
|
|
607
|
-
});
|
|
608
|
-
```
|
|
609
|
-
|
|
610
|
-
**Event structure:**
|
|
611
|
-
- `userRef`: User ID
|
|
612
|
-
- `players`: Array of available players
|
|
613
|
-
- `id`: Player socket ID (for casting)
|
|
614
|
-
- `name`: Player display name
|
|
615
|
-
- `player`: Player type identifier
|
|
616
|
-
|
|
617
|
-
### Complete SSE Example
|
|
618
|
-
|
|
619
|
-
```typescript
|
|
620
|
-
import { RedseatClient } from '@redseat/api';
|
|
621
|
-
import { Subscription } from 'rxjs';
|
|
622
|
-
|
|
623
|
-
const client = new RedseatClient({
|
|
624
|
-
server: { id: 'server-123', url: 'example.com' },
|
|
625
|
-
getIdToken: async () => await getFirebaseToken()
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
// Track subscriptions for cleanup
|
|
629
|
-
const subscriptions: Subscription[] = [];
|
|
630
|
-
|
|
631
|
-
// Monitor connection state
|
|
632
|
-
subscriptions.push(
|
|
633
|
-
client.sseConnectionState$.subscribe(state => {
|
|
634
|
-
console.log('SSE state:', state);
|
|
635
|
-
})
|
|
636
|
-
);
|
|
637
|
-
|
|
638
|
-
// Handle errors
|
|
639
|
-
subscriptions.push(
|
|
640
|
-
client.sseError$.subscribe(error => {
|
|
641
|
-
console.error('SSE error:', error.message);
|
|
642
|
-
})
|
|
643
|
-
);
|
|
644
|
-
|
|
645
|
-
// Subscribe to events
|
|
646
|
-
subscriptions.push(
|
|
647
|
-
client.medias$.subscribe(event => {
|
|
648
|
-
for (const { action, media } of event.medias) {
|
|
649
|
-
console.log(`Media ${action}: ${media.name}`);
|
|
650
|
-
}
|
|
651
|
-
})
|
|
652
|
-
);
|
|
653
|
-
|
|
654
|
-
// Connect to SSE
|
|
655
|
-
await client.connectSSE();
|
|
656
|
-
|
|
657
|
-
// ... later, cleanup
|
|
658
|
-
subscriptions.forEach(sub => sub.unsubscribe());
|
|
659
|
-
client.disconnectSSE();
|
|
660
|
-
|
|
661
|
-
// Or dispose completely when client is no longer needed
|
|
662
|
-
client.dispose();
|
|
663
|
-
```
|
|
664
|
-
|
|
665
|
-
## See Also
|
|
666
|
-
|
|
667
|
-
- [ServerApi Documentation](server.md) - Uses RedseatClient for server operations
|
|
668
|
-
- [LibraryApi Documentation](libraries.md) - Uses RedseatClient for library operations
|
|
669
|
-
- [README](README.md) - Package overview
|
|
670
|
-
|
|
1
|
+
# RedseatClient
|
|
2
|
+
|
|
3
|
+
The `RedseatClient` class is the low-level HTTP client that handles all communication with Redseat servers. It provides automatic token management, local server detection, and request/response interceptors.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`RedseatClient` wraps Axios and adds:
|
|
8
|
+
- Automatic token refresh before expiration
|
|
9
|
+
- Local server detection (for development)
|
|
10
|
+
- 401 error handling with automatic retry
|
|
11
|
+
- Request/response interceptors for authentication
|
|
12
|
+
|
|
13
|
+
## Constructor
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
new RedseatClient(options: ClientOptions)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### ClientOptions Interface
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
interface ClientOptions {
|
|
23
|
+
server: IServer;
|
|
24
|
+
getIdToken: () => Promise<string>;
|
|
25
|
+
refreshThreshold?: number; // milliseconds before expiration to refresh (default: 5 minutes)
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Parameters:**
|
|
30
|
+
- `server`: Server configuration object with `id`, `url`, and optional `port`
|
|
31
|
+
- `getIdToken`: Async function that returns the current ID token from your auth provider
|
|
32
|
+
- `refreshThreshold`: Optional. Milliseconds before token expiration to trigger refresh (default: 300000 = 5 minutes)
|
|
33
|
+
|
|
34
|
+
**Example:**
|
|
35
|
+
```typescript
|
|
36
|
+
const client = new RedseatClient({
|
|
37
|
+
server: {
|
|
38
|
+
id: 'server-123',
|
|
39
|
+
url: 'example.com',
|
|
40
|
+
port: 443
|
|
41
|
+
},
|
|
42
|
+
getIdToken: async () => {
|
|
43
|
+
// Get ID token from your auth provider (Firebase, Auth0, etc.)
|
|
44
|
+
return await getCurrentUserToken();
|
|
45
|
+
},
|
|
46
|
+
refreshThreshold: 5 * 60 * 1000 // 5 minutes
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Features
|
|
51
|
+
|
|
52
|
+
### Automatic Token Refresh
|
|
53
|
+
|
|
54
|
+
The client automatically refreshes tokens before they expire. The refresh happens:
|
|
55
|
+
- Before each request if the token is expired or expiring soon
|
|
56
|
+
- When a 401 error is received (with automatic retry)
|
|
57
|
+
|
|
58
|
+
### Local Server Detection
|
|
59
|
+
|
|
60
|
+
The client automatically detects if a local development server is available by checking `local.{server.url}`. If detected, it uses the local URL instead of the production URL.
|
|
61
|
+
|
|
62
|
+
### Request Interceptor
|
|
63
|
+
|
|
64
|
+
All requests are intercepted to:
|
|
65
|
+
1. Check if token needs refresh
|
|
66
|
+
2. Add `Authorization: Bearer {token}` header
|
|
67
|
+
|
|
68
|
+
### Response Interceptor
|
|
69
|
+
|
|
70
|
+
All responses are intercepted to:
|
|
71
|
+
1. Handle 401 errors by refreshing token and retrying the request
|
|
72
|
+
2. Prevent infinite retry loops with `_retry` flag
|
|
73
|
+
|
|
74
|
+
## Methods
|
|
75
|
+
|
|
76
|
+
### `get<T>(url: string, config?: AxiosRequestConfig)`
|
|
77
|
+
|
|
78
|
+
Performs a GET request.
|
|
79
|
+
|
|
80
|
+
**Parameters:**
|
|
81
|
+
- `url`: The endpoint URL (relative to base URL)
|
|
82
|
+
- `config`: Optional Axios request configuration
|
|
83
|
+
|
|
84
|
+
**Returns:** `Promise<AxiosResponse<T>>`
|
|
85
|
+
|
|
86
|
+
**Example:**
|
|
87
|
+
```typescript
|
|
88
|
+
const response = await client.get<IFile[]>('/libraries/123/medias');
|
|
89
|
+
const medias = response.data;
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### `post<T>(url: string, data?: unknown, config?: AxiosRequestConfig)`
|
|
93
|
+
|
|
94
|
+
Performs a POST request.
|
|
95
|
+
|
|
96
|
+
**Parameters:**
|
|
97
|
+
- `url`: The endpoint URL
|
|
98
|
+
- `data`: Request body data
|
|
99
|
+
- `config`: Optional Axios request configuration
|
|
100
|
+
|
|
101
|
+
**Returns:** `Promise<AxiosResponse<T>>`
|
|
102
|
+
|
|
103
|
+
**Example:**
|
|
104
|
+
```typescript
|
|
105
|
+
const response = await client.post<ITag>('/libraries/123/tags', {
|
|
106
|
+
name: 'Vacation'
|
|
107
|
+
});
|
|
108
|
+
const tag = response.data;
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### `postForm<T>(url: string, data?: unknown, config?: AxiosRequestConfig)`
|
|
112
|
+
|
|
113
|
+
Performs a POST request with FormData. This method is specifically designed for multipart/form-data uploads and provides better compatibility with React Native environments compared to using `post` with FormData.
|
|
114
|
+
|
|
115
|
+
**Parameters:**
|
|
116
|
+
- `url`: The endpoint URL
|
|
117
|
+
- `data`: FormData object or data to send as form data
|
|
118
|
+
- `config`: Optional Axios request configuration (supports `onUploadProgress` for progress tracking)
|
|
119
|
+
|
|
120
|
+
**Returns:** `Promise<AxiosResponse<T>>`
|
|
121
|
+
|
|
122
|
+
**Example:**
|
|
123
|
+
```typescript
|
|
124
|
+
const formData = new FormData();
|
|
125
|
+
formData.append('info', JSON.stringify({ name: 'photo.jpg', size: 1024 }));
|
|
126
|
+
formData.append('file', fileBlob, 'photo.jpg');
|
|
127
|
+
|
|
128
|
+
const response = await client.postForm<IFile>('/libraries/123/medias', formData, {
|
|
129
|
+
onUploadProgress: (progressEvent) => {
|
|
130
|
+
if (progressEvent.total) {
|
|
131
|
+
const percent = (progressEvent.loaded / progressEvent.total) * 100;
|
|
132
|
+
console.log(`Upload progress: ${percent}%`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
const uploadedFile = response.data;
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Note:** Use `postForm` instead of `post` when sending FormData, especially in React Native applications, as it properly handles multipart/form-data headers and encoding.
|
|
140
|
+
|
|
141
|
+
### `put<T>(url: string, data?: unknown, config?: AxiosRequestConfig)`
|
|
142
|
+
|
|
143
|
+
Performs a PUT request.
|
|
144
|
+
|
|
145
|
+
**Parameters:**
|
|
146
|
+
- `url`: The endpoint URL
|
|
147
|
+
- `data`: Request body data
|
|
148
|
+
- `config`: Optional Axios request configuration
|
|
149
|
+
|
|
150
|
+
**Returns:** `Promise<AxiosResponse<T>>`
|
|
151
|
+
|
|
152
|
+
**Example:**
|
|
153
|
+
```typescript
|
|
154
|
+
const formData = new FormData();
|
|
155
|
+
formData.append('file', fileBlob);
|
|
156
|
+
const response = await client.put('/libraries/123/medias/transfert', formData);
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### `patch<T>(url: string, data?: unknown, config?: AxiosRequestConfig)`
|
|
160
|
+
|
|
161
|
+
Performs a PATCH request.
|
|
162
|
+
|
|
163
|
+
**Parameters:**
|
|
164
|
+
- `url`: The endpoint URL
|
|
165
|
+
- `data`: Request body data
|
|
166
|
+
- `config`: Optional Axios request configuration
|
|
167
|
+
|
|
168
|
+
**Returns:** `Promise<AxiosResponse<T>>`
|
|
169
|
+
|
|
170
|
+
**Example:**
|
|
171
|
+
```typescript
|
|
172
|
+
const response = await client.patch<ITag>('/libraries/123/tags/tag-id', {
|
|
173
|
+
rename: 'New Tag Name'
|
|
174
|
+
});
|
|
175
|
+
const updatedTag = response.data;
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### `delete<T>(url: string, config?: AxiosRequestConfig)`
|
|
179
|
+
|
|
180
|
+
Performs a DELETE request.
|
|
181
|
+
|
|
182
|
+
**Parameters:**
|
|
183
|
+
- `url`: The endpoint URL
|
|
184
|
+
- `config`: Optional Axios request configuration (can include `data` for request body)
|
|
185
|
+
|
|
186
|
+
**Returns:** `Promise<AxiosResponse<T>>`
|
|
187
|
+
|
|
188
|
+
**Example:**
|
|
189
|
+
```typescript
|
|
190
|
+
// Simple delete
|
|
191
|
+
await client.delete('/libraries/123/tags/tag-id');
|
|
192
|
+
|
|
193
|
+
// Delete with body
|
|
194
|
+
await client.delete('/libraries/123/medias', {
|
|
195
|
+
data: { ids: ['media-1', 'media-2'] }
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### `request<T>(method: Method, url: string, data?: unknown, config?: AxiosRequestConfig)`
|
|
200
|
+
|
|
201
|
+
Performs a custom HTTP request.
|
|
202
|
+
|
|
203
|
+
**Parameters:**
|
|
204
|
+
- `method`: HTTP method ('GET', 'POST', 'PUT', 'PATCH', 'DELETE', etc.)
|
|
205
|
+
- `url`: The endpoint URL
|
|
206
|
+
- `data`: Request body data
|
|
207
|
+
- `config`: Optional Axios request configuration
|
|
208
|
+
|
|
209
|
+
**Returns:** `Promise<AxiosResponse<T>>`
|
|
210
|
+
|
|
211
|
+
**Example:**
|
|
212
|
+
```typescript
|
|
213
|
+
const response = await client.request<IFile>(
|
|
214
|
+
'GET',
|
|
215
|
+
'/libraries/123/medias/media-id',
|
|
216
|
+
undefined,
|
|
217
|
+
{ responseType: 'blob' }
|
|
218
|
+
);
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### `setToken(token: string | IToken)`
|
|
222
|
+
|
|
223
|
+
Manually set the authentication token. Useful when you already have a valid token.
|
|
224
|
+
|
|
225
|
+
**Parameters:**
|
|
226
|
+
- `token`: Either a token string or an `IToken` object with `token` and `expires` properties
|
|
227
|
+
|
|
228
|
+
**Example:**
|
|
229
|
+
```typescript
|
|
230
|
+
// Set as string (will be treated as expiring soon)
|
|
231
|
+
client.setToken('your-token-string');
|
|
232
|
+
|
|
233
|
+
// Set as IToken object with expiration
|
|
234
|
+
client.setToken({
|
|
235
|
+
token: 'your-token-string',
|
|
236
|
+
expires: Date.now() + 3600000 // 1 hour from now
|
|
237
|
+
});
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Error Handling
|
|
241
|
+
|
|
242
|
+
The client automatically handles:
|
|
243
|
+
- **401 Unauthorized**: Refreshes token and retries the request once
|
|
244
|
+
- **Token expiration**: Refreshes token before making requests
|
|
245
|
+
- **Network errors**: Passes through to caller
|
|
246
|
+
|
|
247
|
+
**Example error handling:**
|
|
248
|
+
```typescript
|
|
249
|
+
try {
|
|
250
|
+
const response = await client.get('/libraries/123/medias');
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (error.response?.status === 401) {
|
|
253
|
+
// Token refresh failed or invalid credentials
|
|
254
|
+
console.error('Authentication failed');
|
|
255
|
+
} else if (error.response?.status === 404) {
|
|
256
|
+
// Resource not found
|
|
257
|
+
console.error('Resource not found');
|
|
258
|
+
} else {
|
|
259
|
+
// Other error
|
|
260
|
+
console.error('Request failed:', error.message);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Usage with Response Types
|
|
266
|
+
|
|
267
|
+
The client supports specifying response types for binary data:
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
// Get as stream
|
|
271
|
+
const stream = await client.get('/libraries/123/medias/media-id', {
|
|
272
|
+
responseType: 'stream'
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Get as ArrayBuffer
|
|
276
|
+
const buffer = await client.get('/libraries/123/medias/media-id', {
|
|
277
|
+
responseType: 'arraybuffer'
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Get as Blob
|
|
281
|
+
const blob = await client.get('/libraries/123/medias/media-id', {
|
|
282
|
+
responseType: 'blob'
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Progress Tracking
|
|
287
|
+
|
|
288
|
+
For file uploads, you can track progress. Use `postForm` for FormData uploads (recommended for React Native compatibility):
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
const formData = new FormData();
|
|
292
|
+
formData.append('file', fileBlob);
|
|
293
|
+
|
|
294
|
+
await client.postForm('/libraries/123/medias', formData, {
|
|
295
|
+
onUploadProgress: (progressEvent) => {
|
|
296
|
+
if (progressEvent.total) {
|
|
297
|
+
const percent = (progressEvent.loaded / progressEvent.total) * 100;
|
|
298
|
+
console.log(`Upload progress: ${percent}%`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Internal Methods (Private)
|
|
305
|
+
|
|
306
|
+
The following methods are used internally and should not be called directly:
|
|
307
|
+
- `refreshToken()` - Refreshes the authentication token
|
|
308
|
+
- `ensureValidToken()` - Ensures token is valid before requests
|
|
309
|
+
- `isTokenExpiredOrExpiringSoon()` - Checks if token needs refresh
|
|
310
|
+
- `detectLocalUrl()` - Detects local development server
|
|
311
|
+
- `getRegularServerUrl()` - Gets production server URL
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Server-Sent Events (SSE)
|
|
316
|
+
|
|
317
|
+
The `RedseatClient` provides real-time event streaming using Server-Sent Events (SSE). Events are delivered as RxJS Observables for easy integration with reactive programming patterns.
|
|
318
|
+
|
|
319
|
+
### Connection Management
|
|
320
|
+
|
|
321
|
+
#### `connectSSE(options?: SSEConnectionOptions): Promise<void>`
|
|
322
|
+
|
|
323
|
+
Connects to the server's SSE endpoint for real-time updates.
|
|
324
|
+
|
|
325
|
+
**Parameters:**
|
|
326
|
+
- `options`: Optional connection configuration:
|
|
327
|
+
- `libraries?`: Array of library IDs to filter events to specific libraries
|
|
328
|
+
- `autoReconnect?`: Enable auto-reconnect on disconnect (default: `true`)
|
|
329
|
+
- `maxReconnectAttempts?`: Maximum reconnect attempts (default: unlimited)
|
|
330
|
+
- `initialReconnectDelay?`: Initial reconnect delay in ms (default: `1000`)
|
|
331
|
+
- `maxReconnectDelay?`: Maximum reconnect delay in ms (default: `30000`)
|
|
332
|
+
|
|
333
|
+
**Example:**
|
|
334
|
+
```typescript
|
|
335
|
+
// Connect with default options
|
|
336
|
+
await client.connectSSE();
|
|
337
|
+
|
|
338
|
+
// Connect with library filter
|
|
339
|
+
await client.connectSSE({
|
|
340
|
+
libraries: ['library-123', 'library-456']
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// Connect with custom reconnection settings
|
|
344
|
+
await client.connectSSE({
|
|
345
|
+
autoReconnect: true,
|
|
346
|
+
maxReconnectAttempts: 10,
|
|
347
|
+
initialReconnectDelay: 2000,
|
|
348
|
+
maxReconnectDelay: 60000
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
#### `disconnectSSE(): void`
|
|
353
|
+
|
|
354
|
+
Disconnects from the SSE endpoint and cleans up resources.
|
|
355
|
+
|
|
356
|
+
**Example:**
|
|
357
|
+
```typescript
|
|
358
|
+
client.disconnectSSE();
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
#### `dispose(): void`
|
|
362
|
+
|
|
363
|
+
Disposes of all SSE resources and completes all observables. Call this when the client is no longer needed.
|
|
364
|
+
|
|
365
|
+
**Example:**
|
|
366
|
+
```typescript
|
|
367
|
+
client.dispose();
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
#### `sseConnectionState: SSEConnectionState`
|
|
371
|
+
|
|
372
|
+
Gets the current SSE connection state. Possible values:
|
|
373
|
+
- `'disconnected'` - Not connected
|
|
374
|
+
- `'connecting'` - Connection in progress
|
|
375
|
+
- `'connected'` - Successfully connected
|
|
376
|
+
- `'reconnecting'` - Attempting to reconnect after disconnect
|
|
377
|
+
- `'error'` - Connection error occurred
|
|
378
|
+
|
|
379
|
+
**Example:**
|
|
380
|
+
```typescript
|
|
381
|
+
if (client.sseConnectionState === 'connected') {
|
|
382
|
+
console.log('SSE is connected');
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Connection State Observables
|
|
387
|
+
|
|
388
|
+
#### `sseConnectionState$: Observable<SSEConnectionState>`
|
|
389
|
+
|
|
390
|
+
Observable that emits connection state changes.
|
|
391
|
+
|
|
392
|
+
**Example:**
|
|
393
|
+
```typescript
|
|
394
|
+
client.sseConnectionState$.subscribe(state => {
|
|
395
|
+
console.log('SSE connection state:', state);
|
|
396
|
+
});
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
#### `sseConnected$: Observable<boolean>`
|
|
400
|
+
|
|
401
|
+
Observable that emits `true` when connected, `false` otherwise.
|
|
402
|
+
|
|
403
|
+
**Example:**
|
|
404
|
+
```typescript
|
|
405
|
+
client.sseConnected$.subscribe(connected => {
|
|
406
|
+
if (connected) {
|
|
407
|
+
console.log('SSE connected');
|
|
408
|
+
} else {
|
|
409
|
+
console.log('SSE disconnected');
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
#### `sseError$: Observable<SSEConnectionError>`
|
|
415
|
+
|
|
416
|
+
Observable that emits connection errors.
|
|
417
|
+
|
|
418
|
+
**Example:**
|
|
419
|
+
```typescript
|
|
420
|
+
client.sseError$.subscribe(error => {
|
|
421
|
+
console.error(`SSE error (${error.type}): ${error.message}`);
|
|
422
|
+
});
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### Event Streams
|
|
426
|
+
|
|
427
|
+
All event streams are typed RxJS Observables. Subscribe to receive real-time updates from the server.
|
|
428
|
+
|
|
429
|
+
#### `library$: Observable<SSELibraryEvent>`
|
|
430
|
+
|
|
431
|
+
Emits when libraries are added, updated, or deleted.
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
client.library$.subscribe(event => {
|
|
435
|
+
console.log(`Library ${event.action}: ${event.library.name}`);
|
|
436
|
+
});
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
#### `libraryStatus$: Observable<SSELibraryStatusEvent>`
|
|
440
|
+
|
|
441
|
+
Emits library status updates (e.g., scanning progress).
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
client.libraryStatus$.subscribe(event => {
|
|
445
|
+
console.log(`Library ${event.library}: ${event.message} (${event.progress}%)`);
|
|
446
|
+
});
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
#### `medias$: Observable<SSEMediasEvent>`
|
|
450
|
+
|
|
451
|
+
Emits when media files are added, updated, or deleted.
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
client.medias$.subscribe(event => {
|
|
455
|
+
for (const { action, media } of event.medias) {
|
|
456
|
+
console.log(`Media ${action}: ${media.name}`);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
#### `uploadProgress$: Observable<SSEUploadProgressEvent>`
|
|
462
|
+
|
|
463
|
+
Emits upload progress updates including download, transfer, and analysis stages.
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
client.uploadProgress$.subscribe(event => {
|
|
467
|
+
const { progress } = event;
|
|
468
|
+
const percent = progress.total ? Math.round((progress.current ?? 0) / progress.total * 100) : 0;
|
|
469
|
+
console.log(`Upload ${progress.id} (${progress.type}): ${percent}% - ${progress.filename}`);
|
|
470
|
+
});
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
#### `convertProgress$: Observable<SSEConvertProgressEvent>`
|
|
474
|
+
|
|
475
|
+
Emits video conversion progress updates.
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
client.convertProgress$.subscribe(event => {
|
|
479
|
+
console.log(`Converting ${event.mediaId}: ${event.progress}% - ${event.status}`);
|
|
480
|
+
});
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
#### `episodes$: Observable<SSEEpisodesEvent>`
|
|
484
|
+
|
|
485
|
+
Emits when episodes are added, updated, or deleted.
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
client.episodes$.subscribe(event => {
|
|
489
|
+
for (const { action, episode } of event.episodes) {
|
|
490
|
+
console.log(`Episode ${action}: ${episode.name}`);
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
#### `series$: Observable<SSESeriesEvent>`
|
|
496
|
+
|
|
497
|
+
Emits when series are added, updated, or deleted.
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
client.series$.subscribe(event => {
|
|
501
|
+
for (const { action, serie } of event.series) {
|
|
502
|
+
console.log(`Series ${action}: ${serie.name}`);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
#### `movies$: Observable<SSEMoviesEvent>`
|
|
508
|
+
|
|
509
|
+
Emits when movies are added, updated, or deleted.
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
client.movies$.subscribe(event => {
|
|
513
|
+
for (const { action, movie } of event.movies) {
|
|
514
|
+
console.log(`Movie ${action}: ${movie.name}`);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
#### `people$: Observable<SSEPeopleEvent>`
|
|
520
|
+
|
|
521
|
+
Emits when people are added, updated, or deleted.
|
|
522
|
+
|
|
523
|
+
```typescript
|
|
524
|
+
client.people$.subscribe(event => {
|
|
525
|
+
for (const { action, person } of event.people) {
|
|
526
|
+
console.log(`Person ${action}: ${person.name}`);
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
#### `tags$: Observable<SSETagsEvent>`
|
|
532
|
+
|
|
533
|
+
Emits when tags are added, updated, or deleted.
|
|
534
|
+
|
|
535
|
+
```typescript
|
|
536
|
+
client.tags$.subscribe(event => {
|
|
537
|
+
for (const { action, tag } of event.tags) {
|
|
538
|
+
console.log(`Tag ${action}: ${tag.name}`);
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
#### `backups$: Observable<SSEBackupsEvent>`
|
|
544
|
+
|
|
545
|
+
Emits backup status updates.
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
client.backups$.subscribe(event => {
|
|
549
|
+
console.log(`Backup ${event.backup.name}: ${event.backup.status}`);
|
|
550
|
+
});
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
#### `backupFiles$: Observable<SSEBackupFilesEvent>`
|
|
554
|
+
|
|
555
|
+
Emits backup file progress updates.
|
|
556
|
+
|
|
557
|
+
```typescript
|
|
558
|
+
client.backupFiles$.subscribe(event => {
|
|
559
|
+
console.log(`Backing up ${event.file}: ${event.progress}%`);
|
|
560
|
+
});
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
#### `mediaRating$: Observable<SSEMediaRatingEvent>`
|
|
564
|
+
|
|
565
|
+
Emits when a user rates a media item.
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
client.mediaRating$.subscribe(event => {
|
|
569
|
+
console.log(`User ${event.rating.userRef} rated media ${event.rating.mediaRef}: ${event.rating.rating}`);
|
|
570
|
+
});
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
**Event structure:**
|
|
574
|
+
- `library`: Library ID where the media is located
|
|
575
|
+
- `rating.userRef`: User who rated
|
|
576
|
+
- `rating.mediaRef`: Media that was rated
|
|
577
|
+
- `rating.rating`: Rating value (0-5)
|
|
578
|
+
- `rating.modified`: Timestamp of the rating
|
|
579
|
+
|
|
580
|
+
#### `mediaProgress$: Observable<SSEMediaProgressEvent>`
|
|
581
|
+
|
|
582
|
+
Emits when a user's playback progress is updated.
|
|
583
|
+
|
|
584
|
+
```typescript
|
|
585
|
+
client.mediaProgress$.subscribe(event => {
|
|
586
|
+
console.log(`User ${event.progress.userRef} watched ${event.progress.mediaRef} to ${event.progress.progress}ms`);
|
|
587
|
+
});
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
**Event structure:**
|
|
591
|
+
- `library`: Library ID where the media is located
|
|
592
|
+
- `progress.userRef`: User whose progress updated
|
|
593
|
+
- `progress.mediaRef`: Media being tracked
|
|
594
|
+
- `progress.progress`: Current playback position in milliseconds
|
|
595
|
+
- `progress.modified`: Timestamp of the update
|
|
596
|
+
|
|
597
|
+
#### `playersList$: Observable<SSEPlayersListEvent>`
|
|
598
|
+
|
|
599
|
+
Emits the full list of available media players for the user when it changes.
|
|
600
|
+
|
|
601
|
+
```typescript
|
|
602
|
+
client.playersList$.subscribe(event => {
|
|
603
|
+
console.log(`Available players for ${event.userRef}:`);
|
|
604
|
+
for (const player of event.players) {
|
|
605
|
+
console.log(` - ${player.name} (${player.player})`);
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
**Event structure:**
|
|
611
|
+
- `userRef`: User ID
|
|
612
|
+
- `players`: Array of available players
|
|
613
|
+
- `id`: Player socket ID (for casting)
|
|
614
|
+
- `name`: Player display name
|
|
615
|
+
- `player`: Player type identifier
|
|
616
|
+
|
|
617
|
+
### Complete SSE Example
|
|
618
|
+
|
|
619
|
+
```typescript
|
|
620
|
+
import { RedseatClient } from '@redseat/api';
|
|
621
|
+
import { Subscription } from 'rxjs';
|
|
622
|
+
|
|
623
|
+
const client = new RedseatClient({
|
|
624
|
+
server: { id: 'server-123', url: 'example.com' },
|
|
625
|
+
getIdToken: async () => await getFirebaseToken()
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
// Track subscriptions for cleanup
|
|
629
|
+
const subscriptions: Subscription[] = [];
|
|
630
|
+
|
|
631
|
+
// Monitor connection state
|
|
632
|
+
subscriptions.push(
|
|
633
|
+
client.sseConnectionState$.subscribe(state => {
|
|
634
|
+
console.log('SSE state:', state);
|
|
635
|
+
})
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
// Handle errors
|
|
639
|
+
subscriptions.push(
|
|
640
|
+
client.sseError$.subscribe(error => {
|
|
641
|
+
console.error('SSE error:', error.message);
|
|
642
|
+
})
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
// Subscribe to events
|
|
646
|
+
subscriptions.push(
|
|
647
|
+
client.medias$.subscribe(event => {
|
|
648
|
+
for (const { action, media } of event.medias) {
|
|
649
|
+
console.log(`Media ${action}: ${media.name}`);
|
|
650
|
+
}
|
|
651
|
+
})
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
// Connect to SSE
|
|
655
|
+
await client.connectSSE();
|
|
656
|
+
|
|
657
|
+
// ... later, cleanup
|
|
658
|
+
subscriptions.forEach(sub => sub.unsubscribe());
|
|
659
|
+
client.disconnectSSE();
|
|
660
|
+
|
|
661
|
+
// Or dispose completely when client is no longer needed
|
|
662
|
+
client.dispose();
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
## See Also
|
|
666
|
+
|
|
667
|
+
- [ServerApi Documentation](server.md) - Uses RedseatClient for server operations
|
|
668
|
+
- [LibraryApi Documentation](libraries.md) - Uses RedseatClient for library operations
|
|
669
|
+
- [README](README.md) - Package overview
|
|
670
|
+
|