Merge pull request #733 from tidusjar/dev

Dev
pull/780/head
Jamie 8 years ago committed by GitHub
commit 10e7a3f598

3
.gitattributes vendored

@ -66,6 +66,3 @@
PlexRequests.UI/Content/* linguist-vendored
PlexRequests.UI/Content/* linguist-vendored
base.scss linguist-vendored=false
requests-1.7.js linguist-vendored=false
search-1.7.js linguist-vendored=false
site-1.7.js linguist-vendored=false

@ -3,6 +3,8 @@ If this is a bug report please make sure you have filled the following in:
#### Plex Requests.Net Version:
#### update Branch:
#### Operating System:

@ -104,8 +104,7 @@ namespace PlexRequests.Api
}
var result = DeserializeXml<T>(response.Content);
return result;
}
return result;}
public T ExecuteJson<T>(IRestRequest request, Uri baseUri) where T : new()
{

@ -65,9 +65,7 @@ namespace PlexRequests.Api
var obj = RetryHandler.Execute(() => Api.ExecuteJson<JObject>(request, baseUrl),
(exception, timespan) => Log.Error(exception, "Exception when calling AddMovie for CP, Retrying {0}", timespan), new[] {
TimeSpan.FromSeconds (2),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)});
TimeSpan.FromSeconds (2)});
if (obj.Count > 0)

@ -33,7 +33,7 @@ namespace PlexRequests.Api
{
public static class RetryHandler
{
private static readonly TimeSpan[] DefaultRetryTime = { TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10) };
private static readonly TimeSpan[] DefaultRetryTime = { TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5) };
public static T Execute<T>(Func<T> action, TimeSpan[] timeSpan = null)
{

@ -117,8 +117,7 @@ namespace PlexRequests.Api
try
{
var policy = RetryHandler.RetryAndWaitPolicy((exception, timespan) => Log.Error(exception, "Exception when calling AddSeries for Sonarr, Retrying {0}", timespan), new TimeSpan[] {
TimeSpan.FromSeconds (1),
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds (2)
});
result = policy.Execute(() => Api.ExecuteJson<SonarrAddSeries>(request, baseUrl));
@ -186,8 +185,7 @@ namespace PlexRequests.Api
try
{
var policy = RetryHandler.RetryAndWaitPolicy((exception, timespan) => Log.Error(exception, "Exception when calling AddSeries for Sonarr, Retrying {0}", timespan), new TimeSpan[] {
TimeSpan.FromSeconds (1),
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds (2)
});
result = policy.Execute(() => Api.ExecuteJson<SonarrAddSeries>(request, baseUrl));
@ -228,8 +226,8 @@ namespace PlexRequests.Api
{
var policy = RetryHandler.RetryAndWaitPolicy((exception, timespan) => Log.Error(exception, "Exception when calling GetSeries for Sonarr, Retrying {0}", timespan), new TimeSpan[] {
TimeSpan.FromSeconds (5),
TimeSpan.FromSeconds(10),
TimeSpan.FromSeconds(30)
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(5)
});
return policy.Execute(() => Api.ExecuteJson<List<Series>>(request, baseUrl));

@ -24,40 +24,29 @@ namespace PlexRequests.Core.Migration
var con = Db.DbConnection();
var versions = GetMigrations();
var dbVersion = con.GetVersionInfo().OrderByDescending(x => x.Version).FirstOrDefault();
if (dbVersion == null)
{
dbVersion = new TableCreation.VersionInfo { Version = 0 };
}
var dbVersion = con.GetVersionInfo().OrderByDescending(x => x.Version).FirstOrDefault() ??
new TableCreation.VersionInfo { Version = 0 };
foreach (var v in versions)
{
#if !DEBUG
if (v.Value.Version > dbVersion.Version)
{
// Assuming only one constructor
var ctor = v.Key.GetConstructors().FirstOrDefault();
var dependencies = new List<object>();
foreach (var param in ctor.GetParameters())
{
Console.WriteLine(string.Format(
"Param {0} is named {1} and is of type {2}",
param.Position, param.Name, param.ParameterType));
var dep = Kernel.Get(param.ParameterType);
dependencies.Add(dep);
}
#endif
// Assuming only one constructor
var ctor = v.Key.GetConstructors().FirstOrDefault();
var dependencies = ctor.GetParameters().Select(param => Kernel.Get(param.ParameterType)).ToList();
var method = v.Key.GetMethod("Start");
if (method != null)
{
object result = null;
var classInstance = Activator.CreateInstance(v.Key, dependencies.Any() ? dependencies.ToArray() : null);
var parametersArray = new object[] { Db.DbConnection() };
var method = v.Key.GetMethod("Start");
if (method != null)
{
var classInstance = Activator.CreateInstance(v.Key, dependencies.Any() ? dependencies.ToArray() : null);
var parametersArray = new object[] { Db.DbConnection() };
method.Invoke(classInstance, parametersArray);
}
method.Invoke(classInstance, parametersArray);
}
#if !DEBUG
}
#endif
}
}

@ -25,34 +25,250 @@
// ************************************************************************/
#endregion
using System;
using System.Collections.Generic;
using System.Data;
using NLog;
using System.Linq;
using PlexRequests.Api.Interfaces;
using PlexRequests.Core.SettingModels;
using PlexRequests.Core.Users;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Permissions;
using PlexRequests.Store;
using PlexRequests.Store.Models;
using PlexRequests.Store.Repository;
namespace PlexRequests.Core.Migration.Migrations
{
[Migration(11000, "v1.10.0.0")]
public class Version1100 : BaseMigration, IMigration
{
public Version1100()
public Version1100(IUserRepository userRepo, IRequestService requestService, ISettingsService<LogSettings> log, IPlexApi plexApi, ISettingsService<PlexSettings> plexService,
IPlexUserRepository plexusers, ISettingsService<PlexRequestSettings> prSettings, ISettingsService<UserManagementSettings> umSettings,
ISettingsService<ScheduledJobsSettings> sjs, IRepository<UsersToNotify> usersToNotify)
{
UserRepo = userRepo;
RequestService = requestService;
Log = log;
PlexApi = plexApi;
PlexSettings = plexService;
PlexUsers = plexusers;
PlexRequestSettings = prSettings;
UserManagementSettings = umSettings;
ScheduledJobSettings = sjs;
UserNotifyRepo = usersToNotify;
}
public int Version => 11000;
private IUserRepository UserRepo { get; }
private IRequestService RequestService { get; }
private ISettingsService<LogSettings> Log { get; }
private IPlexApi PlexApi { get; }
private ISettingsService<PlexSettings> PlexSettings { get; }
private IPlexUserRepository PlexUsers { get; }
private ISettingsService<PlexRequestSettings> PlexRequestSettings { get; }
private ISettingsService<UserManagementSettings> UserManagementSettings { get; }
private ISettingsService<ScheduledJobsSettings> ScheduledJobSettings { get; }
private IRepository<UsersToNotify> UserNotifyRepo { get; }
public void Start(IDbConnection con)
{
UpdateDb(con);
// Update the current admin permissions set
PopulateDefaultUserManagementSettings();
UpdateAdmin();
ResetLogLevel();
UpdatePlexUsers();
UpdateScheduledJobs();
MigrateUserNotifications();
UpdateSchema(con, Version);
}
private void MigrateUserNotifications()
{
var usersToNotify = UserNotifyRepo.GetAll();
var plexUsers = PlexUsers.GetAll().ToList();
var users = UserRepo.GetAll().ToList();
if (usersToNotify == null)
{
return;
}
foreach (var u in usersToNotify)
{
var selectedPlexUser = plexUsers.FirstOrDefault(x => x.Username.Equals(u.Username, StringComparison.CurrentCultureIgnoreCase));
if (selectedPlexUser != null)
{
selectedPlexUser.Features += (int)Features.RequestAddedNotification;
PlexUsers.Update(selectedPlexUser);
}
var selectedLocalUser =
users.FirstOrDefault(x => x.UserName.Equals(u.Username, StringComparison.CurrentCultureIgnoreCase));
if (selectedLocalUser != null)
{
selectedLocalUser.Features += (int)Features.RequestAddedNotification;
UserRepo.Update(selectedLocalUser);
}
}
}
private void UpdateScheduledJobs()
{
var settings = ScheduledJobSettings.GetSettings();
settings.PlexUserChecker = 24;
settings.PlexContentCacher = 60;
ScheduledJobSettings.SaveSettings(settings);
}
private void PopulateDefaultUserManagementSettings()
{
var plexRequestSettings = PlexRequestSettings.GetSettings();
UserManagementSettings.SaveSettings(new UserManagementSettings
{
AutoApproveMovies = !plexRequestSettings.RequireMovieApproval,
RequestTvShows = plexRequestSettings.SearchForTvShows,
RequestMusic = plexRequestSettings.SearchForMusic,
RequestMovies = plexRequestSettings.SearchForMovies,
AutoApproveMusic = !plexRequestSettings.RequireMusicApproval,
AutoApproveTvShows = !plexRequestSettings.RequireTvShowApproval
});
}
private void UpdatePlexUsers()
{
var settings = PlexSettings.GetSettings();
if (string.IsNullOrEmpty(settings.PlexAuthToken))
{
return;
}
var plexUsers = PlexApi.GetUsers(settings.PlexAuthToken);
if (plexUsers?.User == null)
{
return;
}
var prSettings = PlexRequestSettings.GetSettings();
var dbUsers = PlexUsers.GetAll().ToList();
foreach (var user in plexUsers.User)
{
if (dbUsers.FirstOrDefault(x => x.PlexUserId == user.Id) != null)
{
continue;
}
int permissions = 0;
if (prSettings.SearchForMovies)
{
permissions = (int)Permissions.RequestMovie;
}
if (prSettings.SearchForTvShows)
{
permissions += (int)Permissions.RequestTvShow;
}
if (prSettings.SearchForMusic)
{
permissions += (int)Permissions.RequestMusic;
}
if (!prSettings.RequireMovieApproval)
{
permissions += (int)Permissions.AutoApproveMovie;
}
if (!prSettings.RequireTvShowApproval)
{
permissions += (int)Permissions.AutoApproveTv;
}
if (!prSettings.RequireMusicApproval)
{
permissions += (int)Permissions.AutoApproveAlbum;
}
// Add report Issues
permissions += (int)Permissions.ReportIssue;
var m = new PlexUsers
{
PlexUserId = user.Id,
Permissions = permissions,
Features = 0,
UserAlias = string.Empty,
EmailAddress = user.Email,
Username = user.Username,
LoginId = Guid.NewGuid().ToString()
};
PlexUsers.Insert(m);
}
}
private void ResetLogLevel()
{
var logSettings = Log.GetSettings();
logSettings.Level = LogLevel.Error.Ordinal;
Log.SaveSettings(logSettings);
LoggingHelper.ReconfigureLogLevel(LogLevel.FromOrdinal(logSettings.Level));
}
private void UpdateDb(IDbConnection con)
{
// Create the two new columns
con.AlterTable("Users", "ADD", "Permissions", true, "INTEGER");
con.AlterTable("Users", "ADD", "Features", true, "INTEGER");
con.AlterTable("PlexUsers", "ADD", "Permissions", true, "INTEGER");
con.AlterTable("PlexUsers", "ADD", "Features", true, "INTEGER");
con.AlterTable("PlexUsers", "ADD", "Username", true, "VARCHAR(100)");
con.AlterTable("PlexUsers", "ADD", "EmailAddress", true, "VARCHAR(100)");
con.AlterTable("PlexUsers", "ADD", "LoginId", true, "VARCHAR(100)");
//https://image.tmdb.org/t/p/w150/https://image.tmdb.org/t/p/w150//aqhAqttDq7zgsTaBHtCD8wmTk6k.jpg
// UI = https://image.tmdb.org/t/p/w150/{{posterPath}}
// Update old invalid posters
var allRequests = RequestService.GetAll().ToList();
foreach (var req in allRequests)
{
if (req.PosterPath.Contains("https://image.tmdb.org/t/p/w150/"))
{
var newImg = req.PosterPath.Replace("https://image.tmdb.org/t/p/w150/", string.Empty);
req.PosterPath = newImg;
}
}
RequestService.BatchUpdate(allRequests);
}
private void UpdateAdmin()
{
var users = UserRepo.GetAll().ToList();
foreach (var user in users)
{
user.Permissions = (int)
(Permissions.Administrator
| Permissions.ReportIssue
| Permissions.RequestMusic
| Permissions.RequestTvShow
| Permissions.RequestMovie
| Permissions.AutoApproveAlbum
| Permissions.AutoApproveMovie
| Permissions.AutoApproveTv);
}
UserRepo.UpdateAll(users);
}
}
}

@ -45,6 +45,10 @@
<Reference Include="Ninject">
<HintPath>..\packages\Ninject.3.2.0.0\lib\net45-full\Ninject.dll</HintPath>
</Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.3.11\lib\net45\NLog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Quartz, Version=2.3.3.0, Culture=neutral, PublicKeyToken=f6b8c98a402cc8a4, processorArchitecture=MSIL">
<HintPath>..\packages\Quartz.2.3.3\lib\net40\Quartz.dll</HintPath>
<Private>True</Private>
@ -70,10 +74,22 @@
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PlexRequests.Api.Interfaces\PlexRequests.Api.Interfaces.csproj">
<Project>{95834072-A675-415D-AA8F-877C91623810}</Project>
<Name>PlexRequests.Api.Interfaces</Name>
</ProjectReference>
<ProjectReference Include="..\PlexRequests.Api.Models\PlexRequests.Api.Models.csproj">
<Project>{CB37A5F8-6DFC-4554-99D3-A42B502E4591}</Project>
<Name>PlexRequests.Api.Models</Name>
</ProjectReference>
<ProjectReference Include="..\PlexRequests.Core\PlexRequests.Core.csproj">
<Project>{DD7DC444-D3BF-4027-8AB9-EFC71F5EC581}</Project>
<Name>PlexRequests.Core</Name>
</ProjectReference>
<ProjectReference Include="..\PlexRequests.Helpers\PlexRequests.Helpers.csproj">
<Project>{1252336D-42A3-482A-804C-836E60173DFA}</Project>
<Name>PlexRequests.Helpers</Name>
</ProjectReference>
<ProjectReference Include="..\PlexRequests.Store\PlexRequests.Store.csproj">
<Project>{92433867-2B7B-477B-A566-96C382427525}</Project>
<Name>PlexRequests.Store</Name>

@ -2,5 +2,6 @@
<packages>
<package id="Common.Logging" version="3.0.0" targetFramework="net45" />
<package id="Common.Logging.Core" version="3.0.0" targetFramework="net45" />
<package id="NLog" version="4.3.11" targetFramework="net45" />
<package id="Quartz" version="2.3.3" targetFramework="net45" />
</packages>

@ -60,7 +60,6 @@
</Choose>
<ItemGroup>
<Compile Include="AuthenticationSettingsTests.cs" />
<Compile Include="StatusCheckerTests.cs" />
<Compile Include="NotificationMessageResolverTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>

