diff --git a/PlexRequests.Api/ApiRequest.cs b/PlexRequests.Api/ApiRequest.cs index 704880f9b..8cb023ea6 100644 --- a/PlexRequests.Api/ApiRequest.cs +++ b/PlexRequests.Api/ApiRequest.cs @@ -63,6 +63,7 @@ namespace PlexRequests.Api Log.Trace("Api Content Response:"); Log.Trace(response.Content); + if (response.ErrorException != null) { var message = "Error retrieving response. Check inner details for more info."; diff --git a/PlexRequests.Core/PlexRequests.Core.csproj b/PlexRequests.Core/PlexRequests.Core.csproj index e4448d873..2e2d1b3a1 100644 --- a/PlexRequests.Core/PlexRequests.Core.csproj +++ b/PlexRequests.Core/PlexRequests.Core.csproj @@ -86,6 +86,7 @@ <Compile Include="SettingModels\HeadphonesSettings.cs" /> <Compile Include="SettingModels\LandingPageSettings.cs" /> <Compile Include="SettingModels\NotificationSettings.cs" /> + <Compile Include="SettingModels\RequestSettings.cs" /> <Compile Include="SettingModels\ScheduledJobsSettings.cs" /> <Compile Include="SettingModels\SlackNotificationSettings.cs" /> <Compile Include="SettingModels\PushoverNotificationSettings.cs" /> diff --git a/PlexRequests.Core/SettingModels/PlexSettings.cs b/PlexRequests.Core/SettingModels/PlexSettings.cs index e83e61f4d..09be5fb15 100644 --- a/PlexRequests.Core/SettingModels/PlexSettings.cs +++ b/PlexRequests.Core/SettingModels/PlexSettings.cs @@ -24,6 +24,9 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // ************************************************************************/ #endregion + +using Newtonsoft.Json; + namespace PlexRequests.Core.SettingModels { public sealed class PlexSettings : ExternalSettings @@ -36,5 +39,6 @@ namespace PlexRequests.Core.SettingModels public bool EnableTvEpisodeSearching { get; set; } public string PlexAuthToken { get; set; } + public string MachineIdentifier { get; set; } } } \ No newline at end of file diff --git a/PlexRequests.Core/SettingModels/RequestSettings.cs b/PlexRequests.Core/SettingModels/RequestSettings.cs new file mode 100644 index 000000000..5a9140947 --- /dev/null +++ b/PlexRequests.Core/SettingModels/RequestSettings.cs @@ -0,0 +1,57 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: RequestSettings.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; + +namespace PlexRequests.Core.SettingModels +{ + public sealed class RequestSettings : Settings + { + public OrderType Order { get; set; } + public List<FilterType> Filters { get; set; } + } + + public enum OrderType + { + NewRequests, + OldRequests, + NewReleases, + OldReleases + } + + public enum FilterType + { + // ALL is not here, it's managed in the angular controller + Approved, + NotApproved, + Available, + NotAvailable, + Released, + NotReleased + } +} \ No newline at end of file diff --git a/PlexRequests.Core/Setup.cs b/PlexRequests.Core/Setup.cs index 50b27dd2d..dac15a731 100644 --- a/PlexRequests.Core/Setup.cs +++ b/PlexRequests.Core/Setup.cs @@ -26,6 +26,7 @@ #endregion using System; +using System.Linq; using System.Text.RegularExpressions; using Mono.Data.Sqlite; @@ -66,6 +67,11 @@ namespace PlexRequests.Core { MigrateToVersion1900(); } + + if(version > 1899 && version <= 1910) + { + MigrateToVersion1910(); + } } return Db.DbConnection().ConnectionString; @@ -244,5 +250,30 @@ namespace PlexRequests.Core Log.Error(e); } } + + /// <summary> + /// Migrates to version1910. + /// </summary> + public void MigrateToVersion1910() + { + try + { + // Get the new machine Identifier + var settings = new SettingsServiceV2<PlexSettings>(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.Core/UserMapper.cs b/PlexRequests.Core/UserMapper.cs index 5be4a868a..3c15d548e 100644 --- a/PlexRequests.Core/UserMapper.cs +++ b/PlexRequests.Core/UserMapper.cs @@ -99,7 +99,7 @@ namespace PlexRequests.Core return users.Any(); } - private Guid? CreateUser(string username, string password, string[] claims = default(string[]), UserProperties properties = null) + public Guid? CreateUser(string username, string password, string[] claims = default(string[]), UserProperties properties = null) { var salt = PasswordHasher.GenerateSalt(); @@ -134,6 +134,13 @@ namespace PlexRequests.Core return CreateUser(username, password, new[] { UserClaims.User }, properties); } + + public IEnumerable<string> GetAllClaims() + { + var properties = typeof(UserClaims).GetConstantsValues<string>(); + return properties; + } + public bool UpdatePassword(string username, string oldPassword, string newPassword) { var users = Repo.GetAll(); @@ -175,6 +182,8 @@ namespace PlexRequests.Core public interface ICustomUserMapper { + Guid? CreateUser(string username, string password, string[] claims, UserProperties props); + IEnumerable<string> GetAllClaims(); IEnumerable<UsersModel> GetUsers(); Task<IEnumerable<UsersModel>> GetUsersAsync(); UsersModel GetUser(Guid userId); diff --git a/PlexRequests.Helpers.Tests/PlexHelperTests.cs b/PlexRequests.Helpers.Tests/PlexHelperTests.cs index 6f09d6c39..33a1bf1a3 100644 --- a/PlexRequests.Helpers.Tests/PlexHelperTests.cs +++ b/PlexRequests.Helpers.Tests/PlexHelperTests.cs @@ -61,6 +61,12 @@ namespace PlexRequests.Helpers.Tests return PlexHelper.GetSeasonNumberFromTitle(title); } + [TestCaseSource(nameof(MediaUrls))] + public string GetPlexMediaUrlTest(string machineId, string mediaId) + { + return PlexHelper.GetPlexMediaUrl(machineId, mediaId); + } + private static IEnumerable<TestCaseData> PlexGuids { get @@ -75,6 +81,15 @@ namespace PlexRequests.Helpers.Tests } } + private static IEnumerable<TestCaseData> MediaUrls + { + get + { + yield return new TestCaseData("abcd","99").Returns("https://app.plex.tv/web/app#!/server/abcd/details/%2Flibrary%2Fmetadata%2F99").SetName("Test 1"); + yield return new TestCaseData("a54d1db669799308cd704b791f331eca6648b952", "51").Returns("https://app.plex.tv/web/app#!/server/a54d1db669799308cd704b791f331eca6648b952/details/%2Flibrary%2Fmetadata%2F51").SetName("Test 2"); + } + } + private static IEnumerable<TestCaseData> SeasonNumbers { get diff --git a/PlexRequests.Helpers.Tests/TypeHelperTests.cs b/PlexRequests.Helpers.Tests/TypeHelperTests.cs index 295b955ad..1d93c375f 100644 --- a/PlexRequests.Helpers.Tests/TypeHelperTests.cs +++ b/PlexRequests.Helpers.Tests/TypeHelperTests.cs @@ -26,7 +26,7 @@ #endregion using System; using System.Collections.Generic; - +using System.Linq; using NUnit.Framework; using PlexRequests.Store; @@ -42,6 +42,14 @@ namespace PlexRequests.Helpers.Tests return input.GetPropertyNames(); } + [Test] + public void GetConstantsTest() + { + var consts = typeof(UserClaims).GetConstantsValues<string>(); + Assert.That(consts.Contains("Admin"),Is.True); + Assert.That(consts.Contains("PowerUser"),Is.True); + Assert.That(consts.Contains("User"),Is.True); + } private static IEnumerable<TestCaseData> TypeData { diff --git a/PlexRequests.Helpers/PlexHelper.cs b/PlexRequests.Helpers/PlexHelper.cs index baafcd451..1e186ddba 100644 --- a/PlexRequests.Helpers/PlexHelper.cs +++ b/PlexRequests.Helpers/PlexHelper.cs @@ -95,6 +95,13 @@ namespace PlexRequests.Helpers return 0; } + + public static string GetPlexMediaUrl(string machineId, string mediaId) + { + var url = + $"https://app.plex.tv/web/app#!/server/{machineId}/details/%2Flibrary%2Fmetadata%2F{mediaId}"; + return url; + } } public class EpisodeModelHelper diff --git a/PlexRequests.Helpers/TypeHelper.cs b/PlexRequests.Helpers/TypeHelper.cs index 27a108bef..77eae6360 100644 --- a/PlexRequests.Helpers/TypeHelper.cs +++ b/PlexRequests.Helpers/TypeHelper.cs @@ -25,7 +25,9 @@ // ************************************************************************/ #endregion using System; +using System.Collections.Generic; using System.Linq; +using System.Reflection; namespace PlexRequests.Helpers { @@ -35,5 +37,19 @@ namespace PlexRequests.Helpers { return t.GetProperties().Select(x => x.Name).ToArray(); } + + public static IEnumerable<FieldInfo> GetConstants(this Type type) + { + var fieldInfos = type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy); + + return fieldInfos.Where(fi => fi.IsLiteral && !fi.IsInitOnly); + } + + public static IEnumerable<T> GetConstantsValues<T>(this Type type) where T : class + { + var fieldInfos = GetConstants(type); + + return fieldInfos.Select(fi => fi.GetRawConstantValue() as T); + } } } \ No newline at end of file diff --git a/PlexRequests.Services.Tests/PlexAvailabilityCheckerTests.cs b/PlexRequests.Services.Tests/PlexAvailabilityCheckerTests.cs index 5b8ba361f..801af7509 100644 --- a/PlexRequests.Services.Tests/PlexAvailabilityCheckerTests.cs +++ b/PlexRequests.Services.Tests/PlexAvailabilityCheckerTests.cs @@ -42,6 +42,7 @@ 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; @@ -63,6 +64,11 @@ namespace PlexRequests.Services.Tests private Mock<IJobRecord> JobRec { get; set; } private Mock<IRepository<UsersToNotify>> NotifyUsers { get; set; } private Mock<IRepository<PlexEpisodes>> PlexEpisodes { get; set; } + private Mock<INotificationEngine> Engine + { + get; + set; + } [SetUp] public void Setup() @@ -76,7 +82,8 @@ namespace PlexRequests.Services.Tests NotifyUsers = new Mock<IRepository<UsersToNotify>>(); PlexEpisodes = new Mock<IRepository<PlexEpisodes>>(); JobRec = new Mock<IJobRecord>(); - Checker = new PlexAvailabilityChecker(SettingsMock.Object, RequestMock.Object, PlexMock.Object, CacheMock.Object, NotificationMock.Object, JobRec.Object, NotifyUsers.Object, PlexEpisodes.Object); + Engine = new Mock<INotificationEngine>(); + Checker = new PlexAvailabilityChecker(SettingsMock.Object, RequestMock.Object, PlexMock.Object, CacheMock.Object, NotificationMock.Object, JobRec.Object, NotifyUsers.Object, PlexEpisodes.Object, Engine.Object); } @@ -212,8 +219,7 @@ namespace PlexRequests.Services.Tests new PlexEpisodes {EpisodeNumber = 1, ShowTitle = "The Flash",ProviderId = 23.ToString(), SeasonNumber = 1, EpisodeTitle = "Pilot"} }; PlexEpisodes.Setup(x => x.Custom(It.IsAny<Func<IDbConnection, IEnumerable<PlexEpisodes>>>())).Returns(expected); - Checker = new PlexAvailabilityChecker(SettingsMock.Object, RequestMock.Object, PlexMock.Object, CacheMock.Object, NotificationMock.Object, JobRec.Object, NotifyUsers.Object, PlexEpisodes.Object); - + var result = Checker.IsEpisodeAvailable(providerId, season, episode); return result; @@ -242,6 +248,7 @@ namespace PlexRequests.Services.Tests } }); CacheMock.Setup(x => x.Get<List<PlexSearch>>(CacheKeys.PlexLibaries)).Returns(cachedMovies); + SettingsMock.Setup(x => x.GetSettings()).Returns(F.Create<PlexSettings>()); var movies = Checker.GetPlexMovies(); Assert.That(movies.Any(x => x.ProviderId == "1212")); @@ -258,6 +265,7 @@ namespace PlexRequests.Services.Tests new Directory1 {Type = "show", Title = "title1", Year = "2016", ProviderId = "1212", Seasons = new List<Directory1>()} } }); + SettingsMock.Setup(x => x.GetSettings()).Returns(F.Create<PlexSettings>()); CacheMock.Setup(x => x.Get<List<PlexSearch>>(CacheKeys.PlexLibaries)).Returns(cachedTv); var movies = Checker.GetPlexTvShows(); @@ -268,8 +276,6 @@ namespace PlexRequests.Services.Tests public async Task GetAllPlexEpisodes() { PlexEpisodes.Setup(x => x.GetAllAsync()).ReturnsAsync(F.CreateMany<PlexEpisodes>().ToList()); - Checker = new PlexAvailabilityChecker(SettingsMock.Object, RequestMock.Object, PlexMock.Object, CacheMock.Object, NotificationMock.Object, JobRec.Object, NotifyUsers.Object, PlexEpisodes.Object); - var episodes = await Checker.GetEpisodes(); Assert.That(episodes.Count(), Is.GreaterThan(0)); diff --git a/PlexRequests.Services/Interfaces/IAvailabilityChecker.cs b/PlexRequests.Services/Interfaces/IAvailabilityChecker.cs index 8992e6545..d966e2b5f 100644 --- a/PlexRequests.Services/Interfaces/IAvailabilityChecker.cs +++ b/PlexRequests.Services/Interfaces/IAvailabilityChecker.cs @@ -42,6 +42,9 @@ namespace PlexRequests.Services.Interfaces List<PlexAlbum> GetPlexAlbums(); bool IsAlbumAvailable(PlexAlbum[] plexAlbums, string title, string year, string artist); bool IsEpisodeAvailable(string theTvDbId, int season, int episode); + PlexAlbum GetAlbum(PlexAlbum[] plexAlbums, string title, string year, string artist); + PlexMovie GetMovie(PlexMovie[] plexMovies, string title, string year, string providerId = null); + PlexTvShow GetTvShow(PlexTvShow[] plexShows, string title, string year, string providerId = null, int[] seasons = null); /// <summary> /// Gets the episode's stored in the cache. /// </summary> diff --git a/PlexRequests.Services/Interfaces/INotificationEngine.cs b/PlexRequests.Services/Interfaces/INotificationEngine.cs new file mode 100644 index 000000000..ba53eb9bf --- /dev/null +++ b/PlexRequests.Services/Interfaces/INotificationEngine.cs @@ -0,0 +1,39 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: INotificationEngine.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.Store; + +namespace PlexRequests.Services.Interfaces +{ + public interface INotificationEngine + { + Task NotifyUsers(IEnumerable<RequestedModel> modelChanged, string apiKey); + Task NotifyUsers(RequestedModel modelChanged, string apiKey); + } +} \ No newline at end of file diff --git a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs index e58a18c17..c8b46b32f 100644 --- a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs +++ b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs @@ -53,7 +53,7 @@ namespace PlexRequests.Services.Jobs public class PlexAvailabilityChecker : IJob, IAvailabilityChecker { public PlexAvailabilityChecker(ISettingsService<PlexSettings> plexSettings, IRequestService request, IPlexApi plex, ICacheProvider cache, - INotificationService notify, IJobRecord rec, IRepository<UsersToNotify> users, IRepository<PlexEpisodes> repo) + INotificationService notify, IJobRecord rec, IRepository<UsersToNotify> users, IRepository<PlexEpisodes> repo, INotificationEngine e) { Plex = plexSettings; RequestService = request; @@ -63,6 +63,7 @@ namespace PlexRequests.Services.Jobs Job = rec; UserNotifyRepo = users; EpisodeRepo = repo; + NotificationEngine = e; } private ISettingsService<PlexSettings> Plex { get; } @@ -74,6 +75,9 @@ namespace PlexRequests.Services.Jobs private INotificationService Notification { get; } private IJobRecord Job { get; } private IRepository<UsersToNotify> UserNotifyRepo { get; } + private INotificationEngine NotificationEngine { get; } + + public void CheckAndUpdateAll() { var plexSettings = Plex.GetSettings(); @@ -148,7 +152,7 @@ namespace PlexRequests.Services.Jobs if (modifiedModel.Any()) { - NotifyUsers(modifiedModel, plexSettings.PlexAuthToken); + NotificationEngine.NotifyUsers(modifiedModel, plexSettings.PlexAuthToken); RequestService.BatchUpdate(modifiedModel); } @@ -158,6 +162,7 @@ namespace PlexRequests.Services.Jobs public List<PlexMovie> GetPlexMovies() { + var settings = Plex.GetSettings(); var movies = new List<PlexMovie>(); var libs = Cache.Get<List<PlexSearch>>(CacheKeys.PlexLibaries); if (libs != null) @@ -175,6 +180,7 @@ namespace PlexRequests.Services.Jobs ReleaseYear = video.Year, Title = video.Title, ProviderId = video.ProviderId, + Url = PlexHelper.GetPlexMediaUrl(settings.MachineIdentifier, video.RatingKey) })); } } @@ -182,6 +188,12 @@ namespace PlexRequests.Services.Jobs } public bool IsMovieAvailable(PlexMovie[] plexMovies, string title, string year, string providerId = null) + { + var movie = GetMovie(plexMovies, title, year, providerId); + return movie != null; + } + + public PlexMovie GetMovie(PlexMovie[] plexMovies, string title, string year, string providerId = null) { var advanced = !string.IsNullOrEmpty(providerId); foreach (var movie in plexMovies) @@ -191,20 +203,21 @@ namespace PlexRequests.Services.Jobs if (!string.IsNullOrEmpty(movie.ProviderId) && movie.ProviderId.Equals(providerId, StringComparison.InvariantCultureIgnoreCase)) { - return true; + return movie; } } if (movie.Title.Equals(title, StringComparison.CurrentCultureIgnoreCase) && movie.ReleaseYear.Equals(year, StringComparison.CurrentCultureIgnoreCase)) { - return true; + return movie; } } - return false; + return null; } public List<PlexTvShow> GetPlexTvShows() { + var settings = Plex.GetSettings(); var shows = new List<PlexTvShow>(); var libs = Cache.Get<List<PlexSearch>>(CacheKeys.PlexLibaries); if (libs != null) @@ -224,7 +237,9 @@ namespace PlexRequests.Services.Jobs Title = x.Title, ReleaseYear = x.Year, ProviderId = x.ProviderId, - Seasons = x.Seasons?.Select(d => PlexHelper.GetSeasonNumberFromTitle(d.Title)).ToArray() + Seasons = x.Seasons?.Select(d => PlexHelper.GetSeasonNumberFromTitle(d.Title)).ToArray(), + Url = PlexHelper.GetPlexMediaUrl(settings.MachineIdentifier, x.RatingKey) + })); } } @@ -232,6 +247,14 @@ namespace PlexRequests.Services.Jobs } public bool IsTvShowAvailable(PlexTvShow[] plexShows, string title, string year, string providerId = null, int[] seasons = null) + { + var show = GetTvShow(plexShows, title, year, providerId, seasons); + return show != null; + } + + + public PlexTvShow GetTvShow(PlexTvShow[] plexShows, string title, string year, string providerId = null, + int[] seasons = null) { var advanced = !string.IsNullOrEmpty(providerId); foreach (var show in plexShows) @@ -242,23 +265,23 @@ namespace PlexRequests.Services.Jobs { if (seasons.Any(season => show.Seasons.Contains(season))) { - return true; + return show; } - return false; + return null; } if (!string.IsNullOrEmpty(show.ProviderId) && show.ProviderId.Equals(providerId, StringComparison.InvariantCultureIgnoreCase)) { - return true; + return show; } } if (show.Title.Equals(title, StringComparison.CurrentCultureIgnoreCase) && show.ReleaseYear.Equals(year, StringComparison.CurrentCultureIgnoreCase)) { - return true; + return show; } } - return false; + return null; } public bool IsEpisodeAvailable(string theTvDbId, int season, int episode) @@ -328,6 +351,7 @@ namespace PlexRequests.Services.Jobs public List<PlexAlbum> GetPlexAlbums() { + var settings = Plex.GetSettings(); var albums = new List<PlexAlbum>(); var libs = Cache.Get<List<PlexSearch>>(CacheKeys.PlexLibaries); if (libs != null) @@ -344,7 +368,8 @@ namespace PlexRequests.Services.Jobs { Title = x.Title, ReleaseYear = x.Year, - Artist = x.ParentTitle + Artist = x.ParentTitle, + Url = PlexHelper.GetPlexMediaUrl(settings.MachineIdentifier, x.RatingKey) })); } } @@ -355,7 +380,13 @@ namespace PlexRequests.Services.Jobs { return plexAlbums.Any(x => x.Title.Contains(title) && - //x.ReleaseYear.Equals(year, StringComparison.CurrentCultureIgnoreCase) && + x.Artist.Equals(artist, StringComparison.CurrentCultureIgnoreCase)); + } + + public PlexAlbum GetAlbum(PlexAlbum[] plexAlbums, string title, string year, string artist) + { + return plexAlbums.FirstOrDefault(x => + x.Title.Contains(title) && x.Artist.Equals(artist, StringComparison.CurrentCultureIgnoreCase)); } @@ -462,63 +493,6 @@ namespace PlexRequests.Services.Jobs return true; } - private void NotifyUsers(IEnumerable<RequestedModel> modelChanged, string apiKey) - { - try - { - var plexUser = PlexApi.GetUsers(apiKey); - var userAccount = PlexApi.GetAccount(apiKey); - - var adminUsername = userAccount.Username ?? string.Empty; - - var users = UserNotifyRepo.GetAll().ToList(); - Log.Debug("Notifying Users Count {0}", users.Count); - foreach (var model in modelChanged) - { - var selectedUsers = users.Select(x => x.Username).Intersect(model.RequestedUsers); - foreach (var user in selectedUsers) - { - Log.Info("Notifying user {0}", user); - if (user == adminUsername) - { - Log.Info("This user is the Plex server owner"); - PublishUserNotification(userAccount.Username, userAccount.Email, model.Title); - return; - } - - var email = plexUser.User.FirstOrDefault(x => x.Username == user); - if (email == null) - { - Log.Info("There is no email address for this Plex user, cannot send notification"); - // We do not have a plex user that requested this! - continue; - } - - Log.Info("Sending notification to: {0} at: {1}, for title: {2}", email.Username, email.Email, model.Title); - PublishUserNotification(email.Username, email.Email, model.Title); - } - } - } - catch (Exception e) - { - Log.Error(e); - } - } - - private void PublishUserNotification(string username, string email, string title) - { - var notificationModel = new NotificationModel - { - User = username, - UserEmail = email, - NotificationType = NotificationType.RequestAvailable, - Title = title - }; - - // Send the notification to the user. - Notification.Publish(notificationModel); - } - public void Execute(IJobExecutionContext context) { try diff --git a/PlexRequests.Services/Models/PlexAlbum.cs b/PlexRequests.Services/Models/PlexAlbum.cs index 5d2bd7254..09d4b2638 100644 --- a/PlexRequests.Services/Models/PlexAlbum.cs +++ b/PlexRequests.Services/Models/PlexAlbum.cs @@ -1,9 +1,10 @@ -namespace PlexRequests.Services.Models -{ - public class PlexAlbum - { - public string Title { get; set; } - public string Artist { get; set; } - public string ReleaseYear { get; set; } - } -} +namespace PlexRequests.Services.Models +{ + public class PlexAlbum + { + public string Title { get; set; } + public string Artist { get; set; } + public string ReleaseYear { get; set; } + public string Url { get; set; } + } +} diff --git a/PlexRequests.Services/Models/PlexMovie.cs b/PlexRequests.Services/Models/PlexMovie.cs index 0149698ba..27eca9948 100644 --- a/PlexRequests.Services/Models/PlexMovie.cs +++ b/PlexRequests.Services/Models/PlexMovie.cs @@ -1,9 +1,10 @@ -namespace PlexRequests.Services.Models -{ - public class PlexMovie - { - public string Title { get; set; } - public string ReleaseYear { get; set; } - public string ProviderId { get; set; } - } -} +namespace PlexRequests.Services.Models +{ + public class PlexMovie + { + public string Title { get; set; } + public string ReleaseYear { get; set; } + public string ProviderId { get; set; } + public string Url { get; set; } + } +} diff --git a/PlexRequests.Services/Models/PlexTvShow.cs b/PlexRequests.Services/Models/PlexTvShow.cs index 5ac629132..aecf6f088 100644 --- a/PlexRequests.Services/Models/PlexTvShow.cs +++ b/PlexRequests.Services/Models/PlexTvShow.cs @@ -6,5 +6,6 @@ public string ReleaseYear { get; set; } public string ProviderId { get; set; } public int[] Seasons { get; set; } + public string Url { get; set; } } } diff --git a/PlexRequests.Services/Notification/EmailMessageNotification.cs b/PlexRequests.Services/Notification/EmailMessageNotification.cs index 3bbbc43cb..a81a15290 100644 --- a/PlexRequests.Services/Notification/EmailMessageNotification.cs +++ b/PlexRequests.Services/Notification/EmailMessageNotification.cs @@ -104,7 +104,14 @@ namespace PlexRequests.Services.Notification private bool ValidateConfiguration(EmailNotificationSettings settings) { - if (string.IsNullOrEmpty(settings.EmailHost) || string.IsNullOrEmpty(settings.EmailUsername) || string.IsNullOrEmpty(settings.EmailPassword) || string.IsNullOrEmpty(settings.RecipientEmail) || string.IsNullOrEmpty(settings.EmailPort.ToString())) + if (settings.Authentication) + { + if (string.IsNullOrEmpty(settings.EmailUsername) || string.IsNullOrEmpty(settings.EmailPassword)) + { + return false; + } + } + if (string.IsNullOrEmpty(settings.EmailHost) || string.IsNullOrEmpty(settings.RecipientEmail) || string.IsNullOrEmpty(settings.EmailPort.ToString())) { return false; } @@ -186,7 +193,7 @@ namespace PlexRequests.Services.Notification { client.Authenticate(settings.EmailUsername, settings.EmailPassword); } - + Log.Info("sending message to {0} \r\n from: {1}\r\n Are we authenticated: {2}", message.To, message.From, client.IsAuthenticated); await client.SendAsync(message); await client.DisconnectAsync(true); } diff --git a/PlexRequests.Services/Notification/NotificationEngine.cs b/PlexRequests.Services/Notification/NotificationEngine.cs new file mode 100644 index 000000000..ccbe07fec --- /dev/null +++ b/PlexRequests.Services/Notification/NotificationEngine.cs @@ -0,0 +1,156 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: NotificationEngine.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 NLog; +using NLog.Fluent; +using PlexRequests.Api; +using PlexRequests.Api.Interfaces; +using PlexRequests.Core.Models; +using PlexRequests.Services.Interfaces; +using PlexRequests.Store; +using PlexRequests.Store.Models; +using PlexRequests.Store.Repository; + +namespace PlexRequests.Services.Notification +{ + public class NotificationEngine : INotificationEngine + { + public NotificationEngine(IPlexApi p, IRepository<UsersToNotify> repo, INotificationService service) + { + PlexApi = p; + UserNotifyRepo = repo; + Notification = service; + } + + private IPlexApi PlexApi { get; } + private IRepository<UsersToNotify> UserNotifyRepo { get; } + private static Logger Log = LogManager.GetCurrentClassLogger(); + private INotificationService Notification { get; } + + public async Task NotifyUsers(IEnumerable<RequestedModel> modelChanged, string apiKey) + { + try + { + var plexUser = PlexApi.GetUsers(apiKey); + var userAccount = PlexApi.GetAccount(apiKey); + + var adminUsername = userAccount.Username ?? string.Empty; + + var users = UserNotifyRepo.GetAll().ToList(); + Log.Debug("Notifying Users Count {0}", users.Count); + foreach (var model in modelChanged) + { + var selectedUsers = users.Select(x => x.Username).Intersect(model.RequestedUsers, StringComparer.CurrentCultureIgnoreCase); + foreach (var user in selectedUsers) + { + Log.Info("Notifying user {0}", user); + if (user.Equals(adminUsername, StringComparison.CurrentCultureIgnoreCase)) + { + Log.Info("This user is the Plex server owner"); + await PublishUserNotification(userAccount.Username, userAccount.Email, model.Title); + return; + } + + var email = plexUser.User.FirstOrDefault(x => x.Username.Equals(user, StringComparison.CurrentCultureIgnoreCase)); + if (email == null) + { + Log.Info("There is no email address for this Plex user, cannot send notification"); + // We do not have a plex user that requested this! + continue; + } + + 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); + } + } + } + catch (Exception e) + { + Log.Error(e); + } + } + + public async Task NotifyUsers(RequestedModel model, string apiKey) + { + try + { + var plexUser = PlexApi.GetUsers(apiKey); + var userAccount = PlexApi.GetAccount(apiKey); + + var adminUsername = userAccount.Username ?? string.Empty; + + var users = UserNotifyRepo.GetAll().ToList(); + Log.Debug("Notifying Users Count {0}", users.Count); + + var selectedUsers = users.Select(x => x.Username).Intersect(model.RequestedUsers, StringComparer.CurrentCultureIgnoreCase); + foreach (var user in selectedUsers) + { + Log.Info("Notifying user {0}", user); + if (user.Equals(adminUsername, StringComparison.CurrentCultureIgnoreCase)) + { + Log.Info("This user is the Plex server owner"); + await PublishUserNotification(userAccount.Username, userAccount.Email, model.Title); + return; + } + + var email = plexUser.User.FirstOrDefault(x => x.Username.Equals(user, StringComparison.CurrentCultureIgnoreCase)); + if (email == null) + { + Log.Info("There is no email address for this Plex user, cannot send notification"); + // We do not have a plex user that requested this! + continue; + } + + 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); + } + } + catch (Exception e) + { + Log.Error(e); + } + } + + private async Task PublishUserNotification(string username, string email, string title) + { + var notificationModel = new NotificationModel + { + User = username, + UserEmail = email, + NotificationType = NotificationType.RequestAvailable, + Title = title + }; + + // Send the notification to the user. + await Notification.Publish(notificationModel); + } + } +} \ No newline at end of file diff --git a/PlexRequests.Services/PlexRequests.Services.csproj b/PlexRequests.Services/PlexRequests.Services.csproj index df4d694c9..6aed947bc 100644 --- a/PlexRequests.Services/PlexRequests.Services.csproj +++ b/PlexRequests.Services/PlexRequests.Services.csproj @@ -75,6 +75,7 @@ </ItemGroup> <ItemGroup> <Compile Include="Interfaces\IJobRecord.cs" /> + <Compile Include="Interfaces\INotificationEngine.cs" /> <Compile Include="Jobs\JobRecord.cs" /> <Compile Include="Jobs\JobNames.cs" /> <Compile Include="Jobs\PlexEpisodeCacher.cs" /> @@ -97,6 +98,7 @@ <Compile Include="Interfaces\INotification.cs" /> <Compile Include="Interfaces\INotificationService.cs" /> <Compile Include="Notification\EmailMessageNotification.cs" /> + <Compile Include="Notification\NotificationEngine.cs" /> <Compile Include="Notification\NotificationModel.cs" /> <Compile Include="Notification\NotificationService.cs" /> <Compile Include="Notification\PushoverNotification.cs" /> diff --git a/PlexRequests.UI.Tests/UserLoginModuleTests.cs b/PlexRequests.UI.Tests/UserLoginModuleTests.cs index 16b138eea..22f57750e 100644 --- a/PlexRequests.UI.Tests/UserLoginModuleTests.cs +++ b/PlexRequests.UI.Tests/UserLoginModuleTests.cs @@ -72,9 +72,9 @@ namespace PlexRequests.UI.Tests LandingPageMock.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new LandingPageSettings()); IAnalytics = new Mock<IAnalytics>(); Linker = new Mock<IResourceLinker>(); - Linker.Setup(x => x.BuildAbsoluteUri(It.IsAny<NancyContext>(), "SearchIndex", null)).Returns(new Uri("http://www.searchindex.com")); - Linker.Setup(x => x.BuildAbsoluteUri(It.IsAny<NancyContext>(), "LandingPageIndex", null)).Returns(new Uri("http://www.landingpage.com")); - Linker.Setup(x => x.BuildAbsoluteUri(It.IsAny<NancyContext>(), "UserLoginIndex", null)).Returns(new Uri("http://www.userloginindex.com")); + Linker.Setup(x => x.BuildRelativeUri(It.IsAny<NancyContext>(), "SearchIndex", null)).Returns(new Uri("http://www.searchindex.com")); + Linker.Setup(x => x.BuildRelativeUri(It.IsAny<NancyContext>(), "LandingPageIndex", null)).Returns(new Uri("http://www.landingpage.com")); + Linker.Setup(x => x.BuildRelativeUri(It.IsAny<NancyContext>(), "UserLoginIndex", null)).Returns(new Uri("http://www.userloginindex.com")); PlexSettingsMock = new Mock<ISettingsService<PlexSettings>>(); PlexSettingsMock.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings() {PlexAuthToken = "abc"}); Bootstrapper = new ConfigurableBootstrapper(with => diff --git a/PlexRequests.UI/Content/app/requests/requestsController.js b/PlexRequests.UI/Content/app/requests/requestsController.js new file mode 100644 index 000000000..204dcf9a2 --- /dev/null +++ b/PlexRequests.UI/Content/app/requests/requestsController.js @@ -0,0 +1,51 @@ +(function () { + var controller = function($scope, requestsService) { + + $scope.requests = []; + $scope.selectedTab = {}; + $scope.currentPage = 1; + $scope.tabs = []; + + $scope.plexSettings = {}; + $scope.requestSettings = {}; + + // Search + $scope.searchTerm = ""; + + + // Called on page load + $scope.init = function() { + // Get the settings + $scope.plexSettings = requestsService.getPlexRequestSettings(getBaseUrl()); + $scope.requestSettings = requestsService.getRequestSettings(getBaseUrl()); + + if ($scope.plexSettings.SearchForMovies) { + $scope.selectedTab = "movies"; + + // Load the movie Requests + $scope.requests = requestsService.getRequests("movie", getBaseUrl()); + } + }; + + + $scope.changeTab = function(tab) { + // load the data from the tab + switch (tab) { + // Set the selected tab and load the appropriate data + } + + }; + + $scope.search = function() { + $scope.requests = requestsService.getRequests + }; + + function getBaseUrl() { + return $('#baseUrl').val(); + } + + + } + + angular.module('PlexRequests').controller('requestsController', ["$scope", "requestsService", controller]); +}()); \ No newline at end of file diff --git a/PlexRequests.UI/Content/app/requests/requestsService.js b/PlexRequests.UI/Content/app/requests/requestsService.js new file mode 100644 index 000000000..b702c0727 --- /dev/null +++ b/PlexRequests.UI/Content/app/requests/requestsService.js @@ -0,0 +1,49 @@ +(function () { + + var requestsService = function ($http) { + + $http.defaults.headers.common['Content-Type'] = 'application/json'; // Set default headers + + var getRequests = function (type, baseUrl) { + switch (type) { + case "movie": + return $http.get(createBaseUrl(baseUrl, "/requestsbeta/movies")); + case "tv": + return $http.get(createBaseUrl(baseUrl, "/requestsbeta/tvshows")); + case "album": + return $http.get(createBaseUrl(baseUrl, "/requestsbeta/albums")); + } + return null; + }; + + var getPlexRequestSettings = function (baseUrl) { + return $http.get(createBaseUrl(baseUrl, "/requestsbeta/plexrequestsettings")); + } + + var getRequestsSettings = function (baseUrl) { + return $http.get(createBaseUrl(baseUrl, "/requestsbeta/requestsettings")); + } + + var getRequestsSearch = function (type, baseUrl, searchTerm) { + switch (type) { + case "movie": + return $http.get(createBaseUrl(baseUrl, "/requestsbeta/movies/"+ searchTerm)); + case "tv": + return $http.get(createBaseUrl(baseUrl, "/requestsbeta/tvshows/" + searchTerm)); + case "album": + return $http.get(createBaseUrl(baseUrl, "/requestsbeta/albums/" + searchTerm)); + } + return null; + }; + + return { + getRequests: getRequests, + getRequestsSearch: getRequestsSearch, + getPlexRequestSettings: getPlexRequestSettings, + getRequestSettings: getRequestsSettings + }; + } + + angular.module('PlexRequests').factory('requestsService', ["$http", requestsService]); + +}()); \ No newline at end of file diff --git a/PlexRequests.UI/Content/app/userManagement/userManagementController.js b/PlexRequests.UI/Content/app/userManagement/userManagementController.js index e9678e539..88a85e7aa 100644 --- a/PlexRequests.UI/Content/app/userManagement/userManagementController.js +++ b/PlexRequests.UI/Content/app/userManagement/userManagementController.js @@ -4,49 +4,91 @@ $scope.user = {}; // The local user $scope.users = []; // list of users + $scope.claims = []; // List of claims - $scope.selectedUser = {}; + $scope.selectedUser = {}; // User on the right side + $scope.selectedClaims = {}; - $scope.sortType = 'username'; + + $scope.sortType = "username"; $scope.sortReverse = false; - $scope.searchTerm = ''; + $scope.searchTerm = ""; + $scope.error = { error: false, errorMessage: "" }; + // Select a user to populate on the right side $scope.selectUser = function (id) { - $scope.selectedUser = {}; $scope.selectedUser = $scope.users.find(x => x.id === id); } + // Get all users in the system $scope.getUsers = function () { $scope.users = userManagementService.getUsers() - .then(function (data) { - $scope.users = data.data; - }); + .then(function (data) { + $scope.users = data.data; + }); }; + // Get the claims and populate the create dropdown + $scope.getClaims = function () { + userManagementService.getClaims() + .then(function (data) { + $scope.claims = data.data; + }); + } + + // Create a user, do some validation too $scope.addUser = function () { + if (!$scope.user.username || !$scope.user.password) { $scope.error.error = true; $scope.error.errorMessage = "Please provide a correct username and password"; generateNotify($scope.error.errorMessage, 'warning'); return; } - userManagementService.addUser($scope.user).then(function (data) { - if (data.message) { - $scope.error.error = true; - $scope.error.errorMessage = data.message; - } else { - $scope.users.push(data); - $scope.user = {}; - } - }); + + userManagementService.addUser($scope.user, $scope.selectedClaims) + .then(function (data) { + if (data.message) { + $scope.error.error = true; + $scope.error.errorMessage = data.message; + } else { + $scope.users.push(data); // Push the new user into the array to update the DOM + $scope.user = {}; + $scope.selectedClaims = {}; + } + }); }; + + $scope.$watch('claims|filter:{selected:true}', + function (nv) { + $scope.selectedClaims = nv.map(function (claim) { + return claim.name; + }); + }, + true); + + + $scope.updateUser = function () { + + } + + function getBaseUrl() { + return $('#baseUrl').val(); + } + + + // On page load + $scope.init = function () { + $scope.getUsers(); + $scope.getClaims(); + return; + } } angular.module('PlexRequests').controller('userManagementController', ["$scope", "userManagementService", 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 fe7b7ef07..e1640e3ea 100644 --- a/PlexRequests.UI/Content/app/userManagement/userManagementService.js +++ b/PlexRequests.UI/Content/app/userManagement/userManagementService.js @@ -2,27 +2,32 @@ var userManagementService = function ($http) { - $http.defaults.headers.common['Content-Type'] = 'application/x-www-form-urlencoded'; // Set default headers + $http.defaults.headers.common['Content-Type'] = 'application/json'; // Set default headers var getUsers = function () { return $http.get('/usermanagement/users'); }; - var addUser = function (user) { - if (!user) { + var addUser = function (user, claims) { + if (!user || claims.length === 0) { return null; } return $http({ url: '/usermanagement/createuser', method: "POST", - data: $.param(user) + data: { username: user.username, password: user.password, claims: claims, email: user.email } }); } + var getClaims = function () { + return $http.get('/usermanagement/claims'); + } + return { getUsers: getUsers, - addUser: addUser + addUser: addUser, + getClaims: getClaims }; } diff --git a/PlexRequests.UI/Content/requests.js b/PlexRequests.UI/Content/requests.js index 765db5ecb..678fa60fd 100644 --- a/PlexRequests.UI/Content/requests.js +++ b/PlexRequests.UI/Content/requests.js @@ -578,7 +578,7 @@ function tvLoad() { results.forEach(function (result) { var ep = result.episodes; ep.forEach(function (episode) { - var foundItem = tvObject.find(x => x.seasonNumber === episode.seasonNumber); + var foundItem = tvObject.find(function(x) { return x.seasonNumber === episode.seasonNumber }); if (!foundItem) { var obj = { seasonNumber: episode.seasonNumber, episodes: [] } tvObject.push(obj); diff --git a/PlexRequests.UI/Content/search.js b/PlexRequests.UI/Content/search.js index 37b41a95e..310ba27fe 100644 --- a/PlexRequests.UI/Content/search.js +++ b/PlexRequests.UI/Content/search.js @@ -5,6 +5,21 @@ return opts.inverse(this); }); +Function.prototype.bind = function (parent) { + var f = this; + var args = []; + + for (var a = 1; a < arguments.length; a++) { + args[args.length] = arguments[a]; + } + + var temp = function () { + return f.apply(parent, args); + } + + return (temp); +} + $(function () { @@ -56,7 +71,9 @@ $(function () { if (searchTimer) { clearTimeout(searchTimer); } - searchTimer = setTimeout(movieSearch, 800); + searchTimer = setTimeout(function () { + movieSearch(); + }.bind(this), 800); }); @@ -75,7 +92,9 @@ $(function () { if (searchTimer) { clearTimeout(searchTimer); } - searchTimer = setTimeout(tvSearch, 400); + searchTimer = setTimeout(function () { + tvSearch(); + }.bind(this), 800); }); // Click TV dropdown option @@ -116,7 +135,9 @@ $(function () { if (searchTimer) { clearTimeout(searchTimer); } - searchTimer = setTimeout(musicSearch, 400); + searchTimer = setTimeout(function () { + musicSearch(); + }.bind(this), 800); }); @@ -423,7 +444,8 @@ $(function () { imdb: result.imdbId, requested: result.requested, approved: result.approved, - available: result.available + available: result.available, + url: result.plexUrl }; return context; @@ -444,7 +466,8 @@ $(function () { approved: result.approved, available: result.available, episodes: result.episodes, - tvFullyAvailable: result.tvFullyAvailable + tvFullyAvailable: result.tvFullyAvailable, + url: result.plexUrl }; return context; } @@ -464,7 +487,8 @@ $(function () { country: result.country, requested: result.requested, approved: result.approved, - available: result.available + available: result.available, + url: result.plexUrl }; return context; @@ -484,7 +508,7 @@ $(function () { var $content = $("#seasonsBody"); $content.html(""); $('#selectedSeasonsId').val(id); - results.forEach(function(result) { + results.forEach(function (result) { var context = buildSeasonsContext(result); $content.append(seasonsTemplate(context)); }); @@ -503,7 +527,7 @@ $(function () { }; }); - $('#seasonsRequest').click(function(e) { + $('#seasonsRequest').click(function (e) { e.preventDefault(); var tvId = $('#selectedSeasonsId').val(); var url = createBaseUrl(base, '/search/seasons/'); @@ -522,7 +546,7 @@ $(function () { var $checkedSeasons = $('.selectedSeasons:checkbox:checked'); $checkedSeasons.each(function (index, element) { - if (index < $checkedSeasons.length -1) { + if (index < $checkedSeasons.length - 1) { seasonsParam = seasonsParam + element.id + ","; } else { seasonsParam = seasonsParam + element.id; @@ -536,7 +560,7 @@ $(function () { var url = $form.prop('action'); sendRequestAjax(data, type, url, tvId); - + }); $('#episodesModal').on('show.bs.modal', function (event) { @@ -560,7 +584,9 @@ $(function () { results.forEach(function (result) { var episodes = buildEpisodesView(result); - if (!seenSeasons.find(x => x === episodes.season)) { + if (!seenSeasons.find(function(x) { + return x === episodes.season + })) { // Create the seasons heading seenSeasons.push(episodes.season); var context = buildSeasonsCount(result); @@ -586,7 +612,7 @@ $(function () { loadingButton("episodesRequest", "primary"); var tvId = $('#selectedEpisodeId').val(); - + var $form = $('#form' + tvId); var model = []; @@ -623,7 +649,7 @@ $(function () { } }, - error: function(e) { + error: function (e) { console.log(e); generateNotify("Something went wrong!", "danger"); } diff --git a/PlexRequests.UI/Helpers/BaseUrlHelper.cs b/PlexRequests.UI/Helpers/BaseUrlHelper.cs index 625d71380..2ce349ec0 100644 --- a/PlexRequests.UI/Helpers/BaseUrlHelper.cs +++ b/PlexRequests.UI/Helpers/BaseUrlHelper.cs @@ -82,9 +82,8 @@ namespace PlexRequests.UI.Helpers $"<link rel=\"stylesheet\" href=\"{startUrl}/font-awesome.css\" type=\"text/css\"/>", $"<link rel=\"stylesheet\" href=\"{startUrl}/pace.min.css\" type=\"text/css\"/>", $"<link rel=\"stylesheet\" href=\"{startUrl}/awesome-bootstrap-checkbox.css\" type=\"text/css\"/>", - $"<link rel=\"stylesheet\" href=\"{startUrl}/base.css\" type=\"text/css\"/>", - $"<link rel=\"stylesheet\" href=\"{startUrl}/Themes/{settings.ThemeName}\" type=\"text/css\"/>", - $"<link rel=\"stylesheet\" href=\"{startUrl}/datepicker.min.css\" type=\"text/css\"/>", + $"<link rel=\"stylesheet\" href=\"{startUrl}/base.css?v={Assembly}\" type=\"text/css\"/>", + $"<link rel=\"stylesheet\" href=\"{startUrl}/Themes/{settings.ThemeName}?v={Assembly}\" type=\"text/css\"/>", $"<link rel=\"stylesheet\" href=\"{startUrl}/tooltip/tooltipster.bundle.min.css\" type=\"text/css\"/>", }; @@ -95,11 +94,10 @@ namespace PlexRequests.UI.Helpers $"<script src=\"{startUrl}/handlebars.min.js\"></script>", $"<script src=\"{startUrl}/bootstrap.min.js\"></script>", $"<script src=\"{startUrl}/bootstrap-notify.min.js\"></script>", - $"<script src=\"{startUrl}/site.js\"></script>", + $"<script src=\"{startUrl}/site.js?v={Assembly}\"></script>", $"<script src=\"{startUrl}/pace.min.js\"></script>", $"<script src=\"{startUrl}/jquery.mixitup.js\"></script>", $"<script src=\"{startUrl}/moment.min.js\"></script>", - $"<script src=\"{startUrl}/bootstrap-datetimepicker.min.js\"></script>", $"<script src=\"{startUrl}/tooltip/tooltipster.bundle.min.js\"></script>" }; @@ -118,6 +116,17 @@ namespace PlexRequests.UI.Helpers return helper.Raw(sb.ToString()); } + public static IHtmlString LoadDateTimePickerAsset(this HtmlHelpers helper) + { + var content = GetBaseUrl(); + + var sb = new StringBuilder(); + var startUrl = $"{content}/Content"; + sb.AppendLine($"<link rel=\"stylesheet\" href=\"{startUrl}/datepicker.min.css\" type=\"text/css\"/>"); + sb.AppendLine($"<script src=\"{startUrl}/bootstrap-datetimepicker.min.js\"></script>"); + + return helper.Raw(sb.ToString()); + } public static IHtmlString LoadAngularAssets(this HtmlHelpers helper) { var sb = new StringBuilder(); @@ -135,7 +144,7 @@ namespace PlexRequests.UI.Helpers var startUrl = $"{content}/Content"; sb.AppendLine($"<script src=\"{startUrl}/angular.min.js\"></script>"); // Load angular first - sb.AppendLine($"<script src=\"{startUrl}/app/app.js\"></script>"); + sb.AppendLine($"<script src=\"{startUrl}/app/app.js?v={Assembly}\"></script>"); return helper.Raw(sb.ToString()); } @@ -147,7 +156,7 @@ namespace PlexRequests.UI.Helpers var content = GetContentUrl(assetLocation); - sb.AppendLine($"<script src=\"{content}/Content/search.js\" type=\"text/javascript\"></script>"); + sb.AppendLine($"<script src=\"{content}/Content/search.js?v={Assembly}\" type=\"text/javascript\"></script>"); return helper.Raw(sb.ToString()); } @@ -159,7 +168,7 @@ namespace PlexRequests.UI.Helpers var content = GetContentUrl(assetLocation); - sb.AppendLine($"<script src=\"{content}/Content/requests.js\" type=\"text/javascript\"></script>"); + sb.AppendLine($"<script src=\"{content}/Content/requests.js?v={Assembly}\" type=\"text/javascript\"></script>"); return helper.Raw(sb.ToString()); } @@ -171,7 +180,7 @@ namespace PlexRequests.UI.Helpers var content = GetContentUrl(assetLocation); - sb.AppendLine($"<script src=\"{content}/Content/issues.js\" type=\"text/javascript\"></script>"); + sb.AppendLine($"<script src=\"{content}/Content/issues.js?v={Assembly}\" type=\"text/javascript\"></script>"); return helper.Raw(sb.ToString()); } @@ -183,7 +192,7 @@ namespace PlexRequests.UI.Helpers var content = GetContentUrl(assetLocation); - sb.AppendLine($"<script src=\"{content}/Content/wizard.js\" type=\"text/javascript\"></script>"); + sb.AppendLine($"<script src=\"{content}/Content/wizard.js?v={Assembly}\" type=\"text/javascript\"></script>"); return helper.Raw(sb.ToString()); } @@ -193,11 +202,25 @@ namespace PlexRequests.UI.Helpers var assetLocation = GetBaseUrl(); var content = GetContentUrl(assetLocation); - var asset = $"<script src=\"{content}/Content/issue-details.js\" type=\"text/javascript\"></script>"; + var asset = $"<script src=\"{content}/Content/issue-details.js?v={Assembly}\" type=\"text/javascript\"></script>"; return helper.Raw(asset); } + + public static IHtmlString LoadUserManagementAssets(this HtmlHelpers helper) + { + var assetLocation = GetBaseUrl(); + var content = GetContentUrl(assetLocation); + + var controller = $"<script src=\"{content}/Content/app/userManagement/userManagementController.js?v={Assembly}\" type=\"text/javascript\"></script>"; + controller += $"<script src=\"{content}/Content/app/userManagement/userManagementService.js?v={Assembly}\" type=\"text/javascript\"></script>"; + + + return helper.Raw(controller); + } + + public static IHtmlString LoadTableAssets(this HtmlHelpers helper) { var sb = new StringBuilder(); @@ -222,7 +245,7 @@ namespace PlexRequests.UI.Helpers var assetLocation = GetBaseUrl(); var content = GetContentUrl(assetLocation); - var asset = $"<script src=\"{content}/Content/analytics.js\" type=\"text/javascript\"></script>"; + var asset = $"<script src=\"{content}/Content/analytics.js?v={Assembly}\" type=\"text/javascript\"></script>"; return helper.Raw(asset); } diff --git a/PlexRequests.UI/Helpers/TvSender.cs b/PlexRequests.UI/Helpers/TvSender.cs index 13a941512..05b2799ab 100644 --- a/PlexRequests.UI/Helpers/TvSender.cs +++ b/PlexRequests.UI/Helpers/TvSender.cs @@ -131,7 +131,7 @@ namespace PlexRequests.UI.Helpers var result = SonarrApi.AddSeries(model.ProviderId, model.Title, qualityProfile, sonarrSettings.SeasonFolders, sonarrSettings.RootPath, model.SeasonCount, model.SeasonList, sonarrSettings.ApiKey, - sonarrSettings.FullUri); + sonarrSettings.FullUri, true, true); return result; } diff --git a/PlexRequests.UI/Models/SearchViewModel.cs b/PlexRequests.UI/Models/SearchViewModel.cs index 776b9d2b1..9c11d32ef 100644 --- a/PlexRequests.UI/Models/SearchViewModel.cs +++ b/PlexRequests.UI/Models/SearchViewModel.cs @@ -1,37 +1,38 @@ -#region Copyright -// /************************************************************************ -// Copyright (c) 2016 Jamie Rees -// File: SearchTvShowViewModel.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.UI.Models -{ - public class SearchViewModel - { - public bool Approved { get; set; } - public bool Requested { get; set; } - public bool Available { get; set; } - } +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: SearchTvShowViewModel.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.UI.Models +{ + public class SearchViewModel + { + public bool Approved { get; set; } + public bool Requested { get; set; } + public bool Available { get; set; } + public string PlexUrl { get; set; } + } } \ No newline at end of file diff --git a/PlexRequests.UI/Models/SessionKeys.cs b/PlexRequests.UI/Models/SessionKeys.cs index fefc7d8bd..766a84bf6 100644 --- a/PlexRequests.UI/Models/SessionKeys.cs +++ b/PlexRequests.UI/Models/SessionKeys.cs @@ -31,5 +31,6 @@ namespace PlexRequests.UI.Models public const string UsernameKey = "Username"; public const string ClientDateTimeOffsetKey = "ClientDateTimeOffset"; public const string UserWizardPlexAuth = nameof(UserWizardPlexAuth); + public const string UserWizardMachineId = nameof(UserWizardMachineId); } } diff --git a/PlexRequests.UI/Models/UserManagementUsersViewModel.cs b/PlexRequests.UI/Models/UserManagementUsersViewModel.cs index 57fcd1f5a..915f90075 100644 --- a/PlexRequests.UI/Models/UserManagementUsersViewModel.cs +++ b/PlexRequests.UI/Models/UserManagementUsersViewModel.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Newtonsoft.Json; namespace PlexRequests.UI.Models { @@ -15,6 +16,7 @@ namespace PlexRequests.UI.Models public UserType Type { get; set; } public string EmailAddress { get; set; } public UserManagementPlexInformation PlexInfo { get; set; } + public string[] ClaimsArray { get; set; } } public class UserManagementPlexInformation @@ -42,5 +44,18 @@ namespace PlexRequests.UI.Models PlexUser, LocalUser } + + public class UserManagementCreateModel + { + [JsonProperty("username")] + public string Username { get; set; } + [JsonProperty("password")] + public string Password { get; set; } + [JsonProperty("claims")] + public string[] Claims { get; set; } + + [JsonProperty("email")] + public string EmailAddress { get; set; } + } } diff --git a/PlexRequests.UI/Modules/AdminModule.cs b/PlexRequests.UI/Modules/AdminModule.cs index 4d6ca7e43..a04edca3c 100644 --- a/PlexRequests.UI/Modules/AdminModule.cs +++ b/PlexRequests.UI/Modules/AdminModule.cs @@ -161,7 +161,7 @@ namespace PlexRequests.UI.Modules Post["/couchpotato"] = _ => SaveCouchPotato(); Get["/plex"] = _ => Plex(); - Post["/plex"] = _ => SavePlex(); + Post["/plex", true] = async (x, ct) => await SavePlex(); Get["/sonarr"] = _ => Sonarr(); Post["/sonarr"] = _ => SaveSonarr(); @@ -170,13 +170,13 @@ namespace PlexRequests.UI.Modules Post["/sickrage"] = _ => SaveSickrage(); Post["/sonarrprofiles"] = _ => GetSonarrQualityProfiles(); - Post["/cpprofiles", true] = async (x,ct) => await GetCpProfiles(); + Post["/cpprofiles", true] = async (x, ct) => await GetCpProfiles(); Post["/cpapikey"] = x => GetCpApiKey(); Get["/emailnotification"] = _ => EmailNotifications(); Post["/emailnotification"] = _ => SaveEmailNotifications(); Post["/testemailnotification"] = _ => TestEmailNotifications(); - Get["/status", true] = async (x,ct) => await Status(); + Get["/status", true] = async (x, ct) => await Status(); Get["/pushbulletnotification"] = _ => PushbulletNotifications(); Post["/pushbulletnotification"] = _ => SavePushbulletNotifications(); @@ -268,7 +268,7 @@ namespace PlexRequests.UI.Modules Analytics.TrackEventAsync(Category.Admin, Action.Save, "CollectAnalyticData turned off", Username, CookieHelper.GetAnalyticClientId(Cookies)); } var result = PrService.SaveSettings(model); - + Analytics.TrackEventAsync(Category.Admin, Action.Save, "PlexRequestSettings", Username, CookieHelper.GetAnalyticClientId(Cookies)); return Response.AsJson(result ? new JsonResponseModel { Result = true } @@ -377,7 +377,7 @@ namespace PlexRequests.UI.Modules return View["Plex", settings]; } - private Response SavePlex() + private async Task<Response> SavePlex() { var plexSettings = this.Bind<PlexSettings>(); var valid = this.Validate(plexSettings); @@ -386,8 +386,11 @@ 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; - var result = PlexService.SaveSettings(plexSettings); + var result = await PlexService.SaveSettingsAsync(plexSettings); return Response.AsJson(result ? new JsonResponseModel { Result = true, Message = "Successfully Updated the Settings for Plex!" } @@ -517,7 +520,7 @@ namespace PlexRequests.UI.Modules { if (string.IsNullOrEmpty(settings.EmailUsername) || string.IsNullOrEmpty(settings.EmailPassword)) { - return Response.AsJson(new JsonResponseModel {Result = false, Message = "SMTP Authentication is enabled, please specify a username and password"}); + return Response.AsJson(new JsonResponseModel { Result = false, Message = "SMTP Authentication is enabled, please specify a username and password" }); } } @@ -542,7 +545,7 @@ namespace PlexRequests.UI.Modules { 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}); + var md = new Markdown(new MarkdownOptions { AutoNewLines = true, AutoHyperlink = true }); status.ReleaseNotes = md.Transform(status.ReleaseNotes); return View["Status", status]; } @@ -711,7 +714,7 @@ namespace PlexRequests.UI.Modules private Response GetCpApiKey() { var settings = this.Bind<CouchPotatoSettings>(); - + if (string.IsNullOrEmpty(settings.Username) || string.IsNullOrEmpty(settings.Password)) { return Response.AsJson(new { Message = "Please enter a username and password to request the Api Key", Result = false }); @@ -938,12 +941,12 @@ namespace PlexRequests.UI.Modules { await LogsRepo.DeleteAsync(logEntity); } - return Response.AsJson(new JsonResponseModel { Result = true, Message = "Logs cleared successfully."}); + return Response.AsJson(new JsonResponseModel { Result = true, Message = "Logs cleared successfully." }); } catch (Exception e) { Log.Error(e); - return Response.AsJson(new JsonResponseModel { Result = false, Message = e.Message }); + return Response.AsJson(new JsonResponseModel { Result = false, Message = e.Message }); } } } diff --git a/PlexRequests.UI/Modules/BaseModule.cs b/PlexRequests.UI/Modules/BaseModule.cs index 1a205202b..a01253786 100644 --- a/PlexRequests.UI/Modules/BaseModule.cs +++ b/PlexRequests.UI/Modules/BaseModule.cs @@ -122,7 +122,7 @@ namespace PlexRequests.UI.Modules { get { - if (Context?.CurrentUser == null) + if (!LoggedIn) { return false; } @@ -130,6 +130,9 @@ namespace PlexRequests.UI.Modules return claims.Contains(UserClaims.Admin) || claims.Contains(UserClaims.PowerUser); } } + + protected bool LoggedIn => Context?.CurrentUser != null; + protected string Culture { get; set; } protected const string CultureCookieName = "_culture"; protected Response SetCookie() diff --git a/PlexRequests.UI/Modules/IndexModule.cs b/PlexRequests.UI/Modules/IndexModule.cs index 3d7023d39..fd13acb35 100644 --- a/PlexRequests.UI/Modules/IndexModule.cs +++ b/PlexRequests.UI/Modules/IndexModule.cs @@ -59,23 +59,23 @@ namespace PlexRequests.UI.Modules if (!string.IsNullOrEmpty(Username)) { // They are not logged in - return Context.GetRedirect(Linker.BuildAbsoluteUri(Context, "LandingPageIndex").ToString()); + return Context.GetRedirect(Linker.BuildRelativeUri(Context, "LandingPageIndex").ToString()); } - return Context.GetRedirect(Linker.BuildAbsoluteUri(Context, "SearchIndex").ToString()); + return Context.GetRedirect(Linker.BuildRelativeUri(Context, "SearchIndex").ToString()); } // After login if (string.IsNullOrEmpty(Username)) { // Not logged in yet - return Context.GetRedirect(Linker.BuildAbsoluteUri(Context, "UserLoginIndex").ToString()); + return Context.GetRedirect(Linker.BuildRelativeUri(Context, "UserLoginIndex").ToString()); } // Send them to landing - var landingUrl = Linker.BuildAbsoluteUri(Context, "LandingPageIndex").ToString(); + var landingUrl = Linker.BuildRelativeUri(Context, "LandingPageIndex").ToString(); return Context.GetRedirect(landingUrl); } - return Context.GetRedirect(Linker.BuildAbsoluteUri(Context, "UserLoginIndex").ToString()); + return Context.GetRedirect(Linker.BuildRelativeUri(Context, "UserLoginIndex").ToString()); } } } \ No newline at end of file diff --git a/PlexRequests.UI/Modules/LandingPageModule.cs b/PlexRequests.UI/Modules/LandingPageModule.cs index 92517f2f7..dccf69430 100644 --- a/PlexRequests.UI/Modules/LandingPageModule.cs +++ b/PlexRequests.UI/Modules/LandingPageModule.cs @@ -52,7 +52,7 @@ namespace PlexRequests.UI.Modules var s = await LandingSettings.GetSettingsAsync(); if (!s.BeforeLogin && string.IsNullOrEmpty(Username)) //We are signed in { - var url = Linker.BuildAbsoluteUri(Context, "SearchIndex").ToString(); + var url = Linker.BuildRelativeUri(Context, "SearchIndex").ToString(); return Response.AsRedirect(url); } diff --git a/PlexRequests.UI/Modules/LoginModule.cs b/PlexRequests.UI/Modules/LoginModule.cs index 8e6a9d39c..8c3bd550d 100644 --- a/PlexRequests.UI/Modules/LoginModule.cs +++ b/PlexRequests.UI/Modules/LoginModule.cs @@ -1,150 +1,154 @@ -#region Copyright -// /************************************************************************ -// Copyright (c) 2016 Jamie Rees -// File: LoginModule.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.Dynamic; - -using Nancy; -using Nancy.Authentication.Forms; -using Nancy.Extensions; -using Nancy.Responses.Negotiation; -using Nancy.Security; - -using PlexRequests.Core; -using PlexRequests.Core.SettingModels; -using PlexRequests.Helpers; -using PlexRequests.UI.Models; - -namespace PlexRequests.UI.Modules -{ - public class LoginModule : BaseModule - { - public LoginModule(ISettingsService<PlexRequestSettings> pr, ICustomUserMapper m) : base(pr) - { - UserMapper = m; - Get["/login"] = _ => - { - { - dynamic model = new ExpandoObject(); - model.Redirect = Request.Query.redirect.Value ?? string.Empty; - model.Errored = Request.Query.error.HasValue; - var adminCreated = UserMapper.DoUsersExist(); - model.AdminExists = adminCreated; - return View["Index", model]; - } - - }; - - Get["/logout"] = x => this.LogoutAndRedirect(!string.IsNullOrEmpty(BaseUrl) ? $"~/{BaseUrl}/" : "~/"); - - Post["/login"] = x => - { - var username = (string)Request.Form.Username; - var password = (string)Request.Form.Password; - var dtOffset = (int)Request.Form.DateTimeOffset; - var redirect = (string)Request.Form.Redirect; - - var userId = UserMapper.ValidateUser(username, password); - - if (userId == null) - { - return Context.GetRedirect(!string.IsNullOrEmpty(BaseUrl) ? $"~/{BaseUrl}/login?error=true&username=" + username : "~/login?error=true&username=" + username); - } - DateTime? expiry = null; - if (Request.Form.RememberMe.HasValue) - { - expiry = DateTime.Now.AddDays(7); - } - Session[SessionKeys.UsernameKey] = username; - Session[SessionKeys.ClientDateTimeOffsetKey] = dtOffset; - if(redirect.Contains("userlogin")){ - redirect = !string.IsNullOrEmpty(BaseUrl) ? $"/{BaseUrl}/search" : "/search"; - } - return this.LoginAndRedirect(userId.Value, expiry, redirect); - }; - - Get["/register"] = x => - { - { - dynamic model = new ExpandoObject(); - model.Errored = Request.Query.error.HasValue; - - return View["Register", model]; - } - }; - - Post["/register"] = x => - { - var username = (string)Request.Form.Username; - var exists = UserMapper.DoUsersExist(); - if (exists) - { - return Context.GetRedirect(!string.IsNullOrEmpty(BaseUrl) ? $"~/{BaseUrl}/register?error=true" : "~/register?error=true"); - } - var userId = UserMapper.CreateAdmin(username, Request.Form.Password); - Session[SessionKeys.UsernameKey] = username; - return this.LoginAndRedirect((Guid)userId); - }; - - Get["/changepassword"] = _ => ChangePassword(); - Post["/changepassword"] = _ => ChangePasswordPost(); - } - private ICustomUserMapper UserMapper { get; } - - private Negotiator ChangePassword() - { - this.RequiresAuthentication(); - return View["ChangePassword"]; - } - - private Response ChangePasswordPost() - { - var username = Context.CurrentUser.UserName; - var oldPass = Request.Form.OldPassword; - var newPassword = Request.Form.NewPassword; - var newPasswordAgain = Request.Form.NewPasswordAgain; - - if (string.IsNullOrEmpty(oldPass) || string.IsNullOrEmpty(newPassword) || - string.IsNullOrEmpty(newPasswordAgain)) - { - return Response.AsJson(new JsonResponseModel { Message = "Please fill in all fields", Result = false }); - } - - if (!newPassword.Equals(newPasswordAgain)) - { - return Response.AsJson(new JsonResponseModel { Message = "The passwords do not match", Result = false }); - } - - var result = UserMapper.UpdatePassword(username, oldPass, newPassword); - if (result) - { - return Response.AsJson(new JsonResponseModel { Message = "Password has been changed!", Result = true }); - } - - return Response.AsJson(new JsonResponseModel { Message = "Could not update the password in the database", Result = false }); - } - } +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: LoginModule.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.Dynamic; + +using Nancy; +using Nancy.Authentication.Forms; +using Nancy.Extensions; +using Nancy.Linker; +using Nancy.Responses.Negotiation; +using Nancy.Security; + +using PlexRequests.Core; +using PlexRequests.Core.SettingModels; +using PlexRequests.Helpers; +using PlexRequests.UI.Models; + +namespace PlexRequests.UI.Modules +{ + public class LoginModule : BaseModule + { + public LoginModule(ISettingsService<PlexRequestSettings> pr, ICustomUserMapper m, IResourceLinker linker) : base(pr) + { + UserMapper = m; + Get["/login"] = _ => + { + if (LoggedIn) + { + var url = linker.BuildRelativeUri(Context, "SearchIndex"); + return Response.AsRedirect(url.ToString()); + } + dynamic model = new ExpandoObject(); + model.Redirect = Request.Query.redirect.Value ?? string.Empty; + model.Errored = Request.Query.error.HasValue; + var adminCreated = UserMapper.DoUsersExist(); + model.AdminExists = adminCreated; + return View["Index", model]; + }; + + Get["/logout"] = x => this.LogoutAndRedirect(!string.IsNullOrEmpty(BaseUrl) ? $"~/{BaseUrl}/" : "~/"); + + Post["/login"] = x => + { + var username = (string)Request.Form.Username; + var password = (string)Request.Form.Password; + var dtOffset = (int)Request.Form.DateTimeOffset; + var redirect = (string)Request.Form.Redirect; + + var userId = UserMapper.ValidateUser(username, password); + + if (userId == null) + { + return Context.GetRedirect(!string.IsNullOrEmpty(BaseUrl) ? $"~/{BaseUrl}/login?error=true&username=" + username : "~/login?error=true&username=" + username); + } + DateTime? expiry = null; + if (Request.Form.RememberMe.HasValue) + { + expiry = DateTime.Now.AddDays(7); + } + Session[SessionKeys.UsernameKey] = username; + Session[SessionKeys.ClientDateTimeOffsetKey] = dtOffset; + if (redirect.Contains("userlogin")) + { + redirect = !string.IsNullOrEmpty(BaseUrl) ? $"/{BaseUrl}/search" : "/search"; + } + return this.LoginAndRedirect(userId.Value, expiry, redirect); + }; + + Get["/register"] = x => + { + { + dynamic model = new ExpandoObject(); + model.Errored = Request.Query.error.HasValue; + + return View["Register", model]; + } + }; + + Post["/register"] = x => + { + var username = (string)Request.Form.Username; + var exists = UserMapper.DoUsersExist(); + if (exists) + { + return Context.GetRedirect(!string.IsNullOrEmpty(BaseUrl) ? $"~/{BaseUrl}/register?error=true" : "~/register?error=true"); + } + var userId = UserMapper.CreateAdmin(username, Request.Form.Password); + Session[SessionKeys.UsernameKey] = username; + return this.LoginAndRedirect((Guid)userId); + }; + + Get["/changepassword"] = _ => ChangePassword(); + Post["/changepassword"] = _ => ChangePasswordPost(); + } + private ICustomUserMapper UserMapper { get; } + + private Negotiator ChangePassword() + { + this.RequiresAuthentication(); + return View["ChangePassword"]; + } + + private Response ChangePasswordPost() + { + var username = Context.CurrentUser.UserName; + var oldPass = Request.Form.OldPassword; + var newPassword = Request.Form.NewPassword; + var newPasswordAgain = Request.Form.NewPasswordAgain; + + if (string.IsNullOrEmpty(oldPass) || string.IsNullOrEmpty(newPassword) || + string.IsNullOrEmpty(newPasswordAgain)) + { + return Response.AsJson(new JsonResponseModel { Message = "Please fill in all fields", Result = false }); + } + + if (!newPassword.Equals(newPasswordAgain)) + { + return Response.AsJson(new JsonResponseModel { Message = "The passwords do not match", Result = false }); + } + + var result = UserMapper.UpdatePassword(username, oldPass, newPassword); + if (result) + { + return Response.AsJson(new JsonResponseModel { Message = "Password has been changed!", Result = true }); + } + + return Response.AsJson(new JsonResponseModel { Message = "Could not update the password in the database", Result = false }); + } + } } \ No newline at end of file diff --git a/PlexRequests.UI/Modules/RequestsBetaModule.cs b/PlexRequests.UI/Modules/RequestsBetaModule.cs new file mode 100644 index 000000000..a8a1a6e3f --- /dev/null +++ b/PlexRequests.UI/Modules/RequestsBetaModule.cs @@ -0,0 +1,453 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: RequestsModule.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 Nancy; +using Nancy.Responses.Negotiation; +using Nancy.Security; + +using PlexRequests.Core; +using PlexRequests.Core.SettingModels; +using PlexRequests.Services.Interfaces; +using PlexRequests.Services.Notification; +using PlexRequests.Store; +using PlexRequests.UI.Models; +using PlexRequests.Helpers; +using PlexRequests.UI.Helpers; +using System.Collections.Generic; +using PlexRequests.Api.Interfaces; +using System.Threading.Tasks; + +using NLog; + +using PlexRequests.Core.Models; +using PlexRequests.Helpers.Analytics; + +using Action = PlexRequests.Helpers.Analytics.Action; + +namespace PlexRequests.UI.Modules +{ + public class RequestsBetaModule : BaseAuthModule + { + public RequestsBetaModule( + IRequestService service, + ISettingsService<PlexRequestSettings> prSettings, + ISettingsService<RequestSettings> requestSettings, + ISettingsService<PlexSettings> plex, + INotificationService notify, + ISettingsService<SonarrSettings> sonarrSettings, + ISettingsService<SickRageSettings> sickRageSettings, + ISettingsService<CouchPotatoSettings> cpSettings, + ICouchPotatoApi cpApi, + ISonarrApi sonarrApi, + ISickRageApi sickRageApi, + ICacheProvider cache, + IAnalytics an) : base("requestsbeta", prSettings) + { + Service = service; + PrSettings = prSettings; + PlexSettings = plex; + NotificationService = notify; + SonarrSettings = sonarrSettings; + SickRageSettings = sickRageSettings; + CpSettings = cpSettings; + SonarrApi = sonarrApi; + SickRageApi = sickRageApi; + CpApi = cpApi; + Cache = cache; + Analytics = an; + + Get["/"] = x => LoadRequests(); + Get["/plexrequestsettings", true] = async (x, ct) => await GetPlexRequestSettings(); + Get["/requestsettings", true] = async (x, ct) => await GetRequestSettings(); + Get["/movies", true] = async (x, ct) => await GetMovies(); + Get["/movies/{searchTerm}", true] = async (x, ct) => await GetMovies((string)x.searchTerm); + + + // Everything below is not being used in the beta page + Get["/tvshows", true] = async (c, ct) => await GetTvShows(); + Get["/albums", true] = async (x, ct) => await GetAlbumRequests(); + Post["/delete", true] = async (x, ct) => await DeleteRequest((int)Request.Form.id); + Post["/reportissue", true] = async (x, ct) => await ReportIssue((int)Request.Form.requestId, (IssueState)(int)Request.Form.issue, null); + Post["/reportissuecomment", true] = async (x, ct) => await ReportIssue((int)Request.Form.requestId, IssueState.Other, (string)Request.Form.commentArea); + + Post["/clearissues", true] = async (x, ct) => await ClearIssue((int)Request.Form.Id); + + Post["/changeavailability", true] = async (x, ct) => await ChangeRequestAvailability((int)Request.Form.Id, (bool)Request.Form.Available); + } + + private static Logger Log = LogManager.GetCurrentClassLogger(); + private IRequestService Service { get; } + private IAnalytics Analytics { get; } + private INotificationService NotificationService { get; } + private ISettingsService<PlexRequestSettings> PrSettings { get; } + private ISettingsService<PlexSettings> PlexSettings { get; } + private ISettingsService<RequestSettings> RequestSettings { get; } + private ISettingsService<SonarrSettings> SonarrSettings { get; } + private ISettingsService<SickRageSettings> SickRageSettings { get; } + private ISettingsService<CouchPotatoSettings> CpSettings { get; } + private ISonarrApi SonarrApi { get; } + private ISickRageApi SickRageApi { get; } + private ICouchPotatoApi CpApi { get; } + private ICacheProvider Cache { get; } + + private Negotiator LoadRequests() + { + return View["Index"]; + } + + private async Task<Response> GetPlexRequestSettings() + { + return Response.AsJson(await PrSettings.GetSettingsAsync()); + } + + private async Task<Response> GetRequestSettings() + { + return Response.AsJson(await RequestSettings.GetSettingsAsync()); + } + + private async Task<Response> GetMovies(string searchTerm = null, bool approved = false, bool notApproved = false, + bool available = false, bool notAvailable = false, bool released = false, bool notReleased = false) + { + var dbMovies = await FilterMovies(searchTerm, approved, notApproved, available, notAvailable, released, notReleased); + var qualities = await GetQualityProfiles(); + var model = MapMoviesToView(dbMovies.ToList(), qualities); + + return Response.AsJson(model); + } + + private async Task<Response> GetTvShows() + { + var settingsTask = PrSettings.GetSettingsAsync(); + + var requests = await Service.GetAllAsync(); + requests = requests.Where(x => x.Type == RequestType.TvShow); + + var dbTv = requests; + var settings = await settingsTask; + if (settings.UsersCanViewOnlyOwnRequests && !IsAdmin) + { + dbTv = dbTv.Where(x => x.UserHasRequested(Username)).ToList(); + } + + IEnumerable<QualityModel> qualities = new List<QualityModel>(); + if (IsAdmin) + { + try + { + var sonarrSettings = await SonarrSettings.GetSettingsAsync(); + if (sonarrSettings.Enabled) + { + var result = Cache.GetOrSetAsync(CacheKeys.SonarrQualityProfiles, async () => + { + return await Task.Run(() => SonarrApi.GetProfiles(sonarrSettings.ApiKey, sonarrSettings.FullUri)); + }); + qualities = result.Result.Select(x => new QualityModel { Id = x.id.ToString(), Name = x.name }).ToList(); + } + else + { + var sickRageSettings = await SickRageSettings.GetSettingsAsync(); + if (sickRageSettings.Enabled) + { + qualities = sickRageSettings.Qualities.Select(x => new QualityModel { Id = x.Key, Name = x.Value }).ToList(); + } + } + } + catch (Exception e) + { + Log.Info(e); + } + + } + + var viewModel = dbTv.Select(tv => new RequestViewModel + { + ProviderId = tv.ProviderId, + Type = tv.Type, + Status = tv.Status, + ImdbId = tv.ImdbId, + Id = tv.Id, + PosterPath = tv.PosterPath, + ReleaseDate = tv.ReleaseDate, + ReleaseDateTicks = tv.ReleaseDate.Ticks, + RequestedDate = tv.RequestedDate, + RequestedDateTicks = DateTimeHelper.OffsetUTCDateTime(tv.RequestedDate, DateTimeOffset).Ticks, + Released = DateTime.Now > tv.ReleaseDate, + 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, + IssueId = tv.IssueId, + TvSeriesRequestType = tv.SeasonsRequested, + Qualities = qualities.ToArray(), + Episodes = tv.Episodes.ToArray(), + }).ToList(); + + return Response.AsJson(viewModel); + } + + private async Task<Response> GetAlbumRequests() + { + var settings = PrSettings.GetSettings(); + var dbAlbum = await Service.GetAllAsync(); + dbAlbum = dbAlbum.Where(x => x.Type == RequestType.Album); + if (settings.UsersCanViewOnlyOwnRequests && !IsAdmin) + { + dbAlbum = dbAlbum.Where(x => x.UserHasRequested(Username)); + } + + var viewModel = dbAlbum.Select(album => + { + return new RequestViewModel + { + ProviderId = album.ProviderId, + Type = album.Type, + Status = album.Status, + ImdbId = album.ImdbId, + Id = album.Id, + PosterPath = album.PosterPath, + ReleaseDate = album.ReleaseDate, + ReleaseDateTicks = album.ReleaseDate.Ticks, + RequestedDate = album.RequestedDate, + RequestedDateTicks = DateTimeHelper.OffsetUTCDateTime(album.RequestedDate, DateTimeOffset).Ticks, + Released = DateTime.Now > album.ReleaseDate, + Approved = album.Available || album.Approved, + Title = album.Title, + Overview = album.Overview, + RequestedUsers = IsAdmin ? album.AllUsers.ToArray() : new string[] { }, + ReleaseYear = album.ReleaseDate.Year.ToString(), + Available = album.Available, + Admin = IsAdmin, + IssueId = album.IssueId, + TvSeriesRequestType = album.SeasonsRequested, + MusicBrainzId = album.MusicBrainzId, + ArtistName = album.ArtistName + + }; + }).ToList(); + + return Response.AsJson(viewModel); + } + + private async Task<Response> DeleteRequest(int requestid) + { + this.RequiresClaims(UserClaims.Admin); + Analytics.TrackEventAsync(Category.Requests, Action.Delete, "Delete Request", Username, CookieHelper.GetAnalyticClientId(Cookies)); + + var currentEntity = await Service.GetAsync(requestid); + await Service.DeleteRequestAsync(currentEntity); + return Response.AsJson(new JsonResponseModel { Result = true }); + } + + /// <summary> + /// Reports the issue. + /// Comment can be null if the <c>IssueState != Other</c> + /// </summary> + /// <param name="requestId">The request identifier.</param> + /// <param name="issue">The issue.</param> + /// <param name="comment">The comment.</param> + /// <returns></returns> + private async Task<Response> ReportIssue(int requestId, IssueState issue, string comment) + { + var originalRequest = await Service.GetAsync(requestId); + if (originalRequest == null) + { + return Response.AsJson(new JsonResponseModel { Result = false, Message = "Could not add issue, please try again or contact the administrator!" }); + } + originalRequest.Issues = issue; + originalRequest.OtherMessage = !string.IsNullOrEmpty(comment) + ? $"{Username} - {comment}" + : string.Empty; + + + var result = await Service.UpdateRequestAsync(originalRequest); + + var model = new NotificationModel + { + User = Username, + NotificationType = NotificationType.Issue, + Title = originalRequest.Title, + DateTime = DateTime.Now, + Body = issue == IssueState.Other ? comment : issue.ToString().ToCamelCaseWords() + }; + await NotificationService.Publish(model); + + return Response.AsJson(result + ? new JsonResponseModel { Result = true } + : new JsonResponseModel { Result = false, Message = "Could not add issue, please try again or contact the administrator!" }); + } + + private async Task<Response> ClearIssue(int requestId) + { + this.RequiresClaims(UserClaims.Admin); + + var originalRequest = await Service.GetAsync(requestId); + if (originalRequest == null) + { + return Response.AsJson(new JsonResponseModel { Result = false, Message = "Request does not exist to clear it!" }); + } + originalRequest.Issues = IssueState.None; + originalRequest.OtherMessage = string.Empty; + + var result = await Service.UpdateRequestAsync(originalRequest); + return Response.AsJson(result + ? new JsonResponseModel { Result = true } + : new JsonResponseModel { Result = false, Message = "Could not clear issue, please try again or check the logs" }); + } + + private async Task<Response> ChangeRequestAvailability(int requestId, bool available) + { + this.RequiresClaims(UserClaims.Admin); + Analytics.TrackEventAsync(Category.Requests, Action.Update, available ? "Make request available" : "Make request unavailable", Username, CookieHelper.GetAnalyticClientId(Cookies)); + var originalRequest = await Service.GetAsync(requestId); + if (originalRequest == null) + { + return Response.AsJson(new JsonResponseModel { Result = false, Message = "Request does not exist to change the availability!" }); + } + + originalRequest.Available = available; + + var result = await Service.UpdateRequestAsync(originalRequest); + 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" }); + } + + private List<RequestViewModel> MapMoviesToView(List<RequestedModel> dbMovies, List<QualityModel> qualities) + { + return dbMovies.Select(movie => new RequestViewModel + { + ProviderId = movie.ProviderId, + Type = movie.Type, + Status = movie.Status, + ImdbId = movie.ImdbId, + Id = movie.Id, + PosterPath = movie.PosterPath, + ReleaseDate = movie.ReleaseDate, + ReleaseDateTicks = movie.ReleaseDate.Ticks, + RequestedDate = movie.RequestedDate, + Released = DateTime.Now > movie.ReleaseDate, + RequestedDateTicks = DateTimeHelper.OffsetUTCDateTime(movie.RequestedDate, DateTimeOffset).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, + IssueId = movie.IssueId, + Qualities = qualities.ToArray() + }).ToList(); + } + + private async Task<List<QualityModel>> GetQualityProfiles() + { + var qualities = new List<QualityModel>(); + if (IsAdmin) + { + var cpSettings = CpSettings.GetSettings(); + if (cpSettings.Enabled) + { + try + { + var result = await Cache.GetOrSetAsync(CacheKeys.CouchPotatoQualityProfiles, async () => + { + return await Task.Run(() => CpApi.GetProfiles(cpSettings.FullUri, cpSettings.ApiKey)).ConfigureAwait(false); + }); + if (result != null) + { + qualities = result.list.Select(x => new QualityModel { Id = x._id, Name = x.label }).ToList(); + } + } + catch (Exception e) + { + Log.Info(e); + } + } + } + return qualities; + } + + private async Task<IEnumerable<RequestedModel>> FilterMovies(string searchTerm = null, bool approved = false, bool notApproved = false, + bool available = false, bool notAvailable = false, bool released = false, bool notReleased = false) + { + var settings = PrSettings.GetSettings(); + var allRequests = await Service.GetAllAsync(); + allRequests = allRequests.Where(x => x.Type == RequestType.Movie); + + var dbMovies = allRequests; + + if (settings.UsersCanViewOnlyOwnRequests && !IsAdmin) + { + dbMovies = dbMovies.Where(x => x.UserHasRequested(Username)); + } + + // Filter the movies on the search term + if (!string.IsNullOrEmpty(searchTerm)) + { + dbMovies = dbMovies.Where(x => x.Title.Contains(searchTerm)); + } + + if (approved) + { + dbMovies = dbMovies.Where(x => x.Approved); + } + + if (notApproved) + { + dbMovies = dbMovies.Where(x => !x.Approved); + } + + if (available) + { + dbMovies = dbMovies.Where(x => x.Available); + } + + if (notAvailable) + { + dbMovies = dbMovies.Where(x => !x.Available); + } + + if (released) + { + dbMovies = dbMovies.Where(x => DateTime.Now > x.ReleaseDate); + } + + if (notReleased) + { + dbMovies = dbMovies.Where(x => DateTime.Now < x.ReleaseDate); + } + + return dbMovies; + } + } +} diff --git a/PlexRequests.UI/Modules/RequestsModule.cs b/PlexRequests.UI/Modules/RequestsModule.cs index cb8ed6882..8a2a9821a 100644 --- a/PlexRequests.UI/Modules/RequestsModule.cs +++ b/PlexRequests.UI/Modules/RequestsModule.cs @@ -67,7 +67,8 @@ namespace PlexRequests.UI.Modules ISonarrApi sonarrApi, ISickRageApi sickRageApi, ICacheProvider cache, - IAnalytics an) : base("requests", prSettings) + IAnalytics an, + INotificationEngine engine) : base("requests", prSettings) { Service = service; PrSettings = prSettings; @@ -81,6 +82,7 @@ namespace PlexRequests.UI.Modules CpApi = cpApi; Cache = cache; Analytics = an; + NotificationEngine = engine; Get["/", true] = async (x, ct) => await LoadRequests(); Get["/movies", true] = async (x, ct) => await GetMovies(); @@ -108,6 +110,7 @@ namespace PlexRequests.UI.Modules private ISickRageApi SickRageApi { get; } private ICouchPotatoApi CpApi { get; } private ICacheProvider Cache { get; } + private INotificationEngine NotificationEngine { get; } private async Task<Negotiator> LoadRequests() { @@ -376,6 +379,8 @@ namespace PlexRequests.UI.Modules originalRequest.Available = available; var result = await Service.UpdateRequestAsync(originalRequest); + var plexService = await PlexSettings.GetSettingsAsync(); + await NotificationEngine.NotifyUsers(originalRequest, plexService.PlexAuthToken); 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" }); diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index 5f0fa4ed1..73f016f24 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -110,7 +110,7 @@ namespace PlexRequests.UI.Modules Get["movie/{searchTerm}", true] = async (x, ct) => await SearchMovie((string)x.searchTerm); Get["tv/{searchTerm}", true] = async (x, ct) => await SearchTvShow((string)x.searchTerm); - Get["music/{searchTerm}", true] = async (x, ct) => await SearchMusic((string)x.searchTerm); + Get["music/{searchTerm}", true] = async (x, ct) => await SearchAlbum((string)x.searchTerm); Get["music/coverArt/{id}"] = p => GetMusicBrainzCoverArt((string)p.id); Get["movie/upcoming", true] = async (x, ct) => await UpcomingMovies(); @@ -252,9 +252,11 @@ namespace PlexRequests.UI.Modules VoteCount = movie.VoteCount }; var canSee = CanUserSeeThisRequest(viewMovie.Id, settings.UsersCanViewOnlyOwnRequests, dbMovies); - if (Checker.IsMovieAvailable(plexMovies.ToArray(), movie.Title, movie.ReleaseDate?.Year.ToString())) + var plexMovie = Checker.GetMovie(plexMovies.ToArray(), movie.Title, movie.ReleaseDate?.Year.ToString()); + if (plexMovie != null) { viewMovie.Available = true; + viewMovie.PlexUrl = plexMovie.Url; } else if (dbMovies.ContainsKey(movie.Id) && canSee) // compare to the requests db { @@ -343,9 +345,12 @@ namespace PlexRequests.UI.Modules providerId = viewT.Id.ToString(); } - if (Checker.IsTvShowAvailable(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; + viewT.PlexUrl = plexShow.Url; } else if (t.show?.externals?.thetvdb != null) { @@ -371,7 +376,7 @@ namespace PlexRequests.UI.Modules return Response.AsJson(viewTv); } - private async Task<Response> SearchMusic(string searchTerm) + private async Task<Response> SearchAlbum(string searchTerm) { Analytics.TrackEventAsync(Category.Search, Action.Album, searchTerm, Username, CookieHelper.GetAnalyticClientId(Cookies)); var apiAlbums = new List<Release>(); @@ -405,9 +410,11 @@ namespace PlexRequests.UI.Modules DateTime release; DateTimeHelper.CustomParse(a.ReleaseEvents?.FirstOrDefault()?.date, out release); var artist = a.ArtistCredit?.FirstOrDefault()?.artist; - if (Checker.IsAlbumAvailable(plexAlbums.ToArray(), a.title, release.ToString("yyyy"), artist?.name)) + var plexAlbum = Checker.GetAlbum(plexAlbums.ToArray(), a.title, release.ToString("yyyy"), artist?.name); + if (plexAlbum != null) { viewA.Available = true; + viewA.PlexUrl = plexAlbum.Url; } if (!string.IsNullOrEmpty(a.id) && dbAlbum.ContainsKey(a.id)) { diff --git a/PlexRequests.UI/Modules/UserLoginModule.cs b/PlexRequests.UI/Modules/UserLoginModule.cs index 61481faef..e30f4adc2 100644 --- a/PlexRequests.UI/Modules/UserLoginModule.cs +++ b/PlexRequests.UI/Modules/UserLoginModule.cs @@ -61,7 +61,17 @@ namespace PlexRequests.UI.Modules PlexSettings = plexSettings; Linker = linker; - Get["UserLoginIndex", "/", true] = async (x, ct) => await Index(); + Get["UserLoginIndex", "/", true] = async (x, ct) => + { + if (!string.IsNullOrEmpty(Username) || IsAdmin) + { + var uri = Linker.BuildRelativeUri(Context, "SearchIndex"); + return Response.AsRedirect(uri.ToString()); + } + var settings = await AuthService.GetSettingsAsync(); + return View["Index", settings]; + }; + Post["/", true] = async (x, ct) => await LoginUser(); Get["/logout"] = x => Logout(); } @@ -75,12 +85,6 @@ namespace PlexRequests.UI.Modules private static Logger Log = LogManager.GetCurrentClassLogger(); - public async Task<Negotiator> Index() - { - var settings = await AuthService.GetSettingsAsync(); - return View["Index", settings]; - } - private async Task<Response> LoginUser() { var dateTimeOffset = Request.Form.DateTimeOffset; @@ -89,7 +93,7 @@ namespace PlexRequests.UI.Modules if (string.IsNullOrWhiteSpace(username)) { Session["TempMessage"] = Resources.UI.UserLogin_IncorrectUserPass; - var uri = Linker.BuildAbsoluteUri(Context, "UserLoginIndex"); + var uri = Linker.BuildRelativeUri(Context, "UserLoginIndex"); return Response.AsRedirect(uri.ToString()); // TODO Check this } @@ -102,7 +106,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.BuildAbsoluteUri(Context, "UserLoginIndex"); + var uri = Linker.BuildRelativeUri(Context, "UserLoginIndex"); return Response.AsRedirect(uri.ToString()); // TODO Check this } @@ -161,7 +165,7 @@ namespace PlexRequests.UI.Modules if (!authenticated) { - var uri = Linker.BuildAbsoluteUri(Context, "UserLoginIndex"); + var uri = Linker.BuildRelativeUri(Context, "UserLoginIndex"); Session["TempMessage"] = Resources.UI.UserLogin_IncorrectUserPass; return Response.AsRedirect(uri.ToString()); // TODO Check this } @@ -172,11 +176,11 @@ namespace PlexRequests.UI.Modules { if (!landingSettings.BeforeLogin) { - var uri = Linker.BuildAbsoluteUri(Context, "LandingPageIndex"); + var uri = Linker.BuildRelativeUri(Context, "LandingPageIndex"); return Response.AsRedirect(uri.ToString()); } } - var retVal = Linker.BuildAbsoluteUri(Context, "SearchIndex"); + var retVal = Linker.BuildRelativeUri(Context, "SearchIndex"); return Response.AsRedirect(retVal.ToString()); // TODO Check this } diff --git a/PlexRequests.UI/Modules/UserManagementModule.cs b/PlexRequests.UI/Modules/UserManagementModule.cs index 9667dac6f..1e9909565 100644 --- a/PlexRequests.UI/Modules/UserManagementModule.cs +++ b/PlexRequests.UI/Modules/UserManagementModule.cs @@ -4,9 +4,10 @@ using System.Linq; 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.Core; using PlexRequests.Core.Models; @@ -30,9 +31,10 @@ namespace PlexRequests.UI.Modules Get["/"] = x => Load(); Get["/users", true] = async (x, ct) => await LoadUsers(); - Post["/createuser"] = x => CreateUser(Request.Form["username"].ToString(), Request.Form["password"].ToString()); + Post["/createuser"] = x => CreateUser(); Get["/local/{id}"] = x => LocalDetails((Guid)x.id); - Get["/plex/{id}", true] = async (x,ct) => await PlexDetails(x.id); + Get["/plex/{id}", true] = async (x, ct) => await PlexDetails(x.id); + Get["/claims"] = x => GetClaims(); } private ICustomUserMapper UserMapper { get; } @@ -57,11 +59,12 @@ namespace PlexRequests.UI.Modules model.Add(new UserManagementUsersViewModel { - Id= user.UserGuid, + Id = user.UserGuid, Claims = claimsString, Username = user.UserName, Type = UserType.LocalUser, - EmailAddress = userProps.EmailAddress + EmailAddress = userProps.EmailAddress, + ClaimsArray = claims }); } @@ -91,9 +94,17 @@ namespace PlexRequests.UI.Modules return Response.AsJson(model); } - private Response CreateUser(string username, string password) + private Response CreateUser() { - if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) + var body = Request.Body.AsString(); + if (string.IsNullOrEmpty(body)) + { + return Response.AsJson(new JsonResponseModel { Result = false, Message = "Could not save user, invalid JSON body" }); + } + + var model = JsonConvert.DeserializeObject<UserManagementCreateModel>(body); + + if (string.IsNullOrWhiteSpace(model.Username) || string.IsNullOrWhiteSpace(model.Password)) { return Response.AsJson(new JsonResponseModel { @@ -101,7 +112,7 @@ namespace PlexRequests.UI.Modules Message = "Please enter in a valid Username and Password" }); } - var user = UserMapper.CreateRegularUser(username, password); + var user = UserMapper.CreateUser(model.Username, model.Password, model.Claims, new UserProperties { EmailAddress = model.EmailAddress }); if (user.HasValue) { return Response.AsJson(user); @@ -139,6 +150,21 @@ namespace PlexRequests.UI.Modules return Nancy.Response.NoBody; } + + /// <summary> + /// Returns all claims for the users. + /// </summary> + /// <returns></returns> + private Response GetClaims() + { + var retVal = new List<dynamic>(); + var claims = UserMapper.GetAllClaims(); + foreach (var c in claims) + { + retVal.Add(new { Name = c, Selected = false }); + } + return Response.AsJson(retVal); + } } } diff --git a/PlexRequests.UI/Modules/UserWizardModule.cs b/PlexRequests.UI/Modules/UserWizardModule.cs index 253288929..1bfbbd679 100644 --- a/PlexRequests.UI/Modules/UserWizardModule.cs +++ b/PlexRequests.UI/Modules/UserWizardModule.cs @@ -34,7 +34,7 @@ using Nancy.Extensions; using Nancy.ModelBinding; using Nancy.Responses.Negotiation; using Nancy.Validation; - +using NLog; using PlexRequests.Api.Interfaces; using PlexRequests.Core; using PlexRequests.Core.SettingModels; @@ -84,7 +84,9 @@ namespace PlexRequests.UI.Modules private ICustomUserMapper Mapper { get; } private IAnalytics Analytics { get; } - + private static Logger Log = LogManager.GetCurrentClassLogger(); + + private Response PlexAuth() { var user = this.Bind<PlexAuth>(); @@ -103,9 +105,10 @@ namespace PlexRequests.UI.Modules // Set the auth token in the session so we can use it in the next form Session[SessionKeys.UserWizardPlexAuth] = model.user.authentication_token; - + var servers = PlexApi.GetServer(model.user.authentication_token); var firstServer = servers.Server.FirstOrDefault(); + return Response.AsJson(new { Result = true, firstServer?.Port, Ip = firstServer?.LocalAddresses, firstServer?.Scheme }); } @@ -119,6 +122,20 @@ namespace PlexRequests.UI.Modules } form.PlexAuthToken = Session[SessionKeys.UserWizardPlexAuth].ToString(); // Set the auth token from the previous form + // Get the machine ID from the settings (This could have changed) + try + { + var servers = PlexApi.GetServer(form.PlexAuthToken); + var firstServer = servers.Server.FirstOrDefault(x => x.AccessToken == form.PlexAuthToken); + + Session[SessionKeys.UserWizardMachineId] = firstServer?.MachineIdentifier; + } + catch (Exception e) + { + // Probably bad settings, just continue + Log.Error(e); + } + var result = await PlexSettings.SaveSettingsAsync(form); if (result) { diff --git a/PlexRequests.UI/NinjectModules/ConfigurationModule.cs b/PlexRequests.UI/NinjectModules/ConfigurationModule.cs index 5bb10dca3..ae55786e1 100644 --- a/PlexRequests.UI/NinjectModules/ConfigurationModule.cs +++ b/PlexRequests.UI/NinjectModules/ConfigurationModule.cs @@ -50,6 +50,7 @@ namespace PlexRequests.UI.NinjectModules Bind<ICustomUserMapper>().To<UserMapper>(); Bind<INotificationService>().To<NotificationService>().InSingletonScope(); + Bind<INotificationEngine>().To<NotificationEngine>(); } } } \ No newline at end of file diff --git a/PlexRequests.UI/PlexRequests.UI.csproj b/PlexRequests.UI/PlexRequests.UI.csproj index d178bbc92..1779f378b 100644 --- a/PlexRequests.UI/PlexRequests.UI.csproj +++ b/PlexRequests.UI/PlexRequests.UI.csproj @@ -250,6 +250,7 @@ <Compile Include="Modules\CultureModule.cs" /> <Compile Include="Modules\IssuesModule.cs" /> <Compile Include="Modules\LandingPageModule.cs" /> + <Compile Include="Modules\RequestsBetaModule.cs" /> <Compile Include="Modules\UpdateCheckerModule.cs" /> <Compile Include="Modules\UserWizardModule.cs" /> <Compile Include="NinjectModules\ApiModule.cs" /> @@ -282,6 +283,12 @@ <Content Include="Content\app\app.js"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> + <Content Include="Content\app\requests\requestsController.js"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </Content> + <Content Include="Content\app\requests\requestsService.js"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </Content> <Content Include="Content\awesome-bootstrap-checkbox.css"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content> diff --git a/PlexRequests.UI/Resources/UI.resx b/PlexRequests.UI/Resources/UI.resx index 58e8f71c8..4f23fce97 100644 --- a/PlexRequests.UI/Resources/UI.resx +++ b/PlexRequests.UI/Resources/UI.resx @@ -440,4 +440,7 @@ <data name="Requests_ReleaseDate_Unavailable" xml:space="preserve"> <value>There is no information available for the release date</value> </data> + <data name="Search_ViewInPlex" xml:space="preserve"> + <value>View In Plex</value> + </data> </root> \ No newline at end of file diff --git a/PlexRequests.UI/Resources/UI1.Designer.cs b/PlexRequests.UI/Resources/UI1.Designer.cs index 20ab447d7..6cd282c32 100644 --- a/PlexRequests.UI/Resources/UI1.Designer.cs +++ b/PlexRequests.UI/Resources/UI1.Designer.cs @@ -987,6 +987,15 @@ namespace PlexRequests.UI.Resources { } } + /// <summary> + /// Looks up a localized string similar to View In Plex. + /// </summary> + public static string Search_ViewInPlex { + get { + return ResourceManager.GetString("Search_ViewInPlex", resourceCulture); + } + } + /// <summary> /// Looks up a localized string similar to You have reached your weekly request limit for Albums! Please contact your admin.. /// </summary> diff --git a/PlexRequests.UI/Validators/PlexRequestsValidator.cs b/PlexRequests.UI/Validators/PlexRequestsValidator.cs index c82817185..7d61eba2c 100644 --- a/PlexRequests.UI/Validators/PlexRequestsValidator.cs +++ b/PlexRequests.UI/Validators/PlexRequestsValidator.cs @@ -24,6 +24,8 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // ************************************************************************/ #endregion + +using System; using FluentValidation; using PlexRequests.Core.SettingModels; @@ -34,18 +36,18 @@ namespace PlexRequests.UI.Validators { public PlexRequestsValidator() { - RuleFor(x => x.BaseUrl).NotEqual("requests").WithMessage("You cannot use 'requests' as this is reserved by the application."); - RuleFor(x => x.BaseUrl).NotEqual("admin").WithMessage("You cannot use 'admin' as this is reserved by the application."); - RuleFor(x => x.BaseUrl).NotEqual("search").WithMessage("You cannot use 'search' as this is reserved by the application."); - RuleFor(x => x.BaseUrl).NotEqual("issues").WithMessage("You cannot use 'issues' as this is reserved by the application."); - RuleFor(x => x.BaseUrl).NotEqual("userlogin").WithMessage("You cannot use 'userlogin' as this is reserved by the application."); - RuleFor(x => x.BaseUrl).NotEqual("login").WithMessage("You cannot use 'login' as this is reserved by the application."); - RuleFor(x => x.BaseUrl).NotEqual("test").WithMessage("You cannot use 'test' as this is reserved by the application."); - RuleFor(x => x.BaseUrl).NotEqual("approval").WithMessage("You cannot use 'approval' as this is reserved by the application."); - RuleFor(x => x.BaseUrl).NotEqual("updatechecker").WithMessage("You cannot use 'updatechecker' as this is reserved by the application."); - RuleFor(x => x.BaseUrl).NotEqual("usermanagement").WithMessage("You cannot use 'usermanagement' as this is reserved by the application."); - RuleFor(x => x.BaseUrl).NotEqual("api").WithMessage("You cannot use 'api' as this is reserved by the application."); - RuleFor(x => x.BaseUrl).NotEqual("landing").WithMessage("You cannot use 'landing' as this is reserved by the application."); + RuleFor(x => x.BaseUrl).NotEqual("requests",StringComparer.CurrentCultureIgnoreCase).WithMessage("You cannot use 'requests' as this is reserved by the application."); + RuleFor(x => x.BaseUrl).NotEqual("admin", StringComparer.CurrentCultureIgnoreCase).WithMessage("You cannot use 'admin' as this is reserved by the application."); + RuleFor(x => x.BaseUrl).NotEqual("search", StringComparer.CurrentCultureIgnoreCase).WithMessage("You cannot use 'search' as this is reserved by the application."); + RuleFor(x => x.BaseUrl).NotEqual("issues", StringComparer.CurrentCultureIgnoreCase).WithMessage("You cannot use 'issues' as this is reserved by the application."); + RuleFor(x => x.BaseUrl).NotEqual("userlogin", StringComparer.CurrentCultureIgnoreCase).WithMessage("You cannot use 'userlogin' as this is reserved by the application."); + 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("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."); } } } \ No newline at end of file diff --git a/PlexRequests.UI/Views/Admin/LandingPage.cshtml b/PlexRequests.UI/Views/Admin/LandingPage.cshtml index 9f6dcec6f..38d3cc6fa 100644 --- a/PlexRequests.UI/Views/Admin/LandingPage.cshtml +++ b/PlexRequests.UI/Views/Admin/LandingPage.cshtml @@ -1,7 +1,7 @@ @using PlexRequests.UI.Helpers @Html.Partial("_Sidebar") @inherits Nancy.ViewEngines.Razor.NancyRazorViewBase<PlexRequests.Core.SettingModels.LandingPageSettings> - +@Html.LoadDateTimePickerAsset() <div class="col-sm-8 col-sm-push-1"> <form class="form-horizontal" method="POST" id="mainForm"> <fieldset> diff --git a/PlexRequests.UI/Views/Search/Index.cshtml b/PlexRequests.UI/Views/Search/Index.cshtml index 17204abb1..950a7f06d 100644 --- a/PlexRequests.UI/Views/Search/Index.cshtml +++ b/PlexRequests.UI/Views/Search/Index.cshtml @@ -175,6 +175,8 @@ {{#if_eq type "movie"}} {{#if_eq available true}} <button style="text-align: right" class="btn btn-success-outline disabled" disabled><i class="fa fa-check"></i> @UI.Search_Available</button> + <br /> + <a style="text-align: right" class="btn btn-sm btn-primary-outline" href="{{url}}" target="_blank"><i class="fa fa-eye"></i> @UI.Search_ViewInPlex</a> {{else}} {{#if_eq requested true}} <button style="text-align: right" class="btn btn-primary-outline disabled" disabled><i class="fa fa-check"></i> @UI.Search_Requested</button> @@ -186,7 +188,8 @@ {{#if_eq type "tv"}} {{#if_eq tvFullyAvailable true}} @*//TODO Not used yet*@ - <button style="text-align: right" class="btn btn-success-outline disabled" disabled><i class="fa fa-check"></i> @UI.Search_Available</button> + <button style="text-align: right" class="btn btn-success-outline disabled" disabled><i class="fa fa-check"></i> @UI.Search_Available</button><br /> + <a style="text-align: right" class="btn btn-sm btn-primary-outline" href="{{url}}" target="_blank"><i class="fa fa-eye"></i> @UI.Search_ViewInPlex</a> {{else}} <div class="dropdown"> <button id="{{id}}" class="btn btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"> @@ -259,7 +262,8 @@ <form method="POST" action="@url/search/request/{{type}}" id="form{{id}}"> <input name="{{type}}Id" type="text" value="{{id}}" hidden="hidden" /> {{#if_eq available true}} - <button style="text-align: right" class="btn btn-success-outline disabled" disabled><i class="fa fa-check"></i> @UI.Search_Available</button> + <button style="text-align: right" class="btn btn-success-outline disabled" disabled><i class="fa fa-check"></i> @UI.Search_Available</button><br /> + <a style="text-align: right" class="btn btn-sm btn-primary-outline" href="{{url}}" target="_blank"><i class="fa fa-eye"></i> @UI.Search_ViewInPlex</a> {{else}} {{#if_eq requested true}} <button style="text-align: right" class="btn btn-success-outline disabled" disabled><i class="fa fa-check"></i> @UI.Search_Requested</button> diff --git a/PlexRequests.UI/Views/Shared/Partial/_Navbar.cshtml b/PlexRequests.UI/Views/Shared/Partial/_Navbar.cshtml index 1c274fedd..6692f6f74 100644 --- a/PlexRequests.UI/Views/Shared/Partial/_Navbar.cshtml +++ b/PlexRequests.UI/Views/Shared/Partial/_Navbar.cshtml @@ -78,6 +78,7 @@ <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><i class="fa fa-language" aria-hidden="true"><span class="caret"></span></i></a> <ul class="dropdown-menu" role="menu"> <li><a href="@url/culture?l=en&u=@Context.Request.Path">@UI.Layout_English</a></li> + <li><a href="@url/culture?l=fr&u=@Context.Request.Path">@UI.Layout_French</a></li> <li><a href="@url/culture?l=nl&u=@Context.Request.Path">@UI.Layout_Dutch</a></li> <li><a href="@url/culture?l=es&u=@Context.Request.Path">@UI.Layout_Spanish</a></li> <li><a href="@url/culture?l=de&u=@Context.Request.Path">@UI.Layout_German</a></li> diff --git a/PlexRequests.UI/Views/UserManagement/Index.cshtml b/PlexRequests.UI/Views/UserManagement/Index.cshtml index 6a9a4318a..abdc5821b 100644 --- a/PlexRequests.UI/Views/UserManagement/Index.cshtml +++ b/PlexRequests.UI/Views/UserManagement/Index.cshtml @@ -1,9 +1,8 @@ @using PlexRequests.UI.Helpers @inherits PlexRequests.UI.Helpers.AngularViewBase -<script src="~/Content/app/userManagement/userManagementController.js"></script> -<script src="~/Content/app/userManagement/userManagementService.js"></script> -<div ng-controller="userManagementController" ng-init="getUsers()"> +@Html.LoadUserManagementAssets() +<div ng-controller="userManagementController" ng-init="init()"> <br /> <br /> @@ -12,15 +11,26 @@ <br> <br> <div ng-show="error.error" ng-bind="error.errorMessage"></div> - <form ng-submit="addUser()"> + <form name="userform" ng-submit="addUser()" novalidate> <div class="form-group"> <input id="username" type="text" placeholder="user" ng-model="user.username" class="form-control form-control-custom" /> </div> <div class="form-group"> <input id="password" type="password" placeholder="password" ng-model="user.password" class="form-control form-control-custom" /> </div> + <div class="form-group"> + <input id="email" type="email" placeholder="email address" ng-model="user.email" class="form-control form-control-custom" /> + </div> + + <div class="checkbox" ng-repeat="claim in claims"> + <input id="claimCheckbox_{{$id}}" class="checkbox-custom" name="selectedClaims[]" + ng-checked="claim.selected" ng-model="claim.selected" type="checkbox" value="claim" /> + <label for="claimCheckbox_{{$id}}">{{claim.name}}</label> + </div> + <input type="submit" class="btn btn-success-outline" value="Add" /> </form> + <form> <div class="form-group"> <div class="input-group"> @@ -33,6 +43,7 @@ </div> </div> </form> + <table class="table table-striped table-hover table-responsive table-condensed"> <thead> <tr> @@ -98,9 +109,27 @@ <div> <strong>User Type: </strong><span ng-bind="selectedUser.type === 1 ? 'Local User' : 'Plex User'"></span> </div> - </div> + <br/> + <br/> + <div ng-show="selectedUser.type === 1"> + <!--Edit--> + + <strong>Modify Roles:</strong> + <!--Load all claims--> + <div class="checkbox" ng-repeat="claim in claims"> + <input id="claimCheckboxEdit_{{$id}}" class="checkbox-custom" name="selectedClaims[]" ng-checked="@*//TODO: Need to figure our how to preselect them*@" ng-model="claim.selected" type="checkbox" value="claim" /> + <label for="claimCheckboxEdit_{{$id}}">{{claim.name}}</label> + </div> + + + + + + <button ng-click="updateUser()" class="btn btn-primary-outline">Update</button> + </div> + </div> <!-- End of user side menu --> </div> \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index 5370cbdcd..1285d7e4b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,9 +3,9 @@ configuration: Release assembly_info: patch: true file: '**\AssemblyInfo.*' - assembly_version: '1.9.0' + assembly_version: '1.9.1' assembly_file_version: '{version}' - assembly_informational_version: '1.9.0' + assembly_informational_version: '1.9.1' before_build: - cmd: appveyor-retry nuget restore build: