diff --git a/apiclient.js b/apiclient.js new file mode 100644 index 0000000..e5dd53f --- /dev/null +++ b/apiclient.js @@ -0,0 +1,3416 @@ +(function (globalScope, JSON, WebSocket, setTimeout, devicePixelRatio, FileReader) { + + if (!globalScope.MediaBrowser) { + globalScope.MediaBrowser = {}; + } + + /** + * Creates a new api client instance + * @param {String} serverAddress + * @param {String} clientName s + * @param {String} applicationVersion + */ + globalScope.MediaBrowser.ApiClient = function (logger, serverAddress, clientName, applicationVersion, deviceName, deviceId) { + + if (!serverAddress) { + throw new Error("Must supply a serverAddress"); + } + + logger.log('ApiClient serverAddress: ' + serverAddress); + logger.log('ApiClient clientName: ' + clientName); + logger.log('ApiClient applicationVersion: ' + applicationVersion); + logger.log('ApiClient deviceName: ' + deviceName); + logger.log('ApiClient deviceId: ' + deviceId); + + var self = this; + var webSocket; + var serverInfo = {}; + + self.enableAppStorePolicy = false; + + /** + * Gets the server address. + */ + self.serverAddress = function (val) { + + if (val != null) { + + if (val.toLowerCase().indexOf('http') != 0) { + throw new Error('Invalid url: ' + val); + } + + var changed = val != serverAddress; + + serverAddress = val; + + if (changed) { + Events.trigger(this, 'serveraddresschanged'); + } + } + + return serverAddress; + }; + + self.serverInfo = function (info) { + + serverInfo = info || serverInfo; + + return serverInfo; + }; + + var currentUser; + /** + * Gets or sets the current user id. + */ + self.getCurrentUser = function () { + + if (currentUser) { + return new Promise(function (resolve, reject) { + + resolve(currentUser); + }); + } + + var userId = self.getCurrentUserId(); + + if (!userId) { + return new Promise(function (resolve, reject) { + + reject(); + }); + } + + return self.getUser(userId).then(function (user) { + currentUser = user; + return user; + }); + }; + + /** + * Gets or sets the current user id. + */ + self.getCurrentUserId = function () { + + return serverInfo.UserId; + }; + + self.accessToken = function () { + return serverInfo.AccessToken; + }; + + self.deviceName = function () { + return deviceName; + }; + + self.deviceId = function () { + return deviceId; + }; + + self.appName = function () { + return clientName; + }; + + self.appVersion = function () { + return applicationVersion; + }; + + self.clearAuthenticationInfo = function () { + self.setAuthenticationInfo(null, null); + }; + + self.setAuthenticationInfo = function (accessKey, userId) { + currentUser = null; + + serverInfo.AccessToken = accessKey; + serverInfo.UserId = userId; + }; + + self.encodeName = function (name) { + + name = name.split('/').join('-'); + name = name.split('&').join('-'); + name = name.split('?').join('-'); + + var val = paramsToString({ name: name }); + return val.substring(val.indexOf('=') + 1).replace("'", '%27'); + }; + + function onFetchFail(url, response) { + + Events.trigger(self, 'requestfail', [ + { + url: url, + status: response.status, + errorCode: response.headers ? response.headers["X-Application-Error-Code"] : null + }]); + } + + self.setRequestHeaders = function (headers) { + + var currentServerInfo = self.serverInfo(); + + if (clientName) { + + var auth = 'MediaBrowser Client="' + clientName + '", Device="' + deviceName + '", DeviceId="' + deviceId + '", Version="' + applicationVersion + '"'; + + var userId = currentServerInfo.UserId; + + if (userId) { + auth += ', UserId="' + userId + '"'; + } + + headers["X-Emby-Authorization"] = auth; + } + + var accessToken = currentServerInfo.AccessToken; + + if (accessToken) { + headers['X-MediaBrowser-Token'] = accessToken; + } + }; + + /** + * Wraps around jQuery ajax methods to add additional info to the request. + */ + self.ajax = function (request, includeAuthorization) { + + if (!request) { + throw new Error("Request cannot be null"); + } + + return self.fetch(request, includeAuthorization); + }; + + function getFetchPromise(request) { + + var headers = request.headers || {}; + + if (request.dataType == 'json') { + headers.accept = 'application/json'; + } + + var fetchRequest = { + headers: headers, + method: request.type + }; + + var 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'; + } + } + + if (contentType) { + + headers['Content-Type'] = contentType; + } + + if (!request.timeout) { + return fetch(request.url, fetchRequest); + } + + return fetchWithTimeout(request.url, fetchRequest, request.timeout); + } + + function fetchWithTimeout(url, options, timeoutMs) { + + return new Promise(function (resolve, reject) { + + var timeout = setTimeout(reject, timeoutMs); + + fetch(url, options).then(function (response) { + clearTimeout(timeout); + resolve(response); + }, function (error) { + clearTimeout(timeout); + reject(); + }); + }); + } + + function paramsToString(params) { + + var values = []; + + for (var key in params) { + + var value = params[key]; + + if (value !== null && value !== undefined && value !== '') { + values.push(encodeURIComponent(key) + "=" + encodeURIComponent(value)); + } + } + return values.join('&'); + } + + /** + * Wraps around jQuery ajax methods to add additional info to the request. + */ + self.fetch = function (request, includeAuthorization) { + + if (!request) { + throw new Error("Request cannot be null"); + } + + request.headers = request.headers || {}; + + if (includeAuthorization !== false) { + + self.setRequestHeaders(request.headers); + } + + if (self.enableAutomaticNetworking === false || request.type != "GET") { + logger.log('Requesting url without automatic networking: ' + request.url); + + return getFetchPromise(request).then(function (response) { + + if (response.status < 400) { + + if (request.dataType == 'json' || request.headers.accept == 'application/json') { + return response.json(); + } else { + return response; + } + } else { + onFetchFail(request.url, response); + return Promise.reject(response); + } + + }, function (error) { + onFetchFail(request.url, {}); + throw error; + }); + } + + return self.fetchWithFailover(request, true); + }; + + self.getJSON = function (url, includeAuthorization) { + + return self.fetch({ + + url: url, + type: 'GET', + dataType: 'json', + headers: { + accept: 'application/json' + } + + }, includeAuthorization); + }; + + function switchConnectionMode(connectionMode) { + + var currentServerInfo = self.serverInfo(); + var newConnectionMode = connectionMode; + + newConnectionMode--; + if (newConnectionMode < 0) { + newConnectionMode = MediaBrowser.ConnectionMode.Manual; + } + + if (MediaBrowser.ServerInfo.getServerAddress(currentServerInfo, newConnectionMode)) { + return newConnectionMode; + } + + newConnectionMode--; + if (newConnectionMode < 0) { + newConnectionMode = MediaBrowser.ConnectionMode.Manual; + } + + if (MediaBrowser.ServerInfo.getServerAddress(currentServerInfo, newConnectionMode)) { + return newConnectionMode; + } + + return connectionMode; + } + + function tryReconnectInternal(resolve, reject, connectionMode, currentRetryCount) { + + connectionMode = switchConnectionMode(connectionMode); + var url = MediaBrowser.ServerInfo.getServerAddress(self.serverInfo(), connectionMode); + + logger.log("Attempting reconnection to " + url); + + var timeout = connectionMode == MediaBrowser.ConnectionMode.Local ? 7000 : 15000; + + fetchWithTimeout(url + "/system/info/public", { + + method: 'GET', + accept: 'application/json' + + // Commenting this out since the fetch api doesn't have a timeout option yet + //timeout: timeout + + }, timeout).then(function () { + + logger.log("Reconnect succeeded to " + url); + + self.serverInfo().LastConnectionMode = connectionMode; + self.serverAddress(url); + + resolve(); + + }, function () { + + logger.log("Reconnect attempt failed to " + url); + + if (currentRetryCount < 5) { + + var newConnectionMode = switchConnectionMode(connectionMode); + + setTimeout(function () { + tryReconnectInternal(resolve, reject, newConnectionMode, currentRetryCount + 1); + }, 300); + + } else { + reject(); + } + }); + } + + function tryReconnect() { + + return new Promise(function (resolve, reject) { + + setTimeout(function () { + tryReconnectInternal(resolve, reject, self.serverInfo().LastConnectionMode, 0); + }, 300); + }); + } + + self.fetchWithFailover = function (request, enableReconnection) { + + logger.log("Requesting " + request.url); + + request.timeout = 30000; + + return getFetchPromise(request).then(function (response) { + + if (response.status < 400) { + + if (request.dataType == 'json' || request.headers.accept == 'application/json') { + return response.json(); + } else { + return response; + } + } else { + onFetchFail(request.url, response); + return Promise.reject(response); + } + + }, function (error) { + + logger.log("Request failed to " + request.url); + + // http://api.jquery.com/jQuery.ajax/ + if (enableReconnection) { + + logger.log("Attempting reconnection"); + + var previousServerAddress = self.serverAddress(); + + return tryReconnect().then(function () { + + logger.log("Reconnect succeesed"); + request.url = request.url.replace(previousServerAddress, self.serverAddress()); + + return self.fetchWithFailover(request, false); + + }, function (innerError) { + + logger.log("Reconnect failed"); + onFetchFail(request.url, {}); + throw innerError; + }); + + } else { + + logger.log("Reporting request failure"); + + onFetchFail(request.url, {}); + throw error; + } + }); + }; + + self.get = function (url) { + + return self.ajax({ + type: "GET", + url: url + }); + }; + + /** + * Creates an api url based on a handler name and query string parameters + * @param {String} name + * @param {Object} params + */ + self.getUrl = function (name, params) { + + if (!name) { + throw new Error("Url name cannot be empty"); + } + + var url = serverAddress; + + if (!url) { + throw new Error("serverAddress is yet not set"); + } + var lowered = url.toLowerCase(); + if (lowered.indexOf('/emby') == -1 && lowered.indexOf('/mediabrowser') == -1) { + url += '/emby'; + } + + if (name.charAt(0) != '/') { + url += '/'; + } + + url += name; + + if (params) { + params = paramsToString(params); + if (params) { + url += "?" + params; + } + } + + return url; + }; + + self.updateServerInfo = function (server, connectionMode) { + + if (server == null) { + throw new Error('server cannot be null'); + } + + if (connectionMode == null) { + throw new Error('connectionMode cannot be null'); + } + + logger.log('Begin updateServerInfo. connectionMode: ' + connectionMode); + + self.serverInfo(server); + + var serverUrl = MediaBrowser.ServerInfo.getServerAddress(server, connectionMode); + + if (!serverUrl) { + throw new Error('serverUrl cannot be null. serverInfo: ' + JSON.stringify(server)); + } + logger.log('Setting server address to ' + serverUrl); + self.serverAddress(serverUrl); + }; + + self.isWebSocketSupported = function () { + return WebSocket != null; + }; + + self.openWebSocket = function () { + + var accessToken = self.accessToken(); + + if (!accessToken) { + throw new Error("Cannot open web socket without access token."); + } + + var url = self.getUrl("socket").replace("emby/socket", "embywebsocket").replace('http', 'ws'); + + url += "?api_key=" + accessToken; + url += "&deviceId=" + deviceId; + + webSocket = new WebSocket(url); + + webSocket.onmessage = function (msg) { + + msg = JSON.parse(msg.data); + onWebSocketMessage(msg); + }; + + webSocket.onopen = function () { + + logger.log('web socket connection opened'); + setTimeout(function () { + Events.trigger(self, 'websocketopen'); + }, 0); + }; + webSocket.onerror = function () { + setTimeout(function () { + Events.trigger(self, 'websocketerror'); + }, 0); + }; + webSocket.onclose = function () { + setTimeout(function () { + Events.trigger(self, 'websocketclose'); + }, 0); + }; + }; + + self.closeWebSocket = function () { + if (webSocket && webSocket.readyState === WebSocket.OPEN) { + webSocket.close(); + } + }; + + function onWebSocketMessage(msg) { + + if (msg.MessageType === "UserDeleted") { + currentUser = null; + } + else if (msg.MessageType === "UserUpdated" || msg.MessageType === "UserConfigurationUpdated") { + + var user = msg.Data; + if (user.Id == self.getCurrentUserId()) { + + currentUser = null; + } + } + + Events.trigger(self, 'websocketmessage', [msg]); + } + + self.sendWebSocketMessage = function (name, data) { + + logger.log('Sending web socket message: ' + name); + + var msg = { MessageType: name }; + + if (data) { + msg.Data = data; + } + + msg = JSON.stringify(msg); + + webSocket.send(msg); + }; + + self.isWebSocketOpen = function () { + return webSocket && webSocket.readyState === WebSocket.OPEN; + }; + + self.isWebSocketOpenOrConnecting = function () { + return webSocket && (webSocket.readyState === WebSocket.OPEN || webSocket.readyState === WebSocket.CONNECTING); + }; + + self.getProductNews = function (options) { + + options = options || {}; + + var url = self.getUrl("News/Product", options); + + return self.getJSON(url); + }; + + self.getDownloadSpeed = function (byteSize) { + + var url = self.getUrl('Playback/BitrateTest', { + + Size: byteSize + }); + + var now = new Date().getTime(); + + return self.ajax({ + + type: "GET", + url: url, + timeout: 5000 + + }).then(function () { + + var responseTimeSeconds = (new Date().getTime() - now) / 1000; + var bytesPerSecond = byteSize / responseTimeSeconds; + var bitrate = Math.round(bytesPerSecond * 8); + + return bitrate; + }); + }; + + self.detectBitrate = function () { + + // First try a small amount so that we don't hang up their mobile connection + return self.getDownloadSpeed(1000000).then(function (bitrate) { + + if (bitrate < 1000000) { + return Math.round(bitrate * .8); + } else { + + // If that produced a fairly high speed, try again with a larger size to get a more accurate result + return self.getDownloadSpeed(2400000).then(function (bitrate) { + + return Math.round(bitrate * .8); + }); + } + + }); + }; + + /** + * Gets an item from the server + * Omit itemId to get the root folder. + */ + self.getItem = function (userId, itemId) { + + if (!itemId) { + throw new Error("null itemId"); + } + + var url = userId ? + self.getUrl("Users/" + userId + "/Items/" + itemId) : + self.getUrl("Items/" + itemId); + + return self.getJSON(url); + }; + + /** + * Gets the root folder from the server + */ + self.getRootFolder = function (userId) { + + if (!userId) { + throw new Error("null userId"); + } + + var url = self.getUrl("Users/" + userId + "/Items/Root"); + + return self.getJSON(url); + }; + + self.getNotificationSummary = function (userId) { + + if (!userId) { + throw new Error("null userId"); + } + + var url = self.getUrl("Notifications/" + userId + "/Summary"); + + return self.getJSON(url); + }; + + self.getNotifications = function (userId, options) { + + if (!userId) { + throw new Error("null userId"); + } + + var url = self.getUrl("Notifications/" + userId, options || {}); + + return self.getJSON(url); + }; + + self.markNotificationsRead = function (userId, idList, isRead) { + + if (!userId) { + throw new Error("null userId"); + } + + if (!idList) { + throw new Error("null idList"); + } + + var suffix = isRead ? "Read" : "Unread"; + + var params = { + UserId: userId, + Ids: idList.join(',') + }; + + var url = self.getUrl("Notifications/" + userId + "/" + suffix, params); + + return self.ajax({ + type: "POST", + url: url + }); + }; + + self.logout = function () { + + self.closeWebSocket(); + + var done = function () { + self.setAuthenticationInfo(null, null); + }; + + if (self.accessToken()) { + var url = self.getUrl("Sessions/Logout"); + + return self.ajax({ + type: "POST", + url: url + + }).then(done, done); + } + + return new Promise(function (resolve, reject) { + + done(); + resolve(); + }); + }; + + function getRemoteImagePrefix(options) { + + var urlPrefix; + + if (options.artist) { + urlPrefix = "Artists/" + self.encodeName(options.artist); + delete options.artist; + } else if (options.person) { + urlPrefix = "Persons/" + self.encodeName(options.person); + delete options.person; + } else if (options.genre) { + urlPrefix = "Genres/" + self.encodeName(options.genre); + delete options.genre; + } else if (options.musicGenre) { + urlPrefix = "MusicGenres/" + self.encodeName(options.musicGenre); + delete options.musicGenre; + } else if (options.gameGenre) { + urlPrefix = "GameGenres/" + self.encodeName(options.gameGenre); + delete options.gameGenre; + } else if (options.studio) { + urlPrefix = "Studios/" + self.encodeName(options.studio); + delete options.studio; + } else { + urlPrefix = "Items/" + options.itemId; + delete options.itemId; + } + + return urlPrefix; + } + + self.getRemoteImageProviders = function (options) { + + if (!options) { + throw new Error("null options"); + } + + var urlPrefix = getRemoteImagePrefix(options); + + var url = self.getUrl(urlPrefix + "/RemoteImages/Providers", options); + + return self.getJSON(url); + }; + + self.getAvailableRemoteImages = function (options) { + + if (!options) { + throw new Error("null options"); + } + + var urlPrefix = getRemoteImagePrefix(options); + + var url = self.getUrl(urlPrefix + "/RemoteImages", options); + + return self.getJSON(url); + }; + + self.downloadRemoteImage = function (options) { + + if (!options) { + throw new Error("null options"); + } + + var urlPrefix = getRemoteImagePrefix(options); + + var url = self.getUrl(urlPrefix + "/RemoteImages/Download", options); + + return self.ajax({ + type: "POST", + url: url + }); + }; + + self.getLiveTvInfo = function (options) { + + var url = self.getUrl("LiveTv/Info", options || {}); + + return self.getJSON(url); + }; + + self.getLiveTvGuideInfo = function (options) { + + var url = self.getUrl("LiveTv/GuideInfo", options || {}); + + return self.getJSON(url); + }; + + self.getLiveTvChannel = function (id, userId) { + + if (!id) { + throw new Error("null id"); + } + + var options = { + + }; + + if (userId) { + options.userId = userId; + } + + var url = self.getUrl("LiveTv/Channels/" + id, options); + + return self.getJSON(url); + }; + + self.getLiveTvChannels = function (options) { + + var url = self.getUrl("LiveTv/Channels", options || {}); + + return self.getJSON(url); + }; + + self.getLiveTvPrograms = function (options) { + + options = options || {}; + + if (options.channelIds && options.channelIds.length > 1800) { + + return self.ajax({ + type: "POST", + url: self.getUrl("LiveTv/Programs"), + data: JSON.stringify(options), + contentType: "application/json", + dataType: "json" + }); + + } else { + + return self.ajax({ + type: "GET", + url: self.getUrl("LiveTv/Programs", options), + dataType: "json" + }); + } + }; + + self.getLiveTvRecommendedPrograms = function (options) { + + options = options || {}; + + return self.ajax({ + type: "GET", + url: self.getUrl("LiveTv/Programs/Recommended", options), + dataType: "json" + }); + }; + + self.getLiveTvRecordings = function (options) { + + var url = self.getUrl("LiveTv/Recordings", options || {}); + + return self.getJSON(url); + }; + + self.getLiveTvRecordingGroups = function (options) { + + var url = self.getUrl("LiveTv/Recordings/Groups", options || {}); + + return self.getJSON(url); + }; + + self.getLiveTvRecordingGroup = function (id) { + + if (!id) { + throw new Error("null id"); + } + + var url = self.getUrl("LiveTv/Recordings/Groups/" + id); + + return self.getJSON(url); + }; + + self.getLiveTvRecording = function (id, userId) { + + if (!id) { + throw new Error("null id"); + } + + var options = { + + }; + + if (userId) { + options.userId = userId; + } + + var url = self.getUrl("LiveTv/Recordings/" + id, options); + + return self.getJSON(url); + }; + + self.getLiveTvProgram = function (id, userId) { + + if (!id) { + throw new Error("null id"); + } + + var options = { + + }; + + if (userId) { + options.userId = userId; + } + + var url = self.getUrl("LiveTv/Programs/" + id, options); + + return self.getJSON(url); + }; + + self.deleteLiveTvRecording = function (id) { + + if (!id) { + throw new Error("null id"); + } + + var url = self.getUrl("LiveTv/Recordings/" + id); + + return self.ajax({ + type: "DELETE", + url: url + }); + }; + + self.cancelLiveTvTimer = function (id) { + + if (!id) { + throw new Error("null id"); + } + + var url = self.getUrl("LiveTv/Timers/" + id); + + return self.ajax({ + type: "DELETE", + url: url + }); + }; + + self.getLiveTvTimers = function (options) { + + var url = self.getUrl("LiveTv/Timers", options || {}); + + return self.getJSON(url); + }; + + self.getLiveTvTimer = function (id) { + + if (!id) { + throw new Error("null id"); + } + + var url = self.getUrl("LiveTv/Timers/" + id); + + return self.getJSON(url); + }; + + self.getNewLiveTvTimerDefaults = function (options) { + + options = options || {}; + + var url = self.getUrl("LiveTv/Timers/Defaults", options); + + return self.getJSON(url); + }; + + self.createLiveTvTimer = function (item) { + + if (!item) { + throw new Error("null item"); + } + + var url = self.getUrl("LiveTv/Timers"); + + return self.ajax({ + type: "POST", + url: url, + data: JSON.stringify(item), + contentType: "application/json" + }); + }; + + self.updateLiveTvTimer = function (item) { + + if (!item) { + throw new Error("null item"); + } + + var url = self.getUrl("LiveTv/Timers/" + item.Id); + + return self.ajax({ + type: "POST", + url: url, + data: JSON.stringify(item), + contentType: "application/json" + }); + }; + + self.resetLiveTvTuner = function (id) { + + if (!id) { + throw new Error("null id"); + } + + var url = self.getUrl("LiveTv/Tuners/" + id + "/Reset"); + + return self.ajax({ + type: "POST", + url: url + }); + }; + + self.getLiveTvSeriesTimers = function (options) { + + var url = self.getUrl("LiveTv/SeriesTimers", options || {}); + + return self.getJSON(url); + }; + + self.getFileOrganizationResults = function (options) { + + var url = self.getUrl("Library/FileOrganization", options || {}); + + return self.getJSON(url); + }; + + self.deleteOriginalFileFromOrganizationResult = function (id) { + + var url = self.getUrl("Library/FileOrganizations/" + id + "/File"); + + return self.ajax({ + type: "DELETE", + url: url + }); + }; + + self.clearOrganizationLog = function () { + + var url = self.getUrl("Library/FileOrganizations"); + + return self.ajax({ + type: "DELETE", + url: url + }); + }; + + self.performOrganization = function (id) { + + var url = self.getUrl("Library/FileOrganizations/" + id + "/Organize"); + + return self.ajax({ + type: "POST", + url: url + }); + }; + + self.performEpisodeOrganization = function (id, options) { + + var url = self.getUrl("Library/FileOrganizations/" + id + "/Episode/Organize", options || {}); + + return self.ajax({ + type: "POST", + url: url + }); + }; + + self.getLiveTvSeriesTimer = function (id) { + + if (!id) { + throw new Error("null id"); + } + + var url = self.getUrl("LiveTv/SeriesTimers/" + id); + + return self.getJSON(url); + }; + + self.cancelLiveTvSeriesTimer = function (id) { + + if (!id) { + throw new Error("null id"); + } + + var url = self.getUrl("LiveTv/SeriesTimers/" + id); + + return self.ajax({ + type: "DELETE", + url: url + }); + }; + + self.createLiveTvSeriesTimer = function (item) { + + if (!item) { + throw new Error("null item"); + } + + var url = self.getUrl("LiveTv/SeriesTimers"); + + return self.ajax({ + type: "POST", + url: url, + data: JSON.stringify(item), + contentType: "application/json" + }); + }; + + self.updateLiveTvSeriesTimer = function (item) { + + if (!item) { + throw new Error("null item"); + } + + var url = self.getUrl("LiveTv/SeriesTimers/" + item.Id); + + return self.ajax({ + type: "POST", + url: url, + data: JSON.stringify(item), + contentType: "application/json" + }); + }; + + self.getRegistrationInfo = function (feature) { + + var url = self.getUrl("Registrations/" + feature); + + return self.getJSON(url); + }; + + /** + * Gets the current server status + */ + self.getSystemInfo = function () { + + var url = self.getUrl("System/Info"); + + return self.getJSON(url); + }; + + /** + * Gets the current server status + */ + self.getPublicSystemInfo = function () { + + var url = self.getUrl("System/Info/Public"); + + return self.getJSON(url, false); + }; + + self.getInstantMixFromItem = function (itemId, options) { + + var url = self.getUrl("Items/" + itemId + "/InstantMix", options); + + return self.getJSON(url); + }; + + self.getEpisodes = function (itemId, options) { + + var url = self.getUrl("Shows/" + itemId + "/Episodes", options); + + return self.getJSON(url); + }; + + self.getDisplayPreferences = function (id, userId, app) { + + var url = self.getUrl("DisplayPreferences/" + id, { + userId: userId, + client: app + }); + + return self.getJSON(url); + }; + + self.updateDisplayPreferences = function (id, obj, userId, app) { + + var url = self.getUrl("DisplayPreferences/" + id, { + userId: userId, + client: app + }); + + return self.ajax({ + type: "POST", + url: url, + data: JSON.stringify(obj), + contentType: "application/json" + }); + }; + + self.getSeasons = function (itemId, options) { + + var url = self.getUrl("Shows/" + itemId + "/Seasons", options); + + return self.getJSON(url); + }; + + self.getSimilarItems = function (itemId, options) { + + var url = self.getUrl("Items/" + itemId + "/Similar", options); + + return self.getJSON(url); + }; + + /** + * Gets all cultures known to the server + */ + self.getCultures = function () { + + var url = self.getUrl("Localization/cultures"); + + return self.getJSON(url); + }; + + /** + * Gets all countries known to the server + */ + self.getCountries = function () { + + var url = self.getUrl("Localization/countries"); + + return self.getJSON(url); + }; + + /** + * Gets plugin security info + */ + self.getPluginSecurityInfo = function () { + + var url = self.getUrl("Plugins/SecurityInfo"); + + return self.getJSON(url); + }; + + /** + * Gets the directory contents of a path on the server + */ + self.getDirectoryContents = function (path, options) { + + if (!path) { + throw new Error("null path"); + } + + options = options || {}; + + options.path = path; + + var url = self.getUrl("Environment/DirectoryContents", options); + + return self.getJSON(url); + }; + + /** + * Gets shares from a network device + */ + self.getNetworkShares = function (path) { + + if (!path) { + throw new Error("null path"); + } + + var options = {}; + options.path = path; + + var url = self.getUrl("Environment/NetworkShares", options); + + return self.getJSON(url); + }; + + /** + * Gets the parent of a given path + */ + self.getParentPath = function (path) { + + if (!path) { + throw new Error("null path"); + } + + var options = {}; + options.path = path; + + var url = self.getUrl("Environment/ParentPath", options); + + return self.ajax({ + type: "GET", + url: url + }); + }; + + /** + * Gets a list of physical drives from the server + */ + self.getDrives = function () { + + var url = self.getUrl("Environment/Drives"); + + return self.getJSON(url); + }; + + /** + * Gets a list of network devices from the server + */ + self.getNetworkDevices = function () { + + var url = self.getUrl("Environment/NetworkDevices"); + + return self.getJSON(url); + }; + + /** + * Cancels a package installation + */ + self.cancelPackageInstallation = function (installationId) { + + if (!installationId) { + throw new Error("null installationId"); + } + + var url = self.getUrl("Packages/Installing/" + installationId); + + return self.ajax({ + type: "DELETE", + url: url + }); + }; + + /** + * Refreshes metadata for an item + */ + self.refreshItem = function (itemId, options) { + + if (!itemId) { + throw new Error("null itemId"); + } + + var url = self.getUrl("Items/" + itemId + "/Refresh", options || {}); + + return self.ajax({ + type: "POST", + url: url + }); + }; + + /** + * Installs or updates a new plugin + */ + self.installPlugin = function (name, guid, updateClass, version) { + + if (!name) { + throw new Error("null name"); + } + + if (!updateClass) { + throw new Error("null updateClass"); + } + + var options = { + updateClass: updateClass, + AssemblyGuid: guid + }; + + if (version) { + options.version = version; + } + + var url = self.getUrl("Packages/Installed/" + name, options); + + return self.ajax({ + type: "POST", + url: url + }); + }; + + /** + * Instructs the server to perform a restart. + */ + self.restartServer = function () { + + var url = self.getUrl("System/Restart"); + + return self.ajax({ + type: "POST", + url: url + }); + }; + + /** + * Instructs the server to perform a shutdown. + */ + self.shutdownServer = function () { + + var url = self.getUrl("System/Shutdown"); + + return self.ajax({ + type: "POST", + url: url + }); + }; + + /** + * Gets information about an installable package + */ + self.getPackageInfo = function (name, guid) { + + if (!name) { + throw new Error("null name"); + } + + var options = { + AssemblyGuid: guid + }; + + var url = self.getUrl("Packages/" + name, options); + + return self.getJSON(url); + }; + + /** + * Gets the latest available application update (if any) + */ + self.getAvailableApplicationUpdate = function () { + + var url = self.getUrl("Packages/Updates", { PackageType: "System" }); + + return self.getJSON(url); + }; + + /** + * Gets the latest available plugin updates (if any) + */ + self.getAvailablePluginUpdates = function () { + + var url = self.getUrl("Packages/Updates", { PackageType: "UserInstalled" }); + + return self.getJSON(url); + }; + + /** + * Gets the virtual folder list + */ + self.getVirtualFolders = function () { + + var url = "Library/VirtualFolders"; + + url = self.getUrl(url); + + return self.getJSON(url); + }; + + /** + * Gets all the paths of the locations in the physical root. + */ + self.getPhysicalPaths = function () { + + var url = self.getUrl("Library/PhysicalPaths"); + + return self.getJSON(url); + }; + + /** + * Gets the current server configuration + */ + self.getServerConfiguration = function () { + + var url = self.getUrl("System/Configuration"); + + return self.getJSON(url); + }; + + /** + * Gets the current server configuration + */ + self.getDevicesOptions = function () { + + var url = self.getUrl("System/Configuration/devices"); + + return self.getJSON(url); + }; + + /** + * Gets the current server configuration + */ + self.getContentUploadHistory = function () { + + var url = self.getUrl("Devices/CameraUploads", { + DeviceId: self.deviceId() + }); + + return self.getJSON(url); + }; + + self.getNamedConfiguration = function (name) { + + var url = self.getUrl("System/Configuration/" + name); + + return self.getJSON(url); + }; + + /** + * Gets the server's scheduled tasks + */ + self.getScheduledTasks = function (options) { + + options = options || {}; + + var url = self.getUrl("ScheduledTasks", options); + + return self.getJSON(url); + }; + + /** + * Starts a scheduled task + */ + self.startScheduledTask = function (id) { + + if (!id) { + throw new Error("null id"); + } + + var url = self.getUrl("ScheduledTasks/Running/" + id); + + return self.ajax({ + type: "POST", + url: url + }); + }; + + /** + * Gets a scheduled task + */ + self.getScheduledTask = function (id) { + + if (!id) { + throw new Error("null id"); + } + + var url = self.getUrl("ScheduledTasks/" + id); + + return self.getJSON(url); + }; + + self.getNextUpEpisodes = function (options) { + + var url = self.getUrl("Shows/NextUp", options); + + return self.getJSON(url); + }; + + /** + * Stops a scheduled task + */ + self.stopScheduledTask = function (id) { + + if (!id) { + throw new Error("null id"); + } + + var url = self.getUrl("ScheduledTasks/Running/" + id); + + return self.ajax({ + type: "DELETE", + url: url + }); + }; + + /** + * Gets the configuration of a plugin + * @param {String} Id + */ + self.getPluginConfiguration = function (id) { + + if (!id) { + throw new Error("null Id"); + } + + var url = self.getUrl("Plugins/" + id + "/Configuration"); + + return self.getJSON(url); + }; + + /** + * Gets a list of plugins that are available to be installed + */ + self.getAvailablePlugins = function (options) { + + options = options || {}; + options.PackageType = "UserInstalled"; + + if (self.enableAppStorePolicy) { + options.IsAppStoreEnabled = true; + } + + var url = self.getUrl("Packages", options); + + return self.getJSON(url); + }; + + /** + * Uninstalls a plugin + * @param {String} Id + */ + self.uninstallPlugin = function (id) { + + if (!id) { + throw new Error("null Id"); + } + + var url = self.getUrl("Plugins/" + id); + + return self.ajax({ + type: "DELETE", + url: url + }); + }; + + /** + * Removes a virtual folder + * @param {String} name + */ + self.removeVirtualFolder = function (name, refreshLibrary) { + + if (!name) { + throw new Error("null name"); + } + + var url = "Library/VirtualFolders"; + + url = self.getUrl(url, { + refreshLibrary: refreshLibrary ? true : false, + name: name + }); + + return self.ajax({ + type: "DELETE", + url: url + }); + }; + + /** + * Adds a virtual folder + * @param {String} name + */ + self.addVirtualFolder = function (name, type, refreshLibrary, initialPaths) { + + if (!name) { + throw new Error("null name"); + } + + var options = {}; + + if (type) { + options.collectionType = type; + } + + options.refreshLibrary = refreshLibrary ? true : false; + options.name = name; + + var url = "Library/VirtualFolders"; + + url = self.getUrl(url, options); + + return self.ajax({ + type: "POST", + url: url, + data: JSON.stringify({ + Paths: initialPaths + }), + contentType: 'application/json' + }); + }; + + /** + * Renames a virtual folder + * @param {String} name + */ + self.renameVirtualFolder = function (name, newName, refreshLibrary) { + + if (!name) { + throw new Error("null name"); + } + + var url = "Library/VirtualFolders/Name"; + + url = self.getUrl(url, { + refreshLibrary: refreshLibrary ? true : false, + newName: newName, + name: name + }); + + return self.ajax({ + type: "POST", + url: url + }); + }; + + /** + * Adds an additional mediaPath to an existing virtual folder + * @param {String} name + */ + self.addMediaPath = function (virtualFolderName, mediaPath, refreshLibrary) { + + if (!virtualFolderName) { + throw new Error("null virtualFolderName"); + } + + if (!mediaPath) { + throw new Error("null mediaPath"); + } + + var url = "Library/VirtualFolders/Paths"; + + url = self.getUrl(url, { + refreshLibrary: refreshLibrary ? true : false, + path: mediaPath, + name: virtualFolderName + }); + + return self.ajax({ + type: "POST", + url: url + }); + }; + + /** + * Removes a media path from a virtual folder + * @param {String} name + */ + self.removeMediaPath = function (virtualFolderName, mediaPath, refreshLibrary) { + + if (!virtualFolderName) { + throw new Error("null virtualFolderName"); + } + + if (!mediaPath) { + throw new Error("null mediaPath"); + } + + var url = "Library/VirtualFolders/Paths"; + + url = self.getUrl(url, { + refreshLibrary: refreshLibrary ? true : false, + path: mediaPath, + name: virtualFolderName + }); + + return self.ajax({ + type: "DELETE", + url: url + }); + }; + + /** + * Deletes a user + * @param {String} id + */ + self.deleteUser = function (id) { + + if (!id) { + throw new Error("null id"); + } + + var url = self.getUrl("Users/" + id); + + return self.ajax({ + type: "DELETE", + url: url + }); + }; + + /** + * Deletes a user image + * @param {String} userId + * @param {String} imageType The type of image to delete, based on the server-side ImageType enum. + */ + self.deleteUserImage = function (userId, imageType, imageIndex) { + + if (!userId) { + throw new Error("null userId"); + } + + if (!imageType) { + throw new Error("null imageType"); + } + + var url = self.getUrl("Users/" + userId + "/Images/" + imageType); + + if (imageIndex != null) { + url += "/" + imageIndex; + } + + return self.ajax({ + type: "DELETE", + url: url + }); + }; + + self.deleteItemImage = function (itemId, imageType, imageIndex) { + + if (!imageType) { + throw new Error("null imageType"); + } + + var url = self.getUrl("Items/" + itemId + "/Images"); + + url += "/" + imageType; + + if (imageIndex != null) { + url += "/" + imageIndex; + } + + return self.ajax({ + type: "DELETE", + url: url + }); + }; + + self.deleteItem = function (itemId) { + + if (!itemId) { + throw new Error("null itemId"); + } + + var url = self.getUrl("Items/" + itemId); + + return self.ajax({ + type: "DELETE", + url: url + }); + }; + + self.stopActiveEncodings = function (playSessionId) { + + var options = { + deviceId: deviceId + }; + + if (playSessionId) { + options.PlaySessionId = playSessionId; + } + + var url = self.getUrl("Videos/ActiveEncodings", options); + + return self.ajax({ + type: "DELETE", + url: url + }); + }; + + self.reportCapabilities = function (options) { + + var url = self.getUrl("Sessions/Capabilities/Full"); + + return self.ajax({ + type: "POST", + url: url, + data: JSON.stringify(options), + contentType: "application/json" + }); + }; + + self.updateItemImageIndex = function (itemId, imageType, imageIndex, newIndex) { + + if (!imageType) { + throw new Error("null imageType"); + } + + var options = { newIndex: newIndex }; + + var url = self.getUrl("Items/" + itemId + "/Images/" + imageType + "/" + imageIndex + "/Index", options); + + return self.ajax({ + type: "POST", + url: url + }); + }; + + self.getItemImageInfos = function (itemId) { + + var url = self.getUrl("Items/" + itemId + "/Images"); + + return self.getJSON(url); + }; + + self.getCriticReviews = function (itemId, options) { + + if (!itemId) { + throw new Error("null itemId"); + } + + var url = self.getUrl("Items/" + itemId + "/CriticReviews", options); + + return self.getJSON(url); + }; + + self.getSessions = function (options) { + + var url = self.getUrl("Sessions", options); + + return self.getJSON(url); + }; + + /** + * Uploads a user image + * @param {String} userId + * @param {String} imageType The type of image to delete, based on the server-side ImageType enum. + * @param {Object} file The file from the input element + */ + self.uploadUserImage = function (userId, imageType, file) { + + if (!userId) { + throw new Error("null userId"); + } + + if (!imageType) { + throw new Error("null imageType"); + } + + if (!file) { + throw new Error("File must be an image."); + } + + if (file.type != "image/png" && file.type != "image/jpeg" && file.type != "image/jpeg") { + throw new Error("File must be an image."); + } + + return new Promise(function (resolve, reject) { + + var reader = new FileReader(); + + reader.onerror = function () { + reject(); + }; + + reader.onabort = function () { + reject(); + }; + + // Closure to capture the file information. + reader.onload = function (e) { + + // Split by a comma to remove the url: prefix + var data = e.target.result.split(',')[1]; + + var url = self.getUrl("Users/" + userId + "/Images/" + imageType); + + self.ajax({ + type: "POST", + url: url, + data: data, + contentType: "image/" + file.name.substring(file.name.lastIndexOf('.') + 1) + }).then(function (result) { + + resolve(result); + + }, function () { + reject(); + }); + }; + + // Read in the image file as a data URL. + reader.readAsDataURL(file); + }); + }; + + self.uploadItemImage = function (itemId, imageType, file) { + + if (!itemId) { + throw new Error("null itemId"); + } + + if (!imageType) { + throw new Error("null imageType"); + } + + if (!file) { + throw new Error("File must be an image."); + } + + if (file.type != "image/png" && file.type != "image/jpeg" && file.type != "image/jpeg") { + throw new Error("File must be an image."); + } + + var url = self.getUrl("Items/" + itemId + "/Images"); + + url += "/" + imageType; + + return new Promise(function (resolve, reject) { + + var reader = new FileReader(); + + reader.onerror = function () { + reject(); + }; + + reader.onabort = function () { + reject(); + }; + + // Closure to capture the file information. + reader.onload = function (e) { + + // Split by a comma to remove the url: prefix + var data = e.target.result.split(',')[1]; + + self.ajax({ + type: "POST", + url: url, + data: data, + contentType: "image/" + file.name.substring(file.name.lastIndexOf('.') + 1) + }).then(function (result) { + + resolve(result); + + }, function () { + reject(); + }); + }; + + // Read in the image file as a data URL. + reader.readAsDataURL(file); + }); + }; + + /** + * Gets the list of installed plugins on the server + */ + self.getInstalledPlugins = function () { + + var options = {}; + + if (self.enableAppStorePolicy) { + options.IsAppStoreEnabled = true; + } + + var url = self.getUrl("Plugins", options); + + return self.getJSON(url); + }; + + /** + * Gets a user by id + * @param {String} id + */ + self.getUser = function (id) { + + if (!id) { + throw new Error("Must supply a userId"); + } + + var url = self.getUrl("Users/" + id); + + return self.getJSON(url); + }; + + /** + * Gets a user by id + * @param {String} id + */ + self.getOfflineUser = function (id) { + + if (!id) { + throw new Error("Must supply a userId"); + } + + var url = self.getUrl("Users/" + id + "/Offline"); + + return self.getJSON(url); + }; + + /** + * Gets a studio + */ + self.getStudio = function (name, userId) { + + if (!name) { + throw new Error("null name"); + } + + var options = {}; + + if (userId) { + options.userId = userId; + } + + var url = self.getUrl("Studios/" + self.encodeName(name), options); + + return self.getJSON(url); + }; + + /** + * Gets a genre + */ + self.getGenre = function (name, userId) { + + if (!name) { + throw new Error("null name"); + } + + var options = {}; + + if (userId) { + options.userId = userId; + } + + var url = self.getUrl("Genres/" + self.encodeName(name), options); + + return self.getJSON(url); + }; + + self.getMusicGenre = function (name, userId) { + + if (!name) { + throw new Error("null name"); + } + + var options = {}; + + if (userId) { + options.userId = userId; + } + + var url = self.getUrl("MusicGenres/" + self.encodeName(name), options); + + return self.getJSON(url); + }; + + self.getGameGenre = function (name, userId) { + + if (!name) { + throw new Error("null name"); + } + + var options = {}; + + if (userId) { + options.userId = userId; + } + + var url = self.getUrl("GameGenres/" + self.encodeName(name), options); + + return self.getJSON(url); + }; + + /** + * Gets an artist + */ + self.getArtist = function (name, userId) { + + if (!name) { + throw new Error("null name"); + } + + var options = {}; + + if (userId) { + options.userId = userId; + } + + var url = self.getUrl("Artists/" + self.encodeName(name), options); + + return self.getJSON(url); + }; + + /** + * Gets a Person + */ + self.getPerson = function (name, userId) { + + if (!name) { + throw new Error("null name"); + } + + var options = {}; + + if (userId) { + options.userId = userId; + } + + var url = self.getUrl("Persons/" + self.encodeName(name), options); + + return self.getJSON(url); + }; + + self.getPublicUsers = function () { + + var url = self.getUrl("users/public"); + + return self.ajax({ + type: "GET", + url: url, + dataType: "json" + + }, false); + }; + + /** + * Gets all users from the server + */ + self.getUsers = function (options) { + + var url = self.getUrl("users", options || {}); + + return self.getJSON(url); + }; + + /** + * Gets all available parental ratings from the server + */ + self.getParentalRatings = function () { + + var url = self.getUrl("Localization/ParentalRatings"); + + return self.getJSON(url); + }; + + self.getDefaultImageQuality = function (imageType) { + return imageType.toLowerCase() == 'backdrop' ? 80 : 90; + }; + + function normalizeImageOptions(options) { + + var ratio = devicePixelRatio || 1; + + if (ratio) { + + if (options.minScale) { + ratio = Math.max(options.minScale, ratio); + } + + if (options.width) { + options.width = Math.round(options.width * ratio); + } + if (options.height) { + options.height = Math.round(options.height * ratio); + } + if (options.maxWidth) { + options.maxWidth = Math.round(options.maxWidth * ratio); + } + if (options.maxHeight) { + options.maxHeight = Math.round(options.maxHeight * ratio); + } + } + + options.quality = options.quality || self.getDefaultImageQuality(options.type); + + if (self.normalizeImageOptions) { + self.normalizeImageOptions(options); + } + } + + /** + * Constructs a url for a user image + * @param {String} userId + * @param {Object} options + * Options supports the following properties: + * width - download the image at a fixed width + * height - download the image at a fixed height + * maxWidth - download the image at a maxWidth + * maxHeight - download the image at a maxHeight + * quality - A scale of 0-100. This should almost always be omitted as the default will suffice. + * For best results do not specify both width and height together, as aspect ratio might be altered. + */ + self.getUserImageUrl = function (userId, options) { + + if (!userId) { + throw new Error("null userId"); + } + + options = options || {}; + + var url = "Users/" + userId + "/Images/" + options.type; + + if (options.index != null) { + url += "/" + options.index; + } + + normalizeImageOptions(options); + + // Don't put these on the query string + delete options.type; + delete options.index; + + return self.getUrl(url, options); + }; + + /** + * Constructs a url for an item image + * @param {String} itemId + * @param {Object} options + * Options supports the following properties: + * type - Primary, logo, backdrop, etc. See the server-side enum ImageType + * index - When downloading a backdrop, use this to specify which one (omitting is equivalent to zero) + * width - download the image at a fixed width + * height - download the image at a fixed height + * maxWidth - download the image at a maxWidth + * maxHeight - download the image at a maxHeight + * quality - A scale of 0-100. This should almost always be omitted as the default will suffice. + * For best results do not specify both width and height together, as aspect ratio might be altered. + */ + self.getImageUrl = function (itemId, options) { + + if (!itemId) { + throw new Error("itemId cannot be empty"); + } + + options = options || {}; + + var url = "Items/" + itemId + "/Images/" + options.type; + + if (options.index != null) { + url += "/" + options.index; + } + + options.quality = options.quality || self.getDefaultImageQuality(options.type); + + if (self.normalizeImageOptions) { + self.normalizeImageOptions(options); + } + + // Don't put these on the query string + delete options.type; + delete options.index; + + return self.getUrl(url, options); + }; + + self.getScaledImageUrl = function (itemId, options) { + + if (!itemId) { + throw new Error("itemId cannot be empty"); + } + + options = options || {}; + + var url = "Items/" + itemId + "/Images/" + options.type; + + if (options.index != null) { + url += "/" + options.index; + } + + normalizeImageOptions(options); + + // Don't put these on the query string + delete options.type; + delete options.index; + delete options.minScale; + + return self.getUrl(url, options); + }; + + self.getThumbImageUrl = function (item, options) { + + if (!item) { + throw new Error("null item"); + } + + options = options || { + + }; + + options.imageType = "thumb"; + + if (item.ImageTags && item.ImageTags.Thumb) { + + options.tag = item.ImageTags.Thumb; + return self.getImageUrl(item.Id, options); + } + else if (item.ParentThumbItemId) { + + options.tag = item.ImageTags.ParentThumbImageTag; + return self.getImageUrl(item.ParentThumbItemId, options); + + } else { + return null; + } + }; + + /** + * Authenticates a user + * @param {String} name + * @param {String} password + */ + self.authenticateUserByName = function (name, password) { + + return new Promise(function (resolve, reject) { + + if (!name) { + reject(); + return; + } + + var url = self.getUrl("Users/authenticatebyname"); + + require(["cryptojs-sha1"], function () { + var postData = { + password: CryptoJS.SHA1(password || "").toString(), + Username: name + }; + + self.ajax({ + type: "POST", + url: url, + data: JSON.stringify(postData), + dataType: "json", + contentType: "application/json" + + }).then(function (result) { + + if (self.onAuthenticated) { + self.onAuthenticated(self, result); + } + + resolve(result); + + }, reject); + }); + }); + }; + + /** + * Updates a user's password + * @param {String} userId + * @param {String} currentPassword + * @param {String} newPassword + */ + self.updateUserPassword = function (userId, currentPassword, newPassword) { + + return new Promise(function (resolve, reject) { + + if (!userId) { + reject(); + return; + } + + var url = self.getUrl("Users/" + userId + "/Password"); + + require(["cryptojs-sha1"], function () { + + self.ajax({ + type: "POST", + url: url, + data: { + currentPassword: CryptoJS.SHA1(currentPassword).toString(), + newPassword: CryptoJS.SHA1(newPassword).toString() + } + }).then(resolve, reject); + }); + }); + }; + + /** + * Updates a user's easy password + * @param {String} userId + * @param {String} newPassword + */ + self.updateEasyPassword = function (userId, newPassword) { + + return new Promise(function (resolve, reject) { + + if (!userId) { + reject(); + return; + } + + var url = self.getUrl("Users/" + userId + "/EasyPassword"); + + require(["cryptojs-sha1"], function () { + + self.ajax({ + type: "POST", + url: url, + data: { + newPassword: CryptoJS.SHA1(newPassword).toString() + } + }).then(resolve, reject); + }); + }); + }; + + /** + * Resets a user's password + * @param {String} userId + */ + self.resetUserPassword = function (userId) { + + if (!userId) { + throw new Error("null userId"); + } + + var url = self.getUrl("Users/" + userId + "/Password"); + + var postData = { + + }; + + postData.resetPassword = true; + + return self.ajax({ + type: "POST", + url: url, + data: postData + }); + }; + + self.resetEasyPassword = function (userId) { + + if (!userId) { + throw new Error("null userId"); + } + + var url = self.getUrl("Users/" + userId + "/EasyPassword"); + + var postData = { + + }; + + postData.resetPassword = true; + + return self.ajax({ + type: "POST", + url: url, + data: postData + }); + }; + + /** + * Updates the server's configuration + * @param {Object} configuration + */ + self.updateServerConfiguration = function (configuration) { + + if (!configuration) { + throw new Error("null configuration"); + } + + var url = self.getUrl("System/Configuration"); + + return self.ajax({ + type: "POST", + url: url, + data: JSON.stringify(configuration), + contentType: "application/json" + }); + }; + + self.updateNamedConfiguration = function (name, configuration) { + + if (!configuration) { + throw new Error("null configuration"); + } + + var url = self.getUrl("System/Configuration/" + name); + + return self.ajax({ + type: "POST", + url: url, + data: JSON.stringify(configuration), + contentType: "application/json" + }); + }; + + self.updateItem = function (item) { + + if (!item) { + throw new Error("null item"); + } + + var url = self.getUrl("Items/" + item.Id); + + return self.ajax({ + type: "POST", + url: url, + data: JSON.stringify(item), + contentType: "application/json" + }); + }; + + /** + * Updates plugin security info + */ + self.updatePluginSecurityInfo = function (info) { + + var url = self.getUrl("Plugins/SecurityInfo"); + + return self.ajax({ + type: "POST", + url: url, + data: JSON.stringify(info), + contentType: "application/json" + }); + }; + + /** + * Creates a user + * @param {Object} user + */ + self.createUser = function (name) { + + var url = self.getUrl("Users/New"); + + return self.ajax({ + type: "POST", + url: url, + data: { + Name: name + }, + dataType: "json" + }); + }; + + /** + * Updates a user + * @param {Object} user + */ + self.updateUser = function (user) { + + if (!user) { + throw new Error("null user"); + } + + var url = self.getUrl("Users/" + user.Id); + + return self.ajax({ + type: "POST", + url: url, + data: JSON.stringify(user), + contentType: "application/json" + }); + }; + + self.updateUserPolicy = function (userId, policy) { + + if (!userId) { + throw new Error("null userId"); + } + if (!policy) { + throw new Error("null policy"); + } + + var url = self.getUrl("Users/" + userId + "/Policy"); + + return self.ajax({ + type: "POST", + url: url, + data: JSON.stringify(policy), + contentType: "application/json" + }); + }; + + self.updateUserConfiguration = function (userId, configuration) { + + if (!userId) { + throw new Error("null userId"); + } + if (!configuration) { + throw new Error("null configuration"); + } + + var url = self.getUrl("Users/" + userId + "/Configuration"); + + return self.ajax({ + type: "POST", + url: url, + data: JSON.stringify(configuration), + contentType: "application/json" + }); + }; + + /** + * Updates the Triggers for a ScheduledTask + * @param {String} id + * @param {Object} triggers + */ + self.updateScheduledTaskTriggers = function (id, triggers) { + + if (!id) { + throw new Error("null id"); + } + + if (!triggers) { + throw new Error("null triggers"); + } + + var url = self.getUrl("ScheduledTasks/" + id + "/Triggers"); + + return self.ajax({ + type: "POST", + url: url, + data: JSON.stringify(triggers), + contentType: "application/json" + }); + }; + + /** + * Updates a plugin's configuration + * @param {String} Id + * @param {Object} configuration + */ + self.updatePluginConfiguration = function (id, configuration) { + + if (!id) { + throw new Error("null Id"); + } + + if (!configuration) { + throw new Error("null configuration"); + } + + var url = self.getUrl("Plugins/" + id + "/Configuration"); + + return self.ajax({ + type: "POST", + url: url, + data: JSON.stringify(configuration), + contentType: "application/json" + }); + }; + + self.getAncestorItems = function (itemId, userId) { + + if (!itemId) { + throw new Error("null itemId"); + } + + var options = {}; + + if (userId) { + options.userId = userId; + } + + var url = self.getUrl("Items/" + itemId + "/Ancestors", options); + + return self.getJSON(url); + }; + + /** + * Gets items based on a query, typically for children of a folder + * @param {String} userId + * @param {Object} options + * Options accepts the following properties: + * itemId - Localize the search to a specific folder (root if omitted) + * startIndex - Use for paging + * limit - Use to limit results to a certain number of items + * filter - Specify one or more ItemFilters, comma delimeted (see server-side enum) + * sortBy - Specify an ItemSortBy (comma-delimeted list see server-side enum) + * sortOrder - ascending/descending + * fields - additional fields to include aside from basic info. This is a comma delimited list. See server-side enum ItemFields. + * index - the name of the dynamic, localized index function + * dynamicSortBy - the name of the dynamic localized sort function + * recursive - Whether or not the query should be recursive + * searchTerm - search term to use as a filter + */ + self.getItems = function (userId, options) { + + var url; + + if ((typeof userId).toString().toLowerCase() == 'string') { + url = self.getUrl("Users/" + userId + "/Items", options); + } else { + + url = self.getUrl("Items", options); + } + + return self.getJSON(url); + }; + + self.getChannels = function (query) { + + return self.getJSON(self.getUrl("Channels", query || {})); + }; + + self.getUserViews = function (options, userId) { + + options = options || {}; + + var url = self.getUrl("Users/" + (userId || self.getCurrentUserId()) + "/Views", options); + + return self.getJSON(url); + }; + + /** + Gets artists from an item + */ + self.getArtists = function (userId, options) { + + if (!userId) { + throw new Error("null userId"); + } + + options = options || {}; + options.userId = userId; + + var url = self.getUrl("Artists", options); + + return self.getJSON(url); + }; + + /** + Gets artists from an item + */ + self.getAlbumArtists = function (userId, options) { + + if (!userId) { + throw new Error("null userId"); + } + + options = options || {}; + options.userId = userId; + + var url = self.getUrl("Artists/AlbumArtists", options); + + return self.getJSON(url); + }; + + /** + Gets genres from an item + */ + self.getGenres = function (userId, options) { + + if (!userId) { + throw new Error("null userId"); + } + + options = options || {}; + options.userId = userId; + + var url = self.getUrl("Genres", options); + + return self.getJSON(url); + }; + + self.getMusicGenres = function (userId, options) { + + if (!userId) { + throw new Error("null userId"); + } + + options = options || {}; + options.userId = userId; + + var url = self.getUrl("MusicGenres", options); + + return self.getJSON(url); + }; + + self.getGameGenres = function (userId, options) { + + if (!userId) { + throw new Error("null userId"); + } + + options = options || {}; + options.userId = userId; + + var url = self.getUrl("GameGenres", options); + + return self.getJSON(url); + }; + + /** + Gets people from an item + */ + self.getPeople = function (userId, options) { + + if (!userId) { + throw new Error("null userId"); + } + + options = options || {}; + options.userId = userId; + + var url = self.getUrl("Persons", options); + + return self.getJSON(url); + }; + + /** + Gets studios from an item + */ + self.getStudios = function (userId, options) { + + if (!userId) { + throw new Error("null userId"); + } + + options = options || {}; + options.userId = userId; + + var url = self.getUrl("Studios", options); + + return self.getJSON(url); + }; + + /** + * Gets local trailers for an item + */ + self.getLocalTrailers = function (userId, itemId) { + + if (!userId) { + throw new Error("null userId"); + } + if (!itemId) { + throw new Error("null itemId"); + } + + var url = self.getUrl("Users/" + userId + "/Items/" + itemId + "/LocalTrailers"); + + return self.getJSON(url); + }; + + self.getAdditionalVideoParts = function (userId, itemId) { + + if (!itemId) { + throw new Error("null itemId"); + } + + var options = {}; + + if (userId) { + options.userId = userId; + } + + var url = self.getUrl("Videos/" + itemId + "/AdditionalParts", options); + + return self.getJSON(url); + }; + + self.getThemeMedia = function (userId, itemId, inherit) { + + if (!itemId) { + throw new Error("null itemId"); + } + + var options = {}; + + if (userId) { + options.userId = userId; + } + + options.InheritFromParent = inherit || false; + + var url = self.getUrl("Items/" + itemId + "/ThemeMedia", options); + + return self.getJSON(url); + }; + + self.getSearchHints = function (options) { + + var url = self.getUrl("Search/Hints", options); + + return self.getJSON(url); + }; + + /** + * Gets special features for an item + */ + self.getSpecialFeatures = function (userId, itemId) { + + if (!userId) { + throw new Error("null userId"); + } + if (!itemId) { + throw new Error("null itemId"); + } + + var url = self.getUrl("Users/" + userId + "/Items/" + itemId + "/SpecialFeatures"); + + return self.getJSON(url); + }; + + self.getDateParamValue = function (date) { + + function formatDigit(i) { + return i < 10 ? "0" + i : i; + } + + var d = date; + + return "" + d.getFullYear() + formatDigit(d.getMonth() + 1) + formatDigit(d.getDate()) + formatDigit(d.getHours()) + formatDigit(d.getMinutes()) + formatDigit(d.getSeconds()); + }; + + self.markPlayed = function (userId, itemId, date) { + + if (!userId) { + throw new Error("null userId"); + } + + if (!itemId) { + throw new Error("null itemId"); + } + + var options = {}; + + if (date) { + options.DatePlayed = self.getDateParamValue(date); + } + + var url = self.getUrl("Users/" + userId + "/PlayedItems/" + itemId, options); + + return self.ajax({ + type: "POST", + url: url, + dataType: "json" + }); + }; + + self.markUnplayed = function (userId, itemId) { + + if (!userId) { + throw new Error("null userId"); + } + + if (!itemId) { + throw new Error("null itemId"); + } + + var url = self.getUrl("Users/" + userId + "/PlayedItems/" + itemId); + + return self.ajax({ + type: "DELETE", + url: url, + dataType: "json" + }); + }; + + /** + * Updates a user's favorite status for an item. + * @param {String} userId + * @param {String} itemId + * @param {Boolean} isFavorite + */ + self.updateFavoriteStatus = function (userId, itemId, isFavorite) { + + if (!userId) { + throw new Error("null userId"); + } + + if (!itemId) { + throw new Error("null itemId"); + } + + var url = self.getUrl("Users/" + userId + "/FavoriteItems/" + itemId); + + var method = isFavorite ? "POST" : "DELETE"; + + return self.ajax({ + type: method, + url: url, + dataType: "json" + }); + }; + + /** + * Updates a user's personal rating for an item + * @param {String} userId + * @param {String} itemId + * @param {Boolean} likes + */ + self.updateUserItemRating = function (userId, itemId, likes) { + + if (!userId) { + throw new Error("null userId"); + } + + if (!itemId) { + throw new Error("null itemId"); + } + + var url = self.getUrl("Users/" + userId + "/Items/" + itemId + "/Rating", { + likes: likes + }); + + return self.ajax({ + type: "POST", + url: url, + dataType: "json" + }); + }; + + self.getItemCounts = function (userId) { + + var options = {}; + + if (userId) { + options.userId = userId; + } + + var url = self.getUrl("Items/Counts", options); + + return self.getJSON(url); + }; + + /** + * Clears a user's personal rating for an item + * @param {String} userId + * @param {String} itemId + */ + self.clearUserItemRating = function (userId, itemId) { + + if (!userId) { + throw new Error("null userId"); + } + + if (!itemId) { + throw new Error("null itemId"); + } + + var url = self.getUrl("Users/" + userId + "/Items/" + itemId + "/Rating"); + + return self.ajax({ + type: "DELETE", + url: url, + dataType: "json" + }); + }; + + /** + * Reports the user has started playing something + * @param {String} userId + * @param {String} itemId + */ + self.reportPlaybackStart = function (options) { + + if (!options) { + throw new Error("null options"); + } + + var url = self.getUrl("Sessions/Playing"); + + return self.ajax({ + type: "POST", + data: JSON.stringify(options), + contentType: "application/json", + url: url + }); + }; + + /** + * Reports progress viewing an item + * @param {String} userId + * @param {String} itemId + */ + self.reportPlaybackProgress = function (options) { + + if (!options) { + throw new Error("null options"); + } + + if (self.isWebSocketOpen()) { + + return new Promise(function (resolve, reject) { + + var msg = JSON.stringify(options); + self.sendWebSocketMessage("ReportPlaybackProgress", msg); + resolve(); + }); + } + + var url = self.getUrl("Sessions/Playing/Progress"); + + return self.ajax({ + type: "POST", + data: JSON.stringify(options), + contentType: "application/json", + url: url + }); + }; + + self.reportOfflineActions = function (actions) { + + if (!actions) { + throw new Error("null actions"); + } + + var url = self.getUrl("Sync/OfflineActions"); + + return self.ajax({ + type: "POST", + data: JSON.stringify(actions), + contentType: "application/json", + url: url + }); + }; + + self.syncData = function (data) { + + if (!data) { + throw new Error("null data"); + } + + var url = self.getUrl("Sync/Data"); + + return self.ajax({ + type: "POST", + data: JSON.stringify(data), + contentType: "application/json", + url: url, + dataType: "json" + }); + }; + + self.getReadySyncItems = function (deviceId) { + + if (!deviceId) { + throw new Error("null deviceId"); + } + + var url = self.getUrl("Sync/Items/Ready", { + TargetId: deviceId + }); + + return self.getJSON(url); + }; + + self.reportSyncJobItemTransferred = function (syncJobItemId) { + + if (!syncJobItemId) { + throw new Error("null syncJobItemId"); + } + + var url = self.getUrl("Sync/JobItems/" + syncJobItemId + "/Transferred"); + + return self.ajax({ + type: "POST", + url: url + }); + }; + + /** + * Reports a user has stopped playing an item + * @param {String} userId + * @param {String} itemId + */ + self.reportPlaybackStopped = function (options) { + + if (!options) { + throw new Error("null options"); + } + + var url = self.getUrl("Sessions/Playing/Stopped"); + + return self.ajax({ + type: "POST", + data: JSON.stringify(options), + contentType: "application/json", + url: url + }); + }; + + self.sendPlayCommand = function (sessionId, options) { + + if (!sessionId) { + throw new Error("null sessionId"); + } + + if (!options) { + throw new Error("null options"); + } + + var url = self.getUrl("Sessions/" + sessionId + "/Playing", options); + + return self.ajax({ + type: "POST", + url: url + }); + }; + + self.sendCommand = function (sessionId, command) { + + if (!sessionId) { + throw new Error("null sessionId"); + } + + if (!command) { + throw new Error("null command"); + } + + var url = self.getUrl("Sessions/" + sessionId + "/Command"); + + var ajaxOptions = { + type: "POST", + url: url + }; + + ajaxOptions.data = JSON.stringify(command); + ajaxOptions.contentType = "application/json"; + + return self.ajax(ajaxOptions); + }; + + self.sendMessageCommand = function (sessionId, options) { + + if (!sessionId) { + throw new Error("null sessionId"); + } + + if (!options) { + throw new Error("null options"); + } + + var url = self.getUrl("Sessions/" + sessionId + "/Message", options); + + return self.ajax({ + type: "POST", + url: url + }); + }; + + self.sendPlayStateCommand = function (sessionId, command, options) { + + if (!sessionId) { + throw new Error("null sessionId"); + } + + if (!command) { + throw new Error("null command"); + } + + var url = self.getUrl("Sessions/" + sessionId + "/Playing/" + command, options || {}); + + return self.ajax({ + type: "POST", + url: url + }); + }; + + self.createPackageReview = function (review) { + + var url = self.getUrl("Packages/Reviews/" + review.id, review); + + return self.ajax({ + type: "POST", + url: url, + }); + }; + + self.getPackageReviews = function (packageId, minRating, maxRating, limit) { + + if (!packageId) { + throw new Error("null packageId"); + } + + var options = {}; + + if (minRating) { + options.MinRating = minRating; + } + if (maxRating) { + options.MaxRating = maxRating; + } + if (limit) { + options.Limit = limit; + } + + var url = self.getUrl("Packages/" + packageId + "/Reviews", options); + + return self.getJSON(url); + }; + }; + +})(window, window.JSON, window.WebSocket, window.setTimeout, window.devicePixelRatio, window.FileReader); \ No newline at end of file diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..bb6a27c --- /dev/null +++ b/bower.json @@ -0,0 +1,8 @@ +{ + "name": "emby-apiclient", + "main": "apiclient.js", + "devDependencies": { + + }, + "ignore": [] +} diff --git a/connectionmanager.js b/connectionmanager.js new file mode 100644 index 0000000..555ebcb --- /dev/null +++ b/connectionmanager.js @@ -0,0 +1,1514 @@ +(function (globalScope) { + + if (!globalScope.MediaBrowser) { + globalScope.MediaBrowser = {}; + } + + globalScope.MediaBrowser.ConnectionState = { + Unavailable: 0, + ServerSelection: 1, + ServerSignIn: 2, + SignedIn: 3, + ConnectSignIn: 4 + }; + + globalScope.MediaBrowser.ConnectionMode = { + Local: 0, + Remote: 1, + Manual: 2 + }; + + globalScope.MediaBrowser.ServerInfo = { + + getServerAddress: function (server, mode) { + + switch (mode) { + case MediaBrowser.ConnectionMode.Local: + return server.LocalAddress; + case MediaBrowser.ConnectionMode.Manual: + return server.ManualAddress; + case MediaBrowser.ConnectionMode.Remote: + return server.RemoteAddress; + default: + return server.ManualAddress || server.LocalAddress || server.RemoteAddress; + } + } + }; + + globalScope.MediaBrowser.ConnectionManager = function (logger, credentialProvider, appName, appVersion, deviceName, deviceId, capabilities) { + + logger.log('Begin MediaBrowser.ConnectionManager constructor'); + + var self = this; + var apiClients = []; + var defaultTimeout = 20000; + + function mergeServers(list1, list2) { + + for (var i = 0, length = list2.length; i < length; i++) { + credentialProvider.addOrUpdateServer(list1, list2[i]); + } + + return list1; + } + + function resolveFailure(resolve) { + + resolve({ + State: MediaBrowser.ConnectionState.Unavailable, + ConnectUser: self.connectUser() + }); + } + + function updateServerInfo(server, systemInfo) { + + server.Name = systemInfo.ServerName; + server.Id = systemInfo.Id; + + if (systemInfo.LocalAddress) { + server.LocalAddress = systemInfo.LocalAddress; + } + if (systemInfo.WanAddress) { + server.RemoteAddress = systemInfo.WanAddress; + } + if (systemInfo.MacAddress) { + server.WakeOnLanInfos = [ + { MacAddress: systemInfo.MacAddress } + ]; + } + } + + function getEmbyServerUrl(baseUrl, handler) { + return baseUrl + "/emby/" + handler; + } + + function getFetchPromise(request) { + + var headers = request.headers || {}; + + if (request.dataType == 'json') { + headers.accept = 'application/json'; + } + + var fetchRequest = { + headers: headers, + method: request.type + }; + + var 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'; + } + } + + if (contentType) { + + headers['Content-Type'] = contentType; + } + + if (!request.timeout) { + return fetch(request.url, fetchRequest); + } + + return fetchWithTimeout(request.url, fetchRequest, request.timeout); + } + + function fetchWithTimeout(url, options, timeoutMs) { + + logger.log('fetchWithTimeout: timeoutMs: ' + timeoutMs + ', url: ' + url); + + return new Promise(function (resolve, reject) { + + var timeout = setTimeout(reject, timeoutMs); + + fetch(url, options).then(function (response) { + clearTimeout(timeout); + + logger.log('fetchWithTimeout: succeeded connecting to url: ' + url); + + resolve(response); + }, function (error) { + + clearTimeout(timeout); + + logger.log('fetchWithTimeout: timed out connecting to url: ' + url); + + reject(); + }); + }); + } + + function paramsToString(params) { + + var values = []; + + for (var key in params) { + + var value = params[key]; + + if (value !== null && value !== undefined && value !== '') { + values.push(encodeURIComponent(key) + "=" + encodeURIComponent(value)); + } + } + return values.join('&'); + } + + function ajax(request) { + + if (!request) { + throw new Error("Request cannot be null"); + } + + request.headers = request.headers || {}; + + logger.log('ConnectionManager requesting url: ' + request.url); + + return getFetchPromise(request).then(function (response) { + + logger.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); + } + + }, function (err) { + + logger.log('ConnectionManager request failed to url: ' + request.url); + throw err; + }); + } + + function tryConnect(url, timeout) { + + url = getEmbyServerUrl(url, "system/info/public"); + + logger.log('tryConnect url: ' + url); + + return ajax({ + + type: "GET", + url: url, + dataType: "json", + + timeout: timeout || defaultTimeout + + }); + } + + var connectUser; + self.connectUser = function () { + return connectUser; + }; + + self.appVersion = function () { + return appVersion; + }; + + 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) { + + var servers = credentialProvider.credentials().Servers; + + return servers.filter(function (s) { + + return s.Id == id; + + })[0]; + }; + + self.getLastUsedServer = function () { + + var servers = credentialProvider.credentials().Servers; + + servers.sort(function (a, b) { + return (b.DateLastAccessed || 0) - (a.DateLastAccessed || 0); + }); + + if (!servers.length) { + return null; + } + + return servers[0]; + }; + + self.getLastUsedApiClient = function () { + + var servers = credentialProvider.credentials().Servers; + + servers.sort(function (a, b) { + return (b.DateLastAccessed || 0) - (a.DateLastAccessed || 0); + }); + + if (!servers.length) { + return null; + } + + var server = servers[0]; + + return getOrAddApiClient(server, server.LastConnectionMode); + }; + + self.addApiClient = function (apiClient) { + + 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()); + + }); + + var existingServer = existingServers.length ? existingServers[0] : {}; + existingServer.DateLastAccessed = new Date().getTime(); + existingServer.LastConnectionMode = MediaBrowser.ConnectionMode.Manual; + if (existingServer.LastConnectionMode == MediaBrowser.ConnectionMode.Local) { + existingServer.DateLastLocalConnection = new Date().getTime(); + } + existingServer.ManualAddress = apiClient.serverAddress(); + apiClient.serverInfo(existingServer); + + apiClient.onAuthenticated = function (instance, result) { + onAuthenticated(instance, result, {}, true); + }; + + if (!existingServers.length) { + var credentials = credentialProvider.credentials(); + credentials.Servers = [existingServer]; + credentialProvider.credentials(credentials); + } + + Events.trigger(self, 'apiclientcreated', [apiClient]); + + if (existingServer.Id) { + return; + } + + apiClient.getPublicSystemInfo().then(function (systemInfo) { + + var credentials = credentialProvider.credentials(); + existingServer.Id = systemInfo.Id; + apiClient.serverInfo(existingServer); + + credentials.Servers = [existingServer]; + credentialProvider.credentials(credentials); + }); + }; + + self.clearData = function () { + + logger.log('connection manager clearing data'); + + connectUser = null; + var credentials = credentialProvider.credentials(); + credentials.ConnectAccessToken = null; + credentials.ConnectUserId = null; + credentials.Servers = []; + credentialProvider.credentials(credentials); + }; + + function onConnectUserSignIn(user) { + + connectUser = user; + Events.trigger(self, 'connectusersignedin', [user]); + } + + function getOrAddApiClient(server, connectionMode) { + + var apiClient = self.getApiClient(server.Id); + + if (!apiClient) { + + var url = MediaBrowser.ServerInfo.getServerAddress(server, connectionMode); + + apiClient = new MediaBrowser.ApiClient(logger, url, appName, appVersion, deviceName, deviceId); + + apiClients.push(apiClient); + + apiClient.serverInfo(server); + + apiClient.onAuthenticated = function (instance, result) { + onAuthenticated(instance, result, {}, true); + }; + + Events.trigger(self, 'apiclientcreated', [apiClient]); + } + + logger.log('returning instance from getOrAddApiClient'); + return apiClient; + } + + self.getOrCreateApiClient = function (serverId) { + + var credentials = credentialProvider.credentials(); + var 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 getOrAddApiClient(server, server.LastConnectionMode); + }; + + function onAuthenticated(apiClient, result, options, saveCredentials) { + + var credentials = credentialProvider.credentials(); + var servers = credentials.Servers.filter(function (s) { + return s.Id == result.ServerId; + }); + + var server = servers.length ? servers[0] : apiClient.serverInfo(); + + if (options.updateDateLastAccessed !== false) { + server.DateLastAccessed = new Date().getTime(); + + if (server.LastConnectionMode == MediaBrowser.ConnectionMode.Local) { + server.DateLastLocalConnection = new Date().getTime(); + } + } + server.Id = result.ServerId; + + if (saveCredentials) { + server.UserId = result.User.Id; + server.AccessToken = result.AccessToken; + } else { + server.UserId = null; + server.AccessToken = null; + } + + credentialProvider.addOrUpdateServer(credentials.Servers, server); + saveUserInfoIntoCredentials(server, result.User); + credentialProvider.credentials(credentials); + + afterConnected(apiClient, options); + + onLocalUserSignIn(result.User); + } + + function saveUserInfoIntoCredentials(server, user) { + + var info = { + Id: user.Id, + IsSignedInOffline: true + } + + credentialProvider.addOrUpdateUser(server, info); + } + + function afterConnected(apiClient, options) { + + options = options || {}; + + if (options.reportCapabilities !== false) { + apiClient.reportCapabilities(capabilities); + } + + if (options.enableWebSocket !== false) { + if (!apiClient.isWebSocketOpenOrConnecting && apiClient.isWebSocketSupported()) { + logger.log('calling apiClient.openWebSocket'); + + apiClient.openWebSocket(); + } + } + } + + function onLocalUserSignIn(user) { + + Events.trigger(self, 'localusersignedin', [user]); + } + + function ensureConnectUser(credentials) { + + return new Promise(function (resolve, reject) { + + if (connectUser && connectUser.Id == credentials.ConnectUserId) { + resolve(); + } + + else if (credentials.ConnectUserId && credentials.ConnectAccessToken) { + + connectUser = null; + + getConnectUser(credentials.ConnectUserId, credentials.ConnectAccessToken).then(function (user) { + + onConnectUserSignIn(user); + resolve(); + + }, function () { + resolve(); + }); + + } else { + resolve(); + } + }); + } + + function getConnectUser(userId, accessToken) { + + if (!userId) { + throw new Error("null userId"); + } + if (!accessToken) { + throw new Error("null accessToken"); + } + + var url = "https://connect.emby.media/service/user?id=" + userId; + + return ajax({ + type: "GET", + url: url, + 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 = MediaBrowser.ServerInfo.getServerAddress(server, connectionMode); + + url = getEmbyServerUrl(url, "Connect/Exchange?format=json&ConnectUserId=" + credentials.ConnectUserId); + + return ajax({ + type: "GET", + url: url, + dataType: "json", + headers: { + "X-MediaBrowser-Token": server.ExchangeToken + } + + }).then(function (auth) { + + server.UserId = auth.LocalUserId; + server.AccessToken = auth.AccessToken; + return auth; + + }, function () { + + server.UserId = null; + server.AccessToken = null; + return Promise.reject(); + + }); + } + + function validateAuthentication(server, connectionMode) { + + return new Promise(function (resolve, reject) { + + var url = MediaBrowser.ServerInfo.getServerAddress(server, connectionMode); + + ajax({ + + type: "GET", + url: getEmbyServerUrl(url, "System/Info"), + dataType: "json", + headers: { + "X-MediaBrowser-Token": server.AccessToken + } + + }).then(function (systemInfo) { + + updateServerInfo(server, systemInfo); + + if (server.UserId) { + + ajax({ + + type: "GET", + url: getEmbyServerUrl(url, "users/" + server.UserId), + dataType: "json", + headers: { + "X-MediaBrowser-Token": server.AccessToken + } + + }).then(function (user) { + + onLocalUserSignIn(user); + resolve(); + + }, function () { + + server.UserId = null; + server.AccessToken = null; + resolve(); + }); + } + + }, function () { + + server.UserId = null; + server.AccessToken = null; + resolve(); + }); + }); + } + + function getImageUrl(localUser) { + + if (connectUser && connectUser.ImageUrl) { + return { + url: connectUser.ImageUrl + }; + } + if (localUser && localUser.PrimaryImageTag) { + + var apiClient = self.getApiClient(localUser); + + var url = apiClient.getUserImageUrl(localUser.Id, { + tag: localUser.PrimaryImageTag, + type: "Primary" + }); + + return { + url: url, + supportsParams: true + }; + } + + return { + url: null, + supportsParams: false + }; + } + + self.user = function (apiClient) { + + return new Promise(function (resolve, reject) { + + var localUser; + + function onLocalUserDone(e) { + + var image = getImageUrl(localUser); + + resolve({ + localUser: localUser, + name: connectUser ? connectUser.Name : (localUser ? localUser.Name : null), + imageUrl: image.url, + supportsImageParams: image.supportsParams + }); + } + + function onEnsureConnectUserDone() { + + if (apiClient && apiClient.getCurrentUserId()) { + apiClient.getCurrentUser().then(function (u) { + localUser = u; + onLocalUserDone(); + + }, onLocalUserDone); + } else { + onLocalUserDone(); + } + } + + var credentials = credentialProvider.credentials(); + + if (credentials.ConnectUserId && credentials.ConnectAccessToken && !(apiClient && apiClient.getCurrentUserId())) { + ensureConnectUser(credentials).then(onEnsureConnectUserDone, onEnsureConnectUserDone); + } else { + onEnsureConnectUserDone(); + } + }); + }; + + self.isLoggedIntoConnect = function () { + + // Make sure it returns true or false + if (!self.connectToken() || !self.connectUserId()) { + return false; + } + return true; + }; + + self.logout = function () { + + Logger.log('begin connectionManager loguot'); + var promises = []; + + for (var i = 0, length = apiClients.length; i < length; i++) { + + var apiClient = apiClients[i]; + + if (apiClient.accessToken()) { + promises.push(logoutOfServer(apiClient)); + } + } + + return Promise.all(promises).then(function () { + + var credentials = credentialProvider.credentials(); + + var servers = credentials.Servers.filter(function (u) { + return u.UserLinkType != "Guest"; + }); + + for (var j = 0, numServers = servers.length; j < numServers; j++) { + + var server = servers[j]; + + server.UserId = null; + server.AccessToken = null; + server.ExchangeToken = null; + + var serverUsers = server.Users || []; + + for (var k = 0, numUsers = serverUsers.length; k < numUsers; k++) { + + serverUsers[k].IsSignedInOffline = false; + } + } + + credentials.Servers = servers; + credentials.ConnectAccessToken = null; + credentials.ConnectUserId = null; + + credentialProvider.credentials(credentials); + + if (connectUser) { + connectUser = null; + Events.trigger(self, 'connectusersignedout'); + } + }); + }; + + function logoutOfServer(apiClient) { + + var serverInfo = apiClient.serverInfo() || {}; + + var logoutInfo = { + serverId: serverInfo.Id + }; + + return apiClient.logout().then(function () { + + Events.trigger(self, 'localusersignedout', [logoutInfo]); + }, function () { + + Events.trigger(self, 'localusersignedout', [logoutInfo]); + }); + } + + function getConnectServers(credentials) { + + logger.log('Begin getConnectServers'); + + return new Promise(function (resolve, reject) { + + if (!credentials.ConnectAccessToken || !credentials.ConnectUserId) { + resolve([]); + return; + } + + var url = "https://connect.emby.media/service/servers?userId=" + credentials.ConnectUserId; + + ajax({ + type: "GET", + url: url, + dataType: "json", + headers: { + "X-Application": appName + "/" + appVersion, + "X-Connect-UserToken": credentials.ConnectAccessToken + } + + }).then(function (servers) { + + servers = servers.map(function (i) { + return { + 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" + }; + }); + + resolve(servers); + + }, function () { + resolve([]); + + }); + }); + } + + self.getSavedServers = function () { + + var credentials = credentialProvider.credentials(); + + var servers = credentials.Servers.slice(0); + + servers.sort(function (a, b) { + return (b.DateLastAccessed || 0) - (a.DateLastAccessed || 0); + }); + + return servers; + }; + + self.getAvailableServers = function () { + + logger.log('Begin getAvailableServers'); + + // Clone the array + var credentials = credentialProvider.credentials(); + + return Promise.all([getConnectServers(credentials), findServers()]).then(function (responses) { + + var connectServers = responses[0]; + var foundServers = responses[1]; + + var servers = credentials.Servers.slice(0); + mergeServers(servers, foundServers); + mergeServers(servers, connectServers); + + servers = filterServers(servers, connectServers); + + servers.sort(function (a, b) { + return (b.DateLastAccessed || 0) - (a.DateLastAccessed || 0); + }); + + credentials.Servers = servers; + + credentialProvider.credentials(credentials); + + return servers; + }); + }; + + function filterServers(servers, connectServers) { + + return servers.filter(function (server) { + + // It's not a connect server, so assume it's still valid + if (!server.ExchangeToken) { + return true; + } + + return connectServers.filter(function (connectServer) { + + return server.Id == connectServer.Id; + + }).length > 0; + }); + } + + function findServers() { + + return new Promise(function (resolve, reject) { + + require(['serverdiscovery'], function (serverDiscovery) { + serverDiscovery.findServers(1000).then(function (foundServers) { + + var servers = foundServers.map(function (foundServer) { + + var info = { + Id: foundServer.Id, + LocalAddress: foundServer.Address, + Name: foundServer.Name, + ManualAddress: convertEndpointAddressToManualAddress(foundServer), + DateLastLocalConnection: new Date().getTime() + }; + + info.LastConnectionMode = info.ManualAddress ? MediaBrowser.ConnectionMode.Manual : MediaBrowser.ConnectionMode.Local; + + return info; + }); + resolve(servers); + }); + + }); + }); + } + + function convertEndpointAddressToManualAddress(info) { + + if (info.Address && info.EndpointAddress) { + var address = info.EndpointAddress.split(":")[0]; + + // Determine the port, if any + var parts = info.Address.split(":"); + if (parts.length > 1) { + var portString = parts[parts.length - 1]; + + if (!isNaN(parseInt(portString))) { + address += ":" + portString; + } + } + + return normalizeAddress(address); + } + + return null; + } + + self.connect = function () { + + logger.log('Begin connect'); + + return new Promise(function (resolve, reject) { + + self.getAvailableServers().then(function (servers) { + + self.connectToServers(servers).then(function (result) { + + resolve(result); + }); + }); + }); + }; + + self.getOffineResult = function () { + + // TODO: Implement + }; + + self.connectToServers = function (servers) { + + logger.log('Begin connectToServers, with ' + servers.length + ' servers'); + + return new Promise(function (resolve, reject) { + + if (servers.length == 1) { + + self.connectToServer(servers[0]).then(function (result) { + + if (result.State == MediaBrowser.ConnectionState.Unavailable) { + + result.State = result.ConnectUser == null ? + MediaBrowser.ConnectionState.ConnectSignIn : + MediaBrowser.ConnectionState.ServerSelection; + } + + logger.log('resolving connectToServers with result.State: ' + result.State); + resolve(result); + + }); + + } else { + + var firstServer = servers.length ? servers[0] : null; + // See if we have any saved credentials and can auto sign in + if (firstServer) { + self.connectToServer(firstServer).then(function (result) { + + if (result.State == MediaBrowser.ConnectionState.SignedIn) { + + resolve(result); + + } else { + resolve({ + Servers: servers, + State: (!servers.length && !self.connectUser()) ? MediaBrowser.ConnectionState.ConnectSignIn : MediaBrowser.ConnectionState.ServerSelection, + ConnectUser: self.connectUser() + }); + } + + }); + } else { + + resolve({ + Servers: servers, + State: (!servers.length && !self.connectUser()) ? MediaBrowser.ConnectionState.ConnectSignIn : MediaBrowser.ConnectionState.ServerSelection, + ConnectUser: self.connectUser() + }); + } + } + + }); + }; + + function beginWakeServer(server) { + + require(['wakeonlan'], function (wakeonlan) { + var infos = server.WakeOnLanInfos || []; + + for (var i = 0, length = infos.length; i < length; i++) { + + wakeonlan.send(infos[i]); + } + }); + } + + self.connectToServer = function (server, options) { + + return new Promise(function (resolve, reject) { + + var tests = []; + + if (server.LastConnectionMode != null) { + //tests.push(server.LastConnectionMode); + } + if (tests.indexOf(MediaBrowser.ConnectionMode.Manual) == -1) { tests.push(MediaBrowser.ConnectionMode.Manual); } + if (tests.indexOf(MediaBrowser.ConnectionMode.Local) == -1) { tests.push(MediaBrowser.ConnectionMode.Local); } + if (tests.indexOf(MediaBrowser.ConnectionMode.Remote) == -1) { tests.push(MediaBrowser.ConnectionMode.Remote); } + + beginWakeServer(server); + + var wakeOnLanSendTime = new Date().getTime(); + + options = options || {}; + testNextConnectionMode(tests, 0, server, wakeOnLanSendTime, options, resolve); + }); + }; + + function stringEqualsIgnoreCase(str1, str2) { + + return (str1 || '').toLowerCase() == (str2 || '').toLowerCase(); + } + + function testNextConnectionMode(tests, index, server, wakeOnLanSendTime, options, resolve) { + + if (index >= tests.length) { + + logger.log('Tested all connection modes. Failing server connection.'); + resolveFailure(resolve); + return; + } + + var mode = tests[index]; + var address = MediaBrowser.ServerInfo.getServerAddress(server, mode); + var enableRetry = false; + var skipTest = false; + var timeout = defaultTimeout; + + if (mode == MediaBrowser.ConnectionMode.Local) { + + enableRetry = true; + timeout = 8000; + } + + else if (mode == MediaBrowser.ConnectionMode.Manual) { + + if (stringEqualsIgnoreCase(address, server.LocalAddress) || + stringEqualsIgnoreCase(address, server.RemoteAddress)) { + skipTest = true; + } + } + + if (skipTest || !address) { + testNextConnectionMode(tests, index + 1, server, wakeOnLanSendTime, options, resolve); + return; + } + + logger.log('testing connection mode ' + mode + ' with server ' + server.Name); + + tryConnect(address, timeout).then(function (result) { + + logger.log('calling onSuccessfulConnection with connection mode ' + mode + ' with server ' + server.Name); + onSuccessfulConnection(server, result, mode, options, resolve); + + }, function () { + + logger.log('test failed for connection mode ' + mode + ' with server ' + server.Name); + + if (enableRetry) { + + var sleepTime = 10000 - (new Date().getTime() - wakeOnLanSendTime); + + // TODO: Implement delay and retry + + testNextConnectionMode(tests, index + 1, server, wakeOnLanSendTime, options, resolve); + + } else { + testNextConnectionMode(tests, index + 1, server, wakeOnLanSendTime, options, resolve); + + } + }); + } + + function onSuccessfulConnection(server, systemInfo, connectionMode, options, resolve) { + + var credentials = credentialProvider.credentials(); + if (credentials.ConnectAccessToken) { + + ensureConnectUser(credentials).then(function () { + + if (server.ExchangeToken) { + addAuthenticationInfoFromConnect(server, connectionMode, credentials).then(function () { + + afterConnectValidated(server, credentials, systemInfo, connectionMode, true, options, resolve); + + }, function () { + + afterConnectValidated(server, credentials, systemInfo, connectionMode, true, options, resolve); + }); + + } else { + + afterConnectValidated(server, credentials, systemInfo, connectionMode, true, options, resolve); + } + }); + } + else { + afterConnectValidated(server, credentials, systemInfo, connectionMode, true, options, resolve); + } + } + + function afterConnectValidated(server, credentials, systemInfo, connectionMode, verifyLocalAuthentication, options, resolve) { + + if (verifyLocalAuthentication && server.AccessToken) { + + validateAuthentication(server, connectionMode).then(function () { + + afterConnectValidated(server, credentials, systemInfo, connectionMode, false, options, resolve); + }); + + return; + } + + updateServerInfo(server, systemInfo); + + server.LastConnectionMode = connectionMode; + + if (options.updateDateLastAccessed !== false) { + server.DateLastAccessed = new Date().getTime(); + + if (server.LastConnectionMode == MediaBrowser.ConnectionMode.Local) { + server.DateLastLocalConnection = new Date().getTime(); + } + } + credentialProvider.addOrUpdateServer(credentials.Servers, server); + credentialProvider.credentials(credentials); + + var result = { + Servers: [] + }; + + result.ApiClient = getOrAddApiClient(server, connectionMode); + result.State = server.AccessToken ? + MediaBrowser.ConnectionState.SignedIn : + MediaBrowser.ConnectionState.ServerSignIn; + + result.Servers.push(server); + result.ApiClient.updateServerInfo(server, connectionMode); + + if (result.State == MediaBrowser.ConnectionState.SignedIn) { + afterConnected(result.ApiClient, options); + } + + resolve(result); + + Events.trigger(self, 'connected', [result]); + } + + function normalizeAddress(address) { + + // attempt to correct bad input + address = address.trim(); + + if (address.toLowerCase().indexOf('http') != 0) { + address = "http://" + address; + } + + // Seeing failures in iOS when protocol isn't lowercase + address = address.replace('Http:', 'http:'); + address = address.replace('Https:', 'https:'); + + return address; + } + + self.connectToAddress = function (address) { + + return new Promise(function (resolve, reject) { + + if (!address) { + reject(); + return; + } + + address = normalizeAddress(address); + + function onFail() { + logger.log('connectToAddress ' + address + ' failed'); + resolveFailure(resolve); + } + + tryConnect(address, defaultTimeout).then(function (publicInfo) { + + logger.log('connectToAddress ' + address + ' succeeded'); + + var server = { + ManualAddress: address, + LastConnectionMode: MediaBrowser.ConnectionMode.Manual + }; + updateServerInfo(server, publicInfo); + + self.connectToServer(server).then(resolve, onFail); + + }, onFail); + + }); + }; + + self.loginToConnect = function (username, password) { + + return new Promise(function (resolve, reject) { + + if (!username) { + reject(); + return; + } + if (!password) { + reject(); + return; + } + + require(['connectservice', 'cryptojs-md5'], function () { + + var md5 = self.getConnectPasswordHash(password); + + ajax({ + type: "POST", + url: "https://connect.emby.media/service/user/authenticate", + data: { + nameOrEmail: username, + password: md5 + }, + dataType: "json", + contentType: 'application/x-www-form-urlencoded; charset=UTF-8', + headers: { + "X-Application": appName + "/" + appVersion + } + + }).then(function (result) { + + var credentials = credentialProvider.credentials(); + + credentials.ConnectAccessToken = result.AccessToken; + credentials.ConnectUserId = result.User.Id; + + credentialProvider.credentials(credentials); + + onConnectUserSignIn(result.User); + + resolve(result); + + }, reject); + }); + }); + }; + + self.signupForConnect = function (email, username, password, passwordConfirm) { + + return new Promise(function (resolve, reject) { + + if (!email) { + reject({ errorCode: 'invalidinput' }); + return; + } + if (!username) { + reject({ errorCode: 'invalidinput' }); + return; + } + if (!password) { + reject({ errorCode: 'invalidinput' }); + return; + } + if (!passwordConfirm) { + reject({ errorCode: 'passwordmatch' }); + return; + } + if (password != passwordConfirm) { + reject({ errorCode: 'passwordmatch' }); + return; + } + + require(['connectservice', 'cryptojs-md5'], function () { + + var md5 = self.getConnectPasswordHash(password); + + ajax({ + type: "POST", + url: "https://connect.emby.media/service/register", + data: { + email: email, + userName: username, + password: md5 + }, + dataType: "json", + contentType: 'application/x-www-form-urlencoded; charset=UTF-8', + headers: { + "X-Application": appName + "/" + appVersion, + "X-CONNECT-TOKEN": "CONNECT-REGISTER" + } + + }).then(resolve, function (response) { + + try { + return response.json(); + + } catch (err) { + reject(); + } + + }).then(function (result) { + + if (result && result.Status) { + reject({ errorCode: result.Status }); + } + + }, reject); + }); + }); + }; + + self.getConnectPasswordHash = function (password) { + + password = globalScope.MediaBrowser.ConnectService.cleanPassword(password); + + return CryptoJS.MD5(password).toString(); + }; + + self.getApiClient = function (item) { + + // Accept string + object + if (item.ServerId) { + item = item.ServerId; + } + + return apiClients.filter(function (a) { + + var serverInfo = a.serverInfo(); + + // We have to keep this hack in here because of the addApiClient method + return !serverInfo || serverInfo.Id == item; + + })[0]; + }; + + self.getUserInvitations = function () { + + var connectToken = self.connectToken(); + + if (!connectToken) { + throw new Error("null connectToken"); + } + if (!self.connectUserId()) { + throw new Error("null connectUserId"); + } + + var url = "https://connect.emby.media/service/servers?userId=" + self.connectUserId() + "&status=Waiting"; + + return ajax({ + type: "GET", + url: url, + 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; + }); + server = server.length ? server[0] : null; + + return 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) { + onDone(); + return; + } + + var connectToken = self.connectToken(); + var connectUserId = self.connectUserId(); + + if (!connectToken || !connectUserId) { + onDone(); + return; + } + + var url = "https://connect.emby.media/service/serverAuthorizations?serverId=" + server.ConnectServerId + "&userId=" + connectUserId; + + ajax({ + type: "DELETE", + url: url, + 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"); + } + + var url = "https://connect.emby.media/service/ServerAuthorizations/accept?serverId=" + serverId + "&userId=" + self.connectUserId(); + + return ajax({ + type: "GET", + url: url, + headers: { + "X-Connect-UserToken": connectToken, + "X-Application": appName + "/" + appVersion + } + + }); + }; + + self.getRegistrationInfo = function (feature, apiClient) { + + return self.getAvailableServers().then(function (servers) { + + var matchedServers = servers.filter(function (s) { + return stringEqualsIgnoreCase(s.Id, apiClient.serverInfo().Id); + }); + + if (!matchedServers.length) { + return {}; + } + + var match = matchedServers[0]; + + if (!match.DateLastLocalConnection) { + + return ApiClient.getJSON(ApiClient.getUrl('System/Endpoint')).then(function (info) { + + if (info.IsInNetwork) { + + updateDateLastLocalConnection(match.Id); + return apiClient.getRegistrationInfo(feature); + } else { + return {}; + } + + }); + + } else { + return apiClient.getRegistrationInfo(feature); + } + }); + }; + + function updateDateLastLocalConnection(serverId) { + + var credentials = credentialProvider.credentials(); + var servers = credentials.Servers.filter(function (s) { + return s.Id == serverId; + }); + + var server = servers.length ? servers[0] : null; + + if (server) { + server.DateLastLocalConnection = new Date().getTime(); + credentialProvider.addOrUpdateServer(credentials.Servers, server); + credentialProvider.credentials(credentials); + } + } + + return self; + }; + +})(window, window.Logger); \ No newline at end of file diff --git a/connectservice.js b/connectservice.js new file mode 100644 index 0000000..e27ff5e --- /dev/null +++ b/connectservice.js @@ -0,0 +1,34 @@ +(function (globalScope) { + + if (!globalScope.MediaBrowser) { + globalScope.MediaBrowser = {}; + } + + function replaceAll(str, find, replace) { + + return str.split(find).join(replace); + } + + var connectService = { + + cleanPassword: function (password) { + + password = password || ''; + + password = replaceAll(password, "&", "&"); + password = replaceAll(password, "/", "\"); + password = replaceAll(password, "!", "!"); + password = replaceAll(password, "$", "$"); + password = replaceAll(password, "\"", """); + password = replaceAll(password, "<", "<"); + password = replaceAll(password, ">", ">"); + password = replaceAll(password, "'", "'"); + + return password; + } + + }; + + globalScope.MediaBrowser.ConnectService = connectService; + +})(window); \ No newline at end of file diff --git a/credentials.js b/credentials.js new file mode 100644 index 0000000..82abd6e --- /dev/null +++ b/credentials.js @@ -0,0 +1,131 @@ +(function (globalScope, JSON) { + + if (!globalScope.MediaBrowser) { + globalScope.MediaBrowser = {}; + } + + globalScope.MediaBrowser.CredentialProvider = function (key) { + + var self = this; + var credentials = null; + key = key || 'servercredentials3'; + + function ensure() { + + if (!credentials) { + + var json = appStorage.getItem(key) || '{}'; + + Logger.log('credentials initialized with: ' + json); + credentials = JSON.parse(json); + credentials.Servers = credentials.Servers || []; + } + } + + function get() { + + ensure(); + return credentials; + } + + function set(data) { + + if (data) { + credentials = data; + appStorage.setItem(key, JSON.stringify(data)); + } else { + self.clear(); + } + + Events.trigger(self, 'credentialsupdated'); + } + + self.clear = function () { + credentials = null; + appStorage.removeItem(key); + }; + + self.credentials = function (data) { + + if (data) { + set(data); + } + + return get(); + }; + + self.addOrUpdateServer = function (list, server) { + + if (!server.Id) { + throw new Error('Server.Id cannot be null or empty'); + } + + var existing = list.filter(function (s) { + return s.Id == server.Id; + })[0]; + + if (existing) { + + // Merge the data + existing.DateLastAccessed = Math.max(existing.DateLastAccessed || 0, server.DateLastAccessed || 0); + + existing.UserLinkType = server.UserLinkType; + + if (server.AccessToken) { + existing.AccessToken = server.AccessToken; + existing.UserId = server.UserId; + } + if (server.ExchangeToken) { + existing.ExchangeToken = server.ExchangeToken; + } + if (server.RemoteAddress) { + existing.RemoteAddress = server.RemoteAddress; + } + if (server.ManualAddress) { + existing.ManualAddress = server.ManualAddress; + } + if (server.LocalAddress) { + existing.LocalAddress = server.LocalAddress; + } + if (server.Name) { + existing.Name = server.Name; + } + if (server.WakeOnLanInfos && server.WakeOnLanInfos.length) { + existing.WakeOnLanInfos = server.WakeOnLanInfos; + } + if (server.LastConnectionMode != null) { + existing.LastConnectionMode = server.LastConnectionMode; + } + if (server.ConnectServerId) { + existing.ConnectServerId = server.ConnectServerId; + } + existing.DateLastLocalConnection = Math.max(existing.DateLastLocalConnection || 0, server.DateLastLocalConnection || 0); + + return existing; + } + else { + list.push(server); + return server; + } + }; + + self.addOrUpdateUser = function (server, user) { + + server.Users = server.Users || []; + + var existing = server.Users.filter(function (s) { + return s.Id == user.Id; + })[0]; + + if (existing) { + + // Merge the data + existing.IsSignedInOffline = true; + } + else { + server.Users.push(user); + } + }; + }; + +})(window, window.JSON); \ No newline at end of file diff --git a/deferred.js b/deferred.js new file mode 100644 index 0000000..e5632d5 --- /dev/null +++ b/deferred.js @@ -0,0 +1,16 @@ +(function (globalScope) { + + globalScope.DeferredBuilder = { + + Deferred: function () { + return jQuery.Deferred(); + }, + + when: function (promises) { + + return jQuery.when(promises); + } + + }; + +})(window); \ No newline at end of file diff --git a/events.js b/events.js new file mode 100644 index 0000000..6d99c8d --- /dev/null +++ b/events.js @@ -0,0 +1,20 @@ +(function (globalScope) { + + globalScope.Events = { + + on: function (obj, eventName, selector, fn) { + + jQuery(obj).on(eventName, selector, fn); + }, + + off: function (obj, eventName, selector, fn) { + + jQuery(obj).off(eventName, selector, fn); + }, + + trigger: function (obj, eventName, params) { + jQuery(obj).trigger(eventName, params); + } + }; + +})(window); \ No newline at end of file diff --git a/fileupload.js b/fileupload.js new file mode 100644 index 0000000..5eec941 --- /dev/null +++ b/fileupload.js @@ -0,0 +1,22 @@ +(function (globalScope) { + + function fileUpload() { + + var self = this; + + self.upload = function (file, name, url) { + + return new Promise(function (resolve, reject) { + + reject(); + }); + }; + } + + if (!globalScope.MediaBrowser) { + globalScope.MediaBrowser = {}; + } + + globalScope.MediaBrowser.FileUpload = fileUpload; + +})(this); \ No newline at end of file diff --git a/localassetmanager.js b/localassetmanager.js new file mode 100644 index 0000000..b8154da --- /dev/null +++ b/localassetmanager.js @@ -0,0 +1,130 @@ +(function () { + + function getLocalMediaSource(serverId, itemId) { + return new Promise(function (resolve, reject) { + resolve(null); + }); + } + + function saveOfflineUser(user) { + return new Promise(function (resolve, reject) { + resolve(); + }); + } + + function deleteOfflineUser(id) { + return new Promise(function (resolve, reject) { + resolve(); + }); + } + + function getCameraPhotos() { + return new Promise(function (resolve, reject) { + resolve([]); + }); + } + + function getOfflineActions(serverId) { + return new Promise(function (resolve, reject) { + resolve([]); + }); + } + + function deleteOfflineActions(actions) { + return new Promise(function (resolve, reject) { + resolve([]); + }); + } + + function getServerItemIds(serverId) { + return new Promise(function (resolve, reject) { + resolve([]); + }); + } + + function removeLocalItem(itemId, serverId) { + return new Promise(function (resolve, reject) { + resolve(); + }); + } + + function getLocalItem(itemId, serverId) { + return new Promise(function (resolve, reject) { + resolve(); + }); + } + + function addOrUpdateLocalItem(localItem) { + return new Promise(function (resolve, reject) { + resolve(); + }); + } + + function createLocalItem(libraryItem, serverInfo, originalFileName) { + + return new Promise(function (resolve, reject) { + resolve({}); + }); + } + + function downloadFile(url, localPath) { + + return new Promise(function (resolve, reject) { + resolve(); + }); + } + + function downloadSubtitles(url, localItem, subtitleStreamh) { + + return new Promise(function (resolve, reject) { + resolve(""); + }); + } + + function hasImage(serverId, itemId, imageTag) { + return new Promise(function (resolve, reject) { + resolve(false); + }); + } + + function downloadImage(url, serverId, itemId, imageTag) { + return new Promise(function (resolve, reject) { + resolve(false); + }); + } + + function fileExists(path) { + + return new Promise(function (resolve, reject) { + resolve(false); + }); + } + + function translateFilePath(path) { + + return new Promise(function (resolve, reject) { + resolve(path); + }); + } + + window.LocalAssetManager = { + getLocalMediaSource: getLocalMediaSource, + saveOfflineUser: saveOfflineUser, + deleteOfflineUser: deleteOfflineUser, + getCameraPhotos: getCameraPhotos, + getOfflineActions: getOfflineActions, + deleteOfflineActions: deleteOfflineActions, + getServerItemIds: getServerItemIds, + removeLocalItem: removeLocalItem, + getLocalItem: getLocalItem, + addOrUpdateLocalItem: addOrUpdateLocalItem, + createLocalItem: createLocalItem, + downloadFile: downloadFile, + downloadSubtitles: downloadSubtitles, + hasImage: hasImage, + downloadImage: downloadImage, + fileExists: fileExists, + translateFilePath: translateFilePath + }; + +})(); \ No newline at end of file diff --git a/logger.js b/logger.js new file mode 100644 index 0000000..ee3cc72 --- /dev/null +++ b/logger.js @@ -0,0 +1,6 @@ +var Logger = { + + log: function (str) { + console.log(str); + } +}; \ No newline at end of file diff --git a/serverdiscovery.js b/serverdiscovery.js new file mode 100644 index 0000000..ec0b374 --- /dev/null +++ b/serverdiscovery.js @@ -0,0 +1,18 @@ +define([], function () { + + return { + + findServers: function (timeoutMs) { + + return new Promise(function (resolve, reject) { + + var servers = []; + + // Expected server properties + // Name, Id, Address, EndpointAddress (optional) + resolve(servers); + }); + } + }; + +}); \ No newline at end of file diff --git a/store.js b/store.js new file mode 100644 index 0000000..6599f47 --- /dev/null +++ b/store.js @@ -0,0 +1,51 @@ +(function (globalScope, localStorage, sessionStorage) { + + function myStore(defaultObject) { + + var self = this; + self.localData = {}; + + var isDefaultAvailable; + + if (defaultObject) { + try { + defaultObject.setItem('_test', '0'); + defaultObject.removeItem('_test'); + isDefaultAvailable = true; + } catch (e) { + + } + } + + self.setItem = function (name, value) { + + if (isDefaultAvailable) { + defaultObject.setItem(name, value); + } else { + self.localData[name] = value; + } + }; + + self.getItem = function (name) { + + if (isDefaultAvailable) { + return defaultObject.getItem(name); + } + + return self.localData[name]; + }; + + self.removeItem = function (name) { + + if (isDefaultAvailable) { + defaultObject.removeItem(name); + } else { + self.localData[name] = null; + } + }; + } + + globalScope.appStorage = new myStore(localStorage); + globalScope.sessionStore = new myStore(sessionStorage); + +})(window, window.localStorage, window.sessionStorage); \ No newline at end of file diff --git a/sync/contentuploader.js b/sync/contentuploader.js new file mode 100644 index 0000000..f546e92 --- /dev/null +++ b/sync/contentuploader.js @@ -0,0 +1,118 @@ +(function (globalScope) { + + function contentUploader(connectionManager) { + + var self = this; + + self.uploadImages = function (server) { + + var deferred = DeferredBuilder.Deferred(); + + LocalAssetManager.getCameraPhotos().then(function (photos) { + + if (!photos.length) { + deferred.resolve(); + return; + } + + var apiClient = connectionManager.getApiClient(server.Id); + + apiClient.getContentUploadHistory().then(function (uploadHistory) { + + photos = getFilesToUpload(photos, uploadHistory); + + Logger.log('Found ' + photos.length + ' files to upload'); + + uploadNext(photos, 0, server, apiClient, deferred); + + }, function () { + deferred.reject(); + }); + + }, function () { + deferred.reject(); + }); + + return deferred.promise(); + }; + + function getFilesToUpload(files, uploadHistory) { + + return files.filter(function (file) { + + // Seeing some null entries for some reason + if (!file) { + return false; + } + + return uploadHistory.FilesUploaded.filter(function (u) { + + return getUploadId(file) == u.Id; + + }).length == 0; + }); + } + + function getUploadId(file) { + return CryptoJS.SHA1(file + "1").toString(); + } + + function uploadNext(files, index, server, apiClient, deferred) { + + var length = files.length; + + if (index >= length) { + + deferred.resolve(); + return; + } + + uploadFile(files[index], apiClient).then(function () { + + uploadNext(files, index + 1, server, apiClient, deferred); + }, function () { + uploadNext(files, index + 1, server, apiClient, deferred); + }); + } + + function uploadFile(file, apiClient) { + + var deferred = DeferredBuilder.Deferred(); + + require(['fileupload', "cryptojs-sha1"], function () { + + var name = 'camera image ' + new Date().getTime(); + + var url = apiClient.getUrl('Devices/CameraUploads', { + DeviceId: apiClient.deviceId(), + Name: name, + Album: 'Camera Roll', + Id: getUploadId(file), + api_key: apiClient.accessToken() + }); + + Logger.log('Uploading file to ' + url); + + new MediaBrowser.FileUpload().upload(file, name, url).then(function () { + + Logger.log('File upload succeeded'); + deferred.resolve(); + + }, function () { + + Logger.log('File upload failed'); + deferred.reject(); + }); + }); + + return deferred.promise(); + } + } + + if (!globalScope.MediaBrowser) { + globalScope.MediaBrowser = {}; + } + + globalScope.MediaBrowser.ContentUploader = contentUploader; + +})(this); \ No newline at end of file diff --git a/sync/mediasync.js b/sync/mediasync.js new file mode 100644 index 0000000..10ba774 --- /dev/null +++ b/sync/mediasync.js @@ -0,0 +1,536 @@ +(function (globalScope) { + + function mediaSync() { + + var self = this; + + self.sync = function (apiClient, serverInfo, options) { + + var deferred = DeferredBuilder.Deferred(); + + reportOfflineActions(apiClient, serverInfo).then(function () { + + // Do the first data sync + syncData(apiClient, serverInfo, false).then(function () { + + // Download new content + getNewMedia(apiClient, serverInfo, options).then(function () { + + // Do the second data sync + syncData(apiClient, serverInfo, false).then(function () { + + deferred.resolve(); + + }, getOnFail(deferred)); + + }, getOnFail(deferred)); + + }, getOnFail(deferred)); + + }, getOnFail(deferred)); + + return deferred.promise(); + }; + + function reportOfflineActions(apiClient, serverInfo) { + + Logger.log('Begin reportOfflineActions'); + + var deferred = DeferredBuilder.Deferred(); + + require(['localassetmanager'], function () { + + LocalAssetManager.getOfflineActions(serverInfo.Id).then(function (actions) { + + if (!actions.length) { + deferred.resolve(); + return; + } + + apiClient.reportOfflineActions(actions).then(function () { + + LocalAssetManager.deleteOfflineActions(actions).then(function () { + + deferred.resolve(); + + }, getOnFail(deferred)); + + }, getOnFail(deferred)); + + }, getOnFail(deferred)); + }); + + return deferred.promise(); + } + + function syncData(apiClient, serverInfo, syncUserItemAccess) { + + Logger.log('Begin syncData'); + + var deferred = DeferredBuilder.Deferred(); + + require(['localassetmanager'], function () { + + LocalAssetManager.getServerItemIds(serverInfo.Id).then(function (localIds) { + + var request = { + TargetId: apiClient.deviceId(), + LocalItemIds: localIds, + OfflineUserIds: (serverInfo.Users || []).map(function (u) { return u.Id; }) + }; + + apiClient.syncData(request).then(function (result) { + + afterSyncData(apiClient, serverInfo, syncUserItemAccess, result, deferred); + + }, getOnFail(deferred)); + + }, getOnFail(deferred)); + }); + + return deferred.promise(); + } + + function afterSyncData(apiClient, serverInfo, enableSyncUserItemAccess, syncDataResult, deferred) { + + Logger.log('Begin afterSyncData'); + + removeLocalItems(syncDataResult, serverInfo.Id).then(function (result) { + + if (enableSyncUserItemAccess) { + syncUserItemAccess(syncDataResult, serverInfo.Id).then(function () { + + deferred.resolve(); + + }, getOnFail(deferred)); + } + else { + deferred.resolve(); + } + + }, getOnFail(deferred)); + + deferred.resolve(); + } + + function removeLocalItems(syncDataResult, serverId) { + + Logger.log('Begin removeLocalItems'); + + var deferred = DeferredBuilder.Deferred(); + + removeNextLocalItem(syncDataResult.ItemIdsToRemove, 0, serverId, deferred); + + return deferred.promise(); + } + + function removeNextLocalItem(itemIdsToRemove, index, serverId, deferred) { + + var length = itemIdsToRemove.length; + + if (index >= length) { + + deferred.resolve(); + return; + } + + removeLocalItem(itemIdsToRemove[index], serverId).then(function () { + + removeNextLocalItem(itemIdsToRemove, index + 1, serverId, deferred); + }, function () { + removeNextLocalItem(itemIdsToRemove, index + 1, serverId, deferred); + }); + } + + function removeLocalItem(itemId, serverId) { + + Logger.log('Begin removeLocalItem'); + + var deferred = DeferredBuilder.Deferred(); + + require(['localassetmanager'], function () { + + LocalAssetManager.removeLocalItem(itemId, serverId).then(function (localIds) { + + deferred.resolve(); + + }, getOnFail(deferred)); + }); + + return deferred.promise(); + } + + function getNewMedia(apiClient, serverInfo, options) { + + Logger.log('Begin getNewMedia'); + + var deferred = DeferredBuilder.Deferred(); + + apiClient.getReadySyncItems(apiClient.deviceId()).then(function (jobItems) { + + getNextNewItem(jobItems, 0, apiClient, serverInfo, options, deferred); + + }, getOnFail(deferred)); + + return deferred.promise(); + } + + function getNextNewItem(jobItems, index, apiClient, serverInfo, options, deferred) { + + var length = jobItems.length; + + if (index >= length) { + + deferred.resolve(); + return; + } + + var hasGoneNext = false; + var goNext = function () { + + if (!hasGoneNext) { + hasGoneNext = true; + getNextNewItem(jobItems, index + 1, apiClient, serverInfo, options, deferred); + } + }; + + getNewItem(jobItems[index], apiClient, serverInfo, options).then(goNext, goNext); + } + + function getNewItem(jobItem, apiClient, serverInfo, options) { + + Logger.log('Begin getNewItem'); + + var deferred = DeferredBuilder.Deferred(); + + require(['localassetmanager'], function () { + + var libraryItem = jobItem.Item; + LocalAssetManager.createLocalItem(libraryItem, serverInfo, jobItem.OriginalFileName).then(function (localItem) { + + downloadMedia(apiClient, jobItem, localItem, options).then(function (isQueued) { + + if (isQueued) { + deferred.resolve(); + return; + } + + getImages(apiClient, jobItem, localItem).then(function () { + + getSubtitles(apiClient, jobItem, localItem).then(function () { + + apiClient.reportSyncJobItemTransferred(jobItem.SyncJobItemId).then(function () { + + deferred.resolve(); + + }, getOnFail(deferred)); + + }, getOnFail(deferred)); + + }, getOnFail(deferred)); + + }, getOnFail(deferred)); + + }, getOnFail(deferred)); + }); + + return deferred.promise(); + } + + function downloadMedia(apiClient, jobItem, localItem, options) { + + Logger.log('Begin downloadMedia'); + var deferred = DeferredBuilder.Deferred(); + + require(['localassetmanager'], function () { + + var url = apiClient.getUrl("Sync/JobItems/" + jobItem.SyncJobItemId + "/File", { + api_key: apiClient.accessToken() + }); + + var localPath = localItem.LocalPath; + + Logger.log('Downloading media. Url: ' + url + '. Local path: ' + localPath); + + options = options || {}; + + LocalAssetManager.downloadFile(url, localPath, options.enableBackgroundTransfer, options.enableNewDownloads).then(function (path, isQueued) { + + if (isQueued) { + deferred.resolveWith(null, [true]); + return; + } + LocalAssetManager.addOrUpdateLocalItem(localItem).then(function () { + + deferred.resolveWith(null, [false]); + + }, getOnFail(deferred)); + + }, getOnFail(deferred)); + + }); + + return deferred.promise(); + } + + function getImages(apiClient, jobItem, localItem) { + + Logger.log('Begin getImages'); + var deferred = DeferredBuilder.Deferred(); + + getNextImage(0, apiClient, localItem, deferred); + + return deferred.promise(); + } + + function getNextImage(index, apiClient, localItem, deferred) { + + Logger.log('Begin getNextImage'); + if (index >= 4) { + + deferred.resolve(); + return; + } + + // Just for now while media syncing gets worked out + deferred.resolve(); + return; + + var libraryItem = localItem.Item; + + var serverId = libraryItem.ServerId; + var itemId = null; + var imageTag = null; + var imageType = "Primary"; + + switch (index) { + + case 0: + itemId = libraryItem.Id; + imageType = "Primary"; + imageTag = (libraryItem.ImageTags || {})["Primary"]; + break; + case 1: + itemId = libraryItem.SeriesId; + imageType = "Primary"; + imageTag = libraryItem.SeriesPrimaryImageTag; + break; + case 2: + itemId = libraryItem.SeriesId; + imageType = "Thumb"; + imageTag = libraryItem.SeriesPrimaryImageTag; + break; + case 3: + itemId = libraryItem.AlbumId; + imageType = "Primary"; + imageTag = libraryItem.AlbumPrimaryImageTag; + break; + default: + break; + } + + if (!itemId || !imageTag) { + getNextImage(index + 1, apiClient, localItem, deferred); + return; + } + + downloadImage(apiClient, serverId, itemId, imageTag, imageType).then(function () { + + // For the sake of simplicity, limit to one image + deferred.resolve(); + return; + + getNextImage(index + 1, apiClient, localItem, deferred); + + }, getOnFail(deferred)); + } + + function downloadImage(apiClient, serverId, itemId, imageTag, imageType) { + + Logger.log('Begin downloadImage'); + var deferred = DeferredBuilder.Deferred(); + + require(['localassetmanager'], function () { + + LocalAssetManager.hasImage(serverId, itemId, imageTag).then(function (hasImage) { + + if (hasImage) { + deferred.resolve(); + return; + } + + var imageUrl = apiClient.getImageUrl(itemId, { + tag: imageTag, + type: imageType, + api_key: apiClient.accessToken() + }); + + LocalAssetManager.downloadImage(imageUrl, serverId, itemId, imageTag).then(function () { + + deferred.resolve(); + + }, getOnFail(deferred)); + + }); + }); + + return deferred.promise(); + } + + function getSubtitles(apiClient, jobItem, localItem) { + + Logger.log('Begin getSubtitles'); + var deferred = DeferredBuilder.Deferred(); + + require(['localassetmanager'], function () { + + if (!jobItem.Item.MediaSources.length) { + logger.Error("Cannot download subtitles because video has no media source info."); + deferred.resolve(); + return; + } + + var files = jobItem.AdditionalFiles.filter(function (f) { + return f.Type == 'Subtitles'; + }); + + var mediaSource = jobItem.Item.MediaSources[0]; + + getNextSubtitle(files, 0, apiClient, jobItem, localItem, mediaSource, deferred); + }); + + return deferred.promise(); + } + + function getNextSubtitle(files, index, apiClient, jobItem, localItem, mediaSource, deferred) { + + var length = files.length; + + if (index >= length) { + + deferred.resolve(); + return; + } + + getItemSubtitle(file, apiClient, jobItem, localItem, mediaSource).then(function () { + + getNextSubtitle(files, index + 1, apiClient, jobItem, localItem, mediaSource, deferred); + + }, function () { + getNextSubtitle(files, index + 1, apiClient, jobItem, localItem, mediaSource, deferred); + }); + } + + function getItemSubtitle(file, apiClient, jobItem, localItem, mediaSource) { + + Logger.log('Begin getItemSubtitle'); + var deferred = DeferredBuilder.Deferred(); + + var subtitleStream = mediaSource.MediaStreams.filter(function (m) { + return m.Type == 'Subtitle' && m.Index == file.Index; + })[0]; + + if (!subtitleStream) { + + // We shouldn't get in here, but let's just be safe anyway + Logger.log("Cannot download subtitles because matching stream info wasn't found."); + deferred.reject(); + return; + } + + var url = apiClient.getUrl("Sync/JobItems/" + jobItem.SyncJobItemId + "/AdditionalFiles", { + Name: file.Name, + api_key: apiClient.accessToken() + }); + + require(['localassetmanager'], function () { + + LocalAssetManager.downloadSubtitles(url, localItem, subtitleStream).then(function (subtitlePath) { + + subtitleStream.Path = subtitlePath; + LocalAssetManager.addOrUpdateLocalItem(localItem).then(function () { + deferred.resolve(); + }, getOnFail(deferred)); + + }, getOnFail(deferred)); + }); + + return deferred.promise(); + } + + function syncUserItemAccess(syncDataResult, serverId) { + Logger.log('Begin syncUserItemAccess'); + + var deferred = DeferredBuilder.Deferred(); + + var itemIds = []; + for (var id in syncDataResult.ItemUserAccess) { + itemIds.push(id); + } + + syncNextUserAccessForItem(itemIds, 0, syncDataResult, serverId, deferred); + + return deferred.promise(); + } + + function syncNextUserAccessForItem(itemIds, index, syncDataResult, serverId, deferred) { + + var length = itemIds.length; + + if (index >= length) { + + deferred.resolve(); + return; + } + + syncUserAccessForItem(itemIds[index], syncDataResult).then(function () { + + syncNextUserAccessForItem(itemIds, index + 1, syncDataResult, serverId, deferred); + }, function () { + syncNextUserAccessForItem(itemIds, index + 1, syncDataResult, serverId, deferred); + }); + } + + function syncUserAccessForItem(itemId, syncDataResult) { + Logger.log('Begin syncUserAccessForItem'); + + var deferred = DeferredBuilder.Deferred(); + + require(['localassetmanager'], function () { + + LocalAssetManager.getUserIdsWithAccess(itemId, serverId).then(function (savedUserIdsWithAccess) { + + var userIdsWithAccess = syncDataResult.ItemUserAccess[itemId]; + + if (userIdsWithAccess.join(',') == savedUserIdsWithAccess.join(',')) { + // Hasn't changed, nothing to do + deferred.resolve(); + } + else { + + LocalAssetManager.saveUserIdsWithAccess(itemId, serverId, userIdsWithAccess).then(function () { + deferred.resolve(); + }, getOnFail(deferred)); + } + + }, getOnFail(deferred)); + }); + + return deferred.promise(); + } + + function getOnFail(deferred) { + return function () { + + deferred.reject(); + }; + } + } + + if (!globalScope.MediaBrowser) { + globalScope.MediaBrowser = {}; + } + + globalScope.MediaBrowser.MediaSync = mediaSync; + +})(this); \ No newline at end of file diff --git a/sync/multiserversync.js b/sync/multiserversync.js new file mode 100644 index 0000000..4521ce8 --- /dev/null +++ b/sync/multiserversync.js @@ -0,0 +1,52 @@ +(function (globalScope) { + + function multiServerSync(connectionManager) { + + var self = this; + + self.sync = function (options) { + + var deferred = DeferredBuilder.Deferred(); + + var servers = connectionManager.getSavedServers(); + + syncNext(servers, 0, options, deferred); + + return deferred.promise(); + }; + + function syncNext(servers, index, options, deferred) { + + var length = servers.length; + + if (index >= length) { + + deferred.resolve(); + return; + } + + var server = servers[index]; + + Logger.log("Creating ServerSync to server: " + server.Id); + + require(['serversync'], function () { + + new MediaBrowser.ServerSync(connectionManager).sync(server, options).then(function () { + + syncNext(servers, index + 1, options, deferred); + + }, function () { + + syncNext(servers, index + 1, options, deferred); + }); + }); + } + } + + if (!globalScope.MediaBrowser) { + globalScope.MediaBrowser = {}; + } + + globalScope.MediaBrowser.MultiServerSync = multiServerSync; + +})(this); \ No newline at end of file diff --git a/sync/offlineusersync.js b/sync/offlineusersync.js new file mode 100644 index 0000000..2ae3b81 --- /dev/null +++ b/sync/offlineusersync.js @@ -0,0 +1,75 @@ +(function (globalScope) { + + function offlineUserSync() { + + var self = this; + + self.sync = function (apiClient, server) { + + var deferred = DeferredBuilder.Deferred(); + + var users = server.Users || []; + syncNext(users, 0, deferred, apiClient, server); + + return deferred.promise(); + }; + + function syncNext(users, index, deferred, apiClient, server) { + + var length = users.length; + + if (index >= length) { + + deferred.resolve(); + return; + } + + syncUser(users[index], apiClient).then(function () { + + syncNext(users, index + 1, deferred, apiClient, server); + }, function () { + syncNext(users, index + 1, deferred, apiClient, server); + }); + } + + function syncUser(user, apiClient) { + + var deferred = DeferredBuilder.Deferred(); + + apiClient.getOfflineUser(user.Id).then(function (result) { + + require(['localassetmanager'], function () { + + LocalAssetManager.saveOfflineUser(result).then(function () { + deferred.resolve(); + }, function () { + deferred.resolve(); + }); + }); + + }, function () { + + // TODO: We should only delete if there's a 401 response + + require(['localassetmanager'], function () { + + LocalAssetManager.deleteOfflineUser(user.Id).then(function () { + deferred.resolve(); + }, function () { + deferred.resolve(); + }); + }); + }); + + return deferred.promise(); + } + + } + + if (!globalScope.MediaBrowser) { + globalScope.MediaBrowser = {}; + } + + globalScope.MediaBrowser.OfflineUserSync = offlineUserSync; + +})(this); \ No newline at end of file diff --git a/sync/serversync.js b/sync/serversync.js new file mode 100644 index 0000000..ae4234a --- /dev/null +++ b/sync/serversync.js @@ -0,0 +1,134 @@ +(function (globalScope) { + + function serverSync(connectionManager) { + + var self = this; + + self.sync = function (server, options) { + + var deferred = DeferredBuilder.Deferred(); + + if (!server.AccessToken && !server.ExchangeToken) { + + Logger.log('Skipping sync to server ' + server.Id + ' because there is no saved authentication information.'); + deferred.resolve(); + return deferred.promise(); + } + + var connectionOptions = { + updateDateLastAccessed: false, + enableWebSocket: false, + reportCapabilities: false + }; + + connectionManager.connectToServer(server, connectionOptions).then(function (result) { + + if (result.State == MediaBrowser.ConnectionState.SignedIn) { + performSync(server, options, deferred); + } else { + Logger.log('Unable to connect to server id: ' + server.Id); + deferred.reject(); + } + + }, function () { + + Logger.log('Unable to connect to server id: ' + server.Id); + deferred.reject(); + }); + + return deferred.promise(); + }; + + function performSync(server, options, deferred) { + + Logger.log("Creating ContentUploader to server: " + server.Id); + + var nextAction = function () { + syncOfflineUsers(server, options, deferred); + }; + + options = options || {}; + + var uploadPhotos = options.uploadPhotos !== false; + + if (options.cameraUploadServers && options.cameraUploadServers.indexOf(server.Id) == -1) { + uploadPhotos = false; + } + + if (!uploadPhotos) { + nextAction(); + return; + } + + require(['contentuploader'], function () { + + new MediaBrowser.ContentUploader(connectionManager).uploadImages(server).then(function () { + + Logger.log("ContentUploaded succeeded to server: " + server.Id); + + nextAction(); + + }, function () { + + Logger.log("ContentUploaded failed to server: " + server.Id); + + nextAction(); + }); + }); + } + + function syncOfflineUsers(server, options, deferred) { + + if (options.syncOfflineUsers === false) { + syncMedia(server, options, deferred); + return; + } + + require(['offlineusersync'], function () { + + var apiClient = connectionManager.getApiClient(server.Id); + + new MediaBrowser.OfflineUserSync().sync(apiClient, server).then(function () { + + Logger.log("OfflineUserSync succeeded to server: " + server.Id); + + syncMedia(server, options, deferred); + + }, function () { + + Logger.log("OfflineUserSync failed to server: " + server.Id); + + deferred.reject(); + }); + }); + } + + function syncMedia(server, options, deferred) { + + require(['mediasync'], function () { + + var apiClient = connectionManager.getApiClient(server.Id); + + new MediaBrowser.MediaSync().sync(apiClient, server, options).then(function () { + + Logger.log("MediaSync succeeded to server: " + server.Id); + + deferred.resolve(); + + }, function () { + + Logger.log("MediaSync failed to server: " + server.Id); + + deferred.reject(); + }); + }); + } + } + + if (!globalScope.MediaBrowser) { + globalScope.MediaBrowser = {}; + } + + globalScope.MediaBrowser.ServerSync = serverSync; + +})(this); \ No newline at end of file diff --git a/wakeonlan.js b/wakeonlan.js new file mode 100644 index 0000000..5f6dc19 --- /dev/null +++ b/wakeonlan.js @@ -0,0 +1,15 @@ +define([], function () { + + function send(info) { + + return new Promise(function (resolve, reject) { + + resolve(); + }); + } + + return { + send: send + }; + +}); \ No newline at end of file