@ngxsp/core 0.0.1-next.1 → 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/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' | 'netflix';
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;
@@ -611,10 +622,24 @@ declare class PlayerFacade {
611
622
  readonly playlistIndex$: Observable<number | null>;
612
623
  private playlist;
613
624
  private playlistKey;
614
- private externalVttUrl?;
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[]>;
615
635
  constructor(ports?: PlayerPorts);
616
636
  attach(videoEl: HTMLVideoElement, plugins: PlayerPlugin[] | undefined, config: PlayerConfig, ports?: Partial<PlayerPorts>, engines?: EngineFactory[]): void;
617
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;
618
643
  reload(opts?: LoadOptions): Promise<void>;
619
644
  next(): Promise<void>;
620
645
  previous(): Promise<void>;
@@ -631,10 +656,24 @@ declare class PlayerFacade {
631
656
  private toPlayerError;
632
657
  play(): Promise<void>;
633
658
  pause(): Promise<void>;
634
- seekTo(timeSec: number): Promise<void>;
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>;
635
664
  setRate(rate: number): Promise<void>;
636
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
+ */
637
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>;
638
677
  setAudioTrack(trackId: string | null): Promise<void>;
639
678
  setVideoTrack(trackId: string | null): Promise<void>;
640
679
  setAspectRatio(_mode: AspectRatioMode): void;
@@ -646,7 +685,11 @@ declare class PlayerFacade {
646
685
  diagnostics(): PlayerDiagnostics;
647
686
  setExternalVttTrack(lang: string, vtt: string): Promise<void>;
648
687
  setMetadata(meta: VideoMetadata): void;
649
- private ensureExternalTrack;
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;
650
693
  private handleEngineEvent;
651
694
  private tryRestoreStartTime;
652
695
  }
@@ -673,6 +716,7 @@ declare class NativeEngine implements PlaybackEngine {
673
716
  readonly id = "native";
674
717
  private video?;
675
718
  private listeners;
719
+ private emit;
676
720
  attach(videoEl: HTMLVideoElement): Promise<void>;
677
721
  load(source: PlayerSource, opts?: {
678
722
  startTimeSec?: number;
@@ -733,6 +777,21 @@ declare class LocalSubtitleProvider implements SubtitleProvider {
733
777
  fetch(candidate: SubtitleCandidate): Promise<string | ArrayBuffer>;
734
778
  }
735
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
+
736
795
  interface CacheStore {
737
796
  get<T>(key: string): Promise<T | null>;
738
797
  set<T>(key: string, value: T, ttlMs: number): Promise<void>;
@@ -816,6 +875,8 @@ declare class ResiliencePlugin implements PlayerPlugin {
816
875
  readonly id = "resilience";
817
876
  private attempts;
818
877
  private recovering;
878
+ /** Bumped by every load: a retry only reloads the media that failed, never one loaded since. */
879
+ private loadGeneration;
819
880
  onLoadStart(ctx: PlayerContext): Promise<void>;
820
881
  onError(ctx: PlayerContext, err: PlayerError): Promise<void>;
821
882
  private health;
@@ -836,7 +897,7 @@ interface SsaiManifestRewriteConfig {
836
897
  declare function createSsaiManifestRewritePlugin(): PlayerPlugin;
837
898
 
838
899
  type TelemetryLevel = 'debug' | 'info' | 'warn' | 'error';
839
- 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';
840
901
  interface TelemetrySample {
841
902
  ts: number;
842
903
  level: TelemetryLevel;
@@ -876,6 +937,163 @@ declare function applyEngineProfileToSource(source: PlayerSource, profile?: Engi
876
937
 
877
938
  declare function createNativeEngineFactory(): EngineFactory;
878
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
+
879
1097
  declare function applyRemoteConfig(cfg: PlayerConfig, ports: Partial<PlayerPorts>): Promise<{
880
1098
  config: PlayerConfig;
881
1099
  applied: boolean;
@@ -916,37 +1134,480 @@ interface AudioTrack extends MediaItem {
916
1134
  discNo?: number;
917
1135
  year?: number;
918
1136
  }
919
- interface AudioAlbum {
1137
+ interface AudioAlbum<TTrack extends AudioTrack = AudioTrack> {
920
1138
  id: string;
921
1139
  title: string;
922
1140
  artist?: string;
923
1141
  artwork?: string;
924
1142
  year?: number;
925
- tracks: AudioTrack[];
1143
+ tracks: TTrack[];
926
1144
  }
927
- interface AudioArtist {
1145
+ interface AudioArtist<TTrack extends AudioTrack = AudioTrack> {
928
1146
  id: string;
929
1147
  name: string;
930
1148
  artwork?: string;
931
- tracks: AudioTrack[];
1149
+ tracks: TTrack[];
932
1150
  }
933
- interface AudioGenre {
1151
+ interface AudioGenre<TTrack extends AudioTrack = AudioTrack> {
934
1152
  id: string;
935
1153
  name: string;
936
- tracks: AudioTrack[];
1154
+ tracks: TTrack[];
937
1155
  }
938
- interface AudioPlaylistRef {
1156
+ interface AudioPlaylist {
939
1157
  id: string;
940
1158
  name: string;
1159
+ description?: string;
1160
+ artwork?: string;
941
1161
  trackIds: string[];
942
1162
  }
943
- interface AudioLibrarySnapshot {
944
- tracks: AudioTrack[];
945
- albums: AudioAlbum[];
946
- artists: AudioArtist[];
947
- genres: AudioGenre[];
948
- playlists: AudioPlaylistRef[];
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[];
949
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;
950
1611
 
951
1612
  type RepeatMode = 'off' | 'one' | 'all';
952
1613
  interface QueueItem<TItem = unknown> {
@@ -961,7 +1622,166 @@ interface QueueSnapshot<TItem = unknown> {
961
1622
  history: number[];
962
1623
  }
963
1624
 
964
- type DevicePlaybackKind = 'dlna';
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;
965
1785
  type DevicePlaybackStatus = 'idle' | 'connecting' | 'playing' | 'failed';
966
1786
  interface PlaybackDevice {
967
1787
  id: string;
@@ -979,5 +1799,693 @@ interface DevicePlaybackState {
979
1799
  message?: string;
980
1800
  error?: string;
981
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;
982
2490
 
983
- export { type AdsConfig, type AdsMode, type AspectRatioMode, type AudioAlbum, type AudioArtist, type AudioGenre, type AudioLibrarySnapshot, type AudioPlaylistRef, type AudioTrack, type CacheStore, type CapabilityReport, ConsoleLogPort, DEFAULT_ADS_CONFIG, DEFAULT_METADATA_CONFIG, DEFAULT_PROGRESS_CONFIG, DEFAULT_RESILIENCE_CONFIG, DEFAULT_SUBTITLES_CONFIG, DEFAULT_UI_CONFIG, DefaultCacheStore, type DevicePlaybackKind, type DevicePlaybackState, type DevicePlaybackStatus, type DiagnosticsEngineInfo, type DiagnosticsSnapshot, type DrmConfig, type EngineEvent, type EngineFactory, type EngineProfileHints, type EngineProfileId, type EpisodeItem, type HttpPort, type LoadOptions, LocalMetadataProvider, LocalSubtitleProvider, type LogPort, type MediaArtwork, type MediaItem, type MediaKind, type MediaProgress, type MediaTrack, MemoryStoragePort, type MetadataConfig, type MetadataHint, type MetadataMatch, type MetadataProvider, NativeEngine, type NextEpisodeHint, NoopTelemetryPort, type PlaybackCapabilities, type PlaybackDevice, type PlaybackEngine, 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, type RemoteConfigConfig, type RemoteConfigPayload, type RepeatMode, type ResilienceConfig, ResiliencePlugin, type SeriesConfig, SimpleEventBus, type SkipSegment, type SourceType, type SsaiManifestRewriteConfig, type StoragePort, type SubtitleCandidate, type SubtitleHint, type SubtitleProvider, type SubtitleQuery, type SubtitlesConfig, type TelemetryEvent, type TelemetryLevel, type TelemetryPort, type TelemetrySample, type UiConfig, type Unsubscribe, type VideoMetadata, applyEngineProfileToSource, applyRemoteConfig, createCapabilityReport, createDefaultCapabilities, createDefaultConfig, createDefaultPorts, createMediaDecodingReport, createNativeEngineFactory, createSsaiManifestRewritePlugin, detectPlaybackCapabilities, getEngineProfileHints, isCategorizedEventType, selectEngine };
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 };