diff --git a/PlexRequests.Core/IRequestService.cs b/PlexRequests.Core/IRequestService.cs index fb68fab37..fc5187aed 100644 --- a/PlexRequests.Core/IRequestService.cs +++ b/PlexRequests.Core/IRequestService.cs @@ -33,7 +33,7 @@ namespace PlexRequests.Core public interface IRequestService { long AddRequest(RequestedModel model); - bool CheckRequest(int providerId); + RequestedModel CheckRequest(int providerId); void DeleteRequest(RequestedModel request); bool UpdateRequest(RequestedModel model); RequestedModel Get(int id); diff --git a/PlexRequests.Core/JsonRequestService.cs b/PlexRequests.Core/JsonRequestService.cs index 64033b50c..45eccb51e 100644 --- a/PlexRequests.Core/JsonRequestService.cs +++ b/PlexRequests.Core/JsonRequestService.cs @@ -58,10 +58,11 @@ namespace PlexRequests.Core return result ? id : -1; } - public bool CheckRequest(int providerId) + public RequestedModel CheckRequest(int providerId) { var blobs = Repo.GetAll(); - return blobs.Any(x => x.ProviderId == providerId); + var blob = blobs.FirstOrDefault(x => x.ProviderId == providerId); + return blob != null ? ByteConverterHelper.ReturnObject(blob.Content) : null; } public void DeleteRequest(RequestedModel request) diff --git a/PlexRequests.Core/SettingModels/PlexRequestSettings.cs b/PlexRequests.Core/SettingModels/PlexRequestSettings.cs index 9c2811d0d..76266cc07 100644 --- a/PlexRequests.Core/SettingModels/PlexRequestSettings.cs +++ b/PlexRequests.Core/SettingModels/PlexRequestSettings.cs @@ -24,6 +24,10 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // ************************************************************************/ #endregion +using Newtonsoft.Json; +using System; +using System.Collections.Generic; + namespace PlexRequests.Core.SettingModels { public class PlexRequestSettings : Settings @@ -36,6 +40,29 @@ namespace PlexRequests.Core.SettingModels public bool RequireMovieApproval { get; set; } public bool RequireTvShowApproval { get; set; } public bool RequireMusicApproval { get; set; } + public bool UsersCanViewOnlyOwnRequests { get; set; } public int WeeklyRequestLimit { get; set; } + public string NoApprovalUsers { get; set; } + + [JsonIgnore] + public List NoApprovalUserList + { + get + { + var users = new List(); + if (string.IsNullOrEmpty(NoApprovalUsers)) + { + return users; + } + + var splitUsers = NoApprovalUsers.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var user in splitUsers) + { + if (!string.IsNullOrWhiteSpace(user)) + users.Add(user.Trim()); + } + return users; + } + } } } diff --git a/PlexRequests.Services/Interfaces/INotification.cs b/PlexRequests.Services/Interfaces/INotification.cs index 14b09f0e9..2e4e55ea4 100644 --- a/PlexRequests.Services/Interfaces/INotification.cs +++ b/PlexRequests.Services/Interfaces/INotification.cs @@ -27,6 +27,7 @@ using System.Threading.Tasks; using PlexRequests.Services.Notification; +using PlexRequests.Core.SettingModels; namespace PlexRequests.Services.Interfaces { @@ -35,5 +36,7 @@ namespace PlexRequests.Services.Interfaces string NotificationName { get; } Task NotifyAsync(NotificationModel model); + + Task NotifyAsync(NotificationModel model, Settings settings); } } \ No newline at end of file diff --git a/PlexRequests.Services/Interfaces/INotificationService.cs b/PlexRequests.Services/Interfaces/INotificationService.cs index 59db3b509..91563c6de 100644 --- a/PlexRequests.Services/Interfaces/INotificationService.cs +++ b/PlexRequests.Services/Interfaces/INotificationService.cs @@ -27,12 +27,14 @@ using System.Threading.Tasks; using PlexRequests.Services.Notification; +using PlexRequests.Core.SettingModels; namespace PlexRequests.Services.Interfaces { public interface INotificationService { Task Publish(NotificationModel model); + Task Publish(NotificationModel model, Settings settings); void Subscribe(INotification notification); void UnSubscribe(INotification notification); diff --git a/PlexRequests.Services/Notification/EmailMessageNotification.cs b/PlexRequests.Services/Notification/EmailMessageNotification.cs index 472b6a069..4a359fb23 100644 --- a/PlexRequests.Services/Notification/EmailMessageNotification.cs +++ b/PlexRequests.Services/Notification/EmailMessageNotification.cs @@ -46,24 +46,29 @@ namespace PlexRequests.Services.Notification private static readonly Logger Log = LogManager.GetCurrentClassLogger(); private ISettingsService EmailNotificationSettings { get; } - private EmailNotificationSettings Settings => GetConfiguration(); public string NotificationName => "EmailMessageNotification"; public async Task NotifyAsync(NotificationModel model) { var configuration = GetConfiguration(); - if (!ValidateConfiguration(configuration)) - { - return; - } + await NotifyAsync(model, configuration); + } + + public async Task NotifyAsync(NotificationModel model, Settings settings) + { + if (settings == null) await NotifyAsync(model); + + var emailSettings = (EmailNotificationSettings)settings; + + if (!ValidateConfiguration(emailSettings)) return; switch (model.NotificationType) { case NotificationType.NewRequest: - await EmailNewRequest(model); + await EmailNewRequest(model, emailSettings); break; case NotificationType.Issue: - await EmailIssue(model); + await EmailIssue(model, emailSettings); break; case NotificationType.RequestAvailable: throw new NotImplementedException(); @@ -74,6 +79,10 @@ namespace PlexRequests.Services.Notification case NotificationType.AdminNote: throw new NotImplementedException(); + case NotificationType.Test: + await EmailTest(model, emailSettings); + break; + default: throw new ArgumentOutOfRangeException(); } @@ -100,23 +109,23 @@ namespace PlexRequests.Services.Notification return true; } - private async Task EmailNewRequest(NotificationModel model) + private async Task EmailNewRequest(NotificationModel model, EmailNotificationSettings settings) { var message = new MailMessage { IsBodyHtml = true, - To = { new MailAddress(Settings.RecipientEmail) }, + To = { new MailAddress(settings.RecipientEmail) }, Body = $"Hello! The user '{model.User}' has requested {model.Title}! Please log in to approve this request. Request Date: {model.DateTime.ToString("f")}", - From = new MailAddress(Settings.EmailSender), + From = new MailAddress(settings.EmailSender), Subject = $"Plex Requests: New request for {model.Title}!" }; try { - using (var smtp = new SmtpClient(Settings.EmailHost, Settings.EmailPort)) + using (var smtp = new SmtpClient(settings.EmailHost, settings.EmailPort)) { - smtp.Credentials = new NetworkCredential(Settings.EmailUsername, Settings.EmailPassword); - smtp.EnableSsl = Settings.Ssl; + smtp.Credentials = new NetworkCredential(settings.EmailUsername, settings.EmailPassword); + smtp.EnableSsl = settings.Ssl; await smtp.SendMailAsync(message).ConfigureAwait(false); } } @@ -130,23 +139,53 @@ namespace PlexRequests.Services.Notification } } - private async Task EmailIssue(NotificationModel model) + private async Task EmailIssue(NotificationModel model, EmailNotificationSettings settings) { var message = new MailMessage { IsBodyHtml = true, - To = { new MailAddress(Settings.RecipientEmail) }, + To = { new MailAddress(settings.RecipientEmail) }, Body = $"Hello! The user '{model.User}' has reported a new issue {model.Body} for the title {model.Title}!", - From = new MailAddress(Settings.RecipientEmail), + From = new MailAddress(settings.RecipientEmail), Subject = $"Plex Requests: New issue for {model.Title}!" }; try { - using (var smtp = new SmtpClient(Settings.EmailHost, Settings.EmailPort)) + using (var smtp = new SmtpClient(settings.EmailHost, settings.EmailPort)) { - smtp.Credentials = new NetworkCredential(Settings.EmailUsername, Settings.EmailPassword); - smtp.EnableSsl = Settings.Ssl; + smtp.Credentials = new NetworkCredential(settings.EmailUsername, settings.EmailPassword); + smtp.EnableSsl = settings.Ssl; + await smtp.SendMailAsync(message).ConfigureAwait(false); + } + } + catch (SmtpException smtp) + { + Log.Error(smtp); + } + catch (Exception e) + { + Log.Error(e); + } + } + + private async Task EmailTest(NotificationModel model, EmailNotificationSettings settings) + { + var message = new MailMessage + { + IsBodyHtml = true, + To = { new MailAddress(settings.RecipientEmail) }, + Body = "This is just a test! Success!", + From = new MailAddress(settings.RecipientEmail), + Subject = "Plex Requests: Test Message!" + }; + + try + { + using (var smtp = new SmtpClient(settings.EmailHost, settings.EmailPort)) + { + smtp.Credentials = new NetworkCredential(settings.EmailUsername, settings.EmailPassword); + smtp.EnableSsl = settings.Ssl; await smtp.SendMailAsync(message).ConfigureAwait(false); } } diff --git a/PlexRequests.Services/Notification/NotificationService.cs b/PlexRequests.Services/Notification/NotificationService.cs index 116f5aef9..35e52fd7d 100644 --- a/PlexRequests.Services/Notification/NotificationService.cs +++ b/PlexRequests.Services/Notification/NotificationService.cs @@ -32,6 +32,7 @@ using System.Threading.Tasks; using NLog; using PlexRequests.Services.Interfaces; +using PlexRequests.Core.SettingModels; namespace PlexRequests.Services.Notification { @@ -47,6 +48,13 @@ namespace PlexRequests.Services.Notification await Task.WhenAll(notificationTasks).ConfigureAwait(false); } + public async Task Publish(NotificationModel model, Settings settings) + { + var notificationTasks = Observers.Values.Select(notification => NotifyAsync(notification, model, settings)); + + await Task.WhenAll(notificationTasks).ConfigureAwait(false); + } + public void Subscribe(INotification notification) { Observers.TryAdd(notification.NotificationName, notification); @@ -67,6 +75,19 @@ namespace PlexRequests.Services.Notification { Log.Error(ex, $"Notification '{notification.NotificationName}' failed with exception"); } + + } + + private static async Task NotifyAsync(INotification notification, NotificationModel model, Settings settings) + { + try + { + await notification.NotifyAsync(model, settings).ConfigureAwait(false); + } + catch (Exception ex) + { + Log.Error(ex, $"Notification '{notification.NotificationName}' failed with exception"); + } } } } \ No newline at end of file diff --git a/PlexRequests.Services/Notification/NotificationType.cs b/PlexRequests.Services/Notification/NotificationType.cs index bf919fe39..22d0d29b1 100644 --- a/PlexRequests.Services/Notification/NotificationType.cs +++ b/PlexRequests.Services/Notification/NotificationType.cs @@ -33,5 +33,6 @@ namespace PlexRequests.Services.Notification RequestAvailable, RequestApproved, AdminNote, + Test } } diff --git a/PlexRequests.Services/Notification/PushbulletNotification.cs b/PlexRequests.Services/Notification/PushbulletNotification.cs index 4e2f02144..521855dca 100644 --- a/PlexRequests.Services/Notification/PushbulletNotification.cs +++ b/PlexRequests.Services/Notification/PushbulletNotification.cs @@ -51,18 +51,25 @@ namespace PlexRequests.Services.Notification public string NotificationName => "PushbulletNotification"; public async Task NotifyAsync(NotificationModel model) { - if (!ValidateConfiguration()) - { - return; - } + var configuration = GetSettings(); + await NotifyAsync(model, configuration); + } + + public async Task NotifyAsync(NotificationModel model, Settings settings) + { + if (settings == null) await NotifyAsync(model); + + var pushSettings = (PushbulletNotificationSettings)settings; + + if (!ValidateConfiguration(pushSettings)) return; switch (model.NotificationType) { case NotificationType.NewRequest: - await PushNewRequestAsync(model); + await PushNewRequestAsync(model, pushSettings); break; case NotificationType.Issue: - await PushIssueAsync(model); + await PushIssueAsync(model, pushSettings); break; case NotificationType.RequestAvailable: break; @@ -70,18 +77,21 @@ namespace PlexRequests.Services.Notification break; case NotificationType.AdminNote: break; + case NotificationType.Test: + await PushTestAsync(model, pushSettings); + break; default: throw new ArgumentOutOfRangeException(); } } - private bool ValidateConfiguration() + private bool ValidateConfiguration(PushbulletNotificationSettings settings) { - if (!Settings.Enabled) + if (!settings.Enabled) { return false; } - if (string.IsNullOrEmpty(Settings.AccessToken)) + if (string.IsNullOrEmpty(settings.AccessToken)) { return false; } @@ -93,13 +103,13 @@ namespace PlexRequests.Services.Notification return SettingsService.GetSettings(); } - private async Task PushNewRequestAsync(NotificationModel model) + private async Task PushNewRequestAsync(NotificationModel model, PushbulletNotificationSettings settings) { var message = $"{model.Title} has been requested by user: {model.User}"; var pushTitle = $"Plex Requests: {model.Title} has been requested!"; try { - var result = await PushbulletApi.PushAsync(Settings.AccessToken, pushTitle, message, Settings.DeviceIdentifier); + var result = await PushbulletApi.PushAsync(settings.AccessToken, pushTitle, message, settings.DeviceIdentifier); if (result == null) { Log.Error("Pushbullet api returned a null value, the notification did not get pushed"); @@ -111,13 +121,31 @@ namespace PlexRequests.Services.Notification } } - private async Task PushIssueAsync(NotificationModel model) + private async Task PushIssueAsync(NotificationModel model, PushbulletNotificationSettings settings) { var message = $"A new issue: {model.Body} has been reported by user: {model.User} for the title: {model.Title}"; var pushTitle = $"Plex Requests: A new issue has been reported for {model.Title}"; try { - var result = await PushbulletApi.PushAsync(Settings.AccessToken, pushTitle, message, Settings.DeviceIdentifier); + var result = await PushbulletApi.PushAsync(settings.AccessToken, pushTitle, message, settings.DeviceIdentifier); + if (result != null) + { + Log.Error("Pushbullet api returned a null value, the notification did not get pushed"); + } + } + catch (Exception e) + { + Log.Error(e); + } + } + + private async Task PushTestAsync(NotificationModel model, PushbulletNotificationSettings settings) + { + var message = "This is just a test! Success!"; + var pushTitle = "Plex Requests: Test Message!"; + try + { + var result = await PushbulletApi.PushAsync(settings.AccessToken, pushTitle, message, settings.DeviceIdentifier); if (result != null) { Log.Error("Pushbullet api returned a null value, the notification did not get pushed"); diff --git a/PlexRequests.Services/Notification/PushoverNotification.cs b/PlexRequests.Services/Notification/PushoverNotification.cs index bca0b2c90..47854b1d5 100644 --- a/PlexRequests.Services/Notification/PushoverNotification.cs +++ b/PlexRequests.Services/Notification/PushoverNotification.cs @@ -51,18 +51,25 @@ namespace PlexRequests.Services.Notification public string NotificationName => "PushoverNotification"; public async Task NotifyAsync(NotificationModel model) { - if (!ValidateConfiguration()) - { - return; - } + var configuration = GetSettings(); + await NotifyAsync(model, configuration); + } + + public async Task NotifyAsync(NotificationModel model, Settings settings) + { + if (settings == null) await NotifyAsync(model); + + var pushSettings = (PushoverNotificationSettings)settings; + + if (!ValidateConfiguration(pushSettings)) return; switch (model.NotificationType) { case NotificationType.NewRequest: - await PushNewRequestAsync(model); + await PushNewRequestAsync(model, pushSettings); break; case NotificationType.Issue: - await PushIssueAsync(model); + await PushIssueAsync(model, pushSettings); break; case NotificationType.RequestAvailable: break; @@ -70,18 +77,21 @@ namespace PlexRequests.Services.Notification break; case NotificationType.AdminNote: break; + case NotificationType.Test: + await PushTestAsync(model, pushSettings); + break; default: throw new ArgumentOutOfRangeException(); } } - private bool ValidateConfiguration() + private bool ValidateConfiguration(PushoverNotificationSettings settings) { - if (!Settings.Enabled) + if (!settings.Enabled) { return false; } - if (string.IsNullOrEmpty(Settings.AccessToken) || string.IsNullOrEmpty(Settings.UserToken)) + if (string.IsNullOrEmpty(settings.AccessToken) || string.IsNullOrEmpty(settings.UserToken)) { return false; } @@ -93,12 +103,12 @@ namespace PlexRequests.Services.Notification return SettingsService.GetSettings(); } - private async Task PushNewRequestAsync(NotificationModel model) + private async Task PushNewRequestAsync(NotificationModel model, PushoverNotificationSettings settings) { var message = $"Plex Requests: {model.Title} has been requested by user: {model.User}"; try { - var result = await PushoverApi.PushAsync(Settings.AccessToken, message, Settings.UserToken); + var result = await PushoverApi.PushAsync(settings.AccessToken, message, settings.UserToken); if (result?.status != 1) { Log.Error("Pushover api returned a status that was not 1, the notification did not get pushed"); @@ -110,12 +120,29 @@ namespace PlexRequests.Services.Notification } } - private async Task PushIssueAsync(NotificationModel model) + private async Task PushIssueAsync(NotificationModel model, PushoverNotificationSettings settings) { var message = $"Plex Requests: A new issue: {model.Body} has been reported by user: {model.User} for the title: {model.Title}"; try { - var result = await PushoverApi.PushAsync(Settings.AccessToken, message, Settings.UserToken); + var result = await PushoverApi.PushAsync(settings.AccessToken, message, settings.UserToken); + if (result?.status != 1) + { + Log.Error("Pushover api returned a status that was not 1, the notification did not get pushed"); + } + } + catch (Exception e) + { + Log.Error(e); + } + } + + private async Task PushTestAsync(NotificationModel model, PushoverNotificationSettings settings) + { + var message = $"Plex Requests: Test Message!"; + try + { + var result = await PushoverApi.PushAsync(settings.AccessToken, message, settings.UserToken); if (result?.status != 1) { Log.Error("Pushover api returned a status that was not 1, the notification did not get pushed"); diff --git a/PlexRequests.Store/RequestedModel.cs b/PlexRequests.Store/RequestedModel.cs index 836a2abfa..dcceeb1ab 100644 --- a/PlexRequests.Store/RequestedModel.cs +++ b/PlexRequests.Store/RequestedModel.cs @@ -2,12 +2,20 @@ using System.Security.Cryptography; using Dapper.Contrib.Extensions; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; namespace PlexRequests.Store { [Table("Requested")] public class RequestedModel : Entity { + public RequestedModel() + { + RequestedUsers = new List(); + } + // ReSharper disable once IdentifierTypo public int ProviderId { get; set; } public string ImdbId { get; set; } @@ -18,7 +26,10 @@ namespace PlexRequests.Store public RequestType Type { get; set; } public string Status { get; set; } public bool Approved { get; set; } + + [Obsolete("Use RequestedUsers")] public string RequestedBy { get; set; } + public DateTime RequestedDate { get; set; } public bool Available { get; set; } public IssueState Issues { get; set; } @@ -27,6 +38,40 @@ namespace PlexRequests.Store public int[] SeasonList { get; set; } public int SeasonCount { get; set; } public string SeasonsRequested { get; set; } + public List RequestedUsers { get; set; } + + [JsonIgnore] + public List AllUsers + { + get + { + var u = new List(); + if (!string.IsNullOrEmpty(RequestedBy)) + { + u.Add(RequestedBy); + } + + if (RequestedUsers.Any()) + { + u.AddRange(RequestedUsers.Where(requestedUser => requestedUser != RequestedBy)); + } + return u; + } + } + + [JsonIgnore] + public bool CanApprove + { + get + { + return !Approved && !Available; + } + } + + public bool UserHasRequested(string username) + { + return AllUsers.Any(x => x.Equals(username, StringComparison.OrdinalIgnoreCase)); + } } public enum RequestType diff --git a/PlexRequests.UI/Content/custom.css b/PlexRequests.UI/Content/custom.css index b7f9c9f33..3b8122ea7 100644 --- a/PlexRequests.UI/Content/custom.css +++ b/PlexRequests.UI/Content/custom.css @@ -22,7 +22,9 @@ .form-control-custom { background-color: #4e5d6c !important; - color: white !important; } + color: white !important; + border-radius: 0; + box-shadow: 0 0 0 !important; } h1 { font-size: 3.5rem !important; @@ -40,6 +42,18 @@ label { margin-bottom: 0.5rem !important; font-size: 16px !important; } +.nav-tabs > li.active > a, +.nav-tabs > li.active > a:hover, +.nav-tabs > li.active > a:focus { + background: #4e5d6c; } + +.navbar .nav a .fa { + font-size: 130%; + top: 1px; + position: relative; + display: inline-block; + margin-right: 5px; } + .btn-danger-outline { color: #d9534f !important; background-color: transparent; @@ -162,3 +176,14 @@ label { .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; } + diff --git a/PlexRequests.UI/Content/custom.min.css b/PlexRequests.UI/Content/custom.min.css index 72cc0ebe2..045ce1487 100644 --- a/PlexRequests.UI/Content/custom.min.css +++ b/PlexRequests.UI/Content/custom.min.css @@ -1 +1 @@ -@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.btn{border-radius:.25rem !important;}.multiSelect{background-color:#4e5d6c;}.form-control-custom{background-color:#4e5d6c !important;color:#fff !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;}.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 newline at end of file +@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.btn{border-radius:.25rem !important;}.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.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background:#4e5d6c;}.navbar .nav a .fa{font-size:130%;top:1px;position:relative;display:inline-block;margin-right:5px;}.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;} \ No newline at end of file diff --git a/PlexRequests.UI/Content/custom.scss b/PlexRequests.UI/Content/custom.scss index 2f6593c23..9b60351c7 100644 --- a/PlexRequests.UI/Content/custom.scss +++ b/PlexRequests.UI/Content/custom.scss @@ -6,7 +6,9 @@ $info-colour: #5bc0de; $warning-colour: #f0ad4e; $danger-colour: #d9534f; $success-colour: #5cb85c; -$i:!important; +$i: +!important +; @media (min-width: 768px ) { .row { @@ -43,6 +45,8 @@ $i:!important; .form-control-custom { background-color: $form-color $i; color: white $i; + border-radius: 0; + box-shadow: 0 0 0 !important; } @@ -66,6 +70,20 @@ label { font-size: 16px $i; } +.nav-tabs > li.active > a, +.nav-tabs > li.active > a:hover, +.nav-tabs > li.active > a:focus { + background: #4e5d6c; +} + +.navbar .nav a .fa { + font-size: 130%; + top: 1px; + position: relative; + display: inline-block; + margin-right: 5px; +} + .btn-danger-outline { color: $danger-colour $i; background-color: transparent; @@ -157,10 +175,11 @@ label { border-color: $success-colour $i; } -#movieList .mix{ +#movieList .mix { display: none; } -#tvList .mix{ + +#tvList .mix { display: none; } @@ -202,4 +221,18 @@ $border-radius: 10px; .scroll-top-wrapper i.fa { line-height: inherit; } - \ No newline at end of file + + +.no-search-results { + text-align: center; +} + +.no-search-results .no-search-results-icon { + font-size: 10em; + color: $form-color; +} + +.no-search-results .no-search-results-text { + margin: 20px 0; + color: #ccc; +} \ No newline at end of file diff --git a/PlexRequests.UI/Content/requests.js b/PlexRequests.UI/Content/requests.js index 3872da2c2..6b6d00572 100644 --- a/PlexRequests.UI/Content/requests.js +++ b/PlexRequests.UI/Content/requests.js @@ -9,68 +9,122 @@ var searchSource = $("#search-template").html(); var searchTemplate = Handlebars.compile(searchSource); var movieTimer = 0; var tvimer = 0; +var mixItUpDefault = { + animation: { enable: true }, + load: { + filter: 'all', + sort: 'requestorder:desc' + }, + layout: { + display: 'block' + }, + callbacks: { + onMixStart: function (state, futureState) { + $('.mix', this).removeAttr('data-bound').removeData('bound'); // fix for animation issues in other tabs + } + } +}; -movieLoad(); -tvLoad(); +initLoad(); $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { var target = $(e.target).attr('href'); var activeState = ""; - if (target === "#TvShowTab") { - if ($('#movieList').mixItUp('isLoaded')) { - activeState = $('#movieList').mixItUp('getState'); - $('#movieList').mixItUp('destroy'); - } - if (!$('#tvList').mixItUp('isLoaded')) { - $('#tvList').mixItUp({ - load: { - filter: activeState.activeFilter || 'all', - sort: activeState.activeSort || 'default:asc' - }, - layout: { - display: 'block' - } - }); + var $ml = $('#movieList'); + var $tvl = $('#tvList'); + + $('.approve-category').hide(); + if (target === "#TvShowTab") { + $('#approveTVShows').show(); + if ($ml.mixItUp('isLoaded')) { + activeState = $ml.mixItUp('getState'); + $ml.mixItUp('destroy'); } + if ($tvl.mixItUp('isLoaded')) $tvl.mixItUp('destroy'); + $tvl.mixItUp(mixItUpConfig(activeState)); // init or reinit } if (target === "#MoviesTab") { - if ($('#tvList').mixItUp('isLoaded')) { - activeState = $('#tvList').mixItUp('getState'); - $('#tvList').mixItUp('destroy'); - } - if (!$('#movieList').mixItUp('isLoaded')) { - $('#movieList').mixItUp({ - load: { - filter: activeState.activeFilter || 'all', - sort: activeState.activeSort || 'default:asc' - }, - layout: { - display: 'block' - } - }); + $('#approveMovies').show(); + if ($tvl.mixItUp('isLoaded')) { + activeState = $tvl.mixItUp('getState'); + $tvl.mixItUp('destroy'); } + if ($ml.mixItUp('isLoaded')) $ml.mixItUp('destroy'); + $ml.mixItUp(mixItUpConfig(activeState)); // init or reinit } + //$('.mix[data-bound]').removeAttr('data-bound'); }); // Approve all -$('#approveAll').click(function () { +$('#approveMovies').click(function (e) { + e.preventDefault(); + var buttonId = e.target.id; + var origHtml = $(this).html(); + + if ($('#' + buttonId).text() === " Loading...") { + return; + } + + loadingButton(buttonId, "success"); + $.ajax({ type: 'post', - url: '/approval/approveall', + url: '/approval/approveallmovies', dataType: "json", success: function (response) { if (checkJsonResponse(response)) { - generateNotify("Success! All requests approved!", "success"); + generateNotify("Success! All Movie requests approved!", "success"); + movieLoad(); } }, error: function (e) { console.log(e); generateNotify("Something went wrong!", "danger"); + }, + complete: function (e) { + finishLoading(buttonId, "success", origHtml) } }); }); +$('#approveTVShows').click(function (e) { + e.preventDefault(); + var buttonId = e.target.id; + var origHtml = $(this).html(); + + if ($('#' + buttonId).text() === " Loading...") { + return; + } + + loadingButton(buttonId, "success"); + + $.ajax({ + type: 'post', + url: '/approval/approvealltvshows', + dataType: "json", + success: function (response) { + if (checkJsonResponse(response)) { + generateNotify("Success! All TV Show requests approved!", "success"); + tvLoad(); + } + }, + error: function (e) { + console.log(e); + generateNotify("Something went wrong!", "danger"); + }, + complete: function (e) { + finishLoading(buttonId, "success", origHtml) + } + }); +}); + +// filtering/sorting +$('.filter,.sort', '.dropdown-menu').click(function (e) { + var $this = $(this); + $('.fa-square', $this.parents('.dropdown-menu:first')).removeClass('fa-square').addClass('fa-square-o'); + $this.children('.fa').first().removeClass('fa-square-o').addClass('fa-square'); +}); // Report Issue @@ -315,36 +369,54 @@ $(document).on("click", ".change", function (e) { }); +function mixItUpConfig(activeState) { + var conf = mixItUpDefault; + + if (activeState) { + if (activeState.activeFilter) conf['load']['filter'] = activeState.activeFilter; + if (activeState.activeSort) conf['load']['sort'] = activeState.activeSort; + } + return conf; +}; + +function initLoad() { + movieLoad(); + tvLoad(); +} + function movieLoad() { - $("#movieList").html(""); + var $ml = $('#movieList'); + if ($ml.mixItUp('isLoaded')) { + activeState = $ml.mixItUp('getState'); + $ml.mixItUp('destroy'); + } + $ml.html(""); $.ajax("/requests/movies/").success(function (results) { results.forEach(function (result) { var context = buildRequestContext(result, "movie"); - var html = searchTemplate(context); - $("#movieList").append(html); - }); - $('#movieList').mixItUp({ - layout: { - display: 'block' - }, - load: { - filter: 'all' - } + $ml.append(html); }); + $ml.mixItUp(mixItUpConfig()); }); }; function tvLoad() { - $("#tvList").html(""); + var $tvl = $('#tvList'); + if ($tvl.mixItUp('isLoaded')) { + activeState = $tvl.mixItUp('getState'); + $tvl.mixItUp('destroy'); + } + $tvl.html(""); $.ajax("/requests/tvshows/").success(function (results) { results.forEach(function (result) { var context = buildRequestContext(result, "tv"); var html = searchTemplate(context); - $("#tvList").append(html); + $tvl.append(html); }); + $tvl.mixItUp(mixItUpConfig()); }); }; @@ -359,9 +431,11 @@ function buildRequestContext(result, type) { type: type, status: result.status, releaseDate: result.releaseDate, + releaseDateTicks: result.releaseDateTicks, approved: result.approved, - requestedBy: result.requestedBy, + requestedUsers: result.requestedUsers ? result.requestedUsers.join(', ') : '', requestedDate: result.requestedDate, + requestedDateTicks: result.requestedDateTicks, available: result.available, admin: result.admin, issues: result.issues, @@ -373,16 +447,4 @@ function buildRequestContext(result, type) { }; return context; -} - -function startFilter(elementId) { - $('#'+element).mixItUp({ - load: { - filter: activeState.activeFilter || 'all', - sort: activeState.activeSort || 'default:asc' - }, - layout: { - display: 'block' - } - }); } \ No newline at end of file diff --git a/PlexRequests.UI/Content/search.js b/PlexRequests.UI/Content/search.js index cea4eca66..4d7b001bb 100644 --- a/PlexRequests.UI/Content/search.js +++ b/PlexRequests.UI/Content/search.js @@ -7,6 +7,8 @@ var searchSource = $("#search-template").html(); var searchTemplate = Handlebars.compile(searchSource); +var noResultsHtml = "
" + + "
Sorry, we didn't find any results!
"; var movieTimer = 0; var tvimer = 0; @@ -124,6 +126,9 @@ function movieSearch() { $("#movieList").append(html); }); } + else { + $("#movieList").html(noResultsHtml); + } $('#movieSearchButton').attr("class","fa fa-search"); }); }; @@ -140,6 +145,9 @@ function tvSearch() { $("#tvList").append(html); }); } + else { + $("#tvList").html(noResultsHtml); + } $('#tvSearchButton').attr("class", "fa fa-search"); }); }; diff --git a/PlexRequests.UI/Models/RequestViewModel.cs b/PlexRequests.UI/Models/RequestViewModel.cs index 011b9977d..292bc39df 100644 --- a/PlexRequests.UI/Models/RequestViewModel.cs +++ b/PlexRequests.UI/Models/RequestViewModel.cs @@ -37,11 +37,13 @@ namespace PlexRequests.UI.Models public string Title { get; set; } public string PosterPath { get; set; } public string ReleaseDate { get; set; } + public long ReleaseDateTicks { get; set; } public RequestType Type { get; set; } public string Status { get; set; } public bool Approved { get; set; } - public string RequestedBy { get; set; } + public string[] RequestedUsers { get; set; } public string RequestedDate { get; set; } + public long RequestedDateTicks { get; set; } public string ReleaseYear { get; set; } public bool Available { get; set; } public bool Admin { get; set; } diff --git a/PlexRequests.UI/Modules/AdminModule.cs b/PlexRequests.UI/Modules/AdminModule.cs index 9d5a21c99..41716ba2f 100644 --- a/PlexRequests.UI/Modules/AdminModule.cs +++ b/PlexRequests.UI/Modules/AdminModule.cs @@ -53,6 +53,7 @@ using PlexRequests.Store.Models; using PlexRequests.Store.Repository; using PlexRequests.UI.Helpers; using PlexRequests.UI.Models; +using System; namespace PlexRequests.UI.Modules { @@ -144,13 +145,16 @@ namespace PlexRequests.UI.Modules Get["/emailnotification"] = _ => EmailNotifications(); Post["/emailnotification"] = _ => SaveEmailNotifications(); + Post["/testemailnotification"] = _ => TestEmailNotifications(); Get["/status"] = _ => Status(); Get["/pushbulletnotification"] = _ => PushbulletNotifications(); Post["/pushbulletnotification"] = _ => SavePushbulletNotifications(); + Post["/testpushbulletnotification"] = _ => TestPushbulletNotifications(); Get["/pushovernotification"] = _ => PushoverNotifications(); Post["/pushovernotification"] = _ => SavePushoverNotifications(); + Post["/testpushovernotification"] = _ => TestPushoverNotifications(); Get["/logs"] = _ => Logs(); Get["/loglevel"] = _ => GetLogLevels(); @@ -380,6 +384,37 @@ namespace PlexRequests.UI.Modules return View["EmailNotifications", settings]; } + private Response TestEmailNotifications() + { + var settings = this.Bind(); + var valid = this.Validate(settings); + if (!valid.IsValid) + { + return Response.AsJson(valid.SendJsonError()); + } + var notificationModel = new NotificationModel + { + NotificationType = NotificationType.Test, + DateTime = DateTime.Now + }; + try + { + NotificationService.Subscribe(new EmailMessageNotification(EmailService)); + settings.Enabled = true; + NotificationService.Publish(notificationModel, settings); + Log.Info("Sent email notification test"); + } + catch (Exception) + { + Log.Error("Failed to subscribe and publish test Email Notification"); + } + finally + { + NotificationService.UnSubscribe(new EmailMessageNotification(EmailService)); + } + return Response.AsJson(new JsonResponseModel { Result = true, Message = "Successfully sent a test Email Notification!" }); + } + private Response SaveEmailNotifications() { var settings = this.Bind(); @@ -448,6 +483,37 @@ namespace PlexRequests.UI.Modules : new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." }); } + private Response TestPushbulletNotifications() + { + var settings = this.Bind(); + var valid = this.Validate(settings); + if (!valid.IsValid) + { + return Response.AsJson(valid.SendJsonError()); + } + var notificationModel = new NotificationModel + { + NotificationType = NotificationType.Test, + DateTime = DateTime.Now + }; + try + { + NotificationService.Subscribe(new PushbulletNotification(PushbulletApi, PushbulletService)); + settings.Enabled = true; + NotificationService.Publish(notificationModel, settings); + Log.Info("Sent pushbullet notification test"); + } + catch (Exception) + { + Log.Error("Failed to subscribe and publish test Pushbullet Notification"); + } + finally + { + NotificationService.UnSubscribe(new PushbulletNotification(PushbulletApi, PushbulletService)); + } + return Response.AsJson(new JsonResponseModel { Result = true, Message = "Successfully sent a test Pushbullet Notification!" }); + } + private Negotiator PushoverNotifications() { var settings = PushoverService.GetSettings(); @@ -480,6 +546,37 @@ namespace PlexRequests.UI.Modules : new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." }); } + private Response TestPushoverNotifications() + { + var settings = this.Bind(); + var valid = this.Validate(settings); + if (!valid.IsValid) + { + return Response.AsJson(valid.SendJsonError()); + } + var notificationModel = new NotificationModel + { + NotificationType = NotificationType.Test, + DateTime = DateTime.Now + }; + try + { + NotificationService.Subscribe(new PushoverNotification(PushoverApi, PushoverService)); + settings.Enabled = true; + NotificationService.Publish(notificationModel, settings); + Log.Info("Sent pushover notification test"); + } + catch (Exception) + { + Log.Error("Failed to subscribe and publish test Pushover Notification"); + } + finally + { + NotificationService.UnSubscribe(new PushoverNotification(PushoverApi, PushoverService)); + } + return Response.AsJson(new JsonResponseModel { Result = true, Message = "Successfully sent a test Pushover Notification!" }); + } + private Response GetCpProfiles() { var settings = this.Bind(); diff --git a/PlexRequests.UI/Modules/ApprovalModule.cs b/PlexRequests.UI/Modules/ApprovalModule.cs index 1e129e3b3..64d861c96 100644 --- a/PlexRequests.UI/Modules/ApprovalModule.cs +++ b/PlexRequests.UI/Modules/ApprovalModule.cs @@ -61,6 +61,8 @@ namespace PlexRequests.UI.Modules Post["/approve"] = parameters => Approve((int)Request.Form.requestid); Post["/approveall"] = x => ApproveAll(); + Post["/approveallmovies"] = x => ApproveAllMovies(); + Post["/approvealltvshows"] = x => ApproveAllTVShows(); } private IRequestService Service { get; } @@ -216,6 +218,56 @@ namespace PlexRequests.UI.Modules }); } + private Response ApproveAllMovies() + { + if (!Context.CurrentUser.IsAuthenticated()) + { + return Response.AsJson(new JsonResponseModel { Result = false, Message = "You are not an Admin, so you cannot approve any requests." }); + } + + var requests = Service.GetAll().Where(x => x.CanApprove && x.Type == RequestType.Movie); + var requestedModels = requests as RequestedModel[] ?? requests.ToArray(); + if (!requestedModels.Any()) + { + return Response.AsJson(new JsonResponseModel { Result = false, Message = "There are no movie requests to approve. Please refresh." }); + } + + try + { + return UpdateRequests(requestedModels); + } + catch (Exception e) + { + Log.Fatal(e); + return Response.AsJson(new JsonResponseModel { Result = false, Message = "Something bad happened, please check the logs!" }); + } + } + + private Response ApproveAllTVShows() + { + if (!Context.CurrentUser.IsAuthenticated()) + { + return Response.AsJson(new JsonResponseModel { Result = false, Message = "You are not an Admin, so you cannot approve any requests." }); + } + + var requests = Service.GetAll().Where(x => x.CanApprove && x.Type == RequestType.TvShow); + var requestedModels = requests as RequestedModel[] ?? requests.ToArray(); + if (!requestedModels.Any()) + { + return Response.AsJson(new JsonResponseModel { Result = false, Message = "There are no tv show requests to approve. Please refresh." }); + } + + try + { + return UpdateRequests(requestedModels); + } + catch (Exception e) + { + Log.Fatal(e); + return Response.AsJson(new JsonResponseModel { Result = false, Message = "Something bad happened, please check the logs!" }); + } + } + /// /// Approves all. /// @@ -227,23 +279,35 @@ namespace PlexRequests.UI.Modules return Response.AsJson(new JsonResponseModel { Result = false, Message = "You are not an Admin, so you cannot approve any requests." }); } - var requests = Service.GetAll().Where(x => x.Approved == false); + var requests = Service.GetAll().Where(x => x.CanApprove); var requestedModels = requests as RequestedModel[] ?? requests.ToArray(); if (!requestedModels.Any()) { return Response.AsJson(new JsonResponseModel { Result = false, Message = "There are no requests to approve. Please refresh." }); } + try + { + return UpdateRequests(requestedModels); + } + catch (Exception e) + { + Log.Fatal(e); + return Response.AsJson(new JsonResponseModel { Result = false, Message = "Something bad happened, please check the logs!" }); + } + + } + + private Response UpdateRequests(RequestedModel[] requestedModels) + { var cpSettings = CpService.GetSettings(); - - var updatedRequests = new List(); foreach (var r in requestedModels) { if (r.Type == RequestType.Movie) { - var result = SendMovie(cpSettings, r, CpApi); - if (result) + var res = SendMovie(cpSettings, r, CpApi); + if (res) { r.Approved = true; updatedRequests.Add(r); @@ -260,8 +324,8 @@ namespace PlexRequests.UI.Modules var sonarr = SonarrSettings.GetSettings(); if (sr.Enabled) { - var result = sender.SendToSickRage(sr, r); - if (result?.result == "success") + var res = sender.SendToSickRage(sr, r); + if (res?.result == "success") { r.Approved = true; updatedRequests.Add(r); @@ -269,14 +333,14 @@ namespace PlexRequests.UI.Modules else { Log.Error("Could not approve and send the TV {0} to SickRage!", r.Title); - Log.Error("SickRage Message: {0}", result?.message); + Log.Error("SickRage Message: {0}", res?.message); } } if (sonarr.Enabled) { - var result = sender.SendToSonarr(sonarr, r); - if (!string.IsNullOrEmpty(result?.title)) + var res = sender.SendToSonarr(sonarr, r); + if (!string.IsNullOrEmpty(res?.title)) { r.Approved = true; updatedRequests.Add(r); @@ -284,7 +348,7 @@ namespace PlexRequests.UI.Modules else { Log.Error("Could not approve and send the TV {0} to Sonarr!", r.Title); - Log.Error("Error message: {0}", result?.ErrorMessage); + Log.Error("Error message: {0}", res?.ErrorMessage); } } } @@ -292,17 +356,16 @@ namespace PlexRequests.UI.Modules try { - var result = Service.BatchUpdate(updatedRequests); return Response.AsJson(result + var result = Service.BatchUpdate(updatedRequests); + return Response.AsJson(result ? new JsonResponseModel { Result = true } : new JsonResponseModel { Result = false, Message = "We could not approve all of the requests. Please try again or check the logs." }); - - } + } catch (Exception e) { Log.Fatal(e); return Response.AsJson(new JsonResponseModel { Result = false, Message = "Something bad happened, please check the logs!" }); } - } private bool SendMovie(CouchPotatoSettings settings, RequestedModel r, ICouchPotatoApi cp) diff --git a/PlexRequests.UI/Modules/BaseModule.cs b/PlexRequests.UI/Modules/BaseModule.cs index 67dc1b553..f34c46f17 100644 --- a/PlexRequests.UI/Modules/BaseModule.cs +++ b/PlexRequests.UI/Modules/BaseModule.cs @@ -33,16 +33,30 @@ namespace PlexRequests.UI.Modules { public class BaseModule : NancyModule { + private string _username; + + protected string Username + { + get + { + if (string.IsNullOrEmpty(_username)) + { + _username = Session[SessionKeys.UsernameKey].ToString(); + } + return _username; + } + } + public BaseModule() { - Before += (ctx)=> CheckAuth(); + Before += (ctx) => CheckAuth(); } public BaseModule(string modulePath) : base(modulePath) { Before += (ctx) => CheckAuth(); } - + private Response CheckAuth() { diff --git a/PlexRequests.UI/Modules/RequestsModule.cs b/PlexRequests.UI/Modules/RequestsModule.cs index a951e0db8..b52e81475 100644 --- a/PlexRequests.UI/Modules/RequestsModule.cs +++ b/PlexRequests.UI/Modules/RequestsModule.cs @@ -79,28 +79,38 @@ namespace PlexRequests.UI.Modules private Response GetMovies() { + var settings = PrSettings.GetSettings(); var isAdmin = Context.CurrentUser.IsAuthenticated(); var dbMovies = Service.GetAll().Where(x => x.Type == RequestType.Movie); - var viewModel = dbMovies.Select(movie => new RequestViewModel + if (settings.UsersCanViewOnlyOwnRequests && !isAdmin) { - ProviderId = movie.ProviderId, - Type = movie.Type, - Status = movie.Status, - ImdbId = movie.ImdbId, - Id = movie.Id, - PosterPath = movie.PosterPath, - ReleaseDate = movie.ReleaseDate.Humanize(), - RequestedDate = movie.RequestedDate.Humanize(), - Approved = movie.Approved, - Title = movie.Title, - Overview = movie.Overview, - RequestedBy = movie.RequestedBy, - ReleaseYear = movie.ReleaseDate.Year.ToString(), - Available = movie.Available, - Admin = isAdmin, - Issues = movie.Issues.Humanize(LetterCasing.Title), - OtherMessage = movie.OtherMessage, - AdminNotes = movie.AdminNote + dbMovies = dbMovies.Where(x => x.UserHasRequested(Username)); + } + + var viewModel = dbMovies.Select(movie => { + return new RequestViewModel + { + ProviderId = movie.ProviderId, + Type = movie.Type, + Status = movie.Status, + ImdbId = movie.ImdbId, + Id = movie.Id, + PosterPath = movie.PosterPath, + ReleaseDate = movie.ReleaseDate.Humanize(), + ReleaseDateTicks = movie.ReleaseDate.Ticks, + RequestedDate = movie.RequestedDate.Humanize(), + RequestedDateTicks = movie.RequestedDate.Ticks, + Approved = movie.Available || movie.Approved, + Title = movie.Title, + Overview = movie.Overview, + RequestedUsers = isAdmin ? movie.AllUsers.ToArray() : new string[] { }, + ReleaseYear = movie.ReleaseDate.Year.ToString(), + Available = movie.Available, + Admin = isAdmin, + Issues = movie.Issues.Humanize(LetterCasing.Title), + OtherMessage = movie.OtherMessage, + AdminNotes = movie.AdminNote + }; }).ToList(); return Response.AsJson(viewModel); @@ -108,29 +118,39 @@ namespace PlexRequests.UI.Modules private Response GetTvShows() { + var settings = PrSettings.GetSettings(); var isAdmin = Context.CurrentUser.IsAuthenticated(); var dbTv = Service.GetAll().Where(x => x.Type == RequestType.TvShow); - var viewModel = dbTv.Select(tv => new RequestViewModel + if (settings.UsersCanViewOnlyOwnRequests && !isAdmin) { - ProviderId = tv.ProviderId, - Type = tv.Type, - Status = tv.Status, - ImdbId = tv.ImdbId, - Id = tv.Id, - PosterPath = tv.PosterPath, - ReleaseDate = tv.ReleaseDate.Humanize(), - RequestedDate = tv.RequestedDate.Humanize(), - Approved = tv.Approved, - Title = tv.Title, - Overview = tv.Overview, - RequestedBy = tv.RequestedBy, - ReleaseYear = tv.ReleaseDate.Year.ToString(), - Available = tv.Available, - Admin = isAdmin, - Issues = tv.Issues.Humanize(LetterCasing.Title), - OtherMessage = tv.OtherMessage, - AdminNotes = tv.AdminNote, - TvSeriesRequestType = tv.SeasonsRequested + dbTv = dbTv.Where(x => x.UserHasRequested(Username)); + } + + var viewModel = dbTv.Select(tv => { + return new RequestViewModel + { + ProviderId = tv.ProviderId, + Type = tv.Type, + Status = tv.Status, + ImdbId = tv.ImdbId, + Id = tv.Id, + PosterPath = tv.PosterPath, + ReleaseDate = tv.ReleaseDate.Humanize(), + ReleaseDateTicks = tv.ReleaseDate.Ticks, + RequestedDate = tv.RequestedDate.Humanize(), + RequestedDateTicks = tv.RequestedDate.Ticks, + Approved = tv.Available || tv.Approved, + Title = tv.Title, + Overview = tv.Overview, + RequestedUsers = isAdmin ? tv.AllUsers.ToArray() : new string[] { }, + ReleaseYear = tv.ReleaseDate.Year.ToString(), + Available = tv.Available, + Admin = isAdmin, + Issues = tv.Issues.Humanize(LetterCasing.Title), + OtherMessage = tv.OtherMessage, + AdminNotes = tv.AdminNote, + TvSeriesRequestType = tv.SeasonsRequested + }; }).ToList(); return Response.AsJson(viewModel); @@ -165,7 +185,7 @@ namespace PlexRequests.UI.Modules } originalRequest.Issues = issue; originalRequest.OtherMessage = !string.IsNullOrEmpty(comment) - ? $"{Session[SessionKeys.UsernameKey]} - {comment}" + ? $"{Username} - {comment}" : string.Empty; @@ -173,7 +193,7 @@ namespace PlexRequests.UI.Modules var model = new NotificationModel { - User = Session[SessionKeys.UsernameKey].ToString(), + User = Username, NotificationType = NotificationType.Issue, Title = originalRequest.Title, DateTime = DateTime.Now, diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index 9b307f983..6f566217a 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -179,11 +179,20 @@ namespace PlexRequests.UI.Modules Log.Trace(movieInfo.DumpJson); //#if !DEBUG + var settings = PrService.GetSettings(); + + // check if the movie has already been requested Log.Info("Requesting movie with id {0}", movieId); - if (RequestService.CheckRequest(movieId)) + var existingRequest = RequestService.CheckRequest(movieId); + if (existingRequest != null) { - Log.Trace("movie with id {0} exists", movieId); - return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullMovieName} has already been requested!" }); + // check if the current user is already marked as a requester for this movie, if not, add them + if (!existingRequest.UserHasRequested(Username)) + { + existingRequest.RequestedUsers.Add(Username); + RequestService.UpdateRequest(existingRequest); + } + return Response.AsJson(new JsonResponseModel { Result = false, Message = settings.UsersCanViewOnlyOwnRequests ? $"{fullMovieName} was successfully added!" : $"{fullMovieName} has already been requested!" }); } Log.Debug("movie with id {0} doesnt exists", movieId); @@ -213,14 +222,12 @@ namespace PlexRequests.UI.Modules Status = movieInfo.Status, RequestedDate = DateTime.Now, Approved = false, - RequestedBy = Session[SessionKeys.UsernameKey].ToString(), + RequestedUsers = new List() { Username }, Issues = IssueState.None, }; - - var settings = PrService.GetSettings(); Log.Trace(settings.DumpJson()); - if (!settings.RequireMovieApproval) + if (!settings.RequireMovieApproval || settings.NoApprovalUserList.Any(x => x.Equals(Username, StringComparison.OrdinalIgnoreCase))) { var cpSettings = CpService.GetSettings(); @@ -247,7 +254,7 @@ namespace PlexRequests.UI.Modules }; NotificationService.Publish(notificationModel); - return Response.AsJson(new JsonResponseModel {Result = true}); + return Response.AsJson(new JsonResponseModel {Result = true, Message = $"{fullMovieName} was successfully added!" }); } return Response.AsJson(new JsonResponseModel @@ -272,7 +279,7 @@ namespace PlexRequests.UI.Modules }; NotificationService.Publish(notificationModel); - return Response.AsJson(new JsonResponseModel { Result = true }); + return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullMovieName} was successfully added!" }); } } @@ -310,9 +317,20 @@ namespace PlexRequests.UI.Modules string fullShowName = $"{showInfo.name} ({firstAir.Year})"; //#if !DEBUG - if (RequestService.CheckRequest(showId)) + var settings = PrService.GetSettings(); + + // check if the show has already been requested + Log.Info("Requesting tv show with id {0}", showId); + var existingRequest = RequestService.CheckRequest(showId); + if (existingRequest != null) { - return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullShowName} has already been requested!" }); + // check if the current user is already marked as a requester for this show, if not, add them + if (!existingRequest.UserHasRequested(Username)) + { + existingRequest.RequestedUsers.Add(Username); + RequestService.UpdateRequest(existingRequest); + } + return Response.AsJson(new JsonResponseModel { Result = false, Message = settings.UsersCanViewOnlyOwnRequests ? $"{fullShowName} was successfully added!" : $"{fullShowName} has already been requested!" }); } try @@ -340,7 +358,7 @@ namespace PlexRequests.UI.Modules Status = showInfo.status, RequestedDate = DateTime.Now, Approved = false, - RequestedBy = Session[SessionKeys.UsernameKey].ToString(), + RequestedUsers = new List() { Username }, Issues = IssueState.None, ImdbId = showInfo.externals?.imdb ?? string.Empty, SeasonCount = showInfo.seasonCount @@ -363,8 +381,7 @@ namespace PlexRequests.UI.Modules model.SeasonList = seasonsList.ToArray(); - var settings = PrService.GetSettings(); - if (!settings.RequireTvShowApproval) + if (!settings.RequireTvShowApproval || settings.NoApprovalUserList.Any(x => x.Equals(Username, StringComparison.OrdinalIgnoreCase))) { var sonarrSettings = SonarrService.GetSettings(); var sender = new TvSender(SonarrApi, SickrageApi); diff --git a/PlexRequests.UI/Program.cs b/PlexRequests.UI/Program.cs index 5f07f7436..c8f863f40 100644 --- a/PlexRequests.UI/Program.cs +++ b/PlexRequests.UI/Program.cs @@ -69,7 +69,7 @@ namespace PlexRequests.UI if (port == -1) port = GetStartupPort(); - var options = new StartOptions( $"http://+:{port}") + var options = new StartOptions(Debugger.IsAttached ? $"http://localhost:{port}" : $"http://+:{port}") { ServerFactory = "Microsoft.Owin.Host.HttpListener" }; diff --git a/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml b/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml index 41a667e51..86f5e2322 100644 --- a/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml +++ b/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml @@ -88,6 +88,11 @@ +
+
+ +
+
@@ -128,7 +133,32 @@ }); }); - + $('#testEmail').click(function (e) { + e.preventDefault(); + var port = $('#EmailPort').val(); + if (isNaN(port)) { + generateNotify("You must specify a valid Port.", "warning"); + return; + } + var $form = $("#mainForm"); + $.ajax({ + type: $form.prop("method"), + data: $form.serialize(), + url: '/admin/testemailnotification', + dataType: "json", + success: function (response) { + if (response.result === true) { + generateNotify(response.message, "success"); + } else { + generateNotify(response.message, "warning"); + } + }, + error: function (e) { + console.log(e); + generateNotify("Something went wrong!", "danger"); + } + }); + }); }); diff --git a/PlexRequests.UI/Views/Admin/PushbulletNotifications.cshtml b/PlexRequests.UI/Views/Admin/PushbulletNotifications.cshtml index 4f658da53..73d28d87c 100644 --- a/PlexRequests.UI/Views/Admin/PushbulletNotifications.cshtml +++ b/PlexRequests.UI/Views/Admin/PushbulletNotifications.cshtml @@ -36,6 +36,12 @@
+
+
+ +
+
+
@@ -70,5 +76,28 @@ } }); }); + + $('#testPushbullet').click(function (e) { + e.preventDefault(); + + var $form = $("#mainForm"); + $.ajax({ + type: $form.prop("method"), + data: $form.serialize(), + url: '/admin/testpushbulletnotification', + dataType: "json", + success: function (response) { + if (response.result === true) { + generateNotify(response.message, "success"); + } else { + generateNotify(response.message, "warning"); + } + }, + error: function (e) { + console.log(e); + generateNotify("Something went wrong!", "danger"); + } + }); + }); }); \ No newline at end of file diff --git a/PlexRequests.UI/Views/Admin/PushoverNotifications.cshtml b/PlexRequests.UI/Views/Admin/PushoverNotifications.cshtml index 0877739d0..b5fcab7f5 100644 --- a/PlexRequests.UI/Views/Admin/PushoverNotifications.cshtml +++ b/PlexRequests.UI/Views/Admin/PushoverNotifications.cshtml @@ -36,6 +36,12 @@
+
+
+ +
+
+
@@ -70,5 +76,28 @@ } }); }); + + $('#testPushover').click(function (e) { + e.preventDefault(); + + var $form = $("#mainForm"); + $.ajax({ + type: $form.prop("method"), + data: $form.serialize(), + url: '/admin/testpushovernotification', + dataType: "json", + success: function (response) { + if (response.result === true) { + generateNotify(response.message, "success"); + } else { + generateNotify(response.message, "warning"); + } + }, + error: function (e) { + console.log(e); + generateNotify("Something went wrong!", "danger"); + } + }); + }); }); \ No newline at end of file diff --git a/PlexRequests.UI/Views/Admin/Settings.cshtml b/PlexRequests.UI/Views/Admin/Settings.cshtml index a7ed52268..d8e08431e 100644 --- a/PlexRequests.UI/Views/Admin/Settings.cshtml +++ b/PlexRequests.UI/Views/Admin/Settings.cshtml @@ -111,7 +111,31 @@
- + +

