├── wakeonlan.js ├── serverdiscovery.js ├── appstorage-memory.js ├── bower.json ├── appstorage-cache.js ├── README.md ├── LICENSE.md ├── events.js ├── appstorage-localstorage.js ├── credentials.js ├── connectionmanager.js └── apiclient.js /wakeonlan.js: -------------------------------------------------------------------------------- 1 | /* jshint module: true */ 2 | 3 | function send(info) { 4 | 5 | return Promise.reject(); 6 | } 7 | 8 | function isSupported() { 9 | return false; 10 | } 11 | 12 | export default { 13 | send, 14 | isSupported 15 | }; -------------------------------------------------------------------------------- /serverdiscovery.js: -------------------------------------------------------------------------------- 1 | /* jshint module: true */ 2 | 3 | export default { 4 | 5 | findServers(timeoutMs) { 6 | 7 | // Expected server properties 8 | // Name, Id, Address, EndpointAddress (optional) 9 | return Promise.resolve([]); 10 | } 11 | }; -------------------------------------------------------------------------------- /appstorage-memory.js: -------------------------------------------------------------------------------- 1 | /* jshint module: true */ 2 | 3 | export default class MyStore { 4 | constructor() { 5 | 6 | this.localData = {}; 7 | } 8 | 9 | setItem(name, value) { 10 | this.localData[name] = value; 11 | } 12 | 13 | getItem(name) { 14 | return this.localData[name]; 15 | } 16 | 17 | removeItem(name) { 18 | this.localData[name] = null; 19 | } 20 | } -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "emby-apiclient", 3 | "main": "apiclient.js", 4 | "authors": [ 5 | "The Emby Authors" 6 | ], 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/MediaBrowser/Emby.ApiClient.Javascript.git" 10 | }, 11 | "license": "https://github.com/MediaBrowser/Emby.ApiClient.Javascript/blob/master/LICENSE", 12 | "homepage": "https://github.com/MediaBrowser/Emby.ApiClient.Javascript", 13 | "dependencies": { 14 | 15 | }, 16 | "devDependencies": { 17 | 18 | }, 19 | "ignore": [] 20 | } 21 | -------------------------------------------------------------------------------- /appstorage-cache.js: -------------------------------------------------------------------------------- 1 | /* jshint module: true */ 2 | 3 | function updateCache(instance) { 4 | instance.cache.put('data', new Response(JSON.stringify(instance.localData))); 5 | } 6 | 7 | export default class MyStore { 8 | init() { 9 | 10 | const instance = this; 11 | return caches.open('embydata').then(result => { 12 | instance.cache = result; 13 | instance.localData = {}; 14 | }); 15 | } 16 | 17 | setItem(name, value) { 18 | if (this.localData) { 19 | const changed = this.localData[name] !== value; 20 | 21 | if (changed) { 22 | this.localData[name] = value; 23 | updateCache(this); 24 | } 25 | } 26 | } 27 | 28 | getItem(name) { 29 | if (this.localData) { 30 | return this.localData[name]; 31 | } 32 | } 33 | 34 | removeItem(name) { 35 | if (this.localData) { 36 | this.localData[name] = null; 37 | delete this.localData[name]; 38 | updateCache(this); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > ### This repository is outdated and archived! 3 | > You can find up-to-date client libraries in the new [Emby.ApiClient Repository](https://github.com/MediaBrowser/Emby.ApiClients/tree/master) 4 | 5 | --- 6 | 7 | Emby.ApiClient.Javascript 8 | ================================= 9 | 10 | # Usage # 11 | 12 | This is a port of the [Java version](https://github.com/MediaBrowser/Emby.ApiClient.Java "Java version"). Until this is fully documented it is best to refer to it for API usage as the signatures are closely aligned. 13 | 14 | # Notes # 15 | 16 | - this library depends on the native fetch and promise api's. These will be expected to be polyfilled if used in a browser that doesn't support them. 17 | 18 | # Examples # 19 | 20 | This is a port of the [Java version](https://github.com/MediaBrowser/Emby.ApiClient.Java "Java version"). Until this is fully documented it is best to refer to it for API usage as the signatures are closely aligned. 21 | 22 | # Emby Mobile App # 23 | 24 | A new mobile app for Emby is in development and is built with this library: 25 | 26 | https://github.com/MediaBrowser/MediaBrowser.Mobile 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) Emby https://emby.media 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /events.js: -------------------------------------------------------------------------------- 1 | /* jshint module: true */ 2 | 3 | function getCallbacks(obj, name) { 4 | 5 | if (!obj) { 6 | throw new Error("obj cannot be null!"); 7 | } 8 | 9 | obj._callbacks = obj._callbacks || {}; 10 | 11 | let list = obj._callbacks[name]; 12 | 13 | if (!list) { 14 | obj._callbacks[name] = []; 15 | list = obj._callbacks[name]; 16 | } 17 | 18 | return list; 19 | } 20 | 21 | export default { 22 | 23 | on(obj, eventName, fn) { 24 | 25 | const list = getCallbacks(obj, eventName); 26 | 27 | list.push(fn); 28 | }, 29 | 30 | off(obj, eventName, fn) { 31 | 32 | const list = getCallbacks(obj, eventName); 33 | 34 | const i = list.indexOf(fn); 35 | if (i !== -1) { 36 | list.splice(i, 1); 37 | } 38 | }, 39 | 40 | trigger(obj, eventName) { 41 | 42 | const eventObject = { 43 | type: eventName 44 | }; 45 | 46 | const eventArgs = []; 47 | eventArgs.push(eventObject); 48 | 49 | const additionalArgs = arguments[2] || []; 50 | let i, length; 51 | for (i = 0, length = additionalArgs.length; i < length; i++) { 52 | eventArgs.push(additionalArgs[i]); 53 | } 54 | 55 | const callbacks = getCallbacks(obj, eventName).slice(0); 56 | 57 | for (i = 0, length = callbacks.length; i < length; i++) { 58 | 59 | callbacks[i].apply(obj, eventArgs); 60 | } 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /appstorage-localstorage.js: -------------------------------------------------------------------------------- 1 | /* jshint module: true */ 2 | 3 | function onCachePutFail(e) { 4 | console.log(e); 5 | } 6 | 7 | function updateCache(instance) { 8 | 9 | const cache = instance.cache; 10 | 11 | if (cache) { 12 | cache.put('data', new Response(JSON.stringify(instance.localData))).catch(onCachePutFail); 13 | } 14 | } 15 | 16 | function onCacheOpened(result) { 17 | this.cache = result; 18 | this.localData = {}; 19 | } 20 | 21 | export default class MyStore { 22 | constructor() { 23 | 24 | try { 25 | 26 | if (self.caches) { 27 | 28 | caches.open('embydata').then(onCacheOpened.bind(this)); 29 | } 30 | 31 | } catch (err) { 32 | console.log(`Error opening cache: ${err}`); 33 | } 34 | 35 | } 36 | 37 | setItem(name, value) { 38 | localStorage.setItem(name, value); 39 | 40 | const localData = this.localData; 41 | 42 | if (localData) { 43 | const changed = localData[name] !== value; 44 | 45 | if (changed) { 46 | localData[name] = value; 47 | updateCache(this); 48 | } 49 | } 50 | } 51 | 52 | getItem(name) { 53 | return localStorage.getItem(name); 54 | } 55 | 56 | removeItem(name) { 57 | localStorage.removeItem(name); 58 | 59 | const localData = this.localData; 60 | 61 | if (localData) { 62 | localData[name] = null; 63 | delete localData[name]; 64 | updateCache(this); 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /credentials.js: -------------------------------------------------------------------------------- 1 | /* jshint module: true */ 2 | 3 | import events from './events.js'; 4 | 5 | function ensure(instance, data) { 6 | if (!instance._credentials) { 7 | const json = instance.appStorage.getItem(instance.key) || '{}'; 8 | 9 | console.log(`credentials initialized with: ${json}`); 10 | instance._credentials = JSON.parse(json); 11 | instance._credentials.Servers = instance._credentials.Servers || []; 12 | } 13 | } 14 | 15 | function set(instance, data) { 16 | instance._credentials = data; 17 | const json = JSON.stringify(data); 18 | instance.appStorage.setItem(instance.key, json); 19 | 20 | events.trigger(instance, 'credentialsupdated', [{ 21 | credentials: data, 22 | credentialsJson: json 23 | }]); 24 | } 25 | 26 | export default class Credentials { 27 | constructor(appStorage, key) { 28 | this.key = key || 'servercredentials3'; 29 | this.appStorage = appStorage; 30 | } 31 | 32 | clear() { 33 | this._credentials = null; 34 | this.appStorage.removeItem(this.key); 35 | } 36 | 37 | credentials(data) { 38 | if (data) { 39 | set(this, data); 40 | } 41 | 42 | ensure(this); 43 | return this._credentials; 44 | } 45 | 46 | addOrUpdateServer(list, server) { 47 | if (!server.Id) { 48 | throw new Error('Server.Id cannot be null or empty'); 49 | } 50 | 51 | const existing = list.filter(({ Id }) => Id === server.Id)[0]; 52 | 53 | if (existing) { 54 | // Merge the data 55 | existing.DateLastAccessed = Math.max( 56 | existing.DateLastAccessed || 0, 57 | server.DateLastAccessed || 0 58 | ); 59 | 60 | if (server.AccessToken) { 61 | existing.AccessToken = server.AccessToken; 62 | existing.UserId = server.UserId; 63 | } 64 | if (server.ExchangeToken) { 65 | existing.ExchangeToken = server.ExchangeToken; 66 | } 67 | if (server.RemoteAddress) { 68 | existing.RemoteAddress = server.RemoteAddress; 69 | } 70 | if (server.ManualAddress) { 71 | existing.ManualAddress = server.ManualAddress; 72 | } 73 | if (server.LocalAddress) { 74 | existing.LocalAddress = server.LocalAddress; 75 | } 76 | if (server.Name) { 77 | existing.Name = server.Name; 78 | } 79 | if (server.WakeOnLanInfos && server.WakeOnLanInfos.length) { 80 | existing.WakeOnLanInfos = server.WakeOnLanInfos; 81 | } 82 | if (server.LastConnectionMode != null) { 83 | existing.LastConnectionMode = server.LastConnectionMode; 84 | } 85 | if (server.ConnectServerId) { 86 | existing.ConnectServerId = server.ConnectServerId; 87 | } 88 | 89 | return existing; 90 | } else { 91 | list.push(server); 92 | return server; 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /connectionmanager.js: -------------------------------------------------------------------------------- 1 | /* jshint module: true */ 2 | 3 | import events from './events.js'; 4 | 5 | const defaultTimeout = 20000; 6 | 7 | const ConnectionMode = { 8 | Local: 0, 9 | Remote: 1, 10 | Manual: 2 11 | }; 12 | 13 | function getServerAddress(server, mode) { 14 | 15 | switch (mode) { 16 | case ConnectionMode.Local: 17 | return server.LocalAddress; 18 | case ConnectionMode.Manual: 19 | return server.ManualAddress; 20 | case ConnectionMode.Remote: 21 | return server.RemoteAddress; 22 | default: 23 | return server.ManualAddress || server.LocalAddress || server.RemoteAddress; 24 | } 25 | } 26 | 27 | function paramsToString(params) { 28 | 29 | const values = []; 30 | 31 | for (const key in params) { 32 | 33 | const value = params[key]; 34 | 35 | if (value !== null && value !== undefined && value !== '') { 36 | values.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`); 37 | } 38 | } 39 | return values.join('&'); 40 | } 41 | 42 | function mergeServers(credentialProvider, list1, list2) { 43 | 44 | for (let i = 0, length = list2.length; i < length; i++) { 45 | credentialProvider.addOrUpdateServer(list1, list2[i]); 46 | } 47 | 48 | return list1; 49 | } 50 | 51 | function updateServerInfo(server, systemInfo) { 52 | 53 | if (systemInfo.ServerName) { 54 | server.Name = systemInfo.ServerName; 55 | } 56 | if (systemInfo.Id) { 57 | server.Id = systemInfo.Id; 58 | } 59 | if (systemInfo.LocalAddress) { 60 | server.LocalAddress = systemInfo.LocalAddress; 61 | } 62 | if (systemInfo.WanAddress) { 63 | server.RemoteAddress = systemInfo.WanAddress; 64 | } 65 | } 66 | 67 | function getFetchPromise(request, signal) { 68 | 69 | const headers = request.headers || {}; 70 | 71 | if (request.dataType === 'json') { 72 | headers.accept = 'application/json'; 73 | } 74 | 75 | const fetchRequest = { 76 | headers: headers, 77 | method: request.type, 78 | credentials: 'same-origin' 79 | }; 80 | 81 | if (request.timeout) { 82 | 83 | const abortController = new AbortController(); 84 | 85 | const boundAbort = abortController.abort.bind(abortController); 86 | 87 | if (signal) { 88 | signal.addEventListener('abort', boundAbort); 89 | } 90 | 91 | setTimeout(boundAbort, request.timeout); 92 | 93 | signal = abortController.signal; 94 | } 95 | 96 | if (signal) { 97 | fetchRequest.signal = signal; 98 | } 99 | 100 | let contentType = request.contentType; 101 | 102 | if (request.data) { 103 | 104 | if (typeof request.data === 'string') { 105 | fetchRequest.body = request.data; 106 | } else { 107 | fetchRequest.body = paramsToString(request.data); 108 | 109 | contentType = contentType || 'application/x-www-form-urlencoded; charset=UTF-8'; 110 | } 111 | } 112 | 113 | if (contentType) { 114 | 115 | headers['Content-Type'] = contentType; 116 | } 117 | 118 | return fetch(request.url, fetchRequest); 119 | } 120 | 121 | function sortServers(a, b) { 122 | return (b.DateLastAccessed || 0) - (a.DateLastAccessed || 0); 123 | } 124 | 125 | function setServerProperties(server) { 126 | 127 | // These are convenience properties for the UI 128 | server.Type = 'Server'; 129 | } 130 | 131 | function ajax(request, signal) { 132 | 133 | if (!request) { 134 | throw new Error("Request cannot be null"); 135 | } 136 | 137 | request.headers = request.headers || {}; 138 | 139 | console.log(`ConnectionManager requesting url: ${request.url}`); 140 | 141 | return getFetchPromise(request, signal).then(response => { 142 | 143 | console.log(`ConnectionManager response status: ${response.status}, url: ${request.url}`); 144 | 145 | if (response.status < 400) { 146 | 147 | if (request.dataType === 'json') { 148 | return response.json(); 149 | } else if (request.dataType === 'text') { 150 | return response.text(); 151 | } else if (request.headers.accept === 'application/json') { 152 | return response.json(); 153 | } else { 154 | return response; 155 | } 156 | } else { 157 | return Promise.reject(response); 158 | } 159 | 160 | }); 161 | } 162 | 163 | function getConnectUrl(handler) { 164 | return `https://connect.emby.media/service/${handler}`; 165 | } 166 | 167 | function replaceAll(originalString, strReplace, strWith) { 168 | const reg = new RegExp(strReplace, 'ig'); 169 | return originalString.replace(reg, strWith); 170 | } 171 | 172 | function normalizeAddress(address) { 173 | 174 | // attempt to correct bad input 175 | address = address.trim(); 176 | 177 | if (address.toLowerCase().indexOf('http') !== 0) { 178 | address = `http://${address}`; 179 | } 180 | 181 | // Seeing failures in iOS when protocol isn't lowercase 182 | address = replaceAll(address, 'Http:', 'http:'); 183 | address = replaceAll(address, 'Https:', 'https:'); 184 | 185 | return address; 186 | } 187 | 188 | function convertEndpointAddressToManualAddress(info) { 189 | 190 | if (info.Address && info.EndpointAddress) { 191 | let address = info.EndpointAddress.split(":")[0]; 192 | 193 | // Determine the port, if any 194 | const parts = info.Address.split(":"); 195 | if (parts.length > 1) { 196 | const portString = parts[parts.length - 1]; 197 | 198 | if (!isNaN(parseInt(portString))) { 199 | address += `:${portString}`; 200 | } 201 | } 202 | 203 | return normalizeAddress(address); 204 | } 205 | 206 | return null; 207 | } 208 | 209 | function filterServers(servers, connectServers) { 210 | 211 | return servers.filter(server => { 212 | 213 | // It's not a connect server, so assume it's still valid 214 | if (!server.ExchangeToken) { 215 | return true; 216 | } 217 | 218 | return connectServers.filter(connectServer => server.Id === connectServer.Id).length > 0; 219 | }); 220 | } 221 | 222 | function stringEqualsIgnoreCase(str1, str2) { 223 | 224 | return (str1 || '').toLowerCase() === (str2 || '').toLowerCase(); 225 | } 226 | 227 | function compareVersions(a, b) { 228 | 229 | // -1 a is smaller 230 | // 1 a is larger 231 | // 0 equal 232 | a = a.split('.'); 233 | b = b.split('.'); 234 | 235 | for (let i = 0, length = Math.max(a.length, b.length); i < length; i++) { 236 | const aVal = parseInt(a[i] || '0'); 237 | const bVal = parseInt(b[i] || '0'); 238 | 239 | if (aVal < bVal) { 240 | return -1; 241 | } 242 | 243 | if (aVal > bVal) { 244 | return 1; 245 | } 246 | } 247 | 248 | return 0; 249 | } 250 | 251 | function onCredentialsSaved(e, data) { 252 | 253 | events.trigger(this, 'credentialsupdated', [data]); 254 | } 255 | 256 | function onUserDataUpdated(userData) { 257 | 258 | const obj = this; 259 | const instance = obj.instance; 260 | const itemId = obj.itemId; 261 | const userId = obj.userId; 262 | 263 | userData.ItemId = itemId; 264 | 265 | events.trigger(instance, 'message', [{ 266 | 267 | MessageType: 'UserDataChanged', 268 | Data: { 269 | UserId: userId, 270 | UserDataList: [ 271 | userData 272 | ] 273 | } 274 | 275 | }]); 276 | } 277 | 278 | function setTimeoutPromise(timeout) { 279 | 280 | return new Promise(function (resolve, reject) { 281 | 282 | setTimeout(resolve, timeout); 283 | }); 284 | } 285 | 286 | function addAppInfoToConnectRequest(instance, request) { 287 | request.headers = request.headers || {}; 288 | request.headers['X-Application'] = `${instance.appName()}/${instance.appVersion()}`; 289 | } 290 | 291 | function exchangePinInternal(instance, pinInfo) { 292 | 293 | if (!pinInfo) { 294 | throw new Error('pinInfo cannot be null'); 295 | } 296 | 297 | const request = { 298 | type: 'POST', 299 | url: getConnectUrl('pin/authenticate'), 300 | data: { 301 | deviceId: pinInfo.DeviceId, 302 | pin: pinInfo.Pin 303 | }, 304 | dataType: 'json' 305 | }; 306 | 307 | addAppInfoToConnectRequest(instance, request); 308 | 309 | return ajax(request); 310 | } 311 | 312 | function getCacheKey(feature, apiClient, options = {}) { 313 | const viewOnly = options.viewOnly; 314 | 315 | let cacheKey = `regInfo-${apiClient.serverId()}`; 316 | 317 | if (viewOnly) { 318 | cacheKey += '-viewonly'; 319 | } 320 | 321 | return cacheKey; 322 | } 323 | 324 | function allowAddress(instance, address) { 325 | 326 | if (instance.rejectInsecureAddresses) { 327 | 328 | if (address.indexOf('https:') !== 0) { 329 | return false; 330 | } 331 | } 332 | 333 | return true; 334 | } 335 | 336 | function getConnectUser(instance, userId, accessToken) { 337 | 338 | if (!userId) { 339 | throw new Error("null userId"); 340 | } 341 | if (!accessToken) { 342 | throw new Error("null accessToken"); 343 | } 344 | 345 | const url = `https://connect.emby.media/service/user?id=${userId}`; 346 | 347 | return ajax({ 348 | type: "GET", 349 | url, 350 | dataType: "json", 351 | headers: { 352 | "X-Application": `${instance.appName()}/${instance.appVersion()}`, 353 | "X-Connect-UserToken": accessToken 354 | } 355 | 356 | }); 357 | } 358 | 359 | function onConnectUserSignIn(instance, user) { 360 | 361 | instance._connectUser = user; 362 | events.trigger(instance, 'connectusersignedin', [user]); 363 | } 364 | 365 | function ensureConnectUser(instance, credentials) { 366 | 367 | const connectUser = instance.connectUser(); 368 | 369 | if (connectUser && connectUser.Id === credentials.ConnectUserId) { 370 | return Promise.resolve(); 371 | } 372 | 373 | else if (credentials.ConnectUserId && credentials.ConnectAccessToken) { 374 | 375 | instance._connectUser = null; 376 | 377 | return getConnectUser(instance, credentials.ConnectUserId, credentials.ConnectAccessToken).then(user => { 378 | 379 | onConnectUserSignIn(instance, user); 380 | return Promise.resolve(); 381 | 382 | }, () => Promise.resolve()); 383 | 384 | } else { 385 | return Promise.resolve(); 386 | } 387 | } 388 | 389 | function validateAuthentication(instance, server, serverUrl) { 390 | 391 | console.log('connectionManager.validateAuthentication: ' + serverUrl); 392 | 393 | return ajax({ 394 | 395 | type: "GET", 396 | url: instance.getEmbyServerUrl(serverUrl, "System/Info"), 397 | dataType: "json", 398 | headers: { 399 | "X-MediaBrowser-Token": server.AccessToken 400 | } 401 | 402 | }).then(systemInfo => { 403 | 404 | updateServerInfo(server, systemInfo); 405 | return systemInfo; 406 | 407 | }, () => { 408 | 409 | server.UserId = null; 410 | server.AccessToken = null; 411 | return Promise.resolve(); 412 | }); 413 | } 414 | 415 | function findServers(serverDiscoveryFn) { 416 | 417 | const onFinish = function (foundServers) { 418 | const servers = foundServers.map(function (foundServer) { 419 | 420 | const info = { 421 | Id: foundServer.Id, 422 | LocalAddress: convertEndpointAddressToManualAddress(foundServer) || foundServer.Address, 423 | Name: foundServer.Name 424 | }; 425 | 426 | info.LastConnectionMode = info.ManualAddress ? ConnectionMode.Manual : ConnectionMode.Local; 427 | 428 | return info; 429 | }); 430 | return servers; 431 | }; 432 | 433 | return serverDiscoveryFn().then(serverDiscovery => { 434 | return serverDiscovery.findServers(1000).then(onFinish, () => { 435 | return onFinish([]); 436 | }); 437 | }); 438 | } 439 | 440 | function onAuthenticated(apiClient, result) { 441 | 442 | const options = {}; 443 | 444 | const instance = this; 445 | 446 | const credentialProvider = instance.credentialProvider(); 447 | 448 | const credentials = credentialProvider.credentials(); 449 | const servers = credentials.Servers.filter(s => s.Id === result.ServerId); 450 | 451 | const server = servers.length ? servers[0] : apiClient.serverInfo(); 452 | 453 | if (options.updateDateLastAccessed !== false) { 454 | server.DateLastAccessed = Date.now(); 455 | } 456 | server.Id = result.ServerId; 457 | 458 | server.UserId = result.User.Id; 459 | server.AccessToken = result.AccessToken; 460 | 461 | credentialProvider.addOrUpdateServer(credentials.Servers, server); 462 | credentialProvider.credentials(credentials); 463 | 464 | // set this now before updating server info, otherwise it won't be set in time 465 | apiClient.enableAutomaticBitrateDetection = options.enableAutomaticBitrateDetection; 466 | 467 | apiClient.serverInfo(server); 468 | afterConnected(instance, apiClient, options); 469 | 470 | return apiClient.getPublicSystemInfo().then(function (systemInfo) { 471 | 472 | updateServerInfo(server, systemInfo); 473 | credentialProvider.addOrUpdateServer(credentials.Servers, server); 474 | credentialProvider.credentials(credentials); 475 | 476 | return onLocalUserSignIn(instance, server, apiClient.serverAddress()); 477 | }); 478 | } 479 | 480 | function reportCapabilities(instance, apiClient) { 481 | 482 | return instance.capabilities().then(function (capabilities) { 483 | return apiClient.reportCapabilities(capabilities); 484 | }); 485 | } 486 | 487 | function afterConnected(instance, apiClient, options = {}) { 488 | if (options.reportCapabilities !== false) { 489 | reportCapabilities(instance, apiClient); 490 | } 491 | apiClient.enableAutomaticBitrateDetection = options.enableAutomaticBitrateDetection; 492 | apiClient.enableWebSocketAutoConnect = options.enableWebSocket !== false; 493 | 494 | if (apiClient.enableWebSocketAutoConnect) { 495 | console.log('calling apiClient.ensureWebSocket'); 496 | 497 | apiClient.connected = true; 498 | apiClient.ensureWebSocket(); 499 | } 500 | } 501 | 502 | function onLocalUserSignIn(instance, server, serverUrl) { 503 | 504 | // Ensure this is created so that listeners of the event can get the apiClient instance 505 | instance._getOrAddApiClient(server, serverUrl); 506 | 507 | // This allows the app to have a single hook that fires before any other 508 | const promise = instance.onLocalUserSignedIn ? instance.onLocalUserSignedIn.call(instance, server.Id, server.UserId) : Promise.resolve(); 509 | 510 | return promise.then(() => { 511 | events.trigger(instance, 'localusersignedin', [server.Id, server.UserId]); 512 | }); 513 | } 514 | 515 | function addAuthenticationInfoFromConnect(instance, server, systemInfo, serverUrl, credentials) { 516 | 517 | if (!server.ExchangeToken) { 518 | throw new Error("server.ExchangeToken cannot be null"); 519 | } 520 | if (!credentials.ConnectUserId) { 521 | throw new Error("credentials.ConnectUserId cannot be null"); 522 | } 523 | 524 | const url = instance.getEmbyServerUrl(serverUrl, `Connect/Exchange?format=json&ConnectUserId=${credentials.ConnectUserId}`); 525 | 526 | const headers = { 527 | "X-Emby-Token": server.ExchangeToken 528 | }; 529 | 530 | const appName = instance.appName(); 531 | const appVersion = instance.appVersion(); 532 | const deviceName = instance.deviceName(); 533 | const deviceId = instance.deviceId(); 534 | 535 | if (compareVersions(systemInfo.Version, '4.4.0.21') >= 0) { 536 | 537 | if (appName) { 538 | headers['X-Emby-Client'] = appName; 539 | } 540 | 541 | if (deviceName) { 542 | headers['X-Emby-Device-Name'] = encodeURIComponent(deviceName); 543 | } 544 | 545 | if (deviceId) { 546 | headers['X-Emby-Device-Id'] = deviceId; 547 | } 548 | 549 | if (appVersion) { 550 | headers['X-Emby-Client-Version'] = appVersion; 551 | } 552 | } 553 | else { 554 | headers["X-Emby-Authorization"] = 'MediaBrowser Client="' + appName + '", Device="' + encodeURIComponent(deviceName) + '", DeviceId="' + deviceId + '", Version="' + appVersion + '"'; 555 | } 556 | 557 | return ajax({ 558 | type: "GET", 559 | url: url, 560 | dataType: "json", 561 | headers: headers 562 | 563 | }).then(auth => { 564 | 565 | server.UserId = auth.LocalUserId; 566 | server.AccessToken = auth.AccessToken; 567 | return auth; 568 | 569 | }, () => { 570 | 571 | server.UserId = null; 572 | server.AccessToken = null; 573 | return Promise.reject(); 574 | 575 | }); 576 | } 577 | 578 | function logoutOfServer(instance, apiClient) { 579 | 580 | const serverInfo = apiClient.serverInfo() || {}; 581 | 582 | const logoutInfo = { 583 | serverId: serverInfo.Id 584 | }; 585 | 586 | return apiClient.logout().then(() => { 587 | 588 | events.trigger(instance, 'localusersignedout', [logoutInfo]); 589 | }, () => { 590 | 591 | events.trigger(instance, 'localusersignedout', [logoutInfo]); 592 | }); 593 | } 594 | 595 | function getConnectServers(instance, credentials) { 596 | 597 | console.log('Begin getConnectServers'); 598 | 599 | if (!credentials.ConnectAccessToken || !credentials.ConnectUserId) { 600 | return Promise.resolve([]); 601 | } 602 | 603 | const url = `https://connect.emby.media/service/servers?userId=${credentials.ConnectUserId}`; 604 | 605 | return ajax({ 606 | type: "GET", 607 | url, 608 | dataType: "json", 609 | headers: { 610 | "X-Application": `${instance.appName()}/${instance.appVersion()}`, 611 | "X-Connect-UserToken": credentials.ConnectAccessToken 612 | } 613 | 614 | }).then(servers => servers.map(i => ({ 615 | ExchangeToken: i.AccessKey, 616 | ConnectServerId: i.Id, 617 | Id: i.SystemId, 618 | Name: i.Name, 619 | RemoteAddress: i.Url, 620 | LocalAddress: i.LocalAddres 621 | 622 | })), () => credentials.Servers.slice(0).filter(s => s.ExchangeToken)); 623 | } 624 | 625 | function tryReconnectToUrl(instance, url, connectionMode, delay, signal) { 626 | 627 | console.log('tryReconnectToUrl: ' + url); 628 | 629 | return setTimeoutPromise(delay).then(() => { 630 | 631 | return ajax({ 632 | 633 | url: instance.getEmbyServerUrl(url, 'system/info/public'), 634 | timeout: defaultTimeout, 635 | type: 'GET', 636 | dataType: 'json' 637 | 638 | }, signal).then((result) => { 639 | 640 | return { 641 | url: url, 642 | connectionMode: connectionMode, 643 | data: result 644 | }; 645 | }); 646 | }); 647 | } 648 | 649 | function tryReconnect(instance, serverInfo, signal) { 650 | 651 | const addresses = []; 652 | const addressesStrings = []; 653 | 654 | // the timeouts are a small hack to try and ensure the remote address doesn't resolve first 655 | 656 | // manualAddressOnly is used for the local web app that always connects to a fixed address 657 | if (!serverInfo.manualAddressOnly && serverInfo.LocalAddress && addressesStrings.indexOf(serverInfo.LocalAddress) === -1 && allowAddress(instance, serverInfo.LocalAddress)) { 658 | addresses.push({ url: serverInfo.LocalAddress, mode: ConnectionMode.Local, timeout: 0 }); 659 | addressesStrings.push(addresses[addresses.length - 1].url); 660 | } 661 | if (serverInfo.ManualAddress && addressesStrings.indexOf(serverInfo.ManualAddress) === -1 && allowAddress(instance, serverInfo.ManualAddress)) { 662 | addresses.push({ url: serverInfo.ManualAddress, mode: ConnectionMode.Manual, timeout: 100 }); 663 | addressesStrings.push(addresses[addresses.length - 1].url); 664 | } 665 | if (!serverInfo.manualAddressOnly && serverInfo.RemoteAddress && addressesStrings.indexOf(serverInfo.RemoteAddress) === -1 && allowAddress(instance, serverInfo.RemoteAddress)) { 666 | addresses.push({ url: serverInfo.RemoteAddress, mode: ConnectionMode.Remote, timeout: 200 }); 667 | addressesStrings.push(addresses[addresses.length - 1].url); 668 | } 669 | 670 | console.log('tryReconnect: ' + addressesStrings.join('|')); 671 | 672 | if (!addressesStrings.length) { 673 | return Promise.reject(); 674 | } 675 | 676 | const promises = []; 677 | 678 | for (let i = 0, length = addresses.length; i < length; i++) { 679 | 680 | promises.push(tryReconnectToUrl(instance, addresses[i].url, addresses[i].mode, addresses[i].timeout, signal)); 681 | } 682 | 683 | return Promise.any(promises); 684 | } 685 | 686 | function afterConnectValidated( 687 | instance, 688 | server, 689 | credentials, 690 | systemInfo, 691 | connectionMode, 692 | serverUrl, 693 | verifyLocalAuthentication, 694 | options) { 695 | 696 | console.log('connectionManager.afterConnectValidated: ' + serverUrl); 697 | 698 | options = options || {}; 699 | 700 | if (verifyLocalAuthentication && server.AccessToken) { 701 | 702 | return validateAuthentication(instance, server, serverUrl).then((fullSystemInfo) => { 703 | 704 | return afterConnectValidated(instance, server, credentials, fullSystemInfo || systemInfo, connectionMode, serverUrl, false, options); 705 | }); 706 | } 707 | 708 | updateServerInfo(server, systemInfo); 709 | 710 | server.LastConnectionMode = connectionMode; 711 | 712 | if (options.updateDateLastAccessed !== false) { 713 | server.DateLastAccessed = Date.now(); 714 | } 715 | 716 | const credentialProvider = instance.credentialProvider(); 717 | 718 | credentialProvider.addOrUpdateServer(credentials.Servers, server); 719 | credentialProvider.credentials(credentials); 720 | 721 | const result = { 722 | Servers: [] 723 | }; 724 | 725 | result.ApiClient = instance._getOrAddApiClient(server, serverUrl); 726 | 727 | result.ApiClient.setSystemInfo(systemInfo); 728 | 729 | result.State = server.AccessToken && options.enableAutoLogin !== false ? 730 | 'SignedIn' : 731 | 'ServerSignIn'; 732 | 733 | result.Servers.push(server); 734 | 735 | // set this now before updating server info, otherwise it won't be set in time 736 | result.ApiClient.enableAutomaticBitrateDetection = options.enableAutomaticBitrateDetection; 737 | 738 | result.ApiClient.updateServerInfo(server, serverUrl); 739 | 740 | const resolveActions = function () { 741 | 742 | events.trigger(instance, 'connected', [result]); 743 | 744 | return Promise.resolve(result); 745 | }; 746 | 747 | console.log('connectionManager.afterConnectValidated result.State: ' + (result.State || '')); 748 | 749 | if (result.State === 'SignedIn') { 750 | afterConnected(instance, result.ApiClient, options); 751 | 752 | return onLocalUserSignIn(instance, server, serverUrl).then(resolveActions, resolveActions); 753 | } 754 | else { 755 | return resolveActions(); 756 | } 757 | } 758 | 759 | function onSuccessfulConnection(instance, server, systemInfo, connectionMode, serverUrl, options) { 760 | 761 | console.log('connectionManager.onSuccessfulConnection: ' + serverUrl); 762 | 763 | const credentials = instance.credentialProvider().credentials(); 764 | options = options || {}; 765 | if (credentials.ConnectAccessToken && options.enableAutoLogin !== false) { 766 | 767 | return ensureConnectUser(instance, credentials).then(() => { 768 | 769 | if (server.ExchangeToken) { 770 | return addAuthenticationInfoFromConnect(instance, server, systemInfo, serverUrl, credentials).then(() => { 771 | 772 | return afterConnectValidated(instance, server, credentials, systemInfo, connectionMode, serverUrl, true, options); 773 | 774 | }, () => { 775 | 776 | return afterConnectValidated(instance, server, credentials, systemInfo, connectionMode, serverUrl, true, options); 777 | }); 778 | 779 | } else { 780 | 781 | return afterConnectValidated(instance, server, credentials, systemInfo, connectionMode, serverUrl, true, options); 782 | } 783 | }); 784 | } 785 | else { 786 | return afterConnectValidated(instance, server, credentials, systemInfo, connectionMode, serverUrl, true, options); 787 | } 788 | } 789 | 790 | function resolveIfAvailable(instance, url, server, result, connectionMode, options) { 791 | 792 | console.log('connectionManager.resolveIfAvailable: ' + url); 793 | 794 | const promise = instance.validateServerAddress ? instance.validateServerAddress(instance, ajax, url) : Promise.resolve(); 795 | 796 | return promise.then(() => { 797 | return onSuccessfulConnection(instance, server, result, connectionMode, url, options); 798 | }, () => { 799 | console.log('minServerVersion requirement not met. Server version: ' + result.Version); 800 | return { 801 | State: 'ServerUpdateNeeded', 802 | Servers: [server] 803 | }; 804 | }); 805 | } 806 | 807 | export default class ConnectionManager { 808 | constructor( 809 | credentialProvider, 810 | appStorage, 811 | apiClientFactory, 812 | serverDiscoveryFn, 813 | wakeOnLan, 814 | appName, 815 | appVersion, 816 | deviceName, 817 | deviceId, 818 | capabilitiesFn, 819 | devicePixelRatio, 820 | localassetmanager, 821 | itemrepository, 822 | useractionrepository) { 823 | 824 | if (!appName) { 825 | throw new Error("Must supply a appName"); 826 | } 827 | if (!appVersion) { 828 | throw new Error("Must supply a appVersion"); 829 | } 830 | if (!deviceName) { 831 | throw new Error("Must supply a deviceName"); 832 | } 833 | if (!deviceId) { 834 | throw new Error("Must supply a deviceId"); 835 | } 836 | 837 | console.log('Begin ConnectionManager constructor'); 838 | 839 | events.on(credentialProvider, 'credentialsupdated', onCredentialsSaved.bind(this)); 840 | 841 | this.appStorage = appStorage; 842 | this._credentialProvider = credentialProvider; 843 | 844 | this._apiClients = []; 845 | this._apiClientsMap = {}; 846 | 847 | this._minServerVersion = '4.1.1'; 848 | 849 | this._appName = appName; 850 | this._appVersion = appVersion; 851 | this._deviceName = deviceName; 852 | this._deviceId = deviceId; 853 | 854 | this.capabilities = capabilitiesFn; 855 | 856 | this.apiClientFactory = apiClientFactory; 857 | this.wakeOnLan = wakeOnLan; 858 | this.serverDiscoveryFn = serverDiscoveryFn; 859 | this.devicePixelRatio = devicePixelRatio; 860 | this.localassetmanager = localassetmanager; 861 | this.itemrepository = itemrepository; 862 | this.useractionrepository = useractionrepository; 863 | } 864 | 865 | appName() { 866 | return this._appName; 867 | } 868 | 869 | appVersion() { 870 | return this._appVersion; 871 | } 872 | 873 | deviceName() { 874 | return this._deviceName; 875 | } 876 | 877 | deviceId() { 878 | return this._deviceId; 879 | } 880 | 881 | minServerVersion(val) { 882 | 883 | if (val) { 884 | this._minServerVersion = val; 885 | } 886 | 887 | return this._minServerVersion; 888 | } 889 | 890 | connectUser() { 891 | return this._connectUser; 892 | } 893 | 894 | credentialProvider() { 895 | return this._credentialProvider; 896 | } 897 | 898 | connectUserId() { 899 | return this.credentialProvider().credentials().ConnectUserId; 900 | } 901 | 902 | connectToken() { 903 | return this.credentialProvider().credentials().ConnectAccessToken; 904 | } 905 | 906 | getServerInfo(id) { 907 | 908 | const servers = this.credentialProvider().credentials().Servers; 909 | 910 | return servers.filter(s => s.Id === id)[0]; 911 | } 912 | 913 | getLastUsedServer() { 914 | 915 | const servers = this.credentialProvider().credentials().Servers; 916 | 917 | servers.sort(sortServers); 918 | 919 | if (!servers.length) { 920 | return null; 921 | } 922 | 923 | return servers[0]; 924 | } 925 | 926 | addApiClient(apiClient, isOnlyServer) { 927 | 928 | this._apiClients.push(apiClient); 929 | 930 | const credentialProvider = this.credentialProvider(); 931 | 932 | const currentServers = credentialProvider.credentials().Servers; 933 | const existingServers = currentServers.filter(function (s) { 934 | 935 | return stringEqualsIgnoreCase(s.ManualAddress, apiClient.serverAddress()) || 936 | stringEqualsIgnoreCase(s.LocalAddress, apiClient.serverAddress()) || 937 | stringEqualsIgnoreCase(s.RemoteAddress, apiClient.serverAddress()); 938 | 939 | }); 940 | 941 | const existingServer = existingServers.length ? existingServers[0] : apiClient.serverInfo(); 942 | existingServer.DateLastAccessed = Date.now(); 943 | existingServer.LastConnectionMode = ConnectionMode.Manual; 944 | existingServer.ManualAddress = apiClient.serverAddress(); 945 | 946 | if (apiClient.manualAddressOnly) { 947 | existingServer.manualAddressOnly = true; 948 | } 949 | 950 | apiClient.serverInfo(existingServer); 951 | if (existingServer.Id) { 952 | this._apiClientsMap[existingServer.Id] = apiClient; 953 | } 954 | 955 | apiClient.onAuthenticated = onAuthenticated.bind(this); 956 | 957 | if (!existingServers.length || isOnlyServer) { 958 | const credentials = credentialProvider.credentials(); 959 | credentials.Servers = [existingServer]; 960 | credentialProvider.credentials(credentials); 961 | } 962 | 963 | events.trigger(this, 'apiclientcreated', [apiClient]); 964 | } 965 | 966 | clearData() { 967 | 968 | console.log('connection manager clearing data'); 969 | 970 | this._connectUser = null; 971 | const credentialProvider = this.credentialProvider(); 972 | const credentials = credentialProvider.credentials(); 973 | credentials.ConnectAccessToken = null; 974 | credentials.ConnectUserId = null; 975 | credentials.Servers = []; 976 | credentialProvider.credentials(credentials); 977 | } 978 | 979 | _getOrAddApiClient(server, serverUrl) { 980 | 981 | let apiClient = this.getApiClient(server.Id); 982 | 983 | if (!apiClient) { 984 | 985 | const ApiClient = this.apiClientFactory; 986 | 987 | apiClient = new ApiClient(this.appStorage, 988 | this.wakeOnLan, 989 | serverUrl, 990 | this.appName(), 991 | this.appVersion(), 992 | this.deviceName(), 993 | this.deviceId(), 994 | this.devicePixelRatio, 995 | this.localassetmanager, 996 | this.itemrepository, 997 | this.useractionrepository); 998 | 999 | apiClient.rejectInsecureAddresses = this.rejectInsecureAddresses; 1000 | 1001 | this._apiClients.push(apiClient); 1002 | 1003 | apiClient.serverInfo(server); 1004 | 1005 | apiClient.onAuthenticated = onAuthenticated.bind(this); 1006 | 1007 | events.trigger(this, 'apiclientcreated', [apiClient]); 1008 | } 1009 | 1010 | console.log('returning instance from getOrAddApiClient'); 1011 | return apiClient; 1012 | } 1013 | 1014 | getOrCreateApiClient(serverId) { 1015 | 1016 | const credentials = this.credentialProvider().credentials(); 1017 | const servers = credentials.Servers.filter(s => stringEqualsIgnoreCase(s.Id, serverId)); 1018 | 1019 | if (!servers.length) { 1020 | throw new Error(`Server not found: ${serverId}`); 1021 | } 1022 | 1023 | const server = servers[0]; 1024 | 1025 | return this._getOrAddApiClient(server, getServerAddress(server, server.LastConnectionMode)); 1026 | } 1027 | 1028 | logout() { 1029 | 1030 | console.log('begin connectionManager loguot'); 1031 | const promises = []; 1032 | 1033 | for (let i = 0, length = this._apiClients.length; i < length; i++) { 1034 | 1035 | const apiClient = this._apiClients[i]; 1036 | 1037 | if (apiClient.accessToken()) { 1038 | promises.push(logoutOfServer(this, apiClient)); 1039 | } 1040 | } 1041 | 1042 | const instance = this; 1043 | 1044 | return Promise.all(promises).then(() => { 1045 | 1046 | const credentialProvider = instance.credentialProvider(); 1047 | 1048 | const credentials = credentialProvider.credentials(); 1049 | 1050 | const servers = credentials.Servers; 1051 | 1052 | for (let j = 0, numServers = servers.length; j < numServers; j++) { 1053 | 1054 | const server = servers[j]; 1055 | 1056 | server.UserId = null; 1057 | server.AccessToken = null; 1058 | server.ExchangeToken = null; 1059 | } 1060 | 1061 | credentials.Servers = servers; 1062 | credentials.ConnectAccessToken = null; 1063 | credentials.ConnectUserId = null; 1064 | 1065 | credentialProvider.credentials(credentials); 1066 | 1067 | if (instance._connectUser) { 1068 | instance._connectUser = null; 1069 | events.trigger(instance, 'connectusersignedout'); 1070 | } 1071 | }); 1072 | } 1073 | 1074 | getSavedServers() { 1075 | 1076 | const credentialProvider = this.credentialProvider(); 1077 | 1078 | const credentials = credentialProvider.credentials(); 1079 | 1080 | const servers = credentials.Servers.slice(0); 1081 | 1082 | servers.forEach(setServerProperties); 1083 | servers.sort(sortServers); 1084 | 1085 | return servers; 1086 | } 1087 | 1088 | getAvailableServers() { 1089 | 1090 | console.log('Begin getAvailableServers'); 1091 | 1092 | const credentialProvider = this.credentialProvider(); 1093 | 1094 | // Clone the array 1095 | const credentials = credentialProvider.credentials(); 1096 | 1097 | return Promise.all([getConnectServers(this, credentials), findServers(this.serverDiscoveryFn)]).then(responses => { 1098 | 1099 | const connectServers = responses[0]; 1100 | const foundServers = responses[1]; 1101 | 1102 | let servers = credentials.Servers.slice(0); 1103 | mergeServers(credentialProvider, servers, foundServers); 1104 | mergeServers(credentialProvider, servers, connectServers); 1105 | 1106 | servers = filterServers(servers, connectServers); 1107 | 1108 | servers.forEach(setServerProperties); 1109 | servers.sort(sortServers); 1110 | 1111 | credentials.Servers = servers; 1112 | 1113 | credentialProvider.credentials(credentials); 1114 | 1115 | return servers; 1116 | }); 1117 | } 1118 | 1119 | connectToServers(servers, options) { 1120 | 1121 | console.log(`Begin connectToServers, with ${servers.length} servers`); 1122 | 1123 | const firstServer = servers.length ? servers[0] : null; 1124 | // See if we have any saved credentials and can auto sign in 1125 | if (firstServer) { 1126 | return this.connectToServer(firstServer, options).then((result) => { 1127 | 1128 | if (result.State === 'Unavailable') { 1129 | 1130 | result.State = 'ServerSelection'; 1131 | } 1132 | 1133 | console.log('resolving connectToServers with result.State: ' + result.State); 1134 | return result; 1135 | }); 1136 | } 1137 | 1138 | return Promise.resolve({ 1139 | Servers: servers, 1140 | State: (!servers.length && !this.connectUser()) ? 'ConnectSignIn' : 'ServerSelection', 1141 | ConnectUser: this.connectUser() 1142 | }); 1143 | } 1144 | 1145 | connectToServer(server, options) { 1146 | 1147 | console.log('begin connectToServer'); 1148 | 1149 | options = options || {}; 1150 | 1151 | const instance = this; 1152 | 1153 | return tryReconnect(this, server).then((result) => { 1154 | 1155 | const serverUrl = result.url; 1156 | const connectionMode = result.connectionMode; 1157 | result = result.data; 1158 | 1159 | if (compareVersions(instance.minServerVersion(), result.Version) === 1 || 1160 | compareVersions(result.Version, '8.0') === 1) { 1161 | 1162 | console.log('minServerVersion requirement not met. Server version: ' + result.Version); 1163 | return { 1164 | State: 'ServerUpdateNeeded', 1165 | Servers: [server] 1166 | }; 1167 | 1168 | } 1169 | else { 1170 | 1171 | if (server.Id && result.Id !== server.Id && instance.validateServerIds !== false) { 1172 | server = { 1173 | Id: result.Id, 1174 | ManualAddress: serverUrl 1175 | }; 1176 | updateServerInfo(server, result); 1177 | } 1178 | 1179 | return resolveIfAvailable(instance, serverUrl, server, result, connectionMode, options); 1180 | } 1181 | 1182 | }, function () { 1183 | 1184 | return { 1185 | State: 'Unavailable', 1186 | ConnectUser: instance.connectUser() 1187 | }; 1188 | }); 1189 | } 1190 | 1191 | connectToAddress(address, options) { 1192 | 1193 | if (!address) { 1194 | return Promise.reject(); 1195 | } 1196 | 1197 | address = normalizeAddress(address); 1198 | const instance = this; 1199 | 1200 | function onFail() { 1201 | console.log(`connectToAddress ${address} failed`); 1202 | return Promise.resolve({ 1203 | State: 'Unavailable', 1204 | ConnectUser: instance.connectUser() 1205 | }); 1206 | } 1207 | 1208 | const server = { 1209 | ManualAddress: address, 1210 | LastConnectionMode: ConnectionMode.Manual 1211 | }; 1212 | 1213 | return this.connectToServer(server, options).catch(onFail); 1214 | } 1215 | 1216 | loginToConnect(username, password) { 1217 | 1218 | if (!username) { 1219 | return Promise.reject(); 1220 | } 1221 | if (!password) { 1222 | return Promise.reject(); 1223 | } 1224 | 1225 | const credentialProvider = this.credentialProvider(); 1226 | const instance = this; 1227 | 1228 | return ajax({ 1229 | type: "POST", 1230 | url: "https://connect.emby.media/service/user/authenticate", 1231 | data: { 1232 | nameOrEmail: username, 1233 | rawpw: password 1234 | }, 1235 | dataType: "json", 1236 | contentType: 'application/x-www-form-urlencoded; charset=UTF-8', 1237 | headers: { 1238 | "X-Application": `${this.appName()}/${this.appVersion()}` 1239 | } 1240 | 1241 | }).then(result => { 1242 | 1243 | const credentials = credentialProvider.credentials(); 1244 | 1245 | credentials.ConnectAccessToken = result.AccessToken; 1246 | credentials.ConnectUserId = result.User.Id; 1247 | 1248 | credentialProvider.credentials(credentials); 1249 | 1250 | onConnectUserSignIn(instance, result.User); 1251 | 1252 | return result; 1253 | }); 1254 | } 1255 | 1256 | signupForConnect(options) { 1257 | 1258 | const email = options.email; 1259 | const username = options.username; 1260 | const password = options.password; 1261 | const passwordConfirm = options.passwordConfirm; 1262 | 1263 | if (!email) { 1264 | return Promise.reject({ errorCode: 'invalidinput' }); 1265 | } 1266 | if (!username) { 1267 | return Promise.reject({ errorCode: 'invalidinput' }); 1268 | } 1269 | if (!password) { 1270 | return Promise.reject({ errorCode: 'invalidinput' }); 1271 | } 1272 | if (!passwordConfirm) { 1273 | return Promise.reject({ errorCode: 'passwordmatch' }); 1274 | } 1275 | if (password !== passwordConfirm) { 1276 | return Promise.reject({ errorCode: 'passwordmatch' }); 1277 | } 1278 | 1279 | const data = { 1280 | email, 1281 | userName: username, 1282 | rawpw: password 1283 | }; 1284 | 1285 | if (options.grecaptcha) { 1286 | data.grecaptcha = options.grecaptcha; 1287 | } 1288 | 1289 | return ajax({ 1290 | type: "POST", 1291 | url: "https://connect.emby.media/service/register", 1292 | data, 1293 | dataType: "json", 1294 | contentType: 'application/x-www-form-urlencoded; charset=UTF-8', 1295 | headers: { 1296 | "X-Application": `${this.appName()}/${this.appVersion()}`, 1297 | "X-CONNECT-TOKEN": "CONNECT-REGISTER" 1298 | } 1299 | 1300 | }).catch(response => { 1301 | 1302 | return response.json(); 1303 | 1304 | }).then(result => { 1305 | if (result && result.Status) { 1306 | 1307 | if (result.Status === 'SUCCESS') { 1308 | return Promise.resolve(result); 1309 | } 1310 | return Promise.reject({ errorCode: result.Status }); 1311 | } else { 1312 | Promise.reject(); 1313 | } 1314 | }); 1315 | } 1316 | 1317 | getUserInvitations() { 1318 | 1319 | const connectToken = this.connectToken(); 1320 | 1321 | if (!connectToken) { 1322 | throw new Error("null connectToken"); 1323 | } 1324 | if (!this.connectUserId()) { 1325 | throw new Error("null connectUserId"); 1326 | } 1327 | 1328 | const url = `https://connect.emby.media/service/servers?userId=${this.connectUserId()}&status=Waiting`; 1329 | 1330 | return ajax({ 1331 | type: "GET", 1332 | url, 1333 | dataType: "json", 1334 | headers: { 1335 | "X-Connect-UserToken": connectToken, 1336 | "X-Application": `${this.appName()}/${this.appVersion()}` 1337 | } 1338 | 1339 | }); 1340 | } 1341 | 1342 | deleteServer(serverId) { 1343 | 1344 | if (!serverId) { 1345 | throw new Error("null serverId"); 1346 | } 1347 | 1348 | const credentialProvider = this.credentialProvider(); 1349 | let server = credentialProvider.credentials().Servers.filter(s => s.Id === serverId); 1350 | server = server.length ? server[0] : null; 1351 | 1352 | function onDone() { 1353 | const credentials = credentialProvider.credentials(); 1354 | 1355 | credentials.Servers = credentials.Servers.filter(s => s.Id !== serverId); 1356 | 1357 | credentialProvider.credentials(credentials); 1358 | return Promise.resolve(); 1359 | } 1360 | 1361 | if (!server.ConnectServerId) { 1362 | return onDone(); 1363 | } 1364 | 1365 | const connectToken = this.connectToken(); 1366 | const connectUserId = this.connectUserId(); 1367 | 1368 | if (!connectToken || !connectUserId) { 1369 | return onDone(); 1370 | } 1371 | 1372 | const url = `https://connect.emby.media/service/serverAuthorizations?serverId=${server.ConnectServerId}&userId=${connectUserId}`; 1373 | 1374 | return ajax({ 1375 | type: "DELETE", 1376 | url, 1377 | headers: { 1378 | "X-Connect-UserToken": connectToken, 1379 | "X-Application": `${this.appName()}/${this.appVersion()}` 1380 | } 1381 | 1382 | }).then(onDone, onDone); 1383 | } 1384 | 1385 | rejectServer(serverId) { 1386 | 1387 | const connectToken = this.connectToken(); 1388 | 1389 | if (!serverId) { 1390 | throw new Error("null serverId"); 1391 | } 1392 | if (!connectToken) { 1393 | throw new Error("null connectToken"); 1394 | } 1395 | if (!this.connectUserId()) { 1396 | throw new Error("null connectUserId"); 1397 | } 1398 | 1399 | const url = `https://connect.emby.media/service/serverAuthorizations?serverId=${serverId}&userId=${this.connectUserId()}`; 1400 | 1401 | return fetch(url, { 1402 | method: "DELETE", 1403 | headers: { 1404 | "X-Connect-UserToken": connectToken, 1405 | "X-Application": `${this.appName()}/${this.appVersion()}` 1406 | } 1407 | }); 1408 | } 1409 | 1410 | acceptServer(serverId) { 1411 | 1412 | const connectToken = this.connectToken(); 1413 | 1414 | if (!serverId) { 1415 | throw new Error("null serverId"); 1416 | } 1417 | if (!connectToken) { 1418 | throw new Error("null connectToken"); 1419 | } 1420 | if (!this.connectUserId()) { 1421 | throw new Error("null connectUserId"); 1422 | } 1423 | 1424 | const url = `https://connect.emby.media/service/ServerAuthorizations/accept?serverId=${serverId}&userId=${this.connectUserId()}`; 1425 | 1426 | return ajax({ 1427 | type: "GET", 1428 | url, 1429 | headers: { 1430 | "X-Connect-UserToken": connectToken, 1431 | "X-Application": `${this.appName()}/${this.appVersion()}` 1432 | } 1433 | 1434 | }); 1435 | } 1436 | 1437 | resetRegistrationInfo(apiClient) { 1438 | 1439 | let cacheKey = getCacheKey('themes', apiClient, { viewOnly: true }); 1440 | this.appStorage.removeItem(cacheKey); 1441 | 1442 | cacheKey = getCacheKey('themes', apiClient, { viewOnly: false }); 1443 | this.appStorage.removeItem(cacheKey); 1444 | 1445 | events.trigger(this, 'resetregistrationinfo'); 1446 | } 1447 | 1448 | getRegistrationInfo(feature, apiClient, options) { 1449 | 1450 | const params = { 1451 | serverId: apiClient.serverId(), 1452 | deviceId: this.deviceId(), 1453 | deviceName: this.deviceName(), 1454 | appName: this.appName(), 1455 | appVersion: this.appVersion(), 1456 | embyUserName: '' 1457 | }; 1458 | 1459 | options = options || {}; 1460 | 1461 | if (options.viewOnly) { 1462 | params.viewOnly = options.viewOnly; 1463 | } 1464 | 1465 | const cacheKey = getCacheKey(feature, apiClient, options); 1466 | 1467 | const regInfo = JSON.parse(this.appStorage.getItem(cacheKey) || '{}'); 1468 | 1469 | const timeSinceLastValidation = (Date.now() - (regInfo.lastValidDate || 0)); 1470 | 1471 | // Cache for 1 day 1472 | if (timeSinceLastValidation <= 86400000) { 1473 | console.log('getRegistrationInfo returning cached info'); 1474 | return Promise.resolve(); 1475 | } 1476 | 1477 | const regCacheValid = timeSinceLastValidation <= (regInfo.cacheExpirationDays || 7) * 86400000; 1478 | 1479 | const onFailure = err => { 1480 | console.log('getRegistrationInfo failed: ' + err); 1481 | 1482 | // Allow for up to 7 days 1483 | if (regCacheValid) { 1484 | 1485 | console.log('getRegistrationInfo returning cached info'); 1486 | return Promise.resolve(); 1487 | } 1488 | 1489 | throw err; 1490 | }; 1491 | 1492 | params.embyUserName = apiClient.getCurrentUserName(); 1493 | 1494 | const currentUserId = apiClient.getCurrentUserId(); 1495 | if (currentUserId && currentUserId.toLowerCase() === '81f53802ea0247ad80618f55d9b4ec3c' && params.serverId.toLowerCase() === '21585256623b4beeb26d5d3b09dec0ac') { 1496 | return Promise.reject(); 1497 | } 1498 | 1499 | const appStorage = this.appStorage; 1500 | 1501 | const getRegPromise = ajax({ 1502 | url: 'https://mb3admin.com/admin/service/registration/validateDevice?' + paramsToString(params), 1503 | type: 'POST', 1504 | dataType: 'json' 1505 | 1506 | }).then(response => { 1507 | 1508 | appStorage.setItem(cacheKey, JSON.stringify({ 1509 | lastValidDate: Date.now(), 1510 | deviceId: params.deviceId, 1511 | cacheExpirationDays: response.cacheExpirationDays 1512 | })); 1513 | return Promise.resolve(); 1514 | 1515 | }, response => { 1516 | 1517 | const status = (response || {}).status; 1518 | console.log('getRegistrationInfo response: ' + status); 1519 | 1520 | if (status === 403) { 1521 | return Promise.reject('overlimit'); 1522 | } 1523 | 1524 | if (status && status < 500) { 1525 | return Promise.reject(); 1526 | } 1527 | return onFailure(response); 1528 | }); 1529 | 1530 | if (regCacheValid) { 1531 | console.log('getRegistrationInfo returning cached info'); 1532 | return Promise.resolve(); 1533 | } 1534 | 1535 | return getRegPromise; 1536 | } 1537 | 1538 | createPin() { 1539 | 1540 | const request = { 1541 | type: 'POST', 1542 | url: getConnectUrl('pin'), 1543 | data: { 1544 | deviceId: this.deviceId() 1545 | }, 1546 | dataType: 'json' 1547 | }; 1548 | 1549 | addAppInfoToConnectRequest(this, request); 1550 | 1551 | return ajax(request); 1552 | } 1553 | 1554 | getPinStatus(pinInfo) { 1555 | 1556 | if (!pinInfo) { 1557 | throw new Error('pinInfo cannot be null'); 1558 | } 1559 | 1560 | const queryString = { 1561 | deviceId: pinInfo.DeviceId, 1562 | pin: pinInfo.Pin 1563 | }; 1564 | 1565 | const request = { 1566 | type: 'GET', 1567 | url: `${getConnectUrl('pin')}?${paramsToString(queryString)}`, 1568 | dataType: 'json' 1569 | }; 1570 | 1571 | addAppInfoToConnectRequest(this, request); 1572 | 1573 | return ajax(request); 1574 | } 1575 | 1576 | exchangePin(pinInfo) { 1577 | 1578 | if (!pinInfo) { 1579 | throw new Error('pinInfo cannot be null'); 1580 | } 1581 | 1582 | const credentialProvider = this.credentialProvider(); 1583 | 1584 | const instance = this; 1585 | 1586 | return exchangePinInternal(this, pinInfo).then(result => { 1587 | 1588 | const credentials = credentialProvider.credentials(); 1589 | credentials.ConnectAccessToken = result.AccessToken; 1590 | credentials.ConnectUserId = result.UserId; 1591 | credentialProvider.credentials(credentials); 1592 | 1593 | return ensureConnectUser(instance, credentials); 1594 | }); 1595 | } 1596 | 1597 | connect(options) { 1598 | 1599 | console.log('Begin connect'); 1600 | 1601 | const instance = this; 1602 | 1603 | return instance.getAvailableServers().then(servers => instance.connectToServers(servers, options)); 1604 | } 1605 | 1606 | handleMessageReceived(msg) { 1607 | 1608 | const serverId = msg.ServerId; 1609 | if (serverId) { 1610 | const apiClient = this.getApiClient(serverId); 1611 | if (apiClient) { 1612 | 1613 | if (typeof (msg.Data) === 'string') { 1614 | try { 1615 | msg.Data = JSON.parse(msg.Data); 1616 | } 1617 | catch (err) { 1618 | } 1619 | } 1620 | 1621 | apiClient.handleMessageReceived(msg); 1622 | } 1623 | } 1624 | } 1625 | 1626 | onNetworkChanged() { 1627 | 1628 | const apiClients = this._apiClients; 1629 | for (let i = 0, length = apiClients.length; i < length; i++) { 1630 | apiClients[i].onNetworkChanged(); 1631 | } 1632 | } 1633 | 1634 | onAppResume() { 1635 | 1636 | const apiClients = this._apiClients; 1637 | for (let i = 0, length = apiClients.length; i < length; i++) { 1638 | apiClients[i].ensureWebSocket(); 1639 | } 1640 | } 1641 | 1642 | isLoggedIntoConnect() { 1643 | 1644 | // Make sure it returns true or false 1645 | if (!this.connectToken() || !this.connectUserId()) { 1646 | return false; 1647 | } 1648 | return true; 1649 | } 1650 | 1651 | getApiClients() { 1652 | 1653 | const servers = this.getSavedServers(); 1654 | 1655 | for (let i = 0, length = servers.length; i < length; i++) { 1656 | const server = servers[i]; 1657 | if (server.Id) { 1658 | 1659 | const serverUrl = getServerAddress(server, server.LastConnectionMode); 1660 | if (serverUrl) { 1661 | this._getOrAddApiClient(server, serverUrl); 1662 | } 1663 | } 1664 | } 1665 | 1666 | return this._apiClients; 1667 | } 1668 | 1669 | getApiClient(item) { 1670 | 1671 | if (!item) { 1672 | throw new Error('item or serverId cannot be null'); 1673 | } 1674 | 1675 | let serverId = item.ServerId; 1676 | 1677 | // Accept string + object 1678 | 1679 | if (!serverId) { 1680 | if (item.Id && item.Type === 'Server') { 1681 | serverId = item.Id; 1682 | } else { 1683 | serverId = item; 1684 | } 1685 | } 1686 | 1687 | let apiClient; 1688 | 1689 | if (serverId) { 1690 | apiClient = this._apiClientsMap[serverId]; 1691 | if (apiClient) { 1692 | return apiClient; 1693 | } 1694 | } 1695 | 1696 | const apiClients = this._apiClients; 1697 | 1698 | for (let i = 0, length = apiClients.length; i < length; i++) { 1699 | 1700 | apiClient = apiClients[i]; 1701 | const serverInfo = apiClient.serverInfo(); 1702 | 1703 | // We have to keep this hack in here because of the addApiClient method 1704 | if (!serverInfo || serverInfo.Id === serverId) { 1705 | return apiClient; 1706 | } 1707 | } 1708 | 1709 | return null; 1710 | } 1711 | 1712 | getEmbyServerUrl(baseUrl, handler) { 1713 | 1714 | return `${baseUrl}/emby/${handler}`; 1715 | } 1716 | } -------------------------------------------------------------------------------- /apiclient.js: -------------------------------------------------------------------------------- 1 | /* jshint module: true */ 2 | 3 | import events from './events.js'; 4 | 5 | function replaceAll(originalString, strReplace, strWith) { 6 | const reg = new RegExp(strReplace, 'ig'); 7 | return originalString.replace(reg, strWith); 8 | } 9 | 10 | function paramsToString(params) { 11 | 12 | const values = []; 13 | 14 | for (const key in params) { 15 | 16 | const value = params[key]; 17 | 18 | if (value !== null && value !== undefined && value !== '') { 19 | values.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`); 20 | } 21 | } 22 | return values.join('&'); 23 | } 24 | 25 | function getFetchPromise(request, signal) { 26 | 27 | const headers = request.headers || {}; 28 | 29 | if (request.dataType === 'json') { 30 | headers.accept = 'application/json'; 31 | } 32 | 33 | const fetchRequest = { 34 | headers, 35 | method: request.type, 36 | credentials: 'same-origin' 37 | }; 38 | 39 | if (request.timeout) { 40 | 41 | const abortController = new AbortController(); 42 | 43 | const boundAbort = abortController.abort.bind(abortController); 44 | 45 | if (signal) { 46 | signal.addEventListener('abort', boundAbort); 47 | } 48 | 49 | setTimeout(boundAbort, request.timeout); 50 | 51 | signal = abortController.signal; 52 | } 53 | 54 | if (signal) { 55 | fetchRequest.signal = signal; 56 | } 57 | 58 | let contentType = request.contentType; 59 | 60 | if (request.data) { 61 | 62 | if (typeof request.data === 'string') { 63 | fetchRequest.body = request.data; 64 | } else { 65 | fetchRequest.body = paramsToString(request.data); 66 | 67 | contentType = contentType || 'application/x-www-form-urlencoded; charset=UTF-8'; 68 | } 69 | } 70 | 71 | if (contentType) { 72 | 73 | headers['Content-Type'] = contentType; 74 | } 75 | 76 | return fetch(request.url, fetchRequest); 77 | } 78 | 79 | function setSavedEndpointInfo(instance, info) { 80 | 81 | instance._endPointInfo = info; 82 | } 83 | 84 | function onNetworkChanged(instance, resetAddress) { 85 | 86 | if (resetAddress) { 87 | 88 | instance.connected = false; 89 | 90 | const serverInfo = instance.serverInfo(); 91 | const newAddress = getFirstValidAddress(instance, serverInfo); 92 | if (newAddress) { 93 | instance._serverAddress = newAddress; 94 | } 95 | } 96 | 97 | setSavedEndpointInfo(instance, null); 98 | } 99 | 100 | function getFirstValidAddress(instance, { LocalAddress, ManualAddress, RemoteAddress }) { 101 | 102 | if (LocalAddress && allowAddress(instance, LocalAddress)) { 103 | return LocalAddress; 104 | } 105 | if (ManualAddress && allowAddress(instance, ManualAddress)) { 106 | return ManualAddress; 107 | } 108 | if (RemoteAddress && allowAddress(instance, RemoteAddress)) { 109 | return RemoteAddress; 110 | } 111 | return null; 112 | } 113 | 114 | function tryReconnectToUrl(instance, url, delay, signal) { 115 | 116 | console.log(`tryReconnectToUrl: ${url}`); 117 | 118 | return setTimeoutPromise(delay).then(() => getFetchPromise({ 119 | 120 | url: instance.getUrl('system/info/public', null, url), 121 | type: 'GET', 122 | dataType: 'json', 123 | timeout: 15000 124 | 125 | }, signal).then(() => url)); 126 | } 127 | 128 | function allowAddress({ rejectInsecureAddresses }, address) { 129 | 130 | if (rejectInsecureAddresses) { 131 | 132 | if (address.indexOf('https:') !== 0) { 133 | return false; 134 | } 135 | } 136 | 137 | return true; 138 | } 139 | 140 | function setTimeoutPromise(timeout) { 141 | 142 | return new Promise((resolve, reject) => { 143 | 144 | setTimeout(resolve, timeout); 145 | }); 146 | } 147 | 148 | function tryReconnectInternal(instance, signal) { 149 | 150 | const addresses = []; 151 | const addressesStrings = []; 152 | 153 | const serverInfo = instance.serverInfo(); 154 | if (serverInfo.LocalAddress && !addressesStrings.includes(serverInfo.LocalAddress) && allowAddress(instance, serverInfo.LocalAddress)) { 155 | addresses.push({ url: serverInfo.LocalAddress, timeout: 0 }); 156 | addressesStrings.push(addresses[addresses.length - 1].url); 157 | } 158 | if (serverInfo.ManualAddress && !addressesStrings.includes(serverInfo.ManualAddress) && allowAddress(instance, serverInfo.ManualAddress)) { 159 | addresses.push({ url: serverInfo.ManualAddress, timeout: 100 }); 160 | addressesStrings.push(addresses[addresses.length - 1].url); 161 | } 162 | if (serverInfo.RemoteAddress && !addressesStrings.includes(serverInfo.RemoteAddress) && allowAddress(instance, serverInfo.RemoteAddress)) { 163 | addresses.push({ url: serverInfo.RemoteAddress, timeout: 200 }); 164 | addressesStrings.push(addresses[addresses.length - 1].url); 165 | } 166 | 167 | console.log(`tryReconnect: ${addressesStrings.join('|')}`); 168 | 169 | if (!addressesStrings.length) { 170 | return Promise.reject(); 171 | } 172 | 173 | const promises = []; 174 | 175 | for (let i = 0, length = addresses.length; i < length; i++) { 176 | 177 | promises.push(tryReconnectToUrl(instance, addresses[i].url, addresses[i].timeout, signal)); 178 | } 179 | 180 | return Promise.any(promises).then(url => { 181 | instance.serverAddress(url); 182 | return Promise.resolve(url); 183 | }); 184 | } 185 | 186 | function tryReconnect(instance, signal, retryCount = 0) { 187 | 188 | const promise = tryReconnectInternal(instance, signal); 189 | 190 | if (retryCount >= 2) { 191 | return promise; 192 | } 193 | 194 | return promise.catch(err => { 195 | 196 | console.log(`error in tryReconnectInternal: ${err || ''}`); 197 | 198 | return setTimeoutPromise(500).then(() => tryReconnect(instance, signal, retryCount + 1)); 199 | }); 200 | } 201 | 202 | function getUserCacheKey(userId, serverId) { 203 | const key = `user-${userId}-${serverId}`; 204 | 205 | return key; 206 | } 207 | 208 | function getCachedUser(instance, userId) { 209 | 210 | const serverId = instance.serverId(); 211 | if (!serverId) { 212 | return null; 213 | } 214 | 215 | const json = instance.appStorage.getItem(getUserCacheKey(userId, serverId)); 216 | 217 | if (json) { 218 | const user = JSON.parse(json); 219 | 220 | if (user) { 221 | setUserProperties(user); 222 | } 223 | 224 | return user; 225 | } 226 | 227 | return null; 228 | } 229 | 230 | function saveUserInCache({ appStorage }, user) { 231 | 232 | setUserProperties(user); 233 | user.DateLastFetched = Date.now(); 234 | appStorage.setItem(getUserCacheKey(user.Id, user.ServerId), JSON.stringify(user)); 235 | } 236 | 237 | function removeCachedUser({ appStorage }, userId, serverId) { 238 | appStorage.removeItem(getUserCacheKey(userId, serverId)); 239 | } 240 | 241 | function onWebSocketMessage(msg) { 242 | 243 | const instance = this; 244 | msg = JSON.parse(msg.data); 245 | onMessageReceivedInternal(instance, msg); 246 | } 247 | 248 | const messageIdsReceived = {}; 249 | 250 | function onMessageReceivedInternal(instance, msg) { 251 | 252 | const messageId = msg.MessageId; 253 | if (messageId) { 254 | 255 | // message was already received via another protocol 256 | if (messageIdsReceived[messageId]) { 257 | return; 258 | } 259 | 260 | messageIdsReceived[messageId] = true; 261 | } 262 | 263 | const msgType = msg.MessageType; 264 | 265 | if (msgType === "UserUpdated" || msgType === "UserConfigurationUpdated" || msgType === "UserPolicyUpdated") { 266 | 267 | const user = msg.Data; 268 | 269 | if (user.Id === instance.getCurrentUserId()) { 270 | 271 | saveUserInCache(instance, user); 272 | 273 | instance._userViewsPromise = null; 274 | } 275 | 276 | } else if (msgType === 'LibraryChanged') { 277 | 278 | // This might be a little aggressive improve this later 279 | instance._userViewsPromise = null; 280 | } 281 | 282 | events.trigger(instance, 'message', [msg]); 283 | } 284 | 285 | function onWebSocketOpen() { 286 | 287 | const instance = this; 288 | 289 | console.log('web socket connection opened'); 290 | events.trigger(instance, 'websocketopen'); 291 | 292 | let list = this.messageListeners; 293 | if (list) { 294 | list = list.slice(0); 295 | for (let i = 0, length = list.length; i < length; i++) { 296 | this.startMessageListener(list[i], "0,2000"); 297 | } 298 | } 299 | } 300 | 301 | function onWebSocketError() { 302 | 303 | const instance = this; 304 | events.trigger(instance, 'websocketerror'); 305 | } 306 | 307 | function setSocketOnClose(apiClient, socket) { 308 | 309 | socket.onclose = () => { 310 | 311 | console.log('web socket closed'); 312 | 313 | if (apiClient._webSocket === socket) { 314 | console.log('nulling out web socket'); 315 | apiClient._webSocket = null; 316 | } 317 | 318 | setTimeout(() => { 319 | events.trigger(apiClient, 'websocketclose'); 320 | }, 0); 321 | }; 322 | } 323 | 324 | function detectBitrateWithEndpointInfo(instance, { IsInNetwork }) { 325 | 326 | if (IsInNetwork) { 327 | 328 | return 140000000; 329 | } 330 | 331 | if (instance.getMaxBandwidth) { 332 | 333 | const maxRate = instance.getMaxBandwidth(); 334 | if (maxRate) { 335 | return maxRate; 336 | } 337 | } 338 | 339 | return 3000000; 340 | } 341 | 342 | function getRemoteImagePrefix(instance, options) { 343 | 344 | let urlPrefix; 345 | 346 | if (options.artist) { 347 | urlPrefix = `Artists/${instance.encodeName(options.artist)}`; 348 | delete options.artist; 349 | } else if (options.person) { 350 | urlPrefix = `Persons/${instance.encodeName(options.person)}`; 351 | delete options.person; 352 | } else if (options.genre) { 353 | urlPrefix = `Genres/${instance.encodeName(options.genre)}`; 354 | delete options.genre; 355 | } else if (options.musicGenre) { 356 | urlPrefix = `MusicGenres/${instance.encodeName(options.musicGenre)}`; 357 | delete options.musicGenre; 358 | } else if (options.gameGenre) { 359 | urlPrefix = `GameGenres/${instance.encodeName(options.gameGenre)}`; 360 | delete options.gameGenre; 361 | } else if (options.studio) { 362 | urlPrefix = `Studios/${instance.encodeName(options.studio)}`; 363 | delete options.studio; 364 | } else { 365 | urlPrefix = `Items/${options.itemId}`; 366 | delete options.itemId; 367 | } 368 | 369 | return urlPrefix; 370 | } 371 | 372 | function modifyEpgRow(result) { 373 | 374 | result.Type = 'EpgChannel'; 375 | result.ServerId = this.serverId(); 376 | } 377 | 378 | function modifyEpgResponse(result) { 379 | 380 | result.Items.forEach(modifyEpgRow.bind(this)); 381 | 382 | return result; 383 | } 384 | 385 | function mapVirtualFolder(item) { 386 | 387 | item.Type = 'VirtualFolder'; 388 | item.Id = item.ItemId; 389 | item.IsFolder = true; 390 | } 391 | 392 | function setUsersProperties(response) { 393 | 394 | response.forEach(setUserProperties); 395 | return Promise.resolve(response); 396 | } 397 | 398 | function setUserProperties(user) { 399 | user.Type = 'User'; 400 | } 401 | 402 | function setLogsProperties(response) { 403 | 404 | const serverId = this.serverId(); 405 | 406 | for (let i = 0, length = response.length; i < length; i++) { 407 | const log = response[i]; 408 | log.ServerId = serverId; 409 | log.Type = 'Log'; 410 | log.CanDownload = true; 411 | } 412 | 413 | return Promise.resolve(response); 414 | } 415 | 416 | function setApiKeysProperties(response) { 417 | 418 | const serverId = this.serverId(); 419 | 420 | for (let i = 0, length = response.Items.length; i < length; i++) { 421 | const log = response.Items[i]; 422 | log.ServerId = serverId; 423 | log.Type = 'ApiKey'; 424 | log.CanDelete = true; 425 | } 426 | 427 | return Promise.resolve(response); 428 | } 429 | 430 | function normalizeImageOptions({ _devicePixelRatio }, options) { 431 | 432 | const ratio = _devicePixelRatio || 1; 433 | 434 | if (ratio) { 435 | 436 | if (options.width) { 437 | options.width = Math.round(options.width * ratio); 438 | } 439 | if (options.height) { 440 | options.height = Math.round(options.height * ratio); 441 | } 442 | if (options.maxWidth) { 443 | options.maxWidth = Math.round(options.maxWidth * ratio); 444 | } 445 | if (options.maxHeight) { 446 | options.maxHeight = Math.round(options.maxHeight * ratio); 447 | } 448 | } 449 | 450 | if (!options.quality) { 451 | 452 | // TODO: In low bandwidth situations we could do 60/50 453 | if (options.type === 'Backdrop') { 454 | options.quality = 70; 455 | } else { 456 | options.quality = 90; 457 | } 458 | } 459 | } 460 | 461 | function fillServerIdIntoItems(result) { 462 | 463 | const serverId = this.serverId(); 464 | const items = result.Items || result; 465 | 466 | for (let i = 0, length = items.length; i < length; i++) { 467 | items[i].ServerId = serverId; 468 | } 469 | 470 | return result; 471 | } 472 | 473 | function fillTagProperties(result) { 474 | 475 | const serverId = this.serverId(); 476 | const items = result.Items || result; 477 | 478 | const type = 'Tag'; 479 | 480 | for (let i = 0, length = items.length; i < length; i++) { 481 | 482 | const item = items[i]; 483 | 484 | item.ServerId = serverId; 485 | item.Type = type; 486 | } 487 | 488 | return result; 489 | } 490 | 491 | function mapPrefix(i) { 492 | return { Name: i }; 493 | } 494 | 495 | let startingPlaySession = Date.now(); 496 | 497 | function onUserDataUpdated(userData) { 498 | 499 | const obj = this; 500 | const instance = obj.instance; 501 | const itemId = obj.itemId; 502 | const userId = obj.userId; 503 | 504 | userData.ItemId = itemId; 505 | 506 | events.trigger(instance, 'message', [{ 507 | 508 | MessageType: 'UserDataChanged', 509 | Data: { 510 | UserId: userId, 511 | UserDataList: [ 512 | userData 513 | ] 514 | } 515 | 516 | }]); 517 | } 518 | 519 | function getCachedWakeOnLanInfo(instance) { 520 | 521 | const serverId = instance.serverId(); 522 | const json = instance.appStorage.getItem(`server-${serverId}-wakeonlaninfo`); 523 | 524 | if (json) { 525 | return JSON.parse(json); 526 | } 527 | 528 | return []; 529 | } 530 | 531 | function refreshWakeOnLanInfoIfNeeded(instance) { 532 | 533 | if (!instance.wakeOnLan.isSupported()) { 534 | return; 535 | } 536 | 537 | // Re-using enableAutomaticBitrateDetection because it's set to false during background syncing 538 | // We can always have a dedicated option if needed 539 | if (instance.accessToken() && instance.enableAutomaticBitrateDetection !== false) { 540 | console.log('refreshWakeOnLanInfoIfNeeded'); 541 | setTimeout(refreshWakeOnLanInfo.bind(instance), 10000); 542 | } 543 | } 544 | 545 | function refreshWakeOnLanInfo() { 546 | 547 | const instance = this; 548 | 549 | console.log('refreshWakeOnLanInfo'); 550 | instance.getWakeOnLanInfo().then(info => { 551 | 552 | const serverId = instance.serverId(); 553 | instance.appStorage.setItem(`server-${serverId}-wakeonlaninfo`, JSON.stringify(info)); 554 | return info; 555 | 556 | }, err => // could be an older server that doesn't have this api 557 | []); 558 | } 559 | 560 | function sendNextWakeOnLan(wakeOnLan, infos, index) { 561 | 562 | if (index >= infos.length) { 563 | 564 | return Promise.resolve(); 565 | } 566 | 567 | const info = infos[index]; 568 | 569 | console.log(`sending wakeonlan to ${info.MacAddress}`); 570 | 571 | return wakeOnLan.send(info).then(result => sendNextWakeOnLan(wakeOnLan, infos, index + 1), () => sendNextWakeOnLan(wakeOnLan, infos, index + 1)); 572 | } 573 | 574 | function compareVersions(a, b) { 575 | 576 | // -1 a is smaller 577 | // 1 a is larger 578 | // 0 equal 579 | a = a.split('.'); 580 | b = b.split('.'); 581 | 582 | for (let i = 0, length = Math.max(a.length, b.length); i < length; i++) { 583 | const aVal = parseInt(a[i] || '0'); 584 | const bVal = parseInt(b[i] || '0'); 585 | 586 | if (aVal < bVal) { 587 | return -1; 588 | } 589 | 590 | if (aVal > bVal) { 591 | return 1; 592 | } 593 | } 594 | 595 | return 0; 596 | } 597 | 598 | 599 | /** 600 | * Creates a new api client instance 601 | * @param {String} serverAddress 602 | * @param {String} appName 603 | * @param {String} appVersion 604 | */ 605 | class ApiClient { 606 | constructor( 607 | appStorage, 608 | wakeOnLan, 609 | serverAddress, 610 | appName, 611 | appVersion, 612 | deviceName, 613 | deviceId, 614 | devicePixelRatio 615 | ) { 616 | 617 | if (!serverAddress) { 618 | throw new Error("Must supply a serverAddress"); 619 | } 620 | if (!appName) { 621 | throw new Error("Must supply a appName"); 622 | } 623 | if (!appVersion) { 624 | throw new Error("Must supply a appVersion"); 625 | } 626 | if (!deviceName) { 627 | throw new Error("Must supply a deviceName"); 628 | } 629 | if (!deviceId) { 630 | throw new Error("Must supply a deviceId"); 631 | } 632 | 633 | console.log(`ApiClient serverAddress: ${serverAddress}`); 634 | console.log(`ApiClient appName: ${appName}`); 635 | console.log(`ApiClient appVersion: ${appVersion}`); 636 | console.log(`ApiClient deviceName: ${deviceName}`); 637 | console.log(`ApiClient deviceId: ${deviceId}`); 638 | 639 | this.appStorage = appStorage; 640 | this.wakeOnLan = wakeOnLan; 641 | 642 | this._serverInfo = {}; 643 | this._serverAddress = serverAddress; 644 | this._deviceId = deviceId; 645 | this._deviceName = deviceName; 646 | this._appName = appName; 647 | this._appVersion = appVersion; 648 | this._devicePixelRatio = devicePixelRatio; 649 | } 650 | 651 | appName() { 652 | return this._appName; 653 | } 654 | 655 | setAuthorizationInfoIntoRequest(request, includeAccessToken) { 656 | 657 | const headers = request.headers; 658 | 659 | const currentServerInfo = this.serverInfo(); 660 | const appName = this._appName; 661 | const accessToken = currentServerInfo.AccessToken; 662 | 663 | const values = []; 664 | 665 | const queryStringAuth = this._queryStringAuth; 666 | const separateHeaderValues = this._separateHeaderValues; 667 | const authValues = queryStringAuth ? {} : (separateHeaderValues ? headers : null); 668 | 669 | if (appName) { 670 | if (authValues) { 671 | authValues['X-Emby-Client'] = appName; 672 | } else { 673 | values.push(`Client="${appName}"`); 674 | } 675 | } 676 | 677 | if (this._deviceName) { 678 | if (authValues) { 679 | authValues['X-Emby-Device-Name'] = queryStringAuth ? this._deviceName : encodeURIComponent(this._deviceName); 680 | } else { 681 | values.push(`Device="${encodeURIComponent(this._deviceName)}"`); 682 | } 683 | } 684 | 685 | if (this._deviceId) { 686 | if (authValues) { 687 | authValues['X-Emby-Device-Id'] = this._deviceId; 688 | } else { 689 | values.push(`DeviceId="${this._deviceId}"`); 690 | } 691 | } 692 | 693 | if (this._appVersion) { 694 | if (authValues) { 695 | authValues['X-Emby-Client-Version'] = this._appVersion; 696 | } else { 697 | values.push(`Version="${this._appVersion}"`); 698 | } 699 | } 700 | 701 | if (accessToken && includeAccessToken !== false) { 702 | if (authValues) { 703 | authValues['X-Emby-Token'] = accessToken; 704 | } else { 705 | values.push(`Token="${accessToken}"`); 706 | } 707 | } 708 | 709 | if (authValues) { 710 | if (queryStringAuth) { 711 | const queryParams = paramsToString(authValues); 712 | if (queryParams) { 713 | 714 | let url = request.url; 715 | 716 | url += !url.includes('?') ? '?' : '&'; 717 | url += queryParams; 718 | 719 | request.url = url; 720 | } 721 | } 722 | } 723 | else if (values.length) { 724 | 725 | const auth = `MediaBrowser ${values.join(', ')}`; 726 | headers['X-Emby-Authorization'] = auth; 727 | } 728 | } 729 | 730 | appVersion() { 731 | return this._appVersion; 732 | } 733 | 734 | deviceName() { 735 | return this._deviceName; 736 | } 737 | 738 | deviceId() { 739 | return this._deviceId; 740 | } 741 | 742 | /** 743 | * Gets the server address. 744 | */ 745 | serverAddress(val) { 746 | 747 | if (val != null) { 748 | 749 | if (val.toLowerCase().indexOf('http') !== 0) { 750 | throw new Error(`Invalid url: ${val}`); 751 | } 752 | 753 | this._serverAddress = val; 754 | 755 | onNetworkChanged(this); 756 | } 757 | 758 | return this._serverAddress; 759 | } 760 | 761 | onNetworkChanged() { 762 | 763 | onNetworkChanged(this, true); 764 | } 765 | 766 | /** 767 | * Creates an api url based on a handler name and query string parameters 768 | * @param {String} name 769 | * @param {Object} params 770 | */ 771 | getUrl(name, params, serverAddress) { 772 | 773 | if (!name) { 774 | throw new Error("Url name cannot be empty"); 775 | } 776 | 777 | let url = serverAddress || this._serverAddress; 778 | 779 | if (!url) { 780 | throw new Error("serverAddress is yet not set"); 781 | } 782 | const lowered = url.toLowerCase(); 783 | if (!lowered.includes('/emby') && !lowered.includes('/mediabrowser')) { 784 | url += '/emby'; 785 | } 786 | 787 | if (name.charAt(0) !== '/') { 788 | url += '/'; 789 | } 790 | 791 | url += name; 792 | 793 | if (params) { 794 | params = paramsToString(params); 795 | if (params) { 796 | url += `?${params}`; 797 | } 798 | } 799 | 800 | return url; 801 | } 802 | 803 | fetchWithFailover(request, enableReconnection, signal) { 804 | 805 | console.log(`apiclient.fetchWithFailover ${request.url}`); 806 | 807 | request.timeout = 30000; 808 | const instance = this; 809 | 810 | return getFetchPromise(request, signal).then(response => { 811 | 812 | instance.connected = true; 813 | 814 | if (response.status < 400) { 815 | 816 | if (request.dataType === 'json' || request.headers.accept === 'application/json') { 817 | return response.json(); 818 | } else if (request.dataType === 'text' || (response.headers.get('Content-Type') || '').toLowerCase().indexOf('text/') === 0) { 819 | return response.text(); 820 | } else { 821 | return response; 822 | } 823 | } else { 824 | return Promise.reject(response); 825 | } 826 | 827 | }, error => { 828 | 829 | if (!error) { 830 | console.log(`Request timed out to ${request.url}`); 831 | } 832 | else if (error.name === 'AbortError') { 833 | console.log(`AbortError: ${request.url}`); 834 | } 835 | else { 836 | console.log(`Request failed to ${request.url} ${error.status || ''} ${error.toString()}`); 837 | } 838 | 839 | // http://api.jquery.com/jQuery.ajax/ 840 | if ((!error || !error.status) && enableReconnection) { 841 | console.log("Attempting reconnection"); 842 | 843 | const previousServerAddress = instance.serverAddress(); 844 | 845 | return tryReconnect(instance, signal, null).then(newServerAddress => { 846 | 847 | console.log(`Reconnect succeeded to ${newServerAddress}`); 848 | instance.connected = true; 849 | 850 | if (instance.enableWebSocketAutoConnect) { 851 | instance.ensureWebSocket(); 852 | } 853 | 854 | request.url = request.url.replace(previousServerAddress, newServerAddress); 855 | 856 | console.log(`Retrying request with new url: ${request.url}`); 857 | 858 | return instance.fetchWithFailover(request, false, signal); 859 | }); 860 | 861 | } else { 862 | 863 | console.log("Reporting request failure"); 864 | 865 | throw error; 866 | } 867 | }); 868 | } 869 | 870 | /** 871 | * Wraps around jQuery ajax methods to add additional info to the request. 872 | */ 873 | fetch(request, includeAccessToken, signal) { 874 | 875 | if (!request) { 876 | throw new Error("Request cannot be null"); 877 | } 878 | 879 | request.headers = request.headers || {}; 880 | 881 | this.setAuthorizationInfoIntoRequest(request, includeAccessToken); 882 | 883 | if (this.enableAutomaticNetworking === false || request.type !== "GET") { 884 | 885 | return getFetchPromise(request, signal).then(response => { 886 | 887 | if (response.status < 400) { 888 | 889 | if (request.dataType === 'json' || request.headers.accept === 'application/json') { 890 | return response.json(); 891 | } else if (request.dataType === 'text' || (response.headers.get('Content-Type') || '').toLowerCase().indexOf('text/') === 0) { 892 | return response.text(); 893 | } else { 894 | return response; 895 | } 896 | } else { 897 | return Promise.reject(response); 898 | } 899 | 900 | }); 901 | } 902 | 903 | return this.fetchWithFailover(request, true, signal); 904 | } 905 | 906 | setAuthenticationInfo(accessKey, userId) { 907 | 908 | this._serverInfo.AccessToken = accessKey; 909 | 910 | if (this._serverInfo.UserId !== userId) { 911 | this._userViewsPromise = null; 912 | } 913 | 914 | this._serverInfo.UserId = userId; 915 | refreshWakeOnLanInfoIfNeeded(this); 916 | } 917 | 918 | serverInfo(info) { 919 | 920 | if (info) { 921 | 922 | const currentUserId = this.getCurrentUserId(); 923 | this._serverInfo = info; 924 | 925 | if (currentUserId !== this.getCurrentUserId()) { 926 | this._userViewsPromise = null; 927 | } 928 | } 929 | 930 | return this._serverInfo; 931 | } 932 | 933 | /** 934 | * Gets or sets the current user id. 935 | */ 936 | getCurrentUserName() { 937 | 938 | const userId = this.getCurrentUserId(); 939 | 940 | if (!userId) { 941 | return null; 942 | } 943 | 944 | const user = getCachedUser(this, userId); 945 | 946 | return user == null ? null : user.Name; 947 | } 948 | 949 | /** 950 | * Gets or sets the current user id. 951 | */ 952 | getCurrentUserId() { 953 | 954 | return this._serverInfo.UserId; 955 | } 956 | 957 | accessToken() { 958 | return this._serverInfo.AccessToken; 959 | } 960 | 961 | serverId() { 962 | return this.serverInfo().Id; 963 | } 964 | 965 | serverName() { 966 | return this.serverInfo().Name; 967 | } 968 | 969 | /** 970 | * Wraps around jQuery ajax methods to add additional info to the request. 971 | */ 972 | ajax(request, includeAccessToken) { 973 | 974 | if (!request) { 975 | throw new Error("Request cannot be null"); 976 | } 977 | 978 | return this.fetch(request, includeAccessToken); 979 | } 980 | 981 | /** 982 | * Gets or sets the current user id. 983 | */ 984 | getCurrentUser(options) { 985 | 986 | const userId = this.getCurrentUserId(); 987 | 988 | if (!userId) { 989 | return Promise.reject(); 990 | } 991 | 992 | options = options || {}; 993 | return this.getUser(userId, options.enableCache, options.signal); 994 | } 995 | 996 | isLoggedIn() { 997 | 998 | const info = this.serverInfo(); 999 | if (info) { 1000 | if (info.UserId && info.AccessToken) { 1001 | return true; 1002 | } 1003 | } 1004 | 1005 | return false; 1006 | } 1007 | 1008 | logout() { 1009 | 1010 | this.closeWebSocket(); 1011 | 1012 | const done = () => { 1013 | this.setAuthenticationInfo(null, null); 1014 | return Promise.resolve(); 1015 | }; 1016 | 1017 | if (this.accessToken()) { 1018 | const url = this.getUrl("Sessions/Logout"); 1019 | 1020 | return this.ajax({ 1021 | type: "POST", 1022 | url, 1023 | timeout: 10000 1024 | 1025 | }).then(done, done); 1026 | } 1027 | 1028 | return done(); 1029 | } 1030 | 1031 | /** 1032 | * Authenticates a user 1033 | * @param {String} name 1034 | * @param {String} password 1035 | */ 1036 | authenticateUserByName(name, password) { 1037 | 1038 | if (!name) { 1039 | return Promise.reject(); 1040 | } 1041 | 1042 | const url = this.getUrl("Users/authenticatebyname"); 1043 | const instance = this; 1044 | 1045 | const postData = { 1046 | Username: name, 1047 | Pw: password || '' 1048 | }; 1049 | 1050 | return instance.ajax({ 1051 | type: "POST", 1052 | url, 1053 | data: JSON.stringify(postData), 1054 | dataType: "json", 1055 | contentType: "application/json" 1056 | 1057 | }).then(result => { 1058 | 1059 | instance._userViewsPromise = null; 1060 | saveUserInCache(instance, result.User); 1061 | 1062 | const afterOnAuthenticated = () => { 1063 | refreshWakeOnLanInfoIfNeeded(instance); 1064 | return result; 1065 | }; 1066 | 1067 | if (instance.onAuthenticated) { 1068 | return instance.onAuthenticated(instance, result).then(afterOnAuthenticated); 1069 | } else { 1070 | afterOnAuthenticated(); 1071 | return result; 1072 | } 1073 | }); 1074 | } 1075 | 1076 | ensureWebSocket() { 1077 | 1078 | if (!this.connected) { 1079 | return; 1080 | } 1081 | 1082 | if (this.isWebSocketOpenOrConnecting() || !this.isWebSocketSupported()) { 1083 | return; 1084 | } 1085 | 1086 | try { 1087 | this.openWebSocket(); 1088 | } catch (err) { 1089 | console.log(`Error opening web socket: ${err}`); 1090 | } 1091 | } 1092 | 1093 | openWebSocket() { 1094 | 1095 | const accessToken = this.accessToken(); 1096 | 1097 | if (!accessToken) { 1098 | throw new Error("Cannot open web socket without access token."); 1099 | } 1100 | 1101 | let url = this.getUrl("socket"); 1102 | 1103 | url = replaceAll(url, 'emby/socket', 'embywebsocket'); 1104 | url = replaceAll(url, 'https:', 'wss:'); 1105 | url = replaceAll(url, 'http:', 'ws:'); 1106 | 1107 | url += `?api_key=${accessToken}`; 1108 | url += `&deviceId=${this.deviceId()}`; 1109 | 1110 | console.log(`opening web socket with url: ${url}`); 1111 | 1112 | const webSocket = new WebSocket(url); 1113 | 1114 | webSocket.onmessage = onWebSocketMessage.bind(this); 1115 | webSocket.onopen = onWebSocketOpen.bind(this); 1116 | webSocket.onerror = onWebSocketError.bind(this); 1117 | setSocketOnClose(this, webSocket); 1118 | 1119 | this._webSocket = webSocket; 1120 | } 1121 | 1122 | closeWebSocket() { 1123 | 1124 | const socket = this._webSocket; 1125 | 1126 | if (socket && socket.readyState === WebSocket.OPEN) { 1127 | socket.close(); 1128 | } 1129 | } 1130 | 1131 | sendWebSocketMessage(name, data) { 1132 | 1133 | console.log(`Sending web socket message: ${name}`); 1134 | 1135 | let msg = { MessageType: name }; 1136 | 1137 | if (data) { 1138 | msg.Data = data; 1139 | } 1140 | 1141 | msg = JSON.stringify(msg); 1142 | 1143 | this._webSocket.send(msg); 1144 | } 1145 | 1146 | startMessageListener(name, options) { 1147 | 1148 | this.sendMessage(`${name}Start`, options); 1149 | 1150 | let list = this.messageListeners; 1151 | 1152 | if (!list) { 1153 | this.messageListeners = list = []; 1154 | } 1155 | 1156 | if (!list.includes(name)) { 1157 | list.push(name); 1158 | } 1159 | } 1160 | 1161 | stopMessageListener(name) { 1162 | 1163 | this.sendMessage(`${name}Stop`); 1164 | 1165 | let list = this.messageListeners; 1166 | 1167 | if (list && list.includes(name)) { 1168 | this.messageListeners = list = list.filter(n => n !== name); 1169 | } 1170 | } 1171 | 1172 | sendMessage(name, data) { 1173 | 1174 | if (this.isWebSocketOpen()) { 1175 | this.sendWebSocketMessage(name, data); 1176 | } 1177 | } 1178 | 1179 | isMessageChannelOpen() { 1180 | 1181 | return this.isWebSocketOpen(); 1182 | } 1183 | 1184 | isWebSocketOpen() { 1185 | 1186 | const socket = this._webSocket; 1187 | 1188 | if (socket) { 1189 | return socket.readyState === WebSocket.OPEN; 1190 | } 1191 | return false; 1192 | } 1193 | 1194 | isWebSocketOpenOrConnecting() { 1195 | 1196 | const socket = this._webSocket; 1197 | 1198 | if (socket) { 1199 | return socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING; 1200 | } 1201 | return false; 1202 | } 1203 | 1204 | get(url) { 1205 | 1206 | return this.ajax({ 1207 | type: "GET", 1208 | url 1209 | }); 1210 | } 1211 | 1212 | getJSON(url, signal) { 1213 | 1214 | return this.fetch({ 1215 | 1216 | url, 1217 | type: 'GET', 1218 | dataType: 'json', 1219 | headers: { 1220 | accept: 'application/json' 1221 | } 1222 | 1223 | }, null, signal); 1224 | } 1225 | 1226 | getText(url, signal) { 1227 | 1228 | return this.fetch({ 1229 | 1230 | url, 1231 | type: 'GET', 1232 | dataType: 'text' 1233 | 1234 | }, null, signal); 1235 | } 1236 | 1237 | updateServerInfo(server, serverUrl) { 1238 | 1239 | if (server == null) { 1240 | throw new Error('server cannot be null'); 1241 | } 1242 | 1243 | this.serverInfo(server); 1244 | 1245 | if (!serverUrl) { 1246 | throw new Error(`serverUrl cannot be null. serverInfo: ${JSON.stringify(server)}`); 1247 | } 1248 | console.log(`Setting server address to ${serverUrl}`); 1249 | this.serverAddress(serverUrl); 1250 | } 1251 | 1252 | isWebSocketSupported() { 1253 | try { 1254 | return WebSocket != null; 1255 | } 1256 | catch (err) { 1257 | return false; 1258 | } 1259 | } 1260 | 1261 | clearAuthenticationInfo() { 1262 | this.setAuthenticationInfo(null, null); 1263 | } 1264 | 1265 | encodeName(name) { 1266 | 1267 | name = name.split('/').join('-'); 1268 | name = name.split('&').join('-'); 1269 | name = name.split('?').join('-'); 1270 | 1271 | const val = paramsToString({ name }); 1272 | return val.substring(val.indexOf('=') + 1).replace("'", '%27'); 1273 | } 1274 | 1275 | getProductNews(options = {}) { 1276 | const url = this.getUrl("News/Product", options); 1277 | 1278 | return this.getJSON(url); 1279 | } 1280 | 1281 | detectBitrate(force) { 1282 | 1283 | const instance = this; 1284 | 1285 | return this.getEndpointInfo().then(info => detectBitrateWithEndpointInfo(instance, info), info => detectBitrateWithEndpointInfo(instance, {})); 1286 | } 1287 | 1288 | /** 1289 | * Gets an item from the server 1290 | * Omit itemId to get the root folder. 1291 | */ 1292 | getItem(userId, itemId) { 1293 | 1294 | if (!itemId) { 1295 | throw new Error("null itemId"); 1296 | } 1297 | 1298 | const url = userId ? 1299 | this.getUrl(`Users/${userId}/Items/${itemId}`) : 1300 | this.getUrl(`Items/${itemId}`); 1301 | 1302 | return this.getJSON(url); 1303 | } 1304 | 1305 | /** 1306 | * Gets the root folder from the server 1307 | */ 1308 | getRootFolder(userId) { 1309 | 1310 | if (!userId) { 1311 | throw new Error("null userId"); 1312 | } 1313 | 1314 | const url = this.getUrl(`Users/${userId}/Items/Root`); 1315 | 1316 | return this.getJSON(url); 1317 | } 1318 | 1319 | getNotificationSummary(userId) { 1320 | 1321 | if (!userId) { 1322 | throw new Error("null userId"); 1323 | } 1324 | 1325 | const url = this.getUrl(`Notifications/${userId}/Summary`); 1326 | 1327 | return this.getJSON(url); 1328 | } 1329 | 1330 | getNotifications(userId, options) { 1331 | 1332 | if (!userId) { 1333 | throw new Error("null userId"); 1334 | } 1335 | 1336 | const url = this.getUrl(`Notifications/${userId}`, options || {}); 1337 | 1338 | return this.getJSON(url); 1339 | } 1340 | 1341 | markNotificationsRead(userId, idList, isRead) { 1342 | 1343 | if (!userId) { 1344 | throw new Error("null userId"); 1345 | } 1346 | 1347 | if (!idList) { 1348 | throw new Error("null idList"); 1349 | } 1350 | 1351 | const suffix = isRead ? "Read" : "Unread"; 1352 | 1353 | const params = { 1354 | UserId: userId, 1355 | Ids: idList.join(',') 1356 | }; 1357 | 1358 | const url = this.getUrl(`Notifications/${userId}/${suffix}`, params); 1359 | 1360 | return this.ajax({ 1361 | type: "POST", 1362 | url 1363 | }); 1364 | } 1365 | 1366 | getRemoteImageProviders(options) { 1367 | 1368 | if (!options) { 1369 | throw new Error("null options"); 1370 | } 1371 | 1372 | const urlPrefix = getRemoteImagePrefix(this, options); 1373 | 1374 | const url = this.getUrl(`${urlPrefix}/RemoteImages/Providers`, options); 1375 | 1376 | return this.getJSON(url); 1377 | } 1378 | 1379 | getAvailableRemoteImages(options) { 1380 | 1381 | if (!options) { 1382 | throw new Error("null options"); 1383 | } 1384 | 1385 | const urlPrefix = getRemoteImagePrefix(this, options); 1386 | 1387 | const url = this.getUrl(`${urlPrefix}/RemoteImages`, options); 1388 | 1389 | return this.getJSON(url); 1390 | } 1391 | 1392 | downloadRemoteImage(options) { 1393 | 1394 | if (!options) { 1395 | throw new Error("null options"); 1396 | } 1397 | 1398 | const urlPrefix = getRemoteImagePrefix(this, options); 1399 | 1400 | const url = this.getUrl(`${urlPrefix}/RemoteImages/Download`, options); 1401 | 1402 | return this.ajax({ 1403 | type: "POST", 1404 | url 1405 | }); 1406 | } 1407 | 1408 | getRecordingFolders(userId) { 1409 | 1410 | const url = this.getUrl("LiveTv/Recordings/Folders", { userId }); 1411 | 1412 | return this.getJSON(url); 1413 | } 1414 | 1415 | getLiveTvInfo(options) { 1416 | 1417 | const url = this.getUrl("LiveTv/Info", options || {}); 1418 | 1419 | return this.getJSON(url); 1420 | } 1421 | 1422 | getLiveTvGuideInfo(options) { 1423 | 1424 | const url = this.getUrl("LiveTv/GuideInfo", options || {}); 1425 | 1426 | return this.getJSON(url); 1427 | } 1428 | 1429 | getLiveTvChannel(id, userId) { 1430 | 1431 | if (!id) { 1432 | throw new Error("null id"); 1433 | } 1434 | 1435 | const options = { 1436 | 1437 | }; 1438 | 1439 | if (userId) { 1440 | options.userId = userId; 1441 | } 1442 | 1443 | const url = this.getUrl(`LiveTv/Channels/${id}`, options); 1444 | 1445 | return this.getJSON(url); 1446 | } 1447 | 1448 | getLiveTvChannels(options) { 1449 | 1450 | const url = this.getUrl("LiveTv/Channels", options || {}); 1451 | 1452 | return this.getJSON(url); 1453 | } 1454 | 1455 | getLiveTvPrograms(options = {}) { 1456 | if (options.channelIds && options.channelIds.length > 1800) { 1457 | 1458 | return this.ajax({ 1459 | type: "POST", 1460 | url: this.getUrl("LiveTv/Programs"), 1461 | data: JSON.stringify(options), 1462 | contentType: "application/json", 1463 | dataType: "json" 1464 | }); 1465 | 1466 | } else { 1467 | 1468 | return this.getJSON(this.getUrl("LiveTv/Programs", options)); 1469 | } 1470 | } 1471 | 1472 | getEpg(options = {}) { 1473 | options.AddCurrentProgram = false; 1474 | options.EnableUserData = false; 1475 | options.EnableImageTypes = "Primary"; 1476 | 1477 | options.UserId = this.getCurrentUserId(); 1478 | 1479 | if (this.isMinServerVersion('4.4.3')) { 1480 | 1481 | return this.getJSON(this.getUrl("LiveTv/EPG", options)).then(modifyEpgResponse.bind(this)); 1482 | } 1483 | 1484 | const serverId = this.serverId(); 1485 | const instance = this; 1486 | 1487 | const maxStartDate = options.MaxStartDate; 1488 | delete options.MaxStartDate; 1489 | 1490 | const minEndDate = options.MinEndDate; 1491 | delete options.MinEndDate; 1492 | 1493 | return this.getLiveTvChannels(options).then(result => { 1494 | 1495 | const channelIds = []; 1496 | const programMap = {}; 1497 | 1498 | for (let i = 0, length = result.Items.length; i < length; i++) { 1499 | 1500 | const channel = result.Items[i]; 1501 | 1502 | channelIds.push(channel.Id); 1503 | 1504 | const programs = programMap[channel.Id] = []; 1505 | 1506 | result.Items[i] = { 1507 | Type: 'EpgChannel', 1508 | ServerId: serverId, 1509 | Channel: channel, 1510 | Programs: programs 1511 | }; 1512 | } 1513 | 1514 | const programQuery = { 1515 | UserId: instance.getCurrentUserId(), 1516 | MaxStartDate: maxStartDate, 1517 | MinEndDate: minEndDate, 1518 | channelIds: channelIds.join(','), 1519 | ImageTypeLimit: 1, 1520 | EnableImages: false, 1521 | SortBy: "StartDate", 1522 | EnableTotalRecordCount: false, 1523 | EnableUserData: false, 1524 | Fields: options.ProgramFields 1525 | }; 1526 | 1527 | return instance.getLiveTvPrograms(programQuery).then(({ Items }) => { 1528 | 1529 | for (let j = 0, programResultLength = Items.length; j < programResultLength; j++) { 1530 | 1531 | const program = Items[j]; 1532 | 1533 | programMap[program.ChannelId].push(program); 1534 | } 1535 | 1536 | return result; 1537 | }); 1538 | }); 1539 | } 1540 | 1541 | getLiveTvRecommendedPrograms(options = {}) { 1542 | return this.getJSON(this.getUrl("LiveTv/Programs/Recommended", options)); 1543 | } 1544 | 1545 | getLiveTvRecordings(options, signal) { 1546 | 1547 | const url = this.getUrl("LiveTv/Recordings", options || {}); 1548 | 1549 | return this.getJSON(url, signal); 1550 | } 1551 | 1552 | getLiveTvRecordingSeries(options) { 1553 | 1554 | const url = this.getUrl("LiveTv/Recordings/Series", options || {}); 1555 | 1556 | return this.getJSON(url); 1557 | } 1558 | 1559 | getLiveTvRecording(id, userId) { 1560 | 1561 | if (!id) { 1562 | throw new Error("null id"); 1563 | } 1564 | 1565 | const options = { 1566 | 1567 | }; 1568 | 1569 | if (userId) { 1570 | options.userId = userId; 1571 | } 1572 | 1573 | const url = this.getUrl(`LiveTv/Recordings/${id}`, options); 1574 | 1575 | return this.getJSON(url); 1576 | } 1577 | 1578 | getLiveTvProgram(id, userId) { 1579 | 1580 | if (!id) { 1581 | throw new Error("null id"); 1582 | } 1583 | 1584 | const options = { 1585 | 1586 | }; 1587 | 1588 | if (userId) { 1589 | options.userId = userId; 1590 | } 1591 | 1592 | const url = this.getUrl(`LiveTv/Programs/${id}`, options); 1593 | 1594 | return this.getJSON(url); 1595 | } 1596 | 1597 | deleteLiveTvRecording(id) { 1598 | 1599 | if (!id) { 1600 | throw new Error("null id"); 1601 | } 1602 | 1603 | const url = this.getUrl(`LiveTv/Recordings/${id}`); 1604 | 1605 | return this.ajax({ 1606 | type: "DELETE", 1607 | url 1608 | }); 1609 | } 1610 | 1611 | cancelLiveTvTimer(id) { 1612 | 1613 | if (!id) { 1614 | throw new Error("null id"); 1615 | } 1616 | 1617 | const url = this.getUrl(`LiveTv/Timers/${id}`); 1618 | 1619 | return this.ajax({ 1620 | type: "DELETE", 1621 | url 1622 | }); 1623 | } 1624 | 1625 | getLiveTvTimers(options) { 1626 | 1627 | const url = this.getUrl("LiveTv/Timers", options || {}); 1628 | 1629 | return this.getJSON(url); 1630 | } 1631 | 1632 | getLiveTvTimer(id) { 1633 | 1634 | if (!id) { 1635 | throw new Error("null id"); 1636 | } 1637 | 1638 | const url = this.getUrl(`LiveTv/Timers/${id}`); 1639 | 1640 | return this.getJSON(url); 1641 | } 1642 | 1643 | getNewLiveTvTimerDefaults(options = {}) { 1644 | const url = this.getUrl("LiveTv/Timers/Defaults", options); 1645 | 1646 | return this.getJSON(url); 1647 | } 1648 | 1649 | createLiveTvTimer(item) { 1650 | 1651 | if (!item) { 1652 | throw new Error("null item"); 1653 | } 1654 | 1655 | const url = this.getUrl("LiveTv/Timers"); 1656 | 1657 | return this.ajax({ 1658 | type: "POST", 1659 | url, 1660 | data: JSON.stringify(item), 1661 | contentType: "application/json" 1662 | }); 1663 | } 1664 | 1665 | updateLiveTvTimer(item) { 1666 | 1667 | if (!item) { 1668 | throw new Error("null item"); 1669 | } 1670 | 1671 | const url = this.getUrl(`LiveTv/Timers/${item.Id}`); 1672 | 1673 | return this.ajax({ 1674 | type: "POST", 1675 | url, 1676 | data: JSON.stringify(item), 1677 | contentType: "application/json" 1678 | }); 1679 | } 1680 | 1681 | resetLiveTvTuner(id) { 1682 | 1683 | if (!id) { 1684 | throw new Error("null id"); 1685 | } 1686 | 1687 | const url = this.getUrl(`LiveTv/Tuners/${id}/Reset`); 1688 | 1689 | return this.ajax({ 1690 | type: "POST", 1691 | url 1692 | }); 1693 | } 1694 | 1695 | getLiveTvSeriesTimers(options) { 1696 | 1697 | const url = this.getUrl("LiveTv/SeriesTimers", options || {}); 1698 | 1699 | return this.getJSON(url); 1700 | } 1701 | 1702 | getLiveTvSeriesTimer(id) { 1703 | 1704 | if (!id) { 1705 | throw new Error("null id"); 1706 | } 1707 | 1708 | const url = this.getUrl(`LiveTv/SeriesTimers/${id}`); 1709 | 1710 | return this.getJSON(url); 1711 | } 1712 | 1713 | cancelLiveTvSeriesTimer(id) { 1714 | 1715 | if (!id) { 1716 | throw new Error("null id"); 1717 | } 1718 | 1719 | const url = this.getUrl(`LiveTv/SeriesTimers/${id}`); 1720 | 1721 | return this.ajax({ 1722 | type: "DELETE", 1723 | url 1724 | }); 1725 | } 1726 | 1727 | createLiveTvSeriesTimer(item) { 1728 | 1729 | if (!item) { 1730 | throw new Error("null item"); 1731 | } 1732 | 1733 | const url = this.getUrl("LiveTv/SeriesTimers"); 1734 | 1735 | return this.ajax({ 1736 | type: "POST", 1737 | url, 1738 | data: JSON.stringify(item), 1739 | contentType: "application/json" 1740 | }); 1741 | } 1742 | 1743 | updateLiveTvSeriesTimer(item) { 1744 | 1745 | if (!item) { 1746 | throw new Error("null item"); 1747 | } 1748 | 1749 | const url = this.getUrl(`LiveTv/SeriesTimers/${item.Id}`); 1750 | 1751 | return this.ajax({ 1752 | type: "POST", 1753 | url, 1754 | data: JSON.stringify(item), 1755 | contentType: "application/json" 1756 | }); 1757 | } 1758 | 1759 | getRegistrationInfo(feature) { 1760 | 1761 | const url = this.getUrl(`Registrations/${feature}`); 1762 | 1763 | return this.getJSON(url); 1764 | } 1765 | 1766 | /** 1767 | * Gets the current server status 1768 | */ 1769 | getSystemInfo() { 1770 | 1771 | const url = this.getUrl("System/Info"); 1772 | 1773 | const instance = this; 1774 | 1775 | return this.getJSON(url).then(info => { 1776 | 1777 | instance.setSystemInfo(info); 1778 | return Promise.resolve(info); 1779 | }); 1780 | } 1781 | 1782 | /** 1783 | * Gets the current server status 1784 | */ 1785 | getSyncStatus(itemId) { 1786 | 1787 | const url = this.getUrl(`Sync/${itemId}/Status`); 1788 | 1789 | return this.ajax({ 1790 | url, 1791 | type: 'POST', 1792 | dataType: 'json', 1793 | contentType: "application/json", 1794 | data: JSON.stringify({ 1795 | TargetId: this.deviceId() 1796 | }) 1797 | }); 1798 | } 1799 | 1800 | /** 1801 | * Gets the current server status 1802 | */ 1803 | getPublicSystemInfo() { 1804 | 1805 | const url = this.getUrl("System/Info/Public"); 1806 | 1807 | const instance = this; 1808 | 1809 | return this.getJSON(url).then(info => { 1810 | 1811 | instance.setSystemInfo(info); 1812 | return Promise.resolve(info); 1813 | }); 1814 | } 1815 | 1816 | getInstantMixFromItem(itemId, options) { 1817 | 1818 | const url = this.getUrl(`Items/${itemId}/InstantMix`, options); 1819 | 1820 | return this.getJSON(url); 1821 | } 1822 | 1823 | getEpisodes(itemId, options) { 1824 | 1825 | const url = this.getUrl(`Shows/${itemId}/Episodes`, options); 1826 | 1827 | return this.getJSON(url); 1828 | } 1829 | 1830 | getDisplayPreferences(id, userId, app) { 1831 | 1832 | const url = this.getUrl(`DisplayPreferences/${id}`, { 1833 | userId, 1834 | client: app 1835 | }); 1836 | 1837 | return this.getJSON(url); 1838 | } 1839 | 1840 | updateDisplayPreferences(id, obj, userId, app) { 1841 | 1842 | const url = this.getUrl(`DisplayPreferences/${id}`, { 1843 | userId, 1844 | client: app 1845 | }); 1846 | 1847 | return this.ajax({ 1848 | type: "POST", 1849 | url, 1850 | data: JSON.stringify(obj), 1851 | contentType: "application/json" 1852 | }); 1853 | } 1854 | 1855 | getSeasons(itemId, options) { 1856 | 1857 | const url = this.getUrl(`Shows/${itemId}/Seasons`, options); 1858 | 1859 | return this.getJSON(url); 1860 | } 1861 | 1862 | getSimilarItems(itemId, options) { 1863 | 1864 | const url = this.getUrl(`Items/${itemId}/Similar`, options); 1865 | 1866 | return this.getJSON(url); 1867 | } 1868 | 1869 | /** 1870 | * Gets all cultures known to the server 1871 | */ 1872 | getCultures() { 1873 | 1874 | const url = this.getUrl("Localization/cultures"); 1875 | 1876 | return this.getJSON(url); 1877 | } 1878 | 1879 | /** 1880 | * Gets all countries known to the server 1881 | */ 1882 | getCountries() { 1883 | 1884 | const url = this.getUrl("Localization/countries"); 1885 | 1886 | return this.getJSON(url); 1887 | } 1888 | 1889 | getPlaybackInfo(itemId, options, deviceProfile) { 1890 | 1891 | const postData = { 1892 | DeviceProfile: deviceProfile 1893 | }; 1894 | 1895 | return this.ajax({ 1896 | url: this.getUrl(`Items/${itemId}/PlaybackInfo`, options), 1897 | type: 'POST', 1898 | data: JSON.stringify(postData), 1899 | contentType: "application/json", 1900 | dataType: "json" 1901 | }); 1902 | } 1903 | 1904 | getLiveStreamMediaInfo(liveStreamId) { 1905 | 1906 | const postData = { 1907 | LiveStreamId: liveStreamId 1908 | }; 1909 | 1910 | return this.ajax({ 1911 | url: this.getUrl('LiveStreams/MediaInfo'), 1912 | type: 'POST', 1913 | data: JSON.stringify(postData), 1914 | contentType: "application/json", 1915 | dataType: "json" 1916 | }); 1917 | } 1918 | 1919 | getIntros(itemId) { 1920 | 1921 | return this.getJSON(this.getUrl(`Users/${this.getCurrentUserId()}/Items/${itemId}/Intros`)); 1922 | } 1923 | 1924 | /** 1925 | * Gets the directory contents of a path on the server 1926 | */ 1927 | getDirectoryContents(path, options) { 1928 | 1929 | if (!path) { 1930 | throw new Error("null path"); 1931 | } 1932 | if (typeof (path) !== 'string') { 1933 | throw new Error('invalid path'); 1934 | } 1935 | 1936 | options = options || {}; 1937 | 1938 | options.path = path; 1939 | 1940 | const url = this.getUrl("Environment/DirectoryContents", options); 1941 | 1942 | return this.getJSON(url); 1943 | } 1944 | 1945 | /** 1946 | * Gets shares from a network device 1947 | */ 1948 | getNetworkShares(path) { 1949 | 1950 | if (!path) { 1951 | throw new Error("null path"); 1952 | } 1953 | 1954 | const options = {}; 1955 | options.path = path; 1956 | 1957 | const url = this.getUrl("Environment/NetworkShares", options); 1958 | 1959 | return this.getJSON(url); 1960 | } 1961 | 1962 | /** 1963 | * Gets the parent of a given path 1964 | */ 1965 | getParentPath(path) { 1966 | 1967 | if (!path) { 1968 | throw new Error("null path"); 1969 | } 1970 | 1971 | const options = {}; 1972 | options.path = path; 1973 | 1974 | const url = this.getUrl("Environment/ParentPath", options); 1975 | 1976 | return this.ajax({ 1977 | type: "GET", 1978 | url, 1979 | dataType: 'text' 1980 | }); 1981 | } 1982 | 1983 | /** 1984 | * Gets a list of physical drives from the server 1985 | */ 1986 | getDrives() { 1987 | 1988 | const url = this.getUrl("Environment/Drives"); 1989 | 1990 | return this.getJSON(url); 1991 | } 1992 | 1993 | /** 1994 | * Gets a list of network devices from the server 1995 | */ 1996 | getNetworkDevices() { 1997 | 1998 | const url = this.getUrl("Environment/NetworkDevices"); 1999 | 2000 | return this.getJSON(url); 2001 | } 2002 | 2003 | getActivityLog(options) { 2004 | 2005 | const url = this.getUrl("System/ActivityLog/Entries", options || {}); 2006 | 2007 | const serverId = this.serverId(); 2008 | 2009 | return this.getJSON(url).then(result => { 2010 | 2011 | const items = result.Items; 2012 | 2013 | for (let i = 0, length = items.length; i < length; i++) { 2014 | const item = items[i]; 2015 | 2016 | item.Type = 'ActivityLogEntry'; 2017 | item.ServerId = serverId; 2018 | } 2019 | return result; 2020 | }); 2021 | } 2022 | 2023 | /** 2024 | * Cancels a package installation 2025 | */ 2026 | cancelPackageInstallation(installationId) { 2027 | 2028 | if (!installationId) { 2029 | throw new Error("null installationId"); 2030 | } 2031 | 2032 | const url = this.getUrl(`Packages/Installing/${installationId}`); 2033 | 2034 | return this.ajax({ 2035 | type: "DELETE", 2036 | url 2037 | }); 2038 | } 2039 | 2040 | /** 2041 | * Refreshes metadata for an item 2042 | */ 2043 | refreshItem(itemId, options) { 2044 | 2045 | if (!itemId) { 2046 | throw new Error("null itemId"); 2047 | } 2048 | 2049 | const url = this.getUrl(`Items/${itemId}/Refresh`, options || {}); 2050 | 2051 | return this.ajax({ 2052 | type: "POST", 2053 | url 2054 | }); 2055 | } 2056 | 2057 | /** 2058 | * Installs or updates a new plugin 2059 | */ 2060 | installPlugin(name, guid, updateClass, version) { 2061 | 2062 | if (!name) { 2063 | throw new Error("null name"); 2064 | } 2065 | 2066 | if (!updateClass) { 2067 | throw new Error("null updateClass"); 2068 | } 2069 | 2070 | const options = { 2071 | updateClass, 2072 | AssemblyGuid: guid 2073 | }; 2074 | 2075 | if (version) { 2076 | options.version = version; 2077 | } 2078 | 2079 | const url = this.getUrl(`Packages/Installed/${name}`, options); 2080 | 2081 | return this.ajax({ 2082 | type: "POST", 2083 | url 2084 | }); 2085 | } 2086 | 2087 | /** 2088 | * Instructs the server to perform a restart. 2089 | */ 2090 | restartServer() { 2091 | 2092 | const url = this.getUrl("System/Restart"); 2093 | 2094 | return this.ajax({ 2095 | type: "POST", 2096 | url 2097 | }); 2098 | } 2099 | 2100 | /** 2101 | * Instructs the server to perform a shutdown. 2102 | */ 2103 | shutdownServer() { 2104 | 2105 | const url = this.getUrl("System/Shutdown"); 2106 | 2107 | return this.ajax({ 2108 | type: "POST", 2109 | url 2110 | }); 2111 | } 2112 | 2113 | /** 2114 | * Gets information about an installable package 2115 | */ 2116 | getPackageInfo(name, guid) { 2117 | 2118 | if (!name) { 2119 | throw new Error("null name"); 2120 | } 2121 | 2122 | const options = { 2123 | AssemblyGuid: guid 2124 | }; 2125 | 2126 | const url = this.getUrl(`Packages/${name}`, options); 2127 | 2128 | return this.getJSON(url); 2129 | } 2130 | 2131 | /** 2132 | * Gets the latest available application update (if any) 2133 | */ 2134 | getAvailableApplicationUpdate() { 2135 | 2136 | const url = this.getUrl("Packages/Updates", { PackageType: "System" }); 2137 | 2138 | return this.getJSON(url); 2139 | } 2140 | 2141 | /** 2142 | * Gets the latest available plugin updates (if any) 2143 | */ 2144 | getAvailablePluginUpdates() { 2145 | 2146 | const url = this.getUrl("Packages/Updates", { PackageType: "UserInstalled" }); 2147 | 2148 | return this.getJSON(url); 2149 | } 2150 | 2151 | /** 2152 | * Gets the virtual folder list 2153 | */ 2154 | getVirtualFolders() { 2155 | 2156 | let url = "Library/VirtualFolders"; 2157 | 2158 | url = this.getUrl(url); 2159 | const serverId = this.serverId(); 2160 | 2161 | return this.getJSON(url).then(items => { 2162 | 2163 | for (let i = 0, length = items.length; i < length; i++) { 2164 | const item = items[i]; 2165 | 2166 | mapVirtualFolder(item); 2167 | item.ServerId = serverId; 2168 | } 2169 | return items; 2170 | }); 2171 | } 2172 | 2173 | /** 2174 | * Gets all the paths of the locations in the physical root. 2175 | */ 2176 | getPhysicalPaths() { 2177 | 2178 | const url = this.getUrl("Library/PhysicalPaths"); 2179 | 2180 | return this.getJSON(url); 2181 | } 2182 | 2183 | /** 2184 | * Gets the current server configuration 2185 | */ 2186 | getServerConfiguration() { 2187 | 2188 | const url = this.getUrl("System/Configuration"); 2189 | 2190 | return this.getJSON(url); 2191 | } 2192 | 2193 | /** 2194 | * Gets the current server configuration 2195 | */ 2196 | getDevicesOptions() { 2197 | 2198 | const url = this.getUrl("System/Configuration/devices"); 2199 | 2200 | return this.getJSON(url); 2201 | } 2202 | 2203 | /** 2204 | * Gets the current server configuration 2205 | */ 2206 | getContentUploadHistory() { 2207 | 2208 | const url = this.getUrl("Devices/CameraUploads", { 2209 | DeviceId: this.deviceId() 2210 | }); 2211 | 2212 | return this.getJSON(url); 2213 | } 2214 | 2215 | getNamedConfiguration(name) { 2216 | 2217 | const url = this.getUrl(`System/Configuration/${name}`); 2218 | 2219 | return this.getJSON(url); 2220 | } 2221 | 2222 | /** 2223 | Gets available hardware accelerations 2224 | */ 2225 | getHardwareAccelerations() { 2226 | 2227 | const url = this.getUrl("Encoding/HardwareAccelerations"); 2228 | 2229 | return this.getJSON(url); 2230 | } 2231 | 2232 | /** 2233 | Gets available video codecs 2234 | */ 2235 | getVideoCodecInformation() { 2236 | 2237 | const url = this.getUrl("Encoding/CodecInformation/Video"); 2238 | 2239 | return this.getJSON(url); 2240 | } 2241 | 2242 | /** 2243 | * Gets the server's scheduled tasks 2244 | */ 2245 | getScheduledTasks(options = {}) { 2246 | const url = this.getUrl("ScheduledTasks", options); 2247 | 2248 | return this.getJSON(url); 2249 | } 2250 | 2251 | /** 2252 | * Starts a scheduled task 2253 | */ 2254 | startScheduledTask(id) { 2255 | 2256 | if (!id) { 2257 | throw new Error("null id"); 2258 | } 2259 | 2260 | const url = this.getUrl(`ScheduledTasks/Running/${id}`); 2261 | 2262 | return this.ajax({ 2263 | type: "POST", 2264 | url 2265 | }); 2266 | } 2267 | 2268 | /** 2269 | * Gets a scheduled task 2270 | */ 2271 | getScheduledTask(id) { 2272 | 2273 | if (!id) { 2274 | throw new Error("null id"); 2275 | } 2276 | 2277 | const url = this.getUrl(`ScheduledTasks/${id}`); 2278 | 2279 | return this.getJSON(url); 2280 | } 2281 | 2282 | getNextUpEpisodes(options) { 2283 | 2284 | const url = this.getUrl("Shows/NextUp", options); 2285 | 2286 | return this.getJSON(url); 2287 | } 2288 | 2289 | /** 2290 | * Stops a scheduled task 2291 | */ 2292 | stopScheduledTask(id) { 2293 | 2294 | if (!id) { 2295 | throw new Error("null id"); 2296 | } 2297 | 2298 | const url = this.getUrl(`ScheduledTasks/Running/${id}`); 2299 | 2300 | return this.ajax({ 2301 | type: "DELETE", 2302 | url 2303 | }); 2304 | } 2305 | 2306 | /** 2307 | * Gets the configuration of a plugin 2308 | * @param {String} Id 2309 | */ 2310 | getPluginConfiguration(id) { 2311 | 2312 | if (!id) { 2313 | throw new Error("null Id"); 2314 | } 2315 | 2316 | const url = this.getUrl(`Plugins/${id}/Configuration`); 2317 | 2318 | return this.getJSON(url); 2319 | } 2320 | 2321 | /** 2322 | * Gets a list of plugins that are available to be installed 2323 | */ 2324 | getAvailablePlugins(options = {}) { 2325 | options.PackageType = "UserInstalled"; 2326 | 2327 | const url = this.getUrl("Packages", options); 2328 | 2329 | return this.getJSON(url); 2330 | } 2331 | 2332 | /** 2333 | * Uninstalls a plugin 2334 | * @param {String} Id 2335 | */ 2336 | uninstallPlugin(id) { 2337 | 2338 | if (!id) { 2339 | throw new Error("null Id"); 2340 | } 2341 | 2342 | const url = this.getUrl(`Plugins/${id}`); 2343 | 2344 | return this.ajax({ 2345 | type: "DELETE", 2346 | url 2347 | }); 2348 | } 2349 | 2350 | /** 2351 | * Removes a virtual folder 2352 | * @param {String} name 2353 | */ 2354 | removeVirtualFolder(virtualFolder, refreshLibrary) { 2355 | 2356 | if (!virtualFolder) { 2357 | throw new Error("null virtualFolder"); 2358 | } 2359 | 2360 | let url = "Library/VirtualFolders"; 2361 | 2362 | url = this.getUrl(url, { 2363 | refreshLibrary: refreshLibrary ? true : false, 2364 | id: virtualFolder.Id, 2365 | name: virtualFolder.Name 2366 | }); 2367 | 2368 | const instance = this; 2369 | 2370 | return this.ajax({ 2371 | type: "DELETE", 2372 | url 2373 | }).then(() => { 2374 | instance._userViewsPromise = null; 2375 | }); 2376 | } 2377 | 2378 | /** 2379 | * Adds a virtual folder 2380 | * @param {String} name 2381 | */ 2382 | addVirtualFolder(name, type, refreshLibrary, libraryOptions) { 2383 | 2384 | if (!name) { 2385 | throw new Error("null name"); 2386 | } 2387 | 2388 | const options = {}; 2389 | 2390 | if (type) { 2391 | options.collectionType = type; 2392 | } 2393 | 2394 | options.refreshLibrary = refreshLibrary ? true : false; 2395 | options.name = name; 2396 | 2397 | let url = "Library/VirtualFolders"; 2398 | 2399 | url = this.getUrl(url, options); 2400 | 2401 | const instance = this; 2402 | 2403 | return this.ajax({ 2404 | type: "POST", 2405 | url, 2406 | data: JSON.stringify({ 2407 | LibraryOptions: libraryOptions 2408 | }), 2409 | contentType: 'application/json' 2410 | }).then(() => { 2411 | instance._userViewsPromise = null; 2412 | }); 2413 | } 2414 | 2415 | updateVirtualFolderOptions(id, libraryOptions) { 2416 | 2417 | if (!id) { 2418 | throw new Error("null name"); 2419 | } 2420 | 2421 | let url = "Library/VirtualFolders/LibraryOptions"; 2422 | 2423 | url = this.getUrl(url); 2424 | 2425 | const instance = this; 2426 | 2427 | return this.ajax({ 2428 | type: "POST", 2429 | url, 2430 | data: JSON.stringify({ 2431 | Id: id, 2432 | LibraryOptions: libraryOptions 2433 | }), 2434 | contentType: 'application/json' 2435 | }).then(() => { 2436 | instance._userViewsPromise = null; 2437 | }); 2438 | } 2439 | 2440 | /** 2441 | * Renames a virtual folder 2442 | */ 2443 | renameVirtualFolder(virtualFolder, newName, refreshLibrary) { 2444 | 2445 | if (!virtualFolder) { 2446 | throw new Error("null virtualFolder"); 2447 | } 2448 | 2449 | let url = "Library/VirtualFolders/Name"; 2450 | 2451 | url = this.getUrl(url, { 2452 | refreshLibrary: refreshLibrary ? true : false, 2453 | newName, 2454 | name: virtualFolder.Name, 2455 | Id: virtualFolder.Id 2456 | }); 2457 | 2458 | const instance = this; 2459 | 2460 | return this.ajax({ 2461 | type: "POST", 2462 | url 2463 | }).then(() => { 2464 | instance._userViewsPromise = null; 2465 | }); 2466 | } 2467 | 2468 | /** 2469 | * Adds an additional mediaPath to an existing virtual folder 2470 | * @param {String} name 2471 | */ 2472 | addMediaPath(virtualFolder, mediaPath, networkSharePath, refreshLibrary) { 2473 | 2474 | if (!virtualFolder) { 2475 | throw new Error("null virtualFolder"); 2476 | } 2477 | 2478 | if (!mediaPath) { 2479 | throw new Error("null mediaPath"); 2480 | } 2481 | 2482 | let url = "Library/VirtualFolders/Paths"; 2483 | 2484 | const pathInfo = { 2485 | Path: mediaPath 2486 | }; 2487 | if (networkSharePath) { 2488 | pathInfo.NetworkPath = networkSharePath; 2489 | } 2490 | 2491 | url = this.getUrl(url, { 2492 | refreshLibrary: refreshLibrary ? true : false 2493 | }); 2494 | 2495 | const instance = this; 2496 | 2497 | return this.ajax({ 2498 | type: "POST", 2499 | url, 2500 | data: JSON.stringify({ 2501 | Name: virtualFolder.Name, 2502 | PathInfo: pathInfo, 2503 | Id: virtualFolder.Id 2504 | }), 2505 | contentType: 'application/json' 2506 | }).then(() => { 2507 | instance._userViewsPromise = null; 2508 | }); 2509 | } 2510 | 2511 | updateMediaPath(virtualFolder, pathInfo) { 2512 | 2513 | if (!virtualFolder) { 2514 | throw new Error("null virtualFolder"); 2515 | } 2516 | 2517 | if (!pathInfo) { 2518 | throw new Error("null pathInfo"); 2519 | } 2520 | 2521 | let url = "Library/VirtualFolders/Paths/Update"; 2522 | 2523 | url = this.getUrl(url); 2524 | 2525 | const instance = this; 2526 | 2527 | return this.ajax({ 2528 | type: "POST", 2529 | url, 2530 | data: JSON.stringify({ 2531 | Name: virtualFolder.Name, 2532 | PathInfo: pathInfo, 2533 | Id: virtualFolder.Id 2534 | }), 2535 | contentType: 'application/json' 2536 | 2537 | }).then(() => { 2538 | instance._userViewsPromise = null; 2539 | }); 2540 | } 2541 | 2542 | /** 2543 | * Removes a media path from a virtual folder 2544 | * @param {String} name 2545 | */ 2546 | removeMediaPath(virtualFolder, mediaPath, refreshLibrary) { 2547 | 2548 | if (!virtualFolder) { 2549 | throw new Error("null virtualFolder"); 2550 | } 2551 | 2552 | if (!mediaPath) { 2553 | throw new Error("null mediaPath"); 2554 | } 2555 | 2556 | let url = "Library/VirtualFolders/Paths"; 2557 | 2558 | const instance = this; 2559 | 2560 | url = this.getUrl(url, { 2561 | refreshLibrary: refreshLibrary ? true : false, 2562 | path: mediaPath, 2563 | name: virtualFolder.Name, 2564 | Id: virtualFolder.Id 2565 | }); 2566 | 2567 | return this.ajax({ 2568 | type: "DELETE", 2569 | url 2570 | }).then(() => { 2571 | instance._userViewsPromise = null; 2572 | }); 2573 | } 2574 | 2575 | /** 2576 | * Deletes a user 2577 | * @param {String} id 2578 | */ 2579 | deleteUser(id) { 2580 | 2581 | if (!id) { 2582 | throw new Error("null id"); 2583 | } 2584 | 2585 | const url = this.getUrl(`Users/${id}`); 2586 | 2587 | const serverId = this.serverId(); 2588 | const instance = this; 2589 | 2590 | return this.ajax({ 2591 | type: "DELETE", 2592 | url 2593 | }).then(() => { 2594 | removeCachedUser(instance, id, serverId); 2595 | instance._userViewsPromise = null; 2596 | }); 2597 | } 2598 | 2599 | /** 2600 | * Deletes a user image 2601 | * @param {String} userId 2602 | * @param {String} imageType The type of image to delete, based on the server-side ImageType enum. 2603 | */ 2604 | deleteUserImage(userId, imageType, imageIndex) { 2605 | 2606 | if (!userId) { 2607 | throw new Error("null userId"); 2608 | } 2609 | 2610 | if (!imageType) { 2611 | throw new Error("null imageType"); 2612 | } 2613 | 2614 | let url = this.getUrl(`Users/${userId}/Images/${imageType}`); 2615 | 2616 | if (imageIndex != null) { 2617 | url += `/${imageIndex}`; 2618 | } 2619 | 2620 | const serverId = this.serverId(); 2621 | const instance = this; 2622 | 2623 | return this.ajax({ 2624 | type: "DELETE", 2625 | url 2626 | }).then(() => { 2627 | removeCachedUser(instance, userId, serverId); 2628 | }); 2629 | } 2630 | 2631 | deleteItemImage(itemId, imageType, imageIndex) { 2632 | 2633 | if (!imageType) { 2634 | throw new Error("null imageType"); 2635 | } 2636 | 2637 | let url = this.getUrl(`Items/${itemId}/Images`); 2638 | 2639 | url += `/${imageType}`; 2640 | 2641 | if (imageIndex != null) { 2642 | url += `/${imageIndex}`; 2643 | } 2644 | 2645 | return this.ajax({ 2646 | type: "DELETE", 2647 | url 2648 | }); 2649 | } 2650 | 2651 | deleteItem(itemId) { 2652 | 2653 | if (!itemId) { 2654 | throw new Error("null itemId"); 2655 | } 2656 | 2657 | const url = this.getUrl(`Items/${itemId}`); 2658 | 2659 | return this.ajax({ 2660 | type: "DELETE", 2661 | url 2662 | }); 2663 | } 2664 | 2665 | stopActiveEncodings(playSessionId) { 2666 | 2667 | const options = { 2668 | deviceId: this.deviceId() 2669 | }; 2670 | 2671 | if (playSessionId) { 2672 | options.PlaySessionId = playSessionId; 2673 | } 2674 | 2675 | const url = this.getUrl("Videos/ActiveEncodings", options); 2676 | 2677 | return this.ajax({ 2678 | type: "DELETE", 2679 | url 2680 | }); 2681 | } 2682 | 2683 | reportCapabilities(options) { 2684 | 2685 | const url = this.getUrl("Sessions/Capabilities/Full"); 2686 | 2687 | return this.ajax({ 2688 | type: "POST", 2689 | url, 2690 | data: JSON.stringify(options), 2691 | contentType: "application/json" 2692 | }); 2693 | } 2694 | 2695 | updateItemImageIndex(itemId, imageType, imageIndex, newIndex) { 2696 | 2697 | if (!imageType) { 2698 | throw new Error("null imageType"); 2699 | } 2700 | 2701 | const options = { newIndex }; 2702 | 2703 | const url = this.getUrl(`Items/${itemId}/Images/${imageType}/${imageIndex}/Index`, options); 2704 | 2705 | return this.ajax({ 2706 | type: "POST", 2707 | url 2708 | }); 2709 | } 2710 | 2711 | getItemImageInfos(itemId) { 2712 | 2713 | const url = this.getUrl(`Items/${itemId}/Images`); 2714 | 2715 | return this.getJSON(url); 2716 | } 2717 | 2718 | getCriticReviews(itemId, options) { 2719 | 2720 | if (!itemId) { 2721 | throw new Error("null itemId"); 2722 | } 2723 | 2724 | const url = this.getUrl(`Items/${itemId}/CriticReviews`, options); 2725 | 2726 | return this.getJSON(url); 2727 | } 2728 | 2729 | getItemDownloadUrl(itemId, mediaSourceId) { 2730 | 2731 | if (!itemId) { 2732 | throw new Error("itemId cannot be empty"); 2733 | } 2734 | 2735 | const url = `Items/${itemId}/Download`; 2736 | 2737 | return this.getUrl(url, { 2738 | api_key: this.accessToken(), 2739 | mediaSourceId 2740 | }); 2741 | } 2742 | 2743 | getSessions(options) { 2744 | 2745 | const url = this.getUrl("Sessions", options); 2746 | 2747 | return this.getJSON(url); 2748 | } 2749 | 2750 | /** 2751 | * Uploads a user image 2752 | * @param {String} userId 2753 | * @param {String} imageType The type of image to delete, based on the server-side ImageType enum. 2754 | * @param {Object} file The file from the input element 2755 | */ 2756 | uploadUserImage(userId, imageType, file) { 2757 | 2758 | if (!userId) { 2759 | throw new Error("null userId"); 2760 | } 2761 | 2762 | if (!imageType) { 2763 | throw new Error("null imageType"); 2764 | } 2765 | 2766 | if (!file) { 2767 | throw new Error("File must be an image."); 2768 | } 2769 | 2770 | if (file.type !== "image/png" && file.type !== "image/jpeg" && file.type !== "image/jpeg") { 2771 | throw new Error("File must be an image."); 2772 | } 2773 | 2774 | const instance = this; 2775 | const serverId = this.serverId(); 2776 | 2777 | return new Promise((resolve, reject) => { 2778 | 2779 | const reader = new FileReader(); 2780 | 2781 | reader.onerror = () => { 2782 | reject(); 2783 | }; 2784 | 2785 | reader.onabort = () => { 2786 | reject(); 2787 | }; 2788 | 2789 | // Closure to capture the file information. 2790 | reader.onload = ({ target }) => { 2791 | 2792 | // Split by a comma to remove the url: prefix 2793 | const data = target.result.split(',')[1]; 2794 | 2795 | const url = instance.getUrl(`Users/${userId}/Images/${imageType}`); 2796 | 2797 | instance.ajax({ 2798 | type: "POST", 2799 | url, 2800 | data, 2801 | contentType: `image/${file.name.substring(file.name.lastIndexOf('.') + 1)}` 2802 | }).then(() => { 2803 | 2804 | removeCachedUser(instance, userId, serverId); 2805 | resolve(); 2806 | 2807 | }, reject); 2808 | }; 2809 | 2810 | // Read in the image file as a data URL. 2811 | reader.readAsDataURL(file); 2812 | }); 2813 | } 2814 | 2815 | uploadItemImage(itemId, imageType, file) { 2816 | 2817 | if (!itemId) { 2818 | throw new Error("null itemId"); 2819 | } 2820 | 2821 | if (!imageType) { 2822 | throw new Error("null imageType"); 2823 | } 2824 | 2825 | if (!file) { 2826 | throw new Error("File must be an image."); 2827 | } 2828 | 2829 | if (file.type !== "image/png" && file.type !== "image/jpeg" && file.type !== "image/jpeg") { 2830 | throw new Error("File must be an image."); 2831 | } 2832 | 2833 | let url = this.getUrl(`Items/${itemId}/Images`); 2834 | 2835 | url += `/${imageType}`; 2836 | const instance = this; 2837 | 2838 | return new Promise((resolve, reject) => { 2839 | 2840 | const reader = new FileReader(); 2841 | 2842 | reader.onerror = () => { 2843 | reject(); 2844 | }; 2845 | 2846 | reader.onabort = () => { 2847 | reject(); 2848 | }; 2849 | 2850 | // Closure to capture the file information. 2851 | reader.onload = ({ target }) => { 2852 | 2853 | // Split by a comma to remove the url: prefix 2854 | const data = target.result.split(',')[1]; 2855 | 2856 | instance.ajax({ 2857 | type: "POST", 2858 | url, 2859 | data, 2860 | contentType: `image/${file.name.substring(file.name.lastIndexOf('.') + 1)}` 2861 | }).then(resolve, reject); 2862 | }; 2863 | 2864 | // Read in the image file as a data URL. 2865 | reader.readAsDataURL(file); 2866 | }); 2867 | } 2868 | 2869 | /** 2870 | * Gets the list of installed plugins on the server 2871 | */ 2872 | getInstalledPlugins() { 2873 | 2874 | const options = {}; 2875 | 2876 | const url = this.getUrl("Plugins", options); 2877 | 2878 | return this.getJSON(url); 2879 | } 2880 | 2881 | /** 2882 | * Gets a user by id 2883 | * @param {String} id 2884 | */ 2885 | getUser(id, enableCache, signal) { 2886 | 2887 | if (!id) { 2888 | throw new Error("Must supply a userId"); 2889 | } 2890 | 2891 | let cachedUser; 2892 | 2893 | if (enableCache !== false) { 2894 | cachedUser = getCachedUser(this, id); 2895 | 2896 | // time based cache is not ideal, try to improve in the future 2897 | if (cachedUser && (Date.now() - (cachedUser.DateLastFetched || 0)) <= 60000) { 2898 | return Promise.resolve(cachedUser); 2899 | } 2900 | } 2901 | 2902 | const instance = this; 2903 | 2904 | const url = this.getUrl(`Users/${id}`); 2905 | 2906 | const serverPromise = this.getJSON(url, signal).then(user => { 2907 | 2908 | saveUserInCache(instance, user); 2909 | return user; 2910 | 2911 | }, response => { 2912 | 2913 | if (!signal || !signal.aborted) { 2914 | // if timed out, look for cached value 2915 | if (!response || !response.status) { 2916 | 2917 | if (instance.accessToken()) { 2918 | const user = getCachedUser(instance, id); 2919 | if (user) { 2920 | return Promise.resolve(user); 2921 | } 2922 | } 2923 | } 2924 | } 2925 | 2926 | throw response; 2927 | }); 2928 | 2929 | if (enableCache !== false) { 2930 | if (cachedUser) { 2931 | return Promise.resolve(cachedUser); 2932 | } 2933 | } 2934 | 2935 | return serverPromise; 2936 | } 2937 | 2938 | /** 2939 | * Gets a studio 2940 | */ 2941 | getStudio(name, userId) { 2942 | 2943 | if (!name) { 2944 | throw new Error("null name"); 2945 | } 2946 | 2947 | const options = {}; 2948 | 2949 | if (userId) { 2950 | options.userId = userId; 2951 | } 2952 | 2953 | const url = this.getUrl(`Studios/${this.encodeName(name)}`, options); 2954 | 2955 | return this.getJSON(url); 2956 | } 2957 | 2958 | /** 2959 | * Gets a genre 2960 | */ 2961 | getGenre(name, userId) { 2962 | 2963 | if (!name) { 2964 | throw new Error("null name"); 2965 | } 2966 | 2967 | const options = {}; 2968 | 2969 | if (userId) { 2970 | options.userId = userId; 2971 | } 2972 | 2973 | const url = this.getUrl(`Genres/${this.encodeName(name)}`, options); 2974 | 2975 | return this.getJSON(url); 2976 | } 2977 | 2978 | getMusicGenre(name, userId) { 2979 | 2980 | if (!name) { 2981 | throw new Error("null name"); 2982 | } 2983 | 2984 | const options = {}; 2985 | 2986 | if (userId) { 2987 | options.userId = userId; 2988 | } 2989 | 2990 | const url = this.getUrl(`MusicGenres/${this.encodeName(name)}`, options); 2991 | 2992 | return this.getJSON(url); 2993 | } 2994 | 2995 | getGameGenre(name, userId) { 2996 | 2997 | if (!name) { 2998 | throw new Error("null name"); 2999 | } 3000 | 3001 | const options = {}; 3002 | 3003 | if (userId) { 3004 | options.userId = userId; 3005 | } 3006 | 3007 | const url = this.getUrl(`GameGenres/${this.encodeName(name)}`, options); 3008 | 3009 | return this.getJSON(url); 3010 | } 3011 | 3012 | /** 3013 | * Gets an artist 3014 | */ 3015 | getArtist(name, userId) { 3016 | 3017 | if (!name) { 3018 | throw new Error("null name"); 3019 | } 3020 | 3021 | const options = {}; 3022 | 3023 | if (userId) { 3024 | options.userId = userId; 3025 | } 3026 | 3027 | const url = this.getUrl(`Artists/${this.encodeName(name)}`, options); 3028 | 3029 | return this.getJSON(url); 3030 | } 3031 | 3032 | /** 3033 | * Gets a Person 3034 | */ 3035 | getPerson(name, userId) { 3036 | 3037 | if (!name) { 3038 | throw new Error("null name"); 3039 | } 3040 | 3041 | const options = {}; 3042 | 3043 | if (userId) { 3044 | options.userId = userId; 3045 | } 3046 | 3047 | const url = this.getUrl(`Persons/${this.encodeName(name)}`, options); 3048 | 3049 | return this.getJSON(url); 3050 | } 3051 | 3052 | getPublicUsers() { 3053 | 3054 | const url = this.getUrl("users/public"); 3055 | 3056 | return this.ajax({ 3057 | type: "GET", 3058 | url, 3059 | dataType: "json" 3060 | 3061 | }, false).then(setUsersProperties); 3062 | } 3063 | 3064 | /** 3065 | * Gets all users from the server 3066 | */ 3067 | getUsers(options, signal) { 3068 | 3069 | const url = this.getUrl("users", options || {}); 3070 | 3071 | return this.getJSON(url, signal).then(setUsersProperties); 3072 | } 3073 | 3074 | /** 3075 | * Gets api keys from the server 3076 | */ 3077 | getApiKeys(options, signal) { 3078 | 3079 | const url = this.getUrl("Auth/Keys", options || {}); 3080 | 3081 | return this.getJSON(url, signal).then(setApiKeysProperties.bind(this)); 3082 | } 3083 | 3084 | /** 3085 | * Gets logs from the server 3086 | */ 3087 | getLogs(options, signal) { 3088 | 3089 | const url = this.getUrl("System/Logs", options || {}); 3090 | 3091 | return this.getJSON(url, signal).then(setLogsProperties.bind(this)); 3092 | } 3093 | 3094 | getLogDownloadUrl({ Name }) { 3095 | 3096 | let url; 3097 | 3098 | if (this.isMinServerVersion('4.2.0.11')) { 3099 | url = this.getUrl(`System/Logs/${Name}`); 3100 | url += `?api_key=${this.accessToken()}`; 3101 | } 3102 | else { 3103 | 3104 | url = this.getUrl('System/Logs/Log', { 3105 | name: Name 3106 | }); 3107 | 3108 | url += `&api_key=${this.accessToken()}`; 3109 | } 3110 | 3111 | return url; 3112 | } 3113 | 3114 | /** 3115 | * Gets logs from the server 3116 | */ 3117 | getLogLines(options, signal) { 3118 | 3119 | const name = options.name; 3120 | 3121 | options.name = null; 3122 | 3123 | const url = this.getUrl(`System/Logs/${name}/Lines`, options || {}); 3124 | 3125 | return this.getJSON(url, signal); 3126 | } 3127 | 3128 | /** 3129 | * Gets all available parental ratings from the server 3130 | */ 3131 | getParentalRatings() { 3132 | 3133 | const url = this.getUrl("Localization/ParentalRatings"); 3134 | 3135 | return this.getJSON(url); 3136 | } 3137 | 3138 | getImageUrl(itemId, options) { 3139 | 3140 | if (!itemId) { 3141 | throw new Error("itemId cannot be empty"); 3142 | } 3143 | 3144 | options = options || {}; 3145 | 3146 | let url = `Items/${itemId}/Images/${options.type}`; 3147 | 3148 | if (options.index != null) { 3149 | url += `/${options.index}`; 3150 | } 3151 | 3152 | normalizeImageOptions(this, options); 3153 | 3154 | // Don't put these on the query string 3155 | delete options.type; 3156 | delete options.index; 3157 | 3158 | return this.getUrl(url, options); 3159 | } 3160 | 3161 | /** 3162 | * Constructs a url for a user image 3163 | * @param {String} userId 3164 | * @param {Object} options 3165 | * Options supports the following properties: 3166 | * width - download the image at a fixed width 3167 | * height - download the image at a fixed height 3168 | * maxWidth - download the image at a maxWidth 3169 | * maxHeight - download the image at a maxHeight 3170 | * quality - A scale of 0-100. This should almost always be omitted as the default will suffice. 3171 | * For best results do not specify both width and height together, as aspect ratio might be altered. 3172 | */ 3173 | getUserImageUrl(userId, options) { 3174 | 3175 | if (!userId) { 3176 | throw new Error("null userId"); 3177 | } 3178 | 3179 | options = options || {}; 3180 | 3181 | let url = `Users/${userId}/Images/${options.type}`; 3182 | 3183 | if (options.index != null) { 3184 | url += `/${options.index}`; 3185 | } 3186 | 3187 | normalizeImageOptions(this, options); 3188 | 3189 | // Don't put these on the query string 3190 | delete options.type; 3191 | delete options.index; 3192 | 3193 | return this.getUrl(url, options); 3194 | } 3195 | 3196 | getThumbImageUrl(item, options) { 3197 | 3198 | if (!item) { 3199 | throw new Error("null item"); 3200 | } 3201 | 3202 | options = options || { 3203 | 3204 | }; 3205 | 3206 | options.imageType = "thumb"; 3207 | 3208 | if (item.ImageTags && item.ImageTags.Thumb) { 3209 | 3210 | options.tag = item.ImageTags.Thumb; 3211 | return this.getImageUrl(item.Id, options); 3212 | } 3213 | else if (item.ParentThumbItemId) { 3214 | 3215 | options.tag = item.ImageTags.ParentThumbImageTag; 3216 | return this.getImageUrl(item.ParentThumbItemId, options); 3217 | 3218 | } else { 3219 | return null; 3220 | } 3221 | } 3222 | 3223 | /** 3224 | * Updates a user's password 3225 | * @param {String} userId 3226 | * @param {String} currentPassword 3227 | * @param {String} newPassword 3228 | */ 3229 | updateUserPassword(userId, currentPassword, newPassword) { 3230 | 3231 | if (!userId) { 3232 | return Promise.reject(); 3233 | } 3234 | 3235 | const url = this.getUrl(`Users/${userId}/Password`); 3236 | const serverId = this.serverId(); 3237 | 3238 | const instance = this; 3239 | 3240 | return this.ajax({ 3241 | type: "POST", 3242 | url, 3243 | data: JSON.stringify({ 3244 | CurrentPw: currentPassword || '', 3245 | NewPw: newPassword 3246 | }), 3247 | contentType: "application/json" 3248 | 3249 | }).then(() => { 3250 | removeCachedUser(instance, userId, serverId); 3251 | return Promise.resolve(); 3252 | }); 3253 | } 3254 | 3255 | /** 3256 | * Updates a user's easy password 3257 | * @param {String} userId 3258 | * @param {String} newPassword 3259 | */ 3260 | updateEasyPassword(userId, newPassword) { 3261 | 3262 | const instance = this; 3263 | 3264 | if (!userId) { 3265 | Promise.reject(); 3266 | return; 3267 | } 3268 | 3269 | const url = this.getUrl(`Users/${userId}/EasyPassword`); 3270 | const serverId = this.serverId(); 3271 | 3272 | return this.ajax({ 3273 | type: "POST", 3274 | url, 3275 | data: { 3276 | NewPw: newPassword 3277 | } 3278 | }).then(() => { 3279 | removeCachedUser(instance, userId, serverId); 3280 | return Promise.resolve(); 3281 | }); 3282 | } 3283 | 3284 | /** 3285 | * Resets a user's password 3286 | * @param {String} userId 3287 | */ 3288 | resetUserPassword(userId) { 3289 | 3290 | if (!userId) { 3291 | throw new Error("null userId"); 3292 | } 3293 | 3294 | const url = this.getUrl(`Users/${userId}/Password`); 3295 | const serverId = this.serverId(); 3296 | 3297 | const postData = { 3298 | 3299 | }; 3300 | 3301 | postData.resetPassword = true; 3302 | const instance = this; 3303 | 3304 | return this.ajax({ 3305 | type: "POST", 3306 | url, 3307 | data: postData 3308 | }).then(() => { 3309 | removeCachedUser(instance, userId, serverId); 3310 | return Promise.resolve(); 3311 | }); 3312 | } 3313 | 3314 | resetEasyPassword(userId) { 3315 | 3316 | if (!userId) { 3317 | throw new Error("null userId"); 3318 | } 3319 | 3320 | const url = this.getUrl(`Users/${userId}/EasyPassword`); 3321 | const serverId = this.serverId(); 3322 | 3323 | const postData = { 3324 | 3325 | }; 3326 | 3327 | postData.resetPassword = true; 3328 | const instance = this; 3329 | 3330 | return this.ajax({ 3331 | type: "POST", 3332 | url, 3333 | data: postData 3334 | 3335 | }).then(() => { 3336 | removeCachedUser(instance, userId, serverId); 3337 | return Promise.resolve(); 3338 | }); 3339 | } 3340 | 3341 | /** 3342 | * Updates the server's configuration 3343 | * @param {Object} configuration 3344 | */ 3345 | updateServerConfiguration(configuration) { 3346 | 3347 | if (!configuration) { 3348 | throw new Error("null configuration"); 3349 | } 3350 | 3351 | const url = this.getUrl("System/Configuration"); 3352 | 3353 | return this.ajax({ 3354 | type: "POST", 3355 | url, 3356 | data: JSON.stringify(configuration), 3357 | contentType: "application/json" 3358 | }); 3359 | } 3360 | 3361 | updateNamedConfiguration(name, configuration) { 3362 | 3363 | if (!configuration) { 3364 | throw new Error("null configuration"); 3365 | } 3366 | 3367 | const url = this.getUrl(`System/Configuration/${name}`); 3368 | 3369 | return this.ajax({ 3370 | type: "POST", 3371 | url, 3372 | data: JSON.stringify(configuration), 3373 | contentType: "application/json" 3374 | }); 3375 | } 3376 | 3377 | updateItem(item) { 3378 | 3379 | if (!item) { 3380 | throw new Error("null item"); 3381 | } 3382 | 3383 | const url = this.getUrl(`Items/${item.Id}`); 3384 | 3385 | return this.ajax({ 3386 | type: "POST", 3387 | url, 3388 | data: JSON.stringify(item), 3389 | contentType: "application/json" 3390 | }); 3391 | } 3392 | 3393 | /** 3394 | * Updates plugin security info 3395 | */ 3396 | updatePluginSecurityInfo(info) { 3397 | 3398 | const url = this.getUrl("Plugins/SecurityInfo"); 3399 | 3400 | return this.ajax({ 3401 | type: "POST", 3402 | url, 3403 | data: JSON.stringify(info), 3404 | contentType: "application/json" 3405 | }); 3406 | } 3407 | 3408 | /** 3409 | * Creates a user 3410 | * @param {Object} user 3411 | */ 3412 | createUser(name) { 3413 | 3414 | const url = this.getUrl("Users/New"); 3415 | 3416 | return this.ajax({ 3417 | type: "POST", 3418 | url, 3419 | data: { 3420 | Name: name 3421 | }, 3422 | dataType: "json" 3423 | }); 3424 | } 3425 | 3426 | /** 3427 | * Updates a user 3428 | * @param {Object} user 3429 | */ 3430 | updateUser(user) { 3431 | 3432 | if (!user) { 3433 | throw new Error("null user"); 3434 | } 3435 | 3436 | const url = this.getUrl(`Users/${user.Id}`); 3437 | 3438 | return this.ajax({ 3439 | type: "POST", 3440 | url, 3441 | data: JSON.stringify(user), 3442 | contentType: "application/json" 3443 | }); 3444 | } 3445 | 3446 | updateUserPolicy(userId, policy) { 3447 | 3448 | if (!userId) { 3449 | throw new Error("null userId"); 3450 | } 3451 | if (!policy) { 3452 | throw new Error("null policy"); 3453 | } 3454 | 3455 | const url = this.getUrl(`Users/${userId}/Policy`); 3456 | const instance = this; 3457 | 3458 | if (instance.getCurrentUserId() === userId) { 3459 | instance._userViewsPromise = null; 3460 | } 3461 | 3462 | removeCachedUser(instance, userId, instance.serverId()); 3463 | 3464 | return this.ajax({ 3465 | type: "POST", 3466 | url, 3467 | data: JSON.stringify(policy), 3468 | contentType: "application/json" 3469 | }).then(() => { 3470 | 3471 | if (instance.getCurrentUserId() === userId) { 3472 | instance._userViewsPromise = null; 3473 | } 3474 | removeCachedUser(instance, userId, instance.serverId()); 3475 | 3476 | return Promise.resolve(); 3477 | }); 3478 | } 3479 | 3480 | updateUserConfiguration(userId, configuration) { 3481 | 3482 | if (!userId) { 3483 | throw new Error("null userId"); 3484 | } 3485 | if (!configuration) { 3486 | throw new Error("null configuration"); 3487 | } 3488 | 3489 | const url = this.getUrl(`Users/${userId}/Configuration`); 3490 | const instance = this; 3491 | 3492 | if (instance.getCurrentUserId() === userId) { 3493 | instance._userViewsPromise = null; 3494 | } 3495 | removeCachedUser(instance, userId, instance.serverId()); 3496 | 3497 | return this.ajax({ 3498 | type: "POST", 3499 | url, 3500 | data: JSON.stringify(configuration), 3501 | contentType: "application/json" 3502 | 3503 | }).then(() => { 3504 | 3505 | if (instance.getCurrentUserId() === userId) { 3506 | instance._userViewsPromise = null; 3507 | } 3508 | removeCachedUser(instance, userId, instance.serverId()); 3509 | 3510 | return Promise.resolve(); 3511 | }); 3512 | } 3513 | 3514 | /** 3515 | * Updates the Triggers for a ScheduledTask 3516 | * @param {String} id 3517 | * @param {Object} triggers 3518 | */ 3519 | updateScheduledTaskTriggers(id, triggers) { 3520 | 3521 | if (!id) { 3522 | throw new Error("null id"); 3523 | } 3524 | 3525 | if (!triggers) { 3526 | throw new Error("null triggers"); 3527 | } 3528 | 3529 | const url = this.getUrl(`ScheduledTasks/${id}/Triggers`); 3530 | 3531 | return this.ajax({ 3532 | type: "POST", 3533 | url, 3534 | data: JSON.stringify(triggers), 3535 | contentType: "application/json" 3536 | }); 3537 | } 3538 | 3539 | /** 3540 | * Updates a plugin's configuration 3541 | * @param {String} Id 3542 | * @param {Object} configuration 3543 | */ 3544 | updatePluginConfiguration(id, configuration) { 3545 | 3546 | if (!id) { 3547 | throw new Error("null Id"); 3548 | } 3549 | 3550 | if (!configuration) { 3551 | throw new Error("null configuration"); 3552 | } 3553 | 3554 | const url = this.getUrl(`Plugins/${id}/Configuration`); 3555 | 3556 | return this.ajax({ 3557 | type: "POST", 3558 | url, 3559 | data: JSON.stringify(configuration), 3560 | contentType: "application/json" 3561 | }); 3562 | } 3563 | 3564 | getAncestorItems(itemId, userId) { 3565 | 3566 | if (!itemId) { 3567 | throw new Error("null itemId"); 3568 | } 3569 | 3570 | const options = {}; 3571 | 3572 | if (userId) { 3573 | options.userId = userId; 3574 | } 3575 | 3576 | const url = this.getUrl(`Items/${itemId}/Ancestors`, options); 3577 | 3578 | return this.getJSON(url); 3579 | } 3580 | 3581 | /** 3582 | * Gets items based on a query, typically for children of a folder 3583 | * @param {String} userId 3584 | * @param {Object} options 3585 | * Options accepts the following properties: 3586 | * itemId - Localize the search to a specific folder (root if omitted) 3587 | * startIndex - Use for paging 3588 | * limit - Use to limit results to a certain number of items 3589 | * filter - Specify one or more ItemFilters, comma delimeted (see server-side enum) 3590 | * sortBy - Specify an ItemSortBy (comma-delimeted list see server-side enum) 3591 | * sortOrder - ascending/descending 3592 | * fields - additional fields to include aside from basic info. This is a comma delimited list. See server-side enum ItemFields. 3593 | * index - the name of the dynamic, localized index function 3594 | * dynamicSortBy - the name of the dynamic localized sort function 3595 | * recursive - Whether or not the query should be recursive 3596 | * searchTerm - search term to use as a filter 3597 | */ 3598 | getItems(userId, options, signal) { 3599 | 3600 | let url; 3601 | 3602 | if ((typeof userId).toString().toLowerCase() === 'string') { 3603 | url = this.getUrl(`Users/${userId}/Items`, options); 3604 | } else { 3605 | 3606 | url = this.getUrl("Items", options); 3607 | } 3608 | 3609 | return this.getJSON(url, signal); 3610 | } 3611 | 3612 | getResumableItems(userId, options) { 3613 | 3614 | return this.getJSON(this.getUrl(`Users/${userId}/Items/Resume`, options)); 3615 | } 3616 | 3617 | getMovieRecommendations(options) { 3618 | 3619 | return this.getJSON(this.getUrl('Movies/Recommendations', options)); 3620 | } 3621 | 3622 | getUpcomingEpisodes(options) { 3623 | 3624 | return this.getJSON(this.getUrl('Shows/Upcoming', options)); 3625 | } 3626 | 3627 | getUserViews(options, userId, signal) { 3628 | 3629 | const currentUserId = this.getCurrentUserId(); 3630 | userId = userId || currentUserId; 3631 | 3632 | const enableCache = userId === currentUserId && (!options || !options.IncludeHidden); 3633 | 3634 | if (enableCache && this._userViewsPromise) { 3635 | return this._userViewsPromise; 3636 | } 3637 | 3638 | const url = this.getUrl(`Users/${userId}/Views`, options); 3639 | const self = this; 3640 | 3641 | const promise = this.getJSON(url, signal).catch(() => { 3642 | self._userViewsPromise = null; 3643 | }); 3644 | 3645 | if (enableCache) { 3646 | this._userViewsPromise = promise; 3647 | } 3648 | return promise; 3649 | } 3650 | 3651 | /** 3652 | Gets artists from an item 3653 | */ 3654 | getArtists(userId, options) { 3655 | 3656 | if (!userId) { 3657 | throw new Error("null userId"); 3658 | } 3659 | 3660 | options = options || {}; 3661 | options.userId = userId; 3662 | 3663 | const url = this.getUrl("Artists", options); 3664 | 3665 | return this.getJSON(url); 3666 | } 3667 | 3668 | /** 3669 | Gets artists from an item 3670 | */ 3671 | getAlbumArtists(userId, options) { 3672 | 3673 | if (!userId) { 3674 | throw new Error("null userId"); 3675 | } 3676 | 3677 | options = options || {}; 3678 | options.userId = userId; 3679 | 3680 | const url = this.getUrl("Artists/AlbumArtists", options); 3681 | 3682 | return this.getJSON(url); 3683 | } 3684 | 3685 | /** 3686 | Gets genres from an item 3687 | */ 3688 | getGenres(userId, options) { 3689 | 3690 | if (!userId) { 3691 | throw new Error("null userId"); 3692 | } 3693 | 3694 | options = options || {}; 3695 | options.userId = userId; 3696 | 3697 | const url = this.getUrl("Genres", options); 3698 | 3699 | return this.getJSON(url); 3700 | } 3701 | 3702 | getMusicGenres(userId, options) { 3703 | 3704 | if (!userId) { 3705 | throw new Error("null userId"); 3706 | } 3707 | 3708 | options = options || {}; 3709 | options.userId = userId; 3710 | 3711 | const url = this.getUrl("MusicGenres", options); 3712 | 3713 | return this.getJSON(url); 3714 | } 3715 | 3716 | getGameGenres(userId, options) { 3717 | 3718 | if (!userId) { 3719 | throw new Error("null userId"); 3720 | } 3721 | 3722 | options = options || {}; 3723 | options.userId = userId; 3724 | 3725 | const url = this.getUrl("GameGenres", options); 3726 | 3727 | return this.getJSON(url); 3728 | } 3729 | 3730 | /** 3731 | Gets people from an item 3732 | */ 3733 | getPeople(userId, options) { 3734 | 3735 | if (!userId) { 3736 | throw new Error("null userId"); 3737 | } 3738 | 3739 | options = options || {}; 3740 | options.userId = userId; 3741 | 3742 | const url = this.getUrl("Persons", options); 3743 | 3744 | return this.getJSON(url); 3745 | } 3746 | 3747 | /** 3748 | Gets thumbnails from an item 3749 | */ 3750 | getThumbnails(itemId, options) { 3751 | 3752 | if (!this.isMinServerVersion('4.1.0.26')) { 3753 | return Promise.resolve({ Thumbnails: [] }); 3754 | } 3755 | 3756 | const url = this.getUrl(`Items/${itemId}/ThumbnailSet`, options); 3757 | 3758 | return this.getJSON(url); 3759 | } 3760 | 3761 | /** 3762 | Gets thumbnails from an item 3763 | */ 3764 | getDeleteInfo(itemId, options) { 3765 | 3766 | if (!this.isMinServerVersion('4.1.0.15')) { 3767 | return Promise.resolve({ Paths: [] }); 3768 | } 3769 | 3770 | const url = this.getUrl(`Items/${itemId}/DeleteInfo`, options); 3771 | 3772 | return this.getJSON(url); 3773 | } 3774 | 3775 | /** 3776 | Gets studios from an item 3777 | */ 3778 | getStudios(userId, options) { 3779 | 3780 | if (!userId) { 3781 | throw new Error("null userId"); 3782 | } 3783 | 3784 | options = options || {}; 3785 | options.userId = userId; 3786 | 3787 | const url = this.getUrl("Studios", options); 3788 | 3789 | return this.getJSON(url); 3790 | } 3791 | 3792 | getOfficialRatings(userId, options) { 3793 | 3794 | if (!userId) { 3795 | throw new Error("null userId"); 3796 | } 3797 | 3798 | options = options || {}; 3799 | options.userId = userId; 3800 | 3801 | const url = this.getUrl("OfficialRatings", options); 3802 | 3803 | return this.getJSON(url); 3804 | } 3805 | 3806 | getYears(userId, options) { 3807 | 3808 | if (!userId) { 3809 | throw new Error("null userId"); 3810 | } 3811 | 3812 | options = options || {}; 3813 | options.userId = userId; 3814 | 3815 | const url = this.getUrl("Years", options); 3816 | 3817 | return this.getJSON(url); 3818 | } 3819 | 3820 | getTags(userId, options) { 3821 | 3822 | if (!userId) { 3823 | throw new Error("null userId"); 3824 | } 3825 | 3826 | options = options || {}; 3827 | options.userId = userId; 3828 | 3829 | const url = this.getUrl("Tags", options); 3830 | 3831 | return this.getJSON(url).then(fillTagProperties.bind(this)); 3832 | } 3833 | 3834 | getContainers(userId, options) { 3835 | 3836 | if (!userId) { 3837 | throw new Error("null userId"); 3838 | } 3839 | 3840 | options = options || {}; 3841 | options.userId = userId; 3842 | 3843 | const url = this.getUrl("Containers", options); 3844 | 3845 | return this.getJSON(url); 3846 | } 3847 | 3848 | getAudioCodecs(userId, options) { 3849 | 3850 | if (!userId) { 3851 | throw new Error("null userId"); 3852 | } 3853 | 3854 | options = options || {}; 3855 | options.userId = userId; 3856 | 3857 | const url = this.getUrl("AudioCodecs", options); 3858 | 3859 | return this.getJSON(url); 3860 | } 3861 | 3862 | getStreamLanguages(userId, options) { 3863 | 3864 | if (!this.isMinServerVersion('4.5.0.15')) { 3865 | return Promise.resolve({ 3866 | Items: [] 3867 | }); 3868 | } 3869 | 3870 | if (!userId) { 3871 | throw new Error("null userId"); 3872 | } 3873 | 3874 | options = options || {}; 3875 | options.userId = userId; 3876 | 3877 | const url = this.getUrl("StreamLanguages", options); 3878 | 3879 | return this.getJSON(url); 3880 | } 3881 | 3882 | getVideoCodecs(userId, options) { 3883 | 3884 | if (!userId) { 3885 | throw new Error("null userId"); 3886 | } 3887 | 3888 | options = options || {}; 3889 | options.userId = userId; 3890 | 3891 | const url = this.getUrl("VideoCodecs", options); 3892 | 3893 | return this.getJSON(url); 3894 | } 3895 | 3896 | getSubtitleCodecs(userId, options) { 3897 | 3898 | if (!userId) { 3899 | throw new Error("null userId"); 3900 | } 3901 | 3902 | options = options || {}; 3903 | options.userId = userId; 3904 | 3905 | const url = this.getUrl("SubtitleCodecs", options); 3906 | 3907 | return this.getJSON(url); 3908 | } 3909 | 3910 | getDefaultPrefixes() { 3911 | return Promise.resolve(['#', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'].map(mapPrefix)); 3912 | } 3913 | 3914 | getPrefixes(userId, options) { 3915 | 3916 | if (!userId) { 3917 | throw new Error("null userId"); 3918 | } 3919 | 3920 | options = options || {}; 3921 | options.userId = userId; 3922 | 3923 | const url = this.getUrl("Items/Prefixes", options); 3924 | 3925 | return this.getJSON(url); 3926 | } 3927 | 3928 | getArtistPrefixes(userId, options) { 3929 | 3930 | if (!userId) { 3931 | throw new Error("null userId"); 3932 | } 3933 | 3934 | options = options || {}; 3935 | options.userId = userId; 3936 | 3937 | const url = this.getUrl("Artists/Prefixes", options); 3938 | 3939 | return this.getJSON(url); 3940 | } 3941 | 3942 | /** 3943 | * Gets local trailers for an item 3944 | */ 3945 | getLocalTrailers(userId, itemId) { 3946 | 3947 | if (!userId) { 3948 | throw new Error("null userId"); 3949 | } 3950 | if (!itemId) { 3951 | throw new Error("null itemId"); 3952 | } 3953 | 3954 | const url = this.getUrl(`Users/${userId}/Items/${itemId}/LocalTrailers`); 3955 | 3956 | return this.getJSON(url); 3957 | } 3958 | 3959 | getGameSystems() { 3960 | 3961 | const options = {}; 3962 | 3963 | const userId = this.getCurrentUserId(); 3964 | if (userId) { 3965 | options.userId = userId; 3966 | } 3967 | 3968 | const url = this.getUrl("Games/SystemSummaries", options); 3969 | 3970 | return this.getJSON(url); 3971 | } 3972 | 3973 | getAdditionalVideoParts(userId, itemId) { 3974 | 3975 | if (!itemId) { 3976 | throw new Error("null itemId"); 3977 | } 3978 | 3979 | const options = {}; 3980 | 3981 | if (userId) { 3982 | options.userId = userId; 3983 | } 3984 | 3985 | const url = this.getUrl(`Videos/${itemId}/AdditionalParts`, options); 3986 | 3987 | return this.getJSON(url); 3988 | } 3989 | 3990 | getThemeMedia(itemId, options) { 3991 | 3992 | const url = this.getUrl(`Items/${itemId}/ThemeMedia`, options); 3993 | 3994 | return this.getJSON(url); 3995 | } 3996 | 3997 | getAudioStreamUrl( 3998 | { Id }, 3999 | { Container, Protocol, AudioCodec }, 4000 | directPlayContainers, 4001 | maxBitrate, 4002 | maxAudioSampleRate, 4003 | maxAudioBitDepth, 4004 | startPosition, 4005 | enableRemoteMedia 4006 | ) { 4007 | 4008 | const url = `Audio/${Id}/universal`; 4009 | 4010 | startingPlaySession++; 4011 | return this.getUrl(url, { 4012 | UserId: this.getCurrentUserId(), 4013 | DeviceId: this.deviceId(), 4014 | MaxStreamingBitrate: maxBitrate, 4015 | Container: directPlayContainers, 4016 | TranscodingContainer: Container || null, 4017 | TranscodingProtocol: Protocol || null, 4018 | AudioCodec: AudioCodec, 4019 | MaxAudioSampleRate: maxAudioSampleRate, 4020 | MaxAudioBitDepth: maxAudioBitDepth, 4021 | api_key: this.accessToken(), 4022 | PlaySessionId: startingPlaySession, 4023 | StartTimeTicks: startPosition || 0, 4024 | EnableRedirection: true, 4025 | EnableRemoteMedia: enableRemoteMedia 4026 | }); 4027 | } 4028 | 4029 | getAudioStreamUrls( 4030 | items, 4031 | transcodingProfile, 4032 | directPlayContainers, 4033 | maxBitrate, 4034 | maxAudioSampleRate, 4035 | maxAudioBitDepth, 4036 | startPosition, 4037 | enableRemoteMedia 4038 | ) { 4039 | 4040 | const streamUrls = []; 4041 | for (let i = 0, length = items.length; i < length; i++) { 4042 | 4043 | const item = items[i]; 4044 | let streamUrl; 4045 | 4046 | if (item.MediaType === 'Audio') { 4047 | streamUrl = this.getAudioStreamUrl(item, transcodingProfile, directPlayContainers, maxBitrate, maxAudioSampleRate, maxAudioBitDepth, startPosition, enableRemoteMedia); 4048 | } 4049 | 4050 | streamUrls.push(streamUrl || ''); 4051 | 4052 | if (i === 0) { 4053 | startPosition = 0; 4054 | } 4055 | } 4056 | 4057 | return Promise.resolve(streamUrls); 4058 | } 4059 | 4060 | /** 4061 | * Gets special features for an item 4062 | */ 4063 | getSpecialFeatures(userId, itemId) { 4064 | 4065 | if (!userId) { 4066 | throw new Error("null userId"); 4067 | } 4068 | if (!itemId) { 4069 | throw new Error("null itemId"); 4070 | } 4071 | 4072 | const url = this.getUrl(`Users/${userId}/Items/${itemId}/SpecialFeatures`); 4073 | 4074 | return this.getJSON(url); 4075 | } 4076 | 4077 | getDateParamValue(date) { 4078 | 4079 | function formatDigit(i) { 4080 | return i < 10 ? `0${i}` : i; 4081 | } 4082 | 4083 | const d = date; 4084 | 4085 | return `${d.getFullYear()}${formatDigit(d.getMonth() + 1)}${formatDigit(d.getDate())}${formatDigit(d.getHours())}${formatDigit(d.getMinutes())}${formatDigit(d.getSeconds())}`; 4086 | } 4087 | 4088 | markPlayed(userId, itemId, date) { 4089 | 4090 | if (!userId) { 4091 | throw new Error("null userId"); 4092 | } 4093 | 4094 | if (!itemId) { 4095 | throw new Error("null itemId"); 4096 | } 4097 | 4098 | const options = {}; 4099 | 4100 | if (date) { 4101 | options.DatePlayed = this.getDateParamValue(date); 4102 | } 4103 | 4104 | const url = this.getUrl(`Users/${userId}/PlayedItems/${itemId}`, options); 4105 | 4106 | return this.ajax({ 4107 | type: "POST", 4108 | url, 4109 | dataType: "json" 4110 | 4111 | }).then(onUserDataUpdated.bind({ 4112 | instance: this, 4113 | userId, 4114 | itemId 4115 | })); 4116 | } 4117 | 4118 | markUnplayed(userId, itemId) { 4119 | 4120 | if (!userId) { 4121 | throw new Error("null userId"); 4122 | } 4123 | 4124 | if (!itemId) { 4125 | throw new Error("null itemId"); 4126 | } 4127 | 4128 | const url = this.getUrl(`Users/${userId}/PlayedItems/${itemId}`); 4129 | 4130 | return this.ajax({ 4131 | type: "DELETE", 4132 | url, 4133 | dataType: "json" 4134 | 4135 | }).then(onUserDataUpdated.bind({ 4136 | instance: this, 4137 | userId, 4138 | itemId 4139 | })); 4140 | } 4141 | 4142 | /** 4143 | * Updates a user's favorite status for an item. 4144 | * @param {String} userId 4145 | * @param {String} itemId 4146 | * @param {Boolean} isFavorite 4147 | */ 4148 | updateFavoriteStatus(userId, itemId, isFavorite) { 4149 | 4150 | if (!userId) { 4151 | throw new Error("null userId"); 4152 | } 4153 | 4154 | if (!itemId) { 4155 | throw new Error("null itemId"); 4156 | } 4157 | 4158 | const url = this.getUrl(`Users/${userId}/FavoriteItems/${itemId}`); 4159 | 4160 | const method = isFavorite ? "POST" : "DELETE"; 4161 | 4162 | return this.ajax({ 4163 | type: method, 4164 | url, 4165 | dataType: "json" 4166 | 4167 | }).then(onUserDataUpdated.bind({ 4168 | instance: this, 4169 | userId, 4170 | itemId 4171 | })); 4172 | } 4173 | 4174 | /** 4175 | * Updates a user's personal rating for an item 4176 | * @param {String} userId 4177 | * @param {String} itemId 4178 | * @param {Boolean} likes 4179 | */ 4180 | updateUserItemRating(userId, itemId, likes) { 4181 | 4182 | if (!userId) { 4183 | throw new Error("null userId"); 4184 | } 4185 | 4186 | if (!itemId) { 4187 | throw new Error("null itemId"); 4188 | } 4189 | 4190 | const url = this.getUrl(`Users/${userId}/Items/${itemId}/Rating`, { 4191 | likes 4192 | }); 4193 | 4194 | return this.ajax({ 4195 | type: "POST", 4196 | url, 4197 | dataType: "json" 4198 | 4199 | }).then(onUserDataUpdated.bind({ 4200 | instance: this, 4201 | userId, 4202 | itemId 4203 | })); 4204 | } 4205 | 4206 | getItemCounts(userId) { 4207 | 4208 | const options = {}; 4209 | 4210 | if (userId) { 4211 | options.userId = userId; 4212 | } 4213 | 4214 | const url = this.getUrl("Items/Counts", options); 4215 | 4216 | return this.getJSON(url); 4217 | } 4218 | 4219 | /** 4220 | * Clears a user's personal rating for an item 4221 | * @param {String} userId 4222 | * @param {String} itemId 4223 | */ 4224 | clearUserItemRating(userId, itemId) { 4225 | 4226 | if (!userId) { 4227 | throw new Error("null userId"); 4228 | } 4229 | 4230 | if (!itemId) { 4231 | throw new Error("null itemId"); 4232 | } 4233 | 4234 | const url = this.getUrl(`Users/${userId}/Items/${itemId}/Rating`); 4235 | 4236 | return this.ajax({ 4237 | type: "DELETE", 4238 | url, 4239 | dataType: "json" 4240 | 4241 | }).then(onUserDataUpdated.bind({ 4242 | instance: this, 4243 | userId, 4244 | itemId 4245 | })); 4246 | } 4247 | 4248 | /** 4249 | * Reports the user has started playing something 4250 | * @param {String} userId 4251 | * @param {String} itemId 4252 | */ 4253 | reportPlaybackStart(options) { 4254 | 4255 | if (!options) { 4256 | throw new Error("null options"); 4257 | } 4258 | 4259 | this.lastPlaybackProgressReport = 0; 4260 | this.lastPlaybackProgressReportTicks = null; 4261 | 4262 | const url = this.getUrl("Sessions/Playing"); 4263 | 4264 | return this.ajax({ 4265 | type: "POST", 4266 | data: JSON.stringify(options), 4267 | contentType: "application/json", 4268 | url 4269 | }); 4270 | } 4271 | 4272 | /** 4273 | * Reports progress viewing an item 4274 | * @param {String} userId 4275 | * @param {String} itemId 4276 | */ 4277 | reportPlaybackProgress(options) { 4278 | 4279 | if (!options) { 4280 | throw new Error("null options"); 4281 | } 4282 | 4283 | const newPositionTicks = options.PositionTicks; 4284 | 4285 | if ((options.EventName || 'timeupdate') === 'timeupdate') { 4286 | 4287 | const now = Date.now(); 4288 | const msSinceLastReport = now - (this.lastPlaybackProgressReport || 0); 4289 | 4290 | if (msSinceLastReport <= 10000) { 4291 | 4292 | if (!newPositionTicks) { 4293 | return Promise.resolve(); 4294 | } 4295 | 4296 | const expectedReportTicks = (msSinceLastReport * 10000) + (this.lastPlaybackProgressReportTicks || 0); 4297 | 4298 | if (Math.abs((newPositionTicks || 0) - expectedReportTicks) < (5000 * 10000)) { 4299 | 4300 | return Promise.resolve(); 4301 | } 4302 | } 4303 | 4304 | this.lastPlaybackProgressReport = now; 4305 | 4306 | } else { 4307 | 4308 | // allow the next timeupdate 4309 | this.lastPlaybackProgressReport = 0; 4310 | } 4311 | 4312 | this.lastPlaybackProgressReportTicks = newPositionTicks; 4313 | const url = this.getUrl("Sessions/Playing/Progress"); 4314 | 4315 | return this.ajax({ 4316 | type: "POST", 4317 | data: JSON.stringify(options), 4318 | contentType: "application/json", 4319 | url 4320 | }); 4321 | } 4322 | 4323 | reportOfflineActions(actions) { 4324 | 4325 | if (!actions) { 4326 | throw new Error("null actions"); 4327 | } 4328 | 4329 | const url = this.getUrl("Sync/OfflineActions"); 4330 | 4331 | return this.ajax({ 4332 | type: "POST", 4333 | data: JSON.stringify(actions), 4334 | contentType: "application/json", 4335 | url 4336 | }); 4337 | } 4338 | 4339 | syncData(data) { 4340 | 4341 | if (!data) { 4342 | throw new Error("null data"); 4343 | } 4344 | 4345 | const url = this.getUrl("Sync/Data"); 4346 | 4347 | return this.ajax({ 4348 | type: "POST", 4349 | data: JSON.stringify(data), 4350 | contentType: "application/json", 4351 | url, 4352 | dataType: "json" 4353 | }); 4354 | } 4355 | 4356 | getReadySyncItems(deviceId) { 4357 | 4358 | if (!deviceId) { 4359 | throw new Error("null deviceId"); 4360 | } 4361 | 4362 | const url = this.getUrl("Sync/Items/Ready", { 4363 | TargetId: deviceId 4364 | }); 4365 | 4366 | return this.getJSON(url); 4367 | } 4368 | 4369 | reportSyncJobItemTransferred(syncJobItemId) { 4370 | 4371 | if (!syncJobItemId) { 4372 | throw new Error("null syncJobItemId"); 4373 | } 4374 | 4375 | const url = this.getUrl(`Sync/JobItems/${syncJobItemId}/Transferred`); 4376 | 4377 | return this.ajax({ 4378 | type: "POST", 4379 | url 4380 | }); 4381 | } 4382 | 4383 | cancelSyncItems(itemIds, targetId) { 4384 | 4385 | if (!itemIds) { 4386 | throw new Error("null itemIds"); 4387 | } 4388 | 4389 | const url = this.getUrl(`Sync/${targetId || this.deviceId()}/Items`, { 4390 | ItemIds: itemIds.join(',') 4391 | }); 4392 | 4393 | return this.ajax({ 4394 | type: "DELETE", 4395 | url 4396 | }); 4397 | } 4398 | 4399 | /** 4400 | * Reports a user has stopped playing an item 4401 | * @param {String} userId 4402 | * @param {String} itemId 4403 | */ 4404 | reportPlaybackStopped(options) { 4405 | 4406 | if (!options) { 4407 | throw new Error("null options"); 4408 | } 4409 | 4410 | this.lastPlaybackProgressReport = 0; 4411 | this.lastPlaybackProgressReportTicks = null; 4412 | 4413 | const url = this.getUrl("Sessions/Playing/Stopped"); 4414 | 4415 | return this.ajax({ 4416 | type: "POST", 4417 | data: JSON.stringify(options), 4418 | contentType: "application/json", 4419 | url 4420 | }); 4421 | } 4422 | 4423 | sendPlayCommand(sessionId, options) { 4424 | 4425 | if (!sessionId) { 4426 | throw new Error("null sessionId"); 4427 | } 4428 | 4429 | if (!options) { 4430 | throw new Error("null options"); 4431 | } 4432 | 4433 | const url = this.getUrl(`Sessions/${sessionId}/Playing`, options); 4434 | 4435 | return this.ajax({ 4436 | type: "POST", 4437 | url 4438 | }); 4439 | } 4440 | 4441 | sendCommand(sessionId, command) { 4442 | 4443 | if (!sessionId) { 4444 | throw new Error("null sessionId"); 4445 | } 4446 | 4447 | if (!command) { 4448 | throw new Error("null command"); 4449 | } 4450 | 4451 | const url = this.getUrl(`Sessions/${sessionId}/Command`); 4452 | 4453 | const ajaxOptions = { 4454 | type: "POST", 4455 | url 4456 | }; 4457 | 4458 | ajaxOptions.data = JSON.stringify(command); 4459 | ajaxOptions.contentType = "application/json"; 4460 | 4461 | return this.ajax(ajaxOptions); 4462 | } 4463 | 4464 | sendMessageCommand(sessionId, options) { 4465 | 4466 | if (!sessionId) { 4467 | throw new Error("null sessionId"); 4468 | } 4469 | 4470 | if (!options) { 4471 | throw new Error("null options"); 4472 | } 4473 | 4474 | const url = this.getUrl(`Sessions/${sessionId}/Message`); 4475 | 4476 | const ajaxOptions = { 4477 | type: "POST", 4478 | url 4479 | }; 4480 | 4481 | ajaxOptions.data = JSON.stringify(options); 4482 | ajaxOptions.contentType = "application/json"; 4483 | 4484 | return this.ajax(ajaxOptions); 4485 | } 4486 | 4487 | sendPlayStateCommand(sessionId, command, options) { 4488 | 4489 | if (!sessionId) { 4490 | throw new Error("null sessionId"); 4491 | } 4492 | 4493 | if (!command) { 4494 | throw new Error("null command"); 4495 | } 4496 | 4497 | const url = this.getUrl(`Sessions/${sessionId}/Playing/${command}`, options || {}); 4498 | 4499 | return this.ajax({ 4500 | type: "POST", 4501 | url 4502 | }); 4503 | } 4504 | 4505 | getSavedEndpointInfo() { 4506 | 4507 | return this._endPointInfo; 4508 | } 4509 | 4510 | getEndpointInfo() { 4511 | 4512 | const savedValue = this._endPointInfo; 4513 | if (savedValue) { 4514 | return Promise.resolve(savedValue); 4515 | } 4516 | 4517 | const instance = this; 4518 | return this.getJSON(this.getUrl('System/Endpoint')).then(endPointInfo => { 4519 | 4520 | setSavedEndpointInfo(instance, endPointInfo); 4521 | return endPointInfo; 4522 | }); 4523 | } 4524 | 4525 | getWakeOnLanInfo() { 4526 | 4527 | return this.getJSON(this.getUrl('System/WakeOnLanInfo')); 4528 | } 4529 | 4530 | getLatestItems(options = {}) { 4531 | return this.getJSON(this.getUrl(`Users/${this.getCurrentUserId()}/Items/Latest`, options)); 4532 | } 4533 | 4534 | getPlayQueue(options) { 4535 | 4536 | if (!this.isMinServerVersion('4.6')) { 4537 | return Promise.resolve({ Items: [], TotalRecordCount: 0 }); 4538 | } 4539 | 4540 | return this.getJSON(this.getUrl('Sessions/PlayQueue', options)); 4541 | } 4542 | 4543 | supportsWakeOnLan() { 4544 | 4545 | if (!this.wakeOnLan.isSupported()) { 4546 | return false; 4547 | } 4548 | 4549 | return getCachedWakeOnLanInfo(this).length > 0; 4550 | } 4551 | 4552 | wakeOnLan() { 4553 | 4554 | const infos = getCachedWakeOnLanInfo(this); 4555 | 4556 | return sendNextWakeOnLan(this.wakeOnLan, infos, 0); 4557 | } 4558 | 4559 | setSystemInfo({ Version }) { 4560 | this._serverVersion = Version; 4561 | this._queryStringAuth = this.isMinServerVersion('4.4.0.21'); 4562 | this._separateHeaderValues = this.isMinServerVersion('4.4.0.21'); 4563 | } 4564 | 4565 | serverVersion() { 4566 | return this._serverVersion; 4567 | } 4568 | 4569 | isMinServerVersion(version) { 4570 | const serverVersion = this.serverVersion(); 4571 | 4572 | if (serverVersion) { 4573 | return compareVersions(serverVersion, version) >= 0; 4574 | } 4575 | 4576 | return false; 4577 | } 4578 | 4579 | handleMessageReceived(msg) { 4580 | 4581 | onMessageReceivedInternal(this, msg); 4582 | } 4583 | } 4584 | 4585 | ApiClient.prototype.getScaledImageUrl = ApiClient.prototype.getImageUrl; 4586 | 4587 | export default ApiClient; --------------------------------------------------------------------------------