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