@ -30,7 +30,7 @@ namespace PlexRequests.Core
{
public struct TimeFrameMinutes
{
public const int SchedulerCaching = 60;
public const int SchedulerCaching = 120;
}
public const string PlexLibaries = nameof(PlexLibaries);

@ -24,18 +24,16 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NLog;
using PlexRequests.Api.Interfaces;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Store;
namespace PlexRequests.UI.Helpers
namespace PlexRequests.Core
{
public class HeadphonesSender
{

@ -0,0 +1,34 @@
using System;
using Nancy;
using Nancy.Security;
using PlexRequests.Helpers.Permissions;
namespace PlexRequests.Core
{
public interface ISecurityExtensions
{
Response AdminLoginRedirect(Permissions perm, NancyContext context);
Response AdminLoginRedirect(NancyContext context, params Permissions[] perm);
bool DoesNotHavePermissions(Permissions perm, IUserIdentity currentUser);
Response HasAnyPermissionsRedirect(NancyContext context, string routeName, HttpStatusCode code,
params Permissions[] perm);
bool DoesNotHavePermissions(int perm, IUserIdentity currentUser);
Func<NancyContext, Response> ForbiddenIfNot(Func<NancyContext, bool> test);
bool HasAnyPermissions(IUserIdentity user, params Permissions[] perm);
bool HasPermissions(IUserIdentity user, Permissions perm);
Response HasPermissionsRedirect(Permissions perm, NancyContext context, string routeName, HttpStatusCode code);
Func<NancyContext, Response> HttpStatusCodeIfNot(HttpStatusCode statusCode, Func<NancyContext, bool> test);
bool IsLoggedIn(NancyContext context);
bool IsNormalUser(IUserIdentity user);
bool IsPlexUser(IUserIdentity user);
bool HasPermissions(string userName, Permissions perm);
/// <summary>
/// Gets the username this could be the alias! We should always use this method when getting the username
/// </summary>
/// <param name="username">The username.</param>
/// <returns><c>null</c> if we cannot find a user</returns>
string GetUsername(string username);
}
}

@ -0,0 +1,11 @@
using System.Threading.Tasks;
using Octokit;
using PlexRequests.Core.Models;
namespace PlexRequests.Core
{
public interface IStatusChecker
{
Task<StatusModel> GetStatus();
}
}

@ -34,6 +34,7 @@ namespace PlexRequests.Core.Models
RequestApproved,
AdminNote,
Test,
RequestDeclined,
ItemAddedToFaultQueue
}
}

@ -28,7 +28,8 @@ namespace PlexRequests.Core.Models
{
public class StatusModel
{
public string Version { get; set; }
public string CurrentVersion { get; set; }
public string NewVersion { get; set; }
public bool UpdateAvailable { get; set; }
public string UpdateUri { get; set; }
public string DownloadUri { get; set; }

@ -39,9 +39,17 @@
<HintPath>..\packages\Common.Logging.Core.3.0.0\lib\net40\Common.Logging.Core.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Dapper, Version=1.50.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Dapper.1.50.0-beta8\lib\net45\Dapper.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Mono.Data.Sqlite">
<HintPath>..\Assemblies\Mono.Data.Sqlite.dll</HintPath>
</Reference>
<Reference Include="Nancy.Linker, Version=0.3.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Nancy.Linker.0.3.1\lib\net40-Client\Nancy.Linker.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
@ -54,7 +62,12 @@
<HintPath>..\packages\Quartz.2.3.3\lib\net40\Quartz.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="RestSharp, Version=105.2.3.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\RestSharp.105.2.3\lib\net45\RestSharp.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.ComponentModel.DataAnnotations" />
<Reference Include="System.Core" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml.Linq" />
@ -81,7 +94,10 @@
</ItemGroup>
<ItemGroup>
<Compile Include="CacheKeys.cs" />
<Compile Include="HeadphonesSender.cs" />
<Compile Include="IPlexReadOnlyDatabase.cs" />
<Compile Include="ISecurityExtensions.cs" />
<Compile Include="IStatusChecker.cs" />
<Compile Include="Notification\NotificationMessage.cs" />
<Compile Include="Notification\NotificationMessageContent.cs" />
<Compile Include="Notification\NotificationMessageCurlys.cs" />
@ -99,7 +115,9 @@
<Compile Include="Notification\Templates\IEmailBasicTemplate.cs" />
<Compile Include="Notification\TransportType.cs" />
<Compile Include="PlexReadOnlyDatabase.cs" />
<Compile Include="Queue\ITransientFaultQueue.cs" />
<Compile Include="Queue\TransientFaultQueue.cs" />
<Compile Include="SecurityExtensions.cs" />
<Compile Include="SettingModels\AuthenticationSettings.cs" />
<Compile Include="SettingModels\ExternalSettings.cs" />
<Compile Include="SettingModels\HeadphonesSettings.cs" />
@ -120,12 +138,22 @@
<Compile Include="SettingModels\CouchPotatoSettings.cs" />
<Compile Include="SettingModels\PlexRequestSettings.cs" />
<Compile Include="SettingModels\Settings.cs" />
<Compile Include="SettingModels\SystemSettings.cs" />
<Compile Include="SettingModels\UserManagementSettings.cs" />
<Compile Include="SettingsServiceV2.cs" />
<Compile Include="Setup.cs" />
<Compile Include="StatusChecker.cs" />
<Compile Include="StatusChecker\AppveyorArtifactResult.cs" />
<Compile Include="StatusChecker\StatusChecker.cs" />
<Compile Include="StatusChecker\AppveyorBranchResult.cs" />
<Compile Include="TvSender.cs" />
<Compile Include="TvSenderOld.cs" />
<Compile Include="Users\IUserHelper.cs" />
<Compile Include="Users\UserHelper.cs" />
<Compile Include="UserIdentity.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="UserMapper.cs" />
<Compile Include="Users\UserHelperModel.cs" />
<Compile Include="Users\UserManagementHelper.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />

@ -0,0 +1,17 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using PlexRequests.Store;
using PlexRequests.Store.Models;
namespace PlexRequests.Core.Queue
{
public interface ITransientFaultQueue
{
void Dequeue();
Task DequeueAsync();
IEnumerable<RequestQueue> GetQueue();
Task<IEnumerable<RequestQueue>> GetQueueAsync();
void QueueItem(RequestedModel request, string id, RequestType type, FaultType faultType);
Task QueueItemAsync(RequestedModel request, string id, RequestType type, FaultType faultType, string message = null);
}
}

@ -26,7 +26,9 @@
#endregion
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Dapper;
using PlexRequests.Helpers;
using PlexRequests.Store;
using PlexRequests.Store.Models;
@ -34,7 +36,7 @@ using PlexRequests.Store.Repository;
namespace PlexRequests.Core.Queue
{
public class TransientFaultQueue
public class TransientFaultQueue : ITransientFaultQueue
{
public TransientFaultQueue(IRepository<RequestQueue> queue)
{
@ -44,44 +46,85 @@ namespace PlexRequests.Core.Queue
private IRepository<RequestQueue> RequestQueue { get; }
public void QueueItem(RequestedModel request, RequestType type)
public void QueueItem(RequestedModel request, string id, RequestType type, FaultType faultType)
{
//Ensure there is not a duplicate queued item
var existingItem = RequestQueue.Custom(
connection =>
{
connection.Open();
var result = connection.Query<RequestQueue>("select * from RequestQueue where PrimaryIdentifier = @ProviderId", new { ProviderId = id });
return result;
}).FirstOrDefault();
if (existingItem != null)
{
// It's already in the queue
return;
}
var queue = new RequestQueue
{
Type = type,
Content = ByteConverterHelper.ReturnBytes(request),
PrimaryIdentifier = request.ProviderId
PrimaryIdentifier = id,
FaultType = faultType
};
RequestQueue.Insert(queue);
}
public async Task QueueItemAsync(RequestedModel request, RequestType type)
public async Task QueueItemAsync(RequestedModel request, string id, RequestType type, FaultType faultType, string description = null)
{
//Ensure there is not a duplicate queued item
var existingItem = await RequestQueue.CustomAsync(async connection =>
{
connection.Open();
var result = await connection.QueryAsync<RequestQueue>("select * from RequestFaultQueue where PrimaryIdentifier = @ProviderId", new { ProviderId = id });
return result;
});
if (existingItem.FirstOrDefault() != null)
{
// It's already in the queue
return;
}
var queue = new RequestQueue
{
Type = type,
Content = ByteConverterHelper.ReturnBytes(request),
PrimaryIdentifier = request.ProviderId
PrimaryIdentifier = id,
FaultType = faultType,
Message = description ?? string.Empty
};
await RequestQueue.InsertAsync(queue);
}
public IEnumerable<RequestQueue> Dequeue()
public IEnumerable<RequestQueue> GetQueue()
{
var items = RequestQueue.GetAll();
RequestQueue.DeleteAll("RequestQueue");
return items;
}
public async Task<IEnumerable<RequestQueue>> DequeueAsync()
public async Task<IEnumerable<RequestQueue>> GetQueueAsync()
{
var items = RequestQueue.GetAllAsync();
await RequestQueue.DeleteAllAsync("RequestQueue");
return await items;
}
public void Dequeue()
{
RequestQueue.DeleteAll("RequestQueue");
}
public async Task DequeueAsync()
{
await RequestQueue.DeleteAllAsync("RequestQueue");
}
}
}

@ -0,0 +1,302 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: SecurityExtensions.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 Nancy;
using Nancy.Linker;
using Nancy.Responses;
using Nancy.Security;
using PlexRequests.Core.Models;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Permissions;
using PlexRequests.Store.Repository;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
namespace PlexRequests.Core
{
public class SecurityExtensions : ISecurityExtensions
{
public SecurityExtensions(IUserRepository userRepository, IResourceLinker linker, IPlexUserRepository plexUsers)
{
UserRepository = userRepository;
Linker = linker;
PlexUsers = plexUsers;
}
private IUserRepository UserRepository { get; }
private IResourceLinker Linker { get; }
private IPlexUserRepository PlexUsers { get; }
public bool IsLoggedIn(NancyContext context)
{
var userName = context.Request.Session[SessionKeys.UsernameKey];
var realUser = false;
var plexUser = userName != null;
if (context.CurrentUser?.IsAuthenticated() ?? false)
{
realUser = true;
}
return realUser || plexUser;
}
public bool IsPlexUser(IUserIdentity user)
{
if (user == null)
{
return false;
}
var plexUser = PlexUsers.GetUserByUsername(user.UserName);
return plexUser != null;
}
public bool IsNormalUser(IUserIdentity user)
{
if (user == null)
{
return false;
}
var dbUser = UserRepository.GetUserByUsername(user.UserName);
return dbUser != null;
}
/// <summary>
/// Gets the username this could be the alias! We should always use this method when getting the username
/// </summary>
/// <param name="username">The username.</param>
/// <returns><c>null</c> if we cannot find a user</returns>
public string GetUsername(string username)
{
var plexUser = PlexUsers.GetUserByUsername(username);
if (plexUser != null)
{
if (!string.IsNullOrEmpty(plexUser.UserAlias))
{
return plexUser.UserAlias;
}
else
{
return plexUser.Username;
}
}
var dbUser = UserRepository.GetUserByUsername(username);
if (dbUser != null)
{
var userProps = ByteConverterHelper.ReturnObject<UserProperties>(dbUser.UserProperties);
if (!string.IsNullOrEmpty(userProps.UserAlias))
{
return userProps.UserAlias;
}
else
{
return dbUser.UserName;
}
}
return null;
}
/// <summary>
/// Creates a hook to be used in a pipeline before a route handler to ensure
/// that the request was made by an authenticated user does not have the claims.
/// </summary>
/// <param name="perm">Claims the authenticated user needs to have</param>
/// <returns>Hook that returns an Unauthorized response if the user is not
/// authenticated or does have the claims, null otherwise</returns>
private Func<NancyContext, Response> DoesNotHavePermissions(int perm)
{
return ForbiddenIfNot(ctx =>
{
var permissions = GetPermissions(ctx.CurrentUser);
var result = permissions.HasFlag((Permissions)perm);
return !result;
});
}
public bool DoesNotHavePermissions(int perm, IUserIdentity currentUser)
{
return DoesNotHavePermissions((Permissions)perm, currentUser);
}
public bool DoesNotHavePermissions(Permissions perm, IUserIdentity currentUser)
{
var permissions = GetPermissions(currentUser);
var result = permissions.HasFlag(perm);
return !result;
}
public bool HasPermissions(IUserIdentity user, Permissions perm)
{
var permissions = GetPermissions(user);
return permissions.HasFlag(perm);
}
public bool HasPermissions(string userName, Permissions perm)
{
var permissions = GetPermissions(userName);
return permissions.HasFlag(perm);
}
public bool HasAnyPermissions(IUserIdentity user, params Permissions[] perm)
{
var permissions = GetPermissions(user);
foreach (var p in perm)
{
var result = permissions.HasFlag(p);
if (result)
{
return true;
}
}
return false;
}
public Response HasPermissionsRedirect(Permissions perm, NancyContext context, string routeName, HttpStatusCode code)
{
var url = Linker.BuildRelativeUri(context, routeName);
var response = ForbiddenIfNot(ctx =>
{
var permissions = GetPermissions(ctx.CurrentUser);
var result = permissions.HasFlag(perm);
return result;
});
var r = response(context);
return r.StatusCode == code
? new RedirectResponse($"{url.ToString()}?redirect={context.Request.Path}")
: null;
}
public Response HasAnyPermissionsRedirect(NancyContext context, string routeName, HttpStatusCode code, params Permissions[] perm)
{
var url = Linker.BuildRelativeUri(context, routeName);
var response = ForbiddenIfNot(ctx =>
{
var permissions = GetPermissions(ctx.CurrentUser);
var hasPermission = false;
foreach (var p in perm)
{
var result = permissions.HasFlag(p);
if (result)
{
hasPermission = true;
}
}
return hasPermission;
});
var r = response(context);
return r.StatusCode == code
? new RedirectResponse(url.ToString())
: null;
}
public Response AdminLoginRedirect(Permissions perm, NancyContext context)
{
// This will redirect us to the Login Page if we don't have the correct permission passed in (e.g. Admin with Http 403 status code).
return HasPermissionsRedirect(perm, context, "LocalLogin", HttpStatusCode.Forbidden);
}
public Response AdminLoginRedirect(NancyContext context, params Permissions[] perm)
{
// This will redirect us to the Login Page if we don't have the correct permission passed in (e.g. Admin with Http 403 status code).
return HasAnyPermissionsRedirect(context, "LocalLogin", HttpStatusCode.Forbidden, perm);
}
// BELOW IS A COPY FROM THE SecurityHooks CLASS!
/// <summary>
/// Creates a hook to be used in a pipeline before a route handler to ensure that
/// the request satisfies a specific test.
/// </summary>
/// <param name="test">Test that must return true for the request to continue</param>
/// <returns>Hook that returns an Forbidden response if the test fails, null otherwise</returns>
public Func<NancyContext, Response> ForbiddenIfNot(Func<NancyContext, bool> test)
{
return HttpStatusCodeIfNot(HttpStatusCode.Forbidden, test);
}
/// <summary>
/// Creates a hook to be used in a pipeline before a route handler to ensure that
/// the request satisfies a specific test.
/// </summary>
/// <param name="statusCode">HttpStatusCode to use for the response</param>
/// <param name="test">Test that must return true for the request to continue</param>
/// <returns>Hook that returns a response with a specific HttpStatusCode if the test fails, null otherwise</returns>
public Func<NancyContext, Response> HttpStatusCodeIfNot(HttpStatusCode statusCode, Func<NancyContext, bool> test)
{
return ctx =>
{
Response response = new Response
{
StatusCode = HttpStatusCode.OK
};
if (!test(ctx))
{
response = new Response
{
StatusCode = statusCode
};
}
return response;
};
}
private Permissions GetPermissions(IUserIdentity user)
{
return GetPermissions(user?.UserName);
}
private Permissions GetPermissions(string userName)
{
if (string.IsNullOrEmpty(userName)) return 0;
var dbUser = UserRepository.GetUserByUsername(userName);
if (dbUser != null)
{
var permissions = (Permissions)dbUser.Permissions;
return permissions;
}
var plexUser = PlexUsers.GetUserByUsername(userName);
if (plexUser != null)
{
var permissions = (Permissions)plexUser.Permissions;
return permissions;
}
return 0;
}
}
}

@ -44,10 +44,16 @@ namespace PlexRequests.Core.SettingModels
public bool SearchForMovies { get; set; }
public bool SearchForTvShows { get; set; }
public bool SearchForMusic { get; set; }
[Obsolete("Use the user management settings")]
public bool RequireMovieApproval { get; set; }
[Obsolete("Use the user management settings")]
public bool RequireTvShowApproval { get; set; }
[Obsolete("Use the user management settings")]
public bool RequireMusicApproval { get; set; }
[Obsolete("Use the user management settings")]
public bool UsersCanViewOnlyOwnRequests { get; set; }
[Obsolete("Use the user management settings")]
public bool UsersCanViewOnlyOwnIssues { get; set; }
public int MovieWeeklyRequestLimit { get; set; }
public int TvWeeklyRequestLimit { get; set; }

@ -42,5 +42,8 @@ namespace PlexRequests.Core.SettingModels
[Obsolete("We use the CRON job now")]
public int RecentlyAdded { get; set; }
public string RecentlyAddedCron { get; set; }
public int FaultQueueHandler { get; set; }
public int PlexContentCacher { get; set; }
public int PlexUserChecker { get; set; }
}
}

@ -0,0 +1,62 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: SystemSettings.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using PlexRequests.Core.Models;
namespace PlexRequests.Core.SettingModels
{
public class SystemSettings : Settings
{
public Branches Branch { get; set; }
public StatusModel Status { get; set; }
public List<BranchDropdown> BranchDropdown { get; set; }
}
public class BranchDropdown
{
public bool Selected { get; set; }
public string Name { get; set; }
public Branches Value { get; set; }
}
public enum Branches
{
[Display(Name = "Stable")]
Stable,
[Display(Name = "Early Access Preview")]
EarlyAccessPreview,
[Display(Name = "Development")]
Dev,
}
}

@ -0,0 +1,45 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: UserManagementSettings.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
namespace PlexRequests.Core.SettingModels
{
public class UserManagementSettings : Settings
{
public bool RequestMovies { get; set; }
public bool RequestTvShows { get; set; }
public bool RequestMusic { get; set; }
public bool AutoApproveMovies { get; set; }
public bool AutoApproveTvShows { get; set; }
public bool AutoApproveMusic { get; set; }
public bool ReportIssues { get; set; }
public bool UsersCanViewOnlyOwnRequests { get; set; }
public bool UsersCanViewOnlyOwnIssues { get; set; }
// Features
public bool RecentlyAddedNotification { get; set; }
public bool RecentlyAddedNewsletter { get; set; }
}
}

@ -61,65 +61,35 @@ namespace PlexRequests.Core
TableCreation.Vacuum(Db.DbConnection());
}
// The below code is obsolete, we should use PlexRequests.Core.Migrations.MigrationRunner
var version = CheckSchema();
if (version > 0)
{
if (version > 1899 && version <= 1900)
{
MigrateToVersion1900();
}
if(version > 1899 && version <= 1910)
{
MigrateToVersion1910();
}
}
// Add the new 'running' item into the scheduled jobs so we can check if the cachers are running
Db.DbConnection().AlterTable("ScheduledJobs", "ADD", "Running", true, "INTEGER");
return Db.DbConnection().ConnectionString;
}
public static string ConnectionString => Db.DbConnection().ConnectionString;
private int CheckSchema()
private void CreateDefaultSettingsPage(string baseUrl)
{
var productVersion = AssemblyHelper.GetProductVersion();
var trimStatus = new Regex("[^0-9]", RegexOptions.Compiled).Replace(productVersion, string.Empty).PadRight(4, '0');
var version = int.Parse(trimStatus);
var connection = Db.DbConnection();
var schema = connection.GetSchemaVersion();
if (schema == null)
var defaultUserSettings = new UserManagementSettings
{
connection.CreateSchema(version); // Set the default.
schema = connection.GetSchemaVersion();
}
if (version > schema.SchemaVersion)
{
Db.DbConnection().UpdateSchemaVersion(version);
schema = connection.GetSchemaVersion();
}
version = schema.SchemaVersion;
return version;
}
RequestMovies = true,
RequestTvShows = true,
ReportIssues = true,
private void CreateDefaultSettingsPage(string baseUrl)
{
};
var defaultSettings = new PlexRequestSettings
{
RequireTvShowApproval = true,
RequireMovieApproval = true,
SearchForMovies = true,
SearchForTvShows = true,
BaseUrl = baseUrl ?? string.Empty,
CollectAnalyticData = true,
};
var s = new SettingsServiceV2<PlexRequestSettings>(new SettingsJsonRepository(new DbConfiguration(new SqliteFactory()), new MemoryCacheProvider()));
var ctor = new SettingsJsonRepository(new DbConfiguration(new SqliteFactory()), new MemoryCacheProvider());
var s = new SettingsServiceV2<PlexRequestSettings>(ctor);
s.SaveSettings(defaultSettings);
var userSettings = new SettingsServiceV2<UserManagementSettings>(ctor);
userSettings.SaveSettings(defaultUserSettings);
var cron = (Quartz.Impl.Triggers.CronTriggerImpl)CronScheduleBuilder.WeeklyOnDayAndHourAndMinute(DayOfWeek.Friday, 7, 0).Build();
var scheduled = new ScheduledJobsSettings
@ -148,7 +118,6 @@ namespace PlexRequests.Core
Task.Run(() => { CacheSonarrQualityProfiles(mc); });
Task.Run(() => { CacheCouchPotatoQualityProfiles(mc); });
// we don't need to cache sickrage profiles, those are static
// TODO: cache headphones profiles?
}
catch (Exception)
{
@ -156,12 +125,12 @@ namespace PlexRequests.Core
}
}
private void CacheSonarrQualityProfiles(MemoryCacheProvider cacheProvider)
private void CacheSonarrQualityProfiles(ICacheProvider cacheProvider)
{
try
{
Log.Info("Executing GetSettings call to Sonarr for quality profiles");
var sonarrSettingsService = new SettingsServiceV2<SonarrSettings>(new SettingsJsonRepository(new DbConfiguration(new SqliteFactory()), new MemoryCacheProvider()));
var sonarrSettingsService = new SettingsServiceV2<SonarrSettings>(new SettingsJsonRepository(new DbConfiguration(new SqliteFactory()), cacheProvider));
var sonarrSettings = sonarrSettingsService.GetSettings();
if (sonarrSettings.Enabled)
{
@ -178,12 +147,12 @@ namespace PlexRequests.Core
}
}
private void CacheCouchPotatoQualityProfiles(MemoryCacheProvider cacheProvider)
private void CacheCouchPotatoQualityProfiles(ICacheProvider cacheProvider)
{
try
{
Log.Info("Executing GetSettings call to CouchPotato for quality profiles");
var cpSettingsService = new SettingsServiceV2<CouchPotatoSettings>(new SettingsJsonRepository(new DbConfiguration(new SqliteFactory()), new MemoryCacheProvider()));
var cpSettingsService = new SettingsServiceV2<CouchPotatoSettings>(new SettingsJsonRepository(new DbConfiguration(new SqliteFactory()), cacheProvider));
var cpSettings = cpSettingsService.GetSettings();
if (cpSettings.Enabled)
{
@ -199,102 +168,5 @@ namespace PlexRequests.Core
Log.Error(ex, "Failed to cache CouchPotato quality profiles!");
}
}
/// <summary>
/// Migrates to version 1.9.
/// Move the Plex auth token to the new field.
/// Reconfigure the log level
/// Set the wizard flag to true if we already have settings
/// </summary>
public void MigrateToVersion1900()
{
// Need to change the Plex Token location
var authSettings = new SettingsServiceV2<AuthenticationSettings>(new SettingsJsonRepository(Db, new MemoryCacheProvider()));
var auth = authSettings.GetSettings();
var plexSettings = new SettingsServiceV2<PlexSettings>(new SettingsJsonRepository(Db, new MemoryCacheProvider()));
if (auth != null)
{
//If we have an authToken we do not need to go through the setup
if (!string.IsNullOrEmpty(auth.OldPlexAuthToken))
{
var prServuce = new SettingsServiceV2<PlexRequestSettings>(new SettingsJsonRepository(Db, new MemoryCacheProvider()));
var settings = prServuce.GetSettings();
settings.Wizard = true;
prServuce.SaveSettings(settings);
}
// Clear out the old token and save it to the new field
var currentSettings = plexSettings.GetSettings();
if (!string.IsNullOrEmpty(auth.OldPlexAuthToken))
{
currentSettings.PlexAuthToken = auth.OldPlexAuthToken;
plexSettings.SaveSettings(currentSettings);
// Clear out the old value
auth.OldPlexAuthToken = string.Empty;
authSettings.SaveSettings(auth);
}
}
// Set the log level
try
{
var settingsService = new SettingsServiceV2<LogSettings>(new SettingsJsonRepository(Db, new MemoryCacheProvider()));
var logSettings = settingsService.GetSettings();
logSettings.Level = LogLevel.Error.Ordinal;
settingsService.SaveSettings(logSettings);
LoggingHelper.ReconfigureLogLevel(LogLevel.FromOrdinal(logSettings.Level));
}
catch (Exception e)
{
Log.Error(e);
}
// Enable analytics;
try
{
var prSettings = new SettingsServiceV2<PlexRequestSettings>(new SettingsJsonRepository(Db, new MemoryCacheProvider()));
var settings = prSettings.GetSettings();
settings.CollectAnalyticData = true;
var updated = prSettings.SaveSettings(settings);
}
catch (Exception e)
{
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);
}
}
}
}

@ -1,83 +0,0 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: StatusChecker.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using System.Linq;
using System.Threading.Tasks;
using Octokit;
using PlexRequests.Core.Models;
using PlexRequests.Helpers;
namespace PlexRequests.Core
{
public class StatusChecker
{
public StatusChecker()
{
Git = new GitHubClient(new ProductHeaderValue("PlexRequests-StatusChecker"));
}
private IGitHubClient Git { get; }
private const string Owner = "tidusjar";
private const string RepoName = "PlexRequests.Net";
public async Task<Release> GetLatestRelease()
{
var releases = await Git.Repository.Release.GetAll(Owner, RepoName);
return releases.FirstOrDefault();
}
public async Task<StatusModel> GetStatus()
{
var assemblyVersion = AssemblyHelper.GetProductVersion();
var model = new StatusModel
{
Version = assemblyVersion,
};
var latestRelease = await GetLatestRelease();
if (latestRelease == null)
{
return new StatusModel { Version = "Unknown" };
}
var latestVersionArray = latestRelease.Name.Split(new[] { 'v' }, StringSplitOptions.RemoveEmptyEntries);
var latestVersion = latestVersionArray.Length > 1 ? latestVersionArray[1] : string.Empty;
if (!latestVersion.Equals(assemblyVersion, StringComparison.InvariantCultureIgnoreCase))
{
model.UpdateAvailable = true;
model.UpdateUri = latestRelease.HtmlUrl;
}
model.ReleaseNotes = latestRelease.Body;
model.DownloadUri = latestRelease.Assets[0].BrowserDownloadUrl;
model.ReleaseTitle = latestRelease.Name;
return model;
}
}
}

@ -0,0 +1,35 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: AppveyorArtifactResult.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
namespace PlexRequests.Core.StatusChecker
{
public class AppveyorArtifactResult
{
public string fileName { get; set; }
public string type { get; set; }
public int size { get; set; }
}
}

@ -0,0 +1,138 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: AppveyorBranchResult.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Collections.Generic;
namespace PlexRequests.Core.StatusChecker
{
public class NuGetFeed
{
public string id { get; set; }
public string name { get; set; }
public bool publishingEnabled { get; set; }
public string created { get; set; }
}
public class AccessRightDefinition
{
public string name { get; set; }
public string description { get; set; }
}
public class AccessRight
{
public string name { get; set; }
public bool allowed { get; set; }
}
public class RoleAce
{
public int roleId { get; set; }
public string name { get; set; }
public bool isAdmin { get; set; }
public List<AccessRight> accessRights { get; set; }
}
public class SecurityDescriptor
{
public List<AccessRightDefinition> accessRightDefinitions { get; set; }
public List<RoleAce> roleAces { get; set; }
}
public class Project
{
public int projectId { get; set; }
public int accountId { get; set; }
public string accountName { get; set; }
public List<object> builds { get; set; }
public string name { get; set; }
public string slug { get; set; }
public string repositoryType { get; set; }
public string repositoryScm { get; set; }
public string repositoryName { get; set; }
public string repositoryBranch { get; set; }
public bool isPrivate { get; set; }
public bool skipBranchesWithoutAppveyorYml { get; set; }
public bool enableSecureVariablesInPullRequests { get; set; }
public bool enableSecureVariablesInPullRequestsFromSameRepo { get; set; }
public bool enableDeploymentInPullRequests { get; set; }
public bool rollingBuilds { get; set; }
public bool alwaysBuildClosedPullRequests { get; set; }
public string tags { get; set; }
public NuGetFeed nuGetFeed { get; set; }
public SecurityDescriptor securityDescriptor { get; set; }
public string created { get; set; }
public string updated { get; set; }
}
public class Job
{
public string jobId { get; set; }
public string name { get; set; }
public bool allowFailure { get; set; }
public int messagesCount { get; set; }
public int compilationMessagesCount { get; set; }
public int compilationErrorsCount { get; set; }
public int compilationWarningsCount { get; set; }
public int testsCount { get; set; }
public int passedTestsCount { get; set; }
public int failedTestsCount { get; set; }
public int artifactsCount { get; set; }
public string status { get; set; }
public string started { get; set; }
public string finished { get; set; }
public string created { get; set; }
public string updated { get; set; }
}
public class Build
{
public int buildId { get; set; }
public List<Job> jobs { get; set; }
public int buildNumber { get; set; }
public string version { get; set; }
public string message { get; set; }
public string branch { get; set; }
public bool isTag { get; set; }
public string commitId { get; set; }
public string authorName { get; set; }
public string committerName { get; set; }
public string committed { get; set; }
public List<object> messages { get; set; }
public string status { get; set; }
public string started { get; set; }
public string finished { get; set; }
public string created { get; set; }
public string updated { get; set; }
}
public class AppveyorBranchResult
{
public Project project { get; set; }
public Build build { get; set; }
}
}

@ -0,0 +1,174 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: StatusChecker.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Octokit;
using PlexRequests.Api;
using PlexRequests.Core.Models;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using RestSharp;
namespace PlexRequests.Core.StatusChecker
{
public class StatusChecker : IStatusChecker
{
public StatusChecker(ISettingsService<SystemSettings> ss)
{
SystemSettings = ss;
Git = new GitHubClient(new ProductHeaderValue("PlexRequests-StatusChecker"));
}
private ISettingsService<SystemSettings> SystemSettings { get; }
private IGitHubClient Git { get; }
private const string Owner = "tidusjar";
private const string RepoName = "PlexRequests.Net";
private const string AppveyorApiUrl = "https://ci.appveyor.com/api";
private const string Api =
"48Ku58C0794nBrXra8IxWav+dc6NqgkRw+PZB3/bQwbt/D0IrnJQkgtjzo0bd6nkooLMKsC8M+Ab7jyBO+ROjY14VRuxffpDopX9r0iG/fjBl6mZVvqkm+VTDNstDtzp";
public async Task<StatusModel> GetStatus()
{
var settings = await SystemSettings.GetSettingsAsync();
var stable = settings.Branch == Branches.Stable;
if (!stable)
{
// Early Access Preview Releases
return GetAppveyorRelease(settings.Branch);
}
// Stable releases
return await GetLatestGithubRelease();
}
private async Task<StatusModel> GetLatestGithubRelease()
{
var assemblyVersion = AssemblyHelper.GetProductVersion();
var model = new StatusModel
{
CurrentVersion = assemblyVersion,
};
var releases = await Git.Repository.Release.GetAll(Owner, RepoName);
var latestRelease = releases.FirstOrDefault();
if (latestRelease == null)
{
return new StatusModel { NewVersion = "Unknown" };
}
var latestVersionArray = latestRelease.Name.Split(new[] { 'v' }, StringSplitOptions.RemoveEmptyEntries);
var latestVersion = latestVersionArray.Length > 1 ? latestVersionArray[1] : string.Empty;
if (!latestVersion.Equals(assemblyVersion, StringComparison.InvariantCultureIgnoreCase))
{
model.UpdateAvailable = true;
model.UpdateUri = latestRelease.HtmlUrl;
model.NewVersion = latestVersion;
}
model.ReleaseNotes = latestRelease.Body;
model.DownloadUri = latestRelease.Assets[0].BrowserDownloadUrl;
model.ReleaseTitle = latestRelease.Name;
return model;
}
private StatusModel GetAppveyorRelease(Branches branch)
{
var request = new ApiRequest();
// Get latest EAP Build
var eapBranchRequest = new RestRequest
{
Method = Method.GET
};
switch (branch)
{
case Branches.Dev:
eapBranchRequest.Resource = "/projects/tidusjar/requestplex/branch/dev";
break;
case Branches.EarlyAccessPreview:
eapBranchRequest.Resource = "/projects/tidusjar/requestplex/branch/EAP";
break;
}
var api = StringCipher.Decrypt(Api,"Appveyor");
eapBranchRequest.AddHeader("Authorization", $"Bearer {api}");
eapBranchRequest.AddHeader("Content-Type", "application/json");
var branchResult = request.ExecuteJson<AppveyorBranchResult>(eapBranchRequest, new Uri(AppveyorApiUrl));
var jobId = branchResult.build.jobs.FirstOrDefault()?.jobId ?? string.Empty;
if (string.IsNullOrEmpty(jobId))
{
return new StatusModel {UpdateAvailable = false};
}
// Get artifacts from the EAP Build
var eapAtrifactRequest = new RestRequest
{
Resource = $"/buildjobs/{jobId}/artifacts",
Method = Method.GET
};
eapAtrifactRequest.AddHeader("Authorization", $"Bearer {api}");
eapAtrifactRequest.AddHeader("Content-Type", "application/json");
var artifactResult = request.ExecuteJson<List<AppveyorArtifactResult>>(eapAtrifactRequest, new Uri(AppveyorApiUrl)).FirstOrDefault();
var downloadLink = $"{AppveyorApiUrl}/buildjobs/{jobId}/artifacts/{artifactResult.fileName}";
var branchDisplay = EnumHelper<Branches>.GetDisplayValue(branch);
var fileversion = AssemblyHelper.GetFileVersion();
var model = new StatusModel
{
DownloadUri = downloadLink,
ReleaseNotes = $"{branchDisplay} (See recent commits for details)",
ReleaseTitle = $"Plex Requests {branchDisplay}",
NewVersion = branchResult.build.version,
UpdateUri = downloadLink,
CurrentVersion = fileversion
};
if (!fileversion.Equals(branchResult.build.version, StringComparison.CurrentCultureIgnoreCase))
{
model.UpdateAvailable = true;
}
return model;
}
}
}

