@redseat/api 0.3.12 → 0.4.0
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 +275 -2
- package/client.md +9 -2
- package/dist/client.d.ts +29 -2
- package/dist/client.js +257 -26
- package/dist/interfaces.d.ts +166 -8
- package/dist/interfaces.js +1 -0
- package/dist/library.d.ts +85 -63
- package/dist/library.js +205 -11
- package/dist/server.d.ts +5 -2
- package/dist/server.js +53 -3
- package/dist/sse-types.d.ts +24 -4
- package/libraries.md +176 -9
- package/package.json +1 -1
- package/server.md +60 -9
package/dist/client.js
CHANGED
|
@@ -20,6 +20,7 @@ export class RedseatClient {
|
|
|
20
20
|
'episodes': 'episodes',
|
|
21
21
|
'series': 'series',
|
|
22
22
|
'movies': 'movies',
|
|
23
|
+
'books': 'books',
|
|
23
24
|
'people': 'people',
|
|
24
25
|
'tags': 'tags',
|
|
25
26
|
'request_processing': 'requestProcessing',
|
|
@@ -38,6 +39,11 @@ export class RedseatClient {
|
|
|
38
39
|
constructor(options) {
|
|
39
40
|
this.disposed = false;
|
|
40
41
|
this.sseReconnectAttempts = 0;
|
|
42
|
+
this.sseAuthReconnectAttempts = 0;
|
|
43
|
+
this.sseShouldReconnect = false;
|
|
44
|
+
this.sseIsConnecting = false;
|
|
45
|
+
this.sseLifecycleRegistered = false;
|
|
46
|
+
this.SSE_ACTIVITY_TIMEOUT = 90000; // 90 seconds - reconnect if no data received
|
|
41
47
|
// RxJS subjects for SSE
|
|
42
48
|
this._sseConnectionState = new BehaviorSubject('disconnected');
|
|
43
49
|
this._sseError = new Subject();
|
|
@@ -55,6 +61,7 @@ export class RedseatClient {
|
|
|
55
61
|
this.episodes$ = this.createEventStream('episodes');
|
|
56
62
|
this.series$ = this.createEventStream('series');
|
|
57
63
|
this.movies$ = this.createEventStream('movies');
|
|
64
|
+
this.books$ = this.createEventStream('books');
|
|
58
65
|
this.people$ = this.createEventStream('people');
|
|
59
66
|
this.tags$ = this.createEventStream('tags');
|
|
60
67
|
this.backups$ = this.createEventStream('backups');
|
|
@@ -65,6 +72,62 @@ export class RedseatClient {
|
|
|
65
72
|
this.watched$ = this.createEventStream('watched');
|
|
66
73
|
this.unwatched$ = this.createEventStream('unwatched');
|
|
67
74
|
this.requestProcessing$ = this.createEventStream('request_processing');
|
|
75
|
+
// ==================== SSE Methods ====================
|
|
76
|
+
this.onDocumentVisibilityChange = () => {
|
|
77
|
+
if (!this.shouldAttemptReconnect() || typeof document === 'undefined') {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (document.visibilityState === 'hidden') {
|
|
81
|
+
this.sseHiddenAt = Date.now();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!(this.sseOptions?.reconnectOnPageVisible ?? true)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const hiddenAt = this.sseHiddenAt;
|
|
88
|
+
this.sseHiddenAt = undefined;
|
|
89
|
+
if (hiddenAt === undefined) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const hiddenDuration = Date.now() - hiddenAt;
|
|
93
|
+
const minHiddenDuration = this.sseOptions?.reconnectVisibleAfterMs ?? 30000;
|
|
94
|
+
if (hiddenDuration >= minHiddenDuration) {
|
|
95
|
+
this.forceReconnectSSE();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
this.onWindowPageShow = (event) => {
|
|
99
|
+
if (!this.shouldAttemptReconnect()) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (!(this.sseOptions?.reconnectOnPageVisible ?? true)) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (event.persisted || this.sseConnectionState !== 'connected') {
|
|
106
|
+
this.forceReconnectSSE();
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
this.onWindowOnline = () => {
|
|
110
|
+
if (!this.shouldAttemptReconnect()) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (!(this.sseOptions?.reconnectOnOnline ?? true)) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (this.sseConnectionState !== 'connected') {
|
|
117
|
+
this.forceReconnectSSE();
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
this.onWindowFocus = () => {
|
|
121
|
+
if (!this.shouldAttemptReconnect()) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (!(this.sseOptions?.reconnectOnFocus ?? true)) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (this.sseConnectionState !== 'connected') {
|
|
128
|
+
this.forceReconnectSSE();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
68
131
|
this.server = options.server;
|
|
69
132
|
this.redseatUrl = options.redseatUrl;
|
|
70
133
|
this.getIdToken = options.getIdToken;
|
|
@@ -167,7 +230,8 @@ export class RedseatClient {
|
|
|
167
230
|
}
|
|
168
231
|
this.tokenRefreshPromise = (async () => {
|
|
169
232
|
try {
|
|
170
|
-
|
|
233
|
+
// Force refresh the Firebase idToken to ensure it's not stale/cached
|
|
234
|
+
const idToken = await this.getIdToken(true);
|
|
171
235
|
// Use fetchServerToken which uses the global axios instance
|
|
172
236
|
// The token endpoint is on the frontend server, not the backend server
|
|
173
237
|
const newToken = await fetchServerToken(this.serverId, idToken, this.redseatUrl);
|
|
@@ -280,7 +344,52 @@ export class RedseatClient {
|
|
|
280
344
|
});
|
|
281
345
|
return response.data;
|
|
282
346
|
}
|
|
283
|
-
|
|
347
|
+
shouldAttemptReconnect() {
|
|
348
|
+
return !this.disposed && this.sseShouldReconnect && (this.sseOptions?.autoReconnect ?? true);
|
|
349
|
+
}
|
|
350
|
+
clearSSEReconnectTimeout() {
|
|
351
|
+
if (this.sseReconnectTimeout) {
|
|
352
|
+
clearTimeout(this.sseReconnectTimeout);
|
|
353
|
+
this.sseReconnectTimeout = undefined;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
registerSSELifecycleListeners() {
|
|
357
|
+
if (this.sseLifecycleRegistered || typeof window === 'undefined' || typeof document === 'undefined') {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
document.addEventListener('visibilitychange', this.onDocumentVisibilityChange);
|
|
361
|
+
window.addEventListener('pageshow', this.onWindowPageShow);
|
|
362
|
+
window.addEventListener('online', this.onWindowOnline);
|
|
363
|
+
window.addEventListener('focus', this.onWindowFocus);
|
|
364
|
+
this.sseLifecycleRegistered = true;
|
|
365
|
+
if (document.visibilityState === 'hidden') {
|
|
366
|
+
this.sseHiddenAt = Date.now();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
unregisterSSELifecycleListeners() {
|
|
370
|
+
if (!this.sseLifecycleRegistered || typeof window === 'undefined' || typeof document === 'undefined') {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
document.removeEventListener('visibilitychange', this.onDocumentVisibilityChange);
|
|
374
|
+
window.removeEventListener('pageshow', this.onWindowPageShow);
|
|
375
|
+
window.removeEventListener('online', this.onWindowOnline);
|
|
376
|
+
window.removeEventListener('focus', this.onWindowFocus);
|
|
377
|
+
this.sseLifecycleRegistered = false;
|
|
378
|
+
this.sseHiddenAt = undefined;
|
|
379
|
+
}
|
|
380
|
+
forceReconnectSSE() {
|
|
381
|
+
if (!this.shouldAttemptReconnect()) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
this.clearSSEReconnectTimeout();
|
|
385
|
+
if (this.sseAbortController) {
|
|
386
|
+
this.sseAbortController.abort();
|
|
387
|
+
this.sseAbortController = undefined;
|
|
388
|
+
}
|
|
389
|
+
this.sseReconnectAttempts = 0;
|
|
390
|
+
this._sseConnectionState.next('reconnecting');
|
|
391
|
+
void this._connectSSE();
|
|
392
|
+
}
|
|
284
393
|
/**
|
|
285
394
|
* Connects to the server's SSE endpoint for real-time updates.
|
|
286
395
|
* Automatically manages authentication and reconnection.
|
|
@@ -293,23 +402,34 @@ export class RedseatClient {
|
|
|
293
402
|
autoReconnect: true,
|
|
294
403
|
initialReconnectDelay: 1000,
|
|
295
404
|
maxReconnectDelay: 30000,
|
|
405
|
+
reconnectOnPageVisible: true,
|
|
406
|
+
reconnectVisibleAfterMs: 30000,
|
|
407
|
+
reconnectOnOnline: true,
|
|
408
|
+
reconnectOnFocus: true,
|
|
409
|
+
reconnectOnAuthError: true,
|
|
410
|
+
maxAuthReconnectAttempts: 5,
|
|
296
411
|
...options
|
|
297
412
|
};
|
|
298
413
|
this.sseReconnectAttempts = 0;
|
|
414
|
+
this.sseAuthReconnectAttempts = 0;
|
|
415
|
+
this.sseShouldReconnect = true;
|
|
416
|
+
this.registerSSELifecycleListeners();
|
|
299
417
|
await this._connectSSE();
|
|
300
418
|
}
|
|
301
419
|
/**
|
|
302
420
|
* Disconnects from the SSE endpoint and cleans up resources.
|
|
303
421
|
*/
|
|
304
422
|
disconnectSSE() {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
423
|
+
this.sseShouldReconnect = false;
|
|
424
|
+
this.sseIsConnecting = false;
|
|
425
|
+
this.sseAuthReconnectAttempts = 0;
|
|
426
|
+
this.clearSSEReconnectTimeout();
|
|
427
|
+
this.clearActivityTimeout();
|
|
309
428
|
if (this.sseAbortController) {
|
|
310
429
|
this.sseAbortController.abort();
|
|
311
430
|
this.sseAbortController = undefined;
|
|
312
431
|
}
|
|
432
|
+
this.unregisterSSELifecycleListeners();
|
|
313
433
|
this._sseConnectionState.next('disconnected');
|
|
314
434
|
}
|
|
315
435
|
/**
|
|
@@ -335,6 +455,14 @@ export class RedseatClient {
|
|
|
335
455
|
* Internal method to establish SSE connection
|
|
336
456
|
*/
|
|
337
457
|
async _connectSSE() {
|
|
458
|
+
if (this.disposed || !this.sseShouldReconnect || this.sseIsConnecting) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (this.sseConnectionState === 'connected' && this.sseAbortController) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
this.clearSSEReconnectTimeout();
|
|
465
|
+
this.sseIsConnecting = true;
|
|
338
466
|
this._sseConnectionState.next('connecting');
|
|
339
467
|
try {
|
|
340
468
|
// Ensure we have a valid token
|
|
@@ -343,28 +471,54 @@ export class RedseatClient {
|
|
|
343
471
|
throw new Error('No authentication token available');
|
|
344
472
|
}
|
|
345
473
|
const url = this.buildSSEUrl();
|
|
346
|
-
|
|
474
|
+
const abortController = new AbortController();
|
|
475
|
+
this.sseAbortController = abortController;
|
|
347
476
|
console.log("SSSEEEE URL", url);
|
|
348
|
-
|
|
477
|
+
let response = await fetch(url, {
|
|
349
478
|
method: 'GET',
|
|
350
479
|
headers: {
|
|
351
480
|
'Authorization': `Bearer ${this.tokenData.token}`,
|
|
352
481
|
'Accept': 'text/event-stream',
|
|
353
482
|
'Cache-Control': 'no-cache'
|
|
354
483
|
},
|
|
355
|
-
signal:
|
|
484
|
+
signal: abortController.signal
|
|
356
485
|
});
|
|
357
486
|
if (!response.ok) {
|
|
358
487
|
if (response.status === 401) {
|
|
359
|
-
|
|
488
|
+
// Try refreshing token and retry ONCE before giving up
|
|
489
|
+
try {
|
|
490
|
+
console.log('SSE 401 - attempting token refresh and retry');
|
|
491
|
+
await this.refreshToken();
|
|
492
|
+
// Retry the connection with the fresh token
|
|
493
|
+
const retryResponse = await fetch(url, {
|
|
494
|
+
method: 'GET',
|
|
495
|
+
headers: {
|
|
496
|
+
'Authorization': `Bearer ${this.tokenData.token}`,
|
|
497
|
+
'Accept': 'text/event-stream',
|
|
498
|
+
'Cache-Control': 'no-cache'
|
|
499
|
+
},
|
|
500
|
+
signal: this.sseAbortController.signal
|
|
501
|
+
});
|
|
502
|
+
if (!retryResponse.ok) {
|
|
503
|
+
throw Object.assign(new Error('Authentication failed after token refresh'), { type: 'auth' });
|
|
504
|
+
}
|
|
505
|
+
// Use the retry response instead
|
|
506
|
+
response = retryResponse;
|
|
507
|
+
}
|
|
508
|
+
catch (retryError) {
|
|
509
|
+
throw Object.assign(new Error('Authentication failed'), { type: 'auth' });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
throw Object.assign(new Error(`Server returned ${response.status}`), { type: 'server' });
|
|
360
514
|
}
|
|
361
|
-
throw Object.assign(new Error(`Server returned ${response.status}`), { type: 'server' });
|
|
362
515
|
}
|
|
363
516
|
if (!response.body) {
|
|
364
517
|
throw Object.assign(new Error('No response body'), { type: 'server' });
|
|
365
518
|
}
|
|
366
519
|
this._sseConnectionState.next('connected');
|
|
367
520
|
this.sseReconnectAttempts = 0;
|
|
521
|
+
this.sseAuthReconnectAttempts = 0;
|
|
368
522
|
// Process the stream in the background (don't await - it runs forever until disconnected)
|
|
369
523
|
this.processSSEStream(response.body).catch(err => {
|
|
370
524
|
if (err?.name !== 'AbortError') {
|
|
@@ -377,8 +531,12 @@ export class RedseatClient {
|
|
|
377
531
|
// Intentionally disconnected
|
|
378
532
|
return;
|
|
379
533
|
}
|
|
534
|
+
this.sseAbortController = undefined;
|
|
380
535
|
this.handleSSEError(error);
|
|
381
536
|
}
|
|
537
|
+
finally {
|
|
538
|
+
this.sseIsConnecting = false;
|
|
539
|
+
}
|
|
382
540
|
}
|
|
383
541
|
/**
|
|
384
542
|
* Builds the SSE endpoint URL with optional library filters
|
|
@@ -392,6 +550,39 @@ export class RedseatClient {
|
|
|
392
550
|
}
|
|
393
551
|
return url;
|
|
394
552
|
}
|
|
553
|
+
/**
|
|
554
|
+
* Resets the activity timeout timer.
|
|
555
|
+
* Called when data is received from the SSE stream.
|
|
556
|
+
* If no data is received within the timeout period, triggers reconnection.
|
|
557
|
+
*/
|
|
558
|
+
resetActivityTimeout() {
|
|
559
|
+
if (this.sseActivityTimeout) {
|
|
560
|
+
clearTimeout(this.sseActivityTimeout);
|
|
561
|
+
}
|
|
562
|
+
this.sseActivityTimeout = setTimeout(() => {
|
|
563
|
+
if (this.disposed) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
// No activity within timeout period - connection is likely stale
|
|
567
|
+
console.log('SSE activity timeout - reconnecting');
|
|
568
|
+
this._sseConnectionState.next('disconnected');
|
|
569
|
+
// Abort current connection and schedule reconnect
|
|
570
|
+
if (this.sseAbortController) {
|
|
571
|
+
this.sseAbortController.abort();
|
|
572
|
+
this.sseAbortController = undefined;
|
|
573
|
+
}
|
|
574
|
+
this.scheduleReconnect();
|
|
575
|
+
}, this.SSE_ACTIVITY_TIMEOUT);
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Clears the activity timeout timer.
|
|
579
|
+
*/
|
|
580
|
+
clearActivityTimeout() {
|
|
581
|
+
if (this.sseActivityTimeout) {
|
|
582
|
+
clearTimeout(this.sseActivityTimeout);
|
|
583
|
+
this.sseActivityTimeout = undefined;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
395
586
|
/**
|
|
396
587
|
* Processes the SSE stream and emits events
|
|
397
588
|
*/
|
|
@@ -399,15 +590,22 @@ export class RedseatClient {
|
|
|
399
590
|
const reader = body.getReader();
|
|
400
591
|
const decoder = new TextDecoder();
|
|
401
592
|
let buffer = '';
|
|
593
|
+
// Start activity timeout tracking
|
|
594
|
+
this.resetActivityTimeout();
|
|
402
595
|
try {
|
|
403
596
|
while (true) {
|
|
404
597
|
const { done, value } = await reader.read();
|
|
405
598
|
if (done) {
|
|
406
599
|
// Stream ended - server closed connection
|
|
600
|
+
this.clearActivityTimeout();
|
|
407
601
|
this._sseConnectionState.next('disconnected');
|
|
408
|
-
this.
|
|
602
|
+
if (this.shouldAttemptReconnect()) {
|
|
603
|
+
this.scheduleReconnect();
|
|
604
|
+
}
|
|
409
605
|
break;
|
|
410
606
|
}
|
|
607
|
+
// Data received - reset activity timeout
|
|
608
|
+
this.resetActivityTimeout();
|
|
411
609
|
buffer += decoder.decode(value, { stream: true });
|
|
412
610
|
const { events, remainingBuffer } = this.parseSSEBuffer(buffer);
|
|
413
611
|
buffer = remainingBuffer;
|
|
@@ -418,12 +616,14 @@ export class RedseatClient {
|
|
|
418
616
|
}
|
|
419
617
|
}
|
|
420
618
|
catch (error) {
|
|
619
|
+
this.clearActivityTimeout();
|
|
421
620
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
422
621
|
return;
|
|
423
622
|
}
|
|
424
623
|
throw error;
|
|
425
624
|
}
|
|
426
625
|
finally {
|
|
626
|
+
this.sseAbortController = undefined;
|
|
427
627
|
reader.releaseLock();
|
|
428
628
|
}
|
|
429
629
|
}
|
|
@@ -497,6 +697,9 @@ export class RedseatClient {
|
|
|
497
697
|
* Handles SSE errors and triggers reconnection if appropriate
|
|
498
698
|
*/
|
|
499
699
|
handleSSEError(error) {
|
|
700
|
+
if (this.disposed || !this.sseShouldReconnect) {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
500
703
|
const sseError = {
|
|
501
704
|
type: 'network',
|
|
502
705
|
message: error instanceof Error ? error.message : 'Unknown error',
|
|
@@ -509,40 +712,68 @@ export class RedseatClient {
|
|
|
509
712
|
}
|
|
510
713
|
this._sseConnectionState.next('error');
|
|
511
714
|
this._sseError.next(sseError);
|
|
512
|
-
// Don't reconnect on auth errors
|
|
513
715
|
if (sseError.type === 'auth') {
|
|
716
|
+
if (!(this.sseOptions?.reconnectOnAuthError ?? true)) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const maxAuthReconnectAttempts = this.sseOptions?.maxAuthReconnectAttempts ?? 5;
|
|
720
|
+
if (this.sseAuthReconnectAttempts >= maxAuthReconnectAttempts) {
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
this.sseAuthReconnectAttempts++;
|
|
724
|
+
this.scheduleReconnect();
|
|
514
725
|
return;
|
|
515
726
|
}
|
|
727
|
+
this.sseAuthReconnectAttempts = 0;
|
|
516
728
|
this.scheduleReconnect();
|
|
517
729
|
}
|
|
518
730
|
/**
|
|
519
731
|
* Schedules a reconnection attempt with exponential backoff
|
|
520
732
|
*/
|
|
521
733
|
scheduleReconnect() {
|
|
522
|
-
if (!this.
|
|
734
|
+
if (!this.shouldAttemptReconnect()) {
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const options = this.sseOptions;
|
|
738
|
+
if (!options) {
|
|
523
739
|
return;
|
|
524
740
|
}
|
|
525
|
-
if (
|
|
526
|
-
this.sseReconnectAttempts >=
|
|
741
|
+
if (options.maxReconnectAttempts !== undefined &&
|
|
742
|
+
this.sseReconnectAttempts >= options.maxReconnectAttempts) {
|
|
527
743
|
return;
|
|
528
744
|
}
|
|
529
|
-
|
|
530
|
-
|
|
745
|
+
if (this.sseReconnectTimeout) {
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const initialDelay = options.initialReconnectDelay ?? 1000;
|
|
749
|
+
const maxDelay = options.maxReconnectDelay ?? 30000;
|
|
531
750
|
// Exponential backoff with jitter
|
|
532
751
|
const exponentialDelay = initialDelay * Math.pow(2, this.sseReconnectAttempts);
|
|
533
752
|
const jitter = Math.random() * 1000;
|
|
534
753
|
const delay = Math.min(exponentialDelay + jitter, maxDelay);
|
|
535
754
|
this._sseConnectionState.next('reconnecting');
|
|
536
755
|
this.sseReconnectAttempts++;
|
|
537
|
-
this.sseReconnectTimeout = setTimeout(
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
catch {
|
|
543
|
-
// Token refresh failed, will try again on next reconnect
|
|
756
|
+
this.sseReconnectTimeout = setTimeout(() => {
|
|
757
|
+
this.sseReconnectTimeout = undefined;
|
|
758
|
+
if (!this.shouldAttemptReconnect()) {
|
|
759
|
+
return;
|
|
544
760
|
}
|
|
545
|
-
|
|
761
|
+
void (async () => {
|
|
762
|
+
try {
|
|
763
|
+
await this.refreshToken();
|
|
764
|
+
}
|
|
765
|
+
catch (error) {
|
|
766
|
+
console.log('SSE reconnect: token refresh failed, scheduling another retry', error);
|
|
767
|
+
this._sseError.next({
|
|
768
|
+
type: 'auth',
|
|
769
|
+
message: `Token refresh failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
770
|
+
timestamp: Date.now()
|
|
771
|
+
});
|
|
772
|
+
this.scheduleReconnect();
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
await this._connectSSE();
|
|
776
|
+
})();
|
|
546
777
|
}, delay);
|
|
547
778
|
}
|
|
548
779
|
}
|
package/dist/interfaces.d.ts
CHANGED
|
@@ -96,9 +96,6 @@ export interface IFile {
|
|
|
96
96
|
pages?: number;
|
|
97
97
|
progress?: number;
|
|
98
98
|
lang?: string;
|
|
99
|
-
tags?: MediaItemReference[];
|
|
100
|
-
series?: SerieInMedia[];
|
|
101
|
-
people?: MediaItemReference[];
|
|
102
99
|
faces?: FaceEmbedding[];
|
|
103
100
|
backups?: BackupFile[];
|
|
104
101
|
thumb?: string;
|
|
@@ -106,16 +103,73 @@ export interface IFile {
|
|
|
106
103
|
thumbsize?: number;
|
|
107
104
|
iv?: string;
|
|
108
105
|
origin?: RsLink;
|
|
109
|
-
movie?: string;
|
|
110
106
|
uploader?: string;
|
|
111
107
|
uploadkey?: string;
|
|
112
108
|
faceRecognitionError?: string;
|
|
109
|
+
relations?: Relations;
|
|
110
|
+
}
|
|
111
|
+
export interface MediaForUpdate {
|
|
112
|
+
name?: string;
|
|
113
|
+
description?: string;
|
|
114
|
+
mimetype?: string;
|
|
115
|
+
kind?: FileTypes;
|
|
116
|
+
size?: number;
|
|
117
|
+
md5?: string;
|
|
118
|
+
modified?: number;
|
|
119
|
+
created?: number;
|
|
120
|
+
width?: number;
|
|
121
|
+
height?: number;
|
|
122
|
+
orientation?: number;
|
|
123
|
+
colorSpace?: string;
|
|
124
|
+
icc?: string;
|
|
125
|
+
mp?: number;
|
|
126
|
+
vcodecs?: string[];
|
|
127
|
+
acodecs?: string[];
|
|
128
|
+
fps?: number;
|
|
129
|
+
bitrate?: number;
|
|
130
|
+
focal?: number;
|
|
131
|
+
iso?: number;
|
|
132
|
+
model?: string;
|
|
133
|
+
sspeed?: string;
|
|
134
|
+
fNumber?: number;
|
|
135
|
+
pages?: number;
|
|
136
|
+
duration?: number;
|
|
137
|
+
progress?: number;
|
|
138
|
+
addTags?: MediaItemReference[];
|
|
139
|
+
removeTags?: string[];
|
|
140
|
+
tagsLookup?: string[];
|
|
141
|
+
addSeries?: SerieInMedia[];
|
|
142
|
+
removeSeries?: SerieInMedia[];
|
|
143
|
+
seriesLookup?: string[];
|
|
144
|
+
season?: number;
|
|
145
|
+
episode?: number;
|
|
146
|
+
addPeople?: MediaItemReference[];
|
|
147
|
+
removePeople?: string[];
|
|
148
|
+
peopleLookup?: string[];
|
|
149
|
+
long?: number;
|
|
150
|
+
lat?: number;
|
|
151
|
+
gps?: string;
|
|
152
|
+
origin?: RsLink;
|
|
153
|
+
originUrl?: string;
|
|
154
|
+
ignoreOriginDuplicate?: boolean;
|
|
155
|
+
movie?: string;
|
|
156
|
+
book?: string;
|
|
157
|
+
lang?: string;
|
|
158
|
+
rating?: number;
|
|
159
|
+
thumbsize?: number;
|
|
160
|
+
iv?: string;
|
|
161
|
+
uploader?: string;
|
|
162
|
+
uploadkey?: string;
|
|
163
|
+
uploadId?: string;
|
|
164
|
+
originalHash?: string;
|
|
165
|
+
originalId?: string;
|
|
113
166
|
}
|
|
114
167
|
export declare enum LibraryTypes {
|
|
115
168
|
'Photos' = "photos",
|
|
116
169
|
'Shows' = "shows",
|
|
117
170
|
'Movies' = "movies",
|
|
118
|
-
'IPTV' = "iptv"
|
|
171
|
+
'IPTV' = "iptv",
|
|
172
|
+
'Books' = "books"
|
|
119
173
|
}
|
|
120
174
|
export declare enum LibrarySources {
|
|
121
175
|
'Path' = "PathProvider",
|
|
@@ -130,13 +184,36 @@ export declare enum LibraryRole {
|
|
|
130
184
|
share = "share",
|
|
131
185
|
admin = "admin"
|
|
132
186
|
}
|
|
187
|
+
export interface UserMapping {
|
|
188
|
+
from: string;
|
|
189
|
+
to: string;
|
|
190
|
+
}
|
|
191
|
+
export interface ServerLibrarySettings {
|
|
192
|
+
faceThreshold?: number;
|
|
193
|
+
ignoreGroups?: boolean;
|
|
194
|
+
preductionModel?: string;
|
|
195
|
+
mapProgress?: UserMapping[];
|
|
196
|
+
dataPath?: string;
|
|
197
|
+
}
|
|
198
|
+
export interface ServerLibraryForUpdate {
|
|
199
|
+
name?: string;
|
|
200
|
+
source?: LibrarySources;
|
|
201
|
+
root?: string;
|
|
202
|
+
settings?: ServerLibrarySettings;
|
|
203
|
+
credentials?: string;
|
|
204
|
+
plugin?: string;
|
|
205
|
+
}
|
|
133
206
|
export interface ILibrary {
|
|
134
207
|
id?: string;
|
|
135
208
|
name: string;
|
|
136
209
|
type: LibraryTypes;
|
|
137
210
|
source?: LibrarySources;
|
|
211
|
+
root?: string;
|
|
138
212
|
roles?: LibraryRole[];
|
|
139
213
|
crypt?: boolean;
|
|
214
|
+
settings: ServerLibrarySettings;
|
|
215
|
+
credentials?: string;
|
|
216
|
+
plugin?: string;
|
|
140
217
|
hidden?: boolean;
|
|
141
218
|
status?: string;
|
|
142
219
|
}
|
|
@@ -152,6 +229,7 @@ export interface ITag {
|
|
|
152
229
|
alt?: string[];
|
|
153
230
|
thumb?: string;
|
|
154
231
|
params?: any;
|
|
232
|
+
otherids?: string[];
|
|
155
233
|
}
|
|
156
234
|
export declare enum LinkType {
|
|
157
235
|
'profile' = "profile",
|
|
@@ -186,6 +264,7 @@ export interface IPerson {
|
|
|
186
264
|
tmdb?: number;
|
|
187
265
|
trakt?: number;
|
|
188
266
|
socials?: RsLink[];
|
|
267
|
+
otherids?: string[];
|
|
189
268
|
gender?: Gender;
|
|
190
269
|
country?: string;
|
|
191
270
|
bio?: string;
|
|
@@ -207,7 +286,7 @@ export interface ISerie {
|
|
|
207
286
|
tmdb?: number;
|
|
208
287
|
trakt?: number;
|
|
209
288
|
tvdb?: number;
|
|
210
|
-
otherids?: string;
|
|
289
|
+
otherids?: string[];
|
|
211
290
|
imdbRating?: number;
|
|
212
291
|
imdbVotes?: number;
|
|
213
292
|
traktRating?: number;
|
|
@@ -287,6 +366,32 @@ export interface IMovie {
|
|
|
287
366
|
backgroundv: number;
|
|
288
367
|
cardv: number;
|
|
289
368
|
}
|
|
369
|
+
export interface IBook {
|
|
370
|
+
id: string;
|
|
371
|
+
name: string;
|
|
372
|
+
kind?: string;
|
|
373
|
+
serieRef?: string;
|
|
374
|
+
volume?: number;
|
|
375
|
+
chapter?: number;
|
|
376
|
+
year?: number;
|
|
377
|
+
airdate?: number;
|
|
378
|
+
overview?: string;
|
|
379
|
+
pages?: number;
|
|
380
|
+
params?: any;
|
|
381
|
+
lang?: string;
|
|
382
|
+
original?: string;
|
|
383
|
+
isbn13?: string;
|
|
384
|
+
openlibraryEditionId?: string;
|
|
385
|
+
openlibraryWorkId?: string;
|
|
386
|
+
googleBooksVolumeId?: string;
|
|
387
|
+
asin?: string;
|
|
388
|
+
modified: number;
|
|
389
|
+
added: number;
|
|
390
|
+
posterv?: number;
|
|
391
|
+
backgroundv?: number;
|
|
392
|
+
cardv?: number;
|
|
393
|
+
}
|
|
394
|
+
export type BookSort = 'modified' | 'added' | 'name' | 'year';
|
|
290
395
|
export interface IServer {
|
|
291
396
|
id: string;
|
|
292
397
|
name?: string;
|
|
@@ -338,8 +443,58 @@ export interface IBackupFile {
|
|
|
338
443
|
error?: string;
|
|
339
444
|
}
|
|
340
445
|
export interface ExternalImage {
|
|
341
|
-
|
|
342
|
-
|
|
446
|
+
type?: string;
|
|
447
|
+
url: RsRequest;
|
|
448
|
+
aspectRatio?: number;
|
|
449
|
+
height?: number;
|
|
450
|
+
lang?: string;
|
|
451
|
+
voteAverage?: number;
|
|
452
|
+
voteCount?: number;
|
|
453
|
+
width?: number;
|
|
454
|
+
}
|
|
455
|
+
export interface Relations {
|
|
456
|
+
peopleDetails?: IPerson[];
|
|
457
|
+
tagsDetails?: ITag[];
|
|
458
|
+
people?: MediaItemReference[];
|
|
459
|
+
tags?: MediaItemReference[];
|
|
460
|
+
series?: SerieInMedia[];
|
|
461
|
+
seriesDetails?: ISerie[];
|
|
462
|
+
movies?: string[];
|
|
463
|
+
moviesDetails?: IMovie[];
|
|
464
|
+
books?: string[];
|
|
465
|
+
booksDetails?: IBook[];
|
|
466
|
+
extImages?: ExternalImage[];
|
|
467
|
+
}
|
|
468
|
+
export type ItemWithRelations<T> = T & {
|
|
469
|
+
relations?: Relations;
|
|
470
|
+
};
|
|
471
|
+
export interface SearchRelations {
|
|
472
|
+
peopleDetails?: IPerson[];
|
|
473
|
+
tagsDetails?: ITag[];
|
|
474
|
+
people?: MediaItemReference[];
|
|
475
|
+
tags?: MediaItemReference[];
|
|
476
|
+
extImages?: ExternalImage[];
|
|
477
|
+
}
|
|
478
|
+
export interface SearchStreamResultBase {
|
|
479
|
+
relations?: SearchRelations;
|
|
480
|
+
images?: ExternalImage[];
|
|
481
|
+
}
|
|
482
|
+
export type SearchStreamResult<K extends string, T> = SearchStreamResultBase & {
|
|
483
|
+
metadata?: Partial<Record<K, T>>;
|
|
484
|
+
};
|
|
485
|
+
export type SearchResult<K extends string, T> = SearchStreamResult<K, T>;
|
|
486
|
+
export type BookSearchResult = SearchResult<'book', IBook>;
|
|
487
|
+
export type MovieSearchResult = SearchResult<'movie', IMovie>;
|
|
488
|
+
export type SerieSearchResult = SearchResult<'serie', ISerie>;
|
|
489
|
+
export type BookSearchStreamResult = SearchStreamResult<'book', IBook>;
|
|
490
|
+
export type MovieSearchStreamResult = SearchStreamResult<'movie', IMovie>;
|
|
491
|
+
export type SerieSearchStreamResult = SearchStreamResult<'serie', ISerie>;
|
|
492
|
+
export type LookupSearchStreamResult = SearchStreamResult<'book' | 'movie' | 'serie', IBook | IMovie | ISerie>;
|
|
493
|
+
export type GroupedSearchStreamPayload<T> = Record<string, T[]>;
|
|
494
|
+
export interface SearchStreamCallbacks<T> {
|
|
495
|
+
onResults?: (results: GroupedSearchStreamPayload<T>) => void;
|
|
496
|
+
onFinished?: () => void;
|
|
497
|
+
onError?: (error: unknown) => void;
|
|
343
498
|
}
|
|
344
499
|
export interface IChannel {
|
|
345
500
|
id: string;
|
|
@@ -748,6 +903,8 @@ export interface RsRequest {
|
|
|
748
903
|
albums?: string[];
|
|
749
904
|
season?: number;
|
|
750
905
|
episode?: number;
|
|
906
|
+
movie?: string;
|
|
907
|
+
book?: SerieInMedia;
|
|
751
908
|
language?: string;
|
|
752
909
|
resolution?: string;
|
|
753
910
|
videoFormat?: string;
|
|
@@ -769,6 +926,7 @@ export interface RsGroupDownload {
|
|
|
769
926
|
groupFilename?: string;
|
|
770
927
|
groupMime?: string;
|
|
771
928
|
requests: RsRequest[];
|
|
929
|
+
infos?: MediaForUpdate;
|
|
772
930
|
}
|
|
773
931
|
/**
|
|
774
932
|
* Request processing status from plugin-based download/processing.
|