diff --git a/Ombi.Core.Migration/Migrations/Version1100.cs b/Ombi.Core.Migration/Migrations/Version1100.cs index 55d537758..4c184ca22 100644 --- a/Ombi.Core.Migration/Migrations/Version1100.cs +++ b/Ombi.Core.Migration/Migrations/Version1100.cs @@ -37,6 +37,7 @@ using Ombi.Helpers; using Ombi.Helpers.Permissions; using Ombi.Store; using Ombi.Store.Models; +using Ombi.Store.Models.Plex; using Ombi.Store.Repository; namespace Ombi.Core.Migration.Migrations @@ -46,7 +47,7 @@ namespace Ombi.Core.Migration.Migrations { public Version1100(IUserRepository userRepo, IRequestService requestService, ISettingsService log, IPlexApi plexApi, ISettingsService plexService, - IPlexUserRepository plexusers, ISettingsService prSettings, + IExternalUserRepository plexusers, ISettingsService prSettings, ISettingsService umSettings, ISettingsService sjs, IRepository usersToNotify) { @@ -69,7 +70,7 @@ namespace Ombi.Core.Migration.Migrations private ISettingsService Log { get; } private IPlexApi PlexApi { get; } private ISettingsService PlexSettings { get; } - private IPlexUserRepository PlexUsers { get; } + private IExternalUserRepository PlexUsers { get; } private ISettingsService PlexRequestSettings { get; } private ISettingsService UserManagementSettings { get; } private ISettingsService ScheduledJobSettings { get; } diff --git a/Ombi.Core/SecurityExtensions.cs b/Ombi.Core/SecurityExtensions.cs index 7a244554a..71d8284fb 100644 --- a/Ombi.Core/SecurityExtensions.cs +++ b/Ombi.Core/SecurityExtensions.cs @@ -36,13 +36,14 @@ using Ombi.Core.SettingModels; using Ombi.Core.Users; using Ombi.Helpers; using Ombi.Helpers.Permissions; +using Ombi.Store.Models.Plex; using Ombi.Store.Repository; namespace Ombi.Core { public class SecurityExtensions : ISecurityExtensions { - public SecurityExtensions(IUserRepository userRepository, IResourceLinker linker, IPlexUserRepository plexUsers, ISettingsService umSettings) + public SecurityExtensions(IUserRepository userRepository, IResourceLinker linker, IExternalUserRepository plexUsers, ISettingsService umSettings) { UserRepository = userRepository; Linker = linker; @@ -52,7 +53,7 @@ namespace Ombi.Core private IUserRepository UserRepository { get; } private IResourceLinker Linker { get; } - private IPlexUserRepository PlexUsers { get; } + private IExternalUserRepository PlexUsers { get; } private ISettingsService UserManagementSettings { get; } public bool IsLoggedIn(NancyContext context) diff --git a/Ombi.Core/Users/UserHelper.cs b/Ombi.Core/Users/UserHelper.cs index 099b025b8..2e5b523af 100644 --- a/Ombi.Core/Users/UserHelper.cs +++ b/Ombi.Core/Users/UserHelper.cs @@ -30,13 +30,14 @@ using System.Linq; using Ombi.Core.Models; using Ombi.Helpers; using Ombi.Helpers.Permissions; +using Ombi.Store.Models.Plex; using Ombi.Store.Repository; namespace Ombi.Core.Users { public class UserHelper : IUserHelper { - public UserHelper(IUserRepository userRepository, IPlexUserRepository plexUsers, ISecurityExtensions security) + public UserHelper(IUserRepository userRepository, IExternalUserRepository plexUsers, ISecurityExtensions security) { LocalUserRepository = userRepository; PlexUserRepository = plexUsers; @@ -44,7 +45,7 @@ namespace Ombi.Core.Users } private IUserRepository LocalUserRepository { get; } - private IPlexUserRepository PlexUserRepository { get; } + private IExternalUserRepository PlexUserRepository { get; } private ISecurityExtensions Security { get; } diff --git a/Ombi.Helpers/UserType.cs b/Ombi.Helpers/UserType.cs index 30c4a492c..7efd3892c 100644 --- a/Ombi.Helpers/UserType.cs +++ b/Ombi.Helpers/UserType.cs @@ -30,6 +30,7 @@ namespace Ombi.Helpers public enum UserType { PlexUser, - LocalUser + LocalUser, + EmbyUser } } \ No newline at end of file diff --git a/Ombi.Services/Interfaces/IEmbyNotificationEngine.cs b/Ombi.Services/Interfaces/IEmbyNotificationEngine.cs new file mode 100644 index 000000000..c96ebd95b --- /dev/null +++ b/Ombi.Services/Interfaces/IEmbyNotificationEngine.cs @@ -0,0 +1,6 @@ +namespace Ombi.Services.Interfaces +{ + public interface IEmbyNotificationEngine : INotificationEngine + { + } +} \ No newline at end of file diff --git a/Ombi.Services/Interfaces/INotificationEngine.cs b/Ombi.Services/Interfaces/INotificationEngine.cs index bfca46dfa..c2f2a138f 100644 --- a/Ombi.Services/Interfaces/INotificationEngine.cs +++ b/Ombi.Services/Interfaces/INotificationEngine.cs @@ -34,7 +34,7 @@ namespace Ombi.Services.Interfaces { public interface INotificationEngine { - Task NotifyUsers(IEnumerable modelChanged, string apiKey, NotificationType type); - Task NotifyUsers(RequestedModel modelChanged, string apiKey, NotificationType type); + Task NotifyUsers(IEnumerable modelChanged, NotificationType type); + Task NotifyUsers(RequestedModel modelChanged, NotificationType type); } } \ No newline at end of file diff --git a/Ombi.Services/Interfaces/IPlexNotificationEngine.cs b/Ombi.Services/Interfaces/IPlexNotificationEngine.cs new file mode 100644 index 000000000..019668d60 --- /dev/null +++ b/Ombi.Services/Interfaces/IPlexNotificationEngine.cs @@ -0,0 +1,6 @@ +namespace Ombi.Services.Interfaces +{ + public interface IPlexNotificationEngine : INotificationEngine + { + } +} \ No newline at end of file diff --git a/Ombi.Services/Jobs/EmbyAvailabilityChecker.cs b/Ombi.Services/Jobs/EmbyAvailabilityChecker.cs index 467e5227e..1223e3eb8 100644 --- a/Ombi.Services/Jobs/EmbyAvailabilityChecker.cs +++ b/Ombi.Services/Jobs/EmbyAvailabilityChecker.cs @@ -52,7 +52,7 @@ namespace Ombi.Services.Jobs public class EmbyAvailabilityChecker : IJob, IEmbyAvailabilityChecker { public EmbyAvailabilityChecker(ISettingsService embySettings, IRequestService request, IEmbyApi emby, ICacheProvider cache, - INotificationService notify, IJobRecord rec, IRepository users, IRepository repo, INotificationEngine e, IRepository content) + INotificationService notify, IJobRecord rec, IRepository users, IRepository repo, IEmbyNotificationEngine e, IRepository content) { Emby = embySettings; RequestService = request; @@ -148,7 +148,7 @@ namespace Ombi.Services.Jobs if (modifiedModel.Any()) { - //NotificationEngine.NotifyUsers(modifiedModel, embySettings.ApiKey, NotificationType.RequestAvailable); // TODO Emby + NotificationEngine.NotifyUsers(modifiedModel, NotificationType.RequestAvailable); RequestService.BatchUpdate(modifiedModel); } } diff --git a/Ombi.Services/Jobs/PlexAvailabilityChecker.cs b/Ombi.Services/Jobs/PlexAvailabilityChecker.cs index cd64a3b18..241da9842 100644 --- a/Ombi.Services/Jobs/PlexAvailabilityChecker.cs +++ b/Ombi.Services/Jobs/PlexAvailabilityChecker.cs @@ -51,7 +51,7 @@ namespace Ombi.Services.Jobs public class PlexAvailabilityChecker : IJob, IAvailabilityChecker { public PlexAvailabilityChecker(ISettingsService plexSettings, IRequestService request, IPlexApi plex, ICacheProvider cache, - INotificationService notify, IJobRecord rec, IRepository users, IRepository repo, INotificationEngine e, IRepository content) + INotificationService notify, IJobRecord rec, IRepository users, IRepository repo, IPlexNotificationEngine e, IRepository content) { Plex = plexSettings; RequestService = request; @@ -152,7 +152,7 @@ namespace Ombi.Services.Jobs if (modifiedModel.Any()) { - NotificationEngine.NotifyUsers(modifiedModel, plexSettings.PlexAuthToken, NotificationType.RequestAvailable); + NotificationEngine.NotifyUsers(modifiedModel, NotificationType.RequestAvailable); RequestService.BatchUpdate(modifiedModel); } } diff --git a/Ombi.Services/Jobs/PlexContentCacher.cs b/Ombi.Services/Jobs/PlexContentCacher.cs index 44ad9edd3..9a2d770e5 100644 --- a/Ombi.Services/Jobs/PlexContentCacher.cs +++ b/Ombi.Services/Jobs/PlexContentCacher.cs @@ -48,7 +48,7 @@ namespace Ombi.Services.Jobs public class PlexContentCacher : IJob, IPlexContentCacher { public PlexContentCacher(ISettingsService plexSettings, IRequestService request, IPlexApi plex, ICacheProvider cache, - INotificationService notify, IJobRecord rec, IRepository users, IRepository repo, INotificationEngine e, IRepository content) + INotificationService notify, IJobRecord rec, IRepository users, IRepository repo, IPlexNotificationEngine e, IRepository content) { Plex = plexSettings; RequestService = request; diff --git a/Ombi.Services/Jobs/PlexEpisodeCacher.cs b/Ombi.Services/Jobs/PlexEpisodeCacher.cs index b7af87022..e6d1fc9c9 100644 --- a/Ombi.Services/Jobs/PlexEpisodeCacher.cs +++ b/Ombi.Services/Jobs/PlexEpisodeCacher.cs @@ -38,8 +38,10 @@ using Ombi.Core.SettingModels; using Ombi.Helpers; using Ombi.Services.Interfaces; using Ombi.Store.Models; +using Ombi.Store.Models.Plex; using Ombi.Store.Repository; using Quartz; +using PlexMediaType = Ombi.Api.Models.Plex.PlexMediaType; namespace Ombi.Services.Jobs { diff --git a/Ombi.Services/Jobs/PlexUserChecker.cs b/Ombi.Services/Jobs/PlexUserChecker.cs index 708648756..7a56ddeee 100644 --- a/Ombi.Services/Jobs/PlexUserChecker.cs +++ b/Ombi.Services/Jobs/PlexUserChecker.cs @@ -37,6 +37,7 @@ using Ombi.Core.Users; using Ombi.Helpers.Permissions; using Ombi.Services.Interfaces; using Ombi.Store.Models; +using Ombi.Store.Models.Plex; using Ombi.Store.Repository; using Quartz; @@ -46,7 +47,7 @@ namespace Ombi.Services.Jobs { private static readonly Logger Log = LogManager.GetCurrentClassLogger(); - public PlexUserChecker(IPlexUserRepository plexUsers, IPlexApi plexAPi, IJobRecord rec, ISettingsService plexSettings, ISettingsService prSettings, ISettingsService umSettings, + public PlexUserChecker(IExternalUserRepository plexUsers, IPlexApi plexAPi, IJobRecord rec, ISettingsService plexSettings, ISettingsService prSettings, ISettingsService umSettings, IRequestService requestService, IUserRepository localUser) { Repo = plexUsers; @@ -61,7 +62,7 @@ namespace Ombi.Services.Jobs private IJobRecord JobRecord { get; } private IPlexApi PlexApi { get; } - private IPlexUserRepository Repo { get; } + private IExternalUserRepository Repo { get; } private ISettingsService PlexSettings { get; } private ISettingsService PlexRequestSettings { get; } private ISettingsService UserManagementSettings { get; } diff --git a/Ombi.Services/Notification/EmbyNotificationEngine.cs b/Ombi.Services/Notification/EmbyNotificationEngine.cs new file mode 100644 index 000000000..6eab1cae5 --- /dev/null +++ b/Ombi.Services/Notification/EmbyNotificationEngine.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using NLog; +using Ombi.Api.Interfaces; +using Ombi.Core; +using Ombi.Core.Models; +using Ombi.Core.SettingModels; +using Ombi.Core.Users; +using Ombi.Helpers.Permissions; +using Ombi.Services.Interfaces; +using Ombi.Store; +using Ombi.Store.Models; +using Ombi.Store.Repository; + +namespace Ombi.Services.Notification +{ + public class EmbyNotificationEngine : IEmbyNotificationEngine + { + public EmbyNotificationEngine(IEmbyApi p, IRepository repo, ISettingsService embySettings, INotificationService service, IUserHelper userHelper) + { + EmbyApi = p; + UserNotifyRepo = repo; + Notification = service; + UserHelper = userHelper; + EmbySettings = embySettings; + } + + private IEmbyApi EmbyApi { get; } + private IRepository UserNotifyRepo { get; } + private static Logger Log = LogManager.GetCurrentClassLogger(); + private INotificationService Notification { get; } + private IUserHelper UserHelper { get; } + private ISettingsService EmbySettings { get; } + + public async Task NotifyUsers(IEnumerable modelChanged, NotificationType type) + { + try + { + var embySettings = await EmbySettings.GetSettingsAsync(); + var embyUsers = EmbyApi.GetUsers(embySettings.FullUri, embySettings.ApiKey); + var userAccount = embyUsers.FirstOrDefault(x => x.Policy.IsAdministrator); + + var adminUsername = userAccount?.Name ?? string.Empty; + + var users = UserHelper.GetUsersWithFeature(Features.RequestAddedNotification).ToList(); + Log.Debug("Notifying Users Count {0}", users.Count); + foreach (var model in modelChanged) + { + var selectedUsers = new List(); + + foreach (var u in users) + { + var requestUser = model.RequestedUsers.FirstOrDefault( + x => x.Equals(u.Username, StringComparison.CurrentCultureIgnoreCase) || x.Equals(u.UserAlias, StringComparison.CurrentCultureIgnoreCase)); + if (string.IsNullOrEmpty(requestUser)) + { + continue; + } + + // Make sure we do not already have the user + if (!selectedUsers.Contains(requestUser)) + { + selectedUsers.Add(requestUser); + } + } + + 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?.Name, userAccount?.Name, model.Title, model.PosterPath, type, model.Type); // TODO Emby needs email address + return; + } + + var localUser = + users.FirstOrDefault( x => + x.Username.Equals(user, StringComparison.CurrentCultureIgnoreCase) || + x.UserAlias.Equals(user, StringComparison.CurrentCultureIgnoreCase)); + + // So if the request was from an alias, then we need to use the local user (since that contains the alias). + // If we do not have a local user, then we should be using the Plex user if that user exists. + // This will execute most of the time since Plex and Local users will most always be in the database. + if (localUser != null) + { + if (string.IsNullOrEmpty(localUser?.EmailAddress)) + { + Log.Info("There is no email address for this Local user ({0}), cannot send notification", localUser.Username); + continue; + } + + Log.Info("Sending notification to: {0} at: {1}, for : {2}", localUser, localUser.EmailAddress, model.Title); + await PublishUserNotification(localUser.Username, localUser.EmailAddress, model.Title, model.PosterPath, type, model.Type); + + } + else + { + var email = embyUsers.FirstOrDefault(x => x.Name.Equals(user, StringComparison.CurrentCultureIgnoreCase)); + if (string.IsNullOrEmpty(email?.Name)) // TODO this needs to be the email + { + Log.Info("There is no email address for this Emby user ({0}), cannot send notification", email?.Name); //TODO + // We do not have a plex user that requested this! + continue; + } + + Log.Info("Sending notification to: {0} at: {1}, for : {2}", email.Name, email.Name, model.Title); + await PublishUserNotification(email.Name, email.Name, model.Title, model.PosterPath, type, model.Type); //TODO + } + } + } + } + catch (Exception e) + { + Log.Error(e); + } + } + + public async Task NotifyUsers(RequestedModel model, NotificationType type) + { + try + { + var embySettings = await EmbySettings.GetSettingsAsync(); + var embyUsers = EmbyApi.GetUsers(embySettings.FullUri, embySettings.ApiKey); + var userAccount = embyUsers.FirstOrDefault(x => x.Policy.IsAdministrator); + + var adminUsername = userAccount.Name ?? string.Empty; + + var users = UserHelper.GetUsersWithFeature(Features.RequestAddedNotification).ToList(); + Log.Debug("Notifying Users Count {0}", users.Count); + + // Get the usernames or alias depending if they have an alias + var userNamesWithFeature = users.Select(x => x.UsernameOrAlias).ToList(); + Log.Debug("Users with the feature count {0}", userNamesWithFeature.Count); + Log.Debug("Usernames: "); + foreach (var u in userNamesWithFeature) + { + Log.Debug(u); + } + + Log.Debug("Users in the requested model count: {0}", model.AllUsers.Count); + Log.Debug("usernames from model: "); + foreach (var modelAllUser in model.AllUsers) + { + Log.Debug(modelAllUser); + } + + if (model.AllUsers == null || !model.AllUsers.Any()) + { + Log.Debug("There are no users in the model.AllUsers, no users to notify"); + return; + } + var usersToNotify = userNamesWithFeature.Intersect(model.AllUsers, StringComparer.CurrentCultureIgnoreCase).ToList(); + + if (!usersToNotify.Any()) + { + Log.Debug("Could not find any users after the .Intersect()"); + } + + Log.Debug("Users being notified for this request count {0}", users.Count); + foreach (var user in usersToNotify) + { + Log.Info("Notifying user {0}", user); + if (user.Equals(adminUsername, StringComparison.CurrentCultureIgnoreCase)) + { + Log.Info("This user is the Emby server owner"); + await PublishUserNotification(userAccount.Name, userAccount.Name, model.Title, model.PosterPath, type, model.Type); // TODO + return; + } + + var email = embyUsers.FirstOrDefault(x => x.Name.Equals(user, StringComparison.CurrentCultureIgnoreCase)); + if (email == null) + { + Log.Info("There is no email address for this Emby 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.Name, email.Name, model.Title); // TODO + await PublishUserNotification(email.Name, email.Name, model.Title, model.PosterPath, type, model.Type); // TODO + } + } + catch (Exception e) + { + Log.Error(e); + } + } + + private async Task PublishUserNotification(string username, string email, string title, string img, NotificationType type, RequestType requestType) + { + var notificationModel = new NotificationModel + { + User = username, + UserEmail = email, + NotificationType = type, + Title = title, + ImgSrc = requestType == RequestType.Movie ? $"https://image.tmdb.org/t/p/w300/{img}" : img + }; + + // Send the notification to the user. + await Notification.Publish(notificationModel); + } + } +} \ No newline at end of file diff --git a/Ombi.Services/Notification/NotificationEngine.cs b/Ombi.Services/Notification/PlexNotificationEngine.cs similarity index 91% rename from Ombi.Services/Notification/NotificationEngine.cs rename to Ombi.Services/Notification/PlexNotificationEngine.cs index 49340ca1a..94c3fb31a 100644 --- a/Ombi.Services/Notification/NotificationEngine.cs +++ b/Ombi.Services/Notification/PlexNotificationEngine.cs @@ -1,7 +1,7 @@ #region Copyright // /************************************************************************ // Copyright (c) 2016 Jamie Rees -// File: NotificationEngine.cs +// File: PlexNotificationEngine.cs // Created By: Jamie Rees // // Permission is hereby granted, free of charge, to any person obtaining @@ -31,7 +31,9 @@ using System.Linq; using System.Threading.Tasks; using NLog; using Ombi.Api.Interfaces; +using Ombi.Core; using Ombi.Core.Models; +using Ombi.Core.SettingModels; using Ombi.Core.Users; using Ombi.Helpers.Permissions; using Ombi.Services.Interfaces; @@ -41,14 +43,15 @@ using Ombi.Store.Repository; namespace Ombi.Services.Notification { - public class NotificationEngine : INotificationEngine + public class PlexNotificationEngine : IPlexNotificationEngine { - public NotificationEngine(IPlexApi p, IRepository repo, INotificationService service, IUserHelper userHelper) + public PlexNotificationEngine(IPlexApi p, IRepository repo, INotificationService service, IUserHelper userHelper, ISettingsService ps) { PlexApi = p; UserNotifyRepo = repo; Notification = service; UserHelper = userHelper; + PlexSettings = ps; } private IPlexApi PlexApi { get; } @@ -56,13 +59,15 @@ namespace Ombi.Services.Notification private static Logger Log = LogManager.GetCurrentClassLogger(); private INotificationService Notification { get; } private IUserHelper UserHelper { get; } + private ISettingsService PlexSettings { get; } - public async Task NotifyUsers(IEnumerable modelChanged, string apiKey, NotificationType type) + public async Task NotifyUsers(IEnumerable modelChanged, NotificationType type) { try { - var plexUser = PlexApi.GetUsers(apiKey); - var userAccount = PlexApi.GetAccount(apiKey); + var settings = await PlexSettings.GetSettingsAsync(); + var plexUser = PlexApi.GetUsers(settings.PlexAuthToken); + var userAccount = PlexApi.GetAccount(settings.PlexAuthToken); var adminUsername = userAccount.Username ?? string.Empty; @@ -140,12 +145,13 @@ namespace Ombi.Services.Notification } } - public async Task NotifyUsers(RequestedModel model, string apiKey, NotificationType type) + public async Task NotifyUsers(RequestedModel model, NotificationType type) { try { - var plexUser = PlexApi.GetUsers(apiKey); - var userAccount = PlexApi.GetAccount(apiKey); + var settings = await PlexSettings.GetSettingsAsync(); + var plexUser = PlexApi.GetUsers(settings.PlexAuthToken); + var userAccount = PlexApi.GetAccount(settings.PlexAuthToken); var adminUsername = userAccount.Username ?? string.Empty; diff --git a/Ombi.Services/Ombi.Services.csproj b/Ombi.Services/Ombi.Services.csproj index 46cc73965..0405622c0 100644 --- a/Ombi.Services/Ombi.Services.csproj +++ b/Ombi.Services/Ombi.Services.csproj @@ -86,6 +86,8 @@ + + @@ -135,7 +137,8 @@ - + + diff --git a/Ombi.Store/Models/Emby/EmbyUsers.cs b/Ombi.Store/Models/Emby/EmbyUsers.cs new file mode 100644 index 000000000..386dad9a6 --- /dev/null +++ b/Ombi.Store/Models/Emby/EmbyUsers.cs @@ -0,0 +1,43 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: PlexUsers.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using Dapper.Contrib.Extensions; + +namespace Ombi.Store.Models.Emby +{ + [Table(nameof(EmbyUsers))] + public class EmbyUsers : Entity + { + public string PlexUserId { get; set; } + public string UserAlias { get; set; } + public int Permissions { get; set; } + public int Features { get; set; } + public string Username { get; set; } + public string EmailAddress { get; set; } + public string LoginId { get; set; } + } +} \ No newline at end of file diff --git a/Ombi.Store/Models/PlexEpisodes.cs b/Ombi.Store/Models/Plex/PlexEpisodes.cs similarity index 98% rename from Ombi.Store/Models/PlexEpisodes.cs rename to Ombi.Store/Models/Plex/PlexEpisodes.cs index 922fab03c..8fe994103 100644 --- a/Ombi.Store/Models/PlexEpisodes.cs +++ b/Ombi.Store/Models/Plex/PlexEpisodes.cs @@ -27,7 +27,7 @@ using Dapper.Contrib.Extensions; -namespace Ombi.Store.Models +namespace Ombi.Store.Models.Plex { [Table("PlexEpisodes")] public class PlexEpisodes : Entity diff --git a/Ombi.Store/Models/PlexUsers.cs b/Ombi.Store/Models/Plex/PlexUsers.cs similarity index 97% rename from Ombi.Store/Models/PlexUsers.cs rename to Ombi.Store/Models/Plex/PlexUsers.cs index 0a3b735d1..f75e6ff31 100644 --- a/Ombi.Store/Models/PlexUsers.cs +++ b/Ombi.Store/Models/Plex/PlexUsers.cs @@ -26,9 +26,8 @@ #endregion using Dapper.Contrib.Extensions; -using Newtonsoft.Json; -namespace Ombi.Store.Models +namespace Ombi.Store.Models.Plex { [Table(nameof(PlexUsers))] public class PlexUsers : Entity diff --git a/Ombi.Store/Ombi.Store.csproj b/Ombi.Store/Ombi.Store.csproj index 33298c6d1..6bfaa021b 100644 --- a/Ombi.Store/Ombi.Store.csproj +++ b/Ombi.Store/Ombi.Store.csproj @@ -68,8 +68,9 @@ - - + + + @@ -80,6 +81,8 @@ + + @@ -91,7 +94,6 @@ - diff --git a/Ombi.Store/Repository/PlexUserRepository.cs b/Ombi.Store/Repository/BaseExternalUserRepository.cs similarity index 63% rename from Ombi.Store/Repository/PlexUserRepository.cs rename to Ombi.Store/Repository/BaseExternalUserRepository.cs index 69bb31816..979bc6253 100644 --- a/Ombi.Store/Repository/PlexUserRepository.cs +++ b/Ombi.Store/Repository/BaseExternalUserRepository.cs @@ -26,18 +26,16 @@ #endregion using System; -using System.Collections.Generic; using System.Data; using System.Threading.Tasks; using Dapper; using Ombi.Helpers; -using Ombi.Store.Models; namespace Ombi.Store.Repository { - public class PlexUserRepository : BaseGenericRepository, IPlexUserRepository + public class BaseExternalUserRepository : BaseGenericRepository, IExternalUserRepository where T : class { - public PlexUserRepository(ISqliteConfiguration config, ICacheProvider cache) : base(config,cache) + public BaseExternalUserRepository(ISqliteConfiguration config, ICacheProvider cache) : base(config,cache) { DbConfig = config; } @@ -45,53 +43,53 @@ namespace Ombi.Store.Repository private ISqliteConfiguration DbConfig { get; } private IDbConnection Db => DbConfig.DbConnection(); - public PlexUsers GetUser(string userGuid) + public T GetUser(string userGuid) { var sql = @"SELECT * FROM PlexUsers WHERE PlexUserId = @UserGuid COLLATE NOCASE"; - return Db.QueryFirstOrDefault(sql, new {UserGuid = userGuid}); + return Db.QueryFirstOrDefault(sql, new {UserGuid = userGuid}); } - public PlexUsers GetUserByUsername(string username) + public T GetUserByUsername(string username) { var sql = @"SELECT * FROM PlexUsers WHERE Username = @UserName COLLATE NOCASE"; - return Db.QueryFirstOrDefault(sql, new {UserName = username}); + return Db.QueryFirstOrDefault(sql, new {UserName = username}); } - public async Task GetUserAsync(string userguid) + public async Task GetUserAsync(string userguid) { var sql = @"SELECT * FROM PlexUsers WHERE PlexUserId = @UserGuid COLLATE NOCASE"; - return await Db.QueryFirstOrDefaultAsync(sql, new {UserGuid = userguid}); + return await Db.QueryFirstOrDefaultAsync(sql, new {UserGuid = userguid}); } #region abstract implementation #pragma warning disable CS0809 // Obsolete member overrides non-obsolete member [Obsolete] - public override PlexUsers Get(string id) + public override T Get(string id) { throw new System.NotImplementedException(); } [Obsolete] - public override Task GetAsync(int id) + public override Task GetAsync(int id) { throw new System.NotImplementedException(); } [Obsolete] - public override PlexUsers Get(int id) + public override T Get(int id) { throw new System.NotImplementedException(); } [Obsolete] - public override Task GetAsync(string id) + public override Task GetAsync(string id) { throw new System.NotImplementedException(); } @@ -99,22 +97,5 @@ namespace Ombi.Store.Repository #pragma warning restore CS0809 // Obsolete member overrides non-obsolete member #endregion } - - - public interface IPlexUserRepository - { - PlexUsers GetUser(string userGuid); - PlexUsers GetUserByUsername(string username); - Task GetUserAsync(string userguid); - IEnumerable Custom(Func> func); - long Insert(PlexUsers entity); - void Delete(PlexUsers entity); - IEnumerable GetAll(); - bool UpdateAll(IEnumerable entity); - bool Update(PlexUsers entity); - Task> GetAllAsync(); - Task UpdateAsync(PlexUsers users); - Task InsertAsync(PlexUsers users); - } } diff --git a/Ombi.Store/Repository/IExternalUserRepository.cs b/Ombi.Store/Repository/IExternalUserRepository.cs new file mode 100644 index 000000000..25583a97a --- /dev/null +++ b/Ombi.Store/Repository/IExternalUserRepository.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Threading.Tasks; + +namespace Ombi.Store.Repository +{ + public interface IExternalUserRepository where T : class + { + T Get(string id); + T Get(int id); + Task GetAsync(string id); + Task GetAsync(int id); + T GetUser(string userGuid); + Task GetUserAsync(string userguid); + T GetUserByUsername(string username); + + IEnumerable Custom(Func> func); + long Insert(T entity); + void Delete(T entity); + IEnumerable GetAll(); + bool UpdateAll(IEnumerable entity); + bool Update(T entity); + Task> GetAllAsync(); + Task UpdateAsync(T users); + Task InsertAsync(T users); + } +} \ No newline at end of file diff --git a/Ombi.Store/SqlTables.sql b/Ombi.Store/SqlTables.sql index 82c58fa7c..01ad1e8f9 100644 --- a/Ombi.Store/SqlTables.sql +++ b/Ombi.Store/SqlTables.sql @@ -124,6 +124,20 @@ CREATE TABLE IF NOT EXISTS PlexUsers ); CREATE UNIQUE INDEX IF NOT EXISTS PlexUsers_Id ON PlexUsers (Id); + +CREATE TABLE IF NOT EXISTS EmbyUsers +( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + PlexUserId varchar(100) NOT NULL, + UserAlias varchar(100) NOT NULL, + Permissions INTEGER, + Features INTEGER, + Username VARCHAR(100), + EmailAddress VARCHAR(100), + LoginId VARCHAR(100) +); +CREATE UNIQUE INDEX IF NOT EXISTS EmbyUsers_Id ON EmbyUsers (Id); + BEGIN; CREATE TABLE IF NOT EXISTS PlexEpisodes ( diff --git a/Ombi.UI/Authentication/CustomAuthenticationConfiguration.cs b/Ombi.UI/Authentication/CustomAuthenticationConfiguration.cs index d730a798e..548e97053 100644 --- a/Ombi.UI/Authentication/CustomAuthenticationConfiguration.cs +++ b/Ombi.UI/Authentication/CustomAuthenticationConfiguration.cs @@ -26,6 +26,7 @@ #endregion using Nancy.Cryptography; +using Ombi.Store.Models.Plex; using Ombi.Store.Repository; namespace Ombi.UI.Authentication @@ -47,7 +48,7 @@ namespace Ombi.UI.Authentication /// Gets or sets the username/identifier mapper public IUserRepository LocalUserRepository { get; set; } - public IPlexUserRepository PlexUserRepository { get; set; } + public IExternalUserRepository PlexUserRepository { get; set; } /// Gets or sets RequiresSSL property /// The flag that indicates whether SSL is required diff --git a/Ombi.UI/Bootstrapper.cs b/Ombi.UI/Bootstrapper.cs index aa14ed0ab..e2931fa6a 100644 --- a/Ombi.UI/Bootstrapper.cs +++ b/Ombi.UI/Bootstrapper.cs @@ -42,6 +42,7 @@ using Ombi.Core; using Ombi.Core.SettingModels; using Ombi.Helpers; using Ombi.Store; +using Ombi.Store.Models.Plex; using Ombi.Store.Repository; using Ombi.UI.Authentication; using Ombi.UI.Helpers; @@ -88,7 +89,7 @@ namespace Ombi.UI var config = new CustomAuthenticationConfiguration { RedirectUrl = redirect, - PlexUserRepository = container.Get(), + PlexUserRepository = container.Get>(), // TODO emby LocalUserRepository = container.Get() }; diff --git a/Ombi.UI/Content/images/logo-cropped.png b/Ombi.UI/Content/images/logo-cropped.png deleted file mode 100644 index 560a817e6..000000000 Binary files a/Ombi.UI/Content/images/logo-cropped.png and /dev/null differ diff --git a/Ombi.UI/Content/images/logo.png b/Ombi.UI/Content/images/logo.png index fe356bcaf..560a817e6 100644 Binary files a/Ombi.UI/Content/images/logo.png and b/Ombi.UI/Content/images/logo.png differ diff --git a/Ombi.UI/Modules/RequestsModule.cs b/Ombi.UI/Modules/RequestsModule.cs index 699b19b67..193d11c33 100644 --- a/Ombi.UI/Modules/RequestsModule.cs +++ b/Ombi.UI/Modules/RequestsModule.cs @@ -65,7 +65,7 @@ namespace Ombi.UI.Modules ISickRageApi sickRageApi, ICacheProvider cache, IAnalytics an, - INotificationEngine engine, + IPlexNotificationEngine engine, ISecurityExtensions security, ISettingsService customSettings) : base("requests", prSettings, security) { @@ -438,8 +438,7 @@ namespace Ombi.UI.Modules originalRequest.Available = available; var result = await Service.UpdateRequestAsync(originalRequest); - var plexService = await PlexSettings.GetSettingsAsync(); - await NotificationEngine.NotifyUsers(originalRequest, plexService.PlexAuthToken, available ? NotificationType.RequestAvailable : NotificationType.RequestDeclined); + await NotificationEngine.NotifyUsers(originalRequest, available ? NotificationType.RequestAvailable : NotificationType.RequestDeclined); return Response.AsJson(result ? new { Result = true, Available = available, Message = string.Empty } : new { Result = false, Available = false, Message = "Could not update the availability, please try again or check the logs" }); diff --git a/Ombi.UI/Modules/UserLoginModule.cs b/Ombi.UI/Modules/UserLoginModule.cs index 6ed0fc105..8126f1c83 100644 --- a/Ombi.UI/Modules/UserLoginModule.cs +++ b/Ombi.UI/Modules/UserLoginModule.cs @@ -45,6 +45,7 @@ using Ombi.Helpers.Analytics; using Ombi.Helpers.Permissions; using Ombi.Store; using Ombi.Store.Models; +using Ombi.Store.Models.Plex; using Ombi.Store.Repository; using Ombi.UI.Authentication; using ISecurityExtensions = Ombi.Core.ISecurityExtensions; @@ -55,7 +56,7 @@ namespace Ombi.UI.Modules public class UserLoginModule : BaseModule { public UserLoginModule(ISettingsService auth, IPlexApi api, ISettingsService plexSettings, ISettingsService pr, - ISettingsService lp, IAnalytics a, IResourceLinker linker, IRepository userLogins, IPlexUserRepository plexUsers, ICustomUserMapper custom, + ISettingsService lp, IAnalytics a, IResourceLinker linker, IRepository userLogins, IExternalUserRepository plexUsers, ICustomUserMapper custom, ISecurityExtensions security, ISettingsService userManagementSettings) : base("userlogin", pr, security) { @@ -162,7 +163,7 @@ namespace Ombi.UI.Modules private IResourceLinker Linker { get; } private IAnalytics Analytics { get; } private IRepository UserLogins { get; } - private IPlexUserRepository PlexUserRepository { get; } + private IExternalUserRepository PlexUserRepository { get; } private ICustomUserMapper CustomUserMapper { get; } private ISettingsService UserManagementSettings { get; } diff --git a/Ombi.UI/Modules/UserManagementModule.cs b/Ombi.UI/Modules/UserManagementModule.cs index af62f880e..6e559d419 100644 --- a/Ombi.UI/Modules/UserManagementModule.cs +++ b/Ombi.UI/Modules/UserManagementModule.cs @@ -7,6 +7,7 @@ using Nancy.Extensions; using Nancy.Responses.Negotiation; using Newtonsoft.Json; using Ombi.Api.Interfaces; +using Ombi.Api.Models.Emby; using Ombi.Api.Models.Plex; using Ombi.Core; using Ombi.Core.Models; @@ -16,6 +17,8 @@ using Ombi.Helpers.Analytics; using Ombi.Helpers.Permissions; using Ombi.Store; using Ombi.Store.Models; +using Ombi.Store.Models.Emby; +using Ombi.Store.Models.Plex; using Ombi.Store.Repository; using Ombi.UI.Models; using Ombi.UI.Models.UserManagement; @@ -26,8 +29,8 @@ namespace Ombi.UI.Modules { public class UserManagementModule : BaseModule { - public UserManagementModule(ISettingsService pr, ICustomUserMapper m, IPlexApi plexApi, ISettingsService plex, IRepository userLogins, IPlexUserRepository plexRepo - , ISecurityExtensions security, IRequestService req, IAnalytics ana) : base("usermanagement", pr, security) + public UserManagementModule(ISettingsService pr, ICustomUserMapper m, IPlexApi plexApi, ISettingsService plex, IRepository userLogins, IExternalUserRepository plexRepo + , ISecurityExtensions security, IRequestService req, IAnalytics ana, ISettingsService embyService, IEmbyApi embyApi, IExternalUserRepository embyRepo) : base("usermanagement", pr, security) { #if !DEBUG Before += (ctx) => Security.AdminLoginRedirect(Permissions.Administrator, ctx); @@ -40,6 +43,9 @@ namespace Ombi.UI.Modules PlexRequestSettings = pr; RequestService = req; Analytics = ana; + EmbySettings = embyService; + EmbyApi = embyApi; + EmbyRepository = embyRepo; Get["/"] = x => Load(); @@ -57,10 +63,13 @@ namespace Ombi.UI.Modules private IPlexApi PlexApi { get; } private ISettingsService PlexSettings { get; } private IRepository UserLoginsRepo { get; } - private IPlexUserRepository PlexUsersRepository { get; } + private IExternalUserRepository PlexUsersRepository { get; } + private IExternalUserRepository EmbyRepository { get; } private ISettingsService PlexRequestSettings { get; } + private ISettingsService EmbySettings { get; } private IRequestService RequestService { get; } private IAnalytics Analytics { get; } + private IEmbyApi EmbyApi { get; } private Negotiator Load() { @@ -69,48 +78,19 @@ namespace Ombi.UI.Modules private async Task LoadUsers() { - var localUsers = await UserMapper.GetUsersAsync(); - var plexDbUsers = await PlexUsersRepository.GetAllAsync(); - var model = new List(); - var userLogins = UserLoginsRepo.GetAll().ToList(); - - foreach (var user in localUsers) + var plexSettings = await PlexSettings.GetSettingsAsync(); + var embySettings = await EmbySettings.GetSettingsAsync(); + if (plexSettings.Enable) { - var userDb = userLogins.FirstOrDefault(x => x.UserId == user.UserGuid); - model.Add(MapLocalUser(user, userDb?.LastLoggedIn ?? DateTime.MinValue)); + return await LoadPlexUsers(); } - - var plexSettings = await PlexSettings.GetSettingsAsync(); - if (!string.IsNullOrEmpty(plexSettings.PlexAuthToken)) + if (embySettings.Enable) { - //Get Plex Users - var plexUsers = PlexApi.GetUsers(plexSettings.PlexAuthToken); - if (plexUsers != null && plexUsers.User != null) { - foreach (var u in plexUsers.User) { - var dbUser = plexDbUsers.FirstOrDefault (x => x.PlexUserId == u.Id); - var userDb = userLogins.FirstOrDefault (x => x.UserId == u.Id); - - // We don't have the user in the database yet - if (dbUser == null) { - model.Add (MapPlexUser (u, null, userDb?.LastLoggedIn ?? DateTime.MinValue)); - } else { - // The Plex User is in the database - model.Add (MapPlexUser (u, dbUser, userDb?.LastLoggedIn ?? DateTime.MinValue)); - } - } - } - - // Also get the server admin - var account = PlexApi.GetAccount(plexSettings.PlexAuthToken); - if (account != null) - { - var dbUser = plexDbUsers.FirstOrDefault(x => x.PlexUserId == account.Id); - var userDb = userLogins.FirstOrDefault(x => x.UserId == account.Id); - model.Add(MapPlexAdmin(account, dbUser, userDb?.LastLoggedIn ?? DateTime.MinValue)); - } + return await LoadEmbyUsers(); } - return Response.AsJson(model); + + return null; } private async Task CreateUser() @@ -416,7 +396,7 @@ namespace Ombi.UI.Modules var m = new UserManagementUsersViewModel { Id = plexInfo.Id, - PermissionsFormattedString = newUser ? "Processing..." :( permissions == 0 ? "None" : permissions.ToString()), + PermissionsFormattedString = newUser ? "Processing..." : (permissions == 0 ? "None" : permissions.ToString()), FeaturesFormattedString = newUser ? "Processing..." : features.ToString(), Username = plexInfo.Title, Type = UserType.PlexUser, @@ -436,6 +416,36 @@ namespace Ombi.UI.Modules return m; } + private UserManagementUsersViewModel MapEmbyUser(EmbyUser embyInfo, EmbyUsers dbUser, DateTime lastLoggedIn) + { + var newUser = false; + if (dbUser == null) + { + newUser = true; + dbUser = new EmbyUsers(); + } + var features = (Features)dbUser?.Features; + var permissions = (Permissions)dbUser?.Permissions; + + var m = new UserManagementUsersViewModel + { + Id = embyInfo.Id, + PermissionsFormattedString = newUser ? "Processing..." : (permissions == 0 ? "None" : permissions.ToString()), + FeaturesFormattedString = newUser ? "Processing..." : features.ToString(), + Username = embyInfo.Name, + Type = UserType.EmbyUser, + EmailAddress =dbUser.EmailAddress, + Alias = dbUser?.UserAlias ?? string.Empty, + LastLoggedIn = lastLoggedIn, + ManagedUser = false + }; + + m.Permissions.AddRange(GetPermissions(permissions)); + m.Features.AddRange(GetFeatures(features)); + + return m; + } + private UserManagementUsersViewModel MapPlexAdmin(PlexAccount plexInfo, PlexUsers dbUser, DateTime lastLoggedIn) { var newUser = false; @@ -505,6 +515,102 @@ namespace Ombi.UI.Modules } return retVal; } + + private async Task LoadPlexUsers() + { + var localUsers = await UserMapper.GetUsersAsync(); + var plexDbUsers = await PlexUsersRepository.GetAllAsync(); + var model = new List(); + + var userLogins = UserLoginsRepo.GetAll().ToList(); + + foreach (var user in localUsers) + { + var userDb = userLogins.FirstOrDefault(x => x.UserId == user.UserGuid); + model.Add(MapLocalUser(user, userDb?.LastLoggedIn ?? DateTime.MinValue)); + } + + var plexSettings = await PlexSettings.GetSettingsAsync(); + if (!string.IsNullOrEmpty(plexSettings.PlexAuthToken)) + { + //Get Plex Users + var plexUsers = PlexApi.GetUsers(plexSettings.PlexAuthToken); + if (plexUsers != null && plexUsers.User != null) + { + foreach (var u in plexUsers.User) + { + var dbUser = plexDbUsers.FirstOrDefault(x => x.PlexUserId == u.Id); + var userDb = userLogins.FirstOrDefault(x => x.UserId == u.Id); + + // We don't have the user in the database yet + if (dbUser == null) + { + model.Add(MapPlexUser(u, null, userDb?.LastLoggedIn ?? DateTime.MinValue)); + } + else + { + // The Plex User is in the database + model.Add(MapPlexUser(u, dbUser, userDb?.LastLoggedIn ?? DateTime.MinValue)); + } + } + } + + // Also get the server admin + var account = PlexApi.GetAccount(plexSettings.PlexAuthToken); + if (account != null) + { + var dbUser = plexDbUsers.FirstOrDefault(x => x.PlexUserId == account.Id); + var userDb = userLogins.FirstOrDefault(x => x.UserId == account.Id); + model.Add(MapPlexAdmin(account, dbUser, userDb?.LastLoggedIn ?? DateTime.MinValue)); + } + } + return Response.AsJson(model); + } + + private async Task LoadEmbyUsers() + { + var localUsers = await UserMapper.GetUsersAsync(); + var embyDbUsers = await EmbyRepository.GetAllAsync(); + var model = new List(); + + var userLogins = UserLoginsRepo.GetAll().ToList(); + + foreach (var user in localUsers) + { + var userDb = userLogins.FirstOrDefault(x => x.UserId == user.UserGuid); + model.Add(MapLocalUser(user, userDb?.LastLoggedIn ?? DateTime.MinValue)); + } + + var embySettings = await EmbySettings.GetSettingsAsync(); + if (!string.IsNullOrEmpty(embySettings.ApiKey)) + { + //Get Plex Users + var plexUsers = EmbyApi.GetUsers(embySettings.FullUri, embySettings.ApiKey); + if (plexUsers != null) + { + foreach (var u in plexUsers) + { + var dbUser = embyDbUsers.FirstOrDefault(x => x.PlexUserId == u.Id); + var userDb = userLogins.FirstOrDefault(x => x.UserId == u.Id); + + // We don't have the user in the database yet + model.Add(dbUser == null + ? MapEmbyUser(u, null, userDb?.LastLoggedIn ?? DateTime.MinValue) + : MapEmbyUser(u, dbUser, userDb?.LastLoggedIn ?? DateTime.MinValue)); + } + } + + // Also get the server admin + var account = PlexApi.GetAccount(embySettings.PlexAuthToken); + if (account != null) + { + var dbUser = embyDbUsers.FirstOrDefault(x => x.PlexUserId == account.Id); + var userDb = userLogins.FirstOrDefault(x => x.UserId == account.Id); + model.Add(MapPlexAdmin(account, dbUser, userDb?.LastLoggedIn ?? DateTime.MinValue)); + } + } + return Response.AsJson(model); + } } } diff --git a/Ombi.UI/NinjectModules/ConfigurationModule.cs b/Ombi.UI/NinjectModules/ConfigurationModule.cs index 46937b65f..639194228 100644 --- a/Ombi.UI/NinjectModules/ConfigurationModule.cs +++ b/Ombi.UI/NinjectModules/ConfigurationModule.cs @@ -56,7 +56,8 @@ namespace Ombi.UI.NinjectModules Bind().To(); Bind().To().InSingletonScope(); - Bind().To(); + Bind().To(); + Bind().To(); Bind().To(); diff --git a/Ombi.UI/NinjectModules/RepositoryModule.cs b/Ombi.UI/NinjectModules/RepositoryModule.cs index e443924f1..601a7e89f 100644 --- a/Ombi.UI/NinjectModules/RepositoryModule.cs +++ b/Ombi.UI/NinjectModules/RepositoryModule.cs @@ -40,6 +40,7 @@ namespace Ombi.UI.NinjectModules { Bind>().To>(); Bind(typeof(IRepository<>)).To(typeof(GenericRepository<>)); + Bind(typeof(IExternalUserRepository<>)).To(typeof(BaseExternalUserRepository<>)); Bind().To(); Bind().To(); @@ -48,7 +49,6 @@ namespace Ombi.UI.NinjectModules Bind().To(); Bind().To(); - Bind().To(); } } diff --git a/Ombi.UI/Ombi.UI.csproj b/Ombi.UI/Ombi.UI.csproj index 832d395af..eebd32612 100644 --- a/Ombi.UI/Ombi.UI.csproj +++ b/Ombi.UI/Ombi.UI.csproj @@ -402,7 +402,7 @@ PreserveNewest - + PreserveNewest @@ -502,9 +502,6 @@ PreserveNewest - - PreserveNewest - Always diff --git a/Ombi.UI/Views/UserWizard/Index.cshtml b/Ombi.UI/Views/UserWizard/Index.cshtml index 634bf4ad4..051ce0cf5 100644 --- a/Ombi.UI/Views/UserWizard/Index.cshtml +++ b/Ombi.UI/Views/UserWizard/Index.cshtml @@ -10,7 +10,7 @@ } @Html.LoadWizardAssets() - +