@redseat/api 0.3.5 → 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/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
+