@redseat/api 0.0.1

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.
@@ -0,0 +1,589 @@
1
+ import { deriveKey, encryptText as encryptTextUtil, decryptText as decryptTextUtil, encryptBuffer, decryptBuffer, encryptFile as encryptFileUtil, decryptFile as decryptFileUtil, decryptFileThumb, encryptFilename as encryptFilenameUtil, getRandomIV as getRandomIVUtil, } from './encryption';
2
+ import { uint8ArrayFromBase64 } from './crypto';
3
+ export class LibraryApi {
4
+ constructor(client, libraryId, library) {
5
+ this.client = client;
6
+ this.libraryId = libraryId;
7
+ this.library = library;
8
+ }
9
+ async setKey(passPhrase) {
10
+ // Derive keys
11
+ const key = await deriveKey(passPhrase, 'file');
12
+ const keyText = await deriveKey(passPhrase, 'text');
13
+ // If library is encrypted, verify the key by trying to decrypt a media thumb
14
+ if (this.library.crypt === true) {
15
+ try {
16
+ // Get one media with a thumbnail
17
+ const medias = await this.getMedias({ limit: 1 });
18
+ if (medias.length === 0) {
19
+ // No medias to verify against - just store the keys
20
+ this.key = key;
21
+ this.keyText = keyText;
22
+ return;
23
+ }
24
+ const media = medias[0];
25
+ if (!media.thumbsize || media.thumbsize === 0) {
26
+ // No thumbnail to verify against - just store the keys
27
+ this.key = key;
28
+ this.keyText = keyText;
29
+ return;
30
+ }
31
+ // Check if IV is available in metadata
32
+ if (!media.iv) {
33
+ // No IV available - cannot verify key, just store the keys
34
+ this.key = key;
35
+ this.keyText = keyText;
36
+ return;
37
+ }
38
+ // Convert IV from base64 to Uint8Array
39
+ const iv = uint8ArrayFromBase64(media.iv);
40
+ // Get the encrypted thumb (already sliced, not the full file)
41
+ const encryptedThumb = await this.getThumb(media.id, { type: 'arraybuffer' });
42
+ const thumbBuffer = encryptedThumb instanceof ArrayBuffer
43
+ ? encryptedThumb
44
+ : await encryptedThumb.arrayBuffer();
45
+ // Try to decrypt the thumb using decryptBuffer (since getThumb returns the already-sliced encrypted thumb)
46
+ await decryptBuffer(key, iv, thumbBuffer);
47
+ // If decryption succeeds, store the keys
48
+ this.key = key;
49
+ this.keyText = keyText;
50
+ }
51
+ catch (error) {
52
+ // Decryption failed - wrong password
53
+ throw new Error('Invalid encryption key. Failed to decrypt media thumbnail.');
54
+ }
55
+ }
56
+ else {
57
+ // Library is not encrypted - just store the keys
58
+ this.key = key;
59
+ this.keyText = keyText;
60
+ }
61
+ console.log('Key set', this.key, this.keyText);
62
+ }
63
+ getUrl(path) {
64
+ return `/libraries/${this.libraryId}${path}`;
65
+ }
66
+ async getTags() {
67
+ const res = await this.client.get(this.getUrl('/tags'));
68
+ return res.data;
69
+ }
70
+ async getPeople() {
71
+ const res = await this.client.get(this.getUrl('/people'));
72
+ return res.data;
73
+ }
74
+ async getSeries() {
75
+ const res = await this.client.get(this.getUrl('/series'), {
76
+ params: { sort: 'name', order: 'ASC' }
77
+ });
78
+ return res.data;
79
+ }
80
+ async getMovies() {
81
+ const res = await this.client.get(this.getUrl('/movies'));
82
+ return res.data;
83
+ }
84
+ async getMedias(filter) {
85
+ const params = {};
86
+ if (filter) {
87
+ params.filter = JSON.stringify(filter);
88
+ }
89
+ const res = await this.client.get(this.getUrl('/medias'), { params });
90
+ return res.data;
91
+ }
92
+ async createTag(tag) {
93
+ const res = await this.client.post(this.getUrl('/tags'), tag);
94
+ return res.data;
95
+ }
96
+ async removeTag(tagId) {
97
+ await this.client.delete(this.getUrl(`/tags/${tagId}`));
98
+ }
99
+ async renameTag(tagId, newName) {
100
+ const res = await this.client.patch(this.getUrl(`/tags/${tagId}`), { rename: newName });
101
+ return res.data;
102
+ }
103
+ async tagChangeParent(tagId, newParent) {
104
+ const res = await this.client.patch(this.getUrl(`/tags/${tagId}`), { parent: newParent });
105
+ return res.data;
106
+ }
107
+ async tagMerge(fromId, intoId) {
108
+ const res = await this.client.patch(this.getUrl(`/tags/${fromId}/merge`), { into: intoId });
109
+ return res.data;
110
+ }
111
+ async tagAddAlt(tagId, alt) {
112
+ const res = await this.client.patch(this.getUrl(`/tags/${tagId}`), { addAlts: [alt] });
113
+ return res.data;
114
+ }
115
+ async tagRemoveAlt(tagId, alt) {
116
+ const res = await this.client.patch(this.getUrl(`/tags/${tagId}`), { removeAlts: [alt] });
117
+ return res.data;
118
+ }
119
+ async mediaTransfer(formData) {
120
+ await this.client.put(this.getUrl('/medias/transfert'), formData);
121
+ }
122
+ async mediaTransferMany(medias, deleteOriginal, toLibraryId) {
123
+ await this.client.post(this.getUrl(`/medias/transfert/${toLibraryId}`), { ids: medias, deleteOriginal });
124
+ }
125
+ async mediaTransferSingle(mediaId, toLibraryId, formData, params) {
126
+ const queryParams = {};
127
+ if (params) {
128
+ for (const [key, value] of params) {
129
+ queryParams[key] = value;
130
+ }
131
+ }
132
+ await this.client.put(this.getUrl(`/medias/${mediaId}/transfert/${toLibraryId}`), formData, { params: queryParams });
133
+ }
134
+ async removeMedias(mediaIds) {
135
+ await this.client.delete(this.getUrl('/medias'), { data: { ids: mediaIds } });
136
+ }
137
+ async getMediaMetadata(mediaId) {
138
+ const res = await this.client.get(this.getUrl(`/medias/${mediaId}`));
139
+ return res.data;
140
+ }
141
+ async getMediaBackupMetadatas(mediaId) {
142
+ const res = await this.client.get(this.getUrl(`/medias/${mediaId}/backups`));
143
+ return res.data;
144
+ }
145
+ async clean() {
146
+ const res = await this.client.get(this.getUrl('/clean'));
147
+ return res.data;
148
+ }
149
+ async locs() {
150
+ const res = await this.client.get(this.getUrl('/locs'));
151
+ return res.data;
152
+ }
153
+ async createPerson(person) {
154
+ const res = await this.client.post(this.getUrl('/people'), person);
155
+ return res.data;
156
+ }
157
+ async removePerson(personId) {
158
+ await this.client.delete(this.getUrl(`/people/${personId}`));
159
+ }
160
+ async updatePerson(personId, updates) {
161
+ const res = await this.client.patch(this.getUrl(`/people/${personId}`), updates);
162
+ return res.data;
163
+ }
164
+ async createSerie(serie) {
165
+ const res = await this.client.post(this.getUrl('/series'), serie);
166
+ return res.data;
167
+ }
168
+ async createEpisode(episode) {
169
+ const res = await this.client.post(this.getUrl('/episodes'), episode);
170
+ return res.data;
171
+ }
172
+ async serieScrap(serieId, date) {
173
+ await this.client.post(this.getUrl(`/series/${serieId}/scrap`), { date });
174
+ }
175
+ async removeSerie(serieId) {
176
+ await this.client.delete(this.getUrl(`/series/${serieId}`));
177
+ }
178
+ async updateSerie(serieId, updates) {
179
+ const res = await this.client.patch(this.getUrl(`/series/${serieId}`), updates);
180
+ return res.data;
181
+ }
182
+ async getSerieImages(serieId) {
183
+ const res = await this.client.get(this.getUrl(`/series/${serieId}/images`));
184
+ return res.data;
185
+ }
186
+ async updateSeriePoster(serieId, poster, type) {
187
+ await this.client.post(this.getUrl(`/series/${serieId}/image?type=${type}`), poster);
188
+ }
189
+ async updateSerieImageFetch(serieId, image) {
190
+ await this.client.post(this.getUrl(`/series/${serieId}/image/fetch`), image);
191
+ }
192
+ async updateSeriePosterWithMediaThumb(serieId, mediaId, type) {
193
+ const res = await this.client.post(this.getUrl(`/series/${serieId}/${type}?media=${mediaId}`), undefined);
194
+ return res.data;
195
+ }
196
+ async createMovie(movie) {
197
+ const res = await this.client.post(this.getUrl('/movies'), movie);
198
+ return res.data;
199
+ }
200
+ async movieScrap(movieId, date) {
201
+ await this.client.post(this.getUrl(`/movies/${movieId}/scrap`), { date });
202
+ }
203
+ async removeMovie(movieId) {
204
+ await this.client.delete(this.getUrl(`/movies/${movieId}`));
205
+ }
206
+ async updateMovie(movieId, updates) {
207
+ const res = await this.client.patch(this.getUrl(`/movies/${movieId}`), updates);
208
+ return res.data;
209
+ }
210
+ async getMovieImages(movieId) {
211
+ const res = await this.client.get(this.getUrl(`/movies/${movieId}/images`));
212
+ return res.data;
213
+ }
214
+ async setMovieWatched(movieId, date) {
215
+ await this.client.post(this.getUrl(`/movies/${movieId}/watched`), { date });
216
+ }
217
+ async searchMovies(name) {
218
+ const res = await this.client.get(this.getUrl(`/movies/search?name=${name}`));
219
+ return res.data;
220
+ }
221
+ async movieRename(movieId, newName) {
222
+ const res = await this.client.patch(this.getUrl(`/movies/${movieId}`), { name: newName });
223
+ return res.data;
224
+ }
225
+ async updateMoviePoster(movieId, poster, type) {
226
+ await this.client.post(this.getUrl(`/movies/${movieId}/image?type=${type}`), poster);
227
+ }
228
+ async updateMovieImageFetch(movieId, image) {
229
+ await this.client.post(this.getUrl(`/movies/${movieId}/image/fetch`), image);
230
+ }
231
+ async addTagToMedia(mediaId, tagId) {
232
+ await this.client.post(this.getUrl(`/medias/${mediaId}/tags/${tagId}`), {});
233
+ }
234
+ async removeTagFromMedia(mediaId, tagId) {
235
+ await this.client.delete(this.getUrl(`/medias/${mediaId}/tags/${tagId}`));
236
+ }
237
+ async getCryptChallenge() {
238
+ const res = await this.client.get(this.getUrl('/cryptchallenge'));
239
+ return res.data.value;
240
+ }
241
+ async getChannels() {
242
+ const res = await this.client.get(this.getUrl('/channels'));
243
+ return res.data;
244
+ }
245
+ async personRename(personId, newName) {
246
+ const res = await this.client.patch(this.getUrl(`/people/${personId}`), { name: newName });
247
+ return res.data;
248
+ }
249
+ async personUpdate(personId, updates) {
250
+ const res = await this.client.patch(this.getUrl(`/people/${personId}`), updates);
251
+ return res.data;
252
+ }
253
+ async personAddAlt(personId, alt) {
254
+ const res = await this.client.patch(this.getUrl(`/people/${personId}`), { addAlts: [alt] });
255
+ return res.data;
256
+ }
257
+ async personRemoveAlt(personId, alt) {
258
+ const res = await this.client.patch(this.getUrl(`/people/${personId}`), { removeAlts: [alt] });
259
+ return res.data;
260
+ }
261
+ async personAddSocial(personId, social) {
262
+ const res = await this.client.patch(this.getUrl(`/people/${personId}`), { addSocials: [social] });
263
+ return res.data;
264
+ }
265
+ async personRemoveSocial(personId, social) {
266
+ const res = await this.client.patch(this.getUrl(`/people/${personId}`), { removeSocials: [social] });
267
+ return res.data;
268
+ }
269
+ async serieRename(serieId, newName) {
270
+ const res = await this.client.patch(this.getUrl(`/series/${serieId}`), { name: newName });
271
+ return res.data;
272
+ }
273
+ async serieAddAlt(serieId, alt) {
274
+ const res = await this.client.patch(this.getUrl(`/series/${serieId}`), { addAlts: [alt] });
275
+ return res.data;
276
+ }
277
+ async serieRemoveAlt(serieId, alt) {
278
+ const res = await this.client.patch(this.getUrl(`/series/${serieId}`), { removeAlts: [alt] });
279
+ return res.data;
280
+ }
281
+ async updatePersonPortrait(personId, portrait) {
282
+ await this.client.post(this.getUrl(`/people/${personId}/image?size=thumb&type=poster`), portrait);
283
+ }
284
+ async searchSeries(name) {
285
+ const res = await this.client.get(this.getUrl(`/series/search?name=${name}`));
286
+ return res.data;
287
+ }
288
+ async setEpisodeWatched(serieId, season, number, date) {
289
+ await this.client.post(this.getUrl(`/series/${serieId}/seasons/${season}/episodes/${number}/watched`), { date });
290
+ }
291
+ async getUnassignedFaces(params) {
292
+ const queryParams = {};
293
+ if (params) {
294
+ for (const [key, value] of params) {
295
+ queryParams[key] = value;
296
+ }
297
+ }
298
+ const res = await this.client.get(this.getUrl('/faces/unassigned'), { params: queryParams });
299
+ return res.data;
300
+ }
301
+ async getClusters() {
302
+ const res = await this.client.get(this.getUrl('/faces/clusters'));
303
+ return res.data;
304
+ }
305
+ async assignFaces(request) {
306
+ const res = await this.client.post(this.getUrl('/faces/assign'), request);
307
+ return res.data;
308
+ }
309
+ async createPersonFromCluster(request) {
310
+ const res = await this.client.post(this.getUrl('/faces/cluster/person'), request);
311
+ return res.data;
312
+ }
313
+ async splitCluster(request) {
314
+ const res = await this.client.post(this.getUrl('/faces/cluster/split'), request);
315
+ return res.data;
316
+ }
317
+ async unassignFace(request) {
318
+ const res = await this.client.post(this.getUrl('/faces/unassign'), request);
319
+ return res.data;
320
+ }
321
+ async deleteFace(faceId) {
322
+ const res = await this.client.delete(this.getUrl(`/faces/${faceId}`));
323
+ return res.data;
324
+ }
325
+ async getAssignedFaces(personId) {
326
+ const res = await this.client.get(this.getUrl(`/people/${personId}/faces`));
327
+ return res.data;
328
+ }
329
+ async faceClusterPerson(personId) {
330
+ await this.client.post(this.getUrl(`/people/${personId}/faces/cluster`), {});
331
+ }
332
+ async mergePeople(request) {
333
+ const res = await this.client.post(this.getUrl('/people/merge'), request);
334
+ return res.data;
335
+ }
336
+ async clusterFaces(personId) {
337
+ const res = await this.client.post(this.getUrl(`/people/${personId}/faces/cluster`), {});
338
+ return res.data;
339
+ }
340
+ async getMediaPureMetadata(mediaId) {
341
+ const res = await this.client.get(this.getUrl(`/medias/${mediaId}`));
342
+ return res.data;
343
+ }
344
+ async mergeMedias(request) {
345
+ const res = await this.client.post(this.getUrl('/medias/merge'), request);
346
+ return res.data;
347
+ }
348
+ async mediaUpdateMany(update, ids) {
349
+ const res = await this.client.patch(this.getUrl('/medias'), { update, ids });
350
+ return res.data;
351
+ }
352
+ async mediaUpdateProgress(mediaId, progress) {
353
+ const res = await this.client.patch(this.getUrl(`/medias/${mediaId}/progress`), { progress });
354
+ return res.data;
355
+ }
356
+ async mediaUpdateChannel(mediaId, update) {
357
+ const res = await this.client.patch(this.getUrl(`/medias/${mediaId}/channel`), update);
358
+ return res.data;
359
+ }
360
+ async refreshMedia(mediaId) {
361
+ const res = await this.client.get(this.getUrl(`/medias/${mediaId}/metadata/refresh`));
362
+ return res.data;
363
+ }
364
+ async aiTagMedia(mediaId) {
365
+ const res = await this.client.get(this.getUrl(`/medias/${mediaId}/predict`));
366
+ return res.data;
367
+ }
368
+ async mediaUpdate(mediaId, update) {
369
+ const res = await this.client.patch(this.getUrl(`/medias/${mediaId}`), update);
370
+ return res.data;
371
+ }
372
+ async splitZip(mediaId, from, to) {
373
+ const res = await this.client.get(this.getUrl(`/medias/${mediaId}/split`), { params: { from, to } });
374
+ return res.data;
375
+ }
376
+ async deleteFromZip(mediaId, pages) {
377
+ const res = await this.client.get(this.getUrl(`/medias/${mediaId}/split`), { params: { exclude: pages.join(','), replace: 'true' } });
378
+ return res.data;
379
+ }
380
+ async updateMediaThumb(mediaId, thumb) {
381
+ await this.client.post(this.getUrl(`/medias/${mediaId}/image`), thumb);
382
+ }
383
+ async getMediaShares(mediaId) {
384
+ const res = await this.client.get(this.getUrl(`/medias/${mediaId}/shares`));
385
+ return res.data;
386
+ }
387
+ async shareMediaToProvider(mediaId, provider, formData) {
388
+ if (formData) {
389
+ const res = await this.client.post(this.getUrl(`/medias/${mediaId}/share/${provider}`), formData);
390
+ return res.data;
391
+ }
392
+ else {
393
+ const res = await this.client.get(this.getUrl(`/medias/${mediaId}/share/${provider}`));
394
+ return res.data;
395
+ }
396
+ }
397
+ async expandUrl(parsedUrl) {
398
+ const res = await this.client.post(this.getUrl('/plugins/url/expand'), parsedUrl);
399
+ return res.data;
400
+ }
401
+ async parseUrl(url) {
402
+ const res = await this.client.get(this.getUrl('/plugins/url/parse'), { params: { url } });
403
+ return res.data;
404
+ }
405
+ async listVideoConvertPlugins() {
406
+ const res = await this.client.get(this.getUrl('/plugins/videoconvert'));
407
+ return res.data;
408
+ }
409
+ async getMedia(mediaId, options) {
410
+ const type = options?.type ?? 'stream';
411
+ const res = await this.client.get(this.getUrl(`/medias/${mediaId}`), { responseType: type });
412
+ return res.data;
413
+ }
414
+ async getThumb(mediaId, options) {
415
+ const type = options?.type ?? 'stream';
416
+ const res = await this.client.get(this.getUrl(`/medias/${mediaId}/image?size=thumb`), { responseType: type });
417
+ return res.data;
418
+ }
419
+ async getWatermarks() {
420
+ const res = await this.client.get(this.getUrl('/watermarks'));
421
+ return res.data;
422
+ }
423
+ async getWatermark(watermark, options) {
424
+ const watermarkName = watermark || 'default';
425
+ const type = options?.type ?? 'blob';
426
+ const res = await this.client.get(this.getUrl(`/watermarks/${watermarkName}`), { responseType: type });
427
+ return res.data;
428
+ }
429
+ // Encryption methods
430
+ /**
431
+ * Encrypt a text string using a passphrase
432
+ * Returns format: `${base64ToBase64Url(IV)}.${base64ToBase64Url(encryptedData)}`
433
+ */
434
+ async encryptText(passPhrase, text) {
435
+ const key = await deriveKey(passPhrase, 'text');
436
+ return await encryptTextUtil(key, text);
437
+ }
438
+ /**
439
+ * Decrypt a text string using a passphrase
440
+ * Expects format: `${base64ToBase64Url(IV)}.${base64ToBase64Url(encryptedData)}`
441
+ */
442
+ async decryptText(passPhrase, encryptedText) {
443
+ const key = await deriveKey(passPhrase, 'text');
444
+ return await decryptTextUtil(key, encryptedText);
445
+ }
446
+ /**
447
+ * Encrypt a media file buffer using a passphrase
448
+ */
449
+ async encryptMedia(passPhrase, buffer, iv) {
450
+ const key = await deriveKey(passPhrase, 'file');
451
+ const usedIv = iv || getRandomIVUtil();
452
+ return await encryptBuffer(key, usedIv, buffer);
453
+ }
454
+ /**
455
+ * Decrypt a media file buffer using a passphrase
456
+ */
457
+ async decryptMedia(passPhrase, buffer, iv) {
458
+ const key = await deriveKey(passPhrase, 'file');
459
+ return await decryptBuffer(key, iv, buffer);
460
+ }
461
+ /**
462
+ * Encrypt a complete file with thumbnail and metadata
463
+ */
464
+ async encryptFile(passPhrase, options) {
465
+ const key = await deriveKey(passPhrase, 'file');
466
+ return await encryptFileUtil(key, options);
467
+ }
468
+ /**
469
+ * Decrypt a file buffer (extracts and decrypts file data from encrypted file structure)
470
+ */
471
+ async decryptFile(passPhrase, buffer) {
472
+ const key = await deriveKey(passPhrase, 'file');
473
+ return await decryptFileUtil(key, buffer);
474
+ }
475
+ /**
476
+ * Decrypt a file thumbnail (extracts and decrypts thumbnail from encrypted file structure)
477
+ */
478
+ async decryptMediaThumb(passPhrase, buffer) {
479
+ const key = await deriveKey(passPhrase, 'file');
480
+ return await decryptFileThumb(key, buffer);
481
+ }
482
+ /**
483
+ * Encrypt a filename using a passphrase
484
+ * Returns base64url encoded encrypted filename
485
+ */
486
+ async encryptFilename(passPhrase, filename, iv) {
487
+ const key = await deriveKey(passPhrase, 'file');
488
+ return await encryptFilenameUtil(key, filename, iv);
489
+ }
490
+ /**
491
+ * Generate a random 16-byte IV (Initialization Vector)
492
+ */
493
+ getRandomIV() {
494
+ return getRandomIVUtil();
495
+ }
496
+ /**
497
+ * Upload media file to the server
498
+ * @param data - The file data as ArrayBuffer or Uint8Array (clients must convert streams/buffers before calling)
499
+ * @param options - Upload options including thumbnail, metadata, mime types, and progress callback
500
+ * @returns Promise resolving to array of created IFile objects (IV will be in IFile.iv for encrypted files)
501
+ */
502
+ async uploadMedia(data, options) {
503
+ // Validate: thumbnails can only be specified for encrypted libraries
504
+ if (options.thumb && this.library.crypt !== true) {
505
+ throw new Error('Thumbnails can only be specified for encrypted libraries.');
506
+ }
507
+ // Check library configuration
508
+ if (this.library.crypt === true) {
509
+ if (!this.key) {
510
+ throw new Error('Encryption key not set. Call setKey() first.');
511
+ }
512
+ }
513
+ // Normalize inputs to ArrayBuffer
514
+ let dataBuffer;
515
+ if (data instanceof Uint8Array) {
516
+ // Create a new ArrayBuffer and copy the data
517
+ dataBuffer = data.slice().buffer;
518
+ }
519
+ else {
520
+ // Create a new Uint8Array view and get its buffer to ensure it's a proper ArrayBuffer
521
+ const view = new Uint8Array(data);
522
+ dataBuffer = view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
523
+ }
524
+ let fileBlob;
525
+ let metadata = { ...options.metadata };
526
+ let filename;
527
+ if (this.library.crypt === true && this.key) {
528
+ if (!this.keyText) {
529
+ throw new Error('Text encryption key not set. Call setKey() first.');
530
+ }
531
+ // Encryption required - use encryptFile function
532
+ // Normalize thumb buffer
533
+ let thumbBuffer;
534
+ if (options.thumb) {
535
+ if (options.thumb instanceof Uint8Array) {
536
+ thumbBuffer = options.thumb.slice().buffer;
537
+ }
538
+ else {
539
+ const view = new Uint8Array(options.thumb);
540
+ thumbBuffer = view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
541
+ }
542
+ }
543
+ else {
544
+ thumbBuffer = new ArrayBuffer(0);
545
+ }
546
+ const thumbMime = options.thumbMime || 'image/jpeg';
547
+ const originalFilename = options.metadata.name || 'file';
548
+ // Use the existing encryptFile function
549
+ const encrypted = await encryptFileUtil(this.key, {
550
+ filename: originalFilename, // This will be replaced with text-encrypted filename
551
+ buffer: dataBuffer,
552
+ thumb: thumbBuffer,
553
+ thumbMime: thumbMime,
554
+ mime: options.fileMime
555
+ });
556
+ fileBlob = new Blob([encrypted.blob], { type: options.fileMime });
557
+ // Encrypt filename using text encryption format (<iv>.<encryptedtext>)
558
+ const encryptedFilename = await encryptTextUtil(this.keyText, originalFilename);
559
+ // Update metadata with IV and encrypted filename
560
+ metadata.iv = encrypted.ivb64;
561
+ metadata.name = encryptedFilename; // Use text-encrypted filename
562
+ metadata.thumbsize = encrypted.thumbsize;
563
+ // Use encrypted filename for FormData
564
+ filename = encryptedFilename;
565
+ }
566
+ else {
567
+ // No encryption - use original filename
568
+ filename = options.metadata.name || 'file';
569
+ fileBlob = new Blob([dataBuffer], { type: options.fileMime });
570
+ }
571
+ // Create FormData
572
+ const formData = new FormData();
573
+ // Info should always be first in the FormData
574
+ formData.append('info', JSON.stringify({ ...metadata, size: fileBlob.size }));
575
+ // File should always be last in the FormData
576
+ formData.append('file', fileBlob, filename);
577
+ // Send POST request with progress tracking
578
+ const config = {};
579
+ if (options.progressCallback) {
580
+ config.onUploadProgress = (progressEvent) => {
581
+ if (progressEvent.total) {
582
+ options.progressCallback(progressEvent.loaded, progressEvent.total);
583
+ }
584
+ };
585
+ }
586
+ const res = await this.client.post(this.getUrl('/medias'), formData, config);
587
+ return res.data;
588
+ }
589
+ }
@@ -0,0 +1,13 @@
1
+ import { RedseatClient } from './client';
2
+ import { ILibrary } from './interfaces';
3
+ export declare class ServerApi {
4
+ private client;
5
+ constructor(client: RedseatClient);
6
+ getLibraries(): Promise<ILibrary[]>;
7
+ getMe(): Promise<any>;
8
+ addLibrary(library: Partial<ILibrary>): Promise<ILibrary>;
9
+ getSetting(key: string): Promise<string>;
10
+ getPlugins(): Promise<any[]>;
11
+ getCredentials(): Promise<any[]>;
12
+ addLibraryCredential(credential: any): Promise<ILibrary>;
13
+ }
package/dist/server.js ADDED
@@ -0,0 +1,33 @@
1
+ export class ServerApi {
2
+ constructor(client) {
3
+ this.client = client;
4
+ }
5
+ async getLibraries() {
6
+ const res = await this.client.get('/libraries');
7
+ return res.data;
8
+ }
9
+ async getMe() {
10
+ const res = await this.client.get('/users/me');
11
+ return res.data;
12
+ }
13
+ async addLibrary(library) {
14
+ const res = await this.client.post('/libraries', library);
15
+ return res.data;
16
+ }
17
+ async getSetting(key) {
18
+ const res = await this.client.get(`/settings/${key}`);
19
+ return res.data.value;
20
+ }
21
+ async getPlugins() {
22
+ const res = await this.client.get('/plugins');
23
+ return res.data;
24
+ }
25
+ async getCredentials() {
26
+ const res = await this.client.get('/credentials');
27
+ return res.data;
28
+ }
29
+ async addLibraryCredential(credential) {
30
+ const res = await this.client.post('/libraries/credential', credential);
31
+ return res.data;
32
+ }
33
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Helper functions for upload blob formatting
3
+ */
4
+ /**
5
+ * Convert string to ArrayBuffer
6
+ * @param str - The string to convert
7
+ * @param size - Optional size for the buffer (defaults to str.length * 2)
8
+ * @returns ArrayBuffer containing the string as UTF-16
9
+ */
10
+ export declare function str2ab(str: string, size?: number): ArrayBuffer;
11
+ /**
12
+ * Convert number to 4-byte big-endian ArrayBuffer
13
+ * @param num - The number to convert
14
+ * @returns ArrayBuffer containing the number as 4-byte big-endian
15
+ */
16
+ export declare function toBytesInt32(num: number): ArrayBuffer;
package/dist/upload.js ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Helper functions for upload blob formatting
3
+ */
4
+ /**
5
+ * Convert string to ArrayBuffer
6
+ * @param str - The string to convert
7
+ * @param size - Optional size for the buffer (defaults to str.length * 2)
8
+ * @returns ArrayBuffer containing the string as UTF-16
9
+ */
10
+ export function str2ab(str, size) {
11
+ const buf = new ArrayBuffer(size ?? str.length * 2); // 2 bytes for each char
12
+ const bufView = new Uint16Array(buf);
13
+ for (let i = 0, strLen = str.length; i < strLen; i++) {
14
+ bufView[i] = str.charCodeAt(i);
15
+ }
16
+ return buf;
17
+ }
18
+ /**
19
+ * Convert number to 4-byte big-endian ArrayBuffer
20
+ * @param num - The number to convert
21
+ * @returns ArrayBuffer containing the number as 4-byte big-endian
22
+ */
23
+ export function toBytesInt32(num) {
24
+ const arr = new ArrayBuffer(4); // an Int32 takes 4 bytes
25
+ const view = new DataView(arr);
26
+ view.setUint32(0, num, false); // byteOffset = 0; littleEndian = false (big-endian)
27
+ return arr;
28
+ }