@redseat/api 0.2.6 → 0.2.8
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/{agents.md → AGENTS.md} +275 -275
- package/CLAUDE.md +2 -0
- package/README.md +132 -132
- package/client.md +614 -318
- package/dist/client.d.ts +71 -0
- package/dist/client.js +285 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/interfaces.d.ts +9 -31
- package/dist/library.d.ts +34 -9
- package/dist/library.js +30 -10
- package/dist/sse-types.d.ts +121 -0
- package/dist/sse-types.js +1 -0
- package/encryption.md +533 -533
- package/firebase.md +602 -602
- package/libraries.md +250 -0
- package/package.json +49 -49
- package/server.md +196 -196
- package/test.md +291 -291
package/dist/client.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Method, AxiosRequestConfig } from 'axios';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
2
3
|
import { IToken } from './auth.js';
|
|
3
4
|
import { IServer } from './interfaces.js';
|
|
5
|
+
import { SSEConnectionState, SSEConnectionOptions, SSEConnectionError, SSELibraryEvent, SSELibraryStatusEvent, SSEMediasEvent, SSEMediasProgressEvent, SSEConvertProgressEvent, SSEEpisodesEvent, SSESeriesEvent, SSEMoviesEvent, SSEPeopleEvent, SSETagsEvent, SSEBackupsEvent, SSEBackupFilesEvent } from './sse-types.js';
|
|
4
6
|
export interface ClientOptions {
|
|
5
7
|
server: IServer;
|
|
6
8
|
getIdToken: () => Promise<string>;
|
|
@@ -18,6 +20,32 @@ export declare class RedseatClient {
|
|
|
18
20
|
private localServerUrl?;
|
|
19
21
|
private tokenData?;
|
|
20
22
|
private tokenRefreshPromise?;
|
|
23
|
+
private sseAbortController?;
|
|
24
|
+
private sseReconnectTimeout?;
|
|
25
|
+
private sseReconnectAttempts;
|
|
26
|
+
private sseOptions?;
|
|
27
|
+
private readonly _sseConnectionState;
|
|
28
|
+
private readonly _sseError;
|
|
29
|
+
private readonly _sseEvents;
|
|
30
|
+
readonly sseConnectionState$: Observable<SSEConnectionState>;
|
|
31
|
+
readonly sseError$: Observable<SSEConnectionError>;
|
|
32
|
+
readonly sseConnected$: Observable<boolean>;
|
|
33
|
+
readonly library$: Observable<SSELibraryEvent>;
|
|
34
|
+
readonly libraryStatus$: Observable<SSELibraryStatusEvent>;
|
|
35
|
+
readonly medias$: Observable<SSEMediasEvent>;
|
|
36
|
+
readonly mediasProgress$: Observable<SSEMediasProgressEvent>;
|
|
37
|
+
readonly convertProgress$: Observable<SSEConvertProgressEvent>;
|
|
38
|
+
readonly episodes$: Observable<SSEEpisodesEvent>;
|
|
39
|
+
readonly series$: Observable<SSESeriesEvent>;
|
|
40
|
+
readonly movies$: Observable<SSEMoviesEvent>;
|
|
41
|
+
readonly people$: Observable<SSEPeopleEvent>;
|
|
42
|
+
readonly tags$: Observable<SSETagsEvent>;
|
|
43
|
+
readonly backups$: Observable<SSEBackupsEvent>;
|
|
44
|
+
readonly backupFiles$: Observable<SSEBackupFilesEvent>;
|
|
45
|
+
/**
|
|
46
|
+
* Creates a typed observable for a specific SSE event type
|
|
47
|
+
*/
|
|
48
|
+
private createEventStream;
|
|
21
49
|
constructor(options: ClientOptions);
|
|
22
50
|
private getRegularServerUrl;
|
|
23
51
|
private detectLocalUrl;
|
|
@@ -57,4 +85,47 @@ export declare class RedseatClient {
|
|
|
57
85
|
* Returns public server info (IServer).
|
|
58
86
|
*/
|
|
59
87
|
getServer(serverId: string): Promise<IServer>;
|
|
88
|
+
/**
|
|
89
|
+
* Connects to the server's SSE endpoint for real-time updates.
|
|
90
|
+
* Automatically manages authentication and reconnection.
|
|
91
|
+
* @param options - Connection options including library filters and reconnection settings
|
|
92
|
+
*/
|
|
93
|
+
connectSSE(options?: SSEConnectionOptions): Promise<void>;
|
|
94
|
+
/**
|
|
95
|
+
* Disconnects from the SSE endpoint and cleans up resources.
|
|
96
|
+
*/
|
|
97
|
+
disconnectSSE(): void;
|
|
98
|
+
/**
|
|
99
|
+
* Disposes of all SSE resources and completes observables.
|
|
100
|
+
* Call this when the client is no longer needed.
|
|
101
|
+
*/
|
|
102
|
+
dispose(): void;
|
|
103
|
+
/**
|
|
104
|
+
* Gets the current SSE connection state
|
|
105
|
+
*/
|
|
106
|
+
get sseConnectionState(): SSEConnectionState;
|
|
107
|
+
/**
|
|
108
|
+
* Internal method to establish SSE connection
|
|
109
|
+
*/
|
|
110
|
+
private _connectSSE;
|
|
111
|
+
/**
|
|
112
|
+
* Builds the SSE endpoint URL with optional library filters
|
|
113
|
+
*/
|
|
114
|
+
private buildSSEUrl;
|
|
115
|
+
/**
|
|
116
|
+
* Processes the SSE stream and emits events
|
|
117
|
+
*/
|
|
118
|
+
private processSSEStream;
|
|
119
|
+
/**
|
|
120
|
+
* Parses the SSE buffer and extracts complete events
|
|
121
|
+
*/
|
|
122
|
+
private parseSSEBuffer;
|
|
123
|
+
/**
|
|
124
|
+
* Handles SSE errors and triggers reconnection if appropriate
|
|
125
|
+
*/
|
|
126
|
+
private handleSSEError;
|
|
127
|
+
/**
|
|
128
|
+
* Schedules a reconnection attempt with exponential backoff
|
|
129
|
+
*/
|
|
130
|
+
private scheduleReconnect;
|
|
60
131
|
}
|
package/dist/client.js
CHANGED
|
@@ -1,7 +1,36 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
+
import { BehaviorSubject, Subject, filter, map } from 'rxjs';
|
|
2
3
|
import { fetchServerToken } from './auth.js';
|
|
3
4
|
export class RedseatClient {
|
|
5
|
+
/**
|
|
6
|
+
* Creates a typed observable for a specific SSE event type
|
|
7
|
+
*/
|
|
8
|
+
createEventStream(eventName) {
|
|
9
|
+
return this._sseEvents.pipe(filter((event) => event.event === eventName), map(event => event.data));
|
|
10
|
+
}
|
|
4
11
|
constructor(options) {
|
|
12
|
+
this.sseReconnectAttempts = 0;
|
|
13
|
+
// RxJS subjects for SSE
|
|
14
|
+
this._sseConnectionState = new BehaviorSubject('disconnected');
|
|
15
|
+
this._sseError = new Subject();
|
|
16
|
+
this._sseEvents = new Subject();
|
|
17
|
+
// Public observables for SSE connection state
|
|
18
|
+
this.sseConnectionState$ = this._sseConnectionState.asObservable();
|
|
19
|
+
this.sseError$ = this._sseError.asObservable();
|
|
20
|
+
this.sseConnected$ = this._sseConnectionState.pipe(map(state => state === 'connected'));
|
|
21
|
+
// Typed event streams
|
|
22
|
+
this.library$ = this.createEventStream('library');
|
|
23
|
+
this.libraryStatus$ = this.createEventStream('library-status');
|
|
24
|
+
this.medias$ = this.createEventStream('medias');
|
|
25
|
+
this.mediasProgress$ = this.createEventStream('medias_progress');
|
|
26
|
+
this.convertProgress$ = this.createEventStream('convert_progress');
|
|
27
|
+
this.episodes$ = this.createEventStream('episodes');
|
|
28
|
+
this.series$ = this.createEventStream('series');
|
|
29
|
+
this.movies$ = this.createEventStream('movies');
|
|
30
|
+
this.people$ = this.createEventStream('people');
|
|
31
|
+
this.tags$ = this.createEventStream('tags');
|
|
32
|
+
this.backups$ = this.createEventStream('backups');
|
|
33
|
+
this.backupFiles$ = this.createEventStream('backups-files');
|
|
5
34
|
this.server = options.server;
|
|
6
35
|
this.redseatUrl = options.redseatUrl;
|
|
7
36
|
this.getIdToken = options.getIdToken;
|
|
@@ -213,4 +242,260 @@ export class RedseatClient {
|
|
|
213
242
|
});
|
|
214
243
|
return response.data;
|
|
215
244
|
}
|
|
245
|
+
// ==================== SSE Methods ====================
|
|
246
|
+
/**
|
|
247
|
+
* Connects to the server's SSE endpoint for real-time updates.
|
|
248
|
+
* Automatically manages authentication and reconnection.
|
|
249
|
+
* @param options - Connection options including library filters and reconnection settings
|
|
250
|
+
*/
|
|
251
|
+
async connectSSE(options) {
|
|
252
|
+
// Disconnect any existing connection
|
|
253
|
+
this.disconnectSSE();
|
|
254
|
+
this.sseOptions = {
|
|
255
|
+
autoReconnect: true,
|
|
256
|
+
initialReconnectDelay: 1000,
|
|
257
|
+
maxReconnectDelay: 30000,
|
|
258
|
+
...options
|
|
259
|
+
};
|
|
260
|
+
this.sseReconnectAttempts = 0;
|
|
261
|
+
await this._connectSSE();
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Disconnects from the SSE endpoint and cleans up resources.
|
|
265
|
+
*/
|
|
266
|
+
disconnectSSE() {
|
|
267
|
+
if (this.sseReconnectTimeout) {
|
|
268
|
+
clearTimeout(this.sseReconnectTimeout);
|
|
269
|
+
this.sseReconnectTimeout = undefined;
|
|
270
|
+
}
|
|
271
|
+
if (this.sseAbortController) {
|
|
272
|
+
this.sseAbortController.abort();
|
|
273
|
+
this.sseAbortController = undefined;
|
|
274
|
+
}
|
|
275
|
+
this._sseConnectionState.next('disconnected');
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Disposes of all SSE resources and completes observables.
|
|
279
|
+
* Call this when the client is no longer needed.
|
|
280
|
+
*/
|
|
281
|
+
dispose() {
|
|
282
|
+
this.disconnectSSE();
|
|
283
|
+
this._sseConnectionState.complete();
|
|
284
|
+
this._sseError.complete();
|
|
285
|
+
this._sseEvents.complete();
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Gets the current SSE connection state
|
|
289
|
+
*/
|
|
290
|
+
get sseConnectionState() {
|
|
291
|
+
return this._sseConnectionState.value;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Internal method to establish SSE connection
|
|
295
|
+
*/
|
|
296
|
+
async _connectSSE() {
|
|
297
|
+
this._sseConnectionState.next('connecting');
|
|
298
|
+
try {
|
|
299
|
+
// Ensure we have a valid token
|
|
300
|
+
await this.ensureValidToken();
|
|
301
|
+
if (!this.tokenData) {
|
|
302
|
+
throw new Error('No authentication token available');
|
|
303
|
+
}
|
|
304
|
+
const url = this.buildSSEUrl();
|
|
305
|
+
this.sseAbortController = new AbortController();
|
|
306
|
+
const response = await fetch(url, {
|
|
307
|
+
method: 'GET',
|
|
308
|
+
headers: {
|
|
309
|
+
'Authorization': `Bearer ${this.tokenData.token}`,
|
|
310
|
+
'Accept': 'text/event-stream',
|
|
311
|
+
'Cache-Control': 'no-cache'
|
|
312
|
+
},
|
|
313
|
+
signal: this.sseAbortController.signal
|
|
314
|
+
});
|
|
315
|
+
if (!response.ok) {
|
|
316
|
+
if (response.status === 401) {
|
|
317
|
+
throw Object.assign(new Error('Authentication failed'), { type: 'auth' });
|
|
318
|
+
}
|
|
319
|
+
throw Object.assign(new Error(`Server returned ${response.status}`), { type: 'server' });
|
|
320
|
+
}
|
|
321
|
+
if (!response.body) {
|
|
322
|
+
throw Object.assign(new Error('No response body'), { type: 'server' });
|
|
323
|
+
}
|
|
324
|
+
this._sseConnectionState.next('connected');
|
|
325
|
+
this.sseReconnectAttempts = 0;
|
|
326
|
+
// Process the stream
|
|
327
|
+
await this.processSSEStream(response.body);
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
331
|
+
// Intentionally disconnected
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
this.handleSSEError(error);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Builds the SSE endpoint URL with optional library filters
|
|
339
|
+
*/
|
|
340
|
+
buildSSEUrl() {
|
|
341
|
+
let url = `${this.baseUrl}/sse`;
|
|
342
|
+
if (this.sseOptions?.libraries && this.sseOptions.libraries.length > 0) {
|
|
343
|
+
const params = new URLSearchParams();
|
|
344
|
+
this.sseOptions.libraries.forEach(lib => params.append('library', lib));
|
|
345
|
+
url = `${url}?${params.toString()}`;
|
|
346
|
+
}
|
|
347
|
+
return url;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Processes the SSE stream and emits events
|
|
351
|
+
*/
|
|
352
|
+
async processSSEStream(body) {
|
|
353
|
+
const reader = body.getReader();
|
|
354
|
+
const decoder = new TextDecoder();
|
|
355
|
+
let buffer = '';
|
|
356
|
+
try {
|
|
357
|
+
while (true) {
|
|
358
|
+
const { done, value } = await reader.read();
|
|
359
|
+
if (done) {
|
|
360
|
+
// Stream ended - server closed connection
|
|
361
|
+
this._sseConnectionState.next('disconnected');
|
|
362
|
+
this.scheduleReconnect();
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
buffer += decoder.decode(value, { stream: true });
|
|
366
|
+
const { events, remainingBuffer } = this.parseSSEBuffer(buffer);
|
|
367
|
+
buffer = remainingBuffer;
|
|
368
|
+
for (const event of events) {
|
|
369
|
+
this._sseEvents.next(event);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
finally {
|
|
380
|
+
reader.releaseLock();
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Parses the SSE buffer and extracts complete events
|
|
385
|
+
*/
|
|
386
|
+
parseSSEBuffer(buffer) {
|
|
387
|
+
const events = [];
|
|
388
|
+
const lines = buffer.split('\n');
|
|
389
|
+
let currentEvent = {};
|
|
390
|
+
let processedUpTo = 0;
|
|
391
|
+
for (let i = 0; i < lines.length; i++) {
|
|
392
|
+
const line = lines[i];
|
|
393
|
+
// Check if this is an incomplete line (last line without newline)
|
|
394
|
+
if (i === lines.length - 1 && !buffer.endsWith('\n')) {
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
processedUpTo += line.length + 1; // +1 for the newline
|
|
398
|
+
if (line === '') {
|
|
399
|
+
// Empty line marks end of event
|
|
400
|
+
if (currentEvent.event && currentEvent.data !== undefined) {
|
|
401
|
+
events.push(currentEvent);
|
|
402
|
+
}
|
|
403
|
+
currentEvent = {};
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (line.startsWith(':')) {
|
|
407
|
+
// Comment, ignore
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const colonIndex = line.indexOf(':');
|
|
411
|
+
if (colonIndex === -1) {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
const field = line.slice(0, colonIndex);
|
|
415
|
+
// Value starts after colon, skip optional space after colon
|
|
416
|
+
let value = line.slice(colonIndex + 1);
|
|
417
|
+
if (value.startsWith(' ')) {
|
|
418
|
+
value = value.slice(1);
|
|
419
|
+
}
|
|
420
|
+
switch (field) {
|
|
421
|
+
case 'event':
|
|
422
|
+
currentEvent.event = value;
|
|
423
|
+
break;
|
|
424
|
+
case 'data':
|
|
425
|
+
try {
|
|
426
|
+
currentEvent.data = JSON.parse(value);
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
this._sseError.next({
|
|
430
|
+
type: 'parse',
|
|
431
|
+
message: `Failed to parse SSE data: ${value}`,
|
|
432
|
+
timestamp: Date.now()
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
break;
|
|
436
|
+
case 'id':
|
|
437
|
+
currentEvent.id = value;
|
|
438
|
+
break;
|
|
439
|
+
case 'retry':
|
|
440
|
+
currentEvent.retry = parseInt(value, 10);
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
events,
|
|
446
|
+
remainingBuffer: buffer.slice(processedUpTo)
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Handles SSE errors and triggers reconnection if appropriate
|
|
451
|
+
*/
|
|
452
|
+
handleSSEError(error) {
|
|
453
|
+
const sseError = {
|
|
454
|
+
type: 'network',
|
|
455
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
456
|
+
originalError: error instanceof Error ? error : undefined,
|
|
457
|
+
timestamp: Date.now()
|
|
458
|
+
};
|
|
459
|
+
// Determine error type
|
|
460
|
+
if (error && typeof error === 'object' && 'type' in error) {
|
|
461
|
+
sseError.type = error.type;
|
|
462
|
+
}
|
|
463
|
+
this._sseConnectionState.next('error');
|
|
464
|
+
this._sseError.next(sseError);
|
|
465
|
+
// Don't reconnect on auth errors
|
|
466
|
+
if (sseError.type === 'auth') {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
this.scheduleReconnect();
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Schedules a reconnection attempt with exponential backoff
|
|
473
|
+
*/
|
|
474
|
+
scheduleReconnect() {
|
|
475
|
+
if (!this.sseOptions?.autoReconnect) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (this.sseOptions.maxReconnectAttempts !== undefined &&
|
|
479
|
+
this.sseReconnectAttempts >= this.sseOptions.maxReconnectAttempts) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const initialDelay = this.sseOptions.initialReconnectDelay ?? 1000;
|
|
483
|
+
const maxDelay = this.sseOptions.maxReconnectDelay ?? 30000;
|
|
484
|
+
// Exponential backoff with jitter
|
|
485
|
+
const exponentialDelay = initialDelay * Math.pow(2, this.sseReconnectAttempts);
|
|
486
|
+
const jitter = Math.random() * 1000;
|
|
487
|
+
const delay = Math.min(exponentialDelay + jitter, maxDelay);
|
|
488
|
+
this._sseConnectionState.next('reconnecting');
|
|
489
|
+
this.sseReconnectAttempts++;
|
|
490
|
+
this.sseReconnectTimeout = setTimeout(async () => {
|
|
491
|
+
// Refresh token before reconnecting
|
|
492
|
+
try {
|
|
493
|
+
await this.refreshToken();
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
// Token refresh failed, will try again on next reconnect
|
|
497
|
+
}
|
|
498
|
+
this._connectSSE();
|
|
499
|
+
}, delay);
|
|
500
|
+
}
|
|
216
501
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/interfaces.d.ts
CHANGED
|
@@ -519,40 +519,18 @@ export interface RsRequest {
|
|
|
519
519
|
audio?: string[];
|
|
520
520
|
quality?: number;
|
|
521
521
|
ignoreOriginDuplicate: boolean;
|
|
522
|
-
}
|
|
523
|
-
export interface MediaDownloadUrl {
|
|
524
|
-
url: string;
|
|
525
|
-
parse: boolean;
|
|
526
|
-
uploadId?: string;
|
|
527
|
-
ignoreOriginDuplicate?: boolean;
|
|
528
|
-
kind?: FileTypes;
|
|
529
|
-
filename?: string;
|
|
530
|
-
mime?: string;
|
|
531
|
-
description?: string;
|
|
532
|
-
length?: number;
|
|
533
|
-
thumbnailUrl?: string;
|
|
534
|
-
peopleLookup?: string[];
|
|
535
|
-
seriesLookup?: string[];
|
|
536
522
|
tagsLookup?: string[];
|
|
537
|
-
|
|
538
|
-
|
|
523
|
+
peopleLookup?: string[];
|
|
524
|
+
albumsLookup?: string[];
|
|
525
|
+
thumbnailUrl?: string;
|
|
526
|
+
originUrl?: string;
|
|
527
|
+
title?: string;
|
|
528
|
+
kind?: FileTypes;
|
|
539
529
|
}
|
|
540
|
-
export interface
|
|
541
|
-
group
|
|
530
|
+
export interface RsGroupDownload {
|
|
531
|
+
group: boolean;
|
|
542
532
|
groupThumbnailUrl?: string;
|
|
543
533
|
groupFilename?: string;
|
|
544
534
|
groupMime?: string;
|
|
545
|
-
|
|
546
|
-
referer?: string;
|
|
547
|
-
headers?: string[];
|
|
548
|
-
cookies?: string[];
|
|
549
|
-
originUrl?: string;
|
|
550
|
-
title?: string;
|
|
551
|
-
ignoreOriginDuplicate?: boolean;
|
|
552
|
-
description?: string;
|
|
553
|
-
peopleLookup?: string[];
|
|
554
|
-
seriesLookup?: string[];
|
|
555
|
-
tagsLookup?: string[];
|
|
556
|
-
season?: number;
|
|
557
|
-
episode?: number;
|
|
535
|
+
requests: RsRequest[];
|
|
558
536
|
}
|
package/dist/library.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
2
|
+
import { IFile, ITag, IPerson, ISerie, IMovie, MediaRequest, IEpisode, ExternalImage, IBackupFile, ILibrary, SerieInMedia, DeletedQuery, RsDeleted, MovieSort, RsSort, SqlOrder, RsRequest, DetectedFaceResult, UnassignFaceResponse, RsGroupDownload } from './interfaces.js';
|
|
3
|
+
import { SSEMediasEvent, SSEMediasProgressEvent, SSEConvertProgressEvent, SSEEpisodesEvent, SSESeriesEvent, SSEMoviesEvent, SSEPeopleEvent, SSETagsEvent, SSELibraryStatusEvent } from './sse-types.js';
|
|
2
4
|
import { EncryptFileOptions, EncryptedFile } from './encryption.js';
|
|
3
5
|
export interface MediaForUpdate {
|
|
4
6
|
name?: string;
|
|
@@ -74,6 +76,15 @@ export interface LibraryHttpClient {
|
|
|
74
76
|
}>;
|
|
75
77
|
getFullUrl(path: string, params?: Record<string, string>): string;
|
|
76
78
|
getAuthToken(): string;
|
|
79
|
+
readonly medias$?: Observable<SSEMediasEvent>;
|
|
80
|
+
readonly mediasProgress$?: Observable<SSEMediasProgressEvent>;
|
|
81
|
+
readonly convertProgress$?: Observable<SSEConvertProgressEvent>;
|
|
82
|
+
readonly episodes$?: Observable<SSEEpisodesEvent>;
|
|
83
|
+
readonly series$?: Observable<SSESeriesEvent>;
|
|
84
|
+
readonly movies$?: Observable<SSEMoviesEvent>;
|
|
85
|
+
readonly people$?: Observable<SSEPeopleEvent>;
|
|
86
|
+
readonly tags$?: Observable<SSETagsEvent>;
|
|
87
|
+
readonly libraryStatus$?: Observable<SSELibraryStatusEvent>;
|
|
77
88
|
}
|
|
78
89
|
export declare class LibraryApi {
|
|
79
90
|
private client;
|
|
@@ -81,7 +92,27 @@ export declare class LibraryApi {
|
|
|
81
92
|
private library;
|
|
82
93
|
private key?;
|
|
83
94
|
private keyText?;
|
|
95
|
+
private disposed;
|
|
96
|
+
readonly medias$: Observable<SSEMediasEvent>;
|
|
97
|
+
readonly mediasProgress$: Observable<SSEMediasProgressEvent>;
|
|
98
|
+
readonly convertProgress$: Observable<SSEConvertProgressEvent>;
|
|
99
|
+
readonly episodes$: Observable<SSEEpisodesEvent>;
|
|
100
|
+
readonly series$: Observable<SSESeriesEvent>;
|
|
101
|
+
readonly movies$: Observable<SSEMoviesEvent>;
|
|
102
|
+
readonly people$: Observable<SSEPeopleEvent>;
|
|
103
|
+
readonly tags$: Observable<SSETagsEvent>;
|
|
104
|
+
readonly libraryStatus$: Observable<SSELibraryStatusEvent>;
|
|
84
105
|
constructor(client: LibraryHttpClient, libraryId: string, library: ILibrary);
|
|
106
|
+
/**
|
|
107
|
+
* Creates a library-filtered stream from a client stream.
|
|
108
|
+
* Returns EMPTY if the client doesn't have SSE support.
|
|
109
|
+
*/
|
|
110
|
+
private createLibraryFilteredStream;
|
|
111
|
+
/**
|
|
112
|
+
* Marks this LibraryApi as disposed.
|
|
113
|
+
* After calling dispose(), the filtered streams will stop emitting events.
|
|
114
|
+
*/
|
|
115
|
+
dispose(): void;
|
|
85
116
|
setKey(passPhrase: string): Promise<void>;
|
|
86
117
|
private getUrl;
|
|
87
118
|
getTags(query?: {
|
|
@@ -331,19 +362,13 @@ export declare class LibraryApi {
|
|
|
331
362
|
thumbMime?: string;
|
|
332
363
|
progressCallback?: (loaded: number, total: number) => void;
|
|
333
364
|
}): Promise<IFile>;
|
|
334
|
-
/**
|
|
335
|
-
* Upload a media from a request (URL-based download)
|
|
336
|
-
* @param request - The request containing URL and metadata for the media to download
|
|
337
|
-
* @returns The created media file entry
|
|
338
|
-
*/
|
|
339
|
-
uploadRequest(request: RsRequest): Promise<IFile>;
|
|
340
365
|
/**
|
|
341
366
|
* Upload a group of media files from URLs
|
|
342
|
-
* @param download - The group download request containing
|
|
367
|
+
* @param download - The group download request containing requests array
|
|
343
368
|
* @param options - Optional settings (spawn: true to run in background)
|
|
344
369
|
* @returns Array of created media files when spawn=false, or { downloading: true } when spawn=true
|
|
345
370
|
*/
|
|
346
|
-
uploadGroup(download:
|
|
371
|
+
uploadGroup(download: RsGroupDownload, options?: {
|
|
347
372
|
spawn?: boolean;
|
|
348
373
|
}): Promise<IFile[] | {
|
|
349
374
|
downloading: boolean;
|
package/dist/library.js
CHANGED
|
@@ -1,10 +1,39 @@
|
|
|
1
|
+
import { filter, EMPTY } from 'rxjs';
|
|
1
2
|
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.js';
|
|
2
3
|
import { uint8ArrayFromBase64 } from './crypto.js';
|
|
3
4
|
export class LibraryApi {
|
|
4
5
|
constructor(client, libraryId, library) {
|
|
5
6
|
this.client = client;
|
|
6
7
|
this.libraryId = libraryId;
|
|
8
|
+
this.disposed = false;
|
|
7
9
|
this.library = library;
|
|
10
|
+
// Create library-filtered streams
|
|
11
|
+
this.medias$ = this.createLibraryFilteredStream(client.medias$);
|
|
12
|
+
this.mediasProgress$ = this.createLibraryFilteredStream(client.mediasProgress$);
|
|
13
|
+
this.convertProgress$ = this.createLibraryFilteredStream(client.convertProgress$);
|
|
14
|
+
this.episodes$ = this.createLibraryFilteredStream(client.episodes$);
|
|
15
|
+
this.series$ = this.createLibraryFilteredStream(client.series$);
|
|
16
|
+
this.movies$ = this.createLibraryFilteredStream(client.movies$);
|
|
17
|
+
this.people$ = this.createLibraryFilteredStream(client.people$);
|
|
18
|
+
this.tags$ = this.createLibraryFilteredStream(client.tags$);
|
|
19
|
+
this.libraryStatus$ = this.createLibraryFilteredStream(client.libraryStatus$);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Creates a library-filtered stream from a client stream.
|
|
23
|
+
* Returns EMPTY if the client doesn't have SSE support.
|
|
24
|
+
*/
|
|
25
|
+
createLibraryFilteredStream(source$) {
|
|
26
|
+
if (!source$) {
|
|
27
|
+
return EMPTY;
|
|
28
|
+
}
|
|
29
|
+
return source$.pipe(filter(event => !this.disposed && event.library === this.libraryId));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Marks this LibraryApi as disposed.
|
|
33
|
+
* After calling dispose(), the filtered streams will stop emitting events.
|
|
34
|
+
*/
|
|
35
|
+
dispose() {
|
|
36
|
+
this.disposed = true;
|
|
8
37
|
}
|
|
9
38
|
async setKey(passPhrase) {
|
|
10
39
|
// Derive keys
|
|
@@ -824,18 +853,9 @@ export class LibraryApi {
|
|
|
824
853
|
const res = await this.client.postForm(this.getUrl('/medias'), formData, config);
|
|
825
854
|
return res.data;
|
|
826
855
|
}
|
|
827
|
-
/**
|
|
828
|
-
* Upload a media from a request (URL-based download)
|
|
829
|
-
* @param request - The request containing URL and metadata for the media to download
|
|
830
|
-
* @returns The created media file entry
|
|
831
|
-
*/
|
|
832
|
-
async uploadRequest(request) {
|
|
833
|
-
const res = await this.client.post(this.getUrl('/medias/request'), request);
|
|
834
|
-
return res.data;
|
|
835
|
-
}
|
|
836
856
|
/**
|
|
837
857
|
* Upload a group of media files from URLs
|
|
838
|
-
* @param download - The group download request containing
|
|
858
|
+
* @param download - The group download request containing requests array
|
|
839
859
|
* @param options - Optional settings (spawn: true to run in background)
|
|
840
860
|
* @returns Array of created media files when spawn=false, or { downloading: true } when spawn=true
|
|
841
861
|
*/
|