diff --git a/patches/PluginSecurityManager.cs.patch b/patches/PluginSecurityManager.cs.patch index 3b44bde..56a7552 100644 --- a/patches/PluginSecurityManager.cs.patch +++ b/patches/PluginSecurityManager.cs.patch @@ -1,119 +1,126 @@ -*** PluginSecurityManager.cs 2018-05-02 14:25:12.000000000 -0400 ---- PluginSecurityManager.cs.new 2018-05-03 00:12:35.076641948 -0400 -*************** namespace Emby.Server.Implementations.Se -*** 176,178 **** - { -- throw new PaymentRequiredException(); - } ---- 176,177 ---- -*************** namespace Emby.Server.Implementations.Se -*** 207,295 **** - { -- var regInfo = LicenseFile.GetRegInfo(feature); -- var lastChecked = regInfo == null ? DateTime.MinValue : regInfo.LastChecked; -- var expDate = regInfo == null ? DateTime.MinValue : regInfo.ExpirationDate; -- -- var maxCacheDays = 14; -- var nextCheckDate = new [] { expDate, lastChecked.AddDays(maxCacheDays) }.Min(); -- -- if (nextCheckDate > DateTime.UtcNow.AddDays(maxCacheDays)) -- { -- nextCheckDate = DateTime.MinValue; -- } -- -- //check the reg file first to alleviate strain on the MB admin server - must actually check in every 30 days tho -- var reg = new RegRecord -- { -- // Cache the result for up to a week -- registered = regInfo != null && nextCheckDate >= DateTime.UtcNow && expDate >= DateTime.UtcNow, -- expDate = expDate -- }; -- -- var key = SupporterKey; -- -- var success = reg.registered; -- -- if (!(lastChecked > DateTime.UtcNow.AddDays(-1)) || (!reg.registered)) -- { -- var data = new Dictionary -- { -- { "feature", feature }, -- { "key", key }, -- { "mac", _appHost.SystemId }, -- { "systemid", _appHost.SystemId }, -- { "ver", version }, -- { "platform", _appHost.OperatingSystemDisplayName } -- }; -- -- try -- { -- var options = new HttpRequestOptions -- { -- Url = MBValidateUrl, -- -- // Seeing block length errors -- EnableHttpCompression = false, -- BufferContent = false -- }; -- -- options.SetPostData(data); -- -- using (var response = (await _httpClient.Post(options).ConfigureAwait(false))) -- { -- using (var json = response.Content) -- { -- reg = _jsonSerializer.DeserializeFromStream(json); -- success = true; -- } -- } -- -- if (reg.registered) -- { -- _logger.Info("Registered for feature {0}", feature); -- LicenseFile.AddRegCheck(feature, reg.expDate); -- } -- else -- { -- _logger.Info("Not registered for feature {0}", feature); -- LicenseFile.RemoveRegCheck(feature); -- } -- -- } -- catch (Exception e) -- { -- _logger.ErrorException("Error checking registration status of {0}", e, feature); -- } -- } -- - var record = new MBRegistrationRecord - { -! IsRegistered = reg.registered, -! ExpirationDate = reg.expDate, - RegChecked = true, -! RegError = !success - }; - -- record.TrialVersion = IsInTrial(reg.expDate, record.RegChecked, record.IsRegistered); -- record.IsValid = !record.RegChecked || record.IsRegistered || record.TrialVersion; -- - return record; ---- 206,216 ---- - { - var record = new MBRegistrationRecord - { -! IsRegistered = true, - RegChecked = true, -! TrialVersion = false, -! IsValid = true, -! RegError = false - }; - - return record; -*************** namespace Emby.Server.Implementations.Se -*** 310,311 **** - } -! } +--- a 2018-07-21 23:40:59.763170429 -0400 ++++ b 2018-07-21 23:40:56.679819085 -0400 +@@ -188,109 +188,14 @@ + + private async Task GetRegistrationStatusInternal(string feature, bool forceCallToServer, string version, CancellationToken cancellationToken) + { +- await _regCheckLock.WaitAsync(cancellationToken).ConfigureAwait(false); +- +- try +- { +- var regInfo = LicenseFile.GetRegInfo(feature); +- var lastChecked = regInfo == null ? DateTime.MinValue : regInfo.LastChecked; +- var expDate = regInfo == null ? DateTime.MinValue : regInfo.ExpirationDate; +- +- var maxCacheDays = 14; +- var nextCheckDate = new[] { expDate, lastChecked.AddDays(maxCacheDays) }.Min(); +- +- if (nextCheckDate > DateTime.UtcNow.AddDays(maxCacheDays)) +- { +- nextCheckDate = DateTime.MinValue; +- } +- +- //check the reg file first to alleviate strain on the MB admin server - must actually check in every 30 days tho +- var reg = new RegRecord +- { +- // Cache the result for up to a week +- registered = regInfo != null && nextCheckDate >= DateTime.UtcNow && expDate >= DateTime.UtcNow, +- expDate = expDate +- }; +- +- var key = SupporterKey; +- +- if (!forceCallToServer && string.IsNullOrWhiteSpace(key)) +- { +- return new MBRegistrationRecord(); +- } +- +- var success = reg.registered; +- +- if (!(lastChecked > DateTime.UtcNow.AddDays(-1)) || (!reg.registered)) +- { +- var data = new Dictionary +- { +- { "feature", feature }, +- { "key", key }, +- { "mac", _appHost.SystemId }, +- { "systemid", _appHost.SystemId }, +- { "ver", version }, +- { "platform", _appHost.OperatingSystemDisplayName } +- }; +- +- try +- { +- var options = new HttpRequestOptions +- { +- Url = MBValidateUrl, +- +- // Seeing block length errors +- EnableHttpCompression = false, +- BufferContent = false, +- CancellationToken = cancellationToken +- }; +- +- options.SetPostData(data); +- +- using (var response = (await _httpClient.Post(options).ConfigureAwait(false))) +- { +- using (var json = response.Content) +- { +- reg = await _jsonSerializer.DeserializeFromStreamAsync(json).ConfigureAwait(false); +- success = true; +- } +- } +- +- if (reg.registered) +- { +- _logger.Info("Registered for feature {0}", feature); +- LicenseFile.AddRegCheck(feature, reg.expDate); +- } +- else +- { +- _logger.Info("Not registered for feature {0}", feature); +- LicenseFile.RemoveRegCheck(feature); +- } +- +- } +- catch (Exception e) +- { +- _logger.ErrorException("Error checking registration status of {0}", e, feature); +- } +- } +- +- var record = new MBRegistrationRecord +- { +- IsRegistered = reg.registered, +- ExpirationDate = reg.expDate, +- RegChecked = true, +- RegError = !success +- }; +- +- record.TrialVersion = IsInTrial(reg.expDate, record.RegChecked, record.IsRegistered); +- record.IsValid = !record.RegChecked || record.IsRegistered || record.TrialVersion; +- +- return record; +- } +- finally ++ return new MBRegistrationRecord + { +- _regCheckLock.Release(); +- } ++ IsRegistered = true, ++ RegChecked = true, ++ RegError = false, ++ TrialVersion = false, ++ IsValid = true ++ }; + } + + private bool IsInTrial(DateTime expirationDate, bool regChecked, bool isRegistered) +@@ -306,4 +211,4 @@ + return isInTrial && !isRegistered; + } + } +-} \ No newline at end of file ---- 231,232 ---- - } -! } ++} diff --git a/replacements/connectionmanager.js b/replacements/connectionmanager.js index 2fe7ffe..78e909b 100644 --- a/replacements/connectionmanager.js +++ b/replacements/connectionmanager.js @@ -1,727 +1,1483 @@ -define(["events", "apiclient", "appStorage"], function(events, apiClientFactory, appStorage) { - "use strict"; - - function getServerAddress(server, mode) { - switch (mode) { - case ConnectionMode.Local: - return server.LocalAddress; - case ConnectionMode.Manual: - return server.ManualAddress; - case ConnectionMode.Remote: - return server.RemoteAddress; - default: - return server.ManualAddress || server.LocalAddress || server.RemoteAddress - } +import events from './events.js'; + +const defaultTimeout = 20000; + +const ConnectionMode = { + Local: 0, + Remote: 1, + Manual: 2 +}; + +function getServerAddress(server, mode) { + + switch (mode) { + case ConnectionMode.Local: + return server.LocalAddress; + case ConnectionMode.Manual: + return server.ManualAddress; + case ConnectionMode.Remote: + return server.RemoteAddress; + default: + return server.ManualAddress || server.LocalAddress || server.RemoteAddress; } +} + +function paramsToString(params) { + + const values = []; + + for (const key in params) { - function paramsToString(params) { - var values = []; - for (var key in params) { - var value = params[key]; - null !== value && void 0 !== value && "" !== value && values.push(encodeURIComponent(key) + "=" + encodeURIComponent(value)) + const value = params[key]; + + if (value !== null && value !== undefined && value !== '') { + values.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`); } - return values.join("&") } + return values.join('&'); +} - function resolveFailure(instance, resolve) { - resolve({ - State: "Unavailable", - ConnectUser: instance.connectUser() - }) - } +function resolveFailure(instance, resolve) { - function mergeServers(credentialProvider, list1, list2) { - for (var i = 0, length = list2.length; i < length; i++) credentialProvider.addOrUpdateServer(list1, list2[i]); - return list1 - } + resolve({ + State: 'Unavailable', + ConnectUser: instance.connectUser() + }); +} - function updateServerInfo(server, systemInfo) { - server.Name = systemInfo.ServerName, systemInfo.Id && (server.Id = systemInfo.Id), systemInfo.LocalAddress && (server.LocalAddress = systemInfo.LocalAddress), systemInfo.WanAddress && (server.RemoteAddress = systemInfo.WanAddress) - } +function mergeServers(credentialProvider, list1, list2) { - function getEmbyServerUrl(baseUrl, handler) { - return baseUrl + "/emby/" + handler + for (let i = 0, length = list2.length; i < length; i++) { + credentialProvider.addOrUpdateServer(list1, list2[i]); } - function getFetchPromise(request) { - var headers = request.headers || {}; - "json" === request.dataType && (headers.accept = "application/json"); - var fetchRequest = { - headers: headers, - method: request.type, - credentials: "same-origin" - }, - contentType = request.contentType; - return request.data && ("string" == typeof request.data ? fetchRequest.body = request.data : (fetchRequest.body = paramsToString(request.data), contentType = contentType || "application/x-www-form-urlencoded; charset=UTF-8")), contentType && (headers["Content-Type"] = contentType), request.timeout ? fetchWithTimeout(request.url, fetchRequest, request.timeout) : fetch(request.url, fetchRequest) - } + return list1; +} + +function updateServerInfo(server, systemInfo) { - function fetchWithTimeout(url, options, timeoutMs) { - return console.log("fetchWithTimeout: timeoutMs: " + timeoutMs + ", url: " + url), new Promise(function(resolve, reject) { - var timeout = setTimeout(reject, timeoutMs); - options = options || {}, options.credentials = "same-origin", fetch(url, options).then(function(response) { - clearTimeout(timeout), console.log("fetchWithTimeout: succeeded connecting to url: " + url), resolve(response) - }, function(error) { - clearTimeout(timeout), console.log("fetchWithTimeout: timed out connecting to url: " + url), reject() - }) - }) + server.Name = systemInfo.ServerName; + + if (systemInfo.Id) { + server.Id = systemInfo.Id; + } + if (systemInfo.LocalAddress) { + server.LocalAddress = systemInfo.LocalAddress; } + if (systemInfo.WanAddress) { + server.RemoteAddress = systemInfo.WanAddress; + } +} + +function getEmbyServerUrl(baseUrl, handler) { + return `${baseUrl}/emby/${handler}`; +} + +function getFetchPromise(request) { + + const headers = request.headers || {}; - function ajax(request) { - if (!request) throw new Error("Request cannot be null"); - return request.headers = request.headers || {}, console.log("ConnectionManager requesting url: " + request.url), getFetchPromise(request).then(function(response) { - return console.log("ConnectionManager response status: " + response.status + ", url: " + request.url), response.status < 400 ? "json" === request.dataType || "application/json" === request.headers.accept ? response.json() : response : Promise.reject(response) - }, function(err) { - throw console.log("ConnectionManager request failed to url: " + request.url), err - }) + if (request.dataType === 'json') { + headers.accept = 'application/json'; } - function tryConnect(url, timeout) { - return url = getEmbyServerUrl(url, "system/info/public"), console.log("tryConnect url: " + url), ajax({ - type: "GET", - url: url, - dataType: "json", - timeout: timeout || defaultTimeout - }) + const fetchRequest = { + headers, + method: request.type, + credentials: 'same-origin' + }; + + let contentType = request.contentType; + + if (request.data) { + + if (typeof request.data === 'string') { + fetchRequest.body = request.data; + } else { + fetchRequest.body = paramsToString(request.data); + + contentType = contentType || 'application/x-www-form-urlencoded; charset=UTF-8'; + } } - function getConnectUrl(handler) { - return "https://connect.emby.media/service/" + handler + if (contentType) { + + headers['Content-Type'] = contentType; } - function replaceAll(originalString, strReplace, strWith) { - var reg = new RegExp(strReplace, "ig"); - return originalString.replace(reg, strWith) + if (!request.timeout) { + return fetch(request.url, fetchRequest); } - function normalizeAddress(address) { - return address = address.trim(), 0 !== address.toLowerCase().indexOf("http") && (address = "http://" + address), address = replaceAll(address, "Http:", "http:"), address = replaceAll(address, "Https:", "https:") + return fetchWithTimeout(request.url, fetchRequest, request.timeout); +} + +function fetchWithTimeout(url, options, timeoutMs) { + + console.log(`fetchWithTimeout: timeoutMs: ${timeoutMs}, url: ${url}`); + + return new Promise((resolve, reject) => { + + const timeout = setTimeout(reject, timeoutMs); + + options = options || {}; + options.credentials = 'same-origin'; + + fetch(url, options).then(response => { + clearTimeout(timeout); + + console.log(`fetchWithTimeout: succeeded connecting to url: ${url}`); + + resolve(response); + }, error => { + + clearTimeout(timeout); + + console.log(`fetchWithTimeout: timed out connecting to url: ${url}`); + + reject(); + }); + }); +} + +function ajax(request) { + + if (!request) { + throw new Error("Request cannot be null"); } - function stringEqualsIgnoreCase(str1, str2) { - return (str1 || "").toLowerCase() === (str2 || "").toLowerCase() + request.headers = request.headers || {}; + + console.log(`ConnectionManager requesting url: ${request.url}`); + + return getFetchPromise(request).then(response => { + + console.log(`ConnectionManager response status: ${response.status}, url: ${request.url}`); + + if (response.status < 400) { + + if (request.dataType === 'json' || request.headers.accept === 'application/json') { + return response.json(); + } else { + return response; + } + } else { + return Promise.reject(response); + } + + }, err => { + + console.log(`ConnectionManager request failed to url: ${request.url}`); + throw err; + }); +} + +function getConnectUrl(handler) { + return `https://connect.emby.media/service/${handler}`; +} + +function replaceAll(originalString, strReplace, strWith) { + const reg = new RegExp(strReplace, 'ig'); + return originalString.replace(reg, strWith); +} + +function normalizeAddress(address) { + + // attempt to correct bad input + address = address.trim(); + + if (address.toLowerCase().indexOf('http') !== 0) { + address = `http://${address}`; } - function compareVersions(a, b) { - a = a.split("."), b = b.split("."); - for (var i = 0, length = Math.max(a.length, b.length); i < length; i++) { - var aVal = parseInt(a[i] || "0"), - bVal = parseInt(b[i] || "0"); - if (aVal < bVal) return -1; - if (aVal > bVal) return 1 + // Seeing failures in iOS when protocol isn't lowercase + address = replaceAll(address, 'Http:', 'http:'); + address = replaceAll(address, 'Https:', 'https:'); + + return address; +} + +function stringEqualsIgnoreCase(str1, str2) { + + return (str1 || '').toLowerCase() === (str2 || '').toLowerCase(); +} + +function compareVersions(a, b) { + + // -1 a is smaller + // 1 a is larger + // 0 equal + a = a.split('.'); + b = b.split('.'); + + for (let i = 0, length = Math.max(a.length, b.length); i < length; i++) { + const aVal = parseInt(a[i] || '0'); + const bVal = parseInt(b[i] || '0'); + + if (aVal < bVal) { + return -1; + } + + if (aVal > bVal) { + return 1; } - return 0 } - var defaultTimeout = 2e4, - ConnectionMode = { - Local: 0, - Remote: 1, - Manual: 2 - }, - ConnectionManager = function(credentialProvider, appName, appVersion, deviceName, deviceId, capabilities, devicePixelRatio) { - function onConnectUserSignIn(user) { - connectUser = user, events.trigger(self, "connectusersignedin", [user]) - } - - function onAuthenticated(apiClient, result, options, saveCredentials) { - var credentials = credentialProvider.credentials(), - servers = credentials.Servers.filter(function(s) { - return s.Id === result.ServerId - }), - server = servers.length ? servers[0] : apiClient.serverInfo(); - return !1 !== options.updateDateLastAccessed && (server.DateLastAccessed = (new Date).getTime()), server.Id = result.ServerId, saveCredentials ? (server.UserId = result.User.Id, server.AccessToken = result.AccessToken) : (server.UserId = null, server.AccessToken = null), credentialProvider.addOrUpdateServer(credentials.Servers, server), credentialProvider.credentials(credentials), apiClient.enableAutomaticBitrateDetection = options.enableAutomaticBitrateDetection, apiClient.serverInfo(server), afterConnected(apiClient, options), onLocalUserSignIn(server, server.LastConnectionMode, result.User) - } - - function afterConnected(apiClient, options) { - options = options || {}, !1 !== options.reportCapabilities && apiClient.reportCapabilities(capabilities), apiClient.enableAutomaticBitrateDetection = options.enableAutomaticBitrateDetection, !1 !== options.enableWebSocket && (console.log("calling apiClient.ensureWebSocket"), apiClient.ensureWebSocket()) - } - - function onLocalUserSignIn(server, connectionMode, user) { - return self._getOrAddApiClient(server, connectionMode), (self.onLocalUserSignedIn ? self.onLocalUserSignedIn.call(self, user) : Promise.resolve()).then(function() { - events.trigger(self, "localusersignedin", [user]) - }) - } - - function ensureConnectUser(credentials) { - return connectUser && connectUser.Id === credentials.ConnectUserId ? Promise.resolve() : credentials.ConnectUserId && credentials.ConnectAccessToken ? (connectUser = null, getConnectUser(credentials.ConnectUserId, credentials.ConnectAccessToken).then(function(user) { - return onConnectUserSignIn(user), Promise.resolve() - }, function() { - return Promise.resolve() - })) : Promise.resolve() - } - - function getConnectUser(userId, accessToken) { - if (!userId) throw new Error("null userId"); - if (!accessToken) throw new Error("null accessToken"); - return ajax({ - type: "GET", - url: "https://connect.emby.media/service/user?id=" + userId, - dataType: "json", - headers: { - "X-Application": appName + "/" + appVersion, - "X-Connect-UserToken": accessToken - } - }) - } - - function addAuthenticationInfoFromConnect(server, connectionMode, credentials) { - if (!server.ExchangeToken) throw new Error("server.ExchangeToken cannot be null"); - if (!credentials.ConnectUserId) throw new Error("credentials.ConnectUserId cannot be null"); - var url = getServerAddress(server, connectionMode); - url = getEmbyServerUrl(url, "Connect/Exchange?format=json&ConnectUserId=" + credentials.ConnectUserId); - var auth = 'MediaBrowser Client="' + appName + '", Device="' + deviceName + '", DeviceId="' + deviceId + '", Version="' + appVersion + '"'; - return ajax({ - type: "GET", - url: url, - dataType: "json", - headers: { - "X-MediaBrowser-Token": server.ExchangeToken, - "X-Emby-Authorization": auth - } - }).then(function(auth) { - return server.UserId = auth.LocalUserId, server.AccessToken = auth.AccessToken, auth - }, function() { - return server.UserId = null, server.AccessToken = null, Promise.reject() - }) - } - - function validateAuthentication(server, connectionMode) { - return ajax({ - type: "GET", - url: getEmbyServerUrl(getServerAddress(server, connectionMode), "System/Info"), - dataType: "json", - headers: { - "X-MediaBrowser-Token": server.AccessToken - } - }).then(function(systemInfo) { - return updateServerInfo(server, systemInfo), Promise.resolve() - }, function() { - return server.UserId = null, server.AccessToken = null, Promise.resolve() - }) + + return 0; +} + +export default class ConnectionManager { + constructor( + credentialProvider, + appStorage, + apiClientFactory, + serverDiscoveryFn, + wakeOnLanFn, + appName, + appVersion, + deviceName, + deviceId, + capabilities, + devicePixelRatio) { + + console.log('Begin ConnectionManager constructor'); + + const self = this; + this._apiClients = []; + + let connectUser; + self.connectUser = () => connectUser; + + self._minServerVersion = '3.2.33'; + + self.appVersion = () => appVersion; + + self.appName = () => appName; + + self.capabilities = () => capabilities; + + self.deviceId = () => deviceId; + + self.credentialProvider = () => credentialProvider; + + self.connectUserId = () => credentialProvider.credentials().ConnectUserId; + + self.connectToken = () => credentialProvider.credentials().ConnectAccessToken; + + self.getServerInfo = id => { + + const servers = credentialProvider.credentials().Servers; + + return servers.filter(s => s.Id === id)[0]; + }; + + self.getLastUsedServer = () => { + + const servers = credentialProvider.credentials().Servers; + + servers.sort((a, b) => (b.DateLastAccessed || 0) - (a.DateLastAccessed || 0)); + + if (!servers.length) { + return null; } - function getImageUrl(localUser) { - if (connectUser && connectUser.ImageUrl) return { - url: connectUser.ImageUrl - }; - if (localUser && localUser.PrimaryImageTag) { - return { - url: self.getApiClient(localUser).getUserImageUrl(localUser.Id, { - tag: localUser.PrimaryImageTag, - type: "Primary" - }), - supportsParams: !0 - } - } - return { - url: null, - supportsParams: !1 - } + return servers[0]; + }; + + self.addApiClient = apiClient => { + + self._apiClients.push(apiClient); + + const existingServers = credentialProvider.credentials().Servers.filter(s => stringEqualsIgnoreCase(s.ManualAddress, apiClient.serverAddress()) || + stringEqualsIgnoreCase(s.LocalAddress, apiClient.serverAddress()) || + stringEqualsIgnoreCase(s.RemoteAddress, apiClient.serverAddress())); + + const existingServer = existingServers.length ? existingServers[0] : apiClient.serverInfo(); + existingServer.DateLastAccessed = new Date().getTime(); + existingServer.LastConnectionMode = ConnectionMode.Manual; + existingServer.ManualAddress = apiClient.serverAddress(); + + if (apiClient.manualAddressOnly) { + existingServer.manualAddressOnly = true; } - function logoutOfServer(apiClient) { - var serverInfo = apiClient.serverInfo() || {}, - logoutInfo = { - serverId: serverInfo.Id - }; - return apiClient.logout().then(function() { - events.trigger(self, "localusersignedout", [logoutInfo]) - }, function() { - events.trigger(self, "localusersignedout", [logoutInfo]) - }) + apiClient.serverInfo(existingServer); + + apiClient.onAuthenticated = (instance, result) => onAuthenticated(instance, result, {}, true); + + if (!existingServers.length) { + const credentials = credentialProvider.credentials(); + credentials.Servers = [existingServer]; + credentialProvider.credentials(credentials); } - function getConnectServers(credentials) { - return console.log("Begin getConnectServers"), credentials.ConnectAccessToken && credentials.ConnectUserId ? ajax({ - type: "GET", - url: "https://connect.emby.media/service/servers?userId=" + credentials.ConnectUserId, - dataType: "json", - headers: { - "X-Application": appName + "/" + appVersion, - "X-Connect-UserToken": credentials.ConnectAccessToken - } - }).then(function(servers) { - return servers.map(function(i) { - return { - ExchangeToken: i.AccessKey, - ConnectServerId: i.Id, - Id: i.SystemId, - Name: i.Name, - RemoteAddress: i.Url, - LocalAddress: i.LocalAddress, - UserLinkType: "guest" === (i.UserType || "").toLowerCase() ? "Guest" : "LinkedUser" - } - }) - }, function() { - return credentials.Servers.slice(0).filter(function(s) { - return s.ExchangeToken - }) - }) : Promise.resolve([]) - } - - function filterServers(servers, connectServers) { - return servers.filter(function(server) { - return !server.ExchangeToken || connectServers.filter(function(connectServer) { - return server.Id === connectServer.Id - }).length > 0 - }) - } - - function findServers() { - return new Promise(function(resolve, reject) { - var onFinish = function(foundServers) { - var servers = foundServers.map(function(foundServer) { - var info = { - Id: foundServer.Id, - LocalAddress: convertEndpointAddressToManualAddress(foundServer) || foundServer.Address, - Name: foundServer.Name - }; - return info.LastConnectionMode = info.ManualAddress ? ConnectionMode.Manual : ConnectionMode.Local, info - }); - resolve(servers) - }; - require(["serverdiscovery"], function(serverDiscovery) { - serverDiscovery.findServers(1e3).then(onFinish, function() { - onFinish([]) - }) - }) - }) - } - - function convertEndpointAddressToManualAddress(info) { - if (info.Address && info.EndpointAddress) { - var address = info.EndpointAddress.split(":")[0], - parts = info.Address.split(":"); - if (parts.length > 1) { - var portString = parts[parts.length - 1]; - isNaN(parseInt(portString)) || (address += ":" + portString) - } - return normalizeAddress(address) - } - return null - } - - function testNextConnectionMode(tests, index, server, options, resolve) { - if (index >= tests.length) return console.log("Tested all connection modes. Failing server connection."), void resolveFailure(self, resolve); - var mode = tests[index], - address = getServerAddress(server, mode), - skipTest = !1, - timeout = defaultTimeout; - if (mode === ConnectionMode.Local ? (!0, timeout = 8e3, stringEqualsIgnoreCase(address, server.ManualAddress) && (console.log("skipping LocalAddress test because it is the same as ManualAddress"), skipTest = !0)) : mode === ConnectionMode.Manual && stringEqualsIgnoreCase(address, server.LocalAddress) && (!0, timeout = 8e3), skipTest || !address) return console.log("skipping test at index " + index), void testNextConnectionMode(tests, index + 1, server, options, resolve); - console.log("testing connection mode " + mode + " with server " + server.Name), tryConnect(address, timeout).then(function(result) { - 1 === compareVersions(self.minServerVersion(), result.Version) ? (console.log("minServerVersion requirement not met. Server version: " + result.Version), resolve({ - State: "ServerUpdateNeeded", - Servers: [server] - })) : server.Id && result.Id !== server.Id ? (console.log("http request succeeded, but found a different server Id than what was expected"), resolveFailure(self, resolve)) : (console.log("calling onSuccessfulConnection with connection mode " + mode + " with server " + server.Name), onSuccessfulConnection(server, result, mode, options, resolve)) - }, function() { - console.log("test failed for connection mode " + mode + " with server " + server.Name), testNextConnectionMode(tests, index + 1, server, options, resolve) - }) - } - - function onSuccessfulConnection(server, systemInfo, connectionMode, options, resolve) { - var credentials = credentialProvider.credentials(); - options = options || {}, credentials.ConnectAccessToken && !1 !== options.enableAutoLogin ? ensureConnectUser(credentials).then(function() { - server.ExchangeToken ? addAuthenticationInfoFromConnect(server, connectionMode, credentials).then(function() { - afterConnectValidated(server, credentials, systemInfo, connectionMode, !0, options, resolve) - }, function() { - afterConnectValidated(server, credentials, systemInfo, connectionMode, !0, options, resolve) - }) : afterConnectValidated(server, credentials, systemInfo, connectionMode, !0, options, resolve) - }) : afterConnectValidated(server, credentials, systemInfo, connectionMode, !0, options, resolve) - } - - function afterConnectValidated(server, credentials, systemInfo, connectionMode, verifyLocalAuthentication, options, resolve) { - if (options = options || {}, !1 === options.enableAutoLogin) server.UserId = null, server.AccessToken = null; - else if (verifyLocalAuthentication && server.AccessToken && !1 !== options.enableAutoLogin) return void validateAuthentication(server, connectionMode).then(function() { - afterConnectValidated(server, credentials, systemInfo, connectionMode, !1, options, resolve) - }); - updateServerInfo(server, systemInfo), server.LastConnectionMode = connectionMode, !1 !== options.updateDateLastAccessed && (server.DateLastAccessed = (new Date).getTime()), credentialProvider.addOrUpdateServer(credentials.Servers, server), credentialProvider.credentials(credentials); - var result = { - Servers: [] - }; - result.ApiClient = self._getOrAddApiClient(server, connectionMode), result.ApiClient.setSystemInfo(systemInfo), result.State = server.AccessToken && !1 !== options.enableAutoLogin ? "SignedIn" : "ServerSignIn", result.Servers.push(server), result.ApiClient.enableAutomaticBitrateDetection = options.enableAutomaticBitrateDetection, result.ApiClient.updateServerInfo(server, connectionMode); - var resolveActions = function() { - resolve(result), events.trigger(self, "connected", [result]) + events.trigger(self, 'apiclientcreated', [apiClient]); + }; + + self.clearData = () => { + + console.log('connection manager clearing data'); + + connectUser = null; + const credentials = credentialProvider.credentials(); + credentials.ConnectAccessToken = null; + credentials.ConnectUserId = null; + credentials.Servers = []; + credentialProvider.credentials(credentials); + }; + + function onConnectUserSignIn(user) { + + connectUser = user; + events.trigger(self, 'connectusersignedin', [user]); + } + + self._getOrAddApiClient = (server, serverUrl) => { + + let apiClient = self.getApiClient(server.Id); + + if (!apiClient) { + + apiClient = new apiClientFactory(serverUrl, appName, appVersion, deviceName, deviceId, devicePixelRatio); + + self._apiClients.push(apiClient); + + apiClient.serverInfo(server); + + apiClient.onAuthenticated = (instance, result) => { + return onAuthenticated(instance, result, {}, true); }; - "SignedIn" === result.State ? (afterConnected(result.ApiClient, options), result.ApiClient.getCurrentUser().then(function(user) { - onLocalUserSignIn(server, connectionMode, user).then(resolveActions, resolveActions) - }, resolveActions)) : resolveActions() + + events.trigger(self, 'apiclientcreated', [apiClient]); } - function getCacheKey(feature, apiClient, options) { - options = options || {}; - var viewOnly = options.viewOnly, - cacheKey = "regInfo-" + apiClient.serverId(); - return viewOnly && (cacheKey += "-viewonly"), cacheKey + console.log('returning instance from getOrAddApiClient'); + return apiClient; + }; + + self.getOrCreateApiClient = serverId => { + + const credentials = credentialProvider.credentials(); + const servers = credentials.Servers.filter(s => stringEqualsIgnoreCase(s.Id, serverId)); + + if (!servers.length) { + throw new Error(`Server not found: ${serverId}`); + } + + const server = servers[0]; + + return self._getOrAddApiClient(server, getServerAddress(server, server.LastConnectionMode)); + }; + + function onAuthenticated(apiClient, result, options, saveCredentials) { + + const credentials = credentialProvider.credentials(); + const servers = credentials.Servers.filter(s => s.Id === result.ServerId); + + const server = servers.length ? servers[0] : apiClient.serverInfo(); + + if (options.updateDateLastAccessed !== false) { + server.DateLastAccessed = new Date().getTime(); } + server.Id = result.ServerId; - function addAppInfoToConnectRequest(request) { - request.headers = request.headers || {}, request.headers["X-Application"] = appName + "/" + appVersion + if (saveCredentials) { + server.UserId = result.User.Id; + server.AccessToken = result.AccessToken; + } else { + server.UserId = null; + server.AccessToken = null; } - function exchangePin(pinInfo) { - if (!pinInfo) throw new Error("pinInfo cannot be null"); - var request = { - type: "POST", - url: getConnectUrl("pin/authenticate"), - data: { - deviceId: pinInfo.DeviceId, - pin: pinInfo.Pin - }, - dataType: "json" - }; - return addAppInfoToConnectRequest(request), ajax(request) - } - console.log("Begin ConnectionManager constructor"); - var self = this; - this._apiClients = []; - var connectUser; - self.connectUser = function() { - return connectUser - }, self._minServerVersion = "3.2.33", self.appVersion = function() { - return appVersion - }, self.appName = function() { - return appName - }, self.capabilities = function() { - return capabilities - }, self.deviceId = function() { - return deviceId - }, self.credentialProvider = function() { - return credentialProvider - }, self.connectUserId = function() { - return credentialProvider.credentials().ConnectUserId - }, self.connectToken = function() { - return credentialProvider.credentials().ConnectAccessToken - }, self.getServerInfo = function(id) { - return credentialProvider.credentials().Servers.filter(function(s) { - return s.Id === id - })[0] - }, self.getLastUsedServer = function() { - var servers = credentialProvider.credentials().Servers; - return servers.sort(function(a, b) { - return (b.DateLastAccessed || 0) - (a.DateLastAccessed || 0) - }), servers.length ? servers[0] : null - }, self.getLastUsedApiClient = function() { - var servers = credentialProvider.credentials().Servers; - if (servers.sort(function(a, b) { - return (b.DateLastAccessed || 0) - (a.DateLastAccessed || 0) - }), !servers.length) return null; - var server = servers[0]; - return self._getOrAddApiClient(server, server.LastConnectionMode) - }, self.addApiClient = function(apiClient) { - self._apiClients.push(apiClient); - var existingServers = credentialProvider.credentials().Servers.filter(function(s) { - return stringEqualsIgnoreCase(s.ManualAddress, apiClient.serverAddress()) || stringEqualsIgnoreCase(s.LocalAddress, apiClient.serverAddress()) || stringEqualsIgnoreCase(s.RemoteAddress, apiClient.serverAddress()) - }), - existingServer = existingServers.length ? existingServers[0] : apiClient.serverInfo(); - if (existingServer.DateLastAccessed = (new Date).getTime(), existingServer.LastConnectionMode = ConnectionMode.Manual, existingServer.ManualAddress = apiClient.serverAddress(), apiClient.serverInfo(existingServer), apiClient.onAuthenticated = function(instance, result) { - return onAuthenticated(instance, result, {}, !0) - }, !existingServers.length) { - var credentials = credentialProvider.credentials(); - credentials.Servers = [existingServer], credentialProvider.credentials(credentials) - } - events.trigger(self, "apiclientcreated", [apiClient]) - }, self.clearData = function() { - console.log("connection manager clearing data"), connectUser = null; - var credentials = credentialProvider.credentials(); - credentials.ConnectAccessToken = null, credentials.ConnectUserId = null, credentials.Servers = [], credentialProvider.credentials(credentials) - }, self._getOrAddApiClient = function(server, connectionMode) { - var apiClient = self.getApiClient(server.Id); - if (!apiClient) { - var url = getServerAddress(server, connectionMode); - apiClient = new apiClientFactory(url, appName, appVersion, deviceName, deviceId, devicePixelRatio), self._apiClients.push(apiClient), apiClient.serverInfo(server), apiClient.onAuthenticated = function(instance, result) { - return onAuthenticated(instance, result, {}, !0) - }, events.trigger(self, "apiclientcreated", [apiClient]) + credentialProvider.addOrUpdateServer(credentials.Servers, server); + credentialProvider.credentials(credentials); + + // set this now before updating server info, otherwise it won't be set in time + apiClient.enableAutomaticBitrateDetection = options.enableAutomaticBitrateDetection; + + apiClient.serverInfo(server); + afterConnected(apiClient, options); + + return onLocalUserSignIn(server, apiClient.serverAddress(), result.User); + } + + function afterConnected(apiClient, options = {}) { + if (options.reportCapabilities !== false) { + apiClient.reportCapabilities(capabilities); + } + apiClient.enableAutomaticBitrateDetection = options.enableAutomaticBitrateDetection; + + if (options.enableWebSocket !== false) { + console.log('calling apiClient.ensureWebSocket'); + + apiClient.ensureWebSocket(); + } + } + + function onLocalUserSignIn(server, serverUrl, user) { + + // Ensure this is created so that listeners of the event can get the apiClient instance + self._getOrAddApiClient(server, serverUrl); + + // This allows the app to have a single hook that fires before any other + const promise = self.onLocalUserSignedIn ? self.onLocalUserSignedIn.call(self, user) : Promise.resolve(); + + return promise.then(() => { + events.trigger(self, 'localusersignedin', [user]); + }); + } + + function ensureConnectUser(credentials) { + + if (connectUser && connectUser.Id === credentials.ConnectUserId) { + return Promise.resolve(); + } + + else if (credentials.ConnectUserId && credentials.ConnectAccessToken) { + + connectUser = null; + + return getConnectUser(credentials.ConnectUserId, credentials.ConnectAccessToken).then(user => { + + onConnectUserSignIn(user); + return Promise.resolve(); + + }, () => Promise.resolve()); + + } else { + return Promise.resolve(); + } + } + + function getConnectUser(userId, accessToken) { + + if (!userId) { + throw new Error("null userId"); + } + if (!accessToken) { + throw new Error("null accessToken"); + } + + const url = `https://connect.emby.media/service/user?id=${userId}`; + + return ajax({ + type: "GET", + url, + dataType: "json", + headers: { + "X-Application": `${appName}/${appVersion}`, + "X-Connect-UserToken": accessToken } - return console.log("returning instance from getOrAddApiClient"), apiClient - }, self.getOrCreateApiClient = function(serverId) { - var credentials = credentialProvider.credentials(), - servers = credentials.Servers.filter(function(s) { - return stringEqualsIgnoreCase(s.Id, serverId) - }); - if (!servers.length) throw new Error("Server not found: " + serverId); - var server = servers[0]; - return self._getOrAddApiClient(server, server.LastConnectionMode) - }, self.user = function(apiClient) { - return new Promise(function(resolve, reject) { - function onLocalUserDone(e) { - var image = getImageUrl(localUser); - resolve({ - localUser: localUser, - name: connectUser ? connectUser.Name : localUser ? localUser.Name : null, - imageUrl: image.url, - supportsImageParams: image.supportsParams, - connectUser: connectUser - }) - } - function onEnsureConnectUserDone() { - apiClient && apiClient.getCurrentUserId() ? apiClient.getCurrentUser().then(function(u) { - localUser = u, onLocalUserDone() - }, onLocalUserDone) : onLocalUserDone() - } - var localUser, credentials = credentialProvider.credentials(); - !credentials.ConnectUserId || !credentials.ConnectAccessToken || apiClient && apiClient.getCurrentUserId() ? onEnsureConnectUserDone() : ensureConnectUser(credentials).then(onEnsureConnectUserDone, onEnsureConnectUserDone) - }) - }, self.logout = function() { - console.log("begin connectionManager loguot"); - for (var promises = [], i = 0, length = self._apiClients.length; i < length; i++) { - var apiClient = self._apiClients[i]; - apiClient.accessToken() && promises.push(logoutOfServer(apiClient)) + }); + } + + function addAuthenticationInfoFromConnect(server, serverUrl, credentials) { + + if (!server.ExchangeToken) { + throw new Error("server.ExchangeToken cannot be null"); + } + if (!credentials.ConnectUserId) { + throw new Error("credentials.ConnectUserId cannot be null"); + } + + const = getEmbyServerUrl(serverUrl, `Connect/Exchange?format=json&ConnectUserId=${credentials.ConnectUserId}`); + + const auth = `MediaBrowser Client="${appName}", Device="${deviceName}", DeviceId="${deviceId}", Version="${appVersion}"`; + + return ajax({ + type: "GET", + url, + dataType: "json", + headers: { + "X-MediaBrowser-Token": server.ExchangeToken, + "X-Emby-Authorization": auth } - return Promise.all(promises).then(function() { - for (var credentials = credentialProvider.credentials(), servers = credentials.Servers.filter(function(u) { - return "Guest" !== u.UserLinkType - }), j = 0, numServers = servers.length; j < numServers; j++) { - var server = servers[j]; - server.UserId = null, server.AccessToken = null, server.ExchangeToken = null - } - credentials.Servers = servers, credentials.ConnectAccessToken = null, credentials.ConnectUserId = null, credentialProvider.credentials(credentials), connectUser && (connectUser = null, events.trigger(self, "connectusersignedout")) - }) - }, self.getSavedServers = function() { - var credentials = credentialProvider.credentials(), - servers = credentials.Servers.slice(0); - return servers.sort(function(a, b) { - return (b.DateLastAccessed || 0) - (a.DateLastAccessed || 0) - }), servers - }, self.getAvailableServers = function() { - console.log("Begin getAvailableServers"); - var credentials = credentialProvider.credentials(); - return Promise.all([getConnectServers(credentials), findServers()]).then(function(responses) { - var connectServers = responses[0], - foundServers = responses[1], - servers = credentials.Servers.slice(0); - return mergeServers(credentialProvider, servers, foundServers), mergeServers(credentialProvider, servers, connectServers), servers = filterServers(servers, connectServers), servers.sort(function(a, b) { - return (b.DateLastAccessed || 0) - (a.DateLastAccessed || 0) - }), credentials.Servers = servers, credentialProvider.credentials(credentials), servers - }) - }, self.connectToServers = function(servers, options) { - console.log("Begin connectToServers, with " + servers.length + " servers"); - var firstServer = servers.length ? servers[0] : null; - return firstServer ? self.connectToServer(firstServer, options).then(function(result) { - return "Unavailable" === result.State && (result.State = "ServerSelection"), console.log("resolving connectToServers with result.State: " + result.State), result - }) : Promise.resolve({ - Servers: servers, - State: servers.length || self.connectUser() ? "ServerSelection" : "ConnectSignIn", - ConnectUser: self.connectUser() - }) - }, self.connectToServer = function(server, options) { - return console.log("begin connectToServer"), new Promise(function(resolve, reject) { - var tests = []; - server.LastConnectionMode, -1 === tests.indexOf(ConnectionMode.Manual) && tests.push(ConnectionMode.Manual), -1 === tests.indexOf(ConnectionMode.Local) && tests.push(ConnectionMode.Local), -1 === tests.indexOf(ConnectionMode.Remote) && tests.push(ConnectionMode.Remote), options = options || {}, console.log("beginning connection tests"), testNextConnectionMode(tests, 0, server, options, resolve) - }) - }, self.connectToAddress = function(address, options) { - function onFail() { - return console.log("connectToAddress " + address + " failed"), Promise.resolve({ - State: "Unavailable", - ConnectUser: instance.connectUser() - }) + + }).then(auth => { + + server.UserId = auth.LocalUserId; + server.AccessToken = auth.AccessToken; + return auth; + + }, () => { + + server.UserId = null; + server.AccessToken = null; + return Promise.reject(); + + }); + } + + function validateAuthentication(server, serverUrl) { + + return ajax({ + + type: "GET", + url: getEmbyServerUrl(serverUrl, "System/Info"), + dataType: "json", + headers: { + "X-MediaBrowser-Token": server.AccessToken } - if (!address) return Promise.reject(); - address = normalizeAddress(address); - var instance = this, - server = { - ManualAddress: address, - LastConnectionMode: ConnectionMode.Manual - }; - return self.connectToServer(server, options).catch(onFail) - }, self.loginToConnect = function(username, password) { - return username && password ? ajax({ - type: "POST", - url: "https://connect.emby.media/service/user/authenticate", - data: { - nameOrEmail: username, - rawpw: password - }, - dataType: "json", - contentType: "application/x-www-form-urlencoded; charset=UTF-8", - headers: { - "X-Application": appName + "/" + appVersion - } - }).then(function(result) { - var credentials = credentialProvider.credentials(); - return credentials.ConnectAccessToken = result.AccessToken, credentials.ConnectUserId = result.User.Id, credentialProvider.credentials(credentials), onConnectUserSignIn(result.User), result - }) : Promise.reject() - }, self.signupForConnect = function(options) { - var email = options.email, - username = options.username, - password = options.password, - passwordConfirm = options.passwordConfirm; - if (!email) return Promise.reject({ - errorCode: "invalidinput" - }); - if (!username) return Promise.reject({ - errorCode: "invalidinput" - }); - if (!password) return Promise.reject({ - errorCode: "invalidinput" - }); - if (!passwordConfirm) return Promise.reject({ - errorCode: "passwordmatch" - }); - if (password !== passwordConfirm) return Promise.reject({ - errorCode: "passwordmatch" - }); - var data = { - email: email, - userName: username, - rawpw: password + + }).then(systemInfo => { + + updateServerInfo(server, systemInfo); + return Promise.resolve(); + + }, () => { + + server.UserId = null; + server.AccessToken = null; + return Promise.resolve(); + }); + } + + function getImageUrl(localUser) { + + if (connectUser && connectUser.ImageUrl) { + return { + url: connectUser.ImageUrl }; - return options.grecaptcha && (data.grecaptcha = options.grecaptcha), ajax({ - type: "POST", - url: "https://connect.emby.media/service/register", - data: data, - dataType: "json", - contentType: "application/x-www-form-urlencoded; charset=UTF-8", - headers: { - "X-Application": appName + "/" + appVersion, - "X-CONNECT-TOKEN": "CONNECT-REGISTER" - } - }).catch(function(response) { - try { - return response.json() - } catch (err) { - throw err - } - }).then(function(result) { - if (result && result.Status) return "SUCCESS" === result.Status ? Promise.resolve(result) : Promise.reject({ - errorCode: result.Status - }); - Promise.reject() - }) - }, self.getUserInvitations = function() { - var connectToken = self.connectToken(); - if (!connectToken) throw new Error("null connectToken"); - if (!self.connectUserId()) throw new Error("null connectUserId"); - return ajax({ - type: "GET", - url: "https://connect.emby.media/service/servers?userId=" + self.connectUserId() + "&status=Waiting", - dataType: "json", - headers: { - "X-Connect-UserToken": connectToken, - "X-Application": appName + "/" + appVersion - } - }) - }, self.deleteServer = function(serverId) { - if (!serverId) throw new Error("null serverId"); - var server = credentialProvider.credentials().Servers.filter(function(s) { - return s.Id === serverId - }); - return server = server.length ? server[0] : null, new Promise(function(resolve, reject) { - function onDone() { - var credentials = credentialProvider.credentials(); - credentials.Servers = credentials.Servers.filter(function(s) { - return s.Id !== serverId - }), credentialProvider.credentials(credentials), resolve() - } - if (!server.ConnectServerId) return void onDone(); - var connectToken = self.connectToken(), - connectUserId = self.connectUserId(); - if (!connectToken || !connectUserId) return void onDone(); - ajax({ - type: "DELETE", - url: "https://connect.emby.media/service/serverAuthorizations?serverId=" + server.ConnectServerId + "&userId=" + connectUserId, - headers: { - "X-Connect-UserToken": connectToken, - "X-Application": appName + "/" + appVersion - } - }).then(onDone, onDone) - }) - }, self.rejectServer = function(serverId) { - var connectToken = self.connectToken(); - if (!serverId) throw new Error("null serverId"); - if (!connectToken) throw new Error("null connectToken"); - if (!self.connectUserId()) throw new Error("null connectUserId"); - var url = "https://connect.emby.media/service/serverAuthorizations?serverId=" + serverId + "&userId=" + self.connectUserId(); - return fetch(url, { - method: "DELETE", - headers: { - "X-Connect-UserToken": connectToken, - "X-Application": appName + "/" + appVersion - } - }) - }, self.acceptServer = function(serverId) { - var connectToken = self.connectToken(); - if (!serverId) throw new Error("null serverId"); - if (!connectToken) throw new Error("null connectToken"); - if (!self.connectUserId()) throw new Error("null connectUserId"); - return ajax({ - type: "GET", - url: "https://connect.emby.media/service/ServerAuthorizations/accept?serverId=" + serverId + "&userId=" + self.connectUserId(), - headers: { - "X-Connect-UserToken": connectToken, - "X-Application": appName + "/" + appVersion - } - }) - }, self.resetRegistrationInfo = function(apiClient) { - var cacheKey = getCacheKey("themes", apiClient, { - viewOnly: !0 + } + if (localUser && localUser.PrimaryImageTag) { + + const apiClient = self.getApiClient(localUser); + + const url = apiClient.getUserImageUrl(localUser.Id, { + tag: localUser.PrimaryImageTag, + type: "Primary" }); - appStorage.removeItem(cacheKey), cacheKey = getCacheKey("themes", apiClient, { - viewOnly: !1 - }), appStorage.removeItem(cacheKey) - }, self.getRegistrationInfo = function(feature, apiClient, options) { - var cacheKey = getCacheKey(feature, apiClient, options); - appStorage.setItem(cacheKey, JSON.stringify({ - lastValidDate: new Date().getTime(), - deviceId: self.deviceId() - })); - return Promise.resolve(); - }, self.createPin = function() { - var request = { - type: "POST", - url: getConnectUrl("pin"), - data: { - deviceId: deviceId - }, - dataType: "json" + + return { + url, + supportsParams: true }; - return addAppInfoToConnectRequest(request), ajax(request) - }, self.getPinStatus = function(pinInfo) { - if (!pinInfo) throw new Error("pinInfo cannot be null"); - var queryString = { - deviceId: pinInfo.DeviceId, - pin: pinInfo.Pin - }, - request = { - type: "GET", - url: getConnectUrl("pin") + "?" + paramsToString(queryString), - dataType: "json" - }; - return addAppInfoToConnectRequest(request), ajax(request) - }, self.exchangePin = function(pinInfo) { - if (!pinInfo) throw new Error("pinInfo cannot be null"); - return exchangePin(pinInfo).then(function(result) { - var credentials = credentialProvider.credentials(); - return credentials.ConnectAccessToken = result.AccessToken, credentials.ConnectUserId = result.UserId, credentialProvider.credentials(credentials), ensureConnectUser(credentials) - }) } - }; - return ConnectionManager.prototype.connect = function(options) { - console.log("Begin connect"); - var instance = this; - return instance.getAvailableServers().then(function(servers) { - return instance.connectToServers(servers, options) - }) - }, ConnectionManager.prototype.isLoggedIntoConnect = function() { - return !(!this.connectToken() || !this.connectUserId()) - }, ConnectionManager.prototype.getApiClients = function() { - for (var servers = this.getSavedServers(), i = 0, length = servers.length; i < length; i++) { - var server = servers[i]; - server.Id && this._getOrAddApiClient(server, server.LastConnectionMode) - } - return this._apiClients - }, ConnectionManager.prototype.getApiClient = function(item) { - if (!item) throw new Error("item or serverId cannot be null"); - return item.ServerId && (item = item.ServerId), this._apiClients.filter(function(a) { - var serverInfo = a.serverInfo(); - return !serverInfo || serverInfo.Id === item - })[0] - }, ConnectionManager.prototype.minServerVersion = function(val) { - return val && (this._minServerVersion = val), this._minServerVersion - }, ConnectionManager.prototype.handleMessageReceived = function(msg) { - var serverId = msg.ServerId; - if (serverId) { - var apiClient = this.getApiClient(serverId); - apiClient && apiClient.handleMessageReceived(msg) + + return { + url: null, + supportsParams: false + }; } - }, ConnectionManager -}); + + self.user = apiClient => new Promise((resolve, reject) => { + + let localUser; + + function onLocalUserDone(e) { + + const image = getImageUrl(localUser); + + resolve({ + localUser, + name: connectUser ? connectUser.Name : (localUser ? localUser.Name : null), + imageUrl: image.url, + supportsImageParams: image.supportsParams, + connectUser + }); + } + + function onEnsureConnectUserDone() { + + if (apiClient && apiClient.getCurrentUserId()) { + apiClient.getCurrentUser().then(u => { + localUser = u; + onLocalUserDone(); + + }, onLocalUserDone); + } else { + onLocalUserDone(); + } + } + + const credentials = credentialProvider.credentials(); + + if (credentials.ConnectUserId && credentials.ConnectAccessToken && !(apiClient && apiClient.getCurrentUserId())) { + ensureConnectUser(credentials).then(onEnsureConnectUserDone, onEnsureConnectUserDone); + } else { + onEnsureConnectUserDone(); + } + }); + + self.logout = () => { + + console.log('begin connectionManager loguot'); + const promises = []; + + for (let i = 0, length = self._apiClients.length; i < length; i++) { + + const apiClient = self._apiClients[i]; + + if (apiClient.accessToken()) { + promises.push(logoutOfServer(apiClient)); + } + } + + return Promise.all(promises).then(() => { + + const credentials = credentialProvider.credentials(); + + const servers = credentials.Servers.filter(u => u.UserLinkType !== "Guest"); + + for (let j = 0, numServers = servers.length; j < numServers; j++) { + + const server = servers[j]; + + server.UserId = null; + server.AccessToken = null; + server.ExchangeToken = null; + } + + credentials.Servers = servers; + credentials.ConnectAccessToken = null; + credentials.ConnectUserId = null; + + credentialProvider.credentials(credentials); + + if (connectUser) { + connectUser = null; + events.trigger(self, 'connectusersignedout'); + } + }); + }; + + function logoutOfServer(apiClient) { + + const serverInfo = apiClient.serverInfo() || {}; + + const logoutInfo = { + serverId: serverInfo.Id + }; + + return apiClient.logout().then(() => { + + events.trigger(self, 'localusersignedout', [logoutInfo]); + }, () => { + + events.trigger(self, 'localusersignedout', [logoutInfo]); + }); + } + + function getConnectServers(credentials) { + + console.log('Begin getConnectServers'); + + if (!credentials.ConnectAccessToken || !credentials.ConnectUserId) { + return Promise.resolve([]); + } + + const url = `https://connect.emby.media/service/servers?userId=${credentials.ConnectUserId}`; + + return ajax({ + type: "GET", + url, + dataType: "json", + headers: { + "X-Application": `${appName}/${appVersion}`, + "X-Connect-UserToken": credentials.ConnectAccessToken + } + + }).then(servers => servers.map(i => ({ + ExchangeToken: i.AccessKey, + ConnectServerId: i.Id, + Id: i.SystemId, + Name: i.Name, + RemoteAddress: i.Url, + LocalAddress: i.LocalAddress, + UserLinkType: (i.UserType || '').toLowerCase() === "guest" ? "Guest" : "LinkedUser" + })), () => credentials.Servers.slice(0).filter(s => s.ExchangeToken)); + } + + self.getSavedServers = () => { + + const credentials = credentialProvider.credentials(); + + const servers = credentials.Servers.slice(0); + + servers.sort((a, b) => (b.DateLastAccessed || 0) - (a.DateLastAccessed || 0)); + + return servers; + }; + + self.getAvailableServers = () => { + + console.log('Begin getAvailableServers'); + + // Clone the array + const credentials = credentialProvider.credentials(); + + return Promise.all([getConnectServers(credentials), findServers()]).then(responses => { + + const connectServers = responses[0]; + const foundServers = responses[1]; + + let servers = credentials.Servers.slice(0); + mergeServers(credentialProvider, servers, foundServers); + mergeServers(credentialProvider, servers, connectServers); + + servers = filterServers(servers, connectServers); + + servers.sort((a, b) => (b.DateLastAccessed || 0) - (a.DateLastAccessed || 0)); + + credentials.Servers = servers; + + credentialProvider.credentials(credentials); + + return servers; + }); + }; + + function filterServers(servers, connectServers) { + + return servers.filter(server => { + + // It's not a connect server, so assume it's still valid + if (!server.ExchangeToken) { + return true; + } + + return connectServers.filter(connectServer => server.Id === connectServer.Id).length > 0; + }); + } + + function findServers() { + + return new Promise((resolve, reject) => { + + const onFinish = foundServers => { + const servers = foundServers.map(foundServer => { + + const info = { + Id: foundServer.Id, + LocalAddress: convertEndpointAddressToManualAddress(foundServer) || foundServer.Address, + Name: foundServer.Name + }; + + info.LastConnectionMode = info.ManualAddress ? ConnectionMode.Manual : ConnectionMode.Local; + + return info; + }); + resolve(servers); + }; + + serverDiscoveryFn().then(serverDiscovery => { + serverDiscovery.default.findServers(1000).then(onFinish, () => { + onFinish([]); + }); + + }); + }); + } + + function convertEndpointAddressToManualAddress(info) { + + if (info.Address && info.EndpointAddress) { + let address = info.EndpointAddress.split(":")[0]; + + // Determine the port, if any + const parts = info.Address.split(":"); + if (parts.length > 1) { + const portString = parts[parts.length - 1]; + + if (!isNaN(parseInt(portString))) { + address += `:${portString}`; + } + } + + return normalizeAddress(address); + } + + return null; + } + + self.connectToServers = (servers, options) => { + + console.log(`Begin connectToServers, with ${servers.length} servers`); + + const firstServer = servers.length ? servers[0] : null; + // See if we have any saved credentials and can auto sign in + if (firstServer) { + return self.connectToServer(firstServer, options).then((result) => { + + if (result.State === 'Unavailable') { + + result.State = 'ServerSelection'; + } + + console.log('resolving connectToServers with result.State: ' + result.State); + return result; + }); + } + + return Promise.resolve({ + Servers: servers, + State: (!servers.length && !self.connectUser()) ? 'ConnectSignIn' : 'ServerSelection', + ConnectUser: self.connectUser() + }); + }; + + function getTryConnectPromise(url, connectionMode, state, resolve, reject) { + + console.log('getTryConnectPromise ' + url); + + ajax({ + + url: getEmbyServerUrl(url, 'system/info/public'), + timeout: defaultTimeout, + type: 'GET', + dataType: 'json' + + }).then((result) => { + + if (!state.resolved) { + state.resolved = true; + + console.log("Reconnect succeeded to " + url); + resolve({ + url: url, + connectionMode: connectionMode, + data: result + }); + } + + }, () => { + + console.log("Reconnect failed to " + url); + + if (!state.resolved) { + state.rejects++; + if (state.rejects >= state.numAddresses) { + reject(); + } + } + }); + } + + function tryReconnect(serverInfo) { + + const addresses = []; + const addressesStrings = []; + + // the timeouts are a small hack to try and ensure the remote address doesn't resolve first + + // manualAddressOnly is used for the local web app that always connects to a fixed address + if (!serverInfo.manualAddressOnly && serverInfo.LocalAddress && addressesStrings.indexOf(serverInfo.LocalAddress) === -1) { + addresses.push({ url: serverInfo.LocalAddress, mode: ConnectionMode.Local, timeout: 0 }); + addressesStrings.push(addresses[addresses.length - 1].url); + } + if (serverInfo.ManualAddress && addressesStrings.indexOf(serverInfo.ManualAddress) === -1) { + addresses.push({ url: serverInfo.ManualAddress, mode: ConnectionMode.Manual, timeout: 100 }); + addressesStrings.push(addresses[addresses.length - 1].url); + } + if (!serverInfo.manualAddressOnly && serverInfo.RemoteAddress && addressesStrings.indexOf(serverInfo.RemoteAddress) === -1) { + addresses.push({ url: serverInfo.RemoteAddress, mode: ConnectionMode.Remote, timeout: 200 }); + addressesStrings.push(addresses[addresses.length - 1].url); + } + + console.log('tryReconnect: ' + addressesStrings.join('|')); + + return new Promise((resolve, reject) => { + + const state = {}; + state.numAddresses = addresses.length; + state.rejects = 0; + + addresses.map((url) => { + + setTimeout(() => { + if (!state.resolved) { + getTryConnectPromise(url.url, url.mode, state, resolve, reject); + } + + }, url.timeout); + }); + }); + } + + self.connectToServer = (server, options) => { + + console.log('begin connectToServer'); + + return new Promise((resolve, reject) => { + + options = options || {}; + + tryReconnect(server).then((result) => { + + const serverUrl = result.url; + const connectionMode = result.connectionMode; + result = result.data; + + if (compareVersions(self.minServerVersion(), result.Version) === 1) { + + console.log('minServerVersion requirement not met. Server version: ' + result.Version); + resolve({ + State: 'ServerUpdateNeeded', + Servers: [server] + }); + + } + else if (server.Id && result.Id !== server.Id) { + + console.log('http request succeeded, but found a different server Id than what was expected'); + resolveFailure(self, resolve); + + } + else { + onSuccessfulConnection(server, result, connectionMode, serverUrl, options, resolve); + } + + }, () => { + + resolveFailure(self, resolve); + }); + }); + }; + + function onSuccessfulConnection(server, systemInfo, connectionMode, serverUrl, options, resolve) { + + const credentials = credentialProvider.credentials(); + options = options || {}; + if (credentials.ConnectAccessToken && options.enableAutoLogin !== false) { + + ensureConnectUser(credentials).then(() => { + + if (server.ExchangeToken) { + addAuthenticationInfoFromConnect(server, serverUrl, credentials).then(() => { + + afterConnectValidated(server, credentials, systemInfo, connectionMode, serverUrl, true, options, resolve); + + }, () => { + + afterConnectValidated(server, credentials, systemInfo, connectionMode, serverUrl, true, options, resolve); + }); + + } else { + + afterConnectValidated(server, credentials, systemInfo, connectionMode, serverUrl, true, options, resolve); + } + }); + } + else { + afterConnectValidated(server, credentials, systemInfo, connectionMode, serverUrl, true, options, resolve); + } + } + + function afterConnectValidated( + server, + credentials, + systemInfo, + connectionMode, + serverUrl, + verifyLocalAuthentication, + options = {}, + resolve) { + if (options.enableAutoLogin === false) { + + server.UserId = null; + server.AccessToken = null; + + } else if (verifyLocalAuthentication && server.AccessToken && options.enableAutoLogin !== false) { + + validateAuthentication(server, serverUrl).then(() => { + + afterConnectValidated(server, credentials, systemInfo, connectionMode, serverUrl, false, options, resolve); + }); + + return; + } + + updateServerInfo(server, systemInfo); + + server.LastConnectionMode = connectionMode; + + if (options.updateDateLastAccessed !== false) { + server.DateLastAccessed = new Date().getTime(); + } + credentialProvider.addOrUpdateServer(credentials.Servers, server); + credentialProvider.credentials(credentials); + + const result = { + Servers: [] + }; + + result.ApiClient = self._getOrAddApiClient(server, serverUrl); + + result.ApiClient.setSystemInfo(systemInfo); + + result.State = server.AccessToken && options.enableAutoLogin !== false ? + 'SignedIn' : + 'ServerSignIn'; + + result.Servers.push(server); + + // set this now before updating server info, otherwise it won't be set in time + result.ApiClient.enableAutomaticBitrateDetection = options.enableAutomaticBitrateDetection; + + result.ApiClient.updateServerInfo(server, serverUrl); + + const resolveActions = function () { + resolve(result); + + events.trigger(self, 'connected', [result]); + }; + + if (result.State === 'SignedIn') { + afterConnected(result.ApiClient, options); + + result.ApiClient.getCurrentUser().then((user) => { + onLocalUserSignIn(server, serverUrl, user).then(resolveActions, resolveActions); + }, resolveActions); + } + else { + resolveActions(); + } + } + + self.connectToAddress = function (address, options) { + + if (!address) { + return Promise.reject(); + } + + address = normalizeAddress(address); + const instance = this; + + function onFail() { + console.log(`connectToAddress ${address} failed`); + return Promise.resolve({ + State: 'Unavailable', + ConnectUser: instance.connectUser() + }); + } + + const server = { + ManualAddress: address, + LastConnectionMode: ConnectionMode.Manual + }; + + return self.connectToServer(server, options).catch(onFail); + }; + + self.loginToConnect = (username, password) => { + + if (!username) { + return Promise.reject(); + } + if (!password) { + return Promise.reject(); + } + + return ajax({ + type: "POST", + url: "https://connect.emby.media/service/user/authenticate", + data: { + nameOrEmail: username, + rawpw: password + }, + dataType: "json", + contentType: 'application/x-www-form-urlencoded; charset=UTF-8', + headers: { + "X-Application": `${appName}/${appVersion}` + } + + }).then(result => { + + const credentials = credentialProvider.credentials(); + + credentials.ConnectAccessToken = result.AccessToken; + credentials.ConnectUserId = result.User.Id; + + credentialProvider.credentials(credentials); + + onConnectUserSignIn(result.User); + + return result; + }); + }; + + self.signupForConnect = options => { + + const email = options.email; + const username = options.username; + const password = options.password; + const passwordConfirm = options.passwordConfirm; + + if (!email) { + return Promise.reject({ errorCode: 'invalidinput' }); + } + if (!username) { + return Promise.reject({ errorCode: 'invalidinput' }); + } + if (!password) { + return Promise.reject({ errorCode: 'invalidinput' }); + } + if (!passwordConfirm) { + return Promise.reject({ errorCode: 'passwordmatch' }); + } + if (password !== passwordConfirm) { + return Promise.reject({ errorCode: 'passwordmatch' }); + } + + const data = { + email, + userName: username, + rawpw: password + }; + + if (options.grecaptcha) { + data.grecaptcha = options.grecaptcha; + } + + return ajax({ + type: "POST", + url: "https://connect.emby.media/service/register", + data, + dataType: "json", + contentType: 'application/x-www-form-urlencoded; charset=UTF-8', + headers: { + "X-Application": `${appName}/${appVersion}`, + "X-CONNECT-TOKEN": "CONNECT-REGISTER" + } + + }).catch(response => { + + try { + return response.json(); + } catch (err) { + throw err; + } + + }).then(result => { + if (result && result.Status) { + + if (result.Status === 'SUCCESS') { + return Promise.resolve(result); + } + return Promise.reject({ errorCode: result.Status }); + } else { + Promise.reject(); + } + }); + }; + + self.getUserInvitations = () => { + + const connectToken = self.connectToken(); + + if (!connectToken) { + throw new Error("null connectToken"); + } + if (!self.connectUserId()) { + throw new Error("null connectUserId"); + } + + const url = `https://connect.emby.media/service/servers?userId=${self.connectUserId()}&status=Waiting`; + + return ajax({ + type: "GET", + url, + dataType: "json", + headers: { + "X-Connect-UserToken": connectToken, + "X-Application": `${appName}/${appVersion}` + } + + }); + }; + + self.deleteServer = serverId => { + + if (!serverId) { + throw new Error("null serverId"); + } + + let server = credentialProvider.credentials().Servers.filter(s => s.Id === serverId); + server = server.length ? server[0] : null; + + return new Promise((resolve, reject) => { + + function onDone() { + const credentials = credentialProvider.credentials(); + + credentials.Servers = credentials.Servers.filter(s => s.Id !== serverId); + + credentialProvider.credentials(credentials); + resolve(); + } + + if (!server.ConnectServerId) { + onDone(); + return; + } + + const connectToken = self.connectToken(); + const connectUserId = self.connectUserId(); + + if (!connectToken || !connectUserId) { + onDone(); + return; + } + + const url = `https://connect.emby.media/service/serverAuthorizations?serverId=${server.ConnectServerId}&userId=${connectUserId}`; + + ajax({ + type: "DELETE", + url, + headers: { + "X-Connect-UserToken": connectToken, + "X-Application": `${appName}/${appVersion}` + } + + }).then(onDone, onDone); + }); + }; + + self.rejectServer = serverId => { + + const connectToken = self.connectToken(); + + if (!serverId) { + throw new Error("null serverId"); + } + if (!connectToken) { + throw new Error("null connectToken"); + } + if (!self.connectUserId()) { + throw new Error("null connectUserId"); + } + + const url = `https://connect.emby.media/service/serverAuthorizations?serverId=${serverId}&userId=${self.connectUserId()}`; + + return fetch(url, { + method: "DELETE", + headers: { + "X-Connect-UserToken": connectToken, + "X-Application": `${appName}/${appVersion}` + } + }); + }; + + self.acceptServer = serverId => { + + const connectToken = self.connectToken(); + + if (!serverId) { + throw new Error("null serverId"); + } + if (!connectToken) { + throw new Error("null connectToken"); + } + if (!self.connectUserId()) { + throw new Error("null connectUserId"); + } + + const url = `https://connect.emby.media/service/ServerAuthorizations/accept?serverId=${serverId}&userId=${self.connectUserId()}`; + + return ajax({ + type: "GET", + url, + headers: { + "X-Connect-UserToken": connectToken, + "X-Application": `${appName}/${appVersion}` + } + + }); + }; + + function getCacheKey(feature, apiClient, options = {}) { + const viewOnly = options.viewOnly; + + let cacheKey = `regInfo-${apiClient.serverId()}`; + + if (viewOnly) { + cacheKey += '-viewonly'; + } + + return cacheKey; + } + + self.resetRegistrationInfo = apiClient => { + + let cacheKey = getCacheKey('themes', apiClient, { viewOnly: true }); + appStorage.removeItem(cacheKey); + + cacheKey = getCacheKey('themes', apiClient, { viewOnly: false }); + appStorage.removeItem(cacheKey); + }; + + self.getRegistrationInfo = (feature, apiClient, options) => { + var cacheKey = getCacheKey(feature, apiClient, options); + appStorage.setItem(cacheKey, JSON.stringify({ + lastValidDate: new Date().getTime(), + deviceId: self.deviceId() + })); + return Promise.resolve(); + }; + + function addAppInfoToConnectRequest(request) { + request.headers = request.headers || {}; + request.headers['X-Application'] = `${appName}/${appVersion}`; + } + + self.createPin = () => { + + const request = { + type: 'POST', + url: getConnectUrl('pin'), + data: { + deviceId + }, + dataType: 'json' + }; + + addAppInfoToConnectRequest(request); + + return ajax(request); + }; + + self.getPinStatus = pinInfo => { + + if (!pinInfo) { + throw new Error('pinInfo cannot be null'); + } + + const queryString = { + deviceId: pinInfo.DeviceId, + pin: pinInfo.Pin + }; + + const request = { + type: 'GET', + url: `${getConnectUrl('pin')}?${paramsToString(queryString)}`, + dataType: 'json' + }; + + addAppInfoToConnectRequest(request); + + return ajax(request); + + }; + + function exchangePin(pinInfo) { + + if (!pinInfo) { + throw new Error('pinInfo cannot be null'); + } + + const request = { + type: 'POST', + url: getConnectUrl('pin/authenticate'), + data: { + deviceId: pinInfo.DeviceId, + pin: pinInfo.Pin + }, + dataType: 'json' + }; + + addAppInfoToConnectRequest(request); + + return ajax(request); + } + + self.exchangePin = pinInfo => { + + if (!pinInfo) { + throw new Error('pinInfo cannot be null'); + } + + return exchangePin(pinInfo).then(result => { + + const credentials = credentialProvider.credentials(); + credentials.ConnectAccessToken = result.AccessToken; + credentials.ConnectUserId = result.UserId; + credentialProvider.credentials(credentials); + + return ensureConnectUser(credentials); + }); + }; + } + + connect(options) { + + console.log('Begin connect'); + + const instance = this; + + return instance.getAvailableServers().then(servers => instance.connectToServers(servers, options)); + } + + handleMessageReceived(msg) { + + const serverId = msg.ServerId; + if (serverId) { + const apiClient = this.getApiClient(serverId); + if (apiClient) { + + if (typeof (msg.Data) === 'string') { + try { + msg.Data = JSON.parse(msg.Data); + } + catch (err) { + } + } + + apiClient.handleMessageReceived(msg); + } + } + } + + isLoggedIntoConnect() { + + // Make sure it returns true or false + if (!this.connectToken() || !this.connectUserId()) { + return false; + } + return true; + } + + getApiClients() { + + const servers = this.getSavedServers(); + + for (let i = 0, length = servers.length; i < length; i++) { + const server = servers[i]; + if (server.Id) { + this._getOrAddApiClient(server, getServerAddress(server, server.LastConnectionMode)); + } + } + + return this._apiClients; + } + + getApiClient(item) { + + if (!item) { + throw new Error('item or serverId cannot be null'); + } + + // Accept string + object + if (item.ServerId) { + item = item.ServerId; + } + + return this._apiClients.filter(a => { + + const serverInfo = a.serverInfo(); + + // We have to keep this hack in here because of the addApiClient method + return !serverInfo || serverInfo.Id === item; + + })[0]; + } + + minServerVersion(val) { + + if (val) { + this._minServerVersion = val; + } + + return this._minServerVersion; + } +}