@redseat/api 0.4.6 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +19 -1
- package/dist/client.js +83 -9
- package/dist/interfaces.d.ts +46 -13
- package/dist/library.d.ts +59 -8
- package/dist/library.js +142 -14
- package/dist/server.d.ts +4 -1
- package/dist/server.js +25 -36
- package/dist/sse-fetch.d.ts +1 -0
- package/dist/sse-fetch.js +54 -44
- package/dist/sse-types.d.ts +9 -1
- package/libraries.md +50 -12
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Method, AxiosRequestConfig } from 'axios';
|
|
|
2
2
|
import { Observable } from 'rxjs';
|
|
3
3
|
import { IToken } from './auth.js';
|
|
4
4
|
import { IServer } from './interfaces.js';
|
|
5
|
-
import { SSEConnectionState, SSEConnectionOptions, SSEConnectionError, SSELibraryEvent, SSELibraryStatusEvent, SSEMediasEvent, SSEUploadProgressEvent, SSEConvertProgressEvent, SSEEpisodesEvent, SSESeriesEvent, SSEMoviesEvent, SSEBooksEvent, SSEPeopleEvent, SSETagsEvent, SSEBackupsEvent, SSEBackupFilesEvent, SSEMediaRatingEvent, SSEMediaProgressEvent, SSEPlayersListEvent, SSEWatchedEvent, SSEUnwatchedEvent, SSERequestProcessingEvent } from './sse-types.js';
|
|
5
|
+
import { SSEConnectionState, SSEConnectionOptions, SSEConnectionError, SSELibraryEvent, SSELibraryStatusEvent, SSEMediasEvent, SSEUploadProgressEvent, SSEConvertProgressEvent, SSEEpisodesEvent, SSESeriesEvent, SSEMoviesEvent, SSEBooksEvent, SSEPeopleEvent, SSETagsEvent, SSEBackupsEvent, SSEBackupFilesEvent, SSEMediaRatingEvent, SSEMediaProgressEvent, SSEPlayersListEvent, SSEWatchedEvent, SSEUnwatchedEvent, SSERequestProcessingEvent, SSEChannelsEvent } from './sse-types.js';
|
|
6
6
|
export interface ClientOptions {
|
|
7
7
|
server: IServer;
|
|
8
8
|
getIdToken: (forceRefresh?: boolean) => Promise<string>;
|
|
@@ -25,6 +25,7 @@ export declare class RedseatClient {
|
|
|
25
25
|
private sseAbortController?;
|
|
26
26
|
private sseReconnectTimeout?;
|
|
27
27
|
private sseActivityTimeout?;
|
|
28
|
+
private sseHealthCheckInterval?;
|
|
28
29
|
private sseReconnectAttempts;
|
|
29
30
|
private sseAuthReconnectAttempts;
|
|
30
31
|
private sseOptions?;
|
|
@@ -33,6 +34,7 @@ export declare class RedseatClient {
|
|
|
33
34
|
private sseLifecycleRegistered;
|
|
34
35
|
private sseHiddenAt?;
|
|
35
36
|
private readonly SSE_ACTIVITY_TIMEOUT;
|
|
37
|
+
private readonly SSE_HEALTH_CHECK_INTERVAL;
|
|
36
38
|
private readonly _sseConnectionState;
|
|
37
39
|
private readonly _sseError;
|
|
38
40
|
private readonly _sseEvents;
|
|
@@ -58,6 +60,7 @@ export declare class RedseatClient {
|
|
|
58
60
|
readonly watched$: Observable<SSEWatchedEvent>;
|
|
59
61
|
readonly unwatched$: Observable<SSEUnwatchedEvent>;
|
|
60
62
|
readonly requestProcessing$: Observable<SSERequestProcessingEvent>;
|
|
63
|
+
readonly channels$: Observable<SSEChannelsEvent>;
|
|
61
64
|
/**
|
|
62
65
|
* Creates a typed observable for a specific SSE event type.
|
|
63
66
|
* Unwraps the nested data structure from the server (e.g., {uploadProgress: {...}} -> {...})
|
|
@@ -92,6 +95,11 @@ export declare class RedseatClient {
|
|
|
92
95
|
* @throws Error if not authenticated
|
|
93
96
|
*/
|
|
94
97
|
getAuthToken(): string;
|
|
98
|
+
/**
|
|
99
|
+
* Ensures the token is valid (refreshing if needed) and returns it.
|
|
100
|
+
* Use this when you need a guaranteed-fresh token outside of axios interceptors.
|
|
101
|
+
*/
|
|
102
|
+
getValidAuthToken(): Promise<string>;
|
|
95
103
|
/**
|
|
96
104
|
* Fetches all servers for the authenticated user.
|
|
97
105
|
* Returns IServerPrivate for owned servers, IServer for shared servers.
|
|
@@ -148,6 +156,16 @@ export declare class RedseatClient {
|
|
|
148
156
|
* Clears the activity timeout timer.
|
|
149
157
|
*/
|
|
150
158
|
private clearActivityTimeout;
|
|
159
|
+
/**
|
|
160
|
+
* Starts a periodic health check that detects when the reconnect chain
|
|
161
|
+
* has silently died (no pending reconnect, not connecting, not connected)
|
|
162
|
+
* and forces a new reconnection attempt.
|
|
163
|
+
*/
|
|
164
|
+
private startSSEHealthCheck;
|
|
165
|
+
/**
|
|
166
|
+
* Stops the periodic health check.
|
|
167
|
+
*/
|
|
168
|
+
private stopSSEHealthCheck;
|
|
151
169
|
/**
|
|
152
170
|
* Processes the SSE stream and emits events
|
|
153
171
|
*/
|
package/dist/client.js
CHANGED
|
@@ -25,6 +25,7 @@ export class RedseatClient {
|
|
|
25
25
|
'people': 'people',
|
|
26
26
|
'tags': 'tags',
|
|
27
27
|
'request_processing': 'requestProcessing',
|
|
28
|
+
'channels': 'channels',
|
|
28
29
|
};
|
|
29
30
|
const wrapperKey = wrapperMap[eventName];
|
|
30
31
|
return this._sseEvents.pipe(filter((event) => event.event === eventName), map(event => {
|
|
@@ -45,6 +46,7 @@ export class RedseatClient {
|
|
|
45
46
|
this.sseIsConnecting = false;
|
|
46
47
|
this.sseLifecycleRegistered = false;
|
|
47
48
|
this.SSE_ACTIVITY_TIMEOUT = 90000; // 90 seconds - reconnect if no data received
|
|
49
|
+
this.SSE_HEALTH_CHECK_INTERVAL = 60000; // 60 seconds - periodic connection check
|
|
48
50
|
// RxJS subjects for SSE
|
|
49
51
|
this._sseConnectionState = new BehaviorSubject('disconnected');
|
|
50
52
|
this._sseError = new Subject();
|
|
@@ -73,6 +75,7 @@ export class RedseatClient {
|
|
|
73
75
|
this.watched$ = this.createEventStream('watched');
|
|
74
76
|
this.unwatched$ = this.createEventStream('unwatched');
|
|
75
77
|
this.requestProcessing$ = this.createEventStream('request_processing');
|
|
78
|
+
this.channels$ = this.createEventStream('channels');
|
|
76
79
|
// ==================== SSE Methods ====================
|
|
77
80
|
this.onDocumentVisibilityChange = () => {
|
|
78
81
|
if (!this.shouldAttemptReconnect() || typeof document === 'undefined') {
|
|
@@ -317,6 +320,14 @@ export class RedseatClient {
|
|
|
317
320
|
}
|
|
318
321
|
return this.tokenData.token;
|
|
319
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Ensures the token is valid (refreshing if needed) and returns it.
|
|
325
|
+
* Use this when you need a guaranteed-fresh token outside of axios interceptors.
|
|
326
|
+
*/
|
|
327
|
+
async getValidAuthToken() {
|
|
328
|
+
await this.ensureValidToken();
|
|
329
|
+
return this.getAuthToken();
|
|
330
|
+
}
|
|
320
331
|
/**
|
|
321
332
|
* Fetches all servers for the authenticated user.
|
|
322
333
|
* Returns IServerPrivate for owned servers, IServer for shared servers.
|
|
@@ -383,13 +394,21 @@ export class RedseatClient {
|
|
|
383
394
|
return;
|
|
384
395
|
}
|
|
385
396
|
this.clearSSEReconnectTimeout();
|
|
397
|
+
const wasConnecting = this.sseIsConnecting;
|
|
386
398
|
if (this.sseAbortController) {
|
|
387
399
|
this.sseAbortController.abort();
|
|
388
400
|
this.sseAbortController = undefined;
|
|
389
401
|
}
|
|
390
402
|
this.sseReconnectAttempts = 0;
|
|
391
403
|
this._sseConnectionState.next('reconnecting');
|
|
392
|
-
|
|
404
|
+
if (wasConnecting) {
|
|
405
|
+
// _connectSSE is mid-fetch; aborting it will make it catch AbortError and return.
|
|
406
|
+
// Schedule a reconnect so a new attempt happens after it finishes.
|
|
407
|
+
this.scheduleReconnect();
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
void this._connectSSE();
|
|
411
|
+
}
|
|
393
412
|
}
|
|
394
413
|
/**
|
|
395
414
|
* Connects to the server's SSE endpoint for real-time updates.
|
|
@@ -415,6 +434,7 @@ export class RedseatClient {
|
|
|
415
434
|
this.sseAuthReconnectAttempts = 0;
|
|
416
435
|
this.sseShouldReconnect = true;
|
|
417
436
|
this.registerSSELifecycleListeners();
|
|
437
|
+
this.startSSEHealthCheck();
|
|
418
438
|
await this._connectSSE();
|
|
419
439
|
}
|
|
420
440
|
/**
|
|
@@ -426,6 +446,7 @@ export class RedseatClient {
|
|
|
426
446
|
this.sseAuthReconnectAttempts = 0;
|
|
427
447
|
this.clearSSEReconnectTimeout();
|
|
428
448
|
this.clearActivityTimeout();
|
|
449
|
+
this.stopSSEHealthCheck();
|
|
429
450
|
if (this.sseAbortController) {
|
|
430
451
|
this.sseAbortController.abort();
|
|
431
452
|
this.sseAbortController = undefined;
|
|
@@ -528,11 +549,15 @@ export class RedseatClient {
|
|
|
528
549
|
});
|
|
529
550
|
}
|
|
530
551
|
catch (error) {
|
|
552
|
+
this.sseAbortController = undefined;
|
|
531
553
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
532
|
-
//
|
|
554
|
+
// Aborted — if we should still reconnect and no reconnect is pending, schedule one.
|
|
555
|
+
// This handles the case where forceReconnectSSE aborted us mid-fetch.
|
|
556
|
+
if (this.shouldAttemptReconnect() && !this.sseReconnectTimeout) {
|
|
557
|
+
this.scheduleReconnect();
|
|
558
|
+
}
|
|
533
559
|
return;
|
|
534
560
|
}
|
|
535
|
-
this.sseAbortController = undefined;
|
|
536
561
|
this.handleSSEError(error);
|
|
537
562
|
}
|
|
538
563
|
finally {
|
|
@@ -584,6 +609,37 @@ export class RedseatClient {
|
|
|
584
609
|
this.sseActivityTimeout = undefined;
|
|
585
610
|
}
|
|
586
611
|
}
|
|
612
|
+
/**
|
|
613
|
+
* Starts a periodic health check that detects when the reconnect chain
|
|
614
|
+
* has silently died (no pending reconnect, not connecting, not connected)
|
|
615
|
+
* and forces a new reconnection attempt.
|
|
616
|
+
*/
|
|
617
|
+
startSSEHealthCheck() {
|
|
618
|
+
this.stopSSEHealthCheck();
|
|
619
|
+
this.sseHealthCheckInterval = setInterval(() => {
|
|
620
|
+
if (this.disposed || !this.sseShouldReconnect) {
|
|
621
|
+
this.stopSSEHealthCheck();
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const state = this.sseConnectionState;
|
|
625
|
+
if (state !== 'connected' &&
|
|
626
|
+
state !== 'connecting' &&
|
|
627
|
+
!this.sseReconnectTimeout &&
|
|
628
|
+
!this.sseIsConnecting) {
|
|
629
|
+
console.log('SSE health check: connection dead with no pending reconnect, forcing reconnect');
|
|
630
|
+
this.forceReconnectSSE();
|
|
631
|
+
}
|
|
632
|
+
}, this.SSE_HEALTH_CHECK_INTERVAL);
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Stops the periodic health check.
|
|
636
|
+
*/
|
|
637
|
+
stopSSEHealthCheck() {
|
|
638
|
+
if (this.sseHealthCheckInterval) {
|
|
639
|
+
clearInterval(this.sseHealthCheckInterval);
|
|
640
|
+
this.sseHealthCheckInterval = undefined;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
587
643
|
/**
|
|
588
644
|
* Processes the SSE stream and emits events
|
|
589
645
|
*/
|
|
@@ -638,6 +694,10 @@ export class RedseatClient {
|
|
|
638
694
|
throw error;
|
|
639
695
|
}
|
|
640
696
|
finally {
|
|
697
|
+
// Ensure the underlying connection is fully closed before releasing
|
|
698
|
+
if (this.sseAbortController && !this.sseAbortController.signal.aborted) {
|
|
699
|
+
this.sseAbortController.abort();
|
|
700
|
+
}
|
|
641
701
|
this.sseAbortController = undefined;
|
|
642
702
|
reader.releaseLock();
|
|
643
703
|
}
|
|
@@ -713,15 +773,29 @@ export class RedseatClient {
|
|
|
713
773
|
}
|
|
714
774
|
catch (error) {
|
|
715
775
|
console.log('SSE reconnect: token refresh failed, scheduling another retry', error);
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
776
|
+
try {
|
|
777
|
+
this._sseError.next({
|
|
778
|
+
type: 'auth',
|
|
779
|
+
message: `Token refresh failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
780
|
+
timestamp: Date.now()
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
catch { /* prevent subscriber errors from breaking the chain */ }
|
|
721
784
|
this.scheduleReconnect();
|
|
722
785
|
return;
|
|
723
786
|
}
|
|
724
|
-
|
|
787
|
+
try {
|
|
788
|
+
await this._connectSSE();
|
|
789
|
+
}
|
|
790
|
+
catch (error) {
|
|
791
|
+
// Safety net: _connectSSE has its own try/catch, but if anything
|
|
792
|
+
// escapes (e.g. a subscriber throwing in RxJS next()), don't let
|
|
793
|
+
// it silently kill the reconnect chain.
|
|
794
|
+
console.error('SSE reconnect: unexpected error in _connectSSE', error);
|
|
795
|
+
if (this.shouldAttemptReconnect()) {
|
|
796
|
+
this.scheduleReconnect();
|
|
797
|
+
}
|
|
798
|
+
}
|
|
725
799
|
})();
|
|
726
800
|
}, delay);
|
|
727
801
|
}
|
package/dist/interfaces.d.ts
CHANGED
|
@@ -194,6 +194,7 @@ export interface ServerLibrarySettings {
|
|
|
194
194
|
preductionModel?: string;
|
|
195
195
|
mapProgress?: UserMapping[];
|
|
196
196
|
dataPath?: string;
|
|
197
|
+
epgUrl?: string;
|
|
197
198
|
}
|
|
198
199
|
export interface ServerLibraryForUpdate {
|
|
199
200
|
name?: string;
|
|
@@ -202,6 +203,7 @@ export interface ServerLibraryForUpdate {
|
|
|
202
203
|
settings?: ServerLibrarySettings;
|
|
203
204
|
credentials?: string;
|
|
204
205
|
plugin?: string;
|
|
206
|
+
password?: string;
|
|
205
207
|
}
|
|
206
208
|
export interface ILibrary {
|
|
207
209
|
id?: string;
|
|
@@ -216,6 +218,7 @@ export interface ILibrary {
|
|
|
216
218
|
plugin?: string;
|
|
217
219
|
hidden?: boolean;
|
|
218
220
|
status?: string;
|
|
221
|
+
password?: string;
|
|
219
222
|
}
|
|
220
223
|
export interface ITag {
|
|
221
224
|
id: string;
|
|
@@ -451,6 +454,7 @@ export interface ExternalImage {
|
|
|
451
454
|
voteAverage?: number;
|
|
452
455
|
voteCount?: number;
|
|
453
456
|
width?: number;
|
|
457
|
+
matchType?: RsLookupMatchType;
|
|
454
458
|
}
|
|
455
459
|
export interface Relations {
|
|
456
460
|
peopleDetails?: IPerson[];
|
|
@@ -478,6 +482,7 @@ export interface SearchRelations {
|
|
|
478
482
|
export interface SearchStreamResultBase {
|
|
479
483
|
relations?: SearchRelations;
|
|
480
484
|
images?: ExternalImage[];
|
|
485
|
+
matchType?: RsLookupMatchType;
|
|
481
486
|
}
|
|
482
487
|
export type SearchStreamResult<K extends string, T> = SearchStreamResultBase & {
|
|
483
488
|
metadata?: Partial<Record<K, T>>;
|
|
@@ -486,30 +491,60 @@ export type SearchResult<K extends string, T> = SearchStreamResult<K, T>;
|
|
|
486
491
|
export type BookSearchResult = SearchResult<'book', IBook>;
|
|
487
492
|
export type MovieSearchResult = SearchResult<'movie', IMovie>;
|
|
488
493
|
export type SerieSearchResult = SearchResult<'serie', ISerie>;
|
|
494
|
+
export type PersonSearchResult = SearchResult<'person', IPerson>;
|
|
489
495
|
export type BookSearchStreamResult = SearchStreamResult<'book', IBook>;
|
|
490
496
|
export type MovieSearchStreamResult = SearchStreamResult<'movie', IMovie>;
|
|
491
497
|
export type SerieSearchStreamResult = SearchStreamResult<'serie', ISerie>;
|
|
498
|
+
export type PersonSearchStreamResult = SearchStreamResult<'person', IPerson>;
|
|
492
499
|
export type LookupSearchStreamResult = SearchStreamResult<'book' | 'movie' | 'serie', IBook | IMovie | ISerie>;
|
|
493
|
-
export type
|
|
500
|
+
export type RsLookupMatchType = 'exactId' | 'exactText';
|
|
501
|
+
export interface SourceSearchStreamResult {
|
|
502
|
+
request: RsRequest;
|
|
503
|
+
matchType?: RsLookupMatchType;
|
|
504
|
+
}
|
|
505
|
+
export interface SseSearchStreamEvent<T> {
|
|
506
|
+
sourceId: string;
|
|
507
|
+
sourceName: string;
|
|
508
|
+
results: T[];
|
|
509
|
+
nextPageKey?: string | null;
|
|
510
|
+
}
|
|
494
511
|
export interface SearchStreamCallbacks<T> {
|
|
495
|
-
onResults?: (
|
|
512
|
+
onResults?: (event: SseSearchStreamEvent<T>) => void;
|
|
496
513
|
onFinished?: () => void;
|
|
497
514
|
onError?: (error: unknown) => void;
|
|
498
515
|
}
|
|
499
516
|
export interface IChannel {
|
|
500
517
|
id: string;
|
|
501
518
|
name: string;
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
519
|
+
tvgId?: string;
|
|
520
|
+
logo?: string;
|
|
521
|
+
tags?: string[];
|
|
522
|
+
channelNumber?: number;
|
|
523
|
+
modified?: number;
|
|
524
|
+
added?: number;
|
|
525
|
+
variants?: IChannelVariant[];
|
|
506
526
|
}
|
|
507
|
-
export interface
|
|
527
|
+
export interface IChannelVariant {
|
|
508
528
|
id: string;
|
|
509
|
-
|
|
510
|
-
source: string;
|
|
529
|
+
channelRef: string;
|
|
511
530
|
quality?: string;
|
|
512
|
-
|
|
531
|
+
streamUrl: string;
|
|
532
|
+
modified?: number;
|
|
533
|
+
added?: number;
|
|
534
|
+
}
|
|
535
|
+
export interface M3uImportResult {
|
|
536
|
+
channelsAdded: number;
|
|
537
|
+
channelsUpdated: number;
|
|
538
|
+
channelsRemoved: number;
|
|
539
|
+
moviesAdded: number;
|
|
540
|
+
seriesAdded: number;
|
|
541
|
+
episodesAdded: number;
|
|
542
|
+
groupsCreated: number;
|
|
543
|
+
totalParsed: number;
|
|
544
|
+
}
|
|
545
|
+
export interface ChannelQuery {
|
|
546
|
+
tag?: string;
|
|
547
|
+
name?: string;
|
|
513
548
|
}
|
|
514
549
|
export interface UnassignedFace {
|
|
515
550
|
id: string;
|
|
@@ -566,9 +601,6 @@ export interface MergePeopleResponse {
|
|
|
566
601
|
export interface ClusterFacesResponse {
|
|
567
602
|
clusters: number;
|
|
568
603
|
}
|
|
569
|
-
export interface IChannelUpdate {
|
|
570
|
-
[key: string]: any;
|
|
571
|
-
}
|
|
572
604
|
export declare enum PluginAuthType {
|
|
573
605
|
OAUTH = "oauth",
|
|
574
606
|
URL = "url",
|
|
@@ -927,6 +959,7 @@ export interface RsGroupDownload {
|
|
|
927
959
|
groupMime?: string;
|
|
928
960
|
requests: RsRequest[];
|
|
929
961
|
infos?: MediaForUpdate;
|
|
962
|
+
matchType?: RsLookupMatchType;
|
|
930
963
|
}
|
|
931
964
|
/**
|
|
932
965
|
* Request processing status from plugin-based download/processing.
|
package/dist/library.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Observable } from 'rxjs';
|
|
2
2
|
import type { AxiosResponse } from 'axios';
|
|
3
|
-
import { IFile, ITag, IPerson, ISerie, IMovie, IBook, MediaRequest, IEpisode, ExternalImage, IBackupFile, ILibrary, DeletedQuery, RsDeleted, MovieSort, BookSort, RsSort, SqlOrder, RsRequest, DetectedFaceResult, UnassignFaceResponse, RsGroupDownload, IViewProgress, IWatched, IRsRequestProcessing, BookSearchStreamResult, MovieSearchStreamResult, SerieSearchStreamResult, BookSearchResult, MovieSearchResult, SerieSearchResult, ItemWithRelations, SearchStreamCallbacks, MediaForUpdate,
|
|
4
|
-
import { SSEMediasEvent, SSEUploadProgressEvent, SSEConvertProgressEvent, SSEEpisodesEvent, SSESeriesEvent, SSEMoviesEvent, SSEBooksEvent, 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, PersonSearchStreamResult, LookupSearchStreamResult, SourceSearchStreamResult, BookSearchResult, MovieSearchResult, SerieSearchResult, ItemWithRelations, SearchStreamCallbacks, MediaForUpdate, type IChannel, type M3uImportResult, type ChannelQuery } from './interfaces.js';
|
|
4
|
+
import { SSEMediasEvent, SSEUploadProgressEvent, SSEConvertProgressEvent, SSEEpisodesEvent, SSESeriesEvent, SSEMoviesEvent, SSEBooksEvent, SSEPeopleEvent, SSETagsEvent, SSELibraryStatusEvent, SSEMediaRatingEvent, SSEMediaProgressEvent, SSERequestProcessingEvent, SSEChannelsEvent } from './sse-types.js';
|
|
5
5
|
import { EncryptFileOptions, EncryptedFile } from './encryption.js';
|
|
6
6
|
export interface LibraryHttpClient {
|
|
7
7
|
get<T = unknown>(url: string, config?: any): Promise<{
|
|
@@ -37,6 +37,7 @@ export interface LibraryHttpClient {
|
|
|
37
37
|
readonly mediaRating$?: Observable<SSEMediaRatingEvent>;
|
|
38
38
|
readonly mediaProgress$?: Observable<SSEMediaProgressEvent>;
|
|
39
39
|
readonly requestProcessing$?: Observable<SSERequestProcessingEvent>;
|
|
40
|
+
readonly channels$?: Observable<SSEChannelsEvent>;
|
|
40
41
|
}
|
|
41
42
|
export interface UploadMediaMultipartOptions {
|
|
42
43
|
file: Blob;
|
|
@@ -64,6 +65,7 @@ export declare class LibraryApi {
|
|
|
64
65
|
readonly mediaRating$: Observable<SSEMediaRatingEvent>;
|
|
65
66
|
readonly mediaProgress$: Observable<SSEMediaProgressEvent>;
|
|
66
67
|
readonly requestProcessing$: Observable<SSERequestProcessingEvent>;
|
|
68
|
+
readonly channels$: Observable<SSEChannelsEvent>;
|
|
67
69
|
constructor(client: LibraryHttpClient, libraryId: string, library: ILibrary);
|
|
68
70
|
/**
|
|
69
71
|
* Creates a library-filtered stream from a client stream.
|
|
@@ -78,6 +80,10 @@ export declare class LibraryApi {
|
|
|
78
80
|
setKey(passPhrase: string): Promise<void>;
|
|
79
81
|
private getUrl;
|
|
80
82
|
private openSearchStream;
|
|
83
|
+
searchAllStream(name: string, callbacks: SearchStreamCallbacks<LookupSearchStreamResult>, options?: {
|
|
84
|
+
source?: string[];
|
|
85
|
+
pageKey?: string;
|
|
86
|
+
}): () => void;
|
|
81
87
|
getTags(query?: {
|
|
82
88
|
name?: string;
|
|
83
89
|
parent?: string;
|
|
@@ -141,6 +147,7 @@ export declare class LibraryApi {
|
|
|
141
147
|
createPerson(person: Partial<IPerson>): Promise<IPerson>;
|
|
142
148
|
removePerson(personId: string): Promise<void>;
|
|
143
149
|
updatePerson(personId: string, updates: Partial<IPerson>): Promise<IPerson>;
|
|
150
|
+
getPerson(personId: string): Promise<IPerson>;
|
|
144
151
|
createSerie(serie: Partial<ISerie>): Promise<ISerie>;
|
|
145
152
|
createEpisode(episode: Partial<IEpisode>): Promise<IEpisode>;
|
|
146
153
|
serieScrap(serieId: string, date?: number): Promise<void>;
|
|
@@ -160,7 +167,10 @@ export declare class LibraryApi {
|
|
|
160
167
|
getMovieProgress(movieId: string): Promise<IViewProgress>;
|
|
161
168
|
setMovieProgress(movieId: string, progress: number): Promise<void>;
|
|
162
169
|
searchMovies(name: string): Promise<MovieSearchResult[]>;
|
|
163
|
-
searchMoviesStream(name: string, callbacks: SearchStreamCallbacks<MovieSearchStreamResult
|
|
170
|
+
searchMoviesStream(name: string, callbacks: SearchStreamCallbacks<MovieSearchStreamResult>, options?: {
|
|
171
|
+
source?: string[];
|
|
172
|
+
pageKey?: string;
|
|
173
|
+
}): () => void;
|
|
164
174
|
movieRename(movieId: string, newName: string): Promise<IMovie>;
|
|
165
175
|
updateMoviePoster(movieId: string, poster: FormData, type: string): Promise<void>;
|
|
166
176
|
updateMovieImageFetch(movieId: string, image: ExternalImage): Promise<void>;
|
|
@@ -180,7 +190,10 @@ export declare class LibraryApi {
|
|
|
180
190
|
removeBook(bookId: string): Promise<void>;
|
|
181
191
|
updateBook(bookId: string, updates: Partial<IBook>): Promise<IBook>;
|
|
182
192
|
searchBooks(name: string): Promise<BookSearchResult[]>;
|
|
183
|
-
searchBooksStream(name: string, callbacks: SearchStreamCallbacks<BookSearchStreamResult
|
|
193
|
+
searchBooksStream(name: string, callbacks: SearchStreamCallbacks<BookSearchStreamResult>, options?: {
|
|
194
|
+
source?: string[];
|
|
195
|
+
pageKey?: string;
|
|
196
|
+
}): () => void;
|
|
184
197
|
bookRename(bookId: string, newName: string): Promise<IBook>;
|
|
185
198
|
getBookImages(bookId: string): Promise<ExternalImage[]>;
|
|
186
199
|
updateBookPoster(bookId: string, poster: FormData): Promise<void>;
|
|
@@ -188,19 +201,39 @@ export declare class LibraryApi {
|
|
|
188
201
|
addTagToMedia(mediaId: string, tagId: string): Promise<void>;
|
|
189
202
|
removeTagFromMedia(mediaId: string, tagId: string): Promise<void>;
|
|
190
203
|
getCryptChallenge(): Promise<string>;
|
|
191
|
-
getChannels(): Promise<
|
|
204
|
+
getChannels(query?: ChannelQuery): Promise<IChannel[]>;
|
|
205
|
+
getChannel(channelId: string): Promise<IChannel>;
|
|
206
|
+
deleteChannel(channelId: string): Promise<void>;
|
|
207
|
+
importChannels(url?: string): Promise<M3uImportResult>;
|
|
208
|
+
refreshChannels(): Promise<M3uImportResult>;
|
|
209
|
+
getChannelStreamUrl(channelId: string, quality?: string): string;
|
|
210
|
+
getChannelHlsUrl(channelId: string, quality?: string): string;
|
|
211
|
+
stopChannelHls(channelId: string): Promise<void>;
|
|
212
|
+
getAuthToken(): string;
|
|
213
|
+
addTagToChannel(channelId: string, tagId: string): Promise<void>;
|
|
214
|
+
removeTagFromChannel(channelId: string, tagId: string): Promise<void>;
|
|
215
|
+
getChannelImageUrl(channelId: string, size?: string): string;
|
|
192
216
|
personRename(personId: string, newName: string): Promise<IPerson>;
|
|
193
217
|
personUpdate(personId: string, updates: Partial<IPerson>): Promise<IPerson>;
|
|
194
218
|
personAddAlt(personId: string, alt: string): Promise<IPerson>;
|
|
195
219
|
personRemoveAlt(personId: string, alt: string): Promise<IPerson>;
|
|
196
220
|
personAddSocial(personId: string, social: any): Promise<IPerson>;
|
|
197
221
|
personRemoveSocial(personId: string, social: any): Promise<IPerson>;
|
|
222
|
+
personAddOtherId(personId: string, otherId: string): Promise<IPerson>;
|
|
223
|
+
personRemoveOtherId(personId: string, otherId: string): Promise<IPerson>;
|
|
198
224
|
serieRename(serieId: string, newName: string): Promise<ISerie>;
|
|
199
225
|
serieAddAlt(serieId: string, alt: string): Promise<ISerie>;
|
|
200
226
|
serieRemoveAlt(serieId: string, alt: string): Promise<ISerie>;
|
|
201
227
|
updatePersonPortrait(personId: string, portrait: FormData): Promise<void>;
|
|
228
|
+
searchPeopleStream(name: string, callbacks: SearchStreamCallbacks<PersonSearchStreamResult>, options?: {
|
|
229
|
+
source?: string[];
|
|
230
|
+
pageKey?: string;
|
|
231
|
+
}): () => void;
|
|
202
232
|
searchSeries(name: string): Promise<SerieSearchResult[]>;
|
|
203
|
-
searchSeriesStream(name: string, callbacks: SearchStreamCallbacks<SerieSearchStreamResult
|
|
233
|
+
searchSeriesStream(name: string, callbacks: SearchStreamCallbacks<SerieSearchStreamResult>, options?: {
|
|
234
|
+
source?: string[];
|
|
235
|
+
pageKey?: string;
|
|
236
|
+
}): () => void;
|
|
204
237
|
setEpisodeWatched(serieId: string, season: number, number: number, date: number): Promise<void>;
|
|
205
238
|
getEpisodeWatched(serieId: string, season: number, episode: number): Promise<IWatched>;
|
|
206
239
|
getEpisodeProgress(serieId: string, season: number, episode: number): Promise<IViewProgress>;
|
|
@@ -229,6 +262,22 @@ export declare class LibraryApi {
|
|
|
229
262
|
* @returns Array of grouped download requests. Use `.flatMap(g => g.requests)` to get a flat list of RsRequest items.
|
|
230
263
|
*/
|
|
231
264
|
searchMovieMedias(movieId: string, q?: string): Promise<RsGroupDownload[]>;
|
|
265
|
+
searchEpisodeMediasStream(serieId: string, season: number, episode: number, callbacks: SearchStreamCallbacks<SourceSearchStreamResult>, options?: {
|
|
266
|
+
source?: string[];
|
|
267
|
+
pageKey?: string;
|
|
268
|
+
}): () => void;
|
|
269
|
+
searchSeasonMediasStream(serieId: string, season: number, callbacks: SearchStreamCallbacks<SourceSearchStreamResult>, options?: {
|
|
270
|
+
source?: string[];
|
|
271
|
+
pageKey?: string;
|
|
272
|
+
}): () => void;
|
|
273
|
+
searchMovieMediasStream(movieId: string, callbacks: SearchStreamCallbacks<SourceSearchStreamResult>, options?: {
|
|
274
|
+
source?: string[];
|
|
275
|
+
pageKey?: string;
|
|
276
|
+
}): () => void;
|
|
277
|
+
searchBookMediasStream(bookId: string, callbacks: SearchStreamCallbacks<SourceSearchStreamResult>, options?: {
|
|
278
|
+
source?: string[];
|
|
279
|
+
pageKey?: string;
|
|
280
|
+
}): () => void;
|
|
232
281
|
/**
|
|
233
282
|
* Adds a searched episode media to the database for download/storage.
|
|
234
283
|
* @param serieId - The series identifier
|
|
@@ -372,7 +421,6 @@ export declare class LibraryApi {
|
|
|
372
421
|
mediaUpdateProgress(mediaId: string, progress: number): Promise<{
|
|
373
422
|
progress: number;
|
|
374
423
|
}>;
|
|
375
|
-
mediaUpdateChannel(mediaId: string, update: IChannelUpdate): Promise<IFile[]>;
|
|
376
424
|
refreshMedia(mediaId: string): Promise<IFile[]>;
|
|
377
425
|
aiTagMedia(mediaId: string): Promise<IFile[]>;
|
|
378
426
|
mediaUpdate(mediaId: string, update: MediaForUpdate): Promise<IFile[]>;
|
|
@@ -456,11 +504,14 @@ export declare class LibraryApi {
|
|
|
456
504
|
* Upload a group of media files from URLs
|
|
457
505
|
* @param download - The group download request containing requests array
|
|
458
506
|
* @param options - Optional settings (spawn: true to run in background)
|
|
459
|
-
* @returns Array of created media files when spawn=
|
|
507
|
+
* @returns Array of created media files, { downloading: true } when spawn=true, or { needFileSelection, request } when file selection is required
|
|
460
508
|
*/
|
|
461
509
|
uploadGroup(download: RsGroupDownload, options?: {
|
|
462
510
|
spawn?: boolean;
|
|
463
511
|
}): Promise<IFile[] | {
|
|
464
512
|
downloading: boolean;
|
|
513
|
+
} | {
|
|
514
|
+
needFileSelection: true;
|
|
515
|
+
request: RsRequest;
|
|
465
516
|
}>;
|
|
466
517
|
}
|
package/dist/library.js
CHANGED
|
@@ -22,6 +22,7 @@ export class LibraryApi {
|
|
|
22
22
|
this.mediaRating$ = this.createLibraryFilteredStream(client.mediaRating$);
|
|
23
23
|
this.mediaProgress$ = this.createLibraryFilteredStream(client.mediaProgress$);
|
|
24
24
|
this.requestProcessing$ = this.createLibraryFilteredStream(client.requestProcessing$);
|
|
25
|
+
this.channels$ = this.createLibraryFilteredStream(client.channels$);
|
|
25
26
|
}
|
|
26
27
|
/**
|
|
27
28
|
* Creates a library-filtered stream from a client stream.
|
|
@@ -124,6 +125,14 @@ export class LibraryApi {
|
|
|
124
125
|
}
|
|
125
126
|
}, finish, (error) => callbacks.onError?.(error));
|
|
126
127
|
}
|
|
128
|
+
searchAllStream(name, callbacks, options) {
|
|
129
|
+
const params = { name };
|
|
130
|
+
if (options?.source?.length)
|
|
131
|
+
params.source = options.source.join(',');
|
|
132
|
+
if (options?.pageKey)
|
|
133
|
+
params.pageKey = options.pageKey;
|
|
134
|
+
return this.openSearchStream(this.getUrl('/searchstream'), params, callbacks);
|
|
135
|
+
}
|
|
127
136
|
async getTags(query) {
|
|
128
137
|
const params = {};
|
|
129
138
|
if (query) {
|
|
@@ -357,6 +366,10 @@ export class LibraryApi {
|
|
|
357
366
|
const res = await this.client.patch(this.getUrl(`/people/${personId}`), updates);
|
|
358
367
|
return res.data;
|
|
359
368
|
}
|
|
369
|
+
async getPerson(personId) {
|
|
370
|
+
const res = await this.client.get(this.getUrl(`/people/${personId}`));
|
|
371
|
+
return res.data;
|
|
372
|
+
}
|
|
360
373
|
async createSerie(serie) {
|
|
361
374
|
const res = await this.client.post(this.getUrl('/series'), serie);
|
|
362
375
|
return res.data;
|
|
@@ -425,8 +438,13 @@ export class LibraryApi {
|
|
|
425
438
|
const res = await this.client.get(this.getUrl(`/movies/search?name=${name}`));
|
|
426
439
|
return res.data;
|
|
427
440
|
}
|
|
428
|
-
searchMoviesStream(name, callbacks) {
|
|
429
|
-
|
|
441
|
+
searchMoviesStream(name, callbacks, options) {
|
|
442
|
+
const params = { name };
|
|
443
|
+
if (options?.source?.length)
|
|
444
|
+
params.source = options.source.join(',');
|
|
445
|
+
if (options?.pageKey)
|
|
446
|
+
params.pageKey = options.pageKey;
|
|
447
|
+
return this.openSearchStream(this.getUrl('/movies/searchstream'), params, callbacks);
|
|
430
448
|
}
|
|
431
449
|
async movieRename(movieId, newName) {
|
|
432
450
|
const res = await this.client.patch(this.getUrl(`/movies/${movieId}`), {
|
|
@@ -493,8 +511,13 @@ export class LibraryApi {
|
|
|
493
511
|
const res = await this.client.get(this.getUrl(`/books/search?name=${name}`));
|
|
494
512
|
return res.data;
|
|
495
513
|
}
|
|
496
|
-
searchBooksStream(name, callbacks) {
|
|
497
|
-
|
|
514
|
+
searchBooksStream(name, callbacks, options) {
|
|
515
|
+
const params = { name };
|
|
516
|
+
if (options?.source?.length)
|
|
517
|
+
params.source = options.source.join(',');
|
|
518
|
+
if (options?.pageKey)
|
|
519
|
+
params.pageKey = options.pageKey;
|
|
520
|
+
return this.openSearchStream(this.getUrl('/books/searchstream'), params, callbacks);
|
|
498
521
|
}
|
|
499
522
|
async bookRename(bookId, newName) {
|
|
500
523
|
const res = await this.client.patch(this.getUrl(`/books/${bookId}`), {
|
|
@@ -522,10 +545,62 @@ export class LibraryApi {
|
|
|
522
545
|
const res = await this.client.get(this.getUrl('/cryptchallenge'));
|
|
523
546
|
return res.data.value;
|
|
524
547
|
}
|
|
525
|
-
async getChannels() {
|
|
526
|
-
const
|
|
548
|
+
async getChannels(query) {
|
|
549
|
+
const params = {};
|
|
550
|
+
if (query?.tag)
|
|
551
|
+
params.tag = query.tag;
|
|
552
|
+
if (query?.name)
|
|
553
|
+
params.name = query.name;
|
|
554
|
+
const res = await this.client.get(this.getUrl('/channels'), { params });
|
|
555
|
+
return res.data;
|
|
556
|
+
}
|
|
557
|
+
async getChannel(channelId) {
|
|
558
|
+
const res = await this.client.get(this.getUrl(`/channels/${channelId}`));
|
|
559
|
+
return res.data;
|
|
560
|
+
}
|
|
561
|
+
async deleteChannel(channelId) {
|
|
562
|
+
await this.client.delete(this.getUrl(`/channels/${channelId}`));
|
|
563
|
+
}
|
|
564
|
+
async importChannels(url) {
|
|
565
|
+
const res = await this.client.post(this.getUrl('/channels/import'), { url });
|
|
566
|
+
return res.data;
|
|
567
|
+
}
|
|
568
|
+
async refreshChannels() {
|
|
569
|
+
const res = await this.client.post(this.getUrl('/channels/refresh'), {});
|
|
527
570
|
return res.data;
|
|
528
571
|
}
|
|
572
|
+
getChannelStreamUrl(channelId, quality) {
|
|
573
|
+
const token = this.client.getAuthToken();
|
|
574
|
+
const params = { token };
|
|
575
|
+
if (quality)
|
|
576
|
+
params.quality = quality;
|
|
577
|
+
return this.client.getFullUrl(this.getUrl(`/channels/${channelId}/stream`), params);
|
|
578
|
+
}
|
|
579
|
+
getChannelHlsUrl(channelId, quality) {
|
|
580
|
+
const token = this.client.getAuthToken();
|
|
581
|
+
const params = { token };
|
|
582
|
+
if (quality)
|
|
583
|
+
params.quality = quality;
|
|
584
|
+
return this.client.getFullUrl(this.getUrl(`/channels/${channelId}/hls/playlist.m3u8`), params);
|
|
585
|
+
}
|
|
586
|
+
async stopChannelHls(channelId) {
|
|
587
|
+
await this.client.delete(this.getUrl(`/channels/${channelId}/hls`));
|
|
588
|
+
}
|
|
589
|
+
getAuthToken() {
|
|
590
|
+
return this.client.getAuthToken();
|
|
591
|
+
}
|
|
592
|
+
async addTagToChannel(channelId, tagId) {
|
|
593
|
+
await this.client.post(this.getUrl(`/channels/${channelId}/tags`), { tagId });
|
|
594
|
+
}
|
|
595
|
+
async removeTagFromChannel(channelId, tagId) {
|
|
596
|
+
await this.client.delete(this.getUrl(`/channels/${channelId}/tags/${tagId}`));
|
|
597
|
+
}
|
|
598
|
+
getChannelImageUrl(channelId, size) {
|
|
599
|
+
const params = {};
|
|
600
|
+
if (size)
|
|
601
|
+
params.size = size;
|
|
602
|
+
return this.client.getFullUrl(this.getUrl(`/channels/${channelId}/image`), params);
|
|
603
|
+
}
|
|
529
604
|
async personRename(personId, newName) {
|
|
530
605
|
const res = await this.client.patch(this.getUrl(`/people/${personId}`), {
|
|
531
606
|
name: newName
|
|
@@ -560,6 +635,18 @@ export class LibraryApi {
|
|
|
560
635
|
});
|
|
561
636
|
return res.data;
|
|
562
637
|
}
|
|
638
|
+
async personAddOtherId(personId, otherId) {
|
|
639
|
+
const res = await this.client.patch(this.getUrl(`/people/${personId}`), {
|
|
640
|
+
addOtherids: [otherId]
|
|
641
|
+
});
|
|
642
|
+
return res.data;
|
|
643
|
+
}
|
|
644
|
+
async personRemoveOtherId(personId, otherId) {
|
|
645
|
+
const res = await this.client.patch(this.getUrl(`/people/${personId}`), {
|
|
646
|
+
removeOtherids: [otherId]
|
|
647
|
+
});
|
|
648
|
+
return res.data;
|
|
649
|
+
}
|
|
563
650
|
async serieRename(serieId, newName) {
|
|
564
651
|
const res = await this.client.patch(this.getUrl(`/series/${serieId}`), {
|
|
565
652
|
name: newName
|
|
@@ -581,12 +668,25 @@ export class LibraryApi {
|
|
|
581
668
|
async updatePersonPortrait(personId, portrait) {
|
|
582
669
|
await this.client.post(this.getUrl(`/people/${personId}/image?size=thumb&type=poster`), portrait);
|
|
583
670
|
}
|
|
671
|
+
searchPeopleStream(name, callbacks, options) {
|
|
672
|
+
const params = { name };
|
|
673
|
+
if (options?.source?.length)
|
|
674
|
+
params.source = options.source.join(',');
|
|
675
|
+
if (options?.pageKey)
|
|
676
|
+
params.pageKey = options.pageKey;
|
|
677
|
+
return this.openSearchStream(this.getUrl('/people/searchstream'), params, callbacks);
|
|
678
|
+
}
|
|
584
679
|
async searchSeries(name) {
|
|
585
680
|
const res = await this.client.get(this.getUrl(`/series/search?name=${name}`));
|
|
586
681
|
return res.data;
|
|
587
682
|
}
|
|
588
|
-
searchSeriesStream(name, callbacks) {
|
|
589
|
-
|
|
683
|
+
searchSeriesStream(name, callbacks, options) {
|
|
684
|
+
const params = { name };
|
|
685
|
+
if (options?.source?.length)
|
|
686
|
+
params.source = options.source.join(',');
|
|
687
|
+
if (options?.pageKey)
|
|
688
|
+
params.pageKey = options.pageKey;
|
|
689
|
+
return this.openSearchStream(this.getUrl('/series/searchstream'), params, callbacks);
|
|
590
690
|
}
|
|
591
691
|
async setEpisodeWatched(serieId, season, number, date) {
|
|
592
692
|
await this.client.post(this.getUrl(`/series/${serieId}/seasons/${season}/episodes/${number}/watched`), { date });
|
|
@@ -635,6 +735,38 @@ export class LibraryApi {
|
|
|
635
735
|
const res = await this.client.get(this.getUrl(`/movies/${movieId}/search`), q ? { params: { q } } : undefined);
|
|
636
736
|
return res.data;
|
|
637
737
|
}
|
|
738
|
+
searchEpisodeMediasStream(serieId, season, episode, callbacks, options) {
|
|
739
|
+
const params = {};
|
|
740
|
+
if (options?.source?.length)
|
|
741
|
+
params.source = options.source.join(',');
|
|
742
|
+
if (options?.pageKey)
|
|
743
|
+
params.pageKey = options.pageKey;
|
|
744
|
+
return this.openSearchStream(this.getUrl(`/series/${serieId}/seasons/${season}/episodes/${episode}/searchstream`), params, callbacks);
|
|
745
|
+
}
|
|
746
|
+
searchSeasonMediasStream(serieId, season, callbacks, options) {
|
|
747
|
+
const params = {};
|
|
748
|
+
if (options?.source?.length)
|
|
749
|
+
params.source = options.source.join(',');
|
|
750
|
+
if (options?.pageKey)
|
|
751
|
+
params.pageKey = options.pageKey;
|
|
752
|
+
return this.openSearchStream(this.getUrl(`/series/${serieId}/seasons/${season}/searchstream`), params, callbacks);
|
|
753
|
+
}
|
|
754
|
+
searchMovieMediasStream(movieId, callbacks, options) {
|
|
755
|
+
const params = {};
|
|
756
|
+
if (options?.source?.length)
|
|
757
|
+
params.source = options.source.join(',');
|
|
758
|
+
if (options?.pageKey)
|
|
759
|
+
params.pageKey = options.pageKey;
|
|
760
|
+
return this.openSearchStream(this.getUrl(`/movies/${movieId}/searchstream`), params, callbacks);
|
|
761
|
+
}
|
|
762
|
+
searchBookMediasStream(bookId, callbacks, options) {
|
|
763
|
+
const params = {};
|
|
764
|
+
if (options?.source?.length)
|
|
765
|
+
params.source = options.source.join(',');
|
|
766
|
+
if (options?.pageKey)
|
|
767
|
+
params.pageKey = options.pageKey;
|
|
768
|
+
return this.openSearchStream(this.getUrl(`/books/${bookId}/searchstream`), params, callbacks);
|
|
769
|
+
}
|
|
638
770
|
/**
|
|
639
771
|
* Adds a searched episode media to the database for download/storage.
|
|
640
772
|
* @param serieId - The series identifier
|
|
@@ -884,10 +1016,6 @@ export class LibraryApi {
|
|
|
884
1016
|
const res = await this.client.patch(this.getUrl(`/medias/${mediaId}/progress`), { progress });
|
|
885
1017
|
return res.data;
|
|
886
1018
|
}
|
|
887
|
-
async mediaUpdateChannel(mediaId, update) {
|
|
888
|
-
const res = await this.client.patch(this.getUrl(`/medias/${mediaId}/channel`), update);
|
|
889
|
-
return res.data;
|
|
890
|
-
}
|
|
891
1019
|
async refreshMedia(mediaId) {
|
|
892
1020
|
const res = await this.client.get(this.getUrl(`/medias/${mediaId}/metadata/refresh`));
|
|
893
1021
|
return res.data;
|
|
@@ -1141,14 +1269,14 @@ export class LibraryApi {
|
|
|
1141
1269
|
* Upload a group of media files from URLs
|
|
1142
1270
|
* @param download - The group download request containing requests array
|
|
1143
1271
|
* @param options - Optional settings (spawn: true to run in background)
|
|
1144
|
-
* @returns Array of created media files when spawn=
|
|
1272
|
+
* @returns Array of created media files, { downloading: true } when spawn=true, or { needFileSelection, request } when file selection is required
|
|
1145
1273
|
*/
|
|
1146
1274
|
async uploadGroup(download, options) {
|
|
1147
1275
|
const params = {};
|
|
1148
1276
|
if (options?.spawn) {
|
|
1149
1277
|
params.spawn = 'true';
|
|
1150
1278
|
}
|
|
1151
|
-
const res = await this.client.post(this.getUrl('/medias/download'), download, { params });
|
|
1279
|
+
const res = await this.client.post(this.getUrl('/medias/download'), download, { params, timeout: 2 * 60 * 60 * 1000 });
|
|
1152
1280
|
return res.data;
|
|
1153
1281
|
}
|
|
1154
1282
|
}
|
package/dist/server.d.ts
CHANGED
|
@@ -15,7 +15,10 @@ export declare class ServerApi {
|
|
|
15
15
|
updateLibrary(libraryId: string, library: ServerLibraryForUpdate): Promise<ILibrary>;
|
|
16
16
|
deleteLibrary(libraryId: string, deleteMediaContent?: boolean): Promise<void>;
|
|
17
17
|
getPlugins(): Promise<IPlugin[]>;
|
|
18
|
-
searchLookupStream(query: string, type: string, callbacks: SearchStreamCallbacks<LookupSearchStreamResult
|
|
18
|
+
searchLookupStream(query: string, type: string, callbacks: SearchStreamCallbacks<LookupSearchStreamResult>, options?: {
|
|
19
|
+
source?: string[];
|
|
20
|
+
pageKey?: string;
|
|
21
|
+
}): () => void;
|
|
19
22
|
getCredentials(): Promise<ICredential[]>;
|
|
20
23
|
saveCredential(credential: ICredential): Promise<ICredential>;
|
|
21
24
|
updateCredential(credential: ICredential): Promise<ICredential>;
|
package/dist/server.js
CHANGED
|
@@ -1,50 +1,34 @@
|
|
|
1
|
+
import { openFetchSSEStream } from './sse-fetch.js';
|
|
1
2
|
export class ServerApi {
|
|
2
3
|
constructor(client) {
|
|
3
4
|
this.client = client;
|
|
4
5
|
}
|
|
5
6
|
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
7
|
const url = this.client.getFullUrl(path, {
|
|
12
8
|
...params,
|
|
13
9
|
token: this.client.getAuthToken()
|
|
14
10
|
});
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
let finished = false;
|
|
12
|
+
const finish = () => {
|
|
13
|
+
if (!finished) {
|
|
14
|
+
finished = true;
|
|
15
|
+
callbacks.onFinished?.();
|
|
20
16
|
}
|
|
21
|
-
closed = true;
|
|
22
|
-
source.removeEventListener('results', onResults);
|
|
23
|
-
source.removeEventListener('finished', onFinished);
|
|
24
|
-
source.onerror = null;
|
|
25
|
-
source.close();
|
|
26
17
|
};
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
18
|
+
return openFetchSSEStream(url, (eventType, data) => {
|
|
19
|
+
if (eventType === 'results') {
|
|
20
|
+
try {
|
|
21
|
+
const payload = JSON.parse(data);
|
|
22
|
+
callbacks.onResults?.(payload);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
callbacks.onError?.(error);
|
|
26
|
+
}
|
|
31
27
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
close();
|
|
28
|
+
else if (eventType === 'finished') {
|
|
29
|
+
finish();
|
|
35
30
|
}
|
|
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;
|
|
31
|
+
}, () => finish(), (error) => callbacks.onError?.(error));
|
|
48
32
|
}
|
|
49
33
|
async getLibraries() {
|
|
50
34
|
const res = await this.client.get('/libraries');
|
|
@@ -73,8 +57,13 @@ export class ServerApi {
|
|
|
73
57
|
const res = await this.client.get('/plugins');
|
|
74
58
|
return res.data;
|
|
75
59
|
}
|
|
76
|
-
searchLookupStream(query, type, callbacks) {
|
|
77
|
-
|
|
60
|
+
searchLookupStream(query, type, callbacks, options) {
|
|
61
|
+
const params = { q: query, type };
|
|
62
|
+
if (options?.source?.length)
|
|
63
|
+
params.source = options.source.join(',');
|
|
64
|
+
if (options?.pageKey)
|
|
65
|
+
params.pageKey = options.pageKey;
|
|
66
|
+
return this.openSearchStream('/plugins/lookup/searchstream', params, callbacks);
|
|
78
67
|
}
|
|
79
68
|
async getCredentials() {
|
|
80
69
|
const res = await this.client.get('/credentials');
|
package/dist/sse-fetch.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface RawSSEEvent {
|
|
|
10
10
|
/**
|
|
11
11
|
* Parses a raw SSE buffer into discrete events, returning any incomplete trailing data.
|
|
12
12
|
* Follows the SSE spec: events are separated by blank lines, fields are `field: value`.
|
|
13
|
+
* Handles \r\n, \r, and \n line endings per the SSE specification.
|
|
13
14
|
*/
|
|
14
15
|
export declare function parseSSEBuffer(buffer: string): {
|
|
15
16
|
events: RawSSEEvent[];
|
package/dist/sse-fetch.js
CHANGED
|
@@ -1,58 +1,60 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parses a raw SSE buffer into discrete events, returning any incomplete trailing data.
|
|
3
3
|
* Follows the SSE spec: events are separated by blank lines, fields are `field: value`.
|
|
4
|
+
* Handles \r\n, \r, and \n line endings per the SSE specification.
|
|
4
5
|
*/
|
|
5
6
|
export function parseSSEBuffer(buffer) {
|
|
6
7
|
const events = [];
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
let
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (
|
|
8
|
+
const normalised = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
9
|
+
// Split into blocks separated by blank lines (\n\n).
|
|
10
|
+
// Only complete blocks (followed by \n\n) are processed;
|
|
11
|
+
// any trailing incomplete block stays in the buffer.
|
|
12
|
+
let lastEventEnd = 0;
|
|
13
|
+
const blockBoundary = '\n\n';
|
|
14
|
+
let searchFrom = 0;
|
|
15
|
+
while (true) {
|
|
16
|
+
const idx = normalised.indexOf(blockBoundary, searchFrom);
|
|
17
|
+
if (idx === -1)
|
|
17
18
|
break;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
const block = normalised.slice(lastEventEnd, idx);
|
|
20
|
+
lastEventEnd = idx + blockBoundary.length;
|
|
21
|
+
searchFrom = lastEventEnd;
|
|
22
|
+
let eventType = '';
|
|
23
|
+
let eventData = '';
|
|
24
|
+
let eventId;
|
|
25
|
+
let eventRetry;
|
|
26
|
+
for (const line of block.split('\n')) {
|
|
27
|
+
if (line === '' || line.startsWith(':'))
|
|
28
|
+
continue;
|
|
29
|
+
const colonIdx = line.indexOf(':');
|
|
30
|
+
if (colonIdx === -1)
|
|
31
|
+
continue;
|
|
32
|
+
const field = line.slice(0, colonIdx);
|
|
33
|
+
let value = line.slice(colonIdx + 1);
|
|
34
|
+
if (value.startsWith(' '))
|
|
35
|
+
value = value.slice(1);
|
|
36
|
+
switch (field) {
|
|
37
|
+
case 'event':
|
|
38
|
+
eventType = value;
|
|
39
|
+
break;
|
|
40
|
+
case 'data':
|
|
41
|
+
eventData = eventData ? eventData + '\n' + value : value;
|
|
42
|
+
break;
|
|
43
|
+
case 'id':
|
|
44
|
+
eventId = value;
|
|
45
|
+
break;
|
|
46
|
+
case 'retry':
|
|
47
|
+
eventRetry = parseInt(value, 10);
|
|
48
|
+
break;
|
|
24
49
|
}
|
|
25
|
-
eventType = '';
|
|
26
|
-
eventData = '';
|
|
27
|
-
eventId = undefined;
|
|
28
|
-
eventRetry = undefined;
|
|
29
|
-
continue;
|
|
30
50
|
}
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
const colonIdx = line.indexOf(':');
|
|
34
|
-
if (colonIdx === -1)
|
|
35
|
-
continue;
|
|
36
|
-
const field = line.slice(0, colonIdx);
|
|
37
|
-
let value = line.slice(colonIdx + 1);
|
|
38
|
-
if (value.startsWith(' '))
|
|
39
|
-
value = value.slice(1);
|
|
40
|
-
switch (field) {
|
|
41
|
-
case 'event':
|
|
42
|
-
eventType = value;
|
|
43
|
-
break;
|
|
44
|
-
case 'data':
|
|
45
|
-
eventData = eventData ? eventData + '\n' + value : value;
|
|
46
|
-
break;
|
|
47
|
-
case 'id':
|
|
48
|
-
eventId = value;
|
|
49
|
-
break;
|
|
50
|
-
case 'retry':
|
|
51
|
-
eventRetry = parseInt(value, 10);
|
|
52
|
-
break;
|
|
51
|
+
if (eventType) {
|
|
52
|
+
events.push({ event: eventType, data: eventData, id: eventId, retry: eventRetry });
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
|
|
55
|
+
// Note: remainingBuffer is from the normalised string. Callers must replace
|
|
56
|
+
// their buffer entirely (not use offset-based splicing on the original input).
|
|
57
|
+
return { events, remainingBuffer: normalised.slice(lastEventEnd) };
|
|
56
58
|
}
|
|
57
59
|
/**
|
|
58
60
|
* Opens a one-shot fetch-based SSE stream (no reconnection).
|
|
@@ -87,6 +89,14 @@ export function openFetchSSEStream(url, onEvent, onDone, onError) {
|
|
|
87
89
|
while (true) {
|
|
88
90
|
const { done, value } = await reader.read();
|
|
89
91
|
if (done) {
|
|
92
|
+
// Flush any remaining buffered event when the stream closes
|
|
93
|
+
// (server may close without a trailing blank line)
|
|
94
|
+
if (buffer.trim()) {
|
|
95
|
+
const { events: finalEvents } = parseSSEBuffer(buffer + '\n\n');
|
|
96
|
+
for (const event of finalEvents) {
|
|
97
|
+
onEvent(event.event, event.data);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
90
100
|
onDone?.();
|
|
91
101
|
break;
|
|
92
102
|
}
|
package/dist/sse-types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ILibrary, IFile, IEpisode, ISerie, IMovie, IBook, IPerson, ITag, IBackup, IWatched, IUnwatched, IRsRequestProcessing, VideoConvertRequest, type ItemWithRelations } from './interfaces.js';
|
|
1
|
+
import { ILibrary, IFile, IEpisode, ISerie, IMovie, IBook, IPerson, ITag, IBackup, IWatched, IUnwatched, IRsRequestProcessing, VideoConvertRequest, type ItemWithRelations, type IChannel } 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 {
|
|
@@ -98,6 +98,13 @@ export interface SSEMoviesEvent {
|
|
|
98
98
|
movie: ItemWithRelations<IMovie>;
|
|
99
99
|
}[];
|
|
100
100
|
}
|
|
101
|
+
export interface SSEChannelsEvent {
|
|
102
|
+
library: string;
|
|
103
|
+
channels: {
|
|
104
|
+
action: ElementAction;
|
|
105
|
+
channel: IChannel;
|
|
106
|
+
}[];
|
|
107
|
+
}
|
|
101
108
|
export interface SSEBooksEvent {
|
|
102
109
|
library: string;
|
|
103
110
|
books: {
|
|
@@ -205,6 +212,7 @@ export interface SSEEventMap {
|
|
|
205
212
|
'watched': SSEWatchedEvent;
|
|
206
213
|
'unwatched': SSEUnwatchedEvent;
|
|
207
214
|
'request_processing': SSERequestProcessingEvent;
|
|
215
|
+
'channels': SSEChannelsEvent;
|
|
208
216
|
}
|
|
209
217
|
export type SSEEventName = keyof SSEEventMap;
|
|
210
218
|
export interface SSEEvent<T extends SSEEventName = SSEEventName> {
|
package/libraries.md
CHANGED
|
@@ -1812,22 +1812,59 @@ Updates the watch progress for a media file.
|
|
|
1812
1812
|
await libraryApi.mediaUpdateProgress('media-id', 50);
|
|
1813
1813
|
```
|
|
1814
1814
|
|
|
1815
|
-
### `
|
|
1815
|
+
### `getChannels(query?: ChannelQuery): Promise<IChannel[]>`
|
|
1816
1816
|
|
|
1817
|
-
|
|
1817
|
+
Lists all channels in the library, optionally filtered by group tag or name.
|
|
1818
1818
|
|
|
1819
1819
|
**Parameters:**
|
|
1820
1820
|
|
|
1821
|
-
- `
|
|
1822
|
-
- `update`: Channel update object
|
|
1821
|
+
- `query` (optional): Filter options (`{ tag?: string, name?: string }`)
|
|
1823
1822
|
|
|
1824
|
-
**Returns:** Promise resolving to an array of
|
|
1823
|
+
**Returns:** Promise resolving to an array of `IChannel` objects
|
|
1825
1824
|
|
|
1826
|
-
|
|
1825
|
+
### `getChannel(channelId: string): Promise<IChannel>`
|
|
1827
1826
|
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1827
|
+
Gets a single channel with its variants.
|
|
1828
|
+
|
|
1829
|
+
### `deleteChannel(channelId: string): Promise<void>`
|
|
1830
|
+
|
|
1831
|
+
Deletes a channel. Requires admin role.
|
|
1832
|
+
|
|
1833
|
+
### `importChannels(url?: string): Promise<M3uImportResult>`
|
|
1834
|
+
|
|
1835
|
+
Imports channels from an M3U playlist URL. If no URL is provided, uses the library's configured source.
|
|
1836
|
+
|
|
1837
|
+
**Returns:** Promise resolving to `M3uImportResult` with counts of added/updated/removed channels and groups.
|
|
1838
|
+
|
|
1839
|
+
### `refreshChannels(): Promise<M3uImportResult>`
|
|
1840
|
+
|
|
1841
|
+
Refreshes channels from the library's configured M3U source URL.
|
|
1842
|
+
|
|
1843
|
+
### `getChannelStreamUrl(channelId: string, quality?: string): string`
|
|
1844
|
+
|
|
1845
|
+
Returns the URL for streaming a channel via MPEG2-TS proxy. Supports quality selection (`4K`, `FHD`, `HD`, `SD`).
|
|
1846
|
+
|
|
1847
|
+
### `getChannelHlsUrl(channelId: string, quality?: string): string`
|
|
1848
|
+
|
|
1849
|
+
Returns the HLS playlist URL for streaming a channel. The server remuxes the MPEG-TS source into HLS segments using FFmpeg. Supports quality selection (`4K`, `FHD`, `HD`, `SD`).
|
|
1850
|
+
|
|
1851
|
+
**Note:** HLS sessions consume server resources. Call `stopChannelHls()` when playback ends.
|
|
1852
|
+
|
|
1853
|
+
### `stopChannelHls(channelId: string): Promise<void>`
|
|
1854
|
+
|
|
1855
|
+
Stops the server-side HLS session for a channel, terminating FFmpeg and cleaning up temporary segments.
|
|
1856
|
+
|
|
1857
|
+
### `addTagToChannel(channelId: string, tagId: string): Promise<void>`
|
|
1858
|
+
|
|
1859
|
+
Adds a tag to a channel.
|
|
1860
|
+
|
|
1861
|
+
### `removeTagFromChannel(channelId: string, tagId: string): Promise<void>`
|
|
1862
|
+
|
|
1863
|
+
Removes a tag from a channel.
|
|
1864
|
+
|
|
1865
|
+
### `getChannelImageUrl(channelId: string, size?: string): string`
|
|
1866
|
+
|
|
1867
|
+
Returns the URL for a channel's image/logo.
|
|
1831
1868
|
|
|
1832
1869
|
### `refreshMedia(mediaId: string): Promise<IFile[]>`
|
|
1833
1870
|
|
|
@@ -2466,16 +2503,17 @@ Gets a cryptographic challenge for encryption verification.
|
|
|
2466
2503
|
const challenge = await libraryApi.getCryptChallenge();
|
|
2467
2504
|
```
|
|
2468
2505
|
|
|
2469
|
-
### `getChannels(): Promise<
|
|
2506
|
+
### `getChannels(query?: ChannelQuery): Promise<IChannel[]>`
|
|
2470
2507
|
|
|
2471
|
-
Gets all channels (for IPTV libraries).
|
|
2508
|
+
Gets all channels (for IPTV libraries), optionally filtered.
|
|
2472
2509
|
|
|
2473
|
-
**Returns:** Promise resolving to an array of
|
|
2510
|
+
**Returns:** Promise resolving to an array of `IChannel` objects
|
|
2474
2511
|
|
|
2475
2512
|
**Example:**
|
|
2476
2513
|
|
|
2477
2514
|
```typescript
|
|
2478
2515
|
const channels = await libraryApi.getChannels();
|
|
2516
|
+
const filtered = await libraryApi.getChannels({ tag: 'tag-id' });
|
|
2479
2517
|
```
|
|
2480
2518
|
|
|
2481
2519
|
### `getWatermarks(): Promise<string[]>`
|