@ -24,18 +24,19 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NLog;
using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.SickRage;
using PlexRequests.Api.Models.Sonarr;
using PlexRequests.Core.SettingModels;
using PlexRequests.Store;
using System.Linq;
using System.Threading.Tasks;
namespace PlexRequests.UI.Helpers
namespace PlexRequests.Core
{
public class TvSender
{

@ -36,7 +36,7 @@ using PlexRequests.Api.Models.Sonarr;
using PlexRequests.Core.SettingModels;
using PlexRequests.Store;
namespace PlexRequests.UI.Helpers
namespace PlexRequests.Core
{
public class TvSenderOld
{

@ -145,26 +145,6 @@ namespace PlexRequests.Core
Repo.Delete(user);
}
public Guid? CreateAdmin(string username, string password, UserProperties properties = null)
{
return CreateUser(username, password, properties);
}
public Guid? CreatePowerUser(string username, string password, UserProperties properties = null)
{
return CreateUser(username, password, properties);
}
public Guid? CreateRegularUser(string username, string password, UserProperties properties = null)
{
return CreateUser(username, password, properties);
}
public IEnumerable<string> GetAllClaims()
{
var properties = typeof(UserClaims).GetConstantsValues<string>();
return properties;
}
public bool UpdatePassword(string username, string oldPassword, string newPassword)
{
@ -207,11 +187,8 @@ namespace PlexRequests.Core
public interface ICustomUserMapper
{
Guid? CreateUser(string username, string password, UserProperties props);
Guid? CreateUser(string username, string password, int permissions, int features,
UserProperties properties = null);
IEnumerable<string> GetAllClaims();
IEnumerable<UsersModel> GetUsers();
Task<IEnumerable<UsersModel>> GetUsersAsync();
UsersModel GetUser(Guid userId);
@ -219,9 +196,6 @@ namespace PlexRequests.Core
bool DoUsersExist();
Guid? ValidateUser(string username, string password);
bool UpdatePassword(string username, string oldPassword, string newPassword);
Guid? CreateAdmin(string username, string password, UserProperties properties = null);
Guid? CreatePowerUser(string username, string password, UserProperties properties = null);
Guid? CreateRegularUser(string username, string password, UserProperties properties = null);
void DeleteUser(string userId);
}
}

@ -0,0 +1,12 @@
using System.Collections.Generic;
using PlexRequests.Helpers.Permissions;
namespace PlexRequests.Core.Users
{
public interface IUserHelper
{
IEnumerable<UserHelperModel> GetUsers();
IEnumerable<UserHelperModel> GetUsersWithPermission(Permissions permission);
IEnumerable<UserHelperModel> GetUsersWithFeature(Features feature);
}
}

@ -0,0 +1,159 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: UserHelper.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Collections.Generic;
using System.Linq;
using PlexRequests.Core.Models;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Permissions;
using PlexRequests.Store.Repository;
namespace PlexRequests.Core.Users
{
public class UserHelper : IUserHelper
{
public UserHelper(IUserRepository userRepository, IPlexUserRepository plexUsers, ISecurityExtensions security)
{
LocalUserRepository = userRepository;
PlexUserRepository = plexUsers;
Security = security;
}
private IUserRepository LocalUserRepository { get; }
private IPlexUserRepository PlexUserRepository { get; }
private ISecurityExtensions Security { get; }
public IEnumerable<UserHelperModel> GetUsers()
{
var model = new List<UserHelperModel>();
var localUsers = LocalUserRepository.GetAll();
var plexUsers = PlexUserRepository.GetAll();
foreach (var user in localUsers)
{
var props = ByteConverterHelper.ReturnObject<UserProperties>(user.UserProperties);
model.Add(new UserHelperModel
{
Type = UserType.LocalUser,
Username = user.UserName,
UserAlias = props.UserAlias,
EmailAddress = props.EmailAddress,
Permissions = (Permissions)user.Permissions
});
}
model.AddRange(plexUsers.Select(user => new UserHelperModel
{
Type = UserType.LocalUser,
Username = user.Username,
UserAlias = user.UserAlias,
EmailAddress = user.EmailAddress,
Permissions = (Permissions)user.Permissions
}));
return model;
}
public IEnumerable<UserHelperModel> GetUsersWithPermission(Permissions permission)
{
var model = new List<UserHelperModel>();
var localUsers = LocalUserRepository.GetAll().ToList();
var plexUsers = PlexUserRepository.GetAll().ToList();
var filteredLocal = localUsers.Where(x => ((Permissions)x.Permissions).HasFlag(permission));
var filteredPlex = plexUsers.Where(x => ((Permissions)x.Permissions).HasFlag(permission));
foreach (var user in filteredLocal)
{
var props = ByteConverterHelper.ReturnObject<UserProperties>(user.UserProperties);
model.Add(new UserHelperModel
{
Type = UserType.LocalUser,
Username = user.UserName,
UserAlias = props.UserAlias,
EmailAddress = props.EmailAddress,
Permissions = (Permissions)user.Permissions,
Features = (Features)user.Features
});
}
model.AddRange(filteredPlex.Select(user => new UserHelperModel
{
Type = UserType.LocalUser,
Username = user.Username,
UserAlias = user.UserAlias,
EmailAddress = user.EmailAddress,
Permissions = (Permissions)user.Permissions,
Features = (Features)user.Features
}));
return model;
}
public IEnumerable<UserHelperModel> GetUsersWithFeature(Features features)
{
var model = new List<UserHelperModel>();
var localUsers = LocalUserRepository.GetAll().ToList();
var plexUsers = PlexUserRepository.GetAll().ToList();
var filteredLocal = localUsers.Where(x => ((Features)x.Features).HasFlag(features));
var filteredPlex = plexUsers.Where(x => ((Features)x.Features).HasFlag(features));
foreach (var user in filteredLocal)
{
var props = ByteConverterHelper.ReturnObject<UserProperties>(user.UserProperties);
model.Add(new UserHelperModel
{
Type = UserType.LocalUser,
Username = user.UserName,
UserAlias = props.UserAlias,
EmailAddress = props.EmailAddress,
Permissions = (Permissions)user.Permissions,
Features = (Features)user.Features
});
}
model.AddRange(filteredPlex.Select(user => new UserHelperModel
{
Type = UserType.LocalUser,
Username = user.Username,
UserAlias = user.UserAlias,
EmailAddress = user.EmailAddress,
Permissions = (Permissions)user.Permissions,
Features = (Features)user.Features
}));
return model;
}
}
}

@ -1,7 +1,7 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: AuthenticationSettingsTests.cs
// File: UserHelperModel.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
@ -25,22 +25,18 @@
// ************************************************************************/
#endregion
using System;
using NUnit.Framework;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Permissions;
namespace PlexRequests.Core.Tests
namespace PlexRequests.Core.Users
{
[TestFixture]
public class StatusCheckerTests
public class UserHelperModel
{
[Test]
[Ignore("API Limit")]
public void CheckStatusTest()
{
var checker = new StatusChecker();
var status = checker.GetStatus();
Assert.That(status, Is.Not.Null);
}
public string Username { get; set; }
public string UserAlias { get; set; }
public Permissions Permissions { get; set; }
public Features Features { get; set; }
public string EmailAddress { get; set; }
public UserType Type { get; set; }
}
}

@ -0,0 +1,70 @@
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers.Permissions;
namespace PlexRequests.Core.Users
{
public static class UserManagementHelper
{
public static int GetPermissions(UserManagementSettings settings)
{
var permission = 0;
if (settings.AutoApproveMovies)
{
permission += (int)Permissions.AutoApproveMovie;
}
if (settings.AutoApproveMusic)
{
permission += (int)Permissions.AutoApproveAlbum;
}
if (settings.AutoApproveTvShows)
{
permission += (int)Permissions.AutoApproveTv;
}
if (settings.RequestMovies)
{
permission += (int)Permissions.RequestMovie;
}
if (settings.RequestMusic)
{
permission += (int)Permissions.RequestMusic;
}
if (settings.RequestTvShows)
{
permission += (int)Permissions.RequestTvShow;
}
if (settings.ReportIssues)
{
permission += (int)Permissions.ReportIssue;
}
if (settings.UsersCanViewOnlyOwnRequests)
{
permission += (int)Permissions.UsersCanViewOnlyOwnRequests;
}
if (settings.UsersCanViewOnlyOwnIssues)
{
permission += (int)Permissions.UsersCanViewOnlyOwnIssues;
}
return permission;
}
public static int GetFeatures(UserManagementSettings settings)
{
var features = 0;
if (settings.RecentlyAddedNewsletter)
{
features += (int)Features.Newsletter;
}
if (settings.RecentlyAddedNotification)
{
features += (int)Features.RequestAddedNotification;
}
return features;
}
}
}

@ -2,11 +2,14 @@
<packages>
<package id="Common.Logging" version="3.0.0" targetFramework="net45" />
<package id="Common.Logging.Core" version="3.0.0" targetFramework="net45" />
<package id="Dapper" version="1.50.0-beta8" targetFramework="net45" />
<package id="Nancy" version="1.4.3" targetFramework="net45" />
<package id="Nancy.Authentication.Forms" version="1.4.1" targetFramework="net45" />
<package id="Nancy.Linker" version="0.3.1" targetFramework="net45" />
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="net45" />
<package id="NLog" version="4.3.6" targetFramework="net45" />
<package id="Octokit" version="0.19.0" targetFramework="net45" />
<package id="Quartz" version="2.3.3" targetFramework="net45" />
<package id="RestSharp" version="105.2.3" targetFramework="net45" />
<package id="valueinjecter" version="3.1.1.2" targetFramework="net45" />
</packages>

@ -45,5 +45,13 @@ namespace PlexRequests.Helpers
var retVersion = fvi.ProductVersion;
return retVersion;
}
public static string GetFileVersion()
{
var assembly = Assembly.GetExecutingAssembly();
var fvi = FileVersionInfo.GetVersionInfo(assembly.Location);
var retVersion = fvi.FileVersion;
return retVersion;
}
}
}

@ -108,5 +108,10 @@ namespace PlexRequests.Helpers
throw new ArgumentOutOfRangeException(nameof(name));
}
public static int All()
{
return Enum.GetValues(typeof(T)).Cast<int>().Sum();
}
}
}

@ -35,8 +35,8 @@ namespace PlexRequests.Helpers.Permissions
{
[Display(Name = "Newsletter")]
Newsletter = 1,
[Display(Name = "Recently Added Notification")]
RecentlyAddedNotification = 2,
[Display(Name = "Request Added Notification")]
RequestAddedNotification = 2,
}
}

@ -31,6 +31,9 @@ using System.ComponentModel.DataAnnotations;
namespace PlexRequests.Helpers.Permissions
{
[Flags]
////
//// NOTE if any are added, make sure we change the UserManagementHelper
////
public enum Permissions
{
[Display(Name = "Access Administration Settings")]
@ -50,5 +53,23 @@ namespace PlexRequests.Helpers.Permissions
[Display(Name = "Read Only User")]
ReadOnlyUser = 32,
[Display(Name = "Auto Approve Movie Requests")]
AutoApproveMovie = 64,
[Display(Name = "Auto Approve TV Show Requests")]
AutoApproveTv = 128,
[Display(Name = "Auto Approve Album Requests")]
AutoApproveAlbum = 256,
[Display(Name = "Manage Requests")]
ManageRequests = 512,
[Display(Name = "Users can only view their own requests")]
UsersCanViewOnlyOwnRequests = 1024,
[Display(Name = "Users can only view their own issues")]
UsersCanViewOnlyOwnIssues = 2048
}
}

@ -90,6 +90,7 @@
<Compile Include="PlexHelper.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SerializerSettings.cs" />
<Compile Include="SessionKeys.cs" />
<Compile Include="StringCipher.cs" />
<Compile Include="StringHasher.cs" />
<Compile Include="StringHelper.cs" />

