diff options
Diffstat (limited to 'MediaBrowser.WebDashboard')
10 files changed, 1502 insertions, 97 deletions
diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 86e4c10ea7..4c861c61bd 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Common.Extensions; +using System.Reflection; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Common.ScheduledTasks; using MediaBrowser.Controller; @@ -391,7 +392,6 @@ namespace MediaBrowser.WebDashboard.Api { "http://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js", "http://code.jquery.com/mobile/1.3.0/jquery.mobile-1.3.0.min.js", - "../jsapiclient.js" + versionString, "scripts/all.js" + versionString }; @@ -447,20 +447,27 @@ namespace MediaBrowser.WebDashboard.Api var newLineBytes = Encoding.UTF8.GetBytes(Environment.NewLine); + await AppendResource(assembly, memoryStream, "MediaBrowser.WebDashboard.ApiClient.js", newLineBytes).ConfigureAwait(false); + foreach (var file in scriptFiles) { - using (var stream = assembly.GetManifestResourceStream(resourcePrefix + file)) - { - await stream.CopyToAsync(memoryStream).ConfigureAwait(false); - - await memoryStream.WriteAsync(newLineBytes, 0, newLineBytes.Length).ConfigureAwait(false); - } + await AppendResource(assembly, memoryStream, resourcePrefix + file, newLineBytes).ConfigureAwait(false); } memoryStream.Position = 0; return memoryStream; } + private async Task AppendResource(Assembly assembly, Stream outputStream, string path, byte[] newLineBytes) + { + using (var stream = assembly.GetManifestResourceStream(path)) + { + await stream.CopyToAsync(outputStream).ConfigureAwait(false); + + await outputStream.WriteAsync(newLineBytes, 0, newLineBytes.Length).ConfigureAwait(false); + } + } + } } diff --git a/MediaBrowser.WebDashboard/ApiClient.js b/MediaBrowser.WebDashboard/ApiClient.js new file mode 100644 index 0000000000..31022de382 --- /dev/null +++ b/MediaBrowser.WebDashboard/ApiClient.js @@ -0,0 +1,1387 @@ +/** + * Represents a javascript version of ApiClient. + * This should be kept up to date with all possible api methods and parameters + */ +var ApiClient = { + + serverProtocol: "http", + + /** + * Gets or sets the host name of the server + */ + serverHostName: "localhost", + + serverPortNumber: 8096, + + /** + * Detects the hostname and port of MB server based on the current url + */ + inferServerFromUrl: function () { + + var loc = window.location; + + ApiClient.serverProtocol = loc.protocol; + ApiClient.serverHostName = loc.hostname; + ApiClient.serverPortNumber = loc.port; + }, + + /** + * Creates an api url based on a handler name and query string parameters + * @param {String} name + * @param {Object} params + */ + getUrl: function (name, params) { + + if (!name) { + throw new Error("Url name cannot be empty"); + } + + params = params || {}; + + var url = ApiClient.serverProtocol + "//" + ApiClient.serverHostName + ":" + ApiClient.serverPortNumber + "/mediabrowser/" + name; + + if (params) { + url += "?" + $.param(params); + + } + return url; + }, + + /** + * Returns the name of the current browser + */ + getDeviceName: function () { + + /*if ($.browser.chrome) { + return "Chrome"; + } + if ($.browser.safari) { + return "Safari"; + } + if ($.browser.webkit) { + return "WebKit"; + } + if ($.browser.msie) { + return "Internet Explorer"; + } + if ($.browser.firefox) { + return "Firefox"; + } + if ($.browser.mozilla) { + return "Firefox"; + } + if ($.browser.opera) { + return "Opera"; + }*/ + + return "Web Browser"; + }, + + /** + * Creates a custom api url based on a handler name and query string parameters + * @param {String} name + * @param {Object} params + */ + getCustomUrl: function (name, params) { + + if (!name) { + throw new Error("Url name cannot be empty"); + } + + params = params || {}; + params.client = "Dashboard"; + params.device = ApiClient.getDeviceName(); + params.format = "json"; + + var url = ApiClient.serverProtocol + "//" + ApiClient.serverHostName + ":" + ApiClient.serverPortNumber + "/mediabrowser/" + name; + + if (params) { + url += "?" + $.param(params); + + } + return url; + }, + + /** + * Gets an item from the server + * Omit itemId to get the root folder. + */ + getItem: function (userId, itemId) { + + if (!userId) { + throw new Error("null userId"); + } + + var url = ApiClient.getUrl("Users/" + userId + "/Items/" + itemId); + + return $.getJSON(url); + }, + + /** + * Gets the root folder from the server + */ + getRootFolder: function (userId) { + + return ApiClient.getItem(userId); + }, + + /** + * Gets the current server status + */ + getSystemInfo: function () { + + var url = ApiClient.getUrl("System/Info"); + + return $.getJSON(url); + }, + + /** + * Gets all cultures known to the server + */ + getCultures: function () { + + var url = ApiClient.getUrl("Localization/cultures"); + + return $.getJSON(url); + }, + + /** + * Gets all countries known to the server + */ + getCountries: function () { + + var url = ApiClient.getUrl("Localization/countries"); + + return $.getJSON(url); + }, + + /** + * Gets plugin security info + */ + getPluginSecurityInfo: function () { + + var url = ApiClient.getUrl("Plugins/SecurityInfo"); + + return $.getJSON(url); + }, + + /** + * Gets the directory contents of a path on the server + */ + getDirectoryContents: function (path, options) { + + if (!path) { + throw new Error("null path"); + } + + options = options || {}; + + options.path = path; + + var url = ApiClient.getUrl("Environment/DirectoryContents", options); + + return $.getJSON(url); + }, + + /** + * Gets a list of physical drives from the server + */ + getDrives: function () { + + var url = ApiClient.getUrl("Environment/Drives"); + + return $.getJSON(url); + }, + + /** + * Gets a list of network devices from the server + */ + getNetworkDevices: function () { + + var url = ApiClient.getUrl("Environment/NetworkDevices"); + + return $.getJSON(url); + }, + + /** + * Cancels a package installation + */ + cancelPackageInstallation: function (installationId) { + + if (!installationId) { + throw new Error("null installationId"); + } + + var url = ApiClient.getUrl("Packages/Installing/" + id); + + return $.ajax({ + type: "DELETE", + url: url, + dataType: "json" + }); + }, + + /** + * Installs or updates a new plugin + */ + installPlugin: function (name, updateClass, version) { + + if (!name) { + throw new Error("null name"); + } + + if (!updateClass) { + throw new Error("null updateClass"); + } + + var options = { + updateClass: updateClass + }; + + if (version) { + options.version = version; + } + + var url = ApiClient.getUrl("Packages/Installed/" + name, options); + + return $.post(url); + }, + + /** + * Instructs the server to perform a pending kernel reload or app restart. + * If a restart is not currently required, nothing will happen. + */ + performPendingRestart: function () { + + var url = ApiClient.getUrl("System/Restart"); + + return $.post(url); + }, + + /** + * Gets information about an installable package + */ + getPackageInfo: function (name) { + + if (!name) { + throw new Error("null name"); + } + + var url = ApiClient.getUrl("Packages/" + name); + + return $.getJSON(url); + }, + + /** + * Gets the latest available application update (if any) + */ + getAvailableApplicationUpdate: function () { + + var url = ApiClient.getUrl("Packages/Updates", { PackageType: "System" }); + + return $.getJSON(url); + }, + + /** + * Gets the latest available plugin updates (if any) + */ + getAvailablePluginUpdates: function () { + + var url = ApiClient.getUrl("Packages/Updates", { PackageType: "UserInstalled" }); + + return $.getJSON(url); + }, + + /** + * Gets the virtual folder for a view. Specify a userId to get a user view, or omit for the default view. + */ + getVirtualFolders: function (userId) { + + var url = userId ? "Users/" + userId + "/VirtualFolders" : "Library/VirtualFolders"; + + url = ApiClient.getUrl(url); + + return $.getJSON(url); + }, + + /** + * Gets all the paths of the locations in the physical root. + */ + getPhysicalPaths: function () { + + var url = ApiClient.getUrl("Library/PhysicalPaths"); + + return $.getJSON(url); + }, + + /** + * Gets the current server configuration + */ + getServerConfiguration: function () { + + var url = ApiClient.getUrl("System/Configuration"); + + return $.getJSON(url); + }, + + /** + * Gets the server's scheduled tasks + */ + getScheduledTasks: function () { + + var url = ApiClient.getUrl("ScheduledTasks"); + + return $.getJSON(url); + }, + + /** + * Starts a scheduled task + */ + startScheduledTask: function (id) { + + if (!id) { + throw new Error("null id"); + } + + var url = ApiClient.getUrl("ScheduledTasks/Running/" + id); + + return $.post(url); + }, + + /** + * Gets a scheduled task + */ + getScheduledTask: function (id) { + + if (!id) { + throw new Error("null id"); + } + + var url = ApiClient.getUrl("ScheduledTasks/" + id); + + return $.getJSON(url); + }, + + /** + * Stops a scheduled task + */ + stopScheduledTask: function (id) { + + if (!id) { + throw new Error("null id"); + } + + var url = ApiClient.getUrl("ScheduledTasks/Running/" + id); + + return $.ajax({ + type: "DELETE", + url: url, + dataType: "json" + }); + }, + + /** + * Gets the configuration of a plugin + * @param {String} Id + */ + getPluginConfiguration: function (id) { + + if (!id) { + throw new Error("null Id"); + } + + var url = ApiClient.getUrl("Plugins/" + id + "/Configuration"); + + return $.getJSON(url); + }, + + /** + * Gets a list of plugins that are available to be installed + */ + getAvailablePlugins: function () { + + var url = ApiClient.getUrl("Packages", { PackageType: "UserInstalled" }); + + return $.getJSON(url); + }, + + /** + * Uninstalls a plugin + * @param {String} Id + */ + uninstallPlugin: function (id) { + + if (!id) { + throw new Error("null Id"); + } + + var url = ApiClient.getUrl("Plugins/" + id); + + return $.ajax({ + type: "DELETE", + url: url, + dataType: "json" + }); + }, + + /** + * Removes a virtual folder from either the default view or a user view + * @param {String} name + */ + removeVirtualFolder: function (name, userId) { + + if (!name) { + throw new Error("null name"); + } + + var url = userId ? "Users/" + userId + "/VirtualFolders" : "Library/VirtualFolders"; + + url += "/" + name; + url = ApiClient.getUrl(url); + + return $.ajax({ + type: "DELETE", + url: url, + dataType: "json" + }); + }, + + /** + * Adds a virtual folder to either the default view or a user view + * @param {String} name + */ + addVirtualFolder: function (name, userId) { + + if (!name) { + throw new Error("null name"); + } + + var url = userId ? "Users/" + userId + "/VirtualFolders" : "Library/VirtualFolders"; + + url += "/" + name; + url = ApiClient.getUrl(url); + + return $.post(url); + }, + + /** + * Renames a virtual folder, within either the default view or a user view + * @param {String} name + */ + renameVirtualFolder: function (name, newName, userId) { + + if (!name) { + throw new Error("null name"); + } + + var url = userId ? "Users/" + userId + "/VirtualFolders" : "Library/VirtualFolders"; + + url += "/" + name + "/Name"; + + url = ApiClient.getUrl(url, { newName: newName }); + + return $.post(url); + }, + + /** + * Adds an additional mediaPath to an existing virtual folder, within either the default view or a user view + * @param {String} name + */ + addMediaPath: function (virtualFolderName, mediaPath, userId) { + + if (!virtualFolderName) { + throw new Error("null virtualFolderName"); + } + + if (!mediaPath) { + throw new Error("null mediaPath"); + } + + var url = userId ? "Users/" + userId + "/VirtualFolders" : "Library/VirtualFolders"; + + url += "/" + virtualFolderName + "/Paths"; + + url = ApiClient.getUrl(url, { path: mediaPath }); + + return $.post(url); + }, + + /** + * Removes a media path from a virtual folder, within either the default view or a user view + * @param {String} name + */ + removeMediaPath: function (virtualFolderName, mediaPath, userId) { + + if (!virtualFolderName) { + throw new Error("null virtualFolderName"); + } + + if (!mediaPath) { + throw new Error("null mediaPath"); + } + + var url = userId ? "Users/" + userId + "/VirtualFolders" : "Library/VirtualFolders"; + + url += "/" + virtualFolderName + "/Paths"; + + url = ApiClient.getUrl(url, { path: mediaPath }); + + return $.ajax({ + type: "DELETE", + url: url, + dataType: "json" + }); + }, + + /** + * Deletes a user + * @param {String} id + */ + deleteUser: function (id) { + + if (!id) { + throw new Error("null id"); + } + + var url = ApiClient.getUrl("Users/" + id); + + return $.ajax({ + type: "DELETE", + url: url, + dataType: "json" + }); + }, + + /** + * Deletes a user image + * @param {String} userId + * @param {String} imageType The type of image to delete, based on the server-side ImageType enum. + */ + deleteUserImage: function (userId, imageType) { + + if (!userId) { + throw new Error("null userId"); + } + + if (!imageType) { + throw new Error("null imageType"); + } + + var url = ApiClient.getUrl("Users/" + userId + "/Images/" + imageType); + + return $.ajax({ + type: "DELETE", + url: url, + dataType: "json" + }); + }, + + /** + * 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 + */ + uploadUserImage: function (userId, imageType, file) { + + if (!userId) { + throw new Error("null userId"); + } + + if (!imageType) { + throw new Error("null imageType"); + } + + if (!file || !file.type.match('image.*')) { + throw new Error("File must be an image."); + } + + var deferred = $.Deferred(); + + var reader = new FileReader(); + + reader.onerror = function () { + deferred.reject(); + }; + + reader.onabort = function () { + deferred.reject(); + }; + + // Closure to capture the file information. + reader.onload = function (e) { + + var data = window.btoa(e.target.result); + + var url = ApiClient.getUrl("Users/" + userId + "/Images/" + imageType); + + $.ajax({ + type: "POST", + url: url, + data: data, + contentType: "image/" + file.name.substring(file.name.lastIndexOf('.') + 1) + + }).done(function (result) { + + deferred.resolveWith(null, [result]); + + }).fail(function () { + deferred.reject(); + }); + }; + + // Read in the image file as a data URL. + reader.readAsBinaryString(file); + + return deferred.promise(); + }, + + /** + * Gets the list of installed plugins on the server + */ + getInstalledPlugins: function () { + + var url = ApiClient.getUrl("Plugins"); + + return $.getJSON(url); + }, + + /** + * Gets a user by id + * @param {String} id + */ + getUser: function (id) { + + if (!id) { + throw new Error("Must supply a userId"); + } + + var url = ApiClient.getUrl("Users/" + id); + + return $.getJSON(url); + }, + + /** + * Gets a studio + */ + getStudio: function (name) { + + if (!name) { + throw new Error("null name"); + } + + var url = ApiClient.getUrl("Studios/" + name); + + return $.getJSON(url); + }, + + /** + * Gets a genre + */ + getGenre: function (name) { + + if (!name) { + throw new Error("null name"); + } + + var url = ApiClient.getUrl("Genres/" + name); + + return $.getJSON(url); + }, + + /** + * Gets a year + */ + getYear: function (year) { + + if (!year) { + throw new Error("null year"); + } + + var url = ApiClient.getUrl("Years/" + year); + + return $.getJSON(url); + }, + + /** + * Gets a Person + */ + getPerson: function (name) { + + if (!name) { + throw new Error("null name"); + } + + var url = ApiClient.getUrl("Persons/" + name); + + return $.getJSON(url); + }, + + /** + * Gets weather info + * @param {String} location - us zip code / city, state, country / city, country + * Omit location to get weather info using stored server configuration value + */ + getWeatherInfo: function (location) { + + var url = ApiClient.getUrl("weather", { + location: location + }); + + return $.getJSON(url); + }, + + /** + * Gets all users from the server + */ + getAllUsers: function () { + + var url = ApiClient.getUrl("users"); + + return $.getJSON(url); + }, + + /** + * Gets all available parental ratings from the server + */ + getParentalRatings: function () { + + var url = ApiClient.getUrl("Localization/ParentalRatings"); + + return $.getJSON(url); + }, + + /** + * Gets a list of all available conrete BaseItem types from the server + */ + getItemTypes: function (options) { + + var url = ApiClient.getUrl("Library/ItemTypes", options); + + return $.getJSON(url); + }, + + /** + * 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. + */ + 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; + } + + // Don't put these on the query string + delete options.type; + delete options.index; + + return ApiClient.getUrl(url, options); + }, + + /** + * Constructs a url for a person image + * @param {String} name + * @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. + */ + getPersonImageUrl: function (name, options) { + + if (!name) { + throw new Error("null name"); + } + + options = options || { + }; + + var url = "Persons/" + name + "/Images/" + options.type; + + if (options.index != null) { + url += "/" + options.index; + } + + // Don't put these on the query string + delete options.type; + delete options.index; + + return ApiClient.getUrl(url, options); + }, + + /** + * Constructs a url for a year image + * @param {String} year + * @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. + */ + getYearImageUrl: function (year, options) { + + if (!year) { + throw new Error("null year"); + } + + options = options || { + }; + + var url = "Years/" + year + "/Images/" + options.type; + + if (options.index != null) { + url += "/" + options.index; + } + + // Don't put these on the query string + delete options.type; + delete options.index; + + return ApiClient.getUrl(url, options); + }, + + /** + * Constructs a url for a genre image + * @param {String} name + * @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. + */ + getGenreImageUrl: function (name, options) { + + if (!name) { + throw new Error("null name"); + } + + options = options || { + }; + + var url = "Genres/" + name + "/Images/" + options.type; + + if (options.index != null) { + url += "/" + options.index; + } + + // Don't put these on the query string + delete options.type; + delete options.index; + + return ApiClient.getUrl(url, options); + }, + + /** + * Constructs a url for a genre image + * @param {String} name + * @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. + */ + getStudioImageUrl: function (name, options) { + + if (!name) { + throw new Error("null name"); + } + + options = options || { + }; + + var url = "Studios/" + name + "/Images/" + options.type; + + if (options.index != null) { + url += "/" + options.index; + } + + // Don't put these on the query string + delete options.type; + delete options.index; + + return ApiClient.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. + */ + 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; + } + + // Don't put these on the query string + delete options.type; + delete options.index; + + return ApiClient.getUrl(url, options); + }, + + /** + * Constructs a url for an item logo image + * If the item doesn't have a logo, it will inherit a logo from a parent + * @param {Object} item A BaseItem + * @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. + */ + getLogoImageUrl: function (item, options) { + + if (!item) { + throw new Error("null item"); + } + + options = options || { + }; + + options.imageType = "logo"; + + var logoItemId = item.HasLogo ? item.Id : item.ParentLogoItemId; + + return logoItemId ? ApiClient.getImageUrl(logoItemId, options) : null; + }, + + /** + * Constructs an array of backdrop image url's for an item + * If the item doesn't have any backdrops, it will inherit them from a parent + * @param {Object} item A BaseItem + * @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. + */ + getBackdropImageUrl: function (item, options) { + + if (!item) { + throw new Error("null item"); + } + + options = options || { + }; + + options.imageType = "backdrop"; + + var backdropItemId; + var backdropCount; + + if (!item.BackdropCount) { + backdropItemId = item.ParentBackdropItemId; + backdropCount = item.ParentBackdropCount || 0; + } else { + backdropItemId = item.Id; + backdropCount = item.BackdropCount; + } + + if (!backdropItemId) { + return []; + } + + var files = []; + + for (var i = 0; i < backdropCount; i++) { + + options.imageIndex = i; + + files[i] = ApiClient.getImageUrl(backdropItemId, options); + } + + return files; + }, + + /** + * Authenticates a user + * @param {String} userId + * @param {String} password + */ + authenticateUser: function (userId, password) { + + if (!userId) { + throw new Error("null userId"); + } + + var url = ApiClient.getUrl("Users/" + userId + "/authenticate"); + + var postData = { + }; + + if (password) { + postData.password = password; + } + + return $.ajax({ + type: "POST", + url: url, + data: JSON.stringify(postData), + dataType: "json", + contentType: "application/json" + }); + }, + + /** + * Updates a user's password + * @param {String} userId + * @param {String} currentPassword + * @param {String} newPassword + */ + updateUserPassword: function (userId, currentPassword, newPassword) { + + if (!userId) { + throw new Error("null userId"); + } + + var url = ApiClient.getUrl("Users/" + userId + "/Password"); + + var postData = { + }; + + if (currentPassword) { + postData.currentPassword = currentPassword; + } + if (newPassword) { + postData.newPassword = newPassword; + } + return $.post(url, postData); + }, + + /** + * Resets a user's password + * @param {String} userId + */ + resetUserPassword: function (userId) { + + if (!userId) { + throw new Error("null userId"); + } + + var url = ApiClient.getUrl("Users/" + userId + "/Password"); + + var postData = { + }; + + postData.resetPassword = 1; + return $.post(url, postData); + }, + + /** + * Updates the server's configuration + * @param {Object} configuration + */ + updateServerConfiguration: function (configuration) { + + if (!configuration) { + throw new Error("null configuration"); + } + + var url = ApiClient.getUrl("System/Configuration"); + + return $.ajax({ + type: "POST", + url: url, + data: JSON.stringify(configuration), + dataType: "json", + contentType: "application/json" + }); + }, + + /** + * Updates plugin security info + */ + updatePluginSecurityInfo: function (info) { + + var url = ApiClient.getUrl("Plugins/SecurityInfo"); + + return $.ajax({ + type: "POST", + url: url, + data: JSON.stringify(info), + dataType: "json", + contentType: "application/json" + }); + }, + + /** + * Creates a user + * @param {Object} user + */ + createUser: function (user) { + + if (!user) { + throw new Error("null user"); + } + + var url = ApiClient.getUrl("Users"); + + return $.ajax({ + type: "POST", + url: url, + data: JSON.stringify(user), + dataType: "json", + contentType: "application/json" + }); + }, + + /** + * Updates a user + * @param {Object} user + */ + updateUser: function (user) { + + if (!user) { + throw new Error("null user"); + } + + var url = ApiClient.getUrl("Users/" + user.Id); + + return $.ajax({ + type: "POST", + url: url, + data: JSON.stringify(user), + dataType: "json", + contentType: "application/json" + }); + }, + + /** + * Updates the Triggers for a ScheduledTask + * @param {String} id + * @param {Object} triggers + */ + updateScheduledTaskTriggers: function (id, triggers) { + + if (!id) { + throw new Error("null id"); + } + + if (!triggers) { + throw new Error("null triggers"); + } + + var url = ApiClient.getUrl("ScheduledTasks/" + id + "/Triggers"); + + return $.ajax({ + type: "POST", + url: url, + data: JSON.stringify(triggers), + dataType: "json", + contentType: "application/json" + }); + }, + + /** + * Updates a plugin's configuration + * @param {String} Id + * @param {Object} configuration + */ + updatePluginConfiguration: function (id, configuration) { + + if (!id) { + throw new Error("null Id"); + } + + if (!configuration) { + throw new Error("null configuration"); + } + + var url = ApiClient.getUrl("Plugins/" + id + "/Configuration"); + + return $.ajax({ + type: "POST", + url: url, + data: JSON.stringify(configuration), + dataType: "json", + contentType: "application/json" + }); + }, + + /** + * Gets items based on a query, typicall 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 + */ + getItems: function (userId, options) { + + if (!userId) { + throw new Error("null userId"); + } + + return $.getJSON(ApiClient.getUrl("Users/" + userId + "/Items", options)); + }, + + /** + * Marks an item as played or unplayed + * This should not be used to update playstate following playback. + * There are separate playstate check-in methods for that. This should be used for a + * separate option to reset playstate. + * @param {String} userId + * @param {String} itemId + * @param {Boolean} wasPlayed + */ + updatePlayedStatus: function (userId, itemId, wasPlayed) { + + if (!userId) { + throw new Error("null userId"); + } + + if (!itemId) { + throw new Error("null itemId"); + } + + var url = "Users/" + userId + "/PlayedItems/" + itemId; + + var method = wasPlayed ? "POST" : "DELETE"; + + return $.ajax({ + type: method, + url: url, + dataType: "json" + }); + }, + + /** + * Updates a user's favorite status for an item and returns the updated UserItemData object. + * @param {String} userId + * @param {String} itemId + * @param {Boolean} isFavorite + */ + updateFavoriteStatus: function (userId, itemId, isFavorite) { + + if (!userId) { + throw new Error("null userId"); + } + + if (!itemId) { + throw new Error("null itemId"); + } + + var url = "Users/" + userId + "/FavoriteItems/" + itemId; + + var method = isFavorite ? "POST" : "DELETE"; + + return $.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 + */ + updateUserItemRating: function (userId, itemId, likes) { + + if (!userId) { + throw new Error("null userId"); + } + + if (!itemId) { + throw new Error("null itemId"); + } + + var url = ApiClient.getUrl("Users/" + userId + "/Items/" + itemId + "/Rating", { + likes: likes + }); + + return $.post(url); + }, + + /** + * Clears a user's personal rating for an item + * @param {String} userId + * @param {String} itemId + */ + clearUserItemRating: function (userId, itemId) { + + if (!userId) { + throw new Error("null userId"); + } + + if (!itemId) { + throw new Error("null itemId"); + } + + var url = ApiClient.getUrl("Users/" + userId + "/Items/" + itemId + "/Rating"); + + return $.ajax({ + type: "DELETE", + url: url, + dataType: "json" + }); + } +}; + +// Do this initially. The consumer can always override later +ApiClient.inferServerFromUrl(); diff --git a/MediaBrowser.WebDashboard/Html/advancedMetadata.html b/MediaBrowser.WebDashboard/Html/advancedMetadata.html index 60e02ba3c0..6ea3d7e74b 100644 --- a/MediaBrowser.WebDashboard/Html/advancedMetadata.html +++ b/MediaBrowser.WebDashboard/Html/advancedMetadata.html @@ -21,13 +21,10 @@ <label>Disable internet providers for:</label> <div id="divItemTypes"></div> </li> - <li> - <button type="submit" data-theme="b"> + <li style="display: none;"> + <button class="btnSubmit" type="submit" data-theme="b"> Save </button> - <button type="button" onclick="Dashboard.navigate('dashboard.html');"> - Cancel - </button> </li> </ul> </form> diff --git a/MediaBrowser.WebDashboard/Html/metadata.html b/MediaBrowser.WebDashboard/Html/metadata.html index 4618dd4bd8..28fd7dedd0 100644 --- a/MediaBrowser.WebDashboard/Html/metadata.html +++ b/MediaBrowser.WebDashboard/Html/metadata.html @@ -18,32 +18,29 @@ <form id="metadataConfigurationForm"> <ul data-role="listview" class="ulForm"> <li> - <input type="checkbox" id="chkEnableInternetProviders" name="chkEnableInternetProviders" /> + <input type="checkbox" id="chkEnableInternetProviders" name="chkEnableInternetProviders" onchange="MetadataConfigurationPage.submit();" /> <label for="chkEnableInternetProviders">Download metadata from the internet </label> </li> <li> - <input type="checkbox" id="chkSaveLocal" name="chkSaveLocal" /> + <input type="checkbox" id="chkSaveLocal" name="chkSaveLocal onchange="MetadataConfigurationPage.submit();" /> <label for="chkSaveLocal">Save metadata within media folders </label> </li> <li> <label for="txtRefreshDays">Metadata refresh period (days): </label> - <input type="number" id="txtRefreshDays" name="txtRefreshDays" pattern="[0-9]*" required="required" min="1" /> + <input type="number" id="txtRefreshDays" name="txtRefreshDays" pattern="[0-9]*" required="required" min="1" onchange="MetadataConfigurationPage.submit();" /> </li> <li> <label for="selectLanguage">Preferred language: </label> - <select name="selectLanguage" id="selectLanguage"></select> + <select name="selectLanguage" id="selectLanguage" onchange="MetadataConfigurationPage.submit();"></select> </li> <li> <label for="selectCountry">Country: </label> - <select name="selectCountry" id="selectCountry"></select> + <select name="selectCountry" id="selectCountry" onchange="MetadataConfigurationPage.submit();"></select> </li> - <li> - <button type="submit" data-theme="b"> + <li style="display: none;"> + <button class="btnSubmit" type="submit" data-theme="b"> Save </button> - <button type="button" onclick="Dashboard.navigate('dashboard.html');"> - Cancel - </button> </li> </ul> </form> diff --git a/MediaBrowser.WebDashboard/Html/metadataImages.html b/MediaBrowser.WebDashboard/Html/metadataImages.html index b06a3205b7..0197af43da 100644 --- a/MediaBrowser.WebDashboard/Html/metadataImages.html +++ b/MediaBrowser.WebDashboard/Html/metadataImages.html @@ -18,7 +18,7 @@ <form id="metadataImagesConfigurationForm"> <ul data-role="listview" class="ulForm"> <li> - <input type="checkbox" id="chkRefreshItemImages" name="chkRefreshItemImages" /> + <input type="checkbox" id="chkRefreshItemImages" name="chkRefreshItemImages" onchange="MetadataImagesPage.submit();" /> <label for="chkRefreshItemImages">Refresh existing images </label> <div class="fieldDescription"> When enabled, images will be refreshed periodically @@ -26,100 +26,100 @@ </li> <li> <label for="txtNumbackdrops">Max number of backdrops per item: </label> - <input type="number" id="txtNumbackdrops" name="txtNumbackdrops" pattern="[0-9]*" required="required" min="1" /> + <input type="number" id="txtNumbackdrops" name="txtNumbackdrops" pattern="[0-9]*" required="required" min="1" onchange="MetadataImagesPage.submit();" /> </li> <li> <label>Enable additional image downloading:</label> - - <div data-role="collapsible"> - <h3>Movies</h3> - <div data-role="controlgroup"> - <input type="checkbox" data-mini="true" id="chkDownloadMovieArt" name="chkDownloadMovieArt" /> - <label for="chkDownloadMovieArt">Movie Art</label> - <input type="checkbox" data-mini="true" id="chkDownloadMovieBanner" name="chkDownloadMovieBanner" /> - <label for="chkDownloadMovieBanner">Movie Banner</label> + <div data-role="collapsible"> + <h3>Movies</h3> + <div data-role="controlgroup"> + <input type="checkbox" data-mini="true" id="chkDownloadMovieArt" name="chkDownloadMovieArt" onchange="MetadataImagesPage.submit();" /> + <label for="chkDownloadMovieArt">Movie Art</label> - <input type="checkbox" data-mini="true" id="chkDownloadMovieDisc" name="chkDownloadMovieDisc" /> - <label for="chkDownloadMovieDisc">Movie Disc</label> + <input type="checkbox" data-mini="true" id="chkDownloadMovieBanner" name="chkDownloadMovieBanner" onchange="MetadataImagesPage.submit();" /> + <label for="chkDownloadMovieBanner">Movie Banner</label> - <input type="checkbox" data-mini="true" id="chkDownloadMovieLogo" name="chkDownloadMovieLogo" /> - <label for="chkDownloadMovieLogo">Movie Logo</label> + <input type="checkbox" data-mini="true" id="chkDownloadMovieDisc" name="chkDownloadMovieDisc" onchange="MetadataImagesPage.submit();" /> + <label for="chkDownloadMovieDisc">Movie Disc</label> + + <input type="checkbox" data-mini="true" id="chkDownloadMovieLogo" name="chkDownloadMovieLogo" onchange="MetadataImagesPage.submit();" /> + <label for="chkDownloadMovieLogo">Movie Logo</label> + + <input type="checkbox" data-mini="true" id="chkDownloadMovieThumb" name="chkDownloadMovieThumb" onchange="MetadataImagesPage.submit();" /> + <label for="chkDownloadMovieThumb">Movie Thumb</label> - <input type="checkbox" data-mini="true" id="chkDownloadMovieThumb" name="chkDownloadMovieThumb" /> - <label for="chkDownloadMovieThumb">Movie Thumb</label> - - </div> </div> + </div> - <div data-role="collapsible"> - <h3>TV Series</h3> - <div data-role="controlgroup"> - <input type="checkbox" data-mini="true" id="chKDownloadTVArt" name="chKDownloadTVArt" /> - <label for="chKDownloadTVArt">TV Series Art</label> + <div data-role="collapsible"> + <h3>TV Series</h3> + <div data-role="controlgroup"> + <input type="checkbox" data-mini="true" id="chKDownloadTVArt" name="chKDownloadTVArt" onchange="MetadataImagesPage.submit();" /> + <label for="chKDownloadTVArt">TV Series Art</label> - <input type="checkbox" data-mini="true" id="chkDownloadTVBanner" name="chkDownloadTVBanner" /> - <label for="chkDownloadTVBanner">TV Series Banner</label> + <input type="checkbox" data-mini="true" id="chkDownloadTVBanner" name="chkDownloadTVBanner" onchange="MetadataImagesPage.submit();" /> + <label for="chkDownloadTVBanner">TV Series Banner</label> - <input type="checkbox" data-mini="true" id="chkDownloadTVLogo" name="chkDownloadTVLogo" /> - <label for="chkDownloadTVLogo">TV Series Logo</label> + <input type="checkbox" data-mini="true" id="chkDownloadTVLogo" name="chkDownloadTVLogo" onchange="MetadataImagesPage.submit();" /> + <label for="chkDownloadTVLogo">TV Series Logo</label> - <input type="checkbox" data-mini="true" id="chkDownloadTVThumb" name="chkDownloadTVThumb" /> - <label for="chkDownloadTVThumb">TV Series Thumb</label> - </div> + <input type="checkbox" data-mini="true" id="chkDownloadTVThumb" name="chkDownloadTVThumb" onchange="MetadataImagesPage.submit();" /> + <label for="chkDownloadTVThumb">TV Series Thumb</label> </div> + </div> - <div data-role="collapsible"> - <h3>TV Seasons</h3> - <div data-role="controlgroup"> - <input type="checkbox" data-mini="true" id="chkDownloadSeasonBackdrops" name="chkDownloadSeasonBackdrops" /> - <label for="chkDownloadSeasonBackdrops">TV Season Backdrops</label> - - <input type="checkbox" data-mini="true" id="chkDownloadSeasonBanner" name="chkDownloadSeasonBanner" /> - <label for="chkDownloadSeasonBanner">TV Season Banner</label> + <div data-role="collapsible"> + <h3>TV Seasons</h3> + <div data-role="controlgroup"> + <input type="checkbox" data-mini="true" id="chkDownloadSeasonBackdrops" name="chkDownloadSeasonBackdrops" onchange="MetadataImagesPage.submit();" /> + <label for="chkDownloadSeasonBackdrops">TV Season Backdrops</label> - <input type="checkbox" data-mini="true" id="chkDownloadSeasonThumb" name="chkDownloadSeasonThumb" /> - <label for="chkDownloadSeasonThumb">TV Season Thumb</label> - </div> + <input type="checkbox" data-mini="true" id="chkDownloadSeasonBanner" name="chkDownloadSeasonBanner" onchange="MetadataImagesPage.submit();" /> + <label for="chkDownloadSeasonBanner">TV Season Banner</label> + <input type="checkbox" data-mini="true" id="chkDownloadSeasonThumb" name="chkDownloadSeasonThumb" onchange="MetadataImagesPage.submit();" /> + <label for="chkDownloadSeasonThumb">TV Season Thumb</label> </div> - <div data-role="collapsible"> - <h3>Music Artists</h3> - <div data-role="controlgroup"> - <input type="checkbox" data-mini="true" id="chkDownloadArtistThumb" name="chkDownloadArtistThumb" /> - <label for="chkDownloadArtistThumb">Music Artist Thumb (primary image)</label> + </div> - <input type="checkbox" data-mini="true" id="chkDownloadArtistBackdrops" name="chkDownloadArtistBackdrops" /> - <label for="chkDownloadArtistBackdrops">Music Artist Backdrops</label> + <div data-role="collapsible"> + <h3>Music Artists</h3> + <div data-role="controlgroup"> + <input type="checkbox" data-mini="true" id="chkDownloadArtistThumb" name="chkDownloadArtistThumb" onchange="MetadataImagesPage.submit();" /> + <label for="chkDownloadArtistThumb">Music Artist Thumb (primary image)</label> - <input type="checkbox" data-mini="true" id="chkDownloadArtistLogo" name="chkDownloadArtistLogo" /> - <label for="chkDownloadArtistLogo">Music Artist Logo</label> + <input type="checkbox" data-mini="true" id="chkDownloadArtistBackdrops" name="chkDownloadArtistBackdrops" onchange="MetadataImagesPage.submit();" /> + <label for="chkDownloadArtistBackdrops">Music Artist Backdrops</label> - <input type="checkbox" data-mini="true" id="chkDownloadArtistBanner" name="chkDownloadArtistBanner" /> - <label for="chkDownloadArtistBanner">Music Artist Banner</label> - </div> + <input type="checkbox" data-mini="true" id="chkDownloadArtistLogo" name="chkDownloadArtistLogo" onchange="MetadataImagesPage.submit();" /> + <label for="chkDownloadArtistLogo">Music Artist Logo</label> + <input type="checkbox" data-mini="true" id="chkDownloadArtistBanner" name="chkDownloadArtistBanner" onchange="MetadataImagesPage.submit();" /> + <label for="chkDownloadArtistBanner">Music Artist Banner</label> </div> - - <div data-role="collapsible"> - <h3>Music Albums</h3> - <div data-role="controlgroup"> - <input type="checkbox" data-mini="true" id="chkDownloadAlbumPrimary" name="chkDownloadAlbumPrimary" /> - <label for="chkDownloadAlbumPrimary">Music Album Cover</label> - <input type="checkbox" data-mini="true" id="chkDownloadAlbumBackdrops" name="chkDownloadAlbumBackdrops" /> - <label for="chkDownloadAlbumBackdrops">Music Album Backdrops</label> - </div> + </div> + + <div data-role="collapsible"> + <h3>Music Albums</h3> + <div data-role="controlgroup"> + <input type="checkbox" data-mini="true" id="chkDownloadAlbumPrimary" name="chkDownloadAlbumPrimary" onchange="MetadataImagesPage.submit();" /> + <label for="chkDownloadAlbumPrimary">Music Album Cover</label> + <input type="checkbox" data-mini="true" id="chkDownloadAlbumBackdrops" name="chkDownloadAlbumBackdrops" onchange="MetadataImagesPage.submit();" /> + <label for="chkDownloadAlbumBackdrops">Music Album Backdrops</label> </div> + </div> + </li> <li> <label for="selectTmdbPosterDownloadSize">Tmdb poster download size: </label> - <select id="selectTmdbPosterDownloadSize" name="selectTmdbPosterDownloadSize"> + <select id="selectTmdbPosterDownloadSize" name="selectTmdbPosterDownloadSize" onchange="MetadataImagesPage.submit();"> <option value="original">original</option> <option value="w92">w92</option> <option value="w154">w154</option> @@ -130,7 +130,7 @@ </li> <li> <label for="selectTmdbBackdropDownloadSize">Tmdb backdrop download size: </label> - <select id="selectTmdbBackdropDownloadSize" name="selectTmdbBackdropDownloadSize"> + <select id="selectTmdbBackdropDownloadSize" name="selectTmdbBackdropDownloadSize" onchange="MetadataImagesPage.submit();"> <option value="original">original</option> <option value="w380">w380</option> <option value="w780">w780</option> @@ -139,20 +139,17 @@ </li> <li> <label for="selectTmdbPersonImageDownloadSize">Tmdb person image download size: </label> - <select id="selectTmdbPersonImageDownloadSize" name="selectTmdbPersonImageDownloadSize"> + <select id="selectTmdbPersonImageDownloadSize" name="selectTmdbPersonImageDownloadSize" onchange="MetadataImagesPage.submit();"> <option value="original">original</option> <option value="w45">w45</option> <option value="w185">w185</option> <option value="h632">h632</option> </select> </li> - <li> - <button type="submit" data-theme="b"> + <li style="display: none;"> + <button class="btnSubmit" type="submit" data-theme="b"> Save </button> - <button type="button" onclick="Dashboard.navigate('dashboard.html');"> - Cancel - </button> </li> </ul> </form> diff --git a/MediaBrowser.WebDashboard/Html/scripts/AdvancedMetadataConfigurationPage.js b/MediaBrowser.WebDashboard/Html/scripts/AdvancedMetadataConfigurationPage.js index 73ed3f4311..8b89add10a 100644 --- a/MediaBrowser.WebDashboard/Html/scripts/AdvancedMetadataConfigurationPage.js +++ b/MediaBrowser.WebDashboard/Html/scripts/AdvancedMetadataConfigurationPage.js @@ -33,7 +33,7 @@ var checkedAttribute = configuration.InternetProviderExcludeTypes.indexOf(type) != -1 ? ' checked="checked"' : ''; - html += '<input' + checkedAttribute + ' class="chkItemType" data-itemtype="' + type + '" type="checkbox" name="' + id + '" id="' + id + '" />'; + html += '<input' + checkedAttribute + ' class="chkItemType" data-itemtype="' + type + '" type="checkbox" name="' + id + '" id="' + id + '" onchange="AdvancedMetadataConfigurationPage.submit();" />'; html += '<label for="' + id + '">' + type + '</label>'; } @@ -42,6 +42,12 @@ $('#divItemTypes', page).html(html).trigger("create"); }, + submit: function () { + + $('.btnSubmit', $.mobile.activePage)[0].click(); + + }, + onSubmit: function () { Dashboard.showLoadingMsg(); @@ -54,7 +60,7 @@ return currentCheckbox.getAttribute('data-itemtype'); }); - ApiClient.updateServerConfiguration(config).done(Dashboard.processServerConfigurationUpdateResult); + ApiClient.updateServerConfiguration(config); }); // Disable default form submission diff --git a/MediaBrowser.WebDashboard/Html/scripts/MetadataConfigurationPage.js b/MediaBrowser.WebDashboard/Html/scripts/MetadataConfigurationPage.js index e68940b832..f7a9ba07b2 100644 --- a/MediaBrowser.WebDashboard/Html/scripts/MetadataConfigurationPage.js +++ b/MediaBrowser.WebDashboard/Html/scripts/MetadataConfigurationPage.js @@ -79,6 +79,12 @@ $('#selectLanguage', '#metadataConfigurationPage').html(html).selectmenu("refresh"); }, + submit: function () { + + $('.btnSubmit', $.mobile.activePage)[0].click(); + + }, + onSubmit: function () { Dashboard.showLoadingMsg(); @@ -92,7 +98,7 @@ config.PreferredMetadataLanguage = $('#selectLanguage', form).val(); config.MetadataCountryCode = $('#selectCountry', form).val(); - ApiClient.updateServerConfiguration(config).done(Dashboard.processServerConfigurationUpdateResult); + ApiClient.updateServerConfiguration(config); }); // Disable default form submission diff --git a/MediaBrowser.WebDashboard/Html/scripts/MetadataImagesPage.js b/MediaBrowser.WebDashboard/Html/scripts/MetadataImagesPage.js index 0dff46c398..e09172d36e 100644 --- a/MediaBrowser.WebDashboard/Html/scripts/MetadataImagesPage.js +++ b/MediaBrowser.WebDashboard/Html/scripts/MetadataImagesPage.js @@ -42,6 +42,12 @@ Dashboard.hideLoadingMsg(); }, + submit: function () { + + $('.btnSubmit', $.mobile.activePage)[0].click(); + + }, + onSubmit: function () { Dashboard.showLoadingMsg(); @@ -75,7 +81,7 @@ config.DownloadMusicAlbumImages.Primary = $('#chkDownloadAlbumPrimary', form).checked(); config.DownloadMusicAlbumImages.Backdrops = $('#chkDownloadAlbumBackdrops', form).checked(); - ApiClient.updateServerConfiguration(config).done(Dashboard.processServerConfigurationUpdateResult); + ApiClient.updateServerConfiguration(config); }); // Disable default form submission diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index 188c187226..07992b1e38 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -403,6 +403,7 @@ <EmbeddedResource Include="Html\css\images\mblogotextblack.png" />
<EmbeddedResource Include="Html\css\images\mblogotextwhite.png" />
<EmbeddedResource Include="Html\css\images\clients\dlna.png" />
+ <EmbeddedResource Include="ApiClient.js" />
<Content Include="Html\css\images\stars.png" />
<EmbeddedResource Include="Html\scripts\MediaPlayer.js" />
</ItemGroup>
diff --git a/MediaBrowser.WebDashboard/packages.config b/MediaBrowser.WebDashboard/packages.config index 629487c799..fb7a49ef6f 100644 --- a/MediaBrowser.WebDashboard/packages.config +++ b/MediaBrowser.WebDashboard/packages.config @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <packages> + <package id="MediaBrowser.ApiClient.Javascript" version="3.0.40" targetFramework="net45" /> <package id="ServiceStack" version="3.9.38" targetFramework="net45" /> <package id="ServiceStack.Common" version="3.9.38" targetFramework="net45" /> <package id="ServiceStack.OrmLite.SqlServer" version="3.9.39" targetFramework="net45" /> |
