@ngxsp/core 0.0.1-next.0 → 0.1.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/LICENSE +21 -0
- package/README.md +28 -0
- package/index.cjs +4290 -57
- package/index.cjs.map +1 -1
- package/index.d.cts +1646 -8
- package/index.d.ts +1646 -8
- package/index.js +4131 -58
- package/index.js.map +1 -1
- package/package.json +17 -10
package/index.d.cts
CHANGED
|
@@ -140,6 +140,11 @@ interface PlayerSource {
|
|
|
140
140
|
* Optional identifier for progress/metadata/subtitles lookup.
|
|
141
141
|
*/
|
|
142
142
|
contentId?: string;
|
|
143
|
+
/** Optional display metadata, used by UI skins (especially audio mode). */
|
|
144
|
+
title?: string;
|
|
145
|
+
subtitle?: string;
|
|
146
|
+
artist?: string;
|
|
147
|
+
album?: string;
|
|
143
148
|
/**
|
|
144
149
|
* Optional sidecar URL for local metadata (json/txt) or subtitles.
|
|
145
150
|
*/
|
|
@@ -170,9 +175,15 @@ interface NextEpisodeHint {
|
|
|
170
175
|
}
|
|
171
176
|
interface UiConfig {
|
|
172
177
|
/** Optional UI skin preset (default: classic) */
|
|
173
|
-
skin?: 'classic' | '
|
|
178
|
+
skin?: 'classic' | 'vod' | 'edition';
|
|
174
179
|
/** Optional display title used by some skins */
|
|
175
180
|
title?: string;
|
|
181
|
+
/** Optional alternate display title (falls back from `title`). */
|
|
182
|
+
displayTitle?: string;
|
|
183
|
+
/** Optional subtitle shown under the title (e.g. audio "now playing"). */
|
|
184
|
+
subtitle?: string;
|
|
185
|
+
/** Optional longer description shown by some skins. */
|
|
186
|
+
description?: string;
|
|
176
187
|
ratios: AspectRatioMode[];
|
|
177
188
|
defaultRatio: AspectRatioMode;
|
|
178
189
|
keyboardShortcuts: boolean;
|
|
@@ -395,6 +406,40 @@ type PlayerEvent = {
|
|
|
395
406
|
id?: string;
|
|
396
407
|
};
|
|
397
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Sensible, framework-agnostic defaults for the mandatory sub-configs of
|
|
411
|
+
* {@link PlayerConfig}. They mirror the values used by the existing
|
|
412
|
+
* consumers (local audio/video players) so a brand new integration can get a
|
|
413
|
+
* valid config without restating every field.
|
|
414
|
+
*/
|
|
415
|
+
declare const DEFAULT_PROGRESS_CONFIG: ProgressConfig;
|
|
416
|
+
declare const DEFAULT_UI_CONFIG: UiConfig;
|
|
417
|
+
declare const DEFAULT_RESILIENCE_CONFIG: ResilienceConfig;
|
|
418
|
+
declare const DEFAULT_SUBTITLES_CONFIG: SubtitlesConfig;
|
|
419
|
+
declare const DEFAULT_METADATA_CONFIG: MetadataConfig;
|
|
420
|
+
declare const DEFAULT_ADS_CONFIG: AdsConfig;
|
|
421
|
+
/**
|
|
422
|
+
* Build a complete, type-safe {@link PlayerConfig} from a minimal set of
|
|
423
|
+
* overrides. Only `source` is required; the mandatory sub-configs
|
|
424
|
+
* (`progress`, `ui`, `resilience`, `subtitles`, `metadata`, `ads`) are filled
|
|
425
|
+
* with the defaults above and shallow-merged with any override provided.
|
|
426
|
+
*
|
|
427
|
+
* Optional top-level fields (`profile`, `drm`, `series`, `playlist`,
|
|
428
|
+
* `offline`, `remoteConfig`, ...) are passed through untouched when present.
|
|
429
|
+
*
|
|
430
|
+
* @example
|
|
431
|
+
* ```ts
|
|
432
|
+
* const config = createDefaultConfig({
|
|
433
|
+
* source: { type: 'hls', url: 'https://cdn.example.com/movie.m3u8' },
|
|
434
|
+
* drm: { keySystems: { 'com.widevine.alpha': { licenseServerUrl: 'https://drm/widevine' } } },
|
|
435
|
+
* subtitles: { enabled: true, providers: ['local'], preferredLang: 'fr' },
|
|
436
|
+
* });
|
|
437
|
+
* ```
|
|
438
|
+
*/
|
|
439
|
+
declare function createDefaultConfig(overrides: Partial<PlayerConfig> & {
|
|
440
|
+
source: PlayerSource;
|
|
441
|
+
}): PlayerConfig;
|
|
442
|
+
|
|
398
443
|
type EngineEvent = {
|
|
399
444
|
type: 'timeupdate';
|
|
400
445
|
currentTime: number;
|
|
@@ -545,7 +590,6 @@ interface PlayerSnapshot {
|
|
|
545
590
|
*/
|
|
546
591
|
type PlayerDiagnostics = DiagnosticsSnapshot;
|
|
547
592
|
declare class PlayerFacade {
|
|
548
|
-
private chooseEngine;
|
|
549
593
|
private ports;
|
|
550
594
|
private engineFactories;
|
|
551
595
|
private engineId;
|
|
@@ -578,10 +622,24 @@ declare class PlayerFacade {
|
|
|
578
622
|
readonly playlistIndex$: Observable<number | null>;
|
|
579
623
|
private playlist;
|
|
580
624
|
private playlistKey;
|
|
581
|
-
|
|
582
|
-
|
|
625
|
+
/** Subtitles loaded from files, shown by the host from `captionCues$` rather than by the browser. */
|
|
626
|
+
private externalTexts;
|
|
627
|
+
private externalTextCount;
|
|
628
|
+
/** The text track picked here; undefined while the engine's own choice stands. */
|
|
629
|
+
private textSelection;
|
|
630
|
+
private engineTracks;
|
|
631
|
+
private offCueChange?;
|
|
632
|
+
private captionCuesSub;
|
|
633
|
+
/** The cues of the loaded subtitles to show now: the host draws them over the video. */
|
|
634
|
+
readonly captionCues$: Observable<TextTrackCue[]>;
|
|
635
|
+
constructor(ports?: PlayerPorts);
|
|
583
636
|
attach(videoEl: HTMLVideoElement, plugins: PlayerPlugin[] | undefined, config: PlayerConfig, ports?: Partial<PlayerPorts>, engines?: EngineFactory[]): void;
|
|
584
637
|
load(config: PlayerConfig, opts?: LoadOptions): Promise<void>;
|
|
638
|
+
/**
|
|
639
|
+
* Replaces the engines the next load chooses from (a remote device session starting or ending). The native engine is
|
|
640
|
+
* always kept as fallback so progressive and local playback never breaks; call `load` or `reload` to switch now.
|
|
641
|
+
*/
|
|
642
|
+
setEngines(engines?: EngineFactory[]): void;
|
|
585
643
|
reload(opts?: LoadOptions): Promise<void>;
|
|
586
644
|
next(): Promise<void>;
|
|
587
645
|
previous(): Promise<void>;
|
|
@@ -598,10 +656,24 @@ declare class PlayerFacade {
|
|
|
598
656
|
private toPlayerError;
|
|
599
657
|
play(): Promise<void>;
|
|
600
658
|
pause(): Promise<void>;
|
|
601
|
-
|
|
659
|
+
/**
|
|
660
|
+
* Seeks the attached engine. Resolves to false when no engine can take the request yet (a load is replacing it), so
|
|
661
|
+
* callers keep the position and apply it once the media is ready instead of assuming it happened.
|
|
662
|
+
*/
|
|
663
|
+
seekTo(timeSec: number): Promise<boolean>;
|
|
602
664
|
setRate(rate: number): Promise<void>;
|
|
603
665
|
setVolume(volume: number): Promise<void>;
|
|
666
|
+
/**
|
|
667
|
+
* Shows a text track: one of the engine's, one loaded from a file (`addExternalTextTrack`), or none (null).
|
|
668
|
+
* "external" still names the last file loaded.
|
|
669
|
+
*/
|
|
604
670
|
setTextTrack(trackId: string | null): Promise<void>;
|
|
671
|
+
/** Adds subtitles read from a file (WebVTT) to the media playing, and shows them; resolves to the new track's id. */
|
|
672
|
+
addExternalTextTrack(text: {
|
|
673
|
+
label: string;
|
|
674
|
+
language?: string;
|
|
675
|
+
vtt: string;
|
|
676
|
+
}): Promise<string>;
|
|
605
677
|
setAudioTrack(trackId: string | null): Promise<void>;
|
|
606
678
|
setVideoTrack(trackId: string | null): Promise<void>;
|
|
607
679
|
setAspectRatio(_mode: AspectRatioMode): void;
|
|
@@ -613,7 +685,11 @@ declare class PlayerFacade {
|
|
|
613
685
|
diagnostics(): PlayerDiagnostics;
|
|
614
686
|
setExternalVttTrack(lang: string, vtt: string): Promise<void>;
|
|
615
687
|
setMetadata(meta: VideoMetadata): void;
|
|
616
|
-
|
|
688
|
+
/** Shows `shown` (its cues go to `captionCues$`, the browser draws none) and turns the other files' subtitles off. */
|
|
689
|
+
private showExternalText;
|
|
690
|
+
private clearExternalTextTracks;
|
|
691
|
+
/** The engine's tracks and the loaded subtitles, the text track picked here marked active. */
|
|
692
|
+
private emitTracks;
|
|
617
693
|
private handleEngineEvent;
|
|
618
694
|
private tryRestoreStartTime;
|
|
619
695
|
}
|
|
@@ -640,6 +716,7 @@ declare class NativeEngine implements PlaybackEngine {
|
|
|
640
716
|
readonly id = "native";
|
|
641
717
|
private video?;
|
|
642
718
|
private listeners;
|
|
719
|
+
private emit;
|
|
643
720
|
attach(videoEl: HTMLVideoElement): Promise<void>;
|
|
644
721
|
load(source: PlayerSource, opts?: {
|
|
645
722
|
startTimeSec?: number;
|
|
@@ -700,6 +777,21 @@ declare class LocalSubtitleProvider implements SubtitleProvider {
|
|
|
700
777
|
fetch(candidate: SubtitleCandidate): Promise<string | ArrayBuffer>;
|
|
701
778
|
}
|
|
702
779
|
|
|
780
|
+
/**
|
|
781
|
+
* Subtitle files the viewer loads next to a video: SubRip (.srt) or WebVTT (.vtt), in whatever encoding they were
|
|
782
|
+
* saved (UTF-8 today, Windows-1252 for many older French files). The player reads WebVTT only, so SubRip is turned
|
|
783
|
+
* into it.
|
|
784
|
+
*/
|
|
785
|
+
/** The text of a subtitle file: UTF-8 (with or without its byte order mark), else Windows-1252. */
|
|
786
|
+
declare function decodeSubtitleBytes(bytes: Uint8Array): string;
|
|
787
|
+
/** WebVTT from the text of a .vtt or .srt file. */
|
|
788
|
+
declare function subtitleTextToVtt(text: string): string;
|
|
789
|
+
/** The subtitle's name and language, from its file name: "Film.fr.srt" is "Film", in French. */
|
|
790
|
+
declare function describeSubtitleFile(fileName: string): {
|
|
791
|
+
label: string;
|
|
792
|
+
language?: string;
|
|
793
|
+
};
|
|
794
|
+
|
|
703
795
|
interface CacheStore {
|
|
704
796
|
get<T>(key: string): Promise<T | null>;
|
|
705
797
|
set<T>(key: string, value: T, ttlMs: number): Promise<void>;
|
|
@@ -783,6 +875,8 @@ declare class ResiliencePlugin implements PlayerPlugin {
|
|
|
783
875
|
readonly id = "resilience";
|
|
784
876
|
private attempts;
|
|
785
877
|
private recovering;
|
|
878
|
+
/** Bumped by every load: a retry only reloads the media that failed, never one loaded since. */
|
|
879
|
+
private loadGeneration;
|
|
786
880
|
onLoadStart(ctx: PlayerContext): Promise<void>;
|
|
787
881
|
onError(ctx: PlayerContext, err: PlayerError): Promise<void>;
|
|
788
882
|
private health;
|
|
@@ -803,7 +897,7 @@ interface SsaiManifestRewriteConfig {
|
|
|
803
897
|
declare function createSsaiManifestRewritePlugin(): PlayerPlugin;
|
|
804
898
|
|
|
805
899
|
type TelemetryLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
806
|
-
type TelemetryEvent = 'player:init' | 'player:attach' | 'player:source_set' | 'player:engine_selected' | 'player:load_start' | 'player:load_ok' | 'player:load_fail' | 'player:play_request' | 'player:play_ok' | 'player:play_fail' | 'player:pause' | 'player:timeupdate' | 'player:ended' | 'player:error_nonfatal' | 'player:error_fatal' | 'player:playing' | 'qoe:first_frame' | 'qoe:rebuffer_start' | 'qoe:rebuffer_end' | 'qoe:summary' | 'player:auto_next' | 'player:playlist_index' | 'ads:break_start' | 'ads:break_end' | 'ads:ad_start' | 'ads:ad_end' | 'ssai:manifest_rewrite';
|
|
900
|
+
type TelemetryEvent = 'player:init' | 'player:attach' | 'player:source_set' | 'player:engine_selected' | 'player:load_start' | 'player:load_ok' | 'player:load_fail' | 'player:play_request' | 'player:play_ok' | 'player:play_fail' | 'player:play_aborted' | 'player:pause' | 'player:timeupdate' | 'player:ended' | 'player:error_nonfatal' | 'player:error_fatal' | 'player:playing' | 'qoe:first_frame' | 'qoe:rebuffer_start' | 'qoe:rebuffer_end' | 'qoe:summary' | 'player:auto_next' | 'player:playlist_index' | 'ads:break_start' | 'ads:break_end' | 'ads:ad_start' | 'ads:ad_end' | 'ssai:manifest_rewrite';
|
|
807
901
|
interface TelemetrySample {
|
|
808
902
|
ts: number;
|
|
809
903
|
level: TelemetryLevel;
|
|
@@ -843,6 +937,163 @@ declare function applyEngineProfileToSource(source: PlayerSource, profile?: Engi
|
|
|
843
937
|
|
|
844
938
|
declare function createNativeEngineFactory(): EngineFactory;
|
|
845
939
|
|
|
940
|
+
/**
|
|
941
|
+
* Playing on another device of the local network: a DLNA renderer (TV, network speaker), a Chromecast, an AirPlay
|
|
942
|
+
* receiver. Each protocol exposes the same session, so the apps drive a remote device like their own player.
|
|
943
|
+
*/
|
|
944
|
+
type RemotePlaybackKind = 'dlna' | 'chromecast' | 'airplay';
|
|
945
|
+
/** What is sent to the device. The address must be reachable from the device itself, not only from the app. */
|
|
946
|
+
interface RemotePlaybackMedia {
|
|
947
|
+
url: string;
|
|
948
|
+
contentType: string;
|
|
949
|
+
kind: 'audio' | 'video';
|
|
950
|
+
title: string;
|
|
951
|
+
artist?: string;
|
|
952
|
+
album?: string;
|
|
953
|
+
artworkUrl?: string;
|
|
954
|
+
durationSec?: number;
|
|
955
|
+
/** Position to start from, to continue what the app was playing. */
|
|
956
|
+
startSec?: number;
|
|
957
|
+
/** Radios and live streams: no duration, no seeking. */
|
|
958
|
+
live?: boolean;
|
|
959
|
+
/**
|
|
960
|
+
* A stream the app converts on the fly into a format the device reads. It cannot seek: it starts where it is asked
|
|
961
|
+
* to, at `at(position)`, and the device counts from there.
|
|
962
|
+
*/
|
|
963
|
+
conversion?: {
|
|
964
|
+
at(startSec: number): string;
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
type RemotePlayerState = 'idle' | 'loading' | 'buffering' | 'playing' | 'paused' | 'stopped';
|
|
968
|
+
/**
|
|
969
|
+
* Why a device did not play a media, for the apps to explain it in their own words:
|
|
970
|
+
* - `unsupported-format`: the device does not read this format at all; nothing was sent (`format` names it);
|
|
971
|
+
* - `load-failed`: the device tried and gave up (codec it cannot decode, address it cannot reach);
|
|
972
|
+
* - `timeout`: the device did not answer.
|
|
973
|
+
*/
|
|
974
|
+
type RemotePlaybackErrorCode = 'unsupported-format' | 'load-failed' | 'timeout';
|
|
975
|
+
declare class RemotePlaybackError extends Error {
|
|
976
|
+
readonly code: RemotePlaybackErrorCode;
|
|
977
|
+
readonly format?: string | undefined;
|
|
978
|
+
constructor(code: RemotePlaybackErrorCode, message: string, format?: string | undefined);
|
|
979
|
+
}
|
|
980
|
+
/** The player error code a device failure is reported under: `REMOTE_UNSUPPORTED_FORMAT`, `REMOTE_LOAD_FAILED`… */
|
|
981
|
+
declare function remotePlaybackErrorCode(code: RemotePlaybackErrorCode): string;
|
|
982
|
+
/**
|
|
983
|
+
* The device failure behind a player error, if it is one: raised when the media was sent (a `RemotePlaybackError`),
|
|
984
|
+
* or reported later by the device while playing (`REMOTE_PLAYBACK`, which means it gave up on the media).
|
|
985
|
+
*/
|
|
986
|
+
declare function remotePlaybackFailure(error: {
|
|
987
|
+
code?: string;
|
|
988
|
+
details?: unknown;
|
|
989
|
+
} | null | undefined): {
|
|
990
|
+
code: RemotePlaybackErrorCode;
|
|
991
|
+
format?: string;
|
|
992
|
+
} | null;
|
|
993
|
+
interface RemotePlaybackStatus {
|
|
994
|
+
state: RemotePlayerState;
|
|
995
|
+
positionSec?: number;
|
|
996
|
+
durationSec?: number;
|
|
997
|
+
/** 0 to 1. */
|
|
998
|
+
volume?: number;
|
|
999
|
+
/** Set when the device gave up on the media (unreachable address, unsupported format). */
|
|
1000
|
+
error?: string;
|
|
1001
|
+
/** The media played to its end (devices that tell it apart from a stop). */
|
|
1002
|
+
ended?: boolean;
|
|
1003
|
+
}
|
|
1004
|
+
interface RemotePlaybackSession {
|
|
1005
|
+
readonly kind: RemotePlaybackKind;
|
|
1006
|
+
load(media: RemotePlaybackMedia): Promise<void>;
|
|
1007
|
+
play(): Promise<void>;
|
|
1008
|
+
pause(): Promise<void>;
|
|
1009
|
+
seek(positionSec: number): Promise<void>;
|
|
1010
|
+
/** 0 to 1. */
|
|
1011
|
+
setVolume(level: number): Promise<void>;
|
|
1012
|
+
/** Ends playback on the device. */
|
|
1013
|
+
stop(): Promise<void>;
|
|
1014
|
+
status(): Promise<RemotePlaybackStatus>;
|
|
1015
|
+
/** Leaves the device; what it plays keeps playing unless `stop` was called. */
|
|
1016
|
+
close(): Promise<void>;
|
|
1017
|
+
}
|
|
1018
|
+
/** `H:MM:SS` as UPnP and most devices write positions. */
|
|
1019
|
+
declare function formatRemoteClock(seconds: number): string;
|
|
1020
|
+
/** `H+:MM:SS[.F+]` to seconds; undefined for "NOT_IMPLEMENTED" and other placeholders. */
|
|
1021
|
+
declare function parseRemoteClock(value: string | undefined): number | undefined;
|
|
1022
|
+
|
|
1023
|
+
interface RemotePlaybackEngineOptions {
|
|
1024
|
+
/** The device session media goes to; the engine only claims sources while one is set. */
|
|
1025
|
+
session: () => RemotePlaybackSession | null;
|
|
1026
|
+
/** What to send for a source: an address the device can reach, its type and display metadata. */
|
|
1027
|
+
media: (source: PlayerSource, startSec: number) => RemotePlaybackMedia | Promise<RemotePlaybackMedia>;
|
|
1028
|
+
/**
|
|
1029
|
+
* The same media converted on the fly (`conversion`), for a device that refuses it as it is: a format or a codec
|
|
1030
|
+
* it does not read. Only where the app can convert; the device gets it once it has refused the original.
|
|
1031
|
+
*/
|
|
1032
|
+
convertedMedia?: (source: PlayerSource, startSec: number) => Promise<RemotePlaybackMedia>;
|
|
1033
|
+
/** How often the device is asked where it is. */
|
|
1034
|
+
pollMs?: number;
|
|
1035
|
+
/** Above the native (0) and adaptive engines, so a session takes every source. */
|
|
1036
|
+
priority?: number;
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Plays through a remote device (DLNA renderer, Cast device) with the player's own controls. Like a media element,
|
|
1040
|
+
* loading prepares the media and playing starts it: the device receives it on the first `play`, from the position
|
|
1041
|
+
* reached until then. The device's position and state come back by polling. A device that refuses a media gets it
|
|
1042
|
+
* converted when the app can convert it; a converted stream seeks by starting again from the new position.
|
|
1043
|
+
*/
|
|
1044
|
+
declare function createRemotePlaybackEngineFactory(options: RemotePlaybackEngineOptions): EngineFactory;
|
|
1045
|
+
declare class RemotePlaybackEngine implements PlaybackEngine {
|
|
1046
|
+
private readonly options;
|
|
1047
|
+
readonly id = "remote";
|
|
1048
|
+
private readonly listeners;
|
|
1049
|
+
private session;
|
|
1050
|
+
private source;
|
|
1051
|
+
/** What the device plays, as the app described it. */
|
|
1052
|
+
private media;
|
|
1053
|
+
private startSec;
|
|
1054
|
+
private sent;
|
|
1055
|
+
/** The device played the media to its end: it closed it, and only takes it again sent anew. */
|
|
1056
|
+
private finished;
|
|
1057
|
+
private durationSec;
|
|
1058
|
+
private positionSec;
|
|
1059
|
+
/** Where a converted stream starts in the media: the device counts from there. */
|
|
1060
|
+
private offsetSec;
|
|
1061
|
+
/** Furthest position the device reported since the media was sent: an end before any progress is a refusal. */
|
|
1062
|
+
private furthestSec;
|
|
1063
|
+
private conversionTried;
|
|
1064
|
+
private lastState;
|
|
1065
|
+
private timer;
|
|
1066
|
+
private polling;
|
|
1067
|
+
private failures;
|
|
1068
|
+
private destroyed;
|
|
1069
|
+
constructor(options: RemotePlaybackEngineOptions);
|
|
1070
|
+
attach(videoEl: HTMLVideoElement): Promise<void>;
|
|
1071
|
+
load(source: PlayerSource, opts?: LoadOptions): Promise<void>;
|
|
1072
|
+
play(): Promise<void>;
|
|
1073
|
+
pause(): Promise<void>;
|
|
1074
|
+
seekTo(timeSec: number): Promise<void>;
|
|
1075
|
+
setVolume(volume: number): Promise<void>;
|
|
1076
|
+
setPlaybackRate(_rate: number): Promise<void>;
|
|
1077
|
+
setTextTrack(_trackId: string | null): Promise<void>;
|
|
1078
|
+
setAudioTrack(_trackId: string | null): Promise<void>;
|
|
1079
|
+
setVideoTrack(_trackId: string | null): Promise<void>;
|
|
1080
|
+
getTracks(): Promise<MediaTrack[]>;
|
|
1081
|
+
onEvent(cb: (event: EngineEvent) => void): () => void;
|
|
1082
|
+
/** Stops following the device; the session stays open for the next media. */
|
|
1083
|
+
destroy(): Promise<void>;
|
|
1084
|
+
/** Reads the device once; exposed for hosts and tests that do not want to wait for the timer. */
|
|
1085
|
+
poll(): Promise<void>;
|
|
1086
|
+
private restartAt;
|
|
1087
|
+
/** Sends a media to the device from a position of it; a converted stream starts there and counts from 0. */
|
|
1088
|
+
private send;
|
|
1089
|
+
private convertible;
|
|
1090
|
+
/** The device gave up on the media as it is: it gets it converted, from where it was. */
|
|
1091
|
+
private convertAfterRefusal;
|
|
1092
|
+
private startPolling;
|
|
1093
|
+
private requireSession;
|
|
1094
|
+
private emit;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
846
1097
|
declare function applyRemoteConfig(cfg: PlayerConfig, ports: Partial<PlayerPorts>): Promise<{
|
|
847
1098
|
config: PlayerConfig;
|
|
848
1099
|
applied: boolean;
|
|
@@ -850,4 +1101,1391 @@ declare function applyRemoteConfig(cfg: PlayerConfig, ports: Partial<PlayerPorts
|
|
|
850
1101
|
source: 'cache' | 'network' | 'none';
|
|
851
1102
|
}>;
|
|
852
1103
|
|
|
853
|
-
|
|
1104
|
+
type MediaKind = 'audio' | 'video';
|
|
1105
|
+
interface MediaArtwork {
|
|
1106
|
+
src: string;
|
|
1107
|
+
kind?: 'cover' | 'poster' | 'backdrop' | 'thumbnail';
|
|
1108
|
+
}
|
|
1109
|
+
interface MediaProgress {
|
|
1110
|
+
currentTimeSec: number;
|
|
1111
|
+
durationSec?: number;
|
|
1112
|
+
updatedAt: number;
|
|
1113
|
+
}
|
|
1114
|
+
interface MediaItem {
|
|
1115
|
+
id: string;
|
|
1116
|
+
kind: MediaKind;
|
|
1117
|
+
title: string;
|
|
1118
|
+
path?: string;
|
|
1119
|
+
url?: string;
|
|
1120
|
+
subtitle?: string;
|
|
1121
|
+
artwork?: MediaArtwork[];
|
|
1122
|
+
durationSec?: number;
|
|
1123
|
+
sizeBytes?: number;
|
|
1124
|
+
mtimeMs?: number;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
interface AudioTrack extends MediaItem {
|
|
1128
|
+
kind: 'audio';
|
|
1129
|
+
artist?: string;
|
|
1130
|
+
album?: string;
|
|
1131
|
+
albumArtist?: string;
|
|
1132
|
+
genre?: string;
|
|
1133
|
+
trackNo?: number;
|
|
1134
|
+
discNo?: number;
|
|
1135
|
+
year?: number;
|
|
1136
|
+
}
|
|
1137
|
+
interface AudioAlbum<TTrack extends AudioTrack = AudioTrack> {
|
|
1138
|
+
id: string;
|
|
1139
|
+
title: string;
|
|
1140
|
+
artist?: string;
|
|
1141
|
+
artwork?: string;
|
|
1142
|
+
year?: number;
|
|
1143
|
+
tracks: TTrack[];
|
|
1144
|
+
}
|
|
1145
|
+
interface AudioArtist<TTrack extends AudioTrack = AudioTrack> {
|
|
1146
|
+
id: string;
|
|
1147
|
+
name: string;
|
|
1148
|
+
artwork?: string;
|
|
1149
|
+
tracks: TTrack[];
|
|
1150
|
+
}
|
|
1151
|
+
interface AudioGenre<TTrack extends AudioTrack = AudioTrack> {
|
|
1152
|
+
id: string;
|
|
1153
|
+
name: string;
|
|
1154
|
+
tracks: TTrack[];
|
|
1155
|
+
}
|
|
1156
|
+
interface AudioPlaylist {
|
|
1157
|
+
id: string;
|
|
1158
|
+
name: string;
|
|
1159
|
+
description?: string;
|
|
1160
|
+
artwork?: string;
|
|
1161
|
+
trackIds: string[];
|
|
1162
|
+
}
|
|
1163
|
+
type AudioPlaylistRef = AudioPlaylist;
|
|
1164
|
+
interface AudioLibrarySnapshot<TTrack extends AudioTrack = AudioTrack> {
|
|
1165
|
+
tracks: TTrack[];
|
|
1166
|
+
albums: AudioAlbum<TTrack>[];
|
|
1167
|
+
artists: AudioArtist<TTrack>[];
|
|
1168
|
+
genres: AudioGenre<TTrack>[];
|
|
1169
|
+
playlists: AudioPlaylist[];
|
|
1170
|
+
}
|
|
1171
|
+
interface AudioLibrarySearchResult<TTrack extends AudioTrack = AudioTrack> extends AudioLibrarySnapshot<TTrack> {
|
|
1172
|
+
active: boolean;
|
|
1173
|
+
query: string;
|
|
1174
|
+
directTrackIds: string[];
|
|
1175
|
+
matchedTrackIds: string[];
|
|
1176
|
+
matchedAlbumIds: string[];
|
|
1177
|
+
matchedArtistIds: string[];
|
|
1178
|
+
matchedGenreIds: string[];
|
|
1179
|
+
matchedPlaylistIds: string[];
|
|
1180
|
+
}
|
|
1181
|
+
declare function createAudioLibrarySnapshot<TTrack extends AudioTrack>(tracks: readonly TTrack[], playlists?: readonly AudioPlaylist[]): AudioLibrarySnapshot<TTrack>;
|
|
1182
|
+
declare function searchAudioLibrarySnapshot<TTrack extends AudioTrack>(snapshot: AudioLibrarySnapshot<TTrack>, searchTerm: string): AudioLibrarySearchResult<TTrack>;
|
|
1183
|
+
declare function normalizeAudioSearchText(value: string): string;
|
|
1184
|
+
declare function audioTrackMatchesSearch(track: AudioTrack, searchTerm: string): boolean;
|
|
1185
|
+
|
|
1186
|
+
/** Number of tracks whose metadata was changed, per kind of correction. */
|
|
1187
|
+
interface AudioMetadataCorrections {
|
|
1188
|
+
/** Artist or album artist spelled differently from the rest of the library, or prefixed by a track number. */
|
|
1189
|
+
artists: number;
|
|
1190
|
+
/** Album titles unified within the same album, disc suffixes moved to the disc number. */
|
|
1191
|
+
albums: number;
|
|
1192
|
+
/** Genres merged with their other spellings. */
|
|
1193
|
+
genres: number;
|
|
1194
|
+
/** Titles freed from a track number or artist prefix. */
|
|
1195
|
+
titles: number;
|
|
1196
|
+
/** Missing album artist, year, genre or track number filled from the rest of the same album. */
|
|
1197
|
+
filled: number;
|
|
1198
|
+
}
|
|
1199
|
+
interface AudioMetadataHarmony<TTrack extends AudioTrack> {
|
|
1200
|
+
tracks: TTrack[];
|
|
1201
|
+
corrections: AudioMetadataCorrections;
|
|
1202
|
+
/** Tracks with at least one correction. */
|
|
1203
|
+
correctedTracks: number;
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Makes a library consistent without inventing information: spellings of the same artist, album or genre are
|
|
1207
|
+
* unified on their most frequent form, and gaps are only filled from other tracks of the same album.
|
|
1208
|
+
*/
|
|
1209
|
+
declare function harmonizeAudioMetadata<TTrack extends AudioTrack>(tracks: readonly TTrack[]): AudioMetadataHarmony<TTrack>;
|
|
1210
|
+
/** Removes track-number prefixes (`01.Corneille`, `03 - Corneille`) and file-name underscores from a name. */
|
|
1211
|
+
declare function cleanAudioArtistName(value: string | undefined): string | undefined;
|
|
1212
|
+
/** Case, accent and punctuation insensitive key: `Jay-Z`, `JAY Z` and `jay z` share one key. Works for any media name. */
|
|
1213
|
+
declare function mediaMetadataKey(value: string): string;
|
|
1214
|
+
|
|
1215
|
+
/**
|
|
1216
|
+
* Transport used by the online catalogs: resolves a JSON document or throws.
|
|
1217
|
+
* Applications plug their HTTP client here (cache, retries, telemetry); the default uses `fetch`.
|
|
1218
|
+
*/
|
|
1219
|
+
interface OnlineJsonHttp {
|
|
1220
|
+
getJson(url: string, options?: {
|
|
1221
|
+
signal?: AbortSignal;
|
|
1222
|
+
}): Promise<unknown>;
|
|
1223
|
+
}
|
|
1224
|
+
declare function createFetchOnlineJsonHttp(fetchFn?: typeof fetch): OnlineJsonHttp;
|
|
1225
|
+
|
|
1226
|
+
/** AcoustID open fingerprint database (MIT server, CC BY-SA data). Free application keys for non-commercial use. */
|
|
1227
|
+
declare const ACOUSTID_ORIGIN = "https://api.acoustid.org";
|
|
1228
|
+
interface AcoustIdLookupQuery {
|
|
1229
|
+
/** Application key registered at acoustid.org. */
|
|
1230
|
+
clientKey: string;
|
|
1231
|
+
/** Compressed Chromaprint fingerprint (`AQAB…`). */
|
|
1232
|
+
fingerprint: string;
|
|
1233
|
+
durationSec: number;
|
|
1234
|
+
}
|
|
1235
|
+
interface AcoustIdRequestOptions {
|
|
1236
|
+
http?: OnlineJsonHttp;
|
|
1237
|
+
signal?: AbortSignal;
|
|
1238
|
+
origin?: string;
|
|
1239
|
+
}
|
|
1240
|
+
interface AcoustIdRecording {
|
|
1241
|
+
id: string;
|
|
1242
|
+
title?: string;
|
|
1243
|
+
artist?: string;
|
|
1244
|
+
durationSec?: number;
|
|
1245
|
+
}
|
|
1246
|
+
interface AcoustIdResult {
|
|
1247
|
+
id: string;
|
|
1248
|
+
/** Fingerprint similarity, 0 to 1. */
|
|
1249
|
+
score: number;
|
|
1250
|
+
recordings: AcoustIdRecording[];
|
|
1251
|
+
}
|
|
1252
|
+
declare function buildAcoustIdLookupUrl(query: AcoustIdLookupQuery, origin?: string): string;
|
|
1253
|
+
declare function lookupAcoustId(query: AcoustIdLookupQuery, request?: AcoustIdRequestOptions): Promise<AcoustIdResult[]>;
|
|
1254
|
+
|
|
1255
|
+
/** MusicBrainz open music encyclopedia (GPL server, CC0 core data). No key; one request per second per client. */
|
|
1256
|
+
declare const MUSICBRAINZ_ORIGIN = "https://musicbrainz.org";
|
|
1257
|
+
interface MusicBrainzRecordingQuery {
|
|
1258
|
+
title: string;
|
|
1259
|
+
artist?: string;
|
|
1260
|
+
durationSec?: number;
|
|
1261
|
+
/** Tolerance around `durationSec`, in seconds. */
|
|
1262
|
+
durationToleranceSec?: number;
|
|
1263
|
+
limit?: number;
|
|
1264
|
+
}
|
|
1265
|
+
interface MusicBrainzRequestOptions {
|
|
1266
|
+
/** Defaults to plain `fetch`; desktop shells should send a descriptive User-Agent and rate limit requests. */
|
|
1267
|
+
http?: OnlineJsonHttp;
|
|
1268
|
+
signal?: AbortSignal;
|
|
1269
|
+
origin?: string;
|
|
1270
|
+
}
|
|
1271
|
+
interface MusicBrainzRelease {
|
|
1272
|
+
id: string;
|
|
1273
|
+
title: string;
|
|
1274
|
+
artist?: string;
|
|
1275
|
+
status?: string;
|
|
1276
|
+
primaryType?: string;
|
|
1277
|
+
year?: number;
|
|
1278
|
+
discNo?: number;
|
|
1279
|
+
trackNo?: number;
|
|
1280
|
+
}
|
|
1281
|
+
interface MusicBrainzRecording {
|
|
1282
|
+
id: string;
|
|
1283
|
+
/** Search relevance, 0 to 100. */
|
|
1284
|
+
score: number;
|
|
1285
|
+
title: string;
|
|
1286
|
+
artist?: string;
|
|
1287
|
+
durationSec?: number;
|
|
1288
|
+
releases: MusicBrainzRelease[];
|
|
1289
|
+
}
|
|
1290
|
+
declare function buildMusicBrainzRecordingSearchUrl(query: MusicBrainzRecordingQuery, origin?: string): string;
|
|
1291
|
+
declare function searchMusicBrainzRecordings(query: MusicBrainzRecordingQuery, request?: MusicBrainzRequestOptions): Promise<MusicBrainzRecording[]>;
|
|
1292
|
+
/** Recording details by MusicBrainz id, with its releases, release groups and track positions. */
|
|
1293
|
+
declare function lookupMusicBrainzRecording(id: string, request?: MusicBrainzRequestOptions): Promise<MusicBrainzRecording | null>;
|
|
1294
|
+
declare function parseMusicBrainzRecording(entry: unknown): MusicBrainzRecording | null;
|
|
1295
|
+
|
|
1296
|
+
/** Bump when the matching rules change: tracks left unmatched by an older version are checked again once. */
|
|
1297
|
+
declare const AUDIO_IDENTIFICATION_VERSION = 1;
|
|
1298
|
+
/** Unmatched tracks are retried after 30 days, failed checks (offline, service down) after an hour. */
|
|
1299
|
+
declare const AUDIO_IDENTIFICATION_RETRY_MS: {
|
|
1300
|
+
readonly unmatched: number;
|
|
1301
|
+
readonly error: 3600000;
|
|
1302
|
+
};
|
|
1303
|
+
type AudioIdentifiableField = 'title' | 'artist' | 'albumArtist' | 'album' | 'year' | 'trackNo';
|
|
1304
|
+
interface AudioIdentificationInput {
|
|
1305
|
+
title: string;
|
|
1306
|
+
artist?: string;
|
|
1307
|
+
albumArtist?: string;
|
|
1308
|
+
album?: string;
|
|
1309
|
+
year?: number;
|
|
1310
|
+
trackNo?: number;
|
|
1311
|
+
durationSec?: number;
|
|
1312
|
+
/** Fields guessed from file and folder names instead of read from tags. */
|
|
1313
|
+
inferredFields?: readonly AudioIdentifiableField[];
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* - `trusted`: tags were complete, nothing to look up.
|
|
1317
|
+
* - `matched`: an online source confirmed the track; its fields fill what the tags lacked.
|
|
1318
|
+
* - `unmatched`: no reliable answer; retried much later.
|
|
1319
|
+
* - `error`: the check could not run; retried soon.
|
|
1320
|
+
*/
|
|
1321
|
+
type AudioIdentificationStatus = 'trusted' | 'matched' | 'unmatched' | 'error';
|
|
1322
|
+
/** `fingerprint`: Chromaprint + AcoustID recognise the sound itself; `text`: MusicBrainz search on title, artist and duration. */
|
|
1323
|
+
type AudioIdentificationMethod = 'fingerprint' | 'text';
|
|
1324
|
+
interface AudioIdentification {
|
|
1325
|
+
version: number;
|
|
1326
|
+
status: AudioIdentificationStatus;
|
|
1327
|
+
checkedAt: number;
|
|
1328
|
+
/** File size and modification time at check time: a changed file is checked again. */
|
|
1329
|
+
fileKey: string;
|
|
1330
|
+
source?: 'musicbrainz';
|
|
1331
|
+
/** Strongest method used for this check; a track left unmatched by text search is retried once fingerprints are available. */
|
|
1332
|
+
method?: AudioIdentificationMethod;
|
|
1333
|
+
recordingId?: string;
|
|
1334
|
+
releaseId?: string;
|
|
1335
|
+
confidence?: number;
|
|
1336
|
+
fields?: Partial<Pick<AudioIdentificationInput, AudioIdentifiableField>>;
|
|
1337
|
+
}
|
|
1338
|
+
interface AudioIdentificationMatch {
|
|
1339
|
+
status: 'matched' | 'unmatched';
|
|
1340
|
+
method?: AudioIdentificationMethod;
|
|
1341
|
+
recordingId?: string;
|
|
1342
|
+
releaseId?: string;
|
|
1343
|
+
confidence: number;
|
|
1344
|
+
fields: Partial<Pick<AudioIdentificationInput, AudioIdentifiableField>>;
|
|
1345
|
+
}
|
|
1346
|
+
/** Fields worth checking online: guessed from names or missing. Empty when tags are complete. */
|
|
1347
|
+
declare function audioIdentificationDoubts(input: AudioIdentificationInput): AudioIdentifiableField[];
|
|
1348
|
+
/**
|
|
1349
|
+
* Up-to-date tracks (trusted or matched) are never checked again while their file is unchanged. Unmatched tracks are
|
|
1350
|
+
* retried when a stronger method or newer rules become available, or after a long delay; failed checks soon.
|
|
1351
|
+
*/
|
|
1352
|
+
declare function needsAudioIdentification(previous: AudioIdentification | undefined, fileKey: string, now: number, methods?: readonly AudioIdentificationMethod[]): boolean;
|
|
1353
|
+
/**
|
|
1354
|
+
* Picks the MusicBrainz recording behind a fingerprint. The sound decides; names only break ties between the
|
|
1355
|
+
* recordings AcoustID links to the same fingerprint.
|
|
1356
|
+
*/
|
|
1357
|
+
declare function pickAcoustIdRecording(input: AudioIdentificationInput, results: readonly AcoustIdResult[]): {
|
|
1358
|
+
recordingId: string;
|
|
1359
|
+
confidence: number;
|
|
1360
|
+
} | null;
|
|
1361
|
+
/** Metadata of a recording recognised by its fingerprint; album rules are the same as for text matches. */
|
|
1362
|
+
declare function matchFingerprintedRecording(input: AudioIdentificationInput, recording: MusicBrainzRecording | null, confidence: number): AudioIdentificationMatch;
|
|
1363
|
+
/**
|
|
1364
|
+
* Accepts a recording only when title, artist and duration agree with the file. Without an artist a name search is
|
|
1365
|
+
* never trusted ("Track 07" exists many times over): only a fingerprint can identify such files. Album data is taken from a release
|
|
1366
|
+
* with the same album title, or from the artist's own album when the file has no album at all, so folders stay
|
|
1367
|
+
* grouped the way the user organised them.
|
|
1368
|
+
*/
|
|
1369
|
+
declare function matchMusicBrainzRecording(input: AudioIdentificationInput, recordings: readonly MusicBrainzRecording[]): AudioIdentificationMatch;
|
|
1370
|
+
/** Identification fills guessed or missing fields only: values read from tags are never overwritten. */
|
|
1371
|
+
declare function applyAudioIdentification<TItem extends AudioIdentificationInput>(item: TItem, identification: AudioIdentification | undefined): TItem;
|
|
1372
|
+
|
|
1373
|
+
/** A library item as the routine reads it: the identification state and what tells a changed file apart. */
|
|
1374
|
+
interface IdentifiableItem extends AudioIdentificationInput {
|
|
1375
|
+
id: string;
|
|
1376
|
+
identification?: AudioIdentification;
|
|
1377
|
+
size: number;
|
|
1378
|
+
/** Modification time in milliseconds; with the size it says whether the file changed. */
|
|
1379
|
+
mtimeMs: number;
|
|
1380
|
+
}
|
|
1381
|
+
/** What the routine needs from a library, on the desktop as on a phone. */
|
|
1382
|
+
interface IdentificationLibrary<TItem extends IdentifiableItem = IdentifiableItem> {
|
|
1383
|
+
identificationCandidates(): readonly TItem[];
|
|
1384
|
+
recordIdentification(id: string, identification: AudioIdentification): void;
|
|
1385
|
+
setIdentificationProgress(progress: {
|
|
1386
|
+
identifying: boolean;
|
|
1387
|
+
identified: number;
|
|
1388
|
+
identifyTotal: number;
|
|
1389
|
+
}): void;
|
|
1390
|
+
notifyChanged(): void;
|
|
1391
|
+
/** Writes the index; the routine calls it once a run is over. */
|
|
1392
|
+
flush(): Promise<void> | void;
|
|
1393
|
+
}
|
|
1394
|
+
interface IdentificationRoutineOptions<TItem extends IdentifiableItem = IdentifiableItem> {
|
|
1395
|
+
library: IdentificationLibrary<TItem>;
|
|
1396
|
+
identify(item: TItem): Promise<AudioIdentificationMatch>;
|
|
1397
|
+
/** Methods available now; unmatched tracks are tried again when a stronger one appears (fingerprints). */
|
|
1398
|
+
methods?: readonly AudioIdentificationMethod[];
|
|
1399
|
+
source?: AudioIdentification['source'];
|
|
1400
|
+
now?: () => number;
|
|
1401
|
+
retryIntervalMs?: number;
|
|
1402
|
+
/** Offline or service down: a run stops after this many failures in a row. */
|
|
1403
|
+
maxConsecutiveErrors?: number;
|
|
1404
|
+
onError?: (error: unknown) => void;
|
|
1405
|
+
}
|
|
1406
|
+
interface IdentificationRoutine {
|
|
1407
|
+
/** Runs once now; a call during a run schedules another pass, so new files are never missed. */
|
|
1408
|
+
run(): Promise<void>;
|
|
1409
|
+
/** Runs now and then every `retryIntervalMs`. */
|
|
1410
|
+
start(): void;
|
|
1411
|
+
dispose(): void;
|
|
1412
|
+
}
|
|
1413
|
+
/** `size:mtime`, which changes when the file behind an item changes. */
|
|
1414
|
+
declare function fileKeyFor(item: {
|
|
1415
|
+
size: number;
|
|
1416
|
+
mtimeMs: number;
|
|
1417
|
+
}): string;
|
|
1418
|
+
declare function createIdentificationRoutine<TItem extends IdentifiableItem>(options: IdentificationRoutineOptions<TItem>): IdentificationRoutine;
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* Media library model shared by the desktop and mobile shells: file filtering, title cleanup and audio/video
|
|
1422
|
+
* classification from tags, file names and folders. Paths are relative to the library folder and use "/".
|
|
1423
|
+
*/
|
|
1424
|
+
/** Bump when classification changes: indexed files are read again once. */
|
|
1425
|
+
declare const MEDIA_LIBRARY_MODEL_VERSION = 3;
|
|
1426
|
+
type MediaLibraryKind = 'audio' | 'video';
|
|
1427
|
+
type AudioInferredField = 'title' | 'artist' | 'albumArtist' | 'album' | 'trackNo';
|
|
1428
|
+
/** The subset of music-metadata results the model reads. */
|
|
1429
|
+
interface MediaTags {
|
|
1430
|
+
title?: string;
|
|
1431
|
+
artist?: string;
|
|
1432
|
+
artists?: string[];
|
|
1433
|
+
albumartist?: string;
|
|
1434
|
+
album?: string;
|
|
1435
|
+
genre?: string | string[];
|
|
1436
|
+
track?: {
|
|
1437
|
+
no?: number | null;
|
|
1438
|
+
};
|
|
1439
|
+
disk?: {
|
|
1440
|
+
no?: number | null;
|
|
1441
|
+
};
|
|
1442
|
+
year?: number;
|
|
1443
|
+
stik?: number;
|
|
1444
|
+
tvShow?: string;
|
|
1445
|
+
tvSeason?: number;
|
|
1446
|
+
tvEpisode?: number;
|
|
1447
|
+
}
|
|
1448
|
+
interface MediaFormat {
|
|
1449
|
+
duration?: number;
|
|
1450
|
+
}
|
|
1451
|
+
interface MediaTagsInput {
|
|
1452
|
+
/** Path of the file inside the library folder, e.g. `Artist/Album/01 - Song.mp3`. */
|
|
1453
|
+
relativePath: string;
|
|
1454
|
+
common?: MediaTags;
|
|
1455
|
+
format?: MediaFormat;
|
|
1456
|
+
}
|
|
1457
|
+
interface AudioLibraryFields {
|
|
1458
|
+
title: string;
|
|
1459
|
+
artist?: string;
|
|
1460
|
+
albumArtist?: string;
|
|
1461
|
+
album?: string;
|
|
1462
|
+
genre?: string;
|
|
1463
|
+
trackNo?: number;
|
|
1464
|
+
discNo?: number;
|
|
1465
|
+
year?: number;
|
|
1466
|
+
durationSec?: number;
|
|
1467
|
+
/** Fields guessed from file and folder names: identification may correct them, never tagged values. */
|
|
1468
|
+
inferredFields?: AudioInferredField[];
|
|
1469
|
+
}
|
|
1470
|
+
interface VideoLibraryFields {
|
|
1471
|
+
category: 'movie' | 'episode' | 'other';
|
|
1472
|
+
title: string;
|
|
1473
|
+
series?: string;
|
|
1474
|
+
season?: number;
|
|
1475
|
+
episode?: number;
|
|
1476
|
+
year?: number;
|
|
1477
|
+
durationSec?: number;
|
|
1478
|
+
}
|
|
1479
|
+
declare function mediaFileExtension(fileName: string): string;
|
|
1480
|
+
declare function isMediaFileName(fileName: string, kind: MediaLibraryKind): boolean;
|
|
1481
|
+
declare function isImageFileName(fileName: string): boolean;
|
|
1482
|
+
declare function isIgnoredDirectoryName(name: string): boolean;
|
|
1483
|
+
declare function mediaContentType(fileName: string): string;
|
|
1484
|
+
/** Rank of a folder image as album or poster art (0 is best), -1 when it is not one. */
|
|
1485
|
+
declare function folderArtworkRank(fileName: string): number;
|
|
1486
|
+
declare function cleanMediaTitle(fileStemValue: string): string;
|
|
1487
|
+
declare function parseEpisodeFileName(stem: string): {
|
|
1488
|
+
series?: string;
|
|
1489
|
+
season?: number;
|
|
1490
|
+
episode?: number;
|
|
1491
|
+
episodeTitle?: string;
|
|
1492
|
+
} | null;
|
|
1493
|
+
declare function audioFieldsFromTags({ relativePath, common, format }: MediaTagsInput): AudioLibraryFields;
|
|
1494
|
+
declare function videoFieldsFromTags({ relativePath, common, format }: MediaTagsInput): VideoLibraryFields;
|
|
1495
|
+
|
|
1496
|
+
/**
|
|
1497
|
+
* Media servers the apps can browse and play from: a home server (Jellyfin), a UPnP/DLNA server, a Subsonic music
|
|
1498
|
+
* server or a plain WebDAV share. Connectors translate each protocol into the same containers and items, so the
|
|
1499
|
+
* library views and the queue treat every source alike.
|
|
1500
|
+
*/
|
|
1501
|
+
type MediaSourceKind = 'jellyfin' | 'dlna' | 'subsonic' | 'webdav';
|
|
1502
|
+
/** What the listener fills in to connect a server. */
|
|
1503
|
+
interface MediaSourceInput {
|
|
1504
|
+
kind: MediaSourceKind;
|
|
1505
|
+
/** Address of the server, with or without scheme; connectors normalise it. */
|
|
1506
|
+
address: string;
|
|
1507
|
+
name?: string;
|
|
1508
|
+
username?: string;
|
|
1509
|
+
password?: string;
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* A connected server. `secret` is the credential kept to reach it again (an access token when the server issues one,
|
|
1513
|
+
* the password otherwise); applications store it with their own preferences and never send it anywhere else.
|
|
1514
|
+
*/
|
|
1515
|
+
interface MediaSourceAccount {
|
|
1516
|
+
id: string;
|
|
1517
|
+
kind: MediaSourceKind;
|
|
1518
|
+
name: string;
|
|
1519
|
+
baseUrl: string;
|
|
1520
|
+
username?: string;
|
|
1521
|
+
secret?: string;
|
|
1522
|
+
/** Account identifier on the server, when it has one. */
|
|
1523
|
+
userId?: string;
|
|
1524
|
+
}
|
|
1525
|
+
/** A browsable node: a library, a folder, an album, a series. */
|
|
1526
|
+
interface MediaSourceContainer {
|
|
1527
|
+
id: string;
|
|
1528
|
+
title: string;
|
|
1529
|
+
subtitle?: string;
|
|
1530
|
+
artworkUrl?: string;
|
|
1531
|
+
/** What it holds, when the server says so. */
|
|
1532
|
+
kind?: 'audio' | 'video' | 'mixed';
|
|
1533
|
+
childCount?: number;
|
|
1534
|
+
}
|
|
1535
|
+
/** A playable file, in the shape the libraries already use. */
|
|
1536
|
+
interface MediaSourceItem {
|
|
1537
|
+
id: string;
|
|
1538
|
+
kind: 'audio' | 'video';
|
|
1539
|
+
title: string;
|
|
1540
|
+
url: string;
|
|
1541
|
+
artworkUrl?: string;
|
|
1542
|
+
durationSec?: number;
|
|
1543
|
+
artist?: string;
|
|
1544
|
+
albumArtist?: string;
|
|
1545
|
+
album?: string;
|
|
1546
|
+
genre?: string;
|
|
1547
|
+
year?: number;
|
|
1548
|
+
trackNo?: number;
|
|
1549
|
+
discNo?: number;
|
|
1550
|
+
series?: string;
|
|
1551
|
+
season?: number;
|
|
1552
|
+
episode?: number;
|
|
1553
|
+
container?: string;
|
|
1554
|
+
}
|
|
1555
|
+
interface MediaSourcePage {
|
|
1556
|
+
containers: MediaSourceContainer[];
|
|
1557
|
+
items: MediaSourceItem[];
|
|
1558
|
+
/** Total number of items in the container, when the server reports it. */
|
|
1559
|
+
total?: number;
|
|
1560
|
+
}
|
|
1561
|
+
interface MediaSourceRequest {
|
|
1562
|
+
url: string;
|
|
1563
|
+
method?: 'GET' | 'POST' | 'PROPFIND';
|
|
1564
|
+
headers?: Record<string, string>;
|
|
1565
|
+
json?: unknown;
|
|
1566
|
+
body?: string;
|
|
1567
|
+
signal?: AbortSignal;
|
|
1568
|
+
/** For a request that waits on purpose (another device asking its user); transports keep their own otherwise. */
|
|
1569
|
+
timeoutMs?: number;
|
|
1570
|
+
}
|
|
1571
|
+
interface MediaSourceResponse {
|
|
1572
|
+
status: number;
|
|
1573
|
+
json(): Promise<unknown>;
|
|
1574
|
+
text(): Promise<string>;
|
|
1575
|
+
}
|
|
1576
|
+
/** Transport used by connectors; applications plug their HTTP client here. */
|
|
1577
|
+
interface MediaSourceHttp {
|
|
1578
|
+
send(request: MediaSourceRequest): Promise<MediaSourceResponse>;
|
|
1579
|
+
}
|
|
1580
|
+
interface MediaSourceBrowseOptions {
|
|
1581
|
+
containerId?: string;
|
|
1582
|
+
/** Filters what the container returns, for apps that only play one kind. */
|
|
1583
|
+
kind?: 'audio' | 'video';
|
|
1584
|
+
search?: string;
|
|
1585
|
+
startIndex?: number;
|
|
1586
|
+
limit?: number;
|
|
1587
|
+
signal?: AbortSignal;
|
|
1588
|
+
}
|
|
1589
|
+
interface MediaSourceConnector {
|
|
1590
|
+
kind: MediaSourceKind;
|
|
1591
|
+
/** Checks the address and credentials, and returns the account to keep. */
|
|
1592
|
+
connect(input: MediaSourceInput, http: MediaSourceHttp): Promise<MediaSourceAccount>;
|
|
1593
|
+
/** Libraries and folders at the root, then their contents. */
|
|
1594
|
+
browse(account: MediaSourceAccount, options: MediaSourceBrowseOptions, http: MediaSourceHttp): Promise<MediaSourcePage>;
|
|
1595
|
+
/**
|
|
1596
|
+
* Lyrics the server holds for a track, as LRC text or plain words, or null when it has none. Servers that keep
|
|
1597
|
+
* no lyrics leave this out.
|
|
1598
|
+
*/
|
|
1599
|
+
lyrics?(account: MediaSourceAccount, item: MediaSourceItem, http: MediaSourceHttp): Promise<string | null>;
|
|
1600
|
+
}
|
|
1601
|
+
/** `https://host:port/path` without a trailing slash; plain http is kept for servers on a local network. */
|
|
1602
|
+
declare function normalizeMediaSourceAddress(address: string): string;
|
|
1603
|
+
/** Stable id of an item or container inside an account, so queues and playlists survive a restart. */
|
|
1604
|
+
declare function mediaSourceItemId(accountId: string, remoteId: string): string;
|
|
1605
|
+
declare function parseMediaSourceItemId(id: string): {
|
|
1606
|
+
accountId: string;
|
|
1607
|
+
remoteId: string;
|
|
1608
|
+
} | null;
|
|
1609
|
+
/** Ticks used by Jellyfin and Emby (100 ns units) to seconds. */
|
|
1610
|
+
declare function ticksToSeconds(ticks: unknown): number | undefined;
|
|
1611
|
+
|
|
1612
|
+
type RepeatMode = 'off' | 'one' | 'all';
|
|
1613
|
+
interface QueueItem<TItem = unknown> {
|
|
1614
|
+
id: string;
|
|
1615
|
+
item: TItem;
|
|
1616
|
+
}
|
|
1617
|
+
interface QueueSnapshot<TItem = unknown> {
|
|
1618
|
+
items: QueueItem<TItem>[];
|
|
1619
|
+
currentIndex: number;
|
|
1620
|
+
shuffle: boolean;
|
|
1621
|
+
repeat: RepeatMode;
|
|
1622
|
+
history: number[];
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
/**
|
|
1626
|
+
* Cutting a piece of a track to use as a ringtone.
|
|
1627
|
+
*
|
|
1628
|
+
* The piece is taken from where the listener is: they were listening to the part they want, and
|
|
1629
|
+
* asking them to find it again in a waveform is asking them to do the work twice. So the start
|
|
1630
|
+
* is the playhead, the length is a choice between a few, and what comes out is a real file.
|
|
1631
|
+
*
|
|
1632
|
+
* Both edges are faded. A ringtone cut straight out of the middle of a track starts with a click
|
|
1633
|
+
* and ends with one, which is the difference between something made and something taken.
|
|
1634
|
+
*/
|
|
1635
|
+
/** How long a ringtone may be. Android accepts more; a phone that rings for half a minute is enough. */
|
|
1636
|
+
declare const RINGTONE_MAX_SEC = 30;
|
|
1637
|
+
/** Long enough not to click, short enough not to be heard as a fade. */
|
|
1638
|
+
declare const RINGTONE_FADE_SEC = 0.05;
|
|
1639
|
+
interface RingtoneSegment {
|
|
1640
|
+
startSec: number;
|
|
1641
|
+
lengthSec: number;
|
|
1642
|
+
}
|
|
1643
|
+
/**
|
|
1644
|
+
* Where the piece starts and how long it runs, kept inside the track.
|
|
1645
|
+
*
|
|
1646
|
+
* A playhead near the end would give a piece that runs off it, so the start slides back to make
|
|
1647
|
+
* room; a track shorter than the length asked for simply gives all of itself.
|
|
1648
|
+
*/
|
|
1649
|
+
declare function ringtoneSegment(options: {
|
|
1650
|
+
positionSec: number;
|
|
1651
|
+
trackDurationSec: number;
|
|
1652
|
+
lengthSec: number;
|
|
1653
|
+
}): RingtoneSegment;
|
|
1654
|
+
/**
|
|
1655
|
+
* Fades both edges in place, so the piece does not begin or end on a click.
|
|
1656
|
+
*
|
|
1657
|
+
* A piece too short to hold two fades gets shorter ones rather than none: the click is what is
|
|
1658
|
+
* being removed, and half a fade removes it just as well.
|
|
1659
|
+
*/
|
|
1660
|
+
declare function fadeEdges(channels: Float32Array[], sampleRate: number, fadeSec?: number): void;
|
|
1661
|
+
/**
|
|
1662
|
+
* Writes audio as a WAV file: 16-bit PCM, the one encoding a page can produce on its own.
|
|
1663
|
+
*
|
|
1664
|
+
* It is not small, and it is not meant to be kept — it is what is handed to the system, which
|
|
1665
|
+
* stores a ringtone however it likes. What matters is that no library has to be shipped to make
|
|
1666
|
+
* one, and that every platform reads it.
|
|
1667
|
+
*/
|
|
1668
|
+
declare function encodeWav(channels: Float32Array[], sampleRate: number): ArrayBuffer;
|
|
1669
|
+
/** A file name from a track's title: what a viewer will look for in their files. */
|
|
1670
|
+
declare function ringtoneFileName(title: string, extension?: string): string;
|
|
1671
|
+
|
|
1672
|
+
/**
|
|
1673
|
+
* Shaking the device, read from its accelerometer.
|
|
1674
|
+
*
|
|
1675
|
+
* A shake is not one movement but several: the hand throws the device one way and pulls it back,
|
|
1676
|
+
* and does it again. So what is counted is not force but **reversals** — the number of times the
|
|
1677
|
+
* movement changed its mind inside a short window. That is what tells a shake apart from setting
|
|
1678
|
+
* the phone down hard, from a pocket, from a car going over a kerb.
|
|
1679
|
+
*
|
|
1680
|
+
* Direction is deliberately not read. A sideways shake does carry a first impulse one way or the
|
|
1681
|
+
* other, but which way that is depends on how the device is being held, and a control that works
|
|
1682
|
+
* most of the time is worse than one that works the same way every time. So a shake means one
|
|
1683
|
+
* thing, and two shakes in a row mean the other — the same bargain a double click makes.
|
|
1684
|
+
*
|
|
1685
|
+
* One thing follows from that and is worth saying rather than hiding: a shake that simply goes
|
|
1686
|
+
* on long enough crosses the rest and is heard as a second one. Someone still shaking after a
|
|
1687
|
+
* second and a half meant more than "the next one", so reading it as the other gesture is the
|
|
1688
|
+
* least surprising of the available wrong answers.
|
|
1689
|
+
*/
|
|
1690
|
+
/** One reading of the accelerometer, in m/s², gravity included or not — only the changes are read. */
|
|
1691
|
+
interface ShakeSample {
|
|
1692
|
+
x: number;
|
|
1693
|
+
y: number;
|
|
1694
|
+
z: number;
|
|
1695
|
+
/** When it was taken, in milliseconds. */
|
|
1696
|
+
at: number;
|
|
1697
|
+
}
|
|
1698
|
+
type ShakeGesture = 'single' | 'double';
|
|
1699
|
+
interface ShakeOptions {
|
|
1700
|
+
/** How hard a movement has to be before it counts as one, in m/s² of change. */
|
|
1701
|
+
force?: number;
|
|
1702
|
+
/** How many changes of direction make a shake. */
|
|
1703
|
+
reversals?: number;
|
|
1704
|
+
/** How long those changes have to happen inside, in milliseconds. */
|
|
1705
|
+
window?: number;
|
|
1706
|
+
/** How long to ignore the accelerometer after a shake, so one shake is not read as three. */
|
|
1707
|
+
rest?: number;
|
|
1708
|
+
/**
|
|
1709
|
+
* How long a second shake has to arrive within to mean "the other way", counted from the first.
|
|
1710
|
+
* It has to leave room for the rest above, or a second shake would be swallowed before it could
|
|
1711
|
+
* be heard — so the two are set together, not one at a time.
|
|
1712
|
+
*/
|
|
1713
|
+
pairWindow?: number;
|
|
1714
|
+
}
|
|
1715
|
+
/**
|
|
1716
|
+
* Reads samples and says when a shake happened.
|
|
1717
|
+
*
|
|
1718
|
+
* A single shake is reported only once it is clear no second one is coming, so that a double
|
|
1719
|
+
* shake never arrives as a single one first and is then contradicted.
|
|
1720
|
+
*/
|
|
1721
|
+
declare function createShakeDetector(options?: ShakeOptions): {
|
|
1722
|
+
read: (sample: ShakeSample) => ShakeGesture | null;
|
|
1723
|
+
settle: (now: number) => ShakeGesture | null;
|
|
1724
|
+
pendingFor: (now: number) => number;
|
|
1725
|
+
reset: () => void;
|
|
1726
|
+
};
|
|
1727
|
+
|
|
1728
|
+
/**
|
|
1729
|
+
* Media kept behind a lock.
|
|
1730
|
+
*
|
|
1731
|
+
* What is locked is not merely refused: it is **absent**. It is in no shelf, no search, no
|
|
1732
|
+
* history, no queue, until the vault is opened. Anything less tells whoever is holding the
|
|
1733
|
+
* device that there is something to look for, which is the thing a lock is meant to avoid.
|
|
1734
|
+
*
|
|
1735
|
+
* The code is never kept. What is kept is a salt and the result of stretching the code through
|
|
1736
|
+
* PBKDF2 with it, so the stored record cannot be read backwards, and the same code typed twice
|
|
1737
|
+
* on two devices does not produce the same record.
|
|
1738
|
+
*/
|
|
1739
|
+
/** What is written down about the code. The code itself never is. */
|
|
1740
|
+
interface VaultPasscode {
|
|
1741
|
+
/** Random, per vault, so two vaults with the same code do not look alike. */
|
|
1742
|
+
salt: string;
|
|
1743
|
+
/** The stretched code, base64. */
|
|
1744
|
+
hash: string;
|
|
1745
|
+
/** How many rounds produced it, so a record made today still opens after the cost is raised. */
|
|
1746
|
+
rounds: number;
|
|
1747
|
+
}
|
|
1748
|
+
/** The parts of Web Crypto this needs, named so a test can stand in for them. */
|
|
1749
|
+
interface VaultCrypto {
|
|
1750
|
+
getRandomValues(array: Uint8Array): Uint8Array;
|
|
1751
|
+
subtle: Pick<SubtleCrypto, 'importKey' | 'deriveBits'>;
|
|
1752
|
+
}
|
|
1753
|
+
declare const VAULT_ROUNDS = 310000;
|
|
1754
|
+
/**
|
|
1755
|
+
* Stretches a code into something that can be written down.
|
|
1756
|
+
*
|
|
1757
|
+
* The rounds are the cost of one guess. They are stored with the record rather than assumed, so
|
|
1758
|
+
* raising them later does not lock anyone out of a vault they already have.
|
|
1759
|
+
*/
|
|
1760
|
+
declare function hashPasscode(code: string, crypto: VaultCrypto, options?: {
|
|
1761
|
+
salt?: string;
|
|
1762
|
+
rounds?: number;
|
|
1763
|
+
}): Promise<VaultPasscode>;
|
|
1764
|
+
/**
|
|
1765
|
+
* Whether a code opens a record. Compared in constant time, so the time an answer takes says
|
|
1766
|
+
* nothing about how much of the code was right.
|
|
1767
|
+
*/
|
|
1768
|
+
declare function verifyPasscode(code: string, record: VaultPasscode | null, crypto: VaultCrypto): Promise<boolean>;
|
|
1769
|
+
/** A code short enough to guess is not a code. Four is the floor every phone uses. */
|
|
1770
|
+
declare function isUsablePasscode(code: string): boolean;
|
|
1771
|
+
/**
|
|
1772
|
+
* The ids that must not be shown.
|
|
1773
|
+
*
|
|
1774
|
+
* Nothing is hidden while the vault is open, and nothing is hidden when there is no code — a
|
|
1775
|
+
* vault nobody can open is a way of losing media, not of protecting it.
|
|
1776
|
+
*/
|
|
1777
|
+
declare function hiddenIds(locked: Iterable<string>, state: {
|
|
1778
|
+
open: boolean;
|
|
1779
|
+
hasPasscode: boolean;
|
|
1780
|
+
}): Set<string>;
|
|
1781
|
+
/** Keeps only what may be seen, in the order it was given. */
|
|
1782
|
+
declare function withoutHidden<T>(items: readonly T[], idOf: (item: T) => string, hidden: ReadonlySet<string>): T[];
|
|
1783
|
+
|
|
1784
|
+
type DevicePlaybackKind = RemotePlaybackKind;
|
|
1785
|
+
type DevicePlaybackStatus = 'idle' | 'connecting' | 'playing' | 'failed';
|
|
1786
|
+
interface PlaybackDevice {
|
|
1787
|
+
id: string;
|
|
1788
|
+
name: string;
|
|
1789
|
+
kind: DevicePlaybackKind;
|
|
1790
|
+
address: string;
|
|
1791
|
+
model?: string;
|
|
1792
|
+
connected?: boolean;
|
|
1793
|
+
}
|
|
1794
|
+
interface DevicePlaybackState {
|
|
1795
|
+
status: DevicePlaybackStatus;
|
|
1796
|
+
activeDeviceId: string | null;
|
|
1797
|
+
activeDeviceName: string | null;
|
|
1798
|
+
targetUrl?: string;
|
|
1799
|
+
message?: string;
|
|
1800
|
+
error?: string;
|
|
1801
|
+
}
|
|
1802
|
+
type DevicePlaybackEvent = {
|
|
1803
|
+
type: 'connect';
|
|
1804
|
+
device: PlaybackDevice;
|
|
1805
|
+
targetUrl: string;
|
|
1806
|
+
message?: string;
|
|
1807
|
+
} | {
|
|
1808
|
+
type: 'playing';
|
|
1809
|
+
deviceId: string;
|
|
1810
|
+
message?: string;
|
|
1811
|
+
} | {
|
|
1812
|
+
type: 'fail';
|
|
1813
|
+
deviceId: string;
|
|
1814
|
+
error: string;
|
|
1815
|
+
message?: string;
|
|
1816
|
+
} | {
|
|
1817
|
+
type: 'stop';
|
|
1818
|
+
deviceId?: string;
|
|
1819
|
+
message?: string;
|
|
1820
|
+
};
|
|
1821
|
+
declare function createIdleDevicePlaybackState(message?: string): DevicePlaybackState;
|
|
1822
|
+
declare function reduceDevicePlaybackState(state: DevicePlaybackState, event: DevicePlaybackEvent): DevicePlaybackState;
|
|
1823
|
+
|
|
1824
|
+
/**
|
|
1825
|
+
* Local copies of online media (Internet Archive tracks): instant starts, seamless queue transitions and playback
|
|
1826
|
+
* through network drops. Downloads run one at a time in the background and the cache stays under a byte budget by
|
|
1827
|
+
* evicting the least recently used copies, never the ones the queue still needs.
|
|
1828
|
+
*/
|
|
1829
|
+
interface MediaCacheEntry {
|
|
1830
|
+
key: string;
|
|
1831
|
+
url: string;
|
|
1832
|
+
size: number;
|
|
1833
|
+
contentType: string;
|
|
1834
|
+
storedAt: number;
|
|
1835
|
+
lastUsedAt: number;
|
|
1836
|
+
}
|
|
1837
|
+
/** Persistence of cached media; browsers implement it with IndexedDB. */
|
|
1838
|
+
interface MediaCacheStore {
|
|
1839
|
+
entries(): Promise<MediaCacheEntry[]>;
|
|
1840
|
+
read(key: string): Promise<Blob | null>;
|
|
1841
|
+
write(entry: MediaCacheEntry, data: Blob): Promise<void>;
|
|
1842
|
+
touch(key: string, lastUsedAt: number): Promise<void>;
|
|
1843
|
+
remove(keys: readonly string[]): Promise<void>;
|
|
1844
|
+
}
|
|
1845
|
+
interface MediaCacheRequest {
|
|
1846
|
+
key: string;
|
|
1847
|
+
url: string;
|
|
1848
|
+
/** Tried in order when `url` cannot be downloaded (network, HTTP or CORS failure). */
|
|
1849
|
+
alternateUrls?: readonly string[];
|
|
1850
|
+
}
|
|
1851
|
+
interface MediaCacheUsage {
|
|
1852
|
+
bytes: number;
|
|
1853
|
+
count: number;
|
|
1854
|
+
budgetBytes: number;
|
|
1855
|
+
}
|
|
1856
|
+
interface MediaCacheOptions {
|
|
1857
|
+
store: MediaCacheStore;
|
|
1858
|
+
budgetBytes: number;
|
|
1859
|
+
/** Larger media are streamed only. Defaults to a quarter of the budget. */
|
|
1860
|
+
maxEntryBytes?: number;
|
|
1861
|
+
fetch?: typeof fetch;
|
|
1862
|
+
now?: () => number;
|
|
1863
|
+
/** Background downloads wait while this returns false (data saver, slow connection). */
|
|
1864
|
+
canDownload?: () => boolean;
|
|
1865
|
+
}
|
|
1866
|
+
interface MediaCache {
|
|
1867
|
+
/** Resolves once the stored index is loaded; `has` answers false before. */
|
|
1868
|
+
readonly ready: Promise<void>;
|
|
1869
|
+
has(key: string): boolean;
|
|
1870
|
+
/** The local copy, marked as recently used; null when absent or unreadable. */
|
|
1871
|
+
open(key: string): Promise<Blob | null>;
|
|
1872
|
+
/**
|
|
1873
|
+
* Media the player needs next, most urgent first. Missing ones are downloaded in that order; a download no longer
|
|
1874
|
+
* requested is cancelled. Requested keys are protected from eviction until the next call.
|
|
1875
|
+
*/
|
|
1876
|
+
keep(requests: readonly MediaCacheRequest[]): void;
|
|
1877
|
+
usage(): MediaCacheUsage;
|
|
1878
|
+
clear(): Promise<void>;
|
|
1879
|
+
/** Called after downloads, evictions and clears. */
|
|
1880
|
+
onChange(listener: () => void): () => void;
|
|
1881
|
+
}
|
|
1882
|
+
/** Keys to evict, least recently used first, so that `incomingBytes` fits in the budget; null when it cannot fit. */
|
|
1883
|
+
declare function planMediaCacheEviction(entries: readonly MediaCacheEntry[], budgetBytes: number, incomingBytes: number, protectedKeys?: ReadonlySet<string>): string[] | null;
|
|
1884
|
+
declare function createMediaCache(options: MediaCacheOptions): MediaCache;
|
|
1885
|
+
|
|
1886
|
+
/**
|
|
1887
|
+
* Jellyfin, the free software home media server (GPL). One account gives music and videos with their covers,
|
|
1888
|
+
* artists, series and seasons; files are played as they are stored, so seeking works through byte ranges.
|
|
1889
|
+
*/
|
|
1890
|
+
declare const JELLYFIN_CLIENT: {
|
|
1891
|
+
readonly name: "NGXSP";
|
|
1892
|
+
readonly version: "1.0.0";
|
|
1893
|
+
};
|
|
1894
|
+
declare function createJellyfinConnector(deviceId?: string): MediaSourceConnector;
|
|
1895
|
+
/** Signs in and keeps the access token the server issues; the password is not stored. */
|
|
1896
|
+
declare function connectJellyfin(input: MediaSourceInput, http: MediaSourceHttp, deviceId?: string): Promise<MediaSourceAccount>;
|
|
1897
|
+
/** Libraries at the root, then albums, series and seasons, then the files they hold. */
|
|
1898
|
+
declare function browseJellyfin(account: MediaSourceAccount, options: MediaSourceBrowseOptions, http: MediaSourceHttp): Promise<MediaSourcePage>;
|
|
1899
|
+
/**
|
|
1900
|
+
* Lyrics stored next to the track on the server (Jellyfin 10.9 and later). Timed lines come back as ticks of a
|
|
1901
|
+
* hundred nanoseconds and are written as LRC, so the player reads them with its own parser.
|
|
1902
|
+
*/
|
|
1903
|
+
declare function readJellyfinLyrics(account: MediaSourceAccount, item: MediaSourceItem, http: MediaSourceHttp): Promise<string | null>;
|
|
1904
|
+
/** Plays the file as stored on the server; the browser asks for the byte ranges it needs. */
|
|
1905
|
+
declare function jellyfinStreamUrl(account: MediaSourceAccount, itemId: string, kind: 'audio' | 'video'): string;
|
|
1906
|
+
declare function jellyfinImageUrl(account: MediaSourceAccount, itemId: string, tag?: string, maxHeight?: number): string;
|
|
1907
|
+
declare function jellyfinClientHeader(deviceId: string, token?: string): string;
|
|
1908
|
+
declare function parseJellyfinItem(account: MediaSourceAccount, entry: unknown, kind: 'audio' | 'video'): MediaSourceItem | null;
|
|
1909
|
+
|
|
1910
|
+
/**
|
|
1911
|
+
* UPnP AV / DLNA media servers (ReadyMedia/MiniDLNA, Gerbera, Kodi, Jellyfin's DLNA plugin, most NAS): the device
|
|
1912
|
+
* description names the server and its ContentDirectory service, which is browsed with SOAP `Browse` requests that
|
|
1913
|
+
* answer DIDL-Lite documents. Files are served over plain HTTP without credentials.
|
|
1914
|
+
*
|
|
1915
|
+
* Servers answer neither web pages (no CORS) nor discovery from a browser: applications find them with SSDP through
|
|
1916
|
+
* their native shell, using `DLNA_MEDIA_SERVER_SEARCH_TARGET`, or take the address of the description document.
|
|
1917
|
+
*/
|
|
1918
|
+
declare const DLNA_MEDIA_SERVER_SEARCH_TARGET = "urn:schemas-upnp-org:device:MediaServer:1";
|
|
1919
|
+
interface DidlResource {
|
|
1920
|
+
url: string;
|
|
1921
|
+
mimeType?: string;
|
|
1922
|
+
durationSec?: number;
|
|
1923
|
+
size?: number;
|
|
1924
|
+
}
|
|
1925
|
+
interface DidlObject {
|
|
1926
|
+
id: string;
|
|
1927
|
+
parentId?: string;
|
|
1928
|
+
container: boolean;
|
|
1929
|
+
title: string;
|
|
1930
|
+
upnpClass: string;
|
|
1931
|
+
childCount?: number;
|
|
1932
|
+
artists: Array<{
|
|
1933
|
+
name: string;
|
|
1934
|
+
role?: string;
|
|
1935
|
+
}>;
|
|
1936
|
+
creator?: string;
|
|
1937
|
+
album?: string;
|
|
1938
|
+
genre?: string;
|
|
1939
|
+
date?: string;
|
|
1940
|
+
trackNo?: number;
|
|
1941
|
+
artworkUrl?: string;
|
|
1942
|
+
seriesTitle?: string;
|
|
1943
|
+
season?: number;
|
|
1944
|
+
episode?: number;
|
|
1945
|
+
resources: DidlResource[];
|
|
1946
|
+
}
|
|
1947
|
+
declare function createDlnaConnector(): MediaSourceConnector;
|
|
1948
|
+
/** Reads the device description (typed, or found on the network) and checks the device serves media. */
|
|
1949
|
+
declare function connectDlna(input: MediaSourceInput, http: MediaSourceHttp): Promise<MediaSourceAccount>;
|
|
1950
|
+
/**
|
|
1951
|
+
* Children of a container, the root when none is given. Folders come first, then the files the app plays; a search
|
|
1952
|
+
* asks the server for matching titles when it supports it.
|
|
1953
|
+
*/
|
|
1954
|
+
declare function browseDlna(account: MediaSourceAccount, options: MediaSourceBrowseOptions, http: MediaSourceHttp): Promise<MediaSourcePage>;
|
|
1955
|
+
/** Containers and items of a DIDL-Lite document, with relative links resolved against `baseUrl`. */
|
|
1956
|
+
declare function parseDidlLite(xml: string, baseUrl: string): DidlObject[];
|
|
1957
|
+
|
|
1958
|
+
/** A service of a UPnP device (ContentDirectory, AVTransport, RenderingControl…) and where to send its actions. */
|
|
1959
|
+
interface UpnpService {
|
|
1960
|
+
serviceType: string;
|
|
1961
|
+
controlUrl: string;
|
|
1962
|
+
}
|
|
1963
|
+
interface UpnpDeviceDescription {
|
|
1964
|
+
udn?: string;
|
|
1965
|
+
friendlyName: string;
|
|
1966
|
+
deviceType?: string;
|
|
1967
|
+
manufacturer?: string;
|
|
1968
|
+
modelName?: string;
|
|
1969
|
+
iconUrl?: string;
|
|
1970
|
+
/** Every service the device and its embedded devices offer. */
|
|
1971
|
+
services: UpnpService[];
|
|
1972
|
+
contentDirectory?: UpnpService;
|
|
1973
|
+
}
|
|
1974
|
+
/** A UPnP action the device refused; `code` is its UPnP error code when it gave one. */
|
|
1975
|
+
declare class UpnpActionError extends Error {
|
|
1976
|
+
readonly action: string;
|
|
1977
|
+
readonly code: string | undefined;
|
|
1978
|
+
readonly status: number;
|
|
1979
|
+
constructor(action: string, code: string | undefined, status: number);
|
|
1980
|
+
}
|
|
1981
|
+
declare function parseUpnpDeviceDescription(xml: string, descriptionUrl: string): UpnpDeviceDescription;
|
|
1982
|
+
/** The first service of this kind (`AVTransport`, `RenderingControl`…), whatever its version. */
|
|
1983
|
+
declare function findUpnpService(services: readonly UpnpService[], name: string): UpnpService | undefined;
|
|
1984
|
+
/** Sends a SOAP action and returns the answer; a refusal throws `UpnpActionError`. */
|
|
1985
|
+
declare function upnpAction(http: MediaSourceHttp, service: UpnpService, action: string, soapArguments: Record<string, string>, signal?: AbortSignal): Promise<string>;
|
|
1986
|
+
|
|
1987
|
+
/**
|
|
1988
|
+
* UPnP AV / DLNA renderers (smart TVs, network speakers, Kodi, gmrender): media is handed over with AVTransport
|
|
1989
|
+
* `SetAVTransportURI` and driven with `Play`, `Pause`, `Seek` and `Stop`; RenderingControl sets the volume. Renderers
|
|
1990
|
+
* fetch the media themselves, over plain HTTP.
|
|
1991
|
+
*/
|
|
1992
|
+
declare const DLNA_MEDIA_RENDERER_SEARCH_TARGET = "urn:schemas-upnp-org:device:MediaRenderer:1";
|
|
1993
|
+
interface DlnaRendererInfo {
|
|
1994
|
+
name: string;
|
|
1995
|
+
udn?: string;
|
|
1996
|
+
modelName?: string;
|
|
1997
|
+
avTransport: UpnpService;
|
|
1998
|
+
renderingControl?: UpnpService;
|
|
1999
|
+
}
|
|
2000
|
+
/** Reads a renderer's description; devices without AVTransport cannot play what they are sent. */
|
|
2001
|
+
declare function readDlnaRenderer(descriptionUrl: string, http: MediaSourceHttp): Promise<DlnaRendererInfo>;
|
|
2002
|
+
declare function createDlnaRendererSession(descriptionUrl: string, http: MediaSourceHttp): Promise<RemotePlaybackSession>;
|
|
2003
|
+
|
|
2004
|
+
/**
|
|
2005
|
+
* Chromecast and Cast-enabled speakers and TVs, spoken without Google's SDK: CASTV2 messages (protobuf `CastMessage`
|
|
2006
|
+
* frames carrying JSON) over a TLS connection to port 8009. Media plays in the Default Media Receiver, the player
|
|
2007
|
+
* every Cast device ships. The apps' native shells open the TLS connection and find devices with mDNS
|
|
2008
|
+
* (`CAST_MDNS_SERVICE`); everything else happens here.
|
|
2009
|
+
*/
|
|
2010
|
+
declare const CAST_MDNS_SERVICE = "_googlecast._tcp";
|
|
2011
|
+
declare const CAST_PORT = 8009;
|
|
2012
|
+
declare const CAST_DEFAULT_MEDIA_RECEIVER = "CC1AD845";
|
|
2013
|
+
/** The type to send a media to a Cast receiver with, or the format it cannot read. */
|
|
2014
|
+
declare function castContentType(contentType: string): {
|
|
2015
|
+
contentType: string;
|
|
2016
|
+
} | {
|
|
2017
|
+
unsupported: string;
|
|
2018
|
+
};
|
|
2019
|
+
/** A Cast device found on the network. */
|
|
2020
|
+
interface CastDeviceInfo {
|
|
2021
|
+
id: string;
|
|
2022
|
+
name: string;
|
|
2023
|
+
model?: string;
|
|
2024
|
+
address: string;
|
|
2025
|
+
port: number;
|
|
2026
|
+
/** Speakers and speaker groups have no screen. */
|
|
2027
|
+
video: boolean;
|
|
2028
|
+
}
|
|
2029
|
+
/** Cast devices in the answers to an mDNS question for `CAST_MDNS_SERVICE`, one per device id. */
|
|
2030
|
+
declare function castDevicesFromMdns(answers: ReadonlyArray<{
|
|
2031
|
+
from?: string;
|
|
2032
|
+
bytes: Uint8Array;
|
|
2033
|
+
}>): CastDeviceInfo[];
|
|
2034
|
+
/** A TLS connection to a Cast device, opened by the native shell. */
|
|
2035
|
+
interface CastSocket {
|
|
2036
|
+
send(frame: Uint8Array): void | Promise<void>;
|
|
2037
|
+
onData(listener: (chunk: Uint8Array) => void): () => void;
|
|
2038
|
+
onClose(listener: (reason?: string) => void): () => void;
|
|
2039
|
+
close(): void;
|
|
2040
|
+
}
|
|
2041
|
+
interface CastMessage {
|
|
2042
|
+
sourceId: string;
|
|
2043
|
+
destinationId: string;
|
|
2044
|
+
namespace: string;
|
|
2045
|
+
payload: string;
|
|
2046
|
+
}
|
|
2047
|
+
interface CastSessionOptions {
|
|
2048
|
+
appId?: string;
|
|
2049
|
+
senderId?: string;
|
|
2050
|
+
requestTimeoutMs?: number;
|
|
2051
|
+
/** How long a LOAD may take: a receiver that has just started loads its player first (over 10 s on a TV). */
|
|
2052
|
+
loadTimeoutMs?: number;
|
|
2053
|
+
/** Interval of the keep-alive PING; 0 disables it (tests). */
|
|
2054
|
+
heartbeatMs?: number;
|
|
2055
|
+
}
|
|
2056
|
+
/** One length-prefixed protobuf `CastMessage` with a UTF-8 payload. */
|
|
2057
|
+
declare function encodeCastMessage(message: CastMessage): Uint8Array;
|
|
2058
|
+
/** Splits received bytes into messages; returns the messages and the bytes of an incomplete frame. */
|
|
2059
|
+
declare function decodeCastFrames(buffer: Uint8Array): {
|
|
2060
|
+
messages: CastMessage[];
|
|
2061
|
+
rest: Uint8Array;
|
|
2062
|
+
};
|
|
2063
|
+
/** Connects to the device; media is sent with `load`, which starts the Default Media Receiver when needed. */
|
|
2064
|
+
declare function createCastSession(socket: CastSocket, options?: CastSessionOptions): RemotePlaybackSession;
|
|
2065
|
+
|
|
2066
|
+
/**
|
|
2067
|
+
* Just enough DNS for mDNS service discovery (RFC 6762/6763): a PTR question for a service type, and the PTR, SRV,
|
|
2068
|
+
* TXT and A records devices answer with. Apps send the question from an ordinary UDP port to 224.0.0.251:5353
|
|
2069
|
+
* ("legacy unicast" query), so devices answer that port directly and no process has to share port 5353. An app that
|
|
2070
|
+
* advertises a service of its own reads the questions (`parseMdnsQuestions`) and answers them (`encodeMdnsResponse`).
|
|
2071
|
+
*/
|
|
2072
|
+
declare const MDNS_ADDRESS = "224.0.0.251";
|
|
2073
|
+
declare const MDNS_PORT = 5353;
|
|
2074
|
+
type MdnsRecord = {
|
|
2075
|
+
type: 'PTR';
|
|
2076
|
+
name: string;
|
|
2077
|
+
target: string;
|
|
2078
|
+
} | {
|
|
2079
|
+
type: 'SRV';
|
|
2080
|
+
name: string;
|
|
2081
|
+
target: string;
|
|
2082
|
+
port: number;
|
|
2083
|
+
} | {
|
|
2084
|
+
type: 'TXT';
|
|
2085
|
+
name: string;
|
|
2086
|
+
entries: Record<string, string>;
|
|
2087
|
+
} | {
|
|
2088
|
+
type: 'A';
|
|
2089
|
+
name: string;
|
|
2090
|
+
address: string;
|
|
2091
|
+
};
|
|
2092
|
+
/** A service instance put together from the records of one or more answers. */
|
|
2093
|
+
interface MdnsService {
|
|
2094
|
+
/** Full instance name, e.g. `Kitchen._googlecast._tcp.local`. */
|
|
2095
|
+
instance: string;
|
|
2096
|
+
host?: string;
|
|
2097
|
+
address?: string;
|
|
2098
|
+
port?: number;
|
|
2099
|
+
txt: Record<string, string>;
|
|
2100
|
+
}
|
|
2101
|
+
/** A PTR question for a service type (`_googlecast._tcp.local`). */
|
|
2102
|
+
declare function encodeMdnsQuery(serviceType: string, id?: number): Uint8Array;
|
|
2103
|
+
/** The questions of a DNS query (not of a response); malformed messages give none. */
|
|
2104
|
+
declare function parseMdnsQuestions(bytes: Uint8Array): Array<{
|
|
2105
|
+
name: string;
|
|
2106
|
+
type: number;
|
|
2107
|
+
}>;
|
|
2108
|
+
/**
|
|
2109
|
+
* A response carrying `records`: what a service advertises (PTR to its instance, SRV, TXT, A). `ttl` 0 says goodbye.
|
|
2110
|
+
* `id` and `question` echo a legacy unicast query, which expects them back.
|
|
2111
|
+
*/
|
|
2112
|
+
declare function encodeMdnsResponse(records: readonly MdnsRecord[], options?: {
|
|
2113
|
+
id?: number;
|
|
2114
|
+
ttl?: number;
|
|
2115
|
+
question?: {
|
|
2116
|
+
name: string;
|
|
2117
|
+
type: number;
|
|
2118
|
+
};
|
|
2119
|
+
}): Uint8Array;
|
|
2120
|
+
/** Answer and additional records of a DNS message; malformed messages give no records. */
|
|
2121
|
+
declare function parseMdnsMessage(bytes: Uint8Array): MdnsRecord[];
|
|
2122
|
+
/** Instances of a service type found in a set of answers, with their port, TXT entries and IPv4 address. */
|
|
2123
|
+
declare function mdnsServices(serviceType: string, answers: ReadonlyArray<{
|
|
2124
|
+
from?: string;
|
|
2125
|
+
bytes: Uint8Array;
|
|
2126
|
+
}>): MdnsService[];
|
|
2127
|
+
|
|
2128
|
+
/**
|
|
2129
|
+
* Playback transfer between NGXSP apps of a local network: an app hands what it plays (the queue, the item, the
|
|
2130
|
+
* position) to another app of the same kind, which carries on from there.
|
|
2131
|
+
*
|
|
2132
|
+
* - Apps advertise themselves over mDNS as `_ngxsp._tcp` services; TXT entries say which app they are.
|
|
2133
|
+
* - The sender POSTs a `PlaybackTransfer` to `/ngxsp/transfer` on the receiver. Its media addresses are ones the
|
|
2134
|
+
* receiver can fetch: web addresses as they are, the sender's own media shared on the network.
|
|
2135
|
+
* - The first transfer from an app asks on the receiving device (refuse or allow). Allowed, the sender gets a token
|
|
2136
|
+
* it presents from then on, and later transfers go through without asking ("accept once").
|
|
2137
|
+
*/
|
|
2138
|
+
declare const NGXSP_SERVICE_TYPE = "_ngxsp._tcp.local";
|
|
2139
|
+
declare const PLAYBACK_TRANSFER_PATH = "/ngxsp/transfer";
|
|
2140
|
+
declare const PLAYBACK_TRANSFER_VERSION = 1;
|
|
2141
|
+
/** How long the receiving device waits for its user to answer. */
|
|
2142
|
+
declare const PLAYBACK_TRANSFER_PROMPT_MS = 60000;
|
|
2143
|
+
type NgxspAppKind = 'audio' | 'video';
|
|
2144
|
+
/** An NGXSP app as the others know it. */
|
|
2145
|
+
interface NgxspAppIdentity {
|
|
2146
|
+
/** Random, kept by the app for good. */
|
|
2147
|
+
id: string;
|
|
2148
|
+
/** The device's name, as its user knows it. */
|
|
2149
|
+
name: string;
|
|
2150
|
+
app: NgxspAppKind;
|
|
2151
|
+
}
|
|
2152
|
+
/** Another NGXSP app of the network. */
|
|
2153
|
+
interface NgxspPeer extends NgxspAppIdentity {
|
|
2154
|
+
address: string;
|
|
2155
|
+
port: number;
|
|
2156
|
+
}
|
|
2157
|
+
interface PlaybackTransferItem {
|
|
2158
|
+
url: string;
|
|
2159
|
+
title: string;
|
|
2160
|
+
artist?: string;
|
|
2161
|
+
album?: string;
|
|
2162
|
+
artworkUrl?: string;
|
|
2163
|
+
durationSec?: number;
|
|
2164
|
+
/** Radios and live streams. */
|
|
2165
|
+
live?: boolean;
|
|
2166
|
+
}
|
|
2167
|
+
interface PlaybackTransfer {
|
|
2168
|
+
version: number;
|
|
2169
|
+
from: NgxspAppIdentity;
|
|
2170
|
+
items: PlaybackTransferItem[];
|
|
2171
|
+
/** The item playing, in `items`. */
|
|
2172
|
+
index: number;
|
|
2173
|
+
positionSec: number;
|
|
2174
|
+
playing: boolean;
|
|
2175
|
+
}
|
|
2176
|
+
interface TrustedSender {
|
|
2177
|
+
id: string;
|
|
2178
|
+
name: string;
|
|
2179
|
+
/** SHA-256 of the token the sender presents, in hex. */
|
|
2180
|
+
tokenHash: string;
|
|
2181
|
+
since: number;
|
|
2182
|
+
}
|
|
2183
|
+
type PlaybackTransferOutcome = {
|
|
2184
|
+
accepted: true;
|
|
2185
|
+
token?: string;
|
|
2186
|
+
} | {
|
|
2187
|
+
accepted: false;
|
|
2188
|
+
reason: 'refused' | 'busy' | 'timeout' | 'disabled' | 'other-app' | 'invalid' | 'unreachable';
|
|
2189
|
+
};
|
|
2190
|
+
/** The TXT entries an app advertises. */
|
|
2191
|
+
declare function ngxspTxt(identity: NgxspAppIdentity): Record<string, string>;
|
|
2192
|
+
/** The records an app answers `_ngxsp._tcp` questions with, for each of its addresses. */
|
|
2193
|
+
declare function ngxspServiceRecords(identity: NgxspAppIdentity, port: number, addresses: readonly string[]): MdnsRecord[];
|
|
2194
|
+
/** NGXSP apps of kind `app` found in mDNS answers, this one aside. */
|
|
2195
|
+
declare function ngxspPeersFromMdns(answers: ReadonlyArray<{
|
|
2196
|
+
from?: string;
|
|
2197
|
+
bytes: Uint8Array;
|
|
2198
|
+
}>, self: Pick<NgxspAppIdentity, 'id' | 'app'>): NgxspPeer[];
|
|
2199
|
+
/** A transfer as received, checked and trimmed; null when it is not one. */
|
|
2200
|
+
declare function readPlaybackTransfer(value: unknown): PlaybackTransfer | null;
|
|
2201
|
+
interface PlaybackTransferReceiverOptions {
|
|
2202
|
+
app: NgxspAppKind;
|
|
2203
|
+
/** Off, the app refuses every transfer without asking. */
|
|
2204
|
+
enabled: () => boolean;
|
|
2205
|
+
trusted: () => readonly TrustedSender[];
|
|
2206
|
+
saveTrusted: (senders: TrustedSender[]) => void;
|
|
2207
|
+
/** Asks the user whether `from` may send its playback here: true to allow. */
|
|
2208
|
+
ask: (from: NgxspAppIdentity) => Promise<boolean>;
|
|
2209
|
+
/** Plays what was transferred. */
|
|
2210
|
+
apply: (transfer: PlaybackTransfer) => void;
|
|
2211
|
+
sha256Hex: (value: string) => Promise<string>;
|
|
2212
|
+
randomToken: () => string;
|
|
2213
|
+
promptMs?: number;
|
|
2214
|
+
}
|
|
2215
|
+
/** A transfer request as the app's HTTP endpoint hands it over, and what it answers. */
|
|
2216
|
+
interface PlaybackTransferRequest {
|
|
2217
|
+
method: string;
|
|
2218
|
+
body: string;
|
|
2219
|
+
/** The `Authorization` header: `NGXSP <token>` from a sender allowed before. */
|
|
2220
|
+
authorization?: string;
|
|
2221
|
+
}
|
|
2222
|
+
interface PlaybackTransferResponse {
|
|
2223
|
+
status: number;
|
|
2224
|
+
body: Record<string, unknown>;
|
|
2225
|
+
}
|
|
2226
|
+
/** Decides on the transfers an app receives, asking its user the first time an app sends one. */
|
|
2227
|
+
declare function createPlaybackTransferReceiver(options: PlaybackTransferReceiverOptions): {
|
|
2228
|
+
handle: (request: PlaybackTransferRequest) => Promise<PlaybackTransferResponse>;
|
|
2229
|
+
};
|
|
2230
|
+
/** Sends a transfer to a peer, with the token it gave the last time if there is one. */
|
|
2231
|
+
declare function sendPlaybackTransfer(http: MediaSourceHttp, peer: Pick<NgxspPeer, 'address' | 'port'>, transfer: PlaybackTransfer, token?: string): Promise<PlaybackTransferOutcome>;
|
|
2232
|
+
|
|
2233
|
+
/**
|
|
2234
|
+
* Subsonic API, spoken by Navidrome, Airsonic-Advanced, Gonic and others (all open source): a music server with
|
|
2235
|
+
* albums, artists and covers. Every request is signed with md5(password + salt), so the account keeps that signature
|
|
2236
|
+
* and its salt instead of the password.
|
|
2237
|
+
*/
|
|
2238
|
+
declare const SUBSONIC_API_VERSION = "1.16.1";
|
|
2239
|
+
declare const SUBSONIC_CLIENT = "NGXSP";
|
|
2240
|
+
declare function createSubsonicConnector(salt?: () => string): MediaSourceConnector;
|
|
2241
|
+
/** Checks the credentials with `ping` and keeps the request signature, never the password. */
|
|
2242
|
+
declare function connectSubsonic(input: MediaSourceInput, http: MediaSourceHttp, salt?: () => string): Promise<MediaSourceAccount>;
|
|
2243
|
+
/** Albums at the root (with their covers), then the tracks of an album; searches look up tracks directly. */
|
|
2244
|
+
declare function browseSubsonic(account: MediaSourceAccount, options: MediaSourceBrowseOptions, http: MediaSourceHttp): Promise<MediaSourcePage>;
|
|
2245
|
+
declare function subsonicStreamUrl(account: MediaSourceAccount, songId: string): string;
|
|
2246
|
+
declare function subsonicCoverUrl(account: MediaSourceAccount, coverArtId: string, size?: number): string;
|
|
2247
|
+
declare function parseSubsonicSong(account: MediaSourceAccount, entry: unknown): MediaSourceItem | null;
|
|
2248
|
+
|
|
2249
|
+
interface WebDavEntry {
|
|
2250
|
+
/** Decoded path of the entry on the server, starting with `/`. */
|
|
2251
|
+
path: string;
|
|
2252
|
+
name: string;
|
|
2253
|
+
collection: boolean;
|
|
2254
|
+
size?: number;
|
|
2255
|
+
contentType?: string;
|
|
2256
|
+
lastModified?: number;
|
|
2257
|
+
}
|
|
2258
|
+
declare function createWebDavConnector(): MediaSourceConnector;
|
|
2259
|
+
/** Checks that the address is a WebDAV folder the credentials can read. */
|
|
2260
|
+
declare function connectWebDav(input: MediaSourceInput, http: MediaSourceHttp): Promise<MediaSourceAccount>;
|
|
2261
|
+
/** Sub-folders of a folder, then the media files it holds for the kind the app plays. */
|
|
2262
|
+
declare function browseWebDav(account: MediaSourceAccount, options: MediaSourceBrowseOptions, http: MediaSourceHttp): Promise<MediaSourcePage>;
|
|
2263
|
+
/** Parses a WebDAV multistatus document; prefixes and namespaces vary between servers, so tags are matched by name. */
|
|
2264
|
+
declare function parseWebDavMultistatus(xml: string): WebDavEntry[];
|
|
2265
|
+
/** Value for the `Authorization` header of an account, or undefined for anonymous shares. */
|
|
2266
|
+
declare function webDavAuthorization(account: MediaSourceAccount): string | undefined;
|
|
2267
|
+
/** Reads the `.lrc` file sitting next to the track, the way a music folder usually keeps its lyrics. */
|
|
2268
|
+
declare function readWebDavLyrics(account: MediaSourceAccount, item: MediaSourceItem, http: MediaSourceHttp): Promise<string | null>;
|
|
2269
|
+
|
|
2270
|
+
/**
|
|
2271
|
+
* MD5 of a UTF-8 string, as lowercase hex (RFC 1321). Only for protocols that require it, such as Subsonic request
|
|
2272
|
+
* signing; never as a security primitive of its own. WebCrypto does not offer MD5, hence this implementation.
|
|
2273
|
+
*/
|
|
2274
|
+
declare function md5Hex(input: string): string;
|
|
2275
|
+
|
|
2276
|
+
/** Internet Archive public APIs: advanced search, item metadata and file downloads. No key required. */
|
|
2277
|
+
declare const INTERNET_ARCHIVE_ORIGIN = "https://archive.org";
|
|
2278
|
+
type InternetArchiveMediaType = 'movies' | 'audio';
|
|
2279
|
+
interface InternetArchiveRequestOptions {
|
|
2280
|
+
/** Defaults to plain `fetch`. */
|
|
2281
|
+
http?: OnlineJsonHttp;
|
|
2282
|
+
signal?: AbortSignal;
|
|
2283
|
+
origin?: string;
|
|
2284
|
+
}
|
|
2285
|
+
interface InternetArchiveSearchOptions {
|
|
2286
|
+
mediaType: InternetArchiveMediaType;
|
|
2287
|
+
/** Extra Lucene clauses joined with AND, e.g. `collection:(prelinger)`. */
|
|
2288
|
+
filters?: readonly string[];
|
|
2289
|
+
/** Free text matched against title, creator and subject. */
|
|
2290
|
+
text?: string;
|
|
2291
|
+
rows?: number;
|
|
2292
|
+
page?: number;
|
|
2293
|
+
/** Defaults to the most downloaded items first. */
|
|
2294
|
+
sort?: string;
|
|
2295
|
+
origin?: string;
|
|
2296
|
+
}
|
|
2297
|
+
interface InternetArchiveSearchItem {
|
|
2298
|
+
identifier: string;
|
|
2299
|
+
title: string;
|
|
2300
|
+
creator?: string;
|
|
2301
|
+
year?: number;
|
|
2302
|
+
licenseUrl?: string;
|
|
2303
|
+
thumbnailUrl: string;
|
|
2304
|
+
detailsUrl: string;
|
|
2305
|
+
}
|
|
2306
|
+
interface InternetArchiveSearchResult {
|
|
2307
|
+
total: number;
|
|
2308
|
+
page: number;
|
|
2309
|
+
items: InternetArchiveSearchItem[];
|
|
2310
|
+
}
|
|
2311
|
+
interface InternetArchiveFile {
|
|
2312
|
+
name: string;
|
|
2313
|
+
format?: string;
|
|
2314
|
+
source?: string;
|
|
2315
|
+
/** For derivatives, the name of the file they were generated from. */
|
|
2316
|
+
original?: string;
|
|
2317
|
+
title?: string;
|
|
2318
|
+
creator?: string;
|
|
2319
|
+
album?: string;
|
|
2320
|
+
track?: string;
|
|
2321
|
+
length?: string;
|
|
2322
|
+
size?: string;
|
|
2323
|
+
}
|
|
2324
|
+
interface InternetArchiveItem {
|
|
2325
|
+
identifier: string;
|
|
2326
|
+
title: string;
|
|
2327
|
+
creator?: string;
|
|
2328
|
+
year?: number;
|
|
2329
|
+
description?: string;
|
|
2330
|
+
licenseUrl?: string;
|
|
2331
|
+
/** Lending, stream-only or dark items cannot be played from their download URLs. */
|
|
2332
|
+
restricted: boolean;
|
|
2333
|
+
thumbnailUrl: string;
|
|
2334
|
+
detailsUrl: string;
|
|
2335
|
+
/**
|
|
2336
|
+
* Storage servers holding the item (`https://<server>/<dir>`). `/download` redirects to a nearby mirror that may omit
|
|
2337
|
+
* CORS headers; these servers send them, so apps download files from here and keep `/download` for streaming.
|
|
2338
|
+
*/
|
|
2339
|
+
storageUrls: string[];
|
|
2340
|
+
files: InternetArchiveFile[];
|
|
2341
|
+
}
|
|
2342
|
+
interface InternetArchiveVideoSource {
|
|
2343
|
+
fileName: string;
|
|
2344
|
+
format: string;
|
|
2345
|
+
url: string;
|
|
2346
|
+
/** Same file on the item's storage servers, readable with `fetch` from other origins. */
|
|
2347
|
+
storageUrls: string[];
|
|
2348
|
+
durationSec?: number;
|
|
2349
|
+
}
|
|
2350
|
+
interface InternetArchiveAudioTrack {
|
|
2351
|
+
/** `<identifier>/<file name>`, unique across the archive. */
|
|
2352
|
+
id: string;
|
|
2353
|
+
identifier: string;
|
|
2354
|
+
fileName: string;
|
|
2355
|
+
title: string;
|
|
2356
|
+
artist?: string;
|
|
2357
|
+
album: string;
|
|
2358
|
+
trackNo?: number;
|
|
2359
|
+
durationSec?: number;
|
|
2360
|
+
url: string;
|
|
2361
|
+
/** Same file on the item's storage servers, readable with `fetch` from other origins. */
|
|
2362
|
+
storageUrls: string[];
|
|
2363
|
+
artworkUrl: string;
|
|
2364
|
+
}
|
|
2365
|
+
/** Clauses that keep results playable without an account. */
|
|
2366
|
+
declare const INTERNET_ARCHIVE_PLAYABLE_FILTERS: readonly string[];
|
|
2367
|
+
declare function buildInternetArchiveSearchUrl(options: InternetArchiveSearchOptions): string;
|
|
2368
|
+
declare function searchInternetArchive(options: InternetArchiveSearchOptions, request?: InternetArchiveRequestOptions): Promise<InternetArchiveSearchResult>;
|
|
2369
|
+
declare function fetchInternetArchiveItem(identifier: string, request?: InternetArchiveRequestOptions): Promise<InternetArchiveItem>;
|
|
2370
|
+
declare function parseInternetArchiveItem(identifier: string, body: unknown, origin?: string): InternetArchiveItem;
|
|
2371
|
+
/** Picks the most widely playable video derivative of an item. */
|
|
2372
|
+
declare function selectInternetArchiveVideo(item: InternetArchiveItem, origin?: string): InternetArchiveVideoSource | null;
|
|
2373
|
+
/** Tracks of an audio item, using a single format so derivatives are not listed twice. */
|
|
2374
|
+
declare function internetArchiveAudioTracks(item: InternetArchiveItem, origin?: string): InternetArchiveAudioTrack[];
|
|
2375
|
+
declare function internetArchiveDownloadUrl(identifier: string, fileName: string, origin?: string): string;
|
|
2376
|
+
declare function internetArchiveThumbnailUrl(identifier: string, origin?: string): string;
|
|
2377
|
+
declare function internetArchiveDetailsUrl(identifier: string, origin?: string): string;
|
|
2378
|
+
/** Accepts `240.9` seconds as well as `02:31` or `1:31:44`. */
|
|
2379
|
+
declare function parseInternetArchiveLength(length: string | undefined): number | undefined;
|
|
2380
|
+
|
|
2381
|
+
/** Radio Browser community directory of internet radio stations. No key required. */
|
|
2382
|
+
declare const RADIO_BROWSER_ORIGIN = "https://de1.api.radio-browser.info";
|
|
2383
|
+
interface RadioBrowserSearchOptions {
|
|
2384
|
+
/** ISO 3166-1 alpha-2 country code, e.g. `CM`. */
|
|
2385
|
+
countryCode?: string;
|
|
2386
|
+
name?: string;
|
|
2387
|
+
tag?: string;
|
|
2388
|
+
limit?: number;
|
|
2389
|
+
offset?: number;
|
|
2390
|
+
/** Defaults to true: stations failing the directory checks are skipped. */
|
|
2391
|
+
hideBroken?: boolean;
|
|
2392
|
+
/** Defaults to the most listened stations first. */
|
|
2393
|
+
order?: 'clickcount' | 'votes' | 'name' | 'bitrate' | 'lastcheckok';
|
|
2394
|
+
origin?: string;
|
|
2395
|
+
}
|
|
2396
|
+
interface RadioBrowserRequestOptions {
|
|
2397
|
+
/** Defaults to plain `fetch`. */
|
|
2398
|
+
http?: OnlineJsonHttp;
|
|
2399
|
+
signal?: AbortSignal;
|
|
2400
|
+
origin?: string;
|
|
2401
|
+
}
|
|
2402
|
+
interface RadioStation {
|
|
2403
|
+
id: string;
|
|
2404
|
+
name: string;
|
|
2405
|
+
streamUrl: string;
|
|
2406
|
+
homepageUrl?: string;
|
|
2407
|
+
faviconUrl?: string;
|
|
2408
|
+
country?: string;
|
|
2409
|
+
countryCode?: string;
|
|
2410
|
+
language?: string;
|
|
2411
|
+
tags: string[];
|
|
2412
|
+
codec?: string;
|
|
2413
|
+
bitrateKbps?: number;
|
|
2414
|
+
/** HLS playlists need an adaptive engine; other streams play in a media element. */
|
|
2415
|
+
hls: boolean;
|
|
2416
|
+
/** Plain http streams are blocked as mixed content on https pages. */
|
|
2417
|
+
secure: boolean;
|
|
2418
|
+
}
|
|
2419
|
+
declare function buildRadioBrowserSearchUrl(options?: RadioBrowserSearchOptions): string;
|
|
2420
|
+
declare function searchRadioStations(options?: RadioBrowserSearchOptions, request?: RadioBrowserRequestOptions): Promise<RadioStation[]>;
|
|
2421
|
+
declare function parseRadioStation(entry: unknown): RadioStation | null;
|
|
2422
|
+
|
|
2423
|
+
/**
|
|
2424
|
+
* Lyrics of a track, timed or plain. They come from the file itself (tags), from an `.lrc` file beside it, or from
|
|
2425
|
+
* LRCLIB, an open lyrics database (no key, no account). Timed lines let the player follow the words while it plays.
|
|
2426
|
+
*/
|
|
2427
|
+
declare const LRCLIB_ORIGIN = "https://lrclib.net";
|
|
2428
|
+
type LyricsSource = 'tags' | 'file' | 'lrclib';
|
|
2429
|
+
interface LyricsLine {
|
|
2430
|
+
/** Start of the line in the track. */
|
|
2431
|
+
timeSec: number;
|
|
2432
|
+
text: string;
|
|
2433
|
+
}
|
|
2434
|
+
interface Lyrics {
|
|
2435
|
+
source: LyricsSource;
|
|
2436
|
+
/** Timed lines, when the source has them. */
|
|
2437
|
+
lines?: LyricsLine[];
|
|
2438
|
+
/** The whole text, always present when anything was found. */
|
|
2439
|
+
plain: string;
|
|
2440
|
+
/** What the source says it is, to show which version was picked. */
|
|
2441
|
+
title?: string;
|
|
2442
|
+
artist?: string;
|
|
2443
|
+
/** LRCLIB record, to fetch it again or report a wrong match. */
|
|
2444
|
+
id?: number;
|
|
2445
|
+
}
|
|
2446
|
+
interface LyricsQuery {
|
|
2447
|
+
title: string;
|
|
2448
|
+
artist?: string;
|
|
2449
|
+
album?: string;
|
|
2450
|
+
durationSec?: number;
|
|
2451
|
+
}
|
|
2452
|
+
interface LyricsRequestOptions {
|
|
2453
|
+
/** Defaults to plain `fetch`; apps plug their HTTP client (cache, retries). */
|
|
2454
|
+
http?: OnlineJsonHttp;
|
|
2455
|
+
signal?: AbortSignal;
|
|
2456
|
+
origin?: string;
|
|
2457
|
+
}
|
|
2458
|
+
/** `[mm:ss.xx]` lines, `[offset:±ms]` and the usual `[ti:]`/`[ar:]` tags; text without timestamps stays plain. */
|
|
2459
|
+
declare function parseLrc(text: string, source?: LyricsSource): Lyrics | null;
|
|
2460
|
+
/** Lyrics tags of a file (ID3 USLT/SYLT, Vorbis LYRICS), as music-metadata reports them. */
|
|
2461
|
+
interface TaggedLyrics {
|
|
2462
|
+
text?: string;
|
|
2463
|
+
syncText?: Array<{
|
|
2464
|
+
text?: string;
|
|
2465
|
+
timestamp?: number;
|
|
2466
|
+
}>;
|
|
2467
|
+
}
|
|
2468
|
+
/** Lyrics carried by the file itself; timed words win over a plain block. */
|
|
2469
|
+
declare function lyricsFromTags(tags: readonly TaggedLyrics[] | TaggedLyrics | undefined | null): Lyrics | null;
|
|
2470
|
+
/** Index of the line being sung at this position, or -1 before the first one. */
|
|
2471
|
+
declare function lyricsLineIndexAt(lines: readonly LyricsLine[] | undefined, positionSec: number): number;
|
|
2472
|
+
interface LrclibRecord {
|
|
2473
|
+
id?: number;
|
|
2474
|
+
trackName?: string;
|
|
2475
|
+
artistName?: string;
|
|
2476
|
+
duration?: number;
|
|
2477
|
+
instrumental?: boolean;
|
|
2478
|
+
plainLyrics?: string | null;
|
|
2479
|
+
syncedLyrics?: string | null;
|
|
2480
|
+
}
|
|
2481
|
+
declare function buildLrclibGetUrl(query: LyricsQuery, origin?: string): string;
|
|
2482
|
+
declare function buildLrclibSearchUrl(query: LyricsQuery, origin?: string): string;
|
|
2483
|
+
/**
|
|
2484
|
+
* Lyrics for a track from LRCLIB: the exact record when the track, artist, album and duration match, otherwise the
|
|
2485
|
+
* closest of the search results. Returns null when the database has nothing, and for instrumental tracks.
|
|
2486
|
+
*/
|
|
2487
|
+
declare function fetchLrclibLyrics(query: LyricsQuery, options?: LyricsRequestOptions): Promise<Lyrics | null>;
|
|
2488
|
+
/** Closest record: a duration within 5 seconds first, then the title and artist that look most alike. */
|
|
2489
|
+
declare function bestLrclibMatch(records: readonly LrclibRecord[], query: LyricsQuery): LrclibRecord | null;
|
|
2490
|
+
|
|
2491
|
+
export { ACOUSTID_ORIGIN, AUDIO_IDENTIFICATION_RETRY_MS, AUDIO_IDENTIFICATION_VERSION, type AcoustIdLookupQuery, type AcoustIdRecording, type AcoustIdRequestOptions, type AcoustIdResult, type AdsConfig, type AdsMode, type AspectRatioMode, type AudioAlbum, type AudioArtist, type AudioGenre, type AudioIdentifiableField, type AudioIdentification, type AudioIdentificationInput, type AudioIdentificationMatch, type AudioIdentificationMethod, type AudioIdentificationStatus, type AudioInferredField, type AudioLibraryFields, type AudioLibrarySearchResult, type AudioLibrarySnapshot, type AudioMetadataCorrections, type AudioMetadataHarmony, type AudioPlaylist, type AudioPlaylistRef, type AudioTrack, CAST_DEFAULT_MEDIA_RECEIVER, CAST_MDNS_SERVICE, CAST_PORT, type CacheStore, type CapabilityReport, type CastDeviceInfo, type CastMessage, type CastSessionOptions, type CastSocket, ConsoleLogPort, DEFAULT_ADS_CONFIG, DEFAULT_METADATA_CONFIG, DEFAULT_PROGRESS_CONFIG, DEFAULT_RESILIENCE_CONFIG, DEFAULT_SUBTITLES_CONFIG, DEFAULT_UI_CONFIG, DLNA_MEDIA_RENDERER_SEARCH_TARGET, DLNA_MEDIA_SERVER_SEARCH_TARGET, DefaultCacheStore, type DevicePlaybackEvent, type DevicePlaybackKind, type DevicePlaybackState, type DevicePlaybackStatus, type DiagnosticsEngineInfo, type DiagnosticsSnapshot, type DidlObject, type DidlResource, type DlnaRendererInfo, type DrmConfig, type EngineEvent, type EngineFactory, type EngineProfileHints, type EngineProfileId, type EpisodeItem, type HttpPort, INTERNET_ARCHIVE_ORIGIN, INTERNET_ARCHIVE_PLAYABLE_FILTERS, type IdentifiableItem, type IdentificationLibrary, type IdentificationRoutine, type IdentificationRoutineOptions, type InternetArchiveAudioTrack, type InternetArchiveFile, type InternetArchiveItem, type InternetArchiveMediaType, type InternetArchiveRequestOptions, type InternetArchiveSearchItem, type InternetArchiveSearchOptions, type InternetArchiveSearchResult, type InternetArchiveVideoSource, JELLYFIN_CLIENT, LRCLIB_ORIGIN, type LoadOptions, LocalMetadataProvider, LocalSubtitleProvider, type LogPort, type Lyrics, type LyricsLine, type LyricsQuery, type LyricsRequestOptions, type LyricsSource, MDNS_ADDRESS, MDNS_PORT, MEDIA_LIBRARY_MODEL_VERSION, MUSICBRAINZ_ORIGIN, type MdnsRecord, type MdnsService, type MediaArtwork, type MediaCache, type MediaCacheEntry, type MediaCacheOptions, type MediaCacheRequest, type MediaCacheStore, type MediaCacheUsage, type MediaFormat, type MediaItem, type MediaKind, type MediaLibraryKind, type MediaProgress, type MediaSourceAccount, type MediaSourceBrowseOptions, type MediaSourceConnector, type MediaSourceContainer, type MediaSourceHttp, type MediaSourceInput, type MediaSourceItem, type MediaSourceKind, type MediaSourcePage, type MediaSourceRequest, type MediaSourceResponse, type MediaTags, type MediaTagsInput, type MediaTrack, MemoryStoragePort, type MetadataConfig, type MetadataHint, type MetadataMatch, type MetadataProvider, type MusicBrainzRecording, type MusicBrainzRecordingQuery, type MusicBrainzRelease, type MusicBrainzRequestOptions, NGXSP_SERVICE_TYPE, NativeEngine, type NextEpisodeHint, type NgxspAppIdentity, type NgxspAppKind, type NgxspPeer, NoopTelemetryPort, type OnlineJsonHttp, PLAYBACK_TRANSFER_PATH, PLAYBACK_TRANSFER_PROMPT_MS, PLAYBACK_TRANSFER_VERSION, type PlaybackCapabilities, type PlaybackDevice, type PlaybackEngine, type PlaybackTransfer, type PlaybackTransferItem, type PlaybackTransferOutcome, type PlaybackTransferReceiverOptions, type PlaybackTransferRequest, type PlaybackTransferResponse, type PlayerCommand, type PlayerConfig, type PlayerContext, type PlayerDiagnostics, type PlayerError, type PlayerErrorCode, type PlayerEvent, type PlayerEventBus, PlayerFacade, type PlayerPlugin, type PlayerPorts, type PlayerSnapshot, type PlayerSource, type PlayerState, type PlayerTime, type ProgressConfig, type ProgressMode, ProgressPlugin, type QueueItem, type QueueSnapshot, RADIO_BROWSER_ORIGIN, RINGTONE_FADE_SEC, RINGTONE_MAX_SEC, type RadioBrowserRequestOptions, type RadioBrowserSearchOptions, type RadioStation, type RemoteConfigConfig, type RemoteConfigPayload, RemotePlaybackEngine, type RemotePlaybackEngineOptions, RemotePlaybackError, type RemotePlaybackErrorCode, type RemotePlaybackKind, type RemotePlaybackMedia, type RemotePlaybackSession, type RemotePlaybackStatus, type RemotePlayerState, type RepeatMode, type ResilienceConfig, ResiliencePlugin, type RingtoneSegment, SUBSONIC_API_VERSION, SUBSONIC_CLIENT, type SeriesConfig, type ShakeGesture, type ShakeOptions, type ShakeSample, SimpleEventBus, type SkipSegment, type SourceType, type SsaiManifestRewriteConfig, type StoragePort, type SubtitleCandidate, type SubtitleHint, type SubtitleProvider, type SubtitleQuery, type SubtitlesConfig, type TaggedLyrics, type TelemetryEvent, type TelemetryLevel, type TelemetryPort, type TelemetrySample, type TrustedSender, type UiConfig, type Unsubscribe, UpnpActionError, type UpnpDeviceDescription, type UpnpService, VAULT_ROUNDS, type VaultCrypto, type VaultPasscode, type VideoLibraryFields, type VideoMetadata, type WebDavEntry, applyAudioIdentification, applyEngineProfileToSource, applyRemoteConfig, audioFieldsFromTags, audioIdentificationDoubts, audioTrackMatchesSearch, bestLrclibMatch, browseDlna, browseJellyfin, browseSubsonic, browseWebDav, buildAcoustIdLookupUrl, buildInternetArchiveSearchUrl, buildLrclibGetUrl, buildLrclibSearchUrl, buildMusicBrainzRecordingSearchUrl, buildRadioBrowserSearchUrl, castContentType, castDevicesFromMdns, cleanAudioArtistName, cleanMediaTitle, connectDlna, connectJellyfin, connectSubsonic, connectWebDav, createAudioLibrarySnapshot, createCapabilityReport, createCastSession, createDefaultCapabilities, createDefaultConfig, createDefaultPorts, createDlnaConnector, createDlnaRendererSession, createFetchOnlineJsonHttp, createIdentificationRoutine, createIdleDevicePlaybackState, createJellyfinConnector, createMediaCache, createMediaDecodingReport, createNativeEngineFactory, createPlaybackTransferReceiver, createRemotePlaybackEngineFactory, createShakeDetector, createSsaiManifestRewritePlugin, createSubsonicConnector, createWebDavConnector, decodeCastFrames, decodeSubtitleBytes, describeSubtitleFile, detectPlaybackCapabilities, encodeCastMessage, encodeMdnsQuery, encodeMdnsResponse, encodeWav, fadeEdges, fetchInternetArchiveItem, fetchLrclibLyrics, fileKeyFor, findUpnpService, folderArtworkRank, formatRemoteClock, getEngineProfileHints, harmonizeAudioMetadata, hashPasscode, hiddenIds, internetArchiveAudioTracks, internetArchiveDetailsUrl, internetArchiveDownloadUrl, internetArchiveThumbnailUrl, isCategorizedEventType, isIgnoredDirectoryName, isImageFileName, isMediaFileName, isUsablePasscode, jellyfinClientHeader, jellyfinImageUrl, jellyfinStreamUrl, lookupAcoustId, lookupMusicBrainzRecording, lyricsFromTags, lyricsLineIndexAt, matchFingerprintedRecording, matchMusicBrainzRecording, md5Hex, mdnsServices, mediaContentType, mediaFileExtension, mediaMetadataKey, mediaSourceItemId, needsAudioIdentification, ngxspPeersFromMdns, ngxspServiceRecords, ngxspTxt, normalizeAudioSearchText, normalizeMediaSourceAddress, parseDidlLite, parseEpisodeFileName, parseInternetArchiveItem, parseInternetArchiveLength, parseJellyfinItem, parseLrc, parseMdnsMessage, parseMdnsQuestions, parseMediaSourceItemId, parseMusicBrainzRecording, parseRadioStation, parseRemoteClock, parseSubsonicSong, parseUpnpDeviceDescription, parseWebDavMultistatus, pickAcoustIdRecording, planMediaCacheEviction, readDlnaRenderer, readJellyfinLyrics, readPlaybackTransfer, readWebDavLyrics, reduceDevicePlaybackState, remotePlaybackErrorCode, remotePlaybackFailure, ringtoneFileName, ringtoneSegment, searchAudioLibrarySnapshot, searchInternetArchive, searchMusicBrainzRecordings, searchRadioStations, selectEngine, selectInternetArchiveVideo, sendPlaybackTransfer, subsonicCoverUrl, subsonicStreamUrl, subtitleTextToVtt, ticksToSeconds, upnpAction, verifyPasscode, videoFieldsFromTags, webDavAuthorization, withoutHidden };
|