@redseat/api 0.2.8 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client.md CHANGED
@@ -458,13 +458,15 @@ client.medias$.subscribe(event => {
458
458
  });
459
459
  ```
460
460
 
461
- #### `mediasProgress$: Observable<SSEMediasProgressEvent>`
461
+ #### `uploadProgress$: Observable<SSEUploadProgressEvent>`
462
462
 
463
- Emits media processing progress updates.
463
+ Emits upload progress updates including download, transfer, and analysis stages.
464
464
 
465
465
  ```typescript
466
- client.mediasProgress$.subscribe(event => {
467
- console.log(`Media ${event.mediaId}: ${event.progress}%`);
466
+ client.uploadProgress$.subscribe(event => {
467
+ const { progress } = event;
468
+ const percent = progress.total ? Math.round((progress.current ?? 0) / progress.total * 100) : 0;
469
+ console.log(`Upload ${progress.id} (${progress.type}): ${percent}% - ${progress.filename}`);
468
470
  });
469
471
  ```
470
472
 
@@ -558,6 +560,60 @@ client.backupFiles$.subscribe(event => {
558
560
  });
559
561
  ```
560
562
 
563
+ #### `mediaRating$: Observable<SSEMediaRatingEvent>`
564
+
565
+ Emits when a user rates a media item.
566
+
567
+ ```typescript
568
+ client.mediaRating$.subscribe(event => {
569
+ console.log(`User ${event.rating.userRef} rated media ${event.rating.mediaRef}: ${event.rating.rating}`);
570
+ });
571
+ ```
572
+
573
+ **Event structure:**
574
+ - `library`: Library ID where the media is located
575
+ - `rating.userRef`: User who rated
576
+ - `rating.mediaRef`: Media that was rated
577
+ - `rating.rating`: Rating value (0-5)
578
+ - `rating.modified`: Timestamp of the rating
579
+
580
+ #### `mediaProgress$: Observable<SSEMediaProgressEvent>`
581
+
582
+ Emits when a user's playback progress is updated.
583
+
584
+ ```typescript
585
+ client.mediaProgress$.subscribe(event => {
586
+ console.log(`User ${event.progress.userRef} watched ${event.progress.mediaRef} to ${event.progress.progress}ms`);
587
+ });
588
+ ```
589
+
590
+ **Event structure:**
591
+ - `library`: Library ID where the media is located
592
+ - `progress.userRef`: User whose progress updated
593
+ - `progress.mediaRef`: Media being tracked
594
+ - `progress.progress`: Current playback position in milliseconds
595
+ - `progress.modified`: Timestamp of the update
596
+
597
+ #### `playersList$: Observable<SSEPlayersListEvent>`
598
+
599
+ Emits the full list of available media players for the user when it changes.
600
+
601
+ ```typescript
602
+ client.playersList$.subscribe(event => {
603
+ console.log(`Available players for ${event.userRef}:`);
604
+ for (const player of event.players) {
605
+ console.log(` - ${player.name} (${player.player})`);
606
+ }
607
+ });
608
+ ```
609
+
610
+ **Event structure:**
611
+ - `userRef`: User ID
612
+ - `players`: Array of available players
613
+ - `id`: Player socket ID (for casting)
614
+ - `name`: Player display name
615
+ - `player`: Player type identifier
616
+
561
617
  ### Complete SSE Example
562
618
 
563
619
  ```typescript
package/dist/client.d.ts CHANGED
@@ -2,7 +2,7 @@ import { Method, AxiosRequestConfig } from 'axios';
2
2
  import { Observable } from 'rxjs';
3
3
  import { IToken } from './auth.js';
4
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';
5
+ import { SSEConnectionState, SSEConnectionOptions, SSEConnectionError, SSELibraryEvent, SSELibraryStatusEvent, SSEMediasEvent, SSEUploadProgressEvent, SSEConvertProgressEvent, SSEEpisodesEvent, SSESeriesEvent, SSEMoviesEvent, SSEPeopleEvent, SSETagsEvent, SSEBackupsEvent, SSEBackupFilesEvent, SSEMediaRatingEvent, SSEMediaProgressEvent, SSEPlayersListEvent } from './sse-types.js';
6
6
  export interface ClientOptions {
7
7
  server: IServer;
8
8
  getIdToken: () => Promise<string>;
@@ -33,7 +33,7 @@ export declare class RedseatClient {
33
33
  readonly library$: Observable<SSELibraryEvent>;
34
34
  readonly libraryStatus$: Observable<SSELibraryStatusEvent>;
35
35
  readonly medias$: Observable<SSEMediasEvent>;
36
- readonly mediasProgress$: Observable<SSEMediasProgressEvent>;
36
+ readonly uploadProgress$: Observable<SSEUploadProgressEvent>;
37
37
  readonly convertProgress$: Observable<SSEConvertProgressEvent>;
38
38
  readonly episodes$: Observable<SSEEpisodesEvent>;
39
39
  readonly series$: Observable<SSESeriesEvent>;
@@ -42,8 +42,12 @@ export declare class RedseatClient {
42
42
  readonly tags$: Observable<SSETagsEvent>;
43
43
  readonly backups$: Observable<SSEBackupsEvent>;
44
44
  readonly backupFiles$: Observable<SSEBackupFilesEvent>;
45
+ readonly mediaRating$: Observable<SSEMediaRatingEvent>;
46
+ readonly mediaProgress$: Observable<SSEMediaProgressEvent>;
47
+ readonly playersList$: Observable<SSEPlayersListEvent>;
45
48
  /**
46
- * Creates a typed observable for a specific SSE event type
49
+ * Creates a typed observable for a specific SSE event type.
50
+ * Unwraps the nested data structure from the server (e.g., {uploadProgress: {...}} -> {...})
47
51
  */
48
52
  private createEventStream;
49
53
  constructor(options: ClientOptions);
package/dist/client.js CHANGED
@@ -3,10 +3,31 @@ import { BehaviorSubject, Subject, filter, map } from 'rxjs';
3
3
  import { fetchServerToken } from './auth.js';
4
4
  export class RedseatClient {
5
5
  /**
6
- * Creates a typed observable for a specific SSE event type
6
+ * Creates a typed observable for a specific SSE event type.
7
+ * Unwraps the nested data structure from the server (e.g., {uploadProgress: {...}} -> {...})
7
8
  */
8
9
  createEventStream(eventName) {
9
- return this._sseEvents.pipe(filter((event) => event.event === eventName), map(event => event.data));
10
+ // Map event names to their wrapper property names (snake_case -> camelCase)
11
+ const wrapperMap = {
12
+ 'medias': 'medias',
13
+ 'upload_progress': 'uploadProgress',
14
+ 'convert_progress': 'convertProgress',
15
+ 'media_progress': 'mediaProgress',
16
+ 'media_rating': 'mediaRating',
17
+ 'library-status': 'libraryStatus',
18
+ 'backups-files': 'backupsFiles',
19
+ 'players-list': 'Players',
20
+ };
21
+ const wrapperKey = wrapperMap[eventName];
22
+ return this._sseEvents.pipe(filter((event) => event.event === eventName), map(event => {
23
+ //console.log("EVENT", event)
24
+ // If there's a wrapper, unwrap it; otherwise return data as-is
25
+ const data = event.data;
26
+ if (wrapperKey && data && typeof data === 'object' && wrapperKey in data) {
27
+ return data[wrapperKey];
28
+ }
29
+ return event.data;
30
+ }));
10
31
  }
11
32
  constructor(options) {
12
33
  this.sseReconnectAttempts = 0;
@@ -22,7 +43,7 @@ export class RedseatClient {
22
43
  this.library$ = this.createEventStream('library');
23
44
  this.libraryStatus$ = this.createEventStream('library-status');
24
45
  this.medias$ = this.createEventStream('medias');
25
- this.mediasProgress$ = this.createEventStream('medias_progress');
46
+ this.uploadProgress$ = this.createEventStream('upload_progress');
26
47
  this.convertProgress$ = this.createEventStream('convert_progress');
27
48
  this.episodes$ = this.createEventStream('episodes');
28
49
  this.series$ = this.createEventStream('series');
@@ -31,6 +52,9 @@ export class RedseatClient {
31
52
  this.tags$ = this.createEventStream('tags');
32
53
  this.backups$ = this.createEventStream('backups');
33
54
  this.backupFiles$ = this.createEventStream('backups-files');
55
+ this.mediaRating$ = this.createEventStream('media_rating');
56
+ this.mediaProgress$ = this.createEventStream('media_progress');
57
+ this.playersList$ = this.createEventStream('players-list');
34
58
  this.server = options.server;
35
59
  this.redseatUrl = options.redseatUrl;
36
60
  this.getIdToken = options.getIdToken;
@@ -303,6 +327,7 @@ export class RedseatClient {
303
327
  }
304
328
  const url = this.buildSSEUrl();
305
329
  this.sseAbortController = new AbortController();
330
+ console.log("SSSEEEE URL", url);
306
331
  const response = await fetch(url, {
307
332
  method: 'GET',
308
333
  headers: {
@@ -323,8 +348,12 @@ export class RedseatClient {
323
348
  }
324
349
  this._sseConnectionState.next('connected');
325
350
  this.sseReconnectAttempts = 0;
326
- // Process the stream
327
- await this.processSSEStream(response.body);
351
+ // Process the stream in the background (don't await - it runs forever until disconnected)
352
+ this.processSSEStream(response.body).catch(err => {
353
+ if (err?.name !== 'AbortError') {
354
+ this.handleSSEError(err);
355
+ }
356
+ });
328
357
  }
329
358
  catch (error) {
330
359
  if (error instanceof Error && error.name === 'AbortError') {
@@ -366,6 +395,7 @@ export class RedseatClient {
366
395
  const { events, remainingBuffer } = this.parseSSEBuffer(buffer);
367
396
  buffer = remainingBuffer;
368
397
  for (const event of events) {
398
+ //console.log("event process", JSON.stringify(event))
369
399
  this._sseEvents.next(event);
370
400
  }
371
401
  }
@@ -414,6 +414,52 @@ export interface ClusterFacesResponse {
414
414
  export interface IChannelUpdate {
415
415
  [key: string]: any;
416
416
  }
417
+ export declare enum PluginAuthType {
418
+ OAUTH = "oauth",
419
+ URL = "url",
420
+ Token = "token",
421
+ LoginPassword = "password"
422
+ }
423
+ export interface IPluginParam {
424
+ name: string;
425
+ param: Record<string, any>;
426
+ description?: string;
427
+ required?: boolean;
428
+ }
429
+ export interface IPlugin {
430
+ id: string;
431
+ name: string;
432
+ libraries: string[];
433
+ description: string;
434
+ credential: string;
435
+ credentialType: {
436
+ type: PluginAuthType;
437
+ url?: string;
438
+ };
439
+ params?: IPluginParam[];
440
+ oauthUrl?: string;
441
+ repo?: string;
442
+ publisher?: string;
443
+ version?: number;
444
+ installed: boolean;
445
+ capabilities?: string[];
446
+ local?: boolean;
447
+ }
448
+ export interface ICredential {
449
+ id?: string;
450
+ name: string;
451
+ source: string;
452
+ type: {
453
+ type: PluginAuthType;
454
+ url?: string;
455
+ };
456
+ login?: string;
457
+ password?: string;
458
+ settings: Record<string, any>;
459
+ user_ref?: string;
460
+ refreshtoken?: string;
461
+ expires?: number;
462
+ }
417
463
  export declare enum ElementType {
418
464
  Tag = "tag",
419
465
  Person = "person",
@@ -499,6 +545,7 @@ export interface RsRequest {
499
545
  filename?: string;
500
546
  status: RsRequestStatus;
501
547
  permanent: boolean;
548
+ instant?: boolean;
502
549
  jsonBody?: any;
503
550
  method: RsRequestMethod;
504
551
  referer?: string;
@@ -36,6 +36,13 @@ export var LinkType;
36
36
  LinkType["post"] = "post";
37
37
  LinkType["other"] = "other";
38
38
  })(LinkType || (LinkType = {}));
39
+ export var PluginAuthType;
40
+ (function (PluginAuthType) {
41
+ PluginAuthType["OAUTH"] = "oauth";
42
+ PluginAuthType["URL"] = "url";
43
+ PluginAuthType["Token"] = "token";
44
+ PluginAuthType["LoginPassword"] = "password";
45
+ })(PluginAuthType || (PluginAuthType = {}));
39
46
  export var ElementType;
40
47
  (function (ElementType) {
41
48
  ElementType["Tag"] = "tag";
package/dist/library.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Observable } from 'rxjs';
2
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';
3
+ import { SSEMediasEvent, SSEUploadProgressEvent, SSEConvertProgressEvent, SSEEpisodesEvent, SSESeriesEvent, SSEMoviesEvent, SSEPeopleEvent, SSETagsEvent, SSELibraryStatusEvent, SSEMediaRatingEvent, SSEMediaProgressEvent } from './sse-types.js';
4
4
  import { EncryptFileOptions, EncryptedFile } from './encryption.js';
5
5
  export interface MediaForUpdate {
6
6
  name?: string;
@@ -77,7 +77,7 @@ export interface LibraryHttpClient {
77
77
  getFullUrl(path: string, params?: Record<string, string>): string;
78
78
  getAuthToken(): string;
79
79
  readonly medias$?: Observable<SSEMediasEvent>;
80
- readonly mediasProgress$?: Observable<SSEMediasProgressEvent>;
80
+ readonly uploadProgress$?: Observable<SSEUploadProgressEvent>;
81
81
  readonly convertProgress$?: Observable<SSEConvertProgressEvent>;
82
82
  readonly episodes$?: Observable<SSEEpisodesEvent>;
83
83
  readonly series$?: Observable<SSESeriesEvent>;
@@ -85,6 +85,8 @@ export interface LibraryHttpClient {
85
85
  readonly people$?: Observable<SSEPeopleEvent>;
86
86
  readonly tags$?: Observable<SSETagsEvent>;
87
87
  readonly libraryStatus$?: Observable<SSELibraryStatusEvent>;
88
+ readonly mediaRating$?: Observable<SSEMediaRatingEvent>;
89
+ readonly mediaProgress$?: Observable<SSEMediaProgressEvent>;
88
90
  }
89
91
  export declare class LibraryApi {
90
92
  private client;
@@ -94,7 +96,7 @@ export declare class LibraryApi {
94
96
  private keyText?;
95
97
  private disposed;
96
98
  readonly medias$: Observable<SSEMediasEvent>;
97
- readonly mediasProgress$: Observable<SSEMediasProgressEvent>;
99
+ readonly uploadProgress$: Observable<SSEUploadProgressEvent>;
98
100
  readonly convertProgress$: Observable<SSEConvertProgressEvent>;
99
101
  readonly episodes$: Observable<SSEEpisodesEvent>;
100
102
  readonly series$: Observable<SSESeriesEvent>;
@@ -102,6 +104,8 @@ export declare class LibraryApi {
102
104
  readonly people$: Observable<SSEPeopleEvent>;
103
105
  readonly tags$: Observable<SSETagsEvent>;
104
106
  readonly libraryStatus$: Observable<SSELibraryStatusEvent>;
107
+ readonly mediaRating$: Observable<SSEMediaRatingEvent>;
108
+ readonly mediaProgress$: Observable<SSEMediaProgressEvent>;
105
109
  constructor(client: LibraryHttpClient, libraryId: string, library: ILibrary);
106
110
  /**
107
111
  * Creates a library-filtered stream from a client stream.
@@ -235,6 +239,14 @@ export declare class LibraryApi {
235
239
  * @throws Error if the URL is not available or cannot be made permanent
236
240
  */
237
241
  checkRequestPermanent(request: RsRequest): Promise<RsRequest>;
242
+ /**
243
+ * Checks if a request can be processed instantly (immediate availability).
244
+ * @param request - The request to check for instant status
245
+ * @returns Object with instant boolean indicating if the request can be processed immediately
246
+ */
247
+ checkRequestInstant(request: RsRequest): Promise<{
248
+ instant: boolean;
249
+ }>;
238
250
  /**
239
251
  * Get a share token for a request URL.
240
252
  * The token can be used to stream/download the resource without authentication.
package/dist/library.js CHANGED
@@ -9,7 +9,7 @@ export class LibraryApi {
9
9
  this.library = library;
10
10
  // Create library-filtered streams
11
11
  this.medias$ = this.createLibraryFilteredStream(client.medias$);
12
- this.mediasProgress$ = this.createLibraryFilteredStream(client.mediasProgress$);
12
+ this.uploadProgress$ = this.createLibraryFilteredStream(client.uploadProgress$);
13
13
  this.convertProgress$ = this.createLibraryFilteredStream(client.convertProgress$);
14
14
  this.episodes$ = this.createLibraryFilteredStream(client.episodes$);
15
15
  this.series$ = this.createLibraryFilteredStream(client.series$);
@@ -17,6 +17,8 @@ export class LibraryApi {
17
17
  this.people$ = this.createLibraryFilteredStream(client.people$);
18
18
  this.tags$ = this.createLibraryFilteredStream(client.tags$);
19
19
  this.libraryStatus$ = this.createLibraryFilteredStream(client.libraryStatus$);
20
+ this.mediaRating$ = this.createLibraryFilteredStream(client.mediaRating$);
21
+ this.mediaProgress$ = this.createLibraryFilteredStream(client.mediaProgress$);
20
22
  }
21
23
  /**
22
24
  * Creates a library-filtered stream from a client stream.
@@ -492,6 +494,15 @@ export class LibraryApi {
492
494
  const res = await this.client.post(this.getUrl('/plugins/requests/permanent'), request);
493
495
  return res.data;
494
496
  }
497
+ /**
498
+ * Checks if a request can be processed instantly (immediate availability).
499
+ * @param request - The request to check for instant status
500
+ * @returns Object with instant boolean indicating if the request can be processed immediately
501
+ */
502
+ async checkRequestInstant(request) {
503
+ const res = await this.client.post(this.getUrl('/plugins/requests/check-instant'), request);
504
+ return res.data;
505
+ }
495
506
  /**
496
507
  * Get a share token for a request URL.
497
508
  * The token can be used to stream/download the resource without authentication.
package/dist/server.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { RedseatClient } from './client.js';
2
- import { ILibrary } from './interfaces.js';
2
+ import { ILibrary, IPlugin, ICredential } from './interfaces.js';
3
3
  export declare class ServerApi {
4
4
  private client;
5
5
  constructor(client: RedseatClient);
@@ -12,7 +12,11 @@ export declare class ServerApi {
12
12
  }): Promise<any>;
13
13
  addLibrary(library: Partial<ILibrary>): Promise<ILibrary>;
14
14
  getSetting(key: string): Promise<string>;
15
- getPlugins(): Promise<any[]>;
16
- getCredentials(): Promise<any[]>;
15
+ getPlugins(): Promise<IPlugin[]>;
16
+ getCredentials(): Promise<ICredential[]>;
17
+ saveCredential(credential: ICredential): Promise<ICredential>;
18
+ updateCredential(credential: ICredential): Promise<ICredential>;
19
+ deleteCredential(id: string): Promise<void>;
20
+ saveOAuthCredentials(pluginId: string, params: Record<string, string>): Promise<void>;
17
21
  addLibraryCredential(credential: any): Promise<ILibrary>;
18
22
  }
package/dist/server.js CHANGED
@@ -30,6 +30,20 @@ export class ServerApi {
30
30
  const res = await this.client.get('/credentials');
31
31
  return res.data;
32
32
  }
33
+ async saveCredential(credential) {
34
+ const res = await this.client.post('/credentials', credential);
35
+ return res.data;
36
+ }
37
+ async updateCredential(credential) {
38
+ const res = await this.client.patch(`/credentials/${credential.id}`, credential);
39
+ return res.data;
40
+ }
41
+ async deleteCredential(id) {
42
+ await this.client.delete(`/credentials/${id}`);
43
+ }
44
+ async saveOAuthCredentials(pluginId, params) {
45
+ await this.client.post(`/plugins/${pluginId}/oauthtoken`, params);
46
+ }
33
47
  async addLibraryCredential(credential) {
34
48
  const res = await this.client.post('/libraries/credential', credential);
35
49
  return res.data;
@@ -35,11 +35,20 @@ export interface SSEMediasEvent {
35
35
  media: IFile;
36
36
  }[];
37
37
  }
38
- export interface SSEMediasProgressEvent {
38
+ export type RsProgressType = 'download' | 'transfert' | 'analysing' | 'finished' | {
39
+ duplicate: string;
40
+ };
41
+ export interface RsProgress {
42
+ id: string;
43
+ total?: number;
44
+ current?: number;
45
+ filename?: string;
46
+ type: RsProgressType;
47
+ }
48
+ export interface SSEUploadProgressEvent {
39
49
  library: string;
40
- mediaId: string;
41
- progress: number;
42
- status?: string;
50
+ progress: RsProgress;
51
+ remainingSecondes?: number;
43
52
  }
44
53
  export interface SSEConvertProgressEvent {
45
54
  library: string;
@@ -98,11 +107,38 @@ export interface SSEBackupFilesEvent {
98
107
  status?: string;
99
108
  message?: string;
100
109
  }
110
+ export interface SSEMediaRatingEvent {
111
+ library: string;
112
+ rating: {
113
+ userRef: string;
114
+ mediaRef: string;
115
+ rating: number;
116
+ modified: number;
117
+ };
118
+ }
119
+ export interface SSEMediaProgressEvent {
120
+ library: string;
121
+ progress: {
122
+ userRef: string;
123
+ mediaRef: string;
124
+ progress: number;
125
+ modified: number;
126
+ };
127
+ }
128
+ export interface SSEPlayerEvent {
129
+ id: string;
130
+ name: string;
131
+ player: string;
132
+ }
133
+ export interface SSEPlayersListEvent {
134
+ userRef: string;
135
+ players: SSEPlayerEvent[];
136
+ }
101
137
  export interface SSEEventMap {
102
138
  'library': SSELibraryEvent;
103
139
  'library-status': SSELibraryStatusEvent;
104
140
  'medias': SSEMediasEvent;
105
- 'medias_progress': SSEMediasProgressEvent;
141
+ 'upload_progress': SSEUploadProgressEvent;
106
142
  'convert_progress': SSEConvertProgressEvent;
107
143
  'episodes': SSEEpisodesEvent;
108
144
  'series': SSESeriesEvent;
@@ -111,6 +147,9 @@ export interface SSEEventMap {
111
147
  'tags': SSETagsEvent;
112
148
  'backups': SSEBackupsEvent;
113
149
  'backups-files': SSEBackupFilesEvent;
150
+ 'media_rating': SSEMediaRatingEvent;
151
+ 'media_progress': SSEMediaProgressEvent;
152
+ 'players-list': SSEPlayersListEvent;
114
153
  }
115
154
  export type SSEEventName = keyof SSEEventMap;
116
155
  export interface SSEEvent<T extends SSEEventName = SSEEventName> {
package/libraries.md CHANGED
@@ -98,13 +98,15 @@ libraryApi.medias$.subscribe(event => {
98
98
  });
99
99
  ```
100
100
 
101
- #### `mediasProgress$: Observable<SSEMediasProgressEvent>`
101
+ #### `uploadProgress$: Observable<SSEUploadProgressEvent>`
102
102
 
103
- Emits media processing progress updates for this library.
103
+ Emits upload progress updates for this library, including download, transfer, and analysis stages.
104
104
 
105
105
  ```typescript
106
- libraryApi.mediasProgress$.subscribe(event => {
107
- console.log(`Media ${event.mediaId}: ${event.progress}%`);
106
+ libraryApi.uploadProgress$.subscribe(event => {
107
+ const { progress } = event;
108
+ const percent = progress.total ? Math.round((progress.current ?? 0) / progress.total * 100) : 0;
109
+ console.log(`Upload ${progress.id} (${progress.type}): ${percent}% - ${progress.filename}`);
108
110
  });
109
111
  ```
110
112
 
@@ -188,6 +190,40 @@ libraryApi.libraryStatus$.subscribe(event => {
188
190
  });
189
191
  ```
190
192
 
193
+ #### `mediaRating$: Observable<SSEMediaRatingEvent>`
194
+
195
+ Emits when a user rates a media item in this library.
196
+
197
+ ```typescript
198
+ libraryApi.mediaRating$.subscribe(event => {
199
+ console.log(`Rating for ${event.rating.mediaRef}: ${event.rating.rating}`);
200
+ });
201
+ ```
202
+
203
+ **Event structure:**
204
+ - `library`: Library ID
205
+ - `rating.userRef`: User who rated
206
+ - `rating.mediaRef`: Media that was rated
207
+ - `rating.rating`: Rating value (0-5)
208
+ - `rating.modified`: Timestamp of the rating
209
+
210
+ #### `mediaProgress$: Observable<SSEMediaProgressEvent>`
211
+
212
+ Emits when a user's playback progress is updated for a media item in this library.
213
+
214
+ ```typescript
215
+ libraryApi.mediaProgress$.subscribe(event => {
216
+ console.log(`Progress for ${event.progress.mediaRef}: ${event.progress.progress}ms`);
217
+ });
218
+ ```
219
+
220
+ **Event structure:**
221
+ - `library`: Library ID
222
+ - `progress.userRef`: User whose progress updated
223
+ - `progress.mediaRef`: Media being tracked
224
+ - `progress.progress`: Current playback position in milliseconds
225
+ - `progress.modified`: Timestamp of the update
226
+
191
227
  ### Complete SSE Example with LibraryApi
192
228
 
193
229
  ```typescript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redseat/api",
3
- "version": "0.2.8",
3
+ "version": "0.3.5",
4
4
  "description": "TypeScript API client library for interacting with Redseat servers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/server.md CHANGED
@@ -109,17 +109,98 @@ plugins.forEach(plugin => {
109
109
  });
110
110
  ```
111
111
 
112
- ### `getCredentials(): Promise<any[]>`
112
+ ### `getCredentials(): Promise<ICredential[]>`
113
113
 
114
114
  Retrieves all available credentials configured on the server.
115
115
 
116
- **Returns:** Promise resolving to an array of credential objects
116
+ **Returns:** Promise resolving to an array of `ICredential` objects
117
117
 
118
118
  **Example:**
119
119
  ```typescript
120
120
  const credentials = await serverApi.getCredentials();
121
121
  credentials.forEach(cred => {
122
- console.log(`Credential: ${cred.name} (${cred.type})`);
122
+ console.log(`Credential: ${cred.name} (${cred.type.type})`);
123
+ });
124
+ ```
125
+
126
+ ### `saveCredential(credential: ICredential): Promise<ICredential>`
127
+
128
+ Creates a new credential on the server.
129
+
130
+ **Parameters:**
131
+ - `credential`: Credential object with required fields:
132
+ - `name`: Credential name (required)
133
+ - `source`: Plugin/source name (required)
134
+ - `type`: Authentication type object (required)
135
+ - `settings`: Settings record for plugin params (required)
136
+ - `login`: Optional login/username
137
+ - `password`: Optional password/token
138
+
139
+ **Returns:** Promise resolving to the created `ICredential` object with generated `id`
140
+
141
+ **Example:**
142
+ ```typescript
143
+ const newCredential = await serverApi.saveCredential({
144
+ name: 'My API Key',
145
+ source: 'jackett',
146
+ type: { type: PluginAuthType.Token },
147
+ settings: { url: 'http://localhost:9117' },
148
+ password: 'my-api-key'
149
+ });
150
+ console.log(`Created credential with ID: ${newCredential.id}`);
151
+ ```
152
+
153
+ ### `updateCredential(credential: ICredential): Promise<ICredential>`
154
+
155
+ Updates an existing credential.
156
+
157
+ **Parameters:**
158
+ - `credential`: Credential object with `id` and fields to update
159
+
160
+ **Returns:** Promise resolving to the updated `ICredential` object
161
+
162
+ **Example:**
163
+ ```typescript
164
+ const updated = await serverApi.updateCredential({
165
+ id: 'cred-123',
166
+ name: 'Updated Name',
167
+ source: 'jackett',
168
+ type: { type: PluginAuthType.Token },
169
+ settings: { url: 'http://localhost:9117' },
170
+ password: 'new-api-key'
171
+ });
172
+ ```
173
+
174
+ ### `deleteCredential(id: string): Promise<void>`
175
+
176
+ Deletes a credential by ID.
177
+
178
+ **Parameters:**
179
+ - `id`: The credential ID to delete
180
+
181
+ **Returns:** Promise resolving when deletion is complete
182
+
183
+ **Example:**
184
+ ```typescript
185
+ await serverApi.deleteCredential('cred-123');
186
+ ```
187
+
188
+ ### `saveOAuthCredentials(pluginId: string, params: Record<string, string>): Promise<void>`
189
+
190
+ Exchanges OAuth tokens and saves credentials for a plugin.
191
+
192
+ **Parameters:**
193
+ - `pluginId`: The plugin ID to save credentials for
194
+ - `params`: OAuth parameters including tokens and name
195
+
196
+ **Returns:** Promise resolving when credentials are saved
197
+
198
+ **Example:**
199
+ ```typescript
200
+ await serverApi.saveOAuthCredentials('trakt', {
201
+ name: 'My Trakt Account',
202
+ code: 'oauth-code',
203
+ access_token: 'token123'
123
204
  });
124
205
  ```
125
206