A comma separated list of users whose requests do not require approval.

+
+ +
+ +
+
+ +
+
+ + +
+
+ @*
@@ -131,4 +155,3 @@
- diff --git a/PlexRequests.UI/Views/Login/Index.cshtml b/PlexRequests.UI/Views/Login/Index.cshtml index 6ca2e444c..85597f544 100644 --- a/PlexRequests.UI/Views/Login/Index.cshtml +++ b/PlexRequests.UI/Views/Login/Index.cshtml @@ -4,7 +4,7 @@ Password
Remember Me -
+

@if (!Model.AdminExists) diff --git a/PlexRequests.UI/Views/Requests/Index.cshtml b/PlexRequests.UI/Views/Requests/Index.cshtml index cd73d64d9..a206b383c 100644 --- a/PlexRequests.UI/Views/Requests/Index.cshtml +++ b/PlexRequests.UI/Views/Requests/Index.cshtml @@ -2,12 +2,8 @@

Requests

Below you can see yours and all other requests, as well as their download and approval status.

- @if (Context.CurrentUser.IsAuthenticated()) - { - -
-
- } +
+ +
- -
- - +
+
+
+
+ @if (Context.CurrentUser.IsAuthenticated()) + { + @if (Model.SearchForMovies) + { + + } + @if (Model.SearchForTvShows) + { + + } + } +
+ + +
+
@if (Model.SearchForMovies) - { + {
-
-
+
+
@@ -61,12 +79,12 @@ } @if (Model.SearchForTvShows) - { + {
-
-
+
+
@@ -78,7 +96,7 @@