From 6d1eef915406e45add934821087b8bc1b16df5d6 Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Fri, 28 Oct 2016 17:24:45 +0100 Subject: [PATCH 01/87] User perms --- .../MigrationRunner.cs | 2 +- .../Migrations/Version1100.cs | 58 +++++++++ .../PlexRequests.Core.Migration.csproj | 1 + PlexRequests.Core/UserMapper.cs | 25 ++++ PlexRequests.Helpers/EnumExtensions.cs | 54 +++++++++ PlexRequests.Helpers/EnumHelper.cs | 112 ++++++++++++++++++ PlexRequests.Helpers/Permissions/Features.cs | 42 +++++++ .../Permissions/Permissions.cs | 51 ++++++++ .../PlexRequests.Helpers.csproj | 5 + PlexRequests.Store/SqlTables.sql | 5 +- PlexRequests.Store/TableCreation.cs | 3 +- PlexRequests.Store/UsersModel.cs | 3 + .../userManagementController.js | 72 +++++++---- .../userManagement/userManagementService.js | 21 ++-- .../UserManagementUsersViewModel.cs | 38 +++--- .../Modules/UserManagementModule.cs | 101 ++++++++++------ .../Views/UserManagement/Index.cshtml | 35 ++++-- 17 files changed, 524 insertions(+), 104 deletions(-) create mode 100644 PlexRequests.Core.Migration/Migrations/Version1100.cs create mode 100644 PlexRequests.Helpers/EnumExtensions.cs create mode 100644 PlexRequests.Helpers/EnumHelper.cs create mode 100644 PlexRequests.Helpers/Permissions/Features.cs create mode 100644 PlexRequests.Helpers/Permissions/Permissions.cs diff --git a/PlexRequests.Core.Migration/MigrationRunner.cs b/PlexRequests.Core.Migration/MigrationRunner.cs index c8935031a..5a1a67f10 100644 --- a/PlexRequests.Core.Migration/MigrationRunner.cs +++ b/PlexRequests.Core.Migration/MigrationRunner.cs @@ -22,7 +22,7 @@ namespace PlexRequests.Core.Migration public void MigrateToLatest() { var con = Db.DbConnection(); - var versions = GetMigrations().OrderBy(x => x.Key); + var versions = GetMigrations(); var dbVersion = con.GetVersionInfo().OrderByDescending(x => x.Version).FirstOrDefault(); if (dbVersion == null) diff --git a/PlexRequests.Core.Migration/Migrations/Version1100.cs b/PlexRequests.Core.Migration/Migrations/Version1100.cs new file mode 100644 index 000000000..fe03daf88 --- /dev/null +++ b/PlexRequests.Core.Migration/Migrations/Version1100.cs @@ -0,0 +1,58 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: Version1100.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System.Data; +using PlexRequests.Store; + +namespace PlexRequests.Core.Migration.Migrations +{ + [Migration(11000, "v1.10.0.0")] + public class Version1100 : BaseMigration, IMigration + { + public Version1100() + { + + } + public int Version => 11000; + + + public void Start(IDbConnection con) + { + UpdateDb(con); + + UpdateSchema(con, Version); + } + + private void UpdateDb(IDbConnection con) + { + // Create the two new columns + con.AlterTable("Users", "ADD", "Permissions", true, "INTEGER"); + con.AlterTable("Users", "ADD", "Features", true, "INTEGER"); + + } + } +} \ No newline at end of file diff --git a/PlexRequests.Core.Migration/PlexRequests.Core.Migration.csproj b/PlexRequests.Core.Migration/PlexRequests.Core.Migration.csproj index a92ae74d5..2881f95ed 100644 --- a/PlexRequests.Core.Migration/PlexRequests.Core.Migration.csproj +++ b/PlexRequests.Core.Migration/PlexRequests.Core.Migration.csproj @@ -65,6 +65,7 @@ + diff --git a/PlexRequests.Core/UserMapper.cs b/PlexRequests.Core/UserMapper.cs index 24e44999f..8f705147e 100644 --- a/PlexRequests.Core/UserMapper.cs +++ b/PlexRequests.Core/UserMapper.cs @@ -36,6 +36,7 @@ using Nancy.Security; using PlexRequests.Core.Models; using PlexRequests.Helpers; +using PlexRequests.Helpers.Permissions; using PlexRequests.Store; using PlexRequests.Store.Repository; @@ -118,6 +119,27 @@ namespace PlexRequests.Core return new Guid(userRecord.UserGuid); } + public Guid? CreateUser(string username, string password, int permissions, int features, UserProperties properties = null) + { + var salt = PasswordHasher.GenerateSalt(); + + var userModel = new UsersModel + { + UserName = username, + UserGuid = Guid.NewGuid().ToString(), + Salt = salt, + Hash = PasswordHasher.ComputeHash(password, salt), + UserProperties = ByteConverterHelper.ReturnBytes(properties ?? new UserProperties()), + Permissions = permissions, + Features = features, + Claims = new byte[] {0} + }; + Repo.Insert(userModel); + var userRecord = Repo.Get(userModel.UserGuid); + + return new Guid(userRecord.UserGuid); + } + public void DeleteUser(string userId) { var user = Repo.Get(userId); @@ -187,6 +209,9 @@ namespace PlexRequests.Core public interface ICustomUserMapper { Guid? CreateUser(string username, string password, string[] claims, UserProperties props); + + Guid? CreateUser(string username, string password, int permissions, int features, + UserProperties properties = null); IEnumerable GetAllClaims(); IEnumerable GetUsers(); Task> GetUsersAsync(); diff --git a/PlexRequests.Helpers/EnumExtensions.cs b/PlexRequests.Helpers/EnumExtensions.cs new file mode 100644 index 000000000..72e1fcf68 --- /dev/null +++ b/PlexRequests.Helpers/EnumExtensions.cs @@ -0,0 +1,54 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: EnumExtensions.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace PlexRequests.Helpers +{ + public static class EnumExtensions + { + public static IEnumerable GetUniqueFlags(this Enum flags) + { + ulong flag = 1; + foreach (var value in Enum.GetValues(flags.GetType()).Cast()) + { + var bits = Convert.ToUInt64(value); + while (flag < bits) + { + flag <<= 1; + } + + if (flag == bits && flags.HasFlag(value)) + { + yield return value; + } + } + } + } +} \ No newline at end of file diff --git a/PlexRequests.Helpers/EnumHelper.cs b/PlexRequests.Helpers/EnumHelper.cs new file mode 100644 index 000000000..051c78508 --- /dev/null +++ b/PlexRequests.Helpers/EnumHelper.cs @@ -0,0 +1,112 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: EnumHelper.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; + +namespace PlexRequests.Helpers +{ + public static class EnumHelper + { + public static IList GetValues(Enum value) + { + return value.GetType().GetFields(BindingFlags.Static | BindingFlags.Public).Select(fi => (T) Enum.Parse(value.GetType(), fi.Name, false)).ToList(); + } + + public static T Parse(string value) + { + return (T)Enum.Parse(typeof(T), value, true); + } + + public static IList GetNames(Enum value) + { + return value.GetType().GetFields(BindingFlags.Static | BindingFlags.Public).Select(fi => fi.Name).ToList(); + } + + public static IList GetDisplayValues(Enum value) + { + return GetNames(value).Select(obj => GetDisplayValue(Parse(obj))).ToList(); + } + + private static string LookupResource(Type resourceManagerProvider, string resourceKey) + { + foreach (var staticProperty in resourceManagerProvider.GetProperties(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)) + { + if (staticProperty.PropertyType == typeof(System.Resources.ResourceManager)) + { + System.Resources.ResourceManager resourceManager = (System.Resources.ResourceManager)staticProperty.GetValue(null, null); + return resourceManager.GetString(resourceKey); + } + } + + return resourceKey; // Fallback with the key name + } + + public static string GetDisplayValue(T value) + { + var fieldInfo = value.GetType().GetField(value.ToString()); + + var descriptionAttributes = fieldInfo.GetCustomAttributes( + typeof(DisplayAttribute), false) as DisplayAttribute[]; + + if (descriptionAttributes[0].ResourceType != null) + return LookupResource(descriptionAttributes[0].ResourceType, descriptionAttributes[0].Name); + + if (descriptionAttributes == null) return string.Empty; + return (descriptionAttributes.Length > 0) ? descriptionAttributes[0].Name : value.ToString(); + } + + public static T GetValueFromName(string name) + { + var type = typeof(T); + if (!type.IsEnum) throw new InvalidOperationException(); + + foreach (var field in type.GetFields()) + { + var attribute = Attribute.GetCustomAttribute(field, + typeof(DisplayAttribute)) as DisplayAttribute; + if (attribute != null) + { + if (attribute.Name == name) + { + return (T)field.GetValue(null); + } + } + else + { + if (field.Name == name) + return (T)field.GetValue(null); + } + } + + throw new ArgumentOutOfRangeException(nameof(name)); + } + } +} \ No newline at end of file diff --git a/PlexRequests.Helpers/Permissions/Features.cs b/PlexRequests.Helpers/Permissions/Features.cs new file mode 100644 index 000000000..a236a8d15 --- /dev/null +++ b/PlexRequests.Helpers/Permissions/Features.cs @@ -0,0 +1,42 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: Features.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; +using System.ComponentModel.DataAnnotations; + +namespace PlexRequests.Helpers.Permissions +{ + [Flags] + public enum Features + { + [Display(Name = "Newsletter")] + Newsletter = 1, + [Display(Name = "Recently Added Notification")] + RecentlyAddedNotification = 2, + + } +} \ No newline at end of file diff --git a/PlexRequests.Helpers/Permissions/Permissions.cs b/PlexRequests.Helpers/Permissions/Permissions.cs new file mode 100644 index 000000000..ffc0f1cc1 --- /dev/null +++ b/PlexRequests.Helpers/Permissions/Permissions.cs @@ -0,0 +1,51 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: Permissions.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; +using System.ComponentModel.DataAnnotations; + +namespace PlexRequests.Helpers.Permissions +{ + [Flags] + public enum Permissions + { + [Display(Name = "Access Administration Settings")] + AdminSettings = 1, + + [Display(Name = "Request Movie")] + RequestMovie = 2, + + [Display(Name = "Request TV Show")] + RequestTvShow = 4, + + [Display(Name = "Request Music")] + RequestMusic = 8, + + [Display(Name = "Report Issue")] + ReportIssue = 16 + } +} \ No newline at end of file diff --git a/PlexRequests.Helpers/PlexRequests.Helpers.csproj b/PlexRequests.Helpers/PlexRequests.Helpers.csproj index 0ffffb4c8..38c703c97 100644 --- a/PlexRequests.Helpers/PlexRequests.Helpers.csproj +++ b/PlexRequests.Helpers/PlexRequests.Helpers.csproj @@ -48,6 +48,7 @@ True + @@ -69,6 +70,7 @@ + @@ -78,6 +80,9 @@ + + + diff --git a/PlexRequests.Store/SqlTables.sql b/PlexRequests.Store/SqlTables.sql index c0bb6cb27..c0cf01231 100644 --- a/PlexRequests.Store/SqlTables.sql +++ b/PlexRequests.Store/SqlTables.sql @@ -7,8 +7,9 @@ CREATE TABLE IF NOT EXISTS Users UserName varchar(50) NOT NULL, Salt BLOB NOT NULL, Hash BLOB NOT NULL, - Claims BLOB NOT NULL, - UserProperties BLOB + UserProperties BLOB, + Permissions INTEGER, + Features INTEGER ); CREATE TABLE IF NOT EXISTS UserLogins diff --git a/PlexRequests.Store/TableCreation.cs b/PlexRequests.Store/TableCreation.cs index 295a3ca53..24ba97c94 100644 --- a/PlexRequests.Store/TableCreation.cs +++ b/PlexRequests.Store/TableCreation.cs @@ -57,12 +57,13 @@ namespace PlexRequests.Store } } - public static void AddColumn(this IDbConnection connection, string tableName, string alterType, string newColumn, bool allowNulls, string dataType) + public static void AlterTable(this IDbConnection connection, string tableName, string alterType, string newColumn, bool allowNulls, string dataType) { connection.Open(); var result = connection.Query($"PRAGMA table_info({tableName});"); if (result.Any(x => x.name == newColumn)) { + connection.Close(); return; } diff --git a/PlexRequests.Store/UsersModel.cs b/PlexRequests.Store/UsersModel.cs index 88e04b446..45da08fdc 100644 --- a/PlexRequests.Store/UsersModel.cs +++ b/PlexRequests.Store/UsersModel.cs @@ -35,7 +35,10 @@ namespace PlexRequests.Store { public byte[] Hash { get; set; } public byte[] Salt { get; set; } + [Obsolete] public byte[] Claims { get; set; } public byte[] UserProperties { get; set; } + public int Permissions { get; set; } + public int Features { get; set; } } } diff --git a/PlexRequests.UI/Content/app/userManagement/userManagementController.js b/PlexRequests.UI/Content/app/userManagement/userManagementController.js index 39d939b5d..3b67832aa 100644 --- a/PlexRequests.UI/Content/app/userManagement/userManagementController.js +++ b/PlexRequests.UI/Content/app/userManagement/userManagementController.js @@ -4,10 +4,14 @@ $scope.user = {}; // The local user $scope.users = []; // list of users - $scope.claims = []; // List of claims + + $scope.features = []; // List of features + $scope.permissions = []; // List of permissions $scope.selectedUser = {}; // User on the right side - $scope.selectedClaims = {}; + + $scope.selectedFeatures = {}; + $scope.selectedPermissions = {}; $scope.minDate = "0001-01-01T00:00:00.0000000+00:00"; @@ -44,11 +48,16 @@ }); }; - // Get the claims and populate the create dropdown - $scope.getClaims = function () { - userManagementService.getClaims() + // Get the permissions and features and populate the create dropdown + $scope.getFeaturesPermissions = function () { + userManagementService.getFeatures() .then(function (data) { - $scope.claims = data.data; + $scope.features = data.data; + }); + + userManagementService.getPermissions() + .then(function (data) { + $scope.permissions = data.data; }); } @@ -62,14 +71,14 @@ return; } - if (!$scope.selectedClaims) { + if ($scope.selectedPermissions.length === 0) { $scope.error.error = true; $scope.error.errorMessage = "Please select a permission"; generateNotify($scope.error.errorMessage, 'warning'); return; } - userManagementService.addUser($scope.user, $scope.selectedClaims) + userManagementService.addUser($scope.user, $scope.selectedPermissions, $scope.selectedFeatures) .then(function (data) { if (data.message) { $scope.error.error = true; @@ -77,27 +86,33 @@ } else { $scope.users.push(data.data); // Push the new user into the array to update the DOM $scope.user = {}; - $scope.selectedClaims = {}; - $scope.claims.forEach(function (entry) { + $scope.selectedPermissions = {}; // Clear the checkboxes + $scope.selectedFeatures = {}; + $scope.features.forEach(function (entry) { entry.selected = false; }); + $scope.permissions.forEach(function (entry) { + entry.selected = false; + }); + + } }); }; - $scope.hasClaim = function (claim) { - var claims = $scope.selectedUser.claimsArray; - - var result = claims.some(function (item) { - return item === claim.name; - }); - return result; - }; - - $scope.$watch('claims|filter:{selected:true}', + // Watch the checkboxes for updates (Creating a user) + $scope.$watch('features|filter:{selected:true}', function (nv) { - $scope.selectedClaims = nv.map(function (claim) { - return claim.name; + $scope.selectedFeatures = nv.map(function (f) { + return f.name; + }); + }, + true); + + $scope.$watch('permissions|filter:{selected:true}', + function (nv) { + $scope.selectedPermissions = nv.map(function (f) { + return f.name; }); }, true); @@ -105,10 +120,15 @@ $scope.updateUser = function () { var u = $scope.selectedUser; - userManagementService.updateUser(u.id, u.claimsItem, u.alias, u.emailAddress) + userManagementService.updateUser(u.id, u.permissions, u.alias, u.emailAddress) .then(function (data) { if (data) { $scope.selectedUser = data; + + if (open) { + open = false; + $("#wrapper").toggleClass("toggled"); + } return successCallback("Updated User", "success"); } }); @@ -118,7 +138,7 @@ var u = $scope.selectedUser; var result = userManagementService.deleteUser(u.id); - result.success(function(data) { + result.success(function (data) { if (data.result) { removeUser(u.id, true); return successCallback("Deleted User", "success"); @@ -138,7 +158,7 @@ // On page load $scope.init = function () { $scope.getUsers(); - $scope.getClaims(); + $scope.getFeaturesPermissions(); return; } @@ -157,5 +177,5 @@ }; - angular.module('PlexRequests').controller('userManagementController', ["$scope", "userManagementService","moment", controller]); + angular.module('PlexRequests').controller('userManagementController', ["$scope", "userManagementService", "moment", controller]); }()); \ No newline at end of file diff --git a/PlexRequests.UI/Content/app/userManagement/userManagementService.js b/PlexRequests.UI/Content/app/userManagement/userManagementService.js index 209ca20b9..72cd7f375 100644 --- a/PlexRequests.UI/Content/app/userManagement/userManagementService.js +++ b/PlexRequests.UI/Content/app/userManagement/userManagementService.js @@ -8,27 +8,31 @@ return $http.get('/usermanagement/users'); }; - var addUser = function (user, claims) { - if (!user || claims.length === 0) { + var addUser = function (user, permissions, features) { + if (!user || permissions.length === 0) { return null; } return $http({ url: '/usermanagement/createuser', method: "POST", - data: { username: user.username, password: user.password, claims: claims, email: user.email } + data: { username: user.username, password: user.password, permissions: permissions, features : features, email: user.email } }); } - var getClaims = function () { - return $http.get('/usermanagement/claims'); + var getFeatures = function () { + return $http.get('/usermanagement/features'); } - var updateUser = function (id, claims, alias, email) { + var getPermissions = function () { + return $http.get('/usermanagement/permissions'); + } + + var updateUser = function (id, permissions, alias, email) { return $http({ url: '/usermanagement/updateUser', method: "POST", - data: { id: id, claims: claims, alias: alias, emailAddress: email } + data: { id: id, permissions: permissions, alias: alias, emailAddress: email } }); } @@ -43,7 +47,8 @@ return { getUsers: getUsers, addUser: addUser, - getClaims: getClaims, + getFeatures: getFeatures, + getPermissions: getPermissions, updateUser: updateUser, deleteUser: deleteUser }; diff --git a/PlexRequests.UI/Models/UserManagement/UserManagementUsersViewModel.cs b/PlexRequests.UI/Models/UserManagement/UserManagementUsersViewModel.cs index 16abe0615..aad003c05 100644 --- a/PlexRequests.UI/Models/UserManagement/UserManagementUsersViewModel.cs +++ b/PlexRequests.UI/Models/UserManagement/UserManagementUsersViewModel.cs @@ -10,17 +10,20 @@ namespace PlexRequests.UI.Models public UserManagementUsersViewModel() { PlexInfo = new UserManagementPlexInformation(); + Permissions = new List(); + Features = new List(); } public string Username { get; set; } - public string Claims { get; set; } + public string FeaturesFormattedString { get; set; } + public string PermissionsFormattedString { get; set; } public string Id { get; set; } public string Alias { get; set; } public UserType Type { get; set; } public string EmailAddress { get; set; } public UserManagementPlexInformation PlexInfo { get; set; } - public string[] ClaimsArray { get; set; } - public List ClaimsItem { get; set; } public DateTime LastLoggedIn { get; set; } + public List Permissions { get; set; } + public List Features { get; set; } } public class UserManagementPlexInformation @@ -33,6 +36,13 @@ namespace PlexRequests.UI.Models public List Servers { get; set; } } + public class CheckBox + { + public string Name { get; set; } + public int Value { get; set; } + public bool Selected { get; set; } + } + public class UserManagementPlexServers { public int Id { get; set; } @@ -50,8 +60,10 @@ namespace PlexRequests.UI.Models public string Username { get; set; } [JsonProperty("password")] public string Password { get; set; } - [JsonProperty("claims")] - public string[] Claims { get; set; } + [JsonProperty("permissions")] + public List Permissions { get; set; } + [JsonProperty("features")] + public List Features { get; set; } [JsonProperty("email")] public string EmailAddress { get; set; } @@ -61,20 +73,10 @@ namespace PlexRequests.UI.Models { [JsonProperty("id")] public string Id { get; set; } - [JsonProperty("claims")] - public List Claims { get; set; } - + [JsonProperty("permissions")] + public List Permissions { get; set; } public string Alias { get; set; } - public string EmailAddress { get; set; } - - public class ClaimsModel - { - [JsonProperty("name")] - public string Name { get; set; } - [JsonProperty("selected")] - public bool Selected { get; set; } - } - + public string EmailAddress { get; set; } } } diff --git a/PlexRequests.UI/Modules/UserManagementModule.cs b/PlexRequests.UI/Modules/UserManagementModule.cs index 14f99254c..d5879dd72 100644 --- a/PlexRequests.UI/Modules/UserManagementModule.cs +++ b/PlexRequests.UI/Modules/UserManagementModule.cs @@ -13,6 +13,7 @@ using PlexRequests.Core; using PlexRequests.Core.Models; using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; +using PlexRequests.Helpers.Permissions; using PlexRequests.Store; using PlexRequests.Store.Repository; using PlexRequests.UI.Models; @@ -24,7 +25,7 @@ namespace PlexRequests.UI.Modules public UserManagementModule(ISettingsService pr, ICustomUserMapper m, IPlexApi plexApi, ISettingsService plex, IRepository userLogins) : base("usermanagement", pr) { #if !DEBUG - this.RequiresClaims(UserClaims.Admin); + this.RequiresAnyClaims(UserClaims.Admin); #endif UserMapper = m; PlexApi = plexApi; @@ -37,7 +38,8 @@ namespace PlexRequests.UI.Modules Post["/createuser"] = x => CreateUser(); Get["/local/{id}"] = x => LocalDetails((Guid)x.id); Get["/plex/{id}", true] = async (x, ct) => await PlexDetails(x.id); - Get["/claims"] = x => GetClaims(); + Get["/permissions"] = x => GetEnum(); + Get["/features"] = x => GetEnum(); Post["/updateuser"] = x => UpdateUser(); Post["/deleteuser"] = x => DeleteUser(); } @@ -79,7 +81,7 @@ namespace PlexRequests.UI.Modules Username = u.Username, Type = UserType.PlexUser, Id = u.Id, - Claims = "Requestor", + FeaturesFormattedString = "Requestor", EmailAddress = u.Email, PlexInfo = new UserManagementPlexInformation { @@ -110,7 +112,23 @@ namespace PlexRequests.UI.Modules Message = "Please enter in a valid Username and Password" }); } - var user = UserMapper.CreateUser(model.Username, model.Password, model.Claims, new UserProperties { EmailAddress = model.EmailAddress }); + + var featuresVal = 0; + var permissionsVal = 0; + + foreach (var feature in model.Features) + { + var f = (int)EnumHelper.GetValueFromName(feature); + featuresVal += f; + } + + foreach (var permission in model.Permissions) + { + var f = (int)EnumHelper.GetValueFromName(permission); + permissionsVal += f; + } + + var user = UserMapper.CreateUser(model.Username, model.Password, featuresVal, permissionsVal, new UserProperties { EmailAddress = model.EmailAddress }); if (user.HasValue) { return Response.AsJson(MapLocalUser(UserMapper.GetUser(user.Value), DateTime.MinValue)); @@ -137,20 +155,13 @@ namespace PlexRequests.UI.Modules Message = "Couldn't find the user" }); } - - var claims = new List(); - - foreach (var c in model.Claims) - { - if (c.Selected) - { - claims.Add(c.Name); - } - } + + var val = model.Permissions.Where(c => c.Selected).Sum(c => c.Value); var userFound = UserMapper.GetUser(new Guid(model.Id)); - userFound.Claims = ByteConverterHelper.ReturnBytes(claims.ToArray()); + userFound.Permissions = val; + var currentProps = ByteConverterHelper.ReturnObject(userFound.UserProperties); currentProps.UserAlias = model.Alias; currentProps.EmailAddress = model.EmailAddress; @@ -221,53 +232,69 @@ namespace PlexRequests.UI.Modules /// Returns all claims for the users. /// /// - private Response GetClaims() + private Response GetEnum() { - var retVal = new List(); - var claims = UserMapper.GetAllClaims(); - foreach (var c in claims) + var retVal = new List(); + foreach (var p in Enum.GetValues(typeof(T))) { - retVal.Add(new { Name = c, Selected = false }); + var perm = (T)p; + var displayValue = EnumHelper.GetDisplayValue(perm); + + retVal.Add(new CheckBox{ Name = displayValue, Selected = false, Value = (int)p }); } + return Response.AsJson(retVal); } private UserManagementUsersViewModel MapLocalUser(UsersModel user, DateTime lastLoggedIn) { - var claims = ByteConverterHelper.ReturnObject(user.Claims); - var claimsString = string.Join(", ", claims); + var features = (Features) user.Features; + var permissions = (Permissions) user.Permissions; var userProps = ByteConverterHelper.ReturnObject(user.UserProperties); var m = new UserManagementUsersViewModel { Id = user.UserGuid, - Claims = claimsString, + PermissionsFormattedString = permissions == 0 ? "None" : permissions.ToString(), + FeaturesFormattedString = features.ToString(), Username = user.UserName, Type = UserType.LocalUser, EmailAddress = userProps.EmailAddress, Alias = userProps.UserAlias, - ClaimsArray = claims, - ClaimsItem = new List(), - LastLoggedIn = lastLoggedIn + LastLoggedIn = lastLoggedIn, }; - // Add all of the current claims - foreach (var c in claims) + // Add permissions + foreach (var p in Enum.GetValues(typeof(Permissions))) { - m.ClaimsItem.Add(new UserManagementUpdateModel.ClaimsModel { Name = c, Selected = true }); + var perm = (Permissions)p; + var displayValue = EnumHelper.GetDisplayValue(perm); + var pm = new CheckBox + { + Name = displayValue, + Selected = permissions.HasFlag(perm), + Value = (int)perm + }; + + m.Permissions.Add(pm); } - var allClaims = UserMapper.GetAllClaims(); - - // Get me the current claims that the user does not have - var missingClaims = allClaims.Except(claims); - - // Add them into the view - foreach (var missingClaim in missingClaims) + // Add features + foreach (var p in Enum.GetValues(typeof(Features))) { - m.ClaimsItem.Add(new UserManagementUpdateModel.ClaimsModel { Name = missingClaim, Selected = false }); + var perm = (Features)p; + var displayValue = EnumHelper.GetDisplayValue(perm); + var pm = new CheckBox + { + Name = displayValue, + Selected = features.HasFlag(perm), + Value = (int)perm + }; + + m.Features.Add(pm); } + return m; } } diff --git a/PlexRequests.UI/Views/UserManagement/Index.cshtml b/PlexRequests.UI/Views/UserManagement/Index.cshtml index 495dc29d5..7002cb143 100644 --- a/PlexRequests.UI/Views/UserManagement/Index.cshtml +++ b/PlexRequests.UI/Views/UserManagement/Index.cshtml @@ -16,7 +16,10 @@ Email Address:
- Permissions: + Permissions: +
+
+ Features:
User Type: @@ -28,9 +31,9 @@ Modify Roles: -
- - +
+ +
Email Address @@ -71,13 +74,23 @@
- -
- - +

Permissions:

+
+ +
+ +

Features:

+
+ + +
+ + + @@ -119,7 +132,7 @@ - Roles + Permissions @@ -150,7 +163,7 @@ {{u.emailAddress}} - {{u.claims}} + {{u.permissionsFormattedString}} {{u.type === 1 ? 'Local User' : 'Plex User'}} From 0bfc8f83934667ed57339e10150afca55a49b6b0 Mon Sep 17 00:00:00 2001 From: TidusJar Date: Fri, 4 Nov 2016 15:02:58 +0000 Subject: [PATCH 02/87] Fixed build issue --- PlexRequests.UI/Modules/UserManagementModule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PlexRequests.UI/Modules/UserManagementModule.cs b/PlexRequests.UI/Modules/UserManagementModule.cs index d5879dd72..ee9558d1f 100644 --- a/PlexRequests.UI/Modules/UserManagementModule.cs +++ b/PlexRequests.UI/Modules/UserManagementModule.cs @@ -25,7 +25,7 @@ namespace PlexRequests.UI.Modules public UserManagementModule(ISettingsService pr, ICustomUserMapper m, IPlexApi plexApi, ISettingsService plex, IRepository userLogins) : base("usermanagement", pr) { #if !DEBUG - this.RequiresAnyClaims(UserClaims.Admin); + this.RequiresAnyClaim(UserClaims.Admin); #endif UserMapper = m; PlexApi = plexApi; From e39512905aceb0df71d789edb17b253a52ebb285 Mon Sep 17 00:00:00 2001 From: TidusJar Date: Fri, 4 Nov 2016 19:11:25 +0000 Subject: [PATCH 03/87] Fixed some issues with the user management work --- PlexRequests.Core/UserMapper.cs | 13 +-- .../Permissions/Permissions.cs | 2 +- PlexRequests.Store/PlexRequests.Store.csproj | 1 + .../Repository/UserRepository.cs | 110 ++++++++++++++++++ PlexRequests.Store/SqlTables.sql | 3 +- PlexRequests.UI/Modules/BaseModule.cs | 19 ++- .../NinjectModules/RepositoryModule.cs | 2 + 7 files changed, 138 insertions(+), 12 deletions(-) create mode 100644 PlexRequests.Store/Repository/UserRepository.cs diff --git a/PlexRequests.Core/UserMapper.cs b/PlexRequests.Core/UserMapper.cs index 8f705147e..9a0afc8de 100644 --- a/PlexRequests.Core/UserMapper.cs +++ b/PlexRequests.Core/UserMapper.cs @@ -61,7 +61,6 @@ namespace PlexRequests.Core return new UserIdentity { UserName = user.UserName, - Claims = ByteConverterHelper.ReturnObject(user.Claims) }; } @@ -100,7 +99,7 @@ namespace PlexRequests.Core return users.Any(); } - public Guid? CreateUser(string username, string password, string[] claims = default(string[]), UserProperties properties = null) + public Guid? CreateUser(string username, string password, UserProperties properties = null) { var salt = PasswordHasher.GenerateSalt(); @@ -110,7 +109,7 @@ namespace PlexRequests.Core UserGuid = Guid.NewGuid().ToString(), Salt = salt, Hash = PasswordHasher.ComputeHash(password, salt), - Claims = ByteConverterHelper.ReturnBytes(claims), + Claims = new byte[] {0}, UserProperties = ByteConverterHelper.ReturnBytes(properties ?? new UserProperties()), }; Repo.Insert(userModel); @@ -148,17 +147,17 @@ namespace PlexRequests.Core public Guid? CreateAdmin(string username, string password, UserProperties properties = null) { - return CreateUser(username, password, new[] { UserClaims.RegularUser, UserClaims.PowerUser, UserClaims.Admin }, properties); + return CreateUser(username, password, properties); } public Guid? CreatePowerUser(string username, string password, UserProperties properties = null) { - return CreateUser(username, password, new[] { UserClaims.RegularUser, UserClaims.PowerUser }, properties); + return CreateUser(username, password, properties); } public Guid? CreateRegularUser(string username, string password, UserProperties properties = null) { - return CreateUser(username, password, new[] { UserClaims.RegularUser }, properties); + return CreateUser(username, password, properties); } public IEnumerable GetAllClaims() @@ -208,7 +207,7 @@ namespace PlexRequests.Core public interface ICustomUserMapper { - Guid? CreateUser(string username, string password, string[] claims, UserProperties props); + Guid? CreateUser(string username, string password, UserProperties props); Guid? CreateUser(string username, string password, int permissions, int features, UserProperties properties = null); diff --git a/PlexRequests.Helpers/Permissions/Permissions.cs b/PlexRequests.Helpers/Permissions/Permissions.cs index ffc0f1cc1..3d5155ad0 100644 --- a/PlexRequests.Helpers/Permissions/Permissions.cs +++ b/PlexRequests.Helpers/Permissions/Permissions.cs @@ -34,7 +34,7 @@ namespace PlexRequests.Helpers.Permissions public enum Permissions { [Display(Name = "Access Administration Settings")] - AdminSettings = 1, + Administrator = 1, [Display(Name = "Request Movie")] RequestMovie = 2, diff --git a/PlexRequests.Store/PlexRequests.Store.csproj b/PlexRequests.Store/PlexRequests.Store.csproj index d8cf9514c..961b6d4b5 100644 --- a/PlexRequests.Store/PlexRequests.Store.csproj +++ b/PlexRequests.Store/PlexRequests.Store.csproj @@ -85,6 +85,7 @@ + diff --git a/PlexRequests.Store/Repository/UserRepository.cs b/PlexRequests.Store/Repository/UserRepository.cs new file mode 100644 index 000000000..56ad461cb --- /dev/null +++ b/PlexRequests.Store/Repository/UserRepository.cs @@ -0,0 +1,110 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: UserRepository.cs +// Created By: +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; +using System.Collections.Generic; +using System.Data; +using System.Threading.Tasks; +using Dapper; +using Dapper.Contrib.Extensions; +using PlexRequests.Helpers; + +namespace PlexRequests.Store.Repository +{ + public class UserRepository : BaseGenericRepository, IUserRepository + { + public UserRepository(ISqliteConfiguration config, ICacheProvider cache) : base(config,cache) + { + DbConfig = config; + Cache = cache; + } + + private ISqliteConfiguration DbConfig { get; } + private ICacheProvider Cache { get; } + private IDbConnection Db => DbConfig.DbConnection(); + + public UsersModel GetUser(string userGuid) + { + var sql = @"SELECT * FROM UsersModel + WHERE Userguid = @UserGuid"; + return Db.QueryFirstOrDefault(sql, new {UserGuid = userGuid}); + } + + public UsersModel GetUserByUsername(string username) + { + var sql = @"SELECT * FROM UsersModel + WHERE UserName = @UserName"; + return Db.QueryFirstOrDefault(sql, new {UserName = username}); + } + + public async Task GetUserAsync(string userguid) + { + var sql = @"SELECT * FROM UsersModel + WHERE UserGuid = @UserGuid"; + return await Db.QueryFirstOrDefaultAsync(sql, new {UserGuid = userguid}); + } + + #region abstract implimentation + [Obsolete] + public override UsersModel Get(string id) + { + throw new System.NotImplementedException(); + } + + [Obsolete] + public override Task GetAsync(int id) + { + throw new System.NotImplementedException(); + } + + [Obsolete] + public override UsersModel Get(int id) + { + throw new System.NotImplementedException(); + } + + [Obsolete] + public override Task GetAsync(string id) + { + throw new System.NotImplementedException(); + } + + #endregion + } + + + public interface IUserRepository + { + UsersModel GetUser(string userGuid); + UsersModel GetUserByUsername(string username); + Task GetUserAsync(string userguid); + IEnumerable Custom(Func> func); + long Insert(UsersModel entity); + void Delete(UsersModel entity); + } +} + diff --git a/PlexRequests.Store/SqlTables.sql b/PlexRequests.Store/SqlTables.sql index c0cf01231..31b0a73d5 100644 --- a/PlexRequests.Store/SqlTables.sql +++ b/PlexRequests.Store/SqlTables.sql @@ -9,7 +9,8 @@ CREATE TABLE IF NOT EXISTS Users Hash BLOB NOT NULL, UserProperties BLOB, Permissions INTEGER, - Features INTEGER + Features INTEGER, + Claims BLOB ); CREATE TABLE IF NOT EXISTS UserLogins diff --git a/PlexRequests.UI/Modules/BaseModule.cs b/PlexRequests.UI/Modules/BaseModule.cs index a01253786..99b34358b 100644 --- a/PlexRequests.UI/Modules/BaseModule.cs +++ b/PlexRequests.UI/Modules/BaseModule.cs @@ -30,10 +30,13 @@ using System.Linq; using System.Threading; using Nancy; - +using Ninject; using PlexRequests.Core; using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; +using PlexRequests.Helpers.Permissions; +using PlexRequests.Store; +using PlexRequests.Store.Repository; using PlexRequests.UI.Helpers; using PlexRequests.UI.Models; @@ -118,6 +121,10 @@ namespace PlexRequests.UI.Modules protected IDictionary Cookies => Request?.Cookies; + // This is not ideal, but it's cleaner than having to pass it down through each module. + [Inject] + protected IUserRepository UserRepository { get; set; } + protected bool IsAdmin { get @@ -126,8 +133,14 @@ namespace PlexRequests.UI.Modules { return false; } - var claims = Context?.CurrentUser.Claims.ToList(); - return claims.Contains(UserClaims.Admin) || claims.Contains(UserClaims.PowerUser); + + var user = UserRepository.GetUserByUsername(Context?.CurrentUser?.UserName); + + if (user == null) return false; + + var permissions = (Permissions) user.Permissions; + return permissions.HasFlag(Permissions.Administrator); + // TODO: Check admin role } } diff --git a/PlexRequests.UI/NinjectModules/RepositoryModule.cs b/PlexRequests.UI/NinjectModules/RepositoryModule.cs index 850926f74..66b7f6695 100644 --- a/PlexRequests.UI/NinjectModules/RepositoryModule.cs +++ b/PlexRequests.UI/NinjectModules/RepositoryModule.cs @@ -46,6 +46,8 @@ namespace PlexRequests.UI.NinjectModules Bind().To(); Bind().To(); Bind().To(); + + Bind().To(); } } From e46c43610f650f3a9cf97b181d8749986ac130d8 Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Mon, 7 Nov 2016 14:29:13 +0000 Subject: [PATCH 04/87] Fixed #656 and more work on #218 --- .../Permissions/Permissions.cs | 5 +- .../PlexRequests.Helpers.csproj | 4 + PlexRequests.Helpers/packages.config | 1 + .../Repository/UserRepository.cs | 6 +- PlexRequests.UI/Helpers/HtmlSecurityHelper.cs | 25 +++- PlexRequests.UI/Helpers/SecurityExtensions.cs | 120 ++++++++++-------- PlexRequests.UI/Modules/AdminModule.cs | 6 +- PlexRequests.UI/Modules/BaseModule.cs | 25 +++- PlexRequests.UI/Modules/SearchModule.cs | 5 +- PlexRequests.UI/Startup.cs | 3 +- PlexRequests.UI/Views/Admin/Settings.cshtml | 59 ++++----- 11 files changed, 159 insertions(+), 100 deletions(-) diff --git a/PlexRequests.Helpers/Permissions/Permissions.cs b/PlexRequests.Helpers/Permissions/Permissions.cs index 3d5155ad0..b0c28db36 100644 --- a/PlexRequests.Helpers/Permissions/Permissions.cs +++ b/PlexRequests.Helpers/Permissions/Permissions.cs @@ -46,6 +46,9 @@ namespace PlexRequests.Helpers.Permissions RequestMusic = 8, [Display(Name = "Report Issue")] - ReportIssue = 16 + ReportIssue = 16, + + [Display(Name = "Read Only User")] + ReadOnlyUser = 32, } } \ No newline at end of file diff --git a/PlexRequests.Helpers/PlexRequests.Helpers.csproj b/PlexRequests.Helpers/PlexRequests.Helpers.csproj index 38c703c97..90aac0c64 100644 --- a/PlexRequests.Helpers/PlexRequests.Helpers.csproj +++ b/PlexRequests.Helpers/PlexRequests.Helpers.csproj @@ -39,6 +39,10 @@ ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll True + + ..\packages\Ninject.3.2.0.0\lib\net45-full\Ninject.dll + True + ..\packages\NLog.4.3.6\lib\net45\NLog.dll True diff --git a/PlexRequests.Helpers/packages.config b/PlexRequests.Helpers/packages.config index 97b45cbe0..db4648bf2 100644 --- a/PlexRequests.Helpers/packages.config +++ b/PlexRequests.Helpers/packages.config @@ -2,6 +2,7 @@ + \ No newline at end of file diff --git a/PlexRequests.Store/Repository/UserRepository.cs b/PlexRequests.Store/Repository/UserRepository.cs index 56ad461cb..b05609c82 100644 --- a/PlexRequests.Store/Repository/UserRepository.cs +++ b/PlexRequests.Store/Repository/UserRepository.cs @@ -49,21 +49,21 @@ namespace PlexRequests.Store.Repository public UsersModel GetUser(string userGuid) { - var sql = @"SELECT * FROM UsersModel + var sql = @"SELECT * FROM Users WHERE Userguid = @UserGuid"; return Db.QueryFirstOrDefault(sql, new {UserGuid = userGuid}); } public UsersModel GetUserByUsername(string username) { - var sql = @"SELECT * FROM UsersModel + var sql = @"SELECT * FROM Users WHERE UserName = @UserName"; return Db.QueryFirstOrDefault(sql, new {UserName = username}); } public async Task GetUserAsync(string userguid) { - var sql = @"SELECT * FROM UsersModel + var sql = @"SELECT * FROM Users WHERE UserGuid = @UserGuid"; return await Db.QueryFirstOrDefaultAsync(sql, new {UserGuid = userguid}); } diff --git a/PlexRequests.UI/Helpers/HtmlSecurityHelper.cs b/PlexRequests.UI/Helpers/HtmlSecurityHelper.cs index bd08b772b..3d1751257 100644 --- a/PlexRequests.UI/Helpers/HtmlSecurityHelper.cs +++ b/PlexRequests.UI/Helpers/HtmlSecurityHelper.cs @@ -27,23 +27,36 @@ using Nancy.Security; using Nancy.ViewEngines.Razor; +using Ninject; +using PlexRequests.Helpers.Permissions; +using PlexRequests.Store.Repository; namespace PlexRequests.UI.Helpers { public static class HtmlSecurityHelper { - public static bool HasAnyPermission(this HtmlHelpers helper, params string[] claims) + private static SecurityExtensions Security { - if (!helper.CurrentUser.IsAuthenticated()) + + get { - return false; + var userRepo = ServiceLocator.Instance.Resolve(); + return _security ?? (_security = new SecurityExtensions(userRepo, null)); } - return helper.CurrentUser.HasAnyClaim(claims); } - public static bool DoesNotHaveAnyPermission(this HtmlHelpers helper, params string[] claims) + private static SecurityExtensions _security; + + + public static bool HasAnyPermission(this HtmlHelpers helper, int permission) { - return SecurityExtensions.DoesNotHaveClaims(claims, helper.CurrentUser); + return helper.CurrentUser.IsAuthenticated() + && Security.HasPermissions(helper.CurrentUser, (Permissions) permission); + } + + public static bool DoesNotHavePermission(this HtmlHelpers helper, int permission) + { + return Security.DoesNotHavePermissions(permission, helper.CurrentUser); } } } \ No newline at end of file diff --git a/PlexRequests.UI/Helpers/SecurityExtensions.cs b/PlexRequests.UI/Helpers/SecurityExtensions.cs index bf43935d6..36f01a1a5 100644 --- a/PlexRequests.UI/Helpers/SecurityExtensions.cs +++ b/PlexRequests.UI/Helpers/SecurityExtensions.cs @@ -31,14 +31,25 @@ using System.Linq; using Nancy; using Nancy.Extensions; using Nancy.Security; +using Ninject; +using PlexRequests.Helpers.Permissions; +using PlexRequests.Store.Repository; using PlexRequests.UI.Models; namespace PlexRequests.UI.Helpers { - public static class SecurityExtensions + public class SecurityExtensions { + public SecurityExtensions(IUserRepository userRepository, NancyModule context) + { + UserRepository = userRepository; + Module = context; + } + + private IUserRepository UserRepository { get; } + private NancyModule Module { get; } - public static bool IsLoggedIn(this NancyContext context) + public bool IsLoggedIn(NancyContext context) { var userName = context.Request.Session[SessionKeys.UsernameKey]; var realUser = false; @@ -52,7 +63,7 @@ namespace PlexRequests.UI.Helpers return realUser || plexUser; } - public static bool IsPlexUser(this NancyContext context) + public bool IsPlexUser(NancyContext context) { var userName = context.Request.Session[SessionKeys.UsernameKey]; var plexUser = userName != null; @@ -62,7 +73,7 @@ namespace PlexRequests.UI.Helpers return plexUser && !isAuth; } - public static bool IsNormalUser(this NancyContext context) + public bool IsNormalUser(NancyContext context) { var userName = context.Request.Session[SessionKeys.UsernameKey]; var plexUser = userName != null; @@ -72,63 +83,72 @@ namespace PlexRequests.UI.Helpers return isAuth && !plexUser; } - /// - /// This module requires authentication and NO certain claims to be present. - /// - /// Module to enable - /// Claim(s) required - public static void DoesNotHaveClaim(this INancyModule module, params string[] bannedClaims) - { - module.AddBeforeHookOrExecute(SecurityHooks.RequiresAuthentication(), "Requires Authentication"); - module.AddBeforeHookOrExecute(DoesNotHaveClaims(bannedClaims), "Has Banned Claims"); - } - - public static bool DoesNotHaveClaimCheck(this INancyModule module, params string[] bannedClaims) - { - if (!module.Context?.CurrentUser?.IsAuthenticated() ?? false) - { - return false; - } - if (DoesNotHaveClaims(bannedClaims, module.Context)) - { - return false; - } - return true; - } - - public static bool DoesNotHaveClaimCheck(this NancyContext context, params string[] bannedClaims) - { - if (!context?.CurrentUser?.IsAuthenticated() ?? false) - { - return false; - } - if (DoesNotHaveClaims(bannedClaims, context)) - { - return false; - } - return true; - } /// /// Creates a hook to be used in a pipeline before a route handler to ensure /// that the request was made by an authenticated user does not have the claims. /// - /// Claims the authenticated user needs to have + /// Claims the authenticated user needs to have /// Hook that returns an Unauthorized response if the user is not /// authenticated or does have the claims, null otherwise - private static Func DoesNotHaveClaims(IEnumerable claims) - { - return ForbiddenIfNot(ctx => !ctx.CurrentUser.HasAnyClaim(claims)); + private Func DoesNotHavePermissions(int perm) + { + return ForbiddenIfNot(ctx => + { + var dbUser = UserRepository.GetUserByUsername(ctx.CurrentUser.UserName); + + if (dbUser == null) return false; + + var permissions = (Permissions)dbUser.Permissions; + var result = permissions.HasFlag((Permissions)perm); + return !result; + }); } - public static bool DoesNotHaveClaims(IEnumerable claims, NancyContext ctx) + public bool DoesNotHavePermissions(int perm, IUserIdentity currentUser) { - return !ctx.CurrentUser.HasAnyClaim(claims); + return DoesNotHavePermissions((Permissions) perm, currentUser); } - public static bool DoesNotHaveClaims(IEnumerable claims, IUserIdentity identity) + public bool DoesNotHavePermissions(Permissions perm, IUserIdentity currentUser) { - return !identity?.HasAnyClaim(claims) ?? true; + var dbUser = UserRepository.GetUserByUsername(currentUser.UserName); + + if (dbUser == null) return false; + + var permissions = (Permissions)dbUser.Permissions; + var result = permissions.HasFlag((Permissions)perm); + return !result; + } + + public bool HasPermissions(IUserIdentity user, Permissions perm) + { + if (user == null) return false; + + var dbUser = UserRepository.GetUserByUsername(user.UserName); + + if (dbUser == null) return false; + + var permissions = (Permissions)dbUser.Permissions; + var result = permissions.HasFlag(perm); + return result; + } + + public void HasPermissionsResponse(Permissions perm) + { + Module.AddBeforeHookOrExecute( + ForbiddenIfNot(ctx => + { + if (ctx.CurrentUser == null) return false; + + var dbUser = UserRepository.GetUserByUsername(ctx.CurrentUser.UserName); + + if (dbUser == null) return false; + + var permissions = (Permissions)dbUser.Permissions; + var result = permissions.HasFlag(perm); + return result; + }), "Requires Claims"); } @@ -140,7 +160,7 @@ namespace PlexRequests.UI.Helpers /// /// Test that must return true for the request to continue /// Hook that returns an Forbidden response if the test fails, null otherwise - private static Func ForbiddenIfNot(Func test) + public Func ForbiddenIfNot(Func test) { return HttpStatusCodeIfNot(HttpStatusCode.Forbidden, test); } @@ -152,7 +172,7 @@ namespace PlexRequests.UI.Helpers /// HttpStatusCode to use for the response /// Test that must return true for the request to continue /// Hook that returns a response with a specific HttpStatusCode if the test fails, null otherwise - private static Func HttpStatusCodeIfNot(HttpStatusCode statusCode, Func test) + public Func HttpStatusCodeIfNot(HttpStatusCode statusCode, Func test) { return ctx => { diff --git a/PlexRequests.UI/Modules/AdminModule.cs b/PlexRequests.UI/Modules/AdminModule.cs index 9caffcfde..efe797f24 100644 --- a/PlexRequests.UI/Modules/AdminModule.cs +++ b/PlexRequests.UI/Modules/AdminModule.cs @@ -56,6 +56,7 @@ using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; using PlexRequests.Helpers.Analytics; using PlexRequests.Helpers.Exceptions; +using PlexRequests.Helpers.Permissions; using PlexRequests.Services.Interfaces; using PlexRequests.Services.Jobs; using PlexRequests.Services.Notification; @@ -153,7 +154,7 @@ namespace PlexRequests.UI.Modules NotifySettings = notifyService; RecentlyAdded = recentlyAdded; - this.RequiresClaims(UserClaims.Admin); + Security.HasPermissionsResponse(Permissions.Administrator); Get["/"] = _ => Admin(); @@ -849,7 +850,8 @@ namespace PlexRequests.UI.Modules private Response CreateApiKey() { - this.RequiresClaims(UserClaims.Admin); + Security.HasPermissionsResponse(Permissions.Administrator); + Analytics.TrackEventAsync(Category.Admin, Action.Create, "Created API Key", Username, CookieHelper.GetAnalyticClientId(Cookies)); var apiKey = Guid.NewGuid().ToString("N"); var settings = PrService.GetSettings(); diff --git a/PlexRequests.UI/Modules/BaseModule.cs b/PlexRequests.UI/Modules/BaseModule.cs index 99b34358b..35bd4817f 100644 --- a/PlexRequests.UI/Modules/BaseModule.cs +++ b/PlexRequests.UI/Modules/BaseModule.cs @@ -30,6 +30,7 @@ using System.Linq; using System.Threading; using Nancy; +using Nancy.Security; using Ninject; using PlexRequests.Core; using PlexRequests.Core.SettingModels; @@ -121,10 +122,6 @@ namespace PlexRequests.UI.Modules protected IDictionary Cookies => Request?.Cookies; - // This is not ideal, but it's cleaner than having to pass it down through each module. - [Inject] - protected IUserRepository UserRepository { get; set; } - protected bool IsAdmin { get @@ -134,7 +131,9 @@ namespace PlexRequests.UI.Modules return false; } - var user = UserRepository.GetUserByUsername(Context?.CurrentUser?.UserName); + var userRepo = ServiceLocator.Instance.Resolve(); + + var user = userRepo.GetUserByUsername(Context?.CurrentUser?.UserName); if (user == null) return false; @@ -144,6 +143,22 @@ namespace PlexRequests.UI.Modules } } + protected IUserIdentity User => Context?.CurrentUser; + + protected SecurityExtensions Security + { + + get + { + var userRepo = ServiceLocator.Instance.Resolve(); + return _security ?? (_security = new SecurityExtensions(userRepo, this)); + } + } + + private SecurityExtensions _security; + + + protected bool LoggedIn => Context?.CurrentUser != null; protected string Culture { get; set; } diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index 35dfeea65..e10e5b92d 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -55,6 +55,7 @@ using PlexRequests.Api.Models.Sonarr; using PlexRequests.Api.Models.Tv; using PlexRequests.Core.Models; using PlexRequests.Helpers.Analytics; +using PlexRequests.Helpers.Permissions; using PlexRequests.Store.Models; using PlexRequests.Store.Repository; @@ -444,7 +445,7 @@ namespace PlexRequests.UI.Modules private async Task RequestMovie(int movieId) { - if (this.DoesNotHaveClaimCheck(UserClaims.ReadOnlyUser)) + if (Security.DoesNotHavePermissions(Permissions.ReadOnlyUser, User)) { return Response.AsJson(new JsonResponseModel() @@ -553,7 +554,7 @@ namespace PlexRequests.UI.Modules /// private async Task RequestTvShow(int showId, string seasons) { - if (this.DoesNotHaveClaimCheck(UserClaims.ReadOnlyUser)) + if (Security.DoesNotHavePermissions(Permissions.ReadOnlyUser, User)) { return Response.AsJson(new JsonResponseModel() diff --git a/PlexRequests.UI/Startup.cs b/PlexRequests.UI/Startup.cs index 1917ccc53..023e0b912 100644 --- a/PlexRequests.UI/Startup.cs +++ b/PlexRequests.UI/Startup.cs @@ -33,7 +33,6 @@ using NLog; using Owin; using PlexRequests.Core.Migration; -using PlexRequests.Services.Jobs; using PlexRequests.UI.Helpers; using PlexRequests.UI.Jobs; using PlexRequests.UI.NinjectModules; @@ -57,7 +56,7 @@ namespace PlexRequests.UI Debug.WriteLine("Modules found finished."); - var kernel = new StandardKernel(modules); + var kernel = new StandardKernel(new NinjectSettings { InjectNonPublic = true }, modules); Debug.WriteLine("Created Kernel and Injected Modules"); Debug.WriteLine("Added Contravariant Binder"); diff --git a/PlexRequests.UI/Views/Admin/Settings.cshtml b/PlexRequests.UI/Views/Admin/Settings.cshtml index acebf8e31..8f0edb028 100644 --- a/PlexRequests.UI/Views/Admin/Settings.cshtml +++ b/PlexRequests.UI/Views/Admin/Settings.cshtml @@ -57,7 +57,8 @@
- + +
@@ -65,16 +66,16 @@
-
- -
- +
+ +
+ +
-
-
+
@if (Model.SearchForMovies) @@ -192,7 +193,7 @@ }
- +
@@ -278,12 +279,12 @@
- -

A comma separated list of users whose requests do not require approval (These users also do not have a request limit).

+ +

A comma separated list of users whose requests do not require approval (These users also do not have a request limit).

@@ -293,23 +294,23 @@

If the request limits are set to 0 then no request limit is applied.

-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
@@ -320,7 +321,7 @@
-
+
@@ -373,7 +374,7 @@ success: function(response) { if (response) { generateNotify("Success!", "success"); - $('#apiKey').val(response); + $('#ApiKey').val(response); } }, error: function(e) { From ac02d24d657604a8898ed7e2df8c17ef2985d28f Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Mon, 7 Nov 2016 14:33:48 +0000 Subject: [PATCH 05/87] #544 --- PlexRequests.UI/Modules/AdminModule.cs | 10 +++++++--- PlexRequests.UI/Views/Admin/Plex.cshtml | 22 +++++++--------------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/PlexRequests.UI/Modules/AdminModule.cs b/PlexRequests.UI/Modules/AdminModule.cs index efe797f24..e48cd1fb7 100644 --- a/PlexRequests.UI/Modules/AdminModule.cs +++ b/PlexRequests.UI/Modules/AdminModule.cs @@ -404,9 +404,13 @@ namespace PlexRequests.UI.Modules return Response.AsJson(valid.SendJsonError()); } - //Lookup identifier - var server = PlexApi.GetServer(plexSettings.PlexAuthToken); - plexSettings.MachineIdentifier = server.Server.FirstOrDefault(x => x.AccessToken == plexSettings.PlexAuthToken)?.MachineIdentifier; + if (string.IsNullOrEmpty(plexSettings.MachineIdentifier)) + { + //Lookup identifier + var server = PlexApi.GetServer(plexSettings.PlexAuthToken); + plexSettings.MachineIdentifier = + server.Server.FirstOrDefault(x => x.AccessToken == plexSettings.PlexAuthToken)?.MachineIdentifier; + } var result = await PlexService.SaveSettingsAsync(plexSettings); diff --git a/PlexRequests.UI/Views/Admin/Plex.cshtml b/PlexRequests.UI/Views/Admin/Plex.cshtml index 67700e59f..c2ce7be25 100644 --- a/PlexRequests.UI/Views/Admin/Plex.cshtml +++ b/PlexRequests.UI/Views/Admin/Plex.cshtml @@ -87,21 +87,6 @@
- - @*
-
-
- -
-
*@
@@ -110,6 +95,13 @@
+
+ +
+ +
+
+
From 367ca391b1017ef1ebdb8c005f76c420f4d6ac62 Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Mon, 7 Nov 2016 14:46:44 +0000 Subject: [PATCH 06/87] Started on #483 --- PlexRequests.Core/PlexRequests.Core.csproj | 1 + .../Queue/TransientFaultQueue.cs | 87 +++++++++++++++++++ PlexRequests.Store/Models/RequestQueue.cs | 42 +++++++++ PlexRequests.Store/PlexRequests.Store.csproj | 1 + .../Repository/BaseGenericRepository.cs | 17 ++++ PlexRequests.Store/Repository/IRepository.cs | 1 + PlexRequests.Store/SqlTables.sql | 11 +++ 7 files changed, 160 insertions(+) create mode 100644 PlexRequests.Core/Queue/TransientFaultQueue.cs create mode 100644 PlexRequests.Store/Models/RequestQueue.cs diff --git a/PlexRequests.Core/PlexRequests.Core.csproj b/PlexRequests.Core/PlexRequests.Core.csproj index 7fdb4b375..4def9282d 100644 --- a/PlexRequests.Core/PlexRequests.Core.csproj +++ b/PlexRequests.Core/PlexRequests.Core.csproj @@ -99,6 +99,7 @@ + diff --git a/PlexRequests.Core/Queue/TransientFaultQueue.cs b/PlexRequests.Core/Queue/TransientFaultQueue.cs new file mode 100644 index 000000000..e3621ee0f --- /dev/null +++ b/PlexRequests.Core/Queue/TransientFaultQueue.cs @@ -0,0 +1,87 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: TransientFaultQueue.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System.Collections.Generic; +using System.Threading.Tasks; +using PlexRequests.Helpers; +using PlexRequests.Store; +using PlexRequests.Store.Models; +using PlexRequests.Store.Repository; + +namespace PlexRequests.Core.Queue +{ + public class TransientFaultQueue + { + public TransientFaultQueue(IRepository queue) + { + RequestQueue = queue; + } + + private IRepository RequestQueue { get; } + + + public void QueueItem(RequestedModel request, RequestType type) + { + var queue = new RequestQueue + { + Type = type, + Content = ByteConverterHelper.ReturnBytes(request), + PrimaryIdentifier = request.ProviderId + }; + RequestQueue.Insert(queue); + } + + public async Task QueueItemAsync(RequestedModel request, RequestType type) + { + var queue = new RequestQueue + { + Type = type, + Content = ByteConverterHelper.ReturnBytes(request), + PrimaryIdentifier = request.ProviderId + }; + await RequestQueue.InsertAsync(queue); + } + + public IEnumerable Dequeue() + { + var items = RequestQueue.GetAll(); + + RequestQueue.DeleteAll("RequestQueue"); + + return items; + } + + public async Task> DequeueAsync() + { + var items = RequestQueue.GetAllAsync(); + + await RequestQueue.DeleteAllAsync("RequestQueue"); + + return await items; + } + } +} \ No newline at end of file diff --git a/PlexRequests.Store/Models/RequestQueue.cs b/PlexRequests.Store/Models/RequestQueue.cs new file mode 100644 index 000000000..bdbf25a2f --- /dev/null +++ b/PlexRequests.Store/Models/RequestQueue.cs @@ -0,0 +1,42 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: RequestQueue.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using Dapper.Contrib.Extensions; + +namespace PlexRequests.Store.Models +{ + [Table("RequestQueue")] + public class RequestQueue : Entity + { + public int PrimaryIdentifier { get; set; } + + public RequestType Type { get; set; } + + public byte[] Content { get; set; } + + } +} \ No newline at end of file diff --git a/PlexRequests.Store/PlexRequests.Store.csproj b/PlexRequests.Store/PlexRequests.Store.csproj index 961b6d4b5..c1310e1ce 100644 --- a/PlexRequests.Store/PlexRequests.Store.csproj +++ b/PlexRequests.Store/PlexRequests.Store.csproj @@ -69,6 +69,7 @@ + diff --git a/PlexRequests.Store/Repository/BaseGenericRepository.cs b/PlexRequests.Store/Repository/BaseGenericRepository.cs index 9cf67a2bb..9cafca7fd 100644 --- a/PlexRequests.Store/Repository/BaseGenericRepository.cs +++ b/PlexRequests.Store/Repository/BaseGenericRepository.cs @@ -327,5 +327,22 @@ namespace PlexRequests.Store.Repository throw; } } + public async Task DeleteAllAsync(string tableName) + { + try + { + ResetCache(); + using (var db = Config.DbConnection()) + { + db.Open(); + await db.ExecuteAsync($"delete from {tableName}"); + } + } + catch (SqliteException e) when (e.ErrorCode == SQLiteErrorCode.Corrupt) + { + Log.Fatal(CorruptMessage); + throw; + } + } } } \ No newline at end of file diff --git a/PlexRequests.Store/Repository/IRepository.cs b/PlexRequests.Store/Repository/IRepository.cs index 8ab6037d2..4967d6111 100644 --- a/PlexRequests.Store/Repository/IRepository.cs +++ b/PlexRequests.Store/Repository/IRepository.cs @@ -85,5 +85,6 @@ namespace PlexRequests.Store.Repository IEnumerable Custom(Func> func); Task> CustomAsync(Func>> func); void DeleteAll(string tableName); + Task DeleteAllAsync(string tableName); } } diff --git a/PlexRequests.Store/SqlTables.sql b/PlexRequests.Store/SqlTables.sql index 31b0a73d5..a22258a5d 100644 --- a/PlexRequests.Store/SqlTables.sql +++ b/PlexRequests.Store/SqlTables.sql @@ -131,4 +131,15 @@ CREATE TABLE IF NOT EXISTS PlexEpisodes ); CREATE UNIQUE INDEX IF NOT EXISTS PlexEpisodes_Id ON PlexEpisodes (Id); CREATE INDEX IF NOT EXISTS PlexEpisodes_ProviderId ON PlexEpisodes (ProviderId); + + +CREATE TABLE IF NOT EXISTS RequestQueue +( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + PrimaryIdentifier INTEGER NOT NULL, + Type INTEGER NOT NULL, + Content BLOB NOT NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS PlexUsers_Id ON PlexUsers (Id); + COMMIT; \ No newline at end of file From 0950f202785cb2358e29e86395e660fc59fa854a Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Mon, 7 Nov 2016 17:21:05 +0000 Subject: [PATCH 07/87] Almost finished #659 --- PlexRequests.Core/IStatusChecker.cs | 11 ++ PlexRequests.Core/PlexRequests.Core.csproj | 10 +- .../SettingModels/SystemSettings.cs | 38 +++++ PlexRequests.Core/StatusChecker.cs | 83 ---------- .../StatusChecker/AppveyorArtifactResult.cs | 35 ++++ .../StatusChecker/AppveyorBranchResult.cs | 138 ++++++++++++++++ .../StatusChecker/StatusChecker.cs | 153 ++++++++++++++++++ PlexRequests.Core/packages.config | 1 + .../Modules/{ => Admin}/AdminModule.cs | 25 +-- .../Modules/Admin/SystemStatusModule.cs | 100 ++++++++++++ .../Modules/UpdateCheckerModule.cs | 7 +- .../NinjectModules/ConfigurationModule.cs | 3 + PlexRequests.UI/PlexRequests.UI.csproj | 3 +- PlexRequests.UI/Views/Admin/Status.cshtml | 55 ++++++- 14 files changed, 543 insertions(+), 119 deletions(-) create mode 100644 PlexRequests.Core/IStatusChecker.cs create mode 100644 PlexRequests.Core/SettingModels/SystemSettings.cs delete mode 100644 PlexRequests.Core/StatusChecker.cs create mode 100644 PlexRequests.Core/StatusChecker/AppveyorArtifactResult.cs create mode 100644 PlexRequests.Core/StatusChecker/AppveyorBranchResult.cs create mode 100644 PlexRequests.Core/StatusChecker/StatusChecker.cs rename PlexRequests.UI/Modules/{ => Admin}/AdminModule.cs (97%) create mode 100644 PlexRequests.UI/Modules/Admin/SystemStatusModule.cs diff --git a/PlexRequests.Core/IStatusChecker.cs b/PlexRequests.Core/IStatusChecker.cs new file mode 100644 index 000000000..eec365b68 --- /dev/null +++ b/PlexRequests.Core/IStatusChecker.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Octokit; +using PlexRequests.Core.Models; + +namespace PlexRequests.Core +{ + public interface IStatusChecker + { + Task GetStatus(); + } +} \ No newline at end of file diff --git a/PlexRequests.Core/PlexRequests.Core.csproj b/PlexRequests.Core/PlexRequests.Core.csproj index 4def9282d..064f319e3 100644 --- a/PlexRequests.Core/PlexRequests.Core.csproj +++ b/PlexRequests.Core/PlexRequests.Core.csproj @@ -54,6 +54,10 @@ ..\packages\Quartz.2.3.3\lib\net40\Quartz.dll True + + ..\packages\RestSharp.105.2.3\lib\net45\RestSharp.dll + True + @@ -82,6 +86,7 @@ + @@ -120,9 +125,12 @@ + - + + + diff --git a/PlexRequests.Core/SettingModels/SystemSettings.cs b/PlexRequests.Core/SettingModels/SystemSettings.cs new file mode 100644 index 000000000..fe60baabc --- /dev/null +++ b/PlexRequests.Core/SettingModels/SystemSettings.cs @@ -0,0 +1,38 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: SystemSettings.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using PlexRequests.Core.Models; + +namespace PlexRequests.Core.SettingModels +{ + public class SystemSettings : Settings + { + public bool UseEarlyAccessPreviewBuilds { get; set; } + + public StatusModel Status { get; set; } + } +} \ No newline at end of file diff --git a/PlexRequests.Core/StatusChecker.cs b/PlexRequests.Core/StatusChecker.cs deleted file mode 100644 index bf9f0485e..000000000 --- a/PlexRequests.Core/StatusChecker.cs +++ /dev/null @@ -1,83 +0,0 @@ -#region Copyright -// /************************************************************************ -// Copyright (c) 2016 Jamie Rees -// File: StatusChecker.cs -// Created By: Jamie Rees -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// ************************************************************************/ -#endregion -using System; -using System.Linq; -using System.Threading.Tasks; - -using Octokit; - -using PlexRequests.Core.Models; -using PlexRequests.Helpers; - -namespace PlexRequests.Core -{ - public class StatusChecker - { - public StatusChecker() - { - Git = new GitHubClient(new ProductHeaderValue("PlexRequests-StatusChecker")); - } - private IGitHubClient Git { get; } - private const string Owner = "tidusjar"; - private const string RepoName = "PlexRequests.Net"; - - public async Task GetLatestRelease() - { - var releases = await Git.Repository.Release.GetAll(Owner, RepoName); - return releases.FirstOrDefault(); - } - - public async Task GetStatus() - { - var assemblyVersion = AssemblyHelper.GetProductVersion(); - var model = new StatusModel - { - Version = assemblyVersion, - }; - - var latestRelease = await GetLatestRelease(); - if (latestRelease == null) - { - return new StatusModel { Version = "Unknown" }; - } - var latestVersionArray = latestRelease.Name.Split(new[] { 'v' }, StringSplitOptions.RemoveEmptyEntries); - var latestVersion = latestVersionArray.Length > 1 ? latestVersionArray[1] : string.Empty; - - if (!latestVersion.Equals(assemblyVersion, StringComparison.InvariantCultureIgnoreCase)) - { - model.UpdateAvailable = true; - model.UpdateUri = latestRelease.HtmlUrl; - } - - model.ReleaseNotes = latestRelease.Body; - model.DownloadUri = latestRelease.Assets[0].BrowserDownloadUrl; - model.ReleaseTitle = latestRelease.Name; - - return model; - } - } -} \ No newline at end of file diff --git a/PlexRequests.Core/StatusChecker/AppveyorArtifactResult.cs b/PlexRequests.Core/StatusChecker/AppveyorArtifactResult.cs new file mode 100644 index 000000000..9e67262d6 --- /dev/null +++ b/PlexRequests.Core/StatusChecker/AppveyorArtifactResult.cs @@ -0,0 +1,35 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: AppveyorArtifactResult.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +namespace PlexRequests.Core.StatusChecker +{ + public class AppveyorArtifactResult + { + public string fileName { get; set; } + public string type { get; set; } + public int size { get; set; } + } +} \ No newline at end of file diff --git a/PlexRequests.Core/StatusChecker/AppveyorBranchResult.cs b/PlexRequests.Core/StatusChecker/AppveyorBranchResult.cs new file mode 100644 index 000000000..766e5a804 --- /dev/null +++ b/PlexRequests.Core/StatusChecker/AppveyorBranchResult.cs @@ -0,0 +1,138 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: AppveyorBranchResult.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System.Collections.Generic; + +namespace PlexRequests.Core.StatusChecker +{ + public class NuGetFeed + { + public string id { get; set; } + public string name { get; set; } + public bool publishingEnabled { get; set; } + public string created { get; set; } + } + + public class AccessRightDefinition + { + public string name { get; set; } + public string description { get; set; } + } + + public class AccessRight + { + public string name { get; set; } + public bool allowed { get; set; } + } + + public class RoleAce + { + public int roleId { get; set; } + public string name { get; set; } + public bool isAdmin { get; set; } + public List accessRights { get; set; } + } + + public class SecurityDescriptor + { + public List accessRightDefinitions { get; set; } + public List roleAces { get; set; } + } + + public class Project + { + public int projectId { get; set; } + public int accountId { get; set; } + public string accountName { get; set; } + public List builds { get; set; } + public string name { get; set; } + public string slug { get; set; } + public string repositoryType { get; set; } + public string repositoryScm { get; set; } + public string repositoryName { get; set; } + public string repositoryBranch { get; set; } + public bool isPrivate { get; set; } + public bool skipBranchesWithoutAppveyorYml { get; set; } + public bool enableSecureVariablesInPullRequests { get; set; } + public bool enableSecureVariablesInPullRequestsFromSameRepo { get; set; } + public bool enableDeploymentInPullRequests { get; set; } + public bool rollingBuilds { get; set; } + public bool alwaysBuildClosedPullRequests { get; set; } + public string tags { get; set; } + public NuGetFeed nuGetFeed { get; set; } + public SecurityDescriptor securityDescriptor { get; set; } + public string created { get; set; } + public string updated { get; set; } + } + + public class Job + { + public string jobId { get; set; } + public string name { get; set; } + public bool allowFailure { get; set; } + public int messagesCount { get; set; } + public int compilationMessagesCount { get; set; } + public int compilationErrorsCount { get; set; } + public int compilationWarningsCount { get; set; } + public int testsCount { get; set; } + public int passedTestsCount { get; set; } + public int failedTestsCount { get; set; } + public int artifactsCount { get; set; } + public string status { get; set; } + public string started { get; set; } + public string finished { get; set; } + public string created { get; set; } + public string updated { get; set; } + } + + public class Build + { + public int buildId { get; set; } + public List jobs { get; set; } + public int buildNumber { get; set; } + public string version { get; set; } + public string message { get; set; } + public string branch { get; set; } + public bool isTag { get; set; } + public string commitId { get; set; } + public string authorName { get; set; } + public string committerName { get; set; } + public string committed { get; set; } + public List messages { get; set; } + public string status { get; set; } + public string started { get; set; } + public string finished { get; set; } + public string created { get; set; } + public string updated { get; set; } + } + + public class AppveyorBranchResult + { + public Project project { get; set; } + public Build build { get; set; } + } +} \ No newline at end of file diff --git a/PlexRequests.Core/StatusChecker/StatusChecker.cs b/PlexRequests.Core/StatusChecker/StatusChecker.cs new file mode 100644 index 000000000..fcf41f65b --- /dev/null +++ b/PlexRequests.Core/StatusChecker/StatusChecker.cs @@ -0,0 +1,153 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: StatusChecker.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Octokit; +using PlexRequests.Api; +using PlexRequests.Core.Models; +using PlexRequests.Core.SettingModels; +using PlexRequests.Helpers; +using RestSharp; + +namespace PlexRequests.Core.StatusChecker +{ + public class StatusChecker : IStatusChecker + { + public StatusChecker(ISettingsService ss) + { + SystemSettings = ss; + Git = new GitHubClient(new ProductHeaderValue("PlexRequests-StatusChecker")); + } + + private ISettingsService SystemSettings { get; } + + private IGitHubClient Git { get; } + private const string Owner = "tidusjar"; + private const string RepoName = "PlexRequests.Net"; + private const string AppveyorApiUrl = "https://ci.appveyor.com/api"; + + private const string Api = + "48Ku58C0794nBrXra8IxWav+dc6NqgkRw+PZB3/bQwbt/D0IrnJQkgtjzo0bd6nkooLMKsC8M+Ab7jyBO+ROjY14VRuxffpDopX9r0iG/fjBl6mZVvqkm+VTDNstDtzp"; + + public async Task GetStatus() + { + var settings = await SystemSettings.GetSettingsAsync(); + var isEap = settings.UseEarlyAccessPreviewBuilds; + + if (isEap) + { + // Early Access Preview Releases + return GetLatestEapRelease(); + } + + // Stable releases + return await GetLatestGithubRelease(); + } + + private async Task GetLatestGithubRelease() + { + var assemblyVersion = AssemblyHelper.GetProductVersion(); + var model = new StatusModel + { + Version = assemblyVersion, + }; + + var releases = await Git.Repository.Release.GetAll(Owner, RepoName); + var latestRelease = releases.FirstOrDefault(); + + if (latestRelease == null) + { + return new StatusModel { Version = "Unknown" }; + } + var latestVersionArray = latestRelease.Name.Split(new[] { 'v' }, StringSplitOptions.RemoveEmptyEntries); + var latestVersion = latestVersionArray.Length > 1 ? latestVersionArray[1] : string.Empty; + + if (!latestVersion.Equals(assemblyVersion, StringComparison.InvariantCultureIgnoreCase)) + { + model.UpdateAvailable = true; + model.UpdateUri = latestRelease.HtmlUrl; + } + + model.ReleaseNotes = latestRelease.Body; + model.DownloadUri = latestRelease.Assets[0].BrowserDownloadUrl; + model.ReleaseTitle = latestRelease.Name; + + return model; + } + + private StatusModel GetLatestEapRelease() + { + var request = new ApiRequest(); + + // Get latest EAP Build + var eapBranchRequest = new RestRequest + { + Resource = "/projects/tidusjar/requestplex/branch/EAP", + Method = Method.GET + }; + + var api = StringCipher.Decrypt(Api,"Appveyor"); + eapBranchRequest.AddHeader("Authorization", $"Bearer {api}"); + eapBranchRequest.AddHeader("Content-Type", "application/json"); + + var branchResult = request.ExecuteJson(eapBranchRequest, new Uri(AppveyorApiUrl)); + + var jobId = branchResult.build.jobs.FirstOrDefault()?.jobId ?? string.Empty; + + if (string.IsNullOrEmpty(jobId)) + { + return new StatusModel {UpdateAvailable = false}; + } + + // Get artifacts from the EAP Build + var eapAtrifactRequest = new RestRequest + { + Resource = $"/buildjobs/{jobId}/artifacts", + Method = Method.GET + }; + eapAtrifactRequest.AddHeader("Authorization", $"Bearer {api}"); + eapAtrifactRequest.AddHeader("Content-Type", "application/json"); + + var artifactResult = request.ExecuteJson>(eapAtrifactRequest, new Uri(AppveyorApiUrl)).FirstOrDefault(); + + var downloadLink = $"{AppveyorApiUrl}/buildjobs/{jobId}/artifacts/{artifactResult.fileName}"; + + return new StatusModel + { + DownloadUri = downloadLink, + ReleaseNotes = "Early Access Preview (See recent commits for details)", + ReleaseTitle = "Plex Requests Early Access Preview", + Version = branchResult.build.version, + UpdateAvailable = true, + UpdateUri = downloadLink + }; + } + } +} \ No newline at end of file diff --git a/PlexRequests.Core/packages.config b/PlexRequests.Core/packages.config index 6f3ca4a6c..e8f81f5a9 100644 --- a/PlexRequests.Core/packages.config +++ b/PlexRequests.Core/packages.config @@ -8,5 +8,6 @@ + \ No newline at end of file diff --git a/PlexRequests.UI/Modules/AdminModule.cs b/PlexRequests.UI/Modules/Admin/AdminModule.cs similarity index 97% rename from PlexRequests.UI/Modules/AdminModule.cs rename to PlexRequests.UI/Modules/Admin/AdminModule.cs index e48cd1fb7..06fe01780 100644 --- a/PlexRequests.UI/Modules/AdminModule.cs +++ b/PlexRequests.UI/Modules/Admin/AdminModule.cs @@ -186,7 +186,6 @@ namespace PlexRequests.UI.Modules Get["/emailnotification"] = _ => EmailNotifications(); Post["/emailnotification"] = _ => SaveEmailNotifications(); Post["/testemailnotification", true] = async (x, ct) => await TestEmailNotifications(); - Get["/status", true] = async (x, ct) => await Status(); Get["/pushbulletnotification"] = _ => PushbulletNotifications(); Post["/pushbulletnotification"] = _ => SavePushbulletNotifications(); @@ -209,7 +208,7 @@ namespace PlexRequests.UI.Modules Post["/createapikey"] = x => CreateApiKey(); - Post["/autoupdate"] = x => AutoUpdate(); + Post["/testslacknotification", true] = async (x, ct) => await TestSlackNotification(); @@ -568,28 +567,6 @@ namespace PlexRequests.UI.Modules : new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." }); } - private async Task Status() - { - var checker = new StatusChecker(); - var status = await Cache.GetOrSetAsync(CacheKeys.LastestProductVersion, async () => await checker.GetStatus(), 30); - var md = new Markdown(new MarkdownOptions { AutoNewLines = true, AutoHyperlink = true }); - status.ReleaseNotes = md.Transform(status.ReleaseNotes); - return View["Status", status]; - } - - private Response AutoUpdate() - { - var url = Request.Form["url"]; - - var startInfo = Type.GetType("Mono.Runtime") != null - ? new ProcessStartInfo("mono PlexRequests.Updater.exe") { Arguments = url } - : new ProcessStartInfo("PlexRequests.Updater.exe") { Arguments = url }; - - Process.Start(startInfo); - - Environment.Exit(0); - return Nancy.Response.NoBody; - } private Negotiator PushbulletNotifications() { diff --git a/PlexRequests.UI/Modules/Admin/SystemStatusModule.cs b/PlexRequests.UI/Modules/Admin/SystemStatusModule.cs new file mode 100644 index 000000000..e3a04e39d --- /dev/null +++ b/PlexRequests.UI/Modules/Admin/SystemStatusModule.cs @@ -0,0 +1,100 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: SystemStatusModule.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using MarkdownSharp; +using Nancy; +using Nancy.ModelBinding; +using Nancy.Responses.Negotiation; +using PlexRequests.Core; +using PlexRequests.Core.SettingModels; +using PlexRequests.Core.StatusChecker; +using PlexRequests.Helpers; +using PlexRequests.Helpers.Permissions; +using PlexRequests.UI.Models; + +namespace PlexRequests.UI.Modules.Admin +{ + public class SystemStatusModule : BaseModule + { + public SystemStatusModule(ISettingsService settingsService, ICacheProvider cache, ISettingsService ss) : base("admin", settingsService) + { + Cache = cache; + SystemSettings = ss; + + Security.HasPermissionsResponse(Permissions.Administrator); + + + Get["/status", true] = async (x, ct) => await Status(); + Post["/save", true] = async (x, ct) => await Save(); + + Post["/autoupdate"] = x => AutoUpdate(); + } + + private ICacheProvider Cache { get; } + private ISettingsService SystemSettings { get; } + + private async Task Status() + { + var settings = await SystemSettings.GetSettingsAsync(); + var checker = new StatusChecker(SystemSettings); + var status = await Cache.GetOrSetAsync(CacheKeys.LastestProductVersion, async () => await checker.GetStatus(), 30); + var md = new Markdown(new MarkdownOptions { AutoNewLines = true, AutoHyperlink = true }); + status.ReleaseNotes = md.Transform(status.ReleaseNotes); + + settings.Status = status; + + return View["Status", settings]; + } + + private async Task Save() + { + var settings = this.Bind(); + + await SystemSettings.SaveSettingsAsync(settings); + + return Response.AsJson(new JsonResponseModel { Result = true, Message = "Successfully Saved your settings"}); + } + + private Response AutoUpdate() + { + var url = Request.Form["url"]; + + var startInfo = Type.GetType("Mono.Runtime") != null + ? new ProcessStartInfo("mono PlexRequests.Updater.exe") { Arguments = url } + : new ProcessStartInfo("PlexRequests.Updater.exe") { Arguments = url }; + + Process.Start(startInfo); + + Environment.Exit(0); + return Nancy.Response.NoBody; + } + + } +} \ No newline at end of file diff --git a/PlexRequests.UI/Modules/UpdateCheckerModule.cs b/PlexRequests.UI/Modules/UpdateCheckerModule.cs index ddf869f28..3aa6a657d 100644 --- a/PlexRequests.UI/Modules/UpdateCheckerModule.cs +++ b/PlexRequests.UI/Modules/UpdateCheckerModule.cs @@ -33,6 +33,7 @@ using NLog; using PlexRequests.Core; using PlexRequests.Core.SettingModels; +using PlexRequests.Core.StatusChecker; using PlexRequests.Helpers; using PlexRequests.UI.Models; @@ -40,9 +41,10 @@ namespace PlexRequests.UI.Modules { public class UpdateCheckerModule : BaseAuthModule { - public UpdateCheckerModule(ICacheProvider provider, ISettingsService pr) : base("updatechecker", pr) + public UpdateCheckerModule(ICacheProvider provider, ISettingsService pr, ISettingsService settings) : base("updatechecker", pr) { Cache = provider; + SystemSettings = settings; Get["/", true] = async (x,ct) => await CheckLatestVersion(); } @@ -50,6 +52,7 @@ namespace PlexRequests.UI.Modules private ICacheProvider Cache { get; } private static Logger Log = LogManager.GetCurrentClassLogger(); + private ISettingsService SystemSettings { get; } private async Task CheckLatestVersion() { @@ -62,7 +65,7 @@ namespace PlexRequests.UI.Modules #if DEBUG return Response.AsJson(new JsonUpdateAvailableModel {UpdateAvailable = false}); #endif - var checker = new StatusChecker(); + var checker = new StatusChecker(SystemSettings); var release = await Cache.GetOrSetAsync(CacheKeys.LastestProductVersion, async() => await checker.GetStatus(), 30); return Response.AsJson(release.UpdateAvailable diff --git a/PlexRequests.UI/NinjectModules/ConfigurationModule.cs b/PlexRequests.UI/NinjectModules/ConfigurationModule.cs index 57cb76235..0ed22372a 100644 --- a/PlexRequests.UI/NinjectModules/ConfigurationModule.cs +++ b/PlexRequests.UI/NinjectModules/ConfigurationModule.cs @@ -33,6 +33,7 @@ using Ninject.Modules; using PlexRequests.Core; using PlexRequests.Core.Migration; +using PlexRequests.Core.StatusChecker; using PlexRequests.Helpers; using PlexRequests.Services.Interfaces; using PlexRequests.Services.Notification; @@ -56,6 +57,8 @@ namespace PlexRequests.UI.NinjectModules Bind().To().InSingletonScope(); Bind().To(); + + Bind().To(); } } } \ No newline at end of file diff --git a/PlexRequests.UI/PlexRequests.UI.csproj b/PlexRequests.UI/PlexRequests.UI.csproj index bc78a9f77..a18aec630 100644 --- a/PlexRequests.UI/PlexRequests.UI.csproj +++ b/PlexRequests.UI/PlexRequests.UI.csproj @@ -244,6 +244,7 @@ + @@ -324,7 +325,7 @@ - + diff --git a/PlexRequests.UI/Views/Admin/Status.cshtml b/PlexRequests.UI/Views/Admin/Status.cshtml index c6e3f6588..2051ea385 100644 --- a/PlexRequests.UI/Views/Admin/Status.cshtml +++ b/PlexRequests.UI/Views/Admin/Status.cshtml @@ -1,4 +1,5 @@ @using PlexRequests.UI.Helpers +@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase @Html.Partial("_Sidebar")
@@ -8,16 +9,35 @@
- +
+
+
+
+ + @if (Model.UseEarlyAccessPreviewBuilds) + { + + } + else + { + + } + +
+
+ +
+ +
- @if (Model.UpdateAvailable) + @if (Model.Status.UpdateAvailable) { - +
- @**@ //TODO + } else { @@ -26,14 +46,14 @@
- @if (Model.UpdateAvailable) + @if (Model.Status.UpdateAvailable) {

- @Model.ReleaseTitle + @Model.Status.ReleaseTitle


- @Html.Raw(Model.ReleaseNotes) + @Html.Raw(Model.Status.ReleaseNotes) } @@ -58,7 +78,7 @@ $.ajax({ type: "Post", url: "autoupdate", - data: { url: "@Model.DownloadUri" }, + data: { url: "@Model.Status.DownloadUri" }, dataType: "json", error: function () { setTimeout( @@ -68,4 +88,23 @@ } }); }); + + $('#saveSettings').click(function (e) { + e.preventDefault(); + var $form = $("#mainForm"); + $.ajax({ + type: $form.prop("method"), + url: $form.prop("action"), + data: $form.serialize(), + dataType: "json", + success: function (response) { + if (response.result === true) { + generateNotify(response.message, "success"); + + } else { + generateNotify(response.message, "warning"); + } + } + }); + }); \ No newline at end of file From 1c7fb2e93e4c19b0dff12f9e6bae249f027e1b03 Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Tue, 8 Nov 2016 13:35:35 +0000 Subject: [PATCH 08/87] Finished #659 #236 has been modified slightly. Needs testing on Different systems --- PlexRequests.Core/Models/StatusModel.cs | 3 +- PlexRequests.Core/PlexRequests.Core.csproj | 1 + .../SettingModels/SystemSettings.cs | 26 +++++++++- .../StatusChecker/StatusChecker.cs | 48 +++++++++++++----- PlexRequests.UI/Content/base.css | 17 +++++++ PlexRequests.UI/Content/base.min.css | 2 +- PlexRequests.UI/Content/base.scss | 22 +++++++-- .../Modules/Admin/SystemStatusModule.cs | 29 ++++++++++- PlexRequests.UI/Views/Admin/Status.cshtml | 49 +++++++++++++------ 9 files changed, 160 insertions(+), 37 deletions(-) diff --git a/PlexRequests.Core/Models/StatusModel.cs b/PlexRequests.Core/Models/StatusModel.cs index 9cbcf644d..ee55ab373 100644 --- a/PlexRequests.Core/Models/StatusModel.cs +++ b/PlexRequests.Core/Models/StatusModel.cs @@ -28,7 +28,8 @@ namespace PlexRequests.Core.Models { public class StatusModel { - public string Version { get; set; } + public string CurrentVersion { get; set; } + public string NewVersion { get; set; } public bool UpdateAvailable { get; set; } public string UpdateUri { get; set; } public string DownloadUri { get; set; } diff --git a/PlexRequests.Core/PlexRequests.Core.csproj b/PlexRequests.Core/PlexRequests.Core.csproj index 064f319e3..2800771ca 100644 --- a/PlexRequests.Core/PlexRequests.Core.csproj +++ b/PlexRequests.Core/PlexRequests.Core.csproj @@ -59,6 +59,7 @@ True + diff --git a/PlexRequests.Core/SettingModels/SystemSettings.cs b/PlexRequests.Core/SettingModels/SystemSettings.cs index fe60baabc..883c32ba8 100644 --- a/PlexRequests.Core/SettingModels/SystemSettings.cs +++ b/PlexRequests.Core/SettingModels/SystemSettings.cs @@ -25,14 +25,38 @@ // ************************************************************************/ #endregion +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using PlexRequests.Core.Models; namespace PlexRequests.Core.SettingModels { public class SystemSettings : Settings { - public bool UseEarlyAccessPreviewBuilds { get; set; } + public Branches Branch { get; set; } public StatusModel Status { get; set; } + + public List BranchDropdown { get; set; } + } + + public class BranchDropdown + { + public bool Selected { get; set; } + public string Name { get; set; } + public Branches Value { get; set; } + } + + public enum Branches + { + [Display(Name = "Stable")] + Stable, + + [Display(Name = "Early Access Preview")] + EarlyAccessPreview, + + [Display(Name = "Development")] + Dev, } } \ No newline at end of file diff --git a/PlexRequests.Core/StatusChecker/StatusChecker.cs b/PlexRequests.Core/StatusChecker/StatusChecker.cs index fcf41f65b..cf009464b 100644 --- a/PlexRequests.Core/StatusChecker/StatusChecker.cs +++ b/PlexRequests.Core/StatusChecker/StatusChecker.cs @@ -59,12 +59,12 @@ namespace PlexRequests.Core.StatusChecker public async Task GetStatus() { var settings = await SystemSettings.GetSettingsAsync(); - var isEap = settings.UseEarlyAccessPreviewBuilds; + var stable = settings.Branch == Branches.Stable; - if (isEap) + if (!stable) { // Early Access Preview Releases - return GetLatestEapRelease(); + return GetAppveyorRelease(settings.Branch); } // Stable releases @@ -76,7 +76,7 @@ namespace PlexRequests.Core.StatusChecker var assemblyVersion = AssemblyHelper.GetProductVersion(); var model = new StatusModel { - Version = assemblyVersion, + CurrentVersion = assemblyVersion, }; var releases = await Git.Repository.Release.GetAll(Owner, RepoName); @@ -84,7 +84,7 @@ namespace PlexRequests.Core.StatusChecker if (latestRelease == null) { - return new StatusModel { Version = "Unknown" }; + return new StatusModel { NewVersion = "Unknown" }; } var latestVersionArray = latestRelease.Name.Split(new[] { 'v' }, StringSplitOptions.RemoveEmptyEntries); var latestVersion = latestVersionArray.Length > 1 ? latestVersionArray[1] : string.Empty; @@ -93,6 +93,7 @@ namespace PlexRequests.Core.StatusChecker { model.UpdateAvailable = true; model.UpdateUri = latestRelease.HtmlUrl; + model.NewVersion = latestVersion; } model.ReleaseNotes = latestRelease.Body; @@ -102,17 +103,27 @@ namespace PlexRequests.Core.StatusChecker return model; } - private StatusModel GetLatestEapRelease() + private StatusModel GetAppveyorRelease(Branches branch) { var request = new ApiRequest(); // Get latest EAP Build var eapBranchRequest = new RestRequest { - Resource = "/projects/tidusjar/requestplex/branch/EAP", Method = Method.GET }; + + switch (branch) + { + case Branches.Dev: + eapBranchRequest.Resource = "/projects/tidusjar/requestplex/branch/dev"; + break; + case Branches.EarlyAccessPreview: + eapBranchRequest.Resource = "/projects/tidusjar/requestplex/branch/EAP"; + break; + } + var api = StringCipher.Decrypt(Api,"Appveyor"); eapBranchRequest.AddHeader("Authorization", $"Bearer {api}"); eapBranchRequest.AddHeader("Content-Type", "application/json"); @@ -139,15 +150,26 @@ namespace PlexRequests.Core.StatusChecker var downloadLink = $"{AppveyorApiUrl}/buildjobs/{jobId}/artifacts/{artifactResult.fileName}"; - return new StatusModel + var branchDisplay = EnumHelper.GetDisplayValue(branch); + var localVersion = AssemblyHelper.GetProductVersion(); + var localVersionExtended = $"{localVersion.Remove(localVersion.Length - 2, 2)}00"; + + var model = new StatusModel { DownloadUri = downloadLink, - ReleaseNotes = "Early Access Preview (See recent commits for details)", - ReleaseTitle = "Plex Requests Early Access Preview", - Version = branchResult.build.version, - UpdateAvailable = true, - UpdateUri = downloadLink + ReleaseNotes = $"{branchDisplay} (See recent commits for details)", + ReleaseTitle = $"Plex Requests {branchDisplay}", + NewVersion = branchResult.build.version, + UpdateUri = downloadLink, + CurrentVersion = localVersionExtended }; + + if (!localVersionExtended.Equals(branchResult.build.version, StringComparison.CurrentCultureIgnoreCase)) + { + model.UpdateAvailable = true; + } + + return model; } } } \ No newline at end of file diff --git a/PlexRequests.UI/Content/base.css b/PlexRequests.UI/Content/base.css index 7a9495d25..602ee6369 100644 --- a/PlexRequests.UI/Content/base.css +++ b/PlexRequests.UI/Content/base.css @@ -436,3 +436,20 @@ label { position: relative; margin-right: 0; } } +#lightbox { + background-color: grey; + filter: alpha(opacity=50); + /* IE */ + opacity: 0.5; + /* Safari, Opera */ + -moz-opacity: 0.50; + /* FireFox */ + top: 0px; + left: 0px; + z-index: 20; + height: 100%; + width: 100%; + background-repeat: no-repeat; + background-position: center; + position: absolute; } + diff --git a/PlexRequests.UI/Content/base.min.css b/PlexRequests.UI/Content/base.min.css index 583daf53d..5cd192ff8 100644 --- a/PlexRequests.UI/Content/base.min.css +++ b/PlexRequests.UI/Content/base.min.css @@ -1 +1 @@ -@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}.landing-block .media{max-width:450px;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#fff;}hr{border:1px dashed #777;}.btn{border-radius:.25rem !important;}.btn-group-separated .btn,.btn-group-separated .btn+.btn{margin-left:3px;}.multiSelect{background-color:#4e5d6c;}.form-control-custom{background-color:#4e5d6c !important;color:#fff !important;border-radius:0;box-shadow:0 0 0 !important;}h1{font-size:3.5rem !important;font-weight:600 !important;}.request-title{margin-top:0 !important;font-size:1.9rem !important;}p{font-size:1.1rem !important;}label{display:inline-block !important;margin-bottom:.5rem !important;font-size:16px !important;}.nav-tabs>li{font-size:13px;line-height:21px;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background:#4e5d6c;}.nav-tabs>li>a>.fa{padding:3px 5px 3px 3px;}.nav-tabs>li.nav-tab-right{float:right;}.nav-tabs>li.nav-tab-right a{margin-right:0;margin-left:2px;}.nav-tabs>li.nav-tab-icononly .fa{padding:3px;}.navbar .nav a .fa,.dropdown-menu a .fa{font-size:130%;top:1px;position:relative;display:inline-block;margin-right:5px;}.dropdown-menu a .fa{top:2px;}.btn-danger-outline{color:#d9534f !important;background-color:transparent;background-image:none;border-color:#d9534f !important;}.btn-danger-outline:focus,.btn-danger-outline.focus,.btn-danger-outline:active,.btn-danger-outline.active,.btn-danger-outline:hover,.open>.btn-danger-outline.dropdown-toggle{color:#fff !important;background-color:#d9534f !important;border-color:#d9534f !important;}.btn-primary-outline{color:#ff761b !important;background-color:transparent;background-image:none;border-color:#ff761b !important;}.btn-primary-outline:focus,.btn-primary-outline.focus,.btn-primary-outline:active,.btn-primary-outline.active,.btn-primary-outline:hover,.open>.btn-primary-outline.dropdown-toggle{color:#fff !important;background-color:#df691a !important;border-color:#df691a !important;}.btn-info-outline{color:#5bc0de !important;background-color:transparent;background-image:none;border-color:#5bc0de !important;}.btn-info-outline:focus,.btn-info-outline.focus,.btn-info-outline:active,.btn-info-outline.active,.btn-info-outline:hover,.open>.btn-info-outline.dropdown-toggle{color:#fff !important;background-color:#5bc0de !important;border-color:#5bc0de !important;}.btn-warning-outline{color:#f0ad4e !important;background-color:transparent;background-image:none;border-color:#f0ad4e !important;}.btn-warning-outline:focus,.btn-warning-outline.focus,.btn-warning-outline:active,.btn-warning-outline.active,.btn-warning-outline:hover,.open>.btn-warning-outline.dropdown-toggle{color:#fff !important;background-color:#f0ad4e !important;border-color:#f0ad4e !important;}.btn-success-outline{color:#5cb85c !important;background-color:transparent;background-image:none;border-color:#5cb85c !important;}.btn-success-outline:focus,.btn-success-outline.focus,.btn-success-outline:active,.btn-success-outline.active,.btn-success-outline:hover,.open>.btn-success-outline.dropdown-toggle{color:#fff !important;background-color:#5cb85c !important;border-color:#5cb85c !important;}#movieList .mix{display:none;}#tvList .mix{display:none;}.scroll-top-wrapper{position:fixed;opacity:0;visibility:hidden;overflow:hidden;text-align:center;z-index:99999999;background-color:#4e5d6c;color:#eee;width:50px;height:48px;line-height:48px;right:30px;bottom:30px;padding-top:2px;border-top-left-radius:10px;border-top-right-radius:10px;border-bottom-right-radius:10px;border-bottom-left-radius:10px;-webkit-transition:all .5s ease-in-out;-moz-transition:all .5s ease-in-out;-ms-transition:all .5s ease-in-out;-o-transition:all .5s ease-in-out;transition:all .5s ease-in-out;}.scroll-top-wrapper:hover{background-color:#637689;}.scroll-top-wrapper.show{visibility:visible;cursor:pointer;opacity:1;}.scroll-top-wrapper i.fa{line-height:inherit;}.no-search-results{text-align:center;}.no-search-results .no-search-results-icon{font-size:10em;color:#4e5d6c;}.no-search-results .no-search-results-text{margin:20px 0;color:#ccc;}.form-control-search{padding:13px 105px 13px 16px;height:100%;}.form-control-withbuttons{padding-right:105px;}.input-group-addon .btn-group{position:absolute;right:45px;z-index:3;top:10px;box-shadow:0 0 0;}.input-group-addon .btn-group .btn{border:1px solid rgba(255,255,255,.7) !important;padding:3px 12px;color:rgba(255,255,255,.7) !important;}.btn-split .btn{border-radius:0 !important;}.btn-split .btn:not(.dropdown-toggle){border-radius:.25rem 0 0 .25rem !important;}.btn-split .btn.dropdown-toggle{border-radius:0 .25rem .25rem 0 !important;padding:12px 8px;}#updateAvailable{background-color:#df691a;text-align:center;font-size:15px;padding:3px 0;}.checkbox label{display:inline-block;cursor:pointer;position:relative;padding-left:25px;margin-right:15px;font-size:13px;margin-bottom:10px;}.checkbox label:before{content:"";display:inline-block;width:18px;height:18px;margin-right:10px;position:absolute;left:0;bottom:1px;border:2px solid #eee;border-radius:3px;}.checkbox input[type=checkbox]{display:none;}.checkbox input[type=checkbox]:checked+label:before{content:"✓";font-size:13px;color:#fafafa;text-align:center;line-height:13px;}.input-group-sm{padding-top:2px;padding-bottom:2px;}.tab-pane .form-horizontal .form-group{margin-right:15px;margin-left:15px;}.bootstrap-datetimepicker-widget.dropdown-menu{background-color:#4e5d6c;}.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after{border-bottom:6px solid #4e5d6c !important;}.bootstrap-datetimepicker-widget table td.active,.bootstrap-datetimepicker-widget table td.active:hover{color:#fff !important;}.landing-header{display:block;margin:60px auto;}.landing-block{background:#2f2f2f !important;padding:5px;}.landing-block .media{margin:30px auto;max-width:450px;}.landing-block .media .media-left{display:inline-block;float:left;width:70px;}.landing-block .media .media-left i.fa{font-size:3em;}.landing-title{font-weight:bold;}.checkbox-custom{margin-top:0 !important;margin-bottom:0 !important;}.tooltip_templates{display:none;}.shadow{-moz-box-shadow:3px 3px 5px 6px #191919;-webkit-box-shadow:3px 3px 5px 6px #191919;box-shadow:3px 3px 5px 6px #191919;}.img-circle{border-radius:50%;}#wrapper{padding-left:0;-webkit-transition:all .5s ease;-moz-transition:all .5s ease;-o-transition:all .5s ease;transition:all .5s ease;}#wrapper.toggled{padding-right:250px;}#sidebar-wrapper{z-index:1000;position:fixed;right:250px;width:0;height:100%;margin-right:-250px;overflow-y:auto;background:#4e5d6c;padding-left:15px;-webkit-transition:all .5s ease;-moz-transition:all .5s ease;-o-transition:all .5s ease;transition:all .5s ease;}#wrapper.toggled #sidebar-wrapper{width:500px;}#page-content-wrapper{width:100%;position:absolute;padding:15px;}#wrapper.toggled #page-content-wrapper{position:absolute;margin-left:-250px;}.sidebar-nav{position:absolute;top:0;width:500px;margin:0;padding-left:0;list-style:none;}.sidebar-nav li{text-indent:20px;line-height:40px;}.sidebar-nav li a{display:block;text-decoration:none;color:#999;}.sidebar-nav li a:hover{text-decoration:none;color:#fff;background:rgba(255,255,255,.2);}.sidebar-nav li a:active,.sidebar-nav li a:focus{text-decoration:none;}.sidebar-nav>.sidebar-brand{height:65px;font-size:18px;line-height:60px;}.sidebar-nav>.sidebar-brand a{color:#999;}.sidebar-nav>.sidebar-brand a:hover{color:#fff;background:none;}@media(min-width:768px){#wrapper{padding-right:250px;}#wrapper.toggled{padding-right:0;}#sidebar-wrapper{width:500px;}#wrapper.toggled #sidebar-wrapper{width:0;}#page-content-wrapper{padding:20px;position:relative;}#wrapper.toggled #page-content-wrapper{position:relative;margin-right:0;}} \ No newline at end of file +@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}.landing-block .media{max-width:450px;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#fff;}hr{border:1px dashed #777;}.btn{border-radius:.25rem !important;}.btn-group-separated .btn,.btn-group-separated .btn+.btn{margin-left:3px;}.multiSelect{background-color:#4e5d6c;}.form-control-custom{background-color:#4e5d6c !important;color:#fff !important;border-radius:0;box-shadow:0 0 0 !important;}h1{font-size:3.5rem !important;font-weight:600 !important;}.request-title{margin-top:0 !important;font-size:1.9rem !important;}p{font-size:1.1rem !important;}label{display:inline-block !important;margin-bottom:.5rem !important;font-size:16px !important;}.nav-tabs>li{font-size:13px;line-height:21px;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background:#4e5d6c;}.nav-tabs>li>a>.fa{padding:3px 5px 3px 3px;}.nav-tabs>li.nav-tab-right{float:right;}.nav-tabs>li.nav-tab-right a{margin-right:0;margin-left:2px;}.nav-tabs>li.nav-tab-icononly .fa{padding:3px;}.navbar .nav a .fa,.dropdown-menu a .fa{font-size:130%;top:1px;position:relative;display:inline-block;margin-right:5px;}.dropdown-menu a .fa{top:2px;}.btn-danger-outline{color:#d9534f !important;background-color:transparent;background-image:none;border-color:#d9534f !important;}.btn-danger-outline:focus,.btn-danger-outline.focus,.btn-danger-outline:active,.btn-danger-outline.active,.btn-danger-outline:hover,.open>.btn-danger-outline.dropdown-toggle{color:#fff !important;background-color:#d9534f !important;border-color:#d9534f !important;}.btn-primary-outline{color:#ff761b !important;background-color:transparent;background-image:none;border-color:#ff761b !important;}.btn-primary-outline:focus,.btn-primary-outline.focus,.btn-primary-outline:active,.btn-primary-outline.active,.btn-primary-outline:hover,.open>.btn-primary-outline.dropdown-toggle{color:#fff !important;background-color:#df691a !important;border-color:#df691a !important;}.btn-info-outline{color:#5bc0de !important;background-color:transparent;background-image:none;border-color:#5bc0de !important;}.btn-info-outline:focus,.btn-info-outline.focus,.btn-info-outline:active,.btn-info-outline.active,.btn-info-outline:hover,.open>.btn-info-outline.dropdown-toggle{color:#fff !important;background-color:#5bc0de !important;border-color:#5bc0de !important;}.btn-warning-outline{color:#f0ad4e !important;background-color:transparent;background-image:none;border-color:#f0ad4e !important;}.btn-warning-outline:focus,.btn-warning-outline.focus,.btn-warning-outline:active,.btn-warning-outline.active,.btn-warning-outline:hover,.open>.btn-warning-outline.dropdown-toggle{color:#fff !important;background-color:#f0ad4e !important;border-color:#f0ad4e !important;}.btn-success-outline{color:#5cb85c !important;background-color:transparent;background-image:none;border-color:#5cb85c !important;}.btn-success-outline:focus,.btn-success-outline.focus,.btn-success-outline:active,.btn-success-outline.active,.btn-success-outline:hover,.open>.btn-success-outline.dropdown-toggle{color:#fff !important;background-color:#5cb85c !important;border-color:#5cb85c !important;}#movieList .mix{display:none;}#tvList .mix{display:none;}.scroll-top-wrapper{position:fixed;opacity:0;visibility:hidden;overflow:hidden;text-align:center;z-index:99999999;background-color:#4e5d6c;color:#eee;width:50px;height:48px;line-height:48px;right:30px;bottom:30px;padding-top:2px;border-top-left-radius:10px;border-top-right-radius:10px;border-bottom-right-radius:10px;border-bottom-left-radius:10px;-webkit-transition:all .5s ease-in-out;-moz-transition:all .5s ease-in-out;-ms-transition:all .5s ease-in-out;-o-transition:all .5s ease-in-out;transition:all .5s ease-in-out;}.scroll-top-wrapper:hover{background-color:#637689;}.scroll-top-wrapper.show{visibility:visible;cursor:pointer;opacity:1;}.scroll-top-wrapper i.fa{line-height:inherit;}.no-search-results{text-align:center;}.no-search-results .no-search-results-icon{font-size:10em;color:#4e5d6c;}.no-search-results .no-search-results-text{margin:20px 0;color:#ccc;}.form-control-search{padding:13px 105px 13px 16px;height:100%;}.form-control-withbuttons{padding-right:105px;}.input-group-addon .btn-group{position:absolute;right:45px;z-index:3;top:10px;box-shadow:0 0 0;}.input-group-addon .btn-group .btn{border:1px solid rgba(255,255,255,.7) !important;padding:3px 12px;color:rgba(255,255,255,.7) !important;}.btn-split .btn{border-radius:0 !important;}.btn-split .btn:not(.dropdown-toggle){border-radius:.25rem 0 0 .25rem !important;}.btn-split .btn.dropdown-toggle{border-radius:0 .25rem .25rem 0 !important;padding:12px 8px;}#updateAvailable{background-color:#df691a;text-align:center;font-size:15px;padding:3px 0;}.checkbox label{display:inline-block;cursor:pointer;position:relative;padding-left:25px;margin-right:15px;font-size:13px;margin-bottom:10px;}.checkbox label:before{content:"";display:inline-block;width:18px;height:18px;margin-right:10px;position:absolute;left:0;bottom:1px;border:2px solid #eee;border-radius:3px;}.checkbox input[type=checkbox]{display:none;}.checkbox input[type=checkbox]:checked+label:before{content:"✓";font-size:13px;color:#fafafa;text-align:center;line-height:13px;}.input-group-sm{padding-top:2px;padding-bottom:2px;}.tab-pane .form-horizontal .form-group{margin-right:15px;margin-left:15px;}.bootstrap-datetimepicker-widget.dropdown-menu{background-color:#4e5d6c;}.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after{border-bottom:6px solid #4e5d6c !important;}.bootstrap-datetimepicker-widget table td.active,.bootstrap-datetimepicker-widget table td.active:hover{color:#fff !important;}.landing-header{display:block;margin:60px auto;}.landing-block{background:#2f2f2f !important;padding:5px;}.landing-block .media{margin:30px auto;max-width:450px;}.landing-block .media .media-left{display:inline-block;float:left;width:70px;}.landing-block .media .media-left i.fa{font-size:3em;}.landing-title{font-weight:bold;}.checkbox-custom{margin-top:0 !important;margin-bottom:0 !important;}.tooltip_templates{display:none;}.shadow{-moz-box-shadow:3px 3px 5px 6px #191919;-webkit-box-shadow:3px 3px 5px 6px #191919;box-shadow:3px 3px 5px 6px #191919;}.img-circle{border-radius:50%;}#wrapper{padding-left:0;-webkit-transition:all .5s ease;-moz-transition:all .5s ease;-o-transition:all .5s ease;transition:all .5s ease;}#wrapper.toggled{padding-right:250px;}#sidebar-wrapper{z-index:1000;position:fixed;right:250px;width:0;height:100%;margin-right:-250px;overflow-y:auto;background:#4e5d6c;padding-left:15px;-webkit-transition:all .5s ease;-moz-transition:all .5s ease;-o-transition:all .5s ease;transition:all .5s ease;}#wrapper.toggled #sidebar-wrapper{width:500px;}#page-content-wrapper{width:100%;position:absolute;padding:15px;}#wrapper.toggled #page-content-wrapper{position:absolute;margin-left:-250px;}.sidebar-nav{position:absolute;top:0;width:500px;margin:0;padding-left:0;list-style:none;}.sidebar-nav li{text-indent:20px;line-height:40px;}.sidebar-nav li a{display:block;text-decoration:none;color:#999;}.sidebar-nav li a:hover{text-decoration:none;color:#fff;background:rgba(255,255,255,.2);}.sidebar-nav li a:active,.sidebar-nav li a:focus{text-decoration:none;}.sidebar-nav>.sidebar-brand{height:65px;font-size:18px;line-height:60px;}.sidebar-nav>.sidebar-brand a{color:#999;}.sidebar-nav>.sidebar-brand a:hover{color:#fff;background:none;}@media(min-width:768px){#wrapper{padding-right:250px;}#wrapper.toggled{padding-right:0;}#sidebar-wrapper{width:500px;}#wrapper.toggled #sidebar-wrapper{width:0;}#page-content-wrapper{padding:20px;position:relative;}#wrapper.toggled #page-content-wrapper{position:relative;margin-right:0;}}#lightbox{background-color:#808080;filter:alpha(opacity=50);opacity:.5;-moz-opacity:.5;top:0;left:0;z-index:20;height:100%;width:100%;background-repeat:no-repeat;background-position:center;position:absolute;} \ No newline at end of file diff --git a/PlexRequests.UI/Content/base.scss b/PlexRequests.UI/Content/base.scss index e293c9e44..64dd1b83e 100644 --- a/PlexRequests.UI/Content/base.scss +++ b/PlexRequests.UI/Content/base.scss @@ -6,9 +6,7 @@ $info-colour: #5bc0de; $warning-colour: #f0ad4e; $danger-colour: #d9534f; $success-colour: #5cb85c; -$i: -!important -; +$i:!important; @media (min-width: 768px ) { .row { @@ -551,4 +549,20 @@ $border-radius: 10px; position: relative; margin-right: 0; } -} \ No newline at end of file +} + +#lightbox { + + background-color: grey; + filter:alpha(opacity=50); /* IE */ + opacity: 0.5; /* Safari, Opera */ + -moz-opacity:0.50; /* FireFox */ + top: 0px; + left: 0px; + z-index: 20; + height: 100%; + width: 100%; + background-repeat:no-repeat; + background-position:center; + position:absolute; +} diff --git a/PlexRequests.UI/Modules/Admin/SystemStatusModule.cs b/PlexRequests.UI/Modules/Admin/SystemStatusModule.cs index e3a04e39d..05ae0f125 100644 --- a/PlexRequests.UI/Modules/Admin/SystemStatusModule.cs +++ b/PlexRequests.UI/Modules/Admin/SystemStatusModule.cs @@ -26,6 +26,7 @@ #endregion using System; +using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; using MarkdownSharp; @@ -49,8 +50,7 @@ namespace PlexRequests.UI.Modules.Admin SystemSettings = ss; Security.HasPermissionsResponse(Permissions.Administrator); - - + Get["/status", true] = async (x, ct) => await Status(); Post["/save", true] = async (x, ct) => await Save(); @@ -70,6 +70,28 @@ namespace PlexRequests.UI.Modules.Admin settings.Status = status; + settings.BranchDropdown = new List + { + new BranchDropdown + { + Name = EnumHelper.GetDisplayValue(Branches.Stable), + Value = Branches.Stable, + Selected = settings.Branch == Branches.Stable + }, + new BranchDropdown + { + Name = EnumHelper.GetDisplayValue(Branches.EarlyAccessPreview), + Value = Branches.EarlyAccessPreview, + Selected = settings.Branch == Branches.EarlyAccessPreview + }, + new BranchDropdown + { + Name = EnumHelper.GetDisplayValue(Branches.Dev), + Value = Branches.Dev, + Selected = settings.Branch == Branches.Dev + }, + }; + return View["Status", settings]; } @@ -79,6 +101,9 @@ namespace PlexRequests.UI.Modules.Admin await SystemSettings.SaveSettingsAsync(settings); + // Clear the cache + Cache.Remove(CacheKeys.LastestProductVersion); + return Response.AsJson(new JsonResponseModel { Result = true, Message = "Successfully Saved your settings"}); } diff --git a/PlexRequests.UI/Views/Admin/Status.cshtml b/PlexRequests.UI/Views/Admin/Status.cshtml index 2051ea385..93760a064 100644 --- a/PlexRequests.UI/Views/Admin/Status.cshtml +++ b/PlexRequests.UI/Views/Admin/Status.cshtml @@ -1,30 +1,43 @@ @using PlexRequests.UI.Helpers @inherits Nancy.ViewEngines.Razor.NancyRazorViewBase @Html.Partial("_Sidebar") - +
Status
- - + +
+ + @if (Model.Status.UpdateAvailable) + { +
+ + +
+ }
+
-
- - @if (Model.UseEarlyAccessPreviewBuilds) - { - - } - else - { - - } - + +
+
@@ -68,6 +81,7 @@ e.preventDefault(); $('body').append(""); $('#autoUpdate').prop("disabled", "disabled"); + document.getElementById("lightbox").style.display = ""; var count = 0; setInterval(function () { count++; @@ -92,10 +106,15 @@ $('#saveSettings').click(function (e) { e.preventDefault(); var $form = $("#mainForm"); + + var branches = $("#branches option:selected").val(); + + var data = $form.serialize(); + data = data + "&branch=" + branches; $.ajax({ type: $form.prop("method"), url: $form.prop("action"), - data: $form.serialize(), + data: data, dataType: "json", success: function (response) { if (response.result === true) { From 2bd7ece9d086f2a78336975d8fa0328134f1db74 Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Tue, 8 Nov 2016 14:15:33 +0000 Subject: [PATCH 09/87] Finished #633 (First part of the queuing) --- PlexRequests.Core/Models/NotificationType.cs | 3 +- PlexRequests.Core/PlexRequests.Core.csproj | 5 + .../Queue/ITransientFaultQueue.cs | 17 + .../Queue/TransientFaultQueue.cs | 64 ++- PlexRequests.Core/packages.config | 1 + .../Notification/EmailMessageNotification.cs | 35 +- PlexRequests.Store/Models/RequestQueue.cs | 7 + PlexRequests.Store/SqlTables.sql | 1 + PlexRequests.UI/Modules/SearchModule.cs | 478 +++++++++++++----- .../NinjectModules/ServicesModule.cs | 4 +- 10 files changed, 460 insertions(+), 155 deletions(-) create mode 100644 PlexRequests.Core/Queue/ITransientFaultQueue.cs diff --git a/PlexRequests.Core/Models/NotificationType.cs b/PlexRequests.Core/Models/NotificationType.cs index a01d153dd..eace7b018 100644 --- a/PlexRequests.Core/Models/NotificationType.cs +++ b/PlexRequests.Core/Models/NotificationType.cs @@ -34,6 +34,7 @@ namespace PlexRequests.Core.Models RequestApproved, AdminNote, Test, - + RequestDeclined, + ItemAddedToFaultQueue } } diff --git a/PlexRequests.Core/PlexRequests.Core.csproj b/PlexRequests.Core/PlexRequests.Core.csproj index 2800771ca..8faf4e83a 100644 --- a/PlexRequests.Core/PlexRequests.Core.csproj +++ b/PlexRequests.Core/PlexRequests.Core.csproj @@ -39,6 +39,10 @@ ..\packages\Common.Logging.Core.3.0.0\lib\net40\Common.Logging.Core.dll True + + ..\packages\Dapper.1.50.0-beta8\lib\net45\Dapper.dll + True + ..\Assemblies\Mono.Data.Sqlite.dll @@ -105,6 +109,7 @@ + diff --git a/PlexRequests.Core/Queue/ITransientFaultQueue.cs b/PlexRequests.Core/Queue/ITransientFaultQueue.cs new file mode 100644 index 000000000..ac62add0f --- /dev/null +++ b/PlexRequests.Core/Queue/ITransientFaultQueue.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using PlexRequests.Store; +using PlexRequests.Store.Models; + +namespace PlexRequests.Core.Queue +{ + public interface ITransientFaultQueue + { + void Dequeue(); + Task DequeueAsync(); + IEnumerable GetQueue(); + Task> GetQueueAsync(); + void QueueItem(RequestedModel request, RequestType type, FaultType faultType); + Task QueueItemAsync(RequestedModel request, RequestType type, FaultType faultType); + } +} \ No newline at end of file diff --git a/PlexRequests.Core/Queue/TransientFaultQueue.cs b/PlexRequests.Core/Queue/TransientFaultQueue.cs index e3621ee0f..108387f03 100644 --- a/PlexRequests.Core/Queue/TransientFaultQueue.cs +++ b/PlexRequests.Core/Queue/TransientFaultQueue.cs @@ -26,7 +26,9 @@ #endregion using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using Dapper; using PlexRequests.Helpers; using PlexRequests.Store; using PlexRequests.Store.Models; @@ -34,7 +36,7 @@ using PlexRequests.Store.Repository; namespace PlexRequests.Core.Queue { - public class TransientFaultQueue + public class TransientFaultQueue : ITransientFaultQueue { public TransientFaultQueue(IRepository queue) { @@ -44,44 +46,84 @@ namespace PlexRequests.Core.Queue private IRepository RequestQueue { get; } - public void QueueItem(RequestedModel request, RequestType type) + public void QueueItem(RequestedModel request, RequestType type, FaultType faultType) { + //Ensure there is not a duplicate queued item + var existingItem = RequestQueue.Custom( + connection => + { + connection.Open(); + var result = connection.Query("select * from RequestQueue where PrimaryIdentifier = @ProviderId", new { ProviderId = request.ProviderId }); + + return result; + }).FirstOrDefault(); + + if (existingItem != null) + { + // It's already in the queue + return; + } + var queue = new RequestQueue { Type = type, Content = ByteConverterHelper.ReturnBytes(request), - PrimaryIdentifier = request.ProviderId + PrimaryIdentifier = request.ProviderId, + FaultType = faultType }; RequestQueue.Insert(queue); } - public async Task QueueItemAsync(RequestedModel request, RequestType type) + public async Task QueueItemAsync(RequestedModel request, RequestType type, FaultType faultType) { + //Ensure there is not a duplicate queued item + var existingItem = await RequestQueue.CustomAsync(async connection => + { + connection.Open(); + var result = await connection.QueryAsync("select * from RequestQueue where PrimaryIdentifier = @ProviderId", new { ProviderId = request.ProviderId }); + + return result; + }); + + if (existingItem.FirstOrDefault() != null) + { + // It's already in the queue + return; + } + var queue = new RequestQueue { Type = type, Content = ByteConverterHelper.ReturnBytes(request), - PrimaryIdentifier = request.ProviderId + PrimaryIdentifier = request.ProviderId, + FaultType = faultType }; await RequestQueue.InsertAsync(queue); } - public IEnumerable Dequeue() + public IEnumerable GetQueue() { var items = RequestQueue.GetAll(); - RequestQueue.DeleteAll("RequestQueue"); return items; } - public async Task> DequeueAsync() + public async Task> GetQueueAsync() { var items = RequestQueue.GetAllAsync(); - - await RequestQueue.DeleteAllAsync("RequestQueue"); - + return await items; } + + public void Dequeue() + { + RequestQueue.DeleteAll("RequestQueue"); + } + + public async Task DequeueAsync() + { + await RequestQueue.DeleteAllAsync("RequestQueue"); + } } } \ No newline at end of file diff --git a/PlexRequests.Core/packages.config b/PlexRequests.Core/packages.config index e8f81f5a9..d698d0fd5 100644 --- a/PlexRequests.Core/packages.config +++ b/PlexRequests.Core/packages.config @@ -2,6 +2,7 @@ + diff --git a/PlexRequests.Services/Notification/EmailMessageNotification.cs b/PlexRequests.Services/Notification/EmailMessageNotification.cs index 3f6bf9c91..52727dba8 100644 --- a/PlexRequests.Services/Notification/EmailMessageNotification.cs +++ b/PlexRequests.Services/Notification/EmailMessageNotification.cs @@ -87,6 +87,14 @@ namespace PlexRequests.Services.Notification case NotificationType.Test: await EmailTest(model, emailSettings); break; + case NotificationType.RequestDeclined: + throw new NotImplementedException(); + + case NotificationType.ItemAddedToFaultQueue: + await EmailAddedToRequestQueue(model, emailSettings); + break; + default: + throw new ArgumentOutOfRangeException(); } } @@ -129,7 +137,7 @@ namespace PlexRequests.Services.Notification $"Plex Requests: New {model.RequestType.GetString()?.ToLower()} request for {model.Title}!", $"Hello! The user '{model.User}' has requested the {model.RequestType.GetString()?.ToLower()} '{model.Title}'! Please log in to approve this request. Request Date: {model.DateTime.ToString("f")}", model.ImgSrc); - var body = new BodyBuilder { HtmlBody = html, TextBody = "This email is only available on devices that support HTML." }; + var body = new BodyBuilder { HtmlBody = html, TextBody = $"Hello! The user '{model.User}' has requested the {model.RequestType.GetString()?.ToLower()} '{model.Title}'! Please log in to approve this request. Request Date: {model.DateTime.ToString("f")}" }; var message = new MimeMessage { @@ -150,7 +158,7 @@ namespace PlexRequests.Services.Notification $"Plex Requests: New issue for {model.Title}!", $"Hello! The user '{model.User}' has reported a new issue {model.Body} for the title {model.Title}!", model.ImgSrc); - var body = new BodyBuilder { HtmlBody = html, TextBody = "This email is only available on devices that support HTML." }; + var body = new BodyBuilder { HtmlBody = html, TextBody = $"Hello! The user '{model.User}' has reported a new issue {model.Body} for the title {model.Title}!"}; var message = new MimeMessage { @@ -164,6 +172,27 @@ namespace PlexRequests.Services.Notification await Send(message, settings); } + private async Task EmailAddedToRequestQueue(NotificationModel model, EmailNotificationSettings settings) + { + var email = new EmailBasicTemplate(); + var html = email.LoadTemplate( + "Plex Requests: A request could not be added.", + $"Hello! The user '{model.User}' has requested {model.Title} but it could not be added. This has been added into the requests queue and will keep retrying", + model.ImgSrc); + var body = new BodyBuilder { HtmlBody = html, TextBody = $"Hello! The user '{model.User}' has requested {model.Title} but it could not be added. This has been added into the requests queue and will keep retrying" }; + + var message = new MimeMessage + { + Body = body.ToMessageBody(), + Subject = $"Plex Requests: A request could not be added" + }; + message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender)); + message.To.Add(new MailboxAddress(settings.RecipientEmail, settings.RecipientEmail)); + + + await Send(message, settings); + } + private async Task EmailAvailableRequest(NotificationModel model, EmailNotificationSettings settings) { if (!settings.EnableUserEmailNotifications) @@ -175,7 +204,7 @@ namespace PlexRequests.Services.Notification $"Plex Requests: {model.Title} is now available!", $"Hello! You requested {model.Title} on PlexRequests! This is now available on Plex! :)", model.ImgSrc); - var body = new BodyBuilder { HtmlBody = html, TextBody = "This email is only available on devices that support HTML." }; + var body = new BodyBuilder { HtmlBody = html, TextBody = $"Hello! You requested {model.Title} on PlexRequests! This is now available on Plex! :)" }; var message = new MimeMessage { diff --git a/PlexRequests.Store/Models/RequestQueue.cs b/PlexRequests.Store/Models/RequestQueue.cs index bdbf25a2f..15067fbc7 100644 --- a/PlexRequests.Store/Models/RequestQueue.cs +++ b/PlexRequests.Store/Models/RequestQueue.cs @@ -38,5 +38,12 @@ namespace PlexRequests.Store.Models public byte[] Content { get; set; } + public FaultType FaultType { get; set; } + } + + public enum FaultType + { + RequestFault, + MissingInformation } } \ No newline at end of file diff --git a/PlexRequests.Store/SqlTables.sql b/PlexRequests.Store/SqlTables.sql index a22258a5d..220664ba1 100644 --- a/PlexRequests.Store/SqlTables.sql +++ b/PlexRequests.Store/SqlTables.sql @@ -138,6 +138,7 @@ CREATE TABLE IF NOT EXISTS RequestQueue Id INTEGER PRIMARY KEY AUTOINCREMENT, PrimaryIdentifier INTEGER NOT NULL, Type INTEGER NOT NULL, + FaultType INTEGER NOT NULL, Content BLOB NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS PlexUsers_Id ON PlexUsers (Id); diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index e10e5b92d..ef35e08b6 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -1,4 +1,5 @@ #region Copyright + // /************************************************************************ // Copyright (c) 2016 Jamie Rees // File: SearchModule.cs @@ -23,7 +24,9 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // ************************************************************************/ + #endregion + using System; using System.Collections.Generic; using System.Globalization; @@ -54,13 +57,13 @@ using Newtonsoft.Json; using PlexRequests.Api.Models.Sonarr; using PlexRequests.Api.Models.Tv; using PlexRequests.Core.Models; +using PlexRequests.Core.Queue; using PlexRequests.Helpers.Analytics; using PlexRequests.Helpers.Permissions; using PlexRequests.Store.Models; using PlexRequests.Store.Repository; using TMDbLib.Objects.General; -using TMDbLib.Objects.Search; using Action = PlexRequests.Helpers.Analytics.Action; using EpisodesModel = PlexRequests.Store.EpisodesModel; @@ -73,10 +76,13 @@ namespace PlexRequests.UI.Modules ISettingsService prSettings, IAvailabilityChecker checker, IRequestService request, ISonarrApi sonarrApi, ISettingsService sonarrSettings, ISettingsService sickRageService, ICouchPotatoApi cpApi, ISickRageApi srApi, - INotificationService notify, IMusicBrainzApi mbApi, IHeadphonesApi hpApi, ISettingsService hpService, + INotificationService notify, IMusicBrainzApi mbApi, IHeadphonesApi hpApi, + ISettingsService hpService, ICouchPotatoCacher cpCacher, ISonarrCacher sonarrCacher, ISickRageCacher sickRageCacher, IPlexApi plexApi, - ISettingsService plexService, ISettingsService auth, IRepository u, ISettingsService email, - IIssueService issue, IAnalytics a, IRepository rl) : base("search", prSettings) + ISettingsService plexService, ISettingsService auth, + IRepository u, ISettingsService email, + IIssueService issue, IAnalytics a, IRepository rl, ITransientFaultQueue tfQueue) + : base("search", prSettings) { Auth = auth; PlexService = plexService; @@ -104,6 +110,7 @@ namespace PlexRequests.UI.Modules IssueService = issue; Analytics = a; RequestLimitRepo = rl; + FaultQueue = tfQueue; TvApi = new TvMazeApi(); @@ -118,7 +125,8 @@ namespace PlexRequests.UI.Modules Get["movie/playing", true] = async (x, ct) => await CurrentlyPlayingMovies(); Post["request/movie", true] = async (x, ct) => await RequestMovie((int)Request.Form.movieId); - Post["request/tv", true] = async (x, ct) => await RequestTvShow((int)Request.Form.tvId, (string)Request.Form.seasons); + Post["request/tv", true] = + async (x, ct) => await RequestTvShow((int)Request.Form.tvId, (string)Request.Form.seasons); Post["request/tvEpisodes", true] = async (x, ct) => await RequestTvShow(0, "episode"); Post["request/album", true] = async (x, ct) => await RequestAlbum((string)Request.Form.albumId); @@ -128,6 +136,7 @@ namespace PlexRequests.UI.Modules Get["/seasons"] = x => GetSeasons(); Get["/episodes", true] = async (x, ct) => await GetEpisodes(); } + private TvMazeApi TvApi { get; } private IPlexApi PlexApi { get; } private TheMovieDbApi MovieApi { get; } @@ -154,6 +163,7 @@ namespace PlexRequests.UI.Modules private IRepository UsersToNotifyRepo { get; } private IIssueService IssueService { get; } private IAnalytics Analytics { get; } + private ITransientFaultQueue FaultQueue { get; } private IRepository RequestLimitRepo { get; } private static Logger Log = LogManager.GetCurrentClassLogger(); @@ -167,19 +177,22 @@ namespace PlexRequests.UI.Modules private async Task UpcomingMovies() { - Analytics.TrackEventAsync(Category.Search, Action.Movie, "Upcoming", Username, CookieHelper.GetAnalyticClientId(Cookies)); + Analytics.TrackEventAsync(Category.Search, Action.Movie, "Upcoming", Username, + CookieHelper.GetAnalyticClientId(Cookies)); return await ProcessMovies(MovieSearchType.Upcoming, string.Empty); } private async Task CurrentlyPlayingMovies() { - Analytics.TrackEventAsync(Category.Search, Action.Movie, "CurrentlyPlaying", Username, CookieHelper.GetAnalyticClientId(Cookies)); + Analytics.TrackEventAsync(Category.Search, Action.Movie, "CurrentlyPlaying", Username, + CookieHelper.GetAnalyticClientId(Cookies)); return await ProcessMovies(MovieSearchType.CurrentlyPlaying, string.Empty); } private async Task SearchMovie(string searchTerm) { - Analytics.TrackEventAsync(Category.Search, Action.Movie, searchTerm, Username, CookieHelper.GetAnalyticClientId(Cookies)); + Analytics.TrackEventAsync(Category.Search, Action.Movie, searchTerm, Username, + CookieHelper.GetAnalyticClientId(Cookies)); return await ProcessMovies(MovieSearchType.Search, searchTerm); } @@ -192,24 +205,24 @@ namespace PlexRequests.UI.Modules case MovieSearchType.Search: var movies = await MovieApi.SearchMovie(searchTerm).ConfigureAwait(false); apiMovies = movies.Select(x => - new MovieResult - { - Adult = x.Adult, - BackdropPath = x.BackdropPath, - GenreIds = x.GenreIds, - Id = x.Id, - OriginalLanguage = x.OriginalLanguage, - OriginalTitle = x.OriginalTitle, - Overview = x.Overview, - Popularity = x.Popularity, - PosterPath = x.PosterPath, - ReleaseDate = x.ReleaseDate, - Title = x.Title, - Video = x.Video, - VoteAverage = x.VoteAverage, - VoteCount = x.VoteCount - }) - .ToList(); + new MovieResult + { + Adult = x.Adult, + BackdropPath = x.BackdropPath, + GenreIds = x.GenreIds, + Id = x.Id, + OriginalLanguage = x.OriginalLanguage, + OriginalTitle = x.OriginalTitle, + Overview = x.Overview, + Popularity = x.Popularity, + PosterPath = x.PosterPath, + ReleaseDate = x.ReleaseDate, + Title = x.Title, + Video = x.Video, + VoteAverage = x.VoteAverage, + VoteCount = x.VoteCount + }) + .ToList(); break; case MovieSearchType.CurrentlyPlaying: apiMovies = await MovieApi.GetCurrentPlayingMovies(); @@ -239,8 +252,9 @@ namespace PlexRequests.UI.Modules var imdbId = string.Empty; if (counter <= 5) // Let's only do it for the first 5 items { - var movieInfoTask = await MovieApi.GetMovieInformation(movie.Id).ConfigureAwait(false); // TODO needs to be careful about this, it's adding extra time to search... - // https://www.themoviedb.org/talk/5807f4cdc3a36812160041f2 + var movieInfoTask = await MovieApi.GetMovieInformation(movie.Id).ConfigureAwait(false); + // TODO needs to be careful about this, it's adding extra time to search... + // https://www.themoviedb.org/talk/5807f4cdc3a36812160041f2 imdbId = movieInfoTask.ImdbId; counter++; } @@ -263,7 +277,8 @@ namespace PlexRequests.UI.Modules VoteCount = movie.VoteCount }; var canSee = CanUserSeeThisRequest(viewMovie.Id, settings.UsersCanViewOnlyOwnRequests, dbMovies); - var plexMovie = Checker.GetMovie(plexMovies.ToArray(), movie.Title, movie.ReleaseDate?.Year.ToString(), imdbId); + var plexMovie = Checker.GetMovie(plexMovies.ToArray(), movie.Title, movie.ReleaseDate?.Year.ToString(), + imdbId); if (plexMovie != null) { viewMovie.Available = true; @@ -288,7 +303,8 @@ namespace PlexRequests.UI.Modules return Response.AsJson(viewMovies); } - private bool CanUserSeeThisRequest(int movieId, bool usersCanViewOnlyOwnRequests, Dictionary moviesInDb) + private bool CanUserSeeThisRequest(int movieId, bool usersCanViewOnlyOwnRequests, + Dictionary moviesInDb) { if (usersCanViewOnlyOwnRequests) { @@ -302,7 +318,8 @@ namespace PlexRequests.UI.Modules private async Task SearchTvShow(string searchTerm) { - Analytics.TrackEventAsync(Category.Search, Action.TvShow, searchTerm, Username, CookieHelper.GetAnalyticClientId(Cookies)); + Analytics.TrackEventAsync(Category.Search, Action.TvShow, searchTerm, Username, + CookieHelper.GetAnalyticClientId(Cookies)); var plexSettings = await PlexService.GetSettingsAsync(); var prSettings = await PrService.GetSettingsAsync(); var providerId = string.Empty; @@ -360,7 +377,8 @@ namespace PlexRequests.UI.Modules providerId = viewT.Id.ToString(); } - var plexShow = Checker.GetTvShow(plexTvShows.ToArray(), t.show.name, t.show.premiered?.Substring(0, 4), providerId); + var plexShow = Checker.GetTvShow(plexTvShows.ToArray(), t.show.name, t.show.premiered?.Substring(0, 4), + providerId); if (plexShow != null) { viewT.Available = true; @@ -377,7 +395,8 @@ namespace PlexRequests.UI.Modules viewT.Episodes = dbt.Episodes.ToList(); viewT.Approved = dbt.Approved; } - if (sonarrCached.Select(x => x.TvdbId).Contains(tvdbid) || sickRageCache.Contains(tvdbid)) // compare to the sonarr/sickrage db + if (sonarrCached.Select(x => x.TvdbId).Contains(tvdbid) || sickRageCache.Contains(tvdbid)) + // compare to the sonarr/sickrage db { viewT.Requested = true; } @@ -391,7 +410,8 @@ namespace PlexRequests.UI.Modules private async Task SearchAlbum(string searchTerm) { - Analytics.TrackEventAsync(Category.Search, Action.Album, searchTerm, Username, CookieHelper.GetAnalyticClientId(Cookies)); + Analytics.TrackEventAsync(Category.Search, Action.Album, searchTerm, Username, + CookieHelper.GetAnalyticClientId(Cookies)); var apiAlbums = new List(); await Task.Run(() => MusicBrainzApi.SearchAlbum(searchTerm)).ContinueWith((t) => { @@ -448,7 +468,7 @@ namespace PlexRequests.UI.Modules if (Security.DoesNotHavePermissions(Permissions.ReadOnlyUser, User)) { return - Response.AsJson(new JsonResponseModel() + Response.AsJson(new JsonResponseModel { Result = false, Message = "Sorry, you do not have the correct permissions to request a movie!" @@ -457,12 +477,19 @@ namespace PlexRequests.UI.Modules var settings = await PrService.GetSettingsAsync(); if (!await CheckRequestLimit(settings, RequestType.Movie)) { - return Response.AsJson(new JsonResponseModel { Result = false, Message = "You have reached your weekly request limit for Movies! Please contact your admin." }); + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = "You have reached your weekly request limit for Movies! Please contact your admin." + }); } - Analytics.TrackEventAsync(Category.Search, Action.Request, "Movie", Username, CookieHelper.GetAnalyticClientId(Cookies)); + Analytics.TrackEventAsync(Category.Search, Action.Request, "Movie", Username, + CookieHelper.GetAnalyticClientId(Cookies)); var movieInfo = await MovieApi.GetMovieInformation(movieId); - var fullMovieName = $"{movieInfo.Title}{(movieInfo.ReleaseDate.HasValue ? $" ({movieInfo.ReleaseDate.Value.Year})" : string.Empty)}"; + var fullMovieName = + $"{movieInfo.Title}{(movieInfo.ReleaseDate.HasValue ? $" ({movieInfo.ReleaseDate.Value.Year})" : string.Empty)}"; var existingRequest = await RequestService.CheckRequestAsync(movieId); if (existingRequest != null) @@ -474,7 +501,15 @@ namespace PlexRequests.UI.Modules await RequestService.UpdateRequestAsync(existingRequest); } - return Response.AsJson(new JsonResponseModel { Result = true, Message = settings.UsersCanViewOnlyOwnRequests ? $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}" : $"{fullMovieName} {Resources.UI.Search_AlreadyRequested}" }); + return + Response.AsJson(new JsonResponseModel + { + Result = true, + Message = + settings.UsersCanViewOnlyOwnRequests + ? $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}" + : $"{fullMovieName} {Resources.UI.Search_AlreadyRequested}" + }); } try @@ -482,13 +517,23 @@ namespace PlexRequests.UI.Modules var movies = Checker.GetPlexMovies(); if (Checker.IsMovieAvailable(movies.ToArray(), movieInfo.Title, movieInfo.ReleaseDate?.Year.ToString())) { - return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullMovieName} is already in Plex!" }); + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = $"{fullMovieName} is already in Plex!" + }); } } catch (Exception e) { Log.Error(e); - return Response.AsJson(new JsonResponseModel { Result = false, Message = string.Format(Resources.UI.Search_CouldNotCheckPlex, fullMovieName) }); + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = string.Format(Resources.UI.Search_CouldNotCheckPlex, fullMovieName) + }); } //#endif @@ -508,41 +553,58 @@ namespace PlexRequests.UI.Modules Issues = IssueState.None, }; - - if (ShouldAutoApprove(RequestType.Movie, settings)) - { - var cpSettings = await CpService.GetSettingsAsync(); - model.Approved = true; - if (cpSettings.Enabled) - { - Log.Info("Adding movie to CP (No approval required)"); - var result = CouchPotatoApi.AddMovie(model.ImdbId, cpSettings.ApiKey, model.Title, - cpSettings.FullUri, cpSettings.ProfileId); - Log.Debug("Adding movie to CP result {0}", result); - if (result) - { - return await AddRequest(model, settings, $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}"); - } - - return Response.AsJson(new JsonResponseModel - { - Result = false, - Message = Resources.UI.Search_CouchPotatoError - }); - } - model.Approved = true; - return await AddRequest(model, settings, $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}"); - } - try { + if (ShouldAutoApprove(RequestType.Movie, settings)) + { + var cpSettings = await CpService.GetSettingsAsync(); + model.Approved = true; + if (cpSettings.Enabled) + { + Log.Info("Adding movie to CP (No approval required)"); + var result = CouchPotatoApi.AddMovie(model.ImdbId, cpSettings.ApiKey, model.Title, + cpSettings.FullUri, cpSettings.ProfileId); + Log.Debug("Adding movie to CP result {0}", result); + if (result) + { + return + await + AddRequest(model, settings, + $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}"); + } + + return Response.AsJson(new JsonResponseModel + { + Result = false, + Message = Resources.UI.Search_CouchPotatoError + }); + } + model.Approved = true; + return await AddRequest(model, settings, $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}"); + } + + return await AddRequest(model, settings, $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}"); } catch (Exception e) { Log.Fatal(e); + await FaultQueue.QueueItemAsync(model, RequestType.Movie, FaultType.RequestFault); - return Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_CouchPotatoError }); + await NotificationService.Publish(new NotificationModel + { + DateTime = DateTime.Now, + User = Username, + RequestType = RequestType.Movie, + Title = model.Title, + NotificationType = NotificationType.ItemAddedToFaultQueue + }); + + return Response.AsJson(new JsonResponseModel + { + Result = true, + Message = $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}" + }); } } @@ -576,9 +638,15 @@ namespace PlexRequests.UI.Modules var settings = await PrService.GetSettingsAsync(); if (!await CheckRequestLimit(settings, RequestType.TvShow)) { - return Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_WeeklyRequestLimitTVShow }); + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = Resources.UI.Search_WeeklyRequestLimitTVShow + }); } - Analytics.TrackEventAsync(Category.Search, Action.Request, "TvShow", Username, CookieHelper.GetAnalyticClientId(Cookies)); + Analytics.TrackEventAsync(Category.Search, Action.Request, "TvShow", Username, + CookieHelper.GetAnalyticClientId(Cookies)); var sonarrSettings = SonarrService.GetSettingsAsync(); @@ -590,7 +658,13 @@ namespace PlexRequests.UI.Modules var s = await sonarrSettings; if (!s.Enabled) { - return Response.AsJson(new JsonResponseModel { Message = "This is currently only supported with Sonarr, Please enable Sonarr for this feature", Result = false }); + return + Response.AsJson(new JsonResponseModel + { + Message = + "This is currently only supported with Sonarr, Please enable Sonarr for this feature", + Result = false + }); } } @@ -599,14 +673,8 @@ namespace PlexRequests.UI.Modules DateTime.TryParse(showInfo.premiered, out firstAir); string fullShowName = $"{showInfo.name} ({firstAir.Year})"; - if (showInfo.externals?.thetvdb == null) - { - return Response.AsJson(new JsonResponseModel { Result = false, Message = "Our TV Provider (TVMaze) doesn't have a TheTVDBId for this TV Show :( We cannot add the TV Show automatically sorry! Please report this problem to the server admin so he/she can sort it out!" }); - } - var model = new RequestedModel { - ProviderId = showInfo.externals?.thetvdb ?? 0, Type = RequestType.TvShow, Overview = showInfo.summary.RemoveHtml(), PosterPath = showInfo.image?.medium, @@ -622,6 +690,25 @@ namespace PlexRequests.UI.Modules TvDbId = showId.ToString() }; + if (showInfo.externals?.thetvdb == null) + { + await FaultQueue.QueueItemAsync(model, RequestType.TvShow, FaultType.MissingInformation); + await NotificationService.Publish(new NotificationModel + { + DateTime = DateTime.Now, + User = Username, + RequestType = RequestType.TvShow, + Title = model.Title, + NotificationType = NotificationType.ItemAddedToFaultQueue + }); + return Response.AsJson(new JsonResponseModel + { + Result = true, + Message = $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}" + }); + } + + model.ProviderId = showInfo.externals?.thetvdb ?? 0; var seasonsList = new List(); switch (seasons) @@ -642,9 +729,14 @@ namespace PlexRequests.UI.Modules foreach (var ep in episodeModel?.Episodes ?? new Models.EpisodesModel[0]) { - model.Episodes.Add(new EpisodesModel { EpisodeNumber = ep.EpisodeNumber, SeasonNumber = ep.SeasonNumber }); + model.Episodes.Add(new EpisodesModel + { + EpisodeNumber = ep.EpisodeNumber, + SeasonNumber = ep.SeasonNumber + }); } - Analytics.TrackEventAsync(Category.Requests, Action.TvShow, $"Episode request for {model.Title}", Username, CookieHelper.GetAnalyticClientId(Cookies)); + Analytics.TrackEventAsync(Category.Requests, Action.TvShow, $"Episode request for {model.Title}", + Username, CookieHelper.GetAnalyticClientId(Cookies)); break; default: model.SeasonsRequested = seasons; @@ -691,7 +783,12 @@ namespace PlexRequests.UI.Modules else { // We no episodes to approve - return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}" }); + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}" + }); } } else if (model.SeasonList.Except(existingRequest.SeasonList).Any()) @@ -720,9 +817,19 @@ namespace PlexRequests.UI.Modules var cachedEpisodes = cachedEpisodesTask.ToList(); foreach (var d in difference) // difference is from an existing request { - if (cachedEpisodes.Any(x => x.SeasonNumber == d.SeasonNumber && x.EpisodeNumber == d.EpisodeNumber && x.ProviderId == providerId)) + if ( + cachedEpisodes.Any( + x => + x.SeasonNumber == d.SeasonNumber && x.EpisodeNumber == d.EpisodeNumber && + x.ProviderId == providerId)) { - return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullShowName} {d.SeasonNumber} - {d.EpisodeNumber} {Resources.UI.Search_AlreadyInPlex}" }); + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = + $"{fullShowName} {d.SeasonNumber} - {d.EpisodeNumber} {Resources.UI.Search_AlreadyInPlex}" + }); } } @@ -738,66 +845,118 @@ namespace PlexRequests.UI.Modules var result = Checker.IsEpisodeAvailable(showId.ToString(), s.SeasonNumber, s.EpisodeNumber); if (result) { - return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}" }); + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}" + }); } } } - else if (Checker.IsTvShowAvailable(shows.ToArray(), showInfo.name, showInfo.premiered?.Substring(0, 4), providerId, model.SeasonList)) + else if (Checker.IsTvShowAvailable(shows.ToArray(), showInfo.name, showInfo.premiered?.Substring(0, 4), + providerId, model.SeasonList)) { - return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}" }); + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}" + }); } } } catch (Exception) { - return Response.AsJson(new JsonResponseModel { Result = false, Message = string.Format(Resources.UI.Search_CouldNotCheckPlex, fullShowName) }); + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = string.Format(Resources.UI.Search_CouldNotCheckPlex, fullShowName) + }); } - - if (ShouldAutoApprove(RequestType.TvShow, settings)) + try { - model.Approved = true; - var s = await sonarrSettings; - var sender = new TvSenderOld(SonarrApi, SickrageApi); // TODO put back - if (s.Enabled) + if (ShouldAutoApprove(RequestType.TvShow, settings)) { - var result = await sender.SendToSonarr(s, model); - if (!string.IsNullOrEmpty(result?.title)) + model.Approved = true; + var s = await sonarrSettings; + var sender = new TvSenderOld(SonarrApi, SickrageApi); // TODO put back + if (s.Enabled) { - if (existingRequest != null) + var result = await sender.SendToSonarr(s, model); + if (!string.IsNullOrEmpty(result?.title)) { - return await UpdateRequest(model, settings, + if (existingRequest != null) + { + return await UpdateRequest(model, settings, + $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); + } + return + await + AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); } - return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); + Log.Debug("Error with sending to sonarr."); + return + Response.AsJson(ValidationHelper.SendSonarrError(result?.ErrorMessages ?? new List())); } - Log.Debug("Error with sending to sonarr."); - return Response.AsJson(ValidationHelper.SendSonarrError(result?.ErrorMessages ?? new List())); - } - var srSettings = SickRageService.GetSettings(); - if (srSettings.Enabled) - { - var result = sender.SendToSickRage(srSettings, model); - if (result?.result == "success") + var srSettings = SickRageService.GetSettings(); + if (srSettings.Enabled) { - return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); + var result = sender.SendToSickRage(srSettings, model); + if (result?.result == "success") + { + return + await + AddRequest(model, settings, + $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); + } + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = result?.message ?? Resources.UI.Search_SickrageError + }); } - return Response.AsJson(new JsonResponseModel { Result = false, Message = result?.message ?? Resources.UI.Search_SickrageError }); + + if (!srSettings.Enabled && !s.Enabled) + { + return + await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); + } + + return + Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_TvNotSetUp }); + } - - if (!srSettings.Enabled && !s.Enabled) - { - return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); - } - - return Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_TvNotSetUp }); - + return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); + } + catch (Exception e) + { + await FaultQueue.QueueItemAsync(model, RequestType.TvShow, FaultType.RequestFault); + await NotificationService.Publish(new NotificationModel + { + DateTime = DateTime.Now, + User = Username, + RequestType = RequestType.TvShow, + Title = model.Title, + NotificationType = NotificationType.ItemAddedToFaultQueue + }); + Log.Error(e); + return + Response.AsJson(new JsonResponseModel + { + Result = true, + Message = $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}" + }); } - return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); } - private async Task AddUserToRequest(RequestedModel existingRequest, PlexRequestSettings settings, string fullShowName, bool episodeReq = false) + private async Task AddUserToRequest(RequestedModel existingRequest, PlexRequestSettings settings, + string fullShowName, bool episodeReq = false) { // check if the current user is already marked as a requester for this show, if not, add them if (!existingRequest.UserHasRequested(Username)) @@ -812,12 +971,15 @@ namespace PlexRequests.UI.Modules $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); } - return await UpdateRequest(existingRequest, settings, $"{fullShowName} {Resources.UI.Search_AlreadyRequested}"); + return + await UpdateRequest(existingRequest, settings, $"{fullShowName} {Resources.UI.Search_AlreadyRequested}"); } private bool ShouldSendNotification(RequestType type, PlexRequestSettings prSettings) { - var sendNotification = ShouldAutoApprove(type, prSettings) ? !prSettings.IgnoreNotifyForAutoApprovedRequests : true; + var sendNotification = ShouldAutoApprove(type, prSettings) + ? !prSettings.IgnoreNotifyForAutoApprovedRequests + : true; var claims = Context.CurrentUser?.Claims; if (claims != null) { @@ -836,9 +998,15 @@ namespace PlexRequests.UI.Modules var settings = await PrService.GetSettingsAsync(); if (!await CheckRequestLimit(settings, RequestType.Album)) { - return Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_WeeklyRequestLimitAlbums }); + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = Resources.UI.Search_WeeklyRequestLimitAlbums + }); } - Analytics.TrackEventAsync(Category.Search, Action.Request, "Album", Username, CookieHelper.GetAnalyticClientId(Cookies)); + Analytics.TrackEventAsync(Category.Search, Action.Request, "Album", Username, + CookieHelper.GetAnalyticClientId(Cookies)); var existingRequest = await RequestService.CheckRequestAsync(releaseId); if (existingRequest != null) @@ -848,7 +1016,15 @@ namespace PlexRequests.UI.Modules existingRequest.RequestedUsers.Add(Username); await RequestService.UpdateRequestAsync(existingRequest); } - return Response.AsJson(new JsonResponseModel { Result = true, Message = settings.UsersCanViewOnlyOwnRequests ? $"{existingRequest.Title} {Resources.UI.Search_SuccessfullyAdded}" : $"{existingRequest.Title} {Resources.UI.Search_AlreadyRequested}" }); + return + Response.AsJson(new JsonResponseModel + { + Result = true, + Message = + settings.UsersCanViewOnlyOwnRequests + ? $"{existingRequest.Title} {Resources.UI.Search_SuccessfullyAdded}" + : $"{existingRequest.Title} {Resources.UI.Search_AlreadyRequested}" + }); } var albumInfo = MusicBrainzApi.GetAlbum(releaseId); @@ -858,11 +1034,17 @@ namespace PlexRequests.UI.Modules var artist = albumInfo.ArtistCredits?.FirstOrDefault()?.artist; if (artist == null) { - return Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_MusicBrainzError }); + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = Resources.UI.Search_MusicBrainzError + }); } var albums = Checker.GetPlexAlbums(); - var alreadyInPlex = Checker.IsAlbumAvailable(albums.ToArray(), albumInfo.title, release.ToString("yyyy"), artist.name); + var alreadyInPlex = Checker.IsAlbumAvailable(albums.ToArray(), albumInfo.title, release.ToString("yyyy"), + artist.name); if (alreadyInPlex) { @@ -892,28 +1074,46 @@ namespace PlexRequests.UI.Modules ArtistId = artist.id }; - if (ShouldAutoApprove(RequestType.Album, settings)) + try { - model.Approved = true; - var hpSettings = HeadphonesService.GetSettings(); - - if (!hpSettings.Enabled) + if (ShouldAutoApprove(RequestType.Album, settings)) { - await RequestService.AddRequestAsync(model); - return - Response.AsJson(new JsonResponseModel - { - Result = true, - Message = $"{model.Title} {Resources.UI.Search_SuccessfullyAdded}" - }); + model.Approved = true; + var hpSettings = HeadphonesService.GetSettings(); + + if (!hpSettings.Enabled) + { + await RequestService.AddRequestAsync(model); + return + Response.AsJson(new JsonResponseModel + { + Result = true, + Message = $"{model.Title} {Resources.UI.Search_SuccessfullyAdded}" + }); + } + + var sender = new HeadphonesSender(HeadphonesApi, hpSettings, RequestService); + await sender.AddAlbum(model); + return await AddRequest(model, settings, $"{model.Title} {Resources.UI.Search_SuccessfullyAdded}"); } - var sender = new HeadphonesSender(HeadphonesApi, hpSettings, RequestService); - await sender.AddAlbum(model); return await AddRequest(model, settings, $"{model.Title} {Resources.UI.Search_SuccessfullyAdded}"); } + catch (Exception e) + { + Log.Error(e); + await FaultQueue.QueueItemAsync(model, RequestType.Movie, FaultType.RequestFault); - return await AddRequest(model, settings, $"{model.Title} {Resources.UI.Search_SuccessfullyAdded}"); + await NotificationService.Publish(new NotificationModel + { + DateTime = DateTime.Now, + User = Username, + RequestType = RequestType.Album, + Title = model.Title, + NotificationType = NotificationType.ItemAddedToFaultQueue + }); + throw; + } } private string GetMusicBrainzCoverArt(string id) diff --git a/PlexRequests.UI/NinjectModules/ServicesModule.cs b/PlexRequests.UI/NinjectModules/ServicesModule.cs index a180335d6..a7ba11dea 100644 --- a/PlexRequests.UI/NinjectModules/ServicesModule.cs +++ b/PlexRequests.UI/NinjectModules/ServicesModule.cs @@ -25,7 +25,7 @@ // ************************************************************************/ #endregion using Ninject.Modules; - +using PlexRequests.Core.Queue; using PlexRequests.Helpers.Analytics; using PlexRequests.Services.Interfaces; using PlexRequests.Services.Jobs; @@ -51,6 +51,8 @@ namespace PlexRequests.UI.NinjectModules Bind().To(); Bind().To(); Bind().To(); + + Bind().To(); } } } \ No newline at end of file From c5b65a335fc64a46f76722ad9e818ba4d9406a34 Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Tue, 8 Nov 2016 14:32:03 +0000 Subject: [PATCH 10/87] Finished #556 --- .../PlexRequests.Core.Tests.csproj | 1 - PlexRequests.Core.Tests/StatusCheckerTests.cs | 46 ----------------- .../Interfaces/INotificationEngine.cs | 5 +- .../Jobs/PlexAvailabilityChecker.cs | 2 +- .../Notification/EmailMessageNotification.cs | 50 +++++++++++++++++-- .../Notification/NotificationEngine.cs | 16 +++--- PlexRequests.UI.Tests/SearchModuleTests.cs | 5 +- PlexRequests.UI/Modules/ApprovalModule.cs | 4 +- PlexRequests.UI/Modules/RequestsModule.cs | 2 +- 9 files changed, 66 insertions(+), 65 deletions(-) delete mode 100644 PlexRequests.Core.Tests/StatusCheckerTests.cs diff --git a/PlexRequests.Core.Tests/PlexRequests.Core.Tests.csproj b/PlexRequests.Core.Tests/PlexRequests.Core.Tests.csproj index 0e6f4d3e0..5002e3c21 100644 --- a/PlexRequests.Core.Tests/PlexRequests.Core.Tests.csproj +++ b/PlexRequests.Core.Tests/PlexRequests.Core.Tests.csproj @@ -60,7 +60,6 @@ - diff --git a/PlexRequests.Core.Tests/StatusCheckerTests.cs b/PlexRequests.Core.Tests/StatusCheckerTests.cs deleted file mode 100644 index 38b9b4674..000000000 --- a/PlexRequests.Core.Tests/StatusCheckerTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -#region Copyright -// /************************************************************************ -// Copyright (c) 2016 Jamie Rees -// File: AuthenticationSettingsTests.cs -// Created By: Jamie Rees -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// ************************************************************************/ -#endregion - -using System; -using NUnit.Framework; - -namespace PlexRequests.Core.Tests -{ - [TestFixture] - public class StatusCheckerTests - { - [Test] - [Ignore("API Limit")] - public void CheckStatusTest() - { - var checker = new StatusChecker(); - var status = checker.GetStatus(); - - Assert.That(status, Is.Not.Null); - } - } -} diff --git a/PlexRequests.Services/Interfaces/INotificationEngine.cs b/PlexRequests.Services/Interfaces/INotificationEngine.cs index ba53eb9bf..5a4198d00 100644 --- a/PlexRequests.Services/Interfaces/INotificationEngine.cs +++ b/PlexRequests.Services/Interfaces/INotificationEngine.cs @@ -27,13 +27,14 @@ using System.Collections.Generic; using System.Threading.Tasks; +using PlexRequests.Core.Models; using PlexRequests.Store; namespace PlexRequests.Services.Interfaces { public interface INotificationEngine { - Task NotifyUsers(IEnumerable modelChanged, string apiKey); - Task NotifyUsers(RequestedModel modelChanged, string apiKey); + Task NotifyUsers(IEnumerable modelChanged, string apiKey, NotificationType type); + Task NotifyUsers(RequestedModel modelChanged, string apiKey, NotificationType type); } } \ No newline at end of file diff --git a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs index 161ea0ec3..a0d3ed85a 100644 --- a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs +++ b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs @@ -153,7 +153,7 @@ namespace PlexRequests.Services.Jobs if (modifiedModel.Any()) { - NotificationEngine.NotifyUsers(modifiedModel, plexSettings.PlexAuthToken); + NotificationEngine.NotifyUsers(modifiedModel, plexSettings.PlexAuthToken, NotificationType.RequestAvailable); RequestService.BatchUpdate(modifiedModel); } diff --git a/PlexRequests.Services/Notification/EmailMessageNotification.cs b/PlexRequests.Services/Notification/EmailMessageNotification.cs index 52727dba8..cc22155f2 100644 --- a/PlexRequests.Services/Notification/EmailMessageNotification.cs +++ b/PlexRequests.Services/Notification/EmailMessageNotification.cs @@ -79,8 +79,8 @@ namespace PlexRequests.Services.Notification await EmailAvailableRequest(model, emailSettings); break; case NotificationType.RequestApproved: - throw new NotImplementedException(); - + await EmailRequestApproved(model, emailSettings); + break; case NotificationType.AdminNote: throw new NotImplementedException(); @@ -88,8 +88,8 @@ namespace PlexRequests.Services.Notification await EmailTest(model, emailSettings); break; case NotificationType.RequestDeclined: - throw new NotImplementedException(); - + await EmailRequestDeclined(model, emailSettings); + break; case NotificationType.ItemAddedToFaultQueue: await EmailAddedToRequestQueue(model, emailSettings); break; @@ -193,6 +193,48 @@ namespace PlexRequests.Services.Notification await Send(message, settings); } + private async Task EmailRequestDeclined(NotificationModel model, EmailNotificationSettings settings) + { + var email = new EmailBasicTemplate(); + var html = email.LoadTemplate( + "Plex Requests: Your request has been declined", + $"Hello! Your request for {model.Title} has been declined, Sorry!", + model.ImgSrc); + var body = new BodyBuilder { HtmlBody = html, TextBody = $"Hello! Your request for {model.Title} has been declined, Sorry!", }; + + var message = new MimeMessage + { + Body = body.ToMessageBody(), + Subject = $"Plex Requests: Your request has been declined" + }; + message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender)); + message.To.Add(new MailboxAddress(model.UserEmail, model.UserEmail)); + + + await Send(message, settings); + } + + private async Task EmailRequestApproved(NotificationModel model, EmailNotificationSettings settings) + { + var email = new EmailBasicTemplate(); + var html = email.LoadTemplate( + "Plex Requests: Your request has been approved!", + $"Hello! Your request for {model.Title} has been approved!", + model.ImgSrc); + var body = new BodyBuilder { HtmlBody = html, TextBody = $"Hello! Your request for {model.Title} has been approved!", }; + + var message = new MimeMessage + { + Body = body.ToMessageBody(), + Subject = $"Plex Requests: Your request has been approved!" + }; + message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender)); + message.To.Add(new MailboxAddress(model.UserEmail, model.UserEmail)); + + + await Send(message, settings); + } + private async Task EmailAvailableRequest(NotificationModel model, EmailNotificationSettings settings) { if (!settings.EnableUserEmailNotifications) diff --git a/PlexRequests.Services/Notification/NotificationEngine.cs b/PlexRequests.Services/Notification/NotificationEngine.cs index 50dc4fc49..f0ba5bec5 100644 --- a/PlexRequests.Services/Notification/NotificationEngine.cs +++ b/PlexRequests.Services/Notification/NotificationEngine.cs @@ -55,7 +55,7 @@ namespace PlexRequests.Services.Notification private static Logger Log = LogManager.GetCurrentClassLogger(); private INotificationService Notification { get; } - public async Task NotifyUsers(IEnumerable modelChanged, string apiKey) + public async Task NotifyUsers(IEnumerable modelChanged, string apiKey, NotificationType type) { try { @@ -75,7 +75,7 @@ namespace PlexRequests.Services.Notification if (user.Equals(adminUsername, StringComparison.CurrentCultureIgnoreCase)) { Log.Info("This user is the Plex server owner"); - await PublishUserNotification(userAccount.Username, userAccount.Email, model.Title, model.PosterPath); + await PublishUserNotification(userAccount.Username, userAccount.Email, model.Title, model.PosterPath, type); return; } @@ -88,7 +88,7 @@ namespace PlexRequests.Services.Notification } Log.Info("Sending notification to: {0} at: {1}, for title: {2}", email.Username, email.Email, model.Title); - await PublishUserNotification(email.Username, email.Email, model.Title, model.PosterPath); + await PublishUserNotification(email.Username, email.Email, model.Title, model.PosterPath, type); } } } @@ -98,7 +98,7 @@ namespace PlexRequests.Services.Notification } } - public async Task NotifyUsers(RequestedModel model, string apiKey) + public async Task NotifyUsers(RequestedModel model, string apiKey, NotificationType type) { try { @@ -117,7 +117,7 @@ namespace PlexRequests.Services.Notification if (user.Equals(adminUsername, StringComparison.CurrentCultureIgnoreCase)) { Log.Info("This user is the Plex server owner"); - await PublishUserNotification(userAccount.Username, userAccount.Email, model.Title, model.PosterPath); + await PublishUserNotification(userAccount.Username, userAccount.Email, model.Title, model.PosterPath, type); return; } @@ -130,7 +130,7 @@ namespace PlexRequests.Services.Notification } Log.Info("Sending notification to: {0} at: {1}, for title: {2}", email.Username, email.Email, model.Title); - await PublishUserNotification(email.Username, email.Email, model.Title, model.PosterPath); + await PublishUserNotification(email.Username, email.Email, model.Title, model.PosterPath, type); } } catch (Exception e) @@ -139,13 +139,13 @@ namespace PlexRequests.Services.Notification } } - private async Task PublishUserNotification(string username, string email, string title, string img) + private async Task PublishUserNotification(string username, string email, string title, string img, NotificationType type) { var notificationModel = new NotificationModel { User = username, UserEmail = email, - NotificationType = NotificationType.RequestAvailable, + NotificationType = type, Title = title, ImgSrc = img }; diff --git a/PlexRequests.UI.Tests/SearchModuleTests.cs b/PlexRequests.UI.Tests/SearchModuleTests.cs index cffa9487d..08c1ddfdd 100644 --- a/PlexRequests.UI.Tests/SearchModuleTests.cs +++ b/PlexRequests.UI.Tests/SearchModuleTests.cs @@ -33,6 +33,7 @@ using NUnit.Framework; using PlexRequests.Api.Interfaces; using PlexRequests.Api.Models.Plex; using PlexRequests.Core; +using PlexRequests.Core.Queue; using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; using PlexRequests.Helpers.Analytics; @@ -73,6 +74,7 @@ namespace PlexRequests.UI.Tests private Mock> _emailSettings; private Mock _issueService; private Mock _cache; + private Mock _faultQueue; private Mock> RequestLimitRepo { get; set; } private SearchModule Search { get; set; } private readonly Fixture F = new Fixture(); @@ -145,6 +147,7 @@ namespace PlexRequests.UI.Tests RequestLimitRepo = new Mock>(); _emailSettings = new Mock>(); _issueService = new Mock(); + _faultQueue = new Mock(); CreateModule(); } @@ -155,7 +158,7 @@ namespace PlexRequests.UI.Tests _sickRageSettingsMock.Object, _cpApi.Object, _srApi.Object, _notificationService.Object, _music.Object, _hpAPi.Object, _headphonesSettings.Object, _cpCache.Object, _sonarrCache.Object, _srCache.Object, _plexApi.Object, _plexSettingsMock.Object, _authMock.Object, - _userRepo.Object, _emailSettings.Object, _issueService.Object, _analytics.Object, RequestLimitRepo.Object); + _userRepo.Object, _emailSettings.Object, _issueService.Object, _analytics.Object, RequestLimitRepo.Object, _faultQueue.Object); } diff --git a/PlexRequests.UI/Modules/ApprovalModule.cs b/PlexRequests.UI/Modules/ApprovalModule.cs index e0ba7314d..09ceef150 100644 --- a/PlexRequests.UI/Modules/ApprovalModule.cs +++ b/PlexRequests.UI/Modules/ApprovalModule.cs @@ -37,6 +37,7 @@ using NLog; using PlexRequests.Api; using PlexRequests.Api.Interfaces; using PlexRequests.Core; +using PlexRequests.Core.Queue; using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; using PlexRequests.Store; @@ -50,7 +51,7 @@ namespace PlexRequests.UI.Modules public ApprovalModule(IRequestService service, ISettingsService cpService, ICouchPotatoApi cpApi, ISonarrApi sonarrApi, ISettingsService sonarrSettings, ISickRageApi srApi, ISettingsService srSettings, - ISettingsService hpSettings, IHeadphonesApi hpApi, ISettingsService pr) : base("approval", pr) + ISettingsService hpSettings, IHeadphonesApi hpApi, ISettingsService pr, ITransientFaultQueue faultQueue) : base("approval", pr) { this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser); @@ -85,6 +86,7 @@ namespace PlexRequests.UI.Modules private ISickRageApi SickRageApi { get; } private ICouchPotatoApi CpApi { get; } private IHeadphonesApi HeadphoneApi { get; } + private ITransientFaultQueue FaultQueue { get} /// /// Approves the specified request identifier. diff --git a/PlexRequests.UI/Modules/RequestsModule.cs b/PlexRequests.UI/Modules/RequestsModule.cs index 13aecdc03..068c3de16 100644 --- a/PlexRequests.UI/Modules/RequestsModule.cs +++ b/PlexRequests.UI/Modules/RequestsModule.cs @@ -386,7 +386,7 @@ namespace PlexRequests.UI.Modules var result = await Service.UpdateRequestAsync(originalRequest); var plexService = await PlexSettings.GetSettingsAsync(); - await NotificationEngine.NotifyUsers(originalRequest, plexService.PlexAuthToken); + await NotificationEngine.NotifyUsers(originalRequest, plexService.PlexAuthToken, available ? NotificationType.RequestAvailable : NotificationType.RequestDeclined); return Response.AsJson(result ? new { Result = true, Available = available, Message = string.Empty } : new { Result = false, Available = false, Message = "Could not update the availability, please try again or check the logs" }); From bcfd675d3d06a6ba75bb56fd1bdd5549032fdae4 Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Tue, 8 Nov 2016 14:36:01 +0000 Subject: [PATCH 11/87] Finished the notification for the fault queue --- .../Notification/PushbulletNotification.cs | 12 ++++++++++++ .../Notification/PushoverNotification.cs | 11 +++++++++++ .../Notification/SlackNotification.cs | 11 +++++++++++ 3 files changed, 34 insertions(+) diff --git a/PlexRequests.Services/Notification/PushbulletNotification.cs b/PlexRequests.Services/Notification/PushbulletNotification.cs index e2ae3c4c4..ff1d2e48d 100644 --- a/PlexRequests.Services/Notification/PushbulletNotification.cs +++ b/PlexRequests.Services/Notification/PushbulletNotification.cs @@ -81,6 +81,11 @@ namespace PlexRequests.Services.Notification case NotificationType.Test: await PushTestAsync(pushSettings); break; + case NotificationType.RequestDeclined: + break; + case NotificationType.ItemAddedToFaultQueue: + await PushFaultQueue(model, pushSettings); + break; default: throw new ArgumentOutOfRangeException(); } @@ -125,6 +130,13 @@ namespace PlexRequests.Services.Notification await Push(settings, message, pushTitle); } + private async Task PushFaultQueue(NotificationModel model, PushbulletNotificationSettings settings) + { + var message = $"Hello! The user '{model.User}' has requested {model.Title} but it could not be added. This has been added into the requests queue and will keep retrying"; + var pushTitle = $"Plex Requests: The {model.RequestType.GetString()?.ToLower()} {model.Title} has been requested but could not be added!"; + await Push(settings, message, pushTitle); + } + private async Task Push(PushbulletNotificationSettings settings, string message, string title) { try diff --git a/PlexRequests.Services/Notification/PushoverNotification.cs b/PlexRequests.Services/Notification/PushoverNotification.cs index 2adafb11e..f04ed8560 100644 --- a/PlexRequests.Services/Notification/PushoverNotification.cs +++ b/PlexRequests.Services/Notification/PushoverNotification.cs @@ -81,6 +81,11 @@ namespace PlexRequests.Services.Notification case NotificationType.Test: await PushTestAsync(model, pushSettings); break; + case NotificationType.RequestDeclined: + break; + case NotificationType.ItemAddedToFaultQueue: + await PushFaultQueue(model, pushSettings); + break; default: throw new ArgumentOutOfRangeException(); } @@ -122,6 +127,12 @@ namespace PlexRequests.Services.Notification await Push(settings, message); } + private async Task PushFaultQueue(NotificationModel model, PushoverNotificationSettings settings) + { + var message = $"Hello! The user '{model.User}' has requested {model.Title} but it could not be added. This has been added into the requests queue and will keep retrying"; + await Push(settings, message); + } + private async Task Push(PushoverNotificationSettings settings, string message) { try diff --git a/PlexRequests.Services/Notification/SlackNotification.cs b/PlexRequests.Services/Notification/SlackNotification.cs index f8c90d3ca..41ce4cc05 100644 --- a/PlexRequests.Services/Notification/SlackNotification.cs +++ b/PlexRequests.Services/Notification/SlackNotification.cs @@ -88,6 +88,11 @@ namespace PlexRequests.Services.Notification case NotificationType.Test: await PushTest(pushSettings); break; + case NotificationType.RequestDeclined: + break; + case NotificationType.ItemAddedToFaultQueue: + await PushFaultQueue(model, pushSettings); + break; default: throw new ArgumentOutOfRangeException(); } @@ -111,6 +116,12 @@ namespace PlexRequests.Services.Notification await Push(settings, message); } + private async Task PushFaultQueue(NotificationModel model, SlackNotificationSettings settings) + { + var message = $"Hello! The user '{model.User}' has requested {model.Title} but it could not be added. This has been added into the requests queue and will keep retrying"; + await Push(settings, message); + } + private async Task Push(SlackNotificationSettings config, string message) { try From 5a5f7f5610cb19892d3c23e6ab8e55e55876361b Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Tue, 8 Nov 2016 14:46:42 +0000 Subject: [PATCH 12/87] Fixed build --- PlexRequests.UI/Modules/ApprovalModule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PlexRequests.UI/Modules/ApprovalModule.cs b/PlexRequests.UI/Modules/ApprovalModule.cs index 09ceef150..5105b929a 100644 --- a/PlexRequests.UI/Modules/ApprovalModule.cs +++ b/PlexRequests.UI/Modules/ApprovalModule.cs @@ -86,7 +86,7 @@ namespace PlexRequests.UI.Modules private ISickRageApi SickRageApi { get; } private ICouchPotatoApi CpApi { get; } private IHeadphonesApi HeadphoneApi { get; } - private ITransientFaultQueue FaultQueue { get} + private ITransientFaultQueue FaultQueue { get; } /// /// Approves the specified request identifier. From 50dec5f530e52eebd21f9fb6a9e9b6061c5d92ca Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Wed, 9 Nov 2016 14:32:23 +0000 Subject: [PATCH 13/87] Started on the queue for requests #483 TV Requests with missing information has been completed --- PlexRequests.Core/PlexRequests.Core.csproj | 2 + .../Queue/ITransientFaultQueue.cs | 4 +- .../Queue/TransientFaultQueue.cs | 12 +- .../SettingModels/ScheduledJobsSettings.cs | 1 + .../Helpers => PlexRequests.Core}/TvSender.cs | 7 +- .../TvSenderOld.cs | 2 +- .../Jobs/FaultQueueHandler.cs | 214 ++++++++++++++++++ .../PlexRequests.Services.csproj | 1 + PlexRequests.Store/Models/RequestQueue.cs | 4 +- PlexRequests.Store/SqlTables.sql | 5 +- PlexRequests.UI.Tests/TvSenderTests.cs | 27 +-- PlexRequests.UI/Jobs/Scheduler.cs | 20 +- PlexRequests.UI/Modules/SearchModule.cs | 46 ++-- PlexRequests.UI/PlexRequests.UI.csproj | 2 - 14 files changed, 276 insertions(+), 71 deletions(-) rename {PlexRequests.UI/Helpers => PlexRequests.Core}/TvSender.cs (99%) rename {PlexRequests.UI/Helpers => PlexRequests.Core}/TvSenderOld.cs (99%) create mode 100644 PlexRequests.Services/Jobs/FaultQueueHandler.cs diff --git a/PlexRequests.Core/PlexRequests.Core.csproj b/PlexRequests.Core/PlexRequests.Core.csproj index 8faf4e83a..17bb7ceca 100644 --- a/PlexRequests.Core/PlexRequests.Core.csproj +++ b/PlexRequests.Core/PlexRequests.Core.csproj @@ -137,6 +137,8 @@ + + diff --git a/PlexRequests.Core/Queue/ITransientFaultQueue.cs b/PlexRequests.Core/Queue/ITransientFaultQueue.cs index ac62add0f..649209307 100644 --- a/PlexRequests.Core/Queue/ITransientFaultQueue.cs +++ b/PlexRequests.Core/Queue/ITransientFaultQueue.cs @@ -11,7 +11,7 @@ namespace PlexRequests.Core.Queue Task DequeueAsync(); IEnumerable GetQueue(); Task> GetQueueAsync(); - void QueueItem(RequestedModel request, RequestType type, FaultType faultType); - Task QueueItemAsync(RequestedModel request, RequestType type, FaultType faultType); + void QueueItem(RequestedModel request, string id, RequestType type, FaultType faultType); + Task QueueItemAsync(RequestedModel request, string id, RequestType type, FaultType faultType); } } \ No newline at end of file diff --git a/PlexRequests.Core/Queue/TransientFaultQueue.cs b/PlexRequests.Core/Queue/TransientFaultQueue.cs index 108387f03..57fd2eb34 100644 --- a/PlexRequests.Core/Queue/TransientFaultQueue.cs +++ b/PlexRequests.Core/Queue/TransientFaultQueue.cs @@ -46,14 +46,14 @@ namespace PlexRequests.Core.Queue private IRepository RequestQueue { get; } - public void QueueItem(RequestedModel request, RequestType type, FaultType faultType) + public void QueueItem(RequestedModel request, string id, RequestType type, FaultType faultType) { //Ensure there is not a duplicate queued item var existingItem = RequestQueue.Custom( connection => { connection.Open(); - var result = connection.Query("select * from RequestQueue where PrimaryIdentifier = @ProviderId", new { ProviderId = request.ProviderId }); + var result = connection.Query("select * from RequestQueue where PrimaryIdentifier = @ProviderId", new { ProviderId = id }); return result; }).FirstOrDefault(); @@ -68,19 +68,19 @@ namespace PlexRequests.Core.Queue { Type = type, Content = ByteConverterHelper.ReturnBytes(request), - PrimaryIdentifier = request.ProviderId, + PrimaryIdentifier = id, FaultType = faultType }; RequestQueue.Insert(queue); } - public async Task QueueItemAsync(RequestedModel request, RequestType type, FaultType faultType) + public async Task QueueItemAsync(RequestedModel request, string id, RequestType type, FaultType faultType) { //Ensure there is not a duplicate queued item var existingItem = await RequestQueue.CustomAsync(async connection => { connection.Open(); - var result = await connection.QueryAsync("select * from RequestQueue where PrimaryIdentifier = @ProviderId", new { ProviderId = request.ProviderId }); + var result = await connection.QueryAsync("select * from RequestQueue where PrimaryIdentifier = @ProviderId", new { ProviderId = id }); return result; }); @@ -95,7 +95,7 @@ namespace PlexRequests.Core.Queue { Type = type, Content = ByteConverterHelper.ReturnBytes(request), - PrimaryIdentifier = request.ProviderId, + PrimaryIdentifier = id, FaultType = faultType }; await RequestQueue.InsertAsync(queue); diff --git a/PlexRequests.Core/SettingModels/ScheduledJobsSettings.cs b/PlexRequests.Core/SettingModels/ScheduledJobsSettings.cs index 234dc5774..2f1f2ccdc 100644 --- a/PlexRequests.Core/SettingModels/ScheduledJobsSettings.cs +++ b/PlexRequests.Core/SettingModels/ScheduledJobsSettings.cs @@ -42,5 +42,6 @@ namespace PlexRequests.Core.SettingModels [Obsolete("We use the CRON job now")] public int RecentlyAdded { get; set; } public string RecentlyAddedCron { get; set; } + public int FaultQueueHandler { get; set; } } } \ No newline at end of file diff --git a/PlexRequests.UI/Helpers/TvSender.cs b/PlexRequests.Core/TvSender.cs similarity index 99% rename from PlexRequests.UI/Helpers/TvSender.cs rename to PlexRequests.Core/TvSender.cs index b67311d64..7ec462582 100644 --- a/PlexRequests.UI/Helpers/TvSender.cs +++ b/PlexRequests.Core/TvSender.cs @@ -24,18 +24,19 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // ************************************************************************/ #endregion + using System; using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; using NLog; using PlexRequests.Api.Interfaces; using PlexRequests.Api.Models.SickRage; using PlexRequests.Api.Models.Sonarr; using PlexRequests.Core.SettingModels; using PlexRequests.Store; -using System.Linq; -using System.Threading.Tasks; -namespace PlexRequests.UI.Helpers +namespace PlexRequests.Core { public class TvSender { diff --git a/PlexRequests.UI/Helpers/TvSenderOld.cs b/PlexRequests.Core/TvSenderOld.cs similarity index 99% rename from PlexRequests.UI/Helpers/TvSenderOld.cs rename to PlexRequests.Core/TvSenderOld.cs index ced1c81dd..b603272b1 100644 --- a/PlexRequests.UI/Helpers/TvSenderOld.cs +++ b/PlexRequests.Core/TvSenderOld.cs @@ -36,7 +36,7 @@ using PlexRequests.Api.Models.Sonarr; using PlexRequests.Core.SettingModels; using PlexRequests.Store; -namespace PlexRequests.UI.Helpers +namespace PlexRequests.Core { public class TvSenderOld { diff --git a/PlexRequests.Services/Jobs/FaultQueueHandler.cs b/PlexRequests.Services/Jobs/FaultQueueHandler.cs new file mode 100644 index 000000000..0a540d5ad --- /dev/null +++ b/PlexRequests.Services/Jobs/FaultQueueHandler.cs @@ -0,0 +1,214 @@ +#region Copyright + +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: UserRequestLimitResetter.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ + +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; + +using NLog; +using PlexRequests.Api; +using PlexRequests.Api.Interfaces; +using PlexRequests.Core; +using PlexRequests.Core.SettingModels; +using PlexRequests.Helpers; +using PlexRequests.Services.Interfaces; +using PlexRequests.Store; +using PlexRequests.Store.Models; +using PlexRequests.Store.Repository; + +using Quartz; + +namespace PlexRequests.Services.Jobs +{ + public class FaultQueueHandler : IJob + { + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + + public FaultQueueHandler(IJobRecord record, IRepository repo, ISonarrApi sonarrApi, + ISickRageApi srApi, + ISettingsService sonarrSettings, ISettingsService srSettings) + { + Record = record; + Repo = repo; + SonarrApi = sonarrApi; + SrApi = srApi; + SickrageSettings = srSettings; + SonarrSettings = sonarrSettings; + } + + private IRepository Repo { get; } + private IJobRecord Record { get; } + private ISonarrApi SonarrApi { get; } + private ISickRageApi SrApi { get; } + private ISettingsService SonarrSettings { get; set; } + private ISettingsService SickrageSettings { get; set; } + + + public void Execute(IJobExecutionContext context) + { + try + { + var faultedRequests = Repo.GetAll().ToList(); + + + var missingInfo = faultedRequests.Where(x => x.FaultType == FaultType.MissingInformation).ToList(); + ProcessMissingInformation(missingInfo); + + var transientErrors = faultedRequests.Where(x => x.FaultType == FaultType.RequestFault).ToList(); + ProcessTransientErrors(transientErrors); + + } + catch (Exception e) + { + Log.Error(e); + } + finally + { + Record.Record(JobNames.RequestLimitReset); + } + } + + + private void ProcessMissingInformation(List requests) + { + var sonarrSettings = SonarrSettings.GetSettings(); + var sickrageSettings = SickrageSettings.GetSettings(); + + if (!requests.Any()) + { + return; + } + var tv = requests.Where(x => x.Type == RequestType.TvShow); + + // TV + var tvApi = new TvMazeApi(); + foreach (var t in tv) + { + var providerId = int.Parse(t.PrimaryIdentifier); + var showInfo = tvApi.ShowLookupByTheTvDbId(providerId); + + if (showInfo.externals?.thetvdb != null) + { + // We now have the info + var tvModel = ByteConverterHelper.ReturnObject(t.Content); + tvModel.ProviderId = showInfo.externals.thetvdb.Value; + var result = ProcessTvShow(tvModel, sonarrSettings, sickrageSettings); + + if (!result) + { + // we now have the info but couldn't add it, so add it back into the queue but with a different fault + t.Content = ByteConverterHelper.ReturnBytes(tvModel); + t.FaultType = FaultType.RequestFault; + t.LastRetry = DateTime.UtcNow; + Repo.Update(t); + } + else + { + // Successful, remove from the fault queue + Repo.Delete(t); + } + } + } + } + + private bool ProcessTvShow(RequestedModel tvModel, SonarrSettings sonarr, SickRageSettings sickrage) + { + try + { + + var sender = new TvSenderOld(SonarrApi, SrApi); + if (sonarr.Enabled) + { + var task = sender.SendToSonarr(sonarr, tvModel, sonarr.QualityProfile); + var a = task.Result; + if (string.IsNullOrEmpty(a?.title)) + { + // Couldn't send it + return false; + } + return true; + } + + if (sickrage.Enabled) + { + var result = sender.SendToSickRage(sickrage, tvModel); + if (result?.result != "success") + { + // Couldn't send it + return false; + } + return true; + } + + return false; + } + catch (Exception e) + { + Log.Error(e); + return false; // It fails so it will get added back into the queue + } + } + + private void ProcessTransientErrors(List requests) + { + var sonarrSettings = SonarrSettings.GetSettings(); + var sickrageSettings = SickrageSettings.GetSettings(); + + if (!requests.Any()) + { + return; + } + var tv = requests.Where(x => x.Type == RequestType.TvShow); + var movie = requests.Where(x => x.Type == RequestType.Movie); + var album = requests.Where(x => x.Type == RequestType.Album); + + + + foreach (var t in tv) + { + var tvModel = ByteConverterHelper.ReturnObject(t.Content); + var result = ProcessTvShow(tvModel, sonarrSettings, sickrageSettings); + + if (!result) + { + // we now have the info but couldn't add it, so do nothing now. + t.LastRetry = DateTime.UtcNow; + Repo.Update(t); + } + else + { + // Successful, remove from the fault queue + Repo.Delete(t); + } + } + + + } + } +} \ No newline at end of file diff --git a/PlexRequests.Services/PlexRequests.Services.csproj b/PlexRequests.Services/PlexRequests.Services.csproj index d53128879..1f542944b 100644 --- a/PlexRequests.Services/PlexRequests.Services.csproj +++ b/PlexRequests.Services/PlexRequests.Services.csproj @@ -93,6 +93,7 @@ + diff --git a/PlexRequests.Store/Models/RequestQueue.cs b/PlexRequests.Store/Models/RequestQueue.cs index 15067fbc7..f98517ab3 100644 --- a/PlexRequests.Store/Models/RequestQueue.cs +++ b/PlexRequests.Store/Models/RequestQueue.cs @@ -25,6 +25,7 @@ // ************************************************************************/ #endregion +using System; using Dapper.Contrib.Extensions; namespace PlexRequests.Store.Models @@ -32,13 +33,14 @@ namespace PlexRequests.Store.Models [Table("RequestQueue")] public class RequestQueue : Entity { - public int PrimaryIdentifier { get; set; } + public string PrimaryIdentifier { get; set; } public RequestType Type { get; set; } public byte[] Content { get; set; } public FaultType FaultType { get; set; } + public DateTime? LastRetry { get; set; } } public enum FaultType diff --git a/PlexRequests.Store/SqlTables.sql b/PlexRequests.Store/SqlTables.sql index 220664ba1..f0e852717 100644 --- a/PlexRequests.Store/SqlTables.sql +++ b/PlexRequests.Store/SqlTables.sql @@ -136,10 +136,11 @@ CREATE INDEX IF NOT EXISTS PlexEpisodes_ProviderId ON PlexEpisodes (ProviderId); CREATE TABLE IF NOT EXISTS RequestQueue ( Id INTEGER PRIMARY KEY AUTOINCREMENT, - PrimaryIdentifier INTEGER NOT NULL, + PrimaryIdentifier VARCHAR(100) NOT NULL, Type INTEGER NOT NULL, FaultType INTEGER NOT NULL, - Content BLOB NOT NULL + Content BLOB NOT NULL, + LastRetry VARCHAR(100) ); CREATE UNIQUE INDEX IF NOT EXISTS PlexUsers_Id ON PlexUsers (Id); diff --git a/PlexRequests.UI.Tests/TvSenderTests.cs b/PlexRequests.UI.Tests/TvSenderTests.cs index 678a196d9..a63ef719b 100644 --- a/PlexRequests.UI.Tests/TvSenderTests.cs +++ b/PlexRequests.UI.Tests/TvSenderTests.cs @@ -35,6 +35,7 @@ using NUnit.Framework; using PlexRequests.Api.Interfaces; using PlexRequests.Api.Models.Sonarr; +using PlexRequests.Core; using PlexRequests.Core.SettingModels; using PlexRequests.Store; using PlexRequests.UI.Helpers; @@ -146,32 +147,6 @@ namespace PlexRequests.UI.Tests true, It.IsAny()), Times.Once); } - [Test] - public async Task RequestEpisodesWithExistingSeriesTest() - { - var episodesReturned = new List - { - new SonarrEpisodes {episodeNumber = 1, seasonNumber = 2, monitored = false, id=22} - }; - SonarrMock.Setup(x => x.GetEpisodes(It.IsAny(), It.IsAny(), - It.IsAny())).Returns(episodesReturned); - SonarrMock.Setup(x => x.GetEpisode("22", It.IsAny(), It.IsAny())).Returns(new SonarrEpisode {id=22}); - - - Sender = new TvSender(SonarrMock.Object, SickrageMock.Object); - - var model = new RequestedModel - { - Episodes = new List { new EpisodesModel { EpisodeNumber = 1, SeasonNumber = 2 } } - }; - - var series = new Series(); - await Sender.RequestEpisodesWithExistingSeries(model, series, GetSonarrSettings()); - - SonarrMock.Verify(x => x.UpdateEpisode(It.Is(e => e.monitored), It.IsAny(), It.IsAny())); - SonarrMock.Verify(x => x.GetEpisode("22", It.IsAny(), It.IsAny()),Times.Once); - SonarrMock.Verify(x => x.SearchForEpisodes(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); - } private SonarrSettings GetSonarrSettings() { diff --git a/PlexRequests.UI/Jobs/Scheduler.cs b/PlexRequests.UI/Jobs/Scheduler.cs index 669500168..5ea4ba540 100644 --- a/PlexRequests.UI/Jobs/Scheduler.cs +++ b/PlexRequests.UI/Jobs/Scheduler.cs @@ -69,11 +69,10 @@ namespace PlexRequests.UI.Jobs JobBuilder.Create().WithIdentity("StoreBackup", "Database").Build(), JobBuilder.Create().WithIdentity("StoreCleanup", "Database").Build(), JobBuilder.Create().WithIdentity("UserRequestLimiter", "Request").Build(), - JobBuilder.Create().WithIdentity("RecentlyAddedModel", "Email").Build() + JobBuilder.Create().WithIdentity("RecentlyAddedModel", "Email").Build(), + JobBuilder.Create().WithIdentity("FaultQueueHandler", "Fault").Build(), }; - - - + jobs.AddRange(jobList); return jobs; @@ -151,7 +150,10 @@ namespace PlexRequests.UI.Jobs { s.UserRequestLimitResetter = 12; } - + if (s.FaultQueueHandler == 0) + { + s.FaultQueueHandler = 6; + } var triggers = new List(); @@ -221,6 +223,13 @@ namespace PlexRequests.UI.Jobs .WithSimpleSchedule(x => x.WithIntervalInHours(2).RepeatForever()) .Build(); + var fault = + TriggerBuilder.Create() + .WithIdentity("FaultQueueHandler", "Fault") + .StartAt(DateBuilder.FutureDate(10, IntervalUnit.Minute)) + .WithSimpleSchedule(x => x.WithIntervalInHours(s.FaultQueueHandler).RepeatForever()) + .Build(); + triggers.Add(rencentlyAdded); triggers.Add(plexAvailabilityChecker); triggers.Add(srCacher); @@ -230,6 +239,7 @@ namespace PlexRequests.UI.Jobs triggers.Add(storeCleanup); triggers.Add(userRequestLimiter); triggers.Add(plexEpCacher); + triggers.Add(fault); return triggers; } diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index ef35e08b6..fcd82d964 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -589,7 +589,7 @@ namespace PlexRequests.UI.Modules catch (Exception e) { Log.Fatal(e); - await FaultQueue.QueueItemAsync(model, RequestType.Movie, FaultType.RequestFault); + await FaultQueue.QueueItemAsync(model, movieInfo.Id.ToString(), RequestType.Movie, FaultType.RequestFault); await NotificationService.Publish(new NotificationModel { @@ -690,26 +690,6 @@ namespace PlexRequests.UI.Modules TvDbId = showId.ToString() }; - if (showInfo.externals?.thetvdb == null) - { - await FaultQueue.QueueItemAsync(model, RequestType.TvShow, FaultType.MissingInformation); - await NotificationService.Publish(new NotificationModel - { - DateTime = DateTime.Now, - User = Username, - RequestType = RequestType.TvShow, - Title = model.Title, - NotificationType = NotificationType.ItemAddedToFaultQueue - }); - return Response.AsJson(new JsonResponseModel - { - Result = true, - Message = $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}" - }); - } - - model.ProviderId = showInfo.externals?.thetvdb ?? 0; - var seasonsList = new List(); switch (seasons) { @@ -876,6 +856,26 @@ namespace PlexRequests.UI.Modules }); } + if (showInfo.externals?.thetvdb == null) + { + await FaultQueue.QueueItemAsync(model, showInfo.id.ToString(), RequestType.TvShow, FaultType.MissingInformation); + await NotificationService.Publish(new NotificationModel + { + DateTime = DateTime.Now, + User = Username, + RequestType = RequestType.TvShow, + Title = model.Title, + NotificationType = NotificationType.ItemAddedToFaultQueue + }); + return Response.AsJson(new JsonResponseModel + { + Result = true, + Message = $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}" + }); + } + + model.ProviderId = showInfo.externals?.thetvdb ?? 0; + try { if (ShouldAutoApprove(RequestType.TvShow, settings)) @@ -936,7 +936,7 @@ namespace PlexRequests.UI.Modules } catch (Exception e) { - await FaultQueue.QueueItemAsync(model, RequestType.TvShow, FaultType.RequestFault); + await FaultQueue.QueueItemAsync(model, showInfo.id.ToString(), RequestType.TvShow, FaultType.RequestFault); await NotificationService.Publish(new NotificationModel { DateTime = DateTime.Now, @@ -1102,7 +1102,7 @@ namespace PlexRequests.UI.Modules catch (Exception e) { Log.Error(e); - await FaultQueue.QueueItemAsync(model, RequestType.Movie, FaultType.RequestFault); + await FaultQueue.QueueItemAsync(model, albumInfo.id, RequestType.Movie, FaultType.RequestFault); await NotificationService.Publish(new NotificationModel { diff --git a/PlexRequests.UI/PlexRequests.UI.csproj b/PlexRequests.UI/PlexRequests.UI.csproj index a18aec630..949f29117 100644 --- a/PlexRequests.UI/PlexRequests.UI.csproj +++ b/PlexRequests.UI/PlexRequests.UI.csproj @@ -216,8 +216,6 @@ - - From dd92bb179c23d21f8a1b6dc7bb2063d0d24d76a3 Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Thu, 10 Nov 2016 12:55:01 +0000 Subject: [PATCH 14/87] Finished the queue #483 --- .../HeadphonesSender.cs | 6 +- PlexRequests.Core/PlexRequests.Core.csproj | 1 + .../Jobs/FaultQueueHandler.cs | 119 ++++++++++++++---- PlexRequests.Services/Jobs/JobNames.cs | 1 + PlexRequests.UI/Modules/SearchModule.cs | 9 +- PlexRequests.UI/PlexRequests.UI.csproj | 1 - 6 files changed, 105 insertions(+), 32 deletions(-) rename {PlexRequests.UI/Helpers => PlexRequests.Core}/HeadphonesSender.cs (99%) diff --git a/PlexRequests.UI/Helpers/HeadphonesSender.cs b/PlexRequests.Core/HeadphonesSender.cs similarity index 99% rename from PlexRequests.UI/Helpers/HeadphonesSender.cs rename to PlexRequests.Core/HeadphonesSender.cs index 29c428612..19e438be2 100644 --- a/PlexRequests.UI/Helpers/HeadphonesSender.cs +++ b/PlexRequests.Core/HeadphonesSender.cs @@ -24,18 +24,16 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // ************************************************************************/ #endregion + using System.Linq; using System.Threading; using System.Threading.Tasks; - using NLog; - using PlexRequests.Api.Interfaces; -using PlexRequests.Core; using PlexRequests.Core.SettingModels; using PlexRequests.Store; -namespace PlexRequests.UI.Helpers +namespace PlexRequests.Core { public class HeadphonesSender { diff --git a/PlexRequests.Core/PlexRequests.Core.csproj b/PlexRequests.Core/PlexRequests.Core.csproj index 17bb7ceca..506ac3a62 100644 --- a/PlexRequests.Core/PlexRequests.Core.csproj +++ b/PlexRequests.Core/PlexRequests.Core.csproj @@ -90,6 +90,7 @@ + diff --git a/PlexRequests.Services/Jobs/FaultQueueHandler.cs b/PlexRequests.Services/Jobs/FaultQueueHandler.cs index 0a540d5ad..9238a12ec 100644 --- a/PlexRequests.Services/Jobs/FaultQueueHandler.cs +++ b/PlexRequests.Services/Jobs/FaultQueueHandler.cs @@ -51,32 +51,43 @@ namespace PlexRequests.Services.Jobs private static readonly Logger Log = LogManager.GetCurrentClassLogger(); public FaultQueueHandler(IJobRecord record, IRepository repo, ISonarrApi sonarrApi, - ISickRageApi srApi, - ISettingsService sonarrSettings, ISettingsService srSettings) + ISickRageApi srApi, ISettingsService sonarrSettings, ISettingsService srSettings, + ICouchPotatoApi cpApi, ISettingsService cpsettings, IRequestService requestService, + ISettingsService hpSettings, IHeadphonesApi headphonesApi) { Record = record; Repo = repo; SonarrApi = sonarrApi; SrApi = srApi; + CpApi = cpApi; + HpApi = headphonesApi; + + RequestService = requestService; + SickrageSettings = srSettings; SonarrSettings = sonarrSettings; + CpSettings = cpsettings; + HeadphoneSettings = hpSettings; } private IRepository Repo { get; } private IJobRecord Record { get; } private ISonarrApi SonarrApi { get; } private ISickRageApi SrApi { get; } - private ISettingsService SonarrSettings { get; set; } - private ISettingsService SickrageSettings { get; set; } - + private ICouchPotatoApi CpApi { get; } + private IHeadphonesApi HpApi { get; } + private IRequestService RequestService { get; } + private ISettingsService SonarrSettings { get; } + private ISettingsService SickrageSettings { get; } + private ISettingsService CpSettings { get; } + private ISettingsService HeadphoneSettings { get; } public void Execute(IJobExecutionContext context) { try { var faultedRequests = Repo.GetAll().ToList(); - - + var missingInfo = faultedRequests.Where(x => x.FaultType == FaultType.MissingInformation).ToList(); ProcessMissingInformation(missingInfo); @@ -90,7 +101,7 @@ namespace PlexRequests.Services.Jobs } finally { - Record.Record(JobNames.RequestLimitReset); + Record.Record(JobNames.FaultQueueHandler); } } @@ -163,9 +174,66 @@ namespace PlexRequests.Services.Jobs // Couldn't send it return false; } + + // Approve it + tvModel.Approved = true; + RequestService.UpdateRequest(tvModel); return true; } + return false; + } + catch (Exception e) + { + Log.Error(e); + return false; // It fails so it will get added back into the queue + } + } + private bool ProcessMovies(RequestedModel model, CouchPotatoSettings cp) + { + try + { + if (cp.Enabled) + { + var result = CpApi.AddMovie(model.ImdbId, cp.ApiKey, model.Title, + cp.FullUri, cp.ProfileId); + + if (result) + { + // Approve it now + model.Approved = true; + RequestService.UpdateRequest(model); + }; + + return result; + } + return false; + } + catch (Exception e) + { + Log.Error(e); + return false; // It fails so it will get added back into the queue + } + } + + private bool ProcessAlbums(RequestedModel model, HeadphonesSettings hp) + { + try + { + if (hp.Enabled) + { + var sender = new HeadphonesSender(HpApi, hp, RequestService); + var result = sender.AddAlbum(model).Result; + + if (result) + { + // Approve it now + model.Approved = true; + RequestService.UpdateRequest(model); + }; + + return result; + } return false; } catch (Exception e) @@ -179,36 +247,45 @@ namespace PlexRequests.Services.Jobs { var sonarrSettings = SonarrSettings.GetSettings(); var sickrageSettings = SickrageSettings.GetSettings(); + var cpSettings = CpSettings.GetSettings(); + var hpSettings = HeadphoneSettings.GetSettings(); if (!requests.Any()) { return; } - var tv = requests.Where(x => x.Type == RequestType.TvShow); - var movie = requests.Where(x => x.Type == RequestType.Movie); - var album = requests.Where(x => x.Type == RequestType.Album); - - - foreach (var t in tv) + foreach (var request in requests) { - var tvModel = ByteConverterHelper.ReturnObject(t.Content); - var result = ProcessTvShow(tvModel, sonarrSettings, sickrageSettings); + var model = ByteConverterHelper.ReturnObject(request.Content); + var result = false; + switch (request.Type) + { + case RequestType.Movie: + result = ProcessMovies(model, cpSettings); + break; + case RequestType.TvShow: + result = ProcessTvShow(model, sonarrSettings, sickrageSettings); + break; + case RequestType.Album: + result = ProcessAlbums(model, hpSettings); + break; + default: + throw new ArgumentOutOfRangeException(); + } if (!result) { // we now have the info but couldn't add it, so do nothing now. - t.LastRetry = DateTime.UtcNow; - Repo.Update(t); + request.LastRetry = DateTime.UtcNow; + Repo.Update(request); } else { // Successful, remove from the fault queue - Repo.Delete(t); + Repo.Delete(request); } } - - } } } \ No newline at end of file diff --git a/PlexRequests.Services/Jobs/JobNames.cs b/PlexRequests.Services/Jobs/JobNames.cs index 77f177713..0f8b4c96e 100644 --- a/PlexRequests.Services/Jobs/JobNames.cs +++ b/PlexRequests.Services/Jobs/JobNames.cs @@ -37,5 +37,6 @@ namespace PlexRequests.Services.Jobs public const string RequestLimitReset = "Request Limit Reset"; public const string EpisodeCacher = "Plex Episode Cacher"; public const string RecentlyAddedEmail = "Recently Added Email Notification"; + public const string FaultQueueHandler = "Request Fault Queue Handler"; } } \ No newline at end of file diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index fcd82d964..1e0e4c490 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -909,9 +909,7 @@ namespace PlexRequests.UI.Modules var result = sender.SendToSickRage(srSettings, model); if (result?.result == "success") { - return - await - AddRequest(model, settings, + return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); } return @@ -924,8 +922,7 @@ namespace PlexRequests.UI.Modules if (!srSettings.Enabled && !s.Enabled) { - return - await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); + return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); } return @@ -1102,7 +1099,7 @@ namespace PlexRequests.UI.Modules catch (Exception e) { Log.Error(e); - await FaultQueue.QueueItemAsync(model, albumInfo.id, RequestType.Movie, FaultType.RequestFault); + await FaultQueue.QueueItemAsync(model, albumInfo.id, RequestType.Album, FaultType.RequestFault); await NotificationService.Publish(new NotificationModel { diff --git a/PlexRequests.UI/PlexRequests.UI.csproj b/PlexRequests.UI/PlexRequests.UI.csproj index 949f29117..75ecd4701 100644 --- a/PlexRequests.UI/PlexRequests.UI.csproj +++ b/PlexRequests.UI/PlexRequests.UI.csproj @@ -210,7 +210,6 @@ - From 8a61371048142058329c35787917acbff55ff615 Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Thu, 10 Nov 2016 13:37:51 +0000 Subject: [PATCH 15/87] Finished #483 --- .../Queue/TransientFaultQueue.cs | 2 +- PlexRequests.Store/Models/RequestQueue.cs | 2 +- PlexRequests.Store/SqlTables.sql | 2 +- .../Models/FaultedRequestsViewModel.cs | 56 ++++++++++ .../Modules/Admin/FaultQueueModule.cs | 74 +++++++++++++ PlexRequests.UI/Modules/DonationLinkModule.cs | 3 - PlexRequests.UI/Modules/SearchModule.cs | 2 +- PlexRequests.UI/PlexRequests.UI.csproj | 5 + .../Views/Admin/RequestFaultQueue.cshtml | 103 ++++++++++++++++++ PlexRequests.UI/Views/Admin/_Sidebar.cshtml | 1 + 10 files changed, 243 insertions(+), 7 deletions(-) create mode 100644 PlexRequests.UI/Models/FaultedRequestsViewModel.cs create mode 100644 PlexRequests.UI/Modules/Admin/FaultQueueModule.cs create mode 100644 PlexRequests.UI/Views/Admin/RequestFaultQueue.cshtml diff --git a/PlexRequests.Core/Queue/TransientFaultQueue.cs b/PlexRequests.Core/Queue/TransientFaultQueue.cs index 57fd2eb34..5c2f1de71 100644 --- a/PlexRequests.Core/Queue/TransientFaultQueue.cs +++ b/PlexRequests.Core/Queue/TransientFaultQueue.cs @@ -80,7 +80,7 @@ namespace PlexRequests.Core.Queue var existingItem = await RequestQueue.CustomAsync(async connection => { connection.Open(); - var result = await connection.QueryAsync("select * from RequestQueue where PrimaryIdentifier = @ProviderId", new { ProviderId = id }); + var result = await connection.QueryAsync("select * from RequestFaultQueue where PrimaryIdentifier = @ProviderId", new { ProviderId = id }); return result; }); diff --git a/PlexRequests.Store/Models/RequestQueue.cs b/PlexRequests.Store/Models/RequestQueue.cs index f98517ab3..cbbfb62b6 100644 --- a/PlexRequests.Store/Models/RequestQueue.cs +++ b/PlexRequests.Store/Models/RequestQueue.cs @@ -30,7 +30,7 @@ using Dapper.Contrib.Extensions; namespace PlexRequests.Store.Models { - [Table("RequestQueue")] + [Table("RequestFaultQueue")] public class RequestQueue : Entity { public string PrimaryIdentifier { get; set; } diff --git a/PlexRequests.Store/SqlTables.sql b/PlexRequests.Store/SqlTables.sql index f0e852717..84a31306f 100644 --- a/PlexRequests.Store/SqlTables.sql +++ b/PlexRequests.Store/SqlTables.sql @@ -133,7 +133,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS PlexEpisodes_Id ON PlexEpisodes (Id); CREATE INDEX IF NOT EXISTS PlexEpisodes_ProviderId ON PlexEpisodes (ProviderId); -CREATE TABLE IF NOT EXISTS RequestQueue +CREATE TABLE IF NOT EXISTS RequestFaultQueue ( Id INTEGER PRIMARY KEY AUTOINCREMENT, PrimaryIdentifier VARCHAR(100) NOT NULL, diff --git a/PlexRequests.UI/Models/FaultedRequestsViewModel.cs b/PlexRequests.UI/Models/FaultedRequestsViewModel.cs new file mode 100644 index 000000000..d0f5de428 --- /dev/null +++ b/PlexRequests.UI/Models/FaultedRequestsViewModel.cs @@ -0,0 +1,56 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: FaultedRequestsViewModel.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; +using PlexRequests.Store; +using PlexRequests.Store.Models; + +namespace PlexRequests.UI.Models +{ + public class FaultedRequestsViewModel + { + public int Id { get; set; } + public string PrimaryIdentifier { get; set; } + public RequestTypeViewModel Type { get; set; } + public string Title { get; set; } + public FaultTypeViewModel FaultType { get; set; } + public DateTime? LastRetry { get; set; } + } + + public enum RequestTypeViewModel + { + Movie, + TvShow, + Album + } + + public enum FaultTypeViewModel + { + RequestFault, + MissingInformation + } +} \ No newline at end of file diff --git a/PlexRequests.UI/Modules/Admin/FaultQueueModule.cs b/PlexRequests.UI/Modules/Admin/FaultQueueModule.cs new file mode 100644 index 000000000..355e823e2 --- /dev/null +++ b/PlexRequests.UI/Modules/Admin/FaultQueueModule.cs @@ -0,0 +1,74 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: SystemStatusModule.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System.Collections.Generic; +using System.Linq; +using Nancy.Responses.Negotiation; +using PlexRequests.Core; +using PlexRequests.Core.SettingModels; +using PlexRequests.Helpers; +using PlexRequests.Helpers.Permissions; +using PlexRequests.Store; +using PlexRequests.Store.Models; +using PlexRequests.Store.Repository; +using PlexRequests.UI.Models; + +namespace PlexRequests.UI.Modules.Admin +{ + public class FaultQueueModule : BaseModule + { + public FaultQueueModule(ISettingsService settingsService, ICacheProvider cache, IRepository requestQueue) : base("admin", settingsService) + { + Cache = cache; + RequestQueue = requestQueue; + + Security.HasPermissionsResponse(Permissions.Administrator); + + Get["Index", "/faultqueue"] = x => Index(); + } + + private ICacheProvider Cache { get; } + private IRepository RequestQueue { get; } + + private Negotiator Index() + { + var requests = RequestQueue.GetAll(); + + var model = requests.Select(r => new FaultedRequestsViewModel + { + FaultType = (FaultTypeViewModel)(int)r.FaultType, + Type = (RequestTypeViewModel)(int)r.Type, + Title = ByteConverterHelper.ReturnObject(r.Content).Title, + Id = r.Id, + PrimaryIdentifier = r.PrimaryIdentifier, + LastRetry = r.LastRetry + }).ToList(); + + return View["RequestFaultQueue", model]; + } + } +} \ No newline at end of file diff --git a/PlexRequests.UI/Modules/DonationLinkModule.cs b/PlexRequests.UI/Modules/DonationLinkModule.cs index 9ede62e41..5b3aec76a 100644 --- a/PlexRequests.UI/Modules/DonationLinkModule.cs +++ b/PlexRequests.UI/Modules/DonationLinkModule.cs @@ -2,14 +2,11 @@ using System.Threading.Tasks; using Nancy; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using NLog; using PlexRequests.Core; using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; -using PlexRequests.UI.Models; namespace PlexRequests.UI.Modules { diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index 1e0e4c490..0e72cf9c9 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -616,7 +616,7 @@ namespace PlexRequests.UI.Modules /// private async Task RequestTvShow(int showId, string seasons) { - if (Security.DoesNotHavePermissions(Permissions.ReadOnlyUser, User)) + if (Security.HasPermissions(User, Permissions.ReadOnlyUser)) { return Response.AsJson(new JsonResponseModel() diff --git a/PlexRequests.UI/PlexRequests.UI.csproj b/PlexRequests.UI/PlexRequests.UI.csproj index 75ecd4701..82f401655 100644 --- a/PlexRequests.UI/PlexRequests.UI.csproj +++ b/PlexRequests.UI/PlexRequests.UI.csproj @@ -229,6 +229,7 @@ + @@ -241,6 +242,7 @@ + @@ -724,6 +726,9 @@ PreserveNewest + + Always + web.config diff --git a/PlexRequests.UI/Views/Admin/RequestFaultQueue.cshtml b/PlexRequests.UI/Views/Admin/RequestFaultQueue.cshtml new file mode 100644 index 000000000..0e1329c4d --- /dev/null +++ b/PlexRequests.UI/Views/Admin/RequestFaultQueue.cshtml @@ -0,0 +1,103 @@ +@using PlexRequests.UI.Helpers +@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase> +@Html.Partial("_Sidebar") +
+
+ Release Fault Queue + + + + + + + + + + + + + @foreach (var m in Model) + { + + + + + + + } + +
+ Request Title + + Type + + Fault Type + + LastRetry +
+ @m.Title + + @m.Type + + @m.FaultType + + @m.LastRetry +
+ +
+
+ +@**@ \ No newline at end of file diff --git a/PlexRequests.UI/Views/Admin/_Sidebar.cshtml b/PlexRequests.UI/Views/Admin/_Sidebar.cshtml index d65cd9848..836fc856f 100644 --- a/PlexRequests.UI/Views/Admin/_Sidebar.cshtml +++ b/PlexRequests.UI/Views/Admin/_Sidebar.cshtml @@ -18,5 +18,6 @@ @Html.GetSidebarUrl(Context, "/admin/logs", "Logs") @Html.GetSidebarUrl(Context, "/admin/status", "Status") @Html.GetSidebarUrl(Context, "/admin/scheduledjobs", "Scheduled Jobs") + @Html.GetSidebarUrl(Context, "/admin/faultqueue", "Request Fault Queue")
\ No newline at end of file From cb50fc61fbdaa7d1ba9b7ac67859c73e0417e150 Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Thu, 10 Nov 2016 14:37:28 +0000 Subject: [PATCH 16/87] Final Tweaks #483 --- PlexRequests.Core/PlexRequests.Core.csproj | 1 + PlexRequests.Core/RequestHelper.cs | 82 +++++++++++++++++++ .../Jobs/FaultQueueHandler.cs | 24 ++++-- PlexRequests.UI/Jobs/Scheduler.cs | 3 +- PlexRequests.UI/Modules/SearchModule.cs | 25 +----- 5 files changed, 103 insertions(+), 32 deletions(-) create mode 100644 PlexRequests.Core/RequestHelper.cs diff --git a/PlexRequests.Core/PlexRequests.Core.csproj b/PlexRequests.Core/PlexRequests.Core.csproj index 506ac3a62..5f093a928 100644 --- a/PlexRequests.Core/PlexRequests.Core.csproj +++ b/PlexRequests.Core/PlexRequests.Core.csproj @@ -112,6 +112,7 @@ + diff --git a/PlexRequests.Core/RequestHelper.cs b/PlexRequests.Core/RequestHelper.cs new file mode 100644 index 000000000..0e99df6ca --- /dev/null +++ b/PlexRequests.Core/RequestHelper.cs @@ -0,0 +1,82 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: RequestHelper.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using PlexRequests.Core.SettingModels; +using PlexRequests.Store; + +namespace PlexRequests.Core +{ + public static class RequestHelper + { + public static bool ShouldAutoApprove(this RequestType requestType, PlexRequestSettings prSettings, bool isAdmin, string username) + { + // if the user is an admin or they are whitelisted, they go ahead and allow auto-approval + if (isAdmin || prSettings.ApprovalWhiteList.Any(x => x.Equals(username, StringComparison.OrdinalIgnoreCase))) return true; + + // check by request type if the category requires approval or not + switch (requestType) + { + case RequestType.Movie: + return !prSettings.RequireMovieApproval; + case RequestType.TvShow: + return !prSettings.RequireTvShowApproval; + case RequestType.Album: + return !prSettings.RequireMusicApproval; + default: + return false; + } + } + + public static bool ShouldAutoApprove(this RequestType requestType, PlexRequestSettings prSettings, bool isAdmin, List username) + { + // if the user is an admin or they are whitelisted, they go ahead and allow auto-approval + if (isAdmin) return true; + + + if(prSettings.ApprovalWhiteList.Intersect(username).Any()) + { + return true; + } + + // check by request type if the category requires approval or not + switch (requestType) + { + case RequestType.Movie: + return !prSettings.RequireMovieApproval; + case RequestType.TvShow: + return !prSettings.RequireTvShowApproval; + case RequestType.Album: + return !prSettings.RequireMusicApproval; + default: + return false; + } + } + } +} \ No newline at end of file diff --git a/PlexRequests.Services/Jobs/FaultQueueHandler.cs b/PlexRequests.Services/Jobs/FaultQueueHandler.cs index 9238a12ec..f30a4f2d9 100644 --- a/PlexRequests.Services/Jobs/FaultQueueHandler.cs +++ b/PlexRequests.Services/Jobs/FaultQueueHandler.cs @@ -37,6 +37,7 @@ using PlexRequests.Api.Interfaces; using PlexRequests.Core; using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; +using PlexRequests.Helpers.Permissions; using PlexRequests.Services.Interfaces; using PlexRequests.Store; using PlexRequests.Store.Models; @@ -53,7 +54,7 @@ namespace PlexRequests.Services.Jobs public FaultQueueHandler(IJobRecord record, IRepository repo, ISonarrApi sonarrApi, ISickRageApi srApi, ISettingsService sonarrSettings, ISettingsService srSettings, ICouchPotatoApi cpApi, ISettingsService cpsettings, IRequestService requestService, - ISettingsService hpSettings, IHeadphonesApi headphonesApi) + ISettingsService hpSettings, IHeadphonesApi headphonesApi, ISettingsService prSettings) { Record = record; Repo = repo; @@ -68,6 +69,7 @@ namespace PlexRequests.Services.Jobs SonarrSettings = sonarrSettings; CpSettings = cpsettings; HeadphoneSettings = hpSettings; + PrSettings = prSettings.GetSettings(); } private IRepository Repo { get; } @@ -77,6 +79,7 @@ namespace PlexRequests.Services.Jobs private ICouchPotatoApi CpApi { get; } private IHeadphonesApi HpApi { get; } private IRequestService RequestService { get; } + private PlexRequestSettings PrSettings { get; } private ISettingsService SonarrSettings { get; } private ISettingsService SickrageSettings { get; } private ISettingsService CpSettings { get; } @@ -87,7 +90,7 @@ namespace PlexRequests.Services.Jobs try { var faultedRequests = Repo.GetAll().ToList(); - + var missingInfo = faultedRequests.Where(x => x.FaultType == FaultType.MissingInformation).ToList(); ProcessMissingInformation(missingInfo); @@ -108,13 +111,14 @@ namespace PlexRequests.Services.Jobs private void ProcessMissingInformation(List requests) { - var sonarrSettings = SonarrSettings.GetSettings(); - var sickrageSettings = SickrageSettings.GetSettings(); - if (!requests.Any()) { return; } + + var sonarrSettings = SonarrSettings.GetSettings(); + var sickrageSettings = SickrageSettings.GetSettings(); + var tv = requests.Where(x => x.Type == RequestType.TvShow); // TV @@ -122,7 +126,7 @@ namespace PlexRequests.Services.Jobs foreach (var t in tv) { var providerId = int.Parse(t.PrimaryIdentifier); - var showInfo = tvApi.ShowLookupByTheTvDbId(providerId); + var showInfo = tvApi.ShowLookup(providerId); if (showInfo.externals?.thetvdb != null) { @@ -227,8 +231,10 @@ namespace PlexRequests.Services.Jobs if (result) { - // Approve it now - model.Approved = true; + + if (model.Type.ShouldAutoApprove(PrSettings, false, model.RequestedUsers)) + // Approve it now + model.Approved = true; RequestService.UpdateRequest(model); }; @@ -258,7 +264,7 @@ namespace PlexRequests.Services.Jobs foreach (var request in requests) { var model = ByteConverterHelper.ReturnObject(request.Content); - var result = false; + bool result; switch (request.Type) { case RequestType.Movie: diff --git a/PlexRequests.UI/Jobs/Scheduler.cs b/PlexRequests.UI/Jobs/Scheduler.cs index 5ea4ba540..8127abf9a 100644 --- a/PlexRequests.UI/Jobs/Scheduler.cs +++ b/PlexRequests.UI/Jobs/Scheduler.cs @@ -226,7 +226,8 @@ namespace PlexRequests.UI.Jobs var fault = TriggerBuilder.Create() .WithIdentity("FaultQueueHandler", "Fault") - .StartAt(DateBuilder.FutureDate(10, IntervalUnit.Minute)) + //.StartAt(DateBuilder.FutureDate(10, IntervalUnit.Minute)) + .StartNow() .WithSimpleSchedule(x => x.WithIntervalInHours(s.FaultQueueHandler).RepeatForever()) .Build(); diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index 0e72cf9c9..eb7c13960 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -878,7 +878,7 @@ namespace PlexRequests.UI.Modules try { - if (ShouldAutoApprove(RequestType.TvShow, settings)) + if (RequestType.TvShow.ShouldAutoApprove(settings, IsAdmin, Username)) { model.Approved = true; var s = await sonarrSettings; @@ -974,7 +974,7 @@ namespace PlexRequests.UI.Modules private bool ShouldSendNotification(RequestType type, PlexRequestSettings prSettings) { - var sendNotification = ShouldAutoApprove(type, prSettings) + var sendNotification = type.ShouldAutoApprove(prSettings, IsAdmin, Username) ? !prSettings.IgnoreNotifyForAutoApprovedRequests : true; var claims = Context.CurrentUser?.Claims; @@ -1073,7 +1073,7 @@ namespace PlexRequests.UI.Modules try { - if (ShouldAutoApprove(RequestType.Album, settings)) + if (RequestType.Album.ShouldAutoApprove(settings, IsAdmin, Username)) { model.Approved = true; var hpSettings = HeadphonesService.GetSettings(); @@ -1127,25 +1127,6 @@ namespace PlexRequests.UI.Modules return img; } - private bool ShouldAutoApprove(RequestType requestType, PlexRequestSettings prSettings) - { - // if the user is an admin or they are whitelisted, they go ahead and allow auto-approval - if (IsAdmin || prSettings.ApprovalWhiteList.Any(x => x.Equals(Username, StringComparison.OrdinalIgnoreCase))) return true; - - // check by request type if the category requires approval or not - switch (requestType) - { - case RequestType.Movie: - return !prSettings.RequireMovieApproval; - case RequestType.TvShow: - return !prSettings.RequireTvShowApproval; - case RequestType.Album: - return !prSettings.RequireMusicApproval; - default: - return false; - } - } - private async Task NotifyUser(bool notify) { Analytics.TrackEventAsync(Category.Search, Action.Save, "NotifyUser", Username, CookieHelper.GetAnalyticClientId(Cookies), notify ? 1 : 0); From b307b19feff4f6e2fcdbe2acafdbc8faf9dd1fd5 Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Thu, 10 Nov 2016 14:45:16 +0000 Subject: [PATCH 17/87] convert the for to foreach for better readability. Still need to rework this area --- .../Jobs/PlexAvailabilityChecker.cs | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs index a0d3ed85a..7ba84cb79 100644 --- a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs +++ b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs @@ -419,11 +419,11 @@ namespace PlexRequests.Services.Jobs results = GetLibraries(plexSettings); if (plexSettings.AdvancedSearch) { - for (var i = 0; i < results.Count; i++) + foreach (PlexSearch t in results) { - for (var j = 0; j < results[i].Directory.Count; j++) + foreach (Directory1 t1 in t.Directory) { - var currentItem = results[i].Directory[j]; + var currentItem = t1; var metaData = PlexApi.GetMetadata(plexSettings.PlexAuthToken, plexSettings.FullUri, currentItem.RatingKey); @@ -434,25 +434,21 @@ namespace PlexRequests.Services.Jobs currentItem.RatingKey); // We do not want "all episodes" this as a season - var filtered = - seasons.Directory.Where( - x => - !x.Title.Equals("All episodes", - StringComparison.CurrentCultureIgnoreCase)); + var filtered = seasons.Directory.Where( x => !x.Title.Equals("All episodes", StringComparison.CurrentCultureIgnoreCase)); - results[i].Directory[j].Seasons.AddRange(filtered); + t1.Seasons.AddRange(filtered); } var providerId = PlexHelper.GetProviderIdFromPlexGuid(metaData.Directory.Guid); - results[i].Directory[j].ProviderId = providerId; + t1.ProviderId = providerId; } - for (var j = 0; j < results[i].Video.Count; j++) + foreach (Video t1 in t.Video) { - var currentItem = results[i].Video[j]; + var currentItem = t1; var metaData = PlexApi.GetMetadata(plexSettings.PlexAuthToken, plexSettings.FullUri, currentItem.RatingKey); var providerId = PlexHelper.GetProviderIdFromPlexGuid(metaData.Video.Guid); - results[i].Video[j].ProviderId = providerId; + t1.ProviderId = providerId; } } } From 92016abe345626af34613d692f49233a57c4d019 Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Thu, 10 Nov 2016 14:54:58 +0000 Subject: [PATCH 18/87] Fixed build --- PlexRequests.UI/Modules/SearchModule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index eb7c13960..8d398d1f3 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -555,7 +555,7 @@ namespace PlexRequests.UI.Modules }; try { - if (ShouldAutoApprove(RequestType.Movie, settings)) + if (RequestType.Movie.ShouldAutoApprove(settings, IsAdmin, Username)) { var cpSettings = await CpService.GetSettingsAsync(); model.Approved = true; From 6d2bc0eb7219dccc6799af87ba93c22043846fea Mon Sep 17 00:00:00 2001 From: TidusJar Date: Fri, 11 Nov 2016 20:59:50 +0000 Subject: [PATCH 19/87] Migrate users --- .../Migrations/Version1100.cs | 20 +++++++++++++++++-- appveyor.yml | 6 +++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/PlexRequests.Core.Migration/Migrations/Version1100.cs b/PlexRequests.Core.Migration/Migrations/Version1100.cs index fe03daf88..804dfa438 100644 --- a/PlexRequests.Core.Migration/Migrations/Version1100.cs +++ b/PlexRequests.Core.Migration/Migrations/Version1100.cs @@ -27,23 +27,27 @@ using System.Data; using PlexRequests.Store; +using PlexRequests.Store.Repository; namespace PlexRequests.Core.Migration.Migrations { [Migration(11000, "v1.10.0.0")] public class Version1100 : BaseMigration, IMigration { - public Version1100() + public Version1100(IUserRepository userRepo) { } public int Version => 11000; - + public IUserRepository UserRepo {get;set;} public void Start(IDbConnection con) { UpdateDb(con); + // Update the current admin permissions set + UpdateAdmin(con); + UpdateSchema(con, Version); } @@ -54,5 +58,17 @@ namespace PlexRequests.Core.Migration.Migrations con.AlterTable("Users", "ADD", "Features", true, "INTEGER"); } + + private void UpdateAdmin(IDbConnection con) + { + var users = UserRepo.GetAll(); + + foreach (var user in users) + { + user.Permissions = Permissions.Administrator | ReportIssue | RequestMusic + | RequestTvShow + | RequestMovie; + } + } } } \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index 1af2ba46a..4f00bdb6f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,11 +1,11 @@ -version: 1.9.{build} +version: 1.10.{build} configuration: Release assembly_info: patch: true file: '**\AssemblyInfo.*' - assembly_version: '1.9.7' + assembly_version: '1.10.0' assembly_file_version: '{version}' - assembly_informational_version: '1.9.7' + assembly_informational_version: '1.10.0' before_build: - cmd: appveyor-retry nuget restore build: From 84ea45ebaa18e87fa7be11999824052a95060e19 Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Mon, 14 Nov 2016 10:06:35 +0000 Subject: [PATCH 20/87] Fixed #665 --- .../MigrationRunner.cs | 4 --- .../Migrations/Version1100.cs | 25 ++++++++++++------- .../PlexRequests.Core.Migration.csproj | 4 +++ .../Repository/UserRepository.cs | 3 +++ 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/PlexRequests.Core.Migration/MigrationRunner.cs b/PlexRequests.Core.Migration/MigrationRunner.cs index 5a1a67f10..5535782a6 100644 --- a/PlexRequests.Core.Migration/MigrationRunner.cs +++ b/PlexRequests.Core.Migration/MigrationRunner.cs @@ -39,10 +39,6 @@ namespace PlexRequests.Core.Migration foreach (var param in ctor.GetParameters()) { - Console.WriteLine(string.Format( - "Param {0} is named {1} and is of type {2}", - param.Position, param.Name, param.ParameterType)); - var dep = Kernel.Get(param.ParameterType); dependencies.Add(dep); } diff --git a/PlexRequests.Core.Migration/Migrations/Version1100.cs b/PlexRequests.Core.Migration/Migrations/Version1100.cs index 804dfa438..3238b9c27 100644 --- a/PlexRequests.Core.Migration/Migrations/Version1100.cs +++ b/PlexRequests.Core.Migration/Migrations/Version1100.cs @@ -26,6 +26,8 @@ #endregion using System.Data; +using System.Linq; +using PlexRequests.Helpers.Permissions; using PlexRequests.Store; using PlexRequests.Store.Repository; @@ -36,17 +38,17 @@ namespace PlexRequests.Core.Migration.Migrations { public Version1100(IUserRepository userRepo) { - + UserRepo = userRepo; } public int Version => 11000; - public IUserRepository UserRepo {get;set;} + private IUserRepository UserRepo { get; } public void Start(IDbConnection con) { UpdateDb(con); // Update the current admin permissions set - UpdateAdmin(con); + UpdateAdmin(); UpdateSchema(con, Version); } @@ -56,19 +58,24 @@ namespace PlexRequests.Core.Migration.Migrations // Create the two new columns con.AlterTable("Users", "ADD", "Permissions", true, "INTEGER"); con.AlterTable("Users", "ADD", "Features", true, "INTEGER"); - + } - private void UpdateAdmin(IDbConnection con) + private void UpdateAdmin() { - var users = UserRepo.GetAll(); + var users = UserRepo.GetAll().ToList(); foreach (var user in users) { - user.Permissions = Permissions.Administrator | ReportIssue | RequestMusic - | RequestTvShow - | RequestMovie; + user.Permissions = (int) + (Permissions.Administrator + | Permissions.ReportIssue + | Permissions.RequestMusic + | Permissions.RequestTvShow + | Permissions.RequestMovie); } + + UserRepo.UpdateAll(users); } } } \ No newline at end of file diff --git a/PlexRequests.Core.Migration/PlexRequests.Core.Migration.csproj b/PlexRequests.Core.Migration/PlexRequests.Core.Migration.csproj index 2881f95ed..e724145c4 100644 --- a/PlexRequests.Core.Migration/PlexRequests.Core.Migration.csproj +++ b/PlexRequests.Core.Migration/PlexRequests.Core.Migration.csproj @@ -74,6 +74,10 @@ {DD7DC444-D3BF-4027-8AB9-EFC71F5EC581} PlexRequests.Core + + {1252336D-42A3-482A-804C-836E60173DFA} + PlexRequests.Helpers + {92433867-2B7B-477B-A566-96C382427525} PlexRequests.Store diff --git a/PlexRequests.Store/Repository/UserRepository.cs b/PlexRequests.Store/Repository/UserRepository.cs index b05609c82..d661a30ec 100644 --- a/PlexRequests.Store/Repository/UserRepository.cs +++ b/PlexRequests.Store/Repository/UserRepository.cs @@ -105,6 +105,9 @@ namespace PlexRequests.Store.Repository IEnumerable Custom(Func> func); long Insert(UsersModel entity); void Delete(UsersModel entity); + IEnumerable GetAll(); + bool UpdateAll(IEnumerable entity); + bool Update(UsersModel entity); } } From 05bdfcd550b52a5158ada1f78b0f458d98da8b07 Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Mon, 14 Nov 2016 10:44:10 +0000 Subject: [PATCH 21/87] started on #646. Fixed #657 --- .../Migrations/Version1100.cs | 26 ++++++++++++++++++- PlexRequests.Core/CacheKeys.cs | 2 +- .../Interfaces/IJobRecord.cs | 1 + .../Jobs/CouchPotatoCacher.cs | 2 ++ .../Jobs/FaultQueueHandler.cs | 3 +++ PlexRequests.Services/Jobs/JobRecord.cs | 17 ++++++++++++ .../Jobs/PlexAvailabilityChecker.cs | 4 ++- .../Jobs/PlexEpisodeCacher.cs | 2 ++ PlexRequests.Services/Jobs/RecentlyAdded.cs | 3 +++ PlexRequests.Services/Jobs/SickRageCacher.cs | 2 ++ PlexRequests.Services/Jobs/SonarrCacher.cs | 2 ++ PlexRequests.Services/Jobs/StoreBackup.cs | 2 ++ PlexRequests.Services/Jobs/StoreCleanup.cs | 2 ++ .../Jobs/UserRequestLimitResetter.cs | 2 ++ PlexRequests.Store/Models/ScheduledJobs.cs | 1 + PlexRequests.Store/SqlTables.sql | 3 ++- 16 files changed, 70 insertions(+), 4 deletions(-) diff --git a/PlexRequests.Core.Migration/Migrations/Version1100.cs b/PlexRequests.Core.Migration/Migrations/Version1100.cs index 3238b9c27..a631b05d6 100644 --- a/PlexRequests.Core.Migration/Migrations/Version1100.cs +++ b/PlexRequests.Core.Migration/Migrations/Version1100.cs @@ -25,6 +25,7 @@ // ************************************************************************/ #endregion +using System; using System.Data; using System.Linq; using PlexRequests.Helpers.Permissions; @@ -36,12 +37,14 @@ namespace PlexRequests.Core.Migration.Migrations [Migration(11000, "v1.10.0.0")] public class Version1100 : BaseMigration, IMigration { - public Version1100(IUserRepository userRepo) + public Version1100(IUserRepository userRepo, IRequestService requestService) { UserRepo = userRepo; + RequestService = requestService; } public int Version => 11000; private IUserRepository UserRepo { get; } + private IRequestService RequestService { get; } public void Start(IDbConnection con) { @@ -50,6 +53,7 @@ namespace PlexRequests.Core.Migration.Migrations // Update the current admin permissions set UpdateAdmin(); + UpdateSchema(con, Version); } @@ -59,6 +63,26 @@ namespace PlexRequests.Core.Migration.Migrations con.AlterTable("Users", "ADD", "Permissions", true, "INTEGER"); con.AlterTable("Users", "ADD", "Features", true, "INTEGER"); + // Add the new 'running' item into the scheduled jobs so we can check if the cachers are running + con.AlterTable("ScheduledJobs", "ADD", "Running", true, "INTEGER"); + + + + //https://image.tmdb.org/t/p/w150/https://image.tmdb.org/t/p/w150//aqhAqttDq7zgsTaBHtCD8wmTk6k.jpg + + // UI = https://image.tmdb.org/t/p/w150/{{posterPath}} + // Update old invalid posters + var allRequests = RequestService.GetAll().ToList(); + + foreach (var req in allRequests) + { + if (req.PosterPath.Contains("https://image.tmdb.org/t/p/w150/")) + { + var newImg = req.PosterPath.Replace("https://image.tmdb.org/t/p/w150/", string.Empty); + req.PosterPath = newImg; + } + } + RequestService.BatchUpdate(allRequests); } private void UpdateAdmin() diff --git a/PlexRequests.Core/CacheKeys.cs b/PlexRequests.Core/CacheKeys.cs index 0f718f1f1..e40dd9efd 100644 --- a/PlexRequests.Core/CacheKeys.cs +++ b/PlexRequests.Core/CacheKeys.cs @@ -30,7 +30,7 @@ namespace PlexRequests.Core { public struct TimeFrameMinutes { - public const int SchedulerCaching = 60; + public const int SchedulerCaching = 120; } public const string PlexLibaries = nameof(PlexLibaries); diff --git a/PlexRequests.Services/Interfaces/IJobRecord.cs b/PlexRequests.Services/Interfaces/IJobRecord.cs index da6bc6b3f..a47a31bbe 100644 --- a/PlexRequests.Services/Interfaces/IJobRecord.cs +++ b/PlexRequests.Services/Interfaces/IJobRecord.cs @@ -36,5 +36,6 @@ namespace PlexRequests.Services.Interfaces void Record(string jobName); Task> GetJobsAsync(); IEnumerable GetJobs(); + void SetRunning(bool running, string jobName); } } \ No newline at end of file diff --git a/PlexRequests.Services/Jobs/CouchPotatoCacher.cs b/PlexRequests.Services/Jobs/CouchPotatoCacher.cs index 5ad3d99d8..3977f277f 100644 --- a/PlexRequests.Services/Jobs/CouchPotatoCacher.cs +++ b/PlexRequests.Services/Jobs/CouchPotatoCacher.cs @@ -81,6 +81,7 @@ namespace PlexRequests.Services.Jobs finally { Job.Record(JobNames.CpCacher); + Job.SetRunning(false, JobNames.CpCacher); } } } @@ -108,6 +109,7 @@ namespace PlexRequests.Services.Jobs public void Execute(IJobExecutionContext context) { + Job.SetRunning(true, JobNames.CpCacher); Queued(); } } diff --git a/PlexRequests.Services/Jobs/FaultQueueHandler.cs b/PlexRequests.Services/Jobs/FaultQueueHandler.cs index f30a4f2d9..e774043f3 100644 --- a/PlexRequests.Services/Jobs/FaultQueueHandler.cs +++ b/PlexRequests.Services/Jobs/FaultQueueHandler.cs @@ -87,6 +87,8 @@ namespace PlexRequests.Services.Jobs public void Execute(IJobExecutionContext context) { + + Record.SetRunning(true, JobNames.CpCacher); try { var faultedRequests = Repo.GetAll().ToList(); @@ -105,6 +107,7 @@ namespace PlexRequests.Services.Jobs finally { Record.Record(JobNames.FaultQueueHandler); + Record.SetRunning(false, JobNames.CpCacher); } } diff --git a/PlexRequests.Services/Jobs/JobRecord.cs b/PlexRequests.Services/Jobs/JobRecord.cs index 3538a7b0b..629ac8446 100644 --- a/PlexRequests.Services/Jobs/JobRecord.cs +++ b/PlexRequests.Services/Jobs/JobRecord.cs @@ -60,6 +60,23 @@ namespace PlexRequests.Services.Jobs } } + public void SetRunning(bool running, string jobName) + { + var allJobs = Repo.GetAll(); + var storeJob = allJobs.FirstOrDefault(x => x.Name == jobName); + if (storeJob != null) + { + storeJob.Running = running; + Repo.Update(storeJob); + } + else + { + var job = new ScheduledJobs { Running = running, Name = jobName }; + Repo.Insert(job); + } + } + + public async Task> GetJobsAsync() { return await Repo.GetAllAsync(); diff --git a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs index 7ba84cb79..108b115dd 100644 --- a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs +++ b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs @@ -158,7 +158,7 @@ namespace PlexRequests.Services.Jobs } Job.Record(JobNames.PlexChecker); - + Job.SetRunning(false, JobNames.CpCacher); } public List GetPlexMovies() @@ -503,6 +503,8 @@ namespace PlexRequests.Services.Jobs public void Execute(IJobExecutionContext context) { + + Job.SetRunning(true, JobNames.CpCacher); try { CheckAndUpdateAll(); diff --git a/PlexRequests.Services/Jobs/PlexEpisodeCacher.cs b/PlexRequests.Services/Jobs/PlexEpisodeCacher.cs index 3ed43db50..3bf086752 100644 --- a/PlexRequests.Services/Jobs/PlexEpisodeCacher.cs +++ b/PlexRequests.Services/Jobs/PlexEpisodeCacher.cs @@ -145,6 +145,7 @@ namespace PlexRequests.Services.Jobs public void Execute(IJobExecutionContext context) { + Job.SetRunning(true, JobNames.CpCacher); try { var s = Plex.GetSettings(); @@ -171,6 +172,7 @@ namespace PlexRequests.Services.Jobs finally { Job.Record(JobNames.EpisodeCacher); + Job.SetRunning(false, JobNames.CpCacher); } } } diff --git a/PlexRequests.Services/Jobs/RecentlyAdded.cs b/PlexRequests.Services/Jobs/RecentlyAdded.cs index 73e6fba52..204b851be 100644 --- a/PlexRequests.Services/Jobs/RecentlyAdded.cs +++ b/PlexRequests.Services/Jobs/RecentlyAdded.cs @@ -78,6 +78,8 @@ namespace PlexRequests.Services.Jobs public void Execute(IJobExecutionContext context) { + + JobRecord.SetRunning(true, JobNames.CpCacher); try { var settings = NewsletterSettings.GetSettings(); @@ -95,6 +97,7 @@ namespace PlexRequests.Services.Jobs finally { JobRecord.Record(JobNames.RecentlyAddedEmail); + JobRecord.SetRunning(false, JobNames.CpCacher); } } diff --git a/PlexRequests.Services/Jobs/SickRageCacher.cs b/PlexRequests.Services/Jobs/SickRageCacher.cs index cece5100d..10ac125c3 100644 --- a/PlexRequests.Services/Jobs/SickRageCacher.cs +++ b/PlexRequests.Services/Jobs/SickRageCacher.cs @@ -58,6 +58,7 @@ namespace PlexRequests.Services.Jobs public void Queued() { + Job.SetRunning(true, JobNames.CpCacher); Log.Trace("Getting the settings"); var settings = SrSettings.GetSettings(); @@ -79,6 +80,7 @@ namespace PlexRequests.Services.Jobs finally { Job.Record(JobNames.SrCacher); + Job.SetRunning(false, JobNames.CpCacher); } } } diff --git a/PlexRequests.Services/Jobs/SonarrCacher.cs b/PlexRequests.Services/Jobs/SonarrCacher.cs index d94561604..9c7cd2cc1 100644 --- a/PlexRequests.Services/Jobs/SonarrCacher.cs +++ b/PlexRequests.Services/Jobs/SonarrCacher.cs @@ -62,6 +62,7 @@ namespace PlexRequests.Services.Jobs public void Queued() { + Job.SetRunning(true, JobNames.CpCacher); var settings = SonarrSettings.GetSettings(); if (settings.Enabled) { @@ -80,6 +81,7 @@ namespace PlexRequests.Services.Jobs finally { Job.Record(JobNames.SonarrCacher); + Job.SetRunning(false, JobNames.CpCacher); } } } diff --git a/PlexRequests.Services/Jobs/StoreBackup.cs b/PlexRequests.Services/Jobs/StoreBackup.cs index dea5d7df0..1bc45f54b 100644 --- a/PlexRequests.Services/Jobs/StoreBackup.cs +++ b/PlexRequests.Services/Jobs/StoreBackup.cs @@ -53,6 +53,7 @@ namespace PlexRequests.Services.Jobs public void Execute(IJobExecutionContext context) { + JobRecord.SetRunning(true, JobNames.CpCacher); TakeBackup(); Cleanup(); } @@ -91,6 +92,7 @@ namespace PlexRequests.Services.Jobs finally { JobRecord.Record(JobNames.StoreBackup); + JobRecord.SetRunning(false, JobNames.CpCacher); } } diff --git a/PlexRequests.Services/Jobs/StoreCleanup.cs b/PlexRequests.Services/Jobs/StoreCleanup.cs index 5c6b0987b..b329eaa95 100644 --- a/PlexRequests.Services/Jobs/StoreCleanup.cs +++ b/PlexRequests.Services/Jobs/StoreCleanup.cs @@ -78,12 +78,14 @@ namespace PlexRequests.Services.Jobs finally { JobRecord.Record(JobNames.StoreCleanup); + JobRecord.SetRunning(false, JobNames.CpCacher); } } public void Execute(IJobExecutionContext context) { + JobRecord.SetRunning(true, JobNames.CpCacher); Cleanup(); } } diff --git a/PlexRequests.Services/Jobs/UserRequestLimitResetter.cs b/PlexRequests.Services/Jobs/UserRequestLimitResetter.cs index 96d613b72..f32320fc8 100644 --- a/PlexRequests.Services/Jobs/UserRequestLimitResetter.cs +++ b/PlexRequests.Services/Jobs/UserRequestLimitResetter.cs @@ -98,6 +98,7 @@ namespace PlexRequests.Services.Jobs public void Execute(IJobExecutionContext context) { + Record.SetRunning(true, JobNames.CpCacher); try { var settings = Settings.GetSettings(); @@ -115,6 +116,7 @@ namespace PlexRequests.Services.Jobs finally { Record.Record(JobNames.RequestLimitReset); + Record.SetRunning(false, JobNames.CpCacher); } } } diff --git a/PlexRequests.Store/Models/ScheduledJobs.cs b/PlexRequests.Store/Models/ScheduledJobs.cs index 7c242f0bd..10c6f745c 100644 --- a/PlexRequests.Store/Models/ScheduledJobs.cs +++ b/PlexRequests.Store/Models/ScheduledJobs.cs @@ -35,5 +35,6 @@ namespace PlexRequests.Store.Models { public string Name { get; set; } public DateTime LastRun { get; set; } + public bool Running { get; set; } } } diff --git a/PlexRequests.Store/SqlTables.sql b/PlexRequests.Store/SqlTables.sql index 84a31306f..4d180b1d0 100644 --- a/PlexRequests.Store/SqlTables.sql +++ b/PlexRequests.Store/SqlTables.sql @@ -80,7 +80,8 @@ CREATE TABLE IF NOT EXISTS ScheduledJobs ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Name varchar(100) NOT NULL, - LastRun varchar(100) NOT NULL + LastRun varchar(100) NOT NULL, + Running INTEGER ); CREATE UNIQUE INDEX IF NOT EXISTS ScheduledJobs_Id ON ScheduledJobs (Id); From 55f13091405bd575ed66933150e0d05047ac86b4 Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Mon, 14 Nov 2016 13:16:02 +0000 Subject: [PATCH 22/87] finished #646 and fixed #664 --- .../MigrationRunner.cs | 41 ++--- .../Migrations/Version1100.cs | 24 ++- .../PlexRequests.Core.Migration.csproj | 4 + PlexRequests.Core.Migration/packages.config | 1 + PlexRequests.Core/Setup.cs | 153 +----------------- .../Jobs/CouchPotatoCacher.cs | 2 +- .../Jobs/PlexAvailabilityChecker.cs | 11 +- .../Jobs/PlexEpisodeCacher.cs | 5 +- PlexRequests.Services/Jobs/RecentlyAdded.cs | 9 +- PlexRequests.Services/Jobs/SickRageCacher.cs | 6 +- PlexRequests.Services/Jobs/SonarrCacher.cs | 4 +- PlexRequests.UI/Content/Themes/plex.css | 6 + PlexRequests.UI/Content/Themes/plex.min.css | 2 +- PlexRequests.UI/Content/Themes/plex.scss | 7 + PlexRequests.UI/Content/base.css | 6 + PlexRequests.UI/Content/base.min.css | 2 +- PlexRequests.UI/Content/base.scss | 7 + PlexRequests.UI/Helpers/HtmlSecurityHelper.cs | 25 ++- PlexRequests.UI/Helpers/SecurityExtensions.cs | 30 +++- PlexRequests.UI/Modules/Admin/AdminModule.cs | 17 +- .../Modules/Admin/FaultQueueModule.cs | 2 +- .../Modules/Admin/SystemStatusModule.cs | 4 +- PlexRequests.UI/Modules/BaseModule.cs | 5 +- ...UpdateCheckerModule.cs => LayoutModule.cs} | 42 ++++- PlexRequests.UI/Modules/LoginModule.cs | 2 +- PlexRequests.UI/Modules/UserLoginModule.cs | 8 +- PlexRequests.UI/PlexRequests.UI.csproj | 3 +- PlexRequests.UI/Resources/UI.da.resx | 30 ++++ PlexRequests.UI/Resources/UI.de.resx | 32 +++- PlexRequests.UI/Resources/UI.es.resx | 27 ++++ PlexRequests.UI/Resources/UI.fr.resx | 30 ++++ PlexRequests.UI/Resources/UI.it.resx | 30 ++++ PlexRequests.UI/Resources/UI.nl.resx | 30 ++++ PlexRequests.UI/Resources/UI.pt.resx | 27 ++++ PlexRequests.UI/Resources/UI.resx | 3 + PlexRequests.UI/Resources/UI.sv.resx | 30 ++++ PlexRequests.UI/Resources/UI1.Designer.cs | 9 ++ .../Validators/PlexRequestsValidator.cs | 2 +- .../Shared/Partial/_LayoutScripts.cshtml | 29 +++- .../Views/Shared/Partial/_Navbar.cshtml | 46 +++--- PlexRequests.UI/Views/Shared/_Layout.cshtml | 2 - 41 files changed, 494 insertions(+), 261 deletions(-) rename PlexRequests.UI/Modules/{UpdateCheckerModule.cs => LayoutModule.cs} (65%) diff --git a/PlexRequests.Core.Migration/MigrationRunner.cs b/PlexRequests.Core.Migration/MigrationRunner.cs index 5535782a6..d53ab8d3d 100644 --- a/PlexRequests.Core.Migration/MigrationRunner.cs +++ b/PlexRequests.Core.Migration/MigrationRunner.cs @@ -23,37 +23,30 @@ namespace PlexRequests.Core.Migration { var con = Db.DbConnection(); var versions = GetMigrations(); - - var dbVersion = con.GetVersionInfo().OrderByDescending(x => x.Version).FirstOrDefault(); - if (dbVersion == null) - { - dbVersion = new TableCreation.VersionInfo { Version = 0 }; - } + + var dbVersion = con.GetVersionInfo().OrderByDescending(x => x.Version).FirstOrDefault() ?? + new TableCreation.VersionInfo { Version = 0 }; foreach (var v in versions) { +#if !DEBUG if (v.Value.Version > dbVersion.Version) { - // Assuming only one constructor - var ctor = v.Key.GetConstructors().FirstOrDefault(); - var dependencies = new List(); +#endif + // Assuming only one constructor + var ctor = v.Key.GetConstructors().FirstOrDefault(); + var dependencies = ctor.GetParameters().Select(param => Kernel.Get(param.ParameterType)).ToList(); - foreach (var param in ctor.GetParameters()) - { - var dep = Kernel.Get(param.ParameterType); - dependencies.Add(dep); - } + var method = v.Key.GetMethod("Start"); + if (method != null) + { + var classInstance = Activator.CreateInstance(v.Key, dependencies.Any() ? dependencies.ToArray() : null); + var parametersArray = new object[] { Db.DbConnection() }; - var method = v.Key.GetMethod("Start"); - if (method != null) - { - object result = null; - var classInstance = Activator.CreateInstance(v.Key, dependencies.Any() ? dependencies.ToArray() : null); - - var parametersArray = new object[] { Db.DbConnection() }; - - method.Invoke(classInstance, parametersArray); - } + method.Invoke(classInstance, parametersArray); } +#if !DEBUG + } +#endif } } diff --git a/PlexRequests.Core.Migration/Migrations/Version1100.cs b/PlexRequests.Core.Migration/Migrations/Version1100.cs index a631b05d6..d4365f357 100644 --- a/PlexRequests.Core.Migration/Migrations/Version1100.cs +++ b/PlexRequests.Core.Migration/Migrations/Version1100.cs @@ -27,7 +27,10 @@ using System; using System.Data; +using NLog; using System.Linq; +using PlexRequests.Core.SettingModels; +using PlexRequests.Helpers; using PlexRequests.Helpers.Permissions; using PlexRequests.Store; using PlexRequests.Store.Repository; @@ -37,14 +40,16 @@ namespace PlexRequests.Core.Migration.Migrations [Migration(11000, "v1.10.0.0")] public class Version1100 : BaseMigration, IMigration { - public Version1100(IUserRepository userRepo, IRequestService requestService) + public Version1100(IUserRepository userRepo, IRequestService requestService, ISettingsService log) { UserRepo = userRepo; RequestService = requestService; + Log = log; } public int Version => 11000; private IUserRepository UserRepo { get; } private IRequestService RequestService { get; } + private ISettingsService Log { get; } public void Start(IDbConnection con) { @@ -52,22 +57,25 @@ namespace PlexRequests.Core.Migration.Migrations // Update the current admin permissions set UpdateAdmin(); - - + ResetLogLevel(); UpdateSchema(con, Version); } + private void ResetLogLevel() + { + var logSettings = Log.GetSettings(); + logSettings.Level = LogLevel.Error.Ordinal; + Log.SaveSettings(logSettings); + + LoggingHelper.ReconfigureLogLevel(LogLevel.FromOrdinal(logSettings.Level)); + } + private void UpdateDb(IDbConnection con) { // Create the two new columns con.AlterTable("Users", "ADD", "Permissions", true, "INTEGER"); con.AlterTable("Users", "ADD", "Features", true, "INTEGER"); - // Add the new 'running' item into the scheduled jobs so we can check if the cachers are running - con.AlterTable("ScheduledJobs", "ADD", "Running", true, "INTEGER"); - - - //https://image.tmdb.org/t/p/w150/https://image.tmdb.org/t/p/w150//aqhAqttDq7zgsTaBHtCD8wmTk6k.jpg // UI = https://image.tmdb.org/t/p/w150/{{posterPath}} diff --git a/PlexRequests.Core.Migration/PlexRequests.Core.Migration.csproj b/PlexRequests.Core.Migration/PlexRequests.Core.Migration.csproj index e724145c4..fa934dfc0 100644 --- a/PlexRequests.Core.Migration/PlexRequests.Core.Migration.csproj +++ b/PlexRequests.Core.Migration/PlexRequests.Core.Migration.csproj @@ -45,6 +45,10 @@ ..\packages\Ninject.3.2.0.0\lib\net45-full\Ninject.dll + + ..\packages\NLog.4.3.11\lib\net45\NLog.dll + True + ..\packages\Quartz.2.3.3\lib\net40\Quartz.dll True diff --git a/PlexRequests.Core.Migration/packages.config b/PlexRequests.Core.Migration/packages.config index dfdfc5ae9..6d547be19 100644 --- a/PlexRequests.Core.Migration/packages.config +++ b/PlexRequests.Core.Migration/packages.config @@ -2,5 +2,6 @@ + \ No newline at end of file diff --git a/PlexRequests.Core/Setup.cs b/PlexRequests.Core/Setup.cs index 56f3ab95e..b9f2b7518 100644 --- a/PlexRequests.Core/Setup.cs +++ b/PlexRequests.Core/Setup.cs @@ -60,52 +60,13 @@ namespace PlexRequests.Core // Shrink DB TableCreation.Vacuum(Db.DbConnection()); } - - - // The below code is obsolete, we should use PlexRequests.Core.Migrations.MigrationRunner - var version = CheckSchema(); - if (version > 0) - { - if (version > 1899 && version <= 1900) - { - MigrateToVersion1900(); - } - - if(version > 1899 && version <= 1910) - { - MigrateToVersion1910(); - } - } - + + // Add the new 'running' item into the scheduled jobs so we can check if the cachers are running + Db.DbConnection().AlterTable("ScheduledJobs", "ADD", "Running", true, "INTEGER"); + return Db.DbConnection().ConnectionString; } - public static string ConnectionString => Db.DbConnection().ConnectionString; - - - private int CheckSchema() - { - var productVersion = AssemblyHelper.GetProductVersion(); - var trimStatus = new Regex("[^0-9]", RegexOptions.Compiled).Replace(productVersion, string.Empty).PadRight(4, '0'); - var version = int.Parse(trimStatus); - - var connection = Db.DbConnection(); - var schema = connection.GetSchemaVersion(); - if (schema == null) - { - connection.CreateSchema(version); // Set the default. - schema = connection.GetSchemaVersion(); - } - if (version > schema.SchemaVersion) - { - Db.DbConnection().UpdateSchemaVersion(version); - schema = connection.GetSchemaVersion(); - } - version = schema.SchemaVersion; - - return version; - } - private void CreateDefaultSettingsPage(string baseUrl) { var defaultSettings = new PlexRequestSettings @@ -148,7 +109,6 @@ namespace PlexRequests.Core Task.Run(() => { CacheSonarrQualityProfiles(mc); }); Task.Run(() => { CacheCouchPotatoQualityProfiles(mc); }); // we don't need to cache sickrage profiles, those are static - // TODO: cache headphones profiles? } catch (Exception) { @@ -156,12 +116,12 @@ namespace PlexRequests.Core } } - private void CacheSonarrQualityProfiles(MemoryCacheProvider cacheProvider) + private void CacheSonarrQualityProfiles(ICacheProvider cacheProvider) { try { Log.Info("Executing GetSettings call to Sonarr for quality profiles"); - var sonarrSettingsService = new SettingsServiceV2(new SettingsJsonRepository(new DbConfiguration(new SqliteFactory()), new MemoryCacheProvider())); + var sonarrSettingsService = new SettingsServiceV2(new SettingsJsonRepository(new DbConfiguration(new SqliteFactory()), cacheProvider)); var sonarrSettings = sonarrSettingsService.GetSettings(); if (sonarrSettings.Enabled) { @@ -178,12 +138,12 @@ namespace PlexRequests.Core } } - private void CacheCouchPotatoQualityProfiles(MemoryCacheProvider cacheProvider) + private void CacheCouchPotatoQualityProfiles(ICacheProvider cacheProvider) { try { Log.Info("Executing GetSettings call to CouchPotato for quality profiles"); - var cpSettingsService = new SettingsServiceV2(new SettingsJsonRepository(new DbConfiguration(new SqliteFactory()), new MemoryCacheProvider())); + var cpSettingsService = new SettingsServiceV2(new SettingsJsonRepository(new DbConfiguration(new SqliteFactory()), cacheProvider)); var cpSettings = cpSettingsService.GetSettings(); if (cpSettings.Enabled) { @@ -199,102 +159,5 @@ namespace PlexRequests.Core Log.Error(ex, "Failed to cache CouchPotato quality profiles!"); } } - - - /// - /// Migrates to version 1.9. - /// Move the Plex auth token to the new field. - /// Reconfigure the log level - /// Set the wizard flag to true if we already have settings - /// - public void MigrateToVersion1900() - { - // Need to change the Plex Token location - var authSettings = new SettingsServiceV2(new SettingsJsonRepository(Db, new MemoryCacheProvider())); - var auth = authSettings.GetSettings(); - var plexSettings = new SettingsServiceV2(new SettingsJsonRepository(Db, new MemoryCacheProvider())); - - if (auth != null) - { - //If we have an authToken we do not need to go through the setup - if (!string.IsNullOrEmpty(auth.OldPlexAuthToken)) - { - var prServuce = new SettingsServiceV2(new SettingsJsonRepository(Db, new MemoryCacheProvider())); - var settings = prServuce.GetSettings(); - settings.Wizard = true; - prServuce.SaveSettings(settings); - } - - // Clear out the old token and save it to the new field - var currentSettings = plexSettings.GetSettings(); - if (!string.IsNullOrEmpty(auth.OldPlexAuthToken)) - { - currentSettings.PlexAuthToken = auth.OldPlexAuthToken; - plexSettings.SaveSettings(currentSettings); - - // Clear out the old value - auth.OldPlexAuthToken = string.Empty; - authSettings.SaveSettings(auth); - } - - } - - - // Set the log level - try - { - var settingsService = new SettingsServiceV2(new SettingsJsonRepository(Db, new MemoryCacheProvider())); - var logSettings = settingsService.GetSettings(); - logSettings.Level = LogLevel.Error.Ordinal; - settingsService.SaveSettings(logSettings); - - LoggingHelper.ReconfigureLogLevel(LogLevel.FromOrdinal(logSettings.Level)); - } - catch (Exception e) - { - Log.Error(e); - } - - - // Enable analytics; - try - { - - var prSettings = new SettingsServiceV2(new SettingsJsonRepository(Db, new MemoryCacheProvider())); - var settings = prSettings.GetSettings(); - settings.CollectAnalyticData = true; - var updated = prSettings.SaveSettings(settings); - - } - catch (Exception e) - { - Log.Error(e); - } - } - - /// - /// Migrates to version1910. - /// - public void MigrateToVersion1910() - { - try - { - // Get the new machine Identifier - var settings = new SettingsServiceV2(new SettingsJsonRepository(Db, new MemoryCacheProvider())); - var plex = settings.GetSettings(); - if (!string.IsNullOrEmpty(plex.PlexAuthToken)) - { - var api = new PlexApi(new ApiRequest()); - var server = api.GetServer(plex.PlexAuthToken); // Get the server info - plex.MachineIdentifier = server.Server.FirstOrDefault(x => x.AccessToken == plex.PlexAuthToken)?.MachineIdentifier; - - settings.SaveSettings(plex); // Save the new settings - } - } - catch (Exception e) - { - Log.Error(e); - } - } } } diff --git a/PlexRequests.Services/Jobs/CouchPotatoCacher.cs b/PlexRequests.Services/Jobs/CouchPotatoCacher.cs index 3977f277f..a45e74509 100644 --- a/PlexRequests.Services/Jobs/CouchPotatoCacher.cs +++ b/PlexRequests.Services/Jobs/CouchPotatoCacher.cs @@ -65,6 +65,7 @@ namespace PlexRequests.Services.Jobs var settings = CpSettings.GetSettings(); if (settings.Enabled) { + Job.SetRunning(true, JobNames.CpCacher); Log.Trace("Getting all movies from CouchPotato"); try { @@ -109,7 +110,6 @@ namespace PlexRequests.Services.Jobs public void Execute(IJobExecutionContext context) { - Job.SetRunning(true, JobNames.CpCacher); Queued(); } } diff --git a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs index 108b115dd..8ecb00bb5 100644 --- a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs +++ b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs @@ -87,7 +87,6 @@ namespace PlexRequests.Services.Jobs Log.Debug("Validation of the plex settings failed."); return; } - var libraries = CachedLibraries(plexSettings, true); //force setting the cache (10 min intervals via scheduler) if (libraries == null || !libraries.Any()) @@ -156,9 +155,6 @@ namespace PlexRequests.Services.Jobs NotificationEngine.NotifyUsers(modifiedModel, plexSettings.PlexAuthToken, NotificationType.RequestAvailable); RequestService.BatchUpdate(modifiedModel); } - - Job.Record(JobNames.PlexChecker); - Job.SetRunning(false, JobNames.CpCacher); } public List GetPlexMovies() @@ -504,7 +500,7 @@ namespace PlexRequests.Services.Jobs public void Execute(IJobExecutionContext context) { - Job.SetRunning(true, JobNames.CpCacher); + Job.SetRunning(true, JobNames.PlexChecker); try { CheckAndUpdateAll(); @@ -513,6 +509,11 @@ namespace PlexRequests.Services.Jobs { Log.Error(e); } + finally + { + Job.Record(JobNames.PlexChecker); + Job.SetRunning(false, JobNames.PlexChecker); + } } } } \ No newline at end of file diff --git a/PlexRequests.Services/Jobs/PlexEpisodeCacher.cs b/PlexRequests.Services/Jobs/PlexEpisodeCacher.cs index 3bf086752..e91d61e3e 100644 --- a/PlexRequests.Services/Jobs/PlexEpisodeCacher.cs +++ b/PlexRequests.Services/Jobs/PlexEpisodeCacher.cs @@ -145,7 +145,7 @@ namespace PlexRequests.Services.Jobs public void Execute(IJobExecutionContext context) { - Job.SetRunning(true, JobNames.CpCacher); + try { var s = Plex.GetSettings(); @@ -163,6 +163,7 @@ namespace PlexRequests.Services.Jobs return; } } + Job.SetRunning(true, JobNames.EpisodeCacher); CacheEpisodes(s); } catch (Exception e) @@ -172,7 +173,7 @@ namespace PlexRequests.Services.Jobs finally { Job.Record(JobNames.EpisodeCacher); - Job.SetRunning(false, JobNames.CpCacher); + Job.SetRunning(false, JobNames.EpisodeCacher); } } } diff --git a/PlexRequests.Services/Jobs/RecentlyAdded.cs b/PlexRequests.Services/Jobs/RecentlyAdded.cs index 204b851be..fb1d22c1b 100644 --- a/PlexRequests.Services/Jobs/RecentlyAdded.cs +++ b/PlexRequests.Services/Jobs/RecentlyAdded.cs @@ -78,8 +78,6 @@ namespace PlexRequests.Services.Jobs public void Execute(IJobExecutionContext context) { - - JobRecord.SetRunning(true, JobNames.CpCacher); try { var settings = NewsletterSettings.GetSettings(); @@ -87,7 +85,7 @@ namespace PlexRequests.Services.Jobs { return; } - + JobRecord.SetRunning(true, JobNames.RecentlyAddedEmail); Start(settings); } catch (Exception e) @@ -97,7 +95,7 @@ namespace PlexRequests.Services.Jobs finally { JobRecord.Record(JobNames.RecentlyAddedEmail); - JobRecord.SetRunning(false, JobNames.CpCacher); + JobRecord.SetRunning(false, JobNames.RecentlyAddedEmail); } } @@ -264,7 +262,8 @@ namespace PlexRequests.Services.Jobs { foreach (var user in users.User) { - message.Bcc.Add(new MailboxAddress(user.Username, user.Email)); + if (user.Email != null) + message.Bcc.Add(new MailboxAddress(user.Username, user.Email)); } } } diff --git a/PlexRequests.Services/Jobs/SickRageCacher.cs b/PlexRequests.Services/Jobs/SickRageCacher.cs index 10ac125c3..6fedf4b27 100644 --- a/PlexRequests.Services/Jobs/SickRageCacher.cs +++ b/PlexRequests.Services/Jobs/SickRageCacher.cs @@ -58,12 +58,14 @@ namespace PlexRequests.Services.Jobs public void Queued() { - Job.SetRunning(true, JobNames.CpCacher); Log.Trace("Getting the settings"); var settings = SrSettings.GetSettings(); if (settings.Enabled) { + + Job.SetRunning(true, JobNames.SrCacher); + Log.Trace("Getting all shows from SickRage"); try { @@ -80,7 +82,7 @@ namespace PlexRequests.Services.Jobs finally { Job.Record(JobNames.SrCacher); - Job.SetRunning(false, JobNames.CpCacher); + Job.SetRunning(false, JobNames.SrCacher); } } } diff --git a/PlexRequests.Services/Jobs/SonarrCacher.cs b/PlexRequests.Services/Jobs/SonarrCacher.cs index 9c7cd2cc1..9e0304f37 100644 --- a/PlexRequests.Services/Jobs/SonarrCacher.cs +++ b/PlexRequests.Services/Jobs/SonarrCacher.cs @@ -62,10 +62,10 @@ namespace PlexRequests.Services.Jobs public void Queued() { - Job.SetRunning(true, JobNames.CpCacher); var settings = SonarrSettings.GetSettings(); if (settings.Enabled) { + Job.SetRunning(true, JobNames.SonarrCacher); try { var series = SonarrApi.GetSeries(settings.ApiKey, settings.FullUri); @@ -81,7 +81,7 @@ namespace PlexRequests.Services.Jobs finally { Job.Record(JobNames.SonarrCacher); - Job.SetRunning(false, JobNames.CpCacher); + Job.SetRunning(false, JobNames.SonarrCacher); } } } diff --git a/PlexRequests.UI/Content/Themes/plex.css b/PlexRequests.UI/Content/Themes/plex.css index dbd72c8b4..be07c9a3a 100644 --- a/PlexRequests.UI/Content/Themes/plex.css +++ b/PlexRequests.UI/Content/Themes/plex.css @@ -182,3 +182,9 @@ button.list-group-item:focus { #sidebar-wrapper { background: #252424; } +#cacherRunning { + background-color: #333333; + text-align: center; + font-size: 15px; + padding: 3px 0; } + diff --git a/PlexRequests.UI/Content/Themes/plex.min.css b/PlexRequests.UI/Content/Themes/plex.min.css index 006124205..d42c984ba 100644 --- a/PlexRequests.UI/Content/Themes/plex.min.css +++ b/PlexRequests.UI/Content/Themes/plex.min.css @@ -1 +1 @@ -.form-control-custom{background-color:#333 !important;}.form-control-custom-disabled{background-color:#252424 !important;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background:#df691a;}.scroll-top-wrapper{background-color:#333;}.scroll-top-wrapper:hover{background-color:#df691a;}body{font-family:Open Sans Regular,Helvetica Neue,Helvetica,Arial,sans-serif;color:#eee;background-color:#1f1f1f;}.table-striped>tbody>tr:nth-of-type(odd){background-color:#333;}.table-hover>tbody>tr:hover{background-color:#282828;}fieldset{padding:15px;}legend{border-bottom:1px solid #333;}.form-control{color:#fefefe;background-color:#333;}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{margin-left:-0;}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:-15px;}.dropdown-menu{background-color:#282828;}.dropdown-menu .divider{background-color:#333;}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#333;}.input-group-addon{background-color:#333;}.nav>li>a:hover,.nav>li>a:focus{background-color:#df691a;}.nav-tabs>li>a:hover{border-color:#df691a #df691a transparent;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background-color:#df691a;border:1px solid #df691a;}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #df691a;}.navbar-default{background-color:#0a0a0a;}.navbar-default .navbar-brand{color:#df691a;}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#f0ad4e;background-color:#282828;}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{background-color:#282828;}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:#df691a;color:#fff;}.pagination>li>a,.pagination>li>span{background-color:#282828;}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{background-color:#333;}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#fefefe;background-color:#333;}.list-group-item{background-color:#282828;}a.list-group-item:hover,button.list-group-item:hover,a.list-group-item:focus,button.list-group-item:focus{background-color:#333;}.input-addon,.input-group-addon{color:#df691a;}.modal-header,.modal-footer{background-color:#282828;}.modal-content{position:relative;background-color:#282828;border:1px solid transparent;border-radius:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;outline:0;}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:300;color:#ebebeb;line-height:1;vertical-align:middle;white-space:nowrap;text-align:center;background-color:#333;border-radius:10px;}.bootstrap-datetimepicker-widget.dropdown-menu{background-color:#333;}.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after{border-bottom:6px solid #333 !important;}#sidebar-wrapper{background:#252424;} \ No newline at end of file +.form-control-custom{background-color:#333 !important;}.form-control-custom-disabled{background-color:#252424 !important;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background:#df691a;}.scroll-top-wrapper{background-color:#333;}.scroll-top-wrapper:hover{background-color:#df691a;}body{font-family:Open Sans Regular,Helvetica Neue,Helvetica,Arial,sans-serif;color:#eee;background-color:#1f1f1f;}.table-striped>tbody>tr:nth-of-type(odd){background-color:#333;}.table-hover>tbody>tr:hover{background-color:#282828;}fieldset{padding:15px;}legend{border-bottom:1px solid #333;}.form-control{color:#fefefe;background-color:#333;}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{margin-left:-0;}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:-15px;}.dropdown-menu{background-color:#282828;}.dropdown-menu .divider{background-color:#333;}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#333;}.input-group-addon{background-color:#333;}.nav>li>a:hover,.nav>li>a:focus{background-color:#df691a;}.nav-tabs>li>a:hover{border-color:#df691a #df691a transparent;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background-color:#df691a;border:1px solid #df691a;}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #df691a;}.navbar-default{background-color:#0a0a0a;}.navbar-default .navbar-brand{color:#df691a;}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#f0ad4e;background-color:#282828;}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{background-color:#282828;}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:#df691a;color:#fff;}.pagination>li>a,.pagination>li>span{background-color:#282828;}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{background-color:#333;}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#fefefe;background-color:#333;}.list-group-item{background-color:#282828;}a.list-group-item:hover,button.list-group-item:hover,a.list-group-item:focus,button.list-group-item:focus{background-color:#333;}.input-addon,.input-group-addon{color:#df691a;}.modal-header,.modal-footer{background-color:#282828;}.modal-content{position:relative;background-color:#282828;border:1px solid transparent;border-radius:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;outline:0;}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:300;color:#ebebeb;line-height:1;vertical-align:middle;white-space:nowrap;text-align:center;background-color:#333;border-radius:10px;}.bootstrap-datetimepicker-widget.dropdown-menu{background-color:#333;}.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after{border-bottom:6px solid #333 !important;}#sidebar-wrapper{background:#252424;}#cacherRunning{background-color:#333;text-align:center;font-size:15px;padding:3px 0;} \ No newline at end of file diff --git a/PlexRequests.UI/Content/Themes/plex.scss b/PlexRequests.UI/Content/Themes/plex.scss index 067dad666..8a5ffbe8b 100644 --- a/PlexRequests.UI/Content/Themes/plex.scss +++ b/PlexRequests.UI/Content/Themes/plex.scss @@ -228,3 +228,10 @@ button.list-group-item:focus { #sidebar-wrapper { background: $bg-colour-disabled; } + +#cacherRunning { + background-color: $bg-colour; + text-align: center; + font-size: 15px; + padding: 3px 0; +} \ No newline at end of file diff --git a/PlexRequests.UI/Content/base.css b/PlexRequests.UI/Content/base.css index 602ee6369..d48d329f8 100644 --- a/PlexRequests.UI/Content/base.css +++ b/PlexRequests.UI/Content/base.css @@ -257,6 +257,12 @@ label { font-size: 15px; padding: 3px 0; } +#cacherRunning { + background-color: #4e5d6c; + text-align: center; + font-size: 15px; + padding: 3px 0; } + .checkbox label { display: inline-block; cursor: pointer; diff --git a/PlexRequests.UI/Content/base.min.css b/PlexRequests.UI/Content/base.min.css index 5cd192ff8..b47ab325c 100644 --- a/PlexRequests.UI/Content/base.min.css +++ b/PlexRequests.UI/Content/base.min.css @@ -1 +1 @@ -@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}.landing-block .media{max-width:450px;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#fff;}hr{border:1px dashed #777;}.btn{border-radius:.25rem !important;}.btn-group-separated .btn,.btn-group-separated .btn+.btn{margin-left:3px;}.multiSelect{background-color:#4e5d6c;}.form-control-custom{background-color:#4e5d6c !important;color:#fff !important;border-radius:0;box-shadow:0 0 0 !important;}h1{font-size:3.5rem !important;font-weight:600 !important;}.request-title{margin-top:0 !important;font-size:1.9rem !important;}p{font-size:1.1rem !important;}label{display:inline-block !important;margin-bottom:.5rem !important;font-size:16px !important;}.nav-tabs>li{font-size:13px;line-height:21px;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background:#4e5d6c;}.nav-tabs>li>a>.fa{padding:3px 5px 3px 3px;}.nav-tabs>li.nav-tab-right{float:right;}.nav-tabs>li.nav-tab-right a{margin-right:0;margin-left:2px;}.nav-tabs>li.nav-tab-icononly .fa{padding:3px;}.navbar .nav a .fa,.dropdown-menu a .fa{font-size:130%;top:1px;position:relative;display:inline-block;margin-right:5px;}.dropdown-menu a .fa{top:2px;}.btn-danger-outline{color:#d9534f !important;background-color:transparent;background-image:none;border-color:#d9534f !important;}.btn-danger-outline:focus,.btn-danger-outline.focus,.btn-danger-outline:active,.btn-danger-outline.active,.btn-danger-outline:hover,.open>.btn-danger-outline.dropdown-toggle{color:#fff !important;background-color:#d9534f !important;border-color:#d9534f !important;}.btn-primary-outline{color:#ff761b !important;background-color:transparent;background-image:none;border-color:#ff761b !important;}.btn-primary-outline:focus,.btn-primary-outline.focus,.btn-primary-outline:active,.btn-primary-outline.active,.btn-primary-outline:hover,.open>.btn-primary-outline.dropdown-toggle{color:#fff !important;background-color:#df691a !important;border-color:#df691a !important;}.btn-info-outline{color:#5bc0de !important;background-color:transparent;background-image:none;border-color:#5bc0de !important;}.btn-info-outline:focus,.btn-info-outline.focus,.btn-info-outline:active,.btn-info-outline.active,.btn-info-outline:hover,.open>.btn-info-outline.dropdown-toggle{color:#fff !important;background-color:#5bc0de !important;border-color:#5bc0de !important;}.btn-warning-outline{color:#f0ad4e !important;background-color:transparent;background-image:none;border-color:#f0ad4e !important;}.btn-warning-outline:focus,.btn-warning-outline.focus,.btn-warning-outline:active,.btn-warning-outline.active,.btn-warning-outline:hover,.open>.btn-warning-outline.dropdown-toggle{color:#fff !important;background-color:#f0ad4e !important;border-color:#f0ad4e !important;}.btn-success-outline{color:#5cb85c !important;background-color:transparent;background-image:none;border-color:#5cb85c !important;}.btn-success-outline:focus,.btn-success-outline.focus,.btn-success-outline:active,.btn-success-outline.active,.btn-success-outline:hover,.open>.btn-success-outline.dropdown-toggle{color:#fff !important;background-color:#5cb85c !important;border-color:#5cb85c !important;}#movieList .mix{display:none;}#tvList .mix{display:none;}.scroll-top-wrapper{position:fixed;opacity:0;visibility:hidden;overflow:hidden;text-align:center;z-index:99999999;background-color:#4e5d6c;color:#eee;width:50px;height:48px;line-height:48px;right:30px;bottom:30px;padding-top:2px;border-top-left-radius:10px;border-top-right-radius:10px;border-bottom-right-radius:10px;border-bottom-left-radius:10px;-webkit-transition:all .5s ease-in-out;-moz-transition:all .5s ease-in-out;-ms-transition:all .5s ease-in-out;-o-transition:all .5s ease-in-out;transition:all .5s ease-in-out;}.scroll-top-wrapper:hover{background-color:#637689;}.scroll-top-wrapper.show{visibility:visible;cursor:pointer;opacity:1;}.scroll-top-wrapper i.fa{line-height:inherit;}.no-search-results{text-align:center;}.no-search-results .no-search-results-icon{font-size:10em;color:#4e5d6c;}.no-search-results .no-search-results-text{margin:20px 0;color:#ccc;}.form-control-search{padding:13px 105px 13px 16px;height:100%;}.form-control-withbuttons{padding-right:105px;}.input-group-addon .btn-group{position:absolute;right:45px;z-index:3;top:10px;box-shadow:0 0 0;}.input-group-addon .btn-group .btn{border:1px solid rgba(255,255,255,.7) !important;padding:3px 12px;color:rgba(255,255,255,.7) !important;}.btn-split .btn{border-radius:0 !important;}.btn-split .btn:not(.dropdown-toggle){border-radius:.25rem 0 0 .25rem !important;}.btn-split .btn.dropdown-toggle{border-radius:0 .25rem .25rem 0 !important;padding:12px 8px;}#updateAvailable{background-color:#df691a;text-align:center;font-size:15px;padding:3px 0;}.checkbox label{display:inline-block;cursor:pointer;position:relative;padding-left:25px;margin-right:15px;font-size:13px;margin-bottom:10px;}.checkbox label:before{content:"";display:inline-block;width:18px;height:18px;margin-right:10px;position:absolute;left:0;bottom:1px;border:2px solid #eee;border-radius:3px;}.checkbox input[type=checkbox]{display:none;}.checkbox input[type=checkbox]:checked+label:before{content:"✓";font-size:13px;color:#fafafa;text-align:center;line-height:13px;}.input-group-sm{padding-top:2px;padding-bottom:2px;}.tab-pane .form-horizontal .form-group{margin-right:15px;margin-left:15px;}.bootstrap-datetimepicker-widget.dropdown-menu{background-color:#4e5d6c;}.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after{border-bottom:6px solid #4e5d6c !important;}.bootstrap-datetimepicker-widget table td.active,.bootstrap-datetimepicker-widget table td.active:hover{color:#fff !important;}.landing-header{display:block;margin:60px auto;}.landing-block{background:#2f2f2f !important;padding:5px;}.landing-block .media{margin:30px auto;max-width:450px;}.landing-block .media .media-left{display:inline-block;float:left;width:70px;}.landing-block .media .media-left i.fa{font-size:3em;}.landing-title{font-weight:bold;}.checkbox-custom{margin-top:0 !important;margin-bottom:0 !important;}.tooltip_templates{display:none;}.shadow{-moz-box-shadow:3px 3px 5px 6px #191919;-webkit-box-shadow:3px 3px 5px 6px #191919;box-shadow:3px 3px 5px 6px #191919;}.img-circle{border-radius:50%;}#wrapper{padding-left:0;-webkit-transition:all .5s ease;-moz-transition:all .5s ease;-o-transition:all .5s ease;transition:all .5s ease;}#wrapper.toggled{padding-right:250px;}#sidebar-wrapper{z-index:1000;position:fixed;right:250px;width:0;height:100%;margin-right:-250px;overflow-y:auto;background:#4e5d6c;padding-left:15px;-webkit-transition:all .5s ease;-moz-transition:all .5s ease;-o-transition:all .5s ease;transition:all .5s ease;}#wrapper.toggled #sidebar-wrapper{width:500px;}#page-content-wrapper{width:100%;position:absolute;padding:15px;}#wrapper.toggled #page-content-wrapper{position:absolute;margin-left:-250px;}.sidebar-nav{position:absolute;top:0;width:500px;margin:0;padding-left:0;list-style:none;}.sidebar-nav li{text-indent:20px;line-height:40px;}.sidebar-nav li a{display:block;text-decoration:none;color:#999;}.sidebar-nav li a:hover{text-decoration:none;color:#fff;background:rgba(255,255,255,.2);}.sidebar-nav li a:active,.sidebar-nav li a:focus{text-decoration:none;}.sidebar-nav>.sidebar-brand{height:65px;font-size:18px;line-height:60px;}.sidebar-nav>.sidebar-brand a{color:#999;}.sidebar-nav>.sidebar-brand a:hover{color:#fff;background:none;}@media(min-width:768px){#wrapper{padding-right:250px;}#wrapper.toggled{padding-right:0;}#sidebar-wrapper{width:500px;}#wrapper.toggled #sidebar-wrapper{width:0;}#page-content-wrapper{padding:20px;position:relative;}#wrapper.toggled #page-content-wrapper{position:relative;margin-right:0;}}#lightbox{background-color:#808080;filter:alpha(opacity=50);opacity:.5;-moz-opacity:.5;top:0;left:0;z-index:20;height:100%;width:100%;background-repeat:no-repeat;background-position:center;position:absolute;} \ No newline at end of file +@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}.landing-block .media{max-width:450px;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#fff;}hr{border:1px dashed #777;}.btn{border-radius:.25rem !important;}.btn-group-separated .btn,.btn-group-separated .btn+.btn{margin-left:3px;}.multiSelect{background-color:#4e5d6c;}.form-control-custom{background-color:#4e5d6c !important;color:#fff !important;border-radius:0;box-shadow:0 0 0 !important;}h1{font-size:3.5rem !important;font-weight:600 !important;}.request-title{margin-top:0 !important;font-size:1.9rem !important;}p{font-size:1.1rem !important;}label{display:inline-block !important;margin-bottom:.5rem !important;font-size:16px !important;}.nav-tabs>li{font-size:13px;line-height:21px;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background:#4e5d6c;}.nav-tabs>li>a>.fa{padding:3px 5px 3px 3px;}.nav-tabs>li.nav-tab-right{float:right;}.nav-tabs>li.nav-tab-right a{margin-right:0;margin-left:2px;}.nav-tabs>li.nav-tab-icononly .fa{padding:3px;}.navbar .nav a .fa,.dropdown-menu a .fa{font-size:130%;top:1px;position:relative;display:inline-block;margin-right:5px;}.dropdown-menu a .fa{top:2px;}.btn-danger-outline{color:#d9534f !important;background-color:transparent;background-image:none;border-color:#d9534f !important;}.btn-danger-outline:focus,.btn-danger-outline.focus,.btn-danger-outline:active,.btn-danger-outline.active,.btn-danger-outline:hover,.open>.btn-danger-outline.dropdown-toggle{color:#fff !important;background-color:#d9534f !important;border-color:#d9534f !important;}.btn-primary-outline{color:#ff761b !important;background-color:transparent;background-image:none;border-color:#ff761b !important;}.btn-primary-outline:focus,.btn-primary-outline.focus,.btn-primary-outline:active,.btn-primary-outline.active,.btn-primary-outline:hover,.open>.btn-primary-outline.dropdown-toggle{color:#fff !important;background-color:#df691a !important;border-color:#df691a !important;}.btn-info-outline{color:#5bc0de !important;background-color:transparent;background-image:none;border-color:#5bc0de !important;}.btn-info-outline:focus,.btn-info-outline.focus,.btn-info-outline:active,.btn-info-outline.active,.btn-info-outline:hover,.open>.btn-info-outline.dropdown-toggle{color:#fff !important;background-color:#5bc0de !important;border-color:#5bc0de !important;}.btn-warning-outline{color:#f0ad4e !important;background-color:transparent;background-image:none;border-color:#f0ad4e !important;}.btn-warning-outline:focus,.btn-warning-outline.focus,.btn-warning-outline:active,.btn-warning-outline.active,.btn-warning-outline:hover,.open>.btn-warning-outline.dropdown-toggle{color:#fff !important;background-color:#f0ad4e !important;border-color:#f0ad4e !important;}.btn-success-outline{color:#5cb85c !important;background-color:transparent;background-image:none;border-color:#5cb85c !important;}.btn-success-outline:focus,.btn-success-outline.focus,.btn-success-outline:active,.btn-success-outline.active,.btn-success-outline:hover,.open>.btn-success-outline.dropdown-toggle{color:#fff !important;background-color:#5cb85c !important;border-color:#5cb85c !important;}#movieList .mix{display:none;}#tvList .mix{display:none;}.scroll-top-wrapper{position:fixed;opacity:0;visibility:hidden;overflow:hidden;text-align:center;z-index:99999999;background-color:#4e5d6c;color:#eee;width:50px;height:48px;line-height:48px;right:30px;bottom:30px;padding-top:2px;border-top-left-radius:10px;border-top-right-radius:10px;border-bottom-right-radius:10px;border-bottom-left-radius:10px;-webkit-transition:all .5s ease-in-out;-moz-transition:all .5s ease-in-out;-ms-transition:all .5s ease-in-out;-o-transition:all .5s ease-in-out;transition:all .5s ease-in-out;}.scroll-top-wrapper:hover{background-color:#637689;}.scroll-top-wrapper.show{visibility:visible;cursor:pointer;opacity:1;}.scroll-top-wrapper i.fa{line-height:inherit;}.no-search-results{text-align:center;}.no-search-results .no-search-results-icon{font-size:10em;color:#4e5d6c;}.no-search-results .no-search-results-text{margin:20px 0;color:#ccc;}.form-control-search{padding:13px 105px 13px 16px;height:100%;}.form-control-withbuttons{padding-right:105px;}.input-group-addon .btn-group{position:absolute;right:45px;z-index:3;top:10px;box-shadow:0 0 0;}.input-group-addon .btn-group .btn{border:1px solid rgba(255,255,255,.7) !important;padding:3px 12px;color:rgba(255,255,255,.7) !important;}.btn-split .btn{border-radius:0 !important;}.btn-split .btn:not(.dropdown-toggle){border-radius:.25rem 0 0 .25rem !important;}.btn-split .btn.dropdown-toggle{border-radius:0 .25rem .25rem 0 !important;padding:12px 8px;}#updateAvailable{background-color:#df691a;text-align:center;font-size:15px;padding:3px 0;}#cacherRunning{background-color:#4e5d6c;text-align:center;font-size:15px;padding:3px 0;}.checkbox label{display:inline-block;cursor:pointer;position:relative;padding-left:25px;margin-right:15px;font-size:13px;margin-bottom:10px;}.checkbox label:before{content:"";display:inline-block;width:18px;height:18px;margin-right:10px;position:absolute;left:0;bottom:1px;border:2px solid #eee;border-radius:3px;}.checkbox input[type=checkbox]{display:none;}.checkbox input[type=checkbox]:checked+label:before{content:"✓";font-size:13px;color:#fafafa;text-align:center;line-height:13px;}.input-group-sm{padding-top:2px;padding-bottom:2px;}.tab-pane .form-horizontal .form-group{margin-right:15px;margin-left:15px;}.bootstrap-datetimepicker-widget.dropdown-menu{background-color:#4e5d6c;}.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after{border-bottom:6px solid #4e5d6c !important;}.bootstrap-datetimepicker-widget table td.active,.bootstrap-datetimepicker-widget table td.active:hover{color:#fff !important;}.landing-header{display:block;margin:60px auto;}.landing-block{background:#2f2f2f !important;padding:5px;}.landing-block .media{margin:30px auto;max-width:450px;}.landing-block .media .media-left{display:inline-block;float:left;width:70px;}.landing-block .media .media-left i.fa{font-size:3em;}.landing-title{font-weight:bold;}.checkbox-custom{margin-top:0 !important;margin-bottom:0 !important;}.tooltip_templates{display:none;}.shadow{-moz-box-shadow:3px 3px 5px 6px #191919;-webkit-box-shadow:3px 3px 5px 6px #191919;box-shadow:3px 3px 5px 6px #191919;}.img-circle{border-radius:50%;}#wrapper{padding-left:0;-webkit-transition:all .5s ease;-moz-transition:all .5s ease;-o-transition:all .5s ease;transition:all .5s ease;}#wrapper.toggled{padding-right:250px;}#sidebar-wrapper{z-index:1000;position:fixed;right:250px;width:0;height:100%;margin-right:-250px;overflow-y:auto;background:#4e5d6c;padding-left:15px;-webkit-transition:all .5s ease;-moz-transition:all .5s ease;-o-transition:all .5s ease;transition:all .5s ease;}#wrapper.toggled #sidebar-wrapper{width:500px;}#page-content-wrapper{width:100%;position:absolute;padding:15px;}#wrapper.toggled #page-content-wrapper{position:absolute;margin-left:-250px;}.sidebar-nav{position:absolute;top:0;width:500px;margin:0;padding-left:0;list-style:none;}.sidebar-nav li{text-indent:20px;line-height:40px;}.sidebar-nav li a{display:block;text-decoration:none;color:#999;}.sidebar-nav li a:hover{text-decoration:none;color:#fff;background:rgba(255,255,255,.2);}.sidebar-nav li a:active,.sidebar-nav li a:focus{text-decoration:none;}.sidebar-nav>.sidebar-brand{height:65px;font-size:18px;line-height:60px;}.sidebar-nav>.sidebar-brand a{color:#999;}.sidebar-nav>.sidebar-brand a:hover{color:#fff;background:none;}@media(min-width:768px){#wrapper{padding-right:250px;}#wrapper.toggled{padding-right:0;}#sidebar-wrapper{width:500px;}#wrapper.toggled #sidebar-wrapper{width:0;}#page-content-wrapper{padding:20px;position:relative;}#wrapper.toggled #page-content-wrapper{position:relative;margin-right:0;}}#lightbox{background-color:#808080;filter:alpha(opacity=50);opacity:.5;-moz-opacity:.5;top:0;left:0;z-index:20;height:100%;width:100%;background-repeat:no-repeat;background-position:center;position:absolute;} \ No newline at end of file diff --git a/PlexRequests.UI/Content/base.scss b/PlexRequests.UI/Content/base.scss index 64dd1b83e..b1e7abe3e 100644 --- a/PlexRequests.UI/Content/base.scss +++ b/PlexRequests.UI/Content/base.scss @@ -327,6 +327,13 @@ $border-radius: 10px; padding: 3px 0; } +#cacherRunning { + background-color: $form-color; + text-align: center; + font-size: 15px; + padding: 3px 0; +} + .checkbox label { display: inline-block; cursor: pointer; diff --git a/PlexRequests.UI/Helpers/HtmlSecurityHelper.cs b/PlexRequests.UI/Helpers/HtmlSecurityHelper.cs index 3d1751257..ef8d93bc7 100644 --- a/PlexRequests.UI/Helpers/HtmlSecurityHelper.cs +++ b/PlexRequests.UI/Helpers/HtmlSecurityHelper.cs @@ -25,6 +25,8 @@ // ************************************************************************/ #endregion +using Nancy; +using Nancy.Linker; using Nancy.Security; using Nancy.ViewEngines.Razor; using Ninject; @@ -41,22 +43,37 @@ namespace PlexRequests.UI.Helpers get { var userRepo = ServiceLocator.Instance.Resolve(); - return _security ?? (_security = new SecurityExtensions(userRepo, null)); + var linker = ServiceLocator.Instance.Resolve(); + return _security ?? (_security = new SecurityExtensions(userRepo, null, linker)); } } private static SecurityExtensions _security; - public static bool HasAnyPermission(this HtmlHelpers helper, int permission) + public static bool HasAnyPermission(this HtmlHelpers helper, int permission, bool authenticated = true) { - return helper.CurrentUser.IsAuthenticated() - && Security.HasPermissions(helper.CurrentUser, (Permissions) permission); + if (authenticated) + { + return helper.CurrentUser.IsAuthenticated() + && Security.HasPermissions(helper.CurrentUser, (Permissions) permission); + } + return Security.HasPermissions(helper.CurrentUser, (Permissions)permission); } public static bool DoesNotHavePermission(this HtmlHelpers helper, int permission) { return Security.DoesNotHavePermissions(permission, helper.CurrentUser); } + + public static bool IsAdmin(this HtmlHelpers helper, bool isAuthenticated = true) + { + return HasAnyPermission(helper, (int) Permissions.Administrator, isAuthenticated); + } + + public static bool IsLoggedIn(this HtmlHelpers helper, NancyContext context) + { + return Security.IsLoggedIn(context); + } } } \ No newline at end of file diff --git a/PlexRequests.UI/Helpers/SecurityExtensions.cs b/PlexRequests.UI/Helpers/SecurityExtensions.cs index 36f01a1a5..445aadc9c 100644 --- a/PlexRequests.UI/Helpers/SecurityExtensions.cs +++ b/PlexRequests.UI/Helpers/SecurityExtensions.cs @@ -30,6 +30,8 @@ using System.Collections.Generic; using System.Linq; using Nancy; using Nancy.Extensions; +using Nancy.Linker; +using Nancy.Responses; using Nancy.Security; using Ninject; using PlexRequests.Helpers.Permissions; @@ -40,14 +42,16 @@ namespace PlexRequests.UI.Helpers { public class SecurityExtensions { - public SecurityExtensions(IUserRepository userRepository, NancyModule context) + public SecurityExtensions(IUserRepository userRepository, NancyModule context, IResourceLinker linker) { UserRepository = userRepository; Module = context; + Linker = linker; } private IUserRepository UserRepository { get; } private NancyModule Module { get; } + private IResourceLinker Linker { get; } public bool IsLoggedIn(NancyContext context) { @@ -117,7 +121,7 @@ namespace PlexRequests.UI.Helpers if (dbUser == null) return false; var permissions = (Permissions)dbUser.Permissions; - var result = permissions.HasFlag((Permissions)perm); + var result = permissions.HasFlag(perm); return !result; } @@ -134,10 +138,11 @@ namespace PlexRequests.UI.Helpers return result; } - public void HasPermissionsResponse(Permissions perm) + public Response HasPermissionsRedirect(Permissions perm, NancyContext context, string routeName, HttpStatusCode code) { - Module.AddBeforeHookOrExecute( - ForbiddenIfNot(ctx => + var url = Linker.BuildRelativeUri(context, routeName); + + var response = ForbiddenIfNot(ctx => { if (ctx.CurrentUser == null) return false; @@ -145,13 +150,24 @@ namespace PlexRequests.UI.Helpers if (dbUser == null) return false; - var permissions = (Permissions)dbUser.Permissions; + var permissions = (Permissions) dbUser.Permissions; var result = permissions.HasFlag(perm); return result; - }), "Requires Claims"); + }); + + var r = response(context); + return r.StatusCode == code + ? new RedirectResponse(url.ToString()) + : null; } + public Response AdminLoginRedirect(Permissions perm, NancyContext context) + { + // This will redirect us to the Login Page if we don't have the correct permission passed in (e.g. Admin with Http 403 status code). + return HasPermissionsRedirect(perm, context, "LocalLogin", HttpStatusCode.Forbidden); + } + // BELOW IS A COPY FROM THE SecurityHooks CLASS! /// diff --git a/PlexRequests.UI/Modules/Admin/AdminModule.cs b/PlexRequests.UI/Modules/Admin/AdminModule.cs index 1357d3e7d..f621d99cf 100644 --- a/PlexRequests.UI/Modules/Admin/AdminModule.cs +++ b/PlexRequests.UI/Modules/Admin/AdminModule.cs @@ -66,6 +66,7 @@ using PlexRequests.UI.Helpers; using PlexRequests.UI.Models; using Quartz; using Action = PlexRequests.Helpers.Analytics.Action; +using HttpStatusCode = Nancy.HttpStatusCode; namespace PlexRequests.UI.Modules { @@ -154,8 +155,8 @@ namespace PlexRequests.UI.Modules NotifySettings = notifyService; RecentlyAdded = recentlyAdded; - Security.HasPermissionsResponse(Permissions.Administrator); - + Before += (ctx) => Security.AdminLoginRedirect(Permissions.Administrator, ctx); + Get["/"] = _ => Admin(); Get["/authentication", true] = async (x, ct) => await Authentication(); @@ -826,13 +827,11 @@ namespace PlexRequests.UI.Modules return Response.AsJson(result ? new JsonResponseModel { Result = true, Message = "Successfully Updated the Settings for Newsletter!" } : new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." }); - } + } private Response CreateApiKey() { - Security.HasPermissionsResponse(Permissions.Administrator); - Analytics.TrackEventAsync(Category.Admin, Action.Create, "Created API Key", Username, CookieHelper.GetAnalyticClientId(Cookies)); var apiKey = Guid.NewGuid().ToString("N"); var settings = PrService.GetSettings(); @@ -978,11 +977,11 @@ namespace PlexRequests.UI.Modules if (!isValid) { return Response.AsJson(new JsonResponseModel - { - Result = false, - Message = + { + Result = false, + Message = $"CRON {settings.RecentlyAddedCron} is not valid. Please ensure you are using a valid CRON." - }); + }); } } var result = await ScheduledJobSettings.SaveSettingsAsync(settings); diff --git a/PlexRequests.UI/Modules/Admin/FaultQueueModule.cs b/PlexRequests.UI/Modules/Admin/FaultQueueModule.cs index 355e823e2..b65238b23 100644 --- a/PlexRequests.UI/Modules/Admin/FaultQueueModule.cs +++ b/PlexRequests.UI/Modules/Admin/FaultQueueModule.cs @@ -46,7 +46,7 @@ namespace PlexRequests.UI.Modules.Admin Cache = cache; RequestQueue = requestQueue; - Security.HasPermissionsResponse(Permissions.Administrator); + Before += (ctx) => Security.AdminLoginRedirect(Permissions.Administrator, ctx); Get["Index", "/faultqueue"] = x => Index(); } diff --git a/PlexRequests.UI/Modules/Admin/SystemStatusModule.cs b/PlexRequests.UI/Modules/Admin/SystemStatusModule.cs index 05ae0f125..7c5c793c5 100644 --- a/PlexRequests.UI/Modules/Admin/SystemStatusModule.cs +++ b/PlexRequests.UI/Modules/Admin/SystemStatusModule.cs @@ -49,8 +49,8 @@ namespace PlexRequests.UI.Modules.Admin Cache = cache; SystemSettings = ss; - Security.HasPermissionsResponse(Permissions.Administrator); - + Before += (ctx) => Security.AdminLoginRedirect(Permissions.Administrator, ctx); + Get["/status", true] = async (x, ct) => await Status(); Post["/save", true] = async (x, ct) => await Save(); diff --git a/PlexRequests.UI/Modules/BaseModule.cs b/PlexRequests.UI/Modules/BaseModule.cs index 35bd4817f..2c2fb7467 100644 --- a/PlexRequests.UI/Modules/BaseModule.cs +++ b/PlexRequests.UI/Modules/BaseModule.cs @@ -30,6 +30,7 @@ using System.Linq; using System.Threading; using Nancy; +using Nancy.Linker; using Nancy.Security; using Ninject; using PlexRequests.Core; @@ -151,14 +152,14 @@ namespace PlexRequests.UI.Modules get { var userRepo = ServiceLocator.Instance.Resolve(); - return _security ?? (_security = new SecurityExtensions(userRepo, this)); + var linker = ServiceLocator.Instance.Resolve(); + return _security ?? (_security = new SecurityExtensions(userRepo, this, linker)); } } private SecurityExtensions _security; - protected bool LoggedIn => Context?.CurrentUser != null; protected string Culture { get; set; } diff --git a/PlexRequests.UI/Modules/UpdateCheckerModule.cs b/PlexRequests.UI/Modules/LayoutModule.cs similarity index 65% rename from PlexRequests.UI/Modules/UpdateCheckerModule.cs rename to PlexRequests.UI/Modules/LayoutModule.cs index 3aa6a657d..03b3a95c6 100644 --- a/PlexRequests.UI/Modules/UpdateCheckerModule.cs +++ b/PlexRequests.UI/Modules/LayoutModule.cs @@ -1,7 +1,7 @@ #region Copyright // /************************************************************************ // Copyright (c) 2016 Jamie Rees -// File: UpdateCheckerModule.cs +// File: LayoutModule.cs // Created By: Jamie Rees // // Permission is hereby granted, free of charge, to any person obtaining @@ -25,6 +25,7 @@ // ************************************************************************/ #endregion using System; +using System.Linq; using System.Threading.Tasks; using Nancy; @@ -35,24 +36,29 @@ using PlexRequests.Core; using PlexRequests.Core.SettingModels; using PlexRequests.Core.StatusChecker; using PlexRequests.Helpers; +using PlexRequests.Services.Interfaces; +using PlexRequests.Services.Jobs; using PlexRequests.UI.Models; namespace PlexRequests.UI.Modules { - public class UpdateCheckerModule : BaseAuthModule + public class LayoutModule : BaseAuthModule { - public UpdateCheckerModule(ICacheProvider provider, ISettingsService pr, ISettingsService settings) : base("updatechecker", pr) + public LayoutModule(ICacheProvider provider, ISettingsService pr, ISettingsService settings, IJobRecord rec) : base("layout", pr) { Cache = provider; SystemSettings = settings; + Job = rec; Get["/", true] = async (x,ct) => await CheckLatestVersion(); + Get["/cacher", true] = async (x,ct) => await CacherRunning(); } private ICacheProvider Cache { get; } private static Logger Log = LogManager.GetCurrentClassLogger(); private ISettingsService SystemSettings { get; } + private IJobRecord Job { get; } private async Task CheckLatestVersion() { @@ -79,5 +85,35 @@ namespace PlexRequests.UI.Modules return Response.AsJson(new JsonUpdateAvailableModel { UpdateAvailable = false }); } } + + private async Task CacherRunning() + { + try + { + var jobs = await Job.GetJobsAsync(); + + // Check to see if any are running + var runningJobs = jobs.Where(x => x.Running); + + // We only want the cachers + var cacherJobs = runningJobs.Where(x => + x.Name.Equals(JobNames.CpCacher) + || x.Name.Equals(JobNames.EpisodeCacher) + || x.Name.Equals(JobNames.PlexChecker) + || x.Name.Equals(JobNames.SonarrCacher) + || x.Name.Equals(JobNames.SrCacher)); + + + return Response.AsJson(cacherJobs.Any() + ? new { CurrentlyRunning = true, IsAdmin} + : new { CurrentlyRunning = false, IsAdmin }); + } + catch (Exception e) + { + Log.Warn("Exception Thrown when attempting to check the status"); + Log.Warn(e); + return Response.AsJson(new { CurrentlyRunning = false, IsAdmin }); + } + } } } \ No newline at end of file diff --git a/PlexRequests.UI/Modules/LoginModule.cs b/PlexRequests.UI/Modules/LoginModule.cs index e6030207f..37ab086a3 100644 --- a/PlexRequests.UI/Modules/LoginModule.cs +++ b/PlexRequests.UI/Modules/LoginModule.cs @@ -52,7 +52,7 @@ namespace PlexRequests.UI.Modules : base(pr) { UserMapper = m; - Get["/login"] = _ => + Get["LocalLogin","/login"] = _ => { if (LoggedIn) { diff --git a/PlexRequests.UI/Modules/UserLoginModule.cs b/PlexRequests.UI/Modules/UserLoginModule.cs index d4e55182c..f377cf2f3 100644 --- a/PlexRequests.UI/Modules/UserLoginModule.cs +++ b/PlexRequests.UI/Modules/UserLoginModule.cs @@ -99,7 +99,7 @@ namespace PlexRequests.UI.Modules { Session["TempMessage"] = Resources.UI.UserLogin_IncorrectUserPass; var uri = Linker.BuildRelativeUri(Context, "UserLoginIndex"); - return Response.AsRedirect(uri.ToString()); // TODO Check this + return Response.AsRedirect(uri.ToString()); } var authenticated = false; @@ -112,7 +112,7 @@ namespace PlexRequests.UI.Modules Log.Debug("User is in denied list, not allowing them to authenticate"); Session["TempMessage"] = Resources.UI.UserLogin_IncorrectUserPass; var uri = Linker.BuildRelativeUri(Context, "UserLoginIndex"); - return Response.AsRedirect(uri.ToString()); // TODO Check this + return Response.AsRedirect(uri.ToString()); } var password = string.Empty; @@ -178,7 +178,7 @@ namespace PlexRequests.UI.Modules { var uri = Linker.BuildRelativeUri(Context, "UserLoginIndex"); Session["TempMessage"] = Resources.UI.UserLogin_IncorrectUserPass; - return Response.AsRedirect(uri.ToString()); // TODO Check this + return Response.AsRedirect(uri.ToString()); } var landingSettings = await LandingPageSettings.GetSettingsAsync(); @@ -192,7 +192,7 @@ namespace PlexRequests.UI.Modules } } var retVal = Linker.BuildRelativeUri(Context, "SearchIndex"); - return Response.AsRedirect(retVal.ToString()); // TODO Check this + return Response.AsRedirect(retVal.ToString()); } diff --git a/PlexRequests.UI/PlexRequests.UI.csproj b/PlexRequests.UI/PlexRequests.UI.csproj index 82f401655..04f4a0cc6 100644 --- a/PlexRequests.UI/PlexRequests.UI.csproj +++ b/PlexRequests.UI/PlexRequests.UI.csproj @@ -258,7 +258,7 @@ - + @@ -747,6 +747,7 @@ PublicResXFileCodeGenerator UI1.Designer.cs + Designer diff --git a/PlexRequests.UI/Resources/UI.da.resx b/PlexRequests.UI/Resources/UI.da.resx index 14e0154ac..5937e76be 100644 --- a/PlexRequests.UI/Resources/UI.da.resx +++ b/PlexRequests.UI/Resources/UI.da.resx @@ -459,4 +459,34 @@ Der er ingen oplysninger om udgivelsesdatoen + + Udsigt I Plex + + + Doner til Bibliotek vedligeholder + + + Tilgængelig på + + + Movie status + + + Ikke anmodet endnu + + + Afventer godkendelse + + + Behandler forespørgsel + + + Anmodning afvist! + + + Tv-show status! + + + En baggrund proces kører i øjeblikket, så der kan være nogle uventede problemer. Dette bør ikke tage for lang tid. + \ No newline at end of file diff --git a/PlexRequests.UI/Resources/UI.de.resx b/PlexRequests.UI/Resources/UI.de.resx index 039e49e46..fa6f71547 100644 --- a/PlexRequests.UI/Resources/UI.de.resx +++ b/PlexRequests.UI/Resources/UI.de.resx @@ -459,4 +459,34 @@ Es gibt noch keine Informationen für diesen Release-Termin - + + Ansicht In Plex + + + Spenden zur Bibliothek Maintainer + + + Verfügbar auf Plex + + + Film-Status! + + + Noch nicht heraus! + + + Genehmigung ausstehend + + + Die Verarbeitung Anfrage + + + Anfrage verweigert. + + + TV-Show-Status + + + Ein Hintergrundprozess gerade läuft, so könnte es einige unerwartete Verhalten sein. Dies sollte nicht zu lange dauern. + + \ No newline at end of file diff --git a/PlexRequests.UI/Resources/UI.es.resx b/PlexRequests.UI/Resources/UI.es.resx index a1706e45d..e7908b3f5 100644 --- a/PlexRequests.UI/Resources/UI.es.resx +++ b/PlexRequests.UI/Resources/UI.es.resx @@ -459,4 +459,31 @@ No hay información disponible para la fecha de lanzamiento + + Donar a la biblioteca Mantenedor + + + Disponible en Plex + + + estado de película + + + No solicitado + + + PENDIENTES DE APROBACIÓN + + + solicitud de procesamiento + + + Solicitud denegada + + + estado de programa de televisión + + + Un proceso en segundo plano se está ejecutando actualmente, por lo que podría ser un comportamiento inesperado. Esto no debería tomar mucho tiempo. + \ No newline at end of file diff --git a/PlexRequests.UI/Resources/UI.fr.resx b/PlexRequests.UI/Resources/UI.fr.resx index e4a54012d..5767fb4d2 100644 --- a/PlexRequests.UI/Resources/UI.fr.resx +++ b/PlexRequests.UI/Resources/UI.fr.resx @@ -459,4 +459,34 @@ Il n'y a pas d'information disponible pour la date de sortie + + Voir Dans Plex + + + Faire un don à la bibliothèque mainteneur! + + + Disponible sur Plex + + + le statut de film + + + Pas encore demandé + + + En attente de validation + + + Traitement de la demande + + + Requête refusée + + + TV show status + + + Un processus d'arrière-plan est en cours d'exécution, de sorte qu'il pourrait y avoir des comportements inattendus. Cela ne devrait pas prendre trop de temps. + \ No newline at end of file diff --git a/PlexRequests.UI/Resources/UI.it.resx b/PlexRequests.UI/Resources/UI.it.resx index fe73d4586..febcf378a 100644 --- a/PlexRequests.UI/Resources/UI.it.resx +++ b/PlexRequests.UI/Resources/UI.it.resx @@ -459,4 +459,34 @@ Non ci sono informazioni disponibili per la data di uscita + + Guarda In Plex + + + Donare alla libreria Maintainer + + + Disponibile su Plex + + + lo stato di film + + + Non ancora richiesto + + + Approvazione in sospeso + + + Elaborazione richiesta + + + Richiesta negata + + + Show televisivo di stato + + + Un processo in background è in esecuzione, quindi ci potrebbero essere alcuni comportamenti imprevisti. Questo non dovrebbe richiedere troppo tempo. + \ No newline at end of file diff --git a/PlexRequests.UI/Resources/UI.nl.resx b/PlexRequests.UI/Resources/UI.nl.resx index 9ddc8688e..e64511a14 100644 --- a/PlexRequests.UI/Resources/UI.nl.resx +++ b/PlexRequests.UI/Resources/UI.nl.resx @@ -459,4 +459,34 @@ Er is geen informatie beschikbaar voor de release datum + + Bekijk In Plex + + + Doneer aan Library Maintainer + + + Beschikbaar vanaf + + + Movie-status! + + + Nog niet gevraagd + + + Wacht op goedkeuring + + + Verwerking verzoek... + + + Aanvraag afgewezen + + + TV-show-status + + + Een achtergrond taak is momenteel actief, dus er misschien een onverwachte gedrag. Dit moet niet te lang duren. + \ No newline at end of file diff --git a/PlexRequests.UI/Resources/UI.pt.resx b/PlexRequests.UI/Resources/UI.pt.resx index 712b779c4..efedc0856 100644 --- a/PlexRequests.UI/Resources/UI.pt.resx +++ b/PlexRequests.UI/Resources/UI.pt.resx @@ -459,4 +459,31 @@ Não há informações disponíveis para a data de lançamento + + Ver Em Plex + + + Doações para Biblioteca Mantenedor + + + Disponível em Plex + + + status de filme + + + Não solicitada ainda + + + B – P/ Aprovação + + + Processando solicitação... + + + Solicitação negada. + + + Programa de TV status + \ No newline at end of file diff --git a/PlexRequests.UI/Resources/UI.resx b/PlexRequests.UI/Resources/UI.resx index ced499673..ca792000b 100644 --- a/PlexRequests.UI/Resources/UI.resx +++ b/PlexRequests.UI/Resources/UI.resx @@ -467,4 +467,7 @@ TV show status + + A background process is currently running, so there might be some unexpected behavior. This shouldn't take too long. + \ No newline at end of file diff --git a/PlexRequests.UI/Resources/UI.sv.resx b/PlexRequests.UI/Resources/UI.sv.resx index 8b3708153..924f2cc70 100644 --- a/PlexRequests.UI/Resources/UI.sv.resx +++ b/PlexRequests.UI/Resources/UI.sv.resx @@ -459,4 +459,34 @@ Det finns ingen information tillgänglig för release datum + + Vy I Plex + + + Donera till bibliotek Ansvarig + + + Tillgänglig den + + + Film status + + + Inte Begärd ännu + + + Väntar på godkännande + + + Bearbetning förfrågan + + + Förfrågan nekad + + + Visa status + + + En bakgrundsprocess är igång, så det kan finnas några oväntade beteende. Detta bör inte ta alltför lång tid. + \ No newline at end of file diff --git a/PlexRequests.UI/Resources/UI1.Designer.cs b/PlexRequests.UI/Resources/UI1.Designer.cs index 548dafff4..0a57c2412 100644 --- a/PlexRequests.UI/Resources/UI1.Designer.cs +++ b/PlexRequests.UI/Resources/UI1.Designer.cs @@ -222,6 +222,15 @@ namespace PlexRequests.UI.Resources { } } + /// + /// Looks up a localized string similar to A background process is currently running, so there might be some unexpected behavior. This shouldn't take too long.. + /// + public static string Layout_CacherRunning { + get { + return ResourceManager.GetString("Layout_CacherRunning", resourceCulture); + } + } + /// /// Looks up a localized string similar to Change Password. /// diff --git a/PlexRequests.UI/Validators/PlexRequestsValidator.cs b/PlexRequests.UI/Validators/PlexRequestsValidator.cs index 7d61eba2c..481364fd9 100644 --- a/PlexRequests.UI/Validators/PlexRequestsValidator.cs +++ b/PlexRequests.UI/Validators/PlexRequestsValidator.cs @@ -44,7 +44,7 @@ namespace PlexRequests.UI.Validators RuleFor(x => x.BaseUrl).NotEqual("login", StringComparer.CurrentCultureIgnoreCase).WithMessage("You cannot use 'login' as this is reserved by the application."); RuleFor(x => x.BaseUrl).NotEqual("test", StringComparer.CurrentCultureIgnoreCase).WithMessage("You cannot use 'test' as this is reserved by the application."); RuleFor(x => x.BaseUrl).NotEqual("approval", StringComparer.CurrentCultureIgnoreCase).WithMessage("You cannot use 'approval' as this is reserved by the application."); - RuleFor(x => x.BaseUrl).NotEqual("updatechecker", StringComparer.CurrentCultureIgnoreCase).WithMessage("You cannot use 'updatechecker' as this is reserved by the application."); + RuleFor(x => x.BaseUrl).NotEqual("layout", StringComparer.CurrentCultureIgnoreCase).WithMessage("You cannot use 'layout' as this is reserved by the application."); RuleFor(x => x.BaseUrl).NotEqual("usermanagement", StringComparer.CurrentCultureIgnoreCase).WithMessage("You cannot use 'usermanagement' as this is reserved by the application."); RuleFor(x => x.BaseUrl).NotEqual("api", StringComparer.CurrentCultureIgnoreCase).WithMessage("You cannot use 'api' as this is reserved by the application."); RuleFor(x => x.BaseUrl).NotEqual("landing", StringComparer.CurrentCultureIgnoreCase).WithMessage("You cannot use 'landing' as this is reserved by the application."); diff --git a/PlexRequests.UI/Views/Shared/Partial/_LayoutScripts.cshtml b/PlexRequests.UI/Views/Shared/Partial/_LayoutScripts.cshtml index 808fc7a91..62e946479 100644 --- a/PlexRequests.UI/Views/Shared/Partial/_LayoutScripts.cshtml +++ b/PlexRequests.UI/Views/Shared/Partial/_LayoutScripts.cshtml @@ -10,7 +10,7 @@ $(function () { // Check for update - var url = createBaseUrl(urlBase, '/updatechecker'); + var url = createBaseUrl(urlBase, '/layout'); $.ajax({ type: "GET", url: url, @@ -20,7 +20,7 @@ var status = createBaseUrl(urlBase, '/admin/status'); $('#updateAvailable').html(" @UI.Layout_UpdateAvailablePart1 @UI.Layout_UpdateAvailablePart2"); $('#updateAvailable').removeAttr("hidden"); - $('body').addClass('update-available'); + //$('body').addClass('update-available'); } }, error: function (e) { @@ -29,6 +29,7 @@ }); // End Check for update + checkCacheInProgress(); // Scroller $(document).on('scroll', function () { @@ -43,9 +44,6 @@ $('.scroll-top-wrapper').on('click', scrollToTop); // End Scroller - - - // Get Issue count var issueUrl = createBaseUrl(urlBase, '/issues/issuecount'); $.ajax({ @@ -66,6 +64,8 @@ // End issue count + + $('#donate').click(function () { ga('send', 'event', 'Navbar', 'Donate', 'Donate Clicked'); }); @@ -80,4 +80,23 @@ offsetTop = offset.top; $('html, body').animate({ scrollTop: offsetTop }, 500, 'linear'); } + + function checkCacheInProgress() { + + var url = createBaseUrl(urlBase, '/layout/cacher'); + $.ajax({ + type: "GET", + url: url, + dataType: "json", + success: function (response) { + if (response.currentlyRunning) { + $('#cacherRunning').html("@UI.Layout_CacherRunning"); + $('#cacherRunning').removeAttr("hidden"); + } + }, + error: function (e) { + console.log(e); + } + }); + } \ No newline at end of file diff --git a/PlexRequests.UI/Views/Shared/Partial/_Navbar.cshtml b/PlexRequests.UI/Views/Shared/Partial/_Navbar.cshtml index 2dba39e5f..e4d75cc75 100644 --- a/PlexRequests.UI/Views/Shared/Partial/_Navbar.cshtml +++ b/PlexRequests.UI/Views/Shared/Partial/_Navbar.cshtml @@ -35,21 +35,21 @@ @Html.GetNavbarUrl(Context, "/search", UI.Layout_Search, "search") @Html.GetNavbarUrl(Context, "/requests", UI.Layout_Requests, "plus-circle") @Html.GetNavbarUrl(Context, "/issues", UI.Layout_Issues, "exclamation", "") - @if (Context.CurrentUser.IsAuthenticated()) // TODO replace with IsAdmin + @*@if (Context.CurrentUser.IsAuthenticated()) // TODO replace with IsAdmin*@ + @if (Html.IsAdmin()) {
  • } } - @if (Context.Request.Session[SessionKeys.UsernameKey] != null && !Context.CurrentUser.IsAuthenticated()) + @*@if (Context.Request.Session[SessionKeys.UsernameKey] != null && !Context.CurrentUser.IsAuthenticated())*@ + else if (Html.IsLoggedIn(Context)) // Logged in but not admin {
  • + + +
  • @@ -104,16 +105,17 @@ var donateLink = $("#customDonateHref"); var donationText = $("#donationText"); donateLink.attr("href", result.url); - if(result.message) { + if (result.message) { donationText.text(result.message); } } }, - error: function(xhr, status, error) { + error: function (xhr, status, error) { console.log("error " + error); $("#customDonate").hide(); } }); + \ No newline at end of file diff --git a/PlexRequests.UI/Views/Shared/_Layout.cshtml b/PlexRequests.UI/Views/Shared/_Layout.cshtml index 86eae181c..fcd6f8d37 100644 --- a/PlexRequests.UI/Views/Shared/_Layout.cshtml +++ b/PlexRequests.UI/Views/Shared/_Layout.cshtml @@ -16,8 +16,6 @@ - - @*@MiniProfiler.RenderIncludes()*@ @Html.GetInformationalVersion() From c064bc6d44f3a57101a06b5648efdf1657e4d4b4 Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Tue, 15 Nov 2016 15:01:38 +0000 Subject: [PATCH 23/87] Finishing off the user management page #218 #359 #195 --- .../Migrations/Version1100.cs | 3 + .../Permissions/Permissions.cs | 10 + .../Jobs/PlexAvailabilityChecker.cs | 4 +- PlexRequests.Store/Models/PlexUsers.cs | 8 +- PlexRequests.Store/SqlTables.sql | 6 +- .../userManagementController.js | 36 ++-- .../userManagement/userManagementService.js | 4 +- .../UserManagementUsersViewModel.cs | 2 + .../Modules/UserManagementModule.cs | 174 ++++++++++++++---- PlexRequests.UI/Resources/UI.resx | 3 + PlexRequests.UI/Resources/UI1.Designer.cs | 9 + .../Views/Shared/Partial/_Navbar.cshtml | 4 + .../Views/UserManagement/Index.cshtml | 21 ++- 13 files changed, 219 insertions(+), 65 deletions(-) diff --git a/PlexRequests.Core.Migration/Migrations/Version1100.cs b/PlexRequests.Core.Migration/Migrations/Version1100.cs index d4365f357..525114ebb 100644 --- a/PlexRequests.Core.Migration/Migrations/Version1100.cs +++ b/PlexRequests.Core.Migration/Migrations/Version1100.cs @@ -76,6 +76,9 @@ namespace PlexRequests.Core.Migration.Migrations con.AlterTable("Users", "ADD", "Permissions", true, "INTEGER"); con.AlterTable("Users", "ADD", "Features", true, "INTEGER"); + con.AlterTable("PlexUsers", "ADD", "Permissions", true, "INTEGER"); + con.AlterTable("PlexUsers", "ADD", "Features", true, "INTEGER"); + //https://image.tmdb.org/t/p/w150/https://image.tmdb.org/t/p/w150//aqhAqttDq7zgsTaBHtCD8wmTk6k.jpg // UI = https://image.tmdb.org/t/p/w150/{{posterPath}} diff --git a/PlexRequests.Helpers/Permissions/Permissions.cs b/PlexRequests.Helpers/Permissions/Permissions.cs index b0c28db36..8b1d22954 100644 --- a/PlexRequests.Helpers/Permissions/Permissions.cs +++ b/PlexRequests.Helpers/Permissions/Permissions.cs @@ -50,5 +50,15 @@ namespace PlexRequests.Helpers.Permissions [Display(Name = "Read Only User")] ReadOnlyUser = 32, + + [Display(Name = "Auto Approve Movie Requests")] + AutoApproveMovie = 64, + + [Display(Name = "Auto Approve TV Show Requests")] + AutoApproveTv = 128, + + [Display(Name = "Auto Approve Album Requests")] + AutoApproveAlbum = 256 + } } \ No newline at end of file diff --git a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs index 8ecb00bb5..365a7efa0 100644 --- a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs +++ b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs @@ -471,10 +471,10 @@ namespace PlexRequests.Services.Jobs { var sections = PlexApi.GetLibrarySections(plexSettings.PlexAuthToken, plexSettings.FullUri); - List libs = new List(); + var libs = new List(); if (sections != null) { - foreach (var dir in sections.Directories) + foreach (var dir in sections.Directories ?? new List()) { var lib = PlexApi.GetLibrary(plexSettings.PlexAuthToken, plexSettings.FullUri, dir.Key); if (lib != null) diff --git a/PlexRequests.Store/Models/PlexUsers.cs b/PlexRequests.Store/Models/PlexUsers.cs index 884fe1d4d..c860b9551 100644 --- a/PlexRequests.Store/Models/PlexUsers.cs +++ b/PlexRequests.Store/Models/PlexUsers.cs @@ -24,11 +24,17 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // ************************************************************************/ #endregion + +using Dapper.Contrib.Extensions; + namespace PlexRequests.Store.Models { + [Table(nameof(PlexUsers))] public class PlexUsers : Entity { - public int PlexUserId { get; set; } + public string PlexUserId { get; set; } public string UserAlias { get; set; } + public int Permissions { get; set; } + public int Features { get; set; } } } \ No newline at end of file diff --git a/PlexRequests.Store/SqlTables.sql b/PlexRequests.Store/SqlTables.sql index 4d180b1d0..f4a512346 100644 --- a/PlexRequests.Store/SqlTables.sql +++ b/PlexRequests.Store/SqlTables.sql @@ -114,8 +114,10 @@ CREATE UNIQUE INDEX IF NOT EXISTS RequestLimit_Id ON RequestLimit (Id); CREATE TABLE IF NOT EXISTS PlexUsers ( Id INTEGER PRIMARY KEY AUTOINCREMENT, - PlexUserId INTEGER NOT NULL, - UserAlias varchar(100) NOT NULL + PlexUserId varchar(100) NOT NULL, + UserAlias varchar(100) NOT NULL, + Permissions INTEGER, + Features INTEGER ); CREATE UNIQUE INDEX IF NOT EXISTS PlexUsers_Id ON PlexUsers (Id); diff --git a/PlexRequests.UI/Content/app/userManagement/userManagementController.js b/PlexRequests.UI/Content/app/userManagement/userManagementController.js index 3b67832aa..814df40d8 100644 --- a/PlexRequests.UI/Content/app/userManagement/userManagementController.js +++ b/PlexRequests.UI/Content/app/userManagement/userManagementController.js @@ -120,29 +120,30 @@ $scope.updateUser = function () { var u = $scope.selectedUser; - userManagementService.updateUser(u.id, u.permissions, u.alias, u.emailAddress) - .then(function (data) { - if (data) { - $scope.selectedUser = data; + userManagementService.updateUser(u.id, u.permissions, u.features, u.alias, u.emailAddress) + .then(function success(data) { + if (data.data) { + $scope.selectedUser = data.data; - if (open) { - open = false; - $("#wrapper").toggleClass("toggled"); + closeSidebar(); + return successCallback("Updated User", "success"); } - return successCallback("Updated User", "success"); - } - }); + }, function errorCallback(response) { + successCallback(response, "danger"); + }); } $scope.deleteUser = function () { var u = $scope.selectedUser; - var result = userManagementService.deleteUser(u.id); - - result.success(function (data) { - if (data.result) { + userManagementService.deleteUser(u.id) + .then(function sucess(data) { + if (data.data.result) { removeUser(u.id, true); + closeSidebar(); return successCallback("Deleted User", "success"); } + }, function errorCallback(response) { + successCallback(response, "danger"); }); } @@ -170,6 +171,13 @@ $scope.selectedUser = null; } } + + function closeSidebar() { + if (open) { + open = false; + $("#wrapper").toggleClass("toggled"); + } + } } function successCallback(message, type) { diff --git a/PlexRequests.UI/Content/app/userManagement/userManagementService.js b/PlexRequests.UI/Content/app/userManagement/userManagementService.js index 72cd7f375..59dfe3571 100644 --- a/PlexRequests.UI/Content/app/userManagement/userManagementService.js +++ b/PlexRequests.UI/Content/app/userManagement/userManagementService.js @@ -28,11 +28,11 @@ return $http.get('/usermanagement/permissions'); } - var updateUser = function (id, permissions, alias, email) { + var updateUser = function (id, permissions, features, alias, email) { return $http({ url: '/usermanagement/updateUser', method: "POST", - data: { id: id, permissions: permissions, alias: alias, emailAddress: email } + data: { id: id, permissions: permissions, features: features, alias: alias, emailAddress: email }, }); } diff --git a/PlexRequests.UI/Models/UserManagement/UserManagementUsersViewModel.cs b/PlexRequests.UI/Models/UserManagement/UserManagementUsersViewModel.cs index aad003c05..cdc78a07a 100644 --- a/PlexRequests.UI/Models/UserManagement/UserManagementUsersViewModel.cs +++ b/PlexRequests.UI/Models/UserManagement/UserManagementUsersViewModel.cs @@ -75,6 +75,8 @@ namespace PlexRequests.UI.Models public string Id { get; set; } [JsonProperty("permissions")] public List Permissions { get; set; } + [JsonProperty("features")] + public List Features { get; set; } public string Alias { get; set; } public string EmailAddress { get; set; } } diff --git a/PlexRequests.UI/Modules/UserManagementModule.cs b/PlexRequests.UI/Modules/UserManagementModule.cs index ee9558d1f..9870d3533 100644 --- a/PlexRequests.UI/Modules/UserManagementModule.cs +++ b/PlexRequests.UI/Modules/UserManagementModule.cs @@ -6,15 +6,16 @@ using System.Threading.Tasks; using Nancy; using Nancy.Extensions; using Nancy.Responses.Negotiation; -using Nancy.Security; using Newtonsoft.Json; using PlexRequests.Api.Interfaces; +using PlexRequests.Api.Models.Plex; using PlexRequests.Core; using PlexRequests.Core.Models; using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; using PlexRequests.Helpers.Permissions; using PlexRequests.Store; +using PlexRequests.Store.Models; using PlexRequests.Store.Repository; using PlexRequests.UI.Models; @@ -22,15 +23,16 @@ namespace PlexRequests.UI.Modules { public class UserManagementModule : BaseModule { - public UserManagementModule(ISettingsService pr, ICustomUserMapper m, IPlexApi plexApi, ISettingsService plex, IRepository userLogins) : base("usermanagement", pr) + public UserManagementModule(ISettingsService pr, ICustomUserMapper m, IPlexApi plexApi, ISettingsService plex, IRepository userLogins, IRepository plexRepo) : base("usermanagement", pr) { #if !DEBUG - this.RequiresAnyClaim(UserClaims.Admin); + Before += (ctx) => Security.AdminLoginRedirect(Permissions.Administrator, ctx); #endif UserMapper = m; PlexApi = plexApi; PlexSettings = plex; UserLoginsRepo = userLogins; + PlexUsersRepository = plexRepo; Get["/"] = x => Load(); @@ -40,7 +42,7 @@ namespace PlexRequests.UI.Modules Get["/plex/{id}", true] = async (x, ct) => await PlexDetails(x.id); Get["/permissions"] = x => GetEnum(); Get["/features"] = x => GetEnum(); - Post["/updateuser"] = x => UpdateUser(); + Post["/updateuser", true] = async (x, ct) => await UpdateUser(); Post["/deleteuser"] = x => DeleteUser(); } @@ -48,6 +50,7 @@ namespace PlexRequests.UI.Modules private IPlexApi PlexApi { get; } private ISettingsService PlexSettings { get; } private IRepository UserLoginsRepo { get; } + private IRepository PlexUsersRepository { get; } private Negotiator Load() { @@ -57,13 +60,14 @@ namespace PlexRequests.UI.Modules private async Task LoadUsers() { var localUsers = await UserMapper.GetUsersAsync(); + var plexDbUsers = await PlexUsersRepository.GetAllAsync(); var model = new List(); - var usersDb = UserLoginsRepo.GetAll().ToList(); + var userLogins = UserLoginsRepo.GetAll().ToList(); foreach (var user in localUsers) { - var userDb = usersDb.FirstOrDefault(x => x.UserId == user.UserGuid); + var userDb = userLogins.FirstOrDefault(x => x.UserId == user.UserGuid); model.Add(MapLocalUser(user, userDb?.LastLoggedIn ?? DateTime.MinValue)); } @@ -75,20 +79,18 @@ namespace PlexRequests.UI.Modules foreach (var u in plexUsers.User) { - var userDb = usersDb.FirstOrDefault(x => x.UserId == u.Id); - model.Add(new UserManagementUsersViewModel + var dbUser = plexDbUsers.FirstOrDefault(x => x.PlexUserId == u.Id); + var userDb = userLogins.FirstOrDefault(x => x.UserId == u.Id); + // We don't have the user in the database yet + if (dbUser == null) { - Username = u.Username, - Type = UserType.PlexUser, - Id = u.Id, - FeaturesFormattedString = "Requestor", - EmailAddress = u.Email, - PlexInfo = new UserManagementPlexInformation - { - Thumb = u.Thumb - }, - LastLoggedIn = userDb?.LastLoggedIn ?? DateTime.MinValue, - }); + model.Add(MapPlexUser(u, null, userDb?.LastLoggedIn ?? DateTime.MinValue)); + } + else + { + // The Plex User is in the database + model.Add(MapPlexUser(u, dbUser, userDb?.LastLoggedIn ?? DateTime.MinValue)); + } } } return Response.AsJson(model); @@ -128,7 +130,7 @@ namespace PlexRequests.UI.Modules permissionsVal += f; } - var user = UserMapper.CreateUser(model.Username, model.Password, featuresVal, permissionsVal, new UserProperties { EmailAddress = model.EmailAddress }); + var user = UserMapper.CreateUser(model.Username, model.Password, permissionsVal, featuresVal, new UserProperties { EmailAddress = model.EmailAddress }); if (user.HasValue) { return Response.AsJson(MapLocalUser(UserMapper.GetUser(user.Value), DateTime.MinValue)); @@ -137,7 +139,7 @@ namespace PlexRequests.UI.Modules return Response.AsJson(new JsonResponseModel { Result = false, Message = "Could not save user" }); } - private Response UpdateUser() + private async Task UpdateUser() { var body = Request.Body.AsString(); if (string.IsNullOrEmpty(body)) @@ -156,22 +158,68 @@ namespace PlexRequests.UI.Modules }); } - var val = model.Permissions.Where(c => c.Selected).Sum(c => c.Value); + var permissionsValue = model.Permissions.Where(c => c.Selected).Sum(c => c.Value); + var featuresValue = model.Features.Where(c => c.Selected).Sum(c => c.Value); - var userFound = UserMapper.GetUser(new Guid(model.Id)); + Guid outId; + Guid.TryParse(model.Id, out outId); + var localUser = UserMapper.GetUser(outId); - userFound.Permissions = val; + // Update Local User + if (localUser != null) + { + localUser.Permissions = permissionsValue; + localUser.Features = featuresValue; - var currentProps = ByteConverterHelper.ReturnObject(userFound.UserProperties); - currentProps.UserAlias = model.Alias; - currentProps.EmailAddress = model.EmailAddress; + var currentProps = ByteConverterHelper.ReturnObject(localUser.UserProperties); + currentProps.UserAlias = model.Alias; + currentProps.EmailAddress = model.EmailAddress; - userFound.UserProperties = ByteConverterHelper.ReturnBytes(currentProps); + localUser.UserProperties = ByteConverterHelper.ReturnBytes(currentProps); - var user = UserMapper.EditUser(userFound); - var dbUser = UserLoginsRepo.GetAll().FirstOrDefault(x => x.UserId == user.UserGuid); - var retUser = MapLocalUser(user, dbUser?.LastLoggedIn ?? DateTime.MinValue); - return Response.AsJson(retUser); + var user = UserMapper.EditUser(localUser); + var dbUser = UserLoginsRepo.GetAll().FirstOrDefault(x => x.UserId == user.UserGuid); + var retUser = MapLocalUser(user, dbUser?.LastLoggedIn ?? DateTime.MinValue); + return Response.AsJson(retUser); + } + + var plexSettings = await PlexSettings.GetSettingsAsync(); + var plexDbUsers = await PlexUsersRepository.GetAllAsync(); + var plexUsers = PlexApi.GetUsers(plexSettings.PlexAuthToken); + var plexDbUser = plexDbUsers.FirstOrDefault(x => x.PlexUserId == model.Id); + var plexUser = plexUsers.User.FirstOrDefault(x => x.Id == model.Id); + var userLogin = UserLoginsRepo.GetAll().FirstOrDefault(x => x.UserId == model.Id); + if (plexDbUser != null && plexUser != null) + { + // We have a user in the DB for this Plex Account + plexDbUser.Permissions = permissionsValue; + plexDbUser.Features = featuresValue; + + plexDbUser.UserAlias = model.Alias; + + await PlexUsersRepository.UpdateAsync(plexDbUser); + + var retUser = MapPlexUser(plexUser, plexDbUser, userLogin?.LastLoggedIn ?? DateTime.MinValue); + return Response.AsJson(retUser); + } + + // We have a Plex Account but he's not in the DB + if (plexUser != null) + { + var user = new PlexUsers + { + Permissions = permissionsValue, + Features = featuresValue, + UserAlias = model.Alias, + PlexUserId = plexUser.Id + }; + + await PlexUsersRepository.InsertAsync(user); + + var retUser = MapPlexUser(plexUser, user, userLogin?.LastLoggedIn ?? DateTime.MinValue); + return Response.AsJson(retUser); + } + return null; // We should never end up here. } private Response DeleteUser() @@ -248,8 +296,8 @@ namespace PlexRequests.UI.Modules private UserManagementUsersViewModel MapLocalUser(UsersModel user, DateTime lastLoggedIn) { - var features = (Features) user.Features; - var permissions = (Permissions) user.Permissions; + var features = (Features)user.Features; + var permissions = (Permissions)user.Permissions; var userProps = ByteConverterHelper.ReturnObject(user.UserProperties); @@ -297,6 +345,64 @@ namespace PlexRequests.UI.Modules return m; } + + private UserManagementUsersViewModel MapPlexUser(UserFriends plexInfo, PlexUsers dbUser, DateTime lastLoggedIn) + { + if (dbUser == null) + { + dbUser = new PlexUsers(); + } + var features = (Features)dbUser?.Features; + var permissions = (Permissions)dbUser?.Permissions; + + var m = new UserManagementUsersViewModel + { + Id = plexInfo.Id, + PermissionsFormattedString = permissions == 0 ? "None" : permissions.ToString(), + FeaturesFormattedString = features.ToString(), + Username = plexInfo.Username, + Type = UserType.PlexUser, + EmailAddress = plexInfo.Email, + Alias = dbUser?.UserAlias ?? string.Empty, + LastLoggedIn = lastLoggedIn, + PlexInfo = new UserManagementPlexInformation + { + Thumb = plexInfo.Thumb + }, + }; + + // Add permissions + foreach (var p in Enum.GetValues(typeof(Permissions))) + { + var perm = (Permissions)p; + var displayValue = EnumHelper.GetDisplayValue(perm); + var pm = new CheckBox + { + Name = displayValue, + Selected = permissions.HasFlag(perm), + Value = (int)perm + }; + + m.Permissions.Add(pm); + } + + // Add features + foreach (var p in Enum.GetValues(typeof(Features))) + { + var perm = (Features)p; + var displayValue = EnumHelper.GetDisplayValue(perm); + var pm = new CheckBox + { + Name = displayValue, + Selected = features.HasFlag(perm), + Value = (int)perm + }; + + m.Features.Add(pm); + } + + return m; + } } } diff --git a/PlexRequests.UI/Resources/UI.resx b/PlexRequests.UI/Resources/UI.resx index ca792000b..f82c34fce 100644 --- a/PlexRequests.UI/Resources/UI.resx +++ b/PlexRequests.UI/Resources/UI.resx @@ -470,4 +470,7 @@ A background process is currently running, so there might be some unexpected behavior. This shouldn't take too long. + + User Management + \ No newline at end of file diff --git a/PlexRequests.UI/Resources/UI1.Designer.cs b/PlexRequests.UI/Resources/UI1.Designer.cs index 0a57c2412..a5cf28b1f 100644 --- a/PlexRequests.UI/Resources/UI1.Designer.cs +++ b/PlexRequests.UI/Resources/UI1.Designer.cs @@ -402,6 +402,15 @@ namespace PlexRequests.UI.Resources { } } + /// + /// Looks up a localized string similar to User Management. + /// + public static string Layout_Usermanagement { + get { + return ResourceManager.GetString("Layout_Usermanagement", resourceCulture); + } + } + /// /// Looks up a localized string similar to Welcome. /// diff --git a/PlexRequests.UI/Views/Shared/Partial/_Navbar.cshtml b/PlexRequests.UI/Views/Shared/Partial/_Navbar.cshtml index e4d75cc75..1f2f1b232 100644 --- a/PlexRequests.UI/Views/Shared/Partial/_Navbar.cshtml +++ b/PlexRequests.UI/Views/Shared/Partial/_Navbar.cshtml @@ -35,6 +35,10 @@ @Html.GetNavbarUrl(Context, "/search", UI.Layout_Search, "search") @Html.GetNavbarUrl(Context, "/requests", UI.Layout_Requests, "plus-circle") @Html.GetNavbarUrl(Context, "/issues", UI.Layout_Issues, "exclamation", "") + @if (Html.IsAdmin()) + { + @Html.GetNavbarUrl(Context, "/usermanagement", UI.Layout_Usermanagement, "users") + } @*@if (Context.CurrentUser.IsAuthenticated()) // TODO replace with IsAdmin*@ @if (Html.IsAdmin()) { diff --git a/PlexRequests.UI/Views/UserManagement/Index.cshtml b/PlexRequests.UI/Views/UserManagement/Index.cshtml index 7002cb143..d0cd11dc1 100644 --- a/PlexRequests.UI/Views/UserManagement/Index.cshtml +++ b/PlexRequests.UI/Views/UserManagement/Index.cshtml @@ -15,30 +15,31 @@
    Email Address:
    -
    - Permissions: -
    -
    - Features: -
    User Type:


    -
    +
    - Modify Roles: - + Modify Permissions: +
    + Modify Features: + +
    + + +
    + Email Address
    - +
    Alias From fdba68bb3d989b3da53ccbc0d5a9a0c3431a9ece Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Tue, 15 Nov 2016 16:11:07 +0000 Subject: [PATCH 24/87] More user management --- .../Migrations/Version1100.cs | 86 ++++++++++++++++++- .../PlexRequests.Core.Migration.csproj | 8 ++ PlexRequests.Core/PlexRequests.Core.csproj | 1 + .../SettingModels/UserManagementSettings.cs | 38 ++++++++ .../Modules/UserManagementModule.cs | 2 + 5 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 PlexRequests.Core/SettingModels/UserManagementSettings.cs diff --git a/PlexRequests.Core.Migration/Migrations/Version1100.cs b/PlexRequests.Core.Migration/Migrations/Version1100.cs index 525114ebb..dbbfab158 100644 --- a/PlexRequests.Core.Migration/Migrations/Version1100.cs +++ b/PlexRequests.Core.Migration/Migrations/Version1100.cs @@ -26,13 +26,16 @@ #endregion using System; +using System.Collections.Generic; using System.Data; using NLog; using System.Linq; +using PlexRequests.Api.Interfaces; using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; using PlexRequests.Helpers.Permissions; using PlexRequests.Store; +using PlexRequests.Store.Models; using PlexRequests.Store.Repository; namespace PlexRequests.Core.Migration.Migrations @@ -40,16 +43,27 @@ namespace PlexRequests.Core.Migration.Migrations [Migration(11000, "v1.10.0.0")] public class Version1100 : BaseMigration, IMigration { - public Version1100(IUserRepository userRepo, IRequestService requestService, ISettingsService log) + public Version1100(IUserRepository userRepo, IRequestService requestService, ISettingsService log, IPlexApi plexApi, ISettingsService plexService, IRepository plexusers, + ISettingsService prSettings, ISettingsService umSettings) { UserRepo = userRepo; RequestService = requestService; Log = log; + PlexApi = plexApi; + PlexSettings = plexService; + PlexUsers = plexusers; + PlexRequestSettings = prSettings; + UserManagementSettings = umSettings; } public int Version => 11000; private IUserRepository UserRepo { get; } private IRequestService RequestService { get; } private ISettingsService Log { get; } + private IPlexApi PlexApi { get; } + private ISettingsService PlexSettings { get; } + private IRepository PlexUsers { get; } + private ISettingsService PlexRequestSettings { get; } + private ISettingsService UserManagementSettings { get; } public void Start(IDbConnection con) { @@ -58,9 +72,79 @@ namespace PlexRequests.Core.Migration.Migrations // Update the current admin permissions set UpdateAdmin(); ResetLogLevel(); + UpdatePlexUsers(); + PopulateDefaultUserManagementSettings(); + UpdateSchema(con, Version); } + private void PopulateDefaultUserManagementSettings() + { + var plexRequestSettings = PlexRequestSettings.GetSettings(); + + UserManagementSettings.SaveSettings(new UserManagementSettings + { + AutoApproveMovies = !plexRequestSettings.RequireMovieApproval, + SearchForTvShows = plexRequestSettings.SearchForTvShows, + SearchForMusic = plexRequestSettings.SearchForMusic, + SearchForMovies = plexRequestSettings.SearchForMovies, + AutoApproveMusic = !plexRequestSettings.RequireMusicApproval, + AutoApproveTvShows = !plexRequestSettings.RequireTvShowApproval + }); + } + + private void UpdatePlexUsers() + { + var settings = PlexSettings.GetSettings(); + var plexUsers = PlexApi.GetUsers(settings.PlexAuthToken); + var prSettings = PlexRequestSettings.GetSettings(); + + var dbUsers = PlexUsers.GetAll().ToList(); + foreach (var user in plexUsers.User) + { + if (dbUsers.FirstOrDefault(x => x.PlexUserId == user.Id) != null) + { + continue; + } + + int permissions = 0; + if (prSettings.SearchForMovies) + { + permissions = (int) Permissions.RequestMovie; + } + if (prSettings.SearchForTvShows) + { + permissions += (int) Permissions.RequestTvShow; + } + if (prSettings.SearchForMusic) + { + permissions += (int) Permissions.RequestMusic; + } + if (!prSettings.RequireMovieApproval) + { + permissions += (int)Permissions.AutoApproveMovie; + } + if (!prSettings.RequireTvShowApproval) + { + permissions += (int)Permissions.AutoApproveTv; + } + if (!prSettings.RequireMusicApproval) + { + permissions += (int)Permissions.AutoApproveAlbum; + } + + var m = new PlexUsers + { + PlexUserId = user.Id, + Permissions = permissions, + Features = 0, + }; + + PlexUsers.Insert(m); + } + + } + private void ResetLogLevel() { var logSettings = Log.GetSettings(); diff --git a/PlexRequests.Core.Migration/PlexRequests.Core.Migration.csproj b/PlexRequests.Core.Migration/PlexRequests.Core.Migration.csproj index fa934dfc0..0b582b68b 100644 --- a/PlexRequests.Core.Migration/PlexRequests.Core.Migration.csproj +++ b/PlexRequests.Core.Migration/PlexRequests.Core.Migration.csproj @@ -74,6 +74,14 @@ + + {95834072-A675-415D-AA8F-877C91623810} + PlexRequests.Api.Interfaces + + + {CB37A5F8-6DFC-4554-99D3-A42B502E4591} + PlexRequests.Api.Models + {DD7DC444-D3BF-4027-8AB9-EFC71F5EC581} PlexRequests.Core diff --git a/PlexRequests.Core/PlexRequests.Core.csproj b/PlexRequests.Core/PlexRequests.Core.csproj index 5f093a928..cf47610b6 100644 --- a/PlexRequests.Core/PlexRequests.Core.csproj +++ b/PlexRequests.Core/PlexRequests.Core.csproj @@ -134,6 +134,7 @@ + diff --git a/PlexRequests.Core/SettingModels/UserManagementSettings.cs b/PlexRequests.Core/SettingModels/UserManagementSettings.cs new file mode 100644 index 000000000..2e6d7bc0c --- /dev/null +++ b/PlexRequests.Core/SettingModels/UserManagementSettings.cs @@ -0,0 +1,38 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: UserManagementSettings.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +namespace PlexRequests.Core.SettingModels +{ + public class UserManagementSettings + { + public bool SearchForMovies { get; set; } + public bool SearchForTvShows { get; set; } + public bool SearchForMusic { get; set; } + public bool AutoApproveMovies { get; set; } + public bool AutoApproveTvShows { get; set; } + public bool AutoApproveMusic { get; set; } + } +} \ No newline at end of file diff --git a/PlexRequests.UI/Modules/UserManagementModule.cs b/PlexRequests.UI/Modules/UserManagementModule.cs index 9870d3533..0bfd73d65 100644 --- a/PlexRequests.UI/Modules/UserManagementModule.cs +++ b/PlexRequests.UI/Modules/UserManagementModule.cs @@ -33,6 +33,7 @@ namespace PlexRequests.UI.Modules PlexSettings = plex; UserLoginsRepo = userLogins; PlexUsersRepository = plexRepo; + PlexRequestSettings = pr; Get["/"] = x => Load(); @@ -51,6 +52,7 @@ namespace PlexRequests.UI.Modules private ISettingsService PlexSettings { get; } private IRepository UserLoginsRepo { get; } private IRepository PlexUsersRepository { get; } + private ISettingsService PlexRequestSettings { get; } private Negotiator Load() { From 2f34aaec4bef71956b2e6f1342d80f7352d66d35 Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Wed, 16 Nov 2016 08:52:38 +0000 Subject: [PATCH 25/87] Fixed #681 --- PlexRequests.Core/SettingModels/UserManagementSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PlexRequests.Core/SettingModels/UserManagementSettings.cs b/PlexRequests.Core/SettingModels/UserManagementSettings.cs index 2e6d7bc0c..975eebcc3 100644 --- a/PlexRequests.Core/SettingModels/UserManagementSettings.cs +++ b/PlexRequests.Core/SettingModels/UserManagementSettings.cs @@ -26,7 +26,7 @@ #endregion namespace PlexRequests.Core.SettingModels { - public class UserManagementSettings + public class UserManagementSettings : Settings { public bool SearchForMovies { get; set; } public bool SearchForTvShows { get; set; } From cb3c3fe10e93614317ed19d6f1e5e89e701aafec Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Wed, 16 Nov 2016 09:55:05 +0000 Subject: [PATCH 26/87] Fixed potential crash #683 --- PlexRequests.Core.Migration/Migrations/Version1100.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/PlexRequests.Core.Migration/Migrations/Version1100.cs b/PlexRequests.Core.Migration/Migrations/Version1100.cs index dbbfab158..616f0c3d9 100644 --- a/PlexRequests.Core.Migration/Migrations/Version1100.cs +++ b/PlexRequests.Core.Migration/Migrations/Version1100.cs @@ -96,6 +96,10 @@ namespace PlexRequests.Core.Migration.Migrations private void UpdatePlexUsers() { var settings = PlexSettings.GetSettings(); + if (string.IsNullOrEmpty(settings.PlexAuthToken)) + { + return; + } var plexUsers = PlexApi.GetUsers(settings.PlexAuthToken); var prSettings = PlexRequestSettings.GetSettings(); @@ -138,6 +142,7 @@ namespace PlexRequests.Core.Migration.Migrations PlexUserId = user.Id, Permissions = permissions, Features = 0, + UserAlias = string.Empty, }; PlexUsers.Insert(m); From 7412655c5ab31deead4e47434c2d02fd28925863 Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Wed, 16 Nov 2016 14:20:13 +0000 Subject: [PATCH 27/87] Lots of fixed and stuff. --- PlexRequests.Api/ApiRequest.cs | 3 +- PlexRequests.Core/UserMapper.cs | 26 - PlexRequests.Helpers/EnumHelper.cs | 5 + .../Permissions/Permissions.cs | 1 - .../PlexAvailabilityCheckerTests.cs | 500 +++++++++--------- PlexRequests.Services/Jobs/JobNames.cs | 1 + .../Jobs/PlexAvailabilityChecker.cs | 6 +- .../Jobs/PlexContentCacher.cs | 345 ++++++++++++ PlexRequests.Services/Models/PlexAlbum.cs | 2 + PlexRequests.Services/Models/PlexMovie.cs | 1 + PlexRequests.Services/Models/PlexTvShow.cs | 1 + .../PlexRequests.Services.csproj | 1 + PlexRequests.Store/Models/Plex/PlexContent.cs | 53 ++ .../Models/Plex/PlexMediaType .cs | 35 ++ PlexRequests.Store/PlexRequests.Store.csproj | 2 + PlexRequests.Store/SqlTables.sql | 14 + PlexRequests.UI/Helpers/SecurityExtensions.cs | 7 +- PlexRequests.UI/Modules/LoginModule.cs | 3 +- PlexRequests.UI/Modules/UserWizardModule.cs | 3 +- 19 files changed, 725 insertions(+), 284 deletions(-) create mode 100644 PlexRequests.Services/Jobs/PlexContentCacher.cs create mode 100644 PlexRequests.Store/Models/Plex/PlexContent.cs create mode 100644 PlexRequests.Store/Models/Plex/PlexMediaType .cs diff --git a/PlexRequests.Api/ApiRequest.cs b/PlexRequests.Api/ApiRequest.cs index 8cb023ea6..8bf1a2721 100644 --- a/PlexRequests.Api/ApiRequest.cs +++ b/PlexRequests.Api/ApiRequest.cs @@ -104,8 +104,7 @@ namespace PlexRequests.Api } var result = DeserializeXml(response.Content); - return result; - } + return result;} public T ExecuteJson(IRestRequest request, Uri baseUri) where T : new() { diff --git a/PlexRequests.Core/UserMapper.cs b/PlexRequests.Core/UserMapper.cs index 9a0afc8de..50215093d 100644 --- a/PlexRequests.Core/UserMapper.cs +++ b/PlexRequests.Core/UserMapper.cs @@ -145,26 +145,6 @@ namespace PlexRequests.Core Repo.Delete(user); } - public Guid? CreateAdmin(string username, string password, UserProperties properties = null) - { - return CreateUser(username, password, properties); - } - - public Guid? CreatePowerUser(string username, string password, UserProperties properties = null) - { - return CreateUser(username, password, properties); - } - - public Guid? CreateRegularUser(string username, string password, UserProperties properties = null) - { - return CreateUser(username, password, properties); - } - - public IEnumerable GetAllClaims() - { - var properties = typeof(UserClaims).GetConstantsValues(); - return properties; - } public bool UpdatePassword(string username, string oldPassword, string newPassword) { @@ -207,11 +187,8 @@ namespace PlexRequests.Core public interface ICustomUserMapper { - Guid? CreateUser(string username, string password, UserProperties props); - Guid? CreateUser(string username, string password, int permissions, int features, UserProperties properties = null); - IEnumerable GetAllClaims(); IEnumerable GetUsers(); Task> GetUsersAsync(); UsersModel GetUser(Guid userId); @@ -219,9 +196,6 @@ namespace PlexRequests.Core bool DoUsersExist(); Guid? ValidateUser(string username, string password); bool UpdatePassword(string username, string oldPassword, string newPassword); - Guid? CreateAdmin(string username, string password, UserProperties properties = null); - Guid? CreatePowerUser(string username, string password, UserProperties properties = null); - Guid? CreateRegularUser(string username, string password, UserProperties properties = null); void DeleteUser(string userId); } } diff --git a/PlexRequests.Helpers/EnumHelper.cs b/PlexRequests.Helpers/EnumHelper.cs index 051c78508..387f867e9 100644 --- a/PlexRequests.Helpers/EnumHelper.cs +++ b/PlexRequests.Helpers/EnumHelper.cs @@ -108,5 +108,10 @@ namespace PlexRequests.Helpers throw new ArgumentOutOfRangeException(nameof(name)); } + + public static int All() + { + return Enum.GetValues(typeof(T)).Cast().Sum(); + } } } \ No newline at end of file diff --git a/PlexRequests.Helpers/Permissions/Permissions.cs b/PlexRequests.Helpers/Permissions/Permissions.cs index 8b1d22954..1afd34edb 100644 --- a/PlexRequests.Helpers/Permissions/Permissions.cs +++ b/PlexRequests.Helpers/Permissions/Permissions.cs @@ -59,6 +59,5 @@ namespace PlexRequests.Helpers.Permissions [Display(Name = "Auto Approve Album Requests")] AutoApproveAlbum = 256 - } } \ No newline at end of file diff --git a/PlexRequests.Services.Tests/PlexAvailabilityCheckerTests.cs b/PlexRequests.Services.Tests/PlexAvailabilityCheckerTests.cs index 801af7509..1097bfb67 100644 --- a/PlexRequests.Services.Tests/PlexAvailabilityCheckerTests.cs +++ b/PlexRequests.Services.Tests/PlexAvailabilityCheckerTests.cs @@ -1,284 +1,284 @@ -#region Copyright -// /************************************************************************ -// Copyright (c) 2016 Jamie Rees -// File: PlexAvailabilityCheckerTests.cs -// Created By: Jamie Rees -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// ************************************************************************/ -#endregion -using System; -using System.Collections.Generic; -using System.Data; -using System.Linq; -using System.Threading.Tasks; +//#region Copyright +//// /************************************************************************ +//// Copyright (c) 2016 Jamie Rees +//// File: PlexAvailabilityCheckerTests.cs +//// Created By: Jamie Rees +//// +//// Permission is hereby granted, free of charge, to any person obtaining +//// a copy of this software and associated documentation files (the +//// "Software"), to deal in the Software without restriction, including +//// without limitation the rights to use, copy, modify, merge, publish, +//// distribute, sublicense, and/or sell copies of the Software, and to +//// permit persons to whom the Software is furnished to do so, subject to +//// the following conditions: +//// +//// The above copyright notice and this permission notice shall be +//// included in all copies or substantial portions of the Software. +//// +//// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +//// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +//// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +//// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +//// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +//// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +//// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +//// ************************************************************************/ +//#endregion +//using System; +//using System.Collections.Generic; +//using System.Data; +//using System.Linq; +//using System.Threading.Tasks; -using Moq; +//using Moq; -using NUnit.Framework; +//using NUnit.Framework; -using PlexRequests.Api.Interfaces; -using PlexRequests.Api.Models.Plex; -using PlexRequests.Core; -using PlexRequests.Core.SettingModels; -using PlexRequests.Services.Interfaces; -using PlexRequests.Helpers; -using PlexRequests.Services.Jobs; -using PlexRequests.Services.Models; -using PlexRequests.Services.Notification; -using PlexRequests.Store.Models; -using PlexRequests.Store.Repository; +//using PlexRequests.Api.Interfaces; +//using PlexRequests.Api.Models.Plex; +//using PlexRequests.Core; +//using PlexRequests.Core.SettingModels; +//using PlexRequests.Services.Interfaces; +//using PlexRequests.Helpers; +//using PlexRequests.Services.Jobs; +//using PlexRequests.Services.Models; +//using PlexRequests.Services.Notification; +//using PlexRequests.Store.Models; +//using PlexRequests.Store.Repository; -using Ploeh.AutoFixture; +//using Ploeh.AutoFixture; -namespace PlexRequests.Services.Tests -{ - [TestFixture] - public class PlexAvailabilityCheckerTests - { - public IAvailabilityChecker Checker { get; set; } - private Fixture F { get; set; } = new Fixture(); - private Mock> SettingsMock { get; set; } - private Mock> AuthMock { get; set; } - private Mock RequestMock { get; set; } - private Mock PlexMock { get; set; } - private Mock CacheMock { get; set; } - private Mock NotificationMock { get; set; } - private Mock JobRec { get; set; } - private Mock> NotifyUsers { get; set; } - private Mock> PlexEpisodes { get; set; } - private Mock Engine - { - get; - set; - } +//namespace PlexRequests.Services.Tests +//{ +// [TestFixture] +// public class PlexAvailabilityCheckerTests +// { +// public IAvailabilityChecker Checker { get; set; } +// private Fixture F { get; set; } = new Fixture(); +// private Mock> SettingsMock { get; set; } +// private Mock> AuthMock { get; set; } +// private Mock RequestMock { get; set; } +// private Mock PlexMock { get; set; } +// private Mock CacheMock { get; set; } +// private Mock NotificationMock { get; set; } +// private Mock JobRec { get; set; } +// private Mock> NotifyUsers { get; set; } +// private Mock> PlexEpisodes { get; set; } +// private Mock Engine +// { +// get; +// set; +// } - [SetUp] - public void Setup() - { - SettingsMock = new Mock>(); - AuthMock = new Mock>(); - RequestMock = new Mock(); - PlexMock = new Mock(); - NotificationMock = new Mock(); - CacheMock = new Mock(); - NotifyUsers = new Mock>(); - PlexEpisodes = new Mock>(); - JobRec = new Mock(); - Engine = new Mock(); - Checker = new PlexAvailabilityChecker(SettingsMock.Object, RequestMock.Object, PlexMock.Object, CacheMock.Object, NotificationMock.Object, JobRec.Object, NotifyUsers.Object, PlexEpisodes.Object, Engine.Object); +// [SetUp] +// public void Setup() +// { +// SettingsMock = new Mock>(); +// AuthMock = new Mock>(); +// RequestMock = new Mock(); +// PlexMock = new Mock(); +// NotificationMock = new Mock(); +// CacheMock = new Mock(); +// NotifyUsers = new Mock>(); +// PlexEpisodes = new Mock>(); +// JobRec = new Mock(); +// Engine = new Mock(); +// Checker = new PlexAvailabilityChecker(SettingsMock.Object, RequestMock.Object, PlexMock.Object, CacheMock.Object, NotificationMock.Object, JobRec.Object, NotifyUsers.Object, PlexEpisodes.Object, Engine.Object); - } +// } - [Test] - public void InvalidSettings() - { - Checker.CheckAndUpdateAll(); - PlexMock.Verify(x => x.GetLibrary(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); - PlexMock.Verify(x => x.GetAccount(It.IsAny()), Times.Never); - PlexMock.Verify(x => x.GetLibrarySections(It.IsAny(), It.IsAny()), Times.Never); - PlexMock.Verify(x => x.GetStatus(It.IsAny(), It.IsAny()), Times.Never); - PlexMock.Verify(x => x.GetUsers(It.IsAny()), Times.Never); - } +// [Test] +// public void InvalidSettings() +// { +// Checker.CheckAndUpdateAll(); +// PlexMock.Verify(x => x.GetLibrary(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); +// PlexMock.Verify(x => x.GetAccount(It.IsAny()), Times.Never); +// PlexMock.Verify(x => x.GetLibrarySections(It.IsAny(), It.IsAny()), Times.Never); +// PlexMock.Verify(x => x.GetStatus(It.IsAny(), It.IsAny()), Times.Never); +// PlexMock.Verify(x => x.GetUsers(It.IsAny()), Times.Never); +// } - [TestCaseSource(nameof(IsMovieAvailableTestData))] - public bool IsMovieAvailableTest(string title, string year) - { - var movies = new List - { - new PlexMovie {Title = title, ProviderId = null, ReleaseYear = year} - }; - var result = Checker.IsMovieAvailable(movies.ToArray(), "title", "2011"); +// [TestCaseSource(nameof(IsMovieAvailableTestData))] +// public bool IsMovieAvailableTest(string title, string year) +// { +// var movies = new List +// { +// new PlexMovie {Title = title, ProviderId = null, ReleaseYear = year} +// }; +// var result = Checker.IsMovieAvailable(movies.ToArray(), "title", "2011"); - return result; - } +// return result; +// } - private static IEnumerable IsMovieAvailableTestData - { - get - { - yield return new TestCaseData("title", "2011").Returns(true).SetName("IsMovieAvailable True"); - yield return new TestCaseData("title2", "2011").Returns(false).SetName("IsMovieAvailable False different title"); - yield return new TestCaseData("title", "2001").Returns(false).SetName("IsMovieAvailable False different year"); - } - } +// private static IEnumerable IsMovieAvailableTestData +// { +// get +// { +// yield return new TestCaseData("title", "2011").Returns(true).SetName("IsMovieAvailable True"); +// yield return new TestCaseData("title2", "2011").Returns(false).SetName("IsMovieAvailable False different title"); +// yield return new TestCaseData("title", "2001").Returns(false).SetName("IsMovieAvailable False different year"); +// } +// } - [TestCaseSource(nameof(IsMovieAvailableAdvancedTestData))] - public bool IsMovieAvailableAdvancedTest(string title, string year, string providerId) - { - var movies = new List - { - new PlexMovie {Title = title, ProviderId = providerId, ReleaseYear = year } - }; - var result = Checker.IsMovieAvailable(movies.ToArray(), "title", "2011", 9999.ToString()); +// [TestCaseSource(nameof(IsMovieAvailableAdvancedTestData))] +// public bool IsMovieAvailableAdvancedTest(string title, string year, string providerId) +// { +// var movies = new List +// { +// new PlexMovie {Title = title, ProviderId = providerId, ReleaseYear = year } +// }; +// var result = Checker.IsMovieAvailable(movies.ToArray(), "title", "2011", 9999.ToString()); - return result; - } +// return result; +// } - private static IEnumerable IsMovieAvailableAdvancedTestData - { - get - { - yield return new TestCaseData("title", "2011", "9999").Returns(true).SetName("Advanced IsMovieAvailable True"); - yield return new TestCaseData("title2", "2011", "99929").Returns(false).SetName("Advanced IsMovieAvailable False different title"); - yield return new TestCaseData("title", "2001", "99939").Returns(false).SetName("Advanced IsMovieAvailable False different year"); - yield return new TestCaseData("title", "2001", "44445").Returns(false).SetName("Advanced IsMovieAvailable False different providerID"); - } - } +// private static IEnumerable IsMovieAvailableAdvancedTestData +// { +// get +// { +// yield return new TestCaseData("title", "2011", "9999").Returns(true).SetName("Advanced IsMovieAvailable True"); +// yield return new TestCaseData("title2", "2011", "99929").Returns(false).SetName("Advanced IsMovieAvailable False different title"); +// yield return new TestCaseData("title", "2001", "99939").Returns(false).SetName("Advanced IsMovieAvailable False different year"); +// yield return new TestCaseData("title", "2001", "44445").Returns(false).SetName("Advanced IsMovieAvailable False different providerID"); +// } +// } - [TestCaseSource(nameof(IsTvAvailableTestData))] - public bool IsTvAvailableTest(string title, string year) - { - var tv = new List - { - new PlexTvShow {Title = title, ProviderId = null, ReleaseYear = year} - }; - var result = Checker.IsTvShowAvailable(tv.ToArray(), "title", "2011"); +// [TestCaseSource(nameof(IsTvAvailableTestData))] +// public bool IsTvAvailableTest(string title, string year) +// { +// var tv = new List +// { +// new PlexTvShow {Title = title, ProviderId = null, ReleaseYear = year} +// }; +// var result = Checker.IsTvShowAvailable(tv.ToArray(), "title", "2011"); - return result; - } +// return result; +// } - private static IEnumerable IsTvAvailableTestData - { - get - { - yield return new TestCaseData("title", "2011").Returns(true).SetName("IsTvAvailable True"); - yield return new TestCaseData("title2", "2011").Returns(false).SetName("IsTvAvailable False different title"); - yield return new TestCaseData("title", "2001").Returns(false).SetName("IsTvAvailable False different year"); - } - } +// private static IEnumerable IsTvAvailableTestData +// { +// get +// { +// yield return new TestCaseData("title", "2011").Returns(true).SetName("IsTvAvailable True"); +// yield return new TestCaseData("title2", "2011").Returns(false).SetName("IsTvAvailable False different title"); +// yield return new TestCaseData("title", "2001").Returns(false).SetName("IsTvAvailable False different year"); +// } +// } - [TestCaseSource(nameof(IsTvAvailableAdvancedTestData))] - public bool IsTvAvailableAdvancedTest(string title, string year, string providerId) - { - var movies = new List - { - new PlexTvShow {Title = title, ProviderId = providerId, ReleaseYear = year } - }; - var result = Checker.IsTvShowAvailable(movies.ToArray(), "title", "2011", 9999.ToString()); +// [TestCaseSource(nameof(IsTvAvailableAdvancedTestData))] +// public bool IsTvAvailableAdvancedTest(string title, string year, string providerId) +// { +// var movies = new List +// { +// new PlexTvShow {Title = title, ProviderId = providerId, ReleaseYear = year } +// }; +// var result = Checker.IsTvShowAvailable(movies.ToArray(), "title", "2011", 9999.ToString()); - return result; - } +// return result; +// } - private static IEnumerable IsTvAvailableAdvancedTestData - { - get - { - yield return new TestCaseData("title", "2011", "9999").Returns(true).SetName("Advanced IsTvAvailable True"); - yield return new TestCaseData("title2", "2011", "99929").Returns(false).SetName("Advanced IsTvAvailable False different title"); - yield return new TestCaseData("title", "2001", "99939").Returns(false).SetName("Advanced IsTvAvailable False different year"); - yield return new TestCaseData("title", "2001", "44445").Returns(false).SetName("Advanced IsTvAvailable False different providerID"); - } - } +// private static IEnumerable IsTvAvailableAdvancedTestData +// { +// get +// { +// yield return new TestCaseData("title", "2011", "9999").Returns(true).SetName("Advanced IsTvAvailable True"); +// yield return new TestCaseData("title2", "2011", "99929").Returns(false).SetName("Advanced IsTvAvailable False different title"); +// yield return new TestCaseData("title", "2001", "99939").Returns(false).SetName("Advanced IsTvAvailable False different year"); +// yield return new TestCaseData("title", "2001", "44445").Returns(false).SetName("Advanced IsTvAvailable False different providerID"); +// } +// } - [TestCaseSource(nameof(IsTvAvailableAdvancedSeasonsTestData))] - public bool IsTvAvailableAdvancedSeasonsTest(string title, string year, string providerId, int[] seasons) - { - var movies = new List - { - new PlexTvShow {Title = title, ProviderId = providerId, ReleaseYear = year , Seasons = seasons} - }; - var result = Checker.IsTvShowAvailable(movies.ToArray(), "title", "2011", 9999.ToString(), new[] { 1, 2, 3 }); +// [TestCaseSource(nameof(IsTvAvailableAdvancedSeasonsTestData))] +// public bool IsTvAvailableAdvancedSeasonsTest(string title, string year, string providerId, int[] seasons) +// { +// var movies = new List +// { +// new PlexTvShow {Title = title, ProviderId = providerId, ReleaseYear = year , Seasons = seasons} +// }; +// var result = Checker.IsTvShowAvailable(movies.ToArray(), "title", "2011", 9999.ToString(), new[] { 1, 2, 3 }); - return result; - } +// return result; +// } - private static IEnumerable IsTvAvailableAdvancedSeasonsTestData - { - get - { - yield return new TestCaseData("title", "2011", "9999", new[] { 1, 2, 3 }).Returns(true).SetName("Advanced IsTvSeasonsAvailable True"); - yield return new TestCaseData("title2", "2011", "99929", new[] { 5, 6 }).Returns(false).SetName("Advanced IsTvSeasonsAvailable False no seasons"); - yield return new TestCaseData("title2", "2011", "9999", new[] { 1, 6 }).Returns(true).SetName("Advanced IsTvSeasonsAvailable true one season"); - } - } +// private static IEnumerable IsTvAvailableAdvancedSeasonsTestData +// { +// get +// { +// yield return new TestCaseData("title", "2011", "9999", new[] { 1, 2, 3 }).Returns(true).SetName("Advanced IsTvSeasonsAvailable True"); +// yield return new TestCaseData("title2", "2011", "99929", new[] { 5, 6 }).Returns(false).SetName("Advanced IsTvSeasonsAvailable False no seasons"); +// yield return new TestCaseData("title2", "2011", "9999", new[] { 1, 6 }).Returns(true).SetName("Advanced IsTvSeasonsAvailable true one season"); +// } +// } - [TestCaseSource(nameof(IsEpisodeAvailableTestData))] - public bool IsEpisodeAvailableTest(string providerId, int season, int episode) - { - var expected = new List - { - new PlexEpisodes {EpisodeNumber = 1, ShowTitle = "The Flash",ProviderId = 23.ToString(), SeasonNumber = 1, EpisodeTitle = "Pilot"} - }; - PlexEpisodes.Setup(x => x.Custom(It.IsAny>>())).Returns(expected); +// [TestCaseSource(nameof(IsEpisodeAvailableTestData))] +// public bool IsEpisodeAvailableTest(string providerId, int season, int episode) +// { +// var expected = new List +// { +// new PlexEpisodes {EpisodeNumber = 1, ShowTitle = "The Flash",ProviderId = 23.ToString(), SeasonNumber = 1, EpisodeTitle = "Pilot"} +// }; +// PlexEpisodes.Setup(x => x.Custom(It.IsAny>>())).Returns(expected); - var result = Checker.IsEpisodeAvailable(providerId, season, episode); +// var result = Checker.IsEpisodeAvailable(providerId, season, episode); - return result; - } +// return result; +// } - private static IEnumerable IsEpisodeAvailableTestData - { - get - { - yield return new TestCaseData("23", 1, 1).Returns(true).SetName("IsEpisodeAvailable True S01E01"); - yield return new TestCaseData("23", 1, 2).Returns(false).SetName("IsEpisodeAvailable False S01E02"); - yield return new TestCaseData("23", 99, 99).Returns(false).SetName("IsEpisodeAvailable False S99E99"); - yield return new TestCaseData("230", 99, 99).Returns(false).SetName("IsEpisodeAvailable False Incorrect ProviderId"); - } - } +// private static IEnumerable IsEpisodeAvailableTestData +// { +// get +// { +// yield return new TestCaseData("23", 1, 1).Returns(true).SetName("IsEpisodeAvailable True S01E01"); +// yield return new TestCaseData("23", 1, 2).Returns(false).SetName("IsEpisodeAvailable False S01E02"); +// yield return new TestCaseData("23", 99, 99).Returns(false).SetName("IsEpisodeAvailable False S99E99"); +// yield return new TestCaseData("230", 99, 99).Returns(false).SetName("IsEpisodeAvailable False Incorrect ProviderId"); +// } +// } - [Test] - public void GetPlexMoviesTests() - { - var cachedMovies = F.Build().Without(x => x.Directory).CreateMany().ToList(); - cachedMovies.Add(new PlexSearch - { - Video = new List
    From 63c27443363ca2b158c29a86427b76391de9943b Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Fri, 18 Nov 2016 09:46:04 +0000 Subject: [PATCH 35/87] Added loading spinner --- .../Angular/angular-loading-spinner.js | 25 +++ .../Content/Angular/angular-spinner.min.js | 2 + PlexRequests.UI/Content/app/app.js | 2 +- PlexRequests.UI/Content/spin.min.js | 2 + PlexRequests.UI/Helpers/BaseUrlHelper.cs | 6 +- .../Views/UserManagement/Index.cshtml | 161 +++++++++--------- 6 files changed, 116 insertions(+), 82 deletions(-) create mode 100644 PlexRequests.UI/Content/Angular/angular-loading-spinner.js create mode 100644 PlexRequests.UI/Content/Angular/angular-spinner.min.js create mode 100644 PlexRequests.UI/Content/spin.min.js diff --git a/PlexRequests.UI/Content/Angular/angular-loading-spinner.js b/PlexRequests.UI/Content/Angular/angular-loading-spinner.js new file mode 100644 index 000000000..c25717c81 --- /dev/null +++ b/PlexRequests.UI/Content/Angular/angular-loading-spinner.js @@ -0,0 +1,25 @@ +(function(){ + angular.module('ngLoadingSpinner', ['angularSpinner']) + .directive('usSpinner', ['$http', '$rootScope' ,function ($http, $rootScope){ + return { + link: function (scope, elm, attrs) + { + $rootScope.spinnerActive = false; + scope.isLoading = function () { + return $http.pendingRequests.length > 0; + }; + + scope.$watch(scope.isLoading, function (loading) + { + $rootScope.spinnerActive = loading; + if(loading){ + elm.removeClass('ng-hide'); + }else{ + elm.addClass('ng-hide'); + } + }); + } + }; + + }]); +}).call(this); \ No newline at end of file diff --git a/PlexRequests.UI/Content/Angular/angular-spinner.min.js b/PlexRequests.UI/Content/Angular/angular-spinner.min.js new file mode 100644 index 000000000..ef727fe75 --- /dev/null +++ b/PlexRequests.UI/Content/Angular/angular-spinner.min.js @@ -0,0 +1,2 @@ +!function(a){"use strict";function b(a,b){a.module("angularSpinner",[]).factory("usSpinnerService",["$rootScope",function(a){var b={};return b.spin=function(b){a.$broadcast("us-spinner:spin",b)},b.stop=function(b){a.$broadcast("us-spinner:stop",b)},b}]).directive("usSpinner",["$window",function(c){return{scope:!0,link:function(d,e,f){function g(){d.spinner&&d.spinner.stop()}var h=b||c.Spinner;d.spinner=null,d.key=a.isDefined(f.spinnerKey)?f.spinnerKey:!1,d.startActive=a.isDefined(f.spinnerStartActive)?f.spinnerStartActive:d.key?!1:!0,d.spin=function(){d.spinner&&d.spinner.spin(e[0])},d.stop=function(){d.startActive=!1,g()},d.$watch(f.usSpinner,function(a){g(),d.spinner=new h(a),(!d.key||d.startActive)&&d.spinner.spin(e[0])},!0),d.$on("us-spinner:spin",function(a,b){b===d.key&&d.spin()}),d.$on("us-spinner:stop",function(a,b){b===d.key&&d.stop()}),d.$on("$destroy",function(){d.stop(),d.spinner=null})}}}])}"function"==typeof define&&define.amd?define(["angular","spin"],b):b(a.angular)}(window); +//# sourceMappingURL=angular-spinner.min.js.map \ No newline at end of file diff --git a/PlexRequests.UI/Content/app/app.js b/PlexRequests.UI/Content/app/app.js index af97fb710..197a8a7f6 100644 --- a/PlexRequests.UI/Content/app/app.js +++ b/PlexRequests.UI/Content/app/app.js @@ -1,4 +1,4 @@ (function() { - module = angular.module('PlexRequests', []); + module = angular.module('PlexRequests', ['ngLoadingSpinner']); module.constant("moment", moment); }()); \ No newline at end of file diff --git a/PlexRequests.UI/Content/spin.min.js b/PlexRequests.UI/Content/spin.min.js new file mode 100644 index 000000000..aa2b9c3df --- /dev/null +++ b/PlexRequests.UI/Content/spin.min.js @@ -0,0 +1,2 @@ +// http://spin.js.org/#v2.3.2 +!function(a,b){"object"==typeof module&&module.exports?module.exports=b():"function"==typeof define&&define.amd?define(b):a.Spinner=b()}(this,function(){"use strict";function a(a,b){var c,d=document.createElement(a||"div");for(c in b)d[c]=b[c];return d}function b(a){for(var b=1,c=arguments.length;c>b;b++)a.appendChild(arguments[b]);return a}function c(a,b,c,d){var e=["opacity",b,~~(100*a),c,d].join("-"),f=.01+c/d*100,g=Math.max(1-(1-a)/b*(100-f),a),h=j.substring(0,j.indexOf("Animation")).toLowerCase(),i=h&&"-"+h+"-"||"";return m[e]||(k.insertRule("@"+i+"keyframes "+e+"{0%{opacity:"+g+"}"+f+"%{opacity:"+a+"}"+(f+.01)+"%{opacity:1}"+(f+b)%100+"%{opacity:"+a+"}100%{opacity:"+g+"}}",k.cssRules.length),m[e]=1),e}function d(a,b){var c,d,e=a.style;if(b=b.charAt(0).toUpperCase()+b.slice(1),void 0!==e[b])return b;for(d=0;d',c)}k.addRule(".spin-vml","behavior:url(#default#VML)"),h.prototype.lines=function(a,d){function f(){return e(c("group",{coordsize:k+" "+k,coordorigin:-j+" "+-j}),{width:k,height:k})}function h(a,h,i){b(m,b(e(f(),{rotation:360/d.lines*a+"deg",left:~~h}),b(e(c("roundrect",{arcsize:d.corners}),{width:j,height:d.scale*d.width,left:d.scale*d.radius,top:-d.scale*d.width>>1,filter:i}),c("fill",{color:g(d.color,a),opacity:d.opacity}),c("stroke",{opacity:0}))))}var i,j=d.scale*(d.length+d.width),k=2*d.scale*j,l=-(d.width+d.length)*d.scale*2+"px",m=e(f(),{position:"absolute",top:l,left:l});if(d.shadow)for(i=1;i<=d.lines;i++)h(i,-2,"progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)");for(i=1;i<=d.lines;i++)h(i);return b(a,m)},h.prototype.opacity=function(a,b,c,d){var e=a.firstChild;d=d.shadow&&d.lines||0,e&&b+d>1)+"px"})}for(var i,k=0,l=(f.lines-1)*(1-f.direction)/2;k", $"", - $"", + //$"", $"", $"", $"", @@ -224,7 +224,9 @@ namespace PlexRequests.UI.Helpers sb.Append($""); sb.Append($""); sb.Append($""); - + sb.Append($""); + sb.Append($""); + sb.Append($""); return helper.Raw(sb.ToString()); } diff --git a/PlexRequests.UI/Views/UserManagement/Index.cshtml b/PlexRequests.UI/Views/UserManagement/Index.cshtml index eb01aa8c9..cfd0f12ec 100644 --- a/PlexRequests.UI/Views/UserManagement/Index.cshtml +++ b/PlexRequests.UI/Views/UserManagement/Index.cshtml @@ -3,11 +3,14 @@ @Html.LoadUserManagementAssets()
    + + Spinner Active: + {{spinnerActive}}