@ -24,7 +24,7 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
namespace PlexRequests.UI.Models
namespace PlexRequests.Helpers
{
public class SessionKeys
{

@ -1,284 +1,284 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: PlexAvailabilityCheckerTests.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using Moq;
using NUnit.Framework;
using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.Plex;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Services.Interfaces;
using PlexRequests.Helpers;
using PlexRequests.Services.Jobs;
using PlexRequests.Services.Models;
using PlexRequests.Services.Notification;
using PlexRequests.Store.Models;
using PlexRequests.Store.Repository;
using Ploeh.AutoFixture;
namespace PlexRequests.Services.Tests
{
[TestFixture]
public class PlexAvailabilityCheckerTests
{
public IAvailabilityChecker Checker { get; set; }
private Fixture F { get; set; } = new Fixture();
private Mock<ISettingsService<PlexSettings>> SettingsMock { get; set; }
private Mock<ISettingsService<AuthenticationSettings>> AuthMock { get; set; }
private Mock<IRequestService> RequestMock { get; set; }
private Mock<IPlexApi> PlexMock { get; set; }
private Mock<ICacheProvider> CacheMock { get; set; }
private Mock<INotificationService> NotificationMock { get; set; }
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()
{
SettingsMock = new Mock<ISettingsService<PlexSettings>>();
AuthMock = new Mock<ISettingsService<AuthenticationSettings>>();
RequestMock = new Mock<IRequestService>();
PlexMock = new Mock<IPlexApi>();
NotificationMock = new Mock<INotificationService>();
CacheMock = new Mock<ICacheProvider>();
NotifyUsers = new Mock<IRepository<UsersToNotify>>();
PlexEpisodes = new Mock<IRepository<PlexEpisodes>>();
JobRec = new Mock<IJobRecord>();
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);
}
[Test]
public void InvalidSettings()
{
Checker.CheckAndUpdateAll();
PlexMock.Verify(x => x.GetLibrary(It.IsAny<string>(), It.IsAny<Uri>(), It.IsAny<string>()), Times.Never);
PlexMock.Verify(x => x.GetAccount(It.IsAny<string>()), Times.Never);
PlexMock.Verify(x => x.GetLibrarySections(It.IsAny<string>(), It.IsAny<Uri>()), Times.Never);
PlexMock.Verify(x => x.GetStatus(It.IsAny<string>(), It.IsAny<Uri>()), Times.Never);
PlexMock.Verify(x => x.GetUsers(It.IsAny<string>()), Times.Never);
}
[TestCaseSource(nameof(IsMovieAvailableTestData))]
public bool IsMovieAvailableTest(string title, string year)
{
var movies = new List<PlexMovie>
{
new PlexMovie {Title = title, ProviderId = null, ReleaseYear = year}
};
var result = Checker.IsMovieAvailable(movies.ToArray(), "title", "2011");
return result;
}
private static IEnumerable<TestCaseData> IsMovieAvailableTestData
{
get
{
yield return new TestCaseData("title", "2011").Returns(true).SetName("IsMovieAvailable True");
yield return new TestCaseData("title2", "2011").Returns(false).SetName("IsMovieAvailable False different title");
yield return new TestCaseData("title", "2001").Returns(false).SetName("IsMovieAvailable False different year");
}
}
[TestCaseSource(nameof(IsMovieAvailableAdvancedTestData))]
public bool IsMovieAvailableAdvancedTest(string title, string year, string providerId)
{
var movies = new List<PlexMovie>
{
new PlexMovie {Title = title, ProviderId = providerId, ReleaseYear = year }
};
var result = Checker.IsMovieAvailable(movies.ToArray(), "title", "2011", 9999.ToString());
return result;
}
private static IEnumerable<TestCaseData> IsMovieAvailableAdvancedTestData
{
get
{
yield return new TestCaseData("title", "2011", "9999").Returns(true).SetName("Advanced IsMovieAvailable True");
yield return new TestCaseData("title2", "2011", "99929").Returns(false).SetName("Advanced IsMovieAvailable False different title");
yield return new TestCaseData("title", "2001", "99939").Returns(false).SetName("Advanced IsMovieAvailable False different year");
yield return new TestCaseData("title", "2001", "44445").Returns(false).SetName("Advanced IsMovieAvailable False different providerID");
}
}
[TestCaseSource(nameof(IsTvAvailableTestData))]
public bool IsTvAvailableTest(string title, string year)
{
var tv = new List<PlexTvShow>
{
new PlexTvShow {Title = title, ProviderId = null, ReleaseYear = year}
};
var result = Checker.IsTvShowAvailable(tv.ToArray(), "title", "2011");
return result;
}
private static IEnumerable<TestCaseData> IsTvAvailableTestData
{
get
{
yield return new TestCaseData("title", "2011").Returns(true).SetName("IsTvAvailable True");
yield return new TestCaseData("title2", "2011").Returns(false).SetName("IsTvAvailable False different title");
yield return new TestCaseData("title", "2001").Returns(false).SetName("IsTvAvailable False different year");
}
}
[TestCaseSource(nameof(IsTvAvailableAdvancedTestData))]
public bool IsTvAvailableAdvancedTest(string title, string year, string providerId)
{
var movies = new List<PlexTvShow>
{
new PlexTvShow {Title = title, ProviderId = providerId, ReleaseYear = year }
};
var result = Checker.IsTvShowAvailable(movies.ToArray(), "title", "2011", 9999.ToString());
return result;
}
private static IEnumerable<TestCaseData> IsTvAvailableAdvancedTestData
{
get
{
yield return new TestCaseData("title", "2011", "9999").Returns(true).SetName("Advanced IsTvAvailable True");
yield return new TestCaseData("title2", "2011", "99929").Returns(false).SetName("Advanced IsTvAvailable False different title");
yield return new TestCaseData("title", "2001", "99939").Returns(false).SetName("Advanced IsTvAvailable False different year");
yield return new TestCaseData("title", "2001", "44445").Returns(false).SetName("Advanced IsTvAvailable False different providerID");
}
}
[TestCaseSource(nameof(IsTvAvailableAdvancedSeasonsTestData))]
public bool IsTvAvailableAdvancedSeasonsTest(string title, string year, string providerId, int[] seasons)
{
var movies = new List<PlexTvShow>
{
new PlexTvShow {Title = title, ProviderId = providerId, ReleaseYear = year , Seasons = seasons}
};
var result = Checker.IsTvShowAvailable(movies.ToArray(), "title", "2011", 9999.ToString(), new[] { 1, 2, 3 });
return result;
}
private static IEnumerable<TestCaseData> IsTvAvailableAdvancedSeasonsTestData
{
get
{
yield return new TestCaseData("title", "2011", "9999", new[] { 1, 2, 3 }).Returns(true).SetName("Advanced IsTvSeasonsAvailable True");
yield return new TestCaseData("title2", "2011", "99929", new[] { 5, 6 }).Returns(false).SetName("Advanced IsTvSeasonsAvailable False no seasons");
yield return new TestCaseData("title2", "2011", "9999", new[] { 1, 6 }).Returns(true).SetName("Advanced IsTvSeasonsAvailable true one season");
}
}
[TestCaseSource(nameof(IsEpisodeAvailableTestData))]
public bool IsEpisodeAvailableTest(string providerId, int season, int episode)
{
var expected = new List<PlexEpisodes>
{
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);
var result = Checker.IsEpisodeAvailable(providerId, season, episode);
return result;
}
private static IEnumerable<TestCaseData> IsEpisodeAvailableTestData
{
get
{
yield return new TestCaseData("23", 1, 1).Returns(true).SetName("IsEpisodeAvailable True S01E01");
yield return new TestCaseData("23", 1, 2).Returns(false).SetName("IsEpisodeAvailable False S01E02");
yield return new TestCaseData("23", 99, 99).Returns(false).SetName("IsEpisodeAvailable False S99E99");
yield return new TestCaseData("230", 99, 99).Returns(false).SetName("IsEpisodeAvailable False Incorrect ProviderId");
}
}
[Test]
public void GetPlexMoviesTests()
{
var cachedMovies = F.Build<PlexSearch>().Without(x => x.Directory).CreateMany().ToList();
cachedMovies.Add(new PlexSearch
{
Video = new List<Video>
{
new Video {Type = "movie", Title = "title1", Year = "2016", ProviderId = "1212"}
}
});
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"));
}
[Test]
public void GetPlexTvShowsTests()
{
var cachedTv = F.Build<PlexSearch>().Without(x => x.Directory).CreateMany().ToList();
cachedTv.Add(new PlexSearch
{
Directory = new List<Directory1>
{
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();
Assert.That(movies.Any(x => x.ProviderId == "1212"));
}
[Test]
public async Task GetAllPlexEpisodes()
{
PlexEpisodes.Setup(x => x.GetAllAsync()).ReturnsAsync(F.CreateMany<PlexEpisodes>().ToList());
var episodes = await Checker.GetEpisodes();
Assert.That(episodes.Count(), Is.GreaterThan(0));
}
}
}
//#region Copyright
//// /************************************************************************
//// Copyright (c) 2016 Jamie Rees
//// File: PlexAvailabilityCheckerTests.cs
//// Created By: Jamie Rees
////
//// Permission is hereby granted, free of charge, to any person obtaining
//// a copy of this software and associated documentation files (the
//// "Software"), to deal in the Software without restriction, including
//// without limitation the rights to use, copy, modify, merge, publish,
//// distribute, sublicense, and/or sell copies of the Software, and to
//// permit persons to whom the Software is furnished to do so, subject to
//// the following conditions:
////
//// The above copyright notice and this permission notice shall be
//// included in all copies or substantial portions of the Software.
////
//// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
//// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
//// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
//// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
//// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
//// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
//// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//// ************************************************************************/
//#endregion
//using System;
//using System.Collections.Generic;
//using System.Data;
//using System.Linq;
//using System.Threading.Tasks;
//using Moq;
//using NUnit.Framework;
//using PlexRequests.Api.Interfaces;
//using PlexRequests.Api.Models.Plex;
//using PlexRequests.Core;
//using PlexRequests.Core.SettingModels;
//using PlexRequests.Services.Interfaces;
//using PlexRequests.Helpers;
//using PlexRequests.Services.Jobs;
//using PlexRequests.Services.Models;
//using PlexRequests.Services.Notification;
//using PlexRequests.Store.Models;
//using PlexRequests.Store.Repository;
//using Ploeh.AutoFixture;
//namespace PlexRequests.Services.Tests
//{
// [TestFixture]
// public class PlexAvailabilityCheckerTests
// {
// public IAvailabilityChecker Checker { get; set; }
// private Fixture F { get; set; } = new Fixture();
// private Mock<ISettingsService<PlexSettings>> SettingsMock { get; set; }
// private Mock<ISettingsService<AuthenticationSettings>> AuthMock { get; set; }
// private Mock<IRequestService> RequestMock { get; set; }
// private Mock<IPlexApi> PlexMock { get; set; }
// private Mock<ICacheProvider> CacheMock { get; set; }
// private Mock<INotificationService> NotificationMock { get; set; }
// 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()
// {
// SettingsMock = new Mock<ISettingsService<PlexSettings>>();
// AuthMock = new Mock<ISettingsService<AuthenticationSettings>>();
// RequestMock = new Mock<IRequestService>();
// PlexMock = new Mock<IPlexApi>();
// NotificationMock = new Mock<INotificationService>();
// CacheMock = new Mock<ICacheProvider>();
// NotifyUsers = new Mock<IRepository<UsersToNotify>>();
// PlexEpisodes = new Mock<IRepository<PlexEpisodes>>();
// JobRec = new Mock<IJobRecord>();
// 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);
// }
// [Test]
// public void InvalidSettings()
// {
// Checker.CheckAndUpdateAll();
// PlexMock.Verify(x => x.GetLibrary(It.IsAny<string>(), It.IsAny<Uri>(), It.IsAny<string>()), Times.Never);
// PlexMock.Verify(x => x.GetAccount(It.IsAny<string>()), Times.Never);
// PlexMock.Verify(x => x.GetLibrarySections(It.IsAny<string>(), It.IsAny<Uri>()), Times.Never);
// PlexMock.Verify(x => x.GetStatus(It.IsAny<string>(), It.IsAny<Uri>()), Times.Never);
// PlexMock.Verify(x => x.GetUsers(It.IsAny<string>()), Times.Never);
// }
// [TestCaseSource(nameof(IsMovieAvailableTestData))]
// public bool IsMovieAvailableTest(string title, string year)
// {
// var movies = new List<PlexMovie>
// {
// new PlexMovie {Title = title, ProviderId = null, ReleaseYear = year}
// };
// var result = Checker.IsMovieAvailable(movies.ToArray(), "title", "2011");
// return result;
// }
// private static IEnumerable<TestCaseData> IsMovieAvailableTestData
// {
// get
// {
// yield return new TestCaseData("title", "2011").Returns(true).SetName("IsMovieAvailable True");
// yield return new TestCaseData("title2", "2011").Returns(false).SetName("IsMovieAvailable False different title");
// yield return new TestCaseData("title", "2001").Returns(false).SetName("IsMovieAvailable False different year");
// }
// }
// [TestCaseSource(nameof(IsMovieAvailableAdvancedTestData))]
// public bool IsMovieAvailableAdvancedTest(string title, string year, string providerId)
// {
// var movies = new List<PlexMovie>
// {
// new PlexMovie {Title = title, ProviderId = providerId, ReleaseYear = year }
// };
// var result = Checker.IsMovieAvailable(movies.ToArray(), "title", "2011", 9999.ToString());
// return result;
// }
// private static IEnumerable<TestCaseData> IsMovieAvailableAdvancedTestData
// {
// get
// {
// yield return new TestCaseData("title", "2011", "9999").Returns(true).SetName("Advanced IsMovieAvailable True");
// yield return new TestCaseData("title2", "2011", "99929").Returns(false).SetName("Advanced IsMovieAvailable False different title");
// yield return new TestCaseData("title", "2001", "99939").Returns(false).SetName("Advanced IsMovieAvailable False different year");
// yield return new TestCaseData("title", "2001", "44445").Returns(false).SetName("Advanced IsMovieAvailable False different providerID");
// }
// }
// [TestCaseSource(nameof(IsTvAvailableTestData))]
// public bool IsTvAvailableTest(string title, string year)
// {
// var tv = new List<PlexTvShow>
// {
// new PlexTvShow {Title = title, ProviderId = null, ReleaseYear = year}
// };
// var result = Checker.IsTvShowAvailable(tv.ToArray(), "title", "2011");
// return result;
// }
// private static IEnumerable<TestCaseData> IsTvAvailableTestData
// {
// get
// {
// yield return new TestCaseData("title", "2011").Returns(true).SetName("IsTvAvailable True");
// yield return new TestCaseData("title2", "2011").Returns(false).SetName("IsTvAvailable False different title");
// yield return new TestCaseData("title", "2001").Returns(false).SetName("IsTvAvailable False different year");
// }
// }
// [TestCaseSource(nameof(IsTvAvailableAdvancedTestData))]
// public bool IsTvAvailableAdvancedTest(string title, string year, string providerId)
// {
// var movies = new List<PlexTvShow>
// {
// new PlexTvShow {Title = title, ProviderId = providerId, ReleaseYear = year }
// };
// var result = Checker.IsTvShowAvailable(movies.ToArray(), "title", "2011", 9999.ToString());
// return result;
// }
// private static IEnumerable<TestCaseData> IsTvAvailableAdvancedTestData
// {
// get
// {
// yield return new TestCaseData("title", "2011", "9999").Returns(true).SetName("Advanced IsTvAvailable True");
// yield return new TestCaseData("title2", "2011", "99929").Returns(false).SetName("Advanced IsTvAvailable False different title");
// yield return new TestCaseData("title", "2001", "99939").Returns(false).SetName("Advanced IsTvAvailable False different year");
// yield return new TestCaseData("title", "2001", "44445").Returns(false).SetName("Advanced IsTvAvailable False different providerID");
// }
// }
// [TestCaseSource(nameof(IsTvAvailableAdvancedSeasonsTestData))]
// public bool IsTvAvailableAdvancedSeasonsTest(string title, string year, string providerId, int[] seasons)
// {
// var movies = new List<PlexTvShow>
// {
// new PlexTvShow {Title = title, ProviderId = providerId, ReleaseYear = year , Seasons = seasons}
// };
// var result = Checker.IsTvShowAvailable(movies.ToArray(), "title", "2011", 9999.ToString(), new[] { 1, 2, 3 });
// return result;
// }
// private static IEnumerable<TestCaseData> IsTvAvailableAdvancedSeasonsTestData
// {
// get
// {
// yield return new TestCaseData("title", "2011", "9999", new[] { 1, 2, 3 }).Returns(true).SetName("Advanced IsTvSeasonsAvailable True");
// yield return new TestCaseData("title2", "2011", "99929", new[] { 5, 6 }).Returns(false).SetName("Advanced IsTvSeasonsAvailable False no seasons");
// yield return new TestCaseData("title2", "2011", "9999", new[] { 1, 6 }).Returns(true).SetName("Advanced IsTvSeasonsAvailable true one season");
// }
// }
// [TestCaseSource(nameof(IsEpisodeAvailableTestData))]
// public bool IsEpisodeAvailableTest(string providerId, int season, int episode)
// {
// var expected = new List<PlexEpisodes>
// {
// 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);
// var result = Checker.IsEpisodeAvailable(providerId, season, episode);
// return result;
// }
// private static IEnumerable<TestCaseData> IsEpisodeAvailableTestData
// {
// get
// {
// yield return new TestCaseData("23", 1, 1).Returns(true).SetName("IsEpisodeAvailable True S01E01");
// yield return new TestCaseData("23", 1, 2).Returns(false).SetName("IsEpisodeAvailable False S01E02");
// yield return new TestCaseData("23", 99, 99).Returns(false).SetName("IsEpisodeAvailable False S99E99");
// yield return new TestCaseData("230", 99, 99).Returns(false).SetName("IsEpisodeAvailable False Incorrect ProviderId");
// }
// }
// [Test]
// public void GetPlexMoviesTests()
// {
// var cachedMovies = F.Build<PlexSearch>().Without(x => x.Directory).CreateMany().ToList();
// cachedMovies.Add(new PlexSearch
// {
// Video = new List<Video>
// {
// new Video {Type = "movie", Title = "title1", Year = "2016", ProviderId = "1212"}
// }
// });
// 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"));
// }
// [Test]
// public void GetPlexTvShowsTests()
// {
// var cachedTv = F.Build<PlexSearch>().Without(x => x.Directory).CreateMany().ToList();
// cachedTv.Add(new PlexSearch
// {
// Directory = new List<Directory1>
// {
// 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();
// Assert.That(movies.Any(x => x.ProviderId == "1212"));
// }
// [Test]
// public async Task GetAllPlexEpisodes()
// {
// PlexEpisodes.Setup(x => x.GetAllAsync()).ReturnsAsync(F.CreateMany<PlexEpisodes>().ToList());
// var episodes = await Checker.GetEpisodes();
// Assert.That(episodes.Count(), Is.GreaterThan(0));
// }
// }
//}

@ -29,22 +29,23 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using PlexRequests.Store.Models;
using PlexRequests.Store.Models.Plex;
namespace PlexRequests.Services.Interfaces
{
public interface IAvailabilityChecker
{
void CheckAndUpdateAll();
List<PlexMovie> GetPlexMovies();
bool IsMovieAvailable(PlexMovie[] plexMovies, string title, string year, string providerId = null);
List<PlexTvShow> GetPlexTvShows();
bool IsTvShowAvailable(PlexTvShow[] plexShows, string title, string year, string providerId = null, int[] seasons = null);
List<PlexAlbum> GetPlexAlbums();
bool IsAlbumAvailable(PlexAlbum[] plexAlbums, string title, string year, string artist);
IEnumerable<PlexContent> GetPlexMovies(IEnumerable<PlexContent> content);
bool IsMovieAvailable(PlexContent[] plexMovies, string title, string year, string providerId = null);
IEnumerable<PlexContent> GetPlexTvShows(IEnumerable<PlexContent> content);
bool IsTvShowAvailable(PlexContent[] plexShows, string title, string year, string providerId = null, int[] seasons = null);
IEnumerable<PlexContent> GetPlexAlbums(IEnumerable<PlexContent> content);
bool IsAlbumAvailable(PlexContent[] 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);
PlexContent GetAlbum(PlexContent[] plexAlbums, string title, string year, string artist);
PlexContent GetMovie(PlexContent[] plexMovies, string title, string year, string providerId = null);
PlexContent GetTvShow(PlexContent[] plexShows, string title, string year, string providerId = null, int[] seasons = null);
/// <summary>
/// Gets the episode's stored in the cache.
/// </summary>

@ -36,5 +36,6 @@ namespace PlexRequests.Services.Interfaces
void Record(string jobName);
Task<IEnumerable<ScheduledJobs>> GetJobsAsync();
IEnumerable<ScheduledJobs> GetJobs();
void SetRunning(bool running, string jobName);
}
}

@ -27,13 +27,14 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using PlexRequests.Core.Models;
using PlexRequests.Store;
namespace PlexRequests.Services.Interfaces
{
public interface INotificationEngine
{
Task NotifyUsers(IEnumerable<RequestedModel> modelChanged, string apiKey);
Task NotifyUsers(RequestedModel modelChanged, string apiKey);
Task NotifyUsers(IEnumerable<RequestedModel> modelChanged, string apiKey, NotificationType type);
Task NotifyUsers(RequestedModel modelChanged, string apiKey, NotificationType type);
}
}

@ -65,6 +65,7 @@ namespace PlexRequests.Services.Jobs
var settings = CpSettings.GetSettings();
if (settings.Enabled)
{
Job.SetRunning(true, JobNames.CpCacher);
Log.Trace("Getting all movies from CouchPotato");
try
{
@ -81,6 +82,7 @@ namespace PlexRequests.Services.Jobs
finally
{
Job.Record(JobNames.CpCacher);
Job.SetRunning(false, JobNames.CpCacher);
}
}
}

@ -0,0 +1,332 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: UserRequestLimitResetter.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
using PlexRequests.Api;
using PlexRequests.Api.Interfaces;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Permissions;
using PlexRequests.Services.Interfaces;
using PlexRequests.Store;
using PlexRequests.Store.Models;
using PlexRequests.Store.Repository;
using Quartz;
namespace PlexRequests.Services.Jobs
{
public class FaultQueueHandler : IJob
{
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
public FaultQueueHandler(IJobRecord record, IRepository<RequestQueue> repo, ISonarrApi sonarrApi,
ISickRageApi srApi, ISettingsService<SonarrSettings> sonarrSettings, ISettingsService<SickRageSettings> srSettings,
ICouchPotatoApi cpApi, ISettingsService<CouchPotatoSettings> cpsettings, IRequestService requestService,
ISettingsService<HeadphonesSettings> hpSettings, IHeadphonesApi headphonesApi, ISettingsService<PlexRequestSettings> prSettings,
ISecurityExtensions security)
{
Record = record;
Repo = repo;
SonarrApi = sonarrApi;
SrApi = srApi;
CpApi = cpApi;
HpApi = headphonesApi;
RequestService = requestService;
SickrageSettings = srSettings;
SonarrSettings = sonarrSettings;
CpSettings = cpsettings;
HeadphoneSettings = hpSettings;
Security = security;
PrSettings = prSettings.GetSettings();
}
private IRepository<RequestQueue> Repo { get; }
private IJobRecord Record { get; }
private ISonarrApi SonarrApi { get; }
private ISickRageApi SrApi { get; }
private ICouchPotatoApi CpApi { get; }
private IHeadphonesApi HpApi { get; }
private IRequestService RequestService { get; }
private PlexRequestSettings PrSettings { get; }
private ISettingsService<SonarrSettings> SonarrSettings { get; }
private ISettingsService<SickRageSettings> SickrageSettings { get; }
private ISettingsService<CouchPotatoSettings> CpSettings { get; }
private ISettingsService<HeadphonesSettings> HeadphoneSettings { get; }
private ISecurityExtensions Security { get; }
public void Execute(IJobExecutionContext context)
{
Record.SetRunning(true, JobNames.CpCacher);
try
{
var faultedRequests = Repo.GetAll().ToList();
var missingInfo = faultedRequests.Where(x => x.FaultType == FaultType.MissingInformation).ToList();
ProcessMissingInformation(missingInfo);
var transientErrors = faultedRequests.Where(x => x.FaultType == FaultType.RequestFault).ToList();
ProcessTransientErrors(transientErrors);
}
catch (Exception e)
{
Log.Error(e);
}
finally
{
Record.Record(JobNames.FaultQueueHandler);
Record.SetRunning(false, JobNames.CpCacher);
}
}
private void ProcessMissingInformation(List<RequestQueue> requests)
{
if (!requests.Any())
{
return;
}
var sonarrSettings = SonarrSettings.GetSettings();
var sickrageSettings = SickrageSettings.GetSettings();
var tv = requests.Where(x => x.Type == RequestType.TvShow);
// TV
var tvApi = new TvMazeApi();
foreach (var t in tv)
{
var providerId = int.Parse(t.PrimaryIdentifier);
var showInfo = tvApi.ShowLookup(providerId);
if (showInfo.externals?.thetvdb != null)
{
// We now have the info
var tvModel = ByteConverterHelper.ReturnObject<RequestedModel>(t.Content);
tvModel.ProviderId = showInfo.externals.thetvdb.Value;
var result = ProcessTvShow(tvModel, sonarrSettings, sickrageSettings);
if (!result)
{
// we now have the info but couldn't add it, so add it back into the queue but with a different fault
t.Content = ByteConverterHelper.ReturnBytes(tvModel);
t.FaultType = FaultType.RequestFault;
t.LastRetry = DateTime.UtcNow;
Repo.Update(t);
}
else
{
// Successful, remove from the fault queue
Repo.Delete(t);
}
}
}
}
private bool ProcessTvShow(RequestedModel tvModel, SonarrSettings sonarr, SickRageSettings sickrage)
{
try
{
var sender = new TvSenderOld(SonarrApi, SrApi);
if (sonarr.Enabled)
{
var task = sender.SendToSonarr(sonarr, tvModel, sonarr.QualityProfile);
var a = task.Result;
if (string.IsNullOrEmpty(a?.title))
{
// Couldn't send it
return false;
}
return true;
}
if (sickrage.Enabled)
{
var result = sender.SendToSickRage(sickrage, tvModel);
if (result?.result != "success")
{
// Couldn't send it
return false;
}
// Approve it
tvModel.Approved = true;
RequestService.UpdateRequest(tvModel);
return true;
}
return false;
}
catch (Exception e)
{
Log.Error(e);
return false; // It fails so it will get added back into the queue
}
}
private bool ProcessMovies(RequestedModel model, CouchPotatoSettings cp)
{
try
{
if (cp.Enabled)
{
var result = CpApi.AddMovie(model.ImdbId, cp.ApiKey, model.Title,
cp.FullUri, cp.ProfileId);
if (result)
{
// Approve it now
model.Approved = true;
RequestService.UpdateRequest(model);
};
return result;
}
return false;
}
catch (Exception e)
{
Log.Error(e);
return false; // It fails so it will get added back into the queue
}
}
private bool ProcessAlbums(RequestedModel model, HeadphonesSettings hp)
{
try
{
if (hp.Enabled)
{
var sender = new HeadphonesSender(HpApi, hp, RequestService);
var result = sender.AddAlbum(model).Result;
if (result)
{
if (ShouldAutoApprove(model.Type, PrSettings, model.RequestedUsers))
// Approve it now
model.Approved = true;
RequestService.UpdateRequest(model);
};
return result;
}
return false;
}
catch (Exception e)
{
Log.Error(e);
return false; // It fails so it will get added back into the queue
}
}
private void ProcessTransientErrors(List<RequestQueue> requests)
{
var sonarrSettings = SonarrSettings.GetSettings();
var sickrageSettings = SickrageSettings.GetSettings();
var cpSettings = CpSettings.GetSettings();
var hpSettings = HeadphoneSettings.GetSettings();
if (!requests.Any())
{
return;
}
foreach (var request in requests)
{
var model = ByteConverterHelper.ReturnObject<RequestedModel>(request.Content);
bool result;
switch (request.Type)
{
case RequestType.Movie:
result = ProcessMovies(model, cpSettings);
break;
case RequestType.TvShow:
result = ProcessTvShow(model, sonarrSettings, sickrageSettings);
break;
case RequestType.Album:
result = ProcessAlbums(model, hpSettings);
break;
default:
throw new ArgumentOutOfRangeException();
}
if (!result)
{
// we now have the info but couldn't add it, so do nothing now.
request.LastRetry = DateTime.UtcNow;
Repo.Update(request);
}
else
{
// Successful, remove from the fault queue
Repo.Delete(request);
}
}
}
public bool ShouldAutoApprove(RequestType requestType, PlexRequestSettings prSettings, List<string> username)
{
if (prSettings.ApprovalWhiteList.Intersect(username).Any())
{
return true;
}
foreach (var user in username)
{
var admin = Security.HasPermissions(user, Permissions.Administrator);
// if the user is an admin or they are whitelisted, they go ahead and allow auto-approval
if (admin) return true;
// check by request type if the category requires approval or not
switch (requestType)
{
case RequestType.Movie:
return Security.HasPermissions(user, Permissions.AutoApproveMovie);
case RequestType.TvShow:
return Security.HasPermissions(user, Permissions.AutoApproveTv);
case RequestType.Album:
return Security.HasPermissions(user, Permissions.AutoApproveAlbum);
default:
return false;
}
}
return false;
}
}
}

@ -0,0 +1,10 @@
using Quartz;
namespace PlexRequests.Services.Jobs
{
public interface IPlexContentCacher
{
void CacheContent();
void Execute(IJobExecutionContext context);
}
}

@ -33,9 +33,13 @@ namespace PlexRequests.Services.Jobs
public const string SonarrCacher = "Sonarr Cacher";
public const string SrCacher = "SickRage Cacher";
public const string PlexChecker = "Plex Availability Cacher";
public const string PlexCacher = "Plex Cacher";
public const string StoreCleanup = "Database Cleanup";
public const string RequestLimitReset = "Request Limit Reset";
public const string EpisodeCacher = "Plex Episode Cacher";
public const string RecentlyAddedEmail = "Recently Added Email Notification";
public const string FaultQueueHandler = "Request Fault Queue Handler";
public const string PlexUserChecker = "Plex User Checker";
}
}

@ -60,6 +60,23 @@ namespace PlexRequests.Services.Jobs
}
}
public void SetRunning(bool running, string jobName)
{
var allJobs = Repo.GetAll();
var storeJob = allJobs.FirstOrDefault(x => x.Name == jobName);
if (storeJob != null)
{
storeJob.Running = running;
Repo.Update(storeJob);
}
else
{
var job = new ScheduledJobs { Running = running, Name = jobName };
Repo.Insert(job);
}
}
public async Task<IEnumerable<ScheduledJobs>> GetJobsAsync()
{
return await Repo.GetAllAsync();

@ -30,7 +30,7 @@ using System.Linq;
using System.Threading.Tasks;
using Dapper;
using Newtonsoft.Json;
using NLog;
using PlexRequests.Api.Interfaces;
@ -44,6 +44,7 @@ using PlexRequests.Services.Models;
using PlexRequests.Services.Notification;
using PlexRequests.Store;
using PlexRequests.Store.Models;
using PlexRequests.Store.Models.Plex;
using PlexRequests.Store.Repository;
using Quartz;
@ -53,7 +54,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, INotificationEngine e)
INotificationService notify, IJobRecord rec, IRepository<UsersToNotify> users, IRepository<PlexEpisodes> repo, INotificationEngine e, IRepository<PlexContent> content)
{
Plex = plexSettings;
RequestService = request;
@ -64,6 +65,7 @@ namespace PlexRequests.Services.Jobs
UserNotifyRepo = users;
EpisodeRepo = repo;
NotificationEngine = e;
PlexContent = content;
}
private ISettingsService<PlexSettings> Plex { get; }
@ -76,7 +78,7 @@ namespace PlexRequests.Services.Jobs
private IJobRecord Job { get; }
private IRepository<UsersToNotify> UserNotifyRepo { get; }
private INotificationEngine NotificationEngine { get; }
private IRepository<PlexContent> PlexContent { get; }
public void CheckAndUpdateAll()
{
@ -87,18 +89,17 @@ namespace PlexRequests.Services.Jobs
Log.Debug("Validation of the plex settings failed.");
return;
}
var libraries = CachedLibraries(plexSettings, true); //force setting the cache (10 min intervals via scheduler)
if (libraries == null || !libraries.Any())
{
Log.Debug("Did not find any libraries in Plex.");
return;
}
var movies = GetPlexMovies().ToArray();
var shows = GetPlexTvShows().ToArray();
var albums = GetPlexAlbums().ToArray();
//var libraries = CachedLibraries(plexSettings, true); //force setting the cache (10 min intervals via scheduler)
//if (libraries == null || !libraries.Any())
//{
// Log.Debug("Did not find any libraries in Plex.");
// return;
//}
var content = PlexContent.GetAll().ToList();
var movies = GetPlexMovies(content).ToArray();
var shows = GetPlexTvShows(content).ToArray();
var albums = GetPlexAlbums(content).ToArray();
var requests = RequestService.GetAll();
var requestedModels = requests as RequestedModel[] ?? requests.Where(x => !x.Available).ToArray();
@ -153,15 +154,12 @@ namespace PlexRequests.Services.Jobs
if (modifiedModel.Any())
{
NotificationEngine.NotifyUsers(modifiedModel, plexSettings.PlexAuthToken);
NotificationEngine.NotifyUsers(modifiedModel, plexSettings.PlexAuthToken, NotificationType.RequestAvailable);
RequestService.BatchUpdate(modifiedModel);
}
Job.Record(JobNames.PlexChecker);
}
public List<PlexMovie> GetPlexMovies()
public List<PlexMovie> GetPlexMoviesOld()
{
var settings = Plex.GetSettings();
var movies = new List<PlexMovie>();
@ -188,13 +186,18 @@ namespace PlexRequests.Services.Jobs
return movies;
}
public bool IsMovieAvailable(PlexMovie[] plexMovies, string title, string year, string providerId = null)
public IEnumerable<PlexContent> GetPlexMovies(IEnumerable<PlexContent> content)
{
return content.Where(x => x.Type == Store.Models.Plex.PlexMediaType.Movie);
}
public bool IsMovieAvailable(PlexContent[] 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)
public PlexContent GetMovie(PlexContent[] plexMovies, string title, string year, string providerId = null)
{
if (plexMovies.Length == 0)
{
@ -225,45 +228,19 @@ namespace PlexRequests.Services.Jobs
return null;
}
public List<PlexTvShow> GetPlexTvShows()
public IEnumerable<PlexContent> GetPlexTvShows(IEnumerable<PlexContent> content)
{
var settings = Plex.GetSettings();
var shows = new List<PlexTvShow>();
var libs = Cache.Get<List<PlexSearch>>(CacheKeys.PlexLibaries);
if (libs != null)
{
var withDir = libs.Where(x => x.Directory != null);
var tvLibs = withDir.Where(x =>
x.Directory.Any(y =>
y.Type.Equals(PlexMediaType.Show.ToString(), StringComparison.CurrentCultureIgnoreCase)
)
).ToArray();
foreach (var lib in tvLibs)
{
shows.AddRange(lib.Directory.Select(x => new PlexTvShow // shows are in the directory list
{
Title = x.Title,
ReleaseYear = x.Year,
ProviderId = x.ProviderId,
Seasons = x.Seasons?.Select(d => PlexHelper.GetSeasonNumberFromTitle(d.Title)).ToArray(),
Url = PlexHelper.GetPlexMediaUrl(settings.MachineIdentifier, x.RatingKey)
}));
}
}
return shows;
return content.Where(x => x.Type == Store.Models.Plex.PlexMediaType.Show.to);
}
public bool IsTvShowAvailable(PlexTvShow[] plexShows, string title, string year, string providerId = null, int[] seasons = null)
public bool IsTvShowAvailable(PlexContent[] 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,
public PlexContent GetTvShow(PlexContent[] plexShows, string title, string year, string providerId = null,
int[] seasons = null)
{
var advanced = !string.IsNullOrEmpty(providerId);
@ -273,7 +250,8 @@ namespace PlexRequests.Services.Jobs
{
if (show.ProviderId == providerId && seasons != null)
{
if (seasons.Any(season => show.Seasons.Contains(season)))
var showSeasons = ByteConverterHelper.ReturnObject<int[]>(show.Seasons);
if (seasons.Any(season => showSeasons.Contains(season)))
{
return show;
}
@ -359,41 +337,19 @@ namespace PlexRequests.Services.Jobs
return plexEpisodeses;
}
public List<PlexAlbum> GetPlexAlbums()
public IEnumerable<PlexContent> GetPlexAlbums(IEnumerable<PlexContent> content)
{
var settings = Plex.GetSettings();
var albums = new List<PlexAlbum>();
var libs = Cache.Get<List<PlexSearch>>(CacheKeys.PlexLibaries);
if (libs != null)
{
var albumLibs = libs.Where(x =>
x.Directory.Any(y =>
y.Type.Equals(PlexMediaType.Artist.ToString(), StringComparison.CurrentCultureIgnoreCase)
)
).ToArray();
foreach (var lib in albumLibs)
{
albums.AddRange(lib.Directory.Select(x => new PlexAlbum()
{
Title = x.Title,
ReleaseYear = x.Year,
Artist = x.ParentTitle,
Url = PlexHelper.GetPlexMediaUrl(settings.MachineIdentifier, x.RatingKey)
}));
}
}
return albums;
return content.Where(x => x.Type == Store.Models.Plex.PlexMediaType.Artist);
}
public bool IsAlbumAvailable(PlexAlbum[] plexAlbums, string title, string year, string artist)
public bool IsAlbumAvailable(PlexContent[] plexAlbums, string title, string year, string artist)
{
return plexAlbums.Any(x =>
x.Title.Contains(title) &&
x.Artist.Equals(artist, StringComparison.CurrentCultureIgnoreCase));
}
public PlexAlbum GetAlbum(PlexAlbum[] plexAlbums, string title, string year, string artist)
public PlexContent GetAlbum(PlexContent[] plexAlbums, string title, string year, string artist)
{
return plexAlbums.FirstOrDefault(x =>
x.Title.Contains(title) &&
@ -419,11 +375,11 @@ namespace PlexRequests.Services.Jobs
results = GetLibraries(plexSettings);
if (plexSettings.AdvancedSearch)
{
for (var i = 0; i < results.Count; i++)
foreach (PlexSearch t in results)
{
for (var j = 0; j < results[i].Directory.Count; j++)
foreach (Directory1 t1 in t.Directory)
{
var currentItem = results[i].Directory[j];
var currentItem = t1;
var metaData = PlexApi.GetMetadata(plexSettings.PlexAuthToken, plexSettings.FullUri,
currentItem.RatingKey);
@ -434,25 +390,21 @@ namespace PlexRequests.Services.Jobs
currentItem.RatingKey);
// We do not want "all episodes" this as a season
var filtered =
seasons.Directory.Where(
x =>
!x.Title.Equals("All episodes",
StringComparison.CurrentCultureIgnoreCase));
var filtered = seasons.Directory.Where( x => !x.Title.Equals("All episodes", StringComparison.CurrentCultureIgnoreCase));
results[i].Directory[j].Seasons.AddRange(filtered);
t1.Seasons.AddRange(filtered);
}
var providerId = PlexHelper.GetProviderIdFromPlexGuid(metaData.Directory.Guid);
results[i].Directory[j].ProviderId = providerId;
t1.ProviderId = providerId;
}
for (var j = 0; j < results[i].Video.Count; j++)
foreach (Video t1 in t.Video)
{
var currentItem = results[i].Video[j];
var currentItem = t1;
var metaData = PlexApi.GetMetadata(plexSettings.PlexAuthToken, plexSettings.FullUri,
currentItem.RatingKey);
var providerId = PlexHelper.GetProviderIdFromPlexGuid(metaData.Video.Guid);
results[i].Video[j].ProviderId = providerId;
t1.ProviderId = providerId;
}
}
}
@ -479,10 +431,10 @@ namespace PlexRequests.Services.Jobs
{
var sections = PlexApi.GetLibrarySections(plexSettings.PlexAuthToken, plexSettings.FullUri);
List<PlexSearch> libs = new List<PlexSearch>();
var libs = new List<PlexSearch>();
if (sections != null)
{
foreach (var dir in sections.Directories)
foreach (var dir in sections.Directories ?? new List<Directory>())
{
var lib = PlexApi.GetLibrary(plexSettings.PlexAuthToken, plexSettings.FullUri, dir.Key);
if (lib != null)
@ -507,6 +459,8 @@ namespace PlexRequests.Services.Jobs
public void Execute(IJobExecutionContext context)
{
Job.SetRunning(true, JobNames.PlexChecker);
try
{
CheckAndUpdateAll();
@ -515,6 +469,11 @@ namespace PlexRequests.Services.Jobs
{
Log.Error(e);
}
finally
{
Job.Record(JobNames.PlexChecker);
Job.SetRunning(false, JobNames.PlexChecker);
}
}
}
}

@ -0,0 +1,361 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: PlexAvailabilityChecker.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 Dapper;
using Newtonsoft.Json;
using NLog;
using Org.BouncyCastle.Crypto.Modes.Gcm;
using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.Plex;
using PlexRequests.Core;
using PlexRequests.Core.Models;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Services.Interfaces;
using PlexRequests.Services.Models;
using PlexRequests.Services.Notification;
using PlexRequests.Store;
using PlexRequests.Store.Models;
using PlexRequests.Store.Models.Plex;
using PlexRequests.Store.Repository;
using Quartz;
namespace PlexRequests.Services.Jobs
{
public class PlexContentCacher : IJob, IPlexContentCacher
{
public PlexContentCacher(ISettingsService<PlexSettings> plexSettings, IRequestService request, IPlexApi plex, ICacheProvider cache,
INotificationService notify, IJobRecord rec, IRepository<UsersToNotify> users, IRepository<PlexEpisodes> repo, INotificationEngine e, IRepository<PlexContent> content)
{
Plex = plexSettings;
RequestService = request;
PlexApi = plex;
Cache = cache;
Notification = notify;
Job = rec;
UserNotifyRepo = users;
EpisodeRepo = repo;
NotificationEngine = e;
PlexContent = content;
}
private ISettingsService<PlexSettings> Plex { get; }
private IRepository<PlexEpisodes> EpisodeRepo { get; }
private IRequestService RequestService { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
private IPlexApi PlexApi { get; }
private ICacheProvider Cache { get; }
private INotificationService Notification { get; }
private IJobRecord Job { get; }
private IRepository<UsersToNotify> UserNotifyRepo { get; }
private INotificationEngine NotificationEngine { get; }
private IRepository<PlexContent> PlexContent { get; }
public void CacheContent()
{
var plexSettings = Plex.GetSettings();
if (!ValidateSettings(plexSettings))
{
Log.Debug("Validation of the plex settings failed.");
return;
}
var libraries = CachedLibraries(plexSettings);
if (libraries == null || !libraries.Any())
{
Log.Debug("Did not find any libraries in Plex.");
return;
}
}
public List<PlexMovie> GetPlexMovies(List<PlexSearch> libs)
{
var settings = Plex.GetSettings();
var movies = new List<PlexMovie>();
if (libs != null)
{
var movieLibs = libs.Where(x =>
x.Video.Any(y =>
y.Type.Equals(PlexMediaType.Movie.ToString(), StringComparison.CurrentCultureIgnoreCase)
)
).ToArray();
foreach (var lib in movieLibs)
{
movies.AddRange(lib.Video.Select(video => new PlexMovie
{
ReleaseYear = video.Year,
Title = video.Title,
ProviderId = video.ProviderId,
Url = PlexHelper.GetPlexMediaUrl(settings.MachineIdentifier, video.RatingKey)
}));
}
}
return movies;
}
public List<PlexTvShow> GetPlexTvShows(List<PlexSearch> libs)
{
var settings = Plex.GetSettings();
var shows = new List<PlexTvShow>();
if (libs != null)
{
var withDir = libs.Where(x => x.Directory != null);
var tvLibs = withDir.Where(x =>
x.Directory.Any(y =>
y.Type.Equals(PlexMediaType.Show.ToString(), StringComparison.CurrentCultureIgnoreCase)
)
).ToArray();
foreach (var lib in tvLibs)
{
shows.AddRange(lib.Directory.Select(x => new PlexTvShow // shows are in the directory list
{
Title = x.Title,
ReleaseYear = x.Year,
ProviderId = x.ProviderId,
Seasons = x.Seasons?.Select(d => PlexHelper.GetSeasonNumberFromTitle(d.Title)).ToArray(),
Url = PlexHelper.GetPlexMediaUrl(settings.MachineIdentifier, x.RatingKey),
}));
}
}
return shows;
}
public List<PlexAlbum> GetPlexAlbums(List<PlexSearch> libs)
{
var settings = Plex.GetSettings();
var albums = new List<PlexAlbum>();
if (libs != null)
{
var albumLibs = libs.Where(x =>
x.Directory.Any(y =>
y.Type.Equals(PlexMediaType.Artist.ToString(), StringComparison.CurrentCultureIgnoreCase)
)
).ToArray();
foreach (var lib in albumLibs)
{
albums.AddRange(lib.Directory.Select(x => new PlexAlbum()
{
Title = x.Title,
ProviderId = x.ProviderId,
ReleaseYear = x.Year,
Artist = x.ParentTitle,
Url = PlexHelper.GetPlexMediaUrl(settings.MachineIdentifier, x.RatingKey)
}));
}
}
return albums;
}
private List<PlexSearch> CachedLibraries(PlexSettings plexSettings)
{
var results = new List<PlexSearch>();
if (!ValidateSettings(plexSettings))
{
Log.Warn("The settings are not configured");
return results; // don't error out here, just let it go! let it goo!!!
}
try
{
results = GetLibraries(plexSettings);
if (plexSettings.AdvancedSearch)
{
foreach (PlexSearch t in results)
{
foreach (Directory1 t1 in t.Directory)
{
var currentItem = t1;
var metaData = PlexApi.GetMetadata(plexSettings.PlexAuthToken, plexSettings.FullUri,
currentItem.RatingKey);
// Get the seasons for each show
if (currentItem.Type.Equals(PlexMediaType.Show.ToString(), StringComparison.CurrentCultureIgnoreCase))
{
var seasons = PlexApi.GetSeasons(plexSettings.PlexAuthToken, plexSettings.FullUri,
currentItem.RatingKey);
// We do not want "all episodes" this as a season
var filtered = seasons.Directory.Where(x => !x.Title.Equals("All episodes", StringComparison.CurrentCultureIgnoreCase));
t1.Seasons.AddRange(filtered);
}
var providerId = PlexHelper.GetProviderIdFromPlexGuid(metaData.Directory.Guid);
t1.ProviderId = providerId;
}
foreach (Video t1 in t.Video)
{
var currentItem = t1;
var metaData = PlexApi.GetMetadata(plexSettings.PlexAuthToken, plexSettings.FullUri,
currentItem.RatingKey);
var providerId = PlexHelper.GetProviderIdFromPlexGuid(metaData.Video.Guid);
t1.ProviderId = providerId;
}
}
}
if (results != null)
{
var movies = GetPlexMovies(results);
// Time to destroy the plex movies from the DB
PlexContent.Custom(connection =>
{
connection.Open();
connection.Query("delete from PlexContent where type = @type", new { type = 0 });
return new List<PlexContent>();
});
foreach (var m in movies)
{
PlexContent.Insert(new PlexContent
{
ProviderId = m.ProviderId,
ReleaseYear = m.ReleaseYear ?? string.Empty,
Title = m.Title,
Type = Store.Models.Plex.PlexMediaType.Movie,
Url = m.Url
});
}
var tv = GetPlexTvShows(results);
// Time to destroy the plex tv from the DB
PlexContent.Custom(connection =>
{
connection.Open();
connection.Query("delete from PlexContent where type = @type", new { type = 1 });
return new List<PlexContent>();
});
foreach (var t in tv)
{
PlexContent.Insert(new PlexContent
{
ProviderId = t.ProviderId,
ReleaseYear = t.ReleaseYear ?? string.Empty,
Title = t.Title,
Type = Store.Models.Plex.PlexMediaType.Show,
Url = t.Url,
Seasons = ByteConverterHelper.ReturnBytes(t.Seasons)
});
}
var albums = GetPlexAlbums(results);
// Time to destroy the plex movies from the DB
PlexContent.Custom(connection =>
{
connection.Open();
connection.Query("delete from PlexContent where type = @type", new { type = 2 });
return new List<PlexContent>();
});
foreach (var a in albums)
{
PlexContent.Insert(new PlexContent
{
ProviderId = a.ProviderId,
ReleaseYear = a.ReleaseYear ?? string.Empty,
Title = a.Title,
Type = Store.Models.Plex.PlexMediaType.Artist,
Url = a.Url
});
}
}
}
catch (Exception ex)
{
Log.Error(ex, "Failed to obtain Plex libraries");
}
return results;
}
private List<PlexSearch> GetLibraries(PlexSettings plexSettings)
{
var sections = PlexApi.GetLibrarySections(plexSettings.PlexAuthToken, plexSettings.FullUri);
var libs = new List<PlexSearch>();
if (sections != null)
{
foreach (var dir in sections.Directories ?? new List<Directory>())
{
var lib = PlexApi.GetLibrary(plexSettings.PlexAuthToken, plexSettings.FullUri, dir.Key);
if (lib != null)
{
libs.Add(lib);
}
}
}
return libs;
}
private bool ValidateSettings(PlexSettings plex)
{
if (plex?.Ip == null || plex?.PlexAuthToken == null)
{
Log.Warn("A setting is null, Ensure Plex is configured correctly, and we have a Plex Auth token.");
return false;
}
return true;
}
public void Execute(IJobExecutionContext context)
{
Job.SetRunning(true, JobNames.PlexCacher);
try
{
CacheContent();
}
catch (Exception e)
{
Log.Error(e);
}
finally
{
Job.Record(JobNames.PlexCacher);
Job.SetRunning(false, JobNames.PlexCacher);
}
}
}
}

@ -145,6 +145,7 @@ namespace PlexRequests.Services.Jobs
public void Execute(IJobExecutionContext context)
{
try
{
var s = Plex.GetSettings();
@ -162,6 +163,7 @@ namespace PlexRequests.Services.Jobs
return;
}
}
Job.SetRunning(true, JobNames.EpisodeCacher);
CacheEpisodes(s);
}
catch (Exception e)
@ -171,6 +173,7 @@ namespace PlexRequests.Services.Jobs
finally
{
Job.Record(JobNames.EpisodeCacher);
Job.SetRunning(false, JobNames.EpisodeCacher);
}
}
}

@ -0,0 +1,175 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: StoreCleanup.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 NLog;
using PlexRequests.Api.Interfaces;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Core.Users;
using PlexRequests.Services.Interfaces;
using PlexRequests.Store.Models;
using PlexRequests.Store.Repository;
using Quartz;
namespace PlexRequests.Services.Jobs
{
public class PlexUserChecker : IJob
{
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
public PlexUserChecker(IPlexUserRepository plexUsers, IPlexApi plexAPi, IJobRecord rec, ISettingsService<PlexSettings> plexSettings, ISettingsService<PlexRequestSettings> prSettings, ISettingsService<UserManagementSettings> umSettings,
IRequestService requestService, IUserRepository localUser)
{
Repo = plexUsers;
JobRecord = rec;
PlexApi = plexAPi;
PlexSettings = plexSettings;
PlexRequestSettings = prSettings;
UserManagementSettings = umSettings;
RequestService = requestService;
LocalUserRepository = localUser;
}
private IJobRecord JobRecord { get; }
private IPlexApi PlexApi { get; }
private IPlexUserRepository Repo { get; }
private ISettingsService<PlexSettings> PlexSettings { get; }
private ISettingsService<PlexRequestSettings> PlexRequestSettings { get; }
private ISettingsService<UserManagementSettings> UserManagementSettings { get; }
private IRequestService RequestService { get; }
private IUserRepository LocalUserRepository { get; }
public void Execute(IJobExecutionContext context)
{
JobRecord.SetRunning(true, JobNames.PlexUserChecker);
try
{
var settings = PlexSettings.GetSettings();
if (string.IsNullOrEmpty(settings.PlexAuthToken))
{
return;
}
var plexUsers = PlexApi.GetUsers(settings.PlexAuthToken);
var userManagementSettings = UserManagementSettings.GetSettings();
var requests = RequestService.GetAll().ToList();
var dbUsers = Repo.GetAll().ToList();
var localUsers = LocalUserRepository.GetAll().ToList();
foreach (var user in plexUsers.User)
{
var dbUser = dbUsers.FirstOrDefault(x => x.PlexUserId == user.Id);
if (dbUser != null)
{
// We already have the user, let's check if they have updated any of their info.
var needToUpdate = false;
var usernameChanged = false;
// Do we need up update any info?
if (!dbUser.EmailAddress.Equals(user.Email, StringComparison.CurrentCultureIgnoreCase))
{
dbUser.EmailAddress = user.Email;
needToUpdate = true;
}
if (!dbUser.Username.Equals(user.Username, StringComparison.CurrentCultureIgnoreCase))
{
needToUpdate = true;
usernameChanged = true;
}
if (needToUpdate)
{
if (usernameChanged)
{
// The username has changed, let's check if the username matches any local users
var localUser = localUsers.FirstOrDefault(x => x.UserName.Equals(user.Username, StringComparison.CurrentCultureIgnoreCase));
dbUser.Username = user.Username;
if (localUser != null)
{
// looks like we have a local user with the same name...
// We should delete the local user and the Plex user will become the master,
// I am not going to update the Plex Users permissions as that could end up leading to a security vulnerability
// Where anyone could change their Plex Username to the PR.Net server admins name and get all the admin permissions.
LocalUserRepository.Delete(localUser);
}
// Since the username has changed, we need to update all requests with that username (unless we are using the alias! Since the alias won't change)
if (string.IsNullOrEmpty(dbUser.UserAlias))
{
// Update all requests
var requestsWithThisUser = requests.Where(x => x.RequestedUsers.Contains(user.Username)).ToList();
foreach (var r in requestsWithThisUser)
{
r.RequestedUsers.Remove(user.Username); // Remove old
r.RequestedUsers.Add(dbUser.Username); // Add new
}
if (requestsWithThisUser.Any())
{
RequestService.BatchUpdate(requestsWithThisUser);
}
}
}
Repo.Update(dbUser);
}
continue;
}
// Looks like it's a new user!
var m = new PlexUsers
{
PlexUserId = user.Id,
Permissions = UserManagementHelper.GetPermissions(userManagementSettings),
Features = UserManagementHelper.GetFeatures(userManagementSettings),
UserAlias = string.Empty,
EmailAddress = user.Email,
Username = user.Username,
LoginId = Guid.NewGuid().ToString()
};
Repo.Insert(m);
}
}
catch (Exception e)
{
Log.Error(e);
}
finally
{
JobRecord.SetRunning(false, JobNames.PlexUserChecker);
JobRecord.Record(JobNames.PlexUserChecker);
}
}
}
}

@ -39,7 +39,9 @@ using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.Plex;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Core.Users;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Permissions;
using PlexRequests.Services.Interfaces;
using PlexRequests.Services.Jobs.Templates;
using PlexRequests.Store.Models.Plex;
@ -52,8 +54,8 @@ namespace PlexRequests.Services.Jobs
{
public RecentlyAdded(IPlexApi api, ISettingsService<PlexSettings> plexSettings,
ISettingsService<EmailNotificationSettings> email, IJobRecord rec,
ISettingsService<NewletterSettings> newsletter,
IPlexReadOnlyDatabase db)
ISettingsService<NewletterSettings> newsletter,
IPlexReadOnlyDatabase db, IUserHelper userHelper)
{
JobRecord = rec;
Api = api;
@ -61,6 +63,7 @@ namespace PlexRequests.Services.Jobs
EmailSettings = email;
NewsletterSettings = newsletter;
PlexDb = db;
UserHelper = userHelper;
}
private IPlexApi Api { get; }
@ -73,6 +76,7 @@ namespace PlexRequests.Services.Jobs
private ISettingsService<NewletterSettings> NewsletterSettings { get; }
private IJobRecord JobRecord { get; }
private IPlexReadOnlyDatabase PlexDb { get; }
private IUserHelper UserHelper { get; }
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
@ -85,7 +89,7 @@ namespace PlexRequests.Services.Jobs
{
return;
}
JobRecord.SetRunning(true, JobNames.RecentlyAddedEmail);
Start(settings);
}
catch (Exception e)
@ -95,6 +99,7 @@ namespace PlexRequests.Services.Jobs
finally
{
JobRecord.Record(JobNames.RecentlyAddedEmail);
JobRecord.SetRunning(false, JobNames.RecentlyAddedEmail);
}
}
@ -254,16 +259,24 @@ namespace PlexRequests.Services.Jobs
if (!testEmail)
{
if (newletterSettings.SendToPlexUsers)
//if (newletterSettings.SendToPlexUsers)
//{
var users = UserHelper.GetUsersWithFeature(Features.RequestAddedNotification);
if (users != null)
{
var users = Api.GetUsers(plexSettings.PlexAuthToken);
foreach (var user in users.User)
foreach (var user in users)
{
message.Bcc.Add(new MailboxAddress(user.Username, user.Email));
if (!string.IsNullOrEmpty(user.EmailAddress))
{
message.Bcc.Add(new MailboxAddress(user.Username, user.EmailAddress));
}
}
}
//}
if (newletterSettings.CustomUsersEmailAddresses.Any())
if (newletterSettings.CustomUsersEmailAddresses != null
&& newletterSettings.CustomUsersEmailAddresses.Any())
{
foreach (var user in newletterSettings.CustomUsersEmailAddresses)
{

@ -63,6 +63,9 @@ namespace PlexRequests.Services.Jobs
var settings = SrSettings.GetSettings();
if (settings.Enabled)
{
Job.SetRunning(true, JobNames.SrCacher);
Log.Trace("Getting all shows from SickRage");
try
{
@ -79,6 +82,7 @@ namespace PlexRequests.Services.Jobs
finally
{
Job.Record(JobNames.SrCacher);
Job.SetRunning(false, JobNames.SrCacher);
}
}
}

@ -65,6 +65,7 @@ namespace PlexRequests.Services.Jobs
var settings = SonarrSettings.GetSettings();
if (settings.Enabled)
{
Job.SetRunning(true, JobNames.SonarrCacher);
try
{
var series = SonarrApi.GetSeries(settings.ApiKey, settings.FullUri);
@ -80,6 +81,7 @@ namespace PlexRequests.Services.Jobs
finally
{
Job.Record(JobNames.SonarrCacher);
Job.SetRunning(false, JobNames.SonarrCacher);
}
}
}

@ -53,6 +53,7 @@ namespace PlexRequests.Services.Jobs
public void Execute(IJobExecutionContext context)
{
JobRecord.SetRunning(true, JobNames.CpCacher);
TakeBackup();
Cleanup();
}
@ -91,6 +92,7 @@ namespace PlexRequests.Services.Jobs
finally
{
JobRecord.Record(JobNames.StoreBackup);
JobRecord.SetRunning(false, JobNames.CpCacher);
}
}

@ -78,12 +78,14 @@ namespace PlexRequests.Services.Jobs
finally
{
JobRecord.Record(JobNames.StoreCleanup);
JobRecord.SetRunning(false, JobNames.CpCacher);
}
}
public void Execute(IJobExecutionContext context)
{
JobRecord.SetRunning(true, JobNames.CpCacher);
Cleanup();
}
}

@ -98,6 +98,7 @@ namespace PlexRequests.Services.Jobs
public void Execute(IJobExecutionContext context)
{
Record.SetRunning(true, JobNames.CpCacher);
try
{
var settings = Settings.GetSettings();
@ -115,6 +116,7 @@ namespace PlexRequests.Services.Jobs
finally
{
Record.Record(JobNames.RequestLimitReset);
Record.SetRunning(false, JobNames.CpCacher);
}
}
}

@ -6,5 +6,7 @@
public string Artist { get; set; }
public string ReleaseYear { get; set; }
public string Url { get; set; }
public string ProviderId { get; set; }
public string Id { get; set; }
}
}

