@francofantomius/jellyfin 0.2.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +181 -1
- package/dist/index.cjs +1527 -172
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +686 -9
- package/dist/index.d.ts +686 -9
- package/dist/index.js +1520 -173
- package/dist/index.js.map +1 -1
- package/llms.txt +542 -0
- package/package.json +8 -6
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,
|
|
@@ -1169,28 +1188,41 @@ var MediaModule = class {
|
|
|
1169
1188
|
const qualityResolved = resolveAudioQuality(options.quality);
|
|
1170
1189
|
const maxStreamingBitrate = options.maxStreamingBitrate ?? qualityResolved.maxStreamingBitrate;
|
|
1171
1190
|
const {
|
|
1191
|
+
mediaSourceId,
|
|
1192
|
+
static: isStatic = false,
|
|
1172
1193
|
startTimeTicks = 0,
|
|
1173
1194
|
container = "opus,mp3|mp3,aac,m4a,m4b,flac,wav,ogg",
|
|
1174
1195
|
transcodingContainer = "mp3",
|
|
1175
1196
|
audioCodec = "mp3",
|
|
1176
1197
|
useQueryToken = true
|
|
1177
1198
|
} = options;
|
|
1178
|
-
const
|
|
1179
|
-
|
|
1199
|
+
const endpoint = isStatic ? `/Audio/${itemId}/stream` : `/Audio/${itemId}/universal`;
|
|
1200
|
+
const url = new URL(`${serverUrl}${endpoint}`);
|
|
1201
|
+
if (token && useQueryToken) {
|
|
1202
|
+
url.searchParams.append("ApiKey", token);
|
|
1203
|
+
url.searchParams.append("api_key", token);
|
|
1204
|
+
url.searchParams.append("X-Emby-Token", token);
|
|
1205
|
+
}
|
|
1180
1206
|
if (userId) url.searchParams.append("UserId", userId);
|
|
1181
|
-
|
|
1182
|
-
url.searchParams.append("
|
|
1183
|
-
url.searchParams.append("
|
|
1184
|
-
|
|
1185
|
-
|
|
1207
|
+
const targetDeviceId = options.deviceId !== void 0 ? options.deviceId : clientInfo.deviceId;
|
|
1208
|
+
if (targetDeviceId) url.searchParams.append("DeviceId", targetDeviceId);
|
|
1209
|
+
if (mediaSourceId) url.searchParams.append("MediaSourceId", mediaSourceId);
|
|
1210
|
+
if (isStatic) {
|
|
1211
|
+
url.searchParams.append("static", "true");
|
|
1212
|
+
} else {
|
|
1213
|
+
url.searchParams.append("Container", container);
|
|
1214
|
+
url.searchParams.append("TranscodingContainer", transcodingContainer);
|
|
1215
|
+
url.searchParams.append("TranscodingProtocol", "http");
|
|
1216
|
+
url.searchParams.append("AudioCodec", audioCodec);
|
|
1217
|
+
url.searchParams.append("EnableRedirection", "true");
|
|
1218
|
+
url.searchParams.append("EnableRemoteMedia", "false");
|
|
1219
|
+
}
|
|
1186
1220
|
if (maxStreamingBitrate && maxStreamingBitrate !== "Direct") {
|
|
1187
1221
|
url.searchParams.append("MaxStreamingBitrate", String(maxStreamingBitrate));
|
|
1188
1222
|
}
|
|
1189
1223
|
if (startTimeTicks > 0) {
|
|
1190
1224
|
url.searchParams.append("StartTimeTicks", String(startTimeTicks));
|
|
1191
1225
|
}
|
|
1192
|
-
url.searchParams.append("EnableRedirection", "true");
|
|
1193
|
-
url.searchParams.append("EnableRemoteMedia", "false");
|
|
1194
1226
|
return url.toString();
|
|
1195
1227
|
}
|
|
1196
1228
|
/**
|
|
@@ -1205,16 +1237,24 @@ var MediaModule = class {
|
|
|
1205
1237
|
const qualityResolved = resolveAudioQuality(options.quality);
|
|
1206
1238
|
const maxStreamingBitrate = options.maxStreamingBitrate ?? qualityResolved.maxStreamingBitrate;
|
|
1207
1239
|
const {
|
|
1240
|
+
mediaSourceId,
|
|
1208
1241
|
startTimeTicks = 0,
|
|
1209
1242
|
audioCodec = "aac,mp3",
|
|
1210
1243
|
segmentLength = 3,
|
|
1211
1244
|
useQueryToken = true
|
|
1212
1245
|
} = options;
|
|
1213
1246
|
const url = new URL(`${serverUrl}/Audio/${itemId}/master.m3u8`);
|
|
1214
|
-
if (token && useQueryToken)
|
|
1247
|
+
if (token && useQueryToken) {
|
|
1248
|
+
url.searchParams.append("ApiKey", token);
|
|
1249
|
+
url.searchParams.append("api_key", token);
|
|
1250
|
+
url.searchParams.append("X-Emby-Token", token);
|
|
1251
|
+
}
|
|
1215
1252
|
if (userId) url.searchParams.append("UserId", userId);
|
|
1216
|
-
|
|
1217
|
-
url.searchParams.append("
|
|
1253
|
+
const targetDeviceId = options.deviceId !== void 0 ? options.deviceId : clientInfo.deviceId;
|
|
1254
|
+
if (targetDeviceId) url.searchParams.append("DeviceId", targetDeviceId);
|
|
1255
|
+
if (mediaSourceId) {
|
|
1256
|
+
url.searchParams.append("MediaSourceId", mediaSourceId);
|
|
1257
|
+
}
|
|
1218
1258
|
url.searchParams.append("AudioCodec", audioCodec);
|
|
1219
1259
|
url.searchParams.append("TranscodingContainer", "ts");
|
|
1220
1260
|
url.searchParams.append("TranscodingProtocol", "hls");
|
|
@@ -1259,9 +1299,14 @@ var MediaModule = class {
|
|
|
1259
1299
|
} = options;
|
|
1260
1300
|
const endpoint = container && !container.includes(",") ? `/Videos/${itemId}/stream.${container}` : `/Videos/${itemId}/stream`;
|
|
1261
1301
|
const url = new URL(`${serverUrl}${endpoint}`);
|
|
1262
|
-
if (token && useQueryToken)
|
|
1302
|
+
if (token && useQueryToken) {
|
|
1303
|
+
url.searchParams.append("ApiKey", token);
|
|
1304
|
+
url.searchParams.append("api_key", token);
|
|
1305
|
+
url.searchParams.append("X-Emby-Token", token);
|
|
1306
|
+
}
|
|
1263
1307
|
if (userId) url.searchParams.append("UserId", userId);
|
|
1264
|
-
|
|
1308
|
+
const targetDeviceId = options.deviceId !== void 0 ? options.deviceId : clientInfo.deviceId;
|
|
1309
|
+
if (targetDeviceId) url.searchParams.append("DeviceId", targetDeviceId);
|
|
1265
1310
|
if (mediaSourceId) url.searchParams.append("MediaSourceId", mediaSourceId);
|
|
1266
1311
|
if (isStatic) {
|
|
1267
1312
|
url.searchParams.append("Static", "true");
|
|
@@ -1320,9 +1365,14 @@ var MediaModule = class {
|
|
|
1320
1365
|
useQueryToken = true
|
|
1321
1366
|
} = options;
|
|
1322
1367
|
const url = new URL(`${serverUrl}/Videos/${itemId}/master.m3u8`);
|
|
1323
|
-
if (token && useQueryToken)
|
|
1368
|
+
if (token && useQueryToken) {
|
|
1369
|
+
url.searchParams.append("ApiKey", token);
|
|
1370
|
+
url.searchParams.append("api_key", token);
|
|
1371
|
+
url.searchParams.append("X-Emby-Token", token);
|
|
1372
|
+
}
|
|
1324
1373
|
if (userId) url.searchParams.append("UserId", userId);
|
|
1325
|
-
|
|
1374
|
+
const targetDeviceId = options.deviceId !== void 0 ? options.deviceId : clientInfo.deviceId;
|
|
1375
|
+
if (targetDeviceId) url.searchParams.append("DeviceId", targetDeviceId);
|
|
1326
1376
|
if (mediaSourceId) url.searchParams.append("MediaSourceId", mediaSourceId);
|
|
1327
1377
|
url.searchParams.append("VideoCodec", videoCodec);
|
|
1328
1378
|
url.searchParams.append("AudioCodec", audioCodec);
|
|
@@ -1359,6 +1409,7 @@ var MediaModule = class {
|
|
|
1359
1409
|
const sourceId = mediaSourceId || itemId;
|
|
1360
1410
|
const url = new URL(`${serverUrl}/Videos/${itemId}/${sourceId}/Subtitles/${subtitleStreamIndex}/Stream.${format}`);
|
|
1361
1411
|
if (token && useQueryToken) {
|
|
1412
|
+
url.searchParams.append("ApiKey", token);
|
|
1362
1413
|
url.searchParams.append("api_key", token);
|
|
1363
1414
|
}
|
|
1364
1415
|
if (startTimeTicks > 0) {
|
|
@@ -1432,6 +1483,7 @@ var MediaModule = class {
|
|
|
1432
1483
|
if (!serverUrl || !itemId) return "";
|
|
1433
1484
|
const url = new URL(`${serverUrl}/Items/${itemId}/Download`);
|
|
1434
1485
|
if (token && useQueryToken) {
|
|
1486
|
+
url.searchParams.append("ApiKey", token);
|
|
1435
1487
|
url.searchParams.append("api_key", token);
|
|
1436
1488
|
}
|
|
1437
1489
|
if (filename) {
|
|
@@ -1478,13 +1530,17 @@ var MediaModule = class {
|
|
|
1478
1530
|
/**
|
|
1479
1531
|
* Returns authentication headers for media streaming requests (e.g., configuring hls.js xhrSetup).
|
|
1480
1532
|
* Crucial for Jellyfin 12+ servers where query-based auth (?api_key=) is disabled.
|
|
1533
|
+
* Pass { legacy: false } to only send Authorization and avoid conflicting headers or reverse-proxy CORS rejections.
|
|
1481
1534
|
*/
|
|
1482
|
-
getStreamHeaders() {
|
|
1535
|
+
getStreamHeaders(options = {}) {
|
|
1483
1536
|
const authHeader = this.http.getAuthHeader();
|
|
1484
|
-
|
|
1485
|
-
Authorization: authHeader
|
|
1486
|
-
"X-Emby-Authorization": authHeader
|
|
1537
|
+
const headers = {
|
|
1538
|
+
Authorization: authHeader
|
|
1487
1539
|
};
|
|
1540
|
+
if (options.legacy ?? true) {
|
|
1541
|
+
headers["X-Emby-Authorization"] = authHeader;
|
|
1542
|
+
}
|
|
1543
|
+
return headers;
|
|
1488
1544
|
}
|
|
1489
1545
|
};
|
|
1490
1546
|
|
|
@@ -2219,180 +2275,1452 @@ var SearchModule = class {
|
|
|
2219
2275
|
}
|
|
2220
2276
|
};
|
|
2221
2277
|
|
|
2222
|
-
// src/
|
|
2223
|
-
var
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2278
|
+
// src/websocket.ts
|
|
2279
|
+
var WebSocketModule = class {
|
|
2280
|
+
http;
|
|
2281
|
+
getDeviceId;
|
|
2282
|
+
ws = null;
|
|
2283
|
+
keepAliveTimer = null;
|
|
2284
|
+
reconnectTimer = null;
|
|
2285
|
+
listeners = /* @__PURE__ */ new Map();
|
|
2286
|
+
reconnectAttempts = 0;
|
|
2287
|
+
explicitlyClosed = false;
|
|
2288
|
+
options = {};
|
|
2289
|
+
constructor(http, getDeviceId) {
|
|
2290
|
+
this.http = http;
|
|
2291
|
+
this.getDeviceId = getDeviceId;
|
|
2228
2292
|
}
|
|
2229
|
-
|
|
2230
|
-
|
|
2293
|
+
/**
|
|
2294
|
+
* Check whether the WebSocket is currently open and connected.
|
|
2295
|
+
*/
|
|
2296
|
+
get isConnected() {
|
|
2297
|
+
return this.ws !== null && this.ws.readyState === 1;
|
|
2231
2298
|
}
|
|
2232
|
-
|
|
2233
|
-
|
|
2299
|
+
/**
|
|
2300
|
+
* Build the Jellyfin WebSocket URL from current serverUrl, accessToken, and deviceId.
|
|
2301
|
+
*/
|
|
2302
|
+
getWebSocketUrl() {
|
|
2303
|
+
const serverUrl = this.http.getServerUrl();
|
|
2304
|
+
if (!serverUrl) {
|
|
2305
|
+
throw new JellyfinError("Server URL is required to connect to WebSocket.");
|
|
2306
|
+
}
|
|
2307
|
+
const token = this.http.getToken();
|
|
2308
|
+
if (!token) {
|
|
2309
|
+
throw new JellyfinError("Access token is required to connect to WebSocket. Please authenticate first.");
|
|
2310
|
+
}
|
|
2311
|
+
const deviceId = this.getDeviceId();
|
|
2312
|
+
const wsBaseUrl = serverUrl.replace(/^http:\/\//i, "ws://").replace(/^https:\/\//i, "wss://").replace(/\/+$/, "");
|
|
2313
|
+
const url = new URL(`${wsBaseUrl}/socket`);
|
|
2314
|
+
url.searchParams.set("api_key", token);
|
|
2315
|
+
if (deviceId) {
|
|
2316
|
+
url.searchParams.set("deviceId", deviceId);
|
|
2317
|
+
}
|
|
2318
|
+
return url.toString();
|
|
2234
2319
|
}
|
|
2235
|
-
|
|
2236
|
-
|
|
2320
|
+
/**
|
|
2321
|
+
* Connect to the Jellyfin WebSocket endpoint.
|
|
2322
|
+
*/
|
|
2323
|
+
connect(options = {}) {
|
|
2324
|
+
this.options = {
|
|
2325
|
+
autoReconnect: true,
|
|
2326
|
+
reconnectIntervalMs: 5e3,
|
|
2327
|
+
maxReconnectAttempts: 10,
|
|
2328
|
+
keepAliveIntervalMs: 3e4,
|
|
2329
|
+
...options
|
|
2330
|
+
};
|
|
2331
|
+
this.explicitlyClosed = false;
|
|
2332
|
+
this.cleanup();
|
|
2333
|
+
const socketUrl = this.options.url || this.getWebSocketUrl();
|
|
2334
|
+
let SocketConstructor = this.options.webSocketFactory ? null : typeof globalThis !== "undefined" ? globalThis.WebSocket : null;
|
|
2335
|
+
if (this.options.webSocketFactory) {
|
|
2336
|
+
this.ws = this.options.webSocketFactory(socketUrl);
|
|
2337
|
+
} else if (SocketConstructor) {
|
|
2338
|
+
this.ws = new SocketConstructor(socketUrl);
|
|
2339
|
+
} else {
|
|
2340
|
+
throw new JellyfinError("WebSocket constructor is not available in the current environment. Please supply webSocketFactory in options.");
|
|
2341
|
+
}
|
|
2342
|
+
this.setupSocketEvents();
|
|
2237
2343
|
}
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
constructor(dbName = "JellyfinSdkCache", dbVersion = 1, storeName = "api_cache") {
|
|
2245
|
-
this.dbName = dbName;
|
|
2246
|
-
this.dbVersion = dbVersion;
|
|
2247
|
-
this.storeName = storeName;
|
|
2344
|
+
/**
|
|
2345
|
+
* Disconnect the WebSocket and prevent automatic reconnection.
|
|
2346
|
+
*/
|
|
2347
|
+
disconnect() {
|
|
2348
|
+
this.explicitlyClosed = true;
|
|
2349
|
+
this.cleanup();
|
|
2248
2350
|
}
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2351
|
+
/**
|
|
2352
|
+
* Send a raw string or JSON message through the open WebSocket.
|
|
2353
|
+
*/
|
|
2354
|
+
send(message) {
|
|
2355
|
+
if (!this.isConnected) {
|
|
2356
|
+
throw new JellyfinError("Cannot send message: WebSocket is not open.");
|
|
2357
|
+
}
|
|
2358
|
+
const payload = typeof message === "string" ? message : JSON.stringify(message);
|
|
2359
|
+
this.ws.send(payload);
|
|
2360
|
+
}
|
|
2361
|
+
/**
|
|
2362
|
+
* Send a KeepAlive message to the Jellyfin server.
|
|
2363
|
+
*/
|
|
2364
|
+
sendKeepAlive() {
|
|
2365
|
+
this.send({ MessageType: "KeepAlive" });
|
|
2366
|
+
}
|
|
2367
|
+
/**
|
|
2368
|
+
* Subscribe to real-time session updates from the server.
|
|
2369
|
+
*/
|
|
2370
|
+
startSessionsSubscription(intervalMs = 1500) {
|
|
2371
|
+
this.send({
|
|
2372
|
+
MessageType: "SessionsStart",
|
|
2373
|
+
Data: `0,${intervalMs}`
|
|
2268
2374
|
});
|
|
2269
|
-
return this.dbPromise;
|
|
2270
2375
|
}
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
const tx = db.transaction(this.storeName, "readonly");
|
|
2277
|
-
const store = tx.objectStore(this.storeName);
|
|
2278
|
-
const request = store.get(key);
|
|
2279
|
-
request.onsuccess = () => {
|
|
2280
|
-
const record = request.result;
|
|
2281
|
-
resolve(record ? record.data : null);
|
|
2282
|
-
};
|
|
2283
|
-
request.onerror = () => resolve(null);
|
|
2284
|
-
});
|
|
2285
|
-
} catch {
|
|
2286
|
-
return null;
|
|
2287
|
-
}
|
|
2376
|
+
/**
|
|
2377
|
+
* Stop receiving real-time session updates.
|
|
2378
|
+
*/
|
|
2379
|
+
stopSessionsSubscription() {
|
|
2380
|
+
this.send({ MessageType: "SessionsStop" });
|
|
2288
2381
|
}
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
const record = { key, data, timestamp: Date.now() };
|
|
2298
|
-
const request = store.put(record);
|
|
2299
|
-
request.onsuccess = () => resolve();
|
|
2300
|
-
request.onerror = () => resolve();
|
|
2301
|
-
});
|
|
2302
|
-
} catch {
|
|
2303
|
-
}
|
|
2382
|
+
/**
|
|
2383
|
+
* Subscribe to scheduled tasks updates from the server.
|
|
2384
|
+
*/
|
|
2385
|
+
startScheduledTasksSubscription(intervalMs = 1500) {
|
|
2386
|
+
this.send({
|
|
2387
|
+
MessageType: "ScheduledTasksInfoStart",
|
|
2388
|
+
Data: `0,${intervalMs}`
|
|
2389
|
+
});
|
|
2304
2390
|
}
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2391
|
+
/**
|
|
2392
|
+
* Stop scheduled tasks updates.
|
|
2393
|
+
*/
|
|
2394
|
+
stopScheduledTasksSubscription() {
|
|
2395
|
+
this.send({ MessageType: "ScheduledTasksInfoStop" });
|
|
2396
|
+
}
|
|
2397
|
+
/**
|
|
2398
|
+
* Subscribe to server activity log updates.
|
|
2399
|
+
*/
|
|
2400
|
+
startActivityLogSubscription(intervalMs = 1500) {
|
|
2401
|
+
this.send({
|
|
2402
|
+
MessageType: "ActivityLogEntryStart",
|
|
2403
|
+
Data: `0,${intervalMs}`
|
|
2404
|
+
});
|
|
2405
|
+
}
|
|
2406
|
+
/**
|
|
2407
|
+
* Stop server activity log updates.
|
|
2408
|
+
*/
|
|
2409
|
+
stopActivityLogSubscription() {
|
|
2410
|
+
this.send({ MessageType: "ActivityLogEntryStop" });
|
|
2411
|
+
}
|
|
2412
|
+
on(event, listener) {
|
|
2413
|
+
const key = event.toLowerCase();
|
|
2414
|
+
if (!this.listeners.has(key)) {
|
|
2415
|
+
this.listeners.set(key, /* @__PURE__ */ new Set());
|
|
2317
2416
|
}
|
|
2417
|
+
this.listeners.get(key).add(listener);
|
|
2418
|
+
return () => this.off(key, listener);
|
|
2318
2419
|
}
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
const tx = db.transaction(this.storeName, "readwrite");
|
|
2325
|
-
const store = tx.objectStore(this.storeName);
|
|
2326
|
-
const request = store.clear();
|
|
2327
|
-
request.onsuccess = () => resolve();
|
|
2328
|
-
request.onerror = () => resolve();
|
|
2329
|
-
});
|
|
2330
|
-
} catch {
|
|
2420
|
+
off(event, listener) {
|
|
2421
|
+
const key = event.toLowerCase();
|
|
2422
|
+
const set = this.listeners.get(key);
|
|
2423
|
+
if (set) {
|
|
2424
|
+
set.delete(listener);
|
|
2331
2425
|
}
|
|
2332
2426
|
}
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
await cache.set(cacheKey, freshData);
|
|
2339
|
-
if (onFreshData) {
|
|
2427
|
+
emit(event, ...args) {
|
|
2428
|
+
const key = event.toLowerCase();
|
|
2429
|
+
const set = this.listeners.get(key);
|
|
2430
|
+
if (set) {
|
|
2431
|
+
set.forEach((listener) => {
|
|
2340
2432
|
try {
|
|
2341
|
-
|
|
2433
|
+
listener(...args);
|
|
2342
2434
|
} catch (e) {
|
|
2343
|
-
console.
|
|
2435
|
+
console.error(`[Jellyfin SDK] WebSocket listener error on '${event}':`, e);
|
|
2344
2436
|
}
|
|
2345
|
-
}
|
|
2437
|
+
});
|
|
2346
2438
|
}
|
|
2347
|
-
return freshData;
|
|
2348
|
-
}).catch((err) => {
|
|
2349
|
-
console.warn("[Jellyfin Cache] Background fetch failed for %s:", cacheKey, err);
|
|
2350
|
-
if (!cached) throw err;
|
|
2351
|
-
return cached;
|
|
2352
|
-
});
|
|
2353
|
-
if (cached !== null && cached !== void 0) {
|
|
2354
|
-
return cached;
|
|
2355
|
-
}
|
|
2356
|
-
return await backgroundFetch;
|
|
2357
|
-
}
|
|
2358
|
-
|
|
2359
|
-
// src/storage.ts
|
|
2360
|
-
var MemoryStorage = class {
|
|
2361
|
-
store = /* @__PURE__ */ new Map();
|
|
2362
|
-
getItem(key) {
|
|
2363
|
-
return this.store.has(key) ? this.store.get(key) : null;
|
|
2364
|
-
}
|
|
2365
|
-
setItem(key, value) {
|
|
2366
|
-
this.store.set(key, value);
|
|
2367
2439
|
}
|
|
2368
|
-
|
|
2369
|
-
|
|
2440
|
+
// --- Internal Socket Helpers ---
|
|
2441
|
+
setupSocketEvents() {
|
|
2442
|
+
if (!this.ws) return;
|
|
2443
|
+
this.ws.onopen = () => {
|
|
2444
|
+
this.reconnectAttempts = 0;
|
|
2445
|
+
this.startKeepAlive();
|
|
2446
|
+
this.emit("open");
|
|
2447
|
+
};
|
|
2448
|
+
this.ws.onclose = (ev) => {
|
|
2449
|
+
this.stopKeepAlive();
|
|
2450
|
+
this.emit("close", ev?.code, ev?.reason);
|
|
2451
|
+
if (!this.explicitlyClosed && this.options.autoReconnect) {
|
|
2452
|
+
this.scheduleReconnect();
|
|
2453
|
+
}
|
|
2454
|
+
};
|
|
2455
|
+
this.ws.onerror = (err) => {
|
|
2456
|
+
this.emit("error", err);
|
|
2457
|
+
};
|
|
2458
|
+
this.ws.onmessage = (event) => {
|
|
2459
|
+
try {
|
|
2460
|
+
const text = typeof event.data === "string" ? event.data : String(event.data);
|
|
2461
|
+
const parsed = JSON.parse(text);
|
|
2462
|
+
this.emit("message", parsed);
|
|
2463
|
+
if (parsed.MessageType) {
|
|
2464
|
+
this.emit(parsed.MessageType, parsed.Data);
|
|
2465
|
+
if (parsed.MessageType === "ForceKeepAlive") {
|
|
2466
|
+
this.sendKeepAlive();
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
} catch (e) {
|
|
2470
|
+
this.emit("rawMessage", event.data);
|
|
2471
|
+
}
|
|
2472
|
+
};
|
|
2370
2473
|
}
|
|
2371
|
-
|
|
2372
|
-
this.
|
|
2474
|
+
startKeepAlive() {
|
|
2475
|
+
this.stopKeepAlive();
|
|
2476
|
+
const interval = this.options.keepAliveIntervalMs || 3e4;
|
|
2477
|
+
if (interval > 0) {
|
|
2478
|
+
this.keepAliveTimer = setInterval(() => {
|
|
2479
|
+
if (this.isConnected) {
|
|
2480
|
+
try {
|
|
2481
|
+
this.sendKeepAlive();
|
|
2482
|
+
} catch {
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
}, interval);
|
|
2486
|
+
}
|
|
2373
2487
|
}
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
return localStorage.getItem(key);
|
|
2488
|
+
stopKeepAlive() {
|
|
2489
|
+
if (this.keepAliveTimer) {
|
|
2490
|
+
clearInterval(this.keepAliveTimer);
|
|
2491
|
+
this.keepAliveTimer = null;
|
|
2379
2492
|
}
|
|
2380
|
-
return null;
|
|
2381
2493
|
}
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2494
|
+
scheduleReconnect() {
|
|
2495
|
+
const maxAttempts = this.options.maxReconnectAttempts ?? 10;
|
|
2496
|
+
if (this.reconnectAttempts >= maxAttempts) {
|
|
2497
|
+
return;
|
|
2385
2498
|
}
|
|
2499
|
+
const interval = this.options.reconnectIntervalMs ?? 5e3;
|
|
2500
|
+
const backoffDelay = Math.min(interval * Math.pow(1.5, this.reconnectAttempts), 6e4);
|
|
2501
|
+
this.reconnectAttempts++;
|
|
2502
|
+
this.reconnectTimer = setTimeout(() => {
|
|
2503
|
+
if (!this.explicitlyClosed) {
|
|
2504
|
+
try {
|
|
2505
|
+
this.connect(this.options);
|
|
2506
|
+
} catch (e) {
|
|
2507
|
+
this.emit("error", e);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
}, backoffDelay);
|
|
2386
2511
|
}
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2512
|
+
cleanup() {
|
|
2513
|
+
this.stopKeepAlive();
|
|
2514
|
+
if (this.reconnectTimer) {
|
|
2515
|
+
clearTimeout(this.reconnectTimer);
|
|
2516
|
+
this.reconnectTimer = null;
|
|
2517
|
+
}
|
|
2518
|
+
if (this.ws) {
|
|
2519
|
+
try {
|
|
2520
|
+
this.ws.onopen = null;
|
|
2521
|
+
this.ws.onclose = null;
|
|
2522
|
+
this.ws.onerror = null;
|
|
2523
|
+
this.ws.onmessage = null;
|
|
2524
|
+
if (this.ws.readyState === 0 || this.ws.readyState === 1) {
|
|
2525
|
+
this.ws.close();
|
|
2526
|
+
}
|
|
2527
|
+
} catch {
|
|
2528
|
+
}
|
|
2529
|
+
this.ws = null;
|
|
2390
2530
|
}
|
|
2391
2531
|
}
|
|
2392
2532
|
};
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2533
|
+
|
|
2534
|
+
// src/sessions.ts
|
|
2535
|
+
var SessionsModule = class {
|
|
2536
|
+
http;
|
|
2537
|
+
getUserId;
|
|
2538
|
+
constructor(http, getUserId) {
|
|
2539
|
+
this.http = http;
|
|
2540
|
+
this.getUserId = getUserId;
|
|
2541
|
+
}
|
|
2542
|
+
/**
|
|
2543
|
+
* Query active client sessions across the Jellyfin server via GET /Sessions.
|
|
2544
|
+
*/
|
|
2545
|
+
async getSessions(options = {}) {
|
|
2546
|
+
const params = {};
|
|
2547
|
+
if (options.controllableByUserId) {
|
|
2548
|
+
params.ControllableByUserId = options.controllableByUserId;
|
|
2549
|
+
}
|
|
2550
|
+
if (options.deviceId) {
|
|
2551
|
+
params.DeviceId = options.deviceId;
|
|
2552
|
+
}
|
|
2553
|
+
if (options.activeWithinSeconds !== void 0) {
|
|
2554
|
+
params.ActiveWithinSeconds = options.activeWithinSeconds;
|
|
2555
|
+
}
|
|
2556
|
+
const result = await this.http.request("/Sessions", { params });
|
|
2557
|
+
return Array.isArray(result) ? result : [];
|
|
2558
|
+
}
|
|
2559
|
+
/**
|
|
2560
|
+
* Instruct a remote session to start playback of specified items via POST /Sessions/{sessionId}/Playing.
|
|
2561
|
+
*
|
|
2562
|
+
* @param sessionId The target session ID.
|
|
2563
|
+
* @param itemIds Single item ID or array of item IDs to play.
|
|
2564
|
+
* @param options Additional playback parameters (command type, start ticks, etc.).
|
|
2565
|
+
*/
|
|
2566
|
+
async play(sessionId, itemIds, options = {}) {
|
|
2567
|
+
if (!sessionId) {
|
|
2568
|
+
throw new JellyfinError("sessionId is required to send a play command.");
|
|
2569
|
+
}
|
|
2570
|
+
const ids = Array.isArray(itemIds) ? itemIds : [itemIds];
|
|
2571
|
+
if (ids.length === 0) {
|
|
2572
|
+
throw new JellyfinError("At least one itemId is required to send a play command.");
|
|
2573
|
+
}
|
|
2574
|
+
const params = {
|
|
2575
|
+
ItemIds: ids.join(","),
|
|
2576
|
+
PlayCommand: options.playCommand || "PlayNow"
|
|
2577
|
+
};
|
|
2578
|
+
if (options.startPositionTicks !== void 0) {
|
|
2579
|
+
params.StartPositionTicks = options.startPositionTicks;
|
|
2580
|
+
}
|
|
2581
|
+
if (options.mediaSourceId) {
|
|
2582
|
+
params.MediaSourceId = options.mediaSourceId;
|
|
2583
|
+
}
|
|
2584
|
+
if (options.audioStreamIndex !== void 0) {
|
|
2585
|
+
params.AudioStreamIndex = options.audioStreamIndex;
|
|
2586
|
+
}
|
|
2587
|
+
if (options.subtitleStreamIndex !== void 0) {
|
|
2588
|
+
params.SubtitleStreamIndex = options.subtitleStreamIndex;
|
|
2589
|
+
}
|
|
2590
|
+
if (options.startIndex !== void 0) {
|
|
2591
|
+
params.StartIndex = options.startIndex;
|
|
2592
|
+
}
|
|
2593
|
+
await this.http.request(`/Sessions/${sessionId}/Playing`, {
|
|
2594
|
+
method: "POST",
|
|
2595
|
+
params
|
|
2596
|
+
});
|
|
2597
|
+
}
|
|
2598
|
+
/**
|
|
2599
|
+
* Send a playback control command to a remote session via POST /Sessions/{sessionId}/Playing/{command}.
|
|
2600
|
+
*/
|
|
2601
|
+
async sendPlaystateCommand(sessionId, command, params = {}) {
|
|
2602
|
+
if (!sessionId) {
|
|
2603
|
+
throw new JellyfinError("sessionId is required to send a playstate command.");
|
|
2604
|
+
}
|
|
2605
|
+
await this.http.request(`/Sessions/${sessionId}/Playing/${command}`, {
|
|
2606
|
+
method: "POST",
|
|
2607
|
+
params
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
/**
|
|
2611
|
+
* Toggle play / pause on a remote session.
|
|
2612
|
+
*/
|
|
2613
|
+
async playPause(sessionId) {
|
|
2614
|
+
return await this.sendPlaystateCommand(sessionId, "PlayPause");
|
|
2615
|
+
}
|
|
2616
|
+
/**
|
|
2617
|
+
* Pause playback on a remote session.
|
|
2618
|
+
*/
|
|
2619
|
+
async pause(sessionId) {
|
|
2620
|
+
return await this.sendPlaystateCommand(sessionId, "Pause");
|
|
2621
|
+
}
|
|
2622
|
+
/**
|
|
2623
|
+
* Resume playback on a remote session.
|
|
2624
|
+
*/
|
|
2625
|
+
async unpause(sessionId) {
|
|
2626
|
+
return await this.sendPlaystateCommand(sessionId, "Unpause");
|
|
2627
|
+
}
|
|
2628
|
+
/**
|
|
2629
|
+
* Stop playback on a remote session.
|
|
2630
|
+
*/
|
|
2631
|
+
async stop(sessionId) {
|
|
2632
|
+
return await this.sendPlaystateCommand(sessionId, "Stop");
|
|
2633
|
+
}
|
|
2634
|
+
/**
|
|
2635
|
+
* Seek to a specific tick position on a remote session.
|
|
2636
|
+
*
|
|
2637
|
+
* @param sessionId Target session ID.
|
|
2638
|
+
* @param positionTicks Position in ticks (1 tick = 10,000 milliseconds = 100 nanoseconds).
|
|
2639
|
+
*/
|
|
2640
|
+
async seek(sessionId, positionTicks) {
|
|
2641
|
+
return await this.sendPlaystateCommand(sessionId, "Seek", {
|
|
2642
|
+
SeekPositionTicks: positionTicks
|
|
2643
|
+
});
|
|
2644
|
+
}
|
|
2645
|
+
/**
|
|
2646
|
+
* Skip to the next track on a remote session.
|
|
2647
|
+
*/
|
|
2648
|
+
async nextTrack(sessionId) {
|
|
2649
|
+
return await this.sendPlaystateCommand(sessionId, "NextTrack");
|
|
2650
|
+
}
|
|
2651
|
+
/**
|
|
2652
|
+
* Skip to the previous track on a remote session.
|
|
2653
|
+
*/
|
|
2654
|
+
async previousTrack(sessionId) {
|
|
2655
|
+
return await this.sendPlaystateCommand(sessionId, "PreviousTrack");
|
|
2656
|
+
}
|
|
2657
|
+
/**
|
|
2658
|
+
* Send a general command to a remote session via POST /Sessions/{sessionId}/Command.
|
|
2659
|
+
*/
|
|
2660
|
+
async sendGeneralCommand(sessionId, command) {
|
|
2661
|
+
if (!sessionId) {
|
|
2662
|
+
throw new JellyfinError("sessionId is required to send a general command.");
|
|
2663
|
+
}
|
|
2664
|
+
await this.http.request(`/Sessions/${sessionId}/Command`, {
|
|
2665
|
+
method: "POST",
|
|
2666
|
+
body: {
|
|
2667
|
+
Name: command.Name,
|
|
2668
|
+
Arguments: command.Arguments || {}
|
|
2669
|
+
}
|
|
2670
|
+
});
|
|
2671
|
+
}
|
|
2672
|
+
/**
|
|
2673
|
+
* Set volume level on a remote session (0-100).
|
|
2674
|
+
*/
|
|
2675
|
+
async setVolume(sessionId, volume) {
|
|
2676
|
+
const clamped = Math.max(0, Math.min(100, Math.round(volume)));
|
|
2677
|
+
return await this.sendGeneralCommand(sessionId, {
|
|
2678
|
+
Name: "SetVolume",
|
|
2679
|
+
Arguments: { Volume: String(clamped) }
|
|
2680
|
+
});
|
|
2681
|
+
}
|
|
2682
|
+
/**
|
|
2683
|
+
* Mute audio on a remote session.
|
|
2684
|
+
*/
|
|
2685
|
+
async mute(sessionId) {
|
|
2686
|
+
return await this.sendGeneralCommand(sessionId, { Name: "Mute" });
|
|
2687
|
+
}
|
|
2688
|
+
/**
|
|
2689
|
+
* Unmute audio on a remote session.
|
|
2690
|
+
*/
|
|
2691
|
+
async unmute(sessionId) {
|
|
2692
|
+
return await this.sendGeneralCommand(sessionId, { Name: "Unmute" });
|
|
2693
|
+
}
|
|
2694
|
+
/**
|
|
2695
|
+
* Toggle mute on a remote session.
|
|
2696
|
+
*/
|
|
2697
|
+
async toggleMute(sessionId) {
|
|
2698
|
+
return await this.sendGeneralCommand(sessionId, { Name: "ToggleMute" });
|
|
2699
|
+
}
|
|
2700
|
+
/**
|
|
2701
|
+
* Switch the audio stream index on a remote session.
|
|
2702
|
+
*/
|
|
2703
|
+
async setAudioStreamIndex(sessionId, index) {
|
|
2704
|
+
return await this.sendGeneralCommand(sessionId, {
|
|
2705
|
+
Name: "SetAudioStreamIndex",
|
|
2706
|
+
Arguments: { Index: String(index) }
|
|
2707
|
+
});
|
|
2708
|
+
}
|
|
2709
|
+
/**
|
|
2710
|
+
* Switch the subtitle stream index on a remote session.
|
|
2711
|
+
*/
|
|
2712
|
+
async setSubtitleStreamIndex(sessionId, index) {
|
|
2713
|
+
return await this.sendGeneralCommand(sessionId, {
|
|
2714
|
+
Name: "SetSubtitleStreamIndex",
|
|
2715
|
+
Arguments: { Index: String(index) }
|
|
2716
|
+
});
|
|
2717
|
+
}
|
|
2718
|
+
/**
|
|
2719
|
+
* Display an on-screen dialog message modal on a remote session via POST /Sessions/{sessionId}/Message.
|
|
2720
|
+
*/
|
|
2721
|
+
async sendMessage(sessionId, options) {
|
|
2722
|
+
if (!sessionId) {
|
|
2723
|
+
throw new JellyfinError("sessionId is required to send a message.");
|
|
2724
|
+
}
|
|
2725
|
+
const payload = typeof options === "string" ? { Text: options, Header: "Message" } : {
|
|
2726
|
+
Text: options.text,
|
|
2727
|
+
Header: options.header || "Message",
|
|
2728
|
+
TimeoutMs: options.timeoutMs
|
|
2729
|
+
};
|
|
2730
|
+
await this.http.request(`/Sessions/${sessionId}/Message`, {
|
|
2731
|
+
method: "POST",
|
|
2732
|
+
body: payload
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2735
|
+
/**
|
|
2736
|
+
* Instruct a remote session to navigate and view an item or page via POST /Sessions/{sessionId}/Viewing.
|
|
2737
|
+
*/
|
|
2738
|
+
async viewItem(sessionId, itemType, itemId, itemName) {
|
|
2739
|
+
if (!sessionId) {
|
|
2740
|
+
throw new JellyfinError("sessionId is required to instruct session navigation.");
|
|
2741
|
+
}
|
|
2742
|
+
await this.http.request(`/Sessions/${sessionId}/Viewing`, {
|
|
2743
|
+
method: "POST",
|
|
2744
|
+
params: {
|
|
2745
|
+
ItemType: itemType,
|
|
2746
|
+
ItemId: itemId,
|
|
2747
|
+
ItemName: itemName
|
|
2748
|
+
}
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2751
|
+
};
|
|
2752
|
+
|
|
2753
|
+
// src/quick-connect.ts
|
|
2754
|
+
var QuickConnectModule = class {
|
|
2755
|
+
http;
|
|
2756
|
+
constructor(http) {
|
|
2757
|
+
this.http = http;
|
|
2758
|
+
}
|
|
2759
|
+
/**
|
|
2760
|
+
* Check whether Quick Connect is enabled on the connected Jellyfin server.
|
|
2761
|
+
*/
|
|
2762
|
+
async isEnabled() {
|
|
2763
|
+
try {
|
|
2764
|
+
const result = await this.http.request("/QuickConnect/Enabled");
|
|
2765
|
+
if (typeof result === "boolean") {
|
|
2766
|
+
return result;
|
|
2767
|
+
}
|
|
2768
|
+
return String(result).toLowerCase() === "true";
|
|
2769
|
+
} catch {
|
|
2770
|
+
return false;
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
/**
|
|
2774
|
+
* Initiate a Quick Connect authentication session.
|
|
2775
|
+
* Typically called by a client device (e.g. Smart TV or CLI) to generate a user-facing pairing code.
|
|
2776
|
+
*/
|
|
2777
|
+
async initiate() {
|
|
2778
|
+
const result = await this.http.request("/QuickConnect/Initiate", {
|
|
2779
|
+
method: "POST"
|
|
2780
|
+
});
|
|
2781
|
+
if (!result || !result.Code || !result.Secret) {
|
|
2782
|
+
throw new JellyfinError("Quick Connect initiate failed: Invalid server response.");
|
|
2783
|
+
}
|
|
2784
|
+
return result;
|
|
2785
|
+
}
|
|
2786
|
+
/**
|
|
2787
|
+
* Check the current authorization status of an initiated Quick Connect secret.
|
|
2788
|
+
*
|
|
2789
|
+
* @param secret The secret token returned from initiate().
|
|
2790
|
+
*/
|
|
2791
|
+
async check(secret) {
|
|
2792
|
+
if (!secret) {
|
|
2793
|
+
throw new JellyfinError("secret is required to check Quick Connect status.");
|
|
2794
|
+
}
|
|
2795
|
+
return await this.http.request("/QuickConnect/Connect", {
|
|
2796
|
+
method: "GET",
|
|
2797
|
+
params: { secret }
|
|
2798
|
+
});
|
|
2799
|
+
}
|
|
2800
|
+
/**
|
|
2801
|
+
* Authorize a Quick Connect pairing code from an authenticated session.
|
|
2802
|
+
* Typically called on a mobile app or browser where the user is already logged in.
|
|
2803
|
+
*
|
|
2804
|
+
* @param code The pairing code shown on the target device.
|
|
2805
|
+
* @param userId Optional specific user ID to authenticate as (defaults to the currently authenticated user).
|
|
2806
|
+
*/
|
|
2807
|
+
async authorize(code, userId) {
|
|
2808
|
+
if (!code) {
|
|
2809
|
+
throw new JellyfinError("code is required to authorize Quick Connect.");
|
|
2810
|
+
}
|
|
2811
|
+
const params = {
|
|
2812
|
+
Code: code.replace(/\s+/g, "")
|
|
2813
|
+
// Normalize code by removing spaces
|
|
2814
|
+
};
|
|
2815
|
+
if (userId) {
|
|
2816
|
+
params.UserId = userId;
|
|
2817
|
+
}
|
|
2818
|
+
await this.http.request("/QuickConnect/Authorize", {
|
|
2819
|
+
method: "POST",
|
|
2820
|
+
params
|
|
2821
|
+
});
|
|
2822
|
+
return true;
|
|
2823
|
+
}
|
|
2824
|
+
/**
|
|
2825
|
+
* Repeatedly poll until the user authorizes the Quick Connect request or a timeout occurs.
|
|
2826
|
+
*
|
|
2827
|
+
* @param secret The secret token returned from initiate().
|
|
2828
|
+
* @param options Polling configuration (intervalMs, timeoutMs, signal).
|
|
2829
|
+
*/
|
|
2830
|
+
async poll(secret, options = {}) {
|
|
2831
|
+
const { intervalMs = 2e3, timeoutMs = 18e4, signal } = options;
|
|
2832
|
+
const startTime = Date.now();
|
|
2833
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2834
|
+
if (signal?.aborted) {
|
|
2835
|
+
throw new JellyfinError("Quick Connect polling was aborted.");
|
|
2836
|
+
}
|
|
2837
|
+
const state = await this.check(secret);
|
|
2838
|
+
if (state.Authenticated) {
|
|
2839
|
+
return state;
|
|
2840
|
+
}
|
|
2841
|
+
await new Promise((resolve, reject) => {
|
|
2842
|
+
const timer = setTimeout(() => resolve(), intervalMs);
|
|
2843
|
+
if (signal) {
|
|
2844
|
+
signal.addEventListener(
|
|
2845
|
+
"abort",
|
|
2846
|
+
() => {
|
|
2847
|
+
clearTimeout(timer);
|
|
2848
|
+
reject(new JellyfinError("Quick Connect polling was aborted."));
|
|
2849
|
+
},
|
|
2850
|
+
{ once: true }
|
|
2851
|
+
);
|
|
2852
|
+
}
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2855
|
+
throw new JellyfinError(`Quick Connect timed out after ${timeoutMs}ms.`);
|
|
2856
|
+
}
|
|
2857
|
+
};
|
|
2858
|
+
|
|
2859
|
+
// src/trickplay.ts
|
|
2860
|
+
var TrickplayModule = class {
|
|
2861
|
+
http;
|
|
2862
|
+
constructor(http) {
|
|
2863
|
+
this.http = http;
|
|
2864
|
+
}
|
|
2865
|
+
/**
|
|
2866
|
+
* Generates the HLS playlist URL for trickplay scrubbing image tiles.
|
|
2867
|
+
* E.g. /Videos/{itemId}/Trickplay/{width}/tiles.m3u8
|
|
2868
|
+
*/
|
|
2869
|
+
getTilesHlsUrl(itemId, width, options = {}) {
|
|
2870
|
+
const serverUrl = this.http.getServerUrl();
|
|
2871
|
+
const token = this.http.getToken();
|
|
2872
|
+
const { mediaSourceId, useQueryToken = true } = options;
|
|
2873
|
+
const url = new URL(`${serverUrl}/Videos/${itemId}/Trickplay/${width}/tiles.m3u8`);
|
|
2874
|
+
if (token && useQueryToken) {
|
|
2875
|
+
url.searchParams.append("ApiKey", token);
|
|
2876
|
+
url.searchParams.append("api_key", token);
|
|
2877
|
+
url.searchParams.append("X-Emby-Token", token);
|
|
2878
|
+
}
|
|
2879
|
+
if (mediaSourceId) {
|
|
2880
|
+
url.searchParams.append("MediaSourceId", mediaSourceId);
|
|
2881
|
+
}
|
|
2882
|
+
return url.toString();
|
|
2883
|
+
}
|
|
2884
|
+
/**
|
|
2885
|
+
* Generates the URL for a specific trickplay thumbnail tile image sheet.
|
|
2886
|
+
* E.g. /Videos/{itemId}/Trickplay/{width}/{index}.jpg
|
|
2887
|
+
*/
|
|
2888
|
+
getTileImageUrl(itemId, width, index, options = {}) {
|
|
2889
|
+
const serverUrl = this.http.getServerUrl();
|
|
2890
|
+
const token = this.http.getToken();
|
|
2891
|
+
const { mediaSourceId, useQueryToken = true } = options;
|
|
2892
|
+
const url = new URL(`${serverUrl}/Videos/${itemId}/Trickplay/${width}/${index}.jpg`);
|
|
2893
|
+
if (token && useQueryToken) {
|
|
2894
|
+
url.searchParams.append("ApiKey", token);
|
|
2895
|
+
url.searchParams.append("api_key", token);
|
|
2896
|
+
url.searchParams.append("X-Emby-Token", token);
|
|
2897
|
+
}
|
|
2898
|
+
if (mediaSourceId) {
|
|
2899
|
+
url.searchParams.append("MediaSourceId", mediaSourceId);
|
|
2900
|
+
}
|
|
2901
|
+
return url.toString();
|
|
2902
|
+
}
|
|
2903
|
+
/**
|
|
2904
|
+
* Fetches and parses the HLS trickplay manifest (tiles.m3u8) for the given item and resolution.
|
|
2905
|
+
*/
|
|
2906
|
+
async getTrickplayManifest(itemId, width, options = {}) {
|
|
2907
|
+
const params = {};
|
|
2908
|
+
if (options.mediaSourceId) {
|
|
2909
|
+
params.MediaSourceId = options.mediaSourceId;
|
|
2910
|
+
}
|
|
2911
|
+
const endpoint = `/Videos/${itemId}/Trickplay/${width}/tiles.m3u8`;
|
|
2912
|
+
const rawM3u8 = await this.http.requestText(endpoint, { params });
|
|
2913
|
+
const tiles = this.parseTilesM3u8(itemId, width, rawM3u8, options.mediaSourceId, options.useQueryToken);
|
|
2914
|
+
return {
|
|
2915
|
+
width,
|
|
2916
|
+
rawM3u8,
|
|
2917
|
+
tiles
|
|
2918
|
+
};
|
|
2919
|
+
}
|
|
2920
|
+
/**
|
|
2921
|
+
* Helper to parse #EXTINF and tile file names from a tiles.m3u8 playlist.
|
|
2922
|
+
*/
|
|
2923
|
+
parseTilesM3u8(itemId, width, rawM3u8, mediaSourceId, useQueryToken = true) {
|
|
2924
|
+
const lines = rawM3u8.split(/\r?\n/);
|
|
2925
|
+
const tiles = [];
|
|
2926
|
+
let currentDurationMs = 0;
|
|
2927
|
+
for (const line of lines) {
|
|
2928
|
+
const trimmed = line.trim();
|
|
2929
|
+
if (!trimmed) continue;
|
|
2930
|
+
if (trimmed.startsWith("#EXTINF:")) {
|
|
2931
|
+
const secondsStr = trimmed.substring(8).split(",")[0];
|
|
2932
|
+
const seconds = parseFloat(secondsStr);
|
|
2933
|
+
currentDurationMs = !isNaN(seconds) ? Math.round(seconds * 1e3) : 0;
|
|
2934
|
+
} else if (!trimmed.startsWith("#")) {
|
|
2935
|
+
const indexMatch = trimmed.match(/^(\d+)\.(jpg|jpeg|png|webp)/i);
|
|
2936
|
+
const index = indexMatch ? parseInt(indexMatch[1], 10) : tiles.length;
|
|
2937
|
+
const url = this.getTileImageUrl(itemId, width, index, { mediaSourceId, useQueryToken });
|
|
2938
|
+
tiles.push({
|
|
2939
|
+
index,
|
|
2940
|
+
durationMs: currentDurationMs,
|
|
2941
|
+
url
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
return tiles;
|
|
2946
|
+
}
|
|
2947
|
+
/**
|
|
2948
|
+
* Extracts the available trickplay thumbnail widths from a BaseItemDto's Trickplay metadata.
|
|
2949
|
+
*/
|
|
2950
|
+
getAvailableResolutions(item, mediaSourceId) {
|
|
2951
|
+
if (!item.Trickplay) {
|
|
2952
|
+
return [];
|
|
2953
|
+
}
|
|
2954
|
+
const targetMediaSourceId = mediaSourceId || item.MediaSources && item.MediaSources[0]?.Id || Object.keys(item.Trickplay)[0];
|
|
2955
|
+
if (!targetMediaSourceId || !item.Trickplay[targetMediaSourceId]) {
|
|
2956
|
+
return [];
|
|
2957
|
+
}
|
|
2958
|
+
const sourceTrickplay = item.Trickplay[targetMediaSourceId];
|
|
2959
|
+
return Object.keys(sourceTrickplay).map((w) => parseInt(w, 10)).filter((w) => !isNaN(w)).sort((a, b) => a - b);
|
|
2960
|
+
}
|
|
2961
|
+
/**
|
|
2962
|
+
* Retrieves the TrickplayInfoDto metadata for a specified item and optional width.
|
|
2963
|
+
*/
|
|
2964
|
+
getTrickplayInfo(item, width, mediaSourceId) {
|
|
2965
|
+
if (!item.Trickplay) {
|
|
2966
|
+
return null;
|
|
2967
|
+
}
|
|
2968
|
+
const targetMediaSourceId = mediaSourceId || item.MediaSources && item.MediaSources[0]?.Id || Object.keys(item.Trickplay)[0];
|
|
2969
|
+
if (!targetMediaSourceId || !item.Trickplay[targetMediaSourceId]) {
|
|
2970
|
+
return null;
|
|
2971
|
+
}
|
|
2972
|
+
const sourceTrickplay = item.Trickplay[targetMediaSourceId];
|
|
2973
|
+
if (width !== void 0 && sourceTrickplay[String(width)]) {
|
|
2974
|
+
return sourceTrickplay[String(width)];
|
|
2975
|
+
}
|
|
2976
|
+
const availableWidths = Object.keys(sourceTrickplay);
|
|
2977
|
+
if (availableWidths.length === 0) {
|
|
2978
|
+
return null;
|
|
2979
|
+
}
|
|
2980
|
+
return sourceTrickplay[availableWidths[0]];
|
|
2981
|
+
}
|
|
2982
|
+
/**
|
|
2983
|
+
* Calculates the exact tile sheet index, image URL, and sprite coordinates (x, y, width, height)
|
|
2984
|
+
* for a given playback timestamp to render scrubbing thumbnails in a player or seekbar UI.
|
|
2985
|
+
*/
|
|
2986
|
+
getThumbnailForTimestamp(options) {
|
|
2987
|
+
const {
|
|
2988
|
+
item,
|
|
2989
|
+
timestampMs,
|
|
2990
|
+
timestampTicks,
|
|
2991
|
+
width,
|
|
2992
|
+
mediaSourceId,
|
|
2993
|
+
useQueryToken = true
|
|
2994
|
+
} = options;
|
|
2995
|
+
let info = options.trickplayInfo;
|
|
2996
|
+
if (!info && item) {
|
|
2997
|
+
info = this.getTrickplayInfo(item, width, mediaSourceId) || void 0;
|
|
2998
|
+
}
|
|
2999
|
+
if (!info) {
|
|
3000
|
+
return null;
|
|
3001
|
+
}
|
|
3002
|
+
const timeMs = timestampMs !== void 0 ? timestampMs : timestampTicks !== void 0 ? Math.round(timestampTicks / 1e4) : 0;
|
|
3003
|
+
const interval = info.Interval > 0 ? info.Interval : 1e4;
|
|
3004
|
+
let thumbIndex = Math.floor(Math.max(0, timeMs) / interval);
|
|
3005
|
+
if (info.ThumbnailCount > 0 && thumbIndex >= info.ThumbnailCount) {
|
|
3006
|
+
thumbIndex = info.ThumbnailCount - 1;
|
|
3007
|
+
}
|
|
3008
|
+
const tileCols = info.TileWidth > 0 ? info.TileWidth : 1;
|
|
3009
|
+
const tileRows = info.TileHeight > 0 ? info.TileHeight : 1;
|
|
3010
|
+
const thumbsPerSheet = tileCols * tileRows;
|
|
3011
|
+
const sheetIndex = Math.floor(thumbIndex / thumbsPerSheet);
|
|
3012
|
+
const indexInSheet = thumbIndex % thumbsPerSheet;
|
|
3013
|
+
const col = indexInSheet % tileCols;
|
|
3014
|
+
const row = Math.floor(indexInSheet / tileCols);
|
|
3015
|
+
const x = col * info.Width;
|
|
3016
|
+
const y = row * info.Height;
|
|
3017
|
+
const itemId = item?.Id || "";
|
|
3018
|
+
if (!itemId) {
|
|
3019
|
+
throw new JellyfinError("Item or itemId is required to generate the thumbnail URL.");
|
|
3020
|
+
}
|
|
3021
|
+
const imageUrl = this.getTileImageUrl(itemId, info.Width, sheetIndex, {
|
|
3022
|
+
mediaSourceId,
|
|
3023
|
+
useQueryToken
|
|
3024
|
+
});
|
|
3025
|
+
return {
|
|
3026
|
+
tileIndex: sheetIndex,
|
|
3027
|
+
imageUrl,
|
|
3028
|
+
x,
|
|
3029
|
+
y,
|
|
3030
|
+
width: info.Width,
|
|
3031
|
+
height: info.Height,
|
|
3032
|
+
thumbnailIndex: thumbIndex
|
|
3033
|
+
};
|
|
3034
|
+
}
|
|
3035
|
+
};
|
|
3036
|
+
|
|
3037
|
+
// src/chapters.ts
|
|
3038
|
+
var ChaptersModule = class {
|
|
3039
|
+
http;
|
|
3040
|
+
getUserId;
|
|
3041
|
+
constructor(http, getUserId) {
|
|
3042
|
+
this.http = http;
|
|
3043
|
+
this.getUserId = getUserId;
|
|
3044
|
+
}
|
|
3045
|
+
/**
|
|
3046
|
+
* Synchronously extracts chapter markers from an already fetched BaseItemDto.
|
|
3047
|
+
*/
|
|
3048
|
+
extractChapters(item) {
|
|
3049
|
+
return item.Chapters || [];
|
|
3050
|
+
}
|
|
3051
|
+
/**
|
|
3052
|
+
* Fetches the chapter markers for an item from the server.
|
|
3053
|
+
*/
|
|
3054
|
+
async getChapters(itemId) {
|
|
3055
|
+
const userId = this.getUserId ? this.getUserId() : "";
|
|
3056
|
+
const endpoint = userId ? `/Users/${userId}/Items/${itemId}` : `/Items/${itemId}`;
|
|
3057
|
+
const item = await this.http.request(endpoint, {
|
|
3058
|
+
params: {
|
|
3059
|
+
Fields: "Chapters"
|
|
3060
|
+
}
|
|
3061
|
+
});
|
|
3062
|
+
return item.Chapters || [];
|
|
3063
|
+
}
|
|
3064
|
+
/**
|
|
3065
|
+
* Generates the URL for a chapter preview thumbnail image.
|
|
3066
|
+
* E.g. /Items/{itemId}/Images/Chapter/{chapterIndex}
|
|
3067
|
+
*/
|
|
3068
|
+
getChapterImageUrl(itemId, chapterIndex, options = {}) {
|
|
3069
|
+
const serverUrl = this.http.getServerUrl();
|
|
3070
|
+
const token = this.http.getToken();
|
|
3071
|
+
const {
|
|
3072
|
+
maxWidth,
|
|
3073
|
+
maxHeight,
|
|
3074
|
+
width,
|
|
3075
|
+
height,
|
|
3076
|
+
quality,
|
|
3077
|
+
format,
|
|
3078
|
+
tag,
|
|
3079
|
+
fillWidth,
|
|
3080
|
+
fillHeight,
|
|
3081
|
+
useQueryToken = true
|
|
3082
|
+
} = options;
|
|
3083
|
+
const url = new URL(`${serverUrl}/Items/${itemId}/Images/Chapter/${chapterIndex}`);
|
|
3084
|
+
if (token && useQueryToken) {
|
|
3085
|
+
url.searchParams.append("ApiKey", token);
|
|
3086
|
+
url.searchParams.append("api_key", token);
|
|
3087
|
+
url.searchParams.append("X-Emby-Token", token);
|
|
3088
|
+
}
|
|
3089
|
+
if (tag) url.searchParams.append("tag", tag);
|
|
3090
|
+
if (maxWidth) url.searchParams.append("maxWidth", String(maxWidth));
|
|
3091
|
+
if (maxHeight) url.searchParams.append("maxHeight", String(maxHeight));
|
|
3092
|
+
if (width) url.searchParams.append("width", String(width));
|
|
3093
|
+
if (height) url.searchParams.append("height", String(height));
|
|
3094
|
+
if (quality) url.searchParams.append("quality", String(quality));
|
|
3095
|
+
if (format) url.searchParams.append("format", format);
|
|
3096
|
+
if (fillWidth) url.searchParams.append("fillWidth", String(fillWidth));
|
|
3097
|
+
if (fillHeight) url.searchParams.append("fillHeight", String(fillHeight));
|
|
3098
|
+
return url.toString();
|
|
3099
|
+
}
|
|
3100
|
+
/**
|
|
3101
|
+
* Retrieves media segments (Intro, Outro, Recap, Commercials) for an item via Jellyfin 10.10+ /MediaSegments.
|
|
3102
|
+
*/
|
|
3103
|
+
async getMediaSegments(itemId, options = {}) {
|
|
3104
|
+
const params = {
|
|
3105
|
+
itemId
|
|
3106
|
+
};
|
|
3107
|
+
if (options.includeSegmentTypes && options.includeSegmentTypes.length > 0) {
|
|
3108
|
+
params.includeSegmentTypes = options.includeSegmentTypes.join(",");
|
|
3109
|
+
}
|
|
3110
|
+
try {
|
|
3111
|
+
const res = await this.http.request(
|
|
3112
|
+
"/MediaSegments",
|
|
3113
|
+
{ params }
|
|
3114
|
+
);
|
|
3115
|
+
if (Array.isArray(res)) {
|
|
3116
|
+
return res;
|
|
3117
|
+
}
|
|
3118
|
+
if (res && Array.isArray(res.Items)) {
|
|
3119
|
+
return res.Items;
|
|
3120
|
+
}
|
|
3121
|
+
return [];
|
|
3122
|
+
} catch {
|
|
3123
|
+
return [];
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
/**
|
|
3127
|
+
* Resolves intro and end credits markers for an item, combining /MediaSegments API
|
|
3128
|
+
* and chapter marker heuristics (MarkerType or naming like 'Intro', 'Opening', 'Credits', 'Ending').
|
|
3129
|
+
*/
|
|
3130
|
+
async getIntroCredits(itemOrChaptersOrId, mediaSegments) {
|
|
3131
|
+
let itemId = "";
|
|
3132
|
+
let chapters = [];
|
|
3133
|
+
let segments = mediaSegments || [];
|
|
3134
|
+
if (typeof itemOrChaptersOrId === "string") {
|
|
3135
|
+
itemId = itemOrChaptersOrId;
|
|
3136
|
+
if (segments.length === 0) {
|
|
3137
|
+
segments = await this.getMediaSegments(itemId);
|
|
3138
|
+
}
|
|
3139
|
+
if (segments.length === 0) {
|
|
3140
|
+
chapters = await this.getChapters(itemId);
|
|
3141
|
+
}
|
|
3142
|
+
} else if (Array.isArray(itemOrChaptersOrId)) {
|
|
3143
|
+
chapters = itemOrChaptersOrId;
|
|
3144
|
+
} else if (typeof itemOrChaptersOrId === "object" && itemOrChaptersOrId !== null) {
|
|
3145
|
+
itemId = itemOrChaptersOrId.Id;
|
|
3146
|
+
chapters = itemOrChaptersOrId.Chapters || [];
|
|
3147
|
+
if (segments.length === 0 && itemId) {
|
|
3148
|
+
segments = await this.getMediaSegments(itemId);
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
const markers = {
|
|
3152
|
+
segments
|
|
3153
|
+
};
|
|
3154
|
+
for (const seg of segments) {
|
|
3155
|
+
const type = String(seg.Type).toLowerCase();
|
|
3156
|
+
if (type === "intro" && !markers.intro) {
|
|
3157
|
+
markers.intro = {
|
|
3158
|
+
startTicks: seg.StartTicks,
|
|
3159
|
+
endTicks: seg.EndTicks,
|
|
3160
|
+
startMs: Math.round(seg.StartTicks / 1e4),
|
|
3161
|
+
endMs: Math.round(seg.EndTicks / 1e4)
|
|
3162
|
+
};
|
|
3163
|
+
} else if ((type === "outro" || type === "credits") && !markers.credits) {
|
|
3164
|
+
markers.credits = {
|
|
3165
|
+
startTicks: seg.StartTicks,
|
|
3166
|
+
endTicks: seg.EndTicks,
|
|
3167
|
+
startMs: Math.round(seg.StartTicks / 1e4),
|
|
3168
|
+
endMs: Math.round(seg.EndTicks / 1e4)
|
|
3169
|
+
};
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
if ((!markers.intro || !markers.credits) && chapters.length > 0) {
|
|
3173
|
+
for (let i = 0; i < chapters.length; i++) {
|
|
3174
|
+
const chapter = chapters[i];
|
|
3175
|
+
const nextChapter = chapters[i + 1];
|
|
3176
|
+
const name = (chapter.Name || "").trim();
|
|
3177
|
+
const markerType = (chapter.MarkerType || "").toLowerCase();
|
|
3178
|
+
if (!markers.intro) {
|
|
3179
|
+
if (markerType === "introstart" || /^(intro|opening|theme\s*song)/i.test(name)) {
|
|
3180
|
+
const startTicks = chapter.StartPositionTicks;
|
|
3181
|
+
const endTicks = nextChapter ? nextChapter.StartPositionTicks : startTicks + 90 * 1e7;
|
|
3182
|
+
markers.intro = {
|
|
3183
|
+
startTicks,
|
|
3184
|
+
endTicks,
|
|
3185
|
+
startMs: Math.round(startTicks / 1e4),
|
|
3186
|
+
endMs: Math.round(endTicks / 1e4)
|
|
3187
|
+
};
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
if (!markers.credits) {
|
|
3191
|
+
if (markerType === "creditsstart" || /^(credits|outro|ending|end\s*credits)/i.test(name)) {
|
|
3192
|
+
const startTicks = chapter.StartPositionTicks;
|
|
3193
|
+
const endTicks = nextChapter ? nextChapter.StartPositionTicks : startTicks + 180 * 1e7;
|
|
3194
|
+
markers.credits = {
|
|
3195
|
+
startTicks,
|
|
3196
|
+
endTicks,
|
|
3197
|
+
startMs: Math.round(startTicks / 1e4),
|
|
3198
|
+
endMs: Math.round(endTicks / 1e4)
|
|
3199
|
+
};
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
return markers;
|
|
3205
|
+
}
|
|
3206
|
+
/**
|
|
3207
|
+
* Checks whether the current playback position falls within an intro or credits range.
|
|
3208
|
+
*/
|
|
3209
|
+
findCurrentSegment(positionTicks, markersOrSegments) {
|
|
3210
|
+
if (Array.isArray(markersOrSegments)) {
|
|
3211
|
+
for (const seg of markersOrSegments) {
|
|
3212
|
+
if (positionTicks >= seg.StartTicks && positionTicks < seg.EndTicks) {
|
|
3213
|
+
return {
|
|
3214
|
+
type: seg.Type,
|
|
3215
|
+
startTicks: seg.StartTicks,
|
|
3216
|
+
endTicks: seg.EndTicks
|
|
3217
|
+
};
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
return null;
|
|
3221
|
+
}
|
|
3222
|
+
const { intro, credits } = markersOrSegments;
|
|
3223
|
+
if (intro && positionTicks >= intro.startTicks && positionTicks < intro.endTicks) {
|
|
3224
|
+
return {
|
|
3225
|
+
type: "Intro",
|
|
3226
|
+
startTicks: intro.startTicks,
|
|
3227
|
+
endTicks: intro.endTicks
|
|
3228
|
+
};
|
|
3229
|
+
}
|
|
3230
|
+
if (credits && positionTicks >= credits.startTicks && positionTicks < credits.endTicks) {
|
|
3231
|
+
return {
|
|
3232
|
+
type: "Credits",
|
|
3233
|
+
startTicks: credits.startTicks,
|
|
3234
|
+
endTicks: credits.endTicks
|
|
3235
|
+
};
|
|
3236
|
+
}
|
|
3237
|
+
return null;
|
|
3238
|
+
}
|
|
3239
|
+
/**
|
|
3240
|
+
* Returns the timestamp ticks to skip to if the current position is within a skippable segment,
|
|
3241
|
+
* or null if not currently within any skippable range.
|
|
3242
|
+
*/
|
|
3243
|
+
getSkipPosition(positionTicks, markersOrSegments) {
|
|
3244
|
+
const current = this.findCurrentSegment(positionTicks, markersOrSegments);
|
|
3245
|
+
return current ? current.endTicks : null;
|
|
3246
|
+
}
|
|
3247
|
+
};
|
|
3248
|
+
|
|
3249
|
+
// src/genres.ts
|
|
3250
|
+
var GenresModule = class {
|
|
3251
|
+
http;
|
|
3252
|
+
getUserId;
|
|
3253
|
+
constructor(http, getUserId) {
|
|
3254
|
+
this.http = http;
|
|
3255
|
+
this.getUserId = getUserId;
|
|
3256
|
+
}
|
|
3257
|
+
resolveUserId(explicitUserId) {
|
|
3258
|
+
return explicitUserId || this.getUserId() || "";
|
|
3259
|
+
}
|
|
3260
|
+
/**
|
|
3261
|
+
* Retrieves video/movie genres from the server via GET /Genres.
|
|
3262
|
+
*/
|
|
3263
|
+
async getGenres(options = {}) {
|
|
3264
|
+
const userId = this.resolveUserId(options.userId);
|
|
3265
|
+
const params = {};
|
|
3266
|
+
if (userId) params.UserId = userId;
|
|
3267
|
+
if (options.parentId) params.ParentId = options.parentId;
|
|
3268
|
+
if (options.startIndex !== void 0) params.StartIndex = options.startIndex;
|
|
3269
|
+
if (options.limit !== void 0) params.Limit = options.limit;
|
|
3270
|
+
if (options.searchTerm) params.SearchTerm = options.searchTerm;
|
|
3271
|
+
if (options.sortBy) params.SortBy = options.sortBy;
|
|
3272
|
+
if (options.sortOrder) params.SortOrder = options.sortOrder;
|
|
3273
|
+
if (options.enableImages !== void 0) params.EnableImages = options.enableImages;
|
|
3274
|
+
if (options.imageTypeLimit !== void 0) params.ImageTypeLimit = options.imageTypeLimit;
|
|
3275
|
+
if (options.enableImageTypes && options.enableImageTypes.length > 0) {
|
|
3276
|
+
params.EnableImageTypes = options.enableImageTypes.join(",");
|
|
3277
|
+
}
|
|
3278
|
+
return await this.http.request("/Genres", { params });
|
|
3279
|
+
}
|
|
3280
|
+
/**
|
|
3281
|
+
* Retrieves a single genre by name via GET /Genres/{genreName}.
|
|
3282
|
+
*/
|
|
3283
|
+
async getGenre(genreName, options = {}) {
|
|
3284
|
+
const userId = this.resolveUserId(options.userId);
|
|
3285
|
+
const params = {};
|
|
3286
|
+
if (userId) params.UserId = userId;
|
|
3287
|
+
return await this.http.request(`/Genres/${encodeURIComponent(genreName)}`, {
|
|
3288
|
+
params
|
|
3289
|
+
});
|
|
3290
|
+
}
|
|
3291
|
+
/**
|
|
3292
|
+
* Retrieves music genres from the server via GET /MusicGenres.
|
|
3293
|
+
*/
|
|
3294
|
+
async getMusicGenres(options = {}) {
|
|
3295
|
+
const userId = this.resolveUserId(options.userId);
|
|
3296
|
+
const params = {};
|
|
3297
|
+
if (userId) params.UserId = userId;
|
|
3298
|
+
if (options.parentId) params.ParentId = options.parentId;
|
|
3299
|
+
if (options.startIndex !== void 0) params.StartIndex = options.startIndex;
|
|
3300
|
+
if (options.limit !== void 0) params.Limit = options.limit;
|
|
3301
|
+
if (options.searchTerm) params.SearchTerm = options.searchTerm;
|
|
3302
|
+
if (options.sortBy) params.SortBy = options.sortBy;
|
|
3303
|
+
if (options.sortOrder) params.SortOrder = options.sortOrder;
|
|
3304
|
+
if (options.enableImages !== void 0) params.EnableImages = options.enableImages;
|
|
3305
|
+
if (options.imageTypeLimit !== void 0) params.ImageTypeLimit = options.imageTypeLimit;
|
|
3306
|
+
if (options.enableImageTypes && options.enableImageTypes.length > 0) {
|
|
3307
|
+
params.EnableImageTypes = options.enableImageTypes.join(",");
|
|
3308
|
+
}
|
|
3309
|
+
return await this.http.request("/MusicGenres", { params });
|
|
3310
|
+
}
|
|
3311
|
+
/**
|
|
3312
|
+
* Retrieves a single music genre by name via GET /MusicGenres/{genreName}.
|
|
3313
|
+
*/
|
|
3314
|
+
async getMusicGenre(genreName, options = {}) {
|
|
3315
|
+
const userId = this.resolveUserId(options.userId);
|
|
3316
|
+
const params = {};
|
|
3317
|
+
if (userId) params.UserId = userId;
|
|
3318
|
+
return await this.http.request(`/MusicGenres/${encodeURIComponent(genreName)}`, {
|
|
3319
|
+
params
|
|
3320
|
+
});
|
|
3321
|
+
}
|
|
3322
|
+
/**
|
|
3323
|
+
* Retrieves studios/production companies from the server via GET /Studios.
|
|
3324
|
+
*/
|
|
3325
|
+
async getStudios(options = {}) {
|
|
3326
|
+
const userId = this.resolveUserId(options.userId);
|
|
3327
|
+
const params = {};
|
|
3328
|
+
if (userId) params.UserId = userId;
|
|
3329
|
+
if (options.parentId) params.ParentId = options.parentId;
|
|
3330
|
+
if (options.startIndex !== void 0) params.StartIndex = options.startIndex;
|
|
3331
|
+
if (options.limit !== void 0) params.Limit = options.limit;
|
|
3332
|
+
if (options.searchTerm) params.SearchTerm = options.searchTerm;
|
|
3333
|
+
if (options.sortBy) params.SortBy = options.sortBy;
|
|
3334
|
+
if (options.sortOrder) params.SortOrder = options.sortOrder;
|
|
3335
|
+
if (options.enableImages !== void 0) params.EnableImages = options.enableImages;
|
|
3336
|
+
if (options.imageTypeLimit !== void 0) params.ImageTypeLimit = options.imageTypeLimit;
|
|
3337
|
+
if (options.enableImageTypes && options.enableImageTypes.length > 0) {
|
|
3338
|
+
params.EnableImageTypes = options.enableImageTypes.join(",");
|
|
3339
|
+
}
|
|
3340
|
+
return await this.http.request("/Studios", { params });
|
|
3341
|
+
}
|
|
3342
|
+
/**
|
|
3343
|
+
* Retrieves a single studio by name via GET /Studios/{studioName}.
|
|
3344
|
+
*/
|
|
3345
|
+
async getStudio(studioName, options = {}) {
|
|
3346
|
+
const userId = this.resolveUserId(options.userId);
|
|
3347
|
+
const params = {};
|
|
3348
|
+
if (userId) params.UserId = userId;
|
|
3349
|
+
return await this.http.request(`/Studios/${encodeURIComponent(studioName)}`, {
|
|
3350
|
+
params
|
|
3351
|
+
});
|
|
3352
|
+
}
|
|
3353
|
+
};
|
|
3354
|
+
|
|
3355
|
+
// src/collections.ts
|
|
3356
|
+
var CollectionsModule = class {
|
|
3357
|
+
http;
|
|
3358
|
+
getUserId;
|
|
3359
|
+
constructor(http, getUserId) {
|
|
3360
|
+
this.http = http;
|
|
3361
|
+
this.getUserId = getUserId;
|
|
3362
|
+
}
|
|
3363
|
+
resolveUserId(explicitUserId) {
|
|
3364
|
+
return explicitUserId || this.getUserId() || "";
|
|
3365
|
+
}
|
|
3366
|
+
/**
|
|
3367
|
+
* Retrieves user BoxSet / Collections via GET /Collections.
|
|
3368
|
+
*/
|
|
3369
|
+
async getCollections(options = {}) {
|
|
3370
|
+
const userId = this.resolveUserId(options.userId);
|
|
3371
|
+
const params = {};
|
|
3372
|
+
if (userId) params.UserId = userId;
|
|
3373
|
+
if (options.parentId) params.ParentId = options.parentId;
|
|
3374
|
+
if (options.startIndex !== void 0) params.StartIndex = options.startIndex;
|
|
3375
|
+
if (options.limit !== void 0) params.Limit = options.limit;
|
|
3376
|
+
if (options.searchTerm) params.SearchTerm = options.searchTerm;
|
|
3377
|
+
if (options.sortBy) params.SortBy = options.sortBy;
|
|
3378
|
+
if (options.sortOrder) params.SortOrder = options.sortOrder;
|
|
3379
|
+
if (options.enableImages !== void 0) params.EnableImages = options.enableImages;
|
|
3380
|
+
if (options.imageTypeLimit !== void 0) params.ImageTypeLimit = options.imageTypeLimit;
|
|
3381
|
+
if (options.enableImageTypes && options.enableImageTypes.length > 0) {
|
|
3382
|
+
params.EnableImageTypes = options.enableImageTypes.join(",");
|
|
3383
|
+
}
|
|
3384
|
+
return await this.http.request("/Collections", { params });
|
|
3385
|
+
}
|
|
3386
|
+
/**
|
|
3387
|
+
* Retrieves the items belonging to a collection / BoxSet.
|
|
3388
|
+
*/
|
|
3389
|
+
async getCollectionItems(collectionId, options = {}) {
|
|
3390
|
+
const userId = this.resolveUserId(options.userId);
|
|
3391
|
+
const endpoint = userId ? `/Users/${userId}/Items` : "/Items";
|
|
3392
|
+
const params = {
|
|
3393
|
+
ParentId: collectionId
|
|
3394
|
+
};
|
|
3395
|
+
if (options.startIndex !== void 0) params.StartIndex = options.startIndex;
|
|
3396
|
+
if (options.limit !== void 0) params.Limit = options.limit;
|
|
3397
|
+
if (options.fields) params.Fields = options.fields;
|
|
3398
|
+
if (options.sortBy) params.SortBy = options.sortBy;
|
|
3399
|
+
if (options.sortOrder) params.SortOrder = options.sortOrder;
|
|
3400
|
+
return await this.http.request(endpoint, { params });
|
|
3401
|
+
}
|
|
3402
|
+
/**
|
|
3403
|
+
* Creates a new BoxSet / Collection via POST /Collections.
|
|
3404
|
+
*/
|
|
3405
|
+
async createCollection(options) {
|
|
3406
|
+
const params = {
|
|
3407
|
+
Name: options.name
|
|
3408
|
+
};
|
|
3409
|
+
if (options.ids && options.ids.length > 0) {
|
|
3410
|
+
params.Ids = options.ids.join(",");
|
|
3411
|
+
}
|
|
3412
|
+
if (options.parentId) {
|
|
3413
|
+
params.ParentId = options.parentId;
|
|
3414
|
+
}
|
|
3415
|
+
if (options.isFolder !== void 0) {
|
|
3416
|
+
params.IsFolder = options.isFolder;
|
|
3417
|
+
}
|
|
3418
|
+
return await this.http.request("/Collections", {
|
|
3419
|
+
method: "POST",
|
|
3420
|
+
params
|
|
3421
|
+
});
|
|
3422
|
+
}
|
|
3423
|
+
/**
|
|
3424
|
+
* Adds items to an existing BoxSet / Collection via POST /Collections/{collectionId}/Items.
|
|
3425
|
+
*/
|
|
3426
|
+
async addToCollection(collectionId, itemIds) {
|
|
3427
|
+
const ids = Array.isArray(itemIds) ? itemIds.join(",") : itemIds;
|
|
3428
|
+
await this.http.request(`/Collections/${collectionId}/Items`, {
|
|
3429
|
+
method: "POST",
|
|
3430
|
+
params: {
|
|
3431
|
+
Ids: ids
|
|
3432
|
+
}
|
|
3433
|
+
});
|
|
3434
|
+
}
|
|
3435
|
+
/**
|
|
3436
|
+
* Removes items from an existing BoxSet / Collection via DELETE /Collections/{collectionId}/Items.
|
|
3437
|
+
*/
|
|
3438
|
+
async removeFromCollection(collectionId, itemIds) {
|
|
3439
|
+
const ids = Array.isArray(itemIds) ? itemIds.join(",") : itemIds;
|
|
3440
|
+
await this.http.request(`/Collections/${collectionId}/Items`, {
|
|
3441
|
+
method: "DELETE",
|
|
3442
|
+
params: {
|
|
3443
|
+
Ids: ids
|
|
3444
|
+
}
|
|
3445
|
+
});
|
|
3446
|
+
}
|
|
3447
|
+
};
|
|
3448
|
+
|
|
3449
|
+
// src/display-preferences.ts
|
|
3450
|
+
var DisplayPreferencesModule = class {
|
|
3451
|
+
http;
|
|
3452
|
+
getUserId;
|
|
3453
|
+
defaultClient;
|
|
3454
|
+
constructor(http, getUserId, defaultClient = "emby") {
|
|
3455
|
+
this.http = http;
|
|
3456
|
+
this.getUserId = getUserId;
|
|
3457
|
+
this.defaultClient = defaultClient;
|
|
3458
|
+
}
|
|
3459
|
+
resolveUserId(explicitUserId) {
|
|
3460
|
+
const userId = explicitUserId || this.getUserId();
|
|
3461
|
+
if (!userId) {
|
|
3462
|
+
throw new JellyfinError(
|
|
3463
|
+
"User ID is required for DisplayPreferences. Please authenticate or specify userId."
|
|
3464
|
+
);
|
|
3465
|
+
}
|
|
3466
|
+
return userId;
|
|
3467
|
+
}
|
|
3468
|
+
/**
|
|
3469
|
+
* Retrieves the user's display preferences for a specific folder or view.
|
|
3470
|
+
* GET /DisplayPreferences/{displayPreferencesId}
|
|
3471
|
+
*/
|
|
3472
|
+
async getDisplayPreferences(displayPreferencesId, options = {}) {
|
|
3473
|
+
const userId = this.resolveUserId(options.userId);
|
|
3474
|
+
const client = options.client || this.defaultClient;
|
|
3475
|
+
return await this.http.request(
|
|
3476
|
+
`/DisplayPreferences/${displayPreferencesId}`,
|
|
3477
|
+
{
|
|
3478
|
+
params: {
|
|
3479
|
+
userId,
|
|
3480
|
+
client
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
);
|
|
3484
|
+
}
|
|
3485
|
+
/**
|
|
3486
|
+
* Updates display preferences for a specific folder or view.
|
|
3487
|
+
* POST /DisplayPreferences/{displayPreferencesId}
|
|
3488
|
+
*/
|
|
3489
|
+
async updateDisplayPreferences(displayPreferencesId, preferences, options = {}) {
|
|
3490
|
+
const userId = this.resolveUserId(options.userId);
|
|
3491
|
+
const client = options.client || this.defaultClient;
|
|
3492
|
+
const payload = {
|
|
3493
|
+
...preferences,
|
|
3494
|
+
Id: preferences.Id || displayPreferencesId,
|
|
3495
|
+
Client: preferences.Client || client
|
|
3496
|
+
};
|
|
3497
|
+
await this.http.request(`/DisplayPreferences/${displayPreferencesId}`, {
|
|
3498
|
+
method: "POST",
|
|
3499
|
+
params: {
|
|
3500
|
+
userId,
|
|
3501
|
+
client
|
|
3502
|
+
},
|
|
3503
|
+
body: payload
|
|
3504
|
+
});
|
|
3505
|
+
}
|
|
3506
|
+
/**
|
|
3507
|
+
* Sets a custom client-specific key-value pair in CustomPrefs.
|
|
3508
|
+
*/
|
|
3509
|
+
async setCustomPreference(displayPreferencesId, key, value, options = {}) {
|
|
3510
|
+
const current = await this.getDisplayPreferences(displayPreferencesId, options);
|
|
3511
|
+
const customPrefs = {
|
|
3512
|
+
...current.CustomPrefs || {},
|
|
3513
|
+
[key]: value
|
|
3514
|
+
};
|
|
3515
|
+
const updated = {
|
|
3516
|
+
...current,
|
|
3517
|
+
CustomPrefs: customPrefs
|
|
3518
|
+
};
|
|
3519
|
+
await this.updateDisplayPreferences(displayPreferencesId, updated, options);
|
|
3520
|
+
return updated;
|
|
3521
|
+
}
|
|
3522
|
+
/**
|
|
3523
|
+
* Updates sort field and order preferences for a view.
|
|
3524
|
+
*/
|
|
3525
|
+
async setSortPreferences(displayPreferencesId, sortBy, sortOrder, options = {}) {
|
|
3526
|
+
const current = await this.getDisplayPreferences(displayPreferencesId, options);
|
|
3527
|
+
const updated = {
|
|
3528
|
+
...current,
|
|
3529
|
+
SortBy: sortBy,
|
|
3530
|
+
SortOrder: sortOrder,
|
|
3531
|
+
RememberSorting: true
|
|
3532
|
+
};
|
|
3533
|
+
await this.updateDisplayPreferences(displayPreferencesId, updated, options);
|
|
3534
|
+
return updated;
|
|
3535
|
+
}
|
|
3536
|
+
/**
|
|
3537
|
+
* Sets the view presentation layout type (e.g. 'Poster', 'Thumb', 'Banner', 'List').
|
|
3538
|
+
*/
|
|
3539
|
+
async setViewType(displayPreferencesId, viewType, options = {}) {
|
|
3540
|
+
const current = await this.getDisplayPreferences(displayPreferencesId, options);
|
|
3541
|
+
const updated = {
|
|
3542
|
+
...current,
|
|
3543
|
+
ViewType: viewType
|
|
3544
|
+
};
|
|
3545
|
+
await this.updateDisplayPreferences(displayPreferencesId, updated, options);
|
|
3546
|
+
return updated;
|
|
3547
|
+
}
|
|
3548
|
+
};
|
|
3549
|
+
|
|
3550
|
+
// src/cache.ts
|
|
3551
|
+
var MemoryCacheAdapter = class {
|
|
3552
|
+
cache = /* @__PURE__ */ new Map();
|
|
3553
|
+
get(key) {
|
|
3554
|
+
const entry = this.cache.get(key);
|
|
3555
|
+
return entry ? entry.data : null;
|
|
3556
|
+
}
|
|
3557
|
+
set(key, data) {
|
|
3558
|
+
this.cache.set(key, { data, timestamp: Date.now() });
|
|
3559
|
+
}
|
|
3560
|
+
delete(key) {
|
|
3561
|
+
this.cache.delete(key);
|
|
3562
|
+
}
|
|
3563
|
+
clear() {
|
|
3564
|
+
this.cache.clear();
|
|
3565
|
+
}
|
|
3566
|
+
};
|
|
3567
|
+
var IndexedDBCacheAdapter = class {
|
|
3568
|
+
dbName;
|
|
3569
|
+
dbVersion;
|
|
3570
|
+
storeName;
|
|
3571
|
+
dbPromise = null;
|
|
3572
|
+
constructor(dbName = "JellyfinSdkCache", dbVersion = 1, storeName = "api_cache") {
|
|
3573
|
+
this.dbName = dbName;
|
|
3574
|
+
this.dbVersion = dbVersion;
|
|
3575
|
+
this.storeName = storeName;
|
|
3576
|
+
}
|
|
3577
|
+
async getDB() {
|
|
3578
|
+
if (this.dbPromise) return this.dbPromise;
|
|
3579
|
+
this.dbPromise = new Promise((resolve) => {
|
|
3580
|
+
if (typeof window === "undefined" || !("indexedDB" in window)) {
|
|
3581
|
+
resolve(null);
|
|
3582
|
+
return;
|
|
3583
|
+
}
|
|
3584
|
+
const request = indexedDB.open(this.dbName, this.dbVersion);
|
|
3585
|
+
request.onupgradeneeded = (event) => {
|
|
3586
|
+
const db = event.target.result;
|
|
3587
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
3588
|
+
db.createObjectStore(this.storeName, { keyPath: "key" });
|
|
3589
|
+
}
|
|
3590
|
+
};
|
|
3591
|
+
request.onsuccess = () => resolve(request.result);
|
|
3592
|
+
request.onerror = (e) => {
|
|
3593
|
+
console.warn("[Jellyfin Cache] IndexedDB open error:", e);
|
|
3594
|
+
resolve(null);
|
|
3595
|
+
};
|
|
3596
|
+
});
|
|
3597
|
+
return this.dbPromise;
|
|
3598
|
+
}
|
|
3599
|
+
async get(key) {
|
|
3600
|
+
try {
|
|
3601
|
+
const db = await this.getDB();
|
|
3602
|
+
if (!db) return null;
|
|
3603
|
+
return new Promise((resolve) => {
|
|
3604
|
+
const tx = db.transaction(this.storeName, "readonly");
|
|
3605
|
+
const store = tx.objectStore(this.storeName);
|
|
3606
|
+
const request = store.get(key);
|
|
3607
|
+
request.onsuccess = () => {
|
|
3608
|
+
const record = request.result;
|
|
3609
|
+
resolve(record ? record.data : null);
|
|
3610
|
+
};
|
|
3611
|
+
request.onerror = () => resolve(null);
|
|
3612
|
+
});
|
|
3613
|
+
} catch {
|
|
3614
|
+
return null;
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
async set(key, data) {
|
|
3618
|
+
if (!data) return;
|
|
3619
|
+
try {
|
|
3620
|
+
const db = await this.getDB();
|
|
3621
|
+
if (!db) return;
|
|
3622
|
+
return new Promise((resolve) => {
|
|
3623
|
+
const tx = db.transaction(this.storeName, "readwrite");
|
|
3624
|
+
const store = tx.objectStore(this.storeName);
|
|
3625
|
+
const record = { key, data, timestamp: Date.now() };
|
|
3626
|
+
const request = store.put(record);
|
|
3627
|
+
request.onsuccess = () => resolve();
|
|
3628
|
+
request.onerror = () => resolve();
|
|
3629
|
+
});
|
|
3630
|
+
} catch {
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
async delete(key) {
|
|
3634
|
+
try {
|
|
3635
|
+
const db = await this.getDB();
|
|
3636
|
+
if (!db) return;
|
|
3637
|
+
return new Promise((resolve) => {
|
|
3638
|
+
const tx = db.transaction(this.storeName, "readwrite");
|
|
3639
|
+
const store = tx.objectStore(this.storeName);
|
|
3640
|
+
const request = store.delete(key);
|
|
3641
|
+
request.onsuccess = () => resolve();
|
|
3642
|
+
request.onerror = () => resolve();
|
|
3643
|
+
});
|
|
3644
|
+
} catch {
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
3647
|
+
async clear() {
|
|
3648
|
+
try {
|
|
3649
|
+
const db = await this.getDB();
|
|
3650
|
+
if (!db) return;
|
|
3651
|
+
return new Promise((resolve) => {
|
|
3652
|
+
const tx = db.transaction(this.storeName, "readwrite");
|
|
3653
|
+
const store = tx.objectStore(this.storeName);
|
|
3654
|
+
const request = store.clear();
|
|
3655
|
+
request.onsuccess = () => resolve();
|
|
3656
|
+
request.onerror = () => resolve();
|
|
3657
|
+
});
|
|
3658
|
+
} catch {
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
};
|
|
3662
|
+
async function fetchWithCache(cache, cacheKey, fetchFn, onFreshData) {
|
|
3663
|
+
const cached = await cache.get(cacheKey);
|
|
3664
|
+
const backgroundFetch = fetchFn().then(async (freshData) => {
|
|
3665
|
+
if (freshData !== null && freshData !== void 0) {
|
|
3666
|
+
await cache.set(cacheKey, freshData);
|
|
3667
|
+
if (onFreshData) {
|
|
3668
|
+
try {
|
|
3669
|
+
onFreshData(freshData);
|
|
3670
|
+
} catch (e) {
|
|
3671
|
+
console.warn("[Jellyfin Cache] Error in onFreshData callback for %s:", cacheKey, e);
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
return freshData;
|
|
3676
|
+
}).catch((err) => {
|
|
3677
|
+
console.warn("[Jellyfin Cache] Background fetch failed for %s:", cacheKey, err);
|
|
3678
|
+
if (!cached) throw err;
|
|
3679
|
+
return cached;
|
|
3680
|
+
});
|
|
3681
|
+
if (cached !== null && cached !== void 0) {
|
|
3682
|
+
return cached;
|
|
3683
|
+
}
|
|
3684
|
+
return await backgroundFetch;
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
// src/storage.ts
|
|
3688
|
+
var MemoryStorage = class {
|
|
3689
|
+
store = /* @__PURE__ */ new Map();
|
|
3690
|
+
getItem(key) {
|
|
3691
|
+
return this.store.has(key) ? this.store.get(key) : null;
|
|
3692
|
+
}
|
|
3693
|
+
setItem(key, value) {
|
|
3694
|
+
this.store.set(key, value);
|
|
3695
|
+
}
|
|
3696
|
+
removeItem(key) {
|
|
3697
|
+
this.store.delete(key);
|
|
3698
|
+
}
|
|
3699
|
+
clear() {
|
|
3700
|
+
this.store.clear();
|
|
3701
|
+
}
|
|
3702
|
+
};
|
|
3703
|
+
var LocalStorageAdapter = class {
|
|
3704
|
+
getItem(key) {
|
|
3705
|
+
if (typeof localStorage !== "undefined") {
|
|
3706
|
+
return localStorage.getItem(key);
|
|
3707
|
+
}
|
|
3708
|
+
return null;
|
|
3709
|
+
}
|
|
3710
|
+
setItem(key, value) {
|
|
3711
|
+
if (typeof localStorage !== "undefined") {
|
|
3712
|
+
localStorage.setItem(key, value);
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
removeItem(key) {
|
|
3716
|
+
if (typeof localStorage !== "undefined") {
|
|
3717
|
+
localStorage.removeItem(key);
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
};
|
|
3721
|
+
function getDefaultStorage() {
|
|
3722
|
+
if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") {
|
|
3723
|
+
return new LocalStorageAdapter();
|
|
2396
3724
|
}
|
|
2397
3725
|
return new MemoryStorage();
|
|
2398
3726
|
}
|
|
@@ -2421,6 +3749,14 @@ var JellyfinClient = class {
|
|
|
2421
3749
|
nextUp;
|
|
2422
3750
|
latest;
|
|
2423
3751
|
transcode;
|
|
3752
|
+
websocket;
|
|
3753
|
+
sessions;
|
|
3754
|
+
quickConnect;
|
|
3755
|
+
trickplay;
|
|
3756
|
+
chapters;
|
|
3757
|
+
genres;
|
|
3758
|
+
collections;
|
|
3759
|
+
displayPreferences;
|
|
2424
3760
|
http;
|
|
2425
3761
|
storage;
|
|
2426
3762
|
cache;
|
|
@@ -2431,7 +3767,7 @@ var JellyfinClient = class {
|
|
|
2431
3767
|
this.cache = new MemoryCacheAdapter();
|
|
2432
3768
|
this.clientInfo = {
|
|
2433
3769
|
name: options.clientInfo?.name || "@francofantomius/jellyfin",
|
|
2434
|
-
version: options.clientInfo?.version || "0.
|
|
3770
|
+
version: options.clientInfo?.version || "0.4.1",
|
|
2435
3771
|
device: options.clientInfo?.device || (typeof window !== "undefined" ? "Web Browser" : "Node.js"),
|
|
2436
3772
|
deviceId: options.clientInfo?.deviceId || generateDeviceId()
|
|
2437
3773
|
};
|
|
@@ -2466,6 +3802,14 @@ var JellyfinClient = class {
|
|
|
2466
3802
|
this.nextUp = new NextUpModule(this.http, getUserId);
|
|
2467
3803
|
this.latest = new LatestModule(this.http, getUserId);
|
|
2468
3804
|
this.transcode = new TranscodeModule(this.http, getDeviceId);
|
|
3805
|
+
this.websocket = new WebSocketModule(this.http, getDeviceId);
|
|
3806
|
+
this.sessions = new SessionsModule(this.http, getUserId);
|
|
3807
|
+
this.quickConnect = new QuickConnectModule(this.http);
|
|
3808
|
+
this.trickplay = new TrickplayModule(this.http);
|
|
3809
|
+
this.chapters = new ChaptersModule(this.http, getUserId);
|
|
3810
|
+
this.genres = new GenresModule(this.http, getUserId);
|
|
3811
|
+
this.collections = new CollectionsModule(this.http, getUserId);
|
|
3812
|
+
this.displayPreferences = new DisplayPreferencesModule(this.http, getUserId, this.clientInfo.name);
|
|
2469
3813
|
}
|
|
2470
3814
|
// --- Configuration Getters & Setters ---
|
|
2471
3815
|
get serverUrl() {
|
|
@@ -2567,6 +3911,9 @@ var JellyfinClient = class {
|
|
|
2567
3911
|
* Log out and clear tokens and session.
|
|
2568
3912
|
*/
|
|
2569
3913
|
logout() {
|
|
3914
|
+
if (this.websocket.isConnected) {
|
|
3915
|
+
this.websocket.disconnect();
|
|
3916
|
+
}
|
|
2570
3917
|
this.auth.logout();
|
|
2571
3918
|
this.emit("logout");
|
|
2572
3919
|
}
|
|
@@ -2585,7 +3932,11 @@ var JellyfinClient = class {
|
|
|
2585
3932
|
exports.AUDIO_QUALITY_PRESETS = AUDIO_QUALITY_PRESETS;
|
|
2586
3933
|
exports.AuthModule = AuthModule;
|
|
2587
3934
|
exports.COMMON_SEARCH_FIELDS = COMMON_SEARCH_FIELDS;
|
|
3935
|
+
exports.ChaptersModule = ChaptersModule;
|
|
3936
|
+
exports.CollectionsModule = CollectionsModule;
|
|
3937
|
+
exports.DisplayPreferencesModule = DisplayPreferencesModule;
|
|
2588
3938
|
exports.FavoritesModule = FavoritesModule;
|
|
3939
|
+
exports.GenresModule = GenresModule;
|
|
2589
3940
|
exports.HttpTransport = HttpTransport;
|
|
2590
3941
|
exports.IndexedDBCacheAdapter = IndexedDBCacheAdapter;
|
|
2591
3942
|
exports.JellyfinApiError = JellyfinApiError;
|
|
@@ -2603,11 +3954,15 @@ exports.NextUpModule = NextUpModule;
|
|
|
2603
3954
|
exports.OfflineStorageManager = OfflineStorageManager;
|
|
2604
3955
|
exports.PlaybackModule = PlaybackModule;
|
|
2605
3956
|
exports.PlaylistsModule = PlaylistsModule;
|
|
3957
|
+
exports.QuickConnectModule = QuickConnectModule;
|
|
2606
3958
|
exports.ResumeModule = ResumeModule;
|
|
2607
3959
|
exports.SearchModule = SearchModule;
|
|
3960
|
+
exports.SessionsModule = SessionsModule;
|
|
2608
3961
|
exports.SystemModule = SystemModule;
|
|
2609
3962
|
exports.TranscodeModule = TranscodeModule;
|
|
3963
|
+
exports.TrickplayModule = TrickplayModule;
|
|
2610
3964
|
exports.VIDEO_QUALITY_PRESETS = VIDEO_QUALITY_PRESETS;
|
|
3965
|
+
exports.WebSocketModule = WebSocketModule;
|
|
2611
3966
|
exports.buildAuthHeader = buildAuthHeader;
|
|
2612
3967
|
exports.cleanUrl = cleanUrl;
|
|
2613
3968
|
exports.fetchWithCache = fetchWithCache;
|