@redseat/api 0.3.14 → 0.4.2

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/library.d.ts CHANGED
@@ -1,61 +1,8 @@
1
1
  import { Observable } from 'rxjs';
2
2
  import type { AxiosResponse } from 'axios';
3
- import { IFile, ITag, IPerson, ISerie, IMovie, MediaRequest, IEpisode, ExternalImage, IBackupFile, ILibrary, SerieInMedia, DeletedQuery, RsDeleted, MovieSort, RsSort, SqlOrder, RsRequest, DetectedFaceResult, UnassignFaceResponse, RsGroupDownload, IViewProgress, IWatched, IRsRequestProcessing } from './interfaces.js';
4
- import { SSEMediasEvent, SSEUploadProgressEvent, SSEConvertProgressEvent, SSEEpisodesEvent, SSESeriesEvent, SSEMoviesEvent, SSEPeopleEvent, SSETagsEvent, SSELibraryStatusEvent, SSEMediaRatingEvent, SSEMediaProgressEvent, SSERequestProcessingEvent } from './sse-types.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, BookSearchResult, MovieSearchResult, SerieSearchResult, ItemWithRelations, SearchStreamCallbacks, MediaForUpdate, IChannelUpdate } from './interfaces.js';
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
- export interface MediaForUpdate {
7
- name?: string;
8
- description?: string;
9
- mimetype?: string;
10
- size?: number;
11
- md5?: string;
12
- modified?: number;
13
- created?: number;
14
- width?: number;
15
- height?: number;
16
- orientation?: number;
17
- color_space?: string;
18
- icc?: string;
19
- mp?: number;
20
- vcodecs?: string[];
21
- acodecs?: string[];
22
- fps?: number;
23
- bitrate?: number;
24
- focal?: number;
25
- iso?: number;
26
- model?: string;
27
- sspeed?: string;
28
- f_number?: number;
29
- duration?: number;
30
- progress?: number;
31
- addTags?: {
32
- id: string;
33
- conf?: number;
34
- }[];
35
- removeTags?: string[];
36
- tagsLookup?: string[];
37
- addSeries?: SerieInMedia[];
38
- removeSeries?: SerieInMedia[];
39
- addPeople?: {
40
- id: string;
41
- conf?: number;
42
- }[];
43
- removePeople?: string[];
44
- peopleLookup?: string[];
45
- long?: number;
46
- lat?: number;
47
- gps?: string;
48
- origin?: any;
49
- originUrl?: string;
50
- movie?: string;
51
- lang?: string;
52
- rating?: number;
53
- thumbsize?: number;
54
- iv?: string;
55
- uploader?: string;
56
- uploadkey?: string;
57
- uploadId?: string;
58
- }
59
6
  export interface LibraryHttpClient {
60
7
  get<T = unknown>(url: string, config?: any): Promise<{
61
8
  data: T;
@@ -83,6 +30,7 @@ export interface LibraryHttpClient {
83
30
  readonly episodes$?: Observable<SSEEpisodesEvent>;
84
31
  readonly series$?: Observable<SSESeriesEvent>;
85
32
  readonly movies$?: Observable<SSEMoviesEvent>;
33
+ readonly books$?: Observable<SSEBooksEvent>;
86
34
  readonly people$?: Observable<SSEPeopleEvent>;
87
35
  readonly tags$?: Observable<SSETagsEvent>;
88
36
  readonly libraryStatus$?: Observable<SSELibraryStatusEvent>;
@@ -90,6 +38,12 @@ export interface LibraryHttpClient {
90
38
  readonly mediaProgress$?: Observable<SSEMediaProgressEvent>;
91
39
  readonly requestProcessing$?: Observable<SSERequestProcessingEvent>;
92
40
  }
41
+ export interface UploadMediaMultipartOptions {
42
+ file: Blob;
43
+ info?: MediaForUpdate;
44
+ filename?: string;
45
+ progressCallback?: (loaded: number, total: number) => void;
46
+ }
93
47
  export declare class LibraryApi {
94
48
  private client;
95
49
  private libraryId;
@@ -103,6 +57,7 @@ export declare class LibraryApi {
103
57
  readonly episodes$: Observable<SSEEpisodesEvent>;
104
58
  readonly series$: Observable<SSESeriesEvent>;
105
59
  readonly movies$: Observable<SSEMoviesEvent>;
60
+ readonly books$: Observable<SSEBooksEvent>;
106
61
  readonly people$: Observable<SSEPeopleEvent>;
107
62
  readonly tags$: Observable<SSETagsEvent>;
108
63
  readonly libraryStatus$: Observable<SSELibraryStatusEvent>;
@@ -122,6 +77,7 @@ export declare class LibraryApi {
122
77
  dispose(): void;
123
78
  setKey(passPhrase: string): Promise<void>;
124
79
  private getUrl;
80
+ private openSearchStream;
125
81
  getTags(query?: {
126
82
  name?: string;
127
83
  parent?: string;
@@ -152,6 +108,7 @@ export declare class LibraryApi {
152
108
  }[];
153
109
  limit?: number;
154
110
  }): Promise<IEpisode[]>;
111
+ getSerieBooks(serieId: string): Promise<IBook[]>;
155
112
  getMovies(query?: {
156
113
  after?: number;
157
114
  inDigital?: boolean;
@@ -200,10 +157,32 @@ export declare class LibraryApi {
200
157
  getMovieWatched(movieId: string): Promise<IWatched>;
201
158
  getMovieProgress(movieId: string): Promise<IViewProgress>;
202
159
  setMovieProgress(movieId: string, progress: number): Promise<void>;
203
- searchMovies(name: string): Promise<IMovie[]>;
160
+ searchMovies(name: string): Promise<MovieSearchResult[]>;
161
+ searchMoviesStream(name: string, callbacks: SearchStreamCallbacks<MovieSearchStreamResult>): () => void;
204
162
  movieRename(movieId: string, newName: string): Promise<IMovie>;
205
163
  updateMoviePoster(movieId: string, poster: FormData, type: string): Promise<void>;
206
164
  updateMovieImageFetch(movieId: string, image: ExternalImage): Promise<void>;
165
+ getBooks(query?: {
166
+ after?: number;
167
+ sort?: BookSort;
168
+ }): Promise<ItemWithRelations<IBook>[]>;
169
+ getBook(bookId: string): Promise<ItemWithRelations<IBook>>;
170
+ getBookMedias(bookId: string): Promise<IFile[]>;
171
+ searchBookMedias(bookId: string, q?: string): Promise<RsGroupDownload[]>;
172
+ addSearchedBookMedia(bookId: string, request: RsRequest): Promise<IFile>;
173
+ createBook(book: Partial<IBook>, options?: {
174
+ upsertTags?: boolean;
175
+ upsertPeople?: boolean;
176
+ upsertSerie?: boolean;
177
+ }): Promise<IBook>;
178
+ removeBook(bookId: string): Promise<void>;
179
+ updateBook(bookId: string, updates: Partial<IBook>): Promise<IBook>;
180
+ searchBooks(name: string): Promise<BookSearchResult[]>;
181
+ searchBooksStream(name: string, callbacks: SearchStreamCallbacks<BookSearchStreamResult>): () => void;
182
+ bookRename(bookId: string, newName: string): Promise<IBook>;
183
+ getBookImages(bookId: string): Promise<ExternalImage[]>;
184
+ updateBookPoster(bookId: string, poster: FormData): Promise<void>;
185
+ updateBookImageFetch(bookId: string, image: ExternalImage): Promise<void>;
207
186
  addTagToMedia(mediaId: string, tagId: string): Promise<void>;
208
187
  removeTagFromMedia(mediaId: string, tagId: string): Promise<void>;
209
188
  getCryptChallenge(): Promise<string>;
@@ -218,7 +197,8 @@ export declare class LibraryApi {
218
197
  serieAddAlt(serieId: string, alt: string): Promise<ISerie>;
219
198
  serieRemoveAlt(serieId: string, alt: string): Promise<ISerie>;
220
199
  updatePersonPortrait(personId: string, portrait: FormData): Promise<void>;
221
- searchSeries(name: string): Promise<ISerie[]>;
200
+ searchSeries(name: string): Promise<SerieSearchResult[]>;
201
+ searchSeriesStream(name: string, callbacks: SearchStreamCallbacks<SerieSearchStreamResult>): () => void;
222
202
  setEpisodeWatched(serieId: string, season: number, number: number, date: number): Promise<void>;
223
203
  getEpisodeWatched(serieId: string, season: number, episode: number): Promise<IWatched>;
224
204
  getEpisodeProgress(serieId: string, season: number, episode: number): Promise<IViewProgress>;
@@ -228,9 +208,25 @@ export declare class LibraryApi {
228
208
  * @param serieId - The series identifier
229
209
  * @param season - The season number
230
210
  * @param episode - The episode number
231
- * @returns Array of available media requests that can be used to download/stream the episode
211
+ * @param q - Optional search query to filter results
212
+ * @returns Array of grouped download requests. Use `.flatMap(g => g.requests)` to get a flat list of RsRequest items.
232
213
  */
233
- searchEpisodeMedias(serieId: string, season: number, episode: number): Promise<RsRequest[]>;
214
+ searchEpisodeMedias(serieId: string, season: number, episode: number, q?: string): Promise<RsGroupDownload[]>;
215
+ /**
216
+ * Searches for available media sources for an entire season.
217
+ * @param serieId - The series identifier
218
+ * @param season - The season number
219
+ * @param q - Optional search query to filter results
220
+ * @returns Array of grouped download requests. Use `.flatMap(g => g.requests)` to get a flat list of RsRequest items.
221
+ */
222
+ searchSeasonMedias(serieId: string, season: number, q?: string): Promise<RsGroupDownload[]>;
223
+ /**
224
+ * Searches for available media sources for a movie.
225
+ * @param movieId - The movie identifier
226
+ * @param q - Optional search query to filter results
227
+ * @returns Array of grouped download requests. Use `.flatMap(g => g.requests)` to get a flat list of RsRequest items.
228
+ */
229
+ searchMovieMedias(movieId: string, q?: string): Promise<RsGroupDownload[]>;
234
230
  /**
235
231
  * Adds a searched episode media to the database for download/storage.
236
232
  * @param serieId - The series identifier
@@ -240,6 +236,27 @@ export declare class LibraryApi {
240
236
  * @returns The created media file entry
241
237
  */
242
238
  addSearchedEpisodeMedia(serieId: string, season: number, episode: number, request: RsRequest): Promise<IFile>;
239
+ /**
240
+ * Adds a searched movie media to the database for download/storage.
241
+ * @param movieId - The movie identifier
242
+ * @param request - The media request to add (typically from searchMovieMedias results)
243
+ * @returns The created media file entry
244
+ */
245
+ addSearchedMovieMedia(movieId: string, request: RsRequest): Promise<IFile>;
246
+ /**
247
+ * Gets the media files attached to a specific episode.
248
+ * @param serieId - The series identifier
249
+ * @param season - The season number
250
+ * @param episode - The episode number
251
+ * @returns Array of media files for the episode
252
+ */
253
+ getEpisodeMedias(serieId: string, season: number, episode: number): Promise<IFile[]>;
254
+ /**
255
+ * Gets the media files attached to a specific movie.
256
+ * @param movieId - The movie identifier
257
+ * @returns Array of media files for the movie
258
+ */
259
+ getMovieMedias(movieId: string): Promise<IFile[]>;
243
260
  /**
244
261
  * Checks if a request can be made permanent (saved for later use).
245
262
  * Tests the availability of the URL and returns a permanent version if valid.
@@ -341,14 +358,14 @@ export declare class LibraryApi {
341
358
  mergePeople(request: any): Promise<any>;
342
359
  clusterFaces(personId: string): Promise<any>;
343
360
  mergeMedias(request: any): Promise<IFile>;
344
- mediaUpdateMany(update: any, ids: string[]): Promise<IFile[]>;
361
+ mediaUpdateMany(update: MediaForUpdate, ids: string[]): Promise<IFile[]>;
345
362
  mediaUpdateProgress(mediaId: string, progress: number): Promise<{
346
363
  progress: number;
347
364
  }>;
348
- mediaUpdateChannel(mediaId: string, update: any): Promise<IFile[]>;
365
+ mediaUpdateChannel(mediaId: string, update: IChannelUpdate): Promise<IFile[]>;
349
366
  refreshMedia(mediaId: string): Promise<IFile[]>;
350
367
  aiTagMedia(mediaId: string): Promise<IFile[]>;
351
- mediaUpdate(mediaId: string, update: any): Promise<IFile[]>;
368
+ mediaUpdate(mediaId: string, update: MediaForUpdate): Promise<IFile[]>;
352
369
  splitZip(mediaId: string, from: number, to: number): Promise<IFile>;
353
370
  deleteFromZip(mediaId: string, pages: number[]): Promise<IFile>;
354
371
  updateMediaThumb(mediaId: string, thumb: FormData): Promise<void>;
@@ -415,11 +432,16 @@ export declare class LibraryApi {
415
432
  */
416
433
  uploadMedia(data: ArrayBuffer | Uint8Array, options: {
417
434
  thumb?: ArrayBuffer | Uint8Array;
418
- metadata: Omit<MediaForUpdate, 'id'>;
435
+ metadata: MediaForUpdate;
419
436
  fileMime: string;
420
437
  thumbMime?: string;
421
438
  progressCallback?: (loaded: number, total: number) => void;
422
439
  }): Promise<IFile>;
440
+ /**
441
+ * Upload media using raw multipart form data.
442
+ * `info` is optional and will be serialized as the `info` multipart field when provided.
443
+ */
444
+ uploadMediaMultipart(options: UploadMediaMultipartOptions): Promise<unknown>;
423
445
  /**
424
446
  * Upload a group of media files from URLs
425
447
  * @param download - The group download request containing requests array
package/dist/library.js CHANGED
@@ -14,6 +14,7 @@ export class LibraryApi {
14
14
  this.episodes$ = this.createLibraryFilteredStream(client.episodes$);
15
15
  this.series$ = this.createLibraryFilteredStream(client.series$);
16
16
  this.movies$ = this.createLibraryFilteredStream(client.movies$);
17
+ this.books$ = this.createLibraryFilteredStream(client.books$);
17
18
  this.people$ = this.createLibraryFilteredStream(client.people$);
18
19
  this.tags$ = this.createLibraryFilteredStream(client.tags$);
19
20
  this.libraryStatus$ = this.createLibraryFilteredStream(client.libraryStatus$);
@@ -95,6 +96,50 @@ export class LibraryApi {
95
96
  getUrl(path) {
96
97
  return `/libraries/${this.libraryId}${path}`;
97
98
  }
99
+ openSearchStream(path, params, callbacks) {
100
+ const EventSourceCtor = globalThis.EventSource;
101
+ if (!EventSourceCtor) {
102
+ callbacks.onError?.(new Error('EventSource is not available in this runtime.'));
103
+ return () => undefined;
104
+ }
105
+ const url = this.client.getFullUrl(path, {
106
+ ...params,
107
+ token: this.client.getAuthToken()
108
+ });
109
+ const source = new EventSourceCtor(url);
110
+ let closed = false;
111
+ const close = () => {
112
+ if (closed) {
113
+ return;
114
+ }
115
+ closed = true;
116
+ source.removeEventListener('results', onResults);
117
+ source.removeEventListener('finished', onFinished);
118
+ source.onerror = null;
119
+ source.close();
120
+ };
121
+ const onResults = (event) => {
122
+ try {
123
+ const payload = JSON.parse(event.data);
124
+ callbacks.onResults?.(payload);
125
+ }
126
+ catch (error) {
127
+ callbacks.onError?.(error);
128
+ close();
129
+ }
130
+ };
131
+ const onFinished = () => {
132
+ callbacks.onFinished?.();
133
+ close();
134
+ };
135
+ source.addEventListener('results', onResults);
136
+ source.addEventListener('finished', onFinished);
137
+ source.onerror = (error) => {
138
+ callbacks.onError?.(error);
139
+ close();
140
+ };
141
+ return close;
142
+ }
98
143
  async getTags(query) {
99
144
  const params = {};
100
145
  if (query) {
@@ -181,6 +226,10 @@ export class LibraryApi {
181
226
  const res = await this.client.get(this.getUrl('/series/episodes'), { params });
182
227
  return res.data;
183
228
  }
229
+ async getSerieBooks(serieId) {
230
+ const res = await this.client.get(this.getUrl(`/series/${serieId}/books`));
231
+ return res.data;
232
+ }
184
233
  async getMovies(query) {
185
234
  const params = {};
186
235
  if (query) {
@@ -335,7 +384,7 @@ export class LibraryApi {
335
384
  return res.data;
336
385
  }
337
386
  async getSerieImages(serieId) {
338
- const res = await this.client.get(this.getUrl(`/series/${serieId}/images`));
387
+ const res = await this.client.get(this.getUrl(`/series/${serieId}/image/search`));
339
388
  return res.data;
340
389
  }
341
390
  async updateSeriePoster(serieId, poster, type) {
@@ -363,7 +412,7 @@ export class LibraryApi {
363
412
  return res.data;
364
413
  }
365
414
  async getMovieImages(movieId) {
366
- const res = await this.client.get(this.getUrl(`/movies/${movieId}/images`));
415
+ const res = await this.client.get(this.getUrl(`/movies/${movieId}/image/search`));
367
416
  return res.data;
368
417
  }
369
418
  async setMovieWatched(movieId, date) {
@@ -384,6 +433,9 @@ export class LibraryApi {
384
433
  const res = await this.client.get(this.getUrl(`/movies/search?name=${name}`));
385
434
  return res.data;
386
435
  }
436
+ searchMoviesStream(name, callbacks) {
437
+ return this.openSearchStream(this.getUrl('/movies/searchstream'), { name }, callbacks);
438
+ }
387
439
  async movieRename(movieId, newName) {
388
440
  const res = await this.client.patch(this.getUrl(`/movies/${movieId}`), {
389
441
  name: newName
@@ -396,6 +448,78 @@ export class LibraryApi {
396
448
  async updateMovieImageFetch(movieId, image) {
397
449
  await this.client.post(this.getUrl(`/movies/${movieId}/image/fetch`), image);
398
450
  }
451
+ // ==================== Books ====================
452
+ async getBooks(query) {
453
+ const params = {};
454
+ if (query) {
455
+ if (query.after !== undefined) {
456
+ params.after = query.after;
457
+ }
458
+ if (query.sort !== undefined) {
459
+ params.sort = query.sort;
460
+ }
461
+ }
462
+ const res = await this.client.get(this.getUrl('/books'), { params });
463
+ return res.data;
464
+ }
465
+ async getBook(bookId) {
466
+ const res = await this.client.get(this.getUrl(`/books/${bookId}`));
467
+ return res.data;
468
+ }
469
+ async getBookMedias(bookId) {
470
+ const res = await this.client.get(this.getUrl(`/books/${bookId}/medias`));
471
+ return res.data;
472
+ }
473
+ async searchBookMedias(bookId, q) {
474
+ const res = await this.client.get(this.getUrl(`/books/${bookId}/search`), q ? { params: { q } } : undefined);
475
+ return res.data;
476
+ }
477
+ async addSearchedBookMedia(bookId, request) {
478
+ const res = await this.client.post(this.getUrl(`/books/${bookId}/search`), request);
479
+ return res.data;
480
+ }
481
+ async createBook(book, options) {
482
+ const params = new URLSearchParams();
483
+ if (options?.upsertTags)
484
+ params.set('upsertTags', 'true');
485
+ if (options?.upsertPeople)
486
+ params.set('upsertPeople', 'true');
487
+ if (options?.upsertSerie)
488
+ params.set('upsertSerie', 'true');
489
+ const query = params.toString();
490
+ const res = await this.client.post(this.getUrl(`/books${query ? `?${query}` : ''}`), book);
491
+ return res.data;
492
+ }
493
+ async removeBook(bookId) {
494
+ await this.client.delete(this.getUrl(`/books/${bookId}`));
495
+ }
496
+ async updateBook(bookId, updates) {
497
+ const res = await this.client.patch(this.getUrl(`/books/${bookId}`), updates);
498
+ return res.data;
499
+ }
500
+ async searchBooks(name) {
501
+ const res = await this.client.get(this.getUrl(`/books/search?name=${name}`));
502
+ return res.data;
503
+ }
504
+ searchBooksStream(name, callbacks) {
505
+ return this.openSearchStream(this.getUrl('/books/searchstream'), { name }, callbacks);
506
+ }
507
+ async bookRename(bookId, newName) {
508
+ const res = await this.client.patch(this.getUrl(`/books/${bookId}`), {
509
+ name: newName
510
+ });
511
+ return res.data;
512
+ }
513
+ async getBookImages(bookId) {
514
+ const res = await this.client.get(this.getUrl(`/books/${bookId}/image/search`));
515
+ return res.data;
516
+ }
517
+ async updateBookPoster(bookId, poster) {
518
+ await this.client.post(this.getUrl(`/books/${bookId}/image`), poster);
519
+ }
520
+ async updateBookImageFetch(bookId, image) {
521
+ await this.client.post(this.getUrl(`/books/${bookId}/image/fetch`), image);
522
+ }
399
523
  async addTagToMedia(mediaId, tagId) {
400
524
  await this.client.post(this.getUrl(`/medias/${mediaId}/tags/${tagId}`), {});
401
525
  }
@@ -469,6 +593,9 @@ export class LibraryApi {
469
593
  const res = await this.client.get(this.getUrl(`/series/search?name=${name}`));
470
594
  return res.data;
471
595
  }
596
+ searchSeriesStream(name, callbacks) {
597
+ return this.openSearchStream(this.getUrl('/series/searchstream'), { name }, callbacks);
598
+ }
472
599
  async setEpisodeWatched(serieId, season, number, date) {
473
600
  await this.client.post(this.getUrl(`/series/${serieId}/seasons/${season}/episodes/${number}/watched`), { date });
474
601
  }
@@ -488,10 +615,32 @@ export class LibraryApi {
488
615
  * @param serieId - The series identifier
489
616
  * @param season - The season number
490
617
  * @param episode - The episode number
491
- * @returns Array of available media requests that can be used to download/stream the episode
618
+ * @param q - Optional search query to filter results
619
+ * @returns Array of grouped download requests. Use `.flatMap(g => g.requests)` to get a flat list of RsRequest items.
620
+ */
621
+ async searchEpisodeMedias(serieId, season, episode, q) {
622
+ const res = await this.client.get(this.getUrl(`/series/${serieId}/seasons/${season}/episodes/${episode}/search`), q ? { params: { q } } : undefined);
623
+ return res.data;
624
+ }
625
+ /**
626
+ * Searches for available media sources for an entire season.
627
+ * @param serieId - The series identifier
628
+ * @param season - The season number
629
+ * @param q - Optional search query to filter results
630
+ * @returns Array of grouped download requests. Use `.flatMap(g => g.requests)` to get a flat list of RsRequest items.
631
+ */
632
+ async searchSeasonMedias(serieId, season, q) {
633
+ const res = await this.client.get(this.getUrl(`/series/${serieId}/seasons/${season}/search`), q ? { params: { q } } : undefined);
634
+ return res.data;
635
+ }
636
+ /**
637
+ * Searches for available media sources for a movie.
638
+ * @param movieId - The movie identifier
639
+ * @param q - Optional search query to filter results
640
+ * @returns Array of grouped download requests. Use `.flatMap(g => g.requests)` to get a flat list of RsRequest items.
492
641
  */
493
- async searchEpisodeMedias(serieId, season, episode) {
494
- const res = await this.client.get(this.getUrl(`/series/${serieId}/seasons/${season}/episodes/${episode}/search`));
642
+ async searchMovieMedias(movieId, q) {
643
+ const res = await this.client.get(this.getUrl(`/movies/${movieId}/search`), q ? { params: { q } } : undefined);
495
644
  return res.data;
496
645
  }
497
646
  /**
@@ -506,6 +655,36 @@ export class LibraryApi {
506
655
  const res = await this.client.post(this.getUrl(`/series/${serieId}/seasons/${season}/episodes/${episode}/search`), request);
507
656
  return res.data;
508
657
  }
658
+ /**
659
+ * Adds a searched movie media to the database for download/storage.
660
+ * @param movieId - The movie identifier
661
+ * @param request - The media request to add (typically from searchMovieMedias results)
662
+ * @returns The created media file entry
663
+ */
664
+ async addSearchedMovieMedia(movieId, request) {
665
+ const res = await this.client.post(this.getUrl(`/movies/${movieId}/search`), request);
666
+ return res.data;
667
+ }
668
+ /**
669
+ * Gets the media files attached to a specific episode.
670
+ * @param serieId - The series identifier
671
+ * @param season - The season number
672
+ * @param episode - The episode number
673
+ * @returns Array of media files for the episode
674
+ */
675
+ async getEpisodeMedias(serieId, season, episode) {
676
+ const res = await this.client.get(this.getUrl(`/series/${serieId}/seasons/${season}/episodes/${episode}/medias`));
677
+ return res.data;
678
+ }
679
+ /**
680
+ * Gets the media files attached to a specific movie.
681
+ * @param movieId - The movie identifier
682
+ * @returns Array of media files for the movie
683
+ */
684
+ async getMovieMedias(movieId) {
685
+ const res = await this.client.get(this.getUrl(`/movies/${movieId}/medias`));
686
+ return res.data;
687
+ }
509
688
  /**
510
689
  * Checks if a request can be made permanent (saved for later use).
511
690
  * Tests the availability of the URL and returns a permanent version if valid.
@@ -922,13 +1101,28 @@ export class LibraryApi {
922
1101
  filename = options.metadata.name || 'file';
923
1102
  fileBlob = new Blob([dataBuffer], { type: options.fileMime });
924
1103
  }
925
- // Create FormData
1104
+ return (await this.uploadMediaMultipart({
1105
+ file: fileBlob,
1106
+ filename,
1107
+ info: { ...metadata, size: fileBlob.size },
1108
+ progressCallback: options.progressCallback
1109
+ }));
1110
+ }
1111
+ /**
1112
+ * Upload media using raw multipart form data.
1113
+ * `info` is optional and will be serialized as the `info` multipart field when provided.
1114
+ */
1115
+ async uploadMediaMultipart(options) {
926
1116
  const formData = new FormData();
927
- // Info should always be first in the FormData
928
- formData.append('info', JSON.stringify({ ...metadata, size: fileBlob.size }));
929
- // File should always be last in the FormData
930
- formData.append('file', fileBlob, filename);
931
- // Send POST request with progress tracking
1117
+ if (options.info) {
1118
+ const infoPayload = { ...options.info };
1119
+ if (infoPayload.size === undefined) {
1120
+ infoPayload.size = options.file.size;
1121
+ }
1122
+ formData.append('info', JSON.stringify(infoPayload));
1123
+ }
1124
+ const filename = options.filename ?? (options.file.name ?? 'file');
1125
+ formData.append('file', options.file, filename);
932
1126
  const config = {};
933
1127
  if (options.progressCallback) {
934
1128
  config.onUploadProgress = (progressEvent) => {
package/dist/server.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { RedseatClient } from './client.js';
2
- import { ILibrary, IPlugin, ICredential, IWatched, IWatchedForAdd, IViewProgress, IViewProgressForAdd, HistoryQuery } from './interfaces.js';
2
+ import { ILibrary, IPlugin, ICredential, IWatched, IWatchedForAdd, IViewProgress, IViewProgressForAdd, HistoryQuery, ServerLibraryForUpdate, LookupSearchStreamResult, SearchStreamCallbacks } from './interfaces.js';
3
3
  export declare class ServerApi {
4
4
  private client;
5
5
  constructor(client: RedseatClient);
6
+ private openSearchStream;
6
7
  getLibraries(): Promise<ILibrary[]>;
7
8
  ping(options?: {
8
9
  timeout?: number;
@@ -11,8 +12,10 @@ export declare class ServerApi {
11
12
  timeout?: number;
12
13
  }): Promise<any>;
13
14
  addLibrary(library: Partial<ILibrary>): Promise<ILibrary>;
14
- updateLibrary(libraryId: string, library: Partial<ILibrary>): Promise<ILibrary>;
15
+ updateLibrary(libraryId: string, library: ServerLibraryForUpdate): Promise<ILibrary>;
16
+ deleteLibrary(libraryId: string, deleteMediaContent?: boolean): Promise<void>;
15
17
  getPlugins(): Promise<IPlugin[]>;
18
+ searchLookupStream(query: string, type: string, callbacks: SearchStreamCallbacks<LookupSearchStreamResult>): () => void;
16
19
  getCredentials(): Promise<ICredential[]>;
17
20
  saveCredential(credential: ICredential): Promise<ICredential>;
18
21
  updateCredential(credential: ICredential): Promise<ICredential>;
package/dist/server.js CHANGED
@@ -2,6 +2,50 @@ export class ServerApi {
2
2
  constructor(client) {
3
3
  this.client = client;
4
4
  }
5
+ openSearchStream(path, params, callbacks) {
6
+ const EventSourceCtor = globalThis.EventSource;
7
+ if (!EventSourceCtor) {
8
+ callbacks.onError?.(new Error('EventSource is not available in this runtime.'));
9
+ return () => undefined;
10
+ }
11
+ const url = this.client.getFullUrl(path, {
12
+ ...params,
13
+ token: this.client.getAuthToken()
14
+ });
15
+ const source = new EventSourceCtor(url);
16
+ let closed = false;
17
+ const close = () => {
18
+ if (closed) {
19
+ return;
20
+ }
21
+ closed = true;
22
+ source.removeEventListener('results', onResults);
23
+ source.removeEventListener('finished', onFinished);
24
+ source.onerror = null;
25
+ source.close();
26
+ };
27
+ const onResults = (event) => {
28
+ try {
29
+ const payload = JSON.parse(event.data);
30
+ callbacks.onResults?.(payload);
31
+ }
32
+ catch (error) {
33
+ callbacks.onError?.(error);
34
+ close();
35
+ }
36
+ };
37
+ const onFinished = () => {
38
+ callbacks.onFinished?.();
39
+ close();
40
+ };
41
+ source.addEventListener('results', onResults);
42
+ source.addEventListener('finished', onFinished);
43
+ source.onerror = (error) => {
44
+ callbacks.onError?.(error);
45
+ close();
46
+ };
47
+ return close;
48
+ }
5
49
  async getLibraries() {
6
50
  const res = await this.client.get('/libraries');
7
51
  return res.data;
@@ -22,10 +66,16 @@ export class ServerApi {
22
66
  const res = await this.client.patch(`/libraries/${libraryId}`, library);
23
67
  return res.data;
24
68
  }
69
+ async deleteLibrary(libraryId, deleteMediaContent = false) {
70
+ await this.client.delete(`/libraries/${libraryId}?delete_media_content=${deleteMediaContent}`);
71
+ }
25
72
  async getPlugins() {
26
73
  const res = await this.client.get('/plugins');
27
74
  return res.data;
28
75
  }
76
+ searchLookupStream(query, type, callbacks) {
77
+ return this.openSearchStream('/plugins/lookup/searchstream', { q: query, type }, callbacks);
78
+ }
29
79
  async getCredentials() {
30
80
  const res = await this.client.get('/credentials');
31
81
  return res.data;
@@ -1,4 +1,4 @@
1
- import { ILibrary, IFile, IEpisode, ISerie, IMovie, IPerson, ITag, IBackup, IWatched, IUnwatched, IRsRequestProcessing, VideoConvertRequest } from './interfaces.js';
1
+ import { ILibrary, IFile, IEpisode, ISerie, IMovie, IBook, IPerson, ITag, IBackup, IWatched, IUnwatched, IRsRequestProcessing, VideoConvertRequest, type ItemWithRelations } from './interfaces.js';
2
2
  export type ElementAction = 'Added' | 'Updated' | 'Deleted';
3
3
  export type SSEConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error';
4
4
  export interface SSEConnectionOptions {
@@ -81,21 +81,28 @@ export interface SSEEpisodesEvent {
81
81
  library: string;
82
82
  episodes: {
83
83
  action: ElementAction;
84
- episode: IEpisode;
84
+ episode: ItemWithRelations<IEpisode>;
85
85
  }[];
86
86
  }
87
87
  export interface SSESeriesEvent {
88
88
  library: string;
89
89
  series: {
90
90
  action: ElementAction;
91
- serie: ISerie;
91
+ serie: ItemWithRelations<ISerie>;
92
92
  }[];
93
93
  }
94
94
  export interface SSEMoviesEvent {
95
95
  library: string;
96
96
  movies: {
97
97
  action: ElementAction;
98
- movie: IMovie;
98
+ movie: ItemWithRelations<IMovie>;
99
+ }[];
100
+ }
101
+ export interface SSEBooksEvent {
102
+ library: string;
103
+ books: {
104
+ action: ElementAction;
105
+ book: ItemWithRelations<IBook>;
99
106
  }[];
100
107
  }
101
108
  export interface SSEPeopleEvent {
@@ -187,6 +194,7 @@ export interface SSEEventMap {
187
194
  'episodes': SSEEpisodesEvent;
188
195
  'series': SSESeriesEvent;
189
196
  'movies': SSEMoviesEvent;
197
+ 'books': SSEBooksEvent;
190
198
  'people': SSEPeopleEvent;
191
199
  'tags': SSETagsEvent;
192
200
  'backups': SSEBackupsEvent;