@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/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
- const idToken = await this.getIdToken();
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
- // ==================== SSE Methods ====================
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
- if (this.sseReconnectTimeout) {
306
- clearTimeout(this.sseReconnectTimeout);
307
- this.sseReconnectTimeout = undefined;
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
- this.sseAbortController = new AbortController();
474
+ const abortController = new AbortController();
475
+ this.sseAbortController = abortController;
347
476
  console.log("SSSEEEE URL", url);
348
- const response = await fetch(url, {
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: this.sseAbortController.signal
484
+ signal: abortController.signal
356
485
  });
357
486
  if (!response.ok) {
358
487
  if (response.status === 401) {
359
- throw Object.assign(new Error('Authentication failed'), { type: 'auth' });
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.scheduleReconnect();
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.sseOptions?.autoReconnect) {
734
+ if (!this.shouldAttemptReconnect()) {
735
+ return;
736
+ }
737
+ const options = this.sseOptions;
738
+ if (!options) {
523
739
  return;
524
740
  }
525
- if (this.sseOptions.maxReconnectAttempts !== undefined &&
526
- this.sseReconnectAttempts >= this.sseOptions.maxReconnectAttempts) {
741
+ if (options.maxReconnectAttempts !== undefined &&
742
+ this.sseReconnectAttempts >= options.maxReconnectAttempts) {
527
743
  return;
528
744
  }
529
- const initialDelay = this.sseOptions.initialReconnectDelay ?? 1000;
530
- const maxDelay = this.sseOptions.maxReconnectDelay ?? 30000;
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(async () => {
538
- // Refresh token before reconnecting
539
- try {
540
- await this.refreshToken();
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
- this._connectSSE();
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
  }
@@ -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
- url: string;
342
- type: string;
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.