@redseat/api 0.5.0 → 0.6.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/dist/client.d.ts +19 -1
- package/dist/client.js +83 -9
- package/dist/interfaces.d.ts +35 -11
- package/dist/library.d.ts +41 -5
- package/dist/library.js +101 -8
- package/dist/server.js +18 -34
- 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;
|
|
@@ -490,11 +491,17 @@ export type SearchResult<K extends string, T> = SearchStreamResult<K, T>;
|
|
|
490
491
|
export type BookSearchResult = SearchResult<'book', IBook>;
|
|
491
492
|
export type MovieSearchResult = SearchResult<'movie', IMovie>;
|
|
492
493
|
export type SerieSearchResult = SearchResult<'serie', ISerie>;
|
|
494
|
+
export type PersonSearchResult = SearchResult<'person', IPerson>;
|
|
493
495
|
export type BookSearchStreamResult = SearchStreamResult<'book', IBook>;
|
|
494
496
|
export type MovieSearchStreamResult = SearchStreamResult<'movie', IMovie>;
|
|
495
497
|
export type SerieSearchStreamResult = SearchStreamResult<'serie', ISerie>;
|
|
498
|
+
export type PersonSearchStreamResult = SearchStreamResult<'person', IPerson>;
|
|
496
499
|
export type LookupSearchStreamResult = SearchStreamResult<'book' | 'movie' | 'serie', IBook | IMovie | ISerie>;
|
|
497
500
|
export type RsLookupMatchType = 'exactId' | 'exactText';
|
|
501
|
+
export interface SourceSearchStreamResult {
|
|
502
|
+
request: RsRequest;
|
|
503
|
+
matchType?: RsLookupMatchType;
|
|
504
|
+
}
|
|
498
505
|
export interface SseSearchStreamEvent<T> {
|
|
499
506
|
sourceId: string;
|
|
500
507
|
sourceName: string;
|
|
@@ -509,17 +516,35 @@ export interface SearchStreamCallbacks<T> {
|
|
|
509
516
|
export interface IChannel {
|
|
510
517
|
id: string;
|
|
511
518
|
name: string;
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
519
|
+
tvgId?: string;
|
|
520
|
+
logo?: string;
|
|
521
|
+
tags?: string[];
|
|
522
|
+
channelNumber?: number;
|
|
523
|
+
modified?: number;
|
|
524
|
+
added?: number;
|
|
525
|
+
variants?: IChannelVariant[];
|
|
516
526
|
}
|
|
517
|
-
export interface
|
|
527
|
+
export interface IChannelVariant {
|
|
518
528
|
id: string;
|
|
519
|
-
|
|
520
|
-
source: string;
|
|
529
|
+
channelRef: string;
|
|
521
530
|
quality?: string;
|
|
522
|
-
|
|
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;
|
|
523
548
|
}
|
|
524
549
|
export interface UnassignedFace {
|
|
525
550
|
id: string;
|
|
@@ -576,9 +601,6 @@ export interface MergePeopleResponse {
|
|
|
576
601
|
export interface ClusterFacesResponse {
|
|
577
602
|
clusters: number;
|
|
578
603
|
}
|
|
579
|
-
export interface IChannelUpdate {
|
|
580
|
-
[key: string]: any;
|
|
581
|
-
}
|
|
582
604
|
export declare enum PluginAuthType {
|
|
583
605
|
OAUTH = "oauth",
|
|
584
606
|
URL = "url",
|
|
@@ -834,6 +856,8 @@ export interface HistoryQuery {
|
|
|
834
856
|
id?: string;
|
|
835
857
|
/** Pagination key for next page */
|
|
836
858
|
pageKey?: number;
|
|
859
|
+
/** Include soft-deleted entries (date=0) for sync purposes */
|
|
860
|
+
include_deleted?: boolean;
|
|
837
861
|
}
|
|
838
862
|
export type MovieSort = 'modified' | 'added' | 'created' | 'name' | 'digitalairdate';
|
|
839
863
|
export interface DeletedQuery {
|
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, LookupSearchStreamResult, 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.
|
|
@@ -145,6 +147,7 @@ export declare class LibraryApi {
|
|
|
145
147
|
createPerson(person: Partial<IPerson>): Promise<IPerson>;
|
|
146
148
|
removePerson(personId: string): Promise<void>;
|
|
147
149
|
updatePerson(personId: string, updates: Partial<IPerson>): Promise<IPerson>;
|
|
150
|
+
getPerson(personId: string): Promise<IPerson>;
|
|
148
151
|
createSerie(serie: Partial<ISerie>): Promise<ISerie>;
|
|
149
152
|
createEpisode(episode: Partial<IEpisode>): Promise<IEpisode>;
|
|
150
153
|
serieScrap(serieId: string, date?: number): Promise<void>;
|
|
@@ -198,7 +201,18 @@ export declare class LibraryApi {
|
|
|
198
201
|
addTagToMedia(mediaId: string, tagId: string): Promise<void>;
|
|
199
202
|
removeTagFromMedia(mediaId: string, tagId: string): Promise<void>;
|
|
200
203
|
getCryptChallenge(): Promise<string>;
|
|
201
|
-
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;
|
|
202
216
|
personRename(personId: string, newName: string): Promise<IPerson>;
|
|
203
217
|
personUpdate(personId: string, updates: Partial<IPerson>): Promise<IPerson>;
|
|
204
218
|
personAddAlt(personId: string, alt: string): Promise<IPerson>;
|
|
@@ -211,6 +225,10 @@ export declare class LibraryApi {
|
|
|
211
225
|
serieAddAlt(serieId: string, alt: string): Promise<ISerie>;
|
|
212
226
|
serieRemoveAlt(serieId: string, alt: string): Promise<ISerie>;
|
|
213
227
|
updatePersonPortrait(personId: string, portrait: FormData): Promise<void>;
|
|
228
|
+
searchPeopleStream(name: string, callbacks: SearchStreamCallbacks<PersonSearchStreamResult>, options?: {
|
|
229
|
+
source?: string[];
|
|
230
|
+
pageKey?: string;
|
|
231
|
+
}): () => void;
|
|
214
232
|
searchSeries(name: string): Promise<SerieSearchResult[]>;
|
|
215
233
|
searchSeriesStream(name: string, callbacks: SearchStreamCallbacks<SerieSearchStreamResult>, options?: {
|
|
216
234
|
source?: string[];
|
|
@@ -244,6 +262,22 @@ export declare class LibraryApi {
|
|
|
244
262
|
* @returns Array of grouped download requests. Use `.flatMap(g => g.requests)` to get a flat list of RsRequest items.
|
|
245
263
|
*/
|
|
246
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;
|
|
247
281
|
/**
|
|
248
282
|
* Adds a searched episode media to the database for download/storage.
|
|
249
283
|
* @param serieId - The series identifier
|
|
@@ -387,7 +421,6 @@ export declare class LibraryApi {
|
|
|
387
421
|
mediaUpdateProgress(mediaId: string, progress: number): Promise<{
|
|
388
422
|
progress: number;
|
|
389
423
|
}>;
|
|
390
|
-
mediaUpdateChannel(mediaId: string, update: IChannelUpdate): Promise<IFile[]>;
|
|
391
424
|
refreshMedia(mediaId: string): Promise<IFile[]>;
|
|
392
425
|
aiTagMedia(mediaId: string): Promise<IFile[]>;
|
|
393
426
|
mediaUpdate(mediaId: string, update: MediaForUpdate): Promise<IFile[]>;
|
|
@@ -471,11 +504,14 @@ export declare class LibraryApi {
|
|
|
471
504
|
* Upload a group of media files from URLs
|
|
472
505
|
* @param download - The group download request containing requests array
|
|
473
506
|
* @param options - Optional settings (spawn: true to run in background)
|
|
474
|
-
* @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
|
|
475
508
|
*/
|
|
476
509
|
uploadGroup(download: RsGroupDownload, options?: {
|
|
477
510
|
spawn?: boolean;
|
|
478
511
|
}): Promise<IFile[] | {
|
|
479
512
|
downloading: boolean;
|
|
513
|
+
} | {
|
|
514
|
+
needFileSelection: true;
|
|
515
|
+
request: RsRequest;
|
|
480
516
|
}>;
|
|
481
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.
|
|
@@ -365,6 +366,10 @@ export class LibraryApi {
|
|
|
365
366
|
const res = await this.client.patch(this.getUrl(`/people/${personId}`), updates);
|
|
366
367
|
return res.data;
|
|
367
368
|
}
|
|
369
|
+
async getPerson(personId) {
|
|
370
|
+
const res = await this.client.get(this.getUrl(`/people/${personId}`));
|
|
371
|
+
return res.data;
|
|
372
|
+
}
|
|
368
373
|
async createSerie(serie) {
|
|
369
374
|
const res = await this.client.post(this.getUrl('/series'), serie);
|
|
370
375
|
return res.data;
|
|
@@ -540,10 +545,62 @@ export class LibraryApi {
|
|
|
540
545
|
const res = await this.client.get(this.getUrl('/cryptchallenge'));
|
|
541
546
|
return res.data.value;
|
|
542
547
|
}
|
|
543
|
-
async getChannels() {
|
|
544
|
-
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 });
|
|
545
555
|
return res.data;
|
|
546
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'), {});
|
|
570
|
+
return res.data;
|
|
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
|
+
}
|
|
547
604
|
async personRename(personId, newName) {
|
|
548
605
|
const res = await this.client.patch(this.getUrl(`/people/${personId}`), {
|
|
549
606
|
name: newName
|
|
@@ -611,6 +668,14 @@ export class LibraryApi {
|
|
|
611
668
|
async updatePersonPortrait(personId, portrait) {
|
|
612
669
|
await this.client.post(this.getUrl(`/people/${personId}/image?size=thumb&type=poster`), portrait);
|
|
613
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
|
+
}
|
|
614
679
|
async searchSeries(name) {
|
|
615
680
|
const res = await this.client.get(this.getUrl(`/series/search?name=${name}`));
|
|
616
681
|
return res.data;
|
|
@@ -670,6 +735,38 @@ export class LibraryApi {
|
|
|
670
735
|
const res = await this.client.get(this.getUrl(`/movies/${movieId}/search`), q ? { params: { q } } : undefined);
|
|
671
736
|
return res.data;
|
|
672
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
|
+
}
|
|
673
770
|
/**
|
|
674
771
|
* Adds a searched episode media to the database for download/storage.
|
|
675
772
|
* @param serieId - The series identifier
|
|
@@ -919,10 +1016,6 @@ export class LibraryApi {
|
|
|
919
1016
|
const res = await this.client.patch(this.getUrl(`/medias/${mediaId}/progress`), { progress });
|
|
920
1017
|
return res.data;
|
|
921
1018
|
}
|
|
922
|
-
async mediaUpdateChannel(mediaId, update) {
|
|
923
|
-
const res = await this.client.patch(this.getUrl(`/medias/${mediaId}/channel`), update);
|
|
924
|
-
return res.data;
|
|
925
|
-
}
|
|
926
1019
|
async refreshMedia(mediaId) {
|
|
927
1020
|
const res = await this.client.get(this.getUrl(`/medias/${mediaId}/metadata/refresh`));
|
|
928
1021
|
return res.data;
|
|
@@ -1176,14 +1269,14 @@ export class LibraryApi {
|
|
|
1176
1269
|
* Upload a group of media files from URLs
|
|
1177
1270
|
* @param download - The group download request containing requests array
|
|
1178
1271
|
* @param options - Optional settings (spawn: true to run in background)
|
|
1179
|
-
* @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
|
|
1180
1273
|
*/
|
|
1181
1274
|
async uploadGroup(download, options) {
|
|
1182
1275
|
const params = {};
|
|
1183
1276
|
if (options?.spawn) {
|
|
1184
1277
|
params.spawn = 'true';
|
|
1185
1278
|
}
|
|
1186
|
-
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 });
|
|
1187
1280
|
return res.data;
|
|
1188
1281
|
}
|
|
1189
1282
|
}
|
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');
|
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[]>`
|