@redseat/api 0.4.6 → 0.5.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.
@@ -202,6 +202,7 @@ export interface ServerLibraryForUpdate {
202
202
  settings?: ServerLibrarySettings;
203
203
  credentials?: string;
204
204
  plugin?: string;
205
+ password?: string;
205
206
  }
206
207
  export interface ILibrary {
207
208
  id?: string;
@@ -216,6 +217,7 @@ export interface ILibrary {
216
217
  plugin?: string;
217
218
  hidden?: boolean;
218
219
  status?: string;
220
+ password?: string;
219
221
  }
220
222
  export interface ITag {
221
223
  id: string;
@@ -451,6 +453,7 @@ export interface ExternalImage {
451
453
  voteAverage?: number;
452
454
  voteCount?: number;
453
455
  width?: number;
456
+ matchType?: RsLookupMatchType;
454
457
  }
455
458
  export interface Relations {
456
459
  peopleDetails?: IPerson[];
@@ -478,6 +481,7 @@ export interface SearchRelations {
478
481
  export interface SearchStreamResultBase {
479
482
  relations?: SearchRelations;
480
483
  images?: ExternalImage[];
484
+ matchType?: RsLookupMatchType;
481
485
  }
482
486
  export type SearchStreamResult<K extends string, T> = SearchStreamResultBase & {
483
487
  metadata?: Partial<Record<K, T>>;
@@ -490,9 +494,15 @@ export type BookSearchStreamResult = SearchStreamResult<'book', IBook>;
490
494
  export type MovieSearchStreamResult = SearchStreamResult<'movie', IMovie>;
491
495
  export type SerieSearchStreamResult = SearchStreamResult<'serie', ISerie>;
492
496
  export type LookupSearchStreamResult = SearchStreamResult<'book' | 'movie' | 'serie', IBook | IMovie | ISerie>;
493
- export type GroupedSearchStreamPayload<T> = Record<string, T[]>;
497
+ export type RsLookupMatchType = 'exactId' | 'exactText';
498
+ export interface SseSearchStreamEvent<T> {
499
+ sourceId: string;
500
+ sourceName: string;
501
+ results: T[];
502
+ nextPageKey?: string | null;
503
+ }
494
504
  export interface SearchStreamCallbacks<T> {
495
- onResults?: (results: GroupedSearchStreamPayload<T>) => void;
505
+ onResults?: (event: SseSearchStreamEvent<T>) => void;
496
506
  onFinished?: () => void;
497
507
  onError?: (error: unknown) => void;
498
508
  }
@@ -927,6 +937,7 @@ export interface RsGroupDownload {
927
937
  groupMime?: string;
928
938
  requests: RsRequest[];
929
939
  infos?: MediaForUpdate;
940
+ matchType?: RsLookupMatchType;
930
941
  }
931
942
  /**
932
943
  * Request processing status from plugin-based download/processing.
package/dist/library.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Observable } from 'rxjs';
2
2
  import type { AxiosResponse } from 'axios';
3
- import { IFile, ITag, IPerson, ISerie, IMovie, IBook, MediaRequest, IEpisode, ExternalImage, IBackupFile, ILibrary, DeletedQuery, RsDeleted, MovieSort, BookSort, RsSort, SqlOrder, RsRequest, DetectedFaceResult, UnassignFaceResponse, RsGroupDownload, IViewProgress, IWatched, IRsRequestProcessing, BookSearchStreamResult, MovieSearchStreamResult, SerieSearchStreamResult, BookSearchResult, MovieSearchResult, SerieSearchResult, ItemWithRelations, SearchStreamCallbacks, MediaForUpdate, IChannelUpdate } from './interfaces.js';
3
+ import { IFile, ITag, IPerson, ISerie, IMovie, IBook, MediaRequest, IEpisode, ExternalImage, IBackupFile, ILibrary, DeletedQuery, RsDeleted, MovieSort, BookSort, RsSort, SqlOrder, RsRequest, DetectedFaceResult, UnassignFaceResponse, RsGroupDownload, IViewProgress, IWatched, IRsRequestProcessing, BookSearchStreamResult, MovieSearchStreamResult, SerieSearchStreamResult, LookupSearchStreamResult, BookSearchResult, MovieSearchResult, SerieSearchResult, ItemWithRelations, SearchStreamCallbacks, MediaForUpdate, IChannelUpdate } from './interfaces.js';
4
4
  import { SSEMediasEvent, SSEUploadProgressEvent, SSEConvertProgressEvent, SSEEpisodesEvent, SSESeriesEvent, SSEMoviesEvent, SSEBooksEvent, SSEPeopleEvent, SSETagsEvent, SSELibraryStatusEvent, SSEMediaRatingEvent, SSEMediaProgressEvent, SSERequestProcessingEvent } from './sse-types.js';
5
5
  import { EncryptFileOptions, EncryptedFile } from './encryption.js';
6
6
  export interface LibraryHttpClient {
@@ -78,6 +78,10 @@ export declare class LibraryApi {
78
78
  setKey(passPhrase: string): Promise<void>;
79
79
  private getUrl;
80
80
  private openSearchStream;
81
+ searchAllStream(name: string, callbacks: SearchStreamCallbacks<LookupSearchStreamResult>, options?: {
82
+ source?: string[];
83
+ pageKey?: string;
84
+ }): () => void;
81
85
  getTags(query?: {
82
86
  name?: string;
83
87
  parent?: string;
@@ -160,7 +164,10 @@ export declare class LibraryApi {
160
164
  getMovieProgress(movieId: string): Promise<IViewProgress>;
161
165
  setMovieProgress(movieId: string, progress: number): Promise<void>;
162
166
  searchMovies(name: string): Promise<MovieSearchResult[]>;
163
- searchMoviesStream(name: string, callbacks: SearchStreamCallbacks<MovieSearchStreamResult>): () => void;
167
+ searchMoviesStream(name: string, callbacks: SearchStreamCallbacks<MovieSearchStreamResult>, options?: {
168
+ source?: string[];
169
+ pageKey?: string;
170
+ }): () => void;
164
171
  movieRename(movieId: string, newName: string): Promise<IMovie>;
165
172
  updateMoviePoster(movieId: string, poster: FormData, type: string): Promise<void>;
166
173
  updateMovieImageFetch(movieId: string, image: ExternalImage): Promise<void>;
@@ -180,7 +187,10 @@ export declare class LibraryApi {
180
187
  removeBook(bookId: string): Promise<void>;
181
188
  updateBook(bookId: string, updates: Partial<IBook>): Promise<IBook>;
182
189
  searchBooks(name: string): Promise<BookSearchResult[]>;
183
- searchBooksStream(name: string, callbacks: SearchStreamCallbacks<BookSearchStreamResult>): () => void;
190
+ searchBooksStream(name: string, callbacks: SearchStreamCallbacks<BookSearchStreamResult>, options?: {
191
+ source?: string[];
192
+ pageKey?: string;
193
+ }): () => void;
184
194
  bookRename(bookId: string, newName: string): Promise<IBook>;
185
195
  getBookImages(bookId: string): Promise<ExternalImage[]>;
186
196
  updateBookPoster(bookId: string, poster: FormData): Promise<void>;
@@ -195,12 +205,17 @@ export declare class LibraryApi {
195
205
  personRemoveAlt(personId: string, alt: string): Promise<IPerson>;
196
206
  personAddSocial(personId: string, social: any): Promise<IPerson>;
197
207
  personRemoveSocial(personId: string, social: any): Promise<IPerson>;
208
+ personAddOtherId(personId: string, otherId: string): Promise<IPerson>;
209
+ personRemoveOtherId(personId: string, otherId: string): Promise<IPerson>;
198
210
  serieRename(serieId: string, newName: string): Promise<ISerie>;
199
211
  serieAddAlt(serieId: string, alt: string): Promise<ISerie>;
200
212
  serieRemoveAlt(serieId: string, alt: string): Promise<ISerie>;
201
213
  updatePersonPortrait(personId: string, portrait: FormData): Promise<void>;
202
214
  searchSeries(name: string): Promise<SerieSearchResult[]>;
203
- searchSeriesStream(name: string, callbacks: SearchStreamCallbacks<SerieSearchStreamResult>): () => void;
215
+ searchSeriesStream(name: string, callbacks: SearchStreamCallbacks<SerieSearchStreamResult>, options?: {
216
+ source?: string[];
217
+ pageKey?: string;
218
+ }): () => void;
204
219
  setEpisodeWatched(serieId: string, season: number, number: number, date: number): Promise<void>;
205
220
  getEpisodeWatched(serieId: string, season: number, episode: number): Promise<IWatched>;
206
221
  getEpisodeProgress(serieId: string, season: number, episode: number): Promise<IViewProgress>;
package/dist/library.js CHANGED
@@ -124,6 +124,14 @@ export class LibraryApi {
124
124
  }
125
125
  }, finish, (error) => callbacks.onError?.(error));
126
126
  }
127
+ searchAllStream(name, callbacks, options) {
128
+ const params = { name };
129
+ if (options?.source?.length)
130
+ params.source = options.source.join(',');
131
+ if (options?.pageKey)
132
+ params.pageKey = options.pageKey;
133
+ return this.openSearchStream(this.getUrl('/searchstream'), params, callbacks);
134
+ }
127
135
  async getTags(query) {
128
136
  const params = {};
129
137
  if (query) {
@@ -425,8 +433,13 @@ export class LibraryApi {
425
433
  const res = await this.client.get(this.getUrl(`/movies/search?name=${name}`));
426
434
  return res.data;
427
435
  }
428
- searchMoviesStream(name, callbacks) {
429
- return this.openSearchStream(this.getUrl('/movies/searchstream'), { name }, callbacks);
436
+ searchMoviesStream(name, callbacks, options) {
437
+ const params = { name };
438
+ if (options?.source?.length)
439
+ params.source = options.source.join(',');
440
+ if (options?.pageKey)
441
+ params.pageKey = options.pageKey;
442
+ return this.openSearchStream(this.getUrl('/movies/searchstream'), params, callbacks);
430
443
  }
431
444
  async movieRename(movieId, newName) {
432
445
  const res = await this.client.patch(this.getUrl(`/movies/${movieId}`), {
@@ -493,8 +506,13 @@ export class LibraryApi {
493
506
  const res = await this.client.get(this.getUrl(`/books/search?name=${name}`));
494
507
  return res.data;
495
508
  }
496
- searchBooksStream(name, callbacks) {
497
- return this.openSearchStream(this.getUrl('/books/searchstream'), { name }, callbacks);
509
+ searchBooksStream(name, callbacks, options) {
510
+ const params = { name };
511
+ if (options?.source?.length)
512
+ params.source = options.source.join(',');
513
+ if (options?.pageKey)
514
+ params.pageKey = options.pageKey;
515
+ return this.openSearchStream(this.getUrl('/books/searchstream'), params, callbacks);
498
516
  }
499
517
  async bookRename(bookId, newName) {
500
518
  const res = await this.client.patch(this.getUrl(`/books/${bookId}`), {
@@ -560,6 +578,18 @@ export class LibraryApi {
560
578
  });
561
579
  return res.data;
562
580
  }
581
+ async personAddOtherId(personId, otherId) {
582
+ const res = await this.client.patch(this.getUrl(`/people/${personId}`), {
583
+ addOtherids: [otherId]
584
+ });
585
+ return res.data;
586
+ }
587
+ async personRemoveOtherId(personId, otherId) {
588
+ const res = await this.client.patch(this.getUrl(`/people/${personId}`), {
589
+ removeOtherids: [otherId]
590
+ });
591
+ return res.data;
592
+ }
563
593
  async serieRename(serieId, newName) {
564
594
  const res = await this.client.patch(this.getUrl(`/series/${serieId}`), {
565
595
  name: newName
@@ -585,8 +615,13 @@ export class LibraryApi {
585
615
  const res = await this.client.get(this.getUrl(`/series/search?name=${name}`));
586
616
  return res.data;
587
617
  }
588
- searchSeriesStream(name, callbacks) {
589
- return this.openSearchStream(this.getUrl('/series/searchstream'), { name }, callbacks);
618
+ searchSeriesStream(name, callbacks, options) {
619
+ const params = { name };
620
+ if (options?.source?.length)
621
+ params.source = options.source.join(',');
622
+ if (options?.pageKey)
623
+ params.pageKey = options.pageKey;
624
+ return this.openSearchStream(this.getUrl('/series/searchstream'), params, callbacks);
590
625
  }
591
626
  async setEpisodeWatched(serieId, season, number, date) {
592
627
  await this.client.post(this.getUrl(`/series/${serieId}/seasons/${season}/episodes/${number}/watched`), { date });
package/dist/server.d.ts CHANGED
@@ -15,7 +15,10 @@ export declare class ServerApi {
15
15
  updateLibrary(libraryId: string, library: ServerLibraryForUpdate): Promise<ILibrary>;
16
16
  deleteLibrary(libraryId: string, deleteMediaContent?: boolean): Promise<void>;
17
17
  getPlugins(): Promise<IPlugin[]>;
18
- searchLookupStream(query: string, type: string, callbacks: SearchStreamCallbacks<LookupSearchStreamResult>): () => void;
18
+ searchLookupStream(query: string, type: string, callbacks: SearchStreamCallbacks<LookupSearchStreamResult>, options?: {
19
+ source?: string[];
20
+ pageKey?: string;
21
+ }): () => void;
19
22
  getCredentials(): Promise<ICredential[]>;
20
23
  saveCredential(credential: ICredential): Promise<ICredential>;
21
24
  updateCredential(credential: ICredential): Promise<ICredential>;
package/dist/server.js CHANGED
@@ -73,8 +73,13 @@ export class ServerApi {
73
73
  const res = await this.client.get('/plugins');
74
74
  return res.data;
75
75
  }
76
- searchLookupStream(query, type, callbacks) {
77
- return this.openSearchStream('/plugins/lookup/searchstream', { q: query, type }, callbacks);
76
+ searchLookupStream(query, type, callbacks, options) {
77
+ const params = { q: query, type };
78
+ if (options?.source?.length)
79
+ params.source = options.source.join(',');
80
+ if (options?.pageKey)
81
+ params.pageKey = options.pageKey;
82
+ return this.openSearchStream('/plugins/lookup/searchstream', params, callbacks);
78
83
  }
79
84
  async getCredentials() {
80
85
  const res = await this.client.get('/credentials');
@@ -10,6 +10,7 @@ export interface RawSSEEvent {
10
10
  /**
11
11
  * Parses a raw SSE buffer into discrete events, returning any incomplete trailing data.
12
12
  * Follows the SSE spec: events are separated by blank lines, fields are `field: value`.
13
+ * Handles \r\n, \r, and \n line endings per the SSE specification.
13
14
  */
14
15
  export declare function parseSSEBuffer(buffer: string): {
15
16
  events: RawSSEEvent[];
package/dist/sse-fetch.js CHANGED
@@ -1,58 +1,60 @@
1
1
  /**
2
2
  * Parses a raw SSE buffer into discrete events, returning any incomplete trailing data.
3
3
  * Follows the SSE spec: events are separated by blank lines, fields are `field: value`.
4
+ * Handles \r\n, \r, and \n line endings per the SSE specification.
4
5
  */
5
6
  export function parseSSEBuffer(buffer) {
6
7
  const events = [];
7
- const lines = buffer.split('\n');
8
- let processedUpTo = 0;
9
- let eventType = '';
10
- let eventData = '';
11
- let eventId;
12
- let eventRetry;
13
- for (let i = 0; i < lines.length; i++) {
14
- const line = lines[i];
15
- // Incomplete last line — keep in buffer
16
- if (i === lines.length - 1 && !buffer.endsWith('\n')) {
8
+ const normalised = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
9
+ // Split into blocks separated by blank lines (\n\n).
10
+ // Only complete blocks (followed by \n\n) are processed;
11
+ // any trailing incomplete block stays in the buffer.
12
+ let lastEventEnd = 0;
13
+ const blockBoundary = '\n\n';
14
+ let searchFrom = 0;
15
+ while (true) {
16
+ const idx = normalised.indexOf(blockBoundary, searchFrom);
17
+ if (idx === -1)
17
18
  break;
18
- }
19
- processedUpTo += line.length + 1; // +1 for '\n'
20
- if (line === '') {
21
- // Blank line = end of event
22
- if (eventType) {
23
- events.push({ event: eventType, data: eventData, id: eventId, retry: eventRetry });
19
+ const block = normalised.slice(lastEventEnd, idx);
20
+ lastEventEnd = idx + blockBoundary.length;
21
+ searchFrom = lastEventEnd;
22
+ let eventType = '';
23
+ let eventData = '';
24
+ let eventId;
25
+ let eventRetry;
26
+ for (const line of block.split('\n')) {
27
+ if (line === '' || line.startsWith(':'))
28
+ continue;
29
+ const colonIdx = line.indexOf(':');
30
+ if (colonIdx === -1)
31
+ continue;
32
+ const field = line.slice(0, colonIdx);
33
+ let value = line.slice(colonIdx + 1);
34
+ if (value.startsWith(' '))
35
+ value = value.slice(1);
36
+ switch (field) {
37
+ case 'event':
38
+ eventType = value;
39
+ break;
40
+ case 'data':
41
+ eventData = eventData ? eventData + '\n' + value : value;
42
+ break;
43
+ case 'id':
44
+ eventId = value;
45
+ break;
46
+ case 'retry':
47
+ eventRetry = parseInt(value, 10);
48
+ break;
24
49
  }
25
- eventType = '';
26
- eventData = '';
27
- eventId = undefined;
28
- eventRetry = undefined;
29
- continue;
30
50
  }
31
- if (line.startsWith(':'))
32
- continue; // SSE comment
33
- const colonIdx = line.indexOf(':');
34
- if (colonIdx === -1)
35
- continue;
36
- const field = line.slice(0, colonIdx);
37
- let value = line.slice(colonIdx + 1);
38
- if (value.startsWith(' '))
39
- value = value.slice(1);
40
- switch (field) {
41
- case 'event':
42
- eventType = value;
43
- break;
44
- case 'data':
45
- eventData = eventData ? eventData + '\n' + value : value;
46
- break;
47
- case 'id':
48
- eventId = value;
49
- break;
50
- case 'retry':
51
- eventRetry = parseInt(value, 10);
52
- break;
51
+ if (eventType) {
52
+ events.push({ event: eventType, data: eventData, id: eventId, retry: eventRetry });
53
53
  }
54
54
  }
55
- return { events, remainingBuffer: buffer.slice(processedUpTo) };
55
+ // Note: remainingBuffer is from the normalised string. Callers must replace
56
+ // their buffer entirely (not use offset-based splicing on the original input).
57
+ return { events, remainingBuffer: normalised.slice(lastEventEnd) };
56
58
  }
57
59
  /**
58
60
  * Opens a one-shot fetch-based SSE stream (no reconnection).
@@ -87,6 +89,14 @@ export function openFetchSSEStream(url, onEvent, onDone, onError) {
87
89
  while (true) {
88
90
  const { done, value } = await reader.read();
89
91
  if (done) {
92
+ // Flush any remaining buffered event when the stream closes
93
+ // (server may close without a trailing blank line)
94
+ if (buffer.trim()) {
95
+ const { events: finalEvents } = parseSSEBuffer(buffer + '\n\n');
96
+ for (const event of finalEvents) {
97
+ onEvent(event.event, event.data);
98
+ }
99
+ }
90
100
  onDone?.();
91
101
  break;
92
102
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redseat/api",
3
- "version": "0.4.6",
3
+ "version": "0.5.0",
4
4
  "description": "TypeScript API client library for interacting with Redseat servers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",