@ -2,6 +2,7 @@
{
public class PlexMovie
{
public string Id { get; set; }
public string Title { get; set; }
public string ReleaseYear { get; set; }
public string ProviderId { get; set; }

@ -2,6 +2,7 @@
{
public class PlexTvShow
{
public string Id { get; set; }
public string Title { get; set; }
public string ReleaseYear { get; set; }
public string ProviderId { get; set; }

@ -79,14 +79,22 @@ namespace PlexRequests.Services.Notification
await EmailAvailableRequest(model, emailSettings);
break;
case NotificationType.RequestApproved:
throw new NotImplementedException();
await EmailRequestApproved(model, emailSettings);
break;
case NotificationType.AdminNote:
throw new NotImplementedException();
case NotificationType.Test:
await EmailTest(model, emailSettings);
break;
case NotificationType.RequestDeclined:
await EmailRequestDeclined(model, emailSettings);
break;
case NotificationType.ItemAddedToFaultQueue:
await EmailAddedToRequestQueue(model, emailSettings);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
@ -129,7 +137,7 @@ namespace PlexRequests.Services.Notification
$"Plex Requests: New {model.RequestType.GetString()?.ToLower()} request for {model.Title}!",
$"Hello! The user '{model.User}' has requested the {model.RequestType.GetString()?.ToLower()} '{model.Title}'! Please log in to approve this request. Request Date: {model.DateTime.ToString("f")}",
model.ImgSrc);
var body = new BodyBuilder { HtmlBody = html, TextBody = "This email is only available on devices that support HTML." };
var body = new BodyBuilder { HtmlBody = html, TextBody = $"Hello! The user '{model.User}' has requested the {model.RequestType.GetString()?.ToLower()} '{model.Title}'! Please log in to approve this request. Request Date: {model.DateTime.ToString("f")}" };
var message = new MimeMessage
{
@ -150,7 +158,7 @@ namespace PlexRequests.Services.Notification
$"Plex Requests: New issue for {model.Title}!",
$"Hello! The user '{model.User}' has reported a new issue {model.Body} for the title {model.Title}!",
model.ImgSrc);
var body = new BodyBuilder { HtmlBody = html, TextBody = "This email is only available on devices that support HTML." };
var body = new BodyBuilder { HtmlBody = html, TextBody = $"Hello! The user '{model.User}' has reported a new issue {model.Body} for the title {model.Title}!"};
var message = new MimeMessage
{
@ -164,6 +172,69 @@ namespace PlexRequests.Services.Notification
await Send(message, settings);
}
private async Task EmailAddedToRequestQueue(NotificationModel model, EmailNotificationSettings settings)
{
var email = new EmailBasicTemplate();
var html = email.LoadTemplate(
"Plex Requests: A request could not be added.",
$"Hello! The user '{model.User}' has requested {model.Title} but it could not be added. This has been added into the requests queue and will keep retrying",
model.ImgSrc);
var body = new BodyBuilder { HtmlBody = html, TextBody = $"Hello! The user '{model.User}' has requested {model.Title} but it could not be added. This has been added into the requests queue and will keep retrying" };
var message = new MimeMessage
{
Body = body.ToMessageBody(),
Subject = $"Plex Requests: A request could not be added"
};
message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender));
message.To.Add(new MailboxAddress(settings.RecipientEmail, settings.RecipientEmail));
await Send(message, settings);
}
private async Task EmailRequestDeclined(NotificationModel model, EmailNotificationSettings settings)
{
var email = new EmailBasicTemplate();
var html = email.LoadTemplate(
"Plex Requests: Your request has been declined",
$"Hello! Your request for {model.Title} has been declined, Sorry!",
model.ImgSrc);
var body = new BodyBuilder { HtmlBody = html, TextBody = $"Hello! Your request for {model.Title} has been declined, Sorry!", };
var message = new MimeMessage
{
Body = body.ToMessageBody(),
Subject = $"Plex Requests: Your request has been declined"
};
message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender));
message.To.Add(new MailboxAddress(model.UserEmail, model.UserEmail));
await Send(message, settings);
}
private async Task EmailRequestApproved(NotificationModel model, EmailNotificationSettings settings)
{
var email = new EmailBasicTemplate();
var html = email.LoadTemplate(
"Plex Requests: Your request has been approved!",
$"Hello! Your request for {model.Title} has been approved!",
model.ImgSrc);
var body = new BodyBuilder { HtmlBody = html, TextBody = $"Hello! Your request for {model.Title} has been approved!", };
var message = new MimeMessage
{
Body = body.ToMessageBody(),
Subject = $"Plex Requests: Your request has been approved!"
};
message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender));
message.To.Add(new MailboxAddress(model.UserEmail, model.UserEmail));
await Send(message, settings);
}
private async Task EmailAvailableRequest(NotificationModel model, EmailNotificationSettings settings)
{
if (!settings.EnableUserEmailNotifications)
@ -175,7 +246,7 @@ namespace PlexRequests.Services.Notification
$"Plex Requests: {model.Title} is now available!",
$"Hello! You requested {model.Title} on PlexRequests! This is now available on Plex! :)",
model.ImgSrc);
var body = new BodyBuilder { HtmlBody = html, TextBody = "This email is only available on devices that support HTML." };
var body = new BodyBuilder { HtmlBody = html, TextBody = $"Hello! You requested {model.Title} on PlexRequests! This is now available on Plex! :)" };
var message = new MimeMessage
{

@ -34,6 +34,8 @@ using NLog.Fluent;
using PlexRequests.Api;
using PlexRequests.Api.Interfaces;
using PlexRequests.Core.Models;
using PlexRequests.Core.Users;
using PlexRequests.Helpers.Permissions;
using PlexRequests.Services.Interfaces;
using PlexRequests.Store;
using PlexRequests.Store.Models;
@ -43,19 +45,21 @@ namespace PlexRequests.Services.Notification
{
public class NotificationEngine : INotificationEngine
{
public NotificationEngine(IPlexApi p, IRepository<UsersToNotify> repo, INotificationService service)
public NotificationEngine(IPlexApi p, IRepository<UsersToNotify> repo, INotificationService service, IUserHelper userHelper)
{
PlexApi = p;
UserNotifyRepo = repo;
Notification = service;
UserHelper = userHelper;
}
private IPlexApi PlexApi { get; }
private IRepository<UsersToNotify> UserNotifyRepo { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
private INotificationService Notification { get; }
private IUserHelper UserHelper { get; }
public async Task NotifyUsers(IEnumerable<RequestedModel> modelChanged, string apiKey)
public async Task NotifyUsers(IEnumerable<RequestedModel> modelChanged, string apiKey, NotificationType type)
{
try
{
@ -64,23 +68,37 @@ namespace PlexRequests.Services.Notification
var adminUsername = userAccount.Username ?? string.Empty;
var users = UserNotifyRepo.GetAll().ToList();
var users = UserHelper.GetUsersWithFeature(Features.RequestAddedNotification).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);
var selectedUsers = new List<string>();
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;
}
selectedUsers.Add(requestUser);
}
//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, model.PosterPath);
await PublishUserNotification(userAccount.Username, userAccount.Email, model.Title, model.PosterPath, type);
return;
}
var email = plexUser.User.FirstOrDefault(x => x.Username.Equals(user, StringComparison.CurrentCultureIgnoreCase));
if (email == null)
if (string.IsNullOrEmpty(email?.Email))
{
Log.Info("There is no email address for this Plex user, cannot send notification");
// We do not have a plex user that requested this!
@ -88,7 +106,7 @@ namespace PlexRequests.Services.Notification
}
Log.Info("Sending notification to: {0} at: {1}, for title: {2}", email.Username, email.Email, model.Title);
await PublishUserNotification(email.Username, email.Email, model.Title, model.PosterPath);
await PublishUserNotification(email.Username, email.Email, model.Title, model.PosterPath, type);
}
}
}
@ -98,7 +116,7 @@ namespace PlexRequests.Services.Notification
}
}
public async Task NotifyUsers(RequestedModel model, string apiKey)
public async Task NotifyUsers(RequestedModel model, string apiKey, NotificationType type)
{
try
{
@ -107,7 +125,7 @@ namespace PlexRequests.Services.Notification
var adminUsername = userAccount.Username ?? string.Empty;
var users = UserNotifyRepo.GetAll().ToList();
var users = UserHelper.GetUsersWithFeature(Features.RequestAddedNotification).ToList();
Log.Debug("Notifying Users Count {0}", users.Count);
var selectedUsers = users.Select(x => x.Username).Intersect(model.RequestedUsers, StringComparer.CurrentCultureIgnoreCase);
@ -117,7 +135,7 @@ namespace PlexRequests.Services.Notification
if (user.Equals(adminUsername, StringComparison.CurrentCultureIgnoreCase))
{
Log.Info("This user is the Plex server owner");
await PublishUserNotification(userAccount.Username, userAccount.Email, model.Title, model.PosterPath);
await PublishUserNotification(userAccount.Username, userAccount.Email, model.Title, model.PosterPath, type);
return;
}
@ -130,7 +148,7 @@ namespace PlexRequests.Services.Notification
}
Log.Info("Sending notification to: {0} at: {1}, for title: {2}", email.Username, email.Email, model.Title);
await PublishUserNotification(email.Username, email.Email, model.Title, model.PosterPath);
await PublishUserNotification(email.Username, email.Email, model.Title, model.PosterPath, type);
}
}
catch (Exception e)
@ -139,13 +157,13 @@ namespace PlexRequests.Services.Notification
}
}
private async Task PublishUserNotification(string username, string email, string title, string img)
private async Task PublishUserNotification(string username, string email, string title, string img, NotificationType type)
{
var notificationModel = new NotificationModel
{
User = username,
UserEmail = email,
NotificationType = NotificationType.RequestAvailable,
NotificationType = type,
Title = title,
ImgSrc = img
};

@ -81,6 +81,11 @@ namespace PlexRequests.Services.Notification
case NotificationType.Test:
await PushTestAsync(pushSettings);
break;
case NotificationType.RequestDeclined:
break;
case NotificationType.ItemAddedToFaultQueue:
await PushFaultQueue(model, pushSettings);
break;
default:
throw new ArgumentOutOfRangeException();
}
@ -125,6 +130,13 @@ namespace PlexRequests.Services.Notification
await Push(settings, message, pushTitle);
}
private async Task PushFaultQueue(NotificationModel model, PushbulletNotificationSettings settings)
{
var message = $"Hello! The user '{model.User}' has requested {model.Title} but it could not be added. This has been added into the requests queue and will keep retrying";
var pushTitle = $"Plex Requests: The {model.RequestType.GetString()?.ToLower()} {model.Title} has been requested but could not be added!";
await Push(settings, message, pushTitle);
}
private async Task Push(PushbulletNotificationSettings settings, string message, string title)
{
try

@ -81,6 +81,11 @@ namespace PlexRequests.Services.Notification
case NotificationType.Test:
await PushTestAsync(model, pushSettings);
break;
case NotificationType.RequestDeclined:
break;
case NotificationType.ItemAddedToFaultQueue:
await PushFaultQueue(model, pushSettings);
break;
default:
throw new ArgumentOutOfRangeException();
}
@ -122,6 +127,12 @@ namespace PlexRequests.Services.Notification
await Push(settings, message);
}
private async Task PushFaultQueue(NotificationModel model, PushoverNotificationSettings settings)
{
var message = $"Hello! The user '{model.User}' has requested {model.Title} but it could not be added. This has been added into the requests queue and will keep retrying";
await Push(settings, message);
}
private async Task Push(PushoverNotificationSettings settings, string message)
{
try

@ -88,6 +88,11 @@ namespace PlexRequests.Services.Notification
case NotificationType.Test:
await PushTest(pushSettings);
break;
case NotificationType.RequestDeclined:
break;
case NotificationType.ItemAddedToFaultQueue:
await PushFaultQueue(model, pushSettings);
break;
default:
throw new ArgumentOutOfRangeException();
}
@ -111,6 +116,12 @@ namespace PlexRequests.Services.Notification
await Push(settings, message);
}
private async Task PushFaultQueue(NotificationModel model, SlackNotificationSettings settings)
{
var message = $"Hello! The user '{model.User}' has requested {model.Title} but it could not be added. This has been added into the requests queue and will keep retrying";
await Push(settings, message);
}
private async Task Push(SlackNotificationSettings config, string message)
{
try

@ -36,6 +36,14 @@
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.Build.Framework" />
<Reference Include="Nancy, Version=1.4.2.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Nancy.1.4.3\lib\net40\Nancy.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.3.6\lib\net45\NLog.dll</HintPath>
<Private>True</Private>
@ -81,18 +89,22 @@
<Compile Include="Interfaces\IJobRecord.cs" />
<Compile Include="Interfaces\INotificationEngine.cs" />
<Compile Include="Jobs\HtmlTemplateGenerator.cs" />
<Compile Include="Jobs\IPlexContentCacher.cs" />
<Compile Include="Jobs\IRecentlyAdded.cs" />
<Compile Include="Jobs\JobRecord.cs" />
<Compile Include="Jobs\JobNames.cs" />
<Compile Include="Jobs\PlexContentCacher.cs" />
<Compile Include="Jobs\PlexEpisodeCacher.cs" />
<Compile Include="Jobs\RecentlyAdded.cs" />
<Compile Include="Jobs\StoreBackup.cs" />
<Compile Include="Jobs\PlexUserChecker.cs" />
<Compile Include="Jobs\StoreCleanup.cs" />
<Compile Include="Jobs\CouchPotatoCacher.cs" />
<Compile Include="Jobs\PlexAvailabilityChecker.cs" />
<Compile Include="Jobs\SickRageCacher.cs" />
<Compile Include="Jobs\SonarrCacher.cs" />
<Compile Include="Jobs\Templates\RecentlyAddedTemplate.cs" />
<Compile Include="Jobs\FaultQueueHandler.cs" />
<Compile Include="Jobs\UserRequestLimitResetter.cs" />
<Compile Include="Models\PlexAlbum.cs" />
<Compile Include="Models\PlexEpisodeModel.cs" />

@ -5,6 +5,7 @@
<package id="Dapper" version="1.50.0-beta8" targetFramework="net45" />
<package id="MailKit" version="1.2.21" targetFramework="net45" requireReinstallation="True" />
<package id="MimeKit" version="1.2.22" targetFramework="net45" />
<package id="Nancy" version="1.4.3" targetFramework="net45" />
<package id="NLog" version="4.3.6" targetFramework="net45" />
<package id="Quartz" version="2.3.3" targetFramework="net45" />
</packages>

@ -0,0 +1,52 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: PlexContent.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.Data.Linq.Mapping;
namespace PlexRequests.Store.Models.Plex
{
[Table(Name = nameof(PlexContent))]
public class PlexContent : Entity
{
public string Title { get; set; }
public string ReleaseYear { get; set; }
public string ProviderId { get; set; }
public PlexMediaType Type { get; set; }
public string Url { get; set; }
/// <summary>
/// Only used for TV Shows
/// </summary>
public byte[] Seasons { get; set; }
/// <summary>
/// Only used for Albums
/// </summary>
public string Artist { get; set; }
}
}

@ -0,0 +1,35 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: PlexMediaType .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.Store.Models.Plex
{
public enum PlexMediaType
{
Movie = 0,
Show = 1,
Artist = 2
}
}

@ -24,11 +24,21 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using Dapper.Contrib.Extensions;
namespace PlexRequests.Store.Models
{
[Table(nameof(PlexUsers))]
public class PlexUsers : Entity
{
public int PlexUserId { get; set; }
public string PlexUserId { get; set; }
public string UserAlias { get; set; }
public int Permissions { get; set; }
public int Features { get; set; }
public string Username { get; set; }
public string EmailAddress { get; set; }
public string LoginId { get; set; }
}
}

@ -25,18 +25,28 @@
// ************************************************************************/
#endregion
using System;
using Dapper.Contrib.Extensions;
namespace PlexRequests.Store.Models
{
[Table("RequestQueue")]
[Table("RequestFaultQueue")]
public class RequestQueue : Entity
{
public int PrimaryIdentifier { get; set; }
public string PrimaryIdentifier { get; set; }
public RequestType Type { get; set; }
public byte[] Content { get; set; }
public FaultType FaultType { get; set; }
public DateTime? LastRetry { get; set; }
public string Message { get; set; }
}
public enum FaultType
{
RequestFault,
MissingInformation
}
}

@ -35,5 +35,6 @@ namespace PlexRequests.Store.Models
{
public string Name { get; set; }
public DateTime LastRun { get; set; }
public bool Running { get; set; }
}
}

