@francofantomius/jellyfin 0.1.0 → 0.4.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/README.md +249 -0
- package/dist/index.cjs +1879 -164
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +850 -6
- package/dist/index.d.ts +850 -6
- package/dist/index.js +1866 -165
- package/dist/index.js.map +1 -1
- package/package.json +14 -5
package/dist/index.cjs
CHANGED
|
@@ -125,7 +125,15 @@ var HttpTransport = class {
|
|
|
125
125
|
throw new JellyfinAuthError(`Unauthorized (HTTP ${response.status}) on ${endpoint}`, response.status, endpoint);
|
|
126
126
|
}
|
|
127
127
|
if (!response.ok) {
|
|
128
|
-
|
|
128
|
+
let details = "";
|
|
129
|
+
try {
|
|
130
|
+
const text = await response.text();
|
|
131
|
+
if (text) {
|
|
132
|
+
details = `: ${text.slice(0, 500)}`;
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
}
|
|
136
|
+
throw new JellyfinApiError(`Jellyfin API request failed (${endpoint}): ${response.statusText || response.status}${details}`, response.status, endpoint);
|
|
129
137
|
}
|
|
130
138
|
return response;
|
|
131
139
|
}
|
|
@@ -219,11 +227,22 @@ var AuthModule = class {
|
|
|
219
227
|
const payload = {
|
|
220
228
|
PlayableMediaTypes: ["Audio", "Video"],
|
|
221
229
|
SupportedCommands: [
|
|
222
|
-
"
|
|
230
|
+
"MoveUp",
|
|
231
|
+
"MoveDown",
|
|
232
|
+
"MoveLeft",
|
|
233
|
+
"MoveRight",
|
|
234
|
+
"Select",
|
|
235
|
+
"Back",
|
|
236
|
+
"VolumeUp",
|
|
237
|
+
"VolumeDown",
|
|
238
|
+
"Mute",
|
|
239
|
+
"Unmute",
|
|
240
|
+
"ToggleMute",
|
|
241
|
+
"SetVolume",
|
|
223
242
|
"PlayState",
|
|
224
|
-
"PlayNext",
|
|
225
243
|
"SetRepeatMode",
|
|
226
|
-
"SetShuffleQueue"
|
|
244
|
+
"SetShuffleQueue",
|
|
245
|
+
"DisplayMessage"
|
|
227
246
|
],
|
|
228
247
|
SupportsMediaControl: true,
|
|
229
248
|
SupportsSync: false,
|
|
@@ -702,22 +721,41 @@ var LibraryModule = class {
|
|
|
702
721
|
if (!query || !query.trim()) {
|
|
703
722
|
return { Items: [], TotalRecordCount: 0 };
|
|
704
723
|
}
|
|
705
|
-
const userId = this.requireUserId();
|
|
724
|
+
const userId = options.userId || this.requireUserId();
|
|
706
725
|
const {
|
|
707
726
|
limit = 30,
|
|
708
727
|
startIndex = 0,
|
|
709
|
-
|
|
728
|
+
parentId,
|
|
729
|
+
includeItemTypes = "Audio,MusicAlbum,MusicArtist,Playlist,Movie,Series,Episode",
|
|
730
|
+
mediaTypes,
|
|
731
|
+
genres,
|
|
732
|
+
years,
|
|
733
|
+
isFavorite,
|
|
734
|
+
sortBy,
|
|
735
|
+
sortOrder,
|
|
736
|
+
fields = COMMON_ITEM_FIELDS,
|
|
737
|
+
recursive = true
|
|
710
738
|
} = options;
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
739
|
+
const params = {
|
|
740
|
+
SearchTerm: query.trim(),
|
|
741
|
+
Limit: limit,
|
|
742
|
+
StartIndex: startIndex,
|
|
743
|
+
Recursive: recursive,
|
|
744
|
+
Fields: fields
|
|
745
|
+
};
|
|
746
|
+
if (parentId) params.ParentId = parentId;
|
|
747
|
+
if (includeItemTypes) {
|
|
748
|
+
params.IncludeItemTypes = Array.isArray(includeItemTypes) ? includeItemTypes.join(",") : includeItemTypes;
|
|
749
|
+
}
|
|
750
|
+
if (mediaTypes) {
|
|
751
|
+
params.MediaTypes = Array.isArray(mediaTypes) ? mediaTypes.join(",") : mediaTypes;
|
|
752
|
+
}
|
|
753
|
+
if (genres && genres.length > 0) params.Genres = genres.join("|");
|
|
754
|
+
if (years && years.length > 0) params.Years = years.join(",");
|
|
755
|
+
if (isFavorite) params.Filters = "IsFavorite";
|
|
756
|
+
if (sortBy) params.SortBy = sortBy;
|
|
757
|
+
if (sortOrder) params.SortOrder = sortOrder;
|
|
758
|
+
return await this.http.request(`/Users/${userId}/Items`, { params });
|
|
721
759
|
}
|
|
722
760
|
};
|
|
723
761
|
|
|
@@ -960,8 +998,9 @@ var PlaybackModule = class {
|
|
|
960
998
|
}
|
|
961
999
|
/**
|
|
962
1000
|
* Report that playback has stopped for an item.
|
|
1001
|
+
* Optionally teardown active transcoding session if playSessionId is provided.
|
|
963
1002
|
*/
|
|
964
|
-
async reportStopped(itemId, positionTicks = 0) {
|
|
1003
|
+
async reportStopped(itemId, positionTicks = 0, options) {
|
|
965
1004
|
if (!itemId || !this.http.getToken()) return;
|
|
966
1005
|
try {
|
|
967
1006
|
await this.http.request("/Sessions/Playing/Stopped", {
|
|
@@ -971,6 +1010,20 @@ var PlaybackModule = class {
|
|
|
971
1010
|
PositionTicks: positionTicks
|
|
972
1011
|
}
|
|
973
1012
|
});
|
|
1013
|
+
if (options?.playSessionId) {
|
|
1014
|
+
const deviceId = this.http.getClientInfo()?.deviceId;
|
|
1015
|
+
if (deviceId) {
|
|
1016
|
+
await this.http.request("/Videos/ActiveEncodings", {
|
|
1017
|
+
method: "DELETE",
|
|
1018
|
+
params: {
|
|
1019
|
+
DeviceId: deviceId,
|
|
1020
|
+
PlaySessionId: options.playSessionId
|
|
1021
|
+
}
|
|
1022
|
+
}).catch((err) => {
|
|
1023
|
+
console.warn("[Jellyfin SDK] Teardown active encoding error:", err);
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
974
1027
|
} catch (e) {
|
|
975
1028
|
console.warn("[Jellyfin SDK] Stop playback report error:", e);
|
|
976
1029
|
}
|
|
@@ -1135,28 +1188,38 @@ var MediaModule = class {
|
|
|
1135
1188
|
const qualityResolved = resolveAudioQuality(options.quality);
|
|
1136
1189
|
const maxStreamingBitrate = options.maxStreamingBitrate ?? qualityResolved.maxStreamingBitrate;
|
|
1137
1190
|
const {
|
|
1191
|
+
static: isStatic = false,
|
|
1138
1192
|
startTimeTicks = 0,
|
|
1139
1193
|
container = "opus,mp3|mp3,aac,m4a,m4b,flac,wav,ogg",
|
|
1140
1194
|
transcodingContainer = "mp3",
|
|
1141
1195
|
audioCodec = "mp3",
|
|
1142
1196
|
useQueryToken = true
|
|
1143
1197
|
} = options;
|
|
1144
|
-
const
|
|
1145
|
-
|
|
1198
|
+
const endpoint = isStatic ? `/Audio/${itemId}/stream` : `/Audio/${itemId}/universal`;
|
|
1199
|
+
const url = new URL(`${serverUrl}${endpoint}`);
|
|
1200
|
+
if (token && useQueryToken) {
|
|
1201
|
+
url.searchParams.append("api_key", token);
|
|
1202
|
+
url.searchParams.append("X-Emby-Token", token);
|
|
1203
|
+
}
|
|
1146
1204
|
if (userId) url.searchParams.append("UserId", userId);
|
|
1147
|
-
|
|
1148
|
-
url.searchParams.append("
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1205
|
+
const targetDeviceId = options.deviceId !== void 0 ? options.deviceId : clientInfo.deviceId;
|
|
1206
|
+
if (targetDeviceId) url.searchParams.append("DeviceId", targetDeviceId);
|
|
1207
|
+
if (isStatic) {
|
|
1208
|
+
url.searchParams.append("static", "true");
|
|
1209
|
+
} else {
|
|
1210
|
+
url.searchParams.append("Container", container);
|
|
1211
|
+
url.searchParams.append("TranscodingContainer", transcodingContainer);
|
|
1212
|
+
url.searchParams.append("TranscodingProtocol", "http");
|
|
1213
|
+
url.searchParams.append("AudioCodec", audioCodec);
|
|
1214
|
+
url.searchParams.append("EnableRedirection", "true");
|
|
1215
|
+
url.searchParams.append("EnableRemoteMedia", "false");
|
|
1216
|
+
}
|
|
1152
1217
|
if (maxStreamingBitrate && maxStreamingBitrate !== "Direct") {
|
|
1153
1218
|
url.searchParams.append("MaxStreamingBitrate", String(maxStreamingBitrate));
|
|
1154
1219
|
}
|
|
1155
1220
|
if (startTimeTicks > 0) {
|
|
1156
1221
|
url.searchParams.append("StartTimeTicks", String(startTimeTicks));
|
|
1157
1222
|
}
|
|
1158
|
-
url.searchParams.append("EnableRedirection", "true");
|
|
1159
|
-
url.searchParams.append("EnableRemoteMedia", "false");
|
|
1160
1223
|
return url.toString();
|
|
1161
1224
|
}
|
|
1162
1225
|
/**
|
|
@@ -1177,9 +1240,13 @@ var MediaModule = class {
|
|
|
1177
1240
|
useQueryToken = true
|
|
1178
1241
|
} = options;
|
|
1179
1242
|
const url = new URL(`${serverUrl}/Audio/${itemId}/master.m3u8`);
|
|
1180
|
-
if (token && useQueryToken)
|
|
1243
|
+
if (token && useQueryToken) {
|
|
1244
|
+
url.searchParams.append("api_key", token);
|
|
1245
|
+
url.searchParams.append("X-Emby-Token", token);
|
|
1246
|
+
}
|
|
1181
1247
|
if (userId) url.searchParams.append("UserId", userId);
|
|
1182
|
-
|
|
1248
|
+
const targetDeviceId = options.deviceId !== void 0 ? options.deviceId : clientInfo.deviceId;
|
|
1249
|
+
if (targetDeviceId) url.searchParams.append("DeviceId", targetDeviceId);
|
|
1183
1250
|
url.searchParams.append("MediaSourceId", itemId);
|
|
1184
1251
|
url.searchParams.append("AudioCodec", audioCodec);
|
|
1185
1252
|
url.searchParams.append("TranscodingContainer", "ts");
|
|
@@ -1225,9 +1292,13 @@ var MediaModule = class {
|
|
|
1225
1292
|
} = options;
|
|
1226
1293
|
const endpoint = container && !container.includes(",") ? `/Videos/${itemId}/stream.${container}` : `/Videos/${itemId}/stream`;
|
|
1227
1294
|
const url = new URL(`${serverUrl}${endpoint}`);
|
|
1228
|
-
if (token && useQueryToken)
|
|
1295
|
+
if (token && useQueryToken) {
|
|
1296
|
+
url.searchParams.append("api_key", token);
|
|
1297
|
+
url.searchParams.append("X-Emby-Token", token);
|
|
1298
|
+
}
|
|
1229
1299
|
if (userId) url.searchParams.append("UserId", userId);
|
|
1230
|
-
|
|
1300
|
+
const targetDeviceId = options.deviceId !== void 0 ? options.deviceId : clientInfo.deviceId;
|
|
1301
|
+
if (targetDeviceId) url.searchParams.append("DeviceId", targetDeviceId);
|
|
1231
1302
|
if (mediaSourceId) url.searchParams.append("MediaSourceId", mediaSourceId);
|
|
1232
1303
|
if (isStatic) {
|
|
1233
1304
|
url.searchParams.append("Static", "true");
|
|
@@ -1286,9 +1357,13 @@ var MediaModule = class {
|
|
|
1286
1357
|
useQueryToken = true
|
|
1287
1358
|
} = options;
|
|
1288
1359
|
const url = new URL(`${serverUrl}/Videos/${itemId}/master.m3u8`);
|
|
1289
|
-
if (token && useQueryToken)
|
|
1360
|
+
if (token && useQueryToken) {
|
|
1361
|
+
url.searchParams.append("api_key", token);
|
|
1362
|
+
url.searchParams.append("X-Emby-Token", token);
|
|
1363
|
+
}
|
|
1290
1364
|
if (userId) url.searchParams.append("UserId", userId);
|
|
1291
|
-
|
|
1365
|
+
const targetDeviceId = options.deviceId !== void 0 ? options.deviceId : clientInfo.deviceId;
|
|
1366
|
+
if (targetDeviceId) url.searchParams.append("DeviceId", targetDeviceId);
|
|
1292
1367
|
if (mediaSourceId) url.searchParams.append("MediaSourceId", mediaSourceId);
|
|
1293
1368
|
url.searchParams.append("VideoCodec", videoCodec);
|
|
1294
1369
|
url.searchParams.append("AudioCodec", audioCodec);
|
|
@@ -1858,153 +1933,1749 @@ var OfflineStorageManager = class {
|
|
|
1858
1933
|
}
|
|
1859
1934
|
};
|
|
1860
1935
|
|
|
1861
|
-
// src/
|
|
1862
|
-
var
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1936
|
+
// src/resume.ts
|
|
1937
|
+
var RESUME_ITEM_FIELDS = "PrimaryImageAspectRatio,PrimaryImageTag,ImageTags,BackdropImageTags,AlbumPrimaryImageTag,AlbumId,AudioInfo,MediaSources,Chapters,HasLyrics,Overview,Genres,Studios,ProductionYear,OfficialRating,CommunityRating,CriticRating,PremiereDate,RunTimeTicks,IndexNumber,ParentIndexNumber,SeriesName,SeriesId,SeasonId,SeasonName,UserData";
|
|
1938
|
+
var ResumeModule = class {
|
|
1939
|
+
http;
|
|
1940
|
+
getUserId;
|
|
1941
|
+
constructor(http, getUserId) {
|
|
1942
|
+
this.http = http;
|
|
1943
|
+
this.getUserId = getUserId;
|
|
1867
1944
|
}
|
|
1868
|
-
|
|
1869
|
-
|
|
1945
|
+
resolveUserId(explicitUserId) {
|
|
1946
|
+
const userId = explicitUserId || this.getUserId();
|
|
1947
|
+
if (!userId) {
|
|
1948
|
+
throw new JellyfinError("User ID is required for resume requests. Please authenticate or provide a userId.");
|
|
1949
|
+
}
|
|
1950
|
+
return userId;
|
|
1870
1951
|
}
|
|
1871
|
-
|
|
1872
|
-
|
|
1952
|
+
/**
|
|
1953
|
+
* Fetch items currently in-progress for the user via /UserItems/Resume.
|
|
1954
|
+
*/
|
|
1955
|
+
async getResumeItems(options = {}) {
|
|
1956
|
+
const userId = this.resolveUserId(options.userId);
|
|
1957
|
+
const {
|
|
1958
|
+
limit = 20,
|
|
1959
|
+
startIndex = 0,
|
|
1960
|
+
parentId,
|
|
1961
|
+
mediaTypes,
|
|
1962
|
+
enableImages,
|
|
1963
|
+
enableUserData,
|
|
1964
|
+
imageTypeLimit,
|
|
1965
|
+
enableImageTypes,
|
|
1966
|
+
fields = RESUME_ITEM_FIELDS
|
|
1967
|
+
} = options;
|
|
1968
|
+
const params = {
|
|
1969
|
+
UserId: userId,
|
|
1970
|
+
Limit: limit,
|
|
1971
|
+
StartIndex: startIndex,
|
|
1972
|
+
Recursive: true,
|
|
1973
|
+
Fields: fields
|
|
1974
|
+
};
|
|
1975
|
+
if (parentId) params.ParentId = parentId;
|
|
1976
|
+
if (mediaTypes && mediaTypes.length > 0) params.MediaTypes = mediaTypes.join(",");
|
|
1977
|
+
if (enableImages !== void 0) params.EnableImages = enableImages;
|
|
1978
|
+
if (enableUserData !== void 0) params.EnableUserData = enableUserData;
|
|
1979
|
+
if (imageTypeLimit !== void 0) params.ImageTypeLimit = imageTypeLimit;
|
|
1980
|
+
if (enableImageTypes && enableImageTypes.length > 0) params.EnableImageTypes = enableImageTypes.join(",");
|
|
1981
|
+
return await this.http.request("/UserItems/Resume", { params });
|
|
1873
1982
|
}
|
|
1874
|
-
|
|
1875
|
-
|
|
1983
|
+
/**
|
|
1984
|
+
* Convenience alias for getResumeItems.
|
|
1985
|
+
*/
|
|
1986
|
+
async getItems(options = {}) {
|
|
1987
|
+
return await this.getResumeItems(options);
|
|
1876
1988
|
}
|
|
1877
1989
|
};
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
this.
|
|
1886
|
-
this.
|
|
1887
|
-
}
|
|
1888
|
-
async getDB() {
|
|
1889
|
-
if (this.dbPromise) return this.dbPromise;
|
|
1890
|
-
this.dbPromise = new Promise((resolve) => {
|
|
1891
|
-
if (typeof window === "undefined" || !("indexedDB" in window)) {
|
|
1892
|
-
resolve(null);
|
|
1893
|
-
return;
|
|
1894
|
-
}
|
|
1895
|
-
const request = indexedDB.open(this.dbName, this.dbVersion);
|
|
1896
|
-
request.onupgradeneeded = (event) => {
|
|
1897
|
-
const db = event.target.result;
|
|
1898
|
-
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
1899
|
-
db.createObjectStore(this.storeName, { keyPath: "key" });
|
|
1900
|
-
}
|
|
1901
|
-
};
|
|
1902
|
-
request.onsuccess = () => resolve(request.result);
|
|
1903
|
-
request.onerror = (e) => {
|
|
1904
|
-
console.warn("[Jellyfin Cache] IndexedDB open error:", e);
|
|
1905
|
-
resolve(null);
|
|
1906
|
-
};
|
|
1907
|
-
});
|
|
1908
|
-
return this.dbPromise;
|
|
1990
|
+
|
|
1991
|
+
// src/next-up.ts
|
|
1992
|
+
var NEXT_UP_FIELDS = "PrimaryImageAspectRatio,PrimaryImageTag,ImageTags,BackdropImageTags,AlbumPrimaryImageTag,AudioInfo,MediaSources,Chapters,Overview,Genres,ProductionYear,PremiereDate,RunTimeTicks,IndexNumber,ParentIndexNumber,SeriesName,SeriesId,SeasonId,SeasonName,UserData";
|
|
1993
|
+
var NextUpModule = class {
|
|
1994
|
+
http;
|
|
1995
|
+
getUserId;
|
|
1996
|
+
constructor(http, getUserId) {
|
|
1997
|
+
this.http = http;
|
|
1998
|
+
this.getUserId = getUserId;
|
|
1909
1999
|
}
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
return new Promise((resolve) => {
|
|
1915
|
-
const tx = db.transaction(this.storeName, "readonly");
|
|
1916
|
-
const store = tx.objectStore(this.storeName);
|
|
1917
|
-
const request = store.get(key);
|
|
1918
|
-
request.onsuccess = () => {
|
|
1919
|
-
const record = request.result;
|
|
1920
|
-
resolve(record ? record.data : null);
|
|
1921
|
-
};
|
|
1922
|
-
request.onerror = () => resolve(null);
|
|
1923
|
-
});
|
|
1924
|
-
} catch {
|
|
1925
|
-
return null;
|
|
2000
|
+
resolveUserId(explicitUserId) {
|
|
2001
|
+
const userId = explicitUserId || this.getUserId();
|
|
2002
|
+
if (!userId) {
|
|
2003
|
+
throw new JellyfinError("User ID is required for next up requests. Please authenticate or provide a userId.");
|
|
1926
2004
|
}
|
|
2005
|
+
return userId;
|
|
1927
2006
|
}
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
2007
|
+
/**
|
|
2008
|
+
* Fetch the next unplayed episodes for followed series via /Shows/NextUp.
|
|
2009
|
+
*/
|
|
2010
|
+
async getNextUp(options = {}) {
|
|
2011
|
+
const userId = this.resolveUserId(options.userId);
|
|
2012
|
+
const {
|
|
2013
|
+
limit = 24,
|
|
2014
|
+
startIndex = 0,
|
|
2015
|
+
parentId,
|
|
2016
|
+
seriesId,
|
|
2017
|
+
fields = NEXT_UP_FIELDS,
|
|
2018
|
+
enableImages,
|
|
2019
|
+
enableUserData,
|
|
2020
|
+
enableTotalRecordCount,
|
|
2021
|
+
imageTypeLimit,
|
|
2022
|
+
enableImageTypes,
|
|
2023
|
+
disableFirstEpisode,
|
|
2024
|
+
nextUpDateCutoff
|
|
2025
|
+
} = options;
|
|
2026
|
+
const params = {
|
|
2027
|
+
UserId: userId,
|
|
2028
|
+
Limit: limit,
|
|
2029
|
+
StartIndex: startIndex,
|
|
2030
|
+
Fields: fields
|
|
2031
|
+
};
|
|
2032
|
+
if (parentId) params.ParentId = parentId;
|
|
2033
|
+
if (seriesId) params.SeriesId = seriesId;
|
|
2034
|
+
if (enableImages !== void 0) params.EnableImages = enableImages;
|
|
2035
|
+
if (enableUserData !== void 0) params.EnableUserData = enableUserData;
|
|
2036
|
+
if (enableTotalRecordCount !== void 0) params.EnableTotalRecordCount = enableTotalRecordCount;
|
|
2037
|
+
if (imageTypeLimit !== void 0) params.ImageTypeLimit = imageTypeLimit;
|
|
2038
|
+
if (enableImageTypes && enableImageTypes.length > 0) params.EnableImageTypes = enableImageTypes.join(",");
|
|
2039
|
+
if (disableFirstEpisode !== void 0) params.DisableFirstEpisode = disableFirstEpisode;
|
|
2040
|
+
if (nextUpDateCutoff) params.NextUpDateCutoff = nextUpDateCutoff;
|
|
2041
|
+
return await this.http.request("/Shows/NextUp", { params });
|
|
1943
2042
|
}
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
const tx = db.transaction(this.storeName, "readwrite");
|
|
1950
|
-
const store = tx.objectStore(this.storeName);
|
|
1951
|
-
const request = store.delete(key);
|
|
1952
|
-
request.onsuccess = () => resolve();
|
|
1953
|
-
request.onerror = () => resolve();
|
|
1954
|
-
});
|
|
1955
|
-
} catch {
|
|
1956
|
-
}
|
|
2043
|
+
/**
|
|
2044
|
+
* Convenience alias for getNextUp.
|
|
2045
|
+
*/
|
|
2046
|
+
async getEpisodes(options = {}) {
|
|
2047
|
+
return await this.getNextUp(options);
|
|
1957
2048
|
}
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
2049
|
+
};
|
|
2050
|
+
|
|
2051
|
+
// src/latest.ts
|
|
2052
|
+
var LATEST_ITEM_FIELDS = "PrimaryImageAspectRatio,PrimaryImageTag,ImageTags,BackdropImageTags,AlbumPrimaryImageTag,AlbumId,AudioInfo,MediaSources,Chapters,Overview,Genres,Studios,ProductionYear,PremiereDate,RunTimeTicks,IndexNumber,ParentIndexNumber,SeriesName,SeriesId,SeasonId,SeasonName,UserData";
|
|
2053
|
+
var LatestModule = class {
|
|
2054
|
+
http;
|
|
2055
|
+
getUserId;
|
|
2056
|
+
constructor(http, getUserId) {
|
|
2057
|
+
this.http = http;
|
|
2058
|
+
this.getUserId = getUserId;
|
|
2059
|
+
}
|
|
2060
|
+
resolveUserId(explicitUserId) {
|
|
2061
|
+
const userId = explicitUserId || this.getUserId();
|
|
2062
|
+
if (!userId) {
|
|
2063
|
+
throw new JellyfinError("User ID is required for latest media requests. Please authenticate or provide a userId.");
|
|
1970
2064
|
}
|
|
2065
|
+
return userId;
|
|
1971
2066
|
}
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
2067
|
+
/**
|
|
2068
|
+
* Fetch recently added media items for the user via /Users/{userId}/Items/Latest.
|
|
2069
|
+
*/
|
|
2070
|
+
async getLatest(options = {}) {
|
|
2071
|
+
const userId = this.resolveUserId(options.userId);
|
|
2072
|
+
const {
|
|
2073
|
+
parentId,
|
|
2074
|
+
limit = 20,
|
|
2075
|
+
fields = LATEST_ITEM_FIELDS,
|
|
2076
|
+
includeItemTypes,
|
|
2077
|
+
isPlayed,
|
|
2078
|
+
enableImages,
|
|
2079
|
+
imageTypeLimit,
|
|
2080
|
+
enableImageTypes,
|
|
2081
|
+
groupItems
|
|
2082
|
+
} = options;
|
|
2083
|
+
const params = {
|
|
2084
|
+
Limit: limit,
|
|
2085
|
+
Fields: fields
|
|
2086
|
+
};
|
|
2087
|
+
if (parentId) params.ParentId = parentId;
|
|
2088
|
+
if (includeItemTypes && includeItemTypes.length > 0) {
|
|
2089
|
+
params.IncludeItemTypes = includeItemTypes.join(",");
|
|
1985
2090
|
}
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
if (
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
2091
|
+
if (isPlayed !== void 0) params.IsPlayed = isPlayed;
|
|
2092
|
+
if (enableImages !== void 0) params.EnableImages = enableImages;
|
|
2093
|
+
if (imageTypeLimit !== void 0) params.ImageTypeLimit = imageTypeLimit;
|
|
2094
|
+
if (enableImageTypes && enableImageTypes.length > 0) {
|
|
2095
|
+
params.EnableImageTypes = enableImageTypes.join(",");
|
|
2096
|
+
}
|
|
2097
|
+
if (groupItems !== void 0) params.GroupItems = groupItems;
|
|
2098
|
+
const result = await this.http.request(`/Users/${userId}/Items/Latest`, { params });
|
|
2099
|
+
return Array.isArray(result) ? result : [];
|
|
1994
2100
|
}
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
store = /* @__PURE__ */ new Map();
|
|
2001
|
-
getItem(key) {
|
|
2002
|
-
return this.store.has(key) ? this.store.get(key) : null;
|
|
2101
|
+
/**
|
|
2102
|
+
* Convenience helper to fetch recently added movies.
|
|
2103
|
+
*/
|
|
2104
|
+
async getLatestMovies(options = {}) {
|
|
2105
|
+
return await this.getLatest({ ...options, includeItemTypes: ["Movie"] });
|
|
2003
2106
|
}
|
|
2004
|
-
|
|
2005
|
-
|
|
2107
|
+
/**
|
|
2108
|
+
* Convenience helper to fetch recently added TV episodes.
|
|
2109
|
+
*/
|
|
2110
|
+
async getLatestEpisodes(options = {}) {
|
|
2111
|
+
return await this.getLatest({ ...options, includeItemTypes: ["Episode"] });
|
|
2006
2112
|
}
|
|
2007
|
-
|
|
2113
|
+
/**
|
|
2114
|
+
* Convenience helper to fetch recently added music albums.
|
|
2115
|
+
*/
|
|
2116
|
+
async getLatestAlbums(options = {}) {
|
|
2117
|
+
return await this.getLatest({ ...options, includeItemTypes: ["MusicAlbum"] });
|
|
2118
|
+
}
|
|
2119
|
+
};
|
|
2120
|
+
|
|
2121
|
+
// src/transcode.ts
|
|
2122
|
+
var TranscodeModule = class {
|
|
2123
|
+
http;
|
|
2124
|
+
getDeviceId;
|
|
2125
|
+
constructor(http, getDeviceId) {
|
|
2126
|
+
this.http = http;
|
|
2127
|
+
this.getDeviceId = getDeviceId;
|
|
2128
|
+
}
|
|
2129
|
+
/**
|
|
2130
|
+
* Explicitly stop an active server transcoding session via DELETE /Videos/ActiveEncodings.
|
|
2131
|
+
* This terminates lingering FFmpeg processes on the Jellyfin server.
|
|
2132
|
+
*
|
|
2133
|
+
* @param playSessionId The unique playback session ID.
|
|
2134
|
+
* @param deviceId The client device ID (defaults to configured client device ID).
|
|
2135
|
+
*/
|
|
2136
|
+
async stopActiveEncoding(playSessionId, deviceId) {
|
|
2137
|
+
if (!playSessionId) {
|
|
2138
|
+
throw new JellyfinError("playSessionId is required to stop an active encoding session.");
|
|
2139
|
+
}
|
|
2140
|
+
const resolvedDeviceId = deviceId || this.getDeviceId();
|
|
2141
|
+
if (!resolvedDeviceId) {
|
|
2142
|
+
throw new JellyfinError("deviceId is required to stop an active encoding session.");
|
|
2143
|
+
}
|
|
2144
|
+
await this.http.request("/Videos/ActiveEncodings", {
|
|
2145
|
+
method: "DELETE",
|
|
2146
|
+
params: {
|
|
2147
|
+
DeviceId: resolvedDeviceId,
|
|
2148
|
+
PlaySessionId: playSessionId
|
|
2149
|
+
}
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
/**
|
|
2153
|
+
* Convenience alias for stopActiveEncoding.
|
|
2154
|
+
*/
|
|
2155
|
+
async stop(playSessionId, deviceId) {
|
|
2156
|
+
return await this.stopActiveEncoding(playSessionId, deviceId);
|
|
2157
|
+
}
|
|
2158
|
+
};
|
|
2159
|
+
|
|
2160
|
+
// src/search.ts
|
|
2161
|
+
var COMMON_SEARCH_FIELDS = "PrimaryImageAspectRatio,PrimaryImageTag,ImageTags,BackdropImageTags,AlbumPrimaryImageTag,AlbumId,AudioInfo,MediaSources,Chapters,HasLyrics,Overview,Genres,Studios,ProductionYear,OfficialRating,CommunityRating,CriticRating,PremiereDate,RunTimeTicks,IndexNumber,ParentIndexNumber,SeriesName,SeriesId,SeasonId,SeasonName,UserData";
|
|
2162
|
+
var SearchModule = class {
|
|
2163
|
+
http;
|
|
2164
|
+
getUserId;
|
|
2165
|
+
constructor(http, getUserId) {
|
|
2166
|
+
this.http = http;
|
|
2167
|
+
this.getUserId = getUserId;
|
|
2168
|
+
}
|
|
2169
|
+
resolveUserId(explicitUserId) {
|
|
2170
|
+
const userId = explicitUserId || this.getUserId();
|
|
2171
|
+
if (!userId) {
|
|
2172
|
+
throw new JellyfinError("User ID is required for search requests. Please authenticate or provide a userId.");
|
|
2173
|
+
}
|
|
2174
|
+
return userId;
|
|
2175
|
+
}
|
|
2176
|
+
/**
|
|
2177
|
+
* Universal search across Jellyfin items with comprehensive filters.
|
|
2178
|
+
*/
|
|
2179
|
+
async search(query, options = {}) {
|
|
2180
|
+
if (!query || !query.trim()) {
|
|
2181
|
+
return { Items: [], TotalRecordCount: 0 };
|
|
2182
|
+
}
|
|
2183
|
+
const userId = this.resolveUserId(options.userId);
|
|
2184
|
+
const {
|
|
2185
|
+
limit = 30,
|
|
2186
|
+
startIndex = 0,
|
|
2187
|
+
parentId,
|
|
2188
|
+
includeItemTypes = "Audio,MusicAlbum,MusicArtist,Playlist,Movie,Series,Episode",
|
|
2189
|
+
mediaTypes,
|
|
2190
|
+
genres,
|
|
2191
|
+
years,
|
|
2192
|
+
isFavorite,
|
|
2193
|
+
sortBy,
|
|
2194
|
+
sortOrder,
|
|
2195
|
+
fields = COMMON_SEARCH_FIELDS,
|
|
2196
|
+
recursive = true
|
|
2197
|
+
} = options;
|
|
2198
|
+
const params = {
|
|
2199
|
+
SearchTerm: query.trim(),
|
|
2200
|
+
Limit: limit,
|
|
2201
|
+
StartIndex: startIndex,
|
|
2202
|
+
Recursive: recursive,
|
|
2203
|
+
Fields: fields
|
|
2204
|
+
};
|
|
2205
|
+
if (parentId) params.ParentId = parentId;
|
|
2206
|
+
if (includeItemTypes) {
|
|
2207
|
+
params.IncludeItemTypes = Array.isArray(includeItemTypes) ? includeItemTypes.join(",") : includeItemTypes;
|
|
2208
|
+
}
|
|
2209
|
+
if (mediaTypes) {
|
|
2210
|
+
params.MediaTypes = Array.isArray(mediaTypes) ? mediaTypes.join(",") : mediaTypes;
|
|
2211
|
+
}
|
|
2212
|
+
if (genres && genres.length > 0) params.Genres = genres.join("|");
|
|
2213
|
+
if (years && years.length > 0) params.Years = years.join(",");
|
|
2214
|
+
if (isFavorite) params.Filters = "IsFavorite";
|
|
2215
|
+
if (sortBy) params.SortBy = sortBy;
|
|
2216
|
+
if (sortOrder) params.SortOrder = sortOrder;
|
|
2217
|
+
return await this.http.request(`/Users/${userId}/Items`, { params });
|
|
2218
|
+
}
|
|
2219
|
+
/**
|
|
2220
|
+
* Search specifically within a specific library or parent folder.
|
|
2221
|
+
*/
|
|
2222
|
+
async searchInLibrary(parentId, query, options = {}) {
|
|
2223
|
+
return await this.search(query, { ...options, parentId });
|
|
2224
|
+
}
|
|
2225
|
+
/**
|
|
2226
|
+
* Convenience helper to search audio tracks / songs.
|
|
2227
|
+
*/
|
|
2228
|
+
async searchSongs(query, options = {}) {
|
|
2229
|
+
return await this.search(query, { ...options, includeItemTypes: "Audio" });
|
|
2230
|
+
}
|
|
2231
|
+
/**
|
|
2232
|
+
* Convenience helper to search music albums.
|
|
2233
|
+
*/
|
|
2234
|
+
async searchAlbums(query, options = {}) {
|
|
2235
|
+
return await this.search(query, { ...options, includeItemTypes: "MusicAlbum" });
|
|
2236
|
+
}
|
|
2237
|
+
/**
|
|
2238
|
+
* Convenience helper to search music artists.
|
|
2239
|
+
*/
|
|
2240
|
+
async searchArtists(query, options = {}) {
|
|
2241
|
+
return await this.search(query, { ...options, includeItemTypes: "MusicArtist" });
|
|
2242
|
+
}
|
|
2243
|
+
/**
|
|
2244
|
+
* Convenience helper to search movies.
|
|
2245
|
+
*/
|
|
2246
|
+
async searchMovies(query, options = {}) {
|
|
2247
|
+
return await this.search(query, { ...options, includeItemTypes: "Movie" });
|
|
2248
|
+
}
|
|
2249
|
+
/**
|
|
2250
|
+
* Convenience helper to search TV series.
|
|
2251
|
+
*/
|
|
2252
|
+
async searchSeries(query, options = {}) {
|
|
2253
|
+
return await this.search(query, { ...options, includeItemTypes: "Series" });
|
|
2254
|
+
}
|
|
2255
|
+
/**
|
|
2256
|
+
* Convenience helper to search TV episodes.
|
|
2257
|
+
*/
|
|
2258
|
+
async searchEpisodes(query, options = {}) {
|
|
2259
|
+
return await this.search(query, { ...options, includeItemTypes: "Episode" });
|
|
2260
|
+
}
|
|
2261
|
+
};
|
|
2262
|
+
|
|
2263
|
+
// src/websocket.ts
|
|
2264
|
+
var WebSocketModule = class {
|
|
2265
|
+
http;
|
|
2266
|
+
getDeviceId;
|
|
2267
|
+
ws = null;
|
|
2268
|
+
keepAliveTimer = null;
|
|
2269
|
+
reconnectTimer = null;
|
|
2270
|
+
listeners = /* @__PURE__ */ new Map();
|
|
2271
|
+
reconnectAttempts = 0;
|
|
2272
|
+
explicitlyClosed = false;
|
|
2273
|
+
options = {};
|
|
2274
|
+
constructor(http, getDeviceId) {
|
|
2275
|
+
this.http = http;
|
|
2276
|
+
this.getDeviceId = getDeviceId;
|
|
2277
|
+
}
|
|
2278
|
+
/**
|
|
2279
|
+
* Check whether the WebSocket is currently open and connected.
|
|
2280
|
+
*/
|
|
2281
|
+
get isConnected() {
|
|
2282
|
+
return this.ws !== null && this.ws.readyState === 1;
|
|
2283
|
+
}
|
|
2284
|
+
/**
|
|
2285
|
+
* Build the Jellyfin WebSocket URL from current serverUrl, accessToken, and deviceId.
|
|
2286
|
+
*/
|
|
2287
|
+
getWebSocketUrl() {
|
|
2288
|
+
const serverUrl = this.http.getServerUrl();
|
|
2289
|
+
if (!serverUrl) {
|
|
2290
|
+
throw new JellyfinError("Server URL is required to connect to WebSocket.");
|
|
2291
|
+
}
|
|
2292
|
+
const token = this.http.getToken();
|
|
2293
|
+
if (!token) {
|
|
2294
|
+
throw new JellyfinError("Access token is required to connect to WebSocket. Please authenticate first.");
|
|
2295
|
+
}
|
|
2296
|
+
const deviceId = this.getDeviceId();
|
|
2297
|
+
const wsBaseUrl = serverUrl.replace(/^http:\/\//i, "ws://").replace(/^https:\/\//i, "wss://").replace(/\/+$/, "");
|
|
2298
|
+
const url = new URL(`${wsBaseUrl}/socket`);
|
|
2299
|
+
url.searchParams.set("api_key", token);
|
|
2300
|
+
if (deviceId) {
|
|
2301
|
+
url.searchParams.set("deviceId", deviceId);
|
|
2302
|
+
}
|
|
2303
|
+
return url.toString();
|
|
2304
|
+
}
|
|
2305
|
+
/**
|
|
2306
|
+
* Connect to the Jellyfin WebSocket endpoint.
|
|
2307
|
+
*/
|
|
2308
|
+
connect(options = {}) {
|
|
2309
|
+
this.options = {
|
|
2310
|
+
autoReconnect: true,
|
|
2311
|
+
reconnectIntervalMs: 5e3,
|
|
2312
|
+
maxReconnectAttempts: 10,
|
|
2313
|
+
keepAliveIntervalMs: 3e4,
|
|
2314
|
+
...options
|
|
2315
|
+
};
|
|
2316
|
+
this.explicitlyClosed = false;
|
|
2317
|
+
this.cleanup();
|
|
2318
|
+
const socketUrl = this.options.url || this.getWebSocketUrl();
|
|
2319
|
+
let SocketConstructor = this.options.webSocketFactory ? null : typeof globalThis !== "undefined" ? globalThis.WebSocket : null;
|
|
2320
|
+
if (this.options.webSocketFactory) {
|
|
2321
|
+
this.ws = this.options.webSocketFactory(socketUrl);
|
|
2322
|
+
} else if (SocketConstructor) {
|
|
2323
|
+
this.ws = new SocketConstructor(socketUrl);
|
|
2324
|
+
} else {
|
|
2325
|
+
throw new JellyfinError("WebSocket constructor is not available in the current environment. Please supply webSocketFactory in options.");
|
|
2326
|
+
}
|
|
2327
|
+
this.setupSocketEvents();
|
|
2328
|
+
}
|
|
2329
|
+
/**
|
|
2330
|
+
* Disconnect the WebSocket and prevent automatic reconnection.
|
|
2331
|
+
*/
|
|
2332
|
+
disconnect() {
|
|
2333
|
+
this.explicitlyClosed = true;
|
|
2334
|
+
this.cleanup();
|
|
2335
|
+
}
|
|
2336
|
+
/**
|
|
2337
|
+
* Send a raw string or JSON message through the open WebSocket.
|
|
2338
|
+
*/
|
|
2339
|
+
send(message) {
|
|
2340
|
+
if (!this.isConnected) {
|
|
2341
|
+
throw new JellyfinError("Cannot send message: WebSocket is not open.");
|
|
2342
|
+
}
|
|
2343
|
+
const payload = typeof message === "string" ? message : JSON.stringify(message);
|
|
2344
|
+
this.ws.send(payload);
|
|
2345
|
+
}
|
|
2346
|
+
/**
|
|
2347
|
+
* Send a KeepAlive message to the Jellyfin server.
|
|
2348
|
+
*/
|
|
2349
|
+
sendKeepAlive() {
|
|
2350
|
+
this.send({ MessageType: "KeepAlive" });
|
|
2351
|
+
}
|
|
2352
|
+
/**
|
|
2353
|
+
* Subscribe to real-time session updates from the server.
|
|
2354
|
+
*/
|
|
2355
|
+
startSessionsSubscription(intervalMs = 1500) {
|
|
2356
|
+
this.send({
|
|
2357
|
+
MessageType: "SessionsStart",
|
|
2358
|
+
Data: `0,${intervalMs}`
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
/**
|
|
2362
|
+
* Stop receiving real-time session updates.
|
|
2363
|
+
*/
|
|
2364
|
+
stopSessionsSubscription() {
|
|
2365
|
+
this.send({ MessageType: "SessionsStop" });
|
|
2366
|
+
}
|
|
2367
|
+
/**
|
|
2368
|
+
* Subscribe to scheduled tasks updates from the server.
|
|
2369
|
+
*/
|
|
2370
|
+
startScheduledTasksSubscription(intervalMs = 1500) {
|
|
2371
|
+
this.send({
|
|
2372
|
+
MessageType: "ScheduledTasksInfoStart",
|
|
2373
|
+
Data: `0,${intervalMs}`
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
/**
|
|
2377
|
+
* Stop scheduled tasks updates.
|
|
2378
|
+
*/
|
|
2379
|
+
stopScheduledTasksSubscription() {
|
|
2380
|
+
this.send({ MessageType: "ScheduledTasksInfoStop" });
|
|
2381
|
+
}
|
|
2382
|
+
/**
|
|
2383
|
+
* Subscribe to server activity log updates.
|
|
2384
|
+
*/
|
|
2385
|
+
startActivityLogSubscription(intervalMs = 1500) {
|
|
2386
|
+
this.send({
|
|
2387
|
+
MessageType: "ActivityLogEntryStart",
|
|
2388
|
+
Data: `0,${intervalMs}`
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
/**
|
|
2392
|
+
* Stop server activity log updates.
|
|
2393
|
+
*/
|
|
2394
|
+
stopActivityLogSubscription() {
|
|
2395
|
+
this.send({ MessageType: "ActivityLogEntryStop" });
|
|
2396
|
+
}
|
|
2397
|
+
on(event, listener) {
|
|
2398
|
+
const key = event.toLowerCase();
|
|
2399
|
+
if (!this.listeners.has(key)) {
|
|
2400
|
+
this.listeners.set(key, /* @__PURE__ */ new Set());
|
|
2401
|
+
}
|
|
2402
|
+
this.listeners.get(key).add(listener);
|
|
2403
|
+
return () => this.off(key, listener);
|
|
2404
|
+
}
|
|
2405
|
+
off(event, listener) {
|
|
2406
|
+
const key = event.toLowerCase();
|
|
2407
|
+
const set = this.listeners.get(key);
|
|
2408
|
+
if (set) {
|
|
2409
|
+
set.delete(listener);
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
emit(event, ...args) {
|
|
2413
|
+
const key = event.toLowerCase();
|
|
2414
|
+
const set = this.listeners.get(key);
|
|
2415
|
+
if (set) {
|
|
2416
|
+
set.forEach((listener) => {
|
|
2417
|
+
try {
|
|
2418
|
+
listener(...args);
|
|
2419
|
+
} catch (e) {
|
|
2420
|
+
console.error(`[Jellyfin SDK] WebSocket listener error on '${event}':`, e);
|
|
2421
|
+
}
|
|
2422
|
+
});
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
// --- Internal Socket Helpers ---
|
|
2426
|
+
setupSocketEvents() {
|
|
2427
|
+
if (!this.ws) return;
|
|
2428
|
+
this.ws.onopen = () => {
|
|
2429
|
+
this.reconnectAttempts = 0;
|
|
2430
|
+
this.startKeepAlive();
|
|
2431
|
+
this.emit("open");
|
|
2432
|
+
};
|
|
2433
|
+
this.ws.onclose = (ev) => {
|
|
2434
|
+
this.stopKeepAlive();
|
|
2435
|
+
this.emit("close", ev?.code, ev?.reason);
|
|
2436
|
+
if (!this.explicitlyClosed && this.options.autoReconnect) {
|
|
2437
|
+
this.scheduleReconnect();
|
|
2438
|
+
}
|
|
2439
|
+
};
|
|
2440
|
+
this.ws.onerror = (err) => {
|
|
2441
|
+
this.emit("error", err);
|
|
2442
|
+
};
|
|
2443
|
+
this.ws.onmessage = (event) => {
|
|
2444
|
+
try {
|
|
2445
|
+
const text = typeof event.data === "string" ? event.data : String(event.data);
|
|
2446
|
+
const parsed = JSON.parse(text);
|
|
2447
|
+
this.emit("message", parsed);
|
|
2448
|
+
if (parsed.MessageType) {
|
|
2449
|
+
this.emit(parsed.MessageType, parsed.Data);
|
|
2450
|
+
if (parsed.MessageType === "ForceKeepAlive") {
|
|
2451
|
+
this.sendKeepAlive();
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
} catch (e) {
|
|
2455
|
+
this.emit("rawMessage", event.data);
|
|
2456
|
+
}
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
startKeepAlive() {
|
|
2460
|
+
this.stopKeepAlive();
|
|
2461
|
+
const interval = this.options.keepAliveIntervalMs || 3e4;
|
|
2462
|
+
if (interval > 0) {
|
|
2463
|
+
this.keepAliveTimer = setInterval(() => {
|
|
2464
|
+
if (this.isConnected) {
|
|
2465
|
+
try {
|
|
2466
|
+
this.sendKeepAlive();
|
|
2467
|
+
} catch {
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
}, interval);
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
stopKeepAlive() {
|
|
2474
|
+
if (this.keepAliveTimer) {
|
|
2475
|
+
clearInterval(this.keepAliveTimer);
|
|
2476
|
+
this.keepAliveTimer = null;
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
scheduleReconnect() {
|
|
2480
|
+
const maxAttempts = this.options.maxReconnectAttempts ?? 10;
|
|
2481
|
+
if (this.reconnectAttempts >= maxAttempts) {
|
|
2482
|
+
return;
|
|
2483
|
+
}
|
|
2484
|
+
const interval = this.options.reconnectIntervalMs ?? 5e3;
|
|
2485
|
+
const backoffDelay = Math.min(interval * Math.pow(1.5, this.reconnectAttempts), 6e4);
|
|
2486
|
+
this.reconnectAttempts++;
|
|
2487
|
+
this.reconnectTimer = setTimeout(() => {
|
|
2488
|
+
if (!this.explicitlyClosed) {
|
|
2489
|
+
try {
|
|
2490
|
+
this.connect(this.options);
|
|
2491
|
+
} catch (e) {
|
|
2492
|
+
this.emit("error", e);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
}, backoffDelay);
|
|
2496
|
+
}
|
|
2497
|
+
cleanup() {
|
|
2498
|
+
this.stopKeepAlive();
|
|
2499
|
+
if (this.reconnectTimer) {
|
|
2500
|
+
clearTimeout(this.reconnectTimer);
|
|
2501
|
+
this.reconnectTimer = null;
|
|
2502
|
+
}
|
|
2503
|
+
if (this.ws) {
|
|
2504
|
+
try {
|
|
2505
|
+
this.ws.onopen = null;
|
|
2506
|
+
this.ws.onclose = null;
|
|
2507
|
+
this.ws.onerror = null;
|
|
2508
|
+
this.ws.onmessage = null;
|
|
2509
|
+
if (this.ws.readyState === 0 || this.ws.readyState === 1) {
|
|
2510
|
+
this.ws.close();
|
|
2511
|
+
}
|
|
2512
|
+
} catch {
|
|
2513
|
+
}
|
|
2514
|
+
this.ws = null;
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
};
|
|
2518
|
+
|
|
2519
|
+
// src/sessions.ts
|
|
2520
|
+
var SessionsModule = class {
|
|
2521
|
+
http;
|
|
2522
|
+
getUserId;
|
|
2523
|
+
constructor(http, getUserId) {
|
|
2524
|
+
this.http = http;
|
|
2525
|
+
this.getUserId = getUserId;
|
|
2526
|
+
}
|
|
2527
|
+
/**
|
|
2528
|
+
* Query active client sessions across the Jellyfin server via GET /Sessions.
|
|
2529
|
+
*/
|
|
2530
|
+
async getSessions(options = {}) {
|
|
2531
|
+
const params = {};
|
|
2532
|
+
if (options.controllableByUserId) {
|
|
2533
|
+
params.ControllableByUserId = options.controllableByUserId;
|
|
2534
|
+
}
|
|
2535
|
+
if (options.deviceId) {
|
|
2536
|
+
params.DeviceId = options.deviceId;
|
|
2537
|
+
}
|
|
2538
|
+
if (options.activeWithinSeconds !== void 0) {
|
|
2539
|
+
params.ActiveWithinSeconds = options.activeWithinSeconds;
|
|
2540
|
+
}
|
|
2541
|
+
const result = await this.http.request("/Sessions", { params });
|
|
2542
|
+
return Array.isArray(result) ? result : [];
|
|
2543
|
+
}
|
|
2544
|
+
/**
|
|
2545
|
+
* Instruct a remote session to start playback of specified items via POST /Sessions/{sessionId}/Playing.
|
|
2546
|
+
*
|
|
2547
|
+
* @param sessionId The target session ID.
|
|
2548
|
+
* @param itemIds Single item ID or array of item IDs to play.
|
|
2549
|
+
* @param options Additional playback parameters (command type, start ticks, etc.).
|
|
2550
|
+
*/
|
|
2551
|
+
async play(sessionId, itemIds, options = {}) {
|
|
2552
|
+
if (!sessionId) {
|
|
2553
|
+
throw new JellyfinError("sessionId is required to send a play command.");
|
|
2554
|
+
}
|
|
2555
|
+
const ids = Array.isArray(itemIds) ? itemIds : [itemIds];
|
|
2556
|
+
if (ids.length === 0) {
|
|
2557
|
+
throw new JellyfinError("At least one itemId is required to send a play command.");
|
|
2558
|
+
}
|
|
2559
|
+
const params = {
|
|
2560
|
+
ItemIds: ids.join(","),
|
|
2561
|
+
PlayCommand: options.playCommand || "PlayNow"
|
|
2562
|
+
};
|
|
2563
|
+
if (options.startPositionTicks !== void 0) {
|
|
2564
|
+
params.StartPositionTicks = options.startPositionTicks;
|
|
2565
|
+
}
|
|
2566
|
+
if (options.mediaSourceId) {
|
|
2567
|
+
params.MediaSourceId = options.mediaSourceId;
|
|
2568
|
+
}
|
|
2569
|
+
if (options.audioStreamIndex !== void 0) {
|
|
2570
|
+
params.AudioStreamIndex = options.audioStreamIndex;
|
|
2571
|
+
}
|
|
2572
|
+
if (options.subtitleStreamIndex !== void 0) {
|
|
2573
|
+
params.SubtitleStreamIndex = options.subtitleStreamIndex;
|
|
2574
|
+
}
|
|
2575
|
+
if (options.startIndex !== void 0) {
|
|
2576
|
+
params.StartIndex = options.startIndex;
|
|
2577
|
+
}
|
|
2578
|
+
await this.http.request(`/Sessions/${sessionId}/Playing`, {
|
|
2579
|
+
method: "POST",
|
|
2580
|
+
params
|
|
2581
|
+
});
|
|
2582
|
+
}
|
|
2583
|
+
/**
|
|
2584
|
+
* Send a playback control command to a remote session via POST /Sessions/{sessionId}/Playing/{command}.
|
|
2585
|
+
*/
|
|
2586
|
+
async sendPlaystateCommand(sessionId, command, params = {}) {
|
|
2587
|
+
if (!sessionId) {
|
|
2588
|
+
throw new JellyfinError("sessionId is required to send a playstate command.");
|
|
2589
|
+
}
|
|
2590
|
+
await this.http.request(`/Sessions/${sessionId}/Playing/${command}`, {
|
|
2591
|
+
method: "POST",
|
|
2592
|
+
params
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2595
|
+
/**
|
|
2596
|
+
* Toggle play / pause on a remote session.
|
|
2597
|
+
*/
|
|
2598
|
+
async playPause(sessionId) {
|
|
2599
|
+
return await this.sendPlaystateCommand(sessionId, "PlayPause");
|
|
2600
|
+
}
|
|
2601
|
+
/**
|
|
2602
|
+
* Pause playback on a remote session.
|
|
2603
|
+
*/
|
|
2604
|
+
async pause(sessionId) {
|
|
2605
|
+
return await this.sendPlaystateCommand(sessionId, "Pause");
|
|
2606
|
+
}
|
|
2607
|
+
/**
|
|
2608
|
+
* Resume playback on a remote session.
|
|
2609
|
+
*/
|
|
2610
|
+
async unpause(sessionId) {
|
|
2611
|
+
return await this.sendPlaystateCommand(sessionId, "Unpause");
|
|
2612
|
+
}
|
|
2613
|
+
/**
|
|
2614
|
+
* Stop playback on a remote session.
|
|
2615
|
+
*/
|
|
2616
|
+
async stop(sessionId) {
|
|
2617
|
+
return await this.sendPlaystateCommand(sessionId, "Stop");
|
|
2618
|
+
}
|
|
2619
|
+
/**
|
|
2620
|
+
* Seek to a specific tick position on a remote session.
|
|
2621
|
+
*
|
|
2622
|
+
* @param sessionId Target session ID.
|
|
2623
|
+
* @param positionTicks Position in ticks (1 tick = 10,000 milliseconds = 100 nanoseconds).
|
|
2624
|
+
*/
|
|
2625
|
+
async seek(sessionId, positionTicks) {
|
|
2626
|
+
return await this.sendPlaystateCommand(sessionId, "Seek", {
|
|
2627
|
+
SeekPositionTicks: positionTicks
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2630
|
+
/**
|
|
2631
|
+
* Skip to the next track on a remote session.
|
|
2632
|
+
*/
|
|
2633
|
+
async nextTrack(sessionId) {
|
|
2634
|
+
return await this.sendPlaystateCommand(sessionId, "NextTrack");
|
|
2635
|
+
}
|
|
2636
|
+
/**
|
|
2637
|
+
* Skip to the previous track on a remote session.
|
|
2638
|
+
*/
|
|
2639
|
+
async previousTrack(sessionId) {
|
|
2640
|
+
return await this.sendPlaystateCommand(sessionId, "PreviousTrack");
|
|
2641
|
+
}
|
|
2642
|
+
/**
|
|
2643
|
+
* Send a general command to a remote session via POST /Sessions/{sessionId}/Command.
|
|
2644
|
+
*/
|
|
2645
|
+
async sendGeneralCommand(sessionId, command) {
|
|
2646
|
+
if (!sessionId) {
|
|
2647
|
+
throw new JellyfinError("sessionId is required to send a general command.");
|
|
2648
|
+
}
|
|
2649
|
+
await this.http.request(`/Sessions/${sessionId}/Command`, {
|
|
2650
|
+
method: "POST",
|
|
2651
|
+
body: {
|
|
2652
|
+
Name: command.Name,
|
|
2653
|
+
Arguments: command.Arguments || {}
|
|
2654
|
+
}
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2657
|
+
/**
|
|
2658
|
+
* Set volume level on a remote session (0-100).
|
|
2659
|
+
*/
|
|
2660
|
+
async setVolume(sessionId, volume) {
|
|
2661
|
+
const clamped = Math.max(0, Math.min(100, Math.round(volume)));
|
|
2662
|
+
return await this.sendGeneralCommand(sessionId, {
|
|
2663
|
+
Name: "SetVolume",
|
|
2664
|
+
Arguments: { Volume: String(clamped) }
|
|
2665
|
+
});
|
|
2666
|
+
}
|
|
2667
|
+
/**
|
|
2668
|
+
* Mute audio on a remote session.
|
|
2669
|
+
*/
|
|
2670
|
+
async mute(sessionId) {
|
|
2671
|
+
return await this.sendGeneralCommand(sessionId, { Name: "Mute" });
|
|
2672
|
+
}
|
|
2673
|
+
/**
|
|
2674
|
+
* Unmute audio on a remote session.
|
|
2675
|
+
*/
|
|
2676
|
+
async unmute(sessionId) {
|
|
2677
|
+
return await this.sendGeneralCommand(sessionId, { Name: "Unmute" });
|
|
2678
|
+
}
|
|
2679
|
+
/**
|
|
2680
|
+
* Toggle mute on a remote session.
|
|
2681
|
+
*/
|
|
2682
|
+
async toggleMute(sessionId) {
|
|
2683
|
+
return await this.sendGeneralCommand(sessionId, { Name: "ToggleMute" });
|
|
2684
|
+
}
|
|
2685
|
+
/**
|
|
2686
|
+
* Switch the audio stream index on a remote session.
|
|
2687
|
+
*/
|
|
2688
|
+
async setAudioStreamIndex(sessionId, index) {
|
|
2689
|
+
return await this.sendGeneralCommand(sessionId, {
|
|
2690
|
+
Name: "SetAudioStreamIndex",
|
|
2691
|
+
Arguments: { Index: String(index) }
|
|
2692
|
+
});
|
|
2693
|
+
}
|
|
2694
|
+
/**
|
|
2695
|
+
* Switch the subtitle stream index on a remote session.
|
|
2696
|
+
*/
|
|
2697
|
+
async setSubtitleStreamIndex(sessionId, index) {
|
|
2698
|
+
return await this.sendGeneralCommand(sessionId, {
|
|
2699
|
+
Name: "SetSubtitleStreamIndex",
|
|
2700
|
+
Arguments: { Index: String(index) }
|
|
2701
|
+
});
|
|
2702
|
+
}
|
|
2703
|
+
/**
|
|
2704
|
+
* Display an on-screen dialog message modal on a remote session via POST /Sessions/{sessionId}/Message.
|
|
2705
|
+
*/
|
|
2706
|
+
async sendMessage(sessionId, options) {
|
|
2707
|
+
if (!sessionId) {
|
|
2708
|
+
throw new JellyfinError("sessionId is required to send a message.");
|
|
2709
|
+
}
|
|
2710
|
+
const payload = typeof options === "string" ? { Text: options, Header: "Message" } : {
|
|
2711
|
+
Text: options.text,
|
|
2712
|
+
Header: options.header || "Message",
|
|
2713
|
+
TimeoutMs: options.timeoutMs
|
|
2714
|
+
};
|
|
2715
|
+
await this.http.request(`/Sessions/${sessionId}/Message`, {
|
|
2716
|
+
method: "POST",
|
|
2717
|
+
body: payload
|
|
2718
|
+
});
|
|
2719
|
+
}
|
|
2720
|
+
/**
|
|
2721
|
+
* Instruct a remote session to navigate and view an item or page via POST /Sessions/{sessionId}/Viewing.
|
|
2722
|
+
*/
|
|
2723
|
+
async viewItem(sessionId, itemType, itemId, itemName) {
|
|
2724
|
+
if (!sessionId) {
|
|
2725
|
+
throw new JellyfinError("sessionId is required to instruct session navigation.");
|
|
2726
|
+
}
|
|
2727
|
+
await this.http.request(`/Sessions/${sessionId}/Viewing`, {
|
|
2728
|
+
method: "POST",
|
|
2729
|
+
params: {
|
|
2730
|
+
ItemType: itemType,
|
|
2731
|
+
ItemId: itemId,
|
|
2732
|
+
ItemName: itemName
|
|
2733
|
+
}
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
};
|
|
2737
|
+
|
|
2738
|
+
// src/quick-connect.ts
|
|
2739
|
+
var QuickConnectModule = class {
|
|
2740
|
+
http;
|
|
2741
|
+
constructor(http) {
|
|
2742
|
+
this.http = http;
|
|
2743
|
+
}
|
|
2744
|
+
/**
|
|
2745
|
+
* Check whether Quick Connect is enabled on the connected Jellyfin server.
|
|
2746
|
+
*/
|
|
2747
|
+
async isEnabled() {
|
|
2748
|
+
try {
|
|
2749
|
+
const result = await this.http.request("/QuickConnect/Enabled");
|
|
2750
|
+
if (typeof result === "boolean") {
|
|
2751
|
+
return result;
|
|
2752
|
+
}
|
|
2753
|
+
return String(result).toLowerCase() === "true";
|
|
2754
|
+
} catch {
|
|
2755
|
+
return false;
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
/**
|
|
2759
|
+
* Initiate a Quick Connect authentication session.
|
|
2760
|
+
* Typically called by a client device (e.g. Smart TV or CLI) to generate a user-facing pairing code.
|
|
2761
|
+
*/
|
|
2762
|
+
async initiate() {
|
|
2763
|
+
const result = await this.http.request("/QuickConnect/Initiate", {
|
|
2764
|
+
method: "POST"
|
|
2765
|
+
});
|
|
2766
|
+
if (!result || !result.Code || !result.Secret) {
|
|
2767
|
+
throw new JellyfinError("Quick Connect initiate failed: Invalid server response.");
|
|
2768
|
+
}
|
|
2769
|
+
return result;
|
|
2770
|
+
}
|
|
2771
|
+
/**
|
|
2772
|
+
* Check the current authorization status of an initiated Quick Connect secret.
|
|
2773
|
+
*
|
|
2774
|
+
* @param secret The secret token returned from initiate().
|
|
2775
|
+
*/
|
|
2776
|
+
async check(secret) {
|
|
2777
|
+
if (!secret) {
|
|
2778
|
+
throw new JellyfinError("secret is required to check Quick Connect status.");
|
|
2779
|
+
}
|
|
2780
|
+
return await this.http.request("/QuickConnect/Connect", {
|
|
2781
|
+
method: "GET",
|
|
2782
|
+
params: { secret }
|
|
2783
|
+
});
|
|
2784
|
+
}
|
|
2785
|
+
/**
|
|
2786
|
+
* Authorize a Quick Connect pairing code from an authenticated session.
|
|
2787
|
+
* Typically called on a mobile app or browser where the user is already logged in.
|
|
2788
|
+
*
|
|
2789
|
+
* @param code The pairing code shown on the target device.
|
|
2790
|
+
* @param userId Optional specific user ID to authenticate as (defaults to the currently authenticated user).
|
|
2791
|
+
*/
|
|
2792
|
+
async authorize(code, userId) {
|
|
2793
|
+
if (!code) {
|
|
2794
|
+
throw new JellyfinError("code is required to authorize Quick Connect.");
|
|
2795
|
+
}
|
|
2796
|
+
const params = {
|
|
2797
|
+
Code: code.replace(/\s+/g, "")
|
|
2798
|
+
// Normalize code by removing spaces
|
|
2799
|
+
};
|
|
2800
|
+
if (userId) {
|
|
2801
|
+
params.UserId = userId;
|
|
2802
|
+
}
|
|
2803
|
+
await this.http.request("/QuickConnect/Authorize", {
|
|
2804
|
+
method: "POST",
|
|
2805
|
+
params
|
|
2806
|
+
});
|
|
2807
|
+
return true;
|
|
2808
|
+
}
|
|
2809
|
+
/**
|
|
2810
|
+
* Repeatedly poll until the user authorizes the Quick Connect request or a timeout occurs.
|
|
2811
|
+
*
|
|
2812
|
+
* @param secret The secret token returned from initiate().
|
|
2813
|
+
* @param options Polling configuration (intervalMs, timeoutMs, signal).
|
|
2814
|
+
*/
|
|
2815
|
+
async poll(secret, options = {}) {
|
|
2816
|
+
const { intervalMs = 2e3, timeoutMs = 18e4, signal } = options;
|
|
2817
|
+
const startTime = Date.now();
|
|
2818
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2819
|
+
if (signal?.aborted) {
|
|
2820
|
+
throw new JellyfinError("Quick Connect polling was aborted.");
|
|
2821
|
+
}
|
|
2822
|
+
const state = await this.check(secret);
|
|
2823
|
+
if (state.Authenticated) {
|
|
2824
|
+
return state;
|
|
2825
|
+
}
|
|
2826
|
+
await new Promise((resolve, reject) => {
|
|
2827
|
+
const timer = setTimeout(() => resolve(), intervalMs);
|
|
2828
|
+
if (signal) {
|
|
2829
|
+
signal.addEventListener(
|
|
2830
|
+
"abort",
|
|
2831
|
+
() => {
|
|
2832
|
+
clearTimeout(timer);
|
|
2833
|
+
reject(new JellyfinError("Quick Connect polling was aborted."));
|
|
2834
|
+
},
|
|
2835
|
+
{ once: true }
|
|
2836
|
+
);
|
|
2837
|
+
}
|
|
2838
|
+
});
|
|
2839
|
+
}
|
|
2840
|
+
throw new JellyfinError(`Quick Connect timed out after ${timeoutMs}ms.`);
|
|
2841
|
+
}
|
|
2842
|
+
};
|
|
2843
|
+
|
|
2844
|
+
// src/trickplay.ts
|
|
2845
|
+
var TrickplayModule = class {
|
|
2846
|
+
http;
|
|
2847
|
+
constructor(http) {
|
|
2848
|
+
this.http = http;
|
|
2849
|
+
}
|
|
2850
|
+
/**
|
|
2851
|
+
* Generates the HLS playlist URL for trickplay scrubbing image tiles.
|
|
2852
|
+
* E.g. /Videos/{itemId}/Trickplay/{width}/tiles.m3u8
|
|
2853
|
+
*/
|
|
2854
|
+
getTilesHlsUrl(itemId, width, options = {}) {
|
|
2855
|
+
const serverUrl = this.http.getServerUrl();
|
|
2856
|
+
const token = this.http.getToken();
|
|
2857
|
+
const { mediaSourceId, useQueryToken = true } = options;
|
|
2858
|
+
const url = new URL(`${serverUrl}/Videos/${itemId}/Trickplay/${width}/tiles.m3u8`);
|
|
2859
|
+
if (token && useQueryToken) {
|
|
2860
|
+
url.searchParams.append("api_key", token);
|
|
2861
|
+
url.searchParams.append("X-Emby-Token", token);
|
|
2862
|
+
}
|
|
2863
|
+
if (mediaSourceId) {
|
|
2864
|
+
url.searchParams.append("MediaSourceId", mediaSourceId);
|
|
2865
|
+
}
|
|
2866
|
+
return url.toString();
|
|
2867
|
+
}
|
|
2868
|
+
/**
|
|
2869
|
+
* Generates the URL for a specific trickplay thumbnail tile image sheet.
|
|
2870
|
+
* E.g. /Videos/{itemId}/Trickplay/{width}/{index}.jpg
|
|
2871
|
+
*/
|
|
2872
|
+
getTileImageUrl(itemId, width, index, options = {}) {
|
|
2873
|
+
const serverUrl = this.http.getServerUrl();
|
|
2874
|
+
const token = this.http.getToken();
|
|
2875
|
+
const { mediaSourceId, useQueryToken = true } = options;
|
|
2876
|
+
const url = new URL(`${serverUrl}/Videos/${itemId}/Trickplay/${width}/${index}.jpg`);
|
|
2877
|
+
if (token && useQueryToken) {
|
|
2878
|
+
url.searchParams.append("api_key", token);
|
|
2879
|
+
url.searchParams.append("X-Emby-Token", token);
|
|
2880
|
+
}
|
|
2881
|
+
if (mediaSourceId) {
|
|
2882
|
+
url.searchParams.append("MediaSourceId", mediaSourceId);
|
|
2883
|
+
}
|
|
2884
|
+
return url.toString();
|
|
2885
|
+
}
|
|
2886
|
+
/**
|
|
2887
|
+
* Fetches and parses the HLS trickplay manifest (tiles.m3u8) for the given item and resolution.
|
|
2888
|
+
*/
|
|
2889
|
+
async getTrickplayManifest(itemId, width, options = {}) {
|
|
2890
|
+
const params = {};
|
|
2891
|
+
if (options.mediaSourceId) {
|
|
2892
|
+
params.MediaSourceId = options.mediaSourceId;
|
|
2893
|
+
}
|
|
2894
|
+
const endpoint = `/Videos/${itemId}/Trickplay/${width}/tiles.m3u8`;
|
|
2895
|
+
const rawM3u8 = await this.http.requestText(endpoint, { params });
|
|
2896
|
+
const tiles = this.parseTilesM3u8(itemId, width, rawM3u8, options.mediaSourceId, options.useQueryToken);
|
|
2897
|
+
return {
|
|
2898
|
+
width,
|
|
2899
|
+
rawM3u8,
|
|
2900
|
+
tiles
|
|
2901
|
+
};
|
|
2902
|
+
}
|
|
2903
|
+
/**
|
|
2904
|
+
* Helper to parse #EXTINF and tile file names from a tiles.m3u8 playlist.
|
|
2905
|
+
*/
|
|
2906
|
+
parseTilesM3u8(itemId, width, rawM3u8, mediaSourceId, useQueryToken = true) {
|
|
2907
|
+
const lines = rawM3u8.split(/\r?\n/);
|
|
2908
|
+
const tiles = [];
|
|
2909
|
+
let currentDurationMs = 0;
|
|
2910
|
+
for (const line of lines) {
|
|
2911
|
+
const trimmed = line.trim();
|
|
2912
|
+
if (!trimmed) continue;
|
|
2913
|
+
if (trimmed.startsWith("#EXTINF:")) {
|
|
2914
|
+
const secondsStr = trimmed.substring(8).split(",")[0];
|
|
2915
|
+
const seconds = parseFloat(secondsStr);
|
|
2916
|
+
currentDurationMs = !isNaN(seconds) ? Math.round(seconds * 1e3) : 0;
|
|
2917
|
+
} else if (!trimmed.startsWith("#")) {
|
|
2918
|
+
const indexMatch = trimmed.match(/^(\d+)\.(jpg|jpeg|png|webp)/i);
|
|
2919
|
+
const index = indexMatch ? parseInt(indexMatch[1], 10) : tiles.length;
|
|
2920
|
+
const url = this.getTileImageUrl(itemId, width, index, { mediaSourceId, useQueryToken });
|
|
2921
|
+
tiles.push({
|
|
2922
|
+
index,
|
|
2923
|
+
durationMs: currentDurationMs,
|
|
2924
|
+
url
|
|
2925
|
+
});
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
return tiles;
|
|
2929
|
+
}
|
|
2930
|
+
/**
|
|
2931
|
+
* Extracts the available trickplay thumbnail widths from a BaseItemDto's Trickplay metadata.
|
|
2932
|
+
*/
|
|
2933
|
+
getAvailableResolutions(item, mediaSourceId) {
|
|
2934
|
+
if (!item.Trickplay) {
|
|
2935
|
+
return [];
|
|
2936
|
+
}
|
|
2937
|
+
const targetMediaSourceId = mediaSourceId || item.MediaSources && item.MediaSources[0]?.Id || Object.keys(item.Trickplay)[0];
|
|
2938
|
+
if (!targetMediaSourceId || !item.Trickplay[targetMediaSourceId]) {
|
|
2939
|
+
return [];
|
|
2940
|
+
}
|
|
2941
|
+
const sourceTrickplay = item.Trickplay[targetMediaSourceId];
|
|
2942
|
+
return Object.keys(sourceTrickplay).map((w) => parseInt(w, 10)).filter((w) => !isNaN(w)).sort((a, b) => a - b);
|
|
2943
|
+
}
|
|
2944
|
+
/**
|
|
2945
|
+
* Retrieves the TrickplayInfoDto metadata for a specified item and optional width.
|
|
2946
|
+
*/
|
|
2947
|
+
getTrickplayInfo(item, width, mediaSourceId) {
|
|
2948
|
+
if (!item.Trickplay) {
|
|
2949
|
+
return null;
|
|
2950
|
+
}
|
|
2951
|
+
const targetMediaSourceId = mediaSourceId || item.MediaSources && item.MediaSources[0]?.Id || Object.keys(item.Trickplay)[0];
|
|
2952
|
+
if (!targetMediaSourceId || !item.Trickplay[targetMediaSourceId]) {
|
|
2953
|
+
return null;
|
|
2954
|
+
}
|
|
2955
|
+
const sourceTrickplay = item.Trickplay[targetMediaSourceId];
|
|
2956
|
+
if (width !== void 0 && sourceTrickplay[String(width)]) {
|
|
2957
|
+
return sourceTrickplay[String(width)];
|
|
2958
|
+
}
|
|
2959
|
+
const availableWidths = Object.keys(sourceTrickplay);
|
|
2960
|
+
if (availableWidths.length === 0) {
|
|
2961
|
+
return null;
|
|
2962
|
+
}
|
|
2963
|
+
return sourceTrickplay[availableWidths[0]];
|
|
2964
|
+
}
|
|
2965
|
+
/**
|
|
2966
|
+
* Calculates the exact tile sheet index, image URL, and sprite coordinates (x, y, width, height)
|
|
2967
|
+
* for a given playback timestamp to render scrubbing thumbnails in a player or seekbar UI.
|
|
2968
|
+
*/
|
|
2969
|
+
getThumbnailForTimestamp(options) {
|
|
2970
|
+
const {
|
|
2971
|
+
item,
|
|
2972
|
+
timestampMs,
|
|
2973
|
+
timestampTicks,
|
|
2974
|
+
width,
|
|
2975
|
+
mediaSourceId,
|
|
2976
|
+
useQueryToken = true
|
|
2977
|
+
} = options;
|
|
2978
|
+
let info = options.trickplayInfo;
|
|
2979
|
+
if (!info && item) {
|
|
2980
|
+
info = this.getTrickplayInfo(item, width, mediaSourceId) || void 0;
|
|
2981
|
+
}
|
|
2982
|
+
if (!info) {
|
|
2983
|
+
return null;
|
|
2984
|
+
}
|
|
2985
|
+
const timeMs = timestampMs !== void 0 ? timestampMs : timestampTicks !== void 0 ? Math.round(timestampTicks / 1e4) : 0;
|
|
2986
|
+
const interval = info.Interval > 0 ? info.Interval : 1e4;
|
|
2987
|
+
let thumbIndex = Math.floor(Math.max(0, timeMs) / interval);
|
|
2988
|
+
if (info.ThumbnailCount > 0 && thumbIndex >= info.ThumbnailCount) {
|
|
2989
|
+
thumbIndex = info.ThumbnailCount - 1;
|
|
2990
|
+
}
|
|
2991
|
+
const tileCols = info.TileWidth > 0 ? info.TileWidth : 1;
|
|
2992
|
+
const tileRows = info.TileHeight > 0 ? info.TileHeight : 1;
|
|
2993
|
+
const thumbsPerSheet = tileCols * tileRows;
|
|
2994
|
+
const sheetIndex = Math.floor(thumbIndex / thumbsPerSheet);
|
|
2995
|
+
const indexInSheet = thumbIndex % thumbsPerSheet;
|
|
2996
|
+
const col = indexInSheet % tileCols;
|
|
2997
|
+
const row = Math.floor(indexInSheet / tileCols);
|
|
2998
|
+
const x = col * info.Width;
|
|
2999
|
+
const y = row * info.Height;
|
|
3000
|
+
const itemId = item?.Id || "";
|
|
3001
|
+
if (!itemId) {
|
|
3002
|
+
throw new JellyfinError("Item or itemId is required to generate the thumbnail URL.");
|
|
3003
|
+
}
|
|
3004
|
+
const imageUrl = this.getTileImageUrl(itemId, info.Width, sheetIndex, {
|
|
3005
|
+
mediaSourceId,
|
|
3006
|
+
useQueryToken
|
|
3007
|
+
});
|
|
3008
|
+
return {
|
|
3009
|
+
tileIndex: sheetIndex,
|
|
3010
|
+
imageUrl,
|
|
3011
|
+
x,
|
|
3012
|
+
y,
|
|
3013
|
+
width: info.Width,
|
|
3014
|
+
height: info.Height,
|
|
3015
|
+
thumbnailIndex: thumbIndex
|
|
3016
|
+
};
|
|
3017
|
+
}
|
|
3018
|
+
};
|
|
3019
|
+
|
|
3020
|
+
// src/chapters.ts
|
|
3021
|
+
var ChaptersModule = class {
|
|
3022
|
+
http;
|
|
3023
|
+
getUserId;
|
|
3024
|
+
constructor(http, getUserId) {
|
|
3025
|
+
this.http = http;
|
|
3026
|
+
this.getUserId = getUserId;
|
|
3027
|
+
}
|
|
3028
|
+
/**
|
|
3029
|
+
* Synchronously extracts chapter markers from an already fetched BaseItemDto.
|
|
3030
|
+
*/
|
|
3031
|
+
extractChapters(item) {
|
|
3032
|
+
return item.Chapters || [];
|
|
3033
|
+
}
|
|
3034
|
+
/**
|
|
3035
|
+
* Fetches the chapter markers for an item from the server.
|
|
3036
|
+
*/
|
|
3037
|
+
async getChapters(itemId) {
|
|
3038
|
+
const userId = this.getUserId ? this.getUserId() : "";
|
|
3039
|
+
const endpoint = userId ? `/Users/${userId}/Items/${itemId}` : `/Items/${itemId}`;
|
|
3040
|
+
const item = await this.http.request(endpoint, {
|
|
3041
|
+
params: {
|
|
3042
|
+
Fields: "Chapters"
|
|
3043
|
+
}
|
|
3044
|
+
});
|
|
3045
|
+
return item.Chapters || [];
|
|
3046
|
+
}
|
|
3047
|
+
/**
|
|
3048
|
+
* Generates the URL for a chapter preview thumbnail image.
|
|
3049
|
+
* E.g. /Items/{itemId}/Images/Chapter/{chapterIndex}
|
|
3050
|
+
*/
|
|
3051
|
+
getChapterImageUrl(itemId, chapterIndex, options = {}) {
|
|
3052
|
+
const serverUrl = this.http.getServerUrl();
|
|
3053
|
+
const token = this.http.getToken();
|
|
3054
|
+
const {
|
|
3055
|
+
maxWidth,
|
|
3056
|
+
maxHeight,
|
|
3057
|
+
width,
|
|
3058
|
+
height,
|
|
3059
|
+
quality,
|
|
3060
|
+
format,
|
|
3061
|
+
tag,
|
|
3062
|
+
fillWidth,
|
|
3063
|
+
fillHeight,
|
|
3064
|
+
useQueryToken = true
|
|
3065
|
+
} = options;
|
|
3066
|
+
const url = new URL(`${serverUrl}/Items/${itemId}/Images/Chapter/${chapterIndex}`);
|
|
3067
|
+
if (token && useQueryToken) {
|
|
3068
|
+
url.searchParams.append("api_key", token);
|
|
3069
|
+
url.searchParams.append("X-Emby-Token", token);
|
|
3070
|
+
}
|
|
3071
|
+
if (tag) url.searchParams.append("tag", tag);
|
|
3072
|
+
if (maxWidth) url.searchParams.append("maxWidth", String(maxWidth));
|
|
3073
|
+
if (maxHeight) url.searchParams.append("maxHeight", String(maxHeight));
|
|
3074
|
+
if (width) url.searchParams.append("width", String(width));
|
|
3075
|
+
if (height) url.searchParams.append("height", String(height));
|
|
3076
|
+
if (quality) url.searchParams.append("quality", String(quality));
|
|
3077
|
+
if (format) url.searchParams.append("format", format);
|
|
3078
|
+
if (fillWidth) url.searchParams.append("fillWidth", String(fillWidth));
|
|
3079
|
+
if (fillHeight) url.searchParams.append("fillHeight", String(fillHeight));
|
|
3080
|
+
return url.toString();
|
|
3081
|
+
}
|
|
3082
|
+
/**
|
|
3083
|
+
* Retrieves media segments (Intro, Outro, Recap, Commercials) for an item via Jellyfin 10.10+ /MediaSegments.
|
|
3084
|
+
*/
|
|
3085
|
+
async getMediaSegments(itemId, options = {}) {
|
|
3086
|
+
const params = {
|
|
3087
|
+
itemId
|
|
3088
|
+
};
|
|
3089
|
+
if (options.includeSegmentTypes && options.includeSegmentTypes.length > 0) {
|
|
3090
|
+
params.includeSegmentTypes = options.includeSegmentTypes.join(",");
|
|
3091
|
+
}
|
|
3092
|
+
try {
|
|
3093
|
+
const res = await this.http.request(
|
|
3094
|
+
"/MediaSegments",
|
|
3095
|
+
{ params }
|
|
3096
|
+
);
|
|
3097
|
+
if (Array.isArray(res)) {
|
|
3098
|
+
return res;
|
|
3099
|
+
}
|
|
3100
|
+
if (res && Array.isArray(res.Items)) {
|
|
3101
|
+
return res.Items;
|
|
3102
|
+
}
|
|
3103
|
+
return [];
|
|
3104
|
+
} catch {
|
|
3105
|
+
return [];
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
/**
|
|
3109
|
+
* Resolves intro and end credits markers for an item, combining /MediaSegments API
|
|
3110
|
+
* and chapter marker heuristics (MarkerType or naming like 'Intro', 'Opening', 'Credits', 'Ending').
|
|
3111
|
+
*/
|
|
3112
|
+
async getIntroCredits(itemOrChaptersOrId, mediaSegments) {
|
|
3113
|
+
let itemId = "";
|
|
3114
|
+
let chapters = [];
|
|
3115
|
+
let segments = mediaSegments || [];
|
|
3116
|
+
if (typeof itemOrChaptersOrId === "string") {
|
|
3117
|
+
itemId = itemOrChaptersOrId;
|
|
3118
|
+
if (segments.length === 0) {
|
|
3119
|
+
segments = await this.getMediaSegments(itemId);
|
|
3120
|
+
}
|
|
3121
|
+
if (segments.length === 0) {
|
|
3122
|
+
chapters = await this.getChapters(itemId);
|
|
3123
|
+
}
|
|
3124
|
+
} else if (Array.isArray(itemOrChaptersOrId)) {
|
|
3125
|
+
chapters = itemOrChaptersOrId;
|
|
3126
|
+
} else if (typeof itemOrChaptersOrId === "object" && itemOrChaptersOrId !== null) {
|
|
3127
|
+
itemId = itemOrChaptersOrId.Id;
|
|
3128
|
+
chapters = itemOrChaptersOrId.Chapters || [];
|
|
3129
|
+
if (segments.length === 0 && itemId) {
|
|
3130
|
+
segments = await this.getMediaSegments(itemId);
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
const markers = {
|
|
3134
|
+
segments
|
|
3135
|
+
};
|
|
3136
|
+
for (const seg of segments) {
|
|
3137
|
+
const type = String(seg.Type).toLowerCase();
|
|
3138
|
+
if (type === "intro" && !markers.intro) {
|
|
3139
|
+
markers.intro = {
|
|
3140
|
+
startTicks: seg.StartTicks,
|
|
3141
|
+
endTicks: seg.EndTicks,
|
|
3142
|
+
startMs: Math.round(seg.StartTicks / 1e4),
|
|
3143
|
+
endMs: Math.round(seg.EndTicks / 1e4)
|
|
3144
|
+
};
|
|
3145
|
+
} else if ((type === "outro" || type === "credits") && !markers.credits) {
|
|
3146
|
+
markers.credits = {
|
|
3147
|
+
startTicks: seg.StartTicks,
|
|
3148
|
+
endTicks: seg.EndTicks,
|
|
3149
|
+
startMs: Math.round(seg.StartTicks / 1e4),
|
|
3150
|
+
endMs: Math.round(seg.EndTicks / 1e4)
|
|
3151
|
+
};
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
if ((!markers.intro || !markers.credits) && chapters.length > 0) {
|
|
3155
|
+
for (let i = 0; i < chapters.length; i++) {
|
|
3156
|
+
const chapter = chapters[i];
|
|
3157
|
+
const nextChapter = chapters[i + 1];
|
|
3158
|
+
const name = (chapter.Name || "").trim();
|
|
3159
|
+
const markerType = (chapter.MarkerType || "").toLowerCase();
|
|
3160
|
+
if (!markers.intro) {
|
|
3161
|
+
if (markerType === "introstart" || /^(intro|opening|theme\s*song)/i.test(name)) {
|
|
3162
|
+
const startTicks = chapter.StartPositionTicks;
|
|
3163
|
+
const endTicks = nextChapter ? nextChapter.StartPositionTicks : startTicks + 90 * 1e7;
|
|
3164
|
+
markers.intro = {
|
|
3165
|
+
startTicks,
|
|
3166
|
+
endTicks,
|
|
3167
|
+
startMs: Math.round(startTicks / 1e4),
|
|
3168
|
+
endMs: Math.round(endTicks / 1e4)
|
|
3169
|
+
};
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
if (!markers.credits) {
|
|
3173
|
+
if (markerType === "creditsstart" || /^(credits|outro|ending|end\s*credits)/i.test(name)) {
|
|
3174
|
+
const startTicks = chapter.StartPositionTicks;
|
|
3175
|
+
const endTicks = nextChapter ? nextChapter.StartPositionTicks : startTicks + 180 * 1e7;
|
|
3176
|
+
markers.credits = {
|
|
3177
|
+
startTicks,
|
|
3178
|
+
endTicks,
|
|
3179
|
+
startMs: Math.round(startTicks / 1e4),
|
|
3180
|
+
endMs: Math.round(endTicks / 1e4)
|
|
3181
|
+
};
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
return markers;
|
|
3187
|
+
}
|
|
3188
|
+
/**
|
|
3189
|
+
* Checks whether the current playback position falls within an intro or credits range.
|
|
3190
|
+
*/
|
|
3191
|
+
findCurrentSegment(positionTicks, markersOrSegments) {
|
|
3192
|
+
if (Array.isArray(markersOrSegments)) {
|
|
3193
|
+
for (const seg of markersOrSegments) {
|
|
3194
|
+
if (positionTicks >= seg.StartTicks && positionTicks < seg.EndTicks) {
|
|
3195
|
+
return {
|
|
3196
|
+
type: seg.Type,
|
|
3197
|
+
startTicks: seg.StartTicks,
|
|
3198
|
+
endTicks: seg.EndTicks
|
|
3199
|
+
};
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
return null;
|
|
3203
|
+
}
|
|
3204
|
+
const { intro, credits } = markersOrSegments;
|
|
3205
|
+
if (intro && positionTicks >= intro.startTicks && positionTicks < intro.endTicks) {
|
|
3206
|
+
return {
|
|
3207
|
+
type: "Intro",
|
|
3208
|
+
startTicks: intro.startTicks,
|
|
3209
|
+
endTicks: intro.endTicks
|
|
3210
|
+
};
|
|
3211
|
+
}
|
|
3212
|
+
if (credits && positionTicks >= credits.startTicks && positionTicks < credits.endTicks) {
|
|
3213
|
+
return {
|
|
3214
|
+
type: "Credits",
|
|
3215
|
+
startTicks: credits.startTicks,
|
|
3216
|
+
endTicks: credits.endTicks
|
|
3217
|
+
};
|
|
3218
|
+
}
|
|
3219
|
+
return null;
|
|
3220
|
+
}
|
|
3221
|
+
/**
|
|
3222
|
+
* Returns the timestamp ticks to skip to if the current position is within a skippable segment,
|
|
3223
|
+
* or null if not currently within any skippable range.
|
|
3224
|
+
*/
|
|
3225
|
+
getSkipPosition(positionTicks, markersOrSegments) {
|
|
3226
|
+
const current = this.findCurrentSegment(positionTicks, markersOrSegments);
|
|
3227
|
+
return current ? current.endTicks : null;
|
|
3228
|
+
}
|
|
3229
|
+
};
|
|
3230
|
+
|
|
3231
|
+
// src/genres.ts
|
|
3232
|
+
var GenresModule = class {
|
|
3233
|
+
http;
|
|
3234
|
+
getUserId;
|
|
3235
|
+
constructor(http, getUserId) {
|
|
3236
|
+
this.http = http;
|
|
3237
|
+
this.getUserId = getUserId;
|
|
3238
|
+
}
|
|
3239
|
+
resolveUserId(explicitUserId) {
|
|
3240
|
+
return explicitUserId || this.getUserId() || "";
|
|
3241
|
+
}
|
|
3242
|
+
/**
|
|
3243
|
+
* Retrieves video/movie genres from the server via GET /Genres.
|
|
3244
|
+
*/
|
|
3245
|
+
async getGenres(options = {}) {
|
|
3246
|
+
const userId = this.resolveUserId(options.userId);
|
|
3247
|
+
const params = {};
|
|
3248
|
+
if (userId) params.UserId = userId;
|
|
3249
|
+
if (options.parentId) params.ParentId = options.parentId;
|
|
3250
|
+
if (options.startIndex !== void 0) params.StartIndex = options.startIndex;
|
|
3251
|
+
if (options.limit !== void 0) params.Limit = options.limit;
|
|
3252
|
+
if (options.searchTerm) params.SearchTerm = options.searchTerm;
|
|
3253
|
+
if (options.sortBy) params.SortBy = options.sortBy;
|
|
3254
|
+
if (options.sortOrder) params.SortOrder = options.sortOrder;
|
|
3255
|
+
if (options.enableImages !== void 0) params.EnableImages = options.enableImages;
|
|
3256
|
+
if (options.imageTypeLimit !== void 0) params.ImageTypeLimit = options.imageTypeLimit;
|
|
3257
|
+
if (options.enableImageTypes && options.enableImageTypes.length > 0) {
|
|
3258
|
+
params.EnableImageTypes = options.enableImageTypes.join(",");
|
|
3259
|
+
}
|
|
3260
|
+
return await this.http.request("/Genres", { params });
|
|
3261
|
+
}
|
|
3262
|
+
/**
|
|
3263
|
+
* Retrieves a single genre by name via GET /Genres/{genreName}.
|
|
3264
|
+
*/
|
|
3265
|
+
async getGenre(genreName, options = {}) {
|
|
3266
|
+
const userId = this.resolveUserId(options.userId);
|
|
3267
|
+
const params = {};
|
|
3268
|
+
if (userId) params.UserId = userId;
|
|
3269
|
+
return await this.http.request(`/Genres/${encodeURIComponent(genreName)}`, {
|
|
3270
|
+
params
|
|
3271
|
+
});
|
|
3272
|
+
}
|
|
3273
|
+
/**
|
|
3274
|
+
* Retrieves music genres from the server via GET /MusicGenres.
|
|
3275
|
+
*/
|
|
3276
|
+
async getMusicGenres(options = {}) {
|
|
3277
|
+
const userId = this.resolveUserId(options.userId);
|
|
3278
|
+
const params = {};
|
|
3279
|
+
if (userId) params.UserId = userId;
|
|
3280
|
+
if (options.parentId) params.ParentId = options.parentId;
|
|
3281
|
+
if (options.startIndex !== void 0) params.StartIndex = options.startIndex;
|
|
3282
|
+
if (options.limit !== void 0) params.Limit = options.limit;
|
|
3283
|
+
if (options.searchTerm) params.SearchTerm = options.searchTerm;
|
|
3284
|
+
if (options.sortBy) params.SortBy = options.sortBy;
|
|
3285
|
+
if (options.sortOrder) params.SortOrder = options.sortOrder;
|
|
3286
|
+
if (options.enableImages !== void 0) params.EnableImages = options.enableImages;
|
|
3287
|
+
if (options.imageTypeLimit !== void 0) params.ImageTypeLimit = options.imageTypeLimit;
|
|
3288
|
+
if (options.enableImageTypes && options.enableImageTypes.length > 0) {
|
|
3289
|
+
params.EnableImageTypes = options.enableImageTypes.join(",");
|
|
3290
|
+
}
|
|
3291
|
+
return await this.http.request("/MusicGenres", { params });
|
|
3292
|
+
}
|
|
3293
|
+
/**
|
|
3294
|
+
* Retrieves a single music genre by name via GET /MusicGenres/{genreName}.
|
|
3295
|
+
*/
|
|
3296
|
+
async getMusicGenre(genreName, options = {}) {
|
|
3297
|
+
const userId = this.resolveUserId(options.userId);
|
|
3298
|
+
const params = {};
|
|
3299
|
+
if (userId) params.UserId = userId;
|
|
3300
|
+
return await this.http.request(`/MusicGenres/${encodeURIComponent(genreName)}`, {
|
|
3301
|
+
params
|
|
3302
|
+
});
|
|
3303
|
+
}
|
|
3304
|
+
/**
|
|
3305
|
+
* Retrieves studios/production companies from the server via GET /Studios.
|
|
3306
|
+
*/
|
|
3307
|
+
async getStudios(options = {}) {
|
|
3308
|
+
const userId = this.resolveUserId(options.userId);
|
|
3309
|
+
const params = {};
|
|
3310
|
+
if (userId) params.UserId = userId;
|
|
3311
|
+
if (options.parentId) params.ParentId = options.parentId;
|
|
3312
|
+
if (options.startIndex !== void 0) params.StartIndex = options.startIndex;
|
|
3313
|
+
if (options.limit !== void 0) params.Limit = options.limit;
|
|
3314
|
+
if (options.searchTerm) params.SearchTerm = options.searchTerm;
|
|
3315
|
+
if (options.sortBy) params.SortBy = options.sortBy;
|
|
3316
|
+
if (options.sortOrder) params.SortOrder = options.sortOrder;
|
|
3317
|
+
if (options.enableImages !== void 0) params.EnableImages = options.enableImages;
|
|
3318
|
+
if (options.imageTypeLimit !== void 0) params.ImageTypeLimit = options.imageTypeLimit;
|
|
3319
|
+
if (options.enableImageTypes && options.enableImageTypes.length > 0) {
|
|
3320
|
+
params.EnableImageTypes = options.enableImageTypes.join(",");
|
|
3321
|
+
}
|
|
3322
|
+
return await this.http.request("/Studios", { params });
|
|
3323
|
+
}
|
|
3324
|
+
/**
|
|
3325
|
+
* Retrieves a single studio by name via GET /Studios/{studioName}.
|
|
3326
|
+
*/
|
|
3327
|
+
async getStudio(studioName, options = {}) {
|
|
3328
|
+
const userId = this.resolveUserId(options.userId);
|
|
3329
|
+
const params = {};
|
|
3330
|
+
if (userId) params.UserId = userId;
|
|
3331
|
+
return await this.http.request(`/Studios/${encodeURIComponent(studioName)}`, {
|
|
3332
|
+
params
|
|
3333
|
+
});
|
|
3334
|
+
}
|
|
3335
|
+
};
|
|
3336
|
+
|
|
3337
|
+
// src/collections.ts
|
|
3338
|
+
var CollectionsModule = class {
|
|
3339
|
+
http;
|
|
3340
|
+
getUserId;
|
|
3341
|
+
constructor(http, getUserId) {
|
|
3342
|
+
this.http = http;
|
|
3343
|
+
this.getUserId = getUserId;
|
|
3344
|
+
}
|
|
3345
|
+
resolveUserId(explicitUserId) {
|
|
3346
|
+
return explicitUserId || this.getUserId() || "";
|
|
3347
|
+
}
|
|
3348
|
+
/**
|
|
3349
|
+
* Retrieves user BoxSet / Collections via GET /Collections.
|
|
3350
|
+
*/
|
|
3351
|
+
async getCollections(options = {}) {
|
|
3352
|
+
const userId = this.resolveUserId(options.userId);
|
|
3353
|
+
const params = {};
|
|
3354
|
+
if (userId) params.UserId = userId;
|
|
3355
|
+
if (options.parentId) params.ParentId = options.parentId;
|
|
3356
|
+
if (options.startIndex !== void 0) params.StartIndex = options.startIndex;
|
|
3357
|
+
if (options.limit !== void 0) params.Limit = options.limit;
|
|
3358
|
+
if (options.searchTerm) params.SearchTerm = options.searchTerm;
|
|
3359
|
+
if (options.sortBy) params.SortBy = options.sortBy;
|
|
3360
|
+
if (options.sortOrder) params.SortOrder = options.sortOrder;
|
|
3361
|
+
if (options.enableImages !== void 0) params.EnableImages = options.enableImages;
|
|
3362
|
+
if (options.imageTypeLimit !== void 0) params.ImageTypeLimit = options.imageTypeLimit;
|
|
3363
|
+
if (options.enableImageTypes && options.enableImageTypes.length > 0) {
|
|
3364
|
+
params.EnableImageTypes = options.enableImageTypes.join(",");
|
|
3365
|
+
}
|
|
3366
|
+
return await this.http.request("/Collections", { params });
|
|
3367
|
+
}
|
|
3368
|
+
/**
|
|
3369
|
+
* Retrieves the items belonging to a collection / BoxSet.
|
|
3370
|
+
*/
|
|
3371
|
+
async getCollectionItems(collectionId, options = {}) {
|
|
3372
|
+
const userId = this.resolveUserId(options.userId);
|
|
3373
|
+
const endpoint = userId ? `/Users/${userId}/Items` : "/Items";
|
|
3374
|
+
const params = {
|
|
3375
|
+
ParentId: collectionId
|
|
3376
|
+
};
|
|
3377
|
+
if (options.startIndex !== void 0) params.StartIndex = options.startIndex;
|
|
3378
|
+
if (options.limit !== void 0) params.Limit = options.limit;
|
|
3379
|
+
if (options.fields) params.Fields = options.fields;
|
|
3380
|
+
if (options.sortBy) params.SortBy = options.sortBy;
|
|
3381
|
+
if (options.sortOrder) params.SortOrder = options.sortOrder;
|
|
3382
|
+
return await this.http.request(endpoint, { params });
|
|
3383
|
+
}
|
|
3384
|
+
/**
|
|
3385
|
+
* Creates a new BoxSet / Collection via POST /Collections.
|
|
3386
|
+
*/
|
|
3387
|
+
async createCollection(options) {
|
|
3388
|
+
const params = {
|
|
3389
|
+
Name: options.name
|
|
3390
|
+
};
|
|
3391
|
+
if (options.ids && options.ids.length > 0) {
|
|
3392
|
+
params.Ids = options.ids.join(",");
|
|
3393
|
+
}
|
|
3394
|
+
if (options.parentId) {
|
|
3395
|
+
params.ParentId = options.parentId;
|
|
3396
|
+
}
|
|
3397
|
+
if (options.isFolder !== void 0) {
|
|
3398
|
+
params.IsFolder = options.isFolder;
|
|
3399
|
+
}
|
|
3400
|
+
return await this.http.request("/Collections", {
|
|
3401
|
+
method: "POST",
|
|
3402
|
+
params
|
|
3403
|
+
});
|
|
3404
|
+
}
|
|
3405
|
+
/**
|
|
3406
|
+
* Adds items to an existing BoxSet / Collection via POST /Collections/{collectionId}/Items.
|
|
3407
|
+
*/
|
|
3408
|
+
async addToCollection(collectionId, itemIds) {
|
|
3409
|
+
const ids = Array.isArray(itemIds) ? itemIds.join(",") : itemIds;
|
|
3410
|
+
await this.http.request(`/Collections/${collectionId}/Items`, {
|
|
3411
|
+
method: "POST",
|
|
3412
|
+
params: {
|
|
3413
|
+
Ids: ids
|
|
3414
|
+
}
|
|
3415
|
+
});
|
|
3416
|
+
}
|
|
3417
|
+
/**
|
|
3418
|
+
* Removes items from an existing BoxSet / Collection via DELETE /Collections/{collectionId}/Items.
|
|
3419
|
+
*/
|
|
3420
|
+
async removeFromCollection(collectionId, itemIds) {
|
|
3421
|
+
const ids = Array.isArray(itemIds) ? itemIds.join(",") : itemIds;
|
|
3422
|
+
await this.http.request(`/Collections/${collectionId}/Items`, {
|
|
3423
|
+
method: "DELETE",
|
|
3424
|
+
params: {
|
|
3425
|
+
Ids: ids
|
|
3426
|
+
}
|
|
3427
|
+
});
|
|
3428
|
+
}
|
|
3429
|
+
};
|
|
3430
|
+
|
|
3431
|
+
// src/display-preferences.ts
|
|
3432
|
+
var DisplayPreferencesModule = class {
|
|
3433
|
+
http;
|
|
3434
|
+
getUserId;
|
|
3435
|
+
defaultClient;
|
|
3436
|
+
constructor(http, getUserId, defaultClient = "emby") {
|
|
3437
|
+
this.http = http;
|
|
3438
|
+
this.getUserId = getUserId;
|
|
3439
|
+
this.defaultClient = defaultClient;
|
|
3440
|
+
}
|
|
3441
|
+
resolveUserId(explicitUserId) {
|
|
3442
|
+
const userId = explicitUserId || this.getUserId();
|
|
3443
|
+
if (!userId) {
|
|
3444
|
+
throw new JellyfinError(
|
|
3445
|
+
"User ID is required for DisplayPreferences. Please authenticate or specify userId."
|
|
3446
|
+
);
|
|
3447
|
+
}
|
|
3448
|
+
return userId;
|
|
3449
|
+
}
|
|
3450
|
+
/**
|
|
3451
|
+
* Retrieves the user's display preferences for a specific folder or view.
|
|
3452
|
+
* GET /DisplayPreferences/{displayPreferencesId}
|
|
3453
|
+
*/
|
|
3454
|
+
async getDisplayPreferences(displayPreferencesId, options = {}) {
|
|
3455
|
+
const userId = this.resolveUserId(options.userId);
|
|
3456
|
+
const client = options.client || this.defaultClient;
|
|
3457
|
+
return await this.http.request(
|
|
3458
|
+
`/DisplayPreferences/${displayPreferencesId}`,
|
|
3459
|
+
{
|
|
3460
|
+
params: {
|
|
3461
|
+
userId,
|
|
3462
|
+
client
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
);
|
|
3466
|
+
}
|
|
3467
|
+
/**
|
|
3468
|
+
* Updates display preferences for a specific folder or view.
|
|
3469
|
+
* POST /DisplayPreferences/{displayPreferencesId}
|
|
3470
|
+
*/
|
|
3471
|
+
async updateDisplayPreferences(displayPreferencesId, preferences, options = {}) {
|
|
3472
|
+
const userId = this.resolveUserId(options.userId);
|
|
3473
|
+
const client = options.client || this.defaultClient;
|
|
3474
|
+
const payload = {
|
|
3475
|
+
...preferences,
|
|
3476
|
+
Id: preferences.Id || displayPreferencesId,
|
|
3477
|
+
Client: preferences.Client || client
|
|
3478
|
+
};
|
|
3479
|
+
await this.http.request(`/DisplayPreferences/${displayPreferencesId}`, {
|
|
3480
|
+
method: "POST",
|
|
3481
|
+
params: {
|
|
3482
|
+
userId,
|
|
3483
|
+
client
|
|
3484
|
+
},
|
|
3485
|
+
body: payload
|
|
3486
|
+
});
|
|
3487
|
+
}
|
|
3488
|
+
/**
|
|
3489
|
+
* Sets a custom client-specific key-value pair in CustomPrefs.
|
|
3490
|
+
*/
|
|
3491
|
+
async setCustomPreference(displayPreferencesId, key, value, options = {}) {
|
|
3492
|
+
const current = await this.getDisplayPreferences(displayPreferencesId, options);
|
|
3493
|
+
const customPrefs = {
|
|
3494
|
+
...current.CustomPrefs || {},
|
|
3495
|
+
[key]: value
|
|
3496
|
+
};
|
|
3497
|
+
const updated = {
|
|
3498
|
+
...current,
|
|
3499
|
+
CustomPrefs: customPrefs
|
|
3500
|
+
};
|
|
3501
|
+
await this.updateDisplayPreferences(displayPreferencesId, updated, options);
|
|
3502
|
+
return updated;
|
|
3503
|
+
}
|
|
3504
|
+
/**
|
|
3505
|
+
* Updates sort field and order preferences for a view.
|
|
3506
|
+
*/
|
|
3507
|
+
async setSortPreferences(displayPreferencesId, sortBy, sortOrder, options = {}) {
|
|
3508
|
+
const current = await this.getDisplayPreferences(displayPreferencesId, options);
|
|
3509
|
+
const updated = {
|
|
3510
|
+
...current,
|
|
3511
|
+
SortBy: sortBy,
|
|
3512
|
+
SortOrder: sortOrder,
|
|
3513
|
+
RememberSorting: true
|
|
3514
|
+
};
|
|
3515
|
+
await this.updateDisplayPreferences(displayPreferencesId, updated, options);
|
|
3516
|
+
return updated;
|
|
3517
|
+
}
|
|
3518
|
+
/**
|
|
3519
|
+
* Sets the view presentation layout type (e.g. 'Poster', 'Thumb', 'Banner', 'List').
|
|
3520
|
+
*/
|
|
3521
|
+
async setViewType(displayPreferencesId, viewType, options = {}) {
|
|
3522
|
+
const current = await this.getDisplayPreferences(displayPreferencesId, options);
|
|
3523
|
+
const updated = {
|
|
3524
|
+
...current,
|
|
3525
|
+
ViewType: viewType
|
|
3526
|
+
};
|
|
3527
|
+
await this.updateDisplayPreferences(displayPreferencesId, updated, options);
|
|
3528
|
+
return updated;
|
|
3529
|
+
}
|
|
3530
|
+
};
|
|
3531
|
+
|
|
3532
|
+
// src/cache.ts
|
|
3533
|
+
var MemoryCacheAdapter = class {
|
|
3534
|
+
cache = /* @__PURE__ */ new Map();
|
|
3535
|
+
get(key) {
|
|
3536
|
+
const entry = this.cache.get(key);
|
|
3537
|
+
return entry ? entry.data : null;
|
|
3538
|
+
}
|
|
3539
|
+
set(key, data) {
|
|
3540
|
+
this.cache.set(key, { data, timestamp: Date.now() });
|
|
3541
|
+
}
|
|
3542
|
+
delete(key) {
|
|
3543
|
+
this.cache.delete(key);
|
|
3544
|
+
}
|
|
3545
|
+
clear() {
|
|
3546
|
+
this.cache.clear();
|
|
3547
|
+
}
|
|
3548
|
+
};
|
|
3549
|
+
var IndexedDBCacheAdapter = class {
|
|
3550
|
+
dbName;
|
|
3551
|
+
dbVersion;
|
|
3552
|
+
storeName;
|
|
3553
|
+
dbPromise = null;
|
|
3554
|
+
constructor(dbName = "JellyfinSdkCache", dbVersion = 1, storeName = "api_cache") {
|
|
3555
|
+
this.dbName = dbName;
|
|
3556
|
+
this.dbVersion = dbVersion;
|
|
3557
|
+
this.storeName = storeName;
|
|
3558
|
+
}
|
|
3559
|
+
async getDB() {
|
|
3560
|
+
if (this.dbPromise) return this.dbPromise;
|
|
3561
|
+
this.dbPromise = new Promise((resolve) => {
|
|
3562
|
+
if (typeof window === "undefined" || !("indexedDB" in window)) {
|
|
3563
|
+
resolve(null);
|
|
3564
|
+
return;
|
|
3565
|
+
}
|
|
3566
|
+
const request = indexedDB.open(this.dbName, this.dbVersion);
|
|
3567
|
+
request.onupgradeneeded = (event) => {
|
|
3568
|
+
const db = event.target.result;
|
|
3569
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
3570
|
+
db.createObjectStore(this.storeName, { keyPath: "key" });
|
|
3571
|
+
}
|
|
3572
|
+
};
|
|
3573
|
+
request.onsuccess = () => resolve(request.result);
|
|
3574
|
+
request.onerror = (e) => {
|
|
3575
|
+
console.warn("[Jellyfin Cache] IndexedDB open error:", e);
|
|
3576
|
+
resolve(null);
|
|
3577
|
+
};
|
|
3578
|
+
});
|
|
3579
|
+
return this.dbPromise;
|
|
3580
|
+
}
|
|
3581
|
+
async get(key) {
|
|
3582
|
+
try {
|
|
3583
|
+
const db = await this.getDB();
|
|
3584
|
+
if (!db) return null;
|
|
3585
|
+
return new Promise((resolve) => {
|
|
3586
|
+
const tx = db.transaction(this.storeName, "readonly");
|
|
3587
|
+
const store = tx.objectStore(this.storeName);
|
|
3588
|
+
const request = store.get(key);
|
|
3589
|
+
request.onsuccess = () => {
|
|
3590
|
+
const record = request.result;
|
|
3591
|
+
resolve(record ? record.data : null);
|
|
3592
|
+
};
|
|
3593
|
+
request.onerror = () => resolve(null);
|
|
3594
|
+
});
|
|
3595
|
+
} catch {
|
|
3596
|
+
return null;
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
async set(key, data) {
|
|
3600
|
+
if (!data) return;
|
|
3601
|
+
try {
|
|
3602
|
+
const db = await this.getDB();
|
|
3603
|
+
if (!db) return;
|
|
3604
|
+
return new Promise((resolve) => {
|
|
3605
|
+
const tx = db.transaction(this.storeName, "readwrite");
|
|
3606
|
+
const store = tx.objectStore(this.storeName);
|
|
3607
|
+
const record = { key, data, timestamp: Date.now() };
|
|
3608
|
+
const request = store.put(record);
|
|
3609
|
+
request.onsuccess = () => resolve();
|
|
3610
|
+
request.onerror = () => resolve();
|
|
3611
|
+
});
|
|
3612
|
+
} catch {
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
async delete(key) {
|
|
3616
|
+
try {
|
|
3617
|
+
const db = await this.getDB();
|
|
3618
|
+
if (!db) return;
|
|
3619
|
+
return new Promise((resolve) => {
|
|
3620
|
+
const tx = db.transaction(this.storeName, "readwrite");
|
|
3621
|
+
const store = tx.objectStore(this.storeName);
|
|
3622
|
+
const request = store.delete(key);
|
|
3623
|
+
request.onsuccess = () => resolve();
|
|
3624
|
+
request.onerror = () => resolve();
|
|
3625
|
+
});
|
|
3626
|
+
} catch {
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
async clear() {
|
|
3630
|
+
try {
|
|
3631
|
+
const db = await this.getDB();
|
|
3632
|
+
if (!db) return;
|
|
3633
|
+
return new Promise((resolve) => {
|
|
3634
|
+
const tx = db.transaction(this.storeName, "readwrite");
|
|
3635
|
+
const store = tx.objectStore(this.storeName);
|
|
3636
|
+
const request = store.clear();
|
|
3637
|
+
request.onsuccess = () => resolve();
|
|
3638
|
+
request.onerror = () => resolve();
|
|
3639
|
+
});
|
|
3640
|
+
} catch {
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
};
|
|
3644
|
+
async function fetchWithCache(cache, cacheKey, fetchFn, onFreshData) {
|
|
3645
|
+
const cached = await cache.get(cacheKey);
|
|
3646
|
+
const backgroundFetch = fetchFn().then(async (freshData) => {
|
|
3647
|
+
if (freshData !== null && freshData !== void 0) {
|
|
3648
|
+
await cache.set(cacheKey, freshData);
|
|
3649
|
+
if (onFreshData) {
|
|
3650
|
+
try {
|
|
3651
|
+
onFreshData(freshData);
|
|
3652
|
+
} catch (e) {
|
|
3653
|
+
console.warn("[Jellyfin Cache] Error in onFreshData callback for %s:", cacheKey, e);
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
return freshData;
|
|
3658
|
+
}).catch((err) => {
|
|
3659
|
+
console.warn("[Jellyfin Cache] Background fetch failed for %s:", cacheKey, err);
|
|
3660
|
+
if (!cached) throw err;
|
|
3661
|
+
return cached;
|
|
3662
|
+
});
|
|
3663
|
+
if (cached !== null && cached !== void 0) {
|
|
3664
|
+
return cached;
|
|
3665
|
+
}
|
|
3666
|
+
return await backgroundFetch;
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
// src/storage.ts
|
|
3670
|
+
var MemoryStorage = class {
|
|
3671
|
+
store = /* @__PURE__ */ new Map();
|
|
3672
|
+
getItem(key) {
|
|
3673
|
+
return this.store.has(key) ? this.store.get(key) : null;
|
|
3674
|
+
}
|
|
3675
|
+
setItem(key, value) {
|
|
3676
|
+
this.store.set(key, value);
|
|
3677
|
+
}
|
|
3678
|
+
removeItem(key) {
|
|
2008
3679
|
this.store.delete(key);
|
|
2009
3680
|
}
|
|
2010
3681
|
clear() {
|
|
@@ -2049,12 +3720,25 @@ var JellyfinClient = class {
|
|
|
2049
3720
|
auth;
|
|
2050
3721
|
system;
|
|
2051
3722
|
library;
|
|
3723
|
+
search;
|
|
2052
3724
|
playlists;
|
|
2053
3725
|
playback;
|
|
2054
3726
|
media;
|
|
2055
3727
|
lyrics;
|
|
2056
3728
|
favorites;
|
|
2057
3729
|
offline;
|
|
3730
|
+
resume;
|
|
3731
|
+
nextUp;
|
|
3732
|
+
latest;
|
|
3733
|
+
transcode;
|
|
3734
|
+
websocket;
|
|
3735
|
+
sessions;
|
|
3736
|
+
quickConnect;
|
|
3737
|
+
trickplay;
|
|
3738
|
+
chapters;
|
|
3739
|
+
genres;
|
|
3740
|
+
collections;
|
|
3741
|
+
displayPreferences;
|
|
2058
3742
|
http;
|
|
2059
3743
|
storage;
|
|
2060
3744
|
cache;
|
|
@@ -2065,7 +3749,7 @@ var JellyfinClient = class {
|
|
|
2065
3749
|
this.cache = new MemoryCacheAdapter();
|
|
2066
3750
|
this.clientInfo = {
|
|
2067
3751
|
name: options.clientInfo?.name || "@francofantomius/jellyfin",
|
|
2068
|
-
version: options.clientInfo?.version || "0.
|
|
3752
|
+
version: options.clientInfo?.version || "0.4.0",
|
|
2069
3753
|
device: options.clientInfo?.device || (typeof window !== "undefined" ? "Web Browser" : "Node.js"),
|
|
2070
3754
|
deviceId: options.clientInfo?.deviceId || generateDeviceId()
|
|
2071
3755
|
};
|
|
@@ -2082,18 +3766,32 @@ var JellyfinClient = class {
|
|
|
2082
3766
|
}
|
|
2083
3767
|
});
|
|
2084
3768
|
const getUserId = () => this.auth.getUserId();
|
|
3769
|
+
const getDeviceId = () => this.clientInfo.deviceId;
|
|
2085
3770
|
this.system = new SystemModule(this.http, {
|
|
2086
3771
|
serverVersion: options.serverVersion,
|
|
2087
3772
|
targetVersion: options.targetVersion
|
|
2088
3773
|
});
|
|
2089
3774
|
this.auth = new AuthModule(this.http, options.userId || "");
|
|
2090
3775
|
this.library = new LibraryModule(this.http, getUserId);
|
|
3776
|
+
this.search = new SearchModule(this.http, getUserId);
|
|
2091
3777
|
this.playlists = new PlaylistsModule(this.http, getUserId);
|
|
2092
3778
|
this.playback = new PlaybackModule(this.http);
|
|
2093
3779
|
this.media = new MediaModule(this.http, getUserId);
|
|
2094
3780
|
this.lyrics = new LyricsModule(this.http);
|
|
2095
3781
|
this.favorites = new FavoritesModule(this.http, getUserId);
|
|
2096
3782
|
this.offline = new OfflineStorageManager(this.media);
|
|
3783
|
+
this.resume = new ResumeModule(this.http, getUserId);
|
|
3784
|
+
this.nextUp = new NextUpModule(this.http, getUserId);
|
|
3785
|
+
this.latest = new LatestModule(this.http, getUserId);
|
|
3786
|
+
this.transcode = new TranscodeModule(this.http, getDeviceId);
|
|
3787
|
+
this.websocket = new WebSocketModule(this.http, getDeviceId);
|
|
3788
|
+
this.sessions = new SessionsModule(this.http, getUserId);
|
|
3789
|
+
this.quickConnect = new QuickConnectModule(this.http);
|
|
3790
|
+
this.trickplay = new TrickplayModule(this.http);
|
|
3791
|
+
this.chapters = new ChaptersModule(this.http, getUserId);
|
|
3792
|
+
this.genres = new GenresModule(this.http, getUserId);
|
|
3793
|
+
this.collections = new CollectionsModule(this.http, getUserId);
|
|
3794
|
+
this.displayPreferences = new DisplayPreferencesModule(this.http, getUserId, this.clientInfo.name);
|
|
2097
3795
|
}
|
|
2098
3796
|
// --- Configuration Getters & Setters ---
|
|
2099
3797
|
get serverUrl() {
|
|
@@ -2195,6 +3893,9 @@ var JellyfinClient = class {
|
|
|
2195
3893
|
* Log out and clear tokens and session.
|
|
2196
3894
|
*/
|
|
2197
3895
|
logout() {
|
|
3896
|
+
if (this.websocket.isConnected) {
|
|
3897
|
+
this.websocket.disconnect();
|
|
3898
|
+
}
|
|
2198
3899
|
this.auth.logout();
|
|
2199
3900
|
this.emit("logout");
|
|
2200
3901
|
}
|
|
@@ -2212,24 +3913,38 @@ var JellyfinClient = class {
|
|
|
2212
3913
|
|
|
2213
3914
|
exports.AUDIO_QUALITY_PRESETS = AUDIO_QUALITY_PRESETS;
|
|
2214
3915
|
exports.AuthModule = AuthModule;
|
|
3916
|
+
exports.COMMON_SEARCH_FIELDS = COMMON_SEARCH_FIELDS;
|
|
3917
|
+
exports.ChaptersModule = ChaptersModule;
|
|
3918
|
+
exports.CollectionsModule = CollectionsModule;
|
|
3919
|
+
exports.DisplayPreferencesModule = DisplayPreferencesModule;
|
|
2215
3920
|
exports.FavoritesModule = FavoritesModule;
|
|
3921
|
+
exports.GenresModule = GenresModule;
|
|
2216
3922
|
exports.HttpTransport = HttpTransport;
|
|
2217
3923
|
exports.IndexedDBCacheAdapter = IndexedDBCacheAdapter;
|
|
2218
3924
|
exports.JellyfinApiError = JellyfinApiError;
|
|
2219
3925
|
exports.JellyfinAuthError = JellyfinAuthError;
|
|
2220
3926
|
exports.JellyfinClient = JellyfinClient;
|
|
2221
3927
|
exports.JellyfinError = JellyfinError;
|
|
3928
|
+
exports.LatestModule = LatestModule;
|
|
2222
3929
|
exports.LibraryModule = LibraryModule;
|
|
2223
3930
|
exports.LocalStorageAdapter = LocalStorageAdapter;
|
|
2224
3931
|
exports.LyricsModule = LyricsModule;
|
|
2225
3932
|
exports.MediaModule = MediaModule;
|
|
2226
3933
|
exports.MemoryCacheAdapter = MemoryCacheAdapter;
|
|
2227
3934
|
exports.MemoryStorage = MemoryStorage;
|
|
3935
|
+
exports.NextUpModule = NextUpModule;
|
|
2228
3936
|
exports.OfflineStorageManager = OfflineStorageManager;
|
|
2229
3937
|
exports.PlaybackModule = PlaybackModule;
|
|
2230
3938
|
exports.PlaylistsModule = PlaylistsModule;
|
|
3939
|
+
exports.QuickConnectModule = QuickConnectModule;
|
|
3940
|
+
exports.ResumeModule = ResumeModule;
|
|
3941
|
+
exports.SearchModule = SearchModule;
|
|
3942
|
+
exports.SessionsModule = SessionsModule;
|
|
2231
3943
|
exports.SystemModule = SystemModule;
|
|
3944
|
+
exports.TranscodeModule = TranscodeModule;
|
|
3945
|
+
exports.TrickplayModule = TrickplayModule;
|
|
2232
3946
|
exports.VIDEO_QUALITY_PRESETS = VIDEO_QUALITY_PRESETS;
|
|
3947
|
+
exports.WebSocketModule = WebSocketModule;
|
|
2233
3948
|
exports.buildAuthHeader = buildAuthHeader;
|
|
2234
3949
|
exports.cleanUrl = cleanUrl;
|
|
2235
3950
|
exports.fetchWithCache = fetchWithCache;
|