@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.
- package/README.md +132 -0
- package/agents.md +275 -0
- package/client.md +288 -0
- package/dist/auth.d.ts +5 -0
- package/dist/auth.js +10 -0
- package/dist/client.d.ts +32 -0
- package/dist/client.js +157 -0
- package/dist/crypto.d.ts +36 -0
- package/dist/crypto.js +164 -0
- package/dist/encryption.d.ts +73 -0
- package/dist/encryption.js +238 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/interfaces.d.ts +311 -0
- package/dist/interfaces.js +38 -0
- package/dist/library.d.ts +231 -0
- package/dist/library.js +589 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.js +33 -0
- package/dist/upload.d.ts +16 -0
- package/dist/upload.js +28 -0
- package/encryption.md +533 -0
- package/libraries.md +1652 -0
- package/package.json +50 -0
- package/server.md +196 -0
- package/test.md +291 -0
package/dist/library.js
ADDED
|
@@ -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
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/upload.d.ts
ADDED
|
@@ -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
|
+
}
|