@ -69,6 +69,8 @@
<Compile Include="Models\PlexEpisodes.cs" />
<Compile Include="Models\PlexUsers.cs" />
<Compile Include="Models\Plex\MetadataItems.cs" />
<Compile Include="Models\Plex\PlexContent.cs" />
<Compile Include="Models\Plex\PlexMediaType .cs" />
<Compile Include="Models\RequestQueue.cs" />
<Compile Include="Models\ScheduledJobs.cs" />
<Compile Include="Models\RequestLimit.cs" />
@ -86,6 +88,7 @@
<Compile Include="Repository\SettingsJsonRepository.cs" />
<Compile Include="Repository\RequestJsonRepository.cs" />
<Compile Include="Repository\GenericRepository.cs" />
<Compile Include="Repository\PlexUserRepository.cs" />
<Compile Include="Repository\UserRepository.cs" />
<Compile Include="RequestedModel.cs" />
<Compile Include="UserEntity.cs" />

@ -0,0 +1,117 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: UserRepository.cs
// Created By:
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;
using Dapper;
using Dapper.Contrib.Extensions;
using PlexRequests.Helpers;
using PlexRequests.Store.Models;
namespace PlexRequests.Store.Repository
{
public class PlexUserRepository : BaseGenericRepository<PlexUsers>, IPlexUserRepository
{
public PlexUserRepository(ISqliteConfiguration config, ICacheProvider cache) : base(config,cache)
{
DbConfig = config;
Cache = cache;
}
private ISqliteConfiguration DbConfig { get; }
private ICacheProvider Cache { get; }
private IDbConnection Db => DbConfig.DbConnection();
public PlexUsers GetUser(string userGuid)
{
var sql = @"SELECT * FROM PlexUsers
WHERE PlexUserId = @UserGuid";
return Db.QueryFirstOrDefault<PlexUsers>(sql, new {UserGuid = userGuid});
}
public PlexUsers GetUserByUsername(string username)
{
var sql = @"SELECT * FROM PlexUsers
WHERE Username = @UserName";
return Db.QueryFirstOrDefault<PlexUsers>(sql, new {UserName = username});
}
public async Task<PlexUsers> GetUserAsync(string userguid)
{
var sql = @"SELECT * FROM PlexUsers
WHERE PlexUserId = @UserGuid";
return await Db.QueryFirstOrDefaultAsync<PlexUsers>(sql, new {UserGuid = userguid});
}
#region abstract implimentation
[Obsolete]
public override PlexUsers Get(string id)
{
throw new System.NotImplementedException();
}
[Obsolete]
public override Task<PlexUsers> GetAsync(int id)
{
throw new System.NotImplementedException();
}
[Obsolete]
public override PlexUsers Get(int id)
{
throw new System.NotImplementedException();
}
[Obsolete]
public override Task<PlexUsers> GetAsync(string id)
{
throw new System.NotImplementedException();
}
#endregion
}
public interface IPlexUserRepository
{
PlexUsers GetUser(string userGuid);
PlexUsers GetUserByUsername(string username);
Task<PlexUsers> GetUserAsync(string userguid);
IEnumerable<PlexUsers> Custom(Func<IDbConnection, IEnumerable<PlexUsers>> func);
long Insert(PlexUsers entity);
void Delete(PlexUsers entity);
IEnumerable<PlexUsers> GetAll();
bool UpdateAll(IEnumerable<PlexUsers> entity);
bool Update(PlexUsers entity);
Task<IEnumerable<PlexUsers>> GetAllAsync();
Task<bool> UpdateAsync(PlexUsers users);
Task<int> InsertAsync(PlexUsers users);
}
}

@ -105,6 +105,9 @@ namespace PlexRequests.Store.Repository
IEnumerable<UsersModel> Custom(Func<IDbConnection, IEnumerable<UsersModel>> func);
long Insert(UsersModel entity);
void Delete(UsersModel entity);
IEnumerable<UsersModel> GetAll();
bool UpdateAll(IEnumerable<UsersModel> entity);
bool Update(UsersModel entity);
}
}

