@redseat/api 0.2.7 → 0.2.8
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 +296 -0
- package/dist/client.d.ts +71 -0
- package/dist/client.js +285 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/library.d.ts +31 -0
- package/dist/library.js +29 -0
- package/dist/sse-types.d.ts +121 -0
- package/dist/sse-types.js +1 -0
- package/libraries.md +166 -0
- package/package.json +1 -1
package/client.md
CHANGED
|
@@ -310,6 +310,302 @@ The following methods are used internally and should not be called directly:
|
|
|
310
310
|
- `detectLocalUrl()` - Detects local development server
|
|
311
311
|
- `getRegularServerUrl()` - Gets production server URL
|
|
312
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
|
+
#### `mediasProgress$: Observable<SSEMediasProgressEvent>`
|
|
462
|
+
|
|
463
|
+
Emits media processing progress updates.
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
client.mediasProgress$.subscribe(event => {
|
|
467
|
+
console.log(`Media ${event.mediaId}: ${event.progress}%`);
|
|
468
|
+
});
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
#### `convertProgress$: Observable<SSEConvertProgressEvent>`
|
|
472
|
+
|
|
473
|
+
Emits video conversion progress updates.
|
|
474
|
+
|
|
475
|
+
```typescript
|
|
476
|
+
client.convertProgress$.subscribe(event => {
|
|
477
|
+
console.log(`Converting ${event.mediaId}: ${event.progress}% - ${event.status}`);
|
|
478
|
+
});
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
#### `episodes$: Observable<SSEEpisodesEvent>`
|
|
482
|
+
|
|
483
|
+
Emits when episodes are added, updated, or deleted.
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
client.episodes$.subscribe(event => {
|
|
487
|
+
for (const { action, episode } of event.episodes) {
|
|
488
|
+
console.log(`Episode ${action}: ${episode.name}`);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
#### `series$: Observable<SSESeriesEvent>`
|
|
494
|
+
|
|
495
|
+
Emits when series are added, updated, or deleted.
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
client.series$.subscribe(event => {
|
|
499
|
+
for (const { action, serie } of event.series) {
|
|
500
|
+
console.log(`Series ${action}: ${serie.name}`);
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
#### `movies$: Observable<SSEMoviesEvent>`
|
|
506
|
+
|
|
507
|
+
Emits when movies are added, updated, or deleted.
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
client.movies$.subscribe(event => {
|
|
511
|
+
for (const { action, movie } of event.movies) {
|
|
512
|
+
console.log(`Movie ${action}: ${movie.name}`);
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
#### `people$: Observable<SSEPeopleEvent>`
|
|
518
|
+
|
|
519
|
+
Emits when people are added, updated, or deleted.
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
client.people$.subscribe(event => {
|
|
523
|
+
for (const { action, person } of event.people) {
|
|
524
|
+
console.log(`Person ${action}: ${person.name}`);
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
#### `tags$: Observable<SSETagsEvent>`
|
|
530
|
+
|
|
531
|
+
Emits when tags are added, updated, or deleted.
|
|
532
|
+
|
|
533
|
+
```typescript
|
|
534
|
+
client.tags$.subscribe(event => {
|
|
535
|
+
for (const { action, tag } of event.tags) {
|
|
536
|
+
console.log(`Tag ${action}: ${tag.name}`);
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
#### `backups$: Observable<SSEBackupsEvent>`
|
|
542
|
+
|
|
543
|
+
Emits backup status updates.
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
client.backups$.subscribe(event => {
|
|
547
|
+
console.log(`Backup ${event.backup.name}: ${event.backup.status}`);
|
|
548
|
+
});
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
#### `backupFiles$: Observable<SSEBackupFilesEvent>`
|
|
552
|
+
|
|
553
|
+
Emits backup file progress updates.
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
client.backupFiles$.subscribe(event => {
|
|
557
|
+
console.log(`Backing up ${event.file}: ${event.progress}%`);
|
|
558
|
+
});
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
### Complete SSE Example
|
|
562
|
+
|
|
563
|
+
```typescript
|
|
564
|
+
import { RedseatClient } from '@redseat/api';
|
|
565
|
+
import { Subscription } from 'rxjs';
|
|
566
|
+
|
|
567
|
+
const client = new RedseatClient({
|
|
568
|
+
server: { id: 'server-123', url: 'example.com' },
|
|
569
|
+
getIdToken: async () => await getFirebaseToken()
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// Track subscriptions for cleanup
|
|
573
|
+
const subscriptions: Subscription[] = [];
|
|
574
|
+
|
|
575
|
+
// Monitor connection state
|
|
576
|
+
subscriptions.push(
|
|
577
|
+
client.sseConnectionState$.subscribe(state => {
|
|
578
|
+
console.log('SSE state:', state);
|
|
579
|
+
})
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
// Handle errors
|
|
583
|
+
subscriptions.push(
|
|
584
|
+
client.sseError$.subscribe(error => {
|
|
585
|
+
console.error('SSE error:', error.message);
|
|
586
|
+
})
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
// Subscribe to events
|
|
590
|
+
subscriptions.push(
|
|
591
|
+
client.medias$.subscribe(event => {
|
|
592
|
+
for (const { action, media } of event.medias) {
|
|
593
|
+
console.log(`Media ${action}: ${media.name}`);
|
|
594
|
+
}
|
|
595
|
+
})
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
// Connect to SSE
|
|
599
|
+
await client.connectSSE();
|
|
600
|
+
|
|
601
|
+
// ... later, cleanup
|
|
602
|
+
subscriptions.forEach(sub => sub.unsubscribe());
|
|
603
|
+
client.disconnectSSE();
|
|
604
|
+
|
|
605
|
+
// Or dispose completely when client is no longer needed
|
|
606
|
+
client.dispose();
|
|
607
|
+
```
|
|
608
|
+
|
|
313
609
|
## See Also
|
|
314
610
|
|
|
315
611
|
- [ServerApi Documentation](server.md) - Uses RedseatClient for server operations
|
package/dist/client.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Method, AxiosRequestConfig } from 'axios';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
2
3
|
import { IToken } from './auth.js';
|
|
3
4
|
import { IServer } from './interfaces.js';
|
|
5
|
+
import { SSEConnectionState, SSEConnectionOptions, SSEConnectionError, SSELibraryEvent, SSELibraryStatusEvent, SSEMediasEvent, SSEMediasProgressEvent, SSEConvertProgressEvent, SSEEpisodesEvent, SSESeriesEvent, SSEMoviesEvent, SSEPeopleEvent, SSETagsEvent, SSEBackupsEvent, SSEBackupFilesEvent } from './sse-types.js';
|
|
4
6
|
export interface ClientOptions {
|
|
5
7
|
server: IServer;
|
|
6
8
|
getIdToken: () => Promise<string>;
|
|
@@ -18,6 +20,32 @@ export declare class RedseatClient {
|
|
|
18
20
|
private localServerUrl?;
|
|
19
21
|
private tokenData?;
|
|
20
22
|
private tokenRefreshPromise?;
|
|
23
|
+
private sseAbortController?;
|
|
24
|
+
private sseReconnectTimeout?;
|
|
25
|
+
private sseReconnectAttempts;
|
|
26
|
+
private sseOptions?;
|
|
27
|
+
private readonly _sseConnectionState;
|
|
28
|
+
private readonly _sseError;
|
|
29
|
+
private readonly _sseEvents;
|
|
30
|
+
readonly sseConnectionState$: Observable<SSEConnectionState>;
|
|
31
|
+
readonly sseError$: Observable<SSEConnectionError>;
|
|
32
|
+
readonly sseConnected$: Observable<boolean>;
|
|
33
|
+
readonly library$: Observable<SSELibraryEvent>;
|
|
34
|
+
readonly libraryStatus$: Observable<SSELibraryStatusEvent>;
|
|
35
|
+
readonly medias$: Observable<SSEMediasEvent>;
|
|
36
|
+
readonly mediasProgress$: Observable<SSEMediasProgressEvent>;
|
|
37
|
+
readonly convertProgress$: Observable<SSEConvertProgressEvent>;
|
|
38
|
+
readonly episodes$: Observable<SSEEpisodesEvent>;
|
|
39
|
+
readonly series$: Observable<SSESeriesEvent>;
|
|
40
|
+
readonly movies$: Observable<SSEMoviesEvent>;
|
|
41
|
+
readonly people$: Observable<SSEPeopleEvent>;
|
|
42
|
+
readonly tags$: Observable<SSETagsEvent>;
|
|
43
|
+
readonly backups$: Observable<SSEBackupsEvent>;
|
|
44
|
+
readonly backupFiles$: Observable<SSEBackupFilesEvent>;
|
|
45
|
+
/**
|
|
46
|
+
* Creates a typed observable for a specific SSE event type
|
|
47
|
+
*/
|
|
48
|
+
private createEventStream;
|
|
21
49
|
constructor(options: ClientOptions);
|
|
22
50
|
private getRegularServerUrl;
|
|
23
51
|
private detectLocalUrl;
|
|
@@ -57,4 +85,47 @@ export declare class RedseatClient {
|
|
|
57
85
|
* Returns public server info (IServer).
|
|
58
86
|
*/
|
|
59
87
|
getServer(serverId: string): Promise<IServer>;
|
|
88
|
+
/**
|
|
89
|
+
* Connects to the server's SSE endpoint for real-time updates.
|
|
90
|
+
* Automatically manages authentication and reconnection.
|
|
91
|
+
* @param options - Connection options including library filters and reconnection settings
|
|
92
|
+
*/
|
|
93
|
+
connectSSE(options?: SSEConnectionOptions): Promise<void>;
|
|
94
|
+
/**
|
|
95
|
+
* Disconnects from the SSE endpoint and cleans up resources.
|
|
96
|
+
*/
|
|
97
|
+
disconnectSSE(): void;
|
|
98
|
+
/**
|
|
99
|
+
* Disposes of all SSE resources and completes observables.
|
|
100
|
+
* Call this when the client is no longer needed.
|
|
101
|
+
*/
|
|
102
|
+
dispose(): void;
|
|
103
|
+
/**
|
|
104
|
+
* Gets the current SSE connection state
|
|
105
|
+
*/
|
|
106
|
+
get sseConnectionState(): SSEConnectionState;
|
|
107
|
+
/**
|
|
108
|
+
* Internal method to establish SSE connection
|
|
109
|
+
*/
|
|
110
|
+
private _connectSSE;
|
|
111
|
+
/**
|
|
112
|
+
* Builds the SSE endpoint URL with optional library filters
|
|
113
|
+
*/
|
|
114
|
+
private buildSSEUrl;
|
|
115
|
+
/**
|
|
116
|
+
* Processes the SSE stream and emits events
|
|
117
|
+
*/
|
|
118
|
+
private processSSEStream;
|
|
119
|
+
/**
|
|
120
|
+
* Parses the SSE buffer and extracts complete events
|
|
121
|
+
*/
|
|
122
|
+
private parseSSEBuffer;
|
|
123
|
+
/**
|
|
124
|
+
* Handles SSE errors and triggers reconnection if appropriate
|
|
125
|
+
*/
|
|
126
|
+
private handleSSEError;
|
|
127
|
+
/**
|
|
128
|
+
* Schedules a reconnection attempt with exponential backoff
|
|
129
|
+
*/
|
|
130
|
+
private scheduleReconnect;
|
|
60
131
|
}
|
package/dist/client.js
CHANGED
|
@@ -1,7 +1,36 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
+
import { BehaviorSubject, Subject, filter, map } from 'rxjs';
|
|
2
3
|
import { fetchServerToken } from './auth.js';
|
|
3
4
|
export class RedseatClient {
|
|
5
|
+
/**
|
|
6
|
+
* Creates a typed observable for a specific SSE event type
|
|
7
|
+
*/
|
|
8
|
+
createEventStream(eventName) {
|
|
9
|
+
return this._sseEvents.pipe(filter((event) => event.event === eventName), map(event => event.data));
|
|
10
|
+
}
|
|
4
11
|
constructor(options) {
|
|
12
|
+
this.sseReconnectAttempts = 0;
|
|
13
|
+
// RxJS subjects for SSE
|
|
14
|
+
this._sseConnectionState = new BehaviorSubject('disconnected');
|
|
15
|
+
this._sseError = new Subject();
|
|
16
|
+
this._sseEvents = new Subject();
|
|
17
|
+
// Public observables for SSE connection state
|
|
18
|
+
this.sseConnectionState$ = this._sseConnectionState.asObservable();
|
|
19
|
+
this.sseError$ = this._sseError.asObservable();
|
|
20
|
+
this.sseConnected$ = this._sseConnectionState.pipe(map(state => state === 'connected'));
|
|
21
|
+
// Typed event streams
|
|
22
|
+
this.library$ = this.createEventStream('library');
|
|
23
|
+
this.libraryStatus$ = this.createEventStream('library-status');
|
|
24
|
+
this.medias$ = this.createEventStream('medias');
|
|
25
|
+
this.mediasProgress$ = this.createEventStream('medias_progress');
|
|
26
|
+
this.convertProgress$ = this.createEventStream('convert_progress');
|
|
27
|
+
this.episodes$ = this.createEventStream('episodes');
|
|
28
|
+
this.series$ = this.createEventStream('series');
|
|
29
|
+
this.movies$ = this.createEventStream('movies');
|
|
30
|
+
this.people$ = this.createEventStream('people');
|
|
31
|
+
this.tags$ = this.createEventStream('tags');
|
|
32
|
+
this.backups$ = this.createEventStream('backups');
|
|
33
|
+
this.backupFiles$ = this.createEventStream('backups-files');
|
|
5
34
|
this.server = options.server;
|
|
6
35
|
this.redseatUrl = options.redseatUrl;
|
|
7
36
|
this.getIdToken = options.getIdToken;
|
|
@@ -213,4 +242,260 @@ export class RedseatClient {
|
|
|
213
242
|
});
|
|
214
243
|
return response.data;
|
|
215
244
|
}
|
|
245
|
+
// ==================== SSE Methods ====================
|
|
246
|
+
/**
|
|
247
|
+
* Connects to the server's SSE endpoint for real-time updates.
|
|
248
|
+
* Automatically manages authentication and reconnection.
|
|
249
|
+
* @param options - Connection options including library filters and reconnection settings
|
|
250
|
+
*/
|
|
251
|
+
async connectSSE(options) {
|
|
252
|
+
// Disconnect any existing connection
|
|
253
|
+
this.disconnectSSE();
|
|
254
|
+
this.sseOptions = {
|
|
255
|
+
autoReconnect: true,
|
|
256
|
+
initialReconnectDelay: 1000,
|
|
257
|
+
maxReconnectDelay: 30000,
|
|
258
|
+
...options
|
|
259
|
+
};
|
|
260
|
+
this.sseReconnectAttempts = 0;
|
|
261
|
+
await this._connectSSE();
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Disconnects from the SSE endpoint and cleans up resources.
|
|
265
|
+
*/
|
|
266
|
+
disconnectSSE() {
|
|
267
|
+
if (this.sseReconnectTimeout) {
|
|
268
|
+
clearTimeout(this.sseReconnectTimeout);
|
|
269
|
+
this.sseReconnectTimeout = undefined;
|
|
270
|
+
}
|
|
271
|
+
if (this.sseAbortController) {
|
|
272
|
+
this.sseAbortController.abort();
|
|
273
|
+
this.sseAbortController = undefined;
|
|
274
|
+
}
|
|
275
|
+
this._sseConnectionState.next('disconnected');
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Disposes of all SSE resources and completes observables.
|
|
279
|
+
* Call this when the client is no longer needed.
|
|
280
|
+
*/
|
|
281
|
+
dispose() {
|
|
282
|
+
this.disconnectSSE();
|
|
283
|
+
this._sseConnectionState.complete();
|
|
284
|
+
this._sseError.complete();
|
|
285
|
+
this._sseEvents.complete();
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Gets the current SSE connection state
|
|
289
|
+
*/
|
|
290
|
+
get sseConnectionState() {
|
|
291
|
+
return this._sseConnectionState.value;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Internal method to establish SSE connection
|
|
295
|
+
*/
|
|
296
|
+
async _connectSSE() {
|
|
297
|
+
this._sseConnectionState.next('connecting');
|
|
298
|
+
try {
|
|
299
|
+
// Ensure we have a valid token
|
|
300
|
+
await this.ensureValidToken();
|
|
301
|
+
if (!this.tokenData) {
|
|
302
|
+
throw new Error('No authentication token available');
|
|
303
|
+
}
|
|
304
|
+
const url = this.buildSSEUrl();
|
|
305
|
+
this.sseAbortController = new AbortController();
|
|
306
|
+
const response = await fetch(url, {
|
|
307
|
+
method: 'GET',
|
|
308
|
+
headers: {
|
|
309
|
+
'Authorization': `Bearer ${this.tokenData.token}`,
|
|
310
|
+
'Accept': 'text/event-stream',
|
|
311
|
+
'Cache-Control': 'no-cache'
|
|
312
|
+
},
|
|
313
|
+
signal: this.sseAbortController.signal
|
|
314
|
+
});
|
|
315
|
+
if (!response.ok) {
|
|
316
|
+
if (response.status === 401) {
|
|
317
|
+
throw Object.assign(new Error('Authentication failed'), { type: 'auth' });
|
|
318
|
+
}
|
|
319
|
+
throw Object.assign(new Error(`Server returned ${response.status}`), { type: 'server' });
|
|
320
|
+
}
|
|
321
|
+
if (!response.body) {
|
|
322
|
+
throw Object.assign(new Error('No response body'), { type: 'server' });
|
|
323
|
+
}
|
|
324
|
+
this._sseConnectionState.next('connected');
|
|
325
|
+
this.sseReconnectAttempts = 0;
|
|
326
|
+
// Process the stream
|
|
327
|
+
await this.processSSEStream(response.body);
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
331
|
+
// Intentionally disconnected
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
this.handleSSEError(error);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Builds the SSE endpoint URL with optional library filters
|
|
339
|
+
*/
|
|
340
|
+
buildSSEUrl() {
|
|
341
|
+
let url = `${this.baseUrl}/sse`;
|
|
342
|
+
if (this.sseOptions?.libraries && this.sseOptions.libraries.length > 0) {
|
|
343
|
+
const params = new URLSearchParams();
|
|
344
|
+
this.sseOptions.libraries.forEach(lib => params.append('library', lib));
|
|
345
|
+
url = `${url}?${params.toString()}`;
|
|
346
|
+
}
|
|
347
|
+
return url;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Processes the SSE stream and emits events
|
|
351
|
+
*/
|
|
352
|
+
async processSSEStream(body) {
|
|
353
|
+
const reader = body.getReader();
|
|
354
|
+
const decoder = new TextDecoder();
|
|
355
|
+
let buffer = '';
|
|
356
|
+
try {
|
|
357
|
+
while (true) {
|
|
358
|
+
const { done, value } = await reader.read();
|
|
359
|
+
if (done) {
|
|
360
|
+
// Stream ended - server closed connection
|
|
361
|
+
this._sseConnectionState.next('disconnected');
|
|
362
|
+
this.scheduleReconnect();
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
buffer += decoder.decode(value, { stream: true });
|
|
366
|
+
const { events, remainingBuffer } = this.parseSSEBuffer(buffer);
|
|
367
|
+
buffer = remainingBuffer;
|
|
368
|
+
for (const event of events) {
|
|
369
|
+
this._sseEvents.next(event);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
finally {
|
|
380
|
+
reader.releaseLock();
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Parses the SSE buffer and extracts complete events
|
|
385
|
+
*/
|
|
386
|
+
parseSSEBuffer(buffer) {
|
|
387
|
+
const events = [];
|
|
388
|
+
const lines = buffer.split('\n');
|
|
389
|
+
let currentEvent = {};
|
|
390
|
+
let processedUpTo = 0;
|
|
391
|
+
for (let i = 0; i < lines.length; i++) {
|
|
392
|
+
const line = lines[i];
|
|
393
|
+
// Check if this is an incomplete line (last line without newline)
|
|
394
|
+
if (i === lines.length - 1 && !buffer.endsWith('\n')) {
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
processedUpTo += line.length + 1; // +1 for the newline
|
|
398
|
+
if (line === '') {
|
|
399
|
+
// Empty line marks end of event
|
|
400
|
+
if (currentEvent.event && currentEvent.data !== undefined) {
|
|
401
|
+
events.push(currentEvent);
|
|
402
|
+
}
|
|
403
|
+
currentEvent = {};
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (line.startsWith(':')) {
|
|
407
|
+
// Comment, ignore
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const colonIndex = line.indexOf(':');
|
|
411
|
+
if (colonIndex === -1) {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
const field = line.slice(0, colonIndex);
|
|
415
|
+
// Value starts after colon, skip optional space after colon
|
|
416
|
+
let value = line.slice(colonIndex + 1);
|
|
417
|
+
if (value.startsWith(' ')) {
|
|
418
|
+
value = value.slice(1);
|
|
419
|
+
}
|
|
420
|
+
switch (field) {
|
|
421
|
+
case 'event':
|
|
422
|
+
currentEvent.event = value;
|
|
423
|
+
break;
|
|
424
|
+
case 'data':
|
|
425
|
+
try {
|
|
426
|
+
currentEvent.data = JSON.parse(value);
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
this._sseError.next({
|
|
430
|
+
type: 'parse',
|
|
431
|
+
message: `Failed to parse SSE data: ${value}`,
|
|
432
|
+
timestamp: Date.now()
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
break;
|
|
436
|
+
case 'id':
|
|
437
|
+
currentEvent.id = value;
|
|
438
|
+
break;
|
|
439
|
+
case 'retry':
|
|
440
|
+
currentEvent.retry = parseInt(value, 10);
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
events,
|
|
446
|
+
remainingBuffer: buffer.slice(processedUpTo)
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Handles SSE errors and triggers reconnection if appropriate
|
|
451
|
+
*/
|
|
452
|
+
handleSSEError(error) {
|
|
453
|
+
const sseError = {
|
|
454
|
+
type: 'network',
|
|
455
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
456
|
+
originalError: error instanceof Error ? error : undefined,
|
|
457
|
+
timestamp: Date.now()
|
|
458
|
+
};
|
|
459
|
+
// Determine error type
|
|
460
|
+
if (error && typeof error === 'object' && 'type' in error) {
|
|
461
|
+
sseError.type = error.type;
|
|
462
|
+
}
|
|
463
|
+
this._sseConnectionState.next('error');
|
|
464
|
+
this._sseError.next(sseError);
|
|
465
|
+
// Don't reconnect on auth errors
|
|
466
|
+
if (sseError.type === 'auth') {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
this.scheduleReconnect();
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Schedules a reconnection attempt with exponential backoff
|
|
473
|
+
*/
|
|
474
|
+
scheduleReconnect() {
|
|
475
|
+
if (!this.sseOptions?.autoReconnect) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (this.sseOptions.maxReconnectAttempts !== undefined &&
|
|
479
|
+
this.sseReconnectAttempts >= this.sseOptions.maxReconnectAttempts) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const initialDelay = this.sseOptions.initialReconnectDelay ?? 1000;
|
|
483
|
+
const maxDelay = this.sseOptions.maxReconnectDelay ?? 30000;
|
|
484
|
+
// Exponential backoff with jitter
|
|
485
|
+
const exponentialDelay = initialDelay * Math.pow(2, this.sseReconnectAttempts);
|
|
486
|
+
const jitter = Math.random() * 1000;
|
|
487
|
+
const delay = Math.min(exponentialDelay + jitter, maxDelay);
|
|
488
|
+
this._sseConnectionState.next('reconnecting');
|
|
489
|
+
this.sseReconnectAttempts++;
|
|
490
|
+
this.sseReconnectTimeout = setTimeout(async () => {
|
|
491
|
+
// Refresh token before reconnecting
|
|
492
|
+
try {
|
|
493
|
+
await this.refreshToken();
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
// Token refresh failed, will try again on next reconnect
|
|
497
|
+
}
|
|
498
|
+
this._connectSSE();
|
|
499
|
+
}, delay);
|
|
500
|
+
}
|
|
216
501
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/library.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
1
2
|
import { IFile, ITag, IPerson, ISerie, IMovie, MediaRequest, IEpisode, ExternalImage, IBackupFile, ILibrary, SerieInMedia, DeletedQuery, RsDeleted, MovieSort, RsSort, SqlOrder, RsRequest, DetectedFaceResult, UnassignFaceResponse, RsGroupDownload } from './interfaces.js';
|
|
3
|
+
import { SSEMediasEvent, SSEMediasProgressEvent, SSEConvertProgressEvent, SSEEpisodesEvent, SSESeriesEvent, SSEMoviesEvent, SSEPeopleEvent, SSETagsEvent, SSELibraryStatusEvent } from './sse-types.js';
|
|
2
4
|
import { EncryptFileOptions, EncryptedFile } from './encryption.js';
|
|
3
5
|
export interface MediaForUpdate {
|
|
4
6
|
name?: string;
|
|
@@ -74,6 +76,15 @@ export interface LibraryHttpClient {
|
|
|
74
76
|
}>;
|
|
75
77
|
getFullUrl(path: string, params?: Record<string, string>): string;
|
|
76
78
|
getAuthToken(): string;
|
|
79
|
+
readonly medias$?: Observable<SSEMediasEvent>;
|
|
80
|
+
readonly mediasProgress$?: Observable<SSEMediasProgressEvent>;
|
|
81
|
+
readonly convertProgress$?: Observable<SSEConvertProgressEvent>;
|
|
82
|
+
readonly episodes$?: Observable<SSEEpisodesEvent>;
|
|
83
|
+
readonly series$?: Observable<SSESeriesEvent>;
|
|
84
|
+
readonly movies$?: Observable<SSEMoviesEvent>;
|
|
85
|
+
readonly people$?: Observable<SSEPeopleEvent>;
|
|
86
|
+
readonly tags$?: Observable<SSETagsEvent>;
|
|
87
|
+
readonly libraryStatus$?: Observable<SSELibraryStatusEvent>;
|
|
77
88
|
}
|
|
78
89
|
export declare class LibraryApi {
|
|
79
90
|
private client;
|
|
@@ -81,7 +92,27 @@ export declare class LibraryApi {
|
|
|
81
92
|
private library;
|
|
82
93
|
private key?;
|
|
83
94
|
private keyText?;
|
|
95
|
+
private disposed;
|
|
96
|
+
readonly medias$: Observable<SSEMediasEvent>;
|
|
97
|
+
readonly mediasProgress$: Observable<SSEMediasProgressEvent>;
|
|
98
|
+
readonly convertProgress$: Observable<SSEConvertProgressEvent>;
|
|
99
|
+
readonly episodes$: Observable<SSEEpisodesEvent>;
|
|
100
|
+
readonly series$: Observable<SSESeriesEvent>;
|
|
101
|
+
readonly movies$: Observable<SSEMoviesEvent>;
|
|
102
|
+
readonly people$: Observable<SSEPeopleEvent>;
|
|
103
|
+
readonly tags$: Observable<SSETagsEvent>;
|
|
104
|
+
readonly libraryStatus$: Observable<SSELibraryStatusEvent>;
|
|
84
105
|
constructor(client: LibraryHttpClient, libraryId: string, library: ILibrary);
|
|
106
|
+
/**
|
|
107
|
+
* Creates a library-filtered stream from a client stream.
|
|
108
|
+
* Returns EMPTY if the client doesn't have SSE support.
|
|
109
|
+
*/
|
|
110
|
+
private createLibraryFilteredStream;
|
|
111
|
+
/**
|
|
112
|
+
* Marks this LibraryApi as disposed.
|
|
113
|
+
* After calling dispose(), the filtered streams will stop emitting events.
|
|
114
|
+
*/
|
|
115
|
+
dispose(): void;
|
|
85
116
|
setKey(passPhrase: string): Promise<void>;
|
|
86
117
|
private getUrl;
|
|
87
118
|
getTags(query?: {
|
package/dist/library.js
CHANGED
|
@@ -1,10 +1,39 @@
|
|
|
1
|
+
import { filter, EMPTY } from 'rxjs';
|
|
1
2
|
import { deriveKey, encryptText as encryptTextUtil, decryptText as decryptTextUtil, encryptBuffer, decryptBuffer, encryptFile as encryptFileUtil, decryptFile as decryptFileUtil, decryptFileThumb, encryptFilename as encryptFilenameUtil, getRandomIV as getRandomIVUtil } from './encryption.js';
|
|
2
3
|
import { uint8ArrayFromBase64 } from './crypto.js';
|
|
3
4
|
export class LibraryApi {
|
|
4
5
|
constructor(client, libraryId, library) {
|
|
5
6
|
this.client = client;
|
|
6
7
|
this.libraryId = libraryId;
|
|
8
|
+
this.disposed = false;
|
|
7
9
|
this.library = library;
|
|
10
|
+
// Create library-filtered streams
|
|
11
|
+
this.medias$ = this.createLibraryFilteredStream(client.medias$);
|
|
12
|
+
this.mediasProgress$ = this.createLibraryFilteredStream(client.mediasProgress$);
|
|
13
|
+
this.convertProgress$ = this.createLibraryFilteredStream(client.convertProgress$);
|
|
14
|
+
this.episodes$ = this.createLibraryFilteredStream(client.episodes$);
|
|
15
|
+
this.series$ = this.createLibraryFilteredStream(client.series$);
|
|
16
|
+
this.movies$ = this.createLibraryFilteredStream(client.movies$);
|
|
17
|
+
this.people$ = this.createLibraryFilteredStream(client.people$);
|
|
18
|
+
this.tags$ = this.createLibraryFilteredStream(client.tags$);
|
|
19
|
+
this.libraryStatus$ = this.createLibraryFilteredStream(client.libraryStatus$);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Creates a library-filtered stream from a client stream.
|
|
23
|
+
* Returns EMPTY if the client doesn't have SSE support.
|
|
24
|
+
*/
|
|
25
|
+
createLibraryFilteredStream(source$) {
|
|
26
|
+
if (!source$) {
|
|
27
|
+
return EMPTY;
|
|
28
|
+
}
|
|
29
|
+
return source$.pipe(filter(event => !this.disposed && event.library === this.libraryId));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Marks this LibraryApi as disposed.
|
|
33
|
+
* After calling dispose(), the filtered streams will stop emitting events.
|
|
34
|
+
*/
|
|
35
|
+
dispose() {
|
|
36
|
+
this.disposed = true;
|
|
8
37
|
}
|
|
9
38
|
async setKey(passPhrase) {
|
|
10
39
|
// Derive keys
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { ILibrary, IFile, IEpisode, ISerie, IMovie, IPerson, ITag, IBackup } from './interfaces.js';
|
|
2
|
+
export type ElementAction = 'Added' | 'Updated' | 'Deleted';
|
|
3
|
+
export type SSEConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error';
|
|
4
|
+
export interface SSEConnectionOptions {
|
|
5
|
+
/** Filter to specific library IDs */
|
|
6
|
+
libraries?: string[];
|
|
7
|
+
/** Enable auto-reconnect on disconnect (default: true) */
|
|
8
|
+
autoReconnect?: boolean;
|
|
9
|
+
/** Maximum reconnect attempts (default: unlimited) */
|
|
10
|
+
maxReconnectAttempts?: number;
|
|
11
|
+
/** Initial reconnect delay in ms (default: 1000) */
|
|
12
|
+
initialReconnectDelay?: number;
|
|
13
|
+
/** Maximum reconnect delay in ms (default: 30000) */
|
|
14
|
+
maxReconnectDelay?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface SSEConnectionError {
|
|
17
|
+
type: 'network' | 'auth' | 'server' | 'parse';
|
|
18
|
+
message: string;
|
|
19
|
+
originalError?: Error;
|
|
20
|
+
timestamp: number;
|
|
21
|
+
}
|
|
22
|
+
export interface SSELibraryEvent {
|
|
23
|
+
action: ElementAction;
|
|
24
|
+
library: ILibrary;
|
|
25
|
+
}
|
|
26
|
+
export interface SSELibraryStatusEvent {
|
|
27
|
+
message: string;
|
|
28
|
+
library: string;
|
|
29
|
+
progress?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface SSEMediasEvent {
|
|
32
|
+
library: string;
|
|
33
|
+
medias: {
|
|
34
|
+
action: ElementAction;
|
|
35
|
+
media: IFile;
|
|
36
|
+
}[];
|
|
37
|
+
}
|
|
38
|
+
export interface SSEMediasProgressEvent {
|
|
39
|
+
library: string;
|
|
40
|
+
mediaId: string;
|
|
41
|
+
progress: number;
|
|
42
|
+
status?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface SSEConvertProgressEvent {
|
|
45
|
+
library: string;
|
|
46
|
+
mediaId: string;
|
|
47
|
+
progress: number;
|
|
48
|
+
status: string;
|
|
49
|
+
message?: string;
|
|
50
|
+
}
|
|
51
|
+
export interface SSEEpisodesEvent {
|
|
52
|
+
library: string;
|
|
53
|
+
episodes: {
|
|
54
|
+
action: ElementAction;
|
|
55
|
+
episode: IEpisode;
|
|
56
|
+
}[];
|
|
57
|
+
}
|
|
58
|
+
export interface SSESeriesEvent {
|
|
59
|
+
library: string;
|
|
60
|
+
series: {
|
|
61
|
+
action: ElementAction;
|
|
62
|
+
serie: ISerie;
|
|
63
|
+
}[];
|
|
64
|
+
}
|
|
65
|
+
export interface SSEMoviesEvent {
|
|
66
|
+
library: string;
|
|
67
|
+
movies: {
|
|
68
|
+
action: ElementAction;
|
|
69
|
+
movie: IMovie;
|
|
70
|
+
}[];
|
|
71
|
+
}
|
|
72
|
+
export interface SSEPeopleEvent {
|
|
73
|
+
library: string;
|
|
74
|
+
people: {
|
|
75
|
+
action: ElementAction;
|
|
76
|
+
person: IPerson;
|
|
77
|
+
}[];
|
|
78
|
+
}
|
|
79
|
+
export interface SSETagsEvent {
|
|
80
|
+
library: string;
|
|
81
|
+
tags: {
|
|
82
|
+
action: ElementAction;
|
|
83
|
+
tag: ITag;
|
|
84
|
+
}[];
|
|
85
|
+
}
|
|
86
|
+
export interface IBackupWithStatus extends IBackup {
|
|
87
|
+
status?: 'running' | 'completed' | 'failed';
|
|
88
|
+
progress?: number;
|
|
89
|
+
message?: string;
|
|
90
|
+
}
|
|
91
|
+
export interface SSEBackupsEvent {
|
|
92
|
+
backup: IBackupWithStatus;
|
|
93
|
+
}
|
|
94
|
+
export interface SSEBackupFilesEvent {
|
|
95
|
+
library?: string;
|
|
96
|
+
file: string;
|
|
97
|
+
progress: number;
|
|
98
|
+
status?: string;
|
|
99
|
+
message?: string;
|
|
100
|
+
}
|
|
101
|
+
export interface SSEEventMap {
|
|
102
|
+
'library': SSELibraryEvent;
|
|
103
|
+
'library-status': SSELibraryStatusEvent;
|
|
104
|
+
'medias': SSEMediasEvent;
|
|
105
|
+
'medias_progress': SSEMediasProgressEvent;
|
|
106
|
+
'convert_progress': SSEConvertProgressEvent;
|
|
107
|
+
'episodes': SSEEpisodesEvent;
|
|
108
|
+
'series': SSESeriesEvent;
|
|
109
|
+
'movies': SSEMoviesEvent;
|
|
110
|
+
'people': SSEPeopleEvent;
|
|
111
|
+
'tags': SSETagsEvent;
|
|
112
|
+
'backups': SSEBackupsEvent;
|
|
113
|
+
'backups-files': SSEBackupFilesEvent;
|
|
114
|
+
}
|
|
115
|
+
export type SSEEventName = keyof SSEEventMap;
|
|
116
|
+
export interface SSEEvent<T extends SSEEventName = SSEEventName> {
|
|
117
|
+
id?: string;
|
|
118
|
+
event: T;
|
|
119
|
+
data: SSEEventMap[T];
|
|
120
|
+
retry?: number;
|
|
121
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/libraries.md
CHANGED
|
@@ -66,6 +66,172 @@ if (library.crypt) {
|
|
|
66
66
|
|
|
67
67
|
**Note:** This method verifies the key by attempting to decrypt a media thumbnail. If decryption fails, it throws an error.
|
|
68
68
|
|
|
69
|
+
### `dispose(): void`
|
|
70
|
+
|
|
71
|
+
Marks the LibraryApi as disposed. After calling dispose(), the filtered event streams will stop emitting events.
|
|
72
|
+
|
|
73
|
+
**Example:**
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
libraryApi.dispose();
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Real-Time Events (SSE)
|
|
82
|
+
|
|
83
|
+
`LibraryApi` provides library-filtered event streams that automatically filter events to only include those relevant to the library. These streams require the `RedseatClient` to be connected to SSE.
|
|
84
|
+
|
|
85
|
+
### Event Streams
|
|
86
|
+
|
|
87
|
+
All event streams are RxJS Observables that emit events only for this library.
|
|
88
|
+
|
|
89
|
+
#### `medias$: Observable<SSEMediasEvent>`
|
|
90
|
+
|
|
91
|
+
Emits when media files in this library are added, updated, or deleted.
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
libraryApi.medias$.subscribe(event => {
|
|
95
|
+
for (const { action, media } of event.medias) {
|
|
96
|
+
console.log(`Media ${action}: ${media.name}`);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### `mediasProgress$: Observable<SSEMediasProgressEvent>`
|
|
102
|
+
|
|
103
|
+
Emits media processing progress updates for this library.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
libraryApi.mediasProgress$.subscribe(event => {
|
|
107
|
+
console.log(`Media ${event.mediaId}: ${event.progress}%`);
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### `convertProgress$: Observable<SSEConvertProgressEvent>`
|
|
112
|
+
|
|
113
|
+
Emits video conversion progress updates for this library.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
libraryApi.convertProgress$.subscribe(event => {
|
|
117
|
+
console.log(`Converting ${event.mediaId}: ${event.progress}% - ${event.status}`);
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### `episodes$: Observable<SSEEpisodesEvent>`
|
|
122
|
+
|
|
123
|
+
Emits when episodes in this library are added, updated, or deleted.
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
libraryApi.episodes$.subscribe(event => {
|
|
127
|
+
for (const { action, episode } of event.episodes) {
|
|
128
|
+
console.log(`Episode ${action}: ${episode.name}`);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### `series$: Observable<SSESeriesEvent>`
|
|
134
|
+
|
|
135
|
+
Emits when series in this library are added, updated, or deleted.
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
libraryApi.series$.subscribe(event => {
|
|
139
|
+
for (const { action, serie } of event.series) {
|
|
140
|
+
console.log(`Series ${action}: ${serie.name}`);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### `movies$: Observable<SSEMoviesEvent>`
|
|
146
|
+
|
|
147
|
+
Emits when movies in this library are added, updated, or deleted.
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
libraryApi.movies$.subscribe(event => {
|
|
151
|
+
for (const { action, movie } of event.movies) {
|
|
152
|
+
console.log(`Movie ${action}: ${movie.name}`);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### `people$: Observable<SSEPeopleEvent>`
|
|
158
|
+
|
|
159
|
+
Emits when people in this library are added, updated, or deleted.
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
libraryApi.people$.subscribe(event => {
|
|
163
|
+
for (const { action, person } of event.people) {
|
|
164
|
+
console.log(`Person ${action}: ${person.name}`);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
#### `tags$: Observable<SSETagsEvent>`
|
|
170
|
+
|
|
171
|
+
Emits when tags in this library are added, updated, or deleted.
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
libraryApi.tags$.subscribe(event => {
|
|
175
|
+
for (const { action, tag } of event.tags) {
|
|
176
|
+
console.log(`Tag ${action}: ${tag.name}`);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
#### `libraryStatus$: Observable<SSELibraryStatusEvent>`
|
|
182
|
+
|
|
183
|
+
Emits status updates for this library (e.g., scanning progress).
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
libraryApi.libraryStatus$.subscribe(event => {
|
|
187
|
+
console.log(`Status: ${event.message} (${event.progress}%)`);
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Complete SSE Example with LibraryApi
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
import { RedseatClient, LibraryApi, ServerApi } from '@redseat/api';
|
|
195
|
+
import { Subscription } from 'rxjs';
|
|
196
|
+
|
|
197
|
+
const client = new RedseatClient({
|
|
198
|
+
server: { id: 'server-123', url: 'example.com' },
|
|
199
|
+
getIdToken: async () => await getFirebaseToken()
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Connect to SSE first
|
|
203
|
+
await client.connectSSE();
|
|
204
|
+
|
|
205
|
+
// Get library and create LibraryApi
|
|
206
|
+
const serverApi = new ServerApi(client);
|
|
207
|
+
const libraries = await serverApi.getLibraries();
|
|
208
|
+
const library = libraries[0];
|
|
209
|
+
const libraryApi = new LibraryApi(client, library.id!, library);
|
|
210
|
+
|
|
211
|
+
// Track subscriptions
|
|
212
|
+
const subscriptions: Subscription[] = [];
|
|
213
|
+
|
|
214
|
+
// Subscribe to library-specific events
|
|
215
|
+
subscriptions.push(
|
|
216
|
+
libraryApi.medias$.subscribe(event => {
|
|
217
|
+
for (const { action, media } of event.medias) {
|
|
218
|
+
console.log(`[${library.name}] Media ${action}: ${media.name}`);
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
subscriptions.push(
|
|
224
|
+
libraryApi.libraryStatus$.subscribe(event => {
|
|
225
|
+
console.log(`[${library.name}] ${event.message}`);
|
|
226
|
+
})
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// ... later, cleanup
|
|
230
|
+
subscriptions.forEach(sub => sub.unsubscribe());
|
|
231
|
+
libraryApi.dispose();
|
|
232
|
+
client.disconnectSSE();
|
|
233
|
+
```
|
|
234
|
+
|
|
69
235
|
---
|
|
70
236
|
|
|
71
237
|
## Tags
|