@ -80,7 +80,8 @@ CREATE TABLE IF NOT EXISTS ScheduledJobs
(
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Name varchar(100) NOT NULL,
LastRun varchar(100) NOT NULL
LastRun varchar(100) NOT NULL,
Running INTEGER
);
CREATE UNIQUE INDEX IF NOT EXISTS ScheduledJobs_Id ON ScheduledJobs (Id);
@ -113,8 +114,13 @@ CREATE UNIQUE INDEX IF NOT EXISTS RequestLimit_Id ON RequestLimit (Id);
CREATE TABLE IF NOT EXISTS PlexUsers
(
Id INTEGER PRIMARY KEY AUTOINCREMENT,
PlexUserId INTEGER NOT NULL,
UserAlias varchar(100) NOT NULL
PlexUserId varchar(100) NOT NULL,
UserAlias varchar(100) NOT NULL,
Permissions INTEGER,
Features INTEGER,
Username VARCHAR(100),
EmailAddress VARCHAR(100),
LoginId VARCHAR(100)
);
CREATE UNIQUE INDEX IF NOT EXISTS PlexUsers_Id ON PlexUsers (Id);
@ -133,13 +139,29 @@ CREATE UNIQUE INDEX IF NOT EXISTS PlexEpisodes_Id ON PlexEpisodes (Id);
CREATE INDEX IF NOT EXISTS PlexEpisodes_ProviderId ON PlexEpisodes (ProviderId);
CREATE TABLE IF NOT EXISTS RequestQueue
CREATE TABLE IF NOT EXISTS RequestFaultQueue
(
Id INTEGER PRIMARY KEY AUTOINCREMENT,
PrimaryIdentifier INTEGER NOT NULL,
PrimaryIdentifier VARCHAR(100) NOT NULL,
Type INTEGER NOT NULL,
Content BLOB NOT NULL
FaultType INTEGER NOT NULL,
Content BLOB NOT NULL,
LastRetry VARCHAR(100),
Description VARCHAR(100)
);
CREATE UNIQUE INDEX IF NOT EXISTS PlexUsers_Id ON PlexUsers (Id);
CREATE TABLE IF NOT EXISTS PlexContent
(
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Title VARCHAR(100) NOT NULL,
ReleaseYear VARCHAR(100) NOT NULL,
ProviderId VARCHAR(100) NOT NULL,
Url VARCHAR(100) NOT NULL,
Artist VARCHAR(100),
Seasons BLOB,
Type INTEGER NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS PlexContent_Id ON PlexContent (Id);
COMMIT;

@ -1,164 +1,167 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: SearchModuleTests.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
//#region Copyright
//// /************************************************************************
//// Copyright (c) 2016 Jamie Rees
//// File: SearchModuleTests.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 Moq;
using NUnit.Framework;
using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.Plex;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Analytics;
using PlexRequests.Services.Interfaces;
using PlexRequests.Services.Jobs;
using PlexRequests.Store;
using PlexRequests.Store.Models;
using PlexRequests.Store.Repository;
using PlexRequests.UI.Modules;
using Ploeh.AutoFixture;
//using System;
//using System.Collections.Generic;
//using System.Linq;
//using Moq;
//using NUnit.Framework;
//using PlexRequests.Api.Interfaces;
//using PlexRequests.Api.Models.Plex;
//using PlexRequests.Core;
//using PlexRequests.Core.Queue;
//using PlexRequests.Core.SettingModels;
//using PlexRequests.Helpers;
//using PlexRequests.Helpers.Analytics;
//using PlexRequests.Services.Interfaces;
//using PlexRequests.Services.Jobs;
//using PlexRequests.Store;
//using PlexRequests.Store.Models;
//using PlexRequests.Store.Repository;
//using PlexRequests.UI.Modules;
//using Ploeh.AutoFixture;
namespace PlexRequests.UI.Tests
{
[TestFixture]
public class SearchModuleTests
{
private Mock<ISettingsService<HeadphonesSettings>> _headphonesSettings;
private Mock<INotificationService> _notificationService;
private Mock<ISettingsService<SickRageSettings>> _sickRageSettingsMock;
private Mock<ICouchPotatoApi> _cpApi;
private Mock<ISettingsService<SonarrSettings>> _sonarrSettingsMock;
private Mock<ISonarrApi> _sonarrApiMock;
private Mock<ISettingsService<PlexSettings>> _plexSettingsMock;
private Mock<ISettingsService<CouchPotatoSettings>> _cpMock;
private Mock<ISettingsService<PlexRequestSettings>> _plexRequestMock;
private Mock<ISettingsService<AuthenticationSettings>> _authMock;
private Mock<IAnalytics> _analytics;
private Mock<IAvailabilityChecker> _availabilityMock;
private Mock<IRequestService> _rServiceMock;
private Mock<ISickRageApi> _srApi;
private Mock<IMusicBrainzApi> _music;
private Mock<IHeadphonesApi> _hpAPi;
private Mock<ICouchPotatoCacher> _cpCache;
private Mock<ISonarrCacher> _sonarrCache;
private Mock<ISickRageCacher> _srCache;
private Mock<IPlexApi> _plexApi;
private Mock<IRepository<UsersToNotify>> _userRepo;
private Mock<ISettingsService<EmailNotificationSettings>> _emailSettings;
private Mock<IIssueService> _issueService;
private Mock<ICacheProvider> _cache;
private Mock<IRepository<RequestLimit>> RequestLimitRepo { get; set; }
private SearchModule Search { get; set; }
private readonly Fixture F = new Fixture();
//namespace PlexRequests.UI.Tests
//{
// [TestFixture]
// public class SearchModuleTests
// {
// private Mock<ISettingsService<HeadphonesSettings>> _headphonesSettings;
// private Mock<INotificationService> _notificationService;
// private Mock<ISettingsService<SickRageSettings>> _sickRageSettingsMock;
// private Mock<ICouchPotatoApi> _cpApi;
// private Mock<ISettingsService<SonarrSettings>> _sonarrSettingsMock;
// private Mock<ISonarrApi> _sonarrApiMock;
// private Mock<ISettingsService<PlexSettings>> _plexSettingsMock;
// private Mock<ISettingsService<CouchPotatoSettings>> _cpMock;
// private Mock<ISettingsService<PlexRequestSettings>> _plexRequestMock;
// private Mock<ISettingsService<AuthenticationSettings>> _authMock;
// private Mock<IAnalytics> _analytics;
// private Mock<IAvailabilityChecker> _availabilityMock;
// private Mock<IRequestService> _rServiceMock;
// private Mock<ISickRageApi> _srApi;
// private Mock<IMusicBrainzApi> _music;
// private Mock<IHeadphonesApi> _hpAPi;
// private Mock<ICouchPotatoCacher> _cpCache;
// private Mock<ISonarrCacher> _sonarrCache;
// private Mock<ISickRageCacher> _srCache;
// private Mock<IPlexApi> _plexApi;
// private Mock<IRepository<UsersToNotify>> _userRepo;
// private Mock<ISettingsService<EmailNotificationSettings>> _emailSettings;
// private Mock<IIssueService> _issueService;
// private Mock<ICacheProvider> _cache;
// private Mock<ITransientFaultQueue> _faultQueue;
// private Mock<IRepository<RequestLimit>> RequestLimitRepo { get; set; }
// private SearchModule Search { get; set; }
// private readonly Fixture F = new Fixture();
[Test]
public void CheckNoRequestLimitTest()
{
var settings = new PlexRequestSettings { AlbumWeeklyRequestLimit = 0, MovieWeeklyRequestLimit = 2, TvWeeklyRequestLimit = 0 };
var result = Search.CheckRequestLimit(settings, RequestType.Movie).Result;
// [Test]
// public void CheckNoRequestLimitTest()
// {
// var settings = new PlexRequestSettings { AlbumWeeklyRequestLimit = 0, MovieWeeklyRequestLimit = 2, TvWeeklyRequestLimit = 0 };
// var result = Search.CheckRequestLimit(settings, RequestType.Movie).Result;
Assert.That(result, Is.True);
RequestLimitRepo.Verify(x => x.GetAllAsync(), Times.Once);
}
// Assert.That(result, Is.True);
// RequestLimitRepo.Verify(x => x.GetAllAsync(), Times.Once);
// }
[TestCaseSource(nameof(MovieLimitData))]
public bool CheckMovieLimitTest(int requestCount)
{
var users = F.CreateMany<RequestLimit>().ToList();
users.Add(new RequestLimit { Username = "", RequestCount = requestCount, RequestType = RequestType.Movie});
RequestLimitRepo.Setup(x => x.GetAllAsync()).ReturnsAsync(users);
var settings = new PlexRequestSettings { AlbumWeeklyRequestLimit = 0, MovieWeeklyRequestLimit = 5, TvWeeklyRequestLimit = 0 };
var result = Search.CheckRequestLimit(settings, RequestType.Movie).Result;
// [TestCaseSource(nameof(MovieLimitData))]
// public bool CheckMovieLimitTest(int requestCount)
// {
// var users = F.CreateMany<RequestLimit>().ToList();
// users.Add(new RequestLimit { Username = "", RequestCount = requestCount, RequestType = RequestType.Movie});
// RequestLimitRepo.Setup(x => x.GetAllAsync()).ReturnsAsync(users);
// var settings = new PlexRequestSettings { AlbumWeeklyRequestLimit = 0, MovieWeeklyRequestLimit = 5, TvWeeklyRequestLimit = 0 };
// var result = Search.CheckRequestLimit(settings, RequestType.Movie).Result;
RequestLimitRepo.Verify(x => x.GetAllAsync(), Times.Once);
// RequestLimitRepo.Verify(x => x.GetAllAsync(), Times.Once);
return result;
}
// return result;
// }
private static IEnumerable<TestCaseData> MovieLimitData
{
get
{
yield return new TestCaseData(1).Returns(true).SetName("1 Request of 5");
yield return new TestCaseData(2).Returns(true).SetName("2 Request of 5");
yield return new TestCaseData(3).Returns(true).SetName("3 Request of 5");
yield return new TestCaseData(4).Returns(true).SetName("4 Request of 5");
yield return new TestCaseData(5).Returns(false).SetName("5 Request of 5");
yield return new TestCaseData(6).Returns(false).SetName("6 Request of 5");
yield return new TestCaseData(0).Returns(true).SetName("0 Request of 5");
}
}
// private static IEnumerable<TestCaseData> MovieLimitData
// {
// get
// {
// yield return new TestCaseData(1).Returns(true).SetName("1 Request of 5");
// yield return new TestCaseData(2).Returns(true).SetName("2 Request of 5");
// yield return new TestCaseData(3).Returns(true).SetName("3 Request of 5");
// yield return new TestCaseData(4).Returns(true).SetName("4 Request of 5");
// yield return new TestCaseData(5).Returns(false).SetName("5 Request of 5");
// yield return new TestCaseData(6).Returns(false).SetName("6 Request of 5");
// yield return new TestCaseData(0).Returns(true).SetName("0 Request of 5");
// }
// }
[SetUp]
public void Setup()
{
_authMock = new Mock<Core.ISettingsService<AuthenticationSettings>>();
_plexRequestMock = new Mock<ISettingsService<PlexRequestSettings>>();
_plexRequestMock.Setup(x => x.GetSettings()).Returns(new PlexRequestSettings());
_cpMock = new Mock<Core.ISettingsService<CouchPotatoSettings>>();
_plexSettingsMock = new Mock<Core.ISettingsService<PlexSettings>>();
_sonarrApiMock = new Mock<ISonarrApi>();
_sonarrSettingsMock = new Mock<Core.ISettingsService<SonarrSettings>>();
_cpApi = new Mock<ICouchPotatoApi>();
_sickRageSettingsMock = new Mock<Core.ISettingsService<SickRageSettings>>();
_notificationService = new Mock<INotificationService>();
_headphonesSettings = new Mock<Core.ISettingsService<HeadphonesSettings>>();
_cache = new Mock<ICacheProvider>();
// [SetUp]
// public void Setup()
// {
// _authMock = new Mock<Core.ISettingsService<AuthenticationSettings>>();
// _plexRequestMock = new Mock<ISettingsService<PlexRequestSettings>>();
// _plexRequestMock.Setup(x => x.GetSettings()).Returns(new PlexRequestSettings());
// _cpMock = new Mock<Core.ISettingsService<CouchPotatoSettings>>();
// _plexSettingsMock = new Mock<Core.ISettingsService<PlexSettings>>();
// _sonarrApiMock = new Mock<ISonarrApi>();
// _sonarrSettingsMock = new Mock<Core.ISettingsService<SonarrSettings>>();
// _cpApi = new Mock<ICouchPotatoApi>();
// _sickRageSettingsMock = new Mock<Core.ISettingsService<SickRageSettings>>();
// _notificationService = new Mock<INotificationService>();
// _headphonesSettings = new Mock<Core.ISettingsService<HeadphonesSettings>>();
// _cache = new Mock<ICacheProvider>();
_analytics = new Mock<IAnalytics>();
_availabilityMock = new Mock<IAvailabilityChecker>();
_rServiceMock = new Mock<IRequestService>();
_srApi = new Mock<ISickRageApi>();
_music = new Mock<IMusicBrainzApi>();
_hpAPi = new Mock<IHeadphonesApi>();
_cpCache = new Mock<ICouchPotatoCacher>();
_sonarrCache = new Mock<ISonarrCacher>();
_srCache = new Mock<ISickRageCacher>();
_plexApi = new Mock<IPlexApi>();
_userRepo = new Mock<IRepository<UsersToNotify>>();
RequestLimitRepo = new Mock<IRepository<RequestLimit>>();
_emailSettings = new Mock<ISettingsService<EmailNotificationSettings>>();
_issueService = new Mock<IIssueService>();
CreateModule();
}
// _analytics = new Mock<IAnalytics>();
// _availabilityMock = new Mock<IAvailabilityChecker>();
// _rServiceMock = new Mock<IRequestService>();
// _srApi = new Mock<ISickRageApi>();
// _music = new Mock<IMusicBrainzApi>();
// _hpAPi = new Mock<IHeadphonesApi>();
// _cpCache = new Mock<ICouchPotatoCacher>();
// _sonarrCache = new Mock<ISonarrCacher>();
// _srCache = new Mock<ISickRageCacher>();
// _plexApi = new Mock<IPlexApi>();
// _userRepo = new Mock<IRepository<UsersToNotify>>();
// RequestLimitRepo = new Mock<IRepository<RequestLimit>>();
// _emailSettings = new Mock<ISettingsService<EmailNotificationSettings>>();
// _issueService = new Mock<IIssueService>();
// _faultQueue = new Mock<ITransientFaultQueue>();
// CreateModule();
// }
private void CreateModule()
{
Search = new SearchModule(_cache.Object, _cpMock.Object, _plexRequestMock.Object, _availabilityMock.Object,
_rServiceMock.Object, _sonarrApiMock.Object, _sonarrSettingsMock.Object,
_sickRageSettingsMock.Object, _cpApi.Object, _srApi.Object, _notificationService.Object,
_music.Object, _hpAPi.Object, _headphonesSettings.Object, _cpCache.Object, _sonarrCache.Object,
_srCache.Object, _plexApi.Object, _plexSettingsMock.Object, _authMock.Object,
_userRepo.Object, _emailSettings.Object, _issueService.Object, _analytics.Object, RequestLimitRepo.Object);
}
// private void CreateModule()
// {
// Search = new SearchModule(_cache.Object, _cpMock.Object, _plexRequestMock.Object, _availabilityMock.Object,
// _rServiceMock.Object, _sonarrApiMock.Object, _sonarrSettingsMock.Object,
// _sickRageSettingsMock.Object, _cpApi.Object, _srApi.Object, _notificationService.Object,
// _music.Object, _hpAPi.Object, _headphonesSettings.Object, _cpCache.Object, _sonarrCache.Object,
// _srCache.Object, _plexApi.Object, _plexSettingsMock.Object, _authMock.Object,
// _userRepo.Object, _emailSettings.Object, _issueService.Object, _analytics.Object, RequestLimitRepo.Object, _faultQueue.Object);
// }
}
}
// }
//}

@ -35,6 +35,7 @@ using NUnit.Framework;
using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.Sonarr;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Store;
using PlexRequests.UI.Helpers;
@ -146,32 +147,6 @@ namespace PlexRequests.UI.Tests
true, It.IsAny<bool>()), Times.Once);
}
[Test]
public async Task RequestEpisodesWithExistingSeriesTest()
{
var episodesReturned = new List<SonarrEpisodes>
{
new SonarrEpisodes {episodeNumber = 1, seasonNumber = 2, monitored = false, id=22}
};
SonarrMock.Setup(x => x.GetEpisodes(It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<Uri>())).Returns(episodesReturned);
SonarrMock.Setup(x => x.GetEpisode("22", It.IsAny<string>(), It.IsAny<Uri>())).Returns(new SonarrEpisode {id=22});
Sender = new TvSender(SonarrMock.Object, SickrageMock.Object);
var model = new RequestedModel
{
Episodes = new List<EpisodesModel> { new EpisodesModel { EpisodeNumber = 1, SeasonNumber = 2 } }
};
var series = new Series();
await Sender.RequestEpisodesWithExistingSeries(model, series, GetSonarrSettings());
SonarrMock.Verify(x => x.UpdateEpisode(It.Is<SonarrEpisode>(e => e.monitored), It.IsAny<string>(), It.IsAny<Uri>()));
SonarrMock.Verify(x => x.GetEpisode("22", It.IsAny<string>(), It.IsAny<Uri>()),Times.Once);
SonarrMock.Verify(x => x.SearchForEpisodes(It.IsAny<int[]>(), It.IsAny<string>(), It.IsAny<Uri>()), Times.Once);
}
private SonarrSettings GetSonarrSettings()
{

@ -42,6 +42,7 @@ using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.Plex;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Analytics;
using PlexRequests.UI.Models;
using PlexRequests.UI.Modules;

@ -0,0 +1,93 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: CustomAuthenticationConfiguration.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 Nancy.Cryptography;
using PlexRequests.Store.Repository;
namespace PlexRequests.UI.Authentication
{
public class CustomAuthenticationConfiguration
{
internal const string DefaultRedirectQuerystringKey = "returnUrl";
/// <summary>
/// Gets or sets the forms authentication query string key for storing the return url
/// </summary>
public string RedirectQuerystringKey { get; set; }
/// <summary>
/// Gets or sets the redirect url for pages that require authentication
/// </summary>
public string RedirectUrl { get; set; }
/// <summary>Gets or sets the username/identifier mapper</summary>
public IUserRepository LocalUserRepository { get; set; }
public IPlexUserRepository PlexUserRepository { get; set; }
/// <summary>Gets or sets RequiresSSL property</summary>
/// <value>The flag that indicates whether SSL is required</value>
public bool RequiresSSL { get; set; }
/// <summary>
/// Gets or sets whether to redirect to login page during unauthorized access.
/// </summary>
public bool DisableRedirect { get; set; }
/// <summary>Gets or sets the domain of the auth cookie</summary>
public string Domain { get; set; }
/// <summary>Gets or sets the path of the auth cookie</summary>
public string Path { get; set; }
/// <summary>Gets or sets the cryptography configuration</summary>
public CryptographyConfiguration CryptographyConfiguration { get; set; }
/// <summary>
/// Gets a value indicating whether the configuration is valid or not.
/// </summary>
public virtual bool IsValid => (this.DisableRedirect || !string.IsNullOrEmpty(this.RedirectUrl)) && (this.LocalUserRepository != null && PlexUserRepository != null && this.CryptographyConfiguration != null) && (this.CryptographyConfiguration.EncryptionProvider != null && this.CryptographyConfiguration.HmacProvider != null);
/// <summary>
/// Initializes a new instance of the <see cref="T:Nancy.Authentication.Forms.FormsAuthenticationConfiguration" /> class.
/// </summary>
public CustomAuthenticationConfiguration()
: this(CryptographyConfiguration.Default)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="T:Nancy.Authentication.Forms.FormsAuthenticationConfiguration" /> class.
/// </summary>
/// <param name="cryptographyConfiguration">Cryptography configuration</param>
public CustomAuthenticationConfiguration(CryptographyConfiguration cryptographyConfiguration)
{
this.CryptographyConfiguration = cryptographyConfiguration;
this.RedirectQuerystringKey = "returnUrl";
}
}
}

@ -0,0 +1,409 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: CustomAuthenticationProvider.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.Authentication.Forms;
using Nancy.Bootstrapper;
using Nancy.Cookies;
using Nancy.Cryptography;
using Nancy.Extensions;
using Nancy.Helpers;
using Nancy.Security;
using PlexRequests.Core;
using PlexRequests.Helpers;
namespace PlexRequests.UI.Authentication
{
public class CustomAuthenticationProvider
{
private static string formsAuthenticationCookieName = "_ncfa";
private static CustomAuthenticationConfiguration currentConfiguration;
/// <summary>Gets or sets the forms authentication cookie name</summary>
public static string FormsAuthenticationCookieName
{
get
{
return CustomAuthenticationProvider.formsAuthenticationCookieName;
}
set
{
CustomAuthenticationProvider.formsAuthenticationCookieName = value;
}
}
/// <summary>Enables forms authentication for the application</summary>
/// <param name="pipelines">Pipelines to add handlers to (usually "this")</param>
/// <param name="configuration">Forms authentication configuration</param>
public static void Enable(IPipelines pipelines, CustomAuthenticationConfiguration configuration)
{
if (pipelines == null)
throw new ArgumentNullException("pipelines");
if (configuration == null)
throw new ArgumentNullException("configuration");
if (!configuration.IsValid)
throw new ArgumentException("Configuration is invalid", "configuration");
CustomAuthenticationProvider.currentConfiguration = configuration;
pipelines.BeforeRequest.AddItemToStartOfPipeline(CustomAuthenticationProvider.GetLoadAuthenticationHook(configuration));
if (configuration.DisableRedirect)
return;
pipelines.AfterRequest.AddItemToEndOfPipeline(CustomAuthenticationProvider.GetRedirectToLoginHook(configuration));
}
/// <summary>Enables forms authentication for a module</summary>
/// <param name="module">Module to add handlers to (usually "this")</param>
/// <param name="configuration">Forms authentication configuration</param>
public static void Enable(INancyModule module, CustomAuthenticationConfiguration configuration)
{
if (module == null)
throw new ArgumentNullException("module");
if (configuration == null)
throw new ArgumentNullException("configuration");
if (!configuration.IsValid)
throw new ArgumentException("Configuration is invalid", "configuration");
module.RequiresAuthentication();
CustomAuthenticationProvider.currentConfiguration = configuration;
module.Before.AddItemToStartOfPipeline(CustomAuthenticationProvider.GetLoadAuthenticationHook(configuration));
if (configuration.DisableRedirect)
return;
module.After.AddItemToEndOfPipeline(CustomAuthenticationProvider.GetRedirectToLoginHook(configuration));
}
/// <summary>
/// Creates a response that sets the authentication cookie and redirects
/// the user back to where they came from.
/// </summary>
/// <param name="context">Current context</param>
/// <param name="userIdentifier">User identifier guid</param>
/// <param name="cookieExpiry">Optional expiry date for the cookie (for 'Remember me')</param>
/// <param name="fallbackRedirectUrl">Url to redirect to if none in the querystring</param>
/// <returns>Nancy response with redirect.</returns>
public static Response UserLoggedInRedirectResponse(NancyContext context, Guid userIdentifier, DateTime? cookieExpiry = null, string fallbackRedirectUrl = null)
{
var redirectUrl = fallbackRedirectUrl;
if (string.IsNullOrEmpty(redirectUrl))
{
redirectUrl = context.Request.Url.BasePath;
}
if (string.IsNullOrEmpty(redirectUrl))
{
redirectUrl = "/";
}
string redirectQuerystringKey = GetRedirectQuerystringKey(currentConfiguration);
if (context.Request.Query[redirectQuerystringKey].HasValue)
{
var queryUrl = (string)context.Request.Query[redirectQuerystringKey];
if (context.IsLocalUrl(queryUrl))
{
redirectUrl = queryUrl;
}
}
var response = context.GetRedirect(redirectUrl);
var authenticationCookie = BuildCookie(userIdentifier, cookieExpiry, currentConfiguration);
response.WithCookie(authenticationCookie);
return response;
}
/// <summary>
/// Logs the user in.
/// </summary>
/// <param name="userIdentifier">User identifier guid</param>
/// <param name="cookieExpiry">Optional expiry date for the cookie (for 'Remember me')</param>
/// <returns>Nancy response with status <see cref="HttpStatusCode.OK"/></returns>
public static Response UserLoggedInResponse(Guid userIdentifier, DateTime? cookieExpiry = null)
{
var response =
(Response)HttpStatusCode.OK;
var authenticationCookie =
BuildCookie(userIdentifier, cookieExpiry, currentConfiguration);
response.WithCookie(authenticationCookie);
return response;
}
/// <summary>
/// Logs the user out and redirects them to a URL
/// </summary>
/// <param name="context">Current context</param>
/// <param name="redirectUrl">URL to redirect to</param>
/// <returns>Nancy response</returns>
public static Response LogOutAndRedirectResponse(NancyContext context, string redirectUrl)
{
var response = context.GetRedirect(redirectUrl);
var authenticationCookie = BuildLogoutCookie(currentConfiguration);
response.WithCookie(authenticationCookie);
return response;
}
/// <summary>
/// Logs the user out.
/// </summary>
/// <returns>Nancy response</returns>
public static Response LogOutResponse()
{
var response =
(Response)HttpStatusCode.OK;
var authenticationCookie =
BuildLogoutCookie(currentConfiguration);
response.WithCookie(authenticationCookie);
return response;
}
/// <summary>
/// Gets the pre request hook for loading the authenticated user's details
/// from the cookie.
/// </summary>
/// <param name="configuration">Forms authentication configuration to use</param>
/// <returns>Pre request hook delegate</returns>
private static Func<NancyContext, Response> GetLoadAuthenticationHook(CustomAuthenticationConfiguration configuration)
{
if (configuration == null)
{
throw new ArgumentNullException("configuration");
}
return context =>
{
var userGuid = GetAuthenticatedUserFromCookie(context, configuration);
if (userGuid != Guid.Empty)
{
var identity = new UserIdentity();
var plexUsers = configuration.PlexUserRepository.GetAll();
var plexUser = plexUsers.FirstOrDefault(x => Guid.Parse(x.LoginId) == userGuid);
if (plexUser != null)
{
identity.UserName = plexUser.Username;
}
var localUsers = configuration.LocalUserRepository.GetAll();
var localUser = localUsers.FirstOrDefault(x => Guid.Parse(x.UserGuid) == userGuid);
if (localUser != null)
{
identity.UserName = localUser.UserName;
}
context.CurrentUser = identity;
}
return null;
};
}
/// <summary>
/// Gets the post request hook for redirecting to the login page
/// </summary>
/// <param name="configuration">Forms authentication configuration to use</param>
/// <returns>Post request hook delegate</returns>
private static Action<NancyContext> GetRedirectToLoginHook(CustomAuthenticationConfiguration configuration)
{
return context =>
{
if (context.Response.StatusCode == HttpStatusCode.Unauthorized)
{
string redirectQuerystringKey = GetRedirectQuerystringKey(configuration);
context.Response = context.GetRedirect(
string.Format("{0}?{1}={2}",
configuration.RedirectUrl,
redirectQuerystringKey,
context.ToFullPath("~" + context.Request.Path + HttpUtility.UrlEncode(context.Request.Url.Query))));
}
};
}
/// <summary>
/// Gets the authenticated user GUID from the incoming request cookie if it exists
/// and is valid.
/// </summary>
/// <param name="context">Current context</param>
/// <param name="configuration">Current configuration</param>
/// <returns>Returns user guid, or Guid.Empty if not present or invalid</returns>
private static Guid GetAuthenticatedUserFromCookie(NancyContext context, CustomAuthenticationConfiguration configuration)
{
if (!context.Request.Cookies.ContainsKey(formsAuthenticationCookieName))
{
return Guid.Empty;
}
var cookieValueEncrypted = context.Request.Cookies[formsAuthenticationCookieName];
if (string.IsNullOrEmpty(cookieValueEncrypted))
{
return Guid.Empty;
}
var cookieValue = DecryptAndValidateAuthenticationCookie(cookieValueEncrypted, configuration);
Guid returnGuid;
if (string.IsNullOrEmpty(cookieValue) || !Guid.TryParse(cookieValue, out returnGuid))
{
return Guid.Empty;
}
return returnGuid;
}
/// <summary>
/// Build the forms authentication cookie
/// </summary>
/// <param name="userIdentifier">Authenticated user identifier</param>
/// <param name="cookieExpiry">Optional expiry date for the cookie (for 'Remember me')</param>
/// <param name="configuration">Current configuration</param>
/// <returns>Nancy cookie instance</returns>
private static INancyCookie BuildCookie(Guid userIdentifier, DateTime? cookieExpiry, CustomAuthenticationConfiguration configuration)
{
var cookieContents = EncryptAndSignCookie(userIdentifier.ToString(), configuration);
var cookie = new NancyCookie(formsAuthenticationCookieName, cookieContents, true, configuration.RequiresSSL, cookieExpiry);
if (!string.IsNullOrEmpty(configuration.Domain))
{
cookie.Domain = configuration.Domain;
}
if (!string.IsNullOrEmpty(configuration.Path))
{
cookie.Path = configuration.Path;
}
return cookie;
}
/// <summary>
/// Builds a cookie for logging a user out
/// </summary>
/// <param name="configuration">Current configuration</param>
/// <returns>Nancy cookie instance</returns>
private static INancyCookie BuildLogoutCookie(CustomAuthenticationConfiguration configuration)
{
var cookie = new NancyCookie(formsAuthenticationCookieName, String.Empty, true, configuration.RequiresSSL, DateTime.Now.AddDays(-1));
if (!string.IsNullOrEmpty(configuration.Domain))
{
cookie.Domain = configuration.Domain;
}
if (!string.IsNullOrEmpty(configuration.Path))
{
cookie.Path = configuration.Path;
}
return cookie;
}
/// <summary>
/// Encrypt and sign the cookie contents
/// </summary>
/// <param name="cookieValue">Plain text cookie value</param>
/// <param name="configuration">Current configuration</param>
/// <returns>Encrypted and signed string</returns>
private static string EncryptAndSignCookie(string cookieValue, CustomAuthenticationConfiguration configuration)
{
var encryptedCookie = configuration.CryptographyConfiguration.EncryptionProvider.Encrypt(cookieValue);
var hmacBytes = GenerateHmac(encryptedCookie, configuration);
var hmacString = Convert.ToBase64String(hmacBytes);
return String.Format("{1}{0}", encryptedCookie, hmacString);
}
/// <summary>
/// Generate a hmac for the encrypted cookie string
/// </summary>
/// <param name="encryptedCookie">Encrypted cookie string</param>
/// <param name="configuration">Current configuration</param>
/// <returns>Hmac byte array</returns>
private static byte[] GenerateHmac(string encryptedCookie, CustomAuthenticationConfiguration configuration)
{
return configuration.CryptographyConfiguration.HmacProvider.GenerateHmac(encryptedCookie);
}
/// <summary>
/// Decrypt and validate an encrypted and signed cookie value
/// </summary>
/// <param name="cookieValue">Encrypted and signed cookie value</param>
/// <param name="configuration">Current configuration</param>
/// <returns>Decrypted value, or empty on error or if failed validation</returns>
public static string DecryptAndValidateAuthenticationCookie(string cookieValue, CustomAuthenticationConfiguration configuration)
{
var hmacStringLength = Base64Helpers.GetBase64Length(configuration.CryptographyConfiguration.HmacProvider.HmacLength);
var encryptedCookie = cookieValue.Substring(hmacStringLength);
var hmacString = cookieValue.Substring(0, hmacStringLength);
var encryptionProvider = configuration.CryptographyConfiguration.EncryptionProvider;
// Check the hmacs, but don't early exit if they don't match
var hmacBytes = Convert.FromBase64String(hmacString);
var newHmac = GenerateHmac(encryptedCookie, configuration);
var hmacValid = HmacComparer.Compare(newHmac, hmacBytes, configuration.CryptographyConfiguration.HmacProvider.HmacLength);
var decrypted = encryptionProvider.Decrypt(encryptedCookie);
// Only return the decrypted result if the hmac was ok
return hmacValid ? decrypted : string.Empty;
}
/// <summary>
/// Gets the redirect query string key from <see cref="FormsAuthenticationConfiguration"/>
/// </summary>
/// <param name="configuration">The forms authentication configuration.</param>
/// <returns>Redirect Querystring key</returns>
private static string GetRedirectQuerystringKey(CustomAuthenticationConfiguration configuration)
{
string redirectQuerystringKey = null;
if (configuration != null)
{
redirectQuerystringKey = configuration.RedirectQuerystringKey;
}
if (string.IsNullOrWhiteSpace(redirectQuerystringKey))
{
redirectQuerystringKey = CustomAuthenticationConfiguration.DefaultRedirectQuerystringKey;
}
return redirectQuerystringKey;
}
}
}

@ -0,0 +1,108 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: CustomModuleExtensions.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 Nancy;
using Nancy.Authentication.Forms;
using Nancy.Extensions;
namespace PlexRequests.UI.Authentication
{
public static class CustomModuleExtensions
{
/// <summary>
/// Logs the user in and returns either an empty 200 response for ajax requests, or a redirect response for non-ajax. <seealso cref="M:Nancy.Extensions.RequestExtensions.IsAjaxRequest(Nancy.Request)" />
/// </summary>
/// <param name="module">Nancy module</param>
/// <param name="userIdentifier">User identifier guid</param>
/// <param name="cookieExpiry">Optional expiry date for the cookie (for 'Remember me')</param>
/// <param name="fallbackRedirectUrl">Url to redirect to if none in the querystring</param>
/// <returns>Nancy response with redirect if request was not ajax, otherwise with OK.</returns>
public static Response Login(this INancyModule module, Guid userIdentifier, DateTime? cookieExpiry = null, string fallbackRedirectUrl = "/")
{
if (!module.Context.Request.IsAjaxRequest())
return module.LoginAndRedirect(userIdentifier, cookieExpiry, fallbackRedirectUrl);
return module.LoginWithoutRedirect(userIdentifier, cookieExpiry);
}
/// <summary>
/// Logs the user in with the given user guid and redirects.
/// </summary>
/// <param name="module">Nancy module</param>
/// <param name="userIdentifier">User identifier guid</param>
/// <param name="cookieExpiry">Optional expiry date for the cookie (for 'Remember me')</param>
/// <param name="fallbackRedirectUrl">Url to redirect to if none in the querystring</param>
/// <returns>Nancy response instance</returns>
public static Response LoginAndRedirect(this INancyModule module, Guid userIdentifier, DateTime? cookieExpiry = null, string fallbackRedirectUrl = "/")
{
return CustomAuthenticationProvider.UserLoggedInRedirectResponse(module.Context, userIdentifier, cookieExpiry, fallbackRedirectUrl);
}
/// <summary>
/// Logs the user in with the given user guid and returns ok response.
/// </summary>
/// <param name="module">Nancy module</param>
/// <param name="userIdentifier">User identifier guid</param>
/// <param name="cookieExpiry">Optional expiry date for the cookie (for 'Remember me')</param>
/// <returns>Nancy response instance</returns>
public static Response LoginWithoutRedirect(this INancyModule module, Guid userIdentifier, DateTime? cookieExpiry = null)
{
return CustomAuthenticationProvider.UserLoggedInResponse(userIdentifier, cookieExpiry);
}
/// <summary>
/// Logs the user out and returns either an empty 200 response for ajax requests, or a redirect response for non-ajax. <seealso cref="M:Nancy.Extensions.RequestExtensions.IsAjaxRequest(Nancy.Request)" />
/// </summary>
/// <param name="module">Nancy module</param>
/// <param name="redirectUrl">URL to redirect to</param>
/// <returns>Nancy response with redirect if request was not ajax, otherwise with OK.</returns>
public static Response Logout(this INancyModule module, string redirectUrl)
{
if (!module.Context.Request.IsAjaxRequest())
return CustomAuthenticationProvider.LogOutAndRedirectResponse(module.Context, redirectUrl);
return CustomAuthenticationProvider.LogOutResponse();
}
/// <summary>Logs the user out and redirects</summary>
/// <param name="module">Nancy module</param>
/// <param name="redirectUrl">URL to redirect to</param>
/// <returns>Nancy response instance</returns>
public static Response LogoutAndRedirect(this INancyModule module, string redirectUrl)
{
return CustomAuthenticationProvider.LogOutAndRedirectResponse(module.Context, redirectUrl);
}
/// <summary>Logs the user out without a redirect</summary>
/// <param name="module">Nancy module</param>
/// <returns>Nancy response instance</returns>
public static Response LogoutWithoutRedirect(this INancyModule module)
{
return CustomAuthenticationProvider.LogOutResponse();
}
}
}

@ -52,6 +52,7 @@ using PlexRequests.UI.Helpers;
using Nancy.Json;
using Ninject;
using PlexRequests.UI.Authentication;
namespace PlexRequests.UI
{
@ -92,13 +93,14 @@ namespace PlexRequests.UI
var redirect = string.IsNullOrEmpty(baseUrl) ? "~/login" : $"~/{baseUrl}/login";
// Enable forms auth
var formsAuthConfiguration = new FormsAuthenticationConfiguration
var config = new CustomAuthenticationConfiguration
{
RedirectUrl = redirect,
UserMapper = container.Get<IUserMapper>()
PlexUserRepository = container.Get<IPlexUserRepository>(),
LocalUserRepository = container.Get<IUserRepository>()
};
FormsAuthentication.Enable(pipelines, formsAuthConfiguration);
CustomAuthenticationProvider.Enable(pipelines, config);
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls;
ServicePointManager.ServerCertificateValidationCallback +=

@ -0,0 +1,27 @@
(function() {
angular.module('ngLoadingSpinner', ['angularSpinner'])
.directive('usSpinner',
[
'$http', '$rootScope', function($http, $rootScope) {
return {
link: function(scope, elm, attrs) {
$rootScope.spinnerActive = false;
scope.isLoading = function() {
return $http.pendingRequests.length > 0;
};
scope.$watch(scope.isLoading,
function(loading) {
$rootScope.spinnerActive = loading;
if (loading) {
elm.removeClass('ng-hide');
} else {
elm.addClass('ng-hide');
}
});
}
};
}
]);
}).call(this);

@ -0,0 +1,2 @@
!function(a){"use strict";function b(a,b){a.module("angularSpinner",[]).factory("usSpinnerService",["$rootScope",function(a){var b={};return b.spin=function(b){a.$broadcast("us-spinner:spin",b)},b.stop=function(b){a.$broadcast("us-spinner:stop",b)},b}]).directive("usSpinner",["$window",function(c){return{scope:!0,link:function(d,e,f){function g(){d.spinner&&d.spinner.stop()}var h=b||c.Spinner;d.spinner=null,d.key=a.isDefined(f.spinnerKey)?f.spinnerKey:!1,d.startActive=a.isDefined(f.spinnerStartActive)?f.spinnerStartActive:d.key?!1:!0,d.spin=function(){d.spinner&&d.spinner.spin(e[0])},d.stop=function(){d.startActive=!1,g()},d.$watch(f.usSpinner,function(a){g(),d.spinner=new h(a),(!d.key||d.startActive)&&d.spinner.spin(e[0])},!0),d.$on("us-spinner:spin",function(a,b){b===d.key&&d.spin()}),d.$on("us-spinner:stop",function(a,b){b===d.key&&d.stop()}),d.$on("$destroy",function(){d.stop(),d.spinner=null})}}}])}"function"==typeof define&&define.amd?define(["angular","spin"],b):b(a.angular)}(window);
//# sourceMappingURL=angular-spinner.min.js.map

@ -182,3 +182,9 @@ button.list-group-item:focus {
#sidebar-wrapper {
background: #252424; }
#cacherRunning {
background-color: #333333;
text-align: center;
font-size: 15px;
padding: 3px 0; }

@ -1 +1 @@
.form-control-custom{background-color:#333 !important;}.form-control-custom-disabled{background-color:#252424 !important;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background:#df691a;}.scroll-top-wrapper{background-color:#333;}.scroll-top-wrapper:hover{background-color:#df691a;}body{font-family:Open Sans Regular,Helvetica Neue,Helvetica,Arial,sans-serif;color:#eee;background-color:#1f1f1f;}.table-striped>tbody>tr:nth-of-type(odd){background-color:#333;}.table-hover>tbody>tr:hover{background-color:#282828;}fieldset{padding:15px;}legend{border-bottom:1px solid #333;}.form-control{color:#fefefe;background-color:#333;}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{margin-left:-0;}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:-15px;}.dropdown-menu{background-color:#282828;}.dropdown-menu .divider{background-color:#333;}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#333;}.input-group-addon{background-color:#333;}.nav>li>a:hover,.nav>li>a:focus{background-color:#df691a;}.nav-tabs>li>a:hover{border-color:#df691a #df691a transparent;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background-color:#df691a;border:1px solid #df691a;}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #df691a;}.navbar-default{background-color:#0a0a0a;}.navbar-default .navbar-brand{color:#df691a;}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#f0ad4e;background-color:#282828;}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{background-color:#282828;}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:#df691a;color:#fff;}.pagination>li>a,.pagination>li>span{background-color:#282828;}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{background-color:#333;}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#fefefe;background-color:#333;}.list-group-item{background-color:#282828;}a.list-group-item:hover,button.list-group-item:hover,a.list-group-item:focus,button.list-group-item:focus{background-color:#333;}.input-addon,.input-group-addon{color:#df691a;}.modal-header,.modal-footer{background-color:#282828;}.modal-content{position:relative;background-color:#282828;border:1px solid transparent;border-radius:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;outline:0;}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:300;color:#ebebeb;line-height:1;vertical-align:middle;white-space:nowrap;text-align:center;background-color:#333;border-radius:10px;}.bootstrap-datetimepicker-widget.dropdown-menu{background-color:#333;}.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after{border-bottom:6px solid #333 !important;}#sidebar-wrapper{background:#252424;}
.form-control-custom{background-color:#333 !important;}.form-control-custom-disabled{background-color:#252424 !important;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background:#df691a;}.scroll-top-wrapper{background-color:#333;}.scroll-top-wrapper:hover{background-color:#df691a;}body{font-family:Open Sans Regular,Helvetica Neue,Helvetica,Arial,sans-serif;color:#eee;background-color:#1f1f1f;}.table-striped>tbody>tr:nth-of-type(odd){background-color:#333;}.table-hover>tbody>tr:hover{background-color:#282828;}fieldset{padding:15px;}legend{border-bottom:1px solid #333;}.form-control{color:#fefefe;background-color:#333;}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{margin-left:-0;}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:-15px;}.dropdown-menu{background-color:#282828;}.dropdown-menu .divider{background-color:#333;}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#333;}.input-group-addon{background-color:#333;}.nav>li>a:hover,.nav>li>a:focus{background-color:#df691a;}.nav-tabs>li>a:hover{border-color:#df691a #df691a transparent;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background-color:#df691a;border:1px solid #df691a;}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #df691a;}.navbar-default{background-color:#0a0a0a;}.navbar-default .navbar-brand{color:#df691a;}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#f0ad4e;background-color:#282828;}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{background-color:#282828;}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:#df691a;color:#fff;}.pagination>li>a,.pagination>li>span{background-color:#282828;}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{background-color:#333;}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#fefefe;background-color:#333;}.list-group-item{background-color:#282828;}a.list-group-item:hover,button.list-group-item:hover,a.list-group-item:focus,button.list-group-item:focus{background-color:#333;}.input-addon,.input-group-addon{color:#df691a;}.modal-header,.modal-footer{background-color:#282828;}.modal-content{position:relative;background-color:#282828;border:1px solid transparent;border-radius:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;outline:0;}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:300;color:#ebebeb;line-height:1;vertical-align:middle;white-space:nowrap;text-align:center;background-color:#333;border-radius:10px;}.bootstrap-datetimepicker-widget.dropdown-menu{background-color:#333;}.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after{border-bottom:6px solid #333 !important;}#sidebar-wrapper{background:#252424;}#cacherRunning{background-color:#333;text-align:center;font-size:15px;padding:3px 0;}

@ -228,3 +228,10 @@ button.list-group-item:focus {
#sidebar-wrapper {
background: $bg-colour-disabled;
}
#cacherRunning {
background-color: $bg-colour;
text-align: center;
font-size: 15px;
padding: 3px 0;
}

@ -1,4 +1,8 @@
(function() {
module = angular.module('PlexRequests', []);
module = angular.module('PlexRequests', ['ngLoadingSpinner']);
module.constant("moment", moment);
//module.config(['usSpinnerConfigProvider', function (usSpinnerConfigProvider) {
// usSpinnerConfigProvider.setDefaults({ color: 'white' });
//}]);
}());

@ -0,0 +1,39 @@
<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="row">
<div>
<h3 class="col-md-5">Permissions: </h3>
<h3 class="col-md-5">Features: </h3>
</div>
<div class="col-md-5">
<div class="checkbox" ng-repeat="permission in permissions">
<input id="permission_{{$id}}" class="checkbox-custom" name="permission[]"
ng-checked="permission.selected" ng-model="permission.selected" type="checkbox" value="{{permission.value}}"/>
<label for="permission_{{$id}}">{{permission.name}}</label>
</div>
</div>
<div class="col-md-5">
<div class="checkbox" ng-repeat="f in features">
<input id="features_{{$id}}" class="checkbox-custom" name="f[]"
ng-checked="f.selected" ng-model="f.selected" type="checkbox" value="{{f.value}}"/>
<label for="features_{{$id}}">{{f.name}}</label>
</div>
</div>
</div>
<div class="row">
<input type="submit" class="btn btn-success-outline" value="Add" />
<button type="button" ng-click="redirectToSettings()" class="btn btn-primary-outline" style="float: right; margin-right: 10px;">Settings</button>
</div>
</form>

@ -0,0 +1,57 @@
 <!--Sidebar-->
<div id="sidebar-wrapper" class="shadow">
<div style="margin-left:15px">
<br />
<br />
<img ng-show="selectedUser.plexInfo.thumb" class="col-md-pull-1 img-circle" style="position: absolute" ng-src="{{selectedUser.plexInfo.thumb}}" />
<div hidden="hidden" ng-bind="selectedUser.id"></div>
<div>
<strong>Username: </strong><span ng-bind="selectedUser.username"></span>
</div>
<div ng-show="selectedUser.emailAddress">
<strong>Email Address: </strong><span ng-bind="selectedUser.emailAddress"></span>
</div>
<div>
<strong>User Type: </strong><span ng-bind="selectedUser.type === 1 ? 'Local User' : 'Plex User'"></span>
</div>
<br />
<br />
<div ng-show="selectedUser">
<!--Edit-->
<div class="row" style="margin-left: 0; margin-right: 0;">
<div class="col-md-6">
<strong>Modify Permissions:</strong>
<!--Load all permissions-->
<div class="checkbox small-checkbox" ng-repeat="p in selectedUser.permissions">
<input id="permissionsCheckbox_{{$id}}" class="checkbox-custom" name="permissions[]" ng-checked="p.selected" ng-model="p.selected" type="checkbox" value="{{p.value}}" />
<label class="small-label" for="permissionsCheckbox_{{$id}}">{{p.name}}</label>
</div>
</div>
<div class="col-md-6">
<strong>Modify Features:</strong>
<!--Load all features-->
<div class="checkbox small-checkbox" ng-repeat="p in selectedUser.features">
<input id="featuresCheckbox_{{$id}}" class="checkbox-custom" name="features[]" ng-checked="p.selected" ng-model="p.selected" type="checkbox" value="{{p.value}}" />
<label class="small-label" for="featuresCheckbox_{{$id}}">{{p.name}}</label>
</div>
</div>
</div>
<strong>Email Address</strong>
<div class="form-group">
<input id="emailAddress" type="email" ng-model="selectedUser.emailAddress" ng-disabled="selectedUser.type === 0" class="form-control form-control-custom" />
</div>
<strong>Alias</strong>
<div class="form-group">
<input id="alias" type="text" ng-model="selectedUser.alias" class="form-control form-control-custom" />
</div>
<button ng-click="updateUser()" class="btn btn-primary-outline">Update</button>
<button ng-click="deleteUser()" class="btn btn-danger-outline">Delete</button>
<button ng-click="closeSidebarClick()" style="float: right; margin-right: 10px;" class="btn btn-danger-outline">Close</button>
</div>
</div>
</div>
<!--SideBar End-->

@ -0,0 +1,86 @@

<!--Search-->
<form>
<div class="row">
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">
<i class="fa fa-search"></i>
</div>
<input type="text" class="form-control" placeholder="Search" ng-model="searchTerm">
</div>
</div>
</div>
</form>
<!-- Table -->
<table class="table table-striped table-hover table-responsive table-condensed">
<thead>
<tr>
<th>
<a href="#IDoNotExist" ng-click="sortType = 'username'; sortReverse = !sortReverse">
Username
<span ng-show="sortType == 'username' && !sortReverse" class="fa fa-caret-down"></span>
<span ng-show="sortType == 'username' && sortReverse" class="fa fa-caret-up"></span>
</a>
</th>
<th>
<a href="#IDoNotExist" ng-click="sortType = 'alias'; sortReverse = !sortReverse">
Alias
<span ng-show="sortType == 'alias' && !sortReverse" class="fa fa-caret-down"></span>
<span ng-show="sortType == 'alias' && sortReverse" class="fa fa-caret-up"></span>
</a>
</th>
<th>
<a href="#IDoNotExist" ng-click="sortType = 'emailAddress'; sortReverse = !sortReverse">
Email
<span ng-show="sortType == 'emailAddress' && !sortReverse" class="fa fa-caret-down"></span>
<span ng-show="sortType == 'emailAddress' && sortReverse" class="fa fa-caret-up"></span>
</a>
</th>
<th>
Permissions
</th>
<th ng-hide="hideColumns">
<a href="#IDoNotExist" ng-click="sortType = 'type'; sortReverse = !sortReverse">
User Type
<span ng-show="sortType == 'type' && !sortReverse" class="fa fa-caret-down"></span>
<span ng-show="sortType == 'type' && sortReverse" class="fa fa-caret-up"></span>
</a>
</th>
<th ng-hide="hideColumns">
<a href="#IDoNotExist" ng-click="sortType = 'lastLoggedIn'; sortReverse = !sortReverse">
Last Logged In
<span ng-show="sortType == 'lastLoggedIn' && !sortReverse" class="fa fa-caret-down"></span>
<span ng-show="sortType == 'lastLoggedIn' && sortReverse" class="fa fa-caret-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="u in users | orderBy:sortType:sortReverse | filter:searchTerm">
<td>
{{u.username}}
</td>
<td>
{{u.alias}}
</td>
<td>
{{u.emailAddress}}
</td>
<td>
{{u.permissionsFormattedString}}
</td>
<td ng-hide="hideColumns">
{{u.type === 1 ? 'Local User' : 'Plex User'}}
</td>
<td ng-hide="hideColumns" ng-bind="u.lastLoggedIn === minDate ? 'Never' : formatDate(u.lastLoggedIn)"></td>
<td>
<a href="#IDontExist" ng-click="selectUser(u.id)" class="btn btn-sm btn-info-outline">Details/Edit</a>
</td>
</tr>
</tbody>
</table>

@ -0,0 +1,25 @@
(function () {
module.directive('tableComponent',
function () {
return {
templateUrl: createBaseUrl(getBaseUrl(), 'Content/app/userManagement/Directives/table.html')
};
})
.directive('addUser',
function () {
return {
templateUrl: createBaseUrl(getBaseUrl(), 'Content/app/userManagement/Directives/addUser.html')
};
})
.directive('sidebar',
function () {
return {
templateUrl: createBaseUrl(getBaseUrl(), 'Content/app/userManagement/Directives/sidebar.html')
};
});
function getBaseUrl() {
return $('#baseUrl').text();
}
})();

@ -19,12 +19,9 @@
$scope.sortReverse = false;
$scope.searchTerm = "";
$scope.hideColumns = false;
$scope.error = {
error: false,
errorMessage: ""
};
var ReadOnlyPermission = "Read Only User";
var open = false;
// Select a user to populate on the right side
@ -34,10 +31,7 @@
});
$scope.selectedUser = user[0];
if (!open) {
$("#wrapper").toggleClass("toggled");
open = true;
}
openSidebar();
}
// Get all users in the system
@ -63,40 +57,40 @@
// 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');
generateNotify("Please provide a username and password", 'warning');
return;
}
if ($scope.selectedPermissions.length === 0) {
$scope.error.error = true;
$scope.error.errorMessage = "Please select a permission";
generateNotify($scope.error.errorMessage, 'warning');
generateNotify("Please select a permission", 'warning');
return;
}
var hasReadOnly = $scope.selectedPermissions.indexOf(ReadOnlyPermission) !== -1;
if (hasReadOnly) {
if ($scope.selectedPermissions.length > 1) {
generateNotify("Cannot have the " + ReadOnlyPermission + " permission with other permissions.", 'danger');
return;
}
}
var existingUsername = $scope.users.some(function (u) {
return u.username === $scope.user.username;
});
if (existingUsername) {
return generateNotify("A user with the username " + $scope.user.username + " already exists!", 'danger');
}
userManagementService.addUser($scope.user, $scope.selectedPermissions, $scope.selectedFeatures)
.then(function (data) {
if (data.message) {
$scope.error.error = true;
$scope.error.errorMessage = data.message;
generateNotify(data.message, 'warning');
} else {
$scope.users.push(data.data); // Push the new user into the array to update the DOM
$scope.user = {};
$scope.selectedPermissions = {}; // Clear the checkboxes
$scope.selectedFeatures = {};
$scope.features.forEach(function (entry) {
entry.selected = false;
});
$scope.permissions.forEach(function (entry) {
entry.selected = false;
});
}
clearCheckboxes();
};
});
};
@ -120,34 +114,31 @@
$scope.updateUser = function () {
var u = $scope.selectedUser;
userManagementService.updateUser(u.id, u.permissions, u.alias, u.emailAddress)
.then(function (data) {
if (data) {
$scope.selectedUser = data;
if (open) {
open = false;
$("#wrapper").toggleClass("toggled");
userManagementService.updateUser(u.id, u.permissions, u.features, u.alias, u.emailAddress)
.then(function success(data) {
if (data.data) {
$scope.selectedUser = data.data;
closeSidebar();
return successCallback("Updated User", "success");
}
return successCallback("Updated User", "success");
}
});
}, function errorCallback(response) {
successCallback(response, "danger");
});
}
$scope.deleteUser = function () {
var u = $scope.selectedUser;
var result = userManagementService.deleteUser(u.id);
result.success(function (data) {
if (data.result) {
removeUser(u.id, true);
return successCallback("Deleted User", "success");
}
});
}
function getBaseUrl() {
return $('#baseUrl').val();
userManagementService.deleteUser(u.id)
.then(function sucess(data) {
if (data.data.result) {
removeUser(u.id, true);
closeSidebar();
return successCallback("Deleted User", "success");
}
}, function errorCallback(response) {
successCallback(response, "danger");
});
}
$scope.formatDate = function (utcDate) {
@ -162,6 +153,15 @@
return;
}
$scope.closeSidebarClick = function () {
return closeSidebar();
}
$scope.redirectToSettings = function() {
var url = createBaseUrl(getBaseUrl(), '/admin/usermanagementsettings');
window.location.href = url;
}
function removeUser(id, current) {
$scope.users = $scope.users.filter(function (user) {
return user.id !== id;
@ -170,12 +170,44 @@
$scope.selectedUser = null;
}
}
function closeSidebar() {
if (open) {
open = false;
$("#wrapper").toggleClass("toggled");
$scope.hideColumns = false;
}
}
function openSidebar() {
if (!open) {
$("#wrapper").toggleClass("toggled");
open = true;
$scope.hideColumns = true;
}
}
function clearCheckboxes() {
$scope.selectedPermissions = {}; // Clear the checkboxes
$scope.selectedFeatures = {};
$scope.features.forEach(function (entry) {
entry.selected = false;
});
$scope.permissions.forEach(function (entry) {
entry.selected = false;
});
}
function getBaseUrl() {
return $('#baseUrl').text();
}
}
function successCallback(message, type) {
generateNotify(message, type);
};
angular.module('PlexRequests').controller('userManagementController', ["$scope", "userManagementService", "moment", controller]